前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >编译器 bug 系列(1)

编译器 bug 系列(1)

作者头像
酷酷的哀殿
发布2020-10-26 10:21:06
4850
发布2020-10-26 10:21:06
举报
文章被收录于专栏:酷酷的哀殿酷酷的哀殿

前言

作为客户端开发者,我们每天都在接触编译器带来的便利,避免了手写机器码的麻烦,但是,某些情况下,编译器也会代码很多负面的作用。

本系列文章会记录笔者遇到过相关bug,希望能够给读者带来一些新奇的知识。

ARC 下的 block 内存管理问题

在 ARC 环境下,下面的代码的执行结果是什么?

代码语言:javascript
复制
-(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();
  }
代码语言:javascript
复制
输出  this is block 0:916  后闪退

闪退的原因是:第二个 block对象 的内容被破坏了。

实际上,研究过 block对象 原理的同学很容易就会发现下面的两个信息:

  • 第一个 block对象 会被放到 堆区。
  • 第二个 block对象 会被放到 栈区。
代码语言:javascript
复制
void testBlockArray() {
  int val = 10;
  NSArray *arr = [NSArray arrayWithObjects:^(){NSLog(@"blk0:%d", val);},^(){NSLog(@"blk1:%d", val);}, nil];
  NSLog(@"%@", arr);
  
}
代码语言:javascript
复制
模拟器输出:
(    "<__NSMallocBlock__: 0x600003bb6e80>",    "<__NSStackBlock__: 0x7ffedfdf7c10>")

第一个 block对象 被放到 堆区 的原因

在 ARC 下,retain/release 等方法被禁止手动调用,内存相关的管理主要是通过编译器自动添加合适的调用方法实现。

本例中,第一个 block 参数对应方法签名的 firstObj,类型是 id,因为类型不同,编译器会添加一次隐式类型转换 block对象 -》 id。参考链接[1]

代码语言:javascript
复制
+ (instancetype)arrayWithObjects:(ObjectType)firstObj, ...;

知识点一:在 ARC 下,持有本地变量的 block 类型是 __NSStackBlock__。

知识点二:在 ARC 下,block 类型被转换到 id 类型会导致 block 的复制行为发生,类型变为 __NSMallocBlock__。

第二个 block对象 被放到 栈区 的原因

下面,我们看看编译器是如何处理“block 被当作 Obj-C 的方法参数”行为的。

block对象 被当作 Obj-C 的方法参数进行传递时,对应的处理函数是:Sema::CheckMessageArgumentTypes(参考链接[2])。

代码语言:javascript
复制
 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对象 的生命周期。

代码语言:javascript
复制
        // 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();
       }
     }
   
代码语言:javascript
复制
 /// 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
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-06-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 酷酷的哀殿 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档