作者:shwenzhang
Android 内存优化是我们性能优化工作中比较重要的一环,这里其实主要包括两方面的工作:
本文的着重点为第一点,总结概述降低应用运行内存的技巧。在这里我们不再细述PSS、USS等概念与Android应用的内存管理,如对这部分内容感兴趣,可自行阅读文末的参考文章。
内存泄露:简单来说对象由于编码错误或系统原因,仍然存在着对其直接或间接的引用,导致系统无法进行回收。内存泄露,容易留下逻辑隐患,同时增加了应用内存峰值与发生OOM的概率。它属于bug issue,是我们一定要修改的。
下面是造成内存泄露的一些常见原因,但是如何建立一套发现内存泄露、解决内存泄露的闭环方案,才是我们工作的重点。
Square的开源库leakcanry是一个非常不错的选择,它通过弱引用方式侦查Activity或对象的生命周期,若发现内存泄露自动dump Hprof文件,通过HAHA库得到泄露的最短路径,最后通过notification展示。
内存泄露判断与处理的流程如下图 ,各自运行的进程空间(主进程通过idlehandler,HAHA分析使用的是单独的进程):
微信在leakcanry推出之前已经有了自己的内存泄露监控体系,与leakcanry大致有以下的区别:
事实上,通过对leakcanry做简单的定制,我们就可以实现以下一个内存泄露监控闭环。
AndroidExcludedRefs列出了一些由于系统原因导致引用无法释放的例子,同时对于大多数的例子,都会提供建议如何通过hack的建议去修复。在微信中,对TextLine、InputMethodManager、AudioManger、android.os.Message也采用了类似Hack的方式(详细可看参考资料)。
Activity泄漏会导致该Activity引用到的Bitmap、DrawingCache等无法释放,对内存造成大的压力,兜底回收是指对于已泄漏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:
说到内存,bitmap必然是这里的大头。对于bitmap内存占用,想说的有以下几点:
一个好的imageLoader,可以将2.X、4.X或5.X对图片加载的处理对使用者隐藏,同时也可以将自适应大小、质量等放于框架中。
对于系统函数onLowMemory等函数是针对整个系统而已的,对于本进程来说,其dalvik内存距离OOM的差值并没有体现,也没有回调函数供我们及时释放内存。假若能有那么一套机制,可以实时监控进程的堆内存使用率,达到设定值即关于通知相关模块进行内存释放,这会大大的降低OOM。
Runtime.getRuntime().maxMemory(); Runtime.getRuntime().totalMemory() -
Runtime.getRuntime().freeMemory();
WindowManagerGlobal.getInstance().startTrimMemory(TRIM_MEMORY_COMPLETE);
对于webview,图库等,由于存在内存系统泄露或者占用内存过多的问题,我们可以采用单独的进程。微信当前也会把它们放在单独的tools进程中
当系统发生OOM的crash时,我们应当上传更加详细的内存相关信息,方便我们定位当时内存的具体情况。
其他例如使用large heap、inBitmap、SparseArray、Protobuf等不再一一细述,对代码采用优化--埋坑--优化--埋坑的方式并不推荐。我们应该着力于建立一套合理的框架与监控体系,能及时的发现诸如bitmap过大、像素浪费、内存占用过大、应用OOM等问题。
Java拥有GC的机制,不同的系统版本GC的实现可能有比较大的差异。但是无论哪种版本,大量的GC操作则会显著占用帧间隔时间(16ms)。如果在帧间隔时间里面做了过多的GC操作,那么自然其他类似计算,渲染等操作的可用时间就变得少了。
GC的类型有以下几种,其中GC_FOR_ALLOC是同步方式进行,对应用帧率的影响最大。
Memory Churn内存抖动,内存抖动是因为在短时间内大量的对象被创建又马上被释放。瞬间产生大量的对象会严重占用内存区域,当达到阀值,剩余空间不够的时候,会触发GC从而导致刚产生的对象又很快被回收。即使每次分配的对象占用了很少的内存,但是他们叠加在一起会增加Heap的压力,从而触发更多其他类型的GC。这个操作有可能会影响到帧率,并使得用户感知到性能问题。
通过Memory Monitor,我们可以跟踪整个app的内存变化情况。若短时间发生了多次内存的涨跌,这意味着很有可能发生了内存抖动。
通过Heap Viewer,我们可以查看当前内存快照,便于对比分析哪些对象有可能发生了泄漏。更重要的工具是Allocation Tracker,追踪内存对象的类型、堆栈、大小等。手Q有做一个统计工具,对Allocation Tracker的原始数据,按照(类型&堆栈)的组合(堆栈取栈顶的5层)统计某一种对象分配的大小、次数。同时按照次数、大小的排序,从多/大到少/小结合代码分析,并自顶向下的逐轮进行优化。
这样,我们就可以快速知道发生内存抖动时,是因为哪些变量的创建造成频繁GC。一般来说我们需要注意以下几个方面:
1、字符串拼接优化 减少字符串使用加号拼接,改为使用StringBuilder。减少StringBuilder.enlarge,初始化时设置capacity;这里需要注意的是,若打开Looper中Printer回调,也会存在较多的字符串拼接。
Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
2、读文件优化 读文件使用ByteArrayPool,初始设置capacity,减少expand
3、资源重用 建立全球缓存池,对频繁申请、释放的对象类型重用
4、减少不必要或不合理的对象 例如在ondraw、getview中应减少对象申请,尽量重用。更多是一些逻辑上的东西,例如循环中不断申请局部变量等
5、选用合理的数据格式 使用SparseArray, SparseBooleanArray, and LongSparseArray来代替Hashmap
我们并不能将内存优化中用到的所有技巧都一一说明,而且随着Android版本的更替,可能很多方法都会变的过时。我在想更重要的是我们能持续的发现问题,精细化的监控,而不是一直处于"哪个有坑填哪里的"的窘况。在这里给大家的建议有:
[1].Android内存管理 [2].leakcanary [3].AndroidExcludedRefs [4].fresco [5].优化安卓应用内存的神秘方法以及背后的原理 [6].Android性能优化之内存篇
本文来源于:WeMobileDev 微信公众号
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。