欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

Android应用性能提升:图片处理优化技巧

最编程 2024-07-27 18:58:08
...

很久之前写的了,发了吧,原用来总结学习的,再不发估计转行了,文章也参考了一些资料,抠用了一些图,主要是为了说明问题,总结学习

前言

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目录下查找,其他查找规则请继续往下看

图片大小以及dp和px关系一览表

photosizeanddppx.png

假设,在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 = 1280
1080/720。

图片加载优化

常用方案有4个方向:

  • 异步请求:图片在线程或后台请求
  • 图片缓存:列表中的图片进行缓存
  • 网络请求:使用OKHttp
  • 懒加载:当图片呈现到可视区域再进行加载

android 大图片加载方案

BitmapRegionDecoder。这个是api 10时候google提供打开大小超过屏幕的图片的方案。这里不多说了。查了一些资料,用法简单,文章几乎都一毛一样,大家自行学习了解

说一点,由于是打开的超大图片,所以,解决方案中就用到了前面所说的inJustDecodeBounds。通过设置inJustDecodeBounds = true 在图片不加载进内存的情况下能获取图片宽高

框架优化图片加载

常见框架:Universal image loader,picasso,glide,fresco等,待续

推荐阅读