Android | 如何搭建内存优化体系

点赞关注,不再迷路,你的支持对我意义重大!

🔥 Hi,我是丑丑。本文 「Android 路线」| 导读 —— 从零到无穷大 已收录,这里有 Android 进阶成长路线笔记 & 博客,欢迎跟着彭丑丑一起成长。(联系方式在 GitHub)


目录


前置知识

这篇文章的内容会涉及以下前置 / 相关知识,贴心的我都帮你准备好了,请享用~

Java 路线

Android 路线

adj 进程优先级 (high)
Linux内核OOM killer机制:https://juejin.cn/post/6844903878178111502

  • App 内存组成 & 限制(如何查看、监控)
  • Dalvik & ART 内存分配与垃圾回收

1. 重新认识内存

“一切性能问题最终都会变成内存问题。” 举个例子,为了避免资源重复下载,可以缓存到本地存储,而从本地存储加载进内存又需要磁盘 I/O。为了避免重复磁盘 I/O,可以缓存的内存,缓存的数据越来越大,最后变成内存问题。

1.1 什么是内存?

现代 Android 手机内存分为 运行时内存 RAM & 非运行时内存 ROM

  • 运行内存 RAM: 相当于 PC 中的内存条,是暂存 App 临时数据的存储介质。RAM 越大手机就能运行更多程序,且更佳流畅。考虑到体积和功耗,手机 RAM 不会使用 PC 中的 DDR RAM ,而是采用 LPDDR RAM(低功耗双倍数据速率内存);

  • 非运行内存 ROM: 相当于 PC 中的磁盘,是持久化存储数据的存储介质。ROM 越大手机能存储更多数据。

提示:今天我们讨论的内存优化指 “运行内存优化”,而 “非运行内存优化” 我们将在 “存储优化” 专题中讨论。

更多内容:内存指标 —— Android | 内存指标与测量方法

1.2 内存优化的维度

分别针对上面提到的 RAM 和 ROM 两种内存,Android 内存优化是分为两方面的工作:

  • 优化 RAM: 降低程序运行内存占用,防止程序发生 OOM,以及降低被 LMK 机制杀死的概率。同时不合理的内存使用会增大 GC 发生频率,从而导致程序卡顿;

  • 优化包体积: Resource 资源、so 库以及 Dex 文件都会占用内存,包体积越大会占用更多运行内存;

1.3 内存优化的误区

对内存优化的错误认识需要注意规避,主要有:

  • 内存占用越少越好?

内存优化不完全是追求于降低内存占用,当系统内存较充足 / 机型较高端的时候,我们完全可以多使用一些内存来换取更好的体验;而当系统内存不足 / 机型较低端的时候,我们应该更保守,做到 “用时分配,及时释放”。

  • 本地(native)内存不用管?

本地内存是不受 Java 堆大小限制,例如 Android 8.0 就重新把 Bitmap 的图片数据放在本地内存。但也不能滥用本地内存,主要原因是当系统物理内存不足时,LMK 机制也会开始杀进程,内存占用越高越可能被杀死。

1.4 内存优化的意义

优化内存的意义可以归结为如下三点:

  • 1、稳定性:防止程序发生 OOM,提高应用稳定性;
  • 2、顺畅度:减少 GC 频率,降低卡顿;
  • 3、保活:减少内存占用,降低被 LMK 机制杀死的概率。

需要注意的是,发生 OOM 的代码往往是 “压死骆驼的最后一棵稻草” ,但不一定是导致 OOM 的主要代码,完全可能只是刚好执行到这行代码发生 OOM。

1.5 内存优化到底要做什么?

理解了内存优化的重要性,现在我们来讨论内存优化到底要做什么呢,主要是优化三大问题:

  • 内存抖动(memory thrashing)

内存抖动是因为高频率的内存分配与回收,在内存波动图上往往呈现锯齿状,并伴随着程序卡顿。具体见 第 4 节

  • 内存泄漏(memory leak)

内存泄露简单来说就是没有回收不再使用的内存,导致内存占用居高不下,泄露的内存分为两种:Java 内存泄露 & Native 内存泄露。具体见 第 5 节

  • 内存溢出(out of memory)

内存溢出是引用内存申请超过了系统限制的最大堆内存,引发 OutOfMemoryError 异常。具体见 第 6 节


2. Android 内存管理

LMK


3. 内存指标与分析方法

在进行具体的内存优化之前,我们应该掌握基本的 Android 内存指标和相应的分析方法,这部分内容我单独放在这篇文章里:《Android | 内存指标与分析方法》,请享用~


4. 内存抖动问题

内存抖动(memory thrashing)是因为高频率的内存分配与回收,在内存波动图上往往呈现锯齿状。由于高频率 GC 行为会频繁 stop-the-world,虽然暂停的时间很短,但终归是有成本的,程序整体会变得卡顿。此外还有可能进一步引发 OOM,这是更严重的情况。

这个问题在 Dalvik 虚拟机上会更加明显,而 ART 虚拟机在内存管理跟回收策略上都做了大量优化,内存分配和 GC 效率相比提升了 5~10 倍,出现内存抖动的概率会小很多。

出现内存抖动时,内存分配之后马上就回收了,为什么还可能引发 OOM 呢?

主要原因是虚拟机可能会采用了 “无整理功能” 的垃圾收集器,频繁创建对象时会导致堆中的碎片急剧增加,直到虚拟机在分配内存时无法找到足够大小的连续内存时,就会引发 OOM。

Dalvik 虚拟机主要使用标记清除算法,也可以选择使用拷贝算法。ART 也有多个不同的 GC 方案,默认方案是 CMS。

4.1 问题定位

代码 review 是避免程序发生内存抖动的有力保障,但是我们更倾向于使用工具来快速定位,因为有时候我们并不熟悉相关的源码。使用 Android Studio 中的 Profiler 工具 可以帮助我们。

首先使用 Profiler 工具录制一段时间的内存占用情况,如果发现内存波动图呈现明显的锯齿状,或者存在高频率的 GC 事件,说明存在内存抖动。例如:

既然内存抖动是由于频繁分配与回收内存导致的,那么我们就有两种排查思路:

  • 1、排查占用内存最多的类

既然某一些对象的分配和回收有能力导致内存抖动,那么说明这些对象一定是占据了比较高的内存,所以第一种思路就是找出占用内存最多的类。

步骤如下:

  • 1、录制一段 App 内存占用信息,选取一小段时间;
  • 2、观察内存占用情况,可以发现 String[] 占用的内存和个数都是最多的;
    • Allocations:分配数量
    • Shallow Size:内存占用
  • 3、点击String[]条目,在Instance View窗口中会显示对象的实例;
  • 4、点击对象的实例,在Allocation Call Stack窗口中会显示创建对象的堆栈;
  • 5、点击Jump to Source,直接跳转到问题代码;
  • 6、最后定位到代码如下:
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    重复创建 String 数组对象
    handler = Handler {
        val array = Array(1000000) { "" }
        handler.sendEmptyMessageDelayed(0, 5);
        false
    }

    handler.sendEmptyMessageDelayed(0, 30);
}
  • 2、排查频繁调用的方法

程序会频繁分配内存的背后,其实也是在频繁调用分配内存的那个方法,所以第二种思路就是找出频繁调用的方法。

步骤如下:

  • 1、录制一段 App 方法调用信息,选取一小段时间;
  • 2、选择Top Down
  • 3、检查是否存在频繁调用的方法;
  • 4、最终定位到问题代码。

由于程序中频繁调用的代码可能会比较多,而且方法频繁调用并不是内存抖动的充分条件,所以思路二没有思路一明显。

4.2 常见案例

  • 1、对象复用

在 View#onDraw()、循环等场景中的对象应提高到外部进行复用;

  • 2、字符串拼接

使用显式 StringBuilder 替代 + 号,因为后者编译后也是采用了前者的方式,会生成太多中间变量;

  • 3、资源缓存池

采用缓资源存池(例如对象缓存池、线程池、位图缓存池),以重用频繁申请的资源。需要注意在使用结束后手动释放资源。


5. 内存泄露问题

内存泄露( memory leaks)简单来说就是没有回收不再使用的内存,导致内存占用居高不下,泄露的内存分为两种:

  • Java 内存泄露: 无用对象被生命周期更长的 GC Root 引用,导致无法判定为垃圾对象;
  • Native 内存泄露: Native 内存没有垃圾回收机制,需要手动进行回收。

5.2 Android 内存泄露的案例

实际开发中内存泄漏的案例是非常多的,需要分门别类,根据我的总结主要有以下几类:

5.2.1 生命周期误用

大多数内存泄漏是因为 对象的生命周期误用而引发的,例如:

  • 1、监听器对象未注销: 例如对象的监听器、BroadcastReceiver 和 EventBus 订阅者,通常监听器对象是采用强引用,需要手动注销;

  • 2、类的静态变量 / 单例: 类的静态变量 / 单例的生命周期是全局的,需要手动置空;

  • 3、ViewModel 中持有 Activity 对象引用:ViewModel 的生命周期是跨越重建的 Activity 的,如果将 Activity 对象存储在 ViewModel 中,那么在 Activity 重建时,原有的 Activity 对象会依然被 ViewModel;

  • 4、延时任务阻塞: 当对象在延迟任务阻塞等待时也会引起内存泄漏。例如在 MessageQueue 中等待执行的延迟任务会导致就会导致 Handler 无法被回收(Message 持有 Handler 的引用:target),而此时如果 Handler 还持有了 Activity 的引用,那么在 Activity 退出时,Activity 出现内存泄漏了。解决方法有:

    • 1、(必选)Handler 使用静态内部类,并且只持有 Activity 的弱引用;
    • 2、(可选)在 Activity 退出时,移除消息队列中的延迟消息。

5.2.2 资源未释放

当资源无法使用时,应该及时释放,例如:

  • 1、Closable 对象: 例如文件流、数据库连接;

  • 2、集合中的对象: 集合容器中对象如果不再使用,应该及时 clear 清空;

  • 3、资源缓存池: 例如对象缓存池、线程池、位图缓存池。

5.2.3 WebView

WebView 启动一次之后内核是不会释放的。可以用一个单独的进程承载 WebView,并使用 AIDL 与主进程通信。WebView 所在进程可以根据业务需要在合适的时候销毁。

5.2 Java 内存泄露监控

建立类似自动化检查方案,至少在 Activity 和 Fragment 泄漏时自动弹出对话框提醒开发者发现问题,类似 LeakCanary 方案。在线上环境上报需要优化 Hprof 内存快照文件大小,文件越小上传的成功率越高,主要方法是裁剪图片对应的 byte 数组。

有一个 “内存泄漏自动化链路分析组件 Probe”,

5.3 Native 内存泄露监控

高手课 下


6. 内存溢出问题

内存溢出(out of memory)是引用内存申请超过了系统限制的最大堆内存,引发 OutOfMemoryError 异常。Android 设备出厂后,最大堆内存就已经确定,相关的配置位于系统根目录/system/build.prop文件中的dalvik.vm.heapgrowthlimit

MAT

重要概念

incoming references
outgoing references

内存指标

当前设备内存占用情况 / 当前应用内存占用情况

ViewRootImpl 是Activity与Window的桥梁


参考资料


创作不易,你的「三连」是丑丑最大的动力,我们下次见!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354