Block 原理面试(1)

玖: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底层原理

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容