Android 開發藝術探索筆記之十二 -- Bitmap 的加載和 Cache

學習內容:

  • 如何有效加載 Bitmap
  • Android 常用的緩存策略
    • LurChche - 內存緩存
    • DiskLurCache - 存儲緩存
  • 優化列表的卡頓現象




1. Bitmap 的高效加載

如何加載圖片?

四類方法:

  1. BitmapFactory.decodeFile / decodeResource / decodeStream / decodeByteArray
  2. 分別對應從 文件系統 / 資源 / 輸入流 / 以及字節數組 中加載 Bitmap 對象
  3. 關系:decodeFile 和 decodeResource 間接調用了 decodeStream

如何高效加載圖片?

  1. 核心思想:采用 BitmapFactory.Options 加載所需尺寸的圖片

  2. 說明:主要是 inSampleSize 參數,即采樣率。inSampleSize 為 1 時,表示原始大小;當 inSampleSize 為 2 時,采樣后的圖片 寬/高 均變為原來的 1/2,即整個圖片縮小為原來的 1/4。inSampleSize 必須大于 1 才能起作用,效果以此類推。

  3. 具體方法(獲取采樣率):

    1. 將 BitmapFactory.Options 的 inJustDecodeBounds 參數設為 true 并加載圖片
    2. 從 BitmapFactory.Options 中取出圖片的原始寬/高信息,對應于 outWidth 和 outHeight 參數
    3. 計算所需的采樣率 inSampleSize
    4. 將 BitmapFactory.Options 的 inJustDecodeBounds 參數設為 false,然后重新加載圖片

    inJustDecodeBounds 參數設為 true 時,BitmapFactory 只會解析圖片的原始 寬/高 信息,并不會真正的加載圖片




2. Android 中的緩存策略

為什么需要緩存?

兩方面原因:提高程序效率 + 節約流量開銷

緩存策略

一般來說,緩存策略主要包括 緩存的添加、獲取刪除 這三個操作。
目前常用的一種緩存算法是 LRU,即最近最少使用算法,核心思想是緩存滿了時,優先淘汰最近最少使用的緩存對象。

2.1 LruCache

兼容性

LruCache 是 Android 3.1 提供的一個緩存類,如果需要兼容 3.1 以下的版本,則需要使用 support-v4 兼容包中提供的 LruCache

實現思想

LruCache 是一個泛型類,內部采用一個 LinkedHashMap 以強引用的方式存儲外界的緩存對象,通過 get 和 set 方法完成緩存的獲取和添加,當緩存滿時,LruCache 會移除較早使用的緩存對象,然后添加新的緩存對象。

LruCache 是線程安全的。

關于 強引用、軟引用和弱引用:

  • 強引用:直接的對象引用
  • 軟引用:當一個對象只有軟引用存在時,系統內存不足時此對象會被 GC 回收
  • 弱引用:當一個對象只有弱引用存在時,此對象隨時被 GC 回收

原理

留待后續學習。

具體使用

  1. 創建:提供緩存的總容量大小并重寫 sizeOf 方法
  2. 獲?。篻et 方法
  3. 添加:put 方法
  4. 刪除:remove 方法刪除指定的緩存對象

2.2 DiskLruCache

簡述

DiskLruCache 用于實現存儲設備緩存,通過將緩存對象寫入文件系統從而實現緩存的效果。
源碼地址:https://github.com/JakeWharton/DiskLruCache

使用方式

  1. DiskLruCache 的創建

    • 通過 public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) 方法創建自身。
    • 參數:
      • directory:磁盤緩存在文件系統中的存儲路徑
      • appVersion:應用的版本號,一般設為 1 即可
      • valueCount:單個節點所對應的數據的個數,一般設為 1 即可
      • maxSize:緩存總大小,超過這個設定值后,會清除一些緩存
    • 典型代碼如下:
      private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50; //50MB
      
      File diskCacheDir = getDiskCacheDir(mContext,"bitmap");
      if (!diskCacheDir.exists()) {
          distCacheDir.mkdirs();
      }
      mDiskLurCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
      
  2. DiskLruCache 的緩存添加

    • 核心:通過 Editor 完成,Editor 表示一個緩存對象的編輯對象

    • 步驟:

      1. 獲取圖片 url 對應的 key

        public static String hashKeyFromUrl(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();
         }
        
        
      2. 根據 key 通過 edit() 獲取 Editor 對象,得到輸出流,并再下載圖片時通過該輸出流寫入到文件系統,最后通過 commit() 提交。
        注意:此部分應當通過 子線程 執行,避免下載圖片造成 ANR;

        String key = Util.hashKeyFromUrl(url);
        //得到DiskLruCache.Editor
        DiskLruCache.Editor editor = diskLruCache.edit(key);
        if (editor != null) {
            OutputStream outputStream = editor.newOutputStream(0);
            if (downloadUrlToStream(Util.IMG_URL, outputStream)) {
                publishProgress("");
                //寫入緩存
                editor.commit();
            } else {
                //寫入失敗
                editor.abort();
            }
        }
        
      3. 關于下載圖片:

            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 (Exception e) {
                    Log.e(TAG, "Error in downloadBitmap - " + e);
                } finally {
                    if (urlConnection != null) {
                        urlConnection.disconnect();
                    }
                    try {
                        if (out != null) {
                            out.close();
                        }
                        if (in != null) {
                            in.close();
                        }
                    } catch (final IOException e) {
                    }
                }
                return false;
            }
        
  1. DiskLruCache 的緩存查找

    • 將 url 轉換為 key,然后通過 DiskLruCache 的 get 方法得到一個 Snapshot 對象,接著通過 Snapshot.getInputStream() 即可得到緩存的文件輸入流,進而得到緩存圖片:

      private Bitmap getCache() {
           try {
               String key = Util.hashKeyFromUrl(url);
               DiskLruCache.Snapshot snapshot = diskLruCache.get(key);
               if (snapshot != null) {
                   InputStream in = snapshot.getInputStream(0);
                   return BitmapFactory.decodeStream(in);
               }
           } catch (IOException e) {
               e.printStackTrace();
           }
           return null;
      }
      
  2. 關于 FileInputStream 下的縮放

    • 不適用 BitmapFactory.Options 縮放的方法:原因在于 兩次 decodeStream 調用影響了文件流的位置屬性,導致第二次 decodeStream 得到 null。

    • 解決方法:文件描述符

      Bitmap bitmap = null;
      String key = Util.hashKeyFromUrl(url);
      DiskLruCache.Snapshot snapshot = diskLruCache.get(key);
      if (snapshot != null) {
         FileInputStream in = (FileInputStream)snapshot.getInputStream(0);
         FileDescriptor fd = fildInputStream.getFD();
          bitmap = mImageResizer.decodeSampleBitmapFromFileDecriptor(fd,reqWidth,reqHeight);
          if(bitmap != null){
              addBitmapToMemoryCache(key, bitmap);
          }
      }
      

2.3 ImageLoader 的實現

優秀的 ImageLoader 應當具備的功能:

  • 圖片的異步加載
  • 圖片的同步加載
  • 圖片壓縮:降低 OOM
  • 內存緩存:核心,提高效率并降低流量消耗
  • 磁盤緩存:核心,提高效率并降低流量消耗
  • 網絡拉取:當兩種緩存都不可用時,通過網絡拉取圖片

代碼實現

圖片壓縮部分

public class ImageResizer {
    /**
     * 從資源文件中加載圖片。
     * @param res
     * @param resId
     * @param reqWidth
     * @param reqHeight
     * @return
     */
    public static Bitmap decodeSampleBitmapFromResource(Resources res,int resId,int reqWidth,int reqHeight){
 
        BitmapFactory.Options options = new BitmapFactory.Options();
        //第一個options設置為true,去加載圖片
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res,resId,options);
        //計算采樣率
        options.inSampleSize = calculateInSampleSize(options,reqWidth,reqHeight);
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res,resId,options);
    }
 
    /**
     * 從內存卡中加載圖片
     * @param fd
     * @param reqWidth
     * @param reqHeight
     * @return
     */
    public static Bitmap decodeSampleBitmapFromFile(FileDescriptor fd,int reqWidth,int reqHeight){
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFileDescriptor(fd,null,options);
        options.inSampleSize = calculateInSampleSize(options,reqWidth,reqHeight);
        options.inJustDecodeBounds = false;
 
        return BitmapFactory.decodeFileDescriptor(fd,null,options);
    }
    /**
     *計算圖片的采樣率
     * 原理,如果設置的圖片寬、高小于原圖的寬、高。則inSampleSize呈2的指數縮小
     * @param options
     * @param reqWidth
     * @param reqHeight
     * @return
     */
 
    private static int calculateInSampleSize(BitmapFactory.Options options,int reqWidth,int reqHeight){
        if(reqHeight==0 || reqWidth ==0){
            return 1;
        }
        //默認的采樣率
        int inSampleSize = 1;
        //獲取原圖的寬高
        int width = options.outWidth;
        int height = options.outHeight;
 
        if(width > reqWidth || height > reqHeight){
            int halfWidth = width / 2;
            int halfHeight = height / 2;
            while((halfHeight / inSampleSize) >= reqHeight
                    && (halfWidth / inSampleSize) >= reqWidth){
                inSampleSize *= 2;
            }
        }
        return inSampleSize;
    }
}

ImageLoader 部分

public class ImageLoader {
    private static final String TAG = "ImageLoader";
    public static final int MESSAGE_POST_RESULT = 1;

    //CPU 核心數
    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    //CPU 核心線程數
    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 int TAG_KEY_URI = R.id.imageloader_uri;
    private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;
    private static final int IO_BUFFER_SIZE = 8 * 1024;
    private static final int DISK_CACHE_INDEX = 0;
    private boolean mIsDiskLruCacheCreated = false;

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

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

    public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
            CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(Runnable), sThreadFactory);

    private Handler mMainHandler = new Handler(Looper.getMainLooper()) {
        public void handleMessage(Message msg) {
            LoaderResult result = (LoaderResult) msg.obj;
            ImageView imageView = result.imageView;
            String uri = (String) imageView.getTag(TAG_KEY_URI);
            if (uri.equals(result.uri)) {
                imageView.setImageBitmap(result.bitmap);
            } else {
                Log.w(TAG, "set image bitmap,but url has changed,ignored!");
            }
        }
    };

    private Context mContext;
    private ImageResizer mImageResizer = new ImageResizer();
    private LruCache<String, Bitmap> mMemoryCache;
    private DiskLruCache mDiskLruCache;

    //無參構造方法,初始化 LruCache內存緩存 和 DiskLruCache磁盤緩存
    private ImageLoader(Context context) {
        mContext = context.getApplicationContext();
        int maxMemory = (int) (Runtime.getRuntime().maxMemory / 1024);
        int cacheSize = maxMemory / 8;
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
            }
        };
        File diskCacheDir = getDiskCacheDir(mContext, "bitmap");
        if (!diskCacheDir.exists()) {
            diskCacheDir.mkdirs();
        }
        if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
            try {
                mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
                mIsDiskLruCacheCreated = true;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    //提供給外界的創建接口
    public static ImageLoader build(Context context) {
        return new ImageLoader(context);
    }

    //內存緩存 的 添加
    private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
        if (getBitmapFromMemCache(key) == null) {
            mMemoryCache.put(key, bitmap);
        }
    }

    //內存緩存 的 獲取
    private Bitmap getBitmapFromMemCache(String key) {
        return mMemoryCache.get(key);
    }

    public void bindBitmap(final String uri, final ImageView imageView) {
        bindBitmap(uri, imageView, 0, 0);
    }

    /**
     * load bitmap from memory cache or disk or network async,then bind imageview and bitmap
     * NOTE THAT: should run in UI thread
     * 異步加載接口
     */
    public void bindBitmap(final String uri, final ImageView imageView, final int reqWidth, final int reqHeight) {
        imageView.setTag(TAG_KEY_URI, uri);
        Bitmap bitmap = loadBitmapFromMemCache(uri);
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
            return;
        }

        Runnable loadBitmapTask = new Runnable() {
            public void run() {
                Bitmap bitmap = loadBitmap(uri, reqWidth, reqHeight);
                if (bitmap != null) {
                    LoaderResult result = new LoaderResult(imageView, uri, bitmap);
                    mMainHandler.obtainMessage(MESSAGE_POST_RESULT, result).sendToTarget();
                }
            }
        }
        THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
    }

    //同步加載接口
    public Bitmap loadBitmap(String uri, int reqWidth, int reqHeight) {
        Bitmap bitmap = loadBitmapFromMemCache(uri);
        if (bitmap != null) {
            Log.d(TAG, "loadBitmapFromMemCache,url:" + uri);
            return bitmap;
        }

        try {
            bitmap = loadBitmapFromDiskCache(uri, reqWidth, reqHeight);
            if (bitmap != null) {
                Log.d(TAG, "loadBitmapFromDiskCache,url:" + uri);
                return bitmap;
            }
            bitmap = loadBitmapFromHttp(uri, reqWidth, reqHeight);
            Log.d(TAG, "loadBitmapFromHttp,url:" + uri);
        } catch (IOException e) {
            e.printStackTrace();
        }

        if (bitmap == null && !mIsDiskLruCacheCreated) {
            Log.w(TAG, "encounter error,DiskLruCache is not created.");
            bitmap = downloadBitmapFromUrl(uri);
        }
        return bitmap;
    }

    private Bitmap loadBitmapFromMemCache(String url) {
        final String key = hashKeyFromUrl(url);
        Bitmap bitmap = getBitmapFromMemCache(key);
        return bitmap;
    }

    //磁盤緩存 的 添加
    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 = hashKeyFromUrl(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);
    }
    
    //磁盤緩存 的 讀取,并 添加 到 內存緩存
    private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight) {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            Log.w(TAG, "load bitmap from UI Thread,it is not recommended!");
        }
        if (mDiskLruCache == null) {
            return null;
        }

        Bitmap bitmap = null;
        String key = hashKeyFromUrl(url);
        DiskLruCache.SnapShot snapShot = mDiskLruCache.get(key);
        if (snapShot != null) {
            FileInputStream fileInputStream = (FileInputStream) snapShot.getInputStream(DISK_CACHE_INDEX);
            FileDescriptor fileDescriptor = fileInputStream.getFD();
            bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor, reqWidth, reqHeight);
            if (bitmap != null) {
                addBitmapToMemoryCache(key, bitmap);
            }
        }
        return bitmap;
    }

    //將圖片寫入到本地文件中
    public 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 (IOException e) {
            Log.e(TAG, "downloadBitmap failed." + e);
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            MyUtils.close(out);
            MyUtils.close(in);
        }
        return false;
    }

    //下載圖片
    private Bitmap downloadBitmapFromUrl(String urlString) {
        Bitmap bitmap = null;
        HttpURLConnection urlConnection = null;
        BufferedInputStream in = 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 (IOException e) {
            Log.e(TAG, "Error in downloadBitmap:" + e);
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            MyUtils.close(in);
        }
        return bitmap;
    }

    //將 url 轉換為 key
    private String hashKeyFromUrl(String url) {
        String cacheKey;
        try {
            final MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(url.getBytes());
            cacheKey = bytesToHexString(mDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            cacheKey = String.valueOf(url.hasCode());
        }
        return cacheKey;
    }

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

    public File getDiskCacheDir(Context context, String uniqueName) {
        boolean externalStorageAvailable = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
        final String cachePath;
        if (externalStorageAvailable) {
            cachePath = context.getExternalCacheDir().getPath();
        } else {
            cachePath = context.getCacheDir().getPath();
        }
        return new File(cachePath + File.separator + uniqueName);
    }

    private long getUsableSpace(File path) {
        if (Build.VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD) {
            return path.getUsableSpace();
        }
        final StatFs stats = new StatFs(path.getPath());
        return (long) stats.getBloackSize() * (long) stats.getAvailableBlocks();
    }

    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;
        }
    }
}


3. ImageLoader 的使用


3.1 照片墻效果

  • 想要實現寬高相等的 ImageView 時,自定義一個 ImageView 子類,并在 onMeasure 方法中將 heightMeasureSpec 替換為 widthMeasureSpec 即可。這樣做省時省力

    @Override
    protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec){
      super.onMeasure(widthMeasureSpec,widthMeasureSpec);
    }
    
  • 具體實現:

    • ImageAdapter 的 getView 方法中,通過 ImageLoader.bindBitmap() 方法將圖片加載過程交給 ImageLoader

3.2 優化列表的卡頓現象

一些建議

  1. 不要再 getView 中執行耗時操作,比如直接加載圖片
  2. 控制異步任務的執行頻率;如果用戶可以頻繁上下滑動,會在一瞬間產生大量異步任務,會造成線程池的擁堵并帶來大量的 UI 更新操作。應當考慮在滑動時停止加載,列表停下來之后再加載圖片。
  3. 開啟硬件加速,通過設置 android:hardwareAccelerated="true"

大概就這么多吧。

本章結束。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容