如果 TesterHome 观看不方便(目录不方便),可以去:https://wenjie.store/archives/uiautomator2 看;TesterHome 有些格式问题可能还没来得及改
PS:文中的对象 hash 如果出现上下文不一致的情况不要见怪,因为 usb 线有些不稳定,重新调试时对象 hash 就会发生变化
__main__.py
加上init
的启动参数即可,如下图所示:cmd_init
前,我们可以看下传进来的默认参数,如下图所示:--addr 127.0.0.1:7912
,这个并非在 python 层使用的,而是后续传给 atx-agent 作为启动参数使用cmd_init
函数了,可以看到一开始如果没有指定设备序列号的话,会自动遍历所有设备并初始化:install
函数,核心逻辑如下图所示:install
的逻辑其实有很多是重合的,下面只挑一些有差异的点来看
/data/local/tmp/
目录下,代码如下图所示:pm path
、dumpsys package
获取的,下面展示部分代码:-t
的参数,如下图所示:server
:表示启动 atx-agent 内置的 server--nouia
:带上此参数表示启动 atx-agent 时,不要把 uiautomator 也拉起来-d
:表示后台运行--addr
:指定监听的ip:port
ip:7912
请求就完了?实际上并没有,中间还进行了一次端口映射,python client 使用的其实是映射后的端口adb forward tcp:本地电脑随机端口 tcp:7912
,这个命令很好理解,比如{本地电脑随机端口}是 8080,那么你请求127.0.0.1:8080
就等于在请求手机ip:7912:
,代码实现如下:androidTest
下,并且还引入了 JUnit 框架,如下图所示:--nouia
参数,就表示启动 atx-agent 时也启动 uiautomator 服务,此时 golang 代码中fNoUiautomator
的值为 false,如下图所示:am instrument
启动单元测试的命令行,如下图所示:!*fNoUiautomator
为 True 时才会执行,如下图所示:am instrument
的神奇力量吗?并不是,实际上是因为 atx-agent 使用 goroutine 写了个死循环占有进程,要退出循环释放进程的话只能自己传入中断参数,最后还是使用 kill 命令杀掉进程的,这会在后面的【重置 uiautomator_v2 如何进行】处讲到。这部分比较偏向猜想,觉得不对欢迎补充
内网穿透
,你可以试着访问这个页面(随缘在线):https://wenjie.store/chat/,如果成功的话说明你可以间接使用我 4090 的算力了内网穿透
使得你可以通过一个公网的服务器访问到我本地的物理主机,比如上面的链接,你实际上能访问到的是我在自己电脑部署的 ChatGLM2(这东西总不能是一台 1c1g 的电脑能跑起来的)内网穿透
访问到电脑主机,那么手机是不是也可以?答案是肯定的以下操作看不懂就 SKIP 吧,你只需要知道能通过外网 adb 连接手机即可
adb forward
,具体命令如下图所示:localhost:8888/info
就访问到手机上运行的 atx-agent 服务了,如下图所示:adb forward
的好处已经体现出来了,假如你的设备是通过某种代理的手段(如内网穿透)开放出来的,那么 uiautomator 默认获取的网卡 IP 就只是内网 IP,如果你不在这内网之中而是通过代理手段访问的,那返回给你的内网的 IP 你是肯定无法访问的adb forward
的强大之处就在于它不会出现获取错 IP 这种情况,并且我上面的操作中,云端无论是 8888 端口还是 7912 端口的防火墙都是开着的(生效着的),这还意味着adb forward
本身能通过长连接绕过一些规则
PS:你可能会说我都知道
adb connect
的 ip 和 port 了,那我直接访问不就完了?如果你问出这个问题,那你可能还没完全理解上面的意思。在知道远程手机 ip:port 的情况下,如果直接使用ip:7912/info
访问,是必须要打开防火墙 7912 端口的,而我上面使用adb forward
根本就没打开。
uiautomator2 init
指令的流程就基本解释清楚了,uiautomator2 stop
就不多说了,有个意料之外的地方在于它没有停下 atx-agent。uiautomator2 purge
后,再执行如下代码走的click
逻辑:import uiautomator2 as u2
if __name__ == '__main__':
d = u2.connect_usb(serial="af80d1e4") # connect to device
d(text="首页").click(timeout=3)
关于 u2.connect_usb 就不过多讲解了,返回的 Devices 对象里面由多个父类接口组合而成,click 函数也是众多父类的实现之一
d(text="首页")
其实只做了一些包装对象的工作,但如果你在这之前运行过 UI 自动化,你会发现此时有些参数怪怪的,即便你之后执行了uiautomator2 purge
把东西都卸载干净了,接下来就一步步去看d
的初始化,实际上就是包装了一个 UIObject,而传进去的 Selector 其实也只是一层参数包装:session.address
属性时,你会发现已经存在 ip 端口了:上面的 session 不要在断点时展开所有属性,否则你会发现展开得很慢,因为有些属性是通过请求 atx-agent 获取的,而发现 atx-agent 进程不在时,就会自动拉起,正常的启动逻辑不是这样的。而只获取 address 属性不会有这个问题。
uiautomator2 purge
只是卸载 APP+ 可执行文件,并没有删除端口转发,我们可以使用adb forward --list
查看已存在的端口映射,会发现正好等于上面获取到的 port:uiautomator2 init
的流程中是先启动 atx-agent server 再进行端口映射的,但实际上先进行端口映射也没关系,因为 atx-agent server 的端口固定 7912,只要保证 jsonrpc 请求前映射到就行。uiautomator purge
清理),像下面这样这样:while true
do
adb shell rm -rf /data/local/tmp/atx-agent
sleep 0.01
done
must_wait
,这个函数默认就是在规定时间内看指定元素是否存在,代码如下:wait
函数,会发现里面其实是 jsonrpc 的调用:_AgentRequestSession#request
的实现,终于发现初始化 atx-agent 的代码了:_prepare_atx_agent
的执行逻辑我想应该不用多说太多,最终还是会执行到前面uiautomator2 init
提到的setup_atx_agent
函数,所以启动参数啥的都是一样的,调用栈如下图所示:/jsonrpc/0
的请求都转发到127.0.0.1:9008
,上面代码遮住了可能看不清,下面看下完整的:reset_uiautomator
的核心逻辑如下
_prepare_atx_agent
(前面说过这个函数):_force_reset_uiautomator_v2
开始重置 ui2 环境,这段逻辑比较长,下面单独拆分字标题说。_force_reset_uiautomator_v2
,头部逻辑如下:self.shell(...)
是怎么调用的?你是不是觉得是 python 直接在 pc 端运行的命令?如果你这么想恭喜你答错了,实际上self.shell(...)
是把命令给到 atx-agent 去执行的self.uiautomator.stop()
,我们看看这个stop
干了啥:pkeeper.stop()
看看,发现核心就是传了个True
到p.stopC
:pkeeper.start()
是怎么运作的,实际上它就是运行了一个死循环,当p.stopC
传入 True 时就会结束,然后释放进程;截取了部分关键代码如下图所示:self.uiautomator.start()
跟之前的stop
十分有九分相似,python 层依旧是 jsonrpc 请求,只是变成了 post 方法:am instrument
启动单元测试的方式,然后再加个保活锁:reset_uiautomator
函数也到此结束了,后面虽然还有一些兜底逻辑,但大部分都是已经见过的函数实现,所以不再赘述。_jsonrpc_retry_call
处,reset_uiautomator
成功后会重新发起一次请求:com.googlecode.jsonrpc4j.JsonRpcServer
实现了 jsonrpc 服务,并在AutomatorServiceImpl
中实现了具体实现,其中waitForExists
如下:androidx.test.uiautomator
包提供的能力,而uiautomator
提供的能力其实大部分来自AccessibilityService
:com.github.uiautomator.stub.AutomatorServiceImpl#click(int, int, long)
,按下和松开中间有个间隔的就是长按函数了:touchUp
,因为最终的返回值是它决定的,原理大同小异:injectEventSync
继续深入的话需要下载源码,这里就不再深入了,你只需要知道这里使用的是一个同步的注入方法,如果注入失败就会返回 fasle:injectEventSync
的返回值,在某款不知道什么游戏引擎构建的应用上使用自动化点击时,我脚本明明只点了一下,但 APP 上总是点两下。injectEventSync
都返回 false,而内部框架额外处理了这个injectEventSync
的返回值,如果返回 false 就额外点一下,气死个人。ldd
查看可执行文件会发现少了一些 linux 的 so,目测属于硬伤救不了dlv debug
就是GOARCH=arm64 GOOS=linux
即可GOOS=linux GOARCH=arm64 go build -ldflags "-extldflags -static" -ldflags= github.com/go-delve/delve/cmd/dlv
dlv
和./dlv
是有区别的):launch.json
文件如下:GOOS=linux GOARCH=arm64 go build -gcflags="all = -N -l"
http://手机ip:7912/info
试试看,debug 生效的话上面就会停在上面的断点:X/X
screencap --help
后,会发现有如下内容:-d
参数就是可以指定 display id 截图