美文网首页
block详细了解及底层探索

block详细了解及底层探索

作者: 猿人 | 来源:发表于2020-12-14 13:15 被阅读0次

    block 三种类型

    全局block NSGlobalBlock
       void (^block)(void) = ^{
            NSLog(@"hahah");
        };
        
        NSLog(@"%@", block);
    
    • 没有对外界变量进行捕获的时候,它是个函数的区域,直接放在全局区,方便执行调用。
    堆区block NSMallocBlock
       int a = 10;
        void (^block)(void) = ^{
            NSLog(@"Cooci - %d",a);
        };
    
        NSLog(@"%@",block);
    
    • 访问外界变量的时候,它将进行一些处理,因为访问的变量可能在栈区,堆区,如果在全局区,去访问会比较麻烦,所以block进行了相应的copy。copy到了相应 的一些区域。所以向上面进行了一次强引用的时候。此时是堆block.
    栈区block NSStackBlock

    这里有个坑点,在iOS14之前 在block没有进行copy处理的时候它是一个栈区block,而之后却放在了堆里。

      NSLog(@"%@",^{
            NSLog(@"Cooci - %d",a);
        });
     
    
    • iOS14之前 为栈区,也就是ARC下没有被持有的话,向上面写法为栈区。
    • 而现在在堆区。

    栈区的block写法

       int a = 10;
        void (^__weak block)(void) = ^{
            NSLog(@"Cooci - %d",a);
        };
    
        NSLog(@"%@",block);
    
    • 引用了外部变量,当此时对block进行了了一次弱引用它就在栈区。

    block 循环引用

    正常释放

    当 A 对象 持有 B对象 的时候,B对象 的引用计数 会+1


    截屏2021-07-06 下午3.35.11.png

    当A释放的时候会给 B 信号,B接收到 release信号,引用计数 -1 等于0的时候 b的dealloc就会被调用


    截屏2021-07-06 下午3.52.21.png
    循环引用

    当 A 持有 B ,B也持有 A ,你中有我 我中有你的情况。就会造成循环引用。


    截屏2021-07-06 下午3.59.49.png
    循环引用代码示意
       ///会发生循环引用
       self.block = ^(void){
             NSLog(@"%@",self.name);
        };
     ///不会发生循环引用
     [UIView animateWithDuration:0.2 animations:^{
            NSLog(@"%@",self.name);
        }];
    
    • 上面的情况我们都知道,对于发生循环引用 我们该怎么解决呢?
    解决打破循环引用。

    1、__weak typeof(self)weakSelf = self

    __weak  typeof(self)weakSelf = self
       self.block = ^(void){
            NSLog(@"%@", weakSelf.name);
       };
    
    • 这种情况我们都知道用这个方法来打破那为什么?
    • 没有打破之前 的样子是这样的 self ->block -> self(self持有block,block持有self) ,
      而打破之后 就是这样 self -> block ->weakSelf ->self (self持有block,block持有 weakSelf,weakSelf持有 self.)那这样就不会导致循环引用了么?weakSelf也持有者 self呢呀
    • 因为 weakSelf 是弱引用表中的,和当前的self是同一个指针地址。__weak并不会导致self的引用计数发生变化。

    那这样就没问题了吗看下面

       __weak typeof(self) weakSelf = self;
          self.block = ^(void){
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                NSLog(@"%@",weakSelf.name);
            });
        };
        self.block();
    

    此时我们发现 当前页面确实走了 dealloc。但是 当 延时任务回来的时候 ,却发现 打印的为nil. 虽说一个打印任务并无商大雅。但是当里面执行的任务为很重要的时候。我还没走完你就 dealloc,显然不符合我的要求。所以我们正确的用法为 weak - strong -Dance 强弱共舞,保证self的声明周期。

    __weak typeof(self) weakSelf = self;
       self.block = ^(void){
            // 时间 - 精力
            // self 的生命周期
            __strong __typeof(weakSelf)strongSelf = weakSelf; // 可以释放 when
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                NSLog(@"%@",strongSelf.name);
            });
        };
        self.block();
    
    • 在这里我们可以看到 __weak 打破了循环引用。
    • __strong 延长了 self的生命周期。

    2、通过传参的形式将self 传进block任务中。

        self.block = ^(ViewController *vc){
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                NSLog(@"%@",vc.name);
            });
        };
        self.block(self);
    
    • 这样的话就打破了block对vc的持有,此时vc是已传参的形式,它在block里就相当于一个临时变量被压栈进来。

    3、主动打破循环

       __block ViewController *vc = self;
        self.block = ^(void){
             dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
               NSLog(@"%@",vc.name);
              vc = nil;
             });
       };
         self.block();
    
    • __block 为了可以在block里可以进行修改。
    • vc = nil 是为了打破循环引用。
    • 注意:此时不调用循环引用依旧会存在。

    4、NSProxy 也可以,这里就不讲了,自行搜索。

    底层探究

    定义一个简单的.c文件 如下 ;

    int main(){
          void(^block)(void) = ^{
        
            printf("LG_Cooci");
        };
         //block();
        return 0;
    }
    
    • clang 查看 底层被编译成了什么样 xcrun -sdk iphonesimulator clang -arch x86_64 -rewrite-objc block.c
    int main(){
         ///简化去掉返回值类型
        void(*block)(void) =  
    &__main_block_impl_0  (  __main_block_func_0  ,  &__main_block_desc_0_DATA ) ;
    
    
        return 0;
    }
    
    • 清晰的看到一个函数__main_block_impl_0 和两个参数 参数1: __main_block_func_0参数2:__main_block_desc_0_DATA

    查看 __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;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
        
    };
    
    • 可以看到 __main_block_impl_0这个函数它是一个结构体,也就是说block是一个__main_block_impl_0类型的对象。

    • 里面有两个 结构体成员 一个为__block_impl 一个为 __main_block_desc_0类型
      __block_impl 结构类型

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

      __main_block_desc_0结构类型

       static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      
            printf("LG_Cooci");
      }
      
      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)};
      
      
    • 在看一下它(__main_block_impl_0)的构造函数。

      1. 给第一个结构体__block_impl类型的 成员 impl赋值。
        impl 的 isa成员赋值 为 栈block 类型;
        implFlages设置了个标记;
        implFuncPtr成员 赋值为外界 传进来的 __main_block_func_0 函数。
      2. 给第二个成员Desc 赋值外界传进来的__main_block_desc_0_DATA的地址。
    • 画图表示一下这个结构


      截屏2020-11-28 上午11.16.49.png
    • 总结: block的本质 是一个结构体 也可以说是一个对象,它内部有两个属性,一个来存放 块任务的,方法 及设置当前块任务类型的isa。另一个属性来计算当前自己结构体所占空间大小。

    下面 我们看一下block是如何发起调用的。

    依旧是这段代码,打开 下方的 block()调用。

    int main(){
          void(^block)(void) = ^{
        
            printf("LG_Cooci");
        };
        
        block();
        return 0;
    }
    
    

    clang 编译期源码

        void(*block)(void) =  
    &__main_block_impl_0  (  __main_block_func_0  ,  &__main_block_desc_0_DATA ) ;
      
     ((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    

    看到这里 我们就明白了,此时发起调用,它是 将 block指针强转为__block_impl类型。并获取之前存入的 FuncPtr 发起函数调用,并将 block指针作为参数传入。

    block如何捕获外界变量的

    int main(){
          
        int a =10;
        void(^block)(void) = ^{
        
            printf("LG_Cooci%d",a);
        };
        
        block();
        return 0;
    }
    
    

    clang

    int main(){
    
        int a =10;
        void(*block)(void) =  &__main_block_impl_0 (
    
                  __main_block_func_0,
                                                    
                  &__main_block_desc_0_DATA,
                                                    
                  a
            ) ;
         
         ((__block_impl *)block)->FuncPtr)((__block_impl *)block);
        
        return 0;
    }
    
    • 此时我们看到 当block内部引用到了外部变量的时候。__main_block_impl_0 构造函数就会动态的向后添加一个 参数。

    再次看下__main_block_impl_0结构体变化

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int a;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    • 可以看到 __main_block_impl_0结构体中多了一个int 类型的 a. 通过构造函数 将a赋值。

    再次 看 __block_implFuncPtr 赋值 也就是外界传进来的 __main_block_func_0 函数实现

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      int a = __cself->a; // bound by copy
    
    
            printf("LG_Cooci%d",a);
        }
    
    • 此时我们可以看到 当block发起调用的时候 此时 将 __main_block_func_0结构体中的a的值 赋值给了一个临时变量 。

    • 由此就可以下结论,此时是值拷贝,外界a的变化 并不会 引起 block内部 a的变化。

    为了彻底弄清楚 我们 写一个我们平常的oc 对象,在block块内部引用

    请问下面输出什么?

          LGPerson * person = [[LGPerson alloc]init];
          person.tag = @"等风来不如追风去,总有那么一个人在风景正好的季节来到你的身边";
         
           void(^block)(void) = ^{
     
                NSLog(@"%@",person.tag);
                
            };
     
            person.tag = @"45°仰望天空,该死我那无处安放的魅力";
     
            block();
    

    我们 clang 去看

    
            LGPerson * person = (((void *)objc_msgSend)((id)((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc")), sel_registerName("init"));
    
    
            ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)person, sel_registerName("setTag:"), (NSString *)&__NSConstantStringImpl__var_folders_hp_8p1s5vl9501d23q4rjltk8j80000gn_T_main_6caa76_mi_0);
                                 
       ///block 构造函数 结构体赋值
            void(*block)(void) = &__main_block_impl_0(
                                                      __main_block_func_0,
                                                      
                                                      &__main_block_desc_0_DATA,
                                                      
                                                      person,
                                                      
                                                      570425344));
    
            
            ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)person, sel_registerName("setTag:"), (NSString *)&__NSConstantStringImpl__var_folders_hp_8p1s5vl9501d23q4rjltk8j80000gn_T_main_6caa76_mi_2);
    
            ///发起调用
             ((__block_impl *)block)->FuncPtr)((__block_impl *)block);
            
    
    • 此时我们看到 此时__main_block_impl_0person指针捕获进去了。

    再次 看此时的 __main_block_impl_0结构体

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      LGPerson *person;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc,LGPerson *_person, int flags=0) : person(_person) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    • 可以看到结构体内部已经多了一个 对象指针。

    在看一下方法

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      LHPerson *person = __cself->person; // bound by copy
    
    
                NSLog((NSString *)&__NSConstantStringImpl__var_folders_hp_8p1s5vl9501d23q4rjltk8j80000gn_T_main_6caa76_mi_1,((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("tag")));
    
              }
    
    • 当block 发起调用的时候,首先找到 impl中存入的 方法,并用此方法发起调用,并将 block的结构体对象 当参数,传入。此时可以看到 获取了 block结构体中的 person指针。
    • 到现在我们明白了 此时对象类型的捕获的是对象的指针。属于指针copy。也就是浅拷贝。

    值copy 此时 内存空间 两个一样的 内容。指针 不一样。也就是深拷贝。
    指针 copy 此时 copy了一个指针,两个指针指向同一片内存区域。也就是浅拷贝。

    我们对于 值拷贝的基础数据类型的捕获 该如何操作呢?

    __block

    在什么情况下我们需要用__block的修饰?

    • 当block内部需要对外界的变量 修改时,如不用__block修饰,会引起编译器的歧义,导致只能读。
    • 当捕获的是临时变量,如不用__block修饰,会导致内外数据不同步。
    • 如捕获的是容器类型,容器内容发生更改不需要进行__block修饰。
    • 如捕获的是对象,对象的某个属性发生更改,不需要进行__block修饰。
    • 如捕获的是 statc修饰的(局部 /全局)变量 或 全局变量 不需要__block修饰。

    __block又做了哪些事情?带着疑问向下分析

    int main(){
          
        __block int a =10;
        void(^block)(void) = ^{
        
            printf("LG_Cooci%d",a);
        };
        
        a = 20;
        block();
        return 0;
    }
    
    

    继续 clang看编译期变成了什么样

    struct __Block_byref_a_0 {
      void *__isa;
    __Block_byref_a_0 *__forwarding;
     int __flags;
     int __size;
     int a;
    };
     ===============================================
    
        __Block_byref_a_0 a = {
                  0,
                  (__Block_byref_a_0 *) &a,
                  0,
                  sizeof(__Block_byref_a_0),
                  10
                 
             };
    
      void(*block)(void) =  &__main_block_impl_0 (
    
                              __main_block_func_0,
                                                       
                              &__main_block_desc_0_DATA,
                                                       
                              (__Block_byref_a_0 *)&a,
                                                       
                              570425344
                   );
    
            (a.__forwarding->a) = 20;
           ((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    
    
    
    
    
    • 此时我们看到了变量 a 被包装成了 一个 __Block_byref_a_0类型的结构体对象,并相对应的录入变量a的信息, 对应上面结构体可以清楚的看到 里面存有a的地址赋值给__forwarding指针,a的值自身大小 等参数。

    • 将这个包装后的a的结构体对象取地址 , 作为block结构体的构造函数 __main_block_impl_0 参数传入 赋值给 block结构体里边的 a指针

    • 调用执行上一行代码 拿到a结构体指针修改 a变量的值。所以内外同步数据。
      继续查看 block结构体 __main_block_impl_0

    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_func_0

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      __Block_byref_a_0 *a = __cself->a; // bound by ref
    
    
                printf("LG_Cooci%d",(a->__forwarding->a));
            }
    
    • 这里清晰的看到获取到block结构体里面的包装后的__Block_byref_a_0类型的 a指针,并通过 a指针拿到 __forwarding 也就是 指向外界变量的a地址的指针,并取出 变量a真正的值。
    • 这里从不加__Block的值拷贝 变成了 指针拷贝。而这个指针是指向的同 一个结构体地址,这个结构体里面存有 变量 a的地址 和a的值,

    咦?那为啥数据就同步了呢,我不用__Block修饰 我捕获一个字符串,它也是指针那为啥 当我在对block发起调用前重新修改 字符串的值,它怎么数据不同步呢?

        NSString * str = [NSString stringWithFormat:@"等风来不如追风去啊"];
               
               void (^block)(void) = ^{
                 
                   NSLog(@"%@,%p",str,str);
               };
             
               str = @"总有一个人,在风景正好的季节等着你";
               NSLog(@"%@,%p",str,str);
    
            
               block();
            
    
    • 看着上面的疑问 在次陷入深思,我们继续看下clang之后的编译期代码
        ///字符串指针
             NSString * str = ((NSString * _Nonnull (*)(id, SEL, NSString * _Nonnull __strong, ...))(void *)objc_msgSend)((id)objc_getClass("NSString"), sel_registerName("stringWithFormat:"), (NSString *)&__NSConstantStringImpl__var_folders_hp_8p1s5vl9501d23q4rjltk8j80000gn_T_main_e485f6_mi_0);
            
              /// block
               void (*block)(void) =
            __main_block_impl_0(
                                __main_block_func_0,
                                
                                &__main_block_desc_0_DATA,
                                
                                str,
                                
                                570425344
                                );
    
             ///重新赋值 改变指针指向
             str = (NSString *)&__NSConstantStringImpl__var_folders_hp_8p1s5vl9501d23q4rjltk8j80000gn_T_main_e485f6_mi_2;
              
            /// 打印
             NSLog((NSString *)&__NSConstantStringImpl__var_folders_hp_8p1s5vl9501d23q4rjltk8j80000gn_T_main_e485f6_mi_3,str,str);
    
            ///发起调用
             ((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    

    看func函数

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      NSString *__strong str = __cself->str; // bound by copy
    
    
                   NSLog((NSString *)&__NSConstantStringImpl__var_folders_hp_8p1s5vl9501d23q4rjltk8j80000gn_T_main_e485f6_mi_1,str,str);
               }
        }
    
    • 看到这里的的确确是指针拷贝,func函数里的指针指向 和 block析构函数参数指针指向同一片地址空间。
    • 那为啥 我在调用之前更改了 str变量的值里边它里边不同步?

    继续带着这个疑问 我们打印一下上下str的指针指向地址。


    截屏2020-12-11 下午3.53.57.png
    • 虽然是捕获的是指针,在调用之前 指针的指向被改变,它指向了新的一片地址空间 。

    __block修饰 运行

    截屏2020-12-11 下午3.58.58.png
    • 此时我们知道 str被封装为一个结构体对象
    • 而在调用block之前进行进行对 str修改,此时为结构体指针copy。 对它指向的这个结构体地址里的 str的值所占用的内存空间进行了修改。所以数据同步。

    为了验证我们的想法 再次查看用block修饰后的cpp

    
    struct __Block_byref_str_0 {
      void *__isa;
    __Block_byref_str_0 *__forwarding;
     int __flags;
     int __size;
     void (*__Block_byref_id_object_copy)(void*, void*);
     void (*__Block_byref_id_object_dispose)(void*);
     NSString *__strong str;
    ===============================================
    
    
     ///byref结构体对象
             __Block_byref_str_0 str = {
                 (void*)0,
                 
                 (__Block_byref_str_0 *)&str,
                 
                 33554432,
                 
                 sizeof(__Block_byref_str_0),
                 
                 
                 __Block_byref_id_object_copy_131,
                 
                 __Block_byref_id_object_dispose_131,
                 
                 ((NSString * _Nonnull (*)(id, SEL, NSString * _Nonnull __strong, ...))(void *)objc_msgSend)((id)objc_getClass("NSString"), sel_registerName("stringWithFormat:"), (NSString *)&__NSConstantStringImpl__var_folders_hp_8p1s5vl9501d23q4rjltk8j80000gn_T_main_1c411f_mi_0)
                 
             };
    
            
            /// block
              &__main_block_impl_0(
                                   __main_block_func_0,
                                   
                                   &__main_block_desc_0_DATA,
                                   
                                   (__Block_byref_str_0 *)&str,
                                   
                                   570425344
                                   );
    
            ///重新赋值
            (str.__forwarding->str) = (NSString *)&__NSConstantStringImpl__var_folders_hp_8p1s5vl9501d23q4rjltk8j80000gn_T_main_1c411f_mi_2;
              
            ///打印
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_hp_8p1s5vl9501d23q4rjltk8j80000gn_T_main_1c411f_mi_3,(str.__forwarding->str),(str.__forwarding->str));
    
            ///函数调用
            (__block_impl *)block)->FuncPtr)((__block_impl *)block);
            
    
    • 咦这里好像和基本数据类型 int a的包装还不太一样多了两个函数 __Block_byref_id_object_copy 和__Block_byref_id_object_dispose
    • 我们重新赋值是改变的 包装后的结构体中的 str指针。

    在看一下 func

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      __Block_byref_str_0 *str = __cself->str; // bound by ref
    
    
                   NSLog((NSString *)&__NSConstantStringImpl__var_folders_hp_8p1s5vl9501d23q4rjltk8j80000gn_T_main_1c411f_mi_1,(str->__forwarding->str),(str->__forwarding->str));
               }
    
    • 从这里我们就能看出 此时是byref结构体指针copy 通过指针获取 forwarding 地址在取得 str指针指向。

    总结

    • 捕获的外界变量 底层会包装成一个 __Block_byref_a_0类型的结构体。
    • 结构体用来保存 原始的变量的指针 和值。
    • 将包装的成的结构体对象地址 传递 给block ,然后block内部就可以对外界变量进行操作。
    • 但是其内部是到底是怎么操作的为什么 string对象类型 要比 int基本数据类型 byref会多出两个方法?带着这些疑问向下看。

    block真正的类型

    打开汇编,并在下面区域打上断点


    截屏2020-11-28 下午5.58.06.png

    运行


    截屏2020-12-02 下午6.10.52.png

    我们看到到了callq 了 几个很重要的函数 一个

    • objc_retainBlock
    • objc_storeStrong
    • _Block_object_dispose

    分别符号断点下这个 看他来自哪个"星球"
    断点 objc_retainBlock

    截屏2020-12-02 下午6.15.24.png
    • 看到重要线索 它来自 libobjc, 并其实真正调用的是 _Block_copy;
    • 那还等什么去源码看看。

    objc4源码全局搜索 objc_retainBlock

    id objc_retainBlock(id x) {
        return (id)_Block_copy(x);
    }
    
    
    • 嗯没毛病 的确调用的 是 _Block_copy;

    全局搜索 _Block_copy发现Objc并未发现什么

    那接着下符号断点吧它肯定不来自这个库了。


    截屏2020-12-02 下午6.23.02.png
    • 原来它来自 libsystem_blocks.dylib。

    官网找到开源库全局搜索 _Block_copy

    // Copy, or bump refcount, of a block.  If really copying, call the copy helper if present.
    void *_Block_copy(const void *arg) {
        
        // block都是`Block_layout`类型
        struct Block_layout *aBlock;
    
        // 没有内容,直接返回空
        if (!arg) return NULL;
        
        // The following would be better done as a switch statement
        // 将内容转变为`Block_layout`结构体格式
        aBlock = (struct Block_layout *)arg;
        // 检查是否需要释放
        if (aBlock->flags & BLOCK_NEEDS_FREE) {
            latching_incr_int(&aBlock->flags);
            return aBlock;
        }
        // 如果是全局Block,直接返回
        else if (aBlock->flags & BLOCK_IS_GLOBAL) {
            return aBlock;
        }
        //
        else {
            // Its a stack block.  Make a copy.
            // 进入的是栈区block,拷贝一份
            // 开辟一个大小空间的result对象
            struct Block_layout *result =
                (struct Block_layout *)malloc(aBlock->descriptor->size);
            // 开辟失败,就返回
            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。
            result->invoke = aBlock->invoke;
    #endif
            // reset refcount
            // BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING :前16位都为1
            // ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING):前16位都为0
            // 与操作,结果为前16位都为0 引用计数为0
            result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed
            // 设置为需要释放,引用计数为1
            result->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1
            // 生成desc,并记录了result和aBlock
            _Block_call_copy_helper(result, aBlock); //
            // Set isa last so memory analysis tools see a fully-initialized object.
            // 设置isa为堆区Block
            result->isa = _NSConcreteMallocBlock;
            return result;
        }
    }
    
    • 这里我们看到了 block真正的类型 原来它是 Block_layout 类型的结构体
    • 仔细看 上面源代码的几个 if else 判断
      1、如果需要释放的(堆是由程序员管理的) 也就是 堆block的,增加引用计数 返回
      2、如果是全局的,直接返回
      3、如果是栈block.:从栈中 copy到 堆中; 过程: malloc开辟空间 ->memmove内存拷贝 ->invoke 指针拷贝->flag引用计数 设置为1 ->生成desc ->设置isa为堆block ->返回堆block.

    查看 Block_layout

    struct Block_layout {
        void *isa;
        volatile int32_t flags; // contains ref count
        int32_t reserved;
        BlockInvokeFunction invoke;
        struct Block_descriptor_1 *descriptor; //
        // imported variables
    };
    
    • isa : 从静态分析 到 动态库我们都知道了,它就是标记为是什么类型的block。
    • flags: 标识码(每一位都有特殊含义)
    • reserved : 保留字段
    • invoke : block执行函数(存储执行代码块)
    • descriptor: Block详细信息

    查看 Flags:标识码

    // Values for Block_layout->flags to describe block objects
    enum {
        BLOCK_DEALLOCATING =      (0x0001),  // runtime
        BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime
        BLOCK_NEEDS_FREE =        (1 << 24), // runtime
        BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler
        BLOCK_HAS_CTOR =          (1 << 26), // compiler: helpers have C++ code
        BLOCK_IS_GC =             (1 << 27), // runtime
        BLOCK_IS_GLOBAL =         (1 << 28), // compiler
        BLOCK_USE_STRET =         (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
        BLOCK_HAS_SIGNATURE  =    (1 << 30), // compiler
        BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31)  // compiler
    };
    
    • flags的赋值。按bit位表示一些block的附加信息,类似 isa中的位域,其中flags的种类有上面几种

    查看 Block_descriptor_1

    struct Block_descriptor_1 {
        uintptr_t reserved;
        uintptr_t size;
    };
    
    // 可选
    #define BLOCK_DESCRIPTOR_2 1
    struct Block_descriptor_2 {
        // requires BLOCK_HAS_COPY_DISPOSE
        BlockCopyFunction copy;
        BlockDisposeFunction dispose;
    };
    
    #define BLOCK_DESCRIPTOR_3 1
    struct Block_descriptor_3 {
        // requires BLOCK_HAS_SIGNATURE
        const char *signature;
        const char *layout;     // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
    };
    
    
    • 这里我们看到了 这个 类型拥3个结构体样式。
    • 可选类型 descriptor 2 为BLOCK_HAS_COPY_DISPOSE
    • 可选类型 descriptor 3 为BLOCK_HAS_SIGNATURE

    总结

    • block真正的底层结构为block_layout, 它里面包含 isa ,isa为最终确定的类型。还有flag , 类似 isa中的位域 。它里面记录着当前block的状态,如是否需要释放,是否是global ,是否需要签名进行消息发送等。运行时会调用block_copy,通过编译期的flag判断当前block的类型,如果是 需要释放的 操作引用计数并返回,如是全局block不做任何操作返回,如果是栈区的block 需要将 栈区的block Copy 到堆上,(申请内存空间 ,将栈区的block拷贝的堆区 ,将 block的执行函数 invoke拷贝,重新设置 flages 类型,生成对应的 desc,设置 isa类型为堆block) 此时block为最真实的状态。

    查看 _Block_call_copy_helper

    static void _Block_call_copy_helper(void *result, struct Block_layout *aBlock)
    {
        struct Block_descriptor_2 *desc = _Block_descriptor_2(aBlock);
        if (!desc) return;
    
        (*desc->copy)(result, aBlock); // do fixup
    }
    
    • 这里可以看到 如果拥有拓展descriptor2那么会发起一个函数调用

    查看descriptor访问操作

    #if 0
    static struct Block_descriptor_1 * _Block_descriptor_1(struct Block_layout *aBlock)
    {
        return aBlock->descriptor;
    }
    #endif
    
    static struct Block_descriptor_2 * _Block_descriptor_2(struct Block_layout *aBlock)
    {
        if (! (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)) return NULL;
        uint8_t *desc = (uint8_t *)aBlock->descriptor;
        desc += sizeof(struct Block_descriptor_1);
        return (struct Block_descriptor_2 *)desc;
    }
    
    static struct Block_descriptor_3 * _Block_descriptor_3(struct Block_layout *aBlock)
    {
        if (! (aBlock->flags & BLOCK_HAS_SIGNATURE)) return NULL;
        uint8_t *desc = (uint8_t *)aBlock->descriptor;
        desc += sizeof(struct Block_descriptor_1);
        if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE) {
            desc += sizeof(struct Block_descriptor_2);
        }
        return (struct Block_descriptor_3 *)desc;
    }
    
    
    • 这里可以清晰的看到,默认获取 block_layout 里 descriptor信息
    • 根据 block_layout里的flags& BLOCK_HAS_COPY_DISPOSE 如果为真 证明 有descriptor_2附加信息。 拿到 descripor1的指针 平移自身大小 得到 descriptor_2。
    • 根据 block_layout里的flags& BLOCK_HAS_SIGNATURE 如果为真 证明 有descriptor_3附加结构体信息。首先拿到 拿到 descripor1的指针 平移其自身大小 ,并查看是否有descriptor_2附加结构体,如果有,那么在平移加上 decriptor2大小 ,最终得到 descriptor_3

    看到这里我们应该更能体会到 descriptor 属性及上面的附加可选什么意思 下面画个图


    通过指针平移获取desc

    以上为我们开了上帝视角 下面我们实际操作 亲眼所看到 从栈block 拷贝到堆的过程

    上面我们已经通过阅读源码知道了 当底层调用完Block_copy 其真实的block类型就会确定所以我们在调用之前打断点读取

    截屏2020-12-11 下午2.05.42.png
    • 可以看到此时为 NSStackBlock

    按住 ctrl + 鼠标点击 向下箭头 ,跳进 objc_retainBlock 方法继续打印


    截屏2020-12-11 下午1.28.59.png

    跳进了 objc_retainBlock


    截屏2020-12-11 下午2.06.44.png
    • 此时可以看到 依旧为NSStackBlock 地址指针并没有变化

    打入objc_retainBlock的全局断点 并继续读取


    截屏2020-12-11 下午2.08.42.png
    • 可以看到依旧没有变化

    按住 ctrl + 鼠标点击 向下箭头 继续向下走


    截屏2020-12-11 下午2.12.20.png
    • 此时可以看到清晰的它调用 libobjc库的 objc_retainBlock方法
    • 此时 依旧没有变化

    继续跟进跳转


    截屏2020-12-11 下午2.14.45.png
    • 发现太长了 那么这里我们只需要断到其 返回值
    截屏2020-12-11 下午2.16.19.png
    • 此时此刻 它发生了变化。变成了 NSMallocBlock
    • 这也就很清晰的看到了block是什么时候从栈block变为堆的。

    我们分析了block是如何确定最终类型的,那还是不了解block是如何捕获外界变量的,为什么__block修饰后 数据会同步呢? 下面我继续分析 底层

    先看图


    __block clang.jpg
    • 首先经过这两种类型的__block我们发现 不同的地方就是修饰指针类型的对象在byref包装结构体中会多出两个函数。
    • 共同地方是经过__block修饰后在block_impl中的desc结构体会多出两个函数。
      看到这里我们也许就更加明白了,还记的blockLayout结构体中desc吗?它的desc有可选的拓展结构体,是根据 blockLayout里的flags&上 枚举来确定是否拥有,在这里用__block修饰之后,它多出的这两个函数正好和descriptor_2一一对应。
    • 他们底层调用的同属 _Block_object_assign 和 _Block_object_dispose函数

    源码搜索 _Block_object_assign

    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];
            ********/
            // objc 指针地址 weakSelf (self)
                // arc
            _Block_retain_object(object);
                // 持有
            *dest = object;
            break;
    
          case BLOCK_FIELD_IS_BLOCK:
            /*******
            void (^object)(void) = ...;
            [^{ object; } copy];
            ********/
                
                // block 被一个 block 捕获
    
            *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 = _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;
            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;
            break;
    
          default:
            break;
        }
    }
    
    
    • 如果是普通对象,交给系统arc处理,并拷贝对象指针,引用计数+1 ,外界变量不能释放。
    • 如果是block类型的变量,又会回到_Block_copy操作,将block从栈 拷贝到堆区。
    • 如果是__block修饰的变量,调用_Block_byref_copy函数,进行内存拷贝及处理。

    查看 枚举 值

     
    // 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
        //普通对象,即没有其他的引用类型
        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_byref_copy

    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 stack
            copy->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) {
                // 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 heap
        else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
            latching_incr_int(&src->forwarding->flags);
        }
        
        return src->forwarding;
    }
    
    • 可以看到被__block包装的变量,真实的类型为Block_byref结构体。
    • 将栈上的Block_byref 结构体拷贝到堆上,根据大小申请内存空间--> 设置isa为 Null ->设置flags信息
      -> 设置堆上Block_byref结构体的forwarding指针指向 为 自己->更改栈上Block_byref结构体的forwarding 指针指向为堆上的Block_byref ->设置堆byref的size大小 为 栈上的byref的size大小。
    • 判断如果有copy dispose,(这里我们在上面说过,__block修饰的指针类型,比基本数据类型在包装的结构体中会多出来两个函数,此时和这里是一一对应的),通过类似上面获取desc2 和desc3的方式,这里是偏移一个Block_byref 大小 拿到 src2也就是包含copy和dispose成员变量的Block_byref_2结构体,来获取 copy和dispose 函数并将其拷贝到堆中。 判断如果有 layout成员变量,与获取src2一样的效果,这里是偏移一个Block_byref_2的大小来获取src3 并将layout变量拷贝到堆上,也就是堆上Block_byref_3 的变量layout 指向栈中layout。通过调用 byref_keep来实现响应,它就对应外部的__Block_byref_id_object_copy

    我们看一下 Block_byref 结构体

    struct Block_byref {
        void *isa;
        struct Block_byref *forwarding;
        volatile int32_t flags; // contains ref count
        uint32_t size;
    };
    
    struct Block_byref_2 {
        // requires BLOCK_BYREF_HAS_COPY_DISPOSE
        BlockByrefKeepFunction byref_keep; // 结构体 __block  对象
        BlockByrefDestroyFunction byref_destroy;
    };
    
    struct Block_byref_3 {
        // requires BLOCK_BYREF_LAYOUT_EXTENDED
        const char *layout;
    };
    
    

    在看一下 byref中的flags的枚举

    // Values for Block_byref->flags to describe __block variables
    enum {
        // Byref refcount must use the same bits as Block_layout's refcount.
        // BLOCK_DEALLOCATING =      (0x0001),  // runtime
        // BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime
    
        BLOCK_BYREF_LAYOUT_MASK =       (0xf << 28), // compiler
        BLOCK_BYREF_LAYOUT_EXTENDED =   (  1 << 28), // compiler
        BLOCK_BYREF_LAYOUT_NON_OBJECT = (  2 << 28), // compiler
        BLOCK_BYREF_LAYOUT_STRONG =     (  3 << 28), // compiler
        BLOCK_BYREF_LAYOUT_WEAK =       (  4 << 28), // compiler
        BLOCK_BYREF_LAYOUT_UNRETAINED = (  5 << 28), // compiler
    
        BLOCK_BYREF_IS_GC =             (  1 << 27), // runtime
    
        BLOCK_BYREF_HAS_COPY_DISPOSE =  (  1 << 25), // compiler
        BLOCK_BYREF_NEEDS_FREE =        (  1 << 24), // runtime
    };
    
    
    • 此时可以看出 和Block_copy中的处理方式非常的相似

    在_Block_byref_copy中我们看到src2->byref_keep,其实就是调用外部的__Block_byref_id_object_copy_131,为什么?
    这里我们 看 Block_byref_2 中两个函数 ,clang编译器中的两个函数

    struct Block_byref_2 {
        // requires BLOCK_BYREF_HAS_COPY_DISPOSE
        BlockByrefKeepFunction byref_keep; // 结构体 __block  对象
        BlockByrefDestroyFunction byref_destroy;
    };
    
    
    截屏2020-12-14 上午11.24.26.png 截屏2020-12-14 上午11.22.55.png
    • __Block_byref_id_object_copy_131入参里面,有一个内存平移40,

    原因


    截屏2020-12-14 上午11.31.57.png
    • 因为 内存偏移 40才能取到 NSstring*__strong str

    而131 = 128 +3,其中128表示BLOCK_BYREF_CALLER --> 代表__block变量有copy/dispose的内存管理辅助函数

    截屏2020-12-14 上午11.35.20.png

    我们这里示例的对象类型为NSString,就表示上述枚举中这个 BLOCK_FIELD_IS_OBJECT,也就是继承NSObjcet类型的 id类型的 为3,然后和copy函数拼接起来就是 __Block_byref_id_object_copy_131

    所以在_Block_byref_copy 中以下标红出就相当于 __Block_byref_id_object_copy_131的调用


    截屏2020-12-14 上午11.39.39.png

    而这里的调用又会触发 _Block_object_assign

    截屏2020-12-14 上午11.45.54.png

    总结

    详细总结:

    Block真正的底层是Block_layout 对象,clang编译器 会根据捕获类型,来动态的改变,及生成对应的数据结构。如用__block修饰后的对象,clang编译器会将其封装为一个byref的结构体对象,此结构体对象在底层真正的类型为 Block_byref 结构体。
    在运行时 会调用Block_copy 函数 通过 block_layout对象中的flags标记 判断当前block的类型及状态。如果是需要释放的 那么 只操作引用计数并返回,如果是全局block那么直接返回,如果是栈区的block, 开辟内存空间 ,设置属性为堆区的标识及一些设置。其中最具代表性的属性为 desc ,在默认情况下block的描述desc只有一个,当被__block修饰之后 ,clang编译器会在desc结构体中多出两个函数copy/dispose 底层会根据 block的flags 标识 来判断是否拥有 desc2 或者 desc3 的block的拓展信息,如判断拥有 copy/dispose 函数,那么会执行copy函数此时会调用Block_object_assign函数 此函数中同样的会判断当前捕获的是什么类型,进行不同的处理, 此时是__block修饰的变量也就byref结构体 将会掉起 _Block_byref_copy 函数,此函数正是对byref结构体 从栈中copy到堆中的操作, 类似block的copy。首先开辟内存,设置 属性为堆区的标识及一些设置,这里重要的操作为,将堆区的forwarding指针 指向 堆区的Block_byref自己.将栈区的forwarding指针指向更改为堆区的Block_byref结构体。并设置 栈区的大小。同样根据栈区的byref标识flags判断是否支持 copy/和dispose函数,如果支持,通过指针平移获取栈区堆区的 Block_byref2 拓展结构体, 从栈区的这两个函数指针赋值 堆区的 Block_byref2 中。再此判断中还判断了是否支持layout拓展,如支持 同样通过指针平移获取栈区堆区的 Block_byref3拓展结构体,从栈区的这个函数指针赋值 堆区的 Block_byref3 中.
    如支持copy/dispose 函数 那么将再次发起 Block_object_assign函数调用,此时进行的是通过Block_byref结构体偏移获取被修饰的指针变量进行 指针copy 也就是引用计数+1

    非太详细:

    也就是 __block修饰的基本数据类型会进行 二次copy 一个是block的copy 一个是byref结构体的copy 都是从 栈中 copy到堆中。

    如果修饰的是指针类型,那么会进行三次 copy,前两次和上面一样,最后一次 会对修饰的原始指针,进行 指针copy引用计数+1.

    相关文章

      网友评论

          本文标题:block详细了解及底层探索

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