iOS内存管理理解

在Build Phases -> Compile Sources -> 对应的文件加上-fno-objc-arc的编译参数可以启用MRC模式

1.简介

1.1 内存管理的思考方式

  • 自己生成的对象,自己持有
  • 非自己生成的对象,自己也能持有(指针指向这个对象,引用计数+1,就是持有)
  • 不再需要自己持有的对象时释放
  • 非自己持有的对象无法释放

1.2 ARC下的内存管理

ARC虽然能够解决90%的内存管理问题,另外还有10%的需要开发者自己处理

  1. 过度使用block,无法解决循环引用问题
  2. 遇到底层Core Foundation对象,需要自己手动管理它们的引用计数时

2.引用计数(Reference Count)

2.1 什么是引用计数

引用计数是一种管理对象生命周期的方式,一个内存块被强指针指向的数量。

2.2 alloc/retain/release/dealloc

cocoa框架中foundation框架类库的NSObject类是担负着内存管理的

  • alloc: 生成并持有对象。
    自己持有的对象可以使用alloc/new/copy/mutableCopy开头命名,如果自己不持有不能是有这些作为开头。
  • retain:持有对象。
    引用计数+1,超过最大数时会报错。寻址找到对象内存地址头部,减去存放引用计数值的struct的大小的地址,获取到计数值并+1,当计数值为超过最大值时抛出异常代码。
  • release:释放对象。
    引用计数-1,如果释放了非自己持有的对象或者释放了引用计数为0的对象,程序会崩溃。寻址找到对象内存地址头部,减去存放引用计数值的struct的大小的地址,获取到计数值并-1,当计数值为0时调用dealloc方法。
  • dealloc:废弃对象。
    废弃由alloc开辟的内存块

指向一个内存块的每一条指针,都能使用retain和release对这个内存块的引用计数进行改变。

//开辟内存块,引用计数为1
id object1 = [[NSObject alloc] init];
//object2的指针指向object1创建的内存块,内存块的引用计数还是1
id object2 = object1;
//object2使用retain是引用计数+1
[object2 retain];
//object1使用release将内存块上的引用计数-1
[object1 release];
//object1还是可以使用release将内存块的引用计数再-1
//此时内存块上的引用计数值为0,内存空间被系统回收
//tips:系统知道马上就要回收内存了,没必要-1了,直接回收对象
[object1 release];

如果想要销毁一个对象,不仅需要使用release将引用计数-1,还需要将对象的指针置为nil

id object1 = [[NSObject alloc] init];
[object1 release];
object1 = nil;

2.3 引用计数的实现

GNUstep中的实现:采用在内存块头部放置引用计数来进行管理

  1. 分配存放对象所需要的内存空间,将该内存空间置0
  2. 用一个整数来记录retain的引用计数值,并把这个整数写到内存空间的头部
  3. 返回对象指针

GNUstep优点:

  1. 少量代码就能完成
  2. 能够统一管理引用计数用的内存块和对象用内存块

苹果源码中的实现:采用引用技术表(散列表)管理引用计数。

  1. 分配存放对象所需要的内存空间,将空间置0
  2. 在散列表中加入引用计数并附带对象的内存块地址
  3. 返回对象指针

苹果源码优点:

  1. 对象用内存块分配无需考虑内存块头部
  2. 引用技术表各记录中存有内存块地址,可从各个记录追溯到各个内存块地址,在调试时,只要引用计数表没有被破坏,就可以确认内存块位置,就可以检测各对象持有者是否存在。

2.4 别向已经释放的对象发消息

当一个对象引用计数为0时,系统已经将该对象的地址回收,它的输出结果是不一定的,如果被回收的地址为空,则会输出0,如果地址被内存复用了,就会造成系统崩溃。

NSObject *object = [[NSObject alloc] init];
NSLog(@"Reference Count = %u", [object retainCount]);
[object release];
//此时object的内存被释放了,输入为0或者崩溃
NSLog(@"Reference Count = %u", [object retainCount]);

3.循环引用(Reference Cycle)

如果对象A的成员变量是对象B,对象B的成员变量是A,释放对象A需要先释放成员变量B,而释放成员变量B也需要先释放成员变量A,形成一个无法释放的循环,造成内存泄漏,这就是循环引用。

- (void)viewDidLoad {
    [super viewDidLoad];
    CustomView *view = [[CustomView alloc] init];
    [self.view addSubview:view];
    view.callbackBlock = ^{
        //view持有的block中调用了self,意味着view成员变量的一根指针指向了self
        [self doSomething];
    }
}

当 viewDidLoad 方法执行时,创建一个 block 并赋值给对象 view 的 callbackBlock 属性,callbackBlock捕捉 self,self 持有 self.view, v 在 addSubview 后成为 self.view 的子 view 而被 self.view 持有,这样就形成了一个引用循环,self -> self.view -> view -> callBackBlock -> self。

环路越是大的循环引用,越难以被发现。

打破循环方法1:主动断开循环引用

根据业务逻辑主动断开引用,在view 执行完callbackBlock后,将block置为nil,主动断开循环引用。

if (self.callbackBlock) {
    self.callbackBlock();
    //调用方将指向callbackBlock的指向指向了nil,主动释放block
    self.callbackBlock = nil;
}

循环变成在view -> callBackBlock处被主动断开,循环引用打破

解决方法2:弱引用

以代理模式为例,对象A中创建对象B并拿到它的delegate,如果delegate为strong声明的,则会形成循环引用A -> B -> BDelegate -> A,而事实上delegate对象通常都被声明为weak型变量,就是为了避免循环引用,从BDdelegate -> A处打破循环引用。

系统对于每一个有弱引用的对象,都维护一个表来记录它所有的弱引用的指针地址。每当一个对象的引用计数为 0 时,系统就通过这张表,找到指向对象的所有弱引用指针,把它们置成 nil。

weakSelf 和 strongSelf:

weakSelf是将一根weak声明的指针指向了self,在self -> self.view -> view -> callBackBlock -> self循环中,weakSelf使用弱引用的方式从callBackBlock -> self处打破循环引用。

- (void)viewDidLoad {
    [super viewDidLoad];
    CustomView *view = [[CustomView alloc] init];
    [self.view addSubview:view];
    __weak __typeof__(self) weakSelf = self;
    view.callbackBlock = ^{
        [weakSelf doSomething];
    }
}

但是使用weakSelf存在一个 callbackBlock 执行时self对象会释放的问题,如果在刚刚执行callbackBlock 中的方法时 weakSelf 就为 nil了,那么callbackBlock中的所有方法中的weakSelf都会是nil,callbackBlock的输出结果是唯一的,不会造成什么影响。但是如果self是在callbackBlock执行到一半的时候释放的,就会导致 callbackBlock出现多种不同的执行结果,这种时候就需要利用strongSelf来保证在所在block的作用域中self不被释放。

- (void)viewDidLoad {
    [super viewDidLoad];
    CustomView *view = [[CustomView alloc] init];
    [self.view addSubview:view];
    //将指向self的指针变成弱指针,这样不会造成循环引用
    __weak __typeof__(self) weakSelf = self;
    view.callbackBlock = ^{
        //保证在所在block的作用域中self不被释放
        //如果不加strongSelf,可能会出现在doSomething时self还存在,而在执行doAnoterThing时,self变成了nil
        __typeof__(self) strongSelf = weakSelf;
        [strongSelf doSomething];
        [strongSelf doAnoterThing];
    }
}

在嵌套block中,每个block中都需要设置strongSelf。

DemoViewController.m

- (void)viewDidLoad {
    [super viewDidLoad];
    CustomView *view = [[CustomView alloc] init];
    [self.view addSubview:view];
    //将指向self的指针变成弱指针,这样不会造成循环引用
    __weak __typeof__(self) weakSelf = self;
    view.callbackBlock = ^{
        //保证在callbackBlock作用域内的self一直不释放
        //如果self进入block时就为nil,则一直为nil。
        __typeof__(self) strongSelf = weakSelf;
        [strongSelf doSomething];
        [strongSelf doAnoterThing];
        obj.objCallbackBlock = ^{
            //需要重新设置strongSelf,保证在objCallbackBlock作用域内的self一直不释放
            __typeof__(self) strongSelf = weakSelf;
            [strongSelf doObjSomething];
            [strongSelf doObjAnotherThing];
        }
    }
}

4.Core Foundation

  • __bridge
    只做类型转换,不修改相关对象的引用计数,原来的 Core Foundation 对象在不用时,需要调用 CFRelease 方法。
    • Core Foundation对象 -> Objective-C对象
      NSMutableString *mString;
      CFMutableStringRef cfstr;
      {
      //通过CFCreate系列方法创建,内存块引用计数为1
      CFMutableStringRef cfstring = CFStringCreateMutable(kCFAllocatorDefault, 1);
      //由于mString是__strong修饰符修饰的,mString指向内存快时引用计数++
      //引用计数为2
      mString = (__bridge NSMutableString *)cfstring;
      cfstr = cfstring;
      NSLog(@"retain count = %ld",CFGetRetainCount(cfstring));
      }
      //由于CF对象不会自动释放,所以引用计数为2
      NSLog(@"retain count = %ld",CFGetRetainCount(cfstr));
    • Objective-C对象 -> Core Foundation对象
      CFMutableStringRef cfstr;
      {
      //通过alloc方法创建内存块,内存块引用计数为1
      NSMutableString *mString = [[NSMutableString alloc] init];
      //通过__bridge转换,内存块上的引用计数不变,依旧为1
      cfstr = (__bridge CFMutableStringRef)mString;
      NSLog(@"retain count = %ld",CFGetRetainCount(cfstr));
      }
      //内存块通过Arc机制释放,cfstr所指为野指针,崩溃或为未知对象
      NSLog(@"retain count = %ld",CFGetRetainCount(cfstr));
  • __bridge_retained
    类型转换后,将相关对象的引用计数加 1,原来的 Core Foundation 对象在不用时,需要调用 CFRelease 方法。
    • Core Foundation对象 -> Objective-C对象 ERROR
      NSMutableString *mString;
      CFMutableStringRef cfstr;
      {
      //通过CFCreate系列方法创建,内存块引用计数为1
      CFMutableStringRef cfstring = CFStringCreateMutable(kCFAllocatorDefault, 1);
      //由于mString是__strong修饰符修饰的,mString指向内存快时引用计数++
      //但由于是__bridge_retained,引用计数又会+1,在ARC下编译会报错
      mString = (__bridge_retained NSMutableString *)cfstring;
      cfstr = cfstring;
      NSLog(@"retain count = %ld",CFGetRetainCount(cfstring));
      }
      NSLog(@"retain count = %ld",CFGetRetainCount(cfstr));

    • Objective-C对象 -> Core Foundation对象
      CFMutableStringRef cfstr;
      {
      //通过alloc方法创建内存块,内存块引用计数为1
      NSMutableString *mString = [[NSMutableString alloc] init];
      //因为是__bridge_retained 所以引用计数++
      //内存块引用计数为2
      cfstr = (__bridge_retained CFMutableStringRef)mString;
      NSLog(@"retain count = %ld",CFGetRetainCount(cfstr));
      }
      //ARC下出了作用域,mString被释放,引用计数--
      //引用计数为1
      NSLog(@"retain count = %ld",CFGetRetainCount(cfstr));

  • __bridge_transfer
    类型转换后,将该对象的引用计数交给 ARC 管理,Core Foundation 对象在不用时,不再需要调用 CFRelease 方法。
    • Core Foundation对象 -> Objective-C对象
      NSMutableString *mString;
      CFMutableStringRef cfstr;
      {
      //通过CFCreate系列方法创建,内存块引用计数为1
      CFMutableStringRef cfstring = CFStringCreateMutable(kCFAllocatorDefault, 1);
      //因为是__bridge_transfer 所以引用计数交由OC对象管理
      //引用计数为1 不变
      mString = (__bridge_transfer NSMutableString *)cfstring;
      cfstr = cfstring;
      NSLog(@"retain count = %ld",CFGetRetainCount(cfstring));
      }
      NSLog(@"retain count = %ld",CFGetRetainCount(cfstr));

    • Objective-C对象 -> Core Foundation对象 ERROR
      CFMutableStringRef cfstr;
      {
      //通过alloc方法创建内存块,内存块引用计数为1
      NSMutableString *mString = [[NSMutableString alloc] init];
      //因为是__bridge_transfer 所以引用计数交由CF对象管理
      //在ARC下编译会报错
      cfstr = (__bridge_transfer CFMutableStringRef)mString;
      NSLog(@"retain count = %ld",CFGetRetainCount(cfstr));
      }
      NSLog(@"retain count = %ld",CFGetRetainCount(cfstr));

  • 总结
    1. Objective-C对象 -> Core Foundation对象用__bridge或__bridge_retain。
    2. Core Foundation对象 -> Objective-C对象用__bridge或__bridge_transfer。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容