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文件为单位分开的。
这里我们新建一个ZJHAppDemo2工程,然后将静态库导入进入。运行成功后,通过objdump --macho -t
命令查看主程序的符号信息,如下图所示,静态库链接到主程序之后它的符号变成了本地符号,实际上是跟主程序app合并在一起了。
2.1.2、-noall_load
xcode 的build默认是-noall_load
,-noall_load顾名思义就是不会所有符号的加载,而是链接器链接一个静态库之前去扫描静态库文件,找到需要的代码再进行链接。例如ZJHStaticNoUsedTool类没有被用到,就不会被链接。
2.1.3、-all_load
链接所有符号,不管代码有没有使用,比如上面的例子,即使不用是ZJHStaticNoUsedTool也会被链接到app中:
图3:-all_load的使用.png2.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.png2.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 使用,且被包含的路径不同。
- 对于被当成第三方库使用的dylib或Framework,本身
Install name
可以设置为包含@rpath
的值,这个@rpath
其实是一个变量 - 对于动态库的使用者,可以通过设置
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