美文网首页
Block 原理面试(1)

Block 原理面试(1)

作者: FY_Chao | 来源:发表于2020-02-28 22:45 被阅读0次

    玖:Block 原理面试(1)

    • block的原理是怎样的?本质是什么?

    答:Block 的本质是一个封装了函数及其调用环境的 Objective-C 对象。原理详细见「Block 使用及结构」

    • block的属性修饰词为什么是copy?使用block有哪些使用注意?

    答: MRC 下 block 如果没有 copy 到堆上,值捕获不会对外部变量引用。 虽然 ARC 环境 strong 也可以修饰 Block,那是因为编译器会对 strong 修饰的 block 也会进行一次 copy 操作。为什么用 copy 修饰算是历史习惯问题,推荐不管 ARC、MRC 使用 copy 修饰 。使用注意:循环引用问题

    Tip:本文中以下代码均为 ARC 环境,除非特别注明 MRC。

    Block 使用及结构

    来看一段简单的 Block 的代码:

    // main.m
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            void(^block)(void) =  ^{
                NSLog(@"hello world");
            };        
            block();
        }
        return 0;
    }
    

    然后通过 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m 查看编译后的 C++ 代码。

    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
    
            void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    
            ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
        }
        return 0;
    }
    

    可以看到 block 在编译之后转换成了__main_block_impl_0结构体,结构体的包含的成员如下:

    struct __main_block_impl_0 {
      
      // 相当于copy 了整个struct __block_impl impl
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
      // 相当于copy 了整个struct __block_impl impl
        
      // Des 指针(描述 block 的大小    )
      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;
      }
    };
    

    [图片上传失败...(image-f96567-1582901096408)]

    __main_block_impl_0 结构体和对象结构类似,首个成员是 isa 指针,指向类对象,由此可以推断 block 可能也是 OC 对象(在下文「Block 类型」中详细说明)。

    此外 __main_block_impl_0FuncPtr 函数指针指向了封装 block 代码块的函数 __main_block_func_0:

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
                NSLog((NSString *)&__NSConstantStringImpl__var_folders_s5_1zc18cl97tn280nn_vywbl1m0000gp_T_main_23bff8_mi_0);
    }     
    

    一切就绪之后在main函数中开始执行block。

    int main(int argc, const char * argv[]) {
            // __AtAutoreleasePool 后面的文章在做讲解
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
             // 去除强制转换后简化的代码
            void(*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA));
            block->FuncPtr(block);
        }
        return 0;
    }
    

    block 结构体小结:
    [图片上传失败...(image-a24220-1582901096408)]

    其中copydispose 两个函数下文「对象类型的值捕获」会提到。

    Block 值捕获(基本数据类型)

    简单的带参数 Block (不会进行值捕获)

        void(^block)(int,int) =  ^(int a, int b){
            NSLog(@"a = %d, b = %d",a,b);
        };
        block(20,20);
    

    带参数的 block, 在编译之后__main_block_impl_0__main_block_desc_0结构并未发生变化。只有__main_block_func_0在定义和使用中新增了连个 a, b 参数。这种 block 并不涉及到值捕获。

    static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_s5_1zc18cl97tn280nn_vywbl1m0000gp_T_main_aec4c2_mi_0,a,b);
    }
    
    void(*block)(void) = &__main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DATA));
    block->FuncPtr(block,20,20);
    
    

    局部变量捕获

    捕获auto变量

    简单的 auto 变量地址捕获:

          // 局部变量默认 auto 修饰
         int age = 10;  // 相当于 auto int age = 10;
         void(^block)(void) =  ^{
             NSLog(@"age is %d",age);
         };
         age = 20;
         block();
    // 输出 
    age is 10
    

    如果在 block 中访问了 auto 变量, block 的结构体会发生什么变化呢:

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int age;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    在上面的__main_block_impl_0结构体中新增加一个 int age;成员。__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0): age(_age)构造方法也有了一个 _age参数 函数将 _age 赋值给了结构体的 age 成员属于值传递。

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      int age = __cself->age; // bound by copy
    
      NSLog((NSString *)&__NSConstantStringImpl__var_folders_s5_1zc18cl97tn280nn_vywbl1m0000gp_T_main_232207_mi_0,age);
    }
    

    在执行 block 中的代码块函数时,__main_block_impl_0中的 age 是值传递与局部变量 age 无关,所以即使外部的 age 变量修改了值。也是不会影响 block 中早已捕获的 age。

    捕获static变量

    block 捕获静态变量

        static int age = 10;
        void(^block)(void) =  ^{
            NSLog(@"age is %d",age);
        };
        age = 20;
        block();
        
    // 输出 
    age is 20
    

    如果 block 捕获的是静态变量, block 的结构体又会发生什么变化?经过 clang 编译之后:

          static int age = 10;
            void(*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &age));
            age = 20;
            block->FuncPtr(block);
    
    

    和之前 auto 变量比较,static 传递的参数是 age的地址属于地址传递,__main_block_impl_0 的成员 int *age 存放的是 age 的地址,访问的是同一块内存,所以 age 在外部更改之后,block 中的 age 指向的值也会变动。

    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int *age;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_age, int flags=0) : age(_age) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    局部变量捕获 auto 和 static 的区别

    • auto 变量会在作用域之后销毁,所以 block 会将 age 进行值传递,并存放__main_block_impl_0成员 age 中,用于以后可以随时访问。
    • static 的变量在初始化后会一直存放内存中,所以我们可以通过地址直接访问,不用担心变量作用域的问题,block 结构体的构造方法传递的是静态变量 age 的地址。

    全局变量

    static int age = 10;
    int height = 30;
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            void(^block)(void) =  ^{
                NSLog(@"age is %d,height is %d",age,height);
            };
            age = 20;
            height = 40;
            block();
        }
        return 0;
    }
    

    经过 clang 编译之后:

    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 中的代码块需要访问全局变量时,可以直接访问, block 没有必要在进行值捕获。

    // 直接访问全局变量 和 全局静态变量
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_s5_1zc18cl97tn280nn_vywbl1m0000gp_T_main_d25360_mi_0,age,height);
    }
    

    Block 类型

    在前面的 Block 结构体中都存在一个 isa 指针,且在构造函数的时候赋值 &_NSConcreteStackBlock。所以可以猜测认为 block 其实也是对象的一种,
    尝试对 block 调用 class 方法来看看会有什么输出:

        Class cls = [block class];
        while (cls) {
            NSLog(@"%@",cls);
            cls = [cls superclass];
        }
    
    // 依次输出:
    __NSGlobalBlock__
    __NSGlobalBlock
    NSBlock
    NSObject
    
    

    可以看出来 block 确实是对象且主要的 block 类型(都是继承自NSBlock)有以下三种:

    • __NSGlobalBlock__( _NSConcreteGlobalBlock )存放在 数据段
    • __NSStackBlock__( _NSConcreteStackBlock ) 存放在
    • __NSMallocBlock__( _NSConcreteMallocBlock )存放在

    block 是属于哪一种类型总结下来可以用下面的图片表示:

    [图片上传失败...(image-5e405e-1582901096408)]

            // ARC 下赋值给 __Strong(默认)的 变量时会自动调用 copy方法,将 block copy到堆上,无法准确查看 block 类型
            // 下面代码为 MRC 环境
    
            // __NSGlobalBlock__
            void(^block1)(void) =  ^{
                NSLog(@"hello world");
            };
            
            // __NSStackBlock__
            void(^block2)(void) =  ^{
                NSLog(@"hello age:%d",age);
            };
            // __NSMallocBlock__
            void(^block3)(void) = [block2 copy];
            NSLog(@"block1:%@,block2:%@,block3:%@",block1,block2,block3);
           // release 省略下...
           
            // 输出:
            block1:<__NSGlobalBlock__: 0x1000010a8>,
            block2:<__NSStackBlock__: 0x7ffeefbff480>,
            block3:<__NSMallocBlock__: 0x100638080>
    

    补充: ARC 环境下下列操作会自动 block 进行 copy 操作:

    • block 作为方法的返回值
    • 将 block 赋值给 __strong 指针时
    • block 作为Cocoa API中方法名含有usingBlock的方法参数时
    • block 作为GCD API的方法参数时

    Block 值捕获(对象类型)

    前面提到的值捕获都是基本数据类型,如果在 block 捕获的值是对象类型的话, block的结构体又会发生什么变化呢?

        Person *p = [Person new];
        p.name = @"hello block!";
        void(^block)(void) = ^{
            NSLog(@"--- %@",p.name);
        };
        block();
    

    将上面的代码 clang 编译之后:

    [图片上传失败...(image-cc8f86-1582901096408)]

    对比之前捕获的普通 auto 变量,可以在图中看到 block 捕获的对象变量 Person *p时在 desc中新增了两个函数的指针:

     void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
     void (*dispose)(struct __main_block_impl_0*);
    

    在 block 执行构造函数时,会对赋值两个函数的地址。

    _Block_object_assign函数会在 block 进行一次 copy 操作的时候被调用。

    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
        _Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
    }
    

    _Block_object_assign函数会根据 auto 变量的修饰符(__strong(默认)__weak__unsafe_unretained)做出相应的操作,block 结构体中的 Person *p 对外部的 auto 变量形成强引用(strong)或者弱引用(weak)。

    如果block从堆上移除时,会调用 block 内部的_Block_object_dispose函数。

    static void __main_block_dispose_0(struct __main_block_impl_0*src) {
        _Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
    }
    

    _Block_object_dispose函数会对结构体中的 Person *p 进行 release 操作。

    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_FIELD_IS_BLOCK    =  7,  /* a block variable */
        BLOCK_FIELD_IS_BYREF    =  8,  /* the on stack structure holding the __block variable */
        BLOCK_FIELD_IS_WEAK     = 16,  /* declared __weak, only used in byref copy helpers */
        BLOCK_BYREF_CALLER      = 128  /* called from __block (byref) copy/dispose support routines. */
    };
    

    补充:

    • 如果 block 如果在栈上,自身的生命周期都不确定,所以无法对外部变量进行引用。当 block 是__NSStackBlock__类型是不会对 auto 变量进行强引用。

    • __weak 的作用:

       __weak Person *weakPerson = p;
       void(^block)(void) = ^{
         NSLog(@"--- %@",weakPerson.name);
       };
       block();   
    

    使用xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 main.mclang 编译后__main_block_impl_0区别在于 weakPerson是弱引用:

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      Person *__weak weakPerson;
    }
    
    • block 的属性修饰

    在 MRC 环境下:

    @property (copy, nonatomic) void (^block)(void);
    

    在 ARC 环境下block属性的可以用 strong、copy 修饰,ARC 环境下会默认给赋值 strong 的block进行一次 copy 操作。但一般推荐使用 copy 修饰。算是代码习惯。

    @property (strong, nonatomic) void (^block)(void);
    @property (copy, nonatomic) void (^block)(void);
    

    文章首发:由面试题来了解iOS底层原理

    相关文章

      网友评论

          本文标题:Block 原理面试(1)

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