ImageLoader的实现(2)-ImageLoader的具体实现

1.1 ImageLoaderde介绍

一个优秀的ImageLoader应该具备以下功能:

  • 图片的同步加载;
  • 图片的异步加载;
  • 图片压缩;
  • 内存缓存;
  • 磁盘缓存;
  • 网络拉取。

图片的同步加载是指能够以同步的方式向调用者提供所加载的图片,这个图片可能是从内存中读取的,也可能是从磁盘缓存上读取的,还可能是从网络上下载的。图片的异步加载是一个很有用的功能,很多时候调用者不想在单独的线程中以同步的方式加载图片,这个时候ImageLoader内部可以提供在自己的线程中加载图片,并将图片设置给相应的ImageView。图片的压缩以便于降低OOM概率,所以图片的压缩也是很重要的。

内存缓存和磁盘缓存是ImageLoader的核心,通过两级缓存有效的提高图片加载性能并有效的降低用户的流量消耗,只有当这两级缓存不可用时才会从网络上拉取图片。

除此之外,要特别注意的是,ImageLoader还需要处理一些特殊情况,比如在ListView或者GridView中,View的复用会对图片的加载造成很大的影响。在ListView或者GridView中,假设一个item A正在从网络加载图片,它对应的ImageView为A,这个时候用户快速滑动列表,很可能item B复用了ImageView A,然后一段时间之后图片加载完毕了。如果直接给ImageView A设置图片,由于这个时候item B复用了ImageView A,那么item B就会显示item A的图片。所以ImageLoader需要处理这一特殊情况。

1.2 图片压缩功能的实现

ImagetResizer类用于实现团的压缩,具体代码如下:

public class ImageResizer {

    private final String TAG = "ImageResizer";

    public Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
                                                  int reqWidth, int reqHeight) {
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res,resId,options);
        options.inSampleSize = calculateSampleSize(options,reqWidth,reqHeight);
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res,resId,options);
    }

    public Bitmap decodeSampledBitmapFromFileDescriptor(FileDescriptor fd,
                                                        int reqWidth, int reqHeight ){
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFileDescriptor(fd,null,options);
        options.inSampleSize = calculateSampleSize(options,reqWidth,reqHeight);
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeFileDescriptor(fd,null,options);
    }
    private int calculateSampleSize(BitmapFactory.Options options, 
                                    int reqWidth, int reqHeight) {
        if(reqWidth == 0||reqHeight == 0){
            return 1;
        }
        final int width = options.outWidth;
        final int height = options.outHeight;
        Log.d(TAG, "w = "+width+" h = "+height);
        int inSampleSize = 1;
        if (height>reqHeight||width>reqWidth){
            final int halfHeight = height/2;
            final int halfWidth = width/2;
            while ((halfHeight/inSampleSize)>=reqHeight&&(halfWidth/inSampleSize)>=reqWidth){
                inSampleSize *=2;
            }
        }
        Log.d(TAG, "calculateSampleSize: inSampleSize = "+inSampleSize);
        return inSampleSize;
    }
}

1.2 内存和磁盘缓存的实现

这里选择LruCache和DiskLruCache为核心实现内存和磁盘的缓存功能。在ImageLoader初始化时完成LruCache和DiskLruCache的创建。

public class ImageLoader {
    private static final int DISK_CACHE_SIZE = 1024 * 1024 * 50;
    private boolean mIsDiskLruCacheCreated = false;
    private LruCache<String, Bitmap> mMemoryCache;
    private DiskLruCache mDiskLruCache;
    private Context mContext;

    private ImageLoader(Context context) {
        mContext = context.getApplicationContext();
        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);//获取进程最大内存转换为MB
        int cacheSize = maxMemory / 8;//缓存大小设置为总内存大小的1/8
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {//创建LruCache

            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
            }
        };
        File disCacheDir = getDiskCacheDir(mContext, "bitmap");
        if (!disCacheDir.exists()) {
            disCacheDir.mkdirs();
        }
        if (getUsableSpace(disCacheDir)>DISK_CACHE_SIZE){
            try {
                mDiskLruCache = DiskLruCache.open(disCacheDir,1,1,DISK_CACHE_SIZE);
                mIsDiskLruCacheCreated = true;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

    /**
     * 获取SD卡剩余空间
     * @param disCacheDir
     * @return
     */
    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
    private long getUsableSpace(File disCacheDir) {
        StatFs fs = new StatFs(disCacheDir.getPath());
        fs.restat(disCacheDir.getPath());
        return fs.getAvailableBlocksLong()*fs.getBlockSizeLong();
    }

    /**
     * 创建Cache文件
     * @param mContext
     * @param bitmap
     * @return
     */
    private File getDiskCacheDir(Context mContext, String bitmap) {
        boolean equals = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
        final String cachePath;
        if (equals){
            cachePath = mContext.getExternalCacheDir().getPath();
        }else {
            cachePath = mContext.getCacheDir().getPath();
        }
        return new File(cachePath+File.separator+bitmap);
    }
}

内存缓存和磁盘缓存创建完毕后还需要提供添加和获取缓存的方法,如下

public void addBitmapMempryCache(String key,Bitmap bitmap){
        if (getBitmapFromMemCache(key)!=null){
            mMemoryCache.put(key,bitmap);
        }

    }
    public Bitmap getBitmapFromMemCache(String key){
        return mMemoryCache.get(key);
    }

    /**
     * 从网络加载图片
     * @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("不能在UI线程中工作");
        }
        if (mDiskLruCache==null){
            return null;
        }

        String key = hashKeyFormUrl(url);
        DiskLruCache.Editor editor = mDiskLruCache.edit(key);
        if (editor!=null){
            OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
            if (downloadUrlToStream(url,outputStream)){
                editor.commit();
            }else {
                editor.abort();
            }
            mDiskLruCache.flush();
        }

        return loadBitmapFromDiskCache(url,reqWidth,reqHeight);
    }

    /**
     * 在磁盘上加载Cache
     * @param url
     * @param reqWidth
     * @param reqHeight
     * @return
     */
    private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight)
            throws IOException {
        if (Looper.myLooper()==Looper.getMainLooper()){
            throw new RuntimeException("不能在UI线程中工作");
        }
        if(mDiskLruCache==null){
            return null;
        }
        Bitmap bitmap = null;
        String key = hashKeyFormUrl(url);
        DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);//拿到Snapshot对象
        if (snapshot != null) {
            FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);//拿到文件输入流对象
            FileDescriptor fileDescriptor = fileInputStream.getFD();
            //压缩图片
            bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor,reqWidth,reqHeight);
            if (bitmap!=null){
                addBitmapMempryCache(key,bitmap);
            }
        }

        return bitmap;
    }

上面实现了几种场和下加载图片的方法,我们下面灵活运用这几个加载缓存的方法实现同步和异步加载。

同步和异步加载的设计

首先是同步加载,同步加载一般需要外部在线程中调用,这是因为同步加载会比较耗时。它的实现如下所示。

    /**
     * 同步加载
     * @param url
     * @param reqWidth
     * @param reqHeight
     * @return
     * @throws IOException
     */
    public Bitmap loadBitmap(String url,int reqWidth,int reqHeight) throws IOException {
        Bitmap bitmap = getBitmapFromMemCache(url);
        if (bitmap!=null)
            return bitmap;
        bitmap = loadBitmapFromDiskCache(url,reqWidth,reqHeight);
        if (bitmap!=null)
        return bitmap;
        bitmap = loadBitmapFromHttp(url,reqWidth,reqHeight);
        if (bitmap!=null) {
            return bitmap;
        }else if(bitmap == null&&!mIsDiskLruCacheCreated){
            Log.d("TAG", "loadBitmap disklrucache is not created");
            bitmap = downloadBitmapFromUrl(url);
        }
        return bitmap;
    }

从代码可以看出同步加载遵循规则为:首先尝试从内存缓存读取图片,接着尝试从磁盘中读取,最后从网络上拉取。注意,这个方法不可以在主线程中调用,否则会抛出异常。

接下来看异步加载的设计,如下所示。

public void bindBitmap(final String url, final ImageView imageView,
                           final int reqWidth, final int reqHeight) {
        imageView.setTag(TAG_KEY_URL, url);
        final Bitmap bitmap = getBitmapFromMemCache(url);
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
            return;
        }
        Runnable loadBitmapTask = new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap = null;
                try {
                    bitmap = loadBitmap(url, reqWidth, reqHeight);
                } catch (IOException e) {
                    e.printStackTrace();
                }
                if (bitmap != null) {
                    LoaderResult result = new LoaderResult(imageView, url, bitmap);
                    mMainHadler.obtainMessage(MESSAGE_POST_RESULT, result).sendToTarget();
                }
            }
        };
        THREAD_POLL_EXECUTOR.execute(loadBitmapTask);//放进线程池执行
    }

思路是首先在内存缓存中读取图片,如果读取不到图片,就把加载的任务放到线程池中去异步执行。

线程池的创建:

    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
    private static final long KEEP_ALIVE = 10L;


    private static final ThreadFactory sThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);

        @Override
        public Thread newThread(@NonNull Runnable r) {
            return new Thread(r, "ImageLoader#" + mCount.getAndIncrement());
        }
    };

    /**
     * 线程池初始化
     */
    public static final Executor THREAD_POLL_EXECUTOR = new ThreadPoolExecutor(
            CORE_POOL_SIZE, MAXIMUM_POOL_SIZE,
            KEEP_ALIVE, TimeUnit.SECONDS,
            new LinkedBlockingQueue<Runnable>(), sThreadFactory);

图片加载在线程池中异步执行,当图片加载完毕时,我们不能直接在子线程里面给ImageView设置图片,这是因为只有在UI线程(主线程)才能做UI操作,这里我们利用主线程handler给主线程发送一个message来更新ImageView。具体实现如下。

 private Handler mMainHadler = new Handler(Looper.getMainLooper()){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            LoaderResult result = (LoaderResult) msg.obj;
            ImageView imageView = result.imageView;
            String uri = (String) imageView.getTag(TAG_KEY_URL);
            if (uri.equals(result.uri)){//判断Tag,防止item复用问题
                imageView.setImageBitmap(result.bitmap);
            }
        }
    };

当主线程接收到消息时,我们从LoaderResult拿到我们的imageview等的值,最重要的是,我们判断uri.equals(result.uri)来避免item复用问题的出现。

到此为止,ImageLoader细节已经做完了,下面是完整ImageLoader 代码:

public class ImageLoader {
    private static final String TAG = "IamegLoader";
    private static final int TAG_KEY_URL = R.id.imageloader_uri;
    private static final int DISK_CACHE_SIZE = 1024 * 1024 * 50;
    private static final int DISK_CACHE_INDEX = 0;
    private static final int IO_BUFFER_SIZE = 8 * 1024;
    private static final int MESSAGE_POST_RESULT = 1;
    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
    private static final long KEEP_ALIVE = 10L;
    private boolean mIsDiskLruCacheCreated = false;
    private LruCache<String, Bitmap> mMemoryCache;
    private DiskLruCache mDiskLruCache;
    private Context mContext;
    private ImageResizer mImageResizer = new ImageResizer();
    private Handler mMainHadler = new Handler(Looper.getMainLooper()){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            LoaderResult result = (LoaderResult) msg.obj;
            ImageView imageView = result.imageView;
            String uri = (String) imageView.getTag(TAG_KEY_URL);
            if (uri.equals(result.uri)){//判断Tag,防止item复用问题
                imageView.setImageBitmap(result.bitmap);
            }
        }
    };

    private static final ThreadFactory sThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);

        @Override
        public Thread newThread(@NonNull Runnable r) {
            return new Thread(r, "ImageLoader#" + mCount.getAndIncrement());
        }
    };

    /**
     * 线程池初始化
     */
    public static final Executor THREAD_POLL_EXECUTOR = new ThreadPoolExecutor(
            CORE_POOL_SIZE, MAXIMUM_POOL_SIZE,
            KEEP_ALIVE, TimeUnit.SECONDS,
            new LinkedBlockingQueue<Runnable>(), sThreadFactory);


    private ImageLoader(Context context) {
        mContext = context.getApplicationContext();
        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);//获取进程最大内存转换为MB
        int cacheSize = maxMemory / 8;//缓存大小设置为总内存大小的1/8
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {//创建LruCache

            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
            }
        };
        File disCacheDir = getDiskCacheDir(mContext, "bitmap");
        if (!disCacheDir.exists()) {
            disCacheDir.mkdirs();
        }
        if (getUsableSpace(disCacheDir) > DISK_CACHE_SIZE) {
            try {
                mDiskLruCache = DiskLruCache.open(disCacheDir, 1, 1, DISK_CACHE_SIZE);
                mIsDiskLruCacheCreated = true;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }
    /**
     *创建ImageLoader对象
     */
    public static ImageLoader build(Context context){
        return new ImageLoader(context);
    }

    /**
     * 同步加载
     *
     * @param url
     * @param reqWidth
     * @param reqHeight
     * @return
     * @throws IOException
     */
    public Bitmap loadBitmap(String url, int reqWidth, int reqHeight) throws IOException {
        Bitmap bitmap = getBitmapFromMemCache(url);
        if (bitmap != null)
            return bitmap;
        bitmap = loadBitmapFromDiskCache(url, reqWidth, reqHeight);
        if (bitmap != null)
            return bitmap;
        bitmap = loadBitmapFromHttp(url, reqWidth, reqHeight);
        if (bitmap != null) {
            return bitmap;
        } else if (bitmap == null && !mIsDiskLruCacheCreated) {
            Log.d("TAG", "loadBitmap disklrucache is not created");
            bitmap = downloadBitmapFromUrl(url);
        }
        return bitmap;
    }

    public void bindBitmap(final String url, final ImageView imageView) {
        final int reqWidth = imageView.getWidth();
        final int reqHeight = imageView.getHeight();
        imageView.setTag(TAG_KEY_URL, url);
        final Bitmap bitmap = getBitmapFromMemCache(url);
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
            return;
        }
        Runnable loadBitmapTask = new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap = null;
                try {
                    bitmap = loadBitmap(url, reqWidth, reqHeight);
                } catch (IOException e) {
                    e.printStackTrace();
                }
                if (bitmap != null) {
                    LoaderResult result = new LoaderResult(imageView, url, bitmap);
                    mMainHadler.obtainMessage(MESSAGE_POST_RESULT, result).sendToTarget();
                }
            }
        };
        THREAD_POLL_EXECUTOR.execute(loadBitmapTask);//放进线程池执行
    }

    /**
     * 下载图片,加载成Bitmap
     *
     * @param urlString
     * @return
     */
    private Bitmap downloadBitmapFromUrl(String urlString) {
        HttpURLConnection urlConnection = null;
        BufferedInputStream in = null;
        Bitmap bitmap = null;
        try {
            final URL url = new URL(urlString);
            urlConnection = (HttpURLConnection) url.openConnection();
            in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE);
            bitmap = BitmapFactory.decodeStream(in);

        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            try {
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return bitmap;
    }

    /**
     * 添加缓存
     *
     * @param key
     * @param bitmap
     */
    private void addBitmapMempryCache(String key, Bitmap bitmap) {
        if (getBitmapFromMemCache(key) != null) {
            mMemoryCache.put(key, bitmap);
        }

    }

    /**
     * 获取缓存
     *
     * @param key
     * @return
     */
    private Bitmap getBitmapFromMemCache(String key) {
        return mMemoryCache.get(key);
    }

    /**
     * 从网络加载图片
     *
     * @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("不能在UI线程中工作");
        }
        if (mDiskLruCache == null) {
            return null;
        }

        String key = hashKeyFormUrl(url);
        DiskLruCache.Editor editor = mDiskLruCache.edit(key);
        if (editor != null) {
            OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
            if (downloadUrlToStream(url, outputStream)) {
                editor.commit();
            } else {
                editor.abort();
            }
            mDiskLruCache.flush();
        }

        return loadBitmapFromDiskCache(url, reqWidth, reqHeight);
    }

    /**
     * 在磁盘上加载Cache
     *
     * @param url
     * @param reqWidth
     * @param reqHeight
     * @return
     */
    private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight)
            throws IOException {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            throw new RuntimeException("不能在UI线程中工作");
        }
        if (mDiskLruCache == null) {
            return null;
        }
        Bitmap bitmap = null;
        String key = hashKeyFormUrl(url);
        DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);//拿到Snapshot对象
        if (snapshot != null) {
            FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);//拿到文件输入流对象
            FileDescriptor fileDescriptor = fileInputStream.getFD();
            //压缩图片
            bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor, reqWidth, reqHeight);
            if (bitmap != null) {
                addBitmapMempryCache(key, bitmap);
            }
        }

        return bitmap;
    }

    /**
     * 下载图片写入磁盘缓存文件中
     *
     * @param urlString
     * @param 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 (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            try {
                out.close();
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return false;
    }


    /**
     * 把url转换为Key
     *
     * @param url
     * @return
     */
    private String hashKeyFormUrl(String url) {
        String cacheKey;
        try {
            final MessageDigest mDuigest = MessageDigest.getInstance("MD5");
            mDuigest.update(url.getBytes());
            cacheKey = byteToHexString(mDuigest.digest());
        } catch (Exception e) {
            cacheKey = String.valueOf(url.hashCode());
        }
        return cacheKey;
    }

    private String byteToHexString(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();
    }

    /**
     * 获取SD卡剩余空间
     *
     * @param disCacheDir 缓存文件
     * @return
     */
    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
    private long getUsableSpace(File disCacheDir) {
        StatFs fs = new StatFs(disCacheDir.getPath());
        fs.restat(disCacheDir.getPath());
        return fs.getAvailableBlocksLong() * fs.getBlockSizeLong();
    }

    /**
     * 创建Cache文件
     *
     * @param mContext
     * @param bitmap   String类型,最后的文件名
     * @return
     */
    private File getDiskCacheDir(Context mContext, String bitmap) {
        boolean equals = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
        final String cachePath;
        if (equals) {
            cachePath = mContext.getExternalCacheDir().getPath();
        } else {
            cachePath = mContext.getCacheDir().getPath();
        }
        return new File(cachePath + File.separator + bitmap);
    }

    private static class LoaderResult {
        public ImageView imageView;
        public String uri;
        public Bitmap bitmap;

        public LoaderResult(ImageView imageView, String uri, Bitmap bitmap) {
            this.imageView = imageView;
            this.uri = uri;
            this.bitmap = bitmap;
        }
    }
}


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

推荐阅读更多精彩内容