Android Log機(jī)制的原理學(xué)習(xí)

應(yīng)用程序的運(yùn)行與維護(hù),離不開日志斤讥。APP開發(fā)者們有很多選擇纱皆,例如微信的xlog(高可靠性高性能的運(yùn)行期日志組件)等,同樣也離不開原生的日志機(jī)制支持芭商。所以我們從原生Android Log機(jī)制開始學(xué)起:

一. Android Log機(jī)制(基于Android P原生代碼)

APP打印日志派草,最簡單的是使用Log類(android.util.Log),如下例子:

import android.util.Log;

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
           super.onCreate(savedInstanceState);
           setContentView(R.layout.activity_main);
 
           Log.d("kevintest", "onCreate");
    }
    // ...
}

APP運(yùn)行铛楣,進(jìn)入頁面后近迁,通過adb logcat命令,會(huì)看到有一行日志打印出:


1. 日志類型有5類


2. 上述類型中蛉艾,LOG_ID_MAIN钳踊、LOG_ID_RADIO衷敌、LOG_ID_SYSTEM的優(yōu)先級(jí)有6種

一般使用時(shí),級(jí)別值越高拓瞪,打印的日志越重要缴罗,從變量字面意思也可理解。Google官方文檔中有這樣的建議:VERBOSE日志只建議用于開發(fā)調(diào)試的版本祭埂,不能被編譯入其他版本面氓,如release版本;DEBUG日志可用于release版本產(chǎn)品蛆橡,但默認(rèn)不會(huì)打由嘟纭;INFO泰演、WARN呻拌、ERROR日志建議在release版本中使用與保存。手機(jī)中一般默認(rèn)的日志打印級(jí)別是INFO(包括INFO睦焕、WARN藐握、ERROR),可參考:開發(fā)者選項(xiàng)-選擇日志級(jí)別


3. 原生代碼分析

首先我們重點(diǎn)看下android.util.Log類:
Log類定義較簡單垃喊,無父類猾普,final修飾,不可繼承本谜。構(gòu)造函數(shù)是private初家,不可實(shí)例化。舉例乌助,APP調(diào)用Log.v方法打印VERBOSE日志時(shí)溜在,其方法實(shí)現(xiàn)是:

public static int v(String tag, String msg) {
       return println_native(LOG_ID_MAIN, VERBOSE, tag, msg);
}

其中關(guān)鍵的方法是println_native,方法實(shí)現(xiàn)位于:frameworks/base/core/jni/android_util_Log.cpp眷茁,傳入的三個(gè)參數(shù)分別是:LOG_ID_MAIN(類型)炕泳,VERBOSE(級(jí)別),tag(標(biāo)簽)上祈,msg(日志信息)培遵,實(shí)現(xiàn)如下:

static jint android_util_Log_println_native(JNIEnv* env, jobject clazz,
        jint bufID, jint priority, jstring tagObj, jstring msgObj) {
    // ...
    int res = __android_log_buf_write(bufID, (android_LogPriority)priority, tag, msg);
    // ...
    return res;
}

繼續(xù)調(diào)用__android_log_buf_write方法,方法定義:system/core/liblog/include/android/log.h登刺,實(shí)現(xiàn):system/core/liblog/logger_write.c籽腕,其中核心部分:

注意:其他一些系統(tǒng)模塊,例如debuggered等C++代碼都會(huì)直接封裝or調(diào)用__android_log_buf_write來打印日志

LIBLOG_ABI_PUBLIC int __android_log_buf_write(int bufID, int prio,
                                              const char* tag, const char* msg) {
     struct iovec vec[3];
     // ......
     vec[0].iov_base = (unsigned char*)&prio;      // 例如通過Log.v調(diào)用過來纸俭,這里是2(VERBOSE)
     vec[0].iov_len = 1;
     vec[1].iov_base = (void*)tag;
     vec[1].iov_len = strlen(tag) + 1;
     vec[2].iov_base = (void*)msg;
     vec[2].iov_len = strlen(msg) + 1;
 
     return write_to_log(bufID, vec, 3);
}

繼續(xù)調(diào)用write_to_log方法皇耗,實(shí)現(xiàn):system/core/liblog/logger_write.c,其中核心部分:

注意:第一次真正執(zhí)行的方法是:__write_to_log_init揍很,初始化后write_to_log的的方法實(shí)現(xiàn)變?yōu)椋篲_write_to_log_daemon郎楼。詳細(xì)參見__write_to_log_init的方法實(shí)現(xiàn)

static int __write_to_log_init(log_id_t, struct iovec* vec, size_t nr);
static int (*write_to_log)(log_id_t, struct iovec* vec,
                           size_t nr) = __write_to_log_init;
// ...
static int __write_to_log_init(log_id_t log_id, struct iovec* vec, size_t nr) {
    int ret, save_errno = errno;
 
    __android_log_lock();        // 加鎖
 
    if (write_to_log == __write_to_log_init) {
        ret = __write_to_log_initialize();
        if (ret < 0) {
            __android_log_unlock();
            if (!list_empty(&__android_log_persist_write)) {
                __write_to_log_daemon(log_id, vec, nr);
            }
           errno = save_errno;
           return ret;
        }
 
        write_to_log = __write_to_log_daemon;
    }
 
    __android_log_unlock();  // 去鎖
 
    ret = write_to_log(log_id, vec, nr);
    errno = save_errno;
    return ret;
}

首先會(huì)執(zhí)行初始化方法__write_to_log_initialize万伤,作用是:為集合__android_log_transport_write設(shè)置各類writer,例如logdLoggerWrite呜袁,pmsgLoggerWrite等敌买,然后依次調(diào)用writer的open方法,例如logdLoggerWrite#logdOpen方法阶界,如果打開失敗虹钮,則關(guān)閉。

logdLoggerWrite#logdOpen方法膘融,代碼位置:system/core/liblog/logd_writer.c芙粱,其實(shí)現(xiàn)是:

/* log_init_lock assumed */
static int logdOpen() {
    // ...
    int sock = TEMP_FAILURE_RETRY(
        socket(PF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0));   // 注意:SOCK_DGRAM代表著UDP通信,SOCK_NONBLOCK非阻塞式(效率高)
    // ...
    strcpy(un.sun_path, "/dev/socket/logdw");
 
    if (TEMP_FAILURE_RETRY(connect(sock, (struct sockaddr*)&un,
                                     sizeof(struct sockaddr_un))) < 0) {
    // ...
}

繼續(xù)調(diào)用__write_to_log_daemon方法氧映,實(shí)現(xiàn):system/core/liblog/logger_write.c春畔,其中核心部分:

static int __write_to_log_daemon(log_id_t log_id, struct iovec* vec, size_t nr) {
    struct android_log_transport_write* node;
    int ret, save_errno;
    struct timespec ts;
    size_t len, i;
    // ...
    clock_gettime(android_log_clockid(), &ts);         // 獲得日志時(shí)間戳
   
    if (log_id == LOG_ID_SECURITY) {
         // ...
    } else if (log_id == LOG_ID_EVENTS || log_id == LOG_ID_STATS) {
         // ...
    } else {
        /* Validate the incoming tag, tag content can not split across iovec */
        char prio = ANDROID_LOG_VERBOSE;
        const char* tag = vec[0].iov_base;
 
        // ...
        // 變量prio存儲(chǔ)vec[0].iov_base,例如2(VERBOSE)岛都,tag存儲(chǔ)vec[1].iov_base
 
       if (!__android_log_is_loggable_len(prio, tag, len - 1, ANDROID_LOG_VERBOSE)) {     //如果當(dāng)前打印的log級(jí)別低于系統(tǒng)設(shè)置的級(jí)別拐迁,會(huì)直接返回,不會(huì)打印疗绣。默認(rèn)是:ANDROID_LOG_VERBOSE(2),系統(tǒng)設(shè)置的級(jí)別來自于屬性:persist.log.tag 或 log.tag
             errno = save_errno;
             return -EPERM;
        }
   }
 
   //....
   // 以下是核心方法實(shí)現(xiàn)
   write_transport_for_each(node, &__android_log_transport_write) {
       if (node->logMask & i) {
           ssize_t retval;
           retval = (*node->write)(log_id, &ts, vec, nr);
           if (ret >= 0) {
               ret = retval;
           }
       }
    }
 
    write_transport_for_each(node, &__android_log_persist_write) {
        if (node->logMask & i) {
            (void)(*node->write)(log_id, &ts, vec, nr);
        }
    }
 
    errno = save_errno;
    return ret;
}

在上面會(huì)看到將循環(huán)調(diào)用所有writer的write方法來傳輸日志铺韧,舉個(gè)例子logdLoggerWrite的write方法(之前已調(diào)用其logdOpen方法:建立Socket連接多矮,path:/dev/socket/logdw):

static int logdWrite(log_id_t logId, struct timespec* ts, struct iovec* vec,
                     size_t nr) {
 
 
    // ...
    /*
     * The write below could be lost, but will never block.
     *
     * ENOTCONN occurs if logd has died.
     * ENOENT occurs if logd is not running and socket is missing.
     * ECONNREFUSED occurs if we can not reconnect to logd.
     * EAGAIN occurs if logd is overloaded.
     */
    if (sock < 0) {
        ret = sock;
    } else {
        ret = TEMP_FAILURE_RETRY(writev(sock, newVec, i));    // 通過socket寫數(shù)據(jù)
        if (ret < 0) {
            ret = -errno;
        }
   }
   // ...
}



下一步是Logd的邏輯分析,即接收到socket通信傳輸后的數(shù)據(jù)哈打,該如何處理:
Logd進(jìn)程是開機(jī)時(shí)由init進(jìn)程啟動(dòng)塔逃,啟動(dòng)代碼參考:system/core/rootdir/init.rc。Logd進(jìn)程啟動(dòng)時(shí)創(chuàng)建3個(gè)Socket通道通信料仗,代碼實(shí)現(xiàn):system/core/logd/logd.rc湾盗,如下:

service logd /system/bin/logd
    socket logd stream 0666 logd logd
    socket logdr seqpacket 0666 logd logd
    socket logdw dgram+passcred 0222 logd logd
    file /proc/kmsg r
    file /dev/kmsg w
    user logd
    group logd system package_info readproc
    writepid /dev/cpuset/system-background/tasks
//...

例如adb shell連接手機(jī),通過ss -pl查看socket連接:

進(jìn)程啟動(dòng)后立轧,入口方法:system/core/logd/main.cpp格粪,其中入口的main方法實(shí)現(xiàn)不復(fù)雜,主要?jiǎng)?chuàng)建LogBuffer氛改,然后啟動(dòng)5個(gè)listener帐萎,一般重要的是前三個(gè):LogReader,LogListener胜卤,CommandListener疆导,全部繼承于SocketListener(system/core/libsysutils)涌矢,另外還有2個(gè)listener:LogAudit(監(jiān)聽NETLINK_AUDIT疙驾,與selinux有關(guān)),LogKlog,這里不做深究继榆。

int main(int argc, char* argv[]) {
    // ...
     
    // LogBuffer,作用:存儲(chǔ)所有的日志信息
    logBuf = new LogBuffer(times);
     
    // LogReader監(jiān)聽Socket(/dev/socket/logdr)舟山,作用:當(dāng)客戶端連接logd后拢锹,LogReader將LogBuffer中的日志寫給客戶端。線程名:logd.reader囤耳,通過prctl(PR_SET_NAME, "logd.reader");設(shè)定
    LogReader* reader = new LogReader(logBuf);
    if (reader->startListener()) {
        exit(1);
    }
 
    // LogListener監(jiān)聽Socket(/dev/socket/logdw)篙顺,作用:接收傳來的日志信息,寫入LogBuffer充择;同時(shí)LogReader將新的日志傳給已連接的客戶端德玫。線程名:logd.writer
    LogListener* swl = new LogListener(logBuf, reader);
    if (swl->startListener(600)) {
        exit(1);
    }
 
    //CommandListener監(jiān)聽Socket(/dev/socket/logd),作用:接收發(fā)來的命令椎麦。線程名:logd.control
    CommandListener* cl = new CommandListener(logBuf, reader, swl);
    if (cl->startListener()) {
        exit(1);
    }
    // ...
    exit(0);
}

首先看LogListener宰僧,當(dāng)有對(duì)端進(jìn)程通過Socket傳遞過來數(shù)據(jù)后,onDataAvailable方法被調(diào)用观挎,其中主要是解析數(shù)據(jù)琴儿、調(diào)用LogBuffer->log方法存儲(chǔ)日志信息,調(diào)用LogReader→notifyNewLog方法通知有新的日志信息嘁捷,以便發(fā)送給其客戶端造成。如下:

bool LogListener::onDataAvailable(SocketClient* cli) {
    // ...
    // 1. 調(diào)用LogBuffer->log方法存儲(chǔ)日志信息
    int res = logbuf->log(
            logId, header->realtime, cred->uid, cred->pid, header->tid, msg,
            ((size_t)n <= USHRT_MAX) ? (unsigned short)n : USHRT_MAX);
 
    // 2. 調(diào)用LogReader→notifyNewLog方法通知有新的日志信息,以便發(fā)送給其客戶端
    if (res > 0 && reader != nullptr) {
        reader->notifyNewLog(static_cast<log_mask_t>(1 << logId));
    }
    // ...
}

繼續(xù)看LogBuffer的log方法雄嚣,代碼位置:system/core/logd/LogBuffer.cpp

int LogBuffer::log(log_id_t log_id, log_time realtime, uid_t uid, pid_t pid,
                   pid_t tid, const char* msg, unsigned short len) {
    // ...
    // 低于當(dāng)前設(shè)定的日志優(yōu)先級(jí)晒屎,返回
    if (!__android_log_is_loggable_len(prio, tag, tag_len,
                                       ANDROID_LOG_VERBOSE)) {
        // Log traffic received to total
        wrlock();
        stats.addTotal(elem);
        unlock();
        delete elem;
        return -EACCES;
    }
 
    // 調(diào)用重載的log方法
    log(elem);
    unlock();
 
    return len;
}

繼續(xù)log方法,主要作用是通過比對(duì)新進(jìn)日志信息的時(shí)間缓升,將其插入到正確的存儲(chǔ)位置鼓鲁。所有日志存儲(chǔ)在mLogElements變量中,其類型是:typedef std::list<LogBufferElement*>

void LogBuffer::log(LogBufferElement* elem) {
      // 插入正確位置港谊,邏輯相對(duì)復(fù)雜骇吭,摘取其中關(guān)鍵一段
      do {
            last = it;
            if (__predict_false(it == mLogElements.begin())) {
                  break;
            }
            --it;
      } while (((*it)->getRealTime() > elem->getRealTime()) && (!end_set || (end <= (*it)->getRealTime())));
            mLogElements.insert(last, elem);
      }
 
     // ...
     stats.add(elem);           // 初步看做一些統(tǒng)計(jì)工作,例如通過數(shù)組歧寺,統(tǒng)計(jì)不同類型日志的打印次數(shù)燥狰,不同類型日志的字符串總長度等,并且將日志信息以u(píng)id, pid, tid, tag等為單位斜筐,保存elem信息至不同的hashtable中
     maybePrune(elem->getLogId());
}

其中maybePrune方法的作用很重要碾局,當(dāng)不同類型的log日志size超過最大限制時(shí),會(huì)觸發(fā)對(duì)已保存日志信息的裁剪奴艾,一次裁剪量約為10%:

void LogBuffer::maybePrune(log_id_t id) {
    size_t sizes = stats.sizes(id);                                         // 來自LogStatistics->mSizes[id]變量的值净当,統(tǒng)計(jì)不同日志類型的當(dāng)前日志長度(msg)
    unsigned long maxSize = log_buffer_size(id);            // 取不同日志類型的日志長度最大值
    if (sizes > maxSize) {
        size_t sizeOver = sizes - ((maxSize * 9) / 10);
        size_t elements = stats.realElements(id);
        size_t minElements = elements / 100;
        if (minElements < minPrune) {                               // minPrune值是4
            minElements = minPrune;                                 // minElements默認(rèn)是全部日志元素?cái)?shù)的百分之一,最小值是4
        }
        unsigned long pruneRows = elements * sizeOver / sizes;  // 需要裁剪的元素個(gè)數(shù),最小值是4個(gè)像啼,最大值是256個(gè),正常是總元素的比例:1 - (maxSize/sizes)* 0.9 = 約等于10%
        if (pruneRows < minElements) {
            pruneRows = minElements;
        }
        if (pruneRows > maxPrune) {                               // maxPrune值是256
            pruneRows = maxPrune;
        }
        prune(id, pruneRows);                                           // 如果日志存儲(chǔ)已越界忽冻,則最終走到prune裁剪函數(shù)中處理真朗,pruneRows是需要裁剪的元素個(gè)數(shù)
    }
}

重要備注:不同日志類型的日志長度最大值,由上到下取值順序:
persist.logd.size.* // 例如:persist.logd.size.main、persist.logd.size.radio臀防、persist.logd.size.events袱衷、persist.logd.size.system、persist.logd.size.crash笑窜、persist.logd.size.stats致燥、persist.logd.size.security、persist.logd.size.kernel
ro.logd.size.* // 例如:ro.logd.size.main排截、ro.logd.size.radio篡悟、ro.logd.size.events、ro.logd.size.system匾寝、ro.logd.size.crash、ro.logd.size.stats荷腊、ro.logd.size.security艳悔、ro.logd.size.kernel
persist.logd.size // 設(shè)置APP:開發(fā)者選項(xiàng)-日志記錄器緩沖區(qū)大小,默認(rèn)256K
ro.logd.size
LOG_BUFFER_MIN_SIZE // 64K女仰,條件是如果ro.config.low_ram是true猜年,表示低內(nèi)存手機(jī)
LOG_BUFFER_SIZE // 256K

另外可以用adb logcat -g命令查看緩沖區(qū)大小
具體執(zhí)行的prune裁剪方法這里沒有深究,感興趣的同學(xué)可以看下system/core/logd/LogBuffer.cpp#prune方法疾忍,大致思路是:
a. 支持黑/白名單(詳見LogWhiteBlackList.cpp乔外,uid + pid。注意:adb logcat -P可設(shè)置)一罩,白名單中不裁剪
b. 優(yōu)先裁剪黑名單杨幼、打印日志最多的uid,system uid中打印日志最多的pid

至此一次完整的:APP調(diào)用Log.v方法打印VERBOSE日志,調(diào)用執(zhí)行過程完畢差购!



二. Logcat命令行工具

官方定義:

Logcat 是一個(gè)命令行工具四瘫,用于轉(zhuǎn)儲(chǔ)系統(tǒng)消息日志,包括設(shè)備拋出錯(cuò)誤時(shí)的堆棧軌跡欲逃,以及從您的應(yīng)用中使用 Log 類寫入的消息找蜜。

常用命令:

代碼位置:system/core/logcat/,可執(zhí)行文件位于:/system/bin/logcat稳析,每次執(zhí)行adb shell logcat命令后洗做,系統(tǒng)會(huì)新起一個(gè)logcat進(jìn)程,用來處理命令彰居,父進(jìn)程是adbd進(jìn)程诚纸。adb shell logcat命令退出后,進(jìn)程退出裕菠。

logcat進(jìn)程啟動(dòng)時(shí)入口在logcat_main.cpp#main()方法咬清,其中核心android_logcat_run_command方法中調(diào)用__logcat方法來解析命令參數(shù),最終通過Socket發(fā)送給logd處理等奴潘,例如clear命令會(huì)通過發(fā)送給logd的CommandListener類(logd.control線程)來處理旧烧。



三. 參考資料

  1. https://developer.android.com/reference/android/util/Log

  2. https://developer.android.com/studio/command-line/logcat?hl=zh_cn,logcat命令行工具

  3. https://developer.android.com/studio/debug/am-logcat画髓,使用logcat寫入和查看日志

  4. http://gityuan.com/2018/01/27/android-log/掘剪,Android logd日志原理

  5. https://developer.android.com/studio/command-line/logcat





作者:kevin song,2019.10.14于南京建鄴區(qū)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末奈虾,一起剝皮案震驚了整個(gè)濱河市夺谁,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌肉微,老刑警劉巖匾鸥,帶你破解...
    沈念sama閱讀 218,204評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異碉纳,居然都是意外死亡勿负,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門劳曹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來奴愉,“玉大人,你說我怎么就攤上這事铁孵《穑” “怎么了?”我有些...
    開封第一講書人閱讀 164,548評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵蜕劝,是天一觀的道長檀头。 經(jīng)常有香客問我轰异,道長,這世上最難降的妖魔是什么鳖擒? 我笑而不...
    開封第一講書人閱讀 58,657評(píng)論 1 293
  • 正文 為了忘掉前任溉浙,我火速辦了婚禮,結(jié)果婚禮上蒋荚,老公的妹妹穿的比我還像新娘戳稽。我一直安慰自己,他們只是感情好期升,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,689評(píng)論 6 392
  • 文/花漫 我一把揭開白布惊奇。 她就那樣靜靜地躺著,像睡著了一般播赁。 火紅的嫁衣襯著肌膚如雪颂郎。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,554評(píng)論 1 305
  • 那天容为,我揣著相機(jī)與錄音乓序,去河邊找鬼。 笑死坎背,一個(gè)胖子當(dāng)著我的面吹牛替劈,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播得滤,決...
    沈念sama閱讀 40,302評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼陨献,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了懂更?” 一聲冷哼從身側(cè)響起眨业,我...
    開封第一講書人閱讀 39,216評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎沮协,沒想到半個(gè)月后龄捡,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,661評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡慷暂,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,851評(píng)論 3 336
  • 正文 我和宋清朗相戀三年聘殖,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片呜呐。...
    茶點(diǎn)故事閱讀 39,977評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖悍募,靈堂內(nèi)的尸體忽然破棺而出蘑辑,到底是詐尸還是另有隱情,我是刑警寧澤坠宴,帶...
    沈念sama閱讀 35,697評(píng)論 5 347
  • 正文 年R本政府宣布洋魂,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏副砍。R本人自食惡果不足惜衔肢,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,306評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望豁翎。 院中可真熱鬧角骤,春花似錦、人聲如沸心剥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽优烧。三九已至蝉揍,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間畦娄,已是汗流浹背又沾。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評(píng)論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留熙卡,地道東北人杖刷。 一個(gè)月前我還...
    沈念sama閱讀 48,138評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像再膳,于是被迫代替她去往敵國和親挺勿。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,927評(píng)論 2 355

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