描述
OC 语言是一门动态语言,会将程序的一些决定工作从编译期推迟到运行期。所以 OC 不止需要依赖编译器,还需要依赖运行时环境。
OC 语言在编译期都会被编译为 C 语言的 Runtime 代码。二进制执行过程中执行的都是 C 语言代码。
OC 的类本质上都是结构体。在编译时都会以结构体的形式被编译到二进制中。
Runtime 是一套由 C,C++,汇编实现的 API,所有的方法调用都叫做发送消息。
Runtime 在 NSObject 中定义了一些基础操作。
数据结构
简单解释一下下面这张图:
一个普通的 objc_object(实例对象)中有一个 isa 指针,这个指针指向的是该实例对象所对应的 objc_class(类对象)。
因为 objc_class 继承自 objc_object,所以类对象(objc_class)中的 isa 指针指向的是 meta class(元类),子元类的 isa 指针都是根元类,根元类的 isa 指针指向其本身。
基础定义
OC 中主要有如下这些定义:1
2
3
4
5
6typedef struct objc_class *Class;
typedef struct objc_object *id;
typedef struct method_t *Method;
typedef struct ivar_t *Ivar;
typedef struct category_t *Category;
typedef struct property_t *objc_property_t;
objc_object
从 Runtime 源码中可以看出,每个对象都是一个 objc_object 的结构体,在结构体中有一个 isa 指针,该指针指向自己所属的类,由 Runtime 负责创建对象。
objc_class
类被定义为 objc_class 结构体,objc-class 结构体继承自 objc_object,所以类也是对象。在 objc-class 结构体中定义了对象 method,list,protocol,ivar list 等,来表示类的等行为。
meta class
既然类是对象,那么类对象也是其他类的实例。所以 Runtime 设计了 meta class,通过 meta class 来创建类对象,所以类对象的 isa 指针指向对应的 meta class。而 meta class 也是一个对象,所有 meta class 的 isa 都指向其根元类,根元类的 isa 指针指向自己。
method_t
Method 用来表示方法,其包含 SEL 和 IMP,定义如下:1
2
3
4
5
6typedef struct method_t *Method;
struct method_t {
SEL name;
const char *types;
IMP imp;
};
IMP
在 Runtime 中 IMP 本质上就是一个函数指针,有两个默认的参数 id 和 SEL,id 也就是方法中的 self,这和 objc——msgSend()函数传递的参数一样。1
typedef void (*IMP)(void /* id, SEL, ... */ );
property_t
Runtime 中定义了属性的结构体 property_t,用来表示对象中定义的属性。用@property 修饰符来修饰属性,修饰的属性为 objc_property_t 类型,其本质是 property_t 结构体,定义如下:1
2
3
4
5typedef struct property_t *objc_property_t;
struct property_t {
const char *name;
const char *attributes;
};
可以通过下面两个函数,分别获取实例对象的属性列表和协议列表1
2class_copyPropertyList(Class _Nullable cls, unsigned int * _Nullable outCount)
class_copyPropertyList(Class _Nullable cls, unsigned int * _Nullable outCount)
例获取属性列表:1
2
3
4
5
6
7
8
9
10
11unsigned int count;
objc_property_t *propertyList = class_copyPropertyList([Person class], &count);
for (int i = 0; i < count; i++) {
const char *propertyName = property_getName(propertyList[i]);
NSLog(@"property-->%@", [NSString stringWithUTF8String:propertyName]);
}
--------
Output:
2019-02-11 11:48:26.373741+0800 OCDemo[5467:1732583] property-->name
2019-02-11 11:48:26.373945+0800 OCDemo[5467:1732583] property-->name1
2019-02-11 11:48:26.374066+0800 OCDemo[5467:1732583] property-->name2
可以通过下面的方法,通过传入 Class 和 PropertyName,获取对应的 objc_property_t 属性结构体1
2
3
4class_getProperty(Class _Nullable cls, const char * _Nonnull name);
protocol_getProperty(Protocol * _Nonnull proto,
const char * _Nonnull name,
BOOL isRequiredProperty, BOOL isInstanceProperty);
NSObject
之前的定义
1 | // 声明Class和id |
之后的定义
1 | @interface NSObject <NSObject> { |
NSObject 中只有一个 Class 类型的 isa 变量,其他信息都隐藏起来了。
对象结构体
objc_object
OC 中每个对象都是一个结构体,结构体中都包含一个 isa 的成员变量,其位于成员变量的第一位。isa 的成员变量之前是 Class 类型,后来苹果将其改为 isa_t。1
2
3
4struct objc_object {
private:
isa_t isa;
};
OC 中的类和元类也是一样,都是结构体构成,由于类的结构体定义继承自 objc_object,所以其也是一个对象,也具有对象的 isa 特征。所以可以通过 isa_t 来查找对应的类或元类。
isa_t 定义
isa_t 是一个 union 的结构对象,union 类似于 C++结构体,其内部可以定义成员变量和函数。在 isa_t 中定义了 cls,bits,isa_t 三部分。
下面代码不是完整代码,只保留了 arm64 部分。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
struct {
uintptr_t nonpointer : 1; // 是32位还是64位
uintptr_t has_assoc : 1; // 对象是否含有或曾经含有关联引用,如果没有关联引用,可以更快的释放对象
uintptr_t has_cxx_dtor : 1; // 表示是否有C++析构函数或OC的析构函数
uintptr_t shiftcls : 33; // 对象指向类的内存地址,也就是isa指向的地址
uintptr_t magic : 6; // 对象是否初始化完成
uintptr_t weakly_referenced : 1; // 对象是否被弱引用或曾经被弱引用
uintptr_t deallocating : 1; // 对象是否被释放中
uintptr_t has_sidetable_rc : 1; // 对象引用计数太大,是否超出存储区域
uintptr_t extra_rc : 19; // 对象引用计数
类结构体
objc_class 结构体
在 Runtime 中类也是一个对象,类的结构体 objc_class 是继承自 objc_object 的,具备对象所有的特征。
在 objc_class 中定义了三个成员变量:
- superclass 是一个 objc_class 类型的指针,其指向其父类的 objc_class 结构体。
- cache 用来处理已调用方法的缓存。
- bits 是 objc_class 的主角,其内部只定义了一个 uintptr_t 类型的 bits 成员变量。存储了 class_rw_t 的地址。bits 中还定义了一些基本操作,例如获取 class_rw_t, raw isa 状态,是否 swift 等函数。objc_class 结构体中定义的一些函数,其内部都是通过 bits 实现的。
1 | struct objc_class : objc_object { |
class_ro_t 和 class_rw_t
和 class_data_bits_t 相关的有两个很重要的结构体,class_rw_t 和 class_ro_t,其中都定义着 method list, protocol list, property list 等关键信息。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15struct class_rw_t {
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
};
在编译后 class_data_bits_t 指向的是一个 class_ro_t 的地址,这个结构体是不可变的(只读)。在运行时,才会通过 realizeClass 函数将 bits 指向 class_rw_t.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
uint32_t reserved;
const uint8_t * ivarLayout;
const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
};
在程序开始运行后初始化 Class,在这个过程中,会把编译期存储在 bits 中的 class_ro_t 取出,然后创建 class_rw_t,并把 ro 赋值给 rw,成为 rw 的一个成员变量,最后把 rw 设置给 bits,代替之前 bits 中存储的 ro。除了这些操作外,还会有一些其他的赋值的操作。
下面是初始化 Class 的精简代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36static Class realizeClass(Class cls)
{
const class_ro_t *ro;
class_rw_t *rw;
Class supercls;
Class metacls;
bool isMeta;
if (!cls) return nil;
if (cls->isRealized()) return cls;
ro = (const class_ro_t *)cls->data();
rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
rw->ro = ro;
rw->flags = RW_REALIZED|RW_REALIZING;
cls->setData(rw);
isMeta = ro->flags & RO_META;
rw->version = isMeta ? 7 : 0;
supercls = realizeClass(remapClass(cls->superclass));
metacls = realizeClass(remapClass(cls->isa()))
cls->superclass = supercls;
cls->initClassisa(metacls);
cls->setInstanceSize(ro->instanceSize);
if (supercls) {
addSubclass(supercls, cls);
} else {
addRootClass(cls);
}
methodizeClass(cls);
return cls;
}
其中 addRootClass 和 addSubclass 函数,这两个函数的职责是将某个类的子类串成一个列表,大致是下面的链接顺序。由此,我们可以通过 class_rw_t,获取当前类的所有子类。1
superClass.firstSubclass -> subClass1.nextSiblingClass -> subClass2.nextSiblingClass -> ...
初始化 rw 和 ro 之后,rw 的 method list,protocol list 都是空的,需要在下面 methodizeClass 函数中进行赋值。函数中会把 ro 的 list 都取出来,然后赋值给 rw,如果在运行时动态修改,也是对 rw 做的操作。所以 ro 中存储的是编译时就已经决定的原数据,rw 才是运行时动态修改的数据。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31static void methodizeClass(Class cls)
{
bool isMeta = cls->isMetaClass();
auto rw = cls->data();
auto ro = rw->ro;
method_list_t *list = ro->baseMethods();
if (list) {
prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
rw->methods.attachLists(&list, 1);
}
property_list_t *proplist = ro->baseProperties;
if (proplist) {
rw->properties.attachLists(&proplist, 1);
}
protocol_list_t *protolist = ro->baseProtocols;
if (protolist) {
rw->protocols.attachLists(&protolist, 1);
}
if (cls->isRootMetaclass()) {
// root metaclass
addMethod(cls, SEL_initialize, (IMP)&objc_noop_imp, "", NO);
}
// Attach categories.
category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
attachCategories(cls, cats, false /*don't flush caches*/);
}
例如:
创建一个类 LXZObject,继承自 NSObject,并为其加入一个 testMethod 方法。因为在编译后 objc_class 的 bits 对应的是 class_ro_t 结构体,所以打印一下结构体的成员变量,看一下编译后的 class_ro_t 是什么样的。1
2
3
4
5
6
7
8
9
10
11
12
13struct class_ro_t {
flags = 128
instanceStart = 8
instanceSize = 8
reserved = 0
ivarLayout = 0x0000000000000000 <no value available>
name = 0x0000000100000f7a "LXZObject"
baseMethodList = 0x00000001000010c8
baseProtocols = 0x0000000000000000
ivars = 0x0000000000000000
weakIvarLayout = 0x0000000000000000 <no value available>
baseProperties = 0x0000000000000000
}
工作中的运用
动态添加属性
使用场景:分类是不能自定义属性和变量的,这时候可以使用 runtime 动态添加属性方法;
原理:给一个类声明属性,其实本质就是给这个类添加关联,并不是直接把这个值的内存空间添加到类存空间。1
2
3
4
5
6
7
8
9
10
11/** 关联对象、set方法
id object:给哪个对象添加关联,给哪个对象设置属性
const void *key:关联的key,要求唯一,建议用char 可以节省字节
id value:关联的value,给属性设置的值
objc_AssociationPolicy policy:内存管理的策略
*/
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
// 获取关联的对象、get方法
id objc_getAssociatedObject(id object, const void *key)
// 移除关联的对象
void objc_removeAssociatedObjects(id object)
例子:实现一个 UIView 的 Category 添加自定义属性 defaultColor。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18@interface UIView (Color)
@property (nonatomic, strong) UIColor *defaultColor;
@end
@implementation UIView (Color)
static char kDefaultColorKey;
- (void)setDefaultColor:(UIColor *)defaultColor
{
objc_setAssociatedObject(self, &kDefaultColorKey, defaultColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)defaultColor {
return objc_getAssociatedObject(self, &kDefaultColorKey);
}
@end
动态添加方法
使用场景:如果一个类方法非常多,加载类到内存的时候也比较耗费资源,需要给每个方法生成映射表,可以使用动态给某个类,添加方法解决。
主要面试题:有没有使用过 performSelector?1
2
3
4
5// 参数1:给哪个类添加方法
// 参数2:添加方法的方法编号SEL
// 参数3:添加方法的函数实现IMP(函数地址)
// 参数4:函数的类型,(返回值+参数类型)
class_addMethod(Class cls, SEL name, IMP imp, const char * types)
例子:
假如 Person 对象调用 eat 方法,而该方法并没有实现,则会报错。我们可以利用 Runtime 在 Person 类中动态添加 eat 方法,来实现该方法的调用。1
[p performSelector:@selector(eat)];
1 | @implementation Person |
方法交换 Method Swizzling
使用场景:在没有一个类的实现源码的情况下,想改变其中一个方法的实现,除了继承它重写,和借助类别重名方法暴力抢先外,还有更灵活的方法 Method Swizzle。1
2// 交换方法地址,交换两个方法的实现
method_exchangeImplementations(Method m1, Method m2)
封装后1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25@implementation NSObject (Swizzling)
+ (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector bySwizzledSelector:(SEL)swizzledSelector
{
Class class = [self class];
//原有方法
Method originalMethod = class_getInstanceMethod(class, originalSelector);
//替换原有方法的新方法
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
//先尝试給源SEL添加IMP,这里是为了避免源SEL没有实现IMP的情况
BOOL didAddMethod =
class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {//添加成功:说明源SEL没有实现IMP,将源SEL的IMP替换到交换SEL的IMP
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {//添加失败:说明源SEL已经有IMP,直接将两个SEL的IMP交换即可
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
@end
例子:例如我们想要替换 ViewController 生命周期方法,可以这样做1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16@implementation UIViewController (Swizzling)
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self methodSwizzlingWithOriginalSelector:@selector(viewWillAppear:) bySwizzledSelector:@selector(mj_viewWillAppear:)];
});
}
- (void)mj_viewWillAppear:(BOOL)animated{
[self mj_viewWillAppear:animated];
NSLog(@"被调用了");
}
@end
注意点:
- swizzling 建议在+load 中完成。+load 和 +initialize 是 Objective-C runtime 会自动调用两个类方法。+load 是在一个类被初始加载时调用,一定会被调用;+initialize 是在应用第一次调用该类的类方法或实例方法前调用,相当于懒加载方式,可能不被调用。此外 +load 方法还有一个非常重要的特性,那就是子类、父类和分类中的 +load 方法的实现是被区别对待的。换句话说在 Objective-C runtime 自动调用 +load 方法时,分类中的 +load 方法并不会对主类中的 +load 方法造成覆盖。
- swizzling 应该只在 dispatch_once 中完成,由于 swizzling 改变了全局的状态,所以我们需要确保在任何情况下(多线程环境,或者被其他人手动再次调用+load 方法)只交换一次,防止再次调用又将方法交换回来。+load 方法本身即为线程安全,为什么仍需添加 dispatch_once,其原因就在于+load 方法本身无法保证其中代码只被执行一次。
NSCoding 自动归档解档
场景:如果一个模型有许多个属性,实现自定义模型数据持久化时,需要对每个属性都实现一遍 encodeObject 和 decodeObjectForKey 方法,比较麻烦。我们可以使用 Runtime 来解决。
原理:用 runtime 提供的函数遍历 Model 自身所有属性,并对属性进行 encode 和 decode 操作。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
@implementation MJMusicModel
// 设置不需要归解档的属性
- (NSArray *)ignoredNames {
return @[@"_musicUrl"];
}
// 归档调用方法
- (void)encodeWithCoder:(NSCoder *)encoder
{
unsigned int count = 0;
// 获得这个类的所有成员变量
Ivar *ivars = class_copyIvarList([self class], &count);
for (int i = 0; i<count; i++) {
// 取出i位置对应的成员变量
Ivar ivar = ivars[i];
// 获得成员变量的名字
const char *name = ivar_getName(ivar);
// 将每个成员变量名转换为NSString对象类型
NSString *key = [NSString stringWithUTF8String:name];
// 忽略不需要归档的属性
if ([[self ignoredNames] containsObject:key]) {
continue;
}
// 归档
id value = [self valueForKey:key];
[encoder encodeObject:value forKey:key];
}
// 注意释放内存!
free(ivars);
}
// 解档方法
- (id)initWithCoder:(NSCoder *)decoder
{
if (self = [super init]) {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([self class], &count);
for (int i = 0; i<count; i++) {
// 取出i位置对应的成员变量
Ivar ivar = ivars[i];
// 获得成员变量的名字
const char *name = ivar_getName(ivar);
// 将每个成员变量名转换为NSString对象类型
NSString *key = [NSString stringWithUTF8String:name];
// 忽略不需要解档的属性
if ([[self ignoredNames] containsObject:key]) {
continue;
}
// 解档
id value = [decoder decodeObjectForKey:key];
// 设置到成员变量身上
[self setValue:value forKey:key];
}
free(ivars);
}
return self;
}
@end
面试题
objc 在向一个对象发送消息时,发生了什么
objc 在向一个对象发送消息时,runtime 会根据对象的 isa 指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,如果一直到根类还没找到,转向拦截调用,走消息转发机制,一旦找到 ,就去执行它的实现 IMP 。
objc 中向一个 nil 对象发送消息将会发生什么
如果向一个 nil 对象发送消息,首先在寻找对象的 isa 指针时就是 0 地址返回了,所以不会出现任何错误。也不会崩溃。
objc 中向一个对象发送消息[obj foo]和 objc_msgSend()函数之间有什么关系
在编译时,[obj foo] => objc_msgSend(obj, @selector(foo));。
什么时候会报 unrecognized selector 的异常
objc 在向一个对象发送消息时,runtime 库会根据对象的 isa 指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,如果,在最顶层的父类中依然找不到相应的方法时,会进入消息转发阶段,如果消息三次转发流程仍未实现,则程序在运行时会挂掉并抛出异常 unrecognized selector sent to XXX 。
能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量
不能向编译后得到的类中增加实例变量;
因为编译后的类已经注册在 runtime 中,类结构体中的 objc_ivar_list 实例变量的链表和 instance_size 实例变量的内存大小已经确定,同时 runtime 会调用 class_setvarlayout 或 class_setWeaklvarLayout 来处理 strong weak 引用.所以不能向存在的类中添加实例变量。能向运行时创建的类中添加实例变量;
运行时创建的类是可以添加实例变量,调用 class_addIvar 函数. 但是必须在调用 objc_allocateClassPair 之后,objc_registerClassPair 之前,原因同上.
给类添加一个属性后,在类结构体里哪些元素会发生变化
instance_size :实例的内存大小;
objc_ivar_list *ivars:属性列表
[self class] 与 [super class]
1 | @implementation Son : Father |
输出 Son Son
self 是类的隐藏参数,指向当前调用方法的这个类的实例;
super 本质是一个编译器标示符,和 self 是指向的同一个消息接受者。不同点在于:super 会告诉编译器,当调用方法时,去调用父类的方法,而不是本类中的方法。
在 [self class] 时,runtime 会调用 objc_msgSend。而在 [super class] 时,runtime 会调用 objc_msgSendSuper 。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16OBJC_EXPORT void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
/// Specifies the superclass of an instance.
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained id receiver;
/// Specifies the particular superclass of the instance to message.
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained Class class;
__unsafe_unretained Class super_class;
/* super_class is the first class to search */
};
其中有两个参数 receiver 和 super_class。即在 objc_super 结构体指向 superClass 父类的方法列表中查找 selector,找到后以 objc -> receiver 去调用父类的这个 selector。注意调用者还是 objc -> receiver 而不是 super_class 。
runtime 如何通过 selector 找到对应的 IMP 地址
每一个类对象中都一个方法列表,方法列表中记录着方法的名称,方法实现,以及参数类型,其实 selector 本质就是方法名称,通过这个方法名称就可以在方法列表中找到对应的方法实现。
_objc_msgForward 函数是做什么的,直接调用它将会发生什么
_objc_msgForward 是 IMP 类型,用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward 会尝试做消息转发。
runtime 如何实现 weak 变量的自动置 nil
- 初始化时:runtime 会调用 objc_initWeak 函数,初始化一个新的 weak 指针指向对象的地址。
- 添加引用时:objc_initWeak 函数会调用 objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。
- 释放时,调用 clearDeallocating 函数。clearDeallocating 函数首先根据对象地址获取所有 weak 指针地址的数组,然后遍历这个数组把其中的数据设为 nil,最后把这个 entry 从 weak 表中删除,最后清理对象的记录。
使用 runtime Associate 方法关联的对象,需要在主对象 dealloc 的时候释放么
无论在 MRC 下还是 ARC 下均不需要,被关联的对象在生命周期内要比对象本身释放的晚很多,它们会在被 NSObject -dealloc 调用的 object_dispose()方法中释放。