Image

内存所在位置

Android 2.3 之前: 像素数据存在于native heap

Android 3.0 ~ 7.1 之间: 存在于 java heap

Android 8.0及之后: 存在于 native

为什么 2.3.3 ~ 7.0 要放到 Java 堆? 直接放到 Native 中, 然后在 Java 对象 finalize 调用的时候释放不行吗?

  • Java 层的 Bitmap 对象是一个壳, 非常小, 因此有可能会出现 Native 堆快到了 3G, Java 堆才 10 MB, 10MB 是无法触发 Dalvik GC 的, 因此这个 java 对象的 finalize 并非那么容易调用, 因此可能会出现 Native 堆 OOM 的情况, 故需要我们手动 recycle
  • 像素数据直接放置到 Java 堆, Java 堆就能直接统计到真正的内存数据, 能够根据内存使用情况准确触发 GC 回收数据
    • 隐患便是 Java 堆内存空间比较小, 容器造成 Java 堆的 OOM

为什么 8.0 又放置到了 Native 堆中?

  • 使用 NativeAllocationRegistry 解决了这个问题, 触发 ART 堆 GC 的条件不仅仅是堆占用不足, 通过 VMRuntime.registerNativeAllocation 注册的 Native 内存累计超过了阈值(4MB)之后时也会触发 GC
  • 而且 ART 的 GC 性能比 Dalvik 好的多, 不会轻易造成主线程卡顿

bitmap怎么释放:

在 Android 2.3.3 之前开发者必须手动调用 recycle 方法去释放 Native 内存,因为那个时候管理Bitmap内存比较复杂,需要手动维护引用计数器

android6及之前:Bitmap 的内存回收主要是通过 BitmapFinalizer 来完成的,通过覆盖**finalize()**中调用native的析构函数来释放native内存。

Android8及之后:会在 new Bitmap 时会注册 native 的 Finalizer 方法NativeAllocationRegistry.registerNativeAllocation

其实无论是 Android M 前还是之后,释放 Native 层的 Bitmap 对象的思想都是去监听 Java 层的 Bitmap 是否被释放,一旦当 Java 层的 Bitmap 对象被释放则立即去释放 Native 层的 Bitmap 。只不过 Android M 前是基于 Java 的 GC 机制和finalize(),而 Android M 后是注册 native 的 Finalizer 方法。

其中 Fresco 可以留用匿名共享内存 ,在5.0之前像素存在于java heap上时,将像素优化至 Ashmem 匿名共享内存上(近似native)

image-20210930160022162

Android 哪个版本之前AS可以在profiler中预览图片

The “view bitmap” feature is still there (for Android 5.0 to 7.1)

因为profiler 分析的 hprof 中,在8.0版本前,bitmap是存在java heap中的,但是由于8.0之后bitmap回到了native heap,而hprof只分析java heap,故而只能在Android 5.0 to 7.1中使用view bitmap

小结:

2.3之前的像素存储需要的内存是在native上分配的,并且生命周期不太可控,可能需要用户自己回收。 2.3-7.1之间,Bitmap的像素存储在Dalvik的Java堆上,

当然,4.4之前的甚至能在匿名共享内存上分配(Fresco采用),而8.0之后的像素内存又重新回到native上去分配,不需要用户主动回收,8.0之后图像资源的管理更加优秀,极大降低了OOM。

图片内存尺寸

尺寸计算:

image-20210410163522383

//PS: 小米8UD 系统赋的dpi为440(计算值401),density为2.75(计算值为2.5)

mipmap图片资源计算

inDensity表示该资源来源于哪个密度的文件夹,该值从TypedValue获取;

inTargetDensity表示该资源将要显示在哪个密度的设备上。

needSize = (int)(size * ((float)inTargetDensity / inDensity) + 0.5) (四舍五入)

不同目录对应的dpi具体为:

image-20240304153838146

一张172*172的图片放在hdpi目录下,则在dpi为420的设备中尺寸是多少?

1、hdpi密度是240 因此Options.inDesnity = 240
2、设备密度是420 因此Options.inTargetDensity = 420;
3、设备返回bitmap大小=172 * 420 / 240 = 301px

通过上面对dp的了解,我们知道在设定view大小、间距时使用dp能最大限度地屏蔽设备密度之间的差异。可能你就会问了,那bitmap展示的时候如何适配不同密度的设备呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
scss
复制代码 @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(bitmap.getWidth(), bitmap.getHeight());
}

private void init() {
String path = Environment.getExternalStorageDirectory() + "/Download/photo1.jpg";
bitmap = BitmapFactory.decodeFile(path);
paint = new Paint();
paint.setAntiAlias(true);
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

Rect src = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
RectF rectF = new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight());
canvas.drawBitmap(bitmap, src, rectF, paint);
}

自定义view从磁盘上加载一张图片,并将之显示在view上,view的大小决定于bitmap大小。依旧以上述A、B设备为例,展示结果如下:

image.png

左边是A设备,右边是B设备。
明显地看出,在A设备显示比B设备大很多,实际上和我们之前用px来描述view的大小原理是一样的,bitmap的宽、高都是px在描述,而bitmap决定了view的宽、高,最终导致A设备和B设备上的view大小(宽、高像素)是一样的,而它们屏幕密度又不相同,因此产生了差异。
那不会每次都需要我们自己根据屏幕密度来转换bitmap大小吧?幸运的是,Android已经为我们考虑到了。

image.png

如上图,在Android Studio创建工程的时候,默认在res下创建mipmap目录,这些mipmap目录按照密度分为mdpi/hdpi/xhdpi/xxhdpi/xxxhdpi,看起来都在“一个“mipmap”目录下,实际上分为不同的目录:

image.png

生成不同密度的目录有什么作用?
A设备dpi=240,根据dpi范围,属于hdpi
B设备dpi=420,根据dpi范围,属于xxhdpi
图片原始尺寸:photo1.jpg(宽高 172px-172px)
当我们想要在不同密度设备上显示同一张图片并且想要“看起来一样大时”。假设设计的时候以hdpi为准,放置photo1.jpg为172*172,那么根据计算规则在xxhdpi上需要设置photo1.jpg为:

scale = 480 / 240 = 2
width = 172 * 2 = 344
height = 172 * 2= 344
注:这里为什么要放大?可以这么理解,因为B设备密度大,通常来说密度越大单位尺寸内需要的像素越多,假设A设备上172*172占据1inch面积,那么为了能够在B设备上填充满相同的面积需要更多的像素,因此B设备上的图片分辨率应该更大(这里说的通常是因为真正决定设备单位尺寸内容纳的像素个数的因素是ppi,有些设备dpi比较大,但是ppi反而小)

现在hdpi和xxhdpi目录下分别存放了同名图片:photo1.jpg,只是大小不同。当程序运行的时候:

A设备发现自己密度属于hdpi,它会直接到hdpi下寻找对应的photo1.jpg并显示
B设备发现自己密度属于xxhdpi,它会直接到xxhdpi下寻找对应的photo1.jpg并显示

来看看效果:

image.png

左边A设备,右边B设备 针对不同的密度设计不同的图片大小,最大限度保证了同一图片在不同密度设备上表现“看起来差不多大”。
来看看A、B设备上图片占内存大小:

A设备 172 * 172 * 4 = 118336 ≈ 116k
B设备 344 * 344 * 4 = 473344 ≈ 462k
注:解析bitmap时,默认inPreferredConfig=ARGB_8888,也就是每个像素有4个字节来存储

说明在B设备上显示photo1.jpg需要更多的内存。
上边只是列举了hdpi、xxhdipi,同理对于mdpi、xhdpi、xxxhdpi根据规则放入相应大小的图片,程序会根据不同的设备密度从对应的mipmap文件夹下加载资源。如此一来,我们无需关注bitmap在不同密度设备上显示问题了。

只保留一套尺寸的资源

在mipmap各个文件夹下都放置同一套资源的不同尺寸文件似乎有点太占apk大小,能否只放某个密度下图片,其余的靠系统自己适配呢?
现在只保留hdpi下的photo1.jpg图片,看看在A、B设备上运行情况如何:

image.png

看起来和上张图差不多,说明系统会帮我们适配B设备上的图片。
再来看看A、B设备上图片占内存大小:
先看A设备:

image.png

再看B设备:

image.png

A设备 172 * 172 * 4 = 118336 ≈ 116k
B设备 301 * 301 * 4 = 362404 ≈ 354k

对比photo1.jpg 分别放在hdpi、xxhdpi和只放在hdpi下可以看出:B设备上图片所占内存变小了。为什么呢?接下来从源码里寻找答案。

构造Bitmap

1
2
ini
复制代码Bitmap bitmap = BitmapFactory.decodeResource(getContext().getResources(), R.mipmap.photo1);

A、B设备同样加载hdpi/photo1.jpg,返回的bitmap大小不相同,我们从这方法开始一探究竟。

1
2
3
4
5
6
7
8
9
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
public static Bitmap decodeResource(Resources res, int id, BitmapFactory.Options opts) {
validate(opts);
Bitmap bm = null;
InputStream is = null;

try {

final TypedValue value = new TypedValue();
//根据资源id,构造Value对象,这里面需要关注的变量:density
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 BitmapFactory.Options opts) {
validate(opts);
if (opts == null) {
opts = new BitmapFactory.Options();
}

if (opts.inDensity == 0 && value != null) {
//通过value里的density给options里的inDensity赋值
final int density = value.density;
if (density == TypedValue.DENSITY_DEFAULT) {
opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
} else if (density != TypedValue.DENSITY_NONE) {
opts.inDensity = density;
}
}

if (opts.inTargetDensity == 0 && res != null) {
//获取设备屏幕密度并赋予opts.inTargetDensity
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}

//确定option inDensity、inTargetDensity 后传入jni层加载bitmap
return decodeStream(is, pad, opts);
}

上面涉及到的关键点是density,分别是TypedValue的density和Options的density。
先来看看TypedValue density:

1
2
3
4
/**
* If the Value came from a resource, this holds the corresponding pixel density.
* */
public int density;

简单解释:表示该资源从哪个密度文件夹下取的;比如A、B设备取hdpi下的photo1.jpg,那么此时density=240

再来看看Options density

1
2
3
4
5
6
7
8
9
10
11
* The pixel density to use for the bitmap.  This will always result
* in the returned bitmap having a density set for it

public int inDensity;

* The pixel density of the destination this bitmap will be drawn to.
* This is used in conjunction with {@link #inDensity} and
* {@link #inScaled} to determine if and how to scale the bitmap before
* returning it.

public int inTargetDensity;

简单解释:inDensity表示该资源来源于哪个密度的文件夹,该值从TypedValue获取; inTargetDensity表示该资源将要显示在哪个密度的设备上。在构造Bitmap时,会根据inDensity与inTargetDensity决定Bitmap放大缩写的倍数。
计算公式如下:
needSize = (int)(size * ((float)inTargetDensity / inDensity) + 0.5) (四舍五入)

现在分析B设备加载hdpi/photo1.jpg如何做的:

1、hdpi密度是240 因此Options.inDesnity = 240
2、B设备密度是420 因此Options.inTargetDensity = 420;
3、B设备返回bitmap大小=172 * 420 / 240 = 301px

链接:https://juejin.cn/post/7015597280120176676

大图加载:

按比例采样缩小,参见https://developer.android.com/topic/performance/graphics/load-bitmap?hl=zh-cn

巨图按区域显示:

BitmapRegionDecoder显示区域 + 手势移动显示中心

参见https://blog.csdn.net/lmj623565791/article/details/49300989

图片格式

图片格式-webp

WEBP相较传统图片格式(jpg/png/gif)会有较大的文件体积优势(25%以上),但会导致解码时间提升1.5倍以上。即webp有更小的体积,节省带宽,apk体积,更快的加载速度(因为体积雄安),但会少许增加cpu压力

“编解码速度上,根据Google的测试,目前WebP与JPG相比较,毫秒级别上,编码速度慢10倍,解码速度慢1.5倍。编码速度即可被没影响,我们只是在上传时生成一份WebP图片。解码速度则需要客户端综合节省下的流量来综合考虑。总之带宽节省比cpu消耗更有价值”

图片格式-JPG/JPEG

JPG 和 JPEG是同一个东西,不过是因为window平台早期只支持三个字符的文件格式,将JPEG缩写为JPG。

JPG是有损压缩的,是1600万位颜色,不支持透明度的,存储体积较小的

图片格式-PNG

PNG文件利用特殊的编码方法标记重复出现的数据,因而对图像的颜色没有影响,也不可能产生颜色的损失,这样就可以重复保存而不降低图像质量。

PNG是无损压缩的,是1600万位颜色,支持透明度的,存储体积较大的

图片格式-GIF

目前使用范围最广的动图格式,具体为GIF87a版本

GIF 格式的文件按块存储,整体上分为三部分:

  • 文件头(Header)

  • GIF 数据流(GIF Data Stream)

  • 文件结尾(Trailer)

GIF是无损压缩的,是256位颜色,不支持透明度的,存储提交稍大的

PNG JPEG GIF
压缩算法 无损压缩 有损压缩 无损压缩
透明度 保持图像透明度 不保持图像透明度 不支持透明度
图片尺寸 更小 较大
画面质量 更好的 相比PNG不够好 较差
可用颜色 1600万 2^24 1600万 2^24 256 2^8

img

GIF file stream diagram

We will learn more by walking through a sample GIF file. You can see the sample file and its corresponding bytes below.

Actual Size Enlarged Bytes
sample gif, actual size (10x10) sample gif, enlarged (100x100) 47 49 46 38 39 61 0A 00 0A 00 91 00 00 FF FF FF FF 00 00 00 00 FF 00 00 00 21 F9 04 00 00 00 00 00 2C 00 00 00 00 0A 00 0A 00 00 02 16 8C 2D 99 87 2A 1C DC 33 A0 02 75 EC 95 FA A8 DE 60 8C 04 91 4C 01 00 3B

Header Block

From the sample file: 47 49 46 38 39 61 , ASCII解码即为 GIF89a

Logical Screen Descriptor

From Sample File: 0A 00 0A 00 91 00 00

Global Color Table

From the sample file: FF FF FF FF 00 00 00 00 FF 00 00 00

Graphics Control Extension

From the sample file: 21 F9 04 00 00 00 00 00

Image Descriptor

From the sample file: 2C 00 00 00 00 0A 00 0A 00 00

Local Color Table(Opetional)

Null

Image Data

From the sample file: 02 16 8C 2D 99 87 2A 1C DC 33 A0 02 75 EC 95 FA A8 DE 60 8C 04 91 4C 01 00

Plain Text Extension

Example (not in the sample file): 21 01 0C 00 00 00 00 64 00 64 00 14 14 01 00 0B 68 65 6C 6C 6F 20 77 6F 72 6C 64 00

Application Extension

Example (not in sample file): 21 FF 0B 4E 45 54 53 43 41 50 45 32 2E 30 03 01 05 00 00

Comment Extension

Example (not in sample file): 21 FE 09 62 6C 75 65 62 65 72 72 79 00

Trailer

From sample file: 3B //This is the end flag

The trailer block indicates when you’ve reached the end of the file. It is always a byte with a value of 3B.

Author

white crow

Posted on

2021-04-01

Updated on

2024-03-25

Licensed under