关于android:Android性能优化之内存优化

58次阅读

共计 31436 个字符,预计需要花费 79 分钟才能阅读完成。

前言

成为一名优良的 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 办法中调用,代码如下所示:

@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;
}

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

正文完
 0