上篇提到開始了新的工作,這週來稍微聊聊這兩週工作上的一些新奇發現。
沒錯,又是金融業(怎麼台灣好多金融相關的 iOS 工作??)。
上班首日
到職第一天,在 Mac 上建置好開發環境後,主管給我的第一個任務就是熟悉程式碼和文件。他遞給我一本 Uncle Bob 的《Clean Architecture》(聽說是客戶送的),並且知道我是有經驗的開發者,問我能不能也看看專案的 build time。
於是,我從 Git clone 了專案,打開 Xcode workspace,按下 Command+B 開始建置。
然後我等啊等,又在等啊等啊等...
整整八分鐘後,終於完成了。我真是驚呆了。
這意味著同事們把一天中相當長的時間都花在等待 building 上。我的天啊!難怪會叫我看一下!
我打算開始好好分析這個專案。
調查過程
這是一個 legacy Objective-C App,有著:
- 超過 5,000 個檔案
- 大約 5% 的 Swift 程式碼
- 用 CocoaPods 管理套件,不是 SPM
- 專案的 build time 為 8 分鐘起跳
- 產出 SDK(.a 檔案)需要超過 30 分鐘
- 用完所有可用的 Application memory 是家常便飯
我的 Debug 之旅經歷了幾個階段:
1:分析工具
首先,我找了幾個以前用過的工具,嘗試看看能不能有什麼發現:
- BuildTimeAnalyzer-for-Xcode
- FengNiao(用於未使用的資源)
- Periphery(用於未使用的程式碼)
- fui(用於未使用的 imports)
- cloc(用於計算程式碼行數)
不幸的是,這些工具都沒有指出長時間建置的明確元兇。
2:清理 低使用率/低依賴/老舊 的第三方套件
我的下一個想法是移除由 CocoaPods 管理的舊的和不常使用的第三方相依性。然而,這需要大量的重構和廣泛的驗證,使其成為一個高風險、耗時的任務。
3:使用 SPM 現代化
我也考慮將專案遷移到 Swift Package Manager (SPM) 並將其拆分為更小的模組化 libraries。這種方法遇到了兩個主要障礙:
- 程式碼是 Swift 和 Objective-C 的混合,但 SPM 在單一 target 內只能使用單一語言,使得轉換變得複雜
- 我也嘗試製作了一個暫時性的 Package 來測試,但在將它整合到專案,打包成SDK之後,發生了
duplicate symbol
的 error。搜尋了一下解決辦法,看起來這是靜態 library 一直以來跟 SPM 整合的問題,存在 Xcode 中已數年時間都未修復,暫時也只先放棄。
4:深入研究 Build Script
由於最初的嘗試都沒有結果,我轉向研究 SDK build script。
當我嘗試切換到 aggregate target 並 Clean build 來匯出 SDK 時,失敗了。
我很困惑。我問其他同事,他們告訴我必須分別先切換到「Any iOS Device」和「Any iOS Simulator」build library,然後才能 build aggregate target。我簡直不敢相信,這是正常的嗎?
開始深入研究 build log errors,我看到與 CocoaPods 相關的「modulemap not found」錯誤。讓我回頭檢查了 build script 本身,我發現了罪魁禍首:script 使用 xcodebuild
搭配 -target
參數,直接指向 .xcodeproj
檔案,儘管專案是用 .xcworkspace
設置的。這就是為什麼 clean build之後會失敗,以及為什麼其他人需要先各 build 一次模擬器和實機才可以。(這樣加上 script 總共會 build 4 次啊!!!!!)
我修正了 script,改用 -workspace
和 -scheme
參數。立即地,aggregate target clean build 成功了。這個改動將流程簡化為只需按下一次 Command + B,將 SDK 匯出時間縮短了一半!
新的謎團:Runtime Crash
但接下來又遇到了一個新問題:我的新 script 產生的 SDK,會造成 runtime crash,而那個舊的、慢的 script 產生的卻不會。
我又困惑了,我 revert 了 script 的變更,用原本的方式產出 SDK。經過又一次漫長的等待,終於完成了。「現在應該不會 crash 了吧?我把所有東西都 revert 了耶?」,我天真的想。
但沒錯,不出意外就是得出意外,它在 runtime 時仍然會 crash。然而,當我測試最一開始就在專案裡的原始 SDK 檔案時,它看起來完全沒問題。謎團加深了。
我嘗試問同事們都如何產生 SDK。他們跟我說了 Jenkins server 的位置,通常都會用 Jenkins 包版。
我取得了存取權限,並直接在 Jenkins 機器上執行我新的 script。.....很好,產生的 SDK 工作得很完美——沒有 crash。
答案一定在 Jenkins logs 中。我嘗試打開 job history,但網頁一直在載入,載入到 Safari 當機。我嘗試 reload page,出現了 log 的介面,但是在一半的地方就斷掉了。在有顯示的部分中,我發現了關鍵線索:Jenkins server 執行的是 Xcode 14.3,而我本機是 Xcode 16.3。
經過幾次測試,我確認了:Xcode 版本就是原因。新版本的 Xcode 對 Objective-C 有更嚴格的行為,這導致了 crash。專案的 code 需要更新來符合規範。我做了一點點小修正,嘗試用新的 script 產出 SDK。成功啦!不再有 runtime crash!!!!!
到這邊為止,已經是我新工作的第四天結束,主要是因為 build time 太長太痛苦。每做一個小 change,都需要長時間等待才能驗證是否有效。
最終突破
即便 SDK script 修好了,專案的 8 分鐘 build time 仍然是個大問題。
我回到那個 Jenkins log。由於 Safari 無法完整顯示它,我決定試試看下載下來。「只是一個 log 文字檔案,應該幾秒鐘就好了」,我是這樣想的。但我等了... 又等了。將近六分鐘後,下載終於完成了。
我打開下載資料夾中,找到下載的檔案。**它的大小是 5.63 GB。蛤!?**一個 5.63 GB 的 build log!?
這一看就不正常。我問了 Claude
:「這個 Jenkins build log 有 5.63 GB,這會是造成 build time 太久的可能原因嗎?」它給了我肯定的答案。
接著我問它減少 log 輸出的 build settings,它給了我幾個設定的選項嘗試。
我修改了專案設定,按下 Command + B 然後觀察。結果!僅僅 63 秒就完成了!
我找了其他同事,在他們的電腦上也修改了這些設定,同樣達成了這令人難以置信的改善。我們找到了暫時將 build time 減少超過 90% 的方法,提高了全體團隊成員的工作效率,而這一切,都發生在我上工的第一個禮拜。
暫時解決方案與長期目標
有些人應該會說:「啊你沒有修正 warnings,你只是隱藏了它們而已啊。」這樣說是沒錯。
然而,當一個專案有超過 2,500 個被忽略多年的 warnings 時,warnings 本身就成了問題,就算出現了任何新的、重要的 warning,也沒有人會發現。
在我自己的 side projects 中,我嚴重的潔癖讓我能保持 0 warning policy,因為我知道它們有多重要。如果你持續忽略它們,有一天你的專案,必然會變成怪獸反過來吃掉你。
這就是為什麼我稱這為暫時解決方案。為了立即提升整個團隊的生產力,這是最有效的第一步,況且我也沒說我們決定就這樣一直隱藏他們下去。
因此,長期目標是系統性地檢視 legacy 程式碼,修正根本問題,讓 app 更穩定、提高效率。我希望能和團隊夥伴一起開始逐一消除這些 warnings。即使一天只修正一個 warning,也是很棒的進步。
你也有類似的經驗嗎,或者對這種情況有更好的解決方案嗎?留言讓我知道!
那本週分享先到這邊,下次見!