“据上一篇文又是一个月过去了,虽说金九银十,但今年的氛围实在是太冷清了,能有一份工就不错了吧。但愿美元加息早点结束,经济早点好起来~
上一篇所说内容是所有 App 安装到 Android14 设备上的影响和需要注意的内容,本篇接下来就要介绍当 targetSdkVersion 升级到 34 时,App 需要注意和修改的地方。
1. 核心功能变更
1.1 前台服务类型
在 targetSdkVersion >= 34 的情况下,必须为应用内的每个前台服务(Foreground Service)指定至少一种前台服务类型。
什么是前台服务?前台服务(Foreground Service)是一种特殊类型的服务,用于执行与用户当前活动相关的长时间运行的任务,这些服务会在系统状态栏中显示通知,以告知用户应用正在前台执行任务,并且正在使用系统资源。在 Android12(API级别31)及更高版本的设备上,系统对短时间运行的前台服务进行了优化。系统会等待10秒,然后才显示与前台服务相关联的通知,以改善用户体验,减少即时通知的干扰。使用时需要在 Manifest 文件中申请 android.permission.FOREGROUND_SERVICE
权限。
前台服务类型是在 Android10 引入的,通过 android:foregroundServiceType
可以指定 <service> 的服务类型,可供选择的前台服务类型有:
- camera:需要在后台时继续访问摄像头,比如支持多任务处理的视频聊天应用。
- connectedDevice:与需要蓝牙、NFC、IR、USB 或网络连接的外部设备进行交互。
- dataSync:数据传输操作,例如:数据上传或下载、备份与恢复操作、导入或导出操作、获取数据、本地文件处理、通过网络在设备和云之间传输数据。(这种类型可能会在后续 Android 版本中废弃,建议使用
WorkManager
或 user-initiated data transfer jobs 替换) health
:用于任何需要长期运行的用例,以支持健身类应用程序,如运动追踪器。- location:需要位置访问的长时间运行的用例,例如导航和位置共享。
- mediaPlayback:需要在后台持续播放音频或视频,或在 Android TV 上支持数字视频录制(DVR)功能。
- mediaProjection:使用
MediaProjection
API 可以将内容投影到非主显示器或外部设备。这些内容不一定是专门的媒体内容。 - microphone:需要持续在后台 (如录音机或通信应用程序) 进行麦克风捕获。
- phoneCall:需要持续使用
ConnectionService
API 的场景。 remoteMessaging
:将短信从一台设备转移到另一台设备。在用户切换设备时,帮助确保用户消息任务的连续性。shortService
:需要快速完成不能打断或推迟的重要工作;有 5 个特点:1)只能运行较短的时长,大概 3 分钟;2)不支持粘性前台服务;3)无法启动其他前台服务;4)不需要另外申请特定类型的权限,但少不了FOREGROUND_SERVICE
权限;5)正在运行的前台服务不能在 shortService 类型之间切换。超时之后会调用Service.onTimeout()
,这个 API 是 Android14 新增的,为了避免 ANR 建议实现onTimeout
回调。specialUse
:如果不是上述所有类型所包含的,则使用这个类型。除了声明FOREGROUND_SERVICE_TYPE_SPECIAL_USE
前台服务类型外,还应该在 Manifest 中声明用例。即需要在 <service> 元素中指定元素,如下所示。在 Google Play Console 中提交应用时,这些值和相应的用例将被审查。
// code 1
<service android:name="fooService" android:foregroundServiceType="specialUse">
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" android:value="foo"/>
</service>
systemExempted
:预留给系统应用程序和特定的系统集成,以继续使用前台服务。普通 App 开发者不用管。
另外,上述 13 种类型中,做有彩色标记
的是 Android14 上新增的;其他的则是之前就有的。
举个常见的后台播放的例子,先在 Manifest 文件中申明权限,并设置好 foregroundServiceType
:
// code 2
<manifest ...>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<application ...>
<service
android:name=".MusicPlayerService"
android:foregroundServiceType="mediaPlayback"
android:exported="false">
</service>
</application>
</manifest>
然后再去实现 MusicPlayerService 类,主要就是在 onStartCommand
回调中打开通知并开始播放音乐:
// code 3
class MusicPlayerService : Service() {
private var mediaPlayer: MediaPlayer? = null
override fun onBind(intent: Intent?): IBinder? {
return null
}
@RequiresApi(Build.VERSION_CODES.O)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val NOTIFICATION_CHANNEL_ID = "com.example.foregroundservice"
val notificationManager = NotificationManagerCompat.from(this)
// If the notification supports a direct reply action, use
// PendingIntent.FLAG_MUTABLE instead.
val pendingIntent: PendingIntent =
Intent(this, NotificationFullActivity::class.java).let { notificationIntent ->
PendingIntent.getActivity(
this, 0, notificationIntent,
PendingIntent.FLAG_IMMUTABLE
)
}
// 创建通知渠道
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val name: CharSequence = "Notification Channel Name"
val description = "Description of Notification Channel"
val importance = NotificationManager.IMPORTANCE_DEFAULT
val channel = NotificationChannel(NOTIFICATION_CHANNEL_ID, name, importance)
channel.description = description
notificationManager.createNotificationChannel(channel)
}
// 构建通知
val builder: NotificationCompat.Builder =
NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
.setSmallIcon(android.R.drawable.ic_lock_idle_alarm)
.setContentTitle("音乐播放中")
.setContentText("艺术家 - 音乐")
.setContentIntent(pendingIntent)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
// 启动通知
val notificationId = 1 // 每个通知的唯一标识符
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
) {
notificationManager.notify(notificationId, builder.build())
}
// Notification ID cannot be 0.
startForeground(notificationId, builder.build())
// 播放音乐
mediaPlayer = MediaPlayer.create(this, R.raw.music1)
mediaPlayer?.isLooping = true
mediaPlayer?.start()
return START_STICKY
}
}
最后就是启动这个 Service 了:
代码语言:javascript复制// code 4
requireActivity().startForegroundService(Intent(requireActivity(), MusicPlayerService::class.java))
如果没在 Manifest 文件中写明类型,那么在调用 startForeground()
方法时将会抛出 MissingForegroundServiceTypeException
异常。
对于上面的示例代码需要额外注意的是,在 Android13 及以上的手机上弹出 Notification 通知时,需要动态申请 android.permission.POST_NOTIFICATIONS
权限,当然还需要需要创建一个 NotificationChannel 渠道,这在 Android8 及以上就已经要求了。
再说回前台服务,上述每个前台服务类型所需要的权限是不一样的,并且这些权限都被定义成了普通权限,在默认情况下是已经授予的,用户不能撤销这些权限。例如 Camera 服务类型,需要在 Manifest 文件中声明 FOREGROUND_SERVICE_CAMERA
权限,并在运行时申请 Camera 权限。其他的服务类型都是如此:
Foreground Service Type | Manifest requirements | Runtime requirements |
---|---|---|
Camera | FOREGROUND_SERVICE_CAMERA | CAMERA |
Connected device | FOREGROUND_SERVICE_CONNECTED_DEVICE | 在 Manifest 声明 CHANGE_NETWORK_STATE or CHANGE_WIFI_STATE or CHANGE_WIFI_MULTICAST_STATE or NFC or TRANSMIT_IR 或者请求运行时权限 BLUETOOTH_CONNECT or BLUETOOTH_ADVERTISE or BLUETOOTH_SCAN or UWB_RANGING 或者调用 UsbManager.requestPermission() |
Data sync | FOREGROUND_SERVICE_DATA_SYNC | - |
Health | FOREGROUND_SERVICE_HEALTH | 在 Manifest 声明 HIGH_SAMPLING_RATE_SENSORS 或申请运行时权限 (BODY_SENSORS or ACTIVITY_RECOGNITION) |
Location | FOREGROUND_SERVICE_LOCATION | ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION |
MediaPlayback | FOREGROUND_SERVICE_MEDIA_PLAYBACK | - |
Media projection | FOREGROUND_SERVICE_MEDIA_PROJECTION | 需要在之前调用 createScreenCaptureIntent() |
Microphone | FOREGROUND_SERVICE_MICROPHONE | RECORD_AUDIO |
Phone call | FOREGROUND_SERVICE_PHONE_CALL | 在 Manifest 声明 MANAGE_OWN_CALLS |
Remote messaging | FOREGROUND_SERVICE_REMOTE_MESSAGING | - |
Short service | - | - |
Special use | FOREGROUND_SERVICE_SPECIAL_USE | - |
System exempted | FOREGROUND_SERVICE_SYSTEM_EXEMPTED | - |
当然,肯定有特殊情况。如果自己的前台服务与上面提到的这 13 种都不太相关,那么官方建议将这些服务迁移到 WorkManager
或者 user-initiated data transfer jobs.
user-initiated data transfer jobs 就是由用户发起的数据传输任务。此 API 是 Android14 新增的,适用于需要由用户发起的持续时间较长的数据传输,例如从远程服务器下载文件。这些任务需要在通知栏中显示一个通知,会立即启动,并且可能在系统条件允许的情况下长时间运行。我们可以同时运行多个由用户发起的数据传输作业。
小结:如果目前应用中已用到了前台服务,且 targetSdkVersion 想升到 34,那么就得添加这个前台服务的类型;否则不用管。
1.2 蓝牙连接的权限变更
在 Android14 上,调用 BluetoothAdapter
的 getProfileConnectionState()
API 时必须申请 BLUETOOTH_CONNECT
权限,以前不是必须的,现在必须在 Manifest 文件中声明,并且在运行时向用户申请该权限。
很明显 Android 这几年逐渐在回收一些系统权限,对于开发者来说更加麻烦了,但有利于广大的使用者。
1.3 OpenJDK 17 更新
Android14 继续更新 Android 的核心库,使其与最新的 OpenJDK LTS 版本的特性、功能保持一致,包括对库的更新以及对应用和平台开发人员的 Java17 语言的支持。以下的一些变化可能会影响应用的兼容性:
- 正则表达式的变更:有些正则表达式已经更改,及时检查应用中使用了正则表达式的地方,查看是否出错。可以在开发者选项中关闭兼容模式,方便将有问题的地方查找出来,具体的兼容模式开关在 系统 > 高级 > 开发者选项 > 应用兼容性变更 这里(原生系统在这里,其他厂商就不好说了),并在 list 中选中自己的 App 即可关闭或打开。在这里就是需要在 list 中滑到最底下的
Enabled for targetSdkVersion >= 34
的地方,找到 DISALLOW_INVALID_GROUP_REFERENCE 选项切换; - UUID 处理:在验证输入参数时,
java.util.UUID.fromString()
方法会进行更严格的检查,因此可能会在反序列化时抛出IllegalArgumentException
异常。自测方法同上,需要在 应用兼容性变更 下把 ENABLE_STRICT_VALIDATION 选项切换一下; - ProGuard 出现的问题:在一些情况下使用
ProGuard
进行压缩,混淆,优化代码时,在添加了java.lang.ClassValue
之后会出现问题。此问题是因为一个 Kotlin 库改变了运行时的行为,即在执行Class.forName("java.lang.ClassValue")
是否会返回一个 class 而引发的,如果应用是针对没有java.lang.ClassValue
的旧版本开发的,那么这些优化会从java.lang.ClassValue
派生的类中删除computeValue
方法。
小结:JDK17 虽然会向下兼容,但有空还是升级一下比较好,毕竟有许多新的写法和优化。
2. 安全性
Android14 对安全性也有了更高的要求,这也是近几年来 Google 一直在关注的方向。
2.1 对隐式 Intent 和 PendingIntent 的限制
“隐式 Intent(Implicit Intent)是 Android 应用程序组件之间进行通信的一种机制,它不明确指定要启动哪个组件,而是声明要执行的操作。系统会查找能够处理这个操作的组件,并启动它们。隐式 Intent 主要用于在应用程序内或与其他应用程序之间触发各种操作,如启动活动、启动服务、发送广播等。比较常见的例子就是先在 Manifest 文件中设置某个 Activity 中 intent-filter 的 action,然后可以通过设置启动 Intent 中的 action 来匹配这个 Activity 从而启动它.
- 隐式 Intent 只能传递给
android:exported="true"
的组件(四大组件:Activity、Service···)。所以在 App 中使用 Intent 传递数据要么使用显式 Intent 传递给android:exported="false"
的组件;要么使用隐式 Intent 传递给android:exported="true"
的组件。当然显式 Intent 传给 exported="true" 肯定也是可以的。 - 一个可变的
PendingIntent
必须设置 packageName,否则会抛出异常。
举个栗子:
代码语言:javascript复制// code 5
<!-- android:exported 设置为false,隐式 Intent 无法启动 -->
<activity
android:name=".AppActivity"
android:exported="false">
<intent-filter>
<action android:name="com.example.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
代码语言:javascript复制// code 6
// Throws an exception when targeting Android 14. 直接用隐式的 Intent 调用不管用
context.startActivity(Intent("com.example.action.SEND"))
// This makes the intent explicit. 当设置了 intent 中的 package 参数时就可以了
val explicitIntent =
Intent("com.example.action.SEND")
explicitIntent.apply {
`package` = context.packageName
}
context.startActivity(explicitIntent)
小结:这个变更得重视,特别是 android:exported 设置为 false 的组件,启动这些组件可能会崩溃,需要修改。这个更新还是为了安全,因为这些更改可以防止恶意应用拦截应用内部组件使用的隐式 Intent 。
2.2 动态广播接收器必须指定导出的行为
动态注册的广播接收器必须设置一个标记,用于表明接收器是否被导出到设备上的所有 App。标记位是 RECEIVER_EXPORTED
或 RECEIVER_NOT_EXPORTED
。早在 Android13 就引入了这个功能,可以让应用程序指定一个已注册的广播接收器是否应该被导出,并对设备上的其他应用可见。
只不过在 Android14 上变成了“必须设置”。而在以前的 Android 版本中,设备上的任何应用都可以向动态注册的广播接收器发送未受保护的广播,除非该接收器有签名许可。
举个栗子,在 A 应用中注册 AlarmReceiver 并发送广播:
代码语言:javascript复制// code 7
val filter = IntentFilter("alarmReceiver_custom_action")
val listenToBroadcastsFromOtherApps = true
val receiverFlags = if (listenToBroadcastsFromOtherApps) {
ContextCompat.RECEIVER_EXPORTED // 该接收器对其他应用开放
} else {
ContextCompat.RECEIVER_NOT_EXPORTED // 该接收器不对其他应用开放
}
// 这里的 registerReceiver 方法必须设置 receiverFlags 参数
registerReceiver(requireContext(), AlarmReceiver(), filter, receiverFlags)
// 发送广播
val intent = Intent("alarmReceiver_custom_action") // 方式1
//val intent = Intent(requireActivity(), AlarmReceiver::class.java) // 方式2
requireActivity().sendBroadcast(intent)
在其他的应用中只能通过 code7 中的方式1发送广播,如果 A 应用的 listenToBroadcastsFromOtherApps
设置为 true,那么在 A 应用就能收到其他应用通过方式1发送的广播信息了,否则无法收到。
在实践中还发现,如果 A 应用也通过方式1发送自己应用内部的广播,且设置 ContextCompat.RECEIVER_NOT_EXPORTED,那么这个广播是无法收到的,感兴趣的同学可以试试。
如果应用程序只是通过 Context#registerReceiver
方法 (比如 Context#registerReceiver()
)为系统广播注册接收器,那么它可以不在注册接收器时指定该标志。
小结:动态广播的注册方法改了,需要设置是否对其他应用可见,这跟 android:exported 的设置是一样的道理。其实本地广播和全局广播的功能和这个一样,只不过在 targetSdkVersion >= 34 上更加重视了。
2.3 更安全的动态代码加载
所有动态加载的文件都必须标记为只读。否则,系统将抛出异常。官方建议应用尽可能避免动态加载代码,因为这样做会大大增加应用被代码注入或代码篡改破坏的风险。
如必须动态加载代码,则需要将动态加载的文件(如 DEX、JAR 或 APK 文件)在文件打开并写入任何内容之前设置为只读:
代码语言:javascript复制// code 8
val jar = File("DYNAMICALLY_LOADED_FILE.jar")
val os = FileOutputStream(jar)
os.use {
// Set the file to read-only first to prevent race conditions
jar.setReadOnly()
// Then write the actual file content
}
val cl = PathClassLoader(jar.absolutePath, parentClassLoader)
此外,为防止系统对现在已有的动态加载文件抛出异常,官方建议先删除并重新创建文件,然后再尝试在应用中重新动态加载这些文件。重新创建文件时,请按照上述指南在写入时将文件标记为只读。或者,可将现有文件重新标记为只读,但在这种情况下,官方建议先验证文件的完整性(例如,对照可信值检查文件的签名)以保护应用免遭恶意操作的影响。
2.4 Zip 路径遍历
针对 Android14 的应用,Android 系统通过以下方式防止 Zip 路径遍历的漏洞:如果 zip 文件条目名称包含 “..” 或以 “/” 开头,则 ZipFile(String)
和 ZipInputStream.getNextEntry()
会抛出一个 ZipException
异常。
如果不想抛出异常且文件名称又不能改,可以通过调用 dalvik.system.ZipPathValidator.clearCallback()
选择退出验证。当然这是不推荐的。
Zip 路径遍历漏洞:指恶意攻击者通过构造含有 "../" 或以 "/" 开头的文件路径,在解压缩 Zip 文件时可以访问 Zip 文件之外的文件系统上的任意文件或目录,从而对应用程序造成安全风险的漏洞。
2.5 后台启动 Activity 新增限制
在 Android14 上系统进一步限制了 App 从后台启动 Activity 的情况:
- 当 App 使用
PendingIntent#send()
或类似方法发送PendingIntent
时,必须选择是否要授予自己的后台 Activity 启动的权限来发送PendingIntent
。如果选择授权,则需要通过setPendingIntentBackgroundActivityStartMode(MODE_BACKGROUND_ACTIVITY_START_ALLOWED)
方法返回并传递一个ActivityOptions
对象。 - 当一个前台可见应用使用
bindService()
方法绑定另一个后台应用的 Service 时,这个可见应用现在必须选择是否将自己的后台 Activity 启动权限授予被绑定的服务。如果选择授权,应用在调用bindService()
方法时需要设置BIND_ALLOW_ACTIVITY_STARTS
标志。
这些变化扩展了现有的限制集,通过防止恶意应用程序滥用 API 从后台启动破坏性 Activity 来保护用户。
小结:针对后台启动控制得更严格了,如果项目中有相关逻辑,建议跑一跑看能否后台启动,如有问题再对照上面的内容进行修改,硬啃实在是不知道说的啥意思。。。
3. 有关限制非 SDK 接口的更新
Android14 更新了受限的非 SDK 接口列表(基于与 Android 开发者之间的协作以及最新的内部测试使用的 API 列表)。在限制使用非 SDK 接口之前,官方会尽可能确保有可用的公开替代方案。
如果应用并非以 Android14 为目标平台,其中一些变更可能不会立即对应用产生影响。但只要 App 使用任何非 SDK 方法或字段,终归存在导致应用出问题的显著风险。
一般而言,公共 SDK 接口是在 Android 框架 软件包索引(https://developer.android.google.cn/reference/packages) 中记录的那些接口。非 SDK 接口的处理是 API 抽象出来的实现细节,因此这些接口可能会在不另行通知的情况下随时发生更改。
如果不确定自己的应用是否使用了非 SDK 接口,则可以在 Debug 模式下运行测试 App,如果该应用访问了某些非 SDK 接口,系统就会输出一条日志消息。可以检查应用的日志消息,查找以下详细信息: 1)声明的类、名称和类型(采用 Android 运行时所使用的格式); 2)访问方式:链接、反射或 JNI; 3)所访问的非 SDK 接口属于哪个名单; 还可以使用 adb logcat 来查看这些日志消息,这些消息显示在所运行应用的 PID 下。举例而言,日志中可能包含如下条目:
代码语言:javascript复制Accessing hidden field Landroid/os/Message;->flags:I (light greylist, JNI)
如果应用依赖于非 SDK 接口,应该开始计划迁移到 SDK 的替代方案。如果无法为应用中的某项功能找到使用非 SDK 接口的替代方案,应向官方请求新的公共 API。
如需查看 Android14 的所有非 SDK 接口的完整列表,可下载查看以下文件:hiddenapi-flags.csv(https://dl.google.com/developers/android/udc/non-sdk/hiddenapi-flags.csv?hl=zh-cn),这个表格文件内容很多,可用于查询。
小结:普通应用开发者一般情况下也不会用到非 SDK 接口,这个可忽略。
以上就是本篇的所有内容,可以看出,现有的 App 如果直接将 targetSdkVersion 升级到 34(Android14)的话还是有些地方需要注意并进行修改测试的。如果还想了解 Android14 新增了哪些功能,欢迎关注我,咱们下篇见!
更多内容,欢迎关注公众号:修之竹 或者查看 修之竹的 Android 专辑
赞人玫瑰,手留余香!欢迎点赞、转发~ 转发请注明出处~
参考文献
- Android 14 官方文档 https://developer.android.com/about/versions/14
- https://developer.android.google.cn/about/versions/14/behavior-changes-14?hl=zh-cn
- Android 14 快速适配要点; 恋猫de小郭; https://juejin.cn/post/7231835495557890106?searchId=202307240025039D8229C74EA62159077B
- https://developer.android.google.cn/guide/components/foreground-services
- https://developer.android.com/about/versions/14/changes/user-initiated-data-transfers?hl=zh-cn
- https://developer.android.google.cn/about/versions/14/changes/fgs-types-required
- https://developer.android.google.cn/about/versions/13/features#runtime-receivers
- https://developer.android.google.cn/about/versions/14/changes/non-sdk-14?hl=zh-cn