Android 12兼容性适配方案

首先,Android 12的兼容性适配分了所有应用和以Android 12 为目标平台的适配两种,其中前者为必须要做的,后者的适配也很重要,虽然应用可以暂时不设为Android 12 ,但是随着Android开发的不断更新,各种工具更新,总会在某一个时刻我们升级了AS或者使用了其他工具需要我们适配新特性,不如一次到位都适配到。
这里给大家做个梳理,内容如有疑问可到Android 官网查看。

一、所有应用
1.1 前台服务通知延迟
背景说明:为了在 Android 12 上提供针对短时间运行的前台服务的流畅体验,系统可以为某些前台服务延迟 10 秒显示前台服务通知。此更改使某些短期任务在显示通知之前完成。

如果某项前台服务有以下特征,则系统服务启动后立即显示相关通知:

该服务与包含操作按钮的通知关联。
该服务的 foregroundServiceType 为 connectedDevice、mediaPlayback、mediaProjection 或 phoneCall
该服务根据通知的类别属性中的定义,提供与通话、导航或媒体播放相关的用例。
该服务通过在设置通知时调用 setShowForegroundImmediately(),以停用行为变更。

适配建议:了解

1.2 MAC 地址的获取
背景说明:Android 12 进一步限制了所有非系统应用对设备的 MAC 地址(不可重置的标识符)的访问,无论目标 API 级别如何都是如此。

如果应用以 Android 12 为目标平台,则该 API 会返回 null
如果应用以 Android 11 或更低版本为目标平台,则该 API 将返回硬编码占位值:02:00:00:00:00:00
NetworkInterface.getHardware-Address() 时,logcat 输出会显示:CompatibilityChangeReporter: Compat change id reported: 170188668;
开发者可以使用名为 RETURN_NULL_HARDWARE_ADDRESS 的兼容性标志切换 NetworkInter-face.getHardwareAddress() 的行为:在启用时返回 null,或在停用时返回 02:00:00:00:00:0

适配建议:了解

1.3 不受信任的触摸事件被屏蔽
背景说明:
为了维持系统安全并保持良好的用户体验,Android 12 会阻止应用使用触摸事件,使用触摸事件时叠加层会以不安全的方式遮掩应用。 换言之,系统会屏蔽穿透某些窗口的触摸操作.

此变更会影响选择让触摸操作穿透其窗口的应用,例如使用 FLAG_NOT_TOUCHABLE 标志。包括但不限于以下示例:

需要 SYSTEM_ALERT_WINDOW 权限并使用 FLAG_NOT_TOUCHABLE 标志的叠加层,例如使用 TYPE_APPLICATION_OVERLAY 的窗口。
使用 FLAG_NOT_TOUCHABLE 标志的 activity 窗口。
消息框消息。
但是有例外情况,允许执行“穿透”触摸操作:

应用中的互动;可信窗口;不可见窗口;全透明窗口;足够半透明的系统警报窗口。

兼容性影响:影响选择让触摸通过其窗口(例如使用 FLAG_NOT_TOUCHABLE 标志)

包括但不限于以下示例:
需要SYSTEM_ALERT_WINDOW权限的叠加层,例如使用 TYPE_APPLICATION_OVERLAY并使用 FLAG_NOT_TOUCHABLE标志的窗口。
使用FLAG_NOT_TOUCHABLE标志的Activity窗口,Toast消息。
适配建议:各组件检查允许触摸事件通过窗口的场景及功能正常,如果系统屏蔽触摸操作,Logcat会记录以下消息:Untrusted touch due to occlusion by PACKAGE_NAME

1.4 应用无法关闭系统对话框
背景说明:

为了加强用户与应用和系统互动时的控制,从 Android 12 开始,弃用了ACTION_CLOSE_SYSTEM_DIALOGS intent 操作。

兼容性影响:当应用尝试调用包含此操作的 intent 时,系统会基于应用的目标 SDK 版本执行以下操作之一:

如果应用以 Android 12(API 级别 31)为目标平台,则会发生 SecurityException。
如果应用以 Android 11(API 级别 30)或更低版本为目标平台,则系统不会执行 intent,并且 Logcat 中会显示以下消息:

E ActivityTaskManager Permission Denial:
\android.intent.action.CLOSE_SYSTEM_DIALOGS broadcast from
com.package.name requires
android.permission.BROADCAST_CLOSE_SYSTEM_DIALOGS, \dropping
broadcast.

适配建议:了解

1.5 限制非 SDK 接口
背景说明:
如果您的应用依赖于非 SDK 接口,您应该开始计划迁移到 SDK 替代方案。只要应用引用非 SDK 接口或尝试使用反射或 JNI 来获取其句柄,这些限制就适用。

非 SDK 接口的处理是 API 抽象出来的实现细节,因此这些接口可能会在不另行通知的情况下随时发生更改。

为了避免发生崩溃和意外行为,应用应仅使用 SDK 中经过正式记录的类。这也意味着当您的应用通过反射等机制与类互动时,不应访问 SDK 中未列出的方法或字段。

涉及的主要变更有:

目前在 Android 12 中被屏蔽的非 SDK 接口
在 Android 12 中被添加到 SDK 的非 SDK 接口
适配建议:如相关组件依赖于Android 12的受限接口,应迁移到SDK替代方案。

1.6 Bouncy Castle实现的移除
背景说明:Android 12移除了许多之前平台中已标记为deprecated的BouncyCastle加密算法实现,包括所有AES算法。系统改为使用Conscrypt进行替换。

兼容性影响:

如果满足以下任一条件,则此变更会影响您的应用程序:

您的应用使用512位密钥大小。Conscrypt不支持此大小的密钥。 如有必要,请更新您应用的加密逻辑以使用不同大小的密钥。
您使用12字节以外的大小初始化Galois /计数器模式(GCM)密码。 Conscrypt的GcmParameterSpec的实现需要12字节的初始化,这是NIST推荐的。

适配建议:使用Conscrypt进行替换

1.7 麦克风和摄像头开关
背景说明:在Android 12 系统上,快速设置里增加了全局开关,允许用户快速打开或关闭应用访问摄像头和麦克风的权限(仅限支持的设备)。当开关被关闭时,访问相应传感器的应用会收到空白的视频和音频流,系统也会提示用户打开传感器以使用应用的功能。麦克风和摄像头开关控制对所有的应用均生效,无论其目标平台版本如何。

适配说明:开发者可以使用新的 SensorPrivacyManager API 来了解设备对这个开关功能的支持情况。
如需检查设备是否支持麦克风和摄像头切换开关,请添加以下代码段中所示的逻辑:

SensorPrivacyManager sensorPrivacyManager = getApplicationContext()
.getSystemService(SensorPrivacyManager.class);
boolean supportsMicrophoneToggle = sensorPrivacyManager
.supportsSensorToggle(Sensors.MICROPHONE);
boolean supportsCameraToggle = sensorPrivacyManager
.supportsSensorToggle(Sensors.CAMERA);


同时,相关麦克风/摄像头也有了快捷显示图标。

二、影响以Android 12为目标平台的应用
仅影响以 Android 12 为目标平台的应用的行为变更(targetSdkVersion = 31)

2.1 WebView 中的现代 SameSite Cookie 行为
背景说明:

Android 的 WebView 组件基于为 Google 的 Chrome 浏览器提供支持的开源项目 Chromium。Chromium 变更了对第三方 Cookie 的处理方式,目的是为了更好地保护用户的安全和隐私,并赋予用户更高的透明度和控制权。这些变更已面向很多 Chrome 用户发布,从 Android 12 开始,这些变更将应用于 WebView 中。

兼容性影响:

Cookie 的 SameSite 属性决定了它是可以与任何请求一起发送,还是只能与同站点请求一起发送。Android 12 中的 WebView 基础版本(版本 89.0.4385.0)包含以下隐私保护方面的变更,旨在改善对第三方 Cookie 的默认处理方式,并帮助防止意外跨站点共享:

没有 SameSite 属性的 Cookie 被视为 SameSite=Lax。
带有 SameSite=None 的 Cookie 还必须指定 Secure 属性,这意味着它们需要安全的上下文,并应通过 HTTPS 发送。
站点的 HTTP 版本和 HTTPS 版本之间的链接现在被视为跨站点请求,因此除非将 Cookie 正确标记为 SameSite=None; Secure,否则 Cookie 不会被发送。
对于开发者而言,一般指导意见是识别关键用户流中的跨站点 Cookie 依赖项,并确保在需要时使用适当的值显式设置 SameSite 属性。您必须显式指定允许在不同网站上运行的 Cookie,或适用于从 HTTP 切换到 HTTPS 进行同站点导航的 Cookie。

适配建议:如UI组件使用了WebView,或者使用Cookie管理网站或服务,需要在 Android 12 WebView 上测试您的数据流。 如果发现问题,您可能需要更新 Cookie 以支持新的 SameSite 行为。

2.2 更安全地导出组件
背景说明:

如果应用以Android 12为目标平台(targetSdkVersion = 31),且包含使用 intent 过滤器的 activity、服务或广播接收器,您必须为这些应用组件显式声明 android:exported 属性。否则,应用将无法在Android12平台安装。

兼容性影响:

取决于Android Studio版本:

Android Studio 2020.3.1 Canary 11或更高版本时,清单文件中将出现警告;当您尝试编译应用程序时,会生成错误消息;
若是旧版Android Studio,当您尝试安装该应用程序会出现失败INSTALL_FAILED_VERIFICATION_FAILURE
适配建议:代码自查

<service android:name="com.example.app.backgroundService"
android:exported="false">
<intent-filter>
<action android:name="com.example.app.START_BACKGROUND" />
</intent-filter>
</service>

2.3 待处理 intent 必须声明可变性
背景说明:

必须为您的应用创建的每个 PendingIntent 对象指定可变性。这项额外的要求可提高应用的安全性。如需声明特定 PendingIntent 对象是否可变,请分别使用 PendingIntent.FLAG_MUTABLE或PendingIntent. FLAG_IMMUTABLE标志。如果您的应用试图在不设置任何可变标志的情况下创建 PendingIntent对象,系统会抛出 IllegalArgumentException,并在logcat中显示

PACKAGE_NAME: Targeting S+ (version 10000 and above) requires that one
of \FLAG_IMMUTABLE or FLAG_MUTABLE be specified when creating a
PendingIntent.Strongly consider using FLAG_IMMUTABLE, only use
FLAG_MUTABLE if \some functionality depends on the PendingIntent being
mutable, e.g. if \it needs to be used with inline replies or bubbles.

适配建议:尽可能创建不可变的待处理 intent。

PendingIntent pendingIntent = PendingIntent.getActivity(getApplicationContext(),
REQUEST_CODE, intent,
/* flags */ PendingIntent.FLAG_IMMUTABLE);

需要声明给定的PendingIntent对象是可变的或不可变的,请分别使用PendingIntent.FLAG_MUTABLE 或者PendingIntent.FLAG_IMMUTABLE标志。

如果某些业务需要创建可变的 PendingIntent 对象:

通知中的直接回复操作需要变更与回复关联的 PendingIntent 对象中的剪辑数据。通常,您可以通过将 FILL_IN_CLIP_DATA 作为标志传递给 fillIn() 的方法请求此变更。
如果您的应用使用 PendingIntent 将对话放在气泡中,则 intent 应该可变,以便系统可以应用正确的标志,例如 FLAG_ACTIVITY_MULTIPLE_TASK 和 FLAG_ACTIVITY_NEW_DOCUMENT。
如果创建了可变的 PendingIntent 对象,强烈建议您使用显式 intent 并填写ComponentName。如此一来,每当另一个应用调用 PendingIntent 并将控制权传回您的应用时,应用中的相同组件都会启动。

2.4 以不安全的方式启动嵌套 intent
背景说明:如果您的应用以不安全的方式启动嵌套 intent,此功能便会发出警告。嵌套 intent 是在其他 intent 中作为 extra 传递的 intent。

兼容性影响:

如果您的应用同时执行以下两项操作,就会发生 StrictMode 违规行为。

您的应用从已传递的 intent 的 extra 中解封嵌套 intent。
您的应用立即使用该嵌套 intent 启动应用组件,例如将 intent 传递给 startActivity()、startService() 或 bindService()。
适配建议:

嵌套 intent 的内部启动:确保这些组件不会被导出。
嵌套 intent 的跨应用启动:使用 PendingIntent 代替嵌套 intent。如此一来,当 PendingIntent 从包含它的 Intent 中解封时,应用组件可以使用调用进程的身份启动 PendingIntent。该配置允许提供程序应用向调用应用的任何组件(包括未导出的组件)发送回调。

2.5 前台服务启动限制
背景说明:以 Android 12 为目标平台的应用再也无法在后台运行时启动前台服务,但一些特殊情况除外(允许从后台启动前台服务的情况)。如果应用尝试在后台运行时启动前台服务,则会引发异常(少数特殊情况除外)。如果应用在后台运行时尝试启动前台服务,并且前台服务不符合任何特殊情况,则系统会抛出 ForegroundServiceStartNotAllowedException,如果一个应用调用 Context.startForegroundService() 来启动另一个应用拥有的前台服务,只有在这两个应用都以 Android 12 为目标平台时,本页介绍的限制才会生效。

适配建议:如应用受此影响,当相关业务在后台运行时,请考虑使用 WorkManager 来计划和启动工作。从 WorkManager 2.7.0 开始,您的应用可以调用 setExpedited() 来声明 Worker 应使用加急作业

OneTimeWorkRequest request = new OneTimeWorkRequestBuilder<T>()
.setInputData(inputData)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build();

2.6 无法通过服务或广播接收器创建通知 trampoline
背景说明:当用户与通知互动时,某些应用会启动应用组件来响应通知点按操作,此应用组件最终会启动用户最终看到并与之互动的 activity。此应用组件被称为通知 trampoline。

当应用尝试从充当通知 trampoline 的服务或广播接收器启动 activity 时,系统会阻止启动该 activity 启动。

适配建议:如果您的应用从充当通知 trampoline 的服务或广播接收器启动 activity,请完成以下迁移步骤:

创建与一个下列 activity 关联的 PendingIntent 对象:

用户点按通知后会看到的 activity(首选)。 Trampoline activity 或用于启动用户在点按通知后可以看到的activity 的 activity。

在构建通知的过程中,请使用您在上一步中创建的 PendingIntent 对象。

2.7 自定义通知变更
背景说明:

Android 12 改变了完全自定义通知的外观。 以前,自定义通知能够使用整个通知区域并提供自己的布局和样式。由此产生的反模式可能会令用户困惑,或在不同设备上引发布局兼容性问题。

对于以 Android 12 为目标平台的应用,包含自定义内容视图的通知将不再使用完整通知区域;相反,系统会应用标准模板。此模板可确保自定义通知在所有状态下都与其他通知相同,例如,在收起状态下的通知图标和展开功能,以及在展开状态下的通知图标、应用名称和收起功能。此行为与 Notification.DecoratedCustomViewStyle 的行为几乎完全相同。

适配建议:

如果应用使用了完全自定义的通知,需要尽快使用新模板进行测试调整:

启用自定义通知变更:

将应用的 targetSdkVersion 变更为 S 以启用新行为。
重新编译。
在搭载 Android 12 的设备或模拟器上安装您的应用。
测试所有使用自定义视图的通知,确保这些通知在通知栏中看起来符合预期。
注意:提供给自定义通知的高度比之前小,在收起状态下,自定义内容的最大高度已从 106dp 减少到 48dp。此外,水平空间也减小了。

2.8 连接性
背景说明:以 Android 12 及更高版本为目标平台的设备在具有硬件支持的设备上运行时,在与对等设备建立连接时,使用点对点连接不会断开与现有 Wi-Fi 的连接。如需检查是否支持此功能,请使用 WifiManager. isMultiStaConcurrencySupported()

适配建议:了解

2.9 运动传感器刷新速率限制
背景说明:为了保护用户的潜在敏感信息,如果您的应用针对Android 12,则系统会将来自某些运动传感器和位置传感器的数据刷新率限制为200 Hz。此数据包括由设备的加速度,陀螺仪和磁力传感器记录的值。

适配建议:

如果程序以Android 12为目标平台并且需要以更高的速率收集运动传感器数据,则必须声明 HIGH_SAMPLING_RATE_SENSORS 权限。否则未声明此权限的情况下,以更高的速率收集运动传感器数据,则系统会抛出SecurityException异常。

2.10 adb备份限制
背景说明:为了保护私有应用数据,Android 12变更了adb backup 命令的默认行为。对于以Android 12为目标平台的应用,用户运行adb backup命令时,从设备导出的任何其他系统数据都不包含应用数据。

适配建议:了解。

2.11 大概位置
背景信息:使用针对Android 12的应用程序时,用户可以要求该应用程序只能访问大概的位置信息。为了更好地尊重用户隐私,建议您仅请求ACCESS_COARSE_LOCATION。如果您的应用面向Android 12并请求ACCESS_FINE_LOCATION运行时权限,则您还必须请求ACCESS_COARSE_LOCATION权限。您必须在单个运行时请求中同时包含两个权限。

兼容性影响:如应用必须在单个运行时请求中同时包含两个权限。如果尝试仅请求ACCESS_FINE_LOCATION,则系统将忽略该请求,并在Logcat中记录以下错误消息:ACCESS_FINE_LOCATION must be requested with ACCESS_COARSE_LOCATION

适配建议:面向Android 12的应用使用如下代码进行ACCESS_FINE_LOCATION的申请。

ActivityResultLauncher<String[]> locationPermissionRequest =
registerForActivityResult(new ActivityResultContracts
.RequestMultiplePermissions(), result -> {
Boolean fineLocationGranted = result.getOrDefault(
Manifest.permission.ACCESS_FINE_LOCATION, false);
Boolean coarseLocationGranted = result.getOrDefault(
Manifest.permission.ACCESS_COARSE_LOCATION,false);
if (fineLocationGranted != null && fineLocationGranted) {
// Precise location access granted.
} else if (coarseLocationGranted != null && coarseLocationGranted) {
// Only approximate location access granted.
} else {
// No location access granted.
}
}
);

// ...

// Before you perform the actual permission request, check whether your app
// already has the permissions, and whether your app needs to show a permission
// rationale dialog. For more details, see Request permissions.
locationPermissionRequest.launch(new String[] {
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
});

在要求用户将应用程序的访问权限升级到精确位置之前,请考虑您的应用程序用例是否绝对需要这种精确度。如果您的应用需要通过蓝牙或Wi-Fi将设备与附近的设备配对,请考虑使用配对设备配对或新的蓝牙权限,而不是请求ACCESS_FINE_LOCATION权限。 要请求用户将您应用的位置信息访问权限从近似升级为精确,请执行以下操作:如有必要,请说明您的应用为何需要许可。再次一起请求ACCESS_FINE_LOCATIONACCESS_COARSE_LOCATION权限

2.12 应用休眠
背景说明:

Android 12扩展了Android 11(API级别30)中引入的权限自动重置行为。如果您的应用以Android 12为目标,并且用户几个月没有与应用交互,则系统会自动重置所有授予的权限,并将您的应用置于休眠状态。

处于休眠状态的应用程序具有以下特征:

系统针对存储空间而不是性能进行了优化。应用缓存中的所有文件都将被删除。
该应用程序无法从后台运行作业或警报。
该应用无法接收推送通知,包括通过Firebase Cloud Messaging发送的高优先级消息。
适配建议:

重新计划:当用户下次与该应用程序交互时,该应用程序退出休眠状态,并且可以再次创建作业,警报和通知。但是,您需要重新计划,在应用程序进入休眠状态之前计划的所有作业,警报和通知。此工作流程类似于用户手动从系统设置中强制停止您的应用程序。要更轻松地支持此工作流程,请使用WorkManager。您还可以在ACTION_BOOT_COMPLETED广播接收器中添加重新计划逻辑,当您的应用退出休眠状态并在设备启动后被调用。
请求用户禁用休眠:如果您预计应用中的用例会受到休眠的影响,则可以向用户发送请求,以授予您的应用免于休眠和权限自动重置的豁免权。此豁免对于即使用户没有与应用程序进行交互,而用户仍希望应用程序在后台运行的工作也很有用,例如当您的应用程序执行以下一项或多项操作时:

通过定期报告家庭成员的位置来提供家庭安全。
在设备和应用服务器之间同步数据。
与电视等智能设备进行通信。
与伴侣设备配对,例如手表。
请求豁免,请调用包含Intent.ACTION_APPLICATION_DETAILS_SETTINGS意向操作的意向。在出现的界面上,用户可以关闭名为”Remove permissions and free up space”选项。
2.13 GpsStatus API限制
背景说明:Android 7.0 以后官方推荐使用GNSS。目标平台是Android 12及以上的应用程序,所用到的GpsStatus API必须要用GnssStates API替换。

适配建议:如果应用的targetSdk版本是针对Android 12及以上,为保证功能的正常,请使用GnssStatus.Callback对GpsStatus.Listener进行替代

2.14 精确闹钟的新权限
背景说明:闹钟是应用安排定时工作的重要方式。在大多数情况下,应用应该使用非精确闹钟 (inexact alarms),这样可以减少电池消耗。Android 系统可以通过低电耗模式 (Doze) 和应用待机模式 (App Standby) 等机制管理这些闹钟,从而最大限度地减少设备唤醒和电池消耗。对于那些需要精确闹钟的情况,例如闹铃应用和定时器,您仍然可以使用精确闹钟 (exact alarms)。精确闹钟功能非常方便可靠,但也会加大电量消耗,在过度使用的情况下尤其如此。

适配建议:尽可能调整为不需精确闹钟,针对 Android 12 的应用如果想要使用精确闹钟,现在需要申请一个新的权限: SCHEDULE_EXACT_ALARM。这是一个一般权限,所以只要您的应用在清单中进行了声明,就会在第一次启动时被自动授予该权限。但用户仍可拒绝授予或撤销权限。使用canScheduleExactAlarms(),可用来检查应用的权限状态。

三、其他新能力

3.1 圆角API
背景说明:Android 12引入了RoundedCornerWindowInsets.getRoundedCorner(int position),它们提供了圆角的半径和中心点。使用这些API,您的应用程序可以避免UI元素在带有圆角的屏幕上被截断。这些API在非圆角屏幕的设备上不生效。

适配建议:了解。如果应用程序无法占据整个屏幕,则getRoundedCorner API会通过将圆角的中心点置于应用程序的窗口边界范围内来应用圆角

3.2 附近设备权限
背景说明: Android 12 增加了一个新的运行时权限,可以在不使用用户位置信息的情况下扫描附近设备,从而减少对用户数据的访问。之前,手表和耳机等设备的伴侣应用需要位置权限来扫描附近的蓝牙设备并进行配对。这让用户和开发者们很困惑,也会导致用户在没有必要的情况下授予位置权限。对于把 Target SDK 等级升级到 Android 12 的应用,您可以使用新的 BLUETOOTH_SCAN 权限并设置 usesPermissionFlags=“neverForLocation” 属性来扫描附近的设备,从而与精细位置权限脱钩。

3.3 沉浸式手势导航改进
背景说明:Android 12 简化了沉浸模式,使手势导航更易于操作且与其他活动体验(例如观看视频和阅读图书)更加一致。应用仍然可以在全屏游戏体验中防止意外手势,以免用户在玩游戏时意外退出游戏;所有其他全屏或沉浸式体验现在都允许用户通过滑动手势进行导航。

为了实现这一点,从 Android 12 开始,我们已弃用非粘性沉浸式体验(BEHAVIOR_SHOW_BARS_BY_TOUCH、BEHAVIOR_SHOW_BARS_BY_SWIPE)的现有行为。它们已被默认行为 (BEHAVIOR_DEFAULT) 取代,即在隐藏系统栏后,允许使用滑动手势。

3.4 画中画模式提升
Android 12改进了单击和双击pip窗口的行为,单击和双击pip窗口的行为优化

单击pip窗口时仅显示控制组件,以前单击操作会强制展开pip窗口并显示控制组件。
双击pip窗口,窗口会在当前pip窗口大小和最大窗口大小之间切换。以前双击窗口会切换到全屏模式。

3.5 兼容的媒体转码
背景说明:Android 12 引入了一项新功能,允许视频相关应用程序可以对设备上录制的视频进行更现代、存储效率更高的编码,同时又不影响与其他应用的兼容性。

如果视频被不支持 HEVC 格式的应用打开,Android 会自动将以 HEVC (H.265) 等格式录制的视频转码为 AVC (H.264) 格式。

3.6 熄屏状态下支持NFC
背景说明:针对Android 12及更高版本的应用中,可以通过将requireDeviceScreenOn设置为false来启用NFC付款,而无需点亮设备的屏幕。

猜你喜欢

转载自blog.csdn.net/weixin_53545232/article/details/128387675