最近对项目的性能进行测试优化,现在刚好有时间对内存泄漏整理下
什么是内存泄漏
Android 虚拟机的垃圾回收采用的是根搜索算法。GC会从根节点(GC Roots)开始对heap进行遍历。到最后,部分没有直接或者间接引用到GC Roots的就是需要回收的垃圾,会被GC回收掉(蓝色)。而内存泄漏出现的原因就是存在了无效的引用,导致本来需要被GC的对象没有被回收掉。也就是说没有用的对象资源任与GC-Root保持可达路径,导致系统无法进行回收(某个无用的黄色)。
一、Android Monitor
1、Memory Monitor
Memory Monitor可以提供
1:实时查看App内存分配情况;
2:App是否由于GC操作造成卡顿;
3:App是否由于内存不足造成闪退;
蓝色滚动短时间内发生掉落,说明是系统执行了GC,点击小黄车可以手动执行GC,在我们运行自己app时,进行操作时,若出现像A地区那样比较突然增加,比较陡的情景,就得注意下,刚才的操作,导致短时间内分配不少内存,应该分析是bitmap造成的,还是其他什么;这只能从大的方向看看,内存的变化,提供我们分析方向,
2、用Android Monitor对内存的检测,若是 Android Monitor 连接设备后 No Debuggable Processes时,菜单栏找到Tools -> Android -> Enable ADB Integration,选中打钩即可;
Monitors先对提供的四个按钮介绍下,上图1 2 3分别为Initiate GC,Dump Heap JAVA,Start Allocation Tracking,还有1前面的Enabled;
1>Enabled Memory的开关。如果选择关闭,则不对当前进程进行内存监测,默认开启。
2>Initiate GC手动调用GC,在抓内存前先点击下 Initiate GC按钮,手动触发GC,让系统回收内存,这样抓取到的内存就是没有被系统回收的,即泄漏的部分。
3>Dump Heap java点击这个按钮后,就在你点击的时刻,获取hprof文件,点击后稍等片刻,就会生成hprof文件
然后进入Heap Snapshot详细信息面板界面,查看App heap,Class List View下找相关activity,或切换到Package Tree View,根据包查看,点击Analyzer Task,点击绿色三角按钮即Perfom analysis执行分析,Android Monitor就可以为我们自动分析泄漏的Activity,将显示在Leaked Activities中,点击其条目在Reference Tree区域,查看该实例的引用信息,可以从这里面查看内存泄漏的原因,如上图,当前activity的引用,跟着target进入了消息队列,handle持有外部activity引用导致的内存泄漏,(this$0是内部类的意思,内部类的class文件格式:主类+$+内部类名,匿名类的class文件格式:主类+$(0,1,2..);
上图中间顶部A和B指向字段
A:hallow size就是对象本身占用内存的大小,不包含对其他对象的引用,也就是对象头加成员变量(不是成员变量的值)的总和。
B:Retained size是该对象自己的shallow size,加上从该对象能直接或间接访问到对象的shallow size之和。换句话说,retained size是该对象被GC之后所能回收到内存的总和。
此外Total Count:内存中该类的对象个数;Heap Count:堆内存中该类的对象个数;Sizeof:物理大小
右侧depth:深度;Shallow Size:对象本身内存大小;Dominating Size:管辖的内存大小
左上角支持三种类型:app heap/image heap/zygote heap.
分别代表app 堆内存信息,图片堆内存信息,zygote进程的堆内存信息。
上图左侧是生成的hprof文件,一会用Memory Analyzer分析
4>Start Allocaton Tracking
内存分配追踪,第一次点击指定追踪内存的开始位置,第二次点击结束追踪的位置。从而截取了一段要分析的内存,如下图顶部那段,稍等片刻,出现alloc视图.
主要分析了各个线程中的方法所占用内存的大小;
Group by Method:用方法来分类我们的内存分配
Group by Allocator:用内存分配器来分类我们的内存分配
不同选项下面中间的图会呈现不同的统计图。
在Group by Method下,是以线程对象分类,Size和Count都可以点击改变排序,Size就是内存大小,Count就是分配了多少次内存,点击一下线程就会查看每个线程里所有分配内存的方法,层级显示;
在Group by Allocator下,是以包分类,可以方便查看自己项目下内存分配情况。
选中类右击出现Jump To Source,前往源码,有源码直接跳转源码,否则灰色不可点;或者选中类,点击Jump To Source按钮。
底部的统计图,有两种效果样式,如上面的两个图,
一种是Sunburst:环形,由内向外,圆心为起点,向外每层为分配的不同对象,不同颜色端可以点击,来查看右侧具体线程分配了多少次,占用多大内存,Sunburst右边可以选择Size或Count,分别查按分配内存大小或按分配次数,来显示统计图,有人称为轮胎图;
另一种是Layout:以矩形模块显示,以左边为起点,从左到右是每个堆栈信息顺序,矩形模块的宽度以Count/Size的大小计算的,Layout右边也可以进行Size和Count切换,矩形模块也可以点击,来查看相应的分支;
二、Memory Analyzer(MAT)
Memory Analyzer下载地址https://www.eclipse.org/mat/downloads.php下载后运行MemoryAnalyzer.exe,file->Open Heap Dump选中.hprof文件,若是Android Memory Monitor 直接生成的.hprof文件,会报错;
因为Android Memory Monitor 生成的 hprof 文件不是标准格式,所以需要做一下转换,
然后导入 MAT
Leak Suspects
点击3位置三角按钮,选中Leak Suspects执行分析,结果如上图,饼图可看到可疑对象消耗内存的百分比,对于饼图各比例,灰色区域不需关心,它是除了大内存对象之外的其他对象,我们把侧重点放在彩色比例区域,鼠标放在色彩比例图上,单间,会出现一些菜单选项,操作一些功能。根据Problem Suspect 黄色区域内容提示,点击Details,可看点比较详细的信息,从根元素到内存消耗聚集点的最短路径和内存消耗聚集对象信息,根据这些提示结合代码,分析可能泄露的地方,如下图可能是LinkedHashMap存的对象导致泄露的;
Dominator tree
此外也可以点击下图A按钮的位置Dominator tree,会显示Class Name列表,根据列表涉及到的类和Shallow size 、Retained size、Percentage值大小,这里会以占总内存的百分比来列举所有实例对象,直接通过Percentage来发现占大内存的对象,条目可以展开,查看详情信息。
QQL
由于我们内存泄漏一般发生在Activity中,因此只需要查找Activity即可, 点击下图中标记的QQL图标 输入 select * from instanceof android.app.Activity 类似于 SQL语句,查找 Activity相关的信息,点击 红色叹号执行后,如下图,比较方便的找到可疑的类;
histogram
istogram视图主要是查看某个类的实例个数即Objects字段,以及占用内存大小和可释放内存的大小,当检查内存泄漏时候,通过是否频繁创建了对象,通过对对象的个数以及占用内存大的判断;A按钮,可以提高根据Group by class,Group by superclass,Group by class loader,Group by classs四种分类筛选;
三、Android Lint
Lint也挺实用的,一般在发布apk时,为了减少apk的大小,需要去除一些无用的资源,通过Lint进行扫描检测,找到无用资源(Refactor->Removed Unused Resources 这个操作可以直接把无用的资源直接移除),还可以检测国际化问题,manifest文件错误,标准化问题;也可以检测可能造成内存泄漏的问题;
Analyze-> inspect Code->选择检测项目在Performance下,会提示一些内存泄漏的可能地方,下图提示handle没有改为静态内部类,可能会造成内存泄漏,即非静态内部类会持有外部类的引用;
四、LeakCanary
LeakCanary的使用比较简单,按官网简单配置https://www.liaohuqiu.net/cn/posts/leak-canary-read-me/
1>在 build.gradle 中加入引用,不同的编译使用不同的引用
dependencies {
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3'
}
2>在 Application 中
public class ExampleApplication extends Application {在 debug build 中,如果检测到某个 activity 有内存泄露,LeakCanary 就是自动地显示一个通知。
@Override public void onCreate() {
super.onCreate();
LeakCanary.install(this);
}
}
配置好运行程序,随意调整界面,进行各种操作,若有泄漏,会在手机通知栏显示通知,有些手机由于设置问题,通知栏没消息,可直接在手机上点开小黄leaks,查看给的提示信息,如上图的信息,可以大概知道泄漏的问题方向,LeakCanary的每个提示也并非真正有内存泄漏,更准确的确认还需要用MAT进行分析。
五、Android Device Monitor DDMS
程序运行后,菜单栏找到Tools -> Android -> Android Device Monitor;每种操作,选中左侧的进程;若是 Android Monitor 连接设备后 No Debuggable Processes时,菜单栏找到Tools -> Android -> Enable ADB Integration,选中打钩即可;
1、查看线程有关信息
点击右侧Threads,左侧点击Update Threads,然后右侧就显示出有个应用中的线程信息
2、查看堆栈信息
1>点击左侧Update Heap按钮,然后在右侧Heap区,点击Cause GC,查看堆栈信息
A区:
Heap Size 堆栈分配给App的内存大小
Allocated 已分配使用的内存大小
Free 空闲的内存大小
%Used Allocated/Heap Size,使用率
Objects 对象数量
B区:
free 空闲的对象
data object 数据对象,类类型对象,最主要的观察对象
class object 类类型的引用对象
1-byte array(byte[],boolean[]) 一个字节的数组对象
2-byte array(short[],char[]) 两个字节的数组对象
4-byte array(long[],double[]) 4个字节的数组对象
non-Java object 非Java对象
C区:
Count数量
Total Size 总共占用的内存大小
Smallest 将对象占用内存的大小从小往大排,排在第一个的对象占用内存大小
Largest 将对象占用内存的大小从小往大排,排在最后一个的对象占用的内存大小
Median 将对象占用内存的大小从小往大排,拍在中间的对象占用的内存大小
Average 平均值
底部柱状图:
横坐标是对象的内存大小,这些值随着不同对象是不同的,纵坐标是在某个内存大小上的对象的数量。
我们根据这些如何检测呢?
Heap这些数值会在每次GC时会自动更新,正常情况下是系统根据内存使用情况进行GC,但是我们检测的时候就可以人为主动触发GC,即点击Cause GC,我们针对App某个操作,比如A界面跳转B界面,首先进入A界面,我们Cause GC,然后观察上图B区中的data object那列数据或者上图A区中Heap Size和Allocated的数值,然后跳转B界面,返回A界面,这样A界面跳转B界面返回,多次执行后,再GC,查看内存数值会不会回到某个稳定值,若操作多次后,内存数值稳定在某个值,说明没有内存溢出;若是每次GC后,发现内存数值增加,说明有可能发生了内存泄漏。
3、Allocaton Tracker内存追踪,它功能和上面介绍的Start Allocaton Tracking一样,但此处使用,需要root,先不介绍;
4、TraceView
TraceView可以帮我们通过分析每个函数消耗的时间多少来查找性能瓶颈,有卡顿的地方可能存在耗时太多。在DDMS下,左侧选中app进程,点击A处(红色圆点),开始收集数据,然后进行App操作,再点击A处(灰色的正方形),稍等片刻,进入TraceView分析界面;
TraceView分析界面分为上下两个模块时间线面板和函数分析面板;
时间线面板:如上图BCD指处,B处是测试数据中所采集的线程信息,main线程是Android应用的主线程,这个都有,其他线程根据操作不同有所改变,每个线程右边对应的是该线程中每个方法执行的信息即D模块,D处是时间线,时间线上是每个线程测试时间段内所涉及的函数调用信息,包括函数名称,执行时间等,D模块左边为第一个方法执行开始,最右边为最后一个方法执行结束,其中的每一个小立柱就代表一次方法的调用,你可以把鼠标放到立柱上,就会显示该方法调用的详细信息C处;查看某个方法调用,双击时间线上的立柱,下面会自动跳转改方法的详情如下图,选中立柱滑动可以放大显示,双击时间线可以恢复
函数分析面板:如上图E模块,涉及的数据参数比较多,主要展示了某个线程中各个函数的调用情况,包括CPU使用时间,调用次数,函数真实的执行时间等信息,这些信息是查找性能瓶颈的关键依据;涉及字段
Name 方法的详细信息,包括包名和参数信息
Incl Cpu Time Cpu执行该方法该方法及其子方法所花费的时间
Incl Cpu Time % Cpu执行该方法该方法及其子方法所花费占Cpu总执行时间的百分比
Excl Cpu Time Cpu执行该方法所话费的时间
Excl Cpu Time % Cpu执行该方法所花费的时间占Cpu总时间的百分比
Incl Real Time 该方法及其子方法执行所话费的实际时间,从执行该方法到结束一共花了多少时间
Incl Real Time % 上述时间占总的运行时间的百分比
Excl Real Time % 该方法自身的实际允许时间
Excl Real Time 上述时间占总的允许时间的百分比
Calls+Recur 调用次数+递归次数,只在方法中显示,在子展开后的父类和子类方法这一栏被下面的数据代替
Calls/Total 调用次数和总次数的占比
Cpu Time/Call Cpu执行时间和调用次数的百分比,代表该函数消耗cpu的平均时间
Real Time/Call 实际时间于调用次数的百分比,该表该函数平均执行时间
参数比较多,其中Incl(全称Inclusive)代表包含某函数中调用的子函数的执行时间;而Excl(全称Exclusive)代表不包含子函数执行的时间;
上图展开后,Parents代表该方法的父类方法,Children代表该方法调用的子类方法;此外若是方法包含递归的话Parents while recursive代表递归调用时所涉及的父类方法;Children while recursive代表递归调用时所涉及的子类方法;
这里测试的在绘制调用onDraw方法里多次调用了drawStarDynamic子方法,上图中看到,Incl Cpu Time%字段,Parents为100%,因为在这个地方,总时间为当前方法的执行时间,这个时候的Incl Cpu Time%只是计算该方法调用的总时间中被各父类方法调用的时间占比,比如Parents有多个父类方法,那就能看出每个父类方法调用该方法的时间分布。因为我们父类只有一个,所以肯定是100%,Children各方法Incl Cpu Time%总和等于Parents,Incl Cpu Time,Incl Real Time%,Excl Real Time字段对应值也是如此;通过这个方法耗时的时间百分比查看,最大值对应的方法,可能就是导致卡顿等问题的地方;