cocoapods-hmap-prebuilt 是美團平臺迭代組自研的一款 cocoapods 插件,以 Header Map 技術 爲基礎,進一步提高代碼的編譯速度,完善頭文件的搜索機制。html
雖然以二進制組件的方式構建 App 是 HPX (美團移動端統一持續集成/交付平臺)的主流解決方案,但在某些場景下(Profile、Address/Thread/UB/Coverage Sanitizer、App 級別靜態檢查、ObjC 方法調用兼容性檢查等等),咱們的構建工做仍是須要以全源碼編譯的方式進行;並且在實際開發過程當中,大可能是以源碼的方式進行開發,因此咱們將實驗對象設置爲基於全源碼編譯的流程。java
廢話很少說,咱們來看看它的實際使用效果!git
總的來講,以美團和大衆點評的全源碼編譯流程爲實驗對象的前提下,cocoapods-hmap-prebuilt 插件能將總鏈路提高 45% 以上的速度,在 Xcode 打包環節上能提高 50% 以上的速度,是否是有點動心了?github
爲了更好的理解這個插件的價值和功能,咱們不妨先看一下當前的工程中存在的問題。算法
目前,美團內的 App 都是基於 CocoaPods 作包管理方面的工做,因此在實際的開發過程當中,CocoaPods 會在 Pods/Header/
目錄下添加組件名目錄和頭文件軟鏈,相似於下面的形式:xcode
/Users/sketchk/Desktop/MyApp/Pods └── Headers ├── Private │ └── AFNetworking │ ├── AFHTTPRequestOperation.h -> ./XXX/AFHTTPRequestOperation.h │ ├── AFHTTPRequestOperationManager.h -> ./XXX/AFHTTPRequestOperationManager.h │ ├── ... │ └── UIRefreshControl+AFNetworking.h -> ./XXX/UIRefreshControl+AFNetworking.h └── Public └── AFNetworking ├── AFHTTPRequestOperation.h -> ./XXX/AFHTTPRequestOperation.h ├── AFHTTPRequestOperationManager.h -> ./XXX/AFHTTPRequestOperationManager.h ├── ... └── UIRefreshControl+AFNetworking.h -> ./XXX/UIRefreshControl+AFNetworking.h
也正是經過這樣的目錄結構和軟鏈,CocoaPods 得以在 Header Search Path 中添加以下的參數,使得預編譯環節順利進行。ruby
$(inherited) ${PODS_ROOT}/Headers/Private ${PODS_ROOT}/Headers/Private/AFNetworking ${PODS_ROOT}/Headers/Public ${PODS_ROOT}/Headers/Public/AFNetworking
雖然這種構建 Search Path 的方式解決了預編譯的問題,但在某些項目中,例如多達 400+ 組件的巨型項目中,會形成如下幾點問題:微信
-I
選項極速膨脹,在達到必定長度後,甚至會形成沒法編譯的狀況${PODS_ROOT}/Headers/Private
路徑的存在,讓引用其餘組件的私有頭文件變爲了可能。想解決上述的問題,好一點的狀況下,可能會浪費 1 個小時,而很差的狀況,就是讓有風險的代碼上線了,你說工程師會不會所以而感到頭疼?app
還好 cocoapods-hmap-prebuilt 的出現,讓這些問題變成了歷史,不過要想理解它爲何能解決這些問題,咱們得先理解一下什麼是 Header Map。 ide
Header Map 實際上是一組頭文件信息映射表!
爲了更直觀的理解 Header Map,咱們能夠在 Build Setting 中開啓 Use Header Map 選項,真實的體驗一下它。
而後在 Build Log 裏獲取相應組件裏對應文件的編譯命令,並在最後加上 -v
參數,來查看其運行的祕密:
$ clang <list of arguments> -c some-file.m -o some-file.o -v
在 console 的輸出內容中,咱們會發現一段有意思的內容:
經過上面的圖,咱們能夠看到編譯器將尋找頭文件的順序和對應路徑展現出來了,而在這些路徑中,咱們看到了一些陌生的東西,即後綴名爲 .hmap
的文件,後面還有個括號寫着 headermap。
沒錯!它就是 Header Map 的實體。
此時 Clang 已經在剛纔提到的 hmap 文件裏塞入了一份頭文件名和頭文件路徑的映射表,不過它是一種二進制格式的文件,爲了驗證這個的說法,咱們能夠經過 milend 編寫的hmap 工具來查其內容。
在執行相關命令(即 hmap print
)後,咱們能夠發現這些 hmap 裏保存的信息結構大體以下, 相似於一個 Key-Value 的形式,Key 值是頭文件的名稱,Value 是頭文件的實際物理路徑:
須要注意,映射表的鍵值內容會隨着使用場景產生不一樣的變化,例如頭文件引用是在 "..."
的形式下,仍是 <...>
的形式下,又或是在 Build Phase 裏 Header 的配置狀況。例如,你將頭文件設置爲 Public 的時候,在某些 hmap 中,它的 Key 值就爲 PodA/ClassA
,而將其設置爲 project 的時候,它的 Key 值可能就是 ClassA
,而配置這些信息的地方,以下圖所示:
至此我想你應該瞭解到 Header Map 究竟是個什麼東西了。
固然這種技術也不是一個什麼新鮮事兒,在 Facebook 的 buck 工具中也提供了相似的東西,只不過文件類型變成了 HeaderMap.java
的樣子。
此時,我估計你可能並不會對 buck 產生太多的興趣,而是開始思考上一張圖中 Headers 的 Public、Private、Project 到底表明着什麼意思,好像不少同窗歷來沒怎麼關注過,以及爲何它會影響 hmap 裏的內容?
在 Apple 官方的 Xcode Help - What are build phases? 文檔中,咱們能夠看到以下的一段解釋:
Associates public, private, or project header files with the target. Public and private headers define API intended for use by other clients, and are copied into a product for installation. For example, public and private headers in a framework target are copied into Headers and PrivateHeaders subfolders within a product. Project headers define API used and built by a target, but not copied into a product. This phase can be used once per target.
總的來講,咱們能夠知道一點,就是 Build Phases - Headers 中提到 Public 和 Private 是指能夠供外界使用的頭文件,而 Project 中的頭文件是不對外使用的,也不會放在最終的產物中。
若是你繼續翻閱一些資料,例如 StackOverflow - Xcode: Copy Headers: Public vs. Private vs. Project? 和 StackOverflow - Understanding Xcode's Copy Headers phase,你會發如今早期 Xcode Help 的 Project Editor 章節裏,有一段名爲 Setting the Role of a Header File 的段落,裏面詳細記載了三個類型的區別。
Public: The interface is finalized and meant to be used by your product’s clients. A public header is included in the product as readable source code without restriction.
Private: The interface isn’t intended for your clients or it’s in early stages of development. A private header is included in the product, but it’s marked 「private」. Thus the symbols are visible to all clients, but clients should understand that they're not supposed to use them.
Project: The interface is for use only by implementation files in the current project. A project header is not included in the target, except in object code. The symbols are not visible to clients at all, only to you.
至此,咱們應該可以完全瞭解了 Public、Private、Project 的區別。簡而言之,Public 仍是一般意義上的 Public,Private 則表明 In Progress 的含義,至於 Project 纔是一般意義上的 Private 含義。
此時,你會不會聯想到 CocoaPods 中 Podspec 的 Syntax 裏還有 public_header_files
和 private_header_files
兩個字段,它們的真實含義是否和 Xcode 裏的概念衝突呢?
這裏咱們仔細閱讀一下官方文檔的解釋,尤爲是 private_header_files
字段。
咱們能夠看到,private_header_files
在這裏的含義是說,它自己是相對於 Public 而言的,這些頭文件本義是不但願暴露給用戶使用的,並且也不會產生相關文檔,可是在構建的時候,會出如今最終產物中,只有既沒有被 Public 和 Private 標註的頭文件,纔會被認爲是真正的私有頭文件,且不出如今最終的產物裏。
看起來,CocoaPods 對於 Public 和 Private 的官方解釋是和 Xcode 中的描述一致的,兩處的 Private 並不是咱們一般理解的 Private,它的本意更應該是開發者準備對外開放,但又沒徹底 Ready 的頭文件,更像一個 In Progress 的含義。
這一塊是否是讓你有點大跌眼鏡?那麼,在現實世界中,咱們是否正確的使用了它們呢?
前面咱們介紹了 hmap 是什麼,以及怎麼開啓它(啓用 Build Setting 中的 Use Header Map 選項),也介紹了一些影響生成 hmap 的因素(Public、Private、Project)。
那是否是我只要開啓 Xcode 提供的 Use Header Map 就能夠提高編譯速度了呢?
很惋惜,答案是否認的!
至於緣由,咱們就從下面的例子開始提及,假設咱們有一個基於 CocoaPods 構建的全源碼工程項目,它的總體結構以下:
整個結構看起來以下所示:
當構建的產物類型爲 Static Library 的時候,CocoaPods 在建立頭文件產物過程當中,它的邏輯大體以下:
public_header_files
和 private_header_files
,相應的頭文件都會被設置爲 Project 類型。Pods/Headers/Public
中會保存全部被聲明爲 public_header_files
的頭文件。Pods/Headers/Private
中會保存全部頭文件,不管是 public_header_files
或者 private_header_files
描述到,仍是那些未被描述的,這個目錄下是當前組件的全部頭文件全集。Pods/Headers/Public
和 Pods/Headers/Private
的內容同樣且會包含全部頭文件。正是因爲這種機制,會致使一些有意思的問題發生。
Pods/Headers/Private
路徑的存在,咱們徹底能夠引用到其餘組件裏的私有頭文件,例如我只要使用 #import <SomePod/Private_Header.h>
的方式,就會命中私有文件的匹配路徑。#import "ClassA.h"
的鍵值引用,也就是說只有 #import "ClassA.h"
的方式纔會命中 hmap 的策略,不然都將經過 Header Search Path 尋找其相關路徑,例以下圖中的 PodB,在其 build 的過程當中,Xcode 會爲 PodB 生成 5 個 hmap 文件,也就是說這 5 個文件只會在編譯 PodB 中使用,其中 PodB 會依賴 PodA 的一些頭文件,但因爲 PodA 中的頭文件都是 Project 類型的,因此其在 hmap 裏的 Key 所有爲 ClassA.h
,也就是說咱們只能以 #import "ClassA.h"
的方式引入。
而咱們也知道,在引用其餘組件的時候,一般都會採用 #import <A/A.h> 的方式引入。至於爲何會用這種方式,一方面是這種寫法會明確頭文件的由來,避免問題,另外一方面也是這種方式可讓咱們在是否開啓 clang module 中隨意切換。固然,還有一點就是Apple 在 WWDC 裏曾經不止一次建議開發者使用這種方式來引入頭文件。
接着上面的話題來講,因此說在 Static Library 的狀況下且以 #import <A/A.h>
這種標準方式引入頭文件時,開啓 Use Header Map 選項並不會幫咱們提高編譯速度。
但真的就沒有辦法使用 Header Map 了麼?
固然,老是有辦法解決的,咱們徹底能夠本身動手作一個基於 CocoaPods 規則下的 hmap 文件,正是基於這個想法,美團自研的 cocoapods-hmap-prebuilt 插件誕生了!
它的核心功能並很少,大概有如下幾點:
組件名/頭文件名
方式的 Key-Value,排查重名頭文件帶來的異常行爲。聽起來可能有點繞,內容也有點多,不過這些你都不用關心,你只須要經過如下 2 個步驟就能將其使用起來:
// this is part of Gemfile source 'http://sakgems.sankuai.com/' do gem 'cocoapods-hmap-prebuilt' gem 'XXX' ... end // this is part of Podfile target 'XXX' do plugin 'cocoapods-hmap-prebuilt' pod 'XXX' ... end
除此以外,爲了拓展其實用性,咱們還提供了頭文件補丁(解決重名頭文件的定向選取)和環境變量注入(無侵入的在其餘系統中使用)的能力,便於其在不一樣場景下的使用。
至此,關於 cocoapods-hmap-prebuilt 的介紹就要結束了。
回看整個故事的開始,Header Map 是我在研究 Swift 和 Objective-C 混編過程當中發現的一個很小的知識點,並且 Xcode 自身就實現了一套基於 Header Map 的功能,在實際的使用過程當中,它的表現並不理想。
但幸運的是,在後續的探索的過程當中,咱們發現了爲何 Xcode 的 Header Map 沒有生效,以及爲何它與 CocoaPods 出現了不兼容的狀況,雖然它的原理並不複雜,核心點就是將文件查找和讀取等 IO 操做編變成了內存讀取操做,但結合實際的業務場景,咱們發現它的收益是十分可觀的。
或許這是在提醒咱們,要永遠對技術保持一顆好奇的心!
其實,利用 Clang Module 技術也能夠解決本文一開始提到的幾個問題,但它並不在這篇文章的討論範圍中,若是你對 Clang Module 或者對 Swift 與 Objective-C 混編感興趣,歡迎閱讀參考文檔中的 《從預編譯的角度理解 Swift 與 Objective-C 及混編機制》一文,以瞭解更多的詳細信息。
| 想閱讀更多技術文章,請關注美團技術團隊(meituantech)官方微信公衆號。
| 在公衆號菜單欄回覆【2020年貨】、【2019年貨】、【2018年貨】、【2017年貨】、【算法】等關鍵詞,可查看美團技術團隊歷年技術文章合集。