【iOS】 Block再学习
iOS Block再学习
文章目录
- iOS Block再学习
- 前言
- Block的三种类型
- __ NSGlobalBlock__
- __ NSMallocBlock__
- __ NSStackBlock__
- 小结
- Block底层分析
- Block的结构
- 捕获自由变量
- 捕获全局(静态)变量
- 捕获静态变量
- __block修饰符
- forwarding指针
- Block的copy时机
- block作为函数返回值
- 将block赋给__strong指针
- block作为函数中方法名含有usingBlock的方法参数时
- block作为GCD API的方法参数时候
- Block的三层拷贝
- 外界变量的类型
- Block循环引用
- 解决循环引用
- weak-strong-dance
- __block修饰符
- 对象self作为参数
- 小结
前言
笔者之前学习过block的相关内容,但是掌握不牢,今天再重新学习一遍
Block的三种类型
__ NSGlobalBlock__
void(^block)(void) = ^ {NSLog(@"testBlock");};NSLog(@"%@", block);
此时block没有参数也没有返回值,属于全局block
如果访问全局变量:
从上面可以看出无论是否创建block变量,只要访问全局变量的话,他就会创建一个全局区的block
__ NSMallocBlock__
int loaclA = 10;void(^block)(void) = ^ {NSLog(@"testBlock %ld", loaclA);};NSLog(@"%@", block);
此时block捕获了一个临时变量,就是底层拷贝a,所以是堆区block
__ NSStackBlock__
NSLog(@"%@", [^{NSLog(@"%d", loaclA);} class]);
这里在自由变量没有处理之前是栈区block
处理之后是堆区block
,目前的栈区block就越来越少了
这里是不创建Block变量,且访问自由变量的时候才会出现这里的一个栈区Block
小结
- block如果不访问自由变量的话,都是存储在全局区的,如果访问全局变量的话,也是存储在全局区的Block
- block如果访问自由变量的话
- 如果没有创建block变量,才会创建一个栈区Block变量
- 创建了一个Block变量,且访问自由变量,才会创建出一个堆区的Block,这里创建出堆区Block的原因是 栈区的Block执行了拷贝操作
Block底层分析
Block的结构
先看这段代码:
int main(int argc, const char * argv[]) {@autoreleasepool {extern NSString* nanString;int localA = 3;void(^block)(void) = ^{NSLog(@"123%ld", localA);};}return 0;
}
这里是一段司空见惯的Block捕获自由变量的代码.这里我们来分析一下这里的源码来认识一下底层内容:
struct __main_block_impl_0 {struct __block_impl impl;struct __main_block_desc_0* Desc;int localA; // 捕获的自由变量__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _localA, int flags=0) : localA(_localA) {impl.isa = &_NSConcreteStackBlock; // 设置isa指针//&_NSConcreteStackBlock:标识 Block 初始类型为 栈 Block(未进行 copy 操作时)impl.Flags = flags;impl.FuncPtr = fp; // 代码块的函数赋值Desc = desc;} // block的一个构造函数,创建Block调用的内容
};static void __main_block_func_0(struct __main_block_impl_0 *__cself) {int localA = __cself->localA; // bound by copy 这里是一个值拷贝NSLog((NSString *)&__NSConstantStringImpl__var_folders_rb_0ts7dvhs3zg9fc6kk2nvtlr00000gn_T_main_3d8947_mi_0, localA);}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)};
int main(int argc, const char * argv[]) {/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; extern NSString* nanString;int localA = 3;void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, localA)); //相当于block等于__main_block_impl_0,是一个函数}return 0;
}
这里我们先看block的一个结构体,这里可以说明block是一个 __main_block_impl_0
类型的对象.从构造函数可以看出他又一个isa指针
总结
- block其实是一个对象,结构体,函数,又因为block没有名称,所以也被叫做,匿名函数
如果从更加底层的角度来看:
捕获自由变量
可以看到Block的实现结构体里面新增了localA
变量,它在上面这是一个单纯的一个值拷贝
这里我看一下这段核心代码:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {int localA = __cself->localA; // bound by copy 这里是一个值拷贝NSLog((NSString *)&__NSConstantStringImpl__var_folders_rb_0ts7dvhs3zg9fc6kk2nvtlr00000gn_T_main_3d8947_mi_0, localA);}
这里我们可以看到这里如果对于他的值其实是只读的,它并不会修改原来block中持有的变量的值,所以是有问题的
block捕获外界变量时,在内部会自动生成同一个属性来保存
捕获全局(静态)变量
这里我们看一下如果是一个全局变量会是什么形式?
int localA = 10;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;}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {NSLog((NSString *)&__NSConstantStringImpl__var_folders_rb_0ts7dvhs3zg9fc6kk2nvtlr00000gn_T_main_4d4ace_mi_0, localA++);}
这里我们可以看到它对于全局变量就是直接引用了全局变量的内容
捕获静态变量
这里我们看一下如果是一个静态局部变量:
int main(int argc, const char * argv[]) {@autoreleasepool {extern NSString* nanString;static int loaclB = 10;void(^block)(void) = ^{loaclB++;NSLog(@"123%ld", localA++);};}return 0;
}
如果是一个静态局部变量的话他是通过指针来获取的,同时静态局部变量与Block建立关联的是指针(int *)
,也就是说Block捕获的静态局部变量捕获的是变量的指针,因此当我们对静态局部变量进行修改时,Block内部的静态局部变量的值也会随之改变.
struct __main_block_impl_0 {struct __block_impl impl;struct __main_block_desc_0* Desc;int *loaclB;__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_loaclB, int flags=0) : loaclB(_loaclB) {impl.isa = &_NSConcreteStackBlock;impl.Flags = flags;impl.FuncPtr = fp;Desc = desc;}
};
__block修饰符
如果给localA添加一个修饰符__block
,然后在block
中对localA
进行一个加加操作
int main(int argc, const char * argv[]) {@autoreleasepool {extern NSString* nanString;__block int localA = 3;void(^block)(void) = ^{NSLog(@"123%ld", localA++);};}return 0;
}
这时候我们在把它重写成我们的结构体:
struct __main_block_impl_0 {struct __block_impl impl;struct __main_block_desc_0* Desc;__Block_byref_localA_0 *localA; // by ref__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_localA_0 *_localA, int flags=0) : localA(_localA->__forwarding) {impl.isa = &_NSConcreteStackBlock;impl.Flags = flags;impl.FuncPtr = fp;Desc = desc;}
};static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->localA, (void*)src->localA, 8/*BLOCK_FIELD_IS_BYREF*/);}static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->localA, 8/*BLOCK_FIELD_IS_BYREF*/);}//重新看一下函数部分
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {__Block_byref_localA_0 *localA = __cself->localA; // bound by refNSLog((NSString *)&__NSConstantStringImpl__var_folders_rb_0ts7dvhs3zg9fc6kk2nvtlr00000gn_T_main_fd8242_mi_0, (localA->__forwarding->localA)++);}
int main(int argc, const char * argv[]) {/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; extern NSString* nanString;//创建一个__Block_byref_a_0 是一个结构体,相当于把外界对象村对象__attribute__((__blocks__(byref))) __Block_byref_localA_0 localA = {(void*)0,(__Block_byref_localA_0 *)&localA, 0, sizeof(__Block_byref_localA_0), 3};void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_localA_0 *)&localA, 570425344));}return 0;
}
这里我们发现原先的localA
变成了__Block_byref_localA_0
的部分
struct __Block_byref_localA_0 { //__block修饰的外界变量的结构体void *__isa;
__Block_byref_localA_0 *__forwarding;int __flags;int __size;int localA;
};
总结:
外界变量
如果被__block修饰会变成这个结构体- 结构体用来保存变量的指针和值
- 将变量生成的结构体对象中的指针地址 传递给block, 然后在block内部就可以对外界变量进行操作了
两种拷贝对比如下
值拷贝
- 深拷贝,只是拷贝数值,且拷贝的值不可更改,指向不同的内存空间,案例中普通变量loacaA
就是值拷贝
指针拷贝
- 浅拷贝,生成的对象指向同一片内存空间,案例中经过__block
修饰的变量localA
就是指针拷贝
forwarding指针
这里我们首先看下面这段代码:
__block int val = 0;void (^block)(void) = ^{val++;};++val;block();NSLog(@"%ld", val);
这里我们看一下这里的一个val在堆区上面,一个在栈区上面,如果我们不做处理的话对于他的数据计算会有问题:
所以这就是forwarding
这个指针的使命所在–确保可以正确的访问__block变量
我们把它转译成c++源码来看一下:
int main(int argc, const char * argv[]) {/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; extern NSString* nanString;__attribute__((__blocks__(byref))) __Block_byref_loaclB_0 loaclB = {(void*)0,(__Block_byref_loaclB_0 *)&loaclB, 0, sizeof(__Block_byref_loaclB_0), 10};void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_loaclB_0 *)&loaclB, 570425344));(loaclB.__forwarding->loaclB)++;}return 0;
}
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {__Block_byref_loaclB_0 *loaclB = __cself->loaclB; // bound by ref(loaclB->__forwarding->loaclB)++;NSLog((NSString *)&__NSConstantStringImpl__var_folders_rb_0ts7dvhs3zg9fc6kk2nvtlr00000gn_T_main_589979_mi_0, localA++);}
从上面我们可以看出对于这种自由变量的加减操作,都是通过我们的(loaclB->__forwarding->loaclB)++
这一句指令来执行的,无论是堆区的block
还是栈区的block
都是这样操作的,这时候我们看一下Block copy的相关源码:
这部分源码内容会在下面的Block的拷贝部分讲一下,这里先不提起,先让笔者了解一下有关于__Block变量拷贝的图
当block变量从stack copy到heap上面的时候,stack上的forwarding被修改程指向heap上的block变量,通过这个机制,保证我们无论是在stack和heap上面都可以访问到同一个block变量
Block的copy时机
在 Objective-C 中,Block 最初是在栈上创建的。栈上的 Block(NSStackBlock)生命周期与其定义的作用域相关联,一旦该作用域结束,栈上的 Block 将不再有效。这意味着如果你需要在 Block 的定义作用域外使用它,比如将它作为回调传递或保存为后续使用,你需要将它复制到堆上(成为 NSMallocBlock)。
就好比我们在子线程回调到主线程的时候,我们的Block如果在栈上的话,那么超出作用域就会被销毁,无法回到主线程被调用,因此需要拷贝到堆上
在ARC中进行了很多优化的内容,Block的copy操作会在下面这些情况下执行:
block作为函数返回值
- (nxBlock)test {int a = 0;return ^{NSLog(@"1233123 %d", a);};
}
nxBlock testBlock = [self test];
NSLog(@"%@", [testBlock class]);
将block赋给__strong指针
int loaclA = 10;void(^block)(void) = ^ {NSLog(@"testBlock %ld", loaclA);};NSLog(@"%@", block);
block作为函数中方法名含有usingBlock的方法参数时
NSArray *array = @[@1,@4,@5];
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {}];
block作为GCD API的方法参数时候
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{});
Block的三层拷贝
这里有一个Block的三层copy,我们先分析学习有关于
static void *_Block_copy_internal(const void *arg, const int flags)
{struct Block_layout *aBlock;const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE;if (!arg) return NULL;aBlock = (struct Block_layout *)arg; // 强制转化成Block_layout对象,防止对外界造成影响if (aBlock->flags & BLOCK_NEEDS_FREE) // NSConcreteMallocBlock//是否需要释放{latching_incr_int(&aBlock->flags);return aBlock;}else if (aBlock->flags & BLOCK_IS_GLOBAL) // NSConcreteGlobalBlock//如果是全局block,直接返回{return aBlock;}// 为栈block或者是堆区block,由于堆区需要申请内存,所以是栈区的block的操作// Its a stack block. Make a copy.struct Block_layout *result = malloc(aBlock->descriptor->size);if (!result) return (void *)0;//通过memmove内存拷贝,将aBlock拷贝到resultmemmove(result, aBlock, aBlock->descriptor->size); // bitcopy first// reset refcountresult->flags &= ~(BLOCK_REFCOUNT_MASK); // XXX not neededresult->flags |= BLOCK_NEEDS_FREE | 1;result->isa = _NSConcreteMallocBlock;if (result->flags & BLOCK_HAS_COPY_DISPOSE) {(*aBlock->descriptor->copy)(result, aBlock); // do fixup}return result;
}
下面简单讲述一下这个步骤:
- 如果需要释放,如果需要就直接释放
- 如果是
globalBlock
不需要copy,直接返回 - 否则就只有两种情况,栈区block和堆区block,但是由于需要拷贝,所以堆区Block需要申请内存,所以最后是有关于栈区Block的拷贝代码
- 通过malloc申请内存来接受block
- 通过
memmove
将block拷贝到新申请的内存中 - 设置block对象的类型为堆区block,就是
result->isa = _NSConcreteMallocBlock;
外界变量的类型
// Runtime support functions used by compiler when generating copy/dispose helpers// Values for _Block_object_assign() and _Block_object_dispose() parameters
enum {// see function implementation for a more complete description of these fields and combinations//普通对象,即没有其他的引用类型,也就是我们任何的一个Object都是这个逻辑BLOCK_FIELD_IS_OBJECT = 3, // id, NSObject, __attribute__((NSObject)), block, ...//block类型作为变量BLOCK_FIELD_IS_BLOCK = 7, // a block variable//经过__block修饰的变量和对象BLOCK_FIELD_IS_BYREF = 8, // the on stack structure holding the __block variable//weak 弱引用变量BLOCK_FIELD_IS_WEAK = 16, // declared __weak, only used in byref copy helpers//返回的调用对象 - 处理block_byref内部对象内存会加的一个额外标记,配合flags一起使用BLOCK_BYREF_CALLER = 128, // called from __block (byref) copy/dispose support routines.
};
这里我们在看一下这里的_Block_object_assign
的源码,以及和下面这段Block拷贝的代码进行对比:
int main(int argc, const char * argv[]) {@autoreleasepool {extern NSString* nanString;int loaclB = 10;__block NSObject* obj = [[NSObject alloc] init];NSMutableArray* ary = [NSMutableArray array];void(^block)(void) = ^{[ary addObject:@"123"];NSLog(@"123%ld %@", loaclB, obj);};block();[ary addObject:@"23333"];NSLog(@"%@", ary);loaclB++;}return 0;
}//反编译之后变成下面这样:
//block的拷贝函数
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->ary, (void*)src->ary, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_assign((void*)&dst->obj, (void*)src->obj, 8/*BLOCK_FIELD_IS_BYREF*/);} //这里调用了这个_Block_object_assign方法//block里面的匿名函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {__Block_byref_obj_0 *obj = __cself->obj; // bound by refNSMutableArray *ary = __cself->ary; // bound by copyint loaclB = __cself->loaclB; // bound by copy((void (*)(id, SEL, ObjectType _Nonnull))(void *)objc_msgSend)((id)ary, sel_registerName("addObject:"), (id _Nonnull)(NSString *)&__NSConstantStringImpl__var_folders_rb_0ts7dvhs3zg9fc6kk2nvtlr00000gn_T_main_d58752_mi_0);NSLog((NSString *)&__NSConstantStringImpl__var_folders_rb_0ts7dvhs3zg9fc6kk2nvtlr00000gn_T_main_d58752_mi_1, loaclB, (obj->__forwarding->obj));}
//block的结构
struct __main_block_impl_0 {struct __block_impl impl;struct __main_block_desc_0* Desc;NSMutableArray *ary;int loaclB;__Block_byref_obj_0 *obj; // by ref__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSMutableArray *_ary, int _loaclB, __Block_byref_obj_0 *_obj, int flags=0) : ary(_ary), loaclB(_loaclB), obj(_obj->__forwarding) {impl.isa = &_NSConcreteStackBlock;impl.Flags = flags;impl.FuncPtr = fp;Desc = desc;}
};
这里我们看到了这里有两种类型:
BLOCK_FIELD_IS_OBJECT
和BLOCK_FIELD_IS_BYREF
这里在这三层拷贝的最后一层进行一个讲解
这里如果有__block
修饰的变量就会先调用一次这个方法:
static struct Block_byref *_Block_byref_copy(const void *arg) {//强转为Block_byref结构体类型,保存一份struct Block_byref *src = (struct Block_byref *)arg;if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {// src points to stack 申请内存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 stackcopy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;//block内部持有的Block_byref 和 外界的Block_byref 所持有的对象是同一个,这也是为什么__block修饰的变量具有修改能力//copy 和 scr 的地址指针达到了完美的同一份拷贝,目前只有持有能力copy->forwarding = copy; // patch heap copy to point to itself // 把堆区的指针指向自己src->forwarding = copy; // patch stack to point to heap copy // 把栈区的指针指向堆区,保证数值一致copy->size = src->size;//如果有copy能力if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) { // 有自己的一个copy的逻辑// Trust copy helper to copy everything of interest// If more than one field shows up in a byref block this is wrong XXX//Block_byref_2是结构体,__block修饰的可能是对象,对象通过byref_keep保存,在合适的时机进行调用struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);copy2->byref_keep = src2->byref_keep; // 调用自定义复制逻辑copy2->byref_destroy = src2->byref_destroy;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);copy3->layout = src3->layout;}//等价于 __Block_byref_id_object_copy(*src2->byref_keep)(copy, src);}else {// Bitwise copy.// This copy includes Block_byref_3, if any.memmove(copy+1, src+1, src->size - sizeof(*src)); //如果为简单类型就直接进行一个内存拷贝}}// already copied to heapelse if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {latching_incr_int(&src->forwarding->flags);}return src->forwarding;
}
这个函数完成了copy之后会调用我们的最后一个函数_Block_object_assign
这个函数由编译器来处理
第三层拷贝是block对传入对象的变量进行_Block_object_assign,将block内部将要使用的对象的变量拷贝到block内部。
上面我们自己给出的那段代码中有BLOCK_FIELD_IS_OBJECT
和BLOCK_FIELD_IS_BYREF
这两种都会进入我们的最后的_Block_object_assign
然后进行不同的处理
- 如果是普通对象,则交给系统ARC处理,并拷贝对象指针,就是引用计数甲一,所以外界变量不能释放
- 如果是
block类型
的变量,则通过_Block_copy
操作,将block拷贝到堆区 - 如果是
__block修饰
的变量,就通过_Block_byref_copy
函数进行内存拷贝以及常规处理
三层拷贝总结:
第一层通过_Block_copy
实现对象的自身拷贝,从栈区拷贝到堆区
第二层通过调用_Block_byref_copy
这个来实现对于对象拷贝成Block_byref
类型
第三次调用_Block_object_assign
对于__block修饰
的当前变量内部对象的内存管理
当且仅当用__block变量的时候才会有三次拷贝.
Block循环引用
先认识一下什么是循环引用:
- 正常使用: 是指A持有B的引用,当A调用dealloc方法,给B发送release信号,B收到release信号,如果此时B的retainCount为0的时候,则调用B的dealloc方法
- 循环引用:A, B互相持有,所以导致A无法调用dealloc方法给Breleasse信号,所以B也无法接受到release信号,所以A,B此时都无法释放
解决循环引用
self.name = @"123";
self.testBlock = ^(void){NSLog(@"%@", self.name);
};
UIView animateWithDuration:1 animations:^{NSLog(@"%@",self.name);
};
代码第一种发生了一个循环引用的问题,因为在block
中持有了外部变量name
,导致block也持有了self,而self本来是持有block的,所以导致了self和block的互相持有
代码二中没有循环引用,因为self中没有持有animation的block,不构成互相持有
weak-strong-dance
如果block内部并未嵌套block,直接使用__weak
修饰self即可
self.name = @"123";__weak typeof (self) weakSelf = self;self.testBlock = ^(void){NSLog(@"%@", weakSelf.name);};
此时的weakSelf
和self
指向同一片内存空间,且使用__weak不会导致self的引用计数发生变化
__weak typeof (self) weakSelf = self;self.testBlock = ^(void){NSLog(@"%@", weakSelf.name);};
如果block里面嵌套了block,那么就要同时使用__weak
和__strong
self.testBlock = ^(void){__strong typeof(weakSelf) strongSelf = weakSelf;dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{NSLog(@"%@", weakSelf.name);});};
如果不采用强弱共舞的话,可能会出现一个过了一会self被释放导致我们获取不到对应的数据,所以要采用__strong
修饰一下,
其中strongSelf
是一个临时变量,在cjlBlock的作用域内,即内部block执行完
就释放strongSelf
,所以并不会出现block
持有self
的一个情况导致这些问题
__block修饰符
我们可以采用__block修饰符,在主动调用完后手动释放self
__block PersonViewController* vc = self;self.testBlock = ^(void){NSLog(@"%@", vc.name);vc = nil;};
self.testBlock();
需要注意的是这里的block必须调用
如果不调用block的话,vc就不会主动置为nil,那么仍旧是循环引用,self和block都不会被释放
对象self作为参数
主要是把对象self作为参数,提供给block内部使用,不会有引用计数问题
self.testBlock = ^(PersonViewController* vc){NSLog(@"%@", vc.name);};
小结
笔者这里简单总结了有关于Block的一个知识,复习了有关于Block捕获变量,以及使用__block
修饰的时候forwarding
指针的有一个变化,还有一个它发生拷贝的一个时机,以及复习了几种有关于解决Block循环引用的内容.