iOS組件化的那些事 - CTMediator

轉(zhuǎn)載自: 知乎-henry磊 iOS組件化的那些事 - CTMediator

前言

在組件化之前井濒,app都是在一個(gè)工程里開(kāi)發(fā)的导狡,開(kāi)發(fā)的人員也是比較少的,業(yè)務(wù)發(fā)展也不是非吵ㄎ耍快,項(xiàng)目中不引用組件化開(kāi)發(fā)也是合適的。但是當(dāng)開(kāi)發(fā)人員越來(lái)越多潘悼,代碼量也就越來(lái)越多,業(yè)務(wù)也就越來(lái)越復(fù)雜爬橡,這時(shí)候單一的開(kāi)發(fā)模式會(huì)顯露出一些弊端:

  • 耦合代碼嚴(yán)重【代碼沒(méi)有明確的約束挥等,越來(lái)越臃腫】
  • 開(kāi)發(fā)效率不高【難以維護(hù),維護(hù)成本過(guò)高】

為了解決這些問(wèn)題堤尾,于是出現(xiàn)了組件化開(kāi)發(fā)的策略肝劲,擁有以下好處:

  • 方便針對(duì)性測(cè)試
  • 研發(fā)人員維護(hù)不同Module【提高開(kāi)發(fā)效率,降低維護(hù)成本】

目前組件化開(kāi)發(fā)的方式大約有三種:url - block郭宝、protocol - class 以及CTMediator target - action方案辞槐,本篇主要是本人項(xiàng)目中使用到的CTMediator target - action方案,將從源碼分析粘室,以及其優(yōu)劣性榄檬。

一、準(zhǔn)備

了解下面的概念衔统,對(duì)理解swift的靜態(tài)語(yǔ)言特性是有幫助的鹿榜。
問(wèn): 動(dòng)態(tài)語(yǔ)言海雪、靜態(tài)語(yǔ)言、動(dòng)態(tài)類型語(yǔ)言舱殿、靜態(tài)類型語(yǔ)言奥裸、編譯性語(yǔ)言、解釋性語(yǔ)言區(qū)別沪袭?
1.1 編譯型語(yǔ)言

需通過(guò)編譯器【compliler】將源代碼編譯成機(jī)器碼湾宙,之后才能執(zhí)行的語(yǔ)言。一般需經(jīng)過(guò)編譯【compile】冈绊、鏈接【linker】這兩個(gè)步驟侠鳄,編譯是把源代碼編譯成機(jī)器碼,鏈接是把各個(gè)模塊的機(jī)器碼和依賴庫(kù)串聯(lián)起來(lái)生成可執(zhí)行文件死宣。
優(yōu)點(diǎn):

  • 編譯器一般會(huì)有預(yù)編譯的過(guò)程對(duì)代碼進(jìn)行優(yōu)化伟恶。因?yàn)榫幾g只做一次,運(yùn)行時(shí)不需要編譯毅该,所以編譯型語(yǔ)言的程序執(zhí)行效率高知押。

  • 可以脫離語(yǔ)言環(huán)境獨(dú)立運(yùn)行。

缺點(diǎn):

  • 編譯之后如果需要修改某個(gè)模塊就需要重新編譯鹃骂。編譯的時(shí)候根據(jù)對(duì)應(yīng)的運(yùn)行環(huán)境生成機(jī)器碼台盯,不同的操作系統(tǒng)之間移植就會(huì)有問(wèn)題。需要根據(jù)運(yùn)行的操作系統(tǒng)環(huán)境編譯不同的可執(zhí)行文件畏线。

語(yǔ)言: C静盅、C++、Object-C以及Swift

1.2 解釋型語(yǔ)言

解釋型語(yǔ)言的程序不需要編譯寝殴,相比編譯型語(yǔ)言省了道工序蒿叠,解釋性語(yǔ)言在運(yùn)行程序的時(shí)候才逐行翻譯。
優(yōu)點(diǎn):

  • 良好平臺(tái)兼容性蚣常,在任何環(huán)境都可以運(yùn)行
  • 靈活市咽,修改代碼的時(shí)候修改就可以,可以快速部署

缺點(diǎn):

  • 每次運(yùn)行都要解釋一遍抵蚊,性能不如編譯型語(yǔ)言施绎。

語(yǔ)言:JavaScript、Python贞绳、PHP谷醉、Ruby

1.3 動(dòng)態(tài)語(yǔ)言

是一類運(yùn)行時(shí)可以改變其結(jié)構(gòu)的語(yǔ)言:已有的函數(shù),對(duì)象可以被刪除或者其他結(jié)構(gòu)上的變化冈闭。通俗點(diǎn)說(shuō)是運(yùn)行時(shí)可以根據(jù)某些條件改變自身結(jié)構(gòu)俱尼。
語(yǔ)言: Object-C、C#萎攒、JavaScript遇八、PHP矛绘、Python
【Object-C是編譯型語(yǔ)言,但也是動(dòng)態(tài)語(yǔ)言刃永,得益于特有的run time機(jī)制货矮,OC代碼是可以在運(yùn)行時(shí)插入、替換方法的】

1.4 靜態(tài)語(yǔ)言

運(yùn)行時(shí)結(jié)構(gòu)不可變的語(yǔ)言就是靜態(tài)語(yǔ)言揽碘。
語(yǔ)言:Java次屠、C园匹、Swift

1.5 動(dòng)態(tài)類型語(yǔ)言

動(dòng)態(tài)類型語(yǔ)言和動(dòng)態(tài)語(yǔ)言是完全不同的兩個(gè)概念雳刺。
動(dòng)態(tài)類型語(yǔ)言是指在運(yùn)行期間才去做數(shù)據(jù)類型檢查的語(yǔ)言,說(shuō)的是數(shù)據(jù)類型裸违,動(dòng)態(tài)語(yǔ)言說(shuō)的是運(yùn)行是改變結(jié)構(gòu)掖桦,說(shuō)的是代碼結(jié)構(gòu)。
動(dòng)態(tài)類型語(yǔ)言的數(shù)據(jù)類型不是在編譯階段決定的供汛,而是把類型綁定延后到了運(yùn)行階段枪汪。
語(yǔ)言:Python、Ruby怔昨、JavaScript雀久、swift、PHP趁舀。

1.6 靜態(tài)類型語(yǔ)言

靜態(tài)語(yǔ)言的數(shù)據(jù)類型是在編譯其間確定的赖捌,寫編寫代碼的時(shí)候要明確確定變量的數(shù)據(jù)類型。
語(yǔ)言:C矮烹、C++越庇、C#、Java奉狈、Object-C卤唉。

拓展:

@objc 是用來(lái)將Swift 的API導(dǎo)出給Objective-C和Objective-C runtime使用的,如果你的類繼承自O(shè)bjective-C【如NSObject】將會(huì)自動(dòng)被編譯器插入@objc標(biāo)識(shí)仁期。加了@objc標(biāo)識(shí)的方法桑驱、屬性無(wú)法保證都會(huì)被運(yùn)行時(shí)調(diào)用,因?yàn)镾wift會(huì)做靜態(tài)優(yōu)化跛蛋。要想完全被動(dòng)態(tài)調(diào)用碰纬,必須使用dynamic修飾。使用dynamic修飾將會(huì)隱式的加上@objc標(biāo)識(shí)问芬。

二悦析、CTMediator源碼

swift由于靜態(tài)的本質(zhì),至今沒(méi)有什么好的解耦方式此衅,因?yàn)殪o態(tài)語(yǔ)言的類型是在編譯時(shí)就確定的【上面準(zhǔn)備的重要性】强戴,注定了它無(wú)法像上面一樣使用字符串就能獲取到亭螟,所以現(xiàn)在iOS常用的解耦框架都是OC編寫的,如果想要使用解耦框架骑歹,那就只能混編啦预烙!

CTMediator代碼結(jié)構(gòu)圖

image

下面我們將講解CTMediator源碼具體內(nèi)容。

2.1 target-action工作圖

target-action 中間采取了Runtime來(lái)完成調(diào)用道媚。

image

2.2 本地調(diào)用入口

下面是本地調(diào)用的CTMediator的源代碼:
本地組件調(diào)用:本地組件A在一處調(diào)用[[CTMediator sharedInstance] performTarget:targetName action:actionName params:@{...}]向CTMediator發(fā)起了跨組件調(diào)用,CTMediator根據(jù)發(fā)送過(guò)來(lái)的target和action,然后經(jīng)過(guò)OC的runtime機(jī)制轉(zhuǎn)為target實(shí)例以及action,最后調(diào)用到目標(biāo)業(yè)務(wù)提供的邏輯,完成要求.
首先利用runtime進(jìn)行反射扁掸,將類字符串和方法字符串轉(zhuǎn)換成類和SEL方法。

/**
 *  本地組件調(diào)用入口
 *
 *  @param targetName 類對(duì)象   OC中類對(duì)象是要Target_為前綴的
 *  @param actionName 方法名稱  最后實(shí)際調(diào)用的是以Action_為前綴的
 *  @param params     參數(shù)
 *  @param shouldCacheTarget 是否緩存拼接后的類對(duì)象
 *
 *  @return return value JSon格式的字符串
 */
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
    //供swift項(xiàng)目使用最域,swift必須要加入模塊名谴分,swift工程內(nèi)獲取Target需要帶上module的名稱
    NSString *swiftModuleName = params[kCTMediatorParamsKeySwiftTargetModuleName];

    // generate target
    NSString *targetClassString = nil;
    if (swiftModuleName.length > 0) {
        targetClassString = [NSString stringWithFormat:@"%@.Target_%@", swiftModuleName, targetName];
    } else {
        // 拼裝類字符串
        targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
    }
    //先從緩存中取對(duì)象
    NSObject *target = self.cachedTarget[targetClassString];
    if (target == nil) {
        //不存在直接根據(jù)字符串創(chuàng)建類,并且初始化對(duì)象
        Class targetClass = NSClassFromString(targetClassString);
        target = [[targetClass alloc] init];
    }

    // 拼裝方法字符串
    NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
    // 生成SEL
    SEL action = NSSelectorFromString(actionString);
    //先從緩存取镀脂,取不到去創(chuàng)建牺蹄,但是也有可能創(chuàng)建失敗的情況(targetName值不正確)
    if (target == nil) {
        // 這里是處理無(wú)響應(yīng)請(qǐng)求的地方之一,這個(gè)demo做得比較簡(jiǎn)單薄翅,如果沒(méi)有可以響應(yīng)的target沙兰,就直接return了。實(shí)際開(kāi)發(fā)過(guò)程中是可以事先給一個(gè)固定的target專門用于在這個(gè)時(shí)候頂上翘魄,然后處理這種請(qǐng)求的
        [self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
        return nil;
    }
    // 是否緩存該對(duì)象
    if (shouldCacheTarget) {
        self.cachedTarget[targetClassString] = target;
    }
    // 該對(duì)象是否能響應(yīng)調(diào)起該方法
    if ([target respondsToSelector:action]) {
        return [self safePerformAction:action target:target params:params];
    } else {
        // 這里是處理無(wú)響應(yīng)請(qǐng)求的地方鼎天,如果無(wú)響應(yīng),則嘗試調(diào)用對(duì)應(yīng)target的notFound方法統(tǒng)一處理
        SEL action = NSSelectorFromString(@"notFound:");
        if ([target respondsToSelector:action]) {
            return [self safePerformAction:action target:target params:params];
        } else {
            // 這里也是處理無(wú)響應(yīng)請(qǐng)求的地方暑竟,在notFound都沒(méi)有的時(shí)候斋射,這個(gè)demo是直接return了。實(shí)際開(kāi)發(fā)過(guò)程中光羞,可以用前面提到的固定的target頂上的绩鸣。
            [self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
            [self.cachedTarget removeObjectForKey:targetClassString];
            return nil;
        }
    }
}

如果target 和 action 都有值,會(huì)調(diào)用[self safePerformAction:action target:target params:params]方法【【生成的類前面的會(huì)加上Target_,生成的方法前面會(huì)加上Action_】】

- (id)safePerformAction:(SEL)action target:(NSObject *)target params:(NSDictionary *)params
{
    NSMethodSignature* methodSig = [target methodSignatureForSelector:action];
    if(methodSig == nil) {
        return nil;
    }
    const char* retType = [methodSig methodReturnType];

    if (strcmp(retType, @encode(void)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setArgument:&params atIndex:2];
        [invocation setSelector:action];
        [invocation setTarget:target];
        [invocation invoke];
        return nil;
    }

    if (strcmp(retType, @encode(NSInteger)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setArgument:&params atIndex:2];
        [invocation setSelector:action];
        [invocation setTarget:target];
        [invocation invoke];
        NSInteger result = 0;
        [invocation getReturnValue:&result];
        return @(result);
    }

    if (strcmp(retType, @encode(BOOL)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setArgument:&params atIndex:2];
        [invocation setSelector:action];
        [invocation setTarget:target];
        [invocation invoke];
        BOOL result = 0;
        [invocation getReturnValue:&result];
        return @(result);
    }

    if (strcmp(retType, @encode(CGFloat)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setArgument:&params atIndex:2];
        [invocation setSelector:action];
        [invocation setTarget:target];
        [invocation invoke];
        CGFloat result = 0;
        [invocation getReturnValue:&result];
        return @(result);
    }

    if (strcmp(retType, @encode(NSUInteger)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setArgument:&params atIndex:2];
        [invocation setSelector:action];
        [invocation setTarget:target];
        [invocation invoke];
        NSUInteger result = 0;
        [invocation getReturnValue:&result];
        return @(result);
    }

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    return [target performSelector:action withObject:params];
#pragma clang diagnostic pop
}

將消息和消息接受者封裝成一個(gè)對(duì)象纱兑,然后執(zhí)行呀闻。利用target-action生成方法簽名,然后創(chuàng)建NSInvocation對(duì)象潜慎,進(jìn)行執(zhí)行invoke捡多。

這個(gè)方法比較長(zhǎng),但其實(shí)前面就是重復(fù)的代碼铐炫,來(lái)實(shí)現(xiàn)返回類型是基本數(shù)據(jù)類型的調(diào)用垒手,使用的是NSInvocation來(lái)完成調(diào)用;最后使用performSelector實(shí)現(xiàn)返回值是對(duì)象的調(diào)用倒信。 最后performSelector方法前后的#pragma clang diagnostic是消除內(nèi)存警告的預(yù)處理命令

2.2 遠(yuǎn)程調(diào)用入口
遠(yuǎn)程應(yīng)用的調(diào)瓤票帷:遠(yuǎn)程應(yīng)用是通過(guò)openURL的方式,由iOS 系統(tǒng)根據(jù)info.plist里的scheme配置用來(lái)可以找到響應(yīng)的URL的應(yīng)用,應(yīng)用直接通過(guò)AppDelegate接收到URL之后,調(diào)用了CTMediator的OpenURL方法將接收到的信息傳入進(jìn)去.當(dāng)然,CTMediator也可以用CTMediator的openURL:options:方式順便將option也接收,這取決于是否包含了option數(shù)據(jù),傳入U(xiǎn)RL之后,CTMediator進(jìn)行解析URL,將請(qǐng)求的路由到相對(duì)應(yīng)的target-action中,隨后的過(guò)程就變成了上面的本地應(yīng)用調(diào)用過(guò)程了,最終完成了響應(yīng).

/*
 scheme://[target]/[action]?[params]

 url sample:
 aaa://targetA/actionB?id=1234&title=title

 [url query]:  id=1234&title=title
 [url path]:  /actionB
 [url host]:  targetA
 */

- (id)performActionWithUrl:(NSURL *)url completion:(void (^)(NSDictionary *))completion
{
    //url參數(shù)的處理
    NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
    //
    NSString *urlString = [url query];
    for (NSString *param in [urlString componentsSeparatedByString:@"&"]) {
        NSArray *elts = [param componentsSeparatedByString:@"="];
        if([elts count] < 2) continue;
        [params setObject:[elts lastObject] forKey:[elts firstObject]];
    }

    // 這里這么寫主要是出于安全考慮,防止黑客通過(guò)遠(yuǎn)程方式調(diào)用本地模塊。這里的做法足以應(yīng)對(duì)絕大多數(shù)場(chǎng)景榜掌,如果要求更加嚴(yán)苛优妙,也可以做更加復(fù)雜的安全邏輯。
    NSString *actionName = [url.path stringByReplacingOccurrencesOfString:@"/" withString:@""];
    if ([actionName hasPrefix:@"native"]) {
        return @(NO);
    }

    // 這個(gè)demo針對(duì)URL的路由處理非常簡(jiǎn)單憎账,就只是取對(duì)應(yīng)的target名字和method名字套硼,但這已經(jīng)足以應(yīng)對(duì)絕大部份需求。如果需要拓展胞皱,可以在這個(gè)方法調(diào)用之前加入完整的路由邏輯
    id result = [self performTarget:url.host action:actionName params:params shouldCacheTarget:NO];
    if (completion) {
        if (result) {
            completion(@{@"result":result});
        } else {
            completion(nil);
        }
    }
    return result;
}

拓展:

NSClassFromString 通過(guò)字符串的名稱來(lái)獲取一個(gè)類邪意,可以根據(jù)Target來(lái)進(jìn)行獲取
NSSelectorFromString 通過(guò)字符串(已存在的方法名稱)獲取一個(gè)SEL

三、項(xiàng)目
從搜索結(jié)果頁(yè)-另個(gè)模塊詳情頁(yè)界面如上,跨越了兩個(gè)模塊,項(xiàng)目采用了CTMediator的Target-Action模式.下面按照?qǐng)?zhí)行的順序截圖如下:

image

3.1 點(diǎn)擊tableViewCell->func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)

3.1.png

3.2 進(jìn)入openBrokerDetailScene,開(kāi)始調(diào)CTMediator【公司采用Viper的變形版架構(gòu)】

3.2.jpeg

3.3 開(kāi)始進(jìn)入EngineToBroker_viewController,調(diào)用self.performTarget("Broker", action: "brokerDetailVC", params: params, shouldCacheTarget: false)【返回vc】

image

通過(guò)調(diào)用self.performTarget("Broker", action: *****) ,會(huì)執(zhí)行下面方法

3.3.2.jpeg

3.4 最終得到了ViewController,回到即將跳轉(zhuǎn)到

3.45.jpeg

這樣就完成了CTMediator在Swift中的使用反砌。

作者:國(guó)孩
鏈接:https://juejin.im/post/68799807

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末雾鬼,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子于颖,更是在濱河造成了極大的恐慌呆贿,老刑警劉巖嚷兔,帶你破解...
    沈念sama閱讀 218,640評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件森渐,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡冒晰,警方通過(guò)查閱死者的電腦和手機(jī)同衣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,254評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)壶运,“玉大人耐齐,你說(shuō)我怎么就攤上這事〗椋” “怎么了埠况?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,011評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)棵癣。 經(jīng)常有香客問(wèn)我辕翰,道長(zhǎng),這世上最難降的妖魔是什么狈谊? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,755評(píng)論 1 294
  • 正文 為了忘掉前任喜命,我火速辦了婚禮,結(jié)果婚禮上河劝,老公的妹妹穿的比我還像新娘壁榕。我一直安慰自己,他們只是感情好赎瞎,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,774評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布牌里。 她就那樣靜靜地躺著,像睡著了一般务甥。 火紅的嫁衣襯著肌膚如雪牡辽。 梳的紋絲不亂的頭發(fā)上贪染,一...
    開(kāi)封第一講書(shū)人閱讀 51,610評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音催享,去河邊找鬼杭隙。 笑死,一個(gè)胖子當(dāng)著我的面吹牛因妙,可吹牛的內(nèi)容都是我干的痰憎。 我是一名探鬼主播,決...
    沈念sama閱讀 40,352評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼攀涵,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼铣耘!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起以故,我...
    開(kāi)封第一講書(shū)人閱讀 39,257評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤蜗细,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后怒详,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體炉媒,經(jīng)...
    沈念sama閱讀 45,717評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,894評(píng)論 3 336
  • 正文 我和宋清朗相戀三年昆烁,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了吊骤。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,021評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡静尼,死狀恐怖白粉,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情鼠渺,我是刑警寧澤鸭巴,帶...
    沈念sama閱讀 35,735評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站拦盹,受9級(jí)特大地震影響鹃祖,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜掌敬,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,354評(píng)論 3 330
  • 文/蒙蒙 一惯豆、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧奔害,春花似錦楷兽、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,936評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春揭厚,著一層夾襖步出監(jiān)牢的瞬間却特,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,054評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工筛圆, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留裂明,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,224評(píng)論 3 371
  • 正文 我出身青樓太援,卻偏偏與公主長(zhǎng)得像闽晦,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子提岔,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,974評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容