Objective-C 语言中,block3 种类型,可以通过调用 class 方法或者 isa 指针查看具体类型,最终都是继承自 NSBlock 类型

  • _NSConcreteGlobalBlock 全局的静态 block,不会访问任何外部变量,它是设置在程序的数据区域(.data区)中。
  • _NSConcreteStackBlock 保存在栈中的 block,当函数返回时会被销毁,超出变量作用域,栈上的 block 以及 __block 变量都被销毁。
  • _NSConcreteMallocBlock 保存在堆中的 block,当引用计数为 0 时会被销毁,在变量作用域结束时不受影响。

图示

  • 没有访问 auto 变量,变成 _NSConcreteGlobalBlock
  • 不在 ARC 环境下,访问了 auto 变量,变成 _NSConcreteStackBlock,如果在 ARC 环境下,访问了 auto 变量,变成 _NSConcreteMallocBlock
  • __NSStackBlock__ 调用了 copy,变成 _NSConcreteMallocBlock

每一种类型的 block 调用 copy 后的结果:
图示
图示
图示

什么情况下 ARC 会自动将 block 进行一次 copy 操作

ARC 的环境下,block 默认是从栈区 copy 到堆区。

  • block 作为函数返回值时;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    typedef void (^Block)(void);
    Block myblock()
    {
    int a = 10;
    // 上文提到过,block中访问了auto变量,此时block类型应为__NSStackBlock__
    Block block = ^{
    NSLog(@"---------%d", a);
    };
    return block;
    }
    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    Block block = myblock();
    block();
    // 打印block类型为 __NSMallocBlock__
    NSLog(@"%@",[block class]);
    }
    return 0;
    }

    上文提到过,不在 ARC 环境下,访问了 auto 变量,变成 _NSConcreteStackBlock,如果在 ARC 环境下,访问了 auto 变量,变成 _NSConcreteMallocBlock;那么说明 ARC 环境下,在 block 作为函数返回值时会自动帮助我们对 block 进行 copy 操作,以保存 block,并在适当的地方进行 release 操作。

  • block 赋值给 __strong 指针时;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    // block内没有访问auto变量
    Block block = ^{
    NSLog(@"block---------");
    };
    NSLog(@"%@",[block class]);
    int a = 10;
    // block内访问了auto变量,但没有赋值给__strong指针
    NSLog(@"%@",[^{
    NSLog(@"block1---------%d", a);
    } class]);
    // block赋值给__strong指针
    Block block2 = ^{
    NSLog(@"block2---------%d", a);
    };
    NSLog(@"%@",[block1 class]);
    }
    return 0;
    }

    block 被强指针引用时,ARC 环境下也会自动对 block 进行一次 copy 操作。

  • block 作为 Cocoa API 中方法名含有 usingBlock 的方法参数时;

    1
    2
    3
    4
    NSArray *array = @[];
    [array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

    }];
  • block 作为 GCD API 的方法参数时;

    1
    2
    3
    4
    5
    6
    7
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

    });
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

    });

ARC 的环境下,为了解决栈块在其变量作用域结束之后被废弃(释放)的问题,我们需要把 block copy 到堆中,延长其生命周期。大多数情况下编译器会恰当地进行判断是否有需要将 block 从栈复制到堆,如果有,自动生成将 block 从栈上复制到堆上的代码。 block 的复制操作执行的是 copy 实例方法。block 只要调用了 copy 方法,栈块就会变成堆块。

block 对对象变量的捕获

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef void (^Block)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
Block block;
{
Person *person = [[Person alloc] init];
person.age = 10;

block = ^{
NSLog(@"------block内部%d",person.age);
};
} // 执行完毕,person没有被释放
NSLog(@"--------");
} // person 释放
return 0;
}

大括号执行完毕之后,person 依然不会被释放。上一篇文章提到过,personaotu 变量,传入的 block 的变量同样为 person,即 block 有一个强引用引用 person,所以 block 不被销毁的话,peroson 也不会销毁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//MRC环境下代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
Block block;
{
Person *person = [[Person alloc] init];
person.age = 10;
block = ^{
NSLog(@"------block内部%d",person.age);
};
[person release];
} // person被释放
NSLog(@"--------");
}
return 0;
}

将上述代码转移到 MRC 环境下,在 MRC 环境下即使 block 还在,person 却被释放掉了。因为 MRC 环境下 block 在栈空间,栈空间对外面的 person 不会进行强引用。

1
2
3
block = [^{
NSLog(@"------block内部%d",person.age);
} copy];

block 调用 copy 操作之后,person 不会被释放。
只需要对栈空间的 block 进行一次 copy 操作,将栈空间的 block 拷贝到堆中,person 就不会被释放,说明堆空间的 block 可能会对 person 进行一次 retain 操作,以保证 person 不会被销毁。堆空间的 block 自己销毁之后也会对持有的对象进行 release 操作。
也就是说栈空间上的 block 不会对对象强引用,堆空间的 block 有能力持有外部调用的对象,即对对象进行强引用或去除强引用的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef void (^Block)(void);

int main(int argc, const char * argv[]) {
@autoreleasepool {
Block block;
{
Person *person = [[Person alloc] init];
person.age = 10;

__weak Person *weakPerson = person;
block = ^{
NSLog(@"------block内部%d", weakPerson.age);
};
}
NSLog(@"--------");
}
return 0;
}

__weak 添加之后,person 在作用域执行完毕之后就被销毁了。

将代码转化为 c++ 来看一下上述代码之间的差别。
__weak 修饰变量,需要告知编译器使用 ARC 环境及版本号否则会报错,添加说明 -fobjc-arc -fobjc-runtime=ios-8.0.0

1
2
3
4
5
6
7
8
9
10
11
12
13
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
Person *__weak weakPerson;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__weak weakPerson, int flags=0) : weakPerson(_weakPerson) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

__weak 修饰的变量,在生成的 __main_block_impl_0 中也是使用 __weak 修饰。

1
2
3
4
5
6
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};

ARC 的环境下,当 block 中捕获对象类型的变量时,我们发现 block 结构体 __main_block_impl_0 的描述结构体 __main_block_desc_0 中多了两个参数 copydispose 函数。

copy 本质就是 __main_block_copy_0 函数,__main_block_copy_0 函数内部调用 _Block_object_assign 函数;
dispose 本质就是 __main_block_dispose_0 函数,__main_block_dispose_0 函数内部调用 _Block_object_dispose 函数;

上述 __main_block_impl_0 结构体中看出,没有使用 __block 修饰的变量(objectweadObj)则根据他们本身被 block 捕获的指针类型对他们进行强引用或弱引用,而一旦使用 __block 修饰的变量,__main_block_impl_0 结构体内一律使用强指针引用生成的结构体。

_Block_object_assign 函数调用时机及作用

block 进行 copy 操作的时候就会自动调用 __main_block_desc_0 内部的 __main_block_copy_0 函数, __main_block_copy_0 函数内部会调用 _Block_object_assign 函数。

_Block_object_assign 函数会自动根据 __main_block_impl_0 结构体内部的 person 是什么类型的指针,对 person 对象产生强引用或者弱引用。可以理解为 _Block_object_assign 函数内部会对 person 进行引用计数器的操作,如果 __main_block_impl_0 结构体内 person 指针是 __strong 类型,则为强引用,引用计数 +1,如果 __main_block_impl_0 结构体内 person 指针是 __weak 类型,则为弱引用,引用计数不变。

_Block_object_dispose 函数调用时机及作用

block 从堆中移除时就会自动调用 __main_block_desc_0 中的 __main_block_dispose_0 函数,__main_block_dispose_0 函数内部会调用 _Block_object_dispose 函数。

_Block_object_dispose 会对 person 对象做释放操作,类似于 release,也就是断开对 person 对象的引用,而 person 究竟是否被释放还是取决于 person 对象自己的引用计数。

总结

相同点

  • block 内部访问了对象类型的 auto 变量时
    如果 block 是在栈上,将不会对 auto 变量产生强引用;

  • 如果 block 被拷贝到堆上
    会调用 block 内部的 copy 函数,copy 函数内部会调用 _Block_object_assign 函数,_Block_object_assign 函数会根据 auto 变量的修饰符(__strong__weak__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用;

__block 变量(假设变量名叫做a)

1
_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);

对象类型的 auto 变量(假设变量名叫做p)

1
_Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
  • 如果 block 从堆上移除
    会调用 block 内部的 dispose 函数,dispose 函数内部会调用 _Block_object_dispose 函数,_Block_object_dispose 函数会自动释放引用的 auto 变量(release);

__block 变量(假设变量名叫做a)

1
_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);

对象类型的auto变量(假设变量名叫做p)

1
_Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);

不同点
没有使用 __block 修饰的变量(objectweadObj)则根据他们本身被 block 捕获的指针类型对他们进行强引用或弱引用,而一旦使用 __block 修饰的变量,__main_block_impl_0 结构体内一律使用强指针引用生成的结构体。

__forwarding 指针

图示
上面提到过 __forwarding 指针指向的是结构体自己。当使用变量的时候,通过结构体找到 __forwarding 指针,在通过 __forwarding 指针找到相应的变量。这样设计的目的是为了方便内存管理。通过上面对 __block 变量的内存管理分析我们知道,block 被复制到堆上时,会将 block 中引用的变量也复制到堆中。

当在 block 中修改 __block 修饰的变量时,

1
2
3
4
5
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_age_0 *age = __cself->age; // bound by ref
(age->__forwarding->age) = 20;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_jm_dztwxsdn7bvbz__xj2vlp8980000gn_T_main_b05610_mi_0,(age->__forwarding->age));
}

通过源码可以知道,当修改 __block 修饰的变量时,是根据变量生成的结构体这里是 __Block_byref_age_0 找到其中 __forwarding 指针,__forwarding 指针指向的是结构体自己因此可以找到 age 变量进行修改。

block 在栈中时,__Block_byref_age_0 结构体内的 __forwarding 指针指向结构体自己。

而当 block 被复制到堆中时,栈中的 __Block_byref_age_0 结构体也会被复制到堆中一份,而此时栈中的 __Block_byref_age_0 结构体中的 __forwarding 指针指向的就是堆中的 __Block_byref_age_0 结构体,堆中 __Block_byref_age_0 结构体内的 __forwarding 指针依然指向自己。

此时当对 age 进行修改时,

1
2
3
4
5
// 栈中的age
__Block_byref_age_0 *age = __cself->age; // bound by ref
// age->__forwarding获取堆中的age结构体
// age->__forwarding->age 修改堆中age结构体的age变量
(age->__forwarding->age) = 20;

通过 __forwarding 指针巧妙的将修改的变量赋值在堆中的 __Block_byref_age_0中。

__block 修饰的对象类型的内存管理

1
2
3
4
5
6
7
8
9
10
11
typedef void (^Block)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block Person *person = [[Person alloc] init];
Block block = ^ {
NSLog(@"%p", person);
};
block();
}
return 0;
}

查看 __Block_byref_person_0 结构体及其声明:

1
2
3
4
5
6
7
8
9
10
11
typedef void (*Block)(void);
struct __Block_byref_person_0 {
void *__isa; // 8 内存空间
__Block_byref_person_0 *__forwarding; // 8
int __flags; // 4
int __size; // 4
void (*__Block_byref_id_object_copy)(void*, void*); // 8
void (*__Block_byref_id_object_dispose)(void*); // 8
Person *__strong person; // 8
};
// 8 + 8 + 4 + 4 + 8 + 8 + 8 = 48
1
2
3
4
5
6
7
8
9
10
11
12
// __Block_byref_person_0结构体声明

__attribute__((__blocks__(byref))) __Block_byref_person_0 person = {
(void*)0,
(__Block_byref_person_0 *)&person,
33554432,
sizeof(__Block_byref_person_0),
__Block_byref_id_object_copy_131,
__Block_byref_id_object_dispose_131,

((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"))
};

之前提到过 __block 修饰的对象类型生成的结构体中新增加了两个函数 void (*__Block_byref_id_object_copy)(void*, void*)void (*__Block_byref_id_object_dispose)(void*)。这两个函数为 __block 修饰的对象提供了内存管理的操作。

可以看出为 void (*__Block_byref_id_object_copy)(void*, void*)void (*__Block_byref_id_object_dispose)(void*) 赋值的分别为 __Block_byref_id_object_copy_131__Block_byref_id_object_dispose_131。找到这两个函数:

1
2
3
4
5
6
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
static void __Block_byref_id_object_dispose_131(void *src) {
_Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}

可以发现 __Block_byref_id_object_copy_131 函数中同样调用了 _Block_object_assign 函数,而 _Block_object_assign 函数内部拿到 dst 指针即 block 对象自己的地址值加上 40 个字节。并且 _Block_object_assign 最后传入的参数是 131,同 block 直接对对象进行内存管理传入的参数 38 都不同。可以猜想 _Block_object_assign 内部根据传入的参数不同进行不同的操作的。

通过对上面 __Block_byref_person_0 结构体占用空间计算发现 __Block_byref_person_0 结构体占用的空间为 48 个字节。而加 40 恰好指向的就为 person 指针。

也就是说 copy 函数会将 person 地址传入 _Block_object_assign 函数, _Block_object_assign 中对 Person 对象进行强引用或者弱引用。

如果使用 __weak 修饰变量查看一下其中的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
__block __weak Person *weakPerson = person;
Block block = ^ {
NSLog(@"%p", weakPerson);
};
block();
}
return 0;
}

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_weakPerson_0 *weakPerson; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_weakPerson_0 *_weakPerson, int flags=0) : weakPerson(_weakPerson->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

__main_block_impl_0 中没有任何变化,__main_block_impl_0weakPerson 依然是强引用,但是 __Block_byref_weakPerson_0 中对 weakPerson 变为了 __weak 指针。

1
2
3
4
5
6
7
8
9
struct __Block_byref_weakPerson_0 {
void *__isa;
__Block_byref_weakPerson_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
Person *__weak weakPerson;
};

也就是说无论如何 block 内部中对 __block 修饰变量生成的结构体都是强引用,结构体内部对外部变量的引用取决于传入 block 内部的变量是强引用还是弱引用。