iOS --runtime理解與應用

黑白灰的綠i發表於2017-12-13

1.什麼是Runtime?

我所理解的runtime是一個使用C編寫的庫,為C新增了物件導向的特性,它是一個庫(Runtime Library中文:執行時庫).在這個庫中可以用C函式來實現方法,物件也可以用C語言的結構體來表示…所有oc的方法的背後都是通過runtime來執行的.

2.runtime的使用

(1)利用runtime的訊息傳送機制呼叫方法

首先新建一個類RuntimeModel,並實現物件方法eat,在RuntimeViewController中呼叫eat方法,使用oc來語言來實現很簡單了

RuntimeModel * model=[[RuntimeModel alloc]init];
[model eat];
複製程式碼

接下來一點點用runtime實現上面的程式碼,匯入runtime的標頭檔案#import <objc/message.h>,由於xcode5.0開始蘋果不建議我們使用底層的程式碼,所以target->build setting->搜尋msg->將YES改為NO,這樣接下來我們用runtime的時候才會出現提示。 我們使用objc_msgSend(<#id self#>, <#SEL op, ...#>)這個方法,可以看到需要兩個引數,第一個引數是id型別,代表誰要傳送訊息,第二個引數是要把訊息傳送給誰,我們用runtime來實現[model eat];這個方法。

objc_msgSend(model, sel_registerName("eat"));
複製程式碼

而初始化物件同樣是呼叫了alloc init這兩個方法。將匯入的標頭檔案RuntimeModel去掉,用純c語言的程式碼實現上面的功能。

objc_msgSend(objc_msgSend(objc_msgSend(objc_getClass("RuntimeModel"), sel_registerName("alloc")), sel_registerName("init")), sel_registerName("eat"));
複製程式碼

執行,我們可以看到在eat方法中的nslog被呼叫了,雖然我們實現了功能,但是怎麼才能知道我們的oc語言在執行時確實是被轉換成了c語言的程式碼呢? 新建工程,建立命令列工具。

4BEFBE2C-70AE-490F-BE08-926725DC0C2B.png
新建一個person類,然後進入main.m中。 呼叫標頭檔案,在main中例項化person。person * p=[[person alloc]init];關閉專案,開啟終端,進入剛才建的資料夾下,ls開啟可以看到剛才我們新建的類和main.m檔案,接下來執行命令列clang -rewrite-objc main.m 這時我們可以看到,在剛才的工程中出現一個main.cpp的檔案,開啟並且拖到最下面。

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

teatime.gif

(2)交換方法

做為oc的程式設計師最悲慘的就是,執行--崩潰在main裡面,我尼瑪!!! 例如:

NSURL * url=[NSURL URLWithString:@"www.baidu.com.啦啦"];
複製程式碼

當我們沒有進行編碼的時候,這個url在編譯的時候是有效的,但是一旦執行起來,這個url就會變為nil。因為檢測不到這是一個無效的url,會繼續傳送網路請求。 怎麼辦呢?在下面用if做一個判斷?可是要在每一個url下面都做判斷。這時候最先想到的一定是重寫。 建立一個url的分類,重寫URLWithString:<#(nonnull NSString *)#>但是,我們看:

+(instancetype)URLWithString:(NSString *)URLString
{
    //  首先建立一個URL
    NSURL * url= ????????
    if (url==nil) {
        NSLog(@"有問題");
    }
    return url;
}
複製程式碼

我們該怎麼建立呢,死迴圈了是不是,所以runtime就起作用了。 在分類中自定義一個方法

+(instancetype)TY_URLWithStr:(NSString *)Str;
{
    NSURL * url =[NSURL URLWithString:Str];
    if (url == nil) {
        NSLog(@"是空!!!!");
    }
    return url;
}
複製程式碼

但是我們並不是要每一次建立url都呼叫這個方法,因為程式裡有很多建立url的地方,我們還繼續使用系統自帶的方法。在分類中實現load方法,當程式載入這個類的時候最先呼叫這個方法。匯入標頭檔案,開始進行方法交換。

+(void)load
{
    //  Method : 成員方法
    //class_getClassMethod   拿到類方法
    //class_getInstanceMethod   拿到物件方法
    Method URLWithStr = class_getClassMethod([NSURL class], @selector(URLWithString:));
    Method TYURLWithStr = class_getClassMethod([NSURL class], @selector(TY_URLWithStr:));
    //  開始交換方法
    method_exchangeImplementations(URLWithStr, TYURLWithStr);
}
複製程式碼

記得將上面我們自定義的方法中建立url的方法改變回去,不然再次死迴圈,改為

+(instancetype)TY_URLWithStr:(NSString *)Str;
{
    NSURL * url =[NSURL TY_URLWithStr:Str];
    if (url == nil) {
        NSLog(@"是空!!!!");
    }
    return url;
}
複製程式碼

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

teatime.gif

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

oc的序列化在這裡就不多說了,讓我們來說一種常見的情況,當需要歸檔的屬性過多時,我們需要一條條的寫出來,十分繁瑣,有沒有可能簡化一些呢,如果單純的用for迴圈去做,那麼不同的型別該怎麼處理呢,這時候我們的runtime又來了。首先建立一個Person類,多弄一些虛擬屬性。

//  .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;
}
複製程式碼

實現思路:

-(void)encodeWithCoder:(NSCoder *)Coder
{
    for (int i = 0; i < 屬性數量; i++) {
    [Coder encodeObject:屬性值 forKey:屬性名稱];
    }
}
複製程式碼

那麼 回到controller中,匯入runtime標頭檔案,使用一個方法class_copyIvarList(<#__unsafe_unretained Class cls#>, <#unsigned int *outCount#>)獲取屬性列表,第一個引數,傳入一個類[Person class],第二個引數,傳入一個指標,在上面定義unsigned int count = 0;,然後傳入&count。這個count就是獲取的屬性的數量。同時在c語言中是不分.h.m的,所以無論是在哪個檔案中定義的屬性,都可以取到。

unsigned int count = 0;
class_copyIvarList([Person class], &count);
複製程式碼

然後我們需要定義一個Ivar型別的指標,這個指標會指向每一個屬性,下面這個圖說明一下,他並不是同時指向每一個屬性,而是一個一個分別指向來獲取屬性。

DCF79208-E155-4D1B-8797-DDFC8C6F83B5.png
我們使用一個方法通過這個ivars去獲取屬性名稱。

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

列印看到第一個屬性名,可以改變ivars[第幾個],去獲取第幾個。而且即使角標越界,依然不會崩潰。 那麼我們回到person類中,直接用剛才的程式碼實現我們最開始提出的問題。

//  歸檔
-(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];
            //  設定屬性
            [self setValue:value forKey:key];
        }

    }
    return self;
}
複製程式碼

通過上面的講述,這段程式碼就很容易理解了。我們用的是kvc的賦值和取值,所以任何型別的歸檔解檔都是沒有問題的。

teatime.gif

(4)刨析KVO底層實現

我們先用oc實現一個簡單的KVO監聽。

//   controller.m
self.c=[[Cat alloc]init];
self.d=[[Dog alloc]init];
    //  註冊監聽
[self.d addObserver:self.c forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

//   cat.m
//  監聽到了object的物件keyPath屬性變化為樂change
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"監聽到了%@的物件%@屬性變化為樂%@",object,keyPath,change);
}
複製程式碼

這是KVO最簡單的應用,那麼接下來我們看一下KVO底層到底是怎麼實現的呢? 首先在KVO執行的時候會動態的新增一個類,繼承與被觀察者的類。名字叫做NSKVONotifying_Dog這個類。類名可不是瞎編的哦。然後在.m檔案中,呼叫父類的set方法:

-(void)setAge:(int)age
{
    [super setAge:age];
    // 在子類中呼叫這兩個方法
    //  這個是 將要被改變的值是什麼
    [self willChangeValueForKey:@"age"];
    //   這個是   改變之後的新值是什麼
    [self didChangeValueForKey:@"age"];
}
複製程式碼

這樣就會監聽到改變並且傳值,但是為什麼說KVO是這樣實現的呢? 將剛才新建的NSKVONotifying_Dog類刪掉,在controller中實現點選改變值的方法

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

在賦值的地方打斷點,如果程式執行到這裡,d的型別變為剛才那個方法的型別,那麼就說明KVO就是這樣實現的。

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

teatime.gif

(5)動態新增方法

首先建立一個Person類,然後在controller中例項化person直接可以這樣直接呼叫一個不存在的方法

    Person * p=[[Person alloc]init];
    [p performSelector:@selector(eat)];
複製程式碼

這樣雖然編譯可以過,但是執行起來就會崩潰 這時候我沒回到person.m中,來看兩個方法

//  當這個類被呼叫沒有實現的類方法  就會來到這裡
+(BOOL)resolveClassMethod:(SEL)sel
{
    return [super resolveClassMethod:sel];
}
//  當這個類被呼叫沒有實現的物件方法  就會來到這裡
+(BOOL)resolveInstanceMethod:(SEL)sel
{
    return [super resolveInstanceMethod:sel];
}
複製程式碼

方法中的引數就是被呼叫的方法名,然後我們需要實現一個名為eat的函式

void eat(){
    NSLog(@"lalal");
}
複製程式碼

這時候,我們將要用到一個方法class_addMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>, <#IMP imp#>, <#const char *types#>)第一個引數:類型別,第二個引數:方法編號,第三個引數:方法實現(函式指標),第四個引數:返回值型別。關於這第四個引數,這是c語音,我們該怎麼寫返回型別呢?去查一下官方文件關於第四個引數的描述。

047E0D3E-7A83-4E78-8EC4-CE7880A0B008.png
那麼我們來實現程式碼

+(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");
}
複製程式碼

這樣就完成了一個動態新增方法,然後我們接著看文件,文件中有一段程式碼示例

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

我們可以看到,當動態新增方法是會傳入兩個引數,實際上每一個函式被呼叫時都會傳入這兩個引數,叫做隱式引數。引數一:呼叫了哪個類的,引數二:呼叫了哪個方法,我們用nslog列印一下這兩個引數,在這之前需要改一下上面的第四個引數為"v@:",因為我們返回值型別改變了。列印一下:

43DBB6BD-18C3-4D6B-9C7B-C46D0F3B235C.png
接下來,就是如何傳遞引數,我們在呼叫方法的時候傳入一個引數

 Person * p=[[Person alloc]init];
    [p performSelector:@selector(eat:) withObject:@"6666"];
複製程式碼

回去Person類,將判斷的方法名改為eat:,並將eat函式增加一個引數:

+(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);
}
複製程式碼

控制檯:

76FA3A6D-BF28-48B2-A3A4-B8C3BAFAD621.png
引數完美傳遞過來。

teatime.gif

相關文章