iOS開發中,不管是哪種設計模式,Model層都是不可或缺的。而Model層的第三方庫常用的庫有以下幾個
JSONModel Mantle MJExtensionJSON data到對象的轉換原理都差不多,一般的順序如下
根據Runtime,動態的獲取屬性的類型和屬性的名字,(如果需要,做一次Json的key的Mapping 創建對應的對象實例 根據KVC(NSKeyValueCoding協議)來為屬性設置值Mantle就是這樣的一個庫,個人比較喜歡Mantle,而且在Github的Star也是提到的幾個庫中最多的。Mantle除了提供JSON和對象的相互轉化,繼承自MTLModel的對象還自動實現了
NSCopying
NSCoding
isEqual
hash
等幾個工具方法。
首先會講解幾個Runtime的基礎知識,不理解這個,也就沒辦法掌握這幾個JSON到Model轉化的原理
介紹Runtime如何獲取某一個類的全部屬性的名字 介紹Runtime如何動態獲取屬性的類型然後,會講解Mantle本身
類的組織架構關系 JSON到對象的處理流程(對象到JSON的過程類似) NSValueTransformer 如何自動實現NSCoding,NSCopying,hash等方法 異常處理 其他認為有用的,例如編譯選項等本文會很長,希望讀者看完後能有些許收獲,如果發現有任何地方有問題,歡迎指正,我會及時修改。
首先,寫兩個類
@interface Base : NSObject
@property (copy,nonatomic)NSString * baseProperty;
@end
@interface Demo : Base
@property (nonatomic,strong)NSDate * createAt;
@property (nonatomic,copy)NSString * name;
@property (nonatomic,assign)CGFloat count;
@end
然後, 寫一個方法來Log Property
-(void)logAllPropertys{
uint count;
objc_property_t * propertys = class_copyPropertyList(Demo.class,&count);
@try {
for (int i = 0; i < count ; i++) {
objc_property_t property = propertys[i];
NSLog(@"%@",@(property_getName(property)));
}
}@finally {
free(propertys);
}
}
執行這個方法的Log
2016-05-26 22:51:48.996 LearnMantle[4670:165290] createAt
2016-05-26 22:51:49.001 LearnMantle[4670:165290] name
2016-05-26 22:51:49.001 LearnMantle[4670:165290] count
不難發現class_copyPropertyList
僅僅是獲取了當前類的屬性列表,並沒有獲取基類的屬性對象。所以對上述方法進行修改
-(void)logAllPropertys{
Class cls = Demo.class;
while (![cls isEqual:NSObject.class]) {
uint count;
objc_property_t * propertys;
@try {
propertys = class_copyPropertyList(cls,&count);
cls = cls.superclass;
for (int i = 0; i < count ; i++) {
objc_property_t property = propertys[i];
NSLog(@"%@",@(property_getName(property)));
}
}@finally {
free(propertys);
}
}
}
這裡又個Tips:
class_copyPropertyList返回一個數組,這個數字必須要手動釋放,所以用Try-Catch-Finally包裹起來。後面會介紹,Mantle如何用更簡潔的方式來實現。
關鍵方法property_getAttributes
,返回個一個C類型的字符串。
我們先聲明一個這樣的屬性
@property (nonatomic,readonly,copy)id name;
然後,打印出它的attributes信息
NSLog(@"%@",@(property_getAttributes(class_getProperty(self.class,@"name".UTF8String))));
可以看到Log是
2016-05-28 10:09:10.476 LearnMantle[731:17207] T@,R,C,N,V_name
這裡的Attributes字符串是編碼後的字符串,分為三個部分
T@,T
表示開頭,後面跟著屬性的類型,@
表示id
類型 Vname
,V
表示中間部分的結束,後面跟ivar
名字,自動合成呢的情況下前面加下劃線 中間R,C,N
用逗號隔開,表示屬性的描述,R
表示readonly
,C
表示Copy
,N
表示Nonatomic
Mantle和ReactiveCocoa都是采用了extobjc這個OC的Runtime工具類將屬性的詳細信息提取到一個結構體裡的,原理都是一樣的。提取完成的結構體是mtl_propertyAttributes
按照文件的方式,
MTLJSONAdapter.h,定義了協議MTLJSONSerializing
和適配器類MTLJSONAdapter
,這兩個協議/類定義了接口來實現JSON-MTLModel的轉換。
MTLModel.h,定義了協議MTLModel和基類MTLModel,基類MTLModel實現了isEqual
,NSCopying
和hash
幾個方法。
MTLModel+NSCoding.h,MTLModel的類別,讓其支持NSCoding協議
MTLValueTransformer.h,NSValueTransformer
的子類,定義了將一個value轉變成另一個value的接口。例如,返回的一個2020-01-01T15:33:30
字符串,利用轉換block轉換成NSDate
其它的都是工具類,提供工具方法,不全列出來了。
以下面代碼調用為例(為了看起來不那麼臃腫,省略不必要的代碼)
Demo * demo = [MTLJSONAdapter modelOfClass:[Demo class] fromJSONDictionary:json error:&error];
看看這個方法的具體實現,就知道分為兩個大的過程
+ (id)modelOfClass:(Class)modelClass fromJSONDictionary:(NSDictionary *)JSONDictionary error:(NSError **)error {
//1.根據modelClass初始化一個adapter
MTLJSONAdapter *adapter = [[self alloc] initWithModelClass:modelClass];
//2.adapter解析實際的JSON數據
return [adapter modelFromJSONDictionary:JSONDictionary error:error];
}
現在看看整個第一大步,initWithModelClass,Mantle做了什麼,
1.1,斷言檢查,並保存modelClass
NSParameterAssert(modelClass != nil);
NSParameterAssert([modelClass conformsToProtocol:@protocol(MTLJSONSerializing)]);
//...
_modelClass = modelClass;
1.2,獲取所有的屬性名字,獲取MTLJSONSerialing中JSONKeyPathsByPropertyKey
方法提供的屬性名字->JSON key的映射,並進行合法性檢查
//屬性名->JSON key的映射
JSONKeyPathsByPropertyKey = [modelClass JSONKeyPathsByPropertyKey];
//所有的屬性集合
NSSet *propertyKeys = [self.modelClass propertyKeys];
//每一個屬性進行檢查
for (NSString *mappedPropertyKey in _JSONKeyPathsByPropertyKey) {
//檢查屬性名->JSON Key映射的屬性名是否合法
if (![propertyKeys containsObject:mappedPropertyKey]) {
NSAssert(NO, @"%@ is not a property of %@.", mappedPropertyKey, modelClass);
return nil;
}
//獲取對應的JSON key
id value = _JSONKeyPathsByPropertyKey[mappedPropertyKey];
//如果是Array(支持JSON key是Array)
if ([value isKindOfClass:NSArray.class]) {
//Array中的每一個Key必須是String類型
for (NSString *keyPath in value) {
if ([keyPath isKindOfClass:NSString.class]) continue;
NSAssert(NO, @"%@ must either map to a JSON key path or a JSON array of key paths, got: %@.", mappedPropertyKey, value);
return nil;
}
} else if (![value isKindOfClass:NSString.class]) {
//檢查JSON key是否時Array類型
NSAssert(NO, @"%@ must either map to a JSON key path or a JSON array of key paths, got: %@.",mappedPropertyKey, value);
return nil;
}
}
1.3 獲取所有的NSValueTransformer,來方便做值轉換(例如:服務器JSON返回的是2015-10-01T13:15:15,轉換成NSDate)
_valueTransformersByPropertyKey = [self.class valueTransformersForModelClass:modelClass];
用過Mantle的都知道,mantle利用”屬性名+JSONTransformer”的方法名字來提供NSValueTransformer,
這裡Mantle用了一些Runtime稍微高級點的東西,所以這個方法我會詳細講解
+ (NSDictionary *)valueTransformersForModelClass:(Class)modelClass {
//...
for (NSString *key in [modelClass propertyKeys]) {//對每一個key檢查NSValueTransformer
//根據屬性名字+JSONTransformer來合成一個Selector
SEL selector = MTLSelectorWithKeyPattern(key, "JSONTransformer");
if ([modelClass respondsToSelector:selector]) {//如果提供了Transformer方法
//獲取IMP指針,也就是實際方法的執行體
IMP imp = [modelClass methodForSelector:selector];
//OC方法轉換為C方法的時候,前兩個參數是_cmd,和SEL,所以,這裡做一個強制轉化,方便下一行執行
NSValueTransformer * (*function)(id, SEL) = (__typeof__(function))imp;
//獲取transformer,保存到Dictionary
NSValueTransformer *transformer = function(modelClass, selector);
if (transformer != nil) result[key] = transformer;
continue;
}
//檢查是否通過協議方法JSONTransformerForKey來提供NSValueTransformer
if ([modelClass respondsToSelector:@selector(JSONTransformerForKey:)]) {
//...
}
//把一個屬性的類型,關鍵字,屬性名字提取到一個結構體中
objc_property_t property = class_getProperty(modelClass, key.UTF8String);
if (property == NULL) continue;
mtl_propertyAttributes *attributes = mtl_copyPropertyAttributes(property);
@onExit {
free(attributes);
};
NSValueTransformer *transformer = nil;
//如果某一個屬性是id類型
if (*(attributes->type) == *(@encode(id))) {
//獲得該屬性的實際類名
Class propertyClass = attributes->objectClass;
if (propertyClass != nil) {
//獲取該類名型提供的NSValueTransformer,即類是否提供了keyJSONTransformer方法
transformer = [self transformerForModelPropertiesOfClass:propertyClass];
}
//如果該類型也是一個MTLModel,並且實現了MTLJSONSerializing,獲取該對象的NSValueTransformer,也就是保證了在MTLModel的一個屬性也是一個MTLModel的時候能夠正常工作
if (nil == transformer && [propertyClass conformsToProtocol:@protocol(MTLJSONSerializing)]) {
transformer = [self dictionaryTransformerWithModelClass:propertyClass];
}
//如果仍然沒有獲取到transformer,驗證對於modalClass是否可轉換
if (transformer == nil) transformer = [NSValueTransformer mtl_validatingTransformerForClass:propertyClass ?: NSObject.class];
} else {
//不是ID類型,則是值類型的transformer
transformer = [self transformerForModelPropertiesOfObjCType:attributes->type] ?: [NSValueTransformer mtl_validatingTransformerForClass:NSValue.class];
}
if (transformer != nil) result[key] = transformer;
}
return result;
}
再看看第二大步,Adapter如何解析JSON
即這個方法
- (id)modelFromJSONDictionary:(NSDictionary *)JSONDictionary error:(NSError **)error {
//...
}
2.1,檢查是否實現了聚類方式解析JSON,例如解析這樣的JSON
[
{
"key1":"value1",
"key2":"value2"
},
{
"key3":"value3",
"key4":"value4"
}
]
對應代碼塊
if ([self.modelClass respondsToSelector:@selector(classForParsingJSONDictionary:)]) {
//...
}
2.2,對於每一個Property的名字,即propertyKey,獲取對應的JSON key。根據JSON key 來獲取對應的值,主要掉用mtl_valueForJSONKeyPath:success:error:
這個方法很簡單,比如對應json的keyPath是person.name.first
先分解成person,name,first,然後一層一層的獲取json[person][name][first],只不過Mantle在解析的時候,用了個for循環,來給用戶反饋,到底錯誤在哪裡。個人感覺用以下兩個KVC的方法更簡潔一點
//驗證是否可用KVC
- validateValue:forKeyPath:error:
//用KVC來獲取值
- valueForKeyPath:
2.3,對於2.2種,獲取到的值,利用1.3的NSValueTransformer進行轉換,這裡只知道NSValueTransformer能夠把一個值轉換成另一個值就行了,後面會詳細講解如何轉換的。
Tips:
這裡要提到的是,Mantle采用了條件編譯方式來處理異常,即debug模式下會拋出異常給開發者,但是release模式下,不會崩潰
#if DEBUG
@throw ex;
#else
//...
#endif
2.4 根據以上三步得到的值字典,對每一個key利用KVC進行設置值,KVC設置值之前,調用
[obj validateValue:&validatedValue forKey:key error:error]
來驗證是否可以KVC
官方文檔
NSValueTranformer是一個抽象的基類,利用Cocoa Bindings技術來進行值的相互轉換
既然是一個抽象基類,那麼使用的時候要繼承這個基類,然後實現必要的方法,從而才能進行相應的值轉換。
例如,實現一個簡單的NSDate<->NSString轉換的Transformer
@interface LHValueTransformer : NSValueTransformer
@end
@implementation LHValueTransformer
+(BOOL)allowsReverseTransformation{
return YES;
}
+(Class)transformedValueClass{
return [NSString class];
}
-(NSDateFormatter *)dateFormatter{
NSDateFormatter * formatter = [[NSDateFormatter alloc] init];
formatter.dateFormat = @"yyyy-MM-dd HH:mm:ss";
return formatter;
}
-(id)transformedValue:(id)value{
NSAssert([value isKindOfClass:[NSDate class]], @"Should a NSDate value");
return [[self dateFormatter] stringFromDate:value];
}
-(id)reverseTransformedValue:(id)value{
NSAssert([value isKindOfClass:[NSString class]], @"Should be a NSString value");
return [[self dateFormatter] dateFromString:value];
}
@end
然後,這樣掉用
NSValueTransformer * trans = [[LHValueTransformer alloc] init];
NSDate * date = [NSDate date];
NSString * str = [trans transformedValue:date];
NSDate * date2 = [trans reverseTransformedValue:str];
MTLValueTransformer就是這樣的一個子類,只不過它提供了正反兩個轉換的block作為接口。
實現NSCopying和hash很簡單,就是基類根據Runtime動態的獲取所有的屬性,然後對應的進行操作就可以了
#pragma mark NSCopying
- (instancetype)copyWithZone:(NSZone *)zone {
MTLModel *copy = [[self.class allocWithZone:zone] init];
[copy setValuesForKeysWithDictionary:self.dictionaryValue];
return copy;
}
#pragma mark NSObject
- (NSString *)description {
NSDictionary *permanentProperties = [self dictionaryWithValuesForKeys:self.class.permanentPropertyKeys.allObjects];
return [NSString stringWithFormat:@"<%@: %p> %@", self.class, self, permanentProperties];
}
- (NSUInteger)hash {
NSUInteger value = 0;
//每個value取hash值
for (NSString *key in self.class.permanentPropertyKeys) {
value ^= [[self valueForKey:key] hash];
}
return value;
}
- (BOOL)isEqual:(MTLModel *)model {
if (self == model) return YES;
if (![model isMemberOfClass:self.class]) return NO;
for (NSString *key in self.class.permanentPropertyKeys) {
id selfValue = [self valueForKey:key];
id modelValue = [model valueForKey:key];
//每一個value取isEqual
BOOL valuesEqual = ((selfValue == nil && modelValue == nil) || [selfValue isEqual:modelValue]);
if (!valuesEqual) return NO;
}
return YES;
}
NSCoding的支持有些復雜,源代碼MTLModel+NSCoding.m
對於initWithCoder:
1. 根據Runtime,獲取所有的屬性名字
2. 對於每一個屬性,檢查是否響應decodeWithCoder:modelVersion:
,也就是說,支持屬性也是MTLModel對象,如果是,則調用decodeWithCoder:modelVersion:
解析這個MTLModel
3. 如果不是MTLModel子類,則調用decodeObjectForKey
來解析,這裡的key就是屬性的名字
encodeWithCoder類似,不做講解
Mantle中,有一些
@try{}
@catch{}
@finally{}
並且在catch模塊中
#if DEBUG
@throw ex;
#else
//其它處理
#endif
這樣能夠方便調試錯誤,並且在運行時的時候不崩潰。
同時,你還能看到這樣的代碼
mtl_propertyAttributes *attributes = mtl_copyPropertyAttributes(property);
@onExit {
free(attributes);
};
這裡的@onExit
是一個宏定義,保證代碼在在當前域返回(return,break,異常)始終能執行到。其實本質就是把代碼放到了finally裡
__attribute__機制能夠為方法,變量,類型增加額外的屬性。
增加的額外屬性,能夠讓編譯器進行額外的檢查,從而提供額外的提示
比如
@property (nonatomic, strong, readonly) id model __attribute__((unavailable("Replaced by -modelFromJSONDictionary:error:")));
+ (NSArray *)JSONArrayFromModels:(NSArray *)models __attribute__((deprecated("Replaced by +JSONArrayFromModels:error:"))) NS_SWIFT_UNAVAILABLE("Replaced by +JSONArrayFromModels:error:");
就分別提示model當前不可用unavailable,和JSONArrayFromModels
方法被deprecated
後面有時間了,系統的整理下所有的__attribute__,今天很晚了,先這樣吧
下一篇會寫React Native的博客,然後MBProgressHud或者AFN的源碼分析