Android 内存优化是我们性能优化工作中比较重要的一环, 这里其实主要包括两方面的工作:
优化 RAM, 即降低运行时内存这里的目的是防止程序发生 OOM 异常, 以及降低程序由于内存过大被 LMK 机制杀死的概率另一方面, 不合理的内存使用会使 GC 大大增多, 从而导致程序变卡
优化 ROM, 即降低程序占 ROM 的体积这里主要是为了降低程序占用的空间, 防止由于 ROM 空间不足导致程序无法安装
本文的着重点为第一点, 总结概述降低应用运行内存的技巧在这里我们不再细述 PSSUSS 等概念与 Android 应用的内存管理, 如对这部分内容感兴趣, 可自行阅读文末的参考文章
内存泄露的检测与修改
内存泄露: 简单来说对象由于编码错误或系统原因, 仍然存在着对其直接或间接的引用, 导致系统无法进行回收内存泄露, 容易留下逻辑隐患, 同时增加了应用内存峰值与发生 OOM 的概率它属于 bug issue, 是我们一定要修改的
下面是造成内存泄露的一些常见原因, 但是如何建立一套发现内存泄露解决内存泄露的闭环方案, 才是我们工作的重点
一. 内存泄露的监控方案
Square 的开源库 leakcanry 是一个非常不错的选择, 它通过弱引用方式侦查 Activity 或对象的生命周期, 若发现内存泄露自动 dump Hprof 文件, 通过 HAHA 库得到泄露的最短路径, 最后通过 notification 展示
内存泄露判断与处理的流程如下图 , 各自运行的进程空间(主进程通过 idlehandler,HAHA 分析使用的是单独的进程):
微信在 leakcanry 推出之前已经有了自己的内存泄露监控体系, 与 leakcanry 大致有以下的区别:
在微信中, 对于 4.0 以上的机型也是采用通过注册 ActivityLifecycleCallbacks 接口, 对于 4.0 以下的机型我们会尝试反射 ActivityThread 中的 mInstrumentation 对象当然, 现在微信也改成只支持 android-15 以上, 美美哒
leakcanry 尽管使用了 idlehandler 与分进程, 但是 dumphprof 依然会造成应用明显的卡顿 (SuspendAll Thread) 而在三星等一些手机, 系统会缓存最后一个 Activity, 所以在微信, 我们采取了更严格的检测模式, 即泄露三次确认以及经过 5 个新建的 Activity, 确保不是由于系统缓存的原因造成
在微信中, 当发现疑似内存泄露时会弹出对话框, 当我们主动点击时才会去做 dumpHprof 以及上传 Hprof 快照的操作, 而是否误报泄露链等分析工作也是放于服务器端
事实上, 通过对 leakcanry 做简单的定制, 我们就可以实现以下一个内存泄露监控闭环
二. 对系统内存泄露的 Hack Fix
AndroidExcludedRefs 列出了一些由于系统原因导致引用无法释放的例子, 同时对于大多数的例子, 都会提供建议如何通过 hack 的建议去修复在微信中, 对 TextLineInputMethodManagerAudioMangerandroid.os.Message 也采用了类似 Hack 的方式(详细可看参考资料)
三. 通过兜底回收内存
Activity 泄漏会导致该 Activity 引用到的 BitmapDrawingCache 等无法释放, 对内存造成大的压力, 兜底回收是指对于已泄漏 Activity, 尝试回收其持有的资源, 泄漏的仅仅是一个 Activity 空壳, 从而降低对内存的压力
做法也非常简单, 在 Activity onDestory 时候从 view 的 rootview 开始, 递归释放所有子 view 涉及的图片, 背景, DrawingCache, 监听器等等资源, 让 Activity 成为一个不占资源的空壳, 泄露了也不会导致图片资源被持有
- Drawable d = iv.getDrawable(); if (d != null) { d.setCallback(null); } iv.setImageDrawable(null);
- ...
- ...
总的来说, 我们不是只懂得一些内存泄露解决方法就可以, 更重要的是通过日常测试与监控, 得到内存泄露检测与修改的一整套闭环体系
降低运行时内存的一些方法
当我们能确保应用中不会出现内存泄露时, 我们需要一些其他的方法来降低运行时的内存更多的时候, 我们其实只希望降低应用发生 OOM 的概率
Android OOM:
Android 2.x 系统, 当 dalvik allocated + external allocated + 新分配的大小 >= dalvik heap 最大值时候就会发生 OOM 其中 bitmap 是放于 external 中
Android 4.x 系统, 废除了 external 的计数器, 类似 bitmap 的分配改到 dalvik 的 java heap 中申请, 只要 allocated + 新分配的内存 >= dalvik heap 最大值的时候就会发生 OOM(art 运行环境的统计规则还是和 dalvik 保持一致)
一. 减少 bitmap 占用的内存
说到内存, bitmap 必然是这里的大头对于 bitmap 内存占用, 想说的有以下几点:
防止 bitmap 占用资源多大导致 OOMAndroid 2.x 系统 BitmapFactory.Options 里面隐藏的的 inNativeAlloc 反射打开后, 申请的 bitmap 就不会算在 external 中对于 Android 4.x 系统, 可采用 facebook 的 fresco 库, 即可把图片资源放于 native 中
图片按需加载即图片的大小不应该超过 view 的大小在把图片载入内存之前, 我们需要先计算出一个合适的 inSampleSize 缩放比例, 避免不必要的大图载入对此, 我们可以重载 drawable 与 ImageView, 例如在 Activity ondestroy 时, 检测图片大小与 View 的大小, 若超过, 可以上报或提示
统一的 bitmap 加载器 PicassoFresco 都是比较出名的加载库, 同样微信也有自己的库 ImageLoader 加载库的好处在于将版本差异大小处理对使用者不感知有了统一的 bitmap 加载器, 我们可以在加载 bitmap 时, 若发生 OOM(try catch 方式), 可以通过清除 cache, 降低 bitmap format(ARGB8888/RBG565/ARGB4444/ALPHA8)等方式, 重新尝试
图片存在像素浪费对于. 9 图, 美工可能在出图时在拉伸与非拉伸区域都有大量的像素重复通过获取图片的像素 ARGB 值, 计算连续相同的像素区域, 自定义算法判定这些区域是否可以缩放关键也是需要将这些工作做到系统化, 可及时发现问题, 解决问题
一个好的 imageLoader, 可以将 2.X4.X 或 5.X 对图片加载的处理对使用者隐藏, 同时也可以将自适应大小质量等放于框架中
二. 自身内存占用监控
对于系统函数 onLowMemory 等函数是针对整个系统而已的, 对于本进程来说, 其 dalvik 内存距离 OOM 的差值并没有体现, 也没有回调函数供我们及时释放内存假若能有那么一套机制, 可以实时监控进程的堆内存使用率, 达到设定值即关于通知相关模块进行内存释放, 这会大大的降低 OOM
实现原理这个其实比较简单, 通过 Runtime 获得 maxMemory, 而 totalMemory-freeMemory 即为当前真正使用的 dalvik 内存
Runtime.getRuntime().maxMemory(); Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
操作方式我们可以定期 (前台每隔 3 分钟) 去得到这个值, 当我们这个值达到危险值时(例如 80%), 我们应当主要去释放我们的各种 cache 资源(bitmap 的 cache 为大头), 同时显示的去 Trim 应用的 memory, 加速内存收集
WindowManagerGlobal.getInstance().startTrimMemory(TRIM_MEMORY_COMPLETE);
三. 使用多进程
对于 webview, 图库等, 由于存在内存系统泄露或者占用内存过多的问题, 我们可以采用单独的进程微信当前也会把它们放在单独的 tools 进程中
四. 上报 OOM 详细信息
当系统发生 OOM 的 crash 时, 我们应当上传更加详细的内存相关信息, 方便我们定位当时内存的具体情况
其他例如使用 large heapinBitmapSparseArrayProtobuf 等不再一一细述, 对代码采用
优化 -- 埋坑 -- 优化 -- 埋坑
的方式并不推荐我们应该着力于建立一套合理的框架与监控体系, 能及时的发现诸如 bitmap 过大像素浪费内存占用过大应用 OOM 等问题
GC 优化
Java 拥有 GC 的机制, 不同的系统版本 GC 的实现可能有比较大的差异但是无论哪种版本, 大量的 GC 操作则会显著占用帧间隔时间 (16ms) 如果在帧间隔时间里面做了过多的 GC 操作, 那么自然其他类似计算, 渲染等操作的可用时间就变得少了
一. GC 的类型
GC 的类型有以下几种, 其中 GC_FOR_ALLOC 是同步方式进行, 对应用帧率的影响最大
GC_FOR_ALLOC 当堆内存不够的时候容易被触发, 尤其是 new 一个对象的时候, 很容易被触发到, 所以如果要加速启动, 可以提高 dalvik.vm.heapstartsize 的值, 这样在启动过程中可以减少 GC_FOR_ALLOC 的次数注意这个触发是以同步的方式进行的如果 GC 后仍然没有空间, 则堆进行扩张
GC_EXPLICIT 这个 gc 是被可以调用的, 比如 system.gc, 一般 gc 线程的优先级比较低, 所以这个垃圾回收的过程不一定会马上触发, 千万不要认为调用了 system.gc, 内存的情况就能有所好转
GC_CONCURRENT 当分配的对象大小超过 384K 时触发, 注意这是以异步的方式进行回收的. 如果发现大量反复的 Concurrent GC 出现, 说明系统中可能一直有大于 384K 的对象被分配, 而这些往往是一些临时对象, 被反复触发了给到我们的暗示是: 对象的复用不够
GC_EXTERNAL_ALLOC (在 3.0 系统之后被废了)Native 层的内存分配失败了, 这类 GC 就会被触发如果 GPU 的纹理 bitmap 或者 java.nio.ByteBuffers 的使用没有释放, 这种类型的 GC 往往会被频繁触发
二. 内存抖动现象
Memory Churn 内存抖动, 内存抖动是因为在短时间内大量的对象被创建又马上被释放瞬间产生大量的对象会严重占用内存区域, 当达到阀值, 剩余空间不够的时候, 会触发 GC 从而导致刚产生的对象又很快被回收即使每次分配的对象占用了很少的内存, 但是他们叠加在一起会增加 Heap 的压力, 从而触发更多其他类型的 GC 这个操作有可能会影响到帧率, 并使得用户感知到性能问题
通过 Memory Monitor, 我们可以跟踪整个 app 的内存变化情况若短时间发生了多次内存的涨跌, 这意味着很有可能发生了内存抖动
三. GC 优化
通过 Heap Viewer, 我们可以查看当前内存快照, 便于对比分析哪些对象有可能发生了泄漏更重要的工具是 Allocation Tracker, 追踪内存对象的类型堆栈大小等手 Q 有做一个统计工具, 对 Allocation Tracker 的原始数据, 按照 (类型 & 堆栈) 的组合 (堆栈取栈顶的 5 层) 统计某一种对象分配的大小次数同时按照次数大小的排序, 从多 / 大到少 / 小结合代码分析, 并自顶向下的逐轮进行优化
这样, 我们就可以快速知道发生内存抖动时, 是因为哪些变量的创建造成频繁 GC 一般来说我们需要注意以下几个方面:
字符串拼接优化减少字符串使用加号拼接, 改为使用 StringBuilder 减少 StringBuilder.enlarge, 初始化时设置 capacity; 这里需要注意的是, 若打开 Looper 中 Printer 回调, 也会存在较多的字符串拼接
- Printer logging = me.mLogging;
- if (logging != null) {
- logging.println(">>>>> Dispatching to" + msg.target + " " +
- msg.callback + ":" + msg.what);
- }
读文件优化 读文件使用 ByteArrayPool, 初始设置 capacity, 减少 expand
资源重用建立全球缓存池, 对频繁申请释放的对象类型重用
减少不必要或不合理的对象例如在 ondrawgetview 中应减少对象申请, 尽量重用更多是一些逻辑上的东西, 例如循环中不断申请局部变量等
选用合理的数据格式 使用 SparseArray, SparseBooleanArray, and LongSparseArray 来代替 Hashmap
总结
我们并不能将内存优化中用到的所有技巧都一一说明, 而且随着 Android 版本的更替, 可能很多方法都会变的过时我在想更重要的是我们能持续的发现问题, 精细化的监控, 而不是一直处于 "哪个有坑填哪里的" 的窘况在这里给大家的建议有:
率先考虑采用已有的工具; 中国人喜欢重复造轮子, 我们更推荐花精力去优化已有工具, 为广大码农做贡献生活已不易, 码农何为为难码农!
不拘泥于点, 更重要在于如何建立合理的框架避免发生问题, 或者是能及时的发现问题
当前微信内存监控体系中也存在一些不尽人意的地方, 在未来的日子里也同样需要努力去优化
参考文章
Android 内存管理(developer.android.com/intl/zh-cn/)
- leakcanary(github.com/square/leak)
- AndroidExcludedRefs(github.com/square/leak)
- fresco(github.com/facebook/fr)
优化安卓应用内存的神秘方法以及背后的原理(bugly.qq.com/blog/?p=621)
Android 性能优化之内存篇(hukai.me/android-per)
来源: https://juejin.im/entry/5a8fc6d551882524713df9e9