天天看點

【瘋狂造輪子-iOS】JSON轉Model系列之二

【瘋狂造輪子-iOS】JSON轉Model系列之二

本文轉載請注明出處 —— polobymulberry-部落格園

1. 前言

上一篇《【瘋狂造輪子-iOS】JSON轉Model系列之一》實作了一個簡陋的JSON轉Model的庫,不過還存在很多問題。下面我會嘗試一個個去解決。

2. 存在問題及解決思路

2.1 沒有考慮JSON資料并不一定是NSDictionary類型

有時候JSON并不一定是NSDictionary類型,可能是一個字元串,也可能是NSData類型的資料。不過不管是哪種類型,統統先将其轉化為NSData資料,然後使用+[NSJSONSerialization JSONObjectWithData:options:error:]來轉化。是以我在initWithAttributes:上面又封裝了一層。

- (instancetype)initWithJSONData:(id)json
{
    NSDictionary *dict = [self pjx_dictionaryWithJSON:json];
    return [self initWithAttributes:dict];
}

/**
 * @brief 将NSString和NSData格式的json資料轉化為NSDictionary類型
 */
- (NSDictionary *)pjx_dictionaryWithJSON:(id)json
{
    if (!json) {
        return nil;
    }
    // 若是NSDictionary類型,直接傳回
    if ([json isKindOfClass:[NSDictionary class]]) {
        return json;
    }
    
    NSDictionary *dict = nil;
    NSData *jsonData = nil;
    
    if ([json isKindOfClass:[NSString class]]) {
        // 如果是NSString,就先轉化為NSData
        jsonData = [(NSString*)json dataUsingEncoding:NSUTF8StringEncoding];
    } else if ([json isKindOfClass:[NSData class]]) {
        jsonData = json;
    }
    
    if (jsonData && [jsonData isKindOfClass:[NSData class]]) {
        // 如果時NSData類型,使用NSJSONSerialization
        NSError *error = nil;
        dict = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
        if (error) {
            NSLog(@"pjx_dictionaryWithJSON error:%@", error);
            return nil;
        }
        if (![dict isKindOfClass:[NSDictionary class]]) {
            return nil;
        }
    }
    
    return dict;
}      

為此,我在ViewController添加了兩個sample。分别用來解析NSString類型的JSON資料和NSData類型的JSON資料。

// NSString類型的JSON資料
- (void)runSimpleSample2
{
    NSString *userStr = @"                                                              \
                        {                                                               \
                            \"username\"       : \"shuaige\",                           \
                            \"password\"       : \"123456\",                            \
                            \"avatarImageURL\" : \"http://www.example.com/shuaige.png\" \
                        }";
    
    PJXUser *user = [[PJXUser alloc] initWithJSONData:userStr];
    
    NSLog(@"runSimpleSample2\n");
    NSLog(@"----------------------------------------");
    NSLog(@"username:%@\n",user.username);
    NSLog(@"password:%@\n",user.password);
    NSLog(@"avatarImageURL:%@\n",user.avatarImageURL);
}

// NSData類型的JSON資料
- (void)runSimpleSample3
{
    NSString *userInfoFilePath = [[NSBundle mainBundle] pathForResource:@"UserInfo" ofType:@"txt"];
    NSData *data = [NSData dataWithContentsOfFile:userInfoFilePath];
    PJXUser *user = [[PJXUser alloc] initWithJSONData:data];
    
    NSLog(@"runSimpleSample3\n");
    NSLog(@"----------------------------------------");
    NSLog(@"username:%@\n",user.username);
    NSLog(@"password:%@\n",user.password);
    NSLog(@"avatarImageURL:%@\n",user.avatarImageURL);
}      

輸出結果也是正确的:

2.2 沒有考慮使用者傳入的JSON資料的key值和property的名稱不一緻

我第一反應是使用一個映射表。也就是說使用者使用時需要自定義一套property和key的映射表。YYModel中使用了一個+ (NSDictionary *)modelCustomPropertyMapper函數,使用者可以自定義該函數達到映射表的效果,而這個函數是放在一個protocol中的。我挺認同這種設計的,因為modelCustomPropertyMapper這種函數和Model是一種組合關系,可有可無(optional),是以設計成協定更合适。但是作者在設計protocol又說了一句:

// There's no need to add '<YYModel>' to your class header.
@protocol YYModel <NSObject>      

什麼意思呢,就是說你自定義一個NSObject子類(如YYBook)時,如果想實作自定義的property映射關系,隻需要實作modelCustomPropertyMapper函數即可,而不需要寫成@interface YYBook : NSObject <YYModel>。作者的意思是你遵不遵循YYModel這個protocol都沒事,反正你隻要在YYBook實作了modelCustomPropertyMapper即可。具體解釋,大家請參考這個issue。

這種設計我不是很贊同,我是有潔癖的人,要不然你就别定義YYModel這個protocol,說明文檔裡面着重說明一下就行。是以此處我還是選擇判斷NSObject的子類是否遵循protocol,也就是說隻有遵循了這個protocol,才能自定義property映射關系。

首先我們看如何使用自定義propertyMapper。我先建立一個PJXUserPropertyMapper類,遵循了JSONProtocol協定,并實作了propertyMapper協定函數。

// 遵循JSONProtocol協定,這個JSONProtocol中定義的就是我的propertyMapper協定函數
@interface PJXUserPropertyMapper : NSObject <JSONProtocol>

@property (nonatomic, copy) NSString* username; // 使用者名
@property (nonatomic, copy) NSString* password; // 密碼
@property (nonatomic, copy) NSString* avatarImageURL; // 頭像的URL位址

@end

@implementation PJXUserPropertyMapper
// 實作propertyMapper這個協定方法
+ (NSDictionary *)propertyMapper
{
    return @{@"Username" : @"username",
             @"Password" : @"password",
             @"AvatarImageURL" : @"avatarImageURL"};
}

@end      

随後我定義了一個example。

#pragma mark - PropertyMapper Sample
- (void)runPropertyMapperSample
{
    NSDictionary *userDict = @{@"Username" : @"shuaige",
                               @"Password" : @"123456",
                               @"AvatarImageURL" : @"http://www.example.com/shuaige.png"};
    PJXUserPropertyMapper *user = [[PJXUserPropertyMapper alloc] initWithJSONData:userDict];
    
    NSLog(@"runPropertyMapperSample\n");
    NSLog(@"----------------------------------------");
    NSLog(@"username:%@\n",user.username);
    NSLog(@"password:%@\n",user.password);
    NSLog(@"avatarImageURL:%@\n",user.avatarImageURL);
}      

是不是感覺調用上和之前的非property映射沒什麼差別?那是因為我們需要在initWithJSONData中增加一些東西。

具體的做法是在PropertyWithDictionary函數增加了一個查表操作。

// 注意我傳入的dictionary就是使用者提供的JSON資料
// 比如此處傳入的key==@"username",value==@"shuaige"
static void PropertyWithDictionaryFunction(const void *key, const void *value, void *context)
{
    NSString *keyStr    = (__bridge NSString *)(key);
    
    ......       

    // 如果使用了JSONProtocol,并且自定義了propertyMapper,那麼還需要将keyStr轉化下
    if ([modelSelf conformsToProtocol:@protocol(JSONProtocol)] && [[modelSelf class] respondsToSelector:@selector(propertyMapper)]) {
        keyStr = [[[modelSelf class] propertyMapper] objectForKey:keyStr];
    }
    
    ......
}      

這樣就可以啦.我們看看效果:

2.3 沒有考慮JSON資料的value值不一定是NSString類型

開始的時候,挺擔心我這種寫法會不會不相容别的資料類型。不過我覺得應該沒什麼問題,畢竟我使用的setter方法本質上沒啥問題,我的類型全用id來代替了(事實上,我的想法大錯特錯):

((void (*)(id, SEL, id))(void *) objc_msgSend)(modelSelf, info.setter, setValue);      

不過本着不怕一萬,就怕萬一的心态。我還是做了一個example來試驗一下:

@interface PJXUserVariousType : NSObject

@property (nonatomic, copy) NSString *blogTitle; // 部落格标題
@property (nonatomic, strong) NSURL *blogURL; // 部落格網址
@property (nonatomic, assign) NSInteger blogIndex; // 部落格索引值
@property (nonatomic, strong) NSDate *postDate; // 部落格釋出時間
@property (nonatomic, strong) NSArray *friends; // 我的好友名稱
@property (nonatomic, strong) NSSet *collections; // 我的收藏

@end

@implementation PJXUserVariousType

@end

#pragma mark - VariousType Sample
- (void)runVariousTypeSample
{
    NSDictionary *userDict = @{@"blogTitle" : @"iOS developer",
                               @"blogURL" : @"http://www.example.com/blog.html",
                               @"blogIndex" : @666,
                               @"postDate" : [NSDate date],
                               @"friends" : @[@"meinv1", @"meinv2", @"meinv3"],
                               @"collections" : @[@"shuaige1", @"shuaige2", @"shuaige3"]};
    PJXUserVariousType *user = [[PJXUserVariousType alloc] initWithJSONData:userDict];
    
    NSLog(@"runVariousTypeSample\n");
    NSLog(@"----------------------------------------");
    NSLog(@"blogTitle:%@\n",user.blogTitle);
    NSLog(@"blogURL:%@\n",user.blogURL);
    NSLog(@"blogIndex:%ld\n",user.blogIndex);
    NSLog(@"postDate:%@\n",user.postDate);
    NSLog(@"friends:%@\n",user.friends);
    NSLog(@"collections:%@\n",user.collections);
}      

你猜輸出啥?

【瘋狂造輪子-iOS】JSON轉Model系列之二

其他都正确,唯獨我們的blogIndex出錯了。這裡确實是我欠考慮了,類似NSInteger,BOOL這些NSNumber類型(我暫時隻考慮這些常用類型)需要單獨處理一下。這一部分看起來容易,但是為了處理這種特殊情況确實要下很大功夫。比如你得先判斷該屬性是不是double或int這種類型,隻有判斷除了該屬性是double還是int,你才能正确使用setter方法,而此處的調用方式也要單獨寫一個,因為和之前調用方式有一些些差別,需要判斷Number的類型是double,是int,還是BOOl…….

對此我在PJXPropertyInfo中定義了兩個函數,一個叫isNumber,用來判斷該屬性是不是一個Number,另一個叫setNumberValue:withModelSelf:,用來給是Number類型的屬性指派。另外,我仿照YYModel(比YYModel簡化很多了)建了一個PJXEncodingType的enum類型,用來存儲Number的類型(int?double?BOOL?……),與之配套的還有一個PJXGetEncodingType函數,來擷取目前屬性的類型(是int?double?BOOL?),具體怎麼做還挺複雜的,後面會詳細說明。

代碼如下:

// Number類型
typedef NS_ENUM(NSUInteger, PJXEncodingType) {
    PJXEncodingTypeUnknown    = 0, ///< unknown
    PJXEncodingTypeBool       = 1, ///< bool
    PJXEncodingTypeInt8       = 2, ///< char / BOOL
    PJXEncodingTypeUInt8      = 3, ///< unsigned char
    PJXEncodingTypeInt16      = 4, ///< short
    PJXEncodingTypeUInt16     = 5, ///< unsigned short
    PJXEncodingTypeInt32      = 6, ///< int
    PJXEncodingTypeUInt32     = 7, ///< unsigned int
    PJXEncodingTypeInt64      = 8, ///< long long
    PJXEncodingTypeUInt64     = 9, ///< unsigned long long
    PJXEncodingTypeFloat      = 10, ///< float
    PJXEncodingTypeDouble     = 11, ///< double
    PJXEncodingTypeLongDouble = 12, ///< long double
};

// 根據objc_property_attribute_t可以擷取到property的類型PJXEncodingType 
// 參考YYModel
PJXGetEncodingType(const char *encodingType) {
    char *type = (char *)encodingType;
    if (!type) return PJXEncodingTypeUnknown;
    size_t len = strlen(type);
    if (len == 0) return PJXEncodingTypeUnknown;
    
    switch (*type) {
        case 'B': return PJXEncodingTypeBool;
        case 'c': return PJXEncodingTypeInt8;
        case 'C': return PJXEncodingTypeUInt8;
        case 's': return PJXEncodingTypeInt16;
        case 'S': return PJXEncodingTypeUInt16;
        case 'i': return PJXEncodingTypeInt32;
        case 'I': return PJXEncodingTypeUInt32;
        case 'l': return PJXEncodingTypeInt32;
        case 'L': return PJXEncodingTypeUInt32;
        case 'q': return PJXEncodingTypeInt64;
        case 'Q': return PJXEncodingTypeUInt64;
        case 'f': return PJXEncodingTypeFloat;
        case 'd': return PJXEncodingTypeDouble;
        case 'D': return PJXEncodingTypeLongDouble;

        default: return PJXEncodingTypeUnknown;
    }
}

/**
 * @brief 存儲Model中每個property的資訊
 * ......
 * @param type 是一個PJXEncodingType類型變量,為了存儲該屬性是哪種Number(int?double?BOOL?)
 */
@interface PJXPropertyInfo : NSObject
......
@property (nonatomic, assign) PJXEncodingType type;
@end

@implementation PJXPropertyInfo

- (instancetype)initWithPropertyInfo:(objc_property_t)property
{
    self = [self init];
    
    if (self) {
        ......
        
        // 判斷屬性類型
        unsigned int attrCount;
        // 關于objc_property_attribute_t,這裡有一篇文章介紹的很好
        // http://www.henishuo.com/runtime-property-ivar/
        objc_property_attribute_t *attrs = property_copyAttributeList(property, &attrCount);
        for (unsigned int i = 0; i < attrCount; i++) {
            switch (attrs[i].name[0]) {
                case 'T': {//  EncodingType
                    if (attrs[i].value) {
                        //NSLog(@"attrs[%d].value = %s", i, attrs[i].value);
                        // 可以根據value擷取到property類型
                        _type = PJXGetEncodingType(attrs[i].value);
                    }
                    break;
                }
                default:
                    break;
            }
        }
        ......
    }
    
    return self;
}

// 根據propertyInfo中存儲的type判斷其是否為Number
- (BOOL)isNumber
{
    switch (self.type) {
        case PJXEncodingTypeBool:
        case PJXEncodingTypeInt8:
        case PJXEncodingTypeUInt8:
        case PJXEncodingTypeInt16:
        case PJXEncodingTypeUInt16:
        case PJXEncodingTypeInt32:
        case PJXEncodingTypeUInt32:
        case PJXEncodingTypeInt64:
        case PJXEncodingTypeUInt64:
        case PJXEncodingTypeFloat:
        case PJXEncodingTypeDouble:
        case PJXEncodingTypeLongDouble:
            return YES;
        default:
            return NO;
            break;
    }
}

// 使用objc_msgSend調用modelSelf中該屬性對應的setter方法
- (void)setNumberValue:(NSNumber *)number withModelSelf:(id)modelSelf
{
    switch (self.type) {
        case PJXEncodingTypeBool:
            ((void (*)(id, SEL, BOOL))(void *) objc_msgSend)(modelSelf, self.setter, number.boolValue);
            break;
        case PJXEncodingTypeInt8:
            ((void (*)(id, SEL, BOOL))(void *) objc_msgSend)(modelSelf, self.setter, number.charValue);
            break;
        case PJXEncodingTypeUInt8:
            ((void (*)(id, SEL, BOOL))(void *) objc_msgSend)(modelSelf, self.setter, number.unsignedCharValue);
            break;
        case PJXEncodingTypeInt16:
            ((void (*)(id, SEL, BOOL))(void *) objc_msgSend)(modelSelf, self.setter, number.shortValue);
            break;
        case PJXEncodingTypeUInt16:
            ((void (*)(id, SEL, BOOL))(void *) objc_msgSend)(modelSelf, self.setter, number.unsignedShortValue);
            break;
        case PJXEncodingTypeInt32:
            ((void (*)(id, SEL, BOOL))(void *) objc_msgSend)(modelSelf, self.setter, number.intValue);
            break;
        case PJXEncodingTypeUInt32:
            ((void (*)(id, SEL, BOOL))(void *) objc_msgSend)(modelSelf, self.setter, number.unsignedIntValue);
            break;
        case PJXEncodingTypeInt64:
            ((void (*)(id, SEL, uint64_t))(void *) objc_msgSend)(modelSelf, self.setter, number.longLongValue);
            break;
        case PJXEncodingTypeUInt64:
            ((void (*)(id, SEL, uint64_t))(void *) objc_msgSend)(modelSelf, self.setter, number.unsignedLongLongValue);
            break;
        case PJXEncodingTypeFloat:
            ((void (*)(id, SEL, float))(void *) objc_msgSend)(modelSelf, self.setter, number.floatValue);
            break;
        case PJXEncodingTypeDouble:
            ((void (*)(id, SEL, double))(void *) objc_msgSend)(modelSelf, self.setter, number.doubleValue);
            break;
        case PJXEncodingTypeLongDouble:
            ((void (*)(id, SEL, long double))(void *) objc_msgSend)(modelSelf, self.setter, number.doubleValue);
            break;
        default:
            break;
    }
}

@end      

有了上述的幾個方法,後面就好辦了,隻需在PropertyWithDictionaryFunction函數中添加一個Number的判斷就行:

static void PropertyWithDictionaryFunction(const void *key, const void *value, void *context)
{
    ......
    
    // 如果該屬性是Number,那麼就用Number指派方法給其指派
    if ([info isNumber]) {
        [info setNumberValue:setValue withModelSelf:modelSelf];
    } else {
        ((void (*)(id, SEL, id))(void *) objc_msgSend)(modelSelf, info.setter, setValue);
    }
}      

這下終于成功了:

【瘋狂造輪子-iOS】JSON轉Model系列之二

2.4 沒有考慮使用者自定義了Model屬性的setter方法

這個其實比較簡單,隻需要對property的attribute(objc_property_attribute_t)進行判斷即可:

- (instancetype)initWithPropertyInfo:(objc_property_t)property
{
        ......
        
        BOOL isCustomSetter = NO;
        // 判斷屬性類型
        unsigned int attrCount;
        // 關于objc_property_attribute_t,這裡有一篇文章介紹的很好
        // http://www.henishuo.com/runtime-property-ivar/
        objc_property_attribute_t *attrs = property_copyAttributeList(property, &attrCount);
        for (unsigned int i = 0; i < attrCount; i++) {
            switch (attrs[i].name[0]) {
                case 'T': { // EncodingType
                    if (attrs[i].value) {
                        //NSLog(@"attrs[%d].value = %s", i, attrs[i].value);
                        // 可以根據value擷取到property類型
                        _type = PJXGetEncodingType(attrs[i].value);
                    }
                    break;
                }
                case 'S': { // 自定義setter方法
                    if (attrs[i].value) {
                        isCustomSetter = YES;
                        _setter = NSSelectorFromString([NSString stringWithUTF8String:attrs[i].value]);
                    }
                } break;
                default:
                    break;
            }
        }
        
        if (!isCustomSetter) {
            // 如果沒有自定義setter方法,隻考慮系統預設生成setter方法
            // 也就是說屬性username的setter方法為setUsername:
            NSString *setter = [NSString stringWithFormat:@"%@%@", [_name substringToIndex:1].uppercaseString, [_name substringFromIndex:1]];
            _setter = NSSelectorFromString([NSString stringWithFormat:@"set%@:", setter]);
        }
    }
    
    return self;
}      

使用下面這個例子測試:

@interface PJXUserCustomSetter : NSObject

@property (nonatomic, copy, setter=setCustomUserName:) NSString* username; // 使用者名
@property (nonatomic, copy, setter=setCustomBirthday:) NSDate* birthday; // 生日

@end

@implementation PJXUserCustomSetter

- (void)setCustomUserName:(NSString *)username
{
    _username = [NSString stringWithFormat:@"My name is %@", username];
}

- (void)setCustomBirthday:(NSDate *)birthday
{
    NSTimeInterval timeInterval = 24*60*60; // 過一天
    _birthday = [NSDate dateWithTimeInterval:timeInterval sinceDate:birthday];
}

@end

#pragma mark - Custom Setter Sample
- (void)runCustomSetterSample
{
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
    NSDate *birthday = [dateFormatter dateFromString:@"2016-04-07 00:20:03"];
    NSDictionary *userDict = @{@"username" : @"shuaige",
                               @"birthday" : birthday};
    PJXUserCustomSetter *user = [[PJXUserCustomSetter alloc] initWithJSONData:userDict];
    
    NSLog(@"runCustomSetterSample\n");
    NSLog(@"----------------------------------------");
    NSLog(@"username:%@\n",user.username);
    NSLog(@"birthday:%@\n",user.birthday);
}      

得到的結果為:

【瘋狂造輪子-iOS】JSON轉Model系列之二

成功了.

2.5 沒有考慮使用者傳入的JSON資料有嵌套

我個人感覺這個應該沒什麼問題,為什麼這麼說呢?因為我嵌套的無非也是一個NSObject類型,那麼就調用其自身的setter方法就OK啊.不過還是以防萬一,我構造了一下案例:

@interface PJXBlog : NSObject

@property (nonatomic, copy) NSString *title; // 部落格名稱
@property (nonatomic, strong) NSDate *postDate; // 部落格發表日期
@property (nonatomic, copy) PJXUser *author; // 部落格作者

@end

@implementation PJXBlog

@end

#pragma mark - Nest Sample
- (void)runNestSample
{
    NSDictionary *blogDict = @{@"title" : @"how to convert JSON to Model?",
                               @"postDate" : [NSDate date],
                               @"author" : @{@"username" : @"shuaige",
                                             @"password" : @"123456",
                                             @"avatarImageURL":@"http://www.example.com/shuaige.png"}};
    PJXBlog *blog = [[PJXBlog alloc] initWithJSONData:blogDict];
    
    NSLog(@"runNestSample\n");
    NSLog(@"----------------------------------------");
    NSLog(@"title:%@\n",blog.title);
    NSLog(@"postDate:%@\n",blog.postDate);
    NSLog(@"author:%@\n",blog.author);
}      

輸出結果如下:

【瘋狂造輪子-iOS】JSON轉Model系列之二

結果沒什麼問題.不過這樣說可能不是很負責任,但是目前我也想不到反例.暫時先當做成功了.

3. 總結

以我的能力,目前隻能将JSON轉化Model實作到這個地步了.總體來說,實作的難度不是很大(因為我考慮的情況還是比較少的,另外還有些功能沒添加),不過涉及的知識點還是挺多的,挺不錯的一個練手項目:).

附上GitHub位址。