iOS面試中常常問的點 - RunTime

一. RunTime簡介

我將iOS的一些學習視頻書籍資料總結在「碼農Style」公衆號裏,須要的小夥伴能夠自行獲取面試

想要一塊兒探討學習iOS底層原理,架構的能夠加我Q_2336684744歡迎一塊兒學習交流數組

RunTime簡稱運行時。OC就是運行時機制,也就是在運行時候的一些機制,其中最主要的是消息機制。xcode

對於C語言,函數的調用在編譯的時候會決定調用哪一個函數,若是調用未實現的函數就會報錯。 對於OC語言,屬於動態調用過程,在編譯的時候並不能決定真正調用哪一個函數,只有在真正運行的時候纔會根據函數的名稱找到對應的函數來調用。在編譯階段,OC能夠調用任何函數,即便這個函數並未實現,只要聲明過就不會報錯。bash

二. RunTime消息機制

消息機制是運行時裏面最重要的機制,OC中任何方法的調用,本質都是發送消息。 使用運行時,發送消息須要導入框架<objc/message.h>而且xcode5以後,蘋果不建議使用底層方法,若是想要使用運行時,須要關閉嚴格檢查objc_msgSend的調用,BuildSetting->搜索msg 改成NO。服務器

下來看一下實例方法調用底層實現架構

Person *p = [[Person alloc] init];
[p eat];
// 底層會轉化成
//SEL:方法編號,根據方法編號就能夠找到對應方法的實現。
[p performSelector:@selector(eat)];
//performSelector本質即爲運行時,發送消息,誰作事情就調用誰 
objc_msgSend(p, @selector(eat));
// 帶參數
objc_msgSend(p, @selector(eat:),10);
複製代碼

類方法的調用底層框架

// 本質是會將類名轉化成類對象,初始化方法實際上是在建立類對象。
[Person eat];
// Person只是表示一個類名,並非一個真實的對象。只要是方法必需要對象去調用。
// RunTime 調用類方法一樣,類方法也是類對象去調用,因此須要獲取類對象,而後使用類對象去調用方法。
Class personclass = [Persion class];
[[Persion class] performSelector:@selector(eat)];
// 類對象發送消息
objc_msgSend(personclass, @selector(eat));
複製代碼

**@selector (SEL):是一個SEL方法選擇器。**SEL其主要做用是快速的經過方法名字查找到對應方法的函數指針,而後調用其函數。SEL其自己是一個Int類型的地址,地址中存放着方法的名字。 對於一個類中。每個方法對應着一個SEL。因此一個類中不能存在2個名稱相同的方法,即便參數類型不一樣,由於SEL是根據方法名字生成的,相同的方法名稱只能對應一個SEL。函數

運行時發送消息的底層實現 每個類都有一個方法列表 Method List,保存這類裏面全部的方法,根據SEL傳入的方法編號找到方法,至關於value - key的映射。而後找到方法的實現。去方法的實現裏面去實現。如圖所示。 學習

運行時發送消息的底層實現

那麼內部是如何動態查找對應的方法的? 首先咱們知道全部的類中都繼承自NSObject類,在NSObjcet中存在一個Class的isa指針。ui

typedef struct objc_class *Class;
@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}
複製代碼

咱們來到objc_class中查看,其中包含着類的一些基本信息。

struct objc_class {
  Class isa; // 指向metaclass
  
  Class super_class ; // 指向其父類
  const char *name ; // 類名
  long version ; // 類的版本信息,初始化默認爲0,能夠經過runtime函數class_setVersion和class_getVersion進行修改、讀取
  long info; // 一些標識信息,如CLS_CLASS (0x1L) 表示該類爲普通 class ,其中包含對象方法和成員變量;CLS_META (0x2L) 表示該類爲 metaclass,其中包含類方法;
  long instance_size ; // 該類的實例變量大小(包括從父類繼承下來的實例變量);
  struct objc_ivar_list *ivars; // 用於存儲每一個成員變量的地址
  struct objc_method_list **methodLists ; // 與 info 的一些標誌位有關,如CLS_CLASS (0x1L),則存儲對象方法,如CLS_META (0x2L),則存儲類方法;
  struct objc_cache *cache; // 指向最近使用的方法的指針,用於提高效率;
  struct objc_protocol_list *protocols; // 存儲該類遵照的協議
}
複製代碼

下面咱們就以p實例的eat方法來看看具體消息發送以後是怎麼來動態查找對應的方法的。

  1. 實例方法[p eat];底層調用[p performSelector:@selector(eat)];方法,編譯器在將代碼轉化爲objc_msgSend(p, @selector(eat));
  2. objc_msgSend函數中。首先經過pisa指針找到p對應的class。在Class中先去cache中經過SEL查找對應函數method,若是找到則經過method中的函數指針跳轉到對應的函數中去執行。
  3. cache中未找到。再去methodList中查找。若能找到,則將method加入到cache中,以方便下次查找,並經過method中的函數指針跳轉到對應的函數中去執行。
  4. methodlist中未找到,則去superClass中查找。若能找到,則將method加入到cache中,以方便下次查找,並經過method中的函數指針跳轉到對應的函數中去執行。

三. 使用RunTime交換方法:

當系統自帶的方法功能不夠,須要給系統自帶的方法擴展一些功能,而且保持原有的功能時,可使用RunTime交換方法實現。 這裏要實現image添加圖片的時候,自動判斷image是否爲空,若是爲空則提醒圖片不存在。 方法一:使用分類

+ (nullable UIImage *)xx_ccimageNamed:(NSString *)name
{
    // 加載圖片    若是圖片不存在則提醒或發出異常
   UIImage *image = [UIImage imageNamed:name];
    if (image == nil) {
        NSLog(@"圖片不存在");
    }
    return image;
}
複製代碼

缺點:每次使用都須要導入頭文件,而且若是項目比較大,以前使用的方法所有須要更改。

方法二 :RunTime交換方法 交換方法的本質實際上是交換兩個方法的實現,即調換xx_ccimageNamed和imageName方法,達到調用xx_ccimageNamed其實就是調用imageNamed方法的目的

那麼首先須要明白方法在哪裏交換,由於交換隻須要進行一次,因此在分類的load方法中,當加載分類的時候交換方法便可。

+(void)load
{
    // 獲取要交換的兩個方法
    // 獲取類方法  用Method 接受一下
    // class :獲取哪一個類方法 
    // SEL :獲取方法編號,根據SEL就能去對應的類找方法。
    Method imageNameMethod = class_getClassMethod([UIImage class], @selector(imageNamed:));
    // 獲取第二個類方法
    Method xx_ccimageNameMrthod = class_getClassMethod([UIImage class], @selector(xx_ccimageNamed:));
    // 交換兩個方法的實現 方法一 ,方法二。
    method_exchangeImplementations(imageNameMethod, xx_ccimageNameMrthod);
    // IMP其實就是 implementation的縮寫:表示方法實現。
}
複製代碼

交換方法內部實現:

  1. 根據SEL方法編號在Method中找到方法,兩個方法都找到
  2. 交換方法的實現,指針交叉指向。如圖所示:
    交換方法內部實現

注意:交換方法時候 xx_ccimageNamed方法中就不能再調用imageNamed方法了,由於調用imageNamed方法實質上至關於調用 xx_ccimageNamed方法,會循環引用形成死循環。

RunTime也提供了獲取對象方法和方法實現的方法。

// 獲取方法的實現
class_getMethodImplementation(<#__unsafe_unretained Class cls#>, <#SEL name#>) 
// 獲取對象方法
class_getInstanceMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>)
複製代碼

此時,當調用imageNamed:方法的時候就會調用xx_ccimageNamed:方法,爲image添加圖片,並判斷圖片是否存在,若是不存在則提醒圖片不存在。

四. 動態添加方法

若是一個類方法很是多,其中可能許多方法暫時用不到。而加載類方法到內存的時候須要給每一個方法生成映射表,又比較耗費資源。此時可使用RunTime動態添加方法

動態給某個類添加方法,至關於懶加載機制,類中許多方法暫時用不到,那麼就先不加載,等用到的時候再去加載方法。

動態添加方法的方法: 首先咱們先不實現對象方法,當調用performSelector: 方法的時候,再去動態加載方法。 這裏同上建立Person類,使用performSelector: 調用Person類對象的eat方法。

Person *p = [[Person alloc]init];
// 當調用 P中沒有實現的方法時,動態加載方法
[p performSelector:@selector(eat)];
複製代碼

此時編譯的時候是不會報錯的,程序運行時纔會報錯,由於Person類中並無實現eat方法,當去類中的Method List中發現找不到eat方法,會報錯找不到eat方法。

報錯信息:未被選擇器發送到實例

而當找不到對應的方法時就會來到攔截調用,在找不到調用的方法程序崩潰以前調用的方法。 當調用了沒有實現的對象方法的時,就會調用**+(BOOL)resolveInstanceMethod:(SEL)sel方法。 當調用了沒有實現的類方法的時候,就會調用+(BOOL)resolveClassMethod:(SEL)sel**方法。

首先咱們來到API中看一下蘋果的說明,搜索 Dynamic Method Resolution 來到動態方法解析。

Dynamic Method Resolution

Dynamic Method Resolution的API中已經講解的很清晰,咱們能夠實現方法resolveInstanceMethod:或者resolveClassMethod:方法,動態的給實例方法或者類方法添加方法和方法實現。

因此經過這兩個方法就能夠知道哪些方法沒有實現,從而動態添加方法。參數sel即表示沒有實現的方法。

一個objective - C方法最終都是一個C函數,默認任何一個方法都有兩個參數。 self : 方法調用者 _cmd : 調用方法編號。咱們可使用函數class_addMethod爲類添加一個方法以及實現。

這裏仿照API給的例子,動態的爲P實例添加eat對象

+(BOOL)resolveInstanceMethod:(SEL)sel
{
    // 動態添加eat方法
    // 首先判斷sel是否是eat方法 也能夠轉化成字符串進行比較。    
    if (sel == @selector(eat)) {
    /** 
     第一個參數: cls:給哪一個類添加方法
     第二個參數: SEL name:添加方法的編號
     第三個參數: IMP imp: 方法的實現,函數入口,函數名可與方法名不一樣(建議與方法名相同)
     第四個參數: types :方法類型,須要用特定符號,參考API
     */
      class_addMethod(self, sel, (IMP)eat , "v@:");
        // 處理完返回YES
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
複製代碼

重點來看一下class_addMethod方法

class_addMethod(__unsafe_unretained Class cls, SEL name, IMP imp, const char *types)
複製代碼

class_addMethod中的四個參數。第一,二個參數比較好理解,重點是第三,四個參數。

  1. cls : 表示給哪一個類添加方法,這裏要給Person類添加方法,self即表明Person。
  2. SEL name : 表示添加方法的編號。由於這裏只有一個方法須要動態添加,而且以前經過判斷肯定sel就是eat方法,因此這裏可使用sel。
  3. IMP imp : 表示方法的實現,函數入口,函數名可與方法名不一樣(建議與方法名相同)須要本身來實現這個函數。每個方法都默認帶有兩個隱式參數 self : 方法調用者 _cmd : 調用方法的標號,能夠寫也能夠不寫。
void eat(id self ,SEL _cmd)
{
      // 實現內容
      NSLog(@"%@的%@方法動態實現了",self,NSStringFromSelector(_cmd));
}
複製代碼
  1. types : 表示方法類型,須要用特定符號。系統提供的例子中使用的是**"v@:",咱們來到API中看看"v@:"**指定的方法是什麼類型的。
    Objective-C type encodings
    從圖中能夠看出

v -> void 表示無返回值 @ -> object 表示id參數 : -> method selector 表示SEL

至此已經完成了P實例eat方法的動態添加。當P調用eat方法時輸出

p調用eat方法時輸出

動態添加有參數的方法 若是是有參數的方法,須要對方法的實現和class_addMethod方法內方法類型參數作一些修改。 方法實現:由於在C語言函數中,因此對象參數類型只能用id代替。 方法類型參數:由於添加了一個id參數,因此方法類型應該爲**"v@:@"** 來看一下代碼

+(BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(eat:)) {
        class_addMethod(self, sel, (IMP)aaaa , "v@:@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
void aaaa(id self ,SEL _cmd,id Num)
{
    // 實現內容
    NSLog(@"%@的%@方法動態實現了,參數爲%@",self,NSStringFromSelector(_cmd),Num);
}
複製代碼

調用eat:函數

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

輸出爲

p調用eat:方法時輸出

五. RunTime動態添加屬性

使用RunTime給系統的類添加屬性,首先須要瞭解對象與屬性的關係。

對象與屬性的關係

對象一開始初始化的時候其屬性name爲nil,給屬性賦值其實就是讓name屬性指向一塊存儲字符串的內存,使這個對象的屬性跟這塊內存產生一種關聯,我的理解對象的屬性就是一個指針,指向一塊內存區域。

那麼若是想動態的添加屬性,其實就是動態的產生某種關聯就行了。而想要給系統的類添加屬性,只能經過分類。

這裏給NSObject添加name屬性,建立NSObject的分類 咱們可使用@property給分類添加屬性

@property(nonatomic,strong)NSString *name;
複製代碼

雖然在分類中能夠寫@property 添加屬性,可是不會自動生成私有屬性,也不會生成set,get方法的實現,只會生成set,get的聲明,須要咱們本身去實現。

方法一:咱們能夠經過使用靜態全局變量給分類添加屬性

static NSString *_name;
-(void)setName:(NSString *)name
{
    _name = name;
}
-(NSString *)name
{
    return _name;
}
複製代碼

可是這樣_name靜態全局變量與類並無關聯,不管對象建立與銷燬,只要程序在運行_name變量就存在,並非真正意義上的屬性。

方法二:使用RunTime動態添加屬性 RunTime提供了動態添加屬性和得到屬性的方法。

-(void)setName:(NSString *)name
{
    objc_setAssociatedObject(self, @"name",name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(NSString *)name
{
    return objc_getAssociatedObject(self, @"name");    
}
複製代碼
  1. 動態添加屬性
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
複製代碼

參數一:id object : 給哪一個對象添加屬性,這裏要給本身添加屬性,用self。 參數二:void * == id key : 屬性名,根據key獲取關聯對象的屬性的值,在**objc_getAssociatedObject中經過次key得到屬性的值並返回。 參數三:id value** : 關聯的值,也就是set方法傳入的值給屬性去保存。 參數四:objc_AssociationPolicy policy : 策略,屬性以什麼形式保存。 有如下幾種

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,  // 指定一個弱引用相關聯的對象
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // 指定相關對象的強引用,非原子性
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,  // 指定相關的對象被複制,非原子性
    OBJC_ASSOCIATION_RETAIN = 01401,  // 指定相關對象的強引用,原子性
    OBJC_ASSOCIATION_COPY = 01403     // 指定相關的對象被複制,原子性   
};
複製代碼
  1. 得到屬性
objc_getAssociatedObject(id object, const void *key);
複製代碼

參數一:id object : 獲取哪一個對象裏面的關聯的屬性。 參數二:void * == id key : 什麼屬性,與**objc_setAssociatedObject**中的key相對應,即經過key值取出value。

此時已經成功給NSObject添加name屬性,而且NSObject對象能夠經過點語法爲屬性賦值。

NSObject *objc = [[NSObject alloc]init];
objc.name = @"xx_cc";
NSLog(@"%@",objc.name);
複製代碼

六. RunTime字典轉模型

爲了方便之後重用,這裏經過給NSObject添加分類,聲明並實現使用RunTime字典轉模型的類方法。

+ (instancetype)modelWithDict:(NSDictionary *)dict
複製代碼

首先來看一下KVC字典轉模型和RunTime字典轉模型的區別

KVC:KVC字典轉模型實現原理是遍歷字典中全部Key,而後去模型中查找相對應的屬性名,要求屬性名與Key必須一一對應,字典中全部key必須在模型中存在。 RunTime:RunTime字典轉模型實現原理是遍歷模型中的全部屬性名,而後去字典查找相對應的Key,也就是以模型爲準,模型中有哪些屬性,就去字典中找那些屬性。

RunTime字典轉模型的優勢:當服務器返回的數據過多,而咱們只使用其中不多一部分時,沒有用的屬性就沒有必要定義成屬性浪費沒必要要的資源。只保存最有用的屬性便可。

RunTime字典轉模型過程 首先須要瞭解,屬性定義在類裏面,那麼類裏面就有一個屬性列表,屬性列表以數組的形式存在,根據屬性列表就能夠得到類裏面的全部屬性名,因此遍歷屬性列表,也就能夠遍歷模型中的全部屬性名。 因此RunTime字典轉模型過程就很清晰了。

  1. 建立模型對象
id objc = [[self alloc] init];
複製代碼
  1. 使用**class_copyIvarList**方法拷貝成員屬性列表
unsigned int count = 0;
Ivar *ivarList = class_copyIvarList(self, &count);
複製代碼

參數一:__unsafe_unretained Class cls : 獲取哪一個類的成員屬性列表。這裏是self,由於誰調用分類中類方法,誰就是self。 參數二:unsigned int *outCount : 無符號int型指針,這裏建立unsigned int型count,&count就是他的地址,保證在方法中能夠拿到count的地址爲count賦值。傳出來的值爲成員屬性總數。 返回值:Ivar * : 返回的是一個Ivar類型的指針 。指針默認指向的是數組的第0個元素,指針+1會向高地址移動一個Ivar單位的字節,也就是指向第一個元素。Ivar表示成員屬性。 3. 遍歷成員屬性列表,得到屬性列表

for (int i = 0 ; i < count; i++) {
        // 獲取成員屬性
        Ivar ivar = ivarList[i];
}
複製代碼
  1. 使用**ivar_getName(ivar)**得到成員屬性名,由於成員屬性名返回的是C語言字符串,將其轉化成OC字符串
NSString *propertyName = [NSString stringWithUTF8String:ivar_getName(ivar)];
複製代碼

經過**ivar_getTypeEncoding(ivar)**也能夠得到成員屬性類型。 5. 由於得到的是成員屬性名,是帶_的成員屬性,因此須要將下劃線去掉,得到屬性名,也就是字典的key。

// 獲取key
NSString *key = [propertyName substringFromIndex:1];
複製代碼
  1. 獲取字典中key對應的Value。
// 獲取字典的value
id value = dict[key];
複製代碼
  1. 給模型屬性賦值,並將模型返回
if (value) {
 // KVC賦值:不能傳空
[objc setValue:value forKey:key];
}
return objc;
複製代碼

至此已成功將字典轉爲模型。

七. RunTime字典轉模型的二級轉換

在開發過程當中常常用到模型嵌套,也就是模型中還有一個模型,這裏嘗試用RunTime進行模型的二級轉換,實現思路其實比較簡單清晰。

  1. 首先得到一級模型中的成員屬性的類型
// 成員屬性類型
NSString *propertyType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
複製代碼
  1. 判斷當一級字典中的value是字典,而且一級模型中的成員屬性類型不是NSDictionary的時候才須要進行二級轉化。 首先value是字典才進行轉化是必須的,由於咱們一般將字典轉化爲模型,其次,成員屬性類型不是系統類,說明成員屬性是咱們自定義的類,也就是要轉化的二級模型。而當成員屬性類型就是NSDictionary的話就代表,咱們本就想讓成員屬性是一個字典,不須要進行模型的轉換。
id value = dict[key];
if ([value isKindOfClass:[NSDictionary class]] && ![propertyType containsString:@"NS"]) 
{ 
      // 進行二級轉換。
}
複製代碼
  1. 獲取要轉換的模型類型,這裏須要對propertyType成員屬性類型作一些處理,由於propertyType返回給咱們成員屬性類型的是**@\"Mode\",咱們須要對他進行截取爲Mode**。這裏須要注意的是\只是轉義符,不佔位。
// @\"Mode\"去掉前面的@\" NSRange range = [propertyType rangeOfString:@"\""];
propertyType = [propertyType substringFromIndex:range.location + range.length];
// Mode\"去掉後面的\" range = [propertyType rangeOfString:@"\""];
propertyType = [propertyType substringToIndex:range.location];
複製代碼
  1. 獲取須要轉換類的類對象,將字符串轉化爲類名。
Class modelClass =  NSClassFromString(propertyType);
複製代碼
  1. 判斷若是類名不爲空則調用分類的modelWithDict方法,傳value字典,進行二級模型轉換,返回二級模型在賦值給value。
if (modelClass) {
      value =  [modelClass modelWithDict:value];
}  
複製代碼

這裏可能有些繞,從新理一下,咱們經過判斷value是字典而且須要進行二級轉換,而後將value字典轉化爲模型返回,並從新賦值給value,最後給一級模型中相對應的key賦值模型value便可完成二級字典對模型的轉換。

最後附上二級轉換的完整方法

+ (instancetype)modelWithDict:(NSDictionary *)dict{
    // 1.建立對應類的對象
    id objc = [[self alloc] init];
    // count:成員屬性總數
    unsigned int count = 0;
   // 得到成員屬性列表和成員屬性數量
    Ivar *ivarList = class_copyIvarList(self, &count);
    for (int i = 0 ; i < count; i++) {
        // 獲取成員屬性
        Ivar ivar = ivarList[i];
        // 獲取成員名
       NSString *propertyName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        // 獲取key
        NSString *key = [propertyName substringFromIndex:1];
        // 獲取字典的value key:屬性名 value:字典的值
        id value = dict[key];
        // 獲取成員屬性類型
        NSString *propertyType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
        // 二級轉換
        // value值是字典而且成員屬性的類型不是字典,才須要轉換成模型
        if ([value isKindOfClass:[NSDictionary class]] && ![propertyType containsString:@"NS"]) {
            // 進行二級轉換
            // 獲取二級模型類型進行字符串截取,轉換爲類名
            NSRange range = [propertyType rangeOfString:@"\""];
            propertyType = [propertyType substringFromIndex:range.location + range.length];
            range = [propertyType rangeOfString:@"\""];
            propertyType = [propertyType substringToIndex:range.location];
            // 獲取須要轉換類的類對象
           Class modelClass =  NSClassFromString(propertyType);
           // 若是類名不爲空則進行二級轉換
            if (modelClass) {
                // 返回二級模型賦值給value
                value =  [modelClass modelWithDict:value];
            }
        }
        if (value) {
            // KVC賦值:不能傳空
            [objc setValue:value forKey:key];
        }
    }
    // 返回模型
    return objc;
}
複製代碼

以上只是對RunTime淺顯的理解,足以應付iOS面試過程當中Runtime的一些問題。


我將iOS的一些學習視頻書籍資料總結在「碼農Style」公衆號裏,須要的小夥伴能夠自行獲取。

掃碼關注
相關文章
相關標籤/搜索