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中的类指针链主要有两条:
- isa指针链:实例对象的isa指向类对象,类对象的isa指向元类对象,元类对象的isa指向根元类对象(通常是NSObject的元类)。根元类对象的isa指向自己,形成闭环。
- 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语言的规定:
- 每个成员变量的地址必须是其自身大小的整数倍
- 整个结构体的大小必须是其最大成员变量大小的整数倍
例如,对于以下结构体:
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
方法在程序启动加载类和分类时调用。特点:
- 在
main()
函数执行前调用 - 每个类和分类的
+load
方法只会调用一次
调用顺序:
- 父类的
+load
方法会在子类之前调用 - 分类的
+load
方法会在类的+load
方法之后调用 - 通过函数地址直接调用,不走消息发送机制
+ (void)load {
NSLog(@"%@ +load", self);
}
7.2 +initialize方法
+initialize
方法在类第一次接收到消息时调用。特点:
- 懒加载,在类第一次收到消息时调用,而非程序启动时
- 由于懒加载特性,可能永远不会被调用
- 遵循继承规则,如果子类没有实现,会调用父类的实现
- 通过消息发送调用,遵循消息发送机制
- 每个类只会初始化一次,但父类的
+initialize
可能被调用多次(当子类没有实现时) - 如果分类实现了
+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技术:
- 当对一个对象的属性注册观察者时,系统会动态创建该对象所属类的一个子类(NSKVONotifying_XXX)
- 重写被观察属性的setter方法,在其中添加通知机制
- 修改对象的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的搜索顺序如下:
搜索
set<Key>:
或set<Key>
方法如果未找到,则搜索
_set<Key>:
方法如果仍未找到且类实现了
accessInstanceVariablesDirectly
方法(默认返回YES),则按以下顺序直接访问实例变量:
_<key>
_is<Key>
<key>
is<Key>
如果仍未找到,调用
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可以捕获其外部作用域的变量,但捕获机制因变量类型而异:
- 局部auto变量:值捕获,block内部会创建一个变量副本
- 局部static变量:指针捕获,可以在block内修改
- 全局变量:不捕获,直接访问
- 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可以分为三种类型:
- 全局block(
_NSConcreteGlobalBlock
):没有捕获任何变量,或者只捕获全局变量、静态变量 - 栈block(
_NSConcreteStackBlock
):捕获了自动变量,存储在栈上,函数返回时会被销毁 - 堆block(
_NSConcreteMallocBlock
):从栈Block复制到堆上的Block,会持久存在直到引用计数为0
9.4 block的copy
在ARC下,编译器会在必要的情况下自动将栈Block复制到堆上,这些情况包括:
- 将block作为参数传递给方法或函数
- 将block赋值给__strong指针时
- block作为Cocoa API中方法名含有usingBlock的方法参数时
- 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下的内存管理:
- 需要手动调用
copy
和release
来管理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系统的工作原理,开发出更高效、更稳定的应用程序。