Android AOP

本文是對(duì)網(wǎng)絡(luò)中相關(guān)文章的總結(jié)林说,再加上自己的相關(guān)見解得出的,若有侵權(quán)請(qǐng)及時(shí)聯(lián)系

參考的大佬博客:

白話 Android AOP ---> 這個(gè)說的是 ASM + Transform 的方式實(shí)現(xiàn)AOP(難度比較大,維護(hù)起來(lái)比較難)
深入理解Android之AOP ---> 這個(gè)說的是 AspectJ 實(shí)現(xiàn) AOP(相當(dāng)于別人的三方庫(kù)枚钓,使用起來(lái)比較簡(jiǎn)單,容易上手)
Android AOP方案(一)——AspectJ ---> 基本語(yǔ)法加實(shí)現(xiàn)
Android AspectJ詳解 ---> 更為詳細(xì)的基本語(yǔ)法加實(shí)現(xiàn) 主要可以看這篇博客
談?wù)凙ndroid AOP技術(shù)方案

AOP簡(jiǎn)介

AOP(Aspect Oriented Programming 的縮寫),意為:面向切面編程筐带,和OOP(Object Oriented Programming,面向?qū)ο缶幊?以對(duì)象為核心不同缤灵,AOP 則是針對(duì)業(yè)務(wù)處理過程中的相同或者相似的代碼邏輯(切面)進(jìn)行提取伦籍,然后統(tǒng)一處理,它所面對(duì)的是處理過程中的某個(gè)步驟或階段腮出。這兩種設(shè)計(jì)思想在目標(biāo)上有著本質(zhì)的差異帖鸦,但是 AOP 和 OOP 并不是對(duì)立的,相反胚嘲,巧妙的結(jié)合這兩種思想來(lái)指導(dǎo)代碼編寫作儿,會(huì)讓我們的代碼保持可重用性的同時(shí),顯著降低各個(gè)部分之間的耦合度馋劈。

OOP 和 AOP 都是方法論攻锰,是我個(gè)人認(rèn)為對(duì)這兩種思想最準(zhǔn)確的描述和總結(jié)。

自我總結(jié):AOP是一種思想妓雾,其目的是為了完成解耦娶吞,將同一功能從代碼中抽離,然后使用輕量級(jí)的方式注入到代碼中君珠,以實(shí)現(xiàn)某種功能(相同的邏輯在同一個(gè)地方處理)寝志;從這種方面來(lái)想,在 BaseActivity 和 在Application 中添加 ActivityLifecycle 監(jiān)聽的也是一種AOP的實(shí)現(xiàn)方式(可以理解為策添,這種是有現(xiàn)成的切面的AOP)

學(xué)習(xí)之前需要理解一些AOP術(shù)語(yǔ)

AOP術(shù)語(yǔ):

  • Cross-cutting concerns(橫切關(guān)注點(diǎn)):監(jiān)管面向?qū)ο竽P椭写蠖鄶?shù)類會(huì)實(shí)現(xiàn)單一特定的功能材部,但通常也會(huì)開放一些通用的附屬功能給其他類。例如唯竹,我們喜歡在數(shù)據(jù)訪問層中的類添加日志乐导,同時(shí)也希望當(dāng)UI層中一個(gè)縣城進(jìn)入或者退出調(diào)用一個(gè)方法時(shí)添加日志。監(jiān)管每個(gè)類都有一個(gè)區(qū)別于其他類的主要功能浸颓,但在代碼里物臂,仍然經(jīng)常需要添加一些相同的附屬功能旺拉。
  • Advice(通知):注入到class文件中的代碼。典型的Advice類型有before棵磷、after和around蛾狗,分別表示在目標(biāo)方法執(zhí)行之前、執(zhí)行后和完全代替目標(biāo)方法執(zhí)行的代碼仪媒。除了在方法中注入代碼沉桌,也可能會(huì)對(duì)代碼做其他修改,比如在一個(gè)class中增加字段或者接口算吩。
  • Joint Point(連接點(diǎn)):程序中可能作為代碼注入目標(biāo)的特定的點(diǎn)留凭,例如一個(gè)方法調(diào)用或者方法入口。
  • Pointcut(切入點(diǎn)偎巢,切點(diǎn)):告訴代碼注入工具蔼夜,在任何注入一段特定代碼的表達(dá)式。例如压昼,在哪些joint points應(yīng)用一個(gè)特定的Advice求冷。切入點(diǎn)可以選擇唯一一個(gè),比如執(zhí)行某一個(gè)方法巢音,也可以有多個(gè)選擇遵倦,比如,標(biāo)記了一個(gè)定義成@DebugLog的自定義注解的所有方法官撼。
  • Aspect(切面):Pointcut和Advice的組合看做切面。例如似谁,我們?cè)趹?yīng)用中通過定義一個(gè)pointcut和給定恰當(dāng)?shù)腶dvice傲绣,添加一個(gè)日志切面。
  • Weaving(織入):注入代碼(advices)到目標(biāo)位置(joint points)的過程

AOP的Android實(shí)現(xiàn)

實(shí)現(xiàn)AOP的技術(shù)巩踏,主要分為兩大類:

  1. 采用動(dòng)態(tài)代理技術(shù)秃诵,利用截取消息的方式,對(duì)該信息進(jìn)行裝飾塞琼,以取代原有對(duì)象行為的執(zhí)行
  2. 采用靜態(tài)織入的方式菠净,引入特定的語(yǔ)句創(chuàng)建“方面”,從而使得編譯器可以在編譯期間織入有關(guān)“方面的代碼

AOP是一種思想彪杉,要使用這種思想就需要擁有這種思想的工具毅往,下面列舉我現(xiàn)在知道的AOP的實(shí)現(xiàn)方式:

  • ASM+Transformer庫(kù):ASM是一個(gè)通用的Java字節(jié)碼操作和分析框架, Transfrom允許第三方插件在經(jīng)過編譯的 .class 文件轉(zhuǎn)換為 .dex 文件之前對(duì)其進(jìn)行操縱。ASM的class的字節(jié)碼過于復(fù)雜派近,總是出錯(cuò)攀唯,開發(fā)成本非常高,上手難度很大渴丸,但是ASM庫(kù)非常強(qiáng)大侯嘀,更加靈活
  • AsepcJ:它是一種幾乎和Java完全一樣的語(yǔ)言另凌,而且完全兼容Java(AspectJ應(yīng)該就是一種擴(kuò)展Java,但它不是像Groovy那樣的拓展)戒幔。當(dāng)然吠谢,除了使用AspectJ特殊的語(yǔ)言外,AspectJ還支持原生的Java诗茎,只要加上對(duì)應(yīng)的AspectJ注解就好囊卜。所以,使用AspectJ有兩種方法:
    1. 完全使用AspectJ的語(yǔ)言错沃。這語(yǔ)言一點(diǎn)也不難栅组,和Java幾乎一樣,也能在AspectJ中調(diào)用Java的任何類庫(kù)枢析。AspectJ只是多了一些關(guān)鍵詞罷了玉掸。
    2. 或者使用純Java語(yǔ)言開發(fā),然后使用AspectJ注解醒叁,簡(jiǎn)稱 @AspectJ司浪。
  • Hugo:另一個(gè)是Jake大神實(shí)現(xiàn)的Hugo庫(kù)
  • Lancet:一個(gè)輕量級(jí)Android AOP框架

以下以AsepcJ來(lái)講解AOP

AsepcJ的使用實(shí)例

環(huán)境配置

通過插件的形式來(lái)配置AspectJ環(huán)境。 具體可見AspectJX Github地址

  1. 在項(xiàng)目根目錄的build.gradle里依賴AspectJX
buildscript {
    dependencies {
        classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.4'
    }
}
  1. 在需要支持AspectJ的module的build.gradle文件中聲明插件把沼。
apply plugin: 'android-aspectjx'

在編譯階段AspectJ會(huì)遍歷工程中所有class文件(包括第三方類庫(kù)的class)尋找符合條件的切入點(diǎn)啊易,為加快這個(gè)過程或縮小代碼織入范圍,我們可以使用exclude排除掉指定包名的class饮睬。

# app/build.gradle
aspectjx {
    //排除所有package路徑中包含`android.support`的class文件及庫(kù)(jar文件)
    exclude 'android.support'
}

在debug階段我們更注重編譯速度租谈,可以關(guān)閉代碼織入。

# app/build.gradle
aspectjx {
    //關(guān)閉AspectJX功能
    enabled false
}

基本使用

  • @Aspect 用它聲明一個(gè)類捆愁,表示一個(gè)需要執(zhí)行的切面割去。
  • @Pointcut 聲明一個(gè)切點(diǎn)。
  • @Before/@After/@Around/...(統(tǒng)稱為Advice類型) 聲明在切點(diǎn)前昼丑、后呻逆、中執(zhí)行切面代碼。

舉個(gè)例子:

@Aspect  //聲明一個(gè)切面類菩帝,此處是固定的
public class MethodAspect {

    // 此處指定一個(gè)切點(diǎn)咖城,后面括號(hào)中的是切點(diǎn)表達(dá)式(個(gè)人理解:其表達(dá)的就是一個(gè)join point),詳細(xì)見Aspect基本語(yǔ)法
    @Pointcut("call(* com.wandering.sample.aspectj.Animal.fly(..))")
    public void callMethod() {
    }

    //表示一個(gè)通知呼奢,類型為Before并指定切點(diǎn)為上面callMethod方法所表示的那個(gè)切點(diǎn)
    @Before("callMethod()")
    public void beforeMethodCall(JoinPoint joinPoint) {
        Log.e(TAG, "before->" + joinPoint.getTarget().toString());  //織入的代碼
}}

AsepcJ的基本語(yǔ)法

官網(wǎng)文檔
本處基本摘于: Android AspectJ詳解

Join Point

Joint Point 含義
Method call 方法被調(diào)用
Method execution 方法執(zhí)行
Constructor call 構(gòu)造函數(shù)被調(diào)用
Constructor execution 構(gòu)造函數(shù)執(zhí)行
Static initialization static塊初始化
Field get 讀取屬性
Field set 寫入屬性
Handler 異常處理

Method call 和 Method execution的區(qū)別常拿來(lái)比較宜雀,其實(shí)就是調(diào)用與執(zhí)行的區(qū)別

就拿上面Animal的fly方法舉例。demo代碼如下:

Animal a = Animal();
a.fly();

如果我們聲明的織入點(diǎn)為call控妻,再假設(shè)Advice類型是before州袒,則織入后代碼結(jié)構(gòu)是這樣的。

Animal a = new Animal();
//...我是織入代碼
a.fly();

如果我們聲明的織入點(diǎn)為execution弓候,則織入后代碼結(jié)構(gòu)就成這樣了郎哭。

public class Animal {
    public void fly() {
        //...我是織入代碼
        Log.e(TAG, "animal fly method:" + this.toString() + "#fly");
    }}

本質(zhì)上的區(qū)別就是織入對(duì)象不同他匪,call被織入在指定方法被調(diào)用的位置上,而execution被織入到指定的方法內(nèi)部夸研。

Pointcut

Pointcuts是具體的切入點(diǎn)邦蜜,基本上Pointcuts 是和 Join Point 相對(duì)應(yīng)的。

Joint Point Pointcuts 表達(dá)式
Method call call(MethodPattern)
Method execution execution(MethodPattern)
Constructor call call(ConstructorPattern)
Constructor execution execution(ConstructorPattern)
Static initialization staticinitialization(TypePattern)
Field get get(FieldPattern)
Field set set(FieldPattern)
Handler handler(TypePattern)

除了上面與 Join Point 對(duì)應(yīng)的選擇外亥至,Pointcuts 還有其他選擇方法悼沈。

Pointcuts表達(dá)式 說明
within(TypePattern) 符合 TypePattern 的代碼中的 Join Point
withincode(MethodPattern) 在某些方法中的 Join Point
withincode(ConstructorPattern) 在某些構(gòu)造函數(shù)中的 Join Point
cflow(Pointcut) Pointcut 選擇出的切入點(diǎn) P 的控制流中的所有 Join Point,包括 P 本身
cflowbelow(Pointcut) Pointcut 選擇出的切入點(diǎn) P 的控制流中的所有 Join Point姐扮,不包括 P 本身
this(Type or Id) Join Point 所屬的 this 對(duì)象是否 instanceOf Type 或者 Id 的類型
target(Type or Id) Join Point 所在的對(duì)象(例如 call 或 execution 操作符應(yīng)用的對(duì)象)是否 instanceOf Type 或者 Id 的類型
args(Type or Id, ...) 方法或構(gòu)造函數(shù)參數(shù)的類型
if(BooleanExpression) 滿足表達(dá)式的 Join Point絮供,表達(dá)式只能使用靜態(tài)屬性、Pointcuts 或 Advice 暴露的參數(shù)茶敏、thisJoinPoint 對(duì)象

Pattern

Pattern類型 語(yǔ)法
MethodPattern [!] [@Annotation] [public,protected,private] [static] [final] 返回值類型 [類名.]方法名(參數(shù)類型列表) [throws 異常類型]
ConstructorPattern [!] [@Annotation] [public,protected,private] [final] [類名.]new(參數(shù)類型列表) [throws 異常類型]
FieldPattern [!] [@Annotation] [public,protected,private] [static] [final] 屬性類型 [類名.]屬性名
TypePattern 其他 Pattern 涉及到的類型規(guī)則也是一樣壤靶,可以使用 '!'、''惊搏、'..'贮乳、'+','!' 表示取反恬惯,'' 匹配除 . 外的所有字符串向拆,'*' 單獨(dú)使用事表示匹配任意類型,'..' 匹配任意字符串酪耳,'..' 單獨(dú)使用時(shí)表示匹配任意長(zhǎng)度任意類型浓恳,'+' 匹配其自身及子類,還有一個(gè) '...'表示不定個(gè)數(shù)

說明:

@注解 訪問權(quán)限 返回值的類型 包名.函數(shù)名(參數(shù))

  1. @注解和訪問權(quán)限(public/private/protect葡兑,以及static/final)屬于可選項(xiàng)奖蔓。如果不設(shè)置它們,則默認(rèn)都會(huì)選擇讹堤。以訪問權(quán)限為例,如果沒有設(shè)置訪問權(quán)限作為條件厨疙,那么public洲守,private,protect及static沾凄、final的函數(shù)都會(huì)進(jìn)行搜索梗醇。
  2. 返回值類型就是普通的函數(shù)的返回值類型。如果不限定類型的話撒蟀,就用*通配符表示
  3. 包名.函數(shù)名用于查找匹配的函數(shù)叙谨。可以使用通配符保屯,包括和..以及+號(hào)手负。其中號(hào)用于匹配除.號(hào)之外的任意字符涤垫,而..則表示任意子package,+號(hào)表示子類竟终。
    比如:
    java..Date:可以表示java.sql.Date蝠猬,也可以表示java.util.Date
    Test
    :可以表示TestBase,也可以表示TestDervied
    java..:表示java任意子類
    java..
    Model+:表示Java任意package中名字以Model結(jié)尾的子類统捶,比如TabelModel榆芦,TreeModel 等
  4. 最后來(lái)看函數(shù)的參數(shù)惠啄。參數(shù)匹配比較簡(jiǎn)單氧骤,主要是參數(shù)類型愕宋,比如:
    • (int, char):表示參數(shù)只有兩個(gè)形葬,并且第一個(gè)參數(shù)類型是int外盯,第二個(gè)參數(shù)類型是char
    • (String, ..):表示至少有一個(gè)參數(shù)檀何。并且第一個(gè)參數(shù)類型是String扔亥,后面參數(shù)類型不限挂签。在參數(shù)匹配中兑凿,..代表任意參數(shù)個(gè)數(shù)和類型
    • (Object ...):表示不定個(gè)數(shù)的參數(shù)凯力,且類型都是Object,這里的...不是通配符礼华,而是Java中代表不定參數(shù)的意思

Advice

直譯過來(lái)是通知咐鹤,實(shí)際上表示一類代碼織入位置,在AspectJ中有五種類型的注解:Before圣絮、After祈惶、AfterReturning、AfterThrowing扮匠、Around捧请,我們將它們統(tǒng)稱為Advice注解。

Advice 說明
@Before 切入點(diǎn)前織入
@After 切入點(diǎn)后織入棒搜,無(wú)論連接點(diǎn)執(zhí)行如何疹蛉,包括正常的 return 和 throw 異常
@AfterReturning 只有在切入點(diǎn)正常返回之后才會(huì)執(zhí)行,不指定返回類型時(shí)匹配所有類型
@AfterThrowing 只有在切入點(diǎn)拋出異常后才執(zhí)行力麸,不指定異常類型時(shí)匹配所有類型
@Around 替代原有切點(diǎn)可款,如果要執(zhí)行原來(lái)代碼的話,調(diào)用 ProceedingJoinPoint.proceed()

Advice注解修飾的方法有一些約束:

  1. 方法必須為public克蚂。
  2. Before闺鲸、After、AfterReturning埃叭、AfterThrowing 四種類型方法返回值必須為void摸恍。
  3. Around的目標(biāo)是替代原切入點(diǎn),它一般會(huì)有返回值赤屋,這就要求聲明的返回值類型必須與切入點(diǎn)方法的返回值保持一致立镶;不能和其他 Advice 一起使用壁袄,如果在對(duì)一個(gè) Pointcut 聲明 Around 之后還聲明 Before 或者 After 則會(huì)失效。
  4. 方法簽名可以額外聲明JoinPoint谜慌、JoinPointStaticPart然想、JoinPoint.EnclosingStaticPart。

常見方法

@Aspect 定義類為切入類
@Pointcut 聲明一個(gè)切入策略供
@Before @After @ Around @ AfterReturning選擇
@Before 被切入方法執(zhí)行前執(zhí)行
@After 被切入方法執(zhí)行后執(zhí)行
@Around 被切入方法前后都可以加入一些邏輯
@AfterReturning 被切入方法返回時(shí)執(zhí)行
JoinPoint 加入這個(gè)參數(shù)可以獲取被切入方法的名稱和參數(shù)

JoinPoint 對(duì)象
Signature getSignature();//獲取封裝了署名信息的對(duì)象,在該對(duì)象中可以獲取到目標(biāo)方法名,所屬類的Class等信息 (修飾符+包名+類名+方法名)
Object[] getArgs();//獲取傳入目標(biāo)方法的參數(shù)對(duì)象
Object getTarget();//獲取傳入目標(biāo)方法的參數(shù)對(duì)象
Object getThis();//獲取代理對(duì)象
getSignature().getName();//獲取方法名

ProceedingJoinPoint對(duì)象
只用在@Around的切面方法中欣范,是JoinPoint的子接口
Object proceed() throws Throwable //執(zhí)行目標(biāo)方法
Object proceed(Object[] var1) throws Throwable //傳入的新的參數(shù)去執(zhí)行目標(biāo)方法

實(shí)例(快速點(diǎn)擊)

避免快速點(diǎn)擊.png

AspectJ 缺點(diǎn)

  • 如果相應(yīng)的class沒有實(shí)現(xiàn)相應(yīng)的切點(diǎn)方法將無(wú)法織入变泄,如上文中的沒有BlankFragment實(shí)現(xiàn)onResume方法的話,將無(wú)法織入代碼恼琼。
  • 無(wú)法處理Lambda語(yǔ)法會(huì)有一系列兼容性問題妨蛹,如R8、gradle版本不同等性能較差
  • APP項(xiàng)目比較大時(shí)編譯時(shí)間明顯加長(zhǎng)晴竞。
  • 兼容性:如果使用的三方庫(kù)也使用了AspectJ蛙卤,可能導(dǎo)致未知的風(fēng)險(xiǎn)。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末噩死,一起剝皮案震驚了整個(gè)濱河市颤难,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌已维,老刑警劉巖行嗤,帶你破解...
    沈念sama閱讀 211,884評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異垛耳,居然都是意外死亡栅屏,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,347評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門堂鲜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)栈雳,“玉大人,你說我怎么就攤上這事缔莲「缛遥” “怎么了?”我有些...
    開封第一講書人閱讀 157,435評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵痴奏,是天一觀的道長(zhǎng)磺箕。 經(jīng)常有香客問我,道長(zhǎng)抛虫,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,509評(píng)論 1 284
  • 正文 為了忘掉前任简僧,我火速辦了婚禮建椰,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘岛马。我一直安慰自己棉姐,他們只是感情好屠列,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,611評(píng)論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著伞矩,像睡著了一般笛洛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上乃坤,一...
    開封第一講書人閱讀 49,837評(píng)論 1 290
  • 那天苛让,我揣著相機(jī)與錄音,去河邊找鬼湿诊。 笑死狱杰,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的厅须。 我是一名探鬼主播仿畸,決...
    沈念sama閱讀 38,987評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼朗和!你這毒婦竟也來(lái)了错沽?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,730評(píng)論 0 267
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤眶拉,失蹤者是張志新(化名)和其女友劉穎千埃,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體镀层,經(jīng)...
    沈念sama閱讀 44,194評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡镰禾,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,525評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了唱逢。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吴侦。...
    茶點(diǎn)故事閱讀 38,664評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖坞古,靈堂內(nèi)的尸體忽然破棺而出备韧,到底是詐尸還是另有隱情,我是刑警寧澤痪枫,帶...
    沈念sama閱讀 34,334評(píng)論 4 330
  • 正文 年R本政府宣布织堂,位于F島的核電站,受9級(jí)特大地震影響奶陈,放射性物質(zhì)發(fā)生泄漏易阳。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,944評(píng)論 3 313
  • 文/蒙蒙 一吃粒、第九天 我趴在偏房一處隱蔽的房頂上張望潦俺。 院中可真熱鬧,春花似錦、人聲如沸事示。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,764評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)肖爵。三九已至卢鹦,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間劝堪,已是汗流浹背冀自。 一陣腳步聲響...
    開封第一講書人閱讀 31,997評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留幅聘,地道東北人凡纳。 一個(gè)月前我還...
    沈念sama閱讀 46,389評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像帝蒿,于是被迫代替她去往敵國(guó)和親荐糜。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,554評(píng)論 2 349

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