最近在给android程序写测试。
测试对象有UI线程和一个工作线程。UI线程负责处理用户交互,而工作线程做一些大计算量的工作。众所周知(如果不知道,阅读android testing fundmentals),在android的test framework里面,有一个独立的测试线程。
现在给一个跟工作线程交互的Activity UI类写测试。这个Activity会给工作线程发消息A,工作线程处理完之后,再给Activity发个消息B回来,然后Activity再作处理。
写的时候发现测试非常不稳定。加上instrumentation.waitForIdleSync,仍然是无济于事。通过调试,发现是instrumentation.waitForIdleSync之后,虽然UI线程执行完了消息,但工作线程仍然没有完成。那该如何确保工作线程也把消息执行完了呢?
开始想到的方式是使用wait-notify机制。可以在测试代码中wait一定条件的发生,然后Activity对消息B的响应方法执行结束时notify。此时测试代码继续执行。通过这种方式保证了三个线程的同步,所以测试也就可以非常稳定。但这么做的缺点是需要在工作代码中增加为了测试的同步操作;而且针对不同的activity和不同的方法,可能要在多处增加这类代码。对于前者,我们可以通过对activity子类化的办法,只在这个供测试的子类中增加notify操作,避免污染工作类,不该这将产生大量供测试的子类;对于后者,虽然可以通过提取功能类的办法使代码清晰,但还是很难避免多处调用。所以不是很喜欢这种针对特定类specific的办法,而更希望是一种通用的办法。
其实我希望有一种类似于instrumentation.waitForIdleSync的函数。这个函数可以等待UI线程的消息队列中的消息都被处理完,从而实现测试线程和UI线程的同步。我也希望能有一个函数等待工作线程的消息被处理完。
那instrumentation.waitForIdleSync是如何实现的呢?这里是函数的实现代码:
public void waitForIdleSync() { validateNotAppThread(); Idler idler = new Idler(null); mMessageQueue.addIdleHandler(idler); mThread.getHandler().post(new EmptyRunnable()); idler.waitForIdle(); } private static final class EmptyRunnable implements Runnable { public void run() { } } private static final class Idler implements MessageQueue.IdleHandler { private final Runnable mCallback; private boolean mIdle; public Idler(Runnable callback) { mCallback = callback; mIdle = false; } public final boolean queueIdle() { if (mCallback != null) { mCallback.run(); } synchronized (this) { mIdle = true; notifyAll(); } return false; } public void waitForIdle() { synchronized (this) { while (!mIdle) { try { wait(); } catch (InterruptedException e) { } } } } }
这里mMessageQueue是UI线程对应的消息队列类,mThread是UI线程的对应类。waitForIdelSync做了几件重要的事情:
- 在消息队列中增加一个IdleHandler的实现Idler
- 通过handler,在消息队列中增加一个空消息(空消息表示表示其处理函数为空)
- 调用idler.waitForIdle。在其实现中会调用wait,而wait会被idler.queueIdle notify。
简单的说,它是线程的消息队列中插入在线程空闲时会被调用的handler,然后等待该handler被执行。如果该handler被执行了,说明线程消息队列空了,所以instrumentation.waitForIdleSync返回时,UI线程已经执行完所有操作,于是可以进行验证或者做其他操作了。
现在有两个新的问题:
- MessageQueue.IdleHandler具体何时被执行?
- 为何要添加一个空消息?
我们可以阅读下MessageQueue代码。需要关心两个函数:
enqueueMessage是把消息按照时间顺序放到消息队列中。如果发现下面提到的Next被NativePollOnce阻塞,会调用nativeWake唤醒nativePollOnce
next是会获取下一个等待处理的合适消息。如果没有合适的消息(没有消息或者有消息但没到期),则一直不返回。具体而言,它会在一个for循环中做三件事情。
- 以nextPollTimeoutMillis为参数调用nativePollOnce。如果是0,表示马上返回;如果是一个正数,则最多等待该时间,如果之前有native消息到达,也会返回;-1则是一直等待,直到native消息到达。
- 查看消息队列的第一个消息(因为已经按时间排序,所以第一个是最老的)
- 如果找到应该处理的时间在当前时间之前的消息,则直接返回该消息。返回前会设置nextPollTimeoutMillis为0。
- 如果最老的消息的处理时间也晚于当前时间,则设置nextPollTimeoutMillis为二者的时间差。
- 如果消息队列为空,设置nextPollTimeoutMillis为-1。
现在解答了第一个问题,就是IdleHandler会在消息队列中没有消息或者消息都没到期时执行。这里我们也可以看到,如果有延时执行的消息,waitForIdleSync并不会等到这些延时消息执行之后才返回。
至于第二个问题,如果添加IdleHandler之前message队列已经为空,那next会阻塞在nativePollOnce上(参数为-1)。此时通过enqueueMessage一个空消息,它会调用nativeWake唤醒nativePollOnce,之前它会发现一个消息,然后函数返回该空消息,并设置nextPollTimeoutMillis为0;然后再下次调用next的时候,因为nextPollTimeoutMillis为0,所以nativePollOnce不等待,然后没有发现消息,从而能执行IdleHandler。
对于直接用户硬件操作的消息,例如touch,以前我觉得也是放到消息队列中的。后来通过debug,才知道对于这些硬件消息,例如touch,nativePollOnce会直接调用InputEventReceiver.dispatchInputEvent,并进而从根窗口到具体焦点窗口的传递。这类消息并没有进入消息队列。我想这么做的原因是为了保证对用户操作的快速响应吧,否则还要跟其他消息进行排队依次处理。对于instrumentation.waitForIdleSync,因为IdleHandler(for循环的第三步)之前的会调用nativePollOnce(for循环的第一步),所以也会保证这类不进入消息队列的消息被处理完成。
理解了instrumentation.waitForIdleSync的原理,我们可以写一个针对任何线程都可用的等待类:
public class HandlerThreadIdleWaiter { public static void waitForIdleSync(Handler handler) { final Idler idler = new Idler(); handler.post(new Runnable() { @Override public void run() { Looper.myQueue().addIdleHandler(idler); } }); handler.post(new Runnable() { @Override public void run() { } }); idler.waitForIdle(); } private static class Idler implements MessageQueue.IdleHandler { private boolean mIdle; public Idler() { mIdle = false; } @Override public final boolean queueIdle() { synchronized (this) { mIdle = true; notifyAll(); } return false; } public void waitForIdle() { synchronized (this) { while (!mIdle) { try { wait(); } catch (InterruptedException e) { // Do nothing. } } } } } }
使用时,只需要在静态方法中传入需要等待的线程的Handler。
调用的时候,针对前面的例子,我要先调用instrumentation.waitForIdleSync一次,等待UI线程的事情做完;然后调用这个自己写的wait函数一次,等待工作线程做完;然后因为工作线程又给UI线程传了一个消息,所以要在instrumentation wait一次,等待最后那个消息也被处理完。如果写测试时为了偷懒节省分析时间,就把这两个wait循环多调用几次就行了。