前言
在上一篇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 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
线程。
可以总结成如下流程图:
图中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中主要使用了nativeWake
和nativePollOnce
等方法,这就是我们需要想办法替换成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++代码来实现呢?