讀一讀源碼 --- MJExtension

  • MJExtension是json轉(zhuǎn)模型相當(dāng)便捷的一個(gè)三方庫垄惧。本本為一窺其內(nèi)部奧妙缝驳,文中肯定有不足之處慈俯,敬請(qǐng)指正栏尚。

  • json 的一個(gè){}就是一個(gè)json對(duì)象起愈,對(duì)應(yīng)iOS開發(fā)json數(shù)據(jù)可類比NSDictionary,{}就可看做一個(gè)模型類,其他json類型就可看做這個(gè)模型類的屬性。解析是{}解析為NSDictionary,[]NSArray,字符串為NSString,數(shù)字類型為NSNumber抬虽。

  • 我平時(shí)常用MJExtension官觅。其他的比如YYModel,功能上要豐富一些阐污。這里就先看看MJ啦休涤。

  • 來看看MJ的結(jié)構(gòu):


    MJExtension
    MJExtension

    如果屬性名和json數(shù)據(jù)中的key完全相同,核心模塊主要就是紅色部分笛辟。

  • 功能實(shí)現(xiàn)都是以NSObject的分類實(shí)現(xiàn)功氨,所以任何繼承自NSObject的子類都可調(diào)用。

  • 我們先來看看如何提供一些自定義功能的隘膘,有兩種方式:一者通過在模型類實(shí)現(xiàn)MJKeyValue協(xié)議疑故,一者通過NSObject+MJPropertyNSObject+MJClass中對(duì)應(yīng)的類方法。

  • 以下只列出設(shè)置屬性白名單的代碼弯菊,其他請(qǐng)自行查看,處理方式大同小異踱阿。

  • 協(xié)議

@protocol MJKeyValue <NSObject>
@optional
/**
 *  只有這個(gè)數(shù)組中的屬性名才允許進(jìn)行字典和模型的轉(zhuǎn)換
 */
+ (NSArray *)mj_allowedPropertyNames;
@end
  • 因?yàn)?code>NSObject+MJKeyValue分類已經(jīng)遵守了協(xié)議管钳,所以模型類可以直接實(shí)現(xiàn)就是。

  • 類方法
    先看調(diào)用:

[Person mj_setupAllowedPropertyNames:^NSArray *{
            return @[@"name", @"sex"];
        }];
  • 其實(shí)這個(gè)方法類似一個(gè)setter方法,不過這里是類方法的形式
/**
 *  屬性白名單配置
 */ 
+ (void)mj_setupAllowedPropertyNames:
   (MJAllowedPropertyNames)allowedPropertyNames {
    // 內(nèi)部就是將block返回的數(shù)組綁定到MJAllowedPropertyNamesKey
    [self mj_setupBlockReturnValue:allowedPropertyNames key:&MJAllowedPropertyNamesKey];
}
/**
 *  利用 runtime 通過 kvc 方式綁定設(shè)置的屬性白名單到模型類,否則為nil
 */
+ (void)mj_setupBlockReturnValue:(id (^)(void))block key:(const char *)key {
    if (block) {
        objc_setAssociatedObject(self, key, block(), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    } else {
        objc_setAssociatedObject(self, key, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }

    [[self dictForKey:key] removeAllObjects];
}
/**
 *  根據(jù)key返回對(duì)應(yīng)的保存自定義數(shù)據(jù)的字典
 */
+ (NSMutableDictionary *)dictForKey:(const void *)key {
    @synchronized (self) {
        if (key == &MJAllowedPropertyNamesKey)
            return allowedPropertyNamesDict_;
        if (key == &MJIgnoredPropertyNamesKey)
            return ignoredPropertyNamesDict_;
        if (key == &MJAllowedCodingPropertyNamesKey)
            return allowedCodingPropertyNamesDict_;
        if (key == &MJIgnoredCodingPropertyNamesKey)
            return ignoredCodingPropertyNamesDict_;
        return nil;
    }
}
  • objc_setAssociatedObject設(shè)置關(guān)聯(lián)软舌,對(duì)應(yīng)objc_getAssociatedObject就是獲取關(guān)聯(lián)值才漆。

  • 這里用到了runtime的關(guān)聯(lián)機(jī)制,常撤鸬悖看到在分類中添加屬性就是利用這個(gè)機(jī)制醇滥。因?yàn)檫@種方式是基于key的,所以MJ聲明了一些靜態(tài)全局常量超营。這里展示的MJAllowedPropertyNamesKey就是一個(gè)char型靜態(tài)全局常量鸳玩,作用是作為關(guān)聯(lián)block返回值的key。當(dāng)然還有其他的key演闭,這里不一一列舉不跟。

  • 同時(shí)也聲明一個(gè)靜態(tài)全局字典allowedPropertyNamesDict_,目的是以當(dāng)前類名作為key米碰,將當(dāng)前類及父類中關(guān)聯(lián)的block返回值保存窝革。

  • 這里字典的初始化MJ放在了分類的+(void)load方法中,該方法只在App啟動(dòng)時(shí)加載一次吕座。如果父類虐译,子類和分類都實(shí)現(xiàn)了該方法,執(zhí)行順序是父類>子類>分類吴趴。執(zhí)行該方法時(shí)漆诽,程序必定會(huì)阻塞,所以要盡量少的在其中執(zhí)行任務(wù)。

  • 最后拿到字典進(jìn)行清空拴泌,筆者沒猜到意圖魏身。牽強(qiáng)地推測(cè)下:allowedPropertyNamesDict_是全局的,如果有同一個(gè)類重復(fù)調(diào)用setup方法蚪腐,新的數(shù)據(jù)會(huì)追加在字典中箭昵。而如果存在二次調(diào)用,應(yīng)該是不需要之前的自定義數(shù)據(jù)才是(直接更新自定義就是唄···)回季。

  • 另外+dictForKey方法在NSObject+MJPropertyNSObject+MJClass中都有家制,筆者在調(diào)試時(shí)遇到了點(diǎn)小問題。比如在MJClass中執(zhí)行時(shí)泡一,step in會(huì)跳到MJProperty中的該方法玩敏。

  • 既然上面的方法類似setter,當(dāng)然就有類似的getter方法了茂翔。

/**
 *  獲取當(dāng)前類及所有父類的白名單屬性
 */
+ (NSMutableArray *)mj_totalAllowedPropertyNames {
    return [self mj_totalObjectsWithSelector:@selector(mj_allowedPropertyNames) key:&MJAllowedPropertyNamesKey];
}
/**
 *  
 */
+ (NSMutableArray *)mj_totalObjectsWithSelector:(SEL)selector key:(const char *)key {
    // 先嘗試獲取本類的白名單凳兵,有則返回,無則通過協(xié)議或block獲取
    // 其實(shí)就是  [dict objectForkey:]
    NSMutableArray *array = [self dictForKey:key][NSStringFromClass(self)];
    if (array) return array;
    
    // 創(chuàng)建帖蔓、存儲(chǔ)
    // 其實(shí)就是  [dict setObject: forKey:]
    // 以本類名為key矮瘟,value為本類及所有父類的屬性白名單的數(shù)組
    [self dictForKey:key][NSStringFromClass(self)] = array = [NSMutableArray array];
    // 是否響應(yīng)白名單的協(xié)議方法
    if ([self respondsToSelector:selector]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        // 拿到白名單
        NSArray *subArray = [self performSelector:selector];
#pragma clang diagnostic pop
        if (subArray) {
            [array addObjectsFromArray:subArray];
        }
    }
    // 向上遍歷拿到當(dāng)前類及所有父類的白名單,并加入數(shù)組
    [self mj_enumerateAllClasses:^(__unsafe_unretained Class c, BOOL *stop) {
        NSArray *subArray = objc_getAssociatedObject(c, key);
        [array addObjectsFromArray:subArray];
    }];
    return array;
}
  • 處理自定義部分的思路:先利用runtime將預(yù)定義的全局key和自定義部分(以O(shè)C數(shù)據(jù)結(jié)構(gòu)返回塑娇,如NSArray)關(guān)聯(lián)澈侠。獲取時(shí)先嘗試從協(xié)議方法中獲取自定義部分,然后再?gòu)谋绢惢蚋割惛鶕?jù)全局key關(guān)聯(lián)的數(shù)據(jù)結(jié)構(gòu)中獲取埋酬。
  • +mj_enumerateAllClasses遍歷當(dāng)前類及其父類哨啃,里面就是一個(gè)while循環(huán)獲取父類,就不再多說了写妥。

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
#pragma clang diagnostic pop
以上3個(gè)命令是用來忽略一些編譯器警告拳球,push和pop一定搭配使用。

  • 由于篇幅太多耳标,這里只看了白名單設(shè)置醇坝。所有處理自定義部分的思路基本就是上面這些,當(dāng)然具體細(xì)節(jié)還有點(diǎn)不同次坡。比如獲取時(shí)呼猪,如果即實(shí)現(xiàn)了協(xié)議方法和自定義block,白名單包括黑名單設(shè)置會(huì)將兩個(gè)部分都保存砸琅;而其他如屬性替換部分則會(huì)執(zhí)行協(xié)議方法宋距,放棄自定義block中的部分。

接下來看看json轉(zhuǎn)model的調(diào)用:

Pili *budaixi = [Pili mj_objectWithKeyValues:data];
  • 工廠方法返回一個(gè)轉(zhuǎn)換后的模型實(shí)例症脂,來看具體實(shí)現(xiàn):
+ (instancetype)mj_objectWithKeyValues:(id)keyValues {
    return [self mj_objectWithKeyValues:keyValues context:nil];
}

+ (instancetype)mj_objectWithKeyValues:(id)keyValues context:(NSManagedObjectContext *)context {
    // 判斷傳入?yún)?shù)是否已經(jīng)解析成Json谚赎,否則使用NSJsonSerialization解析
    keyValues = [keyValues mj_JSONObject];
    // 判斷解析后的數(shù)據(jù)是否是字典對(duì)象,如果不是基本上json數(shù)據(jù)格式有問題了吧
    // MJ自己實(shí)現(xiàn)的斷言淫僻,滿足條件繼續(xù)執(zhí)行,否則拋出錯(cuò)誤
    MJExtensionAssertError([keyValues isKindOfClass:[NSDictionary class]], nil, [self class], @"keyValues參數(shù)不是一個(gè)字典");
    // 接下來是和數(shù)據(jù)庫相關(guān)的
    if ([self isSubclassOfClass:[NSManagedObject class]] && context) {
        NSString *entityName = [NSStringFromClass(self) componentsSeparatedByString:@"."].lastObject;
        return [[NSEntityDescription insertNewObjectForEntityForName:entityName inManagedObjectContext:context] mj_setKeyValues:keyValues context:context];
    }
    return [[[self alloc] init] mj_setKeyValues:keyValues];
}
  • 注意keyValuesid類型,當(dāng)我們從網(wǎng)絡(luò)獲取到responseData后壶唤,可以不做處理直接傳入雳灵。因?yàn)?code>MJExtension會(huì)做json解析。
  • 平常debug時(shí)可以嘗試使用斷言,設(shè)定一些前置條件闸盔,可以省去一些調(diào)試步驟悯辙。一般使用系統(tǒng)的NSAssert即可
  • 最后初始化并調(diào)用實(shí)例方法進(jìn)行轉(zhuǎn)換

先看看如何做的json解析

- (id)mj_JSONObject
{
    if ([self isKindOfClass:[NSString class]]) {
        // 傳入的是一個(gè)json字符串,類似@"{\"name\":\"yaya\"}",先轉(zhuǎn)成Data
        return [NSJSONSerialization JSONObjectWithData:[((NSString *)self) dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:nil];
    } else if ([self isKindOfClass:[NSData class]]) {
        // 如果外部沒解析過數(shù)據(jù)迎吵,在這處理
        return [NSJSONSerialization JSONObjectWithData:(NSData *)self options:kNilOptions error:nil];
    }
    // 模型 -> 字典
    return self.mj_keyValues;
}
  • 該方法有兩個(gè)作用:一是對(duì)源json數(shù)據(jù)進(jìn)行解析躲撰;二是對(duì)自定義模型轉(zhuǎn)字典,如果不是自定義模型則直接返回原數(shù)據(jù)
  • 能使用self.mj_keyValues并不是聲明了一個(gè)屬性击费,而是直接寫了一個(gè)getter形式的方法拢蛋。目的是用于模型轉(zhuǎn)字典,這里并沒有調(diào)用蔫巩,所以我們到后面再說:
- (NSMutableDictionary *)mj_keyValues {
    return [self mj_keyValuesWithKeys:nil ignoredKeys:nil];
}

然后我們step out往后進(jìn)入核心代碼:

- (instancetype)mj_setKeyValues:(id)keyValues
{
    return [self mj_setKeyValues:keyValues context:nil];
}

/**
 核心代碼:
 */
- (instancetype)mj_setKeyValues:(id)keyValues context:(NSManagedObjectContext *)context
{
    // 獲得解析后的JSON對(duì)象
    // 在這里其實(shí)沒做什么
    keyValues = [keyValues mj_JSONObject];
    // 類型判斷
    MJExtensionAssertError([keyValues isKindOfClass:[NSDictionary class]], self, [self class], @"keyValues參數(shù)不是一個(gè)字典");
    
    Class clazz = [self class];
    // 獲取當(dāng)前類及所有父類的白名單屬性
    NSArray *allowedPropertyNames = [clazz mj_totalAllowedPropertyNames];
    // 獲取當(dāng)前類及所有父類的黑名單屬性
    NSArray *ignoredPropertyNames = [clazz mj_totalIgnoredPropertyNames];
    
    //通過封裝的方法回調(diào)一個(gè)通過運(yùn)行時(shí)編寫的谆棱,用于返回屬性列表的方法。
    // 遍歷每一個(gè)屬性名
    [clazz mj_enumerateProperties:^(MJProperty *property, BOOL *stop) {
    // 核心轉(zhuǎn)換的block批幌,暫時(shí)缺省
    ······
    }];
    
    // 轉(zhuǎn)換完畢
    if ([self respondsToSelector:@selector(mj_keyValuesDidFinishConvertingToObject)]) {
        [self mj_keyValuesDidFinishConvertingToObject];
    }
    return self;
}
  • 注釋都在代碼中了础锐,核心邏輯就在這里完成,主要就是保證屬性和值類型一致荧缘,否則賦值為nil。
  • 由于block是異步執(zhí)行的拦宣,從業(yè)務(wù)邏輯上來說block是在+mj_enumerateProperties方法之后執(zhí)行截粗。所以這里暫時(shí)缺省,放到后面說鸵隧。
  • [clazz mj_enumerateProperties:^(MJProperty *property, BOOL *stop) {}
    在這個(gè)方法中利用runtime獲取當(dāng)前模型類的所有屬性绸罗,并將屬性的** 類型,名字,類型編碼**等分別拆開。用內(nèi)部模型MJProperty保存這些變量豆瘫。然后在block中對(duì)每一個(gè)屬性進(jìn)行轉(zhuǎn)換珊蟀。
  • 說一說遍歷吧。這里MJ自己寫了一個(gè)enumerate,其實(shí)FoundationNSArray,NSDictionary以及NSSet都有enumerateObjectsUsingBlock方法(實(shí)際名稱有所差別)外驱。如果對(duì)index不感興趣育灸,使用自帶的遍歷方法能使代碼更加清晰。

來看看+mj_enumerateProperties

+ (void)mj_enumerateProperties:(MJPropertiesEnumeration)enumeration {
    // 獲得本類及父類所有屬性,并包裝成MJProperty對(duì)象
    NSArray *cachedProperties = [self properties];
    
    // 遍歷每一個(gè)包裝的屬性,在block中進(jìn)行類型轉(zhuǎn)換并賦值
    BOOL stop = NO;
    for (MJProperty *property in cachedProperties) {
        enumeration(property, &stop);
        if (stop) break;
    }
}

沒什么說的昵宇,繼續(xù)深入:

/**
 runtime獲取當(dāng)前類的屬性,轉(zhuǎn)換成MJProperty對(duì)象保存到數(shù)組磅崭,再將數(shù)組關(guān)聯(lián)到本類
 */
+ (NSMutableArray *)properties {
    // 嘗試獲取已經(jīng)包裝過的屬性
    NSMutableArray *cachedProperties = [self dictForKey:&MJCachedPropertiesKey][NSStringFromClass(self)];
    // 沒有存儲(chǔ)過屬性
    if (cachedProperties == nil) {
        cachedProperties = [NSMutableArray array];
        // 遍歷本類及所有父類,獲取所有屬性并拆解包裝成MJProperty對(duì)象
        [self mj_enumerateClasses:^(__unsafe_unretained Class c, BOOL *stop) {
            // 1.獲得所有的成員變量
            unsigned int outCount = 0;
            // 獲取當(dāng)前類的屬性列表
            objc_property_t *properties = class_copyPropertyList(c, &outCount);
            
            // 2.遍歷每一個(gè)成員變量,并打包成一個(gè)MJProperty對(duì)象
            for (unsigned int i = 0; i<outCount; i++) {
                // 將屬性保存為MJProperty對(duì)象
                MJProperty *property = [MJProperty cachedPropertyWithProperty:properties[i]];
                // 如果屬性所在的類不是自定義模型類就進(jìn)行下次循環(huán)
                if ([MJFoundation isClassFromFoundation:property.srcClass]) continue;

                // 設(shè)置屬性所在的模型類
                property.srcClass = c;
                // 處理多級(jí)映射及替換
                [property setOriginKey:[self propertyKey:property.name] forClass:self];
                // 處理數(shù)組中的自定義類型
                [property setObjectClassInArray:[self propertyObjectClassInArray:property.name] forClass:self];
                // 保存對(duì)象化的屬性
                [cachedProperties addObject:property];
            }
            
            // 3.釋放內(nèi)存
            free(properties);
        }];
        // 就是賦值瓦哎,將存儲(chǔ)屬性(MJProperty)的數(shù)組根據(jù)對(duì)應(yīng)key保存
        [self dictForKey:&MJCachedPropertiesKey][NSStringFromClass(self)] = cachedProperties;
    }
    return cachedProperties;
}
  • MJCachedPropertiesKey也是一個(gè)全局key砸喻,對(duì)應(yīng)一個(gè)字典柔逼,該字典的key是本類類名,value是保存包裝過的屬性的數(shù)組割岛。
  • objc_property_t *properties = class_copyPropertyList(c, &outCount)
    聲明一個(gè)objc_property_t類型的指針變量愉适,指向一個(gè)同類型的動(dòng)態(tài)分配的指針數(shù)組。因此所有class_copy系列使用之后癣漆,都要記得free维咸。
    靜態(tài)指針與動(dòng)態(tài)指針的free

接下來進(jìn)入:

+ (void)mj_enumerateClasses:(MJClassesEnumeration)enumeration {
    // 1.沒有block就直接返回
    if (enumeration == nil) return;
    
    // 2.停止遍歷的標(biāo)記
    BOOL stop = NO;
    
    // 3.當(dāng)前正在遍歷的類
    Class c = self;
    
    // 4.開始遍歷每一個(gè)類
    while (c && !stop) {
        // 4.1.執(zhí)行操作
        enumeration(c, &stop);
        
        // 4.2.獲得父類
        c = class_getSuperclass(c);

        // 4.3.如果是OC類,結(jié)束循環(huán)
        if ([MJFoundation isClassFromFoundation:c]) break;
    }
}
  • 又是一個(gè)while循環(huán)獲取父類扑媚。不同的是多了最后一個(gè)Foundation類判斷腰湾。
MJFoundation.m

+ (NSSet *)foundationClasses {
    if (foundationClasses_ == nil) {
        // 集合中沒有NSObject,因?yàn)閹缀跛械念惗际抢^承自NSObject疆股,具體是不是NSObject需要特殊判斷
        foundationClasses_ = [NSSet setWithObjects:
                              [NSURL class],
                              [NSDate class],
                              [NSValue class],
                              [NSData class],
                              [NSError class],
                              [NSArray class],
                              [NSDictionary class],
                              [NSString class],
                              [NSAttributedString class], nil];
    }
    return foundationClasses_;
}
/**
 *  判斷類型是Foundation類還是自定義類
 */
+ (BOOL)isClassFromFoundation:(Class)c {
    if (c == [NSObject class] || c == [NSManagedObject class]) return YES;
    
    __block BOOL result = NO;
    [[self foundationClasses] enumerateObjectsUsingBlock:^(Class foundationClass, BOOL *stop) {
        if ([c isSubclassOfClass:foundationClass]) {
            result = YES;
            // 結(jié)束遍歷
            *stop = YES;
        }
    }];
    return result;
}
  • 這個(gè)方法的作用是為了得到自定義的模型類
  • +isSubclassOfClass判斷兩個(gè)類是否是父子關(guān)系或相等费坊。
  • 由于很多類都是NSObject的子類,所以排除NSObject需要單獨(dú)判斷

接下來看看如何將獲取到的屬性拆解并包裝成MJProperty對(duì)象:

+ (instancetype)cachedPropertyWithProperty:(objc_property_t)property {
    // 當(dāng)前屬性是否已經(jīng)緩存過
    // 若是直接返回
    // 若否進(jìn)行拆解
    MJProperty *propertyObj = objc_getAssociatedObject(self, property);
    if (propertyObj == nil) {
        propertyObj = [[self alloc] init];
        propertyObj.property = property;
        // 呼應(yīng)上面的objc_get
        objc_setAssociatedObject(self, property, propertyObj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return propertyObj;
}
// set方法
- (void)setProperty:(objc_property_t)property
{
    _property = property;
    
    MJExtensionAssertParamNotNil(property);
    
    // 1.屬性名
    _name = @(property_getName(property));
    
    // 2. 對(duì)屬性字符串截取出OC使用的類型
    NSString *attrs = @(property_getAttributes(property));
    NSUInteger dotLoc = [attrs rangeOfString:@","].location;
    NSString *code = nil;
    NSUInteger loc = 1;
    if (dotLoc == NSNotFound) {
        code = [attrs substringFromIndex:loc];
    } else {
        code = [attrs substringWithRange:NSMakeRange(loc, dotLoc - loc)];
    }
    // 3.屬性類型:包裝成MJPropertyType對(duì)象
    _type = [MJPropertyType cachedTypeWithCode:code];
}
  • property_getAttributes獲取的屬性字符串是經(jīng)過OC類型編碼的
    比如 @property (nonatomic, copy) NSString *name;
    編碼 @"T@\"NSString\",&,N,V_name"
    T@\"NSString\":表示NSString類型
    C:表示copy關(guān)鍵字
    N:表示nonatomic關(guān)鍵字
    V_name:表示屬性名name
  • 屬性編碼一定是T開頭后面緊跟類型編碼旬痹,屬性名以V開頭后面緊跟屬性名或?qū)嵗兞棵?有_)附井,然后各部分以逗號(hào)分隔。屬性字符串是含轉(zhuǎn)義字符的两残,使用p命令可以看到永毅。
  • 詳情查看開發(fā)者官網(wǎng)Property Type StringType Coding
  • 以上面name為例,截取后值剩下@\"NSString\"。只需要再截取出NSString字符串人弓,然后利用反射機(jī)制就能得到NSString類型沼死。

對(duì)于屬性類型,MJ 又包裝成了MJPropertyType對(duì)象:

+ (instancetype)cachedTypeWithCode:(NSString *)code {
    MJExtensionAssertParamNotNil2(code, nil);
    @synchronized (self) {
        // 當(dāng)前類型是否已經(jīng)緩存過
        // 若是直接返回
        // 若否進(jìn)行包裝
        MJPropertyType *type = types_[code];
        if (type == nil) {
            type = [[self alloc] init];
            type.code = code;
            types_[code] = type;
        }
        return type;
    }
}

- (void)setCode:(NSString *)code
{
    _code = code;
    
    MJExtensionAssertParamNotNil(code);
    
    if ([code isEqualToString:MJPropertyTypeId]) {
        // id 類型
        _idType = YES;
    } else if (code.length == 0) {
        _KVCDisabled = YES;

    } else if (code.length > 3 && [code hasPrefix:@"@\""]) {

        // 去掉 @\" 和 " 崔赌,截取中間的類型名稱
        _code = [code substringWithRange:NSMakeRange(2, code.length - 3)];
        // 反射機(jī)制
        _typeClass = NSClassFromString(_code);
        // 對(duì)象類型是否屬于Foundation
        _fromFoundation = [MJFoundation isClassFromFoundation:_typeClass];
        // 是否是NSNumber對(duì)象
        _numberType = [_typeClass isSubclassOfClass:[NSNumber class]];
        
    } else if ([code isEqualToString:MJPropertyTypeSEL] ||
               [code isEqualToString:MJPropertyTypeIvar] ||
               [code isEqualToString:MJPropertyTypeMethod]) {
        // SEL類型意蛀,成員變量,IMP類型
        _KVCDisabled = YES;
    }
    
    // 基本數(shù)據(jù)類型
    NSString *lowerCode = _code.lowercaseString;
    NSArray *numberTypes = @[MJPropertyTypeInt, MJPropertyTypeShort, MJPropertyTypeBOOL1, MJPropertyTypeBOOL2, MJPropertyTypeFloat, MJPropertyTypeDouble, MJPropertyTypeLong, MJPropertyTypeLongLong, MJPropertyTypeChar];
    if ([numberTypes containsObject:lowerCode]) {
        _numberType = YES;
        
        if ([lowerCode isEqualToString:MJPropertyTypeBOOL1]
            || [lowerCode isEqualToString:MJPropertyTypeBOOL2]) {
            _boolType = YES;
        }
    }
}
  • 判斷具體類型健芭,對(duì)象類型利用反射機(jī)制形成县钥,基本數(shù)據(jù)類型不能使用反射機(jī)制,所以采用獨(dú)立標(biāo)識(shí)
  • @synchronized 同步鎖慈迈,用于多線程操作時(shí)防止多個(gè)線程對(duì)同一個(gè)對(duì)象寫入若贮。但是這里為什么使用,筆者沒搞清楚痒留。如果有人知道請(qǐng)留言告知谴麦。(是因?yàn)閎lock是異步執(zhí)行的嗎?)
  • 之所以會(huì)有_KVCDisabled屬性狭瞎,是因?yàn)?KVCvalue不支持基礎(chǔ)數(shù)據(jù)類型细移。基礎(chǔ)數(shù)據(jù)類型需使用NSNumber包裝熊锭。

至此對(duì)屬性的拆解和重新包裝就完成了弧轧,接下來回到+ (NSMutableArray *)properties方法雪侥,看看多級(jí)映射以及屬性和json數(shù)據(jù)key不一樣時(shí)的處理。

來看調(diào)用:

[property setOriginKey:[self propertyKey:property.name] forClass:self];
  • + propertyKey方法是根據(jù)當(dāng)前屬性名獲取json數(shù)據(jù)中要替換的keyPath精绎,如果不需要替換速缨,則直接返回該屬性名。內(nèi)部處理類同開頭介紹的白名單獲取方式代乃。
  • 需要注意的是獲取方式有兩個(gè)方法replacedKeyFromPropertyName121replacedKeyFromPropertyName旬牲。
  • 區(qū)別
    前者采用objc_setAssociatedObject關(guān)聯(lián)block和對(duì)應(yīng)的key時(shí),使用的策略是OBJC_ASSOCIATION_RETAIN_NONATOMIC,而后者采用的是OBJC_ASSOCIATION_COPY_NONATOMIC搁吓。在ARC下原茅,一般沒什么差別,因?yàn)橄到y(tǒng)默認(rèn)對(duì)block采用copy操作堕仔。而在MRC下擂橘,block中如果使用了外部變量,block會(huì)存在于棧中摩骨。棧中的對(duì)象釋放由系統(tǒng)控制通贞,所以很可能在block使用前,系統(tǒng)就銷毀了對(duì)象恼五。此時(shí)再使用就可能crash昌罩。因此聲明block時(shí),最好使用copy灾馒,復(fù)制到堆中茎用,由程序員控制對(duì)象的釋放。
    參考鏈接:iOS 非ARC下的block
  • 因此建議使用replacedKeyFromPropertyName121協(xié)議或block睬罗。
/**
 將要替換的json的keyPath拆解并持有保存

 @param originKey json數(shù)據(jù)中要替換的keyPath绘搞,若不需替換則為屬性本身的名稱
 @param c 當(dāng)前類
 */
- (void)setOriginKey:(id)originKey forClass:(Class)c
{
    // 字符串類型的key:可能是不需替換的或者是json中的keyPath
    if ([originKey isKindOfClass:[NSString class]]) {
        // 拆解keyPath并保存到數(shù)組
        NSArray *propertyKeys = [self propertyKeysWithStringKey:originKey];
        if (propertyKeys.count) {
            [self setPorpertyKeys:@[propertyKeys] forClass:c];
        }
    } else if ([originKey isKindOfClass:[NSArray class]]) {
        // 什么情況是數(shù)組?一個(gè)屬性可能對(duì)應(yīng)多個(gè)json keyPath
        // 每一個(gè)keyPath都要拆解包裝并保存在一個(gè)數(shù)組
        // 再將每一數(shù)組按順序保存到一個(gè)數(shù)組
        NSMutableArray *keyses = [NSMutableArray array];
        for (NSString *stringKey in originKey) {
            NSArray *propertyKeys = [self propertyKeysWithStringKey:stringKey];
            if (propertyKeys.count) {
                [keyses addObject:propertyKeys];
            }
        }
        if (keyses.count) {
            [self setPorpertyKeys:keyses forClass:c];
        }
    }
}
/**
 *  拆解多級(jí)映射的keyPath(包括數(shù)組的索引值)傅物,并包裝成MJPropertyKey對(duì)象,存儲(chǔ)到數(shù)組中
 */
- (NSArray *)propertyKeysWithStringKey:(NSString *)stringKey
{
    if (stringKey.length == 0) return nil;
    
    NSMutableArray *propertyKeys = [NSMutableArray array];
    // 如果有多級(jí)映射
    // 模型屬性對(duì)應(yīng)json中第n級(jí)key
    // 處理json中的數(shù)組對(duì)象級(jí)
    // 要求寫keyPath時(shí)采用點(diǎn)語法的形式
    NSArray *oldKeys = [stringKey componentsSeparatedByString:@"."];
    // 遍歷每一個(gè)path
    for (NSString *oldKey in oldKeys) {
        
        NSUInteger start = [oldKey rangeOfString:@"["].location;
        if (start != NSNotFound) {  
            // 1. 處理數(shù)組項(xiàng)
            // 裁減出數(shù)組名
            NSString *prefixKey = [oldKey substringToIndex:start];
            NSString *indexKey = prefixKey;
            if (prefixKey.length) {
                MJPropertyKey *propertyKey = [[MJPropertyKey alloc] init];
                propertyKey.name = prefixKey;
                [propertyKeys addObject:propertyKey];
                // 裁剪出索引值
                indexKey = [oldKey stringByReplacingOccurrencesOfString:prefixKey withString:@""];
            }
            
            /** 解析索引 **/
            NSArray *cmps = [[indexKey stringByReplacingOccurrencesOfString:@"[" withString:@""] componentsSeparatedByString:@"]"];
            // cmps數(shù)組中只有索引值字符串和一個(gè)空字符串
            for (NSInteger i = 0; i<cmps.count - 1; i++) {
                MJPropertyKey *subPropertyKey = [[MJPropertyKey alloc] init];
                subPropertyKey.type = MJPropertyKeyTypeArray;
                subPropertyKey.name = cmps[i];
                [propertyKeys addObject:subPropertyKey];
            }
        } else {
            // 沒有索引的json key部分或者是本來不需替換的json key
            MJPropertyKey *propertyKey = [[MJPropertyKey alloc] init];
            propertyKey.name = oldKey;
            [propertyKeys addObject:propertyKey];
        }
    }
    
    return propertyKeys;
}
- (void)setPorpertyKeys:(NSArray *)propertyKeys forClass:(Class)c
{
    if (propertyKeys.count == 0) return;
    self.propertyKeysDict[NSStringFromClass(c)] = propertyKeys;
}
  • 使用MJPropertyKey對(duì)象來包裝多級(jí)映射的每一級(jí)path琉预。每個(gè)對(duì)象都用一個(gè)枚舉類型屬性表明它的使用方式董饰。
typedef enum {
    // 表明當(dāng)前對(duì)象的值當(dāng)做字典的key
    MJPropertyKeyTypeDictionary = 0, 
    // 表明當(dāng)前對(duì)象的值當(dāng)做數(shù)組的索引值使用
    MJPropertyKeyTypeArray 
} MJPropertyKeyType;
  • 包裝MJPropertyKey對(duì)象時(shí)有一個(gè)細(xì)節(jié):只有包裝索引時(shí)設(shè)置了它的MJPropertyKeyTypeMJPropertyKeyTypeArray。而卻沒有看到設(shè)置MJPropertyKeyTypeDictionary類型的地方圆米。
    因?yàn)槊杜e類型的屬性卒暂,如果不設(shè)置,會(huì)默認(rèn)設(shè)置成枚舉的第一個(gè)成員娄帖,這里即是默認(rèn)設(shè)置成MJPropertyKeyTypeDictionary也祠。
  • 至于屬性和json key不一致,需要替換的情況近速。就將json key看做只有一個(gè)path的keyPath诈嘿,仍然采用上面的方法堪旧。
  • MJProperty中聲明了一個(gè)propertyKeysDict屬性,以模型類名為key奖亚,value為MJPropertyKey對(duì)象的數(shù)組淳梦,用來保存每一級(jí)path

看完多級(jí)映射,現(xiàn)在再回到+ (NSMutableArray *)properties方法昔字,看MJ又如何處理數(shù)組中的模型, 先看調(diào)用:

[property setObjectClassInArray:[self propertyObjectClassInArray:property.name] forClass:self];
  • + propertyObjectClassInArray也是比照白名單獲取方式分析爆袍。這里是根據(jù)屬性名獲取json中對(duì)應(yīng)的數(shù)組的key。
- (void)setObjectClassInArray:(Class)objectClass forClass:(Class)c
{
    if (!objectClass) return;
    self.objectClassInArrayDict[NSStringFromClass(c)] = objectClass;
}
  • 數(shù)組中的模型處理要簡(jiǎn)單的多作郭,直接保存在字典中陨囊,key仍然是外部模型類的類名。
  • objectClassInArrayDictMJProperty的一個(gè)字典屬性夹攒,key是模型類名蜘醋,value則是數(shù)組中的模型類型。

接下來我們來看上面核心代碼中缺省的block:

    // 遍歷每一個(gè)屬性:包裝成了MJProperty對(duì)象
    [clazz mj_enumerateProperties:^(MJProperty *property, BOOL *stop) {
        @try {
            // 0.檢測(cè)是否被忽略
            // 屬性不在白名單中
            if (allowedPropertyNames.count && ![allowedPropertyNames containsObject:property.name]) return;
            // 屬性在黑名單中
            if ([ignoredPropertyNames containsObject:property.name]) return;
            
            // 1.取出屬性值
            id value;
            // 獲取拆解json keyPath得到的數(shù)組芹助,每個(gè)path包裝成了MJPropertyKey對(duì)象
            // 內(nèi)部就是MJProperty中的propertyKeysDict字典屬性
            NSArray *propertyKeyses = [property propertyKeysForClass:clazz];
            for (NSArray *propertyKeys in propertyKeyses) {
                // json對(duì)象
                value = keyValues;
                // propertyKeys: 保存一個(gè)被拆解的json keyPath
                // propertyKey : json keyPath 中的一個(gè)path
                for (MJPropertyKey *propertyKey in propertyKeys) {
                    // kvc 從json字典中取值
                    // 如果是多級(jí)映射堂湖,每次返回的值為下一級(jí)對(duì)象,直到需要的值
                    value = [propertyKey valueInObject:value];
                }
                if (value) break;
            }
            /**
             * json數(shù)據(jù)中獲得的值
             * 1. 有自定義的值處理方式状土,執(zhí)行自定義
             * 2. 無自定義方式无蜂,則將數(shù)據(jù)的類型轉(zhuǎn)換為屬性的類型賦值(保持類型一致)
             */
            
            // 值的進(jìn)一步處理如將字符串處理成NSURL或NSDate,如果不需處理則直接返回原值
            id newValue = [clazz mj_getNewValueFromObject:self oldValue:value property:property];
            if (newValue != value) { // 有過濾后的新值
                [property setValue:newValue forObject:self];
                return;
            }
            
            // 如果沒有值,就直接返回
            if (!value || value == [NSNull null]) return;
            
            // 2.復(fù)雜處理 -- 處理模型中的模型和數(shù)組中的模型
            // 屬性的類型
            MJPropertyType *type = property.type;
            // 屬性的類型所屬的類,基本數(shù)據(jù)類型則為nil
            Class propertyClass = type.typeClass;
            // 獲取數(shù)組中的自定義類型,內(nèi)部就是一個(gè)字典取值
            Class objectClass = [property objectClassInArrayForClass:[self class]];

            // (屬性)不可變 -> 可變處理(獲取的值)
            if (propertyClass == [NSMutableArray class] && [value isKindOfClass:[NSArray class]]) {
                value = [NSMutableArray arrayWithArray:value];
            } else if (propertyClass == [NSMutableDictionary class] && [value isKindOfClass:[NSDictionary class]]) {
                value = [NSMutableDictionary dictionaryWithDictionary:value];
            } else if (propertyClass == [NSMutableString class] && [value isKindOfClass:[NSString class]]) {
                value = [NSMutableString stringWithString:value];
            } else if (propertyClass == [NSMutableData class] && [value isKindOfClass:[NSData class]]) {
                value = [NSMutableData dataWithData:value];
            }

            if (!type.isFromFoundation && propertyClass) {
                // 自定義的模型類,此處是處理模型中的模型
                // 進(jìn)一步的json轉(zhuǎn)模型
                value = [propertyClass mj_objectWithKeyValues:value context:context];
            } else if (objectClass) {
                // 數(shù)組中的模型
                if (objectClass == [NSURL class] && [value isKindOfClass:[NSArray class]]) {
                    // 數(shù)組中的模型是NSURL
                    // string array -> url array
                    NSMutableArray *urlArray = [NSMutableArray array];
                    for (NSString *string in value) {
                        if (![string isKindOfClass:[NSString class]]) continue;
                        [urlArray addObject:string.mj_url];
                    }
                    value = urlArray;
                } else {
                    // 處理數(shù)組中嵌套模型蒙谓,返回的數(shù)組中是json轉(zhuǎn)model過的
                    value = [objectClass mj_objectArrayWithKeyValuesArray:value context:context];
                }
            } else {
                if (propertyClass == [NSString class]) {
                    // 屬性類型是NSString斥季,值的類型是NSNumber或NSURL
                    if ([value isKindOfClass:[NSNumber class]]) {
                        // NSNumber -> NSString
                        value = [value description];
                    } else if ([value isKindOfClass:[NSURL class]]) {
                        // NSURL -> NSString
                        value = [value absoluteString];
                    }
                } else if ([value isKindOfClass:[NSString class]]) {
                    // 屬性類型是基本數(shù)據(jù)類型或者NSURL,但是對(duì)應(yīng)的json的值是字符串類型
                    if (propertyClass == [NSURL class]) {
                        // NSString -> NSURL
                        // url字符串轉(zhuǎn)碼
                        value = [value mj_url];
                        // 基本數(shù)據(jù)類型不能使用反射機(jī)制獲取累驮,通過設(shè)置標(biāo)識(shí)來確定
                    } else if (type.isNumberType) {
                        // 數(shù)字是以字符串形式呈現(xiàn)的
                        NSString *oldValue = value;
                        if (type.typeClass == [NSDecimalNumber class]) {
                            // 浮點(diǎn)數(shù)類酣倾,提供更精確的浮點(diǎn)數(shù)計(jì)算方式,不可變類
                            value = [NSDecimalNumber decimalNumberWithString:oldValue];
                        } else {
                            // 字符串轉(zhuǎn)換成數(shù)組
                            value = [numberFormatter_ numberFromString:oldValue];
                        }
                        
                        // json中的布爾類型數(shù)據(jù)是字符串顯示的
                        if (type.isBoolType) {
                            // 字符串轉(zhuǎn)BOOL(字符串沒有charValue方法)
                            // 系統(tǒng)會(huì)調(diào)用字符串的charValue轉(zhuǎn)為BOOL類型
                            NSString *lower = [oldValue lowercaseString];
                            if ([lower isEqualToString:@"yes"] || [lower isEqualToString:@"true"]) {
                                value = @YES;
                            } else if ([lower isEqualToString:@"no"] || [lower isEqualToString:@"false"]) {
                                value = @NO;
                            }
                        }
                    }
                }
                
                // value和property類型不匹配
                if (propertyClass && ![value isKindOfClass:propertyClass]) {
                    value = nil;
                }
            }
            
            // 3.賦值
            [property setValue:value forObject:self];
        } @catch (NSException *exception) {
            MJExtensionBuildError([self class], exception.reason);
            MJExtensionLog(@"%@", exception);
        }
    }];
  • +mj_getNewValueFromObject 方法用于對(duì)值的進(jìn)一步處理谤专。內(nèi)部邏輯和白名單獲取方式類似,這里也不再分析躁锡。

  • 異常捕獲--詳情可以戳這個(gè)鏈接@_超

      @try {
          // 可能會(huì)發(fā)生異常的代碼
      } @catch (NSException *exception) {
          // 捕獲異常后的處理
      } 
      @try中的代碼塊如果發(fā)生異常,就會(huì)被catch捕獲并形成NSException對(duì)象置侍,可以使用該對(duì)象的reason或userInfo查看原因映之。如果沒有異常則不執(zhí)行catch。此外還有一個(gè)@finally(貌似很少用)蜡坊, 不管是否異常都會(huì)執(zhí)行杠输,即使@try或@catch中有return語句。
    
  • 模型中的模型的處理方式就是使用新模型類將整個(gè)流程再走一次秕衙。
    來看看如何從json數(shù)據(jù)取值

- (id)valueInObject:(id)object
{
    if ([object isKindOfClass:[NSDictionary class]] && self.type == MJPropertyKeyTypeDictionary) {
        // 從json字典中根據(jù)原始key取值
        return object[self.name];
    } else if ([object isKindOfClass:[NSArray class]] && self.type == MJPropertyKeyTypeArray) {
        // json中的數(shù)組對(duì)象蠢甲,index即保存過的索引
        NSArray *array = object;
        NSUInteger index = self.name.intValue;
        if (index < array.count) return array[index];
        return nil;
    }
    return nil;
}

接下來就是處理數(shù)組中的模型:

+ (NSMutableArray *)mj_objectArrayWithKeyValuesArray:(id)keyValuesArray context:(NSManagedObjectContext *)context
{
    // 如果是JSON字符串,新模型的元數(shù)據(jù)是JSON字符串
    keyValuesArray = [keyValuesArray mj_JSONObject];
    
    // 1.判斷真實(shí)性
    MJExtensionAssertError([keyValuesArray isKindOfClass:[NSArray class]], nil, [self class], @"keyValuesArray參數(shù)不是一個(gè)數(shù)組");
    
    // 如果數(shù)組里面放的是Foundation類的數(shù)據(jù)
    if ([MJFoundation isClassFromFoundation:self])
        return [NSMutableArray arrayWithArray:keyValuesArray];

    // 2.創(chuàng)建數(shù)組保存轉(zhuǎn)換后的模型
    NSMutableArray *modelArray = [NSMutableArray array];
    
    // 3.遍歷數(shù)組中的每個(gè)模型的元數(shù)據(jù)
    for (NSDictionary *keyValues in keyValuesArray) {
        if ([keyValues isKindOfClass:[NSArray class]]){
            // 數(shù)組當(dāng)中還有模型据忘,遞歸
            [modelArray addObject:[self mj_objectArrayWithKeyValuesArray:keyValues context:context]];
        } else {
            // 對(duì)數(shù)組中嵌套的模型進(jìn)行json-model轉(zhuǎn)換
            id model = [self mj_objectWithKeyValues:keyValues context:context];
            if (model) [modelArray addObject:model];
        }
    }
    
    return modelArray;
}

然后就是保證屬性的類型和值的類型保持一致鹦牛,最后通過KVC方式為模型類屬性賦值搞糕,整個(gè)轉(zhuǎn)換就完成了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末能岩,一起剝皮案震驚了整個(gè)濱河市寞宫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌拉鹃,老刑警劉巖辈赋,帶你破解...
    沈念sama閱讀 217,826評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異膏燕,居然都是意外死亡钥屈,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門坝辫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來篷就,“玉大人,你說我怎么就攤上這事近忙〗咭担” “怎么了?”我有些...
    開封第一講書人閱讀 164,234評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵及舍,是天一觀的道長(zhǎng)未辆。 經(jīng)常有香客問我,道長(zhǎng)锯玛,這世上最難降的妖魔是什么咐柜? 我笑而不...
    開封第一講書人閱讀 58,562評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮攘残,結(jié)果婚禮上拙友,老公的妹妹穿的比我還像新娘。我一直安慰自己歼郭,他們只是感情好遗契,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,611評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著病曾,像睡著了一般姊途。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上知态,一...
    開封第一講書人閱讀 51,482評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音立叛,去河邊找鬼负敏。 笑死,一個(gè)胖子當(dāng)著我的面吹牛秘蛇,可吹牛的內(nèi)容都是我干的其做。 我是一名探鬼主播顶考,決...
    沈念sama閱讀 40,271評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼妖泄!你這毒婦竟也來了驹沿?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,166評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤蹈胡,失蹤者是張志新(化名)和其女友劉穎渊季,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體罚渐,經(jīng)...
    沈念sama閱讀 45,608評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡却汉,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,814評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了荷并。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片合砂。...
    茶點(diǎn)故事閱讀 39,926評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖源织,靈堂內(nèi)的尸體忽然破棺而出翩伪,到底是詐尸還是另有隱情,我是刑警寧澤谈息,帶...
    沈念sama閱讀 35,644評(píng)論 5 346
  • 正文 年R本政府宣布缘屹,位于F島的核電站,受9級(jí)特大地震影響黎茎,放射性物質(zhì)發(fā)生泄漏囊颅。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,249評(píng)論 3 329
  • 文/蒙蒙 一傅瞻、第九天 我趴在偏房一處隱蔽的房頂上張望踢代。 院中可真熱鬧,春花似錦嗅骄、人聲如沸胳挎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽慕爬。三九已至,卻和暖如春屏积,著一層夾襖步出監(jiān)牢的瞬間医窿,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工炊林, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留姥卢,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,063評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像独榴,于是被迫代替她去往敵國(guó)和親僧叉。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,871評(píng)論 2 354