Android高性能编码 - 第三篇 优化图片加载

图片加载是每个应用的基本功能,而图片对App整体性能的影响是不言而喻的,尤其是在程序加载大量图片和高分辨率图片时,最容易产生OOM异常,因此图片相关的编码都需要从性能的角度仔细考量。

3.1 自定义图片加载的性能要点

尽管我们普遍在应用中引入了第三方的图片加载库,但很多时候还会手动对局部的图片任务进行处理,包括临时加载避开第三方库的默认缓存、所选的第三方库不便于加载本地图片等,在此需要注意以下几个要点。

3.1.1 BitmapFactory.Options的关键配置

该可选项提供使用BitmapFactory加载图片资源时的一些重要参数,我们需要特别配置的主要包括Bitmap.Config、inJustDecodeBounds、inSampleSize,典型示例如下:

1 使用Bitmap.Config.RGB_565颜色样式

大多数情况下,我们可以忽略Alpha通道,替代默认的Bitmap.Config.ARGB_8888,以节省内存资源。

2 使用inSampleSize采样

该选项配合inJustDecodeBounds选项,计算出尽可能精确的inSampleSize,使得图片按控件需要的大小,对图片进行压缩。

3 decodeStream与decodeResource

关于图片加载的这两个方法,目前业内统一的认识是,decodeStream无论是时间上还是空间上,都比decodeResource方法更优秀,网上有一个专门对比,参见文章《BitmapFactory.decodeResourceVS BitmapFactory.decodeStream》。

时间上的优势是,前者使用了系统Native代码加快了加载效率;空间上的原因主要是,后者在加载中多了BitmapFactory.finishDecode这一步,这里有Bitmap.createScaledBitmap操作,多了一个原Bitmap的占用。

因此,良好的性能要求尽可能的使用前者。

3.1.2 自定义LRUCache

自定义加载时,还可能需要做内存缓存,对于缓存,没有大小或者规则适用于所有应用,需要根据自己App的实际制定策略,缓存太小可能只会增加额外的内存使用,缓存太大可能会导致内存溢出或者应用其它模块可使用内存太小。

考虑到自定义加载方式只是第三方加载库的补充使用,在此建议取较小的值;而缓存策略,使用主流的LRUCache。

2.1.3 懒加载与ViewStub

对于一些非实时显示的图片,要求采用懒加载策略,在实际交互需要时,再行加载;加载时,如果不想覆盖原有的图片,考虑配合使用ViewStub控件进行显示,此控件在未显示时不占用空间,非常适合懒加载的场景。

备注:ViewStub适用于所有需要懒加载的UI要素,具体使用参看第四篇UI对应章节。

3.1.4 异步加载

图片的异步加载相对其他耗时操作,往往已经将ImageView控件传递进来,需要在接口内处理好线程切换任务。

简单的处理方法,包括SDK的AsyncTask任务类,以及自定义封装的异步回调任务类。

3.2 图片加载库的二次封装优化

尽管目前有不少优秀的图片加载库供使用,但是良好的规范要求我们对第三方库进行二次封装,同时对一些重要的全局参数进行设定。高性能的封装要求至少需要考虑下述几个方面。

备注:目前主流的几个图片加载库各有优劣之处,不属于本文档的讨论内容,在此不作细表,相关资料参考本篇3.5节的扩展。

3.2.1 统一同步和异步加载接口

显性的提供同步和异步两种加载方式,应用层根据需求,选择对应的策略。

将同步方式明确出来,也进一步提示调用者需要维护上层的工作线程。

3.2.2 提供适当的图片缓存大小和策略

缓存的大小根据具体设备的配置条件而定,一般内存缓存设置为App可用内存的1/4,并启用磁盘缓存,避免调用用时疏忽。

以Glide为例,内部MemorySizeCalculator相关类已经动态地根据手机分辨率,是否高内存设备等要素生成了默认的内存缓存大小,是一个较优的策略,我们要做的是在下载的封装接口中,统一指定缓存要素而避免每次调用的疏忽,或者统一修改缓存要素。

3.2.3 统一配置较优的性能要素

主要包括:图片模式、剪裁方式、默认图片等

图片模式:一般在转换图片格式时,如果没有特别需要,采用Config为Bitmap.Config.RGB_565,以Glide为例默认采用此配置,其它图片库应当检查此参数。

剪裁方式:如果获取的图片面临不同的显示控件的适配,需要对图片做进一步剪裁。考虑使用InSampleSize以及针对控件宽高size进行综合剪裁,使得最终需要的图片最小适配化。

默认图片:当网络较慢,需要显示PlaceHolder默认图片时,大量的默认图片的加载控制,对内存等也有显著的影响,因此需要统一配置比较合理的默认图片。默认图片的规范参考下述3.3小节。

以Glide为例,提供了对应的into()接口,可以按设定的宽高size进行大小剪裁,其他图片库请参照或者自定义。

3.2.4 生命周期、加载周期集成

3.2.4.1 组件生命周期集成

大规模的图片请求无疑会带来很多后台耗时任务,而前台界面销毁/不可见时,这些任务很可能已经不必要,同时还会抢占CPU资源,影响新界面的线程任务调度。

因此,应该尽可能对图片任务的加载周期和前台组件的生命周期进行同步控制。

以Glide为例,如果调用时传入了当前Activity的上下文,则请求会在onStop的时候自动暂停,然后在onStart的时候重新启动,gif的动画也会在onStop的时候停止,以免在后台消耗性能,此外,当设备的网络状态发生改变的时候,所有失败的请求会尝试自动重启,保证数据的正确性,做得比较完善。

因此我们要做的就是,在调用Glide接口尽可能传入当前组件的上下文。

3.2.4.2 列表加载状态集成

在可见生命周期内,还有另一个比较显著的加载问题是,如果用户快速滑动列表,列表较大,发生fling的时候,如果还按FIFO的策略去加载大量图片的话,就可能严重影响最终停止时的图片显示的时间,增加用户的等待时间成本。

因此,应该尽可能对列表的滚屏Scroll状态进行监听,当列表Fling的时候暂停加载,在慢速或停止的时候恢复加载。

以Glide为例,提供了暂停和恢复两个接口,调用者应该适时的在滚动监听回调中进行调用即可。

3.2.5 可切换的封装策略

尽管许多优秀的图片库使用起来已经比较简单,但是进行二次封装的意义还是不言而喻的,而其中最重要的一点就是未来可能发生的第三方库的切换。在本节我们主要讨论如何更好的封装图片加载库,以保证最低的切换代价。

3.2.5.1 单例模式维护唯一的入口

入口唯一才能使上层调用不受底层切换的影响,要求所有图片加载都统一调用类似ImageLoader.java这样的工具类的统一接口,而对于多参数构建的接口,我们一般推荐使用build模式。

3.2.5.2 策略模式将第三方库的依赖解耦

实现了上层的统一调用后,接下来就是第三方库接口的解耦工作。对于可切换策略的设计,我们一般推荐使用策略模式,设计统一的策略接口进行加载。

策略接口:

LoaderConfig中配置策略对象:

ImageLoader中调用策略对象:

3.3 默认图片的规范:

默认图片是图片加载的重要要素,却经常被低估了其价值。如前所述,大量的默认图片的加载控制,对内存等也有显著的影响,因此需要统一配置比较合理的默认图片。从性能的角度来说,默认图片的设置至少需要从以下方面规范:

1.        为所有非静态图片控件提供默认图片;

2.        为每个类型/尺寸大小的图片控件至少提供两套不同分辨率的适配的图片,根据行业实践,至少考虑720和1080分辨率;

3.        默认图片简约明了,甚至可以考虑部分透明,控制图片内存占用;

4.        一般考虑JPG格式,去除不必要的alpha通道;

5.        尽可能考虑.9patch图片或者ShapeDrawable图片。

3.4 一次性大图避免内存缓存

一次性大图的典型例子包括:app闪屏广告,引导页的几张大图。这些图片占用内存较明显,且是一次性的显示,因此应该避免占用内存缓存。但在日常开发场景中,我们可能会因为采用统一接口而忽视这一点,而第三方库更是默认进行缓存。

我们在此要做的工作可从下方面考虑:

1 加载图片时避免缓存

以Glide为例,在调用其加载一次性大图时,可使用skipMemoryCache(true)去明确告诉Glide跳过内存缓存。

2 在切换界面时,及时的清除图片对象

在界面onDestroy时,将该大图关联的Bitmap或Drawable主动释放。

3 扩展:改变磁盘缓存行为

如果图片每次都是动态链接,还可以考虑跳过磁盘缓存。以Glide为例,用diskCacheStrategy()方法为Glide改变磁盘缓存的行为,传入的参数和意义包括:

DiskCacheStrategy.NONE什么都不缓存,就像刚讨论的那样

DiskCacheStrategy.SOURCE仅仅只缓存原来的全分辨率的图像

DiskCacheStrategy.RESULT仅仅缓存最终的图像,即,降低分辨率后的(或者是转换后的)

DiskCacheStrategy.ALL缓存所有版本的图像(默认行为)

3.5 适时使用Xml编写ShapeDrawable

使用Xml文件可以定义一个Drawable资源,根据其语法配置xml节点参数,可以生成简约的ShapeDrawable对象,其内存占用较Bitmap等有明显的优势。

根据实践,ShapeDrawable可以满足一些界面级Layout、大的ViewGroup的背景图片需求,在这种场景下,就可以适时的使用。

3.5.1 Shape标签语法

<!--语法-->

<?xml version="1.0"encoding="utf-8"?>

<shapexmlns:android="http://schemas.android.com/apk/res/android"

   android:shape=["rectangle" | "oval" |"line" | "ring"] >

   <corners

   android:radius="integer"

   android:topLeftRadius="integer"

   android:topRightRadius="integer"

   android:bottomLeftRadius="integer"

   android:bottomRightRadius="integer" />

   <gradient

   android:angle="integer"

   android:centerX="integer"

   android:centerY="integer"

   android:centerColor="integer"

   android:endColor="color"

   android:gradientRadius="integer"

   android:startColor="color"

   android:type=["linear" | "radial" |"sweep"]android:useLevel=["true" | "false"] />

   <padding

   android:left="integer"

   android:top="integer"

   android:right="integer"

   android:bottom="integer" />

   <size

   android:width="integer"

   android:height="integer" />

   <solid android:color="color" />

   <stroke

   android:width="integer"

   android:color="color"

   android:dashWidth="integer"

   android:dashGap="integer" />

</shape>

3.5.2 节点描述

xmlns:android

String类型。必须的,定义xml文件的命名空间,必须是"http://schemas.android.com/apk/res/android"

   

   <shape>

    关键字,定义shape的值,必须是下面的之一:

   "rectangle"      矩阵,这也是默认的shape

   "oval"       椭圆

   "line"        一条水平的直线。这种shape必须使用 <stroke>元素来定义这条线的宽度

   "ring"       圆环

       下面的属性只有当 android:shape="ring"才使用:

       android:innerRadius

       内环的半径。一个尺寸值(dip等等)或者一个尺寸资源。

       android:innerRadiusRatio

       Float类型。这个值表示内部环的比例

       android:thickness

       环的厚度,是一个尺寸值或尺寸的资源。

        android:thicknessRatio

       Float类型。厚度的比例。

       android:useLevel

       Boolean类型。如果用在 LevelListDrawable里,那么就是true。如果通常不出现则为false。

       <corners>

           为Shape创建一个圆角,只有shape是rectangle时候才使用。

           属性:

           android:radius

           Dimension, 圆角的半径。会被下面每个特定的圆角属性重写。

           android:topLeftRadius

           Dimension, top-left 圆角的半径。

           android:topRightRadius

           Dimension, top-right 圆角的半径。

           android:bottomLeftRadius

           Dimension, bottom-left圆角的半径。

           android:bottomRightRadius

           Dimension, bottom-right圆角的半径。

           注意:每个圆角半径值都必须大于1,否侧就没有圆角。

           

           <gradient>

                指定这个shape的渐变颜色。

                属性:

                android:angle

                Integer, 渐变的角度, 0 代表从 left 到 right,90 代表bottom到 top

                android:centerX

                Float, 渐变中心的相对X坐标,在0到1.0之间

                android:centerY

                Float, 渐变中心的相对Y坐标,在0到1.0之间

                android:centerColor

               Color, 可选的颜色值。基于startColor和endColor之间

                android:endColor

                Color,  结束的颜色

                android:gradientRadius

                Float , 渐变的半径,只有在android:type="radial"才使用

                android:startColor

                Color, 开始的颜色值。

                android:type

                Keyword, 渐变的模式,下面值之一:

                "linear"    线形渐变。这也是默认的模式

                "radial"    辐射渐变。startColor即辐射中心的颜色

                "sweep"  扫描线渐变。

                android:useLevel

               Boolean, 如果在LevelListDrawable中使用,则为true

 

                <padding>

                    内容与视图边界的距离

                    属性:

                    android:left

                    Dimension, 左边填充距离

                    android:top

                    Dimension, 顶部填充距离

                    android:right

                    Dimension, 右边填充距离

                    android:bottom

                    Dimension, 底部填充距离

                    <size>

                        这个shape的大小。

                        属性:

                        android:height

                        Dimension, 这个shape的高度

                        android:width

                        Dimension, 这个shape的宽度

                        <solid>

                            填充这个shape的纯色

                            属性:

                            android:color

                            Color, 颜色值,十六进制数,或者一个Color资源

                            <stroke>

                                这个shape使用的笔画,当android:shape="line"的时候,必须设置

                               属性:

                                android:width, 笔画的粗细

                                android:color, 笔画的颜色

                               android:dashGap, 每画一条线就间隔多少

                               android:dashWidth, 每画一条线的长度

备注:默认情况下,这个shape会缩放到与他所在容器大小成正比。当你在一个ImageView中使用这个shape,你可以使用 android:scaleType="center"来限制这种缩放。

3.6 图片资源目录管理

日常开发中经常被忽视的一个问题是,图片资源被放置在mipmap或者不正确的drawable目录下,其中mipmap目录不响应系统的缩放机制,而不当的drawable目录,则可能会因为缩放机制使图片加载时占用过大的内存。

无论是在xml还是在代码中设置图片资源,图片加载到内存中,图片文件本身的大小只是内存的一小部分,最强相关的是图片的尺寸大小!而加载后的内存大小,由于Android自身的加载和缩放机制,又跟手机dpi和drawable目录强相关。所以,需要特别注意图片尺寸大小和文件目录。

对于目录问题,在只有一套图的情况下,需要评估当前终端的主流dpi,放置到对应目录。以手机App为例,在资源不充足的情况下,考虑主流手机的配置,优先考虑将资源放置到drawable的xxhdpi目录,即重点面向1080P。

备注:

1 分辨率对应DPI

"HVGA - mdpi"

"WVGA - hdpi "

"FWVGA - hdpi "

"QHD - hdpi "

"720P - xhdpi"

"1080P - xxhdpi "

2 扩展阅读

图片加载时根据dpi和drawable目录进行缩放的关系,参考文章《关于Android中图片大小、内存占用与drawable文件夹关系

3.7 扩展

3.7.1 主流图片加载库Glide vs Others

         下表将从共同点、优点、缺点、发布时间、start欢迎程度等方面总结,目前来看,Glide还是一般项目主推的库。

猜你喜欢

转载自blog.csdn.net/qq_16206535/article/details/79772054