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。
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容