iOS动态库与静态库的配置与使用

一、静态库和动态库依赖问题

1.1、两个库相关的区别

在构建的过程中: 动态库需要经过静态链接。这里你没有看错,动态库的生成需要静态链接。而静态库的生成,不需要经过静态链接,仅仅只是简单的将对应的 .o 文件压缩。所以这里也可以通过命令行工具将 .o 文件重新解压缩出来。 这里我们重点说一下动态库,动态库和我们项目产出的主工程可执行文件对比,其编译、链接等过程是完全一样的。换句话说,动态库是一个没有 main 函数的可执行文件。

在使用中: 动态库是在程序启动运行时,被动态链接后执行调用的。而静态库则参与程序的静态链接,被链入主工程的二进制可执行文件中。这也就是为什么,动态库需要被拷贝内嵌 (embed) 到包内,静态库不需要的原因。

简单说下静态链接:将多个目标文件合并成一个可执行文件。在这个过程中,把多个目标文件里面相同性质的段合并到一起。静态链接(static linking)是程序构建中的一个重要环节,它负责分析 compiler 等模块输出的 .o、.a、.dylib 、经过对 symbol 的解析、重定向、聚合,组装出 executable 供运行时 loader 和 dynamic linker 来执行,有着承上启下的作用。

动态库和静态库,在生成时,因为其是否经过静态链接,产生了差异。动态库经过静态链接后,会经过符号决议、重定位等流程,会将依赖的静态库链接进来,也就是说,动态库会吸附静态库。如果依赖的是动态库,则走动态链接的流程。

在使用时,因为动态库只需要动态链接,所以不会在主工程编译阶段报错,但可能在运行阶段报找不到库。静态库则需要被主工程静态链接,所以当缺少符号或者符号重复冲突时,会在编译阶段报错。

1.2、动静相互依赖问题

两个静态库有相同符号:

  • 场景:静态库A、B均采用Framework的方式来创建,其中 A、B 包含同一个类 Obj ,然后将 A、B 同时集成到工程中去。
  • 结果:在链接(link)阶段报符号重复。
  • 原因:A、B均需要参与主工程的静态链接,会在静态链接的符号决议过程中,发送冲突。

静态库 A 依赖静态库 B:

  • 场景:静态库A、B均采用 Framework(.a类似) 的方式来创建,其中 A 依赖 B。A 库在 Framework Search Path 中正确设置 B 库路径。A、B库代码如下:
// 静态库A
@interface ObjA : NSObject
+ (void)test;
@end

@implementation ObjA
+ (void)test {
    NSLog(@"ObjA Test");
    [ObjB test]; //依赖 B 库中的类方法
}
@end
  
// 静态库B
@interface ObjB : NSObject
+ (void)test;
@end

@implementation ObjB
+ (void)test {
    NSLog(@"ObjB Test");
}
@end
  
// 主工程中调用 [ObjA test];
  • 结果:主工程中,如果我们 A 正常使用,B 仅设置Framework Search Path,让工程可以正确搜索到Framework,但是没有设置linker flag,或者没有设置 Link Binary With Libraries。则会在编译的时候报缺少符号。

  • 原因:A静态库生成过程,因为并没有经过静态链接,所以并不会包含 B 库的符号。A、B均需要参与主工程的静态链接,但此时B没有设置Link Binary With Libraries,所以会在静态链接的符号决议过程中,找不到对应的符号,报错。

  • 推广:这里如果是静态库 .a 依赖静态库 .framework、.a 依赖 .a也是一样的情况。

  • 注意:上述情况有一个例外:.framework 静态库 依赖 .a 静态库。在这种情况下,如果我们在 A 库中设置了Library Search Path 或 Link Binary With Libraries。会导致静态库的重新压缩,生成出来的 A 库会包含 B 库的.o文件。使用 A 库的时候,也就不再需要引入 .a 静态库B,否则会报符号冲突。如果不想 .a 静态库B被压缩进 .framework 静态库A,则.framework 静态库A仅仅将 .a库B的头文件引入即可,不需要设置Library Search Path 或 Link Binary With Libraries。因为A库生成时仅仅压缩,并没有静态链接,所以这样设置不会报错,只要让编译器可以正常校验通过即可。

两个动态库包含相同的符号:

  • 场景:动态库A、B,其中 A、B 包含同一个类 Obj ,然后将 A、B 同时集成到工程中去。主工程调用[Obj test];
  • 结果:运行无异常,启动时控制台会输出一个警告“Class Obj is implemented in both xxx and xxx”。大概意思就是 A.framework 和 B.framework 的可执行文件里面都包含了 Obj 这个类。至于选哪个,取决于linker flag,或者 Link Binary With Libraries 中的先后顺序,先被动态链接的会被调用到。
  • 原因:A、B均需要参与主工程的动态链接,仅会符号绑定(bind)一次,所以先绑定的会被调用到。

动态库 A 依赖动态库 B:

  • 场景:动态库A、B,其中代码同 2.1.2。A 库在 Framework Search Path 中正确设置 B 库路径、Link Binary With Libraries也需要设置
  • 结果:主工程这里同样只正确引入A,注意动态库需要选 embed。B库不引,或者仅设置Framework Search Path。结果build success,但是程序启动就 crash ,控制台报错“Library not loaded xxx”
  • 原因:因为A 依赖 B,所以 B 也会被动态链接。以为编译时仅仅静态链接,所以编译可以正常通过。但是因为B库没有没内嵌,所以启动时动态链接,会报错,不能正确的加载B库。

静态库和动态库包含相同符号:

  • 场景:静态库A、动态库B,其中 A、B 包含同一个类 Obj ,然后将 A、B 同时集成到工程中去。主工程调用[Obj test];
  • 结果:运行无异常,启动时控制台会输出一个警告“Class Obj is implemented in both xxx and xxx”。主工程中调用,会调入静态库A。动态库B的调用,会调B库自己内部的。
  • 原因:因为静态库会在主工程静态链接时,被正确的链接进二进制可执行文件。同样,动态库B也会在生成时将源码生成的 .o 文件正确的静态链接进去。所以最终各自调用各自的。

静态库依赖动态库:

  • 场景及结论:静态库A、动态库B,A依赖B。生成A的时候,只需设置 Framework Search Path 即可,因为生成A不需要静态链接,仅仅只是压缩 .o 文件,只需要编译器不报错即可。主工程使用时,需要将 A、B都引入。

动态库依赖静态库:

  • 动态库最好不要依赖静态库,是因为静态库不需要在运行时再次加载, 如果多个动态库依赖同一个静态库, 会出现多个静态库的拷贝, 而这些拷贝本身只是对于内存空间的消耗.而且最重要的问题就是,链接的静态库里面的代码将不在保证唯一性和顺序性问题,里面的调用将和编译时链接顺序有关。将会产生诸如多个单例等问题,或者代码逻辑顺序不对等问题。

二、一些参数配置与区别

可下载 Demo

2.1、引入静态库的参数配置说明

2.1.1、查看指令

通过ar -t命令可以列出静态库的所有.o文件,通过命令nm -p查看静态库的符号信息,如下图所示,可以看出静态库实际上是目标文件(.o文件)的集合,它的符号是以.o文件为单位分开的。

图1:查看静态库指令介绍.png

这里我们新建一个ZJHAppDemo2工程,然后将静态库导入进入。运行成功后,通过objdump --macho -t 命令查看主程序的符号信息,如下图所示,静态库链接到主程序之后它的符号变成了本地符号,实际上是跟主程序app合并在一起了。

图2:静态库链接到主程序app.png

2.1.2、-noall_load

xcode 的build默认是-noall_load,-noall_load顾名思义就是不会所有符号的加载,而是链接器链接一个静态库之前去扫描静态库文件,找到需要的代码再进行链接。例如ZJHStaticNoUsedTool类没有被用到,就不会被链接。

2.1.3、-all_load

链接所有符号,不管代码有没有使用,比如上面的例子,即使不用是ZJHStaticNoUsedTool也会被链接到app中:

图3:-all_load的使用.png

2.1.4、-force_load

使用 -force_load,这个你可以指定要载入所有方法的库,后面必须跟一个只想静态库的路径。比如我们在创建一个静态库ZJHStaticSDK2,然后创建相同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语言符号链接的基本单位是类,静态库链接时首先会链接本类,而Category是运行时才会被加载的,因此会被静态链接器直接忽略掉,通过-ObjC命令是告知链接器链接所有的OC代码。比如我们实现ZJHStaticPublicTool+Category,之后在App中引用这个category,然后运行会报错,如下图:

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

添加-ObjC命令之后可以正常调用,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文件、静态库还是动态库,未被使用代码会被剥离。没有被使用的代码,就是dead code 。xcode的默认情况下是会剥离dead code的。

前面提到的链接指令-noall_load、 -all_load、-force_load和-ObjC都是针对静态库的,跟dead code strip没有任何关系。dead code strip是针对的是.o文件、静态库和动态库。

2.2、@executable_path、@loader_path、@rpath

2.2.1 @executable_path

这个变量表示可执行程序所在的目录. 比如 /path/QQ.app/Contents/MacOS/

2.2.2 @loader_path

这个变量表示每一个被加载的 binary (包括App, dylib, framework, plugin等) 所在的目录,对于framework内置模块或plugin特别适合。在一个程序中, 对于每一个模块, @loader_path 会解析成不用的路径, 而 @executable_path 总是被解析为同一个路径(可执行程序所在目录).

比如一个会被多个程序调用的 plugin,位于 /path/Flash Player.plugin/Contents/MacOS/Flash Player,依赖 /path/Flash Player.plugin/Contents/Frameworks/XPSSO.dylib,那么 XPSSO.dylib 的 INSTALL_PATH 可以设置为 @loader_path/../Frameworks, 这样设置的话, 不论 Flash Player.plugin 目录放到什么位置, XPSSO.dylib 都能正确的被加载.

2.2.2 @rpath

@rpath 和前面两个不同,它只是一个保存着一个或多个路径的变量。比如 XPSSO.dylib 被两个.app 使用,且被包含的路径不同。

  1. 对于被当成第三方库使用的dylib或Framework,本身Install name可以设置为包含@rpath的值,这个@rpath其实是一个变量
  2. 对于动态库的使用者,可以通过设置Runpath Search Paths指定多个值,这些值在运行时会用于替代动态库自己设置的@rpath来查找动态库

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

将 XPSSO.dylib 的 INSTALL_PATH 设置成 @loader_path/../dylib 或 @loader_path/../Frameworks 都只能满足其中一个 .app 的需求。

要解决这个问题,就可以用 @rpath,将 XPSSO.dylib 的 INSTALL_PATH 设置成 @rpath,然后在编译 softA.app, softB.app 时分别指定 @rpath 为 @loader_path/../dylib, @loader_path/../Frameworks,问题得到了解决。

@rpath 的另一个优点是可以设置多个路径。如果 softA.app 还需要使用另一个 .plugin (假设它的 INSTALL_PATH 也设置成了 @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、动态库和静态库的设置

use_frameworks!常用的形式:

use_frameworks! :linkage => :static  # 将引入的源码组件打包成静态库。只对源码组件有效
use_frameworks! :linkage => :dynamic # 将引入的源码组件打包成动态库。只对源码组件有效
use_frameworks!     # 根据 pod 类型来决定应该打包成静态库还是动态库。
# use_frameworks!   # 不使用

使用 use_frameworks! 时,如果没有指定源码库打包类型,则会根据对应组件的 podspec 文件中的设置来决定。设置字段如下:s.static_framework = true/false,设置true代表为静态库,设置false代码为动态库。

  • 总之,pod 引入了 Swift 的源码三方库,就使用 use_frameworks!
  • 引入了 dynamic framework 时,使用 user_framework!
  • 其他情况可不用

3.3、批量设置动静库

podfile文件添加以下代码,可以批量的设置组件引用的库为动态库还是静态库,dynamic_frameworks数组里面的都是动态库,不包含的还是默认静态库

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) {}
  
  #把第三方库改成静态库
  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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,069评论 4 358
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,212评论 1 287
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 105,912评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,424评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,741评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,194评论 1 206
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,553评论 2 307
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,289评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,923评论 1 237
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,251评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,775评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,144评论 2 249
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,698评论 3 228
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,936评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,658评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,214评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,159评论 2 258

推荐阅读更多精彩内容