Android图片优化指南

图片做为内存消耗大户,一直是开发人员尝试优化的重点对象。Bitmap的内存从3.0之前的位于native,到后来改为jvm,再到8.0又改回到native。fresco花费不少精力在5.0系统以前把Bitmap内存改回到native,高版本上面则遵循系统实现,却又被官方打脸。程序员

jvm每一个进程都有内存上限,而native则没有限制(不是没有影响,至少不会oom),因此把内存大户Bitmap挪到native多是不少人的梦想,但native的管理和实现明显比jvm更为复杂,除非有现成实现,不多有人去动这一块。行业里面的大部分图片库都没有涉及这块,大部分的程序员也秉着够用就好的态度用了不少年,这说明程序员也是会偷懒的。官方的策略修改到底缘由几何,其实我也没搜到相关说明,有知道的同窗欢迎留言。web

第一条规则:把Bitmap保存到native

一个app里面的图片都会有尺寸,通常状况下面图片的尺寸就是view的大小,而view的大小在咱们使用dp单位后在不一样的机器上面表现出来的实际像素都有差异,为了节约流量开销,加快返回速度,同时符合按需加载的原则,咱们应该只加载实际view尺寸大小的图片。通常图片存储提供商都会提供在线压缩服务,咱们只须要在请求连接里面加上参数便可。这里还有个问题咱们通常请求加载图片的代码都是写在Activity的onCreate,或者Adapter的getView函数里面,这个时候实际上是获取不到view尺寸的(还未measure),这里有几种作法:面试

使用目测:好比一个列表是左右图片布局的,那就能够请求屏幕一半宽度的尺寸图片canvas

view使用了固定尺寸:这个没有问题,咱们直接拿getLayoutParams()的width和height就能够了缓存

view的maxWidth/maxHeight:view没法固定尺寸,咱们能够在xml里面给view配置maxWidth/maxHeight来指导图片库加载什么尺寸的图片微信

加载图片前先measure:不怎么推荐,由于图片加载出来后view还得measure一次网络

通常作法是给图片加载库包装一层,根据传进来的url判断是否已经指定大小(开发者固然能够决定想加载多大图片),若是还未指定则使用上面的策略进行动态调整,若是最后仍是没能加上缩放参数,则有个兜底策略,不加载超过屏幕尺寸大小。app

第二条规则:按需请求

作了上面按需加载后还有个问题,会发现有时候不一样的页面须要加载同一个图片url,但在尺寸上面有细微差异,结果致使请求重复(通常图片加载库都是url做为缓存key),有点弄巧成拙,反倒浪费了流量和时间。这种状况咱们须要作些微调。对于A页面图片尺寸是200x200,对于B页面图片尺寸是180x180,咱们认为可使用200x200的图片缩放到180x180,这有两种作法:第一种是让开发者始终都去加载稍微大一点的图,这个要求有点高,一个页面开发的时候很难先后联系。第二种是修改图片加载库,自动完成这个事情。后者天然合理,修改图片加载库在决定使用缓存的那一步判断是否有比本身大的缓存已经存在便可,固然这个策略能够每一个产品本身调整,好比也能够认为已经存在的缓存尺寸小于必定值也是能够接受也是能够的。还有复杂的状况好比缓存图片高宽比和要增强的不同如何处理等等,策略均可以本身定,但必定有必要作这个事情。jvm

这里还要补充一点,大型产品通常图片域名会有好几个,用来作链路择优用的,必定要记得缓存的时候用来作key的url要去掉域名影响。ide

再补充一点,有些特殊的使用场景能够考虑采用上面说的第一种方式来作,举个例子好比一个操做必定会加载100x100的图,而后也必定会等会加载500x500的同一张图,这种场景下面按第二种方式来处理显然会加载两次,但若是开发者这2个位置写死都加载500x500则明显更好一些。因此方法是死的,人是活的,要看实际使用场景。

还有一些特殊场景,好比程序里面有两个进程,A进程会加载500x500的图,B进程会加载无论什么尺寸的同一张图,默认状况下面这2个请求会同时发出,这就极可能会形成重复请求,这种状况下面须要作一点跨进程同步,或者简单一点其中一个进程请求作一点延迟处理。

第三条规则:合并类似请求

实在不得已要从服务端加载大图或者原始尺寸下来,或者由于上面说的策略故意加载大图下来,在decode的时候要进行采样,这个是老生常谈了,使用options.inJustDecodeBounds来获取原始尺寸,而后按需使用options.inSampleSize来采样图片到接近view尺寸。

第四条规则:按需加载

Bitmap在decode的时候可使用inPreferredConfig指定配置格式,常见的有:
参数取值含义ALPHA_8图片中每一个像素用一个字节(8位)存储,该字节存储的是图片8位的透明度值RGB_565图片中每一个像素用两个字节(16位)存储,两个字节中高5位表示红色通道,中间6位表示绿色通道,低5位表示蓝色通道ARGB_4444图片中每一个像素用两个字节(16位)存储,Alpha,R,G,B四个通道每一个通道用4位表示ARGB_8888图片中每一个像素用四个字节(32位)存储,Alpha,R,G,B四个通道每一个通道用8位表示

对于质量细节要求比较高的图片可使用ARGB_888,这也是fresco的默认配置。而对于JPG图片可使RGB_565,从上面能够看出内存占用之间减小一半,很是有吸引力,而app里面事实上大部分应该都是JPG。但每每在和视觉的PK当中开发每每败下阵来,下降了图片质量不行!!开发老是持之以恒,咱们能够建议采用这样的策略:对于尺寸小于必定尺寸的JPG(好比300),咱们使用565,而对于大图为了保留细节咱们仍然使用8888。仍是那句话策略是活的。

第五条规则:进一步按需加载

使用三级缓存机制,内存磁盘网络,这也是官方推荐的方式。内存缓存旨在加快访问速度,磁盘缓存避免反复请求。关于这一点就不在赘述了,基本开源图片库都会这么作

第六条规则:使用三级缓存机制

不少场景下面咱们须要显示图片的一部分,或者进行图片效果叠加,好比作个倒影之类的。不少同窗上来就准备createBitmap,而后把叠加效果绘制到这个临时Bitmap,或者从原始Bitmap里面先剪一部分出来生成一个新的Bitmap,再设给ImageView。或者使用createScaledBitmap进行缩放。更不当心的同窗可能直接把这些操做代码写在UI线程,而后写在子线程又比较麻烦,这边推荐的是使用自定义绘制,canvas有个drawBitmap方法能够把某个区域绘制到指定位置。叠加效果也能够彻底使用自定义view来本身draw,这样不会有临时Bitmap生成,效率会更高。

若是自定义view有困难,咱们可使用Drawable,只要能拿到canvas,这两种作法是同样的。
这里列举一些实例,好让你们能够进一步理解:

一个按钮有普通和按下状态,按下是普通状态上面叠加一个遮罩,不须要切两张图,按下状态的Drawable可使用自定义Drawable的canvas先绘制普通状态的图,再在上面绘制一层颜色。或者按下状态使用LayerDrawable,这个Drawable自动帮你作了这个事情

须要把Bitmap的[0,0,200,200]的区域显示到ImageView上面,使用canvas.drawBitmap(bitmap, [0,0,200,200], [0,0,图片宽,图片高],paint)

绘制倒影,这个逻辑性比较强了,这里就不具体展开,canvas的操做学习下,结合局部绘制其实很简单

有个图片,须要在左上角显示一个角标,正常状况下面须要在左上角摆一个view,若是使用Drawable自定义绘制,canvas画一下就好,相似下面的示例代码。

给你们一个自定义绘制的例子,随心组合:

class WithLineDrawable extends DrawableWrapper {
 private MyConstantState mMyConstantState;
 private boolean mForTop;
 private Paint mLinePaint = new Paint();

 public WithLineDrawable(Drawable drawable, boolean forTop) {
 super(drawable);
 mLinePaint.setColor(getLineColor());
 mForTop = forTop;
   }

 @Override
 public void draw(Canvas canvas) {
 super.draw(canvas);
 if (mForTop) {
         canvas.drawLine(0, 0, getBounds().width(), 0, mLinePaint);
      } else {
         canvas.drawLine(0, getBounds().height(), getBounds().width(), getBounds().height(), mLinePaint);
      }
   }

 @Nullable
   @Override
 public ConstantState getConstantState() {
 if (mMyConstantState == null) {
 mMyConstantState = new MyConstantState();
      }
 return mMyConstantState;
   }

 class MyConstantState extends ConstantState {
 @NonNull
      @Override
 public Drawable newDrawable() {
 return new WithLineDrawable(getWrappedDrawable().getConstantState().newDrawable(), mForTop);
      }

 @Override
 public int getChangingConfigurations() {
 return 0;
      }
   }
}

必定要把观念从Bitmap转变到Drawable,当还在费劲心思Bitmap该如何处理的时候,想一想Drawable里面如何使用canvas进行各类自定义绘制。

第七条规则:多使用自定义View或者Drawable自定义绘制

图片格式发展到今天已经很是多样了,目前不少开源库都支持了webp来代替jpg和gif,webp在压缩率上面有不少优点,虽然解码上面略逊一筹,通过咱们测试仍是很不错的。也是推荐你们使用,不管是网络图片下载仍是apk内置,用来代替jpg很合适,而代替png则还须要一些时间,主要是低版本系统对于透明webp还有些兼容问题。Android P上面支持了heif格式也是想代替jpg,不过这个格式目前还没仔细研究过。

对于内置apk的图标类,则推荐使用svg,再也不须要切几套图,并且很是小,官方使用的compat包里面解码svg会作缓存,也进一步提高性能。不过也正由于此尽可能不要一个图片使用过多不一样尺寸。大部分的图标都使用代码代替图片后,apk大小能够明显减小,这也符合咱们的原则:能程序画的就毫不切图。

第八条规则:使用更好的图片格式

不少时候咱们须要给图标换色,关于颜色混合有一套理论,官方很早就支持,使用ColorFilter,后来compat包里面出了个tint,因此若是有颜色混合处理的相关逻辑,千万不要去生成临时Bitmap,使用相似以下代码:

//1:经过图片资源文件生成Drawable实例
Drawable drawable = getResources().getDrawable(R.mipmap.ic_launcher).mutate();
//2:先调用DrawableCompat的wrap方法
drawable = DrawableCompat.wrap(drawable);

//3:再调用DrawableCompat的setTint方法,为Drawable实例进行着色

DrawableCompat.setTint(drawable, Color.RED);

第九条规则:使用着色API

内置apk的图片资源很是多,总有一些常规图片仍然须要使用jpg或者png,咱们要想办法进一步压缩他们,这样能够有效控制apk大小,这里推荐使用ImageOptim,这个工具集合了不少种压缩方式,效果显著。

第十条规则:使用压缩工具

后记:
不少面试的时候问如何作图片加载优化,他们会回答recycle

bitmap,事实上这个操做要很谨慎,一不留神就会致使出问题。大部分的应用不太会干这个事情,吃力不讨好,交给jvm垃圾回收多好。图片解码还有一些参数能够优化,好比inBitmap,这里就不具体展开了。

总结下

  • 把Bitmap保存到native
  • 按需请求
  • 按需加载
  • 合并类似请求
  • 使用三级缓存机制
  • 多使用自定义View或者Drawable自定义绘制
  • 使用更好的图片格式
  • 使用着色API
  • 使用压缩工具

更多文章请关注微信公众号:安卓之美

相关文章
相关标签/搜索