从Android优雅权限框架理解AOP思想(1) 表层篇

前言

上一个大的系列文章叫 "手把手讲解", 历时10个月,出产博文二十余篇,讲解细致,几乎每一篇都提供了详实的原理讲解,提供了可运行github Demo,并且针对Demo中的关键地地方进行了重点拆解。相信每一位详细阅读文章的同行都会有所收获。但是,讲解虽详细,但是缺乏对于技术的深度的挖掘。

从今天开始开辟新的专题: 移动架构师专业技能深入浅出,以一步步成为架构师为目标,详述一项架构师技能的最直接使用价值横向周边知识以及纵深专业技术.

最直接使用价值: 网上最怕看到一种文章,全文开篇高大上,让人觉得遥不可及,通篇看下来却没有展示技术如何落地,落地之后是何种效果。文章写出来,就要以最容易让人接受的方式带读者进入作者的世界,而不是装作一副高高在上的样子俯视众生。所以,文章开篇,一定是最直接的展示技术的落地效果。提供可运行Demo可以让读者亲自尝试。

横向周边知识: 一项核心技术,必然不是独立存在,技术是一个体系,但是一篇文章能够详述的技术有限,必然是以一项技术为中心,其他技术作为辅助。核心技术需要详述,但是周边技术,也需要交代,参天大树拔地而起也少不得土壤作为依附。用简明的语言交代周边知识,并提供这些知识正确的研究方向。也是一个负责任的博文作者不可忽视的一步。

纵深专业技术: 做技术,最忌讳的就是浅尝辄止。稍微深入一点就退出去,一来不利于理解底层实现,长此以往永远只是一个技术小白,成不了大师;二来不利于长久记忆, 记忆力再强的人时间长了,技术细节必然会记忆模糊。但是如果深入内核,理解了原理,在技术的大方向上绝对不会偏差。作为要成为架构师的男人,即使记不了那么多细节,但是对于大方向的把握绝对不能错。所以,技术纵深很有必要。

正文大纲

  1. Demo地址

  2. 本文所涉技术盘点

  3. 关于Android权限的梗

  4. 初级/中级/高级android开发的权限请求写法

  5. AOP优雅权限框架详解
    gradle配置
    Java代码

  6. AOP思想以及常用AOP框架

  7. AspectJ AOP框架的深入原理研究


正文

1.Demo地址

Demo地址:https://github.com/18598925736/GracefulPermissionFramework/tree/dev_aspectJ

2. 本文所涉技术盘点

以下适合有一定Android开发年限的开发阅读。并且对以下技术点至少有个基本了解,才能理解本文demo代码

  • java 注解基础

    java代码中大量使用@符号作为注解标志,注解用途多种多样,但是基本都是做标记,用于源码期,编译期,或者运行期的特别处理。注解有自己的特定语法以及API。

  • Gradle插件基础

    Gradle是androidStudio中的项目构建框架,用于将android源码工程整合编译打包成apk。其中可以自定义gradle插件,也可以引用他人发布的gradle插件来给项目构建过程中加入自己想要的逻辑。

  • Java 反射基础

    java反射,某些不方便直接使用的类或者方法,可以通过反射的方式使用。反射通常用在框架设计,hook技术中。

  • Android 权限基础知识

    本文的重点是优雅地写出权限申请的代码,要读懂本文自然不能对权限一无所知。

  • AOP面向切面编程思想

    面向切面编程是代码解耦的重要手段之一。更多信息且看下文。


3. 关于Android权限的梗

权限问题,自android问世以来就是一个梗,最早做android的那一批人,当时可以随便获取用户信息,包括联系人,包括短信内容,包括通话记录,可以说Android被人诟病的安全问题,源自于此。代码层面,开发者只需要对照android官网权限说明,在manifest文件中声明所需的权限,即可在代码种访问所需的数据。各类权限十分繁多,超过上百种权限,适用于各种不同场景,记不下来,一般也不用记。

需要的时候到官网https://developer.android.com/reference/android/Manifest.permission.html查找即可.

下面总结几点Android发展历史种,权限体系的重大变革。

  • Android 1.0 - Android 5.0/5.1 App开发者只需要在清单文件中声明权限,安装的时候就会自动授予。

  • Android 6.0 起 谷歌把所有的权限分为2类,普通权限,即 依然是只需要在清单文件中声明即可。另一类,是危险权限,涉及到用户隐私的权限,除了在清单文件种声明之外,还需要在 App启动之时动态申请,并且,谷歌还提供了 权限组权限的概念差别,把某一些功能类似的权限放在一个组别,当你去申请其中一个权限的时候,其实也是在默认申请该组的其他权限。虽然这种做法可以让你少写一个权限,但是谷歌依然建议把所需的权限写完整,因为保不齐哪天谷歌就变更了权限组,到时候代码出兼容问题,没必要,而且把所需权限写完整也是编程好习惯。

    下图是所有的危险权限以及权限组。

4167938-dfdda724502f3f45.png
  • Android Q 起 又有重大变革。从笔者适配Android Q的过程种,发现
    1. STORAGE 权限组的两个权限,READ_EXTERNAL_STORAGE / WRITE_ETERNAL_STORAGE 无需动态申请(但是依然要在清单文件中声明), 因为Q系统启用了沙盒机制,app访问自己app所属目录无需任何权限,而如果是要访问app所属目录之外的地方,就需要申请 READ_EXTERNAL_STORAGE / WRITE_ETERNAL_STORAGE这两个权限。
    2. 如果设备在后台运行时,需要使用 位置信息,需要动态申请权限,Q 引入了 ACCESS_BACKGROUND_LOCATION 这个新权限,目的是限制后台进程获取悄悄的获取用户位置信息。如果此权限运行在Q以下(不含)的系统时,就会默认授予,但是Q及以上,则必须申请。
    3. 其他一些改动,详见官网,https://developer.android.google.cn/about/versions/10/privacy/changes?hl=zh-tw.
  • 另外说一个比较麻烦的问题,如果你运行我的Demo在多种机型上会发现效果可能会完全不同,举两个极端的例子:android6.0版本的mumu模拟器,很多权限会默认授予比如位置信息权限。另外,某些华为手机上,一些权限会默认拒绝,每一次你申请都是 用户已经永久拒绝。 这是因为 手机厂商或者模拟器厂家已经对 谷歌的原生安卓系统进行了深度定制,更改了权限的相关代码。所以,处理这种问题,要对多种机型进行适配处置。

4. 初级/中级/高级android开发的权限请求写法

权限的梗其实就那么一些,比较简单。上面这些梗,我们需要 特别关注的只有一个,那就是6.0以后的动态权限申请。它的处理方式为:

权限申请流程示意图.png

主要流程转化成代码展示出来:

AndroidManifest.xml

    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

Java 代码

    /**
     * 申请权限
     */
    protected void requestPermission(String[] permissions, int requestCode) {
        // 检查已经有了这些权限
        if (PermissionUtil.hasSelfPermissions(this, permissions)) {
            Log.e(TAG, "Activity,requestPermission: 所有权限都已经有了,无需申请");
        } else {
            // 开始请求权限
            ActivityCompat.requestPermissions(this, permissions, requestCode);
        }
    }
    /**
     * 处理回调
     *
     * @param requestCode
     * @param permissions
     * @param grantResults
     */
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (PermissionUtil.verifyPermissions(grantResults)) {//检查是否都赋予了权限
            granted(requestCode);
        } else {
            // 显示提示
            if (PermissionUtil.shouldShowRequestPermissionRationale(this, permissions)) {
                //shouldShowRequestPermissionRationale 这个方法就是检查,是不是权限被永久拒绝了。。。如果用就拒绝,这里就返回false,只是第一次拒绝,那就返回true
                // 取消权限
                denied(requestCode);
            } else {
                // 权限被拒绝
                deniedForever(requestCode);
            }
        }
    }

上面申请权限 ActivityCompat.requestPermissions处理回调onRequestPermissionsResult是开发者需要手动编码的地方。

同样是上面的逻辑,初级/中级/高级开发者的处理方式截然不同。

  • 初级

一个完整的商业项目,势必会涉及到非常多的ActivityFragment,以及普通Java类等等 ,诸多地方需要使用到特定的权限,如果我们ctrl+H全文搜索一下 onRequestPermissionsResult,发现如下场景:

image-20191112174526712.png

同样一份回调方法,居然在项目中出现了25次之多. 而且是权限申请这种和业务并不直接搭边的代码 还嵌入到业务代码内部。OK,这里就不多说了。

(PS: 其实这个就是我自己公司的代码,我不知道为什么会这样....也许是公司人员更替太多,后人都懒得改架构)

中级开发,作为有一定工作经验的程序员,知道如何优化代码,减少维护成本,那么他很可能会发现,需要用到权限申请的地方,基本上是以Activity和Fragment,只要解决了这里的代码冗余,他会这么写

public abstract class BaseActivity extends AppCompatActivity implements IPermissionCallback {
    protected static final String TAG = "BaseActivity";

    /**
     * 申请权限
     */
    protected void requestPermission(String[] permissions, int requestCode) {
        // 检查已经有了这些权限
        if (PermissionUtil.hasSelfPermissions(this, permissions)) {
            Log.e(TAG, "Activity,requestPermission: 所有权限都已经有了,无需申请");
        } else {
            // 开始请求权限
            ActivityCompat.requestPermissions(this, permissions, requestCode);
        }
    }

    /**
     * 请求回馈
     *
     * @param requestCode
     * @param permissions
     * @param grantResults
     */
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (PermissionUtil.verifyPermissions(grantResults)) {//检查是否都赋予了权限
            granted(requestCode);
        } else {
            // 显示提示
            if (PermissionUtil.shouldShowRequestPermissionRationale(this, permissions)) {
                //shouldShowRequestPermissionRationale 这个方法就是检查,是不是权限被永久拒绝了。。。如果用就拒绝,这里就返回false,只是第一次拒绝,那就返回true
                // 取消权限
                denied(requestCode);
            } else {
                // 权限被拒绝
                deniedForever(requestCode);
            }
        }
    }


}
public abstract class BaseFragment extends Fragment implements IPermissionCallback {

    protected static final String TAG = "BaseFragment";

    /**
     * 申请权限
     */
    protected void requestPermission(String[] permissions, int requestCode) {
        // 是否已经有了这些权限
        if (PermissionUtil.hasSelfPermissions(getActivity(), permissions)) {
            Log.e(TAG, "Activity,requestPermission: 所有权限都已经有了,无需申请");
        } else {
            // 开始请求权限
            ActivityCompat.requestPermissions(getActivity(), permissions, requestCode);
        }
    }

    /**
     * 请求回馈
     *
     * @param requestCode
     * @param permissions
     * @param grantResults
     */
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (PermissionUtil.verifyPermissions(grantResults)) {//检查是否都赋予了权限
            granted(requestCode);
        } else {
            // 显示提示
            if (PermissionUtil.shouldShowRequestPermissionRationale(getActivity(), permissions)) {
                //shouldShowRequestPermissionRationale 这个方法就是检查,是不是权限被永久拒绝了。。。如果用就拒绝,这里就返回false,只是第一次拒绝,那就返回true
                // 取消权限
                denied(requestCode);
            } else {
                // 权限被拒绝
                deniedForever(requestCode);
            }
        }
    }
}

然后使用同样一个IPermissionCallback接口来处理申请权限的可能结果(用户同意,用户拒绝,用户永久拒绝)

/**
 * 权限申请结果接口
 */
public interface IPermissionCallback {

    /**
     * 授予权限
     */
    void granted(int requestCode);

    /**
     * 这次拒绝,但是并没有勾选"以后不再提示"
     */
    void denied(int requestCode);

    /**
     * 勾选"以后不再提示",并且拒绝
     */
    void deniedForever(int requestCode);
}

但是, 我们需要权限申请的地方只有Activity和Fragment么?

,还可能有:

Service : 比如启动一个Service播放本地音乐,可能需要本地存储权限,如果此时才来申请,那么service应该如何申请权限? 经过实验,我发现Service没有办法去申请权限,因为 ActivityCompat.requestPermissions()方法的第一个参数是 Activity, 而在一个Service中,无法直接去获得一个Activity对象。

普通Java类: 一个普通的Java工具类,他的作用是从手机内部存储中读写文件,那么他需要本地存储权限, 它该如何申请?获取你可以想出一点偏方来解决这个问题,但是如果停留在中级开发的层次,永远无法给出一个优雅的解决方案。

  • 高级开发/架构师

    详细的解析下一章节再写,先来看代码效果:

    Activity:

Activity.png

Fragment:

fragment.png

普通Java类:

普通java类.png

Service:

service.png

观察以上三张图中代码的相同点:

都利用了3个自定义注解: @PermissionNeed ,@PermissionDenied,@PermissionCancel

  • @PermissionNeed 修饰修饰的是 用户授予权限之后的java方法

  • @PermissionDenied 注解修饰的是 用户拒绝权限之后的java方法

  • @PermissionCancel 注解修饰的是 用户永久拒绝之后的java方法

3个注解,在Activity,Fragment,Service 以及 普通Java类的使用方式完全相同,也可以说,高级开发/架构师的处理方式,把 Activity,Fragment,Service 以及 普通Java类 的差异化消除了,达成了 代码调用的通用性, 从根本上解决了 动态权限申请在 业务代码中的冗余问题。

使用这种做法,再也不用担心自己的业务代码会和 权限相关的代码发生交叉,让业务代码更加清晰。


5. AOP优雅权限框架详解

Demo地址:https://github.com/18598925736/GracefulPermissionFramework/tree/dev_aspectJ

gradle配置

  • 在project的build.gradle 添加aspectJ gradle插件
aspectJ依赖的gradle插件.png
  • permission model 的build.gradle 引入 aspect类库
permission model.png
  • app module 的build.gradle中启用aspectJ插件,并且引入permission module
appModule的build.gradle.png

Java代码

  • app module 是使用框架的地方

    上面我说到了,使用框架思想,消除了Activity,Fragment,Service,普通类 在申请权限时的差异性,可以全部以普通类的方式来申请权限并且处理回调。所以这里展示 Activity,Fragment,Service 的动态权限申请写法。

普通类

public class LocationUtil {

    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 requestCode
     */
    @PermissionDenied
    public void denied(int requestCode) {
        Log.e(TAG, "用户不给啊");
    }

    @PermissionDeniedForever
    public void deniedForever(int requestCode) {
        Log.e(TAG, "用户永久拒绝");
    }
}

Activity

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "PermissionAspectTag";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.btn_location).setOnClickListener(v -> getLocationPermission());
        findViewById(R.id.btn_contact).setOnClickListener(v -> getContactPermission());
    }

    @PermissionNeed(
            permissions = {Manifest.permission.READ_CONTACTS,Manifest.permission.WRITE_CONTACTS,Manifest.permission.GET_ACCOUNTS},
            requestCode = PermissionRequestCodeConst.REQUEST_CODE_CONTACT)
    private void getContactPermission() {
        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");
    }


    @PermissionDenied
    private void permissionDenied(int requestCode) {
        switch (requestCode) {
            case PermissionRequestCodeConst.REQUEST_CODE_CONTACT:
                Log.d(TAG, "联系人权限被拒绝");
                break;
            case PermissionRequestCodeConst.REQUEST_CODE_LOCATION:
                Log.d(TAG, "位置权限被拒绝");
                break;
            default:
                break;
        }
    }

    @PermissionDeniedForever
    private void permissionDeniedForever(int requestCode) {
        switch (requestCode) {
            case PermissionRequestCodeConst.REQUEST_CODE_CONTACT:
                Log.d(TAG, "权限联系人被永久拒绝");
                break;
            case PermissionRequestCodeConst.REQUEST_CODE_LOCATION:
                Log.d(TAG, "位置联系人被永久拒绝");
                break;
            default:
                break;
        }
    }
}

Fragment

public class MyFragment extends Fragment {

    @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 requestCode
     */
    @PermissionDenied
    public void denied(int requestCode) {
        Log.e(TAG, "用户不给啊");
    }

    @PermissionDeniedForever
    public void deniedForever(int requestCode) {
        Log.e(TAG, "用户永久拒绝");
    }
}

Service

public class MyService extends Service {
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @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 requestCode
     */
    @PermissionDenied
    public void denied(int requestCode) {
        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

    这里包含了框架的核心代码,现在一步一步讲解

    类结构图

    zpermission类结构图.png

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 PermissionAspect {

    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)")
    public void doPermission(final ProceedingJoinPoint joinPoint, PermissionNeed permissionNeed) {
        PermissionAspectActivity.startActivity(getContext(joinPoint), permissionNeed.permissions(), permissionNeed.requestCode(), new IPermissionCallback() {
            @Override
            public void granted(int requestCode) {
                // 如果授予,那么执行joinPoint原方法体
                try {
                    joinPoint.proceed();
                } catch (Throwable throwable) {
                    throwable.printStackTrace();
                }
            }

            @Override
            public void denied(int requestCode) {//这里的getThis也是要给梗
                PermissionUtil.invokeAnnotation(joinPoint.getThis(), PermissionDenied.class, requestCode);
            }

            @Override
            public void deniedForever(int requestCode) {
                PermissionUtil.invokeAnnotation(joinPoint.getThis(), PermissionDeniedForever.class, requestCode);
            }
        });
    }

    private Context getContext(final ProceedingJoinPoint joinPoint) {
        final Object obj = joinPoint.getThis();
        if (obj instanceof Context) {// 如果切入点是一个类?那么这个类的对象是不是context?
            return (Context) obj;
        } else {// 如果切入点不是Context的子类呢? //jointPoint.getThis,其实是得到切入点所在类的对象
            Object[] args = joinPoint.getArgs();
            if (args.length > 0) {//
                if (args[0] instanceof Context) {//看看第一个参数是不是context
                    return (Context) args[0];
                } else {
                    return ApplicationUtil.getApplication();//如果不是,那么就只好hook反射了
                }
            } else {
                return ApplicationUtil.getApplication();//如果不是,那么就只好hook反射了
            }

        }
    }
}

此段代码解读如下:

  • 使用@Aspect注解来修饰 , @Aspect是来自AspectJ框架的注解,被它修饰的类,在编译时会被认为是一个切面类

  • 使用 @Pointcut 注解来修饰方法requestPermission(),被它修饰的方法会被认为是一个切入点.

    所谓切入点,就是 面向切面编程时,我们无侵入式地插入新的逻辑,总要找到一个确切的位置,我们要知道程序执行到哪一行的时候,轮到我们出场了!切入点,一定是方法, 不能是随意哪一段代码!

    切入点可以是以下类型,不同的类型有不同的语法,我目前使用的是 method execution ,也就是 函数执行时。这意味着,当切入点的方法即将开始执行的时候,我们插入的逻辑将会被执行。与之类似的有一个 **method call **,这个不一样,这个是在切入点的方法 被调用时,也就是说,当侦测到该方法被外界调用的时候,而非方法自己执行。这两者有细微差别。至于其他的类型,暂且按下不详述。

    pointCut的类型.png

除了类型之外,这里还有一个重点,那就是 MethodSignature的概念,这个类似于jni里的方法签名,是为了标记一个或者一类方法,AspectJ框架通过这个方法签名,来确定JVM的所有class对象中,有哪些方法需要被插入 新的逻辑。

具体的签名的语法规则为:

签名语法.png

通配符.png

看不懂? 看不懂就对了,举个例子:

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(PermissionNeed permissionNeed) 去使用。

有点绕!一张图说清楚:

一毛一样.png

图中3个字符串必须一摸一样,不然编译就会报错,而且报错原因还不明确。

  • 使用 @Around 注解来修饰 方法 doPermission(),被它修饰的方法会被认为是一个 切入策略。

    Around注解的参数 为: "requestPermission(permissionNeed)", 也就是pointcut修饰的方法名(形参名)

    在我们已经定义好切入点 requestPermission(PermissionNeed permissionNeed)的前提下,如果程序已经执行到了切入点,那么我是选择怎么样的策略, 目前所选择的策略是 Around ,也就是,完全替代切入点的方法,但是依然保留了 执行原方法逻辑的可能性joinPoint.proceed();

    除了@Around策略之外,还有以下:

    切入策略.png

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. 提供一个静态方法

    public static void startActivity(Context context, String[] permissions, int requestCode, IPermissionCallback callback)

    用于启动自己 PermissionAspectActivity,

    接收的参数分别为:context,需要的权限数组,权限返回码,权限结果回调接口

  2. onCreate方法中,检查是否已经有想要申请的权限,如果有,直接调用mCallback.granted(requestCode); 并且结束自身,并且要注意隐藏Activity的切换动画。如果没有,那么,就去requestPermissions(permissions, requestCode);申请权限。

  3. 处理权限申请的回调,并且分情况调用mCallback的回调方法,然后结束自身

需要注意

PermissionAspectActivity必须在module的清单文件中注册

PermissionAspectActivity必须注册.png

并且 要定义它的theme使得Activity完全透明

style.png

Gif图效果演示:

demo.gif

6. AOP思想以及常用AOP框架

所谓AOP(ApsectOrientedProgramming) 面向切面编程。

此概念是基于OOP (ObjectOrientiedProgramming)面向对象编程。在OOP中,我们可以把不同的业务功能都分成一个一个的模块,然后每一个模块有自己的专一职责,从而优化编程过程,降低编程犯错几率。但是随着OOP类的数量的增加,我们会发现,在某一些业务类中,经常有一些相同的代码在重复编写,但是无可奈何,比如日志打印/动态权限申请/埋点数据上报/用户登录状态检查 /服务器端口连通性检查 等等。这些代码,我们虽然可以他们抽离出来整理到一个个专一的模块中,但是调用的时候,还是到处分散的,并且这些调用还入侵了本来不直接相关的业务代码,让我们阅读业务代码十分费劲。

而AOP的出现,就是基于OOP的这种缺陷而出现的优化方案。利用AOP,我们可以对业务逻辑的各个部分进行隔离,使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,提高开发效率,减少犯错概率。

画图表示:

OOP.jpg

如上图,OOP中,同样的一段过程,我们把登录检查,权限检查,埋点上报的调用代码写了3遍,然而都是雷同代码,只是参数不同而已。而,换成AOP的思想来编码。则是如下:

aop (1).jpg

所采取的方案为:

在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的祖宗, 现在依然有自己不可替代的作用。


7. AspectJ AOP框架的深入原理研究

...本来想写成一篇,但是发现篇幅太长,留个尾巴,下一篇,解析AspectJ是如何通过@注解的方式来插入逻辑的。


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343