1. 程式人生 > >Objective-C Runtime 執行時之一:類與物件

Objective-C Runtime 執行時之一:類與物件

Objective-C語言是一門動態語言,它將很多靜態語言在編譯和連結時期做的事放到了執行時來處理。這種動態語言的優勢在於:我們寫程式碼時更具靈活性,如我們可以把訊息轉發給我們想要的物件,或者隨意交換一個方法的實現等。

這種特性意味著Objective-C不僅需要一個編譯器,還需要一個執行時系統來執行編譯的程式碼。對於Objective-C來說,這個執行時系統就像一個作業系統一樣:它讓所有的工作可以正常的執行。這個執行時系統即Objc Runtime。Objc Runtime其實是一個Runtime庫,它基本上是用C和彙編寫的,這個庫使得C語言有了面向物件的能力。

Runtime庫主要做下面幾件事:

  1. 封裝:在這個庫中,物件可以用C語言中的結構體表示,而方法可以用C函式來實現,另外再加上了一些額外的特性。這些結構體和函式被runtime函式封裝後,我們就可以在程式執行時建立,檢查,修改類、物件和它們的方法了。
  2. 找出方法的最終執行程式碼:當程式執行[object doSomething]時,會向訊息接收者(object)傳送一條訊息(doSomething),runtime會根據訊息接收者是否能響應該訊息而做出不同的反應。這將在後面詳細介紹。

Objective-C runtime目前有兩個版本:Modern runtime和Legacy runtime。Modern Runtime 覆蓋了64位的Mac OS X Apps,還有 iOS Apps,Legacy Runtime 是早期用來給32位 Mac OS X Apps 用的,也就是可以不用管就是了。

在這一系列文章中,我們將介紹runtime的基本工作原理,以及如何利用它讓我們的程式變得更加靈活。在本文中,我們先來介紹一下類與物件,這是面向物件的基礎,我們看看在Runtime中,類是如何實現的。

類與物件基礎資料結構

Class

Objective-C類是由Class型別來表示的,它實際上是一個指向objc_class結構體的指標。它的定義如下:

1 typedefstructobjc_class *
Class;

檢視objc/runtime.h中objc_class結構體的定義如下:

12345678910111213141516 structobjc_class{Classisa  OBJC_ISA_AVAILABILITY;#if !__OBJC2__Classsuper_class                       OBJC2_UNAVAILABLE;// 父類constchar*name                        OBJC2_UNAVAILABLE;// 類名longversion                            OBJC2_UNAVAILABLE;// 類的版本資訊,預設為0longinfo                               OBJC2_UNAVAILABLE;// 類資訊,供執行期使用的一些位標識longinstance_size                      OBJC2_UNAVAILABLE;// 該類的例項變數大小structobjc_ivar_list *ivars            OBJC2_UNAVAILABLE;// 該類的成員變數連結串列structobjc_method_list **methodLists   OBJC2_UNAVAILABLE;// 方法定義的連結串列structobjc_cache *cache                OBJC2_UNAVAILABLE;// 方法快取structobjc_protocol_list *protocols    OBJC2_UNAVAILABLE;// 協議連結串列#endif}OBJC2_UNAVAILABLE;

在這個定義中,下面幾個欄位是我們感興趣的

  1. isa:需要注意的是在Objective-C中,所有的類自身也是一個物件,這個物件的Class裡面也有一個isa指標,它指向metaClass(元類),我們會在後面介紹它。
  2. super_class:指向該類的父類,如果該類已經是最頂層的根類(如NSObject或NSProxy),則super_class為NULL。
  3. cache:用於快取最近使用的方法。一個接收者物件接收到一個訊息時,它會根據isa指標去查詢能夠響應這個訊息的物件。在實際使用中,這個物件只有一部分方法是常用的,很多方法其實很少用或者根本用不上。這種情況下,如果每次訊息來時,我們都是methodLists中遍歷一遍,效能勢必很差。這時,cache就派上用場了。在我們每次呼叫過一個方法後,這個方法就會被快取到cache列表中,下次呼叫的時候runtime就會優先去cache中查詢,如果cache沒有,才去methodLists中查詢方法。這樣,對於那些經常用到的方法的呼叫,但提高了呼叫的效率。
  4. version:我們可以使用這個欄位來提供類的版本資訊。這對於物件的序列化非常有用,它可是讓我們識別出不同類定義版本中例項變數佈局的改變。

針對cache,我們用下面例子來說明其執行過程:

1 NSArray *array=[[NSArray alloc]init];

其流程是:

  1. [NSArray alloc]先被執行。因為NSArray沒有+alloc方法,於是去父類NSObject去查詢。
  2. 檢測NSObject是否響應+alloc方法,發現響應,於是檢測NSArray類,並根據其所需的記憶體空間大小開始分配記憶體空間,然後把isa指標指向NSArray類。同時,+alloc也被加進cache列表裡面。
  3. 接著,執行-init方法,如果NSArray響應該方法,則直接將其加入cache;如果不響應,則去父類查詢。
  4. 在後期的操作中,如果再以[[NSArray alloc] init]這種方式來建立陣列,則會直接從cache中取出相應的方法,直接呼叫。

objc_object與id

objc_object是表示一個類的例項的結構體,它的定義如下(objc/objc.h):

12345 structobjc_object{Classisa  OBJC_ISA_AVAILABILITY;};typedefstructobjc_object *id;

可以看到,這個結構體只有一個字型,即指向其類的isa指標。這樣,當我們向一個Objective-C物件傳送訊息時,執行時庫會根據例項物件的isa指標找到這個例項物件所屬的類。Runtime庫會在類的方法列表及父類的方法列表中去尋找與訊息對應的selector指向的方法。找到後即執行這個方法。

當建立一個特定類的例項物件時,分配的記憶體包含一個objc_object資料結構,然後是類的例項變數的資料。NSObject類的alloc和allocWithZone:方法使用函式class_createInstance來建立objc_object資料結構。

另外還有我們常見的id,它是一個objc_object結構型別的指標。它的存在可以讓我們實現類似於C++中泛型的一些操作。該型別的物件可以轉換為任何一種物件,有點類似於C語言中void *指標型別的作用。

objc_cache

上面提到了objc_class結構體中的cache欄位,它用於快取呼叫過的方法。這個欄位是一個指向objc_cache結構體的指標,其定義如下:

12345 structobjc_cache{unsignedintmask/* total = mask + 1 */OBJC2_UNAVAILABLE;unsignedintoccupied                                    OBJC2_UNAVAILABLE;Method buckets[1]OBJC2_UNAVAILABLE;};

該結構體的欄位描述如下:

  1. mask:一個整數,指定分配的快取bucket的總數。在方法查詢過程中,Objective-C runtime使用這個欄位來確定開始線性查詢陣列的索引位置。指向方法selector的指標與該欄位做一個AND位操作(index = (mask & selector))。這可以作為一個簡單的hash雜湊演算法。
  2. occupied:一個整數,指定實際佔用的快取bucket的總數。
  3. buckets:指向Method資料結構指標的陣列。這個陣列可能包含不超過mask+1個元素。需要注意的是,指標可能是NULL,表示這個快取bucket沒有被佔用,另外被佔用的bucket可能是不連續的。這個陣列可能會隨著時間而增長。

元類(Meta Class)

在上面我們提到,所有的類自身也是一個物件,我們可以向這個物件傳送訊息(即呼叫類方法)。如:

1 NSArray *array=[NSArray array];

這個例子中,+array訊息傳送給了NSArray類,而這個NSArray也是一個物件。既然是物件,那麼它也是一個objc_object指標,它包含一個指向其類的一個isa指標。那麼這些就有一個問題了,這個isa指標指向什麼呢?為了呼叫+array方法,這個類的isa指標必須指向一個包含這些類方法的一個objc_class結構體。這就引出了meta-class的概念

1 meta-class是一個類物件的類。

當我們向一個物件傳送訊息時,runtime會在這個物件所屬的這個類的方法列表中查詢方法;而向一個類傳送訊息時,會在這個類的meta-class的方法列表中查詢。

meta-class之所以重要,是因為它儲存著一個類的所有類方法。每個類都會有一個單獨的meta-class,因為每個類的類方法基本不可能完全相同。

再深入一下,meta-class也是一個類,也可以向它傳送一個訊息,那麼它的isa又是指向什麼呢?為了不讓這種結構無限延伸下去,Objective-C的設計者讓所有的meta-class的isa指向基類的meta-class,以此作為它們的所屬類。即,任何NSObject繼承體系下的meta-class都使用NSObject的meta-class作為自己的所屬類,而基類的meta-class的isa指標是指向它自己。這樣就形成了一個完美的閉環。

通過上面的描述,再加上對objc_class結構體中super_class指標的分析,我們就可以描繪出類及相應meta-class類的一個繼承體系了,如下圖所示:

1413628797629491

對於NSObject繼承體系來說,其例項方法對體系中的所有例項、類和meta-class都是有效的;而類方法對於體系內的所有類和meta-class都是有效的。

講了這麼多,我們還是來寫個例子吧:

123456789101112131415161718192021222324252627282930 voidTestMetaClass(id self,SEL _cmd){NSLog(@"This objcet is %p",self);NSLog(@"Class is %@, super class is %@",[selfclass],[selfsuperclass]);ClasscurrentClass=[selfclass];for(inti=0;i<4;i++){NSLog(@"Following the isa pointer %d times gives %p",i,currentClass);currentClass=objc_getClass((__bridge void*)currentClass);}NSLog(@"NSObject's class is %p",[NSObject class]);NSLog(@"NSObject's meta class is %p",objc_getClass((__bridge void*)[NSObject class]));}#pragma mark -@implementation Test-(void)ex_registerClassPair{ClassnewClass=objc_allocateClassPair([NSError class],"TestClass",0);class_addMethod(newClass,@selector(testMetaClass),(IMP)TestMetaClass,"[email protected]:");objc_registerClassPair(newClass);id instance=[[newClass alloc]initWithDomain:@"some domain"code:0userInfo:nil];[instance performSelector:@selector(testMetaClass)];}@end

這個例子是在執行時建立了一個NSError的子類TestClass,然後為這個子類新增一個方法testMetaClass,這個方法的實現是TestMetaClass函式。

執行後,列印結果是

12345678 2014-10-2022:57:07.352mountain[1303:41490]Thisobjcet is0x7a6e22b02014-10-2022:57:07.353mountain[1303:41490]ClassisTestStringClass,superclassisNSError2014-10-2022:57:07.353mountain[1303:41490]Following the isa pointer0times gives0x7a6e21b02014-10-2022:57:07.353mountain[1303:41490]Following the isa pointer1times gives0x02014-10-2022:57:07.353mountain[1303:41490]Following the isa pointer2times gives0x02014-10-2022:57:07.353mountain[1303:41490]Following the isa pointer3times gives0x02014-10-2022:57:07.353mountain[1303:41490]NSObject&#039;s class is 0xe100002014-10-2022:57:07.354mountain[1303:41490]NSObject&#039;s meta class is 0x0

我們在for迴圈中,我們通過objc_getClass來獲取物件的isa,並將其打印出來,依此一直回溯到NSObject的meta-class。分析列印結果,可以看到最後指標指向的地址是0x0,即NSObject的meta-class的類地址。

這裡需要注意的是:我們在一個類物件呼叫class方法是無法獲取meta-class,它只是返回類而已。

類與物件操作函式

runtime提供了大量的函式來操作類與物件。類的操作方法大部分是以class為字首的,而物件的操作方法大部分是以objc或object_為字首。下面我們將根據這些方法的用途來分類討論這些方法的使用。

類相關操作函式

我們可以回過頭去看看objc_class的定義,runtime提供的操作類的方法主要就是針對這個結構體中的各個欄位的。下面我們分別介紹這一些的函式。並在最後以例項來演示這些函式的具體用法。

類名(name)

類名操作的函式主要有:

12 // 獲取類的類名constchar*class_getName(Classcls);
  • 對於class_getName函式,如果傳入的cls為Nil,則返回一個字字串。

父類(super_class)和元類(meta-class)

父類和元類操作的函式主要有:

12345 // 獲取類的父類Classclass_getSuperclass(Classcls);// 判斷給定的Class是否是一個元類BOOLclass_isMetaClass(Classcls);
  • class_getSuperclass函式,當cls為Nil或者cls為根類時,返回Nil。不過通常我們可以使用NSObject類的superclass方法來達到同樣的目的。
  • class_isMetaClass函式,如果是cls是元類,則返回YES;如果否或者傳入的cls為Nil,則返回NO。

例項變數大小(instance_size)

例項變數大小操作的函式有:

12 // 獲取例項大小size_t class_getInstanceSize(Classcls);

成員變數(ivars)及屬性

在objc_class中,所有的成員變數、屬性的資訊是放在連結串列ivars中的。ivars是一個數組,陣列中每個元素是指向Ivar(變數資訊)的指標。runtime提供了豐富的函式來操作這一欄位。大體上可以分為以下幾類:

1.成員變數操作函式,主要包含以下函式:

1234567891011 // 獲取類中指定名稱例項成員變數的資訊Ivar class_getInstanceVariable(Classcls,constchar*name);// 獲取類成員變數的資訊Ivar class_getClassVariable(Classcls,constchar*name);// 新增成員變數BOOLclass_addIvar(Classcls,constchar*name,size_t size,uint8_t alignment,constchar*types);// 獲取整個成員變數列表Ivar *class_copyIvarList(Classcls,unsignedint*outCount);
  • class_getInstanceVariable函式,它返回一個指向包含name指定的成員變數資訊的objc_ivar結構體的指標(Ivar)。
  • class_getClassVariable函式,目前沒有找到關於Objective-C中類變數的資訊,一般認為Objective-C不支援類變數。注意,返回的列表不包含父類的成員變數和屬性。
  • Objective-C不支援往已存在的類中新增例項變數,因此不管是系統庫提供的提供的類,還是我們自定義的類,都無法動態新增成員變數。但如果我們通過執行時來建立一個類的話,又應該如何給它新增成員變數呢?這時我們就可以使用class_addIvar函數了。不過需要注意的是,這個方法只能在objc_allocateClassPair函式與objc_registerClassPair之間呼叫。另外,這個類也不能是元類。成員變數的按位元組最小對齊量是1<<alignment。這取決於ivar的型別和機器的架構。如果變數的型別是指標型別,則傳遞log2(sizeof(pointer_type))。
  • class_copyIvarList函式,它返回一個指向成員變數資訊的陣列,陣列中每個元素是指向該成員變數資訊的objc_ivar結構體的指標。這個陣列不包含在父類中宣告的變數。outCount指標返回陣列的大小。需要注意的是,我們必須使用free()來釋放這個陣列。

2.屬性操作函式,主要包含以下函式: