前言

在上一篇Android on Linux(在Linux主机上运行Android可执行程序)文章中,我们完成了直接在Linux主机上运行Android的可执行程序。其可以用来做一些自动化测试的工作,目前项目中服务端的代码是Android C/C++代码,编译成一个可执行程序,而客户端的代码是一个Java写的Android APK。可以将核心代码移植成一个纯JAVA项目,直接在Linux主机上使用JAVA VM来执行,从而达到自动化测试的目的。

移植过程中发现,其主要需要移植的就是Android Handler消息机制。本文主要重点是使用Java的Object.notify()Object.wait()来替代Android中的nativeWake()nativePollOnce()。本文提到的源码在此下载:https://gitee.com/cqupt/java_implement_android_handler

Handler基本原理

概述

目前解读Android Handler的实现的文章非常多,下面先列出相关文章,读者自行阅读。

Android Handler消息机制原理最全解读(持续补充中)

Android全面解析之由浅及深Handler消息机制

Android Handler机制

ThreadLocal的介绍

深入理解MessageQueue

Android消息机制1-Handler(Java层)

Android framework学习(2)——Handler Native层

简单概括:Handler机制是Android中基于单线消息队列模式的一套线程消息机制。也就是提供了一套API,可以方便的让子线程相互通讯。

总结归纳如下:

1、在使用handler的时候,在handler所创建的线程需要维护一个唯一的Looper对象, 每个线程对应一个Looper,每个线程的Looper通过ThreadLocal来保证。Looper对象的内部又维护有唯一的一个MessageQueue,所以一个线程可以有多个handler,但是只能有一个Looper和一个MessageQueue。
2、Message在MessageQueue不是通过一个列表来存储的,而是将传入的Message存入到了上一个Message的next中,在取出的时候通过顶部的Message就能按放入的顺序依次取出Message。
3、Looper对象通过loop()方法开启了一个死循环,不断地从looper内的MessageQueue中取出Message,然后通过handler将消息分发传回handler所在的线程。

来看一个例子:

import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;


public class MessageMain {
    private static final String TAG = "MessageMain";

    public static class HandlerThread extends Thread {
        private Looper mLooper;

        @Override
        public void run() {
            super.run();
            Looper.prepare();
            synchronized (this) {
                mLooper = Looper.myLooper();
                notifyAll();
            }
            Looper.loop();
        }

        public Looper getLooper() {
            if (!isAlive()) {
                return null;
            }
            synchronized (this) {
                while (isAlive() && mLooper == null) {
                    try {
                        wait();
                    } catch (InterruptedException e) {
                    }
                }
            }
            return mLooper;
        }
    }
    
    public static void main(String[] args) {
        Looper.prepareMainThread();
 
        HandlerThread handlerThread = new HandlerThread();
        handlerThread.start();
        Handler handler = new Handler(handlerThread.getLooper()) {
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                Log.d("TAG", "testSendEmptyMessageDelayed: " + msg.what);
                System.exit(0);
            }
        };

        Message msg = handler.obtainMessage();
        msg.what = 100;
        Log.d("TAG", "testSendEmptyMessageDelayed: ");
        handler.sendEmptyMessageDelayed(msg.what, 400);

        Looper.loop();
    }
}

1、开启HandlerThread线程;
2、HandlerThread线程的run()方法中,首先通过Looper.prepare()准备一个对象,然后通过 Looper.myLooper()获取一个mLooper对象,再Looper.loop()开启循环,处理Message消息;
3、new Handler(handlerThread.getLooper())获取上一步中线程的mLooper对象,传递给Handler构造方法,并重载handleMessage()
4、在主线程中通过handler.obtainMessage()获取一个Message对象,然后通过sendEmptyMessageDelayed()发送一个延时消息到HandlerThread线程。

可以总结成如下流程图:

java handler执行完成 java handler机制_队列


图中ThreadA即HandlerThread线程,ThreadB即主线程。图片来源:Android Handler消息机制原理最全解读(持续补充中)

ThreadLocal

前面提到每个线程中都需要通过Looper.prepare()构建一个的Looper对象,然后通过静态方法Looper.myLooper()获取此对象。我们必须保证每个线程都有一个独立的Looper对象,而不是多个线程共享同一个Looper对象。那么此处是静态方法它是如何做到在不同线程返回不同对象的了呢?此处使用的就是ThreadLocal类。

ThreadLocal是Java中一个用于线程内部存储数据的工具类,通过它可以在指定的线程中存储数据,存储后只有在指定线程中获取到存储的数据,其他线程则无法获取到数据。

更多关于它的讲解请查看:ThreadLocal的介绍

Looper

Looper可以说是Handler机制中的一个非常重要的核心。Looper相当于线程消息机制的引擎,驱动整个消息机制运行。Looper负责从队列中取出消息,然后交给对应的Handler去处理。如果队列中没有消息,则MessageQueue的next方法会阻塞线程,等待新的消息的到来。每个线程有且只能有一个“引擎”,也就是Looper,如果没有Looper,那么消息机制就运行不起来,而如果有多个Looper,则会违背单线操作的概念,造成并发操作。

具体实现过程参考前面的文章,此处我们直接拿Android的代码来用即可。

Handler

Handler是作为整个消息机制的消息发起者与处理者,消息在不同的线程通过Handler发送到目标线程的MessageQueue中,然后目标线程的Looper再调用Handler的dispatchMessage方法来处理消息。

调用Handler不同参数方法发送Message最终都会调用到该方法,注意此处我们重写时在enqueu时对msg.when都赋值,后续会详细说明。

public boolean sendMessageAtTime(Message msg, long time) {
        msg.when = time;
        return mQueue.enqueueMessage(msg);
    }

其它函数接口几乎一致,可以精简后直接在Java中使用。

Message

Message的作用就是承载消息,他的内部有很多的属性用于给用户赋值。同时Message本身也是一个链表结构,无论是在MessageQueue还是在Message内部的回收机制,都是使用这个结构来形成链表。同时官方建议不要直接初始化Message,而是通过Message.obtain()方法来获取一个Message循环利用。一般来说我们不需要去调用recycle进行回收,在Looper中会自动把Message进行回收,后面会讲到。

此类可以直接精简一下后在Java中使用。

MessageQueue

MessageQueue中enqueueMessage的目的有两个:
1.插入消息到消息队列
2.通过nativeWake(mPtr),唤醒Looper中等待的线程(如果是及时消息并且线程是阻塞状态)
同时我们知道了MessageQueue的底层数据结构是单向链表,MessageQueue中的成员变量mMessages指向的就是该链表的头部元素。

MessageQueue中的next()方法主要通过nativePollOnce(ptr, nextPollTimeoutMillis)来阻塞等消息。
1.当首次进入或所有消息队列已经处理完成,由于此刻队列中没有消息(mMessages为null),这时nextPollTimeoutMillis = -1 ,然后会处理一些不紧急的任务(IdleHandler),之后线程会一直阻塞,直到被主动唤醒(插入消息后根据消息类型决定是否需要唤醒)。
2.读取列表中的消息,如果发现消息屏障,则跳过后面的同步消息。
3.如果拿到的消息还没有到时间,则重新赋值nextPollTimeoutMillis = 延时的时间,线程会阻塞,直到时间到后自动唤醒
4.如果消息是及时消息或延时消息的时间到了,则会返回此消息给looper处理。

详细讲解请查看:深入理解MessageQueue

此类在Android中主要使用了nativeWakenativePollOnce等方法,这就是我们需要想办法替换成Java的方法。

已有案例

使用ArrayDeque来实现

用Java实现一个类似AndroidHandler的消息循环文章中,实现了线程间的通讯。
他使用了ArrayDeque来装载Message对象,在enqueueMessage()方法中

public void enqueueMessage(Message message) {
        synchronized (mQueue) {
            mQueue.notify();
            mQueue.add(message);
        }
    }

next()方法中

public Message next() {
        while (true) {
            synchronized (mQueue) {
                try {
                    if (!mQueue.isEmpty()) {
                        return mQueue.poll();
                    }
                    mQueue.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

他主要使用Java里Object的两个final native方法wait和notifyAll,完成了Handler的基本通讯机制,但是未实现延时发送消息功能,只能做到立即处理。

使用DelayQueue来实现

使用java实现android的handler消息机制文章中,他使用了DelayQueue类,来实现延时操作。

DelayQueue是一个支持延时获取元素的无界阻塞队列,队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。

关于DelayQueue更多信息,请查看:DelayQueue详解

此方案解决了不能发送延时消息的问题,满足了需求,但是实现方式依赖DelayQueue,显得不够简单,有没有更简单的方式呢?

开始重写

说明

Android 中 MessageQueue 的 nativePollOnce文章中提到:

. nativePollOnce 和 nativeWake 的核心魔术发生在 native 代码中. native MessageQueue 利用名为 epoll 的 Linux 系统调用, 该系统调用可以监视文件描述符中的 IO 事件. nativePollOnce 在某个文件描述符上调用 epoll_wait, 而 nativeWake 写入一个 IO 操作到描述符, epoll_wait 等待. 然后, 内核从等待状态中取出 epoll 等待线程, 并且该线程继续处理新消息. 如果您熟悉 Java 的 Object.wait()和 Object.notify()方法,可以想象一下 nativePollOnce 大致等同于 Object.wait(), nativeWake 等同于 Object.notify().

但它们的实现完全不同: nativePollOnce 使用 epoll, 而 Object.wait 使用 futex Linux 调用. 值得注意的是, nativePollOnce 和 Object.wait 都不会浪费 CPU 周期, 因为当线程进入任一方法时, 出于线程调度的目的, 该线程将被禁用(引用Object类的javadoc). 但是, 某些事件探查器可能会错误地将等待 epoll 等待(甚至是 Object.wait)的线程识别为正在运行并消耗 CPU 时间, 这是不正确的. 如果这些方法实际上浪费了 CPU 周期, 则所有空闲的应用程序都将使用 100% 的 CPU, 从而加热并降低设备速度.

从上述来看,我们也可以用 Object.wait()和Object.notify()替代nativeWake和nativePollOnce,下面开始吧。

核心方法

enqueueMessage()方法

public boolean enqueueMessage(Message msg) {
        synchronized (lock) {
            Message p = mMessages;
            if (p == null || msg.when == 0 || msg.when < p.when) {
                // New head, wake up the event queue if blocked.
                msg.next = p;
                mMessages = msg;
            } else {
                Message prev;
                for (; ; ) {
                    prev = p;
                    p = p.next;
                    if (p == null || msg.when < p.when) {
                        break;
                    }
                }
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;
            }
            nextPollTimeoutMillis = 0;
            lock.notify();
        }
        return true;
    }

next()方法

public Message next() {
        for (; ; ) {
            synchronized (lock) {
                try {
                    if (nextPollTimeoutMillis < 0) {
                        // 死等
                        lock.wait();
                    } else if (nextPollTimeoutMillis > 0) {
                        // 超时等nextPollTimeoutMillis
                        lock.wait(nextPollTimeoutMillis);
                    } // else nextPollTimeoutMillis = 0; 链表中还有Message未处理,不等
                    final long now = System.currentTimeMillis();
                    Message msg = mMessages;
                    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.
                            mMessages = msg.next;
                            msg.next = null;
                            if (mMessages != null) {
                                nextPollTimeoutMillis = (int) Math.min(mMessages.when - now, Integer.MAX_VALUE);
                                nextPollTimeoutMillis = Math.max(nextPollTimeoutMillis, 0);
                            } else {
                                nextPollTimeoutMillis = -1;
                            }
                            return msg;
                        }
                    } else {
                        // No more messages.
                        nextPollTimeoutMillis = -1;
                        if (DEBUG) Log.d(TAG, "No more messages.");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    return null;
                }
            }
        }
    }

总结如下:
1、Message采用链表的形式,根据时间插入对应的位置,插入取出的方式保持和Android源码一致。
2、在enqueueMessage() 中不管是及时消息还是延时消息,每次将nextPollTimeoutMillis置0,同时notify
3、在next()中判断nextPollTimeoutMillis的值,nextPollTimeoutMillis小于0时,表示下条消息还没到来,一直等;nextPollTimeoutMillis大于0时,超时等待nextPollTimeoutMillis毫秒,处理延时消息;nextPollTimeoutMillis等于0时,链表中还有Message未处理,直接去取出消息。

食用方法

下载源代码:https://gitee.com/cqupt/java_implement_android_handler/tree/master

此项目是一个Android Studio工程,可以使用Android Studio打开运行。

根目录有一个build_and_run.sh文件,可以直接运行MessageMain.java的测试用例。

#!/bin/bash

# 自动测试
# ./build_run_test_fingerprintd.sh -d
dir=$(cd $(dirname $0) && pwd)

cd $dir/app/src/main/java
# # clean
find . -name "*.class"  | xargs rm -f
# build
javac MessageMain.java
# run
java MessageMain
if [ $? -ne 0 ]; then
  REVAL=-1
else
  REVAL=0
fi

cd - >/dev/null 2>&1

function my_return() {
    return $1
}
echo 测试结果:$REVAL
my_return $REVAL

app/src/main/java/TestHandler.java是Android的单元测试用例,对其进行了简单的修改,方便测试。

protected void setUp() throws Exception {
        super.setUp();
        // TODO:没有主线程的loop,这里为了测试通过,需要loop()
        new Thread(new Runnable() {
            @Override
            public void run() {
                Looper.loop(true);
            }
        }).start();

    }

此致,我们使用了简单易懂的方式用纯Java代码,实现了Android的Handler机制,在学习使用中可以查看此源码理解的Android Handler大致实现的原理。

此时还有一个疑问,既然可以比较简单的使用Java的Object.notify()Object.wait()来替代Android中的nativeWake()nativePollOnce(),那么为什么Android会使用C++代码来实现呢?