前言
本文是IOC系列文章的第五篇,也是最后一篇,也是最重要的一篇。之所以說最重要,是因為掌握自定義注解解析器是所有Android架構(gòu)師必備的技能,沒有一個Android架構(gòu)師說自己不會自定義注解解析器的,另外掌握注解解析器更加有助于我們理解那些優(yōu)秀的開源框架,像Retrofit、EventBus和Dagger2等等。本文將詳細給大家?guī)黻P(guān)于自定義注解解析器的知識。
APT的工作流程
在上一篇文章Butterknife源碼全面解析的最后簡單介紹了一下APT技術(shù),這里我再給大家講講APT的工作流程。在代碼編譯階段(javac),會掃描所有AbstractProcessor的已注冊的子類,并且會調(diào)用其process()方法,在該方法中我們可以解析注解并生成java文件,然后在程序調(diào)用我們生成的代碼即可,其大致流程如下圖所示
對我們開發(fā)者來說,最核心的目的就是實現(xiàn)AbstractProcessor并生成相關(guān)代碼。
AbstractProcessor
實現(xiàn)自定義注解必須要掌握的就是AbstractProcessor,它是虛處理器,運行在單獨的JVM中,使用AbstractProcessor對象必須要有javax環(huán)境。
AbstractProcessor有四個重要的方法:
init(ProcessingEnvironment processingEnvironment):會被注解工具所調(diào)用,并傳入ProcessingEnvironment參數(shù),通過ProcessingEnvironment參數(shù)我們可以拿到Filer和Messager等工具類,F(xiàn)iler看名字就知道是文件,生成java代碼的時候使用,Messager是用來輸出日志信息的。
process(Set<? extends TypeElement> annotations, RoundEnvironment env) :AbstractProcessor中最重要的方法,相當于main()函數(shù),通過RoundEnvironment參數(shù)我們可以獲得所有被注解標注的程序元素(包、類、成員變量、構(gòu)造方法、成員方法...),在這里我們可以解析這些程序元素并生成java文件
getSupportedAnnotationTypes():需要解析的注解集合,這里返回的Set<String>,這里可以用@SupportedAnnotationTypes()來代替
getSupportedSourceVersion:獲得支持的java版本,一般直接返回SourceVersion.latestSupported(),同樣可以用@SupportedSourceVersion注解來代替
注冊AbstractProcessor
寫好了AbstractProcessor我們必須要注冊才會生效,如何注冊呢?我們必須把注解解析器打包到jar包,需要在META-INF/services路徑下生成一個javax.annotation.processing.Processor文件,并在該文件中生成你所聲明的Processor對象,是不是聽起來很麻煩,其實做起來也很麻煩。谷歌爸爸為了方便廣大開發(fā)者,特意開發(fā)了auto-service庫,我們只需要在項目中引入auto-service庫,并且在我們聲明的
Processor類上面使用@AutoService(Processor.class)即可
@AutoService(Processor.class)
public class BindingProcessor extends AbstractProcessor {
private Filer mFiler;//文件類
private Messager mMessager;//打印錯誤信息
private static final Map<TypeElement, List<ViewInfo>> bindViews = new HashMap<>();//綁定的view集合
注:這里需要注意的使用gradle版本大于5.0以上
#Sun Apr 26 15:32:47 PDT 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
在gradle5.0以上會自動忽略auto-service,所以在引入的時候我們需要
implementation 'com.google.auto.service:auto-service:1.0-rc6'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'(必須要加上這行代碼,否則無法注冊成功)
接著,我們可以編譯一下項目,然后打開build下面的classes
如圖所示,即代表注冊成功了
JavaPoet
當我們解析好了注解的程序元素以后就需要生成java文件,一行行手打代碼就會很麻煩,java很貼心地推出了JavaPoet開源庫,我們只需要引入這個類庫即可。它可以很幫助開發(fā)者很輕松地生成需要的代碼,直接放上幾行代碼給大家看一下效果
引入類庫
implementation 'com.squareup:javapoet:1.12.1'
FieldSpec fieldSpec = FieldSpec.builder(String.class,"name",Modifier.PRIVATE).build();//成員變量
MethodSpec methodSpec = MethodSpec.constructorBuilder()//生成的方法對象
.addModifiers(Modifier.PUBLIC)//方法的修飾符
.addParameter(className, paramName)//方法中的參數(shù),第一個是參數(shù)類型,第二個是參數(shù)名
.addCode(builder.build())//方法體重的代碼
.build();
TypeSpec typeSpec = TypeSpec.classBuilder(typeElement.getSimpleName().toString() + TestBindView.SUFFIX)//類對象,參數(shù):類名
.addMethod(methodSpec)//添加方法
.addField(fieldSpec)//添加成員變量
.build();
JavaFile javaFile = JavaFile.builder(packageName, typeSpec).build();//javaFile對象,最終用來寫入的對象,參數(shù)1:包名;參數(shù)2:TypeSpec
try {
javaFile.writeTo(mFiler);
} catch (IOException e) {
e.printStackTrace();
}
具體用法這里不多說了,JavaPoet使用起來很簡單,最好就是先去GitHub上看一下官方文檔,接著寫一個Hello JavaPoet就能掌握了,送上github地址 JavaPoet
使用Android Studio搭建IOC項目
了解了AbstractProcessor和JavaPoet我們開始搭建IOC項目中,剛才介紹AbstractProcessor時提到了,其必須要有javax環(huán)境,而我們默認的Android Module中沒有javax,所以必須要建立一個java library要來處理annotation-processor(注解解析器),另外還需要再創(chuàng)建一個java library來處理annotation(注解),這里之所以要把annotation-processor和annotation分開,是因為annotation-processor我們只有在編譯期才用到,所以不必要把annotation-processor的相關(guān)代碼打入到APK中,這里我們通過annotationProcessor方式依賴即可。關(guān)于Android module和java library的依賴關(guān)系是Android module依賴annotation-processor和annotation,其中annotation-processor又依賴annotation,因為要通過annotation-processor解析annotation中的注解,一圖勝千文
注:Demo的github地址在文章的最后
通過自定義注解實現(xiàn)Butterknife的BindView功能
在上一篇文章Butterknife源碼全面解析中給大家介紹了Butterknife的源碼和實現(xiàn)思路,如圖
這里我們仿照Butterknife的思路,首先在annotation中聲明一個@TestBindView注解
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface TestBindView {
int value();
String SUFFIX = "_TestBinding";
}
然后我們在自定義的BindingProcessor中的init()方法中初始化,F(xiàn)iler和Messager對象
@AutoService(Processor.class)
public class BindingProcessor extends AbstractProcessor {
private Filer mFiler;//文件類
private Messager mMessager;//打印錯誤信息
private static final Map<TypeElement, List<ViewInfo>> bindViews = new HashMap<>();//綁定的view集合
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
mFiler = processingEnvironment.getFiler();//初始化文件對象
mMessager = processingEnvironment.getMessager();//初始化信息對象
}
注意:這里Messager對象需要說一下,之所以出現(xiàn)錯誤要使用Messager對象打印出來,而不采取我們傳統(tǒng)的try catch方法是因為往往注解解析器出錯會引發(fā)一大堆的異常,這個時候如果用try catch會導致我們的異常信息特別多,定位問題很麻煩,而如果使用Messager就會讓錯誤信息簡單明了。
下面就來了解析注解并且生成java文件的核心代碼了,這里我想講一下我的思路:
- 通過process()方法里面的RoundEnvironment對象拿到所有被我們目標注解(TestBindView)注解的程序元素,然后遍歷
2.對程序元素進行分類,用一個Map容器,其中key是TypeElement(類元素,這里就代表我們的Activity),value是一個ViewInfo集合,ViewInfo包含viewId和viewName
3.拿到第二步的Map容器,開始利用JavaPoet生成代碼,我們這里的邏輯很簡單就是聲明一個后綴,通過Activity的名字拼接后綴作生成的文件名,然后在構(gòu)造方法里面調(diào)用findViewById()方法
下面直接上代碼
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(TestBindView.class);//獲取TestBindView注解的所有元素
for (Element element : elements) {//遍歷元素
VariableElement variableElement = (VariableElement) element;//因為注解的作用域是成員變量,所以這里可以直接強轉(zhuǎn)成 VariableElement
Set<Modifier> modifiers = variableElement.getModifiers();//權(quán)限修飾符
if (modifiers.contains(Modifier.PRIVATE) || modifiers.contains(Modifier.PROTECTED)) {//類型檢查
mMessager.printMessage(Diagnostic.Kind.ERROR, "成員變量的類型不能是PRIVATE或者PROTECTED");
return false;
}
TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();//獲得外部元素對象
sortToAct(typeElement, variableElement);//以類元素進行分類
}
writeToFile();
return false;
}
第二步驟分類邏輯
/**
* 把view信息跟activity關(guān)聯(lián)在一起
*/
private void sortToAct(TypeElement typeElement, VariableElement variableElement) {
List<ViewInfo> viewInfos;
if (bindViews.get(typeElement) != null) {//判斷之前是否存儲過這個typeElement的ViewInfo集合
viewInfos = bindViews.get(typeElement);
} else {
viewInfos = new ArrayList<>();
}
TestBindView annotation = variableElement.getAnnotation(TestBindView.class);//拿到注解
int viewId = annotation.value();//獲取viewId
String viewName = variableElement.getSimpleName().toString();//獲取viewName
ViewInfo viewInfo = new ViewInfo(viewId, viewName);//生成viewinfo對象
viewInfos.add(viewInfo);//放入集合
bindViews.put(typeElement, viewInfos);//存入map中
}
第三步驟生成代碼
/**
* 生成文件
*/
private void writeToFile() {
Set<TypeElement> typeElements = bindViews.keySet();
String paramName = "target";
for (TypeElement typeElement : typeElements) {
ClassName className = ClassName.get(typeElement);//獲取參數(shù)類型
PackageElement packageElement = (PackageElement) typeElement.getEnclosingElement();//獲得外部對象
String packageName = packageElement.getQualifiedName().toString();//獲得包名
List<ViewInfo> viewInfos = bindViews.get(typeElement);
CodeBlock.Builder builder = CodeBlock.builder();//代碼塊對象
for (ViewInfo viewInfo : viewInfos) {
//生成代碼
builder.add(paramName + "." + viewInfo.getViewName() + " = " + paramName + ".findViewById(" + viewInfo.getViewId() + ");\n");
}
FieldSpec fieldSpec = FieldSpec.builder(String.class,"name",Modifier.PRIVATE).build();//成員變量
MethodSpec methodSpec = MethodSpec.constructorBuilder()//生成的方法對象
.addModifiers(Modifier.PUBLIC)//方法的修飾符
.addParameter(className, paramName)//方法中的參數(shù),第一個是參數(shù)類型,第二個是參數(shù)名
.addCode(builder.build())//方法體重的代碼
.build();
TypeSpec typeSpec = TypeSpec.classBuilder(typeElement.getSimpleName().toString() + TestBindView.SUFFIX)//類對象,參數(shù):類名
.addMethod(methodSpec)//添加方法
.addField(fieldSpec)//添加成員變量
.build();
JavaFile javaFile = JavaFile.builder(packageName, typeSpec).build();//javaFile對象,最終用來寫入的對象,參數(shù)1:包名;參數(shù)2:TypeSpec
try {
javaFile.writeTo(mFiler);//寫入文件
} catch (IOException e) {
e.printStackTrace();
}
}
注意:以上代碼都寫入了詳細的注釋,這里有有兩點要說一下
第一:獲得被標注的元素注解以后我們要判斷一下它的修飾符類型,不能是private或者protected,因為我們要通過Activity對象訪問它的控件對象,這一點跟Butterknife是一致的
第二:就是Element.getEnclosingElement()這個方法很重要,是獲取元素的封閉元素。啥叫封閉元素呢?舉個例子,就是如果你是一個VariableElement(成員變量元素),把你封閉起來的就是TypeElement(類元素);如果你是一個TypeElement的,把你封閉起來的就是PackageElement(包元素);如果你是一個PackageElement,那么返回的就是null了,因為沒有東西把包封閉起來。我們注解的作用域是成員變量,所以我們直接拿到VariableElement,然后再通過VariableElement就可以拿到TypeElement和PackageElement,這對我們后面來生成代碼非常重要。
BindingProcessor里面的代碼寫完以后,我們在App module下寫一個工具類用來加載生成的java文件同時調(diào)用其構(gòu)造方法
public class ViewBindUtil {
/**
* 綁定Activity
* */
public static void bind(Activity activity) {
if (activity == null) {
return;
}
String activityName = activity.getClass().getName();//獲取類的全限定名
ClassLoader classLoader = activity.getClass().getClassLoader();//獲得類加載器
try {
Class<?> loadClass = classLoader.loadClass(activityName + TestBindView.SUFFIX);//加載類
Constructor<?> constructor = loadClass.getConstructor(activity.getClass());
constructor.newInstance(activity);//調(diào)用其構(gòu)造方法
} catch (Exception e) {
e.printStackTrace();
}
}
}
下面我們用一下咱們寫的注解解析器
public class MainActivity extends AppCompatActivity {
@TestBindView(R.id.button)
Button button;
@TestBindView(R.id.textView)
TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ViewBindUtil.bind(this);
button.setText("按鈕");
textView.setText("文本");
}
}
編譯一下項目,先看看生成的代碼,有木有問題
最后運行一下,發(fā)現(xiàn)一切OK,這里就不貼圖,這樣一個簡單的TestBindView注解解析器就完成了,本文的重點是不是這個例子,而且教大家如何完成一個注解解析器,以后再有模板類的代碼咱們都可以考慮使用這種方法簡化,同時對一些優(yōu)秀的開源如何用注解實現(xiàn)的代碼也會更加清楚。
如何調(diào)試Processor中的代碼
考慮到大家剛開始使用注解解析器難免會存在一些問題,所以如何調(diào)試Processor中的代碼還是很有必要講一講。我們運行時的代碼都會調(diào)用,點一下綠色的??即可,而調(diào)試Processor的代碼略微麻煩一點點。
完成前面四步,接下來
在Processor代碼里打好斷點
最后一步,切換成app,點擊運行
這里還有一個很重要的點需要強調(diào)一下,除了第一次運行外,其余每次運行都必須要在app module下改點代碼(哪怕是加個空格),否則,調(diào)試不會生效,切記!!!
總結(jié)
本文是系列文章IOC(依賴控制翻轉(zhuǎn))的最后一篇,終于打完收工了,如開篇所說掌握自定義注解解析器非常重要,未來大家要給公司搭建一些項目架構(gòu),難免會用到這項技術(shù)。本文首先介紹了AbstractProcessor和JavaPoet知識,然后交了如果使用AS搭建一個ioc項目,最后用了一個仿Butterknife的例子手擼一個自定義注解解析器的demo。
最后的最后,放上Demo的github地址:apt_demo,如果您覺得本文還不錯,記得給個贊,謝謝~