如需轉(zhuǎn)載請評論或簡信蹈丸,并注明出處,未經(jīng)允許不得轉(zhuǎn)載
目錄
前言
目前Android社區(qū)涌現(xiàn)出越來越多的IOC框架,ButterKnife
逻杖、Dagger2
奋岁、EventBus3
,這些框架往往能有效幫助我們簡化代碼荸百,模塊解耦闻伶,相信很多人也或多或少的用過其中一些框架。但是够话,有沒有人想過這些框架的內(nèi)部原理都是怎么樣的呢蓝翰?本文就從ButterKnife
入手,手把手教你實現(xiàn)一個仿ButterKnife
的IOC框架
知識準備
Annotation
我們知道annotation
有三個保留級別
-
RetentionPolicy.SOURCE
注解只在源碼階段保留女嘲,在編譯器進行編譯時它將被丟棄忽視畜份。 -
RetentionPolicy.CLASS
注解只被保留到編譯進行的時候,它并不會被加載到 JVM 中欣尼。 -
RetentionPolicy.RUNTIME
注解可以保留到程序運行的時候爆雹,它會被加載進入到 JVM 中,所以在程序運行時可以獲取到它們
annotation
實際上就是一個標簽愕鼓,單獨存在的時候沒有任何實際意義钙态。為了便于理解,這里再延伸一下另一個詞語—Hook菇晃,Hook的英文解釋是鉤子册倒。依我的理解,注解實際上就像這個鉤子磺送,勾住”類“剩失、”方法“、”字段“册着,為了后續(xù)想對這些被“勾住”的東西做一些操作提供了方便
更多關(guān)于注解的知識可以自己查看相關(guān)資料拴孤,這里就不多做介紹了
AnnotationProcessor
annotationProcessor
是APT工具中的一種,他是Google開發(fā)的內(nèi)置框架甲捏,不需要引入演熟,可以直接在build.gradle
文件中使用,如下:
dependencies {
annotationProcessor project(':compiler')
}
APT簡單的說就是注解處理器司顿,主要作用是可以編寫一些規(guī)則在編譯期間找出項目中的特定注解芒粹,以注解中的參數(shù)作為輸入,生成文件.java文件作為輸出大溜。注意化漆,這里的重點是生成.java文件,而不能修改已經(jīng)存在的Java類钦奋,例如不能向已有的類中添加方法
開始ButterKnife之旅
ButterKnife使用簡單介紹
先來看一下ButterKnife
的常規(guī)使用方法座云,我們可以在Activity
中的任意方法中直接使用這個textView
疙赠,省去了findViewById
的操作
public class MainActivity extends AppCompatActivity {
@BindView(R.id.txt_test)
TextView textView;
@BindView(R.id.btn_test)
Button button
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//調(diào)用框架方法
ButterKnife.bind(this);
//業(yè)務(wù)代碼
textView.setText("Hello World");
button.setOnClickListenr(new OnClickListener(View view){})
}
}
問題分析
我們先不去看源碼,我們可以設(shè)想一下ButterKnife.bind(this)
做了什么事情朦拖,我認為大概是像下面這樣:
public class ButterKnife{
public static void bind(MainActivity activity){
activity.textView = activity.findViewById(R.id.txt_test);
activity.button = activity.findViewById(R.id.btn_test)
}
}
接下來會遇到幾個問題:
問題一:我們?nèi)绾螌⒖丶囊煤涂丶膇d關(guān)聯(lián)起來圃阳?我想我們應(yīng)該很快有答案了,@BindView
注解其實就是起到了關(guān)聯(lián)的作用
問題二:前面說到璧帝,APT只能生成.java
文件捍岳,而不能直接在方法中插入代碼。那么怎么辦呢睬隶,我們可以通過APT生成.java
文件锣夹,然后在運行時通過反射調(diào)用它,如下所示
- 創(chuàng)建一個接口(接口是一種約束),這里用到了泛型苏潜,因為我們要適用所有
Activity
public interface BindAdapter<T> {
void bind(T activity);
}
- 我們通過APT生成
BindAdapterImp
類晕城,實現(xiàn)BindAdapter
接口
public class BindAdapterImp implement BindAdapter<MainActivity>{
public void bind(MainActivity activity) {
activity.textView = activity.findViewById(R.id.txt_test);
activity.button = activity.findViewById(R.id.btn_test)
}
}
- 在
ButterKnife
的bind()
里,通過反射生成BindAdapterImp
窖贤,在調(diào)用其bind()
public class ButterKnife {
private static final String CLASS_NAME = "";
public static void bind(Activity activity){
//反射拿到class
Class<?> bindAdapterClass = Class.forName(CLASS_NAME);
//通過class拿到BindAdapterImp對象
BindAdapterImp adapter = (BindAdapterImp) bindAdapterClass.newInstance();
//調(diào)用bind
adapter.bind(activituy)
}
}
問題三:問題又來了砖顷,我們把生成的BindAdapterImp
類放到哪個包下面能讓所有類都能調(diào)用到呢?答案是內(nèi)部類赃梧!
內(nèi)部類在編譯期間生成的實際上是單獨一個.java文件
所以我們會為每一個調(diào)用了ButterKnife.bind()
的Activity
生成一個BindAdapterImp
內(nèi)部類滤蝠,根據(jù)這個思路,我們對上面的代碼進行了一些優(yōu)化授嘀,如下所示
//這里的 MainActivity 是根據(jù)不同的Activity進行變化的
public class MainActivity$BindAdapterImp implement BindAdapter<MainActivity>{
public void bind(MainActivity activity) {
activity.textView = activity.findViewById(R.id.txt_test);
activity.button = activity.findViewById(R.id.btn_test)
}
}
public class ButterKnife {
private static final String SUFFIX = "$BindAdapterImp";
//做了一個緩存物咳,只有第一次bind時才通過反射創(chuàng)建對象
static Map<Class, BindAdapter> mBindCache = new HashMap();
public static void bind(Activity target){
BindAdapter bindAdapter;
if (mBindCache.get(target) != null) {
//如果緩存中有activity,從緩存中取
bindAdapter = mBindCache.get(target);
} else {
//緩存中沒有蹄皱,創(chuàng)建一個
String adapterClassName = target.getClass().getName() + SUFFIX;
Class<?> aClass = Class.forName(adapterClassName);
bindAdapter = (BindAdapter) aClass.newInstance();
mBindCache.put(aClass, bindAdapter);
}
//調(diào)用bind
bindAdapter.bind(target);
}
}
Tips:從上面的代碼我們發(fā)現(xiàn)览闰,為了盡量避免反射的性能消耗,
ButterKnife
內(nèi)部會有一個緩存巷折,這是一種典型的空間換時間的做法压鉴。在做內(nèi)存優(yōu)化的時候,我們往往會提到盡量少用ButterKnife
這種依賴注入框架其實就是這個原因锻拘。這個還需要大家對各自項目作出一個折中的選擇
最后油吭,我們面臨的問題實際上就是如何在編譯期生成上面BindAdapterImp
類,接下來跟著我一步步來吧
創(chuàng)建一個項目
注意如果這里勾選了androidx署拟,而且想要使用Kotlin婉宰,需要用kapt取代AnnotationProcessor
關(guān)于androidx與kotlin兼容問題具體參考:
當ButterKnife8.8.1碰到AndroidX怎么辦
看懂編譯注解annotationProcessor和kapt
創(chuàng)建一個注解類
新建一個java module,命名為annotation
創(chuàng)建編譯器注解類@BindView
推穷,這是一個屬性注解心包,只有在編譯期有效,經(jīng)過編譯后馒铃,注解信息會被丟棄蟹腾,不會保留到編譯好的class
文件里
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
int value();
}
創(chuàng)建AnnotationProcessor
新建一個java module痕惋,命名為processor
創(chuàng)建注解處理器,在編譯期間去掃描@BindView
所標注的屬性
@AutoService(Processor.class)
@SupportedAnnotationTypes("com.geekholt.annotation.BindView")
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class GeekKnifeProcessor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
return false;
}
}
-
@AutoService(Processor.class):向
javac
注冊我們這個自定義的注解處理器岭佳,這樣,在javac
編譯時萧锉,才會調(diào)用到我們這個自定義的注解處理器方法 - @SupportedAnnotationTypes():表示我們這個注解處理器所要處理的注解
- @SupportedSourceVersion():代表JDK版本號珊随,這里是代表java8
-
init():初始化時會自動被調(diào)用,并傳入
processingEnvironment
參數(shù)柿隙,通過該參數(shù)可以獲取到很多有用的工具類:Elements
,Types
,Filer
等等 -
process():
AnnotationProcessor
掃描出的結(jié)果會存儲進roundEnvironment
中叶洞,可以從中獲取注解所標注的內(nèi)容信息 - ProcessingEnvironment
/**用于提供工具類**/
public interface ProcessingEnvironment {
//返回注解處理器的配置參數(shù)
Map<String, String> getOptions();
//Message用來報告錯誤,警告和其他提示信息
Messager getMessager();
//Filer用于創(chuàng)建新的源文件禀崖,class文件或輔助文件(可以用JavaPoet簡化創(chuàng)建文件操作)
Filer getFiler();
//Elements包含用于操作Element的工具方法
Elements getElementUtils();
//Types包含用于操作TypeMirror的工具方法
Types getTypeUtils();
//返回Java版本
SourceVersion getSourceVersion();
//返回當前語言環(huán)境或者null(沒有語言環(huán)境)
Locale getLocale();
}
- RoundEnvironment
/**用于獲取注解所標注的內(nèi)容信息**/
public interface RoundEnvironment {
boolean processingOver();
//返回上一輪注解處理器是否產(chǎn)生錯誤
boolean errorRaised();
//返回上一輪注解處理器生成的根元素
Set<? extends Element> getRootElements();
//返回包含指定注解類型的元素的集合
Set<? extends Element> getElementsAnnotatedWith(TypeElement var1);
//返回包含指定注解類型的元素的集合
Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> var1);
}
- Element
Element
代表一個靜態(tài)的衩辟,語言級別的構(gòu)件,對于Java源文件來說波附,Element
代表程序元素:包艺晴,類,方法都是一種程序元素
VariableElement
:代表一個字段掸屡,枚舉常量封寞,方法或者構(gòu)造方法的參數(shù),局部變量及異常參數(shù)等元素
PackageElement
:代表包元素
TypeElement
:代表類或接口元素
ExecutableExement
:代表方法仅财,構(gòu)造函數(shù)狈究,類或接口的初始化代碼塊等元素,也包括注解類型元素
- TypeMirror
TypeMirror
代表java語言中的類型盏求。Types
包括基本類型抖锥、聲明類型(類類型和接口類型)、數(shù)組碎罚、類型變量和空類型磅废。 也代表通配類型參數(shù),可執(zhí)行文件的簽名和返回類型等荆烈。TypeMirror
類中最重要的是getKind()
方法还蹲, 該方法返回TypeKind
類型
簡單來說,
Element
代表源代碼耙考,TypeElement
代表的是源碼中的類型元素谜喊,比如類。雖然我們可以從TypeElement
中獲取類名倦始, 但是TypeElement
中不包含類本身的信息斗遏,比如它的父類,要想獲取這信息需要借助TypeMirror
鞋邑,可以通過Element
中的asType()
獲取元素對應(yīng)的TypeMirror
創(chuàng)建BindAdapter接口
新建一個android module诵次,命名為butterknife
創(chuàng)建BindAdapter
接口
package com.geekholt.butterknife;
public interface BindAdapter<T> {
void bind(T activity);
}
處理依賴關(guān)系
- app module
compileOnly project(':annotation')
annotationProcessor project(':processor')
api project(':butterknife')
- processor module
api project(':annotation')
編寫AnnotationProcessor
基本工作都已經(jīng)做好了账蓉,我們的目標也已經(jīng)很明確了,我們最終想要生成的就是像下面這樣一個文件
package com.geekholt.geekknife_example;
import com.geekholt.geekknife.adapter.BindAdapter;
public class MainActivity$BindAdapterImp implement BindAdapter<MainActivity>{
public void bind(MainActivity activity) {
activity.textView = activity.findViewById(R.id.txt_test);
activity.button = activity.findViewById(R.id.btn_test)
}
}
我們需要獲取哪些內(nèi)容呢逾一?
包名
注解所在的類的類名(Activity名)
注解的成員變量名(控件名)
注解的元數(shù)據(jù)(資源Id)
所以铸本,最終完成后的AnnotationProcessor
就是下面這樣,獲取到我們需要的內(nèi)容后遵堵,生成java文件箱玷,邏輯其實非常簡單,只是相關(guān)的API不是很常用陌宿,可能需要熟悉一下
@AutoService(Processor.class)
@SupportedAnnotationTypes("com.geekholt.annotation.BindView")
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class ButterKnifeProcessor extends AbstractProcessor {
private Filer mFiler;
private Messager mMessager;
private Elements mElementUtils;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
mFiler = processingEnvironment.getFiler();
mMessager = processingEnvironment.getMessager();
mElementUtils = processingEnvironment.getElementUtils();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
Set<? extends Element> bindViewElements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
for (Element element : bindViewElements) {
//1.獲取包名
PackageElement packageElement = mElementUtils.getPackageOf(element);
String packName = packageElement.getQualifiedName().toString();
print(String.format("package = %s", packName));
//2.注解所在的類的類名
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
String className = enclosingElement.getSimpleName().toString();
print(String.format("enclosindClass = %s", enclosingElement));
//因為BindView只作用于filed锡足,所以這里可直接進行強轉(zhuǎn)
VariableElement bindViewElement = (VariableElement) element;
//3.獲取注解的成員變量名
String fieldName = bindViewElement.getSimpleName().toString();
//4.獲取注解元數(shù)據(jù)
BindView bindView = element.getAnnotation(BindView.class);
int id = bindView.value();
print(String.format("%s = %d", fieldName, id));
//4.生成文件
createFile(packName, className, fieldName, id);
return true;
}
return false;
}
/**創(chuàng)建文件**/
private void createFile(String packName, String className, String fieldName, int id) {
try {
String newClassName = className + "$BindAdapterImp";
JavaFileObject jfo = mFiler.createSourceFile(packName + "." + newClassName, new Element[]{});
Writer writer = jfo.openWriter();
writer.write("package " + packName + ";");
writer.write("\n\n");
writer.write("import com.geekholt.butterknife.BindAdapter;");
writer.write("\n\n\n");
writer.write("public class " + newClassName + " implements BindAdapter<" + className + "> {");
writer.write("\n\n");
writer.write("public void bind(" + className + " target) {");
writer.write("target." + fieldName + " = target.findViewById(" + id + ");");
writer.write("\n");
writer.write(" }");
writer.write("\n\n");
writer.write("}");
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/**打印編譯期間的日志**/
private void print(String msg) {
mMessager.printMessage(Diagnostic.Kind.NOTE, msg);
}
}
創(chuàng)建文件推薦使用javapoet:https://github.com/square/javapoet
rebuild一下項目,在相關(guān)目錄下就可以看到我們想要的文件就已經(jīng)成功生成了
反射調(diào)用生成的代碼
接下來的內(nèi)容其實我們一開始就已經(jīng)說過了壳坪,我們需要在運行時通過反射調(diào)用我們編譯期生成的類
在butterKnife module下創(chuàng)建ButterKnife
類
public class ButterKnife {
private static final String SUFFIX = "$BindAdapterImp";
//做了一個緩存舶得,只有第一次bind時才通過反射創(chuàng)建對象
static Map<Class, BindAdapter> mBindCache = new HashMap();
public static void bind(Activity target) {
BindAdapter bindAdapter = null;
if (mBindCache.get(target) != null) {
//如果緩存中有activity,從緩存中取
bindAdapter = mBindCache.get(target);
} else {
//緩存中沒有爽蝴,創(chuàng)建一個
try {
String adapterClassName = target.getClass().getName() + SUFFIX;
Class<?> aClass = Class.forName(adapterClassName);
bindAdapter = (BindAdapter) aClass.newInstance();
mBindCache.put(aClass, bindAdapter);
} catch (Exception e) {
e.printStackTrace();
}
}
//調(diào)用bind
if (bindAdapter != null) {
bindAdapter.bind(target);
}
}
}
在我們的MainActivity
中調(diào)用ButterKnife.bind(this)
public class MainActivity extends AppCompatActivity {
@BindView(R.id.txt_main)
TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
textView.setText("Hello ButterKnife");
}
}
運行一下項目沐批,看,“ButterKnife”就順利工作了蝎亚!是不是比想象的簡單呢珠插!