近期筆者開源了一個 Android 編譯時注解框架庫——Permissions4M,一款處理 android 6.0 運行時權限的庫。此庫基于鴻洋前輩的MPermissions 二次開發,目前 Android 中主流的幾款編譯時處理框架有大名鼎鼎的 Dagger2 和 Butterknife,希望在閱讀完筆者的這篇博客和筆者的框架后,能夠幫助各位讀者更深一步的幫助各位讀者了解 Android 編譯時注解處理技術,所以希望讀者在閱讀筆者的這篇博客前,請先對注解有些了解,對 Android 6.0 運行時權限有一定了解,更好地是對筆者的庫試一試,便于理解筆者后面所述內容。推薦閱讀以下內容:
為了便于各位讀者理解 Permissions4M,筆者特地畫下了以下這幅圖:
編譯前,你的程序中(為了更方便的理解,后面筆者就不使用程序兩個字,直接使用“Activity”來實例化程序這兩個字)除了自己寫的代碼之外,涉及到注解庫的內容可以被分成兩個部分,一個部分是注解,另一個部分應該是你所調用的 API,例如在 Butterknife 中,你可能會用到注解 @BindView,而使用到 API 應該是 ButterKnife.bind( this ) ;可能各位讀者在寫的時候不怎么會去細分模塊,但是如果是設計一個注解庫的話,應該是將這兩塊分開,一個是為了方面模塊化開發,另一個方面是后面所要開發的注解器的模塊庫要基于注解庫。
在編譯的這個過程中,我們的注解處理器就發揮作用了,它會對 Activity 掃描,然后進行信息提取與處理,后拼接出來一個代理類,同樣的,我們的注解處理器應該也是一個單獨區分于注解模塊和 API 模塊的一個新的模塊,所以來說一共需要三個模塊來進行開發,分別是 API 模塊,用來暴露給用戶進行使用;注解模塊,雖然也是暴露給用戶使用,但是應該區分于 API 模塊,各司其職;注解處理器模塊,對用戶進行屏蔽,模塊作用僅為在編譯器為使用了注解的 Activity 生成代理類。
一直未說到代理類和 API 模塊的作用,實際上 API 模塊就是對生成的代理類進行操作,以此來到達我們所想要的目的,例如ButterKnife.bind( this ) ; ,通俗點說實際上就是在相應的代理類中調用了 findViewById() 方法,那么該代理類是如何將findViewById() 和@BindView 一一對應起來的呢?不好意思筆者不知道,因為筆者沒看過 Butterknife 的源碼。但是筆者可以告訴你 Permissions4M 是如何將權限回調函數與 @PermissionsGranted、@PermissionsDenied、@PermissionsRationale 一一對應起來的。
在 Android Studio 中點擊菜單左上角 File -> New -> Module,選擇 Java module,為什么選擇 java module?前期提到,這個庫中僅涉及到所使用到的注解,所以 Java 類型的庫已經可以滿足我們的所有需求,并不需要使用 android library,否則涉及到多余的資源文件反而使得我們的庫大小會更大,permissions4m-annotation module 截圖如下:
前四個注解可以歸為一類,分別是自定義權限二次申請回調、權限拒絕時回調、權限通過時回調和權限二次申請回調。這里拿出PermissionsGranted 為各位讀者解答下:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface PermissionsGranted {
int[] value();
}
雖然前期筆者提到希望各位讀者具有一點注解基礎,但是筆者還是在這里啰嗦一下,我們需要使用 @Retention(RetentionPolicy.CLASS) 注解和@Target(ElementType.METHOD) 標記我們的注解接口,前者是將接口相關信息只保留到 .class 時期,后者是表明接口修飾的對象是方法。而接口中int[] value() 是什么意思呢?它的意思就是如果我們這樣使用 @PermissionsGranted 注解是不可以的 ——
@PermissionsGranted()
public void grant() {
}
而應該像如下方法使用:
@PermissionsGranted({1, 2})
public void grant(int code) {
}
或
@PermissionsGranted(1)
public void granted() {
}
所以說的簡單點就是需要限制接口必須要傳入一個整型數組才可以,那么為什么我們需要傳入一個整型數組呢?使用過 Permissions4M 的小伙伴就知道,@PermissionsGranted 注解是支持任意個權限申請回調的,那么如何區分是哪個權限申請回調的呢?就是 @PermissionsGranted 注解修飾的方法的參數是一個整型值,根據該參數來進行判斷,而該參數的可能值就是數組中的值,可能這樣說還是不能很清晰,如下:
@PermissionsGranted({1, 2})
public void grant(int code) {
switch (code) {
case 1:
break;
case 2:
break;
case 3:
// nerver
break;
default:
break;
}
}
如果各位讀者在此還是一頭霧水,不要緊,筆者在后期還會提到這塊內容。 permissions4m-annotation 具體源碼查看:jokermonn/permissions4m/permissions4m-annotation。各位讀者發現筆者雖然 BB 了這么多,但是 permissions4m-annotation module 的源碼卻如此簡單~
毫不避諱的說,這是三個模塊中難且難理解的模塊,但是不要慌,筆者會一一為大家解答,首先仍然是在 Android Studio 中點擊 File -> New -> Module,新建一個 java module,為什么是一個 java module?其一是因為如 permissions4m-annotation 中提到的一般,使用 java 庫已經可以滿足我們的設計,其二是因為 Android 中對 javax.annotation 類進行了刪減,所以 Android 庫對自定義處理器支持很不好,所以綜上兩點我們會選擇使用 java module。module 建好后,需要打開 module 下的 build.gradle 添加兩行依賴,行是compile project(':permissions4m-annotation') 表示 permissions4m-annotation 庫依賴,第二行是compile 'com..auto.service:auto-service:1.0-rc3',該庫可以更好地輔助我們完成自定義注解處理器的設計。permissions4m-processor module 截圖如下:
源碼見:jokermonn/permissions4m/permissions4m-processor
AnnotationProcessor 繼承自 AbstractProcessor,實際上它需要完成兩件事,一是獲取我們所需要的信息,二是命令 ProxyInfo 去生成相應的代理類的代碼,首先固定套路如下:
1.構造函數
private Elements mUtils;
private Filer mFiler;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mUtils = processingEnv.getElementUtils();
mFiler = processingEnv.getFiler();
}
mUtils 可以幫助我們獲取到使用注解的類的信息、類的包、方法信息等等,所以說 mUtils 是一個對于我們來說十分強大且在獲取信息這個過程中必不可少的工具類。mFiler 是生成 java 文件的類,也就是我們后期需要用來生成代理類的工具類。
2.覆寫方法
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> set = new HashSet<>(5);
set.add(PermissionsGranted.class.getCanonicalName());
set.add(PermissionsDenied.class.getCanonicalName());
set.add(PermissionsRationale.class.getCanonicalName());
set.add(PermissionsCustomRationale.class.getCanonicalName());
set.add(PermissionsRequestSync.class.getCanonicalName());
return set;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
getSupportedAnnotationTypes() 中將你所需要處理的注解的 getCanonicalName() 組成 Set 并返回即可,而 getSupportedSourceVersion() 中你只需要返回 SourceVersion.latestSupported(); 即可。
3.類頭注解
@AutoService(Processor.class)
public class AnnotationProcessor extends AbstractProcessor {
}
固定套路,使用 @AutoService(Processor.class) 注解當前類。
4.覆寫 process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) 方法
這是注解器的核心方法,就是通過該方法,我們提取到關于注解使用地方的信息,例如類名,類的包名,方法名,注解傳入的參數等等。具體內容會在后面提及到。
前期提到,ProxyInfo 類就是提供代理類的代碼的,那么我們首先要想到,寫一個代理類的話我們需要什么。例如我們在 MainActivity 中使用了我們的注解,那么我們生成的 MainActivity 代理類需要 MainActivity 的包名、注解當中傳入的數組參數、注解所修飾的方法的方法名等等信息。說到這里就差不多告一段落了,我們就開始正式的寫代碼的過程了。
前期提到,核心方法是 process(),而我們整個注解器中真正的邏輯業務代碼也僅需要在此方法內寫上就可以了,permissions4m-processor中的 process 方法源碼如下:
首先,方法在返回 false 的時候處理器將不會做任何事情,直接跳過當前次循環,類似于 continue;,而如果希望處理器處理的話,應當返回 true。方法分成兩個部分,個部分是提取代理類所需要的相關的信息并塞給 ProxyInfo,第二個部分就是使用 mFiler + ProxyInfo 生成我們所需要的代理類。是不是 so easy?好消息是第二部分的東西也是個固定套路,并不需要靈活多變,另一個好消息是部分的源碼實際上也不是很難。首先是創建一個 HashMap,鍵值對為 String-ProxyInfo,實際意義是使用到了注解的類的全路徑-ProxyInfo,例如 “com.joker.test.MainActivity”- ProxyInfo,”com.joker.test.SecondActivity”- ProxyInfo,前期提到,該方法會被多次循環調用,所以為了避免生成重復的代理類,避免生成類的類名已存在異常。,我們需要在開始的地方進行 map.clear() 操作。接下來就是相應的判斷了,如果不符合條件,我們應當直接返回 false,此處我們就只對isAnnotatedWithMethod(RoundEnvironment roundEnv, Class<? extends Annotation> clazz) 方法進行分析,簡化后源碼如下:
private boolean isAnnotatedWithMethod(RoundEnvironment roundEnv, Class<? extends Annotation> clazz) {
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(clazz);
for (Element element : elements) {
if (isValid(element)) {
return false;
}
ExecutableElement method = (ExecutableElement) element;
TypeElement typeElement = (TypeElement) method.getEnclosingElement();
String typeName = typeElement.getQualifiedName().toString();
ProxyInfo info = map.get(typeName);
if (info == null) {
info = new ProxyInfo(mUtils, typeElement);
map.put(typeName, info);
}
Annotation annotation = method.getAnnotation(clazz);
String methodName = method.getSimpleName().toString();
if (annotation instanceof PermissionsGranted) {
int[] value = ((PermissionsGranted) annotation).value();
if (value.length > 1) {
info.grantedMap.put(methodName, value);
} else {
info.singleGrantMap.put(value[0], methodName);
}
}
}
return true;
}
外圍的代碼筆者就不做解析了,直接從 ExecutableElement method = (ExecutableElement) element; 開始解析,經過前面一系列的判斷,我們已經能確保注解的使用方式符合我們的要求(必須注解的是方法、必須是 public 的且非 abstract 的),所以我們將 element 強制成 ExecutableElement 類型,代表它是一個方法,然后我們通過 getEnclosingElement(); 獲取并將其強轉成 TypeElement,該函數獲取到的就是當前的類類型,TypeElement 有個非常好用的方法叫做 getQualifiedName().toString(),就可以將 TypeElement 的名字打印出來,也就是類名。然后我們根據類名來獲取 ProxyInfo,如果我們發現 ProxyInfo 為空,那么我們就 put 一個新的 ProxyInfo 給 map 即可,組成一個新的 ProxyInfo 需要兩個參數,一個是 mUtils(理由很簡單,我們很有可能需要在 ProxyInfo 中使用它來獲取更多的信息),一個是 TypeElement(我們生成的代理類需要和它在同一包下,并且我們也需要這個類的類型,比如說我們的 TypeElement 可能是 Activity 類型的也可能是 Fragment 類型的,而對于不同的類型,我們需要調用的權限申請的 API 是不同的)。說到這里其實已經差不多了,后我們還需要的一樣東西就是注解中的參數,我們肯定是會需要用到它的,而獲取的方式也很簡單,上面的代碼已經很清晰了,筆者就不在此處做擴展了,需要提示的一點是,筆者對于每個注解提供了兩個 map,是因為當參數只有一個和當參數有多個的情況下,生成的函數有不同,此處在后面會有相應的擴展。
在編寫代理類之前,筆者想讓各位讀者從大局的角度上來考慮下,我們前期說到, API module 實際上是調用代理類的方法來完成的,那么肯定的說代理類必須有固定的方法供 API module 來調用,那么如何才能有固定的方法呢?答案就是讓代理類都去實現一個接口,這樣 API module 就很好調用了,當然這部分的知識就涉及到 permissions4m-lib 中了。但是一個接口還是很容易理解的,筆者就在這里放出接口的源碼了 ——
public interface PermissionsProxy<T> {
void rationale(T object, int code);
void denied(T object, int code);
void granted(T object, int code);
boolean customRationale(T object, int code);
void startSyncRequestPermissionsMethod(T object);
}
方法的功能顧名思義,個方法是二次申請時調用,第二個方法是權限被拒時調用,第三個方法是權限通過時調用,第四個方法是指是否是自定義二次申請對話框,如果是的話,將會轉交給 @PermissionsRationale 所修飾的方法,如果不是的話,那么就轉交給@PermissionsCustomRationale 所修飾的方法,而后一個方法就是進行多權限同步申請時所調用的 API。方法中傳入的個參數是一個泛型,實際上該泛型只能是 Activity 或者 Fragment 類型,因為在 Android 中,只有 Activity 或者 Fragment 才能實現權限申請。
關于思考的部分說到這里,下面就是在 ProxyInfo 中進行實際的編碼了,在這里再次回顧一下,我們創建一個代理類需要一些什么東西,我們可以打開一個類,從類的上面開始看起來:
mUtils.getPackageOf(typlElement).getQualifiedName().toString();即可獲取
typeElement.getQualifiedName().toString().substring(packageName.length + 1)
.replace('.', '$');可以獲取到字符串 “MainActivity”,然后添加上 “$$PermissionsProxy” 即可,當然,不要忘了該類需要實現 PerimissionsProxy 接口。
代碼如下:
StringBuilder builder = new StringBuilder();
builder.append("package ").append(packageName).append(";\n\n")
.append("import com.joker.api.*;\n\n")
.append("public class ").append(proxyName).append(" implements ").append
(PERMISSIONS_PROXY).append
("<").append(element.getSimpleName()).append("> {\n");
這里需要說明的一點是,并不需要怕多寫了一個分號少寫了一個花括號什么的,不論多些還是少寫了,編譯后如果不符合 java 語法都是會報錯的,所以大可不必擔心。
@Override 來幫助我們確保后面的方法名不會寫錯,然后就是添加上方法名和方法參數即可。接下來我們得想起來, ProxyInfo 中有幾個 map 是存放了方法名和 resquestCode 的信息的,如下:
所以我們需要對 map 進行遍歷,每次的 key 就是方法名,而 value 就是對應的請求碼。permissions4m 中除了傳統的權限申請之外,還有一項是多項權限同步申請,其實做過同步申請的開發人員應該知道,其實就是對上一個權限的授予或被拒時的函數進行監聽,并在此添加對下一個權限的申請,所以我們在此處應該還要對存放同步申請權限的 map 進行比對一下,如果 map 中有此次權限申請,那么下一次權限申請的代碼應該寫在此次權限申請的 granted/denied 方法中。源碼如下:
private void generateDeniedMethod(StringBuilder builder) {
checkBuilderNonNull(builder);
builder.append("@Override\n").append("public void denied(").append(element.getSimpleName())
.append(" object, int code) {\n")
.append("switch(code) {");
for (String methodName : deniedMap.keySet()) {
int[] ints = deniedMap.get(methodName);
for (int requestCode : ints) {
builder.append("case ").append(requestCode).append(":\n{");
builder.append("object.").append(methodName).append("(").append(requestCode).append(");\n");
// judge whether need write request permission method
addSyncRequestPermissionMethod(builder, requestCode);
builder.append("break;}\n");
if (singleDeniedMap.containsKey(requestCode)) {
singleDeniedMap.remove(requestCode);
}
}
}
for (Integer requestCode : singleDeniedMap.keySet()) {
builder.append("case ").append(requestCode).append(": {\n")
.append("object.").append(singleDeniedMap.get(requestCode)).append("();\nbreak;\n}");
}
builder.append("default:\nbreak;\n").append("}\n}\n\n");
}
還是筆者那句話,這樣看起來很晦澀,可以對照已經編譯好的 .class 文件進行一一對應,理解起來就十分迅速 ——
這是三個 module 中的 Android module,在 Android Studio 中選擇 File|New|Module,然后選擇 Android library 進行創建。為什么使用 Android library?因為 permissions4m-api 是面向開發人員的庫,并且會涉及到 Activity 和 Fragment 等類,所以必須得使用 Android library。jokermonn/permissions4m/permissions4m-api 結構如下:
關于 PermissionsProxy 接口前面已經做了闡述,所以這里介紹一下 Permissions4M 類,Permissions4M 就是暴露于開發人員的接口,對外暴露了如下幾個方法接口,首先是多個權限同步申請:
同步申請多個權限,源碼如下:
代碼大體是一樣的,首先針對當前應用版本進行判斷,如果是低于 Android 6.0 的話,那么將不做任何處理,否則的話將會調用 initProxy()函數,并將傳入的類作為參數傳給該函數:
源碼十分簡單,先獲取傳入類的 className,在拼接成代理類的名字,再使用反射來進行代理類的實例化。讓我們再看看syncRequestPermissions() 中的第三步,也就是 syncRequest() 方法 ——
一目了然,實際上就是調用了代理類的 startSyncRequestPermissionsMethod()。讓我們再理一理流程——確保當前手機版本大于6.0 -> 實例化代理類 -> 調用代理類的多個權限同步申請方法
實際上不僅僅是針對于 syncRequestPermissions() 方法,permissions4m 暴露給開發人員的普通權限申請方法 requestPermission() 也是這么一個套路 ——
關鍵點在于 request 方法——
代碼雖然看著有點多,但是如果讀者有做過 Android 6.0 動態權限適配的經驗的話,對于這段代碼應該再熟悉不過了,筆者就拿 Activity 來說,首先是調用 ContextCompat.checkSelfPermission((Activity) object, permission) != PackageManager.PERMISSION_GRANTED) 方法來查看該權限是否被授予,如果已經被授予,那么就調用代理類的 granted() 方法。如果沒有被授予,那么再通過((Activity) object).shouldShowRequestPermissionRationale(permission) 方法判斷應用是否應該展示相應的提示信息,如果不展示的話,那么應當直接調用 ActivityCompat.requestPermissions((Activity) object, new String[]{permission}, requestCode); 進行權限申請,否則的話就是應當展示信息。進入展示信息的模塊,我們要對開發人員的選擇進行判斷,開發人員是否自定義了二次權限申請的回調方式,如果沒有,那么就進入普通的二次回調方式,顯示回調 @PermissionsRationale 注解修飾的方法,再接著就是調用ActivityCompat.requestPermissions((Activity) object, new String[]{permission}, requestCode); 進行權限申請。
通俗點說,Activity 的權限申請流程了 Permissions4M 來處理,而具體調用什么方法,Permissions4M 就會調用相應的代理類方法來完成。
本站文章版權歸原作者及原出處所有 。內容為作者個人觀點, 并不代表本站贊同其觀點和對其真實性負責,本站只提供參考并不構成任何投資及應用建議。本站是一個個人學習交流的平臺,網站上部分文章為轉載,并不用于任何商業目的,我們已經盡可能的對作者和來源進行了通告,但是能力有限或疏忽,造成漏登,請及時聯系我們,我們將根據著作權人的要求,立即更正或者刪除有關內容。本站擁有對此聲明的最終解釋權。