Android 8.0 - Android 12 适配整理

1,124 阅读22分钟

本文整理记录每个版本的重要适配项以及业务中可能关心的行为变更。

Android 8.0、8.1(Api Level 26、27)

通知通道

从 Android 8.0(API 级别 26)开始,必须为所有通知分配渠道,否则通知将不会显示。通过将通知归类到不同的渠道中,用户可以停用您应用的特定通知渠道(而非停用您的所有通知),还可以控制每个渠道的视觉和听觉选项。
详情可以查看通知概览官方文档。

提醒窗口

使用 SYSTEM_ALERT_WINDOW 权限的应用无法再使用以下窗口类型来在其他应用和系统窗口上方显示提醒窗口:
TYPE_PHONE
TYPE_PRIORITY_PHONE
TYPE_SYSTEM_ALERT
TYPE_SYSTEM_OVERLAY
TYPE_SYSTEM_ERROR
相反,应用必须使用名为 TYPE_APPLICATION_OVERLAY 的新窗口类型。
使用 TYPE_APPLICATION_OVERLAY 窗口类型显示应用的提醒窗口时,请记住新窗口类型的以下特性:

  • 应用的提醒窗口始终显示在状态栏和输入法等关键系统窗口的下面。
    系统可以移动使用 TYPE_APPLICATION_OVERLAY 窗口类型的窗口或调整其大小,以改善屏幕显示效果。
  • 通过打开通知栏,用户可以访问设置来阻止应用显示使用 TYPE_APPLICATION_OVERLAY 窗口类型显示的提醒窗口。

后台执行限制

Android 8.0 为提高电池续航时间而引入的变更之一是,当您的应用进入已缓存状态时,如果没有活动的组件,系统将解除应用具有的所有唤醒锁。
后台执行限制具体表现为:

  1. 在后台运行的应用对后台服务的访问受到限制
  2. 应用无法使用其清单注册大部分隐式广播(即,并非专门针对此应用的广播)

默认情况下,这些限制仅适用于针对 O 的应用。不过,用户可以从 Settings 屏幕为任意应用启用这些限制,即使应用并不是以 O 为目标平台

详情可以查看后台执行限制官方文档。

解决方式有两种:

  • 如果 Service 容易引起用户注意,请将其设置为前台 Service。 例如,播放音频的 Service 始终应为前台 Service。 使用 startForegroundService() 方法创建 Service, 而非 startService()
  • 使用 JobScheduler 等方式实现后台 Service
/** Similar to startService(Intent), but with an implicit promise that the Service will call startForeground(int, android.app.Notification) once it begins running. The service is given an amount of time comparable to the ANR interval to do this, otherwise the system will automatically stop the service and declare the app ANR.
Unlike the ordinary startService(Intent), this method can be used at any time, regardless of whether the app hosting the service is in a foreground state.
Note: Beginning with SDK Version Build.VERSION_CODES.S, apps targeting SDK Version Build.VERSION_CODES.S or higher are not allowed to start foreground services from the background. See Behavior changes: Apps targeting Android 12 for more details.
*/

@Nullable
public abstract ComponentName startForegroundService(Intent service);

广播限制

适配 Android 8.0 或更高版本的应用无法继续在其清单中为隐式广播注册广播接收器。 隐式广播是一种不专门针对该应用的广播。 例如,ACTION_PACKAGE_REPLACED 就是一种隐式广播,因为该广播将被发送给所有已注册侦听器,让后者知道设备上的某些软件包已被替换。 不过,ACTION_MY_PACKAGE_REPLACED 不是隐式广播,因为不管已为该广播注册侦听器的其他应用有多少,它都会只被发送给软件包已被替换的应用。

应用内安装 apk 需要申请权限

需要在清单文件中添加权限,并在代码内判断是否已经授权
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>

其它

  • Only fullscreen opaque activities can request orientation 8.0 系统上的一个缺陷,8.1 及以后版本修复了此问题,在 8.0 中非全屏的透明 Activity 不能设置固定方向,清单中声明 android:screenOrientation="portrait" 或者调用 setRequestedOrientation 方法都会导致崩溃,解决思路可以取消 Activity 的透明属性,或者将 android:screenOrientation 设置为 behind,由 Parent Activity 来决定。
  • 权限 在 Android 8.0 之前,如果应用在运行时请求权限并且被授予该权限,系统会错误地将属于同一权限组并且在清单中注册的其他权限也一起授予应用。
    对于针对 8.0 的应用,此行为已被纠正。系统只会授予应用明确请求的权限。然而,一旦用户为应用授予某个权限,则所有后续对该权限组中权限的请求都将被自动批准。
    例如,假设某个应用在其清单中列出 READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE。应用请求 READ_EXTERNAL_STORAGE,并且用户授予了该权限。如果该应用针对的是 API 级别 24 或更低级别,系统还会同时授予 WRITE_EXTERNAL_STORAGE,因为该权限也属于同一 STORAGE 权限组并且也在清单中注册过。如果该应用针对的是 8.0,则系统此时仅会授予 READ_EXTERNAL_STORAGE;不过,如果该应用后来又请求 WRITE_EXTERNAL_STORAGE,则系统会立即授予该权限,而不会提示用户。

Android 9.0(Api Level 28)

强制执行 FLAG_ACTIVITY_NEW_TASK 要求

在 Android 9 中,您不能从非 Activity(比如 Service,Application)环境中启动 Activity,除非您传递 Intent 标志 FLAG_ACTIVITY_NEW_TASK。 如果您尝试在不传递此标志的情况下启动 Activity,则该 Activity 不会启动,系统会在日志中输出一则消息。

在 Android 7.0(API 级别 24)之前,标志要求一直是期望的行为并被强制执行。 Android 7.0 中的一个错误会临时阻止实施标志要求。


以上是在 9.0 平台针对所有应用的变更,下面则是针对 Target 为 28 的应用的行为变更


前台服务

如果应用以 Android 9 或更高版本为目标平台并使用前台服务,则必须请求 FOREGROUND_SERVICE 权限。这是普通权限,因此,系统会自动为请求权限的应用授予此权限。

默认启用网络传输层安全协议 (TLS)

如果您的应用以 Android 9 或更高版本为目标平台,则 isCleartextTrafficPermitted() 方法默认返回 false。如果您的应用需要针对特定网域启用明文,则您必须在应用的网络安全配置中,针对这些网域明确将 cleartextTrafficPermitted 设置为 true,如:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">a.foo.com</domain>
        <domain includeSubdomains="true">b.foo.net</domain>
    </domain-config>
</network-security-config>

最后在清单中应用配置:

<?xml version="1.0" encoding="utf-8"?>
<manifest ... >
    <application android:networkSecurityConfig="@xml/foo">
            ...
    </application>
</manifest>

按进程分设基于网络的数据目录

在 Android 9 中,为改善应用稳定性和数据完整性,应用无法再让多个进程共享一个 WebView 数据目录。通常情况下,此类数据目录会存储 Cookie、HTTP 缓存以及其他与网络浏览有关的持久性和临时性存储。
在大多数情况下,您的应用应仅在一个进程中使用 android.webkit 软件包中的类(如 WebView 和 CookieManager)。例如,您应该将使用 WebView 的所有 Activity 对象移入同一进程。您可以通过在应用的其他进程中调用 disableWebView(),更严格地执行“仅限一个进程”规则。该调用可防止 WebView 在这些其他进程中被错误地初始化,即使是从依赖内容库进行的调用也能防止。
如果您的应用必须在多个进程中使用 WebView 实例,则您必须先使用 WebView.setDataDirectorySuffix() 方法为每个进程指定唯一的数据目录后缀,然后再在相应进程中使用 WebView 的给定实例。该方法会将每个进程的网络数据放入应用数据目录内其自己的目录中。

注意:即使您使用 setDataDirectorySuffix(),系统也不会跨应用的进程界限共享 Cookie 以及其他网络数据。如果应用中的多个进程需要访问同一网络数据,您需要自行在这些进程之间复制该数据。例如,您可以调用 getCookie()setCookie(),以在不同的进程之间手动传输 Cookie 数据。

Apache HTTP 客户端弃用

在 Android 6.0 中,我们移除了对 Apache HTTP 客户端的支持。从 Android 9 开始,该内容库已从 bootclasspath 中移除,且默认情况下应用无法使用它。
要继续使用 Apache HTTP 客户端,以 Android 9 及更高版本为目标平台的应用可以向其 AndroidManifest.xml 添加以下内容:

<uses-library android:name="org.apache.http.legacy" android:required="false"/>

Android 10(Api Level 29)

限制非 SDK 接口

详情参阅针对非 SDK 接口的限制官方文档

手势导航

从 Android 10 开始系统提供了手势导航来替代「三大金刚」我们需要注意某些页面是否会和系统手势冲突。

  • 与返回手势冲突 新的返回系统手势是从屏幕左侧或右侧边缘向内滑动。这可能会干扰这些区域中的应用导航元素。如需保持位于屏幕左侧和右侧边缘的元素的功能,您需要告知系统哪些区域需要接收轻触输入,从而选择性地停用返回手势。为此,您可以将 List<Rect> 传递给 Android 10 中引入的 View.setSystemGestureExclusionRects() API。从 androidx.core:core:1.1.0-dev01 开始,ViewCompat 中也提供这种方法。
    List<Rect> exclusionRects;

    public void onLayout(
            boolean changedCanvas, int left, int top, int right, int bottom) {
        // Update rect bounds and the exclusionRects list
        setSystemGestureExclusionRects(exclusionRects);
    }

    public void onDraw(Canvas canvas) {
        // Update rect bounds and the exclusionRects list
        setSystemGestureExclusionRects(exclusionRects);
    }
    
  • 与主屏幕/快速切换手势冲突 新的主屏幕/快速切换系统手势都涉及在屏幕底部导航栏以前占用的空间内滑动。应用无法像停用返回手势一样停用这些手势。

为了缓解这个问题,Android 10 引入了 WindowInsets.getMandatorySystemGestureInsets() API,它会告知应用触摸识别阈值。


以上是在 Android 10 平台针对所有应用的变更,下面则是针对 Target 为 29 的应用的行为变更


限制非 SDK 接口

一句话概括就是非 SDK 接口更新了,更详细的文档在这里

针对全屏 Intent 的权限变更

如果应用以 Android 10 或更高版本为目标平台并使用涉及全屏 intent 的通知,则必须在应用的清单文件中请求 USE_FULL_SCREEN_INTENT 权限。这是普通权限,因此,系统会自动为请求权限的应用授予此权限。

        val fullScreenIntent = Intent(this, ImportantActivity::class.java)
    val fullScreenPendingIntent = PendingIntent.getActivity(this, 0,
        fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT)

    var builder = NotificationCompat.Builder(this, CHANNEL_ID)
            .setSmallIcon(R.drawable.notification_icon)
            .setContentTitle("My notification")
            .setContentText("Hello World!")
            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
            .setFullScreenIntent(fullScreenPendingIntent, true)
    

有关通知的更多知识,可以参考官方文档:创建通知

支持可折叠设备

当应用在 Android 10 上运行时,onResume()onPause() 方法的工作原理是不同的。当多个应用同时在多窗口模式或多显示屏模式下显示时,可见堆栈中所有可设置为焦点的顶层 Activity 都处于“已恢复”状态,但实际上焦点仅位于其中一个 Activity 上,即“在最顶层处于已恢复状态”的 Activity。在 Android 10 之前的版本中运行时,一次只能恢复系统中的一个 Activity,而所有其他可见 Activity 都处于已暂停状态。

请不要将“焦点位于”的 Activity 与“在最顶层处于已恢复状态”的 Activity 混淆。系统会根据 Z-Order 来为 Activity 分配优先级,以便为用户最后进行互动的 Activity 提供更高的优先级。Activity 可能在顶层处于已恢复状态,但焦点却并不位于其上(例如,如果通知栏展开)。

在 Android 10(API 级别 29)及更高版本中,您可以订阅 onTopResumedActivityChanged() 回调,以便在 Activity 获取或失去在最顶层处于已恢复状态的位置后收到通知。这相当于 Android 10 之前版本中的已恢复状态;如果您的应用使用的专用或单一资源可能需要与其他应用共享,这可以作为有用的提示。

resizeableActivity 清单属性的行为也发生了变化。如果某个应用在 Android 10(API 级别 29)或更高版本中设置 resizeableActivity=false,则当可用屏幕尺寸发生变化或者该应用从一个屏幕移到另一屏幕时,它可能处于兼容模式下。

重大变更:外部存储访问权限

外部存储访问权限范围限定为应用文件和媒体
默认情况下,对于以 Android 10 及更高版本为目标平台的应用,其访问权限范围限定为外部存储,即分区存储。此类应用可以查看外部存储设备内以下类型的文件,无需请求任何与存储相关的用户权限:

  • 特定于应用的目录中的文件(使用 getExternalFilesDir() 访问)
  • 应用创建的照片、视频和音频片段(通过 MediaStore 访问)

这里提一点,在 Android 4.4(API 级别 19)或更高版本中,应用无需请求任何与存储空间相关的权限即可访问外部存储空间中的应用专属目录。卸载应用后,系统会移除这些目录中存储的文件。

AndroidManifest 中新增属性:

  • android:requestLegacyExternalStorage,设置为 true 可以在 Android 10 上采用遗留存储模式.
  • android:hasFragileUserData,设置为 true 后,可以在应用卸载时保留应用私有目录中的内容(国产 OS 务必要经过测试,很大可能会失效)

在后台运行时访问设备位置信息需要权限
Android 10 引入了 ACCESS_BACKGROUND_LOCATION 权限(运行时申请)。ACCESS_BACKGROUND_LOCATION 权限仅会影响应用在后台运行时对位置信息的访问权限。除非符合以下条件之一,否则应用将被视为在后台访问位置信息:

  • 属于该应用的 Activity 可见。
  • 该应用运行的某个前台设备已声明前台服务类型为 location。 需要注意在 target 28 以及设备升级到 Android 10 后权限的状态变化,可以在这篇文档中查看

针对从后台启动 Activity 的限制
从 Android 10 开始,系统会增加针对从后台启动 Activity 的限制。此项行为变更有助于最大限度地减少对用户造成的中断,并且可以让用户更好地控制其屏幕上显示的内容。 以闹钟功能来说,官方的规范做法应该是改为使用通知:

  • 当用户使用设备时,系统会显示浮动通知,以便用户作出响应。用户可以维护其当前上下文,并控制自己在屏幕上看到的内容。
  • 有时效性的通知遵循用户的请勿打扰规则。例如,启用“请勿打扰”功能后,用户可以仅允许特定联系人或重复来电者的来电。
  • 设备屏幕关闭时,您的全屏 intent 会立即启动。

重大变更:标识符和数据

随机分配 MAC 地址
默认情况下,在搭载 Android 10 或更高版本的设备上,系统会传输随机分配的 MAC 地址。 如果您的应用处理企业使用场景,平台会提供 API,用于执行与 MAC 地址相关的几个操作:

  • 获取随机分配的 MAC 地址:设备所有者应用和资料所有者应用可以通过调用 getRandomizedMacAddress() 检索分配给特定网络的随机分配 MAC 地址。
  • 获取实际的出厂 MAC 地址:设备所有者应用可以通过调用 getWifiMacAddress() 检索设备的实际硬件 MAC 地址。此方法对于跟踪设备队列非常有用。

对 /proc/net 文件系统的访问权限实施了限制
在搭载 Android 10 或更高版本的设备上,应用无法访问 /proc/net,其中包含与设备的网络状态相关的信息。需要访问这些信息的应用(如 VPN)应使用 NetworkStatsManager 或 ConnectivityManager 类。 对不可重置的设备标识符实施了限制
从 Android 10 开始,应用必须具有 READ_PRIVILEGED_PHONE_STATE 特许权限才能访问设备的不可重置标识符(包含 IMEI 和序列号)

然而从 Google Play 商店安装的第三方应用无法声明特许权限

受影响的方法包括:

  • Build
    • getSerial()
  • TelephonyManager
    • getImei()
    • getDeviceId()
    • getMeid()
    • getSimSerialNumber()
    • getSubscriberId()

如果您的应用没有该权限,但您仍尝试查询不可重置标识符的相关信息,则平台的响应会因目标 SDK 版本而异:

  • 如果应用以 Android 10 或更高版本为目标平台,则会发生 SecurityException。
  • 如果应用以 Android 9(API 级别 28)或更低版本为目标平台,则相应方法会返回 null 或占位符数据(如果应用具有 READ_PHONE_STATE 权限)。否则,会发生 SecurityException。 限制了对剪贴板数据的访问权限
    除非您的应用是默认输入法 (IME) 或是目前处于焦点的应用,否则它无法访问 Android 10 或更高版本平台上的剪贴板数据。

Android 11(Api Level 30)

非 SDK 接口限制

非 SDK 接口又又又更新了,详情可以查看这篇文档

分享内容 URI

如果您的应用与其他应用分享内容 URI,相应 intent 必须至少设置以下 intent 标记中的一个,以便授予对 URI 的访问权限:FLAG_GRANT_READ_URI_PERMISSIONFLAG_GRANT_WRITE_URI_PERMISSION。这样一来,如果其他应用以 Android 11 为目标平台,相应应用仍可访问内容 URI。即使内容 URI 与不属于您的应用的内容提供程序相关联,您的应用也必须包含 intent 标记。


以上是在 Android 11 平台针对所有应用的变更,下面则是针对 Target 为 30 的应用的行为变更


强制执行分区存储

对外部存储目录的访问仅限于应用专用目录,以及应用已创建的特定类型的媒体。 AndroidManifest 清单中 requestLegacyExternalStorage 属性在 Android 11 上被忽略,新增 preserveLegacyExternalStorage,设置为 ture 时可以允许 App 在 target 30 以下升级到 target 30 的情况下继续使用遗留存储。注意此时应用一但卸载,则无任何办法退回遗留存储模型。

自动重置权限

如果用户几个月未与应用互动,系统会自动重置应用的敏感权限。 只要保证敏感权限在使用前都确保调用权限验证方法即可。

在后台访问位置信息的权限

用户必须转到系统设置,才能向应用授予在后台访问位置信息的权限。

软件包可见性

Android 11 更改了应用查询用户已在设备上安装的其他应用以及与之交互的方式。使用 <queries> 元素,应用可以定义一组自身可访问的其他软件包。通过告知系统应向您的应用显示哪些其他软件包,此元素有助于鼓励最小权限原则。此外,此元素还可帮助 Google Play 等应用商店评估应用为用户提供的隐私权和安全性。
如果您的应用以 Android 11 或更高版本为目标平台,您可能需要在应用的清单文件中添加 <queries> 元素。在 <queries> 元素中,您可以按软件包名称、intent 签名或提供程序授权指定软件包。
也就是说,除了一些自动对所有应用可见的 App,调用 getInstalledApplications()getInstalledPackages() 两种方法返回的列表都是经过过滤的。

Toast 相关变更

来自后台的自定义消息框被屏蔽
出于安全方面的考虑,同时也为了保持良好的用户体验,如果包含自定义视图的消息框是以 Android 11 或更高版本为目标平台的应用从后台发送的,系统会屏蔽这些消息框。请注意,仍允许使用文本消息框;此类消息框是使用 Toast.makeText() 创建的,并不调用 setView()。如果您的应用仍尝试从后台发布包含自定义视图的消息框,系统不会向用户显示相应的消息,而是会在 logcat 中记录以下消息:

W/NotificationService: Blocking custom toast from package \
  <package> due to package not in the foreground

消息框回调
如果您希望在消息框(文本消息框或自定义消息框)出现或消失时收到通知,请使用 Android 11 中添加的 addCallback() 方法。 文本消息框 API 变更
以 Android 11 或更高版本为目标平台的应用会发现文本消息框受到以下负面影响:

  • getView() 方法返回 null。

以下方法的返回值并不反映实际值,因此您不应在应用中依赖于它们:

  • getHorizontalMargin()
  • getVerticalMargin()
  • getGravity()
  • getXOffset()
  • getYOffset()

以下方法是空操作,因此您的应用不应使用它们:

  • setMargin()
  • setGravity()

APK 签名方案 v2

用户无法在搭载 Android 11 的设备上安装或更新仅通过 APK 签名方案 v1 签名的应用。

Android 12(Api Level 31)

拉伸滚动效果

在搭载 Android 12 及更高版本的设备上,滚动事件的视觉行为发生了变化。
在 Android 11 及更低版本中,滚动事件会使视觉元素发光。在 Android 12 及更高版本中,发生拖动事件时,视觉元素会拉伸和反弹;发生快速滑动事件时,它们会快速滑动和反弹

应用启动画面

如果您之前在 Android 11 或更低版本中实现了自定义启动画面,则需要将您的应用迁移到 SplashScreen API,以确保它从 Android 12 开始正确显示。如果不迁移您的应用,则可能会导致应用启动体验变差或出乎预期。
如需了解相关说明,请参阅将现有的启动画面实现迁移到 Android 12。 此外,从 Android 12 开始,在所有应用的冷启动和温启动期间,系统始终会应用新的 Android 系统默认启动画面。 默认情况下,此系统默认启动画面由应用的启动器图标元素和主题的 windowBackground(如果是单色)构成。

Display#getRealSize 和 getRealMetrics:废弃和限制

Android 设备有许多不同的外形规格,如大屏设备、平板电脑和可折叠设备。为了针对每种设备适当地呈现内容,您的应用需要确定屏幕或显示屏尺寸。随着时间的推移,Android 提供了不同的 API 来检索这些信息。在 Android 11 中,我们引入了 WindowMetrics API 并废弃了以下方法:

  • Display.getSize()
  • Display.getMetrics()

在 Android 12 中,我们继续建议使用 WindowMetrics,并且正在逐步废弃以下方法:

  • Display.getRealSize()
  • Display.getRealMetrics()

为了缓解应用使用 Display API 检索应用边界的行为,Android 12 限制了 API 为不完全可调整大小的应用返回的值。这可能会对将此信息与 MediaProjection 一起使用的应用产生影响。

应用应使用 WindowMetrics API 查询其窗口的边界,并使用 Configuration.densityDpi 查询当前的密度。

为了与较低的 Android 版本实现更广泛的兼容性,您可以使用 Jetpack WindowManager 库,它包含一个 WindowMetrics 类,该类支持 Android 4.0(API 级别 14)及更高版本。

权限软件包可见性

在搭载 Android 12 或更高版本的设备上,根据应用对其他应用的软件包可见性,以 Android 11(API 级别 30)或更高版本为目标平台且调用以下某种方法的应用会收到一组过滤后的结果:

  • getAllPermissionGroups()
  • getPermissionGroupInfo()
  • getPermissionInfo()
  • queryPermissionsByGroup()

按下“返回”按钮时,不再 finish 根启动器 activity

Android 12 更改了在按下“返回”按钮时系统对为其任务根的启动器 activity 的默认处理方式。在以前的版本中,系统会在按下“返回”按钮时 finish 这些 activity。在 Android 12 中,现在系统会将 activity 及其任务移到后台,而不是完成 activity。当使用主屏幕按钮或手势从应用中导航出应用时,新行为与当前行为一致。 对于大多数应用而言,此变更意味着使用“返回”按钮退出应用的用户可以更快地从温状态恢复应用,而不必从冷状态完全重启应用。

更新后的非 SDK 接口限制

上链接:Android 12 中有关限制非 SDK 接口的更新


以上是在 Android 12 平台针对所有应用的变更,下面则是针对 Target 为 31 的应用的行为变更


自定义通知

大致位置

使用以 Android 12 或更高版本为目标平台的应用时,用户可以请求应用只能访问大致位置信息。如果您的应用以 Android 12 或更高版本为目标平台,并请求 ACCESS_FINE_LOCATION 运行时权限,您还必须请求 ACCESS_COARSE_LOCATION 权限。您必须在单个运行时请求中包含这两项权限。

更安全的组件导出

如果您的应用以 Android 12 或更高版本为目标平台,且包含使用 intent 过滤器的 activity、服务或广播接收器,您必须为这些应用组件显式声明 android:exported 属性。

如果 activity、服务或广播接收器使用 intent 过滤器,并且未显式声明 android:exported 的值,您的应用将无法在搭载 Android 12 或更高版本的设备上进行安装。 这里我们借助一个 gradle 脚本来为第三方 sdk 内注册的组件进行适配:

/**
 * 修改 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()

            }

        }
    }
}

记得在 build.gradle(:app) 中调用:

apply from: new File(rootDir, '.buildscripts/manifest_export.gradle')

适配重点之一分区存储的适配工作,限于篇幅会在下一篇文章中整理。