自己动手打造一套IOC注解框架 - (ButterKnife源码阅读)

1. 说明


这节课我们自己动手打造一套IOC注解框架,接下来我们首先看下ButterKnife源码的阅读和使用。

2. ButterKnife 源码阅读和使用


/**
 * Email: [email protected]
 * Created by JackChen 2018/3/28 9:11
 * Version 1.0
 * Params:
 * Description:  View 注解的 Annotation
*/
@Target(ElementType.CONSTRUCTOR)  // Target表示作用在哪里 FIELD:属性  TYPE:类  CONSTRUCTOR:构造方法
@Retention(RetentionPolicy.SOURCE) // Retention表示什么时间生效  RUNTIME:运行时  CLASS:编译时  SOURCE:源码资源
public @interface ViewById {

    // 指的是  --> ViewById(R.id.test_tv)
    int value() ;
}

流程就是:

  1. 在编译的时候,用ButterKnifeProcessor 生成 .java文件 ,然后生成class文件,会把class文件打包到apk里边;
  2. 在运行时候,调用 viewBinder.bind(finder , target , source) 方法

3. 自己手写一个IOC注解框架


在这里特别说明下,之所以要自己手写一个IOC注解框架,其实不是重复造轮子,一方面是可以熟悉下反射加注解的写法,同时也可以在自己IOC注解框架中去拓展一些其他的功能,比如检测网络功能等,还有一个就是如果我们直接使用系统的findViewById()找到控件、 onClick点击事件、检测网络等这些东西的话,可能需要在每一个类中都去findViewById,每一个类中都需要去onClick点击事件、每一个类中都需要去检测网络等等,所以基于这些东西,我们才去手写一个 IOC注解框架,可以根据自己需求去扩展,同时也可以直接用到自己项目中,这些也都是可以的。

3.1:View 注解的 Annotation 用于在类中 @ViewById(R.id.test_tv) 这样用

/**
 * Email: [email protected]
 * Created by JackChen 2018/3/28 9:11
 * Version 1.0
 * Params:
 * Description:  View 注解的 Annotation   用于在类中 @ViewById(R.id.test_tv) 这样用
*/

@Target(ElementType.FIELD)  // Target表示作用在哪里 FIELD:属性  TYPE:类  CONSTRUCTOR:构造方法
@Retention(RetentionPolicy.RUNTIME) // Retention表示什么时间生效  RUNTIME:运行时  CLASS:编译时  SOURCE:源码资源
public @interface ViewById {

    // 指的是  --> ViewById(R.id.test_tv)
    int value() ;
}

3.2: View事件 注解的 Annotation 用于在类中 @OnClick({R.id.test_tv , R.id.test_iv}) 这样用

/**
 * Email: [email protected]
 * Created by JackChen 2018/3/28 9:17
 * Version 1.0
 * Params:
 * Description:  View事件  注解的 Annotation  用于在类中 @OnClick({R.id.test_tv , R.id.test_iv})  这样用
*/
@Target(ElementType.METHOD)  // 代表注解的位置  FIELD:属性  TYPE:类  CONSTRUCTOR:构造方法  METHOD:方法
@Retention(RetentionPolicy.RUNTIME)  // 代表什么时间生效  RUNTIME:运行时  CLASS:编译时  SOURCE:源码资源
public @interface OnClick {

    // {R.id.test_tv , R.id.test_iv}
    int[] value() ;
}

3.3: 检测网络注解的Annotation 用于在类中 @CheckNet 这样用

/**
 * Email: [email protected]
 * Created by JackChen 2018/3/28 9:11
 * Version 1.0
 * Params:
 * Description:  检测网络注解的Annotation   用于在类中 @CheckNet 这样用
*/

@Target(ElementType.METHOD)  // Target表示作用在哪里 FIELD:属性  TYPE:类  CONSTRUCTOR:构造方法
@Retention(RetentionPolicy.RUNTIME) // Retention表示什么时间生效  RUNTIME:运行时  CLASS:编译时  SOURCE:源码资源
public @interface CheckNet {

}

3.4: ViewUtils代码如下

/**
 * Email: [email protected]
 * Created by JackChen 2018/3/31 9:20
 * Version 1.0
 * Params:
 * Description:
*/
public class ViewUtils {


    /**
     * 用于Activity中
     */
    public static void inject(Activity activity){
        inject(new ViewFinder(activity) , activity) ;
    }


    /**
     * 用于自定义View中
     */
    public static void inject(View view){
        inject(new ViewFinder(view) , view) ;
    }


    /**
     * 在项目时有时候会用到Fragment中
     */
    public static void inject(View view  ,Object object){
        inject(new ViewFinder(view) , object) ;
    }


    /**
     * 兼容上边3个方法      object -> 反射需要执行的类
     */
    private static void inject(ViewFinder finder , Object object){
        // 动态注入属性
        injectField(finder , object) ;
        // 动态注入事件
        injectEvent(finder , object) ;
    }


    /**
     * 动态注入属性    object -> 反射需要执行的类
     */
    private static void injectField(ViewFinder finder, Object object) {
        // 1. 获取类中所有属性           ->  private TextView mTextTV , private int mPage ; 等等这些变量属性
        Class clazz = object.getClass();
        // 获取所有的属性 包括private、public、protected等所有属性
        Field[] fields = clazz.getDeclaredFields();


        // 2. 获取ViewById里边的value值  ->  R.id.test_tv这些东西
        for (Field field : fields) {
            ViewById viewById = field.getAnnotation(ViewById.class);
            if (viewById != null){
                // 获取注解里边的 id 值  -  R.id.test_tv
                int viewId = viewById.value();

                // 3. findviewbyid  找到View
                // 这里就相当于在 MainActivity中调用 TextView test_tv = (TextView) findViewById(R.id.test_tv);  是一码事
                View view = finder.findViewById(viewId);

                if(view != null) {
                    // 能够注入所有的修饰符,不管是 private、public、protected都是可以的,相当于添加权限
                    field.setAccessible(true);

                    // 4. 动态的注入属性 找到的View
                    try {
                        field.set(object, view);
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }

                }
            }
        }


    }



    /**
     * 动态注入事件    object -> 反射需要执行的类
     */
    private static void injectEvent(ViewFinder finder, Object object) {
        // 1. 获取类中所有的方法
        Class clazz = object.getClass();
        Method[] methods = clazz.getDeclaredMethods();

        // 2. 获取onClick里面的 value值
        for (Method method : methods) {
            OnClick onClick = method.getAnnotation(OnClick.class);
            if (onClick != null){
                int[] viewIds = onClick.value();
                for (int viewId : viewIds) {

                    // 3. findviewbyid 找到 view
                    View view= finder.findViewById(viewId);


                    // 拓展功能  检测网络  这里 !=null  代表需要检测网络
                    boolean isCheckNet = method.getAnnotation(CheckNet.class) != null;

                    if (view != null){
                        // 4. view.setOnClickListener
                        // 参数1:方法  参数2:谁去执行
                        view.setOnClickListener(new DeclaredOnClickListener(method , object , isCheckNet));
                    }

                }

            }
        }
    }


    /**
     * 这里是根据View的 onClick的源码
     */
    private static class DeclaredOnClickListener implements View.OnClickListener{

        private Method mMethod ;
        private Object mObject ;
        private boolean mIsCheckNet ;

        public DeclaredOnClickListener(Method method, Object object, boolean isCheckNet) {
            this.mMethod = method ;
            this.mObject = object ;
            this.mIsCheckNet = isCheckNet ;
        }


        /**
         * 最终我们在代码中写的点击事件会调用这个方法
         */
        @Override
        public void onClick(View v) {

            // 点击事件之前首先判断 需不需要检测网络
            if (mIsCheckNet){
                // 这里代表需要
                // 如果没网
                if (!networkAvailable(v.getContext())){
                    // 打印toast  "请检查网络" 这里写死会有问题 ,最好是可以去配置
                    Toast.makeText(v.getContext() , "请检查网络" , Toast.LENGTH_SHORT).show();
                    return;
                }
            }

            // 下边是执行有网的操作

            // 参数1:在哪一个类中执行  参数2:传递View
            try {
                // 确保所有类型方法都可以执行,包括private、public、protected等方法都可以执行
                mMethod.setAccessible(true);
                // 5. 反射注入方法
                mMethod.invoke(mObject , v) ;
            } catch (Exception e) {
                e.printStackTrace();


                // 这里是确保在类中的 onClick()方法中,不去传递View view的情况
                try {
                    mMethod.invoke(mObject , null) ;
                } catch (Exception e1) {
                    e1.printStackTrace();
                }
            }
        }
    }



    /**
     * 判断当前网络是否可用
     */
    private static boolean networkAvailable(Context context) {
        // 得到连接管理器对象
        try {
            ConnectivityManager connectivityManager = (ConnectivityManager) context
                    .getSystemService(Context.CONNECTIVITY_SERVICE);
            NetworkInfo activeNetworkInfo = connectivityManager
                    .getActiveNetworkInfo();
            if (activeNetworkInfo != null && activeNetworkInfo.isConnected()) {
                return true;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

}

MainActivity类如下,自己用于测试


public class MainActivity extends AppCompatActivity {


    @ViewById(R.id.test_tv)
    TextView test_tv ;
    @ViewById(R.id.test_iv)
    ImageView test_iv ;


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

        ViewUtils.inject(this);

        test_tv.setText("这里是用baselibrary注解的");

    }


    /**
     * 由于我们自己在ViewUtils中把点击事件已经try cache了,所以无论什么异常情况,程序都不会崩溃,最多是一个bug而已
     * 比如下边的 i=2/0 , 然后打印 i 的值 ,下边点击会没有反应,但是程序不会崩溃,这样就很好,体验就会很好,但是
     * 调试会比较麻烦,需要去看打印的警告 ,需要把控制台的级别点到 Warn 。
     * @param view
     */
    @OnClick({R.id.test_tv , R.id.test_iv})
    @CheckNet   // 表示如果没有网,就不要执行该方法了 ,而是直接打印没有网的toast
    public void onClick(View view){
        int i = 2/1 ;
        Toast.makeText(MainActivity.this , "执行结果是 -> " +i , Toast.LENGTH_SHORT).show();
    }
}

具体代码已上传至github:
https://github.com/shuai999/EssayJoke_day01.git

欢迎star,可以加我qq:2185134304

你可能感兴趣的:(自己动手打造一套IOC注解框架 - (ButterKnife源码阅读))