我是如何讓微博綠洲的啟動速度提升30%的(二)
目錄
0.序言
之前的文章《我是如何讓微博綠洲的啟動速度提升30%的》收到了很多朋友的反饋。
其中,動態庫轉靜態庫的收益相比於二進位制重排收益更大,但在實際操作中大家也遇到了一些問題。
本著裝完X就跑,自己裝的X,跪著也要裝完的原則,在這裡我詳細來講一講這些問題。
1. 修改Mach-O Type到底改變了什麼?
我們先來看看動態庫。這裡我做了2個庫Pod1和Pod2:
Podfile檔案中配置了use_frameworks!
,然後進行pod install
,這樣生成的就是動態庫。
要怎麼確定這個是動態庫呢?
-
首先,這個庫的Mach-O Type是動態庫。
-
執行⌘+B構建之後,我們還是來到Products檔案中的app:
在生成的Demo.app檔案包上面點右鍵,選擇顯示包內容:
開啟Framewoks資料夾,我們可以看到裡面有我們建立的兩個動態Pod1.framework和Pod2.framework。資料夾裡面有程式碼簽名、資源、Info.plist、Pod1(Mach-O)、bundle。
也就是說,如果我們使用的是動態庫,在Framewoks資料夾就會看到它的身影,同時主工程的Mach-O檔案中是沒有相關的程式碼的。
下面我們修改Build Settings
同時按照上一篇文章說的,刪除Pods-Demo-frameworks.sh中install_framework相關的部分:
先執行Clean Build Folder(或⇧+⌘+K),然後再⌘+B進行構建。完成之後,我們還是來開啟Demo.app檔案包:
這次我們發現,Framewoks資料夾是空的!我們再看看主工程的Mach-O檔案:
我們看到我們在兩個庫中建立的類Pod1Object
和Pod2Object
來到了主工程的Mach-O檔案中!
現在應該明白了:
- 動態庫會和主工程的Mach-O分開存放。
- 靜態庫會和主工程的Mach-O合併在一起。
2. 靜態庫可能帶來的問題
之前我們看到靜態庫會和主工程的Mach-O合併在一起,這會引起什麼問題呢?
- 符號衝突
- Bundle的獲取
2.1 符號衝突
回顧下 -ObjC 、 -all_load 、-force_load這三個flag的區別:
- -ObjC 連結器會載入靜態庫中所有的Objective-C類和Category;(導致可執行檔案變大)
- -all_load 連結器會載入靜態庫中所有的Objective-C類和Category(這裡和上面一樣);當靜態庫只有Category時 -ObjC會失效,需要使用這個flag;
- -force_load 載入特定靜態庫的全部類,與 -all_load類似但是隻限定於特定靜態庫,所以 -force_load需要指定靜態庫;當兩個靜態庫存在同樣的符號時,使用 -all_load會出現 duplicate symbol的錯誤,此時可以根據情況選擇將其中一個庫 -force_load。
我們在Pod1庫中複製一份Pod2Object.{h,m},同時在Build Settings中的Other Linker Flags中新增 -all_load。
先執行Clean Build Folder(或⇧+⌘+K),然後再⌘+B進行構建,這時就會出現duplicate symbols報錯:
解決辦法:
任意一個或者都不使用靜態庫。雖然這麼說,其實這也是不安全的。如果能改名字就改一下吧。
2.2 Bundle的獲取
我們在Pod1Object
和Pod2Object
中新增以下方法:
- (nullable NSBundle *)getBundle {
return [NSBundle bundleForClass:[self class]];
}
複製程式碼
再在主工程的ViewController
中新增:
- (void)viewDidLoad {
[super viewDidLoad];
NSBundle *main = [NSBundle mainBundle];
NSBundle *pod1 = [[Pod1Object new] getBundle];
NSBundle *pod2 = [[Pod2Object new] getBundle];
NSLog(@"%@",main);
NSLog(@"%@",pod1);
NSLog(@"%@",pod2);
}
複製程式碼
我們先看一下動態庫的情況:
我們看到Main Bundle是我們的App,而我們的Pod1 Bundle和Pod2 Bundle分別是其對應的framework,類似於它們有自己的沙盒。
我們再來看看靜態庫:
可以看到3個Bundle都變成了我們的Main Bundle!
這是因為靜態庫被合併到了主工程Mach-O檔案中:
[NSBundle bundleForClass:[self class]];
複製程式碼
[self class]
現在在主工程的Mach-O中,那麼上面找到的自然是主工程的Bundle,即Main Bundle。
這個問題解決起來比符號衝突簡單一些,但解決這個問題前,我要先講一下CocoaPods。
2.3 CocoaPods
我們在執行了pod install
之後,CocoaPods會在主工程的Build Phase新增一個 [CP] Embed Pods Frameworks指令碼:
這個指令碼會在Build之後執行。我們之前靜態化後,把三方庫install_framework相關的程式碼註釋(或者刪除)了,來解決Archive之後在Organizer中嘗試Validate App時會報錯的問題:
其實,這個操作過於簡單粗暴,會導致資原始檔的丟失。
之前三方庫中資原始檔較少,沒有發現這個問題,感謝大家的提醒。
我們看仔細看一下install_framework到底是幹嘛的。
# Copies and strips a vendored framework
install_framework()
{
# 設定source變數,三方庫構建之後的路徑
if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then
local source="${BUILT_PRODUCTS_DIR}/$1"
elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then
local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")"
elif [ -r "$1" ]; then
local source="$1"
fi
# 設定destination變數,三方庫需要移動到的路徑
local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}"
# 判斷source是否為連結檔案,需要指向原來的檔案
if [ -L "${source}" ]; then
echo "Symlinked..."
source="$(readlink "${source}")"
fi
# rsync --delete無差異同步,可以簡單理解為網盤同步,或者複製
# 想詳細瞭解rsync,可以在命令列中輸入man rsync
# 這裡相當於把source的檔案(資料夾)同步到destination
# 即把*.framework複製到Frameworks資料夾下
# Use filter instead of exclude so missing patterns don't throw errors.
echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\""
rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}"
# 下面是找到二進位制檔案,即framework的Mach-O
local basename
basename="$(basename -s .framework "$1")"
binary="${destination}/${basename}.framework/${basename}"
if ! [ -r "$binary" ]; then
binary="${destination}/${basename}"
elif [ -L "${binary}" ]; then
echo "Destination binary is symlinked..."
dirname="$(dirname "${binary}")"
binary="${dirname}/$(readlink "${binary}")"
fi
# 去掉無效的架構
# Strip invalid architectures so "fat" simulator / device frameworks work on device
if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then
strip_invalid_archs "$binary"
fi
# 進行程式碼簽名
# Resign the code if required by the build settings to avoid unstable apps
code_sign_if_enabled "${destination}/$(basename "$1")"
# Swift的執行時庫,Xcode 7之後就用不到了,可以不管
# Embed linked Swift runtime libraries. No longer necessary as of Xcode 7.
if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then
local swift_runtime_libs
swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u)
for lib in $swift_runtime_libs; do
echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\""
rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}"
code_sign_if_enabled "${destination}/${lib}"
done
fi
}
複製程式碼
把這部分註釋了,相當於說不會把構建好的 *.framework包複製到App的Frameworks資料夾下,自然 *.framework中的資原始檔也就丟失了。
現在問題已經明瞭了:
- 靜態化會導致Bundle變為Main Bundle。
- 資源沒有從 *.framework中轉移到App中。
解決辦法:
既然現在拿到的Bundle是Main Bundle,我們構建之後利用指令碼把資源拷貝到App資料夾下不就好了。
install_framework_bundle()
{
# 設定source變數,三方庫構建之後的路徑
if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then
local source="${BUILT_PRODUCTS_DIR}/$1"
elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then
local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")"
elif [ -r "$1" ]; then
local source="$1"
fi
# 設定destination變數,三方庫需要移動到的路徑
local destination="${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
# 遍歷framework下的檔案,找到bundle和圖片,有其他資源自己改一下
for filename in `ls ${source} | grep ".*\.bundle\|.*\.jpg\|.*\.jpeg\|.*\.png"`
do
full_path=${source}/${filename}
# 把資源同步到Main Bundle中
rsync -abrv --suffix .conflict "${full_path}" "${destination}"
done
}
複製程式碼
現在我們的操作就是把被靜態化的三方庫從install_framework方法改為install_framework_bundle:
if [[ "$CONFIGURATION" == "Debug" ]]; then
install_framework_bundle "${BUILT_PRODUCTS_DIR}/Pod1/Pod1.framework"
install_framework_bundle "${BUILT_PRODUCTS_DIR}/Pod2/Pod2.framework"
fi
if [[ "$CONFIGURATION" == "Release" ]]; then
install_framework_bundle "${BUILT_PRODUCTS_DIR}/Pod1/Pod1.framework"
install_framework_bundle "${BUILT_PRODUCTS_DIR}/Pod2/Pod2.framework"
fi
複製程式碼
我們來對比一下:
現在資源都能正確訪問了。
// Pod1Object
@implementation Pod1Object
- (nullable NSBundle *)getBundle
{
return [NSBundle bundleForClass:[self class]];
}
- (nullable NSBundle *)getResourceBundle {
NSBundle *bundle = [self getBundle];
return [NSBundle bundleWithPath:[bundle pathForResource:@"image1" ofType:@"bundle"]];
}
@end
// Pod2Object
@implementation Pod2Object
- (nullable NSBundle *)getBundle
{
return [NSBundle bundleForClass:[self class]];
}
- (nullable NSBundle *)getResourceBundle {
NSBundle *bundle = [self getBundle];
return [NSBundle bundleWithPath:[bundle pathForResource:@"image" ofType:@"bundle"]];
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSBundle *pod1 = [[Pod1Object new] getResourceBundle];
NSBundle *pod2 = [[Pod2Object new] getResourceBundle];
UIImage *image1 = [[UIImage alloc] initWithContentsOfFile:[pod1 pathForResource:@"icon121" ofType:@"png"]];
UIImageView *imageView1 = [[UIImageView alloc] initWithFrame:CGRectMake(0,100,100)];
imageView1.contentMode = UIViewContentModeCenter;
[self.view addSubview:imageView1];
imageView1.image = image1;
UIImage *image2 = [[UIImage alloc] initWithContentsOfFile:[pod2 pathForResource:@"icon120" ofType:@"png"]];
UIImageView *imageView2 = [[UIImageView alloc] initWithFrame:CGRectMake(0,200,100)];
imageView2.contentMode = UIViewContentModeCenter;
[self.view addSubview:imageView2];
imageView2.image = image2;
}
@end
複製程式碼
注意:
install_framework_bundle中,我沒有處理重名問題。
-b --suffix .conflict會把重名檔案新增字尾 .conflict,這個字尾是可配的。
處理完你可以用find掃一遍App資料夾,看一下有沒有重名的資源被 .conflict標記出來。
check_conflict()
{
local destination="${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
conflict_list=`find ${destination} -regex '.*\.conflict'`
conflict_list=(${conflict_list/ /})
count=${#conflict_list[*]}
if [ $count -gt 0 ]; then
echo "Found conflicts:"
for var in ${conflict_list[@]}
do
echo $var
done
exit 1
fi
}
複製程式碼
如果資源重名,可能就沒方法靜態化了。
- 如果三方庫程式碼寫得不好,可能發生崩潰。
- 如果沒有發生崩潰,程式碼行為可能受到影響。
3. 動態庫和靜態庫的選擇
雖然這是一個老生常談的問題了,這裡既然在討論靜態庫和動態庫就簡單說一下。
庫型別 | 優點 | 缺點 |
---|---|---|
靜態庫 | 1. 目標程式沒有外部依賴,直接就可以執行。 2. 效率教動態庫高。 |
1. 會使用目標程式的體積增大。 |
動態庫 | 1. 不需要拷貝到目標程式中,不會影響目標程式的體積。 同一份庫可以被多個程式使用。 2. 執行時才載入,可以讓我們隨時對庫進行替換,而不需要重新編譯程式碼。 |
1. 動態載入會帶來一部分效能損失。 2. 動態庫會使得程式依賴於外部環境。如果環境缺少動態庫或者庫的版本不正確,就會導致程式無法執行。 |
iOS平臺上規定不允許存在動態庫,同時在iOS8之前因為App都是執行在沙盒當中,不同的程式之間不能共享程式碼:
- iOS是單程式的,就算使用了動態庫,也沒有可以共享程式碼的物件。
- 動態下載程式碼是被蘋果明令禁止的,也沒法發揮出動態庫的優勢。(如果你不需要上架App Store倒是可以使用)
綜上,所以上動態庫也就沒有存在的必要了。
iOS8之後,iOS有了App Extesion特性。由於iOS主App和Extension需要共享程式碼,於是蘋果後來提出了Embedded Framework。這種動態庫允許App和App Extension共享程式碼,但是這份動態庫的作用範圍被限定在一個App程式內,且需要拷貝到目標程式中。
簡單點可以理解為被閹割的動態庫:因為系統的動態庫是不需要拷貝到目標程式中,且可以被多個程式使用;而我們的動態庫(Embedded Framework)沒有這麼大的能力。
建議:
如果程式使用了App Extesion,且主工程和Extension使用了相同的三方庫:
- 可以使用動態庫來節約記憶體,減少包的大小。
- 如果涉及的庫較多,又想提升啟動速度,可以考慮合併多個動態庫,減少動態庫的數量。
還有什麼問題歡迎大家提出來~
如果覺得本文對你有所幫助,給我點個贊吧~