圖形圖像處理 - 我們所不知道的 Bitmap

  • Bitmap 是怎麼開闢內存的?內存是怎麼複用和銷燬的?本地資源圖片應該怎麼去做適配?
  • 打開我們自己的 APP 發現佔用內存較大的一般都是本地資源圖片,我們該如何去優化這些內存?
  • 大家以後如果有涉及直播這一塊的業務,直播間會有各種活動和各種複雜動畫,線上 buggly 肯定會有大量的 OOM ,我們怎樣才能在 OOM 前去 dump 線上內存來做優化分析?

Bitmap 我們是再熟悉不過了,首先拋幾個問題讓我們一起來思考一下,如果以上幾個問題大家都能找到解決方案,相信我們在以後的開發過程中會省下許多事。

1. Bitmap 的內存大小

不知大家是否還記得在 Android圖片壓縮加密上傳 - JPEG壓縮算法解析 這篇文章中有一個題目:一張 864x582 的 PNG 圖片,我把它放到 drawable-xhdpi 目錄下,在紅米 Note3 上加載,佔用內存是多少(1920x1080像素 ,5.5英寸 )? 我們清晰的知道 圖片大小 = 寬 * 高 * 單個像素點所佔字節數,那麼這麼一算大小應該是 864x582x4 = 2011392 ,但最終調用代碼 Bitmap.getByteCount() 發現是 3465504。 難道我們剛剛所講的公式不對?其實這裏的寬高是 Bitmap 的寬高並不是資源圖片的寬高:

Bitmap 大小 = bitmap.getWidth() * bitmap.getHeight() * 單個像素點所佔字節數 = 1134 * 764 * 4 = 3465504。

那這裏的寬高是怎樣計算而來的?我們通過跟蹤源碼發現 width 和 height 都是在 Bitmap 的構造函數中賦值的:

    /**
     * Private constructor that must received an already allocated native bitmap
     * int (pointer).
     */
    // called from JNI
    Bitmap(long nativeBitmap, int width, int height, int density,
            boolean isMutable, boolean requestPremultiplied,
            byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {
        if (nativeBitmap == 0) {
            throw new RuntimeException("internal error: native bitmap is 0");
        }

        mWidth = width;
        mHeight = height;
        mIsMutable = isMutable;
        mRequestPremultiplied = requestPremultiplied;

        mNinePatchChunk = ninePatchChunk;
        mNinePatchInsets = ninePatchInsets;
        if (density >= 0) {
            mDensity = density;
        }

        mNativePtr = nativeBitmap;
        long nativeSize = NATIVE_ALLOCATION_SIZE + getAllocationByteCount();
        NativeAllocationRegistry registry = new NativeAllocationRegistry(
            Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), nativeSize);
        registry.registerNativeAllocation(this, nativeBitmap);

        if (ResourcesImpl.TRACE_FOR_DETAILED_PRELOAD) {
            sPreloadTracingNumInstantiatedBitmaps++;
            sPreloadTracingTotalBitmapsSize += nativeSize;
        }
    }

called from JNI 這個解釋其實已經很明確了,也就是說這個對象是 Native 層構建返回的。因此我們跟蹤到 BitmapFactory.decodeResource() 中去看看:

    public static Bitmap decodeResource(Resources res, int id, Options opts) {
        validate(opts);
        Bitmap bm = null;
        InputStream is = null; 
        
        try {
            final TypedValue value = new TypedValue();
            is = res.openRawResource(id, value);

            bm = decodeResourceStream(res, value, is, null, opts);
        } catch (Exception e) {
            /*  do nothing.
                If the exception happened on open, bm will be null.
                If it happened on close, bm is still valid.
            */
        } finally {
            try {
                if (is != null) is.close();
            } catch (IOException e) {
                // Ignore
            }
        }

        if (bm == null && opts != null && opts.inBitmap != null) {
            throw new IllegalArgumentException("Problem decoding into existing bitmap");
        }

        return bm;
    }

    public static Bitmap decodeResourceStream(@Nullable Resources res, @Nullable TypedValue value,
            @Nullable InputStream is, @Nullable Rect pad, @Nullable Options opts) {
        validate(opts);
        if (opts == null) {
            opts = new Options();
        }
       
        if (opts.inDensity == 0 && value != null) {
            final int density = value.density;
            if (density == TypedValue.DENSITY_DEFAULT) {
                opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
            } else if (density != TypedValue.DENSITY_NONE) {
                opts.inDensity = density;
            }
        }
        // 獲取當前手機設備的 dpi 
        if (opts.inTargetDensity == 0 && res != null) {
            opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
        }
        
        return decodeStream(is, pad, opts);
    }

    // 省略部分跟蹤代碼 ......

    private static native Bitmap nativeDecodeStream(InputStream is, byte[] storage,
            Rect padding, Options opts);

最終調用的是 native 方法 nativeDecodeStream ,這部分源碼建議大家在 http://androidxref.com/ 上看,因爲不同版本之間有差異,我們不能只看一個版本。當然也可以每個版本都去下載,但需要一百多G的內存。這裏以 Android N 版本爲例:

/frameworks/base/core/jni/android/graphics/BitmapFactory.cpp

static jobject nativeDecodeStream(JNIEnv *env, jobject clazz, jobject is, jbyteArray storage,
                                  jobject padding, jobject options) {
    jobject bitmap = NULL;
    std::unique_ptr<SkStream> stream(CreateJavaInputStreamAdaptor(env, is, storage));

    if (stream.get()) {
        std::unique_ptr<SkStreamRewindable> bufferedStream(
                SkFrontBufferedStream::Create(stream.release(), SkCodec::MinBufferedBytesNeeded()));
        SkASSERT(bufferedStream.get() != NULL);
        bitmap = doDecode(env, bufferedStream.release(), padding, options);
    }
    return bitmap;
}

static jobject doDecode(JNIEnv *env, SkStreamRewindable *stream, jobject padding, jobject options) {
    // This function takes ownership of the input stream.  Since the SkAndroidCodec
    // will take ownership of the stream, we don't necessarily need to take ownership
    // here.  This is a precaution - if we were to return before creating the codec,
    // we need to make sure that we delete the stream.
    std::unique_ptr<SkStreamRewindable> streamDeleter(stream);

    // Set default values for the options parameters.
    int sampleSize = 1;
    // 是否只是獲取圖片的大小
    bool onlyDecodeSize = false;
    SkColorType prefColorType = kN32_SkColorType;
    bool isMutable = false;
    float scale = 1.0f;
    bool requireUnpremultiplied = false;
    jobject javaBitmap = NULL;

    // Update with options supplied by the client.
    // 解析 options 參數
    if (options != NULL) {
        sampleSize = env->GetIntField(options, gOptions_sampleSizeFieldID);
        // Correct a non-positive sampleSize.  sampleSize defaults to zero within the
        // options object, which is strange.
        if (sampleSize <= 0) {
            sampleSize = 1;
        }

        if (env->GetBooleanField(options, gOptions_justBoundsFieldID)) {
            onlyDecodeSize = true;
        }

        // initialize these, in case we fail later on
        env->SetIntField(options, gOptions_widthFieldID, -1);
        env->SetIntField(options, gOptions_heightFieldID, -1);
        env->SetObjectField(options, gOptions_mimeFieldID, 0);
        // 解析 ColorType ,複用參數等等
        jobject jconfig = env->GetObjectField(options, gOptions_configFieldID);
        prefColorType = GraphicsJNI::getNativeBitmapColorType(env, jconfig);
        isMutable = env->GetBooleanField(options, gOptions_mutableFieldID);
        requireUnpremultiplied = !env->GetBooleanField(options, gOptions_premultipliedFieldID);
        javaBitmap = env->GetObjectField(options, gOptions_bitmapFieldID);
        // 計算縮放的比例
        if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
            // 獲取圖片當前 xhdpi 的 density
            const int density = env->GetIntField(options, gOptions_densityFieldID);
            // 獲取當前設備的 dpi
            const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
            const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
            if (density != 0 && targetDensity != 0 && density != screenDensity) {
                // scale = 當前設備的 dpi / xhdpi 的 density
                // scale = 420/320 = 1.3125
                scale = (float) targetDensity / density;
            }
        }
    }

    // Create the codec.
    NinePatchPeeker peeker;
    std::unique_ptr<SkAndroidCodec> codec(SkAndroidCodec::NewFromStream(streamDeleter.release(),
                                                                        280 & peeker));
    if (!codec.get()) {
        return nullObjectReturn("SkAndroidCodec::NewFromStream returned null");
    }

    // Do not allow ninepatch decodes to 565.  In the past, decodes to 565
    // would dither, and we do not want to pre-dither ninepatches, since we
    // know that they will be stretched.  We no longer dither 565 decodes,
    // but we continue to prevent ninepatches from decoding to 565, in order
    // to maintain the old behavior.
    if (peeker.mPatch && kRGB_565_SkColorType == prefColorType) {
        prefColorType = kN32_SkColorType;
    }
    // 獲取當前圖片的大小
    // Determine the output size.
    SkISize size = codec->getSampledDimensions(sampleSize);

    int scaledWidth = size.width();
    int scaledHeight = size.height();
    bool willScale = false;
    // 處理 simpleSize 壓縮,我們這裏沒穿,上面默認是 1 
    // Apply a fine scaling step if necessary.
    if (needsFineScale(codec->getInfo().dimensions(), size, sampleSize)) {
        willScale = true;
        scaledWidth = codec->getInfo().width() / sampleSize;
        scaledHeight = codec->getInfo().height() / sampleSize;
    }

    // Set the options and return if the client only wants the size.
    if (options != NULL) {
        jstring mimeType = encodedFormatToString(env, codec->getEncodedFormat());
        if (env->ExceptionCheck()) {
            return nullObjectReturn("OOM in encodedFormatToString()");
        }
        // 設置 options 對象中的 outWidth 和 outHeight
        env->SetIntField(options, gOptions_widthFieldID, scaledWidth);
        env->SetIntField(options, gOptions_heightFieldID, scaledHeight);
        env->SetObjectField(options, gOptions_mimeFieldID, mimeType);
        // 如果只是獲取大小直接 return null 這裏是 nullptr 而不是 NULL
        if (onlyDecodeSize) {
            return nullptr;
        }
    }

    // Scale is necessary due to density differences.
    if (scale != 1.0f) {
        willScale = true;
        // 計算 scaledWidth 和 scaledHeight
        // scaledWidth = 864 * 1.3125 + 0.5f = 1134 + 0.5f = 1134
        scaledWidth = static_cast<int>(scaledWidth * scale + 0.5f);
        // scaledHeight = 582 * 1.3125 + 0.5f = 763.875 + 0.5f = 764
        scaledHeight = static_cast<int>(scaledHeight * scale + 0.5f);
    }
    // 判斷是否有複用的 Bitmap
    android::Bitmap *reuseBitmap = nullptr;
    unsigned int existingBufferSize = 0;
    if (javaBitmap != NULL) {
        reuseBitmap = GraphicsJNI::getBitmap(env, javaBitmap);
        if (reuseBitmap->peekAtPixelRef()->isImmutable()) {
            // 無法重用一個不變的位圖圖像解碼器的目標。
            ALOGW("Unable to reuse an immutable bitmap as an image decoder target.");
            javaBitmap = NULL;
            reuseBitmap = nullptr;
        } else {
            existingBufferSize = GraphicsJNI::getBitmapAllocationByteCount(env, javaBitmap);
        }
    }
    
    JavaPixelAllocator javaAllocator(env);
    RecyclingPixelAllocator recyclingAllocator(reuseBitmap, existingBufferSize);
    ScaleCheckingAllocator scaleCheckingAllocator(scale, existingBufferSize);
    SkBitmap::HeapAllocator heapAllocator;
    SkBitmap::Allocator *decodeAllocator;
    if (javaBitmap != nullptr && willScale) {
        // This will allocate pixels using a HeapAllocator, since there will be an extra
        // scaling step that copies these pixels into Java memory.  This allocator
        // also checks that the recycled javaBitmap is large enough.
        decodeAllocator = &scaleCheckingAllocator;
    } else if (javaBitmap != nullptr) {
        decodeAllocator = &recyclingAllocator;
    } else if (willScale) {
        // This will allocate pixels using a HeapAllocator, since there will be an extra
        // scaling step that copies these pixels into Java memory.
        decodeAllocator = &heapAllocator;
    } else {
        decodeAllocator = &javaAllocator;
    }

    // Set the decode colorType.  This is necessary because we can't always support
    // the requested colorType.
    SkColorType decodeColorType = codec->computeOutputColorType(prefColorType);

    // Construct a color table for the decode if necessary
    SkAutoTUnref <SkColorTable> colorTable(nullptr);
    SkPMColor *colorPtr = nullptr;
    int *colorCount = nullptr;
    int maxColors = 256;
    SkPMColor colors[256];
    if (kIndex_8_SkColorType == decodeColorType) {
        colorTable.reset(new SkColorTable(colors, maxColors));

        // SkColorTable expects us to initialize all of the colors before creating an
        // SkColorTable.  However, we are using SkBitmap with an Allocator to allocate
        // memory for the decode, so we need to create the SkColorTable before decoding.
        // It is safe for SkAndroidCodec to modify the colors because this SkBitmap is
        // not being used elsewhere.
        colorPtr = const_cast<SkPMColor *>(colorTable->readColors());
        colorCount = &maxColors;
    }

    // Set the alpha type for the decode.
    SkAlphaType alphaType = codec->computeOutputAlphaType(requireUnpremultiplied);
    // 創建 SkImageInfo 信息,寬,高,ColorType,alphaType
    const SkImageInfo decodeInfo = SkImageInfo::Make(size.width(), size.height(), decodeColorType,
                                                     alphaType);
    SkImageInfo bitmapInfo = decodeInfo;
    if (decodeColorType == kGray_8_SkColorType) {
        // The legacy implementation of BitmapFactory used kAlpha8 for
        // grayscale images (before kGray8 existed).  While the codec
        // recognizes kGray8, we need to decode into a kAlpha8 bitmap
        // in order to avoid a behavior change.
        bitmapInfo = SkImageInfo::MakeA8(size.width(), size.height());
    }
    // 解析 SkBitmap 設置 bitmapInfo,tryAllocPixels 開闢內存,具體分析在後面 
    SkBitmap decodingBitmap;
    if (!decodingBitmap.setInfo(bitmapInfo) ||
        !decodingBitmap.tryAllocPixels(decodeAllocator, colorTable)) {
        // SkAndroidCodec should recommend a valid SkImageInfo, so setInfo()
        // should only only fail if the calculated value for rowBytes is too
        // large.
        // tryAllocPixels() can fail due to OOM on the Java heap, OOM on the
        // native heap, or the recycled javaBitmap being too small to reuse.
        return nullptr;
    }

    // Use SkAndroidCodec to perform the decode.
    SkAndroidCodec::AndroidOptions codecOptions;
    codecOptions.fZeroInitialized = (decodeAllocator == &javaAllocator) ?
    SkCodec::kYes_ZeroInitialized : SkCodec::kNo_ZeroInitialized;
    codecOptions.fColorPtr = colorPtr;
    codecOptions.fColorCount = colorCount;
    codecOptions.fSampleSize = sampleSize;
    // 解析獲取像素值
    SkCodec::Result result = codec->getAndroidPixels(decodeInfo, decodingBitmap.getPixels(),
                                                     decodingBitmap.rowBytes(), &codecOptions);
    switch (result) {
        case SkCodec::kSuccess:
        case SkCodec::kIncompleteInput:
            break;
        default:
            return nullObjectReturn("codec->getAndroidPixels() failed.");
    }

    jbyteArray ninePatchChunk = NULL;
    if (peeker.mPatch != NULL) {
        if (willScale) {
            scaleNinePatchChunk(peeker.mPatch, scale, scaledWidth, scaledHeight);
        }

        size_t ninePatchArraySize = peeker.mPatch->serializedSize();
        ninePatchChunk = env->NewByteArray(ninePatchArraySize);
        if (ninePatchChunk == NULL) {
            return nullObjectReturn("ninePatchChunk == null");
        }

        jbyte *array = (jbyte *) env->GetPrimitiveArrayCritical(ninePatchChunk, NULL);
        if (array == NULL) {
            return nullObjectReturn("primitive array == null");
        }

        memcpy(array, peeker.mPatch, peeker.mPatchSize);
        env->ReleasePrimitiveArrayCritical(ninePatchChunk, array, 0);
    }

    jobject ninePatchInsets = NULL;
    if (peeker.mHasInsets) {
        ninePatchInsets = env->NewObject(gInsetStruct_class, gInsetStruct_constructorMethodID,
        peeker.mOpticalInsets[0], peeker.mOpticalInsets[1], peeker.mOpticalInsets[2], peeker.mOpticalInsets[3],
                peeker.mOutlineInsets[0], peeker.mOutlineInsets[1], peeker.mOutlineInsets[2], peeker.mOutlineInsets[3],
                peeker.mOutlineRadius, peeker.mOutlineAlpha, scale);
        if (ninePatchInsets == NULL) {
            return nullObjectReturn("nine patch insets == null");
        }
        if (javaBitmap != NULL) {
            env->SetObjectField(javaBitmap, gBitmap_ninePatchInsetsFieldID, ninePatchInsets);
        }
    }
    // 構建 SkBitmap 這個纔是最終的
    SkBitmap outputBitmap;
    if (willScale) {
        // 如果需要縮放,那需要重新創建一張圖片,上面加載的是圖片的本身大小
        // This is weird so let me explain: we could use the scale parameter
        // directly, but for historical reasons this is how the corresponding
        // Dalvik code has always behaved. We simply recreate the behavior here.
        // The result is slightly different from simply using scale because of
        // the 0.5f rounding bias applied when computing the target image size
        const float sx = scaledWidth / float(decodingBitmap.width());
        const float sy = scaledHeight / float(decodingBitmap.height());

        // Set the allocator for the outputBitmap.
        SkBitmap::Allocator *outputAllocator;
        if (javaBitmap != nullptr) {
            outputAllocator = &recyclingAllocator;
        } else {
            outputAllocator = &javaAllocator;
        }

        SkColorType scaledColorType = colorTypeForScaledOutput(decodingBitmap.colorType());
        // FIXME: If the alphaType is kUnpremul and the image has alpha, the
        // colors may not be correct, since Skia does not yet support drawing
        // to/from unpremultiplied bitmaps.
        // 設置 SkImageInfo ,注意這裏是 scaledWidth ,scaledHeight 
        outputBitmap.setInfo(SkImageInfo::Make(scaledWidth, scaledHeight,
        scaledColorType, decodingBitmap.alphaType()));
        // 開闢當前 Bitmap 圖片的內存
        if (!outputBitmap.tryAllocPixels(outputAllocator, NULL)) {
            // This should only fail on OOM.  The recyclingAllocator should have
            // enough memory since we check this before decoding using the
            // scaleCheckingAllocator.
            return nullObjectReturn("allocation failed for scaled bitmap");
        }

        SkPaint paint;
        // kSrc_Mode instructs us to overwrite the unininitialized pixels in
        // outputBitmap.  Otherwise we would blend by default, which is not
        // what we want.
        paint.setXfermodeMode(SkXfermode::kSrc_Mode);
        paint.setFilterQuality(kLow_SkFilterQuality);
        // decodingBitmap -> 畫到 outputBitmap
        SkCanvas canvas(outputBitmap);
        canvas.scale(sx, sy);
        canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
    } else {
        outputBitmap.swap(decodingBitmap);
    }

    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 we get here, the outputBitmap should have an installed pixelref.
    if (outputBitmap.pixelRef() == NULL) {
        return nullObjectReturn("Got null SkPixelRef");
    }
    
    if (!isMutable && javaBitmap == NULL) {
        // promise we will never change our pixels (great for sharing and pictures)
        outputBitmap.setImmutable();
    }
    // 如果有複用返回原來的 javaBitmap
    bool isPremultiplied = !requireUnpremultiplied;
    if (javaBitmap != nullptr) {
        GraphicsJNI::reinitBitmap(env, javaBitmap, outputBitmap.info(), isPremultiplied);
        outputBitmap.notifyPixelsChanged();
        // If a java bitmap was passed in for reuse, pass it back
        return javaBitmap;
    }

    int bitmapCreateFlags = 0x0;
    if (isMutable) bitmapCreateFlags |= GraphicsJNI::kBitmapCreateFlag_Mutable;
    if (isPremultiplied) bitmapCreateFlags |= GraphicsJNI::kBitmapCreateFlag_Premultiplied;
    // 沒有複用的 Bitmap 創建一個新的 Bitmap
    // now create the java bitmap
    return GraphicsJNI::createBitmap(env, javaAllocator.getStorageObjAndReset(),
    bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
}

jobject GraphicsJNI::createBitmap(JNIEnv *env, android::Bitmap *bitmap,
                                  int bitmapCreateFlags, jbyteArray ninePatchChunk,
                                  jobject ninePatchInsets,
                                  int density) {
    bool isMutable = bitmapCreateFlags & kBitmapCreateFlag_Mutable;
    bool isPremultiplied = bitmapCreateFlags & kBitmapCreateFlag_Premultiplied;
    // The caller needs to have already set the alpha type properly, so the
    // native SkBitmap stays in sync with the Java Bitmap.
    assert_premultiplied(bitmap->info(), isPremultiplied);

    jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID,
    reinterpret_cast<jlong>(bitmap), bitmap->javaByteArray(),
            bitmap->width(), bitmap->height(), density, isMutable, isPremultiplied,
            ninePatchChunk, ninePatchInsets);
    hasException(env); // For the side effect of logging.
    return obj;
}

上面的代碼看起來比較長,其實是非常簡單的,相信大家都能看得懂,這裏我對上面的流程再做一些總結:

  1. 解析 java 層傳遞過來的 Options 的參數,如 simpleSize ,isMutable,javaBitmap 等等,同時計算出 scale 。
  2. 獲取當前圖片的大小,根據 sampleSize 判斷是否需要壓縮,同時計算出 scaledWidth ,scaledHeight。
  3. 設置 options 寬高爲 scaledWidth ,scaledHeight ,如果只是解析寬高那麼就直接返回,也就是 options.inJustDecodeBounds = true 時,但是這裏需要注意返回的是,資源圖片的寬高並不是 Bitmap 最終的寬高。(我們大部分人對這個有誤解)
  4. 創建 native 層的 SkImageInfo 和 SkBitmap ,然後調用 tryAllocPixels 去開闢圖片的內存空間,然後調用 getAndroidPixels 去解析像素值 ,這裏的 decodingBitmap 也並不是最終需要返回的 Bitmap ,而是原資源圖片的 Bitmap 。
  5. 構建需要返回的 outputBitmap ,如果需要縮放那麼重新去開闢一塊內存空間,如果不需要縮放直接調用 swap 方法即可。最後判斷有沒有複用的 JavaBitmap ,如果有複用調用 reinitBitmap 然後直接返回,如果沒有則調用 createBitmap 去創建一個新的 Bitmap 。

通過上面的分析,我們可能會有疑問?我們調用了兩次 tryAllocPixels ,那如果加載一張 (1440x2560) 10M 的圖片,豈不是需要 20M 的內存?

溫馨提示:有 Java 內存和 Native 內存之分。

2. Bitmap 的內存申請

Bitmap 的內存申請不同版本間有些許差異,在 3.0-7.0 的 bitmap 像素內存都是存放在 Java heap 中的,而 8.0 以後則是放在 Native heap 中的,我們可能會想這有啥區別?請看一個簡單的事例:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        logMemory();

        Bitmap bitmap = Bitmap.createBitmap(1024, 1024 * 500, Bitmap.Config.ARGB_8888);

        logMemory();
    }
    
    private void logMemory() {
        ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
        ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
        activityManager.getMemoryInfo(memoryInfo);
        Log.e("TAG", "AvailMem :" + memoryInfo.availMem / 1024 / 1024);
        Log.e("TAG", "lowMemory:" + memoryInfo.lowMemory);
        Log.e("TAG", "NativeHeapAllocatedSize :" + Debug.getNativeHeapAllocatedSize() / 1024 / 1024);
    }

上面我們創建了一張 2G 大小的 bitmap 我們在 8.0 以下的版本運行是會 OOM 的,而我們在 8.0 以上的版本運行是完全沒問題,但 Native 內存多了 2G 的內存。

E/TAG: AvailMem :1654
E/TAG: lowMemory:false
E/TAG: NativeHeapAllocatedSize :4

E/TAG: AvailMem :1656
E/TAG: lowMemory:false
E/TAG: NativeHeapAllocatedSize :2052

通過之前的源碼分析可知 bitmap 的內存創建都是通過 tryAllocPixels 方法來申請的,我們通過源碼來對比一下他們之間的區別,我們首先來看下 7.0 的代碼:

/frameworks/base/core/jni/android/graphics/Bitmap.cpp

bool SkBitmap::tryAllocPixels(Allocator *allocator, SkColorTable *ctable) {
    HeapAllocator stdalloc;

    if (nullptr == allocator) {
        allocator = &stdalloc;
    }
    return allocator->allocPixelRef(this, ctable);
}

bool JavaPixelAllocator::allocPixelRef(SkBitmap *bitmap, SkColorTable *ctable) {
    JNIEnv *env = vm2env(mJavaVM);

    mStorage = GraphicsJNI::allocateJavaPixelRef(env, bitmap, ctable);
    return mStorage != nullptr;
}

android::Bitmap *GraphicsJNI::allocateJavaPixelRef(JNIEnv *env, SkBitmap *bitmap,
                                                    SkColorTable *ctable) {
    const SkImageInfo &info = bitmap->info();
    if (info.colorType() == kUnknown_SkColorType) {
        doThrowIAE(env, "unknown bitmap configuration");
        return NULL;
    }

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

    jbyteArray arrayObj = (jbyteArray) env->CallObjectMethod(gVMRuntime,
                                                             gVMRuntime_newNonMovableArray,
                                                             gByte_class, size);
    if (env->ExceptionCheck() != 0) {
        return NULL;
    }
    SkASSERT(arrayObj);
    jbyte *addr = (jbyte *) env->CallLongMethod(gVMRuntime, gVMRuntime_addressOf, arrayObj);
    if (env->ExceptionCheck() != 0) {
        return NULL;
    }
    SkASSERT(addr);
    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;
}


從上面就可以看到, new android::Bitmap 見:
frameworks/base/core/jni/android/graphics/Bitmap.cpp

Bitmap::Bitmap(JNIEnv *env, jbyteArray storageObj, void *address,
               const SkImageInfo &info, size_t rowBytes, SkColorTable *ctable)
        : mPixelStorageType(PixelStorageType::Java) {
    env->GetJavaVM(&mPixelStorage.java.jvm);
    mPixelStorage.java.jweakRef = env->NewWeakGlobalRef(storageObj);
    mPixelStorage.java.jstrongRef = nullptr;
    mPixelRef.reset(new WrappedPixelRef(this, address, info, rowBytes, ctable));
    // Note: this will trigger a call to onStrongRefDestroyed(), but
    // we want the pixel ref to have a ref count of 0 at this point
    mPixelRef->unref();
}

address 獲取的是 arrayObj 的地址,而 arrayObj 是 jbyteArray 數據類型,也就是說這裏是通過 JNI 世界進入了 Java 世界開闢了內存,好比 Zygote 進入 Java 世界是通過 JNI 調用 com.android.internal.os.ZygoteInit 類的 main 函數是一個道理~ 我們還可以繼續跟到 gVMRuntime_newNonMovableArray 中去看看實現,最後是 runtime->GetHeap() 上分配內存也就是 Java heap 內存。這裏我就不再貼具體的代碼了。

我們還得看下 8.0 的源碼,比較一下它與 7.0 之間的區別:
external/skia/src/core/SkBitmap.cpp

bool SkBitmap::tryAllocPixels(Allocator *allocator, SkColorTable *ctable) {
    HeapAllocator stdalloc;

    if (nullptr == allocator) {
        allocator = &stdalloc;
    }
    return allocator->allocPixelRef(this, ctable);
}

bool HeapAllocator::allocPixelRef(SkBitmap *bitmap, SkColorTable *ctable) {
    mStorage = android::Bitmap::allocateHeapBitmap(bitmap, ctable);
    return !!mStorage;
}

allocateHeapBitmap方法會最終new Bitmap,分配內存 ,見:
/frameworks/base/libs/hwui/hwui/Bitmap.cpp

sk_sp <Bitmap> Bitmap::allocateHeapBitmap(SkBitmap *bitmap, SkColorTable *ctable) {
    return allocateBitmap(bitmap, ctable, &android::allocateHeapBitmap);
}

static sk_sp <Bitmap> allocateBitmap(SkBitmap *bitmap, SkColorTable *ctable, AllocPixeRef alloc) {
    const SkImageInfo &info = bitmap->info();
    if (info.colorType() == kUnknown_SkColorType) {
        LOG_ALWAYS_FATAL("unknown bitmap configuration");
        return nullptr;
    }

    size_t size;

    // we must respect the rowBytes value already set on the bitmap instead of
    // attempting to compute our own.
    const size_t rowBytes = bitmap->rowBytes();
    if (!computeAllocationSize(rowBytes, bitmap->height(), &size)) {
        return nullptr;
    }

    auto wrapper = alloc(size, info, rowBytes, ctable);
    if (wrapper) {
        wrapper->getSkBitmap(bitmap);
        // since we're already allocated, we lockPixels right away
        // HeapAllocator behaves this way too
        bitmap->lockPixels();
    }
    return wrapper;
}

3. Bitmap 的內存回收

通過上面的源碼分析可知,Bitmap 其實佔兩部分對象,一個是 Java Bitmap 對象,還有一個是 Native Bitmap 對象,Java Bitmap 對象當然是垃圾回收機制來管理了,那 Native Bitmap 對象會在什麼時候回收呢?

我們的 Bitmap 提供了一個 recycle 方法可以被開發者調用,但是這個方法我們在真正的開發過程中並沒有調用它,那它到底有啥用呢?我們該在什麼時候調用呢?

首先我先把結論寫出來然後再去看源碼,在 Android 2.3.3 之前開發者必須手動調用 recycle 方法去釋放 Native 內存,因爲那個時候管理Bitmap內存比較複雜,需要手動維護引用計數器,在官網上有如下一段解釋:

On Android 2.3.3 (API level 10) and lower, using recycle() is recommended. If you're displaying large amounts of bitmap data in your app, you're likely to run into OutOfMemoryError errors. The recycle()method allows an app to reclaim memory as soon as possible.
Caution: You should use recycle() only when you are sure that the bitmap is no longer being used. If you call recycle() and later attempt to draw the bitmap, you will get the error: "Canvas: trying to use a recycled bitmap".
The following code snippet gives an example of calling recycle(). It uses reference counting (in the variables mDisplayRefCount and mCacheRefCount) to track whether a bitmap is currently being displayed or in the cache. The code recycles the bitmap when these conditions are met:
The reference count for both mDisplayRefCount and mCacheRefCount is 0.
The bitmap is not null, and it hasn't been recycled yet.

在 Android 2.3.3 以後不需要開發者主動調用 recycle 方法來回收內存了,但 Android K,L,M,N,O 版本上,都還能看到 recycle 方法,爲什麼沒有幹掉呢? 調用它會不會真正的釋放內存呢?既然不需要手動釋放 Native Bitmap ,那 Native 層的對象是怎麼自動釋放的?我們先來看下 7.0 和 8.0 中 recycle 的方法實現。


  /**
  * Free the native object associated with this bitmap, and clear the
  * reference to the pixel data. This will not free the pixel data synchronously;
  * it simply allows it to be garbage collected if there are no other references.
  * The bitmap is marked as "dead", meaning it will throw an exception if
  * getPixels() or setPixels() is called, and will draw nothing. This operation
  * cannot be reversed, so it should only be called if you are sure there are no
  * further uses for the bitmap. This is an advanced call, and normally need
  * not be called, since the normal GC process will free up this memory when
  * there are no more references to this bitmap.
  */
  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;
    }
  }

  private static native boolean nativeRecycle(long nativeBitmap);

8.0 見:
/xref/frameworks/base/core/jni/android/graphics/Bitmap.cpp

static jboolean Bitmap_recycle(JNIEnv *env, jobject, jlong bitmapHandle) {
    LocalScopedBitmap bitmap(bitmapHandle);
    bitmap->freePixels();
    return JNI_TRUE;
}

void freePixels() {
    mInfo = mBitmap->info();
    mHasHardwareMipMap = mBitmap->hasHardwareMipMap();
    mAllocationSize = mBitmap->getAllocationByteCount();
    mRowBytes = mBitmap->rowBytes();
    mGenerationId = mBitmap->getGenerationID();
    mIsHardware = mBitmap->isHardware();
    // 清空了數據
    mBitmap.reset();
}

7.0 見:
/xref/frameworks/base/core/jni/android/graphics/Bitmap.cpp

static jboolean Bitmap_recycle(JNIEnv *env, jobject, jlong bitmapHandle) {
    LocalScopedBitmap bitmap(bitmapHandle);
    bitmap->freePixels();
    return JNI_TRUE;
}

void Bitmap::doFreePixels() {
    switch (mPixelStorageType) {
        case PixelStorageType::Invalid:
            // already free'd, nothing to do
            break;
        case PixelStorageType::External:
            mPixelStorage.external.freeFunc(mPixelStorage.external.address,
                                            183
            mPixelStorage.external.context);
            break;
        case PixelStorageType::Ashmem:
            munmap(mPixelStorage.ashmem.address, mPixelStorage.ashmem.size);
            close(mPixelStorage.ashmem.fd);
            break;
        case PixelStorageType::Java:
            // 只是釋放了 Java 層之前創建的引用
            JNIEnv *env = jniEnv();
            LOG_ALWAYS_FATAL_IF(mPixelStorage.java.jstrongRef,
                                192
            "Deleting a bitmap wrapper while there are outstanding strong "
                    "references! mPinnedRefCount = %d", mPinnedRefCount);
            env->DeleteWeakGlobalRef(mPixelStorage.java.jweakRef);
            break;
    }

    if (android::uirenderer::Caches::hasInstance()) {
        android::uirenderer::Caches::getInstance().textureCache.releaseTexture(
                mPixelRef->getStableID());
    }
}

從上面的源碼可以看出,如果是 8.0 我們手動調用 recycle 方法,數據是會立即釋放的,因爲像素數據本身就是在 Native 層開闢的。但如果是在 8.0 以下,就算我們手動調用 recycle 方法,數據也是不會立即釋放的,而是 DeleteWeakGlobalRef 交由 Java GC 來回收。建議大家翻譯一下 recycle 方法註釋。注意:以上的所說的釋放數據僅代表釋放像素數據,並未釋放 Native 層的 Bitmap 對象。

最後只剩下一個問題了,我們在開發的過程中一般情況下並不會手動去調用 recycle 方法,那 Native 層的 Bitmap 是怎麼回收的呢?如果讓我們來寫這個代碼,我們不妨思考一下該怎麼下手?這裏我就不賣關子了。在 new Bitmap 時,其實就已經指定了誰來控制 Bitmap 的內存回收。Android M 版本及以前的版本, Bitmap 的內存回收主要是通過 BitmapFinalizer 來完成的見:
/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, NinePatchInsetStruct ninePatchInsets) {
        if (nativeBitmap == 0) {
            throw new RuntimeException("internal error: native bitmap is 0");
        }

        mWidth = width;
        mHeight = height;
        mIsMutable = isMutable;
        mRequestPremultiplied = requestPremultiplied;
        mBuffer = buffer;

        mNinePatchChunk = ninePatchChunk;
        mNinePatchInsets = ninePatchInsets;
        if (density >= 0) {
            mDensity = density;
        }

        mNativePtr = nativeBitmap;
        // 這個對象對象來回收
        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 {
                // finalize 這裏是 GC 回收該對象時會調用
                setNativeAllocationByteCount(0);
                nativeDestructor(mNativeBitmap);
                mNativeBitmap = 0;
            }
        }
    }

    private static native void nativeDestructor(long nativeBitmap);

在 Android N 和 Android O 上做了些改動,但改動不大。雖然沒有了 BitmapFinalizer 類,但在 new Bitmap 時會註冊 native 的 Finalizer 方法見: /frameworks/base/graphics/java/android/graphics/Bitmap.java

    /**
     * Private constructor that must received an already allocated native bitmap
     * int (pointer).
     */
    // called from JNI
    Bitmap(long nativeBitmap, int width, int height, int density,
            boolean isMutable, boolean requestPremultiplied,
            byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {
        if (nativeBitmap == 0) {
            throw new RuntimeException("internal error: native bitmap is 0");
        }

        mWidth = width;
        mHeight = height;
        mIsMutable = isMutable;
        mRequestPremultiplied = requestPremultiplied;

        mNinePatchChunk = ninePatchChunk;
        mNinePatchInsets = ninePatchInsets;
        if (density >= 0) {
            mDensity = density;
        }

        mNativePtr = nativeBitmap;
        long nativeSize = NATIVE_ALLOCATION_SIZE + getAllocationByteCount();
        NativeAllocationRegistry registry = new NativeAllocationRegistry(
                Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), nativeSize);
        registry.registerNativeAllocation(this, nativeBitmap);
    }

NativeAllocationRegistry 見:
/libcore/luni/src/main/java/libcore/util/NativeAllocationRegistry.java

    public class NativeAllocationRegistry {

        private final ClassLoader classLoader;
        private final long freeFunction;
        private final long size;

        /**
         * Constructs a NativeAllocationRegistry for a particular kind of native
         * allocation.
         * The address of a native function that can be used to free this kind
         * native allocation should be provided using the
         * <code>freeFunction</code> argument. The native function should have the
         * type:
         * <pre>
         *    void f(void* nativePtr);
         * </pre>
         * <p>
         * The <code>classLoader</code> argument should be the class loader used
         * to load the native library that freeFunction belongs to. This is needed
         * to ensure the native library doesn't get unloaded before freeFunction
         * is called.
         * <p>
         * The <code>size</code> should be an estimate of the total number of
         * native bytes this kind of native allocation takes up. Different
         * NativeAllocationRegistrys must be used to register native allocations
         * with different estimated sizes, even if they use the same
         * <code>freeFunction</code>.
         *
         * @param classLoader  ClassLoader that was used to load the native
         *                     library freeFunction belongs to.
         * @param freeFunction address of a native function used to free this
         *                     kind of native allocation
         * @param size         estimated size in bytes of this kind of native
         *                     allocation
         * @throws IllegalArgumentException If <code>size</code> is negative
         */
        public NativeAllocationRegistry(ClassLoader classLoader, long freeFunction, long size) {
            if (size < 0) {
                throw new IllegalArgumentException("Invalid native allocation size: " + size);
            }

            this.classLoader = classLoader;
            this.freeFunction = freeFunction;
            this.size = size;
        }

        /**
         * Registers a new native allocation and associated Java object with the
         * runtime.
         * This NativeAllocationRegistry's <code>freeFunction</code> will
         * automatically be called with <code>nativePtr</code> as its sole
         * argument when <code>referent</code> becomes unreachable. If you
         * maintain copies of <code>nativePtr</code> outside
         * <code>referent</code>, you must not access these after
         * <code>referent</code> becomes unreachable, because they may be dangling
         * pointers.
         * <p>
         * The returned Runnable can be used to free the native allocation before
         * <code>referent</code> becomes unreachable. The runnable will have no
         * effect if the native allocation has already been freed by the runtime
         * or by using the runnable.
         *
         * @param referent  java object to associate the native allocation with
         * @param nativePtr address of the native allocation
         * @return runnable to explicitly free native allocation
         * @throws IllegalArgumentException if either referent or nativePtr is null.
         * @throws OutOfMemoryError         if there is not enough space on the Java heap
         *                                  in which to register the allocation. In this
         *                                  case, <code>freeFunction</code> will be
         *                                  called with <code>nativePtr</code> as its
         *                                  argument before the OutOfMemoryError is
         *                                  thrown.
         */
        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");
            }

            try {
                registerNativeAllocation(this.size);
            } catch (OutOfMemoryError oome) {
                applyFreeFunction(freeFunction, nativePtr);
                throw oome;
            }

            Cleaner cleaner = Cleaner.create(referent, new CleanerThunk(nativePtr));
            return new CleanerRunner(cleaner);
        }


        private class CleanerThunk implements Runnable {
            private long nativePtr;

            public CleanerThunk() {
                this.nativePtr = 0;
            }

            public CleanerThunk(long nativePtr) {
                this.nativePtr = nativePtr;
            }

            public void run() {
                if (nativePtr != 0) {
                    applyFreeFunction(freeFunction, nativePtr);
                }
                registerNativeFree(size);
            }

            public void setNativePtr(long nativePtr) {
                this.nativePtr = nativePtr;
            }
        }

        private static class CleanerRunner implements Runnable {
            private final Cleaner cleaner;

            public CleanerRunner(Cleaner cleaner) {
                this.cleaner = cleaner;
            }

            public void run() {
                cleaner.clean();
            }
        }

        /**
         * Calls <code>freeFunction</code>(<code>nativePtr</code>).
         * Provided as a convenience in the case where you wish to manually free a
         * native allocation using a <code>freeFunction</code> without using a
         * NativeAllocationRegistry.
         */
        public static native void applyFreeFunction(long freeFunction, long nativePtr);
    }

總結:其實無論是 Android M 前還是之後,釋放 Native 層的 Bitmap 對象的思想都是去監聽 Java 層的 Bitmap 是否被釋放,一旦當 Java 層的 Bitmap 對象被釋放則立即去釋放 Native 層的 Bitmap 。只不過 Android M 前是基於 Java 的 GC 機制,而 Android M 後是註冊 native 的 Finalizer 方法。

4. Bitmap 的內存複用

Bitmap 絕對是我們 Android 開發中最容易引起 OOM 的對象之一,因爲其佔用的像素數據內存比較大,而加載圖片又是很常見的操作。如果不斷反覆的去開闢和銷燬 Bitmap 數據內存,勢必可能會引起應用的內存抖動,因此 Google 的開發者也爲我們想了一些辦法,那就是允許 Bitmap 內存複用,具體如下:

  • 被複用的 Bitmap 必須爲 Mutable(通過 BitmapFactory.Options 設置)
  • 4.4 之前,將要解碼的圖像(無論是資源還是流)必須是 jpeg 或 png 格式且和被複用的 Bitmap 大小一樣,其中BitmapFactory.Options#inSampleSize 字段必須設置爲 1,要求比較嚴苛
  • 4.4 以後,將要解碼的圖像的內存需要大於等於要複用的 Bitmap 的內存
  // 不復用的寫法,消耗內存 32 M
  logMemory();
  Bitmap bitmap1 = BitmapFactory.decodeResource(getResources(), R.drawable.test2);
  Bitmap bitmap2 = BitmapFactory.decodeResource(getResources(), R.drawable.test2);
  logMemory();
  // 複用的寫法,消耗內存 16 M
  logMemory();
  BitmapFactory.Options options = new BitmapFactory.Options();
  options.inMutable = true;
  Bitmap bitmap1 = BitmapFactory.decodeResource(getResources(), R.drawable.test2, options);
  options.inBitmap = bitmap1;
  Bitmap bitmap2 = BitmapFactory.decodeResource(getResources(), R.drawable.test2, options);
  logMemory();

具體的實現源碼上面已經講過了,這裏就不再做過多的囉嗦,最後建議大家去看看 Glide 的源碼,看看這些比較知名的開源框架,到底是怎麼複用 Bitmap 的。某些同學看 C++ 代碼可能比較頭疼,其實我們只需要瞭解一些基礎語法,就能看得懂上面的代碼了。打通我們的任督二脈的確需要一些時間,但換個角度來講,這些時間可以拿來換更多的時間和金錢。

最後,我們不妨再來思考下,本地資源圖片 xhdpi,xxhdpi 和 xxxhdpi 我們應該放哪個目錄的文件夾下,加載效率纔會更高?還是應該放多套圖?

視頻地址:https://pan.baidu.com/s/1CIfeKabrv9mOQ-tfClDe3w
視頻密碼:1xax

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