此次來面試的是一個有着5年工做經驗的小夥,截取了一段對話以下:java
面試官:我看你寫到Glide,爲何用Glide,而不選擇其它圖片加載框架?
小夥:Glide 使用簡單,鏈式調用,很方便,一直用這個。
面試官:有看過它的源碼嗎?跟其它圖片框架相比有哪些優點?
小夥:沒有,只是在項目中使用而已~
面試官:假如如今不讓你用開源庫,須要你本身寫一個圖片加載框架,你會考慮哪些方面的問題,說說大概的思路。
小夥:額~,壓縮吧。
面試官:還有嗎?
小夥:額~,這個沒寫過。android
說到圖片加載框架,你們最熟悉的莫過於Glide了,但我卻不推薦簡歷上寫熟悉Glide,除非你熟讀它的源碼,或者參與Glide的開發和維護。c++
在通常面試中,遇到圖片加載問題的頻率通常不會過低,只是問法會有一些差別,例如:git
帶着問題進入正文~github
Glide因爲其口碑好,不少開發者直接在項目中使用,使用方法至關簡單面試
一、添加依賴:數組
implementation 'com.github.bumptech.glide:glide:4.10.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.10.0'
複製代碼
二、添加網絡權限緩存
<uses-permission android:name="android.permission.INTERNET" />
複製代碼
三、一句代碼加載圖片到ImageViewbash
Glide.with(this).load(imgUrl).into(mIv1);
複製代碼
進階一點的用法,參數設置
RequestOptions options = new RequestOptions()
.placeholder(R.drawable.ic_launcher_background)
.error(R.mipmap.ic_launcher)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.override(200, 100);
Glide.with(this)
.load(imgUrl)
.apply(options)
.into(mIv2);
複製代碼
使用Glide加載圖片如此簡單,這讓不少開發者省下本身處理圖片的時間,圖片加載工做所有交給Glide來就完事,同時,很容易就把圖片處理的相關知識點忘掉。
從前段時間面試的狀況,我發現了這個現象:簡歷上寫熟悉Glide的,基本都是熟悉使用方法,不少3年-6年工做經驗,除了說Glide使用方便,不清楚Glide跟其餘圖片框架如Fresco的對比有哪些優缺點。
首先,當下流行的圖片加載框架有那麼幾個,能夠拿 Glide 跟Fresco對比,例如這些:
Glide:
Fresco:
對於通常App來講,Glide徹底夠用,而對於圖片需求比較大的App,爲了防止加載大量圖片致使OOM,Fresco 會更合適一些。並非說用Glide會致使OOM,Glide默認用的內存緩存是LruCache,內存不會一直往上漲。
首先,梳理一下必要的圖片加載框架的需求:
固然,還有一些不是必要的需求,例如加載動畫等。
線程池,多少個?
緩存通常有三級,內存緩存、硬盤、網絡。
因爲網絡會阻塞,因此讀內存和硬盤能夠放在一個線程池,網絡須要另一個線程池,網絡也能夠採用Okhttp內置的線程池。
讀硬盤和讀網絡須要放在不一樣的線程池中處理,因此用兩個線程池比較合適。
Glide 必然也須要多個線程池,看下源碼是否是這樣
public final class GlideBuilder {
...
private GlideExecutor sourceExecutor; //加載源文件的線程池,包括網絡加載
private GlideExecutor diskCacheExecutor; //加載硬盤緩存的線程池
...
private GlideExecutor animationExecutor; //動畫線程池
複製代碼
Glide使用了三個線程池,不考慮動畫的話就是兩個。
圖片異步加載成功,須要在主線程去更新ImageView,
不管是RxJava、EventBus,仍是Glide,只要是想從子線程切換到Android主線程,都離不開Handler。
看下Glide 相關源碼:
class EngineJob<R> implements DecodeJob.Callback<R>,Poolable {
private static final EngineResourceFactory DEFAULT_FACTORY = new EngineResourceFactory();
//建立Handler
private static final Handler MAIN_THREAD_HANDLER =
new Handler(Looper.getMainLooper(), new MainThreadCallback());
複製代碼
問RxJava是徹底用Java語言寫的,那怎麼實現從子線程切換到Android主線程的? 依然有不少3-6年的開發答不上來這個很基礎的問題,並且只要是這個問題回答不出來的,接下來有關於原理的問題,基本都答不上來。
有很多工做了不少年的Android開發不知道鴻洋、郭霖、玉剛說,不知道掘金是個啥玩意,心裏估計會想是否是還有叫掘銀掘鐵的(我不知道有沒有)。
我想表達的是,幹這一行,真的是須要有對技術的熱情,不斷學習,不怕別人比你優秀,就怕比你優秀的人比你還努力,而你殊不知道。
咱們常說的圖片三級緩存:內存緩存、硬盤緩存、網絡。
通常都是用LruCache
Glide 默認內存緩存用的也是LruCache,只不過並無用Android SDK中的LruCache,不過內部一樣是基於LinkHashMap,因此原理是同樣的。
// -> GlideBuilder#build
if (memoryCache == null) {
memoryCache = new LruResourceCache(memorySizeCalculator.getMemoryCacheSize());
}
複製代碼
既然說到LruCache ,必需要了解一下LruCache的特色和源碼:
爲何用LruCache?
LruCache 採用最近最少使用算法,設定一個緩存大小,當緩存達到這個大小以後,會將最老的數據移除,避免圖片佔用內存過大致使OOM。
public class LruCache<K, V> { // 數據最終存在 LinkedHashMap 中 private final LinkedHashMap<K, V> map; ... public LruCache(int maxSize) { if (maxSize <= 0) { throw new IllegalArgumentException("maxSize <= 0"); } this.maxSize = maxSize; // 建立一個LinkedHashMap,accessOrder 傳true this.map = new LinkedHashMap<K, V>(0, 0.75f, true); } ... 複製代碼
LruCache 構造方法裏建立一個LinkedHashMap,accessOrder 參數傳true,表示按照訪問順序排序,數據存儲基於LinkedHashMap。
先看看LinkedHashMap 的原理吧
LinkedHashMap 繼承 HashMap,在 HashMap 的基礎上進行擴展,put 方法並無重寫,說明LinkedHashMap遵循HashMap的數組加鏈表的結構,
LinkedHashMap重寫了 createEntry 方法。
看下HashMap 的 createEntry 方法
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMapEntry<K,V> e = table[bucketIndex];
table[bucketIndex] = new HashMapEntry<>(hash, key, value, e);
size++;
}
複製代碼
HashMap的數組裏面放的是HashMapEntry
對象
看下LinkedHashMap 的 createEntry方法
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMapEntry<K,V> old = table[bucketIndex];
LinkedHashMapEntry<K,V> e = new LinkedHashMapEntry<>(hash, key, value, old);
table[bucketIndex] = e; //數組的添加
e.addBefore(header); //處理鏈表
size++;
}
複製代碼
LinkedHashMap的數組裏面放的是LinkedHashMapEntry
對象
LinkedHashMapEntry
private static class LinkedHashMapEntry<K,V> extends HashMapEntry<K,V> {
// These fields comprise the doubly linked list used for iteration.
LinkedHashMapEntry<K,V> before, after; //雙向鏈表
private void remove() {
before.after = after;
after.before = before;
}
private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
複製代碼
LinkedHashMapEntry繼承 HashMapEntry,添加before和after變量,因此是一個雙向鏈表結構,還添加了addBefore
和remove
方法,用於新增和刪除鏈表節點。
LinkedHashMapEntry#addBefore
將一個數據添加到Header的前面
private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
複製代碼
existingEntry 傳的都是鏈表頭header,將一個節點添加到header節點前面,只須要移動鏈表指針便可,添加新數據都是放在鏈表頭header 的before位置,鏈表頭節點header的before是最新訪問的數據,header的after則是最舊的數據。
再看下LinkedHashMapEntry#remove
private void remove() {
before.after = after;
after.before = before;
}
複製代碼
鏈表節點的移除比較簡單,改變指針指向便可。
再看下LinkHashMap的put 方法
public final V put(K key, V value) {
V previous;
synchronized (this) {
putCount++;
//size增長
size += safeSizeOf(key, value);
// 一、linkHashMap的put方法
previous = map.put(key, value);
if (previous != null) {
//若是有舊的值,會覆蓋,因此大小要減掉
size -= safeSizeOf(key, previous);
}
}
trimToSize(maxSize);
return previous;
}
複製代碼
LinkedHashMap 結構能夠用這種圖表示
LinkHashMap 的 put方法和get方法最後會調用trimToSize
方法,LruCache 重寫trimToSize
方法,判斷內存若是超過必定大小,則移除最老的數據
LruCache#trimToSize,移除最老的數據
public void trimToSize(int maxSize) {
while (true) {
K key;
V value;
synchronized (this) {
//大小沒有超出,不處理
if (size <= maxSize) {
break;
}
//超出大小,移除最老的數據
Map.Entry<K, V> toEvict = map.eldest();
if (toEvict == null) {
break;
}
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
//這個大小的計算,safeSizeOf 默認返回1;
size -= safeSizeOf(key, value);
evictionCount++;
}
entryRemoved(true, key, value, null);
}
}
複製代碼
對LinkHashMap 還不是很理解的話能夠參考:
圖解LinkedHashMap原理
LruCache小結:
依賴:
implementation 'com.jakewharton:disklrucache:2.0.2'
DiskLruCache 跟 LruCache 實現思路是差很少的,同樣是設置一個總大小,每次往硬盤寫文件,總大小超過閾值,就會將舊的文件刪除。簡單看下remove操做:
// DiskLruCache 內部也是用LinkedHashMap private final LinkedHashMap<String, Entry> lruEntries = new LinkedHashMap<String, Entry>(0, 0.75f, true); ... public synchronized boolean remove(String key) throws IOException { checkNotClosed(); validateKey(key); Entry entry = lruEntries.get(key); if (entry == null || entry.currentEditor != null) { return false; } //一個key可能對應多個value,hash衝突的狀況 for (int i = 0; i < valueCount; i++) { File file = entry.getCleanFile(i); //經過 file.delete() 刪除緩存文件,刪除失敗則拋異常 if (file.exists() && !file.delete()) { throw new IOException("failed to delete " + file); } size -= entry.lengths[i]; entry.lengths[i] = 0; } ... return true; } 複製代碼
能夠看到 DiskLruCache 一樣是利用LinkHashMap的特色,只不過數組裏面存的 Entry 有點變化,Editor 用於操做文件。
private final class Entry {
private final String key;
private final long[] lengths;
private boolean readable;
private Editor currentEditor;
private long sequenceNumber;
...
}
複製代碼
加載圖片很是重要的一點是須要防止OOM,上面的LruCache緩存大小設置,能夠有效防止OOM,可是當圖片需求比較大,可能須要設置一個比較大的緩存,這樣的話發生OOM的機率就提升了,那應該探索其它防止OOM的方法。
回顧一下Java的四大引用:
private Context context;
怎麼理解強引用:
強引用對象的回收時機依賴垃圾回收算法,咱們常說的可達性分析算法,當Activity銷燬的時候,Activity會跟GCRoot斷開,至於GCRoot是誰?這裏能夠大膽猜測,Activity對象的建立是在ActivityThread中,ActivityThread要回調Activity的各個生命週期,確定是持有Activity引用的,那麼這個GCRoot能夠認爲就是ActivityThread,當Activity 執行onDestroy的時候,ActivityThread 就會斷開跟這個Activity的聯繫,Activity到GCRoot不可達,因此會被垃圾回收器標記爲可回收對象。
軟引用的設計就是應用於會發生OOM的場景,大內存對象如Bitmap,能夠經過 SoftReference 修飾,防止大對象形成OOM,看下這段代碼
private static LruCache<String, SoftReference<Bitmap>> mLruCache = new LruCache<String, SoftReference<Bitmap>>(10 * 1024){ @Override protected int sizeOf(String key, SoftReference<Bitmap> value) { //默認返回1,這裏應該返回Bitmap佔用的內存大小,單位:K //Bitmap被回收了,大小是0 if (value.get() == null){ return 0; } return value.get().getByteCount() /1024; } }; 複製代碼
LruCache裏存的是軟引用對象,那麼當內存不足的時候,Bitmap會被回收,也就是說經過SoftReference修飾的Bitmap就不會致使OOM。
固然,這段代碼存在一些問題,Bitmap被回收的時候,LruCache剩餘的大小應該從新計算,能夠寫個方法,當Bitmap取出來是空的時候,LruCache清理一下,從新計算剩餘內存;
還有另外一個問題,就是內存不足時軟引用中的Bitmap被回收的時候,這個LruCache就形同虛設,至關於內存緩存失效了,必然出現效率問題。
當內存不足的時候,Activity、Fragment會調用onLowMemory
方法,能夠在這個方法裏去清除緩存,Glide使用的就是這一種方式來防止OOM。
//Glide public void onLowMemory() { clearMemory(); } public void clearMemory() { // Engine asserts this anyway when removing resources, fail faster and consistently Util.assertMainThread(); // memory cache needs to be cleared before bitmap pool to clear re-pooled Bitmaps too. See #687. memoryCache.clearMemory(); bitmapPool.clearMemory(); arrayPool.clearMemory(); } 複製代碼
咱們知道,系統爲每一個進程,也就是每一個虛擬機分配的內存是有限的,早期的16M、32M,如今100+M,
虛擬機的內存劃分主要有5部分:
而對象的分配通常都是在堆中,堆是JVM中最大的一塊內存,OOM通常都是發生在堆中。
Bitmap 之因此佔內存大不是由於對象自己大,而是由於Bitmap的像素數據, Bitmap的像素數據大小 = 寬 * 高 * 1像素佔用的內存。
1像素佔用的內存是多少?不一樣格式的Bitmap對應的像素佔用內存是不一樣的,具體是多少呢?
在Fresco中看到以下定義代碼
/**
* Bytes per pixel definitions
*/
public static final int ALPHA_8_BYTES_PER_PIXEL = 1;
public static final int ARGB_4444_BYTES_PER_PIXEL = 2;
public static final int ARGB_8888_BYTES_PER_PIXEL = 4;
public static final int RGB_565_BYTES_PER_PIXEL = 2;
public static final int RGBA_F16_BYTES_PER_PIXEL = 8;
複製代碼
若是Bitmap使用 RGB_565
格式,則1像素佔用 2 byte,ARGB_8888
格式則佔4 byte。
在選擇圖片加載框架的時候,能夠將內存佔用這一方面考慮進去,更少的內存佔用意味着發生OOM的機率越低。 Glide內存開銷是Picasso的一半,就是由於默認Bitmap格式不一樣。
至於寬高,是指Bitmap的寬高,怎麼計算的呢?看BitmapFactory.Options
的 outWidth
/**
* The resulting width of the bitmap. If {@link #inJustDecodeBounds} is
* set to false, this will be width of the output bitmap after any
* scaling is applied. If true, it will be the width of the input image
* without any accounting for scaling.
*
* <p>outWidth will be set to -1 if there is an error trying to decode.</p>
*/
public int outWidth;
複製代碼
看註釋的意思,若是 BitmapFactory.Options
中指定 inJustDecodeBounds
爲true,則爲原圖寬高,若是是false,則是縮放後的寬高。因此咱們通常能夠經過壓縮來減少Bitmap像素佔用內存。
扯遠了,上面分析了Bitmap像素數據大小的計算,只是說明Bitmap像素數據爲何那麼大。那是否可讓像素數據不放在java堆中,而是放在native堆中呢?聽說Android 3.0到8.0 之間Bitmap像素數據存在Java堆,而8.0以後像素數據存到native堆中,是否是真的?看下源碼就知道了~
java層建立Bitmap方法
public static Bitmap createBitmap(@Nullable DisplayMetrics display, int width, int height, @NonNull Config config, boolean hasAlpha, @NonNull ColorSpace colorSpace) { ... Bitmap bm; ... if (config != Config.ARGB_8888 || colorSpace == ColorSpace.get(ColorSpace.Named.SRGB)) { //最終都是經過native方法建立 bm = nativeCreate(null, 0, width, width, height, config.nativeInt, true, null, null); } else { bm = nativeCreate(null, 0, width, width, height, config.nativeInt, true, d50.getTransform(), parameters); } ... return bm; } 複製代碼
Bitmap 的建立是經過native方法 nativeCreate
對應源碼 8.0.0_r4/xref/frameworks/base/core/jni/android/graphics/Bitmap.cpp
//Bitmap.cpp static const JNINativeMethod gBitmapMethods[] = { { "nativeCreate", "([IIIIIIZ[FLandroid/graphics/ColorSpace$Rgb$TransferParameters;)Landroid/graphics/Bitmap;", (void*)Bitmap_creator }, ... 複製代碼
JNI動態註冊,nativeCreate 方法 對應 Bitmap_creator
;
//Bitmap.cpp static jobject Bitmap_creator(JNIEnv* env, jobject, jintArray jColors, jint offset, jint stride, jint width, jint height, jint configHandle, jboolean isMutable, jfloatArray xyzD50, jobject transferParameters) { ... //1. 申請堆內存,建立native層Bitmap sk_sp<Bitmap> nativeBitmap = Bitmap::allocateHeapBitmap(&bitmap, NULL); if (!nativeBitmap) { return NULL; } ... //2.建立java層Bitmap return createBitmap(env, nativeBitmap.release(), getPremulBitmapCreateFlags(isMutable)); } 複製代碼
主要兩個步驟:
allocateHeapBitmap
方法// static sk_sp<Bitmap> allocateHeapBitmap(size_t size, const SkImageInfo& info, size_t rowBytes, SkColorTable* ctable) { // calloc 是c++ 的申請內存函數 void* addr = calloc(size, 1); if (!addr) { return nullptr; } return sk_sp<Bitmap>(new Bitmap(addr, size, info, rowBytes, ctable)); } 複製代碼
能夠看到經過c++的 calloc
函數申請了一塊內存空間,而後建立native層Bitmap對象,把內存地址傳過去,也就是native層的Bitmap數據(像素數據)是存在native堆中。
//Bitmap.cpp jobject createBitmap(JNIEnv* env, Bitmap* bitmap, int bitmapCreateFlags, jbyteArray ninePatchChunk, jobject ninePatchInsets, int density) { ... BitmapWrapper* bitmapWrapper = new BitmapWrapper(bitmap); //經過JNI回調Java層,調用java層的Bitmap構造方法 jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID, reinterpret_cast<jlong>(bitmapWrapper), bitmap->width(), bitmap->height(), density, isMutable, isPremultiplied, ninePatchChunk, ninePatchInsets); ... return obj; } 複製代碼
env->NewObject,經過JNI建立Java層Bitmap對象,gBitmap_class,gBitmap_constructorMethodID
這些變量是什麼意思,看下面這個方法,對應java層的Bitmap的類名和構造方法。
//Bitmap.cpp int register_android_graphics_Bitmap(JNIEnv* env) { gBitmap_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, "android/graphics/Bitmap")); gBitmap_nativePtr = GetFieldIDOrDie(env, gBitmap_class, "mNativePtr", "J"); gBitmap_constructorMethodID = GetMethodIDOrDie(env, gBitmap_class, "<init>", "(JIIIZZ[BLandroid/graphics/NinePatch$InsetStruct;)V"); gBitmap_reinitMethodID = GetMethodIDOrDie(env, gBitmap_class, "reinit", "(IIZ)V"); gBitmap_getAllocationByteCountMethodID = GetMethodIDOrDie(env, gBitmap_class, "getAllocationByteCount", "()I"); return android::RegisterMethodsOrDie(env, "android/graphics/Bitmap", gBitmapMethods, NELEM(gBitmapMethods)); } 複製代碼
8.0 的Bitmap建立就兩個點:
像素數據是存在native層Bitmap,也就是證實8.0的Bitmap像素數據存在native堆中。
直接看native層的方法,
/7.0.0_r31/xref/frameworks/base/core/jni/android/graphics/Bitmap.cpp
//JNI動態註冊 static const JNINativeMethod gBitmapMethods[] = { { "nativeCreate", "([IIIIIIZ)Landroid/graphics/Bitmap;", (void*)Bitmap_creator }, ... static jobject Bitmap_creator(JNIEnv* env, jobject, jintArray jColors, jint offset, jint stride, jint width, jint height, jint configHandle, jboolean isMutable) { ... //1.經過這個方法來建立native層Bitmap Bitmap* nativeBitmap = GraphicsJNI::allocateJavaPixelRef(env, &bitmap, NULL); ... return GraphicsJNI::createBitmap(env, nativeBitmap, getPremulBitmapCreateFlags(isMutable)); } 複製代碼
native層Bitmap 建立是經過GraphicsJNI::allocateJavaPixelRef
,看看裏面是怎麼分配的, GraphicsJNI 的實現類是Graphics.cpp
android::Bitmap* GraphicsJNI::allocateJavaPixelRef(JNIEnv* env, SkBitmap* bitmap, SkColorTable* ctable) { const SkImageInfo& info = bitmap->info(); size_t size; //計算須要的空間大小 if (!computeAllocationSize(*bitmap, &size)) { return NULL; } // we must respect the rowBytes value already set on the bitmap instead of // attempting to compute our own. const size_t rowBytes = bitmap->rowBytes(); // 1. 建立一個數組,經過JNI在java層建立的 jbyteArray arrayObj = (jbyteArray) env->CallObjectMethod(gVMRuntime, gVMRuntime_newNonMovableArray, gByte_class, size); ... // 2. 獲取建立的數組的地址 jbyte* addr = (jbyte*) env->CallLongMethod(gVMRuntime, gVMRuntime_addressOf, arrayObj); ... //3. 建立Bitmap,傳這個地址 android::Bitmap* wrapper = new android::Bitmap(env, arrayObj, (void*) addr, info, rowBytes, ctable); wrapper->getSkBitmap(bitmap); // since we're already allocated, we lockPixels right away // HeapAllocator behaves this way too bitmap->lockPixels(); return wrapper; } 複製代碼
能夠看到,7.0 像素內存的分配是這樣的:
由此說明,7.0 的Bitmap像素數據是放在java堆的。
固然,3.0 如下Bitmap像素內存聽說也是放在native堆的,可是須要手動釋放native層的Bitmap,也就是須要手動調用recycle方法,native層內存纔會被回收。這個你們能夠本身去看源碼驗證。
Java層的Bitmap對象由垃圾回收器自動回收,而native層Bitmap印象中咱們是不須要手動回收的,源碼中如何處理的呢?
記得有個面試題是這樣的:
說說final、finally、finalize 的關係
三者除了長得像,其實沒有半毛錢關係,final、finally你們都用的比較多,而 finalize
用的少,或者沒用過,finalize
是 Object 類的一個方法,註釋是這樣的:
/**
* Called by the garbage collector on an object when garbage collection
* determines that there are no more references to the object.
* A subclass overrides the {@code finalize} method to dispose of
* system resources or to perform other cleanup.
* <p>
...**/
protected void finalize() throws Throwable { }
複製代碼
意思是說,垃圾回收器確認這個對象沒有其它地方引用到它的時候,會調用這個對象的finalize
方法,子類能夠重寫這個方法,作一些釋放資源的操做。
在6.0之前,Bitmap 就是經過這個finalize 方法來釋放native層對象的。 6.0 Bitmap.java
Bitmap(long nativeBitmap, byte[] buffer, int width, int height, int density, boolean isMutable, boolean requestPremultiplied, byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) { ... mNativePtr = nativeBitmap; //1.建立 BitmapFinalizer mFinalizer = new BitmapFinalizer(nativeBitmap); int nativeAllocationByteCount = (buffer == null ? getByteCount() : 0); mFinalizer.setNativeAllocationByteCount(nativeAllocationByteCount); } private static class BitmapFinalizer { private long mNativeBitmap; // Native memory allocated for the duration of the Bitmap, // if pixel data allocated into native memory, instead of java byte[] private int mNativeAllocationByteCount; BitmapFinalizer(long nativeBitmap) { mNativeBitmap = nativeBitmap; } public void setNativeAllocationByteCount(int nativeByteCount) { if (mNativeAllocationByteCount != 0) { VMRuntime.getRuntime().registerNativeFree(mNativeAllocationByteCount); } mNativeAllocationByteCount = nativeByteCount; if (mNativeAllocationByteCount != 0) { VMRuntime.getRuntime().registerNativeAllocation(mNativeAllocationByteCount); } } @Override public void finalize() { try { super.finalize(); } catch (Throwable t) { // Ignore } finally { //2.就是這裏了, setNativeAllocationByteCount(0); nativeDestructor(mNativeBitmap); mNativeBitmap = 0; } } } 複製代碼
在Bitmap構造方法建立了一個 BitmapFinalizer
類,重寫finalize 方法,在java層Bitmap被回收的時候,BitmapFinalizer 對象也會被回收,finalize 方法確定會被調用,在裏面釋放native層Bitmap對象。
6.0 以後作了一些變化,BitmapFinalizer 沒有了,被NativeAllocationRegistry取代。
例如 8.0 Bitmap構造方法
Bitmap(long nativeBitmap, int width, int height, int density,
boolean isMutable, boolean requestPremultiplied,
byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {
...
mNativePtr = nativeBitmap;
long nativeSize = NATIVE_ALLOCATION_SIZE + getAllocationByteCount();
// 建立NativeAllocationRegistry這個類,調用registerNativeAllocation 方法
NativeAllocationRegistry registry = new NativeAllocationRegistry(
Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), nativeSize);
registry.registerNativeAllocation(this, nativeBitmap);
}
複製代碼
NativeAllocationRegistry 就不分析了, 無論是BitmapFinalizer 仍是NativeAllocationRegistry,目的都是在java層Bitmap被回收的時候,將native層Bitmap對象也回收掉。 通常狀況下咱們無需手動調用recycle方法,由GC去盤它便可。
上面分析了Bitmap像素存儲位置,咱們知道,Android 8.0 以後Bitmap像素內存放在native堆,Bitmap致使OOM的問題基本不會在8.0以上設備出現了(沒有內存泄漏的狀況下),那8.0 如下設備怎麼辦?趕忙升級或換手機吧~
咱們換手機固然沒問題,可是並非全部人都能跟上Android系統更新的步伐,因此,問題仍是要解決~
Fresco 之因此能跟Glide 正面交鋒,必然有其獨特之處,文中開頭列出 Fresco 的優勢是:「在5.0如下(最低2.3)系統,Fresco將圖片放到一個特別的內存區域(Ashmem區)」 這個Ashmem區是一塊匿名共享內存,Fresco 將Bitmap像素放到共享內存去了,共享內存是屬於native堆內存。
Fresco 關鍵源碼在 PlatformDecoderFactory
這個類
public class PlatformDecoderFactory { /** * Provide the implementation of the PlatformDecoder for the current platform using the provided * PoolFactory * * @param poolFactory The PoolFactory * @return The PlatformDecoder implementation */ public static PlatformDecoder buildPlatformDecoder( PoolFactory poolFactory, boolean gingerbreadDecoderEnabled) { //8.0 以上用 OreoDecoder 這個解碼器 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { int maxNumThreads = poolFactory.getFlexByteArrayPoolMaxNumThreads(); return new OreoDecoder( poolFactory.getBitmapPool(), maxNumThreads, new Pools.SynchronizedPool<>(maxNumThreads)); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { //大於5.0小於8.0用 ArtDecoder 解碼器 int maxNumThreads = poolFactory.getFlexByteArrayPoolMaxNumThreads(); return new ArtDecoder( poolFactory.getBitmapPool(), maxNumThreads, new Pools.SynchronizedPool<>(maxNumThreads)); } else { if (gingerbreadDecoderEnabled && Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { //小於4.4 用 GingerbreadPurgeableDecoder 解碼器 return new GingerbreadPurgeableDecoder(); } else { //這個就是4.4到5.0 用的解碼器了 return new KitKatPurgeableDecoder(poolFactory.getFlexByteArrayPool()); } } } } 複製代碼
8.0 先不看了,看一下 4.4 如下是怎麼獲得Bitmap的,看下GingerbreadPurgeableDecoder
這個類有個獲取Bitmap的方法
//GingerbreadPurgeableDecoder private Bitmap decodeFileDescriptorAsPurgeable( CloseableReference<PooledByteBuffer> bytesRef, int inputLength, byte[] suffix, BitmapFactory.Options options) { // MemoryFile :匿名共享內存 MemoryFile memoryFile = null; try { //將圖片數據拷貝到匿名共享內存 memoryFile = copyToMemoryFile(bytesRef, inputLength, suffix); FileDescriptor fd = getMemoryFileDescriptor(memoryFile); if (mWebpBitmapFactory != null) { // 建立Bitmap,Fresco本身寫了一套建立Bitmap方法 Bitmap bitmap = mWebpBitmapFactory.decodeFileDescriptor(fd, null, options); return Preconditions.checkNotNull(bitmap, "BitmapFactory returned null"); } else { throw new IllegalStateException("WebpBitmapFactory is null"); } } } 複製代碼
捋一捋,4.4如下,Fresco 使用匿名共享內存來保存Bitmap數據,首先將圖片數據拷貝到匿名共享內存中,而後使用Fresco本身寫的加載Bitmap的方法。
Fresco對不一樣Android版本使用不一樣的方式去加載Bitmap,至於4.4-5.0,5.0-8.0,8.0 以上,對應另外三個解碼器,你們能夠從PlatformDecoderFactory
這個類入手,本身去分析,思考爲何不一樣平臺要分這麼多個解碼器,8.0 如下都用匿名共享內存很差嗎?期待你在評論區跟你們分享~
曾經在Vivo駐場開發,帶有頭像功能的頁面被測出內存泄漏,緣由是SDK中有個加載網絡頭像的方法,持有ImageView引用致使的。
固然,修改也比較簡單粗暴,將ImageView用WeakReference修飾就完事了。
事實上,這種方式雖然解決了內存泄露問題,可是並不完美,例如在界面退出的時候,咱們除了但願ImageView被回收,同時但願加載圖片的任務能夠取消,隊未執行的任務能夠移除。
Glide的作法是監聽生命週期回調,看 RequestManager
這個類
public void onDestroy() { targetTracker.onDestroy(); for (Target<?> target : targetTracker.getAll()) { //清理任務 clear(target); } targetTracker.clear(); requestTracker.clearRequests(); lifecycle.removeListener(this); lifecycle.removeListener(connectivityMonitor); mainHandler.removeCallbacks(addSelfToLifecycle); glide.unregisterRequestManager(this); } 複製代碼
在Activity/fragment 銷燬的時候,取消圖片加載任務,細節你們能夠本身去看源碼。
因爲RecyclerView或者LIstView的複用機制,網絡加載圖片開始的時候ImageView是第一個item的,加載成功以後ImageView因爲複用可能跑到第10個item去了,在第10個item顯示第一個item的圖片確定是錯的。
常規的作法是給ImageView設置tag,tag通常是圖片地址,更新ImageView以前判斷tag是否跟url一致。
固然,能夠在item從列表消失的時候,取消對應的圖片加載任務。要考慮放在圖片加載框架作仍是放在UI作比較合適。
列表滑動,會有不少圖片請求,若是是第一次進入,沒有緩存,那麼隊列會有不少任務在等待。因此在請求網絡圖片以前,須要判斷隊列中是否已經存在該任務,存在則不加到隊列去。
本文經過Glide開題,分析一個圖片加載框架必要的需求,以及各個需求涉及到哪些技術和原理。
文中也遺留一些問題,例如:
Fresco爲何要在不一樣Android版本上使用不一樣解碼器去獲取Bitmap,8.0如下都用匿名共享內存不能夠嗎?期待你主動學習而且在評論區跟你們分享~
就這樣,歡迎評論區留言~
相關參考文章:
圖解LinkedHashMap原理
談談fresco的bitmap內存分配
我在掘金髮布的其它文章:
總結UI原理和高級的UI優化方式
面試官:說說多線程併發問題
面試官又來了:你的app卡頓過嗎?
面試官:今日頭條啓動很快,你以爲多是作了哪些優化?