《帶你領略 iOS 知識體系的全貌》這個系列分享自己在極客時間專欄《iOS 開發高手課》的學習心得,作者「戴銘」將專欄一共分成了四個板塊:1)基礎篇,2)應用篇,3)原理篇,4)原生與前端共舞篇。
我也按照作者的劃分整理自己的學習筆記,在這個階段自己偏向於吸收專欄的精華,往後階段再慢慢增加自己的輸出,以見證自己的成長。
今天要分享的是第一個板塊:基礎篇。
00 | 開篇#
2007 年喬布斯發布了第一代 iPhone—— 它重新定義了很多人對於手機的認知,同時也是移動互聯網時代的開端。
2008 年 7 月 WWDC 蘋果全球開發者大會上,蘋果宣布 App Store 正式對外開放 —— 這意味著屬於開發者的移動互聯網時代真正開始了。
相比於尋找移動端下一個熱點是什麼?不如靜下心來好好消化掉這幾年浪潮留下的關鍵技術,在此基礎上再去理解各種 “新技術”,必然會駕輕就熟。
作者戴銘熱愛分享,喜歡將平時學習和工作中的經驗分享到戴銘的博客上,也會將一些技術總結通過代碼發到戴銘的 GitHub上。
01 | 建立你自己的 iOS 開發知識體系#
對於 iOS 的知識體系,作者劃分為基礎、原理、應用開發、原生與前端四大板塊,並送上了一張思維導圖:
總的來說,不要看到啥就去學啥,而應該有目的、成體系地去學習,效果才會更好,進而可以從容地應對技術的更新迭代。
02 | App 啟動速度怎麼做優化與監控?#
App 啟動過程#
一般情況下,App 的啟動分為冷啟動和熱啟動。
- 冷啟動:一次完整的啟動過程。 App 點擊啟動前,它的進程不在系統裡,需要系統新創建一個進程分配給它啟動的情況。
- 熱啟動:表面上打開 App 的過程。在 App 的進程還在系統裡的情況下,用戶重新啟動進入 App 的情況。
用戶能感知到的啟動慢,都發生在主線程上。而 App 的冷啟動主要包括三個階段:
- main () 函數執行前;
- main () 函數執行後;
- 首屏渲染完成後。
main () 函數執行前#
主要過程:
- 加載可執行文件,即 App 的.o 文件的集合;
- 加載動態鏈接庫,進行 rebase 指針調整和 bind 符號綁定;
- Objc 運行時的初始處理,包括 Objc 相關類的註冊、category 註冊、selector 唯一性檢查等;
- 初始化,包括執行 +load () 方法、調用 attribute ((constructor)) 修飾的函數、創建 C++ 靜態全局變量。
優化點:
- 減少動態庫加載(數量上,蘋果公司建議最多使用 6 個非系統動態庫);
- 減少加載啟動後不會去使用的類或者方法;
- +load () 方法裡的內容可以放到首屏渲染完成後再執行,或使用 +initialize () 方法替換掉(在一個 +load () 方法裡,進行運行時方法替換操作會帶來 4 毫秒的消耗,積少成多,其對啟動速度的影響會越來越大);
- 控制 C++ 全局變量的數量。
main () 函數執行後#
主要過程:從 main () 函數執行開始,到 appDelegate 的 didFinishLaunchingWithOptions 方法裡首屏渲染相關方法執行完成。如:首頁的業務代碼都是在這個階段執行的,包括首屏初始化所需配置文件的讀寫、首屏列表大數據的讀取、首屏渲染的大量計算等操作。
優化思路:從功能上梳理出哪些是首屏渲染必要的初始化功能,哪些是 App 啟動必要的初始化功能,而哪些是只需要在對應功能開始使用時才需要初始化的。
首屏渲染完成後#
主要過程:didFinishLaunchingWithOptions 方法作用域內執行首屏渲染之後的所有方法執行完成。其主要進行非首屏其他業務服務模塊的初始化、監聽的註冊、配置文件的讀取等操作。
優化點:優先處理會卡住主線程的方法,否則會影響到用戶後面的交互操作。
明白了 App 啟動階段需要完成的工作後,接下來就有針對性地進行功能級別和方法級別的啟動優化了。
功能級別的啟動優化#
簡單來說,就是在 main () 函數開始執行後到首屏渲染完成前的階段,只處理首屏相關的業務,其他非首屏業務的初始化、監聽註冊、配置文件讀取等都放到首屏渲染完成後去做。
方法級別的啟動優化#
檢查首屏渲染完成前主線程上有哪些耗時方法,將沒必要的耗時方法滯後或者異步執行。
如何進行方法耗時的監控呢?主要有兩種手段:
1)定時抓取主線程上的方法調用堆棧,計算一段時間裡各個方法的耗時。Xcode 工具套件裡自帶的 Time Profiler ,採用的就是這種方式。其特點是精度不高,但也夠用。
2)對 objc_msgSend 方法進行 hook 來掌握所有方法的執行耗時。其特點是非常精確,但只能針對 Objective-C 的方法(對於 c 方法和 block,倒也可以使用 libffi 的 ffi_call 來達成 hook,但編寫維護相關工具的門檻較高)。
基於第 2 種方式監控耗時的完整代碼,見GCDFetchFeed(開源項目),其使用方法:
在需要檢測耗時的地方調用 [SMCallTrace start],結束時調用 stop 和 save 就可以打印出方法的調用層級和耗時了,還可以設置最大深度和最小耗時檢測,來過濾不需要看到的信息。
附objc_msgSend相關知識:
- 源碼見蘋果公司的開源網站;
- 它是 Objective-C 裡方法執行的必經之路,能夠控制所有的 Objective-C 方法,所以 hook 了它,就可以 hook 全部的 Objective-C 方法;
- 執行邏輯:先獲取對象對應類的信息,再獲取方法的緩存,根據方法的 selector 查找函數指針,經過異常錯誤處理後,最後跳到對應函數的實現。
- 用匯編語言寫的原因:1)它的調用頻次最高,在它上面進行的性能優化能夠提升整個 App 生命週期的性能,而匯編語言在性能優化上屬於原子級優化,能夠把優化做到極致;2)其他語言難以實現未知參數跳轉到任意函數指針的功能;
- 如何 hook 它,可以參考 Facebook 的開源庫fishhook—— 實現在 iOS 上運行的 Mach-O 二進制文件中動態地重新綁定符號。
參考資料:
匯編語言入門教程—— 阮一峰
03 | Auto Layout 簡介#
Cassowary是 Auto Layout 用到的佈局算法。
使用 Auto Layout 一定要注意多使用 Compression Resistance Priority 和 Hugging Priority,利用優先級的設置,讓佈局更加靈活,代碼更少,更易於維護,可以參考Auto Layout 相關的 Demo。
在前端出現了 Flexbox 這種高級的響應式佈局思路後,蘋果公司基於 Auto Layout 又封裝了一個類似 Flexbox 的 UIStackView,用來提高 iOS 開發響應式佈局的易用性。
PS:目前工程一般使用基於 Auto Layout 封裝的第三方庫,如Masonry—— 相關博客。
04 | 項目大了人員多了,架構怎麼設計更合理?#
目標:將業務完全解耦,將通用功能下沉,每個業務都是一個獨立的 Git 倉庫,每個業務都能夠生成一個 Pod 庫,最後再集成到一起。
簡單架構向大型項目架構演進中,就需要解決三個問題:
- 模塊粒度應該如何劃分?對於 iOS 這種面向對象編程的開發模式來說,我們首先應該遵循 SOLID 原則。
- 如何分層?建議不要超過三個:底層可以是與業務無關的基礎組件,比如網絡和存儲等;中間層一般是通用的業務組件,比如賬號、埋點、支付、購物車等;最上層是迭代業務組件,更新頻率最高。
- 多團隊如何協作?團隊分工要靈活,不把人員隔離固化,導致做的東西相互都不用;然後要圍繞著具體業務進行功能模塊提煉,去解決重複建設的問題,在這個基礎上把提煉出的模塊做精做扎實。
作者心目中好的架構:
組件間關係協調但沒有固定的標準,協調的優劣成為了衡量架構優劣的一個基本標準。
在實踐中,一般有兩種架構設計方案:
1)協議式:採用協議式編程思路,在編譯層面使用協議定義規範,實現可在不同地方,從而達到分布管理和維護組件的目的。這種方式也遵循了依賴反轉原則,是一種很好的面向對象編程的實踐。
2)中間者:採用中間者統一管理的方式,控制 App 整個生命週期中組件間的調用關係。
在考慮架構設計時,我們更多的還是需要在功能邏輯和組件劃分上做到同層級解耦,上下層依賴清晰,這樣的結構才能夠使得上層組件易插拔,下層組件更穩固。而中間者架構模式更容易維護這種結構,中間者的易管控和易擴展性,使得整體架構能夠長期保持穩健與活力。所以,中間者架構更是作者心目中好的架構。
案例分享:
ArchitectureDemo——Github,其在中間者架構的基礎上增加了對中間件、狀態機、觀察者、工廠模式的支持,此外,在使用上還支持鏈式調用。
參考資料:
iOS 應用架構談 開篇——Casatwy
05 | 連接器:符號是怎麼綁定到地址上的?#
帶著疑問學習:自己參與的項目裡,為什麼有的編譯起來很快,有的卻很慢;編譯完成後,有的啟動得很快,有的卻很慢?
這篇要講的連接器呀,它最主要的作用,就是將符號綁定到地址上。下面我們先從編譯器講起~
iOS 開發為什麼使用編譯器#
iOS 編寫的代碼是先使用編譯器把代碼編譯成機器碼,然後直接在 CPU 上執行。
之所以不使用解釋器來運行代碼,是因為蘋果公司希望程序的執行效率更高、運行速度更快。
相反,解釋器可以在運行時執行代碼,該過程具有動態性,程序運行後能夠通過更新代碼隨時改變程序的邏輯。
現在蘋果公司使用的編譯器是 LLVM,相比於 Xcode 5 版本前使用的 GCC,編譯速度提高了 3 倍。同時,蘋果公司也反過來主導了 LLVM 的發展,讓 LLVM 可以針對蘋果公司的硬件進行更多的優化。
LLVM 本質上是編譯器工具鏈技術的一個集合:編譯器會對每個文件進行編譯,生成 Mach-O(可執行文件);連接器(LLVM 中的 lld 項目)會將項目中的多個 Mach-O 文件合併成一個。
編譯過程:
- 首先,你寫好代碼後,LLVM 會預處理你的代碼,比如把宏嵌入到對應的位置。
- 然後,LLVM 會對代碼進行詞法分析和語法分析,生成 AST(抽象語法樹,結構上比代碼更精簡,遍歷起來更快)。
- 最後, AST 會生成 IR(中間表示),它是一種更接近機器碼的語言,區別在於和平台無關,通過 IR 可以生成多份適合不同平台的機器碼。對於 iOS 系統,IR 生成的可執行文件就是 Mach-O。
編譯時連接器做了什麼?#
連接器的作用,就是完成變量、函數名和其地址綁定這樣的任務,篇首提到的符號,就可以理解為變量名和函數名。
並且,連接器在整理函數的符號調用關係時,會理清有哪些函數是沒被調用的,並自動去除掉。該過程中,連接器會以 main 函數為源頭,跟隨每個引用,並將其標記為 live。跟隨完成後,那些未被標記 live 的函數,就是無用函數。然後,連接器可以通過打開 Dead code stripping 開關,來開啟自動去除無用代碼的功能(這個開關是默認開啟的)。
動態庫連接#
連接的共用庫分為靜態庫和動態庫:
- 靜態庫是編譯時連接的庫,需要連接進你的 Mach-O 文件裡,如果需要更新就要重新編譯一次,無法動態加載和更新;
- 動態庫是運行時連接的庫,使用 dyld 就可以實現動態加載。
使用 dyld 加載動態庫,有兩種方式:1)有程序啟動加載時綁定,2)符號第一次被用到時綁定。為了減少啟動時間,大部分動態庫使用的都是採用第 2 種方式。
常用工具:nm 工具 - 查看符號表、otool 工具 - 找符號所需庫。
希望通過這個系列《帶你領略 iOS 知識體系的全貌》,後續的深度挖掘就靠自己了,可不要淺嘗輒止哦~
投票:春節假期你主要做什麼呢?
A. 走訪親戚
B. 旅遊
C. 組局
D. 看劇
E. 閱讀
F. 敲代碼
G. 其它