一个动态权限库的设计

简介: 在经过上一次尝试剖析源码后,我意识到自己并没有一种比较好的方式去讲解代码,从而无法把自己所知道的知识更好地输出。所以接下来,至少在源码讲解有新想法前,我都不会再去尝试,也尽量减少博客中的非核心代码,而以思路及想法为主。

在经过上一次尝试剖析源码后,我意识到自己并没有一种比较好的方式去讲解代码,从而无法把自己所知道的知识更好地输出。所以接下来,至少在源码讲解有新想法前,我都不会再去尝试,也尽量减少博客中的非核心代码,而以思路及想法为主。另外,我也将尝试改进技术博客的笔法,段落之间尽量连贯,整体内容尽量有节奏感,目标是做到深入浅出地表达出主题相关内容。

从 Android 6.0(API 23)开始,用户可以在应用运行时向其授予权限(6.0 以下,国内厂商也多数做了类似的权限管理),而不是在应用安装时授予。这一方法既可以简化应用的安装过程,也可以让用户对应用的功能进行更多的限制。

在 Android 系统中,系统权限分为两类:正常权限以及危险权限。其中正常权限是不会直接给用户隐私权带来风险的,如果应用在其清单中声明了该权限,则系统会自动授予该权限。而危险权限,从 6.0 开始,而需要应用动态申请,并且由用户授予。

官方的动态权限 API 是从 Android 6.0 才引入的,那么,自然需要做好6.0 起及 6.0 之前的不同版本的权限适配,从而催生了一些动态权限兼容封装库。比如:

然而,我在项目中准备去使用的时候,却发现这几个库都无法完全满足我的想法,于是就想到自己也去造一个轮子,也就是今天要讲的——hey-permission。

我所设想的权限封装库是这样子的:

  • 6.0 前后接口调用的兼容处理。
  • 可以按权限组申请,及申请多个权限组(也就是可变长参数)。
  • 申请前判断是否获得所申请的权限,如果有未获得的权限,则只申请这些未获得的权限。
  • 能分别回调以下结果(开发者有声明则回调):
    • 权限申请通过;
    • 权限申请被拒绝(还可再申请);
    • 权限申请被永久拒绝(再申请会直接回调拒绝)。
  • 一个界面可能有不同的功能需要分别申请相同或不同的权限。所以:
    • 权限申请通过的回调方法里能区分不同的 requestCode;
    • 权限申请被拒绝(包括永久拒绝)的方法可以统一处理,也可以分开处理;
    • 如果父类统一处理,子类可以单独处理某个权限申请被拒绝的结果。
  • 支持不管申请通过还是拒绝都执行同样方法的情况。
  • 申请权限时,如果没有被用户拒绝过,则直接申请。如果被用户拒绝过(但不是永久拒绝),则能弹出提示用户权限重要性的对话框。
  • 如果申请权限时,已经被永久拒绝,也就是无法再弹出申请权限的请求对话框时,可以引导用户到应用设置里面授予权限。
  • 权限申请的回调中,如果用户拒绝了部分权限,但不是永久拒绝,则开发者可以处理是否弹出提示用户权限重要性的对话框。如果开发者在这里已处理,则不回调被拒绝的方法。
  • 轻量,不使用编译期注解(会受构建工具影响)。

那么,再看看 Android 6.0(API 23)里都为此提供了哪些 API ?

  • 请求申请多个权限。
  • 查询是否具有某个权限。
  • 查询是否应该显示提示用户权限重要性的UI。它有如下结果:
    • 如果在之前没有被拒绝过,则返回否。
    • 如果被拒绝过,但还可以再申请,则返回是。
    • 如果被永久拒绝,则返回否。
  • 权限申请结果回调。它会返回请求码,所申请的权限以及处理结果的数组,它们一一对应。

其中根据第三条,我们可以在权限被拒绝时来判断是否被永久拒绝。

接下来,我们可以开始构思整个权限申请的流程,它主要有两部分:一是权限申请;二是申请结果的回调。
下面来绘制一下权限申请的流程:

Created with Raphaël 2.1.2开始请求申请多个权限是否都有权限?回调权限已被授予结束是否显示权限重要性的UI?显示提示权限重要性的UI执行申请未被授予的权限yesnoyesno

对应的核心代码则是:

private static void requestPermissions(@NonNull BasePermissionInvoker invoker,
                                      @IntRange(from = 0) int requestCode,
                                      @Size(min = 1) @NonNull String[]... permissionSets) {
   final List<String> permissionList = new ArrayList<>();
   for (String[] permissionSet : permissionSets) {
       permissionList.addAll(Arrays.asList(permissionSet));
   }
   final String[] permissions = permissionList.toArray(new String[permissionList.size()]);
   if (hasPermissions(invoker.getContext(), permissions)) {
       notifyAlreadyHasPermissions(invoker, requestCode, permissions);
       return;
   }
   if (invoker.shouldShowRequestPermissionRationale(permissions)) {
       if (invokeShowRationaleMethod(false, invoker, requestCode, permissions)) {
           return;
       }
   }
   invoker.executeRequestPermissions(requestCode, permissions);
}

然后是权限申请结果的回调处理:

Created with Raphaël 2.1.2开始权限申请结果回调按被授予和被拒绝对权限分组是否没有被拒绝的权限?回调权限已被授予结束是否显示提示权限重要性的UI?开发者返回已处理显示权限重要性的UI?回调权限已被拒绝回调权限已被永久拒绝yesnoyesnoyesno

对应的核心代码则是:

private static void onRequestPermissionsResult(
       @NonNull BasePermissionInvoker invoker, @IntRange(from = 0) int requestCode,
       @Size(min = 1) @NonNull String[] permissions, @NonNull int[] grantResults) {
   if (!invoker.needHandleThisRequestCode(requestCode)) {
       return;
   }

   final List<String> granted = new ArrayList<>();
   final List<String> denied = new ArrayList<>();
   for (int i = 0; i < permissions.length; i++) {
       if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
           granted.add(permissions[i]);
       } else {
           denied.add(permissions[i]);
       }
   }

   if (denied.isEmpty()) {
       // all permissions were granted
       invokePermissionsResultMethod(PermissionsGranted.class, invoker, requestCode, granted);
       invokePermissionsResultMethod(PermissionsResult.class, invoker, requestCode, denied);
       return;
   }
   final String[] deniedPermissions = denied.toArray(new String[denied.size()]);
   boolean neverAskAgain = true;
   if (invoker.shouldShowRequestPermissionRationale(deniedPermissions)) {
       neverAskAgain = false;
       if (invokeShowRationaleMethod(true, invoker, requestCode, deniedPermissions)) {
           return;
       }
   }
   if (neverAskAgain) {
       invokePermissionsResultMethod(PermissionsNeverAskAgain.class,
               invoker, requestCode, denied);
   } else {
       invokePermissionsResultMethod(PermissionsDenied.class, invoker, requestCode, denied);
   }
   invokePermissionsResultMethod(PermissionsResult.class, invoker, requestCode, denied);
}

以上就是最核心的逻辑。

接下来思考三个问题:

  1. Activity, Fragment, SupportFragment 都会需要申请权限及结果回调。
  2. 采用哪种形式进行回调?是回调接口还是注解?
  3. 在显示权限重要性的回调里,需要提供一个对象用于实际上的直接发起权限申请。

对于第一个问题,我是参考了 google 官方的 easypermissions,对实际发起权限申请的 Activity, Fragment 及 SupportFragment 做了一层包装,抽象为 BasePermissionInvoker 抽象类,并定义了一些依赖它们去实现的行为,如 shouldShowRequestPermissionRationale(@NonNull String... permissions)startActivityForResult(Intent intent, int requestCode)等。
而发起权限的方法并没有定义在这里,这是考虑到了第三个问题的对象只需要直接申请权限的接口,而不需要暴露其他接口。所以针对第三个问题,我另外定义了 PermissionRequestExecutor 接口,里面只声明了一个方法 void executeRequestPermissions(int code, String... permissions);,由 BasePermissionInvoker 去实现。
然后继承自 BasePermissionInvoker,在 Activity, Fragment 及 SupportFragment 的封装中对这些方法做具体的实现。

最后是对第二个问题的思考,回调方式如何选择?

一是开发者向用户显示权限重要性的提示,这个开发者可以显示也可以不显示。并且如上面的流程图所示,如果权限申请被拒绝了,而开发者显示了权限重要性的话,那么即表明权限申请已被拒绝,则不用再回调被拒绝的方法。否则表示开发者未处理,则要回调到权限被拒绝的方法。所以这种情况可以使用回调接口的方法,通过返回值来判断。

第二种情况是权限申请结果的回调。申请结果的回调有通过、拒绝、永久拒绝多种,并且我们希望可以在 Activity 或 Fragment 等的基类能够统一处理被拒绝的情况,另一方面,我们需要不同的权限请求,能够在各自的方法里去处理回调。基于这些想法,因此采用了注解的方式。通过不同的 requestCode 来回调对应的方法;并且被拒绝的注解,它接收的参数是应该是数组类型,也就是我们可以在基本统一处理,也可以在某一个类通过再声明对应的注解来单独处理某一种情况。

以上就是 hey-permission 的整个结构设计。具体实现时会遇到一些细节问题,比如判断权限时考虑 6.0 之前的 Ops,比如 SupportFragment 的权限申请结果是通过 AppCompatActivity 来分发,比如回调的注解方法的参数,等等,这些就不在这里赘述,具体可参考项目源码,地址为:https://github.com/parkingwang/hey-permission

目前已实现如下特性:

  • 单个权限/权限组申请
  • 注解回调结果
    • @PermissionsGranted 申请权限均被允许
    • @PermissionsDenied 申请权限被拒绝(下次还可询问用户)
    • @PermissionsNeverAskAgain 申请权限被永久拒绝
    • @PermissionsResult 申请权限结果(允许或拒绝都会回调)
  • 注解回调方法可以是任意参数
    • 如果参数中有 int,则第一个 int 参数将接收此次的 requestCode
    • 如果参数中有 List,则第一个 List 参数将接收此次申请通过(@PermissionsGranted)或被拒绝(其他注解)的 permissions
  • 回调注解支持处理多个 requestCode
    • 仅申请允许的注解只能处理单个 requestCode
    • 其他权限结果注解支持多个 requestCode,如果为空则表示处理所有 requestCode
  • 提示用户权限的重要性(回调方法)
    • 默认的对话框供调用
  • 被永久拒绝后提示用户去系统设置添加权限的对话框
  • 支持以下组件
    • Activity
    • Fragment
    • SupportFragment
目录
相关文章
|
Dubbo Java 应用服务中间件
Spring Cloud Alibaba 五大组件
Spring Cloud Netflix 部门组件的维护已经停止,这使得 Spring 社区意识到需要寻找新的解决方案来支持微服务架构。同时,由于阿里巴巴在微服务领域拥有丰富的经验和技术实践,因此 Spring 社区选择与阿里巴巴合作,通过整合阿里开源的组件和工具来开发 Spring Cloud Alibaba,以取代 Spring Cloud Netflix。
5779 2
|
11月前
|
前端开发 Java 应用服务中间件
《深入理解Spring》 Spring Boot——约定优于配置的革命者
Spring Boot基于“约定优于配置”理念,通过自动配置、起步依赖、嵌入式容器和Actuator四大特性,简化Spring应用的开发与部署,提升效率,降低门槛,成为现代Java开发的事实标准。
|
负载均衡 监控 Java
微服务稳定性三板斧:熔断、限流与负载均衡全面解析(附 Hystrix-Go 实战代码)
在微服务架构中,高可用与稳定性至关重要。本文详解熔断、限流与负载均衡三大关键技术,结合API网关与Hystrix-Go实战,帮助构建健壮、弹性的微服务系统。
1076 1
微服务稳定性三板斧:熔断、限流与负载均衡全面解析(附 Hystrix-Go 实战代码)
|
11月前
|
缓存 JSON NoSQL
别再手写过滤器!SpringCloud Gateway 内置30 个,少写 80% 重复代码
小富分享Spring Cloud Gateway内置30+过滤器,涵盖请求、响应、路径、安全等场景,无需重复造轮子。通过配置实现Header处理、限流、重试、熔断等功能,提升网关开发效率,避免代码冗余。
815 1
|
12月前
|
Prometheus 监控 Java
日志收集和Spring 微服务监控的最佳实践
在微服务架构中,日志记录与监控对系统稳定性、问题排查和性能优化至关重要。本文介绍了在 Spring 微服务中实现高效日志记录与监控的最佳实践,涵盖日志级别选择、结构化日志、集中记录、服务ID跟踪、上下文信息添加、日志轮转,以及使用 Spring Boot Actuator、Micrometer、Prometheus、Grafana、ELK 堆栈等工具进行监控与可视化。通过这些方法,可提升系统的可观测性与运维效率。
955 1
日志收集和Spring 微服务监控的最佳实践
|
监控 负载均衡 Java
5 大 SpringCloud 核心组件详解,8 张图彻底弄懂
本文图文详解 Spring Cloud 的五大核心组件,帮助深入理解和掌握微服务架构。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
5 大 SpringCloud 核心组件详解,8 张图彻底弄懂
|
存储 缓存 安全
Java HashMap详解及实现原理
Java HashMap是Java集合框架中常用的Map接口实现,基于哈希表结构,允许null键和值,提供高效的存取操作。它通过哈希函数将键映射到数组索引,并使用链表或红黑树解决哈希冲突。HashMap非线程安全,多线程环境下需注意并发问题,常用解决方案包括ConcurrentHashMap和Collections.synchronizedMap()。此外,合理设置初始化容量和加载因子、重写hashCode()和equals()方法有助于提高性能和避免哈希冲突。
1015 17
Java HashMap详解及实现原理
|
人工智能 架构师 决策智能
agentUniverse X 浙大太乙平台,开源共建招募令来啦,3万奖金等你拿!
agentUniverse 首期开源共建活动正式上线啦!3万奖金池等大家贡献瓜分~
|
存储 安全 Java
2025 年一线互联网大厂最新高质量 Java 面试八股文整理及答案汇总
本文整理了一线互联网大厂最新的高质量Java面试八股文及其答案,涵盖Java基础、集合框架与多线程三大核心模块。内容包括面向对象与面向过程的区别、`equals`与`==`的差异、`final`和`static`的用法、集合类如`ArrayList`与`LinkedList`的对比、`HashMap`的工作原理及其与`Hashtable`的区别,以及多线程中的线程创建方式、生命周期、上下文切换和死锁等知识点。通过系统化的梳理与解析,帮助读者高效备考Java面试,掌握核心技术要点。资源可从文末链接下载。
3163 40
|
12月前
|
供应链 安全 BI
两小时搭建一套自动化物资领用与审批系统!
企业常因物资领用混乱导致账实不符、审批随意、库存不清。本文分享如何用简道云零代码平台,两小时搭建一套物资领用与审批系统,实现申请、审批、库存管理、追溯全流程数字化,提升效率,降低浪费。

热门文章

最新文章