性能优化系列
内存分析工具
1.1 heap工具获得hprof文件
heap工具主要是用来检测堆内存的分配情况的。它可以导出一个hprof文件,这个是手机某个时间段的内存镜像,通过分析该文件,就可以得知堆内存的分配情况。heap工具位于Android Device Monitor中(这个Android Device Monitor在Eclipse中即DDMS界面)。
Android Studio中,在你要分析的应用已执行的前提下,通过在Tools→Android→Android Device Monitor中打开。如下图:
打开后,操作步骤顺序如下:
- 在Android Device Monitor界面中选择你要分析的应用程序的包名
- 点击Update Heap来更新统计信息
- 然后点击Cause GC即可查看当前堆的使用情况
- 点击Dump HPROF file,准备生成hprof文件
第4步点击后,几秒钟内会出现一个窗口提示你去保存文件,(这个应用当前的内存信息会被保存在这个hprof文件中),将文件保存在顺手的地方(比如说桌面)即可。
1.2 分析hprof文件(AS、eclipse插件MAT)
其实leakCanary内部也是通过分析hprof文件来进行内存泄露的判断。
1.2.1 通过Android Studio打开hprof文件
拿到一个hprof文件后,可以直接通过Android Studio来打开。只需将该文件拖放到Android Studio中,就打开了。
打开后选择Package Tree View,内存使用情况就是以包名分类。如下图
在界面中找到你的应用程序的包名,打开即可看到内存的使用情况。自己写的类一目了然,我们还可以借助右侧的Analyzer Task去做一些分析,比如说分析出存在泄漏的leakActivity,如下图所示:
1.2.2 通过MAT打开hprof文件
MAT工具(Memory Analysis Tools)其实是Eclipse时代的产物,它也是用来分析 hprof 文件的,不过LeakCanary可以帮助我们自动分析,而使用MAT需要我们自己去导出并且分析hprof 文件,使用的过程有些麻烦, 当前很多比较有经验的老程序员都知晓这个,如果面试的时候能和他们扯起这个,是一个加分项。
首先在eclipse上安装MAT插件:
1、去官方网站,查看最新版本的地址,当前最新地址如下:
- Update Site
- Archived Update Site: MemoryAnalyzer
2、打开eclipse,Help->Install New SoftWare, 输入上面的update site,如图:
安装完成后提示重启Eclipse,重启后打开window→ open perspective,看到Memory Analysis证明安装成功。
为了节省大家的时间,建议直接下载它的Stand-alone版本,免安装。
MAT的使用步骤如下:
首先需要Dump出一份hprof文件,可以在android studio或者eclipse导出。方法在上一节已介绍过(heap的介绍中)
直接Dump出的hprof文件要经过转换才能被 MAT识别,Android SDK提供了这个工具 hprof-conv, 在SDK目录下(sdk/platform-tools)
该工具需要通过命令行来进行转换。以下为参考转换流程。
1、将导出来的hprof文件放到此目录下(sdk/platform-tools),重命令为input.hprof。
2、命令行cd 到此目录下,然后输入命令:hprof-conv input.hprof out.hprof
PS:注意空格,前一段的hprof-conv代表要执行hprof-conv工具;中间的input.hprof代表你想对这个叫input.hprof的文件进行转换;最后那段out.hprof代表你转换出来的结果文件名叫做out.hprof
3、执行后,此目录下(sdk/platform-tools)将会出现一个新文件名为:out.hprof,它即是我们的转换后的结果文件。
4、打开MAT,导入我们的转换后的hprof文件(最好先将out.hprof放到一个独立的文件夹中,因为导入时MAT会在当前文件夹生成很多解析文件出来),导入完成后,先弹出如下start wizard 对话框,默认选第一个“leak suspect report”我们直接点finish。成功打开后如下图所示:
5、在OverView页项下面,点击 Actions下的 Histogram
6、将得到 Histogram结果,它按类名将所有的实例对象列出来。
7、在第一行的正则表达式中输入我们demo里面的类名MainActivity
8、选中匹配出的结果右键打开菜单选择 list objects->with incoming refs。
9、得到该类的实例,以及展开后可以看到它的引用路径
10、 快速找出MainActivity实例没被释放的原因,可以右健 Path to GCRoots–>exclue all phantom/weak/soft etc. reference
11、可以看到,MainActivity在AsyncTask中引用,没有被释放。
1.3 性能分析工具TraceView
TraceView工具可以帮助开发者找到代码的性能瓶颈。方便我们找到一些影响到我们应用性能的代码。比如说某段代码相对其他代码而言,执行时间过长,它就能检测出来,那我们就能对症下药了。
1.3.1 第一种使用方式
该工具和heap工具一样,也位于Android Device Monitor中。使用步骤如下:
1、打开Android Device Monitor,选中我们的app进程,然后按上面的“Start Method Profiling”按钮,如图。
2、点击带红点的按钮后,小红点变黑色,表示TraceView已经开始工作,然后我们就可以操作我们的APP,点击我们demo的按钮,开始阻塞任务。完成操作后点击我们已经变成黑色的“Start Method Profiling”按钮,停止TraceView,生成此操作的Trace,如图:
PS:在实际的项目开发中,我们想检测某一个业务功能的性能,一般是先start trace,然后直接触发某个操作,最好是最小范围的操作,接着stop trace。最后拿到这个“最小操作范围”的trace来分析,跟踪我们的代码瓶颈
1.3.2 第二种使用方式
除了上面针对某操作跟踪之外,我们还可以针对一段代码进行跟踪。
1、比如当我们怀疑某一段代码的性能时,我们可以直接监听这一段代码。
// 开始监听......(需要被监听的代码段)......(需要被监听的代码段)
android.os.Debug.startMethodTracing("xmgTest");
android.os.Debug.stopMethodTracing();//结束监听
2、当代码执行完这一段时,会在sdcard目录下生成一个trace文件(如果按照先前的代码,那么该文件名为:xmgTest.trace)。为了顺利生成,我们需要在我们的程序中添加sd卡的访问权限。
<!-- 往Mainfest文件中添加SD卡写入权限和添加删除文件的权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
3、在SD卡生成的trace文件如下图:
4、将trace文件导出到电脑上,我们可以在我们的DDMS打开,File->open file , 然后选择我们导出的trace文件
DDMS会自动使用traceview.bat工具打开,sdk\tools\traceview.bat
打开后,结果界面将跟第一种使用方式相同,图略。
1.3.3 分析trace结果
如先前的trace结果图片所示,traceView结果分为上下两半进行展示。
上面是时间轴面板 (Timeline Panel)
时间轴面板的左侧显示的是线程信息。时间轴面板的右侧黑色部分是显示执行时间段;白色是线程暂停时间段。时间轴面板的右侧鼠标放在上面会出现时间线纵轴,在顶部会显示当前时间线所执行的具体函数信息。
下面是分析面板(Profile Panel)
分析面板展示的是各个方法函数执行消耗时间性能的统计,点击一个函数,会出现两部分的数据,Parents和Children,分别表示父方法(当前被哪些方法调用)和子方法(当前调用哪些方法)。
下面列出了分析面板(Profile Panel)各参数的含义:
列名 | 描述 |
---|---|
Name | 该线程运行过程中所调用的函数名 |
Incl Cpu Time | 某函数占用的CPU时间,包含内部调用其它函数的CPU时间 |
Excl Cpu Time | 某函数占用的CPU时间,但不含内部调用其它函数所占用的CPU时间 |
Incl Real Time | 某函数运行的真实时间(以毫秒为单位),内含调用其它函数所占用的真实时间 |
Excl Real Time | 某函数运行的真实时间(以毫秒为单位),不含调用其它函数所占用的真实时间 |
Call+Recur Calls/Total | 某函数被调用次数以及递归调用占总调用次数的百分比 |
Cpu Time/Call | 某函数调用CPU时间与调用次数的比。相当于该函数平均执行时间 |
Real Time/Call | 同CPU Time/Call类似,只不过统计单位换成了真实时间 |
通过分析我们每个函数消耗,就能分析出我们的“瓶颈”函数。
1.3.4 traceView举例
例如我们的Demo,里面存在这样的代码:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startTask();
}
});
}
private void startTask() {
// 多次循环打Log
for (int i=0;i<1000;i++){
for (int j=0;j<1000;j++){
String name="xmg"+i*j;
Log.i("xmg", "my name is: "+name);
}
}
}
startBlockTask函数内部有两重的for循环,多次循环打log。我们点击button触发事件后,得出这一段代码的TraceView如下图:
从上图的分析我们可以看出,序号为15的函数消耗了比较多的性能,它是除了UI绘制以外最大的性能消耗“钉子户”,耗时明显远高与其他函数,并且它的调用次数和递归调用次数异常。so,它就是我们的性能“瓶颈”,它的存在使我们的UI变卡,点击展开可以查看它的Parent函数: startBlockTask。根据这个,我们可以定位到我们需要优化的函数并做相应的优化。
1.4 更智能的性能分析工具leakCanary
不再介绍,详情见内存优化
DDMS
DDMS 的全称是Dalvik Debug Monitor Service,是Android 开发环境中的Dalvik 虚拟机调试监控服务
HierarchyViewer
UI性能分析工具,分析布局文件的性能,层级嵌套
UI布局复杂程度及冗余分析,View嵌套的冗余层级
View的性能指标:测量、布局、绘制的渲染时间
1、invalidate Layout按钮
invalidate(),强制刷新
2、requestLayout按钮
requestLayout(),重新测量,布局
使用GPU过度绘制分析UI性能
开发者选项中的GPU过度绘制工具(Show GPU Overdraw)
使用GPU呈现模式图及FPS考核UI性能
开发者选项中的GPU呈现模式分析,Profile GPU Rendering
Android Monitor
TraceView
TraceView 简介
Traceview 是Android 平台特有的数据采集和分析工具,它主要用于分析Android 中应用程序的hotspot(瓶颈)。Traceview 本身只是一个数据分析工具,而数据的采集则需要使用Android SDK 中的Debug 类或者利用DDMS 工具。二者的用法如下:
开发者在一些关键代码段开始前调用Android SDK 中Debug 类的startMethodTracing 函数,并在关键代码段结束前调用stopMethodTracing 函数。这两个函数运行过程中将采集运行时间内该应用所有线程(注意,只能是Java线程)的函数执行情况,并将采集数据保存到/mnt/sdcard/下的一个文件中。开发者然后需要利用SDK 中的Traceview工具来分析这些数据。
借助Android SDK 中的DDMS 工具。DDMS 可采集系统中某个正在运行的进程的函数调用信息。对开发者而言,此方法适用于没有目标应用源代码的情况。DDMS 工具中Traceview 的使用如下图所示。
观察CPU的执行情况,测试的进程中每个线程运行的时间线,线程中各个方法的调用信息(CPU使用时间、调用次数等)
可以方便的查看线程的执行情况,某个方法执行时间、调用次数、在总体中的占比等,从而定位性能点
一般Traceview可以定位两类性能问题
- 方法调运一次需要耗费很长时间导致卡顿
- 方法调运一次耗时不长,但被频繁调运导致累计时长卡顿
点击上图中所示按钮即可以采集目标进程的数据。当停止采集时,DDMS 会自动触发Traceview 工具来浏览采集数据
下面,我们通过一个示例程序介绍Traceview 的使用。
实例程序如下图所示:界面有4 个按钮,对应四个方法。
点击不同的方法会进行不同的耗时操作。
public class MainActivity extends ActionBarActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void method1(View view) {
int result = jisuan();
System.out.println(result);
}
private int jisuan() {
for (int i = 0; i < 10000; i++) {
System.out.println(i);
}
return 1;
}
public void method2(View view) {
SystemClock.sleep(2000);
}
public void method3(View view) {
int sum = 0;
for (int i = 0; i < 1000; i++) {
sum += i;
}
System.out.println("sum=" + sum);
}
public void method4(View view) {
Toast.makeText(this, "" + new Date(), 0).show();
}
}
我们分别点击按钮一次,要求找出最耗时的方法。点击前通过DDMS 启动Start Method Profiling 按钮。
然后依次点击4 个按钮,都执行后再次点击上图中红框中按钮,停止收集数据。
接下来我们开始对数据进行分析。
当我们停止收集数据的时候会出现如下分析图表。该图表分为2 大部分,上面分不同的行,每一行代表一个线程的执行耗时情况。main 线程对应行的的内容非常丰富,而其他线程在这段时间内干得工作则要少得多。图表的下半部分是具体的每个方法执行的时间情况。显示方法执行情况的前提是先选中某个线程。
我们主要是分析main 线程。
上面方法指标参数所代表的意思如下:
列名 | 描述 |
---|---|
Name | 该线程运行过程中所调用的函数名 |
Incl Cpu Time | 某函数占用的CPU 时间,包含内部调用其它函数的CPU 时间 |
Excl Cpu Time | 某函数占用的CPU 时间,但不含内部调用其它函数所占用的CPU 时间 |
Incl Real Time | 某函数运行的真实时间(以毫秒为单位),包含调用其它函数所占用的真实时间 |
Excl Real Time | 某函数运行的真实时间(以毫秒为单位),不含调用其它函数所占用的真实时间 |
Call+Recur Calls/Total | 某函数被调用次数以及递归调用占总调用次数的百分比 |
Cpu Time/Call | 某函数调用CPU 时间与调用次数的比。相当于该函数平均执行时间 |
Real Time/Call | 同CPU Time/Call 类似,只不过统计单位换成了真实时间 |
我们为了找到最耗时的操作,那么可以通过点击Incl Cpu Time,让其按照时间的倒序排列。我点击后效果如下图:
通过分析发现:method1 最耗时,耗时2338 毫秒。
那么有了上面的信息我们可以进入我们的method1 方法查看分析我们的代码了
生成.trace文件
android.os.Debug类,其中重要的两个方法Debug.startMethodTracing()和Debug.stopMethodTracing()。这两个方法用来创建.trace文件,将从Debug.startMethodTracing()开始,到Debug.stopMethodTracing()结束,期间所有的调用过程保存在.trace文件中,包括调用的函数名称和执行的时间等信息。
dmtracedump
dmtracedump -g result.png target.trace //结果png文件 目标trace文件
Allocation Tracker
追踪内存的分配,追踪内存对象的来源,通过这个工具我们可以很方便的知道代码分配了哪类对象、在哪个线程、哪个类、哪个文件的哪一行
运行DDMS,只需简单的选择应用进程并单击Allocation tracker 标签,就会打开一个新的窗口,单击“Start Tracing”按钮;
然后,让应用运行你想分析的代码。运行完毕后,单击“Get Allocations”按钮,一个已分配对象的列表就会出现第一个表格中。
单击第一个表格中的任何一项,在表格二中就会出现导致该内存分配的栈跟踪信息。通过allocation tracker,不仅知道分配了哪类对象,还可以知道在哪个线程、哪个类、哪个文件的哪一行。
Systrace
Systrace其实有些类似Traceview,它是对整个系统进行分析
DDMS->Capture system wide trace using Android systrace
Heap
内存监测工具,分析内存使用情况,查看当前内存快照,便于对比分析哪些对象有可能是泄漏了的
heap 工具可以帮助我们检查代码中是否存在会造成内存泄漏的地方。用heap 监测应用进程使用内存情况的步骤如下:
启动eclipse 后,切换到DDMS 透视图,并确认Devices 视图、Heap 视图都是打开的
点击选中想要监测的进程,比如system_process 进程
点击选中Devices 视图界面中最上方一排图标中的“Update Heap”图标
点击Heap 视图中的“Cause GC”按钮
此时在Heap 视图中就会看到当前选中的进程的内存使用量的详细情况
说明:
点击“Cause GC”按钮相当于向虚拟机请求了一次gc 操作
当内存使用信息第一次显示以后,无须再不断的点击“Cause GC”,Heap 视图界面会定时刷新,在对应用的不断的操作过程中就可以看到内存使用的变化
内存使用信息的各项参数根据名称即可知道其意思,在此不再赘述
如何才能知道我们的程序是否有内存泄漏的可能性呢。这里需要注意一个值:Heap 视图中部有一个Type叫做data object,即数据对象,也就是我们的程序中大量存在的类类型的对象。在data object 一行中有一列是“Total Size”,其值就是当前进程中所有Java 数据对象的内存总量,一般情况下,这个值的大小决定了是否会有内存泄漏。可以这样判断:
不断的操作当前应用,同时注意观察data object 的Total Size 值
正常情况下Total Size 值都会稳定在一个有限的范围内,也就是说由于程序中的的代码良好,没有造成对象不被垃圾回收的情况,所以说虽然我们不断的操作会不断的生成很多对象,而在虚拟机不断的进行GC 的过程中,这些对象都被回收了,内存占用量会会落到一个稳定的水平
反之如果代码中存在没有释放对象引用的情况,则data object 的Total Size 值在每次GC 后不会有明显的回落,随着操作次数的增多Total Size 的值会越来越大,直到到达一个上限后导致进程被kill 掉
此处以system_process 进程为例,在我的测试环境中system_process 进程所占用的内存的data object
的Total Size 正常情况下会稳定在2.2~2.8 之间,而当其值超过3.55 后进程就会被kill总之,使用DDMS 的Heap 视图工具可以很方便的确认我们的程序是否存在内存泄漏的可能性
Leakcanary
Square出品,内存泄露监测神器,GitHub地址
Eclipse Memory Analyzer(MAT)
内存分析工具,这个工具分为Eclipse插件版和独立版两种,如果你是使用Eclipse开发的,那么可以使用插件版MAT,非常方便。如果你是使用Android Studio开发的,那么就只能使用独立版的MAT了
HPROF文件
HPROF文件是MAT能识别的文件,HPROF文件存储的是特定时间点,java进程的内存快照
点击Dump HPROF file按钮,生成HPROF文件,这个文件记录着我们应用程序内部的所有数据。但是目前MAT还是无法打开这个文件的,我们还需要将这个HPROF文件从Dalvik格式转换成J2SE格式,使用hprof-conv命令就可以完成转换工作
hprof-conv dump.hprof converted-dump.hprof
Histogram
Histogram:列出内存中每个对象的名字、数量以及大小
Shallow Heap:当前对象自己所占内存的大小,不包含引用关系的
析大内存的对象,分析对象的数量
Dominator Tree
Dominator Tree:列出最大的对象以及其依赖存活的Object,并且我们可以分析对象之间的引用结构
Retained Heap
表示这个对象以及它所持有的其它引用(包括直接和间接)所占的总内存
在每一行的最左边都有一个文件型的图标,这些图标有的左下角带有一个红色的点,有的则没有。带有红点的对象就表示是可以被GC Roots访问到的,可以被GC Root访问到的对象都是无法被回收的。带红点的对象最右边都有写一个System Class,说明这是一个由系统管理的对象,并不是由我们自己创建并导致内存泄漏的对象
搜索大内存对象通向GC Roots的路径,因为内存占用越高的对象越值得怀疑
GC Roots reference chain(引用链)的起点,是一个在current thread(当前线程)的call stack(调用栈)上的对象(例如方法参数和局部变量),或者是线程自身或者是system class loader(系统类加载器)加载的类以及native code(本地代码)保留的活动对象。所以GC Roots是分析对象为何还存活于内存中的利器。
dumpsys meminfo命令
adb shell dumpsys meminfo <package_name|pid> [-d]
命令后面带-d标志会打印出更多关于内存使用的信息
adb shell dumpsys meminfo com.google.android.apps.maps -d
C:\Users\AllenIverson>adb shell dumpsys meminfo com.qq.googleplay -d
Applications Memory Usage (kB):
Uptime: 588545781 Realtime: 1460567078
** MEMINFO in pid 19204 [com.qq.googleplay] **
Pss Private Private Swapped Heap Heap Heap
Total Dirty Clean Dirty Size Alloc Free
------ ------ ------ ------ ------ ------ ------
Native Heap 0 0 0 0 20480 9524 10955
Dalvik Heap 23402 22936 0 0 40714 38963 1751
Dalvik Other 729 728 0 0
Stack 392 392 0 0
Other dev 5 0 4 0
.so mmap 1586 232 468 0
.apk mmap 281 0 52 0
.ttf mmap 262 0 248 0
.dex mmap 5500 0 5356 0
.oat mmap 2606 0 916 0
.art mmap 2350 924 800 0
Other mmap 1342 4 884 0
Unknown 7798 7748 0 0
TOTAL 46253 32964 8728 0 61194 48487 12706
Objects
Views: 261 ViewRootImpl: 1
AppContexts: 3 Activities: 1
Assets: 4 AssetManagers: 4
Local Binders: 7 Proxy Binders: 16
Parcel memory: 5 Parcel count: 23
Death Recipients: 0 OpenSSL Sockets: 0
Dalvik
isLargeHeap: false
SQL
MEMORY_USED: 0
PAGECACHE_OVERFLOW: 0 MALLOC_SIZE: 0
一般我们只需要关心Pss Total和Private Dirty两列数据,在某些情况下,Private Clean和Heap Alloc两列数据可能会提供你感兴趣的数据
adb shell dumpsys batterystats 电量状态
Lint工具
使用Lint进行资源及冗余UI布局等优化,Lint 有自动修复、提示建议和直接跳转到问题处的功能
集成到androidstudio,点击工具栏的Analysis -> Inspect Code
内存抖动:短时间内有大量频繁的对象创建与释放操作
Lint是Android提供的一个静态扫描应用源码并找出其中的潜在问题的一个强大的工具
运行Lint:点击工具栏的Analysis -> Inspect Code
ProGuard
混淆代码,压缩和优化代码,apk瘦身
GC打印
当发生GC垃圾回收的时候,会在logcat打印日志
I/art : Explicit concurrent mark sweep GC freed 104710(7MB) AllocSpace objects, 21(416KB) LOS objects, 33% free, 25MB/38MB, paused 1.230ms total 67.216ms
Investigating Your RAM Usage
http://android.xsoftlab.net/tools/debugging/debugging-memory.html