Android消息机制–Handler

handler消息机制

每个Android应用在被启动时都会创建一个线程,这个线程被称为主线程或者UI线程,Android应用的所有操作默认都会运行在这个线程中。但是为了保证UI的流畅性,通常都会将耗时的操作放到子线程中,例如IO操作、网络请求等。而在Android 3.0以及以后的版本中,Android甚至禁止在主线程中进行网络请求,否则会抛出异常,可见在UI线程中执行耗时操作是非常不推荐的行为。

Android 默认约定当UI线程阻塞超过20秒时将会引起ANR(Android Not Responding)异常。但是实际上,不要说是20秒,即使是5秒甚至更短,用户都会觉得十分的不爽,用户体验极差,所以切记不要再UI线程里面执行一些耗时的操作。

由于Android开发规范的限制,出于线程安全的考虑,规定只允许UI线程修改Activity里的UI组件。在子线程中不能访问UI控件,否则会触发异常。

系统为什么不允许在子线程中访问UI呢?
这是因为Android的UI线程并不是线程安全的,如果在多线程中并发访问可能导致UI控件处于不可预期的状态,那么为什么系统不对UI控件的访问加上锁机制呢?缺点有两个:首先加上锁机制会让UI访问的逻辑变得复杂;其次锁机制会阻塞某些线程的执行。鉴于这两个缺点,最简单有效的方法就是采用单线程模型来处理UI操作,对于开发者来说,只需要进行线程切换就可以了。

通过上面介绍我们知道:

  1. 不能在UI线程里执行耗时操作
  2. 只允许UI线程修改Activity里的UI组件

我们需要进行一个耗时操作,我们将其放进子线程中,处理后将结果更新到UI线程中的组件上,由于上面两个问题的限制,我们怎么实现?没错,就是接下来讲解的Handler。下面我们由浅入深一步步揭开Handler的面纱。

Handler 类介绍

Android的消息机制主要是指Handler的运行机制,Handler的运行需要底层的MessageQueue和Looper的支持。MessageQueue顾名思义是用来存储消息Message的。

Message

Message是在线程之间传递的消息,它可以在内部携带少量的信息,用于在不同线程之间交换数据。

Handler

顾名思义是处理者的意思,它的主要作用是发送和处理消息。发送消息一般是Handler的sendMessage()方法,而发送的消息经过一系列的辗转处理后,最终会投递到Handler的handleMessage()方法中。

MessageQueue

MessageQueue是消息队列的意思。它的内部存储了一组消息,以队列的形式对外提供插入和删除的工作。虽然队列,但是内部的存储结构不是真正的队列,而是采用单链表的数据结构来存储消息列表的。它存储着所有通过Handler发送的信息。这部分消息会一直存在消息队列中,等待被处理。每个线程中只会有一个MessageQueue对象。

Looper

Looper是每个线程中的MessageQueue的管家,调用Looper的loop()方法后,就会进入到一个无限循环当中,然后每当发现MessageQueue中存在一条消息,就会将它取出,并传递到Handler的handleMessage()方法中。每个线程中只会有一个Looper对象。

了解了MessageQueue、Handler、Looper等部分的概念,我们梳理一下Handler消息机制的流程。

首先需要在主线程中创建一个Handler对象,并重写handleMessage()方法。然后当子线程需要进行UI操作时,就创建一个Message对象,并通过Handler将这条消息发送出去。之后这条消息被添加到MessageQueue的队列中等待被处理。而Looper会一直尝试从MessageQueue中取出待处理的消息,最后分发会Handler的handleMessage()方法中。由于Handler是在主线程中创建的,所以此时handleMessage()方法中的代码也会运行在主线程中,于是我们可以在这里安心的进行UI操作了。整个流程示意图如下

Handler消息机制流程

 

一条Message经过这样一个流程的辗转调用后,从子线程到主线程,从不能更新UI到能更新UI。

Handler 简单实践

上面我们基本了解了Handler的消息机制,下面我们通过一个简单的Dmeo体会一下Handler的妙用。

acitivity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" >

    <Button android:id="@+id/button" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="change_text"/>

    <TextView android:id="@+id/m_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="Hello World!" android:textSize="25sp" />

</RelativeLayout>

MainActivity.java

public class MainActivity extends AppCompatActivity implements View.OnClickListener{
    private TextView mTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView = findViewById(R.id.m_text);
        Button button = findViewById(R.id.button);
        button.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.button:
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        mTextView.setText("update UI");
                    }
                }).start();

                break;
            default:
                break;
        }
    }
}

 

可以看出来,我们在按钮的点击事件里面开启了一个子线程,然后在子线程中更新TextView的显示内容。代码的逻辑非常简单,我们来运行一下程序,并点击按钮,结果如何?

我们看到程序奔溃了,观察Logcat中的错误日志,可以看出来是由于在子线程中更新UI所导致的。

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

由此证实了Android确实是不允许在子线程中进行UI操作的。但是有些时候,我们必须在子线程中去执行一些耗时任务,然后根据任务的执行结果来更新相应的控件,这时候就用到了Handler了。

通过上面对Handler的介绍,我们修改MainActivity.java如下:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    public static final int CHANGE_TEXT = 1;
    private TextView mTextView;


    @SuppressLint("HandlerLeak")
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case CHANGE_TEXT:
                    //在这里进行UI操作
                    mTextView.setText("Handler demo test");
                    break;
                default:
                    break;
            }

        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView = findViewById(R.id.m_text);
        Button button = findViewById(R.id.button);
        button.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.button:
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        Message message = new Message();
                        message.what = CHANGE_TEXT;
                        mHandler.sendMessage(message);
                    }
                }).start();

                break;
            default:
                break;
        }
    }
}

这里我们先是定义一个常量UPDATE_TEXT,用于表示更新TextView的动作。然后通过新建一个Handler对象,并重写父类的handleMessage()方法,在这里面对具体的Message进行处理,如果发现Message的what字段的值等于UPDATE_TEXT,就将TextView的内容进行更新。当点击按钮时,我们没有直接在子线程中更新UI,而是创建了一个Message对象,并将它的what字段的值指定为UPDATE_TEXT,然后调用Handler的sendMessage()方法将这条Message发送出去。Handler收到这条Message后,在handleMessage()方法中进行处理,这时候的handlerMessage()方法已经运行在主线程中了,所以我们可以放心的在这里进行UI操作,接下来对Message的what字段的值进行判断,执行具体的操作。我们再次运行程序,程序运行正常。

通过上面的讲解和Demo的解析,对Handler 有了一定的认识,我们可以通过Handler来解决一开始提出的两个问题,但是Handler不仅仅是用来更新UI的,这仅是Handler的使用场景之一。对于初学者而言,可以好好理解上面的讲解初学者,等对Handler有了感觉之后在接着往下看。

Handler实现流程及解析

通过上面介绍我们对Handler有了初步认识,但是系统如何将消息投递到消息队列的,又是如何获取消息并处理的?Handler是如何关联队列及线程的呢?消息又是如何被处理的?接下来我们通过源码对Handler消息机制进行更加深入的剖析。(SDK android-26)

我们知道在Android应用启动时,会默认有一个主线程(UI线程),在这个线程中会关联一个消息队列,所有的操作都会被封装成消息然后交给主线程来处理。为了保证主线程不会退出,会将获取消息的操作放在一个死循环中,这样程序就相当于一直在执行死循环,因此不会退出。

UI线程的消息循环是在ActivityThread.main方法中创建的,该函数为Android应用程序的入口。

AndroidThread.java

执行ActivityThread.main方法后,应用程序就启动了,并且会一直从消息队列中取消息,然后处理消息,使得系统运转起来。那么系统是如何将消息投递到消息队列中的?又是如何从消息中获取并处理消息的呢?答案就是Handler。

在子线程中执行完耗时操作,很多情况下要更新UI,我们知道不能在子线程中更新UI。此时通过Handler将一个消息Post到UI线程中,然后再在Handler的handleMessage()方法中进行处理。上面的Handler简单实践我们也进行了演示。我们要记住,该Handler必须在主线程中创建,在上面也提到过,但是这是为什么呢?。

其实每个Handler都会关联一个消息队列,消息队列被封装在Looper中,

Looper.java

而每个Looper又会关联一个线程(Looper通过ThreadLocal封装),最终等于每个消息队列会关联一个线程。Handler就是一个消息处理器,将消息投递给消息队列,然后再由对应的线程从消息队列中逐个取出消息,并且执行。默认情况下,消息队列只有一个,即主线程的消息队列,这个消息队列是在ActivityThread.main方法中创建的,通过Looper.prepareMainLooper()来创建,最后执行Looper.loop()来启动消息循环。那么Handler是如何关联消息队列以及线程的呐?我们看Handler的源码:

Handler.java

从Handler的构造函数中可以看到,Handler会在内部通过Loope.myLooper()来获取Looper对象,并且与之关联,最重要的是消息队列。那么Looper.myLooper()又是如何工作的呢?我们继续看:

Looper.java
Looper.java

从上面源码中可以看到myLooper()方法通过sThreadLocal.get()来获取的。那么Looper对象是什么时候存储在sThreadLocal中的呢?观察上面代码可以看到一个熟悉的方法—perpareMainLooper(),在这个方法中调用了prepare()方法,在这个prepare方法中创建了一个Looper对象,并将其设置给SThreadLocal。这样,队列就与线程关联上了,不同的线程不能访问对方的消息队列。

再回到Handler中来,消息队列通过Looper与线程关联上,而Handler 又与Looper关联,因此,Handler最终和线程、线程的消息队列关联上了。这就能解释上面提到的为什么要更新UI的Handler必须要在主线程中创建。就是因为Handler要与主线程的消息队列关联上,这样handleMessage才会执行在UI线程,此时更新UI才是线程安全的!

创建了Looper后,如何执行消息循环呢?通过Handler来Post消息给消息队列(单链表),那么消息是如何被处理的呢?答案就是在消息循环中,消息循环的建立是通过Looper.loop()方法。源码如下:

Looper.java

从上面的的源码中可以看到,loop()方法中实质上就是建立一个死循环,然后哦通过从消息队列中逐个取出消息,最后处理消息的过程,对于Looper我们总结一下:通过Looper.prepare()来创建Looper对象(消息队列封装在Looper对象中),并且保存在SThreadLocal中,然后通过Looper.loop()来执行消息循环,这两步通常是成对出现的。

最后我们看下次处理机制,我们在上面源码的处理消息步骤看到msg.target.dispatchMessage(msg),msg就是Message类型。

Message.java

从源码中可以看到,target是Handler类型。实际上就是转了一圈,通过Handler将消息投递给消息队列,消息对了又将消息分发给Handler来处理。我们接着看:

Handler.java

从上面源码可以看出,dispatchMessage只是一个分发的方法,如果Runnable类型的callback为空,则执行handleMessage来处理,如果callback不为空,则执行handleCallback来处理,该方法会调用callback的run方法。其实这是Handler分发的两种类型,比如我们post(Runnable callbck)则callback不为空,当我们使用Handler来sendMessage时通常不会设置callback,因此,也就执行handleMessage这个分支。我们看看两种实现:

Handler.java
Handler.java
Handler.java
Handler.java

从上面源码中看到,在post(Runnable r)时,会将Runnable包装为Message对象,并将Runnable对象设置给Message对象的callback字段,最后会将给Message对象插入到消息队列。sendMessage也是类似的实现:

Handler.java

不管是post一个Runnable还是Message,都会调用sendMessageDelayed(msg,time)方法。Handler最终将消息追加到MessageQueue中,而Looper不断地从MessageQueue中读取消息,并且调用Handler的dispatchMessage分发消息,这样消息源源不断产生、添加到MessageQueue、被Handler处理,Android应用经运转起来了。

我们知道Handler需要Looper、MessageQueue的底层支持,但是在Handler简单实践中,我们在主线程中直接创建了Handler并使用,是因为主线程已经为我们创建好了Looper,并且已经启动了消息循环。如果我们在子线程中创建并使用Handler时,必须为当前线程创建Looper,并且通过Looper.loop()方法开启消息循环。这样该线程就有了自己的Looper,也就是有了自己的消息队列。

发表评论

电子邮件地址不会被公开。

5 × 5 =