這次來面試的是一個有著5年工作經驗的小伙,截取了一段對話如下:
面試官:我看你寫到Glide,為什么用Glide,而不選擇其它圖片加載框架?
小伙:Glide 使用簡單,鏈式調用,很方便,一直用這個。
面試官:有看過它的源碼嗎?跟其它圖片框架相比有哪些優勢?
小伙:沒有,只是在項目中使用而已~
面試官:假如現在不讓你用開源庫,需要你自己寫一個圖片加載框架,你會考慮哪些方面的問題,說說大概的思路。
小伙:額~,壓縮吧。
面試官:還有嗎?
小伙:額~,這個沒寫過。
說到圖片加載框架,大家最熟悉的莫過于Glide了,但我卻不推薦簡歷上寫熟悉Glide,除非你熟讀它的源碼,或者參與Glide的開發和維護。
在一般面試中,遇到圖片加載問題的頻率一般不會太低,只是問法會有一些差異,例如:
- 簡歷上寫Glide,那么會問一下Glide的設計,以及跟其它同類框架的對比 ;
- 假如讓你寫一個圖片加載框架,說說思路;
- 給一個圖片加載的場景,比如網絡加載一張或多張大圖,你會怎么做;
帶著問題進入正文~
一、談談Glide
1.1 Glide 使用有多簡單?
Glide由于其口碑好,很多開發者直接在項目中使用,使用方法相當簡單
https://github.com/bumptech/glide
1、添加依賴:
implementation 'com.github.bumptech.glide:glide:4.10.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.10.0'
2、添加網絡權限
<uses-permission android:name="android.permission.INTERNET" />
3、一句代碼加載圖片到ImageView
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,內存不會一直往上漲。
二、假如讓你自己寫個圖片加載框架,你會考慮哪些問題?
首先,梳理一下必要的圖片加載框架的需求:
- 異步加載:線程池
- 切換線程: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的數組加鏈表的結構,
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);
// 1、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小結:
- 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;
...
}
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));
}
主要兩個步驟:
- 申請內存,創建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堆中。
- 創建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創建就兩個點:
- 創建native層Bitmap,在native堆申請內存。
- 通過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 像素內存的分配是這樣的:
- 通過JNI調用java層創建一個數組
- 然后創建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以下都用匿名共享內存不可以嗎?期待你主動學習并且跟大家分享~
就這樣,有問題評論區留言~