面試官:簡歷上最好不要寫Glide,不是問源碼那麼簡單

此次來面試的是一個有着5年工做經驗的小夥,截取了一段對話以下:java

面試官:我看你寫到Glide,爲何用Glide,而不選擇其它圖片加載框架?
小夥:Glide 使用簡單,鏈式調用,很方便,一直用這個。
面試官:有看過它的源碼嗎?跟其它圖片框架相比有哪些優點?
小夥:沒有,只是在項目中使用而已~
面試官:假如如今不讓你用開源庫,須要你本身寫一個圖片加載框架,你會考慮哪些方面的問題,說說大概的思路。
小夥:額~,壓縮吧。
面試官:還有嗎?
小夥:額~,這個沒寫過。android

說到圖片加載框架,你們最熟悉的莫過於Glide了,但我卻不推薦簡歷上寫熟悉Glide,除非你熟讀它的源碼,或者參與Glide的開發和維護。c++

在通常面試中,遇到圖片加載問題的頻率通常不會過低,只是問法會有一些差別,例如:git

  • 簡歷上寫Glide,那麼會問一下Glide的設計,以及跟其它同類框架的對比 ;
  • 假如讓你寫一個圖片加載框架,說說思路;
  • 給一個圖片加載的場景,好比網絡加載一張或多張大圖,你會怎麼作;

帶着問題進入正文~github

1、談談Glide

1.1 Glide 使用有多簡單?

Glide因爲其口碑好,不少開發者直接在項目中使用,使用方法至關簡單面試

github.com/bumptech/gl…算法

一、添加依賴:數組

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來就完事,同時,很容易就把圖片處理的相關知識點忘掉。

1.2 爲何用Glide?

從前段時間面試的狀況,我發現了這個現象:簡歷上寫熟悉Glide的,基本都是熟悉使用方法,不少3年-6年工做經驗,除了說Glide使用方便,不清楚Glide跟其餘圖片框架如Fresco的對比有哪些優缺點。

首先,當下流行的圖片加載框架有那麼幾個,能夠拿 Glide 跟Fresco對比,例如這些:

Glide:

  • 多種圖片格式的緩存,適用於更多的內容表現形式(如Gif、WebP、縮略圖、Video)
  • 生命週期集成(根據Activity或者Fragment的生命週期管理圖片加載請求)
  • 高效處理Bitmap(bitmap的複用和主動回收,減小系統回收壓力)
  • 高效的緩存策略,靈活(Picasso只會緩存原始尺寸的圖片,Glide緩存的是多種規格),加載速度快且內存開銷小(默認Bitmap格式的不一樣,使得內存開銷是Picasso的一半)

Fresco:

  • 最大的優點在於5.0如下(最低2.3)的bitmap加載。在5.0如下系統,Fresco將圖片放到一個特別的內存區域(Ashmem區)
  • 大大減小OOM(在更底層的Native層對OOM進行處理,圖片將再也不佔用App的內存)
  • 適用於須要高性能加載大量圖片的場景

對於通常App來講,Glide徹底夠用,而對於圖片需求比較大的App,爲了防止加載大量圖片致使OOM,Fresco 會更合適一些。並非說用Glide會致使OOM,Glide默認用的內存緩存是LruCache,內存不會一直往上漲。

2、假如讓你本身寫個圖片加載框架,你會考慮哪些問題?

首先,梳理一下必要的圖片加載框架的需求:

  • 異步加載:線程池
  • 切換線程:Handler,沒有爭議吧
  • 緩存:LruCache、DiskLruCache
  • 防止OOM:軟引用、LruCache、圖片壓縮、Bitmap像素存儲位置
  • 內存泄露:注意ImageView的正確引用,生命週期管理
  • 列表滑動加載的問題:加載錯亂、隊滿任務過多問題

固然,還有一些不是必要的需求,例如加載動畫等。

2.1 異步加載:

線程池,多少個?

緩存通常有三級,內存緩存、硬盤、網絡。

因爲網絡會阻塞,因此讀內存和硬盤能夠放在一個線程池,網絡須要另一個線程池,網絡也能夠採用Okhttp內置的線程池。

讀硬盤和讀網絡須要放在不一樣的線程池中處理,因此用兩個線程池比較合適。

Glide 必然也須要多個線程池,看下源碼是否是這樣

public final class GlideBuilder {
  ...
  private GlideExecutor sourceExecutor; //加載源文件的線程池,包括網絡加載
  private GlideExecutor diskCacheExecutor; //加載硬盤緩存的線程池
  ...
  private GlideExecutor animationExecutor; //動畫線程池
複製代碼

Glide使用了三個線程池,不考慮動畫的話就是兩個。

2.2 切換線程:

圖片異步加載成功,須要在主線程去更新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開發不知道鴻洋、郭霖、玉剛說,不知道掘金是個啥玩意,心裏估計會想是否是還有叫掘銀掘鐵的(我不知道有沒有)。

我想表達的是,幹這一行,真的是須要有對技術的熱情,不斷學習,不怕別人比你優秀,就怕比你優秀的人比你還努力,而你殊不知道

2.3 緩存

咱們常說的圖片三級緩存:內存緩存、硬盤緩存、網絡。

2.3.1 內存緩存

通常都是用LruCache

Glide 默認內存緩存用的也是LruCache,只不過並無用Android SDK中的LruCache,不過內部一樣是基於LinkHashMap,因此原理是同樣的。

// -> GlideBuilder#build
if (memoryCache == null) {
  memoryCache = new LruResourceCache(memorySizeCalculator.getMemoryCacheSize());
}
複製代碼

既然說到LruCache ,必需要了解一下LruCache的特色和源碼:

爲何用LruCache?

LruCache 採用最近最少使用算法,設定一個緩存大小,當緩存達到這個大小以後,會將最老的數據移除,避免圖片佔用內存過大致使OOM。

LruCache 源碼分析
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的數組加鏈表的結構

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變量,因此是一個雙向鏈表結構,還添加了addBeforeremove 方法,用於新增和刪除鏈表節點。

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 結構能夠用這種圖表示

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小結:

  • LinkHashMap 繼承HashMap,在 HashMap的基礎上,新增了雙向鏈表結構,每次訪問數據的時候,會更新被訪問的數據的鏈表指針,具體就是先在鏈表中刪除該節點,而後添加到鏈表頭header以前,這樣就保證了鏈表頭header節點以前的數據都是最近訪問的(從鏈表中刪除並非真的刪除數據,只是移動鏈表指針,數據自己在map中的位置是不變的)。
  • LruCache 內部用LinkHashMap存取數據,在雙向鏈表保證數據新舊順序的前提下,設置一個最大內存,往裏面put數據的時候,當數據達到最大內存的時候,將最老的數據移除掉,保證內存不超過設定的最大值。

2.3.2 磁盤緩存 DiskLruCache

依賴:

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;
	...
}
複製代碼

2.4 防止OOM

加載圖片很是重要的一點是須要防止OOM,上面的LruCache緩存大小設置,能夠有效防止OOM,可是當圖片需求比較大,可能須要設置一個比較大的緩存,這樣的話發生OOM的機率就提升了,那應該探索其它防止OOM的方法。

方法1:軟引用

回顧一下Java的四大引用:

  • 強引用: 普通變量都屬於強引用,好比 private Context context;
  • 軟應用: SoftReference,在發生OOM以前,垃圾回收器會回收SoftReference引用的對象。
  • 弱引用: WeakReference,發生GC的時候,垃圾回收器會回收WeakReference中的對象。
  • 虛引用: 隨時會被回收,沒有使用場景。

怎麼理解強引用:

強引用對象的回收時機依賴垃圾回收算法,咱們常說的可達性分析算法,當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就形同虛設,至關於內存緩存失效了,必然出現效率問題。

方法2:onLowMemory

當內存不足的時候,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();
  }
複製代碼
方法3:從Bitmap 像素存儲位置考慮

咱們知道,系統爲每一個進程,也就是每一個虛擬機分配的內存是有限的,早期的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堆中,是否是真的?看下源碼就知道了~

8.0 Bitmap

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));
}
複製代碼

主要兩個步驟:

  1. 申請內存,建立native層Bitmap,看下allocateHeapBitmap方法
    8.0.0_r4/xref/frameworks/base/libs/hwui/hwui/Bitmap.cpp
//
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堆中。

  1. 建立java 層Bitmap
//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建立就兩個點:

  1. 建立native層Bitmap,在native堆申請內存。
  2. 經過JNI建立java層Bitmap對象,這個對象在java堆中分配內存。

像素數據是存在native層Bitmap,也就是證實8.0的Bitmap像素數據存在native堆中。

7.0 Bitmap

直接看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 像素內存的分配是這樣的:

  1. 經過JNI調用java層建立一個數組
  2. 而後建立native層Bitmap,把數組的地址傳進去。

由此說明,7.0 的Bitmap像素數據是放在java堆的。

固然,3.0 如下Bitmap像素內存聽說也是放在native堆的,可是須要手動釋放native層的Bitmap,也就是須要手動調用recycle方法,native層內存纔會被回收。這個你們能夠本身去看源碼驗證。

native層Bitmap 回收問題

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 如下都用匿名共享內存很差嗎?期待你在評論區跟你們分享~

2.5 ImageView 內存泄露

曾經在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 銷燬的時候,取消圖片加載任務,細節你們能夠本身去看源碼。

2.6 列表加載問題

圖片錯亂

因爲RecyclerView或者LIstView的複用機制,網絡加載圖片開始的時候ImageView是第一個item的,加載成功以後ImageView因爲複用可能跑到第10個item去了,在第10個item顯示第一個item的圖片確定是錯的。

常規的作法是給ImageView設置tag,tag通常是圖片地址,更新ImageView以前判斷tag是否跟url一致。

固然,能夠在item從列表消失的時候,取消對應的圖片加載任務。要考慮放在圖片加載框架作仍是放在UI作比較合適。

線程池任務過多

列表滑動,會有不少圖片請求,若是是第一次進入,沒有緩存,那麼隊列會有不少任務在等待。因此在請求網絡圖片以前,須要判斷隊列中是否已經存在該任務,存在則不加到隊列去。

總結

本文經過Glide開題,分析一個圖片加載框架必要的需求,以及各個需求涉及到哪些技術和原理。

  • 異步加載:最少兩個線程池
  • 切換到主線程:Handler
  • 緩存:LruCache、DiskLruCache,涉及到LinkHashMap原理
  • 防止OOM:軟引用、LruCache、圖片壓縮沒展開講、Bitmap像素存儲位置源碼分析、Fresco部分源碼分析
  • 內存泄露:注意ImageView的正確引用,生命週期管理
  • 列表滑動加載的問題:加載錯亂用tag、隊滿任務存在則不添加

文中也遺留一些問題,例如:
Fresco爲何要在不一樣Android版本上使用不一樣解碼器去獲取Bitmap,8.0如下都用匿名共享內存不能夠嗎?期待你主動學習而且在評論區跟你們分享~


就這樣,歡迎評論區留言~

相關參考文章:
圖解LinkedHashMap原理
談談fresco的bitmap內存分配

我在掘金髮布的其它文章:

總結UI原理和高級的UI優化方式
面試官:說說多線程併發問題
面試官又來了:你的app卡頓過嗎?
面試官:今日頭條啓動很快,你以爲多是作了哪些優化?

相關文章
相關標籤/搜索