提前声明,我的 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 看点击的步骤。有下面几步
创建 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();
这个方法也可以。
目前先粗暴一点,直接注释掉。
// 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 才行。
这里先看一下 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