大纲
- block对对象变量的捕获
- __main_block_copy_0 和 __main_block_dispose_0
- block内修改变量的值
- __block内存管理
- __forwarding指针
block对对象变量的捕获
block一般使用过程中都是对对象变量的捕获,那么对象变量的捕获同基本数据类型变量相同吗?
思考:当在block中访问的为对象类型时,对象什么时候会销毁?
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
依然不会被释放。
由此可以看出,block
有一个强引用,引用person
,所以block
不被销毁的话,person
也不会被销毁。
在MRC环境下即使block
还在, person
却被释放掉了。因为MRC环境下block
在栈空间,栈空间对外面的person
不会进行强引用。
//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;
}
block调用copy操作之后,person不会被释放。
block = [^{
NSLog(@"------block内部%d",person.age);
} copy];
上文中也提到过,只需要对栈空间的block
进行一次copy
操作,将栈空间的block
拷贝到堆中,person
就不会被释放,说明堆空间的block
可能会对person
进行一次retain
操作,以保证person
不会被销毁。堆空间的block
自己销毁之后也会对持有的对象进行release
操作。
也就是说栈空间上的block不会对对象强引用,堆空间的block有能力持有外部调用的对象,即对对象进行强引用或去除强引用的操作。
_weak
_weak添加后,person在作用域执行完毕之后就会被销毁
typedef void (^Block)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
Block block;
{
Person *person = [[Person alloc] init];
person.age = 10;
__weak Person *waekPerson = person;
block = ^{
NSLog(@"------block内部%d",waekPerson.age);
};
}
NSLog(@"--------");
}
return 0;
}
转化为C++看看
可以看到在结构体中同样是以_weak修饰
__main_block_copy_0 和 __main_block_dispose_0
当block中捕获对象类型的变量时,我们发现block结构体__main_block_impl_0
的描述结构体__main_block_desc_0
中多了两个参数copy和dispose函数,查看源码:
可以看到
copy
和dispose
函数中传入的都是__main_block_impl_0
结构体本身。
copy本质就是
__main_block_copy_0
函数,__main_block_copy_0函数内部调用_Block_object_assign函数,在这里,_Block_object_assign中传入的是person对象的地址,person对象,以及8。(没写错,就是8)
dispose本质就是
__main_block_dispose_0
函数,__main_block_dispose_0函数内部调用_Block_object_dispose函数,这里,_Block_object_dispose函数传入的参数是person对象,以及8。
_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中捕获的变量为对象类型,block结构体中的
__main_block_desc_0
会出两个参数copy
和dispose
。因为访问的是个对象,block希望拥有这个对象,就需要对对象进行引用,也就是进行内存管理的操作。比如说对对象进行retarn操作,因此一旦block捕获的变量是对象类型就会会自动生成copy
和dispose
来对内部引用的对象进行内存管理。 - 当block内部访问了对象类型的auto变量时,如果block是在栈上,block内部不会对person产生强引用。不论block结构体内部的变量是
__strong
修饰还是__weak
修饰,都不会对变量产生强引用。 - 如果block被拷贝到堆上。
_copy
函数会调用__Block_object_assign
_函数,根据auto变量的修饰符(_strong
,_weak
,_unsafe_unretained
)做出相应的操作,形成强引用或者弱引用 - 如果block从堆中移除,dispose函数会调用
__Block_object_dispose
函数,自动释放引用的auto变量。
block内修改变量的值
还记得我们在介绍篇中说到的_block说明符吗?
int main ()
{
__block int age = 10 ;//两个_
Myblock block = ^(void){
age = 20 ;
NSLog(@"%d",age);
};
block();
}
我们将它转化为C++看一下
由此可发现,__block
修饰的age
变量声明变为age
的__Block_byref_age_0
结构体,也就说加上__block
修饰的话捕获到的block
内的变量为__Block_byref_age_0
类型的结构体
通过下图查看__Block_byref_age_0结构体内存储哪些元素。
- __isa指针:说明
__Block_byref_age_0
本质也一个对象 - __forwarding:结构体自己的内存地址
- __flags:0
- __size:即
__Block_byref_age_0
所占用的内存空间。
接着将__Block_byref_age_0
结构体age存入__main_block_impl_0
结构体中,并赋值给__Block_byref_age_0 *age
;
之后调用block
,首先取出__main_block_impl_0
中的age
,通过age
结构体拿到__forwarding
指针,上面提到过__forwarding
中保存的就是__Block_byref_age_0
结构体本身,这里也就是age(__Block_byref_age_0)
,在通过__forwarding
拿到结构体中的age(10)
变量并修改其值。
后续NSLog中使用age
时也通过同样的方式获取age
的值。
到此为止,__block
为什么能修改变量的值已经很清晰了。__block
将变量包装成对象,然后在把age封装在结构体里面,block内部存储的变量为结构体指针,也就可以通过指针找到内存地址进而修改变量的值。
_block修饰对象类型
如果变量本身就是对象类型呢?看看
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block Person *person = [[Person alloc] init];
NSLog(@"%@",person);
Block block = ^{
person = [[Person alloc] init];
NSLog(@"%@",person);
};
block();
}
return 0;
}
通过源码查看,将对象包装在一个新的结构体中。结构体内部会有一个person对象,不一样的地方是结构体内部添加了内存管理的两个函数__Block_byref_id_object_copy
和__Block_byref_id_object_dispose
__block内存管理
前面说到block捕获对象类型的变量时,block中的__main_block_desc_0
结构体内部会自动添加copy
和dispose
函数对捕获的变量进行内存管理
同样地,当捕获__block
修饰的对象类型的变量时,__Block_byref_person_0
结构体内部也会自动添加__Block_byref_id_object_copy
和__Block_byref_id_object_dispose
对被__block
包装成结构体的对象进行内存管理
当block
内存在栈上时,并不会对__block
变量产生内存管理。当block
被copy
到堆上时会调用block
内部的copy
函数,copy
函数内部会调用_Block_object_assign
函数,Block_object_assign
函数对__block
变量形成强引用(相当于retain)
首先通过一张图看一下block复制到堆上时内存变化
当block
被copy
到堆上时,block
内部引用的__block
变量也会被复制到堆上,并且持有变量,如果block
复制到堆上的同时,__block
变量已经存在堆上了,则不会复制。
当block从堆中移除的话,就会调用dispose函数,也就是__main_block_dispose_0
函数,__main_block_dispose_0
函数内部会调用_Block_object_dispose
函数,会自动释放引用的__block变量。
__block
修饰的变量在block结构体中一直都是强引用,而其他类型的是由传入的对象指针类型决定。
一段代码更深入的观察一下
typedef void (^Block)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
int number = 20;
__block int age = 10;
NSObject *object = [[NSObject alloc] init];
__weak NSObject *weakObj = object;
Person *p = [[Person alloc] init];
__block Person *person = p;
__block __weak Person *weakPerson = p;
Block block = ^ {
NSLog(@"%d",number); // 局部变量
NSLog(@"%d",age); // __block修饰的局部变量
NSLog(@"%p",object); // 对象类型的局部变量
NSLog(@"%p",weakObj); // __weak修饰的对象类型的局部变量
NSLog(@"%p",person); // __block修饰的对象类型的局部变量
NSLog(@"%p",weakPerson); // __block,__weak修饰的对象类型的局部变量
};
block();
}
return 0;
}
转化为C++
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int number;
NSObject *__strong object;
NSObject *__weak weakObj;
__Block_byref_age_0 *age; // by ref
__Block_byref_person_1 *person; // by ref
__Block_byref_weakPerson_2 *weakPerson; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _number, NSObject *__strong _object, NSObject *__weak _weakObj, __Block_byref_age_0 *_age, __Block_byref_person_1 *_person, __Block_byref_weakPerson_2 *_weakPerson, int flags=0) : number(_number), object(_object), weakObj(_weakObj), age(_age->__forwarding), person(_person->__forwarding), weakPerson(_weakPerson->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
上述__main_block_impl_0
结构体中看出,没有使用__block
修饰的变量(object 和 weadObj)则根据他们本身被block捕获的指针类型对他们进行强引用或弱引用,而一旦使用__block修饰的变量,__main_block_impl_0
结构体内一律使用强指针引用生成的结构体
__forwarding指针
上面提到过的__forwarding
指针指向的是结构体自己。当使用变量的时候,通过结构体找到__forwarding
指针找到相应的变量。这样设计目的是为了方便内存管理。通过上面对__block
变量的内存管理分析我们知道,block
被复制到堆上时,会将block
中引用的变量也复制到堆中。
我们再来看这一段源码。
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_0v结构体内的
__forwarding```指针指向结构体自己。
而当block被复制到堆中时,栈中的__Block_byref_age_0
结构体也会被复制到堆中一份,而此时栈中的__Block_byref_age_0
结构体中的__forwarding
指针指向的就是堆中的__Block_byref_age_0
结构体,堆中__Block_byref_age_0
结构体内的_forwarding
指针依然指向自己。
最后
贴上大神文章iOS底层原理总结 - 探寻block的本质(二