iOS runtime詳解

引導

runtime是運行時,對于從事iOS開發,想要深入學習OC的人,runtime是必須熟悉掌握的東西。

runtime的概念

Objective-C 是基于 C 的,它為 C 添加了面向對象的特性。它將很多靜態語言在編譯和鏈接時期做的事放到了runtime 運行時來處理,可以說runtime是我們Objective-C幕后工作者。

  • runtime(簡稱運行時),是一套 純C(C和匯編寫的) 的API。而OC就是運行時機制,也就是在運行時候的一些機制,其中最主要的是消息機制。
  • 對于 C 語言,函數的調用在編譯的時候會決定調用哪個函數。
  • OC的函數調用成為消息發送,屬于動態調用過程。在編譯的時候并不能決定真正調用哪個函數,只有在真正運行的時候才會根據函數的名稱找到對應的函數來調用。
  • 在編譯階段,OC可以調用任何函數,即使這個函數并未實現,只要聲明過就不會報錯,只有當運行的時候才會報錯,這是因為OC是運行時動態調用的。而C語言調用未實現的函數就會報錯。

runtime的消息機制

  • 我們寫的OC代碼運行的時候也是轉換成了runtime方式運行的。任何方法調用本質:就是發送一個消息(用runtime發送消息,OC底層實現通過runtime實現)。
  • 消息機制原理:對象根據方法編號SEL去映射表查找對應的方法實現。
  • 每一個OC的方法,底層必然有一個與之對應的runtime方法。

簡單示例:
驗證:方法調用,是否真的是轉換為消息機制?
必須要導入頭文件 #import<objc/message.h>
注解1:我們導入系統的頭文件,一般用尖括號。
注解2:OC 解決消息機制方法提示步驟【查找build setting -> 搜索msg -> objc_msgSend(YES --> NO)】
注解3:最終生成消息機制,編譯器做的事情,最終代碼,需要把當前代碼重新編譯,用xcode編譯器,【clang -rewrite-objc main.m 查看最終生成代碼】,示例:cd main.m --> 輸入前面指令,就會生成 .opp文件(C++代碼)
注解4:這里一般不會直接導入<objc/runtime.h>
示例代碼:OC 方法-->runtime 方法

說明: eat(無參) 和 run(有參) 是 Person模型類中的私有方法「可以幫我調用私有方法」;

// Person *p = [Person alloc];
// 底層的實際寫法
Person *p = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));

// p = [p init];
p = objc_msgSend(p, sel_registerName("init"));

// 調用對象方法(本質:讓對象發送消息)
//[p eat];

// 本質:讓類對象發送消息
objc_msgSend(p, @selector(eat));
objc_msgSend([Person class], @selector(run:),20);

//--------------------------- <#我是分割線#> ------------------------------//
// 也許下面這種好理解一點

// id objc = [NSObject alloc];
id objc = objc_msgSend([NSObject class], @selector(alloc));

// objc = [objc init];
objc = objc_msgSend(objc, @selector(init));

runtime 方法調用流程「消息機制」

消息機制方法調用流程

怎么去調用類方法和實例方法,實例方法:(保存到類對象的方法列表) ,類方法:(保存到元類(Meta Class)中方法列表)。
1.OC在向一個對象發送消息時,runtime庫會根據對象的isa指針找到該對象對應的或其父類中查找方法。
2.注冊方法編號(這里用方法編號的好處,可以快速查找)。
3.根據方法編號去查找對應方法。
4.找到只是最終函數實現地址,根據地址去方法區調用對應函數。

一個objc 對象的 isa 的指針指向什么?有什么作用?

每一個對象內部都有一個isa指針,這個指針是指向它的真實類型,根據這個指針就能知道將來調用哪個類的方法。

runtime 常見作用

  • 動態交換兩個方法的實現
  • 動態添加屬性
  • 實現字典轉模型的自動轉換
  • 發送消息
  • 動態添加方法
  • 攔截并替換方法
  • 實現 NSCoding 的自動歸檔和解檔

runtime 常用開發應用場景「工作掌握」

runtime 交換方法

應用場景:當第三方框架 或者 系統原生方法功能不能滿足我們的時候,我們可以在保持系統原有方法功能的基礎上,添加額外的功能。
需求:加載一張圖片直接用[UIImage imageNamed:@"image"];是無法知道到底有沒有加載成功。給系統的imageNamed添加額外功能(是否加載圖片成功)。
方案一:繼承系統的類,重寫方法.(弊端:每次使用都需要導入)
方案二:使用 runtime,交換方法.
實現步驟:
1.給系統的方法添加分類
2.自己實現一個帶有擴展功能的方法
3.交換方法,只需要交換一次

- (void)viewDidLoad {
    [super viewDidLoad];
    // 方案一:先搞個分類,定義一個能加載圖片并且能打印的方法+ (instancetype)imageWithName:(NSString *)name;
    // 方案二:交換 imageNamed 和 ln_imageNamed 的實現,就能調用 imageNamed,間接調用 ln_imageNamed 的實現。
    UIImage *image = [UIImage imageNamed:@"123"];
}

#import <objc/message.h> 
@implementation UIImage (Image)
/**
 load方法: 把類加載進內存的時候調用,只會調用一次
 方法應先交換,再去調用
 */
+ (void)load {

    // 1.獲取 imageNamed方法地址
    // class_getClassMethod(獲取某個類的方法)
    Method imageNamedMethod = class_getClassMethod(self, @selector(imageNamed:));
    // 2.獲取 ln_imageNamed方法地址
    Method ln_imageNamedMethod = class_getClassMethod(self, @selector(ln_imageNamed:));

    // 3.交換方法地址,相當于交換實現方式;「method_exchangeImplementations 交換兩個方法的實現」
    method_exchangeImplementations(imageNamedMethod, ln_imageNamedMethod);
}

/**
 看清楚下面是不會有死循環的
 調用 imageNamed => ln_imageNamed
 調用 ln_imageNamed => imageNamed
 */
// 加載圖片 且 帶判斷是否加載成功
+ (UIImage *)ln_imageNamed:(NSString *)name {

    UIImage *image = [UIImage ln_imageNamed:name];
    if (image) {
        NSLog(@"runtime添加額外功能--加載成功");
    } else {
        NSLog(@"runtime添加額外功能--加載失敗");
    }
    return image;
}

/**
 不能在分類中重寫系統方法imageNamed,因為會把系統的功能給覆蓋掉,而且分類中不能調用super
 所以第二步,我們要 自己實現一個帶有擴展功能的方法.
 + (UIImage *)imageNamed:(NSString *)name {

 }
 */
@end

// 打印輸出
2017-02-17 17:52:14.693 runtime[12761:543574] runtime添加額外功能--加載成功 

總結:我們交換兩個方法地址指向,必須在系統的imageNamed:方法調用前,所以講代碼卸載分類的load方法中,最后當運行的時候系統的方法就會去找我們的方法的實現。

runtime給分類動態添加屬性

原理:給一個類聲明屬性,其實本質就是給這個類添加關聯,并不是直接把這個值的內存空間添加到類存空間。

應用場景:給系統的類添加屬性的時候,可以使用runtime動態添加屬性方法。
注解:系統 NSObject 添加一個分類,我們知道在分類中是不能夠添加成員屬性的,雖然我們用了@property,但是僅僅會自動生成get和set方法的聲明,并沒有帶下劃線的屬性和方法實現生成。但是我們可以通過runtime就可以做到給它方法的實現。

需求:給系統 NSObject 類動態添加屬性 name 字符串。

案例代碼:方法+調用+打印

@interface NSObject (Property)

// @property分類:只會生成get,set方法聲明,不會生成實現,也不會生成下劃線成員屬性
@property NSString *name;
@property NSString *height;
@end

@implementation NSObject (Property)

- (void)setName:(NSString *)name {

    // objc_setAssociatedObject(將某個值跟某個對象關聯起來,將某個值存儲到某個對象中)
    // object:給哪個對象添加屬性
    // key:屬性名稱
    // value:屬性值
    // policy:保存策略
    objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSString *)name {
    return objc_getAssociatedObject(self, @"name");
}

// 調用
NSObject *objc = [[NSObject alloc] init];
objc.name = @"123";
NSLog(@"runtime動態添加屬性name==%@",objc.name);

// 打印輸出
2017-02-17 19:37:10.530 runtime[12761:543574] runtime動態添加屬性--name == 123

總結:給屬性賦值的本質其實就是讓屬性與一個對象產生關聯,所以要個NSObject的分類的name屬性賦值就是讓name和NSObject產生關聯,runtime可以做到這一點。

runtime字典轉模型

字典轉模型的方式:

  • 一個一個給模型屬性賦值

  • 字典轉模型KVC實現
    1、KVC字典轉模型弊端:必須保證,模型中的屬性和字典中的key一一對應
    2、如果不一致,就會調用[<Status 0x7fa74b545d60> setValue:forUndefinedKey:]key找不到的錯。
    3、分析:模型中的屬性和字典中的key不一一對應,系統就會調用setValue:forUndefinedKey:報錯。
    4、解決:重寫對象的setValue:forUndefinedKey:,把系統的方法覆蓋,就能繼續使用KVC字典轉模型。

  • 字典轉模型Runtime實現
    思路:利用運行時,遍歷模型中的所有屬性,根據模型中的屬性名,去字典中查找key,取出對應的值,給模型的屬性賦值(注:字典中的取值,不一定會全部取出來)。

考慮情況:
1、當字典中的key 和模型的屬性匹配不上。
2、模型中嵌套模型(模型屬性是另一個模型對象)。
3、模型的屬性是一個數組,數組中是一個個模型對象。

注解:字典中的key和模型的屬性不對應的情況有兩種,一種是字典的鍵值對大于模型的屬性數量,這時候我們不需要任何處理,因為runtime是先遍歷模型所有屬性,再去字典中根據屬性名找對應的值進行賦值,多余的鍵值對不需要去看;另外一種情況是模型屬性數量大于字典中的鍵值對,這時候由于屬性沒有對應值會被賦值為nil,就會導致crash,只需加一個判斷即可。

實現步驟:提供一個NSObject分類,專門字典轉模型,以后所有模型都可以通過這個分類實現字典轉模型。

MJExtension字典轉模型實現也是通過底層對runtime進行封裝,才可以把模型中所有屬性遍歷出來。

字典轉模型Runtime方式實現

1、runtime字典轉為模型 -- 字典中的key和模型的屬性不匹配(模型屬性數量大于字典鍵值對),代碼如下:

// Runtime:根據模型中屬性,去字典中取出對應的value給模型屬性賦值
// 思路:遍歷模型中所有屬性->使用運行時
+ (instancetype)modelWithDict:(NSDictionary *)dict
{
    // 1.創建對應的對象
    id objc = [[self alloc] init];

    // 2.利用runtime給對象中的屬性賦值
    /**
     class_copyIvarList: 獲取類中的所有成員變量
     Ivar:成員變量
     第一個參數:表示獲取哪個類中的成員變量
     第二個參數:表示這個類有多少成員變量,傳入一個Int變量地址,會自動給這個變量賦值
     返回值Ivar *:指的是一個ivar數組,會把所有成員屬性放在一個數組中,通過返回的數組就能全部獲取到。
     count: 成員變量個數
     */
    unsigned int count = 0;
    // 獲取類中的所有成員變量
    Ivar *ivarList = class_copyIvarList(self, &count);

    // 遍歷所有成員變量
    for (int i = 0; i < count; i++) {
        // 根據角標,從數組取出對應的成員變量
        Ivar ivar = ivarList[i];

        // 獲取成員變量名字
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];

        // 處理成員變量名->字典中的key(去掉 _ ,從第一個角標開始截取)
        NSString *key = [ivarName substringFromIndex:1];

        // 根據成員屬性名去字典中查找對應的value
        id value = dict[key];

        // 【如果模型屬性數量大于字典鍵值對數理,模型屬性會被賦值為nil】
        // 而報錯 (could not set nil as the value for the key age.)
        if (value) {
            // 給模型中屬性賦值
            [objc setValue:value forKey:key];
        }

    }

    return objc;
}

這里在獲取模型類中的所有屬性名,是采取 class_copyIvarList 先獲取成員變量(以下劃線開頭) ,然后再處理成員變量名->字典中的key(去掉 _ ,從第一個角標開始截取) 得到屬性名。
原因:
Ivar:成員變量,以下劃線開頭,Property 屬性
獲取類里面屬性 class_copyPropertyList
獲取類中的所有成員變量 class_copyIvarList

{
    int _a; // 成員變量
}

@property (nonatomic, assign) NSInteger attitudes_count; // 屬性

這里有成員變量,就不會漏掉屬性;如果有屬性,可能會漏掉成員變量;

使用runtime字典轉模型獲取模型屬性名的時候,最好獲取成員屬性名Ivar因為可能會有個屬性是沒有settergetter方法的。

2、runtime字典轉模型--模型中嵌套模型(模型屬性是另外一個模型對象),代碼如下:

+ (instancetype)modelWithDict2:(NSDictionary *)dict
{
    // 1.創建對應的對象
    id objc = [[self alloc] init];

    // 2.利用runtime給對象中的屬性賦值
    unsigned int count = 0;
    // 獲取類中的所有成員變量
    Ivar *ivarList = class_copyIvarList(self, &count);

    // 遍歷所有成員變量
    for (int i = 0; i < count; i++) {
        // 根據角標,從數組取出對應的成員變量
        Ivar ivar = ivarList[i];

        // 獲取成員變量名字
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        // 獲取成員變量類型
        NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];

        // 替換: @\"User\" -> User
        ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
        ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];

        // 處理成員屬性名->字典中的key(去掉 _ ,從第一個角標開始截取)
        NSString *key = [ivarName substringFromIndex:1];

        // 根據成員屬性名去字典中查找對應的value
        id value = dict[key];

        //--------------------------- <#我是分割線#> ------------------------------//
        //
        // 二級轉換:如果字典中還有字典,也需要把對應的字典轉換成模型
        // 判斷下value是否是字典,并且是自定義對象才需要轉換
        if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]) {

            // 字典轉換成模型 userDict => User模型, 轉換成哪個模型
            // 根據字符串類名生成類對象
            Class modelClass = NSClassFromString(ivarType);

            if (modelClass) { // 有對應的模型才需要轉
                // 把字典轉模型
                value = [modelClass modelWithDict2:value];
            }
        }

        // 給模型中屬性賦值
        if (value) {
            [objc setValue:value forKey:key];
        }
    }
    return objc;
}

3、runtime字典轉模型--數組中裝著模型(模型的屬性是一個數組,數組中是字典模型對象),代碼如下:

// Runtime:根據模型中屬性,去字典中取出對應的value給模型屬性賦值
// 思路:遍歷模型中所有屬性->使用運行時
+ (instancetype)modelWithDict3:(NSDictionary *)dict
{
    // 1.創建對應的對象
    id objc = [[self alloc] init];

    // 2.利用runtime給對象中的屬性賦值
    unsigned int count = 0;
    // 獲取類中的所有成員變量
    Ivar *ivarList = class_copyIvarList(self, &count);

    // 遍歷所有成員變量
    for (int i = 0; i < count; i++) {
        // 根據角標,從數組取出對應的成員變量
        Ivar ivar = ivarList[i];

        // 獲取成員變量名字
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];

        // 處理成員屬性名->字典中的key(去掉 _ ,從第一個角標開始截取)
        NSString *key = [ivarName substringFromIndex:1];

        // 根據成員屬性名去字典中查找對應的value
        id value = dict[key];


        //--------------------------- <#我是分割線#> ------------------------------//
        //

        // 三級轉換:NSArray中也是字典,把數組中的字典轉換成模型.
        // 判斷值是否是數組
        if ([value isKindOfClass:[NSArray class]]) {
            // 判斷對應類有沒有實現字典數組轉模型數組的協議
            // arrayContainModelClass 提供一個協議,只要遵守這個協議的類,都能把數組中的字典轉模型
            if ([self respondsToSelector:@selector(arrayContainModelClass)]) {

                // 轉換成id類型,就能調用任何對象的方法
                id idSelf = self;

                // 獲取數組中字典對應的模型
                NSString *type =  [idSelf arrayContainModelClass][key];

                // 生成模型
                Class classModel = NSClassFromString(type);
                NSMutableArray *arrM = [NSMutableArray array];
                // 遍歷字典數組,生成模型數組
                for (NSDictionary *dict in value) {
                    // 字典轉模型
                    id model =  [classModel modelWithDict3:dict];
                    [arrM addObject:model];
                }

                // 把模型數組賦值給value
                value = arrM;

            }
        }

        // 如果模型屬性數量大于字典鍵值對數理,模型屬性會被賦值為nil,而報錯
        if (value) {
            // 給模型中屬性賦值
            [objc setValue:value forKey:key];
        }
    }
    return objc;
}

總結:我們既然能獲取到屬性類型,那就可以攔截到模型的那個數組屬性,進而對數組中每個模型遍歷并字典轉模型,但是我們不知道數組中的模型都是什么類型,我們可以聲明一個方法,該方法目的不是讓其調用,而是讓其實現并返回模型的類型。

runtime添加方法

應用場景:如果一個類的方法非常多,加載類到內存的時候比較耗資源,需要給每個方法生成映射表,可以使用動態給某個類添加方法解決。

注解:OC中使用的懶加載,當用到的時候才去加載它,實際上只要一個類實現了某個方法,就會被加載到內存。當我們不想加載那么多方法的時候,就可以使用runtime動態的添加方法。

需求:runtime動態添加方法處理調用一個未實現的方法和去除報錯。

案例代碼:方法+調用+打印輸出

- (void)viewDidLoad {
    [super viewDidLoad];   
    Person *p = [[Person alloc] init];
    // 默認person,沒有實現run:方法,可以通過performSelector調用,但是會報錯。
    // 動態添加方法就不會報錯
    [p performSelector:@selector(run:) withObject:@10];
}

@implementation Person
// 沒有返回值,1個參數
// void,(id,SEL)
void aaa(id self, SEL _cmd, NSNumber *meter) {
    NSLog(@"跑了%@米", meter);
}

// 任何方法默認都有兩個隱式參數,self,_cmd(當前方法的方法編號)
// 什么時候調用:只要一個對象調用了一個未實現的方法就會調用這個方法,進行處理
// 作用:動態添加方法,處理未實現
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    // [NSStringFromSelector(sel) isEqualToString:@"run"];
    if (sel == NSSelectorFromString(@"run:")) {
        // 動態添加run方法
        // class: 給哪個類添加方法
        // SEL: 添加哪個方法,即添加方法的方法編號
        // IMP: 方法實現 => 函數 => 函數入口 => 函數名(添加方法的函數實現(函數地址))
        // type: 方法類型,(返回值+參數類型) v:void @:對象->self :表示SEL->_cmd
        class_addMethod(self, sel, (IMP)aaa, "v@:@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
@end

// 打印輸出
2017-02-17 19:05:03.917 runtime[12761:543574] runtime動態添加方法--跑了10米

動態變量控制

現在有一個Person的類,創建xiaoming對象

  • 動態獲取xiaoming類中的所有屬性(包括私有的)
    Ivar *ivar = class_copyIvarList([self.xiaoming class], &count);

  • 遍歷屬性找到對應的name字段
    const char *varName = ivar_getName(var);

  • 修改對應的字段值為20
    object_setIvar(self.xiaoMing, var, @"20");

  • 代碼

-(void)answer{
  unsigned int count = 0;
  Ivar *ivar = class_copyIvarList([self.xiaoMing class], &count);
  for (int i = 0; i<count; i++) {
      Ivar var = ivar[i];
      const char *varName = ivar_getName(var);
      NSString *name = [NSString stringWithUTF8String:varName];
      if ([name isEqualToString:@"_age"]) {
          object_setIvar(self.xiaoMing, var, @"20");
          break;
      }
  }
  NSLog(@"XiaoMing's age is %@",self.xiaoMing.age);
}

實現NSCoding的自動歸檔和解檔

實現自定義的模型持久化的過程,如果一個模型有許多個屬性,需要對每個屬性都實現一遍encodeObjectdecodeObjectForKey方法,當遇到這樣的模型有很多個,這是一件十分麻煩的事情,下面介紹簡單的實現方法。

假設現在有一個Movie類,有3個屬性。先看下 .h文件

// Movie.h文件
//1. 如果想要當前類可以實現歸檔與反歸檔,需要遵守一個協議NSCoding
@interface Movie : NSObject<NSCoding>  
@property (nonatomic, copy) NSString *movieId;
@property (nonatomic, copy) NSString *movieName;
@property (nonatomic, copy) NSString *pic_url;
@end

如果是正常寫法, .m 文件應該是這樣的:

// Movie.m文件
@implementation Movie
- (void)encodeWithCoder:(NSCoder *)aCoder
{
   [aCoder encodeObject:_movieId forKey:@"id"];
   [aCoder encodeObject:_movieName forKey:@"name"];
   [aCoder encodeObject:_pic_url forKey:@"url"];
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
   if (self = [super init]) {
       self.movieId = [aDecoder decodeObjectForKey:@"id"];
       self.movieName = [aDecoder decodeObjectForKey:@"name"];
       self.pic_url = [aDecoder decodeObjectForKey:@"url"];
   }
   return self;
}
@end

如果這里有100個屬性,難道我們也只能把100個屬性都給寫一遍嗎。
使用runtime讓我們有更簡便的方法

#import "Movie.h"
#import <objc/runtime.h>
@implementation Movie
- (void)encodeWithCoder:(NSCoder *)encoder
{
   unsigned int count = 0;
   Ivar *ivars = class_copyIvarList([Movie class], &count);

   for (int i = 0; i<count; i++) {
       // 取出i位置對應的成員變量
       Ivar ivar = ivars[i];
       // 查看成員變量
       const char *name = ivar_getName(ivar);
       // 歸檔
       NSString *key = [NSString stringWithUTF8String:name];
       id value = [self valueForKey:key];
       [encoder encodeObject:value forKey:key];
   }
   free(ivars);
}
- (id)initWithCoder:(NSCoder *)decoder
{
   if (self = [super init]) {
       unsigned int count = 0;
       Ivar *ivars = class_copyIvarList([Movie class], &count);
       for (int i = 0; i<count; i++) {
       // 取出i位置對應的成員變量
       Ivar ivar = ivars[i];
       // 查看成員變量
       const char *name = ivar_getName(ivar);
      // 歸檔
      NSString *key = [NSString stringWithUTF8String:name];
     id value = [decoder decodeObjectForKey:key];
      // 設置到成員變量身上
       [self setValue:value forKey:key];

       }
       free(ivars);
   } 
   return self;
}
@end

這樣的方式實現,不管有多少個屬性,寫這幾行代碼就搞定了。如果嫌代碼有點多,有更加簡便的方法:兩句代碼搞定。

#import "Movie.h"
#import <objc/runtime.h>
#define encodeRuntime(A) \
\
unsigned int count = 0;\
Ivar *ivars = class_copyIvarList([A class], &count);\
for (int i = 0; i<count; i++) {\
Ivar ivar = ivars[i];\
const char *name = ivar_getName(ivar);\
NSString *key = [NSString stringWithUTF8String:name];\
id value = [self valueForKey:key];\
[encoder encodeObject:value forKey:key];\
}\
free(ivars);\
\
#define initCoderRuntime(A) \
\
if (self = [super init]) {\
unsigned int count = 0;\
Ivar *ivars = class_copyIvarList([A class], &count);\
for (int i = 0; i<count; i++) {\
Ivar ivar = ivars[i];\
const char *name = ivar_getName(ivar);\
NSString *key = [NSString stringWithUTF8String:name];\
id value = [decoder decodeObjectForKey:key];\
[self setValue:value forKey:key];\
}\
free(ivars);\
}\
return self;\
\
@implementation Movie
- (void)encodeWithCoder:(NSCoder *)encoder
{
   encodeRuntime(Movie)
}
- (id)initWithCoder:(NSCoder *)decoder
{
   initCoderRuntime(Movie)
}
@end

優化:上面是encodeWithCoder 和 initWithCoder這兩個方法抽成宏。我們可以把這兩個宏單獨放到一個文件里面,這里以后需要進行數據持久化的模型都可以直接使用這兩個宏。

runtime下Class的各項操作

下面是 runtime 下Class的常見方法 及 帶有使用示例代碼。各項操作,【轉載原著】http://www.lxweimin.com/p/46dd81402f63

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]);
}
  • 獲取方法列表
Method *methodList = class_copyMethodList([self class], &count);
for (unsigned int i; i<count; i++) {
   Method method = methodList[i];
   NSLog(@"method---->%@", NSStringFromSelector(method_getName(method)));
}
  • 獲取成員變量列表
Ivar *ivarList = class_copyIvarList([self class], &count);
for (unsigned int i; i<count; i++) {
    Ivar myIvar = ivarList[i];
    const char *ivarName = ivar_getName(myIvar);
    NSLog(@"Ivar---->%@", [NSString stringWithUTF8String:ivarName]);
}
  • 獲得協議列表
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
for (unsigned int i; i<count; i++) {
    Protocol *myProtocal = protocolList[i];
    const char *protocolName = protocol_getName(myProtocal);
    NSLog(@"protocol---->%@", [NSString stringWithUTF8String:protocolName]);
}

現在有一個Person類,和person創建的xiaoming對象,有test1和test2兩個方法

  • 獲得類方法
Class PersonClass = object_getClass([Person class]);
SEL oriSEL = @selector(test1);
Method oriMethod = _class_getMethod(xiaomingClass, oriSEL);
  • 獲得實例方法
Class PersonClass = object_getClass([xiaoming class]);
SEL oriSEL = @selector(test2);
Method cusMethod = class_getInstanceMethod(xiaomingClass, oriSEL);
  • 添加方法
BOOL addSucc = class_addMethod(xiaomingClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
  • 替換原方法實現
class_replaceMethod(toolClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
  • 交換原方法實現
method_exchangeImplementations(oriMethod, cusMethod);

常用方法

// 得到類的所有方法
    Method *allMethods = class_copyMethodList([Person class], &count);
// 得到所有成員變量
    Ivar *allVariables = class_copyIvarList([Person class], &count);
// 得到所有屬性
    objc_property_t *properties = class_copyPropertyList([Person class], &count);
// 根據名字得到類變量的Ivar指針,但是這個在OC中好像毫無意義
Ivar oneCVIvar = class_getClassVariable([Person class], name);
// 根據名字得到實例變量的Ivar指針
    Ivar oneIVIvar = class_getInstanceVariable([Person class], name);
// 找到后可以直接對私有變量賦值
    object_setIvar(_per, oneIVIvar, @"Mike");//強制修改name屬性
/* 動態添加方法:
     第一個參數表示Class cls 類型;
     第二個參數表示待調用的方法名稱;
     第三個參數(IMP)myAddingFunction,IMP是一個函數指針,這里表示指定具體實現方法myAddingFunction;
     第四個參數表方法的參數,0代表沒有參數;
     */
    class_addMethod([_per class], @selector(sayHi), (IMP)myAddingFunction, 0);
// 交換兩個方法
    method_exchangeImplementations(method1, method2);

// 關聯兩個對象
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
/*
 id object                     :表示關聯者,是一個對象,變量名理所當然也是object
 const void *key               :獲取被關聯者的索引key
 id value                      :被關聯者,這里是一個block
 objc_AssociationPolicy policy : 關聯時采用的協議,有assign,retain,copy等協議,一般使用OBJC_ASSOCIATION_RETAIN_NONATOMIC
*/

runtime 幾個參數概念

1、objc_msgSend
這是個最基本的用于發送消息的函數
其實編譯器會根據情況在objc_msgSendobjc_msgSend_stret,objc_msgSendSuper, 或objc_msgSendSuper_stret 四個方法中選擇一個來調用。如果消息是傳遞給超類,那么會調用名字帶有Super的函數;如果消息返回值是數據結構而不是簡單值時,那么會調用名字帶有stret的函數。

2、SEL
objc_msgSend函數第二個參數類型為SEL,它是selectorObjc中的表示類型(Swift中是Selector類)。selector是方法選擇器,可以理解為區分方法的ID,而這個ID的數據結構是SEL:
typedef struct objc_selector *SEL;
其實它就是個映射到方法的C字符串,你可以用Objc編譯器命令@selector()或者Runtime系統的sel_registerName函數來獲得一個SEL類型的方法選擇器。

3、id
objc_msgSend第一個參數類型為id,大家對它都不陌生,它是一個指向類實例的指針:
typedef struct objc_object *id;
objc_object又是啥呢:
struct objc_object { Class isa; };
objc_object結構體包含一個isa指針,根據isa指針就可以順藤摸瓜找到對象所屬的類。

4、runtime.h中Class的定義

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;//每個Class都有一個isa指針

#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;//父類
    const char *name                                         OBJC2_UNAVAILABLE;//類名
    long version                                             OBJC2_UNAVAILABLE;//類版本
    long info                                                OBJC2_UNAVAILABLE;//!*!供運行期使用的一些位標識。如:CLS_CLASS (0x1L)表示該類為普通class; CLS_META(0x2L)表示該類為metaclass等(runtime.h中有詳細列出)
    long instance_size                                       OBJC2_UNAVAILABLE;//實例大小
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;//存儲每個實例變量的內存地址
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;//!*!根據info的信息確定是類還是實例,運行什么函數方法等
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;//緩存
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;//協議
#endif

} OBJC2_UNAVAILABLE;

可以看到運行時一個類還關聯了它的超類指針,類名,成員變量,方法,緩存,還有附屬的協議。
在objc_class結構體中:ivarsobjc_ivar_list指針;methodLists是指向objc_method_list指針的指針。也就是說可以動態修改*methodLists的值來添加成員方法,這也是Category實現的原理。

面試題

1、什么是 method swizzling(俗稱黑魔法)

  • 簡單說就是進行方法交換

  • 在Objective-C中調用一個方法,其實是向一個對象發送消息,查找消息的唯一依據是selector的名字。利用Objective-C的動態特性,可以實現在運行時偷換selector對應的方法實現,達到給方法掛鉤的目的

  • 每個類都有一個方法列表,存放著方法的名字和方法實現的映射關系,selector的本質其實就是方法名,IMP有點類似函數指針,指向具體的Method實現,通過selector就可以找到對應的IMP。


    1738688-2b0484c79165c45b.jpeg
  • 交換方法的幾種實現方式
    • 利用 method_exchangeImplementations 交換兩個方法的實現
    • 利用 class_replaceMethod 替換方法的實現
    • 利用 method_setImplementation 來直接設置某個方法的IMP。


      1738688-d1d965b31b69ab01.jpeg

參考:
Runtime Method Swizzling開發實例匯總(持續更新中)
OC運行時黑魔法 Method Swizzling

2、下面的代碼輸出什么?

@implementation Son : NSObject
- (id)init
{
    self = [super init];
    if (self) {
        NSLog(@"%@", NSStringFromClass([self class]));
        NSLog(@"%@", NSStringFromClass([super class]));
    }
    return self;
}
@end

答案:都輸出 Son

  • class獲取當前方法的調用者的類,superClass獲取當前方法的調用者的父類,super僅僅是一個編譯指示器,就是給編譯器看的,不是一個指針。

  • 本質:只要編譯器看到super這個標志,就會讓當前對象去調用父類方法,本質還是當前對象在調用這個題目主要是考察關于objc中對selfsuper的理解:

  • self是類的隱藏參數,指向當前調用方法的這個類的實例。而super本質是一個編譯器標示符,和self是指向的同一個消息接受者

  • 當使用self調用方法時,會從當前類的方法列表中開始找,如果沒有,就從父類中再找;

  • 而當使用super時,則從父類的方法列表中開始找。然后調用父類的這個方法

  • 調用[self class]時,會轉化成objc_msgSend函數

id objc_msgSend(id self, SEL op, ...)
- 調用 `[super class]`時,會轉化成 `objc_msgSendSuper` 函數.

id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
第一個參數是 objc_super 這樣一個結構體,其定義如下
 struct objc_super {
 __unsafe_unretained id receiver;
 __unsafe_unretained Class super_class;
 };

第一個成員是 receiver, 類似于上面的 objc_msgSend函數第一個參數self
第二個成員是記錄當前類的父類是什么,告訴程序從父類中開始找方法,找到方法后,最后內部是使用 objc_msgSend(objc_super->receiver, @selector(class))去調用, 此時已經和[self class]調用相同了,故上述輸出結果仍然返回 Son

objc Runtime 開源代碼對- (Class)class方法的實現
-(Class)class { return object_getClass(self); 
}

runtime模塊推薦閱讀文章

西木 http://www.lxweimin.com/p/6b905584f536
天口三水羊 http://www.lxweimin.com/p/9e1bc8d890f9
夜千尋墨 http://www.lxweimin.com/p/46dd81402f63
袁崢Seemygo http://www.lxweimin.com/p/e071206103a4
鄭欽洪_ http://www.lxweimin.com/p/bd24c3f3cd0a
HenryCheng http://www.lxweimin.com/p/f6300eb3ec3d

程序員的最愛(不點進去你會后悔的)

【譯文 & 源碼】
【工具類】

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容

  • Runtime的特性主要是消息(方法)傳遞,如果消息(方法)在對象中找不到,就進行轉發,具體怎么實現的呢。我們從下...
    jackyshan閱讀 138,030評論 79 765
  • 引導 對于從事 iOS 開發人員來說,所有的人都會答出「 Runtime 是運行時 」,什么情況下用 Runtim...
    Winny_園球閱讀 4,226評論 3 75
  • 對于從事 iOS 開發人員來說,所有的人都會答出【runtime 是運行時】什么情況下用runtime?大部分人能...
    夢夜繁星閱讀 3,730評論 7 64
  • 轉至元數據結尾創建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數據起始第一章:isa和Class一....
    40c0490e5268閱讀 1,751評論 0 9
  • 前言,文章是轉載的,因為之前收藏的,今天突然發現沒了。不知道什么原因,簡書搜索不到了,有幾篇同樣轉載的,但是代碼沒...
    安靜就好_閱讀 2,650評論 6 42