为包含工作线程Android程序编写稳定的instrumentation测试

最近在给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做了几件重要的事情:

  1. 在消息队列中增加一个IdleHandler的实现Idler
  2. 通过handler,在消息队列中增加一个空消息(空消息表示表示其处理函数为空)
  3. 调用idler.waitForIdle。在其实现中会调用wait,而wait会被idler.queueIdle notify。

简单的说,它是线程的消息队列中插入在线程空闲时会被调用的handler,然后等待该handler被执行。如果该handler被执行了,说明线程消息队列空了,所以instrumentation.waitForIdleSync返回时,UI线程已经执行完所有操作,于是可以进行验证或者做其他操作了。

现在有两个新的问题:

  1. MessageQueue.IdleHandler具体何时被执行?
  2. 为何要添加一个空消息?

我们可以阅读下MessageQueue代码。需要关心两个函数:

enqueueMessage是把消息按照时间顺序放到消息队列中。如果发现下面提到的Next被NativePollOnce阻塞,会调用nativeWake唤醒nativePollOnce

next会获取下一个等待处理的合适消息。如果没有合适的消息(没有消息或者有消息但没到期),则一直不返回。具体而言,它会在一个for循环中做三件事情。

  1. 以nextPollTimeoutMillis为参数调用nativePollOnce。如果是0,表示马上返回;如果是一个正数,则最多等待该时间,如果之前有native消息到达,也会返回;-1则是一直等待,直到native消息到达。
  2. 查看消息队列的第一个消息(因为已经按时间排序,所以第一个是最老的)
    • 如果找到应该处理的时间在当前时间之前的消息,则直接返回该消息。返回前会设置nextPollTimeoutMillis为0。
    • 如果最老的消息的处理时间也晚于当前时间,则设置nextPollTimeoutMillis为二者的时间差。
    • 如果消息队列为空,设置nextPollTimeoutMillis为-1。
  3. 对于上面的后两种情况,会调用注册过的IdleHandler。对于一次性的IdleHandler,则执行后马上删除;持久性的则每个循环都会被执行。是否是一次性的依赖于IdleHandler.queueIdle返回true还是false。

现在解答了第一个问题,就是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循环多调用几次就行了。

原文地址:https://www.cnblogs.com/xichengtie/p/3314565.html