iOS内存管理--Block属性用copy修饰 & 避免循环引用的问题

Posted zzz098zzz

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS内存管理--Block属性用copy修饰 & 避免循环引用的问题相关的知识,希望对你有一定的参考价值。

一、Block的类型

根据Block在内存中的位置分为三种类型NSGlobalBlockNSStackBlock, NSMallocBlock

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


二、Blockcopyretainrelease操作

  不同于NSObjeccopyretainrelease操作:

  • Block_copycopy等效,Block_releaserelease等效;
  • Block不管是retaincopyrelease都不会改变引用计数retainCountretainCount终是1
  • NSGlobalBlockretaincopyrelease操作都无效;
  • NSStackBlockretainrelease操作无效,必须注意的是,NSStackBlock在函数返回后,Block内存将被回收。即使retain也没用。容易犯的错误是[[mutableAarry addObject:stackBlock],(补:在arc中不用担心此问题,因为arc中会默认将实例化的block贝到堆上在函数出栈后,从mutableArray中取到的stackBlock经被回收,变成了野指针。正确的做法是先将stackBlock copy到堆上,然后加入数组:[mutableArray addObject:[[stackBlock copy] autorelease]]支持copycopy之后生成新的NSMallocBlock类型对象
  • NSMallocBlock:支持retainrelease,虽然retainCount终是1,但内存管理器中仍然会增加、减少计数。copy之后不会生成新的对象,只是增加了一次引用,类似retain
  • 尽量不要Block使用retain操作

三、ARC与非ARC下的block

对于引用了外部变量的Block,如果没有对他进行copy,他的作用域只会在声明他的函数栈内(类型是__NSStackBlock__),如果想在非ARC下直接返回此类Block,Xcode会提示编译错误的,如下图:

(Xcode提示Returning block that lives on the local stack)

而在ARC环境下,上述代码会编译通过,因为ARC会自动加入copy操作。

比如可以在ARC下运行如下代码:

//ARC
MyBlock block = func();
NSLog(@"%d", block());
NSLog(@"%@", [block class]);

输出:

123
__NSMallocBlock__

类型是__NSMallocBlock__,说明Block已经被copy到了堆中了。

即便把Block用strong修饰,系统也会把block copy到堆中。

例如:@property (nonatomic,strong)void (^SubmitBlock)(GoodsModel *goodsModel,int number);

 打印一下block的类型为如下图


当然其实在非ARC下,也可以使上面有错误的函数编译通过。如下代码:

typedef int(^MyBlock)();
MyBlock func()

    int i = 123;
    //非ARC下不要这样!!!
    MyBlock ret = ^ return i; ;
    return ret;

    我们把原来的返回值赋给一个变量,然后再返回这个变量,就可以编译通过了。不过虽然编译通过了,这个返回的Block作用域仍是在函数栈中的,因此一旦函数运行完毕后再使用这个Block很可能会引发BAD_ACCESS错误。

以在非ARC下,必须把Block复制到堆中才可以在函数外使用Block,如下正确的代码:

typedef int(^MyBlock)();
MyBlock func()

    //非ARC
    int i = 123;
    return [^ return i;  copy];

    我们可以直接通过输出变量的指针,就可以验证Block被copy后,他所引用的变量被复制到了堆中的情况,如下代码(非ARC下):

//非ARC
void func()

    int a = 123;
    __block int b = 123;
    NSLog(@"%@", @"=== block copy前");
    NSLog(@"&a = %p, &b = %p", &a, &b);
    
    void(^block)() = ^
        NSLog(@"%@", @"=== Block");
        NSLog(@"&a = %p, &b = %p", &a, &b);
        NSLog(@"a = %d, b = %d", a, b = 456);
    ;
    block = [block copy];
    block();
    
    NSLog(@"%@", @"=== block copy后");
    NSLog(@"&a = %p, &b = %p", &a, &b);
    NSLog(@"a = %d, b = %d", a, b);
    
    [block release];

输出:

=== block copy前
&a = 0x7fff5fbff8bc, &b = 0x7fff5fbff8b0
=== Block
&a = 0x100201048, &b = 0x100201068
a = 123, b = 456
=== block copy后
&a = 0x7fff5fbff8bc, &b = 0x100201068
a = 123, b = 456

   可以看到,在Block执行中,他所引用的变量a和b都被复制到了堆上。而被标记__block的变量事实上应该说是被移动到了堆上,因此,当Block执行后,函数栈内访问b的地址会变成堆中的地址。而变量a,仍会指向函数栈内原有的变量a的空间。

四、So,Block属性的声明,首先需要用copy修饰符。Block默认存放在栈中,可能随时被销毁,需要作用域在堆中,所以只有copy后的Block才会在堆中,栈中的Block的生命周期是和栈绑定的


五、循环引用的问题

   循环引用是另一个使用Block时常见的问题。为什么会循环引用?因为retain。

     因为block在拷贝到堆上的时候,会retain其引用的外部变量,那么如果block中如果引用了他的宿主对象,那很有可能引起循环引用,如:

self.myblock = ^
    
    [self doSomething];
    
;

      下面是对ARC环境做的测试:

- (void)dealloc

    NSLog(@"no cycle retain");




- (id)init

    self = [super init];
    
    if (self) 
        
        
#if TestCycleRetainCase1
        
        
        
        //会循环引用
        
        self.myblock = ^
            
            [self doSomething];
            
        ;
        
#elif TestCycleRetainCase2
        
        
        
        //会循环引用
        
        __block TestCycleRetain *weakSelf = self;
        
        self.myblock = ^
            
            [weakSelf doSomething];
            
        ;
        
        
        
#elif TestCycleRetainCase3
        
        
        
        //不会循环引用
        
        __weak TestCycleRetain *weakSelf = self;
        
        self.myblock = ^
            
            [weakSelf doSomething];
            
        ;
        
        
        
#elif TestCycleRetainCase4
        
        //不会循环引用
        
        __unsafe_unretained TestCycleRetain *weakSelf = self;
        
        self.myblock = ^
            
            [weakSelf doSomething];
            
        ;
        
        
#endif
        
        NSLog(@"myblock is %@", self.myblock);
        
    
    
    return self;




- (void)doSomething

    NSLog(@"do Something");




int main(int argc, char *argv[]) 
    
    @autoreleasepool 
        
        TestCycleRetain* obj = [[TestCycleRetain alloc] init];
        
        obj = nil;
        
        return 0;
    


    经过上面的测试发现,在加了__weak__unsafe_unretained变量引入后,TestCycleRetain方法可以正常执行dealloc方法,而不转换和用__block转换的变量都会引起循环引用。
    因此防止循环引用的方法如下:  __weak TestCycleRetain *weakSelf = self;


   所以,在ARC下,由于__block抓取的变量一样会被Block retain,所以必须用弱引用才可以解决循环引用问题,ios 5之后可以直接使用__weak,之前则只能使用__unsafe_unretained了,__unsafe_unretained缺点是指针释放后自己不会置空。示例代码:

//iOS 5之前可以用__unsafe_unretained
//__unsafe_unretained typeof(self) weakSelf = self;
__weak typeof(self) weakSelf = self;
self.myBlock = ^(int paramInt)

    //使用weakSelf访问self成员
    [weakSelf anotherFunc];
;

在非ARC下,显然无法使用弱引用,这里就可以直接使用__block来修饰变量,它不会被Block所retain的,参考代码:

//非ARC
__block typeof(self) weakSelf = self;
self.myBlock = ^(int paramInt)

    //使用weakSelf访问self成员
    [weakSelf anotherFunc];
;


六、__weak 和 __block的区别


1. 循环引用时:有上文可知,__weak用在ARC环境时;__block仅可用在非ARC环境时(因为ARC环境下仍然会被retain)。

2. 修改局部变量时:需要加__block,否则不能在block中修改局部变量。如下:

__block int multiplier = 7;
     int (^myBlock)(int) = ^(int num) 
         multiplier ++;//这样就可以了
         return num * multiplier;
     ;



    

以上是关于iOS内存管理--Block属性用copy修饰 & 避免循环引用的问题的主要内容,如果未能解决你的问题,请参考以下文章

iOS block 为啥官方文档建议用 copy 修饰

iOS中为什么block用copy属性

iOS开发之Block

iOS内存管理--NSArray与NSMutableArray用copy修饰还是strong

iOS内存管理--NSArray与NSMutableArray用copy修饰还是strong

block要用copy修饰,还是用strong