iOS开发技巧iOS开发常用iOS开发技术

YYModel 学习

2017-02-13  本文已影响2631人  勇往直前888

如何集成?

  • 一些可用的API,都在文件“NSObject+YYModel.h”中,注释还是比较详细的。是NSObject的类别,所以对于自定义的Model可以直接使用。这个和JSONModel需要一个固定基类相比在用法上要简洁一点。
  • 定义也在文件“NSObject+YYModel.h”中,却是对NSArray和NSDictionary的类别。提供了方便方法,对于集合中的元素进行JSON -》Model的转化,将JSON对象的集合转变为Model的集合。

基本用法

JSON -》Model

Model -》JSON

实际的例子

YYModel

JSON对象

// JSON:
{
    "uid":123456,
    "name":"Harry",
    "created":"1965-07-31T00:00:00+0000"
}

Model定义

// Model:
@interface User : NSObject
@property UInt64 uid;
@property NSString *name;
@property NSDate *created;
@end

@implementation User
@end

JSON -》Model

// 将 JSON (NSData,NSString,NSDictionary) 转换为 Model:
User *user = [User yy_modelWithJSON:json];

Model -》JSON

// 将 Model 转换为 JSON 对象:
NSDictionary *json = [user yy_modelToJSONObject];

NSArray JSON -》Model

对于JSON对象数组的情况,[{"id":12345, "name":"Mary", "created":"1965-07-31T00:00:00+0000"}, {"id":12346, "name":"Joe", "created":"1970-07-31T00:08:15+0000"}],对数组中的成员进行转换,得到一个Model的数组[user1, user2]

/**
 Provide some data-model method for NSArray.
 */
@interface NSArray (YYModel)

/**
 Creates and returns an array from a json-array.
 This method is thread-safe.
 
 @param cls  The instance's class in array.
 @param json  A json array of `NSArray`, `NSString` or `NSData`.
              Example: [{"name":"Mary"},{"name":"Joe"}]
 
 @return A array, or nil if an error occurs.
 */
+ (nullable NSArray *)yy_modelArrayWithClass:(Class)cls json:(id)json;

@end

NSDictionary JSON -》Model

对于JSON对象字典的情况,{"user1":{"id":12345, "name":"Mary", "created":"1965-07-31T00:00:00+0000"}, "user2": {"id":12346, "name":"Joe", "created":"1970-07-31T00:08:15+0000"}}, 对字典中的值进行转换,key保持不变,得到一个值为Model的新字典{"user1" : user1, "user2" : user2}

/**
 Provide some data-model method for NSDictionary.
 */
@interface NSDictionary (YYModel)

/**
 Creates and returns a dictionary from a json.
 This method is thread-safe.
 
 @param cls  The value instance's class in dictionary.
 @param json  A json dictionary of `NSDictionary`, `NSString` or `NSData`.
              Example: {"user1" : {"name" : "Mary"}, "user2" : {name : "Joe"}}
 
 @return A dictionary, or nil if an error occurs.
 */
+ (nullable NSDictionary *)yy_modelDictionaryWithClass:(Class)cls json:(id)json;
@end

特殊情况处理

通过一个协议,方便让使用者来指定一些特殊情况的处理方法。这些方法都是可选的。

/**
 If the default model transform does not fit to your model class, implement one or
 more method in this protocol to change the default key-value transform process.
 There's no need to add '<YYModel>' to your class header.
 */
@protocol YYModel <NSObject>
@optional
@end

不需要加 <YYModel>是因为实现者(代理)是Model自己。这是特殊的用法。

1. Model 属性名和 JSON 中的 Key 不相同

协议方法:

+ (nullable NSDictionary<NSString *, id> *)modelCustomPropertyMapper;

JSON对象:

// JSON:
{
    "n":"Harry Pottery",
    "p": 256,
    "ext" : {
        "desc" : "A book written by J.K.Rowing."
    },
    "ID" : 100010
}

Model定义:

// Model:
@interface Book : NSObject
@property NSString *name;
@property NSInteger page;
@property NSString *desc;
@property NSString *bookID;
@end
@implementation Book
//返回一个 Dict,将 Model 属性名对映射到 JSON 的 Key。
+ (NSDictionary *)modelCustomPropertyMapper {
    return @{@"name" : @"n",
             @"page" : @"p",
             @"desc" : @"ext.desc",
             @"bookID" : @[@"id",@"ID",@"book_id"]};
}
@end

2. 容器类属性(NSArray/NSSet/NSDictionary)

协议方法:

+ (nullable NSDictionary<NSString *, id> *)modelContainerPropertyGenericClass;

Model定义:

@class Shadow, Border, Attachment;

@interface Attributes
@property NSString *name;
@property NSArray *shadows; //Array<Shadow>
@property NSSet *borders; //Set<Border>
@property NSMutableDictionary *attachments; //Dict<NSString,Attachment>
@end

@implementation Attributes
// 返回容器类中的所需要存放的数据类型 (以 Class 或 Class Name 的形式)。
+ (NSDictionary *)modelContainerPropertyGenericClass {
    return @{@"shadows" : [Shadow class],
             @"borders" : Border.class,
             @"attachments" : @"Attachment" };
}
@end

其他的特性比如黑白名单,转换后的校验,自定义字典key对应的类等功能不是很常用。
Model中包含Model的方式是支持的,不需要特殊指定。对顶级类NSObeject加类别,优势就体现出来了。

对比测试

#define YYModelSynthCoderAndHash \
- (void)encodeWithCoder:(NSCoder *)aCoder { [self yy_modelEncodeWithCoder:aCoder]; } \
- (id)initWithCoder:(NSCoder *)aDecoder { return [self yy_modelInitWithCoder:aDecoder]; } \
- (id)copyWithZone:(NSZone *)zone { return [self yy_modelCopy]; } \
- (NSUInteger)hash { return [self yy_modelHash]; } \
- (BOOL)isEqual:(id)object { return [self yy_modelIsEqual:object]; }

在实际使用中,用上面这个宏,不鼓励也不反对。
根据实际情况,实现上面的函数。使用YYModel后,只要一句话调用就好了,带来了比较大的便利。

begin = CACurrentMediaTime();
@autoreleasepool {
    for (int i = 0; i < count; i++) {
        NSDictionary *json = [user yy_modelToJSONObject];
        [holder addObject:json];
    }
}
end = CACurrentMediaTime();

数据结构

iOS开发-Runtime详解
Objective-C Runtime 运行时之三:方法与消息
Objective-C中的一些特殊的数据类型 id、nil、Nil、SEL

id

在文件objc/objc.h

/// A pointer to an instance of a class.
typedef struct objc_object *id;

/// Represents an instance of a class.
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

Class

在文件objc/objc.h

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

在文件objc/runtime.h

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

注意:OBJC2_UNAVAILABLE是一个Apple对Objc系统运行版本进行约束的宏定义,主要为了兼容非Objective-C 2.0的遗留版本,但我们仍能从中获取一些有用信息。
OC之OBJC2_UNAVAILABLE

SEL

在文件objc/objc.h

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

IMP

在文件objc/objc.h

/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id (*IMP)(id, SEL, ...); 
#endif

Method

在文件objc/runtime.h

/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;

struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
}

方法:SEL(函数名)、IMP(函数实现)、method_types(参数)的统一体。

Ivar

在文件objc/runtime.h

/// An opaque type that represents an instance variable.
typedef struct objc_ivar *Ivar;

struct objc_ivar {
    char *ivar_name                                          OBJC2_UNAVAILABLE;
    char *ivar_type                                          OBJC2_UNAVAILABLE;
    int ivar_offset                                          OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
}

objc_property_t

在文件objc/runtime.h

/// An opaque type that represents an Objective-C declared property.
typedef struct objc_property *objc_property_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;

Meta Class

YYClassInfo文件

根据原生的数据结构,进行的类抽象。

YYEncodingType

YYClassIvarInfo

ptrdiff_t是C/C++标准库中定义的一个与机器相关的数据类型。ptrdiff_t类型变量通常用来保存两个指针减法操作的结果。ptrdiff_t定义在stddef.h(cstddef)这个文件内。ptrdiff_t通常被定义为long int类型。
ptrdiff_t定义在C99标准中。

YYClassMethodInfo

YYClassPropertyInfo

if (_name.length) {
        if (!_getter) {
            _getter = NSSelectorFromString(_name);
        }
        if (!_setter) {
            _setter = NSSelectorFromString([NSString stringWithFormat:@"set%@%@:", [_name substringToIndex:1].uppercaseString, [_name substringFromIndex:1]]);
        }
    }

YYClassInfo

static CFMutableDictionaryRef classCache;
static CFMutableDictionaryRef metaCache;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    classCache = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    metaCache = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    lock = dispatch_semaphore_create(1);
});
if (info) {
    dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
    CFDictionarySetValue(info.isMeta ? metaCache : classCache, (__bridge const void *)(cls), (__bridge const void *)(info));
    dispatch_semaphore_signal(lock);
}

NSObject+YYModel文件

YYEncodingNSType

YYNSDateFromString

typedef NSDate* (^YYNSDateParseBlock)(NSString *string);
#define kParserNum 34
static YYNSDateParseBlock blocks[kParserNum + 1] = {0};
static dispatch_once_t onceToken;
....
YYNSDateParseBlock parser = blocks[string.length];
if (!parser) return nil;
return parser(string);
#undef kParserNum

_YYModelPropertyMeta

_YYModelMeta

// Create all property metas.
NSMutableDictionary *allPropertyMetas = [NSMutableDictionary new];
YYClassInfo *curClassInfo = classInfo;
while (curClassInfo && curClassInfo.superCls != nil) { // recursive parse super class, but ignore root class (NSObject/NSProxy)
    for (YYClassPropertyInfo *propertyInfo in curClassInfo.propertyInfos.allValues) {
        if (!propertyInfo.name) continue;
        if (blacklist && [blacklist containsObject:propertyInfo.name]) continue;
        if (whitelist && ![whitelist containsObject:propertyInfo.name]) continue;
        _YYModelPropertyMeta *meta = [_YYModelPropertyMeta metaWithClassInfo:classInfo
                                                                propertyInfo:propertyInfo
                                                                     generic:genericMapper[propertyInfo.name]];
        if (!meta || !meta->_name) continue;
        if (!meta->_getter || !meta->_setter) continue;
        if (allPropertyMetas[meta->_name]) continue;
        allPropertyMetas[meta->_name] = meta;
    }
    curClassInfo = curClassInfo.superClassInfo;
}
if (allPropertyMetas.count) _allPropertyMetas = allPropertyMetas.allValues.copy;

ModelSetContext

typedef struct {
    void *modelMeta;  ///< _YYModelMeta
    void *model;      ///< id (self)
    void *dictionary; ///< NSDictionary (json)
} ModelSetContext;

函数调用流程

JSON -》Model

  1. 起点:+ (nullable instancetype)yy_modelWithJSON:(id)json;
  2. 将JSON对象转换为字典:+ (NSDictionary *)_yy_dictionaryWithJSON:(id)json

这里用到了系统的JSON转字典API:+ (nullable id)JSONObjectWithData:(NSData *)data options:(NSJSONReadingOptions)opt error:(NSError **)error;

  1. 字典转模型:+ (nullable instancetype)yy_modelWithDictionary:(NSDictionary *)dictionary;
  2. 设置模型属性:- (BOOL)yy_modelSetWithDictionary:(NSDictionary *)dic;
  3. 利用函数CFDictionaryApplyFunction调用static void ModelSetWithDictionaryFunction(const void *_key, const void *_value, void *_context);
    或者,利用函数CFArrayApplyFunction调用static void ModelSetWithPropertyMetaArrayFunction(const void *_propertyMeta, void *_context);
  4. 上面两个函数,都调用全局函数:static void ModelSetValueForProperty(__unsafe_unretained id model, __unsafe_unretained id value, __unsafe_unretained _YYModelPropertyMeta *meta); 所有的实际工作都在这里

Model -》JSON

id的类型是NSDictionary or NSArray,根据实际情况指定

都是基于- (nullable id)yy_modelToJSONObject;的结果做格式转换,这里用到了系统API:+ (nullable NSData *)dataWithJSONObject:(id)obj options:(NSJSONWritingOptions)opt error:(NSError **)error;

几个知识点

objc_msgSend

//objc_msgSend(self,selector,@"test");
((void(*)(id, SEL, id))objc_msgSend)(self, selector, @"test");```
((void (*)(id, SEL, bool))(void *) objc_msgSend)((id)model, meta->_setter, num.boolValue);```

double num = ((long double (*)(id, SEL))(void *) objc_msgSend)((id)model, meta->_getter);```

__bridge

ModelSetContext context = {0};
context.modelMeta = (__bridge void *)(modelMeta);
context.model = (__bridge void *)(self);
context.dictionary = (__bridge void *)(dic);

ModelSetContext是一个C的struct

ModelSetValueForProperty

作用:将JSON(根式是Dictionary)的value(类型是id)赋值给Model的属性
方式:通过setter函数来实现,objc_msgSendSEL参数基本上都是meta->_setter

case1:数字类型

case2:Foundation类型

  1. Model属性类型是NSString或者NSMutableString;对value(id)的具体类型做了容错处理:
    ** NSString:直接设置
    ** NSMutableString:
    转化为NSString之后,调用mutableCopy
    NSNumber:取属性stringValue
    ** NSData:**转化为NSString
    NSURL:取属性absoluteString
    NSAttributedString:取属性string

  2. Model属性类型是NSDecimalNumber;对value(id)的类型是NSDecimalNumberNSNumberNSString的情况做了容错处理

  3. Model属性类型是NSData;对value(id)的类型是NSString的情况做了容错处理

  4. Model属性类型是NSDate;对value(id)的类型是NSString的情况做了容错处理

  5. Model属性类型是NSURL;对value(id)的类型是NSString的情况做了容错处理

  6. Model属性类型是NSDictionaryNSSetNSArray等容器类型时,对容器中每个成员调用函数yy_modelSetWithDictionary,一层层深入下去。

case3:其他类型

都对value为nil的情况做了处理

  1. Model属性类型是id对象类型时,调用函数yy_modelSetWithDictionary,一层层深入下去。

  2. Model属性类型是Class类型时,如果value是NSString,则调用函数NSClassFromString进行转化,然后设置。如果是其他类型,则判断其“元类”class_isMetaClass是否存在。存在,则直接设置

  3. Model属性类型是SEL类型时,如果value是NSString,则调用函数NSSelectorFromString进行转化,然后设置。

  4. Model属性类型是Block类型时,将value强制转换为void (^)()进行设置。

  5. Model属性类型是structunionchar[10]等类型时,如果value是NSValue,则调用函数- (void)setValue:(nullable id)value forKey:(NSString *)key;进行设置。

  6. Model属性类型是void*char*等类型时,如果value是NSValue,则将value强制转换为NSValue,取属性pointerValue进行设置。进行设置。

个人意见

  1. 看上去只有两个文件,但是类有很多,用了很多的内部类。这种方式不是很认同。还是推荐一个类一个源文件的方式。当然,这里的场景是高内聚的一个整体,本来也是把一个类的各子成员(都是struct),还是比较合适的。
  2. 将所有的头文件都归总为一个YYMode.h,这种方式是非常好的,推荐使用。
  3. 对于NSArray这种集合提供方便方法;对于JSON对象,采用id类型,支持NSDictionary, NSString, NSData三种类型;在处理的时候,统一为NSDictionary。这种方式,统筹考虑了实现和使用的方便性,只是增加了几层函数调用,值得推荐。
  4. 协议的定义、使用者、实现者都是同一个(self,Model自己),这里是特殊的使用场景。
    一般情况下应该分3个文件(协议定义,使用者,实现者)或者2个文件(协议的定义放在使用者的文件中)。
  5. 采用协议的设计方式,让使用者对特殊使用场景做自定义,值得推荐
  6. 将_YYModelMeta(runtime中各种struct对应的类)作为转换的中间载体,思维很巧妙
  7. if后面只有一个语句,省略了{},这种习惯不是很好。
    if (num) [num class]; // hold the number

参考文章

YYModel
iOS JSON 模型转换库评测
JSON 数据格式
Objective-C Runtime Programming Guide

上一篇下一篇

猜你喜欢

热点阅读