您好,登錄后才能下訂單哦!
小編給大家分享一下Android如何采用AOP方式封裝6.0權限管理,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!
【一】背景
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 | 適配了小米 |
undefined
【二】效果圖
先上一下最終的效果圖:
效果圖有點模糊,可以下載源碼運行一下看效果
【三】整體思路
首先,先定義一個說法,彈出系統權限彈窗,用戶沒有給權限,并且選中不再提示,這種情況稱為權限被拒絕;如果用戶沒有給權限,但是沒有選中不再提示,這種情況稱為權限被取消。申請權限、權限被取消、權限被拒絕都是采用注解的形式,分別為@NeedPermission、@PermissionCanceled、@PermissionDenied,注解都是聲明在Method級別上的。在我們的Activity、Fragment及Service中聲明注解,然后在AOP中解析我們的注解,并把申請的權限傳遞給一個透明的Activity去處理,并把處理結果返回來。這就是整體思路,可能會遇到的問題:
1、 不同型號的手機兼容問題(申請權限、跳設置界面)
2、AOP解析注解以及傳值問題
上面說了很多,其實用一個圖來表示更清晰一些:
UML時序圖.png
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 == null) return; 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 == 0) return; 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 != 1) return; //獲取方法上的注解 PermissionDenied aInfo = method.getAnnotation(PermissionDenied.class); if (aInfo == null) return; //解析注解上對應的信息 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 == 0) return; 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 != 1) return; //獲取方法上的注解 PermissionCanceled aInfo = method.getAnnotation(PermissionCanceled.class); if (aInfo == null) return; //解析注解上對應的信息 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' ................其他................ }
說明: 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住, 比如:
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(); }
value中聲明了兩個權限,一個電話權限,一個相機權限
4.3 跳轉到設置類
當用戶拒絕權限并選中不再提示后,需要引導用戶去設置界面打開權限,因為國產手機各個設置界面不一樣,用通用的API可能會跳轉不到相應的APP設置界面,這里采用了策略模式(下圖所示)
跳轉到設置類.png
如需做兼容,只需要在庫里修改,調用方是不需要處理的,調用方如需跳轉到設置界面,只需像下面這樣調用就OK了:
【五】總結
回看一下我們的需求,基本上都實現了:
1、首先通過@NeedPermission、@PermissionCanceled、@PermissionDenied三個注解來分別定義權限申請、被取消、被拒絕三種情況,如果不想處理被取消的邏輯就不用使用@PermissionCanceled注解,其他權限申請的邏輯調用方不用關心,是完全解耦的;
2、同時支持在Activity、Fragment、Service中去申請權限;
3、最后關于申請權限、跳設置界面的兼容問題,因為身邊的手機有限,不能測試出所有兼容問題,需要后續優化。
以上是“Android如何采用AOP方式封裝6.0權限管理”這篇文章的所有內容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。