Android IOC——运行时(Runtime)处理

编译时处理可见:Android IOC——编译时(Compile time)处理,撸一个简易版的ButterKnife

IOC概述

IOC是一种设计原则,它的常见实现方式为DI。

  • IOC——Inversion of Control,控制反转
  • DI——Dependency Injection,依赖注入

通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体,将其所依赖的对象的引用传递给它。简单的说,不需要直接new出一个对象来进行赋值了,可以依赖一个系统生成对象,然后通过注入进行对象的赋值。依赖注入有如下实现方式:

  1. 基于接口——实现特定接口以供外部容器注入所依赖类型的对象。
  2. 基于 set 方法——实现特定属性的public set方法,来让外部容器调用传入所依赖类型的对象。
  3. 基于构造函数——实现特定参数的构造函数,在新建对象时传入所依赖类型的对象。
  4. 基于注解

本文的主要目的是讲解如何基于注解实现IOC,因此不会具体讲解什么是IOC。对于做过java spring的一般来说会比较熟悉,如果没有做过也没关系,具体可见:

注解的保留策略(Retention Policy)

注解的保留策略,我们也可以称为注解的生命周期。按照枚举类RetentionPolicy的定义,可以将注解的保留策略划分为以下3种形式:

  1. RetentionPolicy.SOURCE——注解只保留在Java源文件,当Java源文件被编译器编译成class文件的时候,注解就会被遗弃,通常用于源码级别的检查性操作(比如@override或者IDE的代码提示)
  2. RetentionPolicy.CLASS——默认的生命周期,注解将会被保留到class文件,当class文件被JVM加载的时候被遗弃,通常会被用于编译时的注解处理(注解处理器)
  3. RetentionPolicy.RUNTIME——注解始终存在,通常被用于运行时的注解处理(反射)

上述三种策略按照生命周期的长短可用下列表达式来说明:

  • SOURCE < CLASS < RUNTIME

IOC——注解运行时(Runtime)处理

进入正题前,先说下我们运行时IOC的目标——实现Activity的成员变量view和string的依赖注入。那么实现该目标,我们将需要用到注解和发射。

对于注解和反射不熟悉的可详见以下Java官方指南文档:

自定义注解

自定义两个运行时注解——StringInject和ViewInject:

/**
 * 用于字符串的运行时注入
 * <ul>
 * <li>编译完成后,在运行时,注解依然存在</li>
 * </ul>
 *
 * @author xpleemoon
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface StringInject {
    String value() default "IOC——运行时注解处理\n既要会用洋枪洋炮\n又要会造土枪土炮";
}

/**
 * 用于view的运行时注入
 * <ul>
 * <li>编译完成后,在运行时,注解依然存在</li>
 * </ul>
 *
 * @author xpleemoon
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface ViewInject {
    @IdRes int id() default 0;
}

@Target(ElementType.FIELD)限定了它们的使用范围——成员变量,同时注解的属性都有默认值——StringInject的默认值为“IOC——运行时注解处理\n既要会用洋枪洋炮\n又要会造土”,ViewInject的默认id为0。

@Retention(RetentionPolicy.RUNTIME)表明注解的保留策略为runtime,即注解的生命周期一直到runtime都存在。只有这样,才能在运行时通过反射的方式获取注解信息

注解的运行时IOC处理

/**
 * 运行时IOC注入工具,使用方式见下面代码:
 * <pre><code>
 * public class ExampleActivity extends Activity {
 *   {@literal @}ViewInject(R.id.title) TextView titleView;
 *
 *   {@literal @}Override protected void onCreate(Bundle savedInstanceState) {
 *     super.onCreate(savedInstanceState);
 *     setContentView(R.layout.example_activity);
 *     InjectUtils.inject(this);
 *   }
 * }
 * </code></pre>
 *
 * @author xpleemoon
 */
final class InjectUtils {
    private static void check(Activity activity) {
        if (activity == null) {
            throw new IllegalStateException("依赖注入的activity不能为null");
        }

        Window window = activity.getWindow();
        if (window == null || window.getDecorView() == null) {
            throw new IllegalStateException("依赖注入的activity未建立视图");
        }
    }

    public static void inject(@NonNull Activity target) {
        check(target);

        Class<? extends Activity> targetClz = target.getClass();
        Field[] fields = targetClz.getDeclaredFields(); // 获取target中的所有字段
        for (Field field : fields) {
            StringInject stringInject = field.getAnnotation(StringInject.class);
            if (stringInject != null) {
                String str = stringInject.value();
                if (!TextUtils.isEmpty(str)) {
                    try {
                        field.setAccessible(true);
                        field.set(target, str); // 为StringInject修饰的字段注入字符串
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
                continue;
            }

            ViewInject viewInject = field.getAnnotation(ViewInject.class);
            if (viewInject != null) {
                int viewId = viewInject.id();
                if (viewId > 0) {
                    try {
                        Method findViewByIdMethod = targetClz.getMethod("findViewById", int.class);
                        View view = (View) findViewByIdMethod.invoke(target, viewId); // 反射调用,获取view对象
                        field.setAccessible(true);
                        field.set(target, view); // 为ViewInject修饰的字段注入view
                    } catch (NoSuchMethodException e) {
                        e.printStackTrace();
                    } catch (InvocationTargetException e) {
                        e.printStackTrace();
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
                continue;
            }
        }
    }
}

我们重点关注inject(target)方法

  • 首先,Field[] fields = targetClz.getDeclaredFields(),通过反射获取target类的所有成员变量作为数组

  • 其次,for循环遍历数组fields数组,分别解析StringInjectViewInject注解。我们以ViewInject注解的解析为例:

    1. ViewInject viewInject = field.getAnnotation(ViewInject.class),通过反射尝试去获取成员变量的注解,如果viewInject不为null,那么表明field是被ViewInject注解修饰的
    2. int viewId = viewInject.id(),获取注解声明的id值
    3. View view = (View) findViewByIdMethod.invoke(target, viewId),反射调用findViewById(id)方法,获取view对象
    4. field.set(target, view),通过反射ViewInject修饰的字段注入view

说白了,注解的运行时IOC处理就是反射:通过反射获取对应元素,然后获取元素上面的注解,接着得到注解的属性值,最后把得到的属性值通过反射set到注解修饰的目标上。

注解的使用

Runtime-IOC.gif
/**
 * 注意,当前类中使用的注解和{@link com.xpleemoon.annotations.demo.annotationprocess.compile.CompileIOCActivity}的不一致.</br>
 * 当前使用的是runtime包下的
 *
 * @author xpleemoon
 */
public class RuntimeIOCActivity extends AppCompatActivity {
    /**
     * 使用默认值注入
     */
    @StringInject
    String mTextStr;
    @StringInject("运行时注解处理,IOC就这么简单")
    String mToastStr;
    @ViewInject(id = R.id.runtime_inject_text)
    private TextView mText;
    @ViewInject(id = R.id.runtime_inject_button)
    private Button mBtn;

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

        InjectUtils.inject(this);
        mText.setText(mTextStr);
        mBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(getApplicationContext(), mToastStr, Toast.LENGTH_LONG).show();
            }
        });
    }
}

很简单,就是对类型为View或者String的成员变量声明一下注解,然后调用一下InjectUtils.inject(this),这样就为注解修饰的变量完成了数据注入。于是乎,我们就可以愉快的使用这些变量了,比如mText.setText(mTextStr)

  • mTextmTextStrRuntimeIOCActivity都是没有经过直接赋值的,它们的赋值通过控制反转的形式交给了InjectUtils.inject(this)

点击源码,该份源码中既有运行时和编译时的处理。当前文章所讲的只需要关注app module下的runtime包即可。

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

推荐阅读更多精彩内容