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

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是這樣的。

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

再打開.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;
}然後是這些代碼,其實注釋已經很清楚了,我來解釋下。
第一個if,沒的說,檢測傳進來的dict是否為nil。如果為空直接返回nil。如果參數有NSError就直接調用處理ERROR的JSONModelError類來處理。
第二個if檢測dict是否為NSDictionary類型。
第三段落調用self init方法,如果self為空返回nil。
第四個段落,看注釋意思應該是檢查傳入dict的結構。我點進去看之後是一個比較復雜的方法,等下細講。
第五個段落,看注釋應該是導入dict的數據。
第六段是驗真是否有錯誤,如果有錯誤返回nil。
第七段自然是返回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);
於是我谷歌了一下。
在蘋果的官方文檔裡找到了以下的內容。

但是還是不太明白什麼意思,但是我知道了兩個參數的意思了,其實無所謂,我只要知道這個方法能幫我取到這個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);
看到這,因為這些都是字符串,我想把它打出來看看到底是什麼.如圖

不知道大家還記不記得上一篇我用的是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
所以等我先看完這篇文章再說,明天繼續.