滴滴插件化框架学习笔记之virtualapk-gradle-plugin

2,512 阅读9分钟

前言

在集成使用滴滴插件化框架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框架的集成使用和属性配置有进一步认识,知其然知其所以然。