• 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);
            
    };
  • 相关阅读:
    Yocto开发笔记之《Tip-bitbake常用命令》(QQ交流群:519230208)
    Yocto开发笔记之《Tip-设置程序开机启动》(QQ交流群:519230208)
    Yocto开发笔记之《Tip-应用程序无法在目标机运行》(QQ交流群:519230208)
    激光雷达技术
    Yocto开发笔记之《网卡配置》(QQ交流群:519230208)
    Yocto开发笔记之《错误记录》(QQ交流群:519230208)
    Yocto开发笔记之《工具使用:TFTP & NFS & SSH》(QQ交流群:519230208)
    Yocto开发笔记之《U-boot启动内核流程》(QQ交流群:519230208)
    自定义选择提示框
    uitextfield输入字符限制
  • 原文地址:https://www.cnblogs.com/huanying2000/p/6183309.html
Copyright © 2020-2023  润新知