Runtime簡介

Runtime 概念

runtime(簡稱運行時),是一套純C(C和匯編寫的) 的API。而 OC 就是運行時機制(消息機制)。
在編譯階段,OC 調用并未實現的函數,只要聲明過就不會報錯,只有當運行的時候才會報錯,這是因為OC是運行時動態調用的。而C語言,函數的調用在編譯的時候會決定調用哪個函數,調用未實現的函數就會報錯

runtime 消息機制

OC方法調用本質:就是用 runtime發送一個消息,每一個 OC 的方法底層必然有一個與之對應的 runtime 方法.
消息機制原理:對象根據方法編號SEL去映射表查找對應的方法實現。

  1. 例子:
創建一個macos工程,就在main.m里寫下面簡單的代碼
Dog *dog = [[Dog alloc] init];
[dog run];
1. 導入 #import <objc/message.h>,因為這個里面包含下面兩個
#include <objc/objc.h>
#include <objc/runtime.h>
2.去到 build setting -> 搜索msg ->將Enable Strict Checking of objc_msgSend Calls 改為no 
否則使用 objc_msgSend 編譯出錯,因為xcode默認不建議使用
3.去到main.m所在的目錄,在終端用下面命令編譯一下
clang -rewrite-objc main.m
就會生成一個main.cpp文件
4.打開該文件看最下面main方法,可以看到編譯后的代碼就是runtime
  1. 使用:
    objc_msgSend(id self, SEL op, ...)
    參數:oc對象,方法編號,其他參數...
Dog *dog = [[Dog alloc] init];
[dog run];
可以寫成下面的
//Class 類類型  就是一個特殊的對象
Dog *dog = objc_msgSend([Dog class], @selector(alloc));
dog = objc_msgSend(dog, @selector(init));
objc_msgSend(dog, @selector(run));
//
// 底層的實際寫法
Dog *dog = objc_msgSend(objc_getClass("Dog"),sel_registerName("alloc"));
dog = objc_msgSend(dog, sel_registerName("init"));
objc_msgSend(dog, @selector(run));
  1. 消息機制方法調用流程
    對象方法:(保存到類對象的方法列表) ,類方法:(保存到元類(Meta Class)中方法列表)。
    OC 在向一個對象發送消息時,runtime 庫會根據對象的 isa指針找到該對象對應的類或其父類中根據方法編號(SEL)去查找對應方法,找到只是最終函數實現地址(IMP),根據地址去方法區調用對應函數。
    補充:每一個對象內部都有一個isa指針,這個指針是指向它的真實類型,根據這個指針就能知道將來調用哪個類的方法。
runtime 使用場景
  1. 動態交換兩個方法的實現(method swizzling)HOOK思想
    需求:給系統的imageNamed添加額外功能(是否加載圖片成功)
    方案一:繼承系統的類,重寫方法.(弊端:每次使用都需要導入)
    方案二:搞個分類,定義一個能加載圖片并且能打印的方法(弊端:不能在分類中重寫系統方法imageNamed,因為會把系統的功能給覆蓋掉,而且分類中不能調用super,所以要 自己實現一個帶有擴展功能的方法.但這樣就得改調用的方法,改動大)
    runtime方式實現步驟:
    1.給UIImageView添加分類
    2.自定義并實現帶有擴展功能的方法
    3.交換方法
- (void)viewDidLoad {
    [super viewDidLoad];
    UIImage *image = [UIImage imageNamed:@"123"];
}

#import <objc/message.h>
@implementation UIImage (Image)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    // 獲取方法地址
    Method imageNamedMethod = class_getClassMethod(self, @selector(imageNamed:));
    Method ln_imageNamedMethod = class_getClassMethod(self, @selector(ln_imageNamed:));
    // 交換方法地址
    if (!class_addMethod([self class], @selector(ln_imageNamed:), method_getImplementation(ln_imageNamedMethod), method_getTypeEncoding(ln_imageNamedMethod))) {
        method_exchangeImplementations(imageNamedMethod, ln_imageNamedMethod);
    }
    });
}
// 自己定義的方法
+ (UIImage *)ln_imageNamed:(NSString *)name {
    UIImage *image = [UIImage ln_imageNamed:name];
    if (image) {
        NSLog(@"load image success");
    } else {
        NSLog(@"load image failed");
    }
    return image;
}
@end

上面代碼執行過程,會先執行load方法,這個時候imageNamed:和ln_imageNamed:就交換了,走到viewDidLoad的 [UIImage imageNamed:@"123"] 時,實際上執行的是ln_imageNamed:,ln_imageNamed:里面又調用ln_imageNamed:,實際上調用的是imageNamed:,這樣就根據imageNamed:的返回值來判斷。

屏幕快照 2017-08-10 上午12.43.14.png

說明以及注意事項:

  • 方法交換為什么寫在load方法
    load 把類加載進內存的時候調用,只會調用一次
  • 為了避免Swizzling的代碼被重復執行(調用[super load]),利用dispatch_once函數內代碼只會執行一次的特性。
  • class_getClassMethod(獲取某個類的方法)
    class_getInstanceMethod (獲取某個對象的方法)
  • IMP本質上就是函數指針,所以我們可以通過打印函數地址的方式,查看SEL和IMP的交換流程
Method imageNamedMethod = class_getClassMethod(self, @selector(imageNamed:));
Method ln_imageNamedMethod = class_getClassMethod(self, @selector(ln_imageNamed:));
NSLog(@"%p", method_getImplementation(imageNamedMethod));
NSLog(@"%p", method_getImplementation(ln_imageNamedMethod));
method_exchangeImplementations(imageNamedMethod, ln_imageNamedMethod);
  • 使用class_addMethod()函數對Method Swizzling做了一層驗證,如果self沒有實現被交換的方法,會導致失敗。而且self沒有交換的方法實現,但是父類有這個方法(或者自己有這個方法),這樣就會調用父類的方法,結果就不是我們想要的結果了。所以我們在這里通過class_addMethod()的驗證,如果self實現了這個方法,class_addMethod()函數將會返回NO,我們就可以對其進行交換了
  1. runtime結合kvc實現NSCoding的自動歸檔和解檔
    如果一個模型有許多個屬性,那么我們需要對每個屬性都實現一遍encodeObject 和 decodeObjectForKey方法,如果這樣的模型又有很多個,就非常麻煩。
  • 原來的做法
遵守協議NSCoding
@property (nonatomic, copy) NSString *name;
- (void)encodeWithCoder:(NSCoder *)aCoder{
    [aCoder encodeObject:_Name forKey:@"name"];
}
- (id)initWithCoder:(NSCoder *)aDecoder{
    if (self = [super init]) {
        self.movieName = [aDecoder decodeObjectForKey:@"name"];
    }
    return self;
}
  • 新做法(主要代碼)
//解檔
- (void)decode:(NSCoder *)aDecoder {
    // 一層層父類往上查找,對父類的屬性執行歸解檔方法
    Class c = self.class;
    while (c &&c != [NSObject class]) {
        
        unsigned int outCount = 0;
        Ivar *ivars = class_copyIvarList(c, &outCount);
        for (int i = 0; i < outCount; i++) {
            Ivar ivar = ivars[i];
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            
            // 如果有實現該方法再去調用
            if ([self respondsToSelector:@selector(ignoredNames)]) {
                if ([[self ignoredNames] containsObject:key]) continue;
            }
            
            id value = [aDecoder decodeObjectForKey:key];
            [self setValue:value forKey:key];
        }
        free(ivars);
        c = [c superclass];
    }
    
}
// 歸檔
- (void)encode:(NSCoder *)aCoder {
    // 一層層父類往上查找,對父類的屬性執行歸解檔方法
    Class c = self.class;
    while (c &&c != [NSObject class]) {
        
        unsigned int outCount = 0;
        Ivar *ivars = class_copyIvarList([self class], &outCount);
        for (int i = 0; i < outCount; i++) {
            Ivar ivar = ivars[i];
            // 獲取成員變量的名字
            const char *name = ivar_getName(ivar);
            //// C字符串 -> OC字符串
            NSString *key = [NSString stringWithUTF8String:name];
            
            // 如果有實現該方法再去調用
            if ([self respondsToSelector:@selector(ignoredNames)]) {
                if ([[self ignoredNames] containsObject:key]) continue;
            }
            
            id value = [self valueForKeyPath:key];
            [aCoder encodeObject:value forKey:key];
        }
        free(ivars);
        c = [c superclass];
    }
}
  1. 動態添加方法
    如果一個類方法非常多,因為需要給每個方法生成映射表,實際上只要一個類實現了某個方法,就會被加載進內存,加載類到內存的時候就比較耗費資源。當硬件內存過小的時候,如果我們將每個方法都直接加到內存當中去,但是很久都不用一次,這樣就造成了浪費,那如果我想像懶加載一樣,先把方法定義好,但是只有當你用的時候我再加載你,這就需要動態添加了。
    當performSelector方法調用某個sel的時候,這時候會到調用對象的+ (BOOL)resolveInstanceMethod:(SEL)sel方法中,如果這里返回是NO,就表示找不到。
  • 看下面的例子
// 動態添加方法就不會報錯
    Person * p = [[Person alloc] init];
    [p performSelector:@selector(eat:) withObject:@"吃過了"];

//下面代碼在Person.m里
#import <objc/runtime.h>
void addEat(id self, SEL _cmd, NSString *str) {
    NSLog(@"%@", str);
}
// 任何方法默認都有兩個隱式參數,self,_cmd(當前方法的方法編號)
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    // [NSStringFromSelector(sel) isEqualToString:@"run"];
    if (sel == NSSelectorFromString(@"eat:")) {
        BOOL isSuccess = class_addMethod(self, sel, (IMP)addEat, "v@:@");
        return isSuccess;
    }
    return [super resolveInstanceMethod:sel];
}
  • class_addMethod參數解釋(可以command+shift+0查看官方文檔)
    class_addMethod(Class cls, SEL name, IMP imp,const char *types)
  1. class: 給哪個類添加方法
  2. SEL: 添加方法的方法編號
  3. IMP: 方法實現 (添加方法的函數實現(函數地址))
  4. type: 方法類型,(返回值+參數類型)
    (1) v 返回值類型是void
    (2)@ 對象->self
    (3): 表示SEL->_cmd
    (4)@ 第四個參數
  • resolveInstanceMethod的作用
    當調用了沒有實現的方法沒有實現就會調用,然后就可以根據他的參數sel(參數sel就是沒有實現的方法)來做一系列的操作。

4.給分類添加屬性
在分類中,所寫的@property (nonatomic, strong) NSString *name;都僅僅是生成了get和set方法,并沒有生成對應的_name屬性,但是有時候我們會有一種需求,想要讓分類中保存一下新的屬性值,因為set和get方法只能是對已經有的東西做操作,比如說最常用的UIView的分類我們對frame中的x,y,width,height做操作。

//給Person添加一個分類addProperty
//在Person+addProperty.h中
@property (nonatomic, strong) NSString *name;
//在Person+addProperty.m中
#import <objc/message.h>
@implementation Person (addProperty)
- (void)setName:(NSString *)name{
    objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)name{
    return objc_getAssociatedObject(self, @"name"); 
}

- (void)viewDidLoad {
    [super viewDidLoad];
    //給分類動態添加屬性
    Person * p1 = [[Person alloc] init];
    p1.name = @"這是給分類添加的屬性";
    NSLog(@"%@",p1.name);
}

解釋:
objc_setAssociatedObject方法

/**
     *  根據某個對象,還有key,還有對應的策略(copy,strong等) 動態的將值設置到這個對象的key上
     *  @param object 某個對象
     *  @param key    屬性名,根據key去獲取關聯的對象
     *  @param value  要設置的值
     *  @param policy 策略(copy,strong,assign等)
     */
    OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);

objc_getAssociatedObject方法

/**
     *  根據某個對象,還有key 動態的獲取到這個對象的key對應的屬性的值
     *  @param object 某個對象
     *  @param key    key
     *  @return 對象的值
     */
    OBJC_EXPORT id objc_getAssociatedObject(id object, const void *key)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);

4.實現字典轉模型的自動轉換
字典轉模型KVC實現會有很多弊端,利用運行時,遍歷模型中所有屬性,根據模型的屬性名,去字典中查找key,取出對應的值,給模型的屬性賦值。
1.當字典的key和模型的屬性匹配不上。
2.模型中嵌套模型(模型屬性是另外一個模型對象)。
3.數組中裝著模型(模型的屬性是一個數組,數組中是一個個模型對象)。
注解:根據上面的三種特殊情況,先是字典的key和模型的屬性不對應的情況。不對應有兩種,一種是字典的鍵值大于模型屬性數量,這時候我們不需要任何處理,因為runtime是先遍歷模型所有屬性,再去字典中根據屬性名找對應值進行賦值,多余的鍵值對也當然不會去看了;另外一種是模型屬性數量大于字典的鍵值對,這時候由于屬性沒有對應值會被賦值為nil,就會導致crash,我們只需加一個判斷即可。考慮三種情況下面一一注解;

步驟:提供一個NSObject分類,專門字典轉模型,以后所有模型都可以通過這個分類實現字典轉模型。
MJExtension 字典轉模型實現,底層也是對 runtime 的封裝。

注:本文參考 http://www.lxweimin.com/p/19f280afcb24
更全面的例子參考 https://github.com/lizelu/ObjCRuntimeDemo

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 對于從事 iOS 開發人員來說,所有的人都會答出【runtime 是運行時】什么情況下用runtime?大部分人能...
    夢夜繁星閱讀 3,732評論 7 64
  • 一、Runtime簡介 RunTime簡稱運行時。OC就是運行時機制,也就是在運行時候的一些機制,其中最主要的是消...
    竇豆逗閱讀 161評論 0 0
  • Runtime簡介以及常見的使用場景 Runtime簡稱運行時,是一套比較底層的純C語言的API,作為OC的核心...
    輕云_閱讀 1,197評論 5 23
  • RunTime簡稱運行時。OC就是運行時機制,也就是在運行時候的一些機制,其中最主要的是消息機制。對于C語言,函數...
    _心暖閱讀 603評論 1 1
  • 轉至元數據結尾創建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數據起始第一章:isa和Class一....
    40c0490e5268閱讀 1,774評論 0 9