Android 内存优化
运行内存
手机运行内存(RAM)其实相当于 PC 中的内存。是手机中作为 APP 运行过程中临时性数据暂时存储的内存介质。
但考虑到体积和功耗,手机不使用 PC 的 DDR 内存,而是 LPDDR RAM(低功耗双倍数据速率内存)。其中 LP 就是 Lower Power 低功耗的意思。
目前主流的运行内存有 LPDDR3、LPDDR4 以及 LPDDR4X。性能方面,高频好于低频。
手机内存不是越大越好,也跟使用的 LPDDR RAM 的类型有关。
并且,内存也不是个孤立的概念。它跟操作系统、应用生态这些因素都有关。
内存引发的问题
1、异常:比如 OOM、内存分配失败、整体内存不足导致应用被杀死、设备重启等问题。
2、卡顿:比如 Java 内存不足会导致频繁 GC 造成卡顿(GC 触发之后,所有的线程会暂停)。或是物理内存不足时系统会触发 low memory killer(低内存 killer) 机制,系统负载过高造成卡顿。
内存优化和架构设计时的两个误区
1、内存占用越少越好:
应根据实际情况。当系统内存充足时,可多用一些获得更好的性能。当不足时,可以”用时分配,及时释放”。
Android Bitmap 内存分配的变化:
- Android 3.0 之前:
Bitmap对象 放在 Java 堆,而像素数据放在 Native 内存中。如果不手动调用 recycle,Bitmap Native 内存的回收完全依赖 finalize 函数回调,而这个时机不太可控。 - Android 3.0 ~ Android 7.0:
Bitmap 对象和像素数据统一放到 Java 堆中,这样就算不调用 recycle,Bitmap 内存也会随着对象一起被回收。但 Bitmap 是内存消耗大户,而手机的最大 Java 堆存在限制,即使物理内存足够,依然会因 Java 内存不足导致 OOM。
Bitmap 放在 Java 堆的另外一个问题会引起大量的 GC,对系统内存也没有完全利用起来。 - Android 8.0:
使用 NativeAllocationRegistry 辅助回收 Native 内存的机制,来实现像素数据放到 Native 内存中。
还新增了硬件位图 Hardware Bitmap,它可以减少图片内存并提高绘制效率。
2、Native 内存不管用:
即使在 Android 8.0 重新将 Bitmap 内存放回到 Native 中,也不意味着可以随心所欲的使用图片。
当系统物理内存不足时,lmk 会开始杀进程,从后台、桌面、服务、前台、直到系统重启。
测量方法
对于系统内存和应用内存的使用情况,可参考 Android Developer 中《调查 RAM 使用情况》。
1、Java 内存分配:
对于跟踪 Java 堆内存的使用情况,最常用的有 Allocation Tracker可参考《Android 内存申请分析》 和 MAT 这两个工具。
2、Native 内存分配:
Android 8.0 后,可根据这个指南来使用 AddressSanitize 做内存泄漏检测。
关于 Native 内存分配工具,可参考 Android Developer 的一些相关文档调试本地内存使用。
- Malloc 调试:
调试 Native 内存的一些使用问题,如堆破坏、内存泄漏、非法地址等。Android 8.0 之后支持在非 root 的设备做 Native 内存调试,不过跟 AddressSanitize 一样,需要通过wrap.sh做包装。1
adb shell setprop wrap.<APP> '"LIBC_DEBUG_MALLOC_OPTIONS=backtrace logwrapper"'
- Malloc 钩子:
是在 Android P 之后,Android 的 libc 支持拦截在程序执行期间发生的所有分配/释放调用,这样就可以g构建出自定义的内存检测工具。1
adb shell setprop wrap.<APP> '"LIBC_HOOKS_ENABLE=1"'
内存优化探讨
1、设备分级
内存优化首先要根据设备环境来综合考虑。同时,在架构设计时需要做到以下几点:
- 设备分级
使用 device-year-class 的策略对设备分级,对于低端机可关闭复杂的动画,或是某些功能,使用 565 格式图片,使用更小的缓存内存等。 - 缓存管理
需要一套统一的缓存管理机制,可适当的使用内存。使用 onTrimMemory 回调,根据不同状态决定释放多少内存。 - 进程模型
减少应用启动的进程数、减少常驻进程、有节制的保活。 - 安装包大小
安装包中的代码、资源、图片、以及 so 库的体积,跟它们占用的内存有很大的关系。
2、Bitmap 优化
- 统一图片库
如低端机使用 565 格式、更加严格的缩放算法,可以使用 Glide、Fresco 或自研。而且要将 Bitmap.createBitmap、BitmapFactory 相关的接口也一并收拢。 - 统一监控
- 大图片监控
- 重复图片监控
- 图片总内存
3、内存泄漏
内存泄漏主要分两种情况:
- 同一个对象泄露
- 每次都会泄露新的对象,可能会出现几百上千个无用的对象。
对内存泄漏的监控:
- Java 内存泄露。建立类似 LeakCanary 自动化检测方案。可以对生成的 Hprof 内存快照文件做一些优化,裁剪大部分图片对应的 byte 数组减少文件大小。
- OOM 监控
- Native 内存泄漏监控
- 针对无法重编 so 的情况
- 针对可重编 so 情况
开发过程中内存泄漏排查可以使用 Android Profiler 和 MAT 工具配合使用。
内存监控
- 采集方式
用户在前台时,可每五分钟采集一次 PSS、Java 堆、图片总内存。并按用户抽样,对其在一天内持续采集数据。 - 计算指标
通过上面的数据,可计算出下面一些内存指标:
- 内存异常率
可反映出内存占用的异常情况。比如出现新的内存使用不当或是内存泄漏的场景。1
2
3
4/**
* PSS 的值可通过 Debug.MemoryInfo 拿到
*/
内存 UV 异常率 = PSS 超过 400MB 的 UV / 采集 UV - 触顶率
可反应 Java 内存的使用情况,如果超过 85% 最大堆限制,GC 会变得更加频繁,容易造成 OOM 和卡顿。其中是否触顶可通过下面的方式计算得到:1
内存 UV 触顶率 = Java 堆占用超过最大堆限制的 85% 的 UV / 采集 UV
1
2
3
4
5long javaMax = runtime.maxMemory();
long javaTotal = runtime.totalMemory();
long javaUsed = javaTotal - runtime.freeMemory();
// Java 内存使用超过最大限制的 85%
float proportion = (float) javaUsed / javaMax;
- GC 监控
Android 6.0 后,可通过下面的方法拿到更精准的 GC 信息:1
2
3
4
5
6
7
8// 运行的 GC 次数
Debug.getRuntimeStat("art.gc.gc-count");
// GC 使用的总耗时,单位是毫秒
Debug.getRuntimeStat("art.gc.gc-time");
// 阻塞式 GC 的次数
Debug.getRuntimeStat("art.gc.blocking-gc-count");
// 阻塞式 GC 的总耗时
Debug.getRuntimeStat("art.gc.blocking-gc-time");OOM
OOM(内存溢出):
是指内存占有量超过了 VM 所分配的最大值。
出现OOM的原因:
1:加载的对象过大。
2:相应资源过多,来不及释放。
3:内存泄露没有得到及时解决。(个人理解。内存有限,却还一直被无用对象占有,可供使用的内存自然就少了)。
在内存引用上做些处理,在内存中加载图片时直接在内存中处理,如边界压缩,动态回收内存,优化Dalvik虚拟机的堆内存分配,自定义堆内存大小等。
通过命令行获取内存情况:
执行:adb shell
查看瞬时进程:ps 或 ps -ef(Mac)
打印对应进程的内存情况:dumpsys meminfo 包名
Heap Size:总内存
Heap Alloc:已使用
Heap Free:还剩下
通过代码来获取内存情况:
1 | // 内存获取 |
1 | // 获取运行时的内存 |
减少内存占用
可以从如下几个方面去展开说明:
- AutoBoxing(自动装箱): 能用小的坚决不用大的。
- 内存复用
- 使用最优的数据类型
- 枚举类型: 使用注解枚举限制替换 Enum
- 图片内存优化(这里可以从 Glide 等开源框架去说下它们是怎么设计的)
- 基本数据类型如果不用修改的建议全部写成 static final,因为 它不需要进行初始化工作,直接打包到 dex 就可以直接使用,并不会在 类 中进行申请内存
- 字符串拼接别用 +=,使用 StringBuffer 或 StringBuilder
- 不要在 onMeause, onLayout, onDraw 中去刷新 UI
- 尽量使用 C++ 代码转换 YUV 格式,别用 Java 代码转换 RGB 等格式,真的很占用内存
解决OOM的办法;
1;按需求,考虑使用软引用或弱引用。
2;动态回收内存
3;自定义堆内存和虚拟机的内存分配
一些性能优化建议:
- 避免创建过多的对象
- 不要过多使用枚举,枚举占用的内存空间要比整型大
- 常量使用 static final 来修饰
- 使用一些 Android 特有的数据结构,比如 SparseArray 和 Pair 等,它们都具有更好的性能
- 适当使用软引用和弱引用
- 采用内存缓存和磁盘缓存
- 尽量采用静态内部类,可避免潜在的由于内部类而导致的内存泄漏
卡顿
卡顿的根本原因,第一个原理方面是绘制原理,另一个就是刷新原理。
绘制原理:
Activity -> onCreate:当 Activity 成功创建,并且调用 onCreate 生命周期的时候,会执行将 setContentView 的布局 ID 转换为 View 对象的一个过程。
Activity -> onResume:拿到转换后的 View Tree ,请求接收 VSYNC 垂直同步的消息,收到消息之后会执行 performTraversals 函数最后实行绘制。
- onMeasure : 用深度优先的原则递归所有视图的宽高,获取当前 View 的正确宽高之后,可以调用它的成员函数 Measure 来设置它的大小。如果当前正在测量的子视图 child 是一个容器,那么它又会重复执行操作,直到它的所有子视图的大小都测量完毕。
- onLayout:用深度优先原则得到所有视图 View 的位置,当一个子 View 在应用程序窗口左上角的位置确定之后,再结合它在前面测量过程中确定的宽高,就可以完全确定它在应用程序窗口中的布局。
- onDraw: 目前 Android 支持两种绘制方式,软件绘制和硬件加速(GPU),其中硬件加速在 Android 3.0 开始已经全面支持,很明显,硬件加速在 UI 的显示和绘制的效率远远高于 CPU 的绘制,但是硬件加速也有缺点:
- 耗电问题: GPU 的功耗比 CPU 高
- 兼容问题: 某些接口和函数不支持硬件加速
- 内存大: 使用 OpenGL 的接口至少需要 8MB 的内存
刷新原理:
View 的 requestLayout 和 ViewRootImpl##setView 最终都会调用 ViewRootImpl 的 requestLayout 方法,然后通过 scheduleTraversals 方法向 Choreographer 提交一个绘制任务,然后再通过 DisplayEventReceiver 向底层请求 vsync 垂直同步信号,当 vsync 信号来的时候,会通过 JNI 回调回来,在通过 Handler 往消息队列 post 一个异步任务,最终是 ViewRootImpl 去执行绘制任务,最后调用 performTraversals 方法,完成绘制。
卡顿的根本原因:
从刷新原理来看卡顿的根本原理是有两个地方会造成掉帧:
- 一个是主线程有其它耗时操作,导致doFrame 没有机会在 vsync 信号发出之后 16 毫秒内调用;
- 还有一个就是当前doFrame方法耗时,绘制太久,下一个 vsync 信号来的时候这一帧还没画完,造成掉帧。
可从下面四个方面来监控应用程序卡顿:
- 基于 Looper 的 Printer 分发消息的时间差值来判断是否卡顿。
1 | //1. 开启监听 |
基于 Choreographer 回调函数 postFrameCallback 来监控
Android 系统从 4.1(API 16) 开始加入 Choreographer 类,用于同 Vsync 机制配合,实现统一调度界面绘图。 系统每隔 16.6ms 发出 VSYNC 信号,来通知界面进行重绘、渲染,理想情况下每一帧的周期为 16.6ms,代表一帧的刷新频率。开发者可以通过 Choreographer 的 postFrameCallback 设置自己的 callback,你设置的 callcack 会在下一个 frame 被渲染时触发。因此,1S 内有多少次 callback,就代表了实际的帧率。然后我们再记录两次 callback 的时间差,如果大于 16.6ms,那么就说明 UI 主线程发生了卡顿。同时,设置一个报警阀值 100ms,当 UI 主线程卡顿超过 100ms 时,就上报卡顿的耗时以及当时的堆栈信息。
优点:
- 通过调用系统函数自动获取数据,我们只需要在 APP 进行业务操作即可。
- 每一次 APP 进行绘制轮询时 postFrameCallback 都会被调用,即时页面没有更新,能更准确计算帧率和掉帧。
- 在卡顿出现的时刻可以获取应用堆栈信息。
缺点:
- 需要另开子线程获取堆栈信息,会消耗少量系统资源
怎么避免卡顿:
一定要避免在主线程中做耗时任务,总结一下 Android 中主线程的场景:
- UI 生命周期的控制
- 系统事件的处理
- 消息处理
- 界面布局
- 界面绘制
- 界面刷新
- …
还有一个最重要的就是避免内存抖动,不要在短时间内频繁的内存分配和释放。
ANR
响应速度优化和 ANR 日志分析:
响应速度优化的核心思想是避免在主线程中做耗时操作,耗时操作可采用异步的方式。
当一个进程发生 ANR 后,系统会在 /data/anr 目录下创建一个文件 traces.txt 。
Android 规定:
- Activity 如果 5 秒钟之内无法响应屏幕触摸事件或者键盘输入事件就会出现 ANR
- BroadcastReceiver 如果 10 秒钟之内还未执行完操作也会出现 ANR
Application Not Responding, 也就是”应用无响应”. 当操作在一段时间内系统无法处理时, 系统层面会弹出ANR对话框。
在Android里, App的响应能力是由Activity Manager和Window Manager系统服务来监控的. 通常在如下两种情况下会弹出ANR对话框:
1:KeyDispatchTimeout(5 seconds) –主要类型
按键或触摸事件在特定时间内无响应
2:BroadcastTimeout(10 seconds)
BroadcastReceiver在特定时间内无法处理完成
3:ServiceTimeout(20 seconds) –小概率类型
Service在特定的时间内无法处理完成
造成以上两种情况的首要原因就是在主线程(UI线程)里面做了太多的阻塞耗时操作,例如文件磁盘读写, 数据库读写, 网络查询,图像变换,大量创建新对象等。
应该UI线程尽量只做跟UI相关的工作。耗时操作放在单独线程中。尽量用Handler处理UI Thread和别的Thread之间的交互。
排查;log日志,代码,ANR成因,data目录下的trace.txt文件,检测ANR的watchdog,LeakCanary库。
FC(Force Close): Error,OOM,SOFE,Runtime,注意内存。
如何分析ANR?
ANR产生时, 系统会生成一个traces.txt的文件放在/data/anr/下. 开发人员可通过adb命令将其导出到本地 ($adb pull data/anr/traces.txt .)通过分析,可以根据具体的日志查看Anr原因( 如: 普通阻塞,CPU满负荷,内存泄露 )
UI线程主要包括如下:
Activity:onCreate(), onResume(), onDestroy(), onKeyDown(), onClick()
AsyncTask: onPreExecute(), onProgressUpdate(), onPostExecute(), onCancel()
Mainthread handler: handleMessage(), post(runnable r)
other
高性能编程及调优
对于高性能编码:常见的比如说,常量使用static final修饰符,尽量使用系统封装好的API,静态优于抽象,使用增强型for循环,尽量访问本地变量而不是成员变量,对于for的第二个条件不要调用任何方法,避免在内部调用Getter/Setter,避免创建不必要的对象和临时对象,减少不必要的全局变量,采用三级缓存,但也尽量避免过量使用,对于图片合理的缩放,加载时可在内存中直接操作,对于能保存路径地址的就不要存放图片。
对于调优:主要还是处理内存的问题,管理内存也就是内存的分配和内存的回收,分配由系统完成,主要关注的是内存的回收问题。回收靠的是GC操作,也就是垃圾回收机制,对于GC操作,首先要避免的就是内存泄漏的问题,其实,我们还可以在内存不足时或界面不可见时释放内存,使用优化过的数据集合,使用IntentService代替Service,避免在Bitmap上浪费内存。其他还有像布局优化,主要两种手段,一是删除无用控件和层级,并且尽量使用性能较低的ViewGroup。二是跟三个控件相关,对于绘制优化,像自定义View较多,所以避免在View的onDraw()方法中执行大量操作,像创建新的局部变量,耗时操作,大量的循环操作。
对于项目来讲,在布局优化上主要采用的include,merge和viewStab,在性能上比如采用合适的集合。
备注