程序性能优化之内存优化三上篇

34次阅读

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

阿里 P7 移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680
本篇文章将继续从以下两个内容来介绍内存优化:

  • [内存抖动]
  • [内存泄漏]

其实大多数 App 或多或少都存在一定的内存泄漏情况,这些内存泄漏可能存在于特定的运行环境时才会发生。而内存泄漏堆积会引发严重后果 OOM。内存抖动是指内存频繁地分配和回收,而频繁的 gc 会导致卡顿,严重时和内存泄漏一样会导致 OOM。

接下来我们一起讨论该如何查看以及解决这部分问题思路。

一、内存泄漏

内存泄露是指程序中间动态分配了内存,但在程序结束时没有释放这部分内存,从而造成那部分内存不可用的情况,重启计算机可以解决,但也有可能再次发生内存泄露,内存泄露和硬件没有关系,它是由软件设计缺陷引起的。

简单点说:应该被释放的资源没有被释放。

1、内存泄漏的种类

1)常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。

2)偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。

3) 一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。

4) 隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。

2、为什么要修复内存泄漏

少量的内存泄漏可能不会引发什么问题;但是内存泄漏累积,再多的内存也会被耗尽,最终导致 OOM。

二、定位内存泄漏

1、初步定位是否发生内存泄漏

借助 Android Studio 的 Monitor 查看是否发生了内存泄漏情况

通过反复的执行同一个功能,触发 GC 操作,观察内存前后变化情况。

如果内存前后未发生明显变化(增加)此时可以 初步判断 未发生内存泄漏。

比如此时内存使用情况为:21.04MB,然后我们打开一个新的 Activity,然后返回执行 GC 操作,观察此时的内存使用情况。

2、Monitor 栏基本功能说明:

序号 1、 手动触发 GC 操作;

序号 2、Dump Java Heap,获取当前的堆栈信息,生成一个.hprof 文件,AndroidStudio 会自动使用 HeapViewer 打开;一般用于操作之后检测内存泄漏的情况;

序号 3、Start Allocation Tracking 内存分配追踪工具,用于追踪一段时间的内存分配使用情况,能够知道执行一些列操作后,有哪些对象被分配空间。一般用于追踪某项操作之后的内存分配,调整相关的方法调用来优化 app 性能与内存使用;

序号 4、剩余可用 内存;

序号 5、已经使用的内存;

3、Dump Java Heap 进一步定位内存泄漏

通过 Monitor 栏只能初步粗略的观察是否发生内存泄漏,然而要真正的发现内存泄漏以及精确定位内存泄漏位置还需要借助相关工具分析排查。

点击 Memory Monitor 的Dump Java Heap,会生成一个.hprof 文件,AndroidStudio 会自动使用 HeapViewer 打开。

面板说明:

面板 1:

Detect Leaked Activities:检测泄漏的 Activity

Find Duplicate Strings:查找重复的字符串

默认两个选项都是勾选的。

点击绿色箭头,此时大家会看到 Leaked Activities 下有一个 LaunchActvity@31… 的信息,没错发生了内存泄漏,稍后我们分析如何发生的内存泄漏。

面板 2:

Total Count:该类的实例个数

Heap Count:选定的 Heap 中实例的个数

Sizeof:每个实例占用的内存大小

Shallow Size:所有该类的实例占用的内存大小

Retained Size:该类的所有实例可支配的内存大小

面板 3:

Instance:该类的所有实例对象(左侧 Total Count 为 15,此处就有 15 个对象)

Depth:深度, GC Root 点到该实例的最短链路数

Dominating Size:该实例可支配的内存大小

此时发现面板下有个实例存在。

面板 4:

Reference Tree:引用树

通过 面板 1 我们发现有一个 Activity 发生了泄漏。我们可以通过 Reference Tree 面板就可以跟踪到该实例的引用树关系。

首先第一行我们发现一个 LaunchActivity 实例存在,然后展开该实例进一步查看该实例的引用关系,第二行我们可以看出它是被 LaunchActity 匿名内部类持有(this$0),这个匿名内部类实例是 callBack,紧接着会发现该实例在 mPermissionUtil 实例中持有。

此时,我们可以进入代码查看该 callBack 是什么,然后在 mPermissionUtils 的持有。

LaunchActivity 中:

PermissionUitl 中:

跟踪代码发现 callback 是一个接口,然后在 LaunchActvity 中调用了 PermissionUtil 的 requestPermission(callback),然后将该 callback 赋值给 PermissionUtil 中成员引用,由于 PermissionUtil 是一个单例,然后 new PermissionCallBack()匿名内部类会默认持有外部类引用,此时它将持有外部类 LaunchActivity 的实例,然后有赋值给了 PermissionUtil 中的成员引用,所以造成的内存泄漏。这种内存泄漏称之为一次性内存泄漏,只会发生一次且只会泄漏最后一次调用者。

通过使用 Androd Studio 自带的 Dump Java Heap 排查内存泄漏问题对于相对简单的泄漏场景比较适合,如果发生较为复杂的泄漏场景可能使用 Dump Java Heap 不太容易查找问题。此时我们可以借助另外一个工具:MAT(Memory Analyzer Tool)

4、MAT

Memory Analyzer Tool 是 Eclipse 的一个插件,它的使用以及安装这部分资料非常多,故篇幅原因不在展开分析介绍。

下载地址:https://www.eclipse.org/mat/downloads.php

荐:https://blog.csdn.net/u010335298/article/details/52233689

荐:https://blog.csdn.net/itachi85/article/details/77075455

5、其他

我们也可以借助第三方检测库,在运行期间检查内存泄漏情况:LeakCanary

LeakCanary 是 square 出品的一个检测内存泄漏的库,集成到 App 之后便无需关心,在发生内存泄漏之后会 Toast、通知栏弹出等方式提示,可以指出泄漏的引用路径,而且可以抓取当前的堆栈信息供详细分析。

分析内存泄漏主要是定位 GC Root,只有明白 GC Root 点才能够准确分析定位内存泄漏问题。

三、内存抖动

内存抖动是指内存在短时间内频繁地分配和回收,而频繁的 gc 会导致卡顿,严重时和内存泄漏一样会导致 OOM。

内存抖动为什么会造成 OOM 这关系到 Java 的垃圾回收。

1、常见内存抖动场景

循环中创建大量临时对象;

onDraw 中创建 Paint 或 Bitmap 对象等;

2、内存抖动后果

瞬间产生大量的对象会严重占用 Young Generation 的内存区域,当达到阀值,剩余空间不够的时候,也会触发 GC。系统花费在 GC 上的时间越多,进行界面绘制或流音频处理的时间就越短 即使每次分配的对象占用了很少的内存,但是他们叠加在一起会增加 Heap 的压力,从而触发更多其他类型的 GC。这个操作有可能会影响到帧率,并使得用户感知到性能问题。

四、onTrimMemory 与 onLowMemory

Android 系统的每个进程都有一个最大内存限制,如果申请的内存资源超过这个限制,系统就会抛出 OOM 错误。

onTrimMemory

所以在实际开发过程中我们要尽可能避免内存泄漏与内存抖动之外,还要格外注意内存使用情况。根据《Manage Your App’s Memory》,我们可以对内存的状态进行监听,我们的 Application、Acivity、Service、ContentProvider 与 Fragment 都实现了 ComponentCallbacks2 接口。所以能够重写 onTrimMemory 与 onLowMemory 函数。

onTrimMemory 的参数是一个 int 数值,代表不同的内存状态:

TRIM_MEMORY_RUNNING_MODERATE:

你的应用正在运行并且不会被列为可杀死的。但是设备此时正运行于低内存状态下,系统开始触发杀死 LRU Cache 中的 Process 的机制。

TRIM_MEMORY_RUNNING_LOW:

你的应用正在运行且没有被列为可杀死的。但是设备正运行于更低内存的状态下,你应该释放不用的资源用来提升系统性能。

TRIM_MEMORY_RUNNING_CRITICAL:

你的应用仍在运行,但是系统已经把 LRU Cache 中的大多数进程都已经杀死,因此你应该立即释放所有非必须的资源。如果系统不能回收到足够的 RAM 数量,系统将会清除所有的 LRU 缓存中的进程,并且开始杀死那些之前被认为不应该杀死的进程,例如那个包含了一个运行态 Service 的进程。

当应用进程退到后台正在被 Cached 的时候,可能会接收到从 onTrimMemory()中返回的下面的值之一:

TRIM_MEMORY_BACKGROUND:

系统正运行于低内存状态并且你的进程正处于 LRU 缓存名单中最不容易杀掉的位置。尽管你的应用进程并不是处于被杀掉的高危险状态,系统可能已经开始杀掉 LRU 缓存中的其他进程了。你应该释放那些容易恢复的资源,以便于你的进程可以保留下来,这样当用户回退到你的应用的时候才能够迅速恢复。

TRIM_MEMORY_MODERATE:

系统正运行于低内存状态并且你的进程已经已经接近 LRU 名单的中部位置。如果系统开始变得更加内存紧张,你的进程是有可能被杀死的。

TRIM_MEMORY_COMPLETE:

系统正运行于低内存的状态并且你的进程正处于 LRU 名单中最容易被杀掉的位置。你应该释放任何不影响你的应用恢复状态的资源。

TRIM_MEMORY_UI_HIDDEN:

UI 不可见了,应该释放占用大量内存的 UI 数据。

比如说一个 Bitmap,我们缓存下来是为了可能的 (不一定) 再次显示。但是如果接到这个回调,那么还是将它释放掉,如果回到前台,再显示会比较好。

onLowMemory

这个函数看名字就是低内存。这个函数的回调意味着后台进程已经被干掉了。这个回调可以作为 4.0 兼容 onTrimMemory 的 TRIM_MEMORY_COMPLETE 来使用

如果希望在其他组件中也能接收到这些回调可以使用上下文的 registerComponentCallbacks 注册接收,

unRegisterComponentCallbacks 反注册

五、OutOfMemeory

OOM 就是申请的内存超过了 Heap 的最大值。

OOM 的产生不一定是一次申请的内存就超过了最大值,导致 oom 的原因基本上都是一般情况,我们的不良代码平时”积累”下来的。

我们知道 Android 应用的进程都是从一个叫做 Zygote 的进程 fork 出来的。并且每个应 Aandroid 会对其进行内存限制。我们可以查看:

六、有效减少内存占用的建议

1、使用 Android 优化过后的集合

在 Android 开发时,我们使用的大部分都是 Java 的 api。其中我们经常会用到 java 中的集合,比如 HashMap。

使用 HashMap 非常舒服,但是对于 Android 这种内存敏感的移动平台,很多时候使用这些 Java 的 API 并不能达到更好的性能,相反反而更消耗内存,所以针对 Android,google 也推出了更符合自己的 API,比如 SparseArray、ArrayMap 用来代替 HashMap 在有些情况下能带来更好的性能提升。

注意:此处仅考虑内存占用情况,并且在一定的长度的数据集,并不是适合所有场景下。

2、集合初始长度

如:HashMap,他的默认长度为 16,负载因子为 0.75,如果我们知道要存放数据的长度如 5,此时最合适的 HashMap 的初始容量为:5/0.75 = 7;

故:HashMap map = new HashMap(7)

3、Bitmap

Bitmap 可以说是一个内存中的大胖子,作为现在 Android 开发程序是比较幸福,有很多关于图片加载优秀的库,如 Glide。

有关于 Bitmap 的优化我们会在后续单独专题中介绍,故不在此处展开介绍。

荐:《Handling Bitmaps》

4、try{}cacth(Error){}

对高风险 OOM 代码块如展示高清大图等进行 try catch,在 catch 块加载非高清的图片并做相应内存回收的处理。注意 OOM 是 OutOfMemoryError,不能使用 Exception 进行捕获。

5、解决所有内存泄漏问题

少量的内存泄漏可能不会带来较为明显的影响,但是内存泄漏堆积的后果是非常严重的,再多的内存也会被耗尽,最终导致 OOM 发生。

6、避免内存抖动

尽量避免在 循环体或者频繁调用的函数 内创建对象,应该把对象创建移到 循环体 外。

另外还有一个经典的 String 拼接创建大量小的对象造成的内存抖动。

有时会发现频繁的调用 Log 打印日志,App 会变卡顿。

Log.i(TAG,width+”x”+height);

这里会产生 2 个新对象,width+”x”与 width+”x”+height。

而 TAG 与 x 是编译时就存在的字符串常量池,所以不算新对像。

所以一般来说我们会对日志输出 Log 进行控制,或者使用 StringBuilder 进行优化。

7、onTrimMemory 根据不同的内存状态做相应处理

对于未实现 ComponentCallbacks2 组件,我们需要为其注册 ComponentCallbacks2。

七、总结

性能优化是一个长期实践过程;大多数问题都是由一般问题造成的,然后这部分一般问题的积累最终会引发严重后果;压死骆驼的可能就是最后一根稻草 。在项目实际开发过程中: 要特别注意内存泄漏与内存抖动的场景,注意配合使用 onTrimMemory 完成内存的管理工作。

阿里 P7 移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680
原文链接:https://www.jianshu.com/p/607…

正文完
 0