Android Handler机制

JathonW

为什么要有Handler机制?

解决工作线程更新UI的问题。
由于在Android机制中,为了保证UI操作是线程安全的,规定只允许主线程更新Activity的UI组件。但在实际开发中存在多个线程并发操作UI组件的情况,会导致UI操作线程不安全。故采用Handler机制,当工作线程需要更新UI的时候,通过Handler通知主线程,从而在主线程中更新UI。

ps1:为什么不用锁呢?用锁会使UI的访问逻辑变得复杂,锁机制会降低UI访问的效率,锁会阻塞某些线程的执行。

Handler机制包含了什么?

重要概念

  • MainThread,UI线程,子线程(工作线程)
  • Handler 处理者
  • Looper 循环器
  • Message Queue 消息队列
  • Message 消息

UI线程与工作线程

UI线程就是APP启动时,就会开启一条ActivityThread线程,称之为主线程。

工作线程,则是在操作过程中,开启的线程,如网络请求线程等。

Handler

image.png

首先看一下该类的注释,就可以知道该类的功能和用处了。
Handler使你可以发送和处理与线程MessageQueue相关联的Message和Runnable。每个实例都与一个该线程的MessageQueue向关联。当创建Handler后,就会绑定MessageQueue。从绑定之后起,它就可以将Runnable和Message传入消息队列中,并在读取到对应消息时执行他们。

从描述中,我们可以提取到几个关键信息。Handler与线程绑定,与线程的MessageQueue绑定,大概率每个线程就只能有一个MessageQueue。Handler可以发送处理两种类型的数据,Message和Runnable。(这些后面具体描述)

Looper

image.png

Looper是为了为线程提供消息循环。默认情况下,线程没有Looper,可以使用prepare()获取循环,并使用loop()方法开始处理信息,只到循环停止。
与信息循环的大部分交互的都是通过Handler类。

MessageQueue

image.png
包含Looper要发送的信息列表的低级类。消息不是直接添加到MessageQueue中,而是由与Looper关联的Handler类添加的。

从描述中可知,MessageQueue从属于Looper。

Message

就是Handler处理的消息。其中有几个比较重要的属性:

  • target 发送和处理这个Message的Handler对象
  • callback 在主线程执行的回调
  • when 信息的传递时间
  • next 指向下一条Message(链表结构)

怎么使用Handler机制?

从前文可知,Handler需要绑定MessageQueue,而MessageQueue从属于Looper,所以从创建looper开始。

消息处理

一、创建Looper对象

  • 主线程 Looper.prepareMainLooper()
    主线程创建的时候会调用prepareMainLooper方法创建一个looper方法,所以在我们自己写代码时,无需在主线程创建looper。
 public static void prepareMainLooper() {
        prepare(false);
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();
        }
    }

我们可以发现,这里也调用了prepare方法,我们看一下prepare的方法。

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));
   }

这里出现了sThreadLocal字段,这个字段被static final修饰,说明是类共享的常量。(关于ThreadLocal具体讲解可以看上一篇文章)该常量,为每个线程都提供类Looper的副本。

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
ps2 : 如何保证一个线程中只有一个Looper? Looper的构造方法是private,只能在prepare中调用。并且如果一个ThreadLocal获取到相应的value,说明已经创建过。会抛出异常。
ps3:prepare的方法中携带一个布尔类型参数,用于判断是否可以退出循环。子线程的都为true,意味着可以退出;主线程的为false,意味着不可以退出。

接着来看看构造函数,每个looper对象都会绑定当前线程与一个消息队列
image.png

二、使Looper开始工作

当然就是调用loop()方法了。由于这段代码很长,我就截取一些,我个人认为较为关键的代码吧。

    public static void loop() {
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        final MessageQueue queue = me.mQueue;

        ......

        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }

            ......
            try {
                msg.target.dispatchMessage(msg);
                if (observer != null) {
                    observer.messageDispatched(token, msg);
                }
                dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
            } catch (Exception exception) {
                if (observer != null) {
                    observer.dispatchingThrewException(token, msg, exception);
                }
                throw exception;
            } finally {
                ThreadLocalWorkSource.restore(origWorkSource);
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
        ......
        }
    }
  • 首先获得当前的Looper的消息队列 MessageQueue queue = me.mQueue;
  • 然后开启死循环 for(::)
  • 通过消息队列获取MessageQueue获取信息 Message msg = queue.next();(可能造成阻塞)
  • 然后处理消息 msg.target.dispatchMessage(msg) 前文已知Message的target的就是发送和处理该对象的Handler。
ps4: 如何退出循环。looper调用quit方法,或者quitSafely方法后,队列就会放回msg == null,这样就可以结束循环。顾名思义,quit就是立即退出,而quitSafely则打上标志,当消息处理完全之后才结束循环。
ps5:为什么这里阻塞了?会不会影响主线程,或者影响CPU呢?这也是一个值得关注的问题,容后面分析。

三、MessageQueue读取数据

仍然还是贴出关键代码

Message next() {
        ......
        int nextPollTimeoutMillis = 0;
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }

            nativePollOnce(ptr, nextPollTimeoutMillis);

            synchronized (this) {
                // Try to retrieve the next message.  Return if found.
                // 获取当前时间 以便异步消息
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                
                // 消息屏障,查找是否有异步消息在队列中
                if (msg != null && msg.target == null) {
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                if (msg != null) {
                    // 处理延迟消息
                    if (now < msg.when) {
                        // Next message is not ready.  Set a timeout to wake up when it is ready.
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        // Got a message.
                        mBlocked = false;
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }

                ......
            }

           ......
        }
    }

不难发现nextPollTimeoutMillis是很重要的参数。正常循环时,此参数为0。当有延迟消息,消息没准备好时,则会设置一个延迟时间,让下一次循环执行。当message为空时,此参数会设置成-1。

nativePollOnce方法是一个本地方法,也是阻塞这个消息队列的方法。当前面参数为-1时,就会使消息队列陷入等待状态。

注意这里有个同步方法,锁住类MessageQueue对象。由于处理的消息,是由其他线程传入的,为了保证线程安全,就得Synchronized。

四、Handler.dispatchMessage(msg)

image.png

从前文得知Hanler应该可以处理两类数据?为什么这里就只有Message呢?后文再说,我们先看处理方法:
当message有回调时(实际上就是前文的Runnable的类型数据),就会调用这个回调的run方法。

 message.callback.run();

当msg.callback为空时,则此处会使用自定的hanlerMessage方法。

以上四步就是Handler机制处理消息的步骤了。那么Handler机制是如何上传Message的呢?

上传消息

一、重写Handler

从前文三种处理方法中,我们就可以复写出三种重写Handler方法。

  • 新建Handler子类,实现handlerMessage处理方法。(对应最后一种处理)
  • 匿名内部类,通过Callback实现handlerMessag。(对应倒数第二种处理)
  • 在post方法中传入Runnable(对应第三种处理)
ps6: Handler要在处理该Handler(一般为主线程)的线程中创建,然后在工作调用。
ps7:这里的Runnable接口,只是作为一种声明,而不是像传统的要开启一个新的线程,切记。
ps8:子线程中可以用MainLooper去创建Handler吗? 子线程中Handler handler = new Handler(Looper.getMainLooper());,此时两者就不在一个线程中。

二、创建消息对象

  • 创建Runnable对象,可以直接实现一个类,也可以直接在post方法内使用匿名内部类。
  • 创建Message对象

    • 创建举例
   Message msg = Message.obtain(); // 实例化消息对象
   msg.what = 1; // 消息标识
   msg.obj = "AA"; // 消息内容存放
* 
ps9: 创建用了不常见的obtain,而不是new,是因为Message自带缓冲池,避免每次都使用new重新分配内存,只有当线程池无对象时,才会new新对象。

三、发送信息到队列

对应两种类型数据,亦有两条路径:

  • post(Runnable r)

    • sendMessageDelayed(getPostMessage(r), 0); getPostMessage方法中,将Runnable接口封装成了message对象,并将r设置成类msg.callback。
    • sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    • enqueueMessage(queue, msg, uptimeMillis); 将msg与当前的Handler绑定,并将其插入队列中
    • queue.enqueueMessage(msg, uptimeMillis);这步较为关键,贴一下源码。这里使用的是单链表的增添方法,链表的好处是增删便捷,但是查询不便利,这边也很少查询的方法,故使用链表也较为合适。
boolean enqueueMessage(Message msg, long when) {
        ......

        synchronized (this) {
            ......

            msg.markInUse();
            msg.when = when;
            Message p = mMessages;
            boolean needWake;
            // 当当前队列为空时,唤醒等待的队列,并插入头结点
            if (p == null || when == 0 || when < p.when) {
                // New head, wake up the event queue if blocked.
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                // Inserted within the middle of the queue.  Usually we don't have to wake
                // up the event queue unless there is a barrier at the head of the queue
                // and the message is the earliest asynchronous message in the queue.
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                // 当当前队列不为空,则根据时间插入,到适当的位置
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;
            }

            // We can assume mPtr != 0 because mQuitting is false.
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }
  • sendMessage(Runnable r)

    • endMessageDelayed(msg, 0);
    • sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    • enqueueMessage(queue, msg, uptimeMillis);
    • queue.enqueueMessage(msg, uptimeMillis);

步骤基本同上。

ps10:如果需要调用延迟的话,调用postDelayed方法,最后底层还是和上面相似。
ps11:如果简单更新UI则直接使用Runnable即可(可能导致线程耦合度高)。若是要传递数据,则使用Message。

致此,怎么使用Handler机制已经讲述完成了。但是Handler机制的使用还是存在一些问题的,让我们继续探究。

Handler使用疑问

一、处理延迟消息:

1、MessageQueue.next()在取出Msg时,如果发现消息A有延迟且时间没到,会阻塞消息队列。
2、如果此时有非延迟的新消息B,会将其加入消息队列, 且处于消息A的前面,并且唤醒阻塞的消息队列。
3、唤醒后会拿出队列头部的消息B,进行处理。然后会继续因为消息A而阻塞。
4、如果达到了消息A延迟的时间,会取出消息A进行处理。

二、Looper 死循环为什么不会导致应用卡死?会消耗越来越多的资源嘛?

ActivityThread实质是只是App的入口类,而不是真正的线程。

对于线程即是一段可执行的代码,当可执行代码执行完成后,线程生命周期便该终止了,线程退出。而对于主线程,我们是绝不希望会被运行一段时间,自己就退出,那么如何保证能一直存活呢?简单做法就是可执行代码是能一直执行下去的,死循环便能保证不会被退出。

至于消耗资源,涉及到epoll机制(到IO的时候在继续讲解吧),该机制会再有数据到达时,才唤醒主线程工作。否则就让主线程休眠。

三、主线程的消息循环机制是什么?

此点不够清晰,以后详细研究。

四、Handler的内存泄漏问题。

  • 当Handler是非静态内部类或匿名Handler内部类时,会持有外部应用,这样会导致一个问题。当要销毁当前Activity时,可能有消息未处理完全,Message指向了Handler,而当前Handler又持有Activity,所以当前的Activity不能被回收,可能造成内存泄漏。
  • 解决办法:

    • 1、存在“未被处理 / 正处理的消息 -> Handler实例 -> 外部类” 的引用关系
      首先将Handler设置为静态内部类,然后持有Activity的弱引用实例。保证消息队列中所有消息都能执行。
    • 2、Handler的生命周期 > 外部类的生命周期
      在OnDestory方法中,清空Handle队列,并直接终止handler,mHandler.removeCallbacksAndMessages(null);

五、补充

队列中的内容(无论Message还是Runnable)可以要求马上执行,延迟一定时间执行或者指定某个时刻执行,如果将他们放置在队列头,则表示具有最高有限级别,立即执行。这些函数包括有:sendMessage(), sendMessageAtFrontOfQueue(), sendMessageAtTime(), sendMessageDelayed()以及用于在队列中加入Runnable的post(), postAtFrontOfQueue(), postAtTime(),postDelay()。

阅读 615

精致唯一

1 声望
1 粉丝
0 条评论
你知道吗?

精致唯一

1 声望
1 粉丝
宣传栏