Android 中的注解深入探究
本文系gdg android meetup分享内容总结文章
注解是我们经常接触的技术,java有注解,android也有注解,本文将试图介绍android中的注解,以及butterknife和otto这些基于注解的库的一些工作原理.
归纳而言,android中的注解大概有以下好处
- 提高我们的开发效率
- 更早的发现程序的问题或者错误
- 更好的增加代码的描述能力
- 更加利于我们的一些规范约束
- 提供解决问题的更优解
准备工作
默认情况下,android中的注解包并没有包括在framework中,它独立成一个单独的包,通常我们需要引入这个包.
dependencies { compile 'com.android.support:support-annotations:22.2.0' }
但是如果我们已经引入了appcompat则没有必要再次引用support-annotations,因为appcompat默认包含了对其引用.
替代枚举
在最早的时候,当我们想要做一些值得限定实现枚举的效果,通常是
- 定义几个常量用于限定
- 从上面的常量选取值进行使用
- 一个比较描述上面问题的示例代码如下
public static final int color_red = 0; public static final int color_green = 1; public static final int color_yellow = 2; public void setcolor(int color) { //some code here } //调用 setcolor(color_red)
然而上面的还是有不尽完美的地方
setcolor(color_red)与setcolor(0)效果一样,而后者可读性很差,但却可以正常运行
setcolor方法可以接受枚举之外的值,比如setcolor(3),这种情况下程序可能出问题
一个相对较优的解决方法就是使用java中的enum.使用枚举实现的效果如下
// colorenum.java public enum coloremun { red, green, yellow } public void setcolorenum(coloremun colorenum) { //some code here } setcolorenum(coloremun.green);
然而enum也并非最佳,enum因为其相比方案一的常量来说,占用内存相对大很多而受到曾经被google列为不建议使用,为此google特意引入了一些相关的注解来替代枚举.
android中新引入的替代枚举的注解有intdef和stringdef,这里以intdef做例子说明一下.
public class colors { @intdef({red, green, yellow}) @retention(retentionpolicy.source) public @interface lightcolors{} public static final int red = 0; public static final int green = 1; public static final int yellow = 2; }
- 声明必要的int常量
- 声明一个注解为lightcolors
- 使用@intdef修饰lightcolors,参数设置为待枚举的集合
- 使用@retention(retentionpolicy.source)指定注解仅存在与源码中,不加入到class文件中
null相关的注解
和null相关的注解有两个
@nullable 注解的元素可以是null
@nonnull 注解的元素不能是null
上面的两个可以修饰如下的元素
成员属性
方法参数
方法的返回值
@nullable private string obtainreferrerfromintent(@nonnull intent intent) { return intent.getstringextra("apps_referrer"); }
nonnull检测生效的条件
显式传入null
在调用方法之前已经判断了参数为null时
setreferrer(null);//提示警告 //不提示警告 string referrer = getintent().getstringextra("apps_referrer"); setreferrer(referrer); //提示警告 string referrer = getintent().getstringextra("apps_referrer"); if (referrer == null) { setreferrer(referrer); } private void setreferrer(@nonnull string referrer) { //some code here }
区间范围注解
android中的intrange和floatrange是两个用来限定区间范围的注解,
float currentprogress; public void setcurrentprogress(@floatrange(from=0.0f, to=1.0f) float progress) { currentprogress = progress; }
如果我们传入非法的值,如下所示
setcurrentprogress(11);
就会得到这样的错误
value must be >=0.0 and <= 1.0(was 11)
长度以及数组大小限制
限制字符串的长度
private void setkey(@size(6) string key) {
}
限定数组集合的大小
private void setdata(@size(max = 1) string[] data) { } setdata(new string[]{"b", "a"});//error occurs
限定特殊的数组长度,比如3的倍数
private void setitemdata(@size(multiple = 3) string[] data) {
}
权限相关
在android中,有很多场景都需要使用权限,无论是marshmallow之前还是之后的动态权限管理.都需要在manifest中进行声明,如果忘记了,则会导致程序崩溃. 好在有一个注解能辅助我们避免这个问题.使用requirespermission注解即可.
@requirespermission(manifest.permission.set_wallpaper) public void changewallpaper(bitmap bitmap) throws ioexception { }
资源注解
在android中几乎所有的资源都可以有对应的资源id.比如获取定义的字符串,我们可以通过下面的方法
public string getstringbyid(int stringresid) { return getresources().getstring(stringresid); }
使用这个方法,我们可以很容易的获取到定义的字符串,但是这样的写法也存在着风险.
getstringbyid(r.mipmap.ic_launcher)
如果我们在不知情或者疏忽情况下,传入这样的值,就会出现问题. 但是如果我们使用资源相关的注解修饰了参数,就能很大程度上避免错误的情况.
public string getstringbyid(@stringres int stringresid) { return getresources().getstring(stringresid); }
在android中资源注解如下所示
- animres
- animatorres
- anyres
- arrayres
- attrres
- boolres
- colorres
- dimenres
- drawableres
- fractionres
- idres
- integerres
- interpolatorres
- layoutres
- menures
- pluralsres
- rawres
- stringres
- styleres
- styleableres
- transitionres
- xmlres
color值限定
上面部分提到了colorres,用来限定颜色资源id,这里我们将使用colorint,一个用来限定color值的注解. 在较早的textview的settextcolor是这样实现的.
public void settextcolor(int color) { mtextcolor = colorstatelist.valueof(color); updatetextcolors(); }
然而上面的方法在调用时常常会出现这种情况
mytextview.settextcolor(r.color.coloraccent);
如上,如果传递过去的参数为color的资源id就会出现颜色取错误的问题,这个问题在过去还是比较严重的.好在colorint出现了,改变了这一问题.
public void settextcolor(@colorint int color) { mtextcolor = colorstatelist.valueof(color); updatetextcolors(); }
当我们再次传入color资源值时,就会得到错误的提示.
checkresult
这是一个关于返回结果的注解,用来注解方法,如果一个方法得到了结果,却没有使用这个结果,就会有错误出现,一旦出现这种错误,就说明你没有正确使用该方法。
@checkresult public string trim(string s) { return s.trim(); }
线程相关
android中提供了四个与线程相关的注解
- @uithread,通常可以等同于主线程,标注方法需要在uithread执行,比如view类就使用这个注解
- @mainthread 主线程,经常启动后创建的第一个线程
- @workerthread 工作者线程,一般为一些后台的线程,比如asynctask里面的doinbackground就是这样的.
- @binderthread 注解方法必须要在binderthread线程中执行,一般使用较少.
一些示例
new asynctask<void, void, void>() { //doinbackground is already annotated with @workerthread @override protected void doinbackground(void... params) { return null; updateviews();//error } }; @uithread public void updateviews() { log.i(logtag, "updateviews threadinfo=" + thread.currentthread()); }
注意,这种情况下不会出现错误提示
new thread(){ @override public void run() { super.run(); updateviews(); } }.start();
虽然updateviews会在一个新的工作者线程中执行,但是在compile时没有错误提示.
因为它的判断依据是,如果updateview的线程注解(这里为@uithread)和run(没有线程注解)不一致才会错误提示.如果run方法没有线程注解,则不提示.
callsuper
重写的方法必须要调用super方法
使用这个注解,我们可以强制方法在重写时必须调用父类的方法 比如application的oncreate,onconfigurationchanged等.
keep
在android编译生成apk的环节,我们通常需要设置minifyenabled为true实现下面的两个效果
混淆代码
删除没有用的代码
但是出于某一些目的,我们需要不混淆某部分代码或者不删除某处代码,除了配置复杂的proguard文件之外,我们还可以使用@keep注解 .
@keep public static int getbitmapwidth(bitmap bitmap) { return bitmap.getwidth(); }
butterknife
butterknife是一个用来绑定view,资源和回调的提高效率的工具.作者为jake wharton. butterknife的好处
- 使用bindview替代繁琐的findviewbyid和类型转换
- 使用onclick注解方法来替换显式声明的匿名内部类
- 使用bindstring,bindbool,binddrawable等注解实现资源获取
一个摘自github的示例
class exampleactivity extends activity { @bindview(r.id.user) edittext username; @bindview(r.id.pass) edittext password; @bindstring(r.string.login_error) string loginerrormessage; @onclick(r.id.submit) void submit() { // todo call server... } @override public void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); setcontentview(r.layout.simple_activity); butterknife.bind(this); // todo use fields... } }
butterknife工作原理
以bindview注解使用为例,示例代码为
public class mainactivity extends appcompatactivity { @bindview(r.id.mytextview) textview mytextview; @override protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); setcontentview(r.layout.activity_main); butterknife.bind(this); } }
1.程序在compile时,会根据注解自动生成两个类,这里为mainactivity_viewbinder.class和mainactivity_viewbinding.class
2.当我们调用butterknife.bind(this);时,会查找当前类对应的viewbinder类,并调用bind方法,这里会调用到mainactiivty_viewbinder.bind方法.
3.mainactiivty_viewbinder.bind方法实际上是调用了findviewbyid然后在进行类型转换,赋值给mainactivity的mytextview属性
butterknife的bind方法
public static unbinder bind(@nonnull activity target) { return getviewbinder(target).bind(finder.activity, target, target); }
butterknife的getviewbinder和findviewbinderforclass
@nonnull @checkresult @uithread static viewbinder<object> getviewbinder(@nonnull object target) { class<?> targetclass = target.getclass(); if (debug) log.d(tag, "looking up view binder for " + targetclass.getname()); return findviewbinderforclass(targetclass); } @nonnull @checkresult @uithread private static viewbinder<object> findviewbinderforclass(class<?> cls) { //如果内存集合binders中包含,则不再查找 viewbinder<object> viewbinder = binders.get(cls); if (viewbinder != null) { if (debug) log.d(tag, "hit: cached in view binder map."); return viewbinder; } string clsname = cls.getname(); if (clsname.startswith("android.") || clsname.startswith("java.")) { if (debug) log.d(tag, "miss: reached framework class. abandoning search."); return nop_view_binder; } //noinspection trywithidenticalcatches resolves to api 19+ only type. try { //使用反射创建实例 class<?> viewbindingclass = class.forname(clsname + "_viewbinder"); //noinspection unchecked viewbinder = (viewbinder<object>) viewbindingclass.newinstance(); if (debug) log.d(tag, "hit: loaded view binder class."); } catch (classnotfoundexception e) { //如果没有找到,对父类进行查找 if (debug) log.d(tag, "not found. trying superclass " + cls.getsuperclass().getname()); viewbinder = findviewbinderforclass(cls.getsuperclass()); } catch (instantiationexception e) { throw new runtimeexception("unable to create view binder for " + clsname, e); } catch (illegalaccessexception e) { throw new runtimeexception("unable to create view binder for " + clsname, e); } //加入内存集合,便于后续的查找 binders.put(cls, viewbinder); return viewbinder; }
mainactivity_viewbinder的反编译源码
➜ androidannotationsample javap -c mainactivity_viewbinder warning: binary file mainactivity_viewbinder contains com.example.admin.androidannotationsample.mainactivity_viewbinder compiled from "mainactivity_viewbinder.java" public final class com.example.admin.androidannotationsample.mainactivity_viewbinder implements butterknife.internal.viewbinder<com.example.admin.androidannotationsample.mainactivity> { public com.example.admin.androidannotationsample.mainactivity_viewbinder(); code: 0: aload_0 1: invokespecial #1 // method java/lang/object."<init>":()v 4: return public butterknife.unbinder bind(butterknife.internal.finder, com.example.admin.androidannotationsample.mainactivity, java.lang.object); code: 0: new #2 // class com/example/admin/androidannotationsample/mainactivity_viewbinding 3: dup 4: aload_2 5: aload_1 6: aload_3 // 创建viewbinding实例 7: invokespecial #3 // method com/example/admin/androidannotationsample/mainactivity_viewbinding."<init>":(lcom/example/admin/androidannotationsample/mainactivity;lbutterknife/internal/finder;ljava/lang/object;)v 10: areturn public butterknife.unbinder bind(butterknife.internal.finder, java.lang.object, java.lang.object); code: 0: aload_0 1: aload_1 2: aload_2 3: checkcast #4 // class com/example/admin/androidannotationsample/mainactivity 6: aload_3 //调用上面的重载方法 7: invokevirtual #5 // method bind:(lbutterknife/internal/finder;lcom/example/admin/androidannotationsample/mainactivity;ljava/lang/object;)lbutterknife/unbinder; 10: areturn } mainactivity_viewbinding的反编译源码 ➜ androidannotationsample javap -c mainactivity_viewbinding warning: binary file mainactivity_viewbinding contains com.example.admin.androidannotationsample.mainactivity_viewbinding compiled from "mainactivity_viewbinding.java" public class com.example.admin.androidannotationsample.mainactivity_viewbinding<t extends com.example.admin.androidannotationsample.mainactivity> implements butterknife.unbinder { protected t target; public com.example.admin.androidannotationsample.mainactivity_viewbinding(t, butterknife.internal.finder, java.lang.object); code: 0: aload_0 1: invokespecial #1 // method java/lang/object."<init>":()v 4: aload_0 5: aload_1 6: putfield #2 // field target:lcom/example/admin/androidannotationsample/mainactivity; 9: aload_1 10: aload_2 11: aload_3 //调用finder.findrequireviewastype找到view,并进行类型转换,并复制给mainactivity中对一个的变量 12: ldc #4 // int 2131427412 14: ldc #5 // string field 'mytextview' 16: ldc #6 // class android/widget/textview // 内部实际调用了findviewbyid 18: invokevirtual #7 // method butterknife/internal/finder.findrequiredviewastype:(ljava/lang/object;iljava/lang/string;ljava/lang/class;)ljava/lang/object; 21: checkcast #6 // class android/widget/textview 24: putfield #8 // field com/example/admin/androidannotationsample/mainactivity.mytextview:landroid/widget/textview; 27: return public void unbind(); code: 0: aload_0 1: getfield #2 // field target:lcom/example/admin/androidannotationsample/mainactivity; 4: astore_1 5: aload_1 6: ifnonnull 19 9: new #9 // class java/lang/illegalstateexception 12: dup 13: ldc #10 // string bindings already cleared. 15: invokespecial #11 // method java/lang/illegalstateexception."<init>":(ljava/lang/string;)v 18: athrow 19: aload_1 20: aconst_null // 解除绑定,设置对应的变量为null 21: putfield #8 // field com/example/admin/androidannotationsample/mainactivity.mytextview:landroid/widget/textview; 24: aload_0 25: aconst_null 26: putfield #2 // field target:lcom/example/admin/androidannotationsample/mainactivity; 29: return }
finder的源码
package butterknife.internal; import android.app.activity; import android.app.dialog; import android.content.context; import android.support.annotation.idres; import android.view.view; @suppresswarnings("unuseddeclaration") // used by generated code. public enum finder { view { @override public view findoptionalview(object source, @idres int id) { return ((view) source).findviewbyid(id); } @override public context getcontext(object source) { return ((view) source).getcontext(); } @override protected string getresourceentryname(object source, @idres int id) { final view view = (view) source; // in edit mode, getresourceentryname() is unsupported due to use of bridgeresources if (view.isineditmode()) { return "<unavailable while editing>"; } return super.getresourceentryname(source, id); } }, activity { @override public view findoptionalview(object source, @idres int id) { return ((activity) source).findviewbyid(id); } @override public context getcontext(object source) { return (activity) source; } }, dialog { @override public view findoptionalview(object source, @idres int id) { return ((dialog) source).findviewbyid(id); } @override public context getcontext(object source) { return ((dialog) source).getcontext(); } }; //查找对应的finder,如上面的activity, dialog, view public abstract view findoptionalview(object source, @idres int id); public final <t> t findoptionalviewastype(object source, @idres int id, string who, class<t> cls) { view view = findoptionalview(source, id); return castview(view, id, who, cls); } public final view findrequiredview(object source, @idres int id, string who) { view view = findoptionalview(source, id); if (view != null) { return view; } string name = getresourceentryname(source, id); throw new illegalstateexception("required view '" + name + "' with id " + id + " for " + who + " was not found. if this view is optional add '@nullable' (fields) or '@optional'" + " (methods) annotation."); } //来自viewbinding的调用 public final <t> t findrequiredviewastype(object source, @idres int id, string who, class<t> cls) { view view = findrequiredview(source, id, who); return castview(view, id, who, cls); } public final <t> t castview(view view, @idres int id, string who, class<t> cls) { try { return cls.cast(view); } catch (classcastexception e) { string name = getresourceentryname(view, id); throw new illegalstateexception("view '" + name + "' with id " + id + " for " + who + " was of the wrong type. see cause for more info.", e); } } @suppresswarnings("unchecked") // that's the point. public final <t> t castparam(object value, string from, int frompos, string to, int topos) { try { return (t) value; } catch (classcastexception e) { throw new illegalstateexception("parameter #" + (frompos + 1) + " of method '" + from + "' was of the wrong type for parameter #" + (topos + 1) + " of method '" + to + "'. see cause for more info.", e); } } protected string getresourceentryname(object source, @idres int id) { return getcontext(source).getresources().getresourceentryname(id); } public abstract context getcontext(object source); }
otto
otto bus 是一个专为android改装的event bus,在很多项目中都有应用.由square开源共享. public class eventbustest { private static final string logtag = "eventbustest"; bus mbus = new bus(); public void test() { mbus.register(this); } class networkchangedevent { } @produce public networkchangedevent sendnetworkchangedevent() { return new networkchangedevent(); } @subscribe public void onnetworkchanged(networkchangedevent event) { log.i(logtag, "onnetworkchanged event=" + event); } }
otto 的工作原理
- 使用@produce和@subscribe标记方法
- 当调用bus.register方法,去检索注册对象的标记方法,并cache映射关系
- 当post事件时,将事件与handler方法对应加入事件队列
- 抽取事件队列,然后调用handler处理
如下为对otto如何利用注解的分析
register的源码
public void register(object object) { if (object == null) { throw new nullpointerexception("object to register must not be null."); } enforcer.enforce(this); //查找object中的subscriber map<class<?>, set<eventhandler>> foundhandlersmap = handlerfinder.findallsubscribers(object); for (class<?> type : foundhandlersmap.keyset()) { set<eventhandler> handlers = handlersbytype.get(type); if (handlers == null) { //concurrent put if absent set<eventhandler> handlerscreation = new copyonwritearrayset<eventhandler>(); handlers = handlersbytype.putifabsent(type, handlerscreation); if (handlers == null) { handlers = handlerscreation; } } final set<eventhandler> foundhandlers = foundhandlersmap.get(type); if (!handlers.addall(foundhandlers)) { throw new illegalargumentexception("object already registered."); } } for (map.entry<class<?>, set<eventhandler>> entry : foundhandlersmap.entryset()) { class<?> type = entry.getkey(); eventproducer producer = producersbytype.get(type); if (producer != null && producer.isvalid()) { set<eventhandler> foundhandlers = entry.getvalue(); for (eventhandler foundhandler : foundhandlers) { if (!producer.isvalid()) { break; } if (foundhandler.isvalid()) { dispatchproducerresulttohandler(foundhandler, producer); } } } } }
handlerfinder源码
interface handlerfinder { map<class<?>, eventproducer> findallproducers(object listener); map<class<?>, set<eventhandler>> findallsubscribers(object listener); //otto注解查找器 handlerfinder annotated = new handlerfinder() { @override public map<class<?>, eventproducer> findallproducers(object listener) { return annotatedhandlerfinder.findallproducers(listener); } @override public map<class<?>, set<eventhandler>> findallsubscribers(object listener) { return annotatedhandlerfinder.findallsubscribers(listener); } };
具体查找实现
/** this implementation finds all methods marked with a {@link subscribe} annotation. */ static map<class<?>, set<eventhandler>> findallsubscribers(object listener) { class<?> listenerclass = listener.getclass(); map<class<?>, set<eventhandler>> handlersinmethod = new hashmap<class<?>, set<eventhandler>>(); map<class<?>, set<method>> methods = subscribers_cache.get(listenerclass); if (null == methods) { methods = new hashmap<class<?>, set<method>>(); loadannotatedsubscribermethods(listenerclass, methods); } if (!methods.isempty()) { for (map.entry<class<?>, set<method>> e : methods.entryset()) { set<eventhandler> handlers = new hashset<eventhandler>(); for (method m : e.getvalue()) { handlers.add(new eventhandler(listener, m)); } handlersinmethod.put(e.getkey(), handlers); } } return handlersinmethod; }
以上就是关于android中注解的一些总结,文章部分内容参考自 support annotations ,希望能帮助大家对注解有基础的认识,并运用到实际的日常开发之中。
通过此文希望能帮助你彻底了解android 注解机制,谢谢大家对本站的支持!
下一篇: Android高仿微信支付密码输入控件
推荐阅读
-
Android 中的注解深入探究
-
Android ListView中动态显示和隐藏Header&Footer的方法
-
深入探究Java多线程并发编程的要点
-
Android系统进程间通信(IPC)机制Binder中的Client获得Server远程接口过程源代码分析
-
Android shell命令行中过滤adb logcat输出的方法
-
Android系统进程间通信(IPC)机制Binder中的Server启动过程源代码分析
-
深入浅析Java中的final关键字
-
详解Java的Hibernate框架中的注解与缓存
-
Android MVP模式ListView中嵌入checkBox的使用方法
-
Android中BaseAdapter的用法分析与理解