最常用的字节码处理框架有 AspectJ、ASM 等等,它们的相同之处在于输入输出都是 Class 文件。并且,它们都是 在 Java 文件编译成 .class 文件之后,生成 Dalvik 字节码之前执行。 而 AspectJ 作为 Java 中流行的 AOP(aspect-oriented programming) 编程扩展框架,其内部使用的是 BCEL框架 来完成其功能。
它的优势有两点:成熟稳定、使用非常简单。
使用非常简单 AspectJ 可以在如下五个位置插入自定义的代码:
1)、在方法(包括构造方法)被调用的位置。 2)、在方法体(包括构造方法)的内部。 3)、在读写变量的位置。 4)、在静态代码块内部。 5)、在异常处理的位置的前后。 此外,它也可以 直接将原位置的代码替换为自定义的代码。
AspectJ缺点: 1.切入点固定 AspectJ 只能在一些固定的切入点来进行操作
2.正则表达式的局限性 AspectJ 的匹配规则采用了类似正则表达式的规则,比如 匹配 Activity 生命周期的 onXXX 方法,如果有自定义的其他以 on 开头的方法也会匹配到,这样匹配的正确性就无法满足。
3.性能较低 AspectJ 在实现时会包装自己一些特定的类,它并不会直接把 Trace 函数直接插入到代码中,导致生成的字节码比较大,影响性能,如果想对 App 中所有的函数都进行插桩,性能影响肯定会比较大。如果你只插桩一小部分函数,那么 AspectJ 带来的性能损耗几乎可以忽略不计。
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.0'
然后,在 app 目录下的 build.gradle 下加入:
apply plugin: 'android-aspectjx'
implement 'org.aspectj:aspectjrt:1.8.+'
@Before("execution(* android.app.Activity.on**(..))")
public void onActivityCalled(JoinPoint joinPoint) throws Throwable {
Log.d(...)
}
execution 中的是一个匹配规则,第一个 * 代表匹配任意的方法返回值,后面的语法代码匹配所有 Activity 中以 on 开头的方法。这样,我们就可以 在 App 中所有 Activity 中以 on 开头的方法中打印一句 log。
上面的 execution 就是处理 Join Point 的类型,通常有如下两种类型:
1)、call:代表调用方法的位置,插入在函数体外面。 2)、execution:代表方法执行的位置,插入在函数体内部。
@Aspect
public class ApplicationAop {
@Around("call (* com.json.chao.application.BaseApplication.**(..))")
public void getTime(ProceedingJoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String name = signature.toShortString();
long time = System.currentTimeMillis();
try {
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
Log.i(TAG, name + " cost" + (System.currentTimeMillis() - time));
}
}
当 Action 为 Before、After 时,方法入参为 JoinPoint。当 Action 为 Around 时,方法入参为 ProceedingPoint。 而 Around 和 Before、After 的最大区别就是 ProceedingPoint 不同于 JoinPoint,其提供了 proceed 方法执行目标方法。
@Aspect
public class SystraceTraceAspectj {
private static final String TAG = "SystraceTraceAspectj";
@Before("execution(* **(..))")
public void before(JoinPoint joinPoint) {
TraceCompat.beginSection(joinPoint.getSignature().toString());
}
@After("execution(* **(..))")
public void after() {
TraceCompat.endSection();
}
}
奇虎360的 ArgusAPM 性能监控框架来全面分析下 AOP 技术在性能监控方面的应用。主要分为如下 三个部分:
1)、监控应用冷热启动耗时与生命周期耗时。 2)、监控 OKHttp3 的每一次网络请求。 3)、监控 HttpConnection 的每一次网络请求。
@Aspect
public class TraceActivity {
// 1、定义一个切入点方法 baseCondition,用于排除 argusapm 中相应的类。
@Pointcut("!within(com.argusapm.android.aop.*) && !within(com.argusapm.android.core.job.activity.*)")
public void baseCondition() {
}
// 2、定义一个切入点 applicationOnCreate,用于执行 Application 的 onCreate方法。
@Pointcut("execution(* android.app.Application.onCreate(android.content.Context)) && args(context)")
public void applicationOnCreate(Context context) {
}
// 3、定义一个后置通知 applicationOnCreateAdvice,用于在 application 的 onCreate 方法执行完之后插入 AH.applicationOnCreate(context) 这行代码。
@After("applicationOnCreate(context)")
public void applicationOnCreateAdvice(Context context) {
AH.applicationOnCreate(context);
}
// 4、定义一个切入点,用于执行 Application 的 attachBaseContext 方法。
@Pointcut("execution(* android.app.Application.attachBaseContext(android.content.Context)) && args(context)")
public void applicationAttachBaseContext(Context context) {
}
// 5、定义一个前置通知,用于在 application 的 onAttachBaseContext 方法之前插入 AH.applicationAttachBaseContext(context) 这行代码。
@Before("applicationAttachBaseContext(context)")
public void applicationAttachBaseContextAdvice(Context context) {
AH.applicationAttachBaseContext(context);
}
// 6、定义一个切入点,用于执行所有 Activity 中以 on 开头的方法,后面的 ”&& baseCondition()“ 是为了排除 ArgusAPM 中的类。
@Pointcut("execution(* android.app.Activity.on**(..)) && baseCondition()")
public void activityOnXXX() {
}
// 7、定义一个环绕通知,用于在所有 Activity 的 on 开头的方法中的开始和结束处插入相应的代码。(排除了 ArgusAPM 中的类)
@Around("activityOnXXX()")
public Object activityOnXXXAdvice(ProceedingJoinPoint proceedingJoinPoint) {
Object result = null;
try {
Activity activity = (Activity) proceedingJoinPoint.getTarget();
// Log.d("AJAOP", "Aop Info" + activity.getClass().getCanonicalName() +
// "\r\nkind : " + thisJoinPoint.getKind() +
// "\r\nargs : " + thisJoinPoint.getArgs() +
// "\r\nClass : " + thisJoinPoint.getClass() +
// "\r\nsign : " + thisJoinPoint.getSignature() +
// "\r\nsource : " + thisJoinPoint.getSourceLocation() +
// "\r\nthis : " + thisJoinPoint.getThis()
// );
long startTime = System.currentTimeMillis();
result = proceedingJoinPoint.proceed();
String activityName = activity.getClass().getCanonicalName();
Signature signature = proceedingJoinPoint.getSignature();
String sign = "";
String methodName = "";
if (signature != null) {
sign = signature.toString();
methodName = signature.getName();
}
if (!TextUtils.isEmpty(activityName) && !TextUtils.isEmpty(sign) && sign.contains(activityName)) {
invoke(activity, startTime, methodName, sign);
}
} catch (Exception e) {
e.printStackTrace();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return result;
}
public void invoke(Activity activity, long startTime, String methodName, String sign) {
AH.invoke(activity, startTime, methodName, sign);
}
}
我们注意到,在注释4、5这两处代码是用于 在 application 的 onAttachBaseContext 方法之前插入 AH.applicationAttachBaseContext(context) 这行代码。此外,注释2、3两处的代码是用于 在 application 的 onCreate 方法执行完之后插入 AH.applicationOnCreate(context) 这行代码。下面,我们再看看 AH 类中这两个方法的实现,代码如下所示:
public static void applicationAttachBaseContext(Context context) {
ActivityCore.appAttachTime = System.currentTimeMillis();
if (Env.DEBUG) {
LogX.d(Env.TAG, SUB_TAG, "applicationAttachBaseContext time : " + ActivityCore.appAttachTime);
}
}
public static void applicationOnCreate(Context context) {
if (Env.DEBUG) {
LogX.d(Env.TAG, SUB_TAG, "applicationOnCreate");
}
}
在 AH 类的 applicationAttachBaseContext 方法中将启动时间 appAttachTime 记录到了 ActivityCore 实例中。而 applicationOnCreate 基本上什么也没有实现。 然后,我们再回到切面文件 TraceActivity 中,看到注释6、7处的代码,这里用于 在所有 Activity 的 on 开头的方法中的开始和结束处插入相应的代码。这里排除了 ArgusAPM 中的类。 下面,我们来分析下 activityOnXXXAdvice 方法中的操作。首先,在目标方法执行前获取了 startTime。然后,调用了 proceedingJoinPoint.proceed() 用于执行目标方法;最后,调用了 AH 类的 invoke 方法。我们看看 invoke 方法的处理,代码如下所示:
public static void invoke(Activity activity, long startTime, String lifeCycle, Object... extars) {
// 1
boolean isRunning = isActivityTaskRunning();
if (Env.DEBUG) {
LogX.d(Env.TAG, SUB_TAG, lifeCycle + " isRunning : " + isRunning);
}
if (!isRunning) {
return;
}
// 2
if (TextUtils.equals(lifeCycle, ActivityInfo.TYPE_STR_ONCREATE)) {
ActivityCore.onCreateInfo(activity, startTime);
} else {
// 3
int lc = ActivityInfo.ofLifeCycleString(lifeCycle);
if (lc <= ActivityInfo.TYPE_UNKNOWN || lc > ActivityInfo.TYPE_DESTROY) {
return;
}
ActivityCore.saveActivityInfo(activity, ActivityInfo.HOT_START, System.currentTimeMillis() - startTime, lc);
}
}
在注释1处,我们会先去查看当前应用的 Activity 耗时统计任务是否打开了。如果打开了,然后就会走到注释2处,这里 会先判断目标方法名称是否是 “onCreate”,如果是 onCreate 方法,就会执行 ActivityCore 的 onCreateInfo 方法,代码如下所示:
// 是否是第一次启动
public static boolean isFirst = true;
public static long appAttachTime = 0;
// 启动类型
public static int startType;
public static void onCreateInfo(Activity activity, long startTime) {
// 1
startType = isFirst ? ActivityInfo.COLD_START : ActivityInfo.HOT_START;
// 2
activity.getWindow().getDecorView().post(new FirstFrameRunnable(activity, startType, startTime));
//onCreate 时间
long curTime = System.currentTimeMillis();
// 3
saveActivityInfo(activity, startType, curTime - startTime, ActivityInfo.TYPE_CREATE);
}
/**
* OKHTTP3 切面文件
*
* @author ArgusAPM Team
*/
@Aspect
public class OkHttp3Aspect {
// 1、定义一个切入点,用于直接调用 OkHttpClient 的 build 方法。
@Pointcut("call(public okhttp3.OkHttpClient build())")
public void build() {
}
// 2、使用环绕通知在 build 方法执行前添加一个 NetWokrInterceptor。
@Around("build()")
public Object aroundBuild(ProceedingJoinPoint joinPoint) throws Throwable {
Object target = joinPoint.getTarget();
if (target instanceof OkHttpClient.Builder && Client.isTaskRunning(ApmTask.TASK_NET)) {
OkHttpClient.Builder builder = (OkHttpClient.Builder) target;
builder.addInterceptor(new NetWorkInterceptor());
}
return joinPoint.proceed();
}
}
@Override
public Response intercept(Chain chain) throws IOException {
// 1、获取每一个 OkHttp 请求的开始时间
long startNs = System.currentTimeMillis();
mOkHttpData = new OkHttpData();
mOkHttpData.startTime = startNs;
if (Env.DEBUG) {
Log.d(TAG, "okhttp request 开始时间:" + mOkHttpData.startTime);
}
Request request = chain.request();
// 2、记录当前请求的请求 url 和请求数据大小
recordRequest(request);
Response response;
try {
response = chain.proceed(request);
} catch (IOException e) {
if (Env.DEBUG) {
e.printStackTrace();
Log.e(TAG, "HTTP FAILED: " + e);
}
throw e;
}
// 3、记录这次请求花费的时间
mOkHttpData.costTime = System.currentTimeMillis() - startNs;
if (Env.DEBUG) {
Log.d(TAG, "okhttp chain.proceed 耗时:" + mOkHttpData.costTime);
}
// 4、记录当前请求返回的响应码和响应数据大小
recordResponse(response);
if (Env.DEBUG) {
Log.d(TAG, "okhttp chain.proceed end.");
}
// 5、记录 OkHttp 的请求数据
DataRecordUtils.recordUrlRequest(mOkHttpData);
return response;
}
1.定义切点为OkHttpClient 的 build 方法 2.使用环绕通知在 build 方法执行前添加一个 NetWokrInterceptor。 3.在NetWokrInterceptor中执行chain.proceed(request);前后记录时间差即可,因为责任链模式关系,最后所有Interceptor执行完才回到NetWokrInterceptor