Android 11(API 30)引入了多项重要特性和权限变更,核心集中在存储权限、后台位置、包可见性、前台服务 等方面。本文基于 Kotlin 语言,从核心变更点、适配方案、代码示例等维度讲解适配要点。
一、核心变更概览
| 变更类别 | 核心影响 | 适配优先级 |
|---|---|---|
| 存储权限 | 废弃 WRITE_EXTERNAL_STORAGE,引入分区存储强制启用 |
高 |
| 后台位置权限 | 单独申请 ACCESS_BACKGROUND_LOCATION,需用户显式授权 |
高 |
| 包可见性 | 应用默认无法查询其他应用,需在清单声明可见范围 | 中 |
| 前台服务限制 | 新增类型限制,后台启动前台服务需特殊权限 | 中 |
| 权限对话框优化 | 新增「仅本次」选项,权限授权逻辑变更 | 中 |
| 悬浮窗权限 | SYSTEM_ALERT_WINDOW 权限申请流程变更 |
低 |
二、分区存储适配(最核心)
1. 变更说明
Android 11 强制启用分区存储(Scoped Storage),应用只能访问:
- 应用私有目录(
/Android/data/包名/):无需权限 - 媒体文件(图片/音频/视频):通过 MediaStore API 访问
- 下载文件:通过
DownloadManager或存储访问框架(SAF)
2. 适配方案
(1)基础配置(清单文件)
xml
<!-- 保留旧权限(兼容低版本),但Android 11不再生效 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<!-- Android 11访问媒体文件仅需读权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- 如需批量修改/删除媒体文件,需申请此权限 -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<!-- 可选:临时关闭分区存储(仅过渡用,不推荐) -->
<application
android:requestLegacyExternalStorage="true"
android:targetSdkVersion="30">
</application>
(2)读取媒体文件(Kotlin)
通过 MediaStore 查询图片示例:
kotlin
/**
* 读取设备中的图片文件
*/
fun queryImages(context: Context): List<Uri> {
val imageUris = mutableListOf<Uri>()
val projection = arrayOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.DATE_ADDED
)
// 按添加时间倒序查询
val sortOrder = "${MediaStore.Images.Media.DATE_ADDED} DESC"
context.contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection,
null,
null,
sortOrder
)?.use { cursor ->
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
while (cursor.moveToNext()) {
val id = cursor.getLong(idColumn)
val uri = ContentUris.withAppendedId(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
id
)
imageUris.add(uri)
}
}
return imageUris
}
(3)写入媒体文件(Kotlin)
保存图片到公共相册示例:
kotlin
/**
* 保存图片到公共相册
* @param bitmap 要保存的图片
* @param displayName 文件名
*/
fun saveImageToGallery(context: Context, bitmap: Bitmap, displayName: String): Uri? {
val contentValues = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, displayName)
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/MyApp") // 保存到Pictures/MyApp目录
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(MediaStore.Images.Media.IS_PENDING, 1) // 标记为待处理
}
}
val contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
var uri: Uri? = null
try {
uri = context.contentResolver.insert(contentUri, contentValues)
uri?.let {
context.contentResolver.openOutputStream(it)?.use { outputStream ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
contentValues.clear()
contentValues.put(MediaStore.Images.Media.IS_PENDING, 0)
context.contentResolver.update(it, contentValues, null, null)
}
}
} catch (e: Exception) {
uri?.let {
context.contentResolver.delete(it, null, null)
}
e.printStackTrace()
}
return uri
}
(4)批量操作媒体文件(需 MANAGE_EXTERNAL_STORAGE 权限)
kotlin
/**
* 检查是否有管理所有文件的权限
*/
fun hasManageExternalStoragePermission(context: Context): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Environment.isExternalStorageManager()
} else {
true
}
}
/**
* 申请管理所有文件的权限
*/
fun requestManageExternalStoragePermission(activity: Activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) {
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION).apply {
data = Uri.parse("package:${activity.packageName}")
}
activity.startActivityForResult(intent, REQUEST_CODE_MANAGE_STORAGE)
}
}
三、后台位置权限适配
1. 变更说明
Android 11 要求后台位置权限需单独申请 ,且必须先获得前台位置权限(ACCESS_FINE_LOCATION/ACCESS_COARSE_LOCATION)。
2. 适配代码
(1)清单声明
xml
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
(2)权限申请逻辑
kotlin
// 权限请求码
private const val REQUEST_CODE_LOCATION = 1001
/**
* 检查并申请位置权限
*/
fun checkAndRequestLocationPermission(activity: Activity) {
val foregroundPermissions = arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
)
// 先检查前台位置权限
if (ContextCompat.checkSelfPermission(activity, foregroundPermissions[0]) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(activity, foregroundPermissions, REQUEST_CODE_LOCATION)
} else {
// 前台权限已获取,申请后台位置权限(Android 11+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
ContextCompat.checkSelfPermission(activity, Manifest.permission.ACCESS_BACKGROUND_LOCATION) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(
activity,
arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION),
REQUEST_CODE_LOCATION
)
}
}
}
/**
* 处理权限申请结果
*/
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_CODE_LOCATION) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 权限已授予
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
permissions.contains(Manifest.permission.ACCESS_BACKGROUND_LOCATION)) {
// 后台位置权限处理
}
} else {
// 权限被拒绝,引导用户到设置页开启
if (!ActivityCompat.shouldShowRequestPermissionRationale(this, permissions[0])) {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.parse("package:$packageName")
}
startActivity(intent)
}
}
}
}
四、包可见性适配
1. 变更说明
Android 11 限制应用查询设备上其他应用的信息,需在 AndroidManifest.xml 中声明需要访问的应用包名或意图。
2. 适配方案
(1)声明可见应用(清单文件)
xml
<queries>
<!-- 允许查询特定包名的应用 -->
<package android:name="com.example.targetapp" />
<!-- 允许查询支持特定意图的应用 -->
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="http" />
</intent>
<!-- 允许查询所有已安装应用(仅特殊场景使用) -->
<package android:name="androidx.core.content.pm.PackageManagerCompat" />
</queries>
(2)查询应用是否安装(Kotlin)
kotlin
/**
* 检查应用是否安装(Android 11+适配)
*/
fun isAppInstalled(context: Context, packageName: String): Boolean {
return try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.packageManager.getPackageInfo(
packageName,
PackageManager.PackageInfoFlags.of(0)
)
} else {
@Suppress("DEPRECATION")
context.packageManager.getPackageInfo(packageName, 0)
}
true
} catch (e: PackageManager.NameNotFoundException) {
false
}
}
五、前台服务适配
1. 变更说明
Android 11 要求前台服务必须指定类型(如 location、mediaPlayback 等),且禁止后台应用启动前台服务(特殊场景需申请 SYSTEM_ALERT_WINDOW 权限)。
2. 适配代码
(1)清单声明前台服务类型
xml
<service
android:name=".MyForegroundService"
android:foregroundServiceType="location|mediaPlayback">
<intent-filter>
<action android:name="com.example.MyForegroundService" />
</intent-filter>
</service>
(2)启动前台服务(Kotlin)
kotlin
/**
* 启动前台服务(Android 11+适配)
*/
fun startForegroundService(context: Context) {
val serviceIntent = Intent(context, MyForegroundService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(serviceIntent)
} else {
context.startService(serviceIntent)
}
}
// 服务内部实现
class MyForegroundService : Service() {
private val NOTIFICATION_ID = 1001
private val CHANNEL_ID = "ForegroundServiceChannel"
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
createNotificationChannel()
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("前台服务标题")
.setContentText("前台服务内容")
.setSmallIcon(R.drawable.ic_notification)
.build()
// Android 11+需指定服务类型
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION)
} else {
startForeground(NOTIFICATION_ID, notification)
}
// 业务逻辑处理
return START_STICKY
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
"前台服务通道",
NotificationManager.IMPORTANCE_DEFAULT
)
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(channel)
}
}
override fun onBind(intent: Intent): IBinder? = null
}
六、其他适配要点
1. 权限对话框「仅本次」选项处理
Android 11 新增「仅本次」权限授权选项,需处理权限临时授权的场景:
kotlin
/**
* 检查权限是否为临时授权(仅本次)
*/
fun isPermissionTemporaryGranted(activity: Activity, permission: String): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
ActivityCompat.checkSelfPermission(activity, permission) == PackageManager.PERMISSION_GRANTED &&
!ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)
} else {
false
}
}
2. 悬浮窗权限适配
kotlin
/**
* 检查悬浮窗权限
*/
fun hasOverlayPermission(context: Context): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Settings.canDrawOverlays(context)
} else {
true
}
}
/**
* 申请悬浮窗权限
*/
fun requestOverlayPermission(activity: Activity, requestCode: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(activity)) {
val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION).apply {
data = Uri.parse("package:${activity.packageName}")
}
activity.startActivityForResult(intent, requestCode)
}
}
七、适配建议
- 分阶段适配:先兼容 Android 11 核心变更(存储、位置),再处理次要特性;
- 测试覆盖:在 Android 11 真机/模拟器上测试,重点验证权限申请、文件操作、后台服务;
- 兼容低版本 :使用
Build.VERSION.SDK_INT做版本判断,保证低版本正常运行; - 避免滥用权限 :
MANAGE_EXTERNAL_STORAGE仅在必要时申请,优先使用分区存储 API; - 遵循官方规范 :定期查看 Android 11 官方适配文档 跟进最新变更。
八、参考资源
以上适配方案覆盖 Android 11 核心变更点,可根据项目实际需求调整代码逻辑。