背景

最近接触新项目,项目中引入了Android Annotation(AA)依赖注入开源框架,代码中大片的注解代码,对于没用过注解框架(或者说没有如此大面积的使用)的我来说确实看得很费力,于是花时间研究了一下Android中的注解,当然了,这篇文章的目的并非讲解AA的使用,而是主要讲如何自定义注解以及用自定义的注解完成最常见的View Inject。

定义自己的注解

很简单,new->Java Class->选择Annotation,即完成了一个最基本的注解的定义。

public @interface TestAnnotation {

}

顺带提一下,任何一个注解其实都是隐式地实现了java.lang.annotaion.Annotaion接口的。对于一个完备的自定义注解,我们通常还需要两个东西,一是注解的修饰,二是注解的参数。

注解的修饰

我们可以用注解的注解(又叫元注解)来修饰注解,常见的有两个:Retention和Target。

  1. Retention接收一个RetentionPolicy枚举作为value,用以确定注解的保持策略,其源码如下:

    public enum RetentionPolicy {
    /**
    * Annotation is only available in the source code.
    */
    SOURCE,
    /**
    * Annotation is available in the source code and in the class file, but not
    * at runtime. This is the default policy.
    */
    CLASS,
    /**
    * Annotation is available in the source code, the class file and is
    * available at runtime.
    */
    RUNTIME
    }

    对于这三种类型,我的理解是这样的:

    • SOURCE作为最弱的一种策略,被标注为RetentionPolicy.SOURCE的注解,只是作为写码时的一种提醒,告诉我们某个特殊的含义,就像Android Studio中关键字、类名或者方法名的颜色区分,编译器并不会读取它。比如@Override注解,只是为了表明该方法是从父类中重写的,即便我们注释掉该注解,也不影响程序的编译和运行。
    • CLASS是缺省的保持策略,被标注为RetentionPolicy.CLASS的注解会影响到编译过程,但不影响到运行时。比如@TargetApi,可以用他来屏蔽掉编译时的非法版本API的错误,但是,如果你并没有在该注解标注的代码段内处理好新老版本的API使用问题的话,程序还是会在运行时发生异常。
    • RUNTIME作为权限最高的策略,其保持策略会持续到运行时,因此可以使用反射来在运行时检测注解的存在与否,接下来的Demo中使用到的就是这种方式。
  2. Target接收一个ElementType枚举类型的数组作为参数,用以表示该注解的适用对象。常见的有FIELD、TYPE、METHOD、PARAMETER,顾名思义,不再赘述。

注解的参数

注解可以定义为需要多个参数,只要在使用该注解时按类型将实参传入对应的形参即可,如果形参定义时有默认值则可不传该参数,否则必须传。如下所示:

public @interface TestAnnotation {
String param1();
String param2() default "hello";
int param3();
}

使用时:

@TestAnnotation(param1 = "world", param3 = 1)
void test() { }

当然,如果你的注解只需要一个参数,你可以把参数方法名定义成缺省的value()方法,这样在传参时就可以省略掉"value=<yourValue>"前面的"value="。

View Inject实现

前面提到,既然注解可以维持到进行时,那我们是不是也可以像一些注解框架那样实现自己的视图注解,以缓解频繁的findViewbyId呢?首先想到的是定义一个ViewInject注解,该注解用于在标记某View会被自动findViewbyId,当然啦,我们需要一个传入一个id,所以我们的ViewInject类看起来会像这样:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ViewInject {
int value();
}

使用时会像这样:

@ViewInject(R.id.tv_inject)
private TextView mTextView;

现在我们的注解定义和使用的完成了,我们该要解决如何完成我们的findViewById的动作的问题。这里我们开始用到一点反射的知识,首先我们通过反射获取到当前类的Fields集合,然后遍历集合,找出被标注为ViewInject注解的字段并拿出它们注解的value,也就是view在xml布局中的id,有了id我们就能为该字段获取到它相应的view了。我们的代码会像这样:

Class cls = getClass();
Field[] fields = cls.getDeclaredFields();
for (Field field : fields) {
if (!field.isAnnotationPresent(ViewInject.class))
continue;
ViewInject viewInject = field.getAnnotation(ViewInject.class); if (viewInject.value() <= 0)
continue; field.setAccessible(true);
try {
field.set(this, findViewById(viewInject.value()));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}

为了增加重用性,减少重复代码,我们很容易想到抽出一个BaseActivity,把这段代码从子类的onCreate方法拿到父类BaseActivity的onCreate方法中,但是这样问题就来了,如果进到父类的onCreate中时,子类还没有setContentView怎么办?没有根布局咋findView啊,愁人。。咋办呢,我们依葫芦画瓢,为Activity再定义一个注解并传入layout id,这下只要我们在BaseActivity中先反射拿到layout id并setContentView,就再不用担心没有根布局了,于是我们的注入代码进一步变成这样:

Class cls = getClass();
// 整体布局注入
if (!cls.isAnnotationPresent(ActivityInject.class))
return;
ActivityInject layoutInject = (ActivityInject) cls.getAnnotation(ActivityInject.class);
if (layoutInject.value() <= 0)
return;
setContentView(layoutInject.value()); // View变量注入
Field[] fields = cls.getDeclaredFields();
for (Field field : fields) {
if (!field.isAnnotationPresent(ViewInject.class))
continue;
ViewInject viewInject = field.getAnnotation(ViewInject.class); if (viewInject.value() <= 0)
continue; field.setAccessible(true);
try {
field.set(this, findViewById(viewInject.value()));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}

到这里,我们的View Inject其实已经完成了,剩下的就属于进阶~如何进阶呢,前面我们用到的都是FIED和TYPE级的注解来简化findview代码,接下来我们要用ElementType.METHOD来简化OnCreate中后续操作的执行和简化setOnClickListener的操作。新增两个注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AfterCreate {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface OnClick {
int[] value();
}

对应的在BaseActivity中新增注入的解析:

Method[] methods = cls.getDeclaredMethods();
ArrayList<Method> afterMethods = new ArrayList<>();
for (final Method method : methods) {
// 暂存AfterCreate方法
if (method.isAnnotationPresent(AfterCreate.class)) {
afterMethods.add(method);
}
// 处理OnClick方法
if (method.isAnnotationPresent(OnClick.class)) {
OnClick onClick = method.getAnnotation(OnClick.class); View.OnClickListener onClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
Class[] paramClasses = method.getParameterTypes();
// 这里只处理了无参数和只有一个view作参数的情况,其他情况不invoke
if (paramClasses.length == 0) {
method.invoke(BaseActivity.this, null);
} else if (paramClasses.length == 1) {
if (paramClasses[0].equals(View.class)) {
method.invoke(BaseActivity.this, v);
}
}
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
};
for (int id : onClick.value()) {
if (id > 0)
findViewById(id).setOnClickListener(onClickListener);
}
}
} // 执行AfterCreate方法
for (Method method : afterMethods) {
try {
method.invoke(this, null);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}

在子Activity中使用:

@AfterCreate
void init() {
mTextView.setText("Set TextView Content");
mImageView.setImageResource(R.mipmap.ic_launcher);
} @OnClick({R.id.iv_inject,R.id.tv_inject})
void onClick(View view) {
switch (view.getId()) {
case R.id.iv_inject:
Toast.makeText(getApplicationContext(), "ImageView is clicked!", Toast.LENGTH_SHORT).show();
break; case R.id.tv_inject:
Toast.makeText(getApplicationContext(), "TextView is clicked!", Toast.LENGTH_SHORT).show();
break;
} }

到这里就基本结束了。完整的Demo从这里下载。

Android运行时注入浅析与使用的更多相关文章

  1. SPRING IN ACTION 第4版笔记-第三章ADVANCING WIRING-007-给BEAN运行时注入值placeholder、@Value

    一.用placeholder给bean运行时注入值的步骤 Spring取得placeholder的值是用${...} 1.声明placeholder bean (1)java方式 In order t ...

  2. 构建ASP.NET MVC4+EF5+EasyUI+Unity2.x注入的后台管理系统(6)-Unity 2.x依赖注入by运行时注入[附源码]

    原文:构建ASP.NET MVC4+EF5+EasyUI+Unity2.x注入的后台管理系统(6)-Unity 2.x依赖注入by运行时注入[附源码] Unity 2.x依赖注入(控制反转)IOC,对 ...

  3. Android 运行时权限处理(from jianshu)

    https://www.jianshu.com/p/e1ab1a179fbb 翻译的国外一篇文章. android M 的名字官方刚发布不久,最终正式版即将来临! android在不断发展,最近的更新 ...

  4. Qt for android运行时出错 Error: Target id 'android--1' is not valid

    [提问]windows7下Qt for android运行时出错 Error: Target id 'android--1' is not valid[复制链接] 上一主题下一主题   离线yijun ...

  5. Dalvik模式下基于Android运行时类加载的函数dexFindClass脱壳

    本文博客地址:http://blog.csdn.net/qq1084283172/article/details/78003184 前段时间在看雪论坛发现了<发现一个安卓万能脱壳方法>这篇 ...

  6. Android 运行时权限处理

    引言 Android 6.0 (API 23) 开始引入了运行时权限检查 (Permissions at Run Time),用户不需要在安装时同意授予应用权限,而是在应用运行时动态去申请所需要的权限 ...

  7. Android运行时注解

    Android的注解有编译时注解和运行时注解,本文就介绍下运行时注解. 其实非常简单,直接上代码:本文主要是替代传统的findViewById()的功能,就是在我们Activity中不需要再使用fin ...

  8. Android 运行时权限及APP适配

    Android 6.0起,Android加强了权限管理,引入运行时权限概念.对于: 1. Android 5.1(API 22)及以前版本,应用权限必须声明在AndroidManifest.xml中, ...

  9. android 运行时异常捕获

    1,将运行时异常捕获并存到手机SD卡上 可以直接使用logcat 命令Runtime.getRuntime().exec("logcat -f "+ file.getAbsolut ...

随机推荐

  1. 线性DP之机器分配

    题目大意 自己瞅 (懒得打了) 思路 前面是很简单的线性dp,后面是模拟递归输出方案, 模拟递归可以设ny为机器数机器数,nx表示第nx个公司,tot为总盈利,那么则有\(a[nx][i]+dp[nx ...

  2. WPF 最基本的前后台代码对照

    最基本的3D代码对照 xaml代码 <Viewport3D> <Viewport3D.Camera> <PerspectiveCamera Position=" ...

  3. java 面向对象(八):面向对象的特征一:封装性

    面向对象的特征一:封装与隐藏1.为什么要引入封装性?我们程序设计追求“高内聚,低耦合”.高内聚 :类的内部数据操作细节自己完成,不允许外部干涉:低耦合 :仅对外暴露少量的方法用于使用. 隐藏对象内部的 ...

  4. python 并发专题(十三):asyncio (二) 协程中的多任务

    . 本文目录# 协程中的并发 协程中的嵌套 协程中的状态 gather与wait . 协程中的并发# 协程的并发,和线程一样.举个例子来说,就好像 一个人同时吃三个馒头,咬了第一个馒头一口,就得等这口 ...

  5. [C#]正则表达式的基本用法

    C#正则表达式的基本用法 正则表达式(regular expression)描述了一种字符串匹配的模式(pattern),可以用来检查一个串是否含有某种子串.将匹配的子串替换或者从某个串中取出符合某个 ...

  6. 句柄Handle的释放(8)

    本篇首先介绍几个与句柄分配与释放密切相关的类,然后重点介绍句柄的释放. 1.HandleArea.Area与Chunk 句柄都是在HandleArea中分配并存储的,类的定义如下: // Thread ...

  7. 怎样才能做好软件测试——Python自动化测试工程师七年感悟

    即使不想在文章的开头过分的正经严肃,但这是一个十分正经技术类规划类的分享.不讲笑话也不讲故事,直接进入主题. 如何学好软件测试?反推一下作为一名优秀的软件测试工程师需要什么能力.   学习测试讲究实践 ...

  8. Python Ethical Hacking - BACKDOORS(2)

    Refactoring - Creating a Listener Class #!/usr/bin/env python import socket class Listener: def __in ...

  9. es6 Proxy简单使用

    es6的Proxy是什么? 可以理解为,是在访问对象前的一层拦截.只要访问的该对象,就要通过这个一层拦截.这一层的拦截,可以进行数据的过滤和更改 比如下面这个 var p = new Proxy({} ...

  10. vue学习(十七) 使用自定义指令 使文本框获得鼠标焦点

    需求:当我们进入某个页面,页面中的第一个input会自动获得焦点 光标闪烁,代表可输入 <div id="app"> //v-focus 是自定义的 <input ...