Objective-C 物件導向(一)——物件的本質

Neo_joke發表於2018-12-09

對於一門程式語言而言,當初學者掌握了其基本語法和標準庫的使用以後,如果能夠繼續瞭解該語言的核心思想與底層實現,則會漸入佳境,窺探語言實現的本質技術,能夠為以後的效能優化以及規避技術陷阱等複雜工作提供思路。 瞭解Objective-C語言的物件導向的本質,有益於程式設計師更加深刻理解該語言是如何實踐OOP思想,並使得在構建物件導向程式的時候更加遊刃有餘。

背景知識

Objective-C是C語言的超集,也就是說,C語言的語法全部被Objective-C相容,而物件導向的特性則是建立在C語言的基礎之上,當熟悉過C語言的指標、記憶體管理、自定義資料-結構體等一系列知識以後,對於Objective-C的物件導向實現的理解,就容易多了,因為本質上,Objective-C的物件導向,就是使用這些東西構建出來的。 我們需要了解的是,對於C語言來說,除了語言本身定義的資料型別,程式設計師想要自定義資料型別以供程式設計使用,結構體是必然選擇,基於這樣的事實,那麼理應能夠猜到,Objective-C中的一切物件導向概念,諸如類、物件等,都是基於C語言的結構體之上構建的,而如何進行物件方法的呼叫、類方法呼叫等等,則通過Objective-C從smalltalk借鑑過來的訊息呼叫思想而實現的Runtime思想,後者是訊息呼叫思想的鼻祖。

什麼是類和物件

C語言是沒有物件導向概念的,只有基本資料型別、指標、結構體等等。那麼如何通過這些概念構建物件導向的概念,要明白這個的前提是大體總結一下物件和類有什麼特點。

類是描述一個物件規格的模板,即它說明了一個物件有什麼樣的屬性,有什麼樣的方法。物件的構建,通過指定類,並且初始化,從而得到類的例項-物件,那麼也就是說類是一種描述例項物件的資料結構。 在Objective-C中,標準庫為Foundation,事實上幾乎所有的類都繼承與NSObject,那麼類具體有如下表現

  1. 類具有方法和類方法的宣告,描述物件例項有什麼方法和類有什麼方法
  2. 類具有屬性的宣告,描述物件例項有什麼樣的屬性
  3. 類可以被整合或整合其他類,從而給他人提供或從父類獲取物件描述的規格資訊

物件

物件是一個根據類例項化出來的資料結構。具有例項方法,例項變數,物件沒有繼承概念,只有持有其他物件或被其他物件持有,具有以下特點。

  1. 物件具有例項方法
  2. 物件具有例項屬性
  3. 物件可以被其他物件持有或持有其他物件

類和物件的實現

既然類和物件只不過是特點不同的自定義資料型別,那麼類和物件必然要使用結構體實現,Objetcive-C也是這樣設計的,我們看一下NSObject的定義:

類的定義(NSObject)

*@interface* NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
+ (void)load;
+ (void)initialize;
- (instancetype)init
#if NS_ENFORCE_NSOBJECT_DESIGNATED_INITIALIZER
    NS_DESIGNATED_INITIALIZER
#endif
    ;
+ (instancetype) new  OBJC_SWIFT_UNAVAILABLE("use object initializers instead");
+ (instancetype)allocWithZone:(struct _NSZone )zone OBJC_SWIFT_UNAVAILABLE("use object initializers instead");

*@end*
複製程式碼

如何通過C的資料結構實現Objective-C的物件和類

NSObject的定義中,有類方法定義、屬性定義、例項方法定義,如何使用C語言的結構體來表達和儲存這樣的自定義資料結構呢?NSObject是一個Class也就是一個類,在描述中有一個Class isa的變數,按圖索驥查詢到Class的資料結構:

typedef struct objc_class *Class; //class是一個objc_class結構體的指標
struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE; //objc_class是一個結構體,描述了一個類的資訊
複製程式碼

從上面定義可以看出來,一個類包含哪些資訊,是通過objc_class 結構體來表示的,NSObject的定義中,有一個Class isa屬性,而Class是一個指向objc_class結構體的指標,也就是說,NSObject通過isa指標來尋找到其類的資訊所在的結構體。 該結構體中有幾個比較重要的變數:

  • Class isa OBJC_ISA_AVAILABILITY; 又是一個指向objc_class結構體的指標,指向另外一個類資訊的結構體,那麼一個類指向一個說明其規格的類結構體,其意義是來描述類的資訊,一般稱描述類的結構的資料型別稱之為元類,即meta-class,以為著使用元類來描述類的規格,那麼從物件與類的關係類比中,可以將類看作是元類的例項,也就說,元類是類物件的類。
  • super_class 是該類父類的資訊,使用super_class指標,找到父類資訊的結構體。NSObject的例項物件的superclass為null
  • long info OBJC2_UNAVAILABLE; 類資訊,供執行期使用的一些位標識
  • long instance_size OBJC2_UNAVAILABLE; /該類的例項變數大小
  • struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; 類的成員變數連結串列
  • struct objc_method_list methodLists OBJC2_UNAVAILABLE; 方法定義的連結串列
  • struct objc_cache *cache OBJC2_UNAVAILABLE; 方法快取,物件接到一個訊息會根據isa指標查詢訊息物件,這時會在methodLists中遍歷,如果cache了,常用的方法呼叫時就能夠提高呼叫的效率。
  • struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; 協議連結串列

類的例項->物件也是通過一個objc_class結構體描述其結構。如下:

struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};

typedef struct objc_object *id;
複製程式碼

id 型別,即物件,其為一個指向objc_object結構體的指標,意味著任意一個Objective-C的物件,其本質是一個指向objc_object的結構體指標,而objc_object結構體中,有一個isa指標,指向objc_class結構體,來描述其屬於哪個類,也就是上面的類資訊結構體。

檢視根類、元類與子類的具體實現

當定義一個類的時候,如下:

//main.m
@interface ClassA : NSObject
@property(nonatomic,copy)NSString * name;
-(void)sayHello;
+(void)SayHello;
@end
@implementation ClassA
-(void)sayHello{
    NSLog(@"object say hello");
}
+(void)SayHello{
    NSLog(@"class say hello");
}

@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"Hello, World!");
        NSObject * obj1 = [[NSObject alloc]init];
        ClassA * obj2 = [[ClassA alloc]init];
        [obj2 sayHello];
        [ClassA SayHello];
        NSLog(@"%@", NSStringFromClass([obj1 superclass]));
    }
    return 0;
}
複製程式碼

從上面的定義中,按照之前的說明,將該檔案轉換為C++程式碼,將在C++程式碼中得到確切的資訊。

# 得到main.cpp檔案
clang -rewrite-objc main.m 
複製程式碼

對於NSObject這個類,可以得到:

#ifndef _REWRITER_typedef_NSObject
#define _REWRITER_typedef_NSObject
typedef struct objc_object NSObject;
typedef struct {} _objc_exc_NSObject;
#endif
複製程式碼

NSObject是一個objc_object結構體。 ClassA的結構如下:

typedef struct objc_object ClassA;
複製程式碼

ClassA是一個指向objc_object結構體,其相關的其他部分為:

#ifndef _REWRITER_typedef_ClassA
#define _REWRITER_typedef_ClassA
typedef struct objc_object ClassA;
typedef struct {} _objc_exc_ClassA;
#endif

extern "C" unsigned long OBJC_IVAR_$_ClassA$_name;
struct ClassA_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	NSString *_name;
};

// @property(nonatomic,copy)NSString * name;
// -(void)sayHello;
// +(void)SayHello;
/* @end */

// @implementation ClassA

static void _I_ClassA_sayHello(ClassA * self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_dn_6w6g4h112csgf73k_bz07xpr0000gn_T_main_08dee3_mi_0);
}

static void _C_ClassA_SayHello(Class self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_dn_6w6g4h112csgf73k_bz07xpr0000gn_T_main_08dee3_mi_1);
}

static NSString * _I_ClassA_name(ClassA * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_ClassA$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_ClassA_setName_(ClassA * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct ClassA, _name), (id)name, 0, 1); }
// @end
複製程式碼

這個結構中,清楚地描述出了ClassA類中的例項變數、類方法、例項方法的結構和實現。 ClassA是一個objc_object結構體,其類方法和靜態方法在宣告以後,被使用在如下:

extern "C" unsigned long int OBJC_IVAR_$_ClassA$_name __attribute__ ((used, section ("__DATA,__objc_ivar"))) = __OFFSETOFIVAR__(struct ClassA, _name);

static struct /*_ivar_list_t*/ {
	unsigned int entsize;  // sizeof(struct _prop_t)
	unsigned int count;
	struct _ivar_t ivar_list[1];
} _OBJC_$_INSTANCE_VARIABLES_ClassA __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_ivar_t),
	1,
	{{(unsigned long int *)&OBJC_IVAR_$_ClassA$_name, "_name", "@\"NSString\"", 3, 8}}
};

static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[3];
} _OBJC_$_INSTANCE_METHODS_ClassA __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	3,
	{{(struct objc_selector *)"sayHello", "v16@0:8", (void *)_I_ClassA_sayHello},
	{(struct objc_selector *)"name", "@16@0:8", (void *)_I_ClassA_name},
	{(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_ClassA_setName_}}
};

static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[1];
} _OBJC_$_CLASS_METHODS_ClassA __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	1,
	{{(struct objc_selector *)"SayHello", "v16@0:8", (void *)_C_ClassA_SayHello}}
};

static struct /*_prop_list_t*/ {
	unsigned int entsize;  // sizeof(struct _prop_t)
	unsigned int count_of_properties;
	struct _prop_t prop_list[1];
} _OBJC_$_PROP_LIST_ClassA __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_prop_t),
	1,
	{{"name","T@\"NSString\",C,N,V_name"}}
};
複製程式碼

OBJC$_INSTANCE_VARIABLES_ClassA

OBJC$_INSTANCE_METHODS_ClassA

OBJC$_CLASS_METHODS_ClassA

OBJC$_PROP_LIST_ClassA

這四個結構體成員變數為類的例項屬性、例項方法列表與類方法列表等定義的結構體,這些結構體的被用到在:

static struct _class_ro_t _OBJC_CLASS_RO_$_ClassA __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	0, __OFFSETOFIVAR__(struct ClassA, _name), sizeof(struct ClassA_IMPL), 
	(unsigned int)0, 
	0, 
	"ClassA",
	(const struct _method_list_t *)&_OBJC_$_INSTANCE_METHODS_ClassA,
	0, 
	(const struct _ivar_list_t *)&_OBJC_$_INSTANCE_VARIABLES_ClassA,
	0, 
	(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_ClassA,
};
複製程式碼

OBJC_CLASS_RO$_ClassA

結構體變數將類方法、屬性等結構體進行包裝,其資料型別_class_ro_t如下:

struct _class_ro_t {
	unsigned int flags;
	unsigned int instanceStart;
	unsigned int instanceSize;
	unsigned int reserved;
	const unsigned char *ivarLayout;
	const char *name;
	const struct _method_list_t *baseMethods;
	const struct _objc_protocol_list *baseProtocols;
	const struct _ivar_list_t *ivars;
	const unsigned char *weakIvarLayout;
	const struct _prop_list_t *properties;
};
複製程式碼

該結構體其實就是objc_class結構體的變形,代表根類的結構體型別。

OBJC_CLASS_RO$_ClassA

又被另外一個結構體進行包裝:

extern "C" __declspec(dllexport) struct _class_t OBJC_CLASS_$_ClassA __attribute__ ((used, section ("__DATA,__objc_data"))) = {
	0, // &OBJC_METACLASS_$_ClassA,
	0, // &OBJC_CLASS_$_NSObject,
	0, // (void *)&_objc_empty_cache,
	0, // unused, was (void *)&_objc_empty_vtable,
	&_OBJC_CLASS_RO_$_ClassA,
};
複製程式碼

OBJC_CLASS_$_ClassA

則就是ClassA這個類的結構體,其結構體是_class_t。

struct _class_t {
	struct _class_t *isa;
	struct _class_t *superclass;
	void *cache;
	void *vtable;
	struct _class_ro_t *ro;
};
複製程式碼

OBJC_CLASS_$_ClassA

OBJC_CLASS_RO$_ClassA

兩個結構體被用在:

static void OBJC_CLASS_SETUP_$_ClassA(void ) {
	OBJC_METACLASS_$_ClassA.isa = &OBJC_METACLASS_$_NSObject;
	OBJC_METACLASS_$_ClassA.superclass = &OBJC_METACLASS_$_NSObject;
	OBJC_METACLASS_$_ClassA.cache = &_objc_empty_cache;
	OBJC_CLASS_$_ClassA.isa = &OBJC_METACLASS_$_ClassA;
	OBJC_CLASS_$_ClassA.superclass = &OBJC_CLASS_$_NSObject;
	OBJC_CLASS_$_ClassA.cache = &_objc_empty_cache;
}
複製程式碼

這是一個類初始化函式,從函式中得到明確的資訊是:

OBJC_METACLASS_$_ClassA

是一個_class_t(即objc_class)結構體,其isa指標指向OBJC_METACLASS_$_NSObject結構體,其型別為_class_ro_t(objc_class)的結構體

OBJC_METACLASS_$_ClassA.superclass

指明其父類是OBJC_METACLASS_$_NSObject結構體指標

OBJC_CLASS_$_ClassA

其isa指標指向ClassA元類結構

OBJC_METACLASS_$_ClassA

其次

OBJC_CLASS_$_ClassA.superclass

指明其父類是OBJC_CLASS_$_NSObject根類資料結構

例項物件、類、父類、根類、元類的簡單關係

  • 例項物件,在Objective-C中即為一個objc_object的結構體指標
  • 類,也是一個objc_object的結構體指標
  • 父類,子類通過isa指向的objc_object的結構指標
  • 根類,NSObject,所有子類都直接或間接的通過isa指標指向該類例項物件
  • 元類,類的objc_object結構體中的isa指標指向的objc_class結構體

至此,可以非常清晰得出以下結論: 如下圖所示:

關係圖

  1. ClassA的例項物件,是一個objc_object結構體指標,其isa指標指向ClassA類的objc_object結構體
  2. ClassA,是一個_class_t(即objc_class)的結構體,其isa指標指向MetaClassA結構體
  3. ClassA的父類,是NSObject,其superclass是一個指向NSObject類物件的isa指標
  4. ClassA的元類,是MetaClassA,OBJC_METACLASS_元類結構體表明,其isa指標指向MetaNSObject結構體指標
  5. 一個類所擁有的方法、屬性,都會儲存在類的元類中,當呼叫物件的方法的時候,也就是向物件傳送訊息,runtime會在這個物件所屬的類方法列表中查詢訊息對應的方法,但向類傳送訊息的時候,runtime就會在這個類的meta class的方法列表中查詢。 更加通用和清晰的關係圖如下:

關係圖

即:

  1. 物件的isa指標指向類物件
  2. 物件的superclass的指標指向父類類物件
  3. 類物件的isa指標指向元類
  4. 類物件的superclass的指標指向父類元類
  5. 元類的isa指標,指向根類(NSObject)元類
  6. 元類的superclass指標指向父類元類,直接繼承根類的類的元類的superclass指向根類元類(NSObject)
  7. 根類(NSObject)的isa,指向根元類
  8. 根類(NSObject)的superclass為nil
  9. 根元類的superclass指向NSObject類
  10. 根元類的isa指標,指向自身

類與物件的相關資訊

獲取類名

const char * class_getName(Class cls);
複製程式碼

父類與超類的獲取

//獲取父類
Class class_getSuperclass(Class cls);
//判定類是否為一個meta class
BOOL class_isMetaClass(Class cls);
複製程式碼

計算類的被分配的大小

size_t class_gerInstanceSize(Class cls);
複製程式碼

例項物件和類的屬性列表objc_ivar_list與方法列表objc_method

//objc_ivar_list結構體儲存objc_ivar陣列列表
struct objc_ivar_list {
     int ivar_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
     int space OBJC2_UNAVAILABLE;
#endif
     /* variable length structure */
     struct objc_ivar ivar_list[1] OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;

//objc_method_list結構體儲存著objc_method的陣列列表
struct objc_method_list {
     struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
     int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
     int space OBJC2_UNAVAILABLE;
#endif
     /* variable length structure */
     struct objc_method method_list[1] OBJC2_UNAVAILABLE;
}
複製程式碼

objc_ivar_list 為成員變數單向連結串列,其結構體最後一個成員變數是一個objc_ivar結構體陣列,該陣列為變長,所以objc_ivar_list可以是一個變長結構體,objc_ivar標示一個成員變數:

struct objc_ivar {
    char * _Nullable ivar_name  OBJC2_UNAVAILABLE;//變數名
    char * _Nullable ivar_type  OBJC2_UNAVAILABLE;//變數型別 
    int ivar_offset   OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space    OBJC2_UNAVAILABLE;
#endif
}   OBJC2_UNAVAILABLE;
複製程式碼

objc_method_list為方法列表

struct objc_method_list {
    struct objc_method_list * _Nullable obsolete OBJC2_UNAVAILABLE;
    int method_count  OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space    OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_method method_list[1]  OBJC2_UNAVAILABLE;
}            
複製程式碼

objc_method為方法結構體,如下:

struct objc_method {
    SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
    char * _Nullable method_types                            OBJC2_UNAVAILABLE;
    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
}         
複製程式碼

Objective-C的方法實現(IMP)與方法簽名(SEL)

typedef struct  *SEL;
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
#endif
複製程式碼

IMP是一個void * ()的函式指標,實際上就是方法的實現,SEL為一個char * 字串。 每一個objc_class結構體中都有objc_method_list列表,而objc_method_list列表中有objc_method結構體,該結構體為一個SEL對應一個IMP實現。 在runtime執行的時候,載入的每一個類對應有一個virtual table,用來快取SEL與IMP的對應關係,從而能夠通過SEL快速找到其對應實現。

成員變數(ivars)及其屬性

//成員變數操作函式
// 獲取類中指定名稱例項成員變數的資訊
Ivar class_getInstanceVariable ( Class cls, const char *name );

// 獲取類成員變數的資訊
Ivar class_getClassVariable ( Class cls, const char *name );

// 新增成員變數
BOOL class_addIvar ( Class cls, const char *name, size_t size, uint8_t alignment, const char *types ); //這個只能夠向在runtime時建立的類新增成員變數

// 獲取整個成員變數列表
Ivar * class_copyIvarList ( Class cls, unsigned int *outCount ); //必須使用free()來釋放這個陣列

//屬性操作函式
// 獲取類中指定名稱例項成員變數的資訊
Ivar class_getInstanceVariable ( Class cls, const char *name );

// 獲取類成員變數的資訊
Ivar class_getClassVariable ( Class cls, const char *name );

// 新增成員變數
BOOL class_addIvar ( Class cls, const char *name, size_t size, uint8_t alignment, const char *types );

// 獲取整個成員變數列表
Ivar * class_copyIvarList ( Class cls, unsigned int *outCount );
複製程式碼

方法列表methodLists的執行時操作

// 新增方法
BOOL class_addMethod ( Class cls, SEL name, IMP imp, const char *types ); //和成員變數不同的是可以為類動態新增方法。如果有同名會返回NO,修改的話需要使用method_setImplementation

// 獲取例項方法
Method class_getInstanceMethod ( Class cls, SEL name );

// 獲取類方法
Method class_getClassMethod ( Class cls, SEL name );

// 獲取所有方法的陣列
Method * class_copyMethodList ( Class cls, unsigned int *outCount );

// 替代方法的實現
IMP class_replaceMethod ( Class cls, SEL name, IMP imp, const char *types );

// 返回方法的具體實現
IMP class_getMethodImplementation ( Class cls, SEL name );
IMP class_getMethodImplementation_stret ( Class cls, SEL name );

// 類例項是否響應指定的selector
BOOL class_respondsToSelector ( Class cls, SEL sel );
複製程式碼

描述類的objc_protocol_list與Protocol

objc_protocol_list:

struct objc_protocol_list {
    struct objc_protocol_list * _Nullable next;
    long count;
    __unsafe_unretained Protocol * _Nullable list[1];
};
複製程式碼

Protocol的定義如下:

#ifdef __OBJC__
@class Protocol;
#else
typedef struct objc_object Protocol;
#endif
複製程式碼

對protocol的操作為:

// 新增協議
BOOL class_addProtocol ( Class cls, Protocol *protocol );

// 返回類是否實現指定的協議
BOOL class_conformsToProtocol ( Class cls, Protocol *protocol );

// 返回類實現的協議列表
Protocol * class_copyProtocolList ( Class cls, unsigned int *outCount );
複製程式碼

總結

Objective-C基於C語言的結構體定義物件導向的大部分概念,利用結構體指標與函式指標,來實現物件導向的類定義、類繼承、例項化物件、物件成員變數和方法的儲存與定義。 由此,Objective-C這本語言是一種在執行時發揮強大能力的語言,而這又歸功於runtime這一訊息分發系統,在執行時能夠對類進行掃描、查詢、呼叫、修改等等,這部分知識被稱為rumtime核心技術,訊息呼叫與動態型別的結合,使得Objective-C這門語言能夠給予程式設計師非常大的自由度去享受程式設計的樂趣。

相關文章