30分钟撸出一个线程安全的YYModel

镇贴图

前言

做iOS开发以来,从最开始没有数据模型,所有数据都靠NSStringNSDictionaryNSArrray等系统基础的对象存储,到后来自己开始手动撸数据模型,再然后就开始接触JSONModel,彻底脱离了枯燥的重复的动作,后来一些国产的一些优秀的数据模型库也开始崭露头角,如MJExtension,如YYModel等。但别人的轮子始终是别人的,要是中途爆了胎还得去人家的店里(Github)提出问题,等待修复,可是现实中大多数的时候时间都不允许我们这样慢慢的等待,所以就有了这篇文章。

在这篇文章中,你可以了解到一些实用的Runtime技巧,一些面向对象的思想,最重要的是可以自己做出一个可以供自己扩展的数据模型轮子。轮子虽小但优点在于方便理解,扩展性强。

废话不多说,直接进入正题。

一张图说目标功能

功能图解

想一想别人的轮子

要将数据模型的实现原理,先回想一下我们平时是怎么用别人的数据模型的。

  • 首先我们需要根据服务端返回数据格式在我们一个对应的DataModel里面将所有的参数名称定义好,并且定义好对应的类型,如:

    @interface PersonDataModel : NSObject
    
    @property (nonatomic ,assign) NSUInteger age;
    @property (nonatomic ,copy  ) NSString *name;
    @property (nonatomic ,copy  ) NSString *sex;
    
    @end
    
  • 然后我们传入一个NSString或者NSData之类的东西,总之最后我们将它转化为NSDictionary,然后就有了我们需要的一个完整的数据模型。如JSONModel的使用方法:

    PersonDataModel *person = [[PersonDataModel alloc]initWithString:jsonString error:NULL];
    

所以就有了我们的设计思路

得出设计思路

  • 首先我们利用RuntimePersonDataModel中所有的有用信息记录到最重要的ClassPropertyInfo(在下面Lists中会讲出有哪些需要记录的信息)。

  • 从而得到ClassInfo(这里暂时用不到MethodInfoIvarInfo)。

  • 区分需要转化的对象是NSDictionary还是NSArray

  • NSDictionary中的Key与我们刚才记录在ClassPropertyInfo中的name进行对比。

    NSArray拆分成多个NSDictionary(或者String)做。

    暂时不支持NSArray中又是NSArray对象。

  • 将对比上的Key进行差异化赋值。

![](https://github.com/dengbin9009/MyFiles/blob/master/DBModel思维导图.png?raw=true =100x200)

下面我们就来实现具体的步骤

Step

  • 获取关键的ClassPropertyInfo信息

    一条比较丰富的属性长这样:

   @property (nonatomic, strong ,setter=setGroup: ,getter=group) NSArray<Student> * group;

可以看出这个地方对我们有用的有settergetterNSArrayStudentgroup,当然其中的nonatomicstrong也是一些有用的信息,但我们目前姑且不谈。

关于property苹果在<objc/runtime.h>中给了我们这些Api,如图

Runtime_Property
Runtime_Property

其中name就可以通过下面这个Api得到是group

/** 
* Returns the name of a property.
* 
* @param property The property you want to inquire about.
* 
* @return A C string containing the property's name.
*/
OBJC_EXPORT const char *property_getName(objc_property_t property) 
   OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);

其它的都可以在苹果给我们的另外一个Api中全部获取到

```objectivec
/** 
 * Returns an array of property attributes for a property. 
 * 
 * @param property The property whose attributes you want copied.
 * @param outCount The number of attributes returned in the array.
 * 
 * @return An array of property attributes; must be free'd() by the caller. 
 */
OBJC_EXPORT objc_property_attribute_t *property_copyAttributeList(objc_property_t property, unsigned int *outCount)
    OBJC_AVAILABLE(10.7, 4.3, 9.0, 1.0);
```

而这个函数取出来的是一个关于objc_property_attribute_t的数组,而objc_property_attribute_t是一个这样的结构题:

```objectivec
/// Defines a property attribute
typedef struct {
    const char *name;           /**< The name of the attribute */
    const char *value;          /**< The value of the attribute (usually empty) */
} objc_property_attribute_t;    
```

这里的这里namevalue的定义可以参考:

name包括N&WRGSVT

这里面的GS正好对应gettersetter,这两个比较好理解,都是对应SEL的name,不过这个这个时候通过value取出来的是一个char型字符串,这个要注意一下。比如getter就是"group"setter就是"setGroup:"

T就稍稍复杂一点一些,这里的T就是@\"NSArray<Student>"\ (如果有两个protocol则是@\"NSArray<Student><Student2>),我们可以将它分为三部分@NSArrayStudent。其中NSArray是这个属性的ClassStudent是对应的protocols,因为protocols可能有多个,所以它是个数组。同样的它们也都是char型字符串。

最关键的是前面的@它代表这个property是个对象,具体这个char所对应的含义可以参考:#####
* *[iOS方法返回值和参数对应的Type Encodings](http://blog.csdn.net/dengbin9009/article/details/72922244)*
其实在objc/runtime.h第1560行至1589行中也有对应的描述。我们将@这样的字符串单独存入一个新定义的属性type中#####

这里有个Tip可以有效的将@\"NSArray<Student><Student2>分成NSArrayStudentStudent2这样的数组。

NSString *type = @"@\"NSArray<Student><Student2>";
NSMutableArray *values = [type componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"@\"<>,"]].mutableCopy;
[values removeObject:@""];
// 最终values = @[@"NSArray",@"Student",@"Student2"];

到这里关于一条Property最重要的一些信息我们都得到了:

  • clsNSArray
  • namegroup
  • type@
  • getterSel@selector(group)
  • setterSel@selector(setGroup:)
  • protocolsStudent

然后在补上一些能够让我们更方便使用的属性,比如:

* ```property```:通过*runtime*取出的的*property*本身
* ```isCustomPropetry```:是否是系统类
* ```isMutable```:是否是系统类里面的可变类型
* ```superClsInfo```:父类*ClassInfo*,如果父类为*nil*,则它也是*nil*
  • 获取关键的ClassInfo信息。

ClassInfo中对于本文的有用信息不多,目前我们只取:

* ```name ```:类名,如这里的```person```;
* ```cls ```:类本身,如这里的```PersonDataModel```;
* ```propetryInfos ```:参考第一步    
      >  *获取关键的```ClassPropertyInfo```信息*
* ```superClsInfo```:父类的```ClassInfo```,可用一个递归方法实现。
         
        + (instancetype)classInfoWithClass:(Class)cls{
            if ( !cls ) return nil;
            ...
            if ( !classInfo ) {
                classInfo = [[DBClassInfo alloc] initWithClass:cls];
            }
            return classInfo;
        }
            
        - (instancetype)initWithClass:(Class)cls{
            if ( !cls ) return nil;
            self = [super init];
            if ( self ) {
                ...
                _superCls = class_getSuperclass(cls);
                _superClsInfo = [DBClassInfo classInfoWithClass:_superCls];
                ...
            }
            return self;
        }
        
  由于classInfoWithClass是个类方法,所以这一步一定要确保线程安全,具体方式可以见 *[Demo](https://github.com/dengbin9009/DBModel.git)*
  • 区分需要转化的对象是NSDictionary还是NSArray

  • 一般入参有四种

    1. ```NSDate```
    2. ```NSString```
    3. ```NSArray ```
    4. ```NSDictionary ```
    
  • 这里只详细介绍NSDictionary的处理方式,因为无论是NSDate还是NSString我们最终都要转化为NSDictionary或者NSArray,而NSArray通常情况下也是将起转化为一个个NSDictionary的来进行相应的处理的。

    如果NSArray中是都是NSString那么就不需要用到数据模型,如果NSArray中也是NSArray,本类暂不支持这样的JSON格式。在本文也就不做讲述

  • NSDictionary中的Key与我们刚才记录在ClassPropertyInfo中的name进行对比。对比方式嘛就是轮询。

    在这一步我们的目的是得到在我们DataModel中的每个ClassPropertyInfo对应的在NSDictionaryobject

    这句话读起来可能比较绕口:所以我们举个🌰:

    还是上文定义的PersonDataModel

    这个时候传入的NSDictionary是

 { "name": "小明","age": 18,"sex": "男"}

那么这个时候我们要找到的就是PersonDataModelnamesexClassPropertyInfo和它对应的Value

而在这个地方我们就可以做一些比较有意思的事情了,比如白名单黑名单过滤,比如属性名称的映射,而这些有意思的方法可以将它都归为一个Option的协议,并将所有协议单独归类出一个文件DBModelProtocol,这样方便阅读,也方便维护。
> 白名单黑名单比较好理解,就是在对应的Model里面接受对应的名单实现是否对这个属性进行赋值或者不赋值。具体使用类似实现以下两个协议即可

```
+ (NSArray *)modelPropertyBlackList{
    return @[@"teacher",@"groupCount",@"groupArray"];
}

+ (NSArray *)modelPropertyWhiteList{
    return @[@"teacher",@"groupCount",@"groupArray"];
}
```

> 属性名称的映射其实就我常用的重命名,比如服务器返回了我们一个```key```为```id```,但```id```是一个隐藏的系统关键字,我们一个会将它重命名为```personId```或者```teacherId```等更容易理解的属性名称

我们重新在PersonDataModel的基础上定义一个TeacherDataModel的数据模型

@interface TeacherDataModel : PersonDataModel
@property (nonatomic, assign) NSUInteger teacherId;
@end

而服务端返回给我们数据模型却是

 { "id": "110", "name": "黄卫民", "age": 38, "sex": "男"}

这个时候我们就可以在这一步进行一些差异化的对比了:

首先我们先实现协议:

+ (NSDictionary *)customKeyMapper{
   return @{@"id":@"teacherId"};
}

当我们轮询到TeacherDataModelnameteacherIdClassPropertyInfo时取出的NSDictionary中对应keyidobject

  • 将对比上的nameDBClassPropertyInfo的和object进行差异化赋值。

这一步是逻辑最简单,但也是实现起来最繁琐的一步。

  • DBClassPropertyInfo中的type可以让我们知道这个property是什么类型,上文有讲述。

  • object转化为对应的property的类型

    这一步我们新建一个新的文件DBValueTransformer来帮我们做这些数据的处理,并且在这一步我们也可以插入我们的一个协议(NSString->NSDate)

  • 再利用objc_msgSend进行赋值

进行到这已经完成对一个NSDictionary->DataModel的全过程。

小结

虽然本文只是讲述了NSDictionary->DataModel的过程,没有其他的Model功能那么完善,如:

  • Model->Json
  • Model比较
  • 深拷贝

但我相信如果能看到这里的同学对其他功能应该是已经可以手到擒来了。

做事之前先理清楚思路,功能点全部归好类才能更好帮助我们完成它!

本文所有代码可以在这里找到:Demo

参考

喜欢的朋友可以点个下面的喜欢,这是最作者最大的支持,谢谢!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,678评论 0 9
  • 终于把前面的base文件夹简简单单的看了一遍,终于可以回到正片上来了,保证不烂尾。 项目天天用yymodel解析数...
    充满活力的早晨阅读 1,355评论 1 0
  • 我们常常会听说 Objective-C 是一门动态语言,那么这个「动态」表现在哪呢?我想最主要的表现就是 Obje...
    Ethan_Struggle阅读 2,170评论 0 7
  • 这潮湿又闷热的7月,总是时不时来些阵雨,像是在诉说着不快。我喜欢雨后的空气,喜欢那被雨水冲刷后满眼的翠绿,是...
    温蒂的世界阅读 195评论 0 0
  • 近五个月的时间,我写了一百多篇日记了,虽然每天有些记流水账一般的写日记,但是我觉得我的成长还是有的,只是相比之下还...
    坚志阅读 73评论 0 0