Android 圖片載入之Glide快取策略
由於圖片載入是應用開發中非常常見,但是有非常容易消耗資源甚至出現問題的場景,因此出現了很多第三方圖片載入框架,從最早的ImageLoader(Universal ImageLoader),再到Facebook推出的Fresco,還有新興的Picasso和Glide。當然,這些框架的使用場景一般都有很大的重合,所以一般我們只選擇一個進行使用,而本人使用最多的是Google所推薦的Glide,它使用簡單,支援GIF等,而且快取效果得到很多人的肯定,也因此特意去了解了一下Glide的快取機制。至於他們的優劣點,也有很多人進行比較,但是這不是這篇文章的重點。
(這裡說明一下,目前針對Glide 4.4版本進行了解)
1.Glide 的快取機制簡介
2.Glide 的請求流程
3.常見的壓縮方式
4.動態URL與Glide快取機制衝突問題
通過這4個問題,我們進行簡單的介紹,最基礎的使用就不進行說明了,而且內容多數來自參考部落格的大佬們分析,個人進行自我理解式的整理簡化,所以就儘量不貼原始碼了,涉及原始碼不清楚了可以進入相關部落格進行詳細理解或者自己下載原始碼進行閱讀。Glide最常用就是載入圖片,下文以載入圖片為例進行說明。
1.Glide 的快取機制簡介
Glide採取的多級快取機制,能夠較為友好地實現圖片、動圖的載入。其主要有 記憶體快取+硬碟快取 ,當然他們的作用也有不同,其中 記憶體快取主要用於防止將重複的圖讀入記憶體中,硬碟快取則用於防止從網路或者其他地方將重複下載和資料讀取 。由此可見,以APP應用範圍來看,記憶體快取主要針對內在處理,硬碟快取主要針對對外管理,也由二者結合才構成Glide的主要快取機制基礎。預設情況下,記憶體快取和硬體快取,Glide都是開啟的,當然官方也提供了二者的開關設定:skipMemoryCache()方法並傳入true,就表示禁用掉Glide的記憶體快取功能;呼叫diskCacheStrategy()方法並傳入DiskCacheStrategy.NONE,就可以禁用掉Glide的硬碟快取功能了(這裡並不是boolean值型別,因為Glide提供了四種列舉型別,在1.3硬碟快取中會進行說明)。
1.1 Key的生成
上文說了,快取機制中最主要的問題之一就是避免重複載入,那要作為避免重複就需要有依據,這個依據就是 快取Key,它是每個圖的多項資訊經過Glide內部演算法生成的(其中一個重要引數資訊就是圖片的URL,這也造成動態URL的時候會發生重複載入的問題,這我們會在第4部分進行說明)。
在原始碼裡檢視可以知道,每一次的load方法內部,都會有一個fetcher.getId()方法獲得了一個id字串,這個字串也就是我們要載入的圖片的唯一標識,比如說 如果是一張網路上的圖片的話,那麼這個id就是這張圖片的url地址。 然後,這個id會結合signature、width、height等等10個引數一起傳入到EngineKeyFactory的buildKey()方法當中,從而構建出了一個EngineKey物件,這個EngineKey也就是Glide中的快取Key了。
因此,如果你圖片的width或者height發生改變,也會生成一個完全不同的快取Key。
(這裡補充一下:4.4以前是Bitmap複用必須長寬相等才可以複用,而4.4及以後是Size>=所需就可以複用,只 不過需要呼叫reconfigure來調整尺寸 )
1.2 記憶體快取
我們知道,當Glide載入完某個圖片後,就會將它放入記憶體快取中,在它被記憶體快取回收之前,再呼叫就可以直接從記憶體快取裡直接獲取。當然,這是最基本的運用場景,其記憶體快取機制裡所依據的演算法是 LRUCache演算法(Last Recently Used:近期最少使用),Google有提供對應的演算法工具類DiskLruCache,但是Glide是使用的自己編寫的DiskLruCache工具類,不過可以知道的是二者演算法原理是一致的。而且,為了達到更好的效果,還採取了弱引用機制,結合LRUCache,二者奠定了記憶體快取的總體基礎。
如上所述,Glide將記憶體快取中劃分成兩個區域:LruResourceCache(某些人稱為 圖片池,就是Glide實現記憶體快取所使用的LruCache物件了)+activeResources(某些人稱為物件池,即正在使用的圖片管理處)。由此可知,前者使用的是LRU演算法進行管理的快取工具,而activeResources就是使用了弱引用機制(採取了HashMap進行弱引用進行儲存)。
在這裡說明一下,圖片請求開啟,Glide檢查是否開啟記憶體快取後,如果開啟了,則先去LruResourceCache中查詢是否符合的圖片,如果找到了,我們從LruResourceCache中獲取到快取圖片之後會將它從快取中移除,然後將這個快取圖片儲存到activeResources當中,以保護這些圖片不會被LruCache演算法回收掉。如果在LruResourceCache中沒有找到,再去activeResources中進行查詢。如果二者均找不到符合資源,再開啟子執行緒進行載入圖片資源。由此可知,在Glide的記憶體快取機制中,LruResourceCache的優先順序在activeResources之前。
另外,在Glide記憶體快取中,通過EngineResource作為圖片的管理物件,裡面有一個引數變數acquired用來記錄圖片被引用的次數,呼叫acquire()方法會讓變數加1,呼叫release()方法會讓變數減1。當acquired變數大於0的時候,說明圖片正在使用中,也就應該放到activeResources弱引用快取當中。而經過release()之後,如果acquired變數等於0了,說明圖片已經不再被使用了,此時會進行內部回收。這裡的內部回收過程是這樣的:首先會將快取圖片從activeResources中移除,然後再將它put到LruResourceCache當中。這樣也就實現了正在使用中的圖片使用弱引用來進行快取,不在使用中的圖片使用LruCache來進行快取的功能。
1.3 硬碟快取
呼叫diskCacheStrategy()方法並傳入DiskCacheStrategy.NONE,就可以禁用掉Glide的硬碟快取功能了。這個diskCacheStrategy()方法基本上就是Glide硬碟快取功能的一切,它可以接收四種引數:
DiskCacheStrategy.NONE: 表示不快取任何內容。
DiskCacheStrategy.SOURCE: 表示只快取原始圖片。
DiskCacheStrategy.RESULT: 表示只快取轉換過後的圖片(預設選項)。
DiskCacheStrategy.ALL : 表示既快取原始圖片,也快取轉換過後的圖片。
這邊提到了原始圖片和快取圖片的概念,這裡我們需要知道,大多數情況下,Glide會對請求的原始圖片進行壓縮和轉換,再進行展示。其中Glide預設情況下在硬碟快取的就是轉換過後的圖片,我們通過呼叫diskCacheStrategy()方法則可以改變這一預設行為。這樣的機制能夠比較有效地規避解決 大圖和超大圖帶來的OOM問題+大圖小框的資源浪費問題。至於壓縮和轉換,Glide主要採用尺寸壓縮演算法,更多的內容我們會在下文第3部分進行說明。
我們先說說硬碟快取的資源管理機制,其實Glide在這裡也是採用LRU演算法進行管理,這裡就不對Glide的DiskLRUCache工具類進行解析了,有興趣的可以自行搜尋。上頭說到,Glide在記憶體快取中沒有找到對應的圖片資源後,便會開啟新的子執行緒進行圖片載入,此時會執行EngineRunnable的run()方法,run()方法中又會呼叫一個decode()方法:

decode原始碼圖
可知,Glide會先在硬碟快取中進行搜尋,當硬碟快取存在後直接讀取快取的圖片(decodeFromCache方法),當硬碟快取中不存在符合資源時候,再呼叫decodeFromSource()來讀取原始圖片。而在硬碟快取讀取中(decodeFromCache方法)先去呼叫DecodeJob的decodeResultFromCache()方法來獲取快取,如果獲取不到,會再呼叫decodeSourceFromCache()方法獲取快取,這兩個方法的區別其實就是DiskCacheStrategy.RESULT和DiskCacheStrategy.SOURCE這兩個引數的區別,而這兩個指的就是硬碟快取是採取“轉換後的圖片快取”、“原始圖片快取”的選擇。
那什麼時候寫入硬碟快取?簡單說一下,預設下是在網路請求圖片,進行轉化後,寫入硬碟快取的。詳細的就不說了,感興趣可以去看參考部落格或者原始碼。
2.Glide 的請求流程
這裡,我們從最直接的使用角度進行解釋:
1. Glide.with(context) 建立RequestManager
RequestManager負責管理當前context的所有Request
Context可以傳Fragment、Activity或者其他Context,當傳Fragment、Activity時,當前頁面對應的Activity的生命週期可以被RequestManager監控到,從而可以控制Request的pause、resume、clear。 這其中採用的監控方法就是在當前activity中新增一個沒有view的fragment,這樣在activity發生onStart onStop onDestroy的時候,會觸發此fragment的onStart onStop onDestroy 。
RequestManager用來跟蹤眾多當前頁面的Request的是RequestTracker類, 用弱引用來儲存執行中的Request , 用強引用來儲存暫停需要恢復的Request 。
2. Glide.with(context). load(url)建立需要的Request
通常是DrawableTypeRequest,後面可以新增transform、fitCenter、animate、placeholder、error、override、skipMemoryCache、signature等等
如果需要進行Resource的轉化比如轉化為Byte陣列等需要,可以加asBitmap來更改為BitmapTypeRequest
Request是Glide載入圖片的執行單位
3. Glide.with(context).load(url).into(imageview)
在Request的into方法中會呼叫Request的begin方法開始執行
在正式生成EngineJob放入Engine中 執行之前,如果並沒有事先呼叫override(width, height)來指定所需要寬高,Glide則會嘗試去獲取imageview的寬和高,如果當前imageview並沒有初始化完畢取不到高寬,Glide會通過view的ViewTreeObserver來等View初始化完畢之後再獲取寬高再進行下一步 。
這裡我們再著重講一下,資源載入的相關知識:
GlideBuilder在初始化Glide時,會生成一個執行機Engine
Engine中包含LruCache快取及一個當前正在使用的active資源Cache(弱引用)
activeCache輔助LruCache,當Resource從LruCache中取出使用時,會從LruCache中remove並進入acticeCache當中
Cache優先順序LruCache>activeCache
Engine在初始化時要傳入兩個ExecutorService,即會有兩個執行緒池,一個用來從DiskCache獲取resource,另一個用來從Source中獲取(通常是下載)
執行緒的封裝單位是EngineJob,有兩個順序狀態,先是CacheState,在此狀態先進入DiskCacheService中執行獲取,如果沒找到則進入SourceState,進到SourceService中執行下載
3.常見的壓縮方式
Android中圖片是以bitmap形式存在的,那麼bitmap所佔記憶體,直接影響到了應用所佔記憶體大小,首先要知道計算方式:
bitmap所佔記憶體大小 = 圖片長度 * 圖片寬度 * 一個畫素點佔用的位元組數(單位尺寸的畫素密度一般是手機解析度所決定的)
因此我們常見的壓縮方式就是兩種方式:1.將圖片的w、h進行壓縮;2.降低畫素點佔用的位元組數。而我們最常見的是尺寸壓縮,質量壓縮和格式壓縮,這裡我們簡單解釋一下它們。
3.1 尺寸壓縮&取樣率壓縮
說實話,個人感覺這兩個壓縮概念上雖然有差異,但是實際使用中原理是相通的,或者說二者幾乎都是一起使用的。
尺寸壓縮就是將圖片的大小(w、h進行縮小),從而保持單位尺寸上的畫素密度不變,由此畫素點的減少,使得圖片大小得到減少;
取樣率壓縮通過設定取樣率,單位尺寸上減少畫素密度(比如讀取圖片時候,並不讀取所有的畫素點,只讀取部分畫素點),造成單位尺寸上的畫素密度降低, 達到對記憶體中的Bitmap進行壓縮,或者說就是按照一定的倍數對圖片減少單位尺寸的畫素值。其 原理為: 通過減少單位尺寸的畫素值,真正意義上的降低畫素值。但是一般為了保持圖片不失真,會結合尺寸的壓縮。
常見的使用場景:快取縮圖 (頭像的處理)。
通常通過BitmapFactory中的decodeFile方法對圖片進行尺寸壓縮,其中一個引數opts 就是所謂的取樣率,它裡邊有很多屬性可以設定,我們通過設定屬性來達到根據自己的需要,壓縮出指定的圖片。
為了避免OOM異常,最好在解析每張圖片的時候都先檢查一下圖片的大小,除非你非常信任圖片的來源,保證這些圖片都不會超出你程式的可用記憶體。
現在圖片的大小已經知道了,我們就可以決定是把整張圖片載入到記憶體中還是載入一個壓縮版的圖片到記憶體中。以下幾個因素是我們需要考慮的:
1. 預估一下載入整張圖片所需佔用的記憶體。
2. 為了載入這一張圖片你所願意提供多少記憶體。
3. 用於展示這張圖片的控制元件的實際大小。
4. 當前裝置的螢幕尺寸和解析度。
比如,你的ImageView只有128*96畫素的大小,只是為了顯示一張縮圖,這時候把一張1024*768畫素的圖片完全載入到記憶體中顯然是不值得的。
那我們怎樣才能對圖片進行壓縮呢?通過設定BitmapFactory.Options中inSampleSize的值就可以實現。比如我們有一張2048*1536畫素的圖片,將inSampleSize的值設定為4,就可以把這張圖片壓縮成512*384畫素。原本載入這張圖片需要佔用13M的記憶體,壓縮後就只需要佔用0.75M了(假設圖片是ARGB_8888型別,即每個畫素點佔用4個位元組)。下面的方法可以根據傳入的寬和高,計算出合適的inSampleSize值
3.2 質量壓縮
顧名思義就是降低圖片質量(大小)。原理 :通過演算法扣掉(同化)了 圖片中的一些某個點附近相近的畫素,達到降低質量減少檔案大小的目的。
注意 : 通過上述我們知道,它並沒有改變w、h,也沒有單個畫素佔據的位元組數。所以只能實現對 file 的影響,對載入這個圖片出來的bitmap 記憶體是無法節省的,因為質量壓縮不會減少圖片的畫素,它是在保持畫素的前提下改變圖片的位深及透明度等,來達到壓縮圖片的目的。
使用場景 :將圖片儲存到本地 ,或者將圖片上傳 到伺服器 ,根據實際需求來 。
質量壓縮主要藉助Bitmap中的compress方法實現:
public boolean compress (Bitmap.CompressFormat format, int quality, OutputStream stream)
這個方法用來將特定格式的壓縮圖片寫入輸出流(OutputStream)中,當然例如輸出流與檔案聯絡在一起,壓縮後的圖片也就是一個檔案。如果壓縮成功則返回true,其中有三個引數:
format是壓縮後的圖片的格式,可取值:Bitmap.CompressFormat .JPEG、~.PNG、~.WEBP。
quality的取值範圍為[0,100],值越小,經過壓縮後圖片失真越嚴重,當然圖片檔案也會越小。(PNG格式的圖片會忽略這個值的設定)
stream指定壓縮的圖片輸出的地方,比如某檔案。
上述方法還有一個值得注意的地方是:當用BitmapFactory decode檔案時可能返回一個跟原圖片不同位深的圖片,或者丟失了每個畫素的透明值(alpha),比如說,JPEG格式的圖片僅僅支援不透明的畫素。
3.3 RGB_565壓縮
這裡先說明一下點陣圖的一些知識點:
ARGB_8888: 表示32位ARGB點陣圖,即A=8,R=8,G=8,B=8,一個畫素點佔8+8+8+8=32位,4個位元組;
RGB_565: 表示16位RGB點陣圖,即R=5,G=6,B=5, 它沒有透明度 ,一個畫素點佔5+6+5=16位,2個位元組;
其實就是犧牲圖片的透明度(此時一個畫素點佔用的位元組數得到降低),這樣壓縮出來的圖片比 ARGB_8888能達到(並不一定是)一半的記憶體開銷。個人 感覺(如果有誤)通過將圖片進行格式轉化——格式壓縮,通過的主要也是這種方法,尤其JPEG格式的圖片僅僅支援不透明的畫素,png格式的圖片幾乎不改變圖片大小。另外,常見的jpeg和png都有失真壓縮,若非必要,儘量別來回壓縮。影象壓縮很費時間和記憶體的,特別是時間。
使用場景 :無需在意圖片的透明度時候。
其實,網上很多都有具體的程式碼示例,但是多數都是呼叫已有的API進行說明,目前看到的解釋都比較亂一點,活著說本人現在理解的也有點亂。暫時能夠歸置出這一點點壓縮的知識,本人將十萬分地希望和感謝有小夥伴能夠指正或者指點!!!
4.動態URL與Glide快取機制衝突問題
這個問題我直接把參看部落格截圖放上來把:

動態URL問題
上頭我們說過了,Glide對於一個圖片會通過圖片資源的多項資訊,通過本身演算法得到一個key,這個key就是Glide作為對圖片資源的唯一性標識,其中一個引數就是圖片的URL。由此可見,當我們的URL發生變化的時候,對應的key也會發生變化。這樣子就會造成上圖所述的問題。

key生成原始碼截圖
由上圖,我們通過一層層剝進去,在fetcher.getId()裡面,是通過glideUrl.getCacheKey()獲取—》在glideUrl.getCacheKey()裡面,是return stringUrl != null ? stringUrl : url.toString(); 好了,由此我們找到切入點了。我們需要在GlideUrl類中修改對應的getCacheKey方法。由於我們一般不會將原始碼下載到本地,所以無法直接修改Glide的原始碼。因此我們可以新建一個繼承於GlideUrl的子類——MyGlideUrl類,在MyGlideUrl裡重寫getCacheKey方法,最後在Glide請求裡面採用MyGlideUrl,如下:
Glide.with(this)
.load(new MyGlideUrl(url))
.into(imageView);
綜上,這就是本篇所有的知識點。總的來說,Glide的快取實現非常複雜,但是基礎的原理到還好。通過許多部落格的參考和官方文件的查閱,整理出來,如有不足或者錯誤,歡迎指正!
參考博文:
---------------------
作者:guolin 來源:CSDN 題目:Android圖片載入框架最全解析(三),深入探究Glide的快取機制
原文: ofollow,noindex">https://blog.csdn.net/guolin_blog/article/details/54895665
---------------------
作者:蔡睿智 來源:CSDN 題目:Glide載入圖片原理----轉載
原文: https://blog.csdn.net/ss8860524/article/details/50668118
---------------------
作者:Linhaojian來源:簡書 題目: 理解Android虛擬機器體系結構
原文: https://www.jianshu.com/p/e907eca48334
---------------------