编译器 bug 系列(1)
前言
作为客户端开发者,我们每天都在接触编译器带来的便利,避免了手写机器码的麻烦,但是,某些情况下,编译器也会代码很多负面的作用。
本系列文章会记录笔者遇到过相关bug,希望能够给读者带来一些新奇的知识。
ARC 下的 block 内存管理问题
在 ARC 环境下,下面的代码的执行结果是什么?
-(NSArray*) getBlockArray
{
int num = 916;
return [NSArray arrayWithObjects:
^{ NSLog(@"this is block 0:%i", num); },
^{ NSLog(@"this is block 1:%i", num); },
^{ NSLog(@"this is block 2:%i", num); },
nil];
}
- (void) forTest
{
int a = 10;
int b = 20;
}
- (void)test
{
NSArray* blockArr = [self getBlockArray];
[self forTest];
void (^blockObject)(void);
for(blockObject in blockArr){
blockObject();
}
输出 this is block 0:916 后闪退
闪退的原因是:第二个 block对象 的内容被破坏了。
实际上,研究过 block对象 原理的同学很容易就会发现下面的两个信息:
- 第一个 block对象 会被放到 堆区。
- 第二个 block对象 会被放到 栈区。
void testBlockArray() {
int val = 10;
NSArray *arr = [NSArray arrayWithObjects:^(){NSLog(@"blk0:%d", val);},^(){NSLog(@"blk1:%d", val);}, nil];
NSLog(@"%@", arr);
}
模拟器输出:
( "<__NSMallocBlock__: 0x600003bb6e80>", "<__NSStackBlock__: 0x7ffedfdf7c10>")
第一个 block对象 被放到 堆区 的原因
在 ARC 下,retain/release 等方法被禁止手动调用,内存相关的管理主要是通过编译器自动添加合适的调用方法实现。
本例中,第一个 block 参数对应方法签名的 firstObj,类型是 id,因为类型不同,编译器会添加一次隐式类型转换 block对象 -》 id。参考链接[1]
+ (instancetype)arrayWithObjects:(ObjectType)firstObj, ...;
知识点一:在 ARC 下,持有本地变量的 block 类型是 __NSStackBlock__。
知识点二:在 ARC 下,block 类型被转换到 id 类型会导致 block 的复制行为发生,类型变为 __NSMallocBlock__。
第二个 block对象 被放到 栈区 的原因
下面,我们看看编译器是如何处理“block 被当作 Obj-C 的方法参数”行为的。
block对象 被当作 Obj-C 的方法参数进行传递时,对应的处理函数是:Sema::CheckMessageArgumentTypes(参考链接[2])。
bool Sema::CheckMessageArgumentTypes(
const Expr *Receiver, QualType ReceiverType, MultiExprArg Args,
Selector Sel, ArrayRef<SourceLocation> SelectorLocs, ObjCMethodDecl *Method,
bool isClassMessage, bool isSuperMessage, SourceLocation lbrac,
SourceLocation rbrac, SourceRange RecRange, QualType &ReturnType,
ExprValueKind &VK)
该函数中与 block对象生命周期相关的代码主要是:遍历具有名字的参数,提升 block对象 的生命周期。
// If we are type-erasing a block to a block-compatible
// Objective-C pointer type, we may need to extend the lifetime
// of the block object.
if (typeArgs && Args[i]->isRValue() && paramType->isBlockPointerType() &&
Args[i]->getType()->isBlockPointerType() &&
origParamType->isObjCObjectPointerType()) {
ExprResult arg = Args[i];
maybeExtendBlockObject(arg);
Args[i] = arg.get();
}
}
/// Do an explicit extend of the given block pointer if we're in ARC.
void Sema::maybeExtendBlockObject(ExprResult &E) {
assert(E.get()->getType()->isBlockPointerType());
assert(E.get()->isRValue());
// Only do this in an r-value context.
if (!getLangOpts().ObjCAutoRefCount) return;
E = ImplicitCastExpr::Create(Context, E.get()->getType(),
CK_ARCExtendBlockObject, E.get(),
/*base path*/ nullptr, VK_RValue);
Cleanup.setExprNeedsCleanups(true);
}
而本例中,因为第二个 block对象 对应方法签名的省略号,没有实际的名称,导致第二个 block对象 保持默认的 __NSStackBlock__ 类型,最终导致某些场景下因为内存原因出现闪退。
后记:
本文描述的 bug 已经存在多年,建议读者在官方未修复本文描述的 bug 前,添加 copy 调用的方式修复。
参考:
- https://developer.apple.com/documentation/foundation/nsarray/1460145-arraywithobjects
- https://clang.llvm.org/doxygen/SemaExprObjC_8cpp_source.html#l01645
- https://clang.llvm.org/doxygen/SemaExpr_8cpp_source.html#l0698
- https://bugs.llvm.org/show_bug.cgi?id=46399
- Activity之间传递参数
- linux下rsync和tar增量备份梳理
- 重温Delphi之:面向对象
- Android新手之旅(15) Win7下配置遇到的问题
- 重温Delphi之:如何定义一个类
- Android新手之旅(2) 新手问题
- Android新手之旅(2) 新手问题
- Android新手之旅(9) 自定义的折线图
- 2018春节抢票攻略:不仅仅是12306微信小程序启用
- Android新手之旅(9) 自定义的折线图
- Android新手之旅(11) 在现有页面中插入新的view
- Docker容器学习梳理--容器间网络通信设置(Pipework和Open vSwitch)
- 温故而知新:Asp.Net中如何正确使用Session
- Android新手之旅(13) listview中数据重复的问题
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- OpenCV图像读取(imread) 显示(imshow) 保存(imwrite)的冷知识点
- CodeReview实践-Gerrit自动触发JenkinsCI
- OpenCV常用图像拼接方法(一) :直接拼接
- ClickHouse|MergeTree引擎之数据分区
- OpenCV常用图像拼接方法(二) :基于模板匹配拼接
- 为了解决 Prometheus 大内存问题,我竟然强行将 Prometheus Operator 给肢解了。。
- 面试官:webpack原理都不会?
- 算法篇:树之对称二叉树
- 算法篇:树之二叉树的恢复
- 算法篇:树之利用数组处理链表
- 灰子的Go笔记:sync.Map
- 最炫酷的 Kubernetes Dashboard:Octant 迎来重大更新!
- 算法篇:树之树的层次遍历
- mybatis-plus:性能分析插件与性能分析打印
- 《RabbitMQ》什么是死信队列