Android注解框架对比分析
java的注解(annotation)相当于一种标记,在程序中加入注解就等于为程序打上某种标记,标记可以加在包,类,属性,方法,本地变量上。然后你可以写一个注解处理器去解析处理这些注解(人称编译时注解),也可以在程序运行时利用反射得到注解做出相应的处理(人称运行时注解)。
开发android程序时,没完没了的findviewbyid, setonclicklistener等等方法,已经让大多数开发者头疼不已。好在市面上有所谓的注解框架可以帮助开发者简化一些过程。比较流行的有butterknife, annotations, xutils, afinal, roboguice等等。今天我们就来对比一下这些注解框架。
butterknife框架分析
首先看下butterknife,来自jakewharton大神的力作,特点是接入简单,依赖一个库就好了。另外在android studio上还有提供一个插件,自动生成注解与类属性。
butterknife目前支持的注解有: view绑定(bind),资源绑定(bindbool, bindcolor, binddimen, binddrawble, bindint, bindstring),事件绑定(oncheckedchanged, onclick, oneditoraction, onfocuschange, onitemclick, onitemlongclick, onitemselected, onlongclick, onpagechange, ontextchanged, ontouch)。
butterknife的原理是运行时注解。先来看下一个demo。
public class mainactivity extends activity { @bind(r.id.tv1) textview mtv1; @bind(r.id.tv2) textview mtv2; @override protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); setcontentview(r.layout.activity_main); butterknife.bind(this); mtv1.settext("tv1已经得到了控件的索引"); } @onclick(r.id.tv2) public void tv2onclick() { toast.maketext(this, "tv2被点击了", toast.length_short).show(); }
这是一个view绑定的例子,你需要在成员变量上注解需要绑定的控件id,然后再调用butterknife.bind(activity target)方法对带注解的成员变量进行赋值。ok, 看下butterknife.bind()是如何工作的。
/** * bind annotated fields and methods in the specified {@link activity}. the current content * view is used as the view root. * * @param target target activity for view binding. */ public static void bind(activity target) { bind(target, target, finder.activity); }
由上面代码可以看出,最终需要调用bind(object target, object source, finder finder)方法。
static void bind(object target, object source, finder finder) { class<?> targetclass = target.getclass(); try { if (debug) log.d(tag, "looking up view binder for " + targetclass.getname()); viewbinder<object> viewbinder = findviewbinderforclass(targetclass); if (viewbinder != null) { viewbinder.bind(finder, target, source); } } catch (exception e) { throw new runtimeexception("unable to bind views for " + targetclass.getname(), e); } }
这个方法就是寻找或者生成对应的 viewbinder对象,然后调用该对象的bind(finder finder, t target, object source)方法为被注解的变量赋值。先看下findviewbinderforclass(class<?> cls)方法。
private static viewbinder<object> findviewbinderforclass(class<?> cls) throws illegalaccessexception, instantiationexception { 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_prefix) || clsname.startswith(java_prefix)) { if (debug) log.d(tag, "miss: reached framework class. abandoning search."); return nop_view_binder; } try { class<?> viewbindingclass = class.forname(clsname + butterknifeprocessor.suffix); //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()); } binders.put(cls, viewbinder); return viewbinder; }
可以看出是利用反射生成一个viewbinder对象出来,而且还带有缓存,也就是说,同一个class多次调用只会反射一次。反射虽然比原生代码慢一些,但是如果只有一次反射的话,对性能的影响完全可以忽略不计。那现在的问题就是这个反射生成的viewbinder是什么东西, 它的bind(finder finder, t target, object source) 方法是如何为被注解的变量赋值的?
上面说过butterknife框架是编译时注解,一般这类注解会在编译的时候,根据注解标识,动态生成一些类或者生成一些xml都可以,在运行时期,这类注解是没有的~~会依靠动态生成的类做一些操作,因为没有反射,效率和直接调用方法没什么区别~~~。在程序编译时, butterknife会跟据所有的注解生成对应的代码比如说上面的mainactivity类,butterknife会生成
public class mainactivity$viewbinder<t extends mainactivity> implements butterknife.viewbinder<t> { public void bind(butterknife.finder finder, final t target, object source) { target.mtv1 = ((textview)finder.castview((view)finder.findrequiredview(source, 2131492971, "field 'mtv1'"), 2131492971, "field 'mtv1'")); view localview = (view)finder.findrequiredview(source, 2131492972, "field 'mtv2' and method 'tv2onclick'"); target.mtv2 = ((textview)finder.castview(localview, 2131492972, "field 'mtv2'")); localview.setonclicklistener(new debouncingonclicklistener() { public void doclick(view paramanonymousview) { target.tv2onclick(); } }); } public void unbind(t target) { target.mtv1 = null; target.mtv2 = null; } }
可以看出bind注解到最后就是调用生成的代码来findviewbyid然后给其赋值的,事件就是给view设置一个默认的事件,然后里面调用你注解的那个方法。所以在性能上,butterknife并不会影响到app。 butterknife 自动生产的代码也不多,不会对程序的包大小有什么影响。
androidannotations框架分析
再来分析下著名的annotations框架。该框架的原理跟butterknife一样,都是在编译时生成代码,不过annotations并不是生成代码供对应的类调用去给带注解的变量、方法赋值,而是直接生成一个继承带注解的类,这个类里面有对变量赋值,对注解方法调用的代码。运行时,直接运行的是annotations生成的类,而不是我们写的类。说了这么多是不是不太明白,没关系,demo来了!先看下我们写的类。
@eactivity(r.layout.content_main) public class mainactivity extends activity { @viewbyid(r.id.myinput) edittext myinput; @viewbyid(r.id.mytextview) textview textview; @click void mybutton() { string name = myinput.gettext().tostring(); textview.settext("hello "+name); } }
再看下annotations生成的类。
public final class mainactivity_ extends mainactivity implements hasviews, onviewchangedlistener { private final onviewchangednotifier onviewchangednotifier_ = new onviewchangednotifier(); @override public void oncreate(bundle savedinstancestate) { onviewchangednotifier previousnotifier = onviewchangednotifier.replacenotifier(onviewchangednotifier_); init_(savedinstancestate); super.oncreate(savedinstancestate); onviewchangednotifier.replacenotifier(previousnotifier); setcontentview(layout.content_main); } private void init_(bundle savedinstancestate) { onviewchangednotifier.registeronviewchangedlistener(this); } @override public void setcontentview(int layoutresid) { super.setcontentview(layoutresid); onviewchangednotifier_.notifyviewchanged(this); } @override public void setcontentview(view view, layoutparams params) { super.setcontentview(view, params); onviewchangednotifier_.notifyviewchanged(this); } @override public void setcontentview(view view) { super.setcontentview(view); onviewchangednotifier_.notifyviewchanged(this); } public static mainactivity_.intentbuilder_ intent(context context) { return new mainactivity_.intentbuilder_(context); } public static mainactivity_.intentbuilder_ intent(fragment supportfragment) { return new mainactivity_.intentbuilder_(supportfragment); } @override public void onviewchanged(hasviews hasviews) { myinput = ((edittext) hasviews.findviewbyid(id.myinput)); textview = ((textview) hasviews.findviewbyid(id.mytextview)); { view view = hasviews.findviewbyid(id.mybutton); if (view!= null) { view.setonclicklistener(new onclicklistener() { @override public void onclick(view view) { mainactivity_.this.mybutton(); } } ); } } } }
方法调用链:oncreate(bundle saveinstancestate) ----> setcontentview() ----> onviewchangednotifier_.notifyviewchanged(),而onviewchanagednotifier_.notifyviewchanged()方法最终会调用onviewchanged(hasviews hasviews)方法,在此方法中有对变量赋值,事件方法设置的代码,注意看自动生成的类的名字,发现规律了吧,就是我们写的类的名字后面加上一个'_'符号,现在知道为什么用annotations框架,我们的androidmanifest.xml中对activity 的配置,activity的名字要多加一个'_'符号了吧。因为真正加载的是androidannotations生成的代码。写到这里大家发现没,annotations框架里面一个反射都没有,没错这个框架没有用到反射,没有初始化,所有的工作在编译时都做了,不会对我们的程序造成任何速度上的影响。
那annotations支持哪些注解呢?既然annotations性能上跟butterknife差不多,那功能呢?在这里翻译一下官网的features.
1)、依赖注入:注入views, extras, 系统服务,资源,...
2)、简化线程模式:在方法上添加注释来制定该方法是运行在ui线程还是子线程。
3)、事件绑定:在方法上添加注释来制定该方法处理那些views的那个事件。
4)、rest client:创建一个client的接口,androidannotations会生成实现代码,这是关于网络方面的。
5)、清晰明了:androidannotations会在编译时自动生成对应子类,我们可以查看相应的子类来了解程序是怎么运行的。
xutils框架分析
xutils框架是我们现在在用的框架,那我们就来分析一下他的注解功能。xutils的使用方式跟butterknife一样,都是在成员变量,方法上添加注释,然后调用一个方法(xutils是viewutils.inject()方法)对成员变量赋值、事件方法设置到view上。不同的是,butterknife是调用自动生成的代码来赋值,而xutils是通过反射来实现的。ok,拿源码说话。
private static void injectobject(object handler, viewfinder finder) { class<?> handlertype = handler.getclass(); // inject contentview ....... // inject view field[] fields = handlertype.getdeclaredfields(); if (fields != null && fields.length > 0) { for (field field : fields) { viewinject viewinject = field.getannotation(viewinject.class); if (viewinject != null) { try { view view = finder.findviewbyid(viewinject.value(), viewinject.parentid()); if (view != null) { field.setaccessible(true); field.set(handler, view); } } catch (throwable e) { logutils.e(e.getmessage(), e); } } else { resinject resinject = field.getannotation(resinject.class); ...... // 跟viewinject类似 } else { preferenceinject preferenceinject = field.getannotation(preferenceinject.class); ...... // 跟viewinject类似 } } } } // inject event method[] methods = handlertype.getdeclaredmethods(); if (methods != null && methods.length > 0) { for (method method : methods) { annotation[] annotations = method.getdeclaredannotations(); if (annotations != null && annotations.length > 0) { for (annotation annotation : annotations) { class<?> anntype = annotation.annotationtype(); if (anntype.getannotation(eventbase.class) != null) { method.setaccessible(true); try { // proguard:-keep class * extends java.lang.annotation.annotation { *; } method valuemethod = anntype.getdeclaredmethod("value"); method parentidmethod = null; try { parentidmethod = anntype.getdeclaredmethod("parentid"); } catch (throwable e) { } object values = valuemethod.invoke(annotation); object parentids = parentidmethod == null ? null : parentidmethod.invoke(annotation); int parentidslen = parentids == null ? 0 : array.getlength(parentids); int len = array.getlength(values); for (int i = 0; i < len; i++) { viewinjectinfo info = new viewinjectinfo(); info.value = array.get(values, i); info.parentid = parentidslen > i ? (integer) array.get(parentids, i) : 0; eventlistenermanager.addeventmethod(finder, info, annotation, handler, method); } } catch (throwable e) { logutils.e(e.getmessage(), e); } } } } } } }
可以看到反射、反射到处在反射,虽然现在的反射速度也很快了,但是还是不能跟原生代码相比,一旦注释用的多了,这初始化速度会越来越慢。通过上面注释处理的代码可以看出,xutils支持的注释目前主要有ui, 资源,事件,sharedpreference绑定。跟xutils一样是运行时利用反射去解析注释的框架还有afinal, roboguice等。
市面上还有很多其他的注释框架,但是万变不离其宗,不是反射就是自动生成代码。反射功能虽然强大,但是不可取,不仅会拖慢速度还会破话程序的封装性。个人认为生成代码的方案比较好,所有的功能都在编译时做了,并不会影响到用户的体验,唯一的缺点就是比反射难实现,不过我们程序不就是把难处留给自己,把快乐留给用户么!
最后,对上面三种框架总结一下。
上面的难易,强弱,快慢都是相对他们三个自己来说的,比如androidannotations的接入评级是难,并不代表它的接入方式很难,只是相对butterknife和xutils来说比他们难。如果只想使用ui绑定,资源绑定,事件绑定的功能,推荐使用butterknife。以上分析纯属个人观点,仅供参考!
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。