Android 仿ButterKnife写自己的IOC注解框架

1 前言

一般我们在Android 的开发中,我们在Activity中都会涉及到大量的findViewById操作,这个时候我们可以采用一些开源的框架来帮我们省很多的苦力,例如大名鼎鼎的ButterKnife就是我经常使用的。ButterKnife是一种非常流行的注解框架。主要有以下几个优点:

1.强大的View绑定和Click事件处理功能,简化代码,提升开发效率
2.方便的处理Adapter里的ViewHolder绑定问题
3.运行时不会影响APP效率,使用配置方便(采用的是编译时注解)
4.代码清晰,可读性强

注意事项:
1 必须在setContentView()之后绑定
2 由于采用了编译时注解,一代工程大量使用了之后编译时间会增加不少

关于使用可以参考下面这篇博客

http://blog.csdn.net/itjianghuxiaoxiong/article/details/50177549

下面我们要仿照ButterKnife自己来实现一个IOC注解框架,毕竟我们自己要学会慢慢造轮子。

2 IOC注解框架思路

首先我们要明白,类似于ButterKnife注解框架,它的基本思想就是利用反射区拿到对应的注解值,这个值一般是R.id.xxx。然后根据这个域去设置对应的值,如果是方法就去设置这个View的onClickListener方法。步骤如下:

对于属性注入

Bind(R.id.xxx)
View view

思路如下:
利用反射去 获取Annotation –> value –> findViewById –> 反射注入属性

对于事件注入:

@OnClick(R.id.xxx)  
public void submit(View view) {  
  // TODO submit data to server...  
}  

利用反射去 获取Annotation –> value –> findViewById –> setOnclickListener –> 反射执行该方法

当然,ButterKnifer比这个功能强大很多,但是基本思想是类似的。我们今天就来一个简单的例子,比如我们只思想findViewById和设置点击事件的功能。并且,我们是运行时注解,不是编译时注解,虽然会稍微影响性能,但是这个影响非常非常小,几乎可以忽略不计。

我们的绑定view就叫它ViewById吧,事件就叫OnClick。

3 IOC 属性注入ViewById

首先我们定义一个注解ViewById

package com.qiyei.baselibrary.ioc;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Email: [email protected]
 * Created by qiyei2015 on 2017/5/2.
 * Version: 1.0
 * Description: View注解的Annotation
 */

/**
 * @Target(ElementType.FIELD) 代表Annotation的位置  FIELD属性  TYPE类上  CONSTRUCTOR 构造函数上
 * @Retention(RetentionPolicy.CLASS) 什么时候生效 CLASS编译时   RUNTIME运行时  SOURCE源码资源
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ViewById {
    /**
     * --> @ViewById(R.id.xxx)
     * @return
     */
    int value();
}

这里需要有两点需要注意:一是指定生效的是Filed和RUNTIME,表示我们是需要注解在Field上的,并且在运行时生效。另一个就是定义一个变量int value();

接着,我们来定义注解处理器ViewUtils。

package com.qiyei.baselibrary.ioc;

import android.app.Activity;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.util.Log;
import android.view.View;
import android.widget.Toast;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * Email: [email protected]
 * Created by qiyei2015 on 2017/5/2.
 * Version: 1.0
 * Description:
 */
public class ViewUtils {

    private static final String TAG = ViewUtils.class.getSimpleName();

    /**
     * Activity中使用
     * @param activity
     */
    public static void inject(Activity activity){
        inject(new ViewFinder(activity));
    }

    /**
     * 实际处理者
     * @param finder
     */
    private static void inject(ViewFinder finder){
        injectField(finder);
    }

    /**
     * 注入域
     * @param finder
     */
    private static void injectField(ViewFinder finder){
        //获取类里面所有的属性
        Class clazz = finder.findClass();
        Field[] fields = clazz.getDeclaredFields();
        Log.d(TAG,"name:" + clazz.getSimpleName() + " fields.length:" + fields.length);

        //依次遍历并获取域上的ViewById注解
        for (Field field : fields){
            //获取ViewById注解
            ViewById viewById = field.getAnnotation(ViewById.class);
            if (viewById != null){
                //获取id值,对应R.id.xxx
                int id = viewById.value();
                if (id != 0){
                    View view = finder.finderViewById(id);
                    if (view != null){
                        //能够注入所有修饰符 private public protect
                        field.setAccessible(true);
                        try {
                            //动态的注入找到的View
                            field.set(finder.finderObject(),view);
                        } catch (IllegalAccessException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
}

这里可以看到,我们首先定义了一个方法inject(Activity activity),这个方法我们会调用一个我们的ViewUtils辅助类。ViewFinder主要是辅助我们找到View,Object(反射时调用的对象的),class等

/**
 * Email: [email protected]
 * Created by qiyei2015 on 2017/5/2.
 * Version: 1.0
 * Description: View的findViewById的辅助类
 */
public class ViewFinder {
    /**
     * 所保存的activity实例
     */
    private Activity mActivity;
    /**
     * class实例
     */
    private Class mClass;

    public ViewFinder(Activity activity){
        mActivity = activity;
        mClass = activity.getClass();
    }
    /**
     * 根据id找到对应的View
     * @param id
     * @return
     */
    public View finderViewById(int id){
        return mActivity != null ? mActivity.findViewById(id) : null;
    }

    /**
     * 返回对应的对象
     * @return
     */
    public Object finderObject(){
        if (mActivity != null){
            return mActivity;
        }
        return null;
    }

    /**
     * 返回对应的Class实例
     * @return
     */
    public Class findClass(){
        return mClass;
    }
}

ViewFinder主要有以下几个功能:
1 找到View
2 找到Object
3 找到Object对应的class

整个IOC框架比较重要的点:
1 在反射的时候,我们需要调用getDeclaredFields()获取所有的变量,包括public 和private的
2 在ViewFinder中对mClass赋值一定是activity.getClass()而不是Activity.class。因为我们我要获取的反射对象的类是我们的自定义Activity(继承Activity)。如果传入的是Activity类型的class将会报空指针,不信可以试试。

我们在MainActivty 中代码如下:

public class MainActivity extends AppCompatActivity {

    static {
        System.loadLibrary("native-lib");
    }

    @ViewById(R.id.test_tv1)
    private TextView mTextView1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ViewUtils.inject(this);

        // Example of a call to a native method
        TextView tv = (TextView) findViewById(R.id.sample_text);
        //tv.setText(stringFromJNI());
        tv.setText("");

        mTextView1.setText("这是IOC注解生成的");
    }

    /**
     * A native method that is implemented by the 'native-lib' native library,
     * which is packaged with this application.
     */
    public native String stringFromJNI();
}

因为我建工程的时候选择了支持c++,所以会多了一个c++的提示消息。将它屏蔽了。效果如下:
Android 仿ButterKnife写自己的IOC注解框架_第1张图片

4 IOC 事件注入OnClick

有了前面注入域的例子,我们要实现点击事件的注入也很简单,步骤如下:
1 通过反射拿到所有的方法
2 根据注解OnClick找到这个方法
3 根据id值找到对应的View
4 设置这个View的onClickListener
5 在onClickListener中反射调用这个方法

OnClick注解定义如下:

package com.qiyei.baselibrary.ioc;

/**
 * Email: [email protected]
 * Created by qiyei2015 on 2017/5/3.
 * Version: 1.0
 * Description: 方法上设置点击事件的注解
 */

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @Target(ElementType.FIELD) 代表Annotation的位置  FIELD属性  TYPE类上  CONSTRUCTOR 构造函数上
 * @Retention(RetentionPolicy.CLASS) 什么时候生效 CLASS编译时   RUNTIME运行时  SOURCE源码资源
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OnClick {
    int[] value();
}

因为我们要支持多个id绑定到一个方法上,所以定义了一个数组变量

ViewUtils部分代码如下:

/**
     * 实际处理者
     * @param finder
     */
    private static void inject(ViewFinder finder){
        injectField(finder);
        injectEvent(finder);
    }

    /**
     * 注入事件
     * @param finder
     */
    private static void injectEvent(ViewFinder finder){
        //获取所有的方法
        Class clazz = finder.findClass();
        Method[] methods = clazz.getDeclaredMethods();
        //依次遍历并获取方法的ViewById注解
        for (Method method : methods){
            //获取OnClick注解
            OnClick onClick = method.getAnnotation(OnClick.class);
            if (onClick != null){
                //获取id值,对应R.id.xxx
                int[] ids = onClick.value();
                for (int id : ids){
                    View view = finder.finderViewById(id);
                    if (view != null){
                        view.setOnClickListener(new DeclaredOnClickListener(method,finder.finderObject());
                    }
                }
            }
        }
    }

    /**
     * 设置OnClickListener的监听器
     */
    private static class DeclaredOnClickListener implements View.OnClickListener{
        /**
         * 反射时方法调用的对象
         */
        private Object mObject;
        /**
         * 反射的方法
         */
        private Method mMethod;

        public DeclaredOnClickListener(Method method , Object object){
            mMethod = method;
            mObject = object;
           }

        @Override
        public void onClick(View v) {
            //反射该方法
            try {
                mMethod.setAccessible(true);
                mMethod.invoke(mObject,v);
            } catch (Exception e) {
                e.printStackTrace();
                try {
                    mMethod.invoke(mObject,new  Object[]{});
                } catch (IllegalAccessException e1) {
                    e1.printStackTrace();
                } catch (InvocationTargetException e1) {
                    e1.printStackTrace();
                }
            }
        }
    }
    ....

注意:这里支持了注解方法有参和无参两种类型,也就是说我们的注解方法支持一下两种写法:

    @OnClick(R.id.test_tv1)   
    private void textClick(View view){
        Toast.makeText(this,"点击了:" + view.getId(),Toast.LENGTH_LONG).show();
    }

    @OnClick(R.id.test_tv1)
    private void textClick(){
        Toast.makeText(this,"点击了:",Toast.LENGTH_LONG).show();
    }

接下来我们在MainActivity 中修改如下:

    @OnClick(R.id.test_tv1)
    private void textClick(View view){
        Toast.makeText(this,"点击了:" + view.getId(),Toast.LENGTH_LONG).show();
    }

运行效果如下:
Android 仿ButterKnife写自己的IOC注解框架_第2张图片

5 IOC功能扩展与总结

上面我们已经初步的建立了一个IOC框架的原型,但是还远远不够完善,我们需要再完善一下。

功能扩展
1 支持Fragment与View中使用,因此我们需要增加两个方法,并且需要修改ViewFinder
2 增加监测网络的功能两个,比如有些需求中,没有网络的条件下我们是无法去点击某个按钮的,因此需要在点击事件中增加监测网络的功能
3 增加生成ViewHolder 的功能
4 增加其他事件,例如长按事件,双击事件等

这些功能会逐步的添加到IOC框架中,欢迎访问我的githubhttps://github.com/qiyei2015/EssayJoke

总结:
IOC框架主要是省去了我们去findViewById和设置点击事件。所用的知识点主要是反射和注解。我们可以参照XUtils,ButterKnife等框架去完善它,慢慢的提高我们自己的编程能力

参考
http://www.jianshu.com/p/2570c2de028b
http://blog.csdn.net/itjianghuxiaoxiong/article/details/50177549

你可能感兴趣的:(android,应用开发,android,开源框架)