一、前言

  • 本文重点来研究一下 objc 的 block,并具体来分析一下以下一些面试题目:
    • block 的内部实现,结构体是什么样?
    • block 是类吗?有哪些类型?
    • 一个 int 变量被 __block 修饰与否的区别?block 的变量如何截获?
    • block 在修改 NSMutableArray,需不需要添加 __block?
    • block 怎么进行内存管理?
    • block 可以用 strong 修饰吗?
    • 解决循环引用时,为什么要用 __strong、__weak 修饰?
    • block 发生 copy 时机是什么?
    • block 访问对象类型的 auto 变量时,在 ARC 和 MRC 下有什么区别?
  • 在回答这些问题之前,需要了解一些 block 背景相关的知识,如下:
    • 如何查看 block 的内部实现,也就是说转换成背后真正的 c/c++ 代码的 block 是什么样的?
    • block 的转换格式是什么?
    • block 原理是什么?
    • 关于 block 变量的作用域是什么?

二、Objective-C 转 C++ 的方法

  • 现有一个 TestClass.m 类,其中 block 代码如下:
@interface TestClass ()
@end@implementation TestClass
- (void)testMethods {void (^blockA)(int a) = ^(int a) {NSLog(@"%d",a);};if (blockA) {blockA(1990);}
}
@end
  • 打开终端,cd 到 TestClass.m 所在目录,使用如下命令:
clang -rewrite-objc TestClass.m
  • 就会在当前文件夹内自动生成对应的 TestClass.cpp 文件。如果提示 clang 没有的话,则需要安装,输入如下命令:
   brew install clang-format或者brew link clang-forma然后输入 下面命令 测试是否好使clang-format --help
  • 经过上述转换操作,可以在 TestClass.cpp 中最下面发现如下 C++ 代码:
// @interface TestClass ()
/* @end */// @implementation TestClassstruct __TestClass__testMethods_block_impl_0 {struct __block_impl impl;struct __TestClass__testMethods_block_desc_0* Desc;__TestClass__testMethods_block_impl_0(void *fp, struct __TestClass__testMethods_block_desc_0 *desc, int flags=0) {impl.isa = &_NSConcreteStackBlock;impl.Flags = flags;impl.FuncPtr = fp;Desc = desc;}
};static void __TestClass__testMethods_block_func_0(struct __TestClass__testMethods_block_impl_0 *__cself, int a) {NSLog((NSString *)&__NSConstantStringImpl__var_folders_wx_b8tcry0j24dbhr7zlzjq3v340000gn_T_TestClass_ee18d3_mi_0,a);}static struct __TestClass__testMethods_block_desc_0 {size_t reserved;size_t Block_size;
} __TestClass__testMethods_block_desc_0_DATA = { 0, sizeof(struct __TestClass__testMethods_block_impl_0)};static void _I_TestClass_testMethods(TestClass * self, SEL _cmd) {void (*blockA)(int a) = ((void (*)(int))&__TestClass__testMethods_block_impl_0((void *)__TestClass__testMethods_block_func_0, &__TestClass__testMethods_block_desc_0_DATA));if (blockA) {((void (*)(__block_impl *, int))((__block_impl *)blockA)->FuncPtr)((__block_impl *)blockA, 1990);}
}
  • 通过上述代码,可以发现 block 其实是一个结构体类型,底层实现会根据 __类名__方法名_block_impl_下标 (0 代表这个方法或者这个类中第 0 个 block):
struct __类名__方法名_block_impl_下标

三、关于变量的作用域

  • c 语言的函数中可能使用的参数变量种类:
    • 参数类型
    • 自动变量(局部变量)
    • 静态变量(静态局部变量)
    • 静态全局变量
    • 全局变量
  • 由于存储区域特殊,这其中有三种变量是可以在任何时候以任何状态调用的:
    • 静态变量
    • 静态全局变量
    • 全局变量
  • 而其它两种,则是有各自相应的作用域,超过作用域后,会被销毁。

四、block 的内部实现,结构体是什么样子?

  • block 的内部实现如下:
struct __TestClass__testMethods_block_impl_0 {struct __block_impl impl;                        // 成员变量struct __TestClass__testMethods_block_desc_0* Desc; // desc 结构体声明// 构造函数// fp 函数指针// desc 静态全局变量初始化的 __main_block_desc_ 结构体实例指针// flags block 的负载信息(引用计数和类型信息),按位存储.__TestClass__testMethods_block_impl_0(void *fp, struct __TestClass__testMethods_block_desc_0 *desc, int flags=0) {impl.isa = &_NSConcreteStackBlock;impl.Flags = flags;impl.FuncPtr = fp;Desc = desc;}
};
// 将来被调用的block内部的代码:block值被转换为C的函数代码
// 这里,*__cself 是指向Block的值的指针,也就相当于是Block的值它自己(相当于C++里的this,OC里的self)
// __cself 是指向__TestClass__testMethods_block_impl_0结构体实现的指针
// Block结构体就是__TestClass__testMethods_block_impl_0结构体.Block的值就是通过__TestClass__testMethods_block_impl_0构造出来的
static void __TestClass__testMethods_block_func_0(struct __TestClass__testMethods_block_impl_0 *__cself, int a) {NSLog((NSString *)&__NSConstantStringImpl__var_folders_wx_b8tcry0j24dbhr7zlzjq3v340000gn_T_TestClass_9f58f7_mi_0,a);
}static struct __TestClass__testMethods_block_desc_0 {size_t reserved;size_t Block_size;
} __TestClass__testMethods_block_desc_0_DATA = { 0, sizeof(struct __TestClass__testMethods_block_impl_0)};static void _I_TestClass_testMethods(TestClass * self, SEL _cmd) {void (*blockA)(int a) = ((void (*)(int))&__TestClass__testMethods_block_impl_0((void *)__TestClass__testMethods_block_func_0, &__TestClass__testMethods_block_desc_0_DATA));if (blockA) {((void (*)(__block_impl *, int))((__block_impl *)blockA)->FuncPtr)((__block_impl *)blockA, 1990);}
}
  • 可以看得出来 __TestClass__testMethods_block_impl_0 有 3 个部分组成:
    • impl 函数指针指向 __TestClass__testMethods_block_impl_0:
struct __block_impl {void *isa;int Flags;int Reserved;  // 今后版本升级所需的区域void *FuncPtr; // 函数指针};
  • Desc 指向 __TestClass__testMethods_block_impl_0的Desc 指针,用于描述当前这个 block 的附加信息,包括结构体的大小等信息:
static struct __TestClass__testMethods_block_desc_0 {size_t reserved;   // 今后升级版本所需区域size_t Block_size; // block的大小} __TestClass__testMethods_block_desc_0_DATA = { 0, sizeof(struct __TestClass__testMethods_block_impl_0)};
  • __TestClass__testMethods_block_impl_0() 构造函数,也就是该 block 的具体实现:
__TestClass__testMethods_block_impl_0(void *fp, struct __TestClass__testMethods_block_desc_0 *desc, int flags=0) {  impl.isa = &_NSConcreteStackBlock;  impl.Flags = flags;  impl.FuncPtr = fp;  Desc = desc; }
  • 此结构体中,isa 指针保持这所属类的结构体的实例的指针,struct __TestClass__testMethods_block_impl_0 相当于 Objective-C 类对象的结构体,_NSConcreteStackBlock 相当于 block 的结构体实例,也就是说 block 其实就是 Objective-C 对于闭包的对象实现。

五、block 是类吗?有哪些类型?

  • block 也可以理解为类,因为它有 isa 指针、block.isa 的类型,包括:
    • _NSConcreteGlobalBlock 跟全局变量一样,设置在程序的数据区域(.data)中;
    • _NSConcreteStackBlock 栈上(上文的都是栈上 block);
    • _NSConcreteMallocBlock 堆上。
  • block 的 isa 可以按位运算。

六、一个 int 变量被 __block 修饰与否的区别?block的变量如何截获?

① 被 __block 修饰与否的区别

  • 现有如下示例:
__block int a = 10;
int b = 20;   PrintTwoIntBlock block = ^() {  a -= 10;   printf("%d, %d\n",a,b);
};   block();                // 0 20    a += 20;
b += 30;   printf("%d, %d\n",a,b); // 20 50
block();                // 10 20
  • 通过 __block 修饰 int a,block 体中对这个变量的引用是指针拷贝,它会作为 block 结构体构造参数传入到结构体中且复制这个变量的指针引用,从而达到可以修改变量的作用。
  • int b 没有被 __block 修饰,block 内部对 b 是值 copy,因此在 block 内部修改 b 而不会影响外部 b 的变化。

② block的变量如何截获?

  • 通过如下代码,来观察要一下变量的捕获:
blk_t blk;
{    id array = [NSMutableArray new];   blk = [^(id object){       [array addObject:object];       NSLog(@"array count = %ld",[array count]);  } copy];
}
blk([NSObject new]);
blk([NSObject new]);
blk([NSObject new]);
  • 运行结果:
block_demo[28963:1629127] array count = 1
block_demo[28963:1629127] array count = 2
block_demo[28963:1629127] array count = 3
  • 把上面的代码翻译成 C++:
struct __main_block_impl_0 {  struct __block_impl impl;  struct __main_block_desc_0* Desc;  id array; // 截获的对象  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, id _array, int flags=0) : array(_array) {   impl.isa = &_NSConcreteStackBlock;    impl.Flags = flags;    impl.FuncPtr = fp;    Desc = desc;}
};
  • 在 Objc 中,C 结构体里不能含有被 __strong 修饰的变量,因为编译器不知道应该何时初始化和废弃 C 结构体。但是 Objc 的运行时库能够准确把握 block 从栈复制到堆,以及堆上的 block 被废弃的时机,实现上是通过 __TestClass__testMethods_block_copy_0 函数和 __TestClass__testMethods_block_dispose_0 函数进行:
static void __TestClass__testMethods_block_copy_0(struct __TestClass__testMethods_block_impl_0*dst, struct __TestClass__testMethods_block_impl_0*src) {   _Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static void __TestClass__testMethods_block_dispose_0(struct __TestClass__testMethods_block_impl_0*src) {   _Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
    • _Block_object_assign 相当于 retain 操作,将对象赋值在对象类型的结构体成员变量中;
    • _Block_object_dispose 相当于 release 操作。
  • 这两个函数调用的时机是在什么时候呢?
函数 被调用时机
__TestClass__testMethods_block_copy_0 从栈复制到堆时
__TestClass__testMethods_block_dispose_0 堆上的Block被废弃时

③ 什么时候栈上 block 会被复制到堆呢?

  • 调用 block 的 copy 函数时;
  • block 作为函数返回值返回时;
  • 将 block 赋值给附有 __strong 修饰符 id 类型的类或者 block 类型成员变量时;
  • 方法中含有 usingBlock 的 Cocoa 框架方法或者 GCD 的 API 中传递 block 时。

④ block 什么时候被废弃?

  • 堆上的 block 被释放后,谁都不再持有 block 时调用 dispose 函数;

七、block 在修改 NSMutableArray 需不需要添加 __block?

  • 修改 NSMutableArray 的存储内容的话,是不需要添加 __block 修饰的。
  • 修改 NSMutableArray 对象的本身,那必须添加 __block 修饰。

八、block 怎么进行内存管理?

  • 在上文中的 block 的构造函数 __TestClass__testMethods_block_impl_0 中的 isa 指针指向的是 &_NSConcreteStackBlock,它表示当前的 block 位于栈区中。
block内存操作 存储域/存储位置 copy操作的影响
_NSConcreteGlobalBlock 程序的数据区域 什么也不做
_NSConcreteStackBlock 从栈拷贝到堆
_NSConcreteMallocBlock 引用计数增加
  • 全局 block:_NSConcreteGlobalBlock 的结构体实例设置在程序的数据存储区,所以可以在程序的任意位置通过指针来访问,它的产生条件:
    • 记述全局变量的地方有 block 语法时;
    • block 不截获的自动变量。
    • 以上两个条件只要满足一个就可以产生全局 block。
  • 栈 block:_NSConcreteStackBlock 在生成 block 以后,如果这个 block 不是全局 block,那它就是栈 block,生命周期在其所属的变量作用域内(也就是说如果销毁取决于所属的变量作用域)。如果 block 变量和 __block 变量复制到了堆上以后,则不再会受到变量作用域结束的影响,因为它变成了堆 block。
  • 堆 block:_NSConcreteMallocBlock 将栈 block 复制到堆以后,block 结构体的 isa 成员变量变成_NSConcreteMallocBlock。

九、block 可以用 strong 修饰吗?

  • 在 ARC 中可以,因为在 ARC 环境中的 block 只能在堆内存或全局内存中,因此不涉及到从栈拷贝到堆中的操作。
  • 在 MRC 中不行,因为要有拷贝过程,如果执行 copy 用 strong 的话会 crash,strong 是 ARC 中引入的关键字,如果使用 retain 相当于忽视了 block 的 copy 过程。

十、解决循环引用时,为什么要用 __strong、__weak 修饰 block?

  • 首先 block 捕获变量的时候,结构体构造传入了self,造成了默认的引用关系,因此一般在 block 外部对操作对象会加上 __weak;
  • 在 block 内部使用 __strong 修饰符的对象类型的自动变量,那么当 block 从栈复制到堆的时候,该对象就会被 block 所持有,但是持有的是上面加了 __weak,所以形成了彼消此涨的链条,刚好能解决 block 延迟销毁的时候对外部对象生命周期造成的影响,如果不这样做很容易造成循环引用。

十一、block 发生 copy 时机?

  • 在 ARC 中,编译器将创建在栈中 block 会自动拷贝到堆内存中,而 block 作为方法或函数的参数传递时,编译器不会做 copy 操作。
    • 调用 block 的 copy 函数时;
    • block 作为函数返回值返回时;
    • 将 block 赋值给附有 __strong 修饰符 id 类型的类或者 block 类型成员变量时;
    • 方法中含有 usingBlock 的 Cocoa 框架方法或者 GCD 的 API 中传递 block 时。

十二、block 访问对象类型的 auto 变量时,在 ARC 和 MRC 下有什么区别?

  • 在 ARC 下,栈区创建的 block 会自动 copy 到堆区;而 MRC 下,就不会自动拷贝,需要手动调用 copy 函数。
  • block 的 copy 操作,当 block 从栈区 copy 到堆区的过程中,也会对 block 内部访问的外部变量进行处理,它会调用 block_object_assign 函数对变量进行处理,根据外部变量是 strong 还会 weak 对 block 内部捕获的变量进行引用计数 +1 或 -1,从而达到强引用或弱引用的作用。
  • 因此在 ARC 下,由于 block 被自动 copy 到了堆区,从而对外部的对象进行强引用,如果这个对象同样强引用这个 block,就会形成循环引用。
  • 在 MRC 下,由于访问的外部变量是 auto 修饰,所以这个 block 属于栈区的,如果不对 block 手动进行 copy 操作,在运行完 block 的定义代码段后,block 就会被释放,而由于没有进行 copy 操作,所以这个变量也不会经过 block_object_assign 处理,也就不会对变量强引用。
  • 简而言之,ARC 下会对这个对象强引用,而 MRC 下则不会。

iOS经典面试题之深入分析block相关高频面试题相关推荐

  1. 谈谈对数据库索引的了解—这些就很重要!!附相关高频面试题。

    本质:提高数据库性能的一种特殊文件 基础知识 面试题 一.索引的底层实现原理和优化 二. 三.什么情况下设置了索引但无法使用? 索引的类型有哪些,他们的区别是什么? 基础知识 概念: 索引是一种特殊的 ...

  2. 【面试题】2021年PHP高频面试题汇总

    1.get,post 的区别 1.GET在浏览器回退时是无害的,而POST会再次提交请求. 2.GET产生的URL地址可以被Bookmark,而POST不可以. 3.GET请求会被浏览器主动cache ...

  3. Java面试题库,Java大厂高频面试题解析

    一面 正式批(别看了都是正式批,提前批就没让我面!)一面.面试时间 08-18,19:53 - 21:08,全程1个小时15分钟.涉及内容:项目.网络.数据库.算法题 1. 自我介绍 2. 项目中的有 ...

  4. 计算机网络高频面试题最新版

    在秋招过程中看了大量面经,将常见的计算机网络面试题总结如下,并按照面试中提问的频率做了标注(星数越高,面试中提问频率越高),如有帮到你,可以收藏点赞支持哦. 微信搜索公众号路人zhang,回复面试手册 ...

  5. 2022html css高频面试题

    点击查看Vue高频面试题 点击查看JavaScript高频面试题 HTML+CSS高频面试题 html HTML全局属性有哪些 html新增加的语义化标签 div+css较table布局的的优势 介绍 ...

  6. MySQL高频面试题(最新版)

    MySQL高频面试题,题目后面的星数越高,在面试中越高频 微信搜索公众号路人zhang,回复面试手册,领取更多高频面试题PDF版及更多面试资料. 面试手册在线版: www.mianshi.online ...

  7. MySQL数据库优化高频面试题(最新版)

    MySQL数据库基础知识及优化高频面试题 微信搜索公众号路人zhang,回复面试手册,领取更多高频面试题PDF版及更多面试资料. 推荐阅读: Java基础知识高频面试题最新版 计算机网络高频面试题最新 ...

  8. 2022js高频面试题

    点击查看HTML+CSS高频面试题 点击查看Vue高频面试题 js高频面试题 初级js面试题 给dom对象绑定事件都有那些方法 解释一下什么是事件流 删除事件有哪些方法 事件委托与事件冒泡讲一下 常见 ...

  9. 数据库索引高频面试题(最新版)

    MySQL的索引是面试中的高频题目,将常见的索引面试题目总结了一下,如果有帮到你可以点赞收藏呦. 微信搜索公众号路人zhang,回复面试手册,领取更多高频面试题PDF版及更多面试资料. 面试手册在线版 ...

最新文章

  1. Spring Cloud(二): 注册中心Eureka的使用
  2. 修改Spring boot内置的tomcat端口
  3. android中线程和进程
  4. 32和64位jvm_我应该使用32位还是64位JVM?
  5. 大数据环境下的存储系统构建:挑战、方法和趋势
  6. 警告—系统—srv—2013—无
  7. 运筹作业题:一个正三角形平面,在三个角的部分减去一部分,然后沿着剪开部分折叠起来,使折叠后的三棱台体积最大
  8. 【Twitter】时序图神经网络
  9. 逗号,句号。问号?叹号!顿号、冒号:人名分隔·
  10. rank(),允许并列名次、复制名次自动空缺,结果如12245558……
  11. 记录一次es写入操作
  12. 计算机培训ppt课件,计算机基础操作培训ppt课件.ppt
  13. 用python做归结演绎推理_python基础归结
  14. 【vulhub系列】cve-2018-1273S pring Expression Language 漏洞复现
  15. 男人二十岁后应该学会的习惯
  16. 微信开启指纹支付功能 单账户每日限额5000元
  17. 京东网站页面编写(HTML、CSS、JS),包括京东秒杀的倒计时、轮播图等功能
  18. 医疗人工智能发展趋势及机遇
  19. 树莓派(USB麦克风和麦克风阵列) 录音和播放
  20. linux关于日志文件介绍,Linux下重要日志文件介绍

热门文章

  1. Mysql的两种“排名第几且有可能为空的记录”写法(力扣176)
  2. github删除错误的commit并保留之前的提交
  3. 2018.1.21 数论笔记
  4. Gradle在大型Java项目上的应用
  5. MVC 中 Razor 无限分类的展示
  6. 程序员保值的4个秘密
  7. phpcms下的邮箱设置
  8. JQ 为未来元素添加事件处理器—事件委托
  9. 小知识汇总----不断更新中...
  10. XBMC 最新版本错误