iOS:Block 编程3--内存管理、对象、变量

参考:官网<Blocks Programming Topics>,https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Blocks/Articles/00_Introduction.html

参考:http://tanqisen.github.io/blog/2013/04/19/gcd-block-cycle-retain/

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

======================================================

========================Blocks and Variables=============

        本部分描述 blocks 和变量及他们之间的交互,包括内存管理

========================Block Variables & 内存管理========

          block 其实也是一个 NSObject 对象,并且在大多数情况下,block 是分配在栈上面的,只有当 block 被定义为全局变量block 块中没有引用任何 automatic 变量时,block 才分配在全局数据段上(对应下面的堆和text段)。 __block 变量也是分配在栈上面的。
         在 ARC 下,编译器会自动检测为我们处理了 block 的大部分内存管理,但当将 block 当作方法参数时候,编译器不会自动检测,需要我们手动拷贝该 block 对象。幸运的是,Cocoa 库中的大部分名称中包含”usingBlock“的接口以及 GCD 接口在其接口内部已经进行了拷贝操作,不需要我们再手动处理了。但除此之外的情况,就需要我们手动干预了。          

         根据Block在内存中的位置分为三种类型:NSGlobalBlock,NSStackBlock, NSMallocBlock
--NSGlobalBlock:类似函数,位于text段;
--NSStackBlock:位于栈内存,函数返回后Block将无效;
--NSMallocBlock:位于堆内存。

BlkSum blk1 = ^ long (int a, int b) {
  return a + b;
};
NSLog(@"blk1 = %@", blk1);// blk1 = <__NSGlobalBlock__: 0x47d0>

int base = 100;
BlkSum blk2 = ^ long (int a, int b) {
  return base + a + b;
};
NSLog(@"blk2 = %@", blk2); // blk2 = <__NSStackBlock__: 0xbfffddf8>

BlkSum blk3 = [[blk2 copy] autorelease];
NSLog(@"blk3 = %@", blk3); // blk3 = <__NSMallocBlock__: 0x902fda0>
        上面blk1类型是NSGlobalBlock,而blk2类型是NSStackBlock?blk1和blk2的区别在于, blk1没有使用Block以外的任何外部变量,Block不需要建立局部变量值的快照(copy),这使blk1与函数没有任何区别,从blk1所在内存地址0x47d0猜测编译器把blk1放到了text代码段。
- --------------------Block的copy、retain、release操作

         不同于NSObject的copy、retain、release操作:

--Block_copy与copy等效,Block_release与release等效
--对Block不管是retain、copy、release都不会改变引用计数retainCount,retainCount始终是1
--NSGlobalBlock:retain、copy、release操作都无效
--NSStackBlock:retain、release操作无效支持copy,copy之后生成新的NSMallocBlock类型对象。必须注意的是,NSStackBlock在函数返回后,Block内存将被回收。即使retain也没用。容易犯的错误是[[mutableAarry --addObject:stackBlock],在函数出栈后,从mutableAarry中取到的stackBlock已经被回收,变成了野指针。正确的做法是先将stackBlock copy到堆上,然后加入数组:[mutableAarry addObject:[[stackBlock copy] autorelease]]。

- (id) getBlockArray 
{ 
    int val = 10; 
    return [[NSArray alloc] initWithObjects: 
            ^{ KSLog(@"  > block 0:%d", val); },    // block on the stack 
            ^{ KSLog(@"  > block 1:%d", val); },    // block on the stack 
            nil]; 
     
//    return [[NSArray alloc] initWithObjects: 
//            [^{ KSLog(@"  > block 0:%d", val); } copy],    // block copy to heap 
//            [^{ KSLog(@"  > block 1:%d", val); } copy],    // block copy to heap 
//            nil]; 
} 
 
- (void)testManageBlockMemory 
{ 
    id obj = [self getBlockArray]; 
    typedef void (^BlockType)(void); 
    BlockType blockObject = (BlockType)[obj objectAtIndex:0]; 
    blockObject(); 
} 
// 在调用 testManageBlockMemory 时,程序会 crash 掉。因为从 getBlockArray 返回的 block 是分配在 stack 上的
// 正确的做法(被屏蔽的那段代码)是在将 block 添加到 NSArray 中时先 copy 到 heap 上
  --NSMallocBlock支持retain、release,虽然retainCount始终是1,但内存管理器中仍然会增加、减少计数。copy之后不会生成新的对象,只是增加了一次引用,类似retain

--尽量不要对Block使用retain操作

------------------------Copying Blocks

         通常,你不需要 copy(或 retain)一个 block.在你希望 block 在它被声明的作用域(scope within which it was declared)被销毁后继续使用的话,你子需要做一份拷贝。在 ARC 下,对 block 变量进行 copy 始终是安全的,无论它是在栈上,还是全局数据段,还是已经拷贝到堆上。对栈上的 block 进行 copy 是将它拷贝到堆上;对全局数据段中的 block 进行 copy 不会有任何作用;对堆上的 block 进行 copy 只是增加它的引用记数。拷贝会把 block 移到堆里面。

       你可以使用 C 函数来 copy 和 release 一个 block:       

Block_copy();
Block_release();

      如果你使用 Objective-C,你可以给一个 block 发送 copy、retain 和 release(或 autorelease)消息。
      为了避免内存泄露,你必须总是平衡 Block_copy()和 Block_release()。你必须平衡 copy 或 retain 和 release(或 autorelease)--除非是在垃圾回收的环境里面。

注:

1.如果栈上的 block 中引用了__block 类型的变量,在将该 block 拷贝到堆上时也会将 __block 变量拷贝到堆上,如果该 __block 变量在堆上还没有对应的拷贝的话,否则就增加堆上对应的拷贝的引用记数;

========================Block对不同类型的变量的存取=========

------------------------基本类型---------

-------访问:

        你可以引用reference三种标准类型的变量,就像你在函数里面引用那样:

1.全局变量,包括静态局部变量
2.全局函数(在技术上而言这不是变量)。
3.enclosing scope内的局部变量和参数。

        Blocks 同样支持其他两种类型的变量(都是同一作用域enclosing lexical scope中的局部变量):

1. At function level are __block variables. These are mutable within the block (and the enclosing scope) and are preserved if any referencing block is copied to the heap.
2. const imports.

最后,within a method implementation, blocks may reference Objective-C instance variables

        在block 里面使用变量规则

1.全局变量可访问,包括在相同作用域范围内的静态变量static variablesthat exist within the enclosing lexical scope);

2.传递给block的参数可访问(和函数的参数一样)。

3.在lexical scope里的局部变量,就像函数中得局部变量一样。Each invocation of the block provides a new copy of that variable. These variables can in turn be used as const or by-reference variables in blocks enclosed within the block.可以参考第4条。

4.Stack (non-static) variables local to the enclosing lexical scope are captured as const variables(同一作用域范围内非静态的局部变量当做const / imported variable)。它们的值在程序里面的 block 表达式内使用。在nested block里面,该值在nearest enclosing scope被捕获。

5.在enclosing lexical scope并被__block修饰的局部变量,可修改。Any changes are reflected in the enclosing lexical scope, including any other blocks defined within the same enclosing lexical scope.详细见后文。

注意:lexical scope 和 enclosing lexical scope 作用域和闭包所在作用域的意思不同。上文变量使用规则顺序可以看做:从全局的-->局部(block内)-->相同作用域

-------存取:

1.局部自动变量Stack (non-static) variables local在Block中只读。Block定义时(注意是定义,不是运行)copy变量的值,在Block中作为常量使用。

例子:

 // block 在实现时就会对它引用到的它所在方法中定义的 栈变量 进行一次只读拷贝,然后在block块内使用该只读拷贝
- (void)testAccessVariable 
{ 
    NSInteger outsideVariable = 10; 
    //要让 blockObject 修改或同步使用 outside 变量就需要用 __block 来修饰 outside变量
    //__block NSInteger outsideVariable = 10;
    NSMutableArray * outsideArray = [[NSMutableArray alloc] init]; 
     
    void (^blockObject)(void) = ^(void){ 
        NSInteger insideVariable = 20; 
        KSLog(@"  > member variable = %d", self.memberVariable); 
        KSLog(@"  > outside variable = %d", outsideVariable); 
        KSLog(@"  > inside variable = %d", insideVariable);          
        [outsideArray addObject:@"AddedInsideBlock"]; 
    };      
    outsideVariable = 30; 
    self.memberVariable = 30;  

    blockObject();      
    KSLog(@"  > %d items in outsideArray", [outsideArray count]); 
} 
> member variable = 30 
> outside variable = 10 
> inside variable = 20 
> 1 items in outsideArray 

2.static变量、全局变量: 因为全局变量或静态变量在内存中的地址是固定的,Block在读取该变量值的时候是直接从其所在内存读出,获取到的是最新值,而不是在定义时copy的常量。

3.Block变量:被__block修饰的变量称作Block变量。Block变量的存储和 register、auto、static 等存储类型相似,但它们之 间不兼容。

             __block variables live in storage that is shared between the lexical scope of the variable and all blocks and block copies declared or created within the variable’s lexical scope. Thus, the storage will survive the destruction of the stack frame if any copies of the blocks declared within the frame survive beyond the end of the frame (for example, by being enqueued somewhere for later execution). Multiple blocks in a given lexical scope can simultaneously use a shared variable。__block variables可以被在block lexical scope里面的声明、创建的block和 里面的局部变量访问。如果blocks在其作用域销毁的时候还存在,那它的__block variable也存在。多个block可共享__block variable

            注意:__block variables不能存储的:可变长的数组 和 contain C99 variable-length arrays.  

  ---------------------------------------详细例子:

              当程序运行到这里时,stack 空间中有 shared 变量和 captured 变量。这里可以看出,__block 变量开始是处于stack上的。


           当程序运行到这里时,stack 空间中有 shared 变量,captured 变量和block1。这里可以看出,block 类型的变量开始时也是处在stack上的。
          这里值得注意的就是当我们直接修改stack 上的captured变量时,block1中的captured变量仍然是原来的数值10。事实上,从const 我们就可以看出,block1中的captured变量是不能被修改的而且是从stack原有变量的一个const 拷贝


             block在一开始是处在stack上的,这是为了考虑到效率的原因,但是,有时候是需要block的生命周期长于一开始的stack,这时,我们就通过copy block 来将block复制到heap。

            当程序执行完 block2 = [block1 copy];时,__block 类型变量shared,被复制到了heap中,很显然,shared变量需要被block和block2共享(当然还有stack也要共享),而block2被移动到heap中,很可能生命周期会长于stack,所以,shared也被复制到了heap中。而block2中的captured 也被复制到了heap中。


              当程序执行完 block3 = [block2 copy];时, 我们看到的是,block2 和block3 其实指向的是同一片内存空间。事实上,block的数据结构中,保存了引用计数,而对于copy到heap中的block 再copy时,行为同普通对象retain一样,会使引用计数+1。那么如果我们对[block retain]会如何呢? 实际上什么都没有发生,至少在现在的runtime版本下。因为retain中,不仅有引用计数+1在,而且retain的返回值,必须同返回调用对象的地址一样,而block的地址是可能变化的(stack or heap),所以,这里retain的行为几乎是被忽略掉的。

           当heap中的block变量先于stack被销毁时,如调用 [block2 release]; [block3 release];,heap中的block2,block3 由于引用计数为0 而被销毁,而 __block 变量shared则还在heap中,因为stack还要使用,block1 也要使用。


            当heap中的block变量晚于stack时,显然,stack 被清除,function中也啥都没了。

                最后,当block2 和block3 都被release之后。则恢复到最初状态。

------------------------Object Variables---------

         1.在reference-counted环境里,by default when you reference an Objective-C object within a block,it is retained. This is true even if you simply reference an instance variable of the object. Object variables marked with the__block storage type modifier, however,are not retained。

          2.如果你使用了 block within the implementation of a method,对象的内存管理规则更微妙:

1.If you access an instance variable by reference, self is retained;

2.If you access an instance variable by value, the variable is retained.

dispatch_async(queue, ^{
// instanceVariable is used by reference, self is retained
doSomethingWithObject(instanceVariable);
});
id localVariable = instanceVariable;
dispatch_async(queue, ^{
// localVariable is used by value, localVariable is retained (not self)
doSomethingWithObject(localVariable);
});
------copy对objc对象引用计算影响(可理解为block对对象引用计数的影响,因为你要“使用block”,基本都要copy(不然的话,栈上的block(包括那些字面的)随着某方法/函数结束而结束),与上段描述统一;上段描述也可看成block使用过程中影响,这里是block使用结果的影响

@interface MyClass : NSObject {
    NSObject* _instanceObj;
}
@end
@implementation MyClass

NSObject* __globalObj = nil;
- (id) init {
    if (self = [super init]) {
        _instanceObj = [[NSObject alloc] init];
    }
    return self;
}
- (void) test {
    static NSObject* __staticObj = nil;
    __globalObj = [[NSObject alloc] init];
    __staticObj = [[NSObject alloc] init];

    NSObject* localObj = [[NSObject alloc] init];
    __block NSObject* blockObj = [[NSObject alloc] init];

    typedef void (^MyBlock)(void) ;
    MyBlock aBlock = ^{
        NSLog(@"%@", __globalObj);
        NSLog(@"%@", __staticObj);
        NSLog(@"%@", _instanceObj);
        NSLog(@"%@", localObj);
        NSLog(@"%@", blockObj);
    };
    aBlock = [[aBlock copy] autorelease];
    aBlock();

    NSLog(@"%d", [__globalObj retainCount]);
    NSLog(@"%d", [__staticObj retainCount]);
    NSLog(@"%d", [_instanceObj retainCount]);
    NSLog(@"%d", [localObj retainCount]);
    NSLog(@"%d", [blockObj retainCount]);
}
@end
          执行结果为1 1 1 2 1。

         __globalObj和__staticObj在内存中的位置是确定的,所以Block copy时不会retain对象
         _instanceObj在Block copy时也没有直接retain _instanceObj对象本身,但会retain self。所以在Block中可以直接读写_instanceObj变量
         localObj在Block copy时,系统自动retain对象,增加其引用计数
         blockObj在Block copy时也不会retain

         非ObjC对象,如GCD队列dispatch_queue_t。Block copy时并不会自动增加他的引用计数,这点要非常小心

1.When you copy a block, any references to other blocks from within that block are copied if necessary—an entire tree may be copied (from the top).If you have block variables and you reference a block from within the block, that block will be copied。但作为参数的Block是不会发生copy的

void foo() {
  int base = 100;
  BlkSum blk = ^ long (int a, int b) {
    return  base + a + b;
  };
  NSLog(@"%@", blk); // <__NSStackBlock__: 0xbfffdb40>
  bar(blk);
}

void bar(BlkSum sum_blk) {
  NSLog(@"%@",sum_blk); // 与上面一样,说明作为参数传递时,并不会发生copy

  void (^blk) (BlkSum) = ^ (BlkSum sum) {
    NSLog(@"%@",sum);     // 无论blk在堆上还是栈上,作为参数的Block不会发生copy。
    NSLog(@"%@",sum_blk); // 当blk copy到堆上时,sum_blk也被copy了一分到堆上上。
  };
  blk(sum_blk); // blk在栈上
  blk = [[blk copy] autorelease];
  blk(sum_blk); // blk在堆上
}
 ------------------------retain cycle(weak–strong dance 技术来避免循环引用)---------

-------例子1 :

        前文中提到内联 block 可以直接引用 self,但是要非常小心地在 block 中引用 self。因为在一些内联 block 引用 self,可能会导致循环引用。如下例所示:

@interface KSViewController () 
{ 
    id _observer; 
}  
@end  
@implementation KSViewController 
- (void)viewDidLoad 
{ 
    [super viewDidLoad]; 
        
    KSTester * tester = [[KSTester alloc] init]; 
    [tester run];      
    _observer = [[NSNotificationCenter defaultCenter] 
                 addObserverForName:@"TestNotificationKey" 
                 object:nil queue:nil usingBlock:^(NSNotification *n) { 
                     NSLog(@"%@", self); 
                 }]; 
}  
- (void)dealloc 
{ 
    if (_observer) { 
        [[NSNotificationCenter defaultCenter] removeObserver:_observer]; 
    } 
} 
         分析:苹果官方文档中对 addObserverForName:object:queue:usingBlock: 中的 block 变量说明:(The block is copied by the notification center and (the copy) held until the observer registration is removed)相当notification center  retain 该 block的一份拷贝,在block 中引用到了 self,在这里 self 对象被 block retain,而self只要存在,其dealloc就不会被调用,notification center就一直存在,相当self  retain着notification center ,这就形成了 循环引用

          解决办法:weak–strong dance 技术

__weak KSViewController * wself = self; 
_observer = [[NSNotificationCenter defaultCenter] 
             addObserverForName:@"TestNotificationKey" 
             object:nil queue:nil usingBlock:^(NSNotification *n) { 
                 KSViewController * sself = wself; 
                 if (sself) { 
                     NSLog(@"%@", sself); 
                 } 
                 else { 
                     NSLog(@"<self> dealloc before we could run this code."); 
                 } 
             }]; 
           在block中使用self之前先用一个 __weak变量引用self,导致block不会retain self,打破retain cycle,然后在block中使用wself之前先用__strong类型变量引用wself,以确保使用过程中不会dealloc。简而言之就是推迟对self的retain,在使用时才进行retain。这有点像lazy loading的意思。
注:iOS5以下没有__weak,则需使用__unsafe_unretained。
          其他类似情况
//Note in particular that run loops retain their timers, so you can release a timer after you have added it to a run loop
//The object to which to send the message specified by aSelector when the timer fires. The target object is retained by the timer and released when the timer is invalidated.
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)seconds target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats
        结合这两处文档说明,我们就知道只要重复性 timer 还没有被 invalidated,target 对象就会被一直持有而不会被释放。因此当你使用 self 当作 target 时,你就不能期望在 dealloc 中 invalidate timer,因为在 timer 没有被invalidate 之前,dealloc 绝不会被调用。因此,需要找个合适的时机和地方来 invalidate timer,但绝不是在 dealloc 中。
-------例子2
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setCompletionBlock:^{
  NSString* string = [request responseString];
}];
       +-----------+           +-----------+
       | request   |           |   Block   |
  ---> |           | --------> |           |
       | retain 2  | <-------- | retain 1  |
       |           |           |           |
       +-----------+           +-----------+
         解决办法,使用弱引用(weak-strong dance)打断retain cycle:
__block ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setCompletionBlock:^{
  NSString* string = [request responseString];
}];
      +-----------+           +-----------+
      | request   |           |   Block   |
 ---->|           | --------> |           |
      | retain 1  | < - - - - | retain 1  |
      |           |   weak    |           |
      +-----------+           +-----------+
-------例子3
self.myBlock = ^ {
  [self doSomething];
};
 // 这里self和myBlock循环引用,解决办法同上:
__block MyClass* weakSelf = self;
self.myBlock = ^ {
  [weakSelf doSomething];
};
-------例子4
@property (nonatomic, retain) NSString* someVar;
self.myBlock = ^ {
  NSLog(@"%@", _someVer);
};
// 这里在Block中虽然没直接使用self,但使用了成员变量。在Block中使用成员变量,retain的不是这个变量,而会retain self。解决办法也和上面一样。
@property (nonatomic, retain) NSString* someVar;
__block MyClass* weakSelf = self;
self.myBlock = ^ {
  NSLog(@"%@", weakSelf .someVer);
};
// 或者
NSString* str = _someVer;
self.myBlock = ^ {
  NSLog(@"%@", str);
};
-------例子4:(retain cycle不只发生在两个对象之间,也可能发生在多个对象之间)
ClassA* objA = [[[ClassA alloc] init] autorelease];
  objA.myBlock = ^{
    [self doSomething];
  };
  self.objA = objA;
  +-----------+           +-----------+           +-----------+
  |   self    |           |   objA    |           |   Block   |
  |           | --------> |           | --------> |           |
  | retain 1  |           | retain 1  |           | retain 1  |
  |           |           |           |           |           |
  +-----------+           +-----------+           +-----------+
       ^                                                |
       |                                                |
       +------------------------------------------------+
// 解决办法同样是用__block打破循环引用
ClassA* objA = [[[ClassA alloc] init] autorelease];
__block MyClass* weakSelf = self;
objA.myBlock = ^{
  [weakSelf doSomething];
};
self.objA = objA;
 注意: MRC中__block是不会引起retain;但在ARC中__block则会引起retain。ARC中应该使用__weak或__unsafe_unretained弱引用。__weak只能在iOS5以后使用
 ------------------------Block使用对象被提前释放:
例子1:
// 有这种情况,如果不只是request持有了Block,另一个对象也持有了Block。
      +-----------+           +-----------+
      | request   |           |   Block   |   objA
 ---->|           | --------> |           |<--------
      | retain 1  | < - - - - | retain 2  |
      |           |   weak    |           |
      +-----------+           +-----------+
// 这时如果request 被持有者释放。
      +-----------+           +-----------+
      | request   |           |   Block   |   objA
 --X->|           | --------> |           |<--------
      | retain 0  | < - - - - | retain 1  |
      |           |   weak    |           |
      +-----------+           +-----------+
// 这时request已被完全释放,但Block仍被objA持有,没有释放,如果这时触发了Block,在Block中将访问已经销毁的request,这将导致程序crash。为了避免这种情况,开发者必须要注意对象和Block的生命周期
例子2:
// 开发者担心retain cycle错误的使用__block。
__block kkProducView* weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
  weakSelf.xx = xx;
});

// 将Block作为参数传给dispatch_async时,系统会将Block拷贝到堆上,如果Block中使用了实例变量,还将retain self,因为dispatch_async并不知道self会在什么时候被释放,为了确保系统调度执行Block中的任务时self没有被意外释放掉,dispatch_async必须自己retain一次self,任务完成后再release self。但这里使用__block,使dispatch_async没有增加self的引用计数,这使得在系统在调度执行Block之前,self可能已被销毁,但系统并不知道这个情况,导致Block被调度执行时self已经被释放导致crash。
// MyClass.m
- (void) test {
  __block MyClass* weakSelf = self;
  double delayInSeconds = 10.0;
  dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
  dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
    NSLog(@"%@", weakSelf);
});
// other.m
MyClass* obj = [[[MyClass alloc] init] autorelease];
[obj test];
// 这里用dispatch_after模拟了一个异步任务,10秒后执行Block。但执行Block的时候MyClass* obj已经被释放了,导致crash。解决办法是不要使用__block
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值