圖片加載框架

之前實現(xiàn)了一個選擇本地圖片進(jìn)行加載顯示的選擇器,利用Glide作為圖片加載器,Glide是一個十分方便的圖片加載庫泣栈,在項目中使用Glide也十分方便秸抚。由于項目需要自己搭建一個圖片加載框架,實現(xiàn)功能比較簡單,查閱資料后開始著手實現(xiàn)。

一、 實現(xiàn)目標(biāo)

要實現(xiàn)的圖片加載器主要是加載網(wǎng)絡(luò)圖片進(jìn)行顯示变擒,加入LruCache進(jìn)行內(nèi)存緩存,在進(jìn)行列表顯示的時候加載重復(fù)圖片可以直接從緩存中取圖片進(jìn)行顯示寝志,節(jié)約了網(wǎng)絡(luò)資源娇斑,不用再重復(fù)下載策添。但是這個內(nèi)存緩存只能在程序運行時分配到內(nèi)存才能進(jìn)行緩存,在程序結(jié)束時緩存也就釋放了毫缆,下一次打開程序還是要重新下載唯竹。利用硬盤緩存DiskLruCache作為一個二級緩存目錄,將下載的圖片保存到本地緩存苦丁,由于本地緩存目錄是默認(rèn)創(chuàng)建的可以隨程序卸載而刪除浸颓,也可以顯示調(diào)用刪除緩存的方法進(jìn)行清除,因此不用擔(dān)心緩存占用很大空間的問題旺拉。

二产上、 框架實現(xiàn)

1. 線程池

在進(jìn)行圖片下載的時候要開辟一個新線程進(jìn)行耗時操作,往往在加載列表顯示的圖片時會一次性開啟很多線程蛾狗,這里使用線程池來進(jìn)行管理線程晋涣,利用一個任務(wù)Map進(jìn)行管理,如果該圖片地址存在線程正在進(jìn)行下載沉桌,就不會創(chuàng)建新的線程谢鹊,而是等待線程的下載完成。之前已經(jīng)進(jìn)行過線程池的使用留凭,在之前的基礎(chǔ)上進(jìn)行開發(fā)佃扼。

2. LruCache

上一篇日記是關(guān)于LruCache的使用的,在使用內(nèi)存緩存進(jìn)行下載任務(wù)的緩存已經(jīng)搞懂了蔼夜,具體的實現(xiàn)代碼見上一篇兼耀。

3. DiskLruCache

硬盤緩存是在LruCache的基礎(chǔ)上進(jìn)行改進(jìn)的緩存機(jī)制,相比于內(nèi)存緩存挎扰,硬盤緩存可以把緩存保存到本地翠订,利用生成的key進(jìn)行保存和獲取,這個key可以根據(jù)傳入的網(wǎng)絡(luò)地址進(jìn)行生成遵倦,得到一個哈希值,在取值的時候傳入要查找的key就可以找到緩存文件官撼,進(jìn)行解碼顯示梧躺。
首先新建一個類,在這個類的基礎(chǔ)上進(jìn)行

public class ImageDiskCache {

    /**
     * 得到硬盤緩存目錄
     * @param context
     * @param uniqueName
     * @return
     */
    public static File getDiskCacheDir(Context context, String uniqueName) {
        String cachePath;
        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
                || !Environment.isExternalStorageRemovable()) {
            cachePath = context.getExternalCacheDir().getPath();
        } else {
            cachePath = context.getCacheDir().getPath();
        }
        return new File(cachePath + File.separator + uniqueName);
    }

    /**
     * 得到版本號
     * @param context
     * @return
     */
    public static int getAppVersion(Context context) {
        try {
            PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
            return info.versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return 1;
    }

    /**
     * 獲得緩存文件hash值
     * @param key
     * @return
     */
    public static String hashKeyForDisk(String key) {
        String cacheKey;
        try {
            final MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(key.getBytes());
            cacheKey = bytesToHexString(mDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            cacheKey = String.valueOf(key.hashCode());
        }
        return cacheKey;
    }

    private static String bytesToHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            String hex = Integer.toHexString(0xFF & bytes[i]);
            if (hex.length() == 1) {
                sb.append('0');
            }
            sb.append(hex);
        }
        return sb.toString();
    }
}

創(chuàng)建一個DiskLruCache對象不能直接new傲绣,要調(diào)用open方法進(jìn)行

try {
            File cacheDir = ImageDiskCache.getDiskCacheDir (context, "bitmap");
            if(!cacheDir.exists ()) {
                cacheDir.mkdirs ();
            }
            //創(chuàng)建硬盤緩存實例
            mDiskLruCache = DiskLruCache.open (cacheDir, ImageDiskCache.getAppVersion (context), 1, 10 * 1024 * 1024);
        } catch (IOException e) {
            e.printStackTrace ();
        }

下載完成時把緩存文件保存到本地

String key = ImageDiskCache.hashKeyForDisk (imageUrl);
                    DiskLruCache.Editor editor = mDiskLruCache.edit (key);
                    if(editor != null) {
                        OutputStream outputStream = editor.newOutputStream (0);
                        if(downloadStream (imageUrl, outputStream)) {
                            editor.commit ();
                        } else {
                            editor.abort ();
                        }
                    }
                    mDiskLruCache.flush ();

加載圖片時首先訪問內(nèi)存緩存掠哥,如果內(nèi)存緩存有就調(diào)用,沒有繼續(xù)訪問硬盤緩存秃诵,如果硬盤緩存也沒用再開啟一個新的下載線程進(jìn)行下載顯示

public void loadImage(String imageUrl, ImageView iv) {
        iv.setTag (imageUrl);
        Bitmap bitmap = getBitmapFromCache (imageUrl);
        if(bitmap != null) {
            iv.setImageBitmap (bitmap);
            Log.i (TAG, "cache image");
        } else {
            try {
                String key = ImageDiskCache.hashKeyForDisk (imageUrl);
                DiskLruCache.Snapshot snapShot = mDiskLruCache.get (key);
                if(snapShot != null) {
                    InputStream is = snapShot.getInputStream (0);
                    bitmap = BitmapFactory.decodeStream (is);
                    iv.setImageBitmap (bitmap);
                    Log.i (TAG, "disk cache image");
                }
            } catch (IOException e) {
                e.printStackTrace ();
            }
        }
        if(bitmap == null) {
            if(!mLoadMap.containsKey (imageUrl)) {
                ImageDownloaderTask task = new ImageDownloaderTask (imageUrl, iv);
                mLoadMap.put (imageUrl, task);
                try {
                    mThreadPoolExecutor.execute (task);
                } catch (Exception e) {
                    mLoadMap.remove (imageUrl);
                }
            }else {
                ImageDownloaderTask task = mLoadMap.get (imageUrl);
                task.run ();
            }
            Log.i (TAG, "download image");
        }
    }

4. 自定義ImageView

從網(wǎng)絡(luò)上下載的圖片尺寸不一续搀,在使用一般的ImageView進(jìn)行顯示的時候不能實現(xiàn)按圖片比例進(jìn)行填充屏幕的顯示,在顯示圖片的時候?qū)挾裙潭槭謾C(jī)屏幕尺寸菠净,高度隨圖片比例進(jìn)行顯示禁舷。

public class ImageLoadView extends android.support.v7.widget.AppCompatImageView {
    public ImageLoadView(Context context) {
        super (context);
    }

    public ImageLoadView(Context context, @Nullable AttributeSet attrs) {
        super (context, attrs);
    }

    public ImageLoadView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super (context, attrs, defStyleAttr);
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        Drawable drawable = getDrawable ();
        if(drawable != null){
            int width = drawable.getMinimumWidth();
            int height = drawable.getMinimumHeight();
            //確定顯示比例彪杉,寬高比不變
            float scale = (float)height/width;
            int widthMeasure = MeasureSpec.getSize(widthMeasureSpec);
            int heightMeasure = (int)(widthMeasure*scale);
            heightMeasureSpec =  MeasureSpec.makeMeasureSpec(heightMeasure, MeasureSpec.EXACTLY);
        }
        super.onMeasure (widthMeasureSpec, heightMeasureSpec);
    }


}

三、 完整代碼

package com.ruadong.utils.imageloader;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Handler;
import android.util.Log;
import android.util.LruCache;
import android.widget.ImageView;
import android.widget.Toast;

import com.jakewharton.disklrucache.DiskLruCache;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
* @author ruandong
* @create 2018/8/18
* @package com.ruadong.utils
* @Describe 圖片加載器
*/
public class ImageLoader {

   private static final String TAG = "ImageLoader";
   /**
    * 返回Java虛擬機(jī)可用的處理器數(shù)量
    */
   private static final int CPU_COUNT = Runtime.getRuntime ().availableProcessors ();
   /**
    * 線程池基本大小
    */
   private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
   /**
    * 線程池最大線程數(shù)
    */
   private static final int MAXIMUM_POOL_SIZE = CPU_COUNT + 1;
   /**
    * 空閑線程存活時間為2秒
    */
   private static final int KEEP_ALIVE_TIME = 2;
   /**
    * 最大任務(wù)隊列數(shù)
    */
   private static final int MAX_TASK_COUNT = 1000;
   /**
    * 內(nèi)存緩存最大值
    */
   private static final int MAX_CACHE_SIZE = (int) (Runtime.getRuntime ().maxMemory () / 8);
   /**
    * 圖片加載器實例
    */
   private static volatile ImageLoader mImageLoader;
   /**
    * 圖片加載線程池
    */
   private ThreadPoolExecutor mThreadPoolExecutor;
   /**
    * 任務(wù)隊列
    */
   private BlockingQueue<Runnable> mBlockingDeque;
   /**
    * 下載集合
    */
   private Map<String, ImageDownloaderTask> mLoadMap;
   /**
    * 主線程handler
    */
   private Handler mMainHandler;
   /**
    * 上下文
    */
   private Context mContext;
   /**
    * 內(nèi)存緩存LruCache
    */
   private static LruCache<String, Bitmap> mLruCache;
   /**
    * 硬盤緩存DiskLruCache作為圖片加載器的二級緩存,可以緩存從內(nèi)存緩存中remove的bitmap,利用open方法創(chuàng)建實例
    */
   private DiskLruCache mDiskLruCache;

   /**
    * 圖片加載器構(gòu)造器
    */
   private ImageLoader() {
   }

   /**
    * 單例模式,獲取圖片加載器實例
    *
    * @return
    */
   public static ImageLoader getInstance() {
       if(mImageLoader == null) {
           synchronized (ImageLoader.class) {
               if(mImageLoader == null) {
                   mImageLoader = new ImageLoader ();
               }
           }
       }
       return mImageLoader;
   }

   /**
    * 初始化ImageLoader
    *
    * @param context
    */
   public void initImageLoader(Context context) {
       this.mContext = context.getApplicationContext ();
       mBlockingDeque = new LinkedBlockingDeque<> ();
       mThreadPoolExecutor = new ThreadPoolExecutor (CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS, mBlockingDeque);
       mMainHandler = new Handler ();
       mLruCache = new LruCache<String, Bitmap> (MAX_CACHE_SIZE) {
           @Override
           protected int sizeOf(String key, Bitmap value) {
               return value.getByteCount ();
           }
       };
       try {
           File cacheDir = ImageDiskCache.getDiskCacheDir (context, "bitmap");
           if(!cacheDir.exists ()) {
               cacheDir.mkdirs ();
           }
           //創(chuàng)建硬盤緩存實例
           mDiskLruCache = DiskLruCache.open (cacheDir, ImageDiskCache.getAppVersion (context), 1, 10 * 1024 * 1024);
       } catch (IOException e) {
           e.printStackTrace ();
       }
   }

   /**
    * 獲取內(nèi)存緩存
    *
    * @return
    */
   public LruCache<String, Bitmap> getLruCache() {
       return mLruCache;
   }

   /**
    * 清除內(nèi)存緩存
    */
   public void clearLruCache() {
       mLruCache.evictAll ();
   }

   /**
    * 保存bitmap到內(nèi)存緩存
    *
    * @param key
    * @param bitmap
    */
   public void saveBitmapToCache(String key, Bitmap bitmap) {
       if(getBitmapFromCache (key) == null) {
           mLruCache.put (key, bitmap);
           Log.i (TAG, "cache size is " + mLruCache.size ());
       }
   }

   /**
    * 查詢內(nèi)存緩存是否已經(jīng)存在當(dāng)前bitmap
    *
    * @param key
    * @return
    */
   public Bitmap getBitmapFromCache(String key) {
       return mLruCache.get (key);
   }

   /**
    * 從內(nèi)存緩存中移除該bitmap
    */
   public void clearDiskLruCache() {
       try {
           mDiskLruCache.delete ();
           Log.i (TAG, "clear disk cache");
       } catch (IOException e) {
           e.printStackTrace ();
       }
   }

   /**
    * 從外部開啟任務(wù)線程加載圖片
    *
    * @param imageUrl
    * @param iv
    */
   public void loadImage(String imageUrl, ImageView iv) {
       iv.setTag (imageUrl);
       Bitmap bitmap = getBitmapFromCache (imageUrl);
       if(bitmap != null) {
           iv.setImageBitmap (bitmap);
           Log.i (TAG, "cache image");
       } else {
           try {
               String key = ImageDiskCache.hashKeyForDisk (imageUrl);
               DiskLruCache.Snapshot snapShot = mDiskLruCache.get (key);
               if(snapShot != null) {
                   InputStream is = snapShot.getInputStream (0);
                   bitmap = BitmapFactory.decodeStream (is);
                   iv.setImageBitmap (bitmap);
                   Log.i (TAG, "disk cache image");
               }
           } catch (IOException e) {
               e.printStackTrace ();
           }
       }
       if(bitmap == null) {
           if(!mLoadMap.containsKey (imageUrl)) {
               ImageDownloaderTask task = new ImageDownloaderTask (imageUrl, iv);
               mLoadMap.put (imageUrl, task);
               try {
                   mThreadPoolExecutor.execute (task);
               } catch (Exception e) {
                   mLoadMap.remove (imageUrl);
               }
           }else {
               ImageDownloaderTask task = mLoadMap.get (imageUrl);
               task.run ();
           }
           Log.i (TAG, "download image");
       }
   }


   /**
    * 圖片加載任務(wù)類
    */
   private class ImageDownloaderTask implements Runnable {

       private static final int CONNECT_TIMEOUT = 5 * 1000;
       private static final int READ_TIMEOUT = 20 * 1000;
       protected static final String ALLOWED_URI_CHARS = "@#&=*+-_.,:!?()/~'%";
       private Bitmap bitmap;
       private String imageUrl;
       private ImageView ivLoader;

       public ImageDownloaderTask(String imageUrl, ImageView ivLoader) {
           this.imageUrl = imageUrl;
           this.ivLoader = ivLoader;
       }

       @Override
       public void run() {
           boolean flag = true;
           try {
               while (flag) {
                   bitmap = downloadBitmap (imageUrl);
                   mImageLoader.saveBitmapToCache (imageUrl, bitmap);
                   String key = ImageDiskCache.hashKeyForDisk (imageUrl);
                   DiskLruCache.Editor editor = mDiskLruCache.edit (key);
                   if(editor != null) {
                       OutputStream outputStream = editor.newOutputStream (0);
                       if(downloadStream (imageUrl, outputStream)) {
                           editor.commit ();
                       } else {
                           editor.abort ();
                       }
                   }
                   mDiskLruCache.flush ();
                   Log.i (TAG, "disk cache success");
                   //圖片下載完成牵咙,handler通知主線程更新界面
                   if(mMainHandler != null) {
                       mMainHandler.post (new Runnable () {
                           @Override
                           public void run() {
                               if(ivLoader != null && imageUrl.equals (ivLoader.getTag ())) {
                                   ivLoader.setImageBitmap (bitmap);
                                   Toast.makeText (mContext, "download success", Toast.LENGTH_SHORT).show ();
                               }
                           }
                       });
                   }
                   flag = false;
               }
           } catch (Exception e) {
               e.printStackTrace ();
           }
       }

       /**
        * 解析圖片流為bitmap
        *
        * @param imageUrl
        * @return
        */
       protected Bitmap downloadBitmap(String imageUrl) {
           Bitmap bitmap;
           InputStream is = getInputStreamFromURL (imageUrl);
           bitmap = BitmapFactory.decodeStream (is);
           return bitmap;
       }

       /**
        * 根據(jù)傳入的圖片地址獲取網(wǎng)絡(luò)連接
        *
        * @param imageUrl
        * @return
        */
       protected HttpURLConnection getConnection(String imageUrl) {
           HttpURLConnection connection = null;
           String encodedUrl = Uri.encode (imageUrl, ALLOWED_URI_CHARS);
           try {
               if(connection != null) {
                   connection.disconnect ();
               } else {
                   URL url = new URL (encodedUrl);
                   connection = (HttpURLConnection) url.openConnection ();
                   connection.setConnectTimeout (CONNECT_TIMEOUT);
                   connection.setReadTimeout (READ_TIMEOUT);
               }
           } catch (MalformedURLException e) {
               e.printStackTrace ();
           } catch (IOException e) {
               e.printStackTrace ();
           }
           return connection;
       }

       /**
        * 從網(wǎng)絡(luò)連接獲取圖片流
        *
        * @param imageUrl
        * @return
        */
       protected InputStream getInputStreamFromURL(String imageUrl) {
           InputStream inputStream = null;
           HttpURLConnection connection = getConnection (imageUrl);
           try {
               inputStream = connection.getInputStream ();
           } catch (IOException e) {
               e.printStackTrace ();
               if(inputStream != null) {
                   try {
                       inputStream.close ();
                   } catch (IOException e1) {
                       e1.printStackTrace ();
                   }
               }
           }
           return inputStream;
       }

       /**
        * @param imageUrl
        * @param outputStream
        * @return
        */
       protected Boolean downloadStream(String imageUrl, OutputStream outputStream) {
           BufferedOutputStream out = null;
           BufferedInputStream in = null;
           try {
               in = new BufferedInputStream (getInputStreamFromURL (imageUrl), 8 * 1024);
               out = new BufferedOutputStream (outputStream, 8 * 1024);
               int b;
               while ((b = in.read ()) != -1) {
                   out.write (b);
               }
               return true;
           } catch (final IOException e) {
               e.printStackTrace ();
           } finally {
               if(getConnection (imageUrl) != null) {
                   getConnection (imageUrl).disconnect ();
               }
               try {
                   if(out != null) {
                       out.close ();
                   }
                   if(in != null) {
                       in.close ();
                   }
               } catch (final IOException e) {
                   e.printStackTrace ();
               }
           }
           return false;
       }
   }
}

使用也很方便

ImageLoader imageLoader = ImageLoader.getInstance ();
String imageUrl = "...圖片地址";
ImageLoadView imageView = findViewById(R.id.iv);
imageLoader.initImageLoader (context);
imageLoader.loadImage(imageUrl,imageView);
TIM圖片20180928111005.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末派近,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子洁桌,更是在濱河造成了極大的恐慌渴丸,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,816評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件另凌,死亡現(xiàn)場離奇詭異谱轨,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)吠谢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評論 3 385
  • 文/潘曉璐 我一進(jìn)店門碟嘴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人囊卜,你說我怎么就攤上這事娜扇。” “怎么了栅组?”我有些...
    開封第一講書人閱讀 158,300評論 0 348
  • 文/不壞的土叔 我叫張陵雀瓢,是天一觀的道長。 經(jīng)常有香客問我玉掸,道長刃麸,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,780評論 1 285
  • 正文 為了忘掉前任司浪,我火速辦了婚禮泊业,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘啊易。我一直安慰自己吁伺,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,890評論 6 385
  • 文/花漫 我一把揭開白布租谈。 她就那樣靜靜地躺著篮奄,像睡著了一般。 火紅的嫁衣襯著肌膚如雪割去。 梳的紋絲不亂的頭發(fā)上窟却,一...
    開封第一講書人閱讀 50,084評論 1 291
  • 那天,我揣著相機(jī)與錄音呻逆,去河邊找鬼夸赫。 笑死,一個胖子當(dāng)著我的面吹牛咖城,可吹牛的內(nèi)容都是我干的茬腿。 我是一名探鬼主播呼奢,決...
    沈念sama閱讀 39,151評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼滓彰!你這毒婦竟也來了控妻?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,912評論 0 268
  • 序言:老撾萬榮一對情侶失蹤揭绑,失蹤者是張志新(化名)和其女友劉穎弓候,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體他匪,經(jīng)...
    沈念sama閱讀 44,355評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡菇存,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,666評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了邦蜜。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片依鸥。...
    茶點故事閱讀 38,809評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖悼沈,靈堂內(nèi)的尸體忽然破棺而出贱迟,到底是詐尸還是另有隱情,我是刑警寧澤絮供,帶...
    沈念sama閱讀 34,504評論 4 334
  • 正文 年R本政府宣布衣吠,位于F島的核電站,受9級特大地震影響壤靶,放射性物質(zhì)發(fā)生泄漏缚俏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,150評論 3 317
  • 文/蒙蒙 一贮乳、第九天 我趴在偏房一處隱蔽的房頂上張望忧换。 院中可真熱鬧,春花似錦向拆、人聲如沸亚茬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽才写。三九已至,卻和暖如春奖蔓,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背讹堤。 一陣腳步聲響...
    開封第一講書人閱讀 32,121評論 1 267
  • 我被黑心中介騙來泰國打工吆鹤, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人洲守。 一個月前我還...
    沈念sama閱讀 46,628評論 2 362
  • 正文 我出身青樓疑务,卻偏偏與公主長得像沾凄,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子知允,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,724評論 2 351

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