Android編譯期插樁,讓程式自己寫程式碼(二)

青黴素發表於2019-04-18

前言

在上篇文章Android編譯期插樁,讓程式自己寫程式碼(一)的前言部分我放了一張圖,用來說明編譯期插樁的位置和相應的技術。這裡,我還打算這張圖來開篇。

Android編譯期插樁,讓程式自己寫程式碼(二)

AspectJ

在上圖中,我們可以清楚的看到AspectJ的插樁位置是.java與.class之間。這很容易使人聯想到編譯器。事實上,AspectJ就是一種編譯器,它在Java編譯器的基礎上增加了關鍵字識別和編譯方法。因此,AspectJ可以編譯Java程式碼。它還提供了Aspect程式。在編譯期間,將開發者編寫的Aspect程式織入到目標程式中,擴充套件目標程式的功能。

AspectJ可以應用於Android和後端開發中。在後端,AspectJ 應用更為廣泛一些,著名的Spring框架就對AspectJ提供了支援。不過,近些年,AspectJ技術在Android領域也開始嶄露頭角,比較知名的有JakeWharton的hugo。另外,一些企業也開始探索AspectJ在埋點、許可權管理等方面的應用。

關於AspectJ更為詳細的介紹,請大家移步鄧平凡大神的部落格深入理解Android之AOP。這篇文章對於初次接觸AspectJ的人來說十分友好,筆者最初就是通過它進入AspectJ殿堂的。珠玉在前,本文就不再介紹AspectJ的基礎知識了。那本文要說些什麼呢?

  • 一個簡單的Hugo框架。
  • 從位元組碼分析AspectJ。

Hugo

Hugo是JakeWharton基於AspectJ開源的一個除錯框架,其功能是通過註解的方式可以列印出方法的執行時間,方便開發者效能調優。今天,我們就來看一下它的廬山真面目。

配置AspectJ

Hugo是基於AspectJ的,那首先我們就要支援AspectJ。這裡向大家推薦滬江的AspectJX,它不僅使用簡單,而且還支援過濾一些aar或jar包。

首先我們需要在根build.gradle中依賴AspectJX

dependencies {
        classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.4'
        }
複製程式碼

在app專案的build.gradle裡應用外掛,並新增aspectj的依賴庫

apply plugin: 'android-aspectjx'

api 'org.aspectj:aspectjrt:1.8.9'
複製程式碼

這樣就配置完成了,是不是很簡單啊。

注意:筆者這裡採用的gradle版本是3.0.1,如果沒有編譯通過,檢查一下gradle版本。

定義DebugLog註解

十分簡單,直接上程式碼


@Target({METHOD, CONSTRUCTOR})
@Retention(CLASS)
public @interface DebugLog {
}
複製程式碼

編寫Aspect


@Aspect
public class Hugo {

  @Pointcut("execution(@com.hugo.example.lib.DebugLog * *(..))")
  public void method() {}

  @Pointcut("execution(@com.hugo.example.lib.DebugLog *.new(..))")
  public void constructor() {}

  @Around("method() || constructor()")
  public Object logAndExecute(ProceedingJoinPoint joinPoint) throws Throwable {

    CodeSignature codeSignature = (CodeSignature) joinPoint.getSignature();
    Class<?> cls = codeSignature.getDeclaringType();
    String methodName = codeSignature.getName();
    long startNanos = System.nanoTime();
    
    Object result = joinPoint.proceed();
    
    long stopNanos = System.nanoTime();
    long lengthMillis = TimeUnit.NANOSECONDS.toMillis(stopNanos - startNanos);
    StringBuilder builder = new StringBuilder();
    builder.append("methodName:")
            .append(methodName)
            .append("  ----  executeTime:")
            .append(lengthMillis);

    Log.e(asTag(cls), builder.toString());

    return result;
  }

  private static String asTag(Class<?> cls) {
    if (cls.isAnonymousClass()) {
      return asTag(cls.getEnclosingClass());
    }
    return cls.getSimpleName();
  }
}

複製程式碼

如果你學習了深入理解Android之AOP,那麼這段程式碼應該很容易能看懂。這裡我簡單解釋一下。

  1. 選取註解了DebugLog的method和constructor作為pointcut。
  2. 在原方法執行前織入開始時間,在原方法執行後織入結束時間,並計算出執行時間。通過Log.v列印出來。

到此,這個簡化版的Hugo基本就介紹完了。你可以做個Demo試一下了。

  1. 筆者DebugLog的包名是com.hugo.example.lib。因此在method()和constractor()中的註解內容是com.hugo.example.lib.DebugLog。
  2. 其實,真正的Hugo框架核心也只有一個類。它只是在日誌列印時,輸出了更多的方法資訊。本文為了方便讀者理解作了簡化。

測試

我們定義一個Test類,然後在Activity啟動的時候呼叫myThread()方法。Test類如下:

public class Test {

    @DebugLog
    public void myMethod1() throws Exception{
        Thread.sleep(1000);
    }
}
複製程式碼

我們看一下日誌,方法的執行時間被完美的列印出來了。

Android編譯期插樁,讓程式自己寫程式碼(二)

從位元組碼分析AspectJ

我們仍然以Test為例,看一下Test反編之後的位元組碼。

Android編譯期插樁,讓程式自己寫程式碼(二)

反編譯的內容看起來不太方便,我在這裡把它轉換成了如下程式碼:

Android編譯期插樁,讓程式自己寫程式碼(二)

為了觀看方便,上圖將程式碼分為4部分。

我們先看第一部分,這是一個靜態程式碼塊,也就是說在類載入的時候,程式會AspectJ提供的Factory類,建立一個型別為JoinPoint.StaticPart靜態例項STATIC_PART。深入理解Android之AOP中對JoinPoint.StaticPart介紹如下:

thisJoinPointStaticPart物件:在advice程式碼中可直接使用,代表JPoint中那些不變的東西。比如這個JPoint的型別,JPoint所處的程式碼位置等。這裡thisJoinPointStaticPart就是程式碼中的JoinPoint.StaticPart。

第二部分是我們之前定義的myThread方法,它在編譯期間被替換了。在執行時,它首先通過Factory的靜態方法makeJP建立一個JoinPoint物件。makeJp是一個過載方法,我們看一下。

public static JoinPoint makeJP(JoinPoint.StaticPart staticPart, Object _this, Object target) {
    return new JoinPointImpl(staticPart, _this, target, NO_ARGS);
}
public static JoinPoint makeJP(JoinPoint.StaticPart staticPart, Object _this, Object target, Object[] args) {
    return new JoinPointImpl(staticPart, _this, target, args);
}
複製程式碼

通過我列出來了兩個,可以看到JoinPoint除了包含了我們第一步中提到了STATIC_PART物件,還包括了this,target物件,以及方法引數。這和深入理解Android之AOP中對thisJoinpoint描述也是一致的:

thisJoinpoint物件:在advice程式碼中可直接使用。代表JPoint每次被觸發時的一些動態資訊,比如引數啊之類的。

建立完JoinPoint物件後,隨後呼叫了第三部分中的advice()方法。advice()方法大部分都是我們在Hugo中編寫的織入程式碼,這裡只有一個不同,那就是joinPoint.proceed()不見了,替換成了原始碼中具體的處理邏輯。

總結

通過上述分析,我們可以清楚的感知到AspectJ提供了非常強大的功能。但同時,由於其為每個切入點生成一個JoinPoint.StaticPar靜態例項和在執行過程中生成的JoinPoint以及一些其它的封裝,這必然會導致程式在記憶體和處理速度等方面受影響。因此,在小範圍內使用AspectJ是可以的,但是如果涉及範圍較大就要慎重考慮了。

相關文章