runtime 運行時之一:類與對象

本文章是我照著南峰子的runtime博客寫的,加上了一點自己的見解,作為自己的學(xué)習(xí)筆記,侵刪。
附上原博客地址:南峰子的技術(shù)博客

如有錯誤的地方歡迎指出。

runtime 運行時之一:類與對象

Object-C是一門動態(tài)語言,它將很多靜態(tài)語言在編譯和連接時期做的事情放到了運行時來做。

優(yōu)勢:代碼更具有靈活性。

這種特性意味著Objective-C不僅需要一個編譯器,還需要一個運行時系統(tǒng)來執(zhí)行編譯的代碼。這個運行時系統(tǒng)就是Objc Runtime。

Objc Runtime其實就是一個runtime庫,它基本上是用C和匯編寫的。這個庫使得C語言有了面向?qū)ο蟮哪芰Α?/strong>

Runtime庫主要做了下面幾件事情:

  1. 封裝:在這個庫中,對象可以用C語言的結(jié)構(gòu)體表示,方法可以用C函數(shù)來實現(xiàn),另外再加上了一些額外的特性。這些結(jié)構(gòu)體和函數(shù)被Runtime函數(shù)封裝后,我們就可以在程序運行時創(chuàng)建,檢查,修改類、對象和他們的方法了。
  2. 找出方法的最終執(zhí)行代碼:當(dāng)程序執(zhí)行 [object doSomething]時,會向消息接受者(object)發(fā)送一條消息(doSomething),runtime會根據(jù)消息接收者是否能響應(yīng)該消息而做出不同的反應(yīng)。這將在后面詳細介紹。

所以我們會說Objective-C的底層是C語言。

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

類和對象基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)

Class

Objective-C類是由Class類型來表示的,他實際上是一個指向objc_class結(jié)構(gòu)體的指針。它的定義如下:

typedef struct objc_class *Class;

查看objc/runtime.hobjc_class結(jié)構(gòu)體的定義如下:

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;
 #if !__OBJC2__
    Class super_class                       OBJC2_UNAVAILABLE;  // 父類
    const char *name                        OBJC2_UNAVAILABLE;  // 類名
    long version                            OBJC2_UNAVAILABLE;  // 類的版本信息,默認為0
    long info                               OBJC2_UNAVAILABLE;  // 類信息,供運行期使用的一些位標(biāo)識
    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;  // 協(xié)議鏈表
 #endif
} OBJC2_UNAVAILABLE;

在定義中,下面幾個字段使我們感興趣的

  1. isa:在Object-C中,所有實例的isa指針指向類,所有的類自身也是一個對象,這個對象的Class也有一個isa指針,它指向metaClass(元類),我們會在后面介紹它。
  2. super_class:指向該類的父類,如果該類已經(jīng)是最頂層的跟類(如NSObject或NSProxy),則super_class為NULL。
  3. cache:用于緩存最近使用的方法。一個接收者對象接收到一個消息時,它會根據(jù)isa指針去查找能夠響應(yīng)這個消息的對象。在實際使用中,這個對象只有一部分方法是常用的,很多方法其實很少用或者根本用不上。這種情況下,如果每次消息來時,我們都是methodLists中遍歷一遍,性能勢必很差。這時,cache就派上用場了。在我們每次調(diào)用過一個方法后,這個方法就會被緩存到cache列表中,下次調(diào)用的時候runtime就會優(yōu)先去cache中查找,如果cache沒有,才去methodLists中查找方法。這樣,對于那些經(jīng)常用到的方法的調(diào)用,但提高了調(diào)用的效率。
  4. version:我們使用這個字段來提供類的版本信息。這對于對象的序列化非常有用,它可是讓我們識別出不同類定義版本中實例布局的改變。


objc_cache

上面提到了objc_class 結(jié)構(gòu)體中的 cache字段,它用于緩存調(diào)用過的方法。這個字段是一個指向objc_cache結(jié)構(gòu)體的指針,其定義如下:

struct objc_cache {
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
    unsigned int occupied                                    OBJC2_UNAVAILABLE;
    Method buckets[1]                                        OBJC2_UNAVAILABLE;
};
  1. mask:一個整數(shù),指定分配的緩存的buckets的總數(shù)。在方法查找過程中,Objective-C runtime使用這個字段來確定開始線性查找數(shù)組的索引位置。指向方法selector的指針與該字段做一個AND位操作(index = (mask & selector))。這可以作為一個簡單的hash散列算法。
  2. occupied:一個整數(shù),指定實際占用的緩存bucket的總數(shù)。
  3. buckets:指向Method數(shù)據(jù)結(jié)構(gòu)指針的數(shù)組。這個數(shù)組可能包括不超過msak+1個元素。需要注意的是,指針可能是NULL,表示這個緩存Bucket沒有被占用,另外被占用的bucket可能是不連續(xù)的。這個數(shù)組可能隨著時間而增長。

元類(Meta Class)

所有的類自身也是一個對象,我們可以向這個對象發(fā)送消息(即調(diào)用方法)

NSArray *array = [NSArray array];

這個例子中,+array消息發(fā)送給了NSArray類,這個NSArray也是一個對象,那么它也是一個objc_object指針,它包含一個指向其類的一個isa指針。為了調(diào)用+array方法,這個類的isa指針必須指向包含這些類方法的一個objc_object結(jié)構(gòu)體。這就是meta-class(元類)。

meta-class是一個類對象的類

我們向一個對象發(fā)送消息的時候,runtime會在這個對象所屬的這個類的方法列表中查找方法,而向一個類發(fā)送消息的時候,會在這個類的meta-class的方法列表中查找方法。

meta-class存儲著一個類的所有類方法。每一個類都有單獨的meta-class,因為每個類的類方法不可能完全相同。

Object-C的設(shè)計者讓所有的meta-classisa都指向基類的meta-class,以此作為它們的所屬類。所以,任何NSObject繼承體系下的meta-class的isa指針都是NSObject的meta-class,而基類的meta-class指向它自己。這樣就形成了一個完美的閉環(huán)。

下圖很好的說明了meta-class、類和對象的關(guān)系:

image.png

也就是說實例的isa指向類,類對象的isa指向這個類的meta-class(元類),而meta-class的isa指向NSOject的meta-class,NSObject的元類的isa指向本身

類與對象操作函數(shù)

runtime提供了大量的函數(shù)來操作類與對象。類的方法大部分是以class_作為前綴。

注:所有copy操作返回的列表都需要用free()函數(shù)手動釋放。

類相關(guān)操作函數(shù)

// 獲取類的類名
const char * class_getName ( Class cls );
// 獲取類的父類
Class class_getSuperclass ( Class cls );
// 判斷給定的Class是否是一個元類
BOOL class_isMetaClass ( Class cls );
// 獲取實例大小  size_t用%zu輸出
size_t class_getInstanceSize ( Class cls );
// 獲取類中指定名稱實例成員變量的信息
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 );
// 獲取指定的屬性
objc_property_t class_getProperty ( Class cls, const char *name );
// 獲取屬性列表
objc_property_t * class_copyPropertyList ( Class cls, unsigned int *outCount );
// 為類添加屬性
BOOL class_addProperty ( Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount );
// 替換類的屬性
void class_replaceProperty ( Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount );

方法(methodLists)

// 添加方法
BOOL class_addMethod ( Class cls, SEL name, IMP imp, const char *types );
// 獲取實例方法
Method class_getInstanceMethod ( Class cls, SEL name );
// 獲取類方法
Method class_getClassMethod ( Class cls, SEL name );
// 獲取所有方法的數(shù)組
Method * class_copyMethodList ( Class cls, unsigned int *outCount );
// 替代方法的實現(xiàn)
IMP class_replaceMethod ( Class cls, SEL name, IMP imp, const char *types );
// 返回方法的具體實現(xiàn)
IMP class_getMethodImplementation ( Class cls, SEL name );
IMP class_getMethodImplementation_stret ( Class cls, SEL name );
// 類實例是否響應(yīng)指定的selector
BOOL class_respondsToSelector ( Class cls, SEL sel );
  • class_addMethod會覆蓋父類的方法,但不會取代本類中已存在的實現(xiàn),如果本類中國有同名的實現(xiàn),則會返回NO??梢允褂?code>method_serImplementation來修改已經(jīng)實現(xiàn)的方法。

  • 一個Objective-C方法是一個簡單的C函數(shù),它至少有兩個參數(shù)self_cmd,所以我們的實現(xiàn)函數(shù)(IMP參數(shù)指向的函數(shù))也至少需要兩個參數(shù),如下:

void myMethodIMP(id self, SEL _cmd) {
    // implementation...
}

這里我查了一下IMP與SEL的區(qū)別:

  1. IMP是真正的函數(shù)指針,指向方法實現(xiàn)。
  2. Objective-C在運行時所有的方法可以看成一張表,而SEL可以看做是表中每一條的索引。
  3. - (IMP)methodForSelector:(SEL)aSelector 方法可以根據(jù)一個實例SEL得到該方法的IMP(函數(shù)指針)+ (IMP)instanceMethodForSelector:(SEL)aSelector通過類的SEL返回IMP。
  4. 個人見解:SEL和IMP是映射關(guān)系,SEL可以通過runtime來更改它的IMP(函數(shù)指針),而IMP是不能更改它指向的函數(shù),也就是說每一個c函數(shù)對應(yīng)一個IMP,而SEL是動態(tài)的。

與成員變量不同的是,我們可以為類動態(tài)添加方法,不管這個類是否存在。

  • class_getInstanceMethod、class_getClassMethod函數(shù)會去搜索父類的實現(xiàn),而class_copyMethodList不會搜索父類的實現(xiàn)。
  • class_copyMethodList函數(shù)返回包含所有實例方法的列表,而不包含類方法。如果需要獲取到類方法,可以使用object_getClass()來得到meta-class從而得到類方法,簡便寫法:class_copyMethodList(object_getClass(cls),&count)
  • class_replaceMethod函數(shù):如果類中不存在name指定的方法則添加一個方法,如果類中存在name指定的方法則替代它的實現(xiàn)。

  • class_getMethodImplementation函數(shù),該函數(shù)會在向類實例發(fā)送消息時被調(diào)用,返回一個指向方法實現(xiàn)函數(shù)的指針。如果這個類無法響應(yīng)selector返回的函數(shù)指針可能是一個指向runtime內(nèi)部的函數(shù),這也是運行時消息轉(zhuǎn)發(fā)機制的一部分。

  • class_respondsTpSelector函數(shù)和NSObject類的respondsToSelector:instancesRespondToSelector:作用相同。

協(xié)議(objc_protocal_list)

// 添加協(xié)議
BOOL class_addProtocol ( Class cls, Protocol *protocol );
// 返回類是否實現(xiàn)指定的協(xié)議
BOOL class_conformsToProtocol ( Class cls, Protocol *protocol );
// 返回類實現(xiàn)的協(xié)議列表
Protocol * class_copyProtocolList ( Class cls, unsigned int *outCount );
  • class_conformsToProtocol可以用NSObject類的conformsToProtocol方法來替代。

版本(version)

// 獲取版本號
int class_getVersion ( Class cls );
// 設(shè)置版本號
void class_setVersion ( Class cls, int version );

動態(tài)創(chuàng)建類和對象

runtime的強大之處在于它能在運行時創(chuàng)建類和對象

動態(tài)創(chuàng)建類

// 創(chuàng)建類實例
id class_createInstance ( Class cls, size_t extraBytes );
// 在指定位置創(chuàng)建類實例
id objc_constructInstance ( Class cls, void *bytes );
// 銷毀類實例
void * objc_destructInstance ( id obj );
  • class_createInstance函數(shù)在創(chuàng)建實例是,會在默認的內(nèi)存區(qū)域中為類分配內(nèi)存。extraBytes參數(shù)表示分配的額外字節(jié)數(shù)。這些額外字節(jié)數(shù)可用于動態(tài)添加方法和變量。該函數(shù)在ARC環(huán)境下無法使用。

調(diào)用class_createInstance的效果與+alloc方法相似。不過使用class_createInstance時,我們需要知道它的類是什么。

  • objc_constructInstance函數(shù):在指定的位置(bytes)創(chuàng)建實例。
  • objc_destructInstance函數(shù):銷毀一個類的實例,但不會釋放并移除任何與其相關(guān)的引用。

實例操作函數(shù)

  1. 針對整個對象進行操作的函數(shù)。
// 返回指定對象的一份拷貝
id object_copy ( id obj, size_t size );
// 釋放指定對象占用的內(nèi)存
id object_dispose ( id obj );

如果有兩個類分別為A類和B類,并且B類是A類的子類。B類添加了一些額外的屬性。現(xiàn)在我們創(chuàng)建了一個A類的實例對象,想要在運行時將這個對象轉(zhuǎn)化為B類的實例對象,從而添加B類的屬性。這種情況下,B類的實例對象會比A類的實例對象分配的空間大。此時,我們就需要使用以上幾個函數(shù)來處理這種情況。

NSObject *a = [[NSObject alloc] init];
id newB = object_copy(a, class_getInstanceSize(MyClass.class));
object_setClass(newB, MyClass.class);
object_dispose(a);

2.針對對象實例變量進行操作的函數(shù)

// 修改類實例的實例變量的值
Ivar object_setInstanceVariable ( id obj, const char *name, void *value );
// 獲取對象實例變量的值
Ivar object_getInstanceVariable ( id obj, const char *name, void **outValue );
// 返回指向給定對象分配的任何額外字節(jié)的指針
void * object_getIndexedIvars ( id obj );
// 返回對象中實例變量的值
id object_getIvar ( id obj, Ivar ivar );
// 設(shè)置對象中實例變量的值
void object_setIvar ( id obj, Ivar ivar, id value );

3.針對對象的類進行操作的函數(shù)

// 返回給定對象的類名
const char * object_getClassName ( id obj );
// 返回對象的類
Class object_getClass ( id obj );
// 設(shè)置對象的類
Class object_setClass ( id obj, Class cls );

獲取類定義

Object-C動態(tài)運行庫會自動注冊我們代碼中定義的多有類。我們也可以在運行時創(chuàng)建類定義并使用objc_addClass函數(shù)注冊它們。Runtime提供了一系列函數(shù)來獲取類定義相關(guān)的信息,這些函數(shù)主要包括:

// 獲取已注冊的類定義的列表
int objc_getClassList ( Class *buffer, int bufferCount );
// 創(chuàng)建并返回一個指向所有已注冊類的指針列表
Class * objc_copyClassList ( unsigned int *outCount );
// 返回指定類的類定義
Class objc_lookUpClass ( const char *name );
Class objc_getClass ( const char *name );
Class objc_getRequiredClass ( const char *name );
// 返回指定類的元類
Class objc_getMetaClass ( const char *name );
  • objc_getClassList函數(shù):獲取已注冊的類定義的列表。
  • 獲取類定義的方法有三個:objc_lookUpClass, objc_getClassobjc_getRequiredClass。如果類在運行時未注冊,則objc_lookUpClass會返回nil,而objc_getClass會調(diào)用類處理回調(diào),并再次確認類是否注冊,如果確認未注冊,再返回nil。而objc_getRequiredClass函數(shù)的操作與objc_getClass相同,只不過如果沒有找到類,則會殺死進程。
  • objc_getMetaClass函數(shù):如果指定的類沒有注冊,則該函數(shù)會調(diào)用類處理回調(diào),并再次確認類是否注冊,如果確認未注冊,再返回nil。不過,每個類定義都必須有一個有效的元類定義,所以這個函數(shù)總是會返回一個元類定義,不管它是否有效。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容