使用注解(Annotations)改進代碼檢驗

來自谷歌官方文檔的翻譯。原文地址

使用Lint等工具進行代碼檢查可以幫助你查找問題改善代碼質(zhì)量雄妥,但是這些工具只能進行推斷并不能真正進行檢查。因為Android使用一個int類型作為ID枝秤,表示字符串,圖片薇溃,顏色和其他類型的資源沐序,這時如果你在一個需要使用顏色資源ID的地方使用了一個字符串資源ID,代碼檢查工具是不會報錯的,但是你的應(yīng)用仍然會發(fā)生繪制錯誤或者是不能運行晶丘。

注解(Annotations)就是讓你給代碼檢查工具Lint等給一個提示,讓它們注意檢查這些微妙的代碼問題脑题。這些注解就好像元數(shù)據(jù)標(biāo)簽(mentadata tags)一樣和變量叔遂,參數(shù)蚕苇,返回值綁定在一起嚼吞,檢查它們是否合法舱禽。當(dāng)運行代碼檢查工具的時候誊稚,注解就會幫助你檢查比如空指針里伯,資源類型沖突等問題疾瓮。

Android通過Annotations Support Library支持多種注解,你可以通過android.support.annotation包獲取這些注解漫萄。

在工程中添加注解


在工程中打開注解功能,添加support-annotations依賴。你添加的任何注解都會代碼檢查工具運行時或者lint task時進行檢查启昧。

添加注解功能的工程依賴

Support Annotations libraryAndroid Support Repository的一部分跛璧。所以你必須先下載support repository追城,然后在build.gradle中添加support-annotations依賴。

  1. 在工具欄中點擊 SDK Manager

    ,或者選擇 Tools > Android > SDK Manager,打開 SDK Manager伞插。

  2. 點擊 SDK Tools 標(biāo)簽.

  3. 展開 Support Repository, 并選中 Android Support Repository京髓。

  4. 點擊 OK.

  5. 安裝向?qū)е悬c擊 Continue 直到完成安裝.

  6. 將以下代碼添加到 build.gradle 文件中蛇摸,完成添加 support-annotations 依賴:

     dependencies { compile 'com.android.support:support-annotations:24.2.0' } 
    

    這里使用的版本可能低于你下載的版本揽涮,所以這里指定的版本號必須和你在第三步中下載的版本號保持一致。

  7. 最后點擊工具欄或者通知中的 Sync Now

如果你自己的庫模塊中使用了annotations,那么annotations就已經(jīng)以XML的方式存在于AAR(Android Archive (AAR) artifact)文件的annotations.zip中了。使用了你的庫的用戶就沒有必要再以這種添加依賴的方式添加這個模塊了髓梅。

如果你想用Gradle Java plugin這種方式代替默認(rèn)的Android plugin for Gradle (com.android.application 或 com.android.library)诡必,那你必須明確指定SDK庫的位置蟋字,因為Android支持庫并不支持JCenter

repositories {
    jcenter()
    maven { url '<your-SDK-path>/extras/android/m2repository' }
}           

注意:如果你使用了appcompat庫涂炎,那你同樣不需要添加 support-annotations 依賴两蟀,因為 appcompat 庫已經(jīng)添加過這個依賴了蛀序,你可以直接使用annotations遣鼓。

Android支持庫中完整的注解列表你可以通過Support Annotations library reference查詢气笙,或者利用代碼補全功能,在輸入了import android.support.annotation.語句后出現(xiàn)的可用項中查看堵第。

運行代碼檢查功能

在 Android Studio 的工具欄中選擇 Analyze > Inspect Code 胀瞪,啟動代碼檢查功能涵紊,包括確認(rèn)注解的有效性和Lint自動檢查兩部分既忆。Android Studio會顯示沖突信息宇挫,標(biāo)記出在代碼中潛藏的問題绘雁,并且給出相應(yīng)的解決解決建議。
你也可以用命令行啟動lint任務(wù),這對持續(xù)集成服務(wù)器發(fā)現(xiàn)問題很有幫助,但是要注意,這樣啟動的lint任務(wù)并不能檢查nullness注解,只有Android Studio才能具備這個共軛能。關(guān)于Lint檢查的的問題,請看使用Lint改善你的代碼彩掐。

注意弹澎,即使注解沖突產(chǎn)生了警告佩迟,但是這些警告并不會組織代碼的編譯羔沙。

空值注解(Nullness Annotations)


@Nullable 注解表示變量诗充,參數(shù)茎匠,返回值可以null, @NonNull 注解表示變量谊惭,參數(shù)驱敲,返回值不能null

如果一個值為null的變量,被傳遞到了一個參數(shù)被標(biāo)記為@NonNull的方法中厘托,這時編譯就會產(chǎn)生一個non-null的警告包斑;另外一方面萌抵,如果引用一個返回值被標(biāo)記為@Nullable的方法霎桅,并且你沒有對返回結(jié)果進行是否為空的檢查,那么就會受到一個nullness的警告讨永。只有當(dāng)你想提醒方法的使用者滔驶,在每次使用方法前都要明確地進行非空檢查時,才能使用@Nullable注解標(biāo)記方法的返回值住闯。
下面的例子使用了@NonNull注解標(biāo)記了contextparameters兩個參數(shù)瓜浸,表示要檢查傳入的這兩個參數(shù)的值是不能為空,同時還要檢查onCreateView()方法自身的返回值不能為空

import android.support.annotation.NonNull;
...

/** Add support for inflating the <fragment> tag. **/
@NonNull
@Override
public View onCreateView(String name, @NonNull Context context,
  @NonNull AttributeSet attrs) {
  ...
  }
...

空值分析(Nullability Analysis)

Android Studio支持空值分析(nullability analysis)去自動推斷并且在代買中插入空值注解(nullness annotations)比原〔宸穑空值分析會掃描所有方法層次結(jié)構(gòu)中的調(diào)用關(guān)系,去檢查:

  • 調(diào)用的方法可以返回空
  • 調(diào)用的方法不能返回空
  • 變量量窘,字段雇寇,局部變量,參數(shù)等可以為空
  • 變量,字段锨侯,局部變量嫩海,參數(shù)等不能為空

分析完后會在檢查的位置自動插入適當(dāng)?shù)目罩底⒔狻?/p>

在Android Studio中選擇Analyze > Infer Nullity開啟空值分析。Android Studio會在檢測的地方插入Android版本的 @Nullable@NonNull 注解囚痴。下面是一些很好的實踐經(jīng)驗:

注意:當(dāng)加入空值注解的時候叁怪,代碼補全功能會建議我們使用 IntelliJ版本的 @Nullable and @NotNull 注解來代替Android版本的注解,同時也會自動引入相應(yīng)的包深滚。但是 Android Studio Lint 功能只會檢查Android版本的注解奕谭。當(dāng)確認(rèn)你的注解的時候,一定要檢查你的工程使用的是Android版本的注解痴荐,這樣Lint功能才能正常運行血柳。(PS:Android版本的是 @NonNull,IntelliJ的是@NotNull生兆;@Nullable寫法是一樣的)

資源注解(Resource Annotations)


確認(rèn)資源類型非常有用难捌,因為Android對于資源的引用,比如drawable鸦难,string根吁,都是用整數(shù)類型進行傳遞的。如果代碼期待接收特定的資源類型明刷,比如Drawable婴栽,就可以把該資源引用的int值傳遞過去满粗。但是實際上也有可能錯傳了一個R.string資源的int值過去辈末。所以確認(rèn)資源的類型很有用。

我們可以添加了一個@StringRes注解映皆,去檢查參數(shù)的是不是一個R.string類型的資源:

public abstract void setTitle(@StringRes int resId) { … }

如果參數(shù)不是一個R.string類型的資源挤聘,那么在代碼檢查期間就會產(chǎn)生一個警告捅彻。

其他[@DrawableRes][drawableres],[@DimenRes][dimenres],[@ColorRes][colorres],[@InterpolatorRes][interpolatorres]等資源注解都可以按這種格式使用步淹。如果你的參數(shù)支持多個資源格式缭裆,你可以對其添加多個資源注解澈驼。[@AnyRes][anyres]注解表示該菜蔬可以是任意一種R資源格式。

即使你使用了[@ColorRes][colorres]指定了一個資源類型的參數(shù)徘六,但是用RRGGBBAARRGGBB表示的顏色整數(shù)值卻并不會被認(rèn)可。同樣用[@ColorInt][colorint]指定的資源也只認(rèn)可能被解析的顏色整數(shù)值炉擅。編譯工具會標(biāo)記出這些不正確的代碼谍失。

[stringres]:
[drawableres]: https://developer.android.com/reference/android/support/annotation/DrawableRes.html
[dimenres]:https://developer.android.com/reference/android/support/annotation/DimenRes.html
[colorres]:https://developer.android.com/reference/android/support/annotation/ColorRes.html
[interpolatorres]:https://developer.android.com/reference/android/support/annotation/InterpolatorRes.html
[anyres]:https://developer.android.com/reference/android/support/annotation/AnyRes.html
[colorint]:https://developer.android.com/reference/android/support/annotation/ColorInt.html

線程注解(Thread Annotations)


線程注解用來檢查方法是不是在一個特定的線程中被調(diào)用快鱼。支持以下注解

注意:編譯工具將@MainThread@UiThread看成是可互換的抹竹,所以你可以在標(biāo)注為@MainThread的方法中調(diào)用@UiThread的方法止潮,反之亦然喇闸。但是燃乍,在不同線程上有多個視圖的系統(tǒng)應(yīng)用程序的情況下刻蟹,UI線程可能不等同于主線程舆瘪。因此淀衣,您應(yīng)該使用@UiThread注解與應(yīng)用程序視圖層次結(jié)構(gòu)相關(guān)的方法舌缤,并使用@MainThread注解僅與應(yīng)用程序生命周期相關(guān)聯(lián)的方法国撵。

如果類中的所有方法都共享相同的線程介牙,則可以向類添加單個線程注解壮虫,以驗證類中的所有方法是否都從同一類型的線程中被調(diào)用。

線程注解的一個常見用法是驗證AsyncTask類中被覆蓋的方法环础,因為此類在后臺執(zhí)行,并僅在UI線程上發(fā)布結(jié)果线得。

值約束注解(Value Constraint Annotations)


使用@IntRange@FloatRange@Size注解驗證傳進來的參數(shù)的值贯钩。當(dāng)用戶可能輸入不在范圍內(nèi)的參數(shù)時募狂,@IntRange@FloatRange非常有用角雷。

@IntRange注解驗證整數(shù)或長整數(shù)參數(shù)的值是否在指定范圍內(nèi)祸穷。以下示例確保alpha參數(shù)包含從0到255的整數(shù)值:

public void setAlpha(@IntRange(from=0,to=255) int alpha) { … }

@FloatRange注解檢查floatdouble類型參數(shù)值是否在浮點值的指定范圍內(nèi)勺三。以下示例確保alpha參數(shù)包含從0.0到1.0的浮點值:

public void setAlpha(@FloatRange(from=0.0, to=1.0) float alpha) {...}

@Size注解檢查集合或數(shù)組的大小雷滚,以及字符串的長度祈远。 @Size注解可用于驗證以下數(shù)量:

  • 最大尺寸 (如 @Size(min=2))
  • 最小尺寸 (如 @Size(max=2))
  • 精確尺寸 (如 @Size(2))
  • 尺寸的倍數(shù)(如 @Size(multiple=2))

例如炊汹,@Size(min=1)檢查集合是否為空逃顶,@Size(3)驗證數(shù)組是否包含有三個值以政。以下示例確保局部數(shù)組變量至少包含一個元素:

int[] location = new int[3];
button.getLocationOnScreen(@Size(min=1) location);

權(quán)限注解(Permission Annotations)


使用@RequiresPermission注解來驗證方法調(diào)用者的權(quán)限霸褒。要從列表中檢查單個權(quán)限的有效權(quán)限,請使用anyOf屬性盈蛮。 要檢查一組權(quán)限废菱,請使用allOf屬性。以下示例注解setWallpaper()方法,以確保方法的調(diào)用者具有permission.SET_WALLPAPERS權(quán)限:

@RequiresPermission(Manifest.permission.SET_WALLPAPER)
public abstract void setWallpaper(Bitmap bitmap) throws IOException;

此示例要求copyFile()方法的調(diào)用者擁有讀寫外部存儲的權(quán)限:

@RequiresPermission(allOf = {
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE})
public static final void copyFile(String dest, String source) {
    ...
}

對于意圖(intents)的權(quán)限殊轴,將權(quán)限要求放置在定義意圖操作名稱的字符串字段上:

@RequiresPermission(android.Manifest.permission.BLUETOOTH)
public static final String ACTION_REQUEST_DISCOVERABLE =
        "android.bluetooth.adapter.action.REQUEST_DISCOVERABLE";

對于需要對讀寫訪問具有單獨權(quán)限的內(nèi)容提供程序(content providers)的權(quán)限衰倦,請在@RequiresPermission.Read@RequiresPermission.Write注解中包含每個權(quán)限要求:

@RequiresPermission.Read(@RequiresPermission(READ_HISTORY_BOOKMARKS))
@RequiresPermission.Write(@RequiresPermission(WRITE_HISTORY_BOOKMARKS))
public static final Uri BOOKMARKS_URI = Uri.parse("content://browser/bookmarks");

間接權(quán)限(Indirect Permissions)

當(dāng)權(quán)限取決于提供給方法參數(shù)的特定值時,只對該參數(shù)使用@RequiresPermission注解旁理,而不用列出特定的權(quán)限樊零。 例如,startActivity(Intent) 方法對傳遞給方法的intent參數(shù)就使用了間接權(quán)限:

public abstract void startActivity(@RequiresPermission Intent intent, @Nullable Bundle) {...}

當(dāng)你使用間接權(quán)限時孽文,構(gòu)建工具會執(zhí)行數(shù)據(jù)流分析驻襟,以檢查傳遞給方法的參數(shù)是否有@RequiresPermission注解。 然后他們從方法本身的參數(shù)強制執(zhí)行任何現(xiàn)有注解芋哭。 在startActivity(Intent)示例中沉衣,當(dāng)沒有適當(dāng)權(quán)限的意圖傳遞給方法時,Intent類中的注解導(dǎo)致對startActivity(Intent)的無效使用的結(jié)果警告减牺,如圖1所示厢蒜。

構(gòu)建工具從Intent類中相應(yīng)意圖操作名稱上的注解在startActivity(Intent)上生成警告:

@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
@RequiresPermission(Manifest.permission.CALL_PHONE)
public static final String ACTION_CALL = "android.intent.action.CALL";

有時,您可以在注解方法的參數(shù)時烹植,用@RequiresPermission替換@RequiresPermission.Read@RequiresPermission.Write斑鸦。 但是,對于間接權(quán)限草雕,@RequiresPermission不應(yīng)與讀取或?qū)懭霗?quán)限注解混合使用巷屿。

返回值注解(Return Value Annotations)


使用@CheckResult注解來驗證方法的結(jié)果或返回值實際上是否被使用。在容易混淆的方法結(jié)果上添加@CheckResult注解用以區(qū)分墩虹,而不是對每個非void方法都進行注解嘱巾。在容易混淆的方法結(jié)果上添加@CheckResult注解用以區(qū)分,而不是對每個非void方法都進行注解诫钓。 例如旬昭,Java開發(fā)?新手經(jīng)常錯誤地認(rèn)為<String>.trim()是從原始字符串中刪除所有空格。對方法使用包含<String>.trim()@CheckResult注解菌湃,?這樣調(diào)用者就不用對方法的返回值?進行任何操作了问拘。

以下示例中,@CheckResult注解了[checkPermissions()](https://developer.android.com/reference/android/content/pm/PackageManager.html#checkPermission(java.lang.String, java.lang.String))方法惧所,以確保方法的返回值實際被引用骤坐。 它還建議開發(fā)人員將[enforcePermission()](https://developer.android.com/reference/android/content/ContextWrapper.html#enforcePermission(java.lang.String, int, int, java.lang.String))方法作為一種替代方案:

@CheckResult(suggest="#enforcePermission(String,int,int,String)")
public abstract int checkPermission(@NonNull String permission, int pid, int uid);

調(diào)用父類方法注解(CallSuper Annotations)


使用@CallSuper注解來驗證該方法是否調(diào)用了被覆蓋的父類?方法。以下示例中下愈,onCreate()方法使用了該注解纽绍,以確保任何覆蓋了這個方法的實現(xiàn)都必須調(diào)用super.onCreate)

@CallSuper
protected void onCreate(Bundle savedInstanceState) {
}

類型定義注解(Typedef Annotations)


你可以使用@IntDef@StringDef注解,來創(chuàng)建一個整數(shù)或者字符串集合的枚舉類型势似。Typedef注解確保特定參數(shù)拌夏,返回值或字段只能使用特定的一組常量僧著。它們還可以使代碼擁有自動補全功能。

Typedef注解使用@interface聲明新的枚舉注解類型障簿。@IntDef@StringDef以及@Retention注解一起標(biāo)注新的注解它盹愚,并且它們?nèi)齻€是定義一個枚舉類型所必需的。@Retention(RetentionPolicy.SOURCE)注解告訴編譯器不要將被標(biāo)記了的枚舉類型數(shù)據(jù)存儲在.class文件中卷谈。

以下示例說明了創(chuàng)建這種注解的步驟杯拐,以確保作為方法參數(shù)傳遞的值是一組已經(jīng)定義好的常量集合中的一個:

import android.support.annotation.IntDef;
...
public abstract class ActionBar {
...
// Define the list of accepted constants and declare the NavigationMode annotation
@Retention(RetentionPolicy.SOURCE)
@IntDef({NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS})
public @interface NavigationMode {}

// Declare the constants
public static final int NAVIGATION_MODE_STANDARD = 0;
public static final int NAVIGATION_MODE_LIST = 1;
public static final int NAVIGATION_MODE_TABS = 2;

// Decorate the target methods with the annotation
@NavigationMode
public abstract int getNavigationMode();

// Attach the annotation
public abstract void setNavigationMode(@NavigationMode int mode);

如果mode參數(shù)的值不是已經(jīng)定義的(NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, or NAVIGATION_MODE_TABS)其中的一個,編譯時就會受到警告世蔗。

你還可以將 @IntDef@IntRange 一起使用端逼,用來限定一個整數(shù)值既是給定的一組常量中的一個,同時也是一個給定范圍內(nèi)的值污淋。

將常量和標(biāo)志位一起使用(Enable combining constants with flags)

如果用戶想將有效的常量與標(biāo)志位(例如|, &, ^等)組合使用顶滩,你可以結(jié)合flag屬性定義這個注解,以檢查參數(shù)或返回值是否是合法的樣式寸爆。以下示例用一系列DISPLAY_常數(shù)創(chuàng)建了DisplayOptions注解:

import android.support.annotation.IntDef;
...

@IntDef(flag=true, value={
        DISPLAY_USE_LOGO,
        DISPLAY_SHOW_HOME,
        DISPLAY_HOME_AS_UP,
        DISPLAY_SHOW_TITLE,
        DISPLAY_SHOW_CUSTOM
})
@Retention(RetentionPolicy.SOURCE)
public @interface DisplayOptions {}

...

當(dāng)你使用注解標(biāo)志構(gòu)建代碼時礁鲁,如果修飾的參數(shù)或返回值不是有效的樣式時,則會生成警告赁豆。

代碼訪問權(quán)限注解(Code Accessibility Annotations)


使用@VisibleForTesting@Keep注解來表示方法仅醇,類或字段的可訪問性。

@VisibleForTesting注解表示魔种,代碼測試時候析二,被注解了的這段代碼比其所聲明的,有更大的可見性节预。(比如聲明為private叶摄,測試時就變成了public)。

@Keep注解確保被標(biāo)注的元素在編譯代碼中壓縮代碼資源的時候不會被刪除安拟。這個標(biāo)簽的典型應(yīng)用就是添加在要被反射調(diào)用(reflection)的類或者方法上面蛤吓,確保編譯器不會把這些方法或者類當(dāng)做是無用的資源而被優(yōu)化掉(刪除)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末糠赦,一起剝皮案震驚了整個濱河市会傲,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌愉棱,老刑警劉巖唆铐,帶你破解...
    沈念sama閱讀 218,036評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異奔滑,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,046評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來单山,“玉大人掰邢,你說我怎么就攤上這事聚霜∪韵。” “怎么了低剔?”我有些...
    開封第一講書人閱讀 164,411評論 0 354
  • 文/不壞的土叔 我叫張陵账蓉,是天一觀的道長袱蚓。 經(jīng)常有香客問我钞啸,道長,這世上最難降的妖魔是什么喇潘? 我笑而不...
    開封第一講書人閱讀 58,622評論 1 293
  • 正文 為了忘掉前任体斩,我火速辦了婚禮,結(jié)果婚禮上颖低,老公的妹妹穿的比我還像新娘絮吵。我一直安慰自己,他們只是感情好忱屑,可當(dāng)我...
    茶點故事閱讀 67,661評論 6 392
  • 文/花漫 我一把揭開白布蹬敲。 她就那樣靜靜地躺著,像睡著了一般莺戒。 火紅的嫁衣襯著肌膚如雪伴嗡。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,521評論 1 304
  • 那天从铲,我揣著相機與錄音瘪校,去河邊找鬼。 笑死食店,一個胖子當(dāng)著我的面吹牛渣淤,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播吉嫩,決...
    沈念sama閱讀 40,288評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼价认,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了自娩?” 一聲冷哼從身側(cè)響起用踩,我...
    開封第一講書人閱讀 39,200評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎忙迁,沒想到半個月后脐彩,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,644評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡姊扔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,837評論 3 336
  • 正文 我和宋清朗相戀三年惠奸,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片恰梢。...
    茶點故事閱讀 39,953評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡佛南,死狀恐怖梗掰,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情嗅回,我是刑警寧澤及穗,帶...
    沈念sama閱讀 35,673評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站绵载,受9級特大地震影響埂陆,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜娃豹,卻給世界環(huán)境...
    茶點故事閱讀 41,281評論 3 329
  • 文/蒙蒙 一焚虱、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧培愁,春花似錦著摔、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,889評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至私股,卻和暖如春摹察,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背倡鲸。 一陣腳步聲響...
    開封第一講書人閱讀 33,011評論 1 269
  • 我被黑心中介騙來泰國打工供嚎, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人峭状。 一個月前我還...
    沈念sama閱讀 48,119評論 3 370
  • 正文 我出身青樓克滴,卻偏偏與公主長得像,于是被迫代替她去往敵國和親优床。 傳聞我的和親對象是個殘疾皇子劝赔,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,901評論 2 355

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