AppJoint源碼解析

AppJoint的實現核心代碼主要在其Groovy實現的動態編譯插件中,其實他的邏輯對於我們來說不難,但是Groovy編寫動態編譯插件的具體實現理解起來還是需要下一些功夫的。想要順利的讀懂AppJoint的插件,需要先做一些預備知識的準備。

零、相關知識儲備

1.Groovy語言

https://blog.csdn.net/u010451990/article/details/105382861

2.在AndroidStudio中實現Gradle自定義插件

http://www.aoaoyi.com/archives/1274.html

3.瞭解Transform

4.瞭解使用ASM

一、看看框架爲我們做了什麼

在App Joint源碼解讀開始之前,我們先看下AppJoint在生成 apk的時候都做了哪些事(核心邏輯在plugin中)。
這裏我們使用的是AppJoint提供的Demo,沒有做任何更改直接編譯。(編譯後解壓apk,再反編譯成.java文件)

這裏我們需要重點看兩個類,第一個就是在Module:Core中的AppJoint。一個就是在Module:app中帶有

@AppSpec

的App即在manifest中註冊的主Application。

1.編譯後的AppJoint

這裏我們只將重點代碼取出,方便大家找到重點和理解。

編譯前後的AppJoint構造方法對比:

  //Core中編譯前構造方法
  private AppJoint() {
  }
  
  //打包成apk後反解壓的構造方法
  private AppJoint() {
  	
  	//含有@ModuleSpec的 子moudle Application
    this.moduleApplications.add(new Module2Application());
    this.moduleApplications.add(new Module1Application());
    
    //含有@ServiceProvider的 對外暴露的服務接口
    this.routersMap.put(AppService.class, "__app_joint_default", AppServiceImpl.class);
    this.routersMap.put(AppService.class, "another", AppServiceImpl2.class);
    this.routersMap.put(Module1Service.class, "__app_joint_default", Module1ServiceImpl.class);
    this.routersMap.put(Module2Service.class, "__app_joint_default", Module2ServiceImpl.class);
  }

這裏我們看到,編譯後,AppJoint爲我們自動注入了:

  • 含有@ModuleSpec的 子moudle Application
  • 含有@ServiceProvider的 對外暴露的服務接口

其中moduleApplications是列表,routersMap核心是一個map。那麼結合AppJoint的設計思想和源碼:

public void onCreate() {
        for (Application app : moduleApplications) {
            app.onCreate();
        }
}

public static synchronized <T> T service(Class<T> routerType, String name) {
        T requiredRouter = (T) get().getRouterInstanceMap().get(routerType, name);
        if (requiredRouter == null) {
            try {
                requiredRouter = (T) get().routersMap.get(routerType, name).newInstance();
                get().getRouterInstanceMap().put(routerType, name, requiredRouter);
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
        }
        return requiredRouter;
    }

已經很明顯了,這裏就是將含有註解的關鍵單元,注入到統一管理類AppJoint中,然後在實際運行的時候,提供遍歷初始化,和通過Map找到已經實例化的服務對象。

2.編譯後的 App (module:app中的Application)

看完AppJoint,我們在來看下module:app中的Application:

//編譯前的代碼
@AppSpec
class App : AppBase() {
  override fun onCreate() {
    super.onCreate()
    Log.i("app", "app init is called")
  }
}

//編譯後的代碼
public final class App extends AppBase {
  protected void attachBaseContext(Context paramContext) {
    super.attachBaseContext(paramContext);
    AppJoint.get().attachBaseContext(paramContext);
  }
  
  public void onConfigurationChanged(Configuration paramConfiguration) {
    super.onConfigurationChanged(paramConfiguration);
    AppJoint.get().onConfigurationChanged(paramConfiguration);
  }
  
  public void onCreate() {
    super.onCreate();
    Log.i("app", "app init is called");
    AppJoint.get().onCreate();
  }
  
  public void onLowMemory() {
    super.onLowMemory();
    AppJoint.get().onLowMemory();
  }
  
  public void onTerminate() {
    super.onTerminate();
    AppJoint.get().onTerminate();
  }
  
  public void onTrimMemory(int paramInt) {
    super.onTrimMemory(paramInt);
    AppJoint.get().onTrimMemory(paramInt);
  }
}

將AppJoint模板中的生命週期和主Application相綁定,進而做到,子Application中的邏輯和主Application中的生命週期同步。

二、AppJoint的實現流程圖

這裏還是先不談技術細節,我們先談談邏輯,實現上面的代碼需要做哪些工作。
先給出流程圖:
在這裏插入圖片描述
簡單來說,就是找到含有註解的類,然後寫到AppJoint中,再把AppJoint寫到主Application中。上面的流程和AppJoint的實現流程基本一致,所以下面我們就按照流程圖中的流程一步一步的爲大家解讀源碼。

三、找到需要打包的組件

3.1遍歷輸入

找到打包的組件邏輯上來說並不複雜,只是將參與打包的組件遍歷出來就可以了,下面是這段代碼可以變成固定寫法用於在transform階段遍歷input:

	   // Maybe contains the AppJoint class to write code into
        def maybeStubs = []
        // Maybe contains @ModuleSpec, @AppSpec or @ServiceProvider
        def maybeModules = [] 		
        
	    transformInvocation.inputs.each {
            TransformInput input ->
                // Find annotated classes in jar
                input.jarInputs.each { 
                   JarInput jarInput ->}
                // Find annotated classes in dir
                input.directoryInputs.each {
                    DirectoryInput dirInput ->}
        }

上面這段代碼我們可以看出,在transform階段我們需要對輸入遍歷的時候,遍歷主要體現在jarInputsdirectoryInputs兩個集合上,另外還聲明瞭兩個集合,分別是可能含有框架組件的集合和可能還有註解的組件集合。

3.2遍歷jarInputs

jarInputs:是指以jar包方式參與項目編譯的所有本地jar包和遠程jar包(此處的jar包包括aar)

 input.jarInputs.each { JarInput jarInput ->
                    if (!jarInput.file.exists()) return
                    def jarName = jarInput.name
                    if (jarName == ":core") {
                        // maybe stub in dev and handle them later
                        if (maybeStubs.size() == 0) {
                            maybeStubs.add(jarInput)
                        }
                        maybeModules.add(jarInput)
                    } else if (jarName.startsWith(":")) {
                        maybeModules.add(jarInput)
                    } else if (jarName.startsWith("io.github.prototypez:app-joint-core")) {
                        // find the stub
                        maybeStubs.clear()
                        maybeStubs.add(jarInput)
                    } else {
                    	//固定寫法,拿到輸出文件夾
                        def dest = transformInvocation.outputProvider.getContentLocation(
                                jarName,
                                jarInput.contentTypes,
                                jarInput.scopes,
                                Format.JAR)
                        FileUtils.copyFile(jarInput.file, dest)
                    }
                }

這個階段我們的重點在if分支中,首先拿到inputJar集合中的Jar之後,按照命名規則對他們進行區分。
其中 “:core"和"io.github.prototypez:app-joint-core” 代表的是框架module,組件module則使用的規則

if (jarName.startsWith(":"))

進行區分,另外這裏最值得主要的是,普通的jar包是不會參與接下來的代碼插樁的(最後那幾行代碼,直接複製到輸出區),換句話說,如果你對外提供的功能含有註解又是已jar的形式提供的,那麼這些代碼是不會生效的!

3.3 遍歷directoryInputs

directoryInputs是指以源碼的方式參與項目編譯的所有目錄結構及其目錄下的源碼文件

 input.directoryInputs.each {
                    DirectoryInput dirInput ->
                        def outDir = transformInvocation.outputProvider
                                .getContentLocation(
                                        dirInput.name,
                                        dirInput.contentTypes,
                                        dirInput.scopes,
                                        Format.DIRECTORY)
                        // dirInput.file is like "build/intermediates/classes/debug"
                        int pathBitLen = dirInput.file.toString().length()

                        def callback = { File file ->
                            if (file.exists()) {
                                def path = "${file.toString().substring(pathBitLen)}"
                                if (file.isDirectory()) {
                                    new File(outDir, path).mkdirs()
                                } else {
                                    def output = new File(outDir, path)
                                    findAnnotatedClasses(file, output)
                                    if (!output.parentFile.exists())
                                        output.parentFile.mkdirs()
                                    output.bytes = file.bytes
                                }
                            }
                        }

                        if (dirInput.changedFiles != null && !dirInput.changedFiles.isEmpty()) {
                            dirInput.changedFiles.keySet().each(callback)
                        }
                        if (dirInput.file != null && dirInput.file.exists()) {
                            dirInput.file.traverse(callback)
                        }

                }

此處的流程就是,遍歷有變化的文件集合,對這些文件的處理是在前面聲明的callback閉包中進行的。
changedFiles是一個Map<File, Status>,裏面的key是文件,Status是文件變化所對應的狀態。
這裏主要做的就是爲輸出文件做準備,這種準備主要體現在,爲輸出文件創建對應的文件夾。

四、找到含有註解的類

上面遍歷directoryInputs中的callback閉包,裏面調用findAnnotatedClasses方法來處理輸入的文件。
下面我們看看findAnnotatedClasses的核心業務邏輯。

//第一個參數,我們要處理的類文件,第二個是他要輸出的位置。
boolean findAnnotatedClasses(File file, File output)

這個方法大多數的代碼都較爲簡單,這裏我們主要看的是他類訪問器中的(訪問類後,重寫訪問註解的方法)

//第一個參數返回的是註解的全類名,第二個是是否可訪問註解的值
visitAnnotation(String desc, boolean visible)

這裏按照程序的順利,來看在查找註解部分都做了什麼。

4.1找到含有@ModuleSpec註解的類

case "Lio/github/prototypez/appjoint/core/ModuleSpec;":
    //將有註解的類加入到 之前聲明的moduleApplications
    addModuleApplication(new AnnotationModuleSpec(cr.className))
    //返回一個註解方法訪問器 主要是解析 註解中攜帶的優先級 進行解析賦值
    return new AnnotationMethodsVisitor() {
        @Override
        void visit(String name, Object value) {
            //現有隊列中根據類名 找到 moduleApplication
            def moduleApplication = moduleApplications.find({
                it.className == cr.className
            })
            if (moduleApplication) {
                moduleApplication.order = Integer.valueOf(value)
            }
            super.visit(name, value)
        }
} 					

此處要處理邏輯是:
1.將含有此註解的類的類名作爲構造參數創建AnnotationModuleSpec(主要存含有註解的類名,此註解的值,即優先級)實例加入到moduleApplications的集合中。
2.實例化內部類註解訪問器AnnotationMethodsVisitor(主要是對日誌輸出做了切片)。
註解訪問器,拿到剛纔添加到moduleApplications中的註解類,解析註解的值,將這個值賦給優先級字段。
此處可以理解爲,爲後面順序調用子Module的優先級在做準備工作。

4.2找到含有@AppSpec的類

case "Lio/github/prototypez/appjoint/core/AppSpec;":
    //將含有AppSpec註解的文件 放到 appApplications Map中 設置需要更新
    appApplications[file] = output
    needsModification = true
    break

這裏就更簡單了,將含有@AppSpec註解的類,已文件爲Key輸出地址爲value,存到我們之前聲明的appApplications map中。此處需要注意的是,我們的主Applicaiton是不一定在主module中的!
needsModification 是整個方法的返回值,這個值,代表這個類是否需要被編輯。只有主Applicaiton是需要被編輯的,這個類是AppJoint要綁定生命週期的主Application類。

4.3找到含有@ServiceProvider的類

case "Lio/github/prototypez/appjoint/core/ServiceProvider;":
return new AnnotationMethodsVisitor() {

        boolean valueSpecified;
        
        @Override
        void visit(String name, Object value) {
            valueSpecified = true;
            cr.interfaces.each { String interfaceName ->
                //使用Tuple2存儲 接口名和對應值,根據接口 注入類 壓入routerAndImpl中
                routerAndImpl[new Tuple2(interfaceName, value)] = cr.className
            }
            super.visit(name, value)
        }

        @Override
        void visitEnd() {
            if (!valueSpecified) {
                cr.interfaces.each {
                    routerAndImpl[new Tuple2(it, SERVICE_PROVIDER_DEFAULT_NAME)] =
cr.className
                }
            }
            super.visitEnd()
        }
    }

簡單來說這一步就是將接口類名和註解對應的值結合作爲Key,然後將此類作爲Value存放到

def routerAndImpl = new HashMap<Tuple2<String, String>, String>()

結合中。這裏需要注意的是visitEnd()這裏,如果存在沒有值的註解,也會將這個類放到routerAndImpl 結合中,只不過此時的名稱爲

public static final String SERVICE_PROVIDER_DEFAULT_NAME = "__app_joint_default";

的默認名稱。

4.4總結findAnnotatedClasses

簡單來說這個方法就是找到我們所有包含框架內註解的類,將他們放到各自的結合中,爲後面插樁做準備。

五、找到Jar中可能包含的註解類

在解析jarInput和dirctoryInput這兩個單元的時候,他們的不同是dirctoryInput以及可以拿到.class文件了,那麼此時可以直接訪問,判斷其是是否是組件化過程中要用到的類;而jarInput這個單元,知識找到了對應的module,並沒有對其內部的類做出來。

之所以有這樣的區分,是因爲對jar的處理是需要先解壓,解壓後才能拿到他裏面的.class。
下面我們來看下對jar包的處理。

 maybeModules.each { JarInput jarInput ->
            def repackageAction = traversalJar(
                    transformInvocation,
                    jarInput,
                    { File outputFile, File input ->
                        return findAnnotatedClasses(input, outputFile)
                    }
            )
            if (repackageAction) repackageActions.add(repackageAction)
        }

這段的難點是

Closure traversalJar(TransformInvocation transformInvocation, JarInput jarInput, Closure closure)

此段代碼是對Groovy中閉包語法一段較好的展示,他充分的體現了閉包的靈活性!

下面講下traversalJar方法處理的邏輯:

  1. Jar解壓,拿到.class文件
  2. 將解壓的文件,複製到需要打包的文件夾下
  3. 處理這些文件,此處是方法參數閉包的執行邏輯。閉包實際處理方法是findAnnotatedClasses,這個方法返回的是這個類是否需要做,編輯處理,如果需要編輯,那麼他的打包延遲(編輯後才能打包)。
  4. 處理完成之後,生成一段重打包的閉包代碼段。
  5. 如果不需要延遲打包,那麼直接打包什麼都不返回;需要延遲打包的話,返回的是打包代碼段。

最後,此方法由於閉包的原因,他和其他模塊並沒有耦合,所以可以直接提取出來,作爲工具類。

六、找到AppJoint

 maybeStubs.each { JarInput jarInput ->
            def repackageAction = traversalJar(
                    transformInvocation,
                    jarInput,
                    { File outputFile, File input ->
                        return findAppJointClass(input, outputFile)
                    }
            )
            if (repackageAction) repackageActions.add(repackageAction)
        }

有了前面的基礎,看這段代碼就輕鬆多了。這裏的操作是找到AppJoint類,然後將重打包代碼段,加到重打包集合中。

findAppJointClass(input, outputFile)if (name == "io/github/prototypez/appjoint/AppJoint")

此方法就不不在貼出源碼了,此處就是通過類訪問器,然後看類名是否是框架中指定的AppJoint路徑。

七、將代碼寫入到AppJoint中

經過上面的步驟,我們重要找全了,我們要處理的類了。
接下來就是代碼插樁了。
這裏的步驟是:
1.先讀入文件
2.然後通過類訪問器找到構造函數
3.通過方法訪問器訪問構造函數。
4.重寫visitInsn方法,將子Applicaiton代碼和對外接口插入到構造函數中。

這一步的兩個方法:

void insertApplicationAdd(String applicationName) 

void insertRoutersPut(Tuple2<String, String> router, String impl) 

裏面放的都是字節碼代碼,這些代碼大家可能感覺寫起來很難受,這裏我們可以通過,ASM插件來生成。
在這裏插入圖片描述
代碼生成插件。
添加子Applicaiton到構造函數中的語句:

		mv.visitInsn(Opcodes.DUP)
        //執行new 操作
        mv.visitMethodInsn(Opcodes.INVOKESPECIAL, applicationName, "<init>", "()V", false)
        //執行add操作
        mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, "java/util/List", "add", "(Ljava/lang/Object;)Z", true)
        mv.visitInsn(Opcodes.POP)

將接口方法,注入到方法Map中給的語句。

mv.visitLdcInsn(Type.getObjectType(router.first))
mv.visitLdcInsn(router.second)
mv.visitLdcInsn(Type.getObjectType(impl))
 
 mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "io/github/prototypez/appjoint/util/BinaryKeyMap",
                "put",
                 "(Ljava/lang/Object;
                   Ljava/lang/Object;
                   Ljava/lang/Object;)V",
                 true)

將類名注入到Map中。

八、將代碼寫到application中

 appApplications.each { File classFile, File output ->
            inputStream = new FileInputStream(classFile)
            ClassReader reader = new ClassReader(inputStream)
            ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS)
            ClassVisitor visitor = new ClassVisitorApplication(writer)
            reader.accept(visitor, 0)
            output.bytes = writer.toByteArray()
            inputStream.close()
        }

這裏面的邏輯類似,還是通過類訪問器,將代碼插入到主Applicaiton中。
首先找到需要綁定到生命週期上的方法:

switch (name + desc) {
            case "onCreate()V":
                onCreateDefined = true
                return new MethodVisitorAddCallAppJoint(methodVisitor, "onCreate", "()V", false, false)
            case "attachBaseContext(Landroid/content/Context;)V":
                attachBaseContextDefined = true
                return new MethodVisitorAddCallAppJoint(methodVisitor, "attachBaseContext", "(Landroid/content/Context;)V", true, false)
            case "onConfigurationChanged(Landroid/content/res/Configuration;)V":
                onConfigurationChangedDefined = true
                return new MethodVisitorAddCallAppJoint(methodVisitor, "onConfigurationChanged", "(Landroid/content/res/Configuration;)V", true, false)
            case "onLowMemory()V":
                onLowMemoryDefined = true
                return new MethodVisitorAddCallAppJoint(methodVisitor, "onLowMemory", "()V", false, false)
            case "onTerminate()V":
                onTerminateDefined = true
                return new MethodVisitorAddCallAppJoint(methodVisitor, "onTerminate", "()V", false, false)
            case "onTrimMemory(I)V":
                onTrimMemoryDefined = true
                return new MethodVisitorAddCallAppJoint(methodVisitor, "onTrimMemory", "(I)V", false, true)
        }

這裏使用的都是一個方法MethodVisitorAddCallAppJoint。
最後進行插入:

mv.visitMethodInsn(Opcodes.INVOKESTATIC,
                        "io/github/prototypez/appjoint/AppJoint",
                        "get",
                        "()Lio/github/prototypez/appjoint/AppJoint;",
                        false)

                if (aLoad1) {
                    mv.visitVarInsn(Opcodes.ALOAD, 1)
                }

                if (iLoad1) {
                    mv.visitVarInsn(Opcodes.ILOAD, 1)
                }

                mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "io/github/prototypez/appjoint/AppJoint", name, desc, false)

九、重打包

還記4.2中說的重打包嗎?當以上,代碼編輯輸出完畢之後,就會進行壓縮輸出,作爲下一個單元的輸入。

 repackageActions.each { Closure action -> action.call() }

總結:總的來說,AppJoint的源碼拆解之後閱讀難度還是不大的。不過需要較多的Gradle和Groovy的基礎知識。另外直接讀源碼的時候可能由於文件整體交代,帶來一定給的困難,我們可以將一些內部類和工具型的方法提煉出來,以此來降低核心業務類的代碼行數,進而降低閱讀難度。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章