一、前言
本来只想说下Bitmap和内存的基本关系,但发现如果真的想把这看似简单的事情说清楚,实际上未必那么简单,你不信?不妨先尝试下回答下面几个问题!
问1:什么是dpi?什么是dp?答:你在侮辱我?我拒绝回答!(:无辜脸
问2:以Nexus6为例,分辨率1440*2560,5.96英寸,ppi是什么?dpi是什么?1dp是多少像素?
问3:还以Nexus6为例,一张180*180的图片,放置在设置了wrap_content属性的ImageView中,当这张图片,分别放在drawable-nodpi,drawable-mdpi,drawable-hdpi, drawable-xxhdpi,drawable-xxxhdpi这几个资源目录中,在屏幕上分别显示多大(像素)的图片?
问4:这张图片占用的内存大小分别为多少?
问5:如果设置ImageView的宽和高为固定的值,如50px,那么上述情况下,加载的Bitmap占用内存大小分别为多大?
本文主要围绕上述问题,介绍Android图片资源加载的基本机制,图片的内存大小如何计算,以及内存图片分析工具。
二、基础概念px:像素(pixel),指的是屏幕上的物理点,最小的独立显示单位
ppi:每英寸像素点(Pixels Per Inch),之前我自己的理解就是使用勾股定理,通过屏幕长宽计算出对角线的长度,再除以屏幕对角线英寸值。
dpi:每英寸点(Dots Per Inch),和ppi有什么区别?
dp:像素无关点(Density-Independent pixel),这个Android定义的虚拟值,和px关系式是px = dp * (dpi / 160),为啥?
ppi
ppi指的是水平方向或者竖直方向上每英寸的像素值,先说为啥很多对ppi的理解都有问题。很多文章利用勾股定理计算手机屏幕对角线像素个数!!但是手机屏幕的像素分布是点阵分布,点阵的对角线像素点不是用勾股定理算出来的,而是等于它的行数或者列数,为啥用对角线计算?因为一般手机的尺寸说的就是对角线英寸值,而不知道长宽的英寸值。这里有个公式推导:
设 X/x = Y/y = ppi,这里X为水平像素值,x为水平英寸值,Y为竖直像素值,y为竖直英寸值。则(X²+Y²)/(x²+y²)=(X/x)²=(Y/y)²,这个公式可通过勾股定理和相似三角形原理证明。ppi的计算只是形式和勾股定理一样但并不是勾股定理的意思,分子也不是对角线的像素数!!ppi的计算公式可表示为:√(X²+Y²)/对角线英寸。但不意味着ppi是使用勾股定理计算对角线像素值获得的~~
dpi
dpi是由ppi确定的,还是以Nexue6为例,通过ppi的计算公式,可以计算出Nexues6的ppi为492,参照下表,应该在xxxhdpi的范围内,但它的dpi并不是492。实际上dpi只有120(low)、160(medium)、240(high)、260、280、300、320(xhigh)、340、360、400、420、480(xxhpdi)、560、640(xxxhpi)这几种,可以参照android.util.DisplayMetrics的源码。通过getResources().getDisplayMetrics().densityDpi可以获取手机实际的dpi。
dp
1dp在Nexues6上是多少像素?
1dp * (560/160) = 3.5px
如何更改手机的dpi
需要root的手机
更改/system/build.prop中的ro.sf.lcd_density属性,这个值越小,屏幕密度越高
adb reboot 重启手机
如何快速查看手机屏幕信息
adb shell dumpsys display | findstr DisplayDeviceInfo
三、回到问题
以Nexus6为例,一张180*180的图片,放在设置了wrap_content属性的ImageView中,当这张图片,分别放置在drawable-nodpi,drawable-mdpi,drawable-hdpi, drawable-xxhdpi,drawable-xxxhdpi这几个资源目录中,在屏幕上分别显示多大(像素)的图片?
答:根据上面的分析,可以看到nexus6,是属于xxxhpi的屏幕,获取到dpi为560。如果把180大小的图片只放在xxxhdpi目录中,是不是屏幕就会显示180px的大小?其实并不是,dpi=640的才是真正的xxxhdpi(这里需要看DisplayMetrics的源码),当Android系统加载图片时,会针对当前系统的dpi和图片目录的source dpi比较做相应比例的缩放,如果一张图片放在drawable-xxxhdpi目录,这是告诉系统,针对dpi为640的手机屏幕上,这张图片是刚刚好的,它的scale为1.0,同理,drawable-xxhpi对应480dpi,drawable-xhdpi对应的是320dpi。如果希望180*180在nexues6的屏幕上按照原有尺寸显示,只有将其放在drawable-nodpi或者drawable-560dpi目录中才可以。
回到问题,如果将180*180放入hdpi目录中,那实际显示的图片大小应该为:int (180 * (560 / 240) +0.5f ) = 420px,实际图片的大小应该为420px!同理:
放入xhdpi目录中,实际大小应该为int (180 * (560 / 320) +0.5f ) = 315px
放入xxhdpi目录中,实际大小应该为int (180 * (560 / 480) +0.5f ) = 210px
如果用小的Bitmap容器,放置较大的Bitmap,那这个Bitmap的尺寸大小是按照容器的大小计算?还是按照dpi缩放的规则计算?
四、一张图片占多大内存
现在我们已经知道了一张图片放置在不同的资源目录中,系统decode的bitmap的尺寸大小是如何计算的了。一张图片大概占用多少内存,其实只用看一个像素占用多少内存就可以喽?我们都知道屏幕上的颜色是有R,G,B加透明度表示的,Android官方支持的RGB格式,主要是ALPHA_8,RGB_565,ARGB_4444,ARGB_8888这几种。
以ARGB_8888为例,表示一个像素点,使用8位表示透明度,8位表示Red,8位表示Green,8位表示Blue。加起来,一个像素就需要4byte,同理,RGB_565一个像素需要2byte。
1
2
3
4
5
6
7
8
9
10
11public enum Config {
ALPHA_8 (1),
RGB_565 (3),
@Deprecated
ARGB_4444 (4),
ARGB_8888 (5);
final int nativeInt;
Config(int ni) {
this.nativeInt = ni;
}
}
Android decode资源图片时默认会选择ARGB_8888,如果将180*180放入hdpi目录中,那实际大小应该为int (180 * (560 / 240) +0.5f ) = 420px,像素内存大小为 420 * 420 * 4 = 705600byte = 689kb。同理:
放入xhdpi目录中,实际大小应该为int (180 * (560 / 320) +0.5f ) = 315px,像素内存大小为315 * 315 * 4 = 396900byte = 387.6kb
放入xxhdpi目录中,实际大小应该为int (180 * (560 / 480) +0.5f ) = 210px,像素内存大小为210 * 210 * 4 = 176400byte = 172.2kb
五、如何分析内存中的图片问题?
说了这么多,终于到了我原本想说的。Android的内存优化需要重点关注的几个方面,Bitmap的加载,Activity的泄露,webview内存等。图片的内存问题,是很多Android应用,特别是电商APP需要首要关注的问题。那如何简单的分析出内存中Bitmap的情况,这里提供一个工具。借助leakcanary的思路,通过分析内存堆dump文件中android.graphics.Bitmap对象的情况,将内存中Bitmap对象批量保存到图片文件中,这样就可以直观的看到内存中加载的Bitmap个数、内容和大小,进而分析可能的内存问题。
示例
以分析蘑菇街APP内存中bitmap为例:
先使用Android monitor dump hprof
hprof-conv做格式转换
java -jar ./bitmap-analyzer-0.0.1-SNAPSHOT.jar /Users/Ivonhoe/hprof/com.mogujie-conv.hprof 蘑菇街
参数说明:
参数1:需要处理hprof文件的绝对路径
参数2:图片输出的文件夹名称,在hprof文件目录中创建该文件夹,缺省时直接会在hprof文件目录输出文件
输出说明
大小排序.txt 按照图片像素尺寸,从大到小排序
个数排序.txt 按照相同尺寸图片个数,从大到小排序
文件名 width*height_index.png
Jar包地址
六、参考文档