概述
Bitmap 在我們日常開發過程中使用頻次非常高,因爲和它經常關聯的關鍵詞要麼是圖片,要麼就是內存,有時甚至還會談到OOM。大家在談論關於內存優化,一定繞不開關於Bitmap 的使用優化。因此今天就來和大家聊聊Bitmap 的源碼,瞭解它,所謂知彼知己,百戰不殆。再次重申,看源碼一定要有目的性,否則你一定很難堅持下去。我的目的,1、學習源碼的設計精髓,2、解BUG(或者說避免開發階段踩坑),其中2的佔比比較多,哈哈哈哈。
源碼分析
進入正文前,我先問一個小問題,請問創建一個Bitmap對象有那幾種方式?答:兩種:一種是使用BitmapFactory類去加載,另一種是使用Bitmap類加載。【本文所有的Android 代碼使用的版本的是 Android P 即API 28 】
1、BitmapFactory
這個類的作用按照官方的註釋描述是“從各種源創建位圖對象,包括文件、流和字節數組。”這段註釋說使用這個類可以以三種方式加載Bitmap,然而實際上,這個類提供了5種方式,文件、流、字節數組、文件描述符以及資源ID。(官方註釋有點不嚴謹啊)對應的加載源碼如下:
BitmapFactory.decodeFile();
BitmapFactory.decodeByteArray();
BitmapFactory.decodeStream();
BitmapFactory.decodeResource();
BitmapFactory.decodeFileDescriptor();
上面的五種方式中,每一種又都可以細拆分出兩個亞種(借用下生物學分類名詞),一個亞種是方法僅一個參數,例如文件、資源ID、流等(decodeByteArray特殊)。第二個亞種就是方法是兩個參數,相當於重載了第一個亞種,擴展第二個參數是Options。這個Options 相信大家不陌生,我們在做圖片壓縮時,經常看到這個傢伙,後面會聊。
BitmapFactory.decodeFile();
BitmapFactory.decodeStream();
這兩種方式,歸根結底,從最終的執行解析的方法上來說,算是同一種方式,傳入的文件路徑,最終還是要轉換成文件輸入流,然後調用如下方法:
@Nullable
public static Bitmap decodeStream(@Nullable InputStream is, @Nullable Rect outPadding,
@Nullable Options opts) {
.....
try {
if (is instanceof AssetManager.AssetInputStream) {
final long asset = ((AssetManager.AssetInputStream) is).getNativeAsset();
bm = nativeDecodeAsset(asset, outPadding, opts);
} else {
bm = decodeStreamInternal(is, outPadding, opts);
}
......
setDensityFromOptions(bm, opts);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS);
}
return bm;
}
最終會調用Native 層的一個方法去解碼輸入流:
private static native Bitmap nativeDecodeStream(InputStream is, byte[] storage,
Rect padding, Options opts);
谷歌根據不同的不同的輸入資源,提供了五種不同的解析並創建Bitmapd的Native解析方法,分別是:
// 解析輸入流
private static native Bitmap nativeDecodeStream(InputStream is, byte[] storage,Rect padding, Options opts);
// 解析文件描述符
private static native Bitmap nativeDecodeFileDescriptor(FileDescriptor fd,Rect padding, Options opts);
// 解析asset 資源
private static native Bitmap nativeDecodeAsset(long nativeAsset, Rect padding, Options opts);
// 解析字節數組
private static native Bitmap nativeDecodeByteArray(byte[] data, int offset,int length, Options opts);
// 這個方法也是在文件描述符上調用的,具體作用我也不知道???
private static native boolean nativeIsSeekable(FileDescriptor fd);
So,其實這幾個Native方法纔是源頭。
Native 的方法比較多,而且我本人對C++不是很熟悉,這裏僅貼一小部分源碼分析,不會大段講解。以nativeDecodeStream 爲例:
static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage,
jobject padding, jobject options) {
//創建一個bitmap對象
jobject bitmap = NULL;
//創建一個輸入流適配器,(SkAutoTUnref)自解引用
SkAutoTUnref<SkStream> stream(CreateJavaInputStreamAdaptor(env, is, storage));
if (stream.get()) {
SkAutoTUnref<SkStreamRewindable> bufferedStream(
SkFrontBufferedStream::Create(stream, BYTES_TO_BUFFER));
SkASSERT(bufferedStream.get() != NULL);
//圖片解碼
bitmap = doDecode(env, bufferedStream, padding, options);
}
//返回圖片對象,加載失敗的時候返回空
return bitmap;
}
如果大家對Native 層很感興趣,可以自行讀源碼,位置如下:
frameworks\base\core\jni\android\graphics
這裏我想補充一點關於使用文件描述符加載Bitmap 方法。這個用的不是很多,比較常見的場景就是加載Raw文件夾下的圖片,我這裏寫下實現方式:
AssetFileDescriptor assetFileDescriptor = getResources().openRawResourceFd(R.raw.guide);
Bitmap bitmap = BitmapFactory.decodeFileDescriptor(assetFileDescriptor.getFileDescriptor());
imageView.setImageBitmap(bitmap);
這個類裏還有一個非常重要知識點就是Options類,在日常的開發中,我們通常需要避免圖片過大,導致的OOM,因此需要對圖片進行壓縮,例如:
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(stream, null, options);
if (options.outWidth < 1 || options.outHeight < 1) {
return null;
}
options.inSampleSize = calculateOriginal(options,mImageRect.width(), mImageRect.height());
options.inJustDecodeBounds = false;
Bitmap mBitmap = BitmapFactory.decodeStream(is, null, options)
這段代碼大家一定很熟悉,我們使用了Options,同時還將這個類對象作爲參數傳入decodeStream方法中,去創建Bitmap。接下來我將聊聊這個類,以及用這個對象幹了什麼。
Options對圖片進行解碼時使用的一個配置參數類,其中定義了一系列的public成員變量,每個成員變量代表一個配置參數。參數比較多,這裏說下比較常用的:
inJustDecodeBounds
按照官方註釋:如果設置爲true,解碼器將返回null(無位圖),但是仍將設置out…
字段,允許調用者查詢位圖而不必爲其像素分配內存。
說白是就是這如果爲true,Bitmap 並不會加載到內存中,但是卻可以讓你拿到關於這張圖的信息,比如outWidth、outHeight(即圖片的寬高)
inSampleSize
註釋很長,我來白話下,首先這個屬性的作用就是用來設置圖片縮放比的,如果設置值爲N且大於1,就是原圖大小分辨率的1/N,這個值如果小於或者等於1,那麼就是原圖大小,沒有任何縮放。通常我們會跟進設定的目標寬高/原圖的寬高來計算這個值。
inMutable
表示該bitmap緩存是否可變,如果設置爲true,將可被inBitmap複用。這個值尤爲要注意下,默認是false的,如果我們沒有設置這個值爲true就去改變Bitmap 的寬高,那一定是會崩潰的。而使用
Bitmap.createBitmap()
創建Bitmap時,在初始化階段也會設置一個類似於inMutable的值,這個我們後面還會提到。
inPreferredConfig
表示圖片解碼時使用的顏色模式,一共有四個模式,默認是ARGB_8888
- ALPHA_8: 每個像素用佔8位,存儲的是圖片的透明值,佔1個字節
- RGB_565:每個像素用佔16位,分別爲5-R,6-G,5-B通道,佔2個字節
- ARGB-4444:每個像素佔16位,即每個通道用4位表示,佔2個字節
- ARGB_8888:每個像素佔32位,每個通道用8位表示,佔4個字節
其中ARGB_8888 佔據的內存最大,如果我們對圖片的清晰度要求不高,使用RGB_565就夠用了,而且還可以降低內存的使用從而減少OOM的發生。如果需要圖片支持透明度,那就只能ARGB_8888了。
inDensity : 位圖使用的像素密度
inScreenDensity: 正在使用的實際屏幕的像素密度
inTargetDensity: 設備的屏幕密度
最後再來聊聊這哥撒屬性,之所以放在一起是因爲他們都和像素密度有關。這三個屬性官方給的註釋特多,特詳細。
先說下inDensity 這個值,bitmap的像素密度爲屏幕默認像素密度,也就是說如果這個值爲0,系統就會設置一個默認值,這個值在DisplayMetrics 類中:
/**
* Standard quantized DPI for medium-density screens.
*/
public static final int DENSITY_MEDIUM = 160;
而inTargetDensity,有這麼一個方法可以看出它和inDensity的關係:
/**
* Set the newly decoded bitmap's density based on the Options.
*/
private static void setDensityFromOptions(Bitmap outputBitmap, BitmapFactory.Options opts) {
if (outputBitmap == null || opts == null) return;
final int density = opts.inDensity;
if (density != 0) {
outputBitmap.setDensity(density);
final int targetDensity = opts.inTargetDensity;
if (targetDensity == 0 || density == targetDensity || density == opts.inScreenDensity) {
return;
}
byte[] np = outputBitmap.getNinePatchChunk();
final boolean isNinePatch = np != null && NinePatch.isNinePatchChunk(np);
// 是不是.9 的圖,且當前圖片可以縮放
if (opts.inScaled || isNinePatch) {
// 設置當前的像素爲設備密度像素
outputBitmap.setDensity(targetDensity);
}
} else if (opts.inBitmap != null) {
// bitmap was reused, ensure density is reset
// 使用系統的默認發
outputBitmap.setDensity(Bitmap.getDefaultDensity());
}
}
這裏需要注意關於.9 圖的判斷部分。
關於Options類的梳理就先到這,最後在講下這個類是怎麼關聯到Bitmap 的創建的。舉個例子,以傳入輸入流的方式解析,會調用的Native的方法:
private static native Bitmap nativeDecodeStream(InputStream is, byte[] storage,
Rect padding, Options opts);
注意最後一個參數,我們會將Options傳入到Native層解析。那看下Native層的處理邏輯:
位於:frameworks\base\core\jni\android\graphics\BitmapFactory.cpp
/**
* JNIEnv* env jni指針
* SkStreamRewindable* stream 流對象
* jobject padding 邊距對象
* jobject options 圖片選項參數對象
*/
static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
//縮放值,默認不縮放
int sampleSize = 1;
//圖片解碼模式,像素點模式
SkImageDecoder::Mode decodeMode = SkImageDecoder::kDecodePixels_Mode;
...//省略參數初始化
//javabitmap對象
jobject javaBitmap = NULL;
//對於options的參數選項初始化
if (options != NULL) {
//獲得參數中是否需要縮放
sampleSize = env->GetIntField(options, gOptions_sampleSizeFieldID);
if (optionsJustBounds(env, options)) {
//確定現在的圖片解碼模式
//在java中可以設置inJustDecodeBounds參數
//public boolean inJustDecodeBounds;true的時候,只會去加載bitmap的大小
decodeMode = SkImageDecoder::kDecodeBounds_Mode;
}
//圖片的配置相關參數
jobject jconfig = env->GetObjectField(options, gOptions_configFieldID);
//判斷是否需要縮放
if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
const int density = env->GetIntField(options, gOptions_densityFieldID);
const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
if (density != 0 && targetDensity != 0 && density != screenDensity) {
//計算出縮放比列
scale = (float) targetDensity / density;
}
}
}
//通過縮放比例判斷是否需要縮放
const bool willScale = scale != 1.0f;
//解碼器創建
SkImageDecoder* decoder = SkImageDecoder::Factory(stream);
if (decoder == NULL) {
return nullObjectReturn("SkImageDecoder::Factory returned null");
}
//解碼器參數設置
decoder->setSampleSize(sampleSize);
decoder->setDitherImage(doDither);
decoder->setPreferQualityOverSpeed(preferQualityOverSpeed);
decoder->setRequireUnpremultipliedColors(requireUnpremultiplied);
//加載像素的分配器
JavaPixelAllocator javaAllocator(env);
if (options != NULL && env->GetBooleanField(options, gOptions_mCancelID)) {
//用於取消的時候使用
return nullObjectReturn("gOptions_mCancelID");
}
//解碼
SkBitmap decodingBitmap;
if (decoder->decode(stream, &decodingBitmap, prefColorType, decodeMode)
!= SkImageDecoder::kSuccess) {
return nullObjectReturn("decoder->decode returned false");
}
//縮放後的大小,decodingBitmap.width()是默認是圖片大小
int scaledWidth = decodingBitmap.width();
int scaledHeight = decodingBitmap.height();
//縮放
if (willScale && decodeMode != SkImageDecoder::kDecodeBounds_Mode) {
scaledWidth = int(scaledWidth * scale + 0.5f);
scaledHeight = int(scaledHeight * scale + 0.5f);
}
if (options != NULL) {
//更新選項參數
}
// justBounds mode模式下,直接返回,不繼續加載
if (decodeMode == SkImageDecoder::kDecodeBounds_Mode) {
return NULL;
}
//點九圖片相關
jbyteArray ninePatchChunk = NULL;
if (peeker.mPatch != NULL) {
env->ReleasePrimitiveArrayCritical(ninePatchChunk, array, 0);
}
jobject ninePatchInsets = NULL;
if (peeker.mHasInsets) {
if (javaBitmap != NULL) {
env->SetObjectField(javaBitmap, gBitmap_ninePatchInsetsFieldID, ninePatchInsets);
}
}
//縮放操作
if (willScale) {
canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
} else {
outputBitmap->swap(decodingBitmap);
//swap交換,底層實現是交換對象 指針,並不是深拷貝
}
//邊距處理
if (padding) {
if (peeker.mPatch != NULL) {
GraphicsJNI::set_jrect(env, padding,
peeker.mPatch->paddingLeft, peeker.mPatch->paddingTop,
peeker.mPatch->paddingRight, peeker.mPatch->paddingBottom);
} else {
GraphicsJNI::set_jrect(env, padding, -1, -1, -1, -1);
}
}
//如果已經可以 了 就直接返回
if (javaBitmap != NULL) {
bool isPremultiplied = !requireUnpremultiplied;
GraphicsJNI::reinitBitmap(env, javaBitmap, outputBitmap, isPremultiplied);
outputBitmap->notifyPixelsChanged();
// If a java bitmap was passed in for reuse, pass it back
return javaBitmap;
}
//創建bitmap對象返回
return GraphicsJNI::createBitmap(env, outputBitmap, javaAllocator.getStorageObj(),
bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
}
這段代碼很長,註釋也比較詳盡。我來總結下:
- 1、options的參數選項初始化以及參數配置,這裏還給出了計算縮放比例的公式:
scale = (float) targetDensity / density;
- 2、解碼器的初始化設置
- 3、加載像素分配器
- 4、解碼
- 5、處理縮放的相關業務
- 6、處理.9 圖
- 7、創建bitmap對象返回
2、Bitmap
使用Bitmap類提供的方法創建通常會調用
Bitmap bitmap1 = Bitmap.createBitmap(int width, int height, Config config)
這個方法被重載的非常多,根據不同的輸入參數,一共有15個,比樓上的BitmapFactory還多5個,但他們總的設計思路是一樣的,就是你Java層只是做Bitmap 的參數的預處理,真正幹核心工作的仍然在Native層。也就是下面的這個方法:
private static native Bitmap nativeCreate(int[] colors, int offset,
int stride, int width, int height,
int nativeConfig, boolean mutable,
@Nullable @Size(9) float[] xyzD50,
@Nullable ColorSpace.Rgb.TransferParameters p);
可以說關於的Bitmap的很多核心業務方法都是在native層處理的,比如Bitmap的壓縮、複製、獲取像素、設置像素以及回收等。我們先來分析下Java層的業務,最後再來看看native層的方法(由於方法太多,這裏僅講個人認爲核心的部分)
Java 層
在開頭處我說過,關於createBitmap 方法被重載了15個,看起來非常多,但只要大家看下這些方法的調用鏈,最終在Java層的終點就兩個方法,因此可以把這15個方法分成兩類。第一類代碼如下:
public static Bitmap createBitmap(@Nullable DisplayMetrics display, int width, int height,
@NonNull Config config, boolean hasAlpha, @NonNull ColorSpace colorSpace) {
if (width <= 0 || height <= 0) {
throw new IllegalArgumentException("width and height must be > 0");
}
if (config == Config.HARDWARE) {
throw new IllegalArgumentException("can't create mutable bitmap with Config.HARDWARE");
}
if (colorSpace == null) {
throw new IllegalArgumentException("can't create bitmap without a color space");
}
Bitmap bm;
// nullptr color spaces have a particular meaning in native and are interpreted as sRGB
// (we also avoid the unnecessary extra work of the else branch)
if (config != Config.ARGB_8888 || colorSpace == ColorSpace.get(ColorSpace.Named.SRGB)) {
bm = nativeCreate(null, 0, width, width, height, config.nativeInt, true, null, null);
} else {
if (!(colorSpace instanceof ColorSpace.Rgb)) {
throw new IllegalArgumentException("colorSpace must be an RGB color space");
}
ColorSpace.Rgb rgb = (ColorSpace.Rgb) colorSpace;
ColorSpace.Rgb.TransferParameters parameters = rgb.getTransferParameters();
if (parameters == null) {
throw new IllegalArgumentException("colorSpace must use an ICC "
+ "parametric transfer function");
}
ColorSpace.Rgb d50 = (ColorSpace.Rgb) ColorSpace.adapt(rgb, ColorSpace.ILLUMINANT_D50);
bm = nativeCreate(null, 0, width, width, height, config.nativeInt, true,
d50.getTransform(), parameters);
}
if (display != null) {
bm.mDensity = display.densityDpi;
}
bm.setHasAlpha(hasAlpha);
if ((config == Config.ARGB_8888 || config == Config.RGBA_F16) && !hasAlpha) {
nativeErase(bm.mNativePtr, 0xff000000);
}
// No need to initialize the bitmap to zeroes with other configs;
// it is backed by a VM byte array which is by definition preinitialized
// to all zeroes.
return bm;
}
這個方法應用面非常廣,15個重載方法,有10個最終會執行到這個方法,因此咱們需要先來看看這個。
第一個參數DisplayMetrics 表示將繪製此位圖的顯示器的顯示度量,這個值可以爲空,有專門的重載方法傳入。後面兩個參數int width, int height指的是Bitmap的寬度和高度,沒有傳的話的,直接用bm.getWidth(), bm.getHeight(),注意,這兩個值必要有!第四個參數Config,樓上在分析BitmapFactory類時,提到過一個Options類裏的這麼一個參數inPreferredConfig,表示在圖片解碼時使用的顏色模式,這個參數在Native層解碼時用到,這裏的Config參數的作用和inPreferredConfig一樣。最後一個參數colorSpace表示當前顏色的空間範圍。這裏面一大段除了前面一些必要的處理,也就中間那個if語句了,主要還是於處理Config配置,如果我們沒有自定義Config,那麼系統就會幫我們創建一個,同時顏色模式默認就是ARGB_8888,至於後面的那個,顏色範圍,如果自己沒啥要求,系統默認也是幫你設定好的,例如:
public static Bitmap createBitmap(@Nullable DisplayMetrics display, int width, int height,
@NonNull Config config, boolean hasAlpha) {
return createBitmap(display, width, height, config, hasAlpha,
ColorSpace.get(ColorSpace.Named.SRGB));
}
後面的else的一大段,還是判斷顏色空間。這裏我還是簡單解釋下,SRGB。“sRGB”意爲“標準 RGB 色彩空間”,這一標準應用的範圍十分廣泛,其他許許多多的硬件及軟件開發商也都採用了sRGB色彩空間作爲其產品的色彩空間標準,這個標準現在被各大顯示器廣泛使用。基本上不會去執行else裏的代碼。然後就進入native層了。
接下來就是第二類,這個方法用的不多,我把源碼貼出來簡單說下:
public static Bitmap createBitmap(@NonNull DisplayMetrics display,
@NonNull @ColorInt int[] colors, int offset, int stride,
int width, int height, @NonNull Config config) {
checkWidthHeight(width, height);
if (Math.abs(stride) < width) {
throw new IllegalArgumentException("abs(stride) must be >= width");
}
int lastScanline = offset + (height - 1) * stride;
int length = colors.length;
if (offset < 0 || (offset + width > length) || lastScanline < 0 ||
(lastScanline + width > length)) {
throw new ArrayIndexOutOfBoundsException();
}
if (width <= 0 || height <= 0) {
throw new IllegalArgumentException("width and height must be > 0");
}
Bitmap bm = nativeCreate(colors, offset, stride, width, height,
config.nativeInt, false, null, null);
if (display != null) {
bm.mDensity = display.densityDpi;
}
return bm;
}
根據註釋,描述返回具有指定寬度和高度的不可變位圖,每個像素值都設置爲顏色數組中的相應值。其他參數和第一類沒啥區別,也就是colors[]特別,需要自己往裏面傳像素值,我也不知道哪些場景能用到。
整個Bitmap類(Java層)大部分代碼都是和創建Bitmap有關係。
Native 層
先來分析下recycle方法。有的時候,我們爲了避免OOM,會自作聰明的手動調用這個recycle方法,然後代碼一執行程序就稀裏糊塗的崩潰了。。。。。。詳情可以看看老徐寫的《Bitmap.recycle引發的血案》
先來看下Java層裏的recycle方法的代碼:
public void recycle() {
if (!mRecycled && mNativePtr != 0) {
if (nativeRecycle(mNativePtr)) {
// return value indicates whether native pixel object was actually recycled.
// false indicates that it is still in use at the native level and these
// objects should not be collected now. They will be collected later when the
// Bitmap itself is collected.
mNinePatchChunk = null;
}
mRecycled = true;
}
}
行數很少,註釋卻一大堆,官方生怕我們這幫開發者把這個方法用錯了,這個設計思路不錯,以後自己寫代碼,如果有非常特殊的功能或者方法實現,一定要把註釋寫的非常非常的清楚!!!
我們知道,Bitmap的存儲分爲兩部分,一部分是Bitmap的像素數據,另一部分是Bitmap的引用。在Android2.3時代,Bitmap的引用是放在堆中的,而Bitmap的數據部分是放在棧中的,需要用戶調用recycle方法手動進行內存回收,而在Android2.3之後,整個Bitmap,包括數據和引用,都放在了堆中,這樣,整個Bitmap的回收就全部交給GC了,這個recycle方法就再也不需要使用了。既然官方都建議不要手動調用了,這個方法爲啥還不hide下或者拉入黑名單?我現在手上的是Android P的源碼,不知道以後谷歌會不會改這個方法。我們來看看Native的方法:
位置:frameworks\base\core\jni\android\Bitmap.cpp
static void Bitmap_recycle(JNIEnv* env, jobject, jlong bitmapHandle) {
LocalScopedBitmap bitmap(bitmapHandle);
bitmap->freePixels();
}
void freePixels() {
mInfo = mBitmap->info();
mHasHardwareMipMap = mBitmap->hasHardwareMipMap();
mAllocationSize = mBitmap->getAllocationByteCount();
mRowBytes = mBitmap->rowBytes();
mGenerationId = mBitmap->getGenerationID();
mIsHardware = mBitmap->isHardware();
mBitmap.reset();
}
這裏面最重要的就是 mBitmap.reset();清空數據,然而大家需要注意的是,這裏僅釋放了像素部分的數據(看方法名稱),並沒有提到Bitmap 的引用的釋放,那這部分咋回收 ?
在Bitmap.java 中,有個方法:
Bitmap(long nativeBitmap,...){
// 省略其他代碼...
// 分析點 1:native 層需要的內存大小
long nativeSize = NATIVE_ALLOCATION_SIZE + getAllocationByteCount();
// 分析點 2:回收函數 nativeGetNativeFinalizer()
// 分析點 3:加載回收函數的類加載器:Bitmap.class.getClassLoader()
NativeAllocationRegistry registry = new NativeAllocationRegistry(
Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), nativeSize);
// 註冊 Java 層對象引用與 native 層對象的地址
registry.registerNativeAllocation(this, nativeBitmap);
}
這個方法中註冊了NativeAllocationRegistry對象,這個類如果沒有下載Android源碼,是看不到的,具體位置在:
libcore/luni/src/main/java/libcore/util/NativeAllocationRegistry.java
作用就是Android 8.0引入的一種輔助自動回收native內存的一種機制,當Java對象因爲GC被回收後,NativeAllocationRegistry可以輔助回收Java對象所申請的native內存。
這裏需要補充一句, 2.3之前的像素存儲需要的內存是在native上分配的,並且生命週期不太可控,可能需要用戶自己回收。 2.3-7.1之間,Bitmap的像素存儲在Dalvik的Java堆上,當然,4.4之前的甚至能在匿名共享內存上分配(Fresco採用),而8.0之後的像素內存又重新回到native上去分配,不需要用戶主動回收,8.0之後圖像資源的管理更加優秀,極大降低了OOM。我們看剛剛那段Native 層的Bitmap_recycle方法就能知道。
這個類中有兩個方法需要重點看下,先看第一個:
public Runnable registerNativeAllocation(Object referent, long nativePtr) {
if (referent == null) {
throw new IllegalArgumentException("referent is null");
}
if (nativePtr == 0) {
throw new IllegalArgumentException("nativePtr is null");
}
CleanerThunk thunk;
CleanerRunner result;
try {
thunk = new CleanerThunk();
Cleaner cleaner = Cleaner.create(referent, thunk);
result = new CleanerRunner(cleaner);
registerNativeAllocation(this.size);
} catch (VirtualMachineError vme /* probably OutOfMemoryError */) {
applyFreeFunction(freeFunction, nativePtr);
throw vme;
} // Other exceptions are impossible.
// Enable the cleaner only after we can no longer throw anything, including OOME.
thunk.setNativePtr(nativePtr);
return result;
}
這個方法是在我們剛看關於NativeAllocationRegistry創建的地方,註冊 Java 層對象引用與 native 層對象的地址。裏面最重要的地方就是:
thunk = new CleanerThunk();
Cleaner cleaner = Cleaner.create(referent, thunk);
result = new CleanerRunner(cleaner);
registerNativeAllocation(this.size);
其中CleanerThunk是一個子線程,用來釋放Native層的內存。而Cleaner類,繼承自PhantomReference(虛引用或者幻象引用),根據註釋和代碼,它作用是專門用於監控無法被JVM釋放的內存,利用虛引用(PhantomReference)和ReferenceQueue來監控一個對象是否存在強引用。虛引用不影響對象任何的生命週期,當這個對象不具有強引用的時候,JVM會將這個對象加入與之關聯的ReferenceQueue。我之前一直不明白虛引用的應用場景,通過這個類,總算讓我大開眼界了。
接下來就是做了兩件事:1、使用Cleaner 綁定 Java 對象與回收函數,2、註冊 native 內存。最後在子線程回收內存,run()在Java層對象被垃圾回收時觸發。
private class CleanerThunk implements Runnable {
private long nativePtr;
public CleanerThunk() {
this.nativePtr = 0;
}
public void run() {
if (nativePtr != 0) {
applyFreeFunction(freeFunction, nativePtr);
registerNativeFree(size);
}
}
public void setNativePtr(long nativePtr) {
this.nativePtr = nativePtr;
}
}
這個方法中,調用applyFreeFunction,這個方法會調用Native層的內存回收,然後在調用registerNativeFree(size);註銷 native 內存。
寫到這裏,關於Bitmap源碼梳理完成,當然,這些僅僅只是源碼的一部分,源碼實在太多,橫跨Java層和Native層,閱讀起來着實辛苦,不過收穫還是很多的。如有錯誤,還請指正,非常感謝。
總結:
在梳理Bitmap源碼時,有兩個感慨,一個就是Android 官方現在將越來越多的功能放在了Native層去實現,所以,想要更深刻的理解安卓源碼,C層代碼有必要看看。另一個就是註釋,谷歌給Options裏的成員變量寫了大量的註釋,非常詳盡。我自己也寫,但是在給一個成員變量寫註釋,我最多不超過20個字,關於註釋的書寫,我一直在努力改善。記得當時在看OKHttp3 源碼時,那裏面的註釋寫的真是漂亮。
預告下,既然提到了OK3,我去年就整理過一些關於OK3 的源碼梳理,接下來我會整理思路,準備在這分享。
補充:
分享一個方便下載、看源碼的網站:
Android系統所有版本源碼
參考:
Android系統源碼分析-bitmap的加載
淺談BitmapFactory.Options
最詳細的Android Bitmap回收機制(從2.3到7.0,8.0)
Android Bitmap變遷與原理解析(4.x-8.x)
Android | 帶你理解 NativeAllocationRegistry 的原理與設計思想