Android 开发绕不过的坑:你的 Bitmap 究竟占多大内存?

转载:http://bugly.qq.com/bbs/forum.php?mod=viewthread&tid=498&fromuid=6php

0、写在前面
java

本文涉及到屏幕密度的讨论,这里先要搞清楚 DisplayMetrics 的两个变量,摘录官方文档的解释:



  • density:The logical density of the display. This is a scaling factor for the Density Independent Pixel unit, where one DIP is one pixel on an approximately 160 dpi screen (for example a 240x320, 1.5”x2” screen), providing the baseline of the system’s display. Thus on a 160dpi screen this density value will be 1; on a 120 dpi screen it would be .75; etc.
    This value does not exactly follow the real screen size (as given by xdpi and ydpi, but rather is used to scale the size of the overall UI in steps based on gross changes in the display dpi. For example, a 240x320 screen will have a density of 1 even if its width is 1.8”, 1.3”, etc. However, if the screen resolution is increased to 320x480 but the screen size remained 1.5”x2” then the density would be increased (probably to 1.5).
  • densityDpi:The screen density expressed as dots-per-inch.
简单来讲,能够理解为 density 的数值是 1dp=density px;densityDpi 是屏幕每英寸对应多少个点(不是像素点),在 DisplayMetrics 当中,这两个的关系是线性的:
density 1 1.5 2 3 3.5 4
densityDpi 160 240 320 480 560 640
为了避免引发混淆,本文全部提到的密度除非特别说明,都指的是 densityDpi,固然若是你愿意,也能够用 density 来讲明问题。
另外,本文的依据主要来自 android 5.0 的源码,其余版本可能略有出入。文章不免疏漏,欢迎指正~


一、占了多大内存?
android

作移动客户端开发的朋友们确定都由于图头疼过,提及来曾经还有过 leader 由于组里面一哥们在工程里面加了一张 jpg 的图发脾气的事儿,哈哈。
为何头疼呢?吃内存呗,时不时还给你来个 OOM 冲冲喜,让你的每一天过得有滋有味(真是没救了)。那每次工程里面增长一张图片的时候,咱们都须要关心这货究竟要占多大的坑,占多大呢?Android API 有个方便的方法,
[Java]  纯文本查看 复制代码
?
1
2
3
4
public final int getByteCount() {
     // int result permits bitmaps up to 46,340 x 46,340
     return getRowBytes() * getHeight();
}


经过这个方法,咱们就能够获取到一张 Bitmap 在运行时到底占用多大内存了。

 

举个例子
一张 522x686 的 PNG 图片,我把它放到 drawable-xxhdpi 目录下,在三星s6上加载,占用内存2547360B,就能够用这个方法获取到。



二、给我一张图我告诉你占多大内存
算法

每次都问 Bitmap 你到底多大啦。。感受怪怪的,毕竟咱们不能老是去问,而不去搞清楚它为嘛介么大吧。能不能给它算个命,算算它究竟多大呢?固然是能够的,很简单嘛,咱们直接顺藤摸瓜,找出真凶,哦不,找出答案。


2.1 getByteCount
express

getByteCount 的源码咱们刚刚已经认识了,当咱们问 Bitmap 大小的时候,这孩子也是先拿到出生年月日,而后算出来的,那么问题来了,getHeight 就是图片的高度(单位:px),getRowBytes 是什么?
[Java]  纯文本查看 复制代码
?
1
2
3
4
5
6
public final int getrowBytes() {
    if (mRecycled) {
           Log.w(TAG, "Called getRowBytes() on a recycle()'d bitmap! This is undefined behavior!" );
    }
    return nativeRowBytes(mFinalizer.mNativeBitmap);
}


额,感受太对了啊,要 JNI 了。因为在下 C++ 实在用得少,每次想起 JNI 都请想象脑门磕墙的场景,不过呢,毛爷爷说过,一切反动派都是纸老虎~与


nativeRowBytes 对应的函数以下:
Bitmap.cpp
[Java]  纯文本查看 复制代码
?
1
2
3
4
static jint Bitmap_rowBytes(JNIEnv* env, jobject, jlong bitmapHandle) {
      SkBitmap* bitmap = reinterpret_cast<SkBitmap*>(bitmapHandle)
      return static_cast<jint>(bitmap->rowBytes());
}


等等,咱们好像发现了什么,原来 Bitmap 本质上就是一个 SkBitmap。。而这个 SkBitmap 也是大有来头,不信你瞧: Skia。啥也别说了,赶忙瞅瞅 SkBitmap。
SkBitmap.h
[Java]  纯文本查看 复制代码
?
1
2
/** Return the number of bytes between subsequent rows of the bitmap. */
size_t rowBytes() const { return fRowBytes; }


SkBitmap.cpp
[Java]  纯文本查看 复制代码
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
size_t SkBitmap::ComputeRowBytes(Config c, int width) {
     return SkColorTypeMinRowBytes(SkBitmapConfigToColorType(c), width);
}
SkImageInfo.h
 
static int SkColorTypeBytesPerPixel(SkColorType ct) {
    static const uint8_t gSize[] = {
     0 // Unknown
     1 // Alpha_8
     2 // RGB_565
     2 // ARGB_4444
     4 // RGBA_8888
     4 // BGRA_8888
     1 // kIndex_8
   };
   SK_COMPILE_ASSERT(SK_ARRAY_COUNT(gSize) == (size_t)(kLastEnum_SkColorType + 1 ),
                 size_mismatch_with_SkColorType_enum);
 
    SkASSERT((size_t)ct < SK_ARRAY_COUNT(gSize));
    return gSize[ct];
}
 
static inline size_t SkColorTypeMinRowBytes(SkColorType ct, int width) {
     return width * SkColorTypeBytesPerPixel(ct);
}


好,跟踪到这里,咱们发现 ARGB_8888(也就是咱们最经常使用的 Bitmap 的格式)的一个像素占用 4byte,那么 rowBytes 实际上就是 4*width bytes。
那么结论出来了,一张 ARGB_8888 的 Bitmap 占用内存的计算公式
bitmapInRam = bitmapWidth*bitmapHeight *4 bytes
说到这儿你觉得故事就结束了么?有本事你拿去试,算出来的和你获取到的老是会差个倍数,为啥呢?
还记得咱们最开始给出的那个例子么?

一张522*686的 PNG 图片,我把它放到 drawable-xxhdpi 目录下,在三星s6上加载,占用内存2547360B,就能够用这个方法获取到。

然而公式计算出来的但是1432368B。。。


2.2 Density
canvas

知道我为何在举例的时候那么费劲的说放到xxx目录下,还要说用xxx手机么?你觉得 Bitmap 加载只跟宽高有关么?Naive。
仍是先看代码,咱们读取的是 drawable 目录下面的图片,用的是 decodeResource 方法,该方法本质上就两步:



  • 读取原始资源,这个调用了 Resource.openRawResource 方法,这个方法调用完成以后会对 TypedValue 进行赋值,其中包含了原始资源的 density 等信息;
  • 调用 decodeResourceStream 对原始资源进行解码和适配。这个过程实际上就是原始资源的 density 到屏幕 density 的一个映射。
原始资源的 density 其实取决于资源存放的目录(好比 xxhdpi 对应的是480),而屏幕 density 的赋值,请看下面这段代码:


BitmapFactory.java
[Java]  纯文本查看 复制代码
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static Bitmap decodeResourceStream(Resources res, TypedValue value,
     InputStream is, Rect pad, Options opts) {
 
//实际上,咱们这里的opts是null的,因此在这里初始化。
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; //这里density的值若是对应资源目录为hdpi的话,就是240
     }
}
 
if (opts.inTargetDensity == 0 && res != null ) {
//请注意,inTargetDensity就是当前的显示密度,好比三星s6时就是640
     opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}
 
return decodeStream(is, pad, opts);
}


咱们看到 opts 这个值被初始化,而它的构造竟然如此简单:
[Java]  纯文本查看 复制代码
?
1
2
3
4
5
public Options() {
    inDither = false ;
    inScaled = true ;
    inPremultiplied = true ;
}


因此咱们就很容易的看到,Option.inScreenDensity 这个值没有被初始化,而实际上后面咱们也会看到这个值根本不会用到;咱们最应该关心的是什么呢?是 inDensity 和 inTargetDensity,这两个值与下面 cpp 文件里面的 density 和 targetDensity 相对应——重复一下,inDensity 就是原始资源的 density,inTargetDensity 就是屏幕的 density。
紧接着,用到了 nativeDecodeStream 方法,不重要的代码直接略过,直接给出最关键的 doDecode 函数的代码:


BitmapFactory.cpp
[Java]  纯文本查看 复制代码
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
 
......
     if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
         const int density = env->GetIntField(options, gOptions_densityFieldID); //对应hdpi的时候,是240
         const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID); //三星s6的为640
         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;
......
SkBitmap decodingBitmap;
if (!decoder->decode(stream, &decodingBitmap, prefColorType,decodeMode)) {
    return nullObjectReturn( "decoder->decode returned false" );
}
//这里这个deodingBitmap就是解码出来的bitmap,大小是图片原始的大小
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 (willScale) {
     const float sx = scaledWidth / float (decodingBitmap.width());
     const float sy = scaledHeight / float (decodingBitmap.height());
 
     // TODO: avoid copying when scaled size equals decodingBitmap size
     SkColorType colorType = 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.
     outputBitmap->setInfo(SkImageInfo::Make(scaledWidth, scaledHeight,
             colorType, decodingBitmap.alphaType()));
     if (!outputBitmap->allocPixels(outputAllocator, NULL)) {
         return nullObjectReturn( "allocation failed for scaled bitmap" );
     }
 
     // If outputBitmap's pixels are newly allocated by Java, there is no need
     // to erase to 0, since the pixels were initialized to 0.
     if (outputAllocator != &javaAllocator) {
         outputBitmap->eraseColor( 0 );
     }
 
     SkPaint paint;
     paint.setFilterLevel(SkPaint::kLow_FilterLevel);
 
     SkCanvas canvas(*outputBitmap);
     canvas.scale(sx, sy);
     canvas.drawBitmap(decodingBitmap, 0 .0f, 0 .0f, &paint);
}
......
}


注意到其中有个 density 和 targetDensity,前者是 decodingBitmap 的 density,这个值跟这张图片的放置的目录有关(好比 hdpi 是240,xxhdpi 是480),这部分代码我跟了一下,太长了,就不列出来了;targetDensity 其实是咱们加载图片的目标 density,这个值的来源咱们已经在前面给出了,就是 DisplayMetrics 的 densityDpi,若是是三星s6那么这个数值就是640。sx 和sy 其实是约等于 scale 的,由于 scaledWidth 和 scaledHeight 是由 width 和 height 乘以 scale 获得的。咱们看到 Canvas 放大了 scale 倍,而后又把读到内存的这张 bitmap 画上去,至关于把这张 bitmap 放大了 scale 倍。


再来看咱们的例子:

一张522*686的PNG 图片,我把它放到 drawable-xxhdpi 目录下,在三星s6上加载,占用内存2547360B,其中 density 对应 xxhdpi 为480,targetDensity 对应三星s6的密度为640:
522/480 *  640 * 686/480  *640 * 4 = 2546432B


2.3 精度app

愈来愈有趣了是否是,你确定会发现咱们这么细致的计算仍是跟获取到的数值
不!一!样!
为何呢?因为结果已经很是接近,咱们很天然地想到精度问题。来,再把上面这段代码中的一句拿出来看看:
[Java]  纯文本查看 复制代码
?
1
2
outputBitmap->setInfo(SkImageInfo::Make(scaledWidth, scaledHeight,
             colorType, decodingBitmap.alphaType()));


咱们看到最终输出的 outputBitmap 的大小是scaledWidth*scaledHeight,咱们把这两个变量计算的片断拿出来给你们一看就明白了:
[Java]  纯文本查看 复制代码
?
1
2
3
4
if (willScale && decodeMode != SkImageDecoder::kDecodeBounds_Mode) {
     scaledWidth = int (scaledWidth * scale + 0 .5f);
     scaledHeight = int (scaledHeight * scale + 0 .5f);
}


在咱们的例子中,
scaledWidth = int( 522 * 640 / 480f + 0.5) = int(696.5) = 696
scaledHeight = int( 686 * 640 / 480f + 0.5) = int(915.16666…) = 915
下面就是见证奇迹的时刻:
915 * 696 * 4 = 2547360
有木有很兴奋!有木有很激动!!
写到这里,忽然想起《STL源码剖析》一书的扉页,侯捷先生只写了一句话:
“源码以前,了无秘密”。


2.4 小结
函数

其实,经过前面的代码跟踪,咱们就不难知道,Bitmap 在内存当中占用的大小其实取决于:



  • 色彩格式,前面咱们已经提到,若是是 ARGB8888 那么就是一个像素4个字节,若是是 RGB565 那就是2个字节
  • 原始文件存放的资源目录(是 hdpi 仍是 xxhdpi 可不能傻傻分不清楚哈)
  • 目标屏幕的密度(因此同等条件下,红米在资源方面消耗的内存确定是要小于三星S6的)

三、想办法减小 Bitmap 内存占用
3.1 Jpg 和 Pngpost

说到这里,确定会有人会说,咱们用 jpg 吧,jpg 格式的图片不该该比 png 小么?
这确实是个好问题,由于一样一张图片,jpg 确实比 png 会多少小一些(甚至不少),缘由很简单,jpg 是一种有损压缩的图片存储格式,而 png 则是 无损压缩的图片存储格式,显而易见,jpg 会比 png 小,代价也是显而易见的。


但是,这说的是文件存储范畴的事情,它们只存在于文件系统,而非内存或者显存。说得简单一点儿,我有一个极品飞车的免安装硬盘版的压缩包放在个人磁盘里面,这个游戏是不能玩的,我须要先解压,才能玩——jpg 也好,png 也好就是个压缩包的概念,而咱们讨论的内存占用则是从使用角度来讨论的。
因此,jpg 格式的图片与 png 格式的图片在内存当中不该该有什么不一样。
『啪!!!』
『谁这么缺德!!打人不打脸好么!』
确定有人有意见,jpg 图片读到内存就是会小,还会给我拿出例子。固然,他说的不必定是错的。由于 jpg 的图片没有 alpha 通道!!因此读到内存的时候若是用 RGB565的格式存到内存,这下大小只有 ARGB8888的一半,能不小么。。。
不过,抛开 Android 这个平台不谈,从出图的角度来看的话,jpg 格式的图片大小也不必定比 png 的小,这要取决于图像信息的内容:
JPG 不适用于所含颜色不多、具备大块颜色相近的区域或亮度差别十分明显的较简单的图片。对于须要高保真的较复杂的图像,PNG 虽然能无损压缩,但图片文件较大。
若是仅仅是为了 Bitmap 读到内存中的大小而考虑的话,jpg 也好 png 也好,没有什么实质的差异;两者的差异主要体如今:



  • alpha 你是否真的须要?若是须要 alpha 通道,那么没有别的选择,用 png。
  • 你的图色值丰富仍是单调?就像刚才提到的,若是色值丰富,那么用jpg,若是做为按钮的背景,请用 png。
  • 对安装包大小的要求是否很是严格?若是你的 app 资源不多,安装包大小问题不是很凸显,看状况选择 jpg 或者 png(不过,我想如今对资源文件没有苛求的应用会不多吧。。)
  • 目标用户的 cpu 是否强劲?jpg 的图像压缩算法比 png 耗时。这方面仍是要酌情选择,前几年作了一段时间 Cocos2dx,因为资源很是多,项目组要求统一使用 png,可能就是出于这方面的考虑。
嗯,跑题了,咱们其实想说的是怎么减小内存占用的。。这一小节只是想说,休想经过这个方法来减小内存占用。。。XD
 

3.2 使用 inSampleSize测试

有些朋友一看到这个确定就笑了。采样嘛,我之前是学信号处理的,一看到 Sample 就抽抽。。哈哈开个玩笑,这个采样其实就跟统计学里面的采样是同样的,在保证最终效果知足要求的前提下减小样本规模,方便后续的数据采集和处理。
这个方法主要用在图片资源自己较大,或者适当地采样并不会影响视觉效果的条件下,这时候咱们输出地目标可能相对较小,对图片分辨率、大小要求不是很是的严格。

 

举个例子
咱们如今有个需求,要求将一张图片进行模糊,而后做为 ImageView 的 src 呈现给用户,而咱们的原始图片大小为 1080*1920,若是咱们直接拿来模糊的话,一方面模糊的过程费时费力,另外一方面生成的图片又占用内存,实际上在模糊运算过程当中可能会存在输入和输出并存的状况,此时内存将会有一个短暂的峰值。
这时候你必定会想到三个字母在你的脑海里挥之不去,它们就是『OOM』。
既然图片最终是要被模糊的,也看不太状况,还不如直接用一张采样后的图片,若是采样率为 2,那么读出来的图片只有原始图片的 1/4 大小,真是何乐而不为呢??
[Java]  纯文本查看 复制代码
?
1
2
3
BitmapFactory.Options options = new Options();
options.inSampleSize = 2 ;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), resId, options);




3.3 使用矩阵

用到 Bitmap 的地方,总会见到 Matrix。这时候你会想到什么?
『基友』
『是在下输了。。』
其实想一想,Bitmap 的像素点阵,还不就是个矩阵,真是你中有我,我中有你的交情啊。那么何时用矩阵呢?


大图小用用采样,小图大用用矩阵。
仍是用前面模糊图片的例子,咱们不是采样了么?内存是小了,但是图的尺寸也小了啊,我要用 Canvas 绘制这张图可怎么办?固然是用矩阵了:
方式一:
[Java]  纯文本查看 复制代码
?
1
2
3
4
5
Matrix matrix = new Matrix();
matrix.preScale( 2 , 2 , 0f, 0f);
//若是使用直接替换矩阵的话,在Nexus6 5.1.1上必须关闭硬件加速
canvas.concat(matrix);
canvas.drawBitmap(bitmap, 0 , 0 , paint);


须要注意的是,在使用搭载 5.1.1 原生系统的 Nexus6 进行测试时发现,若是使用 Canvas 的 setMatrix 方法,可能会致使与矩阵相关的元素的绘制存在问题,本例当中若是使用 setMatrix 方法,bitmap 将不会出如今屏幕上。所以请尽可能使用 canvas 的 scale、rotate 这样的方法,或者使用 concat 方法。


方式二:
[Java]  纯文本查看 复制代码
?
1
2
3
Matrix matrix = new Matrix();
matrix.preScale( 2 , 2 , 0 , 0 );
canvas.drawBitmap(bitmap, matrix, paint);


这样,绘制出来的图就是放大之后的效果了,不过占用的内存却仍然是咱们采样出来的大小。
若是我要把图片放到 ImageView 当中呢?同样能够,请看:
[Java]  纯文本查看 复制代码
?
1
2
3
4
5
Matrix matrix = new Matrix();
matrix.postScale( 2 , 2 , 0 , 0 );
imageView.setImageMatrix(matrix);
imageView.setScaleType(ScaleType.MATRIX);
imageView.setImageBitmap(bitmap);



3.4 合理选择Bitmap的像素格式

其实前面咱们已经屡次提到这个问题。ARGB8888格式的图片,每像素占用 4 Byte,而 RGB565则是 2 Byte。咱们先看下有多少种格式可选:
格式 描述
ALPHA_8 只有一个alpha通道
ARGB_4444 这个从API 13开始不建议使用,由于质量太差
ARGB_8888 ARGB四个通道,每一个通道8bit
RGB_565 每一个像素占2Byte,其中红色占5bit,绿色占6bit,蓝色占5bit


这几个当中,
ALPHA8 不必用,由于咱们随便用个颜色就能够搞定的。
ARGB4444 虽然占用内存只有 ARGB8888 的一半,不过已经被官方嫌弃,失宠了。。『又要占省内存,又要看着爽,臣妾作不到啊T T』。
ARGB8888 是最经常使用的,你们应该最熟悉了。
RGB565 看到这个,我就看到了资源优化配置无处不在,这个绿色。。(不行了,忽然好邪恶XD),其实若是不须要 alpha 通道,特别是资源自己为 jpg 格式的状况下,用这个格式比较理想。


3.5 高能:索引位图(Indexed Bitmap)

索引位图,每一个像素只占 1 Byte,不只支持 RGB,还支持 alpha,并且看上去效果还不错!等等,请收起你的口水,Android 官方并不支持这个。是的,你没看错,官方并不支持。
[Java]  纯文本查看 复制代码
?
01
02
03
04
05
06
07
08
09
10
public enum Config {
     // these native values must match up with the enum in SkBitmap.h
 
     ALPHA_8     ( 2 ),
     RGB_565     ( 4 ),
     ARGB_4444   ( 5 ),
     ARGB_8888   ( 6 );
 
     final int nativeInt;
}


不过,Skia 引擎是支持的,不信你再看:
[Java]  纯文本查看 复制代码
?
01
02
03
04
05
06
07
08
09
10
11
12
13
enum Config {
    kNo_Config,   //!< bitmap has not been configured
      kA8_Config,   //!< 8-bits per pixel, with only alpha specified (0 is transparent, 0xFF is opaque)
 
    //看这里看这里!!↓↓↓↓↓
     kIndex8_Config, //!< 8-bits per pixel, using SkColorTable to specify the colors 
     kRGB_565_Config, //!< 16-bits per pixel, (see SkColorPriv.h for packing)
     kARGB_4444_Config, //!< 16-bits per pixel, (see SkColorPriv.h for packing)
     kARGB_8888_Config, //!< 32-bits per pixel, (see SkColorPriv.h for packing)
     kRLE_Index8_Config,
 
     kConfigCount
};


其实 Java 层的枚举变量的 nativeInt 对应的就是 Skia 库当中枚举的索引值,因此,若是咱们可以拿到这个索引是否是就能够了?对不起,拿不到。
不行了,废话这么多,确定要挨板砖了T T。
不过呢,在 png 的解码库里面有这么一段代码:
[Java]  纯文本查看 复制代码
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
bool SkPNGImageDecoder::getBitmapColorType(png_structp png_ptr, png_infop info_ptr,
                                        SkColorType* colorTypep,
                                        bool* hasAlphap,
                                        SkPMColor* SK_RESTRICT theTranspColorp) {
png_uint_32 origWidth, origHeight;
int bitDepth, colorType;
png_get_IHDR(png_ptr, info_ptr, &origWidth, &origHeight, &bitDepth,
              &colorType, int_p_NULL, int_p_NULL, int_p_NULL);
 
#ifdef PNG_sBIT_SUPPORTED
   // check for sBIT chunk data, in case we should disable dithering because
   // our data is not truely 8bits per component
   png_color_8p sig_bit;
   if ( this ->getDitherImage() && png_get_sBIT(png_ptr, info_ptr, &sig_bit)) {
# if 0
     SkDebugf( "----- sBIT %d %d %d %d\n" , sig_bit->red, sig_bit->green,
              sig_bit->blue, sig_bit->alpha);
#endif
     // 0 seems to indicate no information available
     if (pos_le(sig_bit->red, SK_R16_BITS) &&
         pos_le(sig_bit->green, SK_G16_BITS) &&
         pos_le(sig_bit->blue, SK_B16_BITS)) {
         this ->setDitherImage( false );
     }
}
#endif
 
 
if (colorType == PNG_COLOR_TYPE_PALETTE) {
     bool paletteHasAlpha = hasTransparencyInPalette(png_ptr, info_ptr);
     *colorTypep = this ->getPrefColorType(kIndex_SrcDepth, paletteHasAlpha);
     // now see if we can upscale to their requested colortype
     //这段代码,若是返回false,那么colorType就被置为索引了,那么咱们看看如何返回false
     if (!canUpscalePaletteToConfig(*colorTypep, paletteHasAlpha)) {
         *colorTypep = kIndex_8_SkColorType;
     }
} else {
......
}
return true ;
}


canUpscalePaletteToConfig 函数若是返回false,那么 colorType 就被置为 kIndex_8_SkColorType了。
[Java]  纯文本查看 复制代码
?
01
02
03
04
05
06
07
08
09
10
11
12
static bool canUpscalePaletteToConfig(SkColorType dstColorType, bool srcHasAlpha) {
   switch (dstColorType) {
     case kN32_SkColorType:
     case kARGB_4444_SkColorType:
         return true ;
     case kRGB_565_SkColorType:
         // only return true if the src is opaque (since 565 is opaque)
         return !srcHasAlpha;
     default :
         return false ;
}
}


若是传入的 dstColorType 是 kRGB_565_SkColorType,同时图片还有 alpha 通道,那么返回 false~~咳咳,那么问题来了,这个dstColorType 是哪儿来的??就是咱们在 decode 的时候,传入的 Options 的inPreferredConfig。

 

下面是实验时间~
准备:在 assets 目录当中放了一个叫 index.png 的文件,大小192*192,这个文件是经过 PhotoShop 编辑以后生成的索引格式的图片。
代码:
[Java]  纯文本查看 复制代码
?
01
02
03
04
05
06
07
08
09
10
try {
    Options options = new Options();
    options.inPreferredConfig = Config.RGB_565;
Bitmap bitmap = BitmapFactory.decodeStream(getResources().getAssets().open( "index.png" ), null , options);
    Log.d(TAG, "bitmap.getConfig() = " + bitmap.getConfig());
    Log.d(TAG, "scaled bitmap.getByteCount() = " + bitmap.getByteCount());
    imageView.setImageBitmap(bitmap);
} catch (IOException e) {
     e.printStackTrace();
}


程序运行在 Nexus6上,因为从 assets 中读取不涉及前面讨论到的 scale 的问题,因此这张图片读到内存之后的大小理论值(ARGB8888):
192 * 192 *4=147456


好,运行咱们的代码,看输出的 Config 和 ByteCount:
[Java]  纯文本查看 复制代码
?
1
2
D/MainActivity: bitmap.getConfig() = null
D/MainActivity: scaled bitmap.getByteCount() = 36864


先说大小为何只有 36864,咱们知道若是前面的讨论是没有问题的话,那么此次解码出来的 Bitmap 应该是索引格式,那么占用的内存只有 ARGB 8888 的1/4是意料之中的;再说 Config 为何为 null。。额。。黑户。。官方说:
public final Bitmap.Config getConfig ()
Added in API level 1
If the bitmap’s internal config is in one of the public formats, return that config, otherwise return null.
再说一遍,黑户。。XD。

看来这个法子还真行啊,占用内存一下小不少。不过因为官方并未作出支持,所以这个方法有诸多限制,好比不能在 xml 中直接配置,,生成的 Bitmap 不能用于构建 Canvas 等等。


3.6 不要辜负。。。『哦,不要姑父!』

其实咱们一直在抱怨资源大,有时候有些场景其实不须要图片也能完成的。好比在开发中咱们会常常遇到 Loading,这些 Loading 一般就是几帧图片,图片也比较简单,只须要黑白灰加 alpha 就齐了。
『排期太紧了,这些给我出一系列图吧』
『好,不过每张图都是 300*30 0的 png 哈,总共 5 张,为了适配不一样的分辨率,须要出 xxhdpi 和 xxxhdpi 的两套图。。』
Orz。。。
若是是这样,你仍是自定义一个 View,覆写 onDraw 本身画一下好了。。。
相关文章
相关标签/搜索