[Objective-C] 程式設計教學:記憶體管理 (Memory Management)

【分享文章】
Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

    前言

    在先前的文章中,我們談到如何建立和使用 Objective-C 物件。限於文章篇幅,當時沒有談到如何管理記憶體。本文延續 Objective-C 物件的主題,說明如何在 Objective-C 程式中管理記憶體。除了沿用原本 C 語言的記憶體管理模式外,Objective-C 發展出數個新的策略,我們會用範例分別展示其寫法。

    C 語言的記憶體管理模式

    在 C 語言中,記憶體管理有三個模式:

    • 靜態記憶體配置 (static memory allocation)
    • 自動記憶體配置 (automatic memory allocation)
    • 手動記憶體配置 (manual memory allocation)

    靜態記憶體用於儲存程式本身、全域變數 (global variable)、靜態變數 (static variable) 等。靜態記憶體不需人為介入,但只能用來初始化固定數值的變數,也無法在執行期動態決定記憶體大小;此外,靜態記體的大小是有限的,故無法把所有的變數都寫到靜態記憶體裡面。濫用全域變數往往造成程式難以維護,故我們會謹慎地使用全域變數。

    自動管理的記憶體儲存在堆疊 (stack) 中,會隨著函式的生命週期而消失;除了主函式外,寫在函式中的局部變數 (local variable) 都要注意生命週期的議題,以免得到垃圾值。自動管理的記憶體不需人為介入,但堆疊的容量大小有限,而且自動管理的值有生命週期的問題,所以也不會所有的資料都使用自動記憶體配置。

    手動管理的記憶體儲存在堆積 (heap) 中,這些記憶體可以跨越函式傳遞。如同其名,手動管理的記憶體需要人為配置和釋放。由於堆積可使用的記憶體上限約略等於系統所有的記憶體,會是 C 語言中主要的記憶體使用模式。

    註:此處的堆疊 (stack) 和堆積 (heap) 不是指資料結構 (data structures),而是記憶體的層級 (layout)。

    我們在這篇文章介紹 C 的指標和記憶體管理,有興趣的讀者可以參考一下。

    Objective-C 的記憶體管理策略

    Objective-C 大部分的物件都是配置在堆積中,故需要人為介入。Objective-C 在發展過程中,發展出以下數種策略:

    • 手動記憶體管理
    • 垃圾回收 (garbage collection)
    • 記憶體池 (memory pool)
    • ARC (automatic reference collecting)

    圾圾回收是即將棄置的 (deprecated) 特性,故本文不討論。

    手動記憶體管理需程式設計師自行配置和釋放記憶體,但 Objective-C 有引入和管理記憶體相關的訊息 (message),而不是用原本在 stdlib.h 中的函式。我們在後文中會有實際的範例來展示如何手動管理記憶體。

    記憶體池是半自動的記憶體管理方式。程式設計者需手動建立和釋放記憶體池,物件也要明確地傳入 autorelease 訊息。在 Objective-C 2.0 之前,記憶體池是主要的記憶體管理方式。

    ARC 則是 Objective-C 2.0 引入的新特性,可以省下手動管理記憶體的工夫。由於 ARC 僅 Clang 有支援,如果讀者的 Objective-C 程式碼要相容於 GCC 的話,就無法使用這項特性,只能繼續使用記憶體池。我們在後文中也會有範例來展示如何使用 ARC。

    手動配置和釋放 Objective-C 物件的記憶體

    在 Objective-C 中,同樣也可以手動管理記憶體,但不是用 stdlib.h 中的函式,而是用訊息傳遞的方式。以下是一個假想的例子:

    #import <Foundation/Foundation.h>                     /*  1 */
    #include <stdio.h>                                    /*  2 */
    
    int main(void)                                        /*  3 */
    {                                                     /*  4 */
        /* Allocate a NSMutableDictionary object. */      /*  5 */
        NSMutableDictionary *dict = \
            [[NSMutableDictionary alloc] init];           /*  6 */
        if (!dict) {                                      /*  7 */
            perror("Failed to allocate a dictionary\n");  /*  8 */
            goto ERROR;                                   /*  9 */
        }                                                 /* 10 */
    
        /* Do something on `dict`. */                     /* 11 */
    
        /* Release the object. */                         /* 12 */
        [dict release];                                   /* 13 */
    
        return 0;                                         /* 14 */
    
    ERROR:                                                /* 15 */
        /* Release the object only if it exists. */       /* 16 */
        if (dict)                                         /* 17 */
            [dict release];                               /* 18 */
    
        return 1;                                         /* 19 */
    }                                                     /* 20 */
    

    我們在第 6 行時為 dict 物件配置記憶體及初始化,最後在第 13 行釋放掉 dict 物件所占的記憶體。這和 C 風格的記憶體管理很像,allocrelease 的訊息成對出現。

    注意 Objective-C 的 allocrelease 訊息不能和純 C 的 malloc()free() 函式混用。使用什麼方式配置記憶體,就要用相對應的訊息或函式來釋放記憶體。

    但以下的物件則不應手動釋放記憶體:

    NSNumber *n = [NSNumber numberWithInteger: 12345];
    

    為什麼會有兩種相異的管理方式呢?這由程式是否對物件有所有權 (ownership) 來決定。

    當我們對物件用 allocnewcopymutableCopy 等訊息來產生時,我們對該物件就有所有權,這時候我們就要用 release 訊息歸還所有權。反之,我們使用其他訊息來產生物件時,我們對該物件就沒有所有權,這時候就不需要使用 release 來歸還所有權 (參考 Objective-C 的記憶體管理策略)。

    如果我們要確保物件的所有權,可以對物件傳遞 retain 訊息。前述連結有說明使用 retain 的時機,現在暫時用不到,只要知道 Objective-C 的物件有這個訊息即可。

    使用記憶體池 (Memory Pool) 半自動管理記憶體

    在 Objective-C 2.0 之前,我們要自行在程式中建立記憶池 (memory pool),再搭配 autorelease 訊息來使用。參考下例:

    #import <Foundation/Foundation.h>                                     /*  1 */
    #include <stdio.h>                                                    /*  2 */
    
    #define PRINT(FORMAT, ...) \
    fprintf(stderr, "%s\n", \
        [[NSString stringWithFormat:FORMAT, ##__VA_ARGS__] UTF8String]);  /*  3 */
    
    int main(void)                                                        /*  4 */
    {                                                                     /*  5 */
        /* Create an autorelease pool object. */                          /*  6 */
        NSAutoreleasePool* pool = \
            [[NSAutoreleasePool alloc] init];                             /*  7 */
        if (!pool) {                                                      /*  8 */
            perror("Failed to allocate a memory pool\n");                 /*  9 */
            return 1;                                                     /* 10 */
        }                                                                 /* 11 */
        
        /* Create a NSDate object with `autorelease` message. */          /* 12 */
        NSDate *now = [[[NSDate alloc] init] autorelease];                /* 13 */
        if (!now) {                                                       /* 14 */
            perror("Failed to allocate a date object\n");                 /* 15 */
            goto ERROR;                                                   /* 16 */
        }                                                                 /* 17 */
        
        PRINT(@"%@", [now description]);                                  /* 18 */
    
        /* Release all objects with `autorelease` message. */             /* 19 */
        [pool drain];                                                     /* 20 */
    
        return 0;                                                         /* 21 */
    
    ERROR:                                                                /* 22 */
        [pool drain];                                                     /* 23 */
    
        return 1;                                                         /* 24 */
    }                                                                     /* 25 */
    

    我們在第 7 行時建立 pool 物件,在第 20 行時釋放 pool 物件。這算是固定的使用方式。

    在建立 autorelease pool 後,要對新建立的物件傳遞 autorelease 訊息,該物件就會在 pool 釋放時一併釋放掉。以本例來說,我們在第 13 行中建立 NSDate 物件時,就一併傳遞了 autorelease 訊息。

    使用 ARC (Automatic Reference Counting) 自動管理記憶體 (Clang 限定)

    在 Objective-C 2.0 後,引入更方便的 @autorelease 區塊,就不該繼續使用前一節所提到的舊式語法,除非是為了繼續相容 GCC。

    我們將先前的例子利用 ARC 改寫如下:

    #import <Foundation/Foundation.h>
    
    #define PRINT(FORMAT, ...) \
    fprintf(stderr, "%s\n", \
        [[NSString stringWithFormat:FORMAT, ##__VA_ARGS__] UTF8String]);
    
    int main(void)
    {
        @autoreleasepool
        {
            NSDate *now = [[NSDate alloc] init];
    
            PRINT(@"%@", [now description]);
        }
    
        return 0;
    }
    

    由於整個程式碼包在 @autoreleasepool 區塊內,我們就不需要再對 now 物件傳遞 autorelease 訊息,轉由系統自動處理記憶體配置和釋放。

    Apple 公司在其官方手冊中提到使用 @autoreleasepool 區塊的效能會比使用記憶體池來得好,如果不需考慮編譯器相容性的話,應該優先使用這個手法管理記憶體。

    對 Objective-C 程式檢查記憶體

    原本用在 C 語言或 C++ 的知名記憶體檢查軟體 Valgrind 對 GNUstep 程式來說並不太合用,因為 GNUstep 現階段的實作會洩露少量記憶體,且目前沒有要改掉這項 bug (參考這裡)。

    替代的方式可以用 Clang 搭配 scan-build 對程式碼進行靜態檢查;由於 Clang 支援多個平台,所以這項工具不限於 Mac 平台。像是在 Ubuntu 或 Debian 的 clang-tools 套件中,就包括了該工具,而 MSYS2 則將該工具包在 clang-analyzer 套件中。

    在 MSYS2 中開啟 MSYS 終端機環境,輸入安裝指令如下:

    $ pacman -S mingw-w64-x86_64-clang-analyzer
    

    使用時,將原本的編譯指令當成 scan-build 的參數即可:

    $ scan-build clang -o prog prog.m -lobjc
    

    也可以搭配 Make 等編譯自動化軟體:

    $ scan-build make
    

    結語

    在本文中,我們介紹了 Objective-C 的記憶體管理方式。主要的使用考量是使用的編譯器。如果只用 Apple 平台的 Clang 編譯程式,使用 ARC 是最方便的。如果要相容 GCC 的話,則要手動建立記憶體池。

    【分享文章】
    Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email
    【追蹤網站】
    Facebook Twitter Plurk
    臉書討論區
    【支持本站】
    Buy me a coffeeBuy me a coffee