上一篇文章 介绍了 DevTools 和 Webkit Debug Protocol 这两个 Web 开发利器的内部原理。本篇主要讲解 iOS 的 Safari 远程调试。
iOS 的 Safari 远程调试,是 iOS7 引入的新功能。它允许开发者通过桌面端 Safari 的调试工具远程检视移动端浏览器打开的页面。这套调试工具,彻底解决了无线开发纯靠 “alert” 的调试困境。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
iOS7 以后,每个 iOS 都有一个 webinspectord 守护进程,负责远程调试的通讯。这个进程暴露了一个服务接口,供外部应用(例如桌面端的 Safari 调试工具)使用。
iOS 上所有的服务(文件浏览、消息推送、app 安装等)都是通过一个 lockdown 服务管理连接上的。
自然的,调试工具也需要透过 USB 接口,通过 lockdown 界面,连接到 webinspector 服务。
由于 iOS 的各种系统组件都极为神秘,没有更多可以解释的了。伟大的开源社区,通过各种手段实现了这些服务的接口,大家可以前去膜拜。
Safari 远程调试服务概述
抛开 USB 通讯、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, 标示请求方(例如 DevTools )实体
大体可以看出,这个调试服务的接口是有状态的。设备和 DevTools 建立连接后,拥有可以复用的链接作为后续通讯的通道。
假设我们需要发送一条调试指令到 iOS 上的某个 Webkit 内核,它的整个编码流程应该是是这样:
- Webkit 能识别的消息对象进行 JSON 序列化为字符串
- 构建 Safari 调试协议中使用的 bplist 消息体,来包装之前得到的字符串。这里用到的 selector 就是
_rpc_forwardSocketData
- 将消息题通过 Socket 传输到 iOS 上的调试服务
- iOS 上调试服务识别消息,并解析 bplist,得倒 Webkit 能识别的消息对象
- 将上一步得倒的消息对象传输给 Webkit
所有的消息,都是以 Apple 自己定义的 RPC 消息提格式进行的。但它实际传输的有效数据,还是 Webkit
能够识别的指令。另外,Safari 没有如同 Chrome 那样,使用了 WebSocket 作为暴露出去的应用层协议。它选择了最基本的 Socket 通讯方式和 bplist 作为传输格式。
调试消息的大冒险
下面以一个具体的消息作为例子,来说说这整个过程。
JSON 消息和 Safari 的 RPC 协议
假设我们要开启网络监控这个面板,需要发送一个 JSON 指令。指令序列化之后我们的到:
1 |
{"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 作为传输格式,应该没有太多理由,大概因为 Apple 体系内部都是用 bplist 的。
JSON 到 plist 的转换
plist 和 bplist 都是 Apple 的通讯格式。其中 plist 非常常见。加入你做过 iOS 或者 Mac 开发,你一定写过不少 plist。plist 就是一种拥有自有 DTD 的 XML 文档类型。说白了,它就是 XML 文档。
例如之前的 JSON 指令,转换为 Safari 调试协议能够理解的 plist 文档:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<?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 中没有定义。但是在 Node.js 中,可以无缝转换为一个 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 就是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
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 5Fyon-h"> 74 6F 72 5F 10 17 5F/p>
iOS 的 Safari 远程调试,是 iOS7 引入的新功能。它允许开发者通过桌面端 Safari 的调试工具远程检视移动端浏览器打开的页面。这套调试工具,彻底解决了无线开发纯靠 “alert” 的调试困境。Safari 的远程调试中使用到的调试协议,与 Google 开放的 Chrome Debug Protocol 有牵丝万缕的关系,体现了这两家互联网巨头相爱相杀的本质:
webinspectord 和 lockdowniOS7 以后,每个 iOS 都有一个 webinspectord 守护进程,负责远程调试的通讯。这个进程暴露了一个服务接口,供外部应用(例如桌面端的 Safari 调试工具)使用。 iOS 上所有的服务(文件浏览、消息推送、app 安装等)都是通过一个 lockdown 服务管理连接上的。 自然的,调试工具也需要透过 USB 接口,通过 lockdown 界面,连接到 webinspector 服务。 由于 iOS 的各种系统组件都极为神秘,没有更多可以解释的了。伟大的开源社区,通过各种手段实现了这些服务的接口,大家可以前去膜拜。 Safari 远程调试服务概述抛开 USB 通讯、lockdown 接口不谈,Safari 远程调试服务所使用的协议本身其实就是 Webkit 调试协议的二次包装。也就是共享了 Webkit 调试协议的大部分功能。 先分析这个协议里面的主体:
还有一些概念字段:
大体可以看出,这个调试服务的接口是有状态的。设备和 DevTools 建立连接后,拥有可以复用的链接作为后续通讯的通道。 假设我们需要发送一条调试指令到 iOS 上的某个 Webkit 内核,它的整个编码流程应该是是这样:
所有的消息,都是以 Apple 自己定义的 RPC 消息提格式进行的。但它实际传输的有效数据,还是 Webkit 调试消息的大冒险下面以一个具体的消息作为例子,来说说这整个过程。 JSON 消息和 Safari 的 RPC 协议假设我们要开启网络监控这个面板,需要发送一个 JSON 指令。指令序列化之后我们的到:
就像之前说的,Safari 不直接使用 JSON 字符串作为传输的序列化方案。Safari 远程调试协议有自己的 RPC 规范,所有的消息都都有 常见的一些方法(其实是 ObjC
另一方面,iOS 端也会传过来很多消息,同样遵循基本消息提的格式,常见的
Safari 不选择 WebSocket 作为传输协议应该是从安全性、复杂性的角度去考虑。但选择 bplist 作为传输格式,应该没有太多理由,大概因为 Apple 体系内部都是用 bplist 的。 JSON 到 plist 的转换plist 和 bplist 都是 Apple 的通讯格式。其中 plist 非常常见。加入你做过 iOS 或者 Mac 开发,你一定写过不少 plist。plist 就是一种拥有自有 DTD 的 XML 文档类型。说白了,它就是 XML 文档。 例如之前的 JSON 指令,转换为 Safari 调试协议能够理解的 plist 文档:
可以看到 plist 拥有多种标签来定义数据类型,例如 dict、string、data 等;同时节点的顺序,都是遵循 key、value 的顺序编写。也就是说 JSON 是可以和 plist 互相转换的。 这个转换过程中,唯一麻烦的是 data 类型。这个标签是用来存储二进制数据的,JSON 中没有定义。但是在 Node.js 中,可以无缝转换为一个 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 就是:
|