在項目開發(fā)中,大部分 Android 開發(fā)者都使用過 ButterKnife 這個通過注解簡化代碼的框架鹉究。部分 Android 開發(fā)者也使用過 xUtils 這種快速開發(fā)的框架宇立。
兩者均使用到了注解。在上一篇 Java注解自赔、反射妈嘹,安卓IOC(一) 中我們知道了注解及反射的用法。本篇會分別介紹下其實現機制绍妨。
運行時注解
首先我們自己簡單實現類似 xUtils 這種運行時注解框架润脸。
綁定 View 控件
創(chuàng)建注解:
@Retention(RetentionPolicy.RUNTIME)//運行時注解
@Target(ElementType.FIELD)//Target為屬性
public @interface FindView {
int value() default -1;
}
View解析代碼:
public class ViewInject {
public static void bind(Activity activity) {
inject(new ViewFinder(activity), activity);
}
public static void bind(View view) {
inject(new ViewFinder(view), view);
}
public static void bind(View view, Object obj) {
inject(new ViewFinder(view), obj);
}
private static void inject(ViewFinder finder, Object obj) {
injectFields(finder, obj);
injectMethods(finder, obj);
}
private static void injectFields(ViewFinder finder, Object obj) {
Class<?> clazz = obj.getClass();
Field[] fields = clazz.getDeclaredFields();//獲取所有變量
for (Field field : fields) {
if (field.isAnnotationPresent(FindView.class)) {
FindView findView = field.getAnnotation(FindView.class);//獲取注解
if (findView.value() < 0) {
throw new IllegalArgumentException("The id can't be -1.");
} else {
View view = finder.findViewById(findView.value());
try {
field.setAccessible(true);//破壞封裝
field.set(obj, view); //設置屬性
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
}
}
主要流程為通過反射獲取并遍歷所有變量,若變量被注解修飾痘绎,則將注解的 ID 賦值給指定方法并調用津函。
綁定 OnClick 事件
創(chuàng)建注解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD) //Target為方法
public @interface OnClick {
int[] id();
}
OnClick 事件注入:
private static void injectMethods(ViewFinder finder, final Object obj) {
Method[] methods = obj.getClass().getDeclaredMethods();
for (final Method method : methods) {
if (method.isAnnotationPresent(OnClick.class)) {
OnClick onClick = method.getAnnotation(OnClick.class);
if (onClick.id().length != 0) {
for (int i : onClick.id()) {
View view = finder.findViewById(i);
method.setAccessible(true);
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
method.invoke(obj, v);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
});
}
}
}
}
}
主要流程為通過反射獲取并遍歷所有方法,若方法被注解修飾孤页,遍歷所有的 ID 尔苦,將注解的 ID 賦值給 findViewById 方法,然后在 setOnClickListener 調用 method 方法行施。
在 Activity 中的使用:
public class IocActivity extends AppCompatActivity {
@FindView(R.id.txt_ioc_test)
private TextView mTxtTest;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_ioc);
ViewInject.bind(this);
mTxtTest.setText("測試");
}
@OnClick(id = {R.id.btn_ioc_test, R.id.btn_ioc_test2})
public void onClick(View view) {
switch (view.getId()) {
case R.id.btn_ioc_test:
Toast.makeText(this, "Click1", Toast.LENGTH_SHORT).show();
break;
case R.id.btn_ioc_test2:
Toast.makeText(this, "Click2", Toast.LENGTH_SHORT).show();
break;
}
}
}
以上為運行時注解的簡單實現允坚,即 xUtils 使用的注解方法。但是這種方法因為通過一系列反射獲取屬性蛾号、方法等稠项,對性能會有所影響,所以不建議在實際項目中使用鲜结,下邊介紹下輕量級的編譯時注解展运。
編譯時注解
ButterKnife 源碼解析網上已經有很多不錯的文章了,例如這篇 ButterKnife源碼分析 講的就很好精刷。這里主要介紹下自己的大致實現以及編譯時注解在 Android Studio 中的使用拗胜。
首先介紹下大概的項目結構,如下圖所示:
- annotation module: Java library - 定義一系列注解怒允。
- injetc module: Android library - 定義注解的接口及調用方法埂软。
- compiler module: Java library - 自定義編譯時注解 AbstractProcessor 在編譯期間生成 java 代碼。
- app: 使用方法纫事。
本篇文章主要為介紹及學習勘畔,所以此處僅實現 setContentView 的編譯時注解所灸。
1、聲明注解
@Retention(RetentionPolicy.CLASS) //編譯時注解
@Target(ElementType.TYPE) //修飾類
public @interface ContentView {
int value();
}
2炫七、聲明外界接口及方法
聲明接口:
public interface ContentInjector<T> {
void injectContent(T obj, Activity activity); //此處僅用 Activity 參數即可實現文章的 demo
}
提供方法:
public class ContentViewInject {
public static void bind(Activity activity) {//綁定
injectContentView(activity);
}
private static void injectContentView(Activity activity) {
Class<? extends Activity> clazz = activity.getClass();
try {
ContentInjector injector = (ContentInjector) Class.forName(clazz.getName()
+ "$$ViewBinder").newInstance();
injector.injectContent(activity, activity);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
以上代碼為簡單使用爬立,butterknife 中的 UnBinder 解綁,使用 Map 緩存等暫不考慮诉字。
此處的 Class.forName("") 以及 class.newInstance() 會對性能略有影響懦尝,butterknife 在此進行了 map 緩存優(yōu)化知纷。
3壤圃、自定義 AbstractProcessor,此處需將 module 設置為 Java library 才可繼承 AbstractProcessor琅轧。
@AutoService(Processor.class)
@SupportedSourceVersion(value = SourceVersion.RELEASE_7)
public class ContentViewInjectProcessor extends AbstractProcessor {
//可用 @SupportedAnnotationTypes("com.lauzy.ContentView") 注解 ContentViewInjectProcessor 代替
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> supportTypes = new LinkedHashSet<>();
supportTypes.add(ContentView.class.getCanonicalName());
return supportTypes;
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
String packageName;
String className;
//遍歷每個被 ContentView 修飾的 class 文件
for (Element element : roundEnvironment.getElementsAnnotatedWith(ContentView.class)) {
if (element.getKind() == ElementKind.CLASS) {
TypeElement typeElement = (TypeElement) element;
PackageElement packageEle = (PackageElement) element.getEnclosingElement();
packageName = packageEle.getQualifiedName().toString();//獲取包名
//因為是 ElementKind.CLASS 類型伍绳,所以可以直接強制轉換,獲取類名
className = typeElement.getSimpleName().toString();
int layoutId = typeElement.getAnnotation(ContentView.class).value();//獲取注解的 id
//拼接 Java 類的字符串
StringBuilder builder = new StringBuilder();
builder.append("package ").append(packageName).append(";\n");
builder.append("import android.view.View;\n");
builder.append("import android.app.Activity;\n");
builder.append("import com.freedom.lauzy.inject.ContentInjector;\n");
builder.append('\n');
builder.append("public class ").append(className + "$$ViewBinder");
builder.append("<T extends ").append(className).append(">");
builder.append(" implements ContentInjector<T>");
builder.append(" {\n");
builder.append("@Override\n")
.append("public void injectContent(final T source, Activity activity) {\n");
builder.append(" ((Activity) source).setContentView(" + layoutId);
builder.append(");\n");
builder.append("}\n\n}\n");
//寫入 Java 文件
try {
JavaFileObject fileObject = processingEnv.getFiler().createSourceFile(
packageName + "." + className + "$$ViewBinder",
typeElement);
Writer writer = fileObject.openWriter();
writer.write(builder.toString());
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
System.out.println("error");
}
}
}
return true;
}
}
butterknife 中使用了 javapoet 生成 Java 代碼文件
此 module 的 gradle 配置如下:
apply plugin: 'java'
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
compile 'com.google.auto.service:auto-service:1.0-rc2' // google 的生成源代碼庫
compile project(':annotation')
}
sourceCompatibility = "1.7"
targetCompatibility = "1.7"
4乍桂、app 使用
app 的 gradle 配置如下:
dependencies {
...
annotationProcessor project(':compiler')
compile project(':annotation')
compile project(':inject')
}
activity 中使用:
@ContentView(R.layout.activity_ioc)
public class IocActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ContentViewInject.bind(this);
}
}
這樣冲杀,整個流程就結束了。此時 build 整個項目則會在 app/build/generated/source/apt/debug/com.lauzy.freedom.lauzycode/IOC 文件夾下生成一個IocActivity$$ViewBinder 的 Java 文件睹酌,代碼如下:
package com.lauzy.freedom.lauzycode.IOC;
import android.app.Activity;
import com.freedom.lauzy.inject.ContentInjector;
public class IocActivity$$ViewBinder<T extends IocActivity> implements ContentInjector<T> {
@Override
public void injectContent(final T source, Activity activity) {
((Activity) source).setContentView(2130968606);
}
}
可以看到权谁,其實是在編譯時生成了一個 Java 文件,并在 activity 的 onCreate 方法中調用了 setContentView 方法憋沿。
注意事項:
1旺芽、若不使用 com.google.auto.service:auto-service:1.0-rc2 這個 google 的生成源代碼庫,則需要手動創(chuàng)建一個 META_INF 文件來指定注解辐啄。
需要在 compiler 中創(chuàng)建 compiler/src/main/resources/META-INF/services 目錄(注意 META-INF 中間不是下劃線采章,減號即可),并新建 txt 文件 javax.annotation.processing.Processor壶辜,
文件中寫入自定義 AbstractProcessor 的全名稱悯舟,如: com.lauzy.ContentViewInjectProcessor 。多個的話換行寫入砸民。
如下圖所示:
2抵怎、本文使用 annotationProcessor 的注解處理器代替 android-apt ,Google 內置的注解處理器岭参,建議使用反惕。
分析
編譯時注解的優(yōu)點 :在于對性能影響很小的情況下,大量簡化程序員的代碼冗荸,像 butterknife 在首次查找類的時候對性能稍有影響承璃,其他情況下影響微乎其微。
編譯時注解的缺點 :build 過程生成更多的代碼蚌本,增加了類和方法的數量盔粹;對性能影響很小隘梨,但是多少會有的。
本人認為編譯時注解在優(yōu)化代碼舷嗡,提高效率方面是有很大優(yōu)勢的轴猎,遠遠大于其缺點。
本篇文章主要是分析及梳理大致的實現方式进萄,僅為學習使用捻脖,所有代碼均托管在 我的Github 上。