想要了解一个开源的项目,最好的方式就是从官方的说明文档开始入手。 我们先看下uiautomator2的由来。
uiautomator2 是一个Android UI自动化框架,支持Python编写测试脚本对设备进行自动化。底层基于Google uiautomator,Google提供的uiautomator库可以获取屏幕上任意一个APP的任意一个控件属性,并对其进行任意操作,但有两个缺点:1. 测试脚本只能使用Java语言 2. 测试脚本必须每次被上传到设备上运行。 我们希望测试能够用一个更脚本化的语言,例如Python编写,同时可以每次所见即所得地修改测试、运行测试。这里要非常感谢 Xiaocong He (@xiaocong),他将这个想法实现了出来(见xiaocong/uiautomator),原理是在手机上运行了一个http服务器,将uiautomator中的功能开放出来,然后再将这些http接口,封装成Python库。 我们的uiautomator2项目是对xiaocong/uiautomator的增强,主要有以下部分:
- 设备和开发机可以脱离数据线,通过WiFi互联(基于atx-agent)
- 集成了openstf/minicap达到实时屏幕投频,以及实时截图
- 集成了openstf/minitouch达到精确实时控制设备
- 修复了xiaocong/uiautomator经常性退出的问题
- 代码进行了重构和精简,方便维护
- Requirements: Android >= 4.4 Python >=2.7 || <= 3.7
从上面的描述我们可以看到部分的一些原理这个原理其实跟appium有点类似,会在手机上运行一个server端由它来去接收对应的消息以及解析处理。
那我们看下要如何去使用 首先当然就是
pip install --upgrade --pre uiautomator2
python -m uiautomator2 init
上边就是我们这次要讲的重点了。 python -m uiautomator2 init到底发生了什么内容呢?
我们先看下运行的结果
从上述的打印信息我们可以看出来, uiautomator2 init的整个流程为以下内容
graph LR
安装minicap-->安装minitouch
安装minitouch-->安装apk/apk-test
安装apk/apk-test-->安装atx-agent
安装atx-agent-->启动atx-agent
那我们还是具体看下代码的实现是如何的
def cmd_init(args):
if not args.serial:
for d in adbutils.devices():
serial = d.get_serial_no()
if d.get_state() != 'device':
logger.warning("Skip invalid device: %s %s", serial,
d.get_state())
continue
logger.info("Init device %s", serial)
_init_with_serial(serial, args.apk_version, args.agent_version,
args.server, args.reinstall)
else:
_init_with_serial(args.serial, args.apk_version, args.agent_version,
args.server, args.reinstall)
以上主要是根据命令行调用的时候是否带有serial来区别是单台设备的初始化还是说所有设备都进行初始化的操作, 下来我们看下_init_with_serial
的内容
def _init_with_serial(serial, apk_version, agent_version, server, reinstall):
log.info("Device(%s) initialing ...", serial)
# Installer实例化时获取对应设备的sdk,arch 用于下来的minitouch以及minicap的安装
ins = Installer(serial)
ins.server_addr = server
ins.install_minicap()
ins.install_minitouch()
ins.install_uiautomator_apk(apk_version, reinstall)
exedir = ins.get_executable_dir()
log.info("atx-agent is already running, force stop")
ins.shell(exedir + "/atx-agent", "-stop", raise_error=False)
ins.shell("killall", "atx-agent", raise_error=False)
ins.shell("rm", "/sdcard/atx-agent.pid", raise_error=False)
ins.shell("rm", "/sdcard/atx-agent.log.old", raise_error=False)
if not ins.check_agent_installed(agent_version):
ins.install_atx_agent(agent_version, reinstall)
ins.check_apk_installed(apk_version)
ins.launch_and_check()
以上代码的过程实际就是前面流程图中的内容。
我们分别看下minicap的安装以及apk的安装步骤
def install_minicap(self):
# 判断了arch以及sdk用语下载对应的软件版本推送到对应的目录下(/data/local/tmp)并修改为可执行的权限
if self.arch == 'x86':
log.info("skip install minicap on emulator")
return
sdk = self.sdk
if self.pre and self.pre != "0":
sdk = sdk + self.pre
base_url = GITHUB_BASEURL + \
"/stf-binaries/raw/master/node_modules/minicap-prebuilt/prebuilt/"
log.debug("install minicap.so")
url = base_url + self.abi + "/lib/android-" + sdk + "/minicap.so"
path = cache_download(url)
# 这里的获取可执行目录作者挺有意思的 #分别设置了两个目录(/data/local/tmp以及/data/data/com.android/shell
# )在对应的目录下创建一个文件叫permtest,进而修改起权限来确认对应的
#目录是否有可执行的权限
exedir = self.get_executable_dir()
minicapdst = "%s/%s" % (exedir, 'minicap.so')
self.push(path, minicapdst)
log.info("install minicap")
url = base_url + self.abi + "/bin/minicap"
path = cache_download(url)
self.push(path, exedir + "/minicap", 0o755)
def install_uiautomator_apk(self, apk_version, reinstall=False):
# 通过 adb shell dumpsys package xx 获取到包的信息,通过判断待安装的包与实际包的版本比较来
#确认是否需要卸载重新安装
pkg_info = self.package_info('com.github.uiautomator')
test_pkg_info = self.package_info('com.github.uiautomator.test')
# For test_pkg_info has no versionName or versionCode
# Just check if the com.github.uiautomator.test apk is installed
if not reinstall and pkg_info and pkg_info['version_name'] == apk_version and test_pkg_info:
log.info("apk(%s) already installed, skip", apk_version)
return
if pkg_info or test_pkg_info:
log.debug("uninstall old apks")
self.uninstall('com.github.uiautomator')
self.uninstall('com.github.uiautomator.test')
(path, pathtest) = self.download_uiautomator_apk(apk_version)
self.install(path)
log.debug("app-uiautomator.apk installed")
self.install(pathtest)
log.debug("app-uiautomator-test.apk installed")
def install_atx_agent(self, agent_version, reinstall=False):
# 通过执行atx-agent -v 获取到版本与待安装的版本进行比较,若不同或版本较低则会
# 获取设备的abi来下载对应的atx-agent,进行解压后推送的设备上,且修改文件的权限
exedir = self.get_executable_dir()
agentpath = '%s/%s' % (exedir, 'atx-agent')
version_output = self.shell(agentpath, '-v', raise_error=False).strip()
m = re.search(r"\d+\.\d+\.\d+", version_output)
current_agent_version = m.group(0) if m else None
if current_agent_version == agent_version:
log.info("atx-agent(%s) already installed, skip", agent_version)
return
if current_agent_version == 'dev' and not reinstall:
log.warn("atx-agent develop version, skip")
return
if current_agent_version:
log.info("atx-agent(%s) need to update", current_agent_version)
files = {
'armeabi-v7a': 'atx-agent_{v}_linux_armv7.tar.gz',
'arm64-v8a': 'atx-agent_{v}_linux_armv7.tar.gz',
'armeabi': 'atx-agent_{v}_linux_armv6.tar.gz',
'x86': 'atx-agent_{v}_linux_386.tar.gz',
}
log.info("atx-agent(%s) is installing, please be patient",
agent_version)
abis = self.shell('getprop',
'ro.product.cpu.abilist').strip() or self.abi
name = None
for abi in abis.split(','):
name = files.get(abi)
if name:
break
if not name:
raise Exception(
"arch(%s) need to be supported yet, please report an issue in github"
% abis)
url = GITHUB_BASEURL + '/atx-agent/releases/download/%s/%s' % (
agent_version, name.format(v=agent_version))
log.debug("download atx-agent(%s) from github releases", agent_version)
path = cache_download(url)
tar = tarfile.open(path, 'r:gz')
bin_path = os.path.join(os.path.dirname(path), 'atx-agent')
tar.extract('atx-agent', os.path.dirname(bin_path))
self.push(bin_path, agentpath, 0o755)
log.debug("atx-agent installed")
def launch_and_check(self):
log.info("launch atx-agent daemon")
# stop first
self.shell(self.atx_agent_path, "server", "--stop", raise_error=False)
# start server
# 这里判断是否有需要连接的服务器(这里应该是设备管理平台), 如果有则启动时增加-t的参数进行连接
args = [self.atx_agent_path, "server", '-d']
if self.server_addr:
args.append('-t')
args.append(self.server_addr)
output = self.shell(*args)
lport = self.forward_port(7912)
# 这里很有意思 通过 adb forward 后 所有本地对应端口的tcp数据都会被转发到手机设备的7912端口, 虽然没有看atx-agent,但是这里应该能够清楚 7912其实就是atx-agent监听的端口
logger.debug("forward remote(tcp:7912) -> local(tcp:%d)", lport)
time.sleep(.5)
cnt = 0
while cnt < 3:
try:
r = requests.get(
'http://localhost:%d/version' % lport, timeout=10)
r.raise_for_status()
log.info("atx-agent version: %s", r.text)
# todo finish the retry logic
print("atx-agent output:", output.strip())
# open uiautomator2 github URL
self.shell("am", "start", "-a", "android.intent.action.VIEW",
"-d", "https://github.com/openatx/uiautomator2")
log.info("success")
break
except (requests.exceptions.ConnectionError,
requests.exceptions.ReadTimeout,
requests.exceptions.HTTPError):
time.sleep(1.5)
cnt += 1
else:
log.error(
"Failure, unable to get result from http://localhost:%d/version",
lport)
以上就是uiautomator init的整个过程了, 其中涉及到了很多内容的安装。下来我们再一步步去看看每一个的作用到底都是什么