天天看點

如何設計一個圖檔加載架構

需要考慮哪些問題?

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

異步加載:線程池

切換線程:Handler,沒有争議吧

緩存:LruCache、DiskLruCache

防止OOM:軟引用、LruCache、圖檔壓縮、Bitmap像素存儲位置

記憶體洩露:注意ImageView的正确引用,生命周期管理

清單滑動加載的問題:加載錯亂、隊滿任務過多問題

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

異步加載:

線程池,多少個?

緩存一般有三級,記憶體緩存、硬碟、網絡。

2個,讀記憶體和硬碟可以放在一個線程池,網絡需要另外一個線程池,網絡也可以采用Okhttp内置的線程池。

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

public final class GlideBuilder {
  ...
  private GlideExecutor sourceExecutor; //加載源檔案的線程池,包括網絡加載
  private GlideExecutor diskCacheExecutor; //加載硬碟緩存的線程池
  ...
  private GlideExecutor animationExecutor; //動畫線程池           

複制

切換線程

圖檔異步加載成功,需要在主線程去更新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());           

複制

緩存

弱引用+LruCache+DiskLruCache

防止OOM

1.軟引用

強引用: 普通變量都屬于強引用,比如 private Context context;

軟應用: SoftReference,在發生OOM之前,垃圾回收器會回收SoftReference引用的對象。

弱引用: WeakReference,發生GC的時候,垃圾回收器會回收WeakReference中的對象。

虛引用: 為一個對象設定虛引用關聯的唯一目的就是能在這個對象被收集器回收時收到一個系統通知。

方法1:軟應用

如果一個對象隻具有軟引用,那麼如果記憶體空間足夠,垃圾回收器就不會回收它;如果記憶體空間不足了,就會回收這些對象的記憶體。隻要垃圾回收器沒有回收它,該對象就可以被程式使用。

軟引用可用來實作記憶體敏感的高速緩存。軟引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果軟引用所引用的對象被垃圾回收,Java虛拟機就會把這個軟引用加入到與之關聯的引用隊列中。

使用場景:LeakCanary

jdk中直接記憶體的回收就用到虛引用,由于jvm自動記憶體管理的範圍是堆記憶體,而直接記憶體是在堆記憶體之外(其實是記憶體映射檔案,自行去了解虛拟記憶體空間的相關概念),是以直接記憶體的配置設定和回收都是有Unsafe類去操作,java在申請一塊直接記憶體之後,會在堆記憶體配置設定一個對象儲存這個堆外記憶體的引用,這個對象被垃圾收集器管理,一旦這個對象被回收,相應的使用者線程會收到通知并對直接記憶體進行清理工作。

android中非靜态handler為什麼會造成記憶體洩漏?

當一個android主線程被建立的時候,同時會有一個Looper對象被建立,而這個Looper對象會實作一個MessageQueue(消息隊列),當我們建立一個handler對象時,而handler的作用就是放入和取出消息從這個消息隊列中,每當我們通過handler将一個msg放入消息隊列時,這個msg就會持有一個handler對象的引用。

是以當Activity被結束後,這個msg在被取出來之前,這msg會繼續存活,但是這個msg持有handler的引用,而handler在Activity中建立,會持有Activity的引用,因而當Activity結束後,Activity對象并不能夠被gc回收,因而出現記憶體洩漏

但是為什麼為static類型就會解決這個問題呢?

因為在java中所有非靜态的對象都會持有目前類的強引用,而靜态對象則隻會持有目前類的弱引用。

聲明為靜态後,handler将會持有一個Activity的弱引用,而弱引用會很容易被gc回收,這樣就能解決Activity結束後,gc卻無法回收的情況。

或者是activity銷毀時候清空隊列裡的消息,即在activity的onDestroy對handler中message進行removeCallbacksAndMessages

回到圖檔架構,軟引用的設計就是應用于會發生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;
        }
    };           

複制

方法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 像素存儲位置考慮

虛拟機的記憶體劃分主要有5部分:

虛拟機棧

本地方法棧

程式計數器

方法區

對象的配置設定一般都是在堆中,堆是JVM中最大的一塊記憶體,OOM一般都是發生在堆中。

Bitmap 之是以占記憶體大不是因為對象本身大,而是因為Bitmap的像素資料, Bitmap的像素資料大小 = 寬 * 高 * 1像素占用的記憶體。

Bitmap使用 RGB_565 格式,則1像素占用 2 byte,ARGB_8888 格式則占4 byte。

Glide記憶體開銷是Picasso的一半,就是因為預設Bitmap格式不同。

如果 BitmapFactory.Options 中指定 inJustDecodeBounds 為true,則為原圖寬高,如果是false,則是縮放後的寬高。是以我們一般可以通過壓縮來減小Bitmap像素占用記憶體。

Android 3.0到8.0 之間Bitmap像素資料存在Java堆,而8.0之後像素資料存到native堆中

主要兩個步驟:

1.申請記憶體,建立native層Bitmap,native層的Bitmap資料(像素資料)是存在native堆中

2.建立java 層Bitmap

通過JNI建立Java層Bitmap對象

8.0 的Bitmap建立就兩個點:

1.建立native層Bitmap,在native堆申請記憶體。

2.通過JNI建立java層Bitmap對象,這個對象在java堆中配置設定記憶體。

像素資料是存在native層Bitmap,也就是證明8.0的Bitmap像素資料存在native堆中。

7.0 像素記憶體的配置設定是這樣的:

1.通過JNI調用java層建立一個數組

2.然後建立native層Bitmap,把數組的位址傳進去。

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

說說final、finally、finalize 的關系

finalize:垃圾回收器确認這個對象沒有其它地方引用到它的時候,會調用這個對象的finalize方法,子類可以重寫這個方法,做一些釋放資源的操作。

在6.0以前,Bitmap 就是通過這個finalize 方法來釋放native層對象的。

在Bitmap構造方法建立了一個 BitmapFinalizer類,重寫finalize 方法,在java層Bitmap被回收的時候,BitmapFinalizer 對象也會被回收,finalize 方法肯定會被調用,在裡面釋放native層Bitmap對象。

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;
            }
        }
    }           

複制

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);
    }           

複制

不管是BitmapFinalizer 還是NativeAllocationRegistry,目的都是在java層Bitmap被回收的時候,将native層Bitmap對象也回收掉。 一般情況下我們無需手動調用recycle方法,由GC去盤它即可。

Android 8.0 之後Bitmap像素記憶體放在native堆,Bitmap導緻OOM的問題基本不會在8.0以上裝置出現了(沒有記憶體洩漏的情況下)

Fresco 的優點是:“在5.0以下(最低2.3)系統,Fresco将圖檔放到一個特别的記憶體區域(Ashmem區)” 這個Ashmem區是一塊匿名共享記憶體,Fresco 将Bitmap像素放到共享記憶體去了,共享記憶體是屬于native堆記憶體。

4.4以下,Fresco 使用匿名共享記憶體來儲存Bitmap資料,首先将圖檔資料拷貝到匿名共享記憶體中,然後使用Fresco自己寫的加載Bitmap的方法。

Fresco對不同Android版本使用不同的方式去加載Bitmap,至于4.4-5.0,5.0-8.0,8.0 以上,對應另外三個解碼器

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一緻。

線程池任務過多

清單滑動,會有很多圖檔請求,如果是第一次進入,沒有緩存,那麼隊列會有很多任務在等待。是以在請求網絡圖檔之前,需要判斷隊列中是否已經存在該任務,存在則不加到隊列去。