剖析ClassLoader深入熱修復(fù)原理

ClassLoader

我們知道一個java程序來是由多個class類組成的于样,我們在運行程序的過程中需要通過ClassLoaderclass類載入到JVM中才可以正常運行屡律。
而Android程序需要正常運行,也同樣需要有ClassLoader機制將class類加載到Android 的 Dalvik(5.0之前版本)/ART(5.0增加的)中浅萧,只不過它和java中的ClassLoader不一樣在于Android的apk打包逐沙,是將class文件打包成一個或者多個 dex文件(由于Android 65k問題,使用 MultiDex 就會生成多個 dex 文件)洼畅,再由BaseDexClassLoader來進行處理吩案。
在安裝apk的過程中,會有一個驗證優(yōu)化dex的機制帝簇,叫做DexOpt徘郭,這個過程會生成一個odex文件( odex 文件也屬于dex文件),即Optimised Dex丧肴。執(zhí)行odex的效率會比直接執(zhí)行dex文件的效率要高很多残揉。運行Apk的時候,直接加載odex文件闪湾,從而避免重復(fù)驗證和優(yōu)化冲甘,加快了Apk的響應(yīng)時間。
注意:Dalvik/ART 無法像 JVM 那樣直接加載 class 文件和 jar 文件中的 class途样,需要通過工具來優(yōu)化轉(zhuǎn)換成 Dalvik byte code 才行江醇,只能通過 dex 或者包含 dex 的jar、apk 文件來加載

dex生成方法

你可以直接在編譯工程后何暇,在app/build/intermediates/classes中拿到你需要的class陶夜,然后再通過dx命令生成dex文件

dx --dex --output=/Users/test/test.dex multi/shengyuan/com/mytestdemo/test.class

雙親委派機制

類加載器雙親委派模型的工作過程是:如果一個類加載器收到一個類加載的請求,它首先將這個請求委派給父類加載器去完成裆站,每一個層次類加載器都是如此条辟,則所有的類加載請求都會傳送到頂層的啟動類加載器,只有父加載器無法完成這個加載請求(即它的搜索范圍中沒有找到所要的類)宏胯,子類才嘗試加載羽嫡。


圖一.png

雙親委派模式優(yōu)勢

采用雙親委派模式的是好處是Java類隨著它的類加載器一起具備了一種帶有優(yōu)先級的層次關(guān)系,通過這種層級關(guān)可以避免類的重復(fù)加載肩袍,當父親已經(jīng)加載了該類時杭棵,就沒有必要子ClassLoader再加載一次。其次是考慮到安全因素氛赐,java核心api中定義類型不會被隨意替換魂爪,假設(shè)通過網(wǎng)絡(luò)傳遞一個名為java.lang.Integer的類先舷,通過雙親委托模式傳遞到啟動類加載器,而啟動類加載器在核心Java API發(fā)現(xiàn)這個名字的類滓侍,發(fā)現(xiàn)該類已被加載蒋川,并不會重新加載網(wǎng)絡(luò)傳遞的過來的java.lang.Integer,而直接返回已加載過的Integer.class撩笆,這樣便可以防止核心API庫被隨意篡改捺球。

Android中的ClassLoader根據(jù)用途可分為一下幾種:
  • BootClassLoader:主要用于加載系統(tǒng)的類,包括javaandroid系統(tǒng)的類庫浇衬,和JVM中不同懒构,BootClassLoader是ClassLoader內(nèi)部類,是由Java實現(xiàn)的耘擂,它也是所有系統(tǒng)ClassLoader的父ClassLoader
  • PathClassLoader:用于加載Android系統(tǒng)類和開發(fā)編寫應(yīng)用的類胆剧,只能加載已經(jīng)安裝應(yīng)用的 dexapk 文件,也是getSystemClassLoader的返回對象
  • DexClassLoader:可以用于加載任意路徑的zip醉冤、dex秩霍、jar或者apk文件,也是進行安卓動態(tài)加載的基礎(chǔ)

DexClassLoader類

public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
    }
}

PathClassLoader類

public class PathClassLoader extends BaseDexClassLoader {

    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }

    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}

BaseDexClassLoader#BaseDexClassLoader方法

PathClassLoader蚁阳、DexClassLoader均繼承BaseDexClassLoader铃绒,所以其super方法均調(diào)用到了BaseDexClassLoader構(gòu)造方法

參數(shù)詳解:
  • dexPath
    待解析文件所在的全路徑,classloader將在該路徑中指定的dex文件尋找指定目標類

  • optimzedDirectory
    優(yōu)化路徑螺捐,指的是虛擬機對于apk中的dex文件進行優(yōu)化后生成文件存放的路徑颠悬,如dalvik虛擬機生成的ODEX文件路徑和ART虛擬機生成的OAT文件路徑。
    這個路徑必須是當前app的內(nèi)部存儲路徑定血,Google認為如果放在公有的路徑下赔癌,存在被惡意注入的危險
    注意:PathClassLoader沒有將optimizedDirectory置為Null,也就是沒設(shè)置優(yōu)化后的存放路徑。其實optimizedDirectory為null時的默認路徑就是/data/dalvik-cache 目錄澜沟。 PathClassLoader是用來加載Android系統(tǒng)類和應(yīng)用的類灾票,并且不建議開發(fā)者使用。

  • libraryPath
    指定native(即so加載路徑)層代碼存放路徑

  • parent
    當前ClassLoader的parent茫虽,和javaclassloaderparent含義一樣

public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
        super(parent);
        this.originalPath = dexPath;
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

ClassLoader#loadClass方法

通過該方法你就能發(fā)現(xiàn)雙親委派機制的妙處了

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // 1 通過調(diào)用c層findLoadedClass檢查該類是否被加載過刊苍,若加載過則返回class對象(緩存機制)
            Class c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        //2 各種類型的類加載器在構(gòu)造時都會傳入一個parent類加載器
                        //2 若parent類不為空,則調(diào)用parent類的loadClass方法
                        c = parent.loadClass(name, false);
                    } else {
                        //3 查閱了PathClassLoader濒析、DexClassLoader并沒有重寫該方法正什,默認是返回null
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }
                if (c == null) {
                    //4  如果父ClassLoader不能加載該類才由自己去加載,這個方法從本ClassLoader的搜索路徑中查找該類
                    long t1 = System.nanoTime();
                    c = findClass(name);
                }
            }
            return c;
    }

BaseDexClassLoader#findClass方法

DexClassLoaderPathClassLoader通過繼承BaseDexClassLoader從而使用其父類findClass方法号杏,在ClassLoader#loadClass方法中第3步進入

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
}

DexPathList#findClass方法

ClassLoader#loadClass方法中我們可以知道埠忘,當走到第2步即會走到如下方法,通過對已構(gòu)建好的dexElements進行遍歷馒索,通過dex.loadClassBinaryName方法load對應(yīng)的class類莹妒,所以這里是一個熱修復(fù)的點,你可以將需要熱修復(fù)的dex文件插入到dexElements數(shù)組前面绰上,這樣遍歷的時候查到你最新插入的則返回旨怠,從而實現(xiàn)動態(tài)替換有問題類

public Class findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            DexFile dex = element.dexFile;

            if (dex != null) {
                 //調(diào)用到c層defineClassNative方法進行查找
                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

DexPathList#makeElements

BaseDexClassLoader的構(gòu)造方法中對DexPathList進行實例化,在DexPathList構(gòu)造方法中調(diào)用makeElements生成dexElements數(shù)組蜈块,首先會根據(jù)傳入的dexPath鉴腻,生成一個file類型的list容器,然后傳入后進行遍歷加載百揭,通過調(diào)用DexFile中的loadDexFiledexFile文件進行加載

private static Element[] makeElements(List<File> files, File optimizedDirectory,
                                          List<IOException> suppressedExceptions,
                                          boolean ignoreDexFiles,
                                          ClassLoader loader) {
        Element[] elements = new Element[files.size()];
        int elementsPos = 0;
        //遍歷所有文件爽哎,并提取 dex 文件。
        for (File file : files) {
            File zip = null;
            File dir = new File("");
            DexFile dex = null;
            String path = file.getPath();
            String name = file.getName();

            if (path.contains(zipSeparator)) {
                String split[] = path.split(zipSeparator, 2);
                zip = new File(split[0]);
                dir = new File(split[1]);
            } else if (file.isDirectory()) {
                //為文件夾時器一,直接存儲
                elements[elementsPos++] = new Element(file, true, null, null);
            } else if (file.isFile()) {
                if (!ignoreDexFiles && name.endsWith(DEX_SUFFIX)) {
                 // loadDexFile 的作用是:根據(jù) file 獲取對應(yīng)的 DexFile 對象课锌。
                    try {
                        dex = loadDexFile(file, optimizedDirectory, loader, elements);
                    } catch (IOException suppressed) {
                        System.logE("Unable to load dex file: " + file, suppressed);
                        suppressedExceptions.add(suppressed);
                    }
                } else {
              // 非 dex 文件,那么 zip 表示包含 dex 文件的壓縮文件祈秕,如 .apk渺贤,.jar 文件等
                    zip = file;
                    if (!ignoreDexFiles) {
                        try {
                            dex = loadDexFile(file, optimizedDirectory, loader, elements);
                        } catch (IOException suppressed) {
                            suppressedExceptions.add(suppressed);
                        }
                    }
                }
            } else {
                System.logW("ClassLoader referenced unknown path: " + file);
            }

            if ((zip != null) || (dex != null)) {
                elements[elementsPos++] = new Element(dir, false, zip, dex);
            }
        }
        if (elementsPos != elements.length) {
            elements = Arrays.copyOf(elements, elementsPos);
        }
        return elements;
    }

插件化

看到這里,你應(yīng)該大概理解了classloader加載流程请毛,其實java這層的classloader代碼量并不多志鞍,主要集中在c層,但是我們在java層進行hook便可實現(xiàn)熱修復(fù)方仿。
結(jié)合網(wǎng)上的資料及源碼的閱讀一共有三種方案

方案1:向dexElements進行插入新的dex(目前最常見的方式)

從上面的ClassLoader#loadClass方法你就會知道固棚,初始化的時候會進入BaseDexClassLoader#findClass方法中通過遍歷dexElements進行查找dex文件,因為dexElements是一個數(shù)組仙蚜,所以我們可以通過反射的形式此洲,將需要熱修復(fù)的dex文件插入到數(shù)組首部,這樣遍歷數(shù)組的時候就會優(yōu)先讀取你插入的dex鳍征,從而實現(xiàn)熱修復(fù)黍翎。

圖二.jpeg

DexClassLoader不是允許你加載外部dex嗎?用DexClassLoader#loadClass不就行了

我們知道DexClassLoader是允許你加載外部dex文件的艳丛,所以網(wǎng)上有一些例子介紹通過DexClassLoader#loadClass可以加載到你的dex文件中的方法匣掸,那么有一些網(wǎng)友就會有疑問,我直接通過調(diào)用DexClassLoader#loadClass去獲取我傳入的外部dex文件中的class氮双,不就行了碰酝,這樣確實是可以的,但是它僅適用于新增的類戴差,而不能去替換舊的類送爸,因為通過上面的dexElements數(shù)組的生成以及委派雙親機制,你就會知道它的父類是先去把你應(yīng)用類組裝進來,當你調(diào)用DexClassLoaderloadClass時袭厂,是先委派父類去loadClass墨吓,如果查找不到才會到子類自行查找,也就是說應(yīng)用中本來就已經(jīng)存在B.class了纹磺,那么父類loadClass會直接返回帖烘,而你真正需要返回的其實是子類中的B.class,所以才說只適用于新增的類橄杨,你不通過一些手段修改源碼層秘症,是無法實現(xiàn)替換類的。

方案2:在ActivityThread中替換LoadedApk的mClassLoader對象

小編在開發(fā)MPlugin的時候式矫,使用了下面的方法乡摹,但發(fā)現(xiàn)當你插件apk中進行跳轉(zhuǎn)的下一個頁面的時候,若引了第三方的庫采转,會拋出無法載入該第三方庫控件異常聪廉。
實現(xiàn)代碼如下:

  public static void loadApkClassLoader(Context context,DexClassLoader dLoader){
        try{
            // 配置動態(tài)加載環(huán)境
            Object currentActivityThread = RefInvoke.invokeStaticMethod(
                    "android.app.ActivityThread", "currentActivityThread",
                    new Class[] {}, new Object[] {});//獲取主線程對象 http://blog.csdn.net/myarrow/article/details/14223493
            String packageName = context.getPackageName();//當前apk的包名
            ArrayMap mPackages = (ArrayMap) RefInvoke.getFieldOjbect(
                    "android.app.ActivityThread", currentActivityThread,
                    "mPackages");
            WeakReference wr = (WeakReference) mPackages.get(packageName);
            RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader",
                    wr.get(), dLoader);
        }catch(Exception e){
             e.printStackTrace();
        }
    }

方案3:通過自定義ClassLoader實現(xiàn)class攔截替換

我們知道PathClassLoader是加載已安裝的apkdex,那我們可以
PathClassLoaderBootClassLoader 之間插入一個 自定義的MyClassLoader氏义,而我們通過ClassLoader#loadClass方法中的第2步知道锄列,若parent不為空,會調(diào)用parent.loadClass方法惯悠,固我們可以在MyClassLoader中重寫loadClass方法邻邮,在這個里面做一個判斷去攔截替換掉我們需要修復(fù)的class

如何拿到我們需要修復(fù)的class呢克婶?

我當時首先想到的是通過DexClassLoader直接去loadClass來獲得需要熱修復(fù)的Class筒严,但是通過ClassLoader#loadClass方法分析,可以知道加載查找class的第1步是調(diào)用findLoadedClass情萤,這個方法主要作用是檢查該類是否被加載過鸭蛙,如果加載過則直接返回,所以如果你想通過DexClassLoader直接去loadClass來獲得你需要熱修復(fù)的Class筋岛,是不可能完成替換的(熱修復(fù))娶视,因為你調(diào)用DexClassLoader.loadClass已經(jīng)屬于首次加載了,那么意味著下次加載就直接在findLoadedClass方法中返回class了睁宰,是不會再往下走肪获,從而MyClassLoader#loadClass方法也不可能會被回調(diào),也就無法實現(xiàn)修復(fù)柒傻。
通過BaseDexClassLoader#findClass方法你就會知道孝赫,這個方法在父ClassLoader不能加載該類的時候才由自己去加載,我們可以通過這個方法來獲得我們的class红符,因為你調(diào)用這個方法的話青柄,是不會被緩存起來伐债。也就不存在ClassLoader#loadClass中的第1步就查找到就被返回。

圖三.jpeg

方案3代碼:

public class HookUtil {
    /**
     * 在 PathClassLoader 和 BootClassLoader 之間插入一個 自定義的MyClassLoader
     * @param classLoader
     * @param newParent
     */
    public static void injectParent(ClassLoader classLoader, ClassLoader newParent) {
        try {
            Field parentField = ClassLoader.class.getDeclaredField("parent");
            parentField.setAccessible(true);
            parentField.set(classLoader, newParent);
        } catch (IllegalArgumentException e) {
            throw new RuntimeException(e);
        } catch (NoSuchFieldException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    /**
     * 反射調(diào)用findClass方法獲取dex中的class類
     * @param context
     * @param dexPath
     * @param className
     */
    public static void hookFindClass(Context context,String dexPath,String className){
        DexClassLoader dexClassLoader = new DexClassLoader(dexPath, context.getDir("dex",context.MODE_PRIVATE).getAbsolutePath(),null, context.getClassLoader());
        try {
            Class<?> herosClass = dexClassLoader.getClass().getSuperclass();
            Method m1 = herosClass.getDeclaredMethod("findClass", String.class);
            m1.setAccessible(true);
            Class newClass = (Class) m1.invoke(dexClassLoader, className);
            ClassLoader pathClassLoader = MyApplication.getContext().getClassLoader();
            MyClassLoader myClassLoader = new MyClassLoader(pathClassLoader.getParent());
            myClassLoader.registerClass(className, newClass);
            injectParent(pathClassLoader, myClassLoader);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}
public class MyClassLoader extends ClassLoader {

    public Map<String,Class> myclassMap;

    public MyClassLoader(ClassLoader parent) {
        super(parent);
        myclassMap = new HashMap<>();
    }

    /**
     * 注冊類名以及對應(yīng)的類
     * @param className
     * @param myclass
     */
    public void registerClass(String className,Class myclass){
        myclassMap.put(className,myclass);
    }

    /**
     * 移除對應(yīng)的類
     * @param className
     */
    public void removeClass(String className){
        myclassMap.remove(className);
    }

    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        Class myclass = myclassMap.get(name);
        //重寫父類loadClass方法致开,實現(xiàn)攔截
        if(myclass!=null){
            return myclass;
        }else{
            return super.loadClass(name, resolve);
        }
    }
}

關(guān)于CLASS_ISPREVERIFIED標記

因為在 Dalvik虛擬機下峰锁,執(zhí)行 dexopt 時,會對類進行掃描喇喉,如果類里面所有直接依賴的類都在同一個 dex 文件中祖今,那么這個類就會被打上 CLASS_ISPREVERIFIED 標記,如果一個類有 CLASS_ISPREVERIFIED標記拣技,那么在熱修復(fù)時,它加載了其他 dex 文件中的類耍目,會報經(jīng)典的Class ref in pre-verified class resolved to unexpected implementation異常
通過源碼搜索并沒有找到CLASS_ISPREVERIFIED標記這個關(guān)鍵詞膏斤,通過在android7.0、8.0上進行熱修復(fù)邪驮,也沒有遇到這個異常莫辨,猜測這個問題只屬于android5.0以前(關(guān)于解決方法網(wǎng)上有很多,本文就不講述了)毅访,因為android5.0后新增了art沮榜。

最后

看到這里相信java層的ClassLoader機制你已經(jīng)熟悉得差不多了,相對于插件化而言你已經(jīng)前進了一步喻粹,但仍有一些問題需要去思考解決的蟆融,比如解決資源加載、混淆守呜、加殼等問題型酥,為了更好的完善熱修復(fù)機制,你也可以去閱讀下c層的邏輯查乒,盡管熱修復(fù)帶來了很多便利弥喉,但個人也并不是太認同熱修復(fù)的使用,畢竟是通過hook去修改源碼層玛迄,因為android的碎片化問題由境,很難確保你的hook能正常使用且不引發(fā)別的問題。
注意:本文源碼閱讀及案例測試是基于android7.0蓖议、8.0編寫的虏杰,案例經(jīng)過實測是可行的

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市拒担,隨后出現(xiàn)的幾起案子嘹屯,更是在濱河造成了極大的恐慌,老刑警劉巖从撼,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件州弟,死亡現(xiàn)場離奇詭異钧栖,居然都是意外死亡,警方通過查閱死者的電腦和手機婆翔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進店門拯杠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人啃奴,你說我怎么就攤上這事潭陪。” “怎么了最蕾?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵依溯,是天一觀的道長。 經(jīng)常有香客問我瘟则,道長黎炉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任醋拧,我火速辦了婚禮慷嗜,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘丹壕。我一直安慰自己庆械,他們只是感情好,可當我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布菌赖。 她就那樣靜靜地躺著缭乘,像睡著了一般。 火紅的嫁衣襯著肌膚如雪盏袄。 梳的紋絲不亂的頭發(fā)上忿峻,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天,我揣著相機與錄音辕羽,去河邊找鬼逛尚。 笑死,一個胖子當著我的面吹牛刁愿,可吹牛的內(nèi)容都是我干的绰寞。 我是一名探鬼主播,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼铣口,長吁一口氣:“原來是場噩夢啊……” “哼滤钱!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起脑题,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤件缸,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后叔遂,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體他炊,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡争剿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了痊末。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蚕苇。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖凿叠,靈堂內(nèi)的尸體忽然破棺而出涩笤,到底是詐尸還是另有隱情,我是刑警寧澤盒件,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布蹬碧,位于F島的核電站,受9級特大地震影響履恩,放射性物質(zhì)發(fā)生泄漏锰茉。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一切心、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧片吊,春花似錦绽昏、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至爷贫,卻和暖如春认然,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背漫萄。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工卷员, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人腾务。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓毕骡,卻偏偏與公主長得像,于是被迫代替她去往敵國和親岩瘦。 傳聞我的和親對象是個殘疾皇子未巫,可洞房花燭夜當晚...
    茶點故事閱讀 44,724評論 2 354

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