前言
大多數程式設計者在寫 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
的公開界面。實作此類別時,會需要 isa
和 refcount
兩個物件屬性 (第 9、10 行),故在此處宣告。
我們接著按照功能來看所需的公開訊息。首先是和建立物件相關的訊息 (第 14 至 16 行):
alloc
allocWithZone
new
由先前的文章可知,Objective-C 建立物件的方式是先配置 (allocate) 空物件,然後再初始化 (initialize) 該物件,所以需要實作 alloc
。至於 allocWithZone
只是為了相容舊程式碼,現在等同於 alloc
。
new
是結合 alloc
和 init
兩個訊息的訊息,相當於在 C++ 或 Java 中使用預設建構子建立物件。
和初始化相關的訊息是 init
(第 17 行)。這時候不需要額外參數。如果衍生類別在初始化時需要使用參數,就要實作另外一個訊息。
和拷貝物件相關的訊息是 copy
(第 18 行)。在基礎類別時不會拷貝物件,只會回傳原物件,這和 NSObject
的行為雷同。若衍生類別需要拷貝物件,則要自行實作 copy
。至於 copyWithZone
(第 19 行) 只是為了相容舊程式碼的實作,實際的功能等同於 copy
。
和釋放記憶體相關的訊息如下 (第 20 至 23 行):
retain
release
dealloc
autorelease
retain
和 release
的功能是相對的。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
class
和 superclass
用法相同。前者會取得 (類別或物件的) 類別,後者會取得 (類別或物件的) 父類別。由於 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
實作的方式和先前雷同,讀者應可自行閱讀。
當類別變多了,這樣的方式會出現許多重覆的程式碼。因此,本節所展示的手法只適用在類別數量很少的時候。