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进行差异化赋值。

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

Step

  • 获取关键的ClassPropertyInfo信息

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

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

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

    关于property苹果在<objc/runtime.h>中给了我们这些Api,如图
    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中全部获取到

    /** 
     * 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是一个这样的结构题:

    /// 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所对应的含义可以参考:
    其实在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

  • 区分需要转化的对象是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"];
}

属性名称的映射其实就我常用的重命名,比如服务器返回了我们一个keyid,但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

参考

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值