[关闭]
@puke3615 2016-08-18T17:29:36.000000Z 字数 7770 阅读 844

Android注解应用

在Java中经常会用到注解,通过注解的方式可以实现很多灵活性的东西。很多优秀的框架都支持注解的方式,如Spring的中对Bean的注解,Hibernate中对POJO类的注解,Mybatis中对Mapper的注解,ButterKnife中对View的注解,Dagger中对各个Component的注解, Retrofit对Api的注解。一言以蔽之,使用注解可以让整个代码风格看起来清爽明了。

一、 传统的代码风格

activity_main.xml

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent"
  5. android:orientation="vertical">
  6. <EditText
  7. android:id="@+id/username"
  8. android:layout_width="match_parent"
  9. android:layout_height="wrap_content"
  10. android:hint="请输入用户名.." />
  11. <Button
  12. android:id="@+id/submit"
  13. android:layout_width="match_parent"
  14. android:layout_height="wrap_content"
  15. android:text="提交" />
  16. </LinearLayout>

很简单的一个界面,只有一个EditText和一个Button,就不过多解释了。

MainActivity.java

  1. package com.puke.annotationdemo;
  2. import android.app.Activity;
  3. import android.os.Bundle;
  4. import android.view.View;
  5. import android.widget.Button;
  6. import android.widget.EditText;
  7. import android.widget.Toast;
  8. public class MainActivity extends Activity {
  9. private EditText mUsername;
  10. private Button mSubmit;
  11. @Override
  12. protected void onCreate(Bundle savedInstanceState) {
  13. super.onCreate(savedInstanceState);
  14. setContentView(R.layout.activity_main);
  15. findView();
  16. initListener();
  17. }
  18. private void findView() {
  19. mUsername = (EditText) findViewById(R.id.username);
  20. mSubmit = (Button) findViewById(R.id.submit);
  21. }
  22. private void initListener() {
  23. mSubmit.setOnClickListener(new View.OnClickListener() {
  24. @Override
  25. public void onClick(View v) {
  26. String username = mUsername.getText().toString().trim();
  27. Toast.makeText(MainActivity.this, username, Toast.LENGTH_SHORT).show();
  28. }
  29. });
  30. }
  31. }

我们这里要做到是用户点击提交的时候,Toast弹出EditText输入的内容,比较简单,略过~

传统的风格是这样的,这样看来也许觉得没什么问题,但是实际的开发当中我们一个页面当中包含的View,以及对应的View的一些事件回调要远远比这个繁琐。我相信一个Activity中要处理十几二十几个View也不算是什么稀奇的事情,那这样会造成什么结果呢。。。

你的属性声明会是这样的

  1. private Button mButton1;
  2. private Button mButton2;
  3. private Button mButton3;
  4. private Button mButton4;
  5. private Button mButton5;
  6. private Button mButton6;
  7. private Button mButton7;
  8. private Button mButton8;

你的findView会是这样的

  1. mButton1 = (Button) findViewById(R.id.button1);
  2. mButton2 = (Button) findViewById(R.id.button2);
  3. mButton3 = (Button) findViewById(R.id.button3);
  4. mButton4 = (Button) findViewById(R.id.button4);
  5. mButton5 = (Button) findViewById(R.id.button5);
  6. mButton6 = (Button) findViewById(R.id.button6);
  7. mButton7 = (Button) findViewById(R.id.button7);
  8. mButton8 = (Button) findViewById(R.id.button8);

你的事件监听会是这样的

  1. mButton1.setOnClickListener(new View.OnClickListener() {
  2. @Override
  3. public void onClick(View v) {
  4. }
  5. });
  6. mButton2.setOnClickListener(new View.OnClickListener() {
  7. @Override
  8. public void onClick(View v) {
  9. }
  10. });
  11. mButton3.setOnClickListener(new View.OnClickListener() {
  12. @Override
  13. public void onClick(View v) {
  14. }
  15. });
  16. mButton4.setOnClickListener(new View.OnClickListener() {
  17. @Override
  18. public void onClick(View v) {
  19. }
  20. });
  21. mButton5.setOnClickListener(new View.OnClickListener() {
  22. @Override
  23. public void onClick(View v) {
  24. }
  25. });
  26. mButton6.setOnClickListener(new View.OnClickListener() {
  27. @Override
  28. public void onClick(View v) {
  29. }
  30. });
  31. mButton7.setOnClickListener(new View.OnClickListener() {
  32. @Override
  33. public void onClick(View v) {
  34. }
  35. });
  36. mButton8.setOnClickListener(new View.OnClickListener() {
  37. @Override
  38. public void onClick(View v) {
  39. }
  40. });

例子举得不怎么恰当,但是足以说明随着业务代码的不断扩大,这些看上去的”无脑操作“也会让我们广大coder变得愈加的不耐烦,而且会使得我们的类变得庞大而臃肿。

那么,接下来我们就通过注解的方式来搞一发~

二、 注解的编码风格

我们打算使用注解的方式实现

  1. xml的配置
  2. View的注入
  3. 点击事件的绑定

接下来就是具体实现逻辑

  1. 首先我们先定义一个Bind注解
  1. package com.puke.annotationdemo;
  2. import java.lang.annotation.ElementType;
  3. import java.lang.annotation.Retention;
  4. import java.lang.annotation.RetentionPolicy;
  5. import java.lang.annotation.Target;
  6. /**
  7. * @author zijiao
  8. * @version 16/8/18
  9. */
  10. @Retention(RetentionPolicy.RUNTIME)
  11. @Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
  12. public @interface Bind {
  13. int value() default 0;
  14. }
  1. 然后写Bind注解对应的注解处理器
  1. package com.puke.annotationdemo;
  2. import android.app.Activity;
  3. import android.view.View;
  4. import java.lang.reflect.Field;
  5. import java.lang.reflect.InvocationTargetException;
  6. import java.lang.reflect.Method;
  7. /**
  8. * @author zijiao
  9. * @version 16/8/18
  10. */
  11. public class BindHandler {
  12. /**
  13. * 处理对Activity的注解
  14. *
  15. * @param activity 目标Activity
  16. */
  17. public static void handleBind(Activity activity) {
  18. Class cls = activity.getClass();
  19. handleSetContentView(activity);
  20. handleFindView(cls.getDeclaredFields(), activity);
  21. handleClickEvent(cls.getDeclaredMethods(), activity);
  22. }
  23. //绑定xml布局
  24. private static void handleSetContentView(Activity activity) {
  25. Class<?> cls = activity.getClass();
  26. if (cls.isAnnotationPresent(Bind.class)) {
  27. //Activity中加入Bind注解时取出注解配置
  28. Bind bind = cls.getAnnotation(Bind.class);
  29. int layout = bind.value();
  30. if (layout != 0) {
  31. activity.setContentView(layout);
  32. }
  33. }
  34. }
  35. //View的注入
  36. private static void handleFindView(Field[] declaredFields, Activity activity) {
  37. if (declaredFields == null || declaredFields.length == 0) {
  38. return;
  39. }
  40. for (Field field : declaredFields) {
  41. //找到被Bind注解且是View的所有属性
  42. if (field.isAnnotationPresent(Bind.class) && View.class.isAssignableFrom(field.getType())) {
  43. Bind bind = field.getAnnotation(Bind.class);
  44. int id = bind.value();
  45. if (id != 0) {
  46. View view = activity.findViewById(id);
  47. field.setAccessible(true);
  48. try {
  49. //直接通过反射set进去
  50. field.set(activity, view);
  51. } catch (IllegalAccessException e) {
  52. e.printStackTrace();
  53. }
  54. }
  55. }
  56. }
  57. }
  58. //点击事件的绑定
  59. private static void handleClickEvent(Method[] declaredMethods, final Activity activity) {
  60. if (declaredMethods == null || declaredMethods.length == 0) {
  61. return;
  62. }
  63. for (final Method method : declaredMethods) {
  64. //找到被Bind注解且无参的所有方法(注意这里限制无参是为了与下面调用method.invoke(activity)的无参保持一致)
  65. if (method.isAnnotationPresent(Bind.class) && method.getParameterTypes().length == 0) {
  66. Bind bind = method.getAnnotation(Bind.class);
  67. int id = bind.value();
  68. if (id != 0) {
  69. activity.findViewById(id).setOnClickListener(new View.OnClickListener() {
  70. @Override
  71. public void onClick(View v) {
  72. try {
  73. method.invoke(activity);
  74. } catch (IllegalAccessException e) {
  75. e.printStackTrace();
  76. } catch (InvocationTargetException e) {
  77. e.printStackTrace();
  78. }
  79. }
  80. });
  81. }
  82. }
  83. }
  84. }
  85. }

好了,到了这一步,我们的注解工作算是结束了,代码相对有点多,但这个是一劳永逸的。

  1. 接下来就是对注解的使用了
  1. package com.puke.annotationdemo;
  2. import android.app.Activity;
  3. import android.os.Bundle;
  4. import android.view.View;
  5. import android.widget.Button;
  6. import android.widget.EditText;
  7. import android.widget.Toast;
  8. @Bind(R.layout.activity_main)
  9. public class MainActivity extends Activity {
  10. @Bind(R.id.username)
  11. private EditText mUsername;
  12. @Bind(R.id.submit)
  13. private Button mSubmit;
  14. @Override
  15. protected void onCreate(Bundle savedInstanceState) {
  16. super.onCreate(savedInstanceState);
  17. BindHandler.handleBind(this);
  18. }
  19. @Bind(R.id.submit)
  20. public void submit() {
  21. String username = mUsername.getText().toString().trim();
  22. Toast.makeText(MainActivity.this, username, Toast.LENGTH_SHORT).show();
  23. }
  24. }

Run一下,完美运行~

我们这里可以看到,使用注解之后

setContentView方法没了

findViewById方法没了

setOnClickListener方法没了

MainActivity整个类减肥了

所有的所有,都让注解处理器一手承包了。而我们要做的是什么,要做的是真正应该由coder做的事情,在对应的地方加上对应的注解配置就ok了。

然后我们可以回过头看一下Bind这个注解,细心的同学可能发现注解声明value()的时候理论上来讲不应该有一个default为0的默认值。原因很简单啊,因为就目前的使用场景来看,无论注入一个layout还是一个id都不会为0,那这里干嘛还要再写一个default 0呢,直接不要default可以限制业务方的使用,强约束业务方一旦使用注解就必须要在注解里面set一个值进来。这里我要说明一下,我们写注解就是为了方便使用,快速开发,既然要懒,我们就懒到家,干脆就让我们的注解处理器能在业务方没有在Bind中注入值的时候也能生效。

就是要实现下面这种效果:

  1. package com.puke.annotationdemo;
  2. import android.app.Activity;
  3. import android.os.Bundle;
  4. import android.view.View;
  5. import android.widget.Button;
  6. import android.widget.EditText;
  7. import android.widget.Toast;
  8. @Bind
  9. public class MainActivity extends Activity {
  10. @Bind
  11. private EditText mUsername;
  12. @Bind
  13. private Button mSubmit;
  14. @Override
  15. protected void onCreate(Bundle savedInstanceState) {
  16. super.onCreate(savedInstanceState);
  17. BindHandler.handleBind(this);
  18. }
  19. @Bind
  20. public void submit() {
  21. String username = mUsername.getText().toString().trim();
  22. Toast.makeText(MainActivity.this, username, Toast.LENGTH_SHORT).show();
  23. }
  24. }

这样一来,只需要几个全裸的注解一顿狂注之后,就完事了。后面这种的实现方式我这里就不写了,大致说一下思路,注解处理器要制定类名—layout,属性名—id,方法名—id,这样一套转换标准出来,然后注解处理器的处理逻辑是先看业务方有没有手动注入,没有手动注入的情况(也就是前面提到的default 0)下,注解处理器再按照这套标准利用反射来取出对应的R类的对应资源值,只要找到对应的资源值,就和手动注入处理结果的完全一样。

三、 一些问题

每当一种事物出现时,只要不是太极端,总会有人拥护,也有人异议。单单站在coder的角度,这种注解的方式给我带来的好处是显而易见的,算是治愈代码密集恐惧症的偏方了。

但是值得深思的是,这里大量使用了反射,在Java中反射的性能问题总是尴尬的不要不要的。虽然jdk每次升级时基本上都在对反射进行优化,但是毕竟是反射,纯理论上讲,它确实没有直接的方法调用高效。

当然针对这个问题,我也有见过这样一种说法,假如我们对所谓”高效“的时间容忍度是1000t(t为一个时间粒度单位),直接方法调用耗时是1t,反射是50 - 200t。也就是说,反射是耗性能,是不效率,但是这个也只是相对与直接方法调用而言的,而还远远没达到我们对性能指标的容忍值。

我曾经也在高效开发和反射性能消耗之间纠结很久,在这里就不去过多评价,仁者见仁,智者见智了~

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注