iOS底层原理系列02-深入了解Objective-C

发布于:2025-03-17 ⋅ 阅读:(16) ⋅ 点赞:(0)

1. Objective-C的本质

用Objective-C编写的代码,底层其实都是C\C++代码

在这里插入图片描述

所以Objective-C面向对象都是基于 C\C++的数据结构(结构体)实现的。

Objective-C并非像其他语言那样在编译期完全确定程序的行为,而是将许多决策推迟到运行时进行,这种特性被称为动态性,这是其区别于C++等静态语言的关键特性。

Objective-C的动态特性由Runtime系统提供支持。Runtime系统是一套C和汇编编写的API,是Objective-C面向对象和动态特性的基础。

// Objective-C程序最终会被编译为C语言代码
// 例如,一个简单的方法调用
[object method];

// 会被编译为
objc_msgSend(object, @selector(method));

这种消息发送机制(而非直接的函数调用)赋予了Objective-C强大的动态特性和灵活性。

2. OC对象

2.1 本质

Objective-C对象在底层实际上是一个结构体,这个结构体的定义在runtime源码中:

struct objc_object {
    Class isa;
};

typedef struct objc_object *id;

所有OC对象都包含一个isa指针,指向该对象所属的类。这个isa指针是实现OC运行时动态特性的关键。

2.2 OC对象的种类

2.2.1 instance对象

Instance对象就是我们平时编程中最常见的实例对象。ininstance对象就是通过类alloc出来的对象,每次调用alloc都会产生新的instance对象。

NSObject *obj = [[NSObject alloc] init];

这里的obj就是一个instance对象。

instance对象存储着:

  • isa指针
  • 其他成员变量

2.2.2 class对象

Class对象是由编译器创建的,每个类只有一个Class对象。获取Class对象的方法有:

// 方法1:通过类名获取
Class cls1 = [NSObject class];

// 方法2:通过实例对象获取
NSObject *obj = [[NSObject alloc] init];
Class cls2 = [obj class];

// 方法3:通过runtime方法获取
Class cls3 = object_getClass(obj);

Class对象存储着:

  • isa指针
  • superClass指针
  • 实例方法列表(instance method)
  • 成员变量列表(ivar)
  • 协议列表(protocol)
  • 属性列表(@property)

2.2.3 meta-class对象(元类对象)

Meta-Class对象也是一个Class对象,每个类在内存中有且只有一个meta-class对象。

Meta-Class存储着:

  • isa指针
  • superClass指针
  • 类方法列表
  • 类属性列表

获取Meta-Class对象:

Class cls = [NSObject class];
Class metaCls = object_getClass(cls);

3. isa指针与类指针链

OC中的对象结构通过isa指针形成了一个完整的指针链。这个指针链是OC面向对象实现和消息传递机制的基础。

3.1 isa指针

在OC中,每个对象都有一个isa指针,指向该对象的类信息。但实际上,在64位架构中,isa指针进行了优化,不再是直接指向Class对象的指针,而是使用了一种叫做指针位域的技术,即用一个64位的值来存储更多信息。

// 简化的64位架构isa定义
struct {
    uintptr_t nonpointer        : 1;  // 是否启用指针位域优化
    uintptr_t has_assoc         : 1;  // 是否有关联对象
    uintptr_t has_cxx_dtor      : 1;  // 是否有C++析构函数
    uintptr_t shiftcls          : 33; // 存储Class、Meta-Class信息
    // ...其他位域
};

从64bit开始,isa需要进行一次位运算,才能计算Class指针的真实地址,需要进行位运算:

Class cls = object_getClass(obj);
// 实际底层会进行位运算提取class指针
// 伪代码:isa & ISA_MASK

3.2 类指针链

Objective-C中的类指针链主要有两条:

  1. isa指针链:实例对象的isa指向类对象,类对象的isa指向元类对象,元类对象的isa指向根元类对象(通常是NSObject的元类)。根元类对象的isa指向自己,形成闭环。
  2. superclass指针链:子类的superclass指向父类,直到根类(通常是NSObject),根类的superclass为nil。而元类的superclass指向父类的元类,根元类的superclass指向根类对象。

这两条链共同构成了Objective-C的继承体系和消息传递机制的基础。
在这里插入图片描述

4. 类与对象的内存结构

为了深入理解OC的对象模型,我们需要了解其内存结构。

4.1 NSObject对象的内存结构

NSObject是OC中的根类,大多数OC类都继承自NSObject。NSObject的定义非常简单:

@interface NSObject <NSObject> {
    Class isa;
}
// 方法声明...
@end

从这个定义可以看出,NSObject只包含一个Class类型的isa指针。所以,一个NSObject实例对象的大小就是一个指针的大小:在64位系统上是8字节,在32位系统上是4字节。

验证NSObject对象大小:

NSObject *obj = [[NSObject alloc] init];
NSLog(@"NSObject实例对象大小:%zd", class_getInstanceSize([NSObject class]));
NSLog(@"NSObject实际分配大小:%zd", malloc_size((__bridge const void *)obj));

这两个打印结果可能不同:

  • class_getInstanceSize返回理论上的大小(8字节)
  • malloc_size返回实际分配的大小(可能是16字节)

这是因为内存分配遵循内存对齐规则,且系统会分配一个更合理的内存大小来存储对象。

4.2 自定义类的内存结构

对于自定义类,其内存结构会更复杂,包含从父类继承的变量和自己声明的变量。

@interface Person : NSObject {
    int _age;
    double _height;
}
@property (nonatomic, copy) NSString *name;
@end

对于上面的Person类:

  • 包含从NSObject继承的isa指针(8字节)
  • 包含一个int类型变量_age(4字节)
  • 包含一个double类型变量_height(8字节)
  • 包含一个NSString*类型的_name指针(由于@property自动合成,8字节)

理论上总大小为:8 + 4 + 8 + 8 = 28字节。但实际分配会考虑内存对齐,可能会是32字节。

4.3 类对象的内存结构

类对象(Class)是一个复杂的结构,在runtime源码中定义如下:

struct objc_class {
    Class isa;  // 指向Meta-Class
    Class superclass;  // 指向父类
    cache_t cache;  // 方法缓存
    class_data_bits_t bits;  // 存储类的详细信息
};

其中的bits成员是一个指向类详细信息的指针,包含:

  • 方法列表
  • 协议列表
  • 属性列表
  • 实例变量布局

5. 内存布局与对齐规则

5.1 内存对齐

内存对齐是CPU访问内存的规则,合理的内存对齐可以提高内存访问效率。

OC中的内存对齐规则遵循C语言的规定:

  1. 每个成员变量的地址必须是其自身大小的整数倍
  2. 整个结构体的大小必须是其最大成员变量大小的整数倍

例如,对于以下结构体:

struct Example {
    char a;     // 1字节
    double b;   // 8字节
    int c;      // 4字节
    short d;    // 2字节
};

在64位系统上,其内存布局为:

  • a: 占用1字节,但会因为b的对齐要求补齐到8字节
  • b: 占用8字节
  • c: 占用4字节
  • d: 占用2字节,但会因为整体对齐要求补齐到8字节

因此总大小为24字节,而非理论上的15字节。

5.2 OC对象的内存布局

OC使用Compact Model来优化对象的内存布局,即尽可能将小的实例变量放在一起,减少因对齐导致的内存浪费。

例如,对于以下类:

@interface OptimizedLayout : NSObject {
    char a;
    int b;
    short c;
    double d;
}
@end

编译器会重新排列这些变量的顺序,以最大限度地减少内存浪费:

isa (8字节)
d (8字节)
b (4字节)
c (2字节)
a (1字节)
padding (1字节,保证整体为8的倍数)

可以通过runtime API查看实例变量的内存布局:

unsigned int count;
Ivar *ivars = class_copyIvarList([OptimizedLayout class], &count);
for (int i = 0; i < count; i++) {
    Ivar ivar = ivars[i];
    NSLog(@"%s offset: %td", ivar_getName(ivar), ivar_getOffset(ivar));
}
free(ivars);

6. Category(分类)

Category是Objective-C中一个强大的特性,允许开发者向现有类添加方法,而无需继承或修改原始类的源代码。

6.1 Category的底层结构

在Runtime中,Category被定义为struct category_t结构体:

struct category_t {
    const char *name;  // 类名
    classref_t cls;    // 类引用
    struct method_list_t *instanceMethods;  // 实例方法列表
    struct method_list_t *classMethods;     // 类方法列表
    struct protocol_list_t *protocols;      // 协议列表
    struct property_list_t *instanceProperties;  // 实例属性列表
    struct property_list_t *_classProperties;    // 类属性列表
};

注意,这个结构体中没有定义实例变量列表。这就是为什么Category不能直接添加实例变量,但可以添加方法、属性和协议。

为什么不设计实例变量列表?

  • 内存布局问题:Category 是在运行时添加到已有类上的,当 Category 加载时,原始类的内存布局已经确定。如果允许 Category 添加实例变量,会改变类对象的内存布局,这会导致已存在的对象实例内存结构无法兼容。

  • 设计目的不同:Category 的主要目的是扩展类的行为(添加方法),而不是修改类的结构。这是 OC 语言设计的一个权衡

6.2 Category的加载处理过程

Category的加载过程是由runtime在启动时完成的,主要步骤如下:

  • 通过Runtime加载某个类的所有Category数据
  • 把所有的Category的方法、属性、协议合并到一个大数组中,后面参与编译的Category数据会在数组前面
  • 将合并后的分类数据插入到原类的方法列表前面。

这也是为什么Category中的方法能够覆盖原类方法的原因 - 因为它们会被放在方法列表前面,先被查找到。

6.3 如何给分类加成员变量

虽然Category不能直接添加实例变量,但可以通过关联对象(Associated Objects)间接实现类似的功能。

// Person+Extension.h
@interface Person (Extension)
@property (nonatomic, copy) NSString *nickname;
@end

// Person+Extension.m
#import <objc/runtime.h>

static char NicknameKey;

@implementation Person (Extension)
- (void)setNickname:(NSString *)nickname {
    objc_setAssociatedObject(self, &NicknameKey, nickname, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)nickname {
    return objc_getAssociatedObject(self, &NicknameKey);
}
@end

6.4 关联对象的原理

关联对象的原理是通过一个全局的哈希表(AssociationsHashMap)来存储关联对象,该哈希表以对象的地址为键,对应一个二级哈希表(ObjectAssociationMap),二级哈希表以关联的key为键,AssociationValue为值。

在这里插入图片描述

关联对象的实现依赖以下关键函数:

// 设置关联对象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);

// 获取关联对象
id objc_getAssociatedObject(id object, const void *key);

// 移除所有关联对象
void objc_removeAssociatedObjects(id object);

关联策略(objc_AssociationPolicy)决定了关联对象的存储和内存管理方式,类似于属性特性:

  • OBJC_ASSOCIATION_ASSIGN:弱引用,不持有关联对象
  • OBJC_ASSOCIATION_RETAIN_NONATOMIC:强引用,非原子性
  • OBJC_ASSOCIATION_COPY_NONATOMIC:复制,非原子性
  • OBJC_ASSOCIATION_RETAIN:强引用,原子性
  • OBJC_ASSOCIATION_COPY:复制,原子性

注意:关联对象是在对象层面而非类层面,因此即使通过关联对象实现的属性,其行为和真正的实例变量还是有差异的。此外,它们在内存上的管理也是独立的,不会随着对象释放自动移除,需要显式调用objc_removeAssociatedObjects

7. load和initialize的区别

+load+initialize是OC中两个重要的类方法,用于类的初始化,但它们的调用时机和特点存在显著差异。

7.1 +load方法

+load方法在程序启动加载类和分类时调用。特点:

  1. main()函数执行前调用
  2. 每个类和分类的+load方法只会调用一次

调用顺序:

  1. 父类的+load方法会在子类之前调用
  2. 分类的+load方法会在类的+load方法之后调用
  3. 通过函数地址直接调用,不走消息发送机制
+ (void)load {
    NSLog(@"%@ +load", self);
}

7.2 +initialize方法

+initialize方法在类第一次接收到消息时调用。特点:

  1. 懒加载,在类第一次收到消息时调用,而非程序启动时
  2. 由于懒加载特性,可能永远不会被调用
  3. 遵循继承规则,如果子类没有实现,会调用父类的实现
  4. 通过消息发送调用,遵循消息发送机制
  5. 每个类只会初始化一次,但父类的+initialize可能被调用多次(当子类没有实现时)
  6. 如果分类实现了+initialize,会覆盖原类的+initialize
+ (void)initialize {
    if (self == [MyClass class]) {
        // 确保只在本类而非子类调用时执行
        NSLog(@"%@ +initialize", self);
    }
}

7.3 对比总结

特性 +load +initialize
调用时机 程序启动加载类和分类时 第一次向类发送消息时
调用方式 直接函数调用 消息发送
调用顺序 父类->子类->分类 不确定
是否必被调用 否(懒加载)
继承性 不遵循继承规则 遵循继承规则
线程安全 保证线程安全 不保证线程安全
应用场景 无需考虑依赖关系的纯粹初始化 需要用到runtime信息的初始化工作

8. KVO和KVC

KVO(Key-Value Observing)和KVC(Key-Value Coding)是OC中两个强大的设计模式,为开发者提供了处理对象属性的灵活方式。

8.1 KVO(Key-Value Observing)

KVO的全称是Key-Value Observing,俗称“键值监听”,是一种观察者模式,允许对象监听其他对象特定属性的变化,并在变化时收到通知。

8.1.1 KVO的使用

// 注册观察者
[person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

// 实现观察者方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if ([keyPath isEqualToString:@"name"]) {
        NSString *oldName = [change objectForKey:NSKeyValueChangeOldKey];
        NSString *newName = [change objectForKey:NSKeyValueChangeNewKey];
        NSLog(@"名字从 %@ 变为 %@", oldName, newName);
    }
}

// 移除观察者
[person removeObserver:self forKeyPath:@"name"];

8.1.2 KVO的底层实现

KVO的底层实现依赖于Runtime机制和isa-swizzling技术:

  1. 当对一个对象的属性注册观察者时,系统会动态创建该对象所属类的一个子类(NSKVONotifying_XXX)
  2. 重写被观察属性的setter方法,在其中添加通知机制
  3. 修改对象的isa指针,指向新创建的子类
    在这里插入图片描述

可以通过以下代码验证KVO的实现原理:

Person *person = [[Person alloc] init];
NSLog(@"注册KVO前: %@ %@", object_getClass(person), [person class]);
    
[person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
NSLog(@"注册KVO后: %@ %@", object_getClass(person), [person class]);
    
// 输出结果:
// 注册KVO前: Person Person
// 注册KVO后: NSKVONotifying_Person Person

注意:object_getClass()返回对象的真实类型(isa指向的类),而[person class]被重写以返回原始类型。

8.2 KVC(Key-Value Coding)

KVC的全称是Key-Value Coding,俗称“键值编码”,KVC提供了一种使用字符串标识符访问对象属性的机制,而不需要通过调用特定的访问器方法。

8.2.1 KVC的使用
// 设置值
[person setValue:@"张三" forKey:@"name"];
[person setValue:@25 forKeyPath:@"address.number"];

// 获取值
NSString *name = [person valueForKey:@"name"];
NSNumber *number = [person valueForKeyPath:@"address.number"];

forKeyPath:方法支持点语法,可以访问对象的嵌套属性。

8.2.2 KVC的底层实现

当使用setValue:forKey:方法时,KVC的搜索顺序如下:

  1. 搜索set<Key>:set<Key>方法

  2. 如果未找到,则搜索_set<Key>:方法

  3. 如果仍未找到且类实现了

    accessInstanceVariablesDirectly
    

    方法(默认返回YES),则按以下顺序直接访问实例变量:

    • _<key>
    • _is<Key>
    • <key>
    • is<Key>
  4. 如果仍未找到,调用setValue:forUndefinedKey:方法(默认抛出异常)

同样,valueForKey:方法也有类似的搜索顺序。

8.2.3 KVC与集合操作

KVC还提供了强大的集合操作符,用于处理集合对象:

// 获取数组所有元素的age属性的平均值
NSNumber *avgAge = [persons valueForKeyPath:@"@avg.age"];

// 获取数组所有元素的name属性的集合
NSArray *names = [persons valueForKeyPath:@"@distinctUnionOfObjects.name"];

常用的集合操作符包括:

  • @count:计数
  • @sum@avg@max@min:数学运算
  • @unionOfObjects@distinctUnionOfObjects:对象合并

9. block

Block是OC中一种强大的语言特性,允许开发者创建匿名函数并能够捕获上下文变量。

9.1 block本质

block的本质是一个OC对象,内部也有isa指针。

block是封装了函数调用和函数调用环境的OC对象

这个结构体的定义如下:

struct Block_layout {
    void *isa;  // 指向所属类的指针
    int flags;  // 标志位,用于表示block的类型和其他属性
    int reserved;  // 保留字段
    void (*invoke)(void *, ...);  // 函数指针,指向block的实现代码
    struct Block_descriptor *descriptor;  // 描述block的附加信息
    // 捕获的变量
};

9.2 变量捕获

Block可以捕获其外部作用域的变量,但捕获机制因变量类型而异:

  1. 局部auto变量:值捕获,block内部会创建一个变量副本
  2. 局部static变量:指针捕获,可以在block内修改
  3. 全局变量:不捕获,直接访问
  4. OC对象:指针捕获,并根据属性特性增加引用计数
int val = 10;  // 局部auto变量
static int staticVal = 20;  // 局部static变量
__block int blockVal = 30;  // __block变量

void (^testBlock)(void) = ^{
    val = 100;  // 错误:无法修改捕获的自动变量
    staticVal = 200;  // 正确:可以修改静态变量
    blockVal = 300;  // 正确:可以修改__block变量
};

使用__block修饰符可以使局部变量在block内部可修改。其原理是将变量包装到一个结构体中,然后通过指针访问这个结构体。

9.3 block类型

根据block捕获变量的情况,block可以分为三种类型:

  1. 全局block_NSConcreteGlobalBlock):没有捕获任何变量,或者只捕获全局变量、静态变量
  2. 栈block_NSConcreteStackBlock):捕获了自动变量,存储在栈上,函数返回时会被销毁
  3. 堆block_NSConcreteMallocBlock):从栈Block复制到堆上的Block,会持久存在直到引用计数为0

9.4 block的copy

在ARC下,编译器会在必要的情况下自动将栈Block复制到堆上,这些情况包括:

  1. 将block作为参数传递给方法或函数
  2. 将block赋值给__strong指针时
  3. block作为Cocoa API中方法名含有usingBlock的方法参数时
  4. block作为GCD API的方法参数时

在MRC下,需要手动调用Block_copy()[block copy]来将栈Block复制到堆上。

// MRC下block属性的建议写法
@property (copy, nonatomic) void (^block)(void);

// ARC下block属性的建议写法
@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);

9.5 内存管理

Block的内存管理在ARC和MRC下有所不同:

ARC下的内存管理:

  • 编译器自动管理Block的引用计数
  • 自动在必要时复制栈Block到堆上
  • 捕获的对象会被Block强引用(除非使用__weak__unsafe_unretained修饰)

MRC下的内存管理:

  • 需要手动调用copyrelease来管理Block的引用计数
  • 栈Block需要手动复制到堆上
  • 捕获的对象默认不会被Block强引用

9.6 block的__forwarding指针

在block被复制到堆上时,会有一个__forwarding指针确保无论是在栈上还是堆上都能访问到正确的block:

struct Block_byref {
    void *isa;
    struct Block_byref *forwarding;  // 指向自身或堆上的副本
    int flags;
    int size;
    void (*dispose)(void *);
    int captured_variable;  // 捕获的变量
};

这个指针的作用是:

  • 对于栈Block,__forwarding指向堆block
  • 对于堆Block,__forwarding指向自身

这样,无论通过栈Block还是堆Block访问捕获的变量,都会通过__forwarding指针找到堆上的值,保证一致性。

9.7 解决循环引用

当block捕获了self或其他对象,并且这些对象也持有block时,会导致循环引用。解决方法有:

__weak修饰符(ARC下):创建一个弱引用,不增加引用计数

__weak typeof(self) weakSelf = self;
self.block = ^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    [strongSelf doSomething];
};

__unsafe_unretained修饰符(ARC下):创建一个不安全的弱引用,可能导致悬挂指针

__unsafe_unretained typeof(self) unsafeObj = self;
self.block = ^{
    [unsafeObj doSomething];
};

__block修饰符(MRC下):在Block执行完毕后将捕获的变量置nil

__block typeof(self) blockSelf = self;
self.block = ^{
    [blockSelf doSomething];
    blockSelf = nil;
};

总结

本文深入探讨了Objective-C的底层实现机制,从对象和类的本质,再到Category和Block等高级特性。通过理解这些底层知识,我们可以更好地利用Objective-C的强大特性,也能更好地理解和解决开发中遇到的问题。

Objective-C的动态性和灵活性使其成为一门强大的编程语言,即使在Swift逐渐成为iOS开发主流语言的今天,理解Objective-C的底层原理对于掌握iOS开发依然至关重要。通过学习这些内容,将能够更深入地理解iOS系统的工作原理,开发出更高效、更稳定的应用程序。