Bo2SS

Bo2SS

1 基礎篇(上)

《帶你領略 iOS 知識體系的全貌》這個系列分享自己在極客時間專欄《iOS 開發高手課》的學習心得,作者「戴銘」將專欄一共分成了四個板塊:1)基礎篇,2)應用篇,3)原理篇,4)原生與前端共舞篇。

我也按照作者的劃分整理自己的學習筆記,在這個階段自己偏向於吸收專欄的精華,往後階段再慢慢增加自己的輸出,以見證自己的成長。

今天要分享的是第一個板塊:基礎篇

image

00 | 開篇#

2007 年喬布斯發布了第一代 iPhone—— 它重新定義了很多人對於手機的認知,同時也是移動互聯網時代的開端。

2008 年 7 月 WWDC 蘋果全球開發者大會上,蘋果宣布 App Store 正式對外開放 —— 這意味著屬於開發者的移動互聯網時代真正開始了。

相比於尋找移動端下一個熱點是什麼?不如靜下心來好好消化掉這幾年浪潮留下的關鍵技術,在此基礎上再去理解各種 “新技術”,必然會駕輕就熟。

作者戴銘熱愛分享,喜歡將平時學習和工作中的經驗分享到戴銘的博客上,也會將一些技術總結通過代碼發到戴銘的 GitHub上。

01 | 建立你自己的 iOS 開發知識體系#

對於 iOS 的知識體系,作者劃分為基礎、原理、應用開發、原生與前端四大板塊,並送上了一張思維導圖:

image

總的來說,不要看到啥就去學啥,而應該有目的、成體系地去學習,效果才會更好,進而可以從容地應對技術的更新迭代。

02 | App 啟動速度怎麼做優化與監控?#

App 啟動過程#

一般情況下,App 的啟動分為冷啟動和熱啟動。

  • 冷啟動:一次完整的啟動過程。 App 點擊啟動前,它的進程不在系統裡,需要系統新創建一個進程分配給它啟動的情況。
  • 熱啟動:表面上打開 App 的過程。在 App 的進程還在系統裡的情況下,用戶重新啟動進入 App 的情況。

用戶能感知到的啟動慢,都發生在主線程上。而 App 的冷啟動主要包括三個階段:

  1. main () 函數執行前;
  2. main () 函數執行後;
  3. 首屏渲染完成後。

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 庫,最後再集成到一起。

簡單架構向大型項目架構演進中,就需要解決三個問題:

  1. 模塊粒度應該如何劃分?對於 iOS 這種面向對象編程的開發模式來說,我們首先應該遵循 SOLID 原則。
  2. 如何分層?建議不要超過三個:底層可以是與業務無關的基礎組件,比如網絡和存儲等;中間層一般是通用的業務組件,比如賬號、埋點、支付、購物車等;最上層是迭代業務組件,更新頻率最高。
  3. 多團隊如何協作?團隊分工要靈活,不把人員隔離固化,導致做的東西相互都不用;然後要圍繞著具體業務進行功能模塊提煉,去解決重複建設的問題,在這個基礎上把提煉出的模塊做精做扎實。

作者心目中好的架構

組件間關係協調但沒有固定的標準,協調的優劣成為了衡量架構優劣的一個基本標準。

在實踐中,一般有兩種架構設計方案:

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 文件合併成一個。

編譯過程

  1. 首先,你寫好代碼後,LLVM 會預處理你的代碼,比如把宏嵌入到對應的位置。
  2. 然後,LLVM 會對代碼進行詞法分析和語法分析,生成 AST(抽象語法樹,結構上比代碼更精簡,遍歷起來更快)。
  3. 最後, 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. 其它

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。