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

前言

Android編譯期插樁,讓程序本身寫代碼(一)中我介紹了APT技術。html

Android編譯期插樁,讓程序本身寫代碼(二)中我介紹了AspectJ技術。java

本文是這一系列的最後一篇,介紹如何使用Javassist在編譯期生成字節碼。老規矩,直接上圖。android

1、Javassist

Javassist是一個可以很是方便操做字節碼的庫。它使Java程序可以在運行時新增或修改類。操做字節碼,Javassist並非惟一選擇,經常使用的還有ASM。相較於ASMJavassist效率更低。可是,Javassist提供了更友好的API,開發者們能夠在不瞭解字節碼的狀況下使用它。這一點,ASM是作不到。Javassist很是簡單,咱們經過兩個例子直觀的感覺一下。git

1.1 第一個例子

這個例子演示瞭如何經過Javassist生成一個class二進制文件。github

public class Main {

    static ClassPool sClassPool = ClassPool.getDefault();

    public static void main(String[] args) throws Exception {
        //構造新的Class MyThread。
      	CtClass myThread = sClassPool.makeClass("com.javassist.example.MyThread");
	//設置MyThread爲public的
        myThread.setModifiers(Modifier.PUBLIC);
        //繼承Thread
        myThread.setSuperclass(sClassPool.getCtClass("java.lang.Thread"));
        //實現Cloneable接口
        myThread.addInterface(sClassPool.get("java.lang.Cloneable"));

        //生成私有成員變量i
        CtField ctField = new CtField(CtClass.intType,"i",myThread);
        ctField.setModifiers(Modifier.PRIVATE);
        myThread.addField(ctField);

        //生成構造方法
        CtConstructor constructor = new CtConstructor(new CtClass[]{CtClass.intType}, myThread);
        constructor.setBody("this.i = $1;");
        myThread.addConstructor(constructor);

        //構造run方法的方法聲明
        CtMethod runMethod = new CtMethod(CtClass.voidType,"run",null,myThread);
        runMethod.setModifiers(Modifier.PROTECTED);
        //爲run方法添加註Override註解
        ClassFile classFile = myThread.getClassFile();
        ConstPool constPool = classFile.getConstPool();
        AnnotationsAttribute overrideAnnotation = new AnnotationsAttribute(constPool,AnnotationsAttribute.visibleTag);
        overrideAnnotation.addAnnotation(new Annotation("Override",constPool));
        runMethod.getMethodInfo().addAttribute(overrideAnnotation);
        //構造run方法的方法體。
      	runMethod.setBody("while (true){" +
                " try {" +
                " Thread.sleep(1000L);" +
                " } catch (InterruptedException e) {" +
                " e.printStackTrace();" +
                " }" +
                " i++;" +
                " }");

        myThread.addMethod(runMethod);

        //輸出文件到當前目錄
        myThread.writeFile(System.getProperty("user.dir"));
    }
}
複製代碼

運行程序,當前項目下生成了如下內容:api

反編譯MyThread.class,內容以下:架構

package com.javassist.example;

public class MyThread extends Thread implements Cloneable {
    private int i;
    public MyThread(int var1) {
        this.i = var1;
    }

    @Override
    protected void run() {
        while(true) {
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException var2) {
                var2.printStackTrace();
            }
            ++this.i;
        }
    }
}
複製代碼

1.2 第二個例子

這個例子演示如何修改class字節碼。咱們爲第一個例子中生成的MyTread.class擴展一些功能。app

public class Main {

    static ClassPool sClassPool = ClassPool.getDefault();

    public static void main(String[] args) throws Exception {
        //爲ClassPool指定搜索路徑。
        sClassPool.insertClassPath(System.getProperty("user.dir"));

        //獲取MyThread
        CtClass myThread = sClassPool.get("com.javassist.example.MyThread");

        //將成員變量i變成靜態的
        CtField iField = myThread.getField("i");
        iField.setModifiers(Modifier.STATIC|Modifier.PRIVATE);

        //獲取run方法
        CtMethod runMethod = myThread.getDeclaredMethod("run");
        //在run方法開始處插入代碼。
        runMethod.insertBefore("System.out.println(\"開始執行\");");
      
        //輸出新的二進制文件
        myThread.writeFile(System.getProperty("user.dir"));
    }
}

複製代碼

運行,再反編譯MyThread.class,結果以下:框架

package com.javassist.example;

public class MyThread extends Thread implements Cloneable {
    private static int i;
    public MyThread(int var1) {
        this.i = var1;
    }

    @Override
    protected void run() {
        System.out.println("開始執行");
        while(true) {
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException var2) {
                var2.printStackTrace();
            }
            ++this.i;
        }
    }
}
複製代碼

編譯期插樁對於Javassist的要求並不高,掌握了上面兩個例子就能夠實現咱們大部分需求了。若是你想了解更高級的用法,請移步這裏。接下來,我只介紹兩個類:CtClassClassPoolide

1.3 CtClass

CtClass表示字節碼中的一個類。CtClass爲咱們提供了能夠構造一個完整Class的API,例如繼承父類、實現接口、增長字段、增長方法等。除此以外,CtClass還提供了writeFile()方法,方便咱們直接輸出二進制文件。

1.4 ClassPool

ClassPool是CtClass的容器。ClassPool能夠新建(makeClass)或獲取(get)CtClass對象。在獲取CtClass對象時,即調用ClassPool.get()方法,須要在ClassPool中指定查找路徑。不然,ClassPool怎麼知道去哪裏加載字節碼文件呢。ClassPool經過鏈表維護這些查找路徑,咱們能夠經過insertClassPath()\appendClassPath()將路徑插入到鏈表的表頭\表尾。

Javassist只是操做字節碼的工具。要實現編譯期生成字節碼還須要Android Gradle爲咱們提供入口,而Transform就是這個入口。接下來咱們進入了Transform環節。

2、Transform

Transform是Android Gradle提供的,能夠操做字節碼的一種方式。App編譯時,咱們的源代碼首先會被編譯成class,而後再被編譯成dex。在class編譯成dex的過程當中,會通過一系列Transform處理。

上圖是Android Gradle定義的一系列TransformJacocoProguardInstantRunMuti-Dex等功能都是經過繼承Transform實現的。當前,咱們也能夠自定義Transform

2.1 Transform的工做原理

咱們先來了解多個Transform是如何配合工做的。直接上圖。

Transform之間採用流式處理方式。每一個Transform須要一個輸入,處理完成後產生一個輸出,而這個輸出又會做爲下一個Transform的輸入。就這樣,全部的Transform依次完成本身的使命。

Transform的輸入和輸出都是一個個的class/jar文件。

2.1.1 輸入(Input)

Transform接收輸入時,會把接收的內容封裝到一個TransformInput集合中。TransformInput由一個JarInput集合和一個DirectoryInput集合組成。JarInput表明Jar文件,DirectoryInput表明目錄。

2.1.2 輸出(Output)

Transform的輸出路徑是不容許咱們自由指定的,必須根據名稱、做用範圍、類型等由TransformOutputProvider生成。具體代碼以下:

String dest = outputProvider.getContentLocation(directoryInput.name,
                        directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
複製代碼

2.2 自定義Transform

2.2.1 繼承Transform

咱們先看一下繼承Transform須要實現的方法。

public class CustomCodeTransform extends Transform {
    @Override
    public String getName() {
        return null;
    }

    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
        return null;
    }

    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
        return null;
    }

    @Override
    public boolean isIncremental() {
        return false;
    }
  
    @Override
    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation);
    }
}
複製代碼
  • getName():Transform起一個名字。

  • getInputTypes():Transform要處理的輸入類型。DefaultContentType提供了兩種類型的輸入方式:

    1. CLASSES: java編譯後的字節碼,多是jar包也多是目錄。
    2. RESOURCES: 標註的Java資源。

    TransformManager爲咱們封裝了InputTypes。具體以下:

    public static final Set<ContentType> CONTENT_CLASS = ImmutableSet.of(CLASSES);
        public static final Set<ContentType> CONTENT_JARS = ImmutableSet.of(CLASSES, RESOURCES);
        public static final Set<ContentType> CONTENT_RESOURCES = ImmutableSet.of(RESOURCES);
    複製代碼
  • getScopes():Transform的處理範圍。它約定了Input的接收範圍。Scope中定義瞭如下幾種範圍:

    1. PROJECT: 只處理當前項目。
    2. SUB_PROJECTS: 只處理子項目。
    3. PROJECT_LOCAL_DEPS: 只處理項目本地依賴庫(本地jars、aar)。
    4. PROVIDED_ONLY: 只處理以provided方式提供的依賴庫。
    5. EXTERNAL_LIBRARIES: 只處理全部外部依賴庫。
    6. SUB_PROJECTS_LOCAL_DEPS: 只處理子項目的本地依賴庫(本地jars、aar)
    7. TESTED_CODE: 只處理測試代碼。

    TransformManager也爲咱們封裝了經常使用的Scope。具體以下:

    public static final Set<ScopeType> PROJECT_ONLY = 
            ImmutableSet.of(Scope.PROJECT);
    
    public static final Set<Scope> SCOPE_FULL_PROJECT =
            Sets.immutableEnumSet(
                    Scope.PROJECT,
                    Scope.SUB_PROJECTS,
                    Scope.EXTERNAL_LIBRARIES);
    
    public static final Set<ScopeType> SCOPE_FULL_WITH_IR_FOR_DEXING =
            new ImmutableSet.Builder<ScopeType>()
                    .addAll(SCOPE_FULL_PROJECT)
                    .add(InternalScope.MAIN_SPLIT)
                    .build();
    
    public static final Set<ScopeType> SCOPE_FULL_LIBRARY_WITH_LOCAL_JARS =
            ImmutableSet.of(Scope.PROJECT, InternalScope.LOCAL_DEPS);
    複製代碼
  • isIncremental(): 是否支持增量更新。

  • transform(): 這裏就是咱們具體的處理邏輯。經過參數TransformInvocation,咱們能夠得到輸入,也能夠獲取決定輸出的TransformOutputProvider

    public interface TransformInvocation {
       /** * Returns the inputs/outputs of the transform. * @return the inputs/outputs of the transform. */
        @NonNull
        Collection<TransformInput> getInputs();
      	
       /** * Returns the output provider allowing to create content. * @return he output provider allowing to create content. */
        @Nullable
        TransformOutputProvider getOutputProvider();
    }
    複製代碼
2.2.2自定義插件,集成Transform

下面到了集成Transform環節。集成Transform須要自定義gradle 插件。寫給Android 開發者的Gradle系列(三)撰寫 plugin介紹了自定義gradle插件的步驟,咱們跟着它就能夠實現一個插件。而後就能夠將CustomCodeTransform註冊到gradle的編譯流程了。

class CustomCodePlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
         AppExtension android = project.getExtensions().getByType(AppExtension.class);
      	 android.registerTransform(new RegisterTransform());
    }
}
複製代碼

3、一個簡易的組件化Activity路由框架

在Android領域,組件化通過多年的發展,已經成爲一種很是成熟的技術。組件化是一種項目架構,它將一個app項目拆分紅多個組件,而各個組件間互不依賴。

既然組件間是互不依賴的,那麼它們就不能像普通項目那樣進行Activity跳轉。那應該怎麼辦呢?下面咱們就來具體了學習一下。

咱們的Activity路由框架有兩個module組成。一個module用來提供API,咱們命名爲common;另外一個module用來處理編譯時字節碼的注入,咱們命名爲plugin

咱們先來看一下common。它只有兩個類,以下:

public interface IRouter {
    void register(Map<String,Class> routerMap);
}
複製代碼
public class Router {

    private static Router INSTANCE;
    private Map<String, Class> mRouterMap = new ConcurrentHashMap<>();

    //單例
    private static Router getInstance() {
        if (INSTANCE == null) {
            synchronized (Router.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Router();
                }
            }
        }
        return INSTANCE;
    }

    private Router() {
        init();
    }
    //在這裏字節碼注入。
    private void init() { }

    /** * Activity跳轉 * @param context * @param activityUrl Activity路由路徑。 */
    public static void startActivity(Context context, String activityUrl) {
        Router router = getInstance();
        Class<?> targetActivityClass = router.mRouterMap.get(activityUrl);

        Intent intent = new Intent(context,targetActivityClass);
        context.startActivity(intent);
    }
}

複製代碼

common的這兩個類十分簡單。IRouter是一個接口。Router對外的方法只有一個startActivity

接下來,咱們跳過plugin,先學習一下框架怎麼使用。假如咱們的項目被拆分紅app、A、B三個module。其中app是一個殼工程,只負責打包,依賴於A、B。A和B是普通的業務組件,A、B之間互不依賴。如今,A組件中有一個AActivity,B組件想跳轉到AActivity。怎麼作呢?

在A組件中新建一個ARouterImpl實現IRouter

public class ARouterImpl implements IRouter {

    private static final String AActivity_PATH = "router://a_activity";

    @Override
    public void register(Map<String, Class> routerMap) {
        routerMap.put(AActivity_PATH, AActivity.class);
    }
}
複製代碼

在B組件中調用時,只須要

Router.startActivity(context,"router://a_activity");
複製代碼

是否是很神奇?其實奧妙就在plugin中。編譯時,pluginRouterinit()中注入了以下代碼:

private void init() { 
		ARouterImpl var1 = new ARouterImpl();
  	var.register(mRouterMap);
}
複製代碼

plugin中的代碼有點多,我就不貼出來了。這一節的代碼都在這裏

這個Demo很是簡單,可是它對於理解ARouter、WMRouter等路由框架的原理十分有用。它們在處理路由表的註冊時,都是採用編譯期字節碼注入的方式,只不過它們沒有使用javassit,而是使用了效率更高的ASM。它們用起來更方即是由於,它們利用APT技術把路徑和Activity之間的映射變透明瞭。即:相似於Demo中的ARouterImpl這種代碼,都是經過APT生成的。

相關文章
相關標籤/搜索