Objective-C设计指北
文章目录
2020.1.8 增加__forwarding指针
尝试回答“给你 C 语言,如何实现一个 Objective-C”。
孙源大神在他的文章中留下了这样一道问题:假如非要让我考一道 Runtime 的题,可能是“给你 C 语言,如何实现一个 Objective-C?”,答到哪儿算哪儿。
这篇文章尝试做一点解答,也整体复习一下Objective-C。
一、类与对象
对象
1 | /// Represents an instance of a class. |
Objective-C实例对象的本质是结构体,混沌初开的时候,其内部有且只有一个成员:Class类型的isa。isa是一个指向objc_class结构体的指针,在arm64上占用8个字节,在armv7上占用4个字节(本文都按照64位平台来说的)。一个NSObject对象只有这一个成员变量,理论上也只需要8个字节,但是通过memory read可以知道系统给它分配了16个字节。查看源码发现allocWithZone最终调用到instanceSize()函数时,为了内存对齐,限制了一个对象的最小占用内存为16个字节。同时,这也是为了呼应“操作系统的内存对齐”:给一个对象分配的字节数量为16的倍数。
1 | size_t instanceSize(size_t extraBytes) { |
在arm64架构之前,isa是一个普通的指针,直接指向类对象或者元类对象的内存地址。从arm64架构开始,isa被优化为一个共用体,使用位域把诸多信息存储在8个字节的方寸之间。
1 | union isa_t { |
这时候使用掩码进行按位与运算就可以计算出它的类对象或者元类对象的地址。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
NSObject *obj = [NSObject new];
// (lldb) p/x (long)obj->isa
// (long) $3 = 0x001dffff9da71141
Class cls = [NSObject class];
// (lldb) p/x (long)obj.class
// (long) $5 = 0x00007fff9da71140
// (lldb) p/x (0x001dffff9da71141 & 0x00007ffffffffff8ULL)
// (unsigned long long) $7 = 0x00007fff9da71140
每个实例对象生而不同,自己的成员变量理所应当地存储在每一个实例对象中,而方法具有唯一性,同一个类实例化的所有的对象应当共享一份对象方法。自然,对象方法应该存储在“类”中,这个“类”,就是类对象。除了对象方法之外还有类方法,就像全局函数,类方法存储在元类对象中。
成员变量与属性
声明一个YAPerson类,并填充一些成员变量、属性、实例方法和类方法。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19@interface YAPerson : NSObject {
@public
uint age;
NSString *nickname;
@private
BOOL isRich;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign, readonly) BOOL isMale;
@end
@implementation YAPerson
- (void)singWithSongName:(NSString *)songName {
NSLog(@"I'm singing a song named %@", songName);
}
+ (BOOL)canWork {
return YES;
}
@end
创建一个YAPerson实例,它包含了继承自NSObject的成员变量isa和自有的成员变量。它的结构是这样的:1
2
3
4
5
6
7
8struct YAPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS; // Class isa;
uint age;
NSString *nickname;
BOOL isRich;
BOOL _isMale;
NSString *_name;
};
迫于结构体内存对齐、系统内存对齐以及一个对象最小16个字节的限制,YAPerson实例占用48个字节。
当创建一个实例对象时,由于消息机制,类方法“new”的调用实际上是被转换为objc_msgSend()函数调用,消息接收者是类对象。而调用实例方法也是通过objc_msgSend()函数,只不过第一个参数是具体的实例对象。这里的字符串@"Love"
会被编译器优化为常量字符串,直接取地址使用了。
1 | [jack singWithSongName:@"Love"]; |
当对实例对象的属性进行赋值时,点语法会被编译器转化为setter方法,最后还是调用objc_msgSend()函数:objc_msgSend(jack, sel_registerName("setName:"), (NSString *)&__NSConstantStringImpl__var_xxxxxxxxx);
当对实例对象的成员变量赋值时就比较有趣了,jack->age = 23;
,大致是这种效果:1
2
3
4#define __OFFSETOFIVAR__(TYPE, MEMBER) ((long long) &((TYPE *)0)->MEMBER)
unsigned long int OBJC_IVAR_$_YAPerson$age = __OFFSETOFIVAR__(struct YAPerson, age);
uint *ageOffset = (uint *)((char *)jack + OBJC_IVAR_$_YAPerson$age);
(*ageOffset) = 23;
首先通过__OFFSETOFIVAR__
宏,获取成员变量age在YAPerson_IMPL
结构体中的偏移量:8个字节(isa占用8个字节),并把它存储为全局的常量,接着根据实例对象jack的地址加上成员变量age的偏移量获取age的地址,最后对age进行赋值。这些在编译的时候,就已经确定了,也即根本不需要通过jack->age = 23;
这样的指针调用。一个成员变量的结构是这样的:
1 | struct ivar_t { |
由于偏移量是全局的常量,所以使用int32_t *
类型的offset存储这个偏移量的地址,而不是使用int32_t
存储这个偏移量的值。name和type描述了成员变量的名称与类型,alignment的值取决于成员变量类型和机器架构,它的最小值为2的n次方。size是该类型变量占用字节。
编译时确定偏移量的原因是这样:
In the modern runtime, if you change the layout of instance variables in a class, you do not have to recompile classes that inherit from it.
在程序启动后,runtime加载类的时候,通过计算基类的大小,runtime动态调整了类成员变量布局。于是我们的程序无需编译,就能在新版本系统上运行。
变量地址 = 对象地址 + ivar.offset
ivar.offset = 基类地址(动态) + ivar在本类中的偏移量(编译时固定)
动态创建一个类,添加成员变量的时候我们会用到这些信息:1
class_addIvar(myClass, "someIvar", sizeof(int), log2(_Alignof(int)), @encode(int))
这个函数只能用在动态创建类的时候。类一旦定义完毕就无法再(在分类中)添加成员变量,因为“类中成员变量的偏移量是由基类大小和本类中成员变量共同决定的,如果一个类添加了成员变量,size发生了变化,会导致子类无法工作”。
一个类的所有成员变量信息汇聚在一起就成为了成员变量列表struct ivar_list_t
,它在内存中只需要一份:1
2
3
4
5
6
7
8
9
10
11
12
13
14static struct {
unsigned int entsize; // sizeof(struct _prop_t)
unsigned int count; // 数量
struct ivar_t ivar_list[6]; // 成员变量数组
} _OBJC_$_INSTANCE_VARIABLES_YAPerson = {
sizeof(ivar_t),
6,
{{(unsigned long int *)&OBJC_IVAR_$_YAPerson$age, "age", "I", 2, 4},
{(unsigned long int *)&OBJC_IVAR_$_YAPerson$nickname, "nickname", "@\"NSString\"", 3, 8},
{(unsigned long int *)&OBJC_IVAR_$_YAPerson$isRich, "isRich", "B", 0, 1},
{(unsigned long int *)&OBJC_IVAR_$_YAPerson$_isMale, "_isMale", "B", 0, 1},
{(unsigned long int *)&OBJC_IVAR_$_YAPerson$_name, "_name", "@\"NSString\"", 3, 8},
{(unsigned long int *)&OBJC_IVAR_$_YAPerson$_block, "_block", "@?", 3, 8}}
};
_OBJC_$_INSTANCE_VARIABLES_YAPerson
是YAPerson类的全局的成员变量列表。
对于我们来说,Objective-C中的属性指的就是成员变量与存取器方法。一般地,声明一个属性的时候,编译器会生成一个下划线打头的成员变量,比如@property (nonatomic, copy) NSString *name;
会生成成员变量:NSString *_name;
。
在语言层面,属性指的是属性的名称与对属性的修饰(类型、原子性、内存策略等)。
1 | struct property_t { |
name是属性的名称,attributes包含的信息就比较多了,可以通过property_getAttributes()获取,比如YAPerson的name属性,得到的是"T@\"NSString\",C,N,V_name"
,“C”表示copy,“N”表示nonatomic,具体含义官方文档说的很详细。
当然,属性列表是所有属性的集合:1
2
3
4
5
6
7
8
9
10
11static struct {
unsigned int entsize; // sizeof(struct _prop_t)
unsigned int count_of_properties;
struct property_t prop_list[3];
} _OBJC_$_PROP_LIST_YAPerson = {
sizeof(property_t),
3,
{{"name","T@\"NSString\",C,N,V_name"},
{"isMale","TB,R,N,V_isMale"},
{"block","T@?,C,N,V_block"}}
};
_OBJC_$_PROP_LIST_YAPerson
是YAPerson类的全局的属性列表。
方法
设计一个方法需要解决三个问题:方法名、方法实现以及方法的参数类型和返回值类型。
SEL
表征方法名的selector是SEL类型:typedef struct objc_selector *SEL;
,也即“Defines an opaque type that represents a method selector”。关于它的解释目前主要有两种看法:
一、有人根据Runtime源码中的定义和官方文档认为它是指向objc_selector结构体的指针,但是objc_selector结构体的具体实现并没有开源。
二、有人说它就是C语言中的字符串常量const char *
类型,理由是Runtime源码中sel_getName()的函数实现表明了SEL
类型可以直接转换为const char *
类型,而且“对于字符串的比较仅仅需要比较他们的地址就可以”,在各种查找中速度更快。
1 | const char *sel_getName(SEL sel) { |
我觉得这两种看法可以结合一下,SEL是指向objc_selector结构体的指针,objc_selector的实现应该是这样的:
1 | struct objc_selector { |
1.从结果上看,可以满足直接转换为const char *
类型:str的值就是“new”。从本质上看,指向结构体的指针就是指向结构体第一个成员(name数组)的指针。1
2
3
4
5
6
7
8// 创建结构体
objc_selector selector = {"new"};
// p为指向结构体的指针
objc_selector *p = &selector;
// 将 p 强制转化为 const char * 类型
const char *str = (const char *)p;
// 实际上str与结构体中的name是等价的
bool res = (str == p->name);
2.与SEL的内存表现是一致的。需要根据方法名的长度,创建相应长度的name数组。比如“new”方法,占用4个字节:6e 65 77 00
。
1 | objc_selector selector = {"new"}; |
看源码不难得知,相同名称对应的selector在内存中只有一个,存储在NXMapTable中。也就是说通过@selector()
语法糖、sel_registerName()
函数、NSSelectorFromString()
函数,都是从NXMapTable中取出selector。如果这个名称对应的selector已经注册在Runtime(存储在NXMapTable)中,直接返回,如果没有注册,才会进行创建。不同指针变量指向同一个结构体,这也解释了为啥可以使用==
运算符比较。
最后一个问题,为什么表征方法名的selector要设计成结构体而不是直接使用const char *
?
When using selectors, you must use the value returned from sel_registerName or the Objective-C compiler directive @selector(). You cannot simply cast a C string to SEL.
苹果希望我们使用诸如sel_registerName()这些转化函数,就是想让方法名与selector建立映射,同时把selector纳入管理。而不是像这样:
1 | SEL sel = (SEL)"show"; |
IMP
对于一般的函数来说,函数名也是指向函数实现的指针。1
2
3void func() {
printf("%s\n", __func__);
}
对于func函数,func()
是通过函数名调用,(*func)()
是通过指向函数的指针调用。在Objective-C中,IMP就是指向函数的指针。
标准的IMP定义是这样的:1
typedef id (*IMP)(id, SEL, ...);
在实际使用时,需要根据具体的参数、返回值重新定义:
1 | typedef NSString *(*_IMP)(id ,SEL, ...); |
Method
为了简便地描述类型、修饰符,字符串形式的类型编码产生了。const char *type = @encode(int)
在编译时便返回了字符串常量“i”,用来表明这是int类型。但是有一些像const
、inout
等方法参数的修饰符无法通过@encode()获取。对于方法来说,const char *method_getTypeEncoding(Method m)
函数可以直接返回方法完整的Type Encode。这里的Method是集成了方法名SEL、函数指针、类型编码的结构体指针。
1 | typedef struct method_t *Method; |
实例方法存放在类对象之中,这样,类对象的方法列表就产生了:1
2
3
4
5
6
7
8
9
10
11
12
13
14static struct {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct method_t method_list[6];
} _OBJC_$_INSTANCE_METHODS_YAPerson = {
sizeof(method_t),
6,
{{(struct objc_selector *)"singWithSongName:", "v24@0:8@16", (void *)_I_YAPerson_singWithSongName_},
{(struct objc_selector *)"name", "@16@0:8", (void *)_I_YAPerson_name},
{(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_YAPerson_setName_},
{(struct objc_selector *)"isMale", "B16@0:8", (void *)_I_YAPerson_isMale},
{(struct objc_selector *)"block", "@?16@0:8", (void *)_I_YAPerson_block},
{(struct objc_selector *)"setBlock:", "v24@0:8@?16", (void *)_I_YAPerson_setBlock_}}
};
类方法保存在元类对象之中,对于YAPerson,只有一个类方法canWork
,其方法列表是这样:1
2
3
4
5
6
7
8
9static struct {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct method_t method_list[1];
} _OBJC_$_CLASS_METHODS_YAPerson = {
sizeof(method_t),
1,
{{(struct objc_selector *)"canWork", "B16@0:8", (void *)_C_YAPerson_canWork}}
};
成员变量列表、属性列表、实例方法列表、类方法列表都已经有了,那么一个类的结构也就出来了。
类
class、meta-class、root meta-class的本质都是objc_class结构体,且都是以单例的形式表现在内存中。objc_class继承自objc_object,它的cache存储着缓存的方法列表,bits中的一些位存储着指向class_rw_t的指针。
1 | struct class_ro_t { |
YAPerson类对象的结构是这样的:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
const uint8_t * ivarLayout;
const char * name;
// baseMethodList、baseProtocols、ivars、baseProperties是一维数组,是只读的,包含了类的初始内容
method_list_t * baseMethodList; // 方法列表
protocol_list_t * baseProtocols; // 协议列表
const ivar_list_t * ivars; // 成员变量列表
const uint8_t * weakIvarLayout;
property_list_t *baseProperties; // 属性列表
} _OBJC_CLASS_RO_$_YAPerson = {
0,
// 计算 YAPerson 中的第一个成员变量在结构体中的偏移量
__OFFSETOFIVAR__(struct YAPerson, age),
sizeof(struct YAPerson_IMPL),
0,
"YAPerson",
(method_list_t *)&_OBJC_$_INSTANCE_METHODS_YAPerson,
0,
(const ivar_list_t *)&_OBJC_$_INSTANCE_VARIABLES_YAPerson,
0,
(property_list_t *)&_OBJC_$_PROP_LIST_YAPerson,
};
元类对象与类对象的结构是相同的,就YAPerson而言,主要存储了类方法列表,内容是这样:
1 | static struct class_ro_t _OBJC_METACLASS_RO_$_YAPerson = { |
二、协议
协议规定了协议遵守者需要实现的方法。如果在协议中声明一个属性,实际上相当于在协议中添加了该属性对应的存取器方法声明。对于协议遵守者,一般来说,需要合成该属性对应的成员变量:@synthesize 属性名;
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15@class YAStudent;
@protocol YAStudentProtocol <NSObject>
@required
// 实例属性
@property (nonatomic, assign) BOOL isExcellent;
// 类属性
@property (class, nonatomic, copy) NSString *protocolName;
// 实例方法
- (BOOL)isSame:(YAStudent *)person;
// 类方法
+ (NSString *)description;
@optional
// 可选的实例属性
@property (readonly, copy) NSString *debugDescription;
@end
上面声明的协议的对应的结构是这样的:
1 | struct protocol_t : objc_object { |
可以看到,协议主要存储了方法。协议的方法列表和类对象中的方法列表结构是一样的,但是有一点,协议方法列表中的方法Method没有具体实现IMP。也容易理解,毕竟协议只能提供方法名、方法参数和返回值。那么有没有可能为协议添加方法实现呢?有。因为,协议继承自objc_object,所以协议也是一个实例对象。当获取它的superclass时,发现就是NSObject。1
2
3
4
5
6Protocol *pro = objc_getProtocol("YAStudentProtocol");
(lldb) po [pro class]
Protocol
(lldb) po [pro superclass]
NSObject
那么,把它当做一个普通对象即可:1
2
3
4
5
6
7
8
9
10Method me = class_getInstanceMethod(YAStudent.class, @selector(show));
IMP imp = method_getImplementation(me);
const char *type = method_getTypeEncoding(me);
BOOL success = class_addMethod(NSClassFromString(@"Protocol"), @selector(show), imp, type);
if (success) {
// "创建"一个叫"YAStudentProtocol"的协议对象, 它的class是Protocol, 它的superclass是NSObject
Protocol *pro = objc_getProtocol("YAStudentProtocol");
// 调用协议对象的show方法
NSString *result = [(id)pro performSelector:@selector(show)];
}
虽然这么做意义并不是很大,但是脑洞再大一点,有没有可能只要遵守一个协议,那么就拥有了协议中的方法声明和方法实现呢?有。ProtocolKit解决了这个问题。
三、类扩展与分类
类扩展和分类都可以给已有的类添加功能,不同的是类扩展是编译时合并到类中,分类是在运行时合并到类中。
1 | struct category_t { |
每创建一个分类时,都会产生一个_category_t结构体:1
static struct category_t _OBJC_$_CATEGORY_YAStudent_$_HighSchoolStudent;
根据源码,分类中的属性、方法、协议等信息,合并到主类中的过程是这样的: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
63
64
65// Attach method lists and properties and protocols from categories to a class.
// Assumes the categories in cats are all loaded and sorted by load order,
// oldest categories first.
static void attachCategories(Class cls, category_list *cats, bool flush_caches) {
// 保证不为空
if (!cats) return;
// "log methods replaced by category implementations"
if (PrintReplacedMethods) printReplacements(cls, cats);
// 是否是元类对象
bool isMeta = cls->isMetaClass();
// 存储空间分配 fixme rearrange to remove these intermediate allocations
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists));
property_list_t **proplists = (property_list_t **)
malloc(cats->count * sizeof(*proplists));
protocol_list_t **protolists = (protocol_list_t **)
malloc(cats->count * sizeof(*protolists));
// Count backwards through cats to get newest categories first
int mcount = 0; // 方法数量
int propcount = 0; // 属性数量
int protocount = 0; // 协议数量
int i = cats->count; // 分类数量
bool fromBundle = NO;
while (i--) {
// 取出分类列表中的最后一个分类
auto& entry = cats->list[i];
// 获取方法列表
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
mlists[mcount++] = mlist;
fromBundle |= entry.hi->isBundle();
}
// 获取属性列表
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
proplists[propcount++] = proplist;
}
// 获取协议列表
protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
protolists[protocount++] = protolist;
}
}
auto rw = cls->data();
// 附加
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
rw->methods.attachLists(mlists, mcount);
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls);
rw->properties.attachLists(proplists, propcount);
free(proplists);
rw->protocols.attachLists(protolists, protocount);
free(protolists);
}
可以看到,分类信息是按照分类列表的逆序逐个进行合并,分类列表的顺序是由加载顺序确定的。方法、属性、协议数组等,都是二维数组,一维对应着分类,二维对应着每条信息。
附加过程是这样的: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
33void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) {
// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
// 重新分配newCount的内存
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
// 把addedLists放到老数据的前面, 形成新的array()->lists
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
}
else {
// 1 list -> many lists
List* oldList = list;
uint32_t oldCount = oldList ? 1 : 0;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)malloc(array_t::byteSize(newCount)));
array()->count = newCount;
// 把oldList放到addedLists的后面
if (oldList) array()->lists[addedCount] = oldList;
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
}
各种情况都有考虑,但是目的只有一个:把addedLists插入到原有数据的前面。这也解释了为什么分类中的方法与主类中的方法同名时,实际上会调用分类的。表面上看是“覆盖”,实际上方法都是存在的,只不过一旦在方法列表中找到需要调用的方法,就不再往下找了。
四、关联对象
由于内存布局在类定义后就已经固定,所有没有办法在分类中添加成员变量,但是相似的场景还是有的,这就需要用到关联对象。
设置关联对象的逻辑是这样的: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
53void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// retain the new value (if any) outside the lock.
ObjcAssociation old_association(0, nil);
// 如果修饰策略是OBJC_ASSOCIATION_SETTER_RETAIN, 则new_value = [value retain]
// 如果修饰策略是OBJC_ASSOCIATION_SETTER_COPY, 则new_value = [value copy]
id new_value = value ? acquireValue(value, policy) : nil;
AssociationsManager manager;
// 获取哈希表(AssociationsHashMap)的引用: associations
AssociationsHashMap &associations(manager.associations());
// 获取object的"按位取反值"
unsigned long disguised_object = DISGUISE(object); // ~(unsigned long)(object)
if (new_value) {
// 根据object的"按位取反值"作为key, 查找对应的iterator, 如未查找到,返回end()
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) { // object已经设置过关联对象, 则需要更新key对应的对象
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) { // 找到key对应的对象, 获取旧的对象, 设置新的对象
old_association = j->second;
j->second = ObjcAssociation(policy, new_value);
} else { // 没有找到key对应的对象, 这是一个新的key
(*refs)[key] = ObjcAssociation(policy, new_value);
}
} else { // 说明在AssociationsHashMap中, object目前还没有对应的ObjectAssociationMap, 也即是第一次设置关联对象, 则创建ObjectAssociationMap
ObjectAssociationMap *refs = new ObjectAssociationMap;
// 存储refs
associations[disguised_object] = refs;
// 在ref中存储关联策略和new_value
(*refs)[key] = ObjcAssociation(policy, new_value);
// 标明当前类具有关联类, 它会将isa中的has_assoc标记为true
object->setHasAssociatedObjects();
}
} else { // value为空, 说明是要删除原先的关联引用(一般也是这么用的, 而不是使用remove)
// 查找对应的iterator
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) { // 说明是有值的
// 获取值(first是键, second是值): ObjectAssociationMap
ObjectAssociationMap *refs = i->second;
// 根据参数key, 获取在refs中对应的iterator
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
// 获取旧值
old_association = j->second;
// 把参数key对应的对象擦除
refs->erase(j);
}
}
}
// 如果旧的关联对象不为空, 就把它release. release the old value (outside of the lock).
if (old_association.hasValue()) ReleaseValue()(old_association);
}
获取关联对象的逻辑是这样的: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
28id _object_get_associative_reference(id object, void *key) {
id value = nil;
uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
AssociationsManager manager;
// 获取AssociationsHashMap
AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object);
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// 找到ObjectAssociationMap
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
// 再找到ObjcAssociation
ObjcAssociation &entry = j->second;
// 取出关联对象和关联策略
value = entry.value();
policy = entry.policy();
// 策略是retain的话, 获取[value retain]
if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) ((id(*)(id, SEL))objc_msgSend)(value, SEL_retain);
}
}
if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
// 策略是autorelease的话, 获取[value autorelease]
((id(*)(id, SEL))objc_msgSend)(value, SEL_autorelease);
}
return value;
}
整体来看是比较简单的,有同学画了一幅图,很清晰地说明了存储层次:
关联对象的存储与类的存储没有关系。全局的AssociationsHashMap存储着所有对象与关联对象的映射关系,并由AssociationsManager管理。在AssociationsHashMap中,每个对象的“按位取反值”作为键,每个对象对应的ObjectAssociationMap作为值。在ObjectAssociationMap中,参数key作为键,关联的对象被包装成ObjcAssociation并作为值存储。
实际使用时,一般把getter方法的selector作为key,避免了新变量的产生;在移除关联对象时,常常使用objc_setAssociatedObject()函数,传入空的object,而不是使用objc_removeAssociatedObjects()函数直接把所有的关联对象都给移除。
一个对象在销毁之前,会自动移除它所有的关联对象。利用这个特性,可以实现不hook dealloc方法而在一个对象生命周期结束的时候触发一个操作。简单地说,就是利用一个中间对象,让它保存需要执行的操作,在其dealloc时执行操作。而把这个中间对象(或者保存中间对象的数组,亦或者是block)作为分类的关联属性,也算是一种思路吧。
最后就是很有意思的weak关联属性,这个在《Weak Associated Object》一文中专门做了思考与说明,巧妙地利用现成的weak关键字和block即可轻松实现。
五、消息机制
Objective-C中的实例方法调用和类方法调用都是基于消息机制的,通过发送消息而不是直接的函数调用使得这个语言非常具有动态性,这也是很有意思的。
假定YAStudent继承自YAPerson,YAPerson继承自NSObject。
以YAStudent为模板创建出一个student实例,它的实例方法大致是这么调用的:首先会先通过isa找到YAStudent的类对象,在类对象的方法列表找到目标方法,没有找到的话再通过superclass找到YAPerson的类对象,再在其方法列表中查找,没有找到的话再通过superclass找到NSObject的类对象,看是否能在其方法列表中找到并调用。
YAStudent的类方法大致是这么调用的:会先通过isa找到YAStudent的元类对象,在其方法列表中查找目标方法,如果没有找到再通过superclass找到YAPerson的元类对象,如果还没有找到就再通过superclass找到NSObject的元类对象,还是没有找到的话最后再通过isa找到NSObject类对象,看是否能在其方法列表中找到并调用。
实例对象的isa指向类对象,类对象的isa指向元类对象,元类对象的isa指向根元类,根元类的isa指向它自己。类对象的superclass指针指向父类的类对象,元类对象的superclass指针指向父类的元类对象,根元类的superclass指针指向根类(NSObject)。
由于根元类的superclass指针指向根类,这就解释了[NSObject show];
这样明明是类方法调用却成功变成了调用NSObject的show实例方法。1
2
3
4
5
6
7@interface NSObject (YAShow)
@end
@implementation NSObject (YAShow)
- (void)show {
NSLog(@"- (void)show");
}
@end
objc_msgSend
编译器会将方法的调用转换为objc_msgSend、objc_msgSend_stret、objc_msgSendSuper和objc_msgSendSuper_stret。
发送给对象的父类的消息会使用 objc_msgSendSuper;
有数据结构作为返回值的方法会使用 objc_msgSendSuper_stret 或 objc_msgSend_stret;
其它的消息都是使用 objc_msgSend 发送的。
objc_msgSend的具体实现由汇编语言编写而成,原因有两个:
一个C++函数不可能调用任意的函数指针,它可以重载、有可变参数,但是不可能有可变的返回值。
通过使用汇编,可以免去大量局部变量拷贝的操作,参数会直接被存放在寄存器中,当找到IMP时,参数已经保存在了寄存器中,可以直接使用,速度更快。
第一条的解释是这样的,假定objc_msgSend是使用C++实现,那么从语法层面它需要做到能接收可变参数、返回值类型可以任意。比如返回值类型可以是NSUInteger
也可以是id
:
1 | NSUInteger n = [array count]; |
可变参数可以做到,任意返回值类型貌似可以利用重载,但是仅仅返回值类型不同无法构成重载:1
2
3
4
5
6
7
8// 编译通不过
NSUInteger objc_msgSend(id, SEL, ...) {
return 0;
}
id objc_msgSend(id, SEL, ...) {
return nil;
}
而且就算这两条都做到了,也没有办法支撑无穷无尽的任意函数指针—不可能把所有函数指针一一穷举。
objc_msgSend的解决办法,主要依据的是:当objc_msgSend被调用时,所有的参数已经被设置好了。
换一种方式来说,就是:在objc_msgSend开始执行时,栈帧(stack frame)的状态、数据,和各个寄存器的组合形式、数据,跟调用具体的函数指针(IMP)时所需的状态、数据,是完全一致的!
1 | ENTRY _objc_msgSend |
消息转发
当一个方法没有相应的实现时,就会进入消息转发机制,在这套流程中,可以动态增加方法实现。
第一阶段
在动态方法解析中可以直接添加上函数实现。
比如调用YAPerson的instanceShow实例方法,当判断出这个selector是instanceShow
的时候,在resolveInstanceMethod
方法中为instanceShow
实例方法添加上函数实现instanceFunc
。而对于classShow类方法,在resolveClassMethod
方法中为classShow方法添加上函数实现classFunc
。
1 | // 实例方法 |
预先把函数实现都写好了,再通过class_addMethod()把实现添加到方法中,那何不如直接完整地把方法实现写了,比如上面的例子,直接把instanceShow实例方法和classShow类方法完整地写出来不就行了?何必多此一举。
实际上,这一阶段常是配合@dynamic
来使用。peerassembly举了一个还不错的例子:
假定需要通过Preferences接管NSUserDefaults,由它提供各个key的存取接口,那一般需要这么做:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20@interface Preferences : NSObject {
NSUserDefaults *_defaults;
}
+ (Preferences *)sharedInstance;
@property (nonatomic, assign) BOOL autoStartBreak;
// ... 还有好多属性
@end
@implementation Preferences
- (BOOL)autoStartBreak {
return [_defaults boolForKey:@"autoStartBreak"];
}
- (void)setAutoStartBreak:(BOOL)autoStartBreak {
[_defaults setBool:autoStartBreak forKey:@"autoStartBreak"];
}
// ... 写好多好多
@end
需要对每一个属性都写上存取方法的实现,如果有20个属性,岂不是要写40个方法?
要是借助resolveInstanceMethod()
,就可以这么办: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@interface Preferences : NSObject
@property (nonatomic, assign) BOOL autoStartBreak;
@property (nonatomic, assign) BOOL iaFromSmartApp;
@property (nonatomic, assign) BOOL isDarkMode;
// ... 还有好多属性
@end
static NSMutableDictionary *_dynamicProperties;
@implementation Preferences
@dynamic autoStartBreak, iaFromSmartApp, isDarkMode;
BOOL paprefBoolGetter(id self, SEL _cmd) {
NSString *selectorString = NSStringFromSelector(_cmd);
PAPropertyDescriptor *propertyDescriptor = _dynamicProperties[selectorString];
return [NSUserDefaults.standardUserDefaults boolForKey:propertyDescriptor.name];
}
void paprefBoolSetter(id self, SEL _cmd, BOOL value) {
NSString *selectorString = NSStringFromSelector(_cmd);
PAPropertyDescriptor *propertyDescriptor = _dynamicProperties[selectorString];
[NSUserDefaults.standardUserDefaults setBool:value forKey:propertyDescriptor.name];
[NSUserDefaults.standardUserDefaults synchronize];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSString *selectorString = NSStringFromSelector(sel);
PAPropertyDescriptor *propertyDescriptor = [PAPropertyDescriptor new];
if ([selectorString hasPrefix:@"set"]) { // setAutoStartBreak -->autoStartBreak
NSString *first = [selectorString substringWithRange:NSMakeRange(3, 1)];
NSString *prop = [selectorString substringWithRange:NSMakeRange(4, selectorString.length - 5)];
propertyDescriptor.name = [first.lowercaseString stringByAppendingString:prop];
class_addMethod(self.class, sel, (IMP)paprefBoolSetter, "v@:B");
} else {
propertyDescriptor.name = selectorString;
class_addMethod(self.class, sel, (IMP)paprefBoolGetter, "B@:");
}
if (!_dynamicProperties[selectorString]) {
_dynamicProperties[selectorString] = propertyDescriptor;
}
return YES;
}
@end
通过建立映射,把所以存取器都统一纳入管理,更清晰高效。上面的只是一个示例,还要考虑许多问题,比如支持不同类型的属性、替换默认的方法实现等,这个作者已经写了一个完整的组件,可参考PAPreferences。
第二阶段
将消息转发给其他target。
第一阶段中无法及时添加上相应的方法实现,就会进入第二阶段,我们可以在这里把消息转发给其他对象处理。如果是实例方法,需要实现- (id)forwardingTargetForSelector:(SEL)aSelector
,如果是类方法,则需要实现+ (id)forwardingTargetForSelector:(SEL)aSelector
。
假定YAPerson的instanceShow实例方法没有具体的实现,而YASinger却是有的,那可以把消息转发给YASinger实例对象,交由它处理:1
2
3- (id)forwardingTargetForSelector:(SEL)aSelector {
return [YASinger new];
}
假定YADancer有classShow类方法的实现,那可以把消息转发给YADancer类对象:1
2
3+ (id)forwardingTargetForSelector:(SEL)aSelector {
return YADancer.class;
}
实际上,把YAPerson的消息转发给其他对象(实例对象或者类对象)之后,其他对象就会开启新一轮的消息解析,它如果没有对应的方法实现,同样会开启这三个阶段。因此,其他对象不一定要完完整整地具备YAPerson所缺省的方法实现。
第三阶段
完整的消息转发。
如若前两个阶段都没办法妥善地处理,就会进入消息解析的第三阶段。在这个阶段中,使用forwardInvocation
配合methodSignatureForSelector
对消息做最后一步的处理。
首先会解析methodSignatureForSelector,只有当它返回的方法签名不为空时,才会进入forwardInvocation流程中,因此我们对该方法重写并返回一个不为空的signature。1
2
3
4
5
6
7
8
9- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
if (!signature) {
if ([YAMethodHelper instancesRespondToSelector:aSelector]) {
signature = [YAMethodHelper instanceMethodSignatureForSelector:aSelector];
}
}
return signature;
}
接着在forwardInvocation中获取创建的NSInvocation对象,调用selector即可:1
2
3
4
5- (void)forwardInvocation:(NSInvocation *)anInvocation {
if ([YAMethodHelper instancesRespondToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:_helper];
}
}
这是很传统的处理步骤,实际上只要保证methodSignatureForSelector返回一个不为空的方法签名,可以在forwardInvocation中对NSInvocation对象做肆意更改。
1 | - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { |
当然,前提是这么做有意义。
其实在这一阶段还可以实现伪多继承: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- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSMethodSignature *first = [(NSObject *)self.firstDelegate methodSignatureForSelector:aSelector];
NSMethodSignature *second = [(NSObject *)self.secondDelegate methodSignatureForSelector:aSelector];
if (first){
return first;
} else if(second) {
return second;
}
return nil;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
SEL aSelector = [anInvocation selector];
if([self.firstDelegate respondsToSelector:aSelector]){
[anInvocation invokeWithTarget:self.firstDelegate];
}
if([self.secondDelegate respondsToSelector:aSelector]){
[anInvocation invokeWithTarget:self.secondDelegate];
}
}
- (BOOL)respondsToSelector:(SEL)aSelector{
if([self.firstDelegate respondsToSelector:aSelector] || [self.secondDelegate respondsToSelector:aSelector]){
return YES;
} else {
return NO;
}
}
NSObject的forwardInvocation:方法实现只是简单调用了doesNotRecognizeSelector:方法,它不会转发任何消息。这样,如果不在以上所述的三个步骤中处理未知消息,则会引发一个异常。
方法缓存
为了加快方法查找速度,方法缓存产生了。调用过的方法会被缓存起来,如果方法是属于父类的,也会把方法缓存在自己的cache中。
1 | struct cache_t { |
1 | struct bucket_t { |
方法查找过程:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23// Selector转换为cache_key_t(unsigned long类型)
cache_key_t getKey(SEL sel) {
assert(sel);
return (cache_key_t)sel;
}
bucket_t *cache_t::find(cache_key_t k, id receiver) {
assert(k != 0);
bucket_t *b = _buckets;
mask_t m = _mask;
mask_t begin = (mask_t)(k & m);
mask_t i = begin;
do {
if (b[i].key() == 0 || b[i].key() == k) {
return &b[i];
}
} while ((i = ((i+1) & m)) != begin);
// hack
Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
cache_t::bad_cache(receiver, (SEL)k, cls);
}
散列表的索引通过index = selector & _mask
获得。
六、block
结构
block的本质是__main_block_impl_0
结构体,它拥有isa指针,是一个封装了函数调用以及函数调用环境的Objective-C对象。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// 函数实现以及isa
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
// 一些描述信息
static struct __main_block_desc_0 {
// 0
size_t reserved;
// block大小
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
// 完整定义
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// 构造函数
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
定义一个block时,会调用__main_block_impl_0
的构造函数,主要是把需要执行的函数地址赋值给FuncPtr指针。
1 | // 函数调用的第一个参数是指向block自己的指针,之后的才是自定义的参数 |
调用block也即执行FuncPtr函数。
1 | // 一个int型参数、一个double型参数 |
类型
从代码段(text区)、数据段(data区)、堆区再到栈区,内存地址逐渐增高。block有三种类型,分别分布在数据段、栈区和堆区。
没有访问auto变量的block是Global类型,继承关系链为:__NSGlobalBlock__
,__NSGlobalBlock
,NSBlock
,NSObject
。比如:1
void (^block)(void) = ^(void) { };
访问了auto变量的block是Stack类型,继承关系链为:__NSStackBlock__
,__NSStackBlock
,NSBlock
,NSObject
。比如:1
2
3
4int age = 10;
void (^block)(void) = ^(void) {
NSLog(@"名字是:%d", age);
};
当__main_block_impl_0
的构造函数调用完毕,栈上的该结构体变量销毁,那取出该结构体的指针,再访问数据就不会是正确的数据了。这时候为了保证访问数据的正确性,需要把block放到堆空间,手动管理该结构体变量的生命周期。
Global类型的block调用copy方法后,它依然是Global类型。Stack类型的block调用copy方法后,就成为Malloc类型,继承关系链为:__NSMallocBlock__
,__NSMallocBlock
,NSBlock
,NSObject
。比如:1
2
3
4int age = 10;
void (^block)(void) = [^(void) {
NSLog(@"名字是:%d", age);
} copy];
而Malloc类型的block调用copy方法后,引用计数增加1。
在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况,block作为函数返回值时、将block赋值给__strong
指针时、block作为Cocoa API中方法名含有usingBlock的方法参数时、block作为GCD API的方法参数时。
请格外注意将block赋值给__strong
指针时这句话,因为绝大多数我们都是使用strong指针引用一个block的,也因此,绝大多数情况下使用的block都是__NSMallocBlock
类型。1
2
3
4
5// __weak指针指向的,是__NSStackBlock类型。但默认都是strong指针
int num = 10;
__weak void (^myBlock)(void) = ^(void) {
int newNum = num;
};
捕获变量
一般来说,当block捕获外界的auto变量时,该变量会作为block对象的一个成员变量存储,是值传递;
当block捕获外界的static变量(非全局变量)时,block对象会增加一个存储该static变量的指针的成员变量,是指针传递;
对于全局变量(无论是否static修饰),block会直接访问,不进行捕获。
1 | // 全局变量 |
特殊地,self作为隐式参数,是局部变量,所以block访问时会进行捕获。而对于直接访问某个对象的成员变量,比如:1
void (^block)(void) = ^(void) { NSLog(@"名字是:%@", _name); };
实际上是首先访问self,接着访问self的成员变量_name
,因此,也会对self进行捕获。
尤其需要说明的是,对于一般的auto变量来说,捕获基本数据类型的自动变量的方式是const copy。而当block内部访问了对象类型的auto变量时,如果block是在栈上,将不会对auto变量产生强引用。如果block被拷贝到堆上,会调用block内部的copy函数,copy函数内部会调用_Block_object_assign
函数,_Block_object_assign
函数会根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)
做出相应的操作,形成强引用(retain)或者弱引用。如果block从堆上移除,会调用block内部的dispose函数,dispose函数内部会调用_Block_object_dispose
函数,_Block_object_dispose
函数会自动释放引用的auto变量(release)。
__block
对于__block
修饰的auto变量(含基本数据类型和对象类型),block捕获时会将该变量包装成新的对象:1
2
3
4
5
6
7
8
9
10
11
12
13NSObject *obj = [NSObject new];
__block int age = 22;
__block NSObject *strongObj = obj;
__block __weak NSObject *weakObj = obj;
void (^myBlock)(void) = ^(void) {
age = 23;
strongObj = nil;
weakObj = nil;
};
myBlock();
// 再次访问
strongObj = [NSObject new];
age = 24;
实际上是这样:
1 | // 把age包装成新的对象 |
而从这一刻开始,再在block中访问该auto变量(age、strongObj、weakObj),实质上是访问包装成新的对象内部的同名变量。也可以说,__block
所起到的作用就是只要观察到该变量被block所持有,就将“外部变量”在栈中的内存地址放到了堆中。
1 | __block int age = 22; |
利用类型转换,打印变量age的地址与结构体age_imp中的age成员的地址,发现是一致的,也印证了这一点:__block
修饰的age就是“不可见”的新结构体内部的age。
这个“被包装的新的对象”的内存管理也需要考虑,当block在栈上时,并不会对这个“新的对象”产生强引用。当block被copy到堆时,会对“新的对象”产生强引用。对于基本数据类型的包装并不需要赘述,而对于对象类型的包装倒值得一提。“新的对象”内部的strongObj以及weakObj的修饰策略很清晰地表明了它对原来对象内存管理的立场:原来是strong,包装后依然是strong;原来是weak,包装后依然是weak。
对于__block
修饰的变量进行装箱也很容易理解,倘若考虑把被修改的auto变量的指针传递给block,这样似乎也能达到修改变量的效果,但是不应该忘记,被修改的auto变量的生命周期是不确定的,假如这个变量很快就销毁,过了很久block才得到执行,这时再去通过指针是无法访问到这个变量的,这种访问方式也很危险。而包装成一个新的对象后,这个对象的生命周期就随着block了,block无论在栈上还是在堆上,都没有关系。
__forwarding
不难发现,当把__block
修饰的变量包装成对象之后,其结构体成员有一个__forwarding
指针。
实际上,再访问__block
修饰的变量时就成了这样:1
2
3// 再次访问
(strongObj.__forwarding->strongObj) = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("new"));
(age.__forwarding->age) = 24;
有点奇怪,既然原先的变量被包装成对象了,那不是应该这样访问吗?1
2strongObj.strongObj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("new"));
age.age = 24;
直接访问对象内部的成员不就好了,为啥还需要个__forwarding指针
?
假定这个block需要被成员变量持有,那么它就需要被copy到堆上而不能过了作用域就销毁。
下面是MRC中的情况:1
2
3
4
5
6
7
8
9- (void)desc {
__block int age = 22; // (1)
age += 5;
void (^myBlock)(void) = [^(void) {
age = 23; // (2)
} copy]; // 调用copy方法,将其copy到堆上
myBlock();
age = 24; // (3)
}
第(1)行中最开始创建的作为局部变量的age对象(也就是包装age变量的新对象__Block_byref_age_0
),是在栈上。第(2)行在myBlock内部访问的age对象随block被copy到堆上也被copy到堆上。第(3)行访问的age(对象)也是在堆上。
这种情况就存在访问栈上的age变量和访问堆上的age变量两种情况,因此单单使用age.age = 24;
会使得这两个变量分离开(不是同一个变量了),逻辑产生错误,因此需要__forwarding
指针来帮助能正确的访问__block
变量。
__forwarding
指针怎么做到的呢?
当block变量从栈到copy到堆上时,栈上的__forwarding
指针被修改为指向堆上的age变量,使得无论是在栈上还是堆上都能访问到同一个block变量。
也即:
1,当block在栈上时,栈上的age对象(__Block_byref_age_0
结构体)内部的__forwarding
指针指向栈上的age对象自己。
2,当block被copy到堆上时,栈上的age对象内部的__forwarding
指针指向被copy到堆上的age对象,而堆上的age对象内部的__forwarding
指针也指向堆上的age对象。
这样便做到了始终访问的是同一个__block
变量。这也是为啥始终需要通过__forwarding
指针来访问,也即__forwarding
指针正是用来解决__block
变量拷贝到堆上还能被正确访问的这件事。
属性修饰
一般来说,只有当栈上的block作为方法的参数时,需要我们手动调用block的copy方法,将栈上的block复制到堆上,以避免坏内存问题。在ARC时代,如果block作为属性,无论是copy修饰还是strong修饰都是可以的。对于getter方法,由于block是作为方法的返回值,则它会自动被copy到堆空间,不需要我们关心。对于setter方法,block是作为方法的参数,如果是copy修饰,栈上的block自然会被调用copy方法而被复制到堆空间。如果是strong修饰,那么当把栈上的block赋值给 __strong id
类型的对象,也是会被调用copy方法复制到堆空间的。
即便如此,还是推荐使用copy修饰,这样能时刻提醒我们这个block将来会被安全地copy到堆空间上。但是,如果是使用weak
、assign
、unsafe_unretained
这些另类的修饰符修饰block,那很轻而易举地就会出问题了。
七、小结
halfrost同学概括了与Runtime有关的应用大概是这些,也是比较全面了。
- 实现多继承(消息转发)
- Method Swizzling
- Aspect Oriented Programming(比如打点)
- Isa Swizzling(KVO的动态派生)
- Associated Object关联对象
- 动态地增加方法
- NSCoding的自动归档和自动解档
- 字典和模型互相转换(KVC也可,利用IMP也行)
这篇文章主要总结了Objective-C“对象”相关的基础知识,最后还提了一下这门语言特有的block。不知道有没有把开头的那道问题回答好(捂脸),写了好久,会经常回顾并完善。
参考感谢:
《方法与消息》
《用代码理解 ObjC 中的发送消息和消息转发》
《如何正确使用 Runtime》
《为什么objc_msgSend()是用汇编实现的》
《通过汇编解读-objc_msgSend》