一 、卡顿介绍及优化工具选择
对用户来说:很多性能问题不易被发现,但是卡顿很容易被直观感受
对开发者来说:卡顿问题难以定位
卡顿问题难在哪里:
产生的原因错综复杂:代码、内存、绘制、IO?
不易复现:与用户当时场景相关
1、CPU Profiler
图形的形式展示执行时间、调用栈等,信息全面,包含所有线程
运行时开销严重,整体都会变慢
使用方式
Debug.startMethodTracing(“文件名”);//使用
Debug.stopMethodTracing();//结束
生成文件在sd卡:Android/data/packagename/files可以直接DeviceFileExplorer中找到
或者通过adb pull /mnt/sdcard/文件名.trace 指定文件目录中.将trace导出指定的文件夹中
2、Systrace
python systrace.py -t 10 [other-options][categories] python脚本
优点:轻量级,开销小,直观反映CPU使用率,给出建议。
https://developer.android.google.cn/studio/command-line/systrace
3、StrictMode
严苛模式,Android提供的一种运行时检测机制,方便强大,包含:线程策略和虚拟机策略检测
线程策略
自定义的耗时调用:detectCustomSlowCalls
磁盘读写操作:detectDiskReads、detectDiskReads
网络操作:detectDiskWrites
虚拟机策略
Activity泄漏:detectActivityLeaks
sqlite对象泄漏:detectLeakedSqlLiteObjects
检测实例数量:setClassInstanceLimit
private void initStrictMode() {
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectCustomSlowCalls() //API等级11,使用StrictMode.noteSlowCode
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()// or .detectAll() for all detectable problems
.penaltyLog() //在Logcat 中打印违规异常信息
.build());
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.setClassInstanceLimit(NewsItem.class, 1)
.detectLeakedClosableObjects() //API等级11
.penaltyLog()
.build());
}
}
二 、自动化卡顿检测方案及优化
系统工具只适合线下针对行分析,线上及测试环境需要自动化检测方案
方案原理
消息处理机制,一个线程只有一个Looper,mLogging对象在每个message处理前后被调用
主线程发生卡顿,是在dispatchMessage执行耗时操作
具体实现
Looper.getMainLooper().setMessageLogging();设置我们的message
匹配>>>>> Dispatching,阀值时间后执行任务(获取堆栈)
匹配<<<<< Finished,任务启动之前取消掉
AndroidPerformanceMonitor
非侵入式的性能监控组件,通知形式弹出卡顿信息,非侵入式,方便精准,定位到代码某一行
https://github.com/markzhai/AndroidPerformanceMonitor
第一步:implementation ‘com.github.markzhai:blockcanary-android:1.5.0’
第二步:BlockCanary.install(this, new AppBlockCanaryContext()).start();
自动检测方案问题:
确实卡顿了,但卡顿堆栈可能不准确
和OOM一样,最后堆栈只是表象,不是真正的问题(我们是在T2时刻获取的信息,但是其实这个已经晚了,实际的卡顿发生在T2之前)
方案优化:
获取监控周期内的多个堆栈,而不是最后一个。
startMonitor -> 高频采集堆栈 -> endMonitor
记录多个堆栈 -> 上报
海量卡顿堆栈处理:
高频卡顿上报量太大,服务端有压力
分析:一个卡顿下有多个堆栈大概率有重复
解决:对一个卡顿下堆栈进行hash排重,找出重复的堆栈
效果:极大的减少展示量,同时更高效找到卡顿堆栈
三、 ANR分析与实战
1、ANR介绍
keyDispatchTimeOut,5s
BroadCastTimeOut,前台10s,后台60s
ServiceTimeOut,前台20s,后台200s
ANR执行流程
发生ANR -> 进行接收异常终止信号,开始写入进行anr信息 -> 弹出ANR提示框(Rom表现不一)
ANR解决套路
adb pull data/anr/traces.txt 存储路径
线上ANR监控方案
通过FileObserver监控文件变化,高版本权限问题
由于权限问题可能监控不到,这时就需要ANR-WatchDog
2、ANR-WatchDog原理及实战
非侵入式的ANR监控组件
com.github.anrwatchdog:anrwatchdog:1.3.0
https://github.com/SalomonBrys/ANR-WatchDog
第一步:implementation ‘com.github.anrwatchdog:anrwatchdog:1.3.0’
第二步:new ANRWatchDog().start()
ANRWatchDog 就是一个Thread 在线程中通过主线程的handler发送一条信息进行+1的操作,接下来这个线程Sleep一段时间,Sleep后检测+1的操作是否被执行,执行了没有卡顿,没执行就是卡顿状态。
3、区别
AndroidPerformanceMonitor:监控主线程每一个Msg的执行
WatchDog:只看最总结果
前者适合监控卡顿,后者适合补充ANR监控
四 、卡顿单点问题检测方案
自动卡顿检测方案并不够(如很多msg要执行,但是每一个msg都没有超过阀值,但是用户能感知到)
体系化解决方案务必尽早暴露问题
单点问题:主线程IPC、DB
1、IPC问题监测
(1)监测指标
- IPC调用类型
- 调用次数、耗时
- 调用堆栈、发生线程
https://blog.csdn.net/qq_26420489/article/details/52278945 IPC的6种方式 ,
https://blog.csdn.net/zeng622peng/article/details/62881834 Android中IPC的几种方式详细分析与优缺点分析
(2)常规方案
IPC前后加埋点,不优雅、容易忘记,维护成本大
(3)IPC问题监测技巧
adb命令
adb shell am trace-ipc start
adb shell am trace-ipc stop ——dump-file/data/local/tmp/ipc-trace.txt
adb pull /data/local/tmp/ipc-trace.txt
(4)优雅方案
AspectJ: 非系统方法
ARTHook:可以Hook系统方法
所有的ipc操作都走BinderProxy的 https://www.jianshu.com/p/afa794939379
try {
DexposedBridge.findAndHookMethod(Class.forName("android.os.BinderProxy"), "transact",
int.class, Parcel.class, Parcel.class, int.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
LogUtils.i( "BinderProxy beforeHookedMethod " + param.thisObject.getClass().getSimpleName()
+ "\n" + Log.getStackTraceString(new Throwable()));
super.beforeHookedMethod(param);
}
});
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
2、单点问题监测方案
利用ARTHook完善线下工具
开发阶段Hook相关操作、暴露、分析问题
监控维度:IPC,IO、DB,View绘制
五、如何实现界面秒开
1、界面秒开实现
界面秒开就是一个小的启动优化,可以借鉴启动优化及布局优化章节
- SysTrace:优雅异步+优雅延迟初始化(跑满cpu)
- 异步Inflate、X2C、绘制优化
- 提前获取页面数据(唯一路径界面)
2、界面秒开率统计
onCreate到onWindowFocusChange的时间作为统计时间或者在Activity中设置特定接口进行统计
Lancet介绍
@Proxy 通常用与对系统API调用的Hook
@Insert 常用于操作App与library的类
插件 apply plugin: ‘me.ele.lancet’
依赖 provided ‘me.ele:lancet-base:1.0.4’
public class ActivityHooker {
public static ActivityRecord sActivityRecord;
static {
sActivityRecord = new ActivityRecord();
}
public static String trace;
@Insert(value = "onCreate",mayCreateSuper = true)
@TargetClass(value = "android.support.v7.app.AppCompatActivity",scope = Scope.ALL)
protected void onCreate(Bundle savedInstanceState) {
sActivityRecord.mOnCreateTime = System.currentTimeMillis();
Origin.callVoid();
}
@Insert(value = "onWindowFocusChanged",mayCreateSuper = true)
@TargetClass(value = "android.support.v7.app.AppCompatActivity",scope = Scope.ALL)
public void onWindowFocusChanged(boolean hasFocus) {
sActivityRecord.mOnWindowsFocusChangedTime = System.currentTimeMillis();
LogUtils.i("onWindowFocusChanged cost "+(sActivityRecord.mOnWindowsFocusChangedTime - sActivityRecord.mOnCreateTime));
Origin.callVoid();
}
public static long sStartTime = 0;
@Insert(value = "acquire")
@TargetClass(value = "com.optimize.performance.wakelock.WakeLockUtils",scope = Scope.SELF)
public static void acquire(Context context){
trace = Log.getStackTraceString(new Throwable());
sStartTime = System.currentTimeMillis();
Origin.callVoid();
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
WakeLockUtils.release();
}
},1000);
}
@Insert(value = "release")
@TargetClass(value = "com.optimize.performance.wakelock.WakeLockUtils",scope = Scope.SELF)
public static void release(){
LogUtils.i("PowerManager "+(System.currentTimeMillis() - sStartTime)+"/n"+trace);
Origin.callVoid();
}
public static long runTime = 0;
@Insert(value = "run")
@TargetClass(value = "java.lang.Runnable",scope = Scope.ALL)
public void run(){
runTime = System.currentTimeMillis();
Origin.callVoid();
LogUtils.i("runTime "+(System.currentTimeMillis() - runTime));
}
@Proxy("i")
@TargetClass("android.util.Log")
public static int i(String tag, String msg) {
msg = msg + "";
return (int) Origin.call();
}
}
3、界面秒开监控纬度
总体耗时,生命周期耗时,生命后期间隔耗时
六、优雅监控耗时盲区
1、耗时盲区监控背景介绍
盲区例子:
- 生命周期间隔
- onResume到Feed展示的间隔
举例:在Activity中postMessage,很可能在Feed之前执行
// 以下代码是为了演示Msg导致的主线程卡顿
new Handler().post(new Runnable() {
@Override
public void run() {
LogUtils.i("Msg 执行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
耗时盲区监控难点:只知道盲区时间,不清楚具体在做什么(onResume -> FeedShwo之前干了什么,包括不同人添加的内容及第三方做的事情),线上盲区无从排查
2、耗时盲区监控方案
(1)耗时盲区监控线下方案:TraceView
特别时合一段时间内的盲区监控
线程具体时间做了什么,一目了然
(2)耗时盲区监控线上方案
思考分析
- 所有方法都是Msg,mLogging?没有Msg具体堆栈(只知道主线程发生了Msg,但是不知道调用栈信息,它所知道的调用栈信息都是系统调用它的)
- AOP切Handler?(只知道开始时间,不清楚准确执行时间)
方案: - 使用统一的Handler:定制具体方法(sendMessageAtTime和dispatchMessage)
- 定制gradle插件,编译期动态替换
//通过gradle将所有Handler都替换成SuperHandler
public class SuperHandler extends Handler {
private long mStartTime = System.currentTimeMillis();
public SuperHandler() {
super(Looper.myLooper(), null);
}
public SuperHandler(Callback callback) {
super(Looper.myLooper(), callback);
}
public SuperHandler(Looper looper, Callback callback) {
super(looper, callback);
}
public SuperHandler(Looper looper) {
super(looper);
}
@Override
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
boolean send = super.sendMessageAtTime(msg, uptimeMillis);
if (send) {
GetDetailHandlerHelper.getMsgDetail().put(msg, Log.getStackTraceString(new Throwable()).replace("java.lang.Throwable", ""));
}
return send;
}
@Override
public void dispatchMessage(Message msg) {
mStartTime = System.currentTimeMillis();
super.dispatchMessage(msg);
if (GetDetailHandlerHelper.getMsgDetail().containsKey(msg)
&& Looper.myLooper() == Looper.getMainLooper()) {
JSONObject jsonObject = new JSONObject();
try {
jsonObject.put("Msg_Cost", System.currentTimeMillis() - mStartTime);
jsonObject.put("MsgTrace", msg.getTarget() + " " + GetDetailHandlerHelper.getMsgDetail().get(msg));
LogUtils.i("MsgDetail " + jsonObject.toString());
GetDetailHandlerHelper.getMsgDetail().remove(msg);
} catch (Exception e) {
}
}
}
}
//数据帮助类
public class GetDetailHandlerHelper {
private static ConcurrentHashMap<Message, String> sMsgDetail = new ConcurrentHashMap<>();
public static ConcurrentHashMap<Message, String> getMsgDetail() {
return sMsgDetail;
}
}
七 、卡顿优化技巧总结
1、卡顿优化实践经验
耗时操作:异步、延迟
布局优化:异步inflate、X2C、重绘解决
内存:降低内存占用,减少GC时间
2、卡顿优化工具建设
系统工具的认识、使用(Systrace-cpu使用、TraceView-每个线程在做什么、StrictMode)
自动化监控及优化(AndroidPerformanceMonitor、ANR-WatchDog、高频采集,找出重复率高的堆栈)
卡顿监控工具(单点问题:AOP、Hook,盲区监控:gradle编译期替换)
卡顿监控指标(卡顿率、ANR率、界面秒开率,交互时间、生命周期时间,上报环境、场景信息)