Android(安卓)Handler机制
16lz
2021-12-04
概述
从开发角度角度来说,Handler是Android消息机制的上层接口,通过handler,可以将一个任务切换到handler所在的线程中执行,我们通常使用handler来更新UI,但更新UI仅仅是的使用场景之一,handler并不是仅仅用来更新UI。 更新UI的具体情况是这样的:和其他GUI库一样,Android的UI也是线程不安全的,也就是说想要更新应用程序中的UI元素,则必须在主线程中进行。所以主线程又叫做UI线程。若在子线程中更新UI程序会报CalledFromWrongThreadException错误。但是我们经常有这样一种需求:需要在子线程中完成一些耗时任务后根据任务执行结果来更新相应的UI。这就需要子线程在执行完耗时任务后向主线程发送消息,主线程来更新UI。使用方法
Handler类包含如下方法,用于发送、处理消息- void handleMessage(Message msg):处理消息的方法。该方法在创建Handler时重写。处理消息
- final boolean hasMessage(int what):检查消息队列中是否包含what属性为指定值(参数中的what)的消息
- final boolean hasMessage(int what,Object object):检查消息队列中是否包含what属性值为指定值且object属性值为指定对象的消息
- 多个重载的 Message obtainMessage():获取Message,经常用于创建一个Message对象。例如obtainMessage(int what,Object object)。
- sendEmptyMessage(int what):发送空消息
- final sendEmptyMessageDelayed(int what,long delayMills):指定delayMills毫秒后发送空消息。
- final boolean sendMessage(Message msg):立即发送消息
- final boolean sendMessageDelayed(Message msg,long delayMills):指定delayMill毫秒后发送消息
Demo_1
//Thread_Main: private final Handler mHandler = new Handler(){ //接收消息 @Override public void handleMessage(Message msg){ if(msg.what == “MSG_1”){ //取出消息中的信息 localvar = msg.object; }else if(msg.what == “MSG_NULL”){ Log.i(“接收到空消息”); } }};//Thread_1//创建messageMessage msg = new Message();//设置参数msg.what=”MSG_1”;msg.obj = new String(“message_1”);//发送消息mHandler.sendMessage(msg);mHandler.sendEmptyMessage(“MSG_NULL”);
Demo_2
private final Handler mHandler = new Handler(){ //接收消息 @Override public void handleMessage(Message msg){ if(msg.what == “MSG_PRESON_INFO”){ //取出消息中的信息 SomeArgs args = (SomeArgs)msg.obj; id = args.arg1; name = args.arg2;.}}};Thread_1SomeArgs args = SomeArgs.obtain();args.arg1 = idFromWeb;args.arg2 = nameFromWeb;mHandler.obtainMessage(“MSG_PRESON_INFO”,args).sendToTarget();//其实在Handler类中的obtainMessage方法中也是new了一个Message对象,设置参数,进行了封装。
Handler工作原理
本节围绕handler的工作过程来分析Android的消息机制,主要包括handler、MessageQueue和Looper,同时也会介绍TreadLocal 首先介绍一下与Handler一起工作的组件- Message:Handler接收和处理的对象
- MessageQueue:消息队列,用于存放Handler发送的消息。这些消息会存在于消息队列中,等待被处理。每个线程中只会有一个MessageQueue。
- Lopper:MessageQueue的管家,调用Lopper的loop()方法后,就会进入到一个无限循环中,发现 MessageQueue中有消息就会取出来传递到Handler的handlerMessage方法中。
下面我们分别介绍组成handler工作的各个组件
ThreadLocal
ThreadLocal是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,存储后只有在指定线程中获取到存储的数据,其他线程则无法获取到数据。 当某些数据是以线程为作用域并且不同线程具有不同的数据副本时,就可以考虑使用ThreadLocal,比如对于handler来说,它需要获取当前线程的looper,这时可以通过ThreadLocal来实现looper的存取,若没有ThreadLocal,则系统需要提供一张哈希表供handler查找指定线程的looper。ThreadLocal相当于一个容器,存储着每个线程的数据, 且所有线程都共享这一个ThreadLocal变量,但每个线程访问该对象时会获得不同的数据 ,而且就算修改这些数据也不会影响到其他线程,下面我们通过一个例子来说明它的使用方法。 //定义一个ThreadLocal,保存数据类型为布尔类型private ThreadLocalmBooleanThreadLocal = new ThreadLocal(); mBooleanThreadLocal.set(true); Log.d(TAG, "[Thread#main]mBooleanThreadLocal=" + mBooleanThreadLocal.get()); new Thread("Thread#1") { @Override public void run() { mBooleanThreadLocal.set(false); Log.d(TAG, "[Thread#1]mBooleanThreadLocal=" + mBooleanThreadLocal.get()); }; }.start(); new Thread("Thread#2") { @Override public void run() { Log.d(TAG, "[Thread#2]mBooleanThreadLocal=" + mBooleanThreadLocal.get()); }; }.start(); //result//[Thread#main]mBooleanThreadLocal=true//[Thread#1]mBooleanThreadLocal=false//[Thread#2]mBooleanThreadLocal=null
从Demo中我们可以看出,三个线程(mainThread、Thread1、Thread2)可以共享同一个TreadLocal。在主线程中设置为true,那么在主线程中get值为true,在Thread1中set为false,那么在Thread2中get便为false,在Thread2中没有set,那么获取为null。 我们可以看到定义时可以看到,ThreadLocal可以使用泛型定义,这样我们就可以根据我们的业务定义不同的数据。 ThreadLocal工作原理
我们从ThreadLocal.set()和ThreadLocal.get()方法入手,解析其工作原理 首先看set方法 public void set(T value) { Thread currentThread = Thread.currentThread(); Values values = values(currentThread); if (values == null) { values = initializeValues(currentThread); } values.put(this, value); }
首先获取当前线程,通过values()方法获取当前线程的threadLocal数据。接着看values()方法: /** * Gets Values instance for this thread and variable type. */ Values values(Thread current) { return current.localValues; }
由此我们可以看出,线程类Thread中保存了localValues字段,查看Thread源码,确实存在该字段,Thread类中相关代码如下: /** * Normal thread local values. */ ThreadLocal.Values localValues;
Thread类中并没有封装对localValues字段进行操作的方法,外部可以直接对该字段进行赋值操作 我们回到set方法中,若通过values()方法获取Thread.localValues为null,则对通过initializeValues()方法对当前线程的localValues字段进行赋值,如下: /** * Creates Values instance for this thread and variable type. */ Values initializeValues(Thread current) { return current.localValues = new Values(); }
set方法中,初始化好当前线程的localValues方法后,调用该 localValues.put(this,value) 方法,注意,这里的this并非刚刚为当前线程创建的localValues,而是客户端定义:private ThreadLocal void put(ThreadLocal<?> key, Object value) { cleanUp(); // Keep track of first tombstone. That's where we want to go back // and add an entry if necessary. int firstTombstone = -1; for (int index = key.hash & mask;; index = next(index)) { Object k = table[index]; if (k == key.reference) { // Replace existing entry. table[index + 1] = value; return; } if (k == null) { if (firstTombstone == -1) { // Fill in null slot. table[index] = key.reference; table[index + 1] = value; size++; return; } // Go back and replace first tombstone. table[firstTombstone] = key.reference; table[firstTombstone + 1] = value; tombstones--; size++; return; } // Remember first tombstone. if (firstTombstone == -1 && k == TOMBSTONE) { firstTombstone = index; } } }
上面的代码实现数据的存储过程,这里不去分析它的具体算法。而是分析它的原理:当前线程的localValues,保存了用户自定义的ThreadLocal(这里是mBooleanThreadLocal)key与用户在当前线程通过mBooleanThreadLocal.set()保存的值。保存方式采用数组:若key.reference在数组中下标值为index(通过掩码计算获得),那么value值的下标值为index+1。例如用户又通过ThreadLocal public T get() { // Optimized for the fast path. Thread currentThread = Thread.currentThread(); Values values = values(currentThread); if (values != null) { Object[] table = values.table; int index = hash & values.mask; if (this.reference == table[index]) { return (T) table[index + 1]; } } else { values = initializeValues(currentThread); } return (T) values.getAfterMiss(this); }
获取时,我们在当前线程调用mBooleanThreadLocal.get() 首先获取当前线程的localValues,若localValues不为null,那么说明在线程中调用过mBooleanThreadLocal.set(set时,会初始化当前线程的localValues字段),那么就从当前线程localValues的table数组中查询mBooleanThreadLocal.reference的数组下标index,并返回值:table[index+1]。 若当前线程localValues为null,则初始化该线程localValues字段,并返回其getAfterMiss(this),其中返回initialValue()返回值,其默认实现为返回null,getAfterMiss(this)方法部分代码如下: /** * Gets value for given ThreadLocal after not finding it in the first * slot. */ Object getAfterMiss(ThreadLocal<?> key) { Object[] table = this.table; int index = key.hash & mask; // If the first slot is empty, the search is over. if (table[index] == null) { Object value = key.initialValue(); // If the table is still the same and the slot is still empty... if (this.table == table && table[index] == null) { table[index] = key.reference; table[index + 1] = value; size++; cleanUp(); return value; } // The table changed during initialValue(). put(key, value); return value; }.......
initialValue()方法如下: /** * Provides the initial value of this variable for the current thread. * The default implementation returns {@code null}. * * @return the initial value of the variable. */ protected T initialValue() { return null; }
我们可以复写该方法 理解ThreadLocal的工作原理,有助于理解HandLer原理。 在Handler原理中,ThreadLocal保存了当前线程的Looper对象 消息队列(MessageQueue)的工作原理 消息队列指Android中的MessageQueue,它主要包括两个操作:插入和读取,读取本身会伴随删除操作。分别对应的方法是,插入:enqueueMessage,作用是向消息队列中插入一条消息,读取:next,作用是从消息队列中取出一条消息并从消息队列中删除,MessageQueue的实现并不是队列,而是单链表。需要提到的是,next方法实际上是一个无限循环,若队列中没有消息,next会一直阻塞在这里,当有新消息到来,next方法会返回这条消息并将消息从单链表中删除。
Looper的工作原理
Looper在Android机制中扮演着消息循环的角色,具体说它会不停地从MessageQueue中查看是否有新消息,如果有就处理,否则就一直阻塞在那里,首先看它的构造方法,会在构造方法中new一个消息队列,并保存当前线程。构造方法如下: private Looper(boolean quitAllowed) { mQueue = new MessageQueue(quitAllowed); mThread = Thread.currentThread(); }
我们注意到,该构造方法是私有的,我们不能从外部直接构造Looper ,那么如何为一个线程创建Looper呢?其实很简单,通过Looper.prepare()可以为当前线程创建Looper,接着通过Looper.loop开启循环。perpare()方法如下所示: /** Initialize the current thread as a looper. * This gives you a chance to create handlers that then reference * this looper, before actually starting the loop. Be sure to call * {@link #loop()} after calling this method, and end it by calling * {@link #quit()}. */ public static void prepare() { prepare(true); } private static void prepare(boolean quitAllowed) { if (sThreadLocal.get() != null) { throw new RuntimeException("Only one Looper may be created per thread"); } sThreadLocal.set(new Looper(quitAllowed)); }
prepare()方法中,便可看到上一章中讲述的ThreadLocal的影子。 创建一个Looper,并通过ThreadLocal与当前线程绑定。在方法myLooper中,则返回的是当前线程的Looper,如下: /** * Return the Looper object associated with the current thread. Returns * null if the calling thread is not associated with a Looper. */ public static @Nullable Looper myLooper() { return sThreadLocal.get(); }
除此之外,还提供了prepareMainLooper方法,这个方法是给主线程也就是ActivityThread方法创建Looper,还提供了getMainLooper来在任何地方获取主线程的Looper。Looper是可以退出的,有两种方法,quit():直接退出Looper,quitSafely:只是设定一个退出标记,然后把消息队列中已有消息处理完再退出。Looper退出后,通过Handler发送的消息会失败,因此在子线程中手动创建Looper后,处理完所有事情后应该quit退出,不然子线程会一直处于等待状态。代码这里就不贴了,感兴趣的同学可以查看Looper.java源码Looper.loop()开启循环: Handler的工作原理 Handler的工作主要包括发送消息和接收处理,发送消息可以通过post的一系列方法以及send方法来实现。Handler的发送消息仅仅是向MessageQueue插入了一条消息,MessageQueue的next的方法会将该消息返回给Looper,Looper收到该消息后就开始处理了,最终消息交由Handler处理,即Handler的dispatchMessage方法会被调用,这时,就进入了消息处理阶段,dispatchMessage方法如下:
/** * Handle system messages here. */ public void dispatchMessage(Message msg) { if (msg.callback != null) { handleCallback(msg); } else { if (mCallback != null) { if (mCallback.handleMessage(msg)) { return; } } handleMessage(msg); } }
首先检查Message的callback是否为null,不为null就通过该callback处理消息,Message的callback实际上是一个Runnable对象,实际上就是Handler.post()方法所传递的Runnable对象,handlerCallback(msg)也很简单,就是调用runnable.run()。 其次,检查mCallback是否为null,不为null就调用mCallback.handleMessage处理消息。Callback是handler内部的接口,它有什么作用呢?通过它可使用如下方法创建handler:Handler handler = new Handler(callback),日常开发中,我们一般通过派生handler子类并重写handler方法来创建handler,callback为我们提供了创建handler的另一种方式。 最后调用handler的handleMessage()方法,也就是我们通常创建时重写的方法。 可以归纳为流程图: 下一篇文章,讲解一个handler应用小例子。一般,我们使用handler都是子线程完成工作通过handler像主线程发送消息,主线程更新UI。我们尝试从主线程向子线程发送消息。这也是我遇到过的面试题之一。
参考资料:
https://blog.csdn.net/singwhatiwanna/article/details/48350919
《Android开发艺术探索》
更多相关文章
- 浅谈Java中Collections.sort对List排序的两种方法
- Python list sort方法的具体使用
- python list.sort()根据多个关键字排序的方法实现
- Android高性能编程(1)--基础篇
- 初学Android,跨进程调用Service(六十)
- webapp打包为Android的apk包的一种方法
- Android消息传递之组件间传递消息
- JS调用Android里面的方法,Android调用JS里面的方法
- 【原创】Android锁定横竖屏、splash,全屏、去除标题的方法