Appium 中 iOS 下的 Hybrid

hcoder 测试交流1 108字数 18295阅读模式

Appium Android 上的 Hybrid 的一些知识可以看下@qddegtya的文章:

  • webviewv36 支持的 Remote Debugging 特性以及与 appium 的关系
  • chrome mobile emulation 及周边漫谈和相关应用 + 想法

今天我们只说 Appium 中 iOS 下的 Hybrid。文章源自玩技e族-https://www.playezu.com/192593.html

UIAutomation

众所周知, Appium 上的 iOS 自动化使用的是苹果自带的 instruments 的 UIAutomation。它可以操控继承自 UIKIT 的 native 的界面控件。那对于 webview(UIAWebView)里面的元素,会把一部分 html 元素隐射成 native 的元素,比如:textFields/links/buttons。我们可以从 logElementTree 方法中看下:文章源自玩技e族-https://www.playezu.com/192593.html

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();

文章源自玩技e族-https://www.playezu.com/192593.html

但是 html 的元素和场景都非常多,在复杂场景下 UIAutomation 就捉襟见肘了(当然暴力坐标系除外)。文章源自玩技e族-https://www.playezu.com/192593.html

那 Appium 是怎么做的呢?文章源自玩技e族-https://www.playezu.com/192593.html

Webkit 远程调试协议

在了解 Appium 怎么做之前,我们需要了解下 Webkit 远程调试协议。用 Chrome 的人应该都知道 Chrome DevTools。很多人都以为它是 Chrome 的一个组件。事实上开发者工具 (DevTools) 是一个独立的 Web 应用程序 (HTML+CSS+Javascript),被集成在浏览器中,通过远程调试协议 (remote debugging protocol) 和浏览器内核进行交互。文章源自玩技e族-https://www.playezu.com/192593.html

文章源自玩技e族-https://www.playezu.com/192593.html

什么是远程调试协议?

文章源自玩技e族-https://www.playezu.com/192593.html
  • 远程调试协议基于 WebSocket,利用 WebSocket 建立连接 DevTools 和浏览器内核的快速数据通道。
  • 浏览器拥有多个 Tab,并为每个 Tab 单独提供 Websocket 的 Endpoint URI
  • 每个 DevTool 实例只能检视一个 Tab,即只能与一个 Tab 保持通讯

我们先来体验一把远程调试文章源自玩技e族-https://www.playezu.com/192593.html

  • 彻底关闭当前 Chrome 进程
  • 打开调试接口
open -a Google Chrome --args --remote-debugging-port=9999
  • 在开启的浏览器里打开任意网页—— http://玩技博客.com
  • 在其他浏览器或者 Chrome 本身打开http://localhost:9222

文章源自玩技e族-https://www.playezu.com/192593.html
  • 点击百度,会得到类似这样的界面: 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。

  • 我们打开这个页面的 devtools 界面。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 ,比如

  • DOM
  • Debugger
  • Network
  • Console
  • Timeline

可以理解为 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 远程调试服务

讲了那么多,终于到苹果了。Safari 6(只能 OS X)之后,你不但可以使用 Safari 开发者工具远程调试 iOS 设备上的移动 Safari 网页,而且可以远程调试任何 UIWebView 里的网页,比如 PhoneGap 应用。

Safari 的远程调试中使用到的调试协议,与 Google 开放的 Chrome Debug Protocol 有牵丝万缕的关系:

  • 这两套协议本质都是 Webkit Debug Protocol 的衍生产物。大部分的实际功能是一模一样的,例如 DOM 检视、网络请求监控、Console 等。
  • 自从 Google 与 Webkit 项目撇清关系,分道扬镳后生下亲儿子 blink 后,自己的调试功能越来越强大,并逐渐产生了一些和 Webkit 调试协议不一样的功能,故自成一套 Chrome Debug Protocol。
  • Apple Safari 虽然主导 Webkit,但是在实际产品使用中,却并没有直接使用 Webkit Debug Protocol,不仅仅使用了 binary plist 来作为序列化方法,还抛弃了 websocket 的通讯手段。没有文档、没有代码,我们姑且叫这个不舍得露面的东西叫做 Safari 调试协议吧。

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 调试协议的大部分功能。

先分析这个协议里面的主体:

  • iOS 设备,iPad、iPhone 等物理设备
    • UDID: 以 40 位 UDID 字符串唯一识别一个设备
  • Application,iOS 设备上运行的开启了 WebView 应用程序。设备上可以同时运行多个 Application
    • Identifier,应用标示符
    • BundleIdentifier,应用的 main bundle 标示符
  • Page,每个 Application 可以打开多个页面
    • Identifier, 页面标示符
    • Title
    • URL

还有一些概念字段:

  • ConnectionId,标示当前连接到 webinspector 服务的连接
  • SenderId,标示请求方(例如 Devtool )实体

大体可以看出,这个调试服务的接口是有状态的。设备和 Safari devtool 建立连接后,拥有可以复用的链接作为后续通讯的通道。假设我们需要发送一条调试指令到 iOS 上的某个 Webkit 内核,它的整个编码流程应该是是这样:

  • 将 Webkit 能识别的消息对象转化为 JSON 字符串
  • 构建 Safari 调试协议中使用的 bplist 消息体,来包装之前得到的字符串。这里用到的 selector 就是 _rpc_forwardSocketData
  • 将 bplist 消息体通过 socket 传输到 iOS 上的调试服务
  • iOS 上调试服务识别消息,并解析 bplist,得倒 Webkit 能识别的消息对象
  • 将上一步得倒的消息对象传输给 Webkit

所有的消息,都是以 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 的字符串表达) 如下:

  • _rpc_reportIdentifier:: 向 webinspector 服务注册当前链接(传输 connectionId)
  • _rpc_getConnectedApplications:: 要求获取连接到 webinspector 的 iOS 应用列表
  • _rpc_forwardGetListing:: 获取某个应用的页面列表(传输 connectionId, appId)
  • _rpc_forwardSocketSetup:: 注册当前会话(传输 connectionId、senderId)
  • _rpc_forwardSocketData:: 利用某个会话传输数据(传输 connectionId、senderId、data)。Webkit 调试协议所传输的 JSON 就是通过这个方法传递的——JSON 字符串的二进制表达被通过这个接口传递到 iOS 设备上的调试服务。

另一方面,iOS 端也会传过来很多消息,同样遵循基本消息提的格式,常见的 __selector 有:

  • _rpc_reportConnectedApplicationList:: 回报连接到 webinspector 的应用列表
  • _rpc_applicationSentListing:: 回报某个应用的页面列表
  • _rpc_applicationConnected:: 某个 iOS 应用连接到了调试服务
  • _rpc_applicationDisconnected:: 某个 iOS 应用从调试服务断开

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 中的两个问题:

  • 没发现任何 JSON 字符串的内容
  • WIRSocketDataKey 里面的竟然是 base64 编码的字符串

事实上,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 都是有相关文档的,所以大家还是制作了不少第三方工具的:

  • https://github.com/libimobiledevice/libplist
  • https://github.com/mgcrea/node-plist-native

Appium 使用的是:

  • https://github.com/joeferner/node-bplist-creator
  • https://github.com/joeferner/node-bplist-parser

最后一步

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 了。我们先看下 Appium 测试 hybrid webview 的步骤:

  • Navigate to a portion of your app where a web view is active
  • Call GET session/:sessionId/contexts —— 注意这个请求到 appium server 之后, appium server 将会调用 ios-hybrid.js 中 listWebFrames 方法。
  • This returns a list of contexts we can access, like 'NATIVE_APP' or 'WEBVIEW_1' —— WEBVIEW_Number 这个数字是变化的。
  • Call POST session/:sessionId/context with the id of the context you want to access —— 切换到 webview。
  • (This puts your Appium session into a mode where all commands are interpreted as being intended for automating the web view, rather than the native portion of the app. For example, if you run getElementByTagName, it will operate on the DOM of the web view, rather than return UIAElements. Of course, certain WebDriver methods only make sense in one context or another, so in the wrong context you will receive an error message). —— 进入 webview 之后,操作的就是 DOM 元素了。
  • To stop automating in the web view context and go back to automating the native portion of the app, simply call context again with the native context id to leave the web frame. —— 如果要退出 webview,只需切换 context 回原生就可以了。

在 Appium 官方文档上是这样说的:

https://github.com/appium/appium/blob/master/docs/en/advanced-concepts/hybrid.md
Automating hybrid iOS apps

To 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.

简单来说就是:

  • 模拟器使用 remote debugger
  • 真机使用 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 中:

  • setConnectionKey
  • connectToApp
  • setSenderKey
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()

就如之前所说,

  1. contexts 方法会产生一个 GET 请求: session/:sessionId/contexts
  2. rest.get('/wd/hub/session/:sessionId?/contexts', controller.getContexts); 映射到 iOS controller 的 getContexts 方法。
  3. getContexts => getContextsAndViews => listWebFrames listWebFrames 方法就会根据 udid 是否存在来初始化 remote 实例。
  4. 如果是真机就使用 ios-webkit-debugger-proxy, 会去请求:host:port/json 返回所有的 webview json object 数组,再加上 native 的作为 contexts 返回给客户端。
  5. 否则如果是模拟器就直接使用 Safari 远程调试。构建 _rpc_forwardGetListing: bplist 消息发送到本地的 27753 socket,模拟器的 webinspector 得到请求,返回所有的 webview,appium 拿到这些 webview,再加上 native 的作为 contexts 返回给客户端。

context()

  1. 产生一个 POST 请求:session/:sessionId/context, 参数是 webview 的 context name。
  2. rest.post('/wd/hub/session/:sessionId?/context', controller.setContext); 映射到 iOS controller 的 setContext 方法。
  3. setContext =》pickContext, pickContext 依旧根据 udid 来选择上下文。
  4. 如果是真机, udid 不为空,那么根据 webview 的名字得到 pageID,在构建 url = 'ws://' + this.host + ':' + this.port + '/devtools/page/' + pageId;,再用 websocket 打开这个 page 所在的 websocket。于是就可以开始沟通了。
  5. 如果是模拟器,就用 selectPage 构建 _rpc_forwardSocketSetup 的 bplist 传递给模拟器,注册一个会话。以上 两步的使用的 remote 是不同的实例。

sendKeys

  1. sendKeys 应该走的是这个路由 rest.post('/wd/hub/session/:sessionId?/element/:elementId?/value', controller.setValue); 如果错了,请纠正,不过不影响命令分析。
  2. 如果是 webview, 就调用 executeAtom 执行 click 和 type 事件。
  3. executeAtom 中会构建 js 命令,然后使用前面初始化好的 remote 对象的 execute 方法来执行。如果是真机的话,就是向前面构建好的 websocket 写入 json 命令。否则的话,就构建 Runtime.evaluate 的 bplist 信息传给模拟器监听的 socket。其实传给真机的 json,也是被 ios-webkit-debugger-proxy 转换成 Runtime.evaluate 的 bplist 信息。 然后剩下的就是设备内部的 webinspector 执行 js 命令了。

总结

至此,appium 操作 iOS webview 的原理基本已经剖析清楚。另外文中部分协议相关的内容来自其他牛人的摸索和总结,我也算是借花献佛。如果有同学觉得侵犯了你的权益,请指出。软件功能测试模板

玩技站长微信
添加好友自动发送入群邀请
weinxin
rainbow-shownow
玩技官方公众号
官方微信公众号
weinxin
PLAYEZU
 
  • 版权提示:本站仅供存储任何法律责任由作者承担▷诈骗举报◁▷新闻不符◁▷我要投稿◁
    风险通知:非原创文章均为网络投稿真实性无法判断,侵权联系2523030730
    免责声明:内容来自用户上传发布或新闻客户端自媒体,切勿!切勿!切勿!添加联系方式以免受骗。
  • 原创转载:https://www.playezu.com/192593.html
    转载说明: 点我前往阅读>>>
    • 卡斯
      卡斯 9

      牛逼点赞学习了!这样的原理剖析不仅学会了原理,还知道了应该如何正确使用这个功能。收藏~冒着生命危险进来提个 BUG,hibird –> hybrid。
      逃。。#5 楼 @anikikun 已修正#6 楼 @lihuazhang ←_← 验证已解决 –> closed…Appium 本身使用 node.js 的好处太多,这算其中之一~我是来顶帖子的!有些细节好像不对。最近看到帖子, 感觉帮了很大的忙, 谢谢 感谢.. 另外有个问题咨询下, 文章中的如下信息:

      _rpc_reportIdentifier:: 向 webinspector 服务注册当前链接(传输 connectionId)
      _rpc_getConnectedApplications:: 要求获取连接到 webinspector 的 iOS 应用列表
      _rpc_forwardGetListing:: 获取某个应用的页面列表(传输 connectionId, appId)
      _rpc_forwardSocketSetup:: 注册当前会话(传输 connectionId、senderId)
      _rpc_forwardSocketData:: 利用某个会话传输数据(传输 connectionId、senderId、data)。Webkit 调试协议所传输的 JSON 就是通过这个方法传递的——JSON 字符串的二进制表达被通过这个接口传递到 iOS 设备上的调试服务。
      另一方面,iOS 端也会传过来很多消息,同样遵循基本消息提的格式,常见的 __selector 有:

      _rpc_reportConnectedApplicationList:: 回报连接到 webinspector 的应用列表
      _rpc_applicationSentListing:: 回报某个应用的页面列表
      _rpc_applicationConnected:: 某个 iOS 应用连接到了调试服务
      _rpc_applicationDisconnected:: 某个 iOS 应用从调试服务断开

      以上这些_rpc_xx 的信息, 如果想看更全一些的, 或者想知道最近几年的一些变化. 应该去哪里找呢? Google 了一下并没有相关信息. 所以想知道这些资料从哪里获得的..
      非常感谢……

    匿名

    发表评论

    匿名网友
    确定