很多性能问题不易被发现,但是卡顿却很容易被直观感受,另外一方面,对开发者来说,卡顿的问题的排查和定位有一定的难度。因为卡死产生的原因是错综复杂的,比如代码问题,内存环境、绘制过程,IO操作等等情形都有可能导致卡顿,尤其是线上的卡顿问题,受限于用户具体的运行环境,在线下难以复现,所以最好是能记录卡顿产生时的环境。
Profiler介绍
图形的形式展示执行的时间、调用栈等信息
信息全面、包含所有线程
运行时开销严重,应用整体变慢(可能带偏优化方向)
使用方式
Systrace介绍
结合Android内核的数据,生成Html报告
API18以上使用,推荐TraceCompat向下兼容
使用方式
TraceCompat.beginSection(“xxx”);// 手动埋点起始点
TraceCompat.endSection();// 手动埋点结束点
python systrace.py -b 32768 -t 5 -a packageName -o trace.html sched gfx view wm am app
优势:
轻量级,开销小
直观反映cpu利用率
根据问题给出建议(比如绘制慢或GC频繁)
Systrace具体使用不是本文重点,有兴趣的可参考: Android应用开发性能优化完全分析
StrictMode介绍
严苛模式,Android提供的一种运行时检测机制,可以用来帮助检测代码中一些不规范的问题,项目中有成千上万行代码,如果通过肉眼对代码进行review,这样不但效率低下,而且还容易遗漏问题。使用StrictMode之后,系统会自动检测出来主线程当中一些违例的情况,同时按照配置给出相应的反应。它主要用来检测两大问题,一个是线程策略,另一个是虚拟机策略,StrictMode方便强大,但是容易因为不被熟悉而忽视。
线程策略的检测内容主要包括一下几个方面
自定义的耗时调用,detectCustomSlowCalls()
磁盘读取写入操作,detectDiskReads
网络操作,detectNetwork
虚拟机策略检测内容主要包括以下几个方面
StrictMode一般用于线下检测,可以在应用的Application、Activity或者其他应用组件的onCreate方法中加入检测代码
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(UserBean.class, 1)
.detectLeakedClosableObjects() //API等级11
.penaltyLog()
.build());
}
可以通过配置peanltyLog(),在Logcat 中打印违规异常信息,或者penaltyDialog(),通过Dialog的方式提示违规异常信息。
前面简单介绍了Profiler、Systrace系统工具,但是系统工具比较适合线下问题针对性分析,但是卡顿问题和实际使用的场景紧密结合,因此线上环境及测试环节需要自动化检测方案,来帮我们定位卡顿,更重要的是记录卡顿发生时的场景。
它的原理基于Android的消息处理机制,一个线程无论有多少个Handler,都只有一个Looper,主线程中执行的任何代码,都会通过Looper.loop()分发执行,而Looper中有一个mLogging对象,它在每个message处理前后都会被调用,如果主线程发生了卡顿,一定是在dispatchMessage中执行了耗时操作,因此可以通过mLogging对dispatchMessage执行的时间进行监控。
public static void loop() {
...
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
// This must be in a local variable, in case a UI event sets the logger
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
try {
msg.target.dispatchMessage(msg);
if (observer != null) {
observer.messageDispatched(token, msg);
}
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
} catch (Exception exception) {
...
throw exception;
} finally {
...
}
...
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
}
}
使用方式类比LeakCanary。
什么是ANR呢?如果应用程序有一段时间响应不够灵敏,系统会向用户显示一个Dialog对话框,这个对话框称作应用程序无响应(ANR:Application Not Responding)对话框。容易忽视的是这个对话框是由系统服务进程SystemServer的AMS弹出的,而且是在子线程弹出的。
KeyDispatchTimeout 点击或触摸事件5s内没有响应完成,也可以说是ActivityTimeout
BroadcastTimeout,前台10s,后台60s
ServiceTimeout,前台20s, 后台200s
ContentProviderTimeout
常规方案:adb pull data/anr/traces.txt 存储路径,即通过adb命令导出anr信息文件,然后根据信息分析是由CPU、IO、锁冲突等哪些原因导致的。
ANR-Watchdog,线上ANR监控方案
这个库只有两个类,ANRError和ANRWatchdog。初始化就是通过new ANRWatchdog().start()即可。ANRWatchdog继承Thread,是一个线程类。主要看run方法即可:
public class ANRWatchDog extends Thread {
private volatile int _tick = 0;
//注释1
//更改-tick的值
private final Runnable _ticker = new Runnable() {
@Override public void run() {
_tick = (_tick + 1) % Integer.MAX_VALUE;
}
};
...
@Override
public void run() {
setName("|ANR-WatchDog|");
int lastTick;
int lastIgnored = -1;
while (!isInterrupted()) {
lastTick = _tick;
//注释2
//post更改更改-tick的值的Runnable到主线程执行
_uiHandler.post(_ticker);
try {
//注释3 sleep 一段时间_timeoutInterval= DEFAULT_ANR_TIMEOUT = 5000
Thread.sleep(_timeoutInterval);
}
catch (InterruptedException e) {
_interruptionListener.onInterrupted(e);
return ;
}
// If the main thread has not handled _ticker, it is blocked. ANR.
//注释4
// 如果更改-tick的值没有发生改变,即Runnable _ticker没有执行,表明主线程发生ANR了
if (_tick == lastTick) {
if (!_ignoreDebugger && Debug.isDebuggerConnected()) {
if (_tick != lastIgnored)
Log.w("ANRWatchdog", "An ANR was detected but ignored because the debugger is connected (you can prevent this with setIgnoreDebugger(true))");
lastIgnored = _tick;
continue ;
}
ANRError error;
if (_namePrefix != null)
error = ANRError.New(_namePrefix, _logThreadsWithoutStackTrace);
else
error = ANRError.NewMainOnly();
_anrListener.onAppNotResponding(error);
return;
}
}
}
}
ANR的原理也比较好理解,它主要有以下几个步骤:
ANRWatchdog线程类调用start方法,执行run方法,将注释1处更改-tick的值的Runnable _ticker通过post的方式,交给主线程执行。
然后sleep一段时间,默认是5s, 见注释2
判断tick的值是否发生了改变,即名为_ticker的Runnable是否执行,如果tick的值没有改变,代表Runnable没有执行,也就间接表明发生ANR了
可根据日志中的ANRError信息,进行分析定位
和前面的AndroidPerformanceMonitor的简单对比
AndroidPerformanceMonitor: 监控Message的执行,在执行前后加上时间戳,通过消息的执行之间来判定卡顿问题
ANR-WatchDog:sleep一段时间,看任务是否执行
毕竟每个message执行的时间相对较短,还不到ANR的级别,时间的粒度不同,也对应了卡顿和ANR不同,所以前者比较适合监控卡顿,后者适合ANR监控。
自动化卡顿监测方案并不能够满足所有场景的要求,比如有很多的message要执行,但是每个message的执行时间都不到卡顿的阈值。那么此时自动化监测方案不能检测出卡顿,但此时用户却觉得卡顿。IPC是比较耗时的操作,但是一般没有引起足够的重视,经常在主线程中做IPC操作,以及频繁调用,虽然没有到达卡顿的阈值,但还是会影响体验,监控维度有IPC、IO、DB、View绘制等。下面以IPC举例进行简要说明。
IPC调用类型(如PackageManager和TelephoneManager等)
调用耗时、次数
调用堆栈、发生线程
通过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
提到埋点的优雅方案就难免ARTHook或者AspectJ,他们还是有区别的ARTHook可以Hook系统方法,而AspectJ(AOP方式),它的实现是会在编译成字节码.class文件的时候,在切面点插入添加相关代码,在运行时切面点代码执行时,也会执行添加的相关代码,达到监测的目的。但是它不能针对系统方法做这些操作。
IPC跨进程都是通过Binder调用,大致流程图如下
这些IPC如PackageManager和TelephoneManager等都会调用BindProxy,所以只需要Hook这个类的transact方法即可起到监听的效果,注意方法参数要与之相对应。
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();
}
Activity生命周期间隔,或者onResume到Feed(首页列表第一条)展示的时间间隔,就是耗时监控的盲区,在统计过程容易被忽视。比如在生命周期方法onCreate中postMessage,很可能在Feed显示之前执行,假如这个message执行耗时1秒,那么Feed的展示就要延迟1秒。更多的情况是不知道这段时间主线程具体做了什么事情,一方面是添加代码的人多,另一方面各种第三方的SDK可能在这段时间有postMessage等操作,这是很普遍的,很难通过review代码排查的,再就是线上盲区就更无从排查了。
耗时盲区监控线下方案
TraceView
耗时盲区监控线上方案
思考分析
具体方案:
定制Handler如下:
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);
}
/**
* 注释1
* 定制的发送消息方法
*/
@Override
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
// 布尔值表示消息是否发送成功
boolean send = super.sendMessageAtTime(msg, uptimeMillis);
if (send) {// 发送成功
//将message对象和它的调用栈信息保存起来,后续就可以知道这个消息是由谁发送的
GetDetailHandlerHelper.getMsgDetail().put(msg, Log.getStackTraceString(new Throwable()).replace("java.lang.Throwable", ""));
}
return send;
}
/**
* 注释2
* 定制的处理消息方法
*/
@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) {
}
}
}
}
在注释1处方法,可以把要发送的msg和调用栈信息保存到GetDetailHandlerHelper类中的ConcurrentHashMap集合中,在注释2处方法,即处理消息的时候,先加上时间戳,消息处理完毕,将消息处理的耗时,以及message的调用栈信息打印出来,这样一来,就可以从日志中详细的看到message的耗时情况,以及调用栈信息(在什么地方,由谁发送和执行)了。
public class GetDetailHandlerHelper {
private static ConcurrentHashMap<Message, String> sMsgDetail = new ConcurrentHashMap<>();
public static ConcurrentHashMap<Message, String> getMsgDetail() {
return sMsgDetail;
}
}
监测到卡顿的地方,或者或者耗时较长的方法,就可以针对性的进行的调优了,具体优化可参考启动优化的相关要点,异步、延迟,根据任务的IO型或是CPU型针对性配置线程池等等。