镇贴图
前言
做iOS开发以来,从最开始没有数据模型,所有数据都靠NSString,NSDictionary,NSArrray等系统基础的对象存储,到后来自己开始手动撸数据模型,再然后就开始接触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];
所以就有了我们的设计思路
得出设计思路
-
首先我们利用
Runtime将PersonDataModel中所有的有用信息记录到最重要的ClassPropertyInfo(在下面Lists中会讲出有哪些需要记录的信息)。 -
从而得到
ClassInfo(这里暂时用不到MethodInfo和IvarInfo)。 -
区分需要转化的对象是
NSDictionary还是NSArray。 -
将
NSDictionary中的Key与我们刚才记录在ClassPropertyInfo中的name进行对比。NSArray拆分成多个NSDictionary(或者String)做。暂时不支持
NSArray中又是NSArray对象。 -
将对比上的Key进行差异化赋值。

下面我们就来实现具体的步骤
Step
-
获取关键的
ClassPropertyInfo信息一条比较丰富的属性长这样:
@property (nonatomic, strong ,setter=setGroup: ,getter=group) NSArray<Student> * group;
可以看出这个地方对我们有用的有setter,getter,NSArray,Student和group,当然其中的nonatomic和strong也是一些有用的信息,但我们目前姑且不谈。
关于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中全部获取到
```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;
```
这里的这里name和value的定义可以参考:
name包括N,&,W,R,G,S,V,T。
这里面的G,S正好对应getter,setter,这两个比较好理解,都是对应SEL的name,不过这个这个时候通过value取出来的是一个char型字符串,这个要注意一下。比如getter就是"group",setter就是"setGroup:"。
T就稍稍复杂一点一些,这里的T就是@\"NSArray<Student>"\ (如果有两个protocol则是@\"NSArray<Student><Student2>),我们可以将它分为三部分@,NSArray和Student。其中NSArray是这个属性的Class,Student是对应的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>分成NSArray,Student,Student2这样的数组。
NSString *type = @"@\"NSArray<Student><Student2>";
NSMutableArray *values = [type componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"@\"<>,"]].mutableCopy;
[values removeObject:@""];
// 最终values = @[@"NSArray",@"Student",@"Student2"];
到这里关于一条Property最重要的一些信息我们都得到了:
-
cls:NSArray -
name:group -
type:@ -
getterSel:@selector(group) -
setterSel:@selector(setGroup:) -
protocols:Student
然后在补上一些能够让我们更方便使用的属性,比如:
* ```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对应的在NSDictionary中object。这句话读起来可能比较绕口:所以我们举个🌰:
还是上文定义的
PersonDataModel这个时候传入的NSDictionary是
{ "name": "小明","age": 18,"sex": "男"}
那么这个时候我们要找到的就是PersonDataModel中name为sex的ClassPropertyInfo和它对应的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"};
}
当我们轮询到TeacherDataModel的name为teacherId的ClassPropertyInfo时取出的NSDictionary中对应key为id的object。
-
将对比上的
name的DBClassPropertyInfo的和object进行差异化赋值。
这一步是逻辑最简单,但也是实现起来最繁琐的一步。
-
DBClassPropertyInfo中的type可以让我们知道这个property是什么类型,上文有讲述。 -
将
object转化为对应的property的类型这一步我们新建一个新的文件
DBValueTransformer来帮我们做这些数据的处理,并且在这一步我们也可以插入我们的一个协议(NSString->NSDate) -
再利用
objc_msgSend进行赋值
进行到这已经完成对一个NSDictionary->DataModel的全过程。
小结
虽然本文只是讲述了NSDictionary->DataModel的过程,没有其他的Model功能那么完善,如:
Model->Json-
Model比较 - 深拷贝
但我相信如果能看到这里的同学对其他功能应该是已经可以手到擒来了。
做事之前先理清楚思路,功能点全部归好类才能更好帮助我们完成它!
本文所有代码可以在这里找到:Demo










网友评论