如果你对以上基础内容都比较了解了,那么我们便开始Android内存优化的探索之旅吧。
内存达到阈值后自动触发HprofDump,将得到的Hprof存档后由人工通过MAT进行分析。
检测和分析报告都在一起,批量自动化测试和事后分析都不太方便。
目前,它的主要功能有三个部分,如下所示:
方便通过减少冗余Bitmap的数量,以降低内存消耗。
在研发阶段需要不断实现更多的工具和组件,以此系统化地提升自动化程度,以最终提升发现问题的效率。
除了常用的内存分析工具MemoryProfiler、MAT、LeakCanary之外,还有一些其它的内存分析工具,下面我将一一为大家进行介绍。
接下来,我们输入以下命令查看top命令的用法:
quchao@quchaodeMacBook-Pro~%adbshelltop--helpusage:top[-Hbq][-kFIELD,][-oFIELD,][-sSORT][-nNUMBER][-dSECONDS][-pPID,][-uUSER,]Showprocessactivityinrealtime.-H Showthreads-k FallbacksortFIELDS(default-S,-%CPU,-ETIME,-PID)-o ShowFIELDS(defPID,USER,PR,NI,VIRT,RES,SHR,S,%CPU,%MEM,TIME+,CMDLINE)-O AddFIELDS(replacingPR,NI,VIRT,RES,SHR,Sfromdefault)-s Sortbyfieldnumber(1-X,default9)-b Batchmode(notty)-d DelaySECONDSbetweeneachcycle(default3)-n ExitafterNUMBERiterations-p ShowthesePIDs-u ShowtheseUSERs-q Quiet(noheaderlines)CursorLEFT/RIGHTtochangesort,UP/DOWNmovelist,spacetoforceupdate,Rtoreversesort,Qtoexit.这里使用top仅显示一次进程信息,以便来讲解进程信息中各字段的含义。
前四行是当前系统情况整体的统计信息区。下面我们看每一行信息的具体意义。
具体信息说明如下所示:
系统现在共有729个进程,其中处于运行中的有1个,715个在休眠(sleep),stoped状态的有0个,zombie状态(僵尸)的有8个。
具体信息如下所示:
具体属性说明如下所示:
对于内存监控,在top里我们要时刻监控第三行swap交换分区的used,如果这个数值在不断的变化,说明内核在不断进行内存和swap的数据交换,这是真正的内存不够用了。
在第五行及以下,就是各进程(任务)的状态监控,项目列信息说明如下所示:
在讲解dumpsysmeminfo命令之前,我们必须先了解下Android中最重要的四大内存指标的概念,如下表所示:
从上可知,它们之间内存的大小关系为VSS>=RSS>=PSS>=USS。
RSS与PSS相似,也包含进程共享内存,但比较麻烦的是RSS并没有把共享内存大小全都平分到使用共享的进程头上,以至于所有进程的RSS相加会超过物理内存很多。而VSS是虚拟地址,它的上限与进程的可访问地址空间有关,和当前进程的内存使用关系并不大。比如有很多的map内存也被算在其中,我们都知道,file的map内存对应的可能是一个文件或硬盘,或者某个奇怪的设备,它与进程使用内存并没有多少关系。
而PSS、USS最大的不同在于“共享内存“(比如两个App使用MMAP方式打开同一个文件,那么打开文件而使用的这部分内存就是共享的),USS不包含进程间共享的内存,而PSS包含。这也造成了USS因为缺少共享内存,所有进程的USS相加要小于物理内存大小的原因。
最早的时候官方就推荐使用PSS曲线图来衡量App的物理内存占用,而Android4.4之后才加入USS。但是PSS,有个很大的问题,就是”共享内存“,考虑一种情况,如果A进程与B进程都会使用一个共享SO库,那么So库中初始化所用掉的那部分内存就会平分到A与B的头上。但是A是在B之后启动的,那么对于B的PSS曲线而言,在A启动的那一刻,即使B没有做任何事情,也会出现一个比较大的阶梯状下滑,这会给用曲线图分析软件内存的行为造成致命的麻烦。
USS虽然没有这个问题,但是由于Dalvik虚拟机申请内存牵扯到GC时延和多种GC策略,这些都会影响到曲线的异常波动。例如异步GC是Android4.0以上系统很重要的特性,但是GC什么时候结束?曲线什么时候”降低“?就无法预计了。还有GC策略,什么时候开始增加Dalvik虚拟机的预申请内存大小(Dalvik启动时是有一个标称的start内存大小,它是为Java代码运行时预留的,避免Java运行时再申请而造成卡顿),但是这个预申请大小是动态变化的,这一点也会造成USS忽大忽小。
了解完Android内存的性能指标之后,下面我们便来说说dumpsysmeminfo这个命令的用法,首先我们输入adbshelldumpsysmeminfo-h查看它的帮助文档:
quchao@quchaodeMacBook-Pro~%adbshelldumpsysmeminfo-hmeminfodumpoptions:[-a][-d][-c][-s][--oom][process]-a:includeallavailableinformationforeachprocess.-d:includedalvikdetails.-c:dumpinacompactmachine-parseablerepresentation.-s:dumponlysummaryofapplicationmemoryusage.-S:dumpalsoSwapPss.--oom:onlyshowprocessesorganizedbyoomadj.--local:onlycollectdetailslocally,don'tcallprocess.--package:interpretprocessargaspackage,dumpingallprocessesthathaveloadedthatpackage.--checkin:dumpdataforacheckinIf[process]isspecifieditcanbethenameorpidofaspecificprocesstodump.接着,我们之间输入adbshelldumpsysmeminfo命令:
此外,为了查看单个App进程的内存信息,我们可以输入如下命令:
如果Views与Activities、AppContexts持续上升,则表明有内存泄漏的风险。
LeakInspector是腾讯内部的使用的一站式内存泄漏解决方案,它是Android手机经过长期积累和提炼、集内存泄漏检测、自动修复系统Bug、自动回收已泄露Activity内资源、自动分析GC链、白名单过滤等功能于一体,并深度对接研发流程、自动分析责任人并提缺陷单的全链路体系。
它们之间主要有四个方面的不同,如下所示:
它们都支持对Activity、Fragment及其它自定义类的泄漏检测,但是,LeakInspector还增加了Btiamp的检测能力,如下所示:
这一个部分的实现原理,我们可以采用ARTHook的方式来实现,还不清楚的朋友请再仔细看看大图检测的部分。
两个工具的泄漏检测原理都是在onDestroy时检查弱引用,不同之处在于LeakInspector直接使用WeakReference来检测对象是否已经被释放,而LeakCanary则使用ReferenceQueue,两者效果是一样的。
并且针对Activity,我们通常都会使用Application的registerActivityLifecycleCallbacks来注册Activity的生命周期,以重写onActivityDestroyed方法实现。但是在Android4.0以下,系统并没有提供这个方法,为了避免手动在每一个Activity的onDestroy中去添加这份代码,我们可以使用反射Instrumentation来截获onDestory,以降低接入成本。代码如下所示:
这里的白名单是为了处理一些系统引起的泄漏问题,以及一些因为业务逻辑要开后门的情形而设置的。分析时如果碰到白名单上标识的类,则不对这个泄漏做后续的处理。二者的配置差异有如下两点:
针对系统泄漏,LeakInspector通过反射自动修复了目前碰到的一些系统泄漏,只要在onDestory里面调用一个修复系统泄漏的方法即可。而LeakCanary虽然能识别系统泄漏,但是它仅仅对该类问题给出了分析,没有提供实际可用的解决方案。
如果检测到发生了内存泄漏,LeakInspector会对整个Activity的View进行遍历,把图片资源等一些占内存的数据释放掉,保证此次泄漏只会泄漏一个Activity的空壳,尽量减少对内存的影响。代码大致如下所示:
if(ViewinstanceofImageView){//ImageViewImageButton处理recycleImageView(app,(ImageView)view);}elseif(viewinstanceofTextView){//释放TextView、Button周边图片资源recycleTextView((TextView)view);}elseif(ViewinstanceofProgressBar){recycleProgressBar((ProgressBar)view);}else{if(viewinstancofandroid.widget.ListView){recycleListView((android.widget.ListView)view);}elseif(viewinstanceofandroid.support.v7.widget.RecyclerView){recycleRecyclerView((android.support.v7.widget.RecyclerView)view);}elseif(viewinstanceofFrameLayout){recycleFrameLayout((FrameLayout)view);}elseif(viewinstanceofLinearLayout){recycleLinearLayout((LinearLayout)view);}if(viewinstanceofViewGroup){recycleViewGroup(app,(ViewGroup)view);}}这里以recycleTextView为例,它回收资源的方式如下所示:
privatestaticvoidrecycleTextView(TextViewtv){Drawable[]ds=tv.getCompoundDrawables();for(Drawabled:ds){if(d!=null){d.setCallback(null);}}tv.setCompoundDrawables(null,null,null,null);//取消焦点,让Editor$Blink这个Runnable不再被post,解决内存泄漏。tv.setCursorVisible(false);}三、后期处理不同1、分析与展示采集dump之后,LeakInspector会上传dump文件,并*调用MAT命令行来进行分析*,得到这次泄漏的GC链。而LeakCanary则用开源组件HAHA来分析得到一个GC链。但是LeakCanary得到的GC链包含被hold住的类对象,一般都不需要用MAT打开Hporf即可解决问题。而LeakInpsector得到的GC链只有类名,还需要MAT打开Hprof才能具体去定位问题,不是很方便。
LeakInspector在dump分析结束之后,会提交缺陷单,并且把缺陷单分配给对应类的负责人。如果发现重复的问题则更新旧单,同时具备重新打开单等状态转换逻辑。而LeakCanary仅会在通知栏提醒用户,需要用户自己记录该问题并做后续处理。
LeakInspector跟自动化测试可以无缝结合,当自动化脚本执行中发现内存泄漏,可以由它采集dump并发送到服务进行分析,最后提单,整个流程是不需要人力介入的。而LeakCanary则把分析结果通过通知栏告知用户,需要人工介入才能进入下一个流程。
JHat是Oracle推出的一款Hprof分析软件,它和MAT并称为Java内存静态分析利器。不同于MAT的单人界面式分析,jHat使用多人界面式分析。它被内置在JDK中,在命令行中输入jhat命令可查看有没有相应的命令。
启动服务器后,我们打开入口地址127.0.0.1:7000即可查看AllClasses界面,如下图所示:
jHat还有两个比较重要的功能,分别如下所示:
打开127.0.0.1:7000/histo/,统计表界面如下所示:
可以到,按TotalSize降序排列了所有的Class,并且,我们还可以查看到每一个Class与之对应的实例数量。
OQL是一种模仿SQL语句的查询语句,通常用来查询某个类的实例数量,打开127.0.0.1:7000/oql/并输入java.lang.String查询String实例的数量,结果如下图所示:
JHat比MAT更加灵活,且符合大型团队安装简单、团队协作的需求。但是,并不适合中小型高效沟通型团队使用。
GC产生的原因有如下九种:
GC类型有如下三种:
GC采集的方法有如下四种:
通过GC日志,我们可以知道GC的量和它对卡顿的影响,也可以初步定位一些如主动调用GC、可分配的内存不足、过多使用WeakReference等问题。
对于HTML5页面而言,抓取JavaScript的内存需要使用ChromeDevtools来进行远程调试。方式有如下两种:
Android4.4及以上系统的原生浏览器就是Chrome浏览器,可以使用ChromeDevtool远程调试WebView,前提是需要在App的代码里把调试开关打开,如下代码所示:
这里总结一下JS中几种常见的内存问题点:
在我们进行内存优化的过程中,有许多内存问题都可以归结为一类问题,为了便于以后快速地解决类似的内存问题,我将它们归结成了以下的多个要点:
解决方案是在Activity关闭,即触发onDestory时解除内类和外部的引用关系。
这也是一个this$0间接引用的问题,对于Handler的解决方案一般可以归结为如下三个步骤:
这里需要在使用过程中注意对WeakReference进行判空。
我们通常都会使用getSystemService方法来获取系统服务,但是当在Activity中调用时,会默认把Activity的Context传给系统服务,在某些不确定的情况下,某些系统服务内部会产生异常,从而hold住外界传入的Context。
解决方案是直接使用Applicaiton的Context去获取系统服务。
我们都知道,对应WebView来说,其网络延时、引擎Session管理、Cookies管理、引擎内核线程、HTML5调用系统声音、视频播放组件等产生的引用链条无法及时打断,造成的内存问题基本上可以用”无解“来形容。
解决方案是我们可以把WebView装入另一个进程。具体为在AndroidManifes中对当前的Activity设置android:process属性即可,最后,在Activity的onDestory中退出进程,这样即可基本上终结WebView造成的泄漏。
我们在平常开发过程中经常需要在Activity创建的时候去注册一些组件,如广播、定时器、事件总线等等。这个时候我们应该在适当的时候对组件进行注销,如onPause或onDestory方法中。
不仅在使用Handler的sendMessage方法时,我们需要在onDestory中使用removeCallbackAndMessage移除回调和消息,在使用到Handler/FrameLayout的postDelyed方法时,我们需要调用removeCallbacks去移除实现控件内部的延时器对Runnable内类的持有。
在做资源适配的时候,因为需要考虑到APK的瘦身问题,无法为每张图片在每个drawable/mipmap目录下安置一张适配图片的副本。很多同学不知道图片应该放哪个目录,如果放到分辨率低的目录如hdpi目录,则可能会造成内存问题,这个时候建议尽量问设计人员要高品质图片然后往高密度目录下方,如xxhdpi目录,这样在低密屏上”放大倍数“是小于1的,在保证画质的前提下,内存也是可控的。也可以使用Drawable.createFromSream替换getResources().getDrawable来加载,这样便可以绕过Android的默认适配规则。
对于已经被用户使用物理“返回键”退回到后台的进程,如果包含了以下两点,则不会被轻易杀死。
我们应该在item被回收不可见时去释放掉对图片的引用。如果你使用的是ListView,由于每次item被回收后被再次利用都会去重新绑定数据,所以只需在ImageView回调其onDetchFromWindow方法的时候区释放掉图片的引用即可。如果你使用的是RecyclerView,因为被回收不可见时第一次选择是放进mCacheView中,但是这里面的item被复用时并不会去执行bindViewHolder来重新绑定数据,只有被回收进mRecyclePool后拿出来复用才会重新绑定数据。所以此时我们应该在item被回收进RecyclePool的时候去释放图片的引用,这里我们只要去重写Adapter中的onViewRecycled方法就可以了,代码如下所示:
@OverridepublicvoidonViewRecycled(@NullableVHholder){super.onViewRecycled(holder);if(holder!=null){//做释放图片引用的操作}}10、使用ViewStub进行占位我们应该使用ViewStub对那些没有马上用到的资源去做延迟加载,并且还有很多大概率不会出现的View更要去做懒加载,这样可以等到要使用时再去为它们分配相应的内存。
产品或者运营为了统计数据会在每个版本中不断地增加新的埋点。所以我们需要定期地去清理一些过时的埋点,以此来适当地优化内存以及CPU的压力。
我们在做子线程操作的时候,喜欢使用匿名内部类Runnable来操作。但是,如果某个Activity放在线程池中的任务不能及时执行完毕,在Activity销毁时很容易导致内存泄漏。因为这个匿名内部类Runnable类持有一个指向Outer类的引用,这样一来如果Activity里面的Runnable不能及时执行,就会使它外围的Activity无法释放,产生内存泄漏。从上面的分析可知,只要在Activity退出时没有这个引用即可,那我们就通过反射,在Runnable进入线程池前先干掉它,代码如下所示:
Fieldf=job.getClass().getDeclaredField("this$0");f.setAccessible(true);f.set(job,null);这个任务就是我们的Runnable对象,而”this$0“就是上面所指的外部类的引用了。这里注意使用WeakReference装起来,要执行了先get一下,如果是null则说明Activity已经回收,任务就放弃执行。
我们发现我们的APP在内存方面可能存在很大的问题,第一方面的原因是我们的线上的OOM率比较高。
第二点呢,我们经常会看到在我们的AndroidStudio的Profiler工具中内存的抖动比较频繁。
这是我们一个初步的现状,然后在我们知道了这个初步的现状之后,进行了问题的确认,我们经过一系列的调研以及深入研究,我们最终发现我们的项目中存在以下几点大问题,比如说:内存抖动、内存溢出、内存泄漏,还有我们的Bitmap使用非常粗犷。
比如内存抖动的解决=>MemoryProfiler工具的使用(呈现了锯齿张图形)=>分析到具体代码存在的问题(频繁被调用的方法中出现了日志字符串的拼接),也可以说说内存泄漏或内存溢出的解决。
为了不增加业务同学的工作量,我们使用了一些工具类或ARTHook这样的大图检测方案,没有任何的侵入性。同时,我们将这些技术教给了大家,然后让大家一起进行工作效率上的提升。
我们一开始并没有直接去分析项目中代码哪些地方存在内存问题,而是先去学习了Google官方的一些文档,比如说学习了MemoryProfiler工具的使用、学习了MAT工具的使用,在我们将这些工具学习熟练之后,当在我们的项目中遇到内存问题时,我们就能够很快地进行排查定位问题进行解决。
一开始,我们做了整体APP运行阶段的一个内存上报,然后,我们在一些重点的内存消耗模块进行了一些监控,但是,后面发现这些监控并没有紧密地结合我们的业务代码,比如说在梳理完项目之后,发现我们项目中存在使用多个图片库的情况,多个图片库的内存缓存肯定是不公用的,所以导致我们整个项目的内存使用量非常高。所以进行技术优化时必须结合我们的业务代码。
我们在做内存优化的过程中,不仅做了Android端的优化工作,还将我们Android端一些数据的采集上报到了我们的服务器,然后传到我们的APM后台,这样,方便我们的无论是Bug跟踪人员或者是Crash跟踪人员进行一系列问题的解决。
比如说大图片的检测,我们最初的一个方案是通过继承ImageView,重写它的onDraw方法来实现。但是,我们在推广它的过程中,发现很多开发人员并不接受,因为很多ImageView之前已经写过了,你现在让他去替换,工作成本是比较高的。所以说,后来我们就想,有没有一种方案可以免替换,最终我们就找到了ARTHook这样一个Hook的方案。
对于内存优化的专项优化而言,我们要着重注意两点,即优化大方向和优化细节。
对于优化的大方向,我们应该优先去做见效快的地方,主要有以下三部分:
对于优化细节,我们应该注意一些系统属性或内存回调的使用等等,主要可以细分为如下六部分:
最后,当监控到应用内存超过阈值时,还定制了完善的兜底策略来重启应用进程。
总的来看,要建立一套全面且成体系的内存优化及监控是非常重要也是极具挑战性的一项工作。并且,目前各大公司的内存优化体系也正处于不断演进的历程之中,其目的不外乎:实现更健全的功能、更深层次的定位问题、快速准确地发现线上问题。