位元詩人 [Objective-C] 程式設計教學:不使用 Foundation 物件庫寫 Objective-C 類別

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

前言

大多數程式設計者在寫 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

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

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

關於作者

身為資訊領域碩士,位元詩人 (ByteBard) 認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

位元詩人喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,位元詩人將所學寫成文章,放在這個網站上和大家分享。