Objective-C 內存管理深入

前言

基礎篇介紹了一些關于Objective-C內存管理的常見概念。本文將在前文的基礎上擴展以下知識:成員變量set方法內存分析屬性屬性特質內存分析等內容,內容淺顯易懂、屬于學習總結。如果有需要瀏覽上一篇文章的同學請點擊Objective-C 內存管理基礎。希望本文能給正在學習Objective-C的小伙伴們更多啟發。

成員變量的set方法

在屬性(property) 這個語法提出之前,iOS開發者常常這樣構建一個類:

@interface Person : NSObject
{
   NSUInteger _number;
}
- (void)setNumber:(NSUInteger)number;
- (NSUInteger)number;

如上的代碼為Person類添加了一個成員變量_number(添加下劃線是為了命名規范)。同時手動申明和實現該成員變量的存取方法:setter方法用于寫入值,getter方法用于讀取值。 這樣的寫法看起來像是一個世紀之前的事情了,但我們能夠從中探究到很多關于內存管理方面的問題。例如:

  • 基本數據類型直接賦值
    由于_numberNSUInteger類型,所以其不再內存管理的范圍,對于基本類型的setter方法只需要直接賦值就好了。
    - (void)setNumber:(NSUInteger)number
{
        _number = number;
}
  • ObjC 對象類型需要進行內存管理
@class Book;
@interface Person : NSObject
{
    Book *_book;
}
  - (void)setBook:(Book *)book;
  - (Book *)book;

上面的Person類存在一個繼承自NSObject類型的成員變量,當實現其setter方法時就需要做到以下幾點了。

  • retain 傳入的變量
    根據內存管理原則:“需要持有對象時就對其做一次retain操作”。所以應該對傳入的book做一次retain操作。初步代碼如下:
@implementation Person
    - (void)setBook:(Book *)book
{   
       _book = [book retain]; //持有該對象對book做一次retain操作
}
@end
  • 對象銷毀時 release 其成員變量
    Person對象銷毀時代表_book沒有人使用了,應該在Persondelloc方法里做一次release操作。
@implementation Person
    - (void)setBook:(Book *)book
{   
       _book = [book retain]; //持有該對象對book做一次retain操作
}
    - (void)dealloc
{
       [_book release]; //Person對象銷毀時不再擁有_book,需要對其做一次release操作
       [super dealloc]; 
}
@end
  • ** release 舊的成員變量**
    上面的代碼非常不嚴謹,因為當出現下面的情況時,舊的成員變量無法被釋放。
  Book *b1 = [Book new];
  [p setBook:b1];
  Book *b2 = [Book new];
  [p setBook:b2];; //重新為_book賦不同的值
  [b2 release];
  [b1 release];

由于setter方法對傳入的b1b2都做了一次retain,在Person對象的delloc方法里卻只對當前的_book(也就是b2) release,導致舊的成員變量b1無法被釋放,由此產生了內存泄露問題。對setter方法優化后的代碼應該如下:

    - (void)setBook:(Book *)book
{
      [_book release]; //對原來使用的成員變量做一次release操作
      _book = [book retain]; //再持有新傳入的變量
}  
  • 判斷傳入的變量
    即便是上面的代碼仍然還有問題,比如:
   Person *p = [Person new];
   Book *b1 = [Book new];
   [p setBook:b1];
   [b1 release];
   [p setBook:b1];; //對_book 重新賦相同的值
   [p release];

[b1 release]代碼執行完畢,b1的引用計數器值為 1 ,接著執行[p setBook:b1]p.book重新賦相同的值時,會進行下面的操作:

     [_book release]; 
     _book = [book retain];

【注意】:此時的_book是第一次設置的變量b1,其引用計數器值為1;再進行release,變量b1的引用計數變為0,系統回收該對象內存;接著執行
_book = [book retain]將發生錯誤,因為此時的b1已經是僵尸對象retain無法將一個僵尸對象起死回生(需要開啟僵尸對象檢測功能)。
【解決方案】:應該對傳入的變量(book)進行判斷,如果傳入的變量(book)和當前成員變量(_book)是同一個變量,那么setter方法不需要做任何操作。

最后的一個完整的setter方法應該如下:

  - (void)setBook:(Book *)book
{
       if (book != _book) {      //兩者進行判斷
          [_book release];       //對原來的成員變量進行release
          _book = [book retain]; //對傳入的變量進行retain
       }
}

屬性

屬性(property)是Objective-C 2.0引入的新特性,其通過將成員變量包裝達到封裝對象中數據的作用,并且提供了“點語法”使開發者更簡單的依照類對象訪問其中的數據。具體來說使用屬性構建類主要有以下好處:

  • 自動合成存取方法
    使用屬性封裝數據可以讓編譯器自動申明和實現與屬性相關的存取方法,此過程有一個專業名稱--“自動合成”(synthesize)。而且該過程是在編譯執行的期間自動完成的,因此無法看到 “合成方法(synthesized method)”的源碼。
@class Book;
@interface Person : NSObject
@property Book *book;
@end
@class Book;
@interface Person : NSObject
{
      Book *_book;
}
    - (void)setBook:(Book *)book;
    - (Book *)book;

上面兩段代碼實際上是等效的。

  • 自動生成對應的成員變量
    除了自動生成方法代碼外,編譯器還會自動向類中添加屬性對應的成員變量,并且在屬性名前面加上下劃線,以此作為成員變量的名稱并和屬性名區分開來。
    - (void)dealloc
{
      [_book release];
      NSLog(@"Person - dealloc");
      [super dealloc];
}

在上面使用屬性語法的Person類中,重寫dealloc能調用[_book release]說明的確是生成了屬性對應的成員變量_book

  • 支持自定義成員變量名
    如果開發中對編譯器自動生成的成員變量名不滿意,使用@synthesize語法在類的實現文件里可以自定義其名稱。
    注意:該語法是將已經存在的屬性名替換成左邊自定義的名稱,以后屬性對應的系統生成的帶有下劃線的成員變量名就被替換成了自定義的名字。同時其存取方法中使用到的成員變量名也將被替換。但是通過點語法調用self.property是不受影響的。
@implementation Person
@synthesize book = _myBook;
 //自定義的_myBook名稱用來替換之前的屬性名稱,
//但是通過點語法調用self.book是不受影響的。
    - (void)dealloc
{
      [_myBook release];
      // [self.book release]; 這行代碼和上面那行代碼是一樣的。
      NSLog(@"Person - dealloc");
      [super dealloc];
}
@end
  • 支持自定義成員變量存取方法
    1,如果通過屬性語法自動生成的gettersetter方法不能滿足開發要求,我們可以重寫屬性對應的gettersetter方法,達到自定義存取方法的目的。
    2,使用dynamic關鍵字,該語法告知編譯器不要為某些屬性創建對應的成員變量和默認實現的存取方法(這樣做的后果是屬性對應的成員變量和其存取方法都需要自己定義和實現)。
@implementation Person
@dynamic book;
    - (void)dealloc
{
      [_book release]; //此行代碼報錯,具體原因為_book成員變量沒有申明
      NSLog(@"Person - dealloc");
      [super dealloc];
}
@end

通過上面的解釋我們得出這樣的結論:
屬性 = 成員變量 + 存取方法 (@property = ivar + getter + setter)

屬性語法極大的簡化了開發人員在構建類時封裝數據的工作量(編譯器默認實現)。另外需要注意的是 synthesize、**dynamic **關鍵字用的較少,因為大部分時候編譯器默認實現的代碼還是比較符合開發需求的。


屬性特質內存分析

在上面講解屬性特性時申明的屬性,仍然是不符合要求的。

@class Book;
@interface Person : NSObject
@property Book *book;
@end

這樣的定義的屬性,其setter方法僅僅類似于普通的基本數據類型直接賦值,驗證代碼如下:

 Person *p = [Person new];
 Book *b = [Book new];
 p.book = b;
  NSLog(@"%lu",b.retainCount); //打印b的引用計數器值為 1 
 [b release];
 [p release];

通過打印b的引用計數器值(b.retainCount = 1),我們發現代碼p.book = b執行后,b的引用計數并沒有增加;如果此時進行的是“處理過的setter方法”,b必定會被retain一次,其引用計數值應該為2,所以得出結論:單純的@property Book *book定義屬性,其setter方法只是進行了簡單的賦值運算,并不符合Objc對象需要進行內存管理的原則。因此在MRC中我們常常這樣改進:

@class Book;
@interface Person : NSObject
@property (retain) Book *book;
@end

通過使用retain編譯器會將該屬性對應的setter方法替換成上面提到的 “完整的setter方法”,進而解決內存管理問題。 在ARC中對于屬性后面的修飾詞處理的更加嚴謹和豐富,具體來說引入了 屬性特質(attributes)概念。

  • 屬性特質(attributes)
    屬性特質(attributes)也可被稱為屬性修飾詞屬性特性。通過分析源碼可以一探究竟。
    propertyruntime中是objc_property_t,其結構如下:
typedef struct objc_property *objc_property_t;

objc_property是一個結構體,包括nameattributes,其結構如下:

struct property_t {
    const char *name;
    const char *attributes;
};

attributes本質是objc_property_attribute_t,定義了property的一些特性,其結構如下如下:

typedef struct {
    const char *name;           /**< The name of the attribute */
    const char *value;          /**< The value of the attribute (usually empty) */
} objc_property_attribute_t;

attributes主要描述特性有:原子性和非原子性、讀寫權限、存取方法名、內存管理語義等。

  • 修飾詞對內存的影響(ARC)

  • 原子性
    atomic:原子性的(編譯器默認該特性)。該特性會為屬性的setter方法加鎖,以保證其操作是線程安全的,同時會影響性能。

    nonatomic:非原子性的。該特性不會為屬性的setter方法加鎖,非線程安全的,但是能極大的提升性能。由于在iOS開發中移動設備性能有限,所以絕大多數情況下使用的該特性。

    iOS中使用同步鎖的開銷加大,這會帶來性能問題。一般情況下并不要求屬性必須是“原子性”的,因為這并不能保證線程安全。若要實現“線程安全”的操作,還需采用更深層的鎖定機制才行。開發iOS程序是一般都會使用nonatomic特性,因為atomic會嚴重影響性能,但是在開發Mac OS X 程序時,使用atomic不會有性能瓶頸。

  • 讀寫權限

readwrite:可讀可寫的(編譯器默認該特性)。該特性表明編譯器會為其生成對應的gettersetter方法。同時你可以設置和讀取該值。

readonly:只讀的。該特性修飾的屬性你將不能直接修改其值,例如使用點語法賦值時報錯,提示不能為readonly 的屬性賦值(但是可以通過KVC機制為該屬性賦值)。

  • 內存管理語義

    assign:直接賦值。用于Objective-C中基本數據類型的變量作為屬性,該特性修飾的屬性其setter方法只會針對“純量類型”進行簡單的賦值操作。例如枚舉類型、布爾類型、整型、浮點型等基本數據類型的變量(基本數據類型變量無需內存管理)。

    weak:非持有關系 。用于Objective-C中對象作為屬性。同assign類似該特性修飾的屬性其setter方法既不保留新值,也不釋放舊值;然而在屬性所指的對象銷毀時,屬性值會被清空。

    ARC下,在有可能出現循環引用情況中往往要通過讓其中一端使用weak來解決,比如:兩個類相互為對方的屬性、申明delegate代理屬性等。另外自身已經對它進行一次強引用,沒有必要再強引用一次,此時也會使用weak,比如:添加子控件是常常用到[self.view addSubView: self.imageView]self.imageView這個屬性就可以使用weak定義。xib中拖線時IBOutlet控件屬性一般也使用weak(當然,也可以使用strong)。

    如果你對 “屬性所指的對象銷毀時,屬性值會被清空”不是很理解,可以參考下面的例子:

@interface ViewController ()
@property (nonatomic, weak) UIView *redView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UIView *redView = [[UIView alloc] init]; //創建子視圖
redView.backgroundColor = [UIColor redColor]; //設置子視圖背景色
[self.view addSubview:redView]; //添加到視圖上
self.redView = redView; //屬性賦值
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"%@",self.redView);
self.redView.frame = CGRectMake(50, 50, 100, 100); //點擊屏幕設置尺寸
}
@end

     【分析】示例代碼將`redView`添加的控制器的`View`上,當用戶點擊屏幕的時候設置其尺寸顯示出來。對于一般繼承自`NSObject`類型的變量作為屬性時通常使用`strong`修飾。這里的`redView`明顯是對象類型,其使用`weak`修飾是因為`[self.view addSubview:redView]`該行代碼將`redView `作為一個元素加入到`self.view.subviews`數組中,`self.view`本身隱式的對`redView `做了一次持有操作。

    (1)當將`[self.view addSubview:redView]`代碼注釋。

    控制臺輸出:`self.redView = (null)`

    【分析】`self.view`未對`self.redView`進行持有操作(`weak`修飾只是簡單的賦值,未對傳入的變量進行`retain`),當`viewDidLoad `方法調完,系統為`UIView`對象在堆區分配出來的空間(`[[UIView alloc] init]`)就被回收了,所以此時`self.redView`指向的對象已經被銷毀,同時由于其是`weak`修飾,編譯器將其指向`nil`,所以打印`self.redView`時其值為`null`。

    (2)如果此時將`redView`聲明為`assign`修飾,即:

@property (nonatomic, assign) UIView *redView;

點擊屏幕時就會崩潰。
![壞內存訪問錯誤](http://upload-images.jianshu.io/upload_images/2474121-15ca43eb54eabcc3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

    【分析】在屬性所指的對象銷毀時,`assign`不會將屬性指向`nil`,所以此時`self.redView`已經是一個野指針其指向了一個僵尸對象,打印`self.redView`時訪問了一塊已經被回收的內存。所以崩潰。
希望能通過這個簡單的例子解釋一下`weak`和`assign`的區別。

   **unsafe_unretained:**非持有關系(同`assign`)。用于Objective-C中對象或者是基本數據類型的變量作為屬性。同`weak`一樣該特性修飾的屬性其`setter`方法既不保留新值(`unretained`),也不釋放舊值。不同的是如果屬性所指的對象被銷毀,指向該對象的指針依然指向原來的內存地址,如果此時繼續訪問該對象很容易產生壞內存訪問/野指針/僵尸對象訪問(`unsafe`)。

    **strong:**持有關系。用于Objective-C中對象作為屬性。該特性修飾的屬性的`setter`方法會對傳入變量做一次`retain`并對之前的成員變量做一次`release`。其作用和MRC下的`retain`操作是一樣的。

   **copy:**拷貝關系。`copy` 的作用與`strong`類似,然而其修飾的屬性的`setter`方法并非`retain`新值,而是將其“拷貝” (`copy`)一份。因為父類指針可以指向子類對象(多態性),使用`copy`的目的是為了讓本對象的屬性不受外界影響,無論給對象傳入是一個可變對象還是不可對象,對象本身持有的就是一個不可變的副本。

    >定義`NSString`、`NSArray`、`NSDictionary`等類型的變量作為屬性時常使用`copy`關鍵字, 因為其有對應的可變類型:
`NSMutableString`、`NSMutableArray`、`NSMutableDictionary`(可變類型用`strong`)。
開發中常用的`block`代碼塊習慣使用`copy`關鍵字修飾(`strong`也是可以的)。

  * **存取方法名**
 **getter = <name>:**通常在為類定義布爾類型的屬性時用于自定義其`getter`方法名。例如:

/// 是否正在工作
@property (nonatomic, assign, getter=isWorking) BOOL working;
/// 是否有訂單有顯示中
@property (nonatomic, assign, getter=isShowing) BOOL showing;


   **setter = <name>:**該修飾詞極少使用,除非特殊情況下,例如:
>在數據反序列化、轉模型的過程中,服務器返回的字段如果以`init`開頭,所以你需要定義一個`init`開頭的屬性,但默認生成的`setter`與`getter`方法也會以`init`開頭,而編譯器會把所有以`init`開頭的方法當成初始化方法,而初始化方法只能返回`self`類型,因此編譯器會報錯。這時你就可以使用下面的方式來避免編譯器報錯:

@property(nonatomic, copy, getter=p_initBy, setter=setP_initBy:) NSString *initBy;


  * **空值約束(iOS9推出的新特性)**

   **nullable:**該屬性值可以為空。用于Objective-C中對象作為屬性,表示該屬性的`getter`和`setter`方法中賦值和取值都是可以為空。
盡管在定義屬性時寫或不寫`nullable `對該屬性的賦值和操作沒有任何影響,但寫上`nullable `更多的作用在于程序員之間的溝通交流,時刻提醒開發人員該屬性不一定是有值的,可能需要空值判斷,要注意使用。 

    **nonnull:**該屬性值不能為空。和`nullable `對立,該特質修飾的屬性其`getter`和`setter`均不能為空值,否則會有警告。
另外下面的寫法是等價的。

@property (nonnull,nonatomic, copy) NSArray *array;
@property (nonatomic, copy) NSArray * __nonnull array1;


    `nullable`、`nonnull`除了在定義屬性時使用,還可以用在函數和方法中對參數和返回值的空值進行約束。例如:

//函數
void text(NSArray * __nonnull array) {
}
//方法
- (NSString *__nonnull)creat:(NSArray * __nonnull)array {
return @"coderYQ";
}

![nullable和nonnull的使用](http://upload-images.jianshu.io/upload_images/2474121-8cb863891576d6cc.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

    當然如果你嫌麻煩,還可以使用宏一次性聲明:

NS_ASSUME_NONNULL_BEGIN
@interface ViewController ()
@property (nonatomic, copy) NSArray *array;
@property (nonatomic, copy) NSArray *array1;
@property (nonatomic, copy) NSArray *array2;
@end
NS_ASSUME_NONNULL_END

如此兩個宏`NS_ASSUME_NONNULL_BEGIN `、`NS_ASSUME_NONNULL_END `之間定義的屬性均有`nonnull `提示,當然你也可以修改其中的約束,例如:

NS_ASSUME_NONNULL_BEGIN
@interface ViewController ()
@property (nullable, nonatomic, copy) NSArray *array;
@property (null_resettable, nonatomic, copy) NSArray *array1;
@property (nonatomic, copy) NSArray *array2;
@end
NS_ASSUME_NONNULL_END


    **null_resettable:**該特性修飾的屬性表示其`setter`中可以賦值為空,但是`getter`方法中返回的值不會為空。使用該特性定義屬性時編譯器會提出警告:

    應該重寫`setter`方法處理賦值為空的情況。
    ![null_resettable的使用](http://upload-images.jianshu.io/upload_images/2474121-72ca8eb876123438.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
一個`null_resettable `經典的使用示例就是蘋果定義`UIViewController`的`view`屬性:

@property(null_resettable, nonatomic,strong) UIView *view;

![null_resettable的使用](http://upload-images.jianshu.io/upload_images/2474121-c61f1fc4a4261aab.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

   大家都知道控制器的`view`是懶加載的,每次調用`getter`方法一旦發現`view`屬性為空,系統會調用`loadView`方法創建并返回`view`。該關鍵字告訴開發人員`self.view`是永遠不會為空的 ,你可以放心的使用。

  由于在構建類時為其定義的各種屬性有不同的類型,所以可以通過屬性修飾詞對存取方法進行微調進而滿足內存管理規范。
>但是開發中有時候會重寫`getter`或`setter`方法。這時我們應該保證實現的方法是具備相關屬性的特質。例如將某個屬性申明為`copy`,那么重寫`setter`方法時應該拷貝傳入的對象,否則誤導該屬性的使用者,嚴重時會產生bug。同理在其他方法中設置屬性值時,也需要遵循屬性修飾詞的規定。

  例如:在下面的自定義初始化方法中`name`應該使用`copy`,而`age`則直接賦值。

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;

  • (instancetype)initWithName:(NSString *)name
    age:(int)age;
    @end

@implementation Person

  • (instancetype)initWithName:(NSString *)name age:(int)age
    {
    if (self = [super init]) {
    _name = [name copy];
    _age = age;
    }
    return self;
    }
    @end

---
##屬性和成員變量的選擇使用
在上面的`- (instancetype)initWithName: age:`初始化方法中,初學者可能會有疑問為什么使用的`_name`成員變量而不是`self.name`屬性呢?這里本人通過查閱《Effective Objective-C 2.0 編寫高質量iOS與OS X代碼的52個有效方法》對屬性和成員變量的使用場景做了一些總結:

 * 初始化方法中設置屬性值時使用成員變量,因為子類可能會覆蓋該屬性的`setter`方法。例如:
  • (instancetype)initWithName:(NSString *)name age:(int)age
    {
    if (self = [super init]) {
    _name = [name copy];
    _age = age;
    }
    return self;
    }

 * 懶加載方法中使用成員變量初始化,并通過屬性訪問。例如:
  • (Book *)book
    {
    if (!_book) {
    _book = [[Book alloc] init];
    }
    return _book;
    }

self.book //懶加載的屬性必須通過“getter”方法訪問,否則成員變量永遠不會被初始化
因為若沒有使用getter方法直接訪問成員變量(_book),該成員變量(_book)此時尚未設置完成。

 * 在`delloc`方法中使用成員變量
  • (void)dealloc
    {
    [_book release];
    [super dealloc];
    }

* 在對象內部盡量直接訪問成員變量

關于屬性和成員變量使用的更多細節請閱讀《Effective Objective-C 2.0 編寫高質量iOS與OS X代碼的52個有效方法》第6、7條。

##文章最后
以上就是筆者對于Objective-C內存管理深入知識的學習總結,部分描述引自《Effective Objective-C 2.0 編寫高質量iOS與OS X代碼的52個有效方法》這里做出說明。

如果文中有任何紕漏或錯誤歡迎在評論區留言指出,本人將在第一時間修改過來;喜歡我的文章,可以關注我以此促進交流學習; 如果覺得此文戳中了你的G點請隨手點贊;轉載請注明出處,謝謝支持。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • iOS開發中, 之前一直使用swift, 因此對于Objective-C的內存管理機制長期處于混亂的一知半解狀態....
    icetime17閱讀 871評論 1 8
  • Objective-C 1. import的用法 拷貝文件內容可以自動防止文件的內容被重復拷貝(#define宏定...
    馬文濤閱讀 5,359評論 3 17
  • *面試心聲:其實這些題本人都沒怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個offer,總結起來就是把...
    Dove_iOS閱讀 27,211評論 30 472
  • 跟你一起其實是個意外。從沒想過我們會一起走這么長時間,結婚、生子、生活、奮斗…… 也許愛情里不僅有一見鐘情、更多的...
    紫mum閱讀 194評論 0 0
  • “對了,聊了這么久還不知道小兄弟來這里做什么呢?”刀疤爽朗的問道,蕭峰沉思了會回道:“大哥,你也別小兄弟這么叫了,...
    一點鑰匙閱讀 187評論 0 0