Bitmap

一、基础

Bitmap指的是一张图片,可以是png格式,也可以是jpg等其它常见的图片格式。

1.加载方式

Android中通常使用BitmapFactory来加载Bitmap,方法如下:

BitmapFactory.decodeByteArray() //从字节数组加载
BitmapFactory.decodeFile() //从文件加载
BitmapFactory.decodeFileDescriptor() //从"文件描述符"加载
BitmapFactory.decodeResource() //从资源加载,通常为drawable资源
BitmapFactory.decodeStream() //从输入流加载

2.内存位置

  • Android 3.0之前:Bitmap的像素数据存放在Native内存,而Bitmap对象本身则存放在Dalvik Heap中。
  • Android 3.0到Android 7.1:Bitmap的内存就全部在Dalvik Heap中。
  • Android 8.0及之后,Bitmap的像素数据又重新回到Native分配了。

3.内存回收

  • Android 3.0之前,使用Bitmap.recycle()进行Bitmap的内存回收。
  • Android 3.0到Android 7.1,不需要手动回收Bitmap。
  • Android 8.0及之后,不需要手动回收Bitmap。

4.内存复用(inBitmap)

  • Android 3.0开始,在Bitmap中引入了一个新的字段BitmapFactory.Options.inBitmap,设置此字段为true后,解码方法会尝试复用一张存在的Bitmap。这意味着Bitmap的内存被复用,避免了内存的回收及申请过程。
  • Android4.4(API 19)之前只有格式为jpg或png、同等宽高、inSampleSize为1的Bitmap才可以复用。
  • Android4.4(API 19)及之后被复用的Bitmap的内存大于或等于需要新申请内存的Bitmap的内存就可以复用了。

用法(和LruCache配合实现内存的两级缓存):
(1)将需要缓存的图片存入LruCache,当LruCache删除不用的图片时,将删除的图片放入软引用中。

protected synchronized void entryRemoved(boolean evicted, Object key, BitmapDrawable oldValue, BitmapDrawable newValue) {
    //reusableBitmaps是HashMap<Object, SoftReference<Bitmap>>用于缓存从LruCache中移除的Bitmap
    reusableBitmaps.put(key, new SoftReference<Bitmap>(oldValue.getBitmap()));
}

(2)在加载图片时,先调用LruCache.get(key),如果拿不到Bitmap对象再调用reusableBitmaps.get(key)进行获取。

5.像素格式

  • ALPHA_8:颜色信息只由透明度组成,占8位。
  • ARGB_4444:颜色信息由透明度与R(Red),G(Green),B(Blue)四部分组成,每个部分都占4位,总共占16位。
  • ARGB_8888:颜色信息由透明度与R(Red),G(Green),B(Blue)四部分组成,每个部分都占8位,总共占32位。该像素格式是Bitmap默认的颜色配置信息,也是最占空间的一种配置。
  • RGB_565:颜色信息由R(Red),G(Green),B(Blue)三部分组成,R占5位,G占6位,B占5位,总共占16位。

通常我们优化Bitmap时,当需要做性能优化或者防止OOM,我们通常会使用RGB_565,因为ALPHA_8只有透明度,显示一般图片没有意义,Bitmap.Config.ARGB_4444显示图片不清楚,Bitmap.Config.ARGB_8888占用内存最多。

6.内存计算

(1)获取Bitmap内存大小方法

getByteCount():
API 12加入,表示存储Bitmap的像素需要的最少内存。

getAllocationByteCount():
API 19加入,用于代替getByteCount(),表示为Bitmap分配的实际内存大小。

一般情况下getByteCount()和getAllocationByteCount()是相等的。但是Bitmap内存如果复用之后,两者就不一样了。通过复用Bitmap来解码图片,如果被复用的Bitmap的内存比待分配内存的Bitmap大,那么getByteCount()表示新解码图片占用的内存大小(并非实际内存大小,实际内存大小是被复用Bitmap的内存大小),getAllocationByteCount()表示被复用Bitmap真实占用的内存大小。

例子:(inBitmap)

  • 三星G9500,屏幕像素密度480
  • res/drawable/ic_launcher2,实际图片分辨率:192*192
  • res/drawable/ic_launcher,实际图片分辨率:72*72
  • drawable目录对应的屏幕像素密度为160
//测试代码
BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher2, options);
Log.d(TAG, "zwm, bitmap = " + bitmap);
Log.d(TAG, "zwm, bitmap width: " + bitmap.getWidth());
Log.d(TAG, "zwm, bitmap height: " + bitmap.getHeight());
Log.d(TAG, "zwm, bitmap getByteCount: " + bitmap.getByteCount());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    Log.d(TAG, "zwm, bitmap getAllocationByteCount: " + bitmap.getAllocationByteCount());
}

options.inMutable = true;
options.inBitmap = bitmap;
Bitmap bitmap2 = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher, options);
Log.d(TAG, "zwm, bitmap2 = " + bitmap2);
Log.d(TAG, "zwm, bitmap2 width: " + bitmap2.getWidth());
Log.d(TAG, "zwm, bitmap2 height: " + bitmap2.getHeight());
Log.d(TAG, "zwm, bitmap2 getByteCount: " + bitmap2.getByteCount());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    Log.d(TAG, "zwm, bitmap2 getAllocationByteCount: " + bitmap2.getAllocationByteCount());
}

//输出log
2019-12-23 16:28:46.137 zwm, bitmap = android.graphics.Bitmap@6497778 //Bitmap对象
2019-12-23 16:28:46.137 zwm, bitmap width: 576
2019-12-23 16:28:46.138 zwm, bitmap height: 576
2019-12-23 16:28:46.138 zwm, bitmap getByteCount: 1327104
2019-12-23 16:28:46.138 zwm, bitmap getAllocationByteCount: 1327104
2019-12-23 16:28:46.141 zwm, bitmap2 = android.graphics.Bitmap@6497778 //对象同上
2019-12-23 16:28:46.141 zwm, bitmap2 width: 216
2019-12-23 16:28:46.141 zwm, bitmap2 height: 216
2019-12-23 16:28:46.141 zwm, bitmap2 getByteCount: 186624 //新解码图片占用的内存大小
2019-12-23 16:28:46.141 zwm, bitmap2 getAllocationByteCount: 1327104 //被复用Bitmap真实占用的内存大小

(2)资源文件夹下加载图片得到Bitmap内存大小计算方法

Android屏幕适配

7.采样(inSampleSize)

public class ImageResizer {
    public ImageResizer(){}

    /**
     * 从资源文件中获取相应的图片
     * @param res
     * @param resId
     * @param reqWidth
     * @param reqHeight
     * @return
     */
    public Bitmap decodeSampleBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {
        Options options = new Options();
        //设置只加载宽高标志位
        options.inJustDecodeBounds = true;
        //加载原始图片宽高到Options中
        BitmapFactory.decodeResource(res, resId, options);
        //通过所需图片宽高和原始图片宽高计算采样率
        options.inSampleSize = calculateSampleSize(reqWidth, reqHeight, options);
        //还原并再次加载图片
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resId, options);
    }

    /** 
     * 从文件描述符中获取相应的图片
     * @param reqWidth
     * @param reqHeight
     * @param options
     * @return
     */
    public Bitmap decodeSampleBitmapFromFileDescriptor(FileDescriptor fd, int reqWidth, int reqHeight) {
        Options options = new Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFileDescriptor(fd, null, options);
        options.inSampleSize = calculateSampleSize(reqWidth, reqHeight, options);
        options.inJustDecodeBounds = false;
        return  BitmapFactory.decodeFileDescriptor(fd, null, options);
    }

    //计算采样率
    public static int calculateSampleSize(int reqWidth, int reqHeight, Options options) {
        //如果传入0参数,则将采样率设成1,即不压缩
        if (reqWidth == 0 || reqHeight == 0) {
            return 1;
        }

        int inSampleSize = 1;
        int width = options.outWidth;
        int height = options.outHeight;
        
        //当所需宽高比实际宽高小时才进行压缩
        if(reqWidth < width && reqHeight < height) {
            int halfWidth = width / 2;
            int halfHeight = height / 2;
            //保证压缩后的宽高不能小于所需宽高
            while(reqWidth <= halfWidth && reqHeight <= halfHeight) {
                inSampleSize *= 2;
                halfWidth /= 2;
                halfHeight /= 2;
            }
        }
        return inSampleSize;
    }
}

8.缩放(inScaled)

  • 如果inScaled设置为true就缩放,设置为false则不缩放,默认值是true。
  • 缩放只针对资源文件有效,对于其他来源的图片不起效果。
  • 缩放的比例为:inTargetDensity / inDensity。

二、缓存策略

Android中三级缓存策略:内存 -- 磁盘 -- 网络。在获取资源时先从内存缓存中获取,如果没有则从磁盘缓存中获取,如果还是没有再从网络中获取。Android中通过LruCache实现内存缓存,通过DiskLruCache实现磁盘缓存,它们采用的都是LRU(Least Recently Used)最近最少使用算法来移除缓存。

1.LruCache

LruCache是Android提供的一个缓存类,它是一个泛型类,内部采用一个LinkedHashMap以强引用的方式存储外界的缓存对象,其提供了get和put方法来完成缓存的获取和添加,当缓存满时,会移除较早使用的缓存对象,然后再添加新的缓存对象。

(1)强引用、弱引用、软引用、虚引用

强引用(StrongReference):
可以通过以下方式创建一个强引用对象:

String str = "abc";

变量str就是字符串对象"abc"的一个强引用。

如下代码会移除str的强引用:

str = null;

此时垃圾回收器就会在某一时刻回收该对象。

强引用可以阻止垃圾回收器回收对象。

弱引用(WeakReference):
可以通过以下方式创建一个弱引用对象:

String str = "test";
WeakReference<String> wr = new WeakReference<String>(str);
str = null;

如果一个对象有弱引用指向它,当移除强引用时,垃圾回收器会立即回收该对象。

弱引用无法阻止垃圾回收器回收对象。

软引用(SoftReference):
可以通过以下方式创建一个软引用对象:

String str = "test";
SoftPreference sr = new SoftPreference(str);
str = null;

如果一个对象有软引用指向它,当移除强引用时,对象不会立即被回收,只有在JVM需要内存时,才会回收该对象。

软引用无法阻止垃圾回收器回收对象,但可以延迟回收。

虚引用(PhantomReference):
可以通过以下方式创建一个虚引用对象:

String str = "test"
PhantomReference pr = new PhantomReference(str);
str = null;

如果一个对象有虚引用指向它,当移除强引用时,拥有虚引用的对象可以在任何时刻被垃圾回收器回收。

虚引用无法阻止垃圾回收器回收对象。

(2)LruCache使用

//初始化LruCache对象
public void initLruCache() {
    //获取当前进程的可用内存,转换成KB单位
    int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
    //分配缓存的大小
    int maxSize = maxMemory / 8;
    //创建LruCache对象并重写sizeOf方法
    lruCache = new LruCache<String, Bitmap>(maxSize) {
        @Override
        protected int sizeOf(String key, Bitmap value) {
            // TODO Auto-generated method stub
            return value.getWidth() * value.getHeight() / 1024;
        }
    };
}

/**
 * 将图片存入缓存
 * @param key 图片的url转化成的key
 * @param bitmap
 */
private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
    if(getBitmapFromMemoryCache(key) == null) {
        mLruCache.put(key, bitmap);
    }
}

private Bitmap getBitmapFromMemoryCache(String key) {
    return mLruCache.get(key);
}

/**
 * 因为外界一般获取到的是url而不是key,因此再做一层封装
 * @param url http url
 * @return bitmap
 */
private Bitmap loadBitmapFromMemoryCache(String url) {
    final String key = hashKeyFromUrl(url);
    return getBitmapFromMemoryCache(key);
}

2.DiskLruCache

DiskLruCache用于实现存储设备缓存,即磁盘缓存,它通过将缓存对象写入文件系统从而实现缓存。DiskLruCache得到了Android官方文档的推荐,但它不属于Android SDK的一部分。

(1)DiskLruCache使用

//初始化DiskLruCache,包括一些参数的设置
public void initDiskLruCache {
    //配置固定参数
    // 缓存空间大小
    private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;
    //下载图片时的缓存大小
    private static final long IO_BUFFER_SIZE = 1024 * 8;
    // 缓存空间索引,用于Editor和Snapshot,设置成0表示Entry下面的第一个文件
    private static final int DISK_CACHE_INDEX = 0;

    //设置缓存目录
    File diskLruCache = getDiskCacheDir(mContext, "bitmap");
    if(!diskLruCache.exists())
        diskLruCache.mkdirs();
    //创建DiskLruCache对象,当然是在空间足够的情况下
    if(getUsableSpace(diskLruCache) > DISK_CACHE_SIZE) {
        try {
            mDiskLruCache = DiskLruCache.open(diskLruCache, 
                    getAppVersion(mContext), 1, DISK_CACHE_SIZE);
            mIsDiskLruCache = true;
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

/**
 * 从网络中下载图片到磁盘中并获取到按需压缩后的图片
 * 1.由于涉及到网络通信,因此该方法应该运行在子线程当中
 * 2.图片是完整下载下来的,reqWidth和reqHeight只是在从磁盘读取的时候进行压缩用的
 * 3.在存到磁盘的时候url是转换过的编码的
 * 4.要通过editor的commit保证被其它的Reader看到,而且在commit中会保证缓存大小不超过阈值
 * 5.最后记得调用flush()将数据确实写入文件系统
 * 
 * @param url 图片网址
 * @param reqWidth 所需宽度
 * @param reqHeight 所需高度
 * @return 按需压缩后的图片
 * @throws IOException
 */
private Bitmap loadBitmapFromHttp(String url, int reqWidth, int reqHeight) throws IOException {
    if(Looper.myLooper() == Looper.getMainLooper())
        throw new RuntimeException("can not visit network from UI Thread.");
    if(mDiskLruCache == null)
        return null;
    
    String key = hashKeyForDisk(url);
    DiskLruCache.Editor editor = mDiskLruCache.edit(key);
    if(editor != null) {
        OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
        //写入完成后绝不能忘了commit和flush
        if(downloadUrlToStream(url, outputStream)) {
            editor.commit();
        } else {
            editor.abort();
        }
        //flush确保数据写入文件系统
        mDiskLruCache.flush();
    }
    
    return loadBitmapFromDiskCache(url, reqWidth, reqHeight);
}

/**
 * 考虑到直接使用URL作为DiskLruCache中LinkedHashMap的Key不太适合,
 * 因为图片URL中可能包含一些特殊字符,这些字符有可能在命名文件时是不合法的。
 * 其实最简单的做法就是将图片的URL进行MD5编码,编码后的字符串肯定是唯一的,
 * 并且只会包含0-F这样的字符,完全符合文件的命名规则。
 *
 * 该方法在写入磁盘和从磁盘读取时都需要用到,因为它是计算出索引的方法
 * 
 * @param key 图片的url
 * @return MD5编码之后的key
 */
public 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 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();  
} 

/**
 * 将图片资源写到文件系统上
 * @param urlString 资源
 * @param outputStream Editor的对应Entry下的文件的OutputStream
 * @return
 */
private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {  
    HttpURLConnection urlConnection = null;  
    BufferedOutputStream out = null;  
    BufferedInputStream in = null;  
    try {  
        final URL url = new URL(urlString);  
        urlConnection = (HttpURLConnection) url.openConnection();  
        in = new BufferedInputStream(urlConnection.getInputStream(),   
                IO_BUFFER_SIZE);  
        out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);  
        int b;  
        while ((b = in.read()) != -1) {  
            out.write(b);  
        }  
        return true;  
    } catch (final IOException e) {  
        e.printStackTrace();  
    } finally {  
        if (urlConnection != null) {  
            urlConnection.disconnect();  
        }  
        try {  
            if (out != null) {  
                out.close();  
            }  
            if (in != null) {  
                in.close();  
            }  
        } catch (final IOException e) {  
            e.printStackTrace();  
        }  
    }  
    return false;  
}

/**
 * 磁盘缓存的读取
 * @param url
 * @param reqWidth
 * @param reqHeight
 * @return
 */
private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight) throws IOException {
    if(Looper.myLooper() == Looper.getMainLooper())
        Log.w(TAG, "it's not recommented load bitmap from UI Thread");
    if(mDiskLruCache == null)
        return null;
    
    Bitmap bitmap = null;
    String key = hashKeyForDisk(url);
    Snapshot snapshot = mDiskLruCache.get(key);
    if(snapshot != null) {
        FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
        FileDescriptor fd = fileInputStream.getFD();
        bitmap = mImageResizer.decodeSampleBitmapFromFileDescriptor(fd, reqWidth, reqHeight);
        
        if(bitmap != null)
            addBitmapToMemoryCache(key, bitmap);
    }
    return bitmap;      
}

三、ImageLoader

ImageLoader封装了Bitmap的高效加载、缓存策略(LruCache和DiskLruCache),应该具备以下功能:

1.图片的同步加载

/**
 * load bitmap from memory cache or disk or network
 * NOTE that should run in a new Thread
 * @param url http url
 * @param reqWidth  the width that imageView desire
 * @param reqHeight the height that imageView desire
 * @return bitmap maybe null
 */
public Bitmap loadBitmap(String url, int reqWidth, int reqHeight) {
    Bitmap bitmap = loadBitmapFromMemoryCache(url);
    
    if(bitmap != null) {
        Log.d(TAG, "loadBitmapFromMemoryCache, url:"+url);
        return bitmap;
    }
    
    try {
        bitmap = loadBitmapFromDiskCache(url, reqWidth, reqHeight);
        if(bitmap != null) {
            Log.d(TAG, "loadBitmapFromDiskCache, url:"+url);
            return bitmap;
        }
        bitmap = loadBitmapFromHttp(url, reqWidth, reqHeight);
        Log.d(TAG, "loadBitmapFromHttp, url:"+url);
    } catch (Exception e) {
        e.printStackTrace();
    }
    
    if(bitmap == null && !mIsDiskLruCacheCreated) {
        Log.w(TAG, "encounter error, DiskLruCache is not created.");
        bitmap = downloadBitmapFromUrl(url);
    }
    
    return bitmap;
}

2.图片的异步加载

/**
 * load bitmap from memory cache or disk or http, 
 *  then bind bitmap and ImageView
 * @param uri
 * @param imageView
 */
public void bindBitmap(final String uri, final ImageView imageView) {
    bindBitmap(uri, imageView, 0, 0);
}

/**
 * 封装了从url获取图片到加载到特定的ImageView当中的方法
 * 先从内存中获取图片,如果获取不到则调用loadBitmap获取图片
 * 如果是从网络中获取图片,则在将图片加载到ImageView前,会将图片的url和ImageView的tag进行对比:
 * (1)如果相同则说明ImageView没有被复用,可以加载图片;
 * (2)如果不同则说明ImageView被复用了,放弃加载下载完成的图片。
 * 
 * @param url
 * @param imageView
 * @param reqWidth
 * @param reqHeight
 */
public void bindBitmap(final String url, final ImageView imageView,
        final int reqWidth, final int reqHeight) {
    imageView.setTag(TAG_KEY_URI);
    
    Bitmap bitmap = loadBitmapFromMemoryCache(url);
    if(bitmap != null) {
        imageView.setImageBitmap(bitmap);
        return;
    }
    
    Runnable loadBitmapTask = new Runnable() {
        @Override
        public void run() {
            Bitmap bitmap = loadBitmap(url, reqWidth, reqHeight);
            if(bitmap != null) {
                LoaderResult result = new LoaderResult(url, imageView, bitmap);
                mMainHandler.obtainMessage(MESSAGE_POST_RESULT, result)
                    .sendToTarget();
            }
        };
    };
    
    THREAD_POOL_EXEUTOR.execute(loadBitmapTask);
}

3.高效加载

  • 采样
  • 像素格式

4.内存缓存

  • LruCache
  • 内存复用

5.磁盘缓存

  • DiskLruCache

6.网络拉取

相关链接

关于Bitmap的内存,加载和回收等

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容