最近唱吧 iOS的6.0版本已經(jīng)成功上線了。18人月的投入,2500個commit,幾十萬行的代碼修改。唱吧iOS已經(jīng)從內至外煥然一新,感謝一起并肩作戰(zhàn)的小伙伴們。
6.0一個很重大的修改就是基于Mantle 重建(新建)了Model層。這里不對Mantle作更多介紹,只分享一下使用Mantle的決策及執(zhí)行過程。
我們遇到的問題
唱吧是一款上線2年多的App,產(chǎn)品形態(tài)的演進和迭代非常快。因此不可避免的遺留了各種問題:
Model層不健全,沒有統(tǒng)一的結構,不同工程師做法差異很大;多數(shù)是啞類型,且沒有統(tǒng)一的序列化機制
業(yè)務邏輯冗余、分散、不一致
模塊劃分隨意,依賴關系混亂,維護困難
NSDictionary作為承載業(yè)務的數(shù)據(jù)類型在各處出現(xiàn)(sqlite, Model object, API, Notification, web, OpenURL etc.),參數(shù)和值的正確性完全沒有編譯器檢查,字符串很容易寫錯,風險延后至運行時,易產(chǎn)生低級bug
基本沒有文檔和注釋(結合上一點,不掛debugger很難讀懂代碼)
幾百個API,業(yè)務復雜,變動快,重構難;同一個API請求可能有重復和不一致
API的一些參數(shù)和返回值,同一個參數(shù)/返回值可能存在類型差異;由于API需要向前兼容,修改API有成本
除此之外,還有其他工程上的約束:
不能影響現(xiàn)有的API,所有的事情只限于iOS端的修改
代碼即文檔,因為沒有精力維護文檔
對不同Model的持久化方式作遷移
避免寫大段枯燥的Model的序列化/反序列化代碼
沒有時間造出足夠成熟、健壯可重用的組件及撰寫文檔
上述的問題都是長期存在且需要解決的,否則嚴重影響開發(fā)效率及代碼質量。11年的時候我還在做社交游戲的時候,設計并實現(xiàn)了一套簡單的基于Objective-C Runtime的數(shù)值表Model結構及轉換工具(Model<=>csv)供數(shù)值策劃使用。但想寫出一套成熟的方案還是有一些距離,而且也沒有資源和時間作維護、測試和文檔。
順著這個思路找到了JSONModel 和Mantle ,前者剛剛1.0,后者在Github for Mac中廣泛使用且社區(qū)更成熟(甚至Slack上有channel),所以成為了更好的選擇。
事實也證明這個選擇是對的,6.0上線后,crash率比之前的版本有顯示的降低,并且Mantle相關的crash占總crash的比率不到3%,大可以直接用在大型的產(chǎn)品上。
除了成熟穩(wěn)定,Mantle基本解決了我們遇到了的所有問題。下面具體介紹一些通用性Mantle使用經(jīng)驗,基本的使用方法請直接移步Mantle的README 。
Property名稱轉換
由于API使用的開發(fā)語言與iOS所使用的Objective-C是截然不同的,所以可能將一些保留關鍵字作為property的名稱(如id),或者不小心override掉基類的屬性(如description)。還有可能API中使用了一個很糟糕的名稱,或者使用了不符合Objective-C命名規(guī)范的名稱,這些我們都需要作轉換。
只需要實現(xiàn)MTLJSONSerializing
protocol并在+JSONKeyPathsByPropertyKey
方法中定義好新舊名稱的映射關系即可,Mantle會在序列化及反序列化時對屬性名進行自動的轉換。
1
2
3
4
5
6
7
8
9
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return @{
@"identifier": @"id",
@"displayDiscription": @"description",
@"thisIsANewShit": @"newShit",
@"creativeProduct": @"copyToChina",
@"betterPropertyName": @"m_wired_propertyName"
}
}
好了很多吧?沒錯,只需要定義一次名稱的映射關系就可以了,Mantle負責model與JSON之間的雙向轉換。不需要將這種邏輯寫得到處都是,并且還得維護它的一致性。
Property的類型映射
iOS中處理URL使用的是NSURL類型,但JSON只支持基本的字符串,Mantle可以自動幫你轉換成NSURL。
1
2
3
+ (NSValueTransformer *)URLJSONTransformer {
return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
}
NSValueTransformer負責在不同類型間進行雙向轉換,請讀者研究一下Mantle的實現(xiàn)方式 。在此前提下,留給讀者一個問題(其實這是一個真實的故事,類似的故事還有很多,詳見iOS應用開發(fā)之十大坑隊友 ):
假設我們有一個entity,名字且叫KTVConcreteEntity吧,它有一個屬性名字叫entityID,類型是NSInteger。問題來了,entityID可能在另外一個API的response中是字符串類型,在不直接修改Mantle的源碼的前提下怎么搞?歡迎在下方留言討論。
空標量異常
有的時候API的response會有空值,比如copyToChina
可能不是每次都有的,JSON是這樣兒的:
1
2
3
{
"copyToChina": null
}
Mantle在這種情況會將newShit轉換為nil,但如果是標量如NSInteger怎么辦?KVC會直接raise NSInvalidArgumentException
。
Mantle是基于KVC給property賦值的,KVC提供了- (void)setNilValueForKey:(NSString *)key
方法,讓我們?yōu)閚il指定一個合理的替代值,我們來看一下此方法的解釋:
Invoked by setValue:forKey: when it’s given a nil value for a scalar value (such as an int or float). Subclasses can override this method to handle the request in some other way, such as by substituting 0 or a sentinel value for nil and invoking setValue:forKey: again or setting the variable directly. The default implementation raises an NSInvalidArgumentException.
對于標量來講,多數(shù)情況下合理的值即為0,我們來看下代碼:
1
2
3
4
5
6
7
8
@interface MTLModel (KTVNullableScalar)
@end
@implementation MTLModel (KTVNullableScalar)
- (void)setNilValueForKey:(NSString *)key {
[self setValue:@0 forKey:key]; // For NSInteger/CGFloat/BOOL
}
@end
問題完美解決,再也不需要到處寫無聊的if/else了。
其它重要特性
Mantle為我們帶來的方便不勝枚舉:
實現(xiàn)了NSCopying
protocol,子類可以直接copy是多么爽的事情
實現(xiàn)了NSCoding
protocol,跟NSUserDefaults說拜拜
提供了-isEqual:
和-hash
的默認實現(xiàn),model作NSDictionary的key方便了許多
簡單且把一件事情做好,不摻雜網(wǎng)絡相關的操作
如此強大優(yōu)雅的設計,讓我不得不向Github的工程師們致敬!
寫在后面
篇幅所限,只介紹了幾個典型的問題,歡迎大家討論。但如果你的App的代碼規(guī)模只有幾萬行,或者API只有十幾個,或者沒有遇到我們這些遺留問題,我建議還是不要引入了,殺雞用指甲刀就夠了,殺不動多磨磨找準要害。Anyway,Mantle的實現(xiàn)和思路是值得每位iOS工程師學習和借鑒的。
附小廣告一則:唱吧iOS團隊誠招iOS工程師,推薦成功即獎勵6000元現(xiàn)金或iPhone 6一部,詳見這篇blog 。