IOS开发入门之Block 在 ARC 下的拷贝
白羽 2018-09-07 来源 :网络 阅读 1382 评论 0

摘要:本文将带你了解IOS开发入门之Block 在 ARC 下的拷贝,希望本文对大家学IOS有所帮助。

        本文将带你了解IOS开发入门之Block 在 ARC 下的拷贝,希望本文对大家学IOS有所帮助。


    
        前言
现在有一种说法,是开启arc选项时,已经没有栈上的block了,所以所有的block都不需要copy来拷贝到堆上了。那么这个说法正确与否呢?
结论是这个说法必须是错误的,首先的一点就是arc只是编译器帮助我们给对象自动增加retain,release方法,我们不需要手动的去管理这些成对出现的内存计数方法,其本质上与mrc是一脉相承的,所以arc下必然还是有stack block的。
再次可以简单的写一个例子(就举参考1的要点3):
    1   -(id) getBlockArray{  
    2       int val =10;  
    3       return [NSArray arrayWithObjects:  
    4           ^{NSLog(@"blk0:%d",val);},  
    5           ^{NSLog(@"blk1:%d",val);},nil];  
    6   }  
    7   // Other Method  
    8   id obj = getBlockArray();  
    9   typedef void (^blk_t)(void);  
    10  blk_t blk = (blk_t){obj objectAtIndex:0};  
    11  blk();  
这段代码会异常,但是作者解释的不正确。首先我们可以打印一下这个Array,会发现第一个是NSMallocBlock,第二个是NSStackBlock。所以这段代码说明了arc下也是有stack block的。其次这段代码异常是因为array释放的时候,第二个block是栈上面的,对其释放必然会引发异常。
为什么会这样呢,我们接着往下看。
一、NSArray 的生成
1. + (instancetype)arrayWithObjects:(ObjectType)firstObj, ...
- (void)show
{
    id obj = [self getBlockArray];
    typedef void (^blk_t)(void);
    blk_t blk = (blk_t)[obj objectAtIndex:0];
    blk();
    blk = (blk_t)[obj objectAtIndex:1];
    blk();
}

-(id) getBlockArray{
    int val =10;

    NSArray* a = [NSArray arrayWithObjects:^{NSLog(@"blk0:%d",val);},^{NSLog(@"blk1:%d",val);}, nil];
    
    return a;
}

通过 clang rewrite 看看得到的c++代码,关键地方如下:
static id _I_BlockTest_getBlockArray(BlockTest * self, SEL _cmd) {
    int val =10;
    NSArray* a = ((NSArray *(*)(id, SEL, ObjectType, ...))(void *)objc_msgSend)((id)objc_getClass("NSArray"), sel_registerName("arrayWithObjects:"), (id)((void (*)())&__BlockTest__getBlockArray_block_impl_0((void *)__BlockTest__getBlockArray_block_func_0, &__BlockTest__getBlockArray_block_desc_0_DATA, val)), ((void (*)())&__BlockTest__getBlockArray_block_impl_1((void *)__BlockTest__getBlockArray_block_func_1, &__BlockTest__getBlockArray_block_desc_1_DATA, val)), __null);
    return a;
}
第一个 block 强转成 id 类型,第二个 block 没有。首先 block 在赋值给 id 类型或者 block 类型的成员变量时,block 会拷贝到堆上,所以第一个 block 变成了堆上的 block,但是第二个还是栈上的内存。
整个 getBlockArray 方法在 show 方法中调用,所以用的是同一个 runloop 中带过来的 autoreleasepool,getBlockArray 中的 NSArray* a = [NSArray arrayWithObjects:^{NSLog(@"blk0:%d",val);},^{NSLog(@"blk1:%d",val);}, nil]; 会往这个 autoreleasepool 中添加两个 NSArray (赋值给 a 是添加一次,show 中得到返回值时添加一次),这两个NSArray 不会立即释放,而会在 这个 runloop 结束的时候释放,这个时机会在show的结束设置更外层的调用。而这个时机已经超过了 getBlockArray 的区域,超过这个区域去访问栈内存,所以会crash。
2.一些其他的 NSArray 生成方法说明
+ (instancetype)arrayWithArray:(NSArray<ObjectType> *)array;
效果同上
- (instancetype)initWithArray:(NSArray<ObjectType> *)array;
效果同上
+ (instancetype)arrayWithObject:(ObjectType)anObject;
rewrite后代码参考上面,这个方法的block会转成 id,所以生成 NSArray 中的 block 是堆上的 block。
- (instancetype)initWithArray:(NSArray<ObjectType> *)array copyItems:(BOOL)flag;
不管flag 是 true 还是 false,都会crash
另外 - (void)addObject:(ObjectType)anObject; 也是因为会转成 id,所以添加的都是堆上的 block。
二、AutoReleasePool
现在给 getBlockArray 加上一个autoreleasepool,看看会发生什么

     -(id) getBlockArray{
         int val =10;

         NSArray* a = nil;
         @autoreleasepool {
             a = [NSArray arrayWithObjects:^{NSLog(@"blk0:%d",val);},^{NSLog(@"blk1:%d",val);}, nil];
         }

         return [[NSArray alloc] initWithArray:a copyItems:YES];
     }
     
此时并不会crash,正确的运行了,clang rewrite 一下看看
     static id _I_BlockTest_getBlockArray(BlockTest * self, SEL _cmd) {
         int val =10;
         NSArray* a = __null;
         /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
             a = ((NSArray *(*)(id, SEL, ObjectType, ...))(void *)objc_msgSend)((id)objc_getClass("NSArray"), sel_registerName("arrayWithObjects:"), (id)((void (*)())&__BlockTest__getBlockArray_block_impl_0((void *)__BlockTest__getBlockArray_block_func_0, &__BlockTest__getBlockArray_block_desc_0_DATA, val)), ((void (*)())&__BlockTest__getBlockArray_block_impl_1((void *)__BlockTest__getBlockArray_block_func_1, &__BlockTest__getBlockArray_block_desc_1_DATA, val)), __null);
         }
         return ((NSArray *(*)(id, SEL, NSArray<ObjectType> *, BOOL))(void *)objc_msgSend)((id)((NSArray *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSArray"), sel_registerName("alloc")), sel_registerName("initWithArray:copyItems:"), (NSArray *)a, ((bool)1));

     }
其实并没有多出来什么特殊的处理,我们可以分析分析。
getBlockArray 给 a 赋值的数组,加入到了我们添加的autoreleasepool中,在这个作用域外面,他已经被释放了,其中的栈上的 block 在自己的作用域内释放,没有任何问题。而 a 默认是一个 strong 修饰的变量,在不在使用的时候,编译器会帮我们添加上 release 而释放,所以在函数结束的时候,他也释放了。
另外 copyItem 是 YES,他会往 withArray 的每个 item 都发送 copy 消息,所以函数返回的 NSArray 中每个 item 都是堆上的 block,所以这一次并不会 crash。但是如果 copyItem 是 NO,那么就会出现超过作用域访问栈上 block 的问题,就会 crash 了。
三、字面量
还有一种生成 NSArray 的方式是:
      NSArray* a = @[^{NSLog(@"blk0:%d",val);},^{NSLog(@"blk1:%d",val);}];
看看对应的 rewrite 代码是什么样的:
      static id _I_BlockTest_getBlockArray(BlockTest * self, SEL _cmd) {
          int val =10;
          NSArray* a = __null;
    
              a = ((NSArray *(*)(Class, SEL, const ObjectType *, NSUInteger))(void *)objc_msgSend)(objc_getClass("NSArray"), sel_registerName("arrayWithObjects:count:"), (const id *)__NSContainer_literal(2U, ((void (*)())&__BlockTest__getBlockArray_block_impl_0((void *)__BlockTest__getBlockArray_block_func_0, &__BlockTest__getBlockArray_block_desc_0_DATA, val)), ((void (*)())&__BlockTest__getBlockArray_block_impl_1((void *)__BlockTest__getBlockArray_block_func_1, &__BlockTest__getBlockArray_block_desc_1_DATA, val))).arr, 2U);
          return a;
}
这里我们看到有 __NSContainer_literal 这个函数,他做了些什么呢?看下他的定义:
struct __NSContainer_literal {
  void * *arr;
  __NSContainer_literal (unsigned int count, ...) {
    va_list marker;
    va_start(marker, count);
    arr = new void *[count];
    for (unsigned i = 0; i < count; i++)
      arr[i] = va_arg(marker, void *);
    va_end( marker );
  };
  ~__NSContainer_literal() {
    delete[] arr;
  }
};
多个栈上的 block 作为参数传进去,然后在内部赋值给数组,当 block 作为返回值返回的时候,会被拷贝到堆上,所以,这个数组里面的每个元素都是堆上的 block,所以不会crash。
四、NSDictionary
由于 NSDictionary 会 copy key 但是不 copy object,所以也会出现上面类似的crash情况。
   

本文由职坐标整理并发布,希望对同学们有所帮助。了解更多详情请关注职坐标移动开发之IOS频道!

本文由 @白羽 发布于职坐标。未经许可,禁止转载。
喜欢 | 0 不喜欢 | 0
看完这篇文章有何感觉?已经有0人表态,0%的人喜欢 快给朋友分享吧~
评论(0)
后参与评论

您输入的评论内容中包含违禁敏感词

我知道了

助您圆梦职场 匹配合适岗位
验证码手机号,获得海同独家IT培训资料
选择就业方向:
人工智能物联网
大数据开发/分析
人工智能Python
Java全栈开发
WEB前端+H5

请输入正确的手机号码

请输入正确的验证码

获取验证码

您今天的短信下发次数太多了,明天再试试吧!

提交

我们会在第一时间安排职业规划师联系您!

您也可以联系我们的职业规划师咨询:

小职老师的微信号:z_zhizuobiao
小职老师的微信号:z_zhizuobiao

版权所有 职坐标-一站式AI+学习就业服务平台 沪ICP备13042190号-4
上海海同信息科技有限公司 Copyright ©2015 www.zhizuobiao.com,All Rights Reserved.
 沪公网安备 31011502005948号    

©2015 www.zhizuobiao.com All Rights Reserved