", "(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層的方法,
```
//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](https://link.juejin.cn/?target=https%3A%2F%2Fwww.androidos.net.cn%2Fandroid%2F7.0.0_r31%2Fxref%2Fframeworks%2Fbase%2Fcore%2Fjni%2Fandroid%2Fgraphics%2FGraphics.cpp "https://www.androidos.net.cn/android/7.0.0_r31/xref/frameworks/base/core/jni/android/graphics/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.
*
...**/
protected void finalize() throws Throwable { }
```
意思是說,垃圾回收器確認這個物件沒有其它地方引用到它的時候,會呼叫這個物件的`finalize`方法,子類可以重寫這個方法,做一些釋放資源的操作。
**在6.0以前,Bitmap 就是透過這個finalize 方法來釋放native層物件的。** [6.0 Bitmap.java](https://link.juejin.cn/?target=https%3A%2F%2Fwww.androidos.net.cn%2Fandroid%2F6.0.1_r16%2Fxref%2Fframeworks%2Fbase%2Fgraphics%2Fjava%2Fandroid%2Fgraphics%2FBitmap.java "https://www.androidos.net.cn/android/6.0.1_r16/xref/frameworks/base/graphics/java/android/graphics/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 以下裝置怎麼辦?趕緊升級或換手機吧~
![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/11/3/16e3183d9ec9fa30~tplv-t2oaga2asx-watermark.image)
我們換手機當然沒問題,但是並不是所有人都能跟上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 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、隊滿任務存在則不新增