zoukankan      html  css  js  c++  java
  • iOS Block 内存管理的探讨

    在很多情况下Block是造成程序循环引用内存泄漏的元凶。下面我们就讲解一下block对内存管理的影响。在讲解之前。希望大家对block有一定的了解。如果大家还不是太清楚block的实现原理。希望大家可以看看这篇文章。里面详细的介绍了block的实现过程。http://blog.devtang.com/2013/07/28/a-look-inside-blocks/。

    MRC和Block

    在MRC时代,block会隐式的对进入其作用域的对象(或者说被Block捕获的指针的指向的对象)加retain 来确保 block使用到该对象时,能够正确访问。

    这件事情在下面展示的代码中要格外小心

    MyViewController *myController = [[MyViewController alloc] init…];
    
    // 隐式地调用[myController retain];造成循环引用
    myController.completionHandler =  ^(NSInteger result) {
       [myController dismissViewControllerAnimated:YES completion:nil];
    };
    
    [self presentViewController:myController animated:YES completion:^{
       [myController release]; // 注意,这里调用[myController release];是在MRC中的一个常规写法,并不能解决上面循环引用的问题
    }];

    在这段代码中 myController的completionhandler调用了myController的方法[dismissController...];这个时候 completionhandler会对myController做retain操作。

    而我们知道myController对completionHandler也至少有一个retain(一般准确的讲是copy)这时就出现了内存管理中最糟糕的情况:循环引用。简单点说就是:myController retain了completionHandler,而completionHandler也retain了myController。循环引用导致了myController和completionHandler最终都不能被释放。我们在delegate关系中,对delegate指针用weak就是为了避免这种问题。

    不过好在,编译器会及时地给我们一个警告,提醒我们可能会发生这类型的问题:

    对这种情况 我们一般用如下的方法解决:给要进入Block的指针加一个__block修饰符。

    这个__Block在MRC时代有两个作用。

    1.说明变量可以被改变

    2.说明指针指向的对象不做这个隐式的retain操作。

    一个变量如果不加__block 是不能在Block中修改的。不过这有一个例外 static变量和全局变量不需要加__block就可以在Block中修改。

    使用这种方法,我们对代码做出修改 解决了循环引用的问题。

    MyViewController * __block myController = [[MyViewController alloc] init…];
    // ...
    myController.completionHandler =  ^(NSInteger result) {
        [myController dismissViewControllerAnimated:YES completion:nil];
    };
    //之后正常的release或者retain

    ARC和Block

    在ARC引入后,不再人为的添加Retain和release操作。情况也发生了改变。在ARC中任何情况下,__block 都只有一个作用:说明变量可以修改。即使加上了__block修饰符,一个被block捕获的请引用也依然是一个强引用。这样 在ARC的情况下,如果我们按照MRC下的写法,completionHandler对myController有一个强引用。myController对completionHandler有一个强引用,这依然是循环引用,没有解决问题:

    于是我们还需要对源码作出修改。简单的情况我们可以这样写

    __block MyViewController * myController = [[MyViewController alloc] init…];
    // ...
    myController.completionHandler =  ^(NSInteger result) {
        [myController dismissViewControllerAnimated:YES completion:nil];
        myController = nil;  // 注意这里,保证了block结束myController强引用的解除
    };

    在completionHandler之后将myController指针置nil,保证了completionHandler对myController强引用的解除,不过也同时解除了myController对myController对象的强引用。这种方法过于简单粗暴了,在大多数情况下,我们有更好的方法。

    这个更好的方法就是使用weak。(或者为了考虑iOS4的兼容性用unsafe_unretained,具体用法和weak相同,考虑到现在iOS4设备可能已经绝迹了,这里就不讲这个方法了)(关于这个方法的本质我们后面会谈到)

    为了保证completionHandler这个Block对myController没有强引用,我们可以定义一个临时的弱引用weakMyViewController来指向原myController的对象,并把这个弱引用传入到Block内,这样就保证了Block对myController持有的是一个弱引用,而不是一个强引用。如此,我们继续修改代码:

    MyViewController *myController = [[MyViewController alloc] init…];
    // ...
    MyViewController * __weak weakMyViewController = myController;
    myController.completionHandler =  ^(NSInteger result) {
        [weakMyViewController dismissViewControllerAnimated:YES completion:nil];
    };

    这样循环引用的问题就解决了,但是却不幸地引入了一个新的问题:由于传入completionHandler的是一个弱引用,那么当myController指向的对象在completionHandler被调用前释放,那么completionHandler就不能正常的运作了。在一般的单线程环境中,这种问题出现的可能性不大,但是到了多线程环境,就很不好说了,所以我们需要继续完善这个方法。

    为了保证在Block内能够访问到正确的myController,我们在block内新定义一个强引用strongMyController来指向weakMyController指向的对象,这样多了一个强引用,就能保证这个myController对象不会在completionHandler被调用前释放掉了。于是,我们对代码再次做出修改:

    MyViewController *myController = [[MyViewController alloc] init…];
    // ...
    MyViewController * __weak weakMyController = myController;
    myController.completionHandler =  ^(NSInteger result) {
        MyViewController *strongMyController = weakMyController;
    
      if (strongMyController) {
            // ...
            [strongMyController dismissViewControllerAnimated:YES completion:nil];
            // ...
        }
        else {
            // Probably nothing...
        }
    };

    到此,一个完善的解决方案就完成了.

    官方文档对这个问题的说明到这里就结束了,但是可能很多朋友会有疑问,不是说不希望Block对原myController对象增加强引用么,这里为啥堂而皇之地在Block内新定义了一个强引用,这个强引用不会造成循环引用么?理解这个问题的关键在于理解被Block捕获的引用和在Block内定义的引用的区别。为了搞得明白这个问题,这里需要了解一些Block的实现原理.关于block的实现原理。文章开头已经推荐了文章。大家可以看一下。

    为了更清楚地说明问题,这里用一个简单的程序举例。比如我们有如下程序:

    #include <stdio.h>
    
    int main()
    {
        int b = 10;
        
        int *a = &b;
        
        void (^blockFunc)() = ^(){
        
            int *c = a;
    
        };
        
        blockFunc();
        
        return 1;

    程序中,同为int型的指针,a是被Block捕获的变量,而c是在Block内定义的变量。我们用clang -rewrite-objc处理后,可以看到如下代码:

    原main函数:

    int main()
    {
        int b = 10;
    
        int *a = &b;
    
        void (*blockFunc)() = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a);
    
        ((void (*)(__block_impl *))((__block_impl *)blockFunc)->FuncPtr)((__block_impl *)blockFunc);
    
        return 1;
    }

    Block的结构:

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      
      int *a; // 被捕获的引用 a 出现在了block的结构体里面
      
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_a, int flags=0) : a(_a) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };

    实际执行的函数:

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      int *a = __cself->a; // bound by copy
    
    
            int *c = a; // 在block中声明的引用 c 在函数中声明,存在于函数栈上
    
        }

    我们可以清楚得看到,a和c存在的位置完全不同,如果Block存在于堆上(在ARC下Block默认在堆上),那么a作为Block结构体的一个成员,也自然会存在于堆上,而c无论如何,永远位于Block内实际执行代码的函数栈内。这也导致了两个变量生命周期的完全不同:c在Block的函数运行完毕,即会被释放,而a呢,只有在Block被从堆上释放的时候才会释放。

    回到我们的MyViewController的例子中,同上理,如果我们直接让Block捕获我们的myController引用,那么这个引用会被复制后(引用类型也会被复制)作为Block的成员变量存在于其所在的堆空间中,也就是为Block增加了一个指向myController对象的强引用,这就是造成循环引用的本质原因。对于MyViewController的例子,Block的结构体可以理解是这个样子

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      
      MyViewController * __strong myController;  // 被捕获的强引用myController
      
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_a, int flags=0) : a(_a) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };

    而反观我们给Block传入一个弱引用weakMyController,这时我们Block的结构:

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      
      MyViewController * __weak weakMyController;  // 被捕获的弱引用weakMyController
      
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_a, int flags=0) : a(_a) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };

    再看在Block内声明的强引用strongMyController,它虽然是强引用,但存在于函数栈中,在函数执行期间,它一直存在,所以myController对象也一直存在,但是当函数执行完毕,strongMyController即被销毁,于是它对myController对象的强引用也被解除,这时Block对myController对象就不存在强引用关系了!加入了strongMyController的函数大体会是这个样子:

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    
      MyViewController * __strong strongMyController = __cself->weakMyController; 
    
        // ....
    
        }

    综上所述,在ARC下(在MRC下会略有不同),Block捕获的引用和Block内声明的引用无论是存在空间与生命周期都是截然不同的,也正是这种不同,造成了我们对他们使用方式的区别。

    好的,最后再提一点,在ARC中,对Block捕获对象的内存管理已经简化了很多,由于没有了retain和release等操作,实际只需要考虑循环引用的问题就行了。比如下面这种,是没有内存泄露的问题的:

    TestObject *aObject = [[TestObject alloc] init];
        
    aObject.name = @"hehe";
    
    self.aBlock = ^(){
        
        NSLog(@"aObject's name = %@",aObject.name);
            
    };
  • 相关阅读:
    宿主机无法访问CentOS7上Jenkins服务的解决办法
    415. Add Strings
    367. Valid Perfect Square
    326. Power of Three
    258. Add Digits
    231. Power of Two
    204. Count Primes
    202. Happy Number
    172. Factorial Trailing Zeroes
    171. Excel Sheet Column Number
  • 原文地址:https://www.cnblogs.com/huanying2000/p/6183309.html
Copyright © 2011-2022 走看看