• 为包含工作线程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循环多调用几次就行了。

  • 相关阅读:
    JSP页面重定向与页面内容转发
    tomcat访问所有的资源,都是用Servlet来实现的
    War包是什么??
    jsp中include的两种用法
    TED #05# How we can face the future without fear, together
    TED #04#
    学写网页 #03# 固定在某个角落
    TED #03# 10 ways to have a better conversation
    TED #02#
    学写网页 #02# 无题
  • 原文地址:https://www.cnblogs.com/xichengtie/p/3314565.html
Copyright © 2020-2023  润新知