前兩天在掘金上看到一篇文章: 一個小需求引起的思考。 需求是根據多個EditText是否有輸入的值來肯定Button是否可點擊。很常見的一個需求吧,可是要怎麼作到簡單優雅呢?文章裏也有講到封裝的過程,這裏我就不細講了。最後做者封裝成了用註解就能夠作到。可是他所有用的反射,反射技術對APP性能影響仍是很大的,做者最後也提到想使butterknife使用的技術。用你們都知道著名的註解框架 butterknife是使用了動態生成java代碼的技術,這對性能影響很是小,我就在想我是否是也能夠試試呢,正好學習一下butterknife原理以及用到的技術。因而說幹就幹,就有了下面的嘗試!(我也是在查閱了許多資料後學習中摸索的,若是有寫的不對的地方請大神指正。另外第一次寫博客,排版什麼的可能不是很美觀。哈哈哈,想把本身的學習心得分享出來。慢慢進步吧!)java
首先要知道butterknife使用了什麼技術,那就得閱讀源碼,網上搜一搜文章一大堆哈。這就不廢話了哈哈哈。其實最重要的技術點就兩個:git
閱讀源碼得知前者使用了 AndroidAnnotations框架,後者則使用了Javapoet框架。這裏簡單介紹一下吧。github
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)
api
同時在java的同級目錄新建resources目錄,新建META-INF/services/javax.annotation.processing.Processor
文件,文件中填寫你自定義的Processor
全類名,這是向JVM聲明解析器。緩存
固然幸虧咱們如今使用的是AndroidStudio,能夠用auto-service來替代以上操做。只要在註解類前面加上@AutoService(Processor.class)就能夠替代以上操做。它是由谷歌開發的,在gradle中加上:bash
compile 'com.google.auto.service:auto-service:1.0-rc2'
複製代碼
有興趣的小夥伴能夠自行網上搜索瞭解更多內容....app
Javapoet是squareup公司提供的可以自動生成java代碼的庫。框架
講一下javapoet裏面經常使用的幾個類: MethodSpec
表明一個構造函數或方法聲明。 TypeSpec
表明一個類,接口,或者枚舉聲明。 FieldSpec
表明一個成員變量,一個字段聲明。 JavaFile
包含一個頂級類的Java文件ide
舉個例子:
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進行測試的。
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);
這兩句話究竟是怎麼生效的呢?這就到下一個模塊了。
先來看這個註解標記
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface WatchEdit {
/**
* 被觀察的輸入框的id
*
* @return
*/
int[] editIds();
}
複製代碼
它位於annotation模塊中,爲了觀察多個EditText,定義一個註解。參數是 要觀察的EditText的id。
再來看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實現類來協調。
複製代碼
如今咱們來看看怎麼生成這個實現類。
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();
}
}
複製代碼