Block原理

作者: NeroXie | 来源:发表于2018-08-16 16:22 被阅读47次

    原文链接Block原理

    Block的实质

    我们先写一个最基础的Block

    int main(int argc, const char * argv[]) {
        void (^testBlock)(void) = ^{
            printf("asddasd");
        };
        testBlock();
        
        return 0;
    }
    

    接着我们使用clang将这段代码翻译,在文件目录下使用clang -rewrite-objc 文件名,会生成一个cpp的文件,里面都是C++代码。其中有关键代码如下:

    int main(int argc, const char * argv[]) {
        void (*testBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        ((void (*)(__block_impl *))((__block_impl *)testBlock)->FuncPtr)((__block_impl *)testBlock);
    
        return 0;
    }
    

    可以看到我们定义的testBlock变成了((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA))。在执行Block的时候将testBlock()变成了((void (*)(__block_impl *))((__block_impl *)testBlock)->FuncPtr)((__block_impl *)testBlock);

    我们先看一下Block声明的部分。

    __main_block_impl_0

    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;//用于初始化__block_impl结构体的isa成员
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    可以看出__main_block_impl_0是一个结构体,它有两个成员变量分别是__block_impl impl__main_block_desc_0* Desc,还有一个__main_block_impl_0的构造函数。((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA))这句代码其实就是通过__main_block_impl_0的构造函数生成一个__main_block_impl_0的结构体,它传了两个参数__main_block_func_0&__main_block_desc_0_DATA

    __block_impl

    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    

    通过这个结构体的定义,我们可以知道Block也是有isa指针的,isa的初始值是_NSConcreteStackBlock,关于_NSConcreteStackBlock就相当于class_t中的结构体。在Block作为OC对象处理的时候,关于该类的信息就放在_NSConcreteStackBlock中。这就意味着Block就是Objective-C的对象。

    __main_block_desc_0

    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的大小以及版本升级所需要的区域。

    接着我们看一下Block执行部分。上面提到在构造结构体的时候,我们分别传了两个参数__main_block_func_0&__main_block_desc_0_DATA

    __main_block_func_0

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        printf("asddasd");
    }
    

    可以看出,我们在Block里写的代码就在__main_block_func_0中,((void (*)(__block_impl *))((__block_impl *)testBlock)->FuncPtr)((__block_impl *)testBlock);这句代码其实就是一个简单的指针调用,调用了FuncPtr,它指向的地址就是__main_block_func_0的地址。Block的调用本质其实就是函数指针的调用。

    截获变量

    截获自动变量值

    根据《Objective-C高级编程》一书中提到,所谓的“截获自动变量值”意味着在执行Block语法时,Block语法表达式所使用的自动变量被保存到Block的结构体实例(即Block自身中)。

    先写一段OC的代码:

    #include <stdio.h>
    
    static int globalStaticValue = 1;
    
    int globalValue = 2;
    
    int main(int argc, char * argv[]) {
        int a = 3;
        static int b = 4;
        void (^testBlock)(void) = ^{
            printf("%d", globalStaticValue);
            printf("%d", globalValue);
            printf("%d", a);
            printf("%d", b);
        };
        testBlock();
        
        return 0;
    }
    

    上面代码分别声明了一个静态全局变量、全局变量、局部变量、静态局部变量。我们将代码转换成C++代码后,看一下它的mian函数和__main_block_impl_0结构体。

    int main(int argc, char * argv[]) {
        int a = 3;
        static int b = 4;
        void (*testBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a, &b));
        ((void (*)(__block_impl *))((__block_impl *)testBlock)->FuncPtr)((__block_impl *)testBlock);
    
        return 0;
    }
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int a;
      int *b;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int *_b, int flags=0) : a(_a), b(_b) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    可以看出Block语法中使用的自动变量被作为成员变量追加到了__main_block_impl_0结构体中:

    • int a; 基本数据类型,直接截获值。
    • *int b; 局部静态变量,截获其指针。

    那么全局变量和全局静态变量呢?

    static int globalStaticValue = 1;
    
    int globalValue = 2;
    
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        int a = __cself->a; // bound by copy
        int *b = __cself->b; // bound by copy
    
        printf("%d", globalStaticValue);
        printf("%d", globalValue);
        printf("%d", a);
        printf("%d", (*b));
    }
    

    __main_block_func_0可以看到,全局变量和全局静态变量并不会被截获,而是直接使用

    截获对象

    Block是如何截获对象型的变量的?还是先给出一段示例代码。

    #import <Foundation/Foundation.h>
    
    int main(int argc, char * argv[]) {
        NSObject *objc = [[NSObject alloc] init];
        void (^testBlock)(void) = ^{
            NSLog(@"%@", objc);
        };
        testBlock();
        
        return 0;
    }
    

    通过clang -rewrite-objc -fobjc-arc 文件名转成C++文件,对应的C++代码:

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      NSObject *__strong objc;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *__strong _objc, int flags=0) : objc(_objc) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      NSObject *__strong objc = __cself->objc; // bound by copy
      NSLog((NSString *)&__NSConstantStringImpl__var_folders_pl_6qlbyntd0zngprtxq_hwdshc0000gn_T_main_b61654_mi_0, objc);
    }
    
    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->objc, (void*)src->objc, 3/*BLOCK_FIELD_IS_OBJECT*/);}
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->objc, 3/*BLOCK_FIELD_IS_OBJECT*/);}
    
    static struct __main_block_desc_0 {
      size_t reserved;
      size_t Block_size;
      void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
      void (*dispose)(struct __main_block_impl_0*);
    } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
    
    int main(int argc, char * argv[]) {
        NSObject *objc = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
        void (*testBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, objc, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)testBlock)->FuncPtr)((__block_impl *)testBlock);
    
        return 0;
    }
    

    __main_block_impl_0的结构体有NSObject *__strong objc;,所以Block在截获对象的时候,会将对象及其所有权修饰符一并截获

    这里我们需要注意两个函数,一个是__main_block_copy_0函数,因为含有__strong修饰符的对象类型变量objc,所以需要恰当管理赋值给objc的对象。__main_block_copy_0函数使用_Block_object_assign函数将对象类型对象赋值给Block结构体中的成员变量objc并持有该对象,这就相当于retain实例方法。
    另外一个函数是__main_block_dispose_0,它使用_Block_object_dispose释放掉结构体成员变量objc中的对象,相当于release方法。

    __block修饰符

    当我们需要对被Block截获的局部变量进行赋值操作的话,需要添加一个__block这个说明符,那__block到底有什么作用呢?
    先看一段代码:

    #include <stdio.h>
    
    int main(int argc, char * argv[]) {
        __block int a = 3;
        void (^testBlock)(void) = ^{
            a = 100;
            printf("%d", a);
        };
        testBlock();
        
        return 0;
    }
    

    由于使用了__block关键字,可以修改变量a的值,所以输出结果是100。
    转换成C++代码:

    先看一下__main_block_impl_0这个结构体

    struct __Block_byref_a_0 {
      void *__isa;
    __Block_byref_a_0 *__forwarding;
     int __flags;
     int __size;
     int a;
    };
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      __Block_byref_a_0 *a; // by ref
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    

    和刚才不一样的是在__main_block_impl_0结构体中多了一个__Block_byref_a_0 *a而不是int a, __Block_byref_a_0 *a也是一个结构体,里面也有一个isa指针,因此我们也可以将它当做一个对象。另外还有一个__Block_byref_a_0 *类型的__forwarding指针和变量a。关于__forwarding指针的作用我们会在后面提到。

    接着是主函数:

    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
    
    static struct __main_block_desc_0 {
      size_t reserved;
      size_t Block_size;
      void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
      void (*dispose)(struct __main_block_impl_0*);
    } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
    
    int main(int argc, char * argv[]) {
        __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 3};
        void (*testBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)testBlock)->FuncPtr)((__block_impl *)testBlock);
    
        return 0;
    }
    

    在主函数中我们可以看到__block int a = 3;转化成了__Block_byref_a_0的一个结构体,其中__forwarding指针指向这个结构体自己。

    最后是Block执行的时候即调用__main_block_func_0函数:

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      __Block_byref_a_0 *a = __cself->a; // bound by ref
      (a->__forwarding->a) = 100;
      printf("%d", (a->__forwarding->a));
    }
    

    原来的a = 100;则被转换成__Block_byref_a_0 *a = __cself->a;(a->__forwarding->a) = 100;。这两句代码的意思是先获取结构体中的a,通过a结构体的__forwarding指针指向成员变量a赋值为100。

    我们总结一下__block的作用:__block将我们定义的数据变成另一个对象类型,将截获到的值存在这个对象中,通过对这个变量进行赋值而更改原有的值(如果是__block修饰对象类型的变量也是一样)

    Block内存管理

    从上面我们知道了__Block_byref_a_0结构体中已经有变量a, 为什么还需要使用__forwarding指针对a赋值呢或者说__forwarding存在的意义是什么?

    通过前面的说明可知,Block转化为Block的结构体类型的自动变量,__block变量转变为__block变量的结构体类型的自动变量。所谓自动变量就是在栈上生成的该结构体的实例。

    关于__main_block_impl_0结构体初始化的时候,isa被赋值为&_NSConcreteStackBlock。关于_NSConcreteStackBlock在《Objective-C高级编程》一书中提到:

    设置对象的存储域
    _NSConcreteStackBlock
    _NSConcreteGlobalBlock 程序的数据区域
    _NSConcreteMallocBlock

    先看copy操作对不同类型block的影响:

    Block的类 副本源的配置存储域 复制效果
    _NSConcreteStackBlock 从栈复制到堆
    _NSConcreteGlobalBlock 程序的数据区域 什么也不做
    _NSConcreteMallocBlock 引用计数增加

    关于__forwarding的作用《Objective-C高级编程》一书中的第111页、第112页中有很明确的说到:__block变量的结构体成员变量__forwarding可以实现无论__block变量配置在栈上还是堆上都能够正确地访问__block变量。

    没有进行copy操作的时候: 栈上的block
    进行copy操作以后: block拷贝
    这里分为两种情况:
    1. 如果我们没有对栈上的Block执⾏copy操作,修改被__block修饰的变量实际上是通过其__forwarding指针指向的自身,对其中的变量进行修改。

    2. 如果我们执行过copy操作,那么栈上的Block的__forwarding指针,实际是指向堆上的__block修饰的变量,⽽堆上的__forwarding指针则指向⾃自身的__block修饰的变量。在变量作用域结束的时候,栈上的__block变量和Block被废弃掉,但是堆上的__block变量和Block不受影响。

    Block的循环引用

    我们在使用Block的时候,如果有一个对象持有了这个Block,而在Block内部又使用了这个对象,就会造成循环引用。如以下写法:

    #import <Foundation/Foundation.h>
    
    typedef void (^TestBlock) (void);
    
    @interface AObject: NSObject {
        TestBlock testBlock;
    }
    
    @end
    
    @implementation AObject
    
    - (instancetype)init {
        if (self = [super init]) {
            testBlock = ^{
                NSLog(@"self = %@", self);
            };
        }
        
        return self;
    }
    
    - (void)dealloc {
        NSLog(@"%@ dealloc", self.class);
    }
    
    @end
    
    int main(int argc, char * argv[]) {
        
        AObject *a = [[AObject alloc] init];
        NSLog(@"%@", a);
        
        return 0;
    }
    
    

    可以看到,上面这段代码发生了循环引用,导致AObject对象无法被释放。通常做法是使用__weak关键字在Block外部声明一个弱引用。重新修改下init方法:

    - (instancetype)init {
        if (self = [super init]) {
            __weak AObject *weakSelf = self;
            testBlock = ^{
                NSLog(@"self = %@", weakSelf);
            };
        }
        
        return self;
    }
    

    前面在讲述截获自动变量值的时候,我们知道Block在截获对象类型的时候,会连同对象的所有权修饰符一起截获,这里截获的是__weak修饰的weakSelf,因此不会发生循环引用。

    在为避免循环引用而使用__weak修饰符的时候,还要注意有可能在Block执行的时候,对象在中途被释放掉了。这个时候需要在Block内部声明一个局部变量强持有对象,这个局部变量会在到Block执行结束时自动释放,不会造成循环引用,而对象也会在Block执行结束后被释放。

    是不是所有的Block都需要在外部声明使用__weak修饰呢?答案是否定的。所谓“引用循环”是指双向的强引用,所以那些“单向的强引用”(block强引用self)没有问题。例如:

    [UIView animateWithDuration:duration
                     animations:^{
                         [self.superview layoutIfNeeded];
                     }];
    

    但如果你使用一些参数中可能含有ivar的系统的api,如NSNotificationCenter就要小心一点。

     __weak __typeof__(self) weakSelf = self;
     _observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"testKey"
                                                                   object:nil
                                                                    queue:nil
                                                               usingBlock:^(NSNotification *note) {
        //使用self会循环引用,但是使用weakSelf或者strongSelf则没有问题                                                      
         __typeof__(self) strongSelf = weakSelf;
         [strongSelf dismissModalViewControllerAnimated:YES];
     }];
    

    可以看出self --> _observer --> block --> self这显然是一个循环引用。总而言之,只有当self直接或间接的持有 Block,并且在Block内部又使用了self的时候,才应该使weakSelf

    Sunnyxx的Block面试题

    sunnyxx_block

    第一题思路

    用一个函数指针保存原有的实现,在hookFunc里面调用一下就可以了。代码如下:

    typedef struct n__block_impl {
        void *isa;
        int Flags;
        int Reserved;
        void *FuncPtr;
    }n__block_impl;
    
    void hookInvokePrintHelloWorld(){
        printf("Hello,world\n");
    }
    
    void HookBlockToPrintHelloWorld(id block){
        n__block_impl *tmp = (__bridge n__block_impl *)block;
        tmp->FuncPtr = &hookInvokePrintHelloWorld;
    }
    
    #pragma mark - Lifecycle
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        NNVoidBlock block = ^{
            NSLog(@"%s", __func__);
        };
        
        HookBlockToPrintHelloWorld(block);
        
        block();
    }
    

    相关文章

      网友评论

        本文标题:Block原理

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