[iOS] objc_class 中 cache 底層分析

1. cache 中存儲的是什么?

在 objc_class結構體中,有 cache 這個成員,而且還是一個結構體類型

 // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    ...

我們直接看cache_t的源碼:

struct cache_t {
//macOS、模擬器 -- 主要是架構區分
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
    // explicit_atomic 顯示原子性,目的是為了能夠 保證 增刪改查時 線程的安全性
   // 方法的緩存數組(以散列表的形式存儲 bucket_t,用來存儲sel imp)
    explicit_atomic<struct bucket_t *> _buckets;
// _buckets 的數組長度-1,容量的臨界值
    explicit_atomic<mask_t> _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 // 64位真機
    // 掩碼和 Buckets 指針共同保存在 uintptr_t 類型的 _maskAndBuckets中
    explicit_atomic<uintptr_t> _maskAndBuckets;
    // 未使用的容量
    mask_t _mask_unused;
    
    // 高 16 位是 mask 掩碼,即 _maskAndBuckets 右移 48 位得到 mask
    static constexpr uintptr_t maskShift = 48;
    // 掩碼后的其他位必須為 0
    // msgSend  利用這些額外的位,在單個指令中從 _maskAndBuckets 構造了值 mask<<4
    static constexpr uintptr_t maskZeroBits = 4;

    // 我們可以保存的最大的 mask 值
    // (64 - maskShift) 即掩碼位數,然后 將 1 左移掩碼位數后再減 1 即 16 位能保存的最大二進制數值
    static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
        
    // 應用于 _maskAndBuckets 的掩碼,以獲取 buckets 指針
    // 1 左移 44(48-4)位后再減 1(44 位 1,其余都是 0 的數值)
    static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << (maskShift - maskZeroBits)) - 1;

    // 確保我們有足夠的位用于存儲 buckets 指針。
    static_assert(bucketsMask >= MACH_VM_MAX_ADDRESS, "Bucket field doesn't have enough bits for arbitrary pointers.");

#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4 //非64位 真機
    explicit_atomic<uintptr_t> _maskAndBuckets;
    mask_t _mask_unused;

    // _maskAndBuckets 將掩碼移位存儲在低 4 位中,并將 buckets  指針存儲在該值的其余部分中
#else
#error Unknown cache mask storage type.
#endif
    
#if __LP64__
    // 如果是 64 位環境的話,會多一個 _flags 標志位
    uint16_t _flags;
#endif
    // 緩存數組的已占用量
    uint16_t _occupied;

    //方法省略.....
}

我們看到最上面有一個宏判斷,其實是架構的處理

#define CACHE_MASK_STORAGE_OUTLINED 1
#define CACHE_MASK_STORAGE_HIGH_16 2
#define CACHE_MASK_STORAGE_LOW_4 3

#if defined(__arm64__) && __LP64__ // 64位真機
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16
#elif defined(__arm64__) && !__LP64__ // 非 64位真機
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_LOW_4
#else
#define CACHE_MASK_STORAGE // 其它 CACHE_MASK_STORAGE_OUTLINED
#endif

我們再來看bucket_t的源碼,分為兩個版本:真機和非真機,不同的區別只是在于 sel 和 imp 的順序不一致

struct bucket_t {
private:
    // IMP-first is better for arm64e ptrauth and no worse for arm64.
    // SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__ // 真機
    explicit_atomic<uintptr_t> _imp;
    explicit_atomic<SEL> _sel;
#else // 非真機
    explicit_atomic<SEL> _sel;
    explicit_atomic<uintptr_t> _imp;
#endif
... 方法等其他部分
}

所以通過上面兩個結構體可以知道,cache 中緩存的是sel-imp
整體結構如下所示:

image.png

2. 在 cache 中查找 sel-imp

cache_t中查找存儲的sel-imp,有以下兩種方式:

  • 通過源碼查找
  • 脫離源碼在項目中查找
2.1 準備工作

創建一個 Person 類,并定義兩個屬性和 5 個實例方法及其實現:

@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, copy) NSString *nickName;

- (void)sayHello;
- (void)sayCode;
- (void)sayMaster;
- (void)sayNB;
+ (void)sayHappy;
@end

@implementation Person

- (void)sayHello{
    NSLog(@"Person say: %s",__func__);
}
- (void)sayCode{
    NSLog(@"Person say: %s",__func__);
}
- (void)sayMaster{
    NSLog(@"Person say: %s",__func__);
}
- (void)sayNB{
    NSLog(@"Person say: %s",__func__);
}
+ (void)sayHappy{
    NSLog(@"Person say: %s",__func__);
}

@end

2.2 通過 lldb 調試和源碼進行查找

在 main 中定義 Person 對象,并調用其中的 3 個實例方法,添加斷點:

截屏2021-01-09 下午5.47.39.png

  • cache的獲取,需要通過 pClass 的首地址平移 16 個字節,即首地址+0x10 獲取 cache 的地址
  • 從源碼中可得,sel-imp是在 cache_t的 _buckets屬性中(目前是 macOS 環境),而且 cache_t結構體也提供了獲取_buckets()屬性的方法buckets()
  • 在沒有執行方法調用時,此時的 cache 是沒有緩存的,執行了一次方法調用,cache 中就有了一個緩存,即調用一次方法就會緩存一次方法
  • 獲取了_buckets屬性,就可以獲取 sel-imp了,這兩個的獲取在bucket_t結構體中同樣提供了相應的獲取方法sel()以及imp(pClass)

接著上面的步驟,我們再次調用一個方法,這次我們想要獲取第二個 sel,調試過程如下:

截屏2021-01-09 下午6.02.32.png

  • 第一個調用方法的存儲獲取很簡單,直接通過_buckets的首地址調用對應的方法即可
  • 獲取第二個bucket_t需要通過_buckets的首地址進行偏移,即p*($9+1)即可獲取第二個bucket_t,如果有多個方法需要獲取,以此類推。
2.3 脫離源碼通過項目查找

脫離源碼環境,就是將所需的源碼的部分拷貝至項目中,其完整代碼如下:

typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits

struct lg_bucket_t {
    SEL _sel;
    IMP _imp;
};

struct lg_cache_t {
    struct lg_bucket_t * _buckets;
    mask_t _mask;
    uint16_t _flags;
    uint16_t _occupied;
};

struct lg_class_data_bits_t {
    uintptr_t bits;
};

struct lg_objc_class {
    Class ISA;
    Class superclass;
    struct lg_cache_t cache;             // formerly cache pointer and vtable
    struct lg_class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [Person alloc];
        Class pClass = [person class];
        
        [person sayHello];
        [person sayCode];
        
        struct lg_objc_class *lg_pClass = (__bridge struct lg_objc_class *)(pClass);
        NSLog(@"%hu - %u",lg_pClass->cache._occupied,lg_pClass->cache._mask);
        for (mask_t i = 0; i<lg_pClass->cache._mask; i++) {
            // 打印獲取的 bucket
            struct lg_bucket_t bucket = lg_pClass->cache._buckets[I];
            NSLog(@"%@ - %p",NSStringFromSelector(bucket._sel),bucket._imp);
        }
    }
    return 0;
}

注:objc_class的ISA 屬性是繼承自 objc_object的,但這里去掉了這個關系,所以將 ISA 屬性直接加上了。

輸出結果:

2021-01-10 14:30:07.436173+0800 DebugTest[5488:251965] Person say: -[Person sayHello]
2021-01-10 14:30:07.436624+0800 DebugTest[5488:251965] Person say: -[Person sayCode]
2021-01-10 14:30:07.436683+0800 DebugTest[5488:251965] 2 - 3
2021-01-10 14:30:07.436827+0800 DebugTest[5488:251965] sayHello - 0x2940
2021-01-10 14:30:07.436913+0800 DebugTest[5488:251965] sayCode - 0x2eb0
2021-01-10 14:30:07.436956+0800 DebugTest[5488:251965] (null) - 0x0

再增加兩個方法的調用,其打印結果如下:

2021-01-10 15:12:27.852728+0800 DebugTest[5627:264649] Person say: -[Person sayHello]
2021-01-10 15:12:27.853139+0800 DebugTest[5627:264649] Person say: -[Person sayCode]
2021-01-10 15:12:27.853204+0800 DebugTest[5627:264649] Person say: -[Person sayHello]
2021-01-10 15:12:27.853247+0800 DebugTest[5627:264649] Person say: -[Person sayNB]
2021-01-10 15:12:27.853286+0800 DebugTest[5627:264649] 1 - 7
2021-01-10 15:12:27.853328+0800 DebugTest[5627:264649] (null) - 0x0
2021-01-10 15:12:27.853469+0800 DebugTest[5627:264649] sayNB - 0x2ed8
2021-01-10 15:12:27.853519+0800 DebugTest[5627:264649] (null) - 0x0
2021-01-10 15:12:27.853557+0800 DebugTest[5627:264649] (null) - 0x0
2021-01-10 15:12:27.853594+0800 DebugTest[5627:264649] (null) - 0x0
2021-01-10 15:12:27.853629+0800 DebugTest[5627:264649] (null) - 0x0
2021-01-10 15:12:27.853665+0800 DebugTest[5627:264649] (null) - 0x0

對于這次的打印結果,有以下幾點疑問:

  • _mask是什么?
  • _occupied是什么?
  • 為什么隨著方法調用的增多,其打印的 occupied 和 mask 會變化?
  • 打印的 bucket 數據為什么只有 sayNB了?

3. cache_t 底層原理分析

3.1 主要流程介紹

首先,從cache_t中的_mask屬性開始分析,找 cache_t中引起變化的函數,發現了incrementOccupied()函數

截屏2021-01-10 下午3.18.03.png

具體實現為:

void cache_t::incrementOccupied() 
{
    _occupied++;
}

全局搜索incrementOccupied()函數,發現只在 cache_t的insert方法中有調用:

截屏2021-01-10 下午3.19.20.png

insert方法就是插入方法了,而 cache 中存儲的就是 sel-imp,所以我們從 insert 方法開始分析,下面是 cache 原理分析的流程圖:

image.png

3.2 insert 方法實現
ALWAYS_INLINE
void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver)
{
#if CONFIG_USE_CACHE_LOCK
    cacheUpdateLock.assertLocked();
#else
    // 加鎖,如果加鎖失敗則執行斷言
    runtimeLock.assertLocked(); // 同樣,__objc2__ 下使用 runtimeLock 
#endif
    
    // 斷言 sel 不能是 0 且 cls 已經完成初始化
    ASSERT(sel != 0 && cls->isInitialized());

    // 如果緩存占用少于 3/4 則可以繼續保持原樣使用。
    
    // 記錄新的占用量(舊的占用量加 1)
    mask_t newOccupied = occupied() + 1;
    
    // 舊容量
    unsigned oldCapacity = capacity(), capacity = oldCapacity;
    
    if (slowpath(isConstantEmptyCache())) { // 很可能為假
        // 如果目前是空緩存的話,空緩存只是 static bucket_t **emptyBucketsList 用來占位的,
        // 實際并不存儲 bucket_t,我們需要重新申請空間,替換空緩存。
        if (!capacity) capacity = INIT_CACHE_SIZE; // 如果 capacity 為 0,則賦值給初始值 4
        // 根據 capacity 申請新空間并初始化 buckets、mask(capacity - 1)、_occupied 
        // 這里還有一個點,由于舊 buckets 是準備的占位的靜態數據是不需要釋放的,
        // 所以最后一個參數傳遞的是 false。
        reallocate(oldCapacity, capacity, /* freeOld */false);
    }
    else if (fastpath(newOccupied + CACHE_END_MARKER <= capacity / 4 * 3)) { 
        // 大部分情況都在這里
        // 緩存占用少于等于 3/4 的空間。照原樣使用。
        
        // 小括號里面加了一個 CACHE_END_MARKER
        // 是因為在 __arm__  ||  __x86_64__  ||  __i386__ 這些平臺下,
        // 會在 buckets 的末尾放一個 bucket_t *end,所以這里又加了 1
        // 而 __arm64__ 平臺下則不存在這個多 +1
    }
    else {
        // 第三種情況則是需要對散列表空間進行擴容
        // 擴大為原始 capacity 的 2 倍
        // 且這里的擴容時為了性能考慮是不會把舊的緩存復制到新空間的。
        
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        
        // 如果大于 MAX_CACHE_SIZE,則使用 MAX_CACHE_SIZE(1 << 16)
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        
        // 申請空間并做一些初始化
        // 不同與 isConstantEmptyCache 的情況,這里擴容后需要釋放舊的 buckets,
        // 所以這里第三個參數傳的是 true,表示需要釋放舊 buckets,而這里它也不是立即釋放的,
        // 在舊 buckets 沒有被使用并且收集的舊 buckets 容量已經到達閥值了,
        // 則會真正進行內存空間的釋放
        reallocate(oldCapacity, capacity, true);
    }

    // 臨時變量
    bucket_t *b = buckets();
    // mask = cap -1
    mask_t m = capacity - 1;
    
    // 使用 sel 和 _mask 進行哈希計算,取得 sel 的哈希值 
    mask_t begin = cache_hash(sel, m);
    mask_t i = begin;

    // 掃描第一個未使用的 "插槽",然后將 bucket_t 插入其中。
    // 保證有一個空插槽,因為最小大小為4,
    // 且上面已經做過判斷如果使用占比超過 3/4 則進行擴容,
    // 且這里的擴容為了性能考慮是不會把舊的緩存復制到新空間的,
    // 舊 buckets 會被拋棄,并在合適時候釋放其內存空間
    
    // 這里如果發生哈希沖突的話 do while 會進行一個線性的哈希探測(開放尋址法),
    // 為 sel 和 imp 找一個空位。
    do {
        if (fastpath(b[i].sel() == 0)) {
            // 如果 self 為 0,則表示 sel 的哈希值對應的下標處剛好是一個空位置,
            // 直接把 sel 和 imp 放在此處即可。
            
            // occupied 已占用數量 +1 
            incrementOccupied();
            
            // 以原子方式把 sel 和 imp 保存在 Bucket_t 的 _sel 和 _imp 中 
            b[i].set<Atomic, Encoded>(sel, imp, cls);
            
            return;
        }
        if (b[i].sel() == sel) {

            // 在 cacheUpdateLock(runtimeLock) 加鎖之前,
            // 該 sel/imp 已由其他一些線程添加到緩存中。
            
            return;
        }
        
      // 下一個哈希值探測,這里不同的平臺不同處理方式依次 +1 或者 -1
    } while (fastpath((i = cache_next(i, m)) != begin));

    // 如果未找到合適的位置則 bad_cache
    cache_t::bad_cache(receiver, (SEL)sel, cls);
}

insert 方法主要分為以下3部分:

  • 1.計算出當前的緩存占用量
  • 2.根據緩存占用量判斷需要執行的操作
  • 3.針對需要存儲的 bucket 進行內部 imp 和 sel 賦值
3.2.1 計算當前的緩存占用量:

根據 occupied 的值計算出當前的緩存占用量,當屬性未賦值以及無方法調用時,此時的 occupied()為 0,而 newOccupied 為 1,如下所示:

mask_t newOccupied = occupied() + 1;

關于緩存占用量的計算,需要注意:

  • alloc申請空間時,此時的對象已經創建,如果再調用init 方法,occupied 也會+1,上面示例中我們沒有調用 init 方法;
  • 當有屬性賦值時,會隱式調用set 方法,occupied 也會增加
  • 當有方法調用時,occupied 也會增加
3.2.2 根據緩存占用量判斷執行的操作:
  • 1.如果是第一次創建,默認開辟 4 個
  • 2.如果緩存占用量<=當前總量的 3/4,則不作任何處理
  • 3.如果緩存占用量超過 3/4,需要進行兩倍擴容,以及重新開辟空間,此時之前的緩存會被釋放,也就是我們上面看到只有 sayNB 一個方法的原因

realloccate 方法(開辟空間):
該方法在第一次創建以及兩倍擴容時,都會使用,其源碼實現如下:

ALWAYS_INLINE
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
{
    // 一個臨時變量用于記錄舊的散列表
    bucket_t *oldBuckets = buckets();
    
    // 為新散列表申請指定容量的空間,此時只是一個臨時變量
    bucket_t *newBuckets = allocateBuckets(newCapacity);

    // Cache's old contents are not propagated.
    // 緩存的舊內容不會傳播。
    
    // This is thought to save cache memory at the cost of extra cache fills.
    // 這被認為是以額外的緩存填充為代價來節省緩存內存的。
    // fixme re-measure this 重新測量

    ASSERT(newCapacity > 0);
    ASSERT((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
    
    // 設置 buckets 和 mask,將臨時的 buckets 和 mask 存入緩存中
    setBucketsAndMask(newBuckets, newCapacity - 1);
    
    if (freeOld) {
        // 這里不是立即釋放舊的 bukckts,而是將舊的 buckets 添加到存放舊散列表的列表中,以便稍后釋放,注意這里是稍后釋放。
        cache_collect_free(oldBuckets, oldCapacity);
    }
}

上面reallocate這個方法主要有以下幾步:

  • allocateBuckets 方法:向系統申請開辟內存,即開辟 bucket,此時的 bucket 只是一個臨時變量
  • setBucketsAndMask 方法,將臨時的 bucket 和 mask 存入緩存;
    如果是真機,根據 bucket 和 mask 的位置存儲,并將occupied 占用設置為 0
    image.png

    如果不是真機,正常存儲 bucket 和 mask,并將 occupied 占用設置為 0:
    image.png
  • 如果有舊的buckets,則需要清理之前的緩存,即調用 cache_collect_free方法,其源碼實現如下:
static void cache_collect_free(bucket_t *data, mask_t capacity)
{
#if CONFIG_USE_CACHE_LOCK
    cacheUpdateLock.assertLocked();
#else
    runtimeLock.assertLocked(); // 加鎖,加鎖失敗執行斷言
#endif

    // 記錄等待釋放的容量 
    if (PrintCaches) recordDeadCache(capacity);

    // 為 garbage 準備空間,需要時進行擴容,創建垃圾回收空間
    _garbage_make_room ();
    
    // 增加 garbage_byte_size 的值
    garbage_byte_size += cache_t::bytesForCapacity(capacity);
    
    // 把舊的 buckets 放進 garbage_refs 中,garbage_count 并自增 1
    garbage_refs[garbage_count++] = data;
    
    // 嘗試去釋放累積的舊緩存(bucket_t)
    cache_collect(false);
}

這個方法主要有以下幾步:

  • _garbage_make_room方法,創建垃圾回收空間:
static void _garbage_make_room(void)
{
    static int first = 1; // 靜態局部變量,下次進來 first 依然是上次的值

    // 第一次需要時創建收集表
    if (first)
    {
        first = 0; // 此處置為 0 后,以后調用 _garbage_make_room 再也不會進到這個 if
        // 申請初始空間
        // 申請 INIT_GARBAGE_COUNT * sizeof(void *) 字節個空間。
        // (malloc 不會對空間進行初始化,會保持申請時的垃圾數據)
        garbage_refs = (bucket_t**)malloc(INIT_GARBAGE_COUNT * sizeof(void *));
        
        // 當前 garbage_refs 的容量是 INIT_GARBAGE_COUNT
        garbage_max = INIT_GARBAGE_COUNT;
    }

    // Double the table if it is full
    // 如果當前 garbage_refs 中 refs 的數量等于 garbage_max 就對 garbage_refs 擴容為當前的 2 倍
    else if (garbage_count == garbage_max)
    {
        // garbage_refs 擴容為 2 倍
        garbage_refs = (bucket_t**)
            realloc(garbage_refs, garbage_max * 2 * sizeof(void *));
        // 更新 garbage_max 為 2 倍
        garbage_max *= 2;
    }
}

  • 記錄存儲這次的 bucket
  • cache_collect方法,釋放累積的舊緩存
3.3.3 針對需要存儲的 bucket 進行內部 imp 和 sel 賦值:

這部分主要是根據 cache_hash方法,即哈希算法, 計算 sel-imp存儲的哈希下標,分為以下三種情況:

  • 1.如果哈希下標的位置未存儲 sel,即該下標位置獲取 sel 為 null,此時將 sel-imp存儲進去,occupied+1
  • 2.如果當前哈希下標存儲的 sel 等于即將插入的 sel,則直接返回
  • 3.如果當前哈希下標存儲的sel 不等于 即將插入的sel,則重新經過cache_next方法 即哈希沖突算法,重新進行哈希計算,得到新的下標,再去對比進行存儲

cache_hash 哈希算法:

// Class points to cache. SEL is key. Cache buckets store SEL+IMP.
// 類指向緩存。 SEL 是 key。Cache 的 buckets 中保存 SEL+IMP(即 struct bucket_t)。

// Caches are never built in the dyld shared cache.
// Caches 永遠不會構建在 dyld 共享緩存中。

static inline mask_t cache_hash(SEL sel, mask_t mask) 
{
    // 覺的 hash 值計算好隨意,就是拿 sel 和 mask 與一下,保證不會越界
    return (mask_t)(uintptr_t)sel & mask;
}

cache_next 哈希沖突算法:
這里是發生哈希沖突時,哈希值的移動探測方式在不同的平臺下有不同的處理:

#if __arm__  ||  __x86_64__  ||  __i386__
// objc_msgSend has few registers available.
// objc_msgSend 的可用寄存器很少。

// Cache scan increments and wraps at special end-marking bucket.
// 緩存掃描增量包裹在特殊的末端標記桶上。
//(此處應該說的是 CACHE_END_MARKER 是 1 時的 endMarker 的位置在 buckets 首位)

#define CACHE_END_MARKER 1

// i 每次向后移動 1,與 mask,保證不會越界
//(并且是到達 mask 后再和 mask 與操作會是 0 ,此時則從 buckets 的 0 下標處開始,
// 然后再依次向后移動探測直到到達 begin,如果還沒有找到合適位置,那說明發生了內存錯誤問題)

static inline mask_t cache_next(mask_t i, mask_t mask) {
    return (i+1) & mask;
}

#elif __arm64__
// objc_msgSend has lots of registers available.
// objc_msgSend 有很多可用的寄存器。
// Cache scan decrements. No end marker needed.
// 緩存掃描減量。無需結束標記。
//(此處說的是 CACHE_END_MARKER 是 0 時,不存在 endMarker 賦值)

#define CACHE_END_MARKER 0

// i 依次遞減
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask;
}

#else

// 未知架構
#error unknown architecture

#endif

到此,cache_t的基本流程分析完成了,現在回答一下上面的幾個問題:

  • _mask是什么?
    _mask是掩碼數據,用在哈希算法或者哈希沖突算法中,其中mask = capacity-1

  • _occupied是什么?
    _occupied表示哈希表中 sel-imp的占用大小init| 屬性賦值 | 方法調用,會導致 occupied 變化。

  • 為什么隨著方法調用的增多,其打印的 occupied 和 mask 會變化?
    在cahce 初始化時分配的空間是4 個,隨著方法調用的增多,存儲的 sel-imp個數超過總容量的3/4時,會對 cache 進行兩倍擴容

  • 打印的 bucket 數據為什么只有 sayNB 了?
    在擴容時,重新申請內存導致。

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

推薦閱讀更多精彩內容