- 文档
- 教程
- 知识库
【免费下载链接】android-tech-frontier
【停止维护】一个定期翻译国外Android优质的技术、开源库、软件架构设计、测试等文章的开源项目
本文整理自 android-tech-frontier 仓库 issue-40/权限 - 第二篇.md,它是 StylingAndroid "Permissions" 系列译文的一部分。该系列从技术角度剖析 Android 6.0(Marshmallow)运行时权限模型,本文聚焦"如何在所有 Activity 中以最少重复代码统一检测缺失权限,并在适当时机把请求流程交给一个专职的 PermissionsActivity"。
读完本文,你将掌握:在onResume()中统一检测权限缺失的原因与写法、PermissionsChecker与PermissionsActivity各司其职的分层架构、以及用户拒绝/勾选"不再询问"后应用应当如何兜底,从而在targetSdkVersion = 23+下构建一套不易崩溃、可复用的权限管理方案。
一、背景回顾:为什么需要运行时权限检测
Android 6.0(Marshmallow)引入了一套全新的权限模型,开发者处理授权的方式与以往完全不同。在早期系统版本中,只要在 Manifest 中声明<uses-permission>,安装时即被直接授权;而在 Marshmallow 及之后(且targetSdkVersion声明为 23 或更高)的版本中,危险权限(dangerous permission)必须由用户运行时显式授予。
具体到本系列示例应用,需要两个权限:
RECORD_AUDIO—— 危险权限,可能影响用户安全或隐私,需要运行时请求;MODIFY_AUDIO_SETTINGS—— 标准权限,系统会自动授权。
因此在 API 23+ 环境下,绝不能想当然地认为 Manifest 声明过就万事大吉。判断某个权限是否已授予,应使用ContextCompat.checkSelfPermission()而非直接调用Context.checkSelfPermission(),前者会自动完成 API-level 判断——在 Marshmallow 之前的设备上,checkSelfPermission()总是返回PERMISSION_GRANTED(因为权限在旧系统上默认可直接获得),从而让同一份代码在所有 OS 版本上都能工作,无需手写版本分支。
第一篇文章将这段检测逻辑封装成了独立的PermissionsChecker类(详见 权限 - 第一篇.md):
class PermissionsChecker { private final Context context; public PermissionsChecker(Context context) { this.context = context; } public boolean lacksPermissions(String... permissions) { for (String permission : permissions) { if (lacksPermission(permission)) { return true; } } return false; } private boolean lacksPermission(String permission) { return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_DENIED; } }单独抽出这个类的原因很直接:稍后应用中的所有 Activity 都需要做同样的检测,把检测逻辑从 Activity 中剥离,可以显著减少重复代码、提高可维护性。这是本文架构的基石。
二、核心架构:检测与请求分离
本文(第二篇)提出的模式可以概括为一句话:
用一个专职负责请求权限的
PermissionsActivity处理所有请求流程,应用中的其他 Activity 只做两件事——检测自己所需权限,若缺失则把控制权交给PermissionsActivity。
所有 Activity(onResume 检测) │ 权限缺失 ▼ PermissionsActivity(集中请求 / 引导设置 / 退出兜底)这种分工带来两个好处:
- 检测开销小、位置分散:权限检测本身很轻量(一次
checkSelfPermission调用),在每个 Activity 恢复时执行几乎无成本; - 请求逻辑集中:权限请求涉及
requestPermissions()回调、onRequestPermissionsResult、说明对话框、跳转系统设置等一整套复杂交互,集中在一个 Activity 中实现一次,所有界面复用。
三、MainActivity 的改造:把检测放到 onResume()
原文档给出了改造后的MainActivity完整代码:
public class MainActivity extends AppCompatActivity { static final String[] PERMISSIONS = new String[]{Manifest.permission.RECORD_AUDIO, Manifest.permission.MODIFY_AUDIO_SETTINGS}; private PermissionsChecker checker; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); checker = new PermissionsChecker(this); } @Override protected void onResume() { super.onResume(); if (checker.lacksPermissions(PERMISSIONS)) { startPermissionsActivity(); } } private void startPermissionsActivity() { PermissionsActivity.startActivity(this, PERMISSIONS); } }关键点拆解:
PERMISSIONS静态数组:列出该 Activity 运行所必需的全部权限。对MainActivity而言是录音相关的两个权限;其他 Activity 可按需声明各自的权限清单,这是"每个 Activity 只检测自己需要的权限"的落地方式。onCreate中创建PermissionsChecker:初始化检测组件,注意它持有Context,应在 Activity 生命周期早期创建。onResume()中执行检测:这是本文最重要的设计决策(详见下一节)。一旦发现权限缺失,立即通过静态入口PermissionsActivity.startActivity(this, PERMISSIONS)启动请求流程。
四、为什么偏偏选 onResume() 而不是 onCreate()?
原文档给出了明确的工程理由:把权限检测放在onResume(),是为了覆盖"用户离开应用、中途改变权限、再返回"的时序问题。具体场景包括:
- 用户暂停了应用,切到系统设置页,撤销了某项权限,然后返回应用——此时 Activity 会走
onResume(); - 用户从
PermissionsActivity跳转到应用设置页授权后再返回; - 权限状态在应用运行期间随时都可能被用户修改。
文档原文强调:用户随时可以进入系统设置,逐个授权或拒绝应用需要的任何权限。这正是"不能只在应用启动时检测一次"的根本原因——每个 Activity 在每次恢复时都要重新确认所需权限是否仍然有效。
作者也坦诚评价了这个方案:"虽说这感觉像是一个防御性方案,但我认为这是一个不需要大量代码的明智的方案。"因为检测逻辑已被PermissionsChecker封装,请求流程集中在PermissionsActivity,各 Activity 自身只增加了几行代码,却换来了对权限变更的完整防御。
五、用户视角:权限请求的三段式交互流程
要让方案真正可用,必须理解用户实际会看到什么。原文档描述了三个典型阶段:
1. 首次运行:请求授权
应用第一次运行时弹出系统授权对话框,用户可以选择"允许"或"拒绝"。
- 允许:流程结束,万事大吉,不再打扰用户。
- 拒绝:进入下一阶段。
2. 再次请求:"不再询问"陷阱
如果用户拒绝了权限申请,再次请求时系统对话框会出现一个"不再询问"(Never ask again)的选项。一旦用户勾选该选项并拒绝,此后代码中任何对这项权限的请求都会被系统自动拒绝,且不再弹窗。
作为开发者必须清醒认识到这一点:此后requestPermissions()的调用不会产生任何可见效果,回调返回的grantResults永远是PERMISSION_DENIED。必须为这种情况预留兜底方案(见第六节)。
3. 设置页兜底
用户还可以进入系统设置的应用详情页,随时授权或拒绝任何权限。这也是为什么每次onResume()都要重新检测——权限状态并不稳定,随时可能被外部改变。
六、闭环补全:PermissionsActivity 如何接手(第三篇内容前瞻)
第二篇在文末预告:"下一篇文章我们会看到PermissionsActivity如何处理这个权限的请求,探索如果用户拒绝了我们需要的权限,如何告知用户我们为什么需要。"仓库中的 权限 - 第三篇.md 给出了完整实现,这里补全整个闭环,便于理解本文架构的最终效果:
public class PermissionsActivity extends AppCompatActivity { private static final int PERMISSION_REQUEST_CODE = 0; private static final String EXTRA_PERMISSIONS = "com.stylingandroid.permissions.EXTRA_PERMISSIONS"; private static final String EXTRA_FINISH = "com.stylingandroid.permissions.EXTRA_FINISH"; private static final String PACKAGE_URL_SCHEME = "package:"; private PermissionsChecker checker; private boolean requiresCheck; public static void startActivityForResult(Activity activity, int requestCode, String... permissions) { Intent intent = new Intent(activity, PermissionsActivity.class); intent.putExtra(EXTRA_PERMISSIONS, permissions); ActivityCompat.startActivityForResult(activity, intent, requestCode, null); } @Override protected void onResume() { super.onResume(); if (requiresCheck) { String[] permissions = getPermissions(); if (checker.lacksPermissions(permissions)) { requestPermissions(permissions); } else { allPermissionsGranted(); } } else { requiresCheck = true; } } // ... }其核心交互如下:
requestPermissions()发起请求,回调onRequestPermissionsResult();- 若所有权限都被授予,则
setResult(PERMISSIONS_GRANTED)并finish(),原 Activity 的onResume()检测通过,正常继续; - 若用户拒绝,则弹出说明对话框:提供"去设置"(跳转
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,配合package:scheme 直达本应用详情页)和"退出"两个选项——因为这类权限是应用核心功能所必需,被永久拒绝后只能引导用户去设置页手动授权,或直接退出应用。
PermissionsActivity内部同样在onResume()里做了一次检测,并用requiresCheck标志避免循环触发,覆盖"用户离开本 Activity 去设置页授权后再返回"的边界情况——这与第二篇的检测哲学一脉相承。
七、工程实践要点与坑位提醒
综合系列四篇文章(第一篇 讲检测、第二篇 讲架构、第三篇 讲请求、第四篇 讲非必需权限与最佳实践),落地这套方案时应注意:
- 区分必需与非必需权限:必需权限缺失时应用无法正常运行,必须硬性请求;非必需权限(如相机应用的地理标签功能所需的 Location)应在功能真正被用到时才请求,而不是启动时一窝蜂弹出。用户第一次打开应用就被连环索要权限,很可能直接卸载。
- 请求时要解释原因:
RECORD_AUDIO这类权限的用途并不显而易见,应在说明文案中告诉用户为什么需要,降低被拒绝的概率。 - 定期审查权限清单:软件迭代中曾经的必需权限可能已超出使用范围,应定期清理 Manifest 中多余的权限声明。
- 注意"清除数据"会重置权限:用户清空应用数据(或卸载重装)后,所有已授权权限都会被重置为拒绝状态,重新启动应用会再次触发请求流程——这既是权限测试的便捷手段,也是客服经常要回答"为什么我已经同意了还要再申请"的原因。
shouldShowRequestPermissionRationale()的取舍:第四篇提到,作者没有在示例代码中使用该检测,因为架构上已经有专门的说明对话框来承担"解释为什么需要权限"的职责。实际项目中可按需决定是否引入。
八、总结
Android 6.0 运行时权限模型下,"检测"与"请求"是两类性质不同的工作:检测轻量且遍布所有 Activity,请求复杂且集中处理更划算。本文的架构正是围绕这一不对称性设计——PermissionsChecker封装检测、每个 Activity 在onResume()自查、缺失即跳转PermissionsActivity统一处理请求与兜底。这套方案代码量小、覆盖面全、易复用,同时天然应对了"用户去设置页改权限"这一最棘手的时序问题。
本系列其余文章与示例可继续在仓库中阅读:
- 权限 - 第一篇:权限检测与 PermissionsChecker 的封装
- 权限 - 第三篇:PermissionsActivity 的请求、拒绝与设置页跳转
- 权限 - 第四篇:非必需权限的请求时机与最佳实践
- 文档
- 教程
- 知识库
【免费下载链接】android-tech-frontier
【停止维护】一个定期翻译国外Android优质的技术、开源库、软件架构设计、测试等文章的开源项目
相关推荐
发明专利点挖掘与融合:基于 patent-disclosure-skill 的 Step 3–4 实战指南
发明专利点挖掘与融合:基于 patent disclosure skill 的 Step 3–4 实战指南 本文聚焦 patent disclosure ski
文档教程知识库HomeMirror权限动态申请:Android 6.0+运行时权限处理
HomeMirror权限动态申请:Android 6.0+运行时权限处理 在Android应用开发中,权限管理是确保应用安全运行的关键环节。从Android 6
物联网移动开发NodeJS JWT Authentication Sample与Postman集成:高效测试API的实用技巧
NodeJS JWT Authentication Sample与Postman集成:高效测试API的实用技巧 NodeJS JWT Authenticatio
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考