Tips:本篇是《深入探索Android内存优化》的基础篇,如果没有掌握Android内存优化的同学建议系统学习一遍。
众所周知,内存优化可以说是性能优化中最重要的优化点之一,可以说,如果你没有掌握系统的内存优化方案,就不能说你对Android的性能优化有过多的研究与探索。本篇,笔者将带领大家一起来系统地学习Android中的内存优化。
可能有不少读者都知道,在内存管理上,JVM拥有垃圾内存回收的机制,自身会在虚拟机层面自动分配和释放内存,因此不需要像使用C/C++一样在代码中分配和释放某一块内存。Android系统的内存管理类似于JVM,通过new关键字来为对象分配内存,内存的释放由GC来回收。并且Android系统在内存管理上有一个 Generational Heap Memory模型,当内存达到某一个阈值时,系统会根据不同的规则自动释放可以释放的内存。即便有了内存管理机制,但是,如果不合理地使用内存,也会造成一系列的性能问题,比如 内存泄漏、内存抖动、短时间内分配大量的内存对象 等等。下面,我就先来谈谈Android的内存管理机制。
我们都知道,应用程序的内存分配和垃圾回收都是由Android虚拟机完成的,在Android 5.0以下,使用的是Dalvik虚拟机,5.0及以上,则使用的是ART虚拟机。
Java代码编译后生成的字节码.class文件从从文件系统中加载到虚拟机之后,便有了JVM上的Java对象,Java对象在JVM上运行有7个阶段,如下:
Java对象的创建分为如下几步:
此时对象至少被一个强引用持有。
当一个对象处于不可见阶段时,说明程序本身不再持有该对象的任何强引用,虽然该对象仍然是存在的。简单的例子就是程序的执行已经超出了该对象的作用域了。但是,该对象仍可能被虚拟机下的某些已装载的静态变量线程或JNI等强引用持有,这些特殊的强引用称为“GC Root”。被这些GC Root强引用的对象会导致该对象的内存泄漏,因而无法被GC回收。
该对象不再被任何强引用持有。
当GC已经对该对象的内存空间重新分配做好准备时,对象进入收集阶段,如果该对象重写了finalize()方法,则执行它。
等待垃圾回收器回收该对象空间。
GC对该对象所占用的内存空间进行回收或者再分配,则该对象彻底消失。
在Android系统中,堆实际上就是一块匿名共享内存。Android虚拟机仅仅只是把它封装成一个 mSpace,由底层C库来管理,并且仍然使用libc提供的函数malloc和free来分配和释放内存。
大多数静态数据会被映射到一个共享的进程中。常见的静态数据包括Dalvik Code、app resources、so文件等等。
在大多数情况下,Android通过显示分配共享内存区域(如Ashmem或者Gralloc)来实现动态RAM区域能够在不同进程之间共享的机制。例如,Window Surface在App和Screen Compositor之间使用共享的内存,Cursor Buffers在Content Provider和Clients之间共享内存。
上面说过,对于Android Runtime有两种虚拟机,Dalvik 和 ART,它们分配的内存区域块是不同的,下面我们就来简单了解下。
不管是Dlavik还是ART,运行时堆都分为 LinearAlloc(类似于ART的Non Moving Space)、Zygote Space 和 Alloc Space。Dalvik中的Linear Alloc是一个线性内存空间,是一个只读区域,主要用来存储虚拟机中的类,因为类加载后只需要只读的属性,并且不会改变它。把这些只读属性以及在整个进程的生命周期都不能结束的永久数据放到线性分配器中管理,能很好地减少堆混乱和GC扫描,提升内存管理的性能。Zygote Space在Zygote进程和应用程序进程之间共享,Allocation Space则是每个进程独占。Android系统的第一个虚拟机由Zygote进程创建并且只有一个Zygote Space。但是当Zygote进程在fork第一个应用程序进程之前,会将已经使用的那部分堆内存划分为一部分,还没有使用的堆内存划分为另一部分,也就是Allocation Space。但无论是应用程序进程,还是Zygote进程,当他们需要分配对象时,都是在各自的Allocation Space堆上进行。
当在ART运行时,还有另外两个区块,即 ImageSpace和Large Object Space。
注意:Image Space的对象只创建一次,而Zygote Space的对象需要在系统每次启动时,根据运行情况都重新创建一遍。
在Android的高级系统版本中,针对Heap空间有一个Generational Heap Memory的模型,其中将整个内存分为三个区域:
模型示意图如下所示:
由一个Eden区和两个Survivor区组成,程序中生成的大部分新的对象都在Eden区中,当Eden区满时,还存活的对象将被复制到其中一个Survivor区,当此Survivor区满时,此区存活的对象又被复制到另一个Survivor区,当这个Survivor区也满时,会将其中存活的对象复制到年老代。
一般情况下,年老代中的对象生命周期都比较长。
用于存放静态的类和方法,持久代对垃圾回收没有显著影响。
系统在Young Generation、Old Generation上采用不同的回收机制。每一个Generation的内存区域都有固定的大小。随着新的对象陆续被分配到此区域,当对象总的大小临近这一级别内存区域的阈值时,会触发GC操作,以便腾出空间来存放其他新的对象。
此外,执行GC占用的时间与Generation和Generation中的对象数量有关,如下所示:
由于其对象存活时间短,因此基于Copying算法(扫描出存活的对象,并复制到一块新的完全未使用的控件中)来回收。新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在Young Generation区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。
由于其对象存活时间较长,比较稳定,因此采用Mark(标记)算法(扫描出存活的对象,然后再回收未被标记的对象,回收后对空出的空间要么合并,要么标记出来便于下次分配,以减少内存碎片带来的效率损耗)来回收。
在Android系统中,GC有三种类型:
接下来,我们来学会如何分析Android虚拟机中的GC日志,日志如下:
D/dalvikvm(7030):GC_CONCURRENT freed 1049K, 60% free 2341K/9351K, external 3502K/6261K, paused 3ms 3ms
GC_CONCURRENT 是当前GC时的类型,GC日志中有以下几种类型:
我们再回到上面打印的日志:
注意:在ART模式下,多了一个Large Object Space,这部分内存并不是分配在堆上,但还是属于应用程序的内存空间。
在Dalvik虚拟机下,GC的操作都是并发的,也就意味着每次触发GC都会导致其它线程暂停工作(包括UI线程)。而在ART模式下,GC时不像Dalvik仅有一种回收算法,ART在不同的情况下会选择不同的回收算法,比如Alloc内存不够时会采用非并发GC,但在Alloc后,发现内存达到一定阈值时又会触发并发GC。所以在ART模式下,并不是所有的GC都是非并发的。
总体来看,在GC方面,与Dalvik相比,ART更为高效,不仅仅是GC的效率,大大地缩短了Pause时间,而且在内存分配上对大内存分配单独的区域,还能有算法在后台做内存整理,减少内存碎片。因此,在ART虚拟机下,可以避免较多的类似GC导致的卡顿问题。
优化内存的意义不言而喻,总的来说可以归结为如下四点:
需要注意的是,出现OOM是因为内存溢出导致,这种情况不一定会发生在相对应的代码处,也不一定是出现OOM的代码使用内存有问题,而是刚好执行到这段代码。
Android系统虚拟机的垃圾回收是通过虚拟机GC机制来实现的。GC会选择一些还存活的对象作为内存遍历的根节点GC Roots,通过对GC Roots的可达性来判断是否需要回收。内存泄漏就是在当前应用周期内不再使用的对象被GC Roots引用,导致不能回收,使实际可使用内存变小。
MAT工具可以帮助开发者定位导致内存泄漏的对象,以及发现大的内存对象,然后解决内存泄漏并通过优化内存对象,以达到减少内存消耗的目的。
./hprof-conv file.hprof converted.hprof
在MAT窗口上,OverView是一个总体概览,显示总体的内存消耗情况和疑似问题。MAT提供了多种分析维度,其中Histogram、Dominator Tree、Top Consumers和Leak Suspects的分析维度是不同的。下面分别介绍下它们,如下所示:
列出内存中的所有实例类型对象和其个数以及大小,并在顶部的regex区域支持正则表达式查找。
列出最大的对象及其依赖存活的Object。相比Histogram,能更方便地看出引用关系。
通过图像列出最大的Object。
通过MAT自动分析内存泄漏的原因和泄漏的一份总体报告。
分析内存最常用的是Histogram和Dominator Tree这两个视图,视图中一共有四列:
还有一种更快速的方法就是对比泄漏前后的HPROF数据:
需要注意的是,如果目标不太明确,可以直接定位RetainedHeap最大的Object,通过Select incoming references查看引用链,定位到可疑的对象,然后通过Path to GC Roots分析引用链。
此外,我们知道,当Hash集合中过多的对象返回相同的Hash值时,会严重影响性能,这时可以用 Map Collision Ratio 查找导致Hash集合的碰撞率较高的罪魁祸首。
在本人平时的项目开发中,一般会使用如下几种方式来快速对指定页面进行内存泄漏的检测(也称为运行时内存分析优化):
1、shell命令 + LeakCanary + MAT:运行程序,所有功能跑一遍,确保没有改出问题,完全退出程序,手动触发GC,然后使用adb shell dumpsys meminfo packagename -d命令查看退出界面后Objects下的Views和Activities数目是否为0,如果不是则通过LeakCanary检查可能存在内存泄露的地方,最后通过MAT分析,如此反复,改善满意为止。
2、Profile MEMORY:运行程序,对每一个页面进行内存分析检查。首先,反复打开关闭页面5次,然后收到GC(点击Profile MEMORY左上角的垃圾桶图标),如果此时total内存还没有恢复到之前的数值,则可能发生了内存泄露。此时,再点击Profile MEMORY左上角的垃圾桶图标旁的heap dump按钮查看当前的内存堆栈情况,选择按包名查找,找到当前测试的Activity,如果引用了多个实例,则表明发生了内存泄露。
3、从首页开始用依次dump出每个页面的内存快照文件,然后利用MAT的对比功能,找出每个页面相对于上个页面内存里主要增加了哪些东西,做针对性优化。
4、利用Android Memory Profiler实时观察进入每个页面后的内存变化情况,然后对产生的内存较大波峰做分析。
此外,除了运行时内存的分析优化,我们还可以对App的静态内存进行分析与优化。静态内存指的是在伴随着App的整个生命周期一直存在的那部分内存,那我们怎么获取这部分内存快照呢?
首先,确保打开每一个主要页面的主要功能,然后回到首页,进开发者选项去打开"不保留后台活动"。然后,将我们的app退到后台,GC,dump出内存快照。最后,我们就可以将对dump出的内存快照进行分析,看看有哪些地方是可以优化的,比如加载的图片、应用中全局的单例数据配置、静态内存与缓存、埋点数据、内存泄漏等等。
对于内存泄漏,其本质可理解为无法回收无用的对象。这里我总结了我在项目中遇到的一些常见的内存泄漏案例(包含解决方案)。
对于资源性对象不再使用时,应该立即调用它的close()函数,将其关闭,然后再置为null。例如Bitmap等资源未关闭会造成内存泄漏,此时我们应该在Activity销毁时及时关闭。
例如BraodcastReceiver、EventBus未注销造成的内存泄漏,我们应该在Activity销毁时及时注销。
尽量避免使用静态变量存储数据,特别是大数据对象,建议使用数据库存储。
优先使用Application的Context,如需使用Activity的Context,可以在传入Context时使用弱引用进行封装,然后,在使用到的地方从弱引用中获取Context,如果获取不到,则直接return即可。
该实例的生命周期和应用一样长,这就导致该静态实例一直持有该Activity的引用,Activity的内存资源不能正常回收。此时,我们可以将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,尽量使用Application Context,如果需要使用Activity Context,就记得用完后置空让GC可以回收,否则还是会内存泄漏。
Message发出之后存储在MessageQueue中,在Message中存在一个target,它是Handler的一个引用,Message在Queue中存在的时间过长,就会导致Handler无法被回收。如果Handler是非静态的,则会导致Activity或者Service不会被回收。并且消息队列是在一个Looper线程中不断地轮询处理消息,当这个Activity退出时,消息队列中还有未处理的消息或者正在处理的消息,并且消息队列中的Message持有Handler实例的引用,Handler又持有Activity的引用,所以导致该Activity的内存资源无法及时回收,引发内存泄漏。解决方案如下所示:
需要注意的是,AsyncTask内部也是Handler机制,同样存在内存泄漏风险,但其一般是临时性的。对于类似AsyncTask或是线程造成的内存泄漏,我们也可以将AsyncTask和Runnable类独立出来或者使用静态内部类。
在退出程序之前,将集合里的东西clear,然后置为null,再退出程序
WebView都存在内存泄漏的问题,在应用中只要使用一次WebView,内存就不会被释放掉。我们可以为WebView开启一个独立的进程,使用AIDL与应用的主进程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,达到正常释放内存的目的。
在构造Adapter时,使用缓存的convertView。
一般使用LeakCanary进行内存泄漏的监控即可,具体使用和原理分析请参见我之前的文章Android主流三方库源码分析(六、深入理解Leakcanary源码)。
除了基本使用外,我们还可以自定义处理结果,首先,继承DisplayLeakService实现一个自定义的监控处理Service,代码如下:
public class LeakCnaryService extends DisplayLeakServcie {
private final String TAG = “LeakCanaryService”;
@Override
protected void afterDefaultHandling(HeapDump heapDump, AnalysisResult result, String leakInfo) {
...
}
}
重写 afterDefaultHanding 方法,在其中处理需要的数据,三个参数的定义如下:
然后在install时,使用自定义的LeakCanaryService即可,代码如下:
public class BaseApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
mRefWatcher = LeakCanary.install(this, LeakCanaryService.calss, AndroidExcludedRefs.createAppDefaults().build());
}
...
}
经过这样的处理,就可以在LeakCanaryService中实现自己的处理方式,如丰富的提示信息,把数据保存在本地、上传到服务器进行分析。
LeakCanaryService需要在AndroidManifest中注册。
从Java 1.2版本开始引入了三种对象引用方式:SoftReference、WeakReference 和 PhantomReference 三个引用类,引用类的主要功能就是能够引用但仍可以被垃圾回收器回收的对象。在引入引用类之前,只能使用Strong Reference,如果没有指定对象引用类型,默认是强引用。下面,我们就分别来介绍下这几种引用。
如果一个对象具有强引用,GC就绝对不会回收它。当内存空间不足时,JVM会抛出OOM错误。
如果一个对象只具有软引用,则内存空间足够,GC时就不会回收它;如果内存不足,就会回收这些对象的内存。可用来实现内存敏感的高速缓存。
软引用可以和一个ReferenceQueue(引用队列)联合使用,如果软引用引用的对象被垃圾回收器回收,JVM会把这个软引用加入与之关联的引用队列中。
在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间是否足够,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
这里要注意,可能需要运行多次GC,才能找到并释放弱引用对象。
只能用于跟踪即将对被引用对象进行的收集。虚拟机必须与ReferenceQueue类联合使用。因为它能够充当通知机制。
自动装箱的核心就是把基础数据类型转换成对应的复杂类型。在自动装箱转化时,都会产生一个新的对象,这样就会产生更多的内存和性能开销。如int只占4字节,而Integer对象有16字节,特别是HashMap这类容器,进行增、删、改、查操作时,都会产生大量的自动装箱操作。
使用TraceView查看耗时,如果发现调用了大量的integer.value,就说明发生了AutoBoxing。
对于内存复用,有如下四种可行的方式:
HashMap是一个散列链表,向HashMap中put元素时,先根据key的HashCode重新计算hash值,根据hash值得到这个元素在数组中的位置,如果数组该位置上已经存放有其它元素了,那么这个位置上的元素将以链表的形式存放,新加入的放在链头,最后加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。也就是说,向HashMap插入一个对象前,会给一个通向Hash阵列的索引,在索引的位置中,保存了这个Key对象的值。这意味着需要考虑的一个最大问题是冲突,当多个对象散列于阵列相同位置时,就会有散列冲突的问题。因此,HashMap会配置一个大的数组来减少潜在的冲突,并且会有其他逻辑防止链接算法和一些冲突的发生。
ArrayMap提供了和HashMap一样的功能,但避免了过多的内存开销,方法是使用两个小数组,而不是一个大数组。并且ArrayMap在内存上是连续不间断的。
总体来说,在ArrayMap中执行插入或者删除操作时,从性能角度上看,比HashMap还要更差一些,但如果只涉及很小的对象数,比如1000以下,就不需要担心这个问题了。因为此时ArrayMap不会分配过大的数组。
此外,Android自身还提供了一系列优化过后的数据集合工具类,如 SparseArray、SparseBooleanArray、LongSparseArray,使用这些API可以让我们的程序更加高效。HashMap 工具类会相对比较 低效,因为它 需要为每一个键值对都提供一个对象入口,而 SparseArray 就 避免 掉了 基本数据类型转换成对象数据类型的时间。
使用枚举类型的dex size是普通常量定义的dex size的13倍以上,同时,运行时的内存分配,一个enum值的声明会消耗至少20bytes。
枚举最大的优点是类型安全,但在Android平台上,枚举的内存开销是直接定义常量的三倍以上。所以Android提供了注解的方式检查类型安全。目前提供了int型和String型两种注解方式:IntDef和StringDef,用来提供编译期的类型检查。
使用IntDef和StringDef需要在Gradle配置中引入相应的依赖包:
compile 'com.android.support:support-annotations:22.0.0'
最近最少使用缓存,使用强引用保存需要缓存的对象,它内部维护了一个由LinkedHashMap组成的双向列表,不支持线程安全,LruCache对它进行了封装,添加了线程安全操作。当其中的一个值被访问时,它被放到队列的尾部,当缓存将满时,队列头部的值(最近最少被访问的)被丢弃,之后可以被GC回收。
除了普通的get/set方法之外,还有sizeOf方法,它用来返回每个缓存对象的大小。此外,还有entryRemoved方法,当一个缓存对象被丢弃时调用的方法,当第一个参数为true:表明环处对象是为了腾出空间而被清理时。否则,表明缓存对象的entry被remove移除或者被put覆盖时。
分配LruCache大小时应考虑应用剩余内存有多大。
在Android默认情况下,当图片文件解码成位图时,会被处理成32bit/像素。红色、绿色、蓝色和透明通道各8bit,即使是没有透明通道的图片,如JEPG隔世是没有透明通道的,但然后会处理成32bit位图,这样分配的32bit中的8bit透明通道数据是没有任何用处的,这完全没有必要,并且在这些图片被屏幕渲染之前,它们首先要被作为纹理传送到GPU,这意味着每一张图片会同时占用CPU内存和GPU内存。下面,我总结了减少内存开销的几种常用方式,如下所示:
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
BitmapFactory.decodeStream(is, null, options);
BitampFactory.Options options = new BitmapFactory.Options();
// 设置为4就是宽和高都变为原来1/4大小的图片
options.inSampleSize = 4;
BitmapFactory.decodeSream(is, null, options);
BitampFactory.Options options = new BitampFactory.Options();
options.inScaled = true;
options.inDensity = srcWidth;
options.inTargetDensity = dstWidth;
BitmapFactory.decodeStream(is, null, options);
上述三种方案的缺点:使用了过多的算法,导致图片显示过程需要更多的时间开销,如果图片很多的话,就影响到图片的显示效果。最好的方案是结合这两个方法,达到最佳的性能结合,首先使用inSampleSize处理图片,转换为接近目标的2次幂,然后用inDensity和inTargetDensity生成最终想要的准确大小,因为inSampleSize会减少像素的数量,而基于输出密码的需要对像素重新过滤。但获取资源图片的大小,需要设置位图对象的inJustDecodeBounds值为true,然后继续解码图片文件,这样才能生产图片的宽高数据,并允许继续优化图片。总体的代码如下所示:
BitmapFactory.Options options = new BitampFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(is, null, options);
options.inScaled = true;
options.inDensity = options.outWidth;
options.inSampleSize = 4;
Options.inTargetDensity = desWith * options.inSampleSize;
options.inJustDecodeBounds = false;
BitmapFactory.decodeStream(is, null, options);
可以结合LruCache来实现,在LruCache移除超出cache size的图片时,暂时缓存Bitamp到一个软引用集合,需要创建新的Bitamp时,可以从这个软用用集合中找到最适合重用的Bitmap,来重用它的内存区域。
需要注意,新申请的Bitmap与旧的Bitmap必须有相同的解码格式,并且在Android 4.4之前,只能重用相同大小的Bitamp的内存区域,而Android 4.4之后可以重用任何bitmap的内存区域。
只需要UI提供一套高分辨率的图,图片建议放在drawable-xxhdpi文件夹下,这样在低分辨率设备中图片的大小只是压缩,不会存在内存增大的情况。如若遇到不需缩放的文件,放在drawable-nodpi文件夹下。
在App退到后台内存紧张即将被Kill掉时选择重写 onTrimMemory/onLowMemory 方法去释放掉图片缓存、静态缓存来自保。
例如,我们可以在字符串拼接的时候使用StringBuffer,StringBuilder。
例如,在onDraw方法里面不要执行对象的创建,一般来说,都应该在自定义View的构造器中创建对象。
除了上面的一些内存优化点之外,这里还有一些内存优化的点我们需要注意,如下所示:
在设计一个模块时,需要考虑以下几点:
在编写代码前先画好UML图,确定每一个对象、方法、接口的功能,首先尽量做到功能单一原则,在这个基础上,再明确模块与模块的直接关系,最后使用代码实现。
ImageLoader是实现图片加载的基类,其中ImageLoader有一个内部类BitmapLoadTask是继承AsyncTask的异步下载管理类,负责图片的下载和刷新,MiniImageLoader是ImageLoader的子类,维护类一个ImageLoader的单例,并且实现了基类的网络加载功能,因为具体的下载在应用中有不同的下载引擎,抽象成接口便于替换。代码如下所示:
public abstract class ImageLoader {
private boolean mExitTasksEarly = false; //是否提前结束
protected boolean mPauseWork = false;
private final Object mPauseWorkLock = new Object();
protected ImageLoader() {
}
public void loadImage(String url, ImageView imageView) {
if (url == null) {
return;
}
BitmapDrawable bitmapDrawable = null;
if (bitmapDrawable != null) {
imageView.setImageDrawable(bitmapDrawable);
} else {
final BitmapLoadTask task = new BitmapLoadTask(url, imageView);
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}
private class BitmapLoadTask extends AsyncTask {
private String mUrl;
private final WeakReference imageViewWeakReference;
public BitmapLoadTask(String url, ImageView imageView) {
mUrl = url;
imageViewWeakReference = new WeakReference(imageView);
}
@Override
protected Bitmap doInBackground(Void... params) {
Bitmap bitmap = null;
BitmapDrawable drawable = null;
synchronized (mPauseWorkLock) {
while (mPauseWork && !isCancelled()) {
try {
mPauseWorkLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
if (bitmap == null
&& !isCancelled()
&& imageViewWeakReference.get() != null
&& !mExitTasksEarly) {
bitmap = downLoadBitmap(mUrl);
}
return bitmap;
}
@Override
protected void onPostExecute(Bitmap bitmap) {
if (isCancelled() || mExitTasksEarly) {
bitmap = null;
}
ImageView imageView = imageViewWeakReference.get();
if (bitmap != null && imageView != null) {
setImageBitmap(imageView, bitmap);
}
}
@Override
protected void onCancelled(Bitmap bitmap) {
super.onCancelled(bitmap);
synchronized (mPauseWorkLock) {
mPauseWorkLock.notifyAll();
}
}
}
public void setPauseWork(boolean pauseWork) {
synchronized (mPauseWorkLock) {
mPauseWork = pauseWork;
if (!mPauseWork) {
mPauseWorkLock.notifyAll();
}
}
}
public void setExitTasksEarly(boolean exitTasksEarly) {
mExitTasksEarly = exitTasksEarly;
setPauseWork(false);
}
private void setImageBitmap(ImageView imageView, Bitmap bitmap) {
imageView.setImageBitmap(bitmap);
}
protected abstract Bitmap downLoadBitmap(String mUrl);
}
setPauseWork方法是图片加载线程控制接口,pauseWork控制图片模块的暂停和继续工作,一般在listView等控件中,滑动时停止加载图片,保证滑动流畅。另外,具体的图片下载和解码是和业务强相关的,因此在ImageLoader中不做具体的实现,只是定义类一个抽象方法。
MiniImageLoader是一个单例,保证一个应用只维护一个ImageLoader,减少对象开销,并管理应用中所有的图片加载。MiniImageLoader代码如下所示:
public class MiniImageLoader extends ImageLoader {
private volatile static MiniImageLoader sMiniImageLoader = null;
private ImageCache mImageCache = null;
public static MiniImageLoader getInstance() {
if (null == sMiniImageLoader) {
synchronized (MiniImageLoader.class) {
MiniImageLoader tmp = sMiniImageLoader;
if (tmp == null) {
tmp = new MiniImageLoader();
}
sMiniImageLoader = tmp;
}
}
return sMiniImageLoader;
}
public MiniImageLoader() {
mImageCache = new ImageCache();
}
@Override
protected Bitmap downLoadBitmap(String mUrl) {
HttpURLConnection urlConnection = null;
InputStream in = null;
try {
final URL url = new URL(mUrl);
urlConnection = (HttpURLConnection) url.openConnection();
in = urlConnection.getInputStream();
Bitmap bitmap = decodeSampledBitmapFromStream(in, null);
return bitmap;
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
urlConnection = null;
}
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
public Bitmap decodeSampledBitmapFromStream(InputStream is, BitmapFactory.Options options) {
return BitmapFactory.decodeStream(is, null, options);
}
}
其中,volatile保证了对象从主内存加载。并且,上面的try …cache层级太多,Java中有一个Closeable接口,该接口标识类一个可关闭的对象,因此可以写如下的工具类:
public class CloseUtils {
public static void closeQuietly(Closeable closeable) {
if (null != closeable) {
try {
closeable.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
改造后如下所示:
finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
CloseUtil.closeQuietly(in);
}
同时,为了使ListView在滑动过程中更流畅,在滑动时暂停图片加载,减少系统开销,代码如下所示:
listView.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView absListView, int scrollState) {
if (scorllState == AbsListView.OnScrollListener.SCROLL_STAE_FLING) {
MiniImageLoader.getInstance().setPauseWork(true);
} else {
MiniImageLoader.getInstance().setPauseWork(false);
}
}
这里使用一个BitmapConfig类来实现参数的配置,代码如下所示:
public class BitmapConfig {
private int mWidth, mHeight;
private Bitmap.Config mPreferred;
public BitmapConfig(int width, int height) {
this.mWidth = width;
this.mHeight = height;
this.mPreferred = Bitmap.Config.RGB_565;
}
public BitmapConfig(int width, int height, Bitmap.Config preferred) {
this.mWidth = width;
this.mHeight = height;
this.mPreferred = preferred;
}
public BitmapFactory.Options getBitmapOptions() {
return getBitmapOptions(null);
}
// 精确计算,需要图片is流现解码,再计算宽高比
public BitmapFactory.Options getBitmapOptions(InputStream is) {
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
if (is != null) {
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(is, null, options);
options.inSampleSize = calculateInSampleSize(options, mWidth, mHeight);
}
options.inJustDecodeBounds = false;
return options;
}
private static int calculateInSampleSize(BitmapFactory.Options options, int mWidth, int mHeight) {
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > mHeight || width > mWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
while ((halfHeight / inSampleSize) > mHeight
&& (halfWidth / inSampleSize) > mWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
}
然后,调用MiniImageLoader的downLoadBitmap方法,增加获取BitmapFactory.Options的步骤:
final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
in = urlConnection.getInputStream();
final BitmapFactory.Options options = mConfig.getBitmapOptions(in);
in.close();
urlConnection.disconnect();
urlConnection = (HttpURLConnection) url.openConnection();
in = urlConnection.getInputStream();
Bitmap bitmap = decodeSampledBitmapFromStream(in, options);
优化后仍存在一些问题:
为了解决这两个问题,就需要有内存池的设计理念,通过内存池控制整体图片内存,不重新加载和解码已经显示过的图片。
内存–本地–网络
使用软引用和弱引用(SoftReference or WeakReference)来实现内存池是以前的常用做法,但是现在不建议。从API 9起(Android 2.3)开始,Android系统垃圾回收器更倾向于回收持有软引用和弱引用的对象,所以不是很靠谱,从Android 3.0开始(API 11)开始,图片的数据无法用一种可遇见的方式将其释放,这就存在潜在的内存溢出风险。
使用LruCache来实现内存管理是一种可靠的方式,它的主要算法原理是把最近使用的对象用强引用来存储在LinkedHashMap中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。使用LruCache实现一个图片的内存缓存的代码如下所示:
public class MemoryCache {
private final int DEFAULT_MEM_CACHE_SIZE = 1024 * 12;
private LruCache mMemoryCache;
private final String TAG = "MemoryCache";
public MemoryCache(float sizePer) {
init(sizePer);
}
private void init(float sizePer) {
int cacheSize = DEFAULT_MEM_CACHE_SIZE;
if (sizePer > 0) {
cacheSize = Math.round(sizePer * Runtime.getRuntime().maxMemory() / 1024);
}
mMemoryCache = new LruCache(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
final int bitmapSize = getBitmapSize(value) / 1024;
return bitmapSize == 0 ? 1 : bitmapSize;
}
@Override
protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
super.entryRemoved(evicted, key, oldValue, newValue);
}
};
}
@TargetApi(Build.VERSION_CODES.KITKAT)
public int getBitmapSize(Bitmap bitmap) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return bitmap.getAllocationByteCount();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
return bitmap.getByteCount();
}
return bitmap.getRowBytes() * bitmap.getHeight();
}
public Bitmap getBitmap(String url) {
Bitmap bitmap = null;
if (mMemoryCache != null) {
bitmap = mMemoryCache.get(url);
}
if (bitmap != null) {
Log.d(TAG, "Memory cache exiet");
}
return bitmap;
}
public void addBitmapToCache(String url, Bitmap bitmap) {
if (url == null || bitmap == null) {
return;
}
mMemoryCache.put(url, bitmap);
}
public void clearCache() {
if (mMemoryCache != null) {
mMemoryCache.evictAll();
}
}
}
上述代码中cacheSize百分比占比多少合适?可以基于以下几点来考虑:
在应用中,如果有一些图片的访问频率要比其它的大一些,或者必须一直显示出来,就需要一直保持在内存中,这种情况可以使用多个LruCache对象来管理多组Bitmap,对Bitmap进行分级,不同级别的Bitmap放到不同的LruCache中。
从Android3.0开始Bitmap支持内存复用,也就是BitmapFactoy.Options.inBitmap属性,如果这个属性被设置有效的目标用对象,decode方法就在加载内容时重用已经存在的bitmap,这意味着Bitmap的内存被重新利用,这可以减少内存的分配回收,提高图片的性能。代码如下所示:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
mReusableBitmaps = Collections.synchronizedSet(newHashSet>());
}
因为inBitmap属性在Android3.0以后才支持,在entryRemoved方法中加入软引用集合,作为复用的源对象,之前是直接删除,代码如下所示:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
mReusableBitmaps.add(new SoftReference(oldValue));
}
同样在3.0以上判断,需要分配一个新的bitmap对象时,首先检查是否有可复用的bitmap对象:
public static Bitmap decodeSampledBitmapFromStream(InputStream is, BitmapFactory.Options options, ImageCache cache) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
addInBitmapOptions(options, cache);
}
return BitmapFactory.decodeStream(is, null, options);
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
private static void addInBitmapOptions(BitmapFactory.Options options, ImageCache cache) {
options.inMutable = true;
if (cache != null) {
Bitmap inBitmap = cache.getBitmapFromReusableSet(options);
if (inBitmap != null) {
options.inBitmap = inBitmap;
}
}
}
接着,我们使用cache.getBitmapForResubleSet方法查找一个合适的bitmap赋值给inBitmap。代码如下所示:
// 获取inBitmap,实现内存复用
public Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {
Bitmap bitmap = null;
if (mReusableBitmaps != null && !mReusableBitmaps.isEmpty()) {
final Iterator> iterator = mReusableBitmaps.iterator();
Bitmap item;
while (iterator.hasNext()) {
item = iterator.next().get();
if (null != item && item.isMutable()) {
if (canUseForInBitmap(item, options)) {
Log.v("TEST", "canUseForInBitmap!!!!");
bitmap = item;
// Remove from reusable set so it can't be used again
iterator.remove();
break;
}
} else {
// Remove from the set if the reference has been cleared.
iterator.remove();
}
}
}
return bitmap;
}
上述方法从软引用集合中查找规格可利用的Bitamp作为内存复用对象,因为使用inBitmap有一些限制,在Android 4.4之前,只支持同等大小的位图。因此使用了canUseForInBitmap方法来判断该Bitmap是否可以复用,代码如下所示:
@TargetApi(Build.VERSION_CODES.KITKAT)
private static boolean canUseForInBitmap(
Bitmap candidate, BitmapFactory.Options targetOptions) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
return candidate.getWidth() == targetOptions.outWidth
&& candidate.getHeight() == targetOptions.outHeight
&& targetOptions.inSampleSize == 1;
}
int width = targetOptions.outWidth / targetOptions.inSampleSize;
int height = targetOptions.outHeight / targetOptions.inSampleSize;
int byteCount = width * height * getBytesPerPixel(candidate.getConfig());
return byteCount <= candidate.getAllocationByteCount();
}
由于磁盘读取时间是不可预知的,所以图片的解码和文件读取都应该在后台进程中完成。DisLruCache是Android提供的一个管理磁盘缓存的类。
public static DiskLruCache open(File directory, int appVersion, int valueCou9nt, long maxSize)
directory一般建议缓存到SD卡上。appVersion发生变化时,会自动删除前一个版本的数据。valueCount是指Key与Value的对应关系,一般情况下是1对1的关系。maxSize是缓存图片的最大缓存数据大小。初始化DiskLruCache的代码如下所示:
private void init(final long cacheSize,final File cacheFile) {
new Thread(new Runnable() {
@Override
public void run() {
synchronized (mDiskCacheLock) {
if(!cacheFile.exists()){
cacheFile.mkdir();
}
MLog.d(TAG,"Init DiskLruCache cache path:" + cacheFile.getPath() + "\r\n" + "Disk Size:" + cacheSize);
try {
mDiskLruCache = DiskLruCache.open(cacheFile, MiniImageLoaderConfig.VESION_IMAGELOADER, 1, cacheSize);
mDiskCacheStarting = false;
// Finished initialization
mDiskCacheLock.notifyAll();
// Wake any waiting threads
}catch(IOException e){
MLog.e(TAG,"Init err:" + e.getMessage());
}
}
}
}).start();
}
如果在初始化前就要操作写或者读会导致失败,所以在整个DiskCache中使用的Object的wait/notifyAll机制来避免同步问题。
首先,获取Editor实例,它需要传入一个key来获取参数,Key必须与图片有唯一对应关系,但由于URL中的字符可能会带来文件名不支持的字符类型,所以取URL的MD4值作为文件名,实现Key与图片的对应关系,通过URL获取MD5值的代码如下所示:
private String hashKeyForDisk(String key) {
String cacheKey;
try {
final MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(key.getBytes());
cacheKey = bytesToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(key.hashCode());
}
return cacheKey;
}
private String bytesToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(0xFF & bytes[i]);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
然后,写入需要保存的图片数据,图片数据写入本地缓存的整体代码如下所示:
public void saveToDisk(String imageUrl, InputStream in) {
// add to disk cache
synchronized (mDiskCacheLock) {
try {
while (mDiskCacheStarting) {
try {
mDiskCacheLock.wait();
} catch (InterruptedException e) {}
}
String key = hashKeyForDisk(imageUrl);
MLog.d(TAG,"saveToDisk get key:" + key);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (in != null && editor != null) {
// 当 valueCount指定为1时,index传0即可
OutputStream outputStream = editor.newOutputStream(0);
MLog.d(TAG, "saveToDisk");
if (FileUtil.copyStream(in,outputStream)) {
MLog.d(TAG, "saveToDisk commit start");
editor.commit();
MLog.d(TAG, "saveToDisk commit over");
} else {
editor.abort();
MLog.e(TAG, "saveToDisk commit abort");
}
}
mDiskLruCache.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
接着,读取图片缓存,通过DiskLruCache的get方法实现,代码如下所示:
public Bitmap getBitmapFromDiskCache(String imageUrl,BitmapConfig bitmapconfig) {
synchronized (mDiskCacheLock) {
// Wait while disk cache is started from background thread
while (mDiskCacheStarting) {
try {
mDiskCacheLock.wait();
} catch (InterruptedException e) {}
}
if (mDiskLruCache != null) {
try {
String key = hashKeyForDisk(imageUrl);
MLog.d(TAG,"getBitmapFromDiskCache get key:" + key);
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
if(null == snapShot){
return null;
}
InputStream is = snapShot.getInputStream(0);
if(is != null){
final BitmapFactory.Options options = bitmapconfig.getBitmapOptions();
return BitmapUtil.decodeSampledBitmapFromStream(is, options);
}else{
MLog.e(TAG,"is not exist");
}
}catch (IOException e){
MLog.e(TAG,"getBitmapFromDiskCache ERROR");
}
}
}
return null;
}
最后,要注意读取并解码Bitmap数据和保存图片数据都是有一定耗时的IO操作。所以这些方法都是在ImageLoader中的doInBackground方法中调用,代码如下所示:
@Override
protected Bitmap doInBackground(Void... params) {
Bitmap bitmap = null;
synchronized (mPauseWorkLock) {
while (mPauseWork && !isCancelled()) {
try {
mPauseWorkLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
if (bitmap == null && !isCancelled()
&& imageViewReference.get() != null && !mExitTasksEarly) {
bitmap = getmImageCache().getBitmapFromDisk(mUrl, mBitmapConfig);
}
if (bitmap == null && !isCancelled()
&& imageViewReference.get() != null && !mExitTasksEarly) {
bitmap = downLoadBitmap(mUrl, mBitmapConfig);
}
if (bitmap != null) {
getmImageCache().addToCache(mUrl, bitmap);
}
return bitmap;
}
目前使用最广泛的有Picasso、Glide和Fresco。Glide和Picasso比较相似,但是Glide相对于Picasso来说,功能更丰富,内部实现更复杂,对Glide有兴趣的同学可以阅读这篇文章Android主流三方库源码分析(三、深入理解Glide源码)。Fresco最大的亮点在于它的内存管理,特别是在低端机和Android 5.0以下的机器上的优势更加明显,而使用Fresco将很好地解决图片占用内存大的问题。因为,Fresco会将图片放到一个特别的内存区域,当图片不再显示时,占用的内存会自动释放。这类总结下Fresco的优点,如下所示:
安装包过大,所以对图片加载和显示要求不是比较高的情况下建议使用Glide。
对于内存优化,一般都是通过使用MAT等工具来进行检查和使用LeakCanary等内存泄漏监控工具来进行监控,以此来发现问题,再分析问题原因,解决发现的问题或者对当前的实现逻辑进行优化,优化完后再进行检查,直到达到预定的性能指标。下一篇文章,将会和大家一起来深入探索Android的内存优化,尽请期待~
1、Android应用性能优化最佳实践
2、必知必会 | Android 性能优化的方面方面都在这儿
3、实践App内存优化:如何有序地做内存分析与优化
欢迎关注我的微信:
bcce5360
微信群如果不能扫码加入,麻烦大家想进微信群的朋友们,加我微信拉你进群。
2千人QQ群,Awesome-Android学习交流群,QQ群号:959936182, 欢迎大家加入~