Android11 新特性与适配指南

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 要求前台服务必须指定类型(如 locationmediaPlayback 等),且禁止后台应用启动前台服务(特殊场景需申请 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)
    }
}

七、适配建议

  1. 分阶段适配:先兼容 Android 11 核心变更(存储、位置),再处理次要特性;
  2. 测试覆盖:在 Android 11 真机/模拟器上测试,重点验证权限申请、文件操作、后台服务;
  3. 兼容低版本 :使用 Build.VERSION.SDK_INT 做版本判断,保证低版本正常运行;
  4. 避免滥用权限MANAGE_EXTERNAL_STORAGE 仅在必要时申请,优先使用分区存储 API;
  5. 遵循官方规范 :定期查看 Android 11 官方适配文档 跟进最新变更。

八、参考资源

以上适配方案覆盖 Android 11 核心变更点,可根据项目实际需求调整代码逻辑。

相关推荐
习惯就好zz3 小时前
在安卓设备上测试 AWS S3 下载速度的完整指南
android·aws·速度测试
_李小白11 小时前
【Android FrameWork】延伸阅读:SurfaceFlinger线程
android
csdn122598733611 小时前
JetPack Compose 入门先搞清楚
android·compose·jetpack
liang_jy12 小时前
Android LaunchMode
android·面试
阿里云云原生13 小时前
Android App 崩溃排查实战:如何利用 RUM 完整数据与符号化技术定位问题?
android·阿里云·云原生·rum
过期动态14 小时前
JDBC高级篇:优化、封装与事务全流程指南
android·java·开发语言·数据库·python·mysql
没有了遇见16 小时前
Android 音乐播放器之MotionLayout实现View流畅变换
android
TheNextByte116 小时前
在 PC 和Android之间同步音乐的 4 种方法
android
君莫啸ོ17 小时前
Android基础-Activity属性 android:configChanges
android