通俗易懂的SDWebImage源碼解析

  • Asynchronous image downloader with cache support as a UIImageView category
    *一個異步的圖片下載與緩存的UIImageViewCategory

對于它究竟是如何工作的,相信大家應該或多或少都已經有所了解。但是它內部是怎么實現,又有那些細節,我卻一直犯懶沒有真正的好好去研究過,最近不是很忙,于是我就仔細的研究了一下它的實現細節,這里我源碼的版本為4.0.0,也就是當前最新的版本。

**在這里我推薦大家去github下載對應的源碼,一邊看blog一邊看對應的源碼,最好再做上自己的注釋,這樣會看的更快,且做做筆記會加深自己的映像,SDWebImage:github地址:https://github.com/rs/SDWebImage **

好了,廢話不多說,直接來看代碼吧。
下面的代碼是我們經常使用的SDWebImage的方法之一,給imageView傳入對應的圖片url和占位圖片,它就幫我們實現了圖片的所有操作。

點進它的具體實現,可以看到它是一個UIImageView的分類,分類的調用方法如下,我已經給對應的參數做出了對應的翻譯:

/**
 * 使用一個url,占位圖片和自定義選項來設置imageView
 * 下載是異步且緩存的
 * url                          圖像的url
 * placeholder                  占位圖片,初始化時被設置,在請求結束時消失
 * options                      在下載圖片的時候使用的選項,看SDWebImageOptions有哪些可能的值
 * progressBlock                當圖像下載時候調用的block,這個block在一個后臺隊列執行
 * completedBlock               當操作結束時調用的block,這個block沒有返回值,把請求到的圖像作為第一個參數,如果發生錯誤的話,第一個參數為空,第二個參數會包含一個NSError對象,第三個參數是一個bool值,指是從本地緩存還是從網絡來重新獲取圖像,第四個參數是圖片原始的url
 */
- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                 completed:(nullable SDExternalCompletionBlock)completedBlock {
    [self sd_internalSetImageWithURL:url
                    placeholderImage:placeholder
                             options:options
                        operationKey:nil
                       setImageBlock:nil
                            progress:progressBlock
                           completed:completedBlock];
}

相信大家對上面的參數,并不陌生,即使曾經沒研究過,看到對應的名稱和注釋也能大概猜出它們的作用,這里唯一不太了解的應該是options的含義了。

options是一個枚舉,下面是options對應的值,作用已經添加在注釋中了

typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
    /**
     * 默認情況下,如果一個url在下載的時候失敗了,那么這個url會被加入黑名單并且library不會嘗試再次下載,這個flag會阻止library把失敗的url加入黑名單(簡單來說如果選擇了這個flag,那么即使某個url下載失敗了,sdwebimage還是會嘗試再次下載他.)
     */
    SDWebImageRetryFailed = 1 << 0,

    /**
     * UI交互期間下載
     * 導致延遲下載在UIScrollView減速的時候,(也就是你滑動的時候scrollview不下載,你手從屏幕上移走,scrollview開始減速的時候才會開始下載圖片)
     */
    SDWebImageLowPriority = 1 << 1,

    /**
     * 只進行內存緩存,不進行磁盤緩存
     */
    SDWebImageCacheMemoryOnly = 1 << 2,

    /**
     * 這個標志可以漸進式下載,顯示的圖像是逐步在下載(就像你用瀏覽器瀏覽網頁的時候那種圖片下載,一截一截的顯示
     */
    SDWebImageProgressiveDownload = 1 << 3,

    /**
     * 即使圖像緩存,也要遵守HTTP響應緩存控制,如果需要,可以從遠程位置刷新圖像
     * 磁盤緩存將由NSURLCache而不是SDWebImage處理,導致輕微的性能降低。
     * 這個選項幫助處理在同樣的網絡請求地址下圖片的改變
     * 如果刷新緩存的圖像,完成的block會在使用緩存圖像的時候調用,還會在最后的圖像被調用
     * 當你不能使你的URL靜態與嵌入式緩存
     */
    SDWebImageRefreshCached = 1 << 4,

    /**
     * 在iOS4以上,如果app進入后臺,也保持下載圖像,這個需要取得用戶權限
     * 如果后臺任務過期,操作將被取消
     */
    SDWebImageContinueInBackground = 1 << 5,

    /**
     * 操作cookies存儲在NSHTTPCookieStore通過設置NSMutableURLRequest.HTTPShouldHandleCookies = YES
     */
    SDWebImageHandleCookies = 1 << 6,

    /**
     * 允許使用無效的SSL證書
     * 用戶測試,生成情況下小心使用
     */
    SDWebImageAllowInvalidSSLCertificates = 1 << 7,

    /**
     * 優先下載
     */
    SDWebImageHighPriority = 1 << 8,
    
    /**
     * 在加載圖片時加載占位圖。 此標志將延遲加載占位符圖像,直到圖像完成加載。
     */
    SDWebImageDelayPlaceholder = 1 << 9,

    /**
     * 我們通常不調用transformDownloadedImage代理方法在動畫圖像上,大多數情況下會對圖像進行耗損
     * 無論什么情況下都使用
     */
    SDWebImageTransformAnimatedImage = 1 << 10,
    
    /**
     * 圖片在下載后被加載到imageView。但是在一些情況下,我們想要設置一下圖片(引用一個濾鏡或者加入透入動畫)
     * 使用這個來手動的設置圖片在下載圖片成功后
     */
    SDWebImageAvoidAutoSetImage = 1 << 11,
    
    /**
     * 圖像將根據其原始大小進行解碼。 在iOS上,此標記會將圖片縮小到與設備的受限內存兼容的大小。
     */
    SDWebImageScaleDownLargeImages = 1 << 12
};

看完上面的枚舉值,大家應該還是不知道有什么作用,沒關系,接著往下看。

繼續往后可以看到,最終它真正調用的是UIView+WebCache.h的方法,這里就是要詳細講解的第一個方法,在下面的代碼中我已經貼了一些注釋來方便講解:

- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
                  placeholderImage:(nullable UIImage *)placeholder
                           options:(SDWebImageOptions)options
                      operationKey:(nullable NSString *)operationKey
                     setImageBlock:(nullable SDSetImageBlock)setImageBlock
                          progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                         completed:(nullable SDExternalCompletionBlock)completedBlock {
    // 獲取可用的operationKey
    NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
    // 取消該key對應的任務
    [self sd_cancelImageLoadOperationWithKey:validOperationKey];
    // 給該視圖的實例對象設置一個屬性
    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
    // 如果options不為SDWebImageDelayPlaceholder,那么先把placeholder設置到該視圖上
    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
        });
    }
    
    if (url) {
        // check if activityView is enabled or not
        // 如果有url,且設置顯示ActivityIndicator,那么顯示
        if ([self sd_showActivityIndicatorView]) {
            [self sd_addActivityIndicator];
        }
        
        __weak __typeof(self)wself = self;
        // ??這里的operation不是繼承自NSOperation的,我們可以把它看做一個關聯視圖操作的對象,我們稱它為op對象
        id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            __strong __typeof (wself) sself = wself;
            // 圖像下載成功后,移除ActivityIndicator
            [sself sd_removeActivityIndicator];
            if (!sself) {
                return;
            }
            dispatch_main_async_safe(^{
                if (!sself) {
                    return;
                }
                // 如果有image且options為SDWebImageAvoidAutoSetImage且有completedBlock
                if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) {
                    // 在這里獲取到圖片,且做一些加工的操作
                    completedBlock(image, error, cacheType, url);
                    return;
                } else if (image) {
                    // 如果有image,設置視圖的圖像
                    [sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                    // 標記設為需要布局
                    [sself sd_setNeedsLayout];
                } else {
                    // image已經嘗試獲取過了,但是沒有從網絡端獲取到
                    // 如果options為SDWebImageDelayPlaceholder,當前視圖設置為占位圖片
                    // 標記設為需要布局
                    if ((options & SDWebImageDelayPlaceholder)) {
                        [sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                        [sself sd_setNeedsLayout];
                    }
                }
                // 有completedBlock且下載finished為yes,將需要的參數傳出去
                if (completedBlock && finished) {
                    completedBlock(image, error, cacheType, url);
                }
            });
        }];
        // 將現在的op對象加到對應的視圖實例中
        [self sd_setImageLoadOperation:operation forKey:validOperationKey];
    } else {
        // 如果url為空,拋出錯誤
        dispatch_main_async_safe(^{
            [self sd_removeActivityIndicator];
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
                completedBlock(nil, error, SDImageCacheTypeNone, url);
            }
        });
    }
}

下面我們將一步步來解釋這些代碼的含義:
1.首先先獲取validOperationKey,如果為空,那么就獲取到當前類的名稱,查看UIImageView+WebCache.h對應的傳入參數,可以發現UIImageView傳入的對應validOperationKeynil,也就是說默認情況下,如果我們不直接給validOperationKey賦值,它就為nil,那么這里獲得的validOperationKey一般也就是對應類的class,也就是說如果是UIImageView調用這個方法,那么對應的validOperationKey也就是UIImageView。

NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);

2.取消該key對應的任務

[self sd_cancelImageLoadOperationWithKey:validOperationKey];

什么情況,怎么還沒開始做事情就開始取消了?

在這里我們做一個標記,一會來解釋
??標記1:- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key是什么意思,為什么一來就取消,有什么作用?

3.給該視圖的實例對象設置一個屬性,這里的知識是使用了runtime,如果對runtime不夠了解的,可以參看資料:讓你快速上手Runtime

通俗點講:這里的作用就是給UIView的實例添加了@property (nonatomic, strong) NSString *url;,只是這個屬性的獲取方式是通過key/value的方式來獲得的,url這個value對應的key&imageURLKey

objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

4.接下來就是設置placeholder,如果不想讓SDWebImage來幫你設置占位圖片,就給它傳入setImageBlock來自定義設置占位圖片。

// 如果options不為SDWebImageDelayPlaceholder,那么先把placeholder設置到該視圖上
    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
        });
    }

這里有兩個需要講解的

  • options & SDWebImageDelayPlaceholder: &是按位與
    舉個例子:a & b a=1 b=2 a== 0000 0001(二進制) b== 0000 0010(二進制) a & b = 0000 0000(二進制)
    放在這里就是,如果options中包含SDWebImageDelayPlaceholder,那么就不設置占位圖。
  • dispatch_main_async_safe:這是一個定義的宏
    如果當前是主進程,就直接執行block,否則把block放到主進程運行。為什么要判斷是否是主進程?因為iOS上任何UI的操作都在主線程上執行,所以主進程還有一個名字,叫做“UI進程”。

ifndef dispatch_main_async_safe

define dispatch_main_async_safe(block)\

if (strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(dispatch_get_main_queue())) == 0) {\
    block();\
} else {\
    dispatch_async(dispatch_get_main_queue(), block);\
}

endif


5.下面的操作是根據url來加載網絡圖片,分為有`url`有值和`url`無值的情況
```obj
  if (url) {
      // check if activityView is enabled or not
      if ([self sd_showActivityIndicatorView]) {
          [self sd_addActivityIndicator];
      }
      
      __weak __typeof(self)wself = self;
      id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
          __strong __typeof (wself) sself = wself;
          [sself sd_removeActivityIndicator];
          if (!sself) {
              return;
          }
          dispatch_main_async_safe(^{
              if (!sself) {
                  return;
              }
              if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) {
                  completedBlock(image, error, cacheType, url);
                  return;
              } else if (image) {
                  [sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                  [sself sd_setNeedsLayout];
              } else {
                  if ((options & SDWebImageDelayPlaceholder)) {
                      [sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                      [sself sd_setNeedsLayout];
                  }
              }
              if (completedBlock && finished) {
                  completedBlock(image, error, cacheType, url);
              }
          });
      }];
      [self sd_setImageLoadOperation:operation forKey:validOperationKey];
  } else {
      dispatch_main_async_safe(^{
          [self sd_removeActivityIndicator];
          if (completedBlock) {
              NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
              completedBlock(nil, error, SDImageCacheTypeNone, url);
          }
      });
  }
  • 先來分析url無值的情況,也就是上面代碼中的else,可以很清晰的看到先會調用[self sd_removeActivityIndicator];,根據名字我們大概能猜到是移除一個ActivityIndicator,然后會使用完成的block在主線程拋出一個NSError對象。

  • 現在來看url有值的情況,首先

          // 如果有url,且設置顯示ActivityIndicator,那么顯示
          if ([self sd_showActivityIndicatorView]) {
              [self sd_addActivityIndicator];
          }
    

    然后通過SDWebImageManager的單例對象調用下面的方法,返回了一個名為operationid類型的對象

    - (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                       options:(SDWebImageOptions)options
                                      progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                     completed:(nullable SDInternalCompletionBlock)completedBlock;
    

    先不看這個方法的實現,先猜一猜這個方法是做什么的?

我想你肯定已經猜到了,這個方法就是下載圖片且給UIImageView設置圖片的方法

現在先來看看這個方法完成的block中的代碼:

            __strong __typeof (wself) sself = wself;
            // 圖像下載成功后,移除ActivityIndicator
            [sself sd_removeActivityIndicator];
            // 如果self為nil,直接返回
            if (!sself) {
                return;
            }

然后如果獲取到圖片,options中包含SDWebImageAvoidAutoSetImage,且完成的block不為空的情況下,直接調用完成block返回

// 如果有image且options為SDWebImageAvoidAutoSetImage且有completedBlock
if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) {
  // 在這里獲取到圖片,且做一些加工的操作
  completedBlock(image, error, cacheType, url);
  return;
}

如果沒有獲取到optionsSDWebImageAvoidAutoSetImage,但是獲取到了image,直接設置對應視圖的image

else if (image) {
    // 如果有image,設置視圖的圖像
    [sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
    // 標記設為需要布局
    [sself sd_setNeedsLayout];
}

然后就是當image沒有獲取到的時候的操作,如果之前設置的optionsSDWebImageDelayPlaceholder(也就是延遲加載占位圖),那么現在也應該把占位圖設置上了

else {
    // image已經嘗試獲取過了,但是沒有從網絡端獲取到
    // 如果options為SDWebImageDelayPlaceholder,當前視圖設置為占位圖片
    if ((options & SDWebImageDelayPlaceholder)) {
        [sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
        [sself sd_setNeedsLayout];
    }

最后,在所有的判斷結束以后,通過completedBlock將對應的參數傳遞出去

// 有completedBlock且下載finished為yes,將需要的參數傳出去
if (completedBlock && finished) {
  completedBlock(image, error, cacheType, url);
}

在url不為nil的邏輯代碼的最后,將前面生成的operation和最開始獲取到的validOperationKey設置到對應的視圖,也就是下面的代碼?。?!

在這里我們再做一個標記,下面來解釋
??標記2:- (void)sd_setImageLoadOperation:(nullable id)operation forKey:(nullable NSString *)key又是什么意思,和上面的標記1有什么關系?

// 將現在的op對象加到對應的視圖實例中
[self sd_setImageLoadOperation:operation forKey:validOperationKey];

上面對對應的邏輯進行大概的梳理,大家應該學習到了一些,但是有些地方肯定還是不清楚,所以看下面吧

下面是解決問題的時間

第一個問題

  • 首先看到??標記1和上面的??標記2
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
[self sd_setImageLoadOperation:operation forKey:validOperationKey];

在所有操作剛開始執行的時候,視圖就執行了個取消的操作,最后又給視圖增加了一個operation,這到底是怎么回事?

1.根據經驗,如果要給一個UIImageView設置image,那么肯定要獲取到對應的image,如果這是一個網絡圖片,那么肯定是要將這個圖片下載,然后下載好了,再將圖片設置到對應的UIImageView,相信大家對這個邏輯是沒有異議的。

2.現在下載圖片對應的操作就是id <SDWebImageOperation> operation來執行,一開始的取消操作就是取消了這樣一個任務
注意:這里的operation可不是繼承自NSOperation的對象,而是一個繼承自NSObject的對象,你可以將它看做一個操作圖片更新的對象

3.看一下- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key對應的實現,首先通過[self operationDictionary]獲取到存有operation的字典(這里的字典也是通過runtime動態來添加的),然后通過對應的key取出對應的operation,調用cancel來取消對應的操作,然后通過key移除對應的operation

- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
    // Cancel in progress downloader from queue
    // operation的字典
    SDOperationsDictionary *operationDictionary = [self operationDictionary];
    id operations = operationDictionary[key];
    if (operations) {
        if ([operations isKindOfClass:[NSArray class]]) {
            for (id <SDWebImageOperation> operation in operations) {
                if (operation) {
                    [operation cancel];
                }
            }
        } else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
            [(id<SDWebImageOperation>) operations cancel];
        }
        [operationDictionary removeObjectForKey:key];
    }
}

4.接著看一下對應的設置方法,設置方法中先調用了sd_cancelImageLoadOperationWithKey,然后再將對應的operation添加到了字典中

- (void)sd_setImageLoadOperation:(nullable id)operation forKey:(nullable NSString *)key {
    if (key) {
        [self sd_cancelImageLoadOperationWithKey:key];
        if (operation) {
            SDOperationsDictionary *operationDictionary = [self operationDictionary];
            operationDictionary[key] = operation;
        }
    }
}

下面我舉個例子來講一下這么做的作用:

在常用的tableViewcell上有圖片是再常見不過的了,如下所示的這種cell

cell.png

  • 在我們使用SDWebImage給上面的cell中的imageview設置網絡圖片的時候,圖片的下載是異步的,那么如果現在給當前cell設置的為cell.imageviewa.png,隨著tableView的滑動,這個cell會被復用,復用后現在cell.imageviewb.png,這里的a.pngb.png都是從網絡上異步下載的,不是本地的資源圖片
  • 一開始cellindex為1,imagea,復用以后cellindex為6,imageb,按道理來說圖片應該先為a,然后為b,但是a很大,b很小,b都已經下載好了,a還沒有下載好,當滑動到顯示index為6的cell的時候,cell的圖片先顯示的b,因為b已經下載好了,過了一會,a也下載好了
    那么神奇的事情發生了,index為6的cell中的圖片ab覆蓋了,應該顯示b的變成顯示a
  • 整個數據都亂了,這實在太可怕了

如果上面我舉的例子沒看懂,請反復多看幾遍??!

好,我現在認為你已經看懂了~

[self sd_cancelImageLoadOperationWithKey:validOperationKey];
[self sd_setImageLoadOperation:operation forKey:validOperationKey];

上面??的兩個方法就是為了防止這種情況的發生,因此先取消對應的圖片操作,再重新添加,剛開始先通過key獲取operation,如果有operation對象---->取消。當重新產生一個operation對象以后,還是看對應的字典中有沒有,有----> 取消(因為現在還沒將新產生的operation添加到字典中),沒有--->operationDictionary[key] = operation;,將這個operation放到字典中,這樣就可以保證一個視圖對象只有一個operation在操作圖像
在這里也就是說如果設置了cell的網絡圖片為b,那么就取消掉之前的a的相關操作,這樣就不會出現顯示錯亂的問題了。
作者的想法真的是很聰明呀!

第二個問題

在SDWebImage中常??梢钥吹?code>options & SDWebImageRefreshCached這種寫法,查看SDWebImageRefreshCached的定義可以看到SDWebImageRefreshCached = 1 << 4
例如:a=1 b=2 a== 0000 0001(二進制) b== 0000 0010(二進制) a & b = 0000 0000 (二進制) 十進制為0
也就是說SDWebImageRefreshCached是將1左移4位的一個值,二進制表示為00010000,十進制為16

在接下來的代碼中還會看到downloaderOptions | SDWebImageDownloaderLowPriority,這種寫法是按位或,也是位運算的一種:
例如:a=5,b=11; 5 ==0000 0101 (二進制) 10==0000 1011(二進制) a | b== 0000 1111(二進制) 十進制為15

如果想了解更多的相關知識,可以參考這篇博客:按位與,按位或

總結

我用了一張流程圖來表示這篇文章的內容,方便大家查看

流程圖.png

以上是一些我的個人理解,如果有什么不對的地方也希望大家能夠指出,互相學習!
這是SDWebImage源碼解析的第一篇,下一篇將會對下面產生operation的方法進行分析,歡迎大家關注!

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

推薦閱讀更多精彩內容