当前位置:   article > 正文

Android 12 自动适配 exported 深入解析避坑 | 开发者说·DTalk

android 12 com.android.tools.build:gradle

0a3ae19a16ed40c3e0cd474f024ccc15.png

本文原作者: 恋猫de小郭,原文布于: GSYTech 

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

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

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

  • 如果出现问题的 AndroidManifest 文件是您本地的,那手动修改即可;

  • 但如果出现问题的是第三方远程依赖,并且对方并没有提供源码和更新,您就无法直接修改;

  • 如果第三方依赖太多,查找哪些出了问题十分费时费力。

9ddc7bfa9d756870a92ca7f4ecf171fd.png

脚本

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

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

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

  1. /**
  2. * 修改 Android 12 因为 exported 的构建问题
  3. */
  4. android.applicationVariants.all { variant ->
  5. variant.outputs.all { output ->
  6. output.processResources.doFirst { pm ->
  7. String manifestPath = output.processResources.manifestFile
  8. def manifestFile = new File(manifestPath)
  9. def xml = new XmlParser(false, true).parse(manifestFile)
  10. def exportedTag = "android:exported"
  11. ///指定 space
  12. def androidSpace = new groovy.xml.Namespace('http://schemas.android.com/apk/res/android', 'android')
  13. def nodes = xml.application[0].'*'.findAll {
  14. //挑选要修改的节点,没有指定的 exported 的才需要增加
  15. (it.name() == 'activity' || it.name() == 'receiver' || it.name() == 'service') && it.attribute(androidSpace.exported) == null
  16. }
  17. ///添加 exported,默认 false
  18. nodes.each {
  19. def isMain = false
  20. it.each {
  21. if (it.name() == "intent-filter") {
  22. it.each {
  23. if (it.name() == "action") {
  24. if (it.attributes().get(androidSpace.name) == "android.intent.action.MAIN") {
  25. isMain = true
  26. println("......................MAIN FOUND......................")
  27. }
  28. }
  29. }
  30. }
  31. }
  32. it.attributes().put(exportedTag, "${isMain}")
  33. }
  34. PrintWriter pw = new PrintWriter(manifestFile)
  35. pw.write(groovy.xml.XmlUtil.serialize(xml))
  36. pw.close()
  37. }
  38. }
  39. }

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

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

  1. /**
  2. * 修改 Android 12 因为 exported 的构建问题
  3. */
  4. android.applicationVariants.all { variant ->
  5. variant.outputs.each { output ->
  6. def processManifest = output.getProcessManifestProvider().get()
  7. processManifest.doLast { task ->
  8. def outputDir = task.multiApkManifestOutputDirectory
  9. File outputDirectory
  10. if (outputDir instanceof File) {
  11. outputDirectory = outputDir
  12. } else {
  13. outputDirectory = outputDir.get().asFile
  14. }
  15. File manifestOutFile = file("$outputDirectory/AndroidManifest.xml")
  16. println("----------- ${manifestOutFile} ----------- ")
  17. if (manifestOutFile.exists() && manifestOutFile.canRead() && manifestOutFile.canWrite()) {
  18. def manifestFile = manifestOutFile
  19. ///这里第二个参数是 false ,所以 namespace 是展开的,所以下面不能用 androidSpace,而是用 nameTag
  20. def xml = new XmlParser(false, false).parse(manifestFile)
  21. def exportedTag = "android:exported"
  22. def nameTag = "android:name"
  23. ///指定 space
  24. //def androidSpace = new groovy.xml.Namespace('http://schemas.android.com/apk/res/android', 'android')
  25. def nodes = xml.application[0].'*'.findAll {
  26. //挑选要修改的节点,没有指定的 exported 的才需要增加
  27. //如果 exportedTag 拿不到可以尝试 it.attribute(androidSpace.exported)
  28. (it.name() == 'activity' || it.name() == 'receiver' || it.name() == 'service') && it.attribute(exportedTag) == null
  29. }
  30. ///添加 exported,默认 false
  31. nodes.each {
  32. def isMain = false
  33. it.each {
  34. if (it.name() == "intent-filter") {
  35. it.each {
  36. if (it.name() == "action") {
  37. //如果 nameTag 拿不到可以尝试 it.attribute(androidSpace.name)
  38. if (it.attributes().get(nameTag) == "android.intent.action.MAIN") {
  39. isMain = true
  40. println("......................MAIN FOUND......................")
  41. }
  42. }
  43. }
  44. }
  45. }
  46. it.attributes().put(exportedTag, "${isMain}")
  47. }
  48. PrintWriter pw = new PrintWriter(manifestFile)
  49. pw.write(groovy.xml.XmlUtil.serialize(xml))
  50. pw.close()
  51. }
  52. }
  53. }
  54. }

这段脚本您可以直接放到 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 文件里显示红色并不会影响运行;

  • gradle-plugin

    https://developer.android.google.cn/studio/releases/gradle-plugin

  • 运行报错提示 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 到 Android 12 的机器上,有 intent-filter 但没有 exoprted 的适配问题,基于这个问题,不知道各位是否有什么好的建议?

3cc264278367a9acaa557676c5faaf2a.png

新脚本

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

  1. android.applicationVariants.all { variant ->
  2. variant.outputs.each { output ->
  3. //println("=============== ${variant.getBuildType().name.toUpperCase()} ===============")
  4. //println("=============== ${variant.getFlavorName()} ===============")
  5. def vn
  6. if (variant.getFlavorName() != null && variant.getFlavorName() != "") {
  7. vn = variant.name;
  8. } else {
  9. if (variant.getBuildType().name == "release") {
  10. vn = "Release"
  11. } else {
  12. vn = "Debug"
  13. }
  14. }
  15. def taskName = "process${vn}MainManifest";
  16. try {
  17. println("=============== taskName ${taskName} ===============")
  18. project.getTasks().getByName(taskName)
  19. } catch (Exception e) {
  20. return
  21. }
  22. ///您的自定义名字
  23. project.getTasks().getByName(taskName).doFirst {
  24. //def method = it.getClass().getMethods()
  25. it.getManifests().getFiles().each {
  26. if (it.exists() && it.canRead()) {
  27. def manifestFile = it
  28. def exportedTag = "android:exported"
  29. def nameTag = "android:name"
  30. ///这里第二个参数是 false ,所以 namespace 是展开的,所以下面不能用 androidSpace,而是用 nameTag
  31. def xml = new XmlParser(false, false).parse(manifestFile)
  32. if (xml.application != null && xml.application.size() > 0) {
  33. def nodes = xml.application[0].'*'.findAll {
  34. //挑选要修改的节点,没有指定的 exported 的才需要增加
  35. //如果 exportedTag 拿不到可以尝试 it.attribute(androidSpace.exported)
  36. (it.name() == 'activity' || it.name() == 'receiver' || it.name() == 'service') && it.attribute(exportedTag) == null
  37. }
  38. if (nodes.application != null && nodes.application.size() > 0) {
  39. nodes.each {
  40. def t = it
  41. it.each {
  42. if (it.name() == "intent-filter") {
  43. println("$manifestFile \n .....................${t.attributes().get(nameTag)}......................")
  44. }
  45. }
  46. }
  47. }
  48. }
  49. }
  50. }
  51. }
  52. }
  53. }

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

c8e6a6008c7d21129359e1583c0e99d7.png

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

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

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

65d12faaba5d005422e2aef1660f9608.png

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

3d95831f8c9e5bf5af53bf4b9919dcc4.png

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

1733b8b4305e63c0407b12aabbe72b07.png

最后

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

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

  1. * What went wrong:
  2. Execution failed for task ':app:installDebug'.
  3. > 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

8f58de0bb5a36e1408edf058aaafe1b5.png

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


长按右侧二维码

查看更多开发者精彩分享

e632ea0a505d7ecf91eaac13b2c5af9a.png

"开发者说·DTalk" 面向427583cb47d055cd031d07354acfa411.png中国开发者们征集 Google 移动应用 (apps & games) 相关的产品/技术内容。欢迎大家前来分享您对移动应用的行业洞察或见解、移动开发过程中的心得或新发现、以及应用出海的实战经验总结和相关产品的使用反馈等。我们由衷地希望可以给这些出众的中国开发者们提供更好展现自己、充分发挥自己特长的平台。我们将通过大家的技术内容着重选出优秀案例进行谷歌开发技术专家 (GDE) 的推荐。

094b93008fffeb4ca3c9ac19d96d3e70.gif 点击屏末 | 阅读原文 | 即刻报名参与 "开发者说·DTalk" 


15106cb526a34ba74acf4eedcd78024a.png

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/824121
推荐阅读
相关标签
  

闽ICP备14008679号