Android 采用AOP方式封装6.0权限管理的方法

【一】背景

6.0运行时申请权限已经是一个老生常谈的内容了,最近项目TargetSDKVersion升到23以上,所以我们也需要做权限管理,我想到的需求是这样的:

1、支持单个权限、多个权限申请

2、运行时申请

3、无侵入式申请,无需关注权限申请的逻辑

4、除了Activity、Fragment之外,还需要支持Service中申请

5、对国产手机做兼容处理

第一、二点,Google都有对应的API;

第三点可以通过自定义注解+AOP切面方式来解决。为什么采用AOP方式呢?首先看AOP定义: 面向切面编程(Aspect-Oriented Programming)。如果说,OOP(面向对象)如果是把问题划分到单个模块的话,那么AOP就是把涉及到众多模块的某一类问题进行统一管理。 因为我们申请权限的逻辑都是基本一样的,所以可以把申请权限的逻辑统一管理。

第四点稍微有点麻烦,因为Google提供的API只支持在Activity和Fragment中去申请权限,Service中并没有相应的API,比如项目中的某个Service里需要拿到当前位置信息,并且不能确定定位权限已经给了,所以在定位之前仍然需要判断有没有定位权限,按照常规逻辑好像是行不通了。肿么办呢?先说一下我想到的办法:通过一个透明的Activity去申请权限,并且把申请结果返回来,最后实践也是这么做的,具体思路请往下看。

第五点也比较麻烦,如果都按Google标准来,那就不用考虑兼容问题了,但是国产安卓手机碎片化比较严重,且基本都修改了ROM,导致申请权限的API跟期望返回的结果不一致,这种的可能就需要特殊处理了。

调研了一下比较流行的三方库,如PermissionsDispatcherRxPermissions,做了一个简单的总结:

权限库 是否使用注解 是否支持链式调用 是否支持Service 是否适配国产机
RxPermissions No Yes No No
PermissionsDispatcher Yes No No 适配了小米

RxPermissions是基于RX的思想,支持链式调用,简单方便,但是他不支持Service调用;PermissionsDispatcher使用了编译时解析注解的方式,通过apt自动生成.class方式帮我们去写申请权限的逻辑,很好很强大,并且适配了小米手机,但是它也不支持Service中去申请权限。考虑到我们项目中的应用场景并且借鉴了PermissionsDispatcher的申请权限的逻辑,决定基于AOP方式手动撸一个权限管理库出来。

【二】效果图

先上一下最终的效果图:

效果图有点模糊,可以下载源码运行一下看效果

【三】整体思路

首先,先定义一个说法,弹出系统权限弹窗,用户没有给权限,并且选中不再提示,这种情况称为权限被拒绝;如果用户没有给权限,但是没有选中不再提示,这种情况称为权限被取消。申请权限、权限被取消、权限被拒绝都是采用注解的形式,分别为@NeedPermission、@PermissionCanceled、@PermissionDenied,注解都是声明在Method级别上的。在我们的Activity、Fragment及Service中声明注解,然后在AOP中解析我们的注解,并把申请的权限传递给一个透明的Activity去处理,并把处理结果返回来。这就是整体思路,可能会遇到的问题:

1、 不同型号的手机兼容问题(申请权限、跳设置界面)

2、AOP解析注解以及传值问题

上面说了很多,其实用一个图来表示更清晰一些:

UML时序图.png

OK,通过上面的图是不是更清晰了呢?其实最关键的地方就是AOP解析注解及传值。AOP面向切面编程是一种编程思想,而AspectJ是对AOP编程思想的一个实践,本文采用AspectJ来实现切面编程,简单介绍AspectJ的几个概念:

  1. JPoint:代码可注入的点,比如一个方法的调用处或者方法内部,对于本文来说即是注解作用的方法。
  2. Pointcut:用来描述 JPoint 注入点的一段表达式。见下面例子
  3. Advice:常见的有 Before、After、Around 等,表示代码执行前、执行后、替换目标代码,也就是在 Pointcut 何处编织代码。
  4. Aspect:切面,Pointcut 和 Advice 合在一起称作 Aspect。

关于AspectJ的介绍及用法的文章很多,不了解的朋友可以去google下,直接列一下AOP切面代码:

@Aspect
public class PermissionAspect {

  private static final String PERMISSION_REQUEST_POINTCUT =
      "execution(@com.ninetripods.aopermission.permissionlib.annotation.NeedPermission * *(..))";

  @Pointcut(PERMISSION_REQUEST_POINTCUT + " && @annotation(needPermission)")
  public void requestPermissionMethod(NeedPermission needPermission) {
  }

  @Around("requestPermissionMethod(needPermission)")
  public void AroundJoinPoint(final ProceedingJoinPoint joinPoint, NeedPermission needPermission) {

    Context context = null;
    final Object object = joinPoint.getThis();
    if (object instanceof Context) {
      context = (Context) object;
    } else if (object instanceof Fragment) {
      context = ((Fragment) object).getActivity();
    } else if (object instanceof android.support.v4.app.Fragment) {
      context = ((android.support.v4.app.Fragment) object).getActivity();
    }
    if (context == null || needPermission == null) return;

    PermissionRequestActivity.PermissionRequest(context, needPermission.value(),
        needPermission.requestCode(), new IPermission() {
          @Override
          public void PermissionGranted() {
            try {
              joinPoint.proceed();
            } catch (Throwable throwable) {
              throwable.printStackTrace();
            }
          }

          @Override
          public void PermissionDenied(int requestCode, List<String> denyList) {
            Class<?> cls = object.getClass();
            Method[] methods = cls.getDeclaredMethods();
            if (methods == null || methods.length == 0) return;
            for (Method method : methods) {
              //过滤不含自定义注解PermissionDenied的方法
              boolean isHasAnnotation = method.isAnnotationPresent(PermissionDenied.class);
              if (isHasAnnotation) {
                method.setAccessible(true);
                //获取方法类型
                Class<?>[] types = method.getParameterTypes();
                if (types == null || types.length != 1) return;
                //获取方法上的注解
                PermissionDenied aInfo = method.getAnnotation(PermissionDenied.class);
                if (aInfo == null) return;
                //解析注解上对应的信息
                DenyBean bean = new DenyBean();
                bean.setRequestCode(requestCode);
                bean.setDenyList(denyList);
                try {
                  method.invoke(object, bean);
                } catch (IllegalAccessException e) {
                  e.printStackTrace();
                } catch (InvocationTargetException e) {
                  e.printStackTrace();
                }
              }
            }
          }

          @Override
          public void PermissionCanceled(int requestCode) {
            Class<?> cls = object.getClass();
            Method[] methods = cls.getDeclaredMethods();
            if (methods == null || methods.length == 0) return;
            for (Method method : methods) {
              //过滤不含自定义注解PermissionCanceled的方法
              boolean isHasAnnotation = method.isAnnotationPresent(PermissionCanceled.class);
              if (isHasAnnotation) {
                method.setAccessible(true);
                //获取方法类型
                Class<?>[] types = method.getParameterTypes();
                if (types == null || types.length != 1) return;
                //获取方法上的注解
                PermissionCanceled aInfo = method.getAnnotation(PermissionCanceled.class);
                if (aInfo == null) return;
                //解析注解上对应的信息
                CancelBean bean = new CancelBean();
                bean.setRequestCode(requestCode);
                try {
                  method.invoke(object, bean);
                } catch (IllegalAccessException e) {
                  e.printStackTrace();
                } catch (InvocationTargetException e) {
                  e.printStackTrace();
                }
              }
            }
          }
        });
  }
}

代码有点多,但是思路还是挺清晰的,首先定义@Pointcut(描述的是我们的注解@NeedPermission),接着由Advice(@Around)及Pointcut构成我们的切面Aspect, 在切面Aspect中,通过joinPoint.getThis()根据不同来源来获得Context,接着跳转到一个透明Activity申请权限并通过接口回调拿到权限申请结果,最后在不同的回调方法里通过反射把回调结果回传给调用方。

【四】使用举例

为了简化AspectJ的各种配置,这里用了一个三方的gradle插件:

https://github.com/HujiangTechnology/gradle_plugin_android_aspectjx

1、权限库引入方式,在app模块的build.gradle中引入如下:

apply plugin: 'android-aspectjx'

dependencies {
   compile 'com.ninetripods:aop-permission:1.0.1'
   ..........其他............
}

2、在整个工程的build.gradle里面配置如下:

dependencies {
  classpath 'com.android.tools.build:gradle:2.3.3'
  classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:1.0.8'
  ................其他................
}

说明: aspectjx:1.0.8不是最新版本,最高支持gradle的版本到2.3.3,如果你的工程里gradle版本是3.0.0以上,请使用aspectjx:1.1.0以上版本,aspectjx历史版本查看地址:

https://github.com/HujiangTechnology/gradle_plugin_android_aspectjx/blob/master/CHANGELOG.md

3、如果你的项目里使用了混淆,需要在AOP代码进行hook的类及方法名不能被混淆,即被注解作用的类及方法不能被混淆,需要在混淆配置里keep住, 比如:

package com.hujiang.test;

public class A {

  @NeedPermission
  public boolean funcA(String args) {
    ....
  }
}

//如果你在AOP代码里对A#funcA(String)进行hook, 那么在混淆配置文件里加上这样的配置

-keep class com.hujiang.test.A {*;}

4、终于配好了,都闪开,我要开始举栗子了:

下面以Activity中申请权限为例,Fragment、Service中使用是一样的,就不一一写了,源码中也有相应使用的Demo

4.1 申请单个权限

申请单个权限:

btn_click.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) {
    callMap();
  }
});

/**
 * 申请权限
 */
@NeedPermission(value = {Manifest.permission.ACCESS_FINE_LOCATION}, requestCode = 0)
private void callMap() {
  Toast.makeText(this, "定位权限申请通过", Toast.LENGTH_SHORT).show();
}

@NeedPermission后面的value代表需要申请的权限,是一个String[]数组;requestCode是请求码,是为了区别开同一个Activity中有多个不同的权限请求,默认是0,如果同一个Activity中只有一个权限申请,requestCode可以忽略不写。

/**
 * 权限被取消
 *
 * @param bean CancelBean
 */
@PermissionCanceled
public void dealCancelPermission(CancelBean bean) {
  Toast.makeText(this, "requestCode:" + bean.getRequestCode(), Toast.LENGTH_SHORT).show();
}

声明一个public方法接收权限被取消的回调, 方法必须有一个CancelBean类型的参数 ,这点类似于EventBus,CancelBean中有requestCode变量,即是我们请求权限时的请求码。

/**
 * 权限被拒绝
 *
 * @param bean DenyBean
 */
@PermissionDenied
public void dealPermission(DenyBean bean) {
    Toast.makeText(this,
    "requestCode:" + bean.getRequestCode()+ ",Permissions: " + Arrays.toString(bean.getDenyList().toArray()), Toast.LENGTH_SHORT).show();
 }

声明一个public方法接收权限被取消的回调, 方法必须有一个DenyBean类型的参数 ,DenyBean中有一个requestCode变量,即是我们请求权限时的请求码,另外还可以通过denyBean.getDenyList()来拿到被权限被拒绝的List。

4.2 申请多个权限

基本用法同上,区别是@NeedPermission后面声明的权限是多个,如下:

/**
 * 申请多个权限
 */
@NeedPermission(value = {Manifest.permission.CALL_PHONE, Manifest.permission.CAMERA}, requestCode = 10)
public void callPhone() {
  Toast.makeText(this, "电话、相机权限申请通过", Toast.LENGTH_SHORT).show();
}

value中声明了两个权限,一个电话权限,一个相机权限

4.3 跳转到设置类

当用户拒绝权限并选中不再提示后,需要引导用户去设置界面打开权限,因为国产手机各个设置界面不一样,用通用的API可能会跳转不到相应的APP设置界面,这里采用了策略模式(下图所示)

跳转到设置类.png

如需做兼容,只需要在库里修改,调用方是不需要处理的,调用方如需跳转到设置界面,只需像下面这样调用就OK了:

【五】总结

回看一下我们的需求,基本上都实现了:

1、首先通过@NeedPermission、@PermissionCanceled、@PermissionDenied三个注解来分别定义权限申请、被取消、被拒绝三种情况,如果不想处理被取消的逻辑就不用使用@PermissionCanceled注解,其他权限申请的逻辑调用方不用关心,是完全解耦的;

2、同时支持在Activity、Fragment、Service中去申请权限;

3、最后关于申请权限、跳设置界面的兼容问题,因为身边的手机有限,不能测试出所有兼容问题,需要后续优化。

关于在AOP中通过 反射方式 把权限申请结果返回给调用方,是参考了EventBus的方式,感觉这样用起来更方便一些;之前的做法是在AOP对应的Java类中声明接口,调用方实现该接口,然后通过接口回调的方式将权限申请结果回传,也能实现同样效果,但是感觉没有反射方式更方便。以上就是全部内容,后面会贴出源码,如有使用不当之处,欢迎批评指正!

【六】源码

源码地址: https://github.com/crazyqiang/Aopermission

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

您可能感兴趣的文章:

  • 一款不错的android6.0、7.0权限管理器推荐
  • 详解Android6.0运行时权限管理
  • 详解Android权限管理之RxPermission解决Android 6.0 适配问题
  • 详解Android权限管理之Android 6.0运行时权限及解决办法
  • Android权限管理之Permission权限机制及使用详解
  • Android获取设备隐私 忽略6.0权限管理
(0)

相关推荐

  • 详解Android权限管理之RxPermission解决Android 6.0 适配问题

    前言: 上篇重点学习了Android 6.0的运行时权限,今天还是围绕着Android 6.0权限适配来总结学习,这里主要介绍一下我们公司解决Android 6.0权限适配的方案:RxJava+RxPermission.这里不再介绍Android 6.0运行时权限了,直接看下如何使用RxPermission. RxPermission: 用于适配Android 6.0新的权限模型的开源框架. 下载地址:点此下载 如何使用? 1.)在app module的build.gradle中添加如下配置 使

  • 详解Android权限管理之Android 6.0运行时权限及解决办法

    前言: 今天还是围绕着最近面试的一个热门话题Android 6.0权限适配来总结学习,其实Android 6.0权限适配我们公司是在今年5月份才开始做,算是比较晚的吧,不过现在Android 6.0以上设备越来越多了,所以Android 6.0 权限适配是必不可少的工作,这里主要介绍一下我们公司是如何做Android 6.0权限适配的. Android 6.0以下非运行时权限: 根据上面博客我们很清楚的知道,Android的权限其实就是为了程序之间更加的安全的访问,所以权限有等级之分,比如:No

  • 一款不错的android6.0、7.0权限管理器推荐

    一款不错的android6.0.7.0权限管理器PermissionsCheckerUtil 初始化权限管理器:构造方法 private final Context mContext; public PermissionsCheckerUtil(Context mContext) { this.mContext = mContext; } 判断使用的权限集合是否已经全部允许 // 判断权限集合 public boolean lacksPermissions(String... permissio

  • 详解Android6.0运行时权限管理

    自从Android6.0发布以来,在权限上做出了很大的变动,不再是之前的只要在manifest设置就可以任意获取权限,而是更加的注重用户的隐私和体验,不会再强迫用户因拒绝不该拥有的权限而导致的无法安装的事情,也不会再不征求用户授权的情况下,就可以任意的访问用户隐私,而且即使在授权之后也可以及时的更改权限.这就是6.0版本做出的更拥护和注重用户的一大体现. 一.认知 今天我们就来学习下Android6.0的权限管理. Android6.0系统把权限分为两个级别: 一个是Normal Permiss

  • Android获取设备隐私 忽略6.0权限管理

    一.前言 (1).由于MIUI等部分国产定制系统也有权限管理,没有相关api,故无法判断用户是否允许获取联系人等隐私.在Android 6.0之后,新增权限管理可以通过官方api判断用户的运行状态: (2).我们指定targetSdkVersion为23或者之后我们还需要在运行时请求这些所需的权限.这很重要,因为已经出现了很多开发者把targetSdkVersion飙到了最新,然后发现自己的app疯狂的崩溃,这是由于他们没有实现执行运行时权限请求的代码.当你已经把一个targeting API

  • Android权限管理之Permission权限机制及使用详解

    前言: 最近突然喜欢上一句诗:"宠辱不惊,看庭前花开花落:去留无意,望天空云卷云舒." 哈哈~,这个和今天的主题无关,最近只要不学习总觉得生活中少了点什么,所以想着围绕着最近面试过程中讨论比较多的一个知识点Android 6.0 权限适配问题来进行学习,不过我不想直接进入这个主题,所以选择先去了解一下Android的Permission权限机制及使用 Android权限机制: 权限是一种安全机制.Android权限主要用于限制应用程序内部某些具有限制性特性的功能使用以及应用程序之间的组

  • Android 采用AOP方式封装6.0权限管理的方法

    [一]背景 6.0运行时申请权限已经是一个老生常谈的内容了,最近项目TargetSDKVersion升到23以上,所以我们也需要做权限管理,我想到的需求是这样的: 1.支持单个权限.多个权限申请 2.运行时申请 3.无侵入式申请,无需关注权限申请的逻辑 4.除了Activity.Fragment之外,还需要支持Service中申请 5.对国产手机做兼容处理 第一.二点,Google都有对应的API: 第三点可以通过自定义注解+AOP切面方式来解决.为什么采用AOP方式呢?首先看AOP定义: 面向

  • django认证系统实现自定义权限管理的方法

    本文记录使用django自带的认证系统实现自定义的权限管理系统,包含组权限.用户权限等实现. 0x01. django认证系统 django自带的认证系统能够很好的实现如登录.登出.创建用户.创建超级用户.修改密码等复杂操作,并且实现了用户组.组权限.用户权限等复杂结构,使用自带的认证系统就能帮助我们实现自定义的权限系统达到权限控制的目的. 0x02. 认证系统User对象 User对象顾名思义即为表示用户的对象,里面的属性包括: username password email first_na

  • vue axios封装及API统一管理的方法

    在vue项目中,每次和后台交互的时候,经常用到的就是axios请求数据,它是基于promise的http库,可运行在浏览器端和node.js中.当项目越来越大的时候,接口的请求也会越来越多,怎么去管理这些接口?多人合作怎么处理?只有合理的规划,才能方便往后的维护以及修改, 安装 安装axios依赖包 cnpm install axios --save 引入 一般会我会在项目src中新建一个untils目录,其中base用于管理接口域名,http处理请求拦截和响应拦截,user.js负责接口文件(

  • PHP巧妙利用位运算实现网站权限管理的方法

    首先我们先定义4个常量来设定四种权限: ===================================== define(ADD,1);//增加数据库记录的权限 define(UPD,2);//修改数据库记录的权限 define(SEL,4);//查找数据库记录的权限 define(DEL,8);//删除数据库记录的权限 ===================================== 接下来假设有3个用户: A用户拥有ADD-UPD-SEL-DEL四个权限,用位或运算计算A的

  • MySQL创建用户和权限管理的方法

    一.如何创建用户和密码 1.进入到mysql数据库下 mysql> use mysql Database changed 2.对新用户增删改 1.创建用户 # 指定ip:192.118.1.1的chao用户登录 create user 'chao'@'192.118.1.1' identified by '123'; # 指定ip:192.118.1.开头的chao用户登录 create user 'chao'@'192.118.1.%' identified by '123'; # 指定任何i

  • Android通过原生方式获取经纬度与城市信息的方法

    一.概述 在项目中需要获取用户所在位置的经纬度和城市上送给风控系统.一般来说,定位有两种方式: 用第三方SDK定位,如百度地图.高德地图.谷歌地图: 用Android原生SDK中的api定位: 本文讲述定位的第二种方式--用Android原生的SDK中的api定位,如果项目定位要求较高还是建议使用第三方地图库. 二.Android原生SDK中的api定位 Android原生方式获取经纬度两种定位方式:GPS定位和Wifi定位 GPS定位相比Wifi定位更精准且可在无网络情况下使用,但在室内基本暴

  • Spring Boot 通过AOP和自定义注解实现权限控制的方法

    本文介绍了Spring Boot 通过AOP和自定义注解实现权限控制,分享给大家,具体如下: 源码:https://github.com/yulc-coding/java-note/tree/master/aop 思路 自定义权限注解 在需要验证的接口上加上注解,并设置具体权限值 数据库权限表中加入对应接口需要的权限 用户登录时,获取当前用户的所有权限列表放入Redis缓存中 定义AOP,将切入点设置为自定义的权限 AOP中获取接口注解的权限值,和Redis中的数据校验用户是否存在该权限,如果R

  • vue2/vue3路由权限管理的方法实例

    1. Vue 路由权限控制一般有2种方法 a.路由元信息(meta) b.动态加载菜单和路由(addRoutes) 2 路由元信息(meta)来进行路由权限控制 2.1 在vue2种的实现 如果一个网站有不同的角色,比如 管理员 和 普通用户 ,要求不同的角色能访问的页面是不一样的 这个时候我们就可以 把所有的页面都放在路由表里 ,只要 在访问的时候判断一下角色权限 .如果有权限就让访问,没有权限的话就拒绝访问,跳转到404页面 vue-router 在构建路由时提供了元信息 meta 配置接口

随机推荐