从汇编看Block底层
block是什么
源码:
1 | - (void)foo2 { |
Assemble后:
1 | "-[Foo foo2]": ; @"\01-[Foo foo2]" |
这里假设起始sp=0x60,方便后续计算。依次解释指令的执行过程和寄存器,栈的值。
1 | sub sp, sp, #96 ; =96 |
- sub指令;sp = sp - 0x60 => sp = 0x00
- store pair指令; x29, x30寄存器的值存到sp+0x50=0x50的地址,x29,x30表示fp,lr寄存器。
- add指令;fp = sp + 0x50 = 0x50
- store指令;x0寄存器的值存到[x29, #-8]=0x50-0x8=0x48的地址。x0=self
- 同上,x1存到0x40的地址。x1=@selector(foo2)
此时寄存器和栈的状态如下
寄存器 | 值 |
---|---|
x0 | self |
x1 | @selector(foo2) |
fp | 0x50 |
sp | 0x00 |
栈地址 | 值 |
---|---|
0x60 | 上个函数的lr |
0x58 | 上个函数的fp |
0x50 | self |
0x48 | @selector(foo2) |
1 | add x8, sp, #24 ; =24 |
- x8 = sp + 24 = 0x18
- x8寄存器的值存到0x08的地址([sp, #8] = 0x00 + 0x8)
- adrp是针对aslr技术,获取偏移后的地址;将__NSConcreteStackBlock的isa读取到x9寄存器
- 将x9寄存器的值保存到地址0x18
- w9(4字节)赋值#-1040187392
- w9寄存器的值存到地址0x20
- wzr(word zero register),地址0x24写入4字节的0。
- 同上,x9寄存器赋值”___11-[Foo foo2]_block_invoke”的地址
- x9寄存器的值存到地址0x28
- 同上将”___block_descriptor_40_e8_32s_e5_v8?0l”的地址存到地址0x30
寄存器 | 值 |
---|---|
x0 | self |
x1 | @selector(foo2) |
fp | 0x50 |
sp | 0x00 |
x8 | 0x18 |
栈地址 | 值 |
---|---|
0x60 | 上个函数的lr |
0x58 | 上个函数的fp |
0x50 | self |
0x48 | @selector(foo2) |
… | |
0x30 | “___block_descriptor_40_e8_32s_e5_v8?0l” |
0x28 | “___11-[Foo foo2]_block_invoke” |
0x20 | -1040187392; 0 |
0x18 | __NSConcreteStackBlock |
0x10 | |
0x08 | 0x18 |
0x00 |
1 | add x8, x8, #32 ; =32 |
- x8 = x8 + 0x20 = 0x38
- x8寄存器值存到地址0x10
- x0 = self
- 调用_objc_retain函数
- x2 = 0x18
- x0寄存器值存到地址0x38
- OBJC_SELECTOR_REFERENCES.2指的是@selector(setMyBlock:)。x1 = @selector(setMyBlock:)
- 调用_objc_msgSend函数
- 地址0x10的值读取到x0寄存器
- x1 = 0
- 调用_objc_storeStrong
寄存器 | 值 |
---|---|
x0 | self |
x1 | @selector(foo2) |
fp | 0x50 |
sp | 0x00 |
x8 | 0x18 |
栈地址 | 值 |
---|---|
0x60 | 上个函数的lr |
0x58 | 上个函数的fp |
0x50 | self |
0x48 | @selector(foo2) |
0x38 | self |
0x30 | “___block_descriptor_40_e8_32s_e5_v8?0l” |
0x28 | “___11-[Foo foo2]_block_invoke” |
0x20 | -1040187392; 0 |
0x18 | __NSConcreteStackBlock |
0x10 | 0x38 |
0x08 | 0x18 |
0x00 |
1 | ldp x29, x30, [sp, #80] ; 16-byte Folded Reload |
- 恢复上个函数的fp,lr寄存器值
- sp -= 0x60
- return
上述就是foo2函数的指令执行过程, 可以发现block初始化的时候是分配在栈空间,block也是个对象,有isa指正,这里是__NSConcreteStackBlock类。
通过clang -rewrite-objc Foo.m
可以看到block结构体的声明,如下:
1 |
|
栈地址 | 值 |
---|---|
0x38 | self |
0x30 | “___block_descriptor_40_e8_32s_e5_v8?0l” |
0x28 | “___11-[Foo foo2]_block_invoke” |
0x20 | -1040187392; 0 |
0x18 | __NSConcreteStackBlock |
跟栈去的内存分配完全吻合。从低地址到高地址依次为isa,Flags,Reserved,FuncPtr,Desc,self。其中self就是block捕获的变量。
接着看下block_descriptor到底是什么。
1 | .private_extern "___block_descriptor_40_e8_32s_e5_v8?0l" ; @"__block_descriptor_40_e8_32s_e5_v8\01?0l" |
可以看到___block_descriptor_40_e8_32s_e5_v8
在data段。
cpp的结构如下
1 | static struct __Foo__foo_block_desc_0 { |
结合__Foo__foo_block_desc_0
的结构,可以得到reserved=0,Block_size=40。跟上述栈的内存分配吻合。而后则是copy和destory函数。
为什么会产生循环引用
1 | bl _objc_retain ; x0 = self |
上述汇编代码中可以看到在构建block时,self被调用retain。从而导致了循环引用的问题出现。
__weak是如何解除循环引用的?
接着上述代码加上weak修饰符,如下👇🏻。
1 | - (void)foo { |
assemble:
1 | "-[Foo foo]": ; @"\01-[Foo foo]" |
从汇编中可以看到,在初始化wself的时候调用了objc_initWeak函数,在构建block结构的时候使用了objc_copyWeak。没有使用objc_retain,因此self的引用计数没有加1。从而没有循环引用的出现。
block内为什么需要__strong呢?
看👇🏻代码,block中两次引用了wself。
1 | - (void)foo { |
assemble:
1 | "___10-[Foo foo]_block_invoke": ; @"__10-[Foo foo]_block_invoke" |
1 | bl _objc_loadWeakRetained |
从这块片段可以看到在引用wself时,先调用了objc_loadWeakRetained,refcnt + 1。然后通过objc_msgSend调用了具体的方法,接着调用了objc_release,refcnt - 1。可以看到苹果底层在引用weak时还是比较严谨的。
可以看到在引用两次wself时,每次引用都会先调用objc_loadWeakRetained,紧接着调用objc_msgSend。在多线程的场景下,两次引用中间self存在被释放的情况,当self被释放后,wself就会被weak系统机制置为nil,从而导致在block的执行过程中self被释放,从而导致一些错误。
接着看一下使用strong修饰后的结果。
1 | - (void)foo { |
assemble:
1 | "___10-[Foo foo]_block_invoke": ; @"__10-[Foo foo]_block_invoke" |
在加上strong修饰后,因为后续代码中都在引用sself,所以sself在最后才通过objc_storeStrong(&sself, nil)的方式进行一次release。从而也保证了在block调用过程中,self的refcnt一直是+1的状态,在block执行过程中不存在被提前释放的情况。