MonkeyRunner is DEAD

UI Automator

developer.android.com/training/te…java

Android 平臺全部自動化測試框架的底層實現都依賴官方提供的 UI Automator 測試框架,適用於跨系統和已安裝應用程序的跨應用程序功能UI測試。主要功能包括三部分:node

  • UI Automator Viewer 檢查佈局層次結構的查看器。
  • UiDevice 設備狀態信息並在目標設備上執行操做的API。
  • UI Automator API 支持跨應用程序UI測試的API。

UI Automator Viewer

PC 端 GUI 工具,掃描和分析 Android 設備上當前顯示的 UI 組件。展現 UI 佈局層次結構,查看設備上當前對用戶可見的 UI 組件的屬性。從名稱能夠看出,它是 UI Automator 的只讀功能部分,即只能查看 UI 組件的樹形結構和屬性,不能操做控制 UI 組件。python

uiautomatorviewer 位於 <android-sdk>/tools/bin 目錄。
啓動入口是一個bash文件,實際調用 <android-sdk>/tools/lib 目錄的 uiautomatorviewer-26.0.0-dev.jar
GUI 基於 Eclipse + SWT 實現,使用 Gradle 構建。
系列工具源碼在 https://android.googlesource.com/platform/tools/swt/ 。 依賴 https://android.googlesource.com/platform/tools/base/
活躍分支: mirror-goog-studio-master-dev
該倉庫還包含如下工具。android

  • chimpchat
  • ddms
  • hierarchyviewer2
  • monkeyrunner
  • swtmenubar
  • traceview

其內部實現基於 adb shell uiautomator dump 。從源碼倉庫提交記錄看,主要功能開發的活躍時間是 2014-2015,2016以後已經不多更新維護。那個年代的 Android 開發主要使用 Eclipse , 因此基於 SWT 實現多平臺 PC GUI ,在當時合理。git

該工具實際使用運行不穩定,極易報錯。github

Error while obtaining UI hierarchy XML file: com.android.ddmlib.SyncException: Remote object doesn't exist!shell

錯誤緣由一般是:express

  • adb 鏈接通道不穩定。
  • 機型兼容性問題,權限問題。
  • 當前手機應用程序界面處於動態,例如播放視頻,動畫。而且10秒超時時間仍未進入靜態。

分析源碼可知,錯誤都源於 Android Framework uiautomator編程

MonkeyRunner

developer.android.com/studio/test…bash

官方提供的另一個工具,封裝 uiautomator API,供 Python 腳本調用,也可注入 java 擴展插件。
相比 uiautomatorvieweruiautomator 命令行工具,可編程擴展性更佳。
MonkeyRunner 使用了比較冷門的 Jython 實現。

1. 啓動運行入口

monkeyrunner -plugin <plugin_jar> <program_filename> <program_options>

monkeyrunner 是一個bash文件,位於 <android-sdk>/tools/bin ,啓動調用 <android-sdk>/tools/lib/monkeyrunner-26.0.0-dev.jar

export ANDROID_HOME="~/Library/Android/sdk"
$ANDROID_HOME/tools/bin/monkeyrunner uiparser.py
複製代碼

2. 主要方法

MonkeyDevice.getProperty()

等同於調用 adb shell getprop <keyword> 。獲取設備系統環境變量。
不一樣廠商的設備,key可能不一樣。針對具體測試機型,可以使用 adb shell getprop ,顯示全部系統環境變量的key。

MonkeyDevce.shell()

等同於調用adb shell命令。

3. 缺陷

MonkeyRunner 基於 Jython 2.5.3 。看上去結合了Java和Python的優點,實際對於Java和Python編程都不友好。

  • Jython 2.5.3 過期,主流的Python 3.x和2.7的不少語法和庫沒法使用。
  • 使用vscode等編輯器編碼時,缺乏智能提示和自動補全。編輯器和pylint沒法識別導入的庫, 例如 from com.android.monkeyrunner import MonkeyRunner, MonkeyDevice, MonkeyImage
  • Jython 彷佛不能像常規的python程序同樣引用外部庫。實測只能使用 MonkeyRunner 內置的 os, sys, subprocess 等庫。
  • Java extend plugin 能作的事情較少。

MonkeyRunner 實際仍然是使用 adb shell 和其中的 uiautomator 命令獲取UI組件狀態和屬性。因此它跟 UI Automator Viewer 同樣受限於 uiautomator 自己的缺陷,致使運行不穩定。

adb shell uiautomator

adb
developer.android.google.cn/studio/comm…

adb shell am
developer.android.google.cn/studio/comm…
使用 Activity Manager (am) 工具發出命令以執行各類系統操做,如啓動 Activity、強行中止進程、廣播 intent、修改設備屏幕屬性及其餘操做。

adb shell pm
developer.android.google.cn/studio/comm…
使用軟件包管理器 Package Manager (pm) 工具發出命令,安裝,卸載,查詢安裝包。

adb shell uiatomator
官網相關頁面已被刪除,僅能從搜索引擎歷史快照中找到。猜想可能近期會有變動,或者官方建議再也不使用。
經過執行命令能夠查看使用方法和參數。

Usage: uiautomator <subcommand> [options]

Available subcommands:

help: displays help message

runtest: executes UI automation tests
    runtest <class spec> [options]
    <class spec>: <JARS> < -c <CLASSES> | -e class <CLASSES> >
      <JARS>: a list of jar files containing test classes and dependencies. If
        the path is relative, it's assumed to be under /data/local/tmp. Use absolute path if the file is elsewhere. Multiple files can be specified, separated by space. <CLASSES>: a list of test class names to run, separated by comma. To a single method, use TestClass#testMethod format. The -e or -c option may be repeated. This option is not required and if not provided then all the tests in provided jars will be run automatically. options: --nohup: trap SIG_HUP, so test won't terminate even if parent process
               is terminated, e.g. USB is disconnected.
      -e debug [true|false]: wait for debugger to connect before starting.
      -e runner [CLASS]: use specified test runner class instead. If
        unspecified, framework default runner will be used.
      -e <NAME> <VALUE>: other name-value pairs to be passed to test classes.
        May be repeated.
      -e outputFormat simple | -s: enabled less verbose JUnit style output.

dump: creates an XML dump of current UI hierarchy
    dump [--verbose][file]
      [--compressed]: dumps compressed layout information.
      [file]: the location where the dumped XML should be stored, default is
      /sdcard/window_dump.xml

events: prints out accessibility events until terminated
複製代碼

uiautomator 缺陷

運行耗時長,失敗率高,頻繁報錯。
ERROR: could not get idle state. 一般表示當前UI處於動態渲染刷新期間,例如正在播放視頻,動畫。在10秒超時時間內仍未進入靜態。由於此時 UI 樹的節點對象快速變化中,不能穩定獲取。

uiautomator 源碼

PC端工具源碼位於倉庫 android.googlesource.com/platform/fr… master 分支。
最新更新於 2014.11.14。以後活躍分支變動爲 android-support-test 分支。uiautomator 源碼被移除,改爲 android.support.test library, expresso 等工具的源碼工程。
手機端框架源碼位於倉庫 android.googlesource.com/platform/fr… master 分支。
uiAutomation.waitForIdle(1000, 1000 * 10); 是報錯的關鍵代碼,即單次超時等待1秒,最長超時等待10秒。超時拋出異常。

DumpCommand.java

android.googlesource.com/platform/fr…

// It appears that the bridge needs time to be ready. Making calls to the
// bridge immediately after connecting seems to cause exceptions. So let's also
// do a wait for idle in case the app is busy.
try {
    UiAutomation uiAutomation = automationWrapper.getUiAutomation();
    uiAutomation.waitForIdle(1000, 1000 * 10);
    AccessibilityNodeInfo info = uiAutomation.getRootInActiveWindow();
    if (info == null) {
        System.err.println("ERROR: null root node returned by UiTestAutomationBridge.");
        return;
    }
    Display display =
            DisplayManagerGlobal.getInstance().getRealDisplay(Display.DEFAULT_DISPLAY);
    int rotation = display.getRotation();
    Point size = new Point();
    display.getSize(size);
    AccessibilityNodeInfoDumper.dumpWindowToFile(info, dumpFile, rotation, size.x, size.y);
} catch (TimeoutException re) {
    System.err.println("ERROR: could not get idle state.");
    return;
} finally {
    automationWrapper.disconnect();
}
System.out.println(
        String.format("UI hierchary dumped to: %s", dumpFile.getAbsolutePath()));
複製代碼

UiAutomation.java

android.googlesource.com/platform/fr…

/** * Waits for the accessibility event stream to become idle, which is not to * have received an accessibility event within <code>idleTimeoutMillis</code>. * The total time spent to wait for an idle accessibility event stream is bounded * by the <code>globalTimeoutMillis</code>. * * @param idleTimeoutMillis The timeout in milliseconds between two events * to consider the device idle. * @param globalTimeoutMillis The maximal global timeout in milliseconds in * which to wait for an idle state. * * @throws TimeoutException If no idle state was detected within * <code>globalTimeoutMillis.</code> */
public void waitForIdle(long idleTimeoutMillis, long globalTimeoutMillis) throws TimeoutException {
    synchronized (mLock) {
        throwIfNotConnectedLocked();
        final long startTimeMillis = SystemClock.uptimeMillis();
        if (mLastEventTimeMillis <= 0) {
            mLastEventTimeMillis = startTimeMillis;
        }
        while (true) {
            final long currentTimeMillis = SystemClock.uptimeMillis();
            // Did we get idle state within the global timeout?
            final long elapsedGlobalTimeMillis = currentTimeMillis - startTimeMillis;
            final long remainingGlobalTimeMillis =
                    globalTimeoutMillis - elapsedGlobalTimeMillis;
            if (remainingGlobalTimeMillis <= 0) {
                throw new TimeoutException("No idle state with idle timeout: "
                        + idleTimeoutMillis + " within global timeout: "
                        + globalTimeoutMillis);
            }
            // Did we get an idle state within the idle timeout?
            final long elapsedIdleTimeMillis = currentTimeMillis - mLastEventTimeMillis;
            final long remainingIdleTimeMillis = idleTimeoutMillis - elapsedIdleTimeMillis;
            if (remainingIdleTimeMillis <= 0) {
                return;
            }
            try {
                  mLock.wait(remainingIdleTimeMillis);
            } catch (InterruptedException ie) {
                  /* ignore */
            }
        }
    }
}
複製代碼

Android Device Monitor

developer.android.com/studio/prof…

Android SDK 工具集的 Android Device Monitor 已廢棄。

Android Device Monitor was deprecated in Android Studio 3.1 and removed from Android Studio 3.2. The features that you could use through the Android Device Monitor have been replaced by new features. The table below helps you decide which features you should use instead of these deprecated and removed features.

官方給出的替代品 Layout Inspector 功能更強大,界面也更美觀,但目前還不成熟,相比 iOS 神器 Reveal , 仍需努力。
developer.android.com/studio/debu…

uiparser

參照 MonkeyRunner 官方文檔實現的 Python Demo。

github.com/9468305/pyt…

TODO

基於上述問題,我準備寫一個更智能更穩定更高效的 UI Inspecotr ,基於 AndroidX UIAutomation ,使用 Kotlin 實現。

相關文章
相關標籤/搜索