update: 动态库、静态库的编译链接细节

This commit is contained in:
FantasticLBP
2025-06-23 01:18:55 +08:00
parent aca020701b
commit 1142064d28
129 changed files with 10932 additions and 2615 deletions

View File

@@ -1177,13 +1177,13 @@ c 数组指针 `array()->lists + addedCount` 可以代表其中的位置。
过程如下
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/runtime-categoryattachLists.png)
![](./../assets/runtime-categoryattachLists.png)
结果就是类方法列表中,最前面的就是所有分类的方法列表,最后是类自身的方法列表。
效果为 `[[PersonWork Category 方法列表], [PersonStudy Category 方法列表], [Person类自身对象方法列表]]`
效果为 `[PersonWork Category 方法列表, PersonStudy Category 方法列表, Person类自身对象方法列表]`
假设 Person 有 m1、m2 对象方法Person+Study 分类有 m3、m4方法Person+Sleep 分类有 m5、m6方法(Person+Sleep 先编译)合并后的方法列表是 [m5, m6, m3, m4, m1, m2]
总结:
@@ -1193,6 +1193,62 @@ Category 编译之后 底层结构为 struct category_t里面存储着分类
QA:
1. Runtime 在将 category 方法和类方法合并的时候,以前版本是
```objective-c
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
```
而新版本为
```objective-c
for (int i = oldCount - 1; i >= 0; i--)
newArray->lists[i + addedCount] = array()->lists[i];
for (unsigned i = 0; i < addedCount; i++)
newArray->lists[i] = addedLists[I]
```
为什么要改?给出专业和权威的回答,尽量参考官方文档
- 线程安全与原子性
旧方案的潜在问题旧版本直接在原内存区域操作memmove 后接 memcpy若此时其他线程访问方法列表可能读到不一致的中间状态如部分旧数据被覆盖、部分新数据未写入导致崩溃或逻辑错误
新方案的改进:新版本通过 分配新内存、完整构建新数组,最后一次性更新指针(如 array()->count 和 array()->lists使得操作具备原子性。其他线程要么看到完整的旧数组要么看到完整的新数组避免了中间状态的不一致性
- 内存拓展的可靠性
`relloc` 的结果不可预测,可能失败或返回新地址,导致原指针失效。若运行时其他模块持有旧指针的引用,会引发野指针问题
新版本显式分配新内存malloc将旧数据迁移到新内存后替换指针。这种方式更可控避免了 `realloc` 的不确定性,同时允许旧内存的安全释放
- 内存重叠与顺序控制
虽 `memmove` 允许源和目标内存重叠,但在原数组基础上直接扩展时,若内存无法原地扩展(如后续内存被占用),`memmove` 可能覆盖有效数据或越界,导致不符合预期的行为
- 手动复制的灵活性
面向未来,方便插入一些其他逻辑。
2. 将 Category 信息和类自身信息合并放到运行时有什么好处?为什么不放到编译时搞定?
- 动态性。
- 延迟绑定Objective-C 的方法调用基于消息转发Messaging方法实现可以在运行时动态绑定。Category 的方法在启动时被合并到类的方法列表中,使得开发者可以在不修改原始类代码的情况下,动态扩展或替换方法。
- 支持热更新与插件化:通过 dlopen 或动态库加载,可以在程序运行时加载新的 Category如插件功能实现功能扩展而无需重新编译主程序。
- 解决多 Category 方法冲突的灵活性
当多个 Category 定义了同名方法时,最后被加载的 Category 方法会覆盖之前的方法。这种策略虽然简单,但需要运行时动态合并:
编译时无法确定 Category 的加载顺序(例如依赖动态库的加载顺序)。
运行时可以通过调整加载顺序实现方法覆盖的灵活控制。
如果放到编译时处理的潜在优势与取舍
如果选择编译时合并 Category可能带来
- 性能提升:方法查找可能更快(无需运行时遍历 Category 列表)。
- 更强的类型安全:编译时能检查所有方法冲突。
但代价是:
- 丧失动态能力如热修复、Method Swizzling、插件化等场景将无法实现。
- 代码僵化:所有扩展必须在编译时确定,无法适应动态环境(如大型应用的模块化延迟加载)
Objective-C 将 Category 合并推迟到运行时是为了最大化语言的动态性和灵活性支持热更新、AOP、插件化等高级场景。这种设计符合其“动态消息语言”的定位但牺牲了部分编译时优化机会。选择编译时或运行时处理本质是灵活性与性能/安全性的权衡,而 Objective-C 明确选择了前者。
### QA
#### 为什么二维数组打了引号?
@@ -1278,7 +1334,7 @@ Demo: 为 Person 类创建2个 Category分别存在同名方法 study
}
```
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/OCCategoryMethodOrderExplore.png" style="zoom:25%">
<img src="./../assets/OCCategoryMethodOrderExplore.png" style="zoom:25%">
可以看到 sayHi 方法存在多个,但是由于 Category 同名的方法在方法列表的前面,所以类自身的方法实现”被覆盖了“(根据 isa 查找方法实现的时候,优先查找到 Category 的方法实现,则停止查找了)
@@ -1291,17 +1347,17 @@ Demo: 为 Person 类创建2个 Category分别存在同名方法 study
Demo: 为 Person 类创建2个 Category分别存在同名方法 study具有不同实现。探索编译顺序决定方法实现
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/OCCategoryBuildOrderDemo1.png" style="zoom:25%">
<img src="./../assets/OCCategoryBuildOrderDemo1.png" style="zoom:25%">
2个对比实验
让 `Person+Study` 参与后编译
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/OCCategoryBuildOrderDemo2.png" style="zoom:25%">
<img src="./../assets/OCCategoryBuildOrderDemo2.png" style="zoom:25%">
让 `Person+Learn` 参与后编译
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/OCCategoryBuildOrderDemo3.png" style="zoom:25%">
<img src="./../assets/OCCategoryBuildOrderDemo3.png" style="zoom:25%">
@@ -1882,10 +1938,11 @@ static bool call_category_loads(void)
}
```
阅读源码发现
阅读源码可以得出2个结论
1. 在调用 `+load` 方法的时候,系统会先调用(可加载)类的 `+load` 方法,再调用分类的 `+load` 方法
2. `call_class_loads`、`call_category_loads` 方法内部实现,是通过 `loadable_class` `loadable_category` 结构体的 method 成员值 ,通过 `load_method_t load_method = (load_method_t)classes[i].method` 找到 `+ load` 方法地址。最后直接调用 `(*load_method)(cls, SEL_load)` 方法本身,没有采用消息机制。
1. `+load` 方法是系统启动后,系统主动调用的;
2. 在调用 `+load` 方法的时候,系统会先调用(可加载)类的 `+load` 方法,再调用分类的 `+load` 方法(先调用 `call_class_loads` 再调用 `call_category_loads`)
3. `call_class_loads`、`call_category_loads` 方法内部实现,是通过 `loadable_class` `loadable_category` 结构体的 method 成员值 ,通过 `load_method_t load_method = (load_method_t)classes[i].method` 找到 `+ load` 方法地址。最后直接调用 `(*load_method)(cls, SEL_load)` 方法本身,没有采用消息机制。
@@ -2004,6 +2061,33 @@ static void schedule_class_load(Class cls)
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
void add_class_to_loadable_list(Class cls)
{
IMP method;
loadMethodLock.assertLocked();
method = cls->getLoadMethod();
if (!method) return; // Don't bother if cls has no +load method
if (PrintLoading) {
_objc_inform("LOAD: class '%s' scheduled for +load",
cls->nameForLogging());
}
if (loadable_classes_used == loadable_classes_allocated) {
loadable_classes_allocated = loadable_classes_allocated*2 + 16;
loadable_classes = (struct loadable_class *)
realloc(loadable_classes,
loadable_classes_allocated *
sizeof(struct loadable_class));
}
// 将
loadable_classes[loadable_classes_used].cls = cls;
loadable_classes[loadable_classes_used].method = method;
loadable_classes_used++;
}
```
通过源码,我们可以看出:
@@ -2079,18 +2163,80 @@ void add_category_to_loadable_list(Category cat)
可以看到通过 `schedule_class_load` 处理完类之后,分类是直接通过 `_getObjc2NonlazyCategoryList(mhdr, &count)` 获取的,之后也是直接添加到 `loadable_categories` 中去。
举个例子:
存在下面 4个类
```objective-c
Class Person : NSObject
Class Person+Good : NSObject
Class Person+Bad : NSObject
Class Student : Person
Class Student+Good : Person
Class Student+Bad : Person
Class Cat: NSObject
Class Dog: NSObject
```
如果在 Xcode 的 Build Phases 中,顺序为
```objective-c
Cat.m
Dog.m
Student+Good.m
Student+Bad.m
Student.m
Person+Good.m
Person+Bad.m
Person.m
运行后打印为:
- Cat load
- Dog load
- Person load
- Student load
- Student+Good + load
- Student+Bad + load
- Person+Good load
- Person+Bad load
```
如果在 Xcode 的 Build Phases 中,顺序为
```objective-c
Cat.m
Student+Good.m
Student+Bad.m
Student.m
Person+Good.m
Person+Bad.m
Person.m
Dog.m
运行后打印为:
- Cat load
- Person load
- Student load
- Student+Good + load
- Student+Bad + load
- Person+Good load
- Person+Bad load
- Dog load
```
### +load 总结
- `+load` 方法是系统通过 runtime 在加载类、分类的时候调用的
- `+load` 方法是系统通过 Runtime 在加载类、分类的时候调用的
- 每个类、分类的 `+load` 在程序运行过程中只调用1次
- 调用顺序方面:
- 先调用类的 `+load` 方法
- 存在继承关系的话,调用子类的 `+load` 之前会调用父类的 `+load` 方法(runtime 会保证好,先调用父类的 `+load` ,再调用子类的 `+load`
- 不存在继承关系的话,会按照编译顺序调用 `+load`(先编译的先调用)
- 存在继承关系的话,调用子类的 `+load` 之前会调用父类的 `+load` 方法(Runtime 会保证好,先调用父类的 `+load` ,再调用子类的 `+load`
- 不存在继承关系的话,会按照编译顺序调用 `+load`(先编译的先调用,编译顺序参考 Xcode 的 Build Phases -> Compile Sources 中的顺序
- 再调用分类的 `+load` 方法
- 按照编译顺序调用 `+load`(先编译的先调用)
- 全局数组 `loadable_classes`,确保后续运行时能按正确顺序(父类 → 子类 → 分类)调用这些方法
@@ -2124,9 +2270,7 @@ Person +load
可以看到,我们主动调用 `[Student load]` 由于 Student 自身没有实现 `+load`,由于存在继承,所以调用了 Person 的 `+load` 。手动调用 `+load` 本质上就是给 runtime 消息机制,等价于 `objc_msgSend([Student class], @selector(load))` ,通过 Student 类对象的 isa找到 Student 元类对象,判断有没有类方法 `+load`,发现没有,则根据 Student 元类对象的 `superclass` 找到父类的元类对象,也就是 Person 的元类对象,发现有 `+load` 实现,即调用了 `+load` ,打印 `Person +load`
2.为什么 load 方法打印顺序是这样的?
2.为什么 load 方法打印顺序是这样的?
因为调用 student alloc相当于发送了消息。则肯定先执行 load 方法。类在 Runtime 启动阶段会调用 `schedule_class_load` 方法。方法内部递归调用,如果当前类存在父类则递归调用,否则将当前类加载到 loadable_classes 最后面。load 方法在本质上是执行 `call_load_methods`,方法地址是确定的(查看下面的源代码可以发现 `load` 方法是在编译期就可以确定的)。不走 `objc_msgSend` 这套流程。所以先打印父类 load、再打印子类 load、最后打印分类 load。如果存在多个分类则按照编译顺序打印 load。
@@ -2155,16 +2299,16 @@ Person +load
```objectivec
+[Person initialize]
+[Student(Good) initialize]
+[Student(Go od) initialize]
```
查看分类在 Runtime 加载类信息时候的调用原理可以知道分类中的类方法、对象方法都会被加载原始类的前面去initialize 是类方法)如下图:
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/runtime-categoryattachLists.png)
![](./../assets/runtime-categoryattachLists.png)
### 为什么给子类发消息,父类和子类的 +initialize 都会被调用?且父类的先调用
梳理下目前掌握的信息:当类第一次接收消息,也就是第一次调用对象方法的时候,该类的 `+initialize` 方法会被调用。所以需要从 objc_msgSend 或者 获取对象方法的角度去查看源码。聚焦下,以 `class_getInstanceMethod` 方法为入口,分析源码
梳理下目前掌握的信息:当类第一次接收消息,也就是第一次调用对象方法的时候,该类的 `+initialize` 方法会被调用。所以需要从 objc_msgSend 或者获取对象方法的角度去查看源码。聚焦下,以 `class_getInstanceMethod` 方法为入口,分析源码
```c++
/***********************************************************************
@@ -2353,7 +2497,7 @@ realizeAndInitializeIfNeeded_locked(id inst, Class cls, bool initialize)
`realizeAndInitializeIfNeeded_locked` 内部判断,当 class 需要被初始化且没有初始化过的时候则执行 `initializeAndLeaveLocked`
```
```c++
// Locking: caller must hold runtimeLock; this may drop and re-acquire it
static Class initializeAndLeaveLocked(Class cls, id obj, mutex_t& lock)
{
@@ -2554,6 +2698,56 @@ void callInitialize(Class cls)
至此,可以解释 Demo 中的现象了。
再举个例子
```objective-c
class Student: Person
class Person: NSObject
class Person+Good: NSObject
class Person+Bad: NSObject
class Teacher: Person
```
其中 Student、Teacher 都没有实现 initialize 方法。只有 Person 实现了、Person+Good 和 Person+Bad(编译顺序较后) 也实现了 initialize 方法.
调用
```objective-c
[Student alloc]
[Teacher alloc]
```
打印输出
```objective-c
- Person+Bad initialize
- Person+Bad initialize
- Person+Bad initialize
```
为什么输出这样的信息?
`[Student alloc]` 等价于 `objc_msgSend([Student Class], @sel(initialize))` 然后 `objc_msgSend` 汇编实现里会调用 `lookUpImpOrForward``lookUpImpOrForward` 内部会调用 `initializeNonMetaClass`,其内部实现会判断是否存在父类,存在父类且父类没有调用过 `initialize`,则会递归先调用当前类的父类的 `initialize` 方法。递归结束则调用 `callInitialize` 方法去完成当前类的 `initialize`
所以上述代码底层类似于
```Objective-c
if (Student 类没有初始化过) {
    if (Student 父类Person 父类存在,但存在分类,也就是 Person+Bad存在 && 父类没有初始化) {
    objc_msgSend(Person + Bad@selector(initializ)) // Person+Bad initialize
    }
objc_msgSend([Student class]@selector(initializ))  // Student 没有 initialize 方法,则根据 isa 查找父类方法。打印 Person+Bad initialize  
}
if (Teacher 类没有初始化过) {
// Person 已经 initialize 过if 里的逻辑进不去
    if (Teacher 父类Person 父类存在,但存在分类,也就是 Person+Bad存在 && 父类没有初始化) {
    objc_msgSend(Person + Bad@selector(initializ))
    }
objc_msgSend([Teacher class]@selector(initializ))  // Teacher 没有 initialize 方法,则根据 isa 查找父类方法。打印 Person+Bad initialize  
}
```
### 为什么 `initialize`方法存在“覆盖”的情况?
@@ -2725,7 +2919,52 @@ NS_ASSUME_NONNULL_END
@end
```
QA
1. 为什么 `category_t` 结构体里的属性命名为 `instanceProperties`? `classProperties` 何时使用?
普通的 property 就是 instanceProperties而用 `@property (class, nonatomic, copy) NSString *name;` class 修饰的就是 `classProperties`。
类属性是 Objective-C 在 Xcode 8 / LLVM 3.0 后引入的特性,旨在提供一种更简洁的方式声明与类相关的全局状态或行为。
属性描述中有 `class` 表明这是类属性,通过类名直接访问(如 ClassName.defaultName而非实例属性。
类属性存储在类的元类metaclass与实例属性完全隔离。
尽管声明方式类似实例属性,类属性不会自动生成存取方法或存储,需开发者手动实现。
```
// Person.h
@interface Person : NSObject
@property (class, nonatomic, copy) NSString *defaultName;
@end
// Person.m
@implementation Person
static NSString *_defaultName = nil;
+ (NSString *)defaultName {
return _defaultName;
}
+ (void)setDefaultName:(NSString *)defaultName {
_defaultName = [defaultName copy]; // 执行 copy 操作
}
@end
// 使用
// 设置类属性
Person.defaultName = @"Anonymous";
// 获取类属性
NSString *name = Person.defaultName;
```
类属性的使用场景:
- 为类定义全局可访问的默认值,例如应用的主题颜色、默认语言等。`@property (class, nonatomic, strong) UIColor *appThemeColor;`
- 单例模式的替代方案.若只需一个简单的共享实例,类属性可以替代传统单例模式。
2. 为什么要给 Category 添加的属性只有属性的 setter、getter却没有成员变量和对应的 setter、getter 实现,为什么这么设计?存在什么优点
为什么不能直接添加成员变量运行时限制Runtime 在程序加载时就确定了类的内存布局,包括成员变量的存储结构,如果允许在运行时动态添加成员变量,可能会破坏类的内存布局,导致兼容性问题
编译时的静态性:成员变量的存储结构需要在编译时确定,而 Category 是一种运行时机制,无法在编译时修改类的结构
设计哲学OC 的设计哲学强调动态性,而动态性体现在方法上,而不是成员变量上。成员变量的静态性有助于保持类的稳定性和可预测性。
Objective-C 的 Category 设计允许动态添加方法,但不允许直接添加成员变量,这是基于运行时机制的限制和语言设计哲学的考虑。这种设计的优点在于灵活性和扩展性,同时避免了对类内存布局的破坏。通过关联对象等机制,开发者可以在 Category 中实现类似成员变量的功能,从而充分利用运行时的动态性。
## 关联对象的底层实现
@@ -2820,24 +3059,26 @@ void _object_set_associative_reference(id object, void *key, id value, uintptr_t
梳理后,如下图所示:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AssociatedSaveValueInRuntimeStructure.png" style="zoom:45%">
<img src="./../assets/AssociatedSaveValueInRuntimeStructure.png" style="zoom:45%">
AssociationsManager 管理的 AssociationHashMap 结构如下:
AssociationsManager 管理的 AssociationsHashMap 结构如下:
```objective-c
{
// Person 实例对象的地址
"0x4927298732": {
"@selector(studyNumber)" : {
"@selector(studyNumber)的地址" : {
"value": "2022122201",
"policy": "retain"
},
"@selector(title)": {
"@selector(title)的地址": {
"value": "Hello category",
"policy": "retain"
}
},
// UIView 实例对象的地址
"0x3666444222": {
"@selector(backgroundColor)" : {
"@selector(backgroundColor)"的地址 : {
"value": "0xff0021",
"policy": "retain"
}
@@ -2937,7 +3178,7 @@ NS_ASSUME_NONNULL_END
### 声明私有方法
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CategoryUsageDeclPrivateMethod.png" style="zoom:30%" />
<img src="./../assets/CategoryUsageDeclPrivateMethod.png" style="zoom:30%" />