Android性能优化——内存优化

Android分配个应用的大小是有限制,且在设备出厂之后已经确定,单个应用可用的最大内存的配置位于/system/build.prop文件中的dalvik.vm.heapgrowthlimit配置项。
虽然Android使用的JVM具有内存管理(自动回收)的能力,但是对内存使用不当会导致应用出现异常,包括常见的OOM、内存泄漏、内存抖动等引发的崩溃、卡顿等现象。我们一般主要针对这三种内存问题进行优化处理:

  • OOM
    Out of memory, 内存溢出,当应用申请内存发现超出了JVM的最大限制时候,就会抛出内存溢出异常,引发程序崩溃。引发OOM常见原因有:内存泄漏的累积导致无法申请更多内存、创建大内存对象(如大容量数组、载入大的文件、载入大的图片等)。
  • 内存泄漏
    一个对象的超出了其生命周期,导致JVM无法回收。这样无法回收的对象堆积多了会导致应用可能无法申请到内存进而导致OOM。常见的内存泄漏有:
    • 单例持引起的内存泄漏,如单例持有activity、context、view、drawabl等
    • 静态变量引起的内存泄漏,如静态变量持有activity、context、view、drawabl等
    • 非静态内部类引起的内存泄漏,原因:非静态内部类会隐式持有外部类实例
    • 匿名内部类引起的内存泄漏,如handler、线程匿名内部类runnable、callback等
    • 资源未释放引起的内存泄漏,如读写文件没有关闭、网络流操作没有关闭、Bitmap没有释放等
    • 广播没有及时取消注册
  • 内存抖动
    内存抖动是因为在频繁的创建、回收对象,引发的频繁GC,进而影响主线程,最终导致卡顿现象。

要知道怎么正确的使用内存,首先需要了解java虚拟机(即JVM)的内存管理机制。

JVM内存管理机制

JVM内存管理机制是通过根搜索算法,在合适的时期检索对象是否可达,当对象不可达时会被回收,如果对象可达则不会被回收。

对象的生命周期

一个对象从创建到销毁回收是其生命周期的表现,在开发阶段我们是可以预测到对象的生命周期范围,什么时候创建什么时候回收,如果没有被正常回收就会引发对象不可被回收导致的内存泄漏。

哪些对象需要回收

使用根搜索算法GC Root Trace通过一系列名为GC Root的对象作为起点,向下搜索,搜索所经过的路径称为引用链,当一个对象到GC Root没有应用链相连,则表明此对象需要回收。以下是可以作为GC Root的对象:

  • 全局静态变量引用的对象
  • 全局常量引用的对象
  • 虚拟机栈帧中本地变量表中引用的对象
  • 本地方法栈帧中本地变量表中引用的对象
什么时候回收内存

介绍什么时候回收内存前,先介绍下Android虚拟机的堆块的管理情况,Android虚拟机遵循java虚拟机堆内存分代管理的机制,主要划分为:新生代、老年代,其中新生代又分为1个Eden(新生代)和2个survisor(Eden幸存的对象),eden、survisor内存默认按8:1:1,因为很多对象创建使用过后就会回收,真正存活下来的不会很多,所以给eden分配80%的占比可以有效提升内存使用率,每次使用eden和1个survisor,回收时将存活的复制到另一个survisor中,然后清空eden和survisor,什么时候会回收内存呢?一般是在各个内存分代区内存不足或者内存快满时会触发内存回收即GC。新生代触发的是Minor GC,因为新生代大多数是朝生夕灭,所以Minor GC比较频繁,但速度会比较快;当survisor中内存不足或者存活年龄达到一定在就会将相应的survisor中的对象复制到老年代中,老年代内存回收是Major GC/Full GC,一般都会伴随至少一次的Minor GC,Major GC速度相对比较慢,相比Minor GC可能会慢10倍。

怎么回收对象

新生代使用复制算法,将eden和1个survisor的内存复制到另一个survisor中,接着清空原先的eden和survisor;老年代使用标记—整理算法,即先标记要回收的对象,再把存活的对象移到一段,接着就是清理掉端边界以外的对象。

不论Minor GC还是Major GC在回收内存的时候都会阻塞其它的工作线程,等完成GC之后再恢复工作线程。

内存优化

上面讲述了虚拟机内存管理机制,对应内存的优化有以下建议:

防止内存泄漏
  • 避免全局静态变量持有资源对象:如activity、非applicationcontext、fragment、view等
  • 避免全局常量持有有资源对象:如activity、非applicationcontext、fragment、view等
  • 避免单例持有资源对象:如activity、非applicationcontext、fragment、view等
  • 对于内部类要么使用静态内部类+弱引用,要么使用弱引用
  • 对于匿名内部类使用弱引用引用外部引用
  • 资源使用完之后,及时释放:文件io、cursor、网络io用完之后及时释放
防止内存抖动
  • 避免创建大内存对象,如:大内存数组、加载大文件或者图片
  • 避免频繁创建对象,如:避免在for语句中创建大量对象
  • 需要频繁使用的对象,可以通过缓存池复用,避免重复创建、释放,在内存紧张OnTrimMemory /OnLowMemory 时适当释放可以释放的资源或者对象
  • 使用图片是可以时候565或者对图片进行裁剪、降低图片质量,也可以使用Glide,滑动时暂停加载图片,不滑动时恢复加载图片
  • 字符串相加或者拼接通过StringBuilder替代,较少创建String对象节省内存
  • 使用SpareArray、ArrayMap替代HashMap

内存分析

LeakCannary

项目中依赖LeakCannary库,使用LeakCannary可以检测内存泄漏

Memory Profiler

使用Android Studio的内存分析器可以对内存分析,根据分析结果进行相应的优化
如需打开内存分析器,按以下步骤操作:

  • 依次点击View——》Tools windows——》Profiler(或者点击工具栏中的Profiler图标)
  • 从Android Profiler工具栏中选择要分析的设备和应用进程
  • 点击MEMORY时间轴上的任意位置打开内存性能分析器

打开内存性能分析器后,其界面如下图所示:


memory_profiler.png

1、强制执行垃圾回收按钮
2、选择捕获堆转存heap dump的按钮
3、暂停/跳转到实时内存数据的按钮
4、事件时间轴:显示应用活动状态、用户输入事件(如touch的down/press等)、屏幕旋转事件等
5、内存分类用量统计

  • total: 总共分配的对象的内存大小
  • Java: java/kotlin代码分配的对象的内存大小
  • Native: c/c++分配的对象的内存大小
  • Graphics: 图片缓冲区队列向屏幕显示像素所使用的内存大小(这部分是CPU共享的内存,而不是GPU)
  • Stack: 应用java和原生堆栈使用的内存。
  • Code: 应用处理代码和资源的内存(包括处理dex字节码、so库、字体等)
  • others: 应用使用了系统不确定如何分类的内存大小
  • Allocated: 应用分配的java/kotlin对象数(不含c/c++分配的对象数)
    6、以图表、坐标轴的方式显示内存分配情况,x坐标显示的是时间、y轴左侧标记部分代表内存大小、y轴右侧标记部分代表分配对象数、图表部分代表各个类别分配的对象的内存大小
捕获堆转储

选择内存分析器中的Capture heap dump,点击下方的Record按钮,就开始捕获堆转储了,可以点击stop结束捕获,结束捕获之后会自动加载捕获到堆转储。下图是捕获到heap dump之后,打开的界面


heap_dump2.png

1、过滤器
这部分主要用于对heap dump的数据进行过滤,过滤我们关注、需要分享的部分,包括

  • 选择需要检查的堆类型:
    • view all heap:检查分配内存的所有堆
    • view app heap:默认,检查应用在使用时分配内存的主堆
    • view image heap: 系统启动映像,包括启动期间预加载的类
    • view zygote heap: 检查写时复制堆,这部分是应用通过zygote 创建启动进程时的堆

我们应用端一般主要分析view app heap进行分析主堆,排在java层面的内存问题

  • 选择如何安排分派
    • Arrang by class:默认,根据类名称对所有内存分配进行分组
    • Arrang by package: 根据包名对所有内存分配进行分组
    • Arrange by callstack: 根据调用堆栈对所有内存分配进行分组

一般采用采用Arrang by class过滤占用内存占比比较高的类进行分析,Arrang by package根据包名定位自己代码、三方代码的内存问题

  • 选择显示那些类型的数据
    • Show all class: 默认,显示所有的类
    • Show activity/fragment Leak: 显示发生内存泄漏的activity/fragment
    • Show project class: 进显示项目相关的类
  • 输入过滤:在输入框中可以输入类名/包名来快速定位到具体类/包名下类的内存分配情况

2、统计信息

  • classes: 类类型总数,不是实例对象哦
  • Leak:发生内存泄漏的数量
  • count: 总关创建的使用的实例对象数
  • Native Size: 原生c/c++使用的内存总量
  • Shallow Size: java使用的内存总量
  • Retained Size: 还在使用保留的内存总量

3、创建的对象数其分配内存情况
这部分会列举过滤之后的所有类名、分配的对象数及内存使用情况,包括

  • Class Name: 类名
  • Allocations: 此类创建的实例对象数量
  • Native Size: 此类总共使用的原生内存总量(只有android7.0+设备才能看到)(单位字节)
  • Shallow Size: 此类使用的java内存总量(单位字节)
  • Retained Size: 此类实例对象仍存活而保留的内存总大小(单位字节)

4、类实例对象列表及其实例对象的详细信息
在3中点击某一个类,会在下半部分显示此类的所有实例对象的信息,如点击图中的bitmap。
这部分左侧显示类的实例对象列表:实例对象+地址;点击某个实例会在右侧显示此实例内存分配的详细信息,包括:

  • Fields
    实例对象每个字段信息,包括如下信息:

    • Instance 此字段的名称及其类型,如果是基本数据类型和String会同时显示此字段的当前值
    • Depth: 此字段字段可达的最短跳数,表示的是任意一个GC Root到此字段的最短链路边数
    • Native Size: 原生内存中此字段的内存大小(只有Android7.0+上的设备才会看到此列)
    • Shallow Size: Java 内存中此字段的内存大小
    • Retained Size: 此字段目前还保留的内存大小
  • References:
    实例对象的引用链信息,References中包括如下信息:

    • Reference: 实例对象的引用链,可以依次点击展开显示此实例被哪些实例对象所引用,通过引用链可以最终追踪到GC Root
    • Depth: 此实例对象可达的最短跳数,表示的是任意一个GC Root到此实例对象的最短链路边数
    • Native Size: 原生内存中此实例对象的内存大小(只有Android7.0+上的设备才会看到此列)
    • Shallow Size: Java 内存中此实例对象的内存大小
    • Retained Size: 此实例对象目前还保留的内存大小

我们可以在Fields和References中分析,如果发现可以点如可能存在内存泄漏等,可以右键选择Go to Instance显示其实例内存数据;或者选择Jump to source进入此实例对象所在的源码片段。

一般我们使用内存分析器对内存进行分析时,注重点在于:

  • 首先关注内存占比比较高的类及其实例对象引用链情况,排查是否有内存泄漏、是否有优化空间
  • 关注某些类的实例对象数量比较大情况,排查是存在大量创建短时间内又销毁引起内存抖动,结合源码分析是否有优化空间,如使用缓存池等
  • 关注activity、context、view、Drawable等对象及其引用链情况,排查这些是否存在内存泄漏
  • 关注项目相关类的内存分配及其应用链情况,排查是否存在内存泄漏、使用不当情况等

你可能感兴趣的:(Android性能优化——内存优化)