AOP优雅权限框架详解(以及更多面试题)
https://github.com/xiangjiana/Android-MS
gradle配置
-
在project的
build.gradle
添加aspectJ gradle
插件} dependencise { classpath 'com.android.tools.build:gradle:3.5.0' //1_1.grade-android-plugin-aspectjx classpath 'com.hujiang.aspectjx:gradle-android-plugun-aspectjx:2.0.5' //2_1.android-maven-gradle-plugin classpath 'com.github.dcendents:android-maven-gradler-plugin:2.1'//ADD //NOTE: Do not place your application dependencise here; they belong //in the individual module build.gradle files }
- permission model 的
build.gradle
引入 aspect类库
- app module 的
build.gradle
中启用aspectJ
插件,并且引入permissionmodule
Java代码
-
appmodule
是使用框架的地方上面我说到了,使用框架思想,消除了Activity,Fragment,Service,普通类 在申请权限时的差异性,可以全部以普通类的方式来申请权限并且处理回调。所以这里展示 Activity,Fragment,Service 的动态权限申请写法。
普通类
public class LocationUtil { private String TAG ="LocationUtil"; @PermissionNeed( permissions ={Manifest.permission.ACCESS_FINE_LOCATION, requestCode = PermissionRequestCodeConst.REQUEST_CODE_LOCATION) public void getLocation() { Log.e(TAG,"申请位置权限之后,我要获取经纬度"); } /** * 这里写的要特别注意,denied方法,必须是带有一个int参数的方法,下面的也一样 * @param requestCode */ @permissionDenied public void denied(int requestcode) { Log.e(TAG, "用户不给阿''); } @permisssionDeniedForever public void denidFoever(int requestcode) { Log.e(TAG,''用户永久拒绝''); } }
Activity
pubilc class MainActivity extends AppcompatActivity { private static final string TAG = ''permissionAspectTag''; @override prtected void onCreate(Bundle saveInstanceState) { super.onCreate(saveInstanceState); setcontenceView(R.layout.activity_main);
findViewById(R.id.btn_location).setonclicklistener(v ->getlocationpermission()
findviewById(R.id.btn_contact).setonclicklistener(v ->getcontactpermission()); br/>}
@permissionNeed(
permissions = {Mainfest.permission.READCONTACTS,Mainfest.permission.WRITE,Manifest.permission.GET_ACCOUNTS},
requestcode = permissionsRequestcodeconst.REQUEST_CODE_CONTACT
private void getcontactpermission() {br/>log.d(TAG,''getcontactpermission'');
}
@PermissionNeed(
permissions ={Manifest.permission.ACCESS_FINE_LOCATION,Manifest.permission.ACCESS_COARSE_LOCATION},
requestCode = PermissionRequestCodeConst.REQUEST_CODE_LOCATION)
private void getLocationPermission() {
Log.d(TAG,"getLocationPermission");}
}
##### Fragment
public class MyFragment extends Fragment {br/>@Nullable
@Override
public View onCreateView(LayoutInflater inflater,@Nullable ViewGroup container,Bundle savedInstanceState) {
getLocation();
return super.onCreateView(inflater,container, savedInstanceState);}
private String TAG ="LocationUtil";@PermissionNeed(
permissions ={Manifest.permission.ACCESS_FINE_LOCATION,Manifest.permission.ACCESS_COARSE_LOCATION},
requestCode = PermissionRequestCodeConst.REQUEST_CODE_LOCATION)public void getLocation() {
Log.e(TAG,"申请位置权限之后,我要获取经纬度");
}
/**- 这里写的要特别注意,denied方法,必须是带有一个int参数的方法,下面的也一样
- @param requestCodebr/>*/
@PermissionDenied
public void denied(int requestCode) {}
@PermissionDeniedForever
br/>Log.e(TAG,"用户不给啊");
}
@PermissionDeniedForever
public void deniedForever(int requestCode) {
Log.e(TAG,"用户永久拒绝");
}
}
##### Service
public class MyService extends Service {br/>@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;br/>}
@Override
public int onStartCommand(Intent intent,int flags,int startId) {
getLocation();
return super.onStartCommand(intent, flags, startId);
}
private String TAG ="LocationUtil";@PermissionNeed(
permissions ={Manifest.permission.ACCESS_FINE_LOCATION,Manifest.permission.ACCESS_COARSE_LOCATION},
requestCode = PermissionRequestCodeConst.REQUEST_CODE_LOCATION)public void getLocation() {
Log.e(TAG,"申请位置权限之后,我要获取经纬度");
}
/*
这里写的要特别注意,denied方法,必须是带有一个int参数的方法,下面的也一样- @param requestCodebr/>*/
@PermissionDenied
public void denied(int requestCode) {}
@PermissionDeniedForever
br/>Log.e(TAG,"用户不给啊");
}
@PermissionDeniedForever
public void deniedForever(int requestCode) {
Log.e(TAG,"用户永久拒绝");
}
}
> 经过观察,Activity,Fragment,Service,和普通类,都是定义了一个或者多个被 `@PermissionNeed`注解修饰的方法, 如果是多个,还要在 `@PermissionDenied`和 `@PermissionDeniedForever`修饰的方法 中switch处理 `requestCode`(参考上方Activity),以应对申请多次申请不同权限的结果 。 也许除了这4个地方之外,还有别的地方需要申请动态权限,但是既然我们消除了差异性,就可以全部以普通类的方式来申请权限以及处理回调。这才叫从根本上解决问题。
这里有个坑: 被 @PermissionDenied
和 @PermissionDeniedForever
修饰的方法,必须有且仅有一个int类型参数, 返回值随意.
-
zpermission
module
这里包含了框架的核心代码,现在一步一步讲解
类结构图
3个注解@PermissionDenied
@PermissionDeniedForever
@PermissionNeed
/**
* 被此注解修饰的方法,会在方法执行之前去申请相应的权限,只有用户授予权限,被修饰的方法体才会执行
*/
@Target(ElementType.METHOD)//此注解用于修饰方法
@Retention(RetentionPolicy.RUNTIME)//注解保留到运行时,因为可能需要反射执行方法(上面说了修饰的是方法!)
public @interface PermissionNeed {
String[] permissions();//需要申请的权限,支持多个,需要传入String数组
int requestCode()default 0;//此次申请权限之后的返回码
}
/**
* 被此注解修饰的方法,会在权限申请失败时被调用
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PermissionDenied {
}
/**
* 被此注解修饰的方法,会在用户永久禁止权限之后被调用
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PermissionDeniedForever {
}
处理权限回调结果的接口 IPermissionCallback
/**
* 权限申请结果接口
*/
public interface IPermissionCallback {
/**
* 授予权限
*/
void granted(int requestCode);
/**
* 这次拒绝,但是并没有勾选"以后不再提示"
*/
void denied(int requestCode);
/**
* 勾选"以后不再提示",并且拒绝
*/
void deniedForever(int requestCode);
}
以上都是事先要预备好的东西,接下来进入核心
PermissionAspect
类
@Aspect
public class permissinbAspect {
private static final String TAG = "PermissionAspectTag";
private final String pointcutExpression="execution(@com.zhou.zpermission.annotation.PermissionNeed * *(..)) && @annotation(permissionNeed)";
@Pointcut(value = pointcutExpression)
public void requestPermission(PermissionNeed permissionNeed) {
Log.d(TAG,"pointCut 定义切入点");
}
@Around("requestPermission(permissionNeed)")
Log.d(TAG,"pointCut 定义切入点");
....
}
此段代码解读如下:
-
使用
@Aspect
注解来修饰类 ,@Aspect
是来自AspectJ
框架的注解,被它修饰的类,在编译时会被认为是一个切面类 - 使用
@Pointcut
注解来修饰方法requestPermission()
,被它修饰的方法会被认为是一个切入点.所谓切入点,就是 面向切面编程时,我们无侵入式地插入新的逻辑,总要找到一个确切的位置,我们要知道程序执行到哪一行的时候,轮到我们出场了!切入点,一定是方法, 不能是随意哪一段代码!
切入点可以是以下类型,不同的类型有不同的语法,我目前使用的是 method execution ,也就是 函数执行时。这意味着,当切入点的方法即将开始执行的时候,我们插入的逻辑将会被执行。与之类似的有一个 method call ,这个不一样,这个是在切入点的方法 被调用时,也就是说,当侦测到该方法被外界调用的时候,而非方法自己执行。这两者有细微差别。至于其他的类型,暂且按下不详述。
除了类型之外,这里还有一个重点,那就是 MethodSignature的概念,这个类似于 jni里的方法签名,是为了标记一个或者一类方法, AspectJ框架通过这个方法签名,来确定 JVM的所有class对象中,有哪些方法需要被插入 新的逻辑。
具体的签名的语法规则为:
看不懂? 看不懂就对了,举个例子:execution(@com.zhou.zpermission.annotation.PermissionNeed**(..))&&@annotation(permissionNeed)
这是Demo中我这么写的,现在逐步解析:
execution
表示方法执行时作为切入点@com.zhou.zpermission.annotation.PermissionNeed
表示 切入点的方法必须有这个注解修饰**(..))
这个比较诡异,我们知道,一个方法写完整一点可能是这个样子private void test(int a)
但是如果我们不计较 访问权限,不计较返回值类型,也不计较 函数名,甚至不计较参数列表的话,就可以写成这个样子 **(..)) . 表示任意方法
除此之外,还有后半截 &&@annotation(permission)
,它的含义为:
切入点方法需要接收来自 注解的参数。
即 切入点@Pointcut
规定切入点的时候,只识别被@com.zhou.zpermission.annotation.PermissionNeed
标记的方法,但是这个@com.zhou.zpermission.annotation.PermissionNeed
注解,是有自己的参数值的,所以,必须传入这个值给到切入方法requestPermission(PermissionNeedpermissionNeed)
去使用。
有点绕!一张图说清楚:
图中3个字符串必须一摸一样,不然编译就会报错,而且报错原因还不明确。
使用 @Around 注解来修饰 方法 doPermission()
,被它修饰的方法会被认为是一个 切入策略。
Around注解的参数为:"requestPermission(permissionNeed)"
, 也就是 pointcut
修饰的方法名(形参名)
在我们已经定义好切入点
requestPermission(PermissionNeedpermissionNeed)
的前提下,如果程序已经执行到了切入点,那么我是选择怎么样的策略, 目前所选择的策略是 Around ,也就是,完全替代切入点的方法,但是依然保留了 执行原方法逻辑的可能性 joinPoint.proceed();
除了@Around
策略之外,还有以下:PermissionAspect
类的作用是: 定义切入点和切入策略,那么现在我们确定切入点是 被注解 @PermissionNeed
修饰的方法,切入策略是 @Around,那么,切入之后我们做了哪些事呢?
接下往下看...
PermissionAspectActivity
类
public class permissionAspectActivity extends AppcompatActivity {
private final static String permissionsTag = "permissions";
private final static String requestCodeTag = "requestCode";
private static IPermissionCallback mCallback;
/**
* 启动当前这个Activity
*/
public static void startActivity(Context context, String[] permissions,int requestCode,IPermissionCallback callback) {
Log.d("PermissionAspectTag","context is : "+ context.getClass().getSimpleName());
if (context == null) return;
mCallback = callback;
//启动当前这个Activiyt并且取消切换动画
Intent intent = new Intent(context,PermissionAspectActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |Intent.FLAG_ACTIVITY_CLEAR_TOP);
//开启新的任务栈并且清除栈顶...为何要清除栈顶
intent.putExtra(permissionsTag, permissions);
intent.putExtra(requestCodeTag, requestCode);
context.startActivity(intent);
//利用context启动activity
if (context instanceof Activity) {
//并且,如果是activity启动的,那么还要屏蔽掉activity切换动画
((Activity) context).overridePendingTransition(0, 0);
}
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
String[] permissions = intent.getStringArrayExtra (permissionsTag);
int requestCode = intent.getIntExtra(requestCodeTag,0);
if (PermissionUtil.hasSelfPermissions(this, permissions)) {
mCallback.granted(requestCode);
finish();
overridePendingTransition(0,0);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestPermissions(permissions, requestCode);
}
}
@Override
public void onRequestPermissionsResult(int requestCode,@NonNull String[] permissions, @NonNull int[] grantResults) {
//现在拿到了权限的申请结果,那么如何处理,我这个Activity只是为了申请,然后把结果告诉外界,所以结果的处理只能是外界传进来
boolean granted = PermissionUtil.verifyPermissions(grantResults);
if (granted) {
//如果用户给了权限
mCallback.granted(requestCode);
} else {
if (PermissionUtil.shouldShowRequestPermissionRationale(this ,permissions)) {
mCallback.denied(requestCode);
} else {
mCallback.deniedForever(requestCode);
}
}
finish();
overridePendingTransition(0,0);
}
}
解读:
1.提供一个静态方法
publicstaticvoidstartActivity(Contextcontext,String[]permissions,intrequestCode,IPermissionCallbackcallback),
用于启动自己PermissionAspectActivity,
接收的参数分别为:context
,需要的权限数组,权限返回码,权限结果回调接口
onCreate
方法中,检查是否已经有想要申请的权限,如果有,直接调用mCallback.granted(requestCode);
并且结束自身,并且要注意隐藏Activity的切换动画。如果没有,那么,就去requestPermissions(permissions,requestCode);
申请权限。- 处理权限申请的回调,并且分情况调用
mCallback
的回调方法,然后结束自身
需要注意:PermissionAspectActivity
必须在module的清单文件中注册
并且 要定义它的 theme使得Activity完全透明
Gif图效果演示:
AOP思想以及常用AOP框架
所谓
AOP
(ApsectOrientedProgramming
) 面向切面编程。此概念是基于
OOP
(ObjectOrientiedProgramming
)面向对象编程。在OOP
中,我们可以把不同的业务功能都分成一个一个的模块,然后每一个模块有自己的专一职责,从而优化编程过程,降低编程犯错几率。但是随着OOP类的数量的增加,我们会发现,在某一些业务类中,经常有一些相同的代码在重复编写,但是无可奈何,比如日志打印/动态权限申请/埋点数据上报/用户登录状态检查 /服务器端口连通性检查 等等。这些代码,我们虽然可以他们抽离出来整理到一个个专一的模块中,但是调用的时候,还是到处分散的,并且这些调用还***了本来不直接相关的业务代码,让我们阅读业务代码十分费劲。而AOP的出现,就是基于OOP的这种缺陷而出现的优化方案。利用AOP,我们可以对业务逻辑的各个部分进行隔离,使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,提高开发效率,减少犯错概率。
画图表示:
如上图,OOP中,同样的一段过程,我们把登录检查,权限检查,埋点上报的调用代码写了3遍,然而都是雷同代码,只是参数不同而已。而,换成AOP的思想来编码。则是如下:
所采取的方案为:
在class A , B, C中 找到切入点,然后在切入点插入共同的逻辑,而不是多次编写雷同的代码。
本文的Demo中,插入相同的逻辑,使用的是 Java自定义注解+@Aspect切面类+@PointCut切入点+@Around切入策略 的方式。这只是AOP方案的一种,叫做 AspectJ
。
除此之外,Android开发中常用的AOP方案还有:
(Java注解存在3个阶段,一个是源码期,一个是编译期,一个运行期)
APT
Java的注解解析技术(AnnotationProcessingTool), Apt的作用时期,是 通过 自定义注解解析类(extends AbastractProcessor),对自定义注解进行解析,然后通过JavaPoet这种java类生成工具,来生成编译期才会有的.java(源码中并没有),然而我们源码中却可以使用这个类。
ASM
Asm是Java的字节码操作框架,它可以动态生成类或者增强既有类的功能。理论上,它可以对class文件做任何他想做的事。包括,改变class文件的内容,或者生成新的class。严格来说AspectJ
底层就是ASM
,只不过AspectJ
帮我们做了ASM
框架做起来很麻烦,容易出错的事情,让我们可以简单的通过 @Aspect
@PointCut
@Around
这样的注解,就能完成AOP
面向切面编程。但是,ASM
作为AspectJ
的祖宗,某些时候依然可以完成AspectJ
所无法触及到的功能, 就像是c/c++作为Java的祖宗, 现在依然有自己不可替代的作用。
AspectJ AOP框架的深入原理研究
本来想写成一篇,但是发现篇幅太长,留个尾巴,下一篇,解析AspectJ是如何通过@注解的方式来插入逻辑的。
文章太长了,顺手留下GitHub链接,需要获取相关内容的可以自己去找
https://github.com/xiangjiana/Android-MS