objc轻量级json模型互转库实现

第一次写技术相关博文,希望支持,以后还会继续分享。

本文讲些什么?

本文主要讲objc轻量级json模型互转库的实现,源码地址在:github地址 。该库主要优点是轻量(单文件、无任何依赖),使用简单,性能高效,高容错率,可定制转换过程。

如何实现?

json(或者说键值对/字典)转模型对象(Model)的实现原理很简单:

1.首先获取对象属性(property)元数据

2.然后基于元数据对值(value)进行转换

3.最后将转换后的value赋值给属性

下面会从三个方面说明具体实现细节,分别是:属性元数据value转换属性赋值

属性元数据

什么是元数据?

元数据是描述数据的数据,属性元数据包括属性的类型、读取特性(是否原子操作、是否readonly等等)、相关联的实例成员变量(ivar)名等等。这些数据是进行value转换及赋值的依据。

如何获取元数据?

属性元数据可通过objc的runtime函数获取,使用到的主要包括:

1.获取属性实例相关函数:

//通过属性名获取属性实例
objc_property_t class_getProperty(Class cls, const char * name);

//拷贝所有属性实例
objc_property_t * class_copyPropertyList(Class cls, unsigned int * outCount);

2.获取属性元数据相关函数:

// 拷贝属性元数据
objc_property_attribute_t * property_copyAttributeList(objc_property_t  property, unsigned int * outCount);

属性元数据以objc_property_attribute_t结构体方式存储,该结构体由属性名(name)及相应的属性值(value)组成,其定义如下:

/// 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;

下面列一个属性名和及其对应值的表格图(图1),其中Code列为name及value共同组成的编码(name<value>格式,尖括号内容为value,其余为name),这个表格不是特别完整,具体请参见官方文档:Declared Properties

图1

需要的元数据

上文说过属性元数据是value转换和属性赋值的依据,那么具体需要哪些元数据呢?下面针对具体过程进行分别说明:

1.Value转换

value转换过程需要知道目标值类型,也就是属性类型,属性类型是通过名字为“T”的objc_property_attribute_t结构体存储的,其值使用TypeEncoding进行编码,下面列一个TypeEncoding表格图(图2),具体参见官方文档:Type Encodings

图2

为了简化转换过程,转换操作只会处理三种类型的属性,分别是C语言数字类型(即基本数据类型,包括整形、浮点、布尔等等)、对象(准确说是对象指针)和结构体/联合体,其他类型(例如非对象指针、C数组等等)将会被忽略。其实这些其他类型属性一般也不会作为模型的属性。我定义了一个枚举来标识这些类型,具体见下:

//属性类型
typedef NS_ENUM(NSInteger,_MyPropertyType) {
    _MyPropertyTypeObject = 0, //对象
    _MyPropertyTypeStruct, //结构体或联合体
    _MyPropertyTypeNumber, //C语言数字
    _MyPropertyTypeOther, //其他
    _MyPropertyTypeUnsupported = _MyPropertyTypeOther //其他不支持
};

对应的获取类型的方法为:

//获取属性类型
static inline _MyPropertyType _getPropertyType(const char * type) {
    
    if (type == NULL) {
        return _MyPropertyTypeOther;
    }
    
    switch (*type) { //读取第一个字符
        case '@': //对象
            return _MyPropertyTypeObject;
            break;
            
        case '{':
        case '(': //结构体和联合体
            return _MyPropertyTypeStruct;
            break;
            
        case 'c':
        case 'i':
        case 's':
        case 'l':
        case 'q':
        case 'C':
        case 'I':
        case 'S':
        case 'L':
        case 'Q':
        case 'f':
        case 'd':
        case 'B': //基本数据类型
            return _MyPropertyTypeNumber;
            break;
            
        default:
            break;
    }
    
    return _MyPropertyTypeOther;
}

2.属性赋值

属性赋值方式一般有三种方法,分别是:

1.KVC

2.调用属性setter方法

3.直接对属性关联的实例成员变量(ivar)进行赋值

使用KVC进行赋值本质上也是使用下面两种方式进行赋值,KVC使用起来十分方便,但是效率堪忧,为了提高赋值效率,将不采用这种方式。通过赋值方式的不同可以将属性分为下面四个类型:

1.无法进行赋值:属性是readonly(没有setter方法)并且没有关联的实例成员变量(ivar)

2.只能使用setter方法赋值:属性不是readonly并且无关联的实例成员变量

3.只能使用关联的实例成员变量赋值:属性是readonly并且有关联的实例成员变量

4.任意赋值:属性不是readonly并且有关联的实例成员变量

对于第一种无法赋值的属性,我们直接忽略即可。第二三种类型属性使用对应的方法进行赋值。第四种类型属性通常情况下两种赋值方式可以任意互换使用,如何取舍下文我会详细说明。

通过对属性赋值方式的分析,我们可以知道属性赋值需要的元数据包括属性是否readonly关联的实例成员变量信息,这两个信息分别对应名字为“R”和“V”的属性元数据。还有一点需要注意的是一般来说属性setter方法名为默认名字set<PropertyName>:,但其也支持自定义名称(见下面代码)

//自定义setter方法名
@property(nonatomic,setter=setFloatValue1:) CGFloat floatValue;

所以还需要获取名字为“S”的属性元数据来获取自定义setter方法名,所以本质上来说就是通过元数据获取到setter方法名,包括默认的和自定义的。

经过上面的分析,我们知道了需要的属性元数据有三个,分别是属性类型(T为关联的元数据名称,下同)、属性setter方法名(R和S)以及属性关联的实例成员变量名(V)。 为了提高效率,获取属性元数据后我们应该对其进行缓存,缓存的数据结构如下:

//属性元数据
@interface _MyModelPropertyData : NSObject
{
    @package
    
    //属性名
    NSString * _name;
    
    //属性类型
    _MyPropertyType _type;
    //属性类型编码
    NSString * _typeEncoding;
    //属性类型所占用内存大小
    size_t _typeSize;
    
    //对象类型的class(如果属性类型是对象该值有意义)
    Class _typeClass;
    //结构体/联合体类型名称(如果属性类型是结构体/联合体该值有意义)
    NSString * _typeStruct;
    
    //属性getter方法
    SEL _getterSelector;
    //属性setter方法(只读属性改值为nil)
    SEL _setterSelector;
    //属性关联的变量(无关联变量为nil)
    Ivar _ivar;
}
@end

可以看到该数据结构不仅仅存储了属性元数据,还存储了对元数据进行预处理后的信息,这也是为了进一步提升速度。关于缓存如何实现这里不会详细说明,关键需要保证线程安全还有最好有数据淘汰机制。为了轻量方便我直接使用了系统库的NSCache进行缓存,缓存的key由类名及属性名共同决定。

Value转换

Value转换是容错率的根本,强大值转换过程才能保证高的容错率。值转换设计目标是:

1.尽可能进行正确转换

2.无法转换也不会导致crash

3.可定制转换过程

值转换有两个方式,分别是自定义转换默认转换。下面分别说明:

1.自定义转换

自定义转换功能通过调用convert<PropertyName>Value:(<PropertyName>为首字母大写的属性名,下同)固定格式转换方法实现,如果实现了相对应属性名的转换方法便会自动调用方法进行值转换,调用前会进行相应的参数及返回值类型判断以减少异常情况。

2.默认转换

默认转换是值转换实现的重点,针对上文定义的三种支持类型的属性,有不同的默认转换机制,分别为:

2.1.属性为对象

直接对值的类型进行判断,类别和属性类别相同不做任何转换,不同则会做一些已知类型的判断和转换,比如属性类别是NSString,Value是其他类别对象,则直接通过description方法转换成NSString对象,这块不做详细介绍了。具体参见方法xyy_updateWithDictionary:的注释:

//更新属性值,更新属性值策略为:
//(1)通过xyy_propertyNameForKey:方法获取key对应的属性名称
//
//(2)判断属性是否有效(可赋值属性)
//属性是否有效需满足四个条件:
//1.属性没有被xyy_needIgnoreProperty:forDicToModel:方法忽视
//2.属性名对应属性存在
//3.属性类型是支持的数据类型,包括对象,C语言数字类型,结构体或联合体
//4.属性不是readonly(即有setter方法)或者属性有关联的成员变量
//
//(3)对属性值进行转换
//属性值转换顺序是:
//1.(如果存在)调用convert<PropertyName>Value:方法进行转换,调用方法前会进行方法参数和返回值判断
//2.进行默认转换
//属性值默认转换策略是:
//1.属性值为空(包括NSNull对象)返回空值
//2.属性类型为对象:首先判断值是否是同一种类对象,直接返回,否则执行默认转换策略进行转换(能进行默认转换的类NSString、NSMutableString、NSMutableArray、NSMutableDictionary、NSNumber、NSDecimalNumber、NSDate以遵循XYYJsonModel协议的类),无法转换则返回nil
//3.属性类型为结构体或联合体:(如果存在)调用<structName/unionName>Value方法进行转换,无法转换则返回空值
//4.属性类型为C语言数字类型:使用数字类型相关方法进行转换,无法转换则返回空值
//属性空值为:
//1.调用(如果存在)nil<PropertyName>Value方法获取空值,调用方法前会进行方法参数和返回值判断
//2.没有自定义空值使用默认空值
//默认空值为:
//1.属性类型为对象:nil
//2.属性类型为结构体或联合体:填充为0的NSValue
//3.属性类型为C语言数字类型:值为0的NSNumber
//
//(4)对属性进行赋值
//属性赋值策略为:
//1.存在setter方法,使用setter方法进行赋值
//2.直接对成员变量进行赋值
- (void)xyy_updateWithDictionary:(NSDictionary *)dictionary;

2.2.属性为数字类型(基本数据类型)

对于数字类型的属性,值转换的目标值就是NSNumber类型的对象,策略是先将值转换成数字类型,然后使用NSNumber进行装箱。值转换成数字类型策略是尝试调用一系列默认转换方法(例如integerValue、doubleValue、floatValue等等)进行转换,调用一系列方法的目的是为了提高转换的成功率,由于调用前会进行参数及返回值判断,所以不用担心这样会带来crash风险。这样的转换方式也提供了另一种自定义转换的实现方式:通过分类的方式为value所属的类实现上述默认转化方法。为了提高正确率,会针对不同类别的数字类型优先调用于其中对应或相近的默认转换方法,例如属性类型是浮点类型,会优先调用doubleValue、floatValue等转换方法,属性值是布尔类型会优先调用boolValue转换方法。

2.3.属性为结构体/联合体

这个和上面数字类型的实现类似,只不过装箱的对象换成NSValue,默认转换方法变成了<structName/unionName>Value格式的方法。另外为了让NSString支持与系统结构体(CGSize、CGRect等等)的互转,通过分类的方式为其添加了相应的方法:

@interface NSString(NSStringSystemStructExtensions)

- (CGPoint)CGPointValue;
- (CGRect)CGRectValue;
- (CGSize)CGSizeValue;
- (CGVector)CGVectorValue;
- (CGAffineTransform)CGAffineTransformValue;
- (UIEdgeInsets)UIEdgeInsetsValue;
- (UIOffset)UIOffsetValue;
- (NSRange)NSRangeValue;

+ (NSString *)stringWithCGPoint:(CGPoint)point;
+ (NSString *)stringWithCGRect:(CGRect)rect;
+ (NSString *)stringWithCGSize:(CGSize)size;
+ (NSString *)stringWithCGVector:(CGVector)vector;
+ (NSString *)stringWithCGAffineTransform:(CGAffineTransform)affineTransform;
+ (NSString *)stringWithUIEdgeInsets:(UIEdgeInsets)edgeInsets;
+ (NSString *)stringWithUIOffset:(UIOffset)offset;
+ (NSString *)stringWithNSRange:(NSRange)range;

@end

3.空值处理

除了上诉的两种值转换方式,值转换还提供了空值转换机制,包括nil<PropertyName>Value这种格式的自定义空值提供方法和默认空值,对于对象来说默认空值就为nil,对于非对象类型,默认空值为NSValue和NSNumber装箱的填充为0的数据。下面我想稍微说一下装箱和拆箱:

4.装箱和拆箱

装箱是指将非对象类型数据包装成对象的过程,拆箱反正。装箱使用的对象是NSValue及其子类,对于数字类型使用NSNumber进行装箱,其他类型直接使用NSValue进行装箱。至于数字类型为什么要使用NSNumber的原因是,使用NSNumber会有提高值的兼容性,比如一个浮点类型数据使用NSNumber进行装箱,其可以直接拆箱成整形,而不需要做其他额外处理。NSNumber就像连接所有数字类型之间的桥梁,让它们之间的转换变得无缝。

属性赋值

值转换完毕后就可以对属性进行赋值了,上文提到过属性赋值有三种方式:KVCsetter方法关联的实例成员变量。为了效率我们不采用KVC,尽管它使用起来很方便。下面我会详细说明后面两种赋值方式如何实现及它们之间如何选择。

1.使用setter方法赋值

使用setter方法赋值也就是调用对象的相应方法(也叫发送消息),一般来说调用对象方法有三种方式,分别是:

1.调用方法的实现函数指针(IMP)

2.调用objc_msgSend函数

3.使用NSInvocation

第一种方式就是直接调用方法实现函数,这种方式不是很安全,参数传错很容易造成crash,对nil对象使用这种方式发消息是不安全的,所以不太建议这种方式。其一般使用姿势如下:

//获取IMP
IMP imp = class_getMethodImplementation([self class], @selector(setDate:));
 
//调用IMP 
((void(*)(id,SEL,id))imp)(self,@selector(setDate:),nil);

第二种方式调用objc_msgSend函数最终也会通过IMP调用方法,但objc_msgSend函数是通过汇编实现,效率肯定比方式一高,而且对nil对象使用这种方式发送消息是安全的。所以使用方式二显然比方式一更好。使用方式如下:

//发送消息
 ((void(*)(id,SEL,id))objc_msgSend)(self,@selector(setDate:),nil);

第三种方式使用NSInvocation本质上是对上述方式的封装,效率上肯定没有上述方式高,理应不应该被使用,但是它为我们解决了一个问题:动态的函数调用栈帧大小。调用一个函数时其返回值和相应参数等都会被压入栈中,这部分内存也就是函数调用栈帧,其大小由函数返回值参数等等决定,通常都是硬编码的方式调用函数,即调用函数是其返回值及参数都已经确定,编译器就可以通过这些信息决定函数调用栈帧的大小。由于属性类型是动态的,其setter函数的参数类型也是动态的,如果属性类型是对象可以用id来定义参数类型,但如果是其他类型,比如结构体,虽然我们知道它占用的内存大小,但无法用硬编码的方式来定义函数调用参数,因为编码时我们并不知道它的名字。我们可以定义一个比较大的类型作为缓存区来存储参数,然后用这个类型定义函数调用参数来解决这个问题,但总觉得这种方式不优雅,所以对于类型为结构体/联合体的属性将采用NSInvocation方式进行setter赋值。使用方式如下:

NSInvocation * invocation = [NSInvocation invocationWithMethodSignature:[[self class] instanceMethodSignatureForSelector:setter]];

 //读取参数值
void* pValue = malloc(invocation.methodSignature.methodReturnLength);
[(NSValue*)value getValue:pValue];

//设置参数
[invocationsetArgument:pValueatIndex:2];

//执行方法
invocation.selector= setter;
[invocationinvokeWithTarget:self];

//清理工作
free(pValue);

对于属性类型为数字,由于基本数据类型类别和大小有限,可以定义一个联合体作为缓存区,用该联合体作为函数调用的参数,联合体定义如下:

//属性数字值
typedef union {
    char charValue;
    unsigned char unsignedCharValue;
    short shortValue;
    unsigned short unsignedShortValue;
    int intValue;
    unsigned int unsignedIntValue;
    long longValue;
    unsigned long unsignedLongValue;
    long long longLongValue;
    unsigned long long unsignedLongLongValue;
    float floatValue;
    double doubleValue;
    bool boolValue;
} _MyPropertyNumberValue;

然后使用objc_msgSend调用setter方法进行赋值,具体实现为:

//拆箱读取值
_MyPropertyNumberValue numberValue = {0};
[value unboxValue:&numberValue typeEncoding:propertyData->_typeEncoding.UTF8String];

//赋值
((void(*)(id,SEL,_MyPropertyNumberValue))objc_msgSend)(self,setter,numberValue);

通过上面的分析,使用setter方法进行赋值的策略是:

1.属性类型为对象:使用objc_msgSend函数调用setter方法

2.属性类型为结构体/联合体:使用NSInvocation调用setter方法

3.属性类型为数字:定义参数缓存区使用objc_msgSend函数调用setter方法

2.直接对成员变量赋值

在说明如何对成员变量进行赋值前,我们需要了解:何为对象?对象的数据结构如何?

何为对象?

对象实质上是就是一段内存空间,该内存空间首地址存储的是其所属类的指针(isa指针),这就是构成一个对象的全部条件。

对象的数据结构如何?

上文说了对象就是首地址指向其所属类的一段内存空间,紧接着isa指针储存的是对象的成员变量,包括其继承链上的所有成员变量,每一个成员变量都有相对首地址的偏移值(offset),首地址+偏移值即为成员变量的存储位置。

如何赋值?

通过上文知道了对象的数据结构,对成员变量赋值变得十分简单:找到成员变量存储地址,使用memcpy函数将数据拷贝进去,即完成了赋值,具体实现为:

//获取成员变量存储地址
ptrdiff_t offset = ivar_getOffset(propertyData->_ivar);
void * location = (((char *)(__bridge void *)self) + offset);

//读取值
void* pValue = malloc(propertyData->_typeSize);
memset(pValue,0, propertyData->_typeSize);
[(NSValue*)value getValue:pValue];

//设置值
memcpy(location, pValue, propertyData->_typeSize);

//清理工作
free(pValue);

但有一点需要注意的是如果成员变量类型是对象,由于需要考虑引用计数问题,直接通过上述方法赋值肯定会出现内存问题,还好runtime函数库为我们提供了针对对象成员变量赋值的函数object_setIvar,直接使用这个函数即可:

//赋值对象成员变量
object_setIvar(self, propertyData->_ivar, value);

通过上面的分析,直接使用成员变量进行赋值的策略是:

1.属性类型为对象:使用object_setIvar函数赋值

2.其他情况:使用memcpy函数赋值

3. setter VS ivar

最前面我们按赋值方式将属性分为四类,分别是:不可赋值、只能setter赋值、只能ivar赋值和两种方式都可。第一种属性直接会被忽略,第二三种没得选,最后一种有的选,也是下面我们即将讨论的:两种赋值方式如何选择?

以效率这个角度来说直接操作成员变量速度更快效率更高,但是直接使用成员变量赋值会丢失定义的属性赋值特性,比如原子性、拷贝值以及自定义实现的setter方法。这样显然会带来风险和不确定因素甚至有时候会导致数据错误,所以出于安全和正确性考虑应该尽可能使用setter方法进行赋值,安全性和正确性比速度和效率更重要。但考虑到存在的性能需求,我设计上把这个选择权交给了使用者,使用者可以根据具体情况在明确不会出现问题的前提下选择直接使用成员变量进行赋值以提升速度和效率。覆盖实现方法xyy_alwaysAccessIvarDirectlyIfCanForDicToModel:即可,具体参见源码的注释。

写在最后

说完了对象赋值,这篇文章也接近尾声,本文只讲了字典(json)转模型的实现原理,我写的库还包括模型转字典(json)以及模型归档功能,由于原理大同小异,也不累赘讲解了,谢谢大家看到这里。

最后打个广告,希望大家能看看用用我的库哈,具体使用方法如下:

1.直接导入源码

下载或者clone源码(地址为:github地址)导入两个文件XYYModel.h和XYYModel.m即可。

2.使用CocoaPods

Podfile文件加入 pod 'XYYModel' 即可

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

推荐阅读更多精彩内容