iOS Block捕获外部变量和ARC自动拷贝block

Block内引用外部变量的问题

#define BLog(prefix,obj) {NSLog(@"位置和指针变量名:%@ ,指针内存地址:%p, 指针值:%p ,指向的对象:%@ ",prefix,&obj,obj,obj);}

// 强引用
- (void)blockVariableStrongReferenceTest
{
    NSLog(@"\n");
    NSObject *obj = [[NSObject alloc] init];
    BLog(@"StrongRef obj",obj);
    void(^testBlock)()= ^(){
        BLog(@"StrongRef in block",obj);
    };
    testBlock();
    // Block外部尝试将obj置为nil
    obj = nil;
    testBlock();  // 第二次调用block
}

运行结果

 位置和指针变量名:StrongRef obj ,指针内存地址:0x7fff543d0c98, 指针值:0x7fcb1bd22390 ,指向的对象:<NSObject: 0x7fcb1bd22390> 
 
 位置和指针变量名:StrongRef in block ,指针内存地址:0x7fcb1c903fb0, 指针值:0x7fcb1bd22390 ,指向的对象:<NSObject: 0x7fcb1bd22390> 
 
 位置和指针变量名:StrongRef in block ,指针内存地址:0x7fcb1c903fb0, 指针值:0x7fcb1bd22390 ,指向的对象:<NSObject: 0x7fcb1bd22390

分析

  • 方法内部的obj变量在栈中,变量内存地址0x7fff543d0c98,所指向的对象<NSObject: 0x7fcb1bd22390>在堆中,内存地址0x7fcb1bd22390,它的引用计数+1。

  • Block中obj指针已经不是外部的obj指针了,它是外部变量obj的拷贝,它的内存地址是0x7fcb1c903fb0,跟外部obj不一样,但是所指向的对象也是0x7fcb1bd22390,0x7fcb1bd22390对象引用计数再+1=2。

  • block中的obj指针(内存地址是0x7fcb1c903fb0)对对象(<NSObject: 0x7fcb1bd22390>)的引用是强引用,在外部将obj(地址0x7fff543d0c98)置为nil后,外部的obj不再指向<NSObject: 0x7fcb1bd22390>对象,0x7fcb1bd22390对象的引用计数-1,引用计数为1,0x7fcb1bd22390对象内存不被自动回收,所以第二次调用block,0x7fcb1bd22390对象还在内存中。

结论
block内部的obj 指针是外部obj指针的拷贝,有2个指针指向同一个NSObject对象,但只将外部的obj指针置为nil,NSObject对象的引用计数不为0,无法回收。

// 弱引用
- (void)blockVariableWeakReferenceTest
{
    NSLog(@"\n");
    NSObject *obj = [[NSObject alloc] init];
    BLog(@"StrongRef obj",obj);  
    __weak NSObject *weakObj = obj;
    BLog(@"WeakRef weakObj", weakObj);
    void(^testBlock)()= ^(){
        BLog(@"weakObj in block",weakObj);
    };
    testBlock();
    obj = nil; 
    testBlock();
}

运行结果

 位置和指针变量名:StrongRef obj ,指针内存地址:0x7fff543d0c98, 指针值:0x7fcb1bd2fee0 ,指向的对象:<NSObject: 0x7fcb1bd2fee0> 
 
 位置和指针变量名:WeakRef weakObj ,指针内存地址:0x7fff543d0c90, 指针值:0x7fcb1bd2fee0 ,指向的对象:<NSObject: 0x7fcb1bd2fee0> 
 
位置和指针变量名:weakObj in block ,指针内存地址:0x7fcb1bc072b0, 指针值:0x7fcb1bd2fee0 ,指向的对象:<NSObject: 0x7fcb1bd2fee0> 

 位置和指针变量名:weakObj in block ,指针内存地址:0x7fcb1bc072b0, 指针值:0x0 ,指向的对象:(null) 

结论

分析

  • 方法内部的weakObj变量在栈中,变量内存地址0x7fff543d0c98,所指向的对象<NSObject: 0x7fcb1bd2fee0>在堆中,内存地址0x7fcb1bd2fee0,它的引用计数+1。
  • weakObj变量也在栈中,内存为0x7fff543d0c90,所指向的对象也是<NSObject: 0x7fcb1bd2fee0>,弱引用,所以0x7fcb1bd2fee0对象的引用计数不增加,仍然为1.
  • Block中weakObj它的内存地址是0x7fcb1bc072b0,跟外部的weakObj不同,但是所指向的对象也是0x7fcb1bd2fee0,弱引用,0x7fcb1bd2fee0对象引用计数还是不增加,仍然是1。
    -在外部将obj(地址0x7fff543d0c98)置为nil后,外部的obj不再指向<NSObject: 0x7fcb1bd2fee0>对象,0x7fcb1bd2fee0对象的引用计数-1,引用计数为0,ARC回收0x7fcb1bd2fee0对象内存,并将指向它的弱引用指针赋值为nil,所以第二次调用block,0x7fcb1bd2fee0对象不在在内存中。

Block生命周期内的对象安全

在block中__weak声明的指针去引用对象 可以避免循环引用的问题,但是当外部对象被释放了,block 内部会访问不到这个对象. 这种问题如何解决呢?先来看一段代码:

//多线程时Block生命周期内对象安全
- (void)blockVariableMutiThreadTest
{
    NSObject *obj = [[NSObject alloc]init]; //obj强引用,<NSObject: 0x7f9413c1c040>对象引用计数+1,=1
    BLog(@"obj", obj);
    __weak NSObject *weakObj = obj;//weakObj弱引用,<NSObject: 0x7f9413c1c040>对象引用计数不变,=1
    BLog(@"weakObj-0", weakObj);
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        __strong NSObject *strongObj = weakObj; //strongObj强引用,<NSObject: 0x7f9413c1c040>对象引用计数+1,=2
        sleep(3);
        BLog(@"weakObj - block", weakObj);
        BLog(@"strongObj - block", strongObj);
    });
    sleep(1);
    obj = nil; //obj被置为nil,<NSObject: 0x7f9413c1c040>对象引用计数-1,=1
    BLog(@"weakObj-1", weakObj);  //没被释放
    sleep(4); //block在异步线程中执行完毕(在另一块内存中执行),block内存被释放,<NSObject: 0x7f9413c1c040>对象引用计数-1,=0;ARC开始把0x7f9413c1c040对象内存回收,把弱引用weakObj置为nil
    BLog(@"weakObj-2", weakObj);
}

执行结果如下:

位置和指针变量名:obj ,指针内存地址:0x7fff51888c98, 指针值:0x7f9413c1c040 ,指向的对象:<NSObject: 0x7f9413c1c040> 
位置和指针变量名:weakObj-0 ,指针内存地址:0x7fff51888c90, 指针值:0x7f9413c1c040 ,指向的对象:<NSObject: 0x7f9413c1c040> 
位置和指针变量名:weakObj-1 ,指针内存地址:0x7fff51888c90, 指针值:0x7f9413c1c040 ,指向的对象:<NSObject: 0x7f9413c1c040> 
位置和指针变量名:weakObj - block ,指针内存地址:0x7f9413d9a880, 指针值:0x7f9413c1c040 ,指向的对象:<NSObject: 0x7f9413c1c040> 
位置和指针变量名:strongObj - block ,指针内存地址:0x1187e2e08, 指针值:0x7f9413c1c040 ,指向的对象:<NSObject: 0x7f9413c1c040> 
位置和指针变量名:weakObj-2 ,指针内存地址:0x7fff51888c90, 指针值:0x0 ,指向的对象:(null) 

总结
多线程的时候,在 block 外部__weak声明的变量指向一个对象, 通过把__weak声明的变量值赋值给block内部__strong变量```,实现在block内对该对象进行强引用,这样可以在block生命周期内保留该对象不被释放,在block生命周期结束后,对象内存被释放。

block修改外部变量 __block变量

- (void)blockVariable
{
   // 使用__block
   NSObject *obj = [[NSObject alloc]init];
   BLog(@"obj",obj); // 1 
   __block NSObject *blockObj = obj;
   obj = nil;
   BLog(@"外部blockObj -1",blockObj); // 2
   void(^testBlock)() = ^(){
       BLog(@"内部blockObj - block",blockObj); // 5
       NSObject *obj2 = [[NSObject alloc]init];
       BLog(@"内部obj2",obj2);   // 6
       blockObj = obj2;
       BLog(@"blockObj - block",blockObj); // 7
   };
   NSLog(@"%@",testBlock);   // 3
   
   BLog(@"外部blockObj -2",blockObj); // 4
   testBlock();
   BLog(@"外部blockObj -3",blockObj);   // 8
​
}
​

运行结果

 位置和指针变量名:obj ,指针内存地址:0x7fff5dd2ec78, 指针值:0x7fa082661a00 ,指向的对象:<NSObject: 0x7fa082661a00> 
 位置和指针变量名:外部blockObj -1 ,指针内存地址:0x7fff5dd2ec70, 指针值:0x7fa082661a00 ,指向的对象:<NSObject: 0x7fa082661a00>
 
 <__NSMallocBlock__: 0x7fa084906fa0>      ------ 这是block地址和类型

 位置和指针变量名:外部blockObj -2 ,指针内存地址:0x7fa084905838, 指针值:0x7fa082661a00 ,指向的对象:<NSObject: 0x7fa082661a00> 
 
 位置和指针变量名:内部blockObj - block ,指针内存地址:0x7fa084905838, 指针值:0x7fa082661a00 ,指向的对象:<NSObject: 0x7fa082661a00>        
 位置和指针变量名:内部obj2 ,指针内存地址:0x7fff5dd2eba8, 指针值:0x7fa084916660 ,指向的对象:<NSObject: 0x7fa084916660> 
 位置和指针变量名:blockObj - block ,指针内存地址:0x7fa084905838, 指针值:0x7fa084916660 ,指向的对象:<NSObject: 0x7fa084916660> 
 
 位置和指针变量名:外部blockObj -3 ,指针内存地址:0x7fa084905838, 指针值:0x7fa084916660 ,指向的对象:<NSObject: 0x7fa084916660> 

分析

  • 第3处日志打印了一个testBlock对象,blockObj的地址发生变化。此时,block对象 从栈拷贝到堆上,__block变量blockObj,也被拷贝到堆上。block对象拥有blockObj指针指向的对象。注意:这是个强引用哦。
  • 关注4到8 处日志,用__block关键字声明blockObj指针后,block内外的变量blockObj都是0x7fa084905838,也就是block内外的blockObj指针是同一个指针。
  • block内部改变 blockObj指针指向的对象,改动在 block外部可见。

关于 block访问外部变量原理

block实现原理(一)

block和变量的内存管理(二)

在oc,在block中直接访问外部变量,访问的是外部变量的copy。用clang后将 .m翻译为.cpp文件后发现,外部函数是通过传值方式将变量值传给block(block结构体、block最终要执行的函数代码).
使用了__block后,外部函数是通过指针传递,将变量传递到 block 内,所以可以修改变量值.

Block在内存中的位置

Block作为C语言的扩展,并不是高新技术,和其他语言的闭包或lambda表达式是一回事。需要注意的是由于Objective-C在iOS中不支持GC机制,使用Block必须自己管理内存,而内存管理正是使用Block坑最多的地方,错误的内存管理 要么导致return cycle内存泄漏要么内存被提前释放导致crash。 Block的使用很像函数指针,不过与函数最大的不同是:Block可以访问函数以外、词法作用域以内的外部变量的值。换句话说,Block不仅 实现函数的功能,还能携带函数的执行环境。

可以这样理解,Block其实包含两个部分内容:

  1. Block执行的代码,这是在编译的时候已经生成好的;
  2. 一个包含Block执行时需要的所有外部变量值的数据结构。 Block将使用到的、作用域附近的变量建立一份快照拷贝

根据Block在内存中的位置分为三种类型:_NSConcreteGlobalBlock,_NSConcreteMallocBlock, _NSConcreteStackBlock。

  • _NSConcreteGlobalBlock:类似函数,位于text段;
  • _NSConcreteStackBlock:位于栈内存,函数返回后Block将无效;
  • _NSConcreteMallocBlock:位于堆内存。
_NSConcreteGlobalBlock:

Blocks that don't capture any variables are global blocks. Since all instances of the block are the same, the compiler can just allocate one copy statically for the life of the program。

_NSConcreteStackBlock 和 _NSConcreteMallocBlock:

Blocks that capture variables (closures) are either stack or heap (malloc) blocks. Blocks start out on the stack, as stack blocks. When a stack block is copied for the first time, it is moved to the heap. Copying a heap block does not create another copy; but simply retains it.
在开启 ARC 时,大部分情况下编译器通常会将创建在栈上的 block 自动拷贝到堆上。

  • 当 block 作为函数返回值返回时,编译器自动将 block 作为 _Block_copy 函数,效果等同于 block 直接调用 copy 方法;
  • 当 block 被赋值给 __strong id 类型的对象或 block 的成员变量时,编译器自动将 block 作为 _Block_copy 函数,效果等同于 block 直接调用 copy 方法;
  • 当 block 作为参数被传入方法名带有 usingBlock 的 Cocoa Framework 方法或 GCD 的 API 时。这些方法会在内部对传递进来的 block 调用 copy 或 _Block_copy 拷贝;

ARC中自动copy block的例子

- (void)blockObjectInMemory
{
   // global block
   void (^globalBlockInMemory)(int number) = ^(int number){
       printf("%d \n",number);
   };
   globalBlockInMemory(90);
   BLog(@"global block %@", globalBlockInMemory);
   
   // malloc block
   int outVariable = 100;
   void (^mallocBlockInMemory)(int number) = ^(int number){
       printf("%d \n",outVariable+number);
   };
   BLog(@"stackBlock block %@", mallocBlockInMemory);     // ARC 自动将栈中block拷贝到堆上
}

 位置和指针变量名:global block %@ ,指针内存地址:0x7fff5b422c78, 指针值:0x1047e01f0 ,指向的对象:<__NSGlobalBlock__: 0x1047e01f0> 
 位置和指针变量名:stackBlock block %@ ,指针内存地址:0x7fff5b422c68, 指针值:0x7fe6849085f0 ,指向的对象:<__NSMallocBlock__: 0x7fe6849085f0> 

- (id)returnBlock
{
    int outVariable = 100;
    void (^mallocBlockInMamory)(void) = ^(void){
            NSLog(@"in block");
    };
    BLog(@" block  ", mallocBlockInMamory);
    return mallocBlockInMamory;
}

- (void)blockInmemory
{
    id block = [self returnBlock];
    BLog(@"a block %@", block);
}


位置和指针变量名: block   ,指针内存地址:0x7fff516a9c30, 指针值:0x10e559250 ,指向的对象:<__NSGlobalBlock__: 0x10e559250> 
位置和指针变量名:a block %@ ,指针内存地址:0x7fff516a9c78, 指针值:0x10e559250 ,指向的对象:<__NSGlobalBlock__: 0x10e559250> 

在没有ARC之前,由于ARC 自动将栈中block拷贝到堆上,所以当returnBlock函数退出,在栈中内存释放后,仍然可以访问到block对象。

ARC 中需要手动拷贝Block的例子

在以下情形中, block 会从栈拷贝到堆:

  • 当 block 调用 copy 方法时,如果 block 在栈上,会被拷贝到堆上;
  • 当 block 作为函数返回值返回时,编译器自动将 block 作为 _Block_copy 函数,效果等同于 block 直接调用 copy 方法;
  • 当 block 被赋值给 __strong id 类型的对象或 block 的成员变量时,编译器自动将 block 作为 _Block_copy 函数,效果等同于 block 直接调用 copy 方法;
  • 当 block 作为参数被传入方法名带有 usingBlock 的 Cocoa Framework 方法或 GCD 的 API 时。这些方法会在内部对传递进来的 block 调用 copy 或 _Block_copy 进行拷贝;

其他情况需要手动拷贝。

- (void)stackBlockInMemory
{
    NSArray *array = [self getBlockArray];
    id block = array[0];
    BLog(@"block %@", block);
}

- (id)getBlockArray
{
    int val = 10;
    return [[NSArray alloc] initWithObjects:
            ^{NSLog(@"value:%d", val);},
            ^{NSLog(@"value:%d", val);}, nil];
}

程序会报EXC_BAD_ACCESS ,getBlockArray返回的数组里面的 block 是不可访问的。

手动copy后,block拷贝到堆上,getBlockArray函数返回的栈帧被销毁后,仍可以访问堆中的block拷贝。

- (id)getBlockArray
{
    int val = 10;
    return [[NSArray alloc] initWithObjects:
            [^{NSLog(@"value:%d", val);} copy],
            [^{NSLog(@"value:%d", val);} copy], nil];
}

Block中造成内存泄漏的一些场景

推荐文章:

http://www.tanhao.me/pieces/310.html/

block 内存管理

block 实现原理

Block-ABI-Apple

正确使用Block避免Cycle Retain和Crash

__weak与__block区别

block实现原理(一)

block和变量的内存管理(二)

How blocks are implemented (and the consequences

block实现原理

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容