YourView是一款桌面App,使用Objective-C语言开发,基于Apple SceneKit技术框架,支持将iOS App的View结构进行远端渲染并支持3D显示模式,还可以动态显示View树结构,方便开发者对App UI进行分析和调试。git
在上一篇文章《UI分析工具YourView开源—App开发者不可多得的利器!》中,咱们列举了一些YourView的特性,以及项目的GitHub地址:https://github.com/TalkingData/YourView 欢迎查看,Star&Fork。github
在开发YourView以前,咱们也有试用过一些其余的UI分析调试工具,可是这些工具大多数是收费的,而现有开源工具在功能上又难以知足须要。所以咱们干脆自行研发并开源了YourView。web
在开发之初,咱们一直在调研相关的技术实现。也曾经一度跑偏,认为有什么黑科技能够把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实现。bash
第二,此次开发不须要特别高的实时性,因此HTTP协议是一个比较好的选择,并且OC语言已经有比较好的webServer实现——GCDWebServer和CocoaHttpServer。因为CocoaHttpServer最近的维护已是几年前了,因此我选择了维护更频繁的GCDWebServer做为通讯的Server端。网络
放在桌面端:因为IP没法固定,每次iOS设备启动的时候须要动态的去配置IP地址,而且若是在iOS端提供输入框或者每次动态配置代码填入IP又太不友好,违背了易用性原则,因而放弃了这个选择。架构
放在iOS端:在iOS App内启动HTTP Server,有被劫持的风险,若是真的作成商用软件,这样作无疑是很危险的。可是做为一个开源软件来说,这是OK的,由于在开放的源代码面前,一切都是透明的。若是开发者想开发本身桌面端,能够加上参数校验签名等机制,提高安全性。app
自动链接:iOS提供了BonjourService。Bonjour是法语你好的意思。这里跑题多讲几句。据研究代表(实际上是瞎编的),全世界人民都比较喜欢异域文字带来的新奇感,就像会有人起名叫Tony同样,英语语系的人们也喜欢起一些奇怪的德系甚至拉丁语系的名字。因此这个BonjourService,翻译过来其实就是HelloService。字面意思很好理解,就是在局域网里广而告之,和局域网里的全部人SayHello。
这个服务最大的优点就是在局域网里能够自动的获取对方的IP地址,完成通讯。如今不少厂商已经内置了BonjourService,特别是打印机厂商。在macOS上则无需知道对方的IP地址,就能够自动完成链接操做,很是方便。
手动链接:因此也考虑过在iOS端开启BonjourService,并且GCDWebServer已经实现了相关的接口。可是实践证实,当在macOS上实现BonjourService Browser以后,虽然可以自动识别设备,可是面对中间的网络异常,好比防火墙致使的网络没法链接等,并无很好地方法进行提示,在链接不上的时候很容易让人摸不着头脑、不能准确了解情况。这样对开发者实际上是不友好的。因为网络只是通讯的必要手段,并非UI查看的重点,因此咱们选择把更多的精力放在UI绘制上,而将网络模块尽可能作得轻量易于调试,因而放弃了自动扫描的方式。
衡量之下,咱们选择在YourView桌面端启动的时候,给用户提供一个IP输入框。用户在输入IP以后,点击链接,若是网络有问题会直接弹框提示。虽然技术上的自动好过手动,可是自动带来的复杂性和不肯定性,以及考虑到在实践中的实际表现,最后仍是选择了手动链接的方式。想来,这可能和老司机喜欢开手动档车是同样的吧,咱们都喜欢操做可控带来的感受(其实也是由于懒)。
若是对树的操做不熟悉,那么能够移步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的父子和兄弟关系。
UIView对象
iOS端须要保存UIView对象,为后续的macOS端的操做(好比编辑)作准备。可是随着界面的滚动,UIView可能被释放掉。因此这里选择了用NSMapTable用来作存储容器。
存储的Key就是UIView的内存地址:
-(NSString*)_address{ return [NSString stringWithFormat:@"%p",self];}复制代码
存储的是UIView对象自己,当UIView由于离开屏幕而被释放的时候,使用内存地址取值为空,不会产生野指针。
map引用传递
稍微改造一下咱们的递归函数,在递归参数中增长用来记录的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};}复制代码
2.递归
对于UITableViewCell和UICollectionViewCell也是一样的操做,在获取IndexPath的时候,也只向上找一级,不然直接从递归的参数中获取,递归的初始值是默认的section=-1,row=-1。
3.Depth和level的处理
Depth代表的是当前View所在树的深度。Depth能够从当前的View直向上找superview,直到superView为空为止。可是这样的处理也会带来和UIViewController获取一样的问题。因此这个和UIViewController同样的处理策略,每次递归的时候,把当前的depth加1,向下传递。序列化后的view属性:
不管是Depth仍是level,抑或ViewController和IndexPath,它们都是从上级递归而来而且在当次递归中拼接本身的参数,因此咱们选择把这些属性封装在一个StepIn对象里,并抽象出一个stepin方法。每次递归开始,StepIn对象都会根据当前的view的状态,进行相应的stepin操做。具体的代码能够参考libyourview/serializer/UIView+YVTraversel
截图的处理
因为截图须要在JSON里传输,因此须要把截图的imgData转换成base64编码的string。截图是针对layer的操做,在截图的时候,必定不能带上sublayer。因此在针对每一个View进行截图的时候,须要把没有hidden的layer变成hidden状态,并保存在数组中,在截图方法调用完毕以后,须要把数组layer的hidden属性进行restore操做。
桌面端一共有三个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的场景编辑器里直观的感觉一下。左边是透视相机,右边是正交相机。
正交相机:orthographicCamera 所见即所得,全部View的scale不随深度变化;
透视相机:View近大远小。
为了更好的视觉效果,YourView选择使用正交相机用来展现View。
目前YourView只实现了3D渲染,对UIView的动态编辑能力还比较弱,后续会继续完善编辑功能;
在View树里增长UIViewController 手势,布局等元素;
UI美化工做和用户体验提高。
Apple SceneKit:https://developer.apple.com/scenekit/
Bonjour:https://developer.apple.com/bonjour/
接下来,咱们会进一步完善与优化YourView,为你们提供更好的使用体验,同时,也欢迎你们使用YourView(项目地址:https://github.com/TalkingData/YourView),也欢迎你们为咱们提供宝贵的建议和意见,让咱们一块儿维护这个项目~
做者:张自玉