iOS動態(tài)庫與靜態(tài)庫的配置與使用

一、靜態(tài)庫和動態(tài)庫依賴問題

1.1、兩個庫相關(guān)的區(qū)別

在構(gòu)建的過程中: 動態(tài)庫需要經(jīng)過靜態(tài)鏈接。這里你沒有看錯,動態(tài)庫的生成需要靜態(tài)鏈接。而靜態(tài)庫的生成,不需要經(jīng)過靜態(tài)鏈接,僅僅只是簡單的將對應的 .o 文件壓縮。所以這里也可以通過命令行工具將 .o 文件重新解壓縮出來。 這里我們重點說一下動態(tài)庫,動態(tài)庫和我們項目產(chǎn)出的主工程可執(zhí)行文件對比,其編譯、鏈接等過程是完全一樣的。換句話說,動態(tài)庫是一個沒有 main 函數(shù)的可執(zhí)行文件。

在使用中: 動態(tài)庫是在程序啟動運行時,被動態(tài)鏈接后執(zhí)行調(diào)用的。而靜態(tài)庫則參與程序的靜態(tài)鏈接,被鏈入主工程的二進制可執(zhí)行文件中。這也就是為什么,動態(tài)庫需要被拷貝內(nèi)嵌 (embed) 到包內(nèi),靜態(tài)庫不需要的原因。

簡單說下靜態(tài)鏈接:將多個目標文件合并成一個可執(zhí)行文件。在這個過程中,把多個目標文件里面相同性質(zhì)的段合并到一起。靜態(tài)鏈接(static linking)是程序構(gòu)建中的一個重要環(huán)節(jié),它負責分析 compiler 等模塊輸出的 .o、.a、.dylib 、經(jīng)過對 symbol 的解析、重定向、聚合,組裝出 executable 供運行時 loader 和 dynamic linker 來執(zhí)行,有著承上啟下的作用。

動態(tài)庫和靜態(tài)庫,在生成時,因為其是否經(jīng)過靜態(tài)鏈接,產(chǎn)生了差異。動態(tài)庫經(jīng)過靜態(tài)鏈接后,會經(jīng)過符號決議、重定位等流程,會將依賴的靜態(tài)庫鏈接進來,也就是說,動態(tài)庫會吸附靜態(tài)庫。如果依賴的是動態(tài)庫,則走動態(tài)鏈接的流程。

在使用時,因為動態(tài)庫只需要動態(tài)鏈接,所以不會在主工程編譯階段報錯,但可能在運行階段報找不到庫。靜態(tài)庫則需要被主工程靜態(tài)鏈接,所以當缺少符號或者符號重復沖突時,會在編譯階段報錯。

1.2、動靜相互依賴問題

兩個靜態(tài)庫有相同符號:

  • 場景:靜態(tài)庫A、B均采用Framework的方式來創(chuàng)建,其中 A、B 包含同一個類 Obj ,然后將 A、B 同時集成到工程中去。
  • 結(jié)果:在鏈接(link)階段報符號重復。
  • 原因:A、B均需要參與主工程的靜態(tài)鏈接,會在靜態(tài)鏈接的符號決議過程中,發(fā)送沖突。

靜態(tài)庫 A 依賴靜態(tài)庫 B:

  • 場景:靜態(tài)庫A、B均采用 Framework(.a類似) 的方式來創(chuàng)建,其中 A 依賴 B。A 庫在 Framework Search Path 中正確設(shè)置 B 庫路徑。A、B庫代碼如下:
// 靜態(tài)庫A
@interface ObjA : NSObject
+ (void)test;
@end

@implementation ObjA
+ (void)test {
    NSLog(@"ObjA Test");
    [ObjB test]; //依賴 B 庫中的類方法
}
@end
  
// 靜態(tài)庫B
@interface ObjB : NSObject
+ (void)test;
@end

@implementation ObjB
+ (void)test {
    NSLog(@"ObjB Test");
}
@end
  
// 主工程中調(diào)用 [ObjA test];
  • 結(jié)果:主工程中,如果我們 A 正常使用,B 僅設(shè)置Framework Search Path,讓工程可以正確搜索到Framework,但是沒有設(shè)置linker flag,或者沒有設(shè)置 Link Binary With Libraries。則會在編譯的時候報缺少符號。

  • 原因:A靜態(tài)庫生成過程,因為并沒有經(jīng)過靜態(tài)鏈接,所以并不會包含 B 庫的符號。A、B均需要參與主工程的靜態(tài)鏈接,但此時B沒有設(shè)置Link Binary With Libraries,所以會在靜態(tài)鏈接的符號決議過程中,找不到對應的符號,報錯。

  • 推廣:這里如果是靜態(tài)庫 .a 依賴靜態(tài)庫 .framework、.a 依賴 .a也是一樣的情況。

  • 注意:上述情況有一個例外:.framework 靜態(tài)庫 依賴 .a 靜態(tài)庫。在這種情況下,如果我們在 A 庫中設(shè)置了Library Search Path 或 Link Binary With Libraries。會導致靜態(tài)庫的重新壓縮,生成出來的 A 庫會包含 B 庫的.o文件。使用 A 庫的時候,也就不再需要引入 .a 靜態(tài)庫B,否則會報符號沖突。如果不想 .a 靜態(tài)庫B被壓縮進 .framework 靜態(tài)庫A,則.framework 靜態(tài)庫A僅僅將 .a庫B的頭文件引入即可,不需要設(shè)置Library Search Path 或 Link Binary With Libraries。因為A庫生成時僅僅壓縮,并沒有靜態(tài)鏈接,所以這樣設(shè)置不會報錯,只要讓編譯器可以正常校驗通過即可。

兩個動態(tài)庫包含相同的符號:

  • 場景:動態(tài)庫A、B,其中 A、B 包含同一個類 Obj ,然后將 A、B 同時集成到工程中去。主工程調(diào)用[Obj test];
  • 結(jié)果:運行無異常,啟動時控制臺會輸出一個警告“Class Obj is implemented in both xxx and xxx”。大概意思就是 A.framework 和 B.framework 的可執(zhí)行文件里面都包含了 Obj 這個類。至于選哪個,取決于linker flag,或者 Link Binary With Libraries 中的先后順序,先被動態(tài)鏈接的會被調(diào)用到。
  • 原因:A、B均需要參與主工程的動態(tài)鏈接,僅會符號綁定(bind)一次,所以先綁定的會被調(diào)用到。

動態(tài)庫 A 依賴動態(tài)庫 B:

  • 場景:動態(tài)庫A、B,其中代碼同 2.1.2。A 庫在 Framework Search Path 中正確設(shè)置 B 庫路徑、Link Binary With Libraries也需要設(shè)置
  • 結(jié)果:主工程這里同樣只正確引入A,注意動態(tài)庫需要選 embed。B庫不引,或者僅設(shè)置Framework Search Path。結(jié)果build success,但是程序啟動就 crash ,控制臺報錯“Library not loaded xxx”
  • 原因:因為A 依賴 B,所以 B 也會被動態(tài)鏈接。以為編譯時僅僅靜態(tài)鏈接,所以編譯可以正常通過。但是因為B庫沒有沒內(nèi)嵌,所以啟動時動態(tài)鏈接,會報錯,不能正確的加載B庫。

靜態(tài)庫和動態(tài)庫包含相同符號:

  • 場景:靜態(tài)庫A、動態(tài)庫B,其中 A、B 包含同一個類 Obj ,然后將 A、B 同時集成到工程中去。主工程調(diào)用[Obj test];
  • 結(jié)果:運行無異常,啟動時控制臺會輸出一個警告“Class Obj is implemented in both xxx and xxx”。主工程中調(diào)用,會調(diào)入靜態(tài)庫A。動態(tài)庫B的調(diào)用,會調(diào)B庫自己內(nèi)部的。
  • 原因:因為靜態(tài)庫會在主工程靜態(tài)鏈接時,被正確的鏈接進二進制可執(zhí)行文件。同樣,動態(tài)庫B也會在生成時將源碼生成的 .o 文件正確的靜態(tài)鏈接進去。所以最終各自調(diào)用各自的。

靜態(tài)庫依賴動態(tài)庫:

  • 場景及結(jié)論:靜態(tài)庫A、動態(tài)庫B,A依賴B。生成A的時候,只需設(shè)置 Framework Search Path 即可,因為生成A不需要靜態(tài)鏈接,僅僅只是壓縮 .o 文件,只需要編譯器不報錯即可。主工程使用時,需要將 A、B都引入。

動態(tài)庫依賴靜態(tài)庫:

  • 動態(tài)庫最好不要依賴靜態(tài)庫,是因為靜態(tài)庫不需要在運行時再次加載, 如果多個動態(tài)庫依賴同一個靜態(tài)庫, 會出現(xiàn)多個靜態(tài)庫的拷貝, 而這些拷貝本身只是對于內(nèi)存空間的消耗.而且最重要的問題就是,鏈接的靜態(tài)庫里面的代碼將不在保證唯一性和順序性問題,里面的調(diào)用將和編譯時鏈接順序有關(guān)。將會產(chǎn)生諸如多個單例等問題,或者代碼邏輯順序不對等問題。

二、一些參數(shù)配置與區(qū)別

可下載 Demo

2.1、引入靜態(tài)庫的參數(shù)配置說明

2.1.1、查看指令

通過ar -t命令可以列出靜態(tài)庫的所有.o文件,通過命令nm -p查看靜態(tài)庫的符號信息,如下圖所示,可以看出靜態(tài)庫實際上是目標文件(.o文件)的集合,它的符號是以.o文件為單位分開的。

圖1:查看靜態(tài)庫指令介紹.png

這里我們新建一個ZJHAppDemo2工程,然后將靜態(tài)庫導入進入。運行成功后,通過objdump --macho -t 命令查看主程序的符號信息,如下圖所示,靜態(tài)庫鏈接到主程序之后它的符號變成了本地符號,實際上是跟主程序app合并在一起了。

圖2:靜態(tài)庫鏈接到主程序app.png

2.1.2、-noall_load

xcode 的build默認是-noall_load,-noall_load顧名思義就是不會所有符號的加載,而是鏈接器鏈接一個靜態(tài)庫之前去掃描靜態(tài)庫文件,找到需要的代碼再進行鏈接。例如ZJHStaticNoUsedTool類沒有被用到,就不會被鏈接。

2.1.3、-all_load

鏈接所有符號,不管代碼有沒有使用,比如上面的例子,即使不用是ZJHStaticNoUsedTool也會被鏈接到app中:

圖3:-all_load的使用.png

2.1.4、-force_load

使用 -force_load,這個你可以指定要載入所有方法的庫,后面必須跟一個只想靜態(tài)庫的路徑。比如我們在創(chuàng)建一個靜態(tài)庫ZJHStaticSDK2,然后創(chuàng)建相同ZJHStaticPublicTool類,之后在ZJHAppDemo2工程中引用兩個庫,這時第二個導入的libZJHStaticSDK2.a,默認會覆蓋第一個。

圖4:-force_load的使用.png

如果我們需要選擇第一個的話,可以使用 -force_load $(SRCROOT)/ZJHAppDemo2/libZJHStaticSDK.a,意思要載入libZJHStaticSDK.a。

2.1.5、-ObjC

這個flag告訴鏈接器把庫中定義的Objective-C類和Category都加載進來,這樣編譯之后的app會變大(因為加載了其他的objc代碼進來)。由于OC語言符號鏈接的基本單位是類,靜態(tài)庫鏈接時首先會鏈接本類,而Category是運行時才會被加載的,因此會被靜態(tài)鏈接器直接忽略掉,通過-ObjC命令是告知鏈接器鏈接所有的OC代碼。比如我們實現(xiàn)ZJHStaticPublicTool+Category,之后在App中引用這個category,然后運行會報錯,如下圖:

圖5:-ObjC配置使用1.png

添加-ObjC命令之后可以正常調(diào)用,Category已被鏈接到主程序,如下圖:

圖6:-ObjC配置使用2.png

2.1.6、dead code striping

dead code strip的作用(Remove functions and data that are unreacheble by entry point or export symbols)不管是.o文件、靜態(tài)庫還是動態(tài)庫,未被使用代碼會被剝離。沒有被使用的代碼,就是dead code 。xcode的默認情況下是會剝離dead code的。

前面提到的鏈接指令-noall_load、 -all_load、-force_load和-ObjC都是針對靜態(tài)庫的,跟dead code strip沒有任何關(guān)系。dead code strip是針對的是.o文件、靜態(tài)庫和動態(tài)庫。

2.2、@executable_path、@loader_path、@rpath

2.2.1 @executable_path

這個變量表示可執(zhí)行程序所在的目錄. 比如 /path/QQ.app/Contents/MacOS/

2.2.2 @loader_path

這個變量表示每一個被加載的 binary (包括App, dylib, framework, plugin等) 所在的目錄,對于framework內(nèi)置模塊或plugin特別適合。在一個程序中, 對于每一個模塊, @loader_path 會解析成不用的路徑, 而 @executable_path 總是被解析為同一個路徑(可執(zhí)行程序所在目錄).

比如一個會被多個程序調(diào)用的 plugin,位于 /path/Flash Player.plugin/Contents/MacOS/Flash Player,依賴 /path/Flash Player.plugin/Contents/Frameworks/XPSSO.dylib,那么 XPSSO.dylib 的 INSTALL_PATH 可以設(shè)置為 @loader_path/../Frameworks, 這樣設(shè)置的話, 不論 Flash Player.plugin 目錄放到什么位置, XPSSO.dylib 都能正確的被加載.

2.2.2 @rpath

@rpath 和前面兩個不同,它只是一個保存著一個或多個路徑的變量。比如 XPSSO.dylib 被兩個.app 使用,且被包含的路徑不同。

  1. 對于被當成第三方庫使用的dylib或Framework,本身Install name可以設(shè)置為包含@rpath的值,這個@rpath其實是一個變量
  2. 對于動態(tài)庫的使用者,可以通過設(shè)置Runpath Search Paths指定多個值,這些值在運行時會用于替代動態(tài)庫自己設(shè)置的@rpath來查找動態(tài)庫

比如:
softA.app/Contents/MacOS/dylib/XPSSO.dylib
softB.app/Contents/MacOS/Frameworks/XPSSO.dylib

將 XPSSO.dylib 的 INSTALL_PATH 設(shè)置成 @loader_path/../dylib 或 @loader_path/../Frameworks 都只能滿足其中一個 .app 的需求。

要解決這個問題,就可以用 @rpath,將 XPSSO.dylib 的 INSTALL_PATH 設(shè)置成 @rpath,然后在編譯 softA.app, softB.app 時分別指定 @rpath 為 @loader_path/../dylib, @loader_path/../Frameworks,問題得到了解決。

@rpath 的另一個優(yōu)點是可以設(shè)置多個路徑。如果 softA.app 還需要使用另一個 .plugin (假設(shè)它的 INSTALL_PATH 也設(shè)置成了 @rpath), 位于 @loader_path/../plugin, 把這個路徑加到 @rpath 即可。

三、組件化中引用庫

3.1、組件引用三方庫

打開配置文件 - 組件名字.podspec,配置組件frameworks依賴,s.vendored_frameworks: 包含的.framework,多個用逗號隔開。例如:

  s.vendored_frameworks = [
      'private/AFNetworking/AFNetworking.framework',
      'private/BeeHive/BeeHive.framework',
      'private/Masonry/Masonry.framework',
      'private/YYModel/YYModel.framework'
  ]

然后 s.vendored_libraries:,可以引用.a文件,使用方式和s.vendored_frameworks相同

3.2、動態(tài)庫和靜態(tài)庫的設(shè)置

use_frameworks!常用的形式:

use_frameworks! :linkage => :static  # 將引入的源碼組件打包成靜態(tài)庫。只對源碼組件有效
use_frameworks! :linkage => :dynamic # 將引入的源碼組件打包成動態(tài)庫。只對源碼組件有效
use_frameworks!     # 根據(jù) pod 類型來決定應該打包成靜態(tài)庫還是動態(tài)庫。
# use_frameworks!   # 不使用

使用 use_frameworks! 時,如果沒有指定源碼庫打包類型,則會根據(jù)對應組件的 podspec 文件中的設(shè)置來決定。設(shè)置字段如下:s.static_framework = true/false,設(shè)置true代表為靜態(tài)庫,設(shè)置false代碼為動態(tài)庫。

  • 總之,pod 引入了 Swift 的源碼三方庫,就使用 use_frameworks!
  • 引入了 dynamic framework 時,使用 user_framework!
  • 其他情況可不用

3.3、批量設(shè)置動靜庫

podfile文件添加以下代碼,可以批量的設(shè)置組件引用的庫為動態(tài)庫還是靜態(tài)庫,dynamic_frameworks數(shù)組里面的都是動態(tài)庫,不包含的還是默認靜態(tài)庫

dynamic_frameworks = Array['AFNetworking','MJExtension', 'YYCache']

pre_install do |installer|
  # workaround for https://github.com/CocoaPods/CocoaPods/issues/3289
  Pod::Installer::Xcode::TargetValidator.send(:define_method, :verify_no_static_framework_transitive_dependencies) {}
  
  #把第三方庫改成靜態(tài)庫
  installer.pod_targets.each do |pod|
    if !dynamic_frameworks.include?(pod.name)
      #puts "Overriding the static_framework? method for #{pod.name}"
      #注意cocoapods 1.7.3以下是static_framework
      def pod.build_as_static_framework?;
         true
      end
      def pod.build_type;
        if Gem::Version.new(Pod::VERSION) >= Gem::Version.new('1.9.0')
           BuildType.static_framework
        else
           Pod::Target::BuildType.static_framework
        end
      end
      def pod.static_framework?;
         true
      end
    end
  end
end

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容