自定义实现简单的 ButterKnife 框架

1,754 阅读16分钟

转载请标明出处:一片枫叶的专栏

目前在  友友用车  项目中使用到了ButterKnife框架,这是一个通过注解的方式简化程序员代码量,自动映射xml布局文件与对象关系的框架。使用了这个框架之后很大程度上简化程序员的工作量,提高了工作效率,让程序员们不在编写findViewById之类的代码,其github上的地址  ButterKnife。  最近也研究了一下ButterKnife的实现原理,下面我就将讲解一下其实现的机制。
这里首先简单介绍一下他的使用方式;android注解Butterknife的使用及代码分析

(一)使用方式

1)在activity中如何使用

@InjectView(R.id.feedback_content_edit)
    EditText feedContent; 

    @InjectView(R.id.feedback_contact_edit)
    EditText feedContact; 

    @InjectView(R.id.b3_button)
    Button feedOk; 

    @OnClick(R.id.open_car_door)
    public void openCarDorClick() {
        dosomething;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_feedback);
        ButterKnife.inject(this);

        initView();
    }

是不是很简单?其实这就是butterKnife的基本用法了

  • 通过注解(@InjectView)的方式将xml布局文件中定义的组件与Activity中定义的组件对象对应起来
  • 通过注解(@OnClick)实现对布局文件的点击事件

  • 在onCreate方法中通过静态方法:ButterKnife.inject(this);真正的将xml布局组件与activity中的组件对象映射起来;

2)在Fragment中如何使用

public class SimpleFragment extends Fragment {

    @InjectView(R.id.fragment_text_view)
    TextView mTextView;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_simple, container, false);
        ButterKnife.inject(this, view);
        mTextView.setText("TextView in Fragment are found!");
        return view;
    }
}

注意这里需要调用ButterKnife的重载方法:

public static void inject(Object target, View source) {
        inject(target, source, ButterKnife.Finder.VIEW);
    }

3)在Adapter的ViewHolder是如何使用的

static class ViewHolder {
        @InjectView(R.id.person_name)
        TextView name;
        @InjectView(R.id.person_age)
        TextView age;
        @InjectView(R.id.person_location)
        TextView location;
        @InjectView(R.id.person_work)
        TextView work;

        public ViewHolder(View view) {
            ButterKnife.inject(this, view);
        }
    }

可以发现ButterKnife的主要使用作用是简化我们加载xml布局文件的写法,通过注解的方式将内存对象与布局文件绑定,这对于懒程序员真是一个偷懒的利器啊。

那么ButterKnife的实现原理是怎样的呢?我们能否自己实现一个简单的ButterKnife框架呢?带着这两个问题,我们开始今天的自定义ButterKnife之旅。

预备知识点:

下面是一段关于java中注解的说明:

注解相当于一种标记,在程序中加了注解就等于为程序打上了某种标记,没加,则等于没有某种标记,以后,javac编译器,开发工具和其他程序可以用反射来了解你的类及各种元素上有无何种标记,看你有什么标记,就去干相应的事。标记可以加在包,类,字段,方法,方法的参数以及局部变量上。

具体关于java注解方面的内容我们可以参考:Java注解Annotation详解

下面一段是百度百科中关于java反射的说明:

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

具体关于java反射方面的内容我们可以参考:JAVA中的反射机制

在了解了java的注解和反射机制之后我们可以开始我们的自定义实现ButterKnife之旅了。

(二)绑定View组件;

@Target(ElementType.FIELD) 
@Retention(RetentionPolicy.RUNTIME) 
public @interface ViewBinder {
    int id() default -1;
}

@interface是用于自定义注解的,它里面定义的方法的声明不能有参数,也不能抛出异常,并且方法的返回值被限制为简单类型、String、Class、emnus、@interface,和这些类型的数组。
关于注解方面的内容,可自行查询学习;

/**
 * View解析类,主要用于解析含有注解的View成员变量
 */
public class ViewBinderParser {

    /**
     * 初始化解析
     * @param object
     */
    public static void inject(Object object) {
        
        ViewBinderParser parser = new ViewBinderParser();
        try {
            parser.parser(object);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 开始执行解析方法
     * @param object
     * @throws Exception
     */
    public void parser(final Object object) throws Exception{
        View view = null;
        
        final Class clazz = object.getClass();
        
        Field[] fields = clazz.getDeclaredFields();
        
        for (Field field : fields) {
            if (field.isAnnotationPresent(ViewBinder.class)) {
                ViewBinder inject = field.getAnnotation(ViewBinder.class);
                int id = inject.id();
                if (id < 0) {
                    throw new Exception("id must not be null!!!");
                }
                if (id > 0) {
                    field.setAccessible(true);
                    if (object instanceof View) {
                        view = ((View) object).findViewById(id);
                    } else if (object instanceof Activity) {
                        view = ((Activity) object).findViewById(id);
                    }
                    field.set(object, view);
                }
            }
        }
    }
}

这里可以看出,主要是通过创建一个解析对象,然后通过反射方法获取定义的成员变量,判断是否注解属性,如果是的话,则将注解的id值通过findViewById获取并给View对象赋值;

/**
 * 测试Activity,主要用于测试通过注册实现View组件的初始化过程
 */ 
public class MainActivity extends AppCompatActivity {

    @ViewBinder(id = R.id.button1)
    public Button button1;


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

        findViewById(R.id.button2).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                ViewBinderParser.inject(MainActivity.this);

                Log.i("tag", button1.getText() + " ####");
            }
        });

    }

}

可以发现当我们点击Button2的时候我们执行了Log.i方法,并将button1的text打印出来了,正式我们在布局文件中初始化的时候设置的text字符串,从而说明我们通过注解的方式实现了button1组件的初始化工作,初始化过程可能有一些地方有待优化,但这个其实就是butterKnife框架实现组件初始化工作的核心流程。

既然我们已经实现了组件的初始化工作,下面我们将尝试的绑定View组件的事件点击事件。

(三)绑定View的OnClick事件

(一)自定义OnClick注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OnClick {
    int id() default -1;
}

和设置View组件的初始化注解类似,这里定义了组件点击事件OnClick的注解。

(二)创建解析类,解析方法

/**
     * 解析成员方法
     * @param clazz
     * @throws Exception
     */
    public void parserMethod(Class clazz, final Object object) throws Exception{
        View view = null;
        
        Method[] methods = clazz.getDeclaredMethods();
        
        for (final Method method : methods) {
            if (method.isAnnotationPresent(OnClick.class)) {
                OnClick inject = method.getAnnotation(OnClick.class);
                int id = inject.id();
                if (id < 0) {
                    throw new Exception("id must not be null!!!");
                }
                if (id > 0) {
                    if (object instanceof View) {
                        view = ((View) object).findViewById(id);
                    } else if (object instanceof Activity) {
                        view = ((Activity) object).findViewById(id);
                    }
                    view.setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            try {
                                method.invoke(object, null);
                            } catch (IllegalAccessException e) {
                                e.printStackTrace();
                            } catch (InvocationTargetException e) {
                                e.printStackTrace();
                            }
                        }
                    });
                }
            }
        }
    }

(三)在activity中测试解析

/**
 * 测试Activity,主要用于测试组件的点击事件
 */ 
public class MainActivity extends AppCompatActivity {

    @ViewBinder(id = R.id.button1)
    public Button button1;

    /**
     * 执行button1的点击事件
     */
    @OnClick(id = R.id.button1)
    public void button1OnClick() {
        Log.i("tag", "这是一个测试的例子");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ViewBinderParser.inject(MainActivity.this);
    }

}

可以发现我们在Activity中为button1OnClick方法绑定了组件id,这样就实现了button1组件的事件绑定定义,然后我们在Activity的onCreate方法执行了ViewBinderParser.inject方法,这样就真正实现了对组件的事件绑定,当我们点击button1的时候执行了Log.i方法,并打印:”这是一个测试的例子”,说明我们组件绑定操作执行成功了。O(∩_∩)O哈哈~

总结:

  • 本文主要分析了一下ButterKnife的简单实用与实现原理,并实际实现了一个简单的ButterKnife框架。

  • 已经将自己实现的ButterKnife框架上传至  我的github中  感兴趣的同学可以star和follow。

  • 自己实现简单的ButterKnife框架主要涉及到了java中的注解和反射相关知识。


另外对github项目,开源项目解析感兴趣的同学可以参考我的:
github项目解析(一)–>上传android项目至github
github项目解析(二)–>将Android项目发布至JCenter代码库
github项目解析(三)–>android内存泄露监测之leakcanary
github项目解析(四)–>动态更改TextView的字体大小
github项目解析(五)–>android日志框架


转载请标明出处:一片枫叶的专栏

上一篇文章中我们讲解了android app实现长连接的几种方式,各自的优缺点以及具体的实现,一般而言使用第三方的推送服务已经可以满足了基本的业务需求,当然了若是对技术有追求的可以通过NIO或者是MINA实现自身的长连接服务,但是自己实现的长连接服务一来比较复杂耗时比较多,而且可能过程中有许多坑要填,一般而言推荐使用第三方的推送服务,稳定简单,具体管理长连接部分的模块可参考:android产品研发(十二)–>App长连接实现

而本文将讲解app端的轮询请求服务,一般而言我们经常将轮询操作用于请求服务器。比如某一个页面我们有定时任务需要时时的从服务器获取更新信息并显示,比如当长连接断掉之后我们可能需要启动轮询请求作为长连接的补充等,所以这时候就用到了轮询服务。

什么是轮询请求

在说明我们轮询请求之前,这里先说明一下什么叫轮询请求,我的理解就是App端每隔一定的时间重复请求的操作就叫做轮询请求,比如:App端每隔一段时间上报一次定位信息,App端每隔一段时间拉去一次用户状态等,这些应该都是轮询请求,那么前一篇我们讲了App端的长连接,为什么我们有了长连接之后还需要轮询操作呢?

这是因为我们的长连接并不是稳定的可靠的,而我们执行轮询操作的时候一般都是要稳定的网络请求,而且轮询操作一般都是有生命周期的,即在一定的生命周期内执行轮询操作,而我们的长连接一般都是整个进程生命周期的,所以从这方面讲也不太适合。

轮询请求实践

与长连接相关的轮询请求

  1. 上一篇我们在讲解长连接的时候说过长连接有可能会断,而这时候在长连接断的时候我们就需要启动一个轮询服务,它作为长连接的补充。
/**
     * 启动轮询服务
     */
    public void startLoopService() {
        
        
        if (!LoopService.isServiceRuning) {
            
            if (UserConfig.isPassLogined()) {
                
                if (MinaLongConnectManager.session != null && MinaLongConnectManager.session.isConnected()) {
                    LoopService.quitLoopService(context);
                    return;
                }
                LoopService.startLoopService(context);
            } else {
                LoopService.quitLoopService(context);
            }
        }
    }

这里就是我们执行轮询服务的操作代码,其作用就是启动了一个轮询service(即轮询服务),然后在轮询服务中执行具体的轮询请求,既然这样我们就具体看一下这个service的代码逻辑。

/**
 * 长连接异常时启动服务,长连接恢复时关闭服务
 */
public class LoopService extends Service {

    public static final String ACTION = "com.youyou.uuelectric.renter.Service.LoopService";

    /**
     * 客户端执行轮询的时间间隔,该值由StartQueryInterface接口返回,默认设置为30s
     */
    public static int LOOP_INTERVAL_SECS = 30;
    /**
     * 轮询时间间隔(MLOOP_INTERVAL_SECS 这个时间间隔变量有服务器下发,此时轮询服务的场景与逻辑与定义时发生变化,涉及到IOS端,因此采用自己定义的常量在客户端写死时间间隔)
     */
    public static int MLOOP_INTERVAL_SECS = 30;
    /**
     * 当前服务是否正在执行
     */
    public static boolean isServiceRuning = false;
    /**
     * 定时任务工具类
     */
    public static Timer timer = new Timer();

    private static Context context;

    public LoopService() {
        isServiceRuning = false;
    }

    

    /**
     * 启动轮询服务
     */
    public static void startLoopService(Context context) {
        if (context == null)
            return;
        quitLoopService(context);
        L.i("开启轮询服务,轮询间隔:" + MLOOP_INTERVAL_SECS + "s");
        AlarmManager manager = (AlarmManager) context.getApplicationContext().getSystemService(Context.ALARM_SERVICE);
        Intent intent = new Intent(context.getApplicationContext(), LoopService.class);
        intent.setAction(LoopService.ACTION);
        PendingIntent pendingIntent = PendingIntent.getService(context.getApplicationContext(), 1, intent, PendingIntent.FLAG_UPDATE_CURRENT);
        
        /**
         * 闹钟的第一次执行时间,以毫秒为单位,可以自定义时间,不过一般使用当前时间。需要注意的是,本属性与第一个属性(type)密切相关,
         * 如果第一个参数对应的闹钟使用的是相对时间(ELAPSED_REALTIME和ELAPSED_REALTIME_WAKEUP),那么本属性就得使用相对时间(相对于系统启动时间来说),
         *      比如当前时间就表示为:SystemClock.elapsedRealtime();
         * 如果第一个参数对应的闹钟使用的是绝对时间(RTC、RTC_WAKEUP、POWER_OFF_WAKEUP),那么本属性就得使用绝对时间,
         *      比如当前时间就表示为:System.currentTimeMillis()。
         */
        manager.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), MLOOP_INTERVAL_SECS * 1000, pendingIntent);
    }

    /**
     * 停止轮询服务
     */
    public static void quitLoopService(Context context) {
        if (context == null)
            return;
        L.i("关闭轮询闹钟服务...");
        AlarmManager manager = (AlarmManager) context.getApplicationContext().getSystemService(Context.ALARM_SERVICE);
        Intent intent = new Intent(context.getApplicationContext(), LoopService.class);
        intent.setAction(LoopService.ACTION);
        PendingIntent pendingIntent = PendingIntent.getService(context.getApplicationContext(), 1, intent, PendingIntent.FLAG_UPDATE_CURRENT);
        manager.cancel(pendingIntent);
        
        L.i("关闭轮询服务...");
        context.stopService(intent);
    }

    @Override
    public void onCreate() {
        super.onCreate();

        context = getApplicationContext();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        L.i("开始执行轮询服务... \n 判断当前用户是否已登录...");
        
        if (UserConfig.isPassLogined()) {
            
            L.i("当前用户已登录... \n 判断长连接是否已经连接...");
            if (MinaLongConnectManager.session != null && MinaLongConnectManager.session.isConnected()) {
                L.i("长连接已恢复连接,退出轮询服务...");
                quitLoopService(context);
            } else {
                if (isServiceRuning) {
                    return START_STICKY;
                }
                
                startLoop();
            }
        } else {
            L.i("用户已退出登录,关闭轮询服务...");
            quitLoopService(context);
        }
        return START_STICKY;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        L.i("轮询服务退出,执行onDestory()方法,inServiceRuning赋值false");
        isServiceRuning = false;
        timer.cancel();
        timer = new Timer();
    }

    @Override
    public IBinder onBind(Intent intent) {
        throw new UnsupportedOperationException("Not yet implemented");
    }

    /**
     * 启动轮询拉去消息
     */
    private void startLoop() {
        if (timer == null) {
            timer = new Timer();
        }
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                isServiceRuning = true;
                L.i("长连接未恢复连接,执行轮询操作... \n 轮询服务中请求getInstance接口...");
                LoopRequest.getInstance(context).sendLoopRequest();
            }
        }, 0, MLOOP_INTERVAL_SECS * 1000);
    }
}

可以发现这里的service轮询服务的代码量还是比较多的,但是轮询服务请求代码注释已经很详细了,所以就不做过多的说明,需要说明的是其核心就是通过Timer对象每个一段时间执行一次网络请求。具体的网络请求代码:

L.i("长连接未恢复连接,执行轮询操作... \n 轮询服务中请求getInstance接口...")

LoopRequest.getInstance(context).sendLoopRequest()

这里的轮询服务请求核心逻辑:当长连接出现异常时,启动轮询服务,并通过Timer对象每隔一定时间拉取服务器状态,当长连接恢复时,关闭轮询服务。这就是我们与长连接有关的轮询服务的代码执行逻辑,看完这部分之后我们再看一下与页面相关的轮询请求的执行逻辑。

与页面相关的轮询请求

  • 与页面相关的轮询请求
    我们的App中当用户停留在某一个页面的时候我们可能需要定时的拉取用户状态,这时候也需要使用轮询请求拉取服务器状态,当用户离开该页面的时候关闭轮询服务请求。

这里我们看一下我们产品当前行程页面的轮询操作,用于轮询请求当前用户的车辆里程,费用,用时等信息,具体可参考下图:

其实在当前Fragment页面有一个定时的拉去订单信息的轮询请求,下面我们具体看一下这个定时请求的执行逻辑:

/**
 * TimerTask对象,主要用于定时拉去服务器信息
 */
public class Task extends TimerTask {
        @Override
        public void run() {
            L.i("开始执行执行timer定时任务...");
            handler.post(new Runnable() {
                @Override
                public void run() {
                    isFirstGetData = false;
                    getData(true);
                }
            });
        }
    }

而这里的getData方法就是拉去服务器状态的方法,这里不做过多的解释,当用户退出这个页面的时候需要清除这里的轮询操作。所以在Fragment的onDesctoryView方法中执行了清除timerTask的操作。

@Override
    public void onDestroyView() {
        super.onDestroyView();
        ...
        if (timer != null) {
            timer.cancel();
            timer = null;
        }
        if (timerTask != null) {
            timerTask.cancel();
            timerTask = null;
        }
        ...
    }

这样当用户打开这个页面的时候初始化TimerTask对象,每个一分钟请求一次服务器拉取订单信息并更新UI,当用户离开页面的时候清除TimerTask对象,即取消轮询请求操作。可以发现上面我们看到的与长连接和页面相关的轮询请求服务都是通过timer对象的定时任务实现的轮询请求服务,下面我们看一下如何通过Handler对象实现轮询请求服务。

通过Handler对象实现轮询请求

  • 下面我们来看一个通过Handler异步消息实现的轮询请求服务。
/**
     * 默认的时间间隔:1分钟
     */
    private static int DEFAULT_INTERVAL = 60 * 1000;
    /**
     * 异常情况下的轮询时间间隔:5秒
     */
    private static int ERROR_INTERVAL = 5 * 1000;
    /**
     * 当前轮询执行的时间间隔
     */
    private static int interval = DEFAULT_INTERVAL;
    /**
     * 轮询Handler的消息类型
     */
    private static int LOOP_WHAT = 10;
    /**
     * 是否是第一次拉取数据
     */
    private boolean isFirstRequest = false;
    /**
     * 第一次请求数据是否成功
     */
    private boolean isFirstRequestSuccess = false;

    /**
     * 开始执行轮询,正常情况下,每隔1分钟轮询拉取一次最新数据
     * 在onStart时开启轮询
     */
    private void startLoop() {
        L.i("页面onStart,需要开启轮询");
        loopRequestHandler.sendEmptyMessageDelayed(LOOP_WHAT, interval);
    }

    /**
     * 关闭轮询,在界面onStop时,停止轮询操作
     */
    private void stopLoop() {
        L.i("页面已onStop,需要停止轮询");
        loopRequestHandler.removeMessages(LOOP_WHAT);
    }

    /**
     * 处理轮询的Handler
     */
    private Handler loopRequestHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {

            
            if (!isFirstRequestSuccess) {
                L.i("首次请求失败,需要将轮询时间设置为:" + ERROR_INTERVAL);
                interval = ERROR_INTERVAL;
            } else {
                interval = DEFAULT_INTERVAL;
            }

            L.i("轮询中-----当前轮询间隔:" + interval);

            loopRequestHandler.removeMessages(LOOP_WHAT);

            
            if (!isFirstRequestSuccess || !Config.locationIsSuccess()) {
                isClickLocationButton = false;
                doLocationOption();
            } else {
                loadData();
            }

            System.gc();

            loopRequestHandler.sendEmptyMessageDelayed(LOOP_WHAT, interval);

        }
    };

这里是通过Handler实现的轮询操作,其核心原理就是在handler的handlerMessage方法中,接收到消息之后再次发送延迟消息,这里的延迟时间就是我们定义的轮询间隔时间,这样当我们下次接收到消息的时候又一次发送延迟消息,从而造成我们时时发送轮询消息的情景。

以上就是我们实现轮询操作的两种方式:

  • Timer对象实现轮询操作

  • Handler对象实现轮询操作

上面我们分析了轮询请求的不同使用场景,作用以及实现方式,当我们在具体的开发过程中需要定时的向服务器拉取消息的时候就可以考虑使用轮询请求了。

总结:

  • 轮询操作一般都是通过定时请求服务器拉取信息并更新UI;

  • 轮询操作一般都有一定的生命周期,比如在某个页面打开时启动轮询操作,在某个页面关闭时取消轮询操作;

  • 轮询操作的请求间隔需要根据具体的需求确定,间隔时间不宜过短,否则可能造成并发性问题;

  • 产品开发过程中,某些需要试试更新服务器拉取信息并更新UI时,可以考虑使用轮询操作实现;

  • 可以通过Timer对象和Handler对象两种方式实现轮询请求操作;


另外对产品研发技术,技巧,实践方面感兴趣的同学可以参考我的:
android产品研发(一)–>实用开发规范
android产品研发(二)–>启动页优化
android产品研发(三)–>基类Activity
android产品研发(四)–>减小Apk大小
android产品研发(五)–>多渠道打包
android产品研发(六)–>Apk混淆
android产品研发(七)–>Apk热修复
android产品研发(八)–>App数据统计
android产品研发(九)–>App网络传输协议
android产品研发(十)–>不使用静态变量保存数据
android产品研发(十一)–>应用内跳转scheme协议
android产品研发(十二)–>App长连接实现

本文以同步至github中:github.com/yipianfengy…,欢迎star和follow