Universal_Image_Loader源碼解析

UIL是一個老牌的開源圖片加載框架,前幾年做Android開發(fā)的小伙伴肯定都非常熟悉。雖然現(xiàn)在UIL的作者已經(jīng)停止進行代碼維護,而且后面又有很多優(yōu)秀的開源圖片加載框架推出,比如Glide、Picasso、Fresco等。但是這并不妨礙我們對于UIL的基本使用。今天就帶著大家一起來從源碼的角度來認識一下Universal_Image_Loader。

一、初識UIL

UIL的基本特性,這里簡單羅列一下:

  1. 多線程下載圖片,圖片可以來源于網(wǎng)絡,文件系統(tǒng),項目文件夾assets中以及drawable中等
  2. 支持隨意的配置ImageLoader,例如線程池,圖片下載器,內(nèi)存緩存策略,硬盤緩存策略,圖片顯示選項以及其他的一些配置
  3. 支持圖片的內(nèi)存緩存,文件系統(tǒng)緩存或者SD卡緩存
  4. 支持圖片下載過程的監(jiān)聽
  5. 根據(jù)控件(ImageView)的大小對Bitmap進行裁剪,減少Bitmap占用過多的內(nèi)存
  6. 較好的控制圖片的加載過程,例如暫停圖片加載,重新開始加載圖片,一般使用在ListView,GridView中,滑動過程中暫停加載圖片,停止滑動的時候去加載圖片
  7. 提供在較慢的網(wǎng)絡下對圖片進行加載

基本使用方法:

  1. 在Gradle的dependency下加入
    compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.3'
  2. 加入權(quán)限:
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.INTERNET" />
  3. 在全局Application中new一個ImageLoder對象,這個對象是單例模式的:
        public class MyApplication extends Application {  
           
             @Override  
             public void onCreate() {  
                 super.onCreate();  
           
                 //創(chuàng)建默認的ImageLoader配置參數(shù)  
                ImageLoaderConfiguration configuration = ImageLoaderConfiguration  
                         .createDefault(this);  
                   
                 //進行初始化操作
                 ImageLoader.getInstance().init(configuration);  
             }  
           
         }  

注意:

  1. 只能配置一次,如多次配置,則默認第一次的配置參數(shù)。
  2. 這里主要是imageloader的配置參數(shù)是個重點。
    一般用默認配置就ok。但也提供了一些特定的設置,只有真的需要的時候才用。
  3. 下面是全部配置,如果需要單獨設定某個值得時候可以使用。
    具體意思,根據(jù)方法名自己理解,或者百度。這里不再一一講解.
        File cacheDir = StorageUtils.getCacheDirectory(context);  //緩存文件夾路徑
        ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context)
                        .memoryCacheExtraOptions(480, 800) // default = device screen dimensions 內(nèi)存緩存文件的最大長寬
                        .diskCacheExtraOptions(480, 800, null)  // 本地緩存的詳細信息(緩存的最大長寬),最好不要設置這個 
                        .taskExecutor(...)
                        .taskExecutorForCachedImages(...)
                        .threadPoolSize(3) // default  線程池內(nèi)加載的數(shù)量
                        .threadPriority(Thread.NORM_PRIORITY - 2) // default 設置當前線程的優(yōu)先級
                        .tasksProcessingOrder(QueueProcessingType.FIFO) // default
                        .denyCacheImageMultipleSizesInMemory()
                        .memoryCache(new LruMemoryCache(2 * 1024 * 1024)) //可以通過自己的內(nèi)存緩存實現(xiàn)
                        .memoryCacheSize(2 * 1024 * 1024)  // 內(nèi)存緩存的最大值
                        .memoryCacheSizePercentage(13) // default
                        .diskCache(new UnlimitedDiscCache(cacheDir)) // default 可以自定義緩存路徑  
                        .diskCacheSize(50 * 1024 * 1024) // 50 Mb sd卡(本地)緩存的最大值
                        .diskCacheFileCount(100)  // 可以緩存的文件數(shù)量 
                        // default為使用HASHCODE對UIL進行加密命名, 還可以用MD5(new Md5FileNameGenerator())加密
                        .diskCacheFileNameGenerator(new HashCodeFileNameGenerator()) 
                        .imageDownloader(new BaseImageDownloader(context)) // default
                        .imageDecoder(new BaseImageDecoder()) // default
                        .defaultDisplayImageOptions(DisplayImageOptions.createSimple()) // default
                        .writeDebugLogs() // 打印debug log
                .build(); //開始構(gòu)建
  1. 配置加載圖片的屬性,這里主要是一些加載失敗的默認圖片,加載過程中的圖片,緩存位置,bitmap屬性等。這里,我在使用的時候定義了三個DisplayImageOptions對象,分別是加載大圖、中圖、小圖。基本涵蓋了app中圖片的使用。下面給出一個小圖的options
DisplayImageOptions smallImageOptions  = new DisplayImageOptions.Builder()
            .showImageOnLoading(R.drawable.default_pic_small)
            .showImageOnFail(R.drawable.default_pic_small)
            .showImageForEmptyUri(R.drawable.default_pic_small)
            .cacheInMemory(true)
            .cacheOnDisk(true)
            .bitmapConfig(Bitmap.Config.RGB_565)
            .build();
  1. 開始加載圖片
  • 網(wǎng)絡圖片
ImageLoader.getInstance().displayImage(url, imageView, option);
  • drawable圖片(非9.png圖片)
String drawableUrl = Scheme.DRAWABLE.wrap("R.drawable.image"); 
ImageLoader.getInstance().displayImage(drawableUrl , imageView, option);
或者 
ImageLoader.getInstance().displayImage("drawable://" + imageId,
                imageView);
  • 本地圖片
String imageUrl = ImageDownloader.Scheme.FILE.wrap("/mnt/sdcard/image.png");
ImageLoader.getInstance().displayImage(imageUrl, imageView, option);
  • assets
String assetsUrl = Scheme.ASSETS.wrap("image.png");  
ImageLoader.getInstance().displayImage(assetsUrl , imageView, option);
  • Content provider
String contentprividerUrl = "content://media/external/audio/albumart/13";  
ImageLoader.getInstance().displayImage(contentprividerUrl , imageView, option);
  1. GirdView,ListView加載圖片
    當我們快速滑動GridView,ListView,我們希望能停止圖片的加載,而在GridView,ListView停止滑動的時候加載當前界面的圖片。我們只需要在初始化ListView和GridView的時候,加上下面兩行配置:
listView.setOnScrollListener(new PauseOnScrollListener(imageLoader, pauseOnScroll, pauseOnFling));  
gridView.setOnScrollListener(new PauseOnScrollListener(imageLoader, pauseOnScroll, pauseOnFling));  

二、UID的圖片緩存策略

  1. 圖片緩存的概念,簡單點來說當你第一次打開某個界面的時候,圖片可能要等待幾秒鐘才能加載完成,但當你第二次進入時,圖片直接就可以顯示出來,相對于第一次加載快了很多。這里運用的就是圖片緩存技術(shù)。
  2. 三級緩存。一般的策略是先從內(nèi)存中找,沒有的話再去本地disk中找,再沒有的話就從網(wǎng)絡獲取。下面的文章主要說一下內(nèi)存緩存和磁盤緩存,網(wǎng)絡緩存就是重新加載了。三級緩存的特點:
  • 內(nèi)存緩存 優(yōu)先加載,速度最快
  • 本地緩存 次優(yōu)先加載 速度稍快
  • 網(wǎng)絡緩存 最后加載 速度由網(wǎng)絡速度決定(浪費流量)

三、內(nèi)存緩存

  1. 內(nèi)存緩存中的強引用和弱引用
  • 強引用是指創(chuàng)建一個對象并把這個對象賦給一個引用變量, 強引用有引用變量指向時永遠不會被垃圾回收。即使內(nèi)存不足的時候?qū)幵笀驩OM也不被垃圾回收器回收,我們new的對象都是強引用
  • 弱引用通過weakReference類來實現(xiàn),它具有很強的不確定性,如果垃圾回收器掃描到有著WeakReference的對象,就會將其回收釋放內(nèi)存
  1. UIL中的內(nèi)存緩存策略
    UIL提供的內(nèi)存緩存策略有下面幾種,大家可以自己去看下源碼,非常簡單。
    (1)使用的是強引用緩存
    • LruMemoryCache(這個類就是這個開源框架默認的內(nèi)存緩存類,緩存的是bitmap的強引用,下面我會從源碼上面分析這個類)

(2) 使用強引用和弱引用相結(jié)合的緩存有
- UsingFreqLimitedMemoryCache(如果緩存的圖片總量超過限定值,先刪除使用頻率最小的bitmap)
- LRULimitedMemoryCache(這個也是使用的lru算法,和LruMemoryCache不同的是,他緩存的是bitmap的弱引用)
- FIFOLimitedMemoryCache(先進先出的緩存策略,當超過設定值,先刪除最先加入緩存的bitmap)
- LargestLimitedMemoryCache(當超過緩存限定值,先刪除最大的bitmap對象)
- LimitedAgeMemoryCache(當 bitmap加入緩存中的時間超過我們設定的值,將其刪除)

(3) 只使用弱引用緩存
- WeakMemoryCache(這個類緩存bitmap的總大小沒有限制,唯一不足的地方就是不穩(wěn)定,緩存的圖片容易被回收掉)

  1. UIL手動修改內(nèi)存緩存策略
    在Application中初始化ImageLoaderConfiguration的時候,加上memoryCache就可以了。
ImageLoaderConfiguration configuration = new ImageLoaderConfiguration.Builder(this)  
        .memoryCache(new WeakMemoryCache())  
        .build();  
  1. LruMemoryCache 類解析
    這里已LruMemoryCache 類為例進行分析,源碼的話,就不貼了。只列出關鍵的代碼。
    private final LinkedHashMap<String, Bitmap> map;  
    private final int maxSize;  
    /** Size of this cache in bytes */  
    private int size;  

LruMemoryCache 緩存了一個LinkedHashMap<String, Bitmap> map對象,用來保存圖片。同時還定義了一個最大緩存值,以及一個當前的緩存值。

 /** 
     * Remove the eldest entries until the total of remaining entries is at or below the requested size. 
     * 
     * @param maxSize the maximum size of the cache before returning. May be -1 to evict even 0-sized elements. 
     */  
    private void trimToSize(int maxSize) {  
        while (true) {  
            String key;  
            Bitmap value;  
            synchronized (this) {  
                if (size < 0 || (map.isEmpty() && size != 0)) {  
                    throw new IllegalStateException(getClass().getName() + ".sizeOf() is reporting inconsistent results!");  
                }  
  
                if (size <= maxSize || map.isEmpty()) {  
                    break;  
                }  
  
                Map.Entry<String, Bitmap> toEvict = map.entrySet().iterator().next();  
                if (toEvict == null) {  
                    break;  
                }  
                key = toEvict.getKey();  
                value = toEvict.getValue();  
                map.remove(key);  
                size -= sizeOf(key, value);  
            }  
        }  
    } 

如果當前緩存的bitmap總數(shù)小于設定值maxSize,不做任何處理,如果當前緩存的bitmap總數(shù)大于maxSize,刪除LinkedHashMap中的第一個元素,size中減去該bitmap對應的byte數(shù)

四、硬盤緩存

  1. UIL提供的硬盤緩存策略
  • FileCountLimitedDiscCache(可以設定緩存圖片的個數(shù),當超過設定值,刪除掉最先加入到硬盤的文件)
  • LimitedAgeDiscCache(設定文件存活的最長時間,當超過這個值,就刪除該文件)
  • TotalSizeLimitedDiscCache(設定緩存bitmap的最大值,當超過這個值,刪除最先加入到硬盤的文件)
  • UnlimitedDiscCache(這個緩存類沒有任何的限制)

五、UIL加載一張網(wǎng)絡圖片的過程

  1. 先看下我們平常使用加載圖片的代碼:
    ImageLoader.getInstance().displayImage(imageUrl, imageView, options);
  2. 再看下displayImage的源碼:
    public void displayImage(String uri, ImageView imageView, DisplayImageOptions options) {
        displayImage(uri, new ImageViewAware(imageView), options, null, null);
    }

這個ImageViewAware是什么?看下源碼的注釋:
"Wrapper for Android {@link android.widget.ImageView ImageView}. Keeps weak reference of ImageView to prevent memory leaks."
將ImageView的強引用變成弱引用,當內(nèi)存不足的時候,可以更好的回收ImageView對象,還有就是獲取ImageView的寬度和高度。使得我們可以根據(jù)ImageView的寬高去對圖片進行一個裁剪,減少內(nèi)存的使用。

看下ImageLoader中的displayImage方法,上代碼:

/**
     * Adds display image task to execution pool. Image will be set to ImageAware when it's turn.<br />
     * <b>NOTE:</b> {@link #init(ImageLoaderConfiguration)} method must be called before this method call
     *
     * @param uri              Image URI (i.e. "http://site.com/image.png", "file:///mnt/sdcard/image.png")
     * @param imageAware       {@linkplain com.nostra13.universalimageloader.core.imageaware.ImageAware Image aware view}
     *                         which should display image
     * @param options          {@linkplain com.nostra13.universalimageloader.core.DisplayImageOptions Options} for image
     *                         decoding and displaying. If <b>null</b> - default display image options
     *                         {@linkplain ImageLoaderConfiguration.Builder#defaultDisplayImageOptions(DisplayImageOptions)
     *                         from configuration} will be used.
     * @param listener         {@linkplain ImageLoadingListener Listener} for image loading process. Listener fires
     *                         events on UI thread if this method is called on UI thread.
     * @param progressListener {@linkplain com.nostra13.universalimageloader.core.listener.ImageLoadingProgressListener
     *                         Listener} for image loading progress. Listener fires events on UI thread if this method
     *                         is called on UI thread. Caching on disk should be enabled in
     *                         {@linkplain com.nostra13.universalimageloader.core.DisplayImageOptions options} to make
     *                         this listener work.
     * @throws IllegalStateException    if {@link #init(ImageLoaderConfiguration)} method wasn't called before
     * @throws IllegalArgumentException if passed <b>imageAware</b> is null
     */
    public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
            ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
        checkConfiguration();
        if (imageAware == null) {
            throw new IllegalArgumentException(ERROR_WRONG_ARGUMENTS);
        }
        if (listener == null) {
            listener = emptyListener;
        }
        if (options == null) {
            options = configuration.defaultDisplayImageOptions;
        }

        if (TextUtils.isEmpty(uri)) {
            engine.cancelDisplayTaskFor(imageAware);
            listener.onLoadingStarted(uri, imageAware.getWrappedView());
            if (options.shouldShowImageForEmptyUri()) {
                imageAware.setImageDrawable(options.getImageForEmptyUri(configuration.resources));
            } else {
                imageAware.setImageDrawable(null);
            }
            listener.onLoadingComplete(uri, imageAware.getWrappedView(), null);
            return;
        }

        ImageSize targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, configuration.getMaxImageSize());
        String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize);
        engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);

        listener.onLoadingStarted(uri, imageAware.getWrappedView());

        Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);
        if (bmp != null && !bmp.isRecycled()) {
            L.d(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey);

            if (options.shouldPostProcess()) {
                ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
                        options, listener, progressListener, engine.getLockForUri(uri));
                ProcessAndDisplayImageTask displayTask = new ProcessAndDisplayImageTask(engine, bmp, imageLoadingInfo,
                        defineHandler(options));
                if (options.isSyncLoading()) {
                    displayTask.run();
                } else {
                    engine.submit(displayTask);
                }
            } else {
                options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
                listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);
            }
        } else {
            if (options.shouldShowImageOnLoading()) {
                imageAware.setImageDrawable(options.getImageOnLoading(configuration.resources));
            } else if (options.isResetViewBeforeLoading()) {
                imageAware.setImageDrawable(null);
            }

            ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
                    options, listener, progressListener, engine.getLockForUri(uri));
            LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo,
                    defineHandler(options));
            if (options.isSyncLoading()) {
                displayTask.run();
            } else {
                engine.submit(displayTask);
            }
        }
    }

我大致說下處理流程:

  1. 檢查配置是否初始化;
  2. 檢查imageAware,也就是ImageView是否為空;
  3. 檢查ImageLoadingListener和ImageLoadingProgressListener是否為空,如果為空就使用默認的配置。
  4. 判斷圖片地址。如果為空就顯示配置的空圖片,并取消網(wǎng)絡任務,同時調(diào)用ImageLoadingListener的onLoadingComplete方法,結(jié)束本次加載。如果圖片地址不為空,那么進行下面的步驟
  5. 將ImageView的寬高封裝成ImageSize對象.如果獲取ImageView的寬高為0,就會使用手機屏幕的寬高作為ImageView的寬高。
  6. 從內(nèi)存中查找這個圖片。如果從內(nèi)存中找到了,那么再判斷顯示圖片的options.shouldPostProcess()是否為true。postProcessor這個屬性主要是判斷是否需要對取到的這個圖片進行處理,比如切成圓角邊等。這個對應的需要自己實現(xiàn)BitmapProcessor接口來處理這個bitmap。如果需要處理圖片,就進行處理,然后顯示。如果不需要處理,就直接顯示到imageAware上去。加載完成。
  7. 如果沒有從內(nèi)存中找到這張圖片,那么就實例化一個LoadAndDisplayImageTask對象。這個對象實現(xiàn)了Runnable,如果配置了isSyncLoading為true, 直接執(zhí)行LoadAndDisplayImageTask的run方法,表示同步,默認是false,將LoadAndDisplayImageTask提交給線程池對象。這個對象的主要作用是去disk或者網(wǎng)絡中去加載圖片。

下面看下LoadAndDisplayImageTask是如何從disk或者網(wǎng)絡加載圖片的。先看它的run方法

    @Override
    public void run() {
        if (waitIfPaused()) return;
        if (delayIfNeed()) return;

        ReentrantLock loadFromUriLock = imageLoadingInfo.loadFromUriLock;
        L.d(LOG_START_DISPLAY_IMAGE_TASK, memoryCacheKey);
        if (loadFromUriLock.isLocked()) {
            L.d(LOG_WAITING_FOR_IMAGE_LOADED, memoryCacheKey);
        }

        loadFromUriLock.lock();
        Bitmap bmp;
        try {
            checkTaskNotActual();

            bmp = configuration.memoryCache.get(memoryCacheKey);
            if (bmp == null || bmp.isRecycled()) {
                bmp = tryLoadBitmap();
                if (bmp == null) return; // listener callback already was fired

                checkTaskNotActual();
                checkTaskInterrupted();

                if (options.shouldPreProcess()) {
                    L.d(LOG_PREPROCESS_IMAGE, memoryCacheKey);
                    bmp = options.getPreProcessor().process(bmp);
                    if (bmp == null) {
                        L.e(ERROR_PRE_PROCESSOR_NULL, memoryCacheKey);
                    }
                }

                if (bmp != null && options.isCacheInMemory()) {
                    L.d(LOG_CACHE_IMAGE_IN_MEMORY, memoryCacheKey);
                    configuration.memoryCache.put(memoryCacheKey, bmp);
                }
            } else {
                loadedFrom = LoadedFrom.MEMORY_CACHE;
                L.d(LOG_GET_IMAGE_FROM_MEMORY_CACHE_AFTER_WAITING, memoryCacheKey);
            }

            if (bmp != null && options.shouldPostProcess()) {
                L.d(LOG_POSTPROCESS_IMAGE, memoryCacheKey);
                bmp = options.getPostProcessor().process(bmp);
                if (bmp == null) {
                    L.e(ERROR_POST_PROCESSOR_NULL, memoryCacheKey);
                }
            }
            checkTaskNotActual();
            checkTaskInterrupted();
        } catch (TaskCancelledException e) {
            fireCancelEvent();
            return;
        } finally {
            loadFromUriLock.unlock();
        }

        DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
        runTask(displayBitmapTask, syncLoading, handler, engine);
    }
  1. if (waitIfPaused()) return;這句話主要是用在ListView或者GridView在滑動過程中,不去加載圖片。這里需要配合
    ListView.setOnScrollListener(new PauseOnScrollListener(pauseOnScroll, pauseOnFling ))
  2. if (waitIfPaused()) return;和if (delayIfNeed()) return;的返回值最終都由一個方法isTaskNotActual決定。看下這個方法的源碼
private boolean isTaskNotActual() {
        return isViewCollected() || isViewReused();
    }

isViewCollected()是判斷我們ImageView是否被垃圾回收器回收了,如果回收了,LoadAndDisplayImageTask方法的run()就直接返回了,isViewReused()判斷該ImageView是否被重用,被重用run()方法也直接返回。被重用的情況主要發(fā)生在ListView和GridView當中,如果當前加載的ImageView已經(jīng)被重用了,那么就沒必要繼續(xù)加載之前的圖片了,所以直接返回。

  1. 檢查完可以去加載圖片了。先加一個鎖ReentrantLock。這個鎖的作用就是相同url的請求只執(zhí)行一次。比如listview中的一個item需要從網(wǎng)上加載圖片。我們用手機滑動ListView讓這個item,一會滑入界面,然后滑出后再馬上滑入。重復幾次的話,就會有很多個相同url的請求。加了這個鎖的作用,這些請求只執(zhí)行一遍,而不是全部去執(zhí)行一遍。
    再看下用了這個url請求后干了什么
  2. checkTaskNotActual()。這個也是檢查view是否被回收和復用。
  3. 再從緩存中檢查一遍,如果緩存中沒有,那么去tryLoadBitmap();這個就是加載的方法。大概的流程就是先從disk緩存中獲取圖片,如果沒有再去從網(wǎng)絡中獲取,然后將bitmap保存在文件系統(tǒng)中。
  4. 從服務器上獲取圖片保存到本地是調(diào)用了tryCacheImageOnDisk這個方法。這個方法里調(diào)用了downloadImage這個方法去下載圖片。
  5. 剩下的就是顯示圖片了。 同樣會檢查一遍view有沒有被回收,被復用。

至此,顯示圖片的過程就結(jié)束了。

筆者在寫這篇博客的過程中,從以下兩篇文章中學到了很多有價值的東西,十分感謝!
Android 開源框架Universal-Image-Loader完全解析
Android開源框架Universal-Image-Loader詳解

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

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