很久之前写的了,发了吧,原用来总结学习的,再不发估计转行了,文章也参考了一些资料,抠用了一些图,主要是为了说明问题,总结学习
前言
app开发中,图片是少不了的。各种图标图片资源,如果不能很好的处理图片的利用。会导致app性能严重下降,影响用户体验,最直观的感受就是卡顿,手机发热,有时候还OOM
android系统给每个app分配有一定的内存,android系统的进程(app级别)有最大内存限制,超过这个限制系统就会抛出OOM错误。虽然在4.0后可以通过在application节点中设置属性android:largeHeap=”true”来突破这个上限,但是由于图片处理不当带来的影响总是影响app性能的
这里引申一下关于android系统给app分配的内存:
ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
int memorySize = activityManager.getMemoryClass();
Android根据设备屏幕尺寸和dpi的不同,给系统分配的单应用程序内存大小也不同,可以实践一下通过上面的方法获取应用的内存,跟下表对比下:
屏幕尺寸 | DPI | 应用内存 |
---|---|---|
small / normal / large | ldpi / mdpi | 16MB |
small / normal / large | tvdpi / hdpi | 32MB |
small / normal / large | xhdpi | 64MB |
small / normal / large | 400dpi | 96MB |
small / normal / large | xxhdpi | 128MB |
xlarge | mdpi | 32MB |
xlarge | tvdpi / hdpi | 64MB |
xlarge | xhdpi | 128MB |
xlarge | 400dpi | 192MB |
xlarge | xxhdpi | 256MB |
抛砖
oom产生原因
-
一个页面一次加载过多图片
-
加载大图片没有进行压缩(尺寸,质量)。。直接使用了imageView.setImageResource()
-
android列表加载大量bitmap没有使用缓存。。。
android 支持的图片
- png:
无损压缩,比较大,需要进行压缩,网站tinypng,一般都是让美工处理。但解码相对简单
- jpeg:
有损压缩,不支持透明通道,比如在ps里背景透明的图片,保持成jpg就不透明了,这里不深入了解。但是解码相对复杂
- webp:
google2010发布,支持有损无损压缩,支持透明通道,所以对图片质量和大小有限制的情况下,webp是首选
- gif:
系统本身不支持,三方图片库支持:glide,fresco
关于android中图片格式的使用,谷歌官方建议:尽量少使用png文件,建议使用webp格式的图片,相比png小45%。所以,项目中图片格式该如何平衡,这个还需要美工结合技术需求拿捏,结合每种格式图片的优缺点,合理规划开发。既然谷歌建议了,那大概是考虑到png占内存大导致的,app开辟的运行内存是一定的,当然内存开销越小,app越流畅嘛
图片存储优化
图片占用内存计算
这里的图片占用内存是指在Navtive中占用的内存,当然BitMap使用的绝大多数内存就是该内存。
因此我们可以简单的认为它就是BitMap所占用的内存
Android中一张图片(BitMap)占用的内存主要和以下几个因数有关:图片长度,图片宽度,单位像素占用的字节数,图片长度和图片宽度的单位是像素。所以有如下计算公式:
内存 = 图片长度 * 图片宽度 * 单位像素占用的字节数
这里注意一下,图片(BitMap)占用的内存应该和屏幕密度(Density)无关,创建一个BitMap时,其单位像素占用的字节数由其参数BitmapFactory.Options的inPreferredConfig变量决定。inPreferredConfig为Bitmap.Config类型。Bitmap.Config类是个枚举类型,如下:
Bitmap.Config | description |
---|---|
ALPHA_8 | Each pixel is stored as a single translucency (alpha) channel. This is very useful to efficiently store masks for instance. No color information is stored. With this configuration, each pixel requires 1 byte of memory.</br>此时图片只有alpha值,没有RGB值,一个像素占用一个字节 |
ARGB_4444 | This field is deprecated. Because of the poor quality of this configuration, it is advised to use ARGB_8888instead. </br>这种格式的图片,看起来质量太差,已经不推荐使用。</br>Each pixel is stored on 2 bytes. The three RGB color channels and the alpha channel (translucency) are stored with a 4 bits precision (16 possible values.) This configuration is mostly useful if the application needs to store translucency information but also needs to save memory. It is recommended to use ARGB_8888 instead of this configuration.</br>一个像素占用2个字节,alpha(A)值,Red(R)值,Green(G)值,Blue(B)值各占4个bites,共16bites,即2个字节 |
ARGB_8888 | Each pixel is stored on 4 bytes. Each channel (RGB and alpha for translucency) is stored with 8 bits of precision (256 possible values.) This configuration is very flexible and offers the best quality. It should be used whenever possible </br>一个像素占用4个字节,alpha(A)值,Red(R)值,Green(G)值,Blue(B)值各占8个bites,共32bites,即4个字节 </br>这是一种高质量的图片格式,电脑上普通采用的格式。它也是Android手机上一个BitMap的默认格式。 |
RGB_565 | Each pixel is stored on 2 bytes and only the RGB channels are encoded: red is stored with 5 bits of precision (32 possible values), green is stored with 6 bits of precision (64 possible values) and blue is stored with 5 bits of precision. This configuration can produce slight visual artifacts depending on the configuration of the source. For instance, without dithering, the result might show a greenish tint. To get better results dithering should be applied. This configuration may be useful when using opaque bitmaps that do not require high color fidelity.</br> 一个像素占用2个字节,没有alpha(A)值,即不支持透明和半透明,Red(R)值占5个bites ,Green(G)值占6个bites ,Blue(B)值占5个bites,共16bites,即2个字节.对于没有透明和半透明颜色的图片来说,该格式的图片能够达到比较的呈现效果,相对于ARGB_8888来说也能减少一半的内存开销。因此它是一个不错的选择。另外我们通过android.content.res.Resources来取得一个张图片时,它也是以该格式来构建BitMap的. |
举个例子,图片大小的计算:
图片格式 | 公式 | 一张100 * 100的图片占用内存大小 |
---|---|---|
ALPHA_8 | 图片长度 * 图片宽度 | 100 * 100=10000字节 |
ARGB_4444 | 图片长度 * 图片宽度 * 2 | 100 * 100 * 2 = 20000字节 |
ARGB_8888 | 图片长度 * 图片宽度 * 4 | 100 * 100 * 4 = 40000字节 |
RGB_565 | 图片长度 * 图片宽度 * 2 | 100 * 100 * 2 = 20000字节 |
注意: ARGB _ 4444 从Android4.0开始,该选项无效。即使设置为该值,系统任然会采用 ARGB _ 8888 来构造图片,系统在把res的图片解析成bitmap时默认是采用ARGB_8888的配置,如下源码
if (config != null) {
switch (config) {
case RGB_565:
newConfig = Config.RGB_565;
break;
case ALPHA_8:
newConfig = Config.ALPHA_8;
break;
case RGBA_F16:
newConfig = Config.RGBA_F16;
break;
//noinspection deprecation
case ARGB_4444:
case ARGB_8888:
default:
newConfig = Config.ARGB_8888;
break;
}
}
图片解码格式枚举下面在讲解图片存储优化-质量压缩时候会给出例子
图片存储优化
上面了解了尽量减少PNG图片的大小是Android里面很重要的一条规范,下面我们需要了解一下为什么要做内存的优化,如何做:
Android的Heap空间是不会自动做兼容压缩的,意思就是如果Heap空间中的图片被收回之后,这块区域并不会和其他已经回收过的区域做重新排序合并处理,那么当一个更大的图片需要放到heap之前,很可能找不到那么大的连续空闲区域,那么就会触发GC,使得heap腾出一块足以放下这张图片的空闲区域,如果无法腾出,就会发生OOM
heapgc.png所以,把图片做小,图片内存重用这是眼前的解决方案,我们可以从三个方面来降低图片内存开销:
imgstorageoptimization.png尺寸压缩
关于图片缩放,有几种方法:
1:Pre-scaling Bitmaps
2:inSampleSize
第一种,android中经常会做图片的缩放,所以,预缩放意义很明显,能缩小图片(这里不单单是缩放图片尺寸,而是操作的bitmap),降低内存分配,提升显示性能,api为createScaledBitmap()。如下:
/**
* bitmap指定宽高
* @param bitmap
* @param width
* @param height
* @return
*/
public static Bitmap resizeBitmap(Bitmap bitmap, int width, int height) {
return Bitmap.createScaledBitmap(bitmap, width, height, true);
}
第二种是inSampleSize,作用是对原图降采样,通过设置inJustDecodeBounds = true 在图片不加载进内存的情况下能获取图片宽高,计算合适的压缩比,设置inSampleSize。
inSampleSize具体原理是直接从点阵中隔行抽取最有效率,所以为了兼顾效率, inSampleSize只能是2的整数次幂,如果不是的话,向下取得最大的2的整数次幂.
比如你将 inSampleSize 赋值为3,系统实际使用的缩放比率为2,那就是每隔2行采1行,每隔2列采一列,那你解析出的图片就是原图大小的1/4.
这个值也可以填写非2的倍数,非2的倍数会被四舍五入.
综上,用这个参数解析bitmap就是为了减少内存占用
Q: inSampleSize取值多少最佳?
A: inSampleSize优化 这里提供了具体的计算算法
总体来讲:就是以图片宽高较大的一边为参考边进行压缩,目的还是为了避免压缩过大导致图片失真严重,效果不好。。
注意:inSampleSize官方解释为必须是2的整数次幂,如果不是,也会向减小方向寻找最近的2的整数次幂的数。但是,经过测试,并不是这样,貌似有些是2的整数倍就行,比如6,24等。官方计算解释是这样的:
// Calculate the largest inSampleSize value that is a power of 2 and
// keeps both
// height and width larger than the requested height and width.
计算最大的inSampleSize值,它是2和的幂,并且仍保持高度和宽度大于要求的高度和宽度
但是inSampleSize的一些规则,可能会有这样的场景,700的图片压缩到600.。结果是不会压缩的,因为计算的inSampleSize还是1,其实解码图片时候,影响大小的不止inSampleSize,还有其他一些参数:inDensity、inTargetDensity和inScreenDensity(备案学习文章)
inScaled:将它设置为true,那么代表这张图片可以缩放
inDensity:图片的原来密度,默认一般为160.
inTargetDensity:图片的目标密度,图片操作之后的密度
inScreenDensity:这个参数默认一直是0,源码中对这个参数没有赋值的地方,只有一处使用的地方
因为项目用的图片一般都放在drawable文件夹中,所以,Options中的inDensity属性会根据drawable文件夹的分辨率来赋值,inTartgetDensity会根据屏幕的像素密度来赋值
对应关系如下:
设备dpi | 密度类型 |
---|---|
0dpi ~ 120dpi | ldpi |
120dpi ~ 160dpi | mdpi |
160dpi ~ 240dpi | hdpi |
240dpi ~ 320dpi | xhdpi |
320dpi ~ 480dpi | xxhdpi |
480dpi ~ 640dpi | xxxhdpi |
drawable类型 | 分辨率 |
---|---|
ldpi | 120 |
mdpi | 160 |
hdpi | 240 |
xhdpi | 320 |
xxhdpi | 480 |
xxxhdpi | 640 |
输出图片宽高的公式如下:
输出图片的宽高= 原图片的宽高 / inSampleSize * (inTargetDensity / inDensity)
注意:
1:上面计算公式仅针对于drawable文件夹的图片来说,而对于一个file或者stream那么inDensity和inTargetDensity是不考虑的!他们默认就是0
2:对于drawable中的图片,inDensity是有默认值的,上面对应关系能看出来
3:inTargetDensity是跟屏幕密度有关的,这是屏幕参数,是常量,所以,可以通过以下方式获得。
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
Log.i("MainActivity", "onCreate: " + metrics.densityDpi);
DisplayMetrics windowm = getApplicationContext().getResources().getDisplayMetrics();
Log.i("MainActivity", "onCreate: " + windowm.densityDpi);
继续看,下面有具体的例子演示如何使用:
质量压缩
质量压缩就是解码率压缩,常见格式的图片在设置到ui上之前需要经过解码过程
Q:如何从解码方面降低图片内存占用
A:使用RGB_565代替ARGB_8888可以降低图片内存占用
1:因为它可以降低一个像素占用的内存,RGB_565一个像素占2个字节,ARGB_8888一个像素占4个字节。通过设置options.inPreferredConfig = Bitmap.Config.RGB_565来处理
private void testPicOptimize(ImageView imageView, int size) {
String sdcard = Environment.getExternalStorageDirectory().getAbsolutePath();
String filePath = sdcard + "/xxx.jpg";
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(filePath, options);
int width = options.outWidth;
options.inSampleSize = width / 200;
options.inScaled = true;
int calsize=options.outHeight>options.outWidth?options.outWidth:options.outHeight;
options.inTargetDensity =(size*options.inDensity)/(calsize/options.inSampleSize);
options.inPreferredConfig = Bitmap.Config.RGB_565;
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeFile(filePath, options);
imageView.setImageBitmap(bitmap);
}
设置inJustDecodeBounds=true,解析图片,在不加载进内存的情况下获取图片宽高,然后进行设置尺寸压缩、解码格式,然后在inJustDecodeBounds=false,重新加载图片到内存中,再讲图片设置到ui上
- 内存重用
android 3.0以后,BitmapFactory.Options提供了一个参数options.inBitmap。如果你使用了这个属性,那么使用这个属性的decode过程中 会直接参考 inBitmap 所引用的那块内存,,大家都知道 很多时候ui卡顿是因为gc操作过多而造成的。使用这个属性 能避免大内存块的申请和释放。带来的好处就是gc 操作的数量减少。这样cpu会有更多的时间 做ui线程,界面会流畅很多,同时还能节省大量内存!
memoryreuse.png private void testInBitmap(ImageView imageView) {
String sdcard = Environment.getExternalStorageDirectory().getAbsolutePath();
String filePath1 = sdcard + "/xxx.jpg";
BitmapFactory.Options options = new BitmapFactory.Options();
//size必须为1 否则是使用inBitmap属性会报异常
options.inSampleSize = 1;
//这个属性一定要在用在src Bitmap decode的时候 不然你再使用哪个inBitmap属性去decode时候会在c++层面报异常
//BitmapFactory: Unable to reuse an immutable bitmap as an image decoder target.
//一定要设置为true 这样返回的bitmap 才是mutable 也就是可重用的,否则是不能重用的
options.inMutable = true;
Bitmap bitmap1 = BitmapFactory.decodeFile(filePath1, options);
//设置复用内存,加载bitmap1已经开辟过内存,所以后续设置了options.inBitmap的图片加载会首先尝试利用bitmap1所指向的内存
options.inBitmap = bitmap1;
String filePath2 = sdcard + "/xxx2.jpg";
//这时候bitmap2的内存是bitmap1的内存
Bitmap bitmap2 = BitmapFactory.decodeFile(filePath2, options);
imageView.setImageBitmap(bitmap2);
}
如上例子,实现了第二张图复用了第一张图的内存。。有个条件,复用内存的图片要小于被复用的内存的大小,不然复用不了
使用options.inBitmap需要注意几点:
1:在SDK 11 -> 18之间,重用的bitmap大小必须是一致的,例如给inBitmap赋值的图片大小为100-100,那么新申请的bitmap必须也为100-100才能够被重用
2:从SDK 19开始,新申请的bitmap大小必须小于或者等于已经赋值过的bitmap大小
3:新申请的bitmap与旧的bitmap必须有相同的解码格式,例如大家都是8888的,如果前面的bitmap是8888,那么就不能支持4444与565格式的bitmap了,不同的编码格式占用的内存是不同的,有时候也可以根据需求指定编码格式
上面的注意点很大程度上限制了我们使用内存重用的灵活性,难道app中的图片都要一样?解码格式也要一样?需求不同可能图片处理就不同,所以,怎们充分的利用options.inBitmap才是真正提升app内存性能的关键:
有一种思路:就是inBitmap池,也就是说管理一个包含多种典型可重用的bitmap集合。这样,就很大程度的提升了bitmap内存重用概率
这种方案,现流行框架glide就是这么处理的,当然,细节更深
Bitmap内存管理
Bitmap 对象在不使用时,我们应该先调用recycle()释放内存,然后才置空,因为加载bitmap对象的内存空间,一部分是java的,一部分是c的(因为Bitmap分配的底层是通过jni调用的,BitMap底层是skia图形库,skia图形库是c实现的,通过jni的方法在java层进行封装)。这个recycle()函数就是针对c部分的内存释放
Q:bitmap的存储在3.0前后有什么改变?api的调用有什么变化?
A:在android 3.0之前,像素数据支持保存在本地内存中的。而位图bitmap本身是存储在Dalvik堆中的,bitmap数据操作完之后,需要调用bitmap.recycle去释放这些像素数据。3.0之后,像素数据和位图都是存储在Dalvik堆中的,所以bitmap对象是会自动回收的
通过dumpsys meminfo命令可以查看一个进程的内存使用情况,
当然也可以通过它来观察我们创建或销毁一张BitMap图片内存的变化,从而推断出图片占用内存的大小
adb shell "dumpsys meminfo com.lenovo.robin"
图片加载优化
mipmap
- 在App中,无论你将图片放在drawable还是mipmap目录,系统只会加载对应density中的图片。
而在Launcher中,如果使用mipmap,那么Launcher会自动加载更加合适的密度的资源。 - 应用内使用到的图片资源,并不会因为你放在mipmap或者drawable目录而产生差异。单纯只是资源路径的差异R.drawable.xxx或者R.mipmap.xxx。(也可能在低版本系统中有差异)
- 一句话来说就是,自动跨设备密度展示的能力是launcher的,而不是mipmap的。
总的来说,app图标(launcher icon) 必须放在mipmap目录中,并且最好准备不同密度的图片,否则缩放后可能导致失真。
而应用内使用到的图片资源,放在drawable目录亦或是mipmap目录中是没有区别的,该准备多个密度的还是要准备多个密度,如果只想使用一份切图,那尽量将切图放在高密度的文件夹中
内存占用与drawable文件夹关系
如标题换个说法,同一张图片,放置在不同的drawable文件夹,在同一设备上运行,对图片大小及内存占用有什么影响,下面一步步了解
手机屏幕密度对应屏幕密度类型
- 获取手机屏幕密度:百度吧
工具..这个算出来的,为啥跟代码算出来的不一样?
- 设备屏幕密度
设备dpi | 密度类型 |
---|---|
0dpi ~ 120dpi | ldpi |
120dpi ~ 160dpi | mdpi |
160dpi ~ 240dpi | hdpi |
240dpi ~ 320dpi | xhdpi |
320dpi ~ 480dpi | xxhdpi |
480dpi ~ 640dpi | xxxhdpi |
从上面的屏幕密度匹配类型能看出,假如你的手机dpi是260,那么你的手机屏幕密度类型就是xhdpi,加载图片,系统首先会去drawable-xhdpi目录下查找,其他查找规则请继续往下看
photosizeanddppx.png图片大小以及dp和px关系一览表
假设,在mdpi屏幕密度的手机上,你将一张60px乘60px的图片放到mdpi中,它的大小是60乘60;若把它拿到hdpi中,那么它的大小应该是45 乘 45,图片缩小,因为系统认为这些图片都是给高分辨率设备使用的.(由上表可以算出60*3/4)
加载顺序
APP在查找图片资源的时候遵循先高后低的原则,假设设备的分辨率是xxhdpi,那么查找顺序如下
- 先去drawable-xxhdpi文件夹查找,如果有这张图片就使用,这个时候图片不会缩放
- 如果没有找到,则去更高密度的文件夹下找,例如drawable-xxxhdpi,密度依次递增,如果找到了,图片将会缩小,因为系统认为这些图片都是给高分辨率设备使用的
- 所有高密度文件夹都没有的话,就会去drawable-nodpi文件夹去找,如果找到,不缩放,使用原图
- 还是没有的话,就会去更低密度的文件夹下面找,xhdpi,hdpi等,密度依次递减,如果找到了,图片将会放大,因为系统认为这个图片是给低分辨率设备使用的
总的来说,系统的规则也是优先向减小app运行内存的方向查找处理资源的,因为找更高密度drawable下的图片,加载为bitmap是要缩小的
图片加载
图片从res中加载到内存都是以图片的原始宽高比进行加载的,比如上文中博主采用的图片是7201280,锤子T1的分辨率是 10801960,
把图片放在drawable-xhdpi文件夹下,图片的大小为10801920,而不是充满屏幕高度的1960。因为图片加载时首先满足的是宽度,比如把720
放大到1080,此时保持图片的宽高比不变,高度应该是等比例放大,h = 12801080/720。
图片加载优化
常用方案有4个方向:
- 异步请求:图片在线程或后台请求
- 图片缓存:列表中的图片进行缓存
- 网络请求:使用OKHttp
- 懒加载:当图片呈现到可视区域再进行加载
android 大图片加载方案
BitmapRegionDecoder。这个是api 10时候google提供打开大小超过屏幕的图片的方案。这里不多说了。查了一些资料,用法简单,文章几乎都一毛一样,大家自行学习了解
说一点,由于是打开的超大图片,所以,解决方案中就用到了前面所说的inJustDecodeBounds。通过设置inJustDecodeBounds = true 在图片不加载进内存的情况下能获取图片宽高
框架优化图片加载
常见框架:Universal image loader,picasso,glide,fresco等,待续
网友评论