Block
Block
Block也算是个类,一共三种类型:
int a = 10;
void(^globalBlock)(void) = ^{
NSLog(@"global");
};
void(^mallocBlock)(void) = ^{
NSLog(@"malloc - %d", a);//引用a
};
//对堆block进行__weak修饰就会变成栈区block。
void(^__weak stackBlock)(void) = ^{
NSLog(@"stack - %d", a);//引用a
};
NSLog(@"%@", globalBlock); 全局block
NSLog(@"%@", mallocBlock); 堆block
NSLog(@"%@", stackBlock); 栈block
非全局gloabl
的block在汇编
时都会默认赋为栈stack block
,因为这时候是编译阶段
,没有获取内存空间。malloc block
是stack在运行时
进行_Block_copy
拷贝到堆区
得到的,拷贝的前面并没有申请空间的相关代码,所以只能是栈区block。
通过xcrun -sdk iphonesimulator clang -arch x86_64 -rewrite-objc block.c
,将block.c 编译成 block.cpp,其中block在底层被编译成了以下的形式
int main(){
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("CJL");
} //block等于__main_block_impl_0,是一个函数
void(*block)(void) = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA));//构造函数
block->FuncPtr(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;// isa 保存 Block 结构体实例
impl.Flags = flags;// 用于保存 Block 结构体的实例指针
impl.FuncPtr = fp;// 函数指针
Desc = desc;
}
};
//**block的结构体类型**
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;//block代码块存储的位置
};
Block本质是一个对象
, 对象中封装了一个函数以及函数执行的上下文
;Block的调用本质是函数Funptr
的调用,因为底层把block的实现的代码部分赋值给了__block_impl的Funptr
,也就是调用的函数指针;
__block
__block说明符,其实会把自动变量包含到一个结构体中。
(__Block_byref_age_0 *)&age
__Block_byref_age_0 *age; // 结构体指针引用
__Block_byref_age_0 *age = __cself->age; // 通过结构体的self指针拿到age结构体的指针
(age->__forwarding->age) = 18; // 通过age结构体指针修改age值
block捕获外界变量时,在内部会自动生成同一个属性来保存。如果变量用__block
修饰,内部会传递此变量的内存地址,即指针浅拷贝
,达到在内部修改外部变量的目的。如果没有__block,那么就是值拷贝,即深拷贝
,内外指向不同的内存空间。
weakSelf 和 self 指向同一片内存空间,且使用__weak不会导致self的引用计数发生变化
__block修饰的变量在汇编层会封装一层结构体Block_byref,结构体保存了变量的指针地址,相当于指针(浅)拷贝,创建的对象a与传入对象的a指向同一片内存空间。没有被__block修饰的却被block捕获的值相当于值(深)拷贝。Block_byref其中的forwarding 栈上的指向堆上的对象,堆上的指向自身,这样就能找到正确存储的block位置了。
weak修饰 虽然也可以解决循环引用,但是如果block任务耗时,快速返回上个界面时,self就为空了,不会崩溃,相当于白白浪费了性能。所以需要weak-strong-dance
。
self不持有block时
,blcok 中截获 self,一般会延长 self 的生命周期(至少到 block 释放后,才会释放 self)。如果 self 同时持有 block
,则会导致循环引用。在日常 block 开发中我们的重点都放在了预防循环引用上,而循环引用之外的延长 self 的生命周期是很容易忽略的一个点。需要注意的是 block 最后都会被执行,不管 UIViewController 是否存活。
所以如果需要页面释放后,异步block回调代码不执行,需要进行self空置判断,如果为空就return
;
block底层真正结构体:Block_layout
struct Block_layout {
//指向block类型的类
void *isa;//8字节
//用来作标识符的,类似于isa中的位域
volatile int32_t flags; // contains ref count 4字节
//保留信息
int32_t reserved;//4字节
//函数指针,指向具体的block实现的调用地址
BlockInvokeFunction invoke;
//block附加信息,签名等
struct Block_descriptor_1 *descriptor;
// imported variables
};
block中可以执行代码块是因为代码块赋给了Block_laout结构体里的invoke。
总结:当用__block修饰的对象类型时使用block整个过程为:
- 首先被__block修饰的变量在底层会被封装成__Block_byref_XXXX_0的对象变量;
- 当block被拷贝到堆上时会调用内部的copy函数,
目的是为了不被出了作用域就被释放
, copy函数调用_Block_object_assign
函数, 会对对象的修饰符__strong, __weak, unsafe_unretained
做出相应的操作(强引用或者弱引用); - 当block执行完/从堆上移出时, 会调用block内部的dispose函数, 其内部调用_Block_object_dispose函数对结构体持有的对象进行类似release的释放操作;
- copy 函数持有截获的对象、dispose 函数释放截获的对象
被__block修饰的对象的三次copy
外部变量拷贝时调用的方法是_Block_object_assign
进入_Block_object_assign
源码
-
如果是
普通对象
,则交给系统arc处理,并拷贝对象指针,即引用计数+1,所以外界变量不能释放 -
如果是
block类型
的变量,则通过_Block_copy操作,将block从栈区拷贝到堆区 -
如果是
__block修饰
的变量,调用_Block_byref_copy
函数 进行内存拷贝
以及常规处理
【第一层】
通过_Block_copy
实现对象的自身拷贝,从栈区拷贝至堆区
【第二层】
通过_Block_byref_copy
方法,将对象拷贝为Block_byref
结构体类型
【第三层】
调用_Block_object_assign
方法,对__block修饰的当前变量的拷贝
byref:参数传递
block可以用strong修饰吗?
在ARC中可以,因为在ARC环境中的block只能在堆内存或全局内存中,因此不涉及到从栈拷贝到堆中的操作.
在MRC中不行,因为要有拷贝过程.如果执行copy用strong的话会crash, strong是ARC中引入的关键字.如果使用retain相当于忽视了block的copy过程.
hook block
iOS中hook oc对象用系统的method swizzer可以很方便的做到。但是block作为特殊的oc对象,方法交换并不适用。hook block的本质就是找到invoke函数指针和block的signature方法签名
。
网上大致有两种实现方式
- 一种是把invoke指针指向消息转发,然后通过NSInvocation进行调用,参考文章Block hook 正确姿势
- 另外一种是使用libffi来动态调用c函数,参考文章 Hook Objective-C Block with Libffi
timer的循环引用
下面这种方式是打不破循环引用的,在页面pop后,timer还会不断的执行任务。
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(fireHome) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
//weakSelf 和 self 的内存地址&是不一样,但都指向同一片内存空间。
造成这个现象的原因是weakSelf
只是打破了self->timer-self
这一条线的循环引用,但是不要忘了runloop同时也对timer强持有。RunLoop
对整个 对象的空间有强持有
,runloop没停,timer 和 weakSelf是无法释放的。
此时timer这个现象和block的循环引用,block捕获的是 weakself对象
的指针
地址,和self无关。
解决方案:只要在恰当的时机执行[self.timer invalidate]; self.timer = nil;
即可,或者才用中介者
模式,target通过NSProxy虚基类
,可以交给其子类实现。
- 定义一个继承自NSProxy的子类,实现消息转发的快速转发
forwardingTargetForSelector
,将self
传进去作为消息转发的目标
。 - 将timer中的target传入
NSProxy子类
对象,即timer持有NSProxy子类对象
这样做的主要目的是将强引用的注意力转移成了消息转发
。虚基类
只负责消息转发,即使用NSProxy作为中间代理、中间者。