1. 程式人生 > IOS開發 >我是如何讓微博綠洲的啟動速度提升30%的(二)

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

目錄

  1. 《我是如何讓微博綠洲的啟動速度提升30%的》
  2. 《我是如何讓微博綠洲的啟動速度提升30%的(二)》
  3. 《懶人版二進位制重排》

0.序言

之前的文章《我是如何讓微博綠洲的啟動速度提升30%的》收到了很多朋友的反饋。

其中,動態庫轉靜態庫的收益相比於二進位制重排收益更大,但在實際操作中大家也遇到了一些問題。

本著裝完X就跑,自己裝的X,跪著也要裝完的原則,在這裡我詳細來講一講這些問題。

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

我們先來看看動態庫。這裡我做了2個庫Pod1Pod2

Podfile檔案中配置了use_frameworks!,然後進行pod install,這樣生成的就是動態庫。

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

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

  • 執行⌘+B構建之後,我們還是來到Products檔案中的app

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

    開啟Framewoks資料夾,我們可以看到裡面有我們建立的兩個動態Pod1.frameworkPod2.framework。資料夾裡面有程式碼簽名、資源、Info.plist、Pod1(Mach-O)、bundle

    也就是說,如果我們使用的是動態庫,在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)];
    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使用了相同的三方庫:

  • 可以使用動態庫來節約記憶體,減少包的大小。
  • 如果涉及的庫較多,又想提升啟動速度,可以考慮合併多個動態庫,減少動態庫的數量。

還有什麼問題歡迎大家提出來~

如果覺得本文對你有所幫助,給我點個贊吧~