公司項目之前的model層代碼是我使用JSON工具直接生成Objective-C代碼的,當時還是覺得相當省事的,畢竟我經歷過無model層的NSDictionary“黑暗”時期。但是隨著項目的推進,問題開始慢慢顯現出來。
於是,在一個多月前,我在Objc.io上看到提及了Mantle,花了一點時間看了一下,決定先在分支上全套改用Mantle。使用了一段時間,性能沒造成什麼瓶頸,穩定性還是可以接受的。後來也基於Mantle、MK和RAC,把網絡的請求整合在一起,在新項目上全面使用。一開始時也是沒什麼問題的,但後來我也逐漸發現了Mantle雖好,但不至於能解決一切問題。最近看到了《為什麼唱吧iOS 6.0選擇了Mantle》文章,我決定寫博客記下一些model層的坑。 簡要分析 先來簡要分析一下各種構建model層方法的優點和缺點: 一、工具生成model 優點: 1、簡單易用,新手也可以10秒上手 2、有一定的容錯代碼 3、代碼生成相對工整和規范,部分工具還可以選擇是否使用ARC 4、生成簡單model耗時少 缺點: 1、工具生成的類名或者屬性名不太符合要求,往往需要自行修改,但是修改起來相當麻煩,卻需要相當專注以防有什麼地方忘記修改。 2、生成的代碼相當冗長。 3、對適應字段變化比較麻煩,一旦屬性需要修改字段時,要麼人工修改,要麼重新生成,但是極有可能需要重復缺點1的步驟。 4、model之間一些繼承關系還是需要自行修改繼承來實現。 二、基於運行時生成的model(Mantle這類) 優點: 1、減少大量模版代碼 2、修改字段映射時相當簡便 3、擴展時相對方便 4、可以實現更多復雜的映射關系和數值轉換 5、調試時的異常可以較好地發現問題 6、實現了NSCopying和NSCoding協議,可以輕松序列化 缺點: 1、基於運行時屬性映射,對性能有一定影響 2、有部分容錯處理需要自行解決,否則很可能崩潰(下文詳解) 3、框架代碼不少 三、NSDictionary型model 優點: 1、無需任何基礎,直接可用 2、容錯性相對較高 3、無視任何數據結構,均能適應 缺點: 1、維護成本昂貴 2、編譯器無法檢查拼寫,需要定義大量key的常量,否則極其容易寫錯 3、調試相對麻煩 其實,NSDictionary型model還是有一定用途的,畢竟有些情況下,不需要浪費精力去構建一個很短小或者很快就會被釋放的model。但大多數情況下,還是需要去構建一個合理的model,來保證項目的健壯性和開發效率。以前,我老大和我說,iOS應用MVC三層,M這一層其實服務端已經幫你完成了大部分,來到客戶端再自己處理model,既消耗性能又降低了開發效率。當時,我覺得還是比較正確的,但隨著MVC的C變得臃腫不堪,M變得越來越輕量的時候。很多東西都耦合在controller,model這層能做好的話,就能一定程度上減輕了controller的復雜度。加上工作了以後發現,一個只有NSDictionary,無真正model的商業應用,真的非常不利於維護。 基於上述的種種理由,我還是決定了正式全面使用Mantle。但Mantle不是萬能的,我還是遇到了幾個問題。 null值 如果你的屬性是基本數值類型的話,JSON返回一個null值,那麼在Mantle生成model的時候,果斷崩潰了。這個問題和解決方案跟《為什麼唱吧iOS 6.0選擇了Mantle》中的一樣,model中實現一下setNilValueForKey:方法即可。建議使用基類繼承,那麼寫一次這個方法就所有model都解決了這個問題。 鍵值的合理映射 復制代碼 1 { 2 "code": 1 3 "result":{ 4 "access_token":"m_xxxxxxx", 5 "user_id":1111 6 } 7 } 復制代碼 例如上述JSON,假設整個JSON是一個model,那麼如果直接按照JSON的格式來映射,就要新建一個“result”額外的model類。但或許不需要這麼繁復,其實可以這麼寫 1 + (NSDictionary *)JSONKeyPathsByPropertyKey 2 { 3 return @{@"accessToken": @"result.access_token", 4 @"userId": @"result.user_id"}; 5 } 這樣做就可以很方便地映射到對應的屬性上,同時也不需要額外新建一個model類。這裡為什麼沒寫“code”的映射呢,因為如果屬性名和JSON的鍵名一致時,是可以省略不寫映射的,具體大家可以看看Mantle的源碼。 值的類型問題 這個問題是最棘手的,不能說後台坑隊友,但是JSON的數值類型和文檔不符乃家常便飯。作為和用戶最近的前線,我只能想盡辦法去收尾,不能完全放任不管吧。常規的方法不外乎以下幾種: 1、轉換model前先進行預處理JSON數據,把類型不符的值轉換或者刪除掉 2、為這些容易崩潰的值,都寫上NSValueTransformer的轉換 這些的確都能解決問題,但是效率就下降了很多,你得關注各種各樣的可能出現的情況。相信我,要是你這麼做,你連睡覺都睡不好。 我列舉一下幾種類型不符會導致的異常: 1、屬性是BOOL類型,返回值是string類型。 2、屬性是NSString類型,返回值是number類型,Mantle只會轉換出NSNumber類型。你調用length等NSString的專用方法時,你懂的。 3、使用了類似上面"result.access_token"的映射,但返回值不是object類型(例如array類型)。 4、屬性是NSArray,使用了轉換,返回值是object。 放心,實際情況中,絕對不會只有上述4種可能的。不過幸運的是,Mantle幫大家處理了1、3、4這些情況(如果object和array都是使用了轉換方法的話,在轉換的時候會處理這些異常的),只會在調試模式下拋出異常,Release的時候是不會崩潰的。如果大家不想在調試的時候被這些異常打斷的話,可以注釋掉MTLValidateAndSetValue這個方法中的對應代碼。 接下來大家可能覺得太匪夷所思了,為什麼一個小小的客戶端還得因為用個Mantle就要去規避這麼多陷阱。我就是遇到這麼多陷阱,想到了解決方法,自己也是成長了。針對JSON的類型,我有了以下的考慮: JSON其實就是只有4種類型,string、number(int 、bool…)、object、array。在Objective-C對應也就是,NSString、NSNumber、NSDictionary、NSArray,因此要規避類型問題也是從這幾個類著手。這裡要說一下為什麼屬性有int、bool等,我只歸了一類NSNumber。因為實質上,Mantle只是轉換了NSNumber類型的對象出來,在setValue的時候,是由系統根據類型調用了對應的NSNumber方法。由於number和string的類型錯誤是最常見,同時也是最隱蔽的(調用類似intValue的方法看不出端倪),為此我寫了一個AvoidMTLModelCrash的category,github上的地址,使用了這個category以後,關於NSString和NSNumber類型問題的崩潰基本都可以解決。如果是NSArray或者NSDictionary設置到NSString的屬性也是無法檢測的,但是面對如此嚴重的類型問題,我建議還是和小伙伴一起坐下來,好好談談“人生”吧。 靈活的轉換 例如某些接口返回的數據是數組,但很多時候只需要用到這個數組的第一個元素。我們可以直接將數組裡面的一個元素影射出來。 1 + (NSValueTransformer *)pointListJSONTransformer 2 { 3 return [MTLValueTransformer transformerWithBlock:^id(NSArray *array) { 4 return [array firstObject]; 5 }]; 6 } 除了屬性映射和每個屬性固定的類型轉換,MTLJSONSerializing的協議還有classForParsingJSONDictionary這麼一個方法可以改變解釋後的類。例如B、C均繼承A,用Mantle生成A類對象。A類可以通過這個方法,選擇不同的子類生成對象。但是對外可見的接口也是A類的接口,這樣就類似NSArray一樣(NSArray其實也是有很多子類的)。