iOS開發筆記(一):記憶體管理

卡布達巨人發表於2017-12-28

這是我的第一篇文章,有什麼不對的地方還煩請大家指出啦,總覺得做開發很有必要做筆記,方便記錄自己的所得也能與大家探討,但是之前一直比較懶所以無所作為,那接下來的時間一起好好努力吧各位加油加油。

1.總起

1.1 瞭解記憶體管理

瞭解記憶體管理的最普通的方式是考慮所有權。如果Manny對Jack曾說過alloc,retain或者copy,則表示Manny已經宣稱對Jack的所有權。多個物件可以同時擁有Jack,但每個物件只負責正確地管理自己對Jack的所有權,最終釋放Jack是每個Jack所有者的責任,而Jack的非所有者永遠不需要釋放Jack。只要所有擁有Jack所有權的物件都以這種方式執行,Jack就不會洩漏也不會有任何指向Jack的指標留下來搖晃。

1.2 相關知識點

  • 野指標:指標變數沒有進行初始化或指向的空間已經釋放。

    • 使用野指標呼叫物件方法,會報異常,程式崩潰。
    • 通常在呼叫完release方法後,把儲存物件指標的地址清空(不是銷燬物件,引用計數為0時才會呼叫dealloc銷燬物件),賦值為nil,但OC中沒有空指標異常,所以[nil retain]呼叫方法不會有異常。
  • 記憶體洩漏:

    • 在ARC自動引用計數模式下,造成記憶體洩漏的情況:

      • 如Person *person = [Person new];(物件指標提前賦值nil或者清空,⚠️提前)在棧區中的person已經釋放,而堆區new產生的物件還沒有釋放,就會造成記憶體洩漏。
    • 在MRC手動引用計數模式下,造成記憶體洩漏的情況:

      • 沒有配對釋放,不符合記憶體管理原則。
      • 物件指標提前賦值nil或者清空,導致release不起作用。
  • 殭屍物件:堆中已經被釋放的物件(retainCount=0)。

  • 空指標:指標賦值為空(nil)。

提醒

一個變數的名稱,包括例項變數,只是一個指標。當你向該指標傳送訊息時,你實際上就是通過該指標將訊息傳送到它指向的物件。記憶體管理的規則是關於物件的原則,而不是關於名稱、引用或指標的規則。你不能遞增或遞減一個指標的保留計數,因為沒有這個東西。指標所佔用的記憶體是自動管理的(而且很小)。記憶體管理所關注的是指標所指向的物件。

2.MRC

2.1 實現原理

Objective-C物件中儲存著引用計數這一整數值。呼叫alloc或者retain方法後,引用計數+1。呼叫release後,引用計數-1。引用計數為0時,呼叫dealloc方法廢棄物件。

2.2 相關操作

物件操作 Objective-C方法 引用計數
生成並持有物件 alloc/new/copy/mutablecopy 1
持有物件 retain +1
釋放物件 release -1
廢棄物件 dealloc 0

2.3 記憶體管理的思考方式

  • 自己生成的物件,自己持有

      NSObject *obj = [[NSObject alloc] init];
      [obj retain];
      NSLog(@"obj - %lu",[obj retainCount]);
    複製程式碼
  • 非自己生成的物件,自己也能持有

      id obj = [NSMutableArray array];
      [obj retain];
      NSLog(@"obj - %lu",[obj retainCount]);
    複製程式碼
  • 不再需要自己持有的物件時釋放

      NSObject *obj = [[NSObject alloc] init];//自己生成自己持有
      [obj retain];
      NSObject *obj2 = [obj retain];//非自己生成,自己持有
      [obj release];
      [obj2 release];
      NSLog(@"obj - %lu",[obj retainCount]);
    複製程式碼
  • 非自己持有的物件自己無法釋放

      id obj = [NSMutableArray array];
      [obj release];
    複製程式碼

提醒

MRC下物件的引用計數可以通過[object retainCount]方法獲得。

2.4 NSAutoreleasePool

autorelease故名思議就是自動釋放,看上去很像ARC,但其實更類似於C語言中的區域性變數,也就是說,超出變數作用域的時候將自動被廢棄,但這裡與C語言不同的是,程式設計人員可以手動設定其作用域。 autorelease的具體使用方法如下:

  • 生成並持有NSAutoreleasePool物件
  • 呼叫已分配的autorelease方法
  • 廢棄NSAutoreleasePool物件

NSAutoreleasePool物件的生命週期相當於C語言變數的作用域,對於所有呼叫過autorelease方法的物件,在廢棄NSAutoreleasePool物件時,都將對物件統一呼叫release方法,程式碼如下所示:

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc]init] ;
id obj = [ [NSObject alloc]init];
[obj autorelease] ;
[pool drain];
複製程式碼

提醒

在Cocoa框架中,如果不是使用alloc/new/copy/mutablecopy這幾個方法返回的物件,其餘方法返回的物件都將自動註冊到NSAutoreleasePool中,id array = [NSMutableArray arrayWithCapacity:10];其實也就等同於:id array = [[[NSMutableArray alloc] initWithCapacity:1] autorelease];

3.ARC

ARC是Objective-C編譯器的特性,而不是執行時特性或者垃圾回收機制,ARC所做的只不過是在程式碼編譯時為你自動在何時的位置插入release或者autorelease,減少了開發的工作量。但我們有時仍需要四種所有權修飾符來配合ARC來進行記憶體管理。

3.1 四種所有權修飾符

  • __strong:強引用,持有所指向物件的所有權,無修飾符情況下的預設值。如需強制釋放,可置nil(這裡先建立一個指標,將新的值retain一次,將指標動態指向新的值,並將舊的值release一次)。

      NSObject *obj = [[NSObject alloc]init];
      //他們是等價的
      NSObject __strong *obj = [[NSObject alloc]init];
    複製程式碼
  • __weak:弱引用,不持有所指向物件的所有權,引用指向的物件記憶體被回收之後,引用本身會置nil,避免野指標.避免迴圈引用,會將物件註冊到autoreleasepool(既不保留新值,也不釋放舊值,動態地將指標指向新的值,如果這個值剛被dealloc,就會將指標更新為一個nil指標)。

  • unsafe_unretained:相當於assign。直接賦值。引用計數不變。他會發生野指標現象。所以不安全。不像weak,當指向的物件為空的時候,將指標置為nil。

  • _autoreleasing:將物件賦值給附有 _ autoreleasing 修飾符的變數等同於ARC 無效時呼叫物件的autorelease方法。

      @autoreleasepool {
      	id __autoreleasing obj = [[NSObject alloc] init];
      }
    複製程式碼

3.2 底層實現

  • __strong底層實現

    • alloc/new/copy/mutablecopy情況下

        //alloc為例的模擬底層程式碼為:
        id __Strong obj = [[NSObject alloc] init];
        id obj = objc_msgSend(NSobject, @selector(alloc));
        objc_msgSend(obj, @selector(init));
        objc_release(obj);
      
        //編譯器自動幫我們加入了release,來釋放物件
      複製程式碼
    • 非alloc/new/copy/mutablecopy情況下

        //array為例的模擬底層程式碼為:
        id obj = objc_msgSend(NSMutableArray, @selector(array));
        objc_retainAutoreleasedReturnValue(obj);
        objc_release(obj);
        
        //objc_retainAutoreleasedReturnValue(obj)是主要用於最優化程式執行
        
        //array方法的底層模擬為:
        id obj = objc_msgSend(NSmutableArray,@selector(alloc));
        objc_msgSend(obj,@selector(init));
        return objc_autorealeaseReturnValue(obj);
      複製程式碼

      objc_autorealeaseReturnValue(obj)與objc_retainAutoreleasedReturnValue(obj)是成對的,objc_autorealeaseReturnValue(obj)會把物件註冊到autorealeasepool中,但是如果它檢測到方法執行列表中出現objc_retainAutoreleasedReturnValue(obj)方法,那麼就不會將返回的物件註冊到autorealeasepool,而是直接傳遞到方法和函式的呼叫方。這樣直接傳遞可以達到最優化。

  • __weak底層實現

    • __weak原始碼

        id __weak obj1 = obj;
        
        //模擬程式碼
        id obj1;
        obj1 = 0;
        objc_storeWeak(&obj1,obj);
        objc_destoryWeak(&obj1);  等同於objc_storeWeak(&obj1,0);
      複製程式碼

      objc_storeWeak(&obj1,obj)函式將第二個引數的賦值物件的地址作為鍵值,將第一個引數的附有__weak修飾符的變數的地址註冊到weak表中,如果第二個引數為0,則把變數的地址從weak中刪除。一個鍵值可以註冊多個變數的地址由此可見,如果大量的weak變數,則會消耗CPU資源,所以weak 只用來避免迴圈引用。

    • __weak與@autoreleasepool

        id __weak obj1 = obj;
        NSLog(@"%@",obj1);
        
        //模擬程式碼
        id obj1;
        objc_initWeak(&obj1,obj);
        id temp = objc_loadWeakRetained(&obj1);
        objc_autorelease(temp);
        NSLog(@"%@",temp);
        objc_destoryWeak(&obj1);
      複製程式碼

      所以在@autorealeasepool塊結束前可以放心使用weak修飾變數

    • __autoreleasing底層實現

      將物件賦值給附有__autoreleasing修飾符的變數等同於ARC無效時,呼叫物件的autorelease方法。

      • 使用alloc/new/copy/mutableCopy時

          @autoreleasepool {
          	id __autoreleasing obj = [[NSObject alloc] init];
          }
          
          //模擬程式碼
          id pool = objc_autoreleasePoolPush();
          id obj = objc_msgSend(NSObject, @selector(alloc));
          objc_msgSend(obj, @selector(init));
          objc_autorelease(obj);
          objc_autoreleasPoolPop(pool);
        複製程式碼
      • 使用alloc/new/copy/mutableCopy以外的方法時

          @autoreleasepool {
            	id __autoreleasing obj = [NSMutableArray array];
          }
          
          // 模擬程式碼
          id pool = objc_autoreleasePoolPush();
          id obj = objc_msgSend(NSMutableArray,@selector(array));
          objc_retainAutoreleasedReturnValue(obj);
          objc_autorelease(obj);
          objc_autoreleasPoolPop(pool);
          // 雖然 obj 持有物件的方法變為 objc_retainAutoreleasedReturnValue, 但是將 obj 所引用的物件註冊到 autoreleasepool 中的方法並沒有改變
        複製程式碼

      關於@autoreleasepool,在ARC下應該使用@autoreleasepool而不是NSAutoreleasePool,@autoreleasepool的具體實現將在之後的文章中繼續探討。

3.3 使用規則

  • 不能使用retain/release/retainCount/autorelease
  • 不過載dealloc(如果是釋放物件記憶體以外的處理,是可以過載該函式的,但是不能呼叫[super dealloc])
  • 不能使用NSAllocateObject, NSDeallocateObject
  • 不能在C結構體中使用物件指標
  • id與void *間的如果cast時需要用特定的方法(__bridge關鍵字)
  • 不能使用NSAutoReleasePool,而需要使用@autoreleasepool塊
  • 不能使用區域(NSZone)

4.屬性的記憶體管理

ObjC2.0引入了@property,提供成員變數訪問方法、許可權、環境、記憶體管理型別的宣告,下面主要說明ARC中屬性的記憶體管理。屬性的引數分為三類,基本資料型別預設為(atomic,readwrite,assign),物件型別預設為(atomic,readwrite,strong),其中第三個引數就是該屬性的記憶體管理方式修飾,修飾詞可以是以下之一:

  • assign

    直接賦值,一般用來修飾基本資料型別。當然也可以修飾ObjC物件,但是不推薦,因為被assign修飾的物件釋放後,指標還是指向釋放前的記憶體,在後續操作中可能會導致記憶體問題引發崩潰。

      @property (nonatomic, assign) NSInteger count;
    複製程式碼
  • retain

    retain和strong一樣,都用來修飾ObjC物件,使用set方法賦值時,實質上是會先保留新值,再釋放舊值,再設定新值,避免新舊值一樣時導致物件被釋放的的問題。

      //MRC寫法如下
      - (void)setCount:(NSObject *)count {
      		[count retain];
       		[_count release];
       		_count = count;
       	}
       
     	//ARC對應寫法
     	- (void)setCount:(NSObject *)count {
      		_count = count;
      	}
    複製程式碼
  • copy

    一般用來修飾String、Dict、Array等需要保護其封裝性的物件,尤其是在其內容可變的情況下,因此會拷貝(深拷貝)一份內容給屬性使用,避免可能造成的對源內容進行改動。使用set方法賦值時,實質上是會先拷貝新值,再釋放舊值,再設定新值。實際上,遵守NSCopying的物件都可以使用copy,當然,如果你確定是要共用同一份可變內容,你也可以使用strong或retain。

  • weak

    ARC新引入修飾詞,可代替assign,比assign多增加一個特性(置nil)。weak和strong一樣用來修飾ObjC物件。使用set方法賦值時,實質上不保留新值,也不釋放舊值,只設定新值。

       @property (weak) id<MyDelegate> delegate;
    複製程式碼
  • strong

    ARC新引入修飾詞,可代替retain,ARC一般都寫strong。

    Person *per = [[Person alloc] init];

    self.person = per;

    如果是strong,物件的retainCount為2,如果為weak,物件的retainCount為1。

  • unsafe_unretained

    等價於assign,可以用來修飾資料型別和OC物件,但是不會使計數器加1,且物件銷燬時也不會將物件指向nil,容易造成野指標錯誤。

5.block的記憶體管理

OC中使用block必須自己管理記憶體,錯誤的記憶體管理將導致迴圈引用等記憶體洩漏問題,這裡主要說明在ARC下block宣告和使用的時候需要注意的兩點:

  • 如果你使用@property去宣告一個block的時候,一般使用copy來進行修飾(當然也可以不寫,編譯器自動進行copy操作),儘量不要使用retain。

      @property (nonatomic, copy) void(^block)(NSData * data);
    複製程式碼
  • block會對內部使用的物件進行強引用,因此在使用的時候應該確定不會引起迴圈引用,當然保險的做法就是新增弱引用標記。

      __weak typeof(self) weakSelf = self;
    複製程式碼

6.參考

相關文章