背景

即刻是國內較早全面擁抱Swift的iOS開發團隊,目前即刻100%的業務程式碼(第三方庫依賴除外)都透過Swift實現。隨著業務的發展,即刻做了多次架構的拆分,專案按模組劃分成多個target,依賴的第三方庫也日漸增多。

在Xcode 9以前,由於官方不支援Swift靜態庫,開發團隊不得不將專案的依賴打包成動態庫。然而,隨著專案裡動態庫越來越多,app的啟動速度不可避免地受到影響(參考Optimizing App Startup Time)。

終於在Xcode 9時代,swift帶來對靜態庫的原生支援。即刻一直以來都關注著swift靜態庫這個feature,因此在Xcode 9到來的時候,我們就決定將專案改造成靜態庫打包的方式。

遇到的問題和方法

靜態庫是Swift社群比較關注的點,因此在Xcode 9 出來的同時,即刻便開始著手支援。

在手動簡單嘗試(改target編譯項)後,我們決定自己來嘗試做這件事情,原因是一來對比測試後,發現靜態庫打包對我們當前專案有所最佳化,二是改動並不大,做完資源管理後,程式碼改動不大。

我們瞭解到cocoapod 也在著手支援,但在我們開始實踐時 cocoapods的支援還存在些問題,而且我們所做的改造即使未來切換為cocoapod的方式也是必要的。於是我們決定先行,並將手動改造指令碼化,使用Xcodeproj 來替代手動改造

main_project

=

Xcodeproj

::

Project

open

‘Ruguo。xcodeproj’

main_target

=

main_project

targets

first

# add complie flag

# 1。 靜態庫連結對於無引用的程式碼會最佳化掉,為binary增加 -all_load,載入全部,防止部分OC符號找不到

main_target

build_configurations

each

do

|

config

|

config

build_settings

‘OTHER_LDFLAGS’

+=

‘-all_load’

unless

config

build_settings

‘OTHER_LDFLAGS’

]。

include?

‘-all_load’

end

# main target build phases

# main target 的不同階段

main_embed_frameworks_phase

=

main_target

build_phases

select

{

|

phase

|

phase

respond_to?

:name

&&

phase

name

==

“Embed Frameworks”

}

first

main_copy_resources_phase

=

main_target

build_phases

select

{

|

phase

|

phase

kind_of?

Xcodeproj

::

Project

::

Object

::

PBXResourcesBuildPhase

}

first

main_linkPhase

=

main_target

build_phases

select

{

|

phase

|

phase

kind_of?

Xcodeproj

::

Project

::

Object

::

PBXFrameworksBuildPhase

}

first

excludeTargets

=

‘主project,有些target,比如拓展程式,不轉成靜態庫’

main_project

targets

each

do

|

target

|

if

not

excludeTargets

include?

target

name

then

target_linkPhase

=

target

build_phases

select

{

|

phase

|

phase

kind_of?

Xcodeproj

::

Project

::

Object

::

PBXFrameworksBuildPhase

}

first

target_copy_resources_phase

=

target

build_phases

select

{

|

phase

|

phase

kind_of?

Xcodeproj

::

Project

::

Object

::

PBXResourcesBuildPhase

}

first

# find dynamic library references

# 1。 靜態庫是一個個Object檔案的集合,對於其依賴的動態庫加到binary最終的依賴就可以

# 2。 tbd 是一種動態庫的 stub library,擁有相應動態庫一樣的連結符號,但沒有相關程式碼,能加速編譯等

target_linkPhase

files_references

each

do

|

file_reference

|

if

file_reference

path

end_with?

“。tbd”

or

file_reference

path

end_with?

“。dylib”

or

file_reference

path

end_with?

“。framework”

then

main_linkPhase

add_file_reference

file_reference

true

end

# stub library is not allow in static library

if

file_reference

path

end_with?

“。tbd”

then

target_linkPhase

remove_file_reference

file_reference

end

end

# add OTHER_LDFLAGS

target

build_configurations

each

do

|

config

|

config

build_settings

‘MACH_O_TYPE’

=

‘staticlib’

# 保留靜態庫符號資訊

config

build_settings

‘STRIP_INSTALLED_PRODUCT’

=

‘NO’

end

# copy frameworks resources into main bundle

target_copy_resources_phase

files_references

each

do

|

file_reference

|

main_copy_resources_phase

add_file_reference

file_reference

true

end

end

end

main_project

save

all_load

打包成靜態庫後,第一個問題就是一些OC的Selector 沒有找到,解決方法便是增加all_load 編譯選項,參考文獻

資源管理

與動態framework 不同,靜態庫的Object最終都將打到main binary,因此,專案裡各個framework和target資源的路徑將會有所不同。為了規範資源使用,對各個模組的資源,我們都建立了單獨的bundle來管理,並修正了專案裡資源的使用方式

符號丟失

成功將專案改造成靜態庫打包後,在測試階段,我們發現採集上來的崩潰日誌資訊上符號資訊錯亂的情況。(如下:堆疊函式地址偏移過大,顯然是符號問題)

即刻Swift靜態庫實踐

透過Hopper 工具,發現jike可執行檔案裡,大量符號丟失。於是立刻將靜態庫的編譯配置改為

- Strip Debug Symbols During Copy: No

- Strip Style: Debugging Symbols

- Strip Linked Product: No

編譯後,打包發現符號恢復了,但包大小增加了近10M。因此,一度懷疑靜態庫帶來的10來M的最佳化是因為符號的丟失,仔細一想覺得不太合理,release 下符號資訊應該存在於單獨的dsym檔案,由dsym 檔案就可以重新符號化,因此不應該將符號打到最終的main binary。透過排查,發現靜態庫確實需要保留符號,因為靜態庫最終將被打到main binary,所以需要保留靜態庫的符號,這樣生成main binary的時候其dsym的符號資訊就能比較完整。

與此同時,設定main binary,因為符號最終無須存在於binary內。

Strip Debug Symbols During Copy: Yes

Strip Style: All Symbols

Strip Linked Product: Yes

這樣我們就能獲得完整符號資訊的dsym和符號瘦身的main binanry。而原來我們的專案是由動態庫構成,動態庫和main binary一樣,是獨立的macho檔案,丟棄符號的同時,也有其對應的dsym檔案,因此也能正常符號化。

進展

目前即刻將專案內的pods 依賴和模組target全部打包成靜態庫,並以及上線,體驗地址

premain 消耗從

800+ms

降低到

500+ms

, iPhone 6s

包大小

48M

減少到

35M

當前沒有找到靜態庫打包和動態庫打包對app bundle 大小影響的直接資料

但根據實踐和我們推測,與動態庫保持相對獨立和通用不同,靜態庫打包後其程式碼都將複製成為main binary的一部分,因此編譯器生成的main binary能獲得更多的最佳化, 比如無用程式碼消除等。

在iOS 平臺,動態庫是PIC(

position independent code

),相比較靜態庫no-PIC。

即刻Swift靜態庫實踐

CocoaPods

在我們著手做靜態庫打包的時候,cocoapods 也正進行相關支援,目前Cocoapods 1。4。0。2-beta 已經支援

Cocoapods 1。4。0。2-beta支援打包靜態庫,但需要在podspec 中新增屬性static_framework = true,由於pod spec 一般由pod owner 提供,絕大部分pod目前都沒有提供靜態庫打包的podspec。因此如果我們想依賴cocoapod完成打包工作,那麼一個可行的方式就是自己提供專案依賴的pod對應的靜態庫 podspec

最好的方式是建立自己私有的repo,並將上傳對應版本的podspec ,注意為每個 podspec 新增static_framework屬性。

對於podspec 裡dependency 也需要為其建立靜態庫版本

。透過這個方式,只要切換podfile指定的版本就能切換動態庫打包和靜態庫打包

Note: 有些混編的庫,目前Cocoapods 1。4。0。2-beta 尚未能很好支援

issue:

https://

github。com/CocoaPods/Co

coaPods/issues/7213

e。g: Rxswift、RxCocoa等

目前新的版本,對於Pod依賴 我們已經使用CocoaPods 來完成靜態庫打包, 主要考慮更小的維護成本,和能更靈活切換動/靜 庫打包, 帶

-static

是即刻私有repo上相應pod 的靜態庫版本,例如:

pod

‘AsyncSwift’

‘2。0。4-static’

pod

‘Alamofire’

‘4。2。0-static’

pod

‘RxSwift’

‘4。0。0’

pod

‘RxCocoa’

‘4。0。0’

pod

‘AsyncDisplayKit’

‘2。0。2-static’

pod

‘SwiftyUserDefaults’

‘3。0。0-static’

pod

‘ObjectMapper’

‘3。1。0-static’

結語&廣告

探索和跟進新技術是即刻iOS一直在前進的方向,歡迎?更多優秀的同學加入即刻,一起努力打造更酷的技術和應用。

招聘連結: 即刻船票

參考

Linker and Libraries Guide ]

[Reliable Software Technologies]

[Framework Programming Guide]

How are static libraries linked and how are dynamic libraries loaded?