我是如何讓微博綠洲的啓動速度提高30%的(二)

0.序言

以前的文章《我是如何讓微博綠洲的啓動速度提高30%的》收到了不少朋友的反饋。git

其中,動態庫轉靜態庫的收益相比於二進制重排收益更大,但在實際操做中你們也遇到了一些問題。objective-c

本着裝完B就跑,本身裝的B,跪着也要裝完的原則,在這裏我詳細來說一講這些問題。shell

1. 修改Mach-O Type到底改變了什麼?

咱們先來看看動態庫。這裏我作了2個庫Pod1Pod2swift

Podfile文件中配置了use_frameworks!,而後進行pod install,這樣生成的就是動態庫。安全

要怎麼肯定這個是動態庫呢?bash

  • 首先,這個庫的Mach-O Type是動態庫。架構

  • 執行⌘+B構建以後,咱們仍是來到Products文件中的appapp

    在生成的Demo.app文件包上面點右鍵,選擇顯示包內容svn

    打開Framewoks文件夾,咱們能夠看到裏面有咱們建立的兩個動態Pod1.frameworkPod2.framework。文件夾裏面有代碼簽名、資源、Info.plist、Pod1(Mach-O)、bundlepost

    也就是說,若是咱們使用的是動態庫,在Framewoks文件夾就會看到它的身影,同時主工程的Mach-O文件中是沒有相關的代碼的。

下面咱們修改Build Settings中的Mach-O Type,將其設置爲靜態庫Static Library

同時按照上一篇文章說的,刪除Pods-Demo-frameworks.shinstall_framework相關的部分:

先執行Clean Build Folder(或⇧+⌘+K),而後再⌘+B進行構建。完成以後,咱們仍是來打開Demo.app文件包:

此次咱們發現,Framewoks文件夾是空的!咱們再看看主工程的Mach-O文件:

咱們看到咱們在兩個庫中建立的類Pod1ObjectPod2Object來到了主工程的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的獲取

咱們在Pod1ObjectPod2Object中添加如下方法:

- (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 BundlePod2 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時會報錯的問題:

0x72613c21

其實,這個操做過於簡單粗暴,會致使資源文件的丟失。

以前三方庫中資源文件較少,沒有發現這個問題,感謝你們的提醒。

咱們看仔細看一下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中。

解決辦法:

既然如今拿到的BundleMain 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, 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, 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使用了相同的三方庫:

  • 可使用動態庫來節約內存,減小包的大小。
  • 若是涉及的庫較多,又想提高啓動速度,能夠考慮合併多個動態庫,減小動態庫的數量。

還有什麼問題歡迎你們提出來~

若是以爲本文對你有所幫助,給我點個贊吧~

相關文章
相關標籤/搜索