Book - Objective-C高級編程

1.1什麼是自動引用計數


關於ARC技術,最重要的還是下面這一點:

++在LLVM編譯器中設置ARC爲有效狀態,就無需再次鍵入retain或者是release代碼。++

1.2 內存管理/引用計數


1.2.1 概要

OC中的內存管理,也就是引用計數。可以用開關燈房間的案例來說明:

假設辦公室照明設備只有一個。上班的人進入辦公室需要照明,所以把燈打開。而隊對於下班離開辦公室的人來說,已經不需要照明瞭,所以把等關掉。若是很多上下班,每個人都開燈或者關燈,那麼辦公室的情況又將如何呢?最早下班的人關燈,那麼辦公室豈不是一片黑暗。

解決這個問題的辦法是使辦公室還有至少一個人的情況下保持開燈狀態,無人的時候保持關狀態。
1. 最早進入辦公室開燈
2. 之後進入辦公室的人,需要照明
3. 下班離開辦公室的人,不需要照明
4. 最後離開辦公室的人關燈(此時已無人需要照明)

根據計數功能來計算 ”需要照明的人數”

  1. 第一個人進入辦公室,辦公室的計數爲1。計數:1
  2. 之後進入辦公室的人,疊加1。計數:2
  3. 每當人有人下班離開,減去1。計數:1
  4. 最後離開辦公室,減去1。計數:0,因此要關燈。

在OC中,”對象” 相當於辦公室的照明設備。

  1. 開燈 –> 生成對象
  2. 需要照明 –> 持有對象
  3. 不再需要照明 –> 釋放對象
  4. 關燈 –> 廢棄對象

1.2.2 內存管理的思考方式

  • 自己生成的對象,自己持有。
  • 非自己生成的對象, 自己也能持有。
  • 不再需要自己持有的對象時釋放。
  • 非自己持有的對象無法釋放。
對象操作 Objective-C方法
生成並持有對象 alloc/new/copy/muetableCopy等方法
持有對象 retain 方法
釋放對象 release 方法
廢棄對象 dealloc 方法
自己生成的對象,自己持有

上面出現了很多 “自己” 一詞。本文所說的 “自己” 固然對應上面提到的 “對象使用環境”,但將之理解爲編程人員 “自身” 也是沒錯的。

下面寫出了自己生成並持有對象,我們使用alloc方法:

//自己生成並持有對象
id obj = [[NSObject alloc] init];

copy方法利用基於NSCopying方法約定,由各類實現copyWithZone:方法生成並持有對象的副本。與Copy方法類似,mutableCopy方法利用基於NSMutableCopying方法約定,由各類實現mutableCopyWhitZone:方法生成持有對象的副本。

這些方法生成的對象,雖然是對象的副本,但同alloc、new方法一樣,在 “自己生成並持有對象” 這點上沒有改變。

非自己生成的對象, 自己也能持有
//取得非自己生成並持有的對象
id obj = [NSMutableArray array];

上述源碼中,NSMutableArray類對象被賦給變量obj,但變量obj自己並不持有改對象。使用retain方法可以持有對象。

//持有對象
[obj retainl];
不再需要自己持有的對象時釋放

自己持有的對象,一旦不再需要,持有者有義務釋放該對象。釋放使用release方法。

//自己生成並持有對象
id obj = [[NSObject alloc] init];

//釋放
[obj release];
對象的持有與非持有原理

用 alloc/new/copy/mutableCopy 方法生成並持有的對象,或者用 retain 方法持有的對象,一旦不再需要,務必要用 release 方法進行釋放。

如果要用某個方法生成對象,並將其返還給該方法的調用方,那麼它的源碼又是怎麼樣的。

+ (NSObject *) allocObject {
    //自己生成並持有對象
    id obj = [[NSObject alloc] init];
    //自己持有對象
    retain obj;
}

原封不動地返回用 alloc 方法生成並持有對象,就能讓調用方也持有該對象。

// 取得非自己生成並持有的對象???
id obj1 = [NSObject allocObject];

allocObject與用alloc方法生成並持有對象的情況完全相同,所有使用allocObject方法也就意味着 “自己生成並持有對象”

那麼,調用 [NSMutableArray array] 方法使取得的對象存在,但是自己又不持有對象,又是怎麼實現的呢?

- (id)object {

    //自己生成並持有對象
    id  obj =  [[NSObject alloc] init];

    //使對象在超出指定的生成範圍時能夠自動並正確地的釋放
    [obj autorelease];

    //返回對象
    return obj;
}

上例中,我們使用了 autorelease 方法。用該方法,可以使取得的對象存在,但是自己不持有對象。

這樣的好處就是,我們可以不用去手動去管理通過此方法 生成的對象 的釋放。

autorelease 提供這樣的功能,是對象在超出指定的生成範圍時能夠自動並正確地釋放(調用release方法)

release 和 autorelease 的區別

使用NSMutableArray類的array類方法等可以取得誰都不持有的對象,這些方法都是通過 autorelease 而實現的。此外 ++根據命名規則,這些取得誰都不持有的對象的方法名不能以 alloc/new/copy/mutableCopy 開頭,這點需要注意。++

無法釋放非自己持有的對象

對於用 alloc/new/copy/mutableCopy 方法生成並持有的對象,或者是用 retain 方法持有的對象,由於持有者都是自己,所以在不需要該對象時需要將其釋放。

而由此以外所得到的對象絕對不能釋放。倘若在應用程序中釋放了非自己所持有的對象會造成崩潰。

例如自己生成並持有對象後,在釋放完後,再次釋放。(重複調用release);

或者是在 “取得對象的存在,但是自己不持有對象” 時釋放。

都會到導致應用程序崩潰!因此絕對不要釋放非自己持有的對象!

以上五項內容,就是 “引用計數式內存管理” 的思考方式。

1.2.3 alloc/reatin/release/dealloc 實現(翻牆失敗,過些天再細究)

我們來看看 GUNstep 源代碼中 NSObject 類的的 alloc 類方法。

id obj = [NSObject alloc];

上述調用 NSObject 類的 alloc 類方法在 NSObjecr.m 源代碼中的實現如下。

+ (id) alloc
{
   return [self allocWithZone: NSDefaultMallocZone()]; 
}
+ (id) allocWithZone:(NSZone *)z
{
    return NSAllocateObject(self,0,z);
}

通過 allocWithZone: 類方法調用 N\NSAllocateObject 函數分配對象。下面我們查看 NSAllocateObject 函數.

struct obj_layout {
    NSUInterger  retained;
}
inline id NSAllocateObject (Class aClass, NSUInterger extraBytes, NSZone *zone) {

    int size = 計算容納對象所需內存大小;
    id new = NSZoneMalloc(zone, size);
    memset(new, 0, size);
    new = (id)&((struct obj_layout *) new)[1];
}

NSAllocateObject 函數通過調用 NSZoneMalloc 函數來分配存放對象所需的內存空間,之後將改內存空間置0,最後返回作爲對象而使用的指針。

image

以下是去掉 NSZone 後簡化了源代碼:

struct obj_layout {
    NSUInterger  retained;
}
+ (id) alloc {
    int size = sizeof(struct obj_layout) + 對象大小;
    struct obj_layout *p = (struct obj_layout *)calloc(1, size);
    return (id)(p+1);
}

alloc 類方法用 struct obj_layout 中的 retained 整數來保存引用計數,並將其寫入對象內存頭部,該對象內存全部置0後返回。

以下用圖來展示有關 GUNstep 的實現,alloc類方法返回對象,如圖

image

對象的引用計數可通過 retainCount 實例方法來取得。

id obj = [NSObject alloc];
NSLog(@"retainCount=%d", [obj retainCount]); // => 1

執行alloc後對象的 retainCount 是 “1”。下面通過 GUNstep 的源代碼來確認。

- (NSUInteger)retainCount {
   return NSExtraRefCount(self) + 1; 
}
inline NSUInterger NSExtraRefCount(id  anObject) {
    return ((struct obj_layout *) anObject)[-1].retained;
}

由對象尋址找到對象內存頭部,從而訪問其中的retained變量。如圖:

image

1.2.4 autorelease

autorelease 就是自動釋放。這看上去很像ARC,但實際上它更類似於C語言中的自動變量(局部變量)的特性。

{
    int a;
}
/*
 因爲超出變量作用域,自動變量 "int a" 被廢棄,不可再訪問
*/

autorelease 會像 C 語言的自動變量那樣來對待對象實例。當超出其作用域(相當於變量作用域)時,對象實例的 release 實例方法被調用。另外,同 C 語言的自動變量不同的是,編程人員可以設定變量的作用域。

autorelease 的具體使用方法如下:

  1. 生成並持有 NSAutoreleasePool 對象
  2. 調用已分配對象的 autorelease 實例方法
  3. 廢棄 NSAutoreleasePood 對象

NSAutoreleasePool 對象的生命週期

NSAutoreleasePool 對象的聲明週期相當於 C 語言變量的作用域。對於所有調用過 autorelease 實例方法的對象,在廢棄 NSAutoreleasePool 對象時,都將調用 release 實例方法。如上圖。

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

id obj = [[NSObject alloc] init];

[obj autorelease];

[pool drain];

“[pool drain]” 等同於 “[obj release]”。

在 Cocoa 框架中,相當於程序主循環的 NSRunLoop 或者在其他程序可運行的地方,對 NSAutoreleasePool 對象進行生成、持有和廢棄處理。因此,應用程序開發者不一定非得使用 NSAutoreleasePool 對象來進行開發工作。

意思就是我們不一定需要去管理 NSAutoreleasePool。

這裏寫圖片描述

儘管如此,但在大量產生 autorelease 的對象時,只要不廢棄 NSAutoreleasePool 對象,那麼生成的對象就不能被釋放,因此有時會產生內存不足的現象。

典型的例子:讀入大量圖像的同時改變尺寸。圖像文件讀入到 NSData 對象,並從中生成 UIImage 對象,改變該對象尺寸後生成新的 UIImage 對象。這種情況下,就會大量生成 autorelease 對象。

for (int i = 0; i < 圖像數; ++i){
    /*
    讀入圖像
    大量產生 autorelease 對象
    由於沒有廢棄 NSAutoreleasePool 對象
    最終導致內存不足
    */
}

在此情況下,有必要在適當的地方生成,持有或者廢棄 NSAutoreleasePool 對象。

for (int i = 0; i < 圖像數; ++i){

    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    /*
    讀入圖像
    大量產生 autorelease 對象
    */

    [pool drain];
}

另外,Cocoa 框架中也有很多類方法用於返回 autorelease 的對象。比如 NSMutableArray 類的 arrayWithCapacity 類方法。

id array = [NSMutableArray arrayWithCapacity:1];

此源代碼等同於以下源代碼

id array = [[[NSMutableArray alloc] initWithCapacity:1] autorelease];

1.2.5 autorelease 實現 - GUNstep是如何實現的

[obj autorelease];

此源代碼調用 NSObject 類的 autorelease 實例方法。

GUNstep實現:

- (id) autorelease {
    [NSAutoreleasePool addObject:self];
}

autorelease 實例方法的本質就是調用 NSAutoreleasePool 對象的 addObject 類方法。

這裏寫圖片描述

+ (void) addObject:(id)anObj {

    NSAutoreleasePool *pool = 取得正在使用的 NSAutoreleasePool 對象;

    if (pool != nil) {
        [pool addObject: anObjc];
    }else{
        NSLog(@"NSAutoreleasePool 對象非存在狀態下調用 autorelease")
    }
}

addObject 類方法調用正在使用的 NSAutoreleasePool 對象的 addObject 實例方法。

如果嵌套或者持有 NSAutoreleasePool 對象,理所當然會使用最內側的對象。

- (void) drain {
    [self dealloc];
}
- (void) dellloc {
    [self emptyPool];
    [array release];
}
- (void) emptyPool {
    for (id obj in array){
        [obj release];
    }
}

雖然調用了好幾個方法,但是可以確定對於數組中的所以對象都調用了 release 實例方法。

1.2.6 蘋果的實現

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章