iOS runtime詳解

簡(jiǎn)介

objective-c(簡(jiǎn)寫objc)屬于動(dòng)態(tài)語言,不像C語言一樣靜態(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源碼是開源的溉瓶,且?guī)缀跞渴褂?code>C語言實(shí)現(xiàn)(有些使用了匯編實(shí)現(xiàn)),可以從蘋果開源官方網(wǎng)站獲取此代碼。

與runtime交互

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

Objective-C源代碼

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

NSObject類

Cocoa中大多數(shù)類都繼承自NSObject類(NSProxy類是個(gè)例外岩馍,它是一個(gè)抽象超類,來充當(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)共享庫趣避。其構(gòu)成了NSObject類的基礎(chǔ),大部分還是使用更上層的接口編程新翎,一般會(huì)用在hook接口或者與其他語言橋接等場(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)我們想通過[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 開發(fā),有了垃圾回收就不理會(huì) retain, release 這些函數(shù)了旷太。

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

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

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

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

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

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

獲取方法地址

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

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

- (IMP)methodForSelector:(SEL)aSelector;

該方法不屬于objc的本身的特性赞季,而為cocoa runtime的特性愧捕,查看源碼實(shí)際為NSObject的類方法,通過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:)];
//無返回值
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ò);若是通過[objc performSelector:@selectro(message)]形式廉涕,需要在運(yùn)行期才能確定是否能響應(yīng)該消息命迈,若無法響應(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ì)通過respondsToSelector:方法來判定是否能響應(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ì)象)征椒,若在此方法通過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ì)于其他類型覆获,具體見官網(wǎng),不過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)方法無法解析,runtime會(huì)繼續(xù)調(diào)用如下方法:

- (id)forwardingTargetForSelector:(SEL)aSelector

方法解析:

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

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

該方法無法對(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ī)制:通過調(diào)用如下方法來轉(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ù)來源于methodSignatureForSelector:方法返回的方法簽名,該方法必須被重寫专肪,否則會(huì)拋出異常;

forwardInvocation:方法就像一個(gè)不能識(shí)別的消息的分發(fā)中心堪侯,將這些消息轉(zhuǎn)發(fā)給不同接收對(duì)象嚎尤。或者它也可以象一個(gè)運(yùn)輸站將所有的消息都發(fā)送給同一個(gè)接收對(duì)象伍宦。它可以將一個(gè)消息翻譯成另外一個(gè)消息芽死,或者簡(jiǎn)單的”吃掉“某些消息,因此沒有響應(yīng)也沒有錯(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只支持單繼承揖曾,但是通過消息轉(zhuǎn)發(fā)機(jī)制可以實(shí)現(xiàn)“多繼承”的效果;如上圖所示亥啦,WarriorDiplomat沒有繼承關(guān)系炭剪,但是Warriornegotiate消息轉(zhuǎn)發(fā)給了Diplomat后,就好似DiplomatWarrior的超類一樣翔脱;

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

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

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

- (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ù)是蘋果的“黑魔法”晾剖,可以不用繼承或者重寫方法锉矢,就可以修改類的方法實(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)致又被交換回來渺杉,進(jìn)而失效;

  • 是否需要dispatch_once

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

  • 為啥調(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)景的影響吗蚌,可見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)景,可見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)一步分離它褪,以提高程序代碼的模塊化程度。通過在現(xiàn)有代碼基礎(chǔ)上增加額外的通知(Advice)機(jī)制翘悉,能夠?qū)Ρ宦暶鳛椤?strong>切點(diǎn)(Pointcut)”的代碼塊進(jìn)行統(tǒng)一管理與裝飾茫打,如“對(duì)所有方法名以‘set*’開頭的方法添加后臺(tái)日志”。該思想使得開發(fā)人員能夠?qū)⑴c代碼核心業(yè)務(wù)邏輯關(guān)系不那么密切的功能(如日志功能)添加至程序中妖混,同時(shí)又不降低業(yè)務(wù)代碼的可讀性老赤。面向切面的程序設(shè)計(jì)思想也是面向切面軟件開發(fā)的基礎(chǔ)。

百度百科:

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

主要目的:將日志記錄渡八,性能統(tǒng)計(jì),安全控制传货,事務(wù)處理屎鳍,異常處理等代碼從業(yè)務(wù)邏輯代碼中劃分出來,通過對(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),將其抽離出來忧饭;主業(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ùn)行時(shí),動(dòng)態(tài)地將代碼切入到類的指定方法赖瞒、指定位置上的編程思想就是AOP(面向切面編程)女揭。

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

Aspects

Aspects 就是一個(gè)不錯(cuò)的 AOP 庫瓢姻,封裝了 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ù),其通過修改object對(duì)象的isa指針指向生成的中間代理類NSKVONotifying_xxx(官方的類名稱)罐监,NSKVONotifying_xxxsuper_class指針指向原有的觀察類對(duì)象object class吴藻;

image.png

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

重寫setter

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

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

重寫setter

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

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

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

- (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;方法生效绢彤,即觀察者能通過上述方法受到屬性被改變的通知,則需要滿足如下:

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

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

重寫dealloc

用來銷毀新生成的NSKVONotifying_類女轿;

重寫_isKVOA方法

這個(gè)私有方法估計(jì)可能是用來標(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ì)象所屬的類,不能依靠它來確定類型链方,而是應(yīng)該用 class 方法來確定實(shí)例對(duì)象的類持痰。因?yàn)镵VO的實(shí)現(xiàn)機(jī)理就是將被觀察對(duì)象的 isa 指針指向一個(gè)中間類而不是真實(shí)的類,這是一種叫做 isa-swizzling 的技術(shù)祟蚀,詳見官方文檔

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)確的說,只是一個(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ì)于字符串的比較僅僅需要比較他們的地址就可以了,可以說速度上無語倫比R刚聪铺!但是,有一個(gè)問題萄窜,就是數(shù)量增多會(huì)增大hash沖突而導(dǎo)致的性能下降(或是沒有沖突铃剔,因?yàn)橐部赡苡玫氖?code>perfect hash)撒桨。但是不管使用什么樣的方法加速,如果能夠?qū)⒖偭繙p少(多個(gè)方法可能對(duì)應(yīng)同一個(gè)SEL)键兜,那將是最犀利的方法凤类。那么,我們就不難理解普气,為什么SEL僅僅是函數(shù)名了谜疤。

可在運(yùn)行時(shí)添加新的selector,也可以通過如下三種方法獲取方法的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為方法選擇器于未;接下來就是參數(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ì)象所屬的類型叫做元類蚯瞧,用來標(biāo)識(shí)類對(duì)象具備的元數(shù)據(jù)。類方法(可以理解為類對(duì)象的實(shí)例方法品擎,區(qū)別于類本身的實(shí)例方法)就定義在類對(duì)象中埋合,每個(gè)類僅有一個(gè)類對(duì)象,每個(gè)類對(duì)象僅有一個(gè)與之相關(guān)的元類萄传,所有的元類最終指向根元類(即為NSObject)甚颂,最終根元類的isa指針指向自己,見下圖秀菱,如[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è)過程中會(huì)拷貝實(shí)例所屬類的成員變量并初始化,其中isa指針也會(huì)被初始化叉谜,讓對(duì)象可以訪問類及類的繼承體系旗吁,但并不拷貝類定義的方法。調(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語言中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緩存中查找鳞尔,如果沒有就通過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ù)霞扬。在方法查找過程中糕韧,Objective-C runtime使用這個(gè)字段來確定開始線性查找數(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ù)組可能包含不超過mask+1個(gè)元素杖小。需要注意的是,指針可能是NULL愚墓,表示這個(gè)緩存bucket沒有被占用予权,另外被占用的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扫腺,前面提到過相同名字的方法即使在不同類中定義,它們的方法選擇器也相同村象;
  • 方法類型 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 );
// 通過引用返回方法的返回值類型字符串
void method_getReturnType ( Method m, char *dst, size_t dst_len );
// 返回方法的參數(shù)的個(gè)數(shù)
unsigned int method_getNumberOfArguments ( Method m );
// 通過引用返回方法指定位置參數(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í)例來查找類中的名字籍救,也稱“反射”习绢;

成員變量的方法調(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`來直接訪問成員變量闪萄,而不使用偏移量;
類成員變量支持權(quán)限控制:

  • @protected是受保護(hù)的奇颠,只能在本類及其子類中訪問败去,在{}聲明的變量默認(rèn)是@protect;
  • @private是私有的烈拒,只能在本類訪問圆裕;
  • @public公開的,可以被在任何地方訪問荆几;
    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來釋放
  • 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è)后就不可再修改卵史,即無法再通過調(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并沒有在<runtime.h>頭文件中提供針對(duì)分類的操作函數(shù)忧设。因?yàn)檫@些分類中的信息都包含在objc_class中,我們可以通過針對(duì)objc_class的操作函數(shù)來獲取分類的信息颠通;

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

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

關(guān)聯(lián)對(duì)象類似字典谨垃,通過key關(guān)鍵詞類設(shè)定類中的成員變量并關(guān)聯(lián)相關(guān)的對(duì)象,并通過key來獲取關(guān)聯(lián)的對(duì)象硼控,不過需要手動(dòng)指定內(nè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ì)象使用起來并不復(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 編寫高質(zhì)量iOS與OS X代碼的52個(gè)有效方法》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末缀匕,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子碰逸,更是在濱河造成了極大的恐慌乡小,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件饵史,死亡現(xiàn)場(chǎng)離奇詭異满钟,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)胳喷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門湃番,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人吭露,你說我怎么就攤上這事吠撮。” “怎么了讲竿?”我有些...
    開封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵泥兰,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我题禀,道長(zhǎng)鞋诗,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任迈嘹,我火速辦了婚禮削彬,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘江锨。我一直安慰自己吃警,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開白布啄育。 她就那樣靜靜地躺著酌心,像睡著了一般。 火紅的嫁衣襯著肌膚如雪挑豌。 梳的紋絲不亂的頭發(fā)上安券,一...
    開封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天,我揣著相機(jī)與錄音氓英,去河邊找鬼侯勉。 笑死,一個(gè)胖子當(dāng)著我的面吹牛铝阐,可吹牛的內(nèi)容都是我干的址貌。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼练对!你這毒婦竟也來了遍蟋?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤螟凭,失蹤者是張志新(化名)和其女友劉穎虚青,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體螺男,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡棒厘,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了下隧。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片奢人。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖汪拥,靈堂內(nèi)的尸體忽然破棺而出达传,到底是詐尸還是另有隱情篙耗,我是刑警寧澤迫筑,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站宗弯,受9級(jí)特大地震影響脯燃,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蒙保,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一辕棚、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧邓厕,春花似錦逝嚎、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至昧互,卻和暖如春挽铁,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背敞掘。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來泰國(guó)打工叽掘, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人玖雁。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓更扁,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子浓镜,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容

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