Android Handler 消息传递机制

出于性能优化考虑,Android 的 UI 操作并不是线程安全的,这意味着如果多个线程并发操作 UI 组件,可能导致线程安全问题。为了解决这个问题,Android 制定了一条简单的规则:只允许 UI 线程修改 Activity 里的 UI 组件

当一个程序启动时,Android 会同时启动一个主线程(Main Thread),主线程主要负责处理与 UI 相关的事件,如用户的按键事件、触屏事件及屏幕绘制事件,并把相关的事件分发到对应的组件进行处理。所以主线程也称为 UI 线程。

Android 的消息传递机制是另一种形式的“事件处理”,这种机制主要是为了解决 Android 应用的多线程问题 —— Android 平台只允许 UI 线程修改 Activity 里的 UI 组件,这样就会导致新启动的线程无法动态改变界面组件的属性值。此时,就需要借助于 Handler 的消息传递机制来实现新启动的线程改变界面组件的属性值。

Handler 类简介###

Handler 类的主要作用有两个:
1.在子线程中发送消息;
2.在主线程中获取、处理消息。

例如,下面的使用示例,自动播放图片:
主布局文件的内容:

<?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/colorGray"
    android:id="@+id/imageView"
    >
</ImageView>

主程序文件的代码:

package com.toby.personal.testlistview;

import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.widget.ImageView;

import java.util.Timer;
import java.util.TimerTask;

public class MainActivity extends AppCompatActivity {

    final private static String TAG = "Toby_Test";

    final private static int MsgWhat = 20170326;
    final private static int Duration = 1200;
    private int currentImageId = 0;

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

        final int[] imageIds = new int[]{
                R.drawable.dog_001, R.drawable.dog_002, R.drawable.dog_003, R.drawable.dog_004,
                R.drawable.dog_005,
        };


        final ImageView imageView = (ImageView) findViewById(R.id.imageView);

        final Handler handler = new Handler() {
            @Override
            public void handleMessage(Message msg) {

                if (msg.what == MsgWhat) {
                    imageView.setImageResource(imageIds[currentImageId++ % imageIds.length]);
                }

                super.handleMessage(msg);
            }
        };

        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                handler.sendEmptyMessage(MsgWhat);
            }
        }, 0, Duration);

    }

}

程序运行之后,将开始自动播放图片,具体使用效果,各位可自行尝试运行。

Handler、Loop、MessageQueue 的工作原理###

与 Handler 一起工作的几个组件:
Message —— Handler 接收和处理的消息对象;
Looper —— 每个线程只能拥有一个 Looper。它的 loop 方法负责读取 MessageQueue 中的消息,读到消息之后就把消息交给发送该消息的 Handler 进行处理。
MessageQueue —— 消息队列,它采用先进先出的方式来管理 Message,程序创建 Looper 对象时会在它的构造器中创建 Looper 对象。Looper 提供的构造器源代码如下:

private Looper() {
    mQueue = new MessageQueue();
    mRun = true;
    mThread = Thread.currentThread();
}

Handler 的作用是发送和处理消息,程序使用 Handler 发送消息,消息需要使用 MessageQueue 对象进行保存,而 MessageQueue 是由 Looper 进行管理的,因此必须在当前线程中有一个 Looper 对象。可分为如下两种情况:
主线程中 —— 系统已经初始化了 Looper 对象,因此程序直接创建 Handler 即可,然后就可以通过 Handler 来发送和处理消息;
子线程中 —— 必须自己创建一个 Looper 对象,并启动它:创建 Looper 对象并调用它的 prepare() 方法即可。

prepare() 方法保证每个线程最多只有一个 Looper 对象。prepare() 方法的源代码如下:

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

然后调用 Looper 的静态 loop() 方法来启动它。loop() 方法使用一个死循环不断取出 MessageQueue 中的消息,并将取出的消息分给该消息对应的 Handler 进行处理。下面是 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;

        // Make sure the identity of this thread is that of the local process,
        // and keep track of what that identity token actually is.
        Binder.clearCallingIdentity();
        final long ident = Binder.clearCallingIdentity();

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

            // This must be in a local variable, in case a UI event sets the logger
            final Printer logging = me.mLogging;
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }

            final long traceTag = me.mTraceTag;
            if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
                Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
            }
            try {
                msg.target.dispatchMessage(msg);
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }

            if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }

            // Make sure that during the course of dispatching the
            // identity of the thread wasn't corrupted.
            final long newIdent = Binder.clearCallingIdentity();
            if (ident != newIdent) {
                Log.wtf(TAG, "Thread identity changed from 0x"
                        + Long.toHexString(ident) + " to 0x"
                        + Long.toHexString(newIdent) + " while dispatching to "
                        + msg.target.getClass().getName() + " "
                        + msg.callback + " what=" + msg.what);
            }

            msg.recycleUnchecked();
        }
    }

总结一下,Looper、MessageQueue、Handler 各自的作用如下。
Looper:每个线程只有一个 Looper,它负责管理 MessageQueue,会不断地从 MessageQueue 中取出消息,并将消息分给对应的 Handler 处理。
MessageQueue:由 Looper 负责管理。它采用先进先出的方式管理 Message。
Handler:它能把消息发送给 Looper 管理的 MessageQueue,并负责处理 Looper 分给它的消息。

在线程中使用 Handler 的步骤如下。
1.调用 Looper 的 prepare() 方法为当前线程创建 Looper 对象,创建 Looper 对象时,它的构造器会创建与之配套的 MessageQueue。
2.有了 Looper 之后,创建 Handler 子类的实例,重写 HandleMessage() 方法,该方法负责处理来自于其他线程的消息。
3.调用 Looper 的 loop() 方法启动 Looper。

Looper 与 Handler 的用法实例,使用新线程计算质数
布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/colorGray"
    android:orientation="vertical"
    >

    <EditText
        android:id="@+id/editText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/show"
        android:onClick="calculation"
        />

</LinearLayout>

主程序文件的代码如下:

package com.toby.personal.testlistview;

import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {

    final private static String TAG = "Toby_Test";

    final private static String KEY_NUM = "KEY_NUM_TEST";
    final private static int MsgWhat = 20170326;

    private EditText editText = null;
    private CalculationThread calculationThread = null;

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

        editText = (EditText) findViewById(R.id.editText);
        calculationThread = new CalculationThread();
        calculationThread.start();
    }

    public void calculation(View view) {
        Message message = new Message();
        message.what = MsgWhat;
        Bundle bundle = new Bundle();
        String numb = editText.getText().toString();
        bundle.putInt(KEY_NUM, Integer.parseInt(numb.isEmpty() ? "100" : numb));
        message.setData(bundle);
        calculationThread.handler.sendMessage(message);
    }

    private class CalculationThread extends Thread {
        Handler handler;

        @Override
        public void run() {

            Looper.prepare();

            handler = new Handler() {
                @Override
                public void handleMessage(Message msg) {

                    if (msg.what == MsgWhat) {
                        final int upper = msg.getData().getInt(KEY_NUM);
                        List<Integer> numbs = new ArrayList<>();
                        outer:
                        for (int i = 2; i <= upper; ++i) {

                            for (int j = 2; j <= Math.sqrt(i); ++j) {
                                if (i != 2 && i % j == 0) {
                                    continue outer;
                                }
                            }
                            numbs.add(i);
                        }

                        Toast.makeText(MainActivity.this, numbs.toString(),
                                Toast.LENGTH_LONG).show();

                    }

                    super.handleMessage(msg);
                }
            };

            Looper.loop();

            super.run();
        }
    }
}

该示例的运行效果如下图所示:


显示效果

参考文献:《疯狂Android讲义(第2版)》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容