个性化阅读
专注于IT技术分析

Java高效线程操作:精通线程通信

本文概述

线程通信

在多线程应用程序中, 任务可以并行运行并协作以产生结果。因此, 线程必须能够进行通信以启用真正的异步处理。在Android中, 本章重点介绍的平台特定的Handler / Looper机制以及传统的Java技术强调了线程通信的重要性。本章涵盖:

  • 通过单向数据管道传递数据
  • 共享内存通讯
  • 使用BlockingQueue实现消费者-生产者模式
  • 消息队列上的操作
  • 将任务发送回UI线程

管道

管道是java.io包的一部分。也就是说, 它们是常规Java功能, 而不是Android特定功能。管道为同一进程中的两个线程提供了一种连接并建立单向数据通道的方式。生产者线程将数据写入管道, 而消费者线程从管道读取数据。

注意

Java管道与Unix和Linux管道运算符(|外壳字符)相当, 后者用于将输出从一个命令重定向到另一命令的输入。管道运算符在Linux中跨进程工作, 但是Java管道在虚拟机中(即, 在进程内)跨线程工作。

管道本身是在内存中分配的循环缓冲区, 仅可用于两个连接的线程。没有其他线程可以访问数据。因此, 确保了在线程安全中讨论的线程安全。管道也是单向的, 只允许一个线程写入而另一个线程读取(图4-1)。

通过管道连接的两个线程

图4-1。与管道的线程通信。

当你有两项长期运行的任务, 而一项任务必须将数据连续卸载到另一项任务时, 通常使用管道。管道使将任务解耦到多个线程变得容易, 而不是只有一个线程来处理许多任务。当一个任务在线程上产生结果时, 它将结果传递到下一个进一步处理数据的线程上。收益来自干净的代码分离和并发执行。不仅可以在工作线程之间使用管道, 还可以从UI线程中卸载工作, 而你希望保持UI线程的明亮以保持响应式用户体验。

管道可以传输二进制或字符数据。二进制数据传输由PipedOutputStream(在生产者中)和PipedInputStream(在消费者中)表示, 而字符数据传输由PipedWriter(在生产者中)和PipedReader(消费者)中表示。除了数据传输类型外, 这两个管道具有相似的功能。管道的生命周期在写入器线程或读取器线程建立连接时开始, 在连接关闭时结束。

基本管道使用

管道的基本生命周期可以概括为三个步骤:设置, 数据传输(只要两个线程要交换数据, 就可以重复)和断开连接。以下示例是使用PipedWriter / PipedReader创建的, 但相同的步骤适用于PipedOutputStream / PipedInputStream。

建立连接

PipedReader r = new PipedReader();
PipedWriter w = new PipedWriter();
w.connect(r);

在此, 连接是由作者与读者建立的。读者也可以建立连接。几个构造函数还隐式设置了一个管道。默认缓冲区大小为1024, 但可以在管道的使用者端进行配置, 如下所示。

int BUFFER_SIZE_IN_CHARS = 1024 * 4;
PipedReader r = new PipedReader(BUFFER_SIZE_IN_CHARS);
PipedWriter w = new PipedWriter(r);

将阅读器传递给处理线程

Thread t = new MyReaderThread(r);
t.start();

读取器线程启动后, 就可以从写入器接收数据了。

传输资料

// Producer thread: Write single character or array of characters
w.write('A');

// Consumer thread: Read the data
int result = r.read();

通信通过阻塞机制坚持到消费者-生产者模式。如果管道已满, 则write()方法将阻塞, 直到已读取足够的数据, 然后从管道中删除该数据为止, 以为写入器尝试添加的数据留出空间。每当没有数据要从管道读取时, read()方法就会阻塞。值得注意的是, read()方法以整数值形式返回字符, 以确保有足够的空间来处理具有不同大小的各种编码。你可以将整数值转换回一个字符。

实际上, 一种更好的方法如下所示:

// Producer thread: Flush the pipe after a write.
w.write('A');
w.flush();

// Consumer thread: Read the data in a loop.
int i;
while((i = reader.read()) != -1){
    char c = (char) i;
    // Handle received data
}

写入管道后调用flush()通知使用者线程新数据可用。从性能角度来看, 这很有用, 因为当缓冲区为空时, PipedReader使用阻塞调用对wait()进行一秒钟的超时。因此, 如果省略了flush()调用, 则使用者线程可能会将数据读取延迟一秒钟。通过调用flush(), 生产者可以缩短使用者线程中的等待时间, 并允许数据处理立即继续进行。

关闭连接

通信阶段结束后, 应断开管道连接。

// Producer thread: Close the writer.
w.close();

// Consumer thread: Close the reader.
r.close();

如果作者和读者已连接, 则仅需关闭其中之一即可。如果关闭写入器, 则管道将断开连接, 但仍可以读取缓冲区中的数据。如果关闭读取器, 则清除缓冲区。

辅助线程上的文本处理

下一个示例说明管道如何处理用户在EditText中输入的文本。为了保持UI线程的响应速度, 用户输入的每个字符都会传递给工作线程, 该工作线程可能会处理一些耗时的处理。为了简洁起见, 示例中省略了异常处理和适当的线程取消。

public class PipeExampleActivity extends Activity {

    private EditText editText;

    PipedReader r;
    PipedWriter w;

    private Thread workerThread;

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        r = new PipedReader();
        w = new PipedWriter();
        w.connect(r);

        setContentView(R.layout.activity_pipes);
        editText = (EditText) findViewById(R.id.edit_text);
        editText.addTextChangedListener(new TextWatcher() {
            @Override
            public void onTextChanged(CharSequence charSequence, int start, int before, int count) {
                // Only handle addition of characters
                if(count > before) {
                        // Write the last entered character to the pipe
                    w.write(charSequence.subSequence(before, count).toString());
                }
            }
        });

        workerThread = new Thread(new TextHandlerTask(r));
        workerThread.start();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        r.close();
        w.close();
    }

    private static class TextHandlerTask implements Runnable {

        private final PipedReader reader;

        public TextHandlerTask(PipedReader reader){
            this.reader = reader;
        }

        @Override
        public void run() {
            while(true){
                // Block background thread and wait for data to process.
                while((i = reader.read()) != -1){
                    char c = (char) i;
                    // Add text processing logic here
                }
            }
        }
    }
}

创建PipeExampleActivity后, 它将显示一个EditText框, 该框具有用于更改内容的侦听器(TextWatcher)。每当在EditText中添加新字符时, 该字符将被写入管道并在TextHandlerTask中读取。消费者任务是一个无限循环, 一旦有任何要读取的内容, 便会从管道读取字符。如果管道为空, 则在调用read()时, 内部while循环将阻塞。

警告

将UI线程与管道一起使用时, 请小心, 因为如果管道已满(生产者在其write()调用中阻止)或空(消费者在其read()调用中阻止), 则可能会阻塞调用。

共享内存

共享内存(使用在编程中称为堆的内存区域)是在线程之间传递信息的常用方法。应用程序中的所有线程都可以在进程内访问相同的地址空间。因此, 如果一个线程在共享内存中的变量上写入一个值, 则其他所有线程都可以读取该值, 如图4-2所示。

多个线程访问同一内存区域

图4-2。与共享内存的线程通信。

如果线程将数据存储为局部变量, 则其他任何线程都看不到它。通过将其存储在共享内存中, 它可以使用变量进行通信并与其他线程共享工作。如果对象的范围是以下之一, 则将它们存储在共享内存中:

  • 实例成员变量
  • 类成员变量
  • 在方法中声明的对象

对象的引用存储在本地线程的堆栈上, 但是对象本身存储在共享内存中。仅当该方法在方法范围之外发布引用时, 才可以从多个线程访问该对象。通过将引用传递给另一个对象的方法。线程通过定义可从多个线程访问的实例和类字段, 通过共享内存进行通信。

发信号

当线程通过共享内存上的状态变量进行通信时, 它们可以轮询状态值以获取对状态的更改。但是, 更有效的机制是Java库的内置信令机制, 该机制使一个线程可以将状态的变化通知其他线程。信令机制根据同步类型而有所不同(请参见表4-1)。

表4-1。线程信令。

synchronized ReentrantLock ReentrantReadWriteLock
阻塞通话, 等待状态。 Object.wait()Object.wait(超时) Condition.await()Condition.await(超时) Condition.await()Condition.await(超时)
信号阻塞线程 Object.notify()Object.notifyAll() Condition.signal()Condition.signalAll() Condition.signal()Condition.signalAll()

当一个线程在另一个线程达到特定状态之前无法继续执行时, 它将调用wait()/ wait(timeout)或等效的await()/ await(timeout), 具体取决于所使用的同步。超时参数指示调用线程在继续执行之前应等待多长时间。

当另一个线程更改了状态时, 将通过notify()/ notifyAll()或等效的signal()/ signalAll()发出更改信号。收到信号后, 等待线程继续执行。因此, 调用支持两种使用条件的不同设计模式:notify()或signal()版本唤醒一个随机选择的线程, 而notifyAll()或signalAll()版本唤醒所有等待信号的线程。

由于多个线程可以接收信号, 并且一个线程可以在其他线程唤醒之前进入临界区, 因此接收信号并不能保证获得正确的状态。等待线程应该应用设计模式, 在执行进一步检查之前, 它会检查所需条件是否得到满足。例如, 如果共享状态受内在锁上的同步保护, 请在调用wait()之前检查条件:

synchronized(this) {
        while(isConditionFulfilled == false) {
                wait();
        }
        // When the execution reaches this point, // the state is correct.
}

此模式检查条件谓词是否满足。如果不是, 则线程通过调用wait()进行阻塞。当另一个线程在监视器上通知并且等待线程唤醒时, 它将再次检查条件是否已满足, 如果没有满足, 则它将再次阻塞, 等待新的信号。

警告

一个非常常见的Android用例是从UI线程创建一个工作线程, 并让该工作线程生成要由某个UI元素使用的结果, 因此UI线程应等待该结果。线程信令不应用于UI线程。 UI线程绝不应阻塞以等待另一个线程创建状态。

阻塞队列

线程信令是一种低级, 高度可配置的机制, 可以适应许多用例, 但它也可能被认为是最容易出错的技术。因此, Java平台在线程信令机制上构建了高级抽象, 以解决线程之间任意对象的单向切换。这种抽象通常称为“解决生产者-消费者同步问题”。问题由用例组成, 其中可能存在产生内容的线程(生产者线程)和消耗内容的线程(消费者线程)。生产者将消息传递给消费者进行处理。线程之间的中介者是具有阻塞行为的队列, 即java.util.concurrent.BlockingQueue(见图4-3)。

生产者和使用者线程通过BlockingQueue进行通信

图4-3。与BlockingQueue的线程通信。

BlockingQueue充当生产者线程和使用者线程之间的协调器, 将列表实现与线程信号包装在一起。该列表包含可配置数量的元素, 生产线程用任意数据消息填充这些元素。另一方面, 使用者线程按入队的顺序提取消息并进行处理。如果生产者和消费者不同步, 则有必要进行协调, 例如, 如果生产者交出的消息量超出了消费者所能承受的范围。因此, BlockingQueue使用线程条件来确保如果BlockingQueue列表已满, 生产者就不能使新消息入队, 并且消费者可以知道何时有消息要提取。线程之间的同步可以通过线程信令来实现, 如示例:使用者和生产者所示。但是BlockingQueue会阻塞线程并发出重要状态更改的信号, 即列表不完整, 列表也不为空。

通过LinkedBlockingQueue-implementation实现的消费者-生产者模式很容易实现, 方法是使用put()将消息添加到队列中, 然后使用take()删除消息, 其中, 如果队列已满, 则put()阻止调用者, 并使用take()如果队列为空, 则阻止呼叫者。

public class ConsumerProducer {

    private final int LIMIT = 10;
    private BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<Integer>(LIMIT);

    public void produce() throws InterruptedException {
        int value = 0;

        while (true) {
            blockingQueue.put(value++);
        }
    }

    public void consume() throws InterruptedException {

        while (true) {
            int value = blockingQueue.take();
        }
    }
}

Android消息传递

到目前为止, 讨论的线程通信选项是常规Java, 可以在任何Java应用程序中使用。这些机制(管道, 共享内存和阻塞队列)适用于Android应用程序, 但是由于它们倾向于阻塞, 因此给UI线程带来了问题。使用具有阻塞行为的机制时, UI线程的响应能力会受到威胁, 因为这有时会导致线程挂起。

Android中最常见的线程通信用例是UI线程和辅助线程之间。因此, Android平台为线程之间的通信定义了自己的消息传递机制。 UI线程可以通过发送要在后台线程上处理的数据消息来减轻长任务的负担。消息传递机制是一种非阻塞的消费者-生产者模式, 其中, 生产者线程和消费者线程都不会在消息移交期间阻塞。

消息处理机制是Android平台的基础, API位于android.os包中, 图4-4中显示了一组实现该功能的类。

Android消息传递组件概述

图4-4。 API概述。

android.os.Looper

与一个唯一的使用者线程关联的消息调度程序。

android.os.Handler

使用者线程消息处理器, 以及生产者线程将消息插入队列的接口。循环程序可以具有许多关联的处理程序, 但是它们都将消息插入同一队列中。

android.os.MessageQueue

使用者线程上要处理的消息的无限制链接列表。每个Looper(和线程)最多具有一个MessageQueue。

android.os.Message

在使用者线程上执行的消息。

消息由生产者线程插入, 并由消费者线程处理, 如图4-5所示。

生产者线程和使用者线程通过MessageQueue通信

图4-5。多个生产者线程和一个使用者线程之间的消息传递机制的概述。每个消息都引用队列中的下一条消息。

  1. 插入生产者线程通过使用连接到使用者线程的Handler将消息插入队列中的消息, 如Handler中所示。
  2. 检索Looper中讨论的Looper, 它在使用者线程中运行, 并按顺序从队列中检索消息。
  3. 调度处理程序负责处理使用者线程上的消息。一个线程可能具有多个用于处理消息的Handler实例。 Looper确保将消息调度到正确的处理程序。

基本消息传递示例

在详细剖析组件之前, 让我们看一个基本的消息传递示例, 以使我们熟悉代码设置。

以下代码实现了可能是最常见的用例之一。用户按下屏幕上的按钮, 该按钮可能会触发较长的操作, 例如网络操作。为了避免延迟UI的渲染, 必须在辅助线程上执行在此处由虚拟doLongRunningOperation()方法表示的long操作。因此, 设置只是一个生产者线程(UI线程)和一个消费者线程(LooperThread)。

我们的代码建立了一个消息队列。 IT照常在onClick()回调中处理按钮单击, 该回调在UI线程上执行。在我们的实现中, 回调将虚拟消息插入消息队列。为简洁起见, 示例代码中未包含布局和UI组件。

public class LooperActivity extends Activity {

        LooperThread mLooperThread;

        private static class LooperThread extends Thread { 

                public Handler mHandler;

                public void run() {
                        Looper.prepare();
                        mHandler = new Handler() { 
                                public void handleMessage(Message msg) { 
                                        if(msg.what == 0) {
                                                doLongRunningOperation();
                                        }
                                }
                        };
                        Looper.loop(); 
                }
        }

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mLooperThread = new LooperThread(); 
        mLooperThread.start();
    }

    public void onClick(View v) {
        if (mLooperThread.mHandler != null) {
            Message msg = mLooperThread.mHandler.obtainMessage(0); 
                        mLooperThread.mHandler.sendMessage(msg); 
        }
    }

        private void doLongRunningOperation() {
                // Add long running operation here.
        }

    protected void onDestroy() {
        mLooperThread.mHandler.getLooper().quit(); 
    }
}

工作线程的定义, 充当消息队列的使用者。

将Looper(以及隐式的MessageQueue)与线程相关联。

设置一个处理程序, 供生产者用来在队列中插入消息。在这里, 我们使用默认构造函数, 因此它将绑定到当前线程的Looper。因此, 只能在Looper.prepare()之后创建此Handler, 否则将没有任何绑定。

当消息已分派到工作线程时运行的回调。它检查什么参数, 然后执行长时间操作。

开始将消息从消息队列分发到使用者线程。这是一个阻塞调用, 因此工作线程将无法完成。

启动工作线程, 以便准备处理消息。

在后台线程上的mHandler设置与UI线程上的这种用法之间存在竞争条件。因此, 请验证mHandler是否可用。

使用what参数任意设置为0初始化Message-object。

将消息插入队列。

终止后台线程。对Looper.quit()的调用将停止消息的分发, 并从阻塞中释放Looper.loop(), 因此run方法可以完成, 从而导致线程终止。

消息传递中使用的类

现在让我们更详细地研究消息传递的特定组件及其用法。

MessageQueue

消息队列由android.os.MessageQueue类表示。它是用链接消息构建的, 构成一个未绑定的单向链接列表。生产者线程插入消息, 这些消息随后将分派给使用者。消息是根据时间戳排序的。时间戳值最低的待处理消息排在第一位, 以便分发给使用者。但是, 仅在时间戳值小于当前时间时才调度消息。如果不是, 则调度将等待直到当前时间超过时间戳记值。

图4-6说明了一个消息队列, 其中包含三个待处理消息, 并按时间戳排序, 其中t1 <t2 <t3。只有一条消息通过了发送屏障, 即当前时间。符合发送条件的邮件的时间戳值小于当前时间, 即图中的“现在”。

具有三个待处理消息的MessageQueue

图4-6。等待队列中的消息。最右边的消息是队列中要处理的第一条消息。

如果在Looper准备检索下一条消息时没有消息通过分发屏障, 则使用方线程将阻塞。消息通过调度屏障后, 将立即恢复执行。

生产者可以随时在队列中的任何位置在队列中插入新消息。队列中的插入位置基于时间戳值。如果新消息的时间戳值比队列中的待处理消息的时间戳值最低, 它将占据队列中的第一个位置, 该位置接下来将被分派。插入始终符合时间戳排序顺序。消息插入将在Handler中进一步讨论。

MessageQueue.IdleHandler

如果没有消息要处理, 则使用者线程有一些空闲时间。例如, 图4-7说明了一个使用者线程处于空闲状态的时隙。默认情况下, 使用者线程仅在空闲时间等待新消息, 而不必等待, 而是可以利用线程在这些空闲时隙期间执行其他任务。此功能可用于让非关键任务推迟执行, 直到没有其他消息争夺执行时间。

空闲handler

图4-7。如果没有消息通过调度屏障, 那么在需要执行下一个挂起的消息之前, 会有一个时隙可以用于执行

当一个待处理的消息已被调度, 并且没有其他消息已通过调度屏障时, 将出现一个时隙, 在该时隙中可以将使用者线程用于执行其他任务。应用程序通过android.os.MessageQueue.IdleHandler-interface掌握了该时隙;它是一个在使用者线程空闲时生成回调的侦听器。侦听器已附加到MessageQueue, 并通过以下调用从其分离:

// Get the message queue of the current thread.
MessageQueue mq = Looper.myQueue();
// Create and register an idle listener.
MessageQueue.IdleHandler idleHandler = new MessageQueue.IdleHandler();
mq.addIdleHandler(idleHandler)
// Unregister an idle listener.
mq.removeIdleHandler(idleHandler)

空闲处理程序接口仅包含一个回调方法:

interface IdleHandler {
    boolean queueIdle();
}

当消息队列检测到使用者线程的空闲时间时, 它将在所有已注册的IdleHandler实例上调用queueIdle()。由应用程序负责地实施回调。你通常应该避免长时间运行的任务, 因为它们会在运行期间延迟待处理的消息。

queueIdle()的实现必须返回具有以下含义的布尔值:

true

空闲处理程序保持活动状态;它会继续接收连续空闲时隙的回调。

false

空闲处理程序处于不活动状态;对于连续的空闲时隙, 它将不再接收回调。这与删除侦听器相同

MessageQueue.removeIdleHandler()

.

示例:使用IdleHandler终止未使用的线程

当线程具有空闲插槽(在该插槽中等待新消息处理)时, 将调用所有已注册到MessageQueue的IdleHandlers。空闲时隙可以出现在第一条消息之前, 消息之间以及最后一条消息之后。如果多个内容生产者应在使用者线程上顺序处理数据, 则在处理所有消息时, 可以使用IdleHandler终止使用者线程, 以便未使用的线程不会在内存中徘徊。使用IdleHandler, 无需跟踪最后插入的消息即可知道何时可以终止线程。

警告

仅当生产线程无延迟地将消息插入到MessageQueue中时, 此用例才适用, 因此使用方线程永远不会空闲, 直到最后一条消息被插入为止。

ConsumeAndQuitThread方法显示带有Looper和MessageQueue的使用线程的结构, 该线程在没有更多消息可处理时终止该线程。

public class ConsumeAndQuitThread extends Thread implements MessageQueue.IdleHandler {

    private static final String THREAD_NAME = "ConsumeAndQuitThread";

    public Handler mConsumerHandler;
    private boolean mIsFirstIdle = true;

    public ConsumeAndQuitThread() {
        super(THREAD_NAME);
    }

    @Override
    public void run() {
        Looper.prepare();

        mConsumerHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                    // Consume data
            }
        };
        Looper.myQueue().addIdleHandler(this);
        Looper.loop();
    }


    @Override
    public boolean queueIdle() {
        if (mIsFirstIdle) { 
            mIsFirstIdle = false;
            return true; 
        }
        mConsumerHandler.getLooper().quit(); 
        return false;
    }

    public void enqueueData(int i) {
        mConsumerHandler.sendEmptyMessage(i);
    }
}

启动IdleHandler并准备Looper时, 在后台线程上注册IdleHandler, 以便设置MessageQueue。

让第一个queueIdle调用通过, 因为它发生在接收第一条消息之前。

第一次调用时返回true, 以便仍注册IdleHandler。

终止线程。

消息插入是同时从多个线程完成的, 并且插入时间具有模拟的随机性。

final ConsumeAndQuitThread consumeAndQuitThread = new ConsumeAndQuitThread();
consumeAndQuitThread.start();

for (int i = 0; i < 10; i++) {
    new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                SystemClock.sleep(new Random().nextInt(10));
                consumeAndQuitThread.enqueueData(i);
            }
        }
    }).start();

信息

MessageQueue上的每个项目都属于android.os.Message类。这是一个承载数据项或任务的容器对象, 从不两者兼有。数据由使用者线程处理, 而任务在出队时仅执行, 而你无需执行其他处理。

注意

该消息知道其接收者处理器, 即处理程序-并可以通过Message.sendToTarget()使其自身入队:

Message m = Message.obtain(handler, runnable);
m.sendToTarget();

正如我们将在Handler中看到的那样, 该处理程序最常用于消息排队, 因为它在消息插入方面提供了更大的灵活性。

数据信息

数据集具有多个可以传递给使用者线程的参数, 如表4-2所示。

表4-2。讯息参数

参数名称 Type Usage

what

int

消息标识符。传达消息的意图。
arg1, arg2

int

简单数据值可处理交接整数的常见用例。如果最多两个整数值要传递给使用者, 则这些参数比分配Bundle更为有效, 如data参数下所述。

obj

Object

任意对象。如果将对象在另一个进程中移交给线程, 则它必须实现Parcelable。

data

Bundle

任意数据值的容器。

replyTo

Messenger

在其他一些过程中引用Handler。启用进程间消息通信, 如双向通信中所述。

callback

Runnable

要在线程上执行的任务。这是一个内部实例字段, 用于保存Handler中Handler.post方法中的Runnable对象。

任务信息

该任务由一个

java.lang.Runnable

在使用者线程上执行的对象。任务消息中不能包含任务本身以外的任何数据。

MessageQueue可以包含数据和任务消息的任何组合。使用者线程以顺序方式处理它们, 而与类型无关。如果消息是数据消息, 则使用者处理数据。通过让Runnable在使用者线程上执行来处理任务消息, 但是使用者线程不会像处理数据消息那样在Handler.handleMessage(Message)中接收要处理的消息。

消息的生命周期很简单:生产者创建消息, 并最终由消费者处理。该描述足以满足大多数用例的需求, 但是当出现问题时, 对消息处理的更深入了解是非常宝贵的。让我们看一下消息在其生命周期中实际发生的情况, 可以将其分为图4-8所示的四个主要状态。运行时将消息对象存储在应用程序范围的池中, 以实现对先前消息的重用。这避免了为每次移交创建新实例的开销。消息对象的执行时间通常很短, 并且每个时间单位处理许多消息。

消息生命周期状态

图4-8。消息生命周期状态。

状态转移部分由应用程序控制, 部分由平台控制。请注意, 状态是不可观察的, 并且应用程序无法跟踪从一种状态到另一种状态的更改(尽管有一些方法可以跟踪消息的移动, 这将在后面的“观察消息队列”中进行解释)。因此, 应用程序在处理消息时不应对当前状态做出任何假设。

已初始化

在初始化状态下, 已创建具有可变状态的消息对象, 如果是数据消息, 则将数据填充。该应用程序负责使用以下调用之一创建消息对象。他们从对象池中获取一个对象。

显式对象构造

Message m  = new Message();

工厂方法

空信息

Message m = Message.obtain();

数据信息

Message m = Message.obtain(Handler h);
Message m = Message.obtain(Handler h, int what);
Message m = Message.obtain(Handler h, int what, Object o);
Message m = Message.obtain(Handler h, int what, int arg1, int arg2);
Message m = Message.obtain(Handler h, int what, int arg1, int arg2, Object o);

任务信息

Message m = Message.obtain(Handler h, Runnable task);

复制构造函数

Message m = Message.obtain(Message originalMsg);

待定

该消息已由生产者线程插入到队列中, 并且正在等待分派给消费者线程。

派遣

在这种状态下, Looper已从队列中检索并删除了消息。该消息已调度到使用者线程, 并且正在处理中。此操作没有应用程序API, 因为分发是由Looper控制的, 而不受应用程序的影响。循环程序分派邮件时, 它将检查邮件的传递信息, 并将该邮件传递给正确的收件人。调度后, 将在使用者线程上执行该消息。

已回收

在生命周期的这一点, 将清除消息状态, 并将实例返回到消息池。循环程序在使用方线程上完成执行时, 处理循环消息。消息的回收是运行时的处理程序, 不应由应用程序明确完成。

注意

将消息插入队列后, 不应更改内容。从理论上讲, 在发送消息之前更改内容是有效的。但是, 由于状态是不可观察的, 因此在生产者尝试更改数据时, 消费者线程可能会处理该消息, 从而增加了线程安全性。如果消息已被回收, 那将更加糟糕, 因为消息随后已被返回到消息池, 并可能被另一个生产者用来在另一个队列中传递数据。

Looper

android.os.Looper类处理队列中的消息向相关处理程序的分配。如图4-6所示, 所有已通过分发屏障的消息都可以由Looper进行分发。只要队列中有符合发送条件的消息, Looper就会确保使用者线程接收到消息。当没有消息通过调度屏障时, 使用者线程将阻塞, 直到消息通过调度屏障为止。

使用者线程不直接与消息队列交互以检索消息。而是在附加循环程序后将消息队列添加到线程中。循环程序管理消息队列, 并促进将消息分发到使用者线程。

默认情况下, 仅UI线程具有Looper;在应用程序中创建的线程需要显式关联一个Looper。为线程创建Looper时, 它会连接到消息队列。 Looper充当队列和线程之间的中介。 Looper的设置是通过线程的run方法完成的:

class ConsumerThread extends Thread {
    @Override
    public void run() {
        Looper.prepare(); 

        // Handler creation omitted.

        Looper.loop(); 
    }
}

第一步是创建Looper, 这是通过静态prepare()方法完成的;它将创建一个消息队列并将其与当前线程关联。此时, 消息队列已准备好插入消息, 但尚未将其分派到使用者线程。

开始处理消息队列中的消息。这是一种确保run()方法未完成的阻塞方法。当run()阻塞时, Looper将消息分发到使用者线程进行处理。

一个线程只能有一个关联的Looper;如果应用程序尝试设置第二个, 则会发生运行时错误。因此, 一个线程只能有一个消息队列, 这意味着多个生产者线程发送的消息将在使用者线程上顺序处理。因此, 当前正在执行的消息将推迟后续消息, 直到它被处理为止。如果执行时间长的消息可以延迟队列中的其他重要任务, 则不应使用它们。

Looper端接

要求Looper停止处理使用quit或quit的消息Safe:quit()阻止Looper从队列中分发更多消息;队列中的所有待处理消息(包括已通过调度屏障的消息)将被丢弃。退出安全地, 另一方面, 仅丢弃尚未通过调度屏障的消息。在Looper终止之前, 将处理符合发送条件的待处理消息。

注意

在API级别18(Jelly Bean 4.3)中添加了quitSafely。以前的API级别仅支持退出。

终止Looper不会终止线程。它仅退出Looper.loop()并让线程在调用循环调用的方法中恢复运行。但是你无法启动旧的循环程序或新的循环程序, 因此线程无法再排队或处理消息。如果调用Looper.prepare(), 则它将抛出RuntimeException, 因为线程已经具有附加的Looper。如果调用Looper.loop(), 它将阻止, 但不会从队列中调度任何消息。

UI线程Looper

默认情况下, UI线程是唯一具有关联Looper的线程。与应用程序本身创建的任何其他线程一样, 它是一个常规线程, 但在初始化应用程序组件之前, Looper会与线程[7]关联。

UI线程Looper和其他应用程序线程Looper之间存在一些实际差异:

  • 可通过Looper.getMainLooper()方法从任何地方访问它。
  • 它无法终止。 Looper.quit()引发RuntimeException。
  • 运行时通过Looper.prepareMainLooper()将Looper关联到UI线程。每个应用程序只能执行一次。因此, 尝试将主循环程序附加到另一个线程将引发异常。

处理器

到目前为止, 焦点一直集中在Android线程通信的内部, 但是应用程序主要与android.os.Handler类进行交互。这是一个双向API, 可处理将消息插入队列和处理消息的过程。如图4-5所示, 通常从生产者线程和使用者线程中调用它, 这些线程通常用于:

  • 建立讯息
  • 将消息插入队列
  • 在使用者线程上处理消息
  • 管理队列中的消息

设定

在执行职责时, 处理程序与Looper, 消息队列和消息进行交互。如图4-4所示, 唯一的直接实例关系是Looper, 后者用于连接到MessageQueue。没有Looper, 处理程序将无法运行。它不能与队列耦合以插入消息, 因此它将不会接收任何要处理的消息。因此, 在构造时已经将Handler实例绑定到Looper实例:

没有显式Looper的构造函数将绑定到当前线程的Looper。

new Handler();
new Handler(Handler.Callback)

具有显式Looper的构造函数绑定到该Looper。

new Handler(Looper);
new Handler(Looper, Handler.Callback);

如果在没有Looper的线程上调用没有显式Looper的构造函数(即它没有调用Looper.prepare()), 则Handler无法绑定到任何东西, 从而导致RuntimeException。一旦将处理程序绑定到Looper, 绑定就是最终的。

一个线程可以有多个处理程序。来自它们的消息共存于队列中, 但被分派到正确的Handler实例, 如图4-9所示。

多个处理程序

图4-9。使用一个Looper的多个处理程序。插入消息的处理程序与处理消息的处理程序相同。

注意

多个处理程序将不会启用并发执行。消息仍在同一队列中并被顺序处理。

讯息建立

为简单起见, Handler类为Initialized中显示的工厂方法提供了包装函数, 以创建Message类的对象。

Message obtainMessage(int what, int arg1, int arg2)
Message obtainMessage()
Message obtainMessage(int what, int arg1, int arg2, Object obj)
Message obtainMessage(int what)
Message obtainMessage(int what, Object obj)

从处理程序获得的消息将从消息池中检索, 并隐式连接到请求该消息的处理程序实例。此连接使循环程序可以将每个消息分派到正确的处理程序。

讯息插入

处理程序根据消息类型以各种方式将消息插入消息队列中。任务消息通过名称以post开头的方法插入, 而数据消息通过名称以send开头的方法插入:

将任务添加到消息队列。

boolean post(Runnable r)f
boolean postAtFrontOfQueue(Runnable r)
boolean postAtTime(Runnable r, Object token, long uptimeMillis)
boolean postAtTime(Runnable r, long uptimeMillis)
boolean postDelayed(Runnable r, long delayMillis)

将数据对象添加到消息队列。

boolean sendMessage(Message msg)
boolean sendMessageAtFrontOfQueue(Message msg)
boolean sendMessageAtTime(Message msg, long uptimeMillis)
boolean sendMessageDelayed(Message msg, long delayMillis)

将简单的数据对象添加到消息队列。

boolean sendEmptyMessage(int what)
boolean sendEmptyMessageAtTime(int what, long uptimeMillis)
boolean sendEmptyMessageDelayed(int what, long delayMillis)

即使应用程序未显式创建Message对象, 所有插入方法也会在队列中放入新的Message对象。对象(例如任务发布中的Runnable和发送内容)被包装到Message对象中, 因为这些是队列中唯一允许的数据类型。

插入队列中的每个消息都带有一个时间参数, 该时间参数指示该消息有资格分发给使用者线程的时间。排序基于时间参数, 这是应用程序可以影响调度顺序的唯一方法。

默认

立即有资格派遣。

at_front

该消息在时间0可以进行分发。因此, 它将是下一个已分发的消息, 除非在处理该消息之前在其前面插入了另一个。

延迟

此消息有资格发送之前经过的时间。

正常运行时间

可以发送此消息的绝对时间。

即使指定了明确的延迟或正常运行时间, 处理每个消息所需的时间仍然不确定。它取决于首先需要处理的任何现有消息以及操作系统调度。

在队列中插入消息不是安全的。表4-3列出了一些可能发生的常见错误。

表4-3。邮件插入错误

Failure 错误回应 典型应用问题
消息没有处理程序。

RuntimeException

消息是从没有指定处理程序的Message.obtain()方法创建的。
消息已被调度并正在处理。

RuntimeException

同一消息实例被插入两次。
Looper已退出。 返回假 在调用Looper.quit()之后插入消息。

警告

Looper使用Handler类的dispatchMessage方法将消息调度到使用者线程。如果直接由应用程序使用, 则消息将在调用线程而不是使用者线程上立即处理。

示例:双向消息传递

HandlerExampleActivity模拟长时间运行的操作, 该操作在用户单击按钮时启动。长时间运行的任务在后台线程上执行;同时, UI显示进度条, 当后台线程将结果报告回UI线程时, 进度条将被删除。

首先, 活动的设置:

public class HandlerExampleActivity extends Activity {

    private final static int SHOW_PROGRESS_BAR = 1;
    private final static int HIDE_PROGRESS_BAR = 0;
    private BackgroundThread mBackgroundThread;

    private TextView mText;
    private Button mButton;
    private ProgressBar mProgressBar;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler_example);

        mBackgroundThread = new BackgroundThread();
        mBackgroundThread.start();

        mText = (TextView) findViewById(R.id.text);
        mProgressBar = (ProgressBar) findViewById(R.id.progress);
        mButton = (Button) findViewById(R.id.button);
        mButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                mBackgroundThread.doWork(); 
            }
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mBackgroundThread.exit();
    }

    // ... The rest of the Activity is defined further down

}

创建HandlerExampleActivity时, 将启动带有消息队列的后台线程。它处理来自UI线程的任务。

当用户单击按钮时, 新任务将发送到后台线程。由于任务将在后台线程上顺序执行, 因此多次单击按钮可能导致任务在处理之前排队。

当HandlerExampleActivity被销毁时, 后台线程停止。

BackgroundThread用于从UI线程中卸载任务。它在HandlerExampleActivity的生存期内运行, 并且可以接收消息。它不公开其内部Handler。而是将所有对Handler的访问包装在公共方法doWork中并退出。

private class BackgroundThread extends Thread {

    private Handler mBackgroundHandler;

    public void run() { 
        Looper.prepare();
        mBackgroundHandler = new Handler(); 
        Looper.loop();
    }

    public void doWork() {
        mBackgroundHandler.post(new Runnable() { 
            @Override
            public void run() {
                Message uiMsg = mUiHandler.obtainMessage(
                    SHOW_PROGRESS_BAR, 0, 0, null); 

                mUiHandler.sendMessage(uiMsg); 

                Random r = new Random();
                int randomInt = r.nextInt(5000);
                SystemClock.sleep(randomInt); 

                uiMsg = mUiHandler.obtainMessage(
                    HIDE_PROGRESS_BAR, randomInt, 0, null); 
                    mUiHandler.sendMessage(uiMsg); 
            }
        });
    }

    public void exit() { 
        mBackgroundHandler.getLooper().quit();
    }
}

将Looper与该线程相关联。

处理程序仅处理Runnable。因此, 不需要实现Handler.handleMessage。

张贴要在后台执行的长任务。

使用UI线程的命令SHOW_PROGRESS_BAR创建一个仅包含what参数的Message对象, 以便它可以显示进度栏。

将启动消息发送到UI线程。

模拟一个随机长度的长任务, 该任务会产生一些数据randomInt。

创建一个带有结果randomInt的Message对象, 该对象在arg1参数中传递。 what参数包含一个命令HIDE_PROGRESS_BAR来删除进度条。

最终结果的消息既通知UI线程任务已完成, 又传递结果。

退出Looper, 以便线程可以完成。

UI线程定义了自己的处理程序, 该处理程序可以接收命令以控制进度条并使用后台线程的结果更新UI。

private final Handler mUiHandler = new Handler() {
    public void handleMessage(Message msg) {

        switch(msg.what) {
            case SHOW_PROGRESS_BAR: 
                mProgressBar.setVisibility(View.VISIBLE);
                break;
            case HIDE_PROGRESS_BAR: 
                mText.setText(String.valueOf(msg.arg1));
                mProgressBar.setVisibility(View.INVISIBLE);
                break;
        }
    }
};

显示进度栏。

隐藏进度条, 并使用产生的结果更新TextView。

讯息处理

Looper分派的消息由处理程序在使用者线程上处理。消息类型确定处理:

任务信息

任务消息仅包含一个

可运行

而且没有数据。因此, 要执行的处理在

run

Runnable的方法, 该方法在使用者线程上自动执行, 而无需调用

Handler.handleMessage()

.

数据信息

当消息包含数据时, 处理程序是数据的接收者, 并负责其处理。使用者线程通过覆盖

Handler.handleMessage(消息消息)

方法。有两种方法可以做到这一点, 下面将对此进行介绍。

定义handleMessage的一种方法是在创建Handler的过程中进行处理。应该在消息队列可用时(在Looper.prepare()被调用之后)但在消息检索开始之前(在Looper.loop()被调用之前)定义该方法。

以下是用于设置数据消息处理的模板:

class ConsumerThread extends Thread {
    Handler mHandler;
    @Override
    public void run() {
        Looper.prepare();
        mHandler = new Handler() {
            public void handleMessage(Message msg) {
                // Process data message here
            }
        };)
        Looper.loop();
    }
}

在此代码中, 将Handler定义为匿名内部类, 但也可以将其定义为常规或内部类。

扩展Handler类的一种方便的替代方法是使用Handler.Callback接口, 该接口定义一个handleMessage方法, 并带有一个不在Handler.handleMessage()中的附加返回参数。

public interface Callback {
    public boolean handleMessage(Message msg);
}

使用Callback接口, 不需要扩展Handler类。相反, 可以将Callback实现传递给Handler构造函数, 然后它将接收分派的消息以进行处理。

public class HandlerCallbackActivity extends Activity implements Handler.Callback {
    Handler mUiHandler;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mUiHandler = new Handler(this); 
    }

    @Override
    public boolean handleMessage(Message message) { 
        // Process messages
        return true;
    }
}

如果处理了消息, Callback.handleMessage应该返回true, 这保证了消息的进一步处理不会完成。但是, 如果返回false, 则消息将传递到Handler.handleMessage方法以进行进一步处理。请注意, 回调不会覆盖Handler.handleMessage。相反, 它添加了一个消息预处理器, 该消息预处理器在Handlers自己的方法之前被调用。回调预处理器可以在处理程序接收消息之前对其进行拦截和更改。以下代码显示了使用回调拦截消息的原理:

public class HandlerCallbackActivity extends Activity implements Handler.Callback {

    @Override
    public boolean handleMessage(Message msg) { 
        Logg.d(TAG, "Primary Handler- msg = " + msg.what);
        switch (msg.what) {
            case 1:
                msg.what = 11;
                return true;
            default:
                msg.what = 22;
                return false;
        }
    }

    // Invoked on button click
    public void onHandlerCallback(View v) {
        Handler handler = new Handler(this) {
            @Override
            public void handleMessage(Message msg) {
                Log.d(TAG, "Secondary Handler - msg = " + msg.what); 
            }
        };
        handler.sendEmptyMessage(1); 
        handler.sendEmptyMessage(2); 
    }
}

HandlerCallbackActivity实现Callback接口以拦截消息。

回调实现截获消息。如果msg.what为1, 则返回true-处理该消息。否则, 它将msg.what的值更改为22并返回false-不处理该消息, 因此将其传递到handleMessage的Handler实现。

记录发送到辅助handle的消息

用msg.what == 1插入一条消息。该消息返回true时, 将被Callback截获。

插入一条消息, 其消息为msg.what ==2。该消息被回调更改, 并传递到打印辅助处理程序的处理程序-msg = 22。

从队列中删除消息

将消息放入队列后, 只要Looper尚未将其出队, 生产者就可以调用Handler类的方法来删除该消息。有时, 应用程序可能希望通过删除所有消息来清理消息队列, 但这是可能的, 但是最常见的情况是需要一种更细粒度的方法:应用程序只希望将消息的一部分作为目标。为此, 它需要能够识别正确的消息。因此, 可以从某些属性中识别消息:

表4-4。讯息识别码

Identifier type Description 适用的消息

Handler

Message receiver

任务和数据消息

Object

讯息标签 任务和数据消息

Integer

消息的什么参数

Data messages

Runnable

要执行的任务 任务信息

对于每条消息, 处理程序标识符都是必需的, 因为消息始终知道将向其分派的处理程序。此要求隐式地将每个Handler限制为仅删除属于该Handler的消息。处理程序无法删除队列中由其他处理程序插入的消息。

Handler类中用于管理消息队列的可用方法是:

从消息队列中删除任务。

removeCallbacks(Runnable r)
removeCallbacks(Runnable r, Object token)

从消息队列中删除数据消息。

removeMessages(int what)
removeMessages(int what, Object object)

从消息队列中删除任务和数据消息。

removeCallbacksAndMessages(Object token)

数据和任务消息中都使用的对象标识符。因此, 它可以作为一种标签分配给消息, 从而使你以后可以删除已用同一对象标记的相关消息。

例如, 以下摘录在队列中插入了两条消息, 以便以后可以根据标签将它们删除。

Object tag = new Object(); 

Handler handler = new Handler()
    public void handleMessage(Message msg) {
        // Process message
        Log.d("Example", "Processing message");
    }
};

Message message = handler.obtainMessage(0, tag); 
handler.sendMessage(message);

handler.postAtTime(new Runnable() { 
    public void run() {
        // Left empty for brevity
    }
}, tag, SystemClock.uptimeMillis());

handler.removeCallbacksAndMessages(tag); 

任务和数据消息共同的消息标签标识符。

Message实例中的对象既用作数据容器又用作隐式定义的消息标签。

使用明确定义的消息标签发布任务消息。

删除所有带有标签的消息。

如前所述, 你无法在发出呼叫将其删除之前查明是否已调度和处理了一条消息。调度消息后, 将消息放入队列的生产者线程无法停止其任务的执行或对其数据的处理。

观察消息队列

可以观察待处理的消息以及将消息从Looper分发到关联的Handler。 Android平台提供了两种观察机制。让我们以示例的方式看一下它们。

第一个示例显示了如何在队列中记录未决消息的当前快照。

拍摄当前消息队列的快照

本示例在创建活动时创建一个工作线程。当用户按下按钮导致调用onClick时, 六种消息以不同的方式添加到队列中。之后, 我们观察消息队列的状态。

public class MQDebugActivity extends Activity {

    private static final String TAG = "EAT";
    Handler mWorkerHandler;

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_mqdebug);

        Thread t = new Thread() {
            @Override
            public void run() {
                Looper.prepare();
                mWorkerHandler = new Handler() {
                    @Override
                    public void handleMessage(Message msg) {
                        Log.d(TAG, "handleMessage - what = " + msg.what);
                    }
                };
                Looper.loop();
            }
        };
        t.start();
    }

    // Called on button click, i.e. from the UI thread.
    public void onClick(View v) {
        mWorkerHandler.sendEmptyMessageDelayed(1, 2000);
        mWorkerHandler.sendEmptyMessage(2);
        mWorkerHandler.obtainMessage(3, 0, 0, new Object()).sendToTarget();
        mWorkerHandler.sendEmptyMessageDelayed(4, 300);
        mWorkerHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG, "Execute");
            }
        }, 400);
        mWorkerHandler.sendEmptyMessage(5);

        mWorkerHandler.dump(new LogPrinter(Log.DEBUG, TAG), "");
    }
}

队列中将添加6条消息, 其参数如图4-10所示。

消息队列示例

图4-10。在队列中添加了消息。

在将消息添加到队列中之后, 立即将快照打印到日志中。仅观察到未决消息。因此, 实际观察到的消息数取决于已经分配给处理程序的消息数量。立即添加了三个消息, 这使它们有资格在快照时进行分发。

上述代码的典型运行会产生以下日志:

 handleMessage - what = 2
 handleMessage - what = 3
 handleMessage - what = 5
 Handler (com.wifill.eat.ui.MQDebugActivity$1$1) {412cb3d8} @ 5994288
 Looper{412cb070}
     mRun=true
     mThread=Thread[Thread-111, 5, main]
     mQueue=android.os.MessageQueue@412cb090
         Message 0: { what=4 when=+293ms }
         Message 1: { what=0 when=+394ms }
         Message 2: { what=1 when=+1s990ms }
         (Total messages: 3)
 handleMessage - what = 4
 Execute
 handleMessage - what = 1

消息队列的快照显示, 队列中有未决参数的消息(0、1和4)。这些消息以分派延迟添加到队列中, 而其他没有分派延迟的消息显然已经被分派了。这是一个合理的结果, 因为处理程序的处理时间很短-只是打印到日志中。

快照还显示了队列中的每条消息通过调度屏障之前还剩下多少时间。例如, 下一个通过屏障的消息是293毫秒内的消息0(等于4)。仍在队列中挂起但有资格发送的消息在日志中将具有负的时间指示, 例如, 何时小于零。

跟踪消息队列处理

消息处理信息可以打印到日志中。从Looper类启用了消息队列日志记录。以下调用启用了登录调用线程的消息队列的功能:

Looper.myLooper().setMessageLogging(new LogPrinter(Log.DEBUG, TAG));

让我们看一个跟踪发布到UI线程的消息的示例:

mHandler.post(new Runnable() {
    @Override
    public void run() {
        Log.d(TAG, "Executing Runnable");
    }
});

mHandler.sendEmptyMessage(42);

该示例将两个事件发布到消息队列:首先是Runnable, 然后是空消息。如预期的那样, 考虑到顺序执行, 首先处理Runnable, 因此首先处理该日志:

>>>>> Dispatching to Handler (android.os.Handler) {4111ef40} com.wifill.eat.ui.MessageTracingActivity$1@41130820: 0
Executing Runnable
<<<<< Finished to Handler (android.os.Handler) {4111ef40} com.wifill.eat.ui.MessageTracingActivity$1@41130820

跟踪显示由三个属性标识的事件的开始和结束:

处理程序实例

android.os.Handler 4111ef40

任务实例

com.wifill.eat.ui.MessageTracingActivity $

1@41130820

什么

参数

0 (

可运行

任务不携带

什么

参数)

同样, 将what参数设置为42的消息的跟踪显示消息参数, 但不打印任何Runnable实例:

>>>>> Dispatching to Handler (android.os.Handler) {4111ef40} null: 42
<<<<< Finished to Handler (android.os.Handler) {4111ef40} null

结合使用消息队列快照和调度跟踪这两种技术, 应用程序可以详细观察消息传递。

UI线程通信

默认情况下, UI线程是应用程序中唯一具有关联Looper的线程, 该Looper在启动第一个Android组件之前已与该线程关联。 UI线程可以是使用者, 其他线程可以向其传递消息。仅将短暂的任务发送到UI线程很重要。 UI线程是应用程序全局的, 并顺序处理android组件和系统消息。因此, 长期存在的任务将对整个应用程序产生全局影响。

消息通过其Looper传递到UI线程, 该Looper可使用Looper.getMainLooper()从所有线程在应用程序中全局访问:

Runnable task = new Runnable() {...};
new Handler(Looper.getMainLooper()).post(task);

与发布线程无关, 消息被插入到UI线程的队列中。如果是UI线程将消息发布到自身, 则可以在完成当前消息后最早处理该消息:

// Method called on UI thread.
private void postFromUiThreadToUiThread() {
    new Handler().post(new Runnable() { ... });

    // The code at this point is part of a message being processed
    // and is executed before the posted message.

}

但是, 从UI线程发布到自身的任务消息可以绕过消息传递, 并使用便捷方法Activity.runOnUiThread(Runnable)在UI线程上当前处理的消息中立即执行:

// Method called on UI thread.
private void postFromUiThreadToUiThread() {
    runOnUiThread(new Runnable() { ... });

    // The code at this point is executed after the message.

}

如果在UI线程之外调用它, 则消息将插入队列中。 runOnUiThread方法只能从Activity实例执行, 但是可以通过跟踪UI线程的ID来实现相同的行为, 例如, 使用Application子类中的便捷方法customRunOnUiThread。 customRunOnUiThread在队列中插入一条消息, 如以下示例所示:

public class EatApplication extends Application {
    private long mUiThreadId;
    private Handler mUiHandler;

    @Override
    public void onCreate() {
        super.onCreate();
        mUiThreadId = Thread.currentThread().getId();
        mUiHandler = new Handler();
    }

    public void customRunOnUiThread(Runnable action) {
        if (Thread.currentThread().getId() != mUiThreadId) {
            mUiHandler.post(action);
        } else {
            action.run();
        }
    }
}

线程通信总结

Android应用程序可以访问常规Java线程通信技术, 该技术非常适合于工作线程通信。但是, 当线程之一是UI线程时, 它们很少适合用例, 这是最常见的情况。本书第二部分讨论了通过各种包装技术在整个应用程序中广泛或广泛地使用Android消息传递, 无论是显式的还是隐式的。


[7] UI线程由平台内部类android.app.ActivityThread管理。

赞(0)
未经允许不得转载:srcmini » Java高效线程操作:精通线程通信

评论 抢沙发

评论前必须登录!