Android中的图片优化完全指南

前言

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

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

概念

图片占用的内存:图片高度 * 图片宽度 * 一个像素占用的内存大小这个公式代表一个图片最终占用的内存大小,项目中的优化图片占用内存都是通过这个三个参数来优化的。

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

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

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

view使用了固定尺寸:这个没有问题,我们直接拿getLayoutParams()的width和height就可以了

view的maxWidth/maxHeight:view无法固定尺寸,我们可以在xml里面给view配置maxWidth/maxHeight来指导图片库加载什么尺寸的图片

加载图片前先measure:不怎么推荐,因为图片加载出来后view还得measure一次

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

第二条规则:按需请求

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

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

再补充一点,有些特殊的使用场景可以考虑采用上面说的第一种方式来做,举个例子比如一个操作一定会加载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
  • 使用压缩工具

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • 总结Android App内存优化之图片优化

    前言 在Android设备内存动不动就上G的情况下,的确没有必要去太在意APP对Android系统内存的消耗,但在实际工作中我做的是教育类的小学APP,APP中的按钮.背景.动画变换基本上全是图片,在2K屏上(分辨率2048*1536)一张背景图片就会占用内存12M,来回切换几次内存占用就会增涨到上百兆,为了在不影响APP的视觉效果的前提下,有必要通过各种手段来降低APP对内存的消耗. 通过DDMS的APP内存占用查看工具分析发现,APP中占用内存最多的是图片,每个Activity中图片占用内存

  • Android图片压缩以及优化实例

    前言 图片压缩在Android技术中已经属于烂大街,上周看了2个开源库然后对自己项目的压缩做了对比,发现一些新东西,记录与此. 为何要压缩 1.体积的原因 如果你的图片是要准备上传的,那动辄几M的大小肯定不行的,况且图片分辨率大于设备分辨率的话毫无意义. 2.内存原因 如果图片要显示下Android设备上,ImageView最终是要加载Bitmap对象的,就要考虑单个Bitmap对象的内存占用了,如何计算一张图片的加载到内存的占用呢?其实就是所有像素的内存占用总和: bitmap内存大小 = 图

  • Android 中对于图片的内存优化方法

    1. 对图片本身进行操作 尽量不要使用 setImageBitmap.setImageResource. BitmapFactory.decodeResource 来设置一张大图,因为这些方法在完成 decode 后,最终都是通过 Java 层的 createBitmap 来完成的,需要消耗更多内存.因此,改用先通过 BitmapFactory.decodeStream 方法,创建出一个 bitmap,再将其设为 ImageView 的 source,decodeStream 最大的秘密在于其直

  • Android优化查询加载大数量的本地相册图片

    一.概述 讲解优化查询相册图片之前,我们先来看下PM提出的需求,PM的需求很简单,就是要做一个类似微信的本地相册图片查询控件,主要包含两个两部分: 进入图片选择页面就要显示出手机中所有的照片,包括系统相册图片和其他目录下的所有图片,并按照时间倒叙排列 切换相册功能,切换相册页面列出手机中所有的图片目录列表,并且显示出每个目录下所有的图片个数以及封面图片 这两个需求看似简单,实则隐藏着一系列的性能优化问题.在做优化之前,我们调研了一些其他比较出名的app在加载大数量图片的性能表现(gif录制的不够

  • Android性能优化之Bitmap图片优化详解

    前言 在Android开发过程中,Bitmap往往会给开发者带来一些困扰,因为对Bitmap操作不慎,就容易造成OOM(Java.lang.OutofMemoryError - 内存溢出),本篇博客,我们将一起探讨Bitmap的性能优化. 为什么Bitmap会导致OOM? 1.每个机型在编译ROM时都设置了一个应用堆内存VM值上限dalvik.vm.heapgrowthlimit,用来限定每个应用可用的最大内存,超出这个最大值将会报OOM.这个阀值,一般根据手机屏幕dpi大小递增,dpi越小的手

  • android内存优化之图片优化

    对图片本身进行操作.尽量不要使用setImageBitmap.setImageResource.BitmapFactory.decodeResource来设置一张大图,因为这些方法在完成decode后,最终都是通过java层的createBitmap来完成的,需要消耗更多内存.因此,改用先通过BitmapFactory.decodeStream方法,创建出一个bitmap,再将其设为ImageView的source,decodeStream最大的秘密在于其直接调用JNI>>nativeDeco

  • Android图片性能优化详解

    1. 图片的格式 目前移动端Android平台原生支持的图片格式主要有:JPEG.PNG.GIF.BMP.和WebP(自从Android 4.0开始支持),但是在Android应用开发中能够使用的编解码格式只有三种:JPEG.PNG.WebP,图片格式可以通过查看Bitmap类的CompressFormat枚举值来确定. public static enum CompressFormat { JPEG. PNG. WebP; private CompressFormat() { } } 如果要在

  • Android中RecyclerView 滑动时图片加载的优化

    RecyclerView 滑动时的优化处理,在滑动时停止加载图片,在滑动停止时开始加载图片,这里用了Glide.pause 和Glide.resume.这里为了避免重复设置增加开销,设置了一个标志变量来做判断. mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(RecyclerView recyclerView, in

  • Android中的图片优化完全指南

    前言 图片作为内存消耗大户,一直是开发人员尝试优化的重点对象.Bitmap的内存从3.0以前的位于native,到后来改成jvm,再到8.0又改回到native.fresco花费很多精力在5.0系统之前把Bitmap内存改回到native,高版本上面则遵循系统实现,却又被官方打脸. jvm每个进程都有内存上限,而native则没有限制(不是没有影响,至少不会oom),所以把内存大户Bitmap挪到native可能是很多人的梦想,但native的管理和实现明显比jvm更为复杂,除非有现成实现,很少

  • Android中WebView图片实现自适应的方法

    本文实例讲述了Android中WebView图片实现自适应的方法.分享给大家供大家参考.具体实现方法如下: 复制代码 代码如下: WebSettings ws = tv.getSettings(); 加上这个属性后,html的图片就会以单列显示就不会变形占了别的位置 ws.setLayoutAlgorithm(LayoutAlgorithm.SINGLE_COLUMN); //让缩放显示的最小值为起始 webView.setInitialScale(5); // 设置支持缩放 webSettin

  • Android中imageView图片放大缩小及旋转功能示例代码

    一.简介 二.方法 1)设置图片放大缩小效果 第一步:将<ImageView>标签中的android:scaleType设置为"fitCenter" android:scaleType="fitCenter" 第二步:获取屏幕的宽度 DisplayMetrics dm=new DisplayMetrics(); getWindowManager().getDefaultDisplay().getMetrics(dm); dm.widthPixels 第三

  • Android中复制图片的实例代码

    activity_main.xml中的配置 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent&quo

  • Android中利用ViewHolder优化自定义Adapter的写法(必看)

    最近写Adapter写得多了,慢慢就熟悉了. 用ViewHolder,主要是进行一些性能优化,减少一些不必要的重复操作.(WXD同学教我的.) 具体不分析了,直接上一份代码吧: public class MarkerItemAdapter extends BaseAdapter { private Context mContext = null; private List<MarkerItem> mMarkerData = null; public MarkerItemAdapter(Cont

  • Django中prefetch_related()函数优化实战指南

    目录 前言 使用方法 *lookups 参数 Prefetch对象 最佳实践 选择哪个函数 小结 总结 前言 对于多对多字段(ManyToManyField)和一对多字段, 可以使用prefetch_related()来进行优化 prefetch_related()和select_related()的设计目的很相似,都是为了减少SQL查询的数量,但是实现的方式不一样.后者是通过JOIN语句,在SQL查询内解决问题.但是对于多对多关系,使用SQL语句解决就显得有些不太明智,因为JOIN得到的表将会

  • Android中SparseArray性能优化的使用方法

    之前一篇文章研究完横向二级菜单,发现其中使用了SparseArray去替换HashMap的使用.于是乎自己查了一些相关资料,自己同时对性能进行了一些测试.首先先说一下SparseArray的原理. SparseArray(稀疏数组).他是Android内部特有的api,标准的jdk是没有这个类的.在Android内部用来替代HashMap<Integer,E>这种形式,使用SparseArray更加节省内存空间的使用,SparseArray也是以key和value对数据进行保存的.使用的时候只

  • Android中超大图片无法显示的问题解决

    发现问题 最近在做图片浏览功能时遇到了一个很蛋疼的问题,在开启硬件加速情况下,超大图无法正常显示(图的长宽有一个大于9000),而且程序不会crash,只是图片加载不出来,View显示为黑色.通过查看日志,发现系统打印出了下面的内容: W OpenGLRenderer( 4014): Bitmap too large to be uploaded into a texture (600x9518, max=8192x8192) 从日志内容可以看出,这是由OpenGL打印出来的日志,是由于图片的尺

  • 使用Java代码在Android中实现图片裁剪功能

    前言 Android应用中经常会遇到上传相册图片的需求,这里记录一下如何进行相册图片的选取和裁剪. 相册选取图片 1. 激活相册或是文件管理器,来获取相片,代码如下: private static final int TAKE_PICTURE_FROM_ALBUM = 1; private void takePictureFromAlbum() { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.setType("ima

随机推荐