自己动手打造一套IOC注解框架

1.概述


这是我们的内涵段子系统架构的第一期分享,希望大家可以先去了解一下这一期的内容:2017Android进阶之路与你同行。在介绍内涵段子整个项目的时候我们也说好了会分析系统源码设计模式,第三方框架源码解析,然后自己动手一点一点打造一套内涵段子框架。这一期的内容对于部分哥们可能有点麻烦,如果觉得抽象请看视频讲解。
  那么什么是IOC,控制反转(Inversion of Control,英文缩写为IOC),其实就是反射加注解如果你学过Java后台这个在三大框架中会经常使用。过多的去解释其实也没什么意思,我们主要来看有什么用处。
  附视频讲解地址:http://pan.baidu.com/s/1kVFMRQJ

2.第三方IOC框架源码解析


今天主要讲的就是Android中IOC框架就是注入控件和布局或者说是设置点击监听,如果你用过xUtils,afinal,butterknife类的框架,你肯定不陌生~
  我们挑两个做一下对比和源码分析,我们就挑xUtils和butterknife这两个代表:

2.1 xUtils的IOC注解使用

xutils如果大家使用过的话它里面的内容会比较多,包括网络,数据库,IOC注入,网络图片使用,那么我们这里主要看看xutils3.0的IOC注解:https://github.com/wyouflf/xUtils3

public class MainActivity extends AppCompatActivity {

    @ViewInject(R.id.icon)
    private ImageView mIconIv;

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

        mIconIv.setImageResource(R.drawable.icon);
    }

    /**
     * 1. 方法必须私有限定,
     * 2. 方法参数形式必须和type对应的Listener接口一致.
     * 3. 注解参数value支持数组: value={id1, id2, id3}
     * 4. 其它参数说明见{@link org.xutils.view.annotation.Event}类的说明.
     **/
    @Event(value = R.id.icon,
            type = View.OnClickListener.class/*可选参数, 默认是View.OnClickListener.class*/)
    private void iconIvClick(View view) {
        Toast.makeText(this, "图片被点击了", Toast.LENGTH_LONG).show();
    }
}

这里写图片描述

  
  我就是设置一张图片和一个点击事件而已,其实主要解决的就是我们不再需要findViewById()和setOnClickListener(),我们简单的来看一下源码到底是怎么实现的:
  
 2.2 xUtils的IOC注解源码解析 
  
  我就挑一些关键的代码来分析一下,视频里面会给大家讲得非常详细,我们主要看一下x.view().inject(this);到底是干了什么:

    @Override
    public void inject(Activity activity) {
        //获取Activity的ContentView的注解
        Class<?> handlerType = activity.getClass();
        try {
            // 找到ContentView这个注解,在activity类上面获取
            ContentView contentView = findContentView(handlerType);
            if (contentView != null) {
                int viewId = contentView.value();
                if (viewId > 0) {
                   // 如果有注解获取layoutId的值,利用反射调用activity的setContentView方法注入视图
                    Method setContentViewMethod = 
                        handlerType.getMethod("setContentView", int.class);
                    setContentViewMethod.invoke(activity, viewId);
                }
            }
        } catch (Throwable ex) {
            LogUtil.e(ex.getMessage(), ex);
        }
        // 处理 findViewById和setOnclickListener的注解
        injectObject(activity, handlerType, new ViewFinder(activity));
    }
private static void injectObject(Object handler, Class<?> handlerType, ViewFinder finder) {
     // .......

        // 从父类到子类递归
        injectObject(handler, handlerType.getSuperclass(), finder);

        // inject view  注入控件View
        Field[] fields = handlerType.getDeclaredFields();
        if (fields != null && fields.length > 0) {
            for (Field field : fields) {
              // ......
               // 获取viewInject 注解
                ViewInject viewInject = field.getAnnotation(ViewInject.class);
                if (viewInject != null) {
                    try {
                       // 其实最终还是调用findViewById的方法
                        View view = finder.findViewById(viewInject.value(),
                             viewInject.parentId());
                        if (view != null) {
                            // 利用反射 把View注入到该属性中
                            field.setAccessible(true);
                            field.set(handler, view);
                        } else {
                           // ......
                        }
                    } catch (Throwable ex) {
                        LogUtil.e(ex.getMessage(), ex);
                    }
                }
            }
        } // end inject view

        // inject event  注入事件
        Method[] methods = handlerType.getDeclaredMethods();
        if (methods != null && methods.length > 0) {
            for (Method method : methods) {
          // ......
                //检查当前方法是否是event注解的方法
                Event event = method.getAnnotation(Event.class);
                if (event != null) {
                    try {
                        // id参数
                        int[] values = event.value();
                        int[] parentIds = event.parentId();
                        int parentIdsLen = parentIds == null ? 0 : parentIds.length;
                        //循环所有id,生成ViewInfo并添加代理反射   主要使用了动态代理的设计模式
                        for (int i = 0; i < values.length; i++) {
                            int value = values[i];
                            if (value > 0) {
                                ViewInfo info = new ViewInfo();
                                info.value = value;
                                info.parentId = parentIdsLen > i ? parentIds[i] : 0;
                                method.setAccessible(true);
                                // EventListenerManager 动态代理执行相应的方法
                                EventListenerManager.addEventMethod(
                                    finder, info, event, handler, method);
                            }
                        }
                    } catch (Throwable ex) {
                        LogUtil.e(ex.getMessage(), ex);
                    }
                }
            }
        } // end inject event

    }

关键的源码大概就这么多,动态代理的部分没有贴出来,这个写可能写不清楚大家可以自己去看看源码或者自己去网上搜搜动态代理的设计模式分析,视频里面会给大家讲清楚。动态代理我记得我刚刚自学那会还真是一道坎但是这个坎我们得迈过去,后面我们讲Android的Hook技术以及插件开发会反复的讲到。
  xutils相对来说应该不难理解吧,其实就是我们利用类的反射循环获取属性的注解值然后通过findViewById之后,动态的注入到控件属性里面;事件注入也是类似首先findViewById然后利用动态代理去反射执行方法。

2.3 butterknife的使用
  
  相比于xutils来讲它可能更受大家的欢迎,第一在性能方面xutils完全是利用的反射,butterknife是轻量级的反射使用的注解都是编译时注解,而且它还提供了一个Android Studio的插件不需要我们去写任何的代码,作者JakeWharton很出名的写过很多大型的第三方框架,https://github.com/JakeWharton/butterknife

public class MainActivity extends AppCompatActivity {

    @Bind(R.id.icon)
    ImageView icon;

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

    @OnClick(R.id.icon)
    public void onClick() {
        Toast.makeText(this, "图片点击了", Toast.LENGTH_LONG).show();
    }
}

上面是由插件给我们自动生成的代码,我们再也不用手动去写这些代码了。属性是不能private,onClick()方法也不能private,否则会报错的待会我们看源码的实现就知道为什么只能这样,插件自动生成的名字和方法总感觉怪怪的和我们的Android源码规范不一致,当然后面我们会自己去写一个Android Studio插件来配合我们自己的IOC注解框架。

2.4 butterknife的源码解析

源码阅读相对于xutils的也麻烦很多,如果我们按照常规的逻辑去找ButterKnife.bind(this);会发现里面好像没什么东西只有这个:

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
      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);
    }
  }

如果从这里看我们好像看不到任何的东西,其实工作流程是怎样的呢?我们可以看一下Bind这个Annotation注解类和ButterKnifeProcessor这两个类其实就能找到线索:

@Retention(CLASS) @Target(FIELD)
public @interface Bind {
  /** View ID to which the field will be bound. */
  int[] value();
}

@Retention 为 CLASS 的 Annotation,由apt(Annotation Processing Tool) 解析自动解析。ButterKnife便是用了Java Annotation Processing技术,就是在Java代码编译成Java字节码的时候就已经处理了@Bind、@OnClick(ButterKnife还支持很多其他的注解)这些注解了。

你可以你定义注解,并且自己定义解析器来处理它们。Annotation processing是在编译阶段执行的,它的原理就是读入Java源代码,解析注解,然后生成新的Java代码。新生成的Java代码最后被编译成Java字节码,注解解析器(Annotation Processor)不能改变读入的Java 类,比如不能加入或删除Java方法。下面我们应该就大概知道工作流程了吧?

2.5 ButterKnife 工作流程

当你编译你的Android工程时,ButterKnife工程中ButterKnifeProcessor类的process()方法会执行以下操作:

  • 开始它会扫描Java代码中所有的ButterKnife注解@Bind、@OnClick、@OnItemClicked等。

  • 当它发现一个类中含有任何一个注解时,ButterKnifeProcessor会帮你生成一个Java类,名字类似$$ViewBinder,这个新生成的类实现了ViewBinder接口。

  • 这个ViewBinder类中包含了所有对应的代码,比如@Bind注解对应findViewById(), @OnClick对应了view.setOnClickListener()等等。

  • 最后当Activity启动ButterKnife.bind(this)执行时,ButterKnife会去加载对应的ViewBinder类调用它们的bind()方法。

现在我们总该明白为什么我们的生成的属性和方法不能私有了吧?我们最后看一下编译时生成的class类吧

public class MainActivity$$ViewBinder<T extends MainActivity> implements ViewBinder<T> {
  @Override public void bind(final Finder finder, final T target, Object source) {
    View view;
    view = finder.findRequiredView(source, 2131427372, "field 'icon' and method 'onClick'");
    target.icon = finder.castView(view, 2131427372, "field 'icon'");
    view.setOnClickListener(
      new butterknife.internal.DebouncingOnClickListener() {
        @Override public void doClick(View p0) {
           target.onClick();
        }
      });
  }

  @Override public void unbind(T target) {
    target.icon = null;
  }
}

如果现在要我们来选,我肯定会选butterknife这个主要是因为它有一个插件我完全不用写任何的代码,而且没有利用不是全反射在性能方面也有一点提升,但是有的时候我想加一个检测网络的注解,这就有点麻烦了,插件生成的属性名称和方法名也有点蛋疼,我们自己来试着写写吧。

3.自己动手丰衣足食


接下来我们自己来实现一套IOC注解框架吧,采用的方式反射加注解和Xutils类似,但我们尽量不写那么麻烦,也不打算采用动态代理,我们扩展一个检测网络的注解,比如没网的时候我们不去执行方法而是给予没有网络的提示同时也不允许用户反复点击。
  这个时候有人就开始喷了,明知道反射会影响性能为什么还要用?这里我就随便说说吧,我承认反射会影响性能但是问题不大我们可以自己去测试反射1万次大概会怎样,如果你非得去纠结那我也没办法,我们还是多花时间在UI渲染和Bitmap以及Service和Handler上面吧,我还从来没有遇到过反射调用gc或者内存溢出的情况,而且后面讲插件化开发的时候也会用到反射那砸门就不做了?不管了开工。
  
 3.1 控件属性注入
  
  这里我就不在介绍Annotation的使用了,如果对于这个不是特别了解的大家可以自己去查一查资料或者看一下我录制的视频吧,先来处理控件属性的注入,但是需要考虑各种情况:

/**
 * Created by Darren on 2017/2/4.
 * Email: 240336124@qq.com
 * Description:  IOC的View属性注解类
 */
// RUNTIME 运行时检测,CLASS 编译时butterKnife使用是这个  SOURCE 源码资源的时候
@Retention(RetentionPolicy.RUNTIME)
// FIELD 注解只能放在属性上    METHOD 方法上  TYPE 类上  CONSTRUCTOR 构造方法上
@Target(ElementType.FIELD)
public @interface ViewById {
    // 代表可以传值int类型  使用的时候:ViewById(R.id.xxx)
    int value();
}
/**
 * Created by Darren on 2017/2/4.
 * Email: 240336124@qq.com
 * Description: IOC 注入 ViewUtils
 */
public class ViewUtils {

    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);
    }

    private static void inject(ViewFinder viewFinder, Object object) {
        injectFiled(viewFinder, object);
        injectEvent(viewFinder, object);
    }

    // 注入事件
    private static void injectEvent(ViewFinder viewFinder, Object object) {

    }

    /**
     * 注入属性
     */
    private static void injectFiled(ViewFinder viewFinder, Object object) {
        // object --> activity or fragment or view 是反射的类
        // viewFinder --> 只是一个view的findViewById的辅助类

        // 1. 获取所有的属性
        Class<?> clazz = object.getClass();
        // 获取所有属性包括私有和公有
        Field[] fields = clazz.getDeclaredFields();

        for (Field field : fields) {
            // 2. 获取属性上面ViewById的值
            ViewById viewById = field.getAnnotation(ViewById.class);

            if (viewById != null) {
                // 获取ViewById属性上的viewId值
                int viewId = viewById.value();
                // 3. 通过findViewById获取View
                View view = viewFinder.findViewById(viewId);

                if (view != null) {
                    // 4. 反射注入View属性
                    // 设置所有属性都能注入包括私有和公有
                    field.setAccessible(true);
                    try {
                        field.set(object, view);
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                } else {
                    throw new RuntimeException("Invalid @ViewInject for "
                            + clazz.getSimpleName() + "." + field.getName());
                }
            }
        }
    }
}

3.2 点击事件注入
 
 事件的注入我们只打算setOnclickListener其他不常见的我们先不管,也不打算采用动态代理的设计模式。

// 事件注入
private static void injectEvent(ViewFinder viewFinder, Object object) {
        // 1.获取所有方法
        Class<?> clazz = object.getClass();
        Method[] methods = clazz.getDeclaredMethods();
        // 2.获取方法上面的所有id
        for (Method method : methods) {
            OnClick onClick = method.getAnnotation(OnClick.class);
            if (onClick != null) {
                int[] viewIds = onClick.value();
                if (viewIds.length > 0) {
                    for (int viewId : viewIds) {
                        // 3.遍历所有的id 先findViewById然后 setOnClickListener
                        View view = viewFinder.findViewById(viewId);
                        if (view != null) {
                            view.setOnClickListener(new DeclaredOnClickListener(method, object));
                        }
                    }
                }
            }
        }
    }


    private static class DeclaredOnClickListener implements View.OnClickListener {
        private Method mMethod;
        private Object mHandlerType;

        public DeclaredOnClickListener(Method method, Object handlerType) {
            mMethod = method;
            mHandlerType = handlerType;
        }

        @Override
        public void onClick(View v) {
            // 4.反射执行方法
            mMethod.setAccessible(true);
            try {
                mMethod.invoke(mHandlerType, v);
            } catch (Exception e) {
                e.printStackTrace();
                try {
                    mMethod.invoke(mHandlerType, null);
                } catch (Exception e1) {
                    e1.printStackTrace();
                }
            }
        }
    }
public class MainActivity extends AppCompatActivity {

    @ViewById(R.id.icon)
    private ImageView mIconIv;

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

    @OnClick(R.id.icon)
    private void onClick(View view) {
        int i = 2 / 0;
        Toast.makeText(this, "图片点击了"+i, Toast.LENGTH_LONG).show();
    }
}

使用起来和xutils类似,方法和属性可以私有,但是有一点我们在Onclick点击事件的方法里面无论做什么操作都是不会报错的,所以如果发现bug需要留意警告日志,这不是坑嗲吗?其实在我们的开发过程给用户或者老板玩的时候我们最怕的是闪退,现在我们就算有Bug也不会出现闪退的情况只是调试的时候需要留意警告日志还是蛮不错的。
 
 3.3 扩展动态检测网络注解

我们最后扩展一下加一个检测网络的注解,有的时候我们在点击的方法里面需要去检测网络,比如登陆注册,我们如果没网就没必要去调接口启动线程了,只需要提示用户当前无网络即可。当然这只是一个扩展而已。

public class MainActivity extends AppCompatActivity {

    @ViewById(R.id.icon)
    private ImageView mIconIv;

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

    @OnClick(R.id.icon)
    @CheckNet          // 检测网络
    private void onClick(View view) {
        Toast.makeText(this, "图片点击了", Toast.LENGTH_LONG).show();
    }
}

扩展我们就写这么个例子吧,这是内涵段子框架搭建的第一期分享,希望大家可以先去了解一下我们所有的分享内容2017Android进阶之路与你同行,

附视频讲解地址:http://pan.baidu.com/s/1kVFMRQJ

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容