iOS 面试题·Block 的原理,Block 的属性修饰词为什么用 copy,使用 Block 时有哪些要注意的?...

640?wx_fmt=gif

640?wx_fmt=jpeg

Linux编程 点击右侧关注,免费入门到精通! 640?wx_fmt=jpeg



作者丨彭序猿
https://www.jianshu.com/p/4db3b4f1d522


640?wx_fmt=gif

前言


Block 在平时开发中经常使用,它是 Objective-C 对 闭包 是实现,定义如下:


Block 是一个里面存储了指向定义 block 时的代码块的函数指针,以及block外部上下文变量信息的结构体。

简单来说就是:带有自动变量的匿名函数。


本篇文章不会阐述 Block 的使用语法,有需要了解 Block 语法可以查看文末的参考链接。本文主要通过学习 Block 源代码来了解 Block 实现原理、内存相关知识、以及如何截获外部变量,然后再通过一些常见的 Block 面试题,进一步加深对 Block 的理解。


640?wx_fmt=gifBlock 对象内存相关知识


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 会把它从栈区拷贝到堆区一份,而 weak 修饰的 Block 不会;


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


640?wx_fmt=gifBlock 源代码分析


640?wx_fmt=gif利用 Clang 将 Objective-C 代码转换成 C++ 代码


通过 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


640?wx_fmt=gif通过源码断点调试 Block


上面 clang 命令只是将 Objective-C 代码转换成 C++ 代码,但是有时候我们想进一步了解 Block 整个的执行过程,我们可以通过 Block 底层源码一步一步断点来研究 Block 的执行过程。


1.首先我们可以去官网上面下载 Block 源代码:

https://opensource.apple.com/source/libclosure/libclosure-65/


2.然后将源码中缺少的库添加进入工程,具体操作可以参考这篇 Blog:

https://blog.csdn.net/WOTors/article/details/54426316


通过上面两个步骤,我们就有一个包含 Block 源码的工程,然后可以编写 Block 代码,去断点观察 Block 具体的执行过程。

配置工程还是比较麻烦的,这里我上传了一份:BlockSourceCode

https://github.com/pengxuyuan/PXYFMWK/tree/master/BlockSourceCode


640?wx_fmt=gif分析简单的 Block C++ 源代码


首先我们通过 clang 将 Block Objective-C 代码转换成以下 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 = { 0sizeof(struct __block_desc_0)};

struct _block_impl_0 {

    struct __block_impl impl;
    struct __block_desc_0Desc;
    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 结构体中。


640?wx_fmt=gif分析具有 __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_0Desc;
  __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 从栈拷贝到堆中,修改的变量都是同一份。


forwarding  指针存在的理由,我们可以看 Block 存储域一节。


640?wx_fmt=gifBlock 是如何解决存储域问题


首先我们知道 Block 底层是用结构体,Block 会转换成 block 结构体,__block 会转换成 __block 结构体。


然后 block 没有截获外部变量、截获全局变量的都是属于全局区的 Block,即 GlobalBlock;其余的都是栈区的 Block,即 StackBlock;

对于全局区的 Block,是不存在作用域的问题,但是栈区 Block 不同,在作用域结束后就会 pop 出栈,__block 变量也是在栈区的,同理作用域结束也会 pop 出栈。


为了解决作用域的问题,Block 提供了 Copy 函数,将 Block 从栈复制到堆上,在 MRC 环境下需要我们自己调用 Block_copy  函数,这里就是为什么 MRC 下,我们为什么需要用 copy 来修饰 Block 的原因。


然而在 ARC 环境下,编译器会尽可能给我们自动添加 copy 操作,这里为什么说尽量呢,因为有些情况编译器无法判断的时候,就不会给我们添加 copy 操作,这里就需要我们自己主动调用 copy 方法了。


640?wx_fmt=gif__block  变量的存储域


Block 从栈复制到堆上,__block 修饰的变量也会从栈复制到堆上;为了结构体 __block 变量无论在栈上还是在堆上,都可以正确的访问变量,我们需要 forwarding 指针;


在 Block 从栈复制到堆上的时候,原本栈上结构体的 forwarding 指针,会改变指向,直接指向堆上的结构体。这样子就可以保证之后我们都是访问同一个结构体中的变量,这里就是为什么 __block 修饰的变量,在 Block 内部中可以修改的原因了。


640?wx_fmt=gifBlock 截获对象需要管理对象的生命周期


我们知道 Block 引用外部变量会将其追加到结构体中,但是编译器是无法判断 C 语言结构体的初始化和废弃的,因此在 __block_desc_0 会增加成员变量 copy 和 dispose;以及 block_copy、block_dispose 函数。


用来 Block 从栈复制到堆、堆上的 Block 废弃的时候分别调用。


640?wx_fmt=gifBlock 会出现循环引用


对于 Block 循环引用算是经典问题了,当 A 持有 B,B 又持有 A,这个时候就会出现循环引用。Block 对于外部变量都会追加到结构体中,所以在实现 Block 时候需要注意这个问题。


ARC 环境一般我们用 __weak 来打破,MRC 环境的话,我们可以使用 __block 来打破循环引用。


640?wx_fmt=gifBlock 面试题


640?wx_fmt=gif1. 下面代码在 MRC 环境 和 ARC 环境运行的情况


void exampleA() {
  char a = 'A';
  ^{
    printf("%cn", a);
  }();
}

//调用:exampleA();


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


640?wx_fmt=gif2. 下面代码在 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 也是引用了普通外部变量,Block 创建在栈上。


MRC 环境上,调用 exampleB_addBlockToArray  函数,会创建一个栈 Block 存放到数组中去,然后 exampleB_addBlockToArray  函数结束,Block 被 pop 出栈,这个时候再去调用 Block,Block 已经被释放了,故出现异常,不能正确执行。


ARC 环境下,在 NSMutableArray 的 addObject 方法中,编译器会自动执行 Copy 操作,将 Block 从栈拷贝到堆(StackBlock -> MallocBlock),故在 ARC 环境可以正确执行。


修改方案如下:


// 主动调用 copy 方法,将 Block 从栈拷贝到堆中,Block_copy(<#...#>)
[array addObject:[^{
    printf("%cn", b);
} copy]];


640?wx_fmt=gif3. 下面代码在 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 环境下都可以正确运行。


640?wx_fmt=gif4. 下面代码在 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 环境下可以正确运行。


640?wx_fmt=gif5. 下面代码在 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 环境下可以正确执行。


640?wx_fmt=gif6. 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


640?wx_fmt=gif7. 有几种方式去调用 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)();
 }


640?wx_fmt=gif8. 如何通过 Block 实现链式编程风格的代码


具体可看实现:Block ChainProgramming

https://github.com/pengxuyuan/PXYFMWK/blob/master/PXYFMWK/PXYFMWK/PXYFMWK/PXYFMWK/Component/PXYChainProgramming/UIView/UIView%2BPXYChainProgramming.m


640?wx_fmt=gifBlock 为什么用 Copy 修饰


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


对于 MRC 环境,使用 Copy 修饰 Block,会将栈区的 Block 拷贝到堆区。


对于 ARC 环境,使用 Strong、Copy 修饰 Block,都会将栈区的 Block 拷贝到堆区。


所以,Block 不是一定要用 Copy 来修饰的,在 ARC 环境下面 Strong 和 Copy 修饰效果是一样的。


640?wx_fmt=gif

总结


这里我们用比较浅显的角度分析了 Block,了解了 Block 也是一个对象,有对应的内存分布;同时作为匿名函数,也会存在作用域的问题,也了解了 Block 是如何截获外部变量的。


对于面试题,主要还是要判断作用域的问题,栈区的 Block 是否复制到堆区中。


 推荐↓↓↓ 

640?wx_fmt=png

?16个技术公众号】都在这里!

涵盖:程序员大咖、源码共读、程序员共读、数据结构与算法、黑客技术和网络安全、大数据科技、编程前端、Java、Python、Web编程开发、Android、iOS开发、Linux、数据库研发、幽默程序员等。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值