Appium WebDriverAgent tap 接口速度优化(分析篇)/session/:sessionId/wda/tap/:uuid

codeskyblue · 2021年04月27日 · 最后由 codeskyblue 回复于 2021年08月10日 · 6337 次阅读

前言

提前声明,我的 OC 是不是很好。发现错误欢迎留言指正。
重新看了一遍自己写的这个文章,还挺混乱的。想直接指导答案的,直接跳转到 最后的 最终方案 就好了

本文基于项目 https://github.com/appium/WebDriverAgent v3.8.0 版本的代码进行修改 Xcode 12.4 Facebook-wda 1.4.0

背景

最近有同学在用facebook-wda在做抖音和快手,微信 这类 app 自动化的时候,反馈说 click 速度太慢,或者是直接卡住了。
这里打算通过修改WebDriverAgent的源码来给 click 提个速

断点调试

从 facebook-wda 看点击的步骤。有下面几步

  1. 先通过/status 接口获取当前界面的 sessionId
  2. 如果没有就创建一个 session(直接填空参数)
  3. 给接口传递坐标

创建 session 速度优化

创建 session

当执行到下面这个请求的时候,发现有点卡。

Shell$ curl -X POST -d '{"capabilities": {}, "desiredCapabilities": {}}' 'http://localhost:8100/session'

Xcode 日志提示

2021-04-27 11:15:55.455125+0800 WebDriverAgentRunner-Runner[7088:1682117] Getting the most recent active application (out of 1 total items)
    t =   300.95s Find the Application 'com.apple.springboard'
    t =   360.96s     Requesting snapshot of accessibility hierarchy for app with pid 61

com.apple.springboard代表的其实是苹果的桌面。通过查找Getting the most recent active application这个日志对应的代码,找到代码执行的位置

具体创建 Session 的逻辑在 FBSessionCommands.m handleCreateSession 中

我们看该函数的最后一行

  if (requirements[DEFAULT_ALERT_ACTION]) {
    [FBSession initWithApplication:app
                defaultAlertAction:(id)requirements[DEFAULT_ALERT_ACTION]];
  } else {
    [FBSession initWithApplication:app];
  }

  return FBResponseWithObject(FBSessionCommands.sessionInformation);
}

这样代码中的FBResponseWithObject(FBSessionCommands.sessionInformation); FBSessionCommands.sessionInformation 看起来是个属性获取,实际上(nnd)竟然是一个函数调用。

看看 sessionInfomation 的实现吧

// File: FBSessionCommands.m
+ (NSDictionary *)sessionInformation
{
  return
  @{
    @"sessionId" : [FBSession activeSession].identifier ?: NSNull.null,
    @"capabilities" : FBSessionCommands.currentCapabilities
  };
}

+ (NSDictionary *)currentCapabilities
{
  FBApplication *application = [FBSession activeSession].activeApplication;
  return
  @{
    @"device": ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) ? @"ipad" : @"iphone",
    @"sdkVersion": [[UIDevice currentDevice] systemVersion],
    @"browserName": application.label ?: [NSNull null],
    @"CFBundleIdentifier": application.bundleID ?: [NSNull null],
  };
}

最后的这个 currentCapabilities 又调用了 [FBSession activeSession].activeApplication获取当前活跃的 application。

继续看 activeApplication 的实现,又跳转到了 fb_activeApplicationWithDefaultBundleId 的调用。
这这个函数中,终于找到了Getting the most recent active application这条日志的代码。

if (activeApplicationElements.count > 0) {
  [FBLogger logFmt:@"Getting the most recent active application (out of %@ total items)", @(activeApplicationElements.count)];
  for (XCAccessibilityElement *appElement in activeApplicationElements) {
    FBApplication *application = [FBApplication fb_applicationWithPID:appElement.processIdentifier];
    if (nil != application) {
      return application;
    }
  }
}

在这个函数的最终调用了 XCUIApplication.framework 中的函数 + (instancetype)applicationWithPID:(pid_t)processID; 断点走的时候这个看起来不会卡住。
具体的 framework 在如下路径/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework 除了头文件,大部分内容都是二进制,我就看不懂了。

卡点 1

最后发现断点卡在了这里 application.label 获取 application 名字,这个最终也是调用的 XCTest。
先直接注释掉试试。一下子创建 session 的速度一下子飞了起来,果然是这个问题。(其他地方应该也有调用 application.label 的地方,都需要干掉)。获取实现一个搞一个 FBApplication+Label.m的文件,定义一下 - NSString * fb_label(); 这个方法也可以。
目前先粗暴一点,直接注释掉。

sessionId 到 Session 的流程 优化

  • FBWebServer.m registerRouteHandlers 中的 mountRequest
// FBRoute.m
// 其中 FBRoute_TargetAction直接继承了FBRoute,所以可以调用FBRoute的方法

@implementation FBRoute

- (instancetype)respondWithTarget:(id)target action:(SEL)action
{
  FBRoute_TargetAction *route = [FBRoute_TargetAction withVerb:self.verb path:self.path requiresSession:self.requiresSession];
  route.target = target;
  route.action = action;
  return route;
}
// FBRoute.m 
// 函数withVerb的实现
@implementation FBRoute

+ (instancetype)withVerb:(NSString *)verb path:(NSString *)pathPattern requiresSession:(BOOL)requiresSession
{
  FBRoute *route = [self new];
  route.verb = verb;
  route.path = [FBRoute pathPatternWithSession:pathPattern requiresSession:requiresSession];
  route.requiresSession = requiresSession;
  return route;
}

在 HTTP 请求到来的时候,会请求 FBRoute_TargetAction 实例的的 mountRequest

@implementation FBRoute_TargetAction

- (void)mountRequest:(FBRouteRequest *)request intoResponse:(RouteResponse *)response
{
  [self decorateRequest:request];
  id<FBResponsePayload> (*requestMsgSend)(id, SEL, FBRouteRequest *) = ((id<FBResponsePayload>(*)(id, SEL, FBRouteRequest *))objc_msgSend);
  id<FBResponsePayload> payload = requestMsgSend(self.target, self.action, request);
  [payload dispatchWithResponse:response];
}

@end
// FBRoute.m
@implementation FBRoute

- (void)decorateRequest:(FBRouteRequest *)request
{
  if (!self.requiresSession) {
    return;
  }
  NSString *sessionID = request.parameters[@"sessionID"];
  if (!sessionID) {
    [self raiseNoSessionException];
    return;
  }
  FBSession *session = [FBSession sessionWithIdentifier:sessionID];
  if (!session) {
    [self raiseNoSessionException];
    return;
  }
  request.session = session;
}

这里倒是可以处理一下。当遇到 sessionId 为 0 的时候,直接范围 nil。这样就可以使用 POST /session/0/wda/tap/0 了。

直接在FBSession *session = [FBSession sessionWithIdentifier:sessionID];的上一行添加

if ([sessionID isEqualToString:@"0"]) { // no need session
  return;
}

又处理掉一个问题。

后续的实验结果,发现 sessionID 不存在的时候,点击不好使。估计 xctest 还有个全局变量什么的,需要先设置一下 application 才行。

tap 速度优化

这里先看一下 handleTap 的实现

// FBElementCommands.m
+ (id<FBResponsePayload>)handleTap:(FBRouteRequest *)request
{
  FBElementCache *elementCache = request.session.elementCache;
  CGPoint tapPoint = CGPointMake((CGFloat)[request.arguments[@"x"] doubleValue], (CGFloat)[request.arguments[@"y"] doubleValue]);
  if ([elementCache hasElementWithUUID:request.parameters[@"uuid"]]) { // 因为uuid为0,所有走不到这里
    XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
    NSError *error;
    if (![element fb_tapCoordinate:tapPoint error:&error]) {
      return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description
                                                                             traceback:nil]);
    }
  } else {
    XCUICoordinate *tapCoordinate = [self.class gestureCoordinateWithCoordinate:tapPoint
                                                                    application:request.session.activeApplication
                                               shouldApplyOrientationWorkaround:isSDKVersionLessThan(@"11.0")];
    [tapCoordinate tap];
  }
  return FBResponseWithOK();
}

断点调试会看到 gestureCoordinateWithCoordinate 这个函数会卡一下。

// FBElementCommands.m 末尾
+ (XCUICoordinate *)gestureCoordinateWithCoordinate:(CGPoint)coordinate application:(XCUIApplication *)application shouldApplyOrientationWorkaround:(BOOL)shouldApplyOrientationWorkaround
{
  CGPoint point = coordinate;
  if (shouldApplyOrientationWorkaround) {
    point = FBInvertPointForApplication(coordinate, application.frame.size, application.interfaceOrientation);
  }

  /**
   If SDK >= 11, the tap coordinate based on application is not correct when
   the application orientation is landscape and
   tapX > application portrait width or tapY > application portrait height.
   Pass the window element to the method [FBElementCommands gestureCoordinateWithCoordinate:element:]
   will resolve the problem.
   More details about the bug, please see the following issues:
   #705: https://github.com/facebook/WebDriverAgent/issues/705
   #798: https://github.com/facebook/WebDriverAgent/issues/798
   #856: https://github.com/facebook/WebDriverAgent/issues/856
   Notice: On iOS 10, if the application is not launched by wda, no elements will be found.
   See issue #732: https://github.com/facebook/WebDriverAgent/issues/732
   */
  XCUIElement *element = application;
  if (isSDKVersionGreaterThanOrEqualTo(@"11.0")) {
    XCUIElement *window = application.windows.fb_firstMatch;
    if (window) {
      element = window;
      XCElementSnapshot *snapshot = element.fb_cachedSnapshot ?: element.fb_takeSnapshot;
      point.x -= snapshot.frame.origin.x;
      point.y -= snapshot.frame.origin.y;
    }
  }
  return [self gestureCoordinateWithCoordinate:point element:element];
}

执行到这里的时候 XCUIElement *window = application.windows.fb_firstMatch;会有概率卡住。先暴力的改成 XCUIElement *window = nil 直接速度又上来了。
不过应该会有点隐患,具体先不管了。

继续到代码 [tapCoordinate tap]; 网上有说改成 XCEventGenerator.h 的函数的,不过这个从 Xcode 10.1 之后就没有这个私有 API 了。

执行代码 facebook-wda 代码 c.click(120, 200)是的 xcode 日志

2021-04-27 17:35:14.694119+0800 WebDriverAgentRunner-Runner[9012:1972216] Waiting up to 2s until com.ss.iphone.ugc.Aweme is in idle state (including animations)
    t =   220.21s     Wait for com.ss.iphone.ugc.Aweme to idle
    t =   220.21s     Find the Application 'com.ss.iphone.ugc.Aweme'
    t =   220.21s         Requesting snapshot of accessibility hierarchy for app with pid 9027
    t =   220.23s     Check for interrupting elements affecting "抖音" Application
    t =   220.23s         Requesting snapshot of accessibility hierarchy for app with pid 9027
    t =   220.24s         Find: Descendants matching predicate identifier == "NotificationShortLookView" OR elementType == 7
    t =   220.24s     Synthesize event
    t =   220.24s         Find the Application 'com.ss.iphone.ugc.Aweme'
    t =   220.24s             Requesting snapshot of accessibility hierarchy for app with pid 9027
2021-04-27 17:35:14.799413+0800 WebDriverAgentRunner-Runner[9012:1972216] Waiting up to 2s until com.ss.iphone.ugc.Aweme is in idle state (including animations)
    t =   220.31s     Wait for com.ss.iphone.ugc.Aweme to idle

看起来 tap 的时候还会,dump hierarchy。先试试修改一下 snapshotMaxDepth。通过 api 也可以更改。

c.appium_settings({"snapshotMaxDepth": 1})

可以点击了。就是不知道为什么。

看了下实现。

+ (void)setSnapshotMaxDepth:(int)maxDepth
{
  FBSnapshotRequestParameters[FBSnapshotMaxDepthKey] = @(maxDepth);
}

额,这个代码完全就是修改了一下本地变量嘛

全局查找一下。有个文件 FBXCAXClientProxy.m

// FBXCAXClientProxy.m
@implementation XCAXClient_iOS (WebDriverAgent)

/**
 Parameters for traversing elements tree from parents to children while requesting XCElementSnapshot.

 @return dictionary with parameters for element's snapshot request
 */
- (NSDictionary *)fb_getParametersForElementSnapshot
{
  return FBConfiguration.snapshotRequestParameters;
}

+ (void)load
// 实现有点看不懂,不贴了,猜测将什么函数给替换了,这样XCTest执行方法的时候,就会回调这个函数。

现在看起来重点就是将这个 snapshotMaxDepth 弄的少一点,然后 XCTest 内置的很多慢的方法都可以快起来了。

关于 load,这个网上查了一下,说是在创建实例执行执行的代码。又看不动了,先这样吧。

最终方案

import wda
c = wda.Client()
app = c.session("com.apple.Preferences") # 启动一个app
app.home() # 回到桌面

app.appium_settings({"snapshotMaxDepth": 0})
app.click(150 200)
app.appium_settings({"snapshotMaxDepth": 50}) # 恢复

或者在 OC 里面操作也行

[FBConfiguration setSnapshotMaxDepth:0]; // 路由开始前
// 代码
[FBConfiguration setSnapshotMaxDepth:50]; // 恢复maxDepth

参考资料

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 2 条回复 时间 点赞

https://github.com/nanoscopic/WebDriverAgent/commit/3ad6454200cfcce93b9eb72a11f01a1a5d42d28d 大佬有试过这个方法吗,感觉纯点坐标明显比原有 wda 的 handleTap 要快不少

Curtain 回复

这个方法今天刚看看到,不过还有很多不懂,你这个直接全部实现了 。👍

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册