苹果官方资源opensource
objc4-838可编译联调源码
libclosure源码
本章节探究:
1.Block的类型
2.Block的实质
3.内容捕获
4.__block的底层原理
5.Block的循环引用
6.面试题
一、Block的类型
1.Global Block - 全局
2.Malloc Block - 堆
3.Stack Block - 栈
__weak
修饰block
会在出方法栈的时候立马被销毁。
Block的类型
总结:
全局Block
:没有捕获外部的局部变量;只使用了全局变量/静态变量。
堆Block
:捕获了局部变量;赋值给了强引用
。
栈Block
:捕获了局部变量;赋值给了弱引用
。
为什么
Block
要⽤copy
关键字修饰?
Block
在创建的时候,它的内存是分配在方法栈上的,⽽不是在堆上。
栈区的特点是:对象随时有可能被销毁,⼀旦被销毁,在调⽤的时候,就会造成系统的崩溃。所以我们要使⽤copy
把它拷⻉到堆上。
在ARC
下,对于Block
使⽤copy
与strong
其实都⼀样,因为block
的retain
就是⽤copy
来实现的。所以在ARC
下block
使⽤copy
和strong
都可以。
二、Block的本质
把上面代码编译成.cpp文件
$ clang -rewrite-objc main.m
打开main.cpp,找到main函数
被编译之后Block
就变成了__main_block_impl_0
的数据结构了
1.NSObject *objc
就是捕获的外部局部变量;
2.impl.isa = &_NSConcreteStackBlock;
这行代码可以看出Block是一个对象
;
3.Block
在创建的时候它是栈Block
。
栈Block具备捕获外部局部变量的能力。
Block不是在调用的时候才捕获外部局部变量,而是在声明的时候就已经捕获了。
1.汇编分析Block的底层调用逻辑
- 打开汇编模式
Always Show Disassembly
给objc_retainBlock
打上符号断点,看看它底层怎么调用的
_Block_copy
的实现是放在libclosure
源码里的,参数是block
打开objc4源码找到objc_retainBlock
2.源码分析Block的底层调用逻辑
打开libclosure源码找到_Block_copy
// Copy, or bump refcount, of a block. If really copying, call the copy helper if present.
// 拷贝 block,
// 如果原来就在堆上,就将引用计数加 1;
// 如果原来在栈上,会拷贝到堆上,引用计数初始化为 1,并且会调用 copy helper 方法(如果存在的话);
// 如果 block 在全局区,不用加引用计数,也不用拷贝,直接返回 block 本身
// 参数 arg 就是 Block_layout 对象,
// 返回值是拷贝后的 block 的地址
// 运行?stack -》malloc
void *_Block_copy(const void *arg) {
struct Block_layout *aBlock;
// 如果 arg 为 NULL,直接返回 NULL
if (!arg) return NULL;
// The following would be better done as a switch statement
// 强转为 Block_layout 类型
aBlock = (struct Block_layout *)arg;
const char *signature = _Block_descriptor_3(aBlock)->signature;
// 1.如果现在已经在堆上
if (aBlock->flags & BLOCK_NEEDS_FREE) {
// latches on high
// 就只将引用计数加 1
latching_incr_int(&aBlock->flags);
return aBlock;
}
// 2.如果 block 在全局区,不用加引用计数,也不用拷贝,直接返回 block 本身
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
}
else {
// Its a stack block. Make a copy.
// 3.block 现在在栈上,现在需要将其拷贝到堆上
// 在堆上重新开辟一块和 aBlock 相同大小的内存
struct Block_layout *result =
(struct Block_layout *)malloc(aBlock->descriptor->size);
// 开辟失败,返回 NULL
if (!result) return NULL;
// 将 aBlock 内存上的数据全部复制新开辟的 result 上
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
#if __has_feature(ptrauth_calls)
// Resign the invoke pointer as it uses address authentication.
result->invoke = aBlock->invoke;
#endif
// reset refcount
// 将 flags 中的 BLOCK_REFCOUNT_MASK 和 BLOCK_DEALLOCATING 部分的位全部清为 0
result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING); // XXX not needed
// 将 result 标记位在堆上,需要手动释放;并且引用计数初始化为 1
result->flags |= BLOCK_NEEDS_FREE | 2; // logical refcount 1
// copy 方法中会调用做拷贝成员变量的工作
_Block_call_copy_helper(result, aBlock);
// Set isa last so memory analysis tools see a fully-initialized object.
// isa 指向 _NSConcreteMallocBlock
result->isa = _NSConcreteMallocBlock;
return result;
}
}
参数arg
是block
,它会先把block
强转成Block_layout
这个结构体类型。
Block的本质就是Block_layout结构体。
(后面会分析)
- 1.
aBlock已经在堆上
,只将引用计数+1; - 2.
aBlock在全局区
,不做操作返回出去; -
3.
aBlock在栈上
,现在需要将其拷贝到堆上,在堆上重新开辟一块和aBlock
相同大小的内存。(原本的栈上的在出方法栈的时候就释放掉了)
Block_layout
结构体
(注意:Block_layout
拥有Block_descriptor_2
里的copy
和dispose
函数是在捕获了堆区外部局部变量的时候才会有。用于这个变量的引用计数管理)
-
_Block_call_copy_helper
函数的作用是拷贝aBlock
的成员变量的工作。
3.为什么Block需要调用的时候才会执行
int main(int argc, const char * argv[]) {
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
}
int a = 10;
void (^block)(void) = ^ {
NSLog(@"%d",a);
};
block();
return NSApplicationMain(argc, argv);
}
将main.m
编译成main.cpp
:
$ $ clang -rewrite-objc main.m
打开main.cpp
找到mian函数
来看看__main_block_impl_0
结构体构造方法和__main_block_func_0
函数实现部分:
这里传递的a是指针(不是地址)。
给struct __block_impl impl;
指定了isa
、flags
、FuncPtr
。
__main_block_impl_0
结构体与源码里的Block_layout
是一样的。这就证实了源码分析部分没有错。
block
声明就是把方法实现保存到了__block_impl
这个结构体中去;等到调用的时候从这里取出函数实现地址直接调用。
三、内容捕获
- 捕获栈区的外部局部变量
-(void)test1 {
int a = 10;
NSLog(@"a--%p",&a);
void (^block)(void) = ^ {
NSLog(@"a--%p",&a);
};
block();
}
/**
打印结果:
a--0x7ff7bf79c29c
a--0x60000137c2f0
*/
很明显两个变量a
同一个东西,block
外部的a
是栈区地址,block
外部的a
是堆区地址。
因为在block
在栈区的时候就已经捕获了a
,当block
被copy
到堆区时,原本捕获的成员变量也需要在堆区开辟内存。
如果需要改变原本a
的值需要使用__block
来修饰a
:
-(void)test1 {
__block int a = 10;
NSLog(@"a--%p",&a);
void (^block)(void) = ^ {
NSLog(@"a--%p",&a);
a++;
};
block();
NSLog(@"a: %d", a);
}
/**
打印结果:
a--0x7ff7bf79c29c
a--0x60000137c2f0
a: 11
*/
- 捕获堆区的外部局部变量
-(void)test2 {
NSObject *objc = [NSObject new];
NSLog(@"%@",objc);
NSLog(@"%ld",(long)CFGetRetainCount((__bridge CFTypeRef)(objc)));
NSLog(@"-------------------");
void (^block)(void) = ^ {
NSLog(@"%@",objc);
NSLog(@"%ld",(long)CFGetRetainCount((__bridge CFTypeRef)(objc)));
};
block();
}
/**
打印结果:
<NSObject: 0x600003a00310>
1
-------------------
<NSObject: 0x600003a00310>
3
*/
可以看到objc
的打印是同一个东西。这是因为objc
所指向的内存分配本身就在堆区,block
在捕获到堆区的外部局部变量时,不需要另外开辟内存空间,只需要堆区block
的成员变量指针指向同一块内存空间即可。
因为栈区block
和堆区block
的成员变量都指向objc
实际的堆区内存区域,所以objc
的引用计数+2。
- 是否能捕获 全局变量、静态变量
int b = 20; // 全局变量
static int c = 30; // 静态变量
int main(int argc, const char * argv[]) {
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
}
__block int a = 10; // 局部变量
NSLog(@"a--%p",&a);
NSLog(@"b--%p",&b);
NSLog(@"c--%p",&c);
void (^block)(void) = ^ {
NSLog(@"捕获a--%p",&a);
NSLog(@"捕获b--%p",&b);
NSLog(@"捕获c--%p",&c);
a++; // 必须使用__block修饰才能修改局部变量a的值
};
block();
NSLog(@"a: %d", a);
return NSApplicationMain(argc, argv);
}
2022-06-13 20:33:01.485055+0800 Block的本质[44760:11096843] a--0x7ff7b3368248
2022-06-13 20:33:01.485157+0800 Block的本质[44760:11096843] b--0x10cb9fd58
2022-06-13 20:33:01.485189+0800 Block的本质[44760:11096843] c--0x10cb9fd5c
2022-06-13 20:33:01.485265+0800 Block的本质[44760:11096843] 捕获a--0x6000004ab938
2022-06-13 20:33:01.485300+0800 Block的本质[44760:11096843] 捕获b--0x10cb9fd58
2022-06-13 20:33:01.485327+0800 Block的本质[44760:11096843] 捕获c--0x10cb9fd5c
2022-06-13 20:33:01.485347+0800 Block的本质[44760:11096843] a: 11
可以看到捕获前后变量a的地址明显不一样,那说明他俩不是一个东西。
再把mian.m
编译成main.cpp
(请注意这里的a.__forwarding->a
,与__block有关后面会讲)
找到__main_block_impl_0
结构体声明的地方
Block
只捕获了外部局部变量a
;没有捕获全局b
和静态c
。
Block
不会捕获全局变量和静态变量。
Block
捕获的变量是在Block
内部生成新的指针指向捕获变量的内存(浅拷贝)。
四、__block
修饰局部变量的底层原理
在block
内部可以修改全局变量和静态变量的值,但是不允许修改局部变量的值。要箱子block
内部修改局部变量的值需要用__block修饰
。
int main(int argc, const char * argv[]) {
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
}
__block NSObject *obc = [NSObject new];
void (^block)(void) = ^ {
NSLog(@"%@",obc);
};
block();
return NSApplicationMain(argc, argv);
}
把mian.m
编译成main.cpp
,找到main函数
__main_block_impl_0
是构造函数,它传递一个捕获外部局部变量(__Block_byref_obc_0 *)&obc
。
同时在构造__Block_byref_obc_0
的时候传递了__Block_byref_id_object_copy_131
和__Block_byref_id_object_dispose_131
这两个函数地址
这里的__forwarding
是指向__Block_byref_obc_0
本身。
_Block_object_assign
的分析
__Block_byref_id_object_copy_131
的调用时机是在Block
从栈拷贝到堆的时候,也就是Block
源码分析环节里的_Block_call_copy_helper
函数
__Block_byref_id_object_copy_131
的底层其实就是调用了_Block_object_assign
函数
在源码中找到_Block_object_assign
的声明
//
// When Blocks or Block_byrefs hold objects then their copy routine helpers use this entry point
// to do the assignment.
// 当 block 和 byref 要持有对象时,它们的 copy helper 函数会调用这个函数来完成 assignment,
// 参数 destAddr 其实是一个二级指针,指向真正的目标指针
void _Block_object_assign(void *destArg, const void *object, const int flags) {
const void **dest = (const void **)destArg;
switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
case BLOCK_FIELD_IS_OBJECT:
/*******
id object = ...;
[^{ object; } copy];
********/
// 默认什么都不干,但在 _Block_use_RR() 中会被 Objc runtime 或者 CoreFoundation 设置 retain 函数,
// 其中,可能会与 runtime 建立联系,操作对象的引用计数什么的
_Block_retain_object(object);
// 使 dest 指向的目标指针指向 object
*dest = object;
break;
case BLOCK_FIELD_IS_BLOCK:
/*******
void (^object)(void) = ...;
[^{ object; } copy];
********/
// 使 dest 指向的拷贝到堆上object
*dest = _Block_copy(object);
break;
case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
case BLOCK_FIELD_IS_BYREF:
/*******
// copy the onstack __block container to the heap
// Note this __weak is old GC-weak/MRC-unretained.
// ARC-style __weak is handled by the copy helper directly.
__block ... x;
__weak __block ... x;
[^{ x; } copy];
********/
// 使 dest 指向的拷贝到堆上的byref
*dest = _Block_byref_copy(object);
break;
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
/*******
// copy the actual field held in the __block container
// Note this is MRC unretained __block only.
// ARC retained __block is handled by the copy helper directly.
__block id object;
__block void (^object)(void);
[^{ object; } copy];
********/
// 使 dest 指向的目标指针指向 object
*dest = object;
break;
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK | BLOCK_FIELD_IS_WEAK:
/*******
// copy the actual field held in the __block container
// Note this __weak is old GC-weak/MRC-unretained.
// ARC-style __weak is handled by the copy helper directly.
__weak __block id object;
__weak __block void (^object)(void);
[^{ object; } copy];
********/
// 使 dest 指向的目标指针指向 object
*dest = object;
break;
default:
break;
}
}
如果是__block
修饰的局部变量,它就会走case BLOCK_FIELD_IS_BYREF
代码段,去调用_Block_byref_copy(object);
_Block_byref_copy
的源码声明:
// 1. 如果 byref 原来在堆上,就将其拷贝到堆上,拷贝的包括 Block_byref、Block_byref_2、Block_byref_3,
// 被 __weak 修饰的 byref 会被修改 isa 为 _NSConcreteWeakBlockVariable,
// 原来 byref 的 forwarding 也会指向堆上的 byref;
// 2. 如果 byref 已经在堆上,就只增加一个引用计数。
// 参数 dest是一个二级指针,指向了目标指针,最终,目标指针会指向堆上的 byref
static struct Block_byref *_Block_byref_copy(const void *arg) {
// arg 强转为 Block_byref * 类型
struct Block_byref *src = (struct Block_byref *)arg;
// 引用计数等于 0
if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
// src points to stack
// 为新的 byref 在堆中分配内存
struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
copy->isa = NULL;
// byref value 4 is logical refcount of 2: one for caller, one for stack
// 新 byref 的 flags 中标记了它是在堆上,且引用计数为 2。
// 为什么是 2 呢?注释说的是 non-GC one for caller, one for stack
// one for caller 很好理解,那 one for stack 是为什么呢?
// 看下面的代码中有一行 src->forwarding = copy。src 的 forwarding 也指向了 copy,相当于引用了 copy
copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
// 堆上 byref 的 forwarding 指向自己
copy->forwarding = copy; // patch heap copy to point to itself
// 原来栈上的 byref 的 forwarding 现在也指向堆上的 byref
src->forwarding = copy; // patch stack to point to heap copy
// 拷贝 size
copy->size = src->size;
// 如果 src 有 copy/dispose helper
if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
// Trust copy helper to copy everything of interest
// If more than one field shows up in a byref block this is wrong XXX
// 取得 src 和 copy 的 Block_byref_2
struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
// copy 的 copy/dispose helper 也与 src 保持一致
// 因为是函数指针,估计也不是在栈上,所以不用担心被销毁
copy2->byref_keep = src2->byref_keep;
copy2->byref_destroy = src2->byref_destroy;
// 如果 src 有扩展布局,也拷贝扩展布局
if (src->flags & BLOCK_BYREF_LAYOUT_EXTENDED) {
struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1);
struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1);
// 没有将 layout 字符串拷贝到堆上,是因为它是 const 常量,不在栈上
copy3->layout = src3->layout;
}
// 调用 copy helper,因为 src 和 copy 的 copy helper 是一样的,所以用谁的都行,调用的都是同一个函数
(*src2->byref_keep)(copy, src);
}
else {
// Bitwise copy.
// This copy includes Block_byref_3, if any.
// 如果 src 没有 copy/dispose helper
// 将 Block_byref 后面的数据都拷贝到 copy 中,一定包括 Block_byref_3
memmove(copy+1, src+1, src->size - sizeof(*src));
}
}
// already copied to heap
// src 已经在堆上,就只将引用计数加 1
else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
latching_incr_int(&src->forwarding->flags);
}
return src->forwarding;
}
- 如果
byref
原来在堆上,就将其拷贝到堆上,拷贝的包括Block_byref
、Block_byref_2
、Block_byref_3
,被__weak
修饰的byref
会被修改isa
为_NSConcreteWeakBlockVariable
,原来byref
的forwarding
也会指向堆上的byref
; - 如果
byref
已经在堆上,就只增加一个引用计数。
理解:
我们代码的Block
在声明的时候一定是在栈上的,当把Block
拷贝到堆上时连同捕获的局部变量一起拷贝(因为都存储在结构体__block_impl
),如果此时使用__block
修饰局部变量了,原本栈上Block
的__forwording
指向栈Block
自己,拷贝到堆上Block
的__forwording
指向堆Block
自己,而此时栈上的Block
的__forwording
会改变指向堆上Block
。
__block
修饰的局部变量原本是在栈上的,需要拷贝到堆;__block
修饰的局部变量原本是在堆上的,将其引用计数+1。
_Block_object_dispose
的分析
// When Blocks or Block_byrefs hold objects their destroy helper routines call this entry point
// to help dispose of the contents
// 当 block 和 byref 要 dispose 对象时,它们的 dispose helper 会调用这个函数
void _Block_object_dispose(const void *object, const int flags) {
switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
// 如果是 byref
case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
case BLOCK_FIELD_IS_BYREF:
// get rid of the __block data structure held in a Block
// 对 byref 对象做 release 操作
_Block_byref_release(object);
break;
// 如果是 block
case BLOCK_FIELD_IS_BLOCK:
// 对 block 做 release 操作
_Block_release(object);
break;
// 如果是对象
case BLOCK_FIELD_IS_OBJECT:
// 默认啥也不干,但在 _Block_use_RR() 中可能会被 Objc runtime 或者 CoreFoundation 设置一个 release 函数,里面可能会涉及到 runtime 的引用计数
_Block_release_object(object);
break;
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK | BLOCK_FIELD_IS_WEAK:
break;
default:
break;
}
}
如果是__block
修饰的外部局部变量,会走case BLOCK_FIELD_IS_BYREF:
调用_Block_byref_release
函数(对 byref
对象做 release
操作)
_Block_byref_release
的源码声明:
// 对 byref 对象做 release 操作,
// 堆上的 byref 需要 release,栈上的不需要 release,
// release 就是引用计数减 1,如果引用计数减到了 0,就将 byref 对象销毁
static void _Block_byref_release(const void *arg) {
struct Block_byref *byref = (struct Block_byref *)arg;
// dereference the forwarding pointer since the compiler isn't doing this anymore (ever?)
// 取得真正指向的 byref,如果 byref 已经被堆拷贝,则取得是堆上的 byref,否则是栈上的,栈上的不需要 release,也没有引用计数
byref = byref->forwarding;
// byref 被拷贝到堆上,需要 release
if (byref->flags & BLOCK_BYREF_NEEDS_FREE) {
// 取得引用计数
int32_t refcount = byref->flags & BLOCK_REFCOUNT_MASK;
os_assert(refcount);
// 引用计数减 1,如果引用计数减到了 0,会返回 true,表示 byref 需要被销毁
if (latching_decr_int_should_deallocate(&byref->flags)) {
// 如果 byref 有 dispose helper,就先调用它的 dispose helper
if (byref->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
struct Block_byref_2 *byref2 = (struct Block_byref_2 *)(byref+1);
// dispose helper 藏在 Block_byref_2 里
(*byref2->byref_destroy)(byref);
}
free(byref);
}
}
}
对 byref对象
做 release
操作,堆上的 byref
需要 release
,栈上的不需要 release
,release
就是引用计数减 1,如果引用计数减到了 0,就将 byref 对象
销毁。
_Block_object_assign
与_Block_object_dispose
是在Block
捕获外部局部变量时成对出现,对捕获的外部变量进行内存管理(引用计数增减)。
总结
__block
的底层原理:用
__block
修饰的变量在编译过后会变成__Block_byref__XXX
类型的结构体,在结构体内部有一 个__forwarding
的结构体指针,指向结构体本身。block
创建的时候是在栈上的,在将栈block
拷⻉到堆上的时候,同时也会将block
中捕获的对象拷⻉到堆上,然后就会将栈上的__block
修饰对象的__forwarding
指针指向堆上的拷⻉之后的对象。 这样我们在block
内部修改的时候虽然是修改堆上的对象的值,但是因为栈上的对象的__forwarding
指针将堆和栈的对象链接起来。因此就可以达到修改的目的。
__block
不可以用于修饰静态变量和全局变量。
五、循环引用
- 案例一:
- (void)test {
self.block = ^{
self.name = @"AnAn";
};
}
原因:self
-> block
-> self
。
block
在声明实现的时候就会捕获外部局部变量,所以案例一不需要调用block
也会产生循环引用。
即便把self.name
改成_name
,在捕获的时候也会把self
对象捕获。
block
要释放必须self
要释放,可使用__weak
去修饰self
就解决循环引用:
- (void)test {
__weak typeof(self) weakSelf = self;
self.block = ^{
weakSelf.name = @"AnAn";
};
}
但是依然会有问题,比如block
里面有一个dispatch_after
-(void)test {
self.name = @"lg";
__weak typeof(self) weakSelf = self;
self.block = ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",weakSelf.name);
});
};
self.block();
}
如果controller
还没等到执行dispatch_after
就pop/dismiss
,等到3s后就会运行dispatch_after
,此时打印weakSelf.name
的结果是null
。
造成这样的结果是controller
在pop/dismiss
后就释放了。
解决方式一:强弱共舞(weak strong dance)
-(void)test {
self.name = @"lg";
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
NSLog(@"%p",&strongSelf);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",strongSelf.name);
});
};
self.block();
}
解决方式二:临时变量
-(void)test1 {
self.name = @"lg";
__block MyViewController *vc = self;
self.block = ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",vc.name);
vc = nil;
});
};
self.block();
}
解决方式三:block参数
-(void)test2 {
self.name = @"lg";
self.block = ^(MyViewController *vc){
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",vc.name);
});
};
self.block(self);
}
- 案例二:
static MyViewController *_staticSelf;
-(void)test2 {
__weak typeof(self) weakSelf = self; // self是MyViewController的实例对象
_staticSelf = weakSelf;
}
// 产生循环引用
__weak typeof(self) weakSelf = self;
的意思是用一个弱引用指针指向self
所指向的堆内存,不会对堆内存引用计数不会+1。
而_staticSelf
是不会自动释放的,所以self
也是一直不被释放。需要手动把_staticSelf = nil
- 案例三:
- (void)test3 {
__weak typeof(self) weakSelf = self;
self.block1 = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
NSLog(@"%p", &strongSelf);
weakSelf.block2 = ^{
NSLog(@"%@", strongSelf);
};
weakSelf.block2();
};
self.block1();
}
// 产生循环引用
weakSelf -> block2 -> StrongSelf
若把NSLog(@"%@", strongSelf);
换成 NSLog(@"%@", weakSelf);
就不存在循环引用了,因为block2
捕获的weakSelf
是弱引用。
六、面试题
- 第1道面试题
-(void)test {
NSObject *objc = [NSObject new];
NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef)objc));
// 栈+1 堆+1
void(^block1)(void) = ^{
NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef)objc));
};
block1();
// 栈+1
void(^__weak block2)(void) = ^{
NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef)objc));
};
block2();
// 堆+1
void(^block3)(void) = [block2 copy];
block3();
void(^block4)(void) = [block1 copy];
block4();
}
// 13455
上面的分析已经知道Block
的本质就是Block_layout
,于是我仿照源码写了一个_MyBlock
结构体:
// NSObject+Block.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
typedef void(*MyBlockCopyFunction)(void *, const void *);
typedef void(*MyBlockDisposeFunction)(const void *);
typedef void(*MyBlockInvokeFunction)(void *, ...);
enum {
BLOCK_HAS_COPY_DISPOSE = (1 << 25),
BLOCK_HAS_CTOR = (1 << 26), // helpers have C++ code
BLOCK_IS_GLOBAL = (1 << 28),
BLOCK_HAS_STRET = (1 << 29), // IFF BLOCK_HAS_SIGNATURE
BLOCK_HAS_SIGNATURE = (1 << 30),
};
struct _MyBlockDescriptor1 {
uintptr_t reserved;
uintptr_t size;
};
struct _MyBlockDescriptor2 {
// requires BLOCK_HAS_COPY_DISPOSE
MyBlockCopyFunction copy;
MyBlockDisposeFunction dispose;
};
struct _MyBlockDescriptor3 {
// requires BLOCK_HAS_SIGNATURE
const char *signature;
const char *layout; // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};
// 底层
struct _MyBlock {
void *isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
// 函数指针
MyBlockInvokeFunction invoke;
struct _MyBlockDescriptor1 *descriptor;
};
static struct _MyBlockDescriptor3 * _My_Block_descriptor_3(struct _MyBlock *aBlock) {
if (! (aBlock->flags & BLOCK_HAS_SIGNATURE)) return nil;
uint8_t *desc = (uint8_t *)aBlock->descriptor;
desc += sizeof(struct _MyBlockDescriptor1);
if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE) {
desc += sizeof(struct _MyBlockDescriptor2);
}
return (struct _MyBlockDescriptor3 *)desc;
}
static const char *MyBlockTypeEncodeString(id blockObj) {
struct _MyBlock *block = (__bridge void *)blockObj;
return _My_Block_descriptor_3(block)->signature;
}
@interface NSObject (Block)
// 打印Block 签名
- (NSMethodSignature *)getBlcokSignature;
// 打印Block 签名
- (NSString *)getBlcokSignatureString;
// 调用block
- (void)invokeBlock;
@end
NS_ASSUME_NONNULL_END
// NSObject+Block.m
#import "NSObject+Block.h"
@implementation NSObject (Block)
- (NSString *)getBlcokSignatureString {
NSMethodSignature *signature = self.getBlcokSignature;
if (signature) {
NSMutableString *blockSignature = [NSMutableString stringWithFormat:@"BlcokSignature: return type: %s, ", [signature methodReturnType]];
for (int i = 0; i < signature.numberOfArguments; i++) {
[blockSignature appendFormat:@"argument number: %d, argument type: %s ", i+1, [signature getArgumentTypeAtIndex:i]];
}
return blockSignature;
}
return nil;
}
- (NSMethodSignature *)getBlcokSignature {
if ([self isKindOfClass:NSClassFromString(@"__NSMallocBlock__")] || [self isKindOfClass:NSClassFromString(@"__NSStackBlock__")] || [self isKindOfClass:NSClassFromString(@"__NSGlobalBlock__")]) {
NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:MyBlockTypeEncodeString(self)];
return signature;
}
return nil;
}
- (void)invokeBlock {
NSMethodSignature *signature = self.getBlcokSignature;
if (signature) {
// 动态的消息转发
NSInvocation *blockInvocation = [NSInvocation invocationWithMethodSignature:signature];
[blockInvocation invokeWithTarget:self];
}
}
// block OC 对象
- (NSString *)description {
if ([self isKindOfClass:NSClassFromString(@"__NSMallocBlock__")] || [self isKindOfClass:NSClassFromString(@"__NSStackBlock__")] || [self isKindOfClass:NSClassFromString(@"__NSGlobalBlock__")]) {
//签名
return [NSString stringWithFormat:@"<%@:%p>--%@", self.class, self, [self getBlcokSignatureString]];
}
return [NSString stringWithFormat:@"<%@:%p>", self.class, self];
}
@end
于是我就可以对系统的Block进行强转成_MyBlock
类型去操作内存:
- 第2.1道面试题:
// ViewController.m
#import "ViewController.h"
#import "NSObject+Block.h"
@implementation ViewController
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-unsafe-retained-assign"
- (void)viewDidLoad {
[super viewDidLoad];
[self blockDemo1];
}
- (void)blockDemo1{
int a = 1;
// 栈block
void(^ __weak weakBlock)(void) = ^{
NSLog(@"-----%d", a);
};
// 强转成自定义的_MyBlock结构体类型
struct _MyBlock *blc = (__bridge struct _MyBlock *)weakBlock;
void(^strongBlock)(void) = weakBlock;
blc->invoke = nil;
strongBlock();
}
@end
此时strongBlock
和weakBlock
指向的是同一块栈block内存;
将block
的函数实现置为nil
,会导致崩溃。
怎么修改不会导致崩溃?
void(^strongBlock)(void) = [weakBlock copy];
将栈block拷贝到堆,此时strongBlock
是指向堆内存,weakBlock
是指向栈内存。blc->invoke = nil;
是修改栈block的内容,并不影响strongBlock
的调用。
- 第2.2道面试题:
#import "ViewController.h"
#import "NSObject+Block.h"
@interface ViewController ()
@end
@implementation ViewController
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-unsafe-retained-assign"
- (void)viewDidLoad {
[super viewDidLoad];
[self blockDemo2];
}
- (void)blockDemo2 {
int a = 10;
void(^__weak block1)(void) = nil;
// {}里的代码块会立马执行
{
// 栈block2
void(^__weak block2)(void) = ^{
NSLog(@"%d",a);
};
block1 = block2;
int b = 5;
NSLog(@"%p", &c);
}
// 此时出了{...}的作用域b会被释放吗?
block1(); // 10
}
@end
此时的block1();
会打印10。
此时出了{...}
的作用域b会被释放吗? 不会。
因为{}的一整块代码需要出了blockDemo2
方法作用域才会被释放。
- 第2.3道面试题:
此时我把blockDemo2
再修改一下
- (void)blockDemo3{
int a = 10;
void(^__weak block1)(void) = nil;
{
// 栈block
void(^__weak block2)(void) = ^{
NSLog(@"%d",a);
};
block1 = [block2 copy];
}
block1();
}
此时block1();
会崩溃!为什么?
因为[block2 copy];
是返回的结果是堆内存的block,同时block1
去弱引用这块堆内存并不会发生引用计数+1的情况(堆的内存管理是通过引用计数的),所以block1
在出{...}
作用域的时候,ARC
会自动地[block1 release]
,导致block1
被释放。
要解决上面的问题,需要一个强引用的方式去引用[block2 copy];
void(^block1)(void) = nil;
-
第2.4道面试题:
此时我再把blockDemo2
再修改一下
- (void)blockDemo4 {
NSObject *objc = [NSObject new];
void(^__weak block1)(void) = nil;
{
void(^__weak block2)(void) = ^{
NSLog(@"%@", objc);
};
block1 = block2;
block1();
}
block1();
}
{...}里的block1()
会打印objc的地址
,block1
会打印null
<NSObject:0x600000060590>
(null)
因为在{...}
里栈block2捕获的objc
是在堆区,并且objc
的引用计数不会改变,在出{...}
后objc
被ARC
自动释放[objc release]
。
将blockDemo4
方法进行如下修改
- (void)blockDemo4{
NSObject *objc = [NSObject new];
void(^block1)(void) = nil;
{
void(^__weak block2)(void) = ^{
NSLog(@"%@", objc);
};
block1 = [block2 copy];
block1();
}
block1();
}
Block与内存布局相关面试题
面试题:Block中可以修改全局变量
,全局静态变量
,局部静态变量
,局部变量
吗?
可以修改全局变量,全局静态变量。
因为全局变量 和 静态全局变量是全局的,作用域很广。可以修改局部静态变量,
不可以修改局部斌量
。
1.局部静态变量
(static修饰的) 和局部变量
,被block从外面捕获,成为__main_block_impl_0
这个结构体的成员变量;
2.局部静态变量是以指针形式
被block捕获的,由于捕获的是指针,所以可以修改局部静态变量的值;
3.局部变量是以值方式
传递到block的构造函数,只会捕获block中会用到的变量,由于只捕获了变量的值,并非内存地址,所以在block内部不能改变局部变量的值。ARC
环境下,一旦使用__block
修饰并在block中修改,就会触发copy
,block就会从栈区copy到堆区
,此时的block是堆区block。ARC
模式下
1.block中引用id类型
的数据,无论有没有__block
修饰,都会retain。
2.block中引用基础数据类型
的数据,没有__block
就无法修改变量值;如果有__block修饰,也是在底层修改__Block_byref_a_0结构体,将其内部的forwarding指针指向copy后的地址,来达到值的修改。