如果 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 截图
