Appium Android 上的 Hybrid 的一些知识可以看下@qddegtya的文章:
今天我们只说 Appium 中 iOS 下的 Hybrid。
众所周知, Appium 上的 iOS 自动化使用的是苹果自带的 instruments 的 UIAutomation。它可以操控继承自 UIKIT 的 native 的界面控件。那对于 webview(UIAWebView)里面的元素,会把一部分 html 元素隐射成 native 的元素,比如:textFields/links/buttons
。我们可以从 logElementTree
方法中看下:
var target = UIATarget.localTarget();
var app = target.frontMostApp();
var mWin = target.frontMostApp().mainWindow();
mWin.textFields()["Enter URL"].textFields()["Enter URL"].tap();
mWin.textFields()["Enter URL"].textFields()["Enter URL"].setValue("http://www.baidu.com");
mWin.buttons()["Go"].tap();
var webview = mWin.scrollViews()[0].webViews()[0];
webview.textFields()[0].tap();
target.delay(2);
UIATarget.localTarget().frontMostApp().keyboard().typeString("hello world!");
webview.buttons()["百度一下"].tap();
webview.logElementTree();
但是 html 的元素和场景都非常多,在复杂场景下 UIAutomation 就捉襟见肘了(当然暴力坐标系除外)。
那 Appium 是怎么做的呢?
在了解 Appium 怎么做之前,我们需要了解下 Webkit 远程调试协议。用 Chrome 的人应该都知道 Chrome DevTools。很多人都以为它是 Chrome 的一个组件。事实上开发者工具 (DevTools) 是一个独立的 Web 应用程序 (HTML+CSS+Javascript),被集成在浏览器中,通过远程调试协议 (remote debugging protocol) 和浏览器内核进行交互。
我们先来体验一把远程调试
open -a Google\ Chrome --args --remote-debugging-port=9999
http://localhost:9999/devtools/devtools.html?ws=localhost:9999/devtools/page/D85B7DF6-FF3D-490D-9770-B372C8F1F1C4
注意看地址栏,我们访问的是一个标准的 HTTP 协议下的网页,不是 chrome 的私有协议,其中 ws=localhost:9999/devtools/page/D85B7DF6-FF3D-490D-9770-B372C8F1F1C4
告诉你连接的 Websocket。
cmd + option + i
从整个调试过程中的 Websocket 通讯可以看出,这个接口里面有两种通讯模式:
request/response :就如同一个异步调用,通过请求的信息,获取相应的返回结果。这样的通讯必然有一个 message id,否则两方都无法正确的判断请求和返回的匹配状况。
request: {"id":1,"method":"Page.canScreencast"}
response: {"id":1,"result":{"result":false}}
notification :和第一种不同,这种模式用于由一方单方面的通知另一方某个信息。和 “事件” 的概念类似。
{"method":"Network.loadingFinished","params":{"requestId":"14307.143","timestamp":1424097364.31611,"encodedDataLength":0}}
域
远程调试协议把操作划分为不同的域 domain ,比如
可以理解为 DevTools 中的不同功能模块。每个域 (domain) 定义了它所支持的 command 和它所产生的 event(就是上面讲的两种通讯方式)。每个 command 包含 request 和 response 两部分,request 部分指定所要进行的操作以及操作说要的参数,response 部分表明操作状态,成功或失败。command 和 event 中可能涉及到非基本数据类型,在 domain 中被归为 Type,比如:'frameId': ,其中 FrameId 为非基本数据类型。
至此,不难理解: domain = command + event + type
远程调试协议结构
command 结构如下:
Page.navigate
request: {
"id": <number>,
"method": "Page.navigate",
"params": {
"url": <string>
}
}
response: {
"id": <number>,
"error": <object>
}
执行 Page.navigate 操作,需要参数 url,id 可以随意指定,不过要确认全局的唯一性,因为需要通过 id 关联 request 和 response。
event 结构如下:
Page.loadEventFired
{
"method": "Page.loadEventFired",
"params": {
"timestamp": <number>
}
}
Page domain 派发 loadEventFired 事件结构数据 (通过 WebSocket 的 onmessage 获取),并包含参数 timestamp
type 结构如下:
Frame: object
id ( string )
Frame unique identifier.
loaderId ( Network.LoaderId )
Identifier of the loader associated with this frame.
mimeType ( string )
Frame document's mimeType as determined by the browser.
name ( optional string )
Frame's name as specified in the tag.
parentId ( optional string )
Parent frame identifier.
securityOrigin ( string )
Frame document's security origin.
url ( string )
Frame document's URL.
Frame type 为包含 id,loaderId,mimeType,name,parentId,securityOrigin 和 url 字段的 Object 数据类型,其中 loaderId 为另外一个定义在 Network domain 中的 type
简单来说,远程调试协议就是利用 WebSocket 建立连接 DevTools 和浏览器内核的快速数据通道。那么我们也可以自己打开这个 websocket,遵从它的协议来发送消息。
回到之前我们打开的 http://localhost:9999
,返回的是一系列的 tab, 其实还可以返回 json 数据—— http://localhost:9999/json
。这是一个数组,每个数组元素都是一个页面的信息,数组按照最近刷新时间排序。
[
{
description: "",
devtoolsFrontendUrl: "/devtools/devtools.html?ws=localhost:9999/devtools/page/72F441DF-6CAB-4D6B-82FD-F264154C7FBD",
id: "72F441DF-6CAB-4D6B-82FD-F264154C7FBD",
thumbnailUrl: "/thumb/72F441DF-6CAB-4D6B-82FD-F264154C7FBD",
title: "Inspectable pages",
type: "other",
url: "http://localhost:9999/",
webSocketDebuggerUrl: "ws://localhost:9999/devtools/page/72F441DF-6CAB-4D6B-82FD-F264154C7FBD"
},
{
description: "",
devtoolsFrontendUrl: "/devtools/devtools.html?ws=localhost:9999/devtools/page/D9509075-5DF6-4DEA-B590-0025C2D957FC",
id: "D9509075-5DF6-4DEA-B590-0025C2D957FC",
title: "打开新的标签页",
type: "page",
url: "http://localhost:9999/json",
webSocketDebuggerUrl: "ws://localhost:9999/devtools/page/D9509075-5DF6-4DEA-B590-0025C2D957FC"
},
....
....
]
我们可以通过 json 数据得到 websocket 的 url,从而创建自己的通道,比如:
#!/usr/bin/env node
var request = require("request");
request("http://localhost:9999/json", function(e, res, data) {
data = JSON.parse(data);
var url = data[0].webSocketDebuggerUrl; // 得到首个tab的websocket的链接
if(!url) {
throw new Error("no url");
}
var commands = [
'{"id":1,"method":"Network.enable","params":{}}',
'{"id":2,"method":"Page.enable","params":{}}',
'{"id":3,"method":"Page.navigate","params":{"url":"http://www.taobao.com"}}' // 跳转淘宝
]
var WebSocket = require('ws');
console.log('open '+url);
var ws = new WebSocket(url)
console.log('opened '+url);
var count = 0
ws.on('open', function() {
console.log('connected');
if (count < commands.length) {
console.log('send '+commands[count])
ws.send(commands[count++])
}
});
ws.on('close', function() {
console.log('disconnected');
ws.close()
});
ws.on('message', function(data, flags) {
console.log('recv %s', data)
if (count < commands.length) {
console.log('send '+commands[count])
ws.send(commands[count++])
} else {
// if we sent Page.enable, we could listen for Page.loadEventFired to
// see if our nav succeeded.
ws.close()
}
});
});
该脚本通过远程调试协议,使浏览器跳转 taobao.com
,脚本亲测可用。
讲了那么多,终于到苹果了。Safari 6(只能 OS X)之后,你不但可以使用 Safari 开发者工具远程调试 iOS 设备上的移动 Safari 网页,而且可以远程调试任何 UIWebView 里的网页,比如 PhoneGap 应用。
Safari 的远程调试中使用到的调试协议,与 Google 开放的 Chrome Debug Protocol 有牵丝万缕的关系:
webinspectord 和 lockdown 协议
webinspectord is launched by Safari when the Developper menu is enabled.
webinspectord 是一个守护进程,负责远程调试的通讯。这个进程暴露了一个服务接口,供外部应用(例如桌面端的 Safari 调试工具)使用。
iOS 上所有的服务(文件浏览、消息推送、app 安装等)都是使用 lockdown 协议连接上的。调试工具透过 Usbmux,再通过 lockdown 协议,连接到 webinspector 服务。由于苹果的封闭,所以基本上没有资料可查,大家可以去 http://www.libimobiledevice.org 看看这些接口的开源实现,另外也可以去 https://theiphonewiki.com/wiki/Main_Page 看看。
Safari 远程调试服务概述
抛开 Usbmux 和 lockdown 协议不谈,Safari 远程调试服务所使用的协议本身其实就是 Webkit 调试协议的二次包装。也就是共享了 Webkit 调试协议的大部分功能。
先分析这个协议里面的主体:
还有一些概念字段:
大体可以看出,这个调试服务的接口是有状态的。设备和 Safari devtool 建立连接后,拥有可以复用的链接作为后续通讯的通道。假设我们需要发送一条调试指令到 iOS 上的某个 Webkit 内核,它的整个编码流程应该是是这样:
selector
就是 _rpc_forwardSocketData
所有的消息,都是以 Apple 自己定义的 RPC 消息提格式进行的。但它实际传输的有效数据,还是 Webkit 能够识别的指令。另外,Safari 没有如同 Chrome 那样,使用了 websocket 作为暴露出去的应用层协议。它选择了最基本的 socket 通讯方式和 bplist 作为传输格式。
调试消息过程
下面以一个具体的消息作为例子,来说说这整个过程。
JSON 消息和 Safari 的 RPC 协议
假设我们要开启网络监控这个面板,需要发送一个 JSON 指令。指令序列化之后我们得到:
{"id":0,"method":"Network.enable"}
就像之前说的,Safari 调试协议不直接使用 JSON 字符串作为传输的序列化方案。Safari 远程调试协议有自己的 RPC 规范,所有的消息都都有 __selector
和 __arguments
两个字段:前者说明调用的方法,后者说明调用时的参数。
常见的一些方法 (其实是 ObjC selector 的字符串表达) 如下:
另一方面,iOS 端也会传过来很多消息,同样遵循基本消息提的格式,常见的 __selector
有:
Safari 不选择 websocket 作为传输协议应该是从安全性、复杂性的角度去考虑。而选择 bplist 作为传输格式是由于 lockdown 协议的关系。
JSON 到 plist/bplist 的转换
plist 和 bplist 都是 Apple 的通讯格式。其中 plist 非常常见。加入你做过 iOS 或者 Mac 开发,你一定写过不少 plist。plist 就是一种拥有自有 DTD 的 XML 文档类型。说白了,它就是 XML 文档。
例如之前的 JSON 指令,转换为 Safari 调试协议能够理解的 plist 文档:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>__selector</key>
<string>_rpc_forwardSocketData:</string>
<key>__argument</key>
<dict>
<key>WIRConnectionIdentifierKey</key>
<string>e0e68c53-5cc9-4dd4-9ebb-a7e69e98ef74</string>
<key>WIRApplicationIdentifierKey</key>
<string>PID:1300</string>
<key>WIRPageIdentifierKey</key>
<integer>1</integer>
<key>WIRSenderKey</key>
<string>50c2e189-a91f-4df5-b33a-741225e9bd85</string>
<key>WIRSocketDataKey</key>
<data>eyJpZCI6MCwibWV0aG9kIjoiTmV0d29yay5lbmFibGUifQ==</data>
</dict>
</dict>
</plist>
可以看到 plist 拥有多种标签来定义数据类型,例如 dict、string、data 等;同时节点的顺序,都是遵循 key、value 的顺序编写。 也就是说 JSON 是可以和 plist 互相转换的。
这个转换过程中,唯一麻烦的是 data 类型。这个标签是用来存储二进制数据的,JSON 中没有定义。但是在 NodeJS 中,可以无缝转换为一个 Buffer。
细心的你一定注意到上面 plist 中的两个问题:
事实上,Safari 的调试协议中,要求 JSON 字符串是被当作 payload data 传输的。而 plist 标准中,data 数据类型,就是进行 base64 编码的。
plist 到 bplist 的转换
bplist 是 binary plist 的简称。它以二进制编码为基础,可以用来存储 plist 格式中同样的内容。这在 socket 通讯中十分有用。
要知道 Safari 调试协议只接受 bplist 格式。具体客户端的开发中,没有规定一定要像本文中将一个指令先转换为 plist,再转换为 bplist。安排这样的转换,只是方便大家理解。你完全可以直接将一个 JSON 构造为 bplist。
前文的那段 plist,转换为 binary plist 就是:
00 00 01 C2 62 70 6C 69 73 74 30 30 D1 01 02 5F 10 12 57 ....bplist00..._..W
49 52 46 69 6E 61 6C 4D 65 73 73 61 67 65 4B 65 79 4F 11 IRFinalMessageKeyO.
01 77 62 70 6C 69 73 74 30 30 D2 01 03 02 04 5A 5F 5F 73 .wbplist00.....Z__s
65 6C 65 63 74 6F 72 5F 10 17 5F 72 70 63 5F 66 6F 72 77 elector_.._rpc_forw
61 72 64 53 6F 63 6B 65 74 44 61 74 61 3A 5A 5F 5F 61 72 ardSocketData:Z__ar
67 75 6D 65 6E 74 D5 05 07 09 0B 0D 06 08 0A gument.........
0C 0E 5F 10 1A 57 49 52 43 6F 6E 6E 65 63 74 69 6F 6E 49 .._..WIRConnectionI
64 65 6E 74 69 66 69 65 72 4B 65 79 5F 10 24 65 30 65 36 dentifierKey_.$e0e6
38 63 35 33 2D 35 63 63 39 2D 34 64 64 34 2D 39 65 62 62 8c53-5cc9-4dd4-9ebb
2D 61 37 65 36 39 65 39 38 65 66 37 34 5F 10 1B 57 49 52 -a7e69e98ef74_..WIR
41 70 70 6C 69 63 61 74 69 6F 6E 49 64 65 6E 74 69 66 69 ApplicationIdentifi
65 72 4B 65 79 58 50 49 44 3A 31 33 30 30 5F 10 14 57 49 erKeyXPID:1300_..WI
52 50 61 67 65 49 64 65 6E 74 69 66 69 65 72 4B 65 79 10 RPageIdentifierKey.
01 5C 57 49 52 53 65 6E 64 65 72 4B 65 79 5F 10 24 35 30 .\WIRSenderKey_.$50
63 32 65 31 38 39 2D 61 39 31 66 2D 34 64 66 35 2D 62 33 c2e189-a91f-4df5-b3
33 61 2D 37 34 31 32 32 35 65 39 62 64 38 35 5F 10 10 57 3a-741225e9bd85_..W
49 52 53 6F 63 6B 65 74 44 61 74 61 4B 65 79 4F 10 22 7B IRSocketDataKeyO."{
22 69 64 22 3A 30 2C 22 6D 65 74 68 6F 64 22 3A 22 4E 65 "id":0,"method":"Ne
74 77 6F 72 6B 2E 65 6E 61 62 6C 65 22 7D A0 00 08 00 0D twork.enable"}.....
00 18 00 32 00 3D 00 48 00 65 00 8C 00 AA 00 B3 00 CA 00 ...2.=.H.e.........
CC 00 D9 01 00 01 13 00 00 00 00 00 00 02 01 00 00 00 00 ...................
00 00 00 0F 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 ...................
39 A0 00 08 00 0B 00 20 00 00 00 00 00 00 02 01 00 00 00 9...... ...........
00 00 00 00 03 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ...................
01 9C ..
plist 和 bplist 都是有相关文档的,所以大家还是制作了不少第三方工具的:
Appium 使用的是:
最后一步
iOS 上的 webinspector 服务接受到这个 bplist 消息,自然会进行一个逆向操作,得倒 JSON 的字符串表达以及其他信息(appId、pageId、senderId)。
最后,通过 Safair 调试协议中的其他辅助信息,将这个 JSON 指令传输给正确的 Webkit 实例。
Safari 调试协议的会话流程
调试协议的会话,本身是有一定流程的,只有一些初始操作完成后,DevTool 才能正确的发送调试指令。为了方便阅读,消息全部以 plist 格式做演示,实际上我们传输的 bplist。
首先,当主机和 iOS 设备的 webinspectord 服务连接(PC <-> Usbmux <-> Lockdown <-> webinspectord)创立的时候,会要求汇报这个连接的标示符。例如:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>__selector</key>
<string>_rpc_reportIdentifier:</string>
<key>__argument</key>
<dict>
<key>WIRConnectionIdentifierKey</key>
<string>3b417e9a-9635-4059-a63e-ca88c98744bf</string>
</dict>
</dict>
</plist>
然后,我们要获取到已经连接到调试服务的 iOS 应用:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>__selector</key>
<string>_rpc_getConnectedApplications:</string>
<key>__argument</key>
<dict>
<key>WIRConnectionIdentifierKey</key>
<string>3b417e9a-9635-4059-a63e-ca88c98744bf</string>
</dict>
</dict>
</plist>
针对某个应用,获取其内部的页面列表:
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>__selector</key>
<string>_rpc_forwardGetListing:</string>
<key>__argument</key>
<dict>
<key>WIRConnectionIdentifierKey</key>
<string>3b417e9a-9635-4059-a63e-ca88c98744bf</string>
<key>WIRApplicationIdentifierKey</key>
<string>PID:26</string>
</dict>
</dict>
</plist>
拿到了 appId、pageId 之后,就可以开始一个调试会话(注册一个 senderId )了:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>__selector</key>
<string>_rpc_forwardSocketSetup:</string>
<key>__argument</key>
<dict>
<key>WIRConnectionIdentifierKey</key>
<string>2819b1eb-27ae-4c48-b195-6e5df02d0260</string>
<key>WIRApplicationIdentifierKey</key>
<string>PID:2851</string>
<key>WIRPageIdentifierKey</key>
<integer>1</integer>
<key>WIRSenderKey</key>
<string>4e9f5b1b-d9b7-4bcf-a315-25c10146a74d</string>
</dict>
</dict>
</plist>
之后,就可以传输具体的调试指令了。如果使用 IOS 模拟器的话,那可以直接打开在监听在本地 27753 端口的 socket。但是如果是真机的话,
就需要借助 libimobiledevice 库,有兴趣的同学可以去看 ios-driver 的实现。
说了那么多,终于到 Appium 了。我们先看下 Appium 测试 hybrid webview 的步骤:
ios-hybrid.js
中 listWebFrames 方法。WEBVIEW_Number
这个数字是变化的。在 Appium 官方文档上是这样说的:
https://github.com/appium/appium/blob/master/docs/en/advanced-concepts/hybrid.md
Automating hybrid iOS appsTo interact with a web view appium establishes a connection using a remote debugger. When executing against a simulator this connection is established directly as the simulator and the appium server are on the same machine.
Once you've set your desired capabilities and started an appium session, follow the generalized instructions above.
Execution against a real iOS device
When executing against a real iOS device appium is unable to access the web view directly. Therefore the connection has to be established through the USB lead. To establish this connection we use the ios-webkit-debugger-proxy.
简单来说就是:
这块的代码实现在 ios-hybrid.js
中 listWebFrames 方法:
...
, rd = require('./remote-debugger.js')
, wkrd = require('./webkit-remote-debugger.js');
...
if (this.args.udid !== null) {
this.remote = wkrd.init(exitCb);
this.remote.pageArrayFromJson(onDone);
} else {
this.remote = rd.init(exitCb);
this.remote.connect(function (appDict) {
...
remote debugger
remote debugger 是对 Safari 远程调试的 nodejs 封装。实现的就是我们上面说的 Safari 调试协议的会话流程。 主要代码在 remote_messages.js
中:
exports.setConnectionKey = function (connId) {
return {
__argument: {
WIRConnectionIdentifierKey: connId
},
__selector : '_rpc_reportIdentifier:'
};
};
exports.connectToApp = function (connId, appIdKey) {
return {
__argument: {
WIRConnectionIdentifierKey: connId,
WIRApplicationIdentifierKey: appIdKey
},
__selector : '_rpc_forwardGetListing:'
};
};
exports.setSenderKey = function (connId, appIdKey, senderId, pageIdKey) {
return {
__argument: {
WIRApplicationIdentifierKey: appIdKey,
WIRConnectionIdentifierKey: connId,
WIRSenderKey: senderId,
WIRPageIdentifierKey: pageIdKey
},
__selector: '_rpc_forwardSocketSetup:'
};
};
ios-webkit-debugger-proxy
由于无法通过 tcp 和真机直接联系,appium 不能采用 remote debugger
方式。Appium 也没有直接采用 libimobiledevice,而是借助了 ios-webkit-debugger-proxy, 当然 ios-webkit-debug-proxy 使用了 libimobiledevice。
ios-webkit-debug-proxy 将 DevTools UI,WebKit Remote Debugging Protocol 和苹果的远程调试都糅合在一起,曝露给用户较为简单的 Devtools UI,而将 json 转换成 bplist,通过 usbmux 传到 iOS 设备(包括真机和模拟器)中去的过程封装起来。如下图:
这样,对于客户端而言,我们就可以像前面说的 Webkit 远程调试一样,向本地的 localhost 27753 端口发送 json 请求,而 ios-webkit-debug-proxy 会在背后将你的请求经过几道加工送去设备。
Appium iOS hybrid webview 实例
这是官方的 sample code 里的代码,https://github.com/appium/sample-code/blob/master/sample-code/examples/node/ios-webview.js, 做了点修改。
it("should get the url", function () {
return driver
.elementByXPath('//UIATextField[@value=\'Enter URL\']')
.sendKeys('www.baidu.com')
.sleep(1000)
.elementByName('Go').click()
.elementByClassName('UIAWebView').click() // dismissing keyboard
.contexts().then(function (contexts) { // get list of available views. Returns array: ["NATIVE_APP","WEBVIEW_1"]
return driver.context(contexts[1]); // choose the webview context
})
.sleep(1000)
.waitForElementByName('word', 5000)
.sendKeys('sauce labs')
.sendKeys(wd.SPECIAL_KEYS.Return)
.sleep(1000)
.title().should.eventually.include('sauce labs');
});
这段代码的意思,是打开百度,然后搜 “sauce labs”,然后返回 title。这里面有几个关键步骤:
contexts()
就如之前所说,
session/:sessionId/contexts
。host:port/json
返回所有的 webview json object 数组,再加上 native 的作为 contexts 返回给客户端。_rpc_forwardGetListing:
bplist 消息发送到本地的 27753 socket,模拟器的 webinspector 得到请求,返回所有的 webview,appium 拿到这些 webview,再加上 native 的作为 contexts 返回给客户端。context()
session/:sessionId/context
, 参数是 webview 的 context name。udid
来选择上下文。udid
不为空,那么根据 webview 的名字得到 pageID,在构建 url = 'ws://' + this.host + ':' + this.port + '/devtools/page/' + pageId;
,再用 websocket 打开这个 page 所在的 websocket。于是就可以开始沟通了。_rpc_forwardSocketSetup
的 bplist 传递给模拟器,注册一个会话。以上 两步的使用的 remote 是不同的实例。
sendKeys
rest.post('/wd/hub/session/:sessionId?/element/:elementId?/value', controller.setValue);
如果错了,请纠正,不过不影响命令分析。Runtime.evaluate
的 bplist 信息传给模拟器监听的 socket。其实传给真机的 json,也是被 ios-webkit-debugger-proxy 转换成 Runtime.evaluate
的 bplist 信息。 然后剩下的就是设备内部的 webinspector 执行 js 命令了。至此,appium 操作 iOS webview 的原理基本已经剖析清楚。另外文中部分协议相关的内容来自其他牛人的摸索和总结,我也算是借花献佛。如果有同学觉得侵犯了你的权益,请指出。