Android中的ClassLoader分析

前言

這篇文章主要是講解Android中的ClassLoader


Dalvik VM

Dalvik是Google公司自己設計用于Android平臺的Java虛擬機。它可以支持已轉換為.dex(即Dalvik Executable)格式的Java應用程序的運行同衣,.dex格式是專為Dalvik設計的一種壓縮格式竟块,可以減少整體文件尺寸,提高I/o操作的類查找速度所以適合內存和處理器速度有限的系統(tǒng)耐齐。

關于Dalvik更多內容參考這篇文章:Dalvik概述
說的直白一點就是針對Android而生的JVM的升級浪秘。他與JVM的不同:

clipboard.png

第一點不用解釋
第二點后面會詳細講解
第三點:JVM只存在一個前弯,DVM 可以存在多個,某一個引用程序掛掉以后不會影響的其他程序秫逝,保證程序的穩(wěn)定性恕出。
第四點:基于棧 表示方法的調用時在棧中完成的 寄存器運行更快


ART與Dalvik的不同
ART模式英文全稱為:Android runtime,谷歌Android 4.4系統(tǒng)新增的一種應用運行模式违帆,與傳統(tǒng)的Dalvik模式不同浙巫,ART模式可以實現(xiàn)更為流暢的安卓系統(tǒng)體驗,對于大家來說刷后,只要明白ART模式可讓系統(tǒng)體驗更加流暢的畴,不過只有在安卓4.4以上系統(tǒng)中采用此功能。這里只做簡單介紹尝胆。

clipboard.png

現(xiàn)在市面的手機基本都是ART模式了丧裁。


Android中的ClassLoader

JVM的類加載器是將字節(jié)碼文件通過讀取后加載到JVM運行時數(shù)據區(qū)。而Android中的ClassLoader的作用是一樣的含衔,只不過是加載到Dalvik中煎娇。

  • Android中的ClassLoader有哪些?

Android中的ClassLoader由一下4個類組成贪染。

圖片.png

1:加載Framework層字節(jié)碼文件
2:加載已經安裝到系統(tǒng)中APK文件中的字節(jié)碼文件(sdk中的文件)
3:加載指定目錄中的字節(jié)碼文件(如lib引入的jar中的文件等)
4:是2.3的父類

從上面我們知道一個應用的運行必須要使用1和2缓呛,2中類加載器。

  • Android中的ClassLoader的特點以及作用杭隙?

前面提到了委派模式哟绊。在Android中叫雙親代理模式。2者的作用與思想是一樣的 痰憎。

特點:如果字節(jié)碼在整個加載器類樹中被一個加載器加載過 那么在整個系統(tǒng)生命周期中中都不會在重新加載 提高效率
作用:類加載的共享功能與隔離功能都是基于雙親代理模式總結而來的票髓。
共享功能:一些底層(如Framework層)的類被頂層類加載器加載過那么以后在任何地方用到就不用再加載。
隔離功能:不同繼承路線類加載器中铣耘,加載的類都是一定是不相同的洽沟,避免用戶寫一些可見的類冒充核心的類庫。如:Object.lang.String類在程序啟動之前就被系統(tǒng)加載了涡拘。如果我們自己寫的String會將系統(tǒng)的String類替換的話玲躯,將會出現(xiàn)嚴重的安全問題。

雙親代理模式:
Android中的classLoader當加載一個字節(jié)碼文件的時候首先會詢問當前加載器是否已經加載過此類 如果已經加載 那么直接返回不再重復加載鳄乏。如果沒有加載,他會查詢當前加載器的Parent類是否已經加載過此字節(jié)碼棘利。如果加載過直接返回Parent類加載的字節(jié)碼文件橱野。如果(所有繼承鏈都沒有加載過)那么就由子加載器加載并返回。

同一個類指的是相同的類名善玫,包名水援,已經是同一個類加載器加載的密强。


Android中的ClassLoader源碼講解

從上面我們知道一個應用的運行必須要使用BootClassLoader和PathClassLoader,2種類加載器蜗元。下面我們新建個Android項目來運行下或渤。代碼如下:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ClassLoader classLoader = getClassLoader();
        Log.e("ggxiaozhi", "classLoader: "+ classLoader);
        if (classLoader.getParent()!=null){
            classLoader=classLoader.getParent();
            Log.e("ggxiaozhi", "classLoader-Parent: "+ classLoader);
        }
    }
}

打印結果:

01-12 06:25:26.880 1842-1842/? E/ggxiaozhi: classLoader: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.example.ggxiaozhi.hotfix-1/base.apk"],nativeLibraryDirectories=[/data/app/com.example.ggxiaozhi.hotfix-1/lib/x86, /vendor/lib, /system/lib]]]
01-12 06:25:26.880 1842-1842/? E/ggxiaozhi: classLoader-Parent: java.lang.BootClassLoader@665444a

從打印結果也可以看到確實是這樣的。下面我們進入源碼分析下奕扣。

 protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded   (1)
            Class c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false); (2)
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name); (3)

                    // this is the defining class loader; record the stats
                }
            }
            return c;
    }

源碼也比較簡單:

  1. (1)首先查找我們的ClassLoader是否加載過我們的當前的class文件薪鹦。
  2. (2)如果沒有找到就查找他的分類有沒有加載過。
  3. (3)如果父類也沒有找到過就去通過findClass(name);去加載這個類文件惯豆。

點進去findClass()這個方法發(fā)現(xiàn)池磁,這是一個空實現(xiàn)說明他的子類實現(xiàn)了這個方法。而他的子類正上上面我們提到的4種類加載器楷兽。由于我們在實際中Framework層的加載器我們接觸不到地熄,所以重點分下其他三種類加載器。由于這幾個方法我們都看不到所有我們通過源碼網查去查詢芯杀。
源碼地址
使用教程文章

  • DexClassLoader
1/**
22 * A class loader that loads classes from {@code .jar} and {@code .apk} files
23 * containing a {@code classes.dex} entry. This can be used to execute code not
24 * installed as part of an application.
25
36 public class DexClassLoader extends BaseDexClassLoader {
55    public DexClassLoader(String dexPath, String optimizedDirectory,
56            String librarySearchPath, ClassLoader parent) {
57        super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
58    }
59}

可以看到它里面就一個構造方法端考,通過類的注釋可以簡單理解為它是加載來自.jar和.apk本身的class文件路徑,也可以用來執(zhí)行不作為應用程序的一部分安裝的代碼揭厚。(所以它也是我們后面要講得動態(tài)更新加載的核心加載器)
這里面的參數(shù)含義分別為:

  • dexPath:要加載的指定文件下的dex文件路徑
  • optimizedDirectory :這是一個copy路徑跛梗。可以理解應用在安裝時棋弥,先將dex文件copy到應用的內部路徑核偿,待需要加載dex文件時去應用內部路徑找到dex文件去加載。(中間還會做一些優(yōu)化)顽染。這個路徑是應用的內部路徑漾岳,在DexClassLoader下這個參數(shù)一定不能為空。
  • librarySearchPath 加載native相關dex文件粉寞。
  • ClassLoader 父類加載器
  • PathClassLoader
public class PathClassLoader extends BaseDexClassLoader {

37    public PathClassLoader(String dexPath, ClassLoader parent) {
38        super(dexPath, null, null, parent);
39    }
40

63    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
64        super(dexPath, null, librarySearchPath, parent);
65    }
66}

從類的注解上來看這個加載器是Android使用作為它的系統(tǒng)類加載器和它的應用程序類加載器尼荆。也就是加載Android項目工程中的類文件加載器。參數(shù)和上面一樣唧垦,正是因為缺少optimizedDirectory參數(shù)所以它只能加載項目本省的dex文件中的類

  • BaseDexClassLoader

BaseDexClassLoader是上面2個類加載器的父類捅儒。由于上面2個類加載器都沒有具體的邏輯方法。還記上面我們在查找ClassLoader時知道加載類的方法是findClass(name).由于這兩個雷都沒有實現(xiàn)這個方法振亮,那么一定就是在他們的父類BaseDexClassLoader中實現(xiàn)的巧还。下面看下他的源碼:

29 public class BaseDexClassLoader extends ClassLoader {
30    private final DexPathList pathList;
31
45    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
46            String librarySearchPath, ClassLoader parent) {
47        super(parent);
48        this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
49    }
50
51    @Override
52    protected Class<?> findClass(String name) throws ClassNotFoundException {
53        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
54        Class c = pathList.findClass(name, suppressedExceptions);
55        if (c == null) {
56            ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on    path: " + pathList);
57            for (Throwable t : suppressedExceptions) {
58                cnfe.addSuppressed(t);
59            }
60            throw cnfe;
61        }
62        return c;
63    }

在這個類中找到了上面我們在ClassLoader中未實現(xiàn)的findClass方法。發(fā)現(xiàn)他是調用了DexPathList類中的方法坊秸,并且這個類是在構造方法中已經初始化并肩參數(shù)傳入其中了麸祷。所以我們在這個類中尋找一下DexPathList#findClass():

 final class DexPathList {
51    private static final String DEX_SUFFIX = ".dex";
52    private static final String zipSeparator = "!/";
53
54    /** class definition context */
55    private final ClassLoader definingContext;
56
57    /**
58     * List of dex/resource (class path) elements.
59     * Should be called pathElements, but the Facebook app uses reflection
60     * to modify 'dexElements' (http://b/7726934).
61     */
62    private Element[] dexElements;
63
64    /** List of native library path elements. */
65    private final Element[] nativeLibraryPathElements;
66
67    /** List of application native library directories. */
68    private final List<File> nativeLibraryDirectories;
69
70    /** List of system native library directories. */
71    private final List<File> systemNativeLibraryDirectories;
72
73    /**
74     * Exceptions thrown during creation of the dexElements list.
75     */
76    private IOException[] dexElementsSuppressedExceptions;
77
78   
96    public DexPathList(ClassLoader definingContext, String dexPath,
97            String librarySearchPath, File optimizedDirectory) {
98         ...

122        this.definingContext = definingContext;
123
124        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
125        // save dexPath for BaseDexClassLoader
126        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
127                                           suppressedExceptions, definingContext);
128
129 
140        this.systemNativeLibraryDirectories =
141                splitPaths(System.getProperty("java.library.path"), true);
142        List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
143        allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
144
145        this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories,
146                                                          suppressedExceptions,
147                                                          definingContext);
148
149        if (suppressedExceptions.size() > 0) {
150            this.dexElementsSuppressedExceptions =
151                suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
152        } else {
153            dexElementsSuppressedExceptions = null;
154        }
155    }
156
157   
        ...
        
         private static Element[] makePathElements(List<File> files, File optimizedDirectory,
280                                              List<IOException> suppressedExceptions) {
281        return makeElements(files, optimizedDirectory, suppressedExceptions, false, null);
282    }
283
       //這個方法的作用就是將指定路徑class文件轉化成dexfile(dex文件) 同時存在Element[]數(shù)組中 //最后在findClass文件中使用
284    private static Element[] makeElements(List<File> files, File optimizedDirectory,
285                                          List<IOException> suppressedExceptions,
286                                          boolean ignoreDexFiles,
287                                          ClassLoader loader) {
        //創(chuàng)建Element數(shù)組
288        Element[] elements = new Element[files.size()];
289        int elementsPos = 0;
290        /*
291         * Open all files and load the (direct or contained) dex files
292         * up front.
293         */
294        for (File file : files) {//遍歷dex文件集合
295            File zip = null;
296            File dir = new File("");
            //dex文件對應的java類
297            DexFile dex = null;
               //獲取文件路徑
298            String path = file.getPath();
               //獲取文件名
299            String name = file.getName();
300
                //path是文件夾繼續(xù)往下遍歷
301            if (path.contains(zipSeparator)) {
302                String split[] = path.split(zipSeparator, 2);
303                zip = new File(split[0]);
304                dir = new File(split[1]);
305            } else if (file.isDirectory()) {
306                // We support directories for looking up resources and native libraries.
307                // Looking up resources in directories is useful for running libcore tests.
308                elements[elementsPos++] = new Element(file, true, null, null);
309            } else if (file.isFile()) {//如果是文件 最后都會調用loadDexFile()f方法創(chuàng)建dex文件
310                if (!ignoreDexFiles && name.endsWith(DEX_SUFFIX)) {//這個文件是不是以.dex文件為后綴的
311                    // Raw dex file (not inside a zip/jar).
312                    try {
                           //如果是就創(chuàng)建一個dex文件
313                        dex = loadDexFile(file, optimizedDirectory, loader, elements);
314                    } catch (IOException suppressed) {
315                        System.logE("Unable to load dex file: " + file, suppressed);
316                        suppressedExceptions.add(suppressed);
317                    }
318                } else {//如果這個文件值zip格式
319                    zip = file;
320
321                    if (!ignoreDexFiles) {
322                        try {
323                            dex = loadDexFile(file, optimizedDirectory, loader, elements);
324                        } catch (IOException suppressed) {
325                            /*
326                             * IOException might get thrown "legitimately" by the DexFile constructor if
327                             * the zip file turns out to be resource-only (that is, no classes.dex file
328                             * in it).
329                             * Let dex == null and hang on to the exception to add to the tea-leaves for
330                             * when findClass returns null.
331                             */
332                            suppressedExceptions.add(suppressed);
333                        }
334                    }
335                }
336            } else {
337                System.logW("ClassLoader referenced unknown path: " + file);
338            }
339
340            if ((zip != null) || (dex != null)) {
341                elements[elementsPos++] = new Element(dir, false, zip, dex);
342            }
343        }
344        if (elementsPos != elements.length) {
345            elements = Arrays.copyOf(elements, elementsPos);
346        }
347        return elements;
348    }

        /**
351     * Constructs a {@code DexFile} instance, as appropriate depending on whether
352     * {@code optimizedDirectory} is {@code null}. An application image file may be associated with
353     * the {@code loader} if it is not null.
354     */
355    private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader,
356                                       Element[] elements)
357            throws IOException {
358        if (optimizedDirectory == null) {//這個路徑是空就說明一個dex文件沒有 我們就要創(chuàng)建一個dex文件
359            return new DexFile(file, loader, elements);
360        } else {//否自會通過解壓等處理最后得到DexFile
361            String optimizedPath = optimizedPathFor(file, optimizedDirectory);
362            return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);
363        }
364    }
413    public Class findClass(String name, List<Throwable> suppressed) {
414        for (Element element : dexElements) {
415            DexFile dex = element.dexFile;
416
417            if (dex != null) {
418                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
419                if (clazz != null) {
420                    return clazz;
421                }
422            }
423        }
424        if (dexElementsSuppressedExceptions != null) {
425            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
426        }
427        return null;
428    }
429
             ....
448
489
490    /**
491     * Element of the dex/resource/native library path
492     */
493    /*package*/ static class Element {
494        private final File dir;
495        private final boolean isDirectory;
496        private final File zip;
497        private final DexFile dexFile;
498
499        private ClassPathURLStreamHandler urlHandler;
500        private boolean initialized;
501
502        public Element(File dir, boolean isDirectory, File zip, DexFile dexFile) {
503            this.dir = dir;
504            this.isDirectory = isDirectory;
505            this.zip = zip;
506            this.dexFile = dexFile;
507        }
508
509        @Override public String toString() {
510            if (isDirectory) {
511                return "directory \"" + dir + "\"";
512            } else if (zip != null) {
513                return "zip file \"" + zip + "\"" +
514                       (dir != null && !dir.getPath().isEmpty() ? ", dir \"" + dir + "\"" : "");
515            } else {
516                return "dex file \"" + dexFile + "\"";
517            }
518        }
566
567       ...
590    }
591}
592

這個類比較長,這里簡單講解下:首先定義一些常量來規(guī)定加載.dex文件格式褒搔,同時定義了Element屬性阶牍。在構造方法中先對一些異常處理并初始化一些常量喷面。下面只看我們上步跟蹤的方法findClass。發(fā)現(xiàn)這個方法先遍歷了Element這個數(shù)組走孽,而這個數(shù)組是通過在構造方法中調用makeElements()方法初始化,然后調用DexFile#loadClassBinaryName()方法惧辈,說明這個類也不是最終加載類的地方。不過在繼續(xù)跟蹤之前我們先對Element有個理解磕瓷。其實他就是DexPathList中的一個內部類盒齿,誰對dex文件的包裝,將路徑與最終加載的類DexFile封裝在一起生宛,并進行一些字符串的拼湊县昂。接著我們在進入DexFile#loadClassBinaryName()方法:

public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
289        return defineClass(name, loader, mCookie, this, suppressed);
290    }
291
292    private static Class defineClass(String name, ClassLoader loader, Object cookie,
293                                     DexFile dexFile, List<Throwable> suppressed) {
294        Class result = null;
295        try {
296            result = defineClassNative(name, loader, cookie, dexFile);
297        } catch (NoClassDefFoundError e) {
298            if (suppressed != null) {
299                suppressed.add(e);
300            }
301        } catch (ClassNotFoundException e) {
302            if (suppressed != null) {
303                suppressed.add(e);
304            }
305        }
306        return result;
307    }
387      private static native Class defineClassNative(String name, ClassLoader loader, Object cookie,
388                                                  DexFile dexFile)
389            throws ClassNotFoundException, NoClassDefFoundError;

這里直接看loadClassBinaryName方法他調用了defineClass方法,最后調用defineClassNative方法陷舅。defineClassNative()這個方法是native倒彰,是用C/C++ 實現(xiàn)的,往后我們就無法查看了莱睁。不過經過前面分析待讳,最后native方法是大概就是通過C/C++
根據指定傳入類的name去查找dex文件中對應的class文件相關信息數(shù)據,然后將dex文件的中的運行數(shù)據區(qū)中的數(shù)據拼成一個class字節(jié)碼返回仰剿。應用層使用创淡。


總結:
注意dex可以理解成把所有的class文件壓縮成了一個dex文件 dex對應轉化的是jar不是class

我的理解dex文件包含各個路徑的jar文件.zip文件 不管用沒有用到 等用到了采用類加載器去根據類名去加載這個類 信息然后最后通過native層在dex文件查找返回這個類的信息并返回。(這個整個串聯(lián)的流程我也好串聯(lián)起來南吮。待后期有更深的研究在完善這部分) 如果大家有相關的書籍推薦下琳彩。

Android中的類加載器流程。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末部凑,一起剝皮案震驚了整個濱河市露乏,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌涂邀,老刑警劉巖瘟仿,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異比勉,居然都是意外死亡劳较,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進店門浩聋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來观蜗,“玉大人,你說我怎么就攤上這事赡勘∩┍悖” “怎么了?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵闸与,是天一觀的道長毙替。 經常有香客問我,道長践樱,這世上最難降的妖魔是什么厂画? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮拷邢,結果婚禮上袱院,老公的妹妹穿的比我還像新娘。我一直安慰自己瞭稼,他們只是感情好忽洛,可當我...
    茶點故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著环肘,像睡著了一般欲虚。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上悔雹,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天复哆,我揣著相機與錄音,去河邊找鬼腌零。 笑死梯找,一個胖子當著我的面吹牛,可吹牛的內容都是我干的益涧。 我是一名探鬼主播锈锤,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼闲询!你這毒婦竟也來了久免?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤嘹裂,失蹤者是張志新(化名)和其女友劉穎妄壶,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體寄狼,經...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡丁寄,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了泊愧。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片伊磺。...
    茶點故事閱讀 39,902評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖删咱,靈堂內的尸體忽然破棺而出屑埋,到底是詐尸還是另有隱情,我是刑警寧澤痰滋,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布摘能,位于F島的核電站续崖,受9級特大地震影響,放射性物質發(fā)生泄漏团搞。R本人自食惡果不足惜严望,卻給世界環(huán)境...
    茶點故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望逻恐。 院中可真熱鬧像吻,春花似錦、人聲如沸复隆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽挽拂。三九已至惭每,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間轻局,已是汗流浹背洪鸭。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留仑扑,地道東北人览爵。 一個月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像镇饮,于是被迫代替她去往敵國和親蜓竹。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,843評論 2 354

推薦閱讀更多精彩內容