news 2026/10/14 8:56:15

Android 11适配核心指南:分区存储、后台定位与隐私新规实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android 11适配核心指南:分区存储、后台定位与隐私新规实战

1. 这不是升级,是重新校准开发坐标系

Android 11 发布时,很多团队第一反应是“又来了”,点开官方文档扫两眼,看到“targetSdkVersion=30”就直接改了 build.gradle,然后打包、测试、上线——结果在灰度阶段突然发现:后台定位权限失效、文件访问报错、通知渠道崩溃、甚至某些老设备上应用直接闪退。这不是 Bug,是 Android 系统底层对应用行为边界的重新划界。它不再容忍“历史遗留式兼容”,而是强制所有以 Android 11 为目标平台的应用,必须按新规则重写与系统交互的逻辑链路。

我参与过三个不同体量项目的 Android 11 适配,最小的是一个纯工具类 App(日活约 8 万),最大的是一个跨平台 SDK 集成项目(覆盖 20+ 子应用)。它们无一例外,在首次提测时都暴露出至少 5 类典型异常,其中 3 类无法通过简单 try-catch 规避,必须重构调用路径。核心原因在于:Android 11 不再是“功能增强”,而是“契约重签”。它把过去十年里开发者靠“绕过”“降级”“静默兼容”维持的灰色地带,全部收归为明确的、不可协商的 API 行为契约。

关键词里虽然没填,但实际适配中绕不开的硬核节点就这几个:分区存储(Scoped Storage)强制启用、后台位置权限收紧、一次性权限(One-time Permission)机制落地、通知渠道分组与默认设置变更、Activity 启动限制(特别是隐式 Intent)、以及 Package Visibility 机制引入。这些不是可选优化项,而是 targetSdkVersion=30 的编译期硬性门槛。你改不改,系统都会按新规则执行;你没改,系统就替你“拒绝执行”。

适合谁来读?如果你正在维护一个已上线超过 18 个月的 Android 应用,且尚未将 targetSdkVersion 升至 30;或者你正准备启动一个新项目,想避开从第一天就埋下的兼容雷区;又或者你是 SDK 提供方,需要确保你的库在 Android 11+ 设备上不触发系统拦截——那这篇就是你接下来两周每天要翻三遍的实操手册。它不讲原理图解,不堆概念定义,只告诉你:每一处变更背后,系统到底在拦什么、为什么拦、你该砍哪段代码、换哪条调用路径、以及实测下来最稳的兜底方案是什么。

2. 分区存储:从“文件系统自由”到“沙盒主权移交”

2.1 为什么这次必须动真格?

Android 10 引入 Scoped Storage 是试探,Android 11 是最终裁决。此前,应用只要声明WRITE_EXTERNAL_STORAGE权限,就能在公共目录(如 DCIM、Download、Pictures)里随意读写。这导致两个严重问题:一是恶意应用批量窃取用户照片、文档;二是大量 App 把缓存、数据库、临时文件全塞进 /sdcard/Android/data/ 下,造成空间碎片化,用户手动清理时根本分不清哪些能删、哪些删了就崩。Google 的解决思路很直接:把外部存储的“主权”还给系统,应用只拥有自己的沙盒(app-specific directory)和明确授权的媒体集合。

关键转折点在于:Android 11 起,requestLegacyExternalStorage=true 彻底失效。你在 AndroidManifest.xml 里加这行,对 targetSdkVersion=30 的应用完全不起作用。系统会无视它,强制启用分区存储。这不是 bug,是设计使然——它堵死了所有“假装没升级”的后门。

2.2 公共目录访问:MediaStore 是唯一合法通道

过去你可能这样保存一张截图:

// ❌ Android 11 下必然失败(targetSdk=30) File dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); File file = new File(dir, "screenshot_" + System.currentTimeMillis() + ".png"); FileOutputStream fos = new FileOutputStream(file); bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos);

这段代码在 Android 11 上会抛出SecurityException,因为getExternalStoragePublicDirectory()返回的路径已被系统标记为“受限访问”。正确路径只有一条:通过 MediaStore 插入媒体项。

实操步骤如下:

  1. 构造 ContentValues,指定媒体类型与相对路径

    ContentValues values = new ContentValues(); values.put(MediaStore.Images.Media.DISPLAY_NAME, "screenshot_" + System.currentTimeMillis() + ".png"); values.put(MediaStore.Images.Media.MIME_TYPE, "image/png"); // 关键:指定相对路径,让系统决定最终存储位置 values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/MyAppScreenshots/");
  2. 通过 ContentResolver 插入,获取 Uri

    Uri uri = getContentResolver().insert( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values );
  3. 用 OutputStream 写入数据(注意:不是 FileOutputStream)

    if (uri != null) { try (OutputStream os = getContentResolver().openOutputStream(uri)) { bitmap.compress(Bitmap.CompressFormat.PNG, 100, os); } }

提示:RELATIVE_PATH的值必须是系统预定义的常量(如DIRECTORY_PICTURES、DIRECTORY_DCIM、DIRECTORY_MOVIES),不能自定义任意路径。否则插入会失败。实测发现,若传入"/MyApp/Cache/"这类非法路径,insert()返回 null,且无明确异常提示,极易被忽略。

2.3 应用专属目录:安全但需主动迁移

/sdcard/Android/data/<package_name>/这个路径在 Android 11 下依然可读写,且无需任何权限。但它有个致命缺陷:当用户卸载应用时,此目录下所有内容被自动清除。这意味着,如果你过去把用户导出的 Excel 报表、离线地图包、加密密钥文件存在这里,升级后用户一卸载就永久丢失。

解决方案是:将重要用户数据迁移到 MediaStore 管理的公共目录,而仅把真正属于应用运行时的缓存(如图片缩略图、网络响应临时文件)留在专属目录。迁移过程必须带用户感知:

  • 对于导出类文件(报表、PDF),在用户点击“导出”时,直接走 MediaStore 流程,保存到DIRECTORY_DOCUMENTS;
  • 对于大体积离线资源(如地图瓦片),改用MediaStore.Downloads.EXTERNAL_CONTENT_URI,并设置IS_PENDING=1标记下载中状态,完成后设为0;
  • 对于纯缓存(如 Glide 的磁盘缓存),保留专属目录,但需在onTrimMemory()中主动清理过期项,避免占满空间。

我负责的一个工具类 App 在迁移时踩过坑:初期把所有文件都往DIRECTORY_DOWNLOADS塞,结果用户反馈“找不到导出的文件”。查因发现,部分文件管理器默认隐藏Downloads目录下的非.apk文件。最终方案是:对用户可见的导出文件,统一存到DIRECTORY_DOCUMENTS,并在通知栏发送一条“文件已保存至文档目录”的提示,附上 ACTION_VIEW Intent 直达该文件。

2.4 Legacy 模式彻底终结:没有“兼容开关”这回事

有些团队寄希望于“先切到 Android 11,但保持旧行为”,这是最大误区。Android 11 的构建工具链(AGP 4.1+)在编译时会校验targetSdkVersion,若为 30,则自动移除requestLegacyExternalStorage的 manifest 属性,即使你手写上去也无效。我们曾用反编译验证:APK 的 AndroidManifest.xml 中该属性确实消失。

更隐蔽的陷阱是android:preserveLegacyExternalStorage。这个属性在 Android 11 中仅对已安装应用的升级包有效,且只在升级瞬间起作用——它允许旧版应用在升级后的一小段时间内继续使用旧存储模型,但一旦用户杀掉进程重启,系统立即强制切换。它不是长期开关,而是“过渡缓冲期”。新安装的应用(包括首次安装或清除数据后重装)完全不受影响,直接受限。

结论很清晰:不存在平滑过渡,只有硬切换。你的适配工作必须在发版前完成,而不是指望用户升级后给你留出“适应时间”。

3. 后台位置权限:从“默认放行”到“显式授权链”

3.1 权限模型的本质重构

Android 10 允许应用在前台获取位置后,后台继续监听(需声明ACCESS_BACKGROUND_LOCATION)。Android 11 将其升级为“双授权链”:用户必须先授予前台位置权限,再单独授予后台位置权限,且两次授权必须由用户主动触发,系统不会合并弹窗。更关键的是:如果用户只给了前台权限,应用在后台尝试调用requestLocationUpdates()会直接抛出SecurityException,而非静默失败。

这改变了整个定位架构的设计逻辑。过去常见的“保活定位服务”模式(如后台 Service 持续上报位置)在 Android 11 下彻底失效。你不能再假设“只要用户点过一次同意,我就能永远后台跑”。

3.2 后台定位的合规路径:仅限四类场景

Google 明确规定,只有以下四类应用可申请后台位置权限,且需在 Play Console 提交详细说明并通过人工审核:

  • 导航类应用:用户正在使用导航功能(Activity 在前台);
  • 出行服务类:如打车、共享单车,用户已发起行程且等待接单;
  • 健康与健身类:用户主动开启运动记录(如跑步、骑行);
  • 家庭看护类:如老人防走失设备,需持续上报位置。

其他所有场景(如“附近商家推送”“天气自动定位”“社交动态更新”)均被禁止后台获取位置。我们曾为一个本地生活 App 申请后台定位权限,理由是“为用户提供实时周边优惠”,被 Play Console 直接拒审,回复是:“该功能可在前台触发,无需后台持续获取”。

3.3 实操中的权限链路设计

合规的后台定位流程必须包含三个强制环节:

  1. 前台权限前置校验
    在申请后台权限前,必须确保前台权限已授予:

    if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { // 先申请前台权限 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, REQUEST_CODE_FOREGROUND_LOCATION); return; }
  2. 后台权限独立申请
    前台权限授予后,再单独申请后台权限:

    // 注意:必须用 ACCESS_BACKGROUND_LOCATION,不能混用 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION}, REQUEST_CODE_BACKGROUND_LOCATION);
  3. 后台服务启动时机控制
    即使权限已获,后台定位服务也只能在用户明确处于相关场景时启动。例如:

    • 导航类:仅当导航 Activity 处于前台且用户点击“开始导航”按钮后;
    • 运动类:仅当运动记录 Fragment 可见且用户点击“开始计时”后;
    • 所有后台服务必须绑定FOREGROUND_SERVICE类型,并显示持续通知(Notification Channel ID 必须为IMPORTANCE_LOW或更高)。

注意:startForegroundService()启动的服务,若在 5 秒内未调用startForeground(),系统会抛出ForegroundServiceDidNotStartInTimeException并杀死服务。这是 Android 11 新增的硬性超时机制,必须严格遵守。

3.4 替代方案:前台定位 + 智能唤醒

对于无法通过审核的场景(如天气 App 的自动定位),可行的替代路径是:放弃后台持续定位,改为前台高频定位 + 智能唤醒。

具体做法:

  • 应用在前台时,每 15 分钟请求一次高精度位置(PRIORITY_HIGH_ACCURACY);
  • 将位置结果缓存到本地数据库;
  • 当用户下次打开 App 时,优先读取最近一次缓存位置,若超过 30 分钟则触发一次新定位;
  • 结合 JobIntentService,在系统空闲时(JobInfo.NETWORK_TYPE_UNMETERED)同步位置数据,避免蜂窝网络消耗。

实测数据显示,这种方案在用户日均打开 App 3 次的前提下,位置更新延迟平均为 22 分钟,完全满足天气、新闻等场景需求,且零后台耗电。

4. 一次性权限与 Package Visibility:隐私墙的双重加固

4.1 一次性权限:从“永久授权”到“本次有效”

Android 11 引入PermissionController的核心变化是:所有危险权限(位置、麦克风、摄像头、身体传感器)默认以“仅本次”方式授予。用户在权限弹窗中选择“允许”,实际获得的是单次会话权限;下次应用进入前台,需重新申请。

这对依赖长期权限的逻辑产生连锁反应。例如,一个视频会议 App 过去在启动时申请摄像头权限,用户点“允许”后,整个会话周期内都可用。现在,用户点“允许”后,仅当前 Activity 可用;若用户切换到其他 App 再切回,摄像头会黑屏,必须再次申请。

解决方案是:将权限申请时机下沉到具体功能触发点,而非应用启动时。

  • 摄像头:在用户点击“开启视频”按钮时才申请;
  • 麦克风:在用户点击“开始语音输入”时才申请;
  • 位置:在用户点击“查找附近”时才申请。

同时,必须处理onRequestPermissionsResult()中的PackageManager.PERMISSION_DENIED状态。过去开发者习惯忽略拒绝情况,现在必须提供明确引导:

if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_DENIED) { // 检查是否勾选了“不再询问” if (!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) { // 弹出自定义 Dialog,解释为什么需要该权限,并提供跳转设置页入口 showPermissionRationaleDialog(); } }

提示:shouldShowRequestPermissionRationale()在 Android 11 下行为更严格。若用户在系统设置中手动关闭权限(而非在弹窗点拒绝),此方法返回 false,此时必须引导用户去设置页手动开启,不能仅靠弹窗说服。

4.2 Package Visibility:看不见的“应用通讯黑名单”

这是 Android 11 最易被忽视却影响最广的变更。过去,应用可通过PackageManager.getPackageInfo()或queryIntentActivities()获取设备上所有已安装应用信息。Android 11 要求:若要查询其他应用,必须在 AndroidManifest.xml 中显式声明<queries>元素。

例如,你的 App 需要调用微信分享,就必须声明:

<queries> <package android:name="com.tencent.mm" /> </queries>

若需查询所有浏览器应用以提供“在浏览器中打开”选项,则需:

<queries> <intent> <action android:name="android.intent.action.VIEW" /> <data android:scheme="http" /> </intent> </queries>

未声明的查询将返回空列表。我们遇到的真实案例:一个工具类 App 内置“清理加速”功能,需扫描所有运行中进程。升级后,ActivityManager.getRunningAppProcesses()返回的进程列表为空,导致清理功能失效。根因是未在<queries>中声明目标包名。修复方案是:列出所有可能被清理的系统应用包名(如com.android.systemui,com.android.phone),并添加通用 intent 查询。

更复杂的情况是 SDK 集成。某支付 SDK 内部会检测设备是否安装支付宝,其代码调用getPackageInfo("com.eg.android.AlipayGphone")。若主 App 未在<queries>中声明该包名,调用返回 null,SDK 误判为“未安装支付宝”,直接禁用支付宝支付入口。解决方案是:在集成 SDK 的文档中,强制要求客户在<queries>中添加 SDK 所需的所有包名,并在 SDK 初始化时做运行时校验,若检测失败则抛出明确异常。

4.3 隐私信息访问的“最小必要”原则落地

Android 11 将“最小必要”从理念变为强制技术约束。例如:

  • 剪贴板访问:应用在后台时,无法读取剪贴板内容(ClipboardManager.getPrimaryClip()返回 null)。仅当前前台应用可读;
  • 传感器访问:后台应用无法访问身体传感器(如心率、步数),SensorManager.getDefaultSensor(Sensor.TYPE_HEART_RATE)返回 null;
  • Wi-Fi 信息:WifiManager.getConnectionInfo()在 Android 11+ 返回的 SSID 和 BSSID 均为<unknown>,除非应用持有ACCESS_FINE_LOCATION权限且用户处于前台。

这些限制意味着:所有依赖后台获取敏感信息的功能必须重构。例如,一个健康 App 原计划在后台持续读取心率传感器数据,现在必须改为:用户开启运动模式后,前台 Activity 持有传感器引用,运动结束时停止监听。后台仅负责数据上传与分析,不参与采集。

5. 通知渠道与 Activity 启动:从“自由调用”到“显式契约”

5.1 通知渠道:分组、默认与用户控制权上收

Android 8.0 引入通知渠道,Android 11 进一步强化其管控力。核心变化有三点:

  1. 渠道分组强制化:所有渠道必须归属某个组(createNotificationChannelGroup()),且组 ID 必须在创建渠道前注册。未分组的渠道在 Android 11 下创建失败。
  2. 默认渠道行为变更:NotificationManager.createNotificationChannel()若传入null作为渠道 ID,系统不再创建默认渠道,而是抛出IllegalArgumentException。
  3. 用户控制粒度细化:用户可在设置中对每个渠道组单独开关“允许通知”、“允许声音”、“允许震动”,应用无法绕过。

实操要点:

  • 分组设计需符合用户认知:例如,消息类 App 可分“聊天消息”、“系统通知”、“营销推广”三组;每组内再建具体渠道(如“好友消息”、“群聊消息”)。
  • 渠道创建必须幂等:在 Application.onCreate() 中检查渠道是否存在,不存在则创建。避免每次启动都重复创建导致崩溃。
  • 默认渠道必须显式命名:不要依赖null,而是定义一个CHANNEL_ID_DEFAULT = "default",并为其设置合理名称与重要性。
// 创建分组 NotificationChannelGroup group = new NotificationChannelGroup( "message_group", "聊天消息"); notificationManager.createNotificationChannelGroup(group); // 创建渠道并指定分组 NotificationChannel channel = new NotificationChannel( "friend_message", "好友消息", IMPORTANCE_HIGH); channel.setGroup("message_group"); // 关键:绑定分组 notificationManager.createNotificationChannel(channel);

5.2 Activity 启动限制:隐式 Intent 的“死刑判决”

Android 11 对隐式 Intent(即不指定 ComponentName,仅靠 Action/Category/DataType 匹配)施加严格限制。以下场景将直接失败:

  • 启动android.intent.action.VIEW且 Scheme 为http/https的 Activity,若目标应用未在<queries>中声明;
  • 启动android.intent.action.SEND分享文本,若目标应用未声明<queries>;
  • 通过PackageManager.resolveActivity()查询匹配 Activity,若未声明<queries>则返回 null。

最典型的崩溃点是:分享功能。过去常用Intent.createChooser()构造分享 Intent,现在必须确保<queries>中包含所有可能的分享目标(如微信、QQ、钉钉的包名),否则resolveActivity()返回 null,startActivity()抛出ActivityNotFoundException。

解决方案是:对分享 Intent 做双重兜底。

  1. 先查询<queries>中声明的目标;
  2. 若无匹配,降级为“复制链接到剪贴板”;
  3. 同时提供“手动选择应用”入口,跳转至系统分享面板(Intent.createChooser()本身仍可用,但需确保 Intent 有足够信息)。
Intent shareIntent = new Intent(Intent.ACTION_SEND); shareIntent.setType("text/plain"); shareIntent.putExtra(Intent.EXTRA_TEXT, "分享内容"); // 检查是否有可处理的应用 if (shareIntent.resolveActivity(getPackageManager()) != null) { startActivity(Intent.createChooser(shareIntent, "分享到")); } else { // 降级:复制到剪贴板 ClipboardManager cm = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); ClipData clip = ClipData.newPlainText("分享链接", "https://example.com"); cm.setPrimaryClip(clip); Toast.makeText(this, "链接已复制", Toast.LENGTH_SHORT).show(); }

5.3 Pending Intent 的显式化:安全边界的最后一道闸

Android 11 要求:所有 PendingIntent 必须指定 FLAG_IMMUTABLE 或 FLAG_MUTABLE。未指定者,系统抛出IllegalArgumentException。

  • FLAG_IMMUTABLE:PendingIntent 内容不可被接收方修改,适用于大多数场景(如通知点击跳转);
  • FLAG_MUTABLE:允许接收方修改 Intent 内容,仅用于必须动态注入数据的场景(如 Widget 更新)。

错误示范:

// ❌ Android 11 编译失败 PendingIntent pendingIntent = PendingIntent.getActivity( this, 0, intent, 0); // 第四个参数为 0,非法

正确写法:

// ✅ 推荐:绝大多数场景用 IMMUTABLE PendingIntent pendingIntent = PendingIntent.getActivity( this, 0, intent, PendingIntent.FLAG_IMMUTABLE); // ✅ 仅 Widget 等需修改 Intent 的场景用 MUTABLE PendingIntent widgetPendingIntent = PendingIntent.getBroadcast( this, 0, intent, PendingIntent.FLAG_MUTABLE);

注意:FLAG_IMMUTABLE是 Android 11 的默认推荐,FLAG_MUTABLE因安全风险较高,Google 强烈建议避免使用,除非绝对必要。我们曾在一个桌面 Widget 项目中因误用FLAG_MUTABLE导致审核被拒,整改后改用FLAG_IMMUTABLE+ 预置多套 Intent 模板,通过PendingIntent.getActivities()组合实现相同效果。

6. 适配 checklist 与灰度发布实战经验

6.1 不可跳过的 12 项硬性检查

在正式提测前,必须逐项确认以下清单。任何一项未通过,都可能导致线上崩溃或功能失效:

检查项检查方法未通过表现修复方案
1.targetSdkVersion=30查 build.gradle编译失败或行为不一致修改build.gradle中compileSdkVersion和targetSdkVersion
2.requestLegacyExternalStorage移除反编译 APK,查 AndroidManifest.xml存储异常仍存在删除 manifest 中该属性,重构存储逻辑
3. 公共目录写入走 MediaStore搜索代码中getExternalStoragePublicDirectorySecurityException替换为MediaStore.Images.Media.EXTERNAL_CONTENT_URI插入
4. 后台定位权限独立申请检查权限申请代码后台定位无响应拆分为前台+后台两次申请,且后台申请前校验前台权限
5.<queries>声明完整检查 AndroidManifest.xmlresolveActivity()返回 null根据所有 Intent 调用,补全<queries>声明
6. PendingIntent 标志位搜索PendingIntent.getActivity等调用IllegalArgumentException添加FLAG_IMMUTABLE或FLAG_MUTABLE
7. 通知渠道分组检查createNotificationChannelGroup()调用通知不显示创建渠道前先创建分组,渠道绑定分组 ID
8. 一次性权限处理检查onRequestPermissionsResult()功能间歇性失效在拒绝时引导用户去设置页,而非静默忽略
9. 剪贴板后台读取移除搜索ClipboardManager.getPrimaryClip()后台粘贴功能失效仅在前台 Activity 中读取剪贴板
10. Wi-Fi 信息访问降级搜索WifiManager.getConnectionInfo()SSID 显示<unknown>改用ConnectivityManager.getNetworkCapabilities()判断网络类型
11. Activity 启动 Intent 显式化搜索startActivity()无 ComponentNameActivityNotFoundException对关键跳转补充setComponentName(),或确保<queries>完整
12. 后台服务 Foreground 绑定检查startForegroundService()调用服务启动超时崩溃在 5 秒内调用startForeground(),并显示通知

6.2 灰度发布的三阶段策略

我们验证有效的灰度路径是分三阶段推进,而非一次性全量:

阶段一:内部真机白名单(3天)

  • 选取 5 款主流机型(覆盖 Samsung、Xiaomi、OPPO、vivo、华为),系统版本严格为 Android 11;
  • 安装包仅对内部测试人员开放,不走任何分发渠道;
  • 重点监控:Crashlytics 中SecurityException、ActivityNotFoundException、NullPointerException(因 MediaStore 返回 null)三类异常;
  • 此阶段目标:暴露所有编译期/运行时硬性崩溃,修复率需达 100%。

阶段二:小流量外网灰度(7天)

  • 通过 Firebase Remote Config 控制,向 0.5% 的 Android 11 用户推送新包;
  • 开启全量日志上报(脱敏后),重点采集:
    • MediaStore.insert()返回 null 的频率与机型分布;
    • 后台定位requestLocationUpdates()调用失败率;
    • <queries>未声明导致的resolveActivity()失败率;
  • 此阶段目标:验证真实用户环境下的稳定性,异常率需低于 0.1%。

阶段三:分批全量(14天)

  • 按机型维度分批:先推送给 Pixel 系列(原生系统,兼容性最好),再推送给 Samsung(One UI),最后推送给国内定制 ROM(MIUI、ColorOS);
  • 每批间隔 48 小时,期间监控 ANR 率、卡顿率、后台耗电增幅;
  • 若某批次 ANR 率突增 20%,立即暂停,回滚至前一版本,排查Activity启动阻塞或MediaStore插入耗时问题。

6.3 我踩过的三个“教科书级”坑

  1. “文件已存在”导致的 MediaStore 插入失败
    我们在保存用户头像时,为避免重名,生成了avatar_123456789.png。但 Android 11 的 MediaStore 对DISPLAY_NAME去重极严:若同名文件已存在,insert()直接返回 null,且无任何异常。修复方案是:插入前先用ContentResolver.query()查询同名文件是否存在,存在则追加时间戳后缀。

  2. 后台服务通知未设置setOngoing(true)
    一个音乐播放器 App 的后台播放服务,在 Android 11 下频繁被系统杀死。查因发现,其通知未调用setOngoing(true),导致系统判定为“可清理通知”,连带杀死关联服务。Android 11 对前台服务的通知要求更严:必须设置setOngoing(true)且setAutoCancel(false),否则服务无法持久化。

  3. <queries>中包名大小写错误
    某 SDK 文档写的是com.tencent.mobileqq,我们复制时误写为com.tencent.MobileQQ(首字母大写)。在 Android 11 下,queryIntentActivities()返回空,导致 QQ 登录入口消失。系统不报错,只静默失败。教训:所有<queries>中的包名,必须从目标应用的AndroidManifest.xml中精确复制,区分大小写。

适配 Android 11 不是一次性任务,而是一次开发范式的校准。它逼着我们放弃“先跑起来再说”的惯性,回归到“用户隐私优先、系统契约至上”的设计原点。那些曾经靠权限堆砌、隐式调用、全局监听实现的功能,现在必须拆解为更细粒度、更透明、更可控的模块。这个过程痛苦,但结果是:你的应用更健壮、更省电、更受用户信任。当用户在设置里看到“我的 App 从未在后台偷偷获取位置”,那种踏实感,是任何技术指标都无法替代的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 8:56:03

GitHub趋势榜速报:AI调试、机器人遥操作与开源项目评估方法

2026-09-30&#xff0c;GitHub 趋势榜准时更新。我照例在早上刷了一遍 Trending&#xff0c;发现今天的榜单很有意思&#xff1a;AI 编码工具不再一家独大&#xff0c;机器人遥操作、个人知识库、还有几个开发者体验向的小工具都挤进了前列。这篇文章就顺着今天的榜单&#xff…

作者头像 李华
网站建设 2026/10/14 8:56:00

本地部署MiniMax H3:ComfyUI视频生成工作流与显存优化实战

1. 为什么我决定把 MiniMax H3 搬进本地工作流第一次看到 MiniMax H3 这个开源多模态视频模型的时候&#xff0c;我其实没太当回事。那阵子开源视频模型一个接一个往外冒&#xff0c;参数一个比一个大&#xff0c;演示视频一个比一个炫&#xff0c;但真正能落到自己机器上跑起来…

作者头像 李华
网站建设 2026/10/14 8:55:40

技能操作系统:用YAML构建可验证、可演化的结构化能力模型

1. 项目概述&#xff1a;当“skills”不再只是简历上的单词&#xff0c;而成为可验证、可组合、可进化的个人能力操作系统最近在多个技术社区、职业发展论坛和高校创新工坊里&#xff0c;“skills”这个词高频出现&#xff0c;但绝不是传统意义上写在简历末尾那行加粗的“Pytho…

作者头像 李华
网站建设 2026/10/14 8:55:19

菲律宾PWA游戏广告投放实战:从素材本地化到数据归因全解析

1. 菲律宾PWA游戏广告投放的底层逻辑拆解 1.1 为什么是菲律宾市场 聊菲律宾之前&#xff0c;先看一组我实际跑过的数据。同样一款轻量级PWA游戏&#xff0c;在东南亚几个国家做投放测试&#xff0c;菲律宾的CPI能压到0.08到0.15美元区间&#xff0c;而邻国普遍在0.2到0.4美元。…

作者头像 李华
网站建设 2026/10/14 8:54:43

PSD2 XS2A动态沙箱实战:状态机、场景编排与联调排坑

简介&#xff1a;XS2A动态沙箱是一套针对PSD2指令开发的银行API测试环境&#xff0c;基于柏林集团NextGenPSD2标准实现&#xff0c;可模拟ASPSP&#xff08;账户服务支付服务提供方&#xff09;的OpenAPI服务&#xff0c;主要面向金融科技开发者、TPP&#xff08;第三方支付服务…

作者头像 李华
网站建设 2026/10/14 8:54:03

LiteLLM实战:统一大模型API调用的AI中间层网关

从一次百模混战说起&#xff1a;为什么我会盯上 LiteLLM 这个不起眼的中间层过去一年&#xff0c;我所在的团队没少在接入各种大模型 API 上折腾。今天这个模型开源评测榜刷屏&#xff0c;明天那个商用接口悄悄涨了价&#xff0c;后天客户又提出要求&#xff1a;“能不能帮我们…

作者头像 李华