这篇文章主要的讲的是这三个方面。
* block捕获对象变量的本质
* __weak 修饰符 详解
* __block 修饰符 详解
* block 内存管理 详解
* 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;
}
上述代码在block中引用了person这个对象。
思考:当在block中访问的为对象类型时,对象什么时候会销毁?
推测:大括号执行完毕之后,person
依然不会被释放。上一篇文章提到过,person
为aotu
变量,传入的block
的变量同样为person
,即block有一个强引用引用person,所以block不被销毁的话,peroson也不会销毁
。
查看源代码如下:
将上述代码转移到MRC环境下,在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 = ^{
NSLog(@"------block内部%d",person.age);
} copy];
在ARC环境下对block执行copy方法之后block就不会被释放了。
在这篇文中iOS block本质详解(一)已经讲到,只需要对栈空间的block进行一次copy操作
,将栈空间的block拷贝到堆中
,person就不会被释放
,说明堆空间的block
可能会对person进行一次retain操作
,以保证person不会被销毁。堆空间的block自己销毁之后也会对持有的对象进行release操作
。
也就是说栈空间上的block不会对对象强引用
,堆空间的block有能力持有外部调用的对象
,即对对象进行强引用或去除强引用的操作
。
__weak
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;
}
__weak
添加之后,person
在作用域执行完毕之后就被销毁了。
将代码转化为c++来看一下上述代码之间的差别。
__weak
修饰的变量,在生成的__main_block_impl_0
中也是使用__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对象
,以及3。
dispose本质就是__main_block_dispose_0
函数,__main_block_dispose_0
函数内部调用_Block_object_dispose
函数,_Block_object_dispose
函数传入的参数是person
对象,以及3。
_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_imps_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对象自己的引用计数
。
总结
1.一旦
block
中捕获的变量为对象类型
,block结构体中
的__main_block_desc_0
会出两个参数copy
和dispose
。因为访问的是个对象,block希望拥有这个对象,就需要对对象进行引用
,也就是进行内存管理
的操作。比如说对对象进行retarn操作
,因此一旦block捕获的变量是对象类型就会会自动生成copy和dispose来对内部引用的对象进行内存管理
。
2.当block
内部访问了对象类型的auto变量
时,如果block
是在栈
上,block
内部不会对person
产生强
引用。不论block结构体内部的变量是__strong
修饰还是__weak
修饰,都不会对变量产生强
引用。
3.如果block被拷贝到堆
上。copy函数会调用_Block_object_assign
函数,根据auto变量
的修饰符(__strong,__weak,unsafe_unretained)
做出相应的操作,形成强引用或者弱引用
4.如果block从堆中移除
,dispose函数
会调用_Block_object_dispose
函数,自动释放引用的auto变量。
思考1: 下列代码person在何时销毁 ?
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
Person *person = [[Person alloc] init];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",person);
});
NSLog(@"touchBegin----------End");
}
原因:上文提到过ARC
环境中,block
作为GCD API
的方法参数时会自动进行copy
操作,因此block在堆空间
,并且使用强引用访问person对象
,因此block内部copy
函数会对person
进行强引用。当block执行完毕
需要被销毁时,调用dispose
函数释放对person
对象的引用,person
没有强指针指向时才会被销毁。
思考2: 下列代码person在何时销毁 ?
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
Person *person = [[Person alloc] init];
__weak Person *waekP = person;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",waekP);
});
NSLog(@"touchBegin----------End");
}
原因:block
中对waekP
为__weak弱引用
,因此block
内部copy
函数会对person同样进行弱引用
,当大括号执行完毕时,person
对象没有强指针引用就会被释放。因此block
块执行的时候打印null
。
思考3: 下列代码person在何时销毁 ?
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
Person *person = [[Person alloc] init];
__weak Person *waekP = person;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"weakP ----- %@",waekP);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"person ----- %@",person);
});
});
NSLog(@"touchBegin----------End");
}
思考4: 下列代码person在何时销毁 ?
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
Person *person = [[Person alloc] init];
__weak Person *waekP = person;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"person ----- %@",person);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"weakP ----- %@",waekP);
});
});
NSLog(@"touchBegin----------End");
}
在这个3秒的延时里面,Person
是用__weak
修饰的,所以这个block不会对person形成强引用,所以在1s后释放了。
__block 修饰符
在工作中如果想在block修改外部的局部变量,那么怎么做呢?
默认情况下block不能修改外部的局部变量。通过之前对源码的分析可以知道。
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
Block block = ^ {
// age = 20; // 无法修改
NSLog(@"%d",age);
};
block();
}
return 0;
}
默认情况下block不能修改外部的局部变量
。通过之前对源码的分析可以知道。
age
是在main
函数内部声明的,说明age的内存
存在于main函数的栈空间内部
,但是block内部的代码
在__main_block_func_0函数内部
。__main_block_func_0
函数内部无法访问age变量的内存空间
,两个函数的栈空间不一样,__main_block_func_0
内部拿到的age
是block
结构体内部的age
,因此无法在__main_block_func_0
函数内部去修改main
函数内部的变量。iOS block本质详解(一)文章中已经有说明,block捕获的age是个数值
,不是地址
。
方式一:age使用static修饰。
前文提到过static
修饰的age
变量传递到block内部的是指针
,在__main_block_func_0
函数内部就可以拿到age变量的内存地址
,因此就可以在block内部修改age的值
。
方式二:__block
__block
用于解决block
内部不能修改auto变量值
的问题,__block不能修饰静态变量(static) 和全局变量
__block int age = 10;
转化成了底层源码发现
首先被__block
修饰的age
变量声明变为名为age
的__Block_byref_age_0
结构体,也就是说加上__block
修饰的话捕获到的block
内的变量为__Block_byref_age_0
类型的结构体。
__isa指针
:__Block_byref_age_0
中也有isa指针
也就是说__Block_byref_age_0
本质也一个对象
。
__forwarding
:__forwarding
是__Block_byref_age_0
结构体类型的,并且__forwarding
存储的值为(__Block_byref_age_0 *)&age
,即结构体自己的内存地址
。
__flags
:0
__size
:sizeof(__Block_byref_age_0)
即__Block_byref_age_0
所占用的内存空间
。
age
:真正存储变量的地方,这里存储局部变量10
。
接着将__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
的值。
为什么要通过__forwarding获取age变量的值?
__forwarding
是指向自己的指针。这样的做法是为了方便内存管理
,之后内存管理章节会详细解释。
到此为止,__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
struct __Block_byref_weakPerson_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
MJPerson *person;
};
__Block_byref_id_object_copy
和__Block_byref_id_object_dispose
函数的调用时机及作用在__block内存管理部分详细分析。
问题
1. 以下代码是否可以正确执行
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSMutableArray *array = [NSMutableArray array];
Block block = ^{
[array addObject: @"5"];
[array addObject: @"5"];
NSLog(@"%@",array);
};
block();
}
return 0;
}
答:可以正确执行,因为在 block
块中仅仅是使用了array的内存地址
,往内存地址中添加内容,并没有修改arry的内存地址
,因此array
不需要使用__block
修饰也可以正确编译。
因此当仅仅是使用局部变量的内存地址
,而不是修改
的时候,尽量不要添加__block
,通过上述分析我们知道一旦添加了__block修饰符
,系统会自动创建相应的结构体
,占用不必要的内存空间
。
2. 上面提到过__block
修饰的age
变量在编译时会被封装为结构体,那么当在外部使用age
变量的时候,使用的是__Block_byref_age_0
结构体呢?还是__Block_byref_age_0
结构体内的age
变量呢?
为了验证上述问题
同样使用自定义结构体的方式来查看其内部结构
typedef void (^Block)(void);
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(void);
void (*dispose)(void);
};
struct __Block_byref_age_0 {
void *__isa;
struct __Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
struct __Block_byref_age_0 *age; // by ref
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int age = 10;
Block block = ^{
age = 20;
NSLog(@"age is %d",age);
};
block();
struct __main_block_impl_0 *blockImpl = (__bridge struct __main_block_impl_0 *)block;
NSLog(@"%p",&age);
}
return 0;
}
打印断点查看结构体内部结构
通过查看
blockImpl
结构体其中的内容,找到age
结构体,其中重点观察两个元素:
__forwarding
其中存储的地址确实是age
结构体变量自己的地址
age
中存储这修改后的变量20
。
上面也提到过,在block
中使用或修改age
的时候都是通过结构体__Block_byref_age_0
找到__forwarding
在找到变量age
的。
另外apple
为了隐藏__Block_byref_age_0
结构体的实现,打印age
变量的地址发现其实是__Block_byref_age_0
结构体内age
变量的地址。
__block内存管理
上文提到当block
中捕获对象类型的变量时,block
中的__main_block_desc_0
结构体内部会自动添加copy
和dispose
函数对捕获的变量进行内存管理
。
那么同样的当block
内部捕获__block修饰的对象类型的变量
时,__Block_byref_person_0
结构体内部也会自动添加__Block_byref_id_object_copy
和__Block_byref_id_object_dispose
对被__block
包装成结构体的对象进行内存管理
。
当block
内存在栈
上时,并不会对__block
变量产生内存管理。当blcok被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
修饰的变量在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
结构体内一律使用强指针
引用生成的结构体。
接着我们来看__block修饰的变量
生成的结构体
有什么不同
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
struct __Block_byref_person_1 {
void *__isa;
__Block_byref_person_1 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
Person *__strong person;
};
struct __Block_byref_weakPerson_2 {
void *__isa;
__Block_byref_weakPerson_2 *__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_byref_id_object_copy
和__Block_byref_id_object_dispose
两个函数,用来对对象类型
的变量进行内存管理
的操作。而结构体对对象的引用类型
,则取决于block捕获的对象类型
的变量。weakPerson
是弱指针,所以__Block_byref_weakPerson_2
对weakPerson
就是弱引用,person
是强指针
,所以__Block_byref_person_1
对person
就是强
引用。
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->age, (void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_assign((void*)&dst->object, (void*)src->object, 3/*BLOCK_FIELD_IS_OBJECT*/);
_Block_object_assign((void*)&dst->weakObj, (void*)src->weakObj, 3/*BLOCK_FIELD_IS_OBJECT*/);
_Block_object_assign((void*)&dst->person, (void*)src->person, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_assign((void*)&dst->weakPerson, (void*)src->weakPerson, 8/*BLOCK_FIELD_IS_BYREF*/);
}
__main_block_copy_0
函数中会根据变量是强弱指针
及有没有被__block
修饰做出不同的处理,强指针在block
内部产生强引用
,弱指针在block
内部产生弱引用
。被__block
修饰的变量最后的参数传入的是8
,没有被__block修饰
的变量最后的参数传入的是3
。
当block
从堆
中移除时通过dispose
函数来释放他们。
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_dispose((void*)src->object, 3/*BLOCK_FIELD_IS_OBJECT*/);
_Block_object_dispose((void*)src->weakObj, 3/*BLOCK_FIELD_IS_OBJECT*/);
_Block_object_dispose((void*)src->person, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_dispose((void*)src->weakPerson, 8/*BLOCK_FIELD_IS_BYREF*/);
}
__forwarding指针
上面提到过__forwarding
指针指向的是结构体自己
。当使用变量的时候,通过结构体找到__forwarding
指针,在通过__forwarding指针
找到相应的变量。这样设计的目的是为了方便内存管理
。通过上面对__block
变量的内存管理
分析我们知道,block
被复制到堆
上时,会将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_0
结构体内的__forwarding
指针指向结构体自己。
而当block被复制到堆
中时,栈中的__Block_byref_age_0
结构体也会被复制到堆
中一份,而此时栈中的__Block_byref_age_0
结构体中的__forwarding指针指向的就是堆
中的__Block_byref_age_0
结构体,堆中__Block_byref_age_0
结构体内的__forwarding
指针依然指向自己。
此时当对age进行修改时
// 栈中的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
中。
我们通过一张图展示__forwarding
指针的作用
因此
block
内部拿到的变量实际就是在堆
上的。当block
进行copy被复制到堆
上时,_Block_object_assign
函数内做的这一系列操作。
被__block修饰的对象类型的内存管理
使用以下代码,生成c++代码查看内部实现
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
结构体及其声明
__Block_byref_person_0结构体
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
// __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
。找到这两个函数
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
直接对对象进行内存管理传入的参数3
,8
都不同。可以猜想_Block_object_assign
内部根据传入的参数
不同进行不同的操作的。
通过对上面__Block_byref_person_0
结构体占用空间计算发现__Block_byref_person_0
结构体占用的空间为48
个字节。而加40
恰好指向的就为person
指针。
也就是说copy
函数会将person
地址传入_Block_object_assign
函数,_Block_object_assign
中对Person
对象进行强引用或者弱引用
。
如果使用
__weak
修饰变量查看一下其中的源码
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_0
对weakPerson
依然是强引用
,但是__Block_byref_weakPerson_0
中对weakPerson
变为了__weak指针
。
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
内部的变量是强引用
还是弱引用
。
mrc
环境下,尽管调用了copy
操作,__block
结构体不会对person
产生强
引用,依然是弱引用
。
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block Person *person = [[Person alloc] init];
Block block = [^ {
NSLog(@"%p", person);
} copy];
[person release];
block();
[block release];
}
return 0;
}
上述代码person
会先释放
block的copy[50480:8737001] -[Person dealloc]
block的copy[50480:8737001] 0x100669a50
当block
从堆中移除的时候。会调用dispose
函数,block
块中去除对__Block_byref_person_0 *person
;的引用,__Block_byref_person_0
结构体中也会调用dispose
操作去除对Person *person
;的引用。以保证结构体和结构体内部的对象可以正常释放。
循环引用
循环引用导致内存泄漏。
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
person.age = 10;
person.block = ^{
NSLog(@"%d",person.age);
};
}
NSLog(@"大括号结束啦");
return 0;
}
运行代码打印内容
block的copy[55423:9158212] 大括号结束啦
通过一张图看一下他们之间的内存结构
上图中可以发现,Person对象和block对象相互之间产生了强引用,导致双方都不会被释放,进而造成内存泄漏。
解决循环引用问题 - ARC
首先为了能随时执行block
,我们肯定希望person
对block
对强引用
,而block内部对person
的引用为`弱引用最好。
使用__weak
和 __unsafe_unretained
修饰符可以解决循环引用
的问题
我们上面也提到过__weak
会使block
内部将指针变为弱指针。block对person对象为弱指针
的话,也就不会出现相互引用
而导致不会被释放了。
__weak
和__unsafe_unretained
的区别。
__weak
不会产生强引用
,指向的对象销毁时,会自动将指针置为nil
。因此一般通过__weak
来解决问题。
__unsafe_unretained
不会产生强引用
,不安全
,指向的对象销毁时,指针存储的地址值不变
。
使用__block
也可以解决循环引用
的问题。
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block Person *person = [[Person alloc] init];
person.age = 10;
person.block = ^{
NSLog(@"%d",person.age);
person = nil;
};
person.block();
}
NSLog(@"大括号结束啦");
return 0;
}
上面我们提到过,
在block
内部使用变量使用的其实是__block修饰的变量
生成的结构体__Block_byref_person_0
内部的person
对象,那么当person
对象置为nil
也就断开了结构体对person
的强引用
,那么三角的循环引用就自动断开。该释放的时候就会释放了。但是有弊端,必须执行block,并且在block内部
将person对象置为nil
。也就是说在block执行之前代码是因为循环引用导致内存泄漏
的。
解决循环引用问题 - MRC
使用__unsafe_unretained
解决。在MRC
环境下不支持使用__weak
,使用原理同ARC
环境下相同,这里不在赘述。
使用__block
也能解决循环引用的问题。因为上文__block
内存管理中提到过,MRC
环境下,尽管调用了copy
操作,__block
结构体不会对person
产生强引用,依然是弱引用
。因此同样可以解决循环引用
的问题。
__strong 和 __weak
__weak typeof(self) weakSelf = self;
person.block = ^{
__strong typeof(weakSelf) myself = weakSelf;
NSLog(@"age is %d", myself->_age);
};
在block
内部重新使用__strong
修饰self
变量是为了在block内部有一个强指针
指向weakSelf
避免在block
调用的时候weakSelf
已经被销毁
。
我的主页
我的博客主页