經過AOP的思想 打造萬能動態權限申請框架Demo徹底解析

AOP優雅權限框架詳解(以及更多面試題)

https://github.com/xiangjiana...java

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()); 
    }
    @permissionNeed(
           permissions = {Mainfest.permission.READ_CONTACTS,Mainfest.permission.WRITE_,Manifest.permission.GET_ACCOUNTS},
           requestcode = permissionsRequestcodeconst.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;

      }
  
 }
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類型參數, 返回值隨意.android

  • zpermission module

這裏包含了框架的核心代碼,如今一步一步講解c++

類結構圖

3個註解@PermissionDenied @PermissionDeniedForever @PermissionNeedgit

/**
 * 被此註解修飾的方法,會在方法執行以前去申請相應的權限,只有用戶授予權限,被修飾的方法體纔會執行
 */
  @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 {
  }

處理權限回調結果的接口 IPermissionCallbackgithub

/**
 * 權限申請結果接口
 */
  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)

可是若是咱們不計較 訪問權限,不計較返回值類型,也不計較 函數名,甚至不計較參數列表的話,就能夠寫成這個樣子 **(..)) . 表示任意方法app

除此以外,還有後半截 &&@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,須要的權限數組,權限返回碼,權限結果回調接口

  1. onCreate方法中,檢查是否已經有想要申請的權限,若是有,直接調用 mCallback.granted(requestCode); 而且結束自身,而且要注意隱藏Activity的切換動畫。若是沒有,那麼,就去requestPermissions(permissions,requestCode);申請權限。
  2. 處理權限申請的回調,而且分狀況調用 mCallback的回調方法,而後結束自身

須要注意:
PermissionAspectActivity必須在module的清單文件中註冊

而且 要定義它的 theme使得Activity徹底透明

Gif圖效果演示:

AOP思想以及經常使用AOP框架

所謂 AOPApsectOrientedProgramming) 面向切面編程。

此概念是基於OOPObjectOrientiedProgramming)面向對象編程。在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

相關文章
相關標籤/搜索