近日有国外机构称:2017年第一季度,Android手机的故障率比苹果iPhone低。受调查的Android手机故障率仅为50%,相比之下iPhone则高达68%。iPhone故障率高的原因在于应用程序崩溃情况(28%)比Android手机(10%)高出近三倍。另外,很iPhone用户碰到其他问题,例如3%设备有GPS问题,0.5%用户有过热的问题。而Android手机则集中于信号(6%)和摄像头故障(3%)。
作者简介大家早上好,新的一周开始啦!
本篇是 潇潇凤儿 的第二篇投稿,从源码角度分析了Android的图片内存占用以及加载机制,希望对大家有所帮助!
潇潇凤儿 的博客地址:
分析开始http://blog.csdn.net/smileiam
在讲解图片占用内存前,我们先问自己几个问题:
我们在对手机进行屏幕适时,常想可不可以只切一套图适配所有的手机呢?
一张图片加载到手机中,占用内存到底有多少?
图片占用内存跟哪些东西有关?跟手机有关系么?同一张图片放在不同的dpi文件夹下内存占用会变化么?
如果是网络图片,加载到手机中,占用内存跟手机屏幕有关系么?
带着这些问题我们来一层层解析。我们先看看加载本地资源,不同手机所占内存情况:
一、加载本地资源,不同手机占内存情况
我们如果加载app内图片,想知道它占用多少内存,可先将此资源转成 bitmap 进行查看。
1. 从资源中获取bitmap
Bitmap bmp = BitmapFactory.decodeResource(getResources(), R.mipmap.testxh);
获取到 bitmap,我们还需要知道此 bitmap 在内存占多少空间,具体方法如下。
2. 获取图片大小
接下来就来测试,不同的手机、同一张图片放在不同的密度文件夹下,占用内存情况。
3. 同一图片在不同屏幕的手机、不同的屏幕密度文件夹下占用内存大小
(1). 经测试同一张图片分别放在不同的 mipmap文件夹(mipmap-hdpi, mipmap-xhdpi, mipmap-xxhdpi)下或是 drawable文件夹(drawable-hdpi, drawable-xhdpi, drawable-xxhdpi)下,相同的dpi下的文件夹下加载出来的图片,bitmap占用内存大小一样;
(2). 对于同一张图片,放在不同手机、不同的屏幕密度文件夹下占用内存情况又是如何呢,这里我们以一张大小为 1024*731 = 748544B, 大小为 485.11K 的图片为例,下面是测试手机占用的内存情况。
从上表可以看出不同屏幕密度的手机加载图片,如果图片放在与自己屏幕密度相同的文件夹下,占用的内存都是 2994176B,与图片本身大小 748544B 存在一个4倍关系,因为图片采用的ARGB-888色彩格式,每个像素点占用4个字节。
从上述测试可以得出,bitmap 占用内存大小,与手机的屏幕密度、图片所放文件夹密度、图片的色彩格式有关。
这里总结一下获取 Bitmap 图片大小的代码:手机在加载图片时,会先查找自己本密度的文夹下是否存在资源,不存在则会向上查找,再向下查找,并对图片进行相应倍数的缩放:
如果在与自己屏幕密度相同的文件夹下存在此资源,会原样显示出来,占用内存正好是: 图片的分辨率*色彩格式占用字节数;
若自己屏幕密度相同的文件夹下不存在此文件,而在大于自己屏幕密度的文件夹下存在此资源,会进行缩小相应的倍数的平方;
若在大于自己屏幕密度的文件夹下没找到此资源,则会向小于自己屏幕密度的文件夹下查找,如果存在,则会进行放大相应的倍数的平方,这两种情况图片占用内存为:占用内存=图片宽度 X 图片高度/((资源文件夹密度/手机屏幕密度)^2) * 色彩格式每一个像素占用字节数
4. 图片占用内存与图片的色彩格式的关系
我们在计算 bitmap 大小时,是通过计算 getRowBytes * bitmap.getHeight() 得来的,后面的乘数就是图片的高度,而第一个乘数 getRowBytes 是什么呢?我们根进 Bitmap 代码查看 getRowBytes 函数:
该方法最终调用的是 Bitmap 中的 native 方法:
private static native int nativeRowBytes(long nativeBitmap);
我们再查看对应的 Bitmap.cpp 里的 nativeRowBytes 方法
我们可以看到这里的 bitmap 形式是以 SkBitmap对象 展现的,这个 Bitmap 就和图片展示的色彩格式有关,我们再看看 SkBitmap 里是怎么计算 rowBytes 的:
可以看到,图片的宽乘以了一个 SkColorTypeBytesPerPixel(ct) 变量,对于不同色彩格式,每个像素占用的字节数就是在 SkColorTypeBytesPerPixel 中定义的。这就是为什么上面得出的 bitmap 大小,在自己屏幕密度的文件夹下图片占用的内存大小都被乘以了4,因为 bitmap 加载默认采用的是 RGBA_8888 编码格式。
那么手机怎么加载图片时,为什么同样的图片在不同的屏幕分辨率的手机上、不同的屏幕密度文件夹下占用内存会相差这么大呢?
在加载资源图片时,我们一般会借助于 BitmapFactory 的 decodeResource方法,此方法的源代码如下:
我们再来看看 BitmapFactory 的 decodeResourceStream 方法:
可以看到这里调用了 native decodeStream 方法:
inDensity,inTargetDensity,inScreenDensity, inScaled三者关系
通过追查代码,我们可以看到图片资源通过数据流解码时,会根据 inDensity,inTargetDensity,inScreenDensity 三个值和是否被缩放标识 inScaled
inDensity:图片本身的像素密度(其实就是图片资源所在的哪个密度文件夹下,如在 xxhdpi 下就是480,如果在asstes、手机内存/sd卡下,默认是160);
inTargetDensity:图片最终在 bitmap 里的像素密度,如果没有赋值,会将 inTargetDensity 设置成 inScreenDensity;
inScreenDensity:手机本身的屏幕密度,如我们测试的三星手机 dpi=640, 如果 inDensity 与 inTargetDensity 不相等时,就需要对图片进行缩放,inScaled = inTargetDensity/inDensity。
我们上面研究了加载应用程序的图片占用内存大小与手机屏幕密码和图片所放的密度文件夹、图片的编码格式有关,那如果加载的是网络图片或是本地图片,在不同的手机上占用内存又是否一样呢?
二、加载sd卡下的资源或是网络图片解析
手机无论是加载 sd卡图片,assets路径下 还是 网络图片,都需要先把图片读成数据流格式,再调用相应的 decodeStream方法,将数据流转成 bitmap形式,在调用 decodeStream 如果不设置 Options 的话,通过以上三款手机打印出图片所占内存大小均为:2994176B,也就是跟手机的屏幕密度没有关系。
那如果设置 Options 中的参数,图片占用的内存会不会与手机的屏幕密度有关系呢?我在测试中发现单独手动设置图片密度 inDensity 或是 inTargetDensity,并不起作用,图片占用内存一直都是图片本身大小。为什么没起作用呢,这需要我们从资源加载的源头看起。
我们先来看一下 BitmapFactory 的 decodeFile函数:
2. 根据网络地址获取图片Bitmap
可以看到通过路径加载图片,最终还是会调用 BitmapFactory 里的 decodeStream方法,我们再来看看 decodeStream方法。
3. 将数据流转成Bitmap
如果数据流来自于资源,则调用 BitmapFactory 的 nativeDecodeAsset:
private static native Bitmap nativeDecodeAsset(long nativeAsset, Rect padding, Options opts);
否则调用 decodeStreamInternal方法:
此方法会调用 native 的 nativeDecodeStream方法。
4. native层的数据流解析
通过追踪上述两种 nativeDecodeStream方法 和 nativeDecodeAsset方法,它们最终都会调用 nativeDecodeStreamScaled 或是 nativeDecodeStreamScaled方法,它们会添加两个参数,一个是 false,一个是 1.0f,这两个参数具体代表什么呢?
nativeDecodeAssetScaled 或是 nativeDecodeStreamScaled 方法中最后两个参数,分别是 applyScale, sclae,一个是是否申请缩放,一个是缩放比例,也就是从这种数据流加载的图片,默认都不会进缩放。我们注意到,这两个函数最终都会走到 doDecode方法 里,我们直接看 nativeDecodeStreamScaled方法,发现此方法只是对输入流进行了转换,转成 SkStream类型。
我们来看最终的 doDecode函数:
通过上面分析直至 native 中的 decode函数,我们发现 options 里的参数只提取了sampleSize、optionsJustBounds,但是没有见到 inDensity,inTargetDensity,inScreenDensity 等参数的提取。如果我在加载流前,设置 ops.inDensity 和 ops.inTargetDensity 参数如下,图片占用内存大小会缩小到原来的1/4
但是如果只设置 inDensity 或是 inTargetDensity 参数,是完全不起作用,感觉是因为只设置了一个参数,另一个参数默认为0, 前面咱们判断过,只要有一个参数为0, 就不会计算缩放比。所以默认还是显示原来图片尺寸大小,只有两个参数均设置,都不为0, 才会去计算缩放比。
通过上面的分析,我们可以回答最开始的问题了。
结论
1. 在对手机进行屏幕适时,可以只切一套图适配所有的手机。
但是如果只切一套小图,那在高屏幕密度手机上,会对图片进行放大,这样图片占用的内存往往比切相应图片放在高密度文件夹下,占用的内存还要大。
那如果只切一套大图放在高幕文件夹下,在小屏幕密度手机上,会缩小显示,按道理是行得通的。但系统在对图片进行缩放时,会进行大量计算,会对手机的性能有一定的影响。同时如果图片缩放比较狠,可能导致图片出现抖动或是毛边。
所以最好切出不同比便的图片放在不同幕度的文件夹下,对于性能要求不大高的图片,可以只切一套大图;
2. 一张图片占用内存=图片长 * 图片宽 / (资源图片文件密度/手机屏幕密度)^2 * 每一象素占用字节数,所以图片占用内存跟图片本身大小、手机屏幕密度、图片所在的文件夹密度,图片编码的色彩格式有关;
3. 对于网络图片,在不同屏幕密度的手机上加载出来,占用内存是一样的。
4. 对于网络或是assets/手机本地图片加载,如果想通过设置 Options 里的 inDensity 或是 inTargetDensity 参数来调整图片的缩放比,必须两个参数均设置才能起作用,只设置一个,不会起作用。
5. drawable 和 mipmap 文件夹存放图片的区别,首先图片放在 drawable-xhdpi 和 mipmap-xhdpi 下,两者占用的内存是一样的, Mipmaps 早在Android2.2+就可以用了,但是直到4.3 google才强烈建议使用。把图片放到 mipmaps 可以提高系统渲染图片的速度,提高图片质量,减少GPU压力。其他并没有什么区别。
更多每天学习累了,看些搞笑的段子放松一下吧。关注最具娱乐精神的公众号,每天都有好心情。
如果你有好的技术文章想和大家分享,欢迎向我的公众号投稿,投稿具体细节请在公众号主页点击“投稿”菜单查看。
欢迎长按下图 -> 识别图中二维码或者扫一扫关注我的公众号: