Objective-C 數組遍歷的性能及原理

數組的遍歷,這個話題貌似沒什么好探究的,該怎么遍歷就怎么遍歷唄!但是如果要回答這些問題:
OC數組有哪幾種遍歷方式?
哪種方式效率最高?為什么?
各種遍歷方式的內部實現是怎么樣的?
NS(Mutable)Array的內部結構是怎么樣的?
我覺得還是需要探究一下.

一.OC數組的類體系

當我們創建一個NSArray對象時,實際上得到的是NSArray的子類__NSArrayI對象.同樣的,我們創建NSMutableArray對象,得到的同樣是其子類__NSArrayM對象.有趣的是,當我們創建只有一個對象的NSArray時,得到的是__NSSingleObjectArrayI類對象.
__NSArrayI__NSArrayM,__NSSingleObjectArrayI為框架隱藏的類.

OC數組的類體系如下:


通過NSArray和NSMutableArray接口,返回的卻是子類對象,怎么做到的?
先介紹另一個私有類:__NSPlaceholderArray,和兩個此類的全局變量___immutablePlaceholderArray,___mutablePlaceholderArray。__NSPlaceholderArray從類命名上看,它只是用來占位的,具體怎么占位法稍后討論,有個重要特點是,__NSPlaceholderArray實現了和NSArray,NSMutableArray一摸一樣的初始化方法,如initWithObjects:count:,initWithCapacity:等.

介紹完__NSPlaceholderArray后,這個機制可以總結為以下兩個大步驟:
(1).NSArray重寫了+ (id)allocWithZone:(struct _NSZone *)zone方法,在方法內部,如果調用類為NSArray則直接返回全局變量___immutablePlaceholderArray,如果調用類為NSMUtableArray則直接返回全局變量___mutablePlaceholderArray。
也就是調用[NSArray alloc]或者[NSMUtableArray alloc]得到的僅僅是兩個占位指針,類型為__NSPlaceholderArray.
(2).在調用了alloc的基礎上,不論是NSArrayNSMutableArray都必定要繼續調用某個initXXX方法,而實際上調用的是__NSPlaceholderArrayinitXXX.在這個initXXX方法內部,如果self == ___immutablePlaceholderArray就會重新構造并返回__NSArrayI 對象,如果self == ___mutablePlaceholderArray就會重新構造并返回_NSArrayM對象.

總結來說,對于NSArrayNSMutableArray,alloc時拿到的僅僅是個占位對象,init后才得到真實的子類對象.

接下來清點一下幾種遍歷方式:

二.OC數組遍歷的幾種方式

1.for 循環

for (NSUInteger i = 0;  i < array.count; ++i) {
        object = array[i];
  }

array[i]會被編譯器轉化為對- (ObjectType)objectAtIndexedSubscript:(NSUInteger)index的調用,此方法內部調用的就是- (ObjectType)objectAtIndex:(NSUInteger)index方法.
2.for in

for (id obj in array) {
        xxx
  }

文章稍后會討論到for in的內部實現
3.enumerateObjectsUsingBlock
通過block回調順序遍歷:

[array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
       xxx
  }];

4.enumerateObjectsWithOptions:usingBlock:
通過block回調,在子線程中遍歷,對象的回調次序是亂序的,而且調用線程會等待該遍歷過程完成:

[array enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        xxx
  }];

通過block回調,在主線程中逆序遍歷:

[array enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        xxx
  }];

5.objectEnumerator/reverseObjectEnumerator
通過Enumerator順序遍歷:

NSEnumerator *enumerator = array.objectEnumerator;
while((object = enumerator.nextObject)) {
    xxx
}

通過ReverseEnumerator逆序遍歷:

NSEnumerator *enumerator = array.reverseObjectEnumerator;
while((object = enumerator.nextObject)) {
    xxx
}

6.enumerateObjectsAtIndexes:options:usingBlock:
通過block回調,在子線程中對指定IndexSet遍歷,對象的回調次序是亂序的,而且調用線程會等待該遍歷過程完成:

[array enumerateObjectsAtIndexes:[NSIndexSet xxx] options:NSEnumerationConcurrent usingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        xxx
 }];

通過block回調,在主線程中對指定IndexSet逆序遍歷:

[array enumerateObjectsAtIndexes:[NSIndexSet xxx] options:NSEnumerationReverse usingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        xxx
 }];

三.性能比較

以100為步長,構造對象數目在0-100萬之間的NSArray, 分別用上述的遍歷方式進行遍歷并計時(單位us),而且在每一次遍歷中,僅僅只是得到對象,沒有其他任何輸入輸出,計算之類的干擾操作。每種遍歷方式采集得1萬組數據,得到如下的性能對比結果:


橫軸為遍歷的對象數目,縱軸為耗時,單位us.

從圖中看出,在對象數目很小的時候,各種方式的性能差別微乎其微。隨著對象數目的增大, 性能差異才體現出來.
其中for in的耗時一直都是最低的,當對象數高達100萬的時候,for in耗時也沒有超過5ms.
其次是for循環耗時較低.
反而,直覺上應該非??焖俚亩嗑€程遍歷方式:

[array enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        xxx
  }];

卻是性能最差的。
enumerateObjectsUsingBlock :reverseObjectEnumerator的遍歷性能非常相近.
為什么會有這樣的結果,文章稍后會從各種遍歷的內部實現來分析原因。

四.OC數組的內部結構

NSArrayNSMutableArray都沒有定義實例變量,只是定義和實現了接口,且對內部數據操作的接口都是在各個子類中實現的.所以真正需要了解的是子類結構,了解了__NSArrayI就相當于了解NSArray,了解了__NSArrayM就相當于了解NSMutableArray.
1. __NSArrayI
__NSArrayI的結構定義為:

@interface __NSArrayI : NSArray
{
    NSUInteger _used;
    id _list[0];
}
@end

_used是數組的元素個數,調用[array count]時,返回的就是_used的值。
id _list[0]是數組內部實際存儲對象的數組,但為何定義為0長度呢?這里有一篇關于0長度數組的文章:http://blog.csdn.net/zhaqiwen/article/details/7904515
這里我們可以把id _list[0]當作id *_list來用,即一個存儲id對象的buff.
由于__NSArrayI的不可變,所以_list一旦分配,釋放之前都不會再有移動刪除操作了,只有獲取對象一種操作.因此__NSArrayI的實現并不復雜.
2. __NSSingleObjectArrayI
__NSSingleObjectArrayI的結構定義為:

@interface __NSSingleObjectArrayI : NSArray
{
    id object;
}
@end

因為只有在"創建只包含一個對象的不可變數組"時,才會得到__NSSingleObjectArrayI對象,所以其內部結構更加簡單,一個object足矣.
3. __NSArrayM
__NSArrayM的結構定義為:

@interface __NSArrayM : NSMutableArray
{
    NSUInteger _used;
    NSUInteger _offset;
    int _size:28;
    int _unused:4;
    uint32_t _mutations;
    id *_list;
}
@end

__NSArrayM稍微復雜一些,但是同樣的,它的內部對象數組也是一塊連續內存id* _list,正如__NSArrayIid _list[0]一樣
_used:當前對象數目
_offset:實際對象數組的起始偏移,這個字段的用處稍后會討論
_size:已分配的_list大小(能存儲的對象個數,不是字節數)
_mutations:修改標記,每次對__NSArrayM的修改操作都會使_mutations加1,“*** Collection <__NSArrayM: 0x1002076b0> was mutated while being enumerated.”這個異常就是通過對_mutations的識別來引發的

id *_list是個循環數組.并且在增刪操作時會動態地重新分配以符合當前的存儲需求.以一個初始包含5個對象,總大小_size為6的_list為例:
_offset = 0,_used = 5,_size=6


在末端追加3個對象后:
_offset = 0,_used = 8,_size=8
_list已重新分配

刪除對象A:
_offset = 1,_used = 7,_size=8

刪除對象E:
_offset = 2,_used = 6,_size=8
B,C往后移動了,E的空缺被填補

在末端追加兩個對象:
_offset = 2,_used = 8,_size=8
_list足夠存儲新加入的兩個對象,因此沒有重新分配,而是將兩個新對象存儲到了_list起始端

因此可見,__NSArrayM_list是個循環數組,它的起始由_offset標識.

五.各種遍歷的內部實現

1.快速枚舉
前面并沒有說過快速枚舉這個詞,怎么這里突然蹦出來了,實際上for in就是基于快速枚舉實現的,但是先不討論for in,先認識一個協議:NSFastEnumeration,它的定義在Foundation框架的NSFastEnumeration .h頭文件中:

@protocol NSFastEnumeration

- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained _Nullable [_Nonnull])buffer count:(NSUInteger)len;

@end

NSFastEnumerationState定義:

typedef struct {
    unsigned long state;
    id __unsafe_unretained _Nullable * _Nullable itemsPtr;
    unsigned long * _Nullable mutationsPtr;
    unsigned long extra[5];
} NSFastEnumerationState;

看了這些定義和蘋果文檔,我也不知道究竟怎么用這個方法,它怎么就叫快速枚舉了呢,除非知道它的實現細節,否則用的時候疑惑太多了.因此我們就先不管怎么用,而是來看看它的實現細節.
__NSArrayI,__NSArrayM,__NSSingleObjectArrayI都實現了NSFastEnumeration協議.
(1) __NSArrayI的實現:
根據匯編反寫可以得到:

- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained _Nullable [_Nonnull])buffer count:(NSUInteger)len {
    if (!buffer && len > 0) {
        CFStringRef errorString = CFStringCreateWithFormat(kCFAllocatorSystemDefault, NULL, CFSTR("*** %s: pointer to objects array is NULL but length is %lu"), "-[__NSArrayI countByEnumeratingWithState:objects:count:]",(unsigned long)len);
        CFAutorelease(errorString);
        [[NSException exceptionWithName:NSInvalidArgumentException reason:(__bridge NSString *)errorString userInfo:nil] raise];
    }
    
    if (len >= 0x40000000) {
        CFStringRef errorString = CFStringCreateWithFormat(kCFAllocatorSystemDefault, NULL, CFSTR("*** %s: count (%lu) of objects array is ridiculous"), "-[__NSArrayI countByEnumeratingWithState:objects:count:]",(unsigned long)len);
        CFAutorelease(errorString);
        [[NSException exceptionWithName:NSInvalidArgumentException reason:(__bridge NSString *)errorString userInfo:nil] raise];
    }
    
    static const unsigned long mu = 0x01000000;
   
    if (state->state == 0) {
        state->mutationsPtr = μ
        state->state = ~0;
        state->itemsPtr = _list;
        return _used;
    }
    return 0;
}

可見在__NSArrayI對這個方法的實現中,主要做的事就是把__NSArrayI的內部數組_list賦給state->itemsPtr,并返回_used即數組大小.state->mutationsPtr指向一個局部靜態變量,state->state看起來是一個標志,如果再次用同一個state調用這個方法就直接返回0了.
至于傳入的buffer,len僅僅只是用來判斷了一下參數合理性。
看來有點明白快速枚舉的意思了,這一下就把全部對象獲取到了,而且在一個c數組里,之后要獲得哪個位置的對象都可以快速尋址到,調用方通過state->itemsPtr來訪問這個數組,通過返回值來確定數組里對象數目.
例如遍歷一個NSArray可以這樣:

    NSFastEnumerationState state = {0};
    NSArray *array = @[@1,@2,@3];
    id buffer[2];
//buffer 實際上內部沒有用上,但還是得傳, 2表示我期望得到2個對象,實際上返回的是全部對象數3
    NSUInteger n = [array countByEnumeratingWithState:&state objects:buffer count:2];
    for (NSUInteger i=0; i<n; ++i) {
        NSLog(@"%@", (__bridge NSNumber *)state.itemsPtr[i]);
    }

看來之所以叫快速遍歷,是因為這種方式直接從c數組里取對象,不用調用別的方法,所以快速.

__NSSingleObjectArrayI的實現也猜得出了,在此就不貼代碼了.我們來看看__NSArrayM是怎么實現這個協議的.
(2) __NSArrayM的實現:

- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained _Nullable [_Nonnull])buffer count:(NSUInteger)len {
    if (!buffer && len > 0) {
        CFStringRef errorString = CFStringCreateWithFormat(kCFAllocatorSystemDefault, NULL, CFSTR("*** %s: pointer to objects array is NULL but length is %lu"), "-[__NSArrayI countByEnumeratingWithState:objects:count:]",(unsigned long)len);
        CFAutorelease(errorString);
        [[NSException exceptionWithName:NSInvalidArgumentException reason:(__bridge NSString *)errorString userInfo:nil] raise];
    }
    
    if (len >= 0x40000000) {
        CFStringRef errorString = CFStringCreateWithFormat(kCFAllocatorSystemDefault, NULL, CFSTR("*** %s: count (%lu) of objects array is ridiculous"), "-[__NSArrayI countByEnumeratingWithState:objects:count:]",(unsigned long)len);
        CFAutorelease(errorString);
        [[NSException exceptionWithName:NSInvalidArgumentException reason:(__bridge NSString *)errorString userInfo:nil] raise];
    }
    
    if (state->state != ~0) {
        if (state->state == 0) {
            state->mutationsPtr = &_mutations;
            //找到_list中元素起始的位置
            state->itemsPtr = _list + _offset;
            if (_offset + _used <= _size) {
                //必定沒有剩余元素
                //標示遍歷完成
                state->state = ~0;
                return _used;
            }
            else {
                //有剩余元素(_list是個循環數組,剩余元素在_list從起始位置開始存儲)
                //state->state存放剩余元素數目
                state->state = _offset + _used - _size;
                //返回本次得到的元素數目 (總數 - 剩余)
                return _used - state->state;
            }
        }
        else {
            //得到剩余元素指針
            state->itemsPtr = _list;
            unsigned long left = state->state;
            //標示遍歷完成了
            state->state = ~0;
            return left;
        }
    }
    return 0;
}

從實現看出,對于__NSArrayM,用快速枚舉的方式最多只要兩次就可以獲取全部元素. 如果_list還沒有構成循環,那么第一次就獲得了全部元素,跟__NSArrayI一樣。但是如果_list構成了循環,那么就需要兩次,第一次獲取_offset_list末端的元素,第二次獲取存放在_list起始處的剩余元素.

2.for in的實現
如前面性能比較一節提到的,for in的性能是最好的,可以猜測for in基于應該就是剛剛討論的快速枚舉。
如下代碼:

    NSArray *arr = @[@1,@2,@3];
    for (id obj in arr) {
        NSLog(@"obj = %@",obj);
    }

通過clang -rewrite-objc main.m命令看看編譯器把for in變成了什么:

//NSArray *arr = @[@1,@2,@3];
NSArray *arr = ((NSArray *(*)(Class, SEL, const ObjectType *, NSUInteger))(void *)objc_msgSend)(objc_getClass("NSArray"), sel_registerName("arrayWithObjects:count:"), (const id *)__NSContainer_literal(3U, ((NSNumber *(*)(Class, SEL, int))(void *)objc_msgSend)(objc_getClass("NSNumber"), sel_registerName("numberWithInt:"), 1), ((NSNumber *(*)(Class, SEL, int))(void *)objc_msgSend)(objc_getClass("NSNumber"), sel_registerName("numberWithInt:"), 2), ((NSNumber *(*)(Class, SEL, int))(void *)objc_msgSend)(objc_getClass("NSNumber"), sel_registerName("numberWithInt:"), 3)).arr, 3U);
    {
//for (id obj in arr) obj的定義
    id obj;
//NSFastEnumerationState
    struct __objcFastEnumerationState enumState = { 0 };
//buffer
    id __rw_items[16];
    id l_collection = (id) arr;
//第一次遍歷,調用countByEnumeratingWithState:objects:count:快速枚舉方法
    _WIN_NSUInteger limit =
        ((_WIN_NSUInteger (*) (id, SEL, struct __objcFastEnumerationState *, id *, _WIN_NSUInteger))(void *)objc_msgSend)
        ((id)l_collection,
        sel_registerName("countByEnumeratingWithState:objects:count:"),
        &enumState, (id *)__rw_items, (_WIN_NSUInteger)16);
    if (limit) {
//保存初次得到的enumState.mutationsPtr的值
    unsigned long startMutations = *enumState.mutationsPtr;
    do {
        unsigned long counter = 0;
        do {
//在獲取enumState.itemsPtr中每個元素前,都檢查一遍enumState.mutationsPtr所指標志是否改變,改變則拋出異常
//對__NSArrayI,enumState.mutationsPtr指向一個靜態局部變量,永遠也不會拋異常
//對__NSArrayM,enumState.mutationsPtr指向_mutations變量, 每次增刪操作后,_mutations會+1
            if (startMutations != *enumState.mutationsPtr)
                objc_enumerationMutation(l_collection);
//獲取每一個obj
            obj = (id)enumState.itemsPtr[counter++]; {
//NSLog(@"obj = %@",obj);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_rg_wm9xjmyn1kz01_pph_34xcqc0000gn_T_main_c95c5d_mi_8,obj);
    };
    __continue_label_2: ;
        } while (counter < limit);
//再一次遍歷,獲取剩余元素
    } while ((limit = ((_WIN_NSUInteger (*) (id, SEL, struct __objcFastEnumerationState *, id *, _WIN_NSUInteger))(void *)objc_msgSend)
        ((id)l_collection,
        sel_registerName("countByEnumeratingWithState:objects:count:"),
        &enumState, (id *)__rw_items, (_WIN_NSUInteger)16)));
//遍歷完成
    obj = ((id)0);
    __break_label_2: ;
    }
//沒有元素,空數組
    else
        obj = ((id)0);
    }

可見,for in就是基于快速枚舉實現的,編譯器將for in轉化為兩層循環,外層調用快速枚舉方法批量獲取元素,內層通過c數組取得一批元素中的每一個,并且在每次獲取元素前,檢查是否對數組對象進行了變更操作,如果是,則拋出異常.
3.enumerateObjectsUsingBlock:
該方法在NSArray中實現,所有子類對象調用的都是這個實現

- (void)enumerateObjectsUsingBlock:(void ( ^)(id obj, NSUInteger idx, BOOL *stop))block {
    if (!block) {
        CFStringRef errorString = CFStringCreateWithFormat(kCFAllocatorSystemDefault, NULL, CFSTR("*** %s: block cannot be nil"), "-[NSArray enumerateObjectsUsingBlock:]");
        CFAutorelease(errorString);
        [[NSException exceptionWithName:NSInvalidArgumentException reason:(__bridge NSString *)errorString userInfo:nil] raise];
    }
    
    [self enumerateObjectsWithOptions:0 usingBlock:block];
}

內部直接以option = 0調用了enumerateObjectsWithOptions: usingBlock:
4. enumerateObjectsWithOptions: usingBlock:
(1)__NSArrayI的實現

- (void)enumerateObjectsWithOptions:(NSEnumerationOptions)opts usingBlock:(void (^)(id _Nonnull, NSUInteger, BOOL * _Nonnull))block {
    if (!block) {
        CFStringRef errorString = CFStringCreateWithFormat(kCFAllocatorSystemDefault, NULL, CFSTR("*** %s: block cannot be nil"), "-[__NSArrayI enumerateObjectsWithOptions:usingBlock:]");
        CFAutorelease(errorString);
        [[NSException exceptionWithName:NSInvalidArgumentException reason:(__bridge NSString *)errorString userInfo:nil] raise];
    }
    
    __block BOOL stoped = NO;
    void (^enumBlock)(NSUInteger idx) = ^(NSUInteger idx) {
        if(!stoped) {
            @autoreleasepool {
                block(_list[idx],idx,&stoped);
            }
        }
    };
    
    if (opts == NSEnumerationConcurrent) {
        dispatch_apply(_used, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), enumBlock);
    }
    else if(opts == NSEnumerationReverse) {
        for (NSUInteger idx = _used - 1; idx != (NSUInteger)-1 && !stoped; idx--) {
            enumBlock(idx);
        }
    }
    //opts == 0
    else {
        if(_used > 0) {
            for (NSUInteger idx = 0; idx != _used - 1 && !stoped; idx++) {
                enumBlock(idx);
            }
        }
    }
}

(1)__NSArrayM的實現
__NSArrayM的實現唯一不同的是enumBlock

 void (^enumBlock)(NSUInteger idx) = ^(NSUInteger idx) {
        if(!stoped) {
            @autoreleasepool {
                NSUInteger idx_ok = _offset + idx;
                //idx對應元素在_list起始處(循環部分)
                if (idx_ok >= _size) {
                    idx_ok -= _size;
                }
                block(_list[idx_ok],idx,&stoped);
            }
        }
    };

5.objectEnumerator/reverseObjectEnumerator
通過array.objectEnumerator得到的是一個__NSFastEnumerationEnumerator私有類對象,在這個enumerator對象上每次調用- (id)nextObject時,實際上內部每次都調用的是array的快速枚舉方法:

- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained _Nullable [_Nonnull])buffer count:(NSUInteger)len 

只不過每次只獲取并返回一個元素.
而通過array.reverseObjectEnumerator得到的是一個__NSArrayReverseEnumerator私有類對象,在這個enumerator對象上每次調用- (id)nextObject時,內部直接調用是:objectAtIndex:來返回對象.
6.enumerateObjectsAtIndexes:options:usingBlock:
由于時間關系后面再貼了.

6.總結

到此,應該可以回答文章開頭提到的幾個問題了.
關于性能的差異:
for in之所以快,是因為它基于快速枚舉,對NSArray只要一次快速枚舉調用就可以獲取到包含全部元素的c數組,對NSMUtableArray最多兩次就可以全部獲取。
for 之所以比 for in稍慢,僅僅是因為它函數調用開銷的問題,相對于for in直接從c數組取每個元素的方式,for靠的是每次調用objectAtIndex:
NSEnumerationConcurrent+Block的方式耗時最大,我認為是因為它采用了多線程,就這個方法來講,多線程的優勢并不在于遍歷有多快,而是在于它的回調在各個子線程,如果有遍歷+分別耗時計算的場景,這個方法應該是最適合的,只是此處只測遍歷速度,它光啟動分發管理線程就耗時不少,所以性能落后了.

希望通過此文能對你有幫助.

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

推薦閱讀更多精彩內容