微信号:frontshow

介绍:InfoQ大前端技术社群:囊括前端、移动、Node全栈一线技术,紧跟业界发展步伐。

免费又好用的UI分析调试工具YourViewUI

2019-06-06 19:29 Talkingdata

作者 | Talkingdata
YourView 简介

YourView 是一款桌面 App,使用 Objective-C 语言开发,基于 Apple SceneKit 技术框架,支持将 iOS App 的 View 结构进行远端渲染并支持 3D 显示模式,还能够动态显示 View 树结构,方便开发者对 App UI 进行分析和调试。

在开发 YourView 之前,我们也有试用过一些其他的 UI 分析调试工具,但是这些工具大多数是收费的,而现有开源工具在功能上又难以满足需要。因此我们干脆自行研发并开源了 YourView。

MacOS 和 iOS 通信

在开发之初,我们一直在调研相关的技术实现。也曾经一度跑偏,认为有什么黑科技可以把 iOS 内存里的 UI 数据直接 dump 到 MacOS 中,然后 MacOS 可以直接渲染绘制,所以一直在研究 XPC 和进程通信。

但后来发现这条路行不通,第一,iOS 和 MacOS 分属于 ARM 架构和 X86 架构,指令集不一样,直接 dump 内存到不同架构的设备上是无法兼容的;第二,MacOS 和 iOS 的开发框架不同,即使能够 dump 出内存,还需要做大量的框架桥接代码。所以最后选择了另外一条更为直接的路,把 UIView 序列化成 JSON 字符串结构,通过网络协议传输,接收方接受 JSON 数据后,再反序列化成内存中的对象,然后绘制展示。

选择哪种网络协议

网络协议可以使用 WebSocket,也可以用 HTTP 协议。最后选择使用了 HTTP 协议。原因如下:

第一,WebSocket 需要 server 端的支持,目前 OC 语言并没有十分好的 WebSocket server 实现。

第二,这次开发不需要特别高的实时性,所以 HTTP 协议是一个比较好的选择,而且 OC 语言已经有比较好的 webServer 实现——GCDWebServer 和 CocoaHttpServer。由于 CocoaHttpServer 最近的维护已经是几年前了,所以我选择了维护更频繁的 GCDWebServer 作为通信的 Server 端。

Server 应该放在哪里
  • 放在桌面端。由于 IP 无法固定,每次 iOS 设备启动的时候需要动态的去配置 IP 地址,并且如果在 iOS 端提供输入框或者每次动态配置代码填入 IP 又太不友好,违背了易用性原则,于是放弃了这个选择。

  • 放在 iOS 端。在 iOS App 内启动 HTTP Server,有被劫持的风险,如果真的做成商用软件,这样做无疑是很危险的。但是作为一个开源软件来讲,这是 OK 的,因为在开放的源代码面前,一切都是透明的。如果开发者想开发自己桌面端,可以加上参数校验签名等机制,提升安全性。

自动连接还是手动连接

iOS 提供了 BonjourService。Bonjour 是法语你好的意思。这里跑题多讲几句。据研究表明(其实是瞎编的),全世界人民都比较喜欢异域文字带来的新奇感,就像会有人起名叫 Tony 一样,英语语系的人们也喜欢起一些奇怪的德系甚至拉丁语系的名字。所以这个 BonjourService,翻译过来其实就是 HelloService。字面意思很好理解,就是在局域网里广而告之,和局域网里的所有人 SayHello。

这个服务最大的优势就是在局域网里可以自动的获取对方的 IP 地址,完成通信。现在很多厂商已经内置了 BonjourService,特别是打印机厂商。在 MacOS 上则无需知道对方的 IP 地址,就可以自动完成连接操作,非常方便。

所以也考虑过在 iOS 端开启 BonjourService,而且 GCDWebServer 已经实现了相关的接口。但是实践证明,当在 MacOS 上实现 BonjourService Browser 之后,虽然能够自动识别设备,但是面对中间的网络异常,比如防火墙导致的网络无法连接等,并没有很好地方法进行提示,在连接不上的时候很容易让人摸不着头脑、不能准确了解状况。这样对开发者其实是不友好的。由于网络只是通信的必要手段,并不是 UI 查看的重点,所以我们选择把更多的精力放在 UI 绘制上,而将网络模块尽量做得轻量易于调试,于是放弃了自动扫描的方式。

衡量之下,我们选择在 YourView 桌面端启动的时候,给用户提供一个 IP 输入框。用户在输入 IP 之后,点击连接,如果网络有问题会直接弹框提示。虽然技术上的自动好过手动,但是自动带来的复杂性和不确定性,以及考虑到在实践中的实际表现,最后还是选择了手动连接的方式。想来,这可能和老司机喜欢开手动档车是一样的吧,我们都喜欢操作可控带来的感觉(其实也是因为懒,划掉)。

UIView 的序列化

如果对树的操作不熟悉,那么可以移步 leetcode,先把关于树的操作的问题敲一遍。UIView 其实就是一棵多叉树。每个节点具有数据域和指针域,数据域就是自身的属性,指针域就是关系,在 UIView 中就是 subviews。所以理解了这一点,就很容易写出序列化代码了。

序列化方案一: 非递归平铺树

借助栈或者队列的帮助,对 view 树进行遍历把每个节点变成一 JSONObject,然后把这些 object 放到一个数组里。最后整棵树就像被拍平了一样,树变成了列表。把拍平的节点数组作为数据源,驱动 TableView 显示,然后根据每个节点自身的深度在对应的 cell 上绘制对应的缩进,用来表示树的层次结构。

这样的做法在 UI 中可以表现出树的层次结构,但是实际上已经丢失了树的两个重要特征,兄弟关系、父子关系。前驱后继关系丢失之后,对节点的收缩和展开操作就不太方便了。

序列化方案二:递归关系树。

贴一段简化过的递归代码

-(NSDictionary*)traversal
{
NSMutableArray * subArr = [NSMutableArray array];
for (UIView * v in self.subviews) {
[subArr addObject:[v traversal]];
}
return @{@"sub":subArr};
}

这段代码执行之后,就在 JSON 结构里保存了 UIView 的父子和兄弟关系。

序列化中对数据域的处理
iOS 端需要保存 UIView 对象

iOS 端需要保存 UIView 对象,为后续的 MacOS 端的操作(比如编辑)做准备。但是随着界面的滚动,UIView 可能被释放掉。所以这里选择了用 NSMapTable 用来做存储容器。

存储的 Key 就是 UIView 的内存地址。

-(NSString*)_address
{
return [NSString stringWithFormat:@"%p",self];
}

存储的是 UIView 对象本身,当 UIView 因为离开屏幕而被释放的时候,使用内存地址取值为空,不会产生野指针。

改造我们的递归函数

在递归参数中增加用来记录的 map。需要注意这个 map 是引用传递,递归中的每次调用都指向同一个 map。

-(NSDictionary*)traversalWithRecorder:(NSMapTable*)map
{
NSMutableArray * subArr = [NSMutableArray array];
[map setObject:self forKey:[NSString stringWithFormat:@"%p",self]];
for (UIView * v in self.subviews) {
[subArr addObject:[v traversalWithRecorder:map]];
}
return @{@"sub":subArr};
}
StepIn 对象
  1. UIViewController 的获取

想获取一个 UIView 对应的 ViewController,可以使用 nextResponser 属性,直到找到 UIViewController 停下。假如 UIView 的平均深度是 10,有 N 个 View,那么需要迭代的次数就是 N*10。这样会使得时间复杂度提升,所以我们对此进行了一些优化。对于 UIView,只找最近一级的 nextResponder,如果这个 responder 是 UIViewController 那么选择记录在自己的 data 域内,并且把这个 UIViewController 作为递归的参数传递到下一级,否则把递归中父节点的 controller 作为自己的 ViewController。再次改造递归函数:

-(NSDictionary*)traversal:(UIViewController*)vc
{
UIViewController * vcToNext = vc;
if ([[self nextResponder]isKindOfClass:[UIViewController class]]) {
vcToNext = [self nextResponder];
}
NSMutableArray * subArr = [NSMutableArray array];
for (UIView * v in self.subviews) {
[subArr addObject:[v traversal:vcToNext]];
}
return @{@"sub":subArr};
}

对于 UITableViewCell 和 UICollectionViewCell 也是同样的操作,在获取 IndexPath 的时候,也只向上找一级,否则直接从递归的参数中获取,递归的初始值是默认的 section=-1,row=-1。

  1. Depth 和 level 的处理

Depth 表明的是当前 view 所在树的深度。Depth 可以从当前的 view 直向上找 superview,直到 superview 为空为止。但是这样的处理也会带来和 UIViewController 获取同样的问题。所以这个和 UIViewController 一样的处理策略,每次递归的时候,把当前的 depth 加 1,向下传递。

序列化后的 view 属性:

无论是 Depth 还是 level,抑或 ViewController 和 IndexPath,它们都是从上级递归而来并且在当次递归中拼接自己的参数,所以我们选择把这些属性封装在一个 StepIn 对象里,并抽象出一个 stepin 方法。每次递归开始,StepIn 对象都会根据当前的 view 的状态,进行相应的 stepin 操作。

  3. 截图的处理

由于截图需要在 JSON 里传输,所以需要把截图的 imgData 转换成 base64 编码的 string。截图是针对 layer 的操作,在截图的时候,一定不能带上 sublayer。所以在针对每个 view 进行截图的时候,需要把没有 hidden 的 layer 变成 hidden 状态,并保存在数组中,在截图方法调用完毕之后,需要把数组 layer 的 hidden 属性进行 restore 操作。

MacOS 端的渲染

桌面端一共有三个 ViewController:Left、Middle 和 Right。其中 Left 负责展示树状结构,Middle 负责 3D 展示,Right 负责展示 view 属性。

Left

由于上文中的序列化操作已经把 UIView 变成了树状的 JSONString,所以直接把序列化之后的 string 转化为 NSDictionary 并作为数据源驱动 NSOutlineView 展示就 OK 了。

Middle
  1. 使用 SceneKit 渲染,用平面 SCNPlane 来展示 UIView 的截图。展示的同时需要把 UIView 的坐标从 UIKit 坐标系转换到 SceneKit 坐标系。转换公式如下:

  2. 射线检测:鼠标移动的时候需要将鼠标指向的 view 边框高亮,边框是当前 Node 的一个 subNode,在被指向的时候,将前一个 unhover,将当前指向的高亮。选中也是同样的道理,鼠标单击的时候,将射线击中 Node 的子 Node 的 hidden 属性置为 No 即可。SceneKit 提供了类似射线检测的 API,直接用 point 和 plane 调用 hitTest 方法,取返回结果的第 0 个元素即可。

  3. Z 轴控制:目前 YourView 共支持三种显示模式。现在大部分开源软件的做法是将所有 view 拍平之后按照深度优先的顺序每层排列一个,如果 view 特别多的话,会造成所有 z 轴特别大,在旋转的时候视觉效果很差。YourView 则实现了智能回溯算法,在深度优先的基础上,递归中记录当前被占据的 level 和 frame,每次新的 view 进来都会从深度优先的基础上向前回溯,一直找到第一个不被遮挡的位置。

  4. 相机的选择:可以在 Xcode 的场景编辑器里直观的感受一下。左边是透视相机,右边是正交相机。

a.     正交相机:orthographicCamera 所见即所得,所有 view 的 scale 不随深度变化;

b.    透视相机:view 近大远小。为了更好的视觉效果,YourView 选择使用正交相机用来展示 View。

后续的优化
  • 目前 YourView 只实现了 3D 渲染,对 UIView 的动态编辑能力还比较弱,后续会继续完善编辑功能;

  • 在 View 树里增加 UIViewController 手势,布局等元素;

  • UI 美化工作和用户体验提升。

参考资料:

  • Apple SceneKit:
    https://developer.apple.com/scenekit/

  • Bonjour:
    https://developer.apple.com/bonjour/

 
前端之巅 更多文章 巧用ES6,轻松优化Vue代码 百度自研面向智能设备的语音交互编程语言VSL:用于实现语音交互视图 开发者拍案叫绝!苹果发布全新Swift UI框架 iTunes将死,苹果计划发布三款独立应用取而代之 Lightning Web组件霸气开源:基于最新的Web标准!可构建任何平台的Web应用
猜您喜欢 大神们,还记得那些年你们吹过的牛X吗? 产品技术多方平衡,企业级容器 PaaS 如何满足用户的多样化需求? 关于JAVA 无声信息与七牛云达成战略合作 共筑网络安全防线 开发可扩展的Vue组件