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),也歡迎你們爲咱們提供寶貴的建議和意見,讓咱們一塊兒維護這個項目~
做者:張自玉