Android性能优化以内存优化

前言

成为一名优秀的Android开发,须要一份完备的知识体系,在这里,让咱们一块儿成长为本身所想的那样~。

Tips:本篇是《深刻探索Android内存优化》的基础篇,若是没有掌握Android内存优化的同窗建议系统学习一遍。
复制代码

众所周知,内存优化能够说是性能优化中最重要的优化点之一,能够说,若是你没有掌握系统的内存优化方案,就不能说你对Android的性能优化有过多的研究与探索。本篇,笔者将带领你们一块儿来系统地学习Android中的内存优化。php

可能有很多读者都知道,在内存管理上,JVM拥有垃圾内存回收的机制,自身会在虚拟机层面自动分配和释放内存,所以不须要像使用C/C++同样在代码中分配和释放某一块内存。Android系统的内存管理相似于JVM,经过new关键字来为对象分配内存,内存的释放由GC来回收。而且Android系统在内存管理上有一个 Generational Heap Memory模型,当内存达到某一个阈值时,系统会根据不一样的规则自动释放能够释放的内存。即使有了内存管理机制,可是,若是不合理地使用内存,也会形成一系列的性能问题,好比 内存泄漏、内存抖动、短期内分配大量的内存对象 等等。下面,我就先来谈谈Android的内存管理机制。android

1、Android内存管理机制

咱们都知道,应用程序的内存分配和垃圾回收都是由Android虚拟机完成的,在Android 5.0如下,使用的是Dalvik虚拟机,5.0及以上,则使用的是ART虚拟机git

一、Java对象生命周期

Java代码编译后生成的字节码.class文件从从文件系统中加载到虚拟机以后,便有了JVM上的Java对象,Java对象在JVM上运行有7个阶段,以下:github

  • Created
  • InUse
  • Invisible
  • Unreachable
  • Collected
  • Finalized
  • Deallocated

一、Created(建立)

Java对象的建立分为以下几步:正则表达式

  • 一、为对象分配存储空间。
  • 二、构造对象。
  • 三、从超类到子类对static成员进行初始化,类的static成员的初始化在ClassLoader加载该类时进行。
  • 四、超类成员变量按顺序初始化,递归调用超类的构造方法。
  • 五、子类成员变量按顺序初始化,一旦对象被建立,子类构造方法就调用该对象并为某些变量赋值。

二、InUse(应用)

此时对象至少被一个强引用持有算法

三、Invisible(不可见)

当一个对象处于不可见阶段时,说明程序自己再也不持有该对象的任何强引用,虽然该对象仍然是存在的。简单的例子就是程序的执行已经超出了该对象的做用域了。可是,该对象仍可能被虚拟机下的某些已装载的静态变量线程或JNI等强引用持有,这些特殊的强引用称为“GC Root”被这些GC Root强引用的对象会致使该对象的内存泄漏,于是没法被GC回收shell

四、Unreachable(不可达)

该对象再也不被任何强引用持有数据库

五、Collected(收集)

GC已经对该对象的内存空间从新分配作好准备时,对象进入收集阶段,若是该对象重写了finalize()方法,则执行它。json

六、Finalized(终结)

等待垃圾回收器回收该对象空间数组

七、Deallocated(对象空间从新分配)

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,它们分配的内存区域块是不一样的,下面咱们就来简单了解下。

Dalvik

  • Linear Alloc
  • Zygote Space
  • Alloc Space

ART

  • Non Moving Space
  • Zygote Space
  • Alloc Space
  • Image Space
  • Large Obj Space

无论是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存放一些预加载类,相似于Dalvik中的Linear Alloc。与Zygote Space同样,在Zygote进程和应用程序进程之间共享
  • Large Object Space离散地址的集合,分配一些大对象,用于提升GC的管理效率和总体性能

注意:Image Space的对象只建立一次,而Zygote Space的对象须要在系统每次启动时,根据运行状况都从新建立一遍。

三、内存回收机制

在Android的高级系统版本中,针对Heap空间有一个Generational Heap Memory的模型,其中将整个内存分为三个区域:

  • Young Generation(年轻代)
  • Old Generation(年老代)
  • Permanent Generation(持久代)

模型示意图以下所示:

一、Young Generation

一个Eden区和两个Survivor区组成,程序中生成的大部分新的对象都在Eden区中,当Eden区满时,还存活的对象将被复制到其中一个Survivor区,当此Survivor区满时,此区存活的对象又被复制到另外一个Survivor区,当这个Survivor区也满时,会将其中存活的对象复制到年老代

二、Old Generation

通常状况下,年老代中的对象生命周期都比较长

三、Permanent Generation

用于存放静态的类和方法,持久代对垃圾回收没有显著影响。(在 JDK 1.8 及以后的版本,在本地内存中实现的元空间(Meta-space)已经代替了永久代)

四、内存对象的处理过程小结

  • 一、对象建立后在Eden区
  • 二、执行GC后,若是对象仍然存活,则复制到S0区
  • 三、当S0区满时,该区域存活对象将复制到S1区,而后S0清空,接下来S0和S1角色互换
  • 四、当第3步达到必定次数(系统版本不一样会有差别)后,存活对象将被复制到Old Generation
  • 五、当这个对象在Old Generation区域停留的时间达到必定程度时,它会被移动到Old Generation,最后累积必定时间再移动到Permanent Generation区域

系统在Young Generation、Old Generation上采用不一样的回收机制。每个Generation的内存区域都有固定的大小。随着新的对象陆续被分配到此区域,当对象总的大小临近这一级别内存区域的阈值时,会触发GC操做,以便腾出空间来存放其余新的对象

此外,执行GC占用的时间与Generation和Generation中的对象数量有关,以下所示:

  • Young Generation < Old Generation < Permanent Generation
  • Generation中的对象数量与执行时间成反比

五、Young Generation GC

因为其对象存活时间短,所以基于Copying算法(扫描出存活的对象,并复制到一块新的彻底未使用的控件中)来回收。新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在Young Generation区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC

六、Old Generation GC

因为其对象存活时间较长,比较稳定,所以采用Mark(标记)算法(扫描出存活的对象,而后再回收未被标记的对象,回收后对空出的空间要么合并,要么标记出来便于下次分配,以减小内存碎片带来的效率损耗)来回收。

四、GC类型

在Android系统中,GC有三种类型:

  • kGcCauseForAlloc:分配内存不够引发的GC,会Stop World。因为是并发GC,其它线程都会中止,直到GC完成。
  • kGcCauseBackground:内存达到必定阈值触发的GC,因为是一个后台GC,因此不会引发Stop World。
  • kGcCauseExplicit:显示调用时进行的GC,当ART打开这个选项时,使用System.gc时会进行GC。

接下来,咱们来学会如何分析Android虚拟机中的GC日志,日志以下:

D/dalvikvm(7030):GC_CONCURRENT freed 1049K, 60% free 2341K/9351K, external 3502K/6261K, paused 3ms 3ms
复制代码

GC_CONCURRENT 是当前GC时的类型,GC日志中有如下几种类型:

  • GC_CONCURRENT:当应用程序中的Heap内存占用上升时(分配对象大小超过384k),避免Heap内存满了而触发的GC。若是发现有大量的GC_CONCURRENT出现,说明应用中可能一直有大于384k的对象被分配,而这通常都是一些临时对象被反复建立,多是对象复用不够所致使的
  • GC_FOR_MALLOC:这是因为Concurrent GC没有及时执行完,而应用又须要分配更多的内存,这时不得不停下来进行Malloc GC。
  • GC_EXTERNAL_ALLOC:这是为external分配的内存执行的GC。
  • GC_HPROF_DUMP_HEAP:建立一个HPROF profile的时候执行。
  • GC_EXPLICIT:显示调用了System.GC()。(尽可能避免)

咱们再回到上面打印的日志:

  • freed 1049k:代表在此次GC中回收了多少内存。
  • 60% free 2341k/9351K:代表回收后60%的Heap可用,存活的对象大小为2341kb,heap大小是9351kb。
  • external 3502/6261K:是Native Memory的数据。存放Bitmap Pixel Data(位图数据)或者堆之外内存(NIO Direct Buffer)之类的数据。第一个值说明在Native Memory中已分配3502kb内存,第二个值是一个浮动的GC阈值,当分配内存达到这个值时,会触发一次GC。
  • paused 3ms 3ms:代表GC的暂停时间,若是是Concurrent GC,会看到两个时间,一个开始,一个结束,且时间很短,若是是其余类型的GC,极可能只会看到一个时间,且这个时间是相对比较长的。而且,越大的Heap Size在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致使的卡顿问题。

2、优化内存的意义

优化内存的意义不言而喻,总的来讲能够归结为以下四点:

  • 一、减小OOM,提升应用稳定性
  • 二、减小卡顿,提升应用流畅度
  • 三、减小内存占用,提升应用后台运行时的存活率
  • 四、减小异常发生和代码逻辑隐患

须要注意的是,出现OOM是由于内存溢出致使,这种状况不必定会发生在相对应的代码处,也不必定是出现OOM的代码使用内存有问题,而是恰好执行到这段代码。

3、避免内存泄漏

一、内存泄漏的定义

Android系统虚拟机的垃圾回收是经过虚拟机GC机制来实现的。GC会选择一些还存活的对象做为内存遍历的根节点GC Roots,经过对GC Roots的可达性来判断是否须要回收。内存泄漏就是在当前应用周期内再也不使用的对象被GC Roots引用,致使不能回收,使实际可以使用内存变小

二、使用MAT来查找内存泄漏

MAT工具能够帮助开发者定位致使内存泄漏的对象,以及发现大的内存对象,而后解决内存泄漏并经过优化内存对象,以达到减小内存消耗的目的。

使用步骤

一、在https://eclipse.org/mat/downloads.php下载MAT客户端。

二、从Android Studio进入Profile的Memory视图,选择须要分析的应用进程,对应用进行怀疑有内存问题的操做,结束操做后,主动GC几回,最后export dump文件。

三、由于Android Studio保存的是Android Dalvik/ART格式的.hprof文件,因此须要转换成J2SE HPROF格式才能被MAT识别和分析。Android SDK自带了一个转换工具在SDK的platform-tools下,其中转换语句为:

./hprof-conv file.hprof converted.hprof
复制代码

四、经过MAT打开转换后的HPROF文件。

MAT视图

在MAT窗口上,OverView是一个整体概览,显示整体的内存消耗状况和疑似问题。MAT提供了多种分析维度,其中Histogram、Dominator Tree、Top Consumers和Leak Suspects的分析维度是不一样的。下面分别介绍下它们,以下所示:

一、Histogram

列出内存中的全部实例类型对象和其个数以及大小,并在顶部的regex区域支持正则表达式查找。

二、Dominator Tree

列出最大的对象及其依赖存活的Object。相比Histogram,能更方便地看出引用关系

三、Top Consumers

经过图像列出最大的Object

四、Leak Suspects

经过MAT自动分析内存泄漏的缘由和泄漏的一份整体报告

分析内存最经常使用的是Histogram和Dominator Tree这两个视图,视图中一共有四列:

  • Class Name:类名。
  • Objects:对象实例个数。
  • Shallow Heap:对象自身占用的内存大小,不包括它引用的对象。非数组的常规对象的Shallow Heap Size由其成员变量的数量和类型决定,数组的Shallow Heap Size由数组元素的类型(对象类型、基本类型)和数组长度决定。真正的内存都在堆上,看起来是一堆原生的byte[]、char[]、int[],对象自己的内存都很小。所以Shallow Heap对分析内存泄漏意义不是很大
  • Retained Heap:是当前对象大小与当前对象可直接或间接引用到的对象的大小总和,包括被递归释放的。即:Retained Size就是当前对象被GC后,从Heap上总共能释放掉的内存大小。

查找内存泄漏具体位置

常规方式

  • 一、按照包名类型分类进行实例筛选或直接使用顶部Regex选取特定实例。
  • 二、右击选中被怀疑的实例对象,选择Merge Shortest Paths to GC Root->exclude all phantom/weak/soft etc references。(显示GC Roots最短路径的强引用)
  • 三、分析引用链或经过代码逻辑找出缘由。

还有一种更快速的方法就是对比泄漏先后的HPROF数据:

  • 一、在两个HPROF文件中,把Histogram或者Dominator Tree增长到Compare Basket。
  • 二、在Compare Basket中单击 ! ,生成对比结果视图。这样就能够对比相同的对象在不一样阶段的对象实例个数和内存占用大小,如明显只须要一个实例的对象,或者不该该增长的对象实例个数却增长了,说明发生了内存泄漏,就须要去代码中定位具体的缘由并解决。

须要注意的是,若是目标不太明确,能够直接定位RetainedHeap最大的Object,经过Select incoming references查看引用链,定位到可疑的对象,而后经过Path to GC Roots分析引用链

此外,咱们知道,当Hash集合中过多的对象返回相同的Hash值时,会严重影响性能,这时能够用 Map Collision Ratio 查找致使Hash集合的碰撞率较高的罪魁祸首

高效方式

在本人平时的项目开发中,通常会使用以下几种方式来快速对指定页面进行内存泄漏的检测(也称为运行时内存分析优化):

  • 一、shell命令 + LeakCanary + MAT:运行程序,全部功能跑一遍,确保没有改出问题,彻底退出程序,手动触发GC,而后使用adb shell dumpsys meminfo packagename -d命令查看退出界面后Objects下的Views和Activities数目是否为0,若是不是则经过LeakCanary检查可能存在内存泄露的地方,最后经过MAT分析,如此反复,改善满意为止。

  • 二、Profile MEMORY:运行程序,对每个页面进行内存分析检查。首先,反复打开关闭页面5次,而后收到GC(点击Profile MEMORY左上角的垃圾桶图标),若是此时total内存尚未恢复到以前的数值,则可能发生了内存泄露。此时,再点击Profile MEMORY左上角的垃圾桶图标旁的heap dump按钮查看当前的内存堆栈状况,选择按包名查找,找到当前测试的Activity,若是引用了多个实例,则代表发生了内存泄露。

  • 三、从首页开始用依次dump出每一个页面的内存快照文件,而后利用MAT的对比功能,找出每一个页面相对于上个页面内存里主要增长了哪些东西,作针对性优化。

  • 四、利用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能够回收,不然仍是会内存泄漏。

六、Handler临时性内存泄漏

Message发出以后存储在MessageQueue中,在Message中存在一个target,它是Handler的一个引用,Message在Queue中存在的时间过长,就会致使Handler没法被回收。若是Handler是非静态的,则会致使Activity或者Service不会被回收。而且消息队列是在一个Looper线程中不断地轮询处理消息,当这个Activity退出时,消息队列中还有未处理的消息或者正在处理的消息,而且消息队列中的Message持有Handler实例的引用,Handler又持有Activity的引用,因此致使该Activity的内存资源没法及时回收,引起内存泄漏。解决方案以下所示:

  • 一、使用一个静态Handler内部类,而后对Handler持有的对象(通常是Activity)使用弱引用,这样在回收时,也能够回收Handler持有的对象。
  • 二、在Activity的Destroy或者Stop时,应该移除消息队列中的消息,避免Looper线程的消息队列中有待处理的消息须要处理。

须要注意的是,AsyncTask内部也是Handler机制,一样存在内存泄漏风险,但其通常是临时性的。对于相似AsyncTask或是线程形成的内存泄漏,咱们也能够将AsyncTask和Runnable类独立出来或者使用静态内部类。

七、容器中的对象没清理形成的内存泄漏

在退出程序以前,将集合里的东西clear,而后置为null,再退出程序

八、WebView

WebView都存在内存泄漏的问题,在应用中只要使用一次WebView,内存就不会被释放掉。咱们能够为WebView开启一个独立的进程,使用AIDL与应用的主进程进行通讯,WebView所在的进程能够根据业务的须要选择合适的时机进行销毁,达到正常释放内存的目的。

九、使用ListView时形成的内存泄漏

在构造Adapter时,使用缓存的convertView。

四、内存泄漏监控

通常使用LeakCanary进行内存泄漏的监控便可,具体使用和原理分析请参见我以前的文章Android主流三方库源码分析(6、深刻理解Leakcanary源码)

除了基本使用外,咱们还能够自定义处理结果,首先,继承DisplayLeakService实现一个自定义的监控处理Service,代码以下:

public class LeakCnaryService extends DisplayLeakServcie {
    
    private final String TAG = “LeakCanaryService”;
    
    @Override
    protected void afterDefaultHandling(HeapDump heapDump, AnalysisResult result, String leakInfo) {
        ...
    }
}
复制代码

重写 afterDefaultHanding 方法,在其中处理须要的数据,三个参数的定义以下:

  • heapDump:堆内存文件,能够拿到完成的hprof文件,以使用MAT分析。
  • result:监控到的内存状态,如是否泄漏等。
  • leakInfo:leak trace详细信息,除了内存泄漏对象,还有设备信息。

而后在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中注册。

4、优化内存空间

一、对象引用

从Java 1.2版本开始引入了三种对象引用方式:SoftReference、WeakReference 和 PhantomReference 三个引用类,引用类的主要功能就是可以引用但仍能够被垃圾回收器回收的对象。在引入引用类以前,只能使用Strong Reference,若是没有指定对象引用类型,默认是强引用。下面,咱们就分别来介绍下这几种引用。

一、强引用

若是一个对象具备强引用,GC就绝对不会回收它。当内存空间不足时,JVM会抛出OOM错误。

二、软引用

若是一个对象只具备软引用,则内存空间足够,GC时就不会回收它;若是内存不足,就会回收这些对象的内存。可用来实现内存敏感的高速缓存。

软引用能够和一个ReferenceQueue(引用队列)联合使用,若是软引用引用的对象被垃圾回收器回收,JVM会把这个软引用加入与之关联的引用队列中。

三、弱引用

在垃圾回收器线程扫描它所管辖的内存区域的过程当中,一旦发现了只具备弱引用的对象,无论当前内存空间是否足够,都会回收它的内存。不过,因为垃圾回收器是一个优先级很低的线程,所以不必定会很快发现那些只具备弱引用的对象。

这里要注意,可能须要运行屡次GC,才能找到并释放弱引用对象。

四、虚引用

只能用于跟踪即将对被引用对象进行的收集。虚拟机必须与ReferenceQueue类联合使用。由于它可以充当通知机制。

二、减小没必要要的内存开销

一、AutoBoxing

自动装箱的核心就是把基础数据类型转换成对应的复杂类型。在自动装箱转化时,都会产生一个新的对象,这样就会产生更多的内存和性能开销。如int只占4字节,而Integer对象有16字节,特别是HashMap这类容器,进行增、删、改、查操做时,都会产生大量的自动装箱操做。

检测方式

使用TraceView查看耗时,若是发现调用了大量的integer.value,就说明发生了AutoBoxing。

二、内存复用

对于内存复用,有以下四种可行的方式:

  • 资源复用:通用的字符串、颜色定义、简单页面布局的复用。
  • 视图复用:可使用ViewHolder实现ConvertView复用。
  • 对象池:显示建立对象池,实现复用逻辑,对相同的类型数据使用同一块内存空间。
  • Bitmap对象的复用:使用inBitmap属性能够告知Bitmap解码器尝试使用已经存在的内存区域,新解码的bitmap会尝试使用以前那张bitmap在heap中占据的pixel data内存区域。

三、使用最优的数据类型

一、HashMap与ArrayMap

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避免 掉了 基本数据类型转换成对象数据类型的时间

二、使用 IntDef和StringDef 替代枚举类型

使用枚举类型的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'
复制代码

三、LruCache

最近最少使用缓存,使用强引用保存须要缓存的对象,它内部维护了一个由LinkedHashMap组成的双向列表,不支持线程安全,LruCache对它进行了封装,添加了线程安全操做。当其中的一个值被访问时,它被放到队列的尾部,当缓存将满时,队列头部的值(最近最少被访问的)被丢弃,以后能够被GC回收。

除了普通的get/set方法以外,还有sizeOf方法,它用来返回每一个缓存对象的大小。此外,还有entryRemoved方法,当一个缓存对象被丢弃时调用的方法,当第一个参数为true:代表环处对象是为了腾出空间而被清理时。不然,代表缓存对象的entry被remove移除或者被put覆盖时。

注意

分配LruCache大小时应考虑应用剩余内存有多大。

四、图片内存优化

在Android默认状况下,当图片文件解码成位图时,会被处理成32bit/像素。红色、绿色、蓝色和透明通道各8bit,即便是没有透明通道的图片,如JEPG隔世是没有透明通道的,但而后会处理成32bit位图,这样分配的32bit中的8bit透明通道数据是没有任何用处的,这彻底没有必要,而且在这些图片被屏幕渲染以前,它们首先要被做为纹理传送到GPU,这意味着每一张图片会同时占用CPU内存和GPU内存。下面,我总结了减小内存开销的几种经常使用方式,以下所示:

一、设置位图的规格:当显示小图片或对图片质量要求不高时能够考虑使用RGB_565,用户头像或圆角图片通常能够尝试ARGB_4444。经过设置inPreferredConfig参数来实现不一样的位图规格,代码以下所示:

BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
BitmapFactory.decodeStream(is, null, options);
复制代码

二、inSampleSize:位图功能对象中的inSampleSize属性实现了位图的缩放功能,代码以下所示:

BitampFactory.Options options = new BitmapFactory.Options();
// 设置为4就是宽和高都变为原来1/4大小的图片
options.inSampleSize = 4;
BitmapFactory.decodeSream(is, null, options);
复制代码

三、inScaled,inDensity和inTargetDensity实现更细的缩放图片:当inScaled设置为true时,系统会按照现有的密度来划分目标密度,代码以下所示:

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);
复制代码

五、inBitmap

能够结合LruCache来实现,在LruCache移除超出cache size的图片时,暂时缓存Bitamp到一个软引用集合,须要建立新的Bitamp时,能够从这个软用用集合中找到最适合重用的Bitmap,来重用它的内存区域。

须要注意,新申请的Bitmap与旧的Bitmap必须有相同的解码格式,而且在Android 4.4以前,只能重用相同大小的Bitamp的内存区域,而Android 4.4以后能够重用任何bitmap的内存区域。

六、图片放置优化

只须要UI提供一套高分辨率的图,图片建议放在drawable-xxhdpi文件夹下,这样在低分辨率设备中图片的大小只是压缩,不会存在内存增大的状况。如若遇到不需缩放的文件,放在drawable-nodpi文件夹下。

七、在App可用内存太低时主动释放内存

在App退到后台内存紧张即将被Kill掉时选择重写 onTrimMemory/onLowMemory 方法去释放掉图片缓存、静态缓存来自保。

八、item被回收不可见时释放掉对图片的引用

  • ListView:所以每次item被回收后再次利用都会从新绑定数据,只需在ImageView onDetachFromWindow的时候释放掉图片引用便可。
  • RecyclerView:由于被回收不可见时第一选择是放进mCacheView中,这里item被复用并不会只需bindViewHolder来从新绑定数据,只有被回收进mRecyclePool中后拿出来复用才会从新绑定数据,所以重写Recycler.Adapter中的onViewRecycled()方法来使item被回收进RecyclePool的时候去释放图片引用。

九、避免创做没必要要的对象

例如,咱们能够在字符串拼接的时候使用StringBuffer,StringBuilder。

十、自定义View中的内存优化

例如,在onDraw方法里面不要执行对象的建立,通常来讲,都应该在自定义View的构造器中建立对象。

十一、其它的内存优化注意事项

除了上面的一些内存优化点以外,这里还有一些内存优化的点咱们须要注意,以下所示:

  • 尽使用static final 优化成员变量。
  • 使用加强型for循环语法。
  • 在没有特殊缘由的状况下,尽可能使用基本数据类型来代替封装数据类型,int比Integer要更加有效,其它数据类型也是同样。
  • 在合适的时候适当采用软引用和弱引用。
  • 采用内存缓存和磁盘缓存。
  • 尽可能采用静态内部类,可避免潜在因为内部类致使的内存泄漏。

5、图片管理模块的设计与实现

在设计一个模块时,须要考虑如下几点:

  • 一、单一职责
  • 二、避免不一样功能之间的耦合
  • 三、接口隔离

在编写代码前先画好UML图肯定每个对象、方法、接口的功能,首先尽可能作到功能单一原则,在这个基础上,再明确模块与模块的直接关系,最后使用代码实现。

一、实现异步加载功能

1.实现网络图片显示

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<Void, Void, Bitmap> {

        private String mUrl;
        private final WeakReference<ImageView> imageViewWeakReference;

        public BitmapLoadTask(String url, ImageView imageView) {
            mUrl = url;
            imageViewWeakReference = new WeakReference<ImageView>(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);
        }
    
}
复制代码

2 单个图片内存优化

这里使用一个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);
复制代码

优化后仍存在一些问题:

  • 1.相同的图片,每次都要从新加载;
  • 2.总体内存开销不可控,虽然减小了单个图片开销,可是在片很是多的状况下,没有合理管理机制仍然对性能有严重影的。

为了解决这两个问题,就须要有内存池的设计理念,经过内存池控制总体图片内存,不从新加载和解码已经显示过的图片。

二、实现三级缓存

内存--本地--网络

一、内存缓存

使用软引用和弱引用(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<String, Bitmap> 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<String, Bitmap>(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百分比占比多少合适?能够基于如下几点来考虑:

  • 1.应用中内存的占用状况,除了图片之外,是否还有大内存的数据须要缓存到内存。
  • 2.在应用中大部分状况要同时显示多少张图片,优先保证最大图片的显示数量的缓存支持。
  • 3.Bitmap的规格,计算出一张图片占用的内存大小。
  • 4.图片访问的频率。

在应用中,若是有一些图片的访问频率要比其它的大一些,或者必须一直显示出来,就须要一直保持在内存中,这种状况可使用多个LruCache对象来管理多组Bitmap,对Bitmap进行分级,不一样级别的Bitmap放到不一样的LruCache中。

二、bitmap内存复用

从Android3.0开始Bitmap支持内存复用,也就是BitmapFactoy.Options.inBitmap属性,若是这个属性被设置有效的目标用对象,decode方法就在加载内容时重用已经存在的bitmap,这意味着Bitmap的内存被从新利用,这能够减小内存的分配回收,提升图片的性能。代码以下所示:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        mReusableBitmaps = Collections.synchronizedSet(newHashSet<SoftReference<Bitmap>>());
}
复制代码

由于inBitmap属性在Android3.0之后才支持,在entryRemoved方法中加入软引用集合,做为复用的源对象,以前是直接删除,代码以下所示:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
    mReusableBitmaps.add(new SoftReference<Bitmap>(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<SoftReference<Bitmap>> 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提供的一个管理磁盘缓存的类。

一、首先调用DiskLruCache的open方法进行初始化,代码以下:

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机制来避免同步问题。

二、写入DiskLruCache

首先,获取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主流三方库源码分析(3、深刻理解Glide源码)。Fresco最大的亮点在于它的内存管理,特别是在低端机和Android 5.0如下的机器上的优点更加明显,而使用Fresco将很好地解决图片占用内存大的问题。由于,Fresco会将图片放到一个特别的内存区域,当图片再也不显示时,占用的内存会自动释放。这类总结下Fresco的优势,以下所示:

  • 一、内存管理
  • 二、渐进式呈现:先呈现大体的图片轮廓,而后随着图片下载的继续,呈现逐渐清晰的图片。
  • 三、支持更多的图片格式:如Gif和Webp。
  • 四、图像加载策略丰富:其中的Image Pipeline能够为同一个图片指定不一样的远程路径,好比先显示已经存在本地缓存中的图片,等高清图下载完成以后在显示高清图集。

缺点

安装包过大,因此对图片加载和显示要求不是比较高的状况下建议使用Glide。

6、总结

对于内存优化,通常都是经过使用MAT等工具来进行检查和使用LeakCanary等内存泄漏监控工具来进行监控,以此来发现问题,再分析问题缘由,解决发现的问题或者对当前的实现逻辑进行优化优化完后再进行检查,直到达到预约的性能指标。下一篇文章,将会和你们一块儿来深刻探索Android的内存优化,尽请期待~

参考连接:

一、Android应用性能优化最佳实践

二、必知必会 | Android 性能优化的方面方面都在这儿

三、实践App内存优化:如何有序地作内存分析与优化

Contanct Me

● 微信:

欢迎关注个人微信:bcce5360

● 微信群:

微信群若是不能扫码加入,麻烦你们想进微信群的朋友们,加我微信拉你进群。

● QQ群:

2千人QQ群,Awesome-Android学习交流群,QQ群号:959936182, 欢迎你们加入~

About me

很感谢您阅读这篇文章,但愿您能将它分享给您的朋友或技术群,这对我意义重大。

但愿咱们能成为朋友,在 Github掘金上一块儿分享知识。

相关文章
相关标签/搜索