Android系統源碼分析-Bitmap系列

概述

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 的原理與設計思想

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章