您的当前位置:首页正文

Android内存优化杂谈

来源:个人技术集锦
Android内存优化杂谈

Android内存优化是我们性能优化⼯作中⽐较重要的⼀环,这⾥其实主要包括两⽅⾯的⼯作:

1、优化RAM,即降低运⾏时内存。这⾥的⽬的是防⽌程序发⽣OOM异常,以及降低程序由于内存过⼤被LMK机制杀死的概率。另⼀⽅⾯,不合理的内存使⽤会使GC⼤⼤增多,从⽽导致程序变卡。

2、优化ROM,即降低程序占ROM的体积。这⾥主要是为了降低程序占⽤的空间,防⽌由于ROM空间不⾜导致程序⽆法安装。

本⽂的着重点为第⼀点,总结概述降低应⽤运⾏内存的技巧。在这⾥我们不再细述PSS、USS等概念与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的建议去修复。在微信中,对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:

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内存占⽤,想说的有以下⼏点:1、防⽌bitmap占⽤资源多⼤导致OOM

Android 2.x 系统 BitmapFactory.Options ⾥⾯隐藏的的inNativeAlloc反射打开后,申请的bitmap就不会算在external中。对于Android 4.x系统,可采⽤facebook的fresco库,即可把图⽚资源放于native中。2、图⽚按需加载

即图⽚的⼤⼩不应该超过view的⼤⼩。在把图⽚载⼊内存之前,我们需要先计算出⼀个合适的inSampleSize缩放⽐例,避免不必要的⼤图载⼊。对此,我们可以重载drawable与ImageView,例如在Activity ondestroy时,检测图⽚⼤⼩与View的⼤⼩,若超过,可以上报或提⽰。3、统⼀的bitmap加载器

Picasso、Fresco都是⽐较出名的加载库,同样微信也有⾃⼰的库ImageLoader。加载库的好处在于将版本差异、⼤⼩处理对使⽤者不感知。有了统⼀的bitmap加载器,我们可以在加载bitmap时,若发⽣OOM(try catch⽅式),可以通过清除cache,降低bitmap format(ARGB8888/RBG565/ARGB4444/ALPHA8)等⽅式,重新尝试。4、图⽚存在像素浪费

对于.9图,美⼯可能在出图时在拉伸与⾮拉伸区域都有⼤量的像素重复。通过获取图⽚的像素ARGB值,计算连续相同的像素区域,⾃定义算法判定这些区域是否可以缩放。关键也是需要将这些⼯作做到系统化,可及时发现问题,解决问题。⼀个好的imageLoader,可以将2.X、4.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 heap、inBitmap、SparseArray、Protobuf等不再⼀⼀细述,对代码采⽤优化--埋坑--优化--埋坑的⽅式并不推荐。我们应该着⼒于建⽴⼀套合理的框架与监控体系,能及时的发现诸如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,我们可以查看当前内存快照,便于对⽐分析哪些对象有可能发⽣了泄漏。更重要的⼯具是AllocationTracker,追踪内存对象的类型、堆栈、⼤⼩等。⼿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资源重⽤

建⽴全球缓存池,对频繁申请、释放的对象类型重⽤

减少不必要或不合理的对象

例如在ondraw、getview中应减少对象申请,尽量重⽤。更多是⼀些逻辑上的东西,例如循环中不断申请局部变量等

选⽤合理的数据格式 使⽤SparseArray, SparseBooleanArray, and LongSparseArray来代替Hashmap总结

我们并不能将内存优化中⽤到的所有技巧都⼀⼀说明,⽽且随着Android版本的更替,可能很多⽅法都会变的过时。我在想更重要的是我们能持续的发现问题,精细化的监控,⽽不是⼀直处于\"哪个有坑填哪⾥的\"的窘况。在这⾥给⼤家的建议有:1、率先考虑采⽤已有的⼯具;中国⼈喜欢重复造轮⼦,我们更推荐花精⼒去优化已有⼯具,为⼴⼤码农做贡献。⽣活已不易,码农何为为难码农!

2、不拘泥于点,更重要在于如何建⽴合理的框架避免发⽣问题,或者是能及时的发现问题。当前微信内存监控体系中也存在⼀些不尽⼈意的地⽅,在未来的⽇⼦⾥也同样需要努⼒去优化。以上就是本⽂的全部内容,希望对⼤家优化Android内存有所帮助。

因篇幅问题不能全部显示,请点此查看更多更全内容