Android 全埋點(diǎn)解決方案(一)

一拱撵、埋點(diǎn)方案總結(jié)
AppStart 、AppEnd 全埋點(diǎn)方案

  • AppClick全埋點(diǎn)方案1: 代理View.OnclickListener
  • AppClick全埋點(diǎn)方案2: 代理Window.Callback
  • AppClick全埋點(diǎn)方案3: 代理View.AccessibilityDelegate
  • AppClick全埋點(diǎn)方案4: 透明層
  • AppClick全埋點(diǎn)方案5: AspectJ
  • AppClick全埋點(diǎn)方案6: ASM
  • AppClick全埋點(diǎn)方案7: JavaSsist
  • AppClick全埋點(diǎn)方案8: AST

二振定、埋點(diǎn)事件簡介

  • AppStart 事件
    是指app啟動(dòng)谢澈,同時(shí)包括冷啟動(dòng)和熱啟動(dòng)拨扶,熱啟動(dòng)是指應(yīng)用程序從后臺恢復(fù)逗余。
  • AppEnd 事件
    是指app退出,包括正常退出影钉、home退到后臺画髓、被強(qiáng)殺、崩潰等場景平委。
  • AppViewScreen 事件
    是指App頁面瀏覽奈虾,切換Activity或者Fragment
  • AppClick 事件
    是指App的點(diǎn)擊事件,所有的view的點(diǎn)擊事件

三、AppClick事件的全埋點(diǎn)整體解決思路
就是要自動(dòng)找到 那個(gè)被點(diǎn)擊事件的控制處理邏輯(后文統(tǒng)稱原處理邏輯)肉微,利用一定的技術(shù)處理匾鸥,來對原處理邏輯進(jìn)行 "攔截" ,或者在原處理邏輯執(zhí)行前面或執(zhí)行后面 "插入" 相應(yīng)的埋點(diǎn)代碼碉纳,從而達(dá)到自動(dòng)埋點(diǎn)的效果勿负。

在編譯器對Java代碼的處理流程中,可以采用不同的埋點(diǎn)方案劳曹。

? ? ? ? ? ? ? ? ??APT? ? ? ? ? ? ??????AspectJ? ? ? ? ? ? ? ? ASM
JavaCode ----------.java ----------- .class ----------- .dex
? ? ? ? ? ? ? ? ?? AST? ? ? ? ? ? ? ? ??? ? ? ? ? ? ? ? ? ? ?????? Javassit

四奴愉、全埋點(diǎn)綜合方案考慮因素

  • 效率
    • 靜態(tài)代理
      通過Gradle Plugin 在應(yīng)用程序編譯期間 “插入”代碼或者修改代碼(.class)。比如AspectJ铁孵、ASM锭硼、JavaSsist、AST等均屬于這種方式库菲。
    • 動(dòng)態(tài)代理
      在代碼運(yùn)行的時(shí)候(Runtime)去進(jìn)行代理账忘。例如:View.OnClickListener志膀、Window.Callback熙宇、View.AccessbilityDelegate等方案均屬于這種方式。

靜態(tài)代理明顯優(yōu)于動(dòng)態(tài)代理溉浙,因?yàn)殪o態(tài)代理是在程序編譯階段處理的烫止,不會對應(yīng)用程序的整體性能有太大影響,而動(dòng)態(tài)代理是在程序運(yùn)行階段發(fā)生的戳稽,所以對程序性能會有一定的影響馆蠕。

  • 兼容性
    Android生態(tài)系統(tǒng)一直在飛速發(fā)展,有不同的開發(fā)語言(Java惊奇、Kotlin互躬、Flutter),不同的Java版本(Java7颂郎、Java8)吼渡、混合開發(fā)、不同的Gradle版本乓序,以及Lambda寺酪、D8、Instant Run替劈、DataBinding寄雀、Fragemnt等,都會給兼容性帶來影響陨献。

  • 擴(kuò)展性
    隨著業(yè)務(wù)快速發(fā)展盒犹,數(shù)據(jù)分析不斷提高,我們自動(dòng)采集要求越來越高等。

五急膀、埋點(diǎn)實(shí)現(xiàn)思路

  • AppViewScreen 事件
    ActivityLifecycleCallbacks是Application的一個(gè)內(nèi)部接口膜蛔,是從API14(Android 4.0)開始提供的。它提供了生命周期的監(jiān)聽脖阵。
    public interface ActivityLifecycleCallbacks {

        /**
         * Called as the first step of the Activity being created. This is always called before
         * {@link Activity#onCreate}.
         */
        default void onActivityPreCreated(@NonNull Activity activity,
                @Nullable Bundle savedInstanceState) {
        }

        /**
         * Called when the Activity calls {@link Activity#onCreate super.onCreate()}.
         */
        void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState);

        /**
         * Called as the last step of the Activity being created. This is always called after
         * {@link Activity#onCreate}.
         */
        default void onActivityPostCreated(@NonNull Activity activity,
                @Nullable Bundle savedInstanceState) {
        }

        /**
         * Called as the first step of the Activity being started. This is always called before
         * {@link Activity#onStart}.
         */
        default void onActivityPreStarted(@NonNull Activity activity) {
        }

        /**
         * Called when the Activity calls {@link Activity#onStart super.onStart()}.
         */
        void onActivityStarted(@NonNull Activity activity);

        /**
         * Called as the last step of the Activity being started. This is always called after
         * {@link Activity#onStart}.
         */
        default void onActivityPostStarted(@NonNull Activity activity) {
        }

        /**
         * Called as the first step of the Activity being resumed. This is always called before
         * {@link Activity#onResume}.
         */
        default void onActivityPreResumed(@NonNull Activity activity) {
        }

        /**
         * Called when the Activity calls {@link Activity#onResume super.onResume()}.
         */
        void onActivityResumed(@NonNull Activity activity);

        /**
         * Called as the last step of the Activity being resumed. This is always called after
         * {@link Activity#onResume} and {@link Activity#onPostResume}.
         */
        default void onActivityPostResumed(@NonNull Activity activity) {
        }

        /**
         * Called as the first step of the Activity being paused. This is always called before
         * {@link Activity#onPause}.
         */
        default void onActivityPrePaused(@NonNull Activity activity) {
        }

        /**
         * Called when the Activity calls {@link Activity#onPause super.onPause()}.
         */
        void onActivityPaused(@NonNull Activity activity);

        /**
         * Called as the last step of the Activity being paused. This is always called after
         * {@link Activity#onPause}.
         */
        default void onActivityPostPaused(@NonNull Activity activity) {
        }

        /**
         * Called as the first step of the Activity being stopped. This is always called before
         * {@link Activity#onStop}.
         */
        default void onActivityPreStopped(@NonNull Activity activity) {
        }

        /**
         * Called when the Activity calls {@link Activity#onStop super.onStop()}.
         */
        void onActivityStopped(@NonNull Activity activity);

        /**
         * Called as the last step of the Activity being stopped. This is always called after
         * {@link Activity#onStop}.
         */
        default void onActivityPostStopped(@NonNull Activity activity) {
        }

        /**
         * Called as the first step of the Activity saving its instance state. This is always
         * called before {@link Activity#onSaveInstanceState}.
         */
        default void onActivityPreSaveInstanceState(@NonNull Activity activity,
                @NonNull Bundle outState) {
        }

        /**
         * Called when the Activity calls
         * {@link Activity#onSaveInstanceState super.onSaveInstanceState()}.
         */
        void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState);

        /**
         * Called as the last step of the Activity saving its instance state. This is always
         * called after{@link Activity#onSaveInstanceState}.
         */
        default void onActivityPostSaveInstanceState(@NonNull Activity activity,
                @NonNull Bundle outState) {
        }

        /**
         * Called as the first step of the Activity being destroyed. This is always called before
         * {@link Activity#onDestroy}.
         */
        default void onActivityPreDestroyed(@NonNull Activity activity) {
        }

        /**
         * Called when the Activity calls {@link Activity#onDestroy super.onDestroy()}.
         */
        void onActivityDestroyed(@NonNull Activity activity);

        /**
         * Called as the last step of the Activity being destroyed. This is always called after
         * {@link Activity#onDestroy}.
         */
        default void onActivityPostDestroyed(@NonNull Activity activity) {
        }
    }

所以皂股,我們可以直接在onResume里面做一個(gè)頁面信息的統(tǒng)計(jì)。

  • AppStart 命黔、 AppEnd 埋點(diǎn)方案
    最好的方案還是使用ActivityLifecycleCallbacks呜呐,監(jiān)聽onResume表示AppStart。但是由于應(yīng)用程序會有多個(gè)進(jìn)程悍募,會導(dǎo)致無法判斷當(dāng)前進(jìn)程是出于前臺還是后臺蘑辑。我們可以采用ContentProvider+SharedPreferences的方案來解決跨進(jìn)程數(shù)據(jù)共享問題。
    然后當(dāng)應(yīng)用程序被強(qiáng)殺坠宴、崩潰的時(shí)候洋魂,我們該如何判斷呢?對于一個(gè)應(yīng)用程序喜鼓,如果一個(gè)頁面退出30s內(nèi)副砍,沒有其他頁面顯示出來,我們就認(rèn)為應(yīng)用程序處于后臺了庄岖,也就是發(fā)生了AppEnd事件豁翎。

  • AppClick 事件
    1.代理View.OnClickListener
    監(jiān)聽OnResume生命周期,我們會寫一個(gè)WrapperOnClickListener類隅忿,來代理點(diǎn)擊事件心剥,以及在后面插入對應(yīng)的統(tǒng)計(jì)代碼。我們可以通過activity.getWindow().getDecorView()來獲取根布局背桐,然后通過遍歷根布局來獲取當(dāng)前設(shè)置了點(diǎn)擊事件的OnclickListener對象优烧,這里通過反射view的getListenerInfo方法拿到ListenerInfo的mOnclickListener字段。反射效率比較低链峭,然后高版本的兼容性也會有些問題畦娄。然后把OnclickListener對象交給WrapperOnclickListener觸發(fā)。
    基本上實(shí)現(xiàn)了埋點(diǎn)熏版,但是在onResume后動(dòng)態(tài)addview纷责,無法注入埋點(diǎn)代碼『扯蹋可以采用ViewTreeObserver.OnGlobalLayoutListener來解決這個(gè)問題再膳。在onGlobalLayout回調(diào)中重新調(diào)用上面的步驟,也就是重新遍歷布局曲横,找到有點(diǎn)擊事件的view喂柒,把點(diǎn)擊事件交給WrapperOnClickListener處理不瓶,并插入對應(yīng)的統(tǒng)計(jì)代碼。
    缺點(diǎn):

    • 由于使用反射灾杰,效率比較低蚊丐,對App的整體性能有一定的影響,也可能會引入兼容性問題艳吠;
    • Application.ActivityLifecycleCallbacks 要求是API 14+麦备;
    • View.hasOnClickListeners()要求API 15+;
    • removeOnGlobalLayoutListener要求API 16+昭娩;
    • 無法采集Activity之上的View的點(diǎn)擊凛篙,比如Dialog,PopupWindow等栏渺。

2.Window.Callback
Window.callback是Window類的一個(gè)內(nèi)部類呛梆。該接口包含了一系列類似于dispatchXXX和onXXX是接口。當(dāng)用戶點(diǎn)擊某個(gè)控件時(shí)磕诊,就會回調(diào)Window.Callback中的dispatchTouchEvent(MotionEvent event)方法填物。

原理概述
在Application中初始化埋點(diǎn)sdk,然后注冊監(jiān)聽Application.ActivityLifecycleCallbacks的onCreate霎终,獲取當(dāng)前的Activity對象滞磺,通過activity拿到當(dāng)前的window,activity.getWindow(),再通過window.getCallback()可以拿到Window.callback對象神僵。然后使用自定義的WrapperWindowCallbcak代理這個(gè)Window.Callback對象雁刷。WrapperWindowCallbcak里面主要是重寫了dispatchTouchEvent(MotionEvent event)方法,通過MotionEvent參數(shù)(點(diǎn)擊的坐標(biāo))找到被點(diǎn)擊的那個(gè)view保礼,并插入埋點(diǎn)代碼,最后在調(diào)用原有的dispatchTouchEvent(MotionEvent event)方法责语,即達(dá)到“插入”埋點(diǎn)代碼的效果炮障。
缺點(diǎn):

  • 由于使用反射,效率比較低坤候,對App的整體性能有一定的影響胁赢,也可能會引入兼容性問題;
    • Application.ActivityLifecycleCallbacks 要求是API 14+白筹;
    • View.hasOnClickListeners()要求API 15+智末;
    • removeOnGlobalLayoutListener要求API 16+;
    • 無法采集Dialog徒河、Popupwindow等游離于Activity之外的控件的點(diǎn)擊事件

3.Accesibility
輔助功能系馆,Android系統(tǒng)通過輔助功能幫助一些功能損失的人更好的使用APP。我們知道顽照,點(diǎn)擊事件是會調(diào)用performClik()的由蘑,里面調(diào)用了mOnclickListener.onClick之后闽寡,還會調(diào)用到sendAccessibilityEvent(AccessbilityEvent.TYPE_VIEW_CLICKED),它里面是調(diào)用了mAccessbilityDelegate對象的sendAccessibilityEvent方法,并傳入View對象和mAccessbilityDelegate.TYPE_VIEW_CLICKED參數(shù)尼酿。

原理概述
首先還是通過Application來監(jiān)聽activity的onResume方法爷狈,拿到DecordView,然后遍歷所有view裳擎,設(shè)置自定義的SensorsDataAccessbilityDelegate代理當(dāng)前View.sendAccessbiityEvent方法涎永。在布局改變的時(shí)候做上面相同的操作(監(jiān)聽ViewTreeObserve)。在自定義SensorsDataAccessbilityDelegate中會調(diào)用原有的sendAccessibilityEvent方法鹿响,并判斷是否是AccessbilityEvent.TYPE_VIEW_CLICKED類型土辩,如果是,說明有點(diǎn)擊事件抢野,就做對應(yīng)的代碼插入拷淘。

缺點(diǎn)

  • 由于使用反射,效率比較低指孤,對App的整體性能有一定的影響启涯,也可能會引入兼容性問題;
  • Application.ActivityLifecycleCallbacks 要求是API 14+恃轩;
  • View.hasOnClickListeners()要求API 15+结洼;
  • removeOnGlobalLayoutListener要求API 16+;
  • 無法采集Dialog叉跛、Popupwindow等游離于Activity之外的控件的點(diǎn)擊事件
  • 輔助功能需要用戶手動(dòng)開啟松忍,在部分android Rom上輔助功能可能會失效。

4.透明層
原理概述
由于Android的事件分發(fā)都是會經(jīng)過onTouchEvent方法筷厘。我們可以獲取到當(dāng)前的Activity鸣峭,在布局的最上層添加一個(gè)自定義透明的view。重寫view的onTouchEvent方法酥艳,獲取當(dāng)前點(diǎn)擊的坐標(biāo)摊溶,從RootView中找到點(diǎn)擊的view,然后交給自定義的WrapperOnClickLitener處理充石。

  • Application.ActivityLifecycleCallbacks 要求是API 14+莫换;
  • View.hasOnClickListeners()要求API 15+;
  • removeOnGlobalLayoutListener要求API 16+骤铃;
  • 無法采集Dialog拉岁、Popupwindow等游離于Activity之外的控件的點(diǎn)擊事件
  • 每次點(diǎn)擊都需要遍歷RootView,效率比較低惰爬。

5.AspectJ
AOP喊暖,面向切面編程,AspectJ實(shí)際上是其中的一種补鼻。對于ApsectJ不了解的可以自行了解哄啄。也需要使用到 Gradle plugin 不了解可以自行學(xué)習(xí)一下雅任。

原理概述
我們可以把AspectJ的處理腳本放到我們自定義的插件里面,然后編寫相應(yīng)的切面類咨跌,再定義合適的PointCut用來匹配我們的織入目標(biāo)的方法(listener對象的相應(yīng)回調(diào)方法)沪么,比如Android.view.View.OnClickListener的onClick方法,就可以在編譯期間插入埋點(diǎn)代碼锌半,從而達(dá)到自動(dòng)埋點(diǎn)即全埋點(diǎn)的效果禽车。
缺點(diǎn)

  • 無法織入第三方庫
  • 由于定義的切點(diǎn)依賴編程語言,目前該方案我無法兼容Lambda語法
  • 會有一些兼容性方面的問題刊殉,比如:D8殉摔、Gradle4.x等。

6.ASM
ASM可以在.class 文件打包成.dex文件之前修改.class文件记焊。
Gradle Transform 可以在編譯的時(shí)候遍歷所有.class文件逸月,并可以轉(zhuǎn)換成所有需要的.class輸出。

原理概述
定義一個(gè)Gradle Plugin遍膜,然后注冊一個(gè)Transform對象碗硬。在transform方法里面,可以分別遍歷目錄和jar包瓢颅,然后我們就可以遍歷當(dāng)前應(yīng)用程序所有的.class文件恩尾。然后再利用ASM框架的API,去加載相應(yīng)的.class文件挽懦、解析.class文件翰意,然后可以找到符合條件的.class文件和相關(guān)方法,最后去修改相應(yīng)的方法以動(dòng)態(tài)插入埋點(diǎn)字節(jié)碼信柿,從而達(dá)到自動(dòng)埋點(diǎn)的效果冀偶。

缺點(diǎn):目前來看ASM是最完美的方法,沒有什么缺點(diǎn)角塑。

7.Javassist
java字節(jié)碼以二進(jìn)制的形式存儲在.class文件中蔫磨,每一個(gè).class文件包含一個(gè)java類和接口。Javassist框架就是一個(gè)用來處理java字節(jié)碼的類庫圃伶。它可以在一個(gè)已編譯好的類中添加新的方法,或者修改已有的方法蒲列,并且不需要對字節(jié)碼方面有深入的了解窒朋。
javassist可以繞過編譯,直接操作字節(jié)碼蝗岖,從而實(shí)現(xiàn)代碼的注入侥猩。所以,使用javassist框架的最佳時(shí)機(jī)就是構(gòu)建工具Gradle將源文件編譯成.class文件之后抵赢,在將.class打包成.dex文件之前欺劳。

原理概述
跟上面ASM的原理一樣唧取,只是把ASM換成了javassist。

8.AST
APT是Annotation Processing Tool 的縮寫划提,即注解處理器枫弟,是一種處理注解的工具。確切來說鹏往,它是javac的一個(gè)工具淡诗,用來在編譯時(shí)掃描和處理注解。注解處理器以java代碼作為輸入伊履,以生成.java文件作為輸出韩容。簡單來說,就是在編譯期間通過注解生成.java文件唐瀑。

AST群凶,是Abstract Syntax Tree的縮寫,即“抽象語法樹”哄辣,是編輯器對代碼的第一步加工之后的結(jié)果请梢,是一個(gè)樹形式表示的源代碼。源代碼的每個(gè)元素映射到一個(gè)節(jié)點(diǎn)或者子樹柔滔。
java的編譯分為三個(gè)階段:
第一階段:所有的源文件都會被解析成語法樹溢陪。
第二階段:調(diào)用注解解析器,即APT模塊睛廊。如果注解解析器產(chǎn)生了新的源文件形真,新的源文件也要參與編譯。
第三個(gè)階段:語法樹會被分析并轉(zhuǎn)化為類文件超全。

原理概述
JavaTXT-->詞語法分析-->生成AST-->編譯字節(jié)碼咆霜、
通過操作AST,可以達(dá)到修改源代碼的功能嘶朱。
在自定義的注解解析器的process方法里蛾坯,通過roundEnvironment.getRootElements()方法可以拿到所有的Element對象,通過tree.getTree(element)方法可以拿到對應(yīng)的抽象語法樹(AST)疏遏,然后我們自定義一個(gè)TreeTranslator脉课,在visitMethodDef里面即可對方法進(jìn)行判斷。如果是目標(biāo)處理方法财异,則通過AST框架的相關(guān)API即可插入埋點(diǎn)代碼倘零,從而實(shí)現(xiàn)全埋點(diǎn)效果。
缺點(diǎn)

  • com.sun.tools.javac.tree相關(guān)語法晦澀戳寸,理解難度大呈驶,要求有一定的編譯原理基礎(chǔ)
  • APT無法掃描其他的module,導(dǎo)致AST無法處理其他module
  • 不支持Lambda語法
  • 帶有返回值的方法疫鹊,很難把埋點(diǎn)代碼插入到方法之后

本文參考資料《Android 全埋點(diǎn)解決方案》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末袖瞻,一起剝皮案震驚了整個(gè)濱河市司致,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌聋迎,老刑警劉巖脂矫,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異砌庄,居然都是意外死亡羹唠,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門娄昆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來佩微,“玉大人,你說我怎么就攤上這事萌焰〔该校” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵扒俯,是天一觀的道長奶卓。 經(jīng)常有香客問我削饵,道長蛔添,這世上最難降的妖魔是什么抡谐? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任牲蜀,我火速辦了婚禮,結(jié)果婚禮上或详,老公的妹妹穿的比我還像新娘佩研。我一直安慰自己享完,他們只是感情好荔茬,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布废膘。 她就那樣靜靜地躺著,像睡著了一般慕蔚。 火紅的嫁衣襯著肌膚如雪丐黄。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天孔飒,我揣著相機(jī)與錄音灌闺,去河邊找鬼。 笑死坏瞄,一個(gè)胖子當(dāng)著我的面吹牛菩鲜,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播惦积,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼猛频!你這毒婦竟也來了狮崩?” 一聲冷哼從身側(cè)響起蛛勉,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎睦柴,沒想到半個(gè)月后诽凌,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡坦敌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年侣诵,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片狱窘。...
    茶點(diǎn)故事閱讀 40,040評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡杜顺,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蘸炸,到底是詐尸還是另有隱情躬络,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布搭儒,位于F島的核電站穷当,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏淹禾。R本人自食惡果不足惜馁菜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望铃岔。 院中可真熱鬧汪疮,春花似錦、人聲如沸德撬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蜓洪。三九已至纤勒,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間隆檀,已是汗流浹背摇天。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留恐仑,地道東北人泉坐。 一個(gè)月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像裳仆,于是被迫代替她去往敵國和親腕让。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評論 2 355

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