刚接触到动态库的时候,那时候带着一个问题,今天有空就把它解了。祝大家国庆长假愉快~~~
动态库和可执行文件,这两种类型的Mach-O产物,如果里面用到了相同的第三方库会怎么样,而这些第三方库可能版本也是不一样的。版本不一样带来功能上的差异,最糟糕的情况,软件会无端端crash。但是事实上,在动态库的使用上,是不会出现上面说的情况的。这主要归功于:two-level namespace
Two-level namespace
google 一下,发现苹果是使用了two-level namespace
解决上面的问题的。关于two-level namespace
的解析,详情请看这里
With the two-level namespace feature enabled, when the static linker records references to imported symbols, it records a reference to the name of the library that contains the symbol and the name of the symbol. Linking your programs with the two level namespace feature offers two benefits over the flat namespace:
Enhanced performance when searching for symbols. With the two-level namespace, the dynamic linker knows exactly where to start looking for the implementation of a symbol. With a flat namespace, the dynamic linker must search all the loaded libraries for the one that contains the symbol.
Enhanced forward compatibility. In the flat namespace, two or more libraries cannot contain symbols with different implementations that share the same name because the dynamic linker cannot know which library contains the preferred implementation. This is not initially a problem, because the static linker catches any such problems when you first build the application. However, if the vendor of one of your dependent shared libraries later releases a new version of the library that contains a symbol with the same name as one in your program or in another dependent shared library, your program will fail to run.
简单来说,符号 = 库的名称 + 原来的符号名称。这样static linker 在链接的时候就可以知道符号来自哪里了,不但优化了搜索符号的速度,也为后面的扩展性进行了兼容。Talk is cheap....
Demo time
通过demo来加深理解吧,以后看到这些符号的时候会有更深一层的理解。
Framework1 和Framework2 都为动态库,Logger 类同时存在于这两个Framework中,编译运行的结果,它们都正确的输出各自对应的logger。
// Logger.m
// Framework1
@implementation Logger
- (void)log
{
NSLog(@"Framework1 logger");
}
@end
// Logger.m
// Framework2
@implementation Logger
- (void)log
{
NSLog(@"Framework2 logger");
}
@end
输出结果如下:
=======================================================
2017-10-02 10:21:13.741 WorkWithFrameworks[6867:990637] Framework1 logger
2017-10-02 10:21:13.741 WorkWithFrameworks[6867:990637] Framework2 logger
一样的符号,不一样的实现。two-level namespace 使得这两个动态库可以正常工作。使用hopper查看一下,符号如下:
cfstring_Framework1_logger
cfstring_Framework2_logger
Problem solved.问题是解决了,发现有几个知识点需要再次学习一下。two-level namespace
算是overture 吧。带着下面几个问题,继续深挖。。。
- 可执行文件与动态库同为mach-o 的最终产物,它们的关系是什么
- 动态库与可执行文件作为最终产物,它为什么比可执行文件大
- 静态库也支持two-level namespace吗?
- two-level namespace 解决了符号重复的问题,但是类名也还是那个类名,会不会有问题
编译的一些细节问题
- 每个源代码由编译器编译成一个
.o
文件,.o
文件是中间产物 - 静态库由多个
.o
文件组成 - 由单个或者多个文件由linker链接成可执行文件,其中没有用到的
.o
文件不会参与链接 - 动态库是最终产物,不参与链接。在可执行文件中有一条对这个动态库的引用,可执行文件在加载的时候会先加载动态库
- 动态库与可执行文件都是mach-o文件
可执行文件与动态库同为mach-o 的最终产物,它们的关系是什么?
可执行文件与动态库同为mach-o文件,当可执行文件里面用到动态库,就会在其内部保留对动态库的引用。
动态库与可执行文件作为最终产物,它为什么比可执行文件大
动态库里面所有的.o
都会被保留,因为它无法确定哪些符号会被外部引用,因此一般的动态库比可执行文件要大
静态库也支持two-level namespace吗?
只有mach-o的文件才支持two-level namespace功能。
通过demo可以知道,静态库libStaticLibrary1.a 与libStaticLibrary2.a,里面都是.o
文件的集合,生成可执行文件的时候,只选取其中一份logger代码,至于选取那一份,跟编译顺序有关。这样会存在另外一个问题:这两个静态库依赖的类虽然是一样的,但是可能实现不一样,这样就会出现意想不到的问题了,最严重就是crash了。
FYI ,之前已经吃过一次这样的亏了,详情请查看:Release 崩溃 ,Debug 不崩? ,自己制作静态库的时候,一定要考虑好兼容性问题,对于一些外部饮用的第三方库除了要特别注意外,还可以通过加前缀来区分。
从上图可以看出,首先编译的是StaticLibrary1。
查看可执行文件可以知道,最终链接进入的是StaticLibrary1 版本的logger。
two-level namespace 解决了符号重复的问题,但是类名也还是那个类名,会不会有问题?
KVO 的实现简单来说就是创建了一个NSKVONotifying_A的新类,继承被观察的类A,它替换原有A类的isa 指针,重写了所提供的keyPath 相关的setter方法,实现观察A类属性变化。平时用,一般不会有什么问题,但是当遇上动态库,情况就不一样了。
在KVO发生的时候,中间类是动态生成添加的新类。查看runtime的源码,了解一下类添加的过程。
添加一个新类,首先调用objc_allocateClassPair ,然后是objc_registerClassPair。下面来看看objc_allocateClassPair
/***********************************************************************
* objc_allocateClassPair
* fixme
* Locking: acquires runtimeLock
**********************************************************************/
Class objc_allocateClassPair(Class superclass, const char *name,
size_t extraBytes)
{
Class cls, meta;
rwlock_writer_t lock(runtimeLock);
// Fail if the class name is in use.
// Fail if the superclass isn't kosher.
if (getClass(name) || !verifySuperclass(superclass, true/*rootOK*/)) {
return nil;
}
// Allocate new classes.
cls = alloc_class_for_subclass(superclass, extraBytes);
meta = alloc_class_for_subclass(superclass, extraBytes);
objc_initializeClassPair_internal(superclass, name, cls, meta);
return cls;
}
static Class getClass(const char *name)
{
runtimeLock.assertLocked();
// Try name as-is
Class result = getClass_impl(name);
if (result) return result;
// Try Swift-mangled equivalent of the given name.
if (char *swName = copySwiftV1MangledName(name)) {
result = getClass_impl(swName);
free(swName);
return result;
}
return nil;
}
注意到getClass,当getClass返回不为空的时候,代表该class已经存在了。也就是说,当两个动态库里面或者动态库与可执行文件里面包含一样的类,虽然two-level namespace可以在符号级别区分它们,但是如果它们各自实现了该类的KVO。那么,因为类名是一样的,所以NSKVONotifying_xxx 也是一样的。后来者注册KVO的时候,由于该类已经存在了,会导致objc_allocateClassPair返回nil。
后面者注册KVO的时候会失败或者导致程序崩溃。
写完收工,祝大家国庆快乐~
网友评论