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

栏目: IOS · 发布时间: 7年前

内容简介:Linux编程点击右侧关注,免费入门到精通!

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

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

Linux编程点击右侧关注,免费入门到精通! iOS 面试题·Block 的原理,Block 的属性修饰词为什么用 copy,使用 Block 时有哪些要注意的?

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

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

前言

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

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

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

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

iOS 面试题·Block 的原理,Block 的属性修饰词为什么用 copy,使用 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 会把它从栈区拷贝到堆区一份,而 weak 修饰的 Block 不会;

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

iOS 面试题·Block 的原理,Block 的属性修饰词为什么用 copy,使用 Block 时有哪些要注意的? Block 源代码分析 iOS 面试题·Block 的原理,Block 的属性修饰词为什么用 copy,使用 Block 时有哪些要注意的? 利用 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

iOS 面试题·Block 的原理,Block 的属性修饰词为什么用 copy,使用 Block 时有哪些要注意的? 通过源码断点调试 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

iOS 面试题·Block 的原理,Block 的属性修饰词为什么用 copy,使用 Block 时有哪些要注意的? 分析简单的 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 = { 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 结构体中。

iOS 面试题·Block 的原理,Block 的属性修饰词为什么用 copy,使用 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 从栈拷贝到堆中,修改的变量都是同一份。

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

iOS 面试题·Block 的原理,Block 的属性修饰词为什么用 copy,使用 Block 时有哪些要注意的? Block 是如何解决存储域问题

首先我们知道 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 方法了。

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

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

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

iOS 面试题·Block 的原理,Block 的属性修饰词为什么用 copy,使用 Block 时有哪些要注意的? Block 截获对象需要管理对象的生命周期

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

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

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

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

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

iOS 面试题·Block 的原理,Block 的属性修饰词为什么用 copy,使用 Block 时有哪些要注意的? Block 面试题 iOS 面试题·Block 的原理,Block 的属性修饰词为什么用 copy,使用 Block 时有哪些要注意的? 1. 下面代码在 MRC 环境 和 ARC 环境运行的情况

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

//调用:exampleA();

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

iOS 面试题·Block 的原理,Block 的属性修饰词为什么用 copy,使用 Block 时有哪些要注意的? 2. 下面代码在 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]];

iOS 面试题·Block 的原理,Block 的属性修饰词为什么用 copy,使用 Block 时有哪些要注意的? 3. 下面代码在 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 环境下都可以正确运行。

iOS 面试题·Block 的原理,Block 的属性修饰词为什么用 copy,使用 Block 时有哪些要注意的? 4. 下面代码在 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 环境下可以正确运行。

iOS 面试题·Block 的原理,Block 的属性修饰词为什么用 copy,使用 Block 时有哪些要注意的? 5. 下面代码在 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 环境下可以正确执行。

iOS 面试题·Block 的原理,Block 的属性修饰词为什么用 copy,使用 Block 时有哪些要注意的? 6. 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

iOS 面试题·Block 的原理,Block 的属性修饰词为什么用 copy,使用 Block 时有哪些要注意的? 7. 有几种方式去调用 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)();
 }

iOS 面试题·Block 的原理,Block 的属性修饰词为什么用 copy,使用 Block 时有哪些要注意的? 8. 如何通过 Block 实现链式编程风格的代码

具体可看实现:Block ChainProgramming

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

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

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

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

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

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

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

总结

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

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

推荐↓↓↓ 

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

长按关注:point_right: 16个技术公众号 】都在这里!

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


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

The Zen of CSS Design

The Zen of CSS Design

Dave Shea、Molly E. Holzschlag / Peachpit Press / 2005-2-27 / USD 44.99

Proving once and for all that standards-compliant design does not equal dull design, this inspiring tome uses examples from the landmark CSS Zen Garden site as the foundation for discussions on how to......一起来看看 《The Zen of CSS Design》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试