Android中的自定义注解(反射实现-运行时注解)

本文介绍如何使用Java注解简化Android开发过程中的繁琐步骤,包括布局加载、视图绑定及事件处理等,通过示例代码详细解析注解的实现原理。

预备知识:
Java注解基础
Java反射原理
Java动态代理

一、布局文件的注解
我们在Android开发的时候,总是会写到setContentView方法,为了避免每次都写重复的代码,我们需要使用注解来代替我们做这个事情,只需要在类Activity上声明一个ContentView注解和对应的布局文件就可以了。

<code class="hljs java has-numbering"><span class="hljs-annotation">@ContentView</span>(R.layout.activity_main)
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MainActivity</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">AppCompatActivity</span> {</span>
    <span class="hljs-annotation">@Override</span>
    <span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onCreate</span>(Bundle savedInstanceState) {
        <span class="hljs-keyword">super</span>.onCreate(savedInstanceState);
        ViewUtils.injectContentView(<span class="hljs-keyword">this</span>);
    }
}</code>

从上面可以看到,上面代码在MainActivity上面使用到了ContentView注解,下面我们来看看ContentView注解。

<code class="hljs cs has-numbering">@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
<span class="hljs-keyword">public</span> @<span class="hljs-keyword">interface</span> ContentView {
    <span class="hljs-keyword">int</span> <span class="hljs-keyword">value</span>();
}</code>

这个注解很简单,它有一个int的value,用来存放布局文件的id,另外它注解的对象为一个类型,需要说明的是,注解的生命周期会一直到运行时,这个很重要,因为程序是在运行时进行反射的,我们来看看ViewUtils.injectContentView(this)方法,它进行的就是注解的处理,就是进行反射调用setContentView()方法。

<code class="hljs cs has-numbering">    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">injectContentView</span>(Activity activity) {
        Class a = activity.getClass();
        <span class="hljs-keyword">if</span> (a.isAnnotationPresent(ContentView.class)) {
            <span class="hljs-comment">// 得到activity这个类的ContentView注解</span>
            ContentView contentView = (ContentView) a.getAnnotation(ContentView.class);
            <span class="hljs-comment">// 得到注解的值</span>
            <span class="hljs-keyword">int</span> layoutId = contentView.<span class="hljs-keyword">value</span>();
            <span class="hljs-comment">// 使用反射调用setContentView</span>
            <span class="hljs-keyword">try</span> {
                Method method = a.getMethod(<span class="hljs-string">"setContentView"</span>, <span class="hljs-keyword">int</span>.class);
                method.setAccessible(<span class="hljs-keyword">true</span>);
                method.invoke(activity, layoutId);
            } <span class="hljs-keyword">catch</span> (NoSuchMethodException e) {
                e.printStackTrace();
            } <span class="hljs-keyword">catch</span> (IllegalAccessException e) {
                e.printStackTrace();
            } <span class="hljs-keyword">catch</span> (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
    }</code>

如果对Java注解比较熟悉的话,上面代码应该很容易看懂。

二、字段的注解
除了setContentView之外,还有一个也是我们在开发中必须写的代码,就是findViewById,同样,它也属于简单但没有价值的编码,我们也应该使用注解来代替我们做这个事情,就是对字段进行注解。

<code class="hljs java has-numbering"><span class="hljs-annotation">@ContentView</span>(R.layout.activity_main)
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MainActivity</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">AppCompatActivity</span> {</span>
    <span class="hljs-annotation">@ViewInject</span>(R.id.btn1)
    <span class="hljs-keyword">private</span> Button mButton1;
    <span class="hljs-annotation">@ViewInject</span>(R.id.btn2)
    <span class="hljs-keyword">private</span> Button mButton2;

    <span class="hljs-annotation">@Override</span>
    <span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onCreate</span>(Bundle savedInstanceState) {
        <span class="hljs-keyword">super</span>.onCreate(savedInstanceState);
        ViewUtils.injectContentView(<span class="hljs-keyword">this</span>);
        ViewUtils.injectViews(<span class="hljs-keyword">this</span>);
    }
}</code>

上面我们看到,使用ViewInject对两个Button进行了注解,这样我们就是不用写findViewById方法,看上去很神奇,但其实原理很简单。我们先来看看ViewInject注解。

<code class="hljs cs has-numbering">@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
<span class="hljs-keyword">public</span> @<span class="hljs-keyword">interface</span> ViewInject {
    <span class="hljs-keyword">int</span> <span class="hljs-keyword">value</span>();
}</code>

这个注解也很简单,就不说了,重点就是注解的处理了。

<code class="hljs cs has-numbering">    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">injectViews</span>(Activity activity) {
        Class a = activity.getClass();
        <span class="hljs-comment">// 得到activity所有字段</span>
        Field[] fields = a.getDeclaredFields();
        <span class="hljs-comment">// 得到被ViewInject注解的字段</span>
        <span class="hljs-keyword">for</span> (Field field : fields) {
            <span class="hljs-keyword">if</span> (field.isAnnotationPresent(ViewInject.class)) {
                <span class="hljs-comment">// 得到字段的ViewInject注解</span>
                ViewInject viewInject = field.getAnnotation(ViewInject.class);
                <span class="hljs-comment">// 得到注解的值</span>
                <span class="hljs-keyword">int</span> viewId = viewInject.<span class="hljs-keyword">value</span>();
                <span class="hljs-comment">// 使用反射调用findViewById,并为字段设置值</span>
                <span class="hljs-keyword">try</span> {
                    Method method = a.getMethod(<span class="hljs-string">"findViewById"</span>, <span class="hljs-keyword">int</span>.class);
                    method.setAccessible(<span class="hljs-keyword">true</span>);
                    Object resView = method.invoke(activity, viewId);
                    field.setAccessible(<span class="hljs-keyword">true</span>);
                    field.<span class="hljs-keyword">set</span>(activity, resView);
                } <span class="hljs-keyword">catch</span> (NoSuchMethodException e) {
                    e.printStackTrace();
                } <span class="hljs-keyword">catch</span> (InvocationTargetException e) {
                    e.printStackTrace();
                } <span class="hljs-keyword">catch</span> (IllegalAccessException e) {
                    e.printStackTrace();
                }

            }
        }
    }</code>

上面的注释很清楚,使用的也是反射调用findViewById函数。

三、事件的注解
在Android开发中,我们也经常遇到setOnClickListener这样的事件方法。同样我们可以使用注解来减少我们的代码量。

<code class="hljs java has-numbering"><span class="hljs-annotation">@ContentView</span>(R.layout.activity_main)
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MainActivity</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">AppCompatActivity</span> {</span>
    <span class="hljs-annotation">@Override</span>
    <span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onCreate</span>(Bundle savedInstanceState) {
        <span class="hljs-keyword">super</span>.onCreate(savedInstanceState);
        ViewUtils.injectContentView(<span class="hljs-keyword">this</span>);
        ViewUtils.injectEvents(<span class="hljs-keyword">this</span>);
    }

    <span class="hljs-annotation">@OnClick</span>({R.id.btn1, R.id.btn2})
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">clickBtnInvoked</span>(View view) {
        <span class="hljs-keyword">switch</span> (view.getId()) {
            <span class="hljs-keyword">case</span> R.id.btn1:
                Toast.makeText(<span class="hljs-keyword">this</span>, <span class="hljs-string">"Button1 OnClick"</span>, Toast.LENGTH_SHORT).show();
                <span class="hljs-keyword">break</span>;
            <span class="hljs-keyword">case</span> R.id.btn2:
                Toast.makeText(<span class="hljs-keyword">this</span>, <span class="hljs-string">"Button2 OnClick"</span>, Toast.LENGTH_SHORT).show();
                <span class="hljs-keyword">break</span>;
        }
    }
}</code>

布局文件如下:

<code class="hljs xml has-numbering"><span class="hljs-pi"><?xml version="1.0" encoding="utf-8"?></span>
<span class="hljs-tag"><<span class="hljs-title">LinearLayout</span> <span class="hljs-attribute">xmlns:android</span>=<span class="hljs-value">"http://schemas.android.com/apk/res/android"</span>
    <span class="hljs-attribute">xmlns:tools</span>=<span class="hljs-value">"http://schemas.android.com/tools"</span>
    <span class="hljs-attribute">android:layout_width</span>=<span class="hljs-value">"match_parent"</span>
    <span class="hljs-attribute">android:layout_height</span>=<span class="hljs-value">"match_parent"</span>
    <span class="hljs-attribute">android:paddingBottom</span>=<span class="hljs-value">"@dimen/activity_vertical_margin"</span>
    <span class="hljs-attribute">android:paddingLeft</span>=<span class="hljs-value">"@dimen/activity_horizontal_margin"</span>
    <span class="hljs-attribute">android:paddingRight</span>=<span class="hljs-value">"@dimen/activity_horizontal_margin"</span>
    <span class="hljs-attribute">android:paddingTop</span>=<span class="hljs-value">"@dimen/activity_vertical_margin"</span>
    <span class="hljs-attribute">android:background</span>=<span class="hljs-value">"#70DBDB"</span>
    <span class="hljs-attribute">android:orientation</span>=<span class="hljs-value">"vertical"</span>
    <span class="hljs-attribute">tools:context</span>=<span class="hljs-value">"statusbartest.hpp.cn.statusbartest.MainActivity"</span>></span>
    <span class="hljs-tag"><<span class="hljs-title">Button
</span>        <span class="hljs-attribute">android:id</span>=<span class="hljs-value">"@+id/btn1"</span>
        <span class="hljs-attribute">android:layout_width</span>=<span class="hljs-value">"wrap_content"</span>
        <span class="hljs-attribute">android:layout_height</span>=<span class="hljs-value">"wrap_content"</span>
        <span class="hljs-attribute">android:text</span>=<span class="hljs-value">"Test1"</span>/></span>

    <span class="hljs-tag"><<span class="hljs-title">Button
</span>        <span class="hljs-attribute">android:id</span>=<span class="hljs-value">"@+id/btn2"</span>
        <span class="hljs-attribute">android:layout_width</span>=<span class="hljs-value">"wrap_content"</span>
        <span class="hljs-attribute">android:layout_height</span>=<span class="hljs-value">"wrap_content"</span>
        <span class="hljs-attribute">android:text</span>=<span class="hljs-value">"Test2"</span>/></span>
<span class="hljs-tag"></<span class="hljs-title">LinearLayout</span>></span></code>

可以看到,上面我们没有对Button调用setOnClickListener,但是当我们点击按钮的时候,就会回调clickBtnInvoked方法,这里我们使用的就是注解来处理的。下面先来看看OnClick注解。

<code class="hljs java has-numbering"><span class="hljs-annotation">@Target</span>(ElementType.METHOD)
<span class="hljs-annotation">@Retention</span>(RetentionPolicy.RUNTIME)
<span class="hljs-annotation">@EventBase</span>(listenerType = View.OnClickListener.class, listenerSetter = <span class="hljs-string">"setOnClickListener"</span>, methodName = <span class="hljs-string">"onClick"</span>)
<span class="hljs-keyword">public</span> @<span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">OnClick</span> {</span>
    <span class="hljs-keyword">int</span>[] value();
}</code>

可以看到这个注解使用了一个自定义的注解。

<code class="hljs java has-numbering"><span class="hljs-annotation">@Target</span>(ElementType.ANNOTATION_TYPE)
<span class="hljs-annotation">@Retention</span>(RetentionPolicy.RUNTIME)
<span class="hljs-keyword">public</span> @<span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">EventBase</span> {</span>
    Class listenerType();
    String listenerSetter();
    String methodName();
}</code>

下面来看看注解的处理。

<code class="hljs d has-numbering">    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> injectEvents(Activity activity) {
        Class a = activity.getClass();
        <span class="hljs-comment">// 得到Activity的所有方法</span>
        Method[] methods = a.getDeclaredMethods();
        <span class="hljs-keyword">for</span> (Method method : methods) {
            <span class="hljs-comment">// 得到被OnClick注解的方法</span>
            <span class="hljs-keyword">if</span> (method.isAnnotationPresent(OnClick.<span class="hljs-keyword">class</span>)) {
                <span class="hljs-comment">// 得到该方法的OnClick注解</span>
                OnClick onClick = method.getAnnotation(OnClick.<span class="hljs-keyword">class</span>);
                <span class="hljs-comment">// 得到OnClick注解的值</span>
                <span class="hljs-keyword">int</span>[] viewIds = onClick.value();
                <span class="hljs-comment">// 得到OnClick注解上的EventBase注解</span>
                EventBase eventBase = onClick.annotationType().getAnnotation(EventBase.<span class="hljs-keyword">class</span>);
                <span class="hljs-comment">// 得到EventBase注解的值</span>
                String listenerSetter = eventBase.listenerSetter();
                Class<?> listenerType = eventBase.listenerType();
                String methodName = eventBase.methodName();
                <span class="hljs-comment">// 使用动态代理</span>
                DynamicHandler handler = <span class="hljs-keyword">new</span> DynamicHandler(activity);
                Object listener = Proxy.newProxyInstance(listenerType.getClassLoader(), <span class="hljs-keyword">new</span> Class<?>[] { listenerType }, handler);
                handler.addMethod(methodName, method);
                <span class="hljs-comment">// 为每个view设置点击事件</span>
                <span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> viewId : viewIds) {
                    <span class="hljs-keyword">try</span> {
                        Method findViewByIdMethod = a.getMethod(<span class="hljs-string">"findViewById"</span>, <span class="hljs-keyword">int</span>.<span class="hljs-keyword">class</span>);
                        findViewByIdMethod.setAccessible(<span class="hljs-literal">true</span>);
                        View view  = (View) findViewByIdMethod.invoke(activity, viewId);
                        Method setEventListenerMethod = view.getClass().getMethod(listenerSetter, listenerType);
                        setEventListenerMethod.setAccessible(<span class="hljs-literal">true</span>);
                        setEventListenerMethod.invoke(view, listener);
                    } <span class="hljs-keyword">catch</span> (NoSuchMethodException e) {
                        e.printStackTrace();
                    } <span class="hljs-keyword">catch</span> (InvocationTargetException e) {
                        e.printStackTrace();
                    } <span class="hljs-keyword">catch</span> (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }

            }

        }
    }</code>

这个代码相对上面的要复杂一些,它使用到了动态代理,关于动态代理的基本用法可以看看前面我提到的预备知识。

<code class="hljs java has-numbering"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">DynamicHandler</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">InvocationHandler</span> {</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> HashMap<String, Method> methodMap = <span class="hljs-keyword">new</span> HashMap<String, Method>(
            <span class="hljs-number">1</span>);
    <span class="hljs-comment">// 因为传进来的为activity,使用弱引用主要是为了防止内存泄漏</span>
    <span class="hljs-keyword">private</span> WeakReference<Object> handlerRef;
    <span class="hljs-keyword">public</span> <span class="hljs-title">DynamicHandler</span>(Object object) {
        <span class="hljs-keyword">this</span>.handlerRef = <span class="hljs-keyword">new</span> WeakReference<Object>(object);
    }

    <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">addMethod</span>(String name, Method method) {
        methodMap.put(name, method);
    }
    <span class="hljs-comment">// 当回到OnClickListener的OnClick方法的时候,它会调用这里的invoke方法</span>
    <span class="hljs-annotation">@Override</span>
    <span class="hljs-keyword">public</span> Object <span class="hljs-title">invoke</span>(Object o, Method method, Object[] objects) <span class="hljs-keyword">throws</span> Throwable {
        <span class="hljs-comment">// 得到activity实例</span>
        Object handler = handlerRef.get();
        <span class="hljs-keyword">if</span> (handler != <span class="hljs-keyword">null</span>) {
            <span class="hljs-comment">// method对应的就是回调方法OnClick,得到方法名</span>
            String methodName = method.getName();
            <span class="hljs-comment">// 得到activtiy里面的clickBtnInvoked方法</span>
            method = methodMap.get(methodName);
            <span class="hljs-keyword">if</span> (method != <span class="hljs-keyword">null</span>) {
                <span class="hljs-comment">// 回调clickBtnInvoked方法</span>
                <span class="hljs-keyword">return</span> method.invoke(handler, objects);
            }
        }
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
    }
}</code>

基本的看注释就应该差不多了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值