滴滴插件化框架學習筆記之virtualapk-gradle-plugin

前言

在集成使用滴滴插件化框架VirtualAPK時,按照官方接入文檔,分別需要在宿主工程和插件工程中進行gradle相關配置,其中特別需要引入VirtualAPK的Gradle插件。

VirtualAPK Gradle插件配置如下:

  • Host Project

在宿主工程根目錄下 build.gradle 中添加插件路徑:

dependencies {
    classpath 'com.didi.virtualapk:gradle:0.9.8.6'
}

在宿主app module的 build.gradle 中應用host插件:

apply plugin: 'com.didi.virtualapk.host'
  • Plugin Project

在插件工程根目錄下 build.gradle 中添加插件路徑:

dependencies {
    classpath 'com.didi.virtualapk:gradle:0.9.8.6'
}

在插件app module的 build.gradle 中應用plugin插件:

apply plugin: 'com.didi.virtualapk.plugin'

在插件app module的 build.gradle 中配置:

virtualApk {
    packageId = 0x6f             // The package id of Resources.
    targetHost='source/host/app' // The path of application module in host project.
    applyHostMapping = true      // [Optional] Default value is true. 
}

以上配置是爲了支持宿主和插件之間的代碼和資源互通。當構建時,通過Gradle插件重設插件資源的packageId,以及去除插件中和宿主共同依賴的代碼和資源。

宿主Gradle Plugin

首先在com.didi.virtualapk.host.properties中找到’com.didi.virtualapk.host’對應的源碼實現。

implementation-class=com.didi.virtualapk.VAHostPlugin

實現類即爲VAHostPlugin.groovy

VAHostPlugin

public class VAHostPlugin implements Plugin<Project> {
    // ···
    @Override
    public void apply(Project project) {
        // ···
        
        // app/build/VAHost目錄用於保存生成的記錄文件
        vaHostDir = new File(project.getBuildDir(), "VAHost")

        // Project配置完成後執行以下閉包
        project.afterEvaluate {

            project.android.applicationVariants.each { ApplicationVariantImpl variant ->
                // 記錄依賴庫信息
                generateDependencies(variant)
                // 備份R.txt文件
                backupHostR(variant)
                // 備份混淆mapping文件
                backupProguardMapping(variant)
                //keepResourceIds(variant)
            }
        }
        
    }
    // ···
}

VAHostPlugin繼承Plugin接口實現apply方法,Gradle構建時當在配置階段完成後,會進行一些文件的備份,保存在app/build/VAHost目錄下。

generateDependencies

/**
 * Generate ${project.buildDir}/VAHost/versions.txt
 */
def generateDependencies(ApplicationVariantImpl applicationVariant) {

    // 在JavaCompile任務最後添加action
    applicationVariant.javaCompile.doLast {

        // Generate ${project.buildDir}/VAHost/allVersions.txt
        // ···

        // 收集依賴庫信息,保存在app/build/VAHost/versions.txt中
        FileUtil.saveFile(vaHostDir, "versions", {
            List<String> deps = new ArrayList<String>()
            Log.i TAG, "Used compileClasspath: ${applicationVariant.name}"
            Set<ArtifactDependencyGraph.HashableResolvedArtifactResult> compileArtifacts
            if (project.extensions.extraProperties.get(Constants.GRADLE_3_1_0)) {
                ImmutableMap<String, String> buildMapping = Reflect.on('com.android.build.gradle.internal.ide.ModelBuilder')
                        .call('computeBuildMapping', project.gradle)
                        .get()
                compileArtifacts = ArtifactDependencyGraph.getAllArtifacts(
                        applicationVariant.variantData.scope, AndroidArtifacts.ConsumedConfigType.COMPILE_CLASSPATH, null, buildMapping)
            } else {
                compileArtifacts = ArtifactDependencyGraph.getAllArtifacts(
                        applicationVariant.variantData.scope, AndroidArtifacts.ConsumedConfigType.COMPILE_CLASSPATH, null)
            }

            compileArtifacts.each { ArtifactDependencyGraph.HashableResolvedArtifactResult artifact ->
                ComponentIdentifier id = artifact.id.componentIdentifier
                if (id instanceof ProjectComponentIdentifier) {
                    deps.add("${id.projectPath.replace(':', '')}:${ArtifactDependencyGraph.getVariant(artifact)}:unspecified ${artifact.file.length()}")

                } else if (id instanceof ModuleComponentIdentifier) {
                    deps.add("${id.group}:${id.module}:${id.version} ${artifact.file.length()}")

                } else {
                    deps.add("${artifact.id.displayName.replace(':', '')}:unspecified:unspecified ${artifact.file.length()}")
                }
            }

            Collections.sort(deps)
            return deps
        })
    }

}

這裏會在app/build/VAHost目錄下創建versions.txt文件,將宿主依賴庫及版本及文件大小信息保存在其中,保存內容示例如圖:

backupHostR

/**
 * Save R symbol file
 */
def backupHostR(ApplicationVariant applicationVariant) {

    final ProcessAndroidResources aaptTask = this.project.tasks["process${applicationVariant.name.capitalize()}Resources"]

    // 在processXXXResources任務最後添加action
    aaptTask.doLast {
        // 拷貝R.txt文件到app/build/VAHost目錄下,並重命名爲Host_R.txt
        project.copy {
            from aaptTask.textSymbolOutputFile
            into vaHostDir
            rename { "Host_R.txt" }
        }
    }
}

備份processXXXResources任務生成的R.txt,保存在app/build/VAHost目錄下,並重命名爲Host_R.txt,截取保存內容示例如圖:

backupProguardMapping

/**
 * Save proguard mapping
 */
def backupProguardMapping(ApplicationVariant applicationVariant) {

    if (applicationVariant.buildType.minifyEnabled) {
        // 若開啓混淆,則在transformClassesAndResourcesWithProguardForXXX任務最後拷貝混淆mapping文件
        TransformTask proguardTask = project.tasks["transformClassesAndResourcesWithProguardFor${applicationVariant.name.capitalize()}"]

        ProGuardTransform proguardTransform = proguardTask.transform
        File mappingFile = proguardTransform.mappingFile

        proguardTask.doLast {
            project.copy {
                from mappingFile
                into vaHostDir
            }
        }
    }

}

備份混淆mapping.txt文件,保存在app/build/VAHost目錄下。

宿主在構建時,會備份宿主的依賴庫信息和資源R信息和混淆mapping文件。最終在app/build/VAHost目錄下生成如圖所示文件:

插件Gradle Plugin

com.didi.virtualapk.plugin.properties中找到’com.didi.virtualapk.plugin’對應的源碼實現。

implementation-class=com.didi.virtualapk.VAPlugin

實現類即爲VAPlugin.groovy

VAPlugin

VAPlugin的apply中首先會調用父類BasePlugin的apply方法:

void apply(final Project project) {
    super.apply(project)
    // ···
}

先看BasePlugin#apply:

public void apply(Project project) {
    // ···
    AppPlugin appPlugin = project.plugins.findPlugin(AppPlugin)

    Reflect reflect = Reflect.on(appPlugin.variantManager)

    // 動態代理hook VariantFactory的preVariantWork方法
    VariantFactory variantFactory = Proxy.newProxyInstance(this.class.classLoader, [VariantFactory.class] as Class[],
            new InvocationHandler() {
                Object delegate = reflect.get('variantFactory')

                @Override
                Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    if ('preVariantWork' == method.name) {
                        checkVariantFactoryInvoked = true
                        Log.i 'VAPlugin', "Evaluating VirtualApk's configurations..."
                        boolean isBuildingPlugin = evaluateBuildingPlugin(appPlugin, project)
                        // beforeCreateAndroidTasks抽象方法,由VAPlugin實現
                        beforeCreateAndroidTasks(isBuildingPlugin)
                    }

                    return method.invoke(delegate, args)
                }
            })
    reflect.set('variantFactory', variantFactory)
    
    // 註冊VAExtention,即插件build.gradle中配置的virtualApk{···}
    project.extensions.create('virtualApk', VAExtention)
    
    project.afterEvaluate {
        // 配置階段後執行
        if (!checkVariantFactoryInvoked) {
            throw new RuntimeException('Evaluating VirtualApk\'s configurations has failed!')
        }

        android.applicationVariants.each { ApplicationVariantImpl variant ->
            // buildType爲release時處理
            if ('release' == variant.buildType.name) {
                // 配置AssemblePlugin任務以及任務依賴
            }
        }
    }

    // 創建AssemblePlugin任務
    project.task('assemblePlugin', dependsOn: "assembleRelease", group: 'build', description: 'Build plugin apk')
}

回到VAPlugin#apply:

void apply(final Project project) {
    super.apply(project)

    // 在插件工程根目錄下創建host目錄
    hostDir = new File(project.rootDir, "host")
    if (!hostDir.exists()) {
        hostDir.mkdirs()
    }

    virtualApk.hostDependenceFile = new File(hostDir, "versions.txt")

    project.afterEvaluate {
        if (!isBuildingPlugin) {
            return
        }

        stripClassAndResTransform.onProjectAfterEvaluate()
        taskHookerManager = new VATaskHookerManager(project, instantiator)
        // 註冊各個Task的Hooker
        taskHookerManager.registerTaskHookers()

        if (android.dataBinding.enabled) {
            project.dependencies.add('annotationProcessor', project.files(jarPath.absolutePath))
        }

        android.applicationVariants.each { ApplicationVariantImpl variant ->

            virtualApk.with {
                VAExtention.VAContext vaContext = getVaContext(variant.name)
                // 保存包名
                vaContext.packageName = variant.applicationId
                // 保存包名路徑(小數點換成反斜杆,如com/didi/virtualapk/demo)
                vaContext.packagePath = vaContext.packageName.replace('.'.charAt(0), File.separatorChar)
                vaContext.hostSymbolFile = new File(hostDir, "Host_R.txt")
            }
        }
    }
}

應用了VAPlugin後,將會在project配置階段完成後進行一系列任務的hook操作。

beforeCreateAndroidTasks

在project配置階段完成後,會執行createAndroidTasks,其中會執行VariantFactory#preVariantWork,而該方法被BasePlugin通過動態代理hook,其中又會調用VAPlugin#beforeCreateAndroidTasks方法。

protected void beforeCreateAndroidTasks(boolean isBuildingPlugin) {
    // ···

    // 檢查gradle中配置和拷貝宿主事先備份的文件
    checkConfig()

    // 註冊自定義Transform
    stripClassAndResTransform = new StripClassAndResTransform(project)
    android.registerTransform(stripClassAndResTransform)

    // 在BuildConfig類中添加成員,如public static final int PACKAGE_ID = 0x6f
    android.defaultConfig.buildConfigField("int", "PACKAGE_ID", "0x" + Integer.toHexString(virtualApk.packageId))

    // ···
    // 遍歷項目各模塊中的依賴庫信息
    project.rootProject.subprojects { Project p ->
        p.configurations.all { Configuration configuration ->
            configuration.resolutionStrategy { ResolutionStrategy resolutionStrategy ->
                resolutionStrategy.eachDependency { DependencyResolveDetails details ->
                    // ···

                    checkConfig()

                    def hostDependency = virtualApk.hostDependencies.get("${details.requested.group}:${details.requested.name}")
                    if (hostDependency != null) {
                        if ("${details.requested.version}" != "${hostDependency['version']}") {
                            // ···
                            // 檢查插件和宿主共同的依賴庫,強制使插件使用和宿主相同的版本
                            if (virtualApk.forceUseHostDependences) {
                                details.useVersion(hostDependency['version'])
                            }
                        }
                    }
                }
            }
        }
    }
}

該方法中調用的checkConfig方法,在checkConfig方法主要做了以下操作:

  1. 檢查virtualApk.packageId是否設置且在合理範圍內[0x01~0x7f],即需要指定插件的資源ID的PP字段
  2. 檢查virtualApk.targetHost是否設置,即需要指定宿主工程主module路徑
  3. 拷貝宿主的build/VAHost/Host_R.txt至插件的host/Host_R.txt
  4. 拷貝宿主的build/VAHost/versions.txt至插件的host/versions.txt
  5. 拷貝宿主的build/VAHost/mapping.txt至插件的host/mapping.txt

執行完成後,在插件工程目錄下生成如下文件:
在這裏插入圖片描述

beforeCreateAndroidTasks方法中還註冊了StripClassAndResTransform,設置插件使用和宿主相同的依賴庫版本。

VATaskHookerManager

在project配置階段完成後,會創建VATaskHookerManager用於註冊Gradle構建關鍵Task對應的hooker。

VATaskHookerManager繼承自TaskHookerManager,在TaskHookerManager構造函數中會進行Task監聽器的註冊:

public TaskHookerManager(Project project, Instantiator instantiator) {
    this.project = project
    this.instantiator = instantiator
    android = project.extensions.findByType(AppExtension)
    // gradle對應一次Gradle構建,註冊TaskExecutionListener
    project.gradle.addListener(new VirtualApkTaskListener())
}
private class VirtualApkTaskListener implements TaskExecutionListener {

    @Override
    void beforeExecute(Task task) {
        // Gradle構建期間每個Task執行前回調
        // ···
    }

    @Override
    void afterExecute(Task task, TaskState taskState) {
        // Gradle構建期間每個Task執行後回調
        // ···
    }
}

這裏監聽所有Task的執行,在執行前後做相關操作。

接着看TaskHookerManager#registerTaskHooker方法,通過該方法註冊Task Hooker:

// 用於保存Task Hooker
protected Map<String, GradleTaskHooker> taskHookerMap = new HashMap<>()

protected void registerTaskHooker(GradleTaskHooker taskHooker) {
    // 使taskHooker持有VATaskHookerManager
    taskHooker.setTaskHookerManager(this)
    // 以要hook的任務名稱爲key,hooker實例爲value,保存在集合中
    taskHookerMap.put(taskHooker.taskName, taskHooker)
}

註冊即是保存hooker在集合中。

繼續看VirtualApkTaskListener實現的兩個回調方法:

void beforeExecute(Task task) {
    if (task.project == project) {
        if (task in TransformTask) {
            taskHookerMap["${task.transform.name}For${task.variantName.capitalize()}".toString()]?.beforeTaskExecute(task)
        } else {
            taskHookerMap[task.name]?.beforeTaskExecute(task)
        }
    }
}

void afterExecute(Task task, TaskState taskState) {
    if (task.project == project) {
        if (task in TransformTask) {
            taskHookerMap["${task.transform.name}For${task.variantName.capitalize()}".toString()]?.afterTaskExecute(task)
        } else {
            taskHookerMap[task.name]?.afterTaskExecute(task)
        }
    }
}

當觸發beforeExecute和afterExecute回調時,會從taskHookerMap查找匹配當前Task的Task Hooker,調用其對應的beforeTaskExecute和afterTaskExecute方法。

回過頭看VATaskHookerManager#registerTaskHookers方法:

void registerTaskHookers() {
    android.applicationVariants.all { ApplicationVariantImpl appVariant ->
        if (!appVariant.buildType.name.equalsIgnoreCase("release")) {
            return
        }
      
        // 註冊Gradle構建流程中的關鍵Task的Hooker
        registerTaskHooker(instantiator.newInstance(PrepareDependenciesHooker, project, appVariant))
        registerTaskHooker(instantiator.newInstance(MergeAssetsHooker, project, appVariant))
        registerTaskHooker(instantiator.newInstance(MergeManifestsHooker, project, appVariant))
        registerTaskHooker(instantiator.newInstance(MergeJniLibsHooker, project, appVariant))
        registerTaskHooker(instantiator.newInstance(ProcessResourcesHooker, project, appVariant))
        registerTaskHooker(instantiator.newInstance(ProguardHooker, project, appVariant))
        registerTaskHooker(instantiator.newInstance(DxTaskHooker, project, appVariant))
    }
}

該方法中註冊Gradle構建流程中的關鍵Task的Hooker:

  • PrepareDependenciesHooker:hook preXXXBuild任務
  • MergeAssetsHooker:hook mergeXXXAssets任務
  • MergeManifestsHooker:hook processXXXManifest任務
  • MergeJniLibsHooker:hook transformXXXMergeJniLibsXXX任務
  • ProcessResourcesHooker:hook processXXXResources任務
  • ProguardHooker:hook transformXXXProguardXXX任務
  • DxTaskHooker:hook transformXXXDexXXX任務

PrepareDependenciesHooker

用於收集宿主使用到的依賴庫、插件中不和宿主重複的依賴庫、與宿主重複的依賴庫,依賴庫包括aar和jar。

  • beforeTaskExecute
@Override
void beforeTaskExecute(AppPreBuildTask task) {

    // 讀取host/versions.txt中的依賴庫信息,保存依賴庫的group和name至hostDependencies集合中
    hostDependencies.addAll(virtualApk.hostDependencies.keySet())

    // 將virtualApk{excludes}配置的依賴信息也添加入hostDependencies
    // ···
}

這裏會解析host/versions.txt中的宿主依賴庫信息,並將依賴信息保存在hostDependencies集合中,例如:[com.android.support:animated-vector-drawable, com.android.support:appcompat-v7, com.didi.virtualapk:core]。
注意:如果是依賴aar,則保存信息格式爲"groupId:artifactId"。若爲jar,則保存信息格式爲"jar文件名稱:unspecified"。

  • afterTaskExecute
@Override
void afterTaskExecute(AppPreBuildTask task) {
    // ···
    // 獲取依賴信息集合
    Dependencies dependencies
    if (project.extensions.extraProperties.get(Constants.GRADLE_3_1_0)) {
        ImmutableMap<String, String> buildMapping = Reflect.on('com.android.build.gradle.internal.ide.ModelBuilder')
                .call('computeBuildMapping', project.gradle)
                .get()
        dependencies = new ArtifactDependencyGraph().createDependencies(scope, false, buildMapping, consumer)
    } else {
        dependencies = new ArtifactDependencyGraph().createDependencies(scope, false, consumer)
    }

    // 遍歷插件工程依賴aar
    dependencies.libraries.each {
        def mavenCoordinates = it.resolvedCoordinates
        if (hostDependencies.contains("${mavenCoordinates.groupId}:${mavenCoordinates.artifactId}")) {
            Log.i 'PrepareDependenciesHooker', "Need strip aar: ${mavenCoordinates.groupId}:${mavenCoordinates.artifactId}:${mavenCoordinates.version}"
            // 若宿主有依賴相同的依賴庫,則將依賴信息添加至stripDependencies集合
            stripDependencies.add(
                    new AarDependenceInfo(
                            mavenCoordinates.groupId,
                            mavenCoordinates.artifactId,
                            mavenCoordinates.version,
                            it))

        } else {
            Log.i 'PrepareDependenciesHooker', "Need retain aar: ${mavenCoordinates.groupId}:${mavenCoordinates.artifactId}:${mavenCoordinates.version}"
            // 僅插件有依賴的庫,添加至retainedAarLibs集合
            retainedAarLibs.add(
                    new AarDependenceInfo(
                            mavenCoordinates.groupId,
                            mavenCoordinates.artifactId,
                            mavenCoordinates.version,
                            it))
        }

    }
    // 遍歷插件工程依賴jar
    dependencies.javaLibraries.each {
        def mavenCoordinates = it.resolvedCoordinates
        if (hostDependencies.contains("${mavenCoordinates.groupId}:${mavenCoordinates.artifactId}")) {
            Log.i 'PrepareDependenciesHooker', "Need strip jar: ${mavenCoordinates.groupId}:${mavenCoordinates.artifactId}:${mavenCoordinates.version}"
            // 若宿主有依賴相同的依賴庫,則將依賴信息添加至stripDependencies集合
            stripDependencies.add(
                    new JarDependenceInfo(
                            mavenCoordinates.groupId,
                            mavenCoordinates.artifactId,
                            mavenCoordinates.version,
                            it))
        } else {
            Log.i 'PrepareDependenciesHooker', "Need retain jar: ${mavenCoordinates.groupId}:${mavenCoordinates.artifactId}:${mavenCoordinates.version}"
            // 僅插件有依賴的庫,添加至retainedJarLib集合
            retainedJarLib.add(
                    new JarDependenceInfo(
                            mavenCoordinates.groupId,
                            mavenCoordinates.artifactId,
                            mavenCoordinates.version,
                            it))
        }

    }

    // ···

    Log.i 'PrepareDependenciesHooker', "Analyzed all dependencis. Get more infomation in dir: ${hostDir.absoluteFile}"

    vaContext.stripDependencies = stripDependencies
    vaContext.retainedAarLibs = retainedAarLibs
    // ···
}

這裏篩選出插件工程中依賴庫信息以及需要剔除的依賴庫信息。stripDependencies保存需要剔除的依賴庫,retainedAarLibs保存需要保留的aar依賴庫,retainedJarLib保存需要保留的jar依賴庫。

MergeManifestsHooker

用於合併AndroidManifest文件時移除需要剔除的依賴庫的Manifest文件,以及剔除AndroidManifest中的application節點中的特定屬性。

  • beforeTaskExecute
@Override
void beforeTaskExecute(MergeManifests task) {

    // 查找stripDependencies集合中AAR依賴庫信息,保存至stripAarNames集合,保存格式如groupId:artifactId:version
    def stripAarNames = vaContext.stripDependencies.
            findAll {
                it.dependenceType == DependenceInfo.DependenceType.AAR
            }.
            collect { DependenceInfo dep ->
                "${dep.group}:${dep.artifact}:${dep.version}"
            } as Set<String>

    // 反射設置MergeManifests的manifests爲FixedArtifactCollection
    Reflect reflect = Reflect.on(task)
    ArtifactCollection manifests = new FixedArtifactCollection(this, reflect.get('manifests'), stripAarNames)
    reflect.set('manifests', manifests)
}

這裏通過反射修改了MergeManifests的manifests成員,當Gradle構建過程中進行收集Manifest時,會調用FixedArtifactCollection的對應方法。

看FixedArtifactCollection#getArtifacts方法:

@Override
Set<ResolvedArtifactResult> getArtifacts() {
    Set<ResolvedArtifactResult> set = origin.getArtifacts()
    set.removeIf(new Predicate<ResolvedArtifactResult>() {
        @Override
        boolean test(ResolvedArtifactResult result) {
            // 判斷是否在需剔除AAR集合中,若是則需要移除
            boolean ret = stripAarNames.contains("${result.id.componentIdentifier.displayName}")
            if (ret) {
                Log.i 'MergeManifestsHooker', "Stripped manifest of artifact: ${result} -> ${result.file}"
            }
            return ret
        }
    })

    hooker.mark()
    return set
}

FixedArtifactCollection在返回收集結果前會移除需要剔除的元素。

  • afterTaskExecute
    在afterTaskExecute回調中獲取任務執行完畢輸出的Manifest文件,重寫剔除application節點中的icon、label、allowBackup、supportsRtl屬性。

示例如下:
剔除前:

<application
    android:name="com.didi.virtualapk.demo.MyApplication"
    android:allowBackup="true"
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/AppTheme" >

剔除後:

<application
    android:theme="@ref/0x6f060001"
    android:name="com.didi.virtualapk.demo.MyApplication">

ProcessResourcesHooker

用於修改經過AAPT生成的resources.arsc文件、R.txt文件、其他相關.xml文件,刪除宿主包含的資源,用virtualApk.packageId重新生成資源ID。(這部分的實現參考了Small框架中的方案)

在afterTaskExecute回調中,獲取任務執行產物 build/intermediates/res/resources-XXX.ap_ 文件,之後調用repackage進行解析處理。

resources-XXX.ap_文件解壓後可獲得resources.arsc文件:
在這裏插入圖片描述

接下來進入repackage方法,該方法中首先進行清理、解壓、備份等文件操作,將在build/intermediates/res目錄下生成文件:

繼續看ProcessResourcesHooker#repackage方法:

void repackage(ProcessAndroidResources par, File apFile) {
    // 清理、解壓、備份 ···
    
    // 收集插件中需要重設ID的資源信息
    resourceCollector = new ResourceCollector(project, par)
    resourceCollector.collect()
    
    // ···
}

ResourceCollector#collect主要進行五步操作:

  1. 解析插件編譯生成的R.txt文件,讀取每行資源信息,保存在allResources集合(Common Res)和allStyleables集合(Styleable)中。

舉例說明:
假設R.txt文件中包含兩條資源信息
Common Res=> int string abc_action_bar_home_description 0x7f090000
Styleable=> int[] styleable TagLayout { 0x010100af, 0x7f0102b5, 0x7f0102b6 }
valueType:int or int[]
resType:attr/string/color etc.
resName:abc_action_bar_home_description or TagLayout
resId:0x7f090000/0x010100af/0x7f0102b5/0x7f0102b6
allResources集合以resType爲key,ResourceEntry(保存resType、resName、resId)爲value緩存信息
allStyleables集合以StyleableEntry(保存resName、resId、valueType)爲元素緩存信息

  1. 解析從宿主拷貝的Host_R.txt文件,讀取每行資源信息,保存在hostResources集合(Common Res)和hostStyleables集合(Styleable)中,規則同上一步。

  2. 篩選出插件獨有資源,保存至pluginResources集合和pluginStyleables集合。同時當檢索到同類型同名稱資源時,將修改插件資源ID成使用宿主資源ID(例如插件有一個資源drawable/bg_home.png,其編譯生成ID爲0x7f08002a,而宿主中也有同名資源drawable/bg_home.png,其ID爲0x7f0900c9,則會將插件資源ID修改爲0x7f0900c9)。

pluginResources = allResources - hostResources
pluginStyleables = allStyleables - hostStyleables

  1. 遍歷pluginResources集合和pluginStyleables集合,爲其中緩存的資源實體設置新的資源ID。資源ID的組成格式爲0x+packageId(1字節)+typeId(1字節)+entryId(2字節),這裏會用到插件build.gradle配置的virtualApk.packageId作爲ID的packageId段,然後按序生成typeId段和entryId段,重新設置插件中的資源ID。

  2. 遍歷retainedAarLibs集合(需要保留的AAR依賴庫),收集其中有使用到的資源信息。

回到ProcessResourcesHooker#repackage方法,當收集和重設資源ID後,繼續往下處理:

void repackage(ProcessAndroidResources par, File apFile) {
    // ···
    
    def aapt = new Aapt(resourcesDir, rSymbolFile, androidConfig.buildToolsRevision)

    //Delete host resources, must do it before filterPackage
    // 刪除resources-XXX.ap_中res目錄下和宿主共享的資源
    aapt.filterResources(retainedTypes, filteredResources)
    //Modify the arsc file, and replace ids of related xml files
    aapt.filterPackage(retainedTypes, retainedStylealbes, virtualApk.packageId, resIdMap, libRefTable, updatedResources)
    
    // ···
}

這裏調用了Aapt#filterPackage方法,進入該方法:

void filterPackage(final List<?> retainedTypes, final List<?> retainedStyleables, final int pp, final Map<?, ?> idMaps, final Map<?, ?> libRefTable, final Set<String> outUpdatedResources) {
    final File arscFile = new File(this.assetDir, RESOURCES_ARSC)
    final def arscEditor = new ArscEditor(arscFile, toolsRevision)

    // Filter R.txt
    if (this.symbolFile != null) {
        // 修改R.txt
        this.filterRTxt(this.symbolFile, retainedTypes, retainedStyleables)
    }

    // 修改resources.arsc文件
    arscEditor.slice(pp, idMaps, libRefTable, retainedTypes)
    outUpdatedResources.add(RESOURCES_ARSC)
    // 修改相關.xml文件
    this.resetAllXmlPackageId(this.assetDir, pp, idMaps, outUpdatedResources)
}
  1. filterRTxt方法中重寫processXXXResources任務產物R.txt文件中的內容,其中資源ID使用新指定的ID值。
  2. ArscEditor#slice方法會對resources.arsc文件進行修改,關於resources.arsc的說明可參考《Android應用程序資源的編譯和打包過程分析》
    在這裏插入圖片描述
              Arsc struct
        +-----------------------+
        | Table Header          |
        +-----------------------+
        | Res string pool       |
        +-----------------------+
        | Package Header        | <-- rewrite entry 1: package id
        +-----------------------+
        | Type strings          |
        +-----------------------+
        | Key strings           |
        +-----------------------+
        | DynamicRefTable chunk | <-- insert entry (for 5.0+)
        +-----------------------+
        | Type spec             |
        |                  * N  |
        | Type info  * M        | <-- rewrite entry 2: entry value
        +-----------------------+

核心原理是修改arsc文件中Package Header的package id段爲指定的pp值,修改每項資源信息entry value(即修改成重新按序生成的資源ID值,以及引用其他資源的索引)。在Android 5.0以上需要往DynamicRefTable插入一組packageId和packageName的映射數組。

DynamicRefTable:它是用來保存資源共享庫中的資源的編譯時ID和運行時ID的映射關係。資源共享庫在編譯時會被分配一個pp段ID,當在運行加載時也會被分配一個pp段ID。資源共享庫在編譯時順序和運行時加載的順序可能不一致,導致分配的ID也可能不一致,因此通過DynamicRefTable來查詢兩者映射關係。

  1. 掃描.xml文件,修改其中使用到的資源ID值。

例如,當在開發時,在AndroidManifest.xml中用到string資源:

<activity
    android:name="com.didi.virtualapk.demo.aidl.BookManagerActivity"
    android:label="@string/title_activity_book_manager" >

當編譯時,會將string引用轉換成ID值:

<activity
    android:label="@ref/0x7f0a0017"
    android:name="com.didi.virtualapk.demo.aidl.BookManagerActivity">

這裏將原ID值修改爲新生成的ID值:

<activity
    android:label="@ref/0x6f050002"
    android:name="com.didi.virtualapk.demo.aidl.BookManagerActivity">

再回到ProcessResourcesHooker#repackage方法,當重寫文件修改資源ID值後,繼續往下處理:

void repackage(ProcessAndroidResources par, File apFile) {
    // ···
    
    /*
     * Delete filtered entries and then add updated resources into resources-${variant.name}.ap_
     */
     // 刪除有發生修改的文件的原文件
    com.didi.virtualapk.utils.ZipUtil.with(apFile).deleteAll(filteredResources + updatedResources)
    
    // 執行aapt add命令
    project.exec {
        executable par.buildTools.getPath(BuildToolInfo.PathId.AAPT)
        workingDir resourcesDir
        args 'add', apFile.path
        args updatedResources
        standardOutput = System.out
        errorOutput = System.err
    }
    
    // ···
}

這裏將有產生修改的文件對應的原始文件刪除,然後執行aapt add命令將修改後的文件打包入resources-XXX.ap_。

繼續看ProcessResourcesHooker#repackage方法:

void repackage(ProcessAndroidResources par, File apFile) {
    // ···
    
    // 最後一步,重新生成R.java
    updateRJava(aapt, par.sourceOutputDir)
}

updateRJava方法中將會使用新的資源ID重新生成R.java(包括AAR依賴庫中的R.java)。

至此便完成了插件資源ID的重設,由原來對0x7fxxxxxx的引用修改爲0x6fxxxxxx(6f是根據virtualApk.packageId的配置),避免了和宿主資源互通時的ID衝突。

MergeAssetsHooker

用於在mergeAssets之前移除和宿主重複的Assets資源。

@Override
void beforeTaskExecute(MergeSourceSetFolders task) {

    // 收集需要剔除的AAR中的assets路徑
    Set<String> strippedAssetPaths = vaContext.stripDependencies.collect {
        if (it instanceof AarDependenceInfo) {
            return it.assetsFolder.path
        }
        return ''
    }

    // 通過反射設置MergeSourceSetFolders的assetSetSupplier成員
    Reflect reflect = Reflect.on(task)
    reflect.set('assetSetSupplier', new FixedSupplier(this, reflect.get('assetSetSupplier'), strippedAssetPaths))
}

這裏使用自定義FixedSupplier hook原assetSetSupplier值,當mergeXXXAssets任務執行時會通過調用其get方法收集各assets資源集合。

FixedSupplier#get:

@Override
List<AssetSet> get() {
    // 執行原邏輯
    List<AssetSet> assetSets = origin.get()
    // 從中剔除不需要打包的
    assetSets.removeIf(new Predicate<AssetSet>() {
        @Override
        boolean test(AssetSet assetSet) {
            // 匹配判斷是否在需剔除資源集合中
            boolean ret = strippedAssetPaths.contains(assetSet.sourceFiles.get(0).path)
            if (ret) {
                Log.i 'MergeAssetsHooker', "Stripped asset of artifact: ${assetSet} -> ${assetSet.sourceFiles.get(0).path}"
            }
            return ret
        }
    })
    hooker.mark()
    return assetSets
}

StripClassAndResTransform

用於剔除和宿主共享的代碼,相當於以provided/compileOnly方式依賴公共庫。

@Override
void transform(final TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {

    VAExtention.VAContext vaContext = virtualApk.getVaContext(transformInvocation.context.variantName)
    // 收集需剔除的jar,解壓獲取其中的文件
    def stripEntries = classAndResCollector.collect(vaContext.stripDependencies)

    if (!isIncremental()) {
        transformInvocation.outputProvider.deleteAll()
    }

    // 遍歷該Transform執行時輸入文件
    transformInvocation.inputs.each {
        it.directoryInputs.each { directoryInput ->
            Log.i 'StripClassAndResTransform', "input dir: ${directoryInput.file.absoluteFile}"
            def destDir = transformInvocation.outputProvider.getContentLocation(
                    directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
            Log.i 'StripClassAndResTransform', "output dir: ${destDir.absoluteFile}"
            directoryInput.file.traverse(type: FileType.FILES) {
                def entryName = it.path.substring(directoryInput.file.path.length() + 1)
                if (!stripEntries.contains(entryName)) {
                    // 比較文件名,若不在需移除集合中,則拷貝到目標輸出目錄
                    def dest = new File(destDir, entryName)
                    FileUtils.copyFile(it, dest)
                } else {
                    Log.i 'StripClassAndResTransform', "Stripped file: ${it.absoluteFile}"
                }
            }
        }

        it.jarInputs.each { jarInput ->
            Log.i 'StripClassAndResTransform', "input jar: ${jarInput.file.absoluteFile}"
            Set<String> jarEntries = HostClassAndResCollector.unzipJar(jarInput.file)
            if (!stripEntries.containsAll(jarEntries)){
                // 比較文件名,若不在需移除集合中,則拷貝到目標輸出目錄
                def dest = transformInvocation.outputProvider.getContentLocation(jarInput.name,
                        jarInput.contentTypes, jarInput.scopes, Format.JAR)
                Log.i 'StripClassAndResTransform', "output jar: ${dest.absoluteFile}"
                FileUtils.copyFile(jarInput.file, dest)
            } else {
                Log.i 'StripClassAndResTransform', "Stripped jar: ${jarInput.file.absoluteFile}"
            }
        }
    }

    vaContext.checkList.mark(name)
}

ProguardHooker

用於應用宿主混淆mapping文件,使插件在混淆打包後對調用宿主代碼或公共代碼部分可以和宿主混淆後代碼一致,不會出現ClassNotFound等異常。

@Override
void beforeTaskExecute(TransformTask task) {

    def proguardTransform = task.transform as ProGuardTransform

    File applyMappingFile;

    //Specifies the proguard mapping file through ${ MAPPING_KEY }
    // 若輸入gradle命令有帶-PapplyMapping參數,則取該參數值作爲mapping文件路徑
    if (project.hasProperty(MAPPING_KEY)) {
        applyMappingFile = new File(project.properties[MAPPING_KEY])
        if (!applyMappingFile.exists()) {
            throw new InvalidUserDataException("${project.properties[MAPPING_KEY]} does not exist")
        }
        if (!applyMappingFile.isFile()) {
            throw new InvalidUserDataException("${project.properties[MAPPING_KEY]} is not a file")
        }
    }

    //Default to use the mapping file generated by host apk
    if (virtualApk.applyHostMapping && applyMappingFile == null) {
        // 使用從宿主拷貝的mapping文件
        applyMappingFile = new File(project.rootProject.projectDir, "host/mapping.txt")
    }

    if (applyMappingFile?.exists()) {
        // 應用該mapping文件
        proguardTransform.applyTestedMapping(applyMappingFile)
    }

    vaContext.stripDependencies.each {
        // 將需移除的jar文件輸入給ProGuardTransform以便進行混淆映射
        proguardTransform.libraryJar(it.jarFile)
        if (it instanceof AarDependenceInfo) {
            it.localJars.each {
                proguardTransform.libraryJar(it)
            }
        }
    }
    mark()
}

DxTaskHooker

用於從R.class中剔除共享的資源常量,僅保留插件獨有使用的常量。

DxTaskHooker的beforeTaskExecute回調中遍歷輸入文件,若是jar則再解壓得到其中文件,再依次調用recompileSplitR方法(僅對插件包名路徑結尾的文件調用)。

boolean recompileSplitR(File pkgDir) {

    // 收集R$XXX.class文件
    File[] RClassFiles = pkgDir.listFiles(new FilenameFilter() {
        @Override
        boolean accept(File dir, String name) {
            return name.startsWith('R$') && name.endsWith('.class')
        }
    })

    if(RClassFiles?.length) {
        RClassFiles.each {
            // 依次刪除文件
            it.delete()
        }

        String baseDir = pkgDir.path - "${File.separator}${vaContext.packagePath}"

        // 使用在ProcessResourcesHooker中修改後的R.java編譯R.class
        project.ant.javac(
            srcdir: vaContext.splitRJavaFile.parentFile,
            source: apkVariant.javaCompiler.sourceCompatibility,
            target: apkVariant.javaCompiler.targetCompatibility,
            destdir: new File(baseDir))

        mark()
        return true
    }

    return false
}

MergeJniLibsHooker

用於移除和宿主重複的so庫。

@Override
void beforeTaskExecute(TransformTask task) {

    def excludeJniFiles = jniLibsCollector.collect(vaContext.stripDependencies)

    // 通過設置packagingOptions的excludes集合來排除so,避免被打包進插件apk
    excludeJniFiles.each {
        androidConfig.packagingOptions.exclude("/${it}")
        Log.i 'MergeJniLibsHooker', "Stripped jni file: ${it}"
    }

    mark()
}

AssemblePlugin

用於輸出插件apk到指定目錄和設置插件apk名稱。

@TaskAction
public void outputPluginApk() {
    // ···

    // 將打包產物apk拷貝到build/outputs/plugin/${variant.name}目錄下,並按包名+時間戳格式重命名apk文件
    getProject().copy {
        from originApkFile
        into pluginApkDir
        rename { "${appPackageName}_${apkTimestamp}.apk" }
    }
}

尾聲

初次接入VirtualAPK框架時,新手經常遇到Gradle構建錯誤,不理解每個配置項的含義作用。通過對VirtualAPK的Gradle構建插件的簡單梳理,對VirtualAPK框架的集成使用和屬性配置有進一步認識,知其然知其所以然。

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