Android 11适配实战:从‘分区存储’到‘软件包可见性’,一个老项目的踩坑与填坑全记录
Android 11适配实战:从存储权限到应用交互的全面升级指南
当我们的开发团队第一次将老项目的targetSdkVersion升级到30时,就像打开了一个潘多拉魔盒——各种意想不到的问题接踵而至。这个已经稳定运行了三年的应用,在Android 11设备上突然出现了图片无法保存、第三方分享失效、视频播放崩溃等一系列问题。本文将分享我们在适配过程中遇到的真实挑战和解决方案,特别适合那些正在维护历史包袱较重的Android应用的开发者。
1. 存储权限的深度适配策略
在Android 11上,存储权限的变化无疑是最大的挑战之一。我们项目中有大量文件操作代码,从用户头像保存到日志文件输出,都需要重新审视。经过两周的密集测试,我们总结出三种主要适配方案,每种都有其适用场景和潜在风险。
1.1 MediaStore API的全面应用
对于媒体文件(图片、视频、音频),MediaStore API是最规范的访问方式。我们发现直接使用File API虽然在某些情况下仍然有效,但会带来明显的性能损耗——随机读写速度下降约50%。以下是典型的图片保存代码示例:
public Uri saveImageToGallery(Context context, Bitmap bitmap) throws IOException { String displayName = "IMG_" + System.currentTimeMillis() + ".jpg"; ContentValues values = new ContentValues(); values.put(MediaStore.Images.Media.DISPLAY_NAME, displayName); values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg"); values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/MyApp"); ContentResolver resolver = context.getContentResolver(); Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); try (OutputStream out = resolver.openOutputStream(uri)) { bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out); } return uri; }关键注意事项:
- 使用RELATIVE_PATH指定子目录,避免文件散乱
- 记得检查返回的Uri是否为null(存储空间不足时可能发生)
- 对于批量操作,考虑使用批量插入API提高性能
1.2 MANAGE_EXTERNAL_STORAGE权限的谨慎使用
我们的应用有一个文件清理功能,需要扫描整个存储空间。这种情况下,我们不得不考虑使用MANAGE_EXTERNAL_STORAGE权限。但要注意,Google Play对这项权限的审核非常严格。
申请流程代码示例:
public static void requestStorageManagementPermission(Activity activity) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) { Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION); activity.startActivityForResult(intent, REQUEST_CODE_STORAGE_PERMISSION); } }使用建议:
- 仅在绝对必要时申请此权限
- 准备充分的理由说明为什么MediaStore和SAF无法满足需求
- 国内应用市场可能要求较低,但仍应保持克制
1.3 私有目录与共享目录的选择
我们发现很多开发者忽略了Android/media目录的特殊性——它既可以被应用私有访问,又能被其他应用通过MediaStore读取。这对于需要跨应用共享但又不想完全公开的文件非常有用。
// 获取应用专属的共享媒体目录 File sharedMediaDir = context.getExternalMediaDirs()[0]; File outputFile = new File(sharedMediaDir, "temp_video.mp4");2. 软件包可见性对应用交互的影响
Android 11引入的软件包可见性限制,对我们应用中第三方登录、支付和分享功能造成了严重影响。最典型的问题是,我们无法再通过常规方式检测微信是否安装。
2.1 精确声明需要的包名
在AndroidManifest.xml中添加queries元素是最规范的解决方案:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.myapp"> <queries> <!-- 微信 --> <package android:name="com.tencent.mm" /> <!-- 支付宝 --> <package android:name="com.eg.android.AlipayGphone" /> <!-- 微博 --> <package android:name="com.sina.weibo" /> </queries> ... </manifest>2.2 通用Intent的声明方式
如果不需要特定包名,而是想查询所有能处理某类Intent的应用,可以这样声明:
<queries> <intent> <action android:name="android.intent.action.SEND" /> <data android:mimeType="image/*" /> </intent> </queries>2.3 避免过度声明
我们发现有些开发者倾向于声明QUERY_ALL_PACKAGES权限或列出几十个包名,这会导致:
- Google Play审核风险
- 不必要的隐私疑虑
- 未来维护困难
最佳实践:
- 只声明确实需要的包名
- 对于不常见的应用,考虑使用Intent直接启动而不预先检查
- 定期审查queries列表,移除不再使用的声明
3. 权限模型的精细化适配
Android 11对权限系统做了多项调整,我们需要特别注意以下几点:
3.1 单次权限的生命周期管理
位置、麦克风和摄像头权限现在支持单次授权。我们需要正确处理权限的临时性:
@Override protected void onResume() { super.onResume(); if (checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { // 权限可能已被撤销,需要重新检查 initCamera(); } }3.2 后台位置权限的独立申请
Android 11要求先获取前台位置权限,再单独申请后台权限。我们实现了分步申请流程:
private void requestLocationPermissions() { if (checkSelfPermission(ACCESS_FINE_LOCATION) != PERMISSION_GRANTED) { // 第一步:申请前台权限 requestPermissions(new String[]{ACCESS_FINE_LOCATION}, REQUEST_FOREGROUND_LOCATION); } else if (checkSelfPermission(ACCESS_BACKGROUND_LOCATION) != PERMISSION_GRANTED) { // 第二步:单独申请后台权限 new AlertDialog.Builder(this) .setMessage("需要后台位置权限以持续跟踪") .setPositiveButton("确定", (d, w) -> { requestPermissions(new String[]{ACCESS_BACKGROUND_LOCATION}, REQUEST_BACKGROUND_LOCATION); }) .show(); } }3.3 权限自动重置的处理
长时间未使用的应用会被系统自动重置权限。我们添加了检测逻辑:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !getPackageManager().isAutoRevokeWhitelisted()) { showPermissionResetWarning(); }4. 其他关键变更与疑难问题
4.1 前台服务类型的强制声明
访问摄像头或麦克风的前台服务现在需要明确声明类型:
<service android:name=".CameraService" android:foregroundServiceType="camera" />4.2 安装APK的行为变更
我们发现Android 11上安装APK时应用会被杀死,这影响了我们的自动更新流程。解决方案是:
public static void installApk(Context context, File apkFile) { Intent install = new Intent(Intent.ACTION_VIEW); Uri uri = FileProvider.getUriForFile(context, AUTHORITY, apkFile); install.setDataAndType(uri, "application/vnd.android.package-archive"); install.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); install.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // 启动安装前保存关键状态 saveApplicationState(); context.startActivity(install); }4.3 指针标记导致的Native崩溃
我们使��的某个视频播放SDK在Android 11上频繁崩溃,日志显示是内存访问问题。临时解决方案是在AndroidManifest中添加:
<application android:allowNativeHeapPointerTagging="false"> ... </application>但需要注意这只是权宜之计,长期应该更新SDK版本。
5. 调试与测试工具
5.1 兼容性框架调试工具
Android 11新增的兼容性调试工具让我们能够逐个测试各项变更,而不必立即升级targetSdkVersion。使用方法:
- 在开发者选项中启用"应用兼容性变更"
- 选择要测试的应用
- 针对特定变更开启/关闭开关
5.2 无线调试的实践
虽然Android 11的无线调试功能理念很好,但我们的体验并不理想:
- 连接稳定性差
- 传输速度慢
- 锁屏后容易断开
建议重要调试工作仍使用有线连接,无线调试仅作为辅助手段。
