前言
成为一名优良的Android开发,须要一份齐备的常识体系,在这里,让咱们一起成长为本人所想的那样~。
`
Tips:本篇是《深刻摸索Android内存优化》的根底篇,如果没有把握Android内存优化的同学倡议零碎学习一遍。
`
家喻户晓,内存优化能够说是性能优化中最重要的优化点之一,能够说,如果你没有把握零碎的内存优化计划,就不能说你对Android的性能优化有过多的钻研与摸索。本篇,笔者将率领大家一起来系统地学习Android中的内存优化。
可能有不少读者都晓得,在内存治理上,JVM领有垃圾内存回收的机制,本身会在虚拟机层面主动调配和开释内存,因而不须要像应用C/C++一样在代码中调配和开释某一块内存。Android零碎的内存治理相似于JVM,通过new关键字来为对象分配内存,内存的开释由GC来回收。并且Android零碎在内存治理上有一个 Generational Heap Memory模型,当内存达到某一个阈值时,零碎会依据不同的规定主动开释能够开释的内存。即使有了内存管理机制,然而,如果不合理地应用内存,也会造成一系列的性能问题,比方 内存透露、内存抖动、短时间内调配大量的内存对象 等等。上面,我就先来谈谈Android的内存管理机制。
一、Android 内存管理机制
咱们都晓得,应用程序的内存调配和垃圾回收都是由Android虚拟机实现的,在Android 5.0以下,应用的是Dalvik虚拟机,5.0及以上,则应用的是ART虚拟机。
1、Java 对象生命周期
Java代码编译后生成的字节码.class文件从文件系统中加载到虚拟机之后,便有了JVM上的Java对象,Java对象在JVM上运行有7个阶段,如下:
- Created
- InUse
- Invisible
- Unreachable
- Collected
- Finalized
- Deallocated
1、Created(创立)
Java对象的创立分为如下几步:
- 1、为对象调配存储空间。
- 2、结构对象。
- 3、从超类到子类对static成员进行初始化,类的static成员的初始化在ClassLoader加载该类时进行。
- 4、超类成员变量按程序初始化,递归调用超类的构造方法。
- 5、子类成员变量按程序初始化,一旦对象被创立,子类构造方法就调用该对象并为某些变量赋值。
2、InUse(利用)
此时对象至多被一个强援用持有。
3、Invisible(不可见)
当一个对象处于不可见阶段时,阐明程序自身不再持有该对象的任何强援用,尽管该对象依然是存在的。简略的例子就是程序的执行曾经超出了该对象的作用域了。然而,该对象仍可能被虚拟机下的某些已装载的动态变量线程或JNI等强援用持有,这些非凡的强援用称为“GC Root”。被这些GC Root强援用的对象会导致该对象的内存透露,因此无奈被GC回收。
4、Unreachable(不可达)
该对象不再被任何强援用持有。
5、Collected(收集)
当GC曾经对该对象的内存空间重新分配做好筹备时,对象进入收集阶段,如果该对象重写了finalize()办法,则执行它。
6、Finalized(终结)
期待垃圾回收器回收该对象空间。
7、Deallocated(对象空间重新分配)
GC对该对象所占用的内存空间进行回收或者再调配,则该对象彻底隐没。
留神
- 1、不须要应用该对象时,及时置空。
- 2、拜访本地变量优于拜访类中的变量。
2、Java 内存调配模型
JVM 将整个内存划分为了几块,别离如下所示:
- 1)、办法区:存储类信息、常量、动态变量等。=> 所有线程共享
- 2)、虚拟机栈:存储局部变量表、操作数栈等。
- 3)、本地办法栈:不同与虚拟机栈为 Java 办法服务、它是为 Native 办法服务的。
- 4)、堆:内存最大的区域,每一个对象理论分配内存都是在堆上进行调配的,,而在虚拟机栈中调配的只是援用,这些援用会指向堆中真正存储的对象。此外,堆也是垃圾回收器(GC)所次要作用的区域,并且,内存透露也都是产生在这个区域。=> 所有线程共享
- 5)、程序计数器:存储以后线程执行指标办法执行到了第几行。
3、Android 内存调配模型
在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的对象须要在零碎每次启动时,依据运行状况都从新创立一遍。
4、Java 内存回收算法
1) 标记-革除算法
实现原理
- 标记出所有须要回收的对象。
- 对立回收所有被标记的对象。
特点
- 标记和革除效率不高。
- 产生大量不间断的内存碎片。
2) 复制算法
实现原理
- 将内存划分为大小相等的两块。
- 一块内存用完之后复制存活对象至另一块。
- 清理另一块内存。
特点
- 实现简略,运行高效。
- 节约一半空间,代价大。
3) 标记-整顿算法
实现原理
- 标记过程与 ”标记-革除“ 算法一样。
- 存活对象往一端进行挪动。
- 清理其余内存。
特点
- 防止 ”标记-革除” 算法导致的内存碎片。
- 防止复制算法的空间节约。
4) 分代收集算法(大多数虚拟机厂商所选用的算法)
特点
- 联合多种收集算法的劣势。
- 新生代对象存活率低 => “复制” 算法(留神这里每一次的复制比例都是能够调整的,如一次仅复制 30% 的存活对象)。
- 老年代对象存活率高 => “标记-整顿” 算法。
5、Android 内存回收机制
对于 Android 设施来说,咱们每关上一个 APP,它的内存都是弹性调配的,并且其调配值与最大值是受具体设施而定的。
此外,咱们须要留神辨别如下两种 OOM 场景:
- 1)、内存真正有余:例如 APP 以后过程最大内存下限为 512 MB,当超过这个值就表明内存真正有余了。
- 2)、可用内存有余:手机零碎内存极度缓和,就算 APP 以后过程最大内存下限为 512 MB,咱们只调配了 200 MB,也会产生内存溢出,因为零碎的可用内存有余了。
在Android的高级零碎版本中,针对Heap空间有一个Generational Heap Memory的模型,其中将整个内存分为三个区域:
- Young Generation(年老代)
- Old Generation(年轻代)
- Permanent Generation(长久代)
模型示意图如下所示:
1、Young Generation
由一个Eden区和两个Survivor区组成,程序中生成的大部分新的对象都在Eden区中,当Eden区满时,还存活的对象将被复制到其中一个Survivor区,当此Survivor区满时,此区存活的对象又被复制到另一个Survivor区,当这个Survivor区也满时,会将其中存活的对象复制到年轻代。
2、Old Generation
个别状况下,年轻代中的对象生命周期都比拟长。
3、Permanent Generation
用于寄存动态的类和办法,长久代对垃圾回收没有显著影响。(在 JDK 1.8 及之后的版本,在本地内存中实现的元空间(Meta-space)曾经代替了永恒代)
4、内存对象的处理过程小结
- 1、对象创立后在Eden区。
- 2、执行GC后,如果对象依然存活,则复制到S0区。
- 3、当S0区满时,该区域存活对象将复制到S1区,而后S0清空,接下来S0和S1角色调换。
- 4、当第3步达到肯定次数(零碎版本不同会有差别)后,存活对象将被复制到Old Generation。
- 5、当这个对象在Old Generation区域停留的工夫达到肯定水平时,它会被挪动到Old Generation,最初累积肯定工夫再挪动到Permanent Generation区域。
零碎在Young Generation、Old Generation上采纳不同的回收机制。每一个Generation的内存区域都有固定的大小。随着新的对象陆续被调配到此区域,当对象总的大小邻近这一级别内存区域的阈值时,会触发GC操作,以便腾出空间来寄存其余新的对象。
此外,执行GC占用的工夫与Generation和Generation中的对象数量无关,如下所示:
- Young Generation < Old Generation < Permanent Generation
- Generation中的对象数量与执行工夫成反比。
5、Young Generation GC
因为其对象存活工夫短,因而基于Copying算法(扫描出存活的对象,并复制到一块新的齐全未应用的控件中)来回收。新生代采纳闲暇指针的形式来管制GC触发,指针放弃最初一个调配的对象在Young Generation区间的地位,当有新的对象要分配内存时,用于查看空间是否足够,不够就触发GC。
6、Old Generation GC
因为其对象存活工夫较长,比较稳定,因而采纳Mark(标记)算法(扫描出存活的对象,而后再回收未被标记的对象,回收后对空出的空间要么合并,要么标记进去便于下次调配,以缩小内存碎片带来的效率损耗)来回收。
7、Dalvik 与 ART 区别
- 1)、Dalivk 仅固定一种回收算法。
- 2)、ART 回收算法可运行期抉择。
- 3)、ART 具备内存整理能力,缩小内存空洞。
6、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导致的卡顿问题。
7、Low Memory Killer 机制
LMK 机制是针对于手机零碎所有过程而制订的,当咱们手机内存不足的状况下,LMK 机制就会针对咱们所有过程进行回收,而其对于不同的过程,它的回收力度也是有不同的,目前零碎的过程类型次要有如下几种:
- 1)、前台过程
- 2)、可见过程
- 3)、服务过程
- 4)、后盾过程
- 5)、空过程
从前台过程到空过程,过程优先级会越来越低,因而,它被 LMK 机制杀死的几率也会相应变大。此外,LMK 机制也会综合思考回收收益,这样就能保障咱们大多数过程不会呈现内存不足的状况。
二、优化内存的意义
优化内存的意义显而易见,总的来说能够归结为如下四点:
- 1、缩小OOM,进步利用稳定性。
- 2、缩小卡顿,进步利用晦涩度。
- 3、缩小内存占用,进步利用后盾运行时的存活率。
- 4、缩小异样产生和代码逻辑隐患。
须要留神的是,呈现OOM是因为内存溢出导致,这种状况不肯定会产生在绝对应的代码处,也不肯定是呈现OOM的代码应用内存有问题,而是刚好执行到这段代码。
三、防止内存透露
1、内存透露的定义
Android零碎虚拟机的垃圾回收是通过虚拟机GC机制来实现的。GC会抉择一些还存活的对象作为内存遍历的根节点GC Roots,通过对GC Roots的可达性来判断是否须要回收。内存透露就是在以后利用周期内不再应用的对象被GC Roots援用,导致不能回收,使理论可应用内存变小。
2、应用MAT来查找内存透露
MAT工具能够帮忙开发者定位导致内存透露的对象,以及发现大的内存对象,而后解决内存透露并通过优化内存对象,以达到缩小内存耗费的目标。
应用步骤
1、在eclipse.org/mat/downloa…
2、从Android Studio进入Profile的Memory视图,抉择须要剖析的利用过程,对利用进行狐疑有内存问题的操作,完结操作后,被动GC几次,最初export dump文件。
3、因为Android Studio保留的是Android Dalvik/ART格局的.hprof文件,所以须要转换成J2SE HPROF格局能力被MAT辨认和剖析。Android SDK自带了一个转换工具在SDK的platform-tools下,其中转换语句为:
./hprof-conv file.hprof converted.hprof
4、通过MAT关上转换后的HPROF文件。
MAT视图
在MAT窗口上,OverView是一个总体概览,显示总体的内存耗费状况和疑似问题。MAT提供了多种剖析维度,其中Histogram、Dominator Tree、Top Consumers和Leak Suspects的剖析维度是不同的。上面别离介绍下它们,如下所示:
1、Histogram
列出内存中的所有实例类型对象和其个数以及大小,并在顶部的regex区域反对正则表达式查找。
2、Dominator Tree
列出最大的对象及其依赖存活的Object。相比Histogram,能更不便地看出援用关系。
3、Top Consumers
通过图像列出最大的Object。
4、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上总共能开释掉的内存大小。
查找内存透露具体位置
惯例形式
- 1、依照包名类型分类进行实例筛选或间接应用顶部Regex选取特定实例。
- 2、右击选中被狐疑的实例对象,抉择Merge Shortest Paths to GC Root->exclude all phantom/weak/soft etc references。(显示GC Roots最短门路的强援用)
- 3、剖析援用链或通过代码逻辑找出起因。
还有一种更疾速的办法就是比照透露前后的HPROF数据:
- 1、在两个HPROF文件中,把Histogram或者Dominator Tree减少到Compare Basket。
- 2、在Compare Basket中单击 ! ,生成比照后果视图。这样就能够比照雷同的对象在不同阶段的对象实例个数和内存占用大小,如显著只须要一个实例的对象,或者不应该减少的对象实例个数却减少了,阐明产生了内存透露,就须要去代码中定位具体的起因并解决。
须要留神的是,如果指标不太明确,能够间接定位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出的内存快照进行剖析,看看有哪些地方是能够优化的,比方加载的图片、利用中全局的单例数据配置、动态内存与缓存、埋点数据、内存透露等等。
3、常见内存透露场景
对于内存透露,其本质可了解为无奈回收无用的对象。这里我总结了我在我的项目中遇到的一些常见的内存透露案例(蕴含解决方案)。
1、资源性对象未敞开
对于资源性对象不再应用时,应该立刻调用它的close()函数,将其敞开,而后再置为null。例如Bitmap等资源未敞开会造成内存透露,此时咱们应该在Activity销毁时及时敞开。
2、注册对象未登记
例如BraodcastReceiver、EventBus未登记造成的内存透露,咱们应该在Activity销毁时及时登记。
3、类的动态变量持有大数据对象
尽量避免应用动态变量存储数据,特地是大数据对象,倡议应用数据库存储。
4、单例造成的内存透露
优先应用Application的Context,如需应用Activity的Context,能够在传入Context时应用弱援用进行封装,而后,在应用到的中央从弱援用中获取Context,如果获取不到,则间接return即可。
5、非动态外部类的动态实例
该实例的生命周期和利用一样长,这就导致该动态实例始终持有该Activity的援用,Activity的内存资源不能失常回收。此时,咱们能够将该外部类设为动态外部类或将该外部类抽取进去封装成一个单例,如果须要应用Context,尽量应用Application Context,如果须要应用Activity Context,就记得用完后置空让GC能够回收,否则还是会内存透露。
6、Handler临时性内存透露
Message收回之后存储在MessageQueue中,在Message中存在一个target,它是Handler的一个援用,Message在Queue中存在的工夫过长,就会导致Handler无奈被回收。如果Handler是非动态的,则会导致Activity或者Service不会被回收。并且音讯队列是在一个Looper线程中一直地轮询解决音讯,当这个Activity退出时,音讯队列中还有未解决的音讯或者正在解决的音讯,并且音讯队列中的Message持有Handler实例的援用,Handler又持有Activity的援用,所以导致该Activity的内存资源无奈及时回收,引发内存透露。解决方案如下所示:
- 1、应用一个动态Handler外部类,而后对Handler持有的对象(个别是Activity)应用弱援用,这样在回收时,也能够回收Handler持有的对象。
- 2、在Activity的Destroy或者Stop时,应该移除音讯队列中的音讯,防止Looper线程的音讯队列中有待解决的音讯须要解决。
须要留神的是,AsyncTask外部也是Handler机制,同样存在内存透露危险,但其个别是临时性的。对于相似AsyncTask或是线程造成的内存透露,咱们也能够将AsyncTask和Runnable类独立进去或者应用动态外部类。
7、容器中的对象没清理造成的内存透露
在退出程序之前,将汇合里的货色clear,而后置为null,再退出程序
8、WebView
WebView都存在内存透露的问题,在利用中只有应用一次WebView,内存就不会被开释掉。咱们能够为WebView开启一个独立的过程,应用AIDL与利用的主过程进行通信,WebView所在的过程能够依据业务的须要抉择适合的机会进行销毁,达到失常开释内存的目标。
9、应用ListView时造成的内存透露
在结构Adapter时,应用缓存的convertView。
4、内存透露监控
个别应用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 办法,在其中解决须要的数据,三个参数的定义如下:
- 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中注册。
四、优化内存空间
1、对象援用
从Java 1.2版本开始引入了三种对象援用形式:SoftReference、WeakReference 和 PhantomReference 三个援用类,援用类的次要性能就是可能援用但仍能够被垃圾回收器回收的对象。在引入援用类之前,只能应用Strong Reference,如果没有指定对象援用类型,默认是强援用。上面,咱们就别离来介绍下这几种援用。
1、强援用
如果一个对象具备强援用,GC就相对不会回收它。当内存空间有余时,JVM会抛出OOM谬误。
2、软援用
如果一个对象只具备软援用,则内存空间足够,GC时就不会回收它;如果内存不足,就会回收这些对象的内存。可用来实现内存敏感的高速缓存。
软援用能够和一个ReferenceQueue(援用队列)联结应用,如果软援用援用的对象被垃圾回收器回收,JVM会把这个软援用退出与之关联的援用队列中。
3、弱援用
在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具备弱援用的对象,不论以后内存空间是否足够,都会回收它的内存。不过,因为垃圾回收器是一个优先级很低的线程,因而不肯定会很快发现那些只具备弱援用的对象。
这里要留神,可能须要运行屡次GC,能力找到并开释弱援用对象。
4、虚援用
只能用于跟踪行将对被援用对象进行的收集。虚拟机必须与ReferenceQueue类联结应用。因为它可能充当告诉机制。
2、缩小不必要的内存开销
1、AutoBoxing
主动装箱的外围就是把根底数据类型转换成对应的简单类型。在主动装箱转化时,都会产生一个新的对象,这样就会产生更多的内存和性能开销。如int只占4字节,而Integer对象有16字节,特地是HashMap这类容器,进行增、删、改、查操作时,都会产生大量的主动装箱操作。
检测形式
应用TraceView查看耗时,如果发现调用了大量的integer.value,就阐明产生了AutoBoxing。
2、内存复用
对于内存复用,有如下四种可行的形式:
- 资源复用:通用的字符串、色彩定义、简略页面布局的复用。
- 视图复用:能够应用ViewHolder实现ConvertView复用。
- 对象池:显示创建对象池,实现复用逻辑,对雷同的类型数据应用同一块内存空间。
- Bitmap对象的复用:应用inBitmap属性能够告知Bitmap解码器尝试应用曾经存在的内存区域,新解码的bitmap会尝试应用之前那张bitmap在heap中占据的pixel data内存区域。
3、应用最优的数据类型
1、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 就 防止 掉了 根本数据类型转换成对象数据类型的工夫。
2、应用 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'
3、LruCache
最近起码应用缓存,应用强援用保留须要缓存的对象,它外部保护了一个由LinkedHashMap组成的双向列表,不反对线程平安,LruCache对它进行了封装,增加了线程平安操作。当其中的一个值被拜访时,它被放到队列的尾部,当缓存将满时,队列头部的值(最近起码被拜访的)被抛弃,之后能够被GC回收。
除了一般的get/set办法之外,还有sizeOf办法,它用来返回每个缓存对象的大小。此外,还有entryRemoved办法,当一个缓存对象被抛弃时调用的办法,当第一个参数为true:表明缓存对象是为了腾出空间而被清理。否则,表明缓存对象的entry是被remove移除或者被put笼罩。
留神
调配LruCache大小时应思考利用残余内存有多大。
4、图片内存优化
在Android默认状况下,当图片文件解码成位图时,会被解决成32bit/像素。红色、绿色、蓝色和通明通道各8bit,即便是没有通明通道的图片,如JEPG隔世是没有通明通道的,但而后会解决成32bit位图,这样调配的32bit中的8bit通明通道数据是没有任何用途的,这齐全没有必要,并且在这些图片被屏幕渲染之前,它们首先要被作为纹理传送到GPU,这意味着每一张图片会同时占用CPU内存和GPU内存。上面,我总结了缩小内存开销的几种罕用形式,如下所示:
1、设置位图的规格:当显示小图片或对图片品质要求不高时能够思考应用RGB\_565,用户头像或圆角图片个别能够尝试ARGB\_4444。通过设置inPreferredConfig参数来实现不同的位图规格,代码如下所示:
BitmapFactory.Options options = new BitmapFactory.Options();options.inPreferredConfig = Bitmap.Config.RGB_565;BitmapFactory.decodeStream(is, null, options);
2、inSampleSize:位图性能对象中的inSampleSize属性实现了位图的缩放性能,代码如下所示:
BitampFactory.Options options = new BitmapFactory.Options();// 设置为4就是宽和高都变为原来1/4大小的图片options.inSampleSize = 4;BitmapFactory.decodeSream(is, null, options);
3、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);
5、inBitmap
能够联合LruCache来实现,在LruCache移除超出cache size的图片时,临时缓存Bitamp到一个软援用汇合,须要创立新的Bitamp时,能够从这个软援用汇合中找到最适宜重用的Bitmap,来重用它的内存区域。
须要留神,新申请的Bitmap与旧的Bitmap必须有雷同的解码格局,并且在Android 4.4之前,只能重用雷同大小的Bitamp的内存区域,而Android 4.4之后能够重用任何bitmap的内存区域。
6、图片搁置优化
只须要UI提供一套高分辨率的图,图片倡议放在drawable-xxhdpi文件夹下,这样在低分辨率设施中图片的大小只是压缩,不会存在内存增大的状况。如若遇到不需缩放的文件,放在drawable-nodpi文件夹下。
7、在App可用内存过低时被动开释内存
在App退到后盾内存缓和行将被Kill掉时抉择重写 onTrimMemory/onLowMemory 办法去开释掉图片缓存、动态缓存来自保。
8、item被回收不可见时开释掉对图片的援用
- ListView:因而每次item被回收后再次利用都会从新绑定数据,只需在ImageView onDetachFromWindow的时候开释掉图片援用即可。
- RecyclerView:因为被回收不可见时第一抉择是放进mCacheView中,这里item被复用并不会只需bindViewHolder来从新绑定数据,只有被回收进mRecyclePool中后拿进去复用才会从新绑定数据,因而重写Recycler.Adapter中的onViewRecycled()办法来使item被回收进RecyclePool的时候去开释图片援用。
9、防止创作不必要的对象
例如,咱们能够在字符串拼接的时候应用StringBuffer,StringBuilder。
10、自定义View中的内存优化
例如,在onDraw办法外面不要执行对象的创立,一般来说,都应该在自定义View的结构器中创建对象。
11、其它的内存优化注意事项
除了下面的一些内存优化点之外,这里还有一些内存优化的点咱们须要留神,如下所示:
- 尽应用static final 优化成员变量。
- 应用增强型for循环语法。
- 在没有非凡起因的状况下,尽量应用根本数据类型来代替封装数据类型,int比Integer要更加无效,其它数据类型也是一样。
- 在适合的时候适当采纳软援用和弱援用。
- 采纳内存缓存和磁盘缓存。
- 尽量采纳动态外部类,可防止潜在因为外部类导致的内存透露。
五、图片治理模块的设计与实现
在设计一个模块时,须要思考以下几点:
- 1、繁多职责
- 2、防止不同性能之间的耦合
- 3、接口隔离
在编写代码前先画好UML图,确定每一个对象、办法、接口的性能,首先尽量做到性能繁多准则,在这个根底上,再明确模块与模块的间接关系,最初应用代码实现。
1、实现异步加载性能
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.整体内存开销不可控,尽管缩小了单个图片开销,然而在片十分多的状况下,没有正当管理机制依然对性能有重大影的。
为了解决这两个问题,就须要有内存池的设计理念,通过内存池管制整体图片内存,不从新加载和解码曾经显示过的图片。
2、实现三级缓存
内存--本地--网络
1、内存缓存
应用软援用和弱援用(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中。
2、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();}
3、磁盘缓存
因为磁盘读取工夫是不可预知的,所以图片的解码和文件读取都应该在后盾过程中实现。DisLruCache是Android提供的一个治理磁盘缓存的类。
1、首先调用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机制来防止同步问题。
2、写入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办法中调用,代码如下所示:
@Overrideprotected 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;}
3、图片加载三方库
目前应用最宽泛的有Picasso、Glide和Fresco。Glide和Picasso比拟类似,然而Glide绝对于Picasso来说,性能更丰盛,外部实现更简单,对Glide有趣味的同学能够浏览这篇文章Android支流三方库源码剖析(三、深刻了解Glide源码)。Fresco最大的亮点在于它的内存治理,特地是在低端机和Android 5.0以下的机器上的劣势更加显著,而应用Fresco将很好地解决图片占用内存大的问题。因为,Fresco会将图片放到一个特地的内存区域,当图片不再显示时,占用的内存会主动开释。这类总结下Fresco的长处,如下所示:
- 1、内存治理。
- 2、渐进式出现:先出现大抵的图片轮廓,而后随着图片下载的持续,出现逐步清晰的图片。
- 3、反对更多的图片格式:如Gif和Webp。
- 4、图像加载策略丰盛:其中的Image Pipeline能够为同一个图片指定不同的近程门路,比方先显示曾经存在本地缓存中的图片,等高清图下载实现之后在显示高清图集。
毛病
安装包过大,所以对图片加载和显示要求不是比拟高的状况下倡议应用Glide。
六、总结
对于内存优化,个别都是通过应用MAT等工具来进行检查和应用LeakCanary等内存透露监控工具来进行监控,以此来发现问题,再剖析问题起因,解决发现的问题或者对以后的实现逻辑进行优化,优化完后再进行查看,直到达到预约的性能指标。下一篇文章,将会和大家一起来深刻摸索Android的内存优化,尽请期待~
很感谢您浏览这篇文章,心愿您能将它分享给您的敌人或技术群,这对我意义重大。
本文转自 https://juejin.cn/post/6844904096541966350,如有侵权,请分割删除。
相干视频举荐:
Android 性能优化学习【一】:APK瘦身优化_哔哩哔哩_bilibili
Android 性能优化学习【二】:APP启动速度优化_哔哩哔哩_bilibili
Android 性能优化学习【三】:如何解决OOM问题_哔哩哔哩_bilibili
Android 性能优化学习【四】:UI卡顿优化_哔哩哔哩_bilibili