Mantle
閱讀一個庫的原始碼,首先要知道,我們為什麼需要這一類的庫。
Mantle的目的
Mantle 的誕生是為了更方便的將服務端返回的資料對映為我們的 Model。
簡單來說,我們在寫 app 的時候,經常需要把服務端返回的資料和我們自己建立 model 關聯起來,這樣,在和 View 層互動的時候就可以使用 model 而不是直接使用字典。
那麼,我們如果不使用 Mantle 的情況下。是如何建立一個 Model 並且把服務端返回的資料填充到這個Model裡呢?我們來看看 Mantle 給的例子,一般是這樣的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
typedef enum : NSUInteger { GHIssueStateOpen, GHIssueStateClosed } GHIssueState; @interface GHIssue : NSObject @property (nonatomic, copy, readonly) NSURL *URL; @property (nonatomic, copy, readonly) NSURL *HTMLURL; @property (nonatomic, copy, readonly) NSNumber *number; @property (nonatomic, assign, readonly) GHIssueState state; @property (nonatomic, copy, readonly) NSString *reporterLogin; @property (nonatomic, copy, readonly) NSDate *updatedAt; @property (nonatomic, strong, readonly) GHUser *assignee; @property (nonatomic, copy, readonly) NSDate *retrievedAt; @property (nonatomic, copy) NSString *title; @property (nonatomic, copy) NSString *body; - (id)initWithDictionary:(NSDictionary *)dictionary; @end |
然後 .m 檔案裡的實現一般是這樣的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
@implementation GHIssue + (NSDateFormatter *)dateFormatter { NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'Z'"; return dateFormatter; } - (id)initWithDictionary:(NSDictionary *)dictionary { self = [self init]; if (self == nil) return nil; _URL = [NSURL URLWithString:dictionary[@"url"]]; _HTMLURL = [NSURL URLWithString:dictionary[@"html_url"]]; _number = dictionary[@"number"]; if ([dictionary[@"state"] isEqualToString:@"open"]) { _state = GHIssueStateOpen; } else if ([dictionary[@"state"] isEqualToString:@"closed"]) { _state = GHIssueStateClosed; } _title = [dictionary[@"title"] copy]; _retrievedAt = [NSDate date]; _body = [dictionary[@"body"] copy]; _reporterLogin = [dictionary[@"user"][@"login"] copy]; _assignee = [[GHUser alloc] initWithDictionary:dictionary[@"assignee"]]; _updatedAt = [self.class.dateFormatter dateFromString:dictionary[@"updated_at"]]; return self; } |
想象一下,如果你的 Model 有幾十種,相當於你要寫幾十次這樣重複的程式碼。因此,為了減少這種重複性的工作,應運而生了 Mantle 這樣的庫。
Mantle的功能
瞭解了Mantle為何誕生。那麼,我們就要看看Mantle到底為我們解決了什麼樣的問題?
- 避免了寫重複性的 init 方法,通過服務端返回的資料自動生成 model ,也可以利用model來反序列化生成 JSON 。
- 當model裡的某個屬性名稱和服務端的某個欄位的名字不一樣的時候,可以利用
+ (NSDictionary *)JSONKeyPathsByPropertyKey
這個方法來進行欄位的匹配。 - 可以通過
+JSONTransformer
這個命名方法來宣告一個轉換方法。舉例來說就是,一般服務端返回日期的時候是以時間戳的方式返回的,通常是一個長整形的數字,1464116217 ,但是你宣告的對應的property是一個NSDate
型別,這時候,你就需要進行長整形 -> 日期型別的轉換,所以 Mantle 提供了這種方式,來進行方便的轉換。 - 自動的decode和encoding。方便 Model 的歸檔。
當然,Mantle 遠不止這些功能。但是,我們先搞清楚主要的功能,那麼一些次要的功能自然就迎刃而解了。
Mantle如何實現這些功能?
如何實現自動將Dictionary的值自動的賦給Model裡對應的property?
我們先來嘗試一下自己實現自動化的賦值。
熟悉OC的朋友應該知道NSObject有一個方法,叫做 - (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues;
這個方法就是利用kvc,直接讓 model 呼叫 - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
方法,通過遍歷傳入的字典的key,對model進行賦值。
舉個簡單例子,現在我們有個Model叫做User。它的property和init方法如下。
1 2 3 4 5 6 7 8 9 10 11 |
@property (nonatomic, copy) NSString *name; @property (nonatomic, copy) NSString *address; - (id)initWithDictionary:(NSDictionary *)dict { if (self = [super init]) { [self setValuesForKeysWithDictionary:dict]; } return self; } |
然後我們這樣給它賦值。
1 2 3 |
NSDictionary *dict = @{@"name":@"zql", @"address":@"beijing"}; User *user = [[User alloc] initWithDictionary:dict]; NSLog(@"User name is %<a href='http://www.jobbole.com/members/uz441800'>@,</a> address is %@", user.name, user.address); |
列印出來的結果就是
1 |
User name is zql, address is beijing |
但是,這裡有可能會出現幾個問題。
- 如果 Dictionary 裡的某一個 key 的名字和Model裡的property的名字不匹配,就會造成
NSUnknownKeyException
然後直接崩潰。 - 沒法進行型別判斷,如果你的dictionary裡某個key對應的值和你的model裡相同的key對應的值得型別不一致,他沒有辦法自動轉換,而且完全不會報錯。
當然,你可能會說,第一個問題,可以通過實現 - (void)setValue:(id)value forUndefinedKey:(NSString *)key
這個方法來進行檢測和修復。
但問題是,如果大量的key不匹配的話,你又回到了原來的問題,需要寫大量重複的程式碼。
這樣分析下來,直接使用 setValuesForKeysWithDictionary
並不能實現我們的需求。
Mantle 是如何做的?
顯然,Mantle需要解決的第一個問題就是,如何建立起 Model 裡的 property 和 Dictionary 裡的key 一一對應的關係。
這個顯然是需要使用者提供的。
因為不同的 App 服務端返回的資料五花八門,命名方式有可能是駝峰命名也有可能不是,那麼我們定義的 Model 也一樣。如何才能建立起這種關係呢?
Mantle 的做法是,你需要
- 建立的 Model 需要繼承自MTLModel
- 必須實現 MTLJSONSerializing 協議。
先看程式碼。
1 2 3 4 5 6 |
@interface Car : MTLModel @property (nonatomic, copy) NSString *carName; @property (nonatomic, copy) NSString *carOwnner; @end |
.m 檔案中是這樣的
1 2 3 4 5 6 |
+ (NSDictionary *)JSONKeyPathsByPropertyKey { return @{@"carName": @"name", @"carOwnner" :@"ownner" }; } |
我們來思考一下,為什麼這樣寫就可以建立起 model 和 Dictionary 一一對應的關係。
首先,服務端返回給我們的 Car 資訊的JSON檔案中,鍵值對分別是, name 對應的是汽車的名稱,ownner 對應的是汽車的擁有者,但是我們在建立 Model 的時候,汽車的名字是 carName,擁有者的名字是carOwnner ,所以,我們需要告訴 Mantle,在利用 kvc 賦值的時候。
1 2 |
id value = dictionary[@"carName"]; [self setValue:value forKey:@"carName"]; |
實際上走到這的時候第一步就錯了,為什麼?
因為dictionary里根本就沒有 carName
這個key,你拿到的是nil。
所以,應該如何處理這種 model 裡的 key 和 字典裡 key 不一致的情況?
Mantle 的處理方法是,你需要告訴我,model 的 property 的名字和字典裡的 key 存在怎樣的一種對應關係。
所以,當 Mantle 在遍歷Car這個類的Property列表的時候,,應當先去使用者在 JSONKeyPathsByPropertyKey
方法中傳回的字典裡尋找,是否有Property對應的服務端的key,再利用這個對應的key去Dictionary裡拿資料,再賦值給我們的property。比如上面這個例子,就應該把carName
替換為name
之後再從字典取值,然後再把取得的值賦給carName
這個property。
需要注意的是,如果 JSONKeyPathsByPropertyKey
裡沒有填寫任何對應關係,最新版本Mantle是不會預設 Model 裡的key和字典中的key 相同的,而是直接跳過。但是早些版本的Mantle會預設這種相同的關係,直接賦值。
本著嚴謹的精神,我去檢視了一下 Mantle release 記錄,看看到底是哪個版本的 Mantle 取消了這種預設的行為。
在 2.0 版本的ChangeLog裡看到了下面一段話。
所以,在 2.0 版本, Mantle 取消了這種隱式的轉換關係。
在這個說明裡,作者還提到了一個方法,+[NSDictionary mtl_identityPropertyMapWithModel:]
,這個方法如何使用呢?當你的 Model 裡的所有屬性的名字和 JSON 裡的所有 key 的名字完全相同的時候,你就可以用這個方法直接生成一個 NSDictionary, 直接返回。省掉了自己寫。例如.
1 2 3 |
+ (NSDictionary *)JSONKeyPathsByPropertyKey { return [NSDictionary mtl_identityPropertyMapWithModel:self]; } |
分析原始碼
說了這麼多。我們就來看看Mantle到底如何實現這麼多好用的功能。
先看看 Mantle的目錄結構。
主要模組有:
- Modules 主要負責最基礎的功能,包括通過 runtime 獲取Class的property,encode 和 decode 功能。
- Adapters 主要負責JSON Model轉換的核心邏輯。
- ValueTransform 主要負責某個Property需要進行自定義轉換的需求。例如服務端返回的時間戳 -> Model中宣告的NSDate型別這種轉換。
- libextobjc 將
property_attribute
這種Type Encodings
過得東西轉化為對應的Model。後面會細講。
基礎知識
1.如何利用Runtime獲取一個類的所有Property的資訊?
之前已經說過了,想要實現ORM的基本條件就是,獲取一個類的所有屬性的名字。拿不到屬性的名字,就沒法利用kvc賦值。
怎麼拿?程式碼如下。
1 2 3 4 5 6 |
+ (void)printAllPropertiesAndVaules { unsigned int outCount, i; objc_property_t *properties =class_copyPropertyList([self class], &outCount); for (i = 0; i |
列印的結果如下。
1 2 |
property name is name, attributes is T@"NSString",C,N,V_name property name is address, attributes is T@"NSString",C,N,V_address |
2.上一個例子裡列印出來的property_attribute是什麼?
名字好理解,輸出的和我們宣告的property名字一致,這個沒什麼問題。但是後面那個attributes是個什麼東西?
我們宣告的@property (nonatomic, copy) NSString *name;
通過runtime取出來之後的形式是這樣的。
T@"NSString",C,N,V_name
關於這樣的一段字串到底是什麼意思,蘋果官方文件是這麼說的
The string starts with a T followed by the @encode type and a comma, and finishes with a V followed by the name of the backing instance variable. Between these, the attributes are specified by the following descriptors, separated by commas:
然後我畫了一張圖來解釋這一段字串到底是什麼意思。
至於每個符號的意思,你們可以從蘋果的官方文件中找到。地址在:https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100-SW1
所以,我們可以通過property_getName
獲取到property的名字,也可以通過property_getAttributes
獲取到property的一些屬性。這樣,也就邁出了第一步,起碼,我們能知道一個類都有那些屬性,這些屬性叫什麼,都是怎麼宣告的。
3.解析EXTRuntimeExtensions這個類,先看看.h檔案
Mantle中對應上述功能的就是這個類。
我們來看看Mantle是如何解析type encoding過的屬性的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
typedef enum { /** * The value is assigned. */ mtl_propertyMemoryManagementPolicyAssign = 0, /** * The value is retained. */ mtl_propertyMemoryManagementPolicyRetain, /** * The value is copied. */ mtl_propertyMemoryManagementPolicyCopy } mtl_propertyMemoryManagementPolicy; /** * Describes the attributes and type information of a property. */ typedef struct { /** * Whether this property was declared with the \c readonly attribute. */ BOOL readonly; /** * Whether this property was declared with the \c nonatomic attribute. */ BOOL nonatomic; /** * Whether the property is a weak reference. */ BOOL weak; /** * Whether the property is eligible for garbage collection. */ BOOL canBeCollected; /** * Whether this property is defined with \c \@dynamic. */ BOOL dynamic; /** * The memory management policy for this property. This will always be * #mtl_propertyMemoryManagementPolicyAssign if #readonly is \c YES. */ mtl_propertyMemoryManagementPolicy memoryManagementPolicy; /** * The selector for the getter of this property. This will reflect any * custom \c getter= attribute provided in the property declaration, or the * inferred getter name otherwise. */ SEL getter; /** * The selector for the setter of this property. This will reflect any * custom \c setter= attribute provided in the property declaration, or the * inferred setter name otherwise. * * <a href='http://www.jobbole.com/members/smartsl'>@note</a> If #readonly is \c YES, this value will represent what the setter * \e would be, if the property were writable. */ SEL setter; /** * The backing instance variable for this property, or \c NULL if \c * \c @synthesize was not used, and therefore no instance variable exists. This * would also be the case if the property is implemented dynamically. */ const char *ivar; /** * If this property is defined as being an instance of a specific class, * this will be the class object representing it. * * This will be \c nil if the property was defined as type \c id, if the * property is not of an object type, or if the class could not be found at * runtime. */ Class objectClass; /** * The type encoding for the value of this property. This is the type as it * would be returned by the \c \@encode() directive. */ char type[]; } mtl_propertyAttributes; |
首先,Mantle定義了一個列舉型別,用來記錄一個property的記憶體管理方式,列舉中有三種型別。分別對應iOS中的assign,retain,copy
.
列舉的名字叫做mtl_propertyMemoryManagementPolicy
.
然後,Mantle宣告瞭一個名為mtl_propertyAttributes
的struct用來記錄一個property的各種屬性。
這個struct有下列成員。
readonly
: 一個Bool值,用來記錄這個property是否為readonly.
nonatomic
: Bool。記錄是否為nonatomic
weak
: Bool。memory manage的方式是否為weak。
canBeCollected
:Bool。是否可以被垃圾回收機制管理。(實際上iOS並沒有自動垃圾回收機制。OSX以前有過,但是在mountain lion之後就被禁用了。)
dynamic
: Bool.是否被宣告為dynamic。
memoryManagementPolicy
:即Mantle自己定義的列舉型別,用來記錄property的記憶體管理方式,如果這個property是readonly的時候,那麼這個值永遠為mtl_propertyMemoryManagementPolicyAssign
.
getter
: SEL。即記錄這個property的自定義getter方法。
setter
: SEL.記錄這個property的自定義setter方法
ivar
: const char 型別。記錄這個property對應的成員變數。例如@property (nonatomic, copy) NSString
user,那麼對應的成員變數應該為
_user.
objectClass: 如果這個property的型別是一個自定義的類。那麼這個objectClass用來記錄這個類。
char type[]: 用來記錄這個property的type encoding之後的值。例如,user這個property的型別是NSString,那麼type encoding之後,NSString會被編譯器轉化成
@這個標記,所以這個type裡儲存的就是
@.
標頭檔案中還定義了一個方法。
mtl_propertyAttributes *mtl_copyPropertyAttributes (objc_property_t property);
這個方法是幹嘛的呢?
很簡單,就是把我們的objc_property_t
型別轉換成我們的mtl_propertyAttributes
的struct。
接下來,我們來看看.m檔案中,Mantle是如何把一個runtime中的objc_property_t
轉換成struct的。
再來看看EXTRuntimeExtensions.m
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 |
mtl_propertyAttributes *mtl_copyPropertyAttributes (objc_property_t property) { const char * const attrString = property_getAttributes(property); if (!attrString) { fprintf(stderr, "ERROR: Could not get attribute string from property %s\n", property_getName(property)); return NULL; } if (attrString[0] != 'T') { fprintf(stderr, "ERROR: Expected attribute string \"%s\" for property %s to start with 'T'\n", attrString, property_getName(property)); return NULL; } const char *typeString = attrString + 1; const char *next = NSGetSizeAndAlignment(typeString, NULL, NULL); if (!next) { fprintf(stderr, "ERROR: Could not read past type in attribute string \"%s\" for property %s\n", attrString, property_getName(property)); return NULL; } size_t typeLength = next - typeString; if (!typeLength) { fprintf(stderr, "ERROR: Invalid type in attribute string \"%s\" for property %s\n", attrString, property_getName(property)); return NULL; } // allocate enough space for the structure and the type string (plus a NUL) mtl_propertyAttributes *attributes = calloc(1, sizeof(mtl_propertyAttributes) + typeLength + 1); if (!attributes) { fprintf(stderr, "ERROR: Could not allocate mtl_propertyAttributes structure for attribute string \"%s\" for property %s\n", attrString, property_getName(property)); return NULL; } // copy the type string strncpy(attributes->type, typeString, typeLength); attributes->type[typeLength] = '\0'; // if this is an object type, and immediately followed by a quoted string... if (typeString[0] == *(@encode(id)) && typeString[1] == '"') { // we should be able to extract a class name const char *className = typeString + 2; next = strchr(className, '"'); if (!next) { fprintf(stderr, "ERROR: Could not read class name in attribute string \"%s\" for property %s\n", attrString, property_getName(property)); return NULL; } if (className != next) { size_t classNameLength = next - className; char trimmedName[classNameLength + 1]; strncpy(trimmedName, className, classNameLength); trimmedName[classNameLength] = '\0'; // attempt to look up the class in the runtime attributes->objectClass = objc_getClass(trimmedName); } } if (*next != '\0') { // skip past any junk before the first flag next = strchr(next, ','); } while (next && *next == ',') { char flag = next[1]; next += 2; switch (flag) { case '\0': break; case 'R': attributes->readonly = YES; break; case 'C': attributes->memoryManagementPolicy = mtl_propertyMemoryManagementPolicyCopy; break; case '&': attributes->memoryManagementPolicy = mtl_propertyMemoryManagementPolicyRetain; break; case 'N': attributes->nonatomic = YES; break; case 'G': case 'S': { const char *nextFlag = strchr(next, ','); SEL name = NULL; if (!nextFlag) { // assume that the rest of the string is the selector const char *selectorString = next; next = ""; name = sel_registerName(selectorString); } else { size_t selectorLength = nextFlag - next; if (!selectorLength) { fprintf(stderr, "ERROR: Found zero length selector name in attribute string \"%s\" for property %s\n", attrString, property_getName(property)); goto errorOut; } char selectorString[selectorLength + 1]; strncpy(selectorString, next, selectorLength); selectorString[selectorLength] = '\0'; name = sel_registerName(selectorString); next = nextFlag; } if (flag == 'G') attributes->getter = name; else attributes->setter = name; } break; case 'D': attributes->dynamic = YES; attributes->ivar = NULL; break; case 'V': // assume that the rest of the string (if present) is the ivar name if (*next == '\0') { // if there's nothing there, let's assume this is dynamic attributes->ivar = NULL; } else { attributes->ivar = next; next = ""; } break; case 'W': attributes->weak = YES; break; case 'P': attributes->canBeCollected = YES; break; case 't': fprintf(stderr, "ERROR: Old-style type encoding is unsupported in attribute string \"%s\" for property %s\n", attrString, property_getName(property)); // skip over this type encoding while (*next != ',' && *next != '\0') ++next; break; default: fprintf(stderr, "ERROR: Unrecognized attribute string flag '%c' in attribute string \"%s\" for property %s\n", flag, attrString, property_getName(property)); } } if (next && *next != '\0') { fprintf(stderr, "Warning: Unparsed data \"%s\" in attribute string \"%s\" for property %s\n", next, attrString, property_getName(property)); } if (!attributes->getter) { // use the property name as the getter by default attributes->getter = sel_registerName(property_getName(property)); } if (!attributes->setter) { const char *propertyName = property_getName(property); size_t propertyNameLength = strlen(propertyName); // we want to transform the name to setProperty: style size_t setterLength = propertyNameLength + 4; char setterName[setterLength + 1]; strncpy(setterName, "set", 3); strncpy(setterName + 3, propertyName, propertyNameLength); // capitalize property name for the setter setterName[3] = (char)toupper(setterName[3]); setterName[setterLength - 1] = ':'; setterName[setterLength] = '\0'; attributes->setter = sel_registerName(setterName); } return attributes; errorOut: free(attributes); return NULL; } |