你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> JSONModel源碼解析

JSONModel源碼解析

編輯:IOS開發基礎

為什麼我們要閱讀源碼?

因為我們自己的代碼寫的夠糟糕。所以我們需要閱讀源碼。

優秀的代碼需要經過大量的驗證,這也是為什麼很多公司提倡CodeReview的原因,正常情況下,一個在github上star數超過一千的優秀開源庫都是經過無數的人使用的,不斷的有人提issue,在stackoverflow上提問等等,這就必然促使作者去改良自己的代碼,使之更穩定可靠。而普通程序猿根本沒有這種顧慮,只需要考慮項目能否運行,跑起來不會出現閃退就可以了。所以我們的日常寫的代碼80%都是shit.凡是在事業和技術上有企圖心的程序員,都應該去找優秀的代碼閱讀。

正文

廢話不多說,我們來看看JSONModel這個開源庫到底是一個什麼樣的原理?這篇BLOG有點特殊,我會把我自己的思維邏輯寫出來,你們可以參考下我是怎麼樣閱讀源碼的。

首先,文件列表是這樣的。

11.png

1.JSONModel這個group

從名字上看應該就是這個庫的主體文件。等下細看。

2.JSONModelCategories

應該是一些分類。

3.JSONModelNetworking

從名字上看估計是作者自己封裝的一些網絡請求庫,看到這我是有點疑惑的,要是我自己寫項目用這個庫肯定只是用來解析的,網絡請求之類的肯定會用AFNetWorking自己封裝了。

4.JSONModelTransformations

看到transformations這個單詞我腦海裡第一個念頭就是看起來好像Mantle這個庫裡面自己根據JSON返回數據進行轉換的那個功能。舉個例子,好比你的Model裡有一個@property (strong, nonatomic) NSURL* html_url;這個參數,那麼實際上你解析JSON後服務器返回給你的字段肯定是字符串類型,這時候你就需要把你的這個字段轉換成model裡的NSURL類型,所以需要transomValue。不過還不一定呢,等下細看。

閱讀DEMO

我看到有一個叫做GitHubDemo的group,點進去看。group是這樣的。

12.png

看這類非UI類的庫完全不用關心XIB之類的文件,應為作者提供的DEMO裡界面的部分肯定及其簡單,只需要關心核心代碼就行了。

打開GitHubUserModel.h這個文件。如下圖所示。

13.png

再打開.m文件,空空如也,果然比mantle簡潔。我記得Mantle還要寫mapping呢。但是我看到有URL類型的property。那麼問題來了。他不寫mapping也不寫transform value是怎麼把JSON裡面的字符串轉換成NSURL類型的?臥槽,太神奇了吧。

(我當時有點小震驚,說真的,感覺好屌。)

我看到這裡直接跑去ViewController裡看他怎麼解析的了。是這麼句話。

self.title = @"GitHub.com user lookup";
[HUD showUIBlockingIndicatorWithText:@"Fetching JSON"];
//1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    //code executed in the background
    //2
    NSData* ghData = [NSData dataWithContentsOfURL:
                        [NSURL URLWithString:@"https://api.github.com/users/icanzilb"]
                        ];
    //3
    NSDictionary* json = nil;
    if (ghData) {
        json = [NSJSONSerialization
                JSONObjectWithData:ghData
                options:kNilOptions
                error:nil];
    }
    //4
    dispatch_async(dispatch_get_main_queue(), ^{
        //code executed on the main queue
        //5
        user = [[GitHubUserModel alloc] initWithDictionary:json error:NULL];
        items = @[user.login, user.html_url, user.company, user.name, user.blog];
        [self.tableView reloadData];
        [HUD hideUIBlockingIndicator];
    });
});

What the hell!

真的沒寫類型轉換!真的只用一個initWithDictionary就解析了!

廢話不說直接Command+右鍵,點進去。

-(id)initWithDictionary:(NSDictionary*)dict error:(NSError**)err
{
//check for nil input
if (!dict) {
    if (err) *err = [JSONModelError errorInputIsNil];
    return nil;
}
//invalid input, just create empty instance
if (![dict isKindOfClass:[NSDictionary class]]) {
    if (err) *err = [JSONModelError errorInvalidDataWithMessage:@"Attempt to initialize JSONModel object using initWithDictionary:error: but the dictionary parameter was not an 'NSDictionary'."];
    return nil;
}
//create a class instance
self = [self init];
if (!self) {
    //super init didn't succeed
    if (err) *err = [JSONModelError errorModelIsInvalid];
    return nil;
}
//check incoming data structure
if (![self __doesDictionary:dict matchModelWithKeyMapper:self.__keyMapper error:err]) {
    return nil;
}
//import the data from a dictionary
if (![self __importDictionary:dict withKeyMapper:self.__keyMapper validation:YES error:err]) {
    return nil;
}
//run any custom model validation
if (![self validate:err]) {
    return nil;
}
//model is valid! yay!
return self;
}

然後是這些代碼,其實注釋已經很清楚了,我來解釋下。

  1. 第一個if,沒的說,檢測傳進來的dict是否為nil。如果為空直接返回nil。如果參數有NSError就直接調用處理ERROR的JSONModelError類來處理。

  2. 第二個if檢測dict是否為NSDictionary類型。

  3. 第三段落調用self init方法,如果self為空返回nil。

  4. 第四個段落,看注釋意思應該是檢查傳入dict的結構。我點進去看之後是一個比較復雜的方法,等下細講。

  5. 第五個段落,看注釋應該是導入dict的數據。

  6. 第六段是驗真是否有錯誤,如果有錯誤返回nil。

  7. 第七段自然是返回self了。

Objc的runtime

Runtime相信大家都聽過。但是很少實踐過,我也沒怎麼寫過runtime的東西,所以這裡我也需要查一下資料。或者說換個思考角度,如果讓你寫這麼一個庫,你怎麼把JSON和你的model類型匹配呢?那第一步肯定是要獲取我model類型裡的每一個attribute的名字然後通過KVC來賦值咯。怎麼才能獲取一個NSObject類的屬性呢?

-(NSArray*)__properties__
{
//fetch the associated object
NSDictionary* classProperties = objc_getAssociatedObject(self.class, &kClasPropertiesKey);
NSLog(@"CLASS properties is %@",classProperties);
if (classProperties) return [classProperties allValues];
//if here, the class needs to inspect itself
[self __setup__];
//return the property list
classProperties = objc_getAssociatedObject(self.class, &kClasPropertiesKey);
return [classProperties allValues];
}

我在JSONModel裡找到了這個方法,根據注釋,這就是獲取一個類它的屬性的方法。NSDictionary* classProperties = objc_getAssociatedObject(self.class, &kClasPropertiesKey);

於是我谷歌了一下。

在蘋果的官方文檔裡找到了以下的內容。

13.png

但是還是不太明白什麼意思,但是我知道了兩個參數的意思了,其實無所謂,我只要知道這個方法能幫我取到這個model裡的所有property就夠了。

現在我們已經知道了怎麼取到了這個類的方法,那麼他是怎麼把值付給他也就知道了其實就是[self setValue:@"value" forKey:@"name"];那個key就是剛才通過getAssociatedObject這個方法取到的,這時候已經存入classProperties裡了,value的話就是也很好取,因為當時我們創建這個Model的時候成員屬性的名字和json裡的key是一致的,所以我們可以先通過相同的key從json裡取值,然後再通過kvc賦值給model。這樣,一套系統就打通了,但這只是我腦海中想象的,他到底是不是這麼做的我們還得往下看。

突然發現不對了

上一篇讀到objc_getAssociateObect,我以為是獲取一個Class的property,但是當我讀到-(void)__inspectProperties 這個方法的時候我發現不對,因為這個方法才是獲取一個Class的property的方法。於是我百度了一下。看到了這篇文章。點擊查看

看這句objective-c有兩個擴展機制:category和associative。我們可以通過category來擴展方法,但是它有個很大的局限性,不能擴展屬性。於是,就有了專門用來擴展屬性的機制:associative。

實際上associative也是類擴展的一個方式,和類別不同的是,類別只能擴展一個類的方法,而associative可以擴展一個類的屬性。好比你想給NSString這個類添加一個首字母是否大寫的BOOL值,通過類別你是不行的,或者你可以說我可以繼承NSString,然後添加一個屬性不就行了,問題是你既然集成了NSString類,那你創建的只是NSString 的子類而不是它本身了。

只是長期以來,這個方法寫法略顯高端,所以使用率遠遠沒有類別高。

那麼我們來好好看看-(void)__inspectProperties這個方法,因為這個方法才是JSONModel的核心。真正看懂了這個方法,我們才知道這裡為什麼要用associate來擴展。

-(void)__inspectProperties
{
//JMLog(@"Inspect class: %@", [self class]);
NSMutableDictionary* propertyIndex = [NSMutableDictionary dictionary];
//temp variables for the loops
Class class = [self class];
NSScanner* scanner = nil;
NSString* propertyType = nil;
// inspect inherited properties up to the JSONModel class
while (class != [JSONModel class]) {
    //JMLog(@"inspecting: %@", NSStringFromClass(class));
    unsigned int propertyCount;
    objc_property_t *properties = class_copyPropertyList(class, &propertyCount);
    //loop over the class properties
    for (unsigned int i = 0; i < propertyCount; i++) {
        JSONModelClassProperty* p = [[JSONModelClassProperty alloc] init];
        //get property name
        objc_property_t property = properties[i];
        const char *propertyName = property_getName(property);
        p.name = @(propertyName);
        JMLog(@"property: %@", p.name);
        //get property attributes
        const char *attrs = property_getAttributes(property);
        NSString* propertyAttributes = @(attrs);
        NSArray* attributeItems = [propertyAttributes componentsSeparatedByString:@","];
        JMLog(@"attributes: %@",propertyAttributes);
        //ignore read-only properties
        if ([attributeItems containsObject:@"R"]) {
            continue; //to next property
        }
        //check for 64b BOOLs
        if ([propertyAttributes hasPrefix:@"Tc,"]) {
            //mask BOOLs as structs so they can have custom convertors
            p.structName = @"BOOL";
        }
        scanner = [NSScanner scannerWithString: propertyAttributes];
        //JMLog(@"attr: %@", [NSString stringWithCString:attrs encoding:NSUTF8StringEncoding]);
        [scanner scanUpToString:@"T" intoString: nil];
        [scanner scanString:@"T" intoString:nil];
        //check if the property is an instance of a class
        if ([scanner scanString:@"@\"" intoString: &propertyType]) {
            [scanner scanUpToCharactersFromSet:[NSCharacterSet characterSetWithCharactersInString:@"\"<"]
                                    intoString:&propertyType];
            //JMLog(@"type: %@", propertyClassName);
            p.type = NSClassFromString(propertyType);
            p.isMutable = ([propertyType rangeOfString:@"Mutable"].location != NSNotFound);
            p.isStandardJSONType = [allowedJSONTypes containsObject:p.type];
            //read through the property protocols
            while ([scanner scanString:@"<" intoString:NULL]) {
                NSString* protocolName = nil;
                [scanner scanUpToString:@">" intoString: &protocolName];
                if ([protocolName isEqualToString:@"Optional"]) {
                    p.isOptional = YES;
                } else if([protocolName isEqualToString:@"Index"]) {
                    p.isIndex = YES;
                    objc_setAssociatedObject(
                                             self.class,
                                             &kIndexPropertyNameKey,
                                             p.name,
                                             OBJC_ASSOCIATION_RETAIN // This is atomic
                                             );
                } else if([protocolName isEqualToString:@"ConvertOnDemand"]) {
                    p.convertsOnDemand = YES;
                } else if([protocolName isEqualToString:@"Ignore"]) {
                    p = nil;
                } else {
                    p.protocol = protocolName;
                }
                [scanner scanString:@">" intoString:NULL];
            }
        }
        //check if the property is a structure
        else if ([scanner scanString:@"{" intoString: &propertyType]) {
            [scanner scanCharactersFromSet:[NSCharacterSet alphanumericCharacterSet]
                                intoString:&propertyType];
            p.isStandardJSONType = NO;
            p.structName = propertyType;
        }
        //the property must be a primitive
        else {
            //the property contains a primitive data type
            [scanner scanUpToCharactersFromSet:[NSCharacterSet characterSetWithCharactersInString:@","]
                                    intoString:&propertyType];
            //get the full name of the primitive type
            propertyType = valueTransformer.primitivesNames[propertyType];
            if (![allowedPrimitiveTypes containsObject:propertyType]) {
                //type not allowed - programmer mistaked -> exception
                @throw [NSException exceptionWithName:@"JSONModelProperty type not allowed"
                                               reason:[NSString stringWithFormat:@"Property type of %@.%@ is not supported by JSONModel.", self.class, p.name]
                                             userInfo:nil];
            }
        }
        NSString *nsPropertyName = @(propertyName);
        if([[self class] propertyIsOptional:nsPropertyName]){
            p.isOptional = YES;
        }
        if([[self class] propertyIsIgnored:nsPropertyName]){
            p = nil;
        }
        //few cases where JSONModel will ignore properties automatically
        if ([propertyType isEqualToString:@"Block"]) {
            p = nil;
        }
        //add the property object to the temp index
        if (p) {
            [propertyIndex setValue:p forKey:p.name];
        }
    }
    free(properties);
    //ascend to the super of the class
    //(will do that until it reaches the root class - JSONModel)
    class = [class superclass];
}
//finally store the property index in the static property index
objc_setAssociatedObject(
                         self.class,
                         &kClassPropertiesKey,
                         [propertyIndex copy],
                         OBJC_ASSOCIATION_RETAIN // This is atomic
                         );
}

我們來一句一句的看

objc_property_t *properties = class_copyPropertyList(class, &propertyCount);

這句話的意思就是獲取Class 的property的數量,然後把這個數量賦值給我們自己定義的propertyCount這個變量,第一個參數class 就是[self Class].(就是自己的類) ,最後,這個函數會返回一個內容為objc_property_t的數組.

然後就是設一個for循環,把數組properties裡的objc_property_t一個一個取出來檢索.

第一步是取出來property的name,用一個函數property_getName,取出來了.

第二部,取出property的attribute,const char *attrs = property_getAttributes(property);

看到這,因為這些都是字符串,我想把它打出來看看到底是什麼.如圖

14.png

不知道大家還記不記得上一篇我用的是github的Model來做例子的.我再把.h文件弄出來讓大家看看.

看到了麼,第一張圖,我們獲取了一個5個property的name,都能和我們的githubModel裡的.h文件裡聲明的一一匹配.說明我們通過這個方法獲取的沒有問題,name屬性完全正確,但是attribute都是這種東西,T@"NSURL",&,N,V_blog.

這是什麼鬼!

不要緊我們繼續往下看.

5.NSArray* attributeItems = [propertyAttributes componentsSeparatedByString:@","];

這句很好懂哈,他把那個我們不知道是什麼鬼的東西用','符號做了個分拆,拆成了一個個字符串.拿我們上面那個東西來當例子的話attributesItems這個數組裡的內容現在應該是這樣的.@[@"T@NSURL",@"&",@"N",@"V_blog"];

if
 ([attributeItems containsObject:@"R"]) {
            continue; //to next property
        }

這句話他檢查我們的數組裡有沒有一個字符串是@"R",如果有,那麼我們的property就是個只讀的屬性,意思就是當時聲明的時候@property(readonly)這樣的,如果這個屬性是只讀的那我們還費什麼勁解析,直接跳過.

7

 //check for 64b BOOLs
        if ([propertyAttributes hasPrefix:@"Tc,"]) {
            //mask BOOLs as structs so they can have custom convertors
            p.structName = @"BOOL";
        }

如果不是只讀的話,再檢查我們的attributes字符串是不是以Tc開頭的,如果是,就給我們的p.structName賦值為@"BOOL".

這裡說一下這個p是什麼,p就是JSONModel裡專門記錄Model的property信息的一個類,你們可以去看看JSONModelClassProperty這個類.

8.然後初始化了一個NSSCanner類.

這個就厲害了,這個類我以前從來沒用過.然後我又谷歌了一下.然後,不得不佩服Raywenderlich這個網站的牛逼之處,他居然有

NSScanner Tutorial: Parsing Data in Mac OS X

所以等我先看完這篇文章再說,明天繼續.

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved