示例:Android註解實現程式碼注入
阿新 • • 發佈:2019-01-26
前面的部落格Android中的註解中,
我們簡單描述了Android中註解的含義和用途。
除了基本的用法外,註解還可以幫助我們實現程式碼注入,達到類似IoC的效果。
本篇部落格以一個簡單的例子,記錄一下相關的內容。
通常的情況下,我們初始化介面的程式碼類似於:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button mBtn1;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super .onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mBtn1 = findViewById(R.id.test);
mBtn1.setOnClickListener(this);
}
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.test:
Log.d("ZJTest" , "click test btn");
break;
default:
break;
}
}
}
在上面程式碼的基礎上,我們看看如何使用註解來實現程式碼注入。
一、注入ContentView
首先,我們來簡單地替換掉setContentView方法。
定義一個註解:
/**
* @author zhangjian on 18-3-16.
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ContentView {
int value();
}
其中,@Target表示該註解可以用於什麼地方,其定義如下:
public enum ElementType {
/**
* Class, interface or enum declaration.
*/
TYPE,
/**
* Field declaration.
*/
FIELD,
/**
* Method declaration.
*/
METHOD,
/**
* Parameter declaration.
*/
PARAMETER,
/**
* Constructor declaration.
*/
CONSTRUCTOR,
/**
* Local variable declaration.
*/
LOCAL_VARIABLE,
/**
* Annotation type declaration.
*/
ANNOTATION_TYPE,
/**
* Package declaration.
*/
PACKAGE
}
@Retention表示:表示需要在什麼級別儲存該註解資訊,其定義類似於:
public enum RetentionPolicy {
/**
* Annotation is only available in the source code.
*/
SOURCE,
/**
* Annotation is available in the source code and in the class file, but not
* at runtime. This is the default policy.
*/
CLASS,
/**
* Annotation is available in the source code, the class file and is
* available at runtime.
*/
RUNTIME
}
定義完該註解後,我們就可以這樣使用了:
//ContentView註解修飾類,其值為R.layout.activity_main
@ContentView(value = R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
.............
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//注入程式碼
ViewInjectUtils.inject(this);
}
.............
}
public class ViewInjectUtils {
public static void inject(Activity activity) {
injectContentView(activity);
..............
}
private static void injectContentView(Activity activity) {
Class<? extends Activity> clazz = activity.getClass();
//獲取註解
ContentView contentView = clazz.getAnnotation(ContentView.class);
if (contentView != null) {
//註解的值就是layout id
int contentViewId = contentView.value();
try {
activity.setContentView(contentViewId);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
單純從上面的程式碼來看,註解注入程式碼實際上沒有任何的優勢。
這裡重要的是瞭解使用方法。
二、注入普通View
現在我們再定義一個註解:
/**
* @author zhangjian on 18-3-16.
*/
//這是修飾成員變數的
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ViewInject {
int value();
}
定義該註解後,就可以這麼使用了:
/**
* @author zhangjian
*/
@ContentView(value = R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
//修飾成員變數
@ViewInject(R.id.test)
private Button mBtn1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ViewInjectUtils.inject(this);
}
..........
}
/**
* @author zhangjian on 18-3-16.
*/
public class ViewInjectUtils {
public static void inject(Activity activity) {
injectContentView(activity);
injectViews(activity);
...........
}
.............
private static void injectViews(Activity activity) {
Class<? extends Activity> clazz = activity.getClass();
//遍歷所有field,找出其中具有ViewInject註解的
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
ViewInject viewInject = field.getAnnotation(ViewInject.class);
if (viewInject != null) {
//取出值
int viewId = viewInject.value();
if (viewId != -1) {
try {
//找到view並賦值
Object view = activity.findViewById(viewId);
field.setAccessible(true);
field.set(activity, view);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
.........
}
有了ViewInject註解後,定義新的View時,只需要標上註解即可,
省略了每次呼叫findViewById。
不過從效率來看,這仍然沒有實際呼叫效率高。
還是那句話,重在體會這種思想。
三、注入事件監聽
實現了View的注入後,我們看看如何來通過注入完成對View點選事件的監聽。
我們先定義一個註解:
/**
* @author zhangjian on 18-3-16.
*/
//這個註解是用來修飾註解的
@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface EventBase {
String setListenerMethod();
Class<?> listenerClass();
String listenerCallback();
}
基於EventBase再來定義一個註解:
/**
* @author zhangjian on 18-3-16.
*/
//修飾method的
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
//定義該註解對應的描述
@EventBase(setListenerMethod = "setOnClickListener",
listenerClass = View.OnClickListener.class, listenerCallback = "onClick")
public @interface OnClick {
int[] value();
}
有了上述註解後,我們之前的demo就可以修改為:
/**
* @author zhangjian
*/
@ContentView(value = R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
@ViewInject(R.id.test)
private Button mBtn1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ViewInjectUtils.inject(this);
}
//我們的目前就是實現,點選R.id.test對應的button後
//會呼叫到clickBtnInvoked函式
@OnClick({R.id.test})
public void clickBtnInvoked(View view) {
switch (view.getId()) {
case R.id.test:
Log.d("ZJTest", "click test btn");
break;
default:
break;
}
}
}
/**
* @author zhangjian on 18-3-16.
*/
public class ViewInjectUtils {
public static void inject(Activity activity) {
injectContentView(activity);
injectViews(activity);
injectEvents(activity);
}
.............
private static void injectEvents(Activity activity) {
Class<? extends Activity> clazz = activity.getClass();
Method[] methods = clazz.getMethods();
//遍歷所有的method
for (Method method : methods) {
//獲取每個method對應的註解
Annotation[] annotations = method.getAnnotations();
for (Annotation annotation : annotations) {
//獲取註解對應的型別
Class<? extends Annotation> annotationType = annotation.annotationType();
//若是EventBase修飾的註解,則需要注入事件監聽
EventBase eventBase = annotationType.getAnnotation(EventBase.class);
if (eventBase != null) {
//class name
Class<?> listenerType = eventBase.listenerClass();
//class method
String listenerSetter = eventBase.setListenerMethod();
//callback method
String methodName = eventBase.listenerCallback();
try {
//獲取註解對應的值
Method annotationMethod = annotationType.getDeclaredMethod("value");
int[] viewIds = (int[])annotationMethod.invoke(annotation);
//建立動態代理
DynamicHandler handler = new DynamicHandler(activity);
//關聯listener的回撥介面和註解實際修飾的method
handler.addMethod(methodName, method);
Object listener = Proxy.newProxyInstance(listenerType.getClassLoader(),
new Class<?>[] {listenerType}, handler);
//為每一個View設定listener
for (int viewId : viewIds) {
View view = activity.findViewById(viewId);
Method setListenerMethod = view.getClass()
.getMethod(listenerSetter, listenerType);
setListenerMethod.invoke(view, listener);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
至此,事件注入實現完畢。
其中,唯一有疑點就是生成動態代理的程式碼。
/**
* @author zhangjian on 18-3-16.
*/
public class DynamicHandler implements InvocationHandler {
private WeakReference<Object> mHandlerRef;
private final HashMap<String, Method> mMethodMap = new HashMap<>(1);
public DynamicHandler(Object handler) {
mHandlerRef = new WeakReference<>(handler);
}
//完成關聯
public void addMethod(String name, Method method) {
mMethodMap.put(name, method);
}
public Object getHandler() {
return mHandlerRef.get();
}
public void setHandler(Object handler) {
mHandlerRef = new WeakReference<>(handler);
}
//這個函式其實就能很好的反映動態代理的功能
@Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
Object handler = mHandlerRef.get();
if (handler != null) {
//呼叫實際的方法
String methodName = method.getName();
Method realM = mMethodMap.get(methodName);
if (realM != null) {
return realM.invoke(handler, objects);
}
}
return null;
}
}
四、總結
至此,通過註解完成程式碼注入的示例介紹完畢。
雖然從上述例子來看,註解的優勢似乎並不明顯;
但這裡重點在於記錄一種思路,
為分析和使用retrofit等開源庫打下基礎。