LRU分析

1.LinkedHashMap

圖片緩存技術(shù)一般使用Lru车摄,其實(shí)Lru就是使用了LinkedHashMap的按訪問順序遍歷;
LinkedHashMap是通過雙向鏈表實(shí)現(xiàn)hashMap遍歷有序静盅,其遍歷方式有2種,一種是按插入順序遍歷,默認(rèn)無參構(gòu)造方法就是按插入順序遍歷,比如:

linkedHashMap.put("a","a");
linkedHashMap.put("b","b");
linkedHashMap.put("c","c");
linkedHashMap.put("a","a");

此時(shí)如果按照遍歷順序獲取,獲取結(jié)果會(huì)是a->b->c乒验;
如下是LinkedHashMap的源碼,可以看出蒂阱,成員變量accessOrder就是控制遍歷順序锻全,如果為true就是按訪問順序遍歷;

public LinkedHashMap() {
        super();
        accessOrder = false;
}

public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
}

如果new LinkedHashMap(16,0.75f,true)方式初始化LinkedHashMap

linkedHashMap.put("a","a");
linkedHashMap.put("b","b");
linkedHashMap.put("c","c");
linkedHashMap.put("a","a");

或者

linkedHashMap.put("a","a");
linkedHashMap.put("b","b");
linkedHashMap.put("c","c");
linkedHashMap.get("a");

其遍歷順序都會(huì)按照b->c->a輸出录煤,其實(shí)這就是LRU的最核心思想鳄厌,最近使用過的放在最后;

2.LRU原理圖解

image.png

3.LRU源碼解析

 public LruCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        //存儲(chǔ)的最大容量
        this.maxSize = maxSize;
        //可以看出Lru就是利用LinkedHashMap按訪問順序遍歷特性實(shí)現(xiàn)的
        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
public final V put(K key, V value) {
        // key/value都不能為null
        if (key == null || value == null) {
            throw new NullPointerException("key == null || value == null");
        }

        V previous;
        synchronized (this) {
            putCount++;
            //統(tǒng)計(jì)size大小
            size += safeSizeOf(key, value);
            //如果之前put過key/value,那獲取到oldValue
            previous = map.put(key, value);
            //將之前的oldValue的size減去
            if (previous != null) {
                size -= safeSizeOf(key, previous);
            }
        }
        //移除之前oldValue
        if (previous != null) {
            //注意這個(gè)方法entryRemoved
            entryRemoved(false, key, previous, value);
        }
        //這個(gè)方法就是容量超過閾值時(shí)候妈踊,移除最老數(shù)據(jù)了嚎,重新將縮容到閾值之內(nèi)
        trimToSize(maxSize);
        return previous;
    }

 private void trimToSize(int maxSize) {
        while (true) {
            K key;
            V value;
            synchronized (this) {
                if (size < 0 || (map.isEmpty() && size != 0)) {
                    throw new IllegalStateException(getClass().getName()
                            + ".sizeOf() is reporting inconsistent results!");
                }
                //容量小于閾值時(shí)候才退出循環(huán)
                if (size <= maxSize) {
                    break;
                }

                Map.Entry<K, V> toEvict = null;
                for (Map.Entry<K, V> entry : map.entrySet()) {
                    toEvict = entry;
                }
                // END LAYOUTLIB CHANGE

                if (toEvict == null) {
                    break;
                }
                //依次遍歷移除老數(shù)據(jù)
                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                size -= safeSizeOf(key, value);
                evictionCount++;
            }
            //移除老數(shù)據(jù)回調(diào)方法,此方法可以重寫,DiskLRU就可以利用
            entryRemoved(true, key, value, null);
        }
    }
public final V get(K key) {
        //key/value都不可以為null
        if (key == null) {
            throw new NullPointerException("key == null");
        }

        V mapValue;
        synchronized (this) {
            mapValue = map.get(key);
            if (mapValue != null) {
                hitCount++;
                //正常情況下歪泳,這里就已經(jīng)退出了
                return mapValue;
            }
            missCount++;
        }

        /*
         * Attempt to create a value. This may take a long time, and the map
         * may be different when create() returns. If a conflicting value was
         * added to the map while create() was working, we leave that value in
         * the map and release the created value.
         */
        //下面的方法都是一些異常情況處理萝勤,先不關(guān)注
        V createdValue = create(key);
        if (createdValue == null) {
            return null;
        }

        synchronized (this) {
            createCount++;
            mapValue = map.put(key, createdValue);

            if (mapValue != null) {
                // There was a conflict so undo that last put
                map.put(key, mapValue);
            } else {
                size += safeSizeOf(key, createdValue);
            }
        }

        if (mapValue != null) {
            entryRemoved(false, key, createdValue, mapValue);
            return mapValue;
        } else {
            trimToSize(maxSize);
            return createdValue;
        }
    }

//size統(tǒng)計(jì)的2個(gè)方法,其中sizeOf一般需重寫
private int safeSizeOf(K key, V value) {
        int result = sizeOf(key, value);
        if (result < 0) {
            throw new IllegalStateException("Negative size: " + key + "=" + value);
        }
        return result;
}

protected int sizeOf(K key, V value) {
        return 1;
}    

可以看到sizeOf默認(rèn)是返回1呐伞,但是LRU里面的Value一般存放的是些大內(nèi)存東西敌卓,因此需要重寫sizeOf統(tǒng)計(jì)size方法,此方法必須重寫伶氢;

/**
     * Clear the cache, calling {@link #entryRemoved} on each removed entry.
     */
public final void evictAll() {
        trimToSize(-1); // -1 will evict 0-sized elements
}

evictAll方法看注釋也知道是清空緩存方法趟径;

4.實(shí)現(xiàn)圖片LRU的3級(jí)緩存

3級(jí)緩存分別是內(nèi)存的LRU,文件的DiskLRU鞍历,網(wǎng)絡(luò)訪問緩存舵抹;
如下先實(shí)現(xiàn)Memory Lru:

public class LoadFromMemory {
    private LruCache<String,Bitmap> mBitmapLru;

    public LoadFromMemory(){
        //初始化容量設(shè)置為最大內(nèi)存的1/8
        mBitmapLru = new LruCache<String, Bitmap>((int) (Runtime.getRuntime().maxMemory()/8)){
            //重寫size統(tǒng)計(jì)大小
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getByteCount();
            }
        };
    }
   
    //往LRU中添加
    public void addBitmap(String key,Bitmap bitmap){
        mBitmapLru.put(key,bitmap);
    }
    //從LRU中獲取
    public Bitmap getBitmap(String key){
        return mBitmapLru.get(key);
    }
    //清空LRU
    public void clearMemory(){
        mBitmapLru.evictAll();
    }
}

了解了LRU源碼后,實(shí)現(xiàn)一個(gè)內(nèi)存的LRU就是小case了劣砍,沒有特別的惧蛹;

之后考慮實(shí)現(xiàn)DiskLRU:

public class LoadFromDiskFile {
    private File mCacheDir;
    private static final String SD_CACHE_DIR = Environment.getExternalStorageDirectory() + "/photo";
    private LruCache<String,File> mLruCache;
    private static int DISK_CACHE_SIZE = 1024*1024*1024;

    public LoadFromDiskFile() {
        File cacheFile = new File(SD_CACHE_DIR);
        if (!cacheFile.exists()){
            cacheFile.mkdir();
        }
        this.mCacheDir = new File(SD_CACHE_DIR);
        mLruCache = new LruCache<String, File>(DISK_CACHE_SIZE){
            //統(tǒng)計(jì)size改為文件的大小
            @Override
            protected int sizeOf(String key, File value) {
                return (int) value.length();
            }
            //內(nèi)存不夠時(shí)候,移除時(shí)候刑枝,回調(diào)此方法香嗓,可以刪除文件 
            @Override
            protected void entryRemoved(boolean evicted, String key, File oldValue, File newValue) {
                if (evicted && oldValue.exists()){
                    oldValue.delete();
                }
            }
        };
        loadAllFile();
    }
    
    //初始化時(shí)候,從磁盤文件中加載文件到LRU中去
    private void loadAllFile() {
        if (mCacheDir.isDirectory()){
            File[] files = mCacheDir.listFiles();
            if (files == null){
                return;
            }
            //按時(shí)間文件最后一次修改的時(shí)間順序排序
            Arrays.sort(files, new Comparator<File>() {
                @Override
                public int compare(File o1, File o2) {
                    return o1.lastModified() < o2.lastModified() ? -1 : 1;
                }
            });
            //排序后装畅,在往LRU中添加靠娱,這樣最近修改過的文件肯定是最后添加進(jìn)去的;
            for (File file:files){
                mLruCache.put(file.getName(),file);
            }
        }
    }

    //添加文件緩存
    public void addBitmap(String key, Bitmap bitmap){
        //將url地址MD5編碼
        String fileName = encode(key);
        //如果已經(jīng)存在了掠兄,不要在寫文件操作
        if (mLruCache.get(fileName) != null) return;
        //寫文件操作
        File file = new File(SD_CACHE_DIR, fileName);
        FileOutputStream outputStream = null;
        try {
            outputStream = new FileOutputStream(file);
            BufferedOutputStream buffer = new BufferedOutputStream(outputStream);
            bitmap.compress(Bitmap.CompressFormat.JPEG,70,buffer);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            return;
        }
        mLruCache.put(key,file);
    }

    //從文件讀取Bitmap
    public Bitmap getBitmap(String key){
        File file = mLruCache.get(encode(key));
        if (file == null){
            return null;
        }
        //使用過了像云,重新設(shè)置下文件的最近更新時(shí)間
        file.setLastModified(System.currentTimeMillis());
        return BitmapFactory.decodeFile(file.getAbsolutePath());
    }
    
    //清空緩存
    public void clearFileCache(){
        mLruCache.evictAll();
    }
    
    //MD5文件名
    public String encode(String string) {
        byte[] hash = new byte[0];
        try {
            hash = MessageDigest.getInstance("MD5").digest(
                    string.getBytes("UTF-8"));
        } catch (Exception e) {
            e.printStackTrace();
        }
        StringBuilder hex = new StringBuilder(hash.length * 2);
        for (byte b : hash) {
            if ((b & 0xFF) < 0x10) {
                hex.append("0");
            }
            hex.append(Integer.toHexString(b & 0xFF));
        }
        return hex.toString();
    }

}

DiskLRU就需要注意幾點(diǎn)了,
1.文件緩存LRU在每次重啟時(shí)候蚂夕,需要重寫將文件loadAllFile到LRU中迅诬,注意這個(gè)過程需要將文件排序,這里我們定一個(gè)規(guī)則婿牍,就是文件的最新修改時(shí)間侈贷,如果最近使用過文件,哪怕文件沒有被重寫過等脂,也可以手動(dòng)給其setLastModified一個(gè)文件修改的時(shí)間俏蛮,表示最近使用過;
2.文件存儲(chǔ)時(shí)候上遥,需要采用加密搏屑,一般使用MD5編解碼;
3.重寫LRU的sizeOf/entryRemoved方法粉楚;

tips:這里addBitmap其實(shí)是有點(diǎn)問題辣恋,并不是真正意義上的LRU,put相同元素時(shí)候,先get看看有沒有元素在其中,如果有就不寫文件了抑党,避免無效的IO操作包警;

最后在實(shí)現(xiàn)一個(gè)網(wǎng)絡(luò)緩存,網(wǎng)絡(luò)緩存一般就是使用Expires和Cache-Control底靠,具體一般使用Last-Modified / If-Modified-Since害晦,Etag / If-None-Match,2個(gè)對(duì)比緩存暑中,這里也不啰嗦了壹瘟,有興趣自己百度,如果訪問緩存數(shù)據(jù)結(jié)果狀態(tài)碼一般是304的重定向鳄逾,如果不是緩存訪問網(wǎng)絡(luò)狀態(tài)碼就是200稻轨;這里直接使用OKHttp自帶支持網(wǎng)絡(luò)緩存實(shí)現(xiàn)吧:

public class LoadFromNetwork {
    private OkHttpClient okHttpClient;

    public LoadFromNetwork(){
        okHttpClient = new OkHttpClient.Builder().retryOnConnectionFailure(true).build();
    }

    public void getBitmap(final String key,final LruUtils.QueryBitmapListener listener){
        final Request request = new Request.Builder().url(key).build();
        okHttpClient.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                listener.failure(e.getMessage());
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                if (!response.isSuccessful()){
                    listener.failure("response error"+key);
                    return;
                }
                InputStream inputStream = response.body().byteStream();
                Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
                if (bitmap!=null){
                    listener.success(bitmap);
                }else {
                    listener.failure("bitmap is null" + key);
                }
            }
        });
    }
}

將3種緩存封裝在一起:

public class LruUtils{
    public static final String TAG = "LruUtils";
    private LoadFromMemory loadFromMemory;
    private LoadFromDiskFile loadFromDiskFile;
    private LoadFromNetwork loadFromNetwork;
    public LruUtils(){
        loadFromMemory = new LoadFromMemory();
        loadFromDiskFile = new LoadFromDiskFile();
        loadFromNetwork = new LoadFromNetwork();
    }

    public interface QueryBitmapListener{
        void success(Bitmap bitmap);
        void failure(String error);
    }

    public void loadBitmap(final String key, final QueryBitmapListener listener){
        Bitmap bitmap = loadFromMemory.getBitmap(key);
        //內(nèi)存加載
        if (bitmap!=null){
            Log.i(TAG,"loadFromMemory");
            listener.success(bitmap);
            return;
        }
        bitmap = loadFromDiskFile.getBitmap(key);
        //文件磁盤加載
        if (bitmap!=null){
            loadFromMemory.addBitmap(key,bitmap);
            Log.i(TAG,"loadFromDiskFile");
            listener.success(bitmap);
            return;
        }
        //網(wǎng)絡(luò)加載
        loadFromNetwork.getBitmap(key, new QueryBitmapListener() {
            @Override
            public void success(Bitmap bitmap) {
                //網(wǎng)絡(luò)加載成功時(shí)候,寫入到磁盤和內(nèi)存
                loadFromMemory.addBitmap(key,bitmap);
                loadFromDiskFile.addBitmap(key,bitmap);
                Log.i(TAG,"loadFromNetwork");
                listener.success(bitmap);
            }

            @Override
            public void failure(String error) {
                listener.failure(error);
            }
        });
    }

    //清空緩存
    public void clearAllCache(){
        loadFromDiskFile.clearFileCache();
        loadFromMemory.clearMemory();
    }
}
public class MainActivity extends AppCompatActivity implements View.OnClickListener,LruUtils.QueryBitmapListener {
    private ImageView imageView;
    private Button loadNext;
    private Button clear;
    private LruUtils lruUtils;
    private int loadIndex = 0;
    private final int PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE = 0;

    private String[] imageUrl = new String[]{
            "http://t-1.tuzhan.com/38c4c77ede92/c-2/l/2013/09/16/12/e6b466e4fc034b50b098535b14ee497d.jpg",
            "http://picapi.zhituad.com/photo/89/78/69ADE.jpg",
            "http://image.biaobaiju.com/uploads/20181001/22/1538404773-AMGObvJmUQ.jpg",
            "http://pic30.nipic.com/20130619/2531170_124430379002_2.jpg",
            "http://pic24.nipic.com/20121017/8362416_132430698000_2.jpg",
            "http://pic29.nipic.com/20130515/12667289_101713416109_2.jpg"
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        imageView = findViewById(R.id.image);
        loadNext = findViewById(R.id.next);
        clear = findViewById(R.id.clear);
        loadNext.setOnClickListener(this);
        clear.setOnClickListener(this);
        lruUtils = new LruUtils();
        requestPermission();

    }
    
    //申請(qǐng)動(dòng)態(tài)權(quán)限PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE
    private void requestPermission() {
        if (ContextCompat.checkSelfPermission(this,
                Manifest.permission.WRITE_EXTERNAL_STORAGE)
                != PackageManager.PERMISSION_GRANTED) {
            if (ActivityCompat.shouldShowRequestPermissionRationale(this,
                    Manifest.permission.READ_CONTACTS)) {
                ActivityCompat.requestPermissions(this,
                        new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
                        PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE);

            } else {
                ActivityCompat.requestPermissions(this,
                        new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
                        PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE);
            }
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode,
                                           String permissions[], int[] grantResults) {
        switch (requestCode) {
            case PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE: {
                // If request is cancelled, the result arrays are empty.
                if (grantResults.length > 0
                        && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    Log.i(LruUtils.TAG,"onRequestPermissionsResult granted");
                } else {
                    Log.i(LruUtils.TAG,"onRequestPermissionsResult denied");
                    showWaringDialog();
                }
                return;
            }
        }
    }

    private void showWaringDialog() {
        AlertDialog dialog = new AlertDialog.Builder(this)
                .setTitle("警告雕凹!")
                .setMessage("請(qǐng)前往設(shè)置->應(yīng)用->PermissionDemo->權(quán)限中打開相關(guān)權(quán)限殴俱,否則功能無法正常運(yùn)行!")
                .setPositiveButton("確定", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        finish();
                    }
                }).show();
    }



    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.next:
                loadIndex = (++loadIndex%imageUrl.length);
                lruUtils.loadBitmap(imageUrl[loadIndex],this);
            break;
            case R.id.clear:
                lruUtils.clearAllCache();
                break;
        }
    }
    
    //回調(diào)結(jié)果枚抵,注意线欲,回調(diào)過程可能是從OKHTTP線程中回來,線程切換下
    @Override
    public void success(final Bitmap bitmap) {
         runOnUiThread(new Runnable() {
             @Override
             public void run() {
                 imageView.setImageBitmap(bitmap);
             }
         });
    }

    @Override
    public void failure(final String error) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(getApplicationContext(),error,Toast.LENGTH_SHORT).show();
            }
        });
    }
}

到此LRU原理和自己動(dòng)手實(shí)現(xiàn)一個(gè)LRU小Demo實(shí)現(xiàn)了汽摹,其實(shí)緩存不光圖片可以這么干李丰,其他任何大對(duì)象都可以如此實(shí)現(xiàn),知其原理方能運(yùn)用自如逼泣;

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末趴泌,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子拉庶,更是在濱河造成了極大的恐慌嗜憔,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,451評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件砍的,死亡現(xiàn)場離奇詭異痹筛,居然都是意外死亡莺治,警方通過查閱死者的電腦和手機(jī)廓鞠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來谣旁,“玉大人床佳,你說我怎么就攤上這事¢螅” “怎么了砌们?”我有些...
    開封第一講書人閱讀 164,782評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我浪感,道長昔头,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,709評(píng)論 1 294
  • 正文 為了忘掉前任影兽,我火速辦了婚禮揭斧,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘峻堰。我一直安慰自己讹开,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,733評(píng)論 6 392
  • 文/花漫 我一把揭開白布捐名。 她就那樣靜靜地躺著旦万,像睡著了一般。 火紅的嫁衣襯著肌膚如雪镶蹋。 梳的紋絲不亂的頭發(fā)上成艘,一...
    開封第一講書人閱讀 51,578評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音贺归,去河邊找鬼狰腌。 笑死,一個(gè)胖子當(dāng)著我的面吹牛牧氮,可吹牛的內(nèi)容都是我干的琼腔。 我是一名探鬼主播,決...
    沈念sama閱讀 40,320評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼踱葛,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼丹莲!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起尸诽,我...
    開封第一講書人閱讀 39,241評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤甥材,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后性含,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體洲赵,經(jīng)...
    沈念sama閱讀 45,686評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,878評(píng)論 3 336
  • 正文 我和宋清朗相戀三年商蕴,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了叠萍。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,992評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡绪商,死狀恐怖苛谷,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情格郁,我是刑警寧澤腹殿,帶...
    沈念sama閱讀 35,715評(píng)論 5 346
  • 正文 年R本政府宣布独悴,位于F島的核電站,受9級(jí)特大地震影響锣尉,放射性物質(zhì)發(fā)生泄漏刻炒。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,336評(píng)論 3 330
  • 文/蒙蒙 一自沧、第九天 我趴在偏房一處隱蔽的房頂上張望落蝙。 院中可真熱鬧,春花似錦暂幼、人聲如沸筏勒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽管行。三九已至,卻和暖如春邪媳,著一層夾襖步出監(jiān)牢的瞬間捐顷,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評(píng)論 1 270
  • 我被黑心中介騙來泰國打工雨效, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留迅涮,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,173評(píng)論 3 370
  • 正文 我出身青樓徽龟,卻偏偏與公主長得像叮姑,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子据悔,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,947評(píng)論 2 355

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