Objective-C Runtime:深入理解類與對象

在那櫻花盛開的季節

概述

常說Objective-C是一門動態語言,那么問題來了,這個動態表現在那些方面呢?

其實最主要的表現就是Objective-C將很多靜態語言在編譯和鏈接時做的事情放到了運行時去做,它在運行時實現了對類、方法、成員變量、屬性等信息的管理機制。

同時,運行時機制為我們開發過程提供很多便利之處,比如:

  • 在運行時創建或者修改一個類;
  • 在運行時修改成員變量、屬性等;
  • 在運行時進行消息分發和分發綁定;
    ......
    與之對應實現的就是Objective-C的Runtime機制。

Objective-C的Runtime目前有兩個版本:Leagcy RuntimeModen RuntimeLeagcy Runtime是最早期給32位Mac OX Apps使用的,而Moden Runtime是給64位Mac OX AppsiOS Apps使用的。

Runtime基本是C和匯編編寫的,有一系列函數和數據結構組成的,具有公共接口的動態共享庫,可見蘋果為了動態系統的高效而作出的努力,你可以在這里下載到蘋果維護的開源代碼。

同時,GNU也有一個開源的Runtime版本,他們在努力保持一致。其頭文件都存放在/usr/include/objc目錄下。在Objective-C Runtime Reference中,有對Runtime函數使用細節的文檔。

類與對象

類的數據結構(Class)

類的數據結構可以在objc/runtime.h源碼中找到,如下所示:

struct objc_class {
   //isa指針指向Class
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class  OBJC2_UNAVAILABLE; // 父類
    const char * _Nonnull name OBJC2_UNAVAILABLE; // 類名
    long version  OBJC2_UNAVAILABLE; // 類的版本信息,默認為0
    long info  OBJC2_UNAVAILABLE; // 類信息,供運行時使用的一些位標識
    long instance_size  OBJC2_UNAVAILABLE; // 類的實例變量大小
   // 類的成員變量列表
    struct objc_ivar_list * _Nullable ivars  OBJC2_UNAVAILABLE; 
    // 方法定義列表
    struct objc_method_list * _Nullable * _Nullable methodLists  OBJC2_UNAVAILABLE; 
    // 方法緩存
    struct objc_cache * _Nonnull cache  OBJC2_UNAVAILABLE; 
    // 協議列表
    struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE; 
#endif

} OBJC2_UNAVAILABLE;

在Objective-C中是由Class表示的,Class是一個指向struct objc_class的指針。

typedef struct objc_class *Class;

在這個類的數據結構中,有幾個字段需要解釋一下:

  • isa:在大多數的面向對象的語言中,都有對象的概念。其中,對象是類的實例,是通過類數據結構的定義創建出來的,對象的isa指針是指向其所屬類的。同時,在Objective-C語言中,類本身也是一個對象,類作為對象時isa指針指向元類(Meta Class),后面會詳解;

  • super_class:指向該類的父類,如果該類已經是根類(NSObjectNSProxy),則 其super_classNULL

  • version:該字段可以獲取類的版本信息,在對象的序列化中可以通過類的版本信息來標識出不同版本的類定義中實例變量布局的改變。

objc_cache與cache

上文object_class中結構體中的cache字段,是用來緩存使用過的方法。這個字段是一個指向objc_cache的指針,具體數據結構如下所示:

struct objc_cache {
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
    unsigned int occupied                                    OBJC2_UNAVAILABLE;
    Method buckets[1]                                        OBJC2_UNAVAILABLE;
};

字段的具體描述如下:

  • mask:整數類型,指定分配的緩存bucket的總數。在方法查找過程中,Objective-C runtime使用這個字段來確定開始線性查找數組的索引位置。指向方法selector的指針與該字段做一個AND位操作(index = (mask & selector))。這可以看作為一個簡單的hash散列算法;

  • occupied:一個整數,指定實際占用的緩存bucket的總數;

  • buckets:指向Method數據結構指針的數組。這個數組可能包含不超過mask+1個元素。需要注意的是,指針可能是NULL,表示這個緩存bucket沒有被占用。另外被占用的bucket可能是不連續的。這個數組可能會隨著時間而增長。

關于上文object_class中結構體中的cache字段,對它的解釋如下:

  • cache:用于緩存最近使用的方法,一個對象可響應的方法列表中通常只有一部分是經常被調用的,cache 則是用來緩存最常調用的方法,從而避免每次方法調用時都去查找對象的整個方法列表,提升性能。

  • 在一些結構較為復雜的類關系中,一個對象的響應方法可能來自于繼承的類結構中,此情況下查找相應的響應方法時就會比較耗時,通常使用cache緩存可以減低查找時間;

舉個栗子:

NSDictionary *dic= [[NSDictionary alloc] init];//執行過程

其緩存調用方法的流程:

    1. [NSDictionary alloc]先被執行。由于NSDictionary沒有+alloc方法,于是去父類NSObject中去查找;
    1. 檢測NSObject是否響應+alloc方法,發現響應,于是檢測NSDictionary類,并根據其所需的內存空間大小開始分配內存空間,然后把isa指針指向NSDictionary類。同時,+alloc方法也被加進對應類的cache列表里;
  • 3.執行-init方法,如果NSDictionary響應該方法,則直接將該方法加入cache列表;如果不響應,則去父類查找;
    1. 在后期的操作中,如果再以[[NSDictionary alloc] init]這種方式來創建數組,則會直接從cache中取出相應的方法,直接調用。

元類(Meta Class)

上面講到,有時候類也是一個對象,這種類對象是某一種類的實例,這種類就是元類(Meta Class)。

好比類與對應的實例描述一樣,元類則是類作為對象的描述。元類中方法列表對應的是類方法(Class Method)列表,這正是類作為一個對象所需要的。

當調用該方法[NSArray alloc]時,Runtime就會在對應的元類方法列表查找其類對應的方法,并匹配調用。

Meta Class是類對象的類。

官方的解釋如下所示:

Since a class is an object, it must be an instance of some other class: a metaclass. The metaclass is the description of the class object, just like the class is the description of ordinary instances. Class methods are described by the metaclass on behalf of the class object, just like instance methods are described by the class on behalf of the instance objects.

至此,又有了新的疑問:元類又是誰的實例呢?它的isa又指向誰呢?答案如下圖所示:

Class vs. Meta Class

由上圖可以看出,元類的isa都指向根元類(Root Meta Class),即元類都是根元類的實例。

而根元類(Root Meta Class)的isa則指向自己,這樣就不會無休止的關聯下去了。

圖中同樣展示類和元類的繼承關系,非常清晰易懂。

類的實例數據結構

在 Objective-C 中類的實例的數據結構是定義在struct objc_object 中(objc/objc.h):

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

可以看出,這個結構體只有一個字段,即指向該實例所屬類的isa指針。

這個指針跟上面介紹的類的isa不一樣:類的isa指向對應的元類(Meta Class),實例的isa則是指向對應的類(Class),而這個Class里包含上述所講的數據:父類、類名、方法列表等等。

當我們向一個類的實例發送消息時,Runtime會根據實例對象的isa找到這個實例對象所屬的類,然后再在這個類的方法列表和其父類的方法列表中查找與消息相對應的selector指向的方法,進而執行目標方法。

當創建某一個類的實例時,分配的內存中會包含一個objc_object數據結構,然后是類的實例變量的相關數據。

NSObject類的allocallocWithZone:方法是使用函數class_createInstance來創建objc_object數據結構。

我們常見的id是一個struct objc_object類型的指針。id類型的對象可以轉換為任何一種類型的對象,它的作用有點類似 C 語言中的 void * 指針類型。

/// A pointer to an instance of a class.
typedef struct objc_object *id;

相關函數

Objective-C的Runtime我們提供了很多運行時狀態跟類與對象相關的函數。類的操作方法大部分是以class_ 為前綴的,而對象的操作方法大部分是以objc_object_為前綴,具體以分類的形式進行討論。

類相關函數

類的相關函數大部分是與objc_class結構體各個字段相關的方法。

類名
// 獲取類的類名
const char * class_getName ( Class cls );
  • 如果cls傳入nil時,則返回nil字符串;
父類(super_class)和元類(meta_class)
// 獲取類的父類
Class class_getSuperclass ( Class cls );
// 判斷給定的Class是否是一個元類
BOOL class_isMetaClass ( Class cls );
  • class_getSuperclass函數,當clsNil或者cls為根類時,返回Nil。我們可以使用NSObject類的superclass方法來達到同樣的目的;

  • class_isMetaClass函數,如果是cls是元類,則返回YES;如果否或者傳入的clsNil,則返回NO

實例變量大小(instance_size)
// 獲取實例大小
size_t class_getInstanceSize ( Class cls );
成員變量(ivars)及屬性

objc_class中,所有的成員變量、屬性的信息是放在鏈表ivars中的。ivars是一個數組,數組中每個元素是指向Ivar(變量信息)的指針。

1、成員變量操作函數:

// 獲取類中指定名稱實例成員變量的信息
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 );
  • 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、屬性相關的操作函數:

// 獲取指定的屬性
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 );

3.MAC OS X系統支持使用垃圾回收器,Runtime提供了幾個函數來確定一個對象的內存區域是否可以被垃圾回收器掃描,以處理strong/weak引用。這幾個函數定義如下:

const uint8_t * class_getIvarLayout ( Class cls );
void class_setIvarLayout ( Class cls, const uint8_t *layout );
const uint8_t * class_getWeakIvarLayout ( Class cls );
void class_setWeakIvarLayout ( Class cls, const uint8_t *layout );

通常情況下,我們不需要去主動調用這些方法;在調用objc_registerClassPair時,會生成合理的布局。

方法(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 );
// 獲取所有方法的數組
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 );
  • class_addMethod的實現會覆蓋父類的方法實現,但不會取代本類中已存在的實現,如果本類中包含一個同名的實現,則函數會返回NO。

  • 如果要修改已存在實現,可以使用method_setImplementation。一個Objective-C方法是一個簡單的C函數,它至少包含兩個參數self_cmd。所以,我們的實現函數(IMP參數指向的函數)至少需要兩個參數,如下所示:

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

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

參數types是一個描述傳遞給方法的參數類型的字符數組,這就涉及到類型編碼。

  • class_getInstanceMethodclass_getClassMethod函數,與class_copyMethodList不同的是,這兩個函數都會去搜索父類的實現;

  • class_copyMethodList函數,返回包含所有實例方法的數組。如果需要獲取類方法,則可以使用class_copyMethodList(object_getClass(cls), &count)(一個類的實例方法是定義在元類里面)。該列表不包含父類實現的方法outCount參數返回方法的個數,在獲取到方法列表后,我們需要使用free()方法來釋放它;

  • class_replaceMethod函數,該函數的行為可以分為兩種:如果類中不存在name指定的方法,則類似于class_addMethod函數一樣會添加方法;如果類中已存在name指定的方法,則類似于method_setImplementation一樣去替代原方法的實現;

  • class_getMethodImplementation函數,該函數在向類實例發送消息時會被調用,并返回一個指向方法實現函數的指針。

  • 這個函數會比method_getImplementation(class_getInstanceMethod(cls, name))更快。返回的函數指針可能是一個指向runtime內部的函數,而不一定是方法的實現。

  • 例如,如果類實例無法響應selector,則返回的函數指針將是運行時消息轉發機制的一部分;

  • class_respondsToSelector函數,我們通常使用NSObject類的respondsToSelector:或者instancesRespondToSelector:方法來達到相同目的。

協議(objc_protocol_list)
// 添加協議
BOOL class_addProtocol ( Class cls, Protocol *protocol );
// 返回類是否實現指定的協議
BOOL class_conformsToProtocol ( Class cls, Protocol *protocol );
// 返回類實現的協議列表
Protocol * class_copyProtocolList ( Class cls, unsigned int *outCount );
  • class_conformsToProtocol函數可以使用NSObject類的conformsToProtocol:方法來替代;
  • class_copyProtocolList函數返回的是一個數組,在使用后我們需要使用free()手動釋放。
版本(version)
// 獲取版本號
int class_getVersion ( Class cls );
// 設置版本號
void class_setVersion ( Class cls, int version );

其它

runtime還提供了兩個不直接使用的函數來供CoreFoundationtool-free bridging使用,即:

Class objc_getFutureClass ( const char *name );
void objc_setFutureClass ( Class cls, const char *name );

使用上述函數時,需要特別的注意一下細節信息和使用規范,具體可以查閱 Objective-C Runtime Reference

動態創建類與對象

Runtime提供在運行時創建類與對象的方法。

動態創建類
// 創建一個新類和元類
Class objc_allocateClassPair ( Class superclass, const char *name, size_t extraBytes );
// 銷毀一個類及其相關聯的類
void objc_disposeClassPair ( Class cls );
// 在應用中注冊由objc_allocateClassPair創建的類
void objc_registerClassPair ( Class cls );
  • objc_allocateClassPair函數:如果我們要創建一個根類,則superclass指定為NilextraBytes通常指定為0,該參數是分配給類和元類對象尾部的索引ivars的字節數;

  • 創建一個新類,首先,我們需要調用objc_allocateClassPair。然后使用諸如class_addMethodclass_addIvar等函數來為新創建的類添加方法、實例變量和屬性等。

  • 完成這些后,我們需要調用objc_registerClassPair函數來注冊類,之后這個新類就可以在程序中使用了;

  • 實例方法和實例變量應該添加到類自身上,而類方法應該添加到類的元類上;

  • objc_disposeClassPair函數用于銷毀一個類,不過需要注意的是,如果程序運行中還存在類或其子類的實例,則不能調用針對類調用該方法,在后面的栗子中也有該方面的講解。

動態創建對象
// 創建類實例
id class_createInstance ( Class cls, size_t extraBytes );
// 在指定位置創建類實例
id objc_constructInstance ( Class cls, void *bytes );
// 銷毀類實例
void * objc_destructInstance ( id obj );
  • class_createInstance函數:創建實例時,會在默認的內存區域為類分配內存。

  • extraBytes參數表示分配的額外字節數。這些額外的字節可用于存儲在類定義中所定義的實例變量之外的實例變量,該函數在ARC環境下無法使用 ;

  • 調用class_createInstance的效果與+alloc方法類似。不過在使用class_createInstance時,我們需要確切的知道我們要用它來做什么。在下面的例子中,我們用NSString來測試一下該函數的實際效果:

- (void)testInstanceMethod{
    id theObject = class_createInstance([NSString class], sizeof(unsigned));
    id str1 = [theObject init];
    NSLog(@"%@", [str1 class]);
    
    id str2 = [[NSString alloc] initWithString:@"test"];
    NSLog(@"%@", [str2 class]);
}

輸出結果:

2018-03-21 22:55:18.503665+0800 RuntimeUsage[2774:32008] NSString
2018-03-21 22:55:21.624606+0800 RuntimeUsage[2774:32008] __NSCFConstantString
  • 可以看到,使用class_createInstance函數獲取的是NSString實例,而不是類簇中的默認占位符類__NSCFConstantString;

  • objc_constructInstance函數:在指定的位置(bytes)創建類實例;

  • objc_destructInstance函數:銷毀一個類的實例,但不會釋放并移除任何與其相關的引用;

實例操作函數

實例操作函數主要是針對我們創建的實例對象的一系列操作函數。

我們可以使用這組函數來從實例對象中獲取我們想要的一些信息,如實例對象中變量的值。這組函數可以分為三小類:

1、針對整個對象進行操作的函數,這類函數包含:

// 返回指定對象的一份拷貝
id object_copy ( id obj, size_t size );
// 釋放指定對象占用的內存
id object_dispose ( id obj );

有這樣一種場景,假設我們有類A和類B,且類B是類A的子類。

類B通過添加一些額外的屬性來擴展類A。現在我們創建了一個A類的實例對象,并希望在運行時將這個對象轉換為B類的實例對象,這樣可以添加數據到B類的屬性中。

這種情況下,我們沒有辦法直接轉換,因為B類的實例會比A類的實例更大,沒有足夠的空間來放置對象。此時,我們就要以使用以上幾個函數來處理這種情況,如下代碼所示:

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

2、針對對象實例變量進行操作的函數,這類函數包含:

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

如果實例變量的Ivar已經知道,那么調用object_getIvar會比object_getInstanceVariable函數快,相同情況下,object_setIvar也比object_setInstanceVariable快。

3、針對對象的類進行操作的函數,這類函數包含:

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

獲取類定義

Objective-C動態運行庫會自動注冊我們代碼中定義的所有的類。

我們也可以在運行時創建類定義并使用objc_addClass函數來注冊它們。Runtime提供了一系列函數來獲取類定義相關的信息,這些函數主要包括:

// 獲取已注冊的類定義的列表
int objc_getClassList ( Class *buffer, int bufferCount );
// 創建并返回一個指向所有已注冊類的指針列表
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函數:獲取已注冊的類定義的列表。我們不能假設從該函數中獲取的類對象是繼承自NSObject體系的,所以在這些類上調用方法是,都應該先檢測一下這個方法是否在這個類中實現。

下面代碼演示了該函數的用法:

- (void)testGetNumberClass{
    int numClasses;
    Class *classes = NULL;
    
    numClasses = objc_getClassList(NULL, 0);
    if (numClasses > 0) {
        classes = (Class *) malloc(sizeof(Class) * numClasses);
        numClasses = objc_getClassList(classes, numClasses);
        NSLog(@"count of classes:%d", numClasses);
        
        for (int i = 0; i < numClasses; i ++) {
            Class cls = classes[I];
           NSLog(@"class name: %s", class_getName(cls));
        }
    }
    free(classes);
}

輸出的結果:

2018-03-21 23:58:57 RuntimeUsage[3378:65534] count of classes:11809
2018-03-21 23:59 RuntimeUsage[3378:65534] class name: _CNZombie_
2018-03-21 23:59 RuntimeUsage[3378:65534] class name: JSExport
2018-03-21 23:59 RuntimeUsage[3378:65534] class name: NSLeafProxy
2018-03-21 23:59 RuntimeUsage[3378:65534] class name: NSProxy
2018-03-21 23:59 RuntimeUsage[3378:65534] class name: _UITargetedProxy
2018-03-21 23:59 RuntimeUsage[3378:65534] class name: _UIViewServiceReplyControlTrampoline
2018-03-21 23:59:04  RuntimeUsage[3378:65534] class name: _UIViewServiceReplyAwaitingTrampoline
····  還有很多
  • 獲取類定義的方法有三個:objc_lookUpClassobjc_getClassobjc_getRequiredClass

  • 如果類在運行時未注冊,則objc_lookUpClass會返回nil,而objc_getClass會調用類處理回調,并再次確認類是否注冊,如果確認未注冊,再返回nil

  • objc_getRequiredClass函數的操作與objc_getClass相同,只不過如果沒有找到類,則會殺死進程。

  • objc_getMetaClass函數:如果指定的類沒有注冊,則該函數會調用類處理回調,并再次確認類是否注冊,如果確認未注冊,再返回nil。不過,每個類定義都必須有一個有效的元類定義,所以這個函數總是會返回一個元類定義,不管它是否有效。

運行時操作操作類與對象的示例代碼

  • 實例、類、父類、元類關系結構的示例代碼

首先,創建繼承關系為Animal->Dog->NSObject的幾個類,然后使用Runtime的方法打印其中的關系,運行結果如下所示:

- (void)verifyClassTypeRelation{
    
    //Use `object_getClass` get Class's `isa`
    
    Dog *aDog = [[Dog alloc] init];
    Class dogCls = object_getClass(aDog);
    NSLog(@"isa->%@ , super_class->%@", dogCls, class_getSuperclass(dogCls));
    // print:isa->Dog, super_class->Animal
    Class dogMetaCls = objc_getMetaClass([NSStringFromClass(dogCls) UTF8String]);
    if (class_isMetaClass(dogMetaCls)) {
        NSLog(@"YES, metaCls->%@ , metaCls's super_Class->%@, metaCls's isa->%@", dogMetaCls, class_getSuperclass(dogMetaCls), object_getClass(dogMetaCls));
        //print: YES, metaCls->Dog , metaCls's super_Class->Animal, metaCls's isa->NSObject
    }else{
        NSLog(@"NO");
    }
    
    Animal *animal = [[Animal alloc] init];
    Class animalCls = object_getClass(animal);
    NSLog(@"isa->%@ , super_class->%@", animalCls, class_getSuperclass(animalCls));
    //print: isa->Animal , super_class->NSObject
    Class animalMetaCls = objc_getMetaClass([NSStringFromClass(animalCls) UTF8String]);
    if (class_isMetaClass(animalMetaCls)) {
        NSLog(@"YES, metaCls->%@ , metaCls's super_Class->%@, metaCls's isa->%@", animalMetaCls, class_getSuperclass(animalMetaCls), object_getClass(animalMetaCls));
        //print:YES, metaCls->Animal , metaCls's super_Class->NSObject, metaCls's isa->NSObject
    }else{
        NSLog(@"NO");
    }
    
    Class viewMetaCls = objc_getMetaClass([NSStringFromClass([UIView class]) UTF8String]);
    if (class_isMetaClass(viewMetaCls)) {
        NSLog(@"YES, metaCls->%@ , metaCls's super_Class->%@, metaCls's isa->%@", viewMetaCls, class_getSuperclass(viewMetaCls), object_getClass(viewMetaCls));
        //print:YES, metaCls->UIView , metaCls's super_Class->UIResponder, metaCls's isa->NSObject
    }
    
    Class rootMetaCls = objc_getMetaClass([NSStringFromClass([NSObject class]) UTF8String]);
    if (class_isMetaClass(rootMetaCls)) {
        NSLog(@"YES, metaCls->%@ , metaCls's super_Class->%@, metaCls's isa->%@", rootMetaCls, class_getSuperclass(rootMetaCls), object_getClass(rootMetaCls));
        //print:YES, metaCls->NSObject , metaCls's super_Class->NSObject, metaCls's isa->NSObject
    }
    
}

打印信息如下所示:

isa->Dog, super_class->Animal
YES, metaCls->Dog , metaCls's super_Class->Animal, metaCls's isa->NSObject
isa->Animal , super_class->NSObject
YES, metaCls->Animal , metaCls's super_Class->NSObject, metaCls's isa->NSObject
YES, metaCls->UIView , metaCls's super_Class->UIResponder, metaCls's isa->NSObject
YES, metaCls->NSObject , metaCls's super_Class->NSObject, metaCls's isa->NSObject

需要特別注意一下,Object_getClass可以獲取當前對象的isa。以Dog類打印信息為例,解釋一下具體實現的原理:

isa->Dog, super_class->Animal
YES, metaCls->Dog , metaCls's super_Class->Animal, metaCls's isa->NSObject
  • 首先,通過Object_getClass獲取實例aDog的Class(isa)為Dog;

  • 然后,通過class_getSuperclass獲取Dog的父類為Animal類;

  • 通過objc_getMetaClass指定類名,獲取對應的元類,通過class_isMetaClass方法可以判斷一個類是否為指定類的元類,這里確認后,打印出YES,打印出的元類名稱為Dog;打印元類父類為Animal;在通過Object_getClass獲取元類的isa,指向NSObject

同理可得,AnimalUIView打印信息解釋同上。NSobject,它元類的isa指針還是指向自己的類——NSobject。打印的信息與上述的關系圖保持一致。

動態操作類與實例的代碼

動態創建類的源碼

/***********************************************************************
* _objc_allocateFutureClass
* Allocate an unresolved future class for the given class name.
* Returns any existing allocation if one was already made.
* Assumes the named class doesn't exist yet.
* Locking: acquires runtimeLock
**********************************************************************/
Class _objc_allocateFutureClass(const char *name)
{
    rwlock_writer_t lock(runtimeLock);

    Class cls;
    NXMapTable *map = futureNamedClasses();

    if ((cls = (Class)NXMapGet(map, name))) {
        // Already have a future class for this name.
        return cls;
    }

    cls = _calloc_class(sizeof(objc_class));
    addFutureNamedClass(name, cls);

    return cls;
}
- (void)dynamicAddMethod{
    
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
    
    //1、Create and register class, add method to class
    Class cls = objc_allocateClassPair([Animal class], "Lion", 0);// 當為`Cat`時,返回的創建類cat地址為0x0,將`Cat`作為關鍵字
    //method: 返回`int32_t`,type使用`i`;參數:`id self`,type使用`@`;`SEL _cmd`,type使用`:`;
    //`NSDictionary *dic`,type使用`@`.綜上,type使用'i@:@'
    ///具體類型可參照 https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html
    BOOL isAddSuccess = class_addMethod(cls, @selector(howlMethod), (IMP)testRuntimeMethodIMP, "i@:@");
    NSLog(@"%@", isAddSuccess ? @"添加方法成功" : @"添加方法失敗");
    //You can only register a class once.
    objc_registerClassPair(cls);
    
    //2、Create instance of class
    id whiteCat = [[cls alloc] init];
     NSLog(@"%@, %@", object_getClass(whiteCat), class_getSuperclass(object_getClass(whiteCat)));
    // Print: Lion, Animal
    Class whiteCatCls = object_getClass(whiteCat);
    Class metaCls = objc_getMetaClass([NSStringFromClass(whiteCatCls) UTF8String]);
    if (class_isMetaClass(metaCls)) {
        NSLog(@"YES, %@, %@, %@", metaCls, class_getSuperclass(metaCls), object_getClass(metaCls));
        // Print: YES, Lion, Animal, NSObject
    }else{
        NSLog(@"NO");
    }
    
    //3、Method of class
    unsigned int methodCount = 0;
    Method *methods = class_copyMethodList(cls, &methodCount);
    for (int32_t i = 0; i < methodCount; i ++) {
        Method aMethod = methods[I];
        NSLog(@"%@, %s", NSStringFromSelector(method_getName(aMethod)), method_getTypeEncoding(aMethod));
        //print:howlMethod, I@:@
    }
    free(methods);
    
    //4、call method
    int32_t result = (int)[whiteCat performSelector:@selector(howlMethod) withObject:@{@"name":@"lion", @"sex": @"male"}];
    NSLog(@"%d", result);//print:9
    
    //5、destory instance and class
    whiteCat = nil;
    
    // Do not call this function if instances of the cls class or any subclass exist.
    objc_disposeClassPair(cls);
    
#pragma clang diagnostic pop
}

打印的信息如下所示:

添加方法成功
 Lion, Animal
YES, Lion, Animal, NSObject
howlMethod, I@:@
testRuntimeMethodIMP: {
    name = lion;
    sex = male;
}
9

在執行objc_allocateClassPair中,類的名稱設置為Cat時,創建出的Class的地址始終指向0x0,創建類失敗。

猜測其中的原因可能是Cat與內部的關鍵字沖突了,導致類創建失敗,改為cat或者其他的都可以創建成功;

  • 在上面的代碼中,在運行時動態創建了Animal 的一個子類:Lion;接著為這個類添加了方法和實現;
  • 打印了 Lion 的類、父類、元類相關信息;
  • 遍歷和打印了 Lion 的方法的相關信息;
  • 調用了 Lion 的方法;
  • 最后銷毀了實例和類。

針對上述代碼,有幾點需要特殊說明一下:

  • 對于#pragma clang diagnostic...幾行代碼,是用于忽略編譯器對于未聲明@selector的警告信息的,在代碼中,我們動態地為一個類添加方法,不會事先聲明的;

  • class_addMethod() 函數的最后一個參數 types 是描述方法返回值和參數列表的字符串。

  • 我們的代碼中的用到的 i@:@ 四個字符分別對應著:返回值 int32_t、參數id self、參數 SEL _cmd、參數 NSDictionary *dic。這個其實就是類型編碼(Type Encoding)的概念。

  • 在 Objective-C 中,為了協助 Runtime 系統,編譯器會將每個方法的返回值和參數列表編碼為一個字符串,這個字符串會與方法對應的 selector 關聯。更詳細的知識可以查閱 Type Encodings;

  • 使用 objc_registerClassPair() 函數需要注意,不能注冊已經注冊過的類;

  • 使用objc_disposeClassPair() 函數時需要注意,當一個類的實例和子類還存在時,不能去銷毀一個類,謹記;

isKindOf 和 isMemberOf

舉個栗子:

@interface TestMetaClass: NSObject
@end

@implementation TestMetaClass
@end

int main(int argc, char * argv[]) {
    @autoreleasepool {
        BOOL result1 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
        BOOL result2 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
        BOOL result3 = [(id)[TestMetaClass class] isMemberOfClass:[TestMetaClass class]];
        BOOL result4 = [(id)[TestMetaClass class] isKindOfClass:[TestMetaClass class]];
        NSLog(@"%d %d %d %d", result1, result2, result3, result4);
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

//log
2018-02-09 16:45:54.048040+0800 RuntimeUsage[9220:5754652] 0 1 0 0

關于isMemberOfClassisKindOfClassObject.mm中的實現,具體如下:

- (BOOL)isMemberOf:aClass
{
     return isa == (Class)aClass;
}

- (BOOL)isKindOf:aClass
{
     Class cls;
     for (cls = isa; cls; cls = cls->superclass)
          if (cls == (Class)aClass)
               return YES;
     return NO;
}
  • 在result1中,從isMemberOf看出NSObject classisa第一次會指向NSObjectMeta Class,因此NSObjectMeta ClassNSObject class是不相等,返回FALSE

  • 在result2中,isKindOf第一次指向NSObjectMeta Class,接著執行superclass時,根元類NSObjectMeta Class根據上面所講的其superclass指針會閉環指向NSObject class,從而結果值為TRUE;

  • 在result3中,isa會指向TestMetaClassMeta Class,與TestMetaClass Class不相等,結果值為FALSE;

  • 在result4中,第一次是TestMetaClass Meta Class,第二次super class后就是NSObject Meta Class,結果值為FALSE;

以上再次驗證了,NSObject Meta Classisa指針指向自身,其super class指向NSObject

小結

本文著重講解了在Runtime時類與對象相關方法和數據結構,通過這些講解可以讓大家對Objective-C底層類與對象實現有大致的了解,并且可以為大家平常編程過程提供一些思路上的啟發。

測試使用的栗子(Demo)都在籃子里

參考

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,494評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,283評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,953評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,714評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,410評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,940評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,776評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,210評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,654評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373