☰
Android 6.0 运行时权限(第二篇):用独立 PermissionsActivity 构建全应用统一的权限检测架构
2026/10/10 11:57:30 网站建设 项目流程
  • 文档
  • 教程
  • 知识库

【免费下载链接】android-tech-frontier

【停止维护】一个定期翻译国外Android优质的技术、开源库、软件架构设计、测试等文章的开源项目

项目地址:https://gitcode.com/gh_mirrors/an/android-tech-frontier
点击查看免费下载

本文整理自 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(集中请求 / 引导设置 / 退出兜底)

这种分工带来两个好处:

  1. 检测开销小、位置分散:权限检测本身很轻量(一次checkSelfPermission调用),在每个 Activity 恢复时执行几乎无成本;
  2. 请求逻辑集中:权限请求涉及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; } } // ... }

其核心交互如下:

  1. requestPermissions()发起请求,回调onRequestPermissionsResult();
  2. 若所有权限都被授予,则setResult(PERMISSIONS_GRANTED)并finish(),原 Activity 的onResume()检测通过,正常继续;
  3. 若用户拒绝,则弹出说明对话框:提供"去设置"(跳转Settings.ACTION_APPLICATION_DETAILS_SETTINGS,配合package:scheme 直达本应用详情页)和"退出"两个选项——因为这类权限是应用核心功能所必需,被永久拒绝后只能引导用户去设置页手动授权,或直接退出应用。

PermissionsActivity内部同样在onResume()里做了一次检测,并用requiresCheck标志避免循环触发,覆盖"用户离开本 Activity 去设置页授权后再返回"的边界情况——这与第二篇的检测哲学一脉相承。

七、工程实践要点与坑位提醒

综合系列四篇文章(第一篇 讲检测、第二篇 讲架构、第三篇 讲请求、第四篇 讲非必需权限与最佳实践),落地这套方案时应注意:

  1. 区分必需与非必需权限:必需权限缺失时应用无法正常运行,必须硬性请求;非必需权限(如相机应用的地理标签功能所需的 Location)应在功能真正被用到时才请求,而不是启动时一窝蜂弹出。用户第一次打开应用就被连环索要权限,很可能直接卸载。
  2. 请求时要解释原因:RECORD_AUDIO这类权限的用途并不显而易见,应在说明文案中告诉用户为什么需要,降低被拒绝的概率。
  3. 定期审查权限清单:软件迭代中曾经的必需权限可能已超出使用范围,应定期清理 Manifest 中多余的权限声明。
  4. 注意"清除数据"会重置权限:用户清空应用数据(或卸载重装)后,所有已授权权限都会被重置为拒绝状态,重新启动应用会再次触发请求流程——这既是权限测试的便捷手段,也是客服经常要回答"为什么我已经同意了还要再申请"的原因。
  5. shouldShowRequestPermissionRationale()的取舍:第四篇提到,作者没有在示例代码中使用该检测,因为架构上已经有专门的说明对话框来承担"解释为什么需要权限"的职责。实际项目中可按需决定是否引入。

八、总结

Android 6.0 运行时权限模型下,"检测"与"请求"是两类性质不同的工作:检测轻量且遍布所有 Activity,请求复杂且集中处理更划算。本文的架构正是围绕这一不对称性设计——PermissionsChecker封装检测、每个 Activity 在onResume()自查、缺失即跳转PermissionsActivity统一处理请求与兜底。这套方案代码量小、覆盖面全、易复用,同时天然应对了"用户去设置页改权限"这一最棘手的时序问题。

本系列其余文章与示例可继续在仓库中阅读:

  • 权限 - 第一篇:权限检测与 PermissionsChecker 的封装
  • 权限 - 第三篇:PermissionsActivity 的请求、拒绝与设置页跳转
  • 权限 - 第四篇:非必需权限的请求时机与最佳实践
  • 文档
  • 教程
  • 知识库

【免费下载链接】android-tech-frontier

【停止维护】一个定期翻译国外Android优质的技术、开源库、软件架构设计、测试等文章的开源项目

项目地址:https://gitcode.com/gh_mirrors/an/android-tech-frontier
点击查看免费下载
上一篇:ReActor 换脸节点完整避坑指南:ComfyUI Reactor 节点安装与调优一次搞定
下一篇:免费开源的小红书原图下载工具 XHS-Downloader:10分钟从粘贴链接到本地素材库的完整指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询