Android 12 自动适配 exported 深入解析避坑
众所周知,从 Android 12 开始,使用了 TargetSDK 31 之后,四大组件如果使用了 intent-filter
, 但是没显性质配置 exported
App 将会无法安装,甚至编译不通过。
比如启动的
Activity
就需要设置exported
为true
,至于其他组件是否设置为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_manifest
下 AndroidManifest.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
临时解决。
具体为什么之前的脚本在高版本 AGP 下无法使用,原因在于新版本在 processDebugMainManifest
,或者说 processXXXXXXMainManifest
的处理逻辑发生了变化,通过找到 processDebugMainManifest
的实现类,可以看到问题出现就是在于 Merging library manifest
。
processDebugMainManifest
的实现在 ProcessApplicationManifest 里,对应路径是 ProcessApplicationManifest -> MainfestHelper mergeManifestsForApplication -> MainfestMerger2
错误是在 Merging library manifest
的阶段出现异常,但是这个阶段的 task 里对于第三方依赖路径的输入,主要是从 private fun computeFullProviderList
方法开始,所以输入到 mergeManifestsForApplication
里的第三方路径是通过这个私有方法生成。
感觉唯一可以考虑操作的就是内部的 manifests
对象去变换路径,但是它是 private
,并且内部并不能很好复写其内容。
另外因为 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
:
基本上内容就这些,具体如何进一步优化还待后续测试, 所以针对脚本实现,你还有什么问题或者想法,欢迎评论交流 ~