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

来源:http://www.cnblogs.com/polobymulberry/archive/2016/03/31/5328630.html
-Advertisement-
Play Games

【瘋狂造輪子-iOS】JSON轉Model系列之一 本文轉載請註明出處 —— polobymulberry-博客園 1. 前言 之前一直看別人的源碼,雖然對自己提升比較大,但畢竟不是自己寫的,很容易遺忘。這段時間準備自己造一些輪子,主要目的還是為了提升自身實力,總不能一遇到問題就Google。 之前 ...


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


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

1. 前言


之前一直看別人的源碼,雖然對自己提升比較大,但畢竟不是自己寫的,很容易遺忘。這段時間準備自己造一些輪子,主要目的還是為了提升自身實力,總不能一遇到問題就Google悲傷

之前寫i博客園客戶端的時候,經常會遇到JSON數據轉Model的功能。一般遇到這種問題我都是自己在對應Model類中定義一個+ (instance)initWithAttributes:(NSDictionary *)attributes函數來將NSDictionary*數據轉化為對應Model。

下麵是i博客園中ICUser的部分代碼,其中就使用了initWithAttributes。

// ICUser.h
#import <Foundation/Foundation.h>

extern NSString *const kUserId;
extern NSString *const kUserBlogId;
extern NSString *const kUserDisplayName;
extern NSString *const kUserAvatarURL;
@interface ICUser : NSObject

@property (nonatomic, copy)     NSString    *userId;
@property (nonatomic, assign)   NSInteger   blogId;
@property (nonatomic, copy)     NSString    *displayName;
@property (nonatomic, strong)   NSURL       *avatarURL;
+ (instancetype)initWithAttributes:(NSDictionary *
)attributes;

@end

// ICUser.m
#import "ICUser.h"

NSString *const kUserId                 = @"UserId";
NSString *const kUserBlogId             = @"BlogId";
NSString *const kUserDisplayName        = @"DisplayName";
NSString *const kUserAvatarURL          = @"Avatar";
@implementation ICUser

+ (instancetype)initWithAttributes:(NSDictionary *
)attributes
{
    ICUser *user = [[ICUser alloc] init];
    user.userId             = attributes[kUserId];
    user.blogId             = [attributes[kUserBlogId] integerValue];
    user.displayName        = attributes[kUserDisplayName];
    user.avatarURL          = [NSURL URLWithString:attributes[kUserAvatarURL]];
return user;
}

@end

如果我們需要處理的情況符合下麵兩個要求:

  1. Model類的個數比較少
  2. 每個Model的成員不是很複雜

這種情況下使用上述方法還可以接受。但是一旦Model這一層急劇膨脹,這時候就會讓人苦不堪言:

  1. initWithAttributes函數容易寫錯,而且出錯後不方便排查。
  2. 機械性的代碼會比較多,不利於提高效率。

考慮到手動轉JSON為Model的種種不便,我決定自己寫一個JSON轉Model的庫。雖然網上已經有很多這方面的第三方庫,但是我還是想自己造輪子,目的是為了更深入地學習iOS

2. 設計思路


1.首先要考慮到輸入輸出是什麼?

輸入:NSDictionary類型的數據

這裡我們先從簡,一般我們使用到解析JSON的場合是在網路請求。伺服器端返回JSON格式的數據,我們需要轉化成本地的Model(此處不討論直接使用NSDictionary好還是轉化為Model好)。並且本篇文章只假設我們網路請求獲取到的JSON數據已經在客戶端處理成了NSDictionary類型的數據(比較常見)。

輸出:Model類型的數據

Model類型的數據。

舉例:

目前我實現的一個簡單的例子:

#pragma mark - PJXUser
@interface PJXUser : NSObject
@property (nonatomic, copy) NSString* username; // 用戶名
@property (nonatomic, copy) NSString* password; // 密碼
@property (nonatomic, copy) NSString* avatarImageURL; // 頭像的URL地址
@end

- (void)runSimpleSample
{
    NSDictionary *userDict = @{@"username"      :@"shuaige",
                               @"password"      :@"123456",
                               @"avatarImageURL":@"http://www.example.com/shuaige.png"};
    
    PJXUser *user = [[PJXUser alloc] initWithAttributes:userDict];;
    
    NSLog(@"username:%@\n",user.username);
    NSLog(@"password:%@\n",user.password);
    NSLog(@"avatarImageURL:%@\n",user.avatarImageURL);
}

這個例子的輸入就是userDict這個NSDictionary數據,輸出則是一個PJXUser類的對象user。不知道大家有沒有註意到,attributes中的key必須和Model中的property的名稱一致,比如上例中PJXUser的username、password等屬性(當然,你可以使用一個映射表解決這個問題,不過我們先暫時不想那麼多)。

2. 核心演算法怎麼做(輸入轉輸出)?

核心演算法部分其實就是調用initWithAttributes:這個函數。那這個函數該如何設計呢?

既然我們需要所有的Model類都可以調用這個initWithAttributes:來完成JSON轉Model的工作。那麼我們首先想到的就是將這個函數添加到NSObject的category中,並要求所有Model類都繼承自NSObject。

所以我首先新建了一個NSObject+Extension的category。併在其中添加了- (instancetype)initWithAttributes:(NSDictionary *)attributes方法。下麵我簡單闡述下該函數的實現。

其實我的實現思路基本參照的YYKit(傳送門)中的YYModel(傳送門)部分其最核心的部分就是調用Model中每個屬性的setter方法,並且將傳入的attributes中每個元素的value作為setter的參數

好的,到此為止最核心的部分已經講完了。可能大家會有很多疑問,比如說如何獲取到屬性的setter方法,獲取後又如何調用setter方法,畢竟此時的操作是在NSObject這個父類中進行的,並沒有具體子類的信息。這裡我簡單提一下,既然編譯期我們無法解決上述問題,那麼我們就需要藉助於OC的runtime機制了。當然,下麵會具體講解如何實現。

3. 具體實現


根據上面的核心思路,我覺得實現起來還存在一些問題:

如何獲取每個屬性的setter方法?如果現在獲取到了每個屬性的setter方法(註意是SEL類型),怎麼給每個屬性調用此方法?

現在是在NSObject中操作,所以不指望使用obj.username = attributes[@"username"]。所以需要使用runtime中的objc_msgSend,使用方法舉例如下:

((void (*)(id, SEL, id))(void *) objc_msgSend)((id)self, NSSelectorFromString(@"setUsername:"), @"shuaige");

可以看到我們只需要把其中的@"setUsername"和@"shuaige"替換成我們自己的變數就行。具體怎麼替換呢?這時候我們就需要創建一些數據結構來處理和保存相關的屬性信息。當然,這些數據結構也是我在實現過程中不斷修正的結果。至於中間如何修正,就不細說了,直接上結果。

數據結構的構建其實也很符合我們的思考習慣。既然我們需要對某個類進行處理,不可避免的,我們需要新建一個類來存儲Class信息(PJXClassInfo),而每個Class是由property、ivar和method組成的,所以針對不同組成,我們需要定義三個類來存儲property、ivar、method。但是此處我們只需要property信息,所以只建立了property相關的類(PJXPropertyInfo)。

我首先創建了一個PJXClassInfo的類。這個類目前只存放了一個NSMutableDictionary類型的propertyInfos屬性,該屬性是用來存儲這個Class的property信息。而propertyInfos中每個元素其實就是一個個PJXPropertyInfo對象。而每個PJXPropertyInfo保存的就是property的name,setter方法等等,當然,後期會根據需求為PJXPropertyInfo添加新的屬性。

這兩個類的關係如下:

image

下麵我們看看具體代碼如何實現,如下:

PJXPropertyInfo代碼

/**
 * @brief 存儲Model中每個property的信息
 * @param property 是一個objc_property_t類型變數
 * @param name 表示該property的名稱
 * @param setter 是一個SEL類型變數,表示該property的setter方法
 */
@interface PJXPropertyInfo : NSObject
@property (nonatomic, assign) objc_property_t property;
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) SEL setter;
@end

@implementation PJXPropertyInfo

- (instancetype)initWithPropertyInfo:(objc_property_t)property
{
    self = [self init];
    
    if (self) {
        // 以備不時之需
        _property = property;
        
        // 使用property_getName獲取到該property的名稱
        const char *name = property_getName(property);
        if (name) {
            _name = [NSString stringWithUTF8String:name];
        }
        
        // 目前不考慮自定義setter方法,只考慮系統預設生成setter方法
         // 也就是說屬性username的setter方法為setUsername:
        NSString *setter = [NSString stringWithFormat:@"%@%@", [_name substringToIndex:1].uppercaseString, [_name substringFromIndex:1]];
        _setter = NSSelectorFromString([NSString stringWithFormat:@"set%@:", setter]);
    }
    
    return self;
}

@end

PJXClassInfo代碼

/**
 * @brief 存儲Model的Class信息,不過目前只存儲Class的property信息
 * @param propertyInfos 是一個NSMutableDictionary類型的變數,key存儲property的名稱,value存儲對應的PJXPropertyInfo對象
 */
@interface PJXClassInfo : NSObject
@property (nonatomic, strong) NSMutableDictionary *propertyInfos;
@end

@implementation PJXClassInfo

- (instancetype)initWithClassInfo:(Class)cls
{
    self = [self init];
    
    // 使用class_copyPropertyList獲取到Class的所有property(objc_property_t類型)
     unsigned int propertyCount = 0;
    objc_property_t *properties = class_copyPropertyList(cls, &propertyCount);
    
    _propertyInfos = [NSMutableDictionary dictionary];
    
    // 遍歷properties數組
     // 根據對應的objc_property_t信息構建出PJXPropertyInfo對象,並給propertyInfos賦值
    if (properties) {
        for (unsigned int i = 0; i < propertyCount; i++) {
            PJXPropertyInfo *propertyInfo = [[PJXPropertyInfo alloc] initWithPropertyInfo:properties[i]];
            _propertyInfos[propertyInfo.name] = propertyInfo;
        }
        // 註意釋放空間
        free(properties);
    }
    
    return self;
}

@end

現在我們回到之前的問題,即如何獲取setter並應用?可以看到有了這兩個數據結構,我們就已經解決瞭如何獲取到每個property的setter的問題(使用PJXClassInfo的propertyInfos的屬性)。剩下的事情就簡單了,調用setter方法進行賦值。這裡參考YYModel中的方式,使用了一個Core Foundation函數CFDictionaryApplyFunction


void CFDictionaryApplyFunction(CFDictionaryRef theDict, CFDictionaryApplierFunction applier, void *context);

該函數的作用是對於theDict每個key-value元素都應用applier函數。


所以我們來看看這個applier函數應該怎麼設計。

註意這種C語言的applier回調函數不能設計為成員函數,因為成員函數隱藏了一個self參數。此處我們將該回調函數設計成static,並且命名為PropertyWithDictionaryFunction。

// 註意我傳入的dictionary就是用戶提供的JSON數據
// 比如此處傳入的key==@"username",value==@"shuaige"
static void PropertyWithDictionaryFunction(const void *key, const void *value, void *context)
{
    // 先將key和value轉化到Cocoa框架下
    NSString *keyStr    = (__bridge NSString *)(key);
    id setValue         = (__bridge id)(value);
    
    // modelSelf其實就是self,不過我這裡用的是static函數,所以沒有預設參數self
    // 此時我們需要藉助context參數來獲取到這個self
    // 所以我設計了一個PJXModelContext,用來存儲self信息
    // 另外,此函數的參數中也沒有保存每個property信息,也得靠context這個參數來傳遞
    // 所以PJXModelContext還需要存儲PJXClassInfo對象信息
    PJXModelContext *modelContext = context;
    
    id modelSelf = (__bridge id)(modelContext->modelSelf);
    
    PJXClassInfo *classInfo = (__bridge PJXClassInfo *)(modelContext->modelClassInfo);
    PJXPropertyInfo *info = classInfo.propertyInfos[keyStr];
    
    ((void (*)(id, SEL, id))(void *) objc_msgSend)(modelSelf, info.setter, setValue);
}

最後一步就是在我們的initWithAttributes:函數中構建PJXModelContext並應用到上述函數。

typedef struct {
    void *modelSelf;
    void *modelClassInfo;
}PJXModelContext;

- (instancetype)initWithAttributes:(NSDictionary *)attributes
{
    self = [self init];
    
    if (self) {
        // 初始化PJXClassInfo對象,並給modelContext賦值
        PJXModelContext modelContext = {0};
        modelContext.modelSelf = (__bridge void *)(self);
        PJXClassInfo *classInfo = [[PJXClassInfo alloc] initWithClassInfo:[self class]];
        modelContext.modelClassInfo = (__bridge void *)classInfo;
        
        // 應用該函數,將得到JSON->Model後的Model數據
        CFDictionaryApplyFunction((CFDictionaryRef)attributes, PropertyWithDictionaryFunction, &modelContext);
    }
    
    return self;
}

4. 測試結果


2.設計思路這一部分,我們舉了一個案例。現在我們運行下,看看NSLog的結果:

image

成功了!

5. 存在問題


目前的函數整體才100來行,還是存在很多問題沒有考慮到。

比如:

  1. 沒有考慮用戶傳入的JSON數據的key值和property的名稱不一致
  2. 沒有考慮用戶傳入的JSON數據有嵌套
  3. 沒有考慮JSON數據的value值不一定是NSString類型
  4. 沒有考慮JSON數據並不一定是NSDictionary類型
  5. 沒有考慮用戶自定義了Model屬性的setter方法
  6. ……

不過一口吃不了一個胖子,在後面我會一一修複這些Bug,敬請期待。附上該代碼的GitHub地址


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Atitit.android js 的鍵盤按鍵檢測Back鍵Home鍵和Menu鍵事件 1. onKeyDown @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEY ...
  • 引子 — 和`@interface` 從一開始學習Objc,我們就“知道”定義一個類時後一種寫法才是正確的。然而如果你試了的話,會發現第一種也是OK的。 言歸正傳 為什麼會有這篇文章呢,源於ReactiveCocoa這個開源庫里定義的幾個巨集: @weakify, @strongify, @WS, @ ...
  • 本文永久鏈接:http://www.cnblogs.com/qianLL/p/5342593.html pod 'AFNetworking', '~>3.0.4' < 第三方 具體他的pod的過過程 http://www.cnblogs.com/qianLL/p/5331624.html 代碼如下 ...
  • 然後我的代碼就按照上面的這個順序輸出。 ...
  • 一、什麼是CocoaPods CocoaPods是iOS項目的依賴管理工具,該項目源碼在Github上管理。開發iOS項目不可避免地要使用第三方開源庫,CocoaPods的出現使得我們可以節省設置和第三方開源庫的時間。在使用CocoaPods之前,開發項目需要用到第三方開源庫的時候,我們需要1.把開 ...
  • 服務端: 首先是編寫一個aidl文件,註意AIDL只支持方法,不能定義靜態成員,並且方法也不能有類似public等的修飾符;AIDL運行方法有任何類型的參數和返回值,在java的類型中,以下的類型使用時不需要導入包(import),基本數據類型、String、Map、List.當然為了避免出錯,建議 ...
  • 在iOS開發中,經常會在APP首頁看到多張圖片進行輪換。剛開始做的時候,感覺很麻煩,不是很好做,查閱資料後,我總結了一下,自己封裝了一個簡單的輪轉圖片庫; UIScrollView無限滑動 ,只需要三個View,左視圖,中視圖,右視圖。無論向左滑動,還是向右滑動,都顯示中間的一個View; ( _s ...
  • 動畫資源 一、分類: (一)、概要: 動畫資源 一、分類: 3.0以前,android支持兩種動畫模式,補間動畫(tween animation),幀動畫(frame animation),在android3.0中又引入了一個新的動畫系統:屬性動畫(property animation)。 這三種動 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...