主目錄見:Android高級進(jìn)階知識(這是總目錄索引)
?我們在開發(fā)的時候為了提高效率往往會選擇一個基于注解的框架,但是有時使用反射通常被認(rèn)為是性能的收割機(jī),所以我們會青睞編譯期注解的使用讨跟,其實早在前面我們分析了[EventBus3.0源碼解析]中我們就有看到,還有我們接下來要講的ButterKnife也會用到,當(dāng)然我今天要用來講的例子[LRouter]這個項目也會使用這個。
一.目標(biāo)
?現(xiàn)在編譯期注解這么火熱趣苏,我們有理由要去學(xué)習(xí)一下,我們今天這篇文章是為了掃盲一下梯轻,使得我們下面分析這類型的框架更加得心應(yīng)手食磕。所以今天目標(biāo)是:
1.自己能編寫一個編譯期注解項目;
2.在實際開發(fā)中能使用到這項技術(shù)喳挑;
3.能學(xué)習(xí)此類型的框架的源碼,然后收為己用彬伦。
二.例子編寫
在編寫這個框架之前,我們需要一些準(zhǔn)備伊诵,因為此類框架需要的模塊有點多:
因為這是個完整的項目单绑,我們只是挑出其中的編譯期注解部分來講:
- lrouter-annotation:用于放注解部分,是個java的模塊
- lrouter-compiler:用于編寫注解處理器曹宴,是個java模塊
- lrouter-api:用于提供用戶使用的api的搂橙,是個android模塊
- app:這個是使用的實例,注解會在這里使用笛坦,也是android模塊
當(dāng)然了区转,目錄不一定就是要分這么多個,大家可以根據(jù)自己的需要版扩,有時候還可以進(jìn)行合并废离。當(dāng)然這些模塊之間是有模塊依賴的:
lrouter-compiler依賴lrouter-annotation模塊
在使用這個注解和api的時候,我們也要添加一些依賴:
lrouter-api依賴lrouter-annotation礁芦,app依賴lrouter-api和lrouter-annotation
當(dāng)然這里的lrouter-api在這里我們使用到的也不多蜻韭,因為這是項目會使用到的api,具體使用可以查看github上面的說明。
1.注解模塊lrouter-annotation實現(xiàn)
注解模塊就是單純地放一些注解湘捎,沒有其他的東西:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Action {
String name();
String provider();
}
我們這里舉其中一個注解來看看诀豁,因為我們是編譯期的注解所以我們設(shè)置保留策略為CLASS即編譯期注解(當(dāng)然如果用反射的話Runtime或者還有一個source用的不多),并且說明我們注解是使用在類上面(還可以說明在Field或者M(jìn)ethod等待上面)窥妇,然后這里因為需要一個name和一個provider其中的值為String類型舷胜。
你如果需要幾個注解就跟這個一樣,只要設(shè)置上響應(yīng)的Target和Retention即可活翩。
2.注解處理器lrouter-compiler的實現(xiàn)
這塊內(nèi)容應(yīng)該是編譯期注解的核心了吧烹骨,不過放心,也不會太難材泄,步驟是很固定的沮焕,在這里我們會使用一個auto-service庫以及后面用來生成代碼的javapoet:
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
compile 'com.google.auto.service:auto-service:1.0-rc2'
compile 'com.squareup:javapoet:1.7.0'
compile project(':lrouter-annotation')
}
auto-service可以幫我們?nèi)ド蒑ETA-INF信息:
接著我們就可以編寫注解處理器的核心代碼了。
2.1.ProviderActionProcessor實現(xiàn)
要實現(xiàn)注解處理器我們要實現(xiàn)一個類繼承AbstractProcessor:
@AutoService(Processor.class)
public class ProviderActionProcessor extends AbstractProcessor{
}
注解處理器里面包含幾個重要的方法:
init()
初始化拉宗,得到Elements峦树、Types、Filer等工具類
getSupportedAnnotationTypes()
描述注解處理器需要處理的注解
getSupportedSourceVersion()
處理器使用的java版本
process()
掃描分析注解旦事,生成代碼
首先我們會復(fù)寫init()方法魁巩,該方法里面會有ProcessingEnvironment參數(shù),我們可以根據(jù)這個來初始化一些輔助類:
private Filer mFileUtils;//跟文件相關(guān)的類姐浮,用于生成java源代碼的
private Elements mElementUtils;//元素相關(guān)的類谷遂,可以理解為獲取代碼中的信息
private Messager mMessager;//是用來打印日志的跟Logger類似
我們重點要說一下Elements,他有幾個子類卖鲤,我們來說明一下:
public class ClassA { // TypeElement
private int var_0; // VariableElement
public ClassA() {} // ExecuteableElement
public void setA( // ExecuteableElement
int newA // TypeElement
) {
}
}
我們看到類為TypeElement肾扰,變量為VariableElement,方法為ExecuteableElement蛋逾。到這里我們對Element有個簡單的認(rèn)知集晚,然后我們繼續(xù)重寫getSupportedAnnotationTypes()方法:
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotationTypes = new LinkedHashSet<>();
annotationTypes.add(Action.class.getCanonicalName());
annotationTypes.add(Provider.class.getCanonicalName());
annotationTypes.add(Service.class.getCanonicalName());
annotationTypes.add(Application.class.getCanonicalName());
annotationTypes.add(IntentInterceptor.class.getCanonicalName());
annotationTypes.add(Interceptor.class.getCanonicalName());
return annotationTypes;
}
這里我們把我們支持的注解都添加進(jìn)來,然后我們復(fù)寫另外一個方法getSupportedSourceVersion():
@Override
public SourceVersion getSupportedSourceVersion(){
return SourceVersion.latestSupported();
}
這個里面我們直接返回我們處理器支持的最新的java版本区匣。接著我們就來實現(xiàn)比較復(fù)雜部分的process方法甩恼。
2.2 process()實現(xiàn)
process方法比較復(fù)雜,因為我們主要的代碼邏輯都在這里面沉颂,一般這個方法的步驟有兩個:
?1.收集信息
?2.生成源代碼
什么叫收集信息呢?就是根據(jù)你的注解聲明悦污,拿到對應(yīng)的Element铸屉,然后獲取到我們所需要的信息,這個信息肯定是為了后面生成JavaFileObject所準(zhǔn)備的切端。
在這個例子中彻坛,我們會針對一個注解生成一個類,例如我們Action在app中使用的時候會生成一個MainAction$$Inject類:
然后我們來看怎樣一步一步生成這個類的。
1)收集信息
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
mProviderMap.clear();
mActionMap.clear();
mServiceMap.clear();
mApplicaitonMap.clear();
mIntentInterceptMap.clear();
mInterceptMap.clear();
if (!annotations.isEmpty()) {
Set<? extends Element> elesWithAction = roundEnv.getElementsAnnotatedWith(Action.class);
Set<? extends Element> elesWithProvider = roundEnv.getElementsAnnotatedWith(Provider.class);
Set<? extends Element> elesWithService = roundEnv.getElementsAnnotatedWith(Service.class);
Set<? extends Element> elesWithApplication = roundEnv.getElementsAnnotatedWith(Application.class);
Set<? extends Element> elesWithIntentInterceptor = roundEnv.getElementsAnnotatedWith(IntentInterceptor.class);
Set<? extends Element> elesWithInterceptor = roundEnv.getElementsAnnotatedWith(Interceptor.class);
...........
return true;
}
return false;
}
我們看到這里會獲取到所有的使用Action等注解的元素集合昌屉,我們看到這里會先調(diào)用clear()方法清除掉map中的數(shù)據(jù)钙蒙,因為process()方法有可能會調(diào)用多次,為了避免生成重復(fù)的源代碼间驮,我們這里做一下清理工作躬厌。獲取到所有的注解的元素集合了我們就可以生成源代碼了。
2)生成代碼
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//省略收集信息代碼
................
try {
generateProviderHelper(elesWithProvider);
generateActionHelper(elesWithAction);
generateServiceHelper(elesWithService);
generateApplicationHelper(elesWithApplication);
generateIntentInterceptorHelper(elesWithIntentInterceptor);
generateInterceptorHelper(elesWithInterceptor);
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
return false;
}
我們看到代碼會分別生成對應(yīng)的注解的源代碼竞帽,我們這里挑一個Action的生成代碼方法來看:
private void generateActionHelper(Set<? extends Element> elesWithAction) throws Exception{
//遍歷添加了Action注解的元素集合
for (Element element : elesWithAction){
//因為我們action是放在類上面的注解扛施,所以我們這里會進(jìn)行檢查一下,而且這里類不能為private的
checkAnnotationValid(element, Action.class);
TypeElement classElement = (TypeElement) element;
//full class name
String actionClassName = classElement.getQualifiedName().toString();
ProxyInfo proxyInfo = mActionMap.get(actionClassName);
if (null == proxyInfo){
//構(gòu)造proxyInfo對象屹篓,里面主要用于存放注解標(biāo)注的類信息與生成類信息
proxyInfo = new ProxyInfo(mElementUtils, classElement);
mActionMap.put(actionClassName, proxyInfo);
}
//同時將action注解里面的name和provider取出設(shè)置給proxyinfo
Action actionAnnotation = classElement.getAnnotation(Action.class);
proxyInfo.setName(actionAnnotation.name());
proxyInfo.setProvider(actionAnnotation.provider());
}
//有了完整的信息之后就遍歷出來生成源代碼
for (String key : mActionMap.keySet()) {
ProxyInfo proxyInfo = mActionMap.get(key);
//這個是用javapoet生成的疙渣,這個是方法的生成
MethodSpec.Builder initBuilder = MethodSpec.methodBuilder(METHOD_NAME)
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.returns(TypeName.VOID);
//這個是方法里面的代碼生成
initBuilder.addStatement("$T $N = $T.getInstance($T.getInstance()).findProvider($S)",
LRProviderClass,LRProviderClass.simpleName().toLowerCase(),LRouterClass,LRouterApplicationClass,proxyInfo.getProvider());
initBuilder.addCode("if(null != $N){\n",LRProviderClass.simpleName().toLowerCase());
initBuilder.addCode("$N.registerAction($S,new $T());\n}\n",
LRProviderClass.simpleName().toLowerCase(),proxyInfo.getName(),ClassName.get(proxyInfo.typeElement));
//這個是類的生成
TypeSpec actionInject = TypeSpec.classBuilder(proxyInfo.proxyClassName)
.addModifiers(Modifier.PUBLIC)
.addSuperinterface(InjectorClass)
.addMethod(initBuilder.build())
.build();
JavaFile javaFile = JavaFile.builder(PACKAGE_NAME,actionInject).build();
javaFile.writeTo(mFileUtils);
}
}
我們看到代碼比較長,其實邏輯不怎么難堆巧,我們都在關(guān)鍵地方有注釋妄荔,代碼其實就是獲取到使用了注解的類的信息,然后再用javapoet來生成代碼谍肤,關(guān)于[javapoet]我們這里不做詳細(xì)的說明啦租,有興趣可以去這個github上面看看怎么使用。
3.注解在例子中的使用
我們前面編寫了注解和注解生成器谣沸,這樣我們就可以來使用這個注解來生成我們的源代碼了刷钢,使用注解很簡單,跟我們平常是一樣的乳附,我們來看下:
@Action(name = "main",provider = "main")
public class MainAction extends LRAction {//動作的執(zhí)行
}
我們看到注解的使用非常簡單内地,因為這個注解是用在類上面的,所以我們用法如上面所示赋除,當(dāng)然在編寫這個之前我們還要配置一下我們的gradle文件阱缓。首先在工程目錄下的gradle文件下添加:
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
然后在當(dāng)前模塊下的gradle文件添加:
annotationProcessor project(':lrouter-compiler')
這樣的話,我們只要編譯一下工程举农,就會在當(dāng)前模塊的build——generated——source——apt——debug下生成我們的源代碼了荆针。
4.生成的源碼的使用
生成完源碼了,我們這里在lrouter-api中進(jìn)行使用颁糟,我們這里會用到一點反射航背,跟大家理解的可能不大一樣,不是說我們用到編譯期注解就一點反射不會使用棱貌,這是不正確的玖媚,有可能會我們還是需要用到一點反射,有時我們會用到緩存來減少性能的消耗婚脱。我們現(xiàn)在看lrouter-api下的PackageScanner類:
public class PackageScanner {
/**
* 掃描
* */
public static List<InjectorPriorityWrapper> scan(Context ctx){
List<InjectorPriorityWrapper> clazzs = new ArrayList<>();
try{
PathClassLoader classLoader = (PathClassLoader) Thread
.currentThread().getContextClassLoader();
DexFile dex = new DexFile(ctx.getPackageResourcePath());
Enumeration<String> entries = dex.entries();
while (entries.hasMoreElements()) {
String entryName = entries.nextElement();
if (entryName.contains("com.lenovohit")){//過濾掉系統(tǒng)的類
Class<?> entryClass = Class.forName(entryName, false,classLoader);
if (entryName.contains("Provider$$Inject")){
clazzs.add(new InjectorPriorityWrapper(InjectorPriorityWrapper.PROVIDER_PRIORITY,entryClass));
}else if (entryName.contains("Action$$Inject")){
clazzs.add(new InjectorPriorityWrapper(InjectorPriorityWrapper.ACTION_PRIORITY,entryClass));
}else if(entryName.contains("$$Inject")){
((Injector)entryClass.newInstance()).inject();
}
}
}
//進(jìn)行實例化
if (null != clazzs && clazzs.size() > 0){
Collections.sort(clazzs);
for (int i = 0; i < clazzs.size(); i ++){
InjectorPriorityWrapper clazz = clazzs.get(i);
((Injector)clazz.mClass.newInstance()).inject();
}
}
}catch (Exception e){
e.printStackTrace();
}
return clazzs;
}
}
這個類是得到dex文件下的信息今魔,接著根據(jù)類名稱反射生成這個類(這個類就是我們生成的源碼)勺像,然后進(jìn)行實例化調(diào)用。到這里我們編譯期注解的例子已經(jīng)講解完畢了错森,其實步驟非常固定吟宦,只要操作一遍就熟悉了。如果文章寫得有不懂的你可以下載源碼下來看看涩维,希望大家能在以后的項目中用到這個技術(shù)殃姓,還是非常方便。
總結(jié):本文通過一個實際的例子來說明這項技術(shù)怎么使用激挪,主要步驟包括:項目結(jié)構(gòu)劃分,注解模塊實現(xiàn)辰狡,注解處理器模塊實現(xiàn),注解生成源碼垄分,生成的源碼的使用等宛篇,希望大家能在學(xué)習(xí)本文完有個大的提高,因為你有能力去閱讀這類型框架的源碼了薄湿,而且能自己實現(xiàn)一套復(fù)雜的框架叫倍,希望本篇是你的第一步引導(dǎo)。