AirTest 基本使用及框架淺剖析——五分鐘上手製作遊戲輔助

簡介

Airtest Project 是爲編寫自動化腳本,達到提升測試效率的一整套解決方案。它可以輕鬆的擴展到多平臺、多引擎上;如基礎的 Android和IOS手機應用、App;Windows上的應用等。

學習使用 Airtest Project 很容易,由於 Airtest Project 是基於Python的,只需要會一點基礎的 Python 基礎知識即可。Airtest Project 需要一個開發環境,推薦使用配套的 AirtestIDE;AirtestIDE針對於 Airtest Project 有一些特殊的功能,使用別的環境可能會讓你開發時工作繁瑣,效率降低等。

Airtest Project 包含了兩個框架,一個是 Airtest 一個是 Poco,這兩個框架都是Python 的第三方庫。在開發過程中,可以在開發時引入其它庫加強你的腳本。

Airtest
是一個跨平臺的、基於圖像識別的UI自動化測試框架,適用於遊戲和App,支持平臺有Windows、Android和iOS——引於官方文檔

Airtest 可實現“即看見可操作”,但是對文本內容的獲取缺無能爲力;這一點在官方文檔中也有說明。這一點缺點也有解決辦法:可通過引入文字識別庫進行補缺,如:pytesseract。

使用 Airtest 進行自動化測試時,操作流程一般爲:圖片截取 → 圖片對比 → 相似度與設定值對比 → 找出座標位置 → 點擊。默認情況下 Airtest 對於不同顏色的對比並不敏感,需要開啓顏色對比。

在測試對象非原生App或無法取得項目源碼時使用 Airtest 進行測試是個很好的選擇。

Poco
是一款基於UI控件識別的自動化測試框架,目前支持Unity3D/cocos2dx-*/Android原生app/iOS原生app/微信小程序,也可以在其他引擎中自行接入poco-sdk來使用——引於官方文檔

在已有項目源碼或測試對象爲原生App時使用Poco進行自動化測試,不僅滿足可對文本的獲取,而且相比 Airtest 更爲簡潔!本篇只講解 Airtest 的操作。

AirtestIDE
是一個我們配套推出的跨平臺的UI自動化測試編輯器,內置了Airtest和Poco的相關插件功能,能夠使用它快速簡單地編寫腳本——引於官方文檔

使用 AirtestIDE 將極大的簡便我們的開發過程,對開發者非常友好。提供了截圖及截圖預覽、可連接設備自動讀取、高亮的編輯界面、腳本錄製、支持設備遠程連接並且在嵌入設備對象窗口實時刷新。

界面

安裝 AirtestIDE 後,打開 AirtestIDE ,打開模擬器中需要測試的App。
AirtestIDE 的設備窗口默認在可是界面的最右邊。
在這裏插入圖片描述
在 AirtestIDE 中,界面元素可以拖拽,佈置成你所喜愛的界面風格。假設一些窗口無意中關閉,可在窗口下拉選項中打開窗口。
在這裏插入圖片描述

連接

連接設備只需要在移動設備的窗口下列表點擊出現的設備信息中的connect,即可連接。
在這裏插入圖片描述
假設設備列表並未出現設備,點擊刷新;
在這裏插入圖片描述
如果是使用真機設備請使用USB線連接手機,並且允許USB調試,之後刷新ADB。
遠程連接需要只是IP及端口號,填入字段點擊連接即可。
更多鏈接本文不再贅述。可查看官方文檔

我當前使用的設備爲模擬器設備,模擬器連接過程直接在出現的設備列中點擊connect即可:
在這裏插入圖片描述

嘗試

在 Airtest 開發中是以“.air”作爲文件後綴。
連接設備後,查看代碼:

# -*- encoding=utf8 -*-
__author__ = "Administrator"

from airtest.core.api import *

其中 from airtest.core.api import * 將Airtest的基本API引入,爲之後編寫做好準備。
查看可視窗口最左側,有Airtest輔助窗與Poco輔助窗,本篇主要講解Airtest。
在這裏插入圖片描述
首先嚐試第一個操作touch,touch中文譯爲“觸摸”,從命名上得知,這是個可實現“觸碰”功能的操作。首先鼠標懸浮在 touch 選項處:
在這裏插入圖片描述
將會提示 touch 功能的相關信息,現在簡單的嘗試一下 touch 功能。
點擊 touch ,把鼠標移動到設備窗,找到你想要實現點擊的按鈕,點下左鍵不放,進行拖拽選中,隨後放手。
在這裏插入圖片描述
這時,代碼編輯區將會出現 touch('你所選中的圖片'),點擊運行腳本,嘗試使用:
在這裏插入圖片描述
運行效果如下:
在這裏插入圖片描述
從效果中可以看到 touch 將會找到與我們所選中的圖形相似的圖案,進行計算匹配,達到匹配的要求後,進行點擊操作。

淺剖析

現在查看一下 touch 函數的實現,從中得到更多的信息,幫助我們進行腳本的開發;點擊文件名,然後選擇“打開當前文件項目目錄”:
在這裏插入圖片描述
找到當前文件目錄後,找到與文件名相同的 .air 文件,使用編輯器進行打開。
以下爲編輯器打開該該文件後的代碼:

# -*- encoding=utf8 -*-
__author__ = "Administrator"

from airtest.core.api import *

touch(Template(r"tpl1587733818550.png", record_pos=(-0.217, 0.565), resolution=(540, 960)))

在不經過 AirtestIDE 處理的代碼中,圖片的表現形式爲路徑,以及使用了 Template 作爲處理,此處,Template 函數接收3個函數,分別爲:圖片路徑\record_pos以及resolution。

Airtest api 文檔中查詢 Template 方法。

在這裏插入圖片描述
查看文檔的值,剛剛使用的Template將會直接使用參數初始化一個類。

其中參數查看文檔得知:

  • filename:文件路徑
  • threshold:圖像識別閾值,是用來判定一張圖片識別是否成功的閾值,例如一張圖片識別到的匹配度是0.65,而我們設置的threshold爲0.7的話,Airtest會認爲匹配失敗,從而進行下一次匹配。
  • target_pos:圖像點擊位置,當識別出一張圖像後,Airtest將會默認去點擊圖像的正中心位置,有時我們希望它識別出圖片後點擊其他位置,可以通過修改target_pos屬性來實現。
  • rgb:切換彩色與灰度識別,在識別圖像時,Airtest會先將圖像轉爲灰度圖再進行識別。因此假如有兩個按鈕,形狀內容相同,只有顏色不同的情況下,Airtest將認爲它們都是相同內容。
    通過勾選rgb選項,或在代碼中加入rgb=True,可以強制指定使用彩色圖像進行識別。
    其中參數,還差 record_pos 與 resolution;以下爲Template類,查看文檔得知:
  • resolution:錄製時的屏幕分辨率
  • record_pos:錄製時屏幕上的座標
class Template(object):
    """
    picture as touch/swipe/wait/exists target and extra info for cv match
    filename: pic filename
    target_pos: ret which pos in the pic
    record_pos: pos in screen when recording
    resolution: screen resolution when recording
    rgb: 識別結果是否使用rgb三通道進行校驗.
    """

    def __init__(self, filename, threshold=None, target_pos=TargetPos.MID, record_pos=None, resolution=(), rgb=False):
        self.filename = filename
        self._filepath = None
        self.threshold = threshold or ST.THRESHOLD
        self.target_pos = target_pos
        self.record_pos = record_pos
        self.resolution = resolution
        self.rgb = rgb

    @property
    def filepath(self):
        if self._filepath:
            return self._filepath
        for dirname in G.BASEDIR:
            filepath = os.path.join(dirname, self.filename)
            if os.path.isfile(filepath):
                self._filepath = filepath
                return self._filepath
        return self.filename

    def __repr__(self):
        filepath = self.filepath if PY3 else self.filepath.encode(sys.getfilesystemencoding())
        return "Template(%s)" % filepath

		def match_in(self, screen):
        match_result = self._cv_match(screen)
        G.LOGGING.debug("match result: %s", match_result)
        if not match_result:
            return None
        focus_pos = TargetPos().getXY(match_result, self.target_pos)
        return focus_pos


	 	def match_all_in(self, screen):
        image = self._imread()
        image = self._resize_image(image, screen, ST.RESIZE_METHOD)
        return self._find_all_template(image, screen)


    @logwrap
    def _cv_match(self, screen):
        # in case image file not exist in current directory:
        image = self._imread()
        image = self._resize_image(image, screen, ST.RESIZE_METHOD)
        ret = None
        for method in ST.CVSTRATEGY:
            # get function definition and execute:
            func = MATCHING_METHODS.get(method, None)
            if func is None:
                raise InvalidMatchingMethodError("Undefined method in CVSTRATEGY: '%s', try 'kaze'/'brisk'/'akaze'/'orb'/'surf'/'sift'/'brief' instead." % method)
            else:
                ret = self._try_match(func, image, screen, threshold=self.threshold, rgb=self.rgb)
            if ret:
                break
        return ret

    @staticmethod
    def _try_match(func, *args, **kwargs):
        G.LOGGING.debug("try match with %s" % func.__name__)
        try:
            ret = func(*args, **kwargs).find_best_result()
        except aircv.NoModuleError as err:
            G.LOGGING.debug("'surf'/'sift'/'brief' is in opencv-contrib module. You can use 'tpl'/'kaze'/'brisk'/'akaze'/'orb' in CVSTRATEGY, or reinstall opencv with the contrib module.")
            return None
        except aircv.BaseError as err:
            G.LOGGING.debug(repr(err))
            return None
        else:
            return ret

    def _imread(self):
        return aircv.imread(self.filepath)

    def _find_all_template(self, image, screen):
        return TemplateMatching(image, screen, threshold=self.threshold, rgb=self.rgb).find_all_results()

    def _find_keypoint_result_in_predict_area(self, func, image, screen):
        if not self.record_pos:
            return None
        # calc predict area in screen
        image_wh, screen_resolution = aircv.get_resolution(image), aircv.get_resolution(screen)
        xmin, ymin, xmax, ymax = Predictor.get_predict_area(self.record_pos, image_wh, self.resolution, screen_resolution)
        # crop predict image from screen
        predict_area = aircv.crop_image(screen, (xmin, ymin, xmax, ymax))
        if not predict_area.any():
            return None
        # keypoint matching in predicted area:
        ret_in_area = func(image, predict_area, threshold=self.threshold, rgb=self.rgb)
        # calc cv ret if found
        if not ret_in_area:
            return None
        ret = deepcopy(ret_in_area)
        if "rectangle" in ret:
            for idx, item in enumerate(ret["rectangle"]):
                ret["rectangle"][idx] = (item[0] + xmin, item[1] + ymin)
        ret["result"] = (ret_in_area["result"][0] + xmin, ret_in_area["result"][1] + ymin)
        return ret

    def _resize_image(self, image, screen, resize_method):
        """模板匹配中,將輸入的截圖適配成 等待模板匹配的截圖."""
        # 未記錄錄製分辨率,跳過
        if not self.resolution:
            return image
        screen_resolution = aircv.get_resolution(screen)
        # 如果分辨率一致,則不需要進行im_search的適配:
        if tuple(self.resolution) == tuple(screen_resolution) or resize_method is None:
            return image
        if isinstance(resize_method, types.MethodType):
            resize_method = resize_method.__func__
        # 分辨率不一致則進行適配,默認使用cocos_min_strategy:
        h, w = image.shape[:2]
        w_re, h_re = resize_method(w, h, self.resolution, screen_resolution)
        # 確保w_re和h_re > 0, 至少有1個像素:
        w_re, h_re = max(1, w_re), max(1, h_re)
        # 調試代碼: 輸出調試信息.
        G.LOGGING.debug("resize: (%s, %s)->(%s, %s), resolution: %s=>%s" % (
                        w, h, w_re, h_re, self.resolution, screen_resolution))
        # 進行圖片縮放:
        image = cv2.resize(image, (w_re, h_re))
        return image

通過以上分析的值,在代碼:

touch(Template(r"tpl1587733818550.png", record_pos=(-0.217, 0.565), resolution=(540, 960)))

其中 resolution 爲當前設備的分辨率爲:540, 960;可是這和我設置的分辨率不一樣,查看文檔得知:“在使用不同分辨率的設備進行圖像識別時,可能會導致識別成功率不佳,因此Airtest提供了默認的分辨率適配規則”。從中也得到了些許信息,如“使用縮放後是否不精確?”,當然,文檔也給出瞭解決方案:“想要提高2d遊戲的識別精度,最好的辦法就是明確指定你的遊戲的分辨率適配規則;下面的代碼指定了一個自定義的縮放規則:直接return原來的值,不管屏幕分辨率,所有UI都不進行縮放。”,
代碼如下:

from airtest.core.api import *

def custom_resize_method(w, h, sch_resolution, src_resolution):
    return int(w), int(h)
# 替換默認的RESIZE_METHOD
ST.RESIZE_METHOD = custom_resize_method

這裏的RESIZE_METHOD,即我們定義的custom_resize_method使用的輸入參數爲:

  • w, h # 錄製下來的UI圖片的寬高
  • sch_resolution # 錄製時的屏幕分辨率
  • src_resolution # 回放時的屏幕分辨率

以上分析得知,通過Template示例後一個對象,作爲參數傳給touch方法,那麼touch方法應該進行剩下的圖片查找及觸摸操作;繼續分析touch方法。

以下在文檔中找到touch方法:
在這裏插入圖片描述
文檔中說明,touch方法爲在設備屏幕上執行觸摸操作。參數有:

  • 一個目標,這個目標可以是 Template 的實例或者是一個座標;
  • 執行多少次點擊
  • 按照平臺的不同所需的不同參數
  • 最終返回位點擊的座標
  • 適用平臺爲 Android, 、Windows 、iOS

點擊源代碼查看實現:

@logwrap
def touch(v, times=1, **kwargs):
    """
    Perform the touch action on the device screen

    :param v: target to touch, either a Template instance or absolute coordinates (x, y)
    :param times: how many touches to be performed
    :param kwargs: platform specific `kwargs`, please refer to corresponding docs
    :return: finial position to be clicked
    :platforms: Android, Windows, iOS
    """
    if isinstance(v, Template):
        pos = loop_find(v, timeout=ST.FIND_TIMEOUT)
    else:
        try_log_screen()
        pos = v
    for _ in range(times):
        G.DEVICE.touch(pos, **kwargs)
        time.sleep(0.05)
    delay_after_operation()
    return pos

經過之前的分析,得知 touch 將會執行查找圖片和點擊的操作;從實現中得知:
傳入參數後,首先判斷傳入的對象 v 是否屬於 Template對象,是這個對象,執行 loop_find方法,傳入對象,設置超時爲 ST.FIND_TIMEOUT,然後把查找得到的座標給予 pos 變量。

之後使用循環實現點擊,循環1次點擊1次,循環2次點擊2次,以此類推,調用G.DEVICE.touch 方法,傳入 pos 及 kwargs 進行點擊。
查看 G 類具體實現,G在helper中,查看helper:

import time
import sys
import os
import six
import traceback
from airtest.core.settings import Settings as ST
from airtest.utils.logwraper import Logwrap, AirtestLogger
from airtest.utils.logger import get_logger


class G(object):
    """Represent the globals variables"""
    BASEDIR = []
    LOGGER = AirtestLogger(None)
    LOGGING = get_logger("airtest.core.api")
    SCREEN = None
    DEVICE = None
    DEVICE_LIST = []
    RECENT_CAPTURE = None
    RECENT_CAPTURE_PATH = None
    CUSTOM_DEVICES = {}

	 @classmethod
    def add_device(cls, dev):
        """
        Add device instance in G and set as current device.

        Examples:
            G.add_device(Android())

        Args:
            dev: device to init

        Returns:
            None

        """
        cls.DEVICE = dev
        cls.DEVICE_LIST.append(dev)


	@classmethod
    def register_custom_device(cls, device_cls):
        cls.CUSTOM_DEVICES[device_cls.__name__.lower()] = device_cls



"""
helper functions
"""


def set_logdir(dirpath):
    """set log dir for logfile and screenshots.

    Args:
        dirpath: directory to save logfile and screenshots

    Returns:

    """
    if not os.path.exists(dirpath):
        os.mkdir(dirpath)
    ST.LOG_DIR = dirpath
    G.LOGGER.set_logfile(os.path.join(ST.LOG_DIR, ST.LOG_FILE))



def log(arg, trace=""):
    """
    Insert user log, will be displayed in Html report.

    :param data: log message or Exception
    :param trace: log traceback if exists, use traceback.format_exc to get best format
    :return: None
    """
    if G.LOGGER:
        if isinstance(arg, Exception):
            G.LOGGER.log("info", {
                    "name": arg.__class__.__name__,
                    "traceback": ''.join(traceback.format_exception(type(arg), arg, arg.__traceback__))
                })
        elif isinstance(arg, six.string_types):
            G.LOGGER.log("info", {"name": arg, "traceback": trace}, 0)
        else:
            raise TypeError("arg must be Exception or string")



def logwrap(f):
    return Logwrap(f, G.LOGGER)



def device_platform(device=None):
    if not device:
        device = G.DEVICE
    return device.__class__.__name__



def using(path):
    if not os.path.isabs(path):
        abspath = os.path.join(ST.PROJECT_ROOT, path)
        if os.path.exists(abspath):
            path = abspath
    G.LOGGING.debug("using path: %s", path)
    if path not in sys.path:
        sys.path.append(path)
    G.BASEDIR.append(path)



def import_device_cls(platform):
    """lazy import device class"""
    platform = platform.lower()
    if platform in G.CUSTOM_DEVICES:
        cls = G.CUSTOM_DEVICES[platform]
    elif platform == "android":
        from airtest.core.android.android import Android as cls
    elif platform == "windows":
        from airtest.core.win.win import Windows as cls
    elif platform == "ios":
        from airtest.core.ios.ios import IOS as cls
    elif platform == "linux":
        from airtest.core.linux.linux import Linux as cls
    else:
        raise RuntimeError("Unknown platform: %s" % platform)
    return cls



def delay_after_operation():
    time.sleep(ST.OPDELAY)

其實在這裏,已經註冊過了設備,默認的編輯窗口已經隱藏了這個過程,我們點擊新建文件可以看到 auto_steup(),該方法實現在 airtest.core.api 中,其中auto_steup()方法定義如下:

def auto_setup(basedir=None, devices=None, logdir=None, project_root=None):
    """
    Auto setup running env and try connect android device if not device connected.
    """
    if devices:
        for dev in devices:
            connect_device(dev)
    elif not G.DEVICE_LIST:
        try:
            connect_device("Android:///")
        except IndexError:
            pass
    if basedir:
        if os.path.isfile(basedir):
            basedir = os.path.dirname(basedir)
        if basedir not in G.BASEDIR:
            G.BASEDIR.append(basedir)
    if logdir:
        set_logdir(logdir)
    if project_root:
        ST.PROJECT_ROOT = project_root

def connect_device(uri):
    """
    Initialize device with uri, and set as current device.

    :param uri: an URI where to connect to device, e.g. `android://adbhost:adbport/serialno?param=value&param2=value2`
    :return: device instance
    :Example:
        * ``android:///`` # local adb device using default params
        * ``android://adbhost:adbport/1234566?cap_method=javacap&touch_method=adb``  # remote device using custom params
        * ``windows:///`` # local Windows application
        * ``ios:///`` # iOS device
    """
    d = urlparse(uri)
    platform = d.scheme
    host = d.netloc
    uuid = d.path.lstrip("/")
    params = dict(parse_qsl(d.query))
    if host:
        params["host"] = host.split(":")
    dev = init_device(platform, uuid, **params)
    return dev

def init_device(platform="Android", uuid=None, **kwargs):
    """
    Initialize device if not yet, and set as current device.

    :param platform: Android, IOS or Windows
    :param uuid: uuid for target device, e.g. serialno for Android, handle for Windows, uuid for iOS
    :param kwargs: Optional platform specific keyword args, e.g. `cap_method=JAVACAP` for Android
    :return: device instance
    """
    cls = import_device_cls(platform)
    dev = cls(uuid, **kwargs)
    for index, instance in enumerate(G.DEVICE_LIST):
        if dev.uuid == instance.uuid:
            G.LOGGING.warn("Device:%s updated %s -> %s" % (dev.uuid, instance, dev))
            G.DEVICE_LIST[index] = dev
            break
    else:
        G.add_device(dev)
    return dev

其中所需的 import_device_cls 方法在 airtest.core.helper中:

def import_device_cls(platform):
    """lazy import device class"""
    platform = platform.lower()
    if platform in G.CUSTOM_DEVICES:
        cls = G.CUSTOM_DEVICES[platform]
    elif platform == "android":
        from airtest.core.android.android import Android as cls
    elif platform == "windows":
        from airtest.core.win.win import Windows as cls
    elif platform == "ios":
        from airtest.core.ios.ios import IOS as cls
    elif platform == "linux":
        from airtest.core.linux.linux import Linux as cls
    else:
        raise RuntimeError("Unknown platform: %s" % platform)
    return cls

很清楚的看到,在 auto_setup 中有層級的調用了connect_device進行設備連接初始化,在connect_device中調用import_device_cls添加設備,隨後使新設備在G類中賦值給G.DEVICE,最後傳給G.DEVICE_LIST。

在這裏出現了 DEVICE_LIST 給對多設備操作的方式有了可能性,當然 Airtest Project 本就是這麼一個解決方案。在文檔中就有多機協作的介紹。以下文字引於文檔。

在我們的腳本中,支持通過 set_current
接口來切換當前連接的手機,因此我們一個腳本中,是能夠調用多臺手機,編寫出一些複雜的多機交互腳本的。

在命令行運行腳本時,只需要將手機依次使用 --device Android:/// 添加到命令行中即可,例如:

>airtest run untitled.air --device Android:///serialno1 --device Android:///serialno2 --device Android:///serialno1

當然多設備並行的方案現在也有很多之後補充。

最終,調用 airtest.core.android.android 中 touch 完成點擊:
在這裏插入圖片描述
實現如下:

def touch(self, pos, duration=0.01):
        """
        Perform touch event on the device

        Args:
            pos: coordinates (x, y)
            duration: how long to touch the screen

        Returns:
            None

        """
        if self.touch_method == TOUCH_METHOD.MINITOUCH:
            pos = self._touch_point_by_orientation(pos)
            self.minitouch.touch(pos, duration=duration)
        elif self.touch_method == TOUCH_METHOD.MAXTOUCH:
            pos = self._touch_point_by_orientation(pos)
            self.maxtouch.touch(pos, duration=duration)
        else:
            self.adb.touch(pos)

以上就是簡單的一個 touch 完成的所實現的過程。

腳本再嘗試

我們現在就來嘗試開啓顏色識別以及閥值設置:
增加 if 判斷,判斷是否存在圖片,存在則點擊,並且提高閥值以及開啓顏色識別:

雙擊圖片進行更改值:
在這裏插入圖片描述

去代碼查看是否改動

# -*- encoding=utf8 -*-
__author__ = "Administrator"

from airtest.core.api import *

if exists(Template(r"tpl1587750838857.png", threshold=0.8, rgb=True, record_pos=(-0.213, 0.57), resolution=(540, 960))):
    touch(Template(r"tpl1587750838857.png", threshold=0.8, rgb=True, record_pos=(-0.213, 0.57), resolution=(540, 960)))

最後優化一下,根據流程編寫了如下腳本:
在這裏插入圖片描述
其中程序代碼爲:

# -*- encoding=utf8 -*-
__author__ = "Administrator"

from airtest.core.api import *

if exists(Template(r"tpl1587750838857.png", threshold=0.8, rgb=True, record_pos=(-0.213, 0.57), resolution=(540, 960))):
    if touch(Template(r"tpl1587750838857.png", threshold=0.8, rgb=True, record_pos=(-0.213, 0.57), resolution=(540, 960))):
        if touch(Template(r"tpl1587751404697.png", record_pos=(0.004, 0.18), resolution=(540, 960))):
            if touch(Template(r"tpl1587751451726.png", rgb=True, record_pos=(0.174, 0.204), resolution=(540, 960))):
                if touch(Template(r"tpl1587751472685.png", threshold=0.8, rgb=True, record_pos=(0.222, 0.794), resolution=(540, 960))):
                    sleep(1)

運行結果如下:
在這裏插入圖片描述
以上腳本使用了 exists 斷言,判斷圖片是否存在,存在返回 pos 座標點,不存在返回False:
在這裏插入圖片描述
使用 exist 判斷可以當做爲腳本邏輯的一個分支,存在,則執行之後的操作,不存在。在使用 exist 時使用if,在同級下,多個if可以有效的讓所有情況出現不交叉的分支,使腳本代碼結構清晰是個不錯的選擇!

以上腳本還存在一個小尾巴,那就是在結尾處點擊訓練後,自動返回主目錄。修改如下:
在這裏插入圖片描述

爲了時腳本保持健壯性,我在點擊訓練意外情況找不到時,用了else語句,使其返回。
爲了更好的深入理解腳本,我們查看一下 exists 的實現;exists 的實現在 airtest.core.api 中:

@logwrap
def exists(v):
    """
    Check whether given target exists on device screen

    :param v: target to be checked
    :return: False if target is not found, otherwise returns the coordinates of the target
    :platforms: Android, Windows, iOS
    """
    try:
        pos = loop_find(v, timeout=ST.FIND_TIMEOUT_TMP)
    except TargetNotFoundError:
        return False
    else:
        return pos

exists 將會在屏幕中查找目標,如果找到將會返回座標值。
在這裏我們已經是第二次看見 loop_find 方法,此方法是 Airtest 的核心方法。loop_find 方法實現於 airtest.core.cv :

@logwrap
def loop_find(query, timeout=ST.FIND_TIMEOUT, threshold=None, interval=0.5, intervalfunc=None):
    """
    Search for image template in the screen until timeout

    Args:
        query: image template to be found in screenshot
        timeout: time interval how long to look for the image template
        threshold: default is None
        interval: sleep interval before next attempt to find the image template
        intervalfunc: function that is executed after unsuccessful attempt to find the image template

    Raises:
        TargetNotFoundError: when image template is not found in screenshot

    Returns:
        TargetNotFoundError if image template not found, otherwise returns the position where the image template has
        been found in screenshot

    """
    G.LOGGING.info("Try finding:\n%s", query)
    start_time = time.time()
    while True:
        screen = G.DEVICE.snapshot(filename=None, quality=ST.SNAPSHOT_QUALITY)

        if screen is None:
            G.LOGGING.warning("Screen is None, may be locked")
        else:
            if threshold:
                query.threshold = threshold
            match_pos = query.match_in(screen)
            if match_pos:
                try_log_screen(screen)
                return match_pos

        if intervalfunc is not None:
            intervalfunc()

        # 超時則raise,未超時則進行下次循環:
        if (time.time() - start_time) > timeout:
            try_log_screen(screen)
            raise TargetNotFoundError('Picture %s not found in screen' % query)
        else:
            time.sleep(interval)

文檔 對於 loop_find 有些接口介紹:
在這裏插入圖片描述
query:截圖對象
timeout:超時
threshold:閥值,也就是對比後的相似度的值,越大越難匹配,要求精度越高
interval:匹配相距時間
intervalfunc:失敗後的響應

在執行 loop_find 時首先給個計時器計時,獲取屏幕後驗證屏幕是否爲None,爲None可能沒連接上;屏幕獲取無異常則,使用截圖對象調用 match_in 方法,成功進行匹配返回座標值,否則返回False。

其中主要方法爲 match_in ,match_in 也在 airtest.core.cv : 中,定義如下:

def match_in(self, screen):
        match_result = self._cv_match(screen)
        G.LOGGING.debug("match result: %s", match_result)
        if not match_result:
            return None
        focus_pos = TargetPos().getXY(match_result, self.target_pos)
        return focus_pos

_cv_match 如下:

 @logwrap
    def _cv_match(self, screen):
        # in case image file not exist in current directory:
        image = self._imread()
        image = self._resize_image(image, screen, ST.RESIZE_METHOD)
        ret = None
        for method in ST.CVSTRATEGY:
            # get function definition and execute:
            func = MATCHING_METHODS.get(method, None)
            if func is None:
                raise InvalidMatchingMethodError("Undefined method in CVSTRATEGY: '%s', try 'kaze'/'brisk'/'akaze'/'orb'/'surf'/'sift'/'brief' instead." % method)
            else:
                ret = self._try_match(func, image, screen, threshold=self.threshold, rgb=self.rgb)
            if ret:
                break
        return ret

其中在 match_in 中調用了 _cv_match,_cv_match 首先對圖片 _imread() 進行CV2 處理,然後後面對圖片進行壓縮,應該是通過文檔中所說的通過COSCOS的規則。然後根據一個策略遍歷裏面的算法進行計算,最後得到 ret 返回計算結果。(在深度就不會了,畢竟我不是搞測試的,點到爲止)

Airtest 的核心淺顯流程搞清楚了,我們得知,在進行 touch 及 exists 時都會進行 loop_find 合理的調整查詢閥值和RGB開啓可以有效的節省匹配時間。優化腳本,合理的把部分圖片的RGB關閉以及部分圖片閥值減小以提升腳本運行效率。
修改後腳本如下:(修改了2個按鈕,降低了閥值及關閉了RGB)

# -*- encoding=utf8 -*-
__author__ = "Administrator"
from airtest.core.api import *
if exists(Template(r"tpl1587750838857.png", threshold=0.8, rgb=True, record_pos=(-0.213, 0.57), resolution=(540, 960))):
    if touch(Template(r"tpl1587750838857.png", threshold=0.8, rgb=True, record_pos=(-0.213, 0.57), resolution=(540, 960))):
        if touch(Template(r"tpl1587751404697.png", record_pos=(0.004, 0.18), resolution=(540, 960))):
            if touch(Template(r"tpl1587751451726.png", threshold=0.75, rgb=True, record_pos=(0.174, 0.204), resolution=(540, 960))):
                if touch(Template(r"tpl1587751472685.png", threshold=0.7, rgb=False, record_pos=(0.222, 0.794), resolution=(540, 960))):
                    touch(Template(r"tpl1587752119032.png", threshold=0.7, rgb=False, record_pos=(-0.441, -0.776), resolution=(540, 960)))
                else:
                     touch(Template(r"tpl1587752119032.png", threshold=0.7, rgb=False, record_pos=(-0.441, -0.776), resolution=(540, 960)))

運行結果(沒有加倍數,確實快了很多):
在這裏插入圖片描述
使用循環讓程序一直掛機吧!
修改程序如下:

在這裏插入圖片描述
結果發現在程序正常運行後,邏輯出現錯誤,運行結果如下:
在這裏插入圖片描述
這時需要修改程序,把頭盔的判斷增加else分支,改爲:
在這裏插入圖片描述
結果再次運行發現訓練過後,第二次訓練時間變成等待時間:
在這裏插入圖片描述
改爲先判斷後點擊,頭盔也是:
在這裏插入圖片描述

這次就很完美了:
在這裏插入圖片描述
那我再按照遊戲左下角提示操作去完成另外的邏輯,一共2個分支在運行判斷:
在這裏插入圖片描述
運行如下:在這裏插入圖片描述
可能某些情況需要拖拽屏幕,這個使用需要使用:
在這裏插入圖片描述
使用 swipe 推薦對於座標系不熟的使用錄製腳本功能編寫,通過這個功能,可以快速的寫好腳本:
在這裏插入圖片描述
點擊後,進入錄製腳本狀態,這個時候直接在屏幕上進行拖拽即可,記得幅度不要過大,不然在運行時導致滑動過多。

斷開連接點擊設備窗右上角。
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章