美文网首页
Android 12 自动适配 exported 深入解析避坑

Android 12 自动适配 exported 深入解析避坑

作者: 恋猫月亮 | 来源:发表于2022-03-12 09:45 被阅读0次

    众所周知,从 Android 12 开始,使用了 TargetSDK 31 之后,四大组件如果使用了 intent-filter, 但是没显性质配置 exported App 将会无法安装,甚至编译不通过。

    比如启动的 Activity 就需要设置 exportedtrue ,至于其他组件是否设置为 true 则看它是否需要被其它应用调用。

    然而这个事情的状态是这样的:

    • 如果出现问题的 AndroidManifest 文件是你本地的,那手动修改即可;
    • 但如果出现问题的是第三方远程依赖,并且对方并没有提供源码和更新,你就无法直接修改;
    • 如果第三方依赖太多,查找哪些出了问题十分费时费力。

    脚本

    所以在之前的 《Android 12 快速适配要点》 一文中提供了一套脚本,专门用于适配 Android 12 下缺少 android:exported 无法编译或者安装的问题,但是在这期间收到了不少问题反馈:

    com.android.tools.build:gradle:4.0.0 以及其下版本

    一下脚本经过测试最高可到支持的版本: gradle:4.0.0 & gradle-6.1.1-all.zip

    /**
     * 修改 Android 12 因为 exported 的构建问题
     */
    android.applicationVariants.all { variant ->
        variant.outputs.all { output ->
            output.processResources.doFirst { pm ->
                String manifestPath = output.processResources.manifestFile
                def manifestFile = new File(manifestPath)
                def xml = new XmlParser(false, true).parse(manifestFile)
                def exportedTag = "android:exported"
                ///指定 space
                def androidSpace = new groovy.xml.Namespace('http://schemas.android.com/apk/res/android', 'android')
    
                def nodes = xml.application[0].'*'.findAll {
                    //挑选要修改的节点,没有指定的 exported 的才需要增加
                    (it.name() == 'activity' || it.name() == 'receiver' || it.name() == 'service') && it.attribute(androidSpace.exported) == null
    
                }
                ///添加 exported,默认 false
                nodes.each {
                    def isMain = false
                    it.each {
                        if (it.name() == "intent-filter") {
                            it.each {
                                if (it.name() == "action") {
                                    if (it.attributes().get(androidSpace.name) == "android.intent.action.MAIN") {
                                        isMain = true
                                        println("......................MAIN FOUND......................")
                                    }
                                }
                            }
                        }
                    }
                    it.attributes().put(exportedTag, "${isMain}")
                }
    
                PrintWriter pw = new PrintWriter(manifestFile)
                pw.write(groovy.xml.XmlUtil.serialize(xml))
                pw.close()
            }
        }
    
    }
    

    com.android.tools.build:gradle:4.0.0 以上版本

    以下脚本经过测试支持的版本: gradle:4.1.0 & gradle-6.5.1-all.zip

    /**
     * 修改 Android 12 因为 exported 的构建问题
     */
    
    android.applicationVariants.all { variant ->
        variant.outputs.each { output ->
            def processManifest = output.getProcessManifestProvider().get()
            processManifest.doLast { task ->
                def outputDir = task.multiApkManifestOutputDirectory
                File outputDirectory
                if (outputDir instanceof File) {
                    outputDirectory = outputDir
                } else {
                    outputDirectory = outputDir.get().asFile
                }
                File manifestOutFile = file("$outputDirectory/AndroidManifest.xml")
                println("----------- ${manifestOutFile} ----------- ")
    
                if (manifestOutFile.exists() && manifestOutFile.canRead() && manifestOutFile.canWrite()) {
                    def manifestFile = manifestOutFile
                    ///这里第二个参数是 false ,所以 namespace 是展开的,所以下面不能用 androidSpace,而是用 nameTag
                    def xml = new XmlParser(false, false).parse(manifestFile)
                    def exportedTag = "android:exported"
                    def nameTag = "android:name"
                    ///指定 space
                    //def androidSpace = new groovy.xml.Namespace('http://schemas.android.com/apk/res/android', 'android')
    
                    def nodes = xml.application[0].'*'.findAll {
                        //挑选要修改的节点,没有指定的 exported 的才需要增加
                        //如果 exportedTag 拿不到可以尝试 it.attribute(androidSpace.exported)
                        (it.name() == 'activity' || it.name() == 'receiver' || it.name() == 'service') && it.attribute(exportedTag) == null
    
                    }
                    ///添加 exported,默认 false
                    nodes.each {
                        def isMain = false
                        it.each {
                            if (it.name() == "intent-filter") {
                                it.each {
                                    if (it.name() == "action") {
                                        //如果 nameTag 拿不到可以尝试 it.attribute(androidSpace.name)
                                        if (it.attributes().get(nameTag) == "android.intent.action.MAIN") {
                                            isMain = true
                                            println("......................MAIN FOUND......................")
                                        }
                                    }
                                }
                            }
                        }
                        it.attributes().put(exportedTag, "${isMain}")
                    }
    
                    PrintWriter pw = new PrintWriter(manifestFile)
                    pw.write(groovy.xml.XmlUtil.serialize(xml))
                    pw.close()
    
                }
    
            }
        }
    }
    

    这段脚本你可以直接放到 app/build.gradle 下执行,也可以单独放到一个 gradle 文件之后 apply 引入,它的作用就是:

    在打包过程中检索所有没有设置 exported 的组件,给他们动态配置上 exported,这里有个特殊需要注意的是,因为启动 Activity 默认就是需要被 Launcher 打开的,所以 "android.intent.action.MAIN" 需要 exported 设置为 true 。(PS:更正规应该是用 LAUNCHER 类别,这里故意用 MAIN

    而后综合问题,具体反馈的问题有 :

    • label直接写死中文,不是引用 @string 导致的在 3.x 的版本可以正常运行,但不能打包 ;

    • XmlParser 类找不到,这个首先确定 AGP 版本和 Gradle 版本是否匹配,具体可见 gradle-plugin,另外可以通过 groovy.util.XmlParser 或者 groovy.xml.XmlParser 全路径指定使用 ,如果是 gradle 文件里显示红色并不会影响运行;

    • 运行报错提示 android:exported needs,这个就是今天需要输入聊的

    Error: android:exported needs to be explicitly specified for <xxxx>. 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.
    

    基于上述脚本测试和反馈,目前的结论是:

    gradle:4.2.0 & gradle-6.7.1-all.zip 开始,TargetSDK 31 下脚本会有异常,因为在 processDebugMainManifest (带有Main) 的阶段,会直接扫描依赖库的 AndroidManifest.xml 然后抛出直接报错,从而进不去 processDebugManifest 任务阶段就编译停止,所以实际上脚本并没有成功运行

    所以此时拿不到 mergerd_manifest 下的文件,因为 mergerd_manifestAndroidManifest.xml 也还没创建成功,没办法进入 task ,也就是该脚本目前只能针对 gradle:4.1.0 以及其下版本安装 apk 到 Android12 的机器上, 有 intent-filter 但没有 exoprted 的适配问题,基于这个问题,不知道各位是否有什么好的建议?

    新脚本

    而目前基于这个问题,这里提供了如下脚本,在 gradle:4.2.0 & gradle-6.7.1-all.zip 以及 7.0 的版本上,该脚本的作用是在运行时自动帮你打印出现问题的 aar 包依赖路径和组建名称

    android.applicationVariants.all { variant ->
        variant.outputs.each { output ->
            //println("=============== ${variant.getBuildType().name.toUpperCase()} ===============")
            //println("=============== ${variant.getFlavorName()} ===============")
            def vn
            if (variant.getFlavorName() != null && variant.getFlavorName() != "") {
                vn = variant.name;
            } else {
                if (variant.getBuildType().name == "release") {
                    vn = "Release"
                } else {
                    vn = "Debug"
                }
            }
            def taskName = "process${vn}MainManifest";
            try {
                println("=============== taskName ${taskName} ===============")
                project.getTasks().getByName(taskName)
            } catch (Exception e) {
                return
            }
            ///你的自定义名字
            project.getTasks().getByName(taskName).doFirst {
                //def method = it.getClass().getMethods()
                it.getManifests().getFiles().each {
                    if (it.exists() && it.canRead()) {
                        def manifestFile = it
                        def exportedTag = "android:exported"
                        def nameTag = "android:name"
                        ///这里第二个参数是 false ,所以 namespace 是展开的,所以下面不能用 androidSpace,而是用 nameTag
                        def xml = new XmlParser(false, false).parse(manifestFile)
                        if (xml.application != null && xml.application.size() > 0) {
                            def nodes = xml.application[0].'*'.findAll {
                                //挑选要修改的节点,没有指定的 exported 的才需要增加
                                //如果 exportedTag 拿不到可以尝试 it.attribute(androidSpace.exported)
                                (it.name() == 'activity' || it.name() == 'receiver' || it.name() == 'service') && it.attribute(exportedTag) == null
    
                            }
                            if (nodes.application != null && nodes.application.size() > 0) {
                                nodes.each {
                                    def t = it
                                    it.each {
                                        if (it.name() == "intent-filter") {
                                            println("$manifestFile \n .....................${t.attributes().get(nameTag)}......................")
                                        }
                                    }
                                }
                            }
                        }
    
    
                    }
                }
            }
        }
    }
    

    如下图所示,因为目前官方如红色信息内容其实指向并不正确,容易误导问题方向,所以通过上述脚本打印,可以快速查找到问题所在的点,然后通过 tool:replace 临时解决

    image

    具体为什么之前的脚本在高版本 AGP 下无法使用,原因在于新版本在 processDebugMainManifest ,或者说 processXXXXXXMainManifest 的处理逻辑发生了变化,通过找到 processDebugMainManifest 的实现类,可以看到问题出现就是在于 Merging library manifest

    processDebugMainManifest 的实现在 ProcessApplicationManifest 里,对应路径是 ProcessApplicationManifest -> MainfestHelper mergeManifestsForApplication -> MainfestMerger2

    错误是在 Merging library manifest 的阶段出现异常,但是这个阶段的 task 里对于第三方依赖路径的输入,主要是从 private fun computeFullProviderList 方法开始,所以输入到 mergeManifestsForApplication 里的第三方路径是通过这个私有方法生成。

    image

    感觉唯一可以考虑操作的就是内部的 manifests 对象去变换路径,但是它是 private ,并且内部并不能很好复写其内容。

    image

    另外因为 aar 文件里的 AndroidManifset 是 readOnly ,所以如果真的要修改,感觉只能在输入之前读取到对应 AndroidManifset, 并生成临时文件,在 manifests 对象中更改其路径来完成,不知道大家有没有什么比较好的思路 。

    如果有好的解决办法,后续再更新。

    最后

    最后再说一个坑 ,如果你是低版本 Gradle 可以打包成功,但是运行到 Android12 机器的时候,可能会因为没有 exported 遇到安装失败的问题:

    1、如果是模拟器 12,你可能会看到如下所示的错误提示 ,提示上显示还是很直观的, 直接告诉你是 android:exported 的问题:

    * What went wrong:
    Execution failed for task ':app:installDebug'.
    > java.util.concurrent.ExecutionException: com.android.builder.testing.api.DeviceException: com.android.ddmlib.InstallException: INSTALL_PARSE_FAILED_MANIFEST_MALFORMED: Failed parse during installPackageLI: /data/app/vmdl487461761.tmp/base.apk (at Binary XML file line #358): xxxxx.Activity: Targeting S+ (version 31 and above) requires that an explicit value for android:exported be defined when intent filters are present
    

    2、如果你是真机 12,那可能就是这样的提示,提示然是 INSTALL_FAILED_USER_RESTRICTED不得不说小米系统这个安装失败很具误导性,比如 minSDK 太高导致无法安装,在小米上也会是 INSTALL_FAILED_USER_RESTRICTED

    image

    基本上内容就这些,具体如何进一步优化还待后续测试, 所以针对脚本实现,你还有什么问题或者想法,欢迎评论交流 ~

    相关文章

      网友评论

          本文标题:Android 12 自动适配 exported 深入解析避坑

          本文链接:https://www.haomeiwen.com/subject/fvekdrtx.html