即刻Swift靜態庫實踐
背景
即刻是國內較早全面擁抱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來管理,並修正了專案裡資源的使用方式
符號丟失
成功將專案改造成靜態庫打包後,在測試階段,我們發現採集上來的崩潰日誌資訊上符號資訊錯亂的情況。(如下:堆疊函式地址偏移過大,顯然是符號問題)
透過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。
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?