引言
前兩天在掘金上看到一篇文章: 一個小需求引發的思考。 需求是根據多個EditText是否有輸入的值來確定Button是否可點選。很常見的一個需求吧,但是要怎麼做到簡單優雅呢?文章裡也有講到封裝的過程,這裡我就不細講了。最後作者封裝成了用註解就可以做到。但是他全部用的反射,反射技術對APP效能影響還是很大的,作者最後也提到想使butterknife使用的技術。用大家都知道著名的註解框架 butterknife是使用了動態生成java程式碼的技術,這對效能影響非常小,我就在想我是不是也可以試試呢,正好學習一下butterknife原理以及用到的技術。於是說幹就幹,就有了下面的嘗試!(我也是在查閱了許多資料後學習中摸索的,如果有寫的不對的地方請大神指正。另外第一次寫部落格,排版什麼的可能不是很美觀。哈哈哈,想把自己的學習心得分享出來。慢慢進步吧!)
分析
首先要知道butterknife使用了什麼技術,那就得閱讀原始碼,網上搜一搜文章一大堆哈。這就不廢話了哈哈哈。其實最重要的技術點就兩個:
- 怎麼解析處理註解
- 怎麼動態生成java程式碼檔案
閱讀原始碼得知前者使用了 AndroidAnnotations框架,後者則使用了Javapoet框架。這裡簡單介紹一下吧。
準備工作
AndroidAnnotations
AndroidAnnotations是一個javax的註解解析技術。我們可以通過繼承javax.annotation.processing.AbstractProcessor這個類來定義一個自己的註解處理類。(由於Android已經不支援javax程式設計了,所以需要在一個java lib 中來寫)。
@AutoService(Processor.class)
public class TestProcessor extends AbstractProcessor {
/**
* 每一個註解處理器類都必須有一個空的建構函式。然而,這裡有一個特殊的init()方法,它會被註解處理工具呼叫,
* 並輸入ProcessingEnviroment引數。ProcessingEnviroment提供很多有用的工具類Elements,Types和Filer。
* @param processingEnvironment
*/
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
}
/**
* 這相當於每個處理器的主函式main()。 在這裡寫掃描、評估和處理註解的程式碼,以及生成Java檔案。
* 輸入引數RoundEnviroment,可以讓查詢出包含特定註解的被註解元素。
* @param set
* @param roundEnvironment
* @return
*/
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
return false;
}
/**
* 這個註解處理器是註冊給哪個註解的。注意,它的返回值是一個字串的集合,
* 包含本處理器想要處理的註解型別的合法全稱。換句話說,在這裡定義你的註解處理器註冊到哪些註解上。
* @return
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
return super.getSupportedAnnotationTypes();
}
/**
* 返回支援的java版本
* @return
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return super.getSupportedSourceVersion();
}
}
複製程式碼
上面的程式碼中通過註釋應該瞭解每一個方法的作用了。我們處理的主要方法就是process()方法了。
另外這個類上面還有一個註解@AutoService(Processor.class)
它是幹嘛的呢?是這樣的:在以往的定義註解解析器時,需要在解析器類定義過程中,做以下操作:
在解析類名前定義:
@SupportedAnnotationTypes("com.bosssoft.cloin.ViewInjectProcesser")
@SupportedSourceVersion(SourceVersion.RELEASE_7)
同時在java的同級目錄新建resources目錄,新建META-INF/services/javax.annotation.processing.Processor
檔案,檔案中填寫你自定義的Processor
全類名,這是向JVM宣告解析器。
當然幸好我們現在使用的是AndroidStudio,可以用auto-service來替代以上操作。只要在註解類前面加上@AutoService(Processor.class)就可以替代以上操作。它是由谷歌開發的,在gradle中加上:
compile 'com.google.auto.service:auto-service:1.0-rc2'
複製程式碼
有興趣的小夥伴可以自行網上搜尋瞭解更多內容....
Javapoet
Javapoet是squareup公司提供的能夠自動生成java程式碼的庫。
講一下javapoet裡面常用的幾個類:
MethodSpec
代表一個建構函式或方法宣告。
TypeSpec
代表一個類,介面,或者列舉宣告。
FieldSpec
代表一個成員變數,一個欄位宣告。
JavaFile
包含一個頂級類的Java檔案
舉個例子:
private void generateHelloworld() throws IOException{
//構建main方法
MethodSpec main = MethodSpec.methodBuilder("main") //main代表方法名
.addModifiers(Modifier.PUBLIC,Modifier.STATIC)//Modifier 修飾的關鍵字
.addParameter(String[].class, "args") //新增string[]型別的名為args的引數
.addStatement("$T.out.println($S)", System.class,"Hello World")//新增程式碼,這裡其實就是新增了System,out.println("Hello World");
.build();
//構建HelloWorld類
TypeSpec typeSpec = TypeSpec.classBuilder("HelloWorld")//HelloWorld是類名
.addModifiers(Modifier.FINAL,Modifier.PUBLIC)
.addMethod(main) //在類中新增方法
.build();
//生成java檔案
JavaFile javaFile = JavaFile.builder("com.example.helloworld", typeSpec)
.build();
javaFile.writeTo(System.out);
}
複製程式碼
執行一下
看到這應該知道java程式碼是怎麼生成的了吧。現在需要用到的技術點都大致瞭解了。準備工作做好了,現在進入正題吧。
擼程式碼
專案結構
annotation:(java lib) 提供註解。 annotation-compiler:(java lib)註解處理。 annotation-api:(Android Lib) 是我們外部用到 api。 app:是呼叫api進行測試的。
APP模組
public class MainActivity extends AppCompatActivity {
@WatchEdit(editIds = {R.id.ed_1, R.id.ed_2, R.id.ed_3})
Button button1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button1 = (Button) findViewById(R.id.bbb11);
ViewAnnoUtil.watch(this);
button1.setEnabled(false);
}
}
複製程式碼
看看我們使用了什麼:
一個註解標記:@WatchEdit
還有一句程式碼: ViewAnnoUtil.watch(this);
這兩句話到底是怎麼生效的呢?這就到下一個模組了。
annotation模組
先來看這個註解標記
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface WatchEdit {
/**
* 被觀察的輸入框的id
*
* @return
*/
int[] editIds();
}
複製程式碼
它位於annotation模組中,為了觀察多個EditText,定義一個註解。引數是 要觀察的EditText的id。
api模組
再來看ViewAnnoUtl.watch(this)
幹了啥:
//最終對外使用的工具類
public class ViewAnnoUtil {
private static ActivityWatcher actWatcher = new ActivityWatcher();
private static Map<String, Injector> WATCH_MAP = new HashMap<>();
public static void watch(Activity activity) {
watch(activity, activity, actWatcher);
}
private static void watch(Object host, Object source, Watcher watcher) {
String className = host.getClass().getName();
try {
Injector injector = WATCH_MAP.get(className);
if (injector == null) {
Class<?> finderClass = Class.forName(className + "$$Injector");
injector = (Injector) finderClass.newInstance();
WATCH_MAP.put(className, injector);
}
injector.inject(host, source, watcher);
} catch (Exception e) {
e.printStackTrace();
}
}
}
複製程式碼
看到這個類中的程式碼,我們知道,watch(this)
實際上呼叫了watch(Object host, Object source, Watcher watcher)
方法。
其中host
和 source
引數都是傳的activity
,watcher
引數則是傳的類裡例項化的ActivityWatcher
物件例項。做了一些由於使用這裡使用了一些反射,所以通過使用記憶體快取來進行優化。最後則呼叫了injector.inject()
方法,那我們看看這些都是什麼東西。
//觀察類的介面
public interface Watcher {
/**
* 查詢view的方法
*
* @param obj view的來源,哪個activity或者fragment
* @param id 要查詢的view的id
* @return 查詢到的view
*/
EditText findView(Object obj, int id) throws ClassCastException;
/**
* 進行觀察
*
* @param editText 被觀察的edit
* @param obser 觀察的view
*/
void watchEdit(EditText editText, View obser);
}
複製程式碼
//提供一個預設的通過Activity實現的Watcher
public class ActivityWatcher implements Watcher, TextWatcher {
private HashMap<View, ArrayList<EditText>> map = new HashMap<>();
@Override
public EditText findView(Object obj, int id) throws ClassCastException {
return (EditText) ((Activity) obj).findViewById(id);
}
@Override
public void watchEdit(EditText editText, final View obser) {
if (map.get(obser) == null) {
ArrayList<EditText> itemEditList = new ArrayList<>();
itemEditList.add(editText);
map.put(obser,itemEditList);
} else {
map.get(obser).add(editText);
}
editText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
if (checkEnable(map.get(obser)))
obser.setEnabled(true);
else obser.setEnabled(false);
}
});
}
private boolean checkEnable(ArrayList<EditText> editList) {
for (EditText text : editList) {
if (TextUtils.isEmpty(text.getText().toString()))
return false;
}
return true;
}
複製程式碼
這很好理解了吧,也就是說具體的讓Button監聽到EditText輸入變化的程式碼在這裡。
再來看injector
:
//繫結的介面類
public interface Injector<T> {
/**
* @param host 目標
* @param source 來源
* @param watcher 提供具體使用的方法 查詢edit,新增監聽
*/
void inject(T host, Object source, Watcher watcher);
}
複製程式碼
可以發現其實它是一個介面,規定了目標從哪裡來,由誰來執行這個監聽操作(Wathcer) 那麼問題來了,光是介面怎麼能夠實現功能呢?肯定得有一個介面的實現類才行吧。 彆著急,我們看這一段程式碼:
String className = host.getClass().getName();
Injector injector = WATCH_MAP.get(className);
if (injector == null) {
Class<?> finderClass = Class.forName(className + "$$Injector");
injector = (Injector) finderClass.newInstance();
WATCH_MAP.put(className, injector);
}
複製程式碼
可以發現其實我們用反射載入了一個類,類名是 host的類名+ "$$Injector" 是不是很熟悉?使用butterknife的小夥伴肯定遇到過 MainActivity&&ViewBinder這類似的類名吧。沒錯就是它。他就是我們 Injector的實現類,完成了具體的實現。只是它是由我們前面提到的 javapoet動態生成的。再來看看這個順序:
ViewAnnoUtil.watch() ----> injector.inject()並傳入了目標的Activity,和我們寫好的ActivityWacther。
通過動態生成的injector實現類來協調。
複製程式碼
現在我們來看看怎麼生成這個實現類。
compiler模組
annotation-compiler中包含註解處理器,java檔案生成等
常量類
//常量工具類
public class TypeUtil {
public static final ClassName WATCHER = ClassName.get("com.colin.annotation_api", "Watcher");
public static final ClassName INJECTOR = ClassName.get("com.colin.annotation_api", "Injector");
}
複製程式碼
註解處理類
@AutoService(Processor.class)
public class WatchEditProcessor extends AbstractProcessor {
//具體程式碼我放後面
}
複製程式碼
前面介紹過怎麼定義註解處理器,我們來看看這個類裡該幹什麼
先來簡單的,定義我們支援的註解,我們這隻支援@WatchEdit這個註解。(可以有多個)
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new LinkedHashSet<>();
types.add(WatchEdit.class.getCanonicalName());
return types;
}
複製程式碼
我們支援的java版本是最高版本
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
複製程式碼
定義幾個成員變數:
//檔案工具類
private Filer mFiler;
//處理元素的工具類
private Elements mElementUtils;
//log工具類
private Messager mMessager;
//使用了註解的類的包裝類的集合
private Map<String, WatchEditAnnotatedClass> mAnnotatedClassMap = new HashMap<>();
複製程式碼
然後在init方法中進行了初始化
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
mElementUtils = processingEnvironment.getElementUtils();
mMessager = processingEnvironment.getMessager();
mFiler = processingEnvironment.getFiler();
}
複製程式碼
最後看最重要的方法 process()
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
mAnnotatedClassMap.clear();
try {
processWatchEdit(roundEnvironment);
} catch (IllegalArgumentException e) {
error(e.getMessage());
return true;
}
try {
for (WatchEditAnnotatedClass annotatedClass : mAnnotatedClassMap.values()) {
info("generating file for %s", annotatedClass.getFullClassName());
annotatedClass.generateWatcher().writeTo(mFiler);
}
} catch (Exception e) {
e.printStackTrace();
error("Generate file failed,reason:%s", e.getMessage());
}
return true;
}
複製程式碼
返回true表示已經處理過。
先看這句processWatchEdit(roundEnvironment);
程式碼幹了什麼:
private void processWatchEdit(RoundEnvironment roundEnv) {
//遍歷處理 使用了 @WatchEdit 註解的類
//一個element代表一個元素(可以是類,成員變數等等)
for (Element element : roundEnv.getElementsAnnotatedWith(WatchEdit.class)) {
WatchEditAnnotatedClass annotatedClass = getAnnotatedClass(element);
//通過 roundEnv工具構建一個成員變數
WatchEditField field = new WatchEditField(element);
//新增使用了@WatchEdit註解的成員變數
annotatedClass.addField(field);
}
}
private WatchEditAnnotatedClass getAnnotatedClass(Element element) {
//得到一個 類元素
TypeElement encloseElement = (TypeElement) element.getEnclosingElement();
//拿到類全名
String fullClassName = encloseElement.getQualifiedName().toString();
//先從快取中取
WatchEditAnnotatedClass annotatedClass = mAnnotatedClassMap.get(fullClassName);
if (annotatedClass == null) {
//沒有就構建一個
annotatedClass = new WatchEditAnnotatedClass(encloseElement, mElementUtils);
//放入快取
mAnnotatedClassMap.put(fullClassName, annotatedClass);
}
return annotatedClass;
}
複製程式碼
這裡又用到了兩個類:
WatchEditField
:被@WatchEdit
註解標記的成員變數的包裝類。
WatchEditAnnotatedClass
:使用了@WatchEdit
註解的類。
拿上面的例子來說MainActivity
就是WatchEditAnnotatedClass
而Button button1
這個button1
就是WatchEditField
這兩個類裡面具體有什麼待會看,現在看下一段程式碼:
for (WatchEditAnnotatedClass annotatedClass : mAnnotatedClassMap.values()) {
info("generating file for %s", annotatedClass.getFullClassName());
annotatedClass.generateWatcher().writeTo(mFiler);
}
複製程式碼
這裡迴圈我們的包裝類,並呼叫generateWatcher()
方法,並寫入到前面提到的檔案工具中。 看這個方法名就知道,這裡就是生成java程式碼的核心方法了。至此流程終於連上了。。。不容易啊 =_=
梳理一下:
流程搞明白了,接下來看看,我們費了大力氣生成的java檔案怎麼生成的,也就是generateWatcher()
裡做了啥,來看程式碼:
public JavaFile generateWatcher() {
String packageName = getPackageName(mClassElement);
String className = getClassName(mClassElement, packageName);
//獲取到當前使用了註解標記的類名(MainActivity)
ClassName bindClassName = ClassName.get(packageName, className);
//構建出重寫的inject方法
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("inject")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.addParameter(TypeName.get(mClassElement.asType()), "host", Modifier.FINAL)
.addParameter(TypeName.OBJECT, "source")
.addParameter(TypeUtil.WATCHER, "watcher");
//新增程式碼
for (WatchEditField field : mFiled) {
//獲得每個button要監聽的EditText的id
int[] ids = field.getResIds();
if (ids != null) {
//為每個EditText新增監聽
for (int i = 0; i < ids.length; i++) {
//新增監聽
methodBuilder.addStatement("watcher.watchEdit(watcher.findView(source,$L),$N)",
ids[i], "host." + field.getFieldName());
// methodBuilder.addStatement("watcher.watchEdit(watcher.findView(source,$L),$N)",
// ids[i], field);
}
}
}
//構建類 MainActivity$$Injector
TypeSpec finderClass = TypeSpec.classBuilder(bindClassName.simpleName() + "$$Injector")
.addModifiers(Modifier.PUBLIC)
.addSuperinterface(ParameterizedTypeName.get(TypeUtil.INJECTOR, TypeName.get(mClassElement.asType())))
.addMethod(methodBuilder.build()) //新增剛剛生成的injector方法
.build();
//生成一個java檔案
return JavaFile.builder(packageName, finderClass).build();
}
複製程式碼
這裡就用到了我們前面提到的javapoet庫了。通過註釋應該很好理解這段程式碼的意思了。 關於javapoet有興趣的小夥伴可以自行搜尋瞭解更多內容。
好了,至此一切都結束了!!!至於WatchEditField的程式碼貼下面了。 測試了一波功能是正常執行了。。不會貼動圖。。。然後接下來就要考慮的是接觸繫結,釋放資源等等優化了。先到這吧。
結束
平時用起來很方便的東西瞭解一下原理才發現還是很複雜的。一個小小的需求,仔細研究一下也會學習到很多知識,學會了新的知識就可以應用到更多的方面去,有學習才有進步。加油。另外Demo我會放到Github上去。裡面也會慢慢更新出更多的東西 GitHub
被@WatchEdit註解標記的成員變數包裝類,如一個 button
public class WatchEditField {
private VariableElement mFieldElement;
private int[] mEditIds;
public WatchEditField(Element element) throws IllegalArgumentException {
if (element.getKind() != ElementKind.FIELD) {
throw new IllegalArgumentException(String.format("Only field can be annotated with @%s",
WatchEdit.class.getSimpleName()));
}
mFieldElement = (VariableElement) element;
WatchEdit bindView = mFieldElement.getAnnotation(WatchEdit.class);
if (bindView != null) {
mEditIds = bindView.editIds();
if (mEditIds == null && mEditIds.length <= 0) {
throw new IllegalArgumentException(String.format("editIds() in %s for field % is not valid",
WatchEdit.class.getSimpleName(), mFieldElement.getSimpleName()));
}
}
}
c
public Name getFieldName() {
return mFieldElement.getSimpleName();
}
public int[] getResIds() {
return mEditIds;
}
public TypeMirror getFieldType() {
return mFieldElement.asType();
}
public Object getConstantValue(){
return mFieldElement.getConstantValue();
}
}
複製程式碼