Android Gradle 中的实例之动态修改AndroidManifest文件

Posted 好人静

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android Gradle 中的实例之动态修改AndroidManifest文件相关的知识,希望对你有一定的参考价值。

前言

        逐步整理的一系列的总结:

        Android Gradle插件开发初次交手(一)

        Android Gradle的基本概念梳理(二)

       Android 自定义Gradle插件的完整流程(三) 

       Android 自定义Task添加到任务队列(四)

       Android 自定义Gradle插件的Extension类(五)

       Android Gradle 中的Transform(六)

       Android Gradle之Java字节码(七)

      Android Gradle 中的字节码插桩之ASM(八)

      Android Gradle 中的使用ASMified插件生成.class的技巧(九)

      android Gradle 中的实例之动态修改AndroidManifest文件(十)


        前几天想把项目升级到Android12,在适配Android12的时候有一条适配为:如果在AndroidManifest.xml文件中注册Activity、Service、BroadcastReceiver的时候,如果使用了intent-filters修饰,那么就必须为该组件显示的声明android:exported属性,用来标记给组件是否支持其他应用调用,否则在编译阶段就会抛出以下异常:

Execution failed for task ':app:processHuaweiDebugMainManifest'.
> Manifest merger failed : Apps targeting Android 12 and higher are required to specify an explicit
value for `android:exported` when the corresponding component has an intent filter defined.
 See https://developer.android.com/guide/topics/manifest/activity-element#exported for details.

        对于APP本身的AndroidManifest.xml文件来说,可以根据实际的需求(即是否支持其他应用调用)来声明android:exported属性,但是如果第三方的SDK的AndroidManifest.xml文件中同样含有上述条件,并且还没有做适配的时候,如果将该项目的targetSdkVersion升级到31的时候,该项目会一直无法编译通过,因为所有的依赖包的AndroidManifest.xml都会被打包到一起。

        问题就可以通过Android Gradle Plugin对AndroidManifest.xml文件中动态添加android:exported属性。

        同样,像某些第三方的SDK的AndroidManifest.xml文件中含有一些节点是我们不需要或者某些权限需要剔除的,都可以用这种方式来解决。

       这里主要是一些思路梳理过程。如果直接想结论性的内容,欢迎移步到个人公众号。自己会更加揣摩一句话该怎么写,怎么能把这个知识点简练的总结出来。

一 Android Gradle Plugin的Task

        在Android Gradle 中的Transform(六)一 APP打,包流程 已经介绍过APK的一个流程:

  • 1.aidl文件经过aidl编译成Java Interface文件,R文件以及java源代码经过Java Compiler编译成.class文件;
  • 2..class文件和第三方库的.class文件经过dex转换成Android虚拟机可以识别的.dex文件;
  • 3..dex文件和一些Application resouces经过aapt编译之后的文件、以及其他的resouce文件经过apkbuilder打包成.apk文件;
  • 4.apk文件经过签名工具最后生成可以签名的apk文件

      Android Studio通过Gradle就是通过一系列的Task来完成整个构建过程。 从Build输出窗口的打印的Task可以看出,在使用Android Studio运行一个APP从编译到打包成一个apk,会输出如下的一些Task:

> Task :app:preBuild
> Task :app:preDebugBuild
> Task :app:compileDebugAidl NO-SOURCE
> Task :app:compileDebugRenderscript NO-SOURCE
> Task :app:generateDebugBuildConfig
> Task :app:javaPreCompileDebug
> Task :app:generateDebugResValues
> Task :app:generateDebugResources
> Task :app:checkDebugAarMetadata
> Task :app:createDebugCompatibleScreenManifests
> Task :app:extractDeepLinksDebug
> Task :app:processDebugMainManifest
> Task :app:processDebugManifest
> Task :app:mergeDebugNativeDebugMetadata NO-SOURCE
> Task :app:mergeDebugShaders
> Task :app:compileDebugShaders NO-SOURCE
> Task :app:generateDebugAssets UP-TO-DATE
> Task :app:mergeDebugAssets
> Task :app:compressDebugAssets
> Task :app:processDebugJavaRes NO-SOURCE
> Task :app:checkDebugDuplicateClasses
> Task :app:desugarDebugFileDependencies
> Task :app:mergeDebugJavaResource
> Task :app:mergeDebugResources
> Task :app:mergeDebugJniLibFolders
> Task :app:mergeLibDexDebug
> Task :app:validateSigningDebug
> Task :app:writeDebugAppMetadata
> Task :app:writeDebugSigningConfigVersions
> Task :app:processDebugManifestForPackage
> Task :app:mergeDebugNativeLibs
> Task :app:stripDebugDebugSymbols NO-SOURCE
> Task :app:mergeExtDexDebug
> Task :app:processDebugResources
> Task :app:compileDebugJavaWithJavac
> Task :app:compileDebugSources
> Task :app:dexBuilderDebug
> Task :app:mergeProjectDexDebug
> Task :app:packageDebug
> Task :app:assembleDebug

         主要了解一些Task的作用,方便在整个过程中选择适合的锚点来添加自定义的Task,从而实现自定义行为。对应的实现类位于gradle-core/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/tasks/目录下。

  • 1.preBuild和preDebugBuild

        空task,仅做锚点使用,两者区别在于preDebugBuild针对变体的锚点,如设置了productFlavors则preDebugBuild就会变成preHuaweiDebugBuild,同样除去preBuild,其他的Task也都会增加该变体标识。对应实现类为AppPreBuildTask.java。

        productFlavors相关内容可参见Android Gradle中的productFlavors

  • 2.compileDebugAidl

        处理.aidl文件,将.aidl文件转换成java interface文件。若没有.aidl文件则会如上显示NO-SOURCE。对应实现类为AidlCompile.java。

  • 3.compileDebugRenderscript

        处理renderscript。若没有对应的文件,同样会显示NO-SOURCE。对应实现类为RenderscriptCompile.java。

        RenderScript 是 Android 3.0 提出的一个高效的计算框架,能够自动的将计算任务分配给CPU、GPU、DSP等,为处理图片、数学模型计算等场景提供高效的计算能力。

         语法类似 C/C++, 但它是在运行时编译,是跨平台的。性能比 Java 好,比 Native 略差。   

        在使用的时候分两步:(1)编写.rs文件;(2)使用 RenderScript。

  • 4.generateDebugBuildConfig

        生成BuildConfig.java文件,里面会包含DEBUG、APPLICATION_ID、FLAVOR、VERSION_CODE、VERSION_NAME以及自定义的属性,如图:

        

           对应实现类为GenerateBuildConfig.java。

  • 5.javaPreCompileDebug

        生成annotaionProcessors.json文件。对应实现类为JavaPreCompileTask.java。

  • 6.generateDebugResValues

       生成revalues、generated.xml。对应实现类为GenerateResValues.java。

  • 7.generateDebugResources

        空task,仅做锚点。

  • 8.checkDebugAarMetadata
  • 9.createDebugCompatibleScreenManifests

        Manifest文件中生成compatible-screens,指定屏幕适配。对应的实现类CompatibleScreenManifests.java。

  • 10.extractDeepLinksDebug     
  • 11.processDebugMainManifest

         合并所有的Manifest文件包含各个依赖包的AndroidManifest.xml(一点思考:可在该Task执行之前,对AndroidManifest.xml进行预处理:例如实现在合并AndroidManifest.xml之前,为符合条件的组件添加Android12的android:exported属性),然后替换掉AndroidManifest.xml文件中的某些在主module中在build.gradle中定义的placeholders或者属性(如package、version_code)等,生成最终的AndroidManifest.xml文件(一点思考:可以处理一些与变体无关的信息:例如可以在该Task执行之后,对最终对AndroidManifest.xml文件进行二次处理:如修改每次上线的versionCode和versionName)保存在app/build/intermediates/merged_manifest,如图:

         

对应的实现类ProcessApplicationManifest.java

  • 12.processDebugManifest

         使用合并的AndroidManifest.xml文件,为所有的变体创建AndroidManifest.xml文件(一点思考:处理与变体相关的信息,可以在执行Task之后,对AndroidManifest.xml文件进行二次处理,最终保存在app/build/intermediates/merged_manifest,如图:

         

         对应实现类ProcessMultiApkApplicationManifest.java。

  • 13.mergeDebugNativeDebugMetadata NO-SOURCE
  • 14.mergeDebugShaders

        编译shaders,对应实现类ShaderCompile.java。

  • 15.compileDebugShaders NO-SOURCE
  • 16.generateDebugAssets UP-TO-DATE
  • 17.mergeDebugAssets

        合并assets文件。对应的实现类MergeSourceSetFolders.java。

  • 18.compressDebugAssets
  • 19.processDebugJavaRes       

         处理java res 。若没有对应的文件,同样会显示NO-SOURCE 。对应实现类为ProcessJavaresConfigAction.java。

  • 20.checkDebugDuplicateClasses
  • 21.desugarDebugFileDependencies
  • 22.mergeDebugJavaResource
  • 23.mergeDebugResources 

        合并资源文件(一点思考:可以在该Task之前对合并之前的资源文件进行预处理:例如在合并资源文件的时会进行去重操作,即相同的资源ID的资源文件会被认为是同一个,那么就改变该逻辑。)包括各个依赖包中的资源文件,最终调用aapt2命令去处理资源文件,生成“原资源文件名.xml.flat”格式的文件保存在app/build/intermediates/res目录下,如图:

         

        对应实现类MergeResources.java。

  • 24.mergeDebugJniLibFolders

          合并jni lib文件,对应实现类MergeSourceSetFolder.java。

  • 25.mergeLibDexDebug
  • 26.validateSigningDebug

          验证签名,对应实现类为ValidateSigningTask.java。

  • 27.writeDebugAppMetadata
  • 28.writeDebugSigningConfigVersions
  • 29.processDebugManifestForPackage
  • 30.mergeDebugNativeLibs
  • 31.stripDebugDebugSymbols NO-SOURCE
  • 32.mergeExtDexDebug
  • 33.processDebugResources

        aapt打包资源,生成最终的R.java(一点思考:可以在该执行该task之后对R文件进行二次处理)以及res.ap_,最终将文件保存app/build/intermediates/processed_res目录下:

        

         对应实现类为ProcessAndroidResouces.java。

        在使用aapt打包res资源文件,res资源文件又会分为二进制文件和非二进制文件,典型的非二进制文件如res/raw、图片,要求保持原样,不被编译。最终这些资源文件被打包成R.java、resources.arsc和res文件

  • 34.compileDebugJavaWithJavac               

        编译java文件,对应的实现类为AndroidJavaCompile.java。

  • 35.compileDebugSources       

        空task,仅做锚点。

  • 36.dexBuilderDebug
  • 37.mergeProjectDexDebug
  • 38.packageDebug       

        打包apk,对应实现类PackageApplication

  • 39.assembleDebug      

          空task,仅做锚点。

        遗留问题:其他的Task可在后面陆续去研究。这次需要使用的仅用到上面红色标记的processDebugMainManifestprocessDebugManifest。

二 实例1:适配Android12的android:exported属性

     在前言提到的Android12如果没有对android:exported属性进行适配,那么执行到processHuaweiDebugMainManifest这个任务的时候就会抛出编译错误。所以借助这个应用场景,通过自定义Android Gradle plugin的方式来在processHuaweiDebugMainManifest这个任务执行之前,完成对第三方的SDK的Android12该属性的适配。

1.思路梳理        

        针对上面提出的问题,整理下代码逻辑: 

  • (1)自定义AddExportForEveryPackageManifestTask;
    • 1)传入所有的依赖包以及主module的AndroidManifest.xml文件;
    • 2)找到该AndroidManifest.xml文件的Activity、Service、BroadcastReceiver组件;
    • 3)判断该组件是否满足“含有intent-filter && 没有添加android:exported”;
    • 4)添加android:exported=true(因为考虑只为第三方的SDK中添加,所以设置为true比较保险。本APP的组件需要根据自身APP的特点由开发人员设置为true或false);
    • 5)将处理好的内容重新写入AndroidManifest.xml文件;
  • (2)找到锚点processDebugMainManifest Task,添加AddExportForEveryPackageManifestTask到任务队列
    • 1)创建该插件的Project文件
    • 2)找到processDebugMainManifest
    • 3)在processDebugMainManifest执行之前添加AddExportForEveryPackageManifestTask
  • (3)发布、使用插件

2.实例代码

        有了上面的思路,开始逐步实现。

(1)新建、发布及使用插件

        怎么创建一个Android Gradle Plugin可参见 Android Gradle插件开发初次交手(一),这里不在多余介绍。最终创建插件的源代码位于manifestplugin这个module下,将该插件应用到项目中。

(2)自定义AddExportForEveryPackageManifestTask

         这个可以直接继承已有的API就可以实现一个自定义的Task,具体内容可参见Android Gradle的基本概念梳理(二)这里仅简单的在总结一下有三种已有的API:

  • DefaultTask:org.gradle.api提供的Task的API。普通类,通常需要继承DefaultTask,就可以实现一个自定义的Task。通过为子类的方法上添加@TaskAction,就可以实现一个可执行的Task的执行方法;
  • IncrementalTask:com.android.build.gradle提供的Task的API。抽象类,需要继承该类,就可以实现一个增量Task;需要通过复写getIncremental()返回true来支持增量编译.该API已经废弃,建议使用NewIncrementalTask
  • NewIncrementalTask:com.android.build.gradle提供的Task的API。抽象类,需要继承该类,就可以实现一个增量Task;
  • Tranform:com.android.build.gradle提供的Task的API。抽象类,需要继承该类,就可以实现一个运行在class文件转换成dex之前,对class文件进行处理的Task;

        而实现 AddExportForEveryPackageManifestTask仅仅来继承DefaultTask即可,代码如下:

class AddExportForEveryPackageManifestTask extends DefaultTask {
    
    @TaskAction
    void run() {
        //处理所有包下的AndroidManifest文件添加android:exported
        SystemPrint.outPrintln("Running .....")
    }
}

        有了这个自定义的Task,先将Task加入到APP的打包编译的任务队列中,然后在逐步添加AddExportForEveryPackageManifestTask里面的逻辑代码。

(3)找到锚点processDebugMainManifest Task,添加AddExportForEveryPackageManifestTask到任务队列

        在找到processDebugMainManifest Task,自己走了一点弯路。因为没有搞清楚processDebugMainManifest和processDebugManifest区别,导致一开始找的是processDebugManifest这个,最后在运行最后的结果的时候,发现是还是会抛出前言中的编译异常,后来经过几次调整在最终确定依赖该Task,代码如下:

class ManifestProject implements Plugin<Project> {
    List variantNames = new ArrayList()

    @Override
    void apply(Project project) {
        SystemPrint.outPrintln("Welcome ManifestProject")
        getAllVariantManifestTask(project)
        addExportTaskForEveryPackageManifest(project)
    }
    /**
     * 获取所有的变体相关的process%sManifest任务名称
     * // processDebugManifest:生成最终 AndroidManifest 文件
     * @param project
     */
    void getAllVariantManifestTask(Project project) {
        project.extensions.findByType(AppExtension.class)
                .variantFilter {
                    variantNames.add(it.name)
                }
    }
    /**
     * 为所有依赖的包的AndroidManifest添加android:exported
     * @param project
     */
    void addExportTaskForEveryPackageManifest(Project project) {
        AddExportForEveryPackageManifestTask beforeAddTask = project.getTasks().create(AddExportForEveryPackageManifestTask.TAG,
                AddExportForEveryPackageManifestTask)
        //在项目配置完成后,添加自定义Task
        project.afterEvaluate {
            //直接通过task的名字找到ProcessApplicationManifest这个task
            variantNames.each {
                //找到processHuaweiDebugMainManifest,在这个之前添加export
                ProcessApplicationManifest processManifestTask = project.getTasks().getByName(String.format("process%sMainManifest", it.capitalize()))
                beforeAddTask.setManifestsFileCollection(processManifestTask.getManifests())
                beforeAddTask.setMainManifestFile(processManifestTask.getMainManifest().get())
                processManifestTask.dependsOn(beforeAddTask)
            }
        }
    }
}

        对上面的代码总结几个自己遇到的问题:

  • 问题1:项目中的变体存在

        因为项目中配置了变体(相关内容可参见Android Gradle中的productFlavors),所以所有的Task都会添加变体的信息,如processDebugMainManifest将会变为processHuaweiDebugMainManifest,其中HuaweiDebug为其中一个变体。那么在找processDebugMainManifest这个锚点的时候,就会变成了找到process${variant}MainManifest,而变体信息是在主module在apply该插件的时候,就会输出的信息,而我添加自定义的Task需要在项目配置完成添加。

        解决方案:在apply()的时候将所有的变体信息保存到variantNames集合中,然后在项目配置完成的时候,通过variantNames.each {}的方式找到每个变体的process${variant}MainManifest Task。

  • 问题2:processDebugMainManifest对应的实现类

        因为要从这个Task中得到所有依赖包以及主Module的AndroidMainfest.xml文件,所以就需要知道processDebugMainManifest这个Task对应的实现类才能知道哪些方法可以获取到这些信息。但是Android Gradle都是在摸索学习中,所以一下子无法得到processDebugMainManifest的实现类,然后也没有搜到相关的解决方案。

        解决方案:但是发现了一个找到这个processDebugMainManifest的便捷方法:随便让project.getTasks().getByName()等于任意类型的一个类,编译发布插件,然后通过Android Studio编译整个项目,发现就会抛出以下异常:

Cannot cast object 'task ':app:processHuaweiDebugMainManifest''
 with class 'com.android.build.gradle.tasks.ProcessApplicationManifest_Decorated' 
to class 'java.lang.Process'

        从提示中可以看到在该任务中将ProcessApplicationManifest类型转换成Process类型,然后就得到了processDebugMainManifest的实现类。

  • 问题3:  添加任务队列

        现在可以通过dependsOn将 AddExportForEveryPackageManifestTask添加到processDebugMainManifest之前,那如果要添加到一个已有的任务队列之后呢?

        解决方案:对比几个常用的设置Task执行顺序的方法,其中it为processDebugManifest这个Task,task为自定义的Task

        1)it.dependsOn(task):将自定义task添加到任务队列中。执行顺序为:先执行task,然后在去执行it这个task;

        2)it.finalizedBy(task):将自定义的task添加的任务队列中。执行顺序为:先执行it,在执行完it之后自动去执行task。

 而mustRunAftershouldRunAfter两个同样是可以定义两个task的执行的先后顺序,但并不会将该task添加到整个任务队列中。

        3)task1.mustRunAfter task2    task1.shouldRunAfter task2:执行顺序为:先执行task2,在执行task1。两者都不会将task添加到任务队列中,区别在于mustRunAfter为必须遵守该顺序,而shouldRunAfter为非必须。通常用于几个task同时依赖一个task的时候,设置这几个task的执行顺序。例如it.dependsOn(task1,task2),那么就可以使用mustRunAftershouldRunAfter 来设置task1和task2的执行顺序。

        经过上面的三步之后,然后将该插件编译发布,通过Android Studio就可以看到在执行processDebugMainManifest这个Task之前,已经执行先执行了AddExportForEveryPackageManifestTask,如下:

> Task :app:generateHuaweiDebugResources UP-TO-DATE
> Task :app:mergeHuaweiDebugResources UP-TO-DATE
> Task :app:createHuaweiDebugCompatibleScreenManifests UP-TO-DATE

> Task :app:AddExportForEveryPackageManifestTask
#@@#@@# ManifestProject #@@#@@#   Running .....
> Task :app:extractDeepLinksHuaweiDebug UP-TO-DATE
> Task :app:processHuaweiDebugMainManifest
> Task :app:processHuaweiDebugManifest
> Task :app:mergeHuaweiDebugNativeDebugMetadata NO-SOURCE
> Task :app:mergeHuaweiDebugShaders

(4)编写AddExportForEveryPackageManifestTask逻辑

        已经有了这个插件的框架,现在就是填充AddExportForEveryPackageManifestTask逻辑了。

  • 1)传入所有的依赖包以及主module的AndroidManifest.xml文件;

        得到依赖包以及主module的AndroidManifest.xml文件,所以在Project中初始化AddExportForEveryPackageManifestTask的时候,从processDebugMainManifest取出来赋值到相应的方法,代码如下:

//获取所有依赖包的manifest文件
beforeAddTask.setManifestsFileCollection(processManifestTask.getManifests())
//获取主module的manifest文件
beforeAddTask.setMainManifestFile(processManifestTask.getMainManifest().get())

       小技巧: 由于对Android Gradle Plugin的源码并不是很清楚,但是看到方法名以及@InputFiles猜测这两个可能就是想要的方法,试了试果然可以。       

        在 AddExportForEveryPackageManifestTask中的代码就是两个set方法,代码如下:


    /**
     * 设置所有的 需要合并的Manifest文件
     * @param collection
     */
    void setManifestsFileCollection(FileCollection collection) {
        manifestCollection = collection
    }

    /**
     *
     * @param file
     */
    void setMainManifestFile(File file){
        mainManifestFile = file
    }
  •   2)找到该AndroidManifest.xml文件的Activity、Service、BroadcastReceiver组件;

        使用groovy提供的XmlParser来解析AndroidManifest.xml文件,具体的代码可参见AddExportForEveryPackageManifestTask.groovy文件中的handlerVariantManifestFile()方法里面的内容,这里仅针对语法上面总结几点:

        点1:通过XmlParser解析的xml获得的文件内容是一个Node

        代码如下:

XmlParser xmlParser = new XmlParser()
def node = xmlParser.parse(manifestFile)

        一个Node与AndroidManifest.xml文件的对比关系如下:

        

        点2:可以通过node.attributes()来获取当前节点的所有属性,当然也可以通过node.attributes().get("package")来获取特定的属性值;

        其中<>除去标签名之前的xxx=xxx为该节点的属性 。

        点3:可以通过node.children()来获取到当前节点的所有子节点,当然也可以通过node.子节点名字的方式来获取特定的子节点;

        其中每个<></>来表示一个节点。

        点4:在each{}中不可以调用private声明的变量或方法;

        点5:在使用each{}循环的时候,return true相当于continue;在使用find{}循环的时候,return true相当于break;

  • 3)判断该组件是否满足“含有intent-filter && 没有添加android:exported”;

                具体的代码可参见AddExportForEveryPackageManifestTask.groovy文件中的见hasAttributeExported()和hasIntentFilter()

  • 4)添加android:exported=true(因为考虑只为第三方的SDK中添加,所以设置为true比较保险。本APP的组件需要根据自身APP的特点由开发人员设置为true或false);

        具体的代码可参见AddExportForEveryPackageManifestTask.groovy文件中的handlerAddExportForNode(),这里仅总结自己遇到的几个坑:

        点1:可通过node.attributes().put()为该node添加新的属性

        因为有android:exported=true属性的该node在输出的时候,如下:

activity[attributes={{http://schemas.android.com/apk/res/android}name=.TestActivity,
{http://schemas.android.com/apk/res/android}exported=true}; 
value=[intent-filter[attributes={}; 
value=[action[attributes={{http://schemas.android.com/apk/res/android}name=android.intent.action.SEARCH}; 
value=[]], category[attributes={{http://schemas.android.com/apk/res/android}name=android.intent.category.DEFAULT};
 value=[]]]]]] 

        在循环遍历node.attributes() 时,输出的key也为 {http://schemas.android.com/apk/res/android}exported,所以一开始在添加android:exported=true属性的时候使用的是:

node.attributes().put({http://schemas.android.com/apk/res/android}exported, true)

        在编译的时候抛出以下异常:

'元素类型 "activity" 必须后跟属性规范 ">" 或 "/>"。'

         后来尝试直接使用:

node.attributes().put("android:exported", true)

        没想到竟然成功了,同时生成的 AndroidManifest.xml文件也是正确的。遗留问题:暂时不知道原因是什么。 

        点2: 可通过node.attributes().get()来修改属性值

        像不带android:的其他属性例如package等信息,可以直接通过node.attributes().get("package")获取到对应的属性值,但是对于带有android:的,例如android:name,不管通过node.attributes().get("android:name")还是node.attributes().get("{http://schemas.android.com/apk/res/android}name")都获取不到属性值,但是可以通过下面的代码获取到:

attrs.find{
    if("http://schemas.android.com/apk/res/android}name".equals(it.key.toString())) {
        String name = attrs.get(it.key)
        //find return true相当于break
        return true
    }
}

       猜测:这个可能跟Map的containsKey()逻辑有关系,因为传入的"{http://schemas.android.com/apk/res/android}name"已经不是之前加入到该Map集合中的key,所以就不会匹配到value值。

  • 5)将处理好的内容重新写入AndroidManifest.xml文件;

        具体逻辑还是见AddExportForEveryPackageManifestTask.groovy文件中的handlerVariantManifestFile(),这里仅简单罗列,代码如下:

 //第四步:保存到原AndroidManifest文件中
 String result = XmlUtil.serialize(node)
 manifestFile.write(result, "utf-8")

        仍然将文件保存到之前传入的AndroidManifest.xml文件中,像前面提到的'元素类型 "activity" 必须后跟属性规范 ">" 或 "/>"。'异常也是在 XmlUtil.serialize()中抛出来的。        具体的代码已经上传到至GitHub - wenjing-bonnie/AndroidPlugin: 用来学习Android Gradle Plugin。因为逐渐在这基础上进行迭代,可回退到Gradle_10.0.1该tag下可以查看相关内容,也可直接查看manifestplugin目录下的相关内容(在实例三中有对该部分代码做优化,所以如果不回退的话,会看到一些定义和这里描述的有出入,但是大的逻辑是没有动的)。

        将插件发布,将主module的compileSdkVersiontargetSdkVersion改为31,添加一个没有适配Android12的第三方插件,如Mobpush(具体可参见官方文档:MobTech集成文档-MobTech),未添加该插件,运行项目回抛出编译异常:

> Task :app:processHuaweiDebugMainManifest FAILED
/Users/j1/Documents/android/code/studio/AndroidPlugin/app/src/main/AndroidManifest.xml Error:
	Apps targeting Android 12 and higher are required to specify an explicit value for `android:exported` when the corresponding component has an intent filter defined. See https://developer.android.com/guide/topics/manifest/activity-element#exported for details.
/Users/j1/Documents/android/code/studio/AndroidPlugin/app/src/main/AndroidManifest.xml Error:
	Apps targeting Android 12 and higher are required to specify an explicit value for `android:exported` when the corresponding component has an intent filter defined. See https://developer.android.com/guide/topics/manifest/activity-element#exported for details.
/Users/j1/Documents/android/code/studio/AndroidPlugin/app/src/main/AndroidManifest.xml Error:
	Apps targeting Android 12 and higher are required to specify an explicit value for `android:exported` when the corresponding component has an intent filter defined. See https://developer.android.com/guide/topics/manifest/activity-element#exported for details.
/Users/j1/Documents/android/code/studio/AndroidPlugin/app/src/main/AndroidManifest.xml Error:
	Apps targeting Android 12 and higher are required to specify an explicit value for `android:exported` when the corresponding component has an intent filter defined. See https://developer.android.com/guide/topics/manifest/activity-element#exported for details.

See http://g.co/androidstudio/manifest-merger for more information about the manifest merger.

        添加该插件之后,通过Android Studio可以成功运行安装APP,并且使用Build->Analyzer查看生成的apk的AndroidManifest.xml文件发现已经添加了android:exported=true的属性,如图:

3.小结

        在实现该自定义Android Gradle Plugin的过程,其实第一个关键点就是要找到合适的锚点来添加自定义的Task行为;第二个关键点就是自定义Task行为的实现。通过这个过程对自定义Android Gradle Plugin越发感兴趣。

三 实例2:读取配置文件的versionCode和versionName

        前一个是对合并所有的AndroidManifest.xml文件之前,对项目中所有用到的依赖包的AndroidManifest.xml文件进行修改,然后在看一个对最后合并之后的AndroidManifest.xml文件进行修改内容的实例。

        实例背景:在项目目录下会有一个version.xml文件,用来记录每次上线的内容以及最新一次的版本等信息,如下:

<?xml version="1.0" encoding="utf-8" ?>
<versions>
    <version latest="true">
        <versionDescription>新增购物车</versionDescription>
        <versionCode>12</versionCode>
        <versionName>2.0.0</versionName>
        <date>2021/09/16</date>
    </version>
    <version>
        <versionDescription>APP第一版本上线</versionDescription>
        <versionCode>12</versionCode>
        <versionName>1.0.0</versionName>
        <date>2021/09/15</date>
    </version>
</versions>

        这样就可以不需要每次手动修改AndroidManifest.xml ,可以清楚的记录每次升级的内容,比在AndroidManifest.xml文件或者build.gradle文件中进行记录更清晰明了。    

1.思路梳理

        针对上述的的实例,整理下代码逻辑:

  • (1)自定义SetLatestVersionForMergedManifestTask
    • 1)传入最后合并好的AndroidManifest.xml,要考虑到有变体情况
    • 2)传入含有版本信息的version.xml,通过扩展属性传入
    • 3)读取version.xml中的版本信息
    • 4)更新到传入的AndroidManifest.xml文件中
  • (2)找到生成最后AndroidManifest.xml的task(即processDebugManifest),作为锚点,在后面添加SetLatestVersionForMergedManifestTask
    • 1)在前一个实例的代码基础上,在Project中找到processDebugManifest
    • 2)在执行processDebugManifest之后,添加SetLatestVersionForMergedManifestTask
  • (3)发布使用插件        

2.实例代码

        有了上面的思路,开始逐步实现代码。因为在二 实例1:适配Android12的android:exported属性已经有了该插件的框架,现在只需要自定义SetLatestVersionForMergedManifestTask和将该SetLatestVersionForMergedManifestTask加入到任务队列即可。

(1)自定义SetLatestVersionForMergedManifestTask

        同样的方式继承DefaultTask,通过@TaskAction添加该任务的action,代码如下:

class SetLatestVersionForMergedManifestTask extends DefaultTask {
   @TaskAction
    void doTaskAction() {
    }
}

(2)找到processDebugManifest ,将SetLatestVersionForMergedManifestTask添加到任务队列中

        processDebugManifest 该任务是根据之前的合并所有依赖包的AndroidManifest.xml的任务processDebugMainManifest得到的合并后的AndroidManifest.xml来得到不同变体的AndroidManifest.xml文件。所以在该任务之后,对AndroidManifest.xml的versionCode和versionName进行修改,才是最终被打包到apk中。

        因为在项目构建完成之后,会将所有变体相关的Task全都塞到project.getTasks()集合中,而在Android Studio在通过菜单栏的Build或者Run编译打包apk,每次只执行其中一个变体的任务集合,也就是说我们在通过Build->Make Project时候,仅仅执行的是在变体集合中活跃的那个变体,如图:

        

        详细的内容见Android Gradle中的productFlavors 

        因为设置了productFlavors{},所以processDebugManifest就会变成了processHuaweiDebugManifest,那么在前面的二 实例1:适配Android12的android:exported属性中的时候为了找到这个任务,将项目中的所有变体集合中循环每一个变体,都来执行下自定义的Task,但是在实际过程中,其实完全没有必要这种。只需要找到这个活跃的变体,就可以直接找到该变体下的任务集合,然后在对应的集合中添加自定义任务即可。所以针对在(3)找到锚点processDebugMainManifest Task,添加AddExportForEveryPackageManifestTask到任务队列这种添加自定义任务的方式做了优化:

  • 1)找到当前活跃的变体

        在执行Build的时候,其中`project.gradle.getStartParameter().getTaskRequests()`返回的信息为:

        [DefaultTaskExecutionRequest{args=[:wjplugin:assemble, :wjplugin:testClasses, :manifestplugin:assemble, :manifestplugin:testClasses, :firstplugin:assemble, :firstplugin:testClasses, :app:assembleHuaweiDebug],projectPath='null'}]

        从该字符串中我们可以看到其中含有当前变体的信息 “HuaweiDebug”,我们只要截取出该字符串就可以得到当前变体的名字,代码如下:

void getVariantNameInBuild(Project project) {
        String parameter = project.gradle.getStartParameter().getTaskRequests().toString()
        //assemble(\\w+)(Release|Debug)仅提取Huawei
        String regex = parameter.contains("assemble") ? "assemble(\\\\w+)" : "generate(\\\\w+)"
        Pattern pattern = Pattern.compile(regex)
        Matcher matcher = pattern.matcher(parameter)
        if (matcher.find()) {
            //group(0)就是指的整个串,group(1) 指的是第一个括号里的东西,group(2)指的第二个括号里的东西
            variantName = matcher.group(1)
        }
    }

        但是执行sync的时候, project.gradle.getStartParameter().getTaskRequests()返回的信息为:                [DefaultTaskExecutionRequest{args=[],projectPath='null'}]

        显然已经无法获取到当前变体名称。那么其实思考一下这个sync,这个是一个同步的过程,我们既可以在sync的时候,对该插件不做任务处理,也可以借助project.extensions.findByType(AppExtension.class).variantFilter{}返回所有的变体信息中抽取任意一个来完成sync过程而已(PS:因为所有的变体相关的Task都会加入到project.getTasks()集合中,所以这里仅仅是为了让sync能够执行成功,在build的时候,又会重新从project.gradle.getStartParameter().getTaskRequests()中返回变体信息)。

        当然不仅仅限于sync,应该还有其他在执行project.gradle.getStartParameter().getTaskRequests()返回无变体信息的内容。

        那么就有了两种实现方案:

        方案一:从所有变体集合中,抽取任意一个变体信息

        在getVariantNameInBuild()中通过matcher匹配到对应的字符串之后,增加一个检验该字符串有效性的处理,如果是一个无效的字符串,则从所有的变体信息中,抽取一个变体的名字设置为当前变体的名称,代码如下:

 void getVariantNameInBuild(Project project) {
        String parameter = project.gradle.getStartParameter().getTaskRequests().toString()
        //assemble(\\w+)(Release|Debug)仅提取Huawei
        String regex = parameter.contains("assemble") ? "assemble(\\\\w+)" : "generate(\\\\w+)"
        Pattern pattern = Pattern.compile(regex)
        Matcher matcher = pattern.matcher(parameter)
        if (matcher.find()) {
            //group(0)就是指的整个串,group(1) 指的是第一个括号里的东西,group(2)指的第二个括号里的东西
            variantName = matcher.group(1)
        }
        //但是sync时返回的内容:[DefaultTaskExecutionRequest{args=[],projectPath='null'}].
        //所以此时走注释中的(2),实现"则直接但是最理想的解决方案是该在sync的时候,可以不执行该插件"这种方案,则直接隐藏下面的代码
        if (!isValidVariantName()) {
            //从AppExtension中获取所有变体,作为获取当前变体的备用方案
            getValidVariantNameFromAllVariant(project)
        }
    }

    /**
     * 获取所有的变体中的一个可用的变体名,仅仅用来保证sync任务可执行而已
     * project.extensions.findByType()有执行时机,所以会出现在getVariantNameInBuild()中直接调用getVariantNameFromAllVariant()将无法更新variantName
     *
     * @param project
     */
    void getValidVariantNameFromAllVariant(Project project) {
        if (isValidVariantName()) {
            return
        }
        //但是sync时返回的内容:[DefaultTaskExecutionRequest{args=[],projectPath='null'}],其实该过程可以不执行该插件也可以
        //直接从所有的变体中取一个可用的变体名,返回
        //
        project.extensions.findByType(AppExtension.class).variantFilter {
            variantName = it.name.capitalize()
            SystemPrint.outPrintln(String.format("Fake variant name from all variant is \\" %s \\"", variantName))
            if (isValidVariantName()) {
                return true
            }
        }
    }

    boolean isValidVariantName() {
        variantName != null && variantName.length() > 0
    }

        该方案仅仅为了sync能够执行成功而已,没有具体实际意义。 

        方案二:sync的时候,该插件不做任务处理

        即在apply()的时候,做一个检验是不是一个有效的variantName,如果不是一个有效的字符串,那么就直接不在执行添加自定义Task的代码,代码如下:


    @Override
    void apply(Project project) {
        //创建ManifestExtension
        createManifestExtension(project)
        //在sync中无法获取到variantName
        getVariantNameInBuild(project)
        SystemPrint.outPrintln(String.format("Welcome %s ManifestProject", variantName))
        //如果不是一个有效的variant,则直接返回
        if (!isValidVariantName()) {
            return
        }
        addTaskForVariantAfterEvaluate(project)
    }

        另外在获取 variantName的方法中去掉从所有的变体集合中找一个可用的变体名,代码如下:

void getVariantNameInBuild(Project project) {
        String parameter = project.gradle.getStartParameter().getTaskRequests().toString()
        //assemble(\\w+)(Release|Debug)仅提取Huawei
        String regex = parameter.contains("assemble") ? "assemble(\\\\w+)" : "generate(\\\\w+)"
        Pattern pattern = Pattern.compile(regex)
        Matcher matcher = pattern.matcher(parameter)
        if (matcher.find()) {
            //group(0)就是指的整个串,group(1) 指的是第一个括号里的东西,group(2)指的第二个括号里的东西
            variantName = matcher.group(1)
        }
    }

        该方案应该更符合实际意义,本来在sync的时候,该插件完全可以不作任何处理。

  • 2) 找到processDebugManifest,添加自定义SetLatestVersionForMergedManifestTask

        同样也是在项目配置完成之后,将SetLatestVersionForMergedManifestTask添加到processDebugManifest后执行,代码如下:


    void addVersionTaskForMergedManifest(Project project, SetLatestVersionForMergedManifestTask versionTask) {
        //在项目配置完成后,添加自定义Task
        //方案一:直接通过task的名字找到ProcessMultiApkApplicationManifest这个task
        //直接找到ProcessDebugManifest,然后在执行后之后执行该Task
        ProcessMultiApkApplicationManifest processManifestTask = project.getTasks().getByName(String.format("process%sManifest", variantName))
        versionTask.setManifestFile(processManifestTask.getMainMergedManifest().asFile.get())
        processManifestTask.finalizedBy(versionTask)
    }

         这样就完成了将SetLatestVersionForMergedManifestTask添加到整个APP的编译打包的任务队列中。

(3)编写SetLatestVersionForMergedManifestTask逻辑

  •    1)传入最后为每个变体创建的的AndroidManifest.xml文件

        在前面添加SetLatestVersionForMergedManifestTask任务的时候已经将processDebugManifest中的变体的AndroidManifest.xml文件通过setManifestFile()方法传入到该自定义Task中

  • 2)通过扩展属性来传入版本信息的version.xml

        该实现方法就是要自定义扩展属性类,然后添加到Project中。

        自定义扩展属性类,代码如下:

class ManifestExtension {
    protected static final String TAG = "ManifestPlugin"
    private File versionFile

    protected void setVersionFile(File file) {
        this.versionFile = file
    }

    protected File getVersionFile() {
        return versionFile
    }

}

        在Project中添加该扩展属性,代码如下:

    @Override
    void apply(Project project) {
        //创建ManifestExtension
        createManifestExtension(project)
        .....    
    }
    
    /**
     * 配置扩展属性
     * @param project
     */
    void createManifestExtension(Project project) {
        project.getExtensions().create(ManifestExtension.TAG, ManifestExtension)
    }

         在主module中的build.gradle中使用该扩展属性,代码如下:

/**'com.wj.plugin.manifest'*/
ManifestPlugin {
    versionFile = file("version.xml")
}
  • 3)读取version.xml信息和更新最终的AndroidMainfest.xml

        这部分内容就是通过XmlParse来读写xml文件,不在多余写代码。

        具体的代码已经上传到至GitHub - wenjing-bonnie/AndroidPlugin: 用来学习Android Gradle Plugin。因为逐渐在这基础上进行迭代,可回退到Gradle_10.0.2该tag下可以查看相关内容,也可直接查看manifestplugin目录下的相关内容。

3.小结

        相比较于实例一,优化了找锚点的方法,通过当前活跃的变体名,找到对应的锚点任务。

四 总结

        经过两个实例,对Android Gradle 自定义插件的有了更深的理解

  • 1.在自定义Gradle插件的几个要点:
    • (1)找到合适的锚点任务;
    • (2)将自定义Task通过dependsOn或finalizedBy添加到锚点任务的之前或之后执行,并且还是必须要通过这两个方法添加到任务队列中;
    • (3)通过扩展属性添加输入参数;
  • 2.几种自定义Task的方式
    • (1)继承DefaultTask,可以将该自定义的Task添加到已有的任务队列
    • (2)继承Transform,会自动添加到任务队列中,并且添加到.class文件被打包成.dex文件之前,用于进行字节码
  • 3.processDebugMainManifest会将所有的依赖包的AndroidManifest.xml文件合并成一个AndroidManifest.xml文件,可以通过该processDebugMainManifest作为锚点解决第三方SDK中未适配Android 12 抛出的“...value for `android:exported`..”编译错误;当然也可以解决去除第三方SDK某些敏感权限的问题;
  • 4.processDebugManifest会为所有的变体生成最终的AndroidManifest.xml文件文件,可以通过processDebugManifest作为锚点来解决修改最终AndroidManifest.xml文件的问题;
  • 5.当前Android Gradle中的其他的Task可以根据实际的功能来解决一些对应的文件处理
  • 6.project.getTasks()返回的是所有变体的Task的集合,而在实际Android Studio编译打包过程中,只有一个活跃的变体,所以可以获取当前活跃变体的信息来找到对应变体的Task;
  • 7.在找Android Gradle的task的实现类的时候,可以通过随便将project.getTasks().getByName()返回值返回给一个类型的变量,那么最后在编译的时候,抛出的异常中就会提示对应的类型;(这个是这次最大的收获!!!我都佩服我自己能发现这么一个便捷的方式。
  • 8.XmlParse可以用来读写xml文件,可以通过node.一级标签名.二级标签名的方式获取到对应的节点

        遗留问题:1.怎么通过NewIncrementalTask 来实现一个增量Task;2.怎么使用@InputFile等注解来添加输入参数    

       加油!好玩          

以上是关于Android Gradle 中的实例之动态修改AndroidManifest文件的主要内容,如果未能解决你的问题,请参考以下文章

Android Gradle 中的实例之动态修改AndroidManifest文件

Android Gradle 中的实例之动态修改AndroidManifest文件

Android Gradle动态打32位或者64位的包

Android Gradle 中的字节码插桩之ASM

Android编译gradle 动态修改版本号

Android编译时动态替换Jar包中的类