iOS面試中經(jīng)常問(wèn)的點(diǎn) - RunTime

一. RunTime簡(jiǎn)介

RunTime簡(jiǎn)稱運(yùn)行時(shí)。OC就是運(yùn)行時(shí)機(jī)制,也就是在運(yùn)行時(shí)候的一些機(jī)制,其中最主要的是消息機(jī)制。

對(duì)于C語(yǔ)言,函數(shù)的調(diào)用在編譯的時(shí)候會(huì)決定調(diào)用哪個(gè)函數(shù),如果調(diào)用未實(shí)現(xiàn)的函數(shù)就會(huì)報(bào)錯(cuò)。 對(duì)于OC語(yǔ)言,屬于動(dòng)態(tài)調(diào)用過(guò)程,在編譯的時(shí)候并不能決定真正調(diào)用哪個(gè)函數(shù),只有在真正運(yùn)行的時(shí)候才會(huì)根據(jù)函數(shù)的名稱找到對(duì)應(yīng)的函數(shù)來(lái)調(diào)用。在編譯階段,OC可以調(diào)用任何函數(shù),即使這個(gè)函數(shù)并未實(shí)現(xiàn),只要聲明過(guò)就不會(huì)報(bào)錯(cuò)。

二. RunTime消息機(jī)制

消息機(jī)制是運(yùn)行時(shí)里面最重要的機(jī)制,OC中任何方法的調(diào)用,本質(zhì)都是發(fā)送消息。 使用運(yùn)行時(shí),發(fā)送消息需要導(dǎo)入框架<objc/message.h>并且xcode5之后,蘋果不建議使用底層方法,如果想要使用運(yùn)行時(shí),需要關(guān)閉嚴(yán)格檢查objc_msgSend的調(diào)用,BuildSetting->搜索msg 改為NO。

下來(lái)看一下實(shí)例方法調(diào)用底層實(shí)現(xiàn)

Person *p = [[Person alloc] init];
[p eat];
// 底層會(huì)轉(zhuǎn)化成
//SEL:方法編號(hào),根據(jù)方法編號(hào)就可以找到對(duì)應(yīng)方法的實(shí)現(xiàn)。
[p performSelector:@selector(eat)];
//performSelector本質(zhì)即為運(yùn)行時(shí),發(fā)送消息,誰(shuí)做事情就調(diào)用誰(shuí) 
objc_msgSend(p, @selector(eat));
// 帶參數(shù)
objc_msgSend(p, @selector(eat:),10);

類方法的調(diào)用底層

// 本質(zhì)是會(huì)將類名轉(zhuǎn)化成類對(duì)象,初始化方法其實(shí)是在創(chuàng)建類對(duì)象。
[Person eat];
// Person只是表示一個(gè)類名,并不是一個(gè)真實(shí)的對(duì)象。只要是方法必須要對(duì)象去調(diào)用。
// RunTime 調(diào)用類方法同樣,類方法也是類對(duì)象去調(diào)用,所以需要獲取類對(duì)象,然后使用類對(duì)象去調(diào)用方法。
Class personclass = [Persion class];
[[Persion class] performSelector:@selector(eat)];
// 類對(duì)象發(fā)送消息
objc_msgSend(personclass, @selector(eat));

@selector (SEL):是一個(gè)SEL方法選擇器。SEL其主要作用是快速的通過(guò)方法名字查找到對(duì)應(yīng)方法的函數(shù)指針,然后調(diào)用其函數(shù)。SEL其本身是一個(gè)Int類型的地址,地址中存放著方法的名字。 對(duì)于一個(gè)類中。每一個(gè)方法對(duì)應(yīng)著一個(gè)SEL。所以一個(gè)類中不能存在2個(gè)名稱相同的方法,即使參數(shù)類型不同,因?yàn)镾EL是根據(jù)方法名字生成的,相同的方法名稱只能對(duì)應(yīng)一個(gè)SEL。

運(yùn)行時(shí)發(fā)送消息的底層實(shí)現(xiàn) 每一個(gè)類都有一個(gè)方法列表 Method List,保存這類里面所有的方法,根據(jù)SEL傳入的方法編號(hào)找到方法,相當(dāng)于value - key的映射。然后找到方法的實(shí)現(xiàn)。去方法的實(shí)現(xiàn)里面去實(shí)現(xiàn)。如圖所示。

那么內(nèi)部是如何動(dòng)態(tài)查找對(duì)應(yīng)的方法的? 首先我們知道所有的類中都繼承自NSObject類,在NSObjcet中存在一個(gè)Class的isa指針。

typedef struct objc_class *Class;
@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}

我們來(lái)到objc_class中查看,其中包含著類的一些基本信息。

struct objc_class {
  Class isa; // 指向metaclass

  Class super_class ; // 指向其父類
  const char *name ; // 類名
  long version ; // 類的版本信息,初始化默認(rèn)為0,可以通過(guò)runtime函數(shù)class_setVersion和class_getVersion進(jìn)行修改、讀取
  long info; // 一些標(biāo)識(shí)信息,如CLS_CLASS (0x1L) 表示該類為普通 class ,其中包含對(duì)象方法和成員變量;CLS_META (0x2L) 表示該類為 metaclass,其中包含類方法;
  long instance_size ; // 該類的實(shí)例變量大小(包括從父類繼承下來(lái)的實(shí)例變量);
  struct objc_ivar_list *ivars; // 用于存儲(chǔ)每個(gè)成員變量的地址
  struct objc_method_list **methodLists ; // 與 info 的一些標(biāo)志位有關(guān),如CLS_CLASS (0x1L),則存儲(chǔ)對(duì)象方法,如CLS_META (0x2L),則存儲(chǔ)類方法;
  struct objc_cache *cache; // 指向最近使用的方法的指針,用于提升效率;
  struct objc_protocol_list *protocols; // 存儲(chǔ)該類遵守的協(xié)議
}

下面我們就以p實(shí)例的eat方法來(lái)看看具體消息發(fā)送之后是怎么來(lái)動(dòng)態(tài)查找對(duì)應(yīng)的方法的。

  1. 實(shí)例方法[p eat];底層調(diào)用[p performSelector:@selector(eat)];方法,編譯器在將代碼轉(zhuǎn)化為objc_msgSend(p, @selector(eat));
  2. objc_msgSend函數(shù)中。首先通過(guò)pisa指針找到p對(duì)應(yīng)的class。在Class中先去cache中通過(guò)SEL查找對(duì)應(yīng)函數(shù)method,如果找到則通過(guò)method中的函數(shù)指針跳轉(zhuǎn)到對(duì)應(yīng)的函數(shù)中去執(zhí)行。
  3. cache中未找到。再去methodList中查找。若能找到,則將method加入到cache中,以方便下次查找,并通過(guò)method中的函數(shù)指針跳轉(zhuǎn)到對(duì)應(yīng)的函數(shù)中去執(zhí)行。
  4. methodlist中未找到,則去superClass中查找。若能找到,則將method加入到cache中,以方便下次查找,并通過(guò)method中的函數(shù)指針跳轉(zhuǎn)到對(duì)應(yīng)的函數(shù)中去執(zhí)行。

三. 使用RunTime交換方法:

當(dāng)系統(tǒng)自帶的方法功能不夠,需要給系統(tǒng)自帶的方法擴(kuò)展一些功能,并且保持原有的功能時(shí),可以使用RunTime交換方法實(shí)現(xiàn)。 這里要實(shí)現(xiàn)image添加圖片的時(shí)候,自動(dòng)判斷image是否為空,如果為空則提醒圖片不存在。 方法一:使用分類

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

缺點(diǎn):每次使用都需要導(dǎo)入頭文件,并且如果項(xiàng)目比較大,之前使用的方法全部需要更改。

方法二 :RunTime交換方法 交換方法的本質(zhì)其實(shí)是交換兩個(gè)方法的實(shí)現(xiàn),即調(diào)換xx_ccimageNamed和imageName方法,達(dá)到調(diào)用xx_ccimageNamed其實(shí)就是調(diào)用imageNamed方法的目的

那么首先需要明白方法在哪里交換,因?yàn)榻粨Q只需要進(jìn)行一次,所以在分類的load方法中,當(dāng)加載分類的時(shí)候交換方法即可。

 +(void)load
{
    // 獲取要交換的兩個(gè)方法
    // 獲取類方法  用Method 接受一下
    // class :獲取哪個(gè)類方法 
    // SEL :獲取方法編號(hào),根據(jù)SEL就能去對(duì)應(yīng)的類找方法。
    Method imageNameMethod = class_getClassMethod([UIImage class], @selector(imageNamed:));
    // 獲取第二個(gè)類方法
    Method xx_ccimageNameMrthod = class_getClassMethod([UIImage class], @selector(xx_ccimageNamed:));
    // 交換兩個(gè)方法的實(shí)現(xiàn) 方法一 ,方法二。
    method_exchangeImplementations(imageNameMethod, xx_ccimageNameMrthod);
    // IMP其實(shí)就是 implementation的縮寫:表示方法實(shí)現(xiàn)。
}

交換方法內(nèi)部實(shí)現(xiàn):

  1. 根據(jù)SEL方法編號(hào)在Method中找到方法,兩個(gè)方法都找到

  2. 交換方法的實(shí)現(xiàn),指針交叉指向。如圖所示:

注意:交換方法時(shí)候 xx_ccimageNamed方法中就不能再調(diào)用imageNamed方法了,因?yàn)檎{(diào)用imageNamed方法實(shí)質(zhì)上相當(dāng)于調(diào)用 xx_ccimageNamed方法,會(huì)循環(huán)引用造成死循環(huán)。

RunTime也提供了獲取對(duì)象方法和方法實(shí)現(xiàn)的方法。

// 獲取方法的實(shí)現(xiàn)
class_getMethodImplementation(<#__unsafe_unretained Class cls#>, <#SEL name#>) 
// 獲取對(duì)象方法
class_getInstanceMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>)

此時(shí),當(dāng)調(diào)用imageNamed:方法的時(shí)候就會(huì)調(diào)用xx_ccimageNamed:方法,為image添加圖片,并判斷圖片是否存在,如果不存在則提醒圖片不存在。

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

如果一個(gè)類方法非常多,其中可能許多方法暫時(shí)用不到。而加載類方法到內(nèi)存的時(shí)候需要給每個(gè)方法生成映射表,又比較耗費(fèi)資源。此時(shí)可以使用RunTime動(dòng)態(tài)添加方法

動(dòng)態(tài)給某個(gè)類添加方法,相當(dāng)于懶加載機(jī)制,類中許多方法暫時(shí)用不到,那么就先不加載,等用到的時(shí)候再去加載方法。

動(dòng)態(tài)添加方法的方法: 首先我們先不實(shí)現(xiàn)對(duì)象方法,當(dāng)調(diào)用performSelector: 方法的時(shí)候,再去動(dòng)態(tài)加載方法。 這里同上創(chuàng)建Person類,使用performSelector: 調(diào)用Person類對(duì)象的eat方法。

Person *p = [[Person alloc]init];
// 當(dāng)調(diào)用 P中沒(méi)有實(shí)現(xiàn)的方法時(shí),動(dòng)態(tài)加載方法
[p performSelector:@selector(eat)];

此時(shí)編譯的時(shí)候是不會(huì)報(bào)錯(cuò)的,程序運(yùn)行時(shí)才會(huì)報(bào)錯(cuò),因?yàn)镻erson類中并沒(méi)有實(shí)現(xiàn)eat方法,當(dāng)去類中的Method List中發(fā)現(xiàn)找不到eat方法,會(huì)報(bào)錯(cuò)找不到eat方法。

而當(dāng)找不到對(duì)應(yīng)的方法時(shí)就會(huì)來(lái)到攔截調(diào)用,在找不到調(diào)用的方法程序崩潰之前調(diào)用的方法。 當(dāng)調(diào)用了沒(méi)有實(shí)現(xiàn)的對(duì)象方法的時(shí),就會(huì)調(diào)用+(BOOL)resolveInstanceMethod:(SEL)sel方法。 當(dāng)調(diào)用了沒(méi)有實(shí)現(xiàn)的類方法的時(shí)候,就會(huì)調(diào)用+(BOOL)resolveClassMethod:(SEL)sel方法。

首先我們來(lái)到API中看一下蘋果的說(shuō)明,搜索 Dynamic Method Resolution 來(lái)到動(dòng)態(tài)方法解析。

Dynamic Method Resolution的API中已經(jīng)講解的很清晰,我們可以實(shí)現(xiàn)方法resolveInstanceMethod:或者resolveClassMethod:方法,動(dòng)態(tài)的給實(shí)例方法或者類方法添加方法和方法實(shí)現(xiàn)。

所以通過(guò)這兩個(gè)方法就可以知道哪些方法沒(méi)有實(shí)現(xiàn),從而動(dòng)態(tài)添加方法。參數(shù)sel即表示沒(méi)有實(shí)現(xiàn)的方法。

一個(gè)objective - C方法最終都是一個(gè)C函數(shù),默認(rèn)任何一個(gè)方法都有兩個(gè)參數(shù)。 self : 方法調(diào)用者 _cmd : 調(diào)用方法編號(hào)。我們可以使用函數(shù)class_addMethod為類添加一個(gè)方法以及實(shí)現(xiàn)。

這里仿照API給的例子,動(dòng)態(tài)的為P實(shí)例添加eat對(duì)象

+(BOOL)resolveInstanceMethod:(SEL)sel
{
    // 動(dòng)態(tài)添加eat方法
    // 首先判斷sel是不是eat方法 也可以轉(zhuǎn)化成字符串進(jìn)行比較。    
    if (sel == @selector(eat)) {
    /** 
     第一個(gè)參數(shù): cls:給哪個(gè)類添加方法
     第二個(gè)參數(shù): SEL name:添加方法的編號(hào)
     第三個(gè)參數(shù): IMP imp: 方法的實(shí)現(xiàn),函數(shù)入口,函數(shù)名可與方法名不同(建議與方法名相同)
     第四個(gè)參數(shù): types :方法類型,需要用特定符號(hào),參考API
     */
      class_addMethod(self, sel, (IMP)eat , "v@:");
        // 處理完返回YES
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

重點(diǎn)來(lái)看一下class_addMethod方法

class_addMethod(__unsafe_unretained Class cls, SEL name, IMP imp, const char *types)

class_addMethod中的四個(gè)參數(shù)。第一,二個(gè)參數(shù)比較好理解,重點(diǎn)是第三,四個(gè)參數(shù)。

  1. cls : 表示給哪個(gè)類添加方法,這里要給Person類添加方法,self即代表Person。
  2. SEL name : 表示添加方法的編號(hào)。因?yàn)檫@里只有一個(gè)方法需要?jiǎng)討B(tài)添加,并且之前通過(guò)判斷確定sel就是eat方法,所以這里可以使用sel。
  3. IMP imp : 表示方法的實(shí)現(xiàn),函數(shù)入口,函數(shù)名可與方法名不同(建議與方法名相同)需要自己來(lái)實(shí)現(xiàn)這個(gè)函數(shù)。每一個(gè)方法都默認(rèn)帶有兩個(gè)隱式參數(shù) self : 方法調(diào)用者 _cmd : 調(diào)用方法的標(biāo)號(hào),可以寫也可以不寫。
void eat(id self ,SEL _cmd)
{
      // 實(shí)現(xiàn)內(nèi)容
      NSLog(@"%@的%@方法動(dòng)態(tài)實(shí)現(xiàn)了",self,NSStringFromSelector(_cmd));
}
  1. types : 表示方法類型,需要用特定符號(hào)。系統(tǒng)提供的例子中使用的是"v@:",我們來(lái)到API中看看"v@:"指定的方法是什么類型的。

從圖中可以看出

v -> void 表示無(wú)返回值 @ -> object 表示id參數(shù) : -> method selector 表示SEL

至此已經(jīng)完成了P實(shí)例eat方法的動(dòng)態(tài)添加。當(dāng)P調(diào)用eat方法時(shí)輸出

動(dòng)態(tài)添加有參數(shù)的方法 如果是有參數(shù)的方法,需要對(duì)方法的實(shí)現(xiàn)和class_addMethod方法內(nèi)方法類型參數(shù)做一些修改。 方法實(shí)現(xiàn):因?yàn)樵贑語(yǔ)言函數(shù)中,所以對(duì)象參數(shù)類型只能用id代替。 方法類型參數(shù):因?yàn)樘砑恿艘粋€(gè)id參數(shù),所以方法類型應(yīng)該為"v@:@" 來(lái)看一下代碼

+(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)
{
    // 實(shí)現(xiàn)內(nèi)容
    NSLog(@"%@的%@方法動(dòng)態(tài)實(shí)現(xiàn)了,參數(shù)為%@",self,NSStringFromSelector(_cmd),Num);
}

調(diào)用eat:函數(shù)

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

輸出為

五. RunTime動(dòng)態(tài)添加屬性

使用RunTime給系統(tǒng)的類添加屬性,首先需要了解對(duì)象與屬性的關(guān)系。

對(duì)象一開(kāi)始初始化的時(shí)候其屬性name為nil,給屬性賦值其實(shí)就是讓name屬性指向一塊存儲(chǔ)字符串的內(nèi)存,使這個(gè)對(duì)象的屬性跟這塊內(nèi)存產(chǎn)生一種關(guān)聯(lián),個(gè)人理解對(duì)象的屬性就是一個(gè)指針,指向一塊內(nèi)存區(qū)域。

那么如果想動(dòng)態(tài)的添加屬性,其實(shí)就是動(dòng)態(tài)的產(chǎn)生某種關(guān)聯(lián)就好了。而想要給系統(tǒng)的類添加屬性,只能通過(guò)分類。

這里給NSObject添加name屬性,創(chuàng)建NSObject的分類 我們可以使用@property給分類添加屬性

@property(nonatomic,strong)NSString *name;

雖然在分類中可以寫@property 添加屬性,但是不會(huì)自動(dòng)生成私有屬性,也不會(huì)生成set,get方法的實(shí)現(xiàn),只會(huì)生成set,get的聲明,需要我們自己去實(shí)現(xiàn)。

方法一:我們可以通過(guò)使用靜態(tài)全局變量給分類添加屬性

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

但是這樣_name靜態(tài)全局變量與類并沒(méi)有關(guān)聯(lián),無(wú)論對(duì)象創(chuàng)建與銷毀,只要程序在運(yùn)行_name變量就存在,并不是真正意義上的屬性。

方法二:使用RunTime動(dòng)態(tài)添加屬性 RunTime提供了動(dòng)態(tài)添加屬性和獲得屬性的方法。

-(void)setName:(NSString *)name
{
    objc_setAssociatedObject(self, @"name",name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(NSString *)name
{
    return objc_getAssociatedObject(self, @"name");    
}
  1. 動(dòng)態(tài)添加屬性
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);

參數(shù)一:id object : 給哪個(gè)對(duì)象添加屬性,這里要給自己添加屬性,用self。 參數(shù)二:void * == id key : 屬性名,根據(jù)key獲取關(guān)聯(lián)對(duì)象的屬性的值,在objc_getAssociatedObject中通過(guò)次key獲得屬性的值并返回。 參數(shù)三:id value : 關(guān)聯(lián)的值,也就是set方法傳入的值給屬性去保存。 參數(shù)四:objc_AssociationPolicy policy : 策略,屬性以什么形式保存。 有以下幾種

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,  // 指定一個(gè)弱引用相關(guān)聯(lián)的對(duì)象
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // 指定相關(guān)對(duì)象的強(qiáng)引用,非原子性
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,  // 指定相關(guān)的對(duì)象被復(fù)制,非原子性
    OBJC_ASSOCIATION_RETAIN = 01401,  // 指定相關(guān)對(duì)象的強(qiáng)引用,原子性
    OBJC_ASSOCIATION_COPY = 01403     // 指定相關(guān)的對(duì)象被復(fù)制,原子性   
};
  1. 獲得屬性
objc_getAssociatedObject(id object, const void *key);

參數(shù)一:id object : 獲取哪個(gè)對(duì)象里面的關(guān)聯(lián)的屬性。 參數(shù)二:void * == id key : 什么屬性,與objc_setAssociatedObject中的key相對(duì)應(yīng),即通過(guò)key值取出value。

此時(shí)已經(jīng)成功給NSObject添加name屬性,并且NSObject對(duì)象可以通過(guò)點(diǎn)語(yǔ)法為屬性賦值。

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

六. RunTime字典轉(zhuǎn)模型

為了方便以后重用,這里通過(guò)給NSObject添加分類,聲明并實(shí)現(xiàn)使用RunTime字典轉(zhuǎn)模型的類方法。

+ (instancetype)modelWithDict:(NSDictionary *)dict

首先來(lái)看一下KVC字典轉(zhuǎn)模型和RunTime字典轉(zhuǎn)模型的區(qū)別

KVC:KVC字典轉(zhuǎn)模型實(shí)現(xiàn)原理是遍歷字典中所有Key,然后去模型中查找相對(duì)應(yīng)的屬性名,要求屬性名與Key必須一一對(duì)應(yīng),字典中所有key必須在模型中存在。 RunTime:RunTime字典轉(zhuǎn)模型實(shí)現(xiàn)原理是遍歷模型中的所有屬性名,然后去字典查找相對(duì)應(yīng)的Key,也就是以模型為準(zhǔn),模型中有哪些屬性,就去字典中找那些屬性。

RunTime字典轉(zhuǎn)模型的優(yōu)點(diǎn):當(dāng)服務(wù)器返回的數(shù)據(jù)過(guò)多,而我們只使用其中很少一部分時(shí),沒(méi)有用的屬性就沒(méi)有必要定義成屬性浪費(fèi)不必要的資源。只保存最有用的屬性即可。

RunTime字典轉(zhuǎn)模型過(guò)程 首先需要了解,屬性定義在類里面,那么類里面就有一個(gè)屬性列表,屬性列表以數(shù)組的形式存在,根據(jù)屬性列表就可以獲得類里面的所有屬性名,所以遍歷屬性列表,也就可以遍歷模型中的所有屬性名。 所以RunTime字典轉(zhuǎn)模型過(guò)程就很清晰了。

  1. 創(chuàng)建模型對(duì)象
id objc = [[self alloc] init];
  1. 使用class_copyIvarList方法拷貝成員屬性列表
unsigned int count = 0;
Ivar *ivarList = class_copyIvarList(self, &count);

參數(shù)一:__unsafe_unretained Class cls : 獲取哪個(gè)類的成員屬性列表。這里是self,因?yàn)檎l(shuí)調(diào)用分類中類方法,誰(shuí)就是self。 參數(shù)二:unsigned int *outCount : 無(wú)符號(hào)int型指針,這里創(chuàng)建unsigned int型count,&count就是他的地址,保證在方法中可以拿到count的地址為count賦值。傳出來(lái)的值為成員屬性總數(shù)。 返回值:Ivar * : 返回的是一個(gè)Ivar類型的指針 。指針默認(rèn)指向的是數(shù)組的第0個(gè)元素,指針+1會(huì)向高地址移動(dòng)一個(gè)Ivar單位的字節(jié),也就是指向第一個(gè)元素。Ivar表示成員屬性。 3. 遍歷成員屬性列表,獲得屬性列表

for (int i = 0 ; i < count; i++) {
        // 獲取成員屬性
        Ivar ivar = ivarList[i];
}
  1. 使用ivar_getName(ivar)獲得成員屬性名,因?yàn)槌蓡T屬性名返回的是C語(yǔ)言字符串,將其轉(zhuǎn)化成OC字符串
NSString *propertyName = [NSString stringWithUTF8String:ivar_getName(ivar)];

通過(guò)ivar_getTypeEncoding(ivar)也可以獲得成員屬性類型。 5. 因?yàn)楂@得的是成員屬性名,是帶_的成員屬性,所以需要將下劃線去掉,獲得屬性名,也就是字典的key。

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

至此已成功將字典轉(zhuǎn)為模型。

七. RunTime字典轉(zhuǎn)模型的二級(jí)轉(zhuǎn)換

在開(kāi)發(fā)過(guò)程中經(jīng)常用到模型嵌套,也就是模型中還有一個(gè)模型,這里嘗試用RunTime進(jìn)行模型的二級(jí)轉(zhuǎn)換,實(shí)現(xiàn)思路其實(shí)比較簡(jiǎn)單清晰。

  1. 首先獲得一級(jí)模型中的成員屬性的類型
// 成員屬性類型
NSString *propertyType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
  1. 判斷當(dāng)一級(jí)字典中的value是字典,并且一級(jí)模型中的成員屬性類型不是NSDictionary的時(shí)候才需要進(jìn)行二級(jí)轉(zhuǎn)化。 首先value是字典才進(jìn)行轉(zhuǎn)化是必須的,因?yàn)槲覀兺ǔ⒆值滢D(zhuǎn)化為模型,其次,成員屬性類型不是系統(tǒng)類,說(shuō)明成員屬性是我們自定義的類,也就是要轉(zhuǎn)化的二級(jí)模型。而當(dāng)成員屬性類型就是NSDictionary的話就表明,我們本就想讓成員屬性是一個(gè)字典,不需要進(jìn)行模型的轉(zhuǎn)換。
id value = dict[key];
if ([value isKindOfClass:[NSDictionary class]] && ![propertyType containsString:@"NS"]) 
{ 
      // 進(jìn)行二級(jí)轉(zhuǎn)換。
}
  1. 獲取要轉(zhuǎn)換的模型類型,這里需要對(duì)propertyType成員屬性類型做一些處理,因?yàn)閜ropertyType返回給我們成員屬性類型的是@\"Mode\",我們需要對(duì)他進(jìn)行截取為Mode。這里需要注意的是\只是轉(zhuǎn)義符,不占位。
// @\"Mode\"去掉前面的@\"
NSRange range = [propertyType rangeOfString:@"\""];
propertyType = [propertyType substringFromIndex:range.location + range.length];
// Mode\"去掉后面的\"
range = [propertyType rangeOfString:@"\""];
propertyType = [propertyType substringToIndex:range.location];
  1. 獲取需要轉(zhuǎn)換類的類對(duì)象,將字符串轉(zhuǎn)化為類名。
Class modelClass =  NSClassFromString(propertyType);
  1. 判斷如果類名不為空則調(diào)用分類的modelWithDict方法,傳value字典,進(jìn)行二級(jí)模型轉(zhuǎn)換,返回二級(jí)模型在賦值給value。
if (modelClass) {
      value =  [modelClass modelWithDict:value];
}  

這里可能有些繞,重新理一下,我們通過(guò)判斷value是字典并且需要進(jìn)行二級(jí)轉(zhuǎn)換,然后將value字典轉(zhuǎn)化為模型返回,并重新賦值給value,最后給一級(jí)模型中相對(duì)應(yīng)的key賦值模型value即可完成二級(jí)字典對(duì)模型的轉(zhuǎn)換。

最后附上二級(jí)轉(zhuǎn)換的完整方法

+ (instancetype)modelWithDict:(NSDictionary *)dict{
    // 1.創(chuàng)建對(duì)應(yīng)類的對(duì)象
    id objc = [[self alloc] init];
    // count:成員屬性總數(shù)
    unsigned int count = 0;
   // 獲得成員屬性列表和成員屬性數(shù)量
    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)];
        // 二級(jí)轉(zhuǎn)換
        // value值是字典并且成員屬性的類型不是字典,才需要轉(zhuǎn)換成模型
        if ([value isKindOfClass:[NSDictionary class]] && ![propertyType containsString:@"NS"]) {
            // 進(jìn)行二級(jí)轉(zhuǎn)換
            // 獲取二級(jí)模型類型進(jìn)行字符串截取,轉(zhuǎn)換為類名
            NSRange range = [propertyType rangeOfString:@"\""];
            propertyType = [propertyType substringFromIndex:range.location + range.length];
            range = [propertyType rangeOfString:@"\""];
            propertyType = [propertyType substringToIndex:range.location];
            // 獲取需要轉(zhuǎn)換類的類對(duì)象
           Class modelClass =  NSClassFromString(propertyType);
           // 如果類名不為空則進(jìn)行二級(jí)轉(zhuǎn)換
            if (modelClass) {
                // 返回二級(jí)模型賦值給value
                value =  [modelClass modelWithDict:value];
            }
        }
        if (value) {
            // KVC賦值:不能傳空
            [objc setValue:value forKey:key];
        }
    }
    // 返回模型
    return objc;
}

以上只是對(duì)RunTime淺顯的理解,足以應(yīng)付iOS面試過(guò)程中Runtime的一些問(wèn)題。

?著作權(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)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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