AutoRegister:一種更高效的元件自動註冊方案(android元件化開發)

齊翊發表於2017-12-11

*本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家釋出

摘要:

在編譯時,掃描即將打包到apk中的所有類,將所有元件類收集起來,通過修改位元組碼的方式生成註冊程式碼到元件管理類中,從而實現編譯時自動註冊的功能,不用再關心專案中有哪些元件類了。 特點:不需要註解,不會增加新的類;效能高,不需要反射,執行時直接呼叫元件的構造方法;能掃描到所有類,不會出現遺漏;支援分級按需載入功能的實現。

前言


最近在公司做android元件化開發框架的搭建,採用元件匯流排的方式進行通訊:提供一個基礎庫,各元件(IComponent介面的實現類)都註冊到元件管理類(元件匯流排:ComponentManager)中,元件之間在同一個app內時,通過ComponentManager轉發呼叫請求來實現通訊(不同app之間的通訊方式不是本文的主題,暫且略去)。但在實現過程中遇到了一個問題:

如何將不同module中的元件類自動註冊到ComponentManager中

目前市面上比較常用的解決方案是使用annotationProcessor:通過編譯時註解動態生成元件對映表程式碼的方式來實現。但嘗試過後發現有問題,因為編譯時註解的特性只在原始碼編譯時生效,無法掃描到aar包裡的註解(project依賴、maven依賴均無效),也就是說必須每個module編譯時生成自己的程式碼,然後要想辦法將這些分散在各aar種的類找出來進行集中註冊。

ARouter的解決方案是:

  • 每個module都生成自己的java類,這些類的包名都是'com.alibaba.android.arouter.routes'
  • 然後在執行時通過讀取每個dex檔案中的這個包下的所有類通過反射來完成對映表的註冊,詳見ClassUtils.java原始碼

執行時通過讀取所有dex檔案遍歷每個entry查詢指定包內的所有類名,然後反射獲取類物件。這種效率看起來並不高。

ActivityRouter的解決方案是(demo中有2個元件名為'app'和'sdk'):

  • 在主app module中有一個@Modules({"app", "sdk"})註解用來標記當前app內有多少元件,根據這個註解生成一個RouterInit類
  • 在RouterInit類的init方法中生成呼叫同一個包內的RouterMapping_app.map
  • 每個module生成的類(RouterMapping_app.java 和 RouterMapping_sdk.java)都放在com.github.mzule.activityrouter.router包內(在不同的aar中,但包名相同)
  • 在RouterMapping_sdk類的map()方法中根據掃描到的當前module內所有路由註解,生成了呼叫Routers.map(...)方法來註冊路由的程式碼
  • 在Routers的所有api介面中最終都會觸發RouterInit.init()方法,從而實現所有路由的對映表註冊

這種方式用一個RouterInit類組合了所有module中的路由對映表類,執行時效率比掃描所有dex檔案的方式要高,但需要額外在主工程程式碼中維護一個元件名稱列表註解: @Modules({"app", "sdk"})

有沒有一種方式可以更高效地管理這個列表呢?

聯想到之前用ASM框架自動生成程式碼的方式做了個AndAop外掛用於自動插入指定程式碼到任意類的任意方法中,於是寫了一個自動生成註冊元件的gradle外掛。 大致思路是:在編譯時,掃描所有類,將符合條件的類收集起來,並通過修改位元組碼生成註冊程式碼到指定的管理類中,從而實現編譯時自動註冊的功能,不用再關心專案中有哪些元件類了。不會增加新的class,不需要反射,執行時直接呼叫元件的構造方法。

效能方面:由於使用效率更高的ASM框架來進行位元組碼分析和修改,並過濾掉android/support包中的所有類(還支援設定自定義的掃描範圍),經公司專案實測,未程式碼混淆前所有dex檔案總計12MB左右,掃描及程式碼插入的**總耗時在2s-3s之間**,相對於整個apk打包所花3分鐘左右的時間來說可以忽略不計(執行環境:MacBookPro 15吋高配 Mid 2015)。

開發完成後,考慮到這個功能的通用性,於是升級元件掃描註冊外掛為通用的自動註冊外掛AutoRegister,支援配置多種型別的掃描註冊,使用方式見github中的README文件。此外掛現已用到元件化開發框架: CC

升級後,AutoRegister外掛的完整功能描述是:

在編譯期掃描即將打包到apk中的所有類,並將指定介面的實現類(或指定類的子類)通過位元組碼操作自動註冊到對應的管理類中。尤其適用於命令模式或策略模式下的對映表生成。

在元件化開發框架中,可有助於實現分級按需載入的功能:

  • 在元件管理類中生成元件自動註冊的程式碼
  • 在元件框架第一次被呼叫時載入此登錄檔
  • 若元件中有很多功能提供給外部呼叫,可以將這些功能包裝成多個Processor,並將它們自動註冊到元件中進行管理
  • 元件被初次呼叫時再載入這些Processor

#實現過程

第一步:準備工作

  1. 首先要知道如何使用Android Studio開發Gradle外掛
  2. 瞭解TransformAPI:Transform API是從Gradle 1.5.0版本之後提供的,它允許第三方在打包Dex檔案之前的編譯過程中修改java位元組碼(自定義外掛註冊的transform會在ProguardTransform和DexTransform之前執行,所以自動註冊的類不需要考慮混淆的情況).參考文章有:
  3. 位元組碼修改框架(相比於Javassist框架ASM較難上手,但效能更高,但相學習難度阻擋不了我們對效能的追求):

第二步:構建外掛工程

  1. 按照如何使用Android Studio開發Gradle外掛文章中的方法建立好外掛工程併發布到本地maven倉庫(我是放在工程根目錄下的一個資料夾中),這樣我們就可以在本地快速除錯了

build.gradle檔案的部分內容如下:

apply plugin: 'groovy'
apply plugin: 'maven'

dependencies {
    compile gradleApi()
    compile localGroovy()
}

repositories {
    mavenCentral()
}
dependencies {
    compile 'com.android.tools.build:gradle:2.2.0'
}


//載入本地maven私服配置(在工程根目錄中的local.properties檔案中進行配置)
Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
def artifactory_user = properties.getProperty("artifactory_user")
def artifactory_password = properties.getProperty("artifactory_password")
def artifactory_contextUrl = properties.getProperty("artifactory_contextUrl")
def artifactory_snapshot_repoKey = properties.getProperty("artifactory_snapshot_repoKey")
def artifactory_release_repoKey = properties.getProperty("artifactory_release_repoKey")

def maven_type_snapshot = true
// 專案引用的版本號,比如compile 'com.yanzhenjie:andserver:1.0.1'中的1.0.1就是這裡配置的。
def artifact_version='1.0.1'
// 唯一包名,比如compile 'com.yanzhenjie:andserver:1.0.1'中的com.yanzhenjie就是這裡配置的。
def artifact_group = 'com.billy.android'
def artifact_id = 'autoregister'
def debug_flag = true //true: 釋出到本地maven倉庫, false: 釋出到maven私服

task sourcesJar(type: Jar) {
    from project.file('src/main/groovy')
    classifier = 'sources'
}

artifacts {
    archives sourcesJar
}
uploadArchives {
    repositories {
        mavenDeployer {
            //deploy到maven倉庫
            if (debug_flag) {
                repository(url: uri('../repo-local')) //deploy到本地倉庫
            } else {//deploy到maven私服中
                def repoKey = maven_type_snapshot ? artifactory_snapshot_repoKey : artifactory_release_repoKey
                repository(url: "${artifactory_contextUrl}/${repoKey}") {
                    authentication(userName: artifactory_user, password: artifactory_password)
                }
            }

            pom.groupId = artifact_group
            pom.artifactId = artifact_id
            pom.version = artifact_version + (maven_type_snapshot ? '-SNAPSHOT' : '')

            pom.project {
                licenses {
                    license {
                        name 'The Apache Software License, Version 2.0'
                        url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
                    }
                }
            }
        }
    }
}
複製程式碼

根目錄的build.gradle檔案中要新增本地倉庫的地址及dependencies

buildscript {
    
    repositories {
        maven{ url rootProject.file("repo-local") }
        maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.0.0-beta6'
        classpath 'com.github.dcendents:android-maven-gradle-plugin:1.4.1'
        classpath 'com.billy.android:autoregister:1.0.1'
    }
}
複製程式碼

2.在Transform類的transform方法中新增類掃描相關的程式碼

// 遍歷輸入檔案
inputs.each { TransformInput input ->

    // 遍歷jar
    input.jarInputs.each { JarInput jarInput ->
        String destName = jarInput.name
        // 重名名輸出檔案,因為可能同名,會覆蓋
        def hexName = DigestUtils.md5Hex(jarInput.file.absolutePath)
        if (destName.endsWith(".jar")) {
            destName = destName.substring(0, destName.length() - 4)
        }
        // 獲得輸入檔案
        File src = jarInput.file
        // 獲得輸出檔案
        File dest = outputProvider.getContentLocation(destName + "_" + hexName, jarInput.contentTypes, jarInput.scopes, Format.JAR)

        //遍歷jar的位元組碼類檔案,找到需要自動註冊的component
        if (CodeScanProcessor.shouldProcessPreDexJar(src.absolutePath)) {
            CodeScanProcessor.scanJar(src, dest)
        }
        FileUtils.copyFile(src, dest)

        project.logger.info "Copying\t${src.absolutePath} \nto\t\t${dest.absolutePath}"
    }
    // 遍歷目錄
    input.directoryInputs.each { DirectoryInput directoryInput ->
        // 獲得產物的目錄
        File dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
        String root = directoryInput.file.absolutePath
        if (!root.endsWith(File.separator))
            root += File.separator
        //遍歷目錄下的每個檔案
        directoryInput.file.eachFileRecurse { File file ->
            def path = file.absolutePath.replace(root, '')
            if(file.isFile()){
                CodeScanProcessor.checkInitClass(path, new File(dest.absolutePath + File.separator + path))
                if (CodeScanProcessor.shouldProcessClass(path)) {
                    CodeScanProcessor.scanClass(file)
                }
            }
        }
        project.logger.info "Copying\t${directoryInput.file.absolutePath} \nto\t\t${dest.absolutePath}"
        // 處理完後拷到目標檔案
        FileUtils.copyDirectory(directoryInput.file, dest)
    }
}
複製程式碼

CodeScanProcessor是一個工具類,其中CodeScanProcessor.scanJar(src, dest)CodeScanProcessor.scanClass(file)分別是用來掃描jar包和class檔案的 掃描的原理是利用ASM的ClassVisitor來檢視每個類的父類類名及所實現的介面名稱,與配置的資訊進行比較,如果符合我們的過濾條件,則記錄下來,在全部掃描完成後將呼叫這些類的無參構造方法進行註冊

static void scanClass(InputStream inputStream) {
    ClassReader cr = new ClassReader(inputStream)
    ClassWriter cw = new ClassWriter(cr, 0)
    ScanClassVisitor cv = new ScanClassVisitor(Opcodes.ASM5, cw)
    cr.accept(cv, ClassReader.EXPAND_FRAMES)
    inputStream.close()
}

static class ScanClassVisitor extends ClassVisitor {
    ScanClassVisitor(int api, ClassVisitor cv) {
        super(api, cv)
    }
    void visit(int version, int access, String name, String signature,
               String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces)
        RegisterTransform.infoList.each { ext ->
            if (shouldProcessThisClassForRegister(ext, name)) {
                if (superName != 'java/lang/Object' && !ext.superClassNames.isEmpty()) {
                    for (int i = 0; i < ext.superClassNames.size(); i++) {
                        if (ext.superClassNames.get(i) == superName) {
                            ext.classList.add(name)
                            return
                        }
                    }
                }
                if (ext.interfaceName && interfaces != null) {
                    interfaces.each { itName ->
                        if (itName == ext.interfaceName) {
                            ext.classList.add(name)
                        }
                    }
                }
            }
        }

    }
}
複製程式碼

3.記錄目標類所在的檔案,因為我們接下來要修改其位元組碼,將註冊程式碼插入進去

 static void checkInitClass(String entryName, File file) {
     if (entryName == null || !entryName.endsWith(".class"))
         return
     entryName = entryName.substring(0, entryName.lastIndexOf('.'))
     RegisterTransform.infoList.each { ext ->
         if (ext.initClassName == entryName)
             ext.fileContainsInitClass = file
     }
 }
複製程式碼

4.掃描完成後,開始修改目標類的位元組碼(使用ASM的MethodVisitor來修改目標類指定方法,若未指定則預設為static塊,即<clinit>方法),生成的程式碼是直接呼叫掃描到的類的無參構造方法,並非通過反射

  • class檔案: 直接修改此位元組碼檔案(其實是重新生成一個class檔案並替換掉原來的檔案)
  • jar檔案:複製此jar檔案,找到jar包中目標類所對應的JarEntry,修改其位元組碼,然後替換原來的jar檔案

import org.apache.commons.io.IOUtils
import org.objectweb.asm.*

import java.util.jar.JarEntry
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry
/**
 *
 * @author billy.qi
 * @since 17/3/20 11:48
 */
class CodeInsertProcessor {
    RegisterInfo extension

    private CodeInsertProcessor(RegisterInfo extension) {
        this.extension = extension
    }

    static void insertInitCodeTo(RegisterInfo extension) {
        if (extension != null && !extension.classList.isEmpty()) {
            CodeInsertProcessor processor = new CodeInsertProcessor(extension)
            File file = extension.fileContainsInitClass
            if (file.getName().endsWith('.jar'))
                processor.insertInitCodeIntoJarFile(file)
            else
                processor.insertInitCodeIntoClassFile(file)
        }
    }

    //處理jar包中的class程式碼注入
    private File insertInitCodeIntoJarFile(File jarFile) {
        if (jarFile) {
            def optJar = new File(jarFile.getParent(), jarFile.name + ".opt")
            if (optJar.exists())
                optJar.delete()
            def file = new JarFile(jarFile)
            Enumeration enumeration = file.entries()
            JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(optJar))

            while (enumeration.hasMoreElements()) {
                JarEntry jarEntry = (JarEntry) enumeration.nextElement()
                String entryName = jarEntry.getName()
                ZipEntry zipEntry = new ZipEntry(entryName)
                InputStream inputStream = file.getInputStream(jarEntry)
                jarOutputStream.putNextEntry(zipEntry)
                if (isInitClass(entryName)) {
                    println('codeInsertToClassName:' + entryName)
                    def bytes = referHackWhenInit(inputStream)
                    jarOutputStream.write(bytes)
                } else {
                    jarOutputStream.write(IOUtils.toByteArray(inputStream))
                }
                inputStream.close()
                jarOutputStream.closeEntry()
            }
            jarOutputStream.close()
            file.close()

            if (jarFile.exists()) {
                jarFile.delete()
            }
            optJar.renameTo(jarFile)
        }
        return jarFile
    }

    boolean isInitClass(String entryName) {
        if (entryName == null || !entryName.endsWith(".class"))
            return false
        if (extension.initClassName) {
            entryName = entryName.substring(0, entryName.lastIndexOf('.'))
            return extension.initClassName == entryName
        }
        return false
    }
    /**
     * 處理class的注入
     * @param file class檔案
     * @return 修改後的位元組碼檔案內容
     */
    private byte[] insertInitCodeIntoClassFile(File file) {
        def optClass = new File(file.getParent(), file.name + ".opt")

        FileInputStream inputStream = new FileInputStream(file)
        FileOutputStream outputStream = new FileOutputStream(optClass)

        def bytes = referHackWhenInit(inputStream)
        outputStream.write(bytes)
        inputStream.close()
        outputStream.close()
        if (file.exists()) {
            file.delete()
        }
        optClass.renameTo(file)
        return bytes
    }


    //refer hack class when object init
    private byte[] referHackWhenInit(InputStream inputStream) {
        ClassReader cr = new ClassReader(inputStream)
        ClassWriter cw = new ClassWriter(cr, 0)
        ClassVisitor cv = new MyClassVisitor(Opcodes.ASM5, cw)
        cr.accept(cv, ClassReader.EXPAND_FRAMES)
        return cw.toByteArray()
    }

    class MyClassVisitor extends ClassVisitor {

        MyClassVisitor(int api, ClassVisitor cv) {
            super(api, cv)
        }

        void visit(int version, int access, String name, String signature,
                   String superName, String[] interfaces) {
            super.visit(version, access, name, signature, superName, interfaces)
        }
        @Override
        MethodVisitor visitMethod(int access, String name, String desc,
                                  String signature, String[] exceptions) {
            MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions)
            if (name == extension.initMethodName) { //注入程式碼到指定的方法之中
                boolean _static = (access & Opcodes.ACC_STATIC) > 0
                mv = new MyMethodVisitor(Opcodes.ASM5, mv, _static)
            }
            return mv
        }
    }

    class MyMethodVisitor extends MethodVisitor {
        boolean _static;

        MyMethodVisitor(int api, MethodVisitor mv, boolean _static) {
            super(api, mv)
            this._static = _static;
        }

        @Override
        void visitInsn(int opcode) {
            if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) {
                extension.classList.each { name ->
                    if (!_static) {
                        //載入this
                        mv.visitVarInsn(Opcodes.ALOAD, 0)
                    }
                    //用無參構造方法建立一個元件例項
                    mv.visitTypeInsn(Opcodes.NEW, name)
                    mv.visitInsn(Opcodes.DUP)
                    mv.visitMethodInsn(Opcodes.INVOKESPECIAL, name, "<init>", "()V", false)
                    //呼叫註冊方法將元件例項註冊到元件庫中
                    if (_static) {
                        mv.visitMethodInsn(Opcodes.INVOKESTATIC
                                , extension.registerClassName
                                , extension.registerMethodName
                                , "(L${extension.interfaceName};)V"
                                , false)
                    } else {
                        mv.visitMethodInsn(Opcodes.INVOKESPECIAL
                                , extension.registerClassName
                                , extension.registerMethodName
                                , "(L${extension.interfaceName};)V"
                                , false)
                    }
                }
            }
            super.visitInsn(opcode)
        }
        @Override
        void visitMaxs(int maxStack, int maxLocals) {
            super.visitMaxs(maxStack + 4, maxLocals)
        }
    }
}
複製程式碼

5.接收擴充套件引數,獲取需要掃描類的特徵及需要插入的程式碼

找了很久沒找到gradle外掛接收自定義物件陣列擴充套件引數的方法,於是退一步改用List<Map>接收後再進行轉換的方式來實現,以此來接收多個掃描任務的擴充套件引數

import org.gradle.api.Project
/**
 * aop的配置資訊
 * @author billy.qi
 * @since 17/3/28 11:48
 */
class AutoRegisterConfig {

    public ArrayList<Map<String, Object>> registerInfo = []

    ArrayList<RegisterInfo> list = new ArrayList<>()

    Project project

    AutoRegisterConfig(){}

    void convertConfig() {
        registerInfo.each { map ->
            RegisterInfo info = new RegisterInfo()
            info.interfaceName = map.get('scanInterface')
            def superClasses = map.get('scanSuperClasses')
            if (!superClasses) {
                superClasses = new ArrayList<String>()
            } else if (superClasses instanceof String) {
                ArrayList<String> superList = new ArrayList<>()
                superList.add(superClasses)
                superClasses = superList
            }
            info.superClassNames = superClasses
            info.initClassName = map.get('codeInsertToClassName') //程式碼注入的類
            info.initMethodName = map.get('codeInsertToMethodName') //程式碼注入的方法(預設為static塊)
            info.registerMethodName = map.get('registerMethodName') //生成的程式碼所呼叫的方法
            info.registerClassName = map.get('registerClassName') //註冊方法所在的類
            info.include = map.get('include')
            info.exclude = map.get('exclude')
            info.init()
            if (info.validate())
                list.add(info)
            else {
                project.logger.error('auto register config error: scanInterface, codeInsertToClassName and registerMethodName should not be null\n' + info.toString())
            }

        }
    }
}
複製程式碼
import java.util.regex.Pattern
/**
 * aop的配置資訊
 * @author billy.qi
 * @since 17/3/28 11:48
 */
class RegisterInfo {
    static final DEFAULT_EXCLUDE = [
            '.*/R(\\$[^/]*)?'
            , '.*/BuildConfig$'
    ]
    //以下是可配置引數
    String interfaceName = ''
    ArrayList<String> superClassNames = []
    String initClassName = ''
    String initMethodName = ''
    String registerClassName = ''
    String registerMethodName = ''
    ArrayList<String> include = []
    ArrayList<String> exclude = []

    //以下不是可配置引數
    ArrayList<Pattern> includePatterns = []
    ArrayList<Pattern> excludePatterns = []
    File fileContainsInitClass //initClassName的class檔案或含有initClassName類的jar檔案
    ArrayList<String> classList = new ArrayList<>()

    RegisterInfo(){}

    boolean validate() {
        return interfaceName && registerClassName && registerMethodName
    }

    //用於在console中輸出日誌
    @Override
    String toString() {
        StringBuilder sb = new StringBuilder('{')
        sb.append('\n\t').append('scanInterface').append('\t\t\t=\t').append(interfaceName)
        sb.append('\n\t').append('scanSuperClasses').append('\t\t=\t[')
        for (int i = 0; i < superClassNames.size(); i++) {
            if (i > 0) sb.append(',')
            sb.append(' \'').append(superClassNames.get(i)).append('\'')
        }
        sb.append(' ]')
        sb.append('\n\t').append('codeInsertToClassName').append('\t=\t').append(initClassName)
        sb.append('\n\t').append('codeInsertToMethodName').append('\t=\t').append(initMethodName)
        sb.append('\n\t').append('registerMethodName').append('\t\t=\tpublic static void ')
                .append(registerClassName).append('.').append(registerMethodName)
        sb.append('\n\t').append('include').append(' = [')
        include.each { i ->
            sb.append('\n\t\t\'').append(i).append('\'')
        }
        sb.append('\n\t]')
        sb.append('\n\t').append('exclude').append(' = [')
        exclude.each { i ->
            sb.append('\n\t\t\'').append(i).append('\'')
        }
        sb.append('\n\t]\n}')
        return sb.toString()
    }

    void init() {
        if (include == null) include = new ArrayList<>()
        if (include.empty) include.add(".*") //如果沒有設定則預設為include所有
        if (exclude == null) exclude = new ArrayList<>()
        if (!registerClassName)
            registerClassName = initClassName

        //將interfaceName中的'.'轉換為'/'
        if (interfaceName)
            interfaceName = convertDotToSlash(interfaceName)
        //將superClassName中的'.'轉換為'/'
        if (superClassNames == null) superClassNames = new ArrayList<>()
        for (int i = 0; i < superClassNames.size(); i++) {
            def superClass = convertDotToSlash(superClassNames.get(i))
            superClassNames.set(i, superClass)
            if (!exclude.contains(superClass))
                exclude.add(superClass)
        }
        //interfaceName新增到排除項
        if (!exclude.contains(interfaceName))
            exclude.add(interfaceName)
        //註冊和初始化的方法所在的類預設為同一個類
        initClassName = convertDotToSlash(initClassName)
        //預設插入到static塊中
        if (!initMethodName)
            initMethodName = "<clinit>"
        registerClassName = convertDotToSlash(registerClassName)
        //新增預設的排除項
        DEFAULT_EXCLUDE.each { e ->
            if (!exclude.contains(e))
                exclude.add(e)
        }
        initPattern(include, includePatterns)
        initPattern(exclude, excludePatterns)
    }

    private static String convertDotToSlash(String str) {
        return str ? str.replaceAll('\\.', '/').intern() : str
    }

    private static void initPattern(ArrayList<String> list, ArrayList<Pattern> patterns) {
        list.each { s ->
            patterns.add(Pattern.compile(s))
        }
    }
}
複製程式碼

第三步: 在application中配置自動註冊外掛所需的相關擴充套件引數

在主app module的build.gradle檔案中新增擴充套件引數,示例如下:

//auto register extension
// 功能介紹:
//  在編譯期掃描將打到apk包中的所有類
//  將 scanInterface的實現類 或 scanSuperClasses的子類
//  並在 codeInsertToClassName 類的 codeInsertToMethodName 方法中生成如下程式碼:
//  codeInsertToClassName.registerMethodName(scanInterface)
// 要點:
//  1. codeInsertToMethodName 若未指定,則預設為static塊
//  2. codeInsertToMethodName 與 registerMethodName 需要同為static或非static
// 自動生成的程式碼示例:
/*
  在com.billy.app_lib_interface.CategoryManager.class檔案中
  static
  {
    register(new CategoryA()); //scanInterface的實現類
    register(new CategoryB()); //scanSuperClass的子類
  }
 */
apply plugin: 'auto-register'
autoregister {
    registerInfo = [
        [
            'scanInterface'             : 'com.billy.app_lib_interface.ICategory'
            // scanSuperClasses 會自動被加入到exclude中,下面的exclude只作為演示,其實可以不用手動新增
            , 'scanSuperClasses'        : ['com.billy.android.autoregister.demo.BaseCategory']
            , 'codeInsertToClassName'   : 'com.billy.app_lib_interface.CategoryManager'
            //未指定codeInsertToMethodName,預設插入到static塊中,故此處register必須為static方法
            , 'registerMethodName'      : 'register' //
            , 'exclude'                 : [
                //排除的類,支援正規表示式(包分隔符需要用/表示,不能用.)
                'com.billy.android.autoregister.demo.BaseCategory'.replaceAll('\\.', '/') //排除這個基類
            ]
        ],
        [
            'scanInterface'             : 'com.billy.app_lib.IOther'
            , 'codeInsertToClassName'   : 'com.billy.app_lib.OtherManager'
            , 'codeInsertToMethodName'  : 'init' //非static方法
            , 'registerMethodName'      : 'registerOther' //非static方法
        ]
    ]
}
複製程式碼

總結


本文介紹了AutoRegister外掛的功能及其在元件化開發框架中的應用。重點對其原理做了說明,主要介紹了此外掛的實現過程,其中涉及到的技術點有TransformAPI、ASM、groovy相關語法、gradle機制。

本外掛的所有程式碼及其用法demo已開源到github上,歡迎fork、start

接下來就用這個外掛來為我們自動管理登錄檔吧!

相關文章