众所周知,block可以封装一个匿名函数为对象,并捕获上下文所需的数据,并传给目标对象在适当的时候回调。正因为将抽象的函数具体化为一个可以存储管理的对象,block可以很容易被建立,管理,回调,销毁,也能很好的管理其执行所需要的数据,再加上即用即走和对代码逻辑上下文完整等优点,被大多数开发者广泛使用。虽然使用者很多,但还是有不少人对其实现和编译器背后如何支持还有一些疑惑,通过阅读本文相信你对block将会有一个比较清晰的认知。在解决一些棘手的内存问题的时候将会更加得心应手。
block的本质
首先写一个简单的block
int main(int argc, char * argv[]) {
void(^blockA)(void) = ^{};
blockA();
return 0;
}
使用简单的clang main.m -rewrite-objc
得到C++的main.cpp文件
关注我们感兴趣的部分,还是做个注释吧(64bit),熟悉这些偏移量比较重要,对分析问题很有帮助,block基础大小是32byte。
extern "C" _declspec(dllexport) void *_NSConcreteGlobalBlock[32];
extern "C" _declspec(dllexport) void *_NSConcreteStackBlock[32];
struct __block_impl {
void *isa; //8byte,isa指针,很重要的标志,意味着block很可能是个OC的类
int Flags;//4byte,包含的引用个数
int Reserved;//4byte
void *FuncPtr;//8byte,回调函数指针
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;//8byte,block的描述,辅助工具
//如果有捕获外部变量的话会定义在这里
...
__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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
//自定义block函数体
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};//这里Block_size=32byte
int main(int argc, char * argv[]) {
//定义函数指针,然后赋值上面静态函数,具体的代码实现被移到了上面的函数中
void(*blockA)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
//调用和传参数
((void (*)(__block_impl *))((__block_impl *)blockA)->FuncPtr)((__block_impl *)blockA);
return 0;
}
我们可以拷贝这些代码来运行,但有几点需要注意的不要引入OC头文件,否则_NSConcreteStackBlock
会重复定义,我们只需要定义同样的全局的变量来替代它或者删掉就可以了;main中第一句代码会报错taking the address of a temporary object of type '__main_block_impl_0'
, 这是因为这里调用了构造函数 _main_block_impl_0
,这会生成一个临时返回值,在c++ 11语法里面这个返回值是个右值引用,是无法进行取地址运算的。所以这里改写一下就可以运行了,代码运行起来其调用过程就比较好办了,这里就不具体细说了。
__main_block_impl_0 block_struct = __main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);
void (*blockA)() = (void (*)())&block_struct;
void (*block_func)(struct main_block_impl_0 *__cself) = (void (*)(struct __main_block_impl_0 *))((__block_impl *)blockA)->FuncPtr;
block_func((struct __main_block_impl_0 *)(*blockA));
注意:我这里改写的这个代码是存在一些问题的,因为block_struct是栈上的,所以一旦赋值给强引用时会copy一份放到堆上(GlobalBlock除外),调用block的时候可能已经超出block_struct的生命周期了。
接着看代码
在代码中我们看到了OC类的标志isa指针,并且指向_NSConcreteStackBlock
,但具体是不是那么回事还是需要证明一下。毕竟编译和运行时还是有些不一样。
在bockA();这句加断点;在编译器debug窗口左侧有当前调用栈帧可见变量,找到blockA,发现 __isa=__NSGlobalBlock__
和上面改写代码中的impl.isa = &_NSConcreteStackBlock
还是有出入的,我们选择相信运行时。
选中__isa
右键菜单View Memery of "__isa"
可以浏览当前内存的值,我这里isa指向的地址是0x1003b8048,然后可以看到这里值是70 94 59 0b 01 00 00 00(8byte),小端机器,实际的数据是010B599470,观察代码void *_NSConcreteGlobalBlock[32]
发现这是一个地址,对应的地址就是0x10B599470
,管它是不是OC对象,我们在lldb下po 0x10B599470输出一下是__NSGlobalBlock__
,呵,可能有谱,再追踪这个地址
可以看到以下内存数据前8个byte是010B5994F0,顺便输出一下也打印了__NSGlobalBlock__
,再看发现这个地址就在附近,里面记录的第一个数据是010780DE58,我们知道OC对象第一个数据就是isa指针,将其构建成地址0x10780de58
,输出一下,打印了NSObject
,这里可以理出关系blockA.isa->__NSGlobalBlock__.isa->NSObject
,也就是说__NSGlobalBlock__
的元类是NSObject
,这基本可以证明__NSGlobalBlock__
应该是个OC类型。
但我们希望得到最直接的证明就是一直找superclass
直至找到NSObject
。
找到objc_class
的定义,发现其继承自objc_object
struct objc_class : objc_object {
Class superclass;//Class就是objc_class *
...
}
struct objc_object {
private:
isa_t isa;
}
objc_object
只有一个isa_t
的数据
union isa_t {
Class cls;
uintptr_t bits;//是个unsigned long
...//带位域的struct,这里不关注
}
由此可见objc_class的前8byte是isa指针,第二个8byte是superclass
指针。
我这里一次是0x010a6112a0(__NSGlobalBlock)
,0x10a611110(NSBlock)
,0x10780dea8(NSObject)
对照上面的NSObject
,其地址0x10780de58
,有点蒙,到底哪个对?其实都算对。
这里我定义了一个NSObject的对象,利用其运行时isa和superclass的数据,做了一个两者的关系图。
还记得这个继承体系图,和上面结果一致。
从相关资料可以了解,OC中除了NSProxy外,其他的类都是NSObject的子类,包括元类,这个NSObject就是下图中的0x10780dea8。
OK,至此证明block是个OC对象,其继承自NSBlock,NSObject。
上面的环也可以解释一些经典的问题,比如(写本文的时候查资料时刚好遇到,就贴上来了):
Class cls1 = [NSObject class];//0x10780dea8 id cls2 = (id)[NSObject class];//0x10780dea8 BOOL r1 = [cls2 isKindOfClass:cls1];//isa找到0x10780de58,再找到0x10780dea8比较 BOOL r2 = [cls2 isMemberOfClass:cls1];//isa找到0x10780de58比较 //User不存在这个环,也就不会出现这个现象 Class cls3 = [User class]; id cls4 = (id)[User class]; BOOL r3 = [cls4 isKindOfClass:cls3]; BOOL r4 = [cls4 isMemberOfClass:cls3]; NSLog(@"%d %d %d %d",r1, r2, r3, r4);//结果是 1 0 0 0
之所以费力的证明Block是个OC对象,是因为这可以更好的认知Block,得到很多的信息和用法。我们或许可以像用普通的OC对象一样使用Block,可以方便得被ARC管理,不用担心内存泄露或者非法访问。weak,autorelease等等也都可以使用,还可以放在集合里面,可以被别的对象持有,当然也可以持有别的对象,了解到这一点对于我们分析block的相关的内存管理和循环引用意义重大。
但在重写的C++的代码中我们看不到编译器帮我们插入的release,retain这样的代码,所以我们不得不用别的办法来了解Block具体是否被ARC管理的。
Block本身的内存管理
首先要明确一件事:Block本身内存的管理和Block捕获的对象的内存管理是两个问题。这里我们先讨论前者。
前面遗留了一个问题就是,代码里面isa指针明明指向了_NSConcreteStackBlock
,怎么到了运行时的时候就变成了__NSGlobalBlock__
?
我们再做一个实验,将代码改为
void afunc() {
__unsafe_unretained void(^blockA)(void) = ^{};
blockA();
}
int main(int argc, char * argv[]) {
afunc();
return 0;
}
在blockA()处下个断点,查看debug数据,发现isa指针确实指向的是__NSGlobalBlock__
,也就是说在这之前就被更新了,目前我还没有找到这个更新时机。
我们发现调用blockA()可以成功,没有crash,我尝试取了一下retainCount发现是1,去掉__unsafe_unretained
也一样。
注意:通过kvc可以获取对象的引用计数,如果一个函数来打印对象的引用计数,这函数的参数声明是有讲究的
void printRetainCount(__unsafe_unretained id o) {
void *p = (__bridge void *)o;
NSLog(@"%p:%d",p,[o valueForKey:@"retainCount"]);
}
参数需要用
__unsafe_unretained
来修饰,最好不要用强引用,这会导致引用计数器+1,更不能用weak,这会导致每次使用weak对象的时候,retainCount都会增加,这个坑一不小心就会忽略,导致获取的数据可能不准确,关于这个问题具体情况以后有机会再讨论。
修改代码增加全局__weak void(^blockB)(void) = nil;
,并在afunc()对其赋值,在main()中调用blockB(),发现也可以调用成功,并没有crash。通过__NSGlobalBlock__
这个名字大概可以猜测出这个是一个全局的block,其生命周期全局有效,即使主动调用copy,也不会入堆,似乎不受ARC控制。对照源码可知,globalblock其实并不依赖外部数据,只要有代码入口就可以使用,甚至不需要知道block,只有有函数入口地址就可以直接调用,而另外两种都需要通过block去调用,而不能直接调用block内函数指针(当然要是自己准备各种参数也是可以的)。
将代码修改为:
void afunc() {
int a = 100;
__unsafe_unretained void(^blockA)(void) = ^{
int b = a;
};
blockA();
}
int main(int argc, char * argv[]) {
afunc();
return 0;
}
在blockA()处下个断点,查看debug数据,发现isa指针指向的是__NSStackBlock__
,去掉 __unsafe_unretained
后isa指针变成了__NSMallocBlock__
。
我们发现调用blockA()可以成功,没有crash,我尝试取了一下retainCount发现是1,去掉__unsafe_unretained
也一样,😓,跟一般的OC对象不太一样?
再次修改代码和上面一下增加__weak void(^blockB)(void) = nil
,在main中调用blockB(),发现其crash了,证明blockB已经无效了。再次将__unsafe_unretained
修改为__autoreleasing
,发现其可以调用成功,所以证明block此时被autoreleasepool接管了,看上去ARC还是有作用的。
那么在ARC下,如果增加一个强引用指向block会不会导致retainCount增加呢?通过实验发现不会,依旧是1,这一点又和普通的对象不太一样。
难道这就是真相,no,这无法解释之前观察到的各种现象。我多次运行,多次调用并打印block的地址,发现其地址都一样。
Printing description of *(blockA).__FuncPtr:
(void (*)(NSString *, int)) __FuncPtr = 0x0000000100f66570 (Block`__afunc_block_invoke at main.m:58)
仔细一看,发现其打印的地址和__FuncPtr地址一样,那么同理取的block的retainCount也就可有能不正确,去objc源码中搜索了一下发现其实现为
-(NSUInteger)retainCount {
return (_rc_ivar + 2) >> 1;
}
也就是说,只要对象没有被释放,那么其retainCount至少是1。换句话说,如果某个对象没有_rc_ivar
,或者_rc_ivar=0
,那么其结果都是1,所以这里通过KVC取retainCount在block这里并不可靠,因为ARC机制下并不允许访问retainCount,所以其可靠性在有些情况还是会受到质疑的,不足以作为判断标准。但是我们发现一个问题,就是分配在栈上的block出了作用域已经无效了,那也就是说block应该在一定程度上受到ARC机制的约束,这需要进一步求证。
还记isa_a定义么,接下来我们去撸一下完整源码:
union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};
}
我们这次只关注其中shiftcls和extra_rc,前者是存放isa指针存储数据的真正空间,后者是存放对象额外引用计数的,如果这里19 bits还不够的话,就使用sidetable来记录。也就是说,绝大部分情况下,引用计数器是存在对象的isa里面的,所以我们只需要去查看isa的内存值,解析最后19bits的值就可以得到引用计数。
打个断点,在这里选择debug窗口左侧的variables view窗口,选中某个block指针,右键 view memery of "*blockA"
,可能不能浏览,所以view memery of "blockA"
,我这里是内存地址0x16ee9f8f8存储了以下数据
90 54 44 C0 01 00 00 00
将其构建出地址0x01c0555490,在Address中输入该值,跳转到新地址,我这里结果如下:
88 FF A7 B3 01 00 00 00 02 00 00 C3 00 00 00 00 70 65 F6 00 01 00 00 00
看到最后一个8个byte,有点眼熟,就是之前打印的__FuncPtr = 0x0000000100f66570
。那前俩byte存的是啥呢?第一个byte明显是一个指针,打印一下,就是__NSMallocBlock__
,那剩下的8byte呢?从block的数据结构了解到其对应的就是
int Flags;//4byte,包含的引用个数
int Reserved;//4byte
后者是0,前者是有值而且会变化,我尝试再给block一个强引用,发现02 00 00 C3变成了04 00 00 C3,再赋值就变成了06 00 00 C3,所以这个06应该就是引用计数器,而且也符合retainCount的运算逻辑,从内存布局上看,19bits的存储位置应该在一个8-byte的末尾,也就是包含02这段空间,但只是不太了解为啥isa被分成了两个64bits存储。
同理我尝试了仅仅在stack上的block,其数据位00 00 00 C2,计数器为0。
同理我尝试了global的block数据位00 00 00 50,计数器为0。
结果符合预期,除了进入堆上block会受ARC约束,其他的block都不需要ARC参与就可以完成内存管理。
小结
- Block如果不捕获外界变量,就没有上下文依赖,编译器会将其标记为global类型(当然可能编译器标记为stack,运行时优化glabal常量);否则编译器会在创建时将其标记为stack,当运行时对象被强引用时或者主动调用copy会被标记为malloc类型。
- global和stack的block都不需要ARC参与内存管理。malloc的block将受到ARC管理,包括autorelease和weak。
Block参数传递
前面的小节研究了Block的本质和其本身的内存管理,我们几乎可以把他当做普通对象来使用,同时其拥有唯一的成员函数,其执行所要依赖的数据来源有两个,一个是当前上下文环境的各种变量,另外就是调用方的传参。block传参和函数传参并没有什么不同,这里就不做具体讨论。
Block如何捕获外界变量
之前为了重写简单我并没有引入OC基础框架,而要将一般的OC代码转成C++,比如以下代码就引用了NSString:
typedef void (^ABlock)(void);
ABlock afunc() {
NSString *a = @"this is a demo";
void(^blockA)(void) = ^{
NSString *b = a;
};
return blockA;
}
int main(int argc, char *argv[]) {
ABlock aBlock = afunc();
aBlock();
return 0;
}
对于这类引用了OC类型的代码,需要使用clang -rewrite-objc -fobjc-arc -fobjc-runtime=macosx-10.10 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m
可以将OC代码转成C++代码。
这里需要指定编译的sdk库(我使用的是iPhoneSimulator.sdk),否则会出现“UIKit/UIKit.h” file not found,还需要指定-fobjc-arc
开启ARC的编译开关和-fobjc-runtime=macosx-10.10
,否则会出现“cannot create __weak reference in file using manual reference counting”类似的错误。
编译真机的话需要指定支持的CPU架构和库等(折腾了挺久才试出这些参数,😂)clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-10.0 -arch arm64 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk main.m
编译器会根据捕获的原始变量的不同情况,定义不同类型的变量来存储这些数据。
根据变量定义类型,这里我分成以下几类:
- 以下是捕获一个基本类型临时变量i的c++代码
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
//如果有捕获外部变量的话会定义在这里
int i;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _i, int flags=0) : i(_i) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以看到,编译器定义了一个int i
来对应外界的int i
,同时接收了外界i的值来初始化。
- 同理如果是局部指针这里将定义为对应的指针,例如这里是 int *ip。
- 如果需要捕获的是一个局部OC对象,其实和2中情况一致,不同之处在于ARC会介这个对象的管理。
- 对于全局变量,因为访问是开放的,所以编译器不需要做处理,直接使用该变量就行。
根据变量定义的附加修饰特性:
- 对于局部static变量,因为访问不开放,所以会被编译器升级为指针,例如:static int staticInt = 100,会定义一个int *staticInt来捕获staticInt的地址,以便以后修改。
- 对于__weak修饰的对象引用,这个重点说明。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
NSString *__weak weakSelf;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSString *__weak _weakSelf, int flags=0) : weakSelf(_weakSelf) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以看到对于__weak修饰的引用,编译器也在Block中定义一个一模一样的引用,然后通过构造函数通过参数传入初始化结构体(c++中struct和class绝大部分情况是等效的),这是意味着什么呢?我们知道所有函数参数传递就一种方式——copy,这里的参数捕获间接套用了该性质。一句话来说,对象还是那个对象,但引用已经不是那个引用了。
这里我画了一个简图,实际上每个weakSelf都是不一样的,只是其指示的内容是同样的。
- __block修饰的对象,这里改写为c++代码出错,我没能解决这个问题。所以就只有推测了,其做法应该和局部的static变量捕获差不多,都会定义一个同类型的指针或者引用,以便可以在block中访问该变量修改变量。
小结
- 参数捕获和参数传递,前者发生在block定义初始化的时候,是对当前现场的一种保存,后者发生在调用的时候传参,其存储上的区别是前者是成员变量持续存储,后者是临时变量。相同之处就是获取方式完全一致,都是函数参数传递。
- 编译器会对待不同类型的参数捕获处理方式都一样,全部浅拷贝;对于不同修饰参数则不太一样,会根据不同的情况来决定是否升级为指针捕获;OC对象将会引入ARC机制去管理。
Block循环引用及解决办法
如果能明确认识到block就是个对象,那么造成循环引用的原因就不难理解了,block可以持有对象也可以被对象持有,如果两者直接或者间接包含同一对象时就成了环,实际上就是object->block(->…)->object。
那么为什么用weak strong dance就可以解决这个问题呢?看下面这个典型例子。
__weak typeof(self) weakSelf = self;
void (^block)(void) = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
};
通过前面的C++的代码分析,答案已经很清晰了,这里就再解释一次:
我们知道block外部定义了一个weakSelf(为了方便说明,可以认为是weakSelf1),而在block内部并没有直接使用这个weakSelf1(就是没有使用这个weakSelf1这个硬编码或者说其对应的地址),而是另外定义了一个对应的构造函数参数__weak weakSelf
(weakSelf2),通过指针copy传参的方式,weakSelf2指向了weakSelf1指向的内容,同时block内部的成员变量 __weak weakSelf
(weakSelf3)通过weakSelf赋值也指向了weakSelf1指向的内容。所以从始至终这些weakSelf都不是同一个东西。至于strongSelf就简单了,对象赋值给强引用会导致retainCount+1,还记我之前文章里面的观点么,ARC是用栈管理引用,用引用生命周期管理对象,所有strongSelf生命周期结束,自然retainCount-1。
所以在block还没有执行的时候,self的生命周期不受block影响,如果执行的时候self已经被释放, weakSelf3=nil,也不会导致问题,但是如果weakSelf3还有值,strongSelf就会导致retainCount+1。有很多人认为,无论如何必须等到block执行完或者销毁self才会释放是不正确的。仔细对照block和delegate就会发现两者在这方面其实本质是一样的的,如果delegate不使用weak也一样可能循环引用。还是那句话,内存中通信就一个招,拿到地址,所以无论是直接的delegate,block,target-action,还是间接的Notification,或者其他的玩法都一样。
注意:__strong不能省略。
当然并不是说见到block就需要weak strong dance,对于以下情况就可以不使用(从调用方和回调方分析)
- 如果能确定block的调用方并不会长期持有block,比如传给B一个A持有的block,B并不存储,而是立刻回调,常见的就是把block当函数参数传递。
- 如果确定block调用方会在必要的时候去除强引用,比如:dispatch_async,其虽然会被队列强引用,但在block回调的时候,
_dispatch_call_block_and_release
会在执行完release,这也不会导致循环引用。 - block创建方不会直接或间接强引用block。
- 对于绝不可能持有block的对象,可以放心捕获,比如NSString,NSDate,NSURL等等,但对于一些可能存储block需要小心,比如:NSArray,NSDictionary,自定义的对象(self)。
如果你是创建方,不想去分析也不知道调用方干了什么,建议就无脑weak strong dance,几乎可以就可以解决问题了。如果你是调用方,会麻烦一些,需要具体问题具体分析。
Block捕获对象的内存管理
这分成三个方面,如果只是基本类型,那就不需要操心;如果是C指针,那指向对象的生命周期需要开发者手动管理;如果是个OC对象,内存管理由ARC代劳,只需要注意一些特殊情况就好。前两者不做讨论,研究一下后者。
typedef void(^ABlock)();
void pc(__unsafe_unretained id o) {
void *p = (__bridge void *)o;
NSLog(@"%@ %p:%@",o, p, [o valueForKey:@"retainCount"]);
}
@interface BlockDemo : NSObject
@end
@implementation BlockDemo
static int global = 1000;
- (ABlock)afunc:(NSString *)string {
pc(self);
pc(string);
ABlock b;
ABlock c;
__weak typeof(self) weakSelf = self;
b = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
pc(strongSelf);
pc(string);
};
c = b;
b();
pc(self);
pc(string);
return b;
}
@end
int main(int argc, char * argv[]) {
NSString *string = [[NSString alloc] initWithUTF8String:"this is a demo"];
pc(string);
BlockDemo *block = [BlockDemo new];
ABlock a = [block afunc:string];
a();
}
此时输出日志如下:
2018-05-15 11:44:46.626942+0800 Demo2[3175:1513369] this is a demo 0x1c403cc40:1
2018-05-15 11:44:46.627326+0800 Demo2[3175:1513369] <Block: 0x1c400e690> 0x1c400e690:1
2018-05-15 11:44:46.627515+0800 Demo2[3175:1513369] this is a demo 0x1c403cc40:2
2018-05-15 11:44:46.627588+0800 Demo2[3175:1513369] <Block: 0x1c400e690> 0x1c400e690:2
2018-05-15 11:44:46.627719+0800 Demo2[3175:1513369] this is a demo 0x1c403cc40:4
2018-05-15 11:44:46.627786+0800 Demo2[3175:1513369] <Block: 0x1c400e690> 0x1c400e690:1
2018-05-15 11:44:46.627810+0800 Demo2[3175:1513369] this is a demo 0x1c403cc40:4
2018-05-15 11:44:46.627995+0800 Demo2[3175:1513369] <Block: 0x1c400e690> 0x1c400e690:2
2018-05-15 11:44:46.628072+0800 Demo2[3175:1513369] this is a demo 0x1c403cc40:2
可以看到BlockDemo只在main中被持有+1,然后调用时被strongSelf持有+1,weakSelf并没导致引用计数器增加,与输出日志相符。
进入afunc:时,string引用计数为2,一个发生在main,一个发生在afunc:的参数中声明中。调用b()时,retainCount=4,这是因为这是存在一个StackBlock和一个MallocBlock,这两个都会有一个引用指向string。在afunc:函数末尾打印string是4也是同样的原因;当afunc:执行完后,StackBlock已经释放,返回block给main中的a,此时调用a(),输出2,其中引用来自于MallocBlock,符合预期。
c=b这句赋值,只引起block计数器增加,而不会导致捕获OC对象引用计数器增加,符合预期。
我们在lldb设置俩符号断点:
breakpoint set -n _Block_copy
breakpoint set -n _Block_release
可以发现_Block_copy和普通对象retain时机调用类似。
需要注意的问题
-
block捕获变量防止循环引用容易漏掉一些情况,在捕获时需要多注意,举个例子,直接捕获成员变量。
假设在一个对象方法里面,比如ViewController void (^block)(void) = ^{ //这里是等效于self->_name,编译器编码为self+offset(_name),依然会导致强引用 NSString *name = _name; };
-
直接修改捕获的变量不能成功,因为里外的两个array不是一个array,需要加上
__block
,变量捕获通过函数传参的方式实现,而传参全是copy。NSArray *array = @[@1,@2]; void (^block)(void) = ^{ array = @[]; };
附加内容
之前从宏观层面了解了block和其捕获对象的生命周期,但具体是怎样还是不太清晰,有兴趣的话可以看下面一段内容,具体了解block是怎么玩的,汇编较长,看起来也比较绕,没兴趣的话可以忽略,看看其中的一些要点。
源码:
typedef void (^ABlock)(void);
ABlock afunc() {
NSString *a = @"demo";
void(^blockA)(void) = ^{
NSString *b = a;
};
return blockA;
}
int main(int argc, char *argv[]) {
ABlock aBlock = afunc();
aBlock();
return 0;
}
汇编:
.section __TEXT,__text,regular,pure_instructions
.ios_version_min 11, 0
.file 1 "/Users/Wei/File/program/Block" "/Users/Wei/File/program/Block/Block/main.m"
.globl _afunc ; -- Begin function afunc
.p2align 2
_afunc: ; @afunc
Lfunc_begin0:
.loc 1 33 0 ; /Users/Wei/File/program/Block/Block/main.m:33:0
.cfi_startproc
; BB#0:
sub sp, sp, #112 ; =112
stp x29, x30, [sp, #96] ; 8-byte Folded Spill
add x29, sp, #96 ; =96
Lcfi0:
.cfi_def_cfa w29, 16
Lcfi1:
.cfi_offset w30, -8
Lcfi2:
.cfi_offset w29, -16
Ltmp0:
.loc 1 34 15 prologue_end ; /Users/Wei/File/program/Block/Block/main.m:34:15
adrp x0, l__unnamed_cfstring_@PAGE
add x0, x0, l__unnamed_cfstring_@PAGEOFF
bl _objc_retain
stur x0, [x29, #-8]
add x0, sp, #40 ; =40
.loc 1 36 11 ; /Users/Wei/File/program/Block/Block/main.m:36:11
add x30, x0, #32 ; =32
.loc 1 36 27 is_stmt 0 ; /Users/Wei/File/program/Block/Block/main.m:36:27
adrp x8, __NSConcreteStackBlock@GOTPAGE
ldr x8, [x8, __NSConcreteStackBlock@GOTPAGEOFF]
str x8, [sp, #40]
mov w9, #-1040187392
str w9, [sp, #48]
mov w9, #0
str w9, [sp, #52]
adrp x8, ___afunc_block_invoke@PAGE
add x8, x8, ___afunc_block_invoke@PAGEOFF
str x8, [sp, #56]
adrp x8, ___block_descriptor_tmp@PAGE
add x8, x8, ___block_descriptor_tmp@PAGEOFF
str x8, [sp, #64]
ldur x8, [x29, #-8]
str x0, [sp, #32] ; 8-byte Folded Spill
mov x0, x8
str x30, [sp, #24] ; 8-byte Folded Spill
bl _objc_retain
str x0, [sp, #72]
.loc 1 36 11 ; /Users/Wei/File/program/Block/Block/main.m:36:11
ldr x0, [sp, #32] ; 8-byte Folded Reload
bl _objc_retainBlock
stur x0, [x29, #-16]
.loc 1 44 12 is_stmt 1 ; /Users/Wei/File/program/Block/Block/main.m:44:12
ldur x0, [x29, #-16]
bl _objc_retainBlock
sub x8, x29, #16 ; =16
mov x30, #0
.loc 1 45 1 ; /Users/Wei/File/program/Block/Block/main.m:45:1
str x0, [sp, #16] ; 8-byte Folded Spill
mov x0, x8
mov x1, x30
str x30, [sp, #8] ; 8-byte Folded Spill
bl _objc_storeStrong
ldr x0, [sp, #24] ; 8-byte Folded Reload
ldr x1, [sp, #8] ; 8-byte Folded Reload
bl _objc_storeStrong
sub x0, x29, #8 ; =8
ldr x1, [sp, #8] ; 8-byte Folded Reload
bl _objc_storeStrong
ldr x0, [sp, #16] ; 8-byte Folded Reload
ldp x29, x30, [sp, #96] ; 8-byte Folded Reload
add sp, sp, #112 ; =112
b _objc_autoreleaseReturnValue
Ltmp1:
Lfunc_end0:
.cfi_endproc
; -- End function
.p2align 2 ; -- Begin function __afunc_block_invoke
___afunc_block_invoke: ; @__afunc_block_invoke
Lfunc_begin1:
.loc 1 36 0 ; /Users/Wei/File/program/Block/Block/main.m:36:0
.cfi_startproc
; BB#0:
sub sp, sp, #48 ; =48
stp x29, x30, [sp, #32] ; 8-byte Folded Spill
add x29, sp, #32 ; =32
Lcfi3:
.cfi_def_cfa w29, 16
Lcfi4:
.cfi_offset w30, -8
Lcfi5:
.cfi_offset w29, -16
stur x0, [x29, #-8]//sp+24的位置
Ltmp2:
.loc 1 36 28 prologue_end ; /Users/Wei/File/program/Block/Block/main.m:36:28
mov x8, x0
str x8, [sp, #16]
Ltmp3:
.loc 1 37 20 ; /Users/Wei/File/program/Block/Block/main.m:37:20
ldr x8, [x0, #32] //x0是block首地址,x0+32是捕获的第一个变量位置,就是NSString
mov x0, x8
bl _objc_retain
mov x8, #0
add x30, sp, #8 ; =8
str x0, [sp, #8] //将其存在了栈上sp+8的位置,就是b变量
Ltmp4:
.loc 1 38 5 ; /Users/Wei/File/program/Block/Block/main.m:38:5
mov x0, x30
mov x1, x8
bl _objc_storeStrong
ldp x29, x30, [sp, #32] ; 8-byte Folded Reload
add sp, sp, #48 ; =48
ret
Ltmp5:
Lfunc_end1:
.cfi_endproc
; -- End function
.p2align 2 ; -- Begin function __copy_helper_block_
___copy_helper_block_: ; @__copy_helper_block_
Lfunc_begin2:
.loc 1 38 0 ; /Users/Wei/File/program/Block/Block/main.m:38:0
.cfi_startproc
; BB#0:
sub sp, sp, #48 ; =48
stp x29, x30, [sp, #32] ; 8-byte Folded Spill
add x29, sp, #32 ; =32
Lcfi6:
.cfi_def_cfa w29, 16
Lcfi7:
.cfi_offset w30, -8
Lcfi8:
.cfi_offset w29, -16
mov x8, #0
stur x0, [x29, #-8] //目标地址
str x1, [sp, #16] //block
Ltmp6:
.loc 1 36 27 prologue_end ; /Users/Wei/File/program/Block/Block/main.m:36:27
ldr x0, [sp, #16]//block
ldur x1, [x29, #-8]//目标地址
mov x9, x1
add x9, x9, #32 //目标地址 ; =32
ldr x0, [x0, #32] //对象a
str x8, [x1, #32]
str x0, [sp, #8] ; 8-byte Folded Spill
mov x0, x9
ldr x1, [sp, #8] //对象a ; 8-byte Folded Reload
bl _objc_storeStrong
ldp x29, x30, [sp, #32] ; 8-byte Folded Reload
add sp, sp, #48 ; =48
ret
Ltmp7:
Lfunc_end2:
.cfi_endproc
; -- End function
.p2align 2 ; -- Begin function __destroy_helper_block_
___destroy_helper_block_: ; @__destroy_helper_block_
Lfunc_begin3:
.loc 1 36 0 ; /Users/Wei/File/program/Block/Block/main.m:36:0
.cfi_startproc
; BB#0:
sub sp, sp, #32 ; =32
stp x29, x30, [sp, #16] ; 8-byte Folded Spill
add x29, sp, #16 ; =16
Lcfi9:
.cfi_def_cfa w29, 16
Lcfi10:
.cfi_offset w30, -8
Lcfi11:
.cfi_offset w29, -16
mov x8, #0
str x0, [sp, #8]
Ltmp8:
.loc 1 36 27 prologue_end ; /Users/Wei/File/program/Block/Block/main.m:36:27
ldr x0, [sp, #8]
add x0, x0, #32 ; =32
mov x1, x8
bl _objc_storeStrong
ldp x29, x30, [sp, #16] ; 8-byte Folded Reload
add sp, sp, #32 ; =32
ret
Ltmp9:
Lfunc_end3:
.cfi_endproc
; -- End function
.globl _main ; -- Begin function main
.p2align 2
_main: ; @main
Lfunc_begin4:
.loc 1 47 0 ; /Users/Wei/File/program/Block/Block/main.m:47:0
.cfi_startproc
; BB#0:
sub sp, sp, #64 ; =64
stp x29, x30, [sp, #48] ; 8-byte Folded Spill
add x29, sp, #48 ; =48
Lcfi12:
.cfi_def_cfa w29, 16
Lcfi13:
.cfi_offset w30, -8
Lcfi14:
.cfi_offset w29, -16
stur wzr, [x29, #-4]
stur w0, [x29, #-8]
stur x1, [x29, #-16]
Ltmp10:
.loc 1 50 16 prologue_end ; /Users/Wei/File/program/Block/Block/main.m:50:16
bl _afunc
.loc 1 50 12 is_stmt 0 ; /Users/Wei/File/program/Block/Block/main.m:50:12
; InlineAsm Start
mov x29, x29 ; marker for objc_retainAutoreleaseReturnValue
; InlineAsm End
bl _objc_retainAutoreleasedReturnValue
str x0, [sp, #24]
.loc 1 51 5 is_stmt 1 ; /Users/Wei/File/program/Block/Block/main.m:51:5
ldr x0, [sp, #24]
mov x1, x0
ldr x0, [x0, #16]
str x0, [sp, #16] ; 8-byte Folded Spill
mov x0, x1
ldr x1, [sp, #16] ; 8-byte Folded Reload
blr x1
mov x0, #0
add x1, sp, #24 ; =24
.loc 1 62 5 ; /Users/Wei/File/program/Block/Block/main.m:62:5
stur wzr, [x29, #-4]
.loc 1 63 1 ; /Users/Wei/File/program/Block/Block/main.m:63:1
str x0, [sp, #8] ; 8-byte Folded Spill
mov x0, x1
ldr x1, [sp, #8] ; 8-byte Folded Reload
bl _objc_storeStrong
ldur w0, [x29, #-4]
ldp x29, x30, [sp, #48] ; 8-byte Folded Reload
add sp, sp, #64 ; =64
ret
Ltmp11:
Lfunc_end4:
.cfi_endproc
我们需要从上层函数入手,只有了解了传入的参数具体分析,才比较容易了解代码功能,不然就头疼了,这里就是先分析main函数。
main:
- bl _afunc,没有参数直接跳转,从源码可知返回了一个block对象在x0中,bl _objc_retainAutoreleasedReturnValue表明其被autoreleasepool管理。
- 接下将x0存在了sp+24这里,再下一句没有啥意义。
- 将x0赋值给x1,挪出空间,加载x0+16的值到x0,找到最开始struct __block_impl的内存布局,发现这个地址存放的是回调函数的指针。
- 接下来通过[sp, #16]中转,将x1和x0内容互换,至此x0是block首地址,x1是回调函数地址;blr x1跳转到x1;2,3,4步加一起就是源码里面的aBlock()。
- 最后那段就是在release之前的block对象。
_afunc:
最前面栈增长了112个byte,这里局部变量较多所以,栈分配的较大。
- 直接看Ltmp0:,前面一个_objc_retain是引用了字符串"demo"
. stur x0, [x29, #-8],将x0("demo")存在了x29-8这个位置,也就是sp+88的位置。
然后x0=sp+40,x30=sp+72
-
接下来两句加载"__NSConcreteStackBlock"符号对应的地址,然后将其存在sp+40这个地址,而x0目前是指向这个地址的。
struct __block_impl { void *isa; //8byte,isa指针,很重要的标志,意味着block很可能是个OC的类 int Flags;//4byte,包含的引用个数 int Reserved;//4byte void *FuncPtr;//8byte,回调函数指针 }; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc;//8byte,block的描述 //如果有捕获外部变量的话会定义在这里 id a; };
这里贴一个内存布局,sp+40就是
__main_block_impl_0
首地址,也是isa地址,这里就是“StackBlock”类似符号。 接下来就简单了,依次存储了Flags(sp+48),Reserved(sp+52)各4个byte。
存储FuncPtr指针,就是汇编符号
___afunc_block_invoke
对应的地址,sp+56的8个byte。arm64指针占8个byte。存储Desc,这也是个指针,存储的是
___block_descriptor_tmp
对应的地址,其记录了___copy_helper_block
,___destory_helper_block
和method signature,block size等信息,对应sp+64的8个byte。接下来加载x29-8的内容到x8就是"demo"字符串。然后将x0暂存在sp+32,同时将x0=x8,然后存储x30到sp+24
调用_objc_retain,参数x0,所以结果是retain了“demo”一次。之后将x0存储在了sp+72这里,就是
struct __main_block_impl_0
中的id a
,id a
是我随意写的,但实际定义也应该差不多的。需要注意的是,这里的调用是因为创建了一个StackBlock,其也是要使用"demo"这个数据的,所以retain了一次。但MallocBlock的引用计数则由___copy_helper_block来管理。)然后将sp+32的内容加载到x0,也就是sp+40即
__main_block_impl_0
的首地址。调用_objc_retainBlock,去苹果源码中找一下:
id objc_retainBlock(id x) {
return (id)_Block_copy(x);
}
其调用了_Block_copy
,这个函数在Block.h中声明的,我没有找到相关的实现源码,不过可以证明的是对于block来说retain和copy效果一致。
-
再次调用了
_objc_retainBlock
这里的两次调用一次是赋值给blockA,一次是return blockA造成的。
-
后面有三个
_objc_storeStrong
,x1=0,都是在做release操作,具体过程比较繁琐,我就直接给结论了,其中第一个release一次blockA,第二release一次“demo”字符串(提示:sp+24存的是x30,x30=sp+40+32,这里存的是“demo”),第三个也是release一次"demo"(sub x0, x29, #8,提示一下,x29=sp+112-96,所以这句也是x0=sp+24)通过这里的分析,对于Block捕获对象,ARC怎么作用的,相应的数据结构:block retain会被copy,捕获OC类型参数的时候会retain参数,参数的传递方式——拷贝。
___afunc_block_invoke:
- ldr x8, [x0, #32],x0是block的首地址,x0+32就是变量a的地址,mov x0,x8,bl _objc_retain,retain了a这个对象,和源码功能一致。
- 后面就是在release这个对象,源码里面也确实没有别的操作了。
___copy_helper_block:
这个这里找不到调用方,所以传递的参数就无法知道。先尝试分析一下:看它使用了x0,x1俩寄存器,应该有俩参数。所以顺序分析可能就不太好使了,但我们发现这里就只调用了_objc_storeStrong
,这个函数就比较熟悉了,第一个参数是id *,第二个参数是id,那我们就倒着分析。x0=x9,x9=x9+32,x9=x1,x1=(x29-8)=x0,所以x0=x0+32,再看x1=(sp+8)=x0=[x0+32]=(sp+16)=x1,所以x1=[x1+32](其中小括号是内存暂存,方括号是加载该内存地址的数据)。见到“+32”偏移量就熟悉了,这就是之前a的地址,整个函数的功能就是retain并且store一下block中捕获的变量a,如果有多个引用将会有多次这种操作,但不适用于基本数据类型。
理论分析确实很麻烦,但这里提供另外一种办法就是运行下断点breakpoint set -n __copy_helper_block_
,打印x0,x1
可以看到在_Block_copy
中调用这个函数,打印一下
(lldb) po $x0
<__NSStackBlock__: 0x1c0250680>
(lldb) po $x1
<__NSStackBlock__: 0x16afeb8f8>
这里也可以通过直接浏览的方式
在_Block_copy
调用时其还是在栈block,不同的是虽然x0(目标地址)的isa还是指向StackBlock,但实际内容已经是MallocBlock,比如x0已经产生引用计数了。(我研究了一下_Block_copy
汇编代码,其会malloc一段新的内存,将数据填充过去,同时修改Flags的值,Reserved字段全被赋值为了0,x0是新地址,x1是旧地址,然后跳转__copy_helper_block_
,做OC参数的retain操作)
___destroy_helper_block:
这个调用_objc_storeStrong
,x1=x8=0,很明显是在做release。
总结一下:
- 每一个block背后都有一个struct做数据支撑,与一般的对象的结构组织和行为模式基本一致。一般的对象是一份数据结构可以对应多个方法,而block却是一个方法对应多个数据,导致其占用资源较多。
- Block的OC类型参数捕获时,如果只是栈Block,则直接插入retain和release解决对象引用的问题。如果Block对象被拷贝到堆上,则需要通过调用
_Block_copy
通过对应的的___copy_helper_block
和___destroy_helper_block
函数来支撑捕获对象的生命周期管理。 - __main_block_desc_0还会同时保存的方法签名(这里是v8@?0),还有block的大小,捕获参数个数会造成这个大小的改变。
最后说一下Block零碎的东西
-
在Block_private.h文件中发现,除了我们熟知的三种block意外还有三种运行时的block
BLOCK_EXPORT void * _NSConcreteAutoBlock[32] __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2); BLOCK_EXPORT void * _NSConcreteFinalizingBlock[32] __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2); BLOCK_EXPORT void * _NSConcreteWeakBlockVariable[32] __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
目前还不知道具体用途。
-
BLOCK_EXPORT size_t Block_size(void *aBlock); BLOCK_EXPORT const char * _Block_signature(void *aBlock) __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_4_3); BLOCK_EXPORT const char * _Block_layout(void *aBlock) __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_4_3);
这三个函数我比较感兴趣,但却是在Block_private.h中,但不碍事,知道名字了,就好办了。
我们知道.h头文件的一个重要作用就是编译指示,啥意思呢?简单来说就是告诉编译器当前环境的其他编译模块有某些符号,到时候编译器自己去寻找并链接。所以我们只需要在使用前做以下声明就行
extern const char *_Block_signature(id block); extern size_t Block_size(id block); extern const char* _Block_layout(id ablock);
我调用了一下,发现_Block_layout输出是个null,不清楚有啥作用。
Block_size调用结果是40,因为捕获了一个NSString*(8byte)+ Block基础的32byte,正好。
_Block_signature
输出v20@?0@"NSString"8i16
,我的Block原型是typedef void (^ABlock)(NSString *s, int i);
其中v是void,20是总共需要内存大小,@?是Block的encode,0是第一个默认参数block结构体从偏移量0开始,@"NSString"8,则指NSString从偏移量8开始,最后是i从偏移量16开始占4位。
有了这么详细的签名,动态调用或者动态替换实现就方便了,也许用它还能搞一波事情。
为什么要花这么多时间去分析汇编,了解的那么详细。其实一般的情况下是用不上的,但是如果遇到了线上crash,棘手的内存问题,要debug,这时候这些知识就会很有用了,你了解的越多,你解决问题的方式就越多,就更容易解决问题。当然也不是说什么都需要去仔细了解,也不是了解的越多越好,这个就需要根据自己的兴趣和需求去决定了。但是对于基础知识,确实可以多时间和精力去完善之,有了这些才能高屋建瓴得心应手。
感谢阅读。