UiAutomator源码分析之UiAutomatorBridge框架

上一篇文章《UIAutomator源码分析之启动和运行》我们描述了uitautomator从命令行运行到加载测试用例运行测试的整个流程,过程中我们也描述了UiAutomatorBridge这个类的重要性,说它相当于UiAutomation的代理(我们都知道UiAutomator是通过UiAutomation和AccessibilityService进行连接然后获取界面空间信息和注入事件的).那么今天开始我们就围绕这个类以及跟它有关系的类进行进一步的分析。


1. UiAutomatorBridge框架

这一章节我们会先看UiAutomatorBridge的整体框架,往后我会编写其他文章通过一些具体的例子把它们串起来。因为我的mackbook pro上没有安装类图软件,所以下图是手画的


往下我们就去初步描述下UiAutomatorBridge跟每一个相关的类的关系。


2. UiAutomatorBridge与UiAutomation的聚合关系

UiAutomatorBridge拥有一个UiAutomation的成员变量,它们是聚合的关系,注意不是组合,因为UiAutomation不一定只能依赖UiAutomatorBridge而存在,我们上一章节的UiAutomatorTestRunner就拥有一个UiAutomation的成员变量。

一旦UiAutomator工具需要通过UiAutomatorBridge获取界面或者注入事件的时候,就会调用该成员变量.比如下面这个很关键的去获取当前界面的Root Node的方法:

[java]  view plain copy
  1. /*     */   public AccessibilityNodeInfo getRootInActiveWindow() {  
  2. /*  66 */     return this.mUiAutomation.getRootInActiveWindow();  
  3. /*     */   }  


3. UiAutomatorBridge与QueryController的关联关系

QueryController做的所有事情就是去把UiSelector这个UI控件选择子翻译成真实的适合我们使用的android.view.accessibility.AccessibilityNodeInfo。 UiAutomatorBridge拥有一个成员变量mQueryController保存了QueryController的一个实例:

[java]  view plain copy
  1. /*     */   private final QueryController mQueryController;  
  2. /*     */     
当UiObject需要获取一个UiSelector指定的控件信息时,会去调用UiAutomatorBridge的getQueryController方法来获得这个mQueryController对象来进行相应的操作,如以下的UiObject的方法findAccessibilityNodeInfo就是这样做的:

[java]  view plain copy
  1. /*      */   protected AccessibilityNodeInfo findAccessibilityNodeInfo(long timeout)  
  2. /*      */   {  
  3. /*  164 */     AccessibilityNodeInfo node = null;  
  4. /*  165 */     long startMills = SystemClock.uptimeMillis();  
  5. /*  166 */     long currentMills = 0L;  
  6. /*  167 */     while (currentMills <= timeout) {  
  7. /*  168 */       node = getQueryController().findAccessibilityNodeInfo(getSelector());  
  8. /*  169 */       if (node != null) {  
  9. /*      */         break;  
  10. /*      */       }  
  11. /*      */         
  12. /*  173 */       UiDevice.getInstance().runWatchers();  
  13. /*      */         
  14. /*  175 */       currentMills = SystemClock.uptimeMillis() - startMills;  
  15. /*  176 */       if (timeout > 0L) {  
  16. /*  177 */         SystemClock.sleep(1000L);  
  17. /*      */       }  
  18. /*      */     }  
  19. /*  180 */     return node;  
  20. /*      */   }  

该getQueryController方法会去调用UiAutomatorBridge的getQueryController方法:

[java]  view plain copy
  1. /*      */   QueryController getQueryController()  
  2. /*      */   {  
  3. /*  100 */     return UiDevice.getInstance().getAutomatorBridge().getQueryController();  
  4. /*      */   }  
从上面的类图我们可以看到,除了UiAutomatorBridge会调用QueryController做事情外,QueryController又会反过来调用UiAutomatorBridge来做事情,因为如图所描述的,只有UiAutomatorBridge拥有UiAutomation的实例,所以QueryController会持有一个UiAutomatorBridge的实例:

[java]  view plain copy
  1. /*     */   private final UiAutomatorBridge mUiAutomatorBridge;  
然后在需要的时候再调用UiAutomatorBridge,如下面的获得Root Node的方法:

[java]  view plain copy
  1. /*     */   protected AccessibilityNodeInfo getRootNode()  
  2. /*     */   {  
  3. /* 168 */     int maxRetry = 4;  
  4. /* 169 */     long waitInterval = 250L;  
  5. /* 170 */     AccessibilityNodeInfo rootNode = null;  
  6. /* 171 */     for (int x = 0; x < 4; x++) {  
  7. /* 172 */       rootNode = this.mUiAutomatorBridge.getRootInActiveWindow();  
  8. /* 173 */       if (rootNode != null) {  
  9. /* 174 */         return rootNode;  
  10. /*     */       }  
  11. /* 176 */       if (x < 3) {  
  12. /* 177 */         Log.e(LOG_TAG, "Got null root node from accessibility - Retrying...");  
  13. /* 178 */         SystemClock.sleep(250L);  
  14. /*     */       }  
  15. /*     */     }  
  16. /* 181 */     return rootNode;  
  17. /*     */   }  

4. UiAutomatorBridge与InteractionController的关联关系

道理与以上的QueryController一样,只是 UiAutomatorBridge需要通过InteractionController做的事情不是去获得控件信息,而是去注入事件

5. UiAutomatorBridge与ShellUiAutomatorBridge的继承关系

UiAutomatorBridge是一个抽象类,里面的方法有以下几个:

  • getRootInActiveWindow :通过UiAutomation获取当前窗口控件xml信息的根节点(通过它可以循环获取所有控件)
  • injectInputEvent :通过UiAutomation注入事件
  • waitForIdle :   通过UiAutomation睡眠指定时间

  • executeCommandAndWaitForAccessibilityEvent:通过UiAutomation执行指定线程的操作然后等待预期的时间返回

  • takeScreenshot :通过UiAutomation进行截图

  • performGlobalAction :  通过UiAutomation去执行一些全局的动作,如打开最近打开过的app列表,回到home界面等

从中可以看到这些动过都是需要通过UiAutomation来执行的,但也有一些动作是不需要用UiAutomation执行的,所以我相信google是为了代码清晰和可维护性,提供了子类ShellUiAutomatorBridge来专门处理那些不需要用到UiAutomation的情况,比如以下的isScreenOn方法就不需要用到UiAutomation,而是直接用PowerManager服务来判断当前屏幕是否是打开的:

[java]  view plain copy
  1. /*     */   public boolean isScreenOn()  
  2. /*     */   {  
  3. /* 111 */     IPowerManager pm = IPowerManager.Stub.asInterface(ServiceManager.getService("power"));  
  4. /*     */       
  5. /* 113 */     boolean ret = false;  
  6. /*     */     try {  
  7. /* 115 */       ret = pm.isScreenOn();  
  8. /*     */     } catch (RemoteException e) {  
  9. /* 117 */       Log.e(LOG_TAG, "Error getting screen status", e);  
  10. /* 118 */       throw new RuntimeException(e);  
  11. /*     */     }  
  12. /* 120 */     return ret;  
  13. /*     */   }  
  14. 上一篇文章《UiAutomator源码分析之UiAutomatorBridge框架》中我们把UiAutomatorBridge以及它相关的类进行的描述,往下我们会尝试根据两个实例将这些类给串联起来,我准备做的是用如下两个很有代表性的实例:

    • 注入事件
    • 获取控件
    这一篇文章我们会通过分析UiDevice的pressHome这个方法来分析UiAutomator是如何注入事件的,下一篇文章会描述如何获取控件,敬请期待。

    1. UiObject.pressHome顺序图

    首先我们看一下我手画的非规范的顺序图,从中我们可以看到pressHome这个动作究竟需要和多少个类进行交互,以及它们是怎么交互的。


    2.这些类是什么时候初始化的

    在我们编写测试用例脚本的时候我们不会对以上所有的类进行初始化,包括UiObject对象都是通过直接在脚本中调用父类UiAutomationTestCase的getUiDevice()这个方法来获得的。其实这些都是在uiautomator运行时由RunTestCommand类的start()这个方法进行初始化的,具体请看《 UIAutomator源码分析之启动和运行》的 3.6章节“初始化UiDevice和UiAutomationBridge“,这里就不做累述。我们这里会看下在初始化UiAutomatorBridge的时候是如何把QuneryControoler和InteractionController一并初始化了的,具体请看UiAutomatorBridge的构造函数:
    [java]  view plain copy
    1. /*     */   UiAutomatorBridge(UiAutomation uiAutomation)  
    2. /*     */   {  
    3. /*  48 */     this.mUiAutomation = uiAutomation;  
    4. /*  49 */     this.mInteractionController = new InteractionController(this);  
    5. /*  50 */     this.mQueryController = new QueryController(this);  
    6. /*     */   }  

    3. 代码跟踪

    首先看UiDevice的pressHome方法:
    [java]  view plain copy
    1. public boolean pressHome() {  
    2. 218        Tracer.trace();  
    3. 219        waitForIdle();  
    4. 220        return getAutomatorBridge().getInteractionController().sendKeyAndWaitForEvent(  
    5. 221                KeyEvent.KEYCODE_HOME, 0, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED,  
    6. 222                KEY_PRESS_EVENT_TIMEOUT);  
    7. 223    }  
    220行:
    • 获得UiDevice对象保存的UiAutomatorBridge对象。着两个对象都是在运行时初始化的,不清楚的话请翻看上面提到的文章
    • 通过UiAutomatorBridge对象获得上面章节初始化的InteractionController对象
    • 调用InteractionController对象的sendKeyAndWaitForEvent方法,里面参数关键是第一个keycode和第二个eventType
      • keycode:代表我们要注入的是按下哪个按键的事件,比如这里我们是KEYCODE_HOME
      • eventType:代表我们注射了该事件后预期会获得窗口返回来的哪种AccessibilityEvent类型,比如我们这里是TYPE_WINDOW_CONTENT_CHANGE
    进入InteractionController类的sendKeyAndWaitForEvent:
    [java]  view plain copy
    1. /*     */   public boolean sendKeyAndWaitForEvent(final int keyCode, final int metaState, int eventType, long timeout)  
    2. /*     */   {  
    3. /* 188 */     Runnable command = new Runnable()  
    4. /*     */     {  
    5. /*     */       public void run() {  
    6. /* 191 */         long eventTime = SystemClock.uptimeMillis();  
    7. /* 192 */         KeyEvent downEvent = new KeyEvent(eventTime, eventTime, 0, keyCode, 0, metaState, -100257);  
    8. /*     */           
    9. /*     */   
    10. /* 195 */         if (InteractionController.this.injectEventSync(downEvent)) {  
    11. /* 196 */           KeyEvent upEvent = new KeyEvent(eventTime, eventTime, 1, keyCode, 0, metaState, -100257);  
    12. /*     */             
    13. /*     */   
    14. /* 199 */           InteractionController.this.injectEventSync(upEvent);  
    15. /*     */         }  
    16. /*     */           
    17. /*     */       }  
    18. /* 203 */     };  
    19. /* 204 */     return runAndWaitForEvents(command, new WaitForAnyEventPredicate(eventType), timeout) != null;  
    20. /*     */   }  
    代码中创建了一个Runnable的线程,线程里面run重写方法要做的事情就是去做注入事件的事情,那么为什么我们不直接去调用事件而需要创建一个线程了,这是因为我们在注入完事件之后还要去等待我们上面定义的预期的eventType是否有出现来判断我们的事件注入究竟是否成功,这个就是204行runAndWaitForEvents做的事情。但我们这里还是先看下线程中是如何注入事件的:
    [java]  view plain copy
    1. /*     */   private boolean injectEventSync(InputEvent event) {  
    2. /* 655 */     return this.mUiAutomatorBridge.injectInputEvent(event, true);  
    3. /*     */   }  
    再跟踪到UiAutomatorBridge对象:
    [java]  view plain copy
    1. /*     */   public boolean injectInputEvent(InputEvent event, boolean sync) {  
    2. /*  70 */     return this.mUiAutomation.injectInputEvent(event, sync);  
    3. /*     */   }  
    可以看到最终还是通过UiAutomation来注入事件的,和我们的预期是一致的。
    我们继续看InteractionController中真正执行注入事件线程的runAndWaitForEvents方法:
    [java]  view plain copy
    1. /*     */   private AccessibilityEvent runAndWaitForEvents(Runnable command, UiAutomation.AccessibilityEventFilter filter, long timeout)  
    2. /*     */   {  
    3. /*     */     try  
    4. /*     */     {  
    5. /* 161 */       return this.mUiAutomatorBridge.executeCommandAndWaitForAccessibilityEvent(command, filter, timeout);  
    6. /*     */     }  
    7. /*     */     catch (TimeoutException e) {  
    8. /* 164 */       Log.w(LOG_TAG, "runAndwaitForEvent timedout waiting for events");  
    9. /* 165 */       return null;  
    10. /*     */     } catch (Exception e) {  
    11. /* 167 */       Log.e(LOG_TAG, "exception from executeCommandAndWaitForAccessibilityEvent", e); }  
    12. /* 168 */     return null;  
    13. /*     */   }  
    代码又跳到了UiAutomatorBridge这个类
    [java]  view plain copy
    1. /*     */   public AccessibilityEvent executeCommandAndWaitForAccessibilityEvent(Runnable command, UiAutomation.AccessibilityEventFilter filter, long timeoutMillis) throws TimeoutException  
    2. /*     */   {  
    3. /* 104 */     return this.mUiAutomation.executeAndWaitForEvent(command, filter, timeoutMillis);  
    4. /*     */   }  
    最终把要执行的runnable执行注入事件的线程command和我们预期事件发生后返回来的窗口事件filter以及超时timeoutMillis传进去,UiAutomation就会和AccessibilityService进行交互以注入事件并且等待预期AccessibilityEvent发生或者超时返回。至于UiAutomation是如何和AccessibilityService交互的,这就超出了这个系列文章的范畴了。也许今后有充裕的时间的话我们再来深入去了解分析它。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值