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

Posted Android编程精选

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android采用AOP方式封装权限管理相关的知识,希望对你有一定的参考价值。

点击上方“ Android编程精选”,选择“置顶公众号”

关键时刻,第一时间送达!















































































































































































































































































































    先不说楚枫的这般年纪,能够踏入元武一重说明了什么,最主要的是,楚枫在刚刚踏入核心地带时,明明只是灵武七重,而在这两个月不到的时间,连跳两重修为,又跳过一个大境界,踏入了元武一重,这般进步速度,简直堪称变态啊。


    “这楚枫不简单,原来是一位天才,若是让他继续成长下去,绝对能成为一号人物,不过可惜,他太狂妄了,竟与龚师兄定下生死约战,一年时间,他再厉害也无法战胜龚师兄。”有人认识到楚枫的潜力后,为楚枫感到惋惜。


    “哼,何须一年,此子今日就必败,巫九与龚师兄关系甚好,早就看他不顺眼了,如今他竟敢登上生死台挑战巫九,巫九岂会放过他?”但也有人认为,楚枫今日就已是在劫难逃。


    “何人挑战老子?”就在这时,又是一声爆喝响起,而后一道身影自人群之中掠出,最后稳稳的落在了比斗台上。


    这位身材瘦弱,身高平平,长得那叫一个猥琐,金钩鼻子蛤蟆眼,嘴巴一张牙带色儿,说话臭气能传三十米,他若是当面对谁哈口气,都能让那人跪在地上狂呕不止。


    不过别看这位长得不咋地,他在核心地带可是鼎鼎有名,剑道盟创建者,青龙榜第九名,正是巫九是也。


    “你就是巫九?”楚枫眼前一亮,第一次发现,世间还有长得如此奇葩的人。


    巫九鼻孔一张,大嘴一咧,拍着那干瘪的肚子,得意洋洋的道:“老子就是巫九,你挑战老子?”


    “不是挑战你,是要宰了你。”楚枫冷声笑道。


    “好,老子满足你这个心愿,长老,拿张生死状来,老子今日在这里了解了这小子。”巫九扯开嗓子,对着下方吼了一声。


    如果他对内门长老这么说话,也就算了,但是敢这么跟核心长老说话的,他可真是算作胆肥的,就连许多核心弟子,都是倒吸了一口凉气,心想这楚枫够狂,想不到这巫九更狂。


    不过最让人无言的就是,巫九话音落下不久,真有一位核心长老自人群走出,缓缓得来到了比斗台上,左手端着笔墨,右手拿着生死状,来到了巫九的身前。


    “我去,这巫九什么身份,竟能这般使唤核心长老?”有人吃惊不已,那长老修为不低,乃是元武七重,比巫九还要高两个层次,但却这般听巫九的话,着实让人吃惊不已。


    “这你就不知道了吧,巫九在前些时日,拜了钟离长老为师尊,已正式得到钟离长老的亲传。”有人解释道。


    “钟离长老?可是那位性情古怪的钟离一护?”


    “没错,就是他。”


    “天哪,巫九竟然拜入了他的门下?”


    人们再次大吃一惊,那钟离一护在青龙宗可是赫赫有名,若要是论其个人实力,在青龙宗内绝对能够排入前三,连护宗六老单打独斗都不会是他的对手。


    只不过那钟离一护,如同诸葛青云一样,也是一位客卿长老,所以在青龙宗内只是挂个头衔,什么事都不管,更别说传授宗内弟子技艺了,如今巫九竟然能拜入他老人家门下,着实让人羡慕不已。


    “恩怨生死台,的确可以决斗生死,但必须要有所恩怨,你们两个人,可有恩怨?”那位长老开口询问道。































































































来源:安卓巴士android开发者门户

https://mp.weixin.qq.com/s/bd8f0QGcbhceGAlTby2GFA

Android编程精整理发布,转载请联系作者获得授权

【一】背景

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跟期望返回的结果不一致,这种的可能就需要特殊处理了。

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

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

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

【二】效果图

先上一下最终的效果图:

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

运行效果图

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

【三】整体思路

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

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

Android采用AOP方式封装权限管理
UML时序图

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

  • JPoint:代码可注入的点,比如一个方法的调用处或者方法内部,对于本文来说即是注解作用的方法。

  • Pointcut:用来描述 JPoint 注入点的一段表达式。见下面例子

  • Advice:常见的有 Before、After、Around 等,表示代码执行前、执行后、替换目标代码,也就是在 Pointcut 何处编织代码。

  • 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 == nullreturn;

        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 == 0return;
                        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 != 1return;
                                //获取方法上的注解
                                PermissionDenied aInfo = method.getAnnotation(PermissionDenied.class);
                                if (aInfo == nullreturn;
                                //解析注解上对应的信息
                                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 == 0return;
                        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 != 1return;
                                //获取方法上的注解
                                PermissionCanceled aInfo = method.getAnnotation(PermissionCanceled.class);
                                if (aInfo == nullreturn;
                                //解析注解上对应的信息
                                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'
    ................其他................
}

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();
}

4.3 跳转到设置类

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

跳转到设置类

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

SettingUtil.go2Setting(this);

【五】总结

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

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

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

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

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

【六】源码

https://github.com/crazyqiang/Aopermission

以上是关于Android采用AOP方式封装权限管理的主要内容,如果未能解决你的问题,请参考以下文章

AOP面向切面编程(个人笔记1.1)

spring AOP 整理

Spring AOP 应用:三种配置及实现方式

Android6.0M权限管理实战,完美轻量级封装

Android6.0M权限管理实战,完美轻量级封装

Spring的学习(Spring中的AOP)