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 的原理与设计思想

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