Android方法耗时监控工具

1、背景

为了监控一些方法耗时

2、需求

要实现以下功能:

  1. Application onCreate()方法耗时
  2. Activity生命周期方法耗时
  3. Fragment生命周期方法耗时(TODO)
  4. 自定义方法耗时
  5. 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 。页面加载流程图如下:

方法耗时 iOS 方法耗时监控页面_asm

根据上图,得到如下指标(粗略计算):

指标

描述

计算方式

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();
        }
    }
}

方法耗时 iOS 方法耗时监控页面_asm_02

Launch:计算App onCreate()到MainActivity onResume()之间的耗时

intiSdk:单个方法耗时