iOS --runtime理解與應(yīng)用

1.什么是Runtime?

我所理解的runtime是一個(gè)使用C編寫(xiě)的庫(kù),為C添加了面向?qū)ο蟮奶匦?它是一個(gè)庫(kù)(Runtime Library中文:運(yùn)行時(shí)庫(kù)).在這個(gè)庫(kù)中可以用C函數(shù)來(lái)實(shí)現(xiàn)方法,對(duì)象也可以用C語(yǔ)言的結(jié)構(gòu)體來(lái)表示…所有oc的方法的背后都是通過(guò)runtime來(lái)運(yùn)行的.

2.runtime的使用

(1)利用runtime的消息發(fā)送機(jī)制調(diào)用方法

首先新建一個(gè)類(lèi)RuntimeModel,并實(shí)現(xiàn)對(duì)象方法eat,在RuntimeViewController中調(diào)用eat方法,使用oc來(lái)語(yǔ)言來(lái)實(shí)現(xiàn)很簡(jiǎn)單了

RuntimeModel * model=[[RuntimeModel alloc]init];
[model eat];

接下來(lái)一點(diǎn)點(diǎn)用runtime實(shí)現(xiàn)上面的代碼,導(dǎo)入runtime的頭文件#import <objc/message.h>,由于xcode5.0開(kāi)始蘋(píng)果不建議我們使用底層的代碼,所以target->build setting->搜索msg->將YES改為NO,這樣接下來(lái)我們用runtime的時(shí)候才會(huì)出現(xiàn)提示。
我們使用objc_msgSend(<#id self#>, <#SEL op, ...#>)這個(gè)方法,可以看到需要兩個(gè)參數(shù),第一個(gè)參數(shù)是id類(lèi)型,代表誰(shuí)要發(fā)送消息,第二個(gè)參數(shù)是要把消息發(fā)送給誰(shuí),我們用runtime來(lái)實(shí)現(xiàn)[model eat];這個(gè)方法。

objc_msgSend(model, sel_registerName("eat"));

而初始化對(duì)象同樣是調(diào)用了alloc init這兩個(gè)方法。將導(dǎo)入的頭文件RuntimeModel去掉,用純c語(yǔ)言的代碼實(shí)現(xiàn)上面的功能。

objc_msgSend(objc_msgSend(objc_msgSend(objc_getClass("RuntimeModel"), sel_registerName("alloc")), sel_registerName("init")), sel_registerName("eat"));

運(yùn)行,我們可以看到在eat方法中的nslog被調(diào)用了,雖然我們實(shí)現(xiàn)了功能,但是怎么才能知道我們的oc語(yǔ)言在運(yùn)行時(shí)確實(shí)是被轉(zhuǎn)換成了c語(yǔ)言的代碼呢?
新建工程,創(chuàng)建命令行工具。

4BEFBE2C-70AE-490F-BE08-926725DC0C2B.png

新建一個(gè)person類(lèi),然后進(jìn)入main.m中。
調(diào)用頭文件,在main中實(shí)例化person。person * p=[[person alloc]init];關(guān)閉項(xiàng)目,打開(kāi)終端,進(jìn)入剛才建的文件夾下,ls打開(kāi)可以看到剛才我們新建的類(lèi)和main.m文件,接下來(lái)執(zhí)行命令行clang -rewrite-objc main.m
這時(shí)我們可以看到,在剛才的工程中出現(xiàn)一個(gè)main.cpp的文件,打開(kāi)并且拖到最下面。

783A655E-6F04-41F8-8E16-DAE6C21468E2.png
teatime.gif

(2)交換方法

做為oc的程序員最悲慘的就是,運(yùn)行--崩潰在main里面,我尼瑪?。?! 例如:

NSURL * url=[NSURL URLWithString:@"www.baidu.com.啦啦"];

當(dāng)我們沒(méi)有進(jìn)行編碼的時(shí)候,這個(gè)url在編譯的時(shí)候是有效的,但是一旦運(yùn)行起來(lái),這個(gè)url就會(huì)變?yōu)閚il。因?yàn)闄z測(cè)不到這是一個(gè)無(wú)效的url,會(huì)繼續(xù)發(fā)送網(wǎng)絡(luò)請(qǐng)求。
怎么辦呢?在下面用if做一個(gè)判斷?可是要在每一個(gè)url下面都做判斷。這時(shí)候最先想到的一定是重寫(xiě)。
創(chuàng)建一個(gè)url的分類(lèi),重寫(xiě)URLWithString:<#(nonnull NSString *)#>但是,我們看:

+(instancetype)URLWithString:(NSString *)URLString
{
    //  首先創(chuàng)建一個(gè)URL
    NSURL * url= ????????
    if (url==nil) {
        NSLog(@"有問(wèn)題");
    }
    return url;
}

我們?cè)撛趺磩?chuàng)建呢,死循環(huán)了是不是,所以runtime就起作用了。
在分類(lèi)中自定義一個(gè)方法

+(instancetype)TY_URLWithStr:(NSString *)Str;
{
    NSURL * url =[NSURL URLWithString:Str];
    if (url == nil) {
        NSLog(@"是空?。。?!");
    }
    return url;
}

但是我們并不是要每一次創(chuàng)建url都調(diào)用這個(gè)方法,因?yàn)槌绦蚶镉泻芏鄤?chuàng)建url的地方,我們還繼續(xù)使用系統(tǒng)自帶的方法。在分類(lèi)中實(shí)現(xiàn)load方法,當(dāng)程序加載這個(gè)類(lèi)的時(shí)候最先調(diào)用這個(gè)方法。導(dǎo)入頭文件,開(kāi)始進(jìn)行方法交換。

+(void)load
{
    //  Method : 成員方法
    //class_getClassMethod   拿到類(lèi)方法
    //class_getInstanceMethod   拿到對(duì)象方法
    Method URLWithStr = class_getClassMethod([NSURL class], @selector(URLWithString:));
    Method TYURLWithStr = class_getClassMethod([NSURL class], @selector(TY_URLWithStr:));
    //  開(kāi)始交換方法
    method_exchangeImplementations(URLWithStr, TYURLWithStr);
}

記得將上面我們自定義的方法中創(chuàng)建url的方法改變回去,不然再次死循環(huán),改為

+(instancetype)TY_URLWithStr:(NSString *)Str;
{
    NSURL * url =[NSURL TY_URLWithStr:Str];
    if (url == nil) {
        NSLog(@"是空?。。。?);
    }
    return url;
}

是不是有種一葉落而知天下秋的感覺(jué)。

teatime.gif

(3)遍歷屬性列表簡(jiǎn)化序列化

oc的序列化在這里就不多說(shuō)了,讓我們來(lái)說(shuō)一種常見(jiàn)的情況,當(dāng)需要?dú)w檔的屬性過(guò)多時(shí),我們需要一條條的寫(xiě)出來(lái),十分繁瑣,有沒(méi)有可能簡(jiǎn)化一些呢,如果單純的用for循環(huán)去做,那么不同的類(lèi)型該怎么處理呢,這時(shí)候我們的runtime又來(lái)了。首先創(chuàng)建一個(gè)Person類(lèi),多弄一些虛擬屬性。

//  .h
@property(nonatomic,strong) NSString * name;
@property(nonatomic,strong) NSString * name1;
@property(nonatomic,assign) int age;
@property(nonatomic,assign) int age1;
@property(nonatomic,assign) double age2;

// .m
-(void)encodeWithCoder:(NSCoder *)Coder
{
    
}

-(instancetype)initWithCoder:(NSCoder *)aDecoder
{
    self=[super init];
    if (self) {
        
    }
    return self;
}

實(shí)現(xiàn)思路:

-(void)encodeWithCoder:(NSCoder *)Coder
{
    for (int i = 0; i < 屬性數(shù)量; i++) {
    [Coder encodeObject:屬性值 forKey:屬性名稱(chēng)];
    }
}

那么 回到controller中,導(dǎo)入runtime頭文件,使用一個(gè)方法class_copyIvarList(<#__unsafe_unretained Class cls#>, <#unsigned int *outCount#>)獲取屬性列表,第一個(gè)參數(shù),傳入一個(gè)類(lèi)[Person class],第二個(gè)參數(shù),傳入一個(gè)指針,在上面定義unsigned int count = 0;,然后傳入&count。這個(gè)count就是獲取的屬性的數(shù)量。同時(shí)在c語(yǔ)言中是不分.h.m的,所以無(wú)論是在哪個(gè)文件中定義的屬性,都可以取到。

unsigned int count = 0;
class_copyIvarList([Person class], &count);

然后我們需要定義一個(gè)Ivar類(lèi)型的指針,這個(gè)指針會(huì)指向每一個(gè)屬性,下面這個(gè)圖說(shuō)明一下,他并不是同時(shí)指向每一個(gè)屬性,而是一個(gè)一個(gè)分別指向來(lái)獲取屬性。

DCF79208-E155-4D1B-8797-DDFC8C6F83B5.png

我們使用一個(gè)方法通過(guò)這個(gè)ivars去獲取屬性名稱(chēng)。

unsigned int count = 0;
    Ivar * ivars = class_copyIvarList([Person class], &count);
    Ivar ivar = ivars[0];
    const char * name = ivar_getName(ivar);
    NSLog(@"%s",name);

打印看到第一個(gè)屬性名,可以改變ivars[第幾個(gè)],去獲取第幾個(gè)。而且即使角標(biāo)越界,依然不會(huì)崩潰。
那么我們回到person類(lèi)中,直接用剛才的代碼實(shí)現(xiàn)我們最開(kāi)始提出的問(wèn)題。

//  歸檔
-(void)encodeWithCoder:(NSCoder *)Coder
{
    unsigned int count = 0;
    Ivar * ivars = class_copyIvarList([Person class], &count);
    for (int i = 0; i < count; i++) {
        Ivar ivar= ivars[i];
        const char * name = ivar_getName(ivar);
        NSString * key = [[NSString alloc]initWithUTF8String:name];
        //  使用KVC  拿出屬性的值
        [Coder encodeObject:[self valueForKey:key] forKey:key];
    }
}
//  解檔
-(instancetype)initWithCoder:(NSCoder *)Decoder
{
    self=[super init];
    if (self) {
        unsigned int count = 0;
        Ivar * ivars = class_copyIvarList([Person class], &count);
        for (int i = 0; i < count; i++) {
            Ivar ivar= ivars[i];
            const char * name = ivar_getName(ivar);
            NSString * key = [[NSString alloc]initWithUTF8String:name];
            //  使用KVC  拿出屬性的值
            id value = [Decoder decodeObjectForKey:key];
            //  設(shè)置屬性
            [self setValue:value forKey:key];
        }

    }
    return self;
}

通過(guò)上面的講述,這段代碼就很容易理解了。我們用的是kvc的賦值和取值,所以任何類(lèi)型的歸檔解檔都是沒(méi)有問(wèn)題的。

teatime.gif

(4)刨析KVO底層實(shí)現(xiàn)

我們先用oc實(shí)現(xiàn)一個(gè)簡(jiǎn)單的KVO監(jiān)聽(tīng)。

//   controller.m
self.c=[[Cat alloc]init];
self.d=[[Dog alloc]init];
    //  注冊(cè)監(jiān)聽(tīng)
[self.d addObserver:self.c forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

//   cat.m
//  監(jiān)聽(tīng)到了object的對(duì)象keyPath屬性變化為樂(lè)change
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"監(jiān)聽(tīng)到了%@的對(duì)象%@屬性變化為樂(lè)%@",object,keyPath,change);
}

這是KVO最簡(jiǎn)單的應(yīng)用,那么接下來(lái)我們看一下KVO底層到底是怎么實(shí)現(xiàn)的呢?
首先在KVO運(yùn)行的時(shí)候會(huì)動(dòng)態(tài)的添加一個(gè)類(lèi),繼承與被觀察者的類(lèi)。名字叫做NSKVONotifying_Dog這個(gè)類(lèi)。類(lèi)名可不是瞎編的哦。然后在.m文件中,調(diào)用父類(lèi)的set方法:

-(void)setAge:(int)age
{
    [super setAge:age];
    // 在子類(lèi)中調(diào)用這兩個(gè)方法
    //  這個(gè)是 將要被改變的值是什么
    [self willChangeValueForKey:@"age"];
    //   這個(gè)是   改變之后的新值是什么
    [self didChangeValueForKey:@"age"];
}

這樣就會(huì)監(jiān)聽(tīng)到改變并且傳值,但是為什么說(shuō)KVO是這樣實(shí)現(xiàn)的呢?
將剛才新建的NSKVONotifying_Dog類(lèi)刪掉,在controller中實(shí)現(xiàn)點(diǎn)擊改變值的方法

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    self.d.age=99;
}

在賦值的地方打斷點(diǎn),如果程序運(yùn)行到這里,d的類(lèi)型變?yōu)閯偛拍莻€(gè)方法的類(lèi)型,那么就說(shuō)明KVO就是這樣實(shí)現(xiàn)的。


B528C61F-E2C4-46BD-9F23-2A06A67867BA.png
teatime.gif

(5)動(dòng)態(tài)添加方法

首先創(chuàng)建一個(gè)Person類(lèi),然后在controller中實(shí)例化person直接可以這樣直接調(diào)用一個(gè)不存在的方法

    Person * p=[[Person alloc]init];
    [p performSelector:@selector(eat)];

這樣雖然編譯可以過(guò),但是運(yùn)行起來(lái)就會(huì)崩潰
這時(shí)候我沒(méi)回到person.m中,來(lái)看兩個(gè)方法

//  當(dāng)這個(gè)類(lèi)被調(diào)用沒(méi)有實(shí)現(xiàn)的類(lèi)方法  就會(huì)來(lái)到這里
+(BOOL)resolveClassMethod:(SEL)sel
{
    return [super resolveClassMethod:sel];
}
//  當(dāng)這個(gè)類(lèi)被調(diào)用沒(méi)有實(shí)現(xiàn)的對(duì)象方法  就會(huì)來(lái)到這里
+(BOOL)resolveInstanceMethod:(SEL)sel
{
    return [super resolveInstanceMethod:sel];
}

方法中的參數(shù)就是被調(diào)用的方法名,然后我們需要實(shí)現(xiàn)一個(gè)名為eat的函數(shù)

void eat(){
    NSLog(@"lalal");
}

這時(shí)候,我們將要用到一個(gè)方法class_addMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>, <#IMP imp#>, <#const char *types#>)第一個(gè)參數(shù):類(lèi)類(lèi)型,第二個(gè)參數(shù):方法編號(hào),第三個(gè)參數(shù):方法實(shí)現(xiàn)(函數(shù)指針),第四個(gè)參數(shù):返回值類(lèi)型。關(guān)于這第四個(gè)參數(shù),這是c語(yǔ)音,我們?cè)撛趺磳?xiě)返回類(lèi)型呢?去查一下官方文檔關(guān)于第四個(gè)參數(shù)的描述。

047E0D3E-7A83-4E78-8EC4-CE7880A0B008.png

那么我們來(lái)實(shí)現(xiàn)代碼

+(BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(eat)) {
        class_addMethod([Person class], sel, (IMP)eat, "v");
    }
    return [super resolveClassMethod:sel];
    return [super resolveInstanceMethod:sel];
}

void eat(){
    NSLog(@"lalal");
}

這樣就完成了一個(gè)動(dòng)態(tài)添加方法,然后我們接著看文檔,文檔中有一段代碼示例

BB20A099-E697-4F05-9580-0ECBB4BFD995.png

我們可以看到,當(dāng)動(dòng)態(tài)添加方法是會(huì)傳入兩個(gè)參數(shù),實(shí)際上每一個(gè)函數(shù)被調(diào)用時(shí)都會(huì)傳入這兩個(gè)參數(shù),叫做隱式參數(shù)。參數(shù)一:調(diào)用了哪個(gè)類(lèi)的,參數(shù)二:調(diào)用了哪個(gè)方法,我們用nslog打印一下這兩個(gè)參數(shù),在這之前需要改一下上面的第四個(gè)參數(shù)為"v@:",因?yàn)槲覀兎祷刂殿?lèi)型改變了。打印一下:

43DBB6BD-18C3-4D6B-9C7B-C46D0F3B235C.png

接下來(lái),就是如何傳遞參數(shù),我們?cè)谡{(diào)用方法的時(shí)候傳入一個(gè)參數(shù)

 Person * p=[[Person alloc]init];
    [p performSelector:@selector(eat:) withObject:@"6666"];

回去Person類(lèi),將判斷的方法名改為eat:,并將eat函數(shù)增加一個(gè)參數(shù):

+(BOOL)resolveInstanceMethod:(SEL)sel
{
    //  方法名的判斷
    if (sel == @selector(eat:)) {
        class_addMethod([Person class], sel, (IMP)eat, "v@:");
    }
    return [super resolveClassMethod:sel];
    return [super resolveInstanceMethod:sel];
}

void eat(id self, SEL _cmd ,id obj){
    NSLog(@"%@ ",obj);
}

控制臺(tái):

76FA3A6D-BF28-48B2-A3A4-B8C3BAFAD621.png

參數(shù)完美傳遞過(guò)來(lái)。

teatime.gif
最后編輯于
?著作權(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)容

  • 對(duì)于從事 iOS 開(kāi)發(fā)人員來(lái)說(shuō),所有的人都會(huì)答出【runtime 是運(yùn)行時(shí)】什么情況下用runtime?大部分人能...
    夢(mèng)夜繁星閱讀 3,804評(píng)論 7 64
  • *面試心聲:其實(shí)這些題本人都沒(méi)怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個(gè)offer,總結(jié)起來(lái)就是把...
    Dove_iOS閱讀 27,595評(píng)論 30 472
  • 參考鏈接: http://www.cnblogs.com/ioshe/p/5489086.html 簡(jiǎn)介 Runt...
    樂(lè)樂(lè)的簡(jiǎn)書(shū)閱讀 2,247評(píng)論 0 9
  • 哈,剛才敲長(zhǎng)大時(shí)多敲了一個(gè) n 出來(lái)了 賬單 長(zhǎng)大后,學(xué)會(huì)的最深遠(yuǎn)的一種方法 就是:趨利避害 是件好事 但是,也就...
    AmNobody閱讀 218評(píng)論 1 0
  • 由河正宇執(zhí)導(dǎo)并與河智苑主演的喜劇片《許三觀》于1月14日在韓國(guó)上映,當(dāng)天就吸引了7.3626萬(wàn)觀影人次。這部電影改...
    耳東陳閱讀 3,871評(píng)論 4 29

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