430, runtime的原理(面試點(diǎn):Objective-C是一門動(dòng)態(tài)語言,isa指針 對(duì)象的isa指針是指類對(duì)象,類對(duì)象的isa指針指向元類,一個(gè)對(duì)象或者實(shí)例就是一個(gè)struct objc_...

方法的調(diào)用[p eat],會(huì)被編譯器轉(zhuǎn)成runtime庫中的objc_msgSend調(diào)用的方式來執(zhí)行,即:

[p eat] 轉(zhuǎn)
objc_msgSend(p, sel_registerName("eat"))。

第一步:對(duì)象通過isa指針找到它所繼承的類class;
第二步:在classmethod_list中查找對(duì)應(yīng)的方法;
第三步:如果未查找到當(dāng)前方法,會(huì)向superclass類中查找,直到找到當(dāng)前調(diào)用的方法。

如果每次調(diào)用方法都需要遍歷,系統(tǒng)消耗比較大,因此需要對(duì)常用的方法做緩存操作,每次查找先找緩存,就可以避免大量的無效操作。
每一個(gè)對(duì)象都存在一個(gè)isa指針,指向?qū)ο蟮念悾愐彩且粋€(gè)對(duì)象也存在一個(gè)isa指針指向元類,元類指向根元類,根元類指向自己。類中保存所有的實(shí)列方法,元類保存了所有類方法。方法查找過程:

image.png

我們都知道Objective-C是一門動(dòng)態(tài)語言, 動(dòng)態(tài)之處體現(xiàn)在它將許多靜態(tài)語言編譯鏈接時(shí)要做的事通通放到運(yùn)行時(shí)去做, 這大大增加了我們編程的靈活性.
毫不過分地說, Runtime就是OC的靈魂.

接下來我就要撥開OC最外層的外衣, 帶大家看看OC的真面目(C/C++).

目錄

1.類和對(duì)象
2.消息發(fā)送和轉(zhuǎn)發(fā)
3.KVO原理

深入代碼理解instance、class object、metaclass

面向?qū)ο缶幊讨校钪匾母拍罹褪穷?,下面我們就從代碼入手,看看OC是如何實(shí)現(xiàn)類的。

instance對(duì)象實(shí)例

我們經(jīng)常使用id來聲明一個(gè)對(duì)象,那id的本質(zhì)又是什么呢?打開#import<objc/objc.h>文件,可以發(fā)現(xiàn)以下幾行代碼

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

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

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

通過注釋和代碼不難發(fā)現(xiàn),我們創(chuàng)建的一個(gè)對(duì)象或?qū)嵗鋵?shí)就是一個(gè)struct objc_object結(jié)構(gòu)體,而我們常用的id也就是這個(gè)結(jié)構(gòu)體的指針。

這個(gè)結(jié)構(gòu)體只有一個(gè)成員變量,這是一個(gè)Class類型的變量isa,也是一個(gè)結(jié)構(gòu)體指針,那這個(gè)指針又指向什么呢?

面向?qū)ο笾忻恳粋€(gè)對(duì)象都必須依賴一個(gè)類來創(chuàng)建,因此對(duì)象的isa指針就指向?qū)ο笏鶎俚念惛鶕?jù)這個(gè)類模板能夠創(chuàng)建出實(shí)例變量、實(shí)例方法等。
比如有如下代碼

NSString *str = @"Hello World";

通過上文我們知道這個(gè)str對(duì)象本質(zhì)就是一個(gè)objc_object結(jié)構(gòu)體,而這個(gè)結(jié)構(gòu)體的成員變量isa指針則表明了str is a NSString,因此這個(gè)isa就指向了NSString類,這個(gè)NSString類其實(shí)是類對(duì)象,不明白就繼續(xù)往下看。

class object(類對(duì)象)/metaclass(元類)

繼續(xù)查看結(jié)構(gòu)體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;
/* Use `Class` instead of `struct objc_class *` */

struct objc_classs結(jié)構(gòu)體里存放的數(shù)據(jù)稱為元數(shù)據(jù)(metadata),通過成員變量的名稱我們可以猜測(cè)里面存放有指向父類的指針、類的名字、版本、實(shí)例大小、實(shí)例變量列表、方法列表、緩存、遵守的協(xié)議列表等,這些信息就足夠創(chuàng)建一個(gè)實(shí)例了,該結(jié)構(gòu)體的第一個(gè)成員變量也是isa指針,這就說明了Class本身其實(shí)也是一個(gè)對(duì)象,我們稱之為類對(duì)象,類對(duì)象在編譯期產(chǎn)生用于創(chuàng)建實(shí)例對(duì)象,是單例,因此前文中的栗子其實(shí)應(yīng)該表達(dá)為str的isa指針指向了NSString類對(duì)象那么這個(gè)結(jié)構(gòu)體的isa指針又指向什么呢?

類對(duì)象中的元數(shù)據(jù)存儲(chǔ)的都是如何創(chuàng)建一個(gè)實(shí)例的相關(guān)信息,那么類對(duì)象類方法應(yīng)該從哪里創(chuàng)建呢?就是從isa指針指向的結(jié)構(gòu)體創(chuàng)建,類對(duì)象的isa指針指向的我們稱之為元類(metaclass),元類中保存了創(chuàng)建類對(duì)象以及類方法所需的所有信息,因此整個(gè)結(jié)構(gòu)應(yīng)該如下圖所示:

image.png

通過上圖我們可以清晰的看出來一個(gè)實(shí)例對(duì)象也就是struct objc_object結(jié)構(gòu)體它的isa指針指向類對(duì)象,類對(duì)象的isa指針指向了元類,super_class指針指向了父類的類對(duì)象,而元類super_class指針指向了父類的元類,那元類的isa指針又指向了什么?為了更清晰的表達(dá)直接使用一個(gè)大神畫的圖。

image.png

通過上圖我們可以看出整個(gè)體系構(gòu)成了一個(gè)自閉環(huán),如果是從NSObject中繼承而來的上圖中的Root class就是NSObject。至此,整個(gè)實(shí)例、類對(duì)象、元類的概念也就講清了,接下來我們?cè)诖a中看看這些概念該怎么應(yīng)用。

如圖所示

  • 1.每一個(gè)實(shí)例包含一個(gè)isa對(duì)象

  • 2.isa指向類,類是一個(gè)objc_class結(jié)構(gòu)體,包含實(shí)例的方法列表,參數(shù)列表,category等,除此之外,objc_class中還有一個(gè)super_class,指向其類的父類,isa指針,這里的isa指針指向元類,即metaClass,元類存儲(chǔ)類方法等信息

  • 3.元類里也包含isa指針,元類里的isa指針指向 根元類,根元類的isa指針指向自己

  • 4.obj_msgSend發(fā)送實(shí)例消息的時(shí)候,先找到實(shí)例,然后通過實(shí)例的isa指針找到類的方法列表及參數(shù)列表等,如果找到,返回,如果沒有找到,則通過super_class在其父類中重復(fù)此過程

  • 5.obj_msgSend發(fā)送類消息的時(shí)候,通過類的isa,找到元類,然后流程與步驟4相同

類對(duì)象

有如下代碼

@interface Person : NSObject

@property (nonatomic, copy) NSString* name;
@property (nonatomic, assign) NSUInteger age;

@end

@implementation Person

@synthesize name = _name;
@synthesize age = _age;

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] init];
        Class c1 = [p class];
        Class c2 = [Person class];
        //輸出 1
        NSLog(@"%d", c1 == c2);
    }
    return 0;
}

c1是通過一個(gè)實(shí)例對(duì)象獲取的Class,實(shí)例對(duì)象可以獲取到其類對(duì)象,類名作為消息的接受者時(shí)代表的是類對(duì)象,因此類對(duì)象獲取Class得到的是其本身,同時(shí)也印證了類對(duì)象是一個(gè)單例的想法。
那么如果我們想獲取isa指針的指向?qū)ο竽兀?/p>

介紹兩個(gè)函數(shù)

OBJC_EXPORT BOOL class_isMetaClass(Class cls) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);

OBJC_EXPORT Class object_getClass(id obj) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);

class_isMetaClass用于判斷Class對(duì)象是否為元類,object_getClass用于獲取對(duì)象的isa指針指向的對(duì)象。

再看如下代碼:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] init];
        //輸出1
        NSLog(@"%d", [p class] == object_getClass(p));
        //輸出0
        NSLog(@"%d", class_isMetaClass(object_getClass(p)));
        //輸出1
        NSLog(@"%d", class_isMetaClass(object_getClass([Person class])));
        //輸出0
        NSLog(@"%d", object_getClass(p) == object_getClass([Person class]));
    }
    return 0;
}

通過代碼可以看出,一個(gè)實(shí)例對(duì)象通過class方法獲取的Class就是它的isa指針指向的類對(duì)象,而類對(duì)象不是元類,類對(duì)象的isa指針指向的對(duì)象是元類。

類和對(duì)象

@interface Person : NSObject {
    NSString *_name;
    int _age;
}

- (void)study;
+ (void)study;

@end

@implementation Person

- (void)study
{
    NSLog(@"instance - study");
}

+ (void)study
{
    NSLog(@"class - study");
}

@end

為了更好地說明類在底層的表現(xiàn)形式是怎樣, 我們將上面代碼利用clang -rewrite-objc Person.m指令將其用C/C++重寫, 一窺究竟.

把不必要的刪除, 整理后為下面

struct _class_t { 
    struct _class_t *isa; // isa指針
    struct _class_t *superclass; // 父類
    void *cache;
    void *vtable;
    struct _class_ro_t *ro; // class的其他信息
};
// class包含的信息
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; // 協(xié)議列表
    const struct _ivar_list_t *ivars; // ivar列表
    const unsigned char *weakIvarLayout;
    const struct _prop_list_t *properties; // 屬性列表
};

// Person(class)
struct _class_t OBJC_CLASS_$_Person  = {
    .isa = &OBJC_METACLASS_$_Person, // 指向Person-metaclass
    .superclass = &OBJC_CLASS_$_NSObject, // 指向NSObject-class
    .cache = &_objc_empty_cache,
    0, // unused, was (void *)&_objc_empty_vtable,
    &_OBJC_CLASS_RO_$_Person, // 包含了實(shí)例方法, ivar信息等
};

// Person(metaclass)
struct _class_t OBJC_METACLASS_$_Person = {
    .isa = &OBJC_METACLASS_$_NSObject, // 指向NSObject-metaclass
    .superclass = &OBJC_METACLASS_$_NSObject, // 指向NSObject-metaclass
    .cache = &_objc_empty_cache,
    0, // unused, was (void *)&_objc_empty_vtable,
    &_OBJC_METACLASS_RO_$_Person, // 包含了類方法
};

原來(顯然), 我們的類其實(shí)就是一個(gè)結(jié)構(gòu)體!!! 類跟我們的對(duì)象一樣, 都有一個(gè)isa指針, 所以類其實(shí)也是對(duì)象的一種.

isa指針

isa指針非常重要, 對(duì)象需要通過isa指針找到它的類, 類需要通過isa找到它的元類. 這在調(diào)用實(shí)例方法和類方法的時(shí)候起到重要的作用.

image.png

實(shí)例對(duì)象在調(diào)用方法時(shí), 首先通過isa指針找到它所屬的類, 然后在類的緩存(cache)里找該方法的IMP, 如果沒有, 則去類的方法列表中查找, 然后找到則調(diào)用該方法, 找不到則報(bào)錯(cuò).

類對(duì)象調(diào)用方法則如出一轍, 通過isa指針找到元類, 然后就跟上述一致了. 這里涉及的發(fā)送消息機(jī)制下面會(huì)詳細(xì)講..

下面展示一些運(yùn)行時(shí)動(dòng)態(tài)獲取對(duì)象和類的屬性的C語言方法

類和類名 :

// 返回對(duì)象的類
Class object_getClass ( id obj );
// 設(shè)置對(duì)象的類
Class object_setClass ( id obj, Class cls );
// 獲取類的父類
Class class_getSuperclass ( Class cls );
// 創(chuàng)建一個(gè)新類和元類
Class objc_allocateClassPair ( Class superclass, const char *name, size_t extraBytes );
// 在應(yīng)用中注冊(cè)由objc_allocateClassPair創(chuàng)建的類
void objc_registerClassPair ( Class cls );
// 銷毀一個(gè)類及其相關(guān)聯(lián)的類
void objc_disposeClassPair ( Class cls );
// 獲取類的類名
const char * class_getName ( Class cls );
// 返回給定對(duì)象的類名
const char * object_getClassName ( id obj );

ivar和屬性 :

// 添加成員變量
BOOL class_addIvar ( Class cls, const char *name, size_t size, uint8_t alignment, const char *types );
// 添加屬性
BOOL class_addProperty ( Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount );
// 返回類的某一ivar
Ivar class_getInstanceVariable(__unsafe_unretained Class cls, const char *name)
// 返回對(duì)象中實(shí)例變量的值
id object_getIvar ( id obj, Ivar ivar );
// 設(shè)置對(duì)象中實(shí)例變量的值
void object_setIvar ( id obj, Ivar ivar, id value );
// 獲取整個(gè)成員變量列表
Ivar * class_copyIvarList ( Class cls, unsigned int *outCount );
// 獲取屬性列表
objc_property_t * class_copyPropertyList ( Class cls, unsigned int *outCount );

方法 :

// 添加方法
BOOL class_addMethod ( Class cls, SEL name, IMP imp, const char *types );
// 獲取實(shí)例方法
Method class_getInstanceMethod ( Class cls, SEL name );
// 獲取類方法
Method class_getClassMethod ( Class cls, SEL name );
// 獲取所有方法的數(shù)組
Method * class_copyMethodList ( Class cls, unsigned int *outCount );
// 替代方法的實(shí)現(xiàn)
IMP class_replaceMethod ( Class cls, SEL name, IMP imp, const char *types );
// 交換兩個(gè)方法的實(shí)現(xiàn)(Method Swizzling)
void method_exchangeImplementations(Method m1, Method m2);

這里說個(gè)注意點(diǎn) : addIvar并不能為一個(gè)已經(jīng)存在的類添加成員變量, 只能為那些運(yùn)行時(shí)動(dòng)態(tài)添加的類, 并且只能在objc_allocateClassPairobjc_registerClassPair這兩個(gè)方法之間才能添加Ivar.

消息發(fā)送和轉(zhuǎn)發(fā)機(jī)制

在OC中, 如果向某對(duì)象發(fā)送消息, 那就會(huì)使用動(dòng)態(tài)綁定機(jī)制來決定需要調(diào)用的方法. OC的方法在底層都是普通的C語言函數(shù), 所以對(duì)象收到消息后究竟要調(diào)用什么函數(shù)完全由運(yùn)行時(shí)決定, 甚至可以在運(yùn)行時(shí)改變執(zhí)行的方法.

[person read:book];
會(huì)被編譯成
objc_msgSend(person, @selector(read:), book);

objc_msgSend的具體流程如下

1. 通過isa指針找到所屬類
2. 查找類的cache列表, 如果沒有則下一步
3. 查找類的"方法列表"
4. 如果能找到與選擇子名稱相符的方法, 就跳至其實(shí)現(xiàn)代碼
5. 找不到, 就沿著繼承體系繼續(xù)向上查找
6. 如果能找到與選擇子名稱相符的方法, 就跳至其實(shí)現(xiàn)代碼
7. 找不到, 執(zhí)行"消息轉(zhuǎn)發(fā)".
image.png

消息轉(zhuǎn)發(fā)

上面我們提到, 如果到最后都找不到, 就會(huì)來到消息轉(zhuǎn)發(fā)

  • 動(dòng)態(tài)方法解析 : 先問接收者所屬的類, 你看能不能動(dòng)態(tài)添加個(gè)方法來處理這個(gè)"未知的選擇子"? 如果能, 則消息轉(zhuǎn)發(fā)結(jié)束.

  • 備胎(后備接收者) : 請(qǐng)接收者看看有沒有其他對(duì)象能處理這條消息? 如果有, 則把消息轉(zhuǎn)給那個(gè)對(duì)象, 消息轉(zhuǎn)發(fā)結(jié)束.

  • 消息簽名 : 這里會(huì)要求你返回一個(gè)消息簽名, 如果返回nil, 則消息轉(zhuǎn)發(fā)結(jié)束.

  • 完整的消息轉(zhuǎn)發(fā) : 備胎都搞不定了, 那就只能把該消息相關(guān)的所有細(xì)節(jié)都封裝到一個(gè)NSInvocation對(duì)象, 再問接收者一次, 快想辦法把這個(gè)搞定了. 到了這個(gè)地步如果還無法處理, 消息轉(zhuǎn)發(fā)機(jī)制也無能為力了.

動(dòng)態(tài)方法解析 :

對(duì)象在收到無法解讀的消息后, 首先調(diào)用其所屬類的這個(gè)類方法 :

+ (BOOL)resolveInstanceMethod:(SEL)selector 
// selector : 那個(gè)未知的選擇子
// 返回YES則結(jié)束消息轉(zhuǎn)發(fā)
// 返回NO則進(jìn)入備胎

假如尚未實(shí)現(xiàn)的方法不是實(shí)例方法而是類方法, 則會(huì)調(diào)用另一個(gè)方法resolveClassMethod:

備胎 :

動(dòng)態(tài)方法解析失敗, 則調(diào)用這個(gè)方法

- (id)forwardingTargetForSelector:(SEL)selector
// selector : 那個(gè)未知的選擇子
// 返回一個(gè)能響應(yīng)該未知選擇子的備胎對(duì)象

通過備胎這個(gè)方法, 可以用"組合"來模擬出"多重繼承".

消息簽名 :

備胎搞不定, 這個(gè)方法就準(zhǔn)備要被包裝成一個(gè)NSInvocation對(duì)象, 在這里要先返回一個(gè)方法簽名

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
// NSMethodSignature : 該selector對(duì)應(yīng)的方法簽名

完整的消息轉(zhuǎn)發(fā) :

給接收者最后一次機(jī)會(huì)把這個(gè)方法處理了, 搞不定就直接程序崩潰!

- (void)forwardInvocation:(NSInvocation *)invocation
// invocation : 封裝了與那條尚未處理的消息相關(guān)的所有細(xì)節(jié)的對(duì)象

在這里能做的比較現(xiàn)實(shí)的事就是 : 在觸發(fā)消息前, 先以某種方式改變消息內(nèi)容, 比如追加另外一個(gè)參數(shù), 或是改變選擇子等等. 實(shí)現(xiàn)此方法時(shí), 如果發(fā)現(xiàn)某調(diào)用操作不應(yīng)該由本類處理, 可以調(diào)用超類的同名方法. 則繼承體系中的每個(gè)類都有機(jī)會(huì)處理該請(qǐng)求, 直到NSObject. 如果NSObject搞不定, 則還會(huì)調(diào)用doesNotRecognizeSelector:來拋出異常, 此時(shí)你就會(huì)在控制臺(tái)看到那熟悉的unrecognized selector sent to instance..

image.png

上面這4個(gè)方法均是模板方法,開發(fā)者可以override,由runtime來調(diào)用。最常見的實(shí)現(xiàn)消息轉(zhuǎn)發(fā),就是重寫方法3和4,忽略這個(gè)消息或者代理給其他對(duì)象.

Method Swizzling

被稱為黑魔法的一個(gè)方法, 可以把兩個(gè)方法的實(shí)現(xiàn)互換.
如上文所述, 類的方法列表會(huì)把選擇子的名稱映射到相關(guān)的方法實(shí)現(xiàn)上, 使得"動(dòng)態(tài)消息派發(fā)系統(tǒng)"能夠據(jù)此找到應(yīng)該調(diào)用的方法. 這些方法均以函數(shù)指針的形式來表示, 這種指針叫做IMP,
<pre> id (*IMP)(id, SEL, ...)</pre>

image.png

OC運(yùn)行時(shí)系統(tǒng)提供了幾個(gè)方法能夠用來操作這張表, 動(dòng)態(tài)增加, 刪除, 改變選擇子對(duì)應(yīng)的方法實(shí)現(xiàn), 甚至交換兩個(gè)選擇子所映射到的指針. 如,

image.png

如何交換兩個(gè)已經(jīng)寫好的方法實(shí)現(xiàn)?

// 取得方法
Method class_getInstanceMethod(Class aClass, SEL aSelector)
// 交換實(shí)現(xiàn)
void method_exchangeImplementations(Method m1, Method m2)

通過Method Swizzling可以為一些完全不知道其具體實(shí)現(xiàn)的黑盒方法增加日志記錄功能, 利于我們調(diào)試程序. 并且我們可以將某些系統(tǒng)類的具體實(shí)現(xiàn)換成我們自己寫的方法, 以達(dá)到某些目的. (例如, 修改主題, 修改字體等等)

KVO原理

KVO的實(shí)現(xiàn)也依賴Runtime. Apple文檔曾簡單提到過KVO的實(shí)現(xiàn)原理 :

Apple的文檔提得不多, 但是大神Mike Ash在很早很早以前就已經(jīng)做過研究, 摘下了KVO神秘的面紗了, 有興趣的可以去查下, 這里不多深究, 只是簡單闡述下原理.

原來當(dāng)你對(duì)一個(gè)對(duì)象進(jìn)行觀察時(shí), 系統(tǒng)會(huì)自動(dòng)新建一個(gè)類繼承自原類, 然后重寫被觀察屬性的setter方法. 然后重寫的setter方法會(huì)負(fù)責(zé)在調(diào)用原setter方法前后通知觀察者. 然后把原對(duì)象的isa指針指向這個(gè)新類, 我們知道, 對(duì)象是通過isa指針去查找自己是屬于哪個(gè)類, 并去所在類的方法列表中查找方法的, 所以這個(gè)時(shí)候這個(gè)對(duì)象就自然地變成了新類的實(shí)例對(duì)象.

不僅如此, Apple還重寫了原類的- class方法, 視圖欺騙我們, 這個(gè)類沒有變, 還是原來的那個(gè)類. 只要我們懂得Runtime的原理, 這一切都只是掩耳盜鈴罷了.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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