大概两三周前通过学习《Objective-C高级编程 iOS与OS X多线程和内存管理》中的Block
章节,系统深入了解了Block
相关原理和内存管理的内容,昨天闲暇时回想起来,感觉有些东西又模糊了,内容记得七七八八,太碎片化了。索性好记性不如烂笔头,把自己的理解整理记录一下。
将Objective-C代码转换为C\C++代码
Clang
(LLVM
编译器)具有转换为我们可读源代码的功能。
//如果需要链接其他框架,使用-framework参数。比如-framework UIKit
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 输出的cpp文件
设置了sdk
的平台和cpu
架构,减少转换出来的代码量,方便查阅。
可能会遇到以下问题:
cannot create __weak reference in file using manual reference
解决方案:支持ARC、指定运行时系统版本,比如
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 OC源文件 -o 输出的cpp文件
Block
底层结构
当Block
没有自动捕获变量时:
//Block定义的结构体
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
在底层会被转换成类似上面这样子的结构体类型,struct __main_block_impl_0
。
而struct __main_block_impl_0
中包含了两个结构体:struct _block_impl impl
和struct __main_block_desc_0 *Desc
,以及一个构造函数。再看一下两个结构体的定义。
struct _block_impl {
void *isa; //指向了block所属的类型
int Flags;
int Reserved; // 预留
void *FuncPtr; // 函数指针,指向block中方法实现
};
//存储block的其他信息,大小等
struct __main_block_desc_0 {
unsigned long reserved; // 预留
unsigned long Block_size; // Block的大小
}
通过上面可以看出Blcok
也是包含一个isa
指针,因此也是一种OC
对象。具体是什么类,因为涉及到Blcok
的内存管理,所以后面篇幅再深入讨论。
再看一下给Blcok
结构体赋值和调用的代码:
//赋值部分
struct __main_block_impl_0 temp = __main_block_impl_0(__main_block_func_0,&__mainBlock_desc_0_DATA);
struct __main_block_impl_0 *blk = &temp;
//调用部分
(*blk->impl.FuncPtr)(blk);
赋值部分就是调用了__main_block_impl_0
的构造函数,将方法和__main_block_desc_0
类型的结构体作为参数传递进入。
方法调用是通过Blcok
的结构体取出其中的函数指针,直接调用该函数,同时将Block
自身作为参数传递给方法实现。
先对简单的Block
有个印象。
Block
变量捕获机制
int c = 30; // 全局变量(数据段,不需要捕获)
- (void)blockTest {
auto int a = 10;//局部auto变量(栈区,值捕获)
auto __strong NSObject *object = [[NSObject alloc] init];//局部auto变量(栈区,值捕获)
static int b = 20;//局部static变量(数据段,指针捕获)
void (^block)(void) = ^(void) {
NSLog(@"a:%d b:%d c:%d",a,b,c);
NSLog(@"object:%@",object);
};
block();
}
为了在Block
内部可以访问外部的变量,Block
有个变量捕获机制。那么什么样的变量才会捕获,什么样的不会捕获呢?
-
局部变量
-
auto
变量(平时我们在方法中声明的非static
局部变量,只是省略了auto关键字),这种情况是值捕获。 -
static
变量和结构体,这种情况是指针捕获。
-
全局变量:不会捕获,因为不需要捕获就可以访问。
总结就是:只捕获局部变量。
Block
捕获变量之后代码什么样子?
将上面的- (void)blockTest
转换C看一下:
struct __blockTest_block_impl_0 {
struct __block_impl impl;
struct __blockTest_block_desc_0* Desc;
int a;
int *b;
NSObject *object;
// 省略构造函数...
};
嗯,备注的没有错。变量a
和object
都是值捕获,而变量b
捕获的是*b
,是指针的捕获,而c
没有捕获。
Block
的内存管理
Block
有3种类型,可以通过调用class
方法或者isa
指针查看具体类型,最终都是继承自NSBlock
类型。
- NSGlobalBlock ( _NSConcreteGlobalBlock )//全局
- NSStackBlock ( _NSConcreteStackBlock ) //栈
- NSMallocBlock ( _NSConcreteMallocBlock )//堆
除了打印,那么怎么判断一个Block
的具体类型...?
-
NSGlobalBlock : 没有访问
auto
变量。 -
NSStackBlock : 访问了
auto
变量。 -
NSMallocBlock :
__NSStackBlock__
调用了copy
。
可能有的同学在这里这样子测试一下,发现上面的判断依据并不对...
明明Block访问的是auto
变量,但是Block
的类型是__NSMallocBlock__
呐,并不是__NSStackBlock__
,你说的不对。不着急,其实这里还涉及到另外一个问题:Block
的内存管理。
对一个Blcok
进行copy
操作后,对三种类型的Blcok
产生的影响:
-
__NSGlobalBlock__
:-
copy
前:Block
位于数据段中; -
copy
后:不产生任何影响。
-
-
__NSStackBlock__
:-
copy
前:Block
位于函数栈中; -
copy
后:从栈中复制一份到堆中。
-
-
__NSMallocBlock__
:-
copy
前:Block
位于堆中; -
copy
后:Block
的引用计数增加。
-
在ARC环境下,编译器会根据情况自动将栈上的Block复制到堆上,比如以下情况:
-
Block
作为函数返回值时。 - 将
Block
赋值给__strong
指针时。 -
Block
作为Cocoa API
中方法名含有usingBlock
的方法参数时。 -
Block
作为GCD API
的方法参数时。
在之前的图片(001)中,就是其中的第二种情况,Block
被赋值给__strong
指针。
这也是为什么我们习惯于用copy
关键字,来修饰一个Block
。以及将Block
当做参数传递时,安全起见,会对Block
参数执行copy
操作。
Block
对对象类型变量的强弱引用问题
-
当
Block
内部访问了对象类型的auto变量
时:- 如果
Block
是在栈上,将不会对auto变量
产生强引用。就是说栈上的Block
不会强引用一个对象
- 如果
-
当
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
的作用:
-
__block
可以用于解决Block
内部无法修改auto变量
值的问题。 -
__block
不能修饰全局变量、静态变量(static
)。
编译器会将__block
变量包装成一个对象:
void blockTest() {
__block int a = 10;
__block NSObject *object = [[NSObject alloc] init];
NSLog(@"a:%d",a);
void (^block)(void) = ^(void) {
a = 20;
NSLog(@"object --- %@",object);
};
block();
}
将上面的代码转换成C++之后可以看到:
// __block int a 被转换为下面的结构体
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
// __block NSObject *object 被转换为下面的结构体
struct __Block_byref_object_1 {
void *__isa;
__Block_byref_object_1 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSObject *object;
};
__Block_byref_a_0 *__forwarding
是一个指向自身的指针。
当我们的OC代码中,去访问被__block
修饰的变量,在底层中是如何去读取变量呢?
上面的代码中有一段打印的代码:
NSLog(@"a:%d",a);
在C++中被转成了:
NSLog((NSString *)//此处省略,影响阅读//,(a.__forwarding->a));
在Blcok中修改a的值:
a = 20;
在C++中被转成了:
// 从blcok取出blcok捕获的被`__block`修饰的变量
__Block_byref_a_0 *a = __cself->a; //__cself是blcok
(a->__forwarding->a) = 20;
在Block
的外部和外部访问变量是通过a.__forwarding->a
,访问结构体的__forwarding
指针找到值。可能有的同学有疑问:不是多此一举的嘛?结构体->__forwarding->结构体->val
,直接结构体->val
不就可以了吗?
目前能看出的作用是保持统一的写法,当然还有其他的原因,后面讲解。
总结:
- 当没有使用
__block
时,由于是值捕获,所以哪怕在Block
内修改,也不能影响到Block
外变量的值,因此苹果不允许直接修改。 - 而当我们在
Block
去修改被__block
修饰的变量时,由于是捕获到__block
结构体的指针,这样就可以我们可以修改Block
外面的值了。
__block
的内存管理
- 开始时
__block
结构体是一个在栈
上的结构体,在栈上的内存无所谓强弱引用的关系。而__block
结构体包装的对象是强或弱引用,是通过你使用__weak
和__strong
哪个来修饰决定的。
NSObject *object = [[NSObject alloc] init];
__block __weak typeof(object) weakObject = object;
__block NSObject *strongObjce = object;
-
当
Block
被拷贝到堆
上时,会自动将捕获的__block
结构体也拷贝到堆上。由于__block
的结构体也有isa
指针,同时还在堆空间中,我们可以将它理解成一个OC的对象,__block
对象。
-
于此同时还会将栈上的
__block
结构体中的__forwarding
指针,指向堆空间中的__block
对象。Block
捕获的指针,会从栈上的__block
结构体变为堆空间中的__block
对象,同时对__block
对象强引用。
- 当
Block
从堆中移除时:
这样分析一下,除了会将__block
结构体从栈移动到堆之外,和普通形式的auto
对象内存管理,流程上没有什么差别。当然具体内部调用的函数参数还是有点区别的:
当Block
拷贝到堆上时,都会通过copy
函数来处理它们
__block变量(假设变量名叫做a)
_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
对象类型的auto变量(假设变量名叫做p)
_Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
当Block
从堆上移除时,都会通过dispose
函数来释放它们:
__block变量(假设变量名叫做a)
_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
对象类型的auto变量(假设变量名叫做p)
_Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
能看到,虽然调用的方法相同,但是传递的参数类型不同:3和8。这决定了方法内部如何去处理流程吧。
循环引用
想必读到这里,应该可以理解循环引用是怎么产生的了。
注意:self
是方法调用时传递的第一个参数,是局部变量。
怎么解决?
不让Block
强引用self
,断掉一条线,就不会产生循环引用。
我们一般通过__weak
来修饰变量,比如这样:
__weak typeof(self) weakSelf = self;
也可以使用__unsafe_unretained
修饰变量解决。关于__unsafe_unretained
可以看这篇文章。
他们的区别:
__weak
: 对于__weak,指针的对象在它指向的对象释放的时候回转换为nil,这是一种特别安全的行为。
__unsafe_unretained
: 就像他的名字表达那样,__unsafe_unretained会继续指向对象存在的那个内存,即使是在它已经销毁之后。这会导致因为访问那个已释放对象引起的崩溃。
为了更安全的使用,我们经常是这样写:
__weak typeof(self) weakSelf = self; //解决循环应用
self.block = ^{
__strong typeof(self) strongSelf = weakSelf;
//在Block方法内部,即:局部变量内;对weakSelf进行一个强引用,
//这样可以确保,当self其他的强引用都释放时,仍然保持有一个强引用,
//这样self不会再block内部突然释放掉,导致后面的代码出现未知的问题。
//do someThing...//
};