Android插件化原理(Small)

插件化原理(small)

ClassLoader

DexClassLoader 和 PathClassLoader

android 中的calssloader舀透,區(qū)別在于DexClassLoader多了一個optimize的優(yōu)化目錄,其可以加載外部的dex尊沸,zip,so等包些楣,而pathclassloader只能加載內(nèi)部的dex续崖,apk等包

而兩個都是繼承自BaseDexClassLoader ,而BaseDexClassLoader的主要工作是交給DexPathList是做,接下來讓我們看看這個DexPathList的構(gòu)造方法


  public DexPathList(ClassLoader definingContext, String dexPath,
            String libraryPath, File optimizedDirectory) {
        if (definingContext == null) {
            throw new NullPointerException("definingContext == null");
        }
        if (dexPath == null) {
            throw new NullPointerException("dexPath == null");
        }
        if (optimizedDirectory != null) {
            if (!optimizedDirectory.exists())  {
                throw new IllegalArgumentException(
                        "optimizedDirectory doesn't exist: "
                        + optimizedDirectory);
            }
            if (!(optimizedDirectory.canRead()
                            && optimizedDirectory.canWrite())) {
                throw new IllegalArgumentException(
                        "optimizedDirectory not readable/writable: "
                        + optimizedDirectory);
            }
        }
        this.definingContext = definingContext;
        this.dexElements =
            makeDexElements(splitDexPath(dexPath), optimizedDirectory);
        this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
    }

dexPath就是我們需要加載插件的路徑鼻百,可以看到主要是由makeDexElements這個方法實(shí)現(xiàn)

    private static Element[] makeDexElements(ArrayList<File> files,
            File optimizedDirectory) {
        ArrayList<Element> elements = new ArrayList<Element>();
        /*
         * Open all files and load the (direct or contained) dex files
         * up front.
         */
        for (File file : files) {
            ZipFile zip = null;
            DexFile dex = null;
            String name = file.getName();
            if (name.endsWith(DEX_SUFFIX)) {
                // Raw dex file (not inside a zip/jar).
                try {
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException ex) {
                    System.logE("Unable to load dex file: " + file, ex);
                }
            } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
                    || name.endsWith(ZIP_SUFFIX)) {
                try {
                    zip = new ZipFile(file);
                } catch (IOException ex) {
                    /*
                     * Note: ZipException (a subclass of IOException)
                     * might get thrown by the ZipFile constructor
                     * (e.g. if the file isn't actually a zip/jar
                     * file).
                     */
                    System.logE("Unable to open zip file: " + file, ex);
                }
                try {
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException ignored) {
                    /*
                     * IOException might get thrown "legitimately" by
                     * the DexFile constructor if the zip file turns
                     * out to be resource-only (that is, no
                     * classes.dex file in it). Safe to just ignore
                     * the exception here, and let dex == null.
                     */
                }
            } else {
                System.logW("Unknown file type for: " + file);
            }
            if ((zip != null) || (dex != null)) {
                elements.add(new Element(file, zip, dex));
            }
        }
        return elements.toArray(new Element[elements.size()]);
    }

該方法返回的是一個element的數(shù)組绞旅。
看到這里,我們先不用深入理解makeDexElements內(nèi)部的邏輯實(shí)現(xiàn)温艇,先思考一個問題因悲,何為插件化?
我們做插件的目的是有很多種勺爱,例如:減少包體積晃琳,熱更新 。。卫旱。
插件化意味著宿主和插件之間能夠進(jìn)行通信人灼,宿主可以調(diào)用插件里的對象,宿主可以訪問插件里的資源等等顾翼。

所以每個BaseDexClassLoader構(gòu)造完之后都會有一個dexElements投放,這就說明宿主的classloader有一個,我們插件內(nèi)部自己的classloader也會有一個适贸,說到這里已經(jīng)說明插件化類訪問的原理了灸芳。其核心就是分為以下步驟:

    1. 宿主的classloader通過反射拿到內(nèi)部的dexPathList數(shù)組
    1. 構(gòu)造一個我們插件的DexClassLoader(而不是PathClassLoader),然后通過反射拿到其中的dexPathList數(shù)組
    1. 將兩個數(shù)組進(jìn)行合并拜姿,然后通過反射設(shè)置會宿主的classloader中

事實(shí)上耗绿,Android官方的multidex就是這個原理。完成這些步驟以后砾隅,我們在宿主中就可以調(diào)用插件的類了误阻,但是工作還沒完,資源如何訪問晴埂?

Resources

設(shè)想一個問題究反,我們將兩個dexPathList進(jìn)行了合并,此時宿主可以調(diào)用插件,但是假設(shè)插件內(nèi)部根據(jù)一個id查找一個資源儒洛,會報(bào)ResourcesNotFind的異常精耐,為什么呢?我們來看看源碼琅锻,假設(shè)當(dāng)前處在插件中的某個activity卦停,根據(jù)id獲取獲取某個drawable并設(shè)置進(jìn)
imageview中

        imageview.setImageDrawable(getResources().getDrawable(R.drawable.ic_launcher_background))

最終回到ContextThemeWrapper中:

    @Override
    public Resources getResources() {
        return getResourcesInternal();
    }

    private Resources getResourcesInternal() {
        //mResouces為空
        if (mResources == null) {
            if (mOverrideConfiguration == null) {
                    //將會調(diào)用super.getResources()
                mResources = super.getResources();
            } else {
                final Context resContext = createConfigurationContext(mOverrideConfiguration);
                mResources = resContext.getResources();
            }
        }
        return mResources;
    }

而super.getReources最終實(shí)現(xiàn)是ContextImpl.getResources()中
而ContextImpl是在ActivityThread中由系統(tǒng)執(zhí)行各個步驟時創(chuàng)建的,我們插件化的activity根本不會走這樣一套流程(如果走這套流程的話恼蓬,插件化就毫無意義啦~~)
所以惊完,拿到的ContextImpl則是宿主的。而這個ComtextImpl在Application到Activity的各個階段都會有所區(qū)別

具體在于处硬,Application的mBase成員是通過ContextImpl.createAppContext經(jīng)過attachBaseContext后創(chuàng)建的小槐,而Activity的mBase成員是通過ContextImpl.createActivityContext創(chuàng)建的,兩者的區(qū)別有興趣可以閱讀下源碼

無論以哪種方式荷辕,最終都會來到ResourcesManager.getOrCreateResources()方法創(chuàng)建資源對象凿跳,
而經(jīng)過層層判斷之后,又會來到createResourcesImpl()方法疮方,
而createResourcesImpl()內(nèi)部

    private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {

        final AssetManager assets = createAssetManager(key);
        if (assets == null) {
            return null;
        }

        final DisplayMetrics dm = getDisplayMetrics(key.mDisplayId, daj);
        final Configuration config = generateConfig(key, dm);
        final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);


        return impl;
    }

而createAssetManager()方法:

 protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) {
        AssetManager assets = new AssetManager();

        // resDir can be null if the 'android' package is creating a new Resources object.
        // This is fine, since each AssetManager automatically loads the 'android' package
        // already.
        if (key.mResDir != null) {
        
                //看到這里大概猜到為什么插件無法訪問資源了
            if (assets.addAssetPath(key.mResDir) == 0) {
                    ...
                return null;
            }
        }
            
        ...
        ...

        return assets;
    }

原來assets.addAssetPath()方法是把key.mResDir加進(jìn)去assetmanager中控嗜,這樣就可以訪問到資源,mResDir就是res文件
至此我們終于知道為啥在插件訪問不到資源了骡显。

看到這里有兩個實(shí)現(xiàn)方法

  1. 在插件的Activity重寫getResources方法疆栏,然后根據(jù)重新創(chuàng)建一個AssetManager曾掂,這樣插件內(nèi)的資源可能通過自己的AssetManager進(jìn)行資源
  2. 通過反射拿到宿主的AssetManager,然后調(diào)用內(nèi)部addAssetPath()將當(dāng)前插件的路徑傳進(jìn)去承边,相當(dāng)于進(jìn)行資源的合并遭殉。

這兩種方法都可行石挂,第一種會導(dǎo)致資源爆炸博助,宿主一份,插件一份痹愚,而且這里面的資源無法公用富岳。第二種則會導(dǎo)致資源id沖突,但是可以通過某些手段進(jìn)行控制(比如控制分配id的段達(dá)到防止資源id沖突)

而small用的是第二種拯腮,并且配合gradle介入資源id段(PP)的分配情況
具體原理則是:在gradle執(zhí)行到mergeAndroidResources這個task時窖式,將R.java,R.txt替換為small extention中配置的packageId字段动壤,并且替換完成后萝喘,重寫整個resources.arsc文件,將原來的arsc文件里面的索引的id替換成配置后的id琼懊。如原來生成的id為0x7F010001 替換成自定義 0x21010001

替換資源id這個方法阁簸,除了上述這個之外,還可以手動修改AAPT的源碼,然后重新編譯一個aapt工具

至此哼丈,資源也可以訪問了启妹。

四大組件

類和資源都可以訪問了,我們都知道四大組件要在宿主的AndroidManifest.xml中注冊才可以使用醉旦,否則會提示找不到該component饶米。以activity為例,如果將插件中的activity在宿主AndroidManifest中注冊,那插件化將毫無意義车胡,因?yàn)槊看斡行耡ctivity都需要更新宿主檬输,插件的思想也就無從談起。

有什么辦法可以做到不在宿主中注冊也可以調(diào)用呢匈棘?

Small使用hook褪猛,主要是hook住Instrumentation,ActivityThread和mH這幾個類。

App創(chuàng)建過程 說過 Instrumentation最初的目的是為了給UI測試預(yù)留的接口羹饰,沒想到可以被插件化玩出花樣來伊滋,可能谷歌一開始也沒想到。

步驟如下:

  • Step 1
public static void hookInstrumentation() {
        
        try {
            Class at = Class.forName("android.app.ActivityThread");
            Method atMethod = at.getDeclaredMethod("currentActivityThread",null);
            atMethod.setAccessible(true);
            Object activityThread = atMethod.invoke(null,null
            );
            
            Field instruFiled = at.getDeclaredField("mInstrumentation");
            instruFiled.setAccessible(true);
            Instrumentation instrumentation = (Instrumentation) instruFiled.get(activityThread);
            
            TestInstrumentationWrapper wrapper = new TestInstrumentationWrapper(instrumentation);
            instruFiled.set(activityThread, wrapper);
            Log.d(TAG,"hook init success");
        } catch (Throwable e) {
            e.printStackTrace();
        }
        
    }
    
    private static class TestInstrumentationWrapper extends Instrumentation {
        
        private Instrumentation mBase;
        
        public TestInstrumentationWrapper(Instrumentation base) {
            mBase = base;
        }
        
        public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent
                intent,
                int requestCode, Bundle options) {
            Log.d(TAG, "TestInstrumentationWrapper hook 1");
            //step1
            return realExecStartActivity1(who, contextThread, token, target, intent, requestCode, options);
        }
        
        public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, String target, Intent intent,
                int requestCode, Bundle options) {
            
            Log.d(TAG, "TestInstrumentationWrapper hook 2");
            
            return realExecStartActivity2(who, contextThread, token, target, intent, requestCode, options);
        }
        
        public ActivityResult execStartActivity(
                Context who, IBinder contextThread, IBinder token, String resultWho,
                Intent intent, int requestCode, Bundle options, UserHandle user) {
            
            Log.d(TAG, "TestInstrumentationWrapper hook 3");
            
            return realExecStartActivity3(who,contextThread,token,resultWho,intent,requestCode,options,user);
        }
        
        @SuppressWarnings("NewApi")
        private ActivityResult realExecStartActivity3(Context who, IBinder contextThread, IBinder token, String resultWho,
                Intent intent, int requestCode, Bundle options, UserHandle user) {
            ActivityResult activityResult = null;
            try {
                Class c = mBase.getClass();
                Method execStartActivity = c.getDeclaredMethod("execStartActivity",
                        Context.class,
                        IBinder.class,
                        IBinder.class,
                        String.class,
                        Intent.class,
                        int.class,
                        Bundle.class,
                        UserHandle.class
                );
                
                activityResult = (ActivityResult) execStartActivity.invoke(mBase,
                        who,
                        contextThread,
                        token,
                        resultWho,
                        intent,
                        requestCode,
                        options,
                        user
                );
                
            } catch (Exception e) {
                e.printStackTrace();
            }
            
            return activityResult;
            
        }
        
        
        private ActivityResult realExecStartActivity2(Context who, IBinder contextThread, IBinder token, String target,
                Intent intent, int requestCode, Bundle options) {
            ActivityResult activityResult = null;
            try {
                Class c = mBase.getClass();
                Method execStartActivity = c.getDeclaredMethod("execStartActivity",
                        Context.class,
                        IBinder.class,
                        IBinder.class,
                        String.class,
                        Intent.class,
                        int.class,
                        Bundle.class
                );
                
                activityResult = (ActivityResult) execStartActivity.invoke(mBase,
                        who,
                        contextThread,
                        token,
                        target,
                        intent,
                        requestCode,
                        options
                );
                
            } catch (Exception e) {
                e.printStackTrace();
            }
            
            return activityResult;
            
        }
        
        private ActivityResult realExecStartActivity1(Context who, IBinder contextThread, IBinder token, Activity target,
                Intent intent, int requestCode, Bundle options) {
            ActivityResult activityResult = null;
            try {
                Class c = mBase.getClass();
                Method execStartActivity = c.getDeclaredMethod("execStartActivity",
                        Context.class,
                        IBinder.class,
                        IBinder.class,
                        Activity.class,
                        Intent.class,
                        int.class,
                        Bundle.class
                );
                
                activityResult = (ActivityResult) execStartActivity.invoke(mBase,
                        who,
                        contextThread,
                        token,
                        target,
                        intent,
                        requestCode,
                        options
                );
                
            } catch (Exception e) {
                e.printStackTrace();
            }
            
            return activityResult;
            
        }
        
        @Override
        public Activity newActivity(ClassLoader cl, String className, Intent intent)
                throws InstantiationException, IllegalAccessException, ClassNotFoundException {
            //step2
            //在這里實(shí)例化插件的activity
            return super.newActivity(cl, className, intent);
        }


    // on Applicaiton
    
class App : Application(){

    override fun onCreate() {
        super.onCreate()
        HookUtil.hookInstrumentation()
    }
    
}

在MainActivity中通過intent啟動一個TestActivity队秩,運(yùn)行結(jié)果:

2019-03-23 15:36:09.820 6537-6537/com.example.simpleapp D/Hook: hook init success
2019-03-23 15:36:13.068 6537-6537/com.example.simpleapp D/Hook: TestInstrumentationWrapper hook 1

此時我們已經(jīng)hook住了startActivity過程笑旺,那么我可以在宿主中占坑一個ProxyActivity,在啟動插件activity的過程中馍资,重定向至ProxyActivity達(dá)到偷梁換柱的目的筒主。

step2:
有去有回,經(jīng)過上面已經(jīng)可以做到將插件的activity換了個皮變成宿主中的ProxyActivity,但是怎么將這個ProxyActivity換回來呢?

這涉及app啟動流程乌妙,主要是本地app進(jìn)程(ActivityThread)和系統(tǒng)SystemServer進(jìn)程(ActivityManagerService)進(jìn)行binder通信的過程,有興趣的看下之前寫過的 一篇文章 分析app創(chuàng)建流程

現(xiàn)在我們只需要知道使兔,Context.startActivity()最終會來到
ActivityStackSupervisor.realStartActivityLocked()


  final boolean realStartActivityLocked(ActivityRecord r, ProcessRecord app,
            boolean andResume, boolean checkConfig) throws RemoteException {
    
    ...
     app.thread.scheduleLaunchActivity(new Intent(r.intent), r.appToken,
                        System.identityHashCode(r), r.info,
                        // TODO: Have this take the merged configuration instead of separate global
                        // and override configs.
                        mergedConfiguration.getGlobalConfiguration(),
                        mergedConfiguration.getOverrideConfiguration(), r.compat,
                        r.launchedFromPackage, task.voiceInteractor, app.repProcState, r.icicle,
                        r.persistentState, results, newIntents, !andResume,
                        mService.isNextTransitionForward(), profilerInfo);           
 
 
 }

而app.thread是ApplicationThread,它是一個ActivityThread的內(nèi)部類藤韵,可以理解為在ActivityManagerService這一側(cè)的ActivityThread代理對象虐沥,主要是通過binder與遠(yuǎn)端(app進(jìn)程)進(jìn)行調(diào)用,接著我們分析

  public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident,
                ActivityInfo info, Configuration curConfig, Configuration overrideConfig,
                CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor,
                int procState, Bundle state, PersistableBundle persistentState,
                List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents,
                boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) {

            updateProcessState(procState, false);

            ActivityClientRecord r = new ActivityClientRecord();

            r.token = token;
            r.ident = ident;
            r.intent = intent;
            r.referrer = referrer;
            r.voiceInteractor = voiceInteractor;
            r.activityInfo = info;
            r.compatInfo = compatInfo;
            r.state = state;
            r.persistentState = persistentState;

            r.pendingResults = pendingResults;
            r.pendingIntents = pendingNewIntents;

            r.startsNotResumed = notResumed;
            r.isForward = isForward;

            r.profilerInfo = profilerInfo;

            r.overrideConfig = overrideConfig;
            updatePendingConfiguration(curConfig);

            sendMessage(H.LAUNCH_ACTIVITY, r);
        }

主要是通過mH這個handler發(fā)送消息然后進(jìn)行處理泽艘,最終又會來到performLaunchActivity這個方法里面:


 private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        ...
        ...
        Activity activity = null;
        try {
            java.lang.ClassLoader cl = appContext.getClassLoader();
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
            StrictMode.incrementExpectedActivityCount(activity.getClass());
            r.intent.setExtrasClassLoader(cl);
            r.intent.prepareToEnterProcess();
            if (r.state != null) {
                r.state.setClassLoader(cl);
            }
        } catch (Exception e) {
            if (!mInstrumentation.onException(activity, e)) {
                throw new RuntimeException(
                    "Unable to instantiate activity " + component
                    + ": " + e.toString(), e);
            }
        }

        ...
        ...


        return activity;
    }


就是利用Instrumentation.newActivity()方法通過反射調(diào)用實(shí)例化我們插件中的activity欲险,從而將插件中的activity交給系統(tǒng)托管。

而Small正是利用了這一點(diǎn)匹涮,核心原理大概講完了.而其余組件的

總結(jié)

當(dāng)然這里只是對核心原理進(jìn)行了一下簡略的描述天试,要想達(dá)到生產(chǎn)需求還要許多工作要做。

例如:正確區(qū)分宿主的activity和插件的activity然低,當(dāng)某個activity處在宿主中且已注冊時喜每,直接跳過插件化的步驟,交給系統(tǒng)處理即可雳攘。

再例如带兜,當(dāng)我們的需求需要在start多個相同的launchMode的activity時,需要在宿主占坑多少個這樣的proxy activity来农?

像上文提到的mH這個handler萧诫,我們其實(shí)可以hook住這個mH然后所有的分發(fā)事件兔跌。插件化的實(shí)現(xiàn)有很多種虚青,但無非都是在App創(chuàng)建流程中在ActivityThread绸栅,Instrumentation,ActivityManagerNative(AMS的本地代理對象)做文章,所以理解App創(chuàng)建流程對于插件化思想至關(guān)重要繁莹,說不定可以找到某個新奇的突破點(diǎn)進(jìn)行插件化檩互。

值得一提的是Android9開始對反射進(jìn)行限制,像反射調(diào)用ActivityThread里的currentActivityThread(),mH都被標(biāo)為淺灰名單咨演。
可能在日后的版本中插件化思想將不能使用了闸昨。。薄风。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末饵较,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子遭赂,更是在濱河造成了極大的恐慌循诉,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件撇他,死亡現(xiàn)場離奇詭異茄猫,居然都是意外死亡狈蚤,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進(jìn)店門划纽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來脆侮,“玉大人,你說我怎么就攤上這事勇劣【副埽” “怎么了?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵芭毙,是天一觀的道長筋蓖。 經(jīng)常有香客問我卸耘,道長退敦,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任蚣抗,我火速辦了婚禮侈百,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘翰铡。我一直安慰自己钝域,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布锭魔。 她就那樣靜靜地躺著例证,像睡著了一般。 火紅的嫁衣襯著肌膚如雪迷捧。 梳的紋絲不亂的頭發(fā)上织咧,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天,我揣著相機(jī)與錄音漠秋,去河邊找鬼笙蒙。 笑死,一個胖子當(dāng)著我的面吹牛庆锦,可吹牛的內(nèi)容都是我干的捅位。 我是一名探鬼主播,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼搂抒,長吁一口氣:“原來是場噩夢啊……” “哼艇搀!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起求晶,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤焰雕,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后誉帅,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體淀散,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡右莱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了档插。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片慢蜓。...
    茶點(diǎn)故事閱讀 40,675評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖郭膛,靈堂內(nèi)的尸體忽然破棺而出晨抡,到底是詐尸還是另有隱情,我是刑警寧澤则剃,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布耘柱,位于F島的核電站,受9級特大地震影響棍现,放射性物質(zhì)發(fā)生泄漏调煎。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一己肮、第九天 我趴在偏房一處隱蔽的房頂上張望士袄。 院中可真熱鬧,春花似錦谎僻、人聲如沸娄柳。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽赤拒。三九已至,卻和暖如春诱鞠,著一層夾襖步出監(jiān)牢的瞬間挎挖,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工般甲, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留肋乍,地道東北人。 一個月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓敷存,卻偏偏與公主長得像墓造,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子锚烦,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評論 2 360

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