0%

基本认识

block本质上是一个封装了函数调用以及函数调用环境的OC对象,内部也包含一个isa指针。

block的底层实现

简单的block底层实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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;
}
};

struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr; //函数指针
};

static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size; //内存占用
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

变量捕获-基础数据类型

auto变量

定义如下一个捕获auto类型变量(离开作用域自动销毁)的block:

1
2
3
4
int age = 10;
void (^block)(void) = ^(){
NSLog(@"%d",age);
};

将其转换为c++代码:

1
2
int age = 10;
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));

block底层结构为:

1
2
3
4
5
6
7
8
9
10
11
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

可见,block在捕获auto变量的方式为值传递。

static变量

定义如下一个捕获static类型变量的block:

1
2
3
4
static int age = 10;
void (^block)(void) = ^(){
NSLog(@"%d",age);
};

将其转换为c++代码:

1
2
static int age = 10;
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &age));

block底层结构为:

1
2
3
4
5
6
7
8
9
10
11
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *age;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

可见,block在捕获static变量的方式为指针传递。

全局变量

定义如下一个捕获全局变量的block:

1
2
3
4
//age此时被定义为全局变量
void (^block)(void) = ^(){
NSLog(@"%d",age);
};

将其转换为c++代码:

1
2
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

block底层结构为:

1
2
3
4
5
6
7
8
9
10
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捕获。

总结

变量捕获-对象类型

当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的底层相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->person, (void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src)
{
_Block_object_dispose((void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

block的类型

blcok有三种类型,都继承自NSBlock。可以通过class方法或者isa指针查看其具体类型。

  • __NSGlobalBlock__ (_NSConcreteGlobalBlock) :没有访问auto变量
  • __NSStackBlock__ (_NSConcreteStackBlock : 访问了auto变量
  • __NSMallocBlock__ (_NSConcreteMallocBlock): __NSStackBlock__调用了copy方法

下面代码的输出结果为:__NSGlobalBlock__

1
2
3
4
5
void (^block)(void) = ^(){
NSLog(@"Hello World");
};
NSLog(@"%@",[block class]);

下面代码ARC环境下的输出结果为:__NSMallocBlock__,MRC的输出结果为:__NSStackBlock__

1
2
3
4
5
6
int age = 10;
void (^block)(void) = ^(){
NSLog(@"%d",age);
};
NSLog(@"%@",[block class]);

下面代码的输出结果为:__NSStackBlock__

1
2
3
4
int age = 10;
NSLog(@"%@",[^{
NSLog(@"%d",age);
} class]);

三种类型block的内存存储以及每一种类型的block调用copy后的结果如下所示:

__block修饰符

底层实现

__block可以用于解决block内部无法修改auto变量值的问题。__block不能修饰全局变量、静态变量(static)。

1
2
3
4
5
6
7
__block int age = 10;
__block NSObject *obj = [[NSObject alloc] init];

void (^block)(void) = ^{
obj = nil;
age = 20;
};

上面的代码会被编译为:

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
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_obj_1 *obj; // by ref
__Block_byref_age_0 *age; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};

struct __Block_byref_obj_1 {
void *__isa;
__Block_byref_obj_1 *__forwarding;
int __flags;
int __size;
//内存管理
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSObject *obj;
};

//实际执行的block代码
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_obj_1 *obj = __cself->obj; // bound by ref
__Block_byref_age_0 *age = __cself->age; // bound by ref

(obj->__forwarding->obj) = __null;
(age->__forwarding->age) = 20;
}

可见,__block修饰的变量会被包装成一个对象。

__block变量的内存管理

当block在栈上时,并不会对__block变量产生强引用。

当block被copy到堆时:

  • 会调用block内部的copy函数
  • copy函数内部会调用_Block_object_assign函数
  • _Block_object_assign函数会对__block变量形成强引用(retain)[仅ARC, MRC不会retain]

当block从堆中移除时:

  • 会调用block内部的dispose函数
  • dispose函数内部会调用_Block_object_dispose函数
  • _Block_object_dispose函数会自动释放引用的__block变量(release)

__forwarding指针

为什么会指向自己呢,原因是为了确保当栈中的Block复制到堆中的时候,在栈中仍然能正确访问堆中的变量。

循环引用

循环引用产生的原因:

解决循环引用:

  • ARC: __weak、__unsafe_unretained
  • MRC: __block

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

调用顺序:先调用父类的+initialize,再调用子类的+initialize,每个类只会初始化1次

+initialize方法相关源码阅读顺序:

  1. objc-msg-arm64.s

    • objc_msgSend
  2. objc-runtime-new.mm

    • class_getInstanceMethod
    • lookUpImpOrNil
    • lookUpImpOrForward
    • _class_initialize
    • callInitialize
    • objc_msgSend(cls, SEL_initialize)

+initialize和+load的最大区别是,+initialize是通过objc_msgSend进行调用的,所以有以下特点:

  • 如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被调用多次)
  • 如果分类实现了+initialize,就覆盖类本身的+initialize调用

+load方法会在runtime加载类、分类时调用。每个类、分类的+load,在程序运行过程中只调用一次。

调用顺序:

  1. 先调用类的+load

    • 按照编译先后顺序调用(先编译,先调用)
    • 调用子类的+load之前会先调用父类的+load
  2. 再调用分类的+load

    • 按照编译先后顺序调用(先编译,先调用)

为什么原本类的+load方法不会被分类的+load方法覆盖呢?这是由于+load方法是根据方法地址直接调用,并不是经过objc_msgSend函数调用。

+load方法相关源码阅读顺序:

  1. _objc_init
  2. load_images
  3. prepare_load_methods
    • schedule_class_load
    • add_class_to_loadable_list
    • add_category_to_loadable_list
  4. calls
    • call_class_loads
    • call_category_loads
    • (*load_method)(cls, SEL_load)

Category的底层结构

分类底层结构定义的如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//objc-runtime-new.h

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;
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;

method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}

property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

项目中每定义一个分类,底层都会增加一个category_t对象。

Category的加载过程

Category源码阅读顺序:

  1. objc-os.mm (runtime入口)
    • _objc_init (runtime初始化)
    • map_images
    • map_images_nolock
  2. objc-runtime-new.mm
    
    • _read_images
    • remethodizeClass
    • attachCategories
    • attachLists
    • realloc、memmove、 memcpy

category的加载过程:

  1. 通过runtime加载类的所有分类
  2. 将所有分类的方法,属性,协议分别合并到一个数组
  3. 将合并后的分类数据插入到类原来到数据之前

由源码可见,对同名方法而言,会优先调用分类中的方法。如果多个分类中包含同名方法,则会调用最后参与编译的分类中的方法。

摘录源码中核心的attachCategories实现如下(objc4-756.2):

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
66
67
// 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;
if (PrintReplacedMethods) printReplacements(cls, cats);

bool isMeta = cls->isMetaClass();

// fixme rearrange to remove these intermediate allocations
//方法二维数组
//[[method_t,method_t],[method_t,method_t,method_t]]
//二维数组中的一个元素(数组)存放一个分类中的方法列表
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;
}
}
//取出(元)类对象中的数据(class_rw_t)
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);
}

Category和Extension的区别

  • 类扩展中的信息在编译时会合并到类信息中。
  • 分类中的信息在运行时才会合并到类信息中。

KVC的全称是Key-Value Coding,即“键值编码”,可以通过指定的key来访问相应的对象属性。

常见的API有:

    • (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
    • (void)setValue:(id)value forKey:(NSString *)key;
    • (id)valueForKeyPath:(NSString *)keyPath;
    • (id)valueForKey:(NSString *)key;

key和keyPath的区别在于:

  • key:只能接受当前类所具有的属性,不管是自己的,还是从父类继承过来的;
  • keypath: 除了能接受当前类的属性,还能接受当前类属性的属性,即可以接受关系链;

KVC设值原理:

KVC取值原理:

需要注意的是,通过KVC修改对象属性的值,无论是否调用了setKey方法,始终都会触发KVO。这是因为KVC修改对象属性的值过程中会调用对象的willChangeValueForKey:didChangeValueForKey:方法,只要调用了这两个方法,就会触发KVO。

KVO全称为Key-Value Observing, 即键值监听,用于指定对象属性值的改变。

定义如下一个Person类:

1
2
3
@interface Person : NSObject
@property (assign, nonatomic) int age;
@end

给Person类的instance对象添加KVO:

1
2
3
4
// 给person1对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person addObserver:self forKeyPath:@"age" options:options context:nil];

当给Person的instance对象添加KVO监听后,Runtime会动态创建一个名为NSKVONotifying_Person的Person子类,并将instance对象的isa指针指向这个子类Class。

NSKVONotifying_Person类的setAge:方法的伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)setAge:(int)age
{
//调用Foundation框架的_NSSet***ValueAndNotify方法,与具体参数类型有关
_NSSetIntValueAndNotify();
}

void _NSSetIntValueAndNotify()
{
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
}

- (void)didChangeValueForKey:(NSString *)key
{
// 通知监听器,属性值发生了改变
[oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
}

其他方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 屏蔽内部实现,隐藏了NSKVONotifying_Person类的存在
- (Class)class
{
return [Person class];
}

- (void)dealloc
{
// 收尾工作
}

- (BOOL)_isKVOA
{
return YES;
}

由此可见,直接修改成员变量的值不会触发KVO。

本文使用的objc源码版本为objc4-756.2

Class本质上为一个结构体类型:

1
2
typedef struct objc_class *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
36
37
38
39
40
41
42
43
44
45
46
47
48
struct objc_object {
private:
isa_t isa;
//以下省略
}

struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // 方法缓存
class_data_bits_t bits; // 类的具体信息
class_rw_t *data() {
return bits.data();
}
//以下省略
}

struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;

const class_ro_t *ro;

method_array_t methods; //方法列表
property_array_t properties; //属性信息
protocol_array_t protocols; //协议列表
}

struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize; //instance对象占用的内存大小
#ifdef __LP64__
uint32_t reserved;
#endif

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;
}

isa指针

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

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

1
2
3
4
5
//调用对象方法
objc_msgSend(obj,sel_registerName("objMethodName"))

//调用类方法
objc_msgSend(objc_getClass("className"), sel_registerName("classMethodName"))

superclass指针

假如存在以下继承关系: Stundent->Person->NSObject。

当Student的instance对象要调用Person的对象方法时,会先通过isa找到Student的class,然后通过superclass找到Person的class,最后找到相应的对象方法进行调用。

当Student的class要调用Person的类方法时,会先通过isa找到Student的meta-class,然后通过superclass找到Person的meta-class,最后找到类方法的实现进行调用。

总结

  • instance的isa指向class
  • class的isa指向meta-class
  • meta-class的isa指向基类的meta-class
  • class的superclass指向父类的class,如果没有父类,superclass指针为nil
  • meta-class的superclass指向父类的meta-class,基类的meta-class的superclass指向基类的class
  • instance调用对象方法的轨迹:isa找到class,方法不存在,就通过superclass找父类
  • class调用类方法的轨迹:isa找meta-class,方法不存在,就通过superclass找父类

OC中的对象,主要可以分为三类:

instance对象

instance对象(实例对象)就是通过类alloc出来的对象,每次类调用alloc都会生成一个新的instance对象。instance对象在内存中存储的主要信息有:

  • isa指针
  • 其他类的成员变量(值)
1
2
NSObject *obj1 = [[NSObject alloc] init];
NSObject *obj2 = [[NSObject alloc] init];

obj1和obj2就是两个NSObject的实例对象,分别占据两块的不同的内存。

class对象

每个类在内存中有且只有一个class对象(类对象),class对象在内存中存储的主要信息有:

  • isa指针
  • superclass指针
  • 类的属性信息(@property)
  • 类的对象方法信息(instance method)
  • 类的协议信息(protocol)
  • 类的成员变量信息(类型,名称)

下面代码获取到的都是NSObject类对象:

1
2
3
4
5
6
7
8
NSObject *obj1 = [[NSObject alloc] init];
NSObject *obj2 = [[NSObject alloc] init];
Class objClass1 = [obj1 class];
Class objClass2 = [obj2 class];
Class objClass3 = [NSObject class];
Class objClass4 = object_getClass(obj1);
Class objClass5 = object_getClass(obj2);
Class objClass6 = objc_getClass("NSObject");

meta-class对象

每个类在内存中有且只有一个meta-class对象(元类对象)。meta-class对象和class对象的内存结构是一样的,但是用途不一样。meta-class对象在内存中存储的主要信息有:

  • isa指针
  • superclass指针
  • 类方法信息(class method)
  • ……

下面代码获取到的metaClass就是NSObject的meta-class对象:

1
Class metaClass = object_getClass([NSObject class]);

可以通过class_isMetaClass方法判断一个Class对象是否为meta-class对象。

需要注意的是,以下代码获取到的是Class对象,并不是meta-class对象:

1
2
Class objClass = [[NSObject class] class];

注意点

  1. Class objc_getClass(const char *aClassName)
  • 传入字符串类名
  • 返回对应的类对象
  1. Class object_getClass(id obj)
  • 传入的obj可能是instance对象、class对象、meta-class对象
  • 如果传入的是instance对象,返回class对象
  • 如果传入的是class对象,返回meta-class对象
  • 如果传入的是meta-class对象,返回NSObject(基类)的meta-class对象
  1. -(Class)class、+ (Class)class 返回的就是类对象

定义如下一个类:

1
2
3
4
5
6
7
8
@interface Person : NSObject
@property (nonatomic,assign) int height;
@property (nonatomic,assign) int age;
@property (nonatomic,assign) int no;
@end

@implementation Person
@end

将其转换为C++代码:

1
2
3
4
5
6
7
8
9
10
11
struct NSObject_IMPL {
Class isa;
};

struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS; //8个字节
int _height; //4个字节
int _age; //4个字节
int _no; //4个字节
};

根据分析可知,Person_IMPL结构体内所有成员的总大小为20个字节,但由于内存对齐的原因,Person_IMPL结构体占用的内存大小应为24个字节。

实际上,使用class_getInstanceSize打印的结果为24,但是使用malloc_size打印的结果为32. 可见,Person实例对象实际占用的内存大小为32字节,尽管其只使用了24个字节。

alloc方法底层调用的方法为allocWithZone, 而allocWithZone方法则会调用calloc函数分配内存. 相关的源码可以在https://opensource.apple.com/tarballs/libmalloc/进行下载。

根据源码可以发现,系统为了优化内存的使用效率,规定iOS中分配的内存大小都必须为16的倍数.