Objective-C,不用也要知道的runtime知識(shí)(簡單易懂)

寫在前面,對runtime的學(xué)習(xí),已經(jīng)悄悄地進(jìn)行了很長時(shí)間,知識(shí)結(jié)構(gòu),也由之前的模模糊糊,變得越來越清晰,網(wǎng)上已經(jīng)有很多關(guān)于runtime的學(xué)習(xí)研究篇章了,但是,別人的東西,不是我的東西(包括我的東西,也不是你的東西,如果你看了我的文章,對你有一點(diǎn)啟發(fā)就好,別的不多求),每個(gè)人都會(huì)自己的學(xué)習(xí)記憶體系,把知識(shí)轉(zhuǎn)變?yōu)樽约菏煜さ奶茁?那,在日后回看的時(shí)候,也會(huì)覺得,還是熟悉的味道,上手,也會(huì)極快滴.

1.runtime是什么?(俗一點(diǎn)的說法:說說你對runtime的理解.)

runtime,國內(nèi)叫運(yùn)行時(shí),是一套底層的C語言API,objective-c代碼,底層都是基于它來實(shí)現(xiàn)的.

感受一下發(fā)送消息的代碼:

#import "ViewController.h"
#import "Person.h"http://項(xiàng)目中創(chuàng)建好的一個(gè)Person類,它有一個(gè)working方法
#import <objc/message.h>
- (void)viewDidLoad {
    [super viewDidLoad];
    //Person *p = [Person alloc];
    //查找build setting -> 搜索msg->設(shè)置為NO,才會(huì)有msg代碼提醒
    Person *p = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));
    //p = [p init];
    p = objc_msgSend(p, sel_registerName("init"));
    //[p working];
    objc_msgSend(p, @selector(working));
    //只有對象才能發(fā)消息,所以發(fā)消息的前綴用obj
}

2.運(yùn)行時(shí)和編譯時(shí)特性的對比.

(知識(shí)儲(chǔ)備)源碼的解釋流程:預(yù)編譯->編譯->鏈接->運(yùn)行
編譯:C語言在編譯階段就要切確知道被調(diào)用函數(shù)的真實(shí)類型了,如果在編譯時(shí)不能確定真實(shí)類型,則會(huì)報(bào)錯(cuò).
運(yùn)行時(shí):OC在調(diào)用方法/定義類/定義成員變量時(shí),在編譯時(shí)還是不能知道他們的真實(shí)類型.OC把這一切行為推遲到運(yùn)行時(shí).也就是意味著,有類型不匹配的情況,在運(yùn)行時(shí)才會(huì)拋出異常.OC中調(diào)用方法,也叫消息發(fā)送.注意區(qū)別C語言中的函數(shù)調(diào)用.
注意:編碼時(shí),報(bào)錯(cuò)信息盡量在編譯階段就調(diào)試出來.

3.runtime的相關(guān)術(shù)語

SEL :方法選擇器,他對應(yīng)方法的名字,OC中方法的名字包括冒號.
id :指向某個(gè)類的實(shí)例的指針.
Class :指向 objc_class 結(jié)構(gòu)體的指針.
Method :代表類中的某個(gè)方法,它存儲(chǔ)了方法名(SEL),方法類型(參數(shù)類型和返回值類型),方法實(shí)現(xiàn)(IMP).
IMP :指向函數(shù)的指針,代表了方法的實(shí)現(xiàn).
Cache : 專門用來緩存方法的實(shí)現(xiàn)的(IMP).
Property :屬性

4.OC中的隱形參數(shù)

開發(fā)中,我們經(jīng)常會(huì)用到一個(gè)全局self,但是你沒有想過他是怎么來的呢?其實(shí),這個(gè)self,是每個(gè)方法中都帶有的隱形參數(shù),跟他一起的,還有一個(gè)_cmd參數(shù),他是SEL類型的變量,這兩個(gè)參數(shù),都是蘋果在運(yùn)行時(shí),悄悄咪咪地添加進(jìn)去的.

有關(guān)這兩個(gè)隱形參數(shù):
①觀察這兩個(gè)表達(dá)式typedef id (*IPM) (id, SEL,...)objc_msgSend(id, SEL,...),我們可以確定,一組idSEL參數(shù),就能確定唯一的方法實(shí)現(xiàn)地址,相反,一個(gè)確定的方法,也只有唯一的一組idSEL 參數(shù).讀到這里,你心中的這個(gè)謎團(tuán),有沒有解開了呢?那就是:OC的一個(gè)類中,不能有同名的方法.
②開發(fā)中,除了上面點(diǎn)出的兩個(gè)變量之外,你還用過哪些變量感覺"情不知何起,而一往情深的"? 看代碼,說出你心中的執(zhí)行結(jié)果.

@implementation Dog : Animal
- (instancetype)init{
    if (self = [super init]) {
        NSLog(@"%@", NSStringFromClass([self class]));
        NSLog(@"%@", NSStringFromClass([super class]));
    }
    return self;
}
//打印結(jié)果
NSLog(@"%@", NSStringFromClass([self class])); =>Dog
NSLog(@"%@", NSStringFromClass([super class])); =>Dog

很多人會(huì)想當(dāng)然的認(rèn)為“ superself類似,應(yīng)該是指向父類的指針吧!”。這是很普遍的一個(gè)誤區(qū)。其實(shí)super 是一個(gè) Magic Key Word, 它本質(zhì)是一個(gè)編譯器標(biāo)示符,和 self是指向的同一個(gè)消息接受者!他們兩個(gè)的不同點(diǎn)在于:super 會(huì)告訴編譯器,調(diào)用class這個(gè)方法時(shí),要去父類的方法,而不是本類里的。

super使用的理性解釋:當(dāng)super接到消息時(shí),編譯器會(huì)添加一個(gè)objc_super 結(jié)構(gòu)體:

struct objc_super {
id receiver; //receiver仍然是self本身
Class class;//Class是指向objc_class結(jié)構(gòu)體的指針,結(jié)構(gòu)體內(nèi)有指向父類的指針的成員變量,所以規(guī)定了`super`直接找父類方法
}

編譯器將指向selfid指針和classSEL傳遞給objc_msgSendSuper函數(shù)(參數(shù)又進(jìn)行了一次傳遞,內(nèi)容沒有變哦),而class方法只有在NSObject才能找到,OC底層將class方法轉(zhuǎn)為object_getClass(),緊接著又會(huì)被編譯器將代碼轉(zhuǎn)為objc_msgSend(objc_super->receiver,@selector(class)),因?yàn)?一開始就是self去調(diào)用class方法,打印出來的,也就是Dog,而不是Animal.讀到這,如果不是很理解,那你多讀幾遍,畢竟文字嘛,理解起來是沒那么形象.實(shí)在理解不了,就記住那段"感性的super"吧,開發(fā)中,夠用的了.

5.消息發(fā)送

本來想偷偷懶,用語言描述消息發(fā)送的步驟的,但是,寫著寫著,自己都繞暈了,so,a picture speaks all.


消息發(fā)送步驟.png

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

當(dāng)一個(gè)消息被發(fā)送出來后,會(huì)經(jīng)過一系列的查找,其中有一個(gè)步驟就是判斷有沒有動(dòng)態(tài)添加方法.那么runtime提供什么樣的接口給外界進(jìn)行動(dòng)態(tài)添加方法呢?

+ (BOOL)resolveInstanceMethod:(SEL)se//動(dòng)態(tài)對象方法
+ (BOOL)resolveClassMethod:(SEL)sel;//動(dòng)態(tài)類方法
通過重寫上面方法,調(diào)用class_addMethod();函數(shù)來動(dòng)態(tài)添加方法,同時(shí)返回YES即可,如果返回NO,則會(huì)進(jìn)入消息轉(zhuǎn)發(fā)機(jī)制.
eg:

#import "ViewController.h"
#import <objc/runtime.h>
@implementation ViewController
//沒有返回值沒參數(shù)類型的函數(shù)
void dynamicMethIMP (id self, SEL _cmd){
    NSLog(@"我是動(dòng)態(tài)添加的方法實(shí)現(xiàn)");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self performSelector:@selector(resolveThisMethodDynamically)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(resolveThisMethodDynamically)) {        
        /**
         @param sel __unsafe_unretained Class cls 類的類型(需要添加方法的類)
         @param SEL name 方法選擇器(方法的名稱)
         @param IMP  方法的實(shí)現(xiàn)
         @param const char *types 函數(shù)類型
         */
        class_addMethod([self class], sel, (IMP)dynamicMethIMP, "v@");
        return YES;
    }
    return [super resolveClassMethod:sel];
}
@end

7.消息轉(zhuǎn)發(fā)

消息轉(zhuǎn)發(fā),前提是沒人要的消息,才會(huì)被轉(zhuǎn)發(fā).我個(gè)人認(rèn)為是runtime比較精彩的部分.

1)重定向

消息轉(zhuǎn)發(fā)之前,runtime系統(tǒng)允許外界替換消息的接受者為其他對象,通過-(id)forwardingTargetForSelector:(SEL)aSelector;該方法不能指定對象為self了.如果沒有指定其他對象來發(fā)送這個(gè)信息,則進(jìn)入消息轉(zhuǎn)發(fā)機(jī)制.

2)轉(zhuǎn)發(fā)

在轉(zhuǎn)發(fā)機(jī)制里面,執(zhí)行的方法是- (void)forwardInvocation:(NSInvocation *)aInvocation;這個(gè)方法需要傳入一個(gè)NSInvocation對象,這個(gè)對象系統(tǒng)會(huì)自動(dòng)調(diào)用- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;返回一個(gè)方法簽名對象,用來生成NSInvocation對象當(dāng)做參數(shù)傳進(jìn)去使用,所以在重寫forwardInvocation:方法來做各種處理(eg:修改方法實(shí)現(xiàn),修改響應(yīng)對象)的同時(shí)也要重寫methodSignatureForSelector:,否則會(huì)拋異常.

如果沒有實(shí)現(xiàn)forwardInvocation:方法,系統(tǒng)會(huì)調(diào)-(void)doesNotRecognizeSelector:(SEL)aSelector方法,如果外界也沒有實(shí)現(xiàn)這個(gè)方法,那么程序就會(huì)crash.到此,消息轉(zhuǎn)發(fā)就告一段落了.

假如發(fā)送了消息之后,沒有方法實(shí)現(xiàn),那么,有多少次機(jī)會(huì)補(bǔ)救呢?我的理解是3次.

8.方法交換
#import "NSObject+CHLog.h"
#import <objc/runtime.h>
#import "CHTools.h"

@implementation NSObject (CHLog)

+(void)load{
    //1.獲取系統(tǒng)的description對象方法類型
    Method instanceDescription = class_getInstanceMethod(self, @selector(description));
    //2.獲取myLog對象的方法類型
    Method ch_instanceDescription = class_getInstanceMethod(self, @selector(myLog));
    //3.交換方法
    method_exchangeImplementations(instanceDescription, ch_instanceDescription);
    
    //1.獲取系統(tǒng)的description類對象方法類型
    Method classDescription = class_getClassMethod(self, @selector(description));
    //2.獲取系統(tǒng)的myLog類對象方法類型
    Method ch_classDescription = class_getClassMethod(self, @selector(myLog));
    
    //3.交換方法
    method_exchangeImplementations(classDescription, ch_classDescription);
}

- (NSString *)myLog{
    NSString *str = [NSString stringWithFormat:@"[文件名:%s], " "[函數(shù)名:%s], " "[行號:%d], [時(shí)間:%@]\n打印內(nèi)容:", [[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], __FUNCTION__, __LINE__, [LXHTools getTodayDetailDateString]] ;
    
    return str;
}
+ (NSString *)myLog{
    NSString *str = [NSString stringWithFormat:@"[文件名:%s], " "[函數(shù)名:%s], " "[行號:%d], [時(shí)間:%@]\n打印內(nèi)容:", [[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], __FUNCTION__, __LINE__, [CHTools getTodayDetailDateString]] ;    
    return str;
}
@end

介紹方法交換,主要是想大家有一個(gè)思想在,就是在沒有.m文件的情況下,想修改一個(gè)類的方法,除了使用繼承和分類暴力搶先之外,還可以利用runtime來實(shí)現(xiàn).并且runtime有一個(gè)好處就是只需要修改一次就能一勞永逸.你設(shè)想,你接觸一個(gè)很老的項(xiàng)目,而項(xiàng)目的需求是要你在系統(tǒng)的方法上添加新的功能,你難道要為系統(tǒng)的類寫一個(gè)分類,再去每個(gè)使用了該方法的類中去導(dǎo)入頭文件,再手動(dòng)把方法替換?如果你不會(huì)使用方法交換,那寫分類,確實(shí)是一個(gè)解決的方法.

交換方法的實(shí)現(xiàn),其實(shí)就是OC中Method Swizzle的實(shí)踐,除了method_exchangeImplementations,我們還可以利用class_replaceMethod來修改類,利用method_setImplementation來直接設(shè)置某個(gè)方法的IMP,歸根到底,都是偷換了selector的IMP.so far,有沒有覺得runtime非常牛逼,但是,runtime雖好,使用需謹(jǐn)慎啊.
如下面的使用的時(shí)候,調(diào)用description方法已經(jīng)被替換成了調(diào)用我的myLog方法。

//項(xiàng)目中用到description方法的地方都會(huì)偷偷變成我的myLog方法的實(shí)現(xiàn)
- (void)viewDidLoad {
    [super viewDidLoad]; 
    // description => myLog 交互這兩個(gè)方法實(shí)現(xiàn)
    NSLog(@"%@", [Person description]); 
    Person *p = [[Person alloc] init]; 
    NSLog(@"%@", [p description]); 
}
2016-02-23 16:42:11.599 runtime[56314:6093330] [文件名:NSObject+CHLog.m], [函數(shù)名:+[NSObject(CHLog) myLog]], [行號:38], [時(shí)間:2016-02-23 16:42:11]
打印內(nèi)容:
2016-02-23 16:42:11.600 runtime[56314:6093330] [文件名:NSObject+CHLog.m], [函數(shù)名:-[NSObject(CHLog) myLog]], [行號:33], [時(shí)間:2016-02-23 16:42:11]
打印內(nèi)容:
9.動(dòng)態(tài)添加屬性

在這之前,你是不是也認(rèn)為category中只能添加方法,不能添加屬性?
但是,利用runtime,添加屬性,也變成了可能!下面是我在UIView的分類中添加的一個(gè)字符串nameTag屬性,以后直接通過點(diǎn)語法,可以設(shè)置/獲取控件的字符串tag,設(shè)置控件的tag就不再拘泥于NSInteger了.

#import "UIView+CHFrame.h"
#import <objc/runtime.h>
@implementation UIView (CHFrame)
static char nametag_key;

- (void)setNameTag:(NSString *)NameTag {
    objc_setAssociatedObject(self, &nametag_key, NameTag, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSString *)nameTag {
    return objc_getAssociatedObject(self, &nametag_key);
}
- (UIView *)viewWithNameTag:(NSString *)aName {
    if (!aName) return nil; 
    // Is this the right view? 查找view
    if ([[self nameTag] isEqualToString:aName])
        return self;
    // Recurse depth first on subviews;
    for (UIView *subview in self.subviews) {
        UIView *resultView = [subview viewNamed:aName];
        if (resultView) return  resultView;
    }
    // Not found
    return nil;
}

- (UIView *)viewNamed:(NSString *)aName {
    if (!aName) return nil;
    return [self viewWithNameTag:aName];
}
@end
10.字典轉(zhuǎn)模型的安全實(shí)現(xiàn)原理

字典轉(zhuǎn)模型,字典,才需要轉(zhuǎn)成模型(將字典中的key,跟模型的屬性名一一對應(yīng)起來,在開發(fā)中直接通過點(diǎn)語法來取值,會(huì)更方便開發(fā)和利于糾錯(cuò)).

1) 自動(dòng)生成模型屬性

通常,服務(wù)器返回的字段會(huì)比較多,有些字段我們用不上,開發(fā)中,多數(shù)時(shí)候我們都是去字典中一個(gè)一個(gè)找,然后再去模型中定義對應(yīng)的屬性,下面介紹一個(gè)方便設(shè)計(jì)模型的方法--遍歷字典中所有的key,并以@property(nonatomic,strong) NSString *name的形式拼接打印出來,拷貝去使用就可以了.

//給NSDictionary寫一個(gè)分類
#import "NSDictionary+Property.h"

@implementation NSDictionary (Property)
// isKindOfClass:判斷是否是當(dāng)前類或者子類
- (void)createModelPropertyFormatter
{  
    NSMutableString *formatter = [NSMutableString string];
    // 遍歷字典
    [self enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull value, BOOL * _Nonnull stop) {       
        NSString *formatterKind;
        if ([value isKindOfClass:[NSString class]]) {
            formatterKind = [NSString stringWithFormat:@"@property (nonatomic, copy) NSString *%@;",key];
        } else if ([value isKindOfClass:NSClassFromString(@"__NSCFBoolean")]) {
            formatterKind = [NSString stringWithFormat:@"@property (nonatomic, assign) BOOL %@;",key];
        } else if ([value isKindOfClass:[NSNumber class]]) {
             formatterKind = [NSString stringWithFormat:@"@property (nonatomic, assign) NSInteger %@;",key];
        } else if ([value isKindOfClass:[NSArray class]]) {
             formatterKind = [NSString stringWithFormat:@"@property (nonatomic, strong) NSArray *%@;",key];
        } else if ([value isKindOfClass:[NSDictionary class]]) {
             formatterKind = [NSString stringWithFormat:@"@property (nonatomic, strong) NSDictionary *%@;",key];
        }     
        [formatter appendFormat:@"\n%@\n",formatterKind];     //換行
    }];    
    NSLog(@"%@",formatter);    
}
@end
//我的經(jīng)驗(yàn):將服務(wù)器獲取到的字段轉(zhuǎn)成plist文件,層級更加清晰,獲取字段中的字典數(shù)組,遍歷數(shù)組,得到單個(gè)的字典,通過調(diào)createModelPropertyFormatter方法,把字典的key統(tǒng)統(tǒng)打印出來,拷貝到模型中,刪除多余的字段,此時(shí),模型已經(jīng)創(chuàng)建好了.
//如果不懂使用這個(gè)快捷方法,可以留言,代碼這里就不貼出來了.

2) KVC--Key Value Coding

蘋果已經(jīng)提供了一個(gè)方法,可以快速進(jìn)行字典轉(zhuǎn)模型了

#import "CHTagItem.h"

@implementation CHTagItem
+ (instancetype)tagWithDict:(NSDictionary *)dict{
    CHTagItem *tagItem = [[self alloc] init];
    [tagItem setValuesForKeysWithDictionary:dict];
    return tagItem;
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key{}
@end

setValuesForKeysWithDictionary:dict快速賦值
setValue:forUndefinedKey:重寫系統(tǒng)方法(空實(shí)現(xiàn)),以防找不到一一對應(yīng)的字段時(shí)拋異常.
上面那種方法,只適用比較簡單的模型設(shè)計(jì).如果服務(wù)器返回的字段中字典有嵌套關(guān)系,只是簡單地重寫setValue:forUndefinedKey方法使程序是不拋異常,你會(huì)發(fā)現(xiàn),模型設(shè)計(jì)得并不是你想要的結(jié)果.下面舉個(gè):

#import <Foundation/Foundation.h>
#import "CHUserItem.h"

@interface CHCategoryItem : NSObject
@property (nonatomic, strong)NSString *name;
@property(nonatomic, strong)NSString *id;
/** 用戶信息模型的集合*/
@property (nonatomic, strong)NSArray<CHUserItem *> *users;
/**用戶信息的模型*/
@property (nonatomic, strong)CHUserItem *user;
@end

#import <Foundation/Foundation.h>
@interface CHUserItem : NSObject
@property (nonatomic, strong)NSString *screen_name;
@property (nonatomic, strong)NSString *header;
@property (nonatomic, strong)NSString *fans_count;
@end

在調(diào)用setValuesForKeysWithDictionary:方法時(shí),系統(tǒng)流程如下:
1.遍歷模型的屬性,調(diào)屬性的setter方法賦值--setName:
2.如果該屬性沒有setter方法,系統(tǒng)會(huì)去找不帶下劃線的屬性賦值--name.
3.如果該屬性沒有不帶下劃線的屬性,那系統(tǒng)會(huì)去找?guī)聞澗€的屬性去賦值--_name.
4.如果第3步還是沒有找到對應(yīng)的屬性賦值,系統(tǒng)就會(huì)調(diào)setValue:forUndefinedKey拋出異常,告訴外界找不到該字段去賦值.
根據(jù)上面的流程,我們可以通過重寫屬性的setter方法,攔截?cái)?shù)據(jù)處理好了再賦值,這樣才可以保證字典轉(zhuǎn)模型成功.

- (void)setUsers:(NSArray *)users{
    //保存字典數(shù)據(jù),這一步是寫setter方法的規(guī)范寫法
    _users = users;
    //字典轉(zhuǎn)模型
    NSMutableArray *arrayM = [NSMutableArray array];
    for (NSDictionary *dict in users) {
       //前提:在CHUserItem模型中已經(jīng)實(shí)現(xiàn)userItemsWithDict:方法
        CHUserItem *item = [CHUserItem userItemsWithDict:dict];
        [arrayM addObject:item];
    }
    //保存模型數(shù)據(jù)
    _users = arrayM;
}
-(void)setUser:(CHUserItem *)user{
    _user = user;
    NSDictionary *dict = (NSDictionary *)user;
    
    CHUserItem *item = [CHUserItem userItemsWithDict:dict];
    _user = item;   
}
3) MJExtension的簡單實(shí)現(xiàn)--runtime的運(yùn)用

蘋果因?yàn)椴恢滥愕哪P褪窃鯓釉O(shè)計(jì)的,所以他只能做到像KVC那樣快速賦值.但是我們自己設(shè)計(jì)好了模型之后,我們可以通過遍歷模型的屬性,去字典中找對應(yīng)的字段去賦值,這樣不但減少了遍歷的次數(shù),而且還排除了找不到未定義的key的錯(cuò)誤.MJExtension的實(shí)現(xiàn)原理便是這樣,下面,我貼出不是那么嚴(yán)謹(jǐn)?shù)腗JExtension實(shí)現(xiàn)代碼來共大家理解.

- (void)test{
    // 解析Plist文件
    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"category.plist" ofType:nil];
    
    NSDictionary *categoryDict = [NSDictionary dictionaryWithContentsOfFile:filePath];
    NSArray *array = categoryDict[@"list"];
    NSMutableArray *m = [NSMutableArray array];
    for (NSDictionary *dict in array) {
        
        CHCategoryItem *categoryItem = [CHCategoryItem modelWithDictionay:dict];
        [m addObject:categoryItem];
        NSLog(@"%@,%@,%@",categoryItem.name, categoryItem.users[0].screen_name,categoryItem.user);
    }
  }
//2016-02-27 23:28:07.931 runtime[12395:6880878] 網(wǎng)紅,中二函,<CHUserItem: 0x608000026580>
2016-02-27 23:28:07.932 runtime[12395:6880878] 精品,A號逗比,<CHUserItem: 0x6000000273a0>
2016-02-27 23:28:07.932 runtime[12395:6880878] 搞笑,梁猛搞笑視頻,<CHUserItem: 0x6000000276c0>
2016-02-27 23:28:07.933 runtime[12395:6880878] 創(chuàng)意,小越女simida,<CHUserItem: 0x6080000268a0>
2016-02-27 23:28:07.933 runtime[12395:6880878] 視頻,說方言的王子濤濤,<CHUserItem: 0x608000026bc0>
2016-02-27 23:28:07.933 runtime[12395:6880878] 圖文,阿葩罩爺,<CHUserItem: 0x6000000279e0>
2016-02-27 23:28:07.934 runtime[12395:6880878] 潛力,大哥vip,<CHUserItem: 0x600000027d00>
2016-02-27 23:28:07.934 runtime[12395:6880878] 生活,草莓阿三,<CHUserItem: 0x600000028020>
2016-02-27 23:28:07.934 runtime[12395:6880878] 原創(chuàng),5毛團(tuán)隊(duì),<CHUserItem: 0x600000028340>
#import <Foundation/Foundation.h>

@protocol ClassNameInItemArray <NSObject>
/**
 return 字典,key是模型中的名稱,value則是數(shù)組所裝的模型的名稱
 */
+ (NSDictionary *)classNameInItemArray;
@end

@interface NSObject (DictToModel)<ClassNameInItemArray>

/**字典轉(zhuǎn)模型的runtime實(shí)現(xiàn)*/
+ (instancetype)modelWithDictionay:(NSDictionary *)dict;
@end
#import "NSObject+DictToModel.h"
#import <objc/runtime.h>

@implementation NSObject (DictToModel)
// 獲取類里面所有方法
// class_copyMethodList(<#__unsafe_unretained Class cls#>, <#unsigned int *outCount#>)
// 獲取類里面的Property
//  class_copyPropertyList(<#__unsafe_unretained Class cls#>, <#unsigned int *outCount#>)

// Ivar:實(shí)例變量 以下劃線開頭,Property:屬性
+ (instancetype)modelWithDictionay:(NSDictionary *)dict
{
    id objc = [[self alloc] init];//創(chuàng)建模型對象
    unsigned int count = 0;    // count:實(shí)例變量個(gè)數(shù)
    // 獲取實(shí)例變量的數(shù)組集合
    Ivar *ivarLists = class_copyIvarList(self, &count);
    // 遍歷數(shù)組
    for (int i = 0; i < count; i++) {
        Ivar ivar = ivarLists[i];     // 1.取出單個(gè)實(shí)例變量
        NSString *name = [NSString stringWithUTF8String:ivar_getName(ivar)];  // 2.獲取實(shí)例變量名字(帶下劃線的)
        NSString *key = [name substringFromIndex:1]; // 截串,_name -->name
        // 獲取實(shí)例變量類型,下面需要用到的
        NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
        // 斷點(diǎn)查看可看到type為@\"CHUserItem\" 截串獲取:CHUserItem
        type = [type stringByReplacingOccurrencesOfString:@"\"" withString:@""];
        type = [type stringByReplacingOccurrencesOfString:@"@" withString:@""];
        // 根據(jù)key去服務(wù)器返回的字典中查找對應(yīng)的value
        id value = dict[key];
        //取出來的value是字典,并且模型屬性類型是自定義的(不是NS開頭)
        //@property (nonatomic, strong)CHUserItem *user;類型是自定義的類型
        if ([value isKindOfClass:[NSDictionary class]] && ![type hasPrefix:@"NS"]) {
            // 字典轉(zhuǎn)換成自定義的模型
            Class modelClass = NSClassFromString(type);// 使用映射,得到類對象
            if (modelClass) {
                value = [modelClass modelWithDictionay:value];
            }
            
        }
        // 判斷值是否是數(shù)組
        if ([value isKindOfClass:[NSArray class]]) {
            // 字典數(shù)組轉(zhuǎn)換成模型數(shù)組.
            // 校驗(yàn)self對應(yīng)的類中,是否實(shí)現(xiàn)我的協(xié)議方法(該方法的作用請看頭文件里的具體說明)
            if ([self respondsToSelector:@selector(classNameInItemArray)]) {                
                id idSelf = self;// 轉(zhuǎn)換成id類型,就能調(diào)用任何對象的方法
                // 獲取數(shù)組中字典對應(yīng)的模型字符串
                NSString *type = [idSelf classNameInItemArray][key];
                Class classModel = NSClassFromString(type);
                NSMutableArray *arrM = [NSMutableArray array];
                // 遍歷字典數(shù)組,生成模型數(shù)組
                for (NSDictionary *dict in value) {
                    // 字典轉(zhuǎn)模型
                    id model =  [classModel modelWithDictionay:dict];
                    [arrM addObject:model];
                }             
                // 把模型數(shù)組賦值給value
                value = arrM;
                
            }
        }
        // 給模型中屬性賦值
        if (value) {
            [objc setValue:value forKey:key];
        }
    }    
    return objc;
}
@end

plist文件的概覽圖如下:


categoryPlist.png

如果你英語水平不錯(cuò),可以點(diǎn)擊runtime官網(wǎng)文檔進(jìn)行自己的學(xué)習(xí)研究.
對文章有什么建議或意見,歡迎糾正.
寫文不易,如果覺得本文章對你有用請點(diǎn)個(gè)喜歡/關(guān)注,謝謝!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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