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