2. Android Build系統

轉自:https://yq.aliyun.com/articles/73256?spm=5176.100239.bloglist.132.W2trbE

概述
Android Build 系統是用來編譯 Android 系統、Android SDK 以及相關文檔的一套框架。在Android系統中,Android 的源碼中包含了許許多多的模塊。 不同產商的不同設備對于 Android 系統的定制都是不一樣的。如何將這些模塊統一管理起來,如何能夠在不同的操作系統上進行編譯,如何在編譯時能夠支持面向不同的硬件設備,不同的編譯類型,且還要提供面向各個產商的定制擴展,Android系統如何解決這些問題呢?這就是我們不得不談的Android Build 系統。

Android源碼目錄結構:
這里寫圖片描述

Linux系統的make命令
在講解Android編譯系統之前,我們首先需要了解Linux系統的make命令。在Linux系統中,我們可以通過make命令來編譯代碼。Make命令在執行的時候,默認會在當前目錄找到一個Makefile文件,然后根據Makefile文件中的指令來對代碼進行編譯。如gcc,Linux系統中的shell命令cp、rm等等。

看到這里,有的小伙伴可能會說,在Linux系統中,shell和make命令有什么區別呢?make命令事實也是通過shell命令來完成任務的,但是它的神奇之處是可以幫我們處理好文件之間的依賴關系。例如有一個文件T,它依賴于另外一個文件D,要求只有當文件D的內容發生變化,才重新生成文件T。
Make命令是怎么知道兩個文件之間存在依賴關系,以及當被依賴文件發生變化時如何處理目標文件的呢?答案就在前面提到的Makefile文件。Makefile文件實際上是一個腳本文件,就像普通的shell腳本文件一樣,只不過它遵循的是Makefile語法。Makefile文件最基礎的功能就是描述文件之間的依賴關系,以及怎么處理這些依賴關系。
Android Build簡介
Android Build 系統是 Android 系統的一部分,主要用來編譯 Android 系統,Android SDK 以及相關文檔。該系統主要由 Make 文件,Shell 腳本以及 Python 腳本組成。Android build分類:
build/core 目錄下的文件,這是Android Build的系統框架核心;
device目錄下的文件,存放的是具體的產品配置文件;
各個模塊的編譯文件:Android.mk,位于模塊的原文件目錄下。

Android Build系統核心
Android Build系統核心在目錄build/core,這個目錄中有mk文件、shell腳本和per腳本,他們構成Android Build系統的基礎和架構。
在核心的buil/core里,系統主要干了三件事情:

這里寫圖片描述

常用命令:
source build/envsetup.sh lunch make

envsetup.sh

而在build/envsetup.sh中主要完成了三件事:
這里寫圖片描述

執行Android系統的編譯,必須先執行envsetup.sh腳本,這個腳本會建立Android的編譯環境。其具體執行的是建立shell命令以及調用add_lunch_combo命令,這個命令的將調用該命令的所傳遞的參數存放到一個全局的數組變量LUNCH_MENU_CHOICES中。

envsetup.sh腳本中定義的常用shell命令:
命令
說明

contact-button
指定當前編譯的產品

croot
快速切換到源碼的根目錄,方便開始編譯

m
編譯整個源碼,但不用將當前的目錄切換到源碼的根目錄

mm
編譯當前目錄下的所有模塊,但是不編譯他們的依賴項

mm
編譯當前目錄下的所有模塊,但是不編譯他們的依賴項

cgrep
對系統中所有的C/C++文件執行grep命令

sgrep
對系統中所有的源文件執行grep命令

編譯 Android 系統
Android 系統的編譯環境目前只支持 Ubuntu 以及 Mac OS 兩種操作系統。在編譯Android系統之前我們需要先獲取完整的 Android 源碼。打開控制臺之后轉到 Android 源碼的根目錄,然后執行如下命名:
source build/envsetup.sh lunch full-eng make -j8

關于這幾條命令的意思,我們上面提過。第一步命令“source build/envsetup.sh”引入了 build/envsetup.sh腳本,該腳本的作用是初始化編譯環境,并引入一些輔助的 Shell 函數;
第二步命令“lunch full-eng”是調用 lunch 函數,并指定參數為“full-eng”。lunch 函數的參數用來指定此次編譯的目標設備以及編譯類型。
第三部命令“make -j8”才真正開始執行編譯。make 的參數“-j”指定了同時編譯的 Job 數量,這是個整數,該值通常是編譯主機 CPU 支持的并發線程總數的 1 倍或 2 倍(例如:在一個 4 核,每個核支持兩個線程的 CPU 上,可以使用 make -j8 或 make -j16)。完整的編譯時間依賴于編譯主機的配置。
Build 結果
所有的編譯產物都將位于 /out 目錄下,該目錄下主要包含:
/out/host/:該目錄下包含了針對主機的 Android 開發工具的產物。即 SDK 中的各種工具,例如:emulator,adb,aapt 等。
/out/target/common/:該目錄下包含了針對設備的共通的編譯產物,主要是 Java 應用代碼和 Java 庫。
/out/target/product//:包含了針對特定設備的編譯結果以及平臺相關的 C/C++ 庫和二進制文件。其中,是具體目標設備的名稱。
/out/dist/:包含了為多種分發而準備的包,通過“make disttarget”將文件拷貝到該目錄,默認的編譯目標不會產生該目錄。

Build 生成的鏡像文件
Build 的產物中最重要的是三個鏡像文件,它們都位于 /out/target/product// 目錄下:
system.img:包含了 Android OS 的系統文件,庫,可執行文件以及預置的應用程序,將被掛載為根分區。
ramdisk.img:在啟動時將被 Linux 內核掛載為只讀分區,它包含了 /init文件和一些配置文件。它用來掛載其他系統鏡像并啟動 init 進程。
userdata.img:將被掛載為 /data,包含了應用程序相關的數據以及和用戶相關的數據。

Make 文件
整個 Build 系統的入口文件是源碼樹根目錄下名稱為“Makefile”的文件,當在源代碼根目錄上調用 make 命令時,make 命令首先將讀取該文件。Makefile 文件的內容只有一行:“include build/core/main.mk”。該行代碼的作用很明顯:包含 build/core/main.mk 文件。在 main.mk 文件中又會包含其他的文件,其他文件中又會包含更多的文件,這樣就引入了整個 Build 系統。

在整個Build系統中,Make 文件間的關系是相當復雜的??匆粡坢ake文件主要的關系圖:
這里寫圖片描述

Make 常用文件:
文件名

說明

main.mk
主要的 Make 文件,該文件中首先將對編譯環境進行檢查,同時引入其他的 Make 文件。另外,該文件中還定義了幾個最主要的 Make 目標,例如 droid,sdk,等(參見后文“Make 目標說明”)。

help.mk
含了名稱為 help 的 Make 目標的定義,該目標將列出主要的 Make 目標及其說明。

envsetup.mk
配置 Build 系統需要的環境變量,例如:TARGET_PRODUCT,TARGET_BUILD_VARIANT,HOST_OS,HOST_ARCH 等。 當前編譯的主機平臺信息(例如操作系統,CPU 類型等信息)就是在這個文件中確定的。 另外,該文件中還指定了各種編譯結果的輸出路徑。

pathmap.mk
將許多頭文件的路徑通過名值對的方式定義為映射表,并提供 include-path-for 函數來獲取。例如,通過 $(call include-path-for, frameworks-native)便可以獲取到 framework 本地代碼需要的頭文件路徑。

combo/select.mk
根據當前編譯器的平臺選擇平臺相關的 Make 文件。

dumpvar.mk
在 Build 開始之前,顯示此次 Build 的配置信息。

config.mk
整個 Build 系統的配置文件,最重要的 Make 文件之一。該文件中主要包含以下內容: 定義了許多的常量來負責不同類型模塊的編譯。 定義編譯器參數以及常見文件后綴,例如 .zip,.jar.apk。 根據 BoardConfig.mk 文件,配置產品相關的參數。 設置一些常用工具的路徑,例如 flex,e2fsck,dx。

definitions.mk
最重要的 Make 文件之一,在其中定義了大量的函數。這些函數都是 Build 系統的其他文件將用到的。例如:my-dir,all-subdir-makefiles,find-subdir-files,sign-package 等,關于這些函數的說明請參見每個函數的代碼注釋。

distdir.mk
針對 dist 目標的定義。dist 目標用來拷貝文件到指定路徑

dex_preopt.mk
針對啟動 jar 包的預先優化。

pdk_config.mk
顧名思義,針對 pdk(Platform Developement Kit)的配置文件。

post_clean.mk
在前一次 Build 的基礎上檢查當前 Build 的配置,并執行必要清理工作。

legacy_prebuilts.mk
該文件中只定義了 GRANDFATHERED_ALL_PREBUILT 變量。

Makefile
被 main.mk 包含,該文件中的內容是輔助 main.mk 的一些額外內容。

Android 源碼中包含了許多的模塊,模塊的類型有很多種,例如:Java 庫,C/C++ 庫,APK 應用,以及可執行文件等 。并且,Java 或者 C/C++ 庫還可以分為靜態的或者動態的,庫或可執行文件既可能是針對設備(本文的“設備”指的是 Android 系統將被安裝的設備,例如某個型號的手機或平板)的也可能是針對主機(本文的“主機”指的是開發 Android 系統的機器,例如裝有 Ubuntu 操作系統的 PC 機或裝有 MacOS 的 iMac 或 Macbook)的。不同類型的模塊的編譯步驟和方法是不一樣,為了能夠一致且方便的執行各種類型模塊的編譯,在 config.mk 中定義了許多的常量,這其中的每個常量描述了一種類型模塊的編譯方式。常見的有: BUILD_HOST_STATIC_LIBRARY BUILD_HOST_SHARED_LIBRARY BUILD_STATIC_LIBRARY BUILD_SHARED_LIBRARY BUILD_EXECUTABLE BUILD_HOST_EXECUTABLE BUILD_PACKAGE BUILD_PREBUILT BUILD_MULTI_PREBUILT BUILD_HOST_PREBUILT BUILD_JAVA_LIBRARY BUILD_STATIC_JAVA_LIBRARY BUILD_HOST_JAVA_LIBRARY 不同類型的模塊的編譯過程會有一些相同的步驟,例如:編譯一個 Java 庫和編譯一個 APK 文件都需要定義如何編譯 Java 文件。為了減少代碼冗余,需要將共同的代碼復用起來,復用的方式是將共同代碼放到專門的文件中,然后在其他文件中包含這些文件的方式來實現的。 模塊的編譯方式定義文件的包含關系:
這里寫圖片描述

## Make 編譯鏡像 ### make /make droid 如果在源碼樹的根目錄直接調用“make”命令而不指定任何目標,則會選擇默認目標:“droid”(在 main.mk 中定義)。因此,這和執行“make droid”效果是一樣的。 droid 目標將編譯出整個系統的鏡像。從源代碼到編譯出系統鏡像,整個編譯過程非常復雜。這個過程并不是在 droid 一個目標中定義的,而是 droid 目標會依賴許多其他的目標,這些目標的互相配合導致了整個系統的編譯。 那么需要編譯出系統鏡像,需要哪些依賴呢?
這里寫圖片描述
droid 所依賴的其他 Make目標說明:名稱
說明

apps_only
該目標將編譯出當前配置下不包含 user,userdebug,eng 標簽(關于標簽,請參見后文“添加新的模塊”)的應用程序。

droidcore
該目標僅僅是所依賴的幾個目標的組合,其本身不做更多的處理。

dist_files
該目標用來拷貝文件到 /out/dist 目錄。

files
該目標僅僅是所依賴的幾個目標的組合,其本身不做更多的處理

prebuilt
該目標依賴于 $(ALL_PREBUILT),$(ALL_PREBUILT)的作用就是處理所有已編譯好的文件。

$(modules_to_install)
modules_to_install 變量包含了當前配置下所有會被安裝的模塊(一個模塊是否會被安裝依賴于該產品的配置文件,模塊的標簽等信息),因此該目標將導致所有會被安裝的模塊的編譯。

$(modules_to_check)
該目標用來確保我們定義的構建模塊是沒有冗余的。

$(INSTALLED_ANDROID_INFO_TXT_TARGET)
該目標會生成一個關于當前 Build 配置的設備信息的文件,該文件的生成路徑是:out/target/product//android-info.txt

systemimage
生成 system.img。

Build 系統中包含的其他一些 Make 目標:
Make目標說明
說明

make clean
執行清理,等同于:rm -rf out/

make sdk
編譯出 Android 的 SDK

Make目標說明
說明

make clean-sdk
清理 SDK 的編譯產物

make update-api
更新 API。在 framework API 改動之后,需要首先執行該命令來更新 API,公開的 API 記錄在 frameworks/base/api 目錄下。

make dist
執行 Build,并將 MAKECMDGOALS 變量定義的輸出文件拷貝到 /out/dist 目錄

make all
編譯所有內容,不管當前產品的定義中是否會包含

make help
幫助信息

make snod
從已經編譯出的包快速重建系統鏡像

make libandroid_runtime
編譯所有 JNI framework 內容

makeframework
編譯所有 Java framework 內容

makeservices
編譯系統服務和相關內容

make
編譯一個指定的模塊,local_target 為模塊的名稱

make clean-
清理一個指定模塊的編譯結果

makedump-products
顯示所有產品的編譯配置信息,例如:產品名,產品支持的地區語言,產品中會包含的模塊等信息

makePRODUCT-xxx-yyy
編譯某個指定的產品

makebootimage
生成 boot.img

定制 Build 系統中內容 當我們要開發一款新的 Android 產品的時候,我們首先就需要在 Build 系統中添加對于該產品的定義。在 Android Build 系統中對產品定義的文件通常位于 device 目錄下,device 目錄下可以公司名以及產品名分為二級目錄,然后加入到系統中,如以前小米等基于Android深度定制的系統。 通常,對于一個產品的定義通常至少會包括四個文件:AndroidProducts.mk,產品版本定義文件,BoardConfig.mk 以及 verndorsetup.sh。 ## AndroidProducts.mk 該文件只需要定義一個變量,名稱為“PRODUCT_MAKEFILES”。 PRODUCT_MAKEFILES := \ $(LOCAL_DIR)/full_stingray.mk \ $(LOCAL_DIR)/stingray_emu.mk \ $(LOCAL_DIR)/generic_stingray.mk ## 產品版本定義文件 該文件中包含了對于特定產品版本的定義。該文件可能不只一個,因為同一個產品可能會有多種版本。 通常情況下,我們并不需要定義所有這些變量。Build 系統的已經預先定義好了一些組合,它們都位于 /build/target/product 下,每個文件定義了一個組合,我們只要繼承這些預置的定義,然后再覆蓋自己想要的變量定義即可。 # 繼承 full_base.mk 文件中的定義 $(call inherit-product, $(SRC_TARGET_DIR)/product/full_base.mk) # 覆蓋其中已經定義的一些變量 PRODUCT_NAME := full_lt26 PRODUCT_DEVICE := lt26 PRODUCT_BRAND := Android PRODUCT_MODEL := Full Android on LT26 ## BoardConfig.mk 該文件用來配置硬件主板,它其中定義的都是設備底層的硬件特性。例如:該設備的主板相關信息,Wifi 相關信息,還有 bootloader,內核,radioimage 等信息。 ## vendorsetup.sh 該文件中作用是通過 add_lunch_combo 函數在 lunch 函數中添加一個菜單選項。該函數的參數是產品名稱加上編譯類型,中間以“-”連接,例如:add_lunch_combo full_lt26-userdebug。/build/envsetup.sh 會掃描所有 device 和 vender 二 級目 錄下的名稱 為"vendorsetup.sh"文件,并根據其中的內容來確定 lunch 函數的 菜單選項。 在配置了以上的文件之后,便可以編譯出我們新添加的設備的系統鏡像了。 我們可以使用命令: source build/envsetup.sh 來查看Build 系統已經引入了剛剛添加的 vendorsetup.sh 文件。 ## 添加新模塊 在源碼樹中,一個模塊的所有文件通常都位于同一個文件夾中。為了將當前模塊添加到整個 Build 系統中,每個模塊都需要一個專門的 Make 文件,該文件的名稱為“Android.mk”。Build 系統會掃描名稱為“Android.mk”的文件,并根據該文件中內容編譯出相應的產物。 注: 在 Android Build 系統中,編譯是以模塊(而不是文件)作為單位的,每個模塊都有一個唯一的名稱,一個模塊的依賴對象只能是另外一個模塊,而不能是其他類型的對象。對于已經編譯好的二進制庫,如果要用來被當作是依賴對象,那么應當將這些已經編譯好的庫作為單獨的模塊。對于這些已經編譯好的庫使用 BUILD_PREBUILT 或 BUILD_MULTI_PREBUILT。例如:當編譯某個 Java 庫需要依賴一些 Jar 包時,并不能直接指定 Jar 包的路徑作為依賴,而必須首先將這些 Jar 包定義為一個模塊,然后在編譯 Java 庫的時候通過模塊的名稱來依賴這些 Jar 包。 那么怎么編寫Android.mk 文件呢? Android.mk 文件通常以以下兩行代碼作為開頭: LOCAL_PATH := $(call my-dir) //設置當前模塊的編譯路徑為當前文件夾路徑 include $(CLEAR_VARS)//清理編譯環境中用到的變量 為了方便模塊的編譯,Build 系統設置了很多的編譯環境變量。要編譯一個模塊,只要在編譯之前根據需要設置這些變量然后執行編譯即可。常見的如: - LOCAL_SRC_FILES:當前模塊包含的所有源代碼文件。 - LOCAL_MODULE:當前模塊的名稱,這個名稱應當是唯一的,模塊間的依賴關系就是通過這個名稱來引用的。 - LOCAL_C_INCLUDES:C 或 C++ 語言需要的頭文件的路徑。 - LOCAL_STATIC_LIBRARIES:當前模塊在靜態鏈接時需要的庫的名稱。 - LOCAL_SHARED_LIBRARIES:當前模塊在運行時依賴的動態庫的名稱。 - LOCAL_CFLAGS:提供給 C/C++ 編譯器的額外編譯參數。 - LOCAL_JAVA_LIBRARIES:當前模塊依賴的 Java 共享庫。 - LOCAL_STATIC_JAVA_LIBRARIES:當前模塊依賴的 Java 靜態庫。 - LOCAL_PACKAGE_NAME:當前 APK 應用的名稱。 - LOCAL_CERTIFICATE:簽署當前應用的證書名稱。 - LOCAL_MODULE_TAGS:當前模塊所包含的標簽,一個模塊可以包含多個標簽。標簽的值可能是 debug, eng,user,development 或者 optional。其中,optional是默認標簽。標簽是提供給編譯類型使用的,不同的編譯類型會安裝包含不同標簽的模塊。 編譯類型說明:名稱

說明

eng
默認類型,該編譯類型適用于開發階段。 當選擇這種類型時,編譯結果將: 安裝包含 eng, debug, user,development 標簽的模塊 安裝所有沒有標簽的非 APK 模塊 安裝所有產品定義文件中指定的 APK 模塊

user
該編譯類型適合用于最終發布階段。 當選擇這種類型時,編譯結果將: 安裝所有帶有 user 標簽的模塊 安裝所有沒有標簽的非 APK 模塊 安裝所有產品定義文件中指定的 APK 模塊,APK 模塊的標簽將被忽略

userdebug
該編譯類型適合用于 debug 階段。 該類型和 user 一樣,除了: 會安裝包含 debug 標簽的模塊 編譯出的系統具有 root 訪問權限

根據上表各種類型模塊的編譯方式,要執行編譯,只需要引入表 3 中對應的 Make 文件即可。例如,要編譯一個 APK 文件,只需要在 Android.mk 文件中,加入“include $(BUILD_PACKAGE)。除此以外,Build 系統中還定義了一些便捷的函數以便在 Android.mk 中使用,包括:
$(call my-dir):獲取當前文件夾路徑。
$(call all-java-files-under, ):獲取指定目錄下的所有 Java 文件。
$(call all-c-files-under, ):獲取指定目錄下的所有 C 語言文件。
$(call all-Iaidl-files-under, ) :獲取指定目錄下的所有 AIDL 文件。
$(call all-makefiles-under, ):獲取指定目錄下的所有 Make 文件。
$(call intermediates-dir-for, , , , ):獲取 Build 輸出的目標文件夾路徑。

如:編譯一個 APK 文件
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) # 獲取所有子目錄中的 Java 文件 LOCAL_SRC_FILES := $(call all-subdir-java-files) # 當前模塊依賴的靜態 Java 庫,如果有多個以空格分隔 LOCAL_STATIC_JAVA_LIBRARIES := static-library # 當前模塊的名稱 LOCAL_PACKAGE_NAME := LocalPackage # 編譯 APK 文件 include $(BUILD_PACKAGE)

編譯一個 Java 的靜態庫:
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) # 獲取所有子目錄中的 Java 文件 LOCAL_SRC_FILES := $(call all-subdir-java-files) # 當前模塊依賴的動態 Java 庫名稱 LOCAL_JAVA_LIBRARIES := android.test.runner # 當前模塊的名稱 LOCAL_MODULE := sample # 將當前模塊編譯成一個靜態的 Java 庫 include $(BUILD_STATIC_JAVA_LIBRARY)

附:Android編譯系統詳解

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內容