Android方法耗时监控工具
1、背景
为了监控一些方法耗时
2、需求
要实现以下功能:
- Application onCreate()方法耗时
- Activity生命周期方法耗时
- Fragment生命周期方法耗时(TODO)
- 自定义方法耗时
- webview网页加载耗时
- …
3、实现
3.1、技术方案
利用Transform + ASM字节码修改技术动态插入代码
3.2、Application onCreate()方法耗时
考虑到App多重继承的情况,即App继承BaseApp,BaseApp继承Application;或者三方库需要代理App类,如Tinker。因此为了方便、统一处理,通过注解的方式,显示指定App类。
/**
* @description app类注解,用于指定启动的App类
* @Author pxq
* @Date 2020/8/7 17:44
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface App {
}
@App
public class MyApp extends BaseApp {
@Override
public void onCreate() {
//todo ...
}
}
3.3、Activity生命周期方法耗时
3.3.1、理论
1、Hook Instrumentation
优点:可以精确到各个生命周期
缺点:很多框架都Hook,比如插件化,需要重写大量方法兼容
2、Hook Handler或者Looper的Printer
优点:简单
缺点:只能无法精确到各个生命周期,只能计算onCreate 到 onResume的时间、存在Android版本兼容问题
参考:
3.3.2、 Hook Instrumentation
Hook Instrumentation是比较完美的方案,这里使用这种方式。
/**
* @description 自定义代理Instrumentation,统计Activity生命周期耗时
* @Author panxq
* @Date 2020/8/7 17:59
*/
public class ProxyInstrumentation extends Instrumentation {
private Instrumentation baseIns;
// 控制一些变量,如打印阈值,是否重复打印等
private ActivityWire activityWire;
public ProxyInstrumentation(Instrumentation baseIns, ActivityWire activityWire) {
this.baseIns = baseIns;
this.activityWire = activityWire;
}
@Override
public void callActivityOnCreate(Activity activity, Bundle icicle) {
// 计算onCreate执行耗时
callOnPrev(activity, Trace.TRACE_ON_CREATE);
baseIns.callActivityOnCreate(activity, icicle);
callOnPost(activity, Trace.TRACE_ON_CREATE);
}
...下同
}
/**
* @description ActivityThread hook类
* @Author pxq
* @Date 2020/8/7 17:50
*/
public class ActivityThreadHooker {
private static final String TAG = "ActivityThreadHooker";
public static void hookIns(ActivityWire activityWire) throws Exception {
// 获取ActivityThread类
@SuppressLint("PrivateApi")
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
// 获取currentActivityThread方法
@SuppressLint("DiscouragedPrivateApi")
Method currentActivityThread = activityThreadClass.getDeclaredMethod("currentActivityThread");
// 获取activityThread对象
Object activityThreadObj = currentActivityThread.invoke(null);
Log.d(TAG, "hookIns: activityThreadObj " + (activityThreadObj == null));
// 获取mInstrumentation成员变量
Field mInstrumentationField = activityThreadClass.getDeclaredField("mInstrumentation");
mInstrumentationField.setAccessible(true);
// 获取mInstrumentation对象
Instrumentation instrumentation = (Instrumentation) mInstrumentationField.get(activityThreadObj);
// 替换成代理对象
mInstrumentationField.set(activityThreadObj, new ProxyInstrumentation(instrumentation, activityWire));
}
}
3.4、自定义方法耗时
同样通过注解标记要计算的方法,分两种情况
- 单个方法:只计算当前方法的耗时
- 跨方法:计算开始的方法,到结束方法这一段时间的耗时
/**
* @description 自定义方法打印,单个方法
* @Author pxq
* @Date 2020/8/7 17:44
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface Trace {
String traceTag(); // Trace Tag
int threshold() default 0; // 打印阈值
boolean repeat() default true; // 是否重复打印
}
/**
* @description 自定义方法计时开始
* @Author pxq
* @Date 2020/8/7 17:46
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface TraceBegin {
String traceTag(); // Trace Tag
String traceMethod() default ""; // Trace Method
int threshold() default 0; // 打印阈值
boolean repeat() default true; // 是否重复打印
}
/**
* @description 自定义方法计时结束
* @Author pxq
* @Date 2020/8/7 17:49
*/
public @interface TraceEnd {
String traceTag(); // Trace Tag
String traceMethod() default ""; // Trace Method
}
3.5、ASM写入的代码
在指定方法的开始插入打印开始方法,保存当前时间;在指定方法结束出插入打印结束的方法打印出方法耗时Log
/**
* @description 打印工具
* @Author pxq
* @Date 2020/8/7 18:07
*/
public class Trace {
/**
* 保存要计算的方法一些信息
* traceTag1 :
* methodName1 -- startTime1
* methodName2 -- startTime2
* traceTag2 :
* methodName1 -- startTime1
* methodName2 -- startTime2
*/
private static Map<String, Map<String, Traceable>> traceMap = new HashMap<>();
/**
* 记录打印开始信息
* [traceTag]: 打印TAG
* [methodName]:打印方法
* [start]: 打印开始时间
*/
public static void traceBegin(String traceTag, String traceMethod, int threshold, boolean repeat, long start) {
...
}
/**
* 打印方法耗时
* [traceTag]: 打印TAG
* [methodName]: 打印方法
* [end]: 打印结束时间
*/
public static void traceEnd(String traceTag, String traceMethod) {
...
}
}
3.6、网页加载耗时
主要利用了 Performance API 。页面加载流程图如下:
根据上图,得到如下指标(粗略计算):
指标 | 描述 | 计算方式 |
redirect | 重定向 | redirectEnd - redirectStart |
dns | 域名解析 | domainLookUpEnd - domainLookUpStart |
trans | html网络请求 | responseEnd - requestStart |
dom | DOM解析 | domInteractive - responseEnds |
fpt | 白屏时间 | responseEnd - navigationStart |
load | 页面完全加载 | loadEventStart - navigationStart |
要获取上述数据,需要把本地的js代码注入到webview中并执行,注入时机是网页加载完毕的时候(因为大多数数据加载完毕才能得到)。
1、主要代码:获取navigationTiming、resourceTiming的方法并执行,回调给android端
console.log("... webpm inject success!!!");
function printPerformanceEntries() {
// 需要吧webPM对象注入进来
if(!window.webPM){
return;
}
window.webPM.onNavigation(JSON.stringify(window.performance.timing)); //获取网页加载相关事件时间
var resJson = {};
var p = performance.getEntriesByType("resource");
resJson["size"] = p.length;
var resArray = [];
for (var i=0; i < p.length; i++) {
resArray.push(parseRes(p[i]));
}
resJson["recs"] = resArray;
window.webPM.onResource(JSON.stringify(resJson));
}
function parseRes(perfEntry) { //转成json字符串
var jsonObj = {};
var properties = [
"name",
"entryType",
"startTime",
"duration",
"initiatorType",
"redirectStart",
"redirectEnd",
"fetchStart",
"domainLookupStart",
"domainLookupEnd",
"connectStart",
"connectEnd",
"secureConnectionStart",
"requestStart",
"responseStart",
"responseEnd"
];
for (var i=0; i < properties.length; i++) {
var supported = properties[i] in perfEntry;
if (supported) {
var value = perfEntry[properties[i]];
jsonObj[properties[i]] = value;
} else {
jsonObj[properties[i]] = "0";
}
}
return jsonObj;
}
printPerformanceEntries();
2、调用
在网页加载完毕时调用
mWebView.setWebViewClient(new WebViewClient(){
...
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
// 注入js
Cage.onPageFinish(mWebView);
}
});
4、测试
/**
* @description 启动Application
* @Author pxq
* @Date 2020/8/17 11:47
*/
@App
public class MyApp extends Application {
@TraceBegin(traceTag = "Launch") // 计算一段方法耗时
@Override
public void onCreate() {
super.onCreate();
Log.d("MyApp", "onCreate: sdk init");
initSdk();
// hook入口
Cage.install();
}
@Trace(traceTag = "initSdk") // 计算当个方法耗时
private int initSdk() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return initSdk1();
}
private int initSdk1() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 100;
}
}
// MainActivity
public class MainActivity extends AppCompatActivity {
...
@TraceEnd(traceTag = "Launch") // Launch结束
@Override
protected void onResume() {
super.onResume();
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Launch:计算App onCreate()到MainActivity onResume()之间的耗时
intiSdk:单个方法耗时