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。

  3. _NSConcreteGlobalBlock:类似函数,位于text段;

  4. _NSConcreteStackBlock:位于栈内存,函数返回后Block将无效;
  5. _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实现原理



相关文章
|
7月前
|
Swift iOS开发 开发者
IOS开发基础知识:什么是 ARC(自动引用计数)?如何工作?
IOS开发基础知识:什么是 ARC(自动引用计数)?如何工作?
102 1
|
iOS开发
iOS block修饰符用copy还是strong
iOS block修饰符用copy还是strong
162 0
|
iOS开发 Python
iOS小技能:lldb打印block参数签名
iOS逆向时经常会遇到参数为block类型,本文介绍一个lldb script,可快速打印出Objective-C方法中block参数的类型。
202 0
iOS小技能:lldb打印block参数签名
|
iOS开发 开发者
iOS开发 - 如何写出漂亮的block
iOS开发 - 如何写出漂亮的block
107 0
|
iOS开发
iOS开发- 关于Block的几种应用
iOS开发- 关于Block的几种应用
120 0
|
自然语言处理 iOS开发
IOS——Block
IOS——Block
80 0
|
iOS开发
iOS代理 通知 block传值的规范写法
iOS代理 通知 block传值的规范写法
146 0
|
存储 API iOS开发
iOS Principle:Block(下)
iOS Principle:Block(下)
133 0
iOS Principle:Block(下)
|
C语言 iOS开发 C++
iOS Principle:Block(上)
iOS Principle:Block(上)
118 0
iOS Principle:Block(上)
|
存储 C语言 iOS开发
iOS Principle:Block(中)
iOS Principle:Block(中)
122 0