目录
目录
3.1.4 通过perfetto工具或者systrace分析trace文件
1、内存泄露优化
1.1 抓取内存泄露方法
方法一(适用于右发际线高着使用):
1、执行dumpsys meminfo ‘包名’记录当前内存占用
2、执行压测脚本循环遍历业务功能
3、压测一段时间后退出界面等待一段时间后输入命令:dumpsys meminfo ‘包名’查看Activities是否为0,内存有无明显增加的情况
方法二(适用于左发际线高着使用):
通过Android Profile抓取内存快照查看是否有内存泄露
1、使用Memory Profile实时跟踪应用的内存信息,针对内存分配异常的情况,可以通过抓取内存快照进行分析
2、点击Profile的Memory显示条即可进入Memory Profile
2、启动优化
2.1、冷启动
2.1.1 抓取冷启动方法
方法一:通过串口命令获取
- 在测试前先查询进程是否存在 命令 ps -A | grep 包名
- 如果进程存在则需要杀死进程使用 命令 kill -9 进程号
- 在杀死进程后在使用启动命令查看启动时间 命令am start -W 包名/类路径名
应用自身启动耗时,参考TotalTime;
系统启动应用耗时(包含上一个应用activity的pause),参考WaitTime;
方法二:抓取trace判断启动时长
分析冷启动时间:定位到当前应用的UI Thread,如下图,从ZygoteInit到serviceStart后的第一帧结束,查看时间长度做为启动时长(注意该时长比应用测试部用视频采集卡采集的时间要短,视频采集卡采集的是用户感知的真正时长,我们这里的时长仅用作自己应用启动时长分析);
2.2、热启动
2.2.1 抓取热启动方法
方法一:通过命令抓取
- 让应用退出前台
- 在测试前先查询进程是否存在,如果存在在进行下步操作 命令 ps -A | grep 包名
- 使用启动命令查看启动时间 命令am start -W 包名/类路径名
方法二:抓取trace判断启动时长
分析热启动时间:定位到当前应用的UI Thread,如果是activity进入,则从activityStart到serviceStart后的第一帧结束,查看时间长度做为启动时长。
如果是service进入,则从serviceCreate到serviceStart后的第一帧结束,查看时间长度做为启动时长。
2.3 启动优化
启动我们可以查看是否Application初始化或者Activity初始化时存在耗时,可以做出如下改善
ContentProvider:
ContentProvider的onCreate方法会在Application的onCreate方法前执行,其onCreate方法不要放耗时操作,可以通过Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler()延时加载
Application层面:
- 将Application的onCreate方法中第三方SDK初始化放入线程中处理
- Multidex预加载优化,5.0以下多dex情况
Activity层面:
- 首屏Activity的渲染不要有耗时操作,如果有可以放入子线程或者IntentService中处理
- 预创建Activity,对象预创建
- 预加载数据
UI层面:
- 消除启动时的白屏、黑屏。windowbackground
- 避免布局嵌套,多层嵌套
编译层面:
- ART 如何编译 DEX 代码还有个compile filter以参数的形式来决定:从 Android O 开始,有四个官方支持的过滤器:
- verify----只运行 DEX 代码验证。
- quicken----运行 DEX 代码验证,并优化一些 DEX 指令,以获得更好的解释器性能。
- speed-profile—运行 DEX 代码验证,并对配置文件中列出的方法进行 AOT 编译。
- speed----运行 DEX 代码验证,并对所有方法进行 AOT 编译
- verify 和quicken 他俩都没执行编译,之后代码执行需要跑解释器。而speed-profile 和 speed 都执行了编译,区别是speed-profile根据profile记录的热点函数来编译,属于部分编译,而speed属于全编。
- 执行效率:verify < quicken < speed-profile < speed
- 编译速度上:verify > quicken > speed-profile > speed
- 查看应用编译状态:dumpsys package 包名 | grep status
- 串口修改编译状态:pm compile -m speed 包名
参考:启动优化
3、卡顿优化
3.1、CPU占用查询
3.1.1 通过脚本命令抓取CPU占用
1、定义脚本文件gen_memory.sh(代码在第五章节),修改包名和U盘挂载路径
2、将gen_memory.sh拷贝到/data/目录,赋权限777(chmod 777)
3、运行脚本(进入到data目录后输入 ./gen_memory.sh),然后遍历各选项功能;
4、退出应用,然后运行脚本(进入到data目录后输入 ./gen_memory.sh);
5、等待一段时间后,建议10分钟以上,拔出U盘,查看U盘中指定包名文件夹下的cpu.csv文件,进行分析查看内存占用是否合理。
3.1.2 通过TOP命令抓取CPU占用
分析系统和进程的CPU占用情况:top -m 20 -n 1 (输出一次cpu占用前20的所有进程数据)
分析高占用进程的线程CPU实时占用情况:top -Hp1635 -m 20 -d 1(输出pid为1853 的进程中cpu占用前20的线程数据,一秒一次)
技巧:同时开两个cmd窗口,分别输入命令同时监控进程和线程的实时占用,可定位到线程,可做成脚本控制频率,动态采集高占用的进程,再结合业务逻辑做归因分析。
3.1.3 抓取trace文件
1、通过脚本命令抓取trace
抓取10秒内trace:
atrace -z -b 10000 gfx input view wm am res dalvik rs sched freq idle disk binder_driver binder_lock -t 10 > /data/trace_output1.atrace
持续抓取:
开始抓取 atrace --async_start -c -b 12800 -z gfx view input webview wm am sm audio video camera hal dalvik res rs bionic pm ss database network disk sched freq idle binder_lock binder_driver
复现到后停止 atrace --async_stop -c -b 12800 -z -o /data/local/tmp/trace.out
2、通过profile抓取trace
3.1.3 CPU分析
CPU Profile可以跟踪如下几种数据:
- Sample Java Method 为间断采样,适用于大部分情况,profile调试不会对现有程序造成影响
- Trace Java Method 持续采集,适用于跟踪全部method执行状况,会对现有程序运行造成影响
- Sample C 跟踪C层函数(Android 8.0+)
- Trace System Call 跟踪系统调用,如核心运行状态和掉帧情况( Android 7.0+)
3.1.4 通过perfetto工具或者systrace分析trace文件
systrace是google 提供的一个分析工具,脚本命令为Android SDK的platform-tools\systrace\systrace.py , 它的运行依赖python2.7+,所以在进行trace分析前需要安装python对应环境。
转换命令:python "D:\Program Files\Android\sdk\platform-tools\systrace\systrace.py" --from-file=D:\9615_trace\trace008.out
我们可以从frame看出是那一帧卡顿,红色是验证卡顿、橙色为轻微卡顿、绿色不卡顿。然后再从堆栈分析draw、layout的耗时情况。
线程状态
绿色:正在运行,线程正在完成与某个进程相关的工作或正在响应中断。
蓝色:可运行,线程可以运行但目前未进行调度。
白色:休眠,线程没有可执行的任务,可能是因为线程在遇到斥锁定时被阻止。
橙色:不可中断的休眠,线程在遇到 I/O 操作时被阻止或正在等待磁盘操作完成。
紫色:可中断的休眠,线程在遇到另一项内核操作(通常是内存管理)时被阻止。
3.2、内存占用分析
3.2.1 通过脚本命令获取内存
1、定义脚本文件gen_cpu.sh(代码在第五章节),修改包名和U盘挂载路径
2、将gen_cpu.sh拷贝到/data/目录,赋权限777
3、运行脚本(进入到data目录后输入 ./gen_cpu.sh),然后遍历各选项功能;
4、退出应用,然后运行脚本(进入到data目录后输入 ./gen_cpu.sh);
5、等待一段时间后,建议10分钟以上,拔出U盘,查看U盘中指定包名文件夹下的meminfo.csv文件,进行分析查看内存占用是否合理。
3.2.2 通过TOP命令抓取内存
技巧:1,先使用命令dumpsys meminfo查看设备的内存占用情况,分析内存不足时各个进程的占用,这个命令统计时间较长,比较耗资源,不适合频繁使用
2,使用命令dumpsysmeminfopkg/pid查看对应进程的详细内存分布,用于前期初步分析定位内存的分布:
- Native Heap 较高时,一般是Bitmap对象较多(结合Views判断);或者是JNI里C/C++申请的对象占用内存较多
- Java Heap 较高时大部分占用都是应用程序里的申请的对象内存,抓取内存快照可详细分析
- Code 较高代表so库,jar包数量多;代码量大,大量的类加载(多个class.dex分包),资源多等原因,可解压apk查看,需要关注apk瘦身优化
- Stack 占用高通常是线程数量多太,栈内存高导致,检查无用线程,尽量用线程池创建线程Graphics 是UI渲染任务繁重,检查Views层级,过度绘制,注重图层优化
- System 是可系统相关的内存,可能是使用较多的系统服务,应用程序框架等原因
- 通过Activites数量持续增加/退出后不减少的情况,可以初步判断内存泄漏
3.2.2 抓取trace文件
1、通过脚本命令抓取trace
抓取10秒内trace:
atrace -z -b 10000 gfx input view wm am res dalvik rs sched freq idle disk binder_driver binder_lock -t 10 > /data/trace_output1.atrace
持续抓取:
开始抓取 atrace --async_start -c -b 12800 -z gfx view input webview wm am sm audio video camera hal dalvik res rs bionic pm ss database network disk sched freq idle binder_lock binder_driver
复现到后停止 atrace --async_stop -c -b 12800 -z -o /data/local/tmp/trace.out
2、通过profile抓取trace
3.2.3 内存分析
内存分类:
- Java表示Java代码或Kotlin代码分配的内存;
- Native表示C或C++代码分配的内存(即使App没有native层,调用framework代码时,也有可能触发分配native内存);
- Graphics表示图像相关缓存队列占用的内存;
- Stack表示native和java占用的栈内存;
- Code表示代码、资源文件、库文件等占用的内存;
- Others表示无法明确分类的内存;
3.3 卡顿分析
Systrace会统计卡顿的具体类型,一般常用的类型有:
1、CPU分配不足:
进程大部分处于Runnable,CPU资源分配不足
- Scheduling delay:调度延迟
- Expensive measure/layout pass:绘测或者布局时间长
- Long View#draw: view 绘制时间长
导致卡顿可能有如下原因:
- 过于复杂的布局
- UI线程的复杂运算
- 频繁的GC,如内存抖动,即大量的对象被创建后又短时间被回收,或者瞬间产生大量的对象会严重占用内存区域
4、性能抓取命令总结
方式 | 命令 |
帧率 | dumpsys gfxinfo 包名 丢帧率:Janky frames的前后差值 / Total frames的前后差值 |
内存泄露、内存占用 | dumpsys meminfo 包名 |
查看前20个进程的cpu使用 | top -m 20 |
应用安装大小 | du -sh 安装路径 |
5、抓取CPU和内存脚本命令
#!/system/bin/sh
#设置包名
packagename="";
#设置U盘路径
path="/mnt/media_rw/2DD0-A606/$packagename";
#设置du与dumpsys的间隔时间
sleeptime=15
#设置top间隔时间,建议15秒以上
toptime=15
a=0
b=0
c=0
d=0
#设置总次数
total=100000
#设置常量
u=1024
#v=6
if [ ! -d "$path" ];
then
mkdir $path
else
rm -rf $path
mkdir $path
fi ;
#查看Android版本
v=$(getprop|grep ro.build.version.release |awk '{print$2}'|awk '{print substr($1,2,1)}');
echo $v >> $path/android_ver.csv ;
#获取CPU个数
processor=$(($(cat /sys/devices/system/cpu/possible |awk -F'[-]' '{print$2}')+1));
#生成ddr表的表头
echo -n "time," >> $path/meminfo.csv;
echo -n "meminfo," >> $path/meminfo.csv;
echo "Activity" >> $path/meminfo.csv;
#生成cpu表的表头
echo -n "time," >> $path/cpu.csv;
echo -n "cpu," >> $path/cpu.csv;
echo "Activity" >> $path/cpu.csv;
while (($b < $total));do
let "b += 1";
if [ -d "$path" ]; then
echo -n $(date "+%H:%M:%S,") >>$path/meminfo.csv;
#echo -n $(date "+%H:%M:%S,") >>$path/ddr.csv;
#输出Meminfo大小 单位:M
ddr=$(dumpsys meminfo $packagename |grep TOTAL |awk 'BEGIN{ddr=0}{ddr += $2}END{print ddr}');
echo -n "`awk -v ddr=$ddr -v u=$u 'BEGIN{printf "%.2f",ddr/u}'`," >>$path/meminfo.csv;
#获取当前Activity
echo $(dumpsys window |grep mCurrentFocus |awk '{print$3}'|awk -F "}" '{print$1}') >>$path/meminfo.csv;
sleep $sleeptime;
fi ;
done&
while (($c < $total));do
let "c += 1";
if [ -d "$path" ];then
#CPU
cpu=$(top -b -n 1|grep $packagename|awk 'BEGIN{cpu=0}{cpu += $9} END {print cpu"%"}');
echo -n $(date "+%H:%M:%S,") >>$path/cpu.csv;
#生成CPU占用
echo -n "`awk -v cpu=$cpu -v processor=$processor 'BEGIN{printf "%.2f%%",cpu/processor}'`," >>$path/cpu.csv;
#获取当前Activity
echo "`dumpsys window |grep mCurrentFocus |awk '{print$3}'|awk -F "}" '{print$1}'`," >>$path/cpu.csv;
sleep $toptime;
fi ;
done&