iOS runtime詳解

簡(jiǎn)介

objective-c(簡(jiǎn)寫(xiě)objc)屬于動(dòng)態(tài)語(yǔ)言,不像C語(yǔ)言一樣靜態(tài)編譯期,就確定了調(diào)用方法的指針,而objc所謂的方法調(diào)用只是消息的發(fā)送,如下:

[recevier message];
//轉(zhuǎn)換為
objc_msgSend(recevier, selector);
//如果存在參數(shù)如下:
objc_msgSend(recevier, selector, arg1, arg2, ...);

因此具體的調(diào)用的函數(shù)指針是在運(yùn)行期確定的,并且在此期間還可以動(dòng)態(tài)修改最終調(diào)用的函數(shù)指針位置,如isa-swizzlingmethod-swizzling技術(shù),或者若未存在此方法,可以動(dòng)態(tài)的添加方法;

另外,objc源碼是開(kāi)源的,且?guī)缀跞渴褂?code>C語(yǔ)言實(shí)現(xiàn)(有些使用了匯編實(shí)現(xiàn)),可以從蘋(píng)果開(kāi)源官方網(wǎng)站獲取此代碼。

與runtime交互

objc 從三種不同的層級(jí)上與 Runtime 系統(tǒng)進(jìn)行交互,分別是通過(guò) Objective-C 源代碼,通過(guò) Foundation 框架的NSObject類定義的方法,通過(guò)對(duì) runtime 函數(shù)的直接調(diào)用。

Objective-C源代碼

大部分都是編寫(xiě)objc代碼,其他都交由runtime系統(tǒng)來(lái)后臺(tái)執(zhí)行具體的操作,如[recevier message]轉(zhuǎn)為調(diào)用objc_msgSend方法來(lái)執(zhí)行;

NSObject類

Cocoa中大多數(shù)類都繼承自NSObject類(NSProxy類是個(gè)例外,它是一個(gè)抽象超類,來(lái)充當(dāng)其他對(duì)象或尚不存在的對(duì)象的替代者),自然也繼承了其方法,如description方法可重載實(shí)現(xiàn)定義類描述;還提供了一些運(yùn)行時(shí)獲取類信息并檢查一些特性:如class獲取類對(duì)象; isMemberOfClass檢查類對(duì)象是否在給定類的實(shí)例,isKindOfClass檢查類實(shí)例是否為給定類或者類的繼承類實(shí)例;respondsToSelector:檢查對(duì)象能否響應(yīng)指定的消息;conformsToProtocol:檢查對(duì)象是否實(shí)現(xiàn)了指定協(xié)議類的方法;methodForSelector:則返回指定方法實(shí)現(xiàn)的地址。

Runtime函數(shù)

runtime系統(tǒng)是一個(gè)由一系列數(shù)據(jù)結(jié)構(gòu)和函數(shù)組成,具有公共接口的動(dòng)態(tài)共享庫(kù)。其構(gòu)成了NSObject類的基礎(chǔ),大部分還是使用更上層的接口編程,一般會(huì)用在hook接口或者與其他語(yǔ)言橋接等場(chǎng)景;

消息

objc方法調(diào)用是以消息發(fā)送的形式傳遞的并獲取到相應(yīng)的函數(shù)指針,從而實(shí)現(xiàn)函數(shù)的直接調(diào)用,具體使用的objc方法為:

void objc_msgSend(receiver, selector, arg1, ....);

其中發(fā)送調(diào)用時(shí)隱含了receiver=self, selector=_cmd,這兩個(gè)參數(shù)是由編譯器編譯時(shí)自動(dòng)添加上的;

而方法中的super關(guān)鍵詞接收到消息時(shí),編譯器會(huì)創(chuàng)建一個(gè)objc_super的結(jié)構(gòu)體,如下:

struct objc_super { id receiver; Class class; };

這個(gè)結(jié)構(gòu)體指明了消息應(yīng)該被傳遞給特定超類的定義。但receiver仍然是self本身,這點(diǎn)需要注意,因?yàn)楫?dāng)我們想通過(guò)[super class]獲取超類時(shí),編譯器只是將指向selfid指針和class的SEL傳遞給了objc_msgSendSuper函數(shù),因?yàn)橹挥性?code>NSObject類才能找到class方法,然后class方法調(diào)用object_getClass(),接著調(diào)用objc_msgSend(objc_super->receiver, @selector(class)),傳入的第一個(gè)參數(shù)是指向selfid指針,與調(diào)用[self class]相同,所以我們得到的永遠(yuǎn)都是self的類型。

劃重點(diǎn)以上表明:

在同一對(duì)象中調(diào)用[self class][super class]返回的都是selfClass類isa指針

@implementation Son : Father
- (id)init {
    self = [super init];
    if (self) {
        NSLog(@"%@", NSStringFromClass([self class]));
        NSLog(@"%@", NSStringFromClass([super class]));
    }
    return self;
}
@end

方法的調(diào)用流程

image.png
  • 檢測(cè)這個(gè) selector 是不是要忽略的。比如 Mac OS X 開(kāi)發(fā),有了垃圾回收就不理會(huì) retain, release 這些函數(shù)了。

  • 檢測(cè)這個(gè) target 是不是 nil 對(duì)象。ObjC 的特性是允許對(duì)一個(gè) nil 對(duì)象執(zhí)行任何一個(gè)方法不會(huì) Crash,因?yàn)闀?huì)被忽略掉。

  • 如果上面兩個(gè)都過(guò)了,那就通過(guò)獲取self實(shí)例對(duì)象的isa獲取類結(jié)構(gòu)體(包含父類super_class、cache緩存及方法列表);

  • 優(yōu)先從cache緩存中查找,若查找到就跳轉(zhuǎn)到對(duì)應(yīng)的IMP函數(shù)指針去執(zhí)行;

  • cache緩存中未找到,就去methodLists方法列表中查找;

  • 如果方法列表中未找到,就去super_class父類結(jié)構(gòu)體中查找,一直找到NSObject類為止;

  • 如果還找不到就要開(kāi)始進(jìn)入消息轉(zhuǎn)發(fā)流程了,后面會(huì)提到。

獲取方法地址

直接獲取到方法地址(IMP函數(shù)指針)可有效避免runtime消息發(fā)送流程,對(duì)于大量同函數(shù)調(diào)用的情況,可提升調(diào)用效率,但不太常見(jiàn);

NSObject提供了通過(guò)idselector來(lái)獲取對(duì)應(yīng)IMP函數(shù)指針(包括實(shí)例對(duì)象或者類對(duì)象的函數(shù)指針)的方法:

- (IMP)methodForSelector:(SEL)aSelector;

該方法不屬于objc的本身的特性,而為cocoa runtime的特性,查看源碼實(shí)際為NSObject的類方法,通過(guò)class_getMethodImplementation方法獲取,源碼如下:

+ (IMP)instanceMethodForSelector:(SEL)sel {
    if (!sel) [self doesNotRecognizeSelector:sel];
    return class_getMethodImplementation(self, sel);
}

具體的IMP函數(shù)指針調(diào)用,需要轉(zhuǎn)換為具體的函數(shù)指針類型,包括返回值類型,參數(shù)列表及其類型,如下:

IMP imp = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)];
//無(wú)返回值
void (*setter)(id, SEL, BOOL) = (void *)imp;
//存在返回值,則指定返回值類型即可
id (*setter)(id, SEL, BOOL) = (void *)imp;
//or
int (*setter)(id, SEL, BOOL) = (void *)imp;

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

對(duì)于[objc message]消息發(fā)送調(diào)用形式,若找不到該方法,在編譯期就會(huì)報(bào)錯(cuò);若是通過(guò)[objc performSelector:@selectro(message)]形式,需要在運(yùn)行期才能確定是否能響應(yīng)該消息,若無(wú)法響應(yīng)就會(huì)報(bào)找不到該實(shí)例方法,進(jìn)而引發(fā)崩潰;

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[KVCTest test1]: unrecognized selector sent to instance 0x600000c4e8e0'
*** First throw call stack:
xxxx

因此通常會(huì)通過(guò)respondsToSelector:方法來(lái)判定是否能響應(yīng)此消息,才觸發(fā)消息的發(fā)送;

if ([self respondsToSelector:@selector(method)]) {
    [self performSelector:@selector(method)];
}

消息轉(zhuǎn)發(fā)步驟如下:

  • 動(dòng)態(tài)方法解析
  • 備用接收者
  • 完整轉(zhuǎn)發(fā)


    image.png

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

對(duì)象在緩存及方法列表中未找到相應(yīng)的方法后,runtime首先會(huì)調(diào)用+ (BOOL)resolveInstanceMethod:(SEL)sel(實(shí)例對(duì)象)或者+ (BOOL)resolveClassMethod:(SEL)sel(類對(duì)象),若在此方法通過(guò)class_addMethod方法添加方法并且返回YES(或者在父類往上繼承體系添加也可),則會(huì)調(diào)用添加的方法,具體的方法使用如下:

+ (BOOL)resolveInstanceMethod:(SEL)sel;
+ (BOOL)resolveClassMethod:(SEL)sel;
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);

其中class_addMethod涉及到類型編碼(Type Encodings),其指定的是imp(id, SEL, ...)方法返回值+輸入?yún)?shù)組合類型,為字符數(shù)組(每個(gè)對(duì)應(yīng)一個(gè)字符);types變量第一個(gè)輸入?yún)?shù)一定是id對(duì)象類型,則類型為@;第二個(gè)參數(shù)為SEL方法選擇器類型,則為:;對(duì)于其他類型,具體見(jiàn)官網(wǎng),不過(guò)runtime也提供了相應(yīng)的獲取類型編碼的方法:

 const char * method_getTypeEncoding(Method m);

使用如下:

@property(nonatomic, copy) NSString *propertyName;
@property(nonatomic, copy, class) NSString *className;

@dynamic propertyName;
@dynamic className;

+ (NSString *)name {
    return  @"class test";
}

- (NSString *)description {
    return @"test";
}

//動(dòng)態(tài)方法解析
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(propertyName)) {
//        const char *typeCode = method_getTypeEncoding(class_getInstanceMethod([self class], @selector(description)));
        const char *typeCode = "@@:";
        class_addMethod([self class], sel, class_getMethodImplementation([self class], @selector(description)), typeCode);
        return YES;
    } else if (sel == @selector(test1)) {
        class_addMethod([self class], sel, class_getMethodImplementation([self class], @selector(test)), "i@:");
        return YES;
    }
    
    return [super resolveInstanceMethod:sel];
}

+ (BOOL)resolveClassMethod:(SEL)sel {
    if (sel == @selector(className)) {
        class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(name)), "@@:");
        return YES;
    }
    
    return [class_getSuperclass(self) resolveClassMethod:sel];
}

@dynamic關(guān)鍵字在類的實(shí)現(xiàn)文件中修飾一個(gè)屬性,表明系統(tǒng)不用自動(dòng)生成setter/getter方法,由自己去動(dòng)態(tài)實(shí)現(xiàn);

注意:[self class] object_getClass(self) object_getClass([self class])的區(qū)別,先上源碼:

+ (id)self {
    return (id)self;
}

- (id)self {
    return self;
}

+ (Class)class {
    return self;
}

- (Class)class {
    return object_getClass(self);
}

+ (Class)superclass {
    return self->superclass;
}

- (Class)superclass {
    return [self class]->superclass;
}

self為實(shí)例對(duì)象時(shí),[self class]返回的類對(duì)象,等價(jià)于object_getClass(self),object_getClass([self class])返回的是元類;

self為類對(duì)象時(shí),[self class]返回的是self自身即類對(duì)象,且object_getClass(self)object_getClass([self class])等價(jià),且都返回的是元類對(duì)象,不同于類對(duì)象;

動(dòng)態(tài)方法解析,一般用于@dynamic屬性,且需要指定已經(jīng)實(shí)現(xiàn)了的處理方法,如上descriptionname;

備用接收者

如果動(dòng)態(tài)方法無(wú)法解析,runtime會(huì)繼續(xù)調(diào)用如下方法:

- (id)forwardingTargetForSelector:(SEL)aSelector

方法解析:

如果對(duì)象實(shí)現(xiàn)(或者繼承)此方法,并且返回非nil,則消息會(huì)轉(zhuǎn)發(fā)至新的接收者;返回的對(duì)象不能為self,會(huì)導(dǎo)致無(wú)限循環(huán);

如果在非根類對(duì)象實(shí)現(xiàn)此方法且未返回任何內(nèi)容(即未指定新的接收者),應(yīng)該調(diào)用[super forwardingTargetForSelector]將其轉(zhuǎn)發(fā)給父類,以此往上繼承調(diào)用;

該方法無(wú)法對(duì)消息進(jìn)行處理,如操作消息的參數(shù)和返回值;

該方法適合將消息轉(zhuǎn)發(fā)給能處理該消息的接收者,如實(shí)現(xiàn)多繼承;

使用如下:

+ (id)forwardingTargetForSelector:(SEL)aSelector {
    if(aSelector == @selector(xxx)) {
        return NSClassFromString(@"Class name");
    }
    return [super forwardingTargetForSelector:aSelector];
}

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

當(dāng)動(dòng)態(tài)方法不能解析且未轉(zhuǎn)發(fā)給其他接收者時(shí),runtime就啟動(dòng)完整消息轉(zhuǎn)發(fā)機(jī)制:通過(guò)調(diào)用如下方法來(lái)轉(zhuǎn)發(fā)給其他接收者,并且可以修改該消息;

- (void)forwardInvocation:(NSInvocation *)invocation;

方法解析如下:

NSObject對(duì)象實(shí)現(xiàn)了該方法,但只是調(diào)用doesNotRecognizeSelector:方法,若繼承對(duì)象未實(shí)現(xiàn)該方法(前提是未動(dòng)態(tài)解析方法或轉(zhuǎn)發(fā)備用接收者),就會(huì)拋出異常;

該消息的唯一參數(shù)為NSInvocation類型的對(duì)象,該對(duì)象封裝了原始的消息及消息參數(shù);該參數(shù)來(lái)源于methodSignatureForSelector:方法返回的方法簽名,該方法必須被重寫(xiě),否則會(huì)拋出異常;

forwardInvocation:方法就像一個(gè)不能識(shí)別的消息的分發(fā)中心,將這些消息轉(zhuǎn)發(fā)給不同接收對(duì)象?;蛘咚部梢韵笠粋€(gè)運(yùn)輸站將所有的消息都發(fā)送給同一個(gè)接收對(duì)象。它可以將一個(gè)消息翻譯成另外一個(gè)消息,或者簡(jiǎn)單的”吃掉“某些消息,因此沒(méi)有響應(yīng)也沒(méi)有錯(cuò)誤。forwardInvocation:方法也可以對(duì)不同的消息提供同樣的響應(yīng),這一切都取決于方法的具體實(shí)現(xiàn)。該方法所提供是將不同的對(duì)象鏈接到消息鏈的能力。
使用如下:

- (void)forwardInvocation:(NSInvocation *)invocation
{
    SEL aSelector = [invocation selector];
 
    if ([friend respondsToSelector:aSelector])
        [invocation invokeWithTarget:friend];
    else
        [super forwardInvocation:invocation];
}

轉(zhuǎn)發(fā)與多繼承

image.png

objective-c只支持單繼承,但是通過(guò)消息轉(zhuǎn)發(fā)機(jī)制可以實(shí)現(xiàn)“多繼承”的效果;如上圖所示,WarriorDiplomat沒(méi)有繼承關(guān)系,但是Warriornegotiate消息轉(zhuǎn)發(fā)給了Diplomat后,就好似DiplomatWarrior的超類一樣;

注意: forwardInvocation:方法只有在消息接收對(duì)象中無(wú)法正常響應(yīng)消息時(shí)才會(huì)被調(diào)用。 所以,如果我們希望一個(gè)對(duì)象將negotiate消息轉(zhuǎn)發(fā)給其它對(duì)象,則這個(gè)對(duì)象不能有negotiate方法。否則,forwardInvocation:將不可能會(huì)被調(diào)用。

轉(zhuǎn)發(fā)與繼承

不過(guò)消息轉(zhuǎn)發(fā)雖然類似于繼承,但NSObject的一些方法還是能區(qū)分兩者。如respondsToSelector:isKindOfClass:只能用于繼承體系,而不能用于轉(zhuǎn)發(fā)鏈。便如果我們想讓這種消息轉(zhuǎn)發(fā)看起來(lái)像是繼承,則可以重寫(xiě)這些方法,如以下代碼所示:

- (BOOL)respondsToSelector:(SEL)aSelector
{
    if ( [super respondsToSelector:aSelector])
        return YES;
    else {
        /* Here, test whether the aSelector message can     *
         * be forwarded to another object and whether that  *
         * object can respond to it. Return YES if it can.  */
    }
    return NO;  
}

方法調(diào)配 Method Swizzling

Method Swizzling方法調(diào)配技術(shù)是蘋(píng)果的“黑魔法”,可以不用繼承或者重寫(xiě)方法,就可以修改類的方法實(shí)現(xiàn);

常用的方法如下:

Method class_getInstanceMethod(Class cls, SEL name);
void method_exchangeImplementations(Method m1, Method m2);

具體使用如下:

#import <objc/runtime.h>
@implementation UIViewController (Tracking)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];         
        // When swizzling a class method, use the following:
        // Class class = object_getClass((id)self);
        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(xxx_viewWillAppear:);
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        BOOL didAddMethod = class_addMethod(class,
                originalSelector,
                method_getImplementation(swizzledMethod),
                method_getTypeEncoding(swizzledMethod));
        if (didAddMethod) {
            class_replaceMethod(class,
                swizzledSelector,
                method_getImplementation(originalMethod),
                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}
#pragma mark - Method Swizzling
- (void)xxx_viewWillAppear:(BOOL)animated {
    [self xxx_viewWillAppear:animated];
    NSLog(@"viewWillAppear: %@", self);
}
@end

針對(duì)使用的答疑如下:

  • 為啥使用load方法

    +load方法是類初始加載時(shí)(應(yīng)用啟動(dòng)就會(huì)加載)調(diào)用且只調(diào)用一次,load方法按照父類到子類,類自身到Category的順序被調(diào)用,且若都實(shí)現(xiàn)了該方法,都會(huì)被調(diào)用;不同于+initialize方法,該方法是在第一次調(diào)用類方法或者實(shí)例方法是調(diào)用,有可能被調(diào)用多次(若子類未實(shí)現(xiàn)此方法或者子類調(diào)用[super initialize]都會(huì)導(dǎo)致被調(diào)用多次);

    注意:在方法中不能調(diào)用[super load],因此+load方法在父類、子類是被分別調(diào)用的,且存在順序;如果調(diào)用,若父類已經(jīng)交換IMP,導(dǎo)致又被交換回來(lái),進(jìn)而失效;

  • 是否需要dispatch_once

    +load方法系統(tǒng)會(huì)自動(dòng)調(diào)用且只調(diào)用一次,但不保證被手動(dòng)調(diào)用,為防止被多次調(diào)用并發(fā)問(wèn)題,建議添加dispatch_once來(lái)保證唯一性;

  • 為啥調(diào)用class_addMethod,直接調(diào)用method_exchangeImplementations交換方法不就行了

    class_getInstanceMethod返回的可能是父類的實(shí)現(xiàn),即子類未實(shí)現(xiàn)被交換的方法,導(dǎo)致父類的實(shí)現(xiàn)指向了子類交換的方法,進(jìn)而導(dǎo)致父類調(diào)用被交換的方法(交換后實(shí)際調(diào)用的是子類交換的方法)引發(fā)崩潰;

    對(duì)于該方法調(diào)用不同場(chǎng)景的影響,可見(jiàn)Runtime Method Swizzling 實(shí)戰(zhàn)

  • xxx_viewWillAppear方法中調(diào)用[self xxx_viewWillAppear:animated]是否會(huì)造成死循環(huán)

    不會(huì),[self xxx_viewWillAppear:animated]實(shí)際調(diào)用的被交換的方法viewWillAppear:;

對(duì)于不同類方法交換的場(chǎng)景,可見(jiàn)Method Swizzling的各種姿勢(shì);
Method Swizzling方法調(diào)配使用的場(chǎng)景如下:

AOP 面向切面編程

AOP(Aspect Oriented Program)面向切面編程,如下是百度百科及維基百科的解釋:

面向切面的程序設(shè)計(jì)(Aspect-oriented programming,AOP,又譯作面向方面的程序設(shè)計(jì)、剖面導(dǎo)向程序設(shè)計(jì))是計(jì)算機(jī)科學(xué)中的一種程序設(shè)計(jì)思想,旨在將橫切關(guān)注點(diǎn)與業(yè)務(wù)主體進(jìn)行進(jìn)一步分離,以提高程序代碼的模塊化程度。通過(guò)在現(xiàn)有代碼基礎(chǔ)上增加額外的通知(Advice)機(jī)制,能夠?qū)Ρ宦暶鳛椤?strong>切點(diǎn)(Pointcut)”的代碼塊進(jìn)行統(tǒng)一管理與裝飾,如“對(duì)所有方法名以‘set*’開(kāi)頭的方法添加后臺(tái)日志”。該思想使得開(kāi)發(fā)人員能夠?qū)⑴c代碼核心業(yè)務(wù)邏輯關(guān)系不那么密切的功能(如日志功能)添加至程序中,同時(shí)又不降低業(yè)務(wù)代碼的可讀性。面向切面的程序設(shè)計(jì)思想也是面向切面軟件開(kāi)發(fā)的基礎(chǔ)。

百度百科:

在軟件業(yè),AOP為Aspect Oriented Programming的縮寫(xiě),意為:面向切面編程,通過(guò)預(yù)編譯方式和運(yùn)行期間動(dòng)態(tài)代理實(shí)現(xiàn)程序功能的統(tǒng)一維護(hù)的一種技術(shù)。AOP是OOP的延續(xù),是軟件開(kāi)發(fā)中的一個(gè)熱點(diǎn),也是Spring框架中的一個(gè)重要內(nèi)容,是函數(shù)式編程的一種衍生范型。利用AOP可以對(duì)業(yè)務(wù)邏輯的各個(gè)部分進(jìn)行隔離,從而使得業(yè)務(wù)邏輯各部分之間的耦合度降低,提高程序的可重用性,同時(shí)提高了開(kāi)發(fā)的效率。

主要目的:將日志記錄,性能統(tǒng)計(jì),安全控制,事務(wù)處理,異常處理等代碼從業(yè)務(wù)邏輯代碼中劃分出來(lái),通過(guò)對(duì)這些行為的分離,我們希望可以將它們獨(dú)立到非指導(dǎo)業(yè)務(wù)邏輯的方法中,進(jìn)而改變這些行為的時(shí)候不影響業(yè)務(wù)邏輯的代碼。

理解:

橫切關(guān)注點(diǎn)不同于主業(yè)務(wù)邏輯代碼,應(yīng)該是主業(yè)務(wù)邏輯代碼中的非業(yè)務(wù)邏輯代碼通用部分,如主業(yè)務(wù)邏輯中添加行為統(tǒng)計(jì),“行為統(tǒng)計(jì)”就是橫切關(guān)注點(diǎn),將其抽離出來(lái);主業(yè)務(wù)邏輯(可能是多個(gè)業(yè)務(wù)模塊)需要橫切關(guān)注點(diǎn)時(shí)可統(tǒng)一插入到主業(yè)務(wù)邏輯中,而不影響主業(yè)務(wù)邏輯。

典型案例就是日志記錄,因?yàn)槿罩竟δ芡鶛M跨系統(tǒng)中的每個(gè)業(yè)務(wù)模塊,即“橫切”所有有日志需求的類及方法體。

針對(duì)ios的面向切面編程就是Method Swizzling黑魔法,可以在不改變?cè)写a(或者函數(shù))邏輯上,添加非業(yè)務(wù)邏輯,如添加行為日志記錄及上報(bào);

在 Objective-C 的實(shí)現(xiàn)結(jié)構(gòu)中 Runtime 的動(dòng)態(tài)派發(fā)機(jī)制保證了這么語(yǔ)言的靈活性,而在運(yùn)行時(shí),動(dòng)態(tài)地將代碼切入到類的指定方法、指定位置上的編程思想就是AOP(面向切面編程)。

Method Swizzling如上所述,不做闡述,不過(guò)有人通過(guò)該技術(shù)實(shí)現(xiàn)了優(yōu)秀的AOP庫(kù),如Aspects;

Aspects

Aspects 就是一個(gè)不錯(cuò)的 AOP 庫(kù),封裝了 Runtime , Method Swizzling 這些黑色技巧,只提供兩個(gè)簡(jiǎn)單的API:

+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
                          withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;

使用舉例如下:

  1. viewWillAppear`中添加日志
[UIViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, BOOL animated) {
    NSLog(@"View Controller %@ will appear animated: %tu", aspectInfo.instance, animated);
} error:NULL];
  1. 調(diào)試查看點(diǎn)擊狀態(tài)

    [_singleTapGesture aspect_hookSelector:@selector(setState:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) {
        NSLog(@"%@: %@", aspectInfo.instance, aspectInfo.arguments);
    } error:NULL];
    
  2. 為系統(tǒng)類添加處理程序,如UIVieweController,官方demo:

    @implementation UIViewController (DismissActionHook)
    
    // Will add a dismiss action once the controller gets dismissed.
    - (void)pspdf_addWillDismissAction:(void (^)(void))action {
        PSPDFAssert(action != NULL);
    
        [self aspect_hookSelector:@selector(viewWillDisappear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) {
            if ([aspectInfo.instance isBeingDismissed]) {
                action();
            }
        } error:NULL];
    }
    
    @end
    

isa swizzling

isa swizzlingKVO(鍵值觀察)機(jī)制的實(shí)現(xiàn)技術(shù),其通過(guò)修改object對(duì)象的isa指針指向生成的中間代理類NSKVONotifying_xxx(官方的類名稱)NSKVONotifying_xxxsuper_class指針指向原有的觀察類對(duì)象object class;

image.png

NSKVONotifying_xxx生成的中間類,重寫(xiě)被觀察的對(duì)象四個(gè)方法:class,setter,dealloc_isKVOA;

重寫(xiě)setter

重寫(xiě)class方法目的是讓被觀察者對(duì)象調(diào)用[object class]時(shí)返回的原有的類實(shí)例;

官方文檔上對(duì)于KVO的實(shí)現(xiàn)的最后,給出了需要我們注意的一點(diǎn)是,永遠(yuǎn)不要用用isa來(lái)判斷一個(gè)類的繼承關(guān)系,而是應(yīng)該用class方法來(lái)判斷類的實(shí)例。

重寫(xiě)setter

重寫(xiě)setter方法目的是能監(jiān)聽(tīng)到被觀察者調(diào)用屬性設(shè)置方法,如setXxxx(Xxxx為屬性名稱)或者調(diào)用setValude:forKey:時(shí),能添加通知消息方法:

- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key

didChangValueForKey:中調(diào)用觀察者必須重寫(xiě)的方法:

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context

因此,若- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;方法生效,即觀察者能通過(guò)上述方法受到屬性被改變的通知,則需要滿足如下:

  • 自動(dòng)通知,NSObject實(shí)現(xiàn)了自動(dòng)通知的方法;

    • 存在setter訪問(wèn)器方法,并且通過(guò)setter方法或者self.xxx間接調(diào)用setter方法,則中間類會(huì)添加will/didChangeValueForKey:觸發(fā)事件通知;
    • 不存在setter訪問(wèn)器方法,需要通過(guò)setValude:forKey:方法來(lái)修改屬性,中間類會(huì)添加will/didChangeValueForKey:觸發(fā)事件通知;
    • 對(duì)于集合類,如NSMutalArray,需要通過(guò)mutableArrayValueForKey來(lái)獲取中間代理類,觸發(fā)通知,否則直接通過(guò)addObject:無(wú)法收到通知;
    • 對(duì)于存在依賴關(guān)系的屬性,具體可查看官方文檔;
  • 手動(dòng)通知:手動(dòng)通知提供了更自由的方式去決定什么時(shí)間,什么方式去通知觀察者。這可以幫助你最少限度觸發(fā)不必要的通知,或者一組改變值發(fā)出一個(gè)通知,想要使用手動(dòng)通知必須實(shí)現(xiàn)automaticallyNotifies-ObserversForKey: 方法;并且手動(dòng)調(diào)用will/didChangeValueForKey:來(lái)觸發(fā)通知;

重寫(xiě)dealloc

用來(lái)銷毀新生成的NSKVONotifying_類;

重寫(xiě)_isKVOA方法

這個(gè)私有方法估計(jì)可能是用來(lái)標(biāo)示該類是一個(gè) KVO 機(jī)制聲稱的類。

YYModel

具體的實(shí)現(xiàn)機(jī)制可參考YYModel對(duì)于setter封裝:

/// 獲取監(jiān)控的屬性
objc_property_t getKVOProperty(Class cls, NSString *keyPath) {
    if (!keyPath || !cls) {
        return NULL;
    }
    
    objc_property_t res = NULL;
    unsigned int count = 0;
    const char *property_name = keyPath.UTF8String;
    objc_property_t *properties = class_copyPropertyList(cls, &count);
    
    for (unsigned int idx = 0; idx < count; idx++) {
        objc_property_t property = properties[idx];
        if (strcmp(property_name, property_getName(property)) == 0) {
            res = property;
            break;
        }
    }
    free(properties);
    return res;
}

/// 檢測(cè)屬性是否存在setter方法
BOOL ifPropertyHasSetter(objc_property_t property) {
    BOOL res = NO;
    unsigned int attrCount;
    objc_property_attribute_t *attrs = property_copyAttributeList(property, &attrCount);
    
    for (unsigned int idx = 0; idx < attrCount; idx++) {
        if (attrs[idx].name[0] == 'S') {
            res = YES;
        }
    }
    free(attrs);
    return res;
}

/// 獲取屬性的數(shù)據(jù)類型
YYEncodingType getPropertyType(objc_property_t) {
    unsigned int attrCount;
    YYEncodingType type = YYEncodingTypeUnknown;
    objc_property_attribute_t *attrs = property_copyAttributeList(property, &attrCount);
    
    for (unsigned int idx = 0; idx < attrCount; idx++) {
        if (attrs[idx].name[0] == 'T') {
            type = YYEncodingGetType(attrs[idx].value);
        }
    }
    free(attrs);
    return type;
}

/// 根據(jù)setter名稱獲取屬性名
NSString *getPropertyNameFromSelector(SEL selector) {
    NSString *selName = [NSStringFromSelector(selector) substringFromIndex: 3];
    NSString *firstAlpha = [[selName substringToIndex: 1] lowercaseString];
    return [selName stringByReplacingCharactersInRange: NSMakeRange(0, 1) withString: firstAlpha];
}

/// 根據(jù)屬性名獲取setter名稱
SEL getSetterFromKeyPath(NSString *keyPath) {
    NSString *firstAlpha = [[keyPath substringToIndex: 1] uppercaseString];
    NSString *selName = [NSString stringWithFormat: @"set%@", [keyPath stringByReplacingCharactersInRange: NSMakeRange(0,  1) withString: firstAlpha]];
    return NSSelectorFromString(selName);
}

/// 設(shè)置bool屬性的kvo setter
static void setBoolVal(id self, SEL _cmd, BOOL val) {
    NSString *name = getPropertyNameFromSelector(_cmd);
    void (*objc_msgSendKVO)(void *, SEL, NSString *) = (void *)objc_msgSend;
    void (*objc_msgSendSuperKVO)(void *, SEL, BOOL) = (void *)objc_msgSendSuper;
    
    objc_msgSendKVO(self, @selector(willChangeValueForKey:), val);
    objc_msgSendSuperKVO(self, _cmd, val);
    objc_msgSendKVO(self, @selector(didChangeValueForKey:), val);
}

/// KVO實(shí)現(xiàn)
static void addObserver(id observedObj, id observer, NSString *keyPath) {
    objc_property_t observedProperty = getKVOProperty([observedObj class], keyPath);
    if (!ifPropertyHasSetter(observedProperty)) {
        return;
    }
    
    NSString *kvoClassName = [@"SLObserved_" stringByAppendString: NSStringFromClass([observedObj class])];
    Class kvoClass = NSClassFromString(kvoClassName);
    if (!kvoClass)) {
        kvoClass = objc_allocateClassPair([observedObj class], kvoClassName.UTF8String, NULL);
        
        Class(^classBlock)(id) = ^Class(id self) {
            return class_getSuperclass([self class]);
        };
        class_addMethod(kvoClass, @selector(class), imp_implementationWithBlock(classBlock), method_getTypeEncoding(class_getMethodImplementation([observedObj class], @selector(class))));
        objc_registerClassPair(kvoClass);
    }
    
    YYEncodingType type = getPropertyType(observedProperty);
    SEL setter = getSetterFromKeyPath(observedProperty);
    switch (type) {
        case YYEncodingTypeBool: {
            class_addMethod(kvoClass, setter, (IMP)setBoolVal, method_getTypeEncoding(class_getMethodImplementation([observedObj class], setter)));
        }   break;
        ......
    }
}

其中實(shí)現(xiàn)點(diǎn)包含了TypeCode鍵值編碼runtime中創(chuàng)建類、消息發(fā)送、獲取屬性列表、添加類對(duì)象方法等知識(shí)點(diǎn);
YYModel 源碼

神經(jīng)病院 Objective-C Runtime 出院第三天——如何正確使用 Runtime

Key-Value Observing Programming Guide

iOS KVO(鍵值觀察) 總覽

概念及數(shù)據(jù)結(jié)構(gòu)

id

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

//objc-private.h
struct objc_object {
private:
    isa_t isa;

public:

    // ISA() assumes this is NOT a tagged pointer object
    Class ISA();

    // getIsa() allows this to be a tagged pointer object
    Class getIsa();
    ... 此處省略其他方法聲明
}
//objc.h
/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

指向類實(shí)例的指針;

objc_object 結(jié)構(gòu)體包含一個(gè) isa 指針,類型為 isa_t 聯(lián)合體。根據(jù) isa 就可以順藤摸瓜找到對(duì)象所屬的類。isa 這里還涉及到 tagged pointer 等概念。因?yàn)?isa_t 使用 union 實(shí)現(xiàn),所以可能表示多種形態(tài),既可以當(dāng)成是指針,也可以存儲(chǔ)標(biāo)志位。有關(guān) isa_t 聯(lián)合體的更多內(nèi)容可以查看 Objective-C 引用計(jì)數(shù)原理。

PS: isa 指針不總是指向?qū)嵗龑?duì)象所屬的類,不能依靠它來(lái)確定類型,而是應(yīng)該用 class 方法來(lái)確定實(shí)例對(duì)象的類。因?yàn)镵VO的實(shí)現(xiàn)機(jī)理就是將被觀察對(duì)象的 isa 指針指向一個(gè)中間類而不是真實(shí)的類,這是一種叫做 isa-swizzling 的技術(shù),詳見(jiàn)官方文檔

SEL

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

稱為方法選擇器,是一個(gè)方法selector的指針,用于類查找方法列表中的對(duì)應(yīng)的IMP;

objc源碼中未找到objc_selector結(jié)構(gòu)體的定義,objc在編譯時(shí)會(huì)根據(jù)方法的名字及參數(shù)列表,生成一個(gè)整型標(biāo)識(shí),這個(gè)標(biāo)識(shí)就是SEL;本質(zhì)上SEL是一個(gè)指向方法的指針(準(zhǔn)確的說(shuō),只是一個(gè)根據(jù)方法名hash化了的KEY值,能唯一代表一個(gè)方法,不同類查找的方法列表不同,因此不會(huì)導(dǎo)致類方法指向同一個(gè)IMP),為了加快方法的查詢速度;

不同類中相同方法名及參數(shù)名的SEL相同,即使參數(shù)的類型不同,因此在一個(gè)類中方法名及參數(shù)名相同但參數(shù)類型不同的方法編譯錯(cuò)誤,objc使用了大量帶參數(shù)類型的方法名稱,導(dǎo)致objc方法名都很長(zhǎng);

工程中的所有的SEL組成一個(gè)Set集合,Set的特點(diǎn)就是唯一,因此SEL是唯一的。因此,如果我們想到這個(gè)方法集合中查找某個(gè)方法時(shí),只需要去找到這個(gè)方法對(duì)應(yīng)的SEL就行了,SEL實(shí)際上就是根據(jù)方法名hash化了的一個(gè)字符串,而對(duì)于字符串的比較僅僅需要比較他們的地址就可以了,可以說(shuō)速度上無(wú)語(yǔ)倫比??!但是,有一個(gè)問(wèn)題,就是數(shù)量增多會(huì)增大hash沖突而導(dǎo)致的性能下降(或是沒(méi)有沖突,因?yàn)橐部赡苡玫氖?code>perfect hash)。但是不管使用什么樣的方法加速,如果能夠?qū)⒖偭繙p少(多個(gè)方法可能對(duì)應(yīng)同一個(gè)SEL),那將是最犀利的方法。那么,我們就不難理解,為什么SEL僅僅是函數(shù)名了。

可在運(yùn)行時(shí)添加新的selector,也可以通過(guò)如下三種方法獲取方法的SEL

  • sel_registerName函數(shù)
  • Objective-C編譯器提供的@selector()
  • NSSelectorFromString()方法

IMP

IMPobjc.h中的定義是:

typedef void (*IMP)(void /* id, SEL, ... */ );

它就是一個(gè)函數(shù)指針,這是由編譯器生成的。第一個(gè)參數(shù)為self的指針(如果是實(shí)例方法,則是類實(shí)例的內(nèi)存地址;如果是類方法,則指向元類的指針);第二個(gè)SEL為方法選擇器;接下來(lái)就是參數(shù)列表;

Class

Class 其實(shí)是一個(gè)指向 objc_class 結(jié)構(gòu)體的指針:

typedef struct objc_class *Class;
//objc/objc-private.h定義
struct objc_class : objc_object {
    // Class ISA;                           //add, 指向類對(duì)象的指針
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    class_rw_t *data() { 
        return bits.data();
    }
    ... 省略其他方法
}
// objc/runtime.h定義
struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;                                                   //注意此處,類自身存在指向類對(duì)象的指針,稱為元類
#if !__OBJC2__
    Class super_class                       OBJC2_UNAVAILABLE;  // 父類
    const char *name                        OBJC2_UNAVAILABLE;  // 類名
    long version                            OBJC2_UNAVAILABLE;  // 類的版本信息,默認(rèn)為0
    long info                               OBJC2_UNAVAILABLE;  // 類信息,供運(yùn)行期使用的一些位標(biāo)識(shí)
    long instance_size                      OBJC2_UNAVAILABLE;  // 該類的實(shí)例變量大小
    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;

objc_class繼承自objc_object,因此類本身也是類對(duì)象,為了處理類本身及類對(duì)象的關(guān)系,runtime創(chuàng)建了元類(Meta-Class),類對(duì)象所屬的類型叫做元類,用來(lái)標(biāo)識(shí)類對(duì)象具備的元數(shù)據(jù)。類方法(可以理解為類對(duì)象的實(shí)例方法,區(qū)別于類本身的實(shí)例方法)就定義在類對(duì)象中,每個(gè)類僅有一個(gè)類對(duì)象,每個(gè)類對(duì)象僅有一個(gè)與之相關(guān)的元類,所有的元類最終指向根元類(即為NSObject),最終根元類的isa指針指向自己,見(jiàn)下圖,如[NSObject alloc]消息發(fā)送是,會(huì)在類對(duì)象中查詢能夠響應(yīng)消息的方法。
同時(shí),可以看到運(yùn)行時(shí)一個(gè)類還關(guān)聯(lián)了它的超類指針,類名,成員變量,方法,緩存,還有附屬的協(xié)議;

image.png

image.png

圖中類實(shí)例的isa指向類本身,類本身的isa指向元類,元類的isa指向根元類Root class,根元類的isa指向自身;其他的super_class指向父類的類本身及元類,最終父類指向nil;
image.png

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

typedef struct objc_object *id;

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

實(shí)例對(duì)象是我們對(duì)類對(duì)象alloc或者new操作時(shí)所創(chuàng)建的,在這個(gè)過(guò)程中會(huì)拷貝實(shí)例所屬類的成員變量并初始化,其中isa指針也會(huì)被初始化,讓對(duì)象可以訪問(wèn)類及類的繼承體系,但并不拷貝類定義的方法。調(diào)用實(shí)例方法時(shí),系統(tǒng)會(huì)根據(jù)實(shí)例的isa指針去類的方法列表及父類的方法列表中尋找與消息對(duì)應(yīng)的selector指向的方法。

實(shí)例對(duì)象即id類型,其是一個(gè)objc_object結(jié)構(gòu)類型的指針。該類型的對(duì)象可以轉(zhuǎn)換為任何一種對(duì)象,類似于C語(yǔ)言中void *指針類型的作用;

runtime - iOS類對(duì)象、實(shí)例對(duì)象、元類對(duì)象

類對(duì)象

類對(duì)象即類本身,即實(shí)例對(duì)象的isa指向的地址。類對(duì)象存儲(chǔ)著類的成員變量、緩存及實(shí)例方法列表,但不存儲(chǔ)類方法;

元類對(duì)象

元類對(duì)象即類對(duì)象的isa指向的地址,存儲(chǔ)著類方法,其isa指向根元類(Metal-Class,NSObject),根元類的isa指向自身;

cache

接收者收到消息,優(yōu)先去cache緩存中查找,如果沒(méi)有就通過(guò)isa指針去類本身的實(shí)例方法列表中methodLists查找,提升查找速度;

該字段指向struct objc_cache的結(jié)構(gòu)體,具體如下:

struct objc_cache {
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
    unsigned int occupied                                    OBJC2_UNAVAILABLE;
    Method _Nullable buckets[1]                              OBJC2_UNAVAILABLE;
};
    • mask:一個(gè)整數(shù),指定分配的緩存bucket的總數(shù)。在方法查找過(guò)程中,Objective-C runtime使用這個(gè)字段來(lái)確定開(kāi)始線性查找數(shù)組的索引位置。指向方法selector的指針與該字段做一個(gè)AND位操作(index = (mask & selector))。這可以作為一個(gè)簡(jiǎn)單的hash散列算法。
  • occupied:一個(gè)整數(shù),指定實(shí)際占用的緩存bucket的總數(shù)。

  • buckets:指向Method數(shù)據(jù)結(jié)構(gòu)指針的數(shù)組。這個(gè)數(shù)組可能包含不超過(guò)mask+1個(gè)元素。需要注意的是,指針可能是NULL,表示這個(gè)緩存bucket沒(méi)有被占用,另外被占用的bucket可能是不連續(xù)的。這個(gè)數(shù)組可能會(huì)隨著時(shí)間而增長(zhǎng)。

Method

/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;

struct objc_method {
    SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
    char * _Nullable method_types                            OBJC2_UNAVAILABLE;
    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

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;
}  

對(duì)于struct objc_method結(jié)構(gòu)體描述如下:

  • 方法名類型為 SEL,前面提到過(guò)相同名字的方法即使在不同類中定義,它們的方法選擇器也相同;
  • 方法類型 types 是個(gè)char指針,其實(shí)存儲(chǔ)著方法的參數(shù)類型和返回值類型;
  • imp 指向了方法的實(shí)現(xiàn),本質(zhì)上是一個(gè)函數(shù)指針;

相關(guān)的runtime調(diào)動(dòng)方法如下:

// 調(diào)用指定方法的實(shí)現(xiàn)
id method_invoke ( id receiver, Method m, ... );
// 調(diào)用返回一個(gè)數(shù)據(jù)結(jié)構(gòu)的方法的實(shí)現(xiàn)
void method_invoke_stret ( id receiver, Method m, ... );
// 獲取方法名
SEL method_getName ( Method m );
// 返回方法的實(shí)現(xiàn)
IMP method_getImplementation ( Method m );
// 獲取描述方法參數(shù)和返回值類型的字符串
const char * method_getTypeEncoding ( Method m );
// 獲取方法的返回值類型的字符串
char * method_copyReturnType ( Method m );
// 獲取方法的指定位置參數(shù)的類型字符串
char * method_copyArgumentType ( Method m, unsigned int index );
// 通過(guò)引用返回方法的返回值類型字符串
void method_getReturnType ( Method m, char *dst, size_t dst_len );
// 返回方法的參數(shù)的個(gè)數(shù)
unsigned int method_getNumberOfArguments ( Method m );
// 通過(guò)引用返回方法指定位置參數(shù)的類型字符串
void method_getArgumentType ( Method m, unsigned int index, char *dst, size_t dst_len );
// 返回指定方法的方法描述結(jié)構(gòu)體
struct objc_method_description * method_getDescription ( Method m );
// 設(shè)置方法的實(shí)現(xiàn)
IMP method_setImplementation ( Method m, IMP imp );
// 交換兩個(gè)方法的實(shí)現(xiàn)
void method_exchangeImplementations ( Method m1, Method m2 );

Ivar

typedef struct objc_ivar *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;

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;
}  

Ivar是類中實(shí)例變量的類型,可以根據(jù)實(shí)例來(lái)查找類中的名字,也稱“反射”;

成員變量的方法調(diào)用如下:

// 獲取成員變量名
const char * ivar_getName ( Ivar v );
// 獲取成員變量類型編碼
const char * ivar_getTypeEncoding ( Ivar v );
// 獲取成員變量的偏移量
ptrdiff_t ivar_getOffset ( Ivar v );

ivar_getOffset函數(shù),對(duì)于類型id或其它對(duì)象類型的實(shí)例變量,可以調(diào)用object_getIvarobject_setIvar`來(lái)直接訪問(wèn)成員變量,而不使用偏移量;
類成員變量支持權(quán)限控制:

  • @protected是受保護(hù)的,只能在本類及其子類中訪問(wèn),在{}聲明的變量默認(rèn)是@protect;
  • @private是私有的,只能在本類訪問(wèn);
  • @public公開(kāi)的,可以被在任何地方訪問(wèn);
    objc_property_t**

objc_property_t是表示Objective-C聲明的屬性的類型,其實(shí)際是指向objc_property結(jié)構(gòu)體的指針,其定義如下:

typedef struct objc_property *objc_property_t;

屬性相關(guān)的方法如下:

// 獲取屬性名
const char * property_getName ( objc_property_t property );
// 獲取屬性特性描述字符串
const char * property_getAttributes ( objc_property_t property );
// 獲取屬性中指定的特性
char * property_copyAttributeValue ( objc_property_t property, const char *attributeName );
// 獲取屬性的特性列表
objc_property_attribute_t * property_copyAttributeList ( objc_property_t property, unsigned int *outCount );
  • property_copyAttributeValue函數(shù),返回的char *在使用完后需要調(diào)用free()釋放。
  • property_copyAttributeList函數(shù),返回值在使用完后需要調(diào)用free()釋放。
    objc_property_attribute_t**

objc_property_attribute_t定義了屬性的特性(attribute),它是一個(gè)結(jié)構(gòu)體,定義如下:

typedef struct {
    const char *name;           // 特性名
    const char *value;          // 特性值
} objc_property_attribute_t;

protocol協(xié)議

struct objc_protocol_list {
    struct objc_protocol_list * _Nullable next;
    long count;
    __unsafe_unretained Protocol * _Nullable list[1];
};

struct protocol_t : objc_object {
    const char *mangledName;
    struct protocol_list_t *protocols;
    method_list_t *instanceMethods;
    method_list_t *classMethods;
    method_list_t *optionalInstanceMethods;
    method_list_t *optionalClassMethods;
    property_list_t *instanceProperties;
    uint32_t size;   // sizeof(protocol_t)
    uint32_t flags;
    // Fields below this point are not always present on disk.
    const char **_extendedMethodTypes;
    const char *_demangledName;
    property_list_t *_classProperties;
        //省略一些封裝的便捷 get 方法
    ....
};

@interface Protocol : NSObject
@end

Protocol就是繼承自NSObject的對(duì)象,其id結(jié)構(gòu)體類型為struct protocol_t;

runtime提供了一系列關(guān)于協(xié)議的方法,如下:

// 返回指定的協(xié)議
Protocol * objc_getProtocol ( const char *name );
// 獲取運(yùn)行時(shí)所知道的所有協(xié)議的數(shù)組
Protocol ** objc_copyProtocolList ( unsigned int *outCount );
// 創(chuàng)建新的協(xié)議實(shí)例
Protocol * objc_allocateProtocol ( const char *name );
// 在運(yùn)行時(shí)中注冊(cè)新創(chuàng)建的協(xié)議
void objc_registerProtocol ( Protocol *proto );
// 為協(xié)議添加方法
void protocol_addMethodDescription ( Protocol *proto, SEL name, const char *types, BOOL isRequiredMethod, BOOL isInstanceMethod );
// 添加一個(gè)已注冊(cè)的協(xié)議到協(xié)議中
void protocol_addProtocol ( Protocol *proto, Protocol *addition );
// 為協(xié)議添加屬性
void protocol_addProperty ( Protocol *proto, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount, BOOL isRequiredProperty, BOOL isInstanceProperty );
// 返回協(xié)議名
const char * protocol_getName ( Protocol *p );
// 測(cè)試兩個(gè)協(xié)議是否相等
BOOL protocol_isEqual ( Protocol *proto, Protocol *other );
// 獲取協(xié)議中指定條件的方法的方法描述數(shù)組
struct objc_method_description * protocol_copyMethodDescriptionList ( Protocol *p, BOOL isRequiredMethod, BOOL isInstanceMethod, unsigned int *outCount );
// 獲取協(xié)議中指定方法的方法描述
struct objc_method_description protocol_getMethodDescription ( Protocol *p, SEL aSel, BOOL isRequiredMethod, BOOL isInstanceMethod );
// 獲取協(xié)議中的屬性列表
objc_property_t * protocol_copyPropertyList ( Protocol *proto, unsigned int *outCount );
// 獲取協(xié)議的指定屬性
objc_property_t protocol_getProperty ( Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty );
// 獲取協(xié)議采用的協(xié)議
Protocol ** protocol_copyProtocolList ( Protocol *proto, unsigned int *outCount );
// 查看協(xié)議是否采用了另一個(gè)協(xié)議
BOOL protocol_conformsToProtocol ( Protocol *proto, Protocol *other );
  • objc_getProtocol函數(shù),需要注意的是如果僅僅是聲明了一個(gè)協(xié)議,而未在任何類中實(shí)現(xiàn)這個(gè)協(xié)議,則該函數(shù)返回的是nil。
  • objc_copyProtocolList函數(shù),獲取到的數(shù)組需要使用free來(lái)釋放
  • objc_allocateProtocol函數(shù),如果同名的協(xié)議已經(jīng)存在,則返回nil
  • objc_registerProtocol函數(shù),創(chuàng)建一個(gè)新的協(xié)議后,必須調(diào)用該函數(shù)以在運(yùn)行時(shí)中注冊(cè)新的協(xié)議。協(xié)議注冊(cè)后便可以使用,但不能再做修改,即注冊(cè)完后不能再向協(xié)議添加方法或協(xié)議

需要強(qiáng)調(diào)的是,協(xié)議一旦注冊(cè)后就不可再修改,即無(wú)法再通過(guò)調(diào)用protocol_addMethodDescriptionprotocol_addProtocolprotocol_addProperty往協(xié)議中添加方法等。

category類別

/// An opaque type that represents a category.
typedef struct objc_category *Category;

struct objc_category {
    char * _Nonnull category_name                            OBJC2_UNAVAILABLE;
    char * _Nonnull class_name                               OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable instance_methods     OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable class_methods        OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
}  

這個(gè)結(jié)構(gòu)體主要包含了分類定義的實(shí)例方法與類方法,其中instance_methods列表是objc_class中方法列表的一個(gè)子集,而class_methods列表是元類方法列表的一個(gè)子集。

Runtime并沒(méi)有在<runtime.h>頭文件中提供針對(duì)分類的操作函數(shù)。因?yàn)檫@些分類中的信息都包含在objc_class中,我們可以通過(guò)針對(duì)objc_class的操作函數(shù)來(lái)獲取分類的信息;

可以通過(guò)類別增加類的方法,但不能通過(guò)類別增加實(shí)例變量,不過(guò)objc提供了解決方案--關(guān)聯(lián)對(duì)象(Associated Object);

關(guān)聯(lián)對(duì)象

關(guān)聯(lián)對(duì)象類似字典,通過(guò)key關(guān)鍵詞類設(shè)定類中的成員變量并關(guān)聯(lián)相關(guān)的對(duì)象,并通過(guò)key來(lái)獲取關(guān)聯(lián)的對(duì)象,不過(guò)需要手動(dòng)指定內(nèi)存策略,來(lái)告知runtime如何管理關(guān)聯(lián)的對(duì)象,具體的內(nèi)存策略如下:

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                            *   The association is made atomically. */
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                            *   The association is made atomically. */
};

具體的<runtime.h>定義的方法如下:

//若value為nil,則移除指定的存在的關(guān)聯(lián)對(duì)象
void objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
                         id _Nullable value, objc_AssociationPolicy policy);
id _Nullable objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key);
//移除所有的關(guān)聯(lián)對(duì)象
void objc_removeAssociatedObjects(id _Nonnull object);

typedef void (*objc_hook_setAssociatedObject)(id _Nonnull object, const void * _Nonnull key,
                                              id _Nullable value, objc_AssociationPolicy policy);

void objc_setHook_setAssociatedObject(objc_hook_setAssociatedObject _Nonnull newValue,
                                       objc_hook_setAssociatedObject _Nullable * _Nonnull outOldValue)

關(guān)聯(lián)對(duì)象使用起來(lái)并不復(fù)雜,它讓我們可以動(dòng)態(tài)地增強(qiáng)類現(xiàn)有的功能,如關(guān)聯(lián)UIAlertView點(diǎn)擊按鈕關(guān)聯(lián)響應(yīng)的block,在按鈕點(diǎn)擊代理實(shí)現(xiàn)中直接獲取關(guān)聯(lián)的按鈕block執(zhí)行;

參考資料

Objective-C Runtime Programming Guide

Objective-C Runtime

NSProxy

Objective-C Runtime 運(yùn)行時(shí)之一:類與對(duì)象

Objective-C Runtime 運(yùn)行時(shí)之二:成員變量與屬性

Objective-C Runtime 運(yùn)行時(shí)之三:方法與消息

Objective-C Runtime 運(yùn)行時(shí)之四:Method Swizzling

Objective-C Runtime 運(yùn)行時(shí)之五:協(xié)議與分類

《Effective Objective-C 2.0 編寫(xiě)高質(zhì)量iOS與OS X代碼的52個(gè)有效方法》

最后編輯于
?著作權(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),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • 引導(dǎo) runtime是運(yùn)行時(shí),對(duì)于從事iOS開(kāi)發(fā),想要深入學(xué)習(xí)OC的人,runtime是必須熟悉掌握的東西。 ru...
    叫我小黑閱讀 961評(píng)論 1 4
  • 本文轉(zhuǎn)載自:http://yulingtianxia.com/blog/2014/11/05/objective-...
    ant_flex閱讀 867評(píng)論 0 1
  • 我們常常會(huì)聽(tīng)說(shuō) Objective-C 是一門(mén)動(dòng)態(tài)語(yǔ)言,那么這個(gè)「動(dòng)態(tài)」表現(xiàn)在哪呢?我想最主要的表現(xiàn)就是 Obje...
    Ethan_Struggle閱讀 2,319評(píng)論 0 7
  • 1、runtime(運(yùn)行時(shí)機(jī)制)是什么 runtime是屬于OC的底層,是一套比較底層的純C語(yǔ)言API, 屬于1個(gè)...
    AKyS佐毅閱讀 588評(píng)論 0 16
  • 本文詳細(xì)整理了 Cocoa 的 Runtime 系統(tǒng)的知識(shí),它使得 Objective-C 如虎添翼,具備了靈活的...
    lylaut閱讀 864評(píng)論 0 4

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