美文网首页
—— block详解

—— block详解

作者: 大成小栈 | 来源:发表于2021-07-13 20:16 被阅读0次

    新建工程,并实现以下代码:

    #import <Foundation/Foundation.h>
    
    void testFunc() {
        int a =10;
        __block int b = 20;
        
        void (^testBlock)(int) = ^(int c){
             int  d = a + b + c;
             NSLog(@"d=%d", d);
         };
     
        //修改值不会影响testBlock内的计算结果
        a = 20;
        //修改值会影响testBlock内的计算结果。
        b = 40;
        
        testBlock(30);
    }
     
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            testFunc();
        }
        return 0;
    }
    

    在工程目录启动命令行输入以下命令:
    $ clang -rewrite-objc main.m

    将生成main.cpp文件拉至最下,可见如下代码:

    struct __Block_byref_b_0 {
      void *__isa;
    __Block_byref_b_0 *__forwarding;
     int __flags;
     int __size;
     int b;
    };
    
    struct __testFunc_block_impl_0 {
      struct __block_impl impl;
      struct __testFunc_block_desc_0* Desc;
      int a;
      __Block_byref_b_0 *b; // by ref
      __testFunc_block_impl_0(void *fp, struct __testFunc_block_desc_0 *desc, int _a, __Block_byref_b_0 *_b, int flags=0) : a(_a), b(_b->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    static void __testFunc_block_func_0(struct __testFunc_block_impl_0 *__cself, int c) {
      __Block_byref_b_0 *b = __cself->b; // bound by ref
      int a = __cself->a; // bound by copy
    
             int d = a + (b->__forwarding->b) + c;
             NSLog((NSString *)&__NSConstantStringImpl__var_folders_1y_tktqm9p53zz3khyhzvqxtb9m0000gn_T_main_2edacd_mi_0, d);
         }
    static void __testFunc_block_copy_0(struct __testFunc_block_impl_0*dst, struct __testFunc_block_impl_0*src) {_Block_object_assign((void*)&dst->b, (void*)src->b, 8/*BLOCK_FIELD_IS_BYREF*/);}
    
    static void __testFunc_block_dispose_0(struct __testFunc_block_impl_0*src) {_Block_object_dispose((void*)src->b, 8/*BLOCK_FIELD_IS_BYREF*/);}
    
    static struct __testFunc_block_desc_0 {
      size_t reserved;
      size_t Block_size;
      void (*copy)(struct __testFunc_block_impl_0*, struct __testFunc_block_impl_0*);
      void (*dispose)(struct __testFunc_block_impl_0*);
    } __testFunc_block_desc_0_DATA = { 0, sizeof(struct __testFunc_block_impl_0), __testFunc_block_copy_0, __testFunc_block_dispose_0};
    void testFunc() {
    
        int a =10;
        __attribute__((__blocks__(byref))) __Block_byref_b_0 b = {(void*)0,(__Block_byref_b_0 *)&b, 0, sizeof(__Block_byref_b_0), 20};
    
        void (*testBlock)(int) = ((void (*)(int))&__testFunc_block_impl_0((void *)__testFunc_block_func_0, &__testFunc_block_desc_0_DATA, a, (__Block_byref_b_0 *)&b, 570425344));
    
    
        a = 20;
    
        (b.__forwarding->b) = 40;
    
        ((void (*)(__block_impl *, int))((__block_impl *)testBlock)->FuncPtr)((__block_impl *)testBlock, 30);
    }
    
    
    
    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
            testFunc();
        }
        return 0;
    }
    static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
    

    1. block 的底层结构

    1.1 __testFunc_block_impl_0
    struct __testFunc_block_impl_0 {
      struct __block_impl impl;
      struct __testFunc_block_desc_0* Desc;
      int a;
      __Block_byref_b_0 *b; // by ref
      __testFunc_block_impl_0(void *fp, struct __testFunc_block_desc_0 *desc, int _a, __Block_byref_b_0 *_b, int flags=0) : a(_a), b(_b->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    __testFunc_block_impl_0是该block以C++实现的一个结构体,且从命名可以看出其为testFunc方法中的第0个block,通常包含两个成员变量__block_impl impl,__testBlock_block_desc_0* Desc和一个构造函数。

    • __block_impl 结构体
    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    
    1. isa指向一个类对象(三种类型:_NSConcreteStackBlock、_NSConcreteGlobalBlock、_NSConcreteMallocBlock),本例中为_NSConcreteStackBlock;
    2. Flags:block的负载信息(引用计数和类型信息),按位存储;
    3. Reserved:保留变量;
    4. FuncPtr指针指向Block执行时调用的函数(Block需要执行的代码块),本例中为__blockTest_block_func_0函数。
    • __blockTest_block_desc_0 结构体
    static struct __blockTest_block_desc_0 {
      size_t reserved;
      size_t Block_size;
    } __blockTest_block_desc_0_DATA = { 0, sizeof(struct __blockTest_block_impl_0)};
    

    其中,__blockTest_block_desc_0_DATA是一个__blockTest_block_desc_0的一个实例。
    reserved:Block版本升级所需的预留区空间,在这里为0。
    Block_size:Block大小(sizeof(struct __blockTest_block_impl_0))。

    1.2 __testFunc_block_func_0
    static void __testFunc_block_func_0(struct __testFunc_block_impl_0 *__cself, int c) {
      __Block_byref_b_0 *b = __cself->b; // bound by ref
      int a = __cself->a; // bound by copy
    
      int d = a + (b->__forwarding->b) + c;
      NSLog((NSString *)&__NSConstantStringImpl__var_folders_1y_tktqm9p53zz3khyhzvqxtb9m0000gn_T_main_2edacd_mi_0, d);
    }
    

    __testFunc_block_func_0就是block在执行时调用的函数,参数是一个__testFunc_block_impl_0类型的指针。

    1.3 testFunc()
    void testFunc() {
        int a =10;
        __attribute__((__blocks__(byref))) __Block_byref_b_0 b = {(void*)0,(__Block_byref_b_0 *)&b, 0, sizeof(__Block_byref_b_0), 20};
        void (*testBlock)(int) = ((void (*)(int))&__testFunc_block_impl_0((void *)__testFunc_block_func_0, &__testFunc_block_desc_0_DATA, a, (__Block_byref_b_0 *)&b, 570425344));
    
        a = 20;
        (b.__forwarding->b) = 40;
        ((void (*)(__block_impl *, int))((__block_impl *)testBlock)->FuncPtr)((__block_impl *)testBlock, 30);
    }
    
    • 定义Block
    void (*testBlock)(int) = ((void (*)(int))&__testFunc_block_impl_0((void *)__testFunc_block_func_0, &__testFunc_block_desc_0_DATA, a, (__Block_byref_b_0 *)&b, 570425344));
    

    testBlock实际上是一个指针,指向一个通过构造函数实例化的__testFunc_block_impl_0结构体实例。初始化时需要两个参数:
    __testFunc_block_func_0:block块的函数指针;
    __testFunc_block_desc_0_DATA:作为静态全局变量初始化__main_block_desc_0的结构体实例指针。

    • 调用Block
    ((void (*)(__block_impl *, int))((__block_impl *)testBlock)->FuncPtr)((__block_impl *)testBlock, 30);
    

    上面方法调用通过testBlock->FuncPtr指针找到__blockTest_block_func_0函数并且转成(void (*)(__block_impl *, int))类型,然后将((__block_impl *) testBlock)作为参数传给这个函数进行调用。

    2. 变量的截获(__block)

    文章开始处的testFunc()中定义了两个变量:

    int a =10;
    __block int b = 20;
    

    经过clang命令处理后的.cpp文件的__testFunc_block_func_0中,又看到以下代码:

    __Block_byref_b_0 *b = __cself->b; // bound by ref
    int a = __cself->a; // bound by copy
    

    在clang后的testFunc()中,__block int b = 20;变成了以下代码:

    __attribute__((__blocks__(byref))) __Block_byref_b_0 b = {(void*)0,(__Block_byref_b_0 *)&b, 0, sizeof(__Block_byref_b_0), 20};
    

    其中,还多了针对 b 的结构体 __Block_byref_b_0 的定义:

    struct __Block_byref_b_0 {
      void *__isa;
    __Block_byref_b_0 *__forwarding;
     int __flags;
     int __size;
     int b;
    };
    

    可见,日常使用block想改变外部变量的值需要加__block修饰,原因就在于:
    block截获普通变量时,是值传递;
    截获__block修饰的变量时,是地址传递;

    另外,还可以试图定义静态局部变量、全局变量等,来观察clang后代码的变化。
    静态局部变量也变成指针;全局变量仍然为全局直接作参传递。

    3. block循环引用

    @interface Person : NSObject
    
    @property (nonatomic, strong) NSString *name;
    @property (nonatomic, copy) void (^block)(void);
    
    - (void)testReferenceSelf;
    
    @end
    
    @implementation Person
    
    - (void)testReferenceSelf {
        self.block = ^ {
            NSLog(@"self.name = %s", self.name.UTF8String);
        };
        self.block();
    }
    
    - (void)dealloc {
        NSLog(@"-------dealloc-------");
    }
    
    @end
    
    
    int main(int argc, char * argv[]) {
        Person *person = [[Person alloc] init];
        person.name = @"roy";
        [person testReferenceSelf];
    }
    

    打印结果是self.name = roy,Person的析构方法dealloc并没有执行,这是典型的循环引用,下面我们研究研究为啥会循环引用。clang改写后的代码如下:

    struct __Person__testReferenceSelf_block_impl_0 {
      struct __block_impl impl;
      struct __Person__testReferenceSelf_block_desc_0* Desc;
      Person *const __strong self;
      __Person__testReferenceSelf_block_impl_0(void *fp, struct __Person__testReferenceSelf_block_desc_0 *desc, Person *const __strong _self, int flags=0) : self(_self) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    static void _I_Person_testReferenceSelf(Person * self, SEL _cmd) {
        ((void (*)(id, SEL, void (*)()))(void *)objc_msgSend)((id)self, sel_registerName("setBlock:"), ((void (*)())&__Person__testReferenceSelf_block_impl_0((void *)__Person__testReferenceSelf_block_func_0, &__Person__testReferenceSelf_block_desc_0_DATA, self, 570425344)));
        ((void (*(*)(id, SEL))())(void *)objc_msgSend)((id)self, sel_registerName("block"))();
    }
    

    我们看到本来Person中testReferenceSelf方法是没有参数的,但是转成C++之后多出来两个参数:* self和_cmd,再看看__Person__testReferenceSelf_block_impl_0中多出来一个成员变量Person *const __strong self;,因此我们知道Person中block捕获了self,并且block强引用self,同时self也强引用block,因此形成循环引用。

    • __weak 解除循环引用
    @implementation Person
    
    - (void)testReferenceSelf {
        __weak typeof(self) weakself = self;
        self.block = ^ {
            __strong typeof(self) strongself = weakself;
            NSLog(@"self.name = %s", strongself.name.UTF8String);
        };
        self.block();
    }
    
    - (void)dealloc {
        NSLog(@"-------dealloc-------");
    }
    
    @end
    
    打印结果:
    //2019-05-04 19:27:48.274358+0800 BlockTest[37426:17007507] self.name = roy
    //2019-05-04 19:27:48.275016+0800 BlockTest[37426:17007507] -------dealloc-------
    

    我们看到Person对象被正常释放了,说明不存在循环引用,为什么呢?clang改写后的代码如下:

    struct __Person__testReferenceSelf_block_impl_0 {
      struct __block_impl impl;
      struct __Person__testReferenceSelf_block_desc_0* Desc;
      Person *const __weak weakself;
      __Person__testReferenceSelf_block_impl_0(void *fp, struct __Person__testReferenceSelf_block_desc_0 *desc, Person *const __weak _weakself, int flags=0) : weakself(_weakself) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    static void _I_Person_testReferenceSelf(Person * self, SEL _cmd) {
        __attribute__((objc_ownership(weak))) typeof(self) weakself = self;
        ((void (*)(id, SEL, void (*)()))(void *)objc_msgSend)((id)self, sel_registerName("setBlock:"), ((void (*)())&__Person__testReferenceSelf_block_impl_0((void *)__Person__testReferenceSelf_block_func_0, &__Person__testReferenceSelf_block_desc_0_DATA, weakself, 570425344)));
        ((void (*(*)(id, SEL))())(void *)objc_msgSend)((id)self, sel_registerName("block"))();
    }
    

    可以看到__Person__testReferenceSelf_block_impl_0结构体中weakself成员是一个__weak修饰的Person类型对象,也就是说__Person__testReferenceSelf_block_impl_0对Person的依赖是弱依赖。weak修饰变量是在runtime中进行处理的,在Person对象的Dealloc方法中会调用weak引用的处理方法,从weak_table中寻找弱引用的依赖对象,进行清除处理。

    • __strong(block执行过程中的强制持有)

    当在block外使用 __weak 修饰对象,虽然可以防止循环引用,但是这个对象在block正在执行的过程中也可能会变成nil,这就可能会带来一些未知问题。这种情况一般在多线程回调过程中比较常见,如子线程处理完任务后回调时,不能保证block中的原对象还未被释放。

    TestObj *obj = [[TestObj alloc] init];
    __weak TestObj *weakObj = obj;
    NSLog(@"before block retainCount:%zd",[obj arcDebugRetainCount]);
    block = ^(){
        NSLog(@"TestObj对象地址:%@",weakObj);
        dispatch_async(dispatch_queue_create(DISPATCH_QUEUE_PRIORITY_DEFAULT, NULL), ^{
            for (int i = 0; i < 1000000; i++) {
                // 模拟一个耗时的任务
            }
            NSLog(@"耗时的任务 结束 TestObj对象地址:%@",weakObj);
        });
    };
    NSLog(@"after block retainCount:%zd",[obj arcDebugRetainCount]);
    block();
    
    //打印结果:
    //DemoWeek[19247:6816518] before block retainCount:1
    //DemoWeek[19247:6816518] after block retainCount:1
    //DemoWeek[19247:6816518] TestObj对象地址:<TestObj: 0x602000006af0>
    //DemoWeek[19247:6816518] TestObj 对象已释放
    //DemoWeek[19247:6816544] 耗时的任务 结束 TestObj对象地址:(null)
    

    正确的做法是,在block执行的开始检验弱引用的对象是否还存在,如果还存在则使用__strong进行强引用。这样,在block执行的整个过程中,该对象就不会被置为nil,而在block执行完毕后,对象的引用计数就会-1,这样就不会导致对象无法释放。

    block = ^(){
        __strong  TestObj *strongObj = weakObj;
        if(! strongObj) return;
        NSLog(@"TestObj对象地址:%@",strongObj);
        dispatch_async(dispatch_queue_create(DISPATCH_QUEUE_PRIORITY_DEFAULT, NULL), ^{
            for (int i = 0; i < 1000000; i++) {
                // 模拟一个耗时的任务
            }
            NSLog(@"耗时的任务 结束 TestObj对象地址:%@",strongObj);
        });
    
    //打印结果:
    //DemoWeek[19280:6819437] before block retainCount:1
    //DemoWeek[19280:6819437] after block retainCount:1
    //DemoWeek[19280:6819437] TestObj对象地址:<TestObj: 0x602000006b30>
    //DemoWeek[19280:6819464] 耗时的任务 结束 TestObj对象地址:<TestObj: 0x602000006b30>
    //DemoWeek[19280:6819464] TestObj 对象已释放
    

    4. block 的存储区域

    上面代码中结构体__block_impl中的isa指针指向_NSConcreteStackBlock类型的类对象,其实该类对象总共有三种类型:

    _NSConcreteStackBlock // 栈
    _NSConcreteMallocBlock // 堆
    _NSConcreteGlobalBlock // 数据区
    

    block不同存储区域,实例如下:

    void (^globalBlock)(void) = ^{
        NSLog(@"Global Block");
    };
     
    int main() {
        globalBlock();
        NSLog(@"%@",[globalBlock class]); // 打印:__NSGlobalBlock__
        
    
        void (^stackBlock)(void) = ^{ // 没有截获自动变量的Block
            NSLog(@"Stack Block");
        };
        stackBlock();
        NSLog(@"%@",[stackBlock class]); // 打印:__NSStackBlock__
        
    
        int i = 1;
        void (^mallocBlock)(void) = ^{ // 截获自动变量i的Block
            NSLog(@"Malloc Block:%d", i);
        };
        mallocBlock();
        NSLog(@"%@",[mallocBlock class]);//打印:__NSMallocBlock__
    }
    

    block 是一个对象,所以 block 理论上是可以 retain/release 的,但是 block 在创建的时候它的内存是默认分配在栈(stack)上,而不是堆(heap)上的,所以它的作用域仅限创建时候的上下文(函数,方法···),当你在该作用域外调用该 block 时,程序就会奔溃.

    如果其所属的栈作用域结束,该block就会被废弃,对于超出block作用域仍需使用block的情况,从clang过的block相关代码中可见,其提供了将block从栈上复制到堆上的方法来解决这种问题。即便Block栈作用域已结束,但被拷贝到堆上的block还可以继续存在。

    复制到堆上的block实例,会修改其成员变量isa为_NSConcreteMallocBlock类对象:

    impl.isa = &_NSConcreteMallocBlock;
    

    在ARC有效时,大多数情况下编译器会进行判断,自动生成将Block从栈上复制到堆上的代码,以下几种情况栈上的block会自动复制到堆上:
    调用block的copy方法;
    将block作为函数返回值时;
    将block赋值给__strong修改的变量时;
    向Cocoa框架含有usingBlock的方法或者GCD的API传递Block参数时。

    非ARC情况下向方法的参数中传递block,则需要开发者手动调用copy方法复制。

    相关文章

      网友评论

          本文标题:—— block详解

          本文链接:https://www.haomeiwen.com/subject/fbxeeltx.html