在剛接觸安卓的第二天 , 自己最熟悉的代碼 , 就是那句findViewById. 記得當(dāng)時(shí)特別舒服的啪啪啪敲完一行有用的代碼 , 心里美滋兒啊 , 心里想著 , 啥時(shí)候?qū)懫渌倪壿嬀拖襁@段啪啪啪就能完成的代碼一樣 , 那多舒服 孝宗。
但是吧 , 人都是有惰性的 , 我說(shuō)的是偷懶的那種惰性:
在面對(duì)一個(gè)比較復(fù)雜的界面的時(shí)候 , 你需要機(jī)械化的吧所有的組件統(tǒng)統(tǒng)findViewById找出來(lái) , 然后再去做相關(guān)操作 ; 有時(shí)候僅僅是為了設(shè)置一個(gè)點(diǎn)擊事件 , 但卻必須要先聲明 , 查找 , 才能繼續(xù)完成接下來(lái)的工作 .于是 , 我想偷個(gè)懶了.....
抽取方法
當(dāng)然了,最先想到的蒿涎,當(dāng)然是想吧這些麻煩的重復(fù)性操作抽出來(lái)舷礼,其實(shí)也就是少寫(xiě)了findViewById的這么幾個(gè)字候齿,本質(zhì)上還是和以前的邏輯一樣:
protected final <T extends View> T $(@IdRes int id) {
return (T) view.findViewById(id);
}
這樣迄靠,就可以吧代碼簡(jiǎn)化為iv_head = $(R.id.iv_head);
emmm,好像意義不是很大卓缰,仍然是重復(fù)的工作叙赚。
IOC的出現(xiàn)
偶然的機(jī)會(huì),接觸到了ButterKnife的框架僚饭,這個(gè)框架極大地簡(jiǎn)化了組件查找,事件等一系列的操作胧砰,只需要一個(gè)注解就可以輕松搞定那些繁雜的工作鳍鸵。
@Bind(R.id.toolbar)
protected Toolbar toolbar;
這樣,通過(guò)注解就可以完成定義到查找的兩個(gè)工作尉间,恩~舒服偿乖,但是這個(gè)注解的背后又存在著怎么樣的實(shí)現(xiàn)呢?
略微看一下BK的源碼吧
- 首先哲嘲,我們點(diǎn)進(jìn)去@Bind這個(gè)注解贪薪,來(lái)到了響應(yīng)的注解接口
@Retention(CLASS) @Target(FIELD)
public @interface Bind {
/** View ID to which the field will be bound. */
int[] value();
}
哇什么鬼,還有@Interface,是不是沒(méi)見(jiàn)過(guò)的科技? 不要急 , 慢慢來(lái)看
這里一共有三個(gè)注解
Rentation: Reteniton的作用是定義被它所注解的注解保留多久,它的取值是一個(gè)枚舉類(lèi)型眠副,有三種:
SOURCE 被編譯器忽略
CLASS 注解將會(huì)被保留在Class文件中画切,但在運(yùn)行時(shí)并不會(huì)被VM保留。這是默認(rèn)行為
RUNTIME 保留至運(yùn)行時(shí)囱怕。所以我們可以通過(guò)反射去獲取注解信息霍弹。Target 用于設(shè)定注解使用范圍,接收參數(shù)為ElementType的枚舉
METHOD 可用于方法上
TYPE 可用于類(lèi)或者接口上
ANNOTATION_TYPE 可用于注解類(lèi)型上(被@interface修飾的類(lèi)型)
CONSTRUCTOR 可用于構(gòu)造方法上
FIELD 可用于域上
LOCAL_VARIABLE 可用于局部變量上
PACKAGE 用于記錄java文件的package信息
PARAMETER 可用于參數(shù)上-
interface: 用于自定義注解
自定義注解也就是可以自己寫(xiě)需要的注解
image.png
- 使用@interface關(guān)鍵字定義注解娃弓,注意關(guān)鍵字的位置
- 成員以無(wú)參數(shù)無(wú)異常的方式聲明典格,注意區(qū)別一般類(lèi)成員變量的聲明
- 可以使用default為成員指定一個(gè)默認(rèn)值,如上所示
- 成員類(lèi)型是受限的台丛,合法的類(lèi)型包括原始類(lèi)型以及String耍缴、Class、Annotation、Enumeration (JAVA的基本數(shù)據(jù)類(lèi)型有8種:byte(字節(jié))防嗡、short(短整型)变汪、int(整數(shù)型)、long(長(zhǎng)整型)本鸣、float(單精度浮點(diǎn)數(shù)類(lèi)型)疫衩、double(雙精度浮點(diǎn)數(shù)類(lèi)型)、char(字符類(lèi)型)荣德、boolean(布爾類(lèi)型)
- 注解類(lèi)可以沒(méi)有成員闷煤,沒(méi)有成員的注解稱(chēng)為標(biāo)識(shí)注解
- 如果注解只有一個(gè)成員,并且把成員取名為value()涮瞻,則在使用時(shí)可以忽略成員名和賦值號(hào)“=” ,例如JDK注解的@SuppviseWarnings 鲤拿;如果成員名不為value,則使用時(shí)需指明成員名和賦值號(hào)"="
打造手?jǐn)]的IOC框架
什么署咽,才剛剛不到5分鐘就可以開(kāi)始手?jǐn)]了么近顷,別慌, 剛宁否!
首先 窒升, 我們模擬三種需求:
- 使用注解設(shè)置布局,代替setContentView
- 使用ViewInject代替findViewById
- 使用@OnClick和@OnLongClick代替點(diǎn)擊與長(zhǎng)按
IOC的本質(zhì)就是控制反轉(zhuǎn) , 也就是將A要做的事情, 委托給B來(lái)做 , 所以我們需要一個(gè)第三方的容器類(lèi)來(lái)完成這些工作
首先做好準(zhǔn)備工作, 將BaseActivity搭建好 , 在里面調(diào)用容器的初始化 , 這樣讓每一個(gè)Activity去繼承改基類(lèi)就可以完成初始化的操作
public abstract class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//將當(dāng)前對(duì)象注入到第三方的容器
InjectUtils.inject(this);
increate(savedInstanceState);
}
public abstract void increate(Bundle savedInstanceState);
}
然后新建InjectUtils類(lèi) , 編寫(xiě)靜態(tài)的Inject方法 , 這里傳入上下文對(duì)象.
public static void inject(Context context) {
//先注入視圖,再注入控件
injectLayout(context);
injectView(context);
injectEvents(context);
}
定義好三個(gè)方法之后 , 我們的基礎(chǔ)框架就基本完成了 , 接下來(lái)實(shí)現(xiàn)一下具體的注解實(shí)現(xiàn)細(xì)節(jié)
任務(wù)1. 使用注解設(shè)置布局
@ConvertView(R.layout.activity_second)
首先 , 既然是自定義的注解 , 當(dāng)然要先有一個(gè)類(lèi)似于剛剛上面的自定義的@inteface , 然后默認(rèn)的方法參數(shù)為int類(lèi)型 , 代碼如下:
//運(yùn)行是也存在,用于注解
@Retention(RetentionPolicy.RUNTIME)
//用在類(lèi)上的注解, 寫(xiě)在類(lèi)的上方
@Target(ElementType.TYPE)
public @interface ConvertView {
int value();
}
定義好了注解之后 , 就要完成第三方容器中的方法了 , 這里我們使用反射的方法去找到注解, 然后反射到響應(yīng)的方法 , 并調(diào)用方法本身 , 就完成了容器的作用 , 也就是上面準(zhǔn)備的injectLayout方法
public static void injectLayout(Context context) {
int layoutId = 0;
Class clazz = context.getClass();
//從注解出拿到注解中的值
ConvertView view = (ConvertView) clazz.getAnnotation(ConvertView.class);
if (null != view) {
//從接口的value函數(shù)中獲取id值
try {
layoutId = view.value();
//利用反射獲取需要的方法
Method method = clazz.getMethod("setContentView", int.class);
//拿到setContentView后調(diào)用函數(shù)
method.invoke(context, layoutId);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
至此 , 我們完成了第一個(gè)需求 , 以后不再有setContentView, 只需要注解就可以了:
@ConvertView(R.layout.activity_second)
public class SecondActivity extends BaseActivity {
@Override
public void increate(Bundle savedInstanceState) {
}
}
任務(wù)2. 使用ViewInject代替findViewById
有了布局的經(jīng)驗(yàn)之后 , 我們可以輕車(chē)熟路的按照流程來(lái)實(shí)現(xiàn)這個(gè)方法
首先是自定義的注解
//運(yùn)行時(shí)也存在
@Retention(RetentionPolicy.RUNTIME)
//用在域上的注解
@Target(ElementType.FIELD)
public @interface ViewInject {
int value();
}
接下來(lái)在容器中實(shí)現(xiàn)委托的操作
public static void injectView(Context context) {
Class<? extends Context> aClass = context.getClass();
//獲取到上下文中所有成員變量
Field[] declaredFields = aClass.getDeclaredFields();
for (Field f : declaredFields) {
//獲取有注解的控件,注意注解之后沒(méi)有加分號(hào),意味著注解和類(lèi)型聲明是同一行語(yǔ)句 , 這里利用注解獲取控件的本質(zhì)是通過(guò)反射到成員變量
ViewInject annotation = f.getAnnotation(ViewInject.class);
//如果有注解 , 則獲取注解的值
if (null != annotation) {
int value = annotation.value();
try {
//調(diào)用了Activity的findViewById方法,context中沒(méi)有該方法,需要反射獲取
Method findViewById = aClass.getMethod("findViewById", int.class);
View view = (View) findViewById.invoke(context, value);
//允許反射私有變量
f.setAccessible(true);
//為反射到的變量賦值
f.set(context, view);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
}
好像比設(shè)置布局要復(fù)雜一點(diǎn)?其實(shí)是差不多的 , 只不過(guò)這里在拿到方法之后, 有設(shè)置了返回值并實(shí)例化(改變字段) , 懂了第一個(gè)之后 , 也不是很難理解吧 .
任務(wù)3. 使用@OnClick和@OnLongClick代替點(diǎn)擊與長(zhǎng)按
這個(gè)相比于前兩個(gè)就要復(fù)雜一些了 ,
- setContentView()只需要調(diào)用方法傳入?yún)?shù)即可
- findViewById(), 需要傳入?yún)?shù)并拿到返回值即可
- setOnclickListener()需要傳入一個(gè)接口并實(shí)現(xiàn)其方法 .
納尼?要傳入一個(gè)方法慕匠,也就是View.OnclickListener的實(shí)現(xiàn)方法饱须。我們知道這個(gè)方法是在View中的,這里難道還要傳入一個(gè)View的參數(shù)么台谊?沒(méi)必要這么麻煩的蓉媳。當(dāng)我們需要訪問(wèn)某個(gè)對(duì)象但存在困難時(shí),可以通過(guò)一個(gè)代理對(duì)象去間接的訪問(wèn)锅铅,所以就需要繞個(gè)小彎子: 使用代理模式酪呻。
在定義注解之前我們先想一下 , 如果要用注解編寫(xiě)點(diǎn)擊事件的話(huà) , 我們會(huì)省略掉一下幾個(gè)步驟:
- 點(diǎn)擊事件的方法 被省略
- 點(diǎn)擊事件的參數(shù): 被省略
- 匿名內(nèi)部類(lèi)的回調(diào)方法的方法回調(diào) 被省略
所以我們?cè)谧⒔庵行枰齻€(gè)參數(shù) 。而且后期我們會(huì)添加各種各樣的監(jiān)聽(tīng)事件盐须,所以在點(diǎn)擊事件上做一個(gè)封裝玩荠,用來(lái)管理所有的事件
@Retention(RetentionPolicy.RUNTIME)
//用于注解上的注解
@Target(ElementType.ANNOTATION_TYPE)
public @interface EventBase {
//設(shè)置監(jiān)聽(tīng)方法
String listenerSetter();
//事件類(lèi)型
Class listenerType();
//事件回調(diào)
String callBackMethod();
}
可以理解為注解的基類(lèi)。我們的點(diǎn)擊事件也好 , 長(zhǎng)按事件也好 , 都基于該基類(lèi)擴(kuò)展 , 所以該基類(lèi)會(huì)作為一個(gè)參數(shù)出現(xiàn)在另一個(gè)注解中 , 所以我們的@Target必須為ANNOTATION_TYPE , 也就是注解中的注解. 并且贼邓,將參數(shù)設(shè)置為String而不是Method姨蟋,也是為了反射與擴(kuò)展的方便。
首先我們編寫(xiě)OnClick注解立帖,使用基類(lèi)來(lái)傳遞參數(shù):
@Retention(RetentionPolicy.RUNTIME)
//用于方法上的注解
@Target(ElementType.METHOD)
@EventBase(listenerSetter = "setOnClickListener",
listenerType = View.OnClickListener.class,
callBackMethod = "onClick")
public @interface OnClick {
int[] value() default -1;
}
現(xiàn)在明白基類(lèi)的作用了吧眼溶,其實(shí)就是限制了參數(shù)的傳遞規(guī)范(這個(gè)規(guī)范使用枚舉會(huì)更有可讀性,這里就不浪了晓勇,大家自己來(lái)吧)堂飞。
敲重點(diǎn)灌旧,在委托容器中完成注入操作之前 , 我們先要編寫(xiě)代理 , 在代理中調(diào)用方法:
public class ListenerInvactionHandler implements InvocationHandler {
//被代理的真實(shí)對(duì)象的應(yīng)用
private Context context;
//保存方法名與方法體, 用來(lái)判斷是否需要被代理
private Map<String, Method> methodMap;
public ListenerInvactionHandler(Context context, Map<String, Method> methodMap) {
this.context = context;
this.methodMap = methodMap;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//首先獲取到方法名
String name = method.getName();
//根據(jù)方法名找方法,看是否需要代理
Method metf = methodMap.get(name);
if (metf != null) {
//需要代理,使用代理調(diào)用
return metf.invoke(context, args);
} else {
return method.invoke(proxy, args);
}
}
}
還算好理解吧 , 上面這個(gè)方法中, 集合時(shí)用來(lái)保存類(lèi)中所有的方法, 然后根據(jù)方法名來(lái)查看該方法是否需要被代理 , 如果需要的話(huà)使用代理來(lái)調(diào)用 , 否則使用本身來(lái)調(diào)用.
接下來(lái)是容器中的操作先貼上代碼,在代碼中吧重點(diǎn)都注釋了.
@SuppressLint("NewApi")
private static void injectEvents(Context context) {
Class<? extends Context> aClass = context.getClass();
//拿到類(lèi)中所有的方法
Method[] declaredMethods = aClass.getDeclaredMethods();
//遍歷所有的方法并查找?guī)ё⒔獾姆椒? for (Method m : declaredMethods) {
Annotation[] annotations = m.getAnnotations();
for (Annotation a : annotations) {
//獲取注解 annoType:OnClick
Class<? extends Annotation> annoType = a.annotationType();
//獲取注解的值,onClick注解上面的EventBase
EventBase base = annoType.getAnnotation(EventBase.class);
if (null == base) {
continue; //跳出本輪循環(huán)
}
/**
*拿到帶注解的方法, 開(kāi)始獲取事件三要素 , 通過(guò)反射注入進(jìn)去拿到真正的方法
*/
//1. 返回setOnclickListener字符串
String listenerSetter = base.listenerSetter();
//2. 返回View.OnClickListener字節(jié)碼
Class<?> listenerType = base.listenerType();
//3. 返回onClick字符串
String callMethod = base.callBackMethod();
//保存方法名與方法的應(yīng)映射, 在接下來(lái)的操作中方便使用
Map<String, Method> methodMap = new HashMap<>();
methodMap.put(callMethod, m);
try {
//拿到注解中的value方法
Method value = annoType.getDeclaredMethod("value");
//對(duì)應(yīng)value方法的返回值,這里通過(guò)反射是為了通用性,如果指定具體的類(lèi)可以直接獲取,但是擴(kuò)展性很低
int[] ids = (int[]) value.invoke(a);
//注入事件
for (int viewId : ids) {
Method findv = aClass.getMethod("findViewById", int.class);
//通過(guò)反射拿到View
View view = (View) findv.invoke(context, viewId);
if (null == view) continue;
/**
* @Param listenerSetter: setOnClickListener的字符串,可以反射出方法
* @Param listenerType: 參數(shù)類(lèi)型,為View.OnClickListener.class
*/
Method setOnclick = view.getClass().getMethod(listenerSetter, listenerType);
ListenerInvactionHandler handler = new ListenerInvactionHandler(context, methodMap);
//設(shè)置返回對(duì)象的類(lèi)型: proxyy是實(shí)現(xiàn)了OnclickListener接口,也就是listenerType接口的代理對(duì)象
Object proxy = Proxy.newProxyInstance(listenerType.getClassLoader(),
new Class[]{listenerType},
handler);
//利用代理設(shè)置監(jiān)聽(tīng)
setOnclick.invoke(view,proxy);
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
}
邏輯是復(fù)雜了一點(diǎn) , 這里只是有一個(gè)三重的for循環(huán) , 慢慢拆解下來(lái) , 其實(shí)也不算很難 , 重要的注釋都寫(xiě)在方法中了 , 去嘗試一下吧 .
但是 , 懂的老鐵們會(huì)說(shuō) , 你這個(gè)是運(yùn)行時(shí)注解绰筛,在使用的時(shí)候枢泰,大量的反射會(huì)影響性能,是的铝噩,這個(gè)確實(shí)是一個(gè)很大的缺點(diǎn)衡蚂,所以我們可以參考ButterKnife的做法,打造自己的編譯時(shí)注解框架骏庸。