Android編譯期插樁,讓程序本身寫代碼(二)

前言

在上篇文章Android編譯期插樁,讓程序本身寫代碼(一)的前言部分我放了一張圖,用來講明編譯期插樁的位置和相應的技術。這裏,我還打算這張圖來開篇。 java

AspectJ

在上圖中,咱們能夠清楚的看到AspectJ的插樁位置是.java與.class之間。這很容易令人聯想到編譯器。事實上,AspectJ就是一種編譯器,它在Java編譯器的基礎上增長了關鍵字識別和編譯方法。所以,AspectJ能夠編譯Java代碼。它還提供了Aspect程序。在編譯期間,將開發者編寫的Aspect程序織入到目標程序中,擴展目標程序的功能。android

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

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

  • 一個簡單的Hugo框架。
  • 從字節碼分析AspectJ。

Hugo

Hugo是JakeWharton基於AspectJ開源的一個調試框架,其功能是經過註解的方式能夠打印出方法的運行時間,方便開發者性能調優。今天,咱們就來看一下它的廬山真面目。後端

配置AspectJ

Hugo是基於AspectJ的,那首先咱們就要支持AspectJ。這裏向你們推薦滬江的AspectJX,它不只使用簡單,並且還支持過濾一些aar或jar包。api

首先咱們須要在根build.gradle中依賴AspectJXbash

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

在app項目的build.gradle裏應用插件,並添加aspectj的依賴庫app

apply plugin: 'android-aspectjx'

api 'org.aspectj:aspectjrt:1.8.9'
複製代碼

這樣就配置完成了,是否是很簡單啊。框架

注意:筆者這裏採用的gradle版本是3.0.1,若是沒有編譯經過,檢查一下gradle版本。post

定義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);
    }
}
複製代碼

咱們看一下日誌,方法的運行時間被完美的打印出來了。

從字節碼分析AspectJ

咱們仍然以Test爲例,看一下Test反編以後的字節碼。

反編譯的內容看起來不太方便,我在這裏把它轉換成了以下代碼:

爲了觀看方便,上圖將代碼分爲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是能夠的,可是若是涉及範圍較大就要慎重考慮了。

相關文章
相關標籤/搜索