从一个Uiautomator的官方demo说源码

首先我们先看一个官方给的uiautomator2的例子

# coding: utf-8

import uiautomator2 as u2


def main():
    u = u2.connect_usb()
    u.app_start('com.netease.cloudmusic')
    u(text='私人FM').click()
    u(description='转到上一层级').click()
    u(text='每日歌曲推荐').click()
    u(description='转到上一层级').click()
    u(text='歌单').click()
    u(description='转到上一层级').click()
    u(text='排行榜').click()
    u(description='转到上一层级').click()

if __name__ == '__main__':
    main()

由于云音乐版本的变化 可能这里面有些跑不通的情况,不过这个不是重点。从代码上至少我们大概能明白整个流程。 首先连接设备获取到设备对象后。开始对查找的文本或者描述信息进行查找后点击。

首先我们看这个代码不是解释这个例子要怎么写。而是想通过这个例子来看看uiautomator2背后的过程。
现在我们一步步的进行分析看看。

首先我们先从u = u2.connect_usb()从这里能看出来,因为通过这个方法会返回一个uiautomator的对象,后续的元素定位等都是通过这个对象进行的, 所以我们需要知道具体这个方法到底做了什么?

def connect_usb(serial=None):
    """
    Args:
        serial (str): android device serial

    Returns:
        UIAutomatorServer
    """
    # 实例化了adb的对象,同时做了一个端口转发将本地某个端口(lport)的tcp数据转发到手机上的7912端口上,而手机上监听这个端口的服务实际上就是atx-agent。
    # 同时调用一个connect_wifi的方法我们看到UIAutomatorServer的实例化对象是由这个方法返回的
    # 同时判断agent是否已启动,若没有则手动拉起agent。
    adb = adbutils.Adb(serial)
    lport = adb.forward_port(7912)
    d = connect_wifi('127.0.0.1:' + str(lport))
    if not d.agent_alive:
        warnings.warn("backend atx-agent is not alive, start again ...",
                      RuntimeWarning)
        # TODO: /data/local/tmp might not be execuable and atx-agent can be somewhere else
        adb.shell("/data/local/tmp/atx-agent", "server", "-d")
        deadline = time.time() + 3
        while time.time() < deadline:
            if d.alive:
                break
    elif not d.alive:
        warnings.warn("backend uiautomator2 is not alive, start again ...",
                      RuntimeWarning)
        d.reset_uiautomator()
    return d
def connect_wifi(addr=None):
    """
    Args:
        addr (str) uiautomator server address.

    Returns:
        UIAutomatorServer

    Examples:
        connect_wifi("10.0.0.1")
    """
    # 这里的代码就比较简单了,就是对参数地址做补全,然后实例化了UIAutomatorServer
    if '://' not in addr:
        addr = 'http://' + addr
    if addr.startswith('http://'):
        u = urlparse.urlparse(addr)
        host = u.hostname
        port = u.port or 7912
        return UIAutomatorServer(host, port)
    else:
        raise RuntimeError("address should start with http://")

而具体UIAutomatorServer类的构造函数如下:

class UIAutomatorServer(object):
    __isfrozen = False
    __plugins = {}
    # 从构造函数看的话,没有看出具体的内容,只是做了一些初始化的工作而已。但是这里比较关键的是实例化了Session这个类
    def __init__(self, host, port=7912):
        """
        Args:
            host (str): host address
            port (int): port number

        Raises:
            EnvironmentError
        """
        self._host = host
        self._port = port
        self._reqsess = TimeoutRequestsSession(
        )  # use requests.Session to enable HTTP Keep-Alive
        self._server_url = 'http://{}:{}'.format(host, port)
        self._server_jsonrpc_url = self._server_url + "/jsonrpc/0"
        self._default_session = Session(self, None)
        self._cached_plugins = {}
        self.__devinfo = None
        self._hooks = {}
        self.platform = None  # hot fix for weditor

        self.ash = AdbShell(self.shell)  # the powerful adb shell
        self.wait_timeout = 20.0  # wait element timeout
        self.click_post_delay = None  # wait after each click
        self._freeze()  # prevent creating new attrs
        # self._atx_agent_check()

我们再来看看Session这个类的构造函数

class Session(object):
    __orientation = (  # device orientation
        (0, "natural", "n", 0), (1, "left", "l", 90),
        (2, "upsidedown", "u", 180), (3, "right", "r", 270))

    # 这里的server实际上就是uiautomatorServer的实例化对象,相当于在session这个类中完全能够调用uiautomatorServer所有暴露的方法。 
    # 另外这里面的jsonrpc,以及shell其实是两个比较关键的点,但是这里我们先不介绍 因为下面具体的执行还是会涉及到的
    def __init__(self, server, pkg_name=None, pid=None):
        self.server = server
        self._pkg_name = pkg_name
        self._pid = pid
        self._jsonrpc = server.jsonrpc
        if pid and pkg_name:
            jsonrpc_url = server.path2url('/session/%d:%s/jsonrpc/0' %
                                          (pid, pkg_name))
            self._jsonrpc = server.setup_jsonrpc(jsonrpc_url)

        # hot fix for session missing shell function
        self.shell = self.server.shell

另外回到connect_usb函数的逻辑中
判断atx-agent是否在线

@property
def agent_alive(self):
    # 通过发送http请求获取axt-agent的版本,若获取失败则 atx-agent启动失败 
    try:
        r = self._reqsess.get(self.path2url('/version'), timeout=2)
        return r.status_code == 200
    except:
        return False

那么我们再看看 u.app_start('com.netease.cloudmusic')这个方法主要做了什么

    def app_start(self,
                  pkg_name,
                  activity=None,
                  extras={},
                  wait=True,
                  stop=False,
                  unlock=False):
        """ Launch application
        Args:
            pkg_name (str): package name
            activity (str): app activity
            stop (bool): Stop app before starting the activity. (require activity)
        """
        # 这里判断设备是锁屏状态,如果是的话则解锁操作
        if unlock:
            self.unlock()
        # 这里判断是否带有activity的参数 如果有的话 则启动指定的activity
        if activity:
            # -D: enable debugging
            # -W: wait for launch to complete
            # -S: force stop the target app before starting the activity
            # --user  | current: Specify which user to run as; if not
            #    specified then run as the current user.
            # -e  
            # --ei  
            # --ez  
            args = ['am', 'start', '-a', 'android.intent.action.MAIN',
                    '-c', 'android.intent.category.LAUNCHER']
            if wait:
                args.append('-W')
            if stop:
                args.append('-S')
            args += ['-n', '{}/{}'.format(pkg_name, activity)]
            # -e --ez
            extra_args = []
            for k, v in extras.items():
                if isinstance(v, bool):
                    extra_args.extend(['--ez', k, 'true' if v else 'false'])
                elif isinstance(v, int):
                    extra_args.extend(['--ei', k, str(v)])
                else:
                    extra_args.extend(['-e', k, v])
            args += extra_args
            # 'am', 'start', '-W', '-n', '{}/{}'.format(pkg_name, activity))
            self.shell(args)
        else:
            # 若没有则通过shell 命令运行monkey的命令将对应的app 唤起 这个方式很有意思
            if stop:
                self.app_stop(pkg_name)
            self.shell([
                'monkey', '-p', pkg_name, '-c',
                'android.intent.category.LAUNCHER', '1'
            ])

下来则到了我们的真正重要的内容了 u(text='私人FM').click()到这里的话我们首先要注意一个事情,因为前面我们有讲过connect_usb返回的是一个uiautomator的对象,而这里的调用是将一个类对象通过函数传参的方式进行调用了。所以这里会涉及到一个方法叫__call__()

call

所有的函数都是可调用对象。一个类实例也可以变成一个可调用对象,只需要实现一个特殊方法__call__()

所以我们这里需要看的是UIAutomatorServer的__call__方法的实现了。

# 从这里我们又可以看到这里实际上又调用了Session这个类的__call__方法
def __call__(self, **kwargs):
    return self._default_session(**kwargs)

这里顺便普及下python中*args和**kwargs

*args表示任何多个无名参数,它是一个tuple;**kwargs表示关键字参数,它是一个 dict。

所以u(text='私人FM')实际上就是个关键字传参,所以kwargs 实际上就是 {text: ‘私人FM’}

我们继续往下看

#  嗯 这里返回了一个UiObject的对象回去了。 我们暂时就先不看Selecotr这个类了
def __call__(self, **kwargs):
    # 这里传了两个参数 一个是session的实例化对象,另外一个是Selector的实例化对象
    return UiObject(self, Selector(**kwargs))

以下则是UiObject类的构造函数, 以下其实就是几个赋值,重点的还是jsonrpc的赋值,其实这个jsonrpc是uiautomatorServer中的jsonrpc,这一点我们要注意。

class UiObject(object):
    def __init__(self, session, selector):
        self.session = session
        self.selector = selector
        self.jsonrpc = session.jsonrpc

uiobject的对象已经实例化完成了,下来当然就是最后的click()方法的内容了

def click(self, timeout=None, offset=None):
    """
    Click UI element. 

    Args:
        timeout: seconds wait element show up
        offset: (xoff, yoff) default (0.5, 0.5) -> center

    The click method does the same logic as java uiautomator does.
    1. waitForExists 2. get VisibleBounds center 3. send click event

    Raises:
        UiObjectNotFoundError
    """
    self.must_wait(timeout=timeout)
    x, y = self.center(offset=offset)
    # ext.htmlreport need to comment bellow code
    # if info['clickable']:
    #     return self.jsonrpc.click(self.selector)
    self.session.click(x, y)
    delay = self.session.server.click_post_delay
    if delay:
        time.sleep(delay)

这里我们重点先关注self.must_wait(timeout=timeout)因为这个是真正的去查找元素的方法,我们代码继续往下看。

def wait(self, exists=True, timeout=None):
    """
    Wait until UI Element exists or gone

    Args:
        timeout (float): wait element timeout

    Example:
        d(text="Clock").wait()
        d(text="Settings").wait("gone") # wait until it's gone
    """
    if timeout is None:
        timeout = self.wait_timeout
    http_wait = timeout + 10
    if exists:
        try:
            return self.jsonrpc.waitForExists(
                self.selector, int(timeout * 1000), http_timeout=http_wait)
        except requests.ReadTimeout as e:
            warnings.warn("waitForExists readTimeout: %s" %
                          e, RuntimeWarning)
            return self.exists()
    else:
        try:
            return self.jsonrpc.waitUntilGone(
                self.selector, int(timeout * 1000), http_timeout=http_wait)
        except requests.ReadTimeout as e:
            warnings.warn("waitForExists readTimeout: %s" %
                          e, RuntimeWarning)
            return not self.exists()

这里我们会发现其实wait方法最后调用到的是self.jsonrpc.waitForExists( self.selector, int(timeout * 1000), http_timeout=http_wait)这个方法上了,所以我们还是前面说的 我们这里就需要好好的分析下jsonrpc到底是做什么的了, 我们回到uiautomatorServer的类中去查看。

# 实际上jsonrpc是一个 JSONRpcWrapper 这个内部类的一个实例化对象,
def setup_jsonrpc(self, jsonrpc_url=None):
    """
    Wrap jsonrpc call into object
    Usage example:
        self.setup_jsonrpc().pressKey("home")
    """
    if not jsonrpc_url:
        jsonrpc_url = self._server_jsonrpc_url

    class JSONRpcWrapper():
        def __init__(self, server):
            self.server = server
            self.method = None

        def __getattr__(self, method):
            self.method = method  # jsonrpc function name
            return self

        def __call__(self, *args, **kwargs):
            http_timeout = kwargs.pop('http_timeout', HTTP_TIMEOUT)
            params = args if args else kwargs
            return self.server.jsonrpc_retry_call(jsonrpc_url, self.method,
                                                  params, http_timeout)

    return JSONRpcWrapper(self)

self.jsonrpc.waitForExists这个方法的调用我们会发现 实际上JSONRpcWrapper这个类中根本就没有这个方法存在,但是根据python的特性 它实际上是首先调用了__getattr__这个方法。所以其实是给method进行了赋值,再来根据我们前面讲到的__call__所以其实self.jsonrpc.waitForExists(self.selector, int(timeout * 1000), http_timeout=http_wait)这个方法实际上调用的就是JSONRpcWrapper中的__call__方法。 所以我们又要继续往下看jsonrpc_retry_call这个方法了。

# 实际上 jsonrpc_retry_call最后是调用jsonrpc_call。我们可以发现其实这里的逻辑就是发送一个http请求给到我们前面我们本地的一个http,端口则是前面说到端口转发的时候获取到的一个本地端口,通过这个http adb则会将这个端口转发到手机的7912端口 由atx-agent进行处理 返回相应的处理结果。
def jsonrpc_call(self, jsonrpc_url, method, params=[], http_timeout=60):
    """ jsonrpc2 call
    Refs:
        - http://www.jsonrpc.org/specification
    """
    request_start = time.time()
    data = {
        "jsonrpc": "2.0",
        "id": self._jsonrpc_id(method),
        "method": method,
        "params": params,
    }
    data = json.dumps(data).encode('utf-8')
    res = self._reqsess.post(
        jsonrpc_url,  # +"?m="+method, #?method is for debug
        headers={"Content-Type": "application/json"},
        timeout=http_timeout,
        data=data)
    if DEBUG:
        print("Shell$ curl -X POST -d '{}' {}".format(data, jsonrpc_url))
        print("Output> " + res.text)
    if res.status_code == 502:
        raise GatewayError(res, "gateway error, time used %.1fs" %
                           (time.time() - request_start))
    if res.status_code == 410:  # http status gone: session broken
        raise SessionBrokenError("app quit or crash", jsonrpc_url,
                                 res.text)
    if res.status_code != 200:
        raise UiaError(jsonrpc_url, data, res.status_code, res.text,
                       "HTTP Return code is not 200", res.text)
    jsondata = res.json()
    error = jsondata.get('error')
    if not error:
        return jsondata.get('result')

    # error happends
    err = JsonRpcError(error, method)

    if isinstance(err.data, six.string_types) and 'UiAutomation not connected' in err.data:
        err.__class__ = UiAutomationNotConnectedError
    elif err.message:
        if 'uiautomator.UiObjectNotFoundException' in err.message:
            err.__class__ = UiObjectNotFoundError
        elif 'android.support.test.uiautomator.StaleObjectException' in err.message:
            # StaleObjectException
            # https://developer.android.com/reference/android/support/test/uiautomator/StaleObjectException.html
            # A StaleObjectException exception is thrown when a UiObject2 is used after the underlying View has been destroyed.
            # In this case, it is necessary to call findObject(BySelector) to obtain a new UiObject2 instance.
            err.__class__ = StaleObjectExceptionError
        elif 'java.lang.NullObjectException' in err.message:
            err.__class__ = NullObjectExceptionError
        elif 'java.lang.NullPointerException' == err.message:
            err.__class__ = NullPointerExceptionError
    raise err

所以我们再回到UIObject的click方法中重新梳理下这个流程

def click(self, timeout=None, offset=None):
    """
    Click UI element. 

    Args:
        timeout: seconds wait element show up
        offset: (xoff, yoff) default (0.5, 0.5) -> center

    The click method does the same logic as java uiautomator does.
    1. waitForExists 2. get VisibleBounds center 3. send click event

    Raises:
        UiObjectNotFoundError
    """
    # 通过http请求发送method为waitForExists的请求 确认元素是否存在
    self.must_wait(timeout=timeout)
    # 通过http请求发送method为objectInfo的请求获取元素的的可见区域,再定位到元素的中点
    x, y = self.center(offset=offset)
    # ext.htmlreport need to comment bellow code
    # if info['clickable']:
    #     return self.jsonrpc.click(self.selector)
    # 通过http请求发送method为click的请求点击对应的坐标
    self.session.click(x, y)
    delay = self.session.server.click_post_delay
    if delay:
        time.sleep(delay)

至此u(text='私人FM').click()这个过程就结束了,那么剩下的其他步骤其实整个逻辑基本都是这样子的。

不过其实我们还需要了解atx-agent的逻辑以另外真正执行操作的apk的逻辑才能够真正清楚uiautomator2的整个过程。

你可能感兴趣的:(automator)