[转]权限管理库
1. 1背景
6.0运行时申请权限已经是一个老生常谈的内容了,最近项目TargetSDKVersion升到23以上,所以我们也需要做权限管理,我想到的需求是这样的:
- 支持单个权限、多个权限申请
- 运行时申请
- 无侵入式申请,无需关注权限申请的逻辑
- 除了Activity、Fragment之外,还需要支持Service中申请
- 对国产手机做兼容处理
第一、二点,Google都有对应的API;
第三点可以通过自定义注解+AOP切面方式来解决。
为什么采用AOP方式呢?首先看AOP定义: 面向切面编程(Aspect-Oriented Programming)。
如果说,OOP(面向对象)如果是把问题划分到单个模块的话,那么AOP就是把涉及到众多模块的某一类问题进行统一管理。 因为我们申请权限的逻辑都是基本一样的,所以可以把申请权限的逻辑统一管理。
第四点稍微有点麻烦,因为Google提供的API只支持在Activity和Fragment中去申请权限,Service中并没有相应的API,比如项目中的某个Service里需要拿到当前位置信息,并且不能确定定位权限已经给了,所以在定位之前仍然需要判断有没有定位权限,按照常规逻辑好像是行不通了。肿么办呢?先说一下我想到的办法:通过一个透明的Activity去申请权限,并且把申请结果返回来,最后实践也是这么做的,具体思路请往下看。
第五点也比较麻烦,如果都按Google标准来,那就不用考虑兼容问题了,但是国产安卓手机碎片化比较严重,且基本都修改了ROM,导致申请权限的API跟期望返回的结果不一致,这种的可能就需要特殊处理了。
调研了一下比较流行的三方库,如 PermissionsDispatcher、RxPermissions,做了一个简单的总结:
https://github.com/permissions-dispatcher/PermissionsDispatcher
https://github.com/tbruyelle/RxPermissions
RxPermissions是基于RX的思想,支持链式调用,简单方便,但是他不支持Service调用;
PermissionsDispatcher使用了编译时解析注解的方式,通过apt自动生成.class方式帮我们去写申请权限的逻辑,很好很强大,并且适配了小米手机,但是它也不支持Service中去申请权限。考虑到我们项目中的应用场景并且借鉴了PermissionsDispatcher的申请权限的逻辑,决定基于AOP方式手动撸一个权限管理库出来。
2. 2效果图
先上一下最终的效果图:
效果图有点模糊,可以下载源码运行一下看效果。
3. 3整体思路
首先,先定义一个说法,弹出系统权限弹窗,用户没有给权限,并且选中不再提示,这种情况称为权限被拒绝;
如果用户没有给权限,但是没有选中不再提示,这种情况称为权限被取消。
申请权限、权限被取消、权限被拒绝都是采用注解的形式,分别为@NeedPermission、@PermissionCanceled、@PermissionDenied,注解都是声明在Method级别上的。
在我们的Activity、Fragment及Service中声明注解,然后在AOP中解析我们的注解,并把申请的权限传递给一个透明的Activity去处理,并把处理结果返回来。这就是整体思路,可能会遇到的问题:
1、 不同型号的手机兼容问题(申请权限、跳设置界面)
2、AOP解析注解以及传值问题
上面说了很多,其实用一个图来表示更清晰一些:
OK,通过上面的图是不是更清晰了呢?其实最关键的地方就是AOP解析注解及传值。AOP面向切面编程是一种编程思想,而AspectJ是对AOP编程思想的一个实践,本文采用AspectJ来实现切面编程,简单介绍AspectJ的几个概念:
– JPoint:代码可注入的点,比如一个方法的调用处或者方法内部,对于本文来说即是注解作用的方法。
– Pointcut:用来描述 JPoint 注入点的一段表达式。见下面例子
– Advice:常见的有 Before、After、Around 等,表示代码执行前、执行后、替换目标代码,也就是在 Pointcut 何处编织代码。
– Aspect:切面,Pointcut 和 Advice 合在一起称作 Aspect。
关于AspectJ的介绍及用法的文章很多,不了解的朋友可以去google下,直接列一下AOP切面代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
<span class="">@Aspect</span> <span class="">public</span> <span class="">class PermissionAspect </span>{ <span class="">private</span> static <span class="">final</span> String PERMISSION_REQUEST_POINTCUT = <span class="">"execution(@com.ninetripods.aopermission.permissionlib.annotation.NeedPermission * *(..))"</span>; <span class="">@Pointcut(PERMISSION_REQUEST_POINTCUT + " && @annotation(needPermission)")</span> <span class="">public</span> void requestPermissionMethod(NeedPermission needPermission) { } <span class="">@Around("requestPermissionMethod(needPermission)")</span> <span class="">public</span> void AroundJoinPoint(<span class="">final</span> ProceedingJoinPoint joinPoint, NeedPermission needPermission) { Context context = <span class="">null</span>; <span class="">final</span> Object <span class="">object</span> = joinPoint.getThis(); <span class="">if</span> (<span class="">object</span> instanceof Context) { context = (Context) <span class="">object</span>; } <span class="">else</span> <span class="">if</span> (<span class="">object</span> instanceof Fragment) { context = ((Fragment) <span class="">object</span>).getActivity(); } <span class="">else</span> <span class="">if</span> (<span class="">object</span> instanceof android.support.v4.app.Fragment) { context = ((android.support.v4.app.Fragment) <span class="">object</span>).getActivity(); } <span class="">if</span> (context == <span class="">null</span> || needPermission == <span class="">null</span>) <span class="">return</span>; PermissionRequestActivity.PermissionRequest(context, needPermission.value(), needPermission.requestCode(), new IPermission() { <span class="">@Override</span> <span class="">public</span> void PermissionGranted() { <span class="">try</span> { joinPoint.proceed(); } <span class="">catch</span> (Throwable throwable) { throwable.printStackTrace(); } } <span class="">@Override</span> <span class="">public</span> void PermissionDenied(int requestCode, List<String> denyList) { Class<?> cls = <span class="">object</span>.getClass(); Method[] methods = cls.getDeclaredMethods(); <span class="">if</span> (methods == <span class="">null</span> || methods.length == <span class="">0</span>) <span class="">return</span>; <span class="">for</span> (Method method : methods) { <span class="">//过滤不含自定义注解PermissionDenied的方法</span> boolean isHasAnnotation = method.isAnnotationPresent(PermissionDenied.<span class="">class</span>); <span class="">if</span> (isHasAnnotation) { method.setAccessible(<span class="">true</span>); <span class="">//获取方法类型</span> Class<?>[] types = method.getParameterTypes(); <span class="">if</span> (types == <span class="">null</span> || types.length != <span class="">1</span>) <span class="">return</span>; <span class="">//获取方法上的注解</span> PermissionDenied aInfo = method.getAnnotation(PermissionDenied.<span class="">class</span>); <span class="">if</span> (aInfo == <span class="">null</span>) <span class="">return</span>; <span class="">//解析注解上对应的信息</span> DenyBean bean = new DenyBean(); bean.setRequestCode(requestCode); bean.setDenyList(denyList); <span class="">try</span> { method.invoke(<span class="">object</span>, bean); } <span class="">catch</span> (IllegalAccessException e) { e.printStackTrace(); } <span class="">catch</span> (InvocationTargetException e) { e.printStackTrace(); } } } } <span class="">@Override</span> <span class="">public</span> void PermissionCanceled(int requestCode) { Class<?> cls = <span class="">object</span>.getClass(); Method[] methods = cls.getDeclaredMethods(); <span class="">if</span> (methods == <span class="">null</span> || methods.length == <span class="">0</span>) <span class="">return</span>; <span class="">for</span> (Method method : methods) { <span class="">//过滤不含自定义注解PermissionCanceled的方法</span> boolean isHasAnnotation = method.isAnnotationPresent(PermissionCanceled.<span class="">class</span>); <span class="">if</span> (isHasAnnotation) { method.setAccessible(<span class="">true</span>); <span class="">//获取方法类型</span> Class<?>[] types = method.getParameterTypes(); <span class="">if</span> (types == <span class="">null</span> || types.length != <span class="">1</span>) <span class="">return</span>; <span class="">//获取方法上的注解</span> PermissionCanceled aInfo = method.getAnnotation(PermissionCanceled.<span class="">class</span>); <span class="">if</span> (aInfo == <span class="">null</span>) <span class="">return</span>; <span class="">//解析注解上对应的信息</span> CancelBean bean = new CancelBean(); bean.setRequestCode(requestCode); <span class="">try</span> { method.invoke(<span class="">object</span>, bean); } <span class="">catch</span> (IllegalAccessException e) { e.printStackTrace(); } <span class="">catch</span> (InvocationTargetException e) { e.printStackTrace(); } } } } }); } } |
代码有点多,但是思路还是挺清晰的,首先定义@Pointcut(描述的是我们的注解@NeedPermission),接着由Advice(@Around)及Pointcut构成我们的切面Aspect, 在切面Aspect中,通过joinPoint.getThis()根据不同来源来获得Context,接着跳转到一个透明Activity申请权限并通过接口回调拿到权限申请结果,最后在不同的回调方法里通过反射把回调结果回传给调用方。
4. 4使用举例
为了简化AspectJ的各种配置,这里用了一个三方的gradle插件:
https://github.com/HujiangTechnology/gradle_plugin_android_aspectjx
1、权限库引入方式,在app模块的build.gradle中引入如下:
1 2 3 4 5 6 |
<span class="">apply</span> plugin: <span class="">'android-aspectjx'</span> dependencies { <span class="">compile</span> <span class="">'com.ninetripods:aop-permission:1.0.1'</span> ..........其他............ } |
2、在整个工程的build.gradle里面配置如下:
1 2 3 4 5 6 |
<span class="">dependencies</span> { <span class="">classpath</span> <span class="">'com.android.tools.build:gradle:2.3.3'</span> classpath <span class="">'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:1.0.8'</span> ................其他................ } |
说明: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住, 比如:
1 2 3 4 5 6 7 8 9 10 |
<span class="">package</span> com.hujiang.test; <span class="">public</span> <span class="">class A </span>{ <span class="">@NeedPermission</span> <span class="">public boolean funcA(String args) </span>{ .... } } <span class="">//如果你在AOP代码里对A#funcA(String)进行hook, 那么在混淆配置文件里加上这样的配置</span> -keep <span class="">class com.hujiang.test.A </span>{*;} |
4、终于配好了,都闪开,我要开始举栗子了:
下面以Activity中申请权限为例,Fragment、Service中使用是一样的,就不一一写了,源码中也有相应使用的Demo
4-1. 4.1 申请单个权限
申请单个权限:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
btn_click.setOnClickListener(<span class="">new</span> View.OnClickListener() { @<span class="">Override public void onClick(View v) </span>{ callMap(); } }); <span class="">/** * 申请权限 */</span> @NeedPermission(<span class="">value</span> = {Manifest.permission.ACCESS_FINE_LOCATION}, requestCode = <span class="">0</span>) <span class="">private void callMap() </span>{ Toast.makeText(<span class="">this</span>, <span class="">"定位权限申请通过"</span>, Toast.LENGTH_SHORT).show(); } |
@NeedPermission后面的value代表需要申请的权限,是一个String[]数组;requestCode是请求码,是为了区别开同一个Activity中有多个不同的权限请求,默认是0,如果同一个Activity中只有一个权限申请,requestCode可以忽略不写。
1 2 3 4 5 6 7 8 9 10 |
<span class="">/** * 权限被取消 * * @param bean CancelBean */</span> @<span class="">PermissionCanceled</span> public void dealCancelPermission(CancelBean bean) { <span class="">Toast</span><span class="">.makeText</span>(<span class="">this</span>, "<span class="">requestCode</span><span class="">:"</span> + <span class="">bean</span><span class="">.getRequestCode</span>(), <span class="">Toast</span><span class="">.LENGTH_SHORT</span>)<span class="">.show</span>(); } |
声明一个public方法接收权限被取消的回调,方法必须有一个CancelBean类型的参数,这点类似于EventBus,CancelBean中有requestCode变量,即是我们请求权限时的请求码。
1 2 3 4 5 6 7 8 9 10 11 |
<span class="">/** * 权限被拒绝 * * @param bean DenyBean */</span> @<span class="">PermissionDenied</span> public void dealPermission(DenyBean bean) { <span class="">Toast</span><span class="">.makeText</span>(<span class="">this</span>, "<span class="">requestCode</span><span class="">:"</span> + <span class="">bean</span><span class="">.getRequestCode</span>()+ ",<span class="">Permissions</span>: " + <span class="">Arrays</span><span class="">.toString</span>(<span class="">bean</span><span class="">.getDenyList</span>()<span class="">.toArray</span>()), <span class="">Toast</span><span class="">.LENGTH_SHORT</span>)<span class="">.show</span>(); } |
声明一个public方法接收权限被取消的回调,方法必须有一个DenyBean类型的参数,DenyBean中有一个requestCode变量,即是我们请求权限时的请求码,另外还可以通过denyBean.getDenyList()来拿到被权限被拒绝的List。
4-2. 4.2 申请多个权限
基本用法同上,区别是@NeedPermission后面声明的权限是多个,如下:
1 2 3 4 5 6 7 8 |
<span class="">/** * 申请多个权限 */</span> @NeedPermission(<span class="">value</span> = {Manifest.permission.CALL_PHONE, Manifest.permission.CAMERA}, requestCode = <span class="">10</span>) <span class="">public void callPhone() </span>{ Toast.makeText(<span class="">this</span>, <span class="">"电话、相机权限申请通过"</span>, Toast.LENGTH_SHORT).show(); } |
4-2-1. 4.3 跳转到设置类
当用户拒绝权限并选中不再提示后,需要引导用户去设置界面打开权限,因为国产手机各个设置界面不一样,用通用的API可能会跳转不到相应的APP设置界面,这里采用了策略模式(下图所示)
如需做兼容,只需要在库里修改,调用方是不需要处理的,调用方如需跳转到设置界面,只需像下面这样调用就OK了:
5. 5总结
回看一下我们的需求,基本上都实现了:
1、首先通过@NeedPermission、@PermissionCanceled、@PermissionDenied三个注解来分别定义权限申请、被取消、被拒绝三种情况,如果不想处理被取消的逻辑就不用使用@PermissionCanceled注解,其他权限申请的逻辑调用方不用关心,是完全解耦的;
2、同时支持在Activity、Fragment、Service中去申请权限;
3、最后关于申请权限、跳设置界面的兼容问题,因为身边的手机有限,不能测试出所有兼容问题,需要后续优化。
关于在AOP中通过反射方式把权限申请结果返回给调用方,是参考了EventBus的方式,感觉这样用起来更方便一些;之前的做法是在AOP对应的Java类中声明接口,调用方实现该接口,然后通过接口回调的方式将权限申请结果回传,也能实现同样效果,但是感觉没有反射方式更方便。以上就是全部内容,后面会贴出源码,如有使用不当之处,欢迎批评指正!
源码地址:
https://github.com/crazyqiang/Aopermission
[转]一起来封装个权限管理库