Android 8/9 适配指南:通知渠道与前台服务
Android 8.0(API 26)和 9.0(API 28)虽然版本较旧,但在存量设备中仍有相当比例。本文梳理这两个版本的适配要点,帮助开发者兼容旧设备。
一、Android 8.0(API 26)适配要点
Android 8.0 是行为变更较大的一代,以下变更在 targetSdk ≥ 26 时生效。
1.1 通知渠道(Notification Channel)【必须适配】
背景: Android 8.0 引入了通知渠道概念,应用必须创建通知渠道才能显示通知。
Manifest 声明:
<!-- Android 8.0+ 需要小型图标和颜色 -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_stat_notification" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_color"
android:resource="@color/color_accent" />
创建通知渠道(兼容代码):
fun createNotificationChannels(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// 重要通知渠道
val importantChannel = NotificationChannel(
"important_channel_id",
"重要通知",
NotificationManager.IMPORTANCE_HIGH
).apply {
description = "订单状态、支付通知等重要消息"
enableVibration(true)
vibrationPattern = longArrayOf(0, 100, 200, 300)
}
// 普通通知渠道
val defaultChannel = NotificationChannel(
"default_channel_id",
"普通通知",
NotificationManager.IMPORTANCE_DEFAULT
).apply {
description = "系统通知、活动通知等"
}
// 静默通知渠道
val silentChannel = NotificationChannel(
"silent_channel_id",
"静默通知",
NotificationManager.IMPORTANCE_LOW
).apply {
description = "日志同步、后台状态等非紧急通知"
}
val notificationManager = context.getSystemService(NotificationManager::class.java)
notificationManager.createNotificationChannels(listOf(
importantChannel,
defaultChannel,
silentChannel
))
}
}
发送通知(兼容代码):
fun showNotification(context: Context, title: String, message: String) {
val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Notification.Builder(context, "default_channel_id")
} else {
Notification.Builder(context)
}
builder.setContentTitle(title)
.setContentText(message)
.setSmallIcon(R.drawable.ic_stat_notification)
.setAutoCancel(true)
val notificationManager = context.getSystemService(NotificationManager::class.java)
notificationManager.notify(1, builder.build())
}
⚠️ 重要提示:
- 渠道一旦创建,用户可以在系统设置中修改渠道属性,应用无法再次修改
- 渠道 ID 是唯一的,不要随意更换
- 建议在应用首次启动时创建所有渠道
1.2 后台执行限制
变更说明: Android 8.0 对后台应用限制了 Service 的创建。
影响:
- 应用处于后台时,不再允许创建后台服务
- 需要使用
JobIntentService或WorkManager替代
适配方案:使用 JobIntentService
// 定义 JobIntentService
class MyBackgroundService : JobIntentService() {
companion object {
private const val JOB_ID = 1000
fun enqueueWork(context: Context, work: Intent) {
enqueueWork(context, MyBackgroundService::class.java, JOB_ID, work)
}
}
override fun onHandleWork(intent: Intent) {
// 在这里执行后台任务(已在独立线程中)
val data = intent.getStringExtra("data")
processData(data)
}
}
// 启动任务
val intent = Intent(context, MyBackgroundService::class.java).apply {
putExtra("data", "some data")
}
MyBackgroundService.enqueueWork(context, intent)
更好的方案:使用 WorkManager(推荐)
// 定义 Worker
class MyWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
override fun doWork(): Result {
val data = inputData.getString("data") ?: return Result.failure()
processData(data)
return Result.success()
}
}
// 调度任务
val workRequest = OneTimeWorkRequestBuilder<MyWorker>()
.setInputData(
Data.Builder().putString("data", "some data").build()
)
.build()
WorkManager.getInstance(context).enqueue(workRequest)
1.3 安装 APK 需要权限
变更说明: Android 8.0 开始,安装未知来源应用需要 REQUEST_INSTALL_PACKAGES 权限。
Manifest 声明:
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
请求安装权限:
fun installApk(context: Context, apkFile: File) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (!context.packageManager.canRequestPackageInstalls()) {
// 请求权限
val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply {
data = Uri.parse("package:${context.packageName}")
}
(context as Activity).startActivityForResult(intent, REQUEST_INSTALL_PERMISSION)
return
}
}
// 安装 APK
val uri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
apkFile
)
val installIntent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(uri, "application/vnd.android.package-archive")
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
context.startActivity(installIntent)
}
1.4 透明 Activity 限制
变更说明: Android 8.0 不允许透明主题的全屏 Activity。
适配方案:
<!-- ❌ 错误:透明主题 + 全屏 -->
<style name="AppTheme.Transparent">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowFullscreen">true</item>
</style>
<!-- ✅ 正确:如果需要悬浮效果,使用 Dialog 主题 -->
<style name="AppTheme.Dialog">
<item name="android:windowIsFloating">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
</style>
二、Android 9.0(API 28)适配要点
2.1 Apache HTTP 客户端移除
变更说明: Android 9.0 默认移除了 org.apache.http.legacy 库。
适配方案1:在 Manifest 中声明使用
<!-- AndroidManifest.xml -->
<application ...>
<uses-library
android:name="org.apache.http.legacy"
android:required="false" />
</application>
适配方案2:使用 HttpURLConnection 或第三方库(推荐)
// 使用 HttpURLConnection(系统自带,无需额外依赖)
fun httpGet(urlString: String): String {
val url = URL(urlString)
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "GET"
return connection.inputStream.bufferedReader().use { it.readText() }
}
// 或使用 OkHttp(推荐)
// implementation "com.squareup.okhttp3:okhttp:4.12.0"
val client = OkHttpClient()
val request = Request.Builder().url(urlString).build()
val response = client.newCall(request).execute()
val body = response.body?.string()
2.2 前台服务需要权限
变更说明: Android 9.0 开始,启动前台服务需要 FOREGROUND_SERVICE 权限。
Manifest 声明:
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
启动前台服务:
// 创建通知(Android 8.0+ 需要渠道)
val notification = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Notification.Builder(this, "default_channel_id")
.setContentTitle("服务运行中")
.setSmallIcon(R.drawable.ic_notification)
.build()
} else {
Notification.Builder(this)
.setContentTitle("服务运行中")
.setSmallIcon(R.drawable.ic_notification)
.build()
}
// 启动前台服务
startForeground(1, notification)
2.3 刘海屏(Display Cutout)适配
背景: Android 9.0 正式支持刘海屏(Notch)。
获取刘海区域:
fun getDisplayCutout(rootView: View) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val cutout = rootView.rootWindowInsets?.displayCutout
cutout?.let {
val boundingRect = it.boundingRects
// 处理刘海区域
boundingRect.forEach { rect ->
Log.d("Cutout", "刘海区域:$rect")
}
}
}
}
使用 WindowInsets 处理刘海:
// 在 Activity 的 onCreate 中
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
window.attributes.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
}
2.4 HTTPS 网络请求限制
变更说明: Android 9.0 默认禁止明文流量(HTTP 请求)。
适配方案1:启用明文流量(仅开发环境)
<!-- res/xml/network_security_config.xml -->
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config>
<domain includeSubdomains="true">example.com</domain>
<trust-anchors>
<certificates src="user" />
</trust-anchors>
</domain-config>
</network-security-config>
<!-- AndroidManifest.xml -->
<application
android:networkSecurityConfig="@xml/network_security_config"
... >
适配方案2:全量迁移到 HTTPS(推荐)
// 使用 HTTPS 进行网络请求
val url = "https://api.example.com/data"
三、Gradle 配置建议
// app/build.gradle
android {
compileSdk 34
defaultConfig {
minSdk 21 // 支持 Android 5.0+
targetSdk 34 // 紧跟最新版本
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
}
四、适配检查清单
完成 Android 8/9 适配后,建议对照以下清单进行检查:
-
targetSdk ≥ 26时,已创建通知渠道 - 后台任务已迁移到
WorkManager或JobIntentService - 安装 APK 功能已适配
REQUEST_INSTALL_PACKAGES权限 - 如果使用了
org.apache.http,已在 Manifest 中声明<uses-library> - 前台服务已声明
FOREGROUND_SERVICE权限 - 刘海屏设备已测试 UI 是否被遮挡
- 网络请求已全量使用 HTTPS(或已配置网络安全配置)
- 在 Android 8.x 和 9.0 设备上分别测试
五、参考资源
如果本文对你有帮助,欢迎点赞收藏。如有疑问,欢迎在评论区交流。