mirror of
https://github.com/NohamR/knowledge-kit.git
synced 2026-05-24 20:00:37 +00:00
update: 动态库、静态库的编译链接细节
This commit is contained in:
@@ -12,7 +12,7 @@ BSS段(bss segment):通常用来存储程序中未被初始化的全局变
|
||||
|
||||
代码段(code segment):通常是指用来存储程序可执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量。
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ struct NSObject_IMPL {
|
||||
|
||||
因此可以知道,OC 的类底层是由 c/c++ 的继承实现的。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/OCObjectLayoutWhenISA.png" style="zoom:45%">
|
||||
<img src="./../assets/OCObjectLayoutWhenISA.png" style="zoom:45%">
|
||||
|
||||
由于 obj 对象没有任何属性和方法,只有一个 isa 指针,且类的本质就是结构体,所以当结构体只有1个成员时,该成员的地址值,就是该结构体的地址。
|
||||
|
||||
@@ -118,7 +118,7 @@ struct Student_IMPL {
|
||||
|
||||
类的本质是结构体,结构体成员内存紧挨着。内存布局如图所示:
|
||||
|
||||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/StudentClassLayout.png" style="zoom:45%">
|
||||
<img src="./../assets/StudentClassLayout.png" style="zoom:45%">
|
||||
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ struct Student_IMPL {
|
||||
|
||||
如果上述结论正确,那是不是可以声明一个 ` Student_IMPL` 类型的结构体指针,指向 st 指针指向的对象。然后通过结构体指针访问成员变量,看看取值是不是正确的
|
||||
|
||||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/StructPointerVistorClassIvars.png" style="zoom:25%">
|
||||
<img src="./../assets/StructPointerVistorClassIvars.png" style="zoom:25%">
|
||||
|
||||
发现是可以正确访问的。
|
||||
|
||||
@@ -193,7 +193,7 @@ struct Student_IMPL {
|
||||
|
||||
为什么 `class_getInstanceSize([Person class])` 也是16,不是8+4吗?因为存在内存对齐,结构体的大小必须是最大成员大小的倍数(Person 中也就是8的倍数)
|
||||
|
||||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/StudentClassExtendsFromPersonClass.png" style="zoom:25%">
|
||||
<img src="./../assets/StudentClassExtendsFromPersonClass.png" style="zoom:25%">
|
||||
|
||||
|
||||
|
||||
@@ -330,12 +330,12 @@ int main(int argc, const char * argv[]) {
|
||||
|
||||
`Person *p1 = [Person new];` 这句代码在内存分配原理如下图所示
|
||||
|
||||

|
||||

|
||||
|
||||
**结论**
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
**可以 看到Person类的3个对象p1、p2、p3的isa的值相同。**
|
||||
|
||||
@@ -530,13 +530,64 @@ size_t instanceSize(size_t extraBytes) {
|
||||
|
||||
- `class_getInstanceSize([NSObject class])` :8,返回实例对象内存对齐后的的成员变量所占用的内存大小(即代码注释的 `Class's ivar size rounded up to a pointer-size boundary.` ),一个空对象,只有 isa 指针,所以只有8字节。可以理解为 **创建一个对象,至少需要多少内存**
|
||||
- `malloc_size((__bridge const void *)obj)` :16,Apple 规定,对象至少16个字节。但是只有一个 isa,所以只占用8个字节。
|
||||
内存对齐:结构体的最终大小必须是最大成员的倍数。可以理解为**创建一个对象,实际上分配了多少内存**
|
||||
|
||||
- 内存对齐:结构体的最终大小必须是最大成员的倍数。可以理解为**创建一个对象,实际上分配了多少内存**
|
||||
|
||||
- 系统分配了16个字节给 NSObject 对象(通过 malloc_size 函数获得)
|
||||
- 但 NSObject 对象内部只使用了8个字节的空间(64位环境下,通过 class_getInstanceSize 函数获得)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
```objective-c
|
||||
@interface Person : NSObject
|
||||
@end
|
||||
|
||||
Person *person = [[Person alloc] init];
|
||||
NSLog(@"%zd", class_getInstanceSize([person class])); // 8
|
||||
NSLog(@"%zd", malloc_size((__bridge const void *)person)); // 16
|
||||
```
|
||||
|
||||
|
||||
|
||||
为对象增加2个属性呢
|
||||
|
||||
```objective-c
|
||||
@interface Person : NSObject
|
||||
@property (nonatomic, assign) int age;
|
||||
@property (nonatomic, assign) int height;
|
||||
@end
|
||||
|
||||
Person *person = [[Person alloc] init];
|
||||
NSLog(@"%zd", class_getInstanceSize([person class])); // 16
|
||||
NSLog(@"%zd", malloc_size((__bridge const void *)person)); // 16
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
创建一个实例对象,至少需要多少内存?
|
||||
|
||||
```objective-c
|
||||
#import <objc/runtime.h>
|
||||
class_getInstanceSize([Person class])
|
||||
sizeOf()
|
||||
```
|
||||
|
||||
创建一个实例对象,实际上分配了多少内存?
|
||||
|
||||
```objective-c
|
||||
#import <malloc/malloc.h>
|
||||
malloc_size((__bridge const Person *)obj)
|
||||
```
|
||||
|
||||
占用内存,只需要遵循结构体内存对齐规则即可:即结构体成员变量内存最大的整数倍数
|
||||
|
||||
实际内存,除了考虑结构体内存对齐规则以外,还需要考虑系统为了内存访问速度而设计的内存分配 buckets 大小。
|
||||
|
||||
|
||||
|
||||
## 五、属性和方法
|
||||
|
||||
```objective-c
|
||||
@@ -563,7 +614,17 @@ struct Person_IMPL {
|
||||
};
|
||||
```
|
||||
|
||||
我们知道 `@property` 的本质是生成一个带下划线的 ivar,即 `_height`,还有 height 的 getter、setter 方法。为什么在结构体里没有看到方法?因为对象可以存在多个,这些方法的实现需要公用,没必要每个对象里都保存一份。
|
||||
我们知道 `@property` 的本质是生成一个带下划线的 ivar,即 `_height`,还有 height 的 getter、setter 方法。
|
||||
|
||||
为什么在结构体里没有看到方法?
|
||||
|
||||
1. 内存优化:
|
||||
- 因为对象可以存在多个,这些方法的实现需要公用,没必要每个对象里都保存一份。如果方法存储在实例中,1000 个实例会有 1000 份相同的方法代码,导致内存浪费。
|
||||
- **共享方法实现是面向对象语言的通用设计**(如 C++、Java)
|
||||
2. **动态性支持**:
|
||||
- 方法存储在类对象中,使得 Objective-C 的**运行时方法替换**(Method Swizzling)成为可能。例如,可以通过 `class_replaceMethod` 动态修改类的方法实现,所有实例立即生效。
|
||||
3. **继承与多态**:
|
||||
- 子类可以重写父类方法,方法查找会沿着类继承链进行,这种机制依赖于方法存储在类对象中。
|
||||
|
||||
|
||||
|
||||
@@ -707,7 +768,7 @@ iOS 中,系统分配内存,都是16的倍数。pageSize?系统在分配内
|
||||
GUN 都存在内存对齐这个概念。
|
||||
`sizeof` 本质是运算符。在 Xcode 编译后就替换为真正的值。通过指令 `xcrun --sdk iphoneos clang -arch arm64 -S -emit-llvm ViewController.m -o ViewController.ll` 查看 IR。
|
||||
|
||||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcoedeViewSizeOfViaAssembly.png" style="zoom:25%">
|
||||
<img src="./../assets/XcoedeViewSizeOfViaAssembly.png" style="zoom:25%">
|
||||
|
||||
|
||||
|
||||
@@ -718,21 +779,21 @@ GUN 都存在内存对齐这个概念。
|
||||
|
||||
`objc_getClass()` 如果传递 instance 实例对象,返回 class 类对象;传递 Class 类对象,返回 meta-class 元类对象;传递 meta-class 元对象,则返回 NSObject(基类)的 meta-class 对象
|
||||
|
||||

|
||||

|
||||
|
||||
instance 的 isa 指向 Class。当调用方法时,通过 instance 的 isa 找到 Class,最后找到对象方法的实现进行调用
|
||||
class 的 isa 指向 meta-class。当调用类方法的时,通过 class 的 isa 找到 meta-class,最后找到类方法进行调用。
|
||||
|
||||

|
||||

|
||||
|
||||
当 Student 实例对象调用 Person 的对象方法时,首先根据 Student 对象的 isa 找到 Stduent 的 Class 类对象,然后根据 Stduent Class 类对象中的 superClass 找到 Person 的 Class 类对象,在类对象的对象方法列表中找到方法实现并调用。
|
||||
当 Stduent 实例对象调用 init 方法时候,首先根据 Student 对象的 isa 找到 Stduent 的 Class 类对象,然后根据 Stduent Class 类对象中的 superClass 找到 Person 的 Class 类对象,找到 Person Claas 的 superClass 到 NSObject 类对象,在 NSObject 类对象的方法列表中找到 `init` 方法并调用。
|
||||
|
||||

|
||||

|
||||
|
||||
当 Stduent 对象调用类方法的时候,先根据 isa 找到 Student 的元类对象,然后在元类对象的 superclass 找到 Person 的元类对象,再根据 Person 元类对象的 superClass 找到 NSObject 的元类对象。最后找到元类对象的方法列表,调用到对象方法。
|
||||
|
||||

|
||||
<img src="./../assets/class-isa-superclass.png" style="zoom: 40%" />
|
||||
|
||||
```objectivec
|
||||
@interface Student : NSObject
|
||||
@@ -753,7 +814,7 @@ class 的 isa 指向 meta-class。当调用类方法的时,通过 class 的 is
|
||||
@end
|
||||
```
|
||||
|
||||
奇怪的是,我们给 Student 类对象调用 test 方法,`[Student test]` 则调用成功。是不是很奇怪?站在面向对象的角度出发,Student 类对象根据 isa 找到元对象,此时元对象方法列表中没有类对象,所以根据 superclass 找到 NSObject 元类对象,发现元类对象自身也没有类方法。但是为什么调用了 `-(void)test` 对象方法?
|
||||
奇怪的是,我们给 Student 类对象调用 test 方法,`[Student test]` 则调用成功。是不是很奇怪?站在面向对象的角度出发,Student 类对象根据 isa 找到元对象,此时元对象方法列表中没有类方法,所以再继续根据 superclass 找到 NSObject 元类对象,发现元类对象自身也没有类方法。但是为什么调用了 `-(void)test` 对象方法?
|
||||
|
||||
因为NSObject 元类对象的 superClass 继承自 NSObject 的类对象,类对象是存储对象方法的,所以定义在 NSObject 分类中的 `-(void)test` 最终会被调用。
|
||||
|
||||
@@ -778,7 +839,7 @@ struct mock_object_class *student = (__bridge mock_object_class *)[[Student allo
|
||||
```
|
||||
|
||||
如何查看类真正的结构?在 Xcode 中打印出来
|
||||
思路:查看 Class 内部的数据,发现是 struct,所以我们自己定义一个 struct,去承接类对象的元类对象信息
|
||||
思路:查看 Class 内部的数据,发现是 struct,所以我们查看源码,自己定义一个 struct,去承接类对象的元类对象信息
|
||||
|
||||
```c
|
||||
#import <Foundation/Foundation.h>
|
||||
@@ -916,7 +977,11 @@ public:
|
||||
};
|
||||
|
||||
#endif /* MockClassInfo_h */
|
||||
```
|
||||
|
||||
使用用
|
||||
|
||||
```objective-c
|
||||
Student *stu = [[Student alloc] init];
|
||||
stu->_weight = 10;
|
||||
|
||||
@@ -930,6 +995,10 @@ class_rw_t *studentMetaClassData = studentClass->metaClass()->data();
|
||||
class_rw_t *personMetaClassData = personClass->metaClass()->data();
|
||||
```
|
||||
|
||||
可以看到 Xcode 报错了,因为是 `main.m` 引入了 `MockClassInfo.h` ,会当作 OC 去编译。为了解决编译报错,将 `main.m` 改为 `main.mm` ,变成为 objective-c++ 文件,支持 OC 和 C++ 混编。
|
||||
|
||||
<img src='./../assets/XcodeCompileErrorOnMockObjcClass.png' style="zoom:40%" />
|
||||
|
||||
|
||||
|
||||
## 六、 内存对齐
|
||||
@@ -937,7 +1006,7 @@ class_rw_t *personMetaClassData = personClass->metaClass()->data();
|
||||
内存对齐是指数据在内存中存储时按照一定规则对齐到特定的地址上。在 iOS 开发中,内存对齐是为了提高内存访问的效率和性能。内存对齐的原因主要包括以下几点:
|
||||
1. 提高访问速度:内存对齐可以使数据在内存中的存储更加高效,因为大部分计算机体系结构都要求数据按照特定的边界对齐,这样可以减少内存访问的次数,提高访问速度。CPU访问非对齐的内存时需要进行多次拼接。
|
||||
如下图,比如需要读取从[2, 5]的内存,需要分别读取两次,然后还需要做位移的运算,最后才能得到需要的数据。这中间的损耗就会影响访问速度。
|
||||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/MemoryAlignReason.png" style="zoom:30%">
|
||||
<img src="./../assets/MemoryAlignReason.png" style="zoom:30%">
|
||||
|
||||
2. 为了方便移植。CPU是一块块的进行进行内存访问。有一些硬件平台不允许随机访问,只能访问对齐后的内存地址,否则会报异常。
|
||||
很多 CPU(如基于 Alpha,IA-64,MIPS,和 SuperH 体系的)拒绝读取未对齐数据。当一个程序要求这些 CPU 读取未对齐数据时,这时 CPU 会进入异常处理状态并且通知程序不能继续执行。举个例子,在 ARM,MIPS,和 SH 硬件平台上,当操作系统被要求存取一个未对齐数据时会默认给应用程序抛出硬件异常。
|
||||
@@ -990,9 +1059,26 @@ NSLog(@"%zd", sizeof(struct Person_IMPL)); // 24
|
||||
NSLog(@"%zd", malloc_size((__bridge const void *)person)); // 32
|
||||
```
|
||||
|
||||
`isa 指针8字节` + `int _age 4字节` + `_height 4字节` + `_no 4 字节` = 20 字节,因为存在内存对齐,因为结构体本身对齐内存对齐,必须为8的倍数,所以占据24个字节的内存。结构体成员变量,内存对齐时,对齐基数必须是各个成员变量中最大字节数的一个。
|
||||
QA:`isa 指针8字节` + `int _age 4字节` + `_height 4字节` + `_no 4 字节` = 20 字节。
|
||||
|
||||
结构体占据24字节,为什么运行起来后通过 `malloc_size` 得到32个字节?这个涉及到运行时内存对齐。规定 **iOS 中内存对齐以 16 的倍数为准**。
|
||||
结构体内存对齐规则:结构体的总大小必须是**最大成员对齐基数**的倍数。也就是 isa 8个字节的整数倍。所以占据24个字节的内存。**结构体成员变量,内存对齐时,对齐基数必须是成员变量最大一个的字节数的倍数**。
|
||||
|
||||
比如 `Person_IMPL` 结构体的最大成员变量是 isa,大小为8,所以内存对齐时,结构体占用内存必须是8的倍数。内存占用为8的倍数,实际大小为20,但为了内存对齐,则为24
|
||||
|
||||
QA:结构体占据24字节,为什么运行起来后通过 `malloc_size` 得到32个字节?
|
||||
|
||||
这个涉及到运行时内存对齐。规定 **iOS 中内存对齐以 16 的倍数为准**。内存分配器的底层策略:
|
||||
|
||||
大多数系统的内存分配器(如 macOS/iOS 的 `malloc`)会按 **特定大小的块(Bucket)** 分配内存,以提高性能和减少碎片。例如:
|
||||
|
||||
- 如果请求的内存大小在 **16~32 字节** 之间,分配器可能直接分配 **32 字节**。
|
||||
- 这种策略称为 **Size Class**,目的是减少内存碎片和管理
|
||||
|
||||
系统原理:
|
||||
|
||||
- malloc 的实现:macOS/iOS 使用 `libmalloc`,其内存块按 **16 字节粒度** 分配。
|
||||
- **`malloc_size` 的行为**:返回系统实际分配的大小,而非用户请求的大小。
|
||||
- **内存对齐的硬件原因**:CPU 访问对齐的内存地址效率更高,未对齐可能导致性能损失或崩溃。
|
||||
|
||||
|
||||
|
||||
@@ -1017,10 +1103,11 @@ NSLog(@"%zd", malloc_size(temp));
|
||||
成员变量占用8字节对齐,每个对象的第一个都是 isa 指针,必须要占用8字节。举例一个极端 case,假设 n 个对象,其中 m 个对象没有成员变量,只有 isa 指针占用8字节,其中的 n-m个对象既有 isa 指针,又有成员变量。每个类交错排列,那么 CPU 在访问对象的时候会耗费大量时间去识别具体的对象。很多时候会取舍,这个 case 就是时间换空间。以16字节对齐,会加快访问速度(参考链表和数组的设计)
|
||||
|
||||
上述是 Apple 官方的角度出发探究的,其他系统,比如 Linux 也是存在内存对齐的。由于 Linux 也是采用 GNU 的东西,所以探索下 GNU 下 glibc malloc 的实现。从[这里](https://ftp.gnu.org/gnu/glibc/)下载 glibc 源码。然后拖到 Xcode 中查看
|
||||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/GlibcInXcodeProject.png" style="zoom:25%">
|
||||
<img src="./../assets/GlibcInXcodeProject.png" style="zoom:25%">
|
||||
|
||||
可以看到 GNU 源码里面,内存对齐 `MALLOC_ALIGNMENT`
|
||||
在 i386 里面是16,在非 i386 里面有个判断
|
||||
|
||||
```c
|
||||
#define MALLOC_ALIGNMENT (2 * SIZE_SZ < __alignof__ (long double) \
|
||||
? __alignof__ (long double) : 2 * SIZE_SZ)
|
||||
@@ -1031,7 +1118,7 @@ NSLog(@"%zd", malloc_size(temp));
|
||||
# define INTERNAL_SIZE_T size_t
|
||||
```
|
||||
在 Xcode 打印输出, `__alignof__ (long double)` 为16,`sizeof(size_t)` 为8,即 `2 * SIZE_SZ` = 16,所以不管怎么看,在 GUN 里面内存对齐一定都是16.
|
||||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/GLibcMallocAlignment.png" style="zoom:25%">
|
||||
<img src="./../assets/GLibcMallocAlignment.png" style="zoom:25%">
|
||||
Todo: 研究探索 libmalloc 源码
|
||||
|
||||
|
||||
@@ -1055,6 +1142,24 @@ NSLog(@"%p %p %p %p %p", cls1, cls2, cls3, cls4, cls5); // 0x7ff85ca74270 0x7ff8
|
||||
- cls1...cls5 都是 NSObject 的 class 对象,也就是类对象。
|
||||
- 它都是同一个对象,每个类在内存中有且只有一个 class 对象
|
||||
|
||||
```objective-c
|
||||
Person *person = [[Person alloc] init];
|
||||
NSLog(@"%zd", class_getInstanceSize([person class])); // 24
|
||||
NSLog(@"%zd", malloc_size((__bridge const void *)person)); // 32
|
||||
|
||||
// 类对象
|
||||
Person *p = objc_getClass("Person");
|
||||
Class pCls0 = [person class];
|
||||
Class pCls1 = [Person class];
|
||||
Class pCls11 = [[[Person class] class] class];
|
||||
// 元类对象
|
||||
Class pcls2 = object_getClass([Person class]);
|
||||
Class pcls22 = object_getClass(object_getClass(person));
|
||||
|
||||
NSLog(@"%p %p %p %p %p %p", p, pCls0, pCls1, pCls11, pcls2, pcls22);
|
||||
// 0x100008268 0x100008268 0x100008268 0x100008268 0x100008240 0x100008240
|
||||
```
|
||||
|
||||
|
||||
|
||||
Class 对象在内存中存储的信息主要包括:
|
||||
@@ -1176,13 +1281,13 @@ Class objc_getClass(const char *aClassName)
|
||||
struct NSObject_IMPL {
|
||||
class isa;
|
||||
}
|
||||
|
||||
|
||||
struct Person_IMPL {
|
||||
struct NSObject_IMPL NSObject_IAVRS;
|
||||
int _age;
|
||||
int _height;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
struct Person_IMPL {
|
||||
class isa;
|
||||
@@ -1193,3 +1298,50 @@ Class objc_getClass(const char *aClassName)
|
||||
- Class 对象:属性信息、对象方法信息、成员变量信息、协议信息、superclass、isa 存放在类对象中。
|
||||
- Meta-Class 对象:类方法信息,存放在元类对象中。
|
||||
|
||||
|
||||
|
||||
## 总结
|
||||
|
||||
<img src="./../assets/class-isa-superclass.png" style="zoom: 40%" />
|
||||
|
||||
1. 实例对象的 isa 指针指向类对象
|
||||
2. 类对象的 isa 指针指向元类对象
|
||||
3. 元类对象的 isa 指针指向基类对象的元类对象
|
||||
4. 基类的元类对象的 isa 指针指向基类的类对象
|
||||
5. 同一个类的多个实例对象只有1个类对象
|
||||
6. 同一个类的多个实例对象只有1个元类对象
|
||||
7. 如果一个类美元父类,比如 NSObject 类,则 superclass 指针为 nil
|
||||
8. 实例对象的 isa 地址,按位与 `ISA_MASK` 得到类对象的地址
|
||||
9. 类对象的 isa 地址,按位与 `ISA_MASK` 得到元类对象的地址
|
||||
|
||||
对象的 isa 指针指向哪里?
|
||||
|
||||
- 实例对象(instance)的 isa 指针指向类对象(class 对象)
|
||||
- 类对象(class 对象) 的 isa 指针指向元类对象(meta-class 对象)
|
||||
- 元类对象(meta-class 对象) 的 isa 指针指向基类的元类对象(meta-class 对象)
|
||||
|
||||
OC 的类信息存放在哪里?
|
||||
|
||||
```objective-c
|
||||
@interface Student : Person<NSCopying> {
|
||||
int _no;
|
||||
}
|
||||
@property (nonatomic, assign) int score;
|
||||
|
||||
|
||||
- (void)study;
|
||||
|
||||
+ (void)live;
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
- 对象方法、属性、成员变量、协议信息存储在类对象(class 对象)中
|
||||
|
||||
比如 `-(void)study` 方法、score 属性、`_no` 成员变量,`NSCopying` 协议
|
||||
|
||||
- 类方法,存储在元类对象(meta-class 对象)中
|
||||
|
||||
比如 `+(void)live` 方法
|
||||
|
||||
- 成员变量的具体值,存放在实例对象的内存中。比如 student1 的 score 为30,student2 的 score 为 90
|
||||
|
||||
Reference in New Issue
Block a user