不使用 Foundation 物件庫寫 Objective-C 類別

    前言

    大多數程式設計者在寫 Objective-C 程式時,都會使用 Cocoa 或 GNUstep 所提供的物件。然而,有時候只是要透過 NSObject 取得基礎物件的特性,這時候 Foundation 物件庫就顯得太肥大了。在本文中,我們介紹不使用 Foundation 來實作 Objective-C 類別的方式。

    由於沒有 NSObject 可用,我們得自行填上這個空缺。本文的重點就在於自製基礎類別。這時候只會相依 libobjc 和 C 標準函式庫,完全不會相依到 Cocoa 或 GNUstep,可說是相當輕量。

    在製作基礎類別時,重點不在於完美複製 NSObject。由於 Cocoa 沒有公開程式碼,要完美複製 NSObject 相當困難。此外,也會做出許多沒用到的訊息。我們的目標應該是做出堪用的基礎類別,讓其他類別可以繼承該類別。

    自製基礎類別的外部界面

    本範例程式的基礎類別為 BaseClass,其公開界面如下:

    /* baseclass.h */                                                /*  1 */
    #pragma once                                                     /*  2 */
    
    #include <objc/objc.h>                                           /*  3 */
    
    #ifdef __clang__                                                 /*  4 */
    #pragma clang diagnostic ignored "-Wdeprecated-objc-isa-usage"   /*  5 */
    #pragma clang diagnostic ignored "-Wobjc-root-class"             /*  6 */
    #endif                                                           /*  7 */
    
    
    @interface BaseClass {                                           /*  8 */
        Class isa;                                                   /*  9 */
        int refcount;                                                /* 10 */
    }                                                                /* 11 */
    +(Class) class;                                                  /* 12 */
    +(Class) superclass;                                             /* 13 */
    
    +(id) alloc;                                                     /* 14 */
    +(id) allocWithZone: (void *)zone;                               /* 15 */
    +(id) new;                                                       /* 16 */
    
    -(id) init;                                                      /* 17 */
    
    -(id) copy;                                                      /* 18 */
    -(id) copyWithZone: (void *)zone;                                /* 19 */
    
    -(id) retain;                                                    /* 20 */
    -(void) release;                                                 /* 21 */
    -(void) dealloc;                                                 /* 22 */
    -(id) autorelease;                                               /* 23 */
    
    -(Class) class;                                                  /* 24 */
    -(Class) superclass;                                             /* 25 */
    
    -(BOOL) isKindOfClass: (Class)aClass;                            /* 26 */
    -(BOOL) isMemberOfClass: (Class)aClass;                          /* 27 */
    @end                                                             /* 28 */
    

    第 3 行引入 objc/objc.h,這是實作基礎類別時會用到的唯一相依函式庫。

    第 4 行至第 7 行關掉一些 Clang 的警告訊息,因為我們在實作 BaseClass 時不得不違反一些 Clang 的警告,所以就把不必要的訊息給關了。

    接下來是 BaseClass 的公開界面。實作此類別時,會需要 isarefcount 兩個物件屬性 (第 9、10 行),故在此處宣告。

    我們接著按照功能來看所需的公開訊息。首先是和建立物件相關的訊息 (第 14 至 16 行):

    • alloc
    • allocWithZone
    • new

    由先前的文章可知,Objective-C 建立物件的方式是先配置 (allocate) 空物件,然後再初始化 (initialize) 該物件,所以需要實作 alloc。至於 allocWithZone 只是為了相容舊程式碼,現在等同於 alloc

    new 是結合 allocinit 兩個訊息的訊息,相當於在 C++ 或 Java 中使用預設建構子建立物件。

    和初始化相關的訊息是 init (第 17 行)。這時候不需要額外參數。如果衍生類別在初始化時需要使用參數,就要實作另外一個訊息。

    和拷貝物件相關的訊息是 copy (第 18 行)。在基礎類別時不會拷貝物件,只會回傳原物件,這和 NSObject 的行為雷同。若衍生類別需要拷貝物件,則要自行實作 copy。至於 copyWithZone (第 19 行) 只是為了相容舊程式碼的實作,實際的功能等同於 copy

    和釋放記憶體相關的訊息如下 (第 20 至 23 行):

    • retain
    • release
    • dealloc
    • autorelease

    retainrelease 的功能是相對的。retain 會使物件內部的 refcount 加 1,而 release 會使物件內部的 refcount 減 1。當 refcount 降為 0 時,會自動呼叫 dealloc 訊息,將該物件所占用的記憶體釋放掉。

    由於本基礎類別不依賴 NSObject,實作此類別時得自行實作 dealloc。詳見後文。

    autorelease 是為了和記憶體池 (memory pool) 搭配而實作的訊息。理論上不使用 Cocoa 或 GNUstep 時,沒有記憶體池可用,不需要實作此訊息。但自己實作的物件仍有可能會和 Cocoa 或 GNUstep 混用,最好還是花時間實作一下。

    最後是和元程式 (metaprogramming) 相關的訊息 (第 12、13 行,第 24 至 27 行):

    • (類別的) class
    • (類別的) superclass
    • (物件的) class
    • (物件的) superclass
    • isKindOfClass
    • isMemberOfClass

    classsuperclass 用法相同。前者會取得 (類別或物件的) 類別,後者會取得 (類別或物件的) 父類別。由於 Objective-C 把類別本身當成物件,所以同名的訊息會有兩個版本。

    isKindOfClass 用來確認物件間的繼承關係,只要和物件同類別或父類別皆回傳 YES。相對來說,isMemberOfClass 則是要相同類別才會回傳 YES。由於 Objective-C 是動態語言,不建議在程式中檢查類別。重點應放在是否有實作特定訊息才是。

    自製基礎類別的內部實作

    在本節中,我們來看 BaseClass 的內部實作:

    /* baseclass.m */                                               /*   1 */
    #include <objc/objc.h>                                          /*   2 */
    #include <objc/runtime.h>                                       /*   3 */
    
    #include <math.h>                                               /*   4 */
    #include <stdlib.h>                                             /*   5 */
    #import "baseclass.h"                                           /*   6 */
    
    #ifdef __clang__                                                /*   7 */
    #pragma clang diagnostic ignored "-Wobjc-method-access"         /*   8 */
    #endif                                                          /*   9 */
    
    
    @implementation BaseClass                                       /*  10 */
    +(Class) class {                                                /*  11 */
        return self;                                                /*  12 */
    }                                                               /*  13 */
    
    +(Class) superclass {                                           /*  14 */
        return class_getSuperclass(self);                           /*  15 */
    }                                                               /*  16 */
    
    +(id) alloc {                                                   /*  17 */
        BaseClass *bc = \
            (BaseClass *) malloc(class_getInstanceSize(self));      /*  18 */
        if (!bc)                                                    /*  19 */
            return bc;                                              /*  20 */
    
        bc->isa = (id) self;                                        /*  21 */
    
        return bc;                                                  /*  22 */
    }                                                               /*  23 */
    
    /* Compatible with legacy code. */                              /*  24 */
    +(id) allocWithZone: (void *)zone {                             /*  25 */
        /* `zone` is just ignored. */                               /*  26 */
        return [self alloc];                                        /*  27 */
    }                                                               /*  28 */
    
    +(id) new {                                                     /*  29 */
        return [[self alloc] init];                                 /*  30 */
    }                                                               /*  31 */
    
    -(id) init {                                                    /*  32 */
        return self;                                                /*  33 */
    }                                                               /*  34 */
    
    -(id) copy {                                                    /*  35 */
        /* The base class of Objective-C
           doesn't really copy itself.
    
           Override it in its subclass. */
        return self;                                                /*  36 */
    }                                                               /*  37 */
    
    /* Compatible with legacy code. */                              /*  38 */
    -(id) copyWithZone: (void *)zone {                              /*  39 */
        /* `zone` is just ignored. */                               /*  40 */
        return [self copy];                                         /*  41 */
    }                                                               /*  42 */
    
    -(id) retain {                                                  /*  43 */
       __sync_fetch_and_add(&refcount, 1);                          /*  44 */
       return self;                                                 /*  45 */
    }                                                               /*  46 */
    
    -(void) release {                                               /*  47 */
       if (__sync_sub_and_fetch(&refcount, 1) < 0)                  /*  48 */
          [self dealloc];                                           /*  49 */
    }                                                               /*  50 */
    
    -(void) dealloc {                                               /*  51 */
        free(self);                                                 /*  52 */
    }                                                               /*  53 */
    
    -(id) autorelease {                                             /*  54 */
        if (objc_getClass("NSAutoreleasePool"))                     /*  55 */
            [objc_getClass("NSAutoreleasePool") addObject: self];   /*  56 */
    
        return self;                                                /*  57 */
    }                                                               /*  58 */
    
    -(Class) class {                                                /*  59 */
        return object_getClass(self);                               /*  60 */
    }                                                               /*  61 */
    
    -(Class) superclass {                                           /*  62 */
        return class_getSuperclass(object_getClass(self));          /*  63 */
    }                                                               /*  64 */
    
    -(BOOL) isKindOfClass: (Class)aClass {                          /*  65 */
       Class cls = object_getClass(self);                           /*  66 */
    
       while (cls != nil) {                                         /*  67 */
          if (aClass == cls)                                        /*  68 */
             return YES;                                            /*  69 */
    
          cls = class_getSuperclass(cls);                           /*  70 */
       }                                                            /*  71 */
    
       return NO;                                                   /*  72 */
    }                                                               /*  73 */
    
    -(BOOL) isMemberOfClass: (Class)aClass {                        /*  74 */
       return (object_getClass(self) == aClass) ? YES : NO;         /*  75 */
    }                                                               /*  76 */
    @end                                                            /*  77 */
    

    由於 Clang 會發出不必要的警告,此程式在第 8 行關掉該警告。

    alloc (第 17 至 23 行) 中,除了少數 Objective-C 特有函式外,基本上回歸 C 的動態記憶體配置。在 Objective-C 沒有提供功能時,還是要會用純 C 來補足。注意第 21 行的敘述是必要的,否則物件系統不會正常運作。

    現在的 Objective-C 類別在回應 allocWithZone 訊息 (第 25 至 28 行) 時,都直接忽略傳入的物件 zone。所以這個訊息只是間接呼叫 alloc 而已。

    注意我們沒有真正實作 copy 訊息 (第 35 至 37 行),這是為了相容於 NSObject 的行為。

    retain (第 43 至 46 行) 和 release (第 47 至 50 行) 可以窺見 Objective-C 管理記憶體的方式就是使用內部計數器 refcount。當 refcount 降至 0 時,就會自動呼叫 dealloc 來釋放記憶體。

    本基礎類別的 dealloc (第 51 至 53 行) 內部直接使用 C 的 free 函式,並沒有什麼特別的技巧。

    記憶體池內部的資料結構是指針堆疊,autorelease (第 54 至 58 行) 的動作就是把物件加入該堆疊。在記憶體池釋放時,會對堆疊中的物件自動傳遞 release 訊息,所以每個物件的 refcount 會滅 1。當某個物件的 refcount 降為 0 時,會自動釋放該物件的記憶體。

    在幾個和元程式相關的訊息中,可以看一下 isKindOfClass (第 65 至 73 行) 的實作。由於 Objective-C 靈活的特性,程式設計者可以直接在程式中操作程式本身的資訊,像是物件的類別。

    使用基礎類別的座標點 (Point) 類別

    做好基礎類別 BaseClass 後,我們以 Point (座標點) 類別來繼承該類別。以下是 Point 的外部界面:

    /* point.h */
    #pragma once
    
    #import "baseclass.h"
    
    
    @interface Point : BaseClass {
        double x;
        double y;
    }
    
    +(double) distanceFrom: (Point *)a to: (Point *)b;
    
    +(Point *) newWithX: (double)x andY: (double)y;
    
    -(Point *) init;
    -(Point *) initWithX: (double)x andY: (double)y;
    -(Point *) copy;
    
    -(double) x;
    -(double) y;
    @end
    

    由於和類別相關的特性都做在 BaseClass 了,這裡的 Point 所宣告的訊息相當簡單,而且大部分只和座標點的領域知識相關。例如,這裡沒有宣告 new 訊息,當我們用 [Point new] 建立新 Point 物件時,會自動轉為 [[Point alloc] init],這就是繼承自 BaseClass 的行為。

    以下是 Point 的內部實作:

    /* point.m */
    #include <math.h>
    #import "point.h"
    
    
    @implementation Point
    +(double) distanceFrom: (Point *)a to: (Point *)b {
        double dx = [a x] - [b x];
        double dy = [a y] - [b y];
    
        return sqrt(dx * dx + dy * dy);
    }
    
    +(Point *) newWithX: (double)x andY: (double)y {
        return [[[self class] alloc] initWithX: x andY: y];
    }
    
    -(Point *) init {
        return [self initWithX: 0.0 andY: 0.0];
    }
    
    -(Point *) initWithX: (double)_x andY: (double)_y {
        self = [super init];
    
        if (!self)
            return self;
    
        x = _x;
        y = _y;
    
        return self;
    }
    
    -(Point *) copy {
        return [[[self class] alloc] \
            initWithX: [self x] andY: [self y]];
    }
    
    -(double) x {
        return x;
    }
    
    -(double) y {
        return y;
    }
    @end
    

    這裡沒用到什麼複雜的數學,讀者應可自行閱讀。注意我們重新實作了 copy 訊息,讓該訊息有實質的功能。

    使用座標點 (Point) 類別的外部程式

    我們寫了個簡短的外部程式來使用 Point 類別,這時會間接使用到 BaseClass 類別:

    #include <assert.h>
    #include <stdlib.h>
    #import "point.h"
    
    #define ABS(n) ((n) > 0 ? (n) : -(n))
    #define IS_EQUAL(a, b, epsilon) (ABS(a - b) <= (epsilon))
    
    
    int main(void)
    {
        Point *a = NULL;
        Point *b = NULL;
        Point *c = NULL;
    
        a = [Point new];
        if (!a)
            goto ERROR_MAIN;
    
        b = [Point newWithX: 3.0 andY: 4.0];
        if (!b)
            goto ERROR_MAIN;
    
        c = [b copy];
        if (!c)
            goto ERROR_MAIN;
    
        assert(IS_EQUAL( \
            5.0, [Point distanceFrom: a to: c], 0.00001));
    
        assert([a isKindOfClass: [b superclass]]);
        assert([a isMemberOfClass: [c class]]);
    
        [c release];
        [b release];
        [a release];
    
        return 0;
    
    ERROR_MAIN:
        if (c)
            [c release];
    
        if (b)
            [b release];
    
        if (a)
            [a release];
    
        return 1;
    }
    

    除了一般的座標點計算外,我們刻意使用幾個和元程式相關的訊息,確認 BaseClass 的實作是正常的。

    (替代法) 不使用基礎類別的座標點 (Point) 類別

    本範例實作了兩個類別,這兩個類別間有繼承關係。這樣的實作泛用性比較好,但實作起來會比較複雜。如果讀者不想用這種方式,也可以直接在 Point 類別中實作類別的功能,這樣就不用考慮複雜的繼承關係。

    我們將 Point 的外部宣告修改如下:

    /* point.h */
    #pragma once
    
    #include <objc/objc.h>
    
    #ifdef __clang__
    #pragma clang diagnostic ignored "-Wdeprecated-objc-isa-usage"
    #pragma clang diagnostic ignored "-Wobjc-root-class"
    #endif
    
    
    @interface Point {
        Class isa;
        int refcount;
    
        double x;
        double y;
    }
    
    +(id) alloc;
    +(id) new;
    +(Point *) newWithX: (double)x andY: (double)y;
    
    +(double) distanceFrom: (Point *)a to: (Point *)b;
    
    -(Point *) init;
    -(Point *) initWithX: (double)x andY: (double)y;
    -(Point *) copy;
    
    -(id) retain;
    -(void) release;
    -(void) dealloc;
    -(id) autorelease;
    
    -(double) x;
    -(double) y;
    @end
    

    為了簡化程式碼,這裡放棄和元程式相關的訊息,只實作和記憶體管理相關的訊息。

    再將 Point 的內部實作修改如下:

    /* point.m */
    #include <objc/objc.h>
    #include <objc/runtime.h>
    
    #include <math.h>
    #include <stdlib.h>
    #import "point.h"
    
    
    @implementation Point
    +(id) alloc {
        Point *pt = \
            (Point *) malloc(class_getInstanceSize(self));
        if (!pt)
            return pt;
    
        pt->isa = (id) self;
    
        return (id) pt;
    }
    
    +(id) new {
        return [[self alloc] init];
    }
    
    +(double) distanceFrom: (Point *)a to: (Point *)b {
        double dx = [a x] - [b x];
        double dy = [a y] - [b y];
    
        return sqrt(dx * dx + dy * dy);
    }
    
    +(Point *) newWithX: (double)x andY: (double)y {
        return [[self alloc] initWithX: x andY: y];
    }
    
    -(Point *) init {
        return [self initWithX: 0.0 andY: 0.0];
    }
    
    -(Point *) initWithX: (double)_x andY: (double)_y {
        if (!self)
            return self;
    
        x = _x;
        y = _y;
    
        return self;
    }
    
    -(Point *) copy {
        return [[object_getClass(self) alloc] \
            initWithX: [self x] andY: [self y]];
    }
    
    -(id) retain {
       __sync_fetch_and_add(&refcount, 1);
       return self;
    }
    
    -(void) release {
       if (__sync_sub_and_fetch(&refcount, 1) < 0)
          [self dealloc];
    }
    
    -(void) dealloc {
        free(self);
    }
    
    -(id) autorelease {
        if (objc_getClass("NSAutoreleasePool"))
            [objc_getClass("NSAutoreleasePool") addObject: self];
    
        return self;
    }
    
    -(double) x {
        return x;
    }
    
    -(double) y {
        return y;
    }
    @end
    

    實作的方式和先前雷同,讀者應可自行閱讀。

    當類別變多了,這樣的方式會出現許多重覆的程式碼。因此,本節所展示的手法只適用在類別數量很少的時候。

    【分享本文】
    Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Yahoo
    【追蹤本站】
    Facebook Facebook Twitter