Block面试题(原理, 属性修饰词为什么用copy,使用的时候有哪些注意点)

定义

Block是一个里面存储了指向定义block时的代码块的函数指针,以及block外部上下文变量信息的结构体,简单说就是:带有自动变量的匿名函数

Block对象内存相关

iOS内存分布一般为:栈区、堆区、全局区、常量区、代码区.其实Block也是一个Objective-C的对象,常见的有以下三种block

  • NSMallocBlock : 存放在堆区的Block
  • NSStackBlock : 存放在栈区的Block
  • NSGlobalBlock: 存放在全局区的Block

通过代码实验(声明 strong、copy、weak 修饰的 Block,分别引用全局变量、全局静态变量、局部静态变量、普通外部变量) ,得出初步的结论:

  1. Block内部没有引用外部变量,Block在全局区,属于GlobalBlock
  2. Block 内部有引用外部变量
    a. 引用全局变量、全局静态变量、局部静态变量 : Block在全局区,属于GlobalBlock

b. 引用普通的外部变量,用copy、strong修饰的Block就放在堆区,属于是MallocBlock.用weak修饰的Block存放在栈区.属于StackBlock

注意:Block引用普通外部变量,都是在栈区创建的,只是用strong、copy修饰的Block会把它从栈区拷贝到堆区一份(栈区太小了2M),尔weak修饰的Block不会.

通过上面的可以知道,在ARC中,用strong、copy修饰的Block,会从栈区拷贝到堆区,所以ARC中,用strong、copy修饰Block效果是一样的.

Block源码分析

通过clang命令将Objective-C代码转成C++代码,可以了解其底层机制,有助于我们更深刻的认识其实现原理.下面是clang相关命令

//1.最简单的命令:
clang -rewrite-objc mian.m

//2.但是如果遇到 main.m:9:9: fatal error: 'UIKit/UIKit.h' file not found 类似的错误需要我们指定下框架
xcrun -sdk iphonesimulator11.4 clang -S -rewrite-objc -fobjc-arc -fobjc-runtime=ios-11.4 main.m

//3.展示 SDK 版本命令
xcodebuild -showsdks

1.下载Block源码:
https://opensource.apple.com/source/libclosure/libclosure-65/

  1. 然后将源码中缺少的库添加进入工程,具体操作可以参考这篇 Blog:
    https://blog.csdn.net/WOTors/article/details/54426316
    3.通过上面两个步骤,我们就有一个包含 Block 源码的工程,然后可以编写 Block 代码,去断点观察 Block 具体的执行过程。
    配置工程还是比较麻烦的,这里我上传了一份:BlockSourceCode
    https://github.com/pengxuyuan/PXYFMWK/tree/master/BlockSourceCode

简单分析Block C++源码

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

static struct __block_desc_0 {
    size_t reserved;
    size_t Block_size;
} _block_desc_0_DATA = { 0, sizeof(struct __block_desc_0)};

struct _block_impl_0 {

    struct __block_impl impl;
    struct __block_desc_0* Desc;
    int i; // 这个是引用外部变量 i
    _block_impl_0(void *fp, struct __block_desc_0 *desc, int _i, int flags=0) :i(_i){

        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

基本可以得出几点结论:
1.结构体中有isa指针,证明Block也是一个对象
2.Block底层是用结构体实现的,结构体 _block_impl_0 包含了 __block_impl 结构体和__block_desc_0结构体(作用后续补充)
3.__block_impl 结构体中的FuncPtr函数指针,指向的就是我们的Block的具体实现.真正调用Block就是利用函数指针去调用的.
4.为什么能访问到外部变量就是因为将外部变量复制到了结构体中(int _i 就是外部变量),即自动变量回作为成员变量追加到Block结构体中.

分析具有__block修饰外部变量的Block源码

我们知道Block截获外部变量是将外部变量作为成员变量追加到Block结构体中国,但是匿名函数存在作用域的问题,这个就是为什么我们不能再Block内部去修改普通外部变量的原因.所以就出现__block修饰符来解决这个问题.

下面我们看下__block修饰的变量转换成C++代码的样子

//Objective-C 代码
 - (void)blockDataBlockFunction {
 __block int a = 100;  ///在栈区
 void (^blockDataBlock)(void) = ^{
 a = 1000;
 NSLog(@"%d", a);
 };  ///在堆区
 blockDataBlock();
 }

//C++ 代码
struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

struct __BlockStructureViewController__blockDataBlockFunction_block_impl_0 {
  struct __block_impl impl;
  struct __BlockStructureViewController__blockDataBlockFunction_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref
};

具有__block修饰的变量,会生成一个 Block_byref_a_0结构体来表示外部变量,然后再追加到Block的结构体中,这里生成Block_byref_a_0这个结构体的原因有两个:一个是抽象出一个结构体,可以让多个Block同事引用这个外部变量:两一个是好管理:,因为Block_byref_a_0中有个非常重要的成员变量forwarding指针,这个指针非常重要(指向Block_byref_a_0结构体),这里是保证当我们将Block从栈区拷贝到堆区中,修改的变量是同一份.

Block是如何解决存储域的问题

首先我们知道Block底层是结构体,Block会转换成block结构体,__block会转换成__blcok结构体
然后block没有截获外部变量、截获全局变量的都属于是全局区的block,即GlobalBlock:其余的都是栈区的Block.
为了解决作用域的问题,Block提供了copy函数,将Block从栈复制到堆上,在MRC环境下需要我们自己调用Block_copy函数,这里就是为什么MRC下,我们为什么要用copy来修饰Block的原因.
在ARC环境下,编译器会尽可能的给我们自动添加copy的操作,这里为什么说尽量呢,因为有些情况编译器无法判断的时候,就不会给我们添加copy操作,这里就需要我们自己主动调用copy方法.

__block 变量的存储域

Block从栈复制到堆上,__block修饰的变量也会从栈复制到堆上;为了结构体__block变量无论在栈上还是在堆上,都可以正确的访问变量,我们需要forwarding指针
在Block从栈复制到堆的时候,原来栈上结构体的forwarding指针,会改变指向,直接指向堆上的结构体,这样就可以保证之后我们都是访问同一个结构体中的变量,这里就是问什么__block修饰的变量,在block内部中可以修饰的原因了.

Block截获对象需要管理对象的生命周期

我们知道Block引用外部变量会将其追加到结构体中,但是编译器是无法判断C语言结构体的初始化和废弃的,因此__block-desc_0会增加成员变量copy和dispose;以及block_copy、block_dispose函数.用来Block从栈复制到堆、堆上的Block废弃的时候分别调用.

Block会出现循环引用

对于Block循环引用算是经典问题了,当A持有B,B持有A的时候就会出现循环引用.Block对于外部比那两都会追加到结构体中,所以在实现Block时候需要注意这个问题.
ARC环境一般我们用__weak来打破,MRC环境下的话,我们可以使用__block来打破循环引用.

Block面试题

  • 下面代码在ARC和MRC环境下运行情况
void exampleA() {
  char a = 'A';
  ^{
    printf("%cn", a);
  }();
}
exampleA();

答: 首先这个Block引用了普通的外部变量,所以这个Block是在栈上创建的.Block是在exampleA()函数内创建的.然后创建完马上调用了,这个时候 exampleA() 并没有执行完,所以这个栈Block 是存在的,不会被 pop 出战.所以在MRC和ARC 环境下都能正确编译运行.

  • 下面代码在MRC环境和ARC环境下运行的情况
void exampleB_addBlockToArray(NSMutableArray *array) {
  char b = 'B';
  [array addObject:^{
    printf("%cn", b);
  }];
}

void exampleB() {
  NSMutableArray *array = [NSMutableArray array];
  exampleB_addBlockToArray(array);
  void (^block)() = [array objectAtIndex:0];
  block();
}
exampleB();

答: 这个跟第一题的区别就是将Block的创建放到一个函数中去.同理分析exampleB_addBlockToArray中创建的Block也是引用了普通的外部变量,Bloock创建在栈上.
MRC 环境上,调用exampleB_addBlockToArray 函数,会创建一个栈 Block 存放到数组中去,然后 exampleB_addBlockToArray 函数结束, Block 被pop 出栈. 这个时候再去调用Block , Block 已经被释放了,所以出现异常,不能正确执行.
ARC 环境下,在 NSMutableArray 的 addObject 方法中,编译器会自动执行 Copy 的操作,将Block 从栈拷贝到堆, 所以ARC 环境没问题.
修改方案

// 主动调用 copy 方法,将 Block 从栈拷贝到堆中,Block_copy(<#...#>)
[array addObject:[^{
    printf("%cn", b);
} copy]];
  • 下面代码在MRC 和 ARC 环境下会出现什么问题
void exampleC_addBlockToArray(NSMutableArray *array) {
  [array addObject:^{
    printf("Cn");
  }];
}

void exampleC() {
  NSMutableArray *array = [NSMutableArray array];
  exampleC_addBlockToArray(array);
  void (^block)() = [array objectAtIndex:0];
  block();
}
exampleC();

答:exampleC_addBlockToArray 中的 Block 并没有引用外部变量,所以 Block 是创建在全局区的,是一个 GlobalBlock,生命周期是跟随着程序的,故 MRC、ARC 环境下都可以正确运行。

  • 下面代码在MRC 和 ARC 环境下会出现什么问题
typedef void (^dBlock)();
dBlock exampleD_getBlock() {
  char d = 'D';
  return ^{
    printf("%cn", d);
  };
}
void exampleD() {
  exampleD_getBlock()();
}
exampleD();

答:这题跟第二题差不多,区别在于这里是将 Block 作为函数返回值了;一样栈区 Block 在 exampleD_getBlock 函数执行完就会释放,MRC 环境下会调用异常,但是这里编译器能检查到这种情况,这里实际效果是编译不通过。
在 ARC 环境下,Block 作为函数返回值,会自动调用 Copy 方法,将 Block 从栈复制到堆上(StackBlock -> MallocBlock),故 ARC 环境下可以正确运行。

  • 下面代码在 MRC 环境 和 ARC 环境运行的情况
typedef void (^eBlock)();
eBlock exampleE_getBlock() {
  char e = 'E';
  void (^block)() = ^{
    printf("%cn", e);
  };
  return block;
}
void exampleE() {
  eBlock block = exampleE_getBlock();
  block()
}
exampleE();

答:这题跟第四题是一样的,这里在 MRC 环境下,可以编译通过,但是调用异常;ARC 环境下可以正确执行。

  • ARC 环境下输出的结果
__block NSString *key = @"AAA";

    objc_setAssociatedObject(self, &key, @1, OBJC_ASSOCIATION_ASSIGN);
    id a = objc_getAssociatedObject(self, &key);

    void (^block)(void) = ^ {
        objc_setAssociatedObject(self, &key, @2, OBJC_ASSOCIATION_ASSIGN);
    };

    id m = objc_getAssociatedObject(self, &key);
    block();
    id n = objc_getAssociatedObject(self, &key);
    objc_setAssociatedObject(self, &key, @3, OBJC_ASSOCIATION_ASSIGN);
    id p = objc_getAssociatedObject(self, &key);
    NSLog(@"%@ --- %@ --- %@ --- %@",a,m,n,p);

答:输入结果:1 --- (null) --- 2 --- 3,代码执行过程如下:

1.__block 修饰的 key,创建在栈区,访问变量 key 为:&(结构体->forwarding->key) ,key 在栈区,此时利用栈区地址作为 Key 来存值

2.变量 a 使用栈区地址取值,故 a 的值为 1

3.声明一个 block,引用到了外部变量 key,此时将 block 从栈拷贝堆,访问变量 key 为:&(结构体->forwarding->key) ,key 在堆区

4.变量 m 用堆区地址来取值,故为 null

5.执行 block,用堆区地址将 2 存进去

6.变量 n 用堆区地址来取值,故为 2

7.再用堆区地址将 3 存进去

8.变量 p 用堆区地址来取值,故为 3

  • 有几种方式去调用 Block
void (^block)(void) = ^{
 NSLog(@"block get called");
 };

 //1. blcok()
 block();

 //2. 利用其它方法去执行 block
 [UIView animateWithDuration:0 animations:block];

 //3.
 [[NSBlockOperation blockOperationWithBlock:block] start];

 //4. NSInvocation
 NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@?"];
 NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
 [invocation invokeWithTarget:block];

 //5.DLIntrospection invoke
 [block invoke];

 //6. 指针调用
 void *pBlock = (__bridge void *)block;
 void (*invoke)(void *, ...) = *((void **)pBlock + 2);
 invoke(pBlock);

 //7. 利用 Clang
 __strong void(^cleaner)(void) __attribute ((cleanup(blockCleanUp),unused)) = block;


 //8. 内联一个汇编 完成调用
 asm("callq *0x10(%rax)");

 static void blockCleanUp (__strong void (^*block)(void)) {
 (*block)();
 }
  • 如何通过 Block 实现链式编程风格的代码
    具体可看实现:Block ChainProgramming
    https://github.com/pengxuyuan/PXYFMWK/blob/master/PXYFMWK/PXYFMWK/PXYFMWK/PXYFMWK/Component/PXYChainProgramming/UIView/UIView%2BPXYChainProgramming.m
    具体参考 Masonry , Snapkit

  • Block 为什么用 Copy 修饰
    对于这个问题,得区分 MRC 环境 和 ARC 环境;首先,通过上面小节可知,Block 引用了普通外部变量,都是创建在栈区的;对于分配在栈区的对象,我们很容易会在释放之后继续调用,导致程序奔溃,所以我们使用的时候需要将栈区的对象移到堆区,来延长该对象的生命周期。
    对于 MRC 环境,使用 Copy 修饰 Block,会将栈区的 Block 拷贝到堆区。
    对于 ARC 环境,使用 Strong、Copy 修饰 Block,都会将栈区的 Block 拷贝到堆区。
    所以,Block 不是一定要用 Copy 来修饰的,在 ARC 环境下面 Strong 和 Copy 修饰效果是一样的。

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

推荐阅读更多精彩内容