在這篇文章中,我會帶大家利用 Xcode Debugger,對 Apple 的 Maps iOS App 進行逆向工程 (reverse engineer),看看它是如何構建而成的!讀完這篇文章之後,你會學到:
- 如何為視圖階層 (view hierarchy) 除錯 (debug),以獲取當前的 UI 狀態和佈局 (layout) 信息。
- 如何為 Memory Graph 除錯,以獲取物件的階層,並了解實作特定 API 的步驟。
廢話少說,讓我們開始吧!
開始吧
在為 App 除錯之前,我們先要在 Mac 禁用系統完整保護 (System Integrity Protection)。簡單來說,這個設定在電腦上預設為啟用,以阻止我們在 Xcode 中為自己以外的 iOS App 進行除錯。
- 在修復模式 (Recovery Mode) 下重新啟動 Mac:Mac 開機後立即按下 Command (⌘) + R,當螢幕上出現 Apple 標誌時鬆開按鍵。
- 然後,選擇 “Utility”,然後打開 Terminal。
- 輸入
csrutil disable;reboot
,如此一來,就可以禁用系統完整保護,並重新啟動 Mac。
現在,我們已經準備好在 Xcode 對 Apple Maps App 進行逆向工程了。
備註:讀完整篇教學文章之後,記得要以修復模式重新啟動 Mac,然後在 Terminal 執行 csrutil enable; reboot
,來重新啟用系統完整保護,並重新啟動 Mac。
首先,打開 Xcode,建立一個新專案或是打開一個現有專案:
![reverse-engineer](https://appcoda.com.tw/wp-content/uploads/2021/11/Reverse-Engineer-1.png)
在 Xcode 模擬器或裝置上,打開 Apple Maps App:
![](https://appcoda.com.tw/wp-content/uploads/2021/11/Reverse-Engineer-2.png)
接著,到 “Debug” tab 並選擇 Attach to Process by PID or Name
:
![](https://appcoda.com.tw/wp-content/uploads/2021/11/Reverse-Engineer-3.png)
讓我們搜尋 “Maps” process 並點擊 “Attach”:
![](https://appcoda.com.tw/wp-content/uploads/2021/11/Reverse-Engineer-4.png)
進度會從 “Attaching to Maps …” 變成 “Running Maps …”,這代表除錯器已經連接到 App 了。現在,讓我們看看下一個步驟吧。
為視圖階層除錯
打開了 Maps 又連接好除錯器後,點擊 Debug View Hierarchy
按鈕來顯示 UI:
![](https://appcoda.com.tw/wp-content/uploads/2021/11/Reverse-Engineer-5.png)
![](https://appcoda.com.tw/wp-content/uploads/2021/11/Reverse-Engineer-6.png)
我們來了解 Bottom Sheet 的架構,以及 UI 和 Behavior 是如何實作的:
![](https://appcoda.com.tw/wp-content/uploads/2021/11/Reverse-Engineer-7.gif)
讓我們揭示 UI 階層,並展開一些視圖:
![](https://appcoda.com.tw/wp-content/uploads/2021/11/Reverse-Engineer-8.png)
在這裡,我們可以看到 Apple 用了容器 (container) 和子視圖控制器 (child view controller),來把一個螢幕分成更小、而且可重用的部分。
而且,我們可以像其他自己的 App 一樣,偵測自動佈局 (Auto Layout) 的警告:
![](https://appcoda.com.tw/wp-content/uploads/2021/11/Reverse-Engineer-9.png)
更重要的是,當我們在專案的程式碼中搜尋需要的類別時,可以找到每個視圖或視圖控制器的名字,這一點非常有用:
![](https://appcoda.com.tw/wp-content/uploads/2021/11/Reverse-Engineer-10.png)
接著,讓我們來看看 Memory Graph,以了解 Apple 用了什麼 API 來建立前文所說的 Bottom Sheet Behavior。
為 Memory Graph 除錯
首先,點擊 Debug Memory Graph
按鈕:
![](https://appcoda.com.tw/wp-content/uploads/2021/11/Reverse-Engineer-11.png)
然後,我們會看到當前存在的物件:
![](https://appcoda.com.tw/wp-content/uploads/2021/11/Reverse-Engineer-12.png)
我們可以從視圖階層中,看到 Bottom Sheet 視圖控制器的名稱是 SearchViewController
。讓我們在 Debug Navigator 中搜尋它:
![](https://appcoda.com.tw/wp-content/uploads/2021/11/Reverse-Engineer-13.png)
點擊 SearchViewController
,就會在右邊看到 Dependency Graph:
![](https://appcoda.com.tw/wp-content/uploads/2021/11/Reverse-Engineer-14.png)
如此一來,我們會知道有一個名為 UIFormSheetPresentationController
的類別,它負責顯示 Botton Sheet。點擊這個類別來顯示一個 Memory inspector
,我們就會看到詳細的階層。
![](https://appcoda.com.tw/wp-content/uploads/2021/11/Reverse-Engineer-15.png)
如你所見,負責 Bottom Sheet 的物件是 UISheetPresentationController。這是從 iOS 15.0 和 Swift 5.5 新增的 API。我們查閱文檔,就可以發現它使用 detents
陣列來控制 Bottom Sheet 尺寸的設定。
![](https://appcoda.com.tw/wp-content/uploads/2021/11/Reverse-Engineer-16.png)
我們也會發現,現在只有兩個可用的 detents
,就是 .large()
和 .medium()
:
![](https://appcoda.com.tw/wp-content/uploads/2021/11/Reverse-Engineer-17.png)
然而,Apple 的 Maps App 在不同位置明明有 .medium()
和 .large()
以外的 Bottom Sheet,例如以下這個截圖,Bottom Sheet 可以顯示在螢幕上更低的位置:
![](https://appcoda.com.tw/wp-content/uploads/2021/11/Reverse-Engineer-18.png)
為什麼會這樣呢?讓我們來拆解原因吧!在 Debug Navigator 中搜尋 “Detent” 關鍵字,會找到 detents
陣列:
![](https://appcoda.com.tw/wp-content/uploads/2021/11/Reverse-Engineer-19.png)
如此印出陣列的 Description:
![](https://appcoda.com.tw/wp-content/uploads/2021/11/Reverse-Engineer-20.png)
我們會看到 Apple 用了 3 個 .custom
detent:
![](https://appcoda.com.tw/wp-content/uploads/2021/11/Reverse-Engineer-21.png)
我們就會發現 UISheetPresentationController
的部分是 Private 的,而且開發者尚未可用這個控制器。不過,相信這個 API 很快會更新,讓我可以建立客製化的 Detent。
我們已經成功透過視圖階層來獲取 UI 信息,以及透過 Memory Graph 了解物件的關係。另外,我們也可以在任何 App 上使用 Instruments
,來監察網絡請求 (network request)、偵測記憶體洩漏 (memory leak) 等等。
![](https://appcoda.com.tw/wp-content/uploads/2021/11/Reverse-Engineer-22.png)
總結
如果你想在 iOS 15 建立 Bottom Sheet,可以參考我這篇關於 UISheetPresentationController 的文章。謝謝你的閱讀。