下圖是一個(gè)客戶端圖片加載模塊常見的處理流程。

本文以UniversalImageLoader為例分析了這一流程,然后分析了Fresco的優(yōu)勢和問題,最終推薦大家使用Glide。
從UniversalImageLoader分析圖片加載中需要處理的問題
網(wǎng)絡(luò)
主要用于下載網(wǎng)絡(luò)圖片,在UIL中是將圖片地址變?yōu)镮nputStream。UIL支持多種類型來源的圖片顯示,包括:
- 網(wǎng)絡(luò)
- 文件
- Uri資源(如果是視頻,會找到縮略圖顯示)
- assets
- drawable
緩存
- UIL使用兩級緩存:磁盤緩存圖片文件、內(nèi)存緩存Bitmap
- UIL已經(jīng)實(shí)現(xiàn)了多種緩存策略,但一般都使用LRU緩存
- UIL可以指定圖片緩存的路徑,和緩存文件名的生成規(guī)則
- 需要自己確定緩存的大小,確定內(nèi)存緩存的大小尤其重要
- 通過
ActivityManager#getMemoryClass或Runtime.getRuntime().maxMemory()獲得單個(gè)應(yīng)用的最大內(nèi)存(前者單位為MB,后者單位為B),一般最多劃分1/4的最大內(nèi)存,否則容易導(dǎo)致OOM。 - 圖片較多,大圖較多的應(yīng)用,需要使用較大的緩存,提高緩存的命中率
- 內(nèi)存也不宜太小,最少應(yīng)該能緩存2~3個(gè)屏幕大小的Bitmap
- 通過
解碼
- 要按需解碼,否則會造成內(nèi)存的浪費(fèi),主要通過
options.inJustDecodeBounds進(jìn)行預(yù)解析 - ImageAware可以幫助控制解碼圖片的大小,ImageViewAware就是對ImageView的一個(gè)封裝
- 注意照片方向Exif,避免圖片錯(cuò)誤旋轉(zhuǎn)
顯示
實(shí)現(xiàn)接口BitmapDisplayer可以自定義顯示效果,已實(shí)現(xiàn)的包括:帶描邊的圓形、漸入、圓角矩形(不能四個(gè)角分別指定)等。
某些設(shè)計(jì)可能會出現(xiàn)兩個(gè)角圓角、另外兩個(gè)直角的特殊裁剪模式。自己實(shí)現(xiàn)這類Displayer時(shí),不要生成一個(gè)新的Bitmap,定義一個(gè)Drawable會更高效。因?yàn)樯尚碌腂itmap會引起內(nèi)存分配和回收,從而使GC更加頻繁,而Drawable只是在繪制時(shí)會使用很少的計(jì)算資源??梢詤⒖荚创a中RoundedBitmapDisplayer。
多線程
圖片的下載和解碼都需要再后臺線程中處理,而且為了提高效率,一般都使用多個(gè)線程分別進(jìn)行解碼和網(wǎng)絡(luò)請求。
共有三個(gè)Executor,分別用于
- 分發(fā)任務(wù)
- 處理已緩存圖片
- 處理未緩存圖片
已緩存的圖片主要占用計(jì)算資源,未緩存的圖片則主要占用網(wǎng)絡(luò)資源,所以不應(yīng)該在一個(gè)Executor中競爭??梢苑謩e指定Executor的線程數(shù)量,UIL默認(rèn)為3個(gè)。
監(jiān)聽
UIL向外提供了兩類監(jiān)聽:ImageLoadingListener和ImageLoadingProgressListener
PauseOnScrollListener主要用于,在列表滾動時(shí)暫停圖片加載,但在現(xiàn)在的RecyclerView中無法使用。需要自己使用ImageLoader#pause和ImageLoader#resume
了解了UIL是如何處理圖片加載的問題之后,其他的第三方庫也都是大同小異,下面再介紹下Fresco和Glide。
Fresco的優(yōu)勢和問題
Fresco在解決圖片加載問題上的思路和其他框架有很大的不同。它最大的問題有兩個(gè):
- 不能直接使用ImageView:這個(gè)問題對于老項(xiàng)目的重構(gòu)幾乎是致命的。
- 需要指定寬高:指定寬高對于圖片加載其實(shí)是一件好事,但在加載網(wǎng)絡(luò)圖片的時(shí)候需要服務(wù)端告知原圖的尺寸,才能幫助客戶端實(shí)現(xiàn)更好的體驗(yàn)。
相比UIL,它的優(yōu)點(diǎn)主要包括:
- 5.0以下的系統(tǒng)上,使用ASHMEM,不會占用Java堆內(nèi)容
- 多一級未解碼圖片的內(nèi)存緩存,減少文件IO(很多Android機(jī)器使用1年之后變慢,很大的原因就是IO變慢了,所以這一優(yōu)化的效果還是很顯著的)
- 支持多圖請求,可以在大圖顯示之前展現(xiàn)縮略圖
- 支持Gif動畫和漸進(jìn)式JPEG(圖片還未下載完成的時(shí)候就可以先顯示一部分)
更多關(guān)于Fresco的特點(diǎn)可以參考Android圖片加載開源庫深度推薦,安利Fresco
緩存和網(wǎng)絡(luò):Image Pipeline
在5.0系統(tǒng)以下,Image Pipeline 使用 pinned purgeables 將Bitmap數(shù)據(jù)避開Java堆內(nèi)存,存在ASHMEM中。這要求圖片不使用時(shí),要顯式地釋放內(nèi)存。SimpleDraweeView自動處理了這個(gè)釋放過程,所以沒有特殊情況,盡量使用SimpleDraweeView。
顯示
修改圖片的顯示效果需要使用DraweeHolder,它不僅能控制圖片的顯示,還可以處理View的Touch事件。默認(rèn)支持圓角和圓形。
另一個(gè)控制圖片顯示的方法是在Postprocessor中修改Bitmap,但效率很低。
監(jiān)聽
- ControllerListener:監(jiān)聽圖片顯示的過程
- RequestListener:監(jiān)聽圖片獲取的過程
Glide是個(gè)不錯(cuò)的選擇
優(yōu)點(diǎn)
- 支持本地Video
- 分別控制每次請求的優(yōu)先級
- 支持縮略圖
- 可直接更新AppWidget和Notification中的圖片
- 自定義圖片轉(zhuǎn)換效果,還可使用GPU轉(zhuǎn)換(使用GPU處理圖片變換,并保持到緩存文件中,在Demo中可以看到GPU變換的10種特效)
- 定義加載動畫
- 很方便的使用圖片裁剪服務(wù)
- 使用Bitmap回收池,減少系統(tǒng)gc
可參考Glide相關(guān)文章了解怎么通過自定義的GlideModule優(yōu)化加載的圖片。本文也提供給了參考代碼,在代碼中引入七牛裁圖服務(wù),同時(shí)也可體驗(yàn)10種GPU變化的特效。
關(guān)于Transformation和BitmapImageViewTarget的使用
-
Transformation#transform:在緩存前對圖片進(jìn)行處理,處理之后的圖片才會進(jìn)行緩存。處理圖片的過程中會產(chǎn)生額外的內(nèi)存消耗,處理后的圖片會占據(jù)獨(dú)立的緩存空間。但第二次使用的時(shí)候,不再需要處理,直接從緩存中讀取。 -
BitmapImageViewTarget#setResource:控制圖片的顯示邏輯,每次顯示的時(shí)候都會處理。因此在setResource中應(yīng)該定義特殊的Drawable來控制顯示效果,而不應(yīng)該對Bitmap進(jìn)行處理。(Bitmap的頻繁生成和回收會導(dǎo)致gc;Drawable是在繪制的時(shí)候,通過Paint設(shè)置特殊的繪制效果,不會產(chǎn)生新的Bitmap)
關(guān)于Bitmap的回收機(jī)制
系統(tǒng)的Bitmap內(nèi)存管理機(jī)制隨著Android系統(tǒng)的演進(jìn)可以分為三個(gè)階段,關(guān)于這部分可以參考官網(wǎng)文章Managing Bitmap Memory。
- 2.3.3及以下:Bitmap的像素?cái)?shù)據(jù)存儲在native內(nèi)存中,但依舊會計(jì)算在一個(gè)進(jìn)程的內(nèi)存上限之中。
- 3.0~4.4:Bitmap的像素?cái)?shù)據(jù)存儲在Java堆內(nèi)存中,解碼Bitmap時(shí)可以通過
Options#inBitmap復(fù)用不再使用的Bitmap,從而減少系統(tǒng)gc。但要求被復(fù)用的Bitmap和新Bitmap的像素?cái)?shù)據(jù)一樣大。 - 5.0及以上:對于復(fù)用Bitmap的限制不再嚴(yán)格要求一樣大,只要?jiǎng)e復(fù)用的Bitmap的像素?cái)?shù)據(jù)不小于新Bitmap即可。
如有興趣,可參考筆者的另一篇文章了解《Glide如何通過引用計(jì)數(shù)復(fù)用Bitmap》
Android系統(tǒng)使用的內(nèi)存除了native heap和Java heap以外,還有ASHMEM。在5.0之前可以通過Options#inPurgeable將Bitmap的數(shù)據(jù)存儲在ASHMEM中,從而不占據(jù)一個(gè)進(jìn)程的內(nèi)存上限。
BitmapFactory.Options = new BitmapFactory.Options();
options.inPurgeable = true;
Bitmap bitmap = BitmapFactory.decodeByteArray(jpeg, 0, jpeg.length, options);
關(guān)于Android系統(tǒng)中三種內(nèi)存的和Bitmap的關(guān)系可以參考Introducing Fresco: A new image library for Android