runtime原理及應用

runtime簡介

Runtime就是運行時, 核心就是消息機制. 對OC的函數(shù)調(diào)用,是一個動態(tài)調(diào)用過程,只有在運行的時候runtime系統(tǒng)才能知道真正調(diào)用的哪一個函數(shù)(C語言在函數(shù)調(diào)用過程中, 編譯時候就已經(jīng)決定會調(diào)用哪個函數(shù)了).

iOS Runtime中實例對象和類的本質(zhì)

實例對象的本質(zhì)

OC是一門面向?qū)ο蟮木幊陶Z言,在編譯過程中,編譯器會將OC對象轉(zhuǎn)化成結(jié)構(gòu)體.
在objc.h中找到:

typedef struct objc_class *Class;
typedef struct objc_object *id;

struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

可以看到我們常用的Class, id等關(guān)鍵字的定義.
OC中實際的類Class, 會被編譯成struct objc_class. 我們操作的類的對象實例是struct objc_object, 并且該結(jié)構(gòu)體中有一個指針指向struct objc_class.

iOS OC中類的本質(zhì)

OC對象的結(jié)構(gòu)體中有一個Class指針能夠理解, 因為要知道該對象是哪個類的對象.但是我們在objc-runtime-new.h中發(fā)現(xiàn)objc_class繼承自objc_object的.

struct objc_class : objc_object {
    // Class ISA; // 繼承了
    Class superclass;
    ...

在runtime.h中, 我們看到OC類的結(jié)構(gòu)體struct objc_class的具體定義

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

OC中類Class中也有一個指針指向Class, 因此類Class本質(zhì)上也是一個對象, 我們一般稱為類對象, 這個指向的Class是就是元類(metaClass)的對象.

當我們調(diào)用對象方法時候, 會通過對象中的Class指針找到對應的Class,然后調(diào)用實例方法,同理當我們調(diào)用類方法時候, 會通過Class中的Class指針找到對應的meta Class,然后調(diào)用meta Class中的方法.

OC一般會隱藏元類, 并且元類也是某個類的實例, 這個類我們一般稱為根元類(root meta Class). 并且所有的元類的根元類都是一個, 并且根元類的元類是它自己. (實際中根元類是NSObject的元類)

NSString實例的isa指針鏈:


image
iOS 中OC方法調(diào)用的本質(zhì)

OC中的方法調(diào)用稱為消息發(fā)送, 具體格式是[receiver message].例如:

NSMutableString *str = [[NSMutableString alloc] initWithString:@"hello"];
[str appendString:@" world"];

其中str就是receiver, appendString:就是message.

在message.h頭文件中如下方法,這個方法是runtime的核心方法,

void objc_msgSend(void /* id self, SEL op, ... */ )
objc_msgSend(receiever, selector, arg1, arg2, ...)

調(diào)用實例如下:

objc_msgSend(str, @selector(appendString:), @" world");

該消息方法為消息的動態(tài)綁定完成了以下工作:

  • 它會主動查找receiver的selector對應的方法實現(xiàn)IMP
  • 然后將參數(shù)傳遞給receiver object, 然后調(diào)用這IMP
  • 最后返回該方法的返回值

==IMP:一個函數(shù)指針,保存了方法的地址==

為了使得objc_msgSend能完成通過selector查找receiver對應的IMP, 我們知道一個OC類和OC對象會有一個isa指針,指向他們各自的Class, 同時OC類還有一個super指針指向父類.

具體過程就是通過isa指針找到對應的class struct, 然后在dispatch table里面查找selector對應的方法, 如果沒有找到,那么通過super指針查找父類的dispatch table, 一直找下去, 直到NSObject類, 如果還沒有找到,就調(diào)用NSObject的doesNotRecognizeSelector:方法, 然后報unrecognized selector錯誤.

iOS runtime實戰(zhàn)應用

1.iOS runtime 進行添加屬性,并支持KVO監(jiān)聽

iOS 中category和runtime的AssociatedObject是兩大非常重要的工具:

category可以給既有類直接添加方法
associateObject可以給既有類添加屬性(類似成員變量)
結(jié)合這兩個工具, 那么通過category添加property方法.然后結(jié)合associateObject增加關(guān)聯(lián)對象,完成屬性存取.

==需要加入頭文件#import <objc/runtime.h>==

@interface UIViewController (Extension)
@property (nonatomic, copy) NSString * categoryString;
@end

@implementation UIViewController (Extension)
-(NSString *)categoryString{
    return objc_getAssociatedObject(self, @selector(categoryString));
}


-(void)setCategoryString:(NSString *)categoryString{
    objc_setAssociatedObject(self, @selector(categoryString), categoryString, OBJC_ASSOCIATION_COPY);
}

@end

并且這種方法也支持KVO的監(jiān)聽:

-(void)test{
    self.categoryString = @"Runtime生成的屬性";
    [self addObserver:self forKeyPath:@"categoryString" options:NSKeyValueObservingOptionNew context:nil];
}

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"<接收到通知: object:%@ keyPath:%@ change:%@>", object, keyPath,change);
}

2.(1)交換兩個方法的實現(xiàn)缓呛,(2)攔截系統(tǒng)自帶的方法調(diào)用功能

Method class_getClassMethod(Class cls , SEL name) //獲得某個類的類方法

Method class_getInstanceMethod(Class cls , SEL name)//獲得某個類的實例對象方法

void method_exchangeImplementations(Method m1 , Method m2)//交換兩個方法的實現(xiàn)
案例1:方法簡單的交換

創(chuàng)建一個Person類,類中實現(xiàn)以下兩個類方法墩剖,并在.h 文件中聲明

+ (void)game {
    NSLog(@"游戲");
}

+ (void)study {
    NSLog(@"學習");
}
[Person study];
[Person game];

下面通過runtime 實現(xiàn)方法交換晾嘶,類方法用class_getClassMethod 果覆,對象方法用class_getInstanceMethod

// 獲取兩個類的類方法
Method m1 = class_getClassMethod([Person class], @selector(run));
Method m2 = class_getClassMethod([Person class], @selector(study));
// 開始交換方法實現(xiàn)
method_exchangeImplementations(m1, m2);
// 交換后
[Person study];
[Person game];

控制臺打印

2018-04-26 15:20:37.224745+0800 runtime-test[15020:7430831] 學習
2018-04-26 15:20:37.224835+0800 runtime-test[15020:7430831] 游戲
2018-04-26 15:20:37.225668+0800 runtime-test[15020:7430831] 游戲
2018-04-26 15:20:37.225720+0800 runtime-test[15020:7430831] 學習
案例2:攔截系統(tǒng)方法

1摇幻、為UIImage建一個分類(UIImage+Category)
2颓芭、在分類中實現(xiàn)一個自定義方法顷锰,方法中寫要在系統(tǒng)方法中加入的語句,添加自己的邏輯判斷

+ (UIImage *)xxx_imageNamed:(NSString *)name {
    double version = 11.11;
    if (version == 11.11) {
        name = [name stringByAppendingString:@"5.1_time"];
    }
    return [UIImage xxx_imageNamed:name];
}

3亡问、分類中重寫UIImage的load方法官紫,實現(xiàn)方法的交換(只要能讓其執(zhí)行一次方法交換語句,load再合適不過了)

+ (void)load {
    // 獲取兩個類的類方法
    Method m1 = class_getClassMethod([UIImage class], @selector(imageNamed:));
    Method m2 = class_getClassMethod([UIImage class], @selector(xxx_imageNamed:));
    // 開始交換方法實現(xiàn)
    method_exchangeImplementations(m1, m2);
}

==注意:自定義方法中最后一定要再調(diào)用一下系統(tǒng)的方法州藕,讓其有加載圖片的功能束世,但是由于方法交換,系統(tǒng)的方法名已經(jīng)變成了我們自定義的方法名(就是用我們的名字能調(diào)用系統(tǒng)的方法床玻,用系統(tǒng)的名字能調(diào)用我們的方法)毁涉,這就實現(xiàn)了系統(tǒng)方法的攔截!==

3.獲得一個類的成員變量( Ivar )锈死、屬性( Property )贫堰、方法( Method )、協(xié)議( Protocol )

獲得某個類的所有成員變量(outCount 會返回成員變量的總數(shù))

參數(shù):
1待牵、哪個類
2其屏、放一個接收值的地址,用來存放屬性的個數(shù)
3缨该、返回值:存放所有獲取到的屬性漫玄,通過下面兩個方法可以調(diào)出名字和類型

Ivar *class_copyIvarList(Class cls , unsigned int *outCount)
獲得成員變量的名字
const char *ivar_getName(Ivar v)
獲得成員變量的類型
const char *ivar_getTypeEndcoding(Ivar v)
案例1:獲取Person類中所有成員變量的名字和類型
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList([Person class], &outCount);

// 遍歷所有成員變量
for (int i = 0; i < outCount; i++) {
    // 取出i位置對應的成員變量
    Ivar ivar = ivars[i];
    const char *name = ivar_getName(ivar);
    const char *type = ivar_getTypeEncoding(ivar);
    NSLog(@"成員變量名:%s 成員變量類型:%s",name,type);
}
// 注意釋放內(nèi)存!
free(ivars);

==同樣:==

// 測試 打印屬性列表
- (void)testPrintPropertyList {
    unsigned int count;
    
    objc_property_t *propertyList = class_copyPropertyList([self class], &count);
    for (unsigned int i=0; i<count; i++) {
        const char *propertyName = property_getName(propertyList[i]);
        NSLog(@"property----="">%@", [NSString stringWithUTF8String:propertyName]);
    }
    
    free(propertyList);
}
// 測試 打印方法列表
- (void)testPrintMethodList {
    unsigned int count;
    
    Method *methodList = class_copyMethodList([self class], &count);
    for (unsigned int i=0; i<count; i++) {
        Method method = methodList[i];
        NSLog(@"method----="">%@", NSStringFromSelector(method_getName(method)));
    }
    
    free(methodList);
}
// 測試 打印協(xié)議列表
- (void)testPrintProtocolList {
    unsigned int count;
    
    __unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
    for (unsigned int i=0; i<count; i++) {
        Protocol *myProtocal = protocolList[i];
        const char *protocolName = protocol_getName(myProtocal);
        NSLog(@"protocol----="">%@", [NSString stringWithUTF8String:protocolName]);
    }
    
    free(protocolList);
}
案例2:利用Runtime進行 json/dict -> model
-(instancetype)initWithNSDictionary:(NSDictionary *)dict{
    self = [super init];
    if (self) {
        [self processDict:dict];
    }
    return self;
}

-(void)processDict:(NSDictionary *)dict{
    NSMutableArray *keys = [[NSMutableArray alloc] init];
    unsigned int count = 0;
    objc_property_t *props = class_copyPropertyList([self class], &count);
    for (int i = 0; i < count; i++) {
        objc_property_t prop = props[i];
        const char *propCStr = property_getName(prop);
        NSString *propName = [NSString stringWithCString:propCStr encoding:NSUTF8StringEncoding];
        [keys addObject:propName];
    }
    free(props);
    for (NSString *key in keys) {
        if ([dict valueForKey:key]) {
            [self setValue:[dict valueForKey:key] forKey:key];
        }
    }
}
案例3:利用runtime 獲取所有屬性來重寫歸檔解檔方法
// 解檔方法
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    // 獲取所有成員變量
    unsigned int outCount = 0;
    Ivar *ivars = class_copyIvarList([self class], &outCount);
    
    for (int i = 0; i < outCount; i++) {
        Ivar ivar = ivars[i];
        // 將每個成員變量名轉(zhuǎn)換為NSString對象類型
        NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
        
        // 忽略不需要解檔的屬性
        if ([[self ignoredNames] containsObject:key]) {
            continue;
        }
        
        // 根據(jù)變量名解檔取值压彭,無論是什么類型
        id value = [aDecoder decodeObjectForKey:key];
        // 取出的值再設(shè)置給屬性
        [self setValue:value forKey:key];
        // 這兩步就相當于以前的 self.age = [aDecoder decodeObjectForKey:@"_age"];
    }
    free(ivars);
    return self;
}

// 歸檔調(diào)用方法
- (void)encodeWithCoder:(NSCoder *)aCoder {
    // 獲取所有成員變量
    unsigned int outCount = 0;
    Ivar *ivars = class_copyIvarList([self class], &outCount);
    for (int i = 0; i < outCount; i++) {
        Ivar ivar = ivars[i];
        // 將每個成員變量名轉(zhuǎn)換為NSString對象類型
        NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
        
        // 忽略不需要歸檔的屬性
        if ([[self ignoredNames] containsObject:key]) {
            continue;
        }
        
        // 通過成員變量名睦优,取出成員變量的值
        id value = [self valueForKeyPath:key];
        // 再將值歸檔
        [aCoder encodeObject:value forKey:key];
        // 這兩步就相當于 [aCoder encodeObject:@(self.age) forKey:@"_age"];
    }
    free(ivars);
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市壮不,隨后出現(xiàn)的幾起案子汗盘,更是在濱河造成了極大的恐慌,老刑警劉巖询一,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件隐孽,死亡現(xiàn)場離奇詭異癌椿,居然都是意外死亡,警方通過查閱死者的電腦和手機菱阵,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進店門踢俄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人晴及,你說我怎么就攤上這事都办。” “怎么了虑稼?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵琳钉,是天一觀的道長。 經(jīng)常有香客問我蛛倦,道長歌懒,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任溯壶,我火速辦了婚禮及皂,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘且改。我一直安慰自己验烧,他們只是感情好,可當我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布钾虐。 她就那樣靜靜地躺著,像睡著了一般笋庄。 火紅的嫁衣襯著肌膚如雪效扫。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天直砂,我揣著相機與錄音菌仁,去河邊找鬼。 笑死静暂,一個胖子當著我的面吹牛济丘,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播洽蛀,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼摹迷,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了郊供?” 一聲冷哼從身側(cè)響起峡碉,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎驮审,沒想到半個月后鲫寄,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體吉执,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年地来,在試婚紗的時候發(fā)現(xiàn)自己被綠了戳玫。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡未斑,死狀恐怖咕宿,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情颂碧,我是刑警寧澤荠列,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站载城,受9級特大地震影響肌似,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜诉瓦,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一川队、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧睬澡,春花似錦固额、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至昔脯,卻和暖如春啄糙,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背云稚。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工隧饼, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人静陈。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓燕雁,卻偏偏與公主長得像,于是被迫代替她去往敵國和親鲸拥。 傳聞我的和親對象是個殘疾皇子拐格,可洞房花燭夜當晚...
    茶點故事閱讀 44,611評論 2 353

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

  • 周末沒事,我也休息一下刑赶。出去跟朋友們聚會交流禁荒,很長時間沒有出去了。他們都在問我天天忙什么角撞?我說在家寫寫字呛伴,...
    Vultr閱讀 546評論 0 0
  • 每一個出口热康,都成了束縛沛申。 煙的舞,看似散漫姐军,誰又知道他不是在掙扎點什么铁材。 寫幾個字,覺得短了奕锌,就又要寫到長點著觉,覺得...
    寫一個世界的閱讀 220評論 0 0