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

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

本文轉載請註明出處 —— polobymulberry-博客園html


1. 前言


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

2. 存在問題及解決思路


2.1 沒有考慮JSON數據並不必定是NSDictionary類型

有時候JSON並不必定是NSDictionary類型,多是一個字符串,也多是NSData類型的數據。不過不論是哪一種類型,通通先將其轉化爲NSData數據,而後使用+[NSJSONSerialization JSONObjectWithData:options:error:]來轉化。因此我在initWithAttributes:上面又封裝了一層。github

- (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數據。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);
}

輸出結果也是正確的:app

image

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便可。具體解釋,你們請參考這個issuepost

這種設計我不是很贊同,我是有潔癖的人,要否則你就別定義YYModel這個protocol,說明文檔裏面着重說明一下就行。因此此處我仍是選擇判斷NSObject的子類是否遵循protocol,也就是說只有遵循了這個protocol,才能自定義property映射關係。測試

首先咱們看如何使用自定義propertyMapper。我先創建一個PJXUserPropertyMapper類,遵循了JSONProtocol協議,並實現了propertyMapper協議函數。ui

// 遵循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。atom

#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];
    }
    
    ......
}

這樣就能夠啦.咱們看看效果:

image

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);
}

你猜輸出啥?

image

其餘都正確,惟獨咱們的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);
    }
}

這下終於成功了:

image

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);
}

獲得的結果爲:

image
成功了.

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);
}

輸出結果以下:

image

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

3. 總結


以個人能力,目前只能將JSON轉化Model實現到這個地步了.整體來講,實現的難度不是很大(由於我考慮的狀況仍是比較少的,另外還有些功能沒添加),不過涉及的知識點仍是挺多的,挺不錯的一個練手項目:).

附上GitHub地址

相關文章
相關標籤/搜索