iOS 底層探索 - cache_t

iOS 底層探索 - cache_t.png

iOS 底層探索系列

上一篇我們一起探索了 iOS 類(lèi)的底層原理,其中比較重要的四個(gè)屬性我們都簡(jiǎn)單的過(guò)了一遍,我們接下來(lái)要重點(diǎn)探索第三個(gè)屬性 cache_t,對(duì)于這個(gè)屬性,我們可以學(xué)習(xí)到蘋(píng)果對(duì)于緩存的設(shè)計(jì)與理解,同時(shí)也會(huì)接觸到消息發(fā)送相關(guān)的知識(shí)。

一、探索 cache_t

1.1 cache_t 基本結(jié)構(gòu)

我們還是先過(guò)一遍 OC 中類(lèi)的結(jié)構(gòu):

struct objc_class : objc_object {
    // 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

    class_rw_t *data() { 
        return bits.data();
    }

    ...省略代碼...    
}

接著我們查看源碼中 cache_t 的定義:

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
    
    ...省略代碼... 
}

然后我們發(fā)現(xiàn) cache_t 結(jié)構(gòu)體的第一個(gè)成員 _buckets 也是一個(gè)結(jié)構(gòu)體類(lèi)型 bucket_t,我們?cè)俨榭匆幌?bucket_t 的定義:

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__
    MethodCacheIMP _imp;
    cache_key_t _key;
#else
    cache_key_t _key;
    MethodCacheIMP _imp;
#endif

public:
    inline cache_key_t key() const { return _key; }
    inline IMP imp() const { return (IMP)_imp; }
    inline void setKey(cache_key_t newKey) { _key = newKey; }
    inline void setImp(IMP newImp) { _imp = newImp; }

    void set(cache_key_t newKey, IMP newImp);
};

從源碼定義中不難看出,bucket_t 其實(shí)緩存的是方法實(shí)現(xiàn) IMP。這里有一個(gè)注意點(diǎn),就是 IMP-firstSEL-first。

IMP-first is better for arm64e ptrauth and no worse for arm64.

  • IMP-first 對(duì) arm64e 的效果更好,對(duì) arm64 不會(huì)有壞的影響。

SEL-first is better for armv7* and i386 and x86_64.

  • SEL-first 適用于 armv7 * 和 i386 和 x86_64。

如果對(duì) SELIMP 不是很熟悉的同學(xué)可以去 objc4-756 源碼中查看方法 method_t 的定義:

struct method_t {
    SEL name;   // 方法選擇器
    const char *types; // 方法類(lèi)型字符串
    MethodListIMP imp;  // 方法實(shí)現(xiàn)

    ...省略代碼... 
};

通過(guò)上面的源碼,我們大致了解了 bucket_t 類(lèi)型的結(jié)構(gòu),那么現(xiàn)在問(wèn)題來(lái)了,類(lèi)中的 cache 是在什么時(shí)候以什么樣的方式來(lái)進(jìn)行緩存的呢?

1.2 LLDB 大法

了解到 cache_tbucket_t 的基本結(jié)構(gòu)后,我們可以通過(guò) LLDB 來(lái)打印驗(yàn)證一下:

image.png

cache_t 內(nèi)部的這三個(gè)屬性,我們從其名稱(chēng)不難看出 _occupied 應(yīng)該是表示當(dāng)前已經(jīng)占用了多少緩存,_mask 暫時(shí)不知道,_buckets 應(yīng)該是存放具體緩存的地方。那么為了驗(yàn)證我們的猜想,我們調(diào)用代碼來(lái)測(cè)試:

image.png

我們發(fā)現(xiàn),斷點(diǎn)斷到 45 行的時(shí)候,_ocuupied 的值為 1,我們打印一下 _buckets 里面的內(nèi)容看看:

image.png

我們可以看到,打印到 _buckets 的第三個(gè)元素的時(shí)候,我們的 init 方法被緩存了,也就是說(shuō) _ocuupied 確實(shí)是表示當(dāng)前被緩存方法的個(gè)數(shù)。這里可能讀者會(huì)說(shuō)為什么 allocclass 為什么沒(méi)有被緩存呢?其實(shí)這是因?yàn)?allocclass 是類(lèi)方法,而根據(jù)我們前面探索類(lèi)底層原理的時(shí)候,類(lèi)方法是存儲(chǔ)在元類(lèi)里面的,所以這里類(lèi)的緩存里面只會(huì)存儲(chǔ)對(duì)象方法。
我們接著把斷點(diǎn)過(guò)到 46 行:

image.png

_ocuupied 的值果然發(fā)生了變化,我們剛才的猜想進(jìn)一步得到了驗(yàn)證,我們?cè)偻旅孀咭恍?

image.png

此時(shí) _ocuupied 值已經(jīng)為 3 了,我們回顧一下當(dāng)前緩存里面緩存的方法:

_ocuupied 的值 緩存的方法
1 NSObject下的init
2 NSObject下的init,person下的 sayHello
3 NSObject下的init,person下的 sayHello, person下的 sayCode

那么,當(dāng)我們的斷點(diǎn)斷到下一行的時(shí)候,是不是 _ocuupied 就會(huì)變?yōu)?4 呢? 我們接著往下走:

image.png

令人驚奇的事情發(fā)生了,_ocuupied 的值變成了 1,而 _mask 變成了 7。這是為什么呢?

如果讀者了解并掌握散列表這種數(shù)據(jù)結(jié)構(gòu)的話(huà),相信已經(jīng)看出端倪了。是的,這里其實(shí)就是用到了 開(kāi)放尋址法 來(lái)解決散列沖突(哈希沖突)。

關(guān)于哈希沖突,可以借助鴿籠理論,即把 11 只鴿子放進(jìn) 10 個(gè)抽屜里面,肯定會(huì)有一個(gè)抽屜里面有 2 只鴿子。是不是理解起來(lái)很簡(jiǎn)單? :)

通過(guò)上面的測(cè)試,我們明確了方法緩存使用的是哈希表存儲(chǔ),并且為了解決無(wú)法避免的哈希沖突使用的是開(kāi)放尋址法,而開(kāi)放尋址法必然要在合適的時(shí)機(jī)進(jìn)行擴(kuò)容,這個(gè)時(shí)機(jī)肯定不是會(huì)在數(shù)據(jù)已經(jīng)裝滿(mǎn)的時(shí)候,我們可以進(jìn)源碼探索一下,我們快速定位到 cache_t 的源碼處:

void cache_t::expand()
{
    cacheUpdateLock.assertLocked();
    
    uint32_t oldCapacity = capacity();
    uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;

    if ((uint32_t)(mask_t)newCapacity != newCapacity) {
        // mask overflow - can't grow further
        // fixme this wastes one bit of mask
        newCapacity = oldCapacity;
    }

    reallocate(oldCapacity, newCapacity);
}

從上面的代碼不難看出 expand 方法就是擴(kuò)容的核心算法,我們梳理一下里面的邏輯:

cacheUpdateLock.assertLocked();
  • 緩存鎖斷言一下判斷當(dāng)前執(zhí)行上下文是否已經(jīng)上鎖
uint32_t oldCapacity = capacity();
  • 通過(guò) capacity() 方法獲取當(dāng)前的容量大小
uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
  • 判斷當(dāng)前的容量大小,如果為0,則賦值為 INIT_CACHE_SIZE,而根據(jù)
enum {
    INIT_CACHE_SIZE_LOG2 = 2,
    INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2)
};

可知 INIT_CACHE_SIZE 初始值為 4;如果當(dāng)前容量大小不為 0,則直接翻倍。

到了這里相信聰明的讀者根據(jù)我們上面的測(cè)試應(yīng)該猜到了,我們的 _mask 其實(shí)就是容量大小減 1 后的結(jié)果。

reallocate(oldCapacity, newCapacity);
  • 最后調(diào)用 reallocate 方法進(jìn)行緩存大小的重置

我們接著進(jìn)入 reallocate 內(nèi)部一探究竟:

void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{
    bool freeOld = canBeFreed();

    bucket_t *oldBuckets = buckets();
    bucket_t *newBuckets = allocateBuckets(newCapacity);

    assert(newCapacity > 0);
    assert((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);

    setBucketsAndMask(newBuckets, newCapacity - 1);
    
    if (freeOld) {
        cache_collect_free(oldBuckets, oldCapacity);
        cache_collect(false);
    }
}

void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
{
    mega_barrier();

    _buckets = newBuckets;
    
    mega_barrier();
    
    _mask = newMask;
    _occupied = 0;
}

顯然,_mask 是這一步 setBucketsAndMask(newBuckets, newCapacity - 1); 被賦值為容量減 1 的。

同樣的,我們還可以通過(guò) capacity 方法來(lái)驗(yàn)證

mask_t cache_t::capacity() 
{
    return mask() ? mask()+1 : 0; 
}

二、深入 cache_t

其實(shí)我們?cè)谔剿?iOS 底層的時(shí)候,盡量不要站在上帝視角去審視相應(yīng)的技術(shù)點(diǎn),我們可以盡量給自己多拋出幾個(gè)問(wèn)題,然后嘗試去解決每個(gè)問(wèn)題,通過(guò)這樣的探索,對(duì)提高我們閱讀源碼的能力十分重要。

通過(guò)前面的探索,我們知道了 cache_t 實(shí)質(zhì)上是緩存了我們類(lèi)的實(shí)例方法,那么對(duì)于類(lèi)方法來(lái)說(shuō),自然就是緩存在了元類(lèi)上了。這一點(diǎn)我相信讀者應(yīng)該都能理解。

2.1 方法緩存策略

按照最常規(guī)的思維,緩存內(nèi)容最省時(shí)省力的辦法肯定是來(lái)一個(gè)緩存一個(gè),那么我們的 cache_t 是不是這么做的呢,實(shí)踐出真知,我們一試便知。

我們?cè)谠创a中搜索 capacity() 方法,我們找到了 cache_fill_nolock 方法:

static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
    cacheUpdateLock.assertLocked();

    // Never cache before +initialize is done
    if (!cls->isInitialized()) return;

    // Make sure the entry wasn't added to the cache by some other thread 
    // before we grabbed the cacheUpdateLock.
    if (cache_getImp(cls, sel)) return;

    cache_t *cache = getCache(cls);
    cache_key_t key = getKey(sel);

    // Use the cache as-is if it is less than 3/4 full
    mask_t newOccupied = cache->occupied() + 1;
    mask_t capacity = cache->capacity();
    if (cache->isConstantEmptyCache()) {
        // Cache is read-only. Replace it.
        cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
    }
    else if (newOccupied <= capacity / 4 * 3) {
        // Cache is less than 3/4 full. Use it as-is.
    }
    else {
        // Cache is too full. Expand it.
        cache->expand();
    }

    // Scan for the first unused slot and insert there.
    // There is guaranteed to be an empty slot because the 
    // minimum size is 4 and we resized at 3/4 full.
    bucket_t *bucket = cache->find(key, receiver);
    if (bucket->key() == 0) cache->incrementOccupied();
    bucket->set(key, imp);
}

cache_fill_nolock 方法乍一看有些復(fù)雜,我們不妨將它分解一下:

第一行代碼還是加鎖的判斷,我們直接略過(guò),來(lái)到第二行:

if (cache_getImp(cls, sel)) return;
  • 通過(guò) cache_getImp 來(lái)判斷當(dāng)前 cls 下的 sel 是否已經(jīng)被緩存了,如果是,直接返回。而 cache_getImp 底層實(shí)現(xiàn)是 _cache_getImp,并且是在匯編層實(shí)現(xiàn)的。
cache_t *cache = getCache(cls);
cache_key_t key = getKey(sel);
  • 調(diào)用 getCache 來(lái)獲取 cls 的方法緩存,然后通過(guò) getKey 來(lái)獲取到緩存的 key,這里的 getKey 其實(shí)是將 SEL 類(lèi)型強(qiáng)轉(zhuǎn)成 cache_key_t 類(lèi)型。
mask_t newOccupied = cache->occupied() + 1;
  • cache 已經(jīng)占用的基礎(chǔ)上進(jìn)行加 1,得到的是新的緩存占用大小 newOccupied。
mask_t capacity = cache->capacity();
  • 然后讀取現(xiàn)在緩存的容量 capacity。

然后接下來(lái)是一系列的判斷:

if (cache->isConstantEmptyCache()) {
    // Cache is read-only. Replace it.
    cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
}
  • 如果緩存為空了,那么就重新申請(qǐng)一下內(nèi)存并覆蓋之前的緩存,之所以這樣做是因?yàn)榫彺媸侵蛔x的。
else if (newOccupied <= capacity / 4 * 3) {
        // Cache is less than 3/4 full. Use it as-is.
}
  • 如果新的緩存占用大小 小于等于 緩存容量的四分之三,則可以進(jìn)行緩存流程
else {
        // Cache is too full. Expand it.
        cache->expand();
}
  • 如果緩存不為空,且緩存占用大小已經(jīng)超過(guò)了容量的四分之三,則需要進(jìn)行擴(kuò)容。
bucket_t *bucket = cache->find(key, receiver);
  • 通過(guò)前面生成的 key 在緩存中查找對(duì)應(yīng)的 bucket_t,也就是對(duì)應(yīng)的方法實(shí)現(xiàn)。
if (bucket->key() == 0) cache->incrementOccupied();
bucket->set(key, imp);
  • 判斷獲取到的 bucket 是否是新的桶,如果是的話(huà),就在緩存里面增加一個(gè)占用大小。然后把 keyimp 放到桶里面。

cache_fill_nolock 的基本流程我們分析完了,這個(gè)方法主要針對(duì)的是沒(méi)有緩存的情況。<br />但是這個(gè)方法里面的 cache->find 我們并不知道是怎么實(shí)現(xiàn)的,我們接著探索這個(gè)方法:

2.2 查找緩存策略

bucket_t * cache_t::find(cache_key_t k, id receiver)
{
    assert(k != 0);

    bucket_t *b = buckets();
    mask_t m = mask();
    mask_t begin = cache_hash(k, m);
    mask_t i = begin;
    do {
        if (b[i].key() == 0  ||  b[i].key() == k) {
            return &b[i];
        }
    } while ((i = cache_next(i, m)) != begin);

    // hack
    Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
    cache_t::bad_cache(receiver, (SEL)k, cls);
}

find 方法我們乍一看會(huì)發(fā)現(xiàn)有一個(gè) do-while 循環(huán),因?yàn)檫@個(gè)方法的作用是根據(jù) key 查找 IMP,但需要注意的是,這里返回的并不是一個(gè) IMP,而是 bucket_t 結(jié)構(gòu)體指針。

  • 通過(guò) buckets() 方法獲取當(dāng)前 cache_t 下所有的緩存。
  • 通過(guò) mask() 方法獲取當(dāng)前 cache_t 的緩存大小減一的值 mask_t
  • 然后把 mask_t 的值作為循環(huán)的索引。
  • do-while 循環(huán)里遍歷整個(gè) bucket_t,如果 key 為 0,說(shuō)明當(dāng)前索引位置上還沒(méi)有緩存過(guò)方法,則需要停止循環(huán),返回當(dāng)前位置上的 bucket_t;如果 key 為要查詢(xún)的 k,說(shuō)明緩存命中了,則直接返回結(jié)果。
  • 這里的循環(huán)遍歷是通過(guò) cache_next 方法實(shí)現(xiàn)的,這個(gè)方法內(nèi)部就是當(dāng)前下標(biāo) imask_t 的值進(jìn)行與操作,來(lái)實(shí)現(xiàn)索引更新的。

三、cache_t 探索后的疑問(wèn)點(diǎn)

整個(gè) cache_t 的工作流程,簡(jiǎn)略描述如下:

  • 當(dāng)前查找的 IMP 沒(méi)有被緩存,調(diào)用 cache_fill_nolock 方法進(jìn)行填充緩存。
  • 當(dāng)前查找的 IMP 已經(jīng)被緩存了,然后判斷緩存容量是否已經(jīng)達(dá)到 3/4 的臨界點(diǎn)
    • 如果已經(jīng)到了臨界點(diǎn),則需要進(jìn)行擴(kuò)容,擴(kuò)容大小為原來(lái)緩存大小的 2 倍。擴(kuò)容后處于效率的考慮,會(huì)清空之前的內(nèi)容,然后把當(dāng)前要查找的 IMP 通過(guò) cache_fill_nolock 方法緩存起來(lái)。
    • 如果沒(méi)有到臨界點(diǎn),那么直接返回找到的 IMP。

我們梳理完 cache_t 的大致流程之后,我們還有一些遺留問(wèn)題沒(méi)有解決,接下來(lái)一一來(lái)解決一下。

3.1 mask 的作用

我們先回顧一下 mask 出現(xiàn)在了哪些地方:

setBucketsAndMask(newBuckets, newCapacity - 1);

void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
{
    mega_barrier();

    _buckets = newBuckets;
    
    mega_barrier();
    
    _mask = newMask;
    _occupied = 0;
}

mask_t cache_t::capacity() 
{
    return mask() ? mask()+1 : 0; 
}

首先,mask 是作為 cache_t 的屬性存在的,它代表的是緩存容量的大小減一的值。這一點(diǎn)在 setBucketsAndMaskcapacity 方法中可以得到證實(shí)。

cache_fill_nolock {
    cache_key_t key = getKey(sel);
    
    bucket_t *bucket = cache->find(key, receiver);
}

find { 

    // Class points to cache. SEL is key. Cache buckets store SEL+IMP.
    // Caches are never built in the dyld shared cache.
    static inline mask_t cache_hash(cache_key_t key, mask_t mask) 
    {
        return (mask_t)(key & mask);
    }
    
    static inline mask_t cache_next(mask_t i, mask_t mask) {
        return (i+1) & mask;
    }
}

根據(jù)上面的偽代碼,cache_fill_nolock 方法里面,會(huì)先根據(jù)要查找的 sel 強(qiáng)轉(zhuǎn)成 cache_key_t 結(jié)構(gòu),這是因?yàn)?sel 其實(shí)為方法名:

image

而經(jīng)過(guò)強(qiáng)轉(zhuǎn)之后為:

image

也就是說(shuō)最后緩存的 key 其實(shí)是一個(gè)無(wú)符號(hào)長(zhǎng)整型值,這樣相對(duì)于直接拿字符串來(lái)作為鍵值,明顯效率更高。

經(jīng)過(guò)強(qiáng)轉(zhuǎn)之后,把 key 傳給 find 方法。然后會(huì)有一個(gè) cache_hash 方法,其注釋如下:

類(lèi)指向緩存,SEL 是鍵,buckets緩存存儲(chǔ)的是 SEL + IMP。
方法緩存永遠(yuǎn)不會(huì)存儲(chǔ)在 dyld 共享緩存里面。

image

實(shí)際測(cè)試如上圖所示,cache_hash 方法其實(shí)就是哈希算法,得到的是一個(gè)哈希值。拿到這個(gè)哈希值后就可以在哈希表中進(jìn)行查詢(xún)。在 find 方法中就是獲得索引的起始值。

image

通過(guò)上圖的測(cè)試我們可以得出這里是使用的 LRU 緩存算法。

LRU 算法的全稱(chēng)是 Least Recently Used ,也就是最近最少使用策略。這個(gè)策略的核心思想就是先淘汰最近最少使用的內(nèi)容。

3.2 capacity 的變化

capacity 的變化主要發(fā)生在擴(kuò)容的時(shí)候,當(dāng)緩存已經(jīng)占滿(mǎn)了四分之三的時(shí)候,會(huì)進(jìn)行兩倍原來(lái)緩存空間大小的擴(kuò)容,這一步是為了避免哈希沖突。

3.3 為什么是在 3/4 時(shí)進(jìn)行擴(kuò)容

在哈希這種數(shù)據(jù)結(jié)構(gòu)里面,有一個(gè)概念叫裝載因子,裝載因子是用來(lái)表示空位的多少。其公式為:

散列表的裝載因子=填入表中的元素個(gè)數(shù)/散列表的長(zhǎng)度

裝載因子越大,說(shuō)明空閑位置越少,沖突越多,散列表的性能會(huì)下降。
蘋(píng)果這里設(shè)計(jì)的裝載因子顯然為 1 - 3/4 = 1/4 => 0.25 。
因?yàn)楸举|(zhì)上方法緩存就是為了更快的執(zhí)行效率,所以為了避免發(fā)生哈希沖突,在采用開(kāi)放尋址法的前提下,盡可能小的裝載因子可以提高散列表的性能。

/* Initial cache bucket count. INIT_CACHE_SIZE must be a power of two. */
enum {
    INIT_CACHE_SIZE_LOG2 = 2,
    INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2)
};

cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);

初始化的緩存大小是 1 左移 2,結(jié)果為 4。然后在 reallocate 方法進(jìn)行一下緩存的重新開(kāi)辟。這也就意味著初始的緩存空間大小為 4。

3.4 方法緩存是否有序

方法緩存是無(wú)序的,這是因?yàn)橛?jì)算緩存下標(biāo)是一個(gè)哈希算法:

static inline mask_t cache_hash(cache_key_t key, mask_t mask) 
{
    return (mask_t)(key & mask);
}

通過(guò) cache_hash 之后計(jì)算出來(lái)的下標(biāo)并不是有序的,下標(biāo)值取決于 keymask 的值。

3.5 bucket 與 mask, capacity, sel, imp 的關(guān)系

一個(gè)類(lèi)有一個(gè)屬性 cache_t,而一個(gè) cache_tbuckets 會(huì)有多個(gè) bucket。一個(gè) bucket 存儲(chǔ)的是 impcache_key_t 。

mask 的值對(duì)于 bucket 來(lái)說(shuō),主要是用來(lái)在緩存查找時(shí)的哈希算法。
capacity 則可以獲取到 cache_tbucket 的數(shù)量。

sel 在緩存的時(shí)候是被強(qiáng)轉(zhuǎn)成了 cache_key_t 的形式,更方便查詢(xún)使用。
imp 則是函數(shù)指針,也就是方法的具體實(shí)現(xiàn),緩存的主要目的就是通過(guò)一系列策略讓編譯器更快的執(zhí)行消息發(fā)送的邏輯。

四、總結(jié)

  • OC 中實(shí)例方法緩存在類(lèi)上面,類(lèi)方法緩存在元類(lèi)上面。
  • cache_t 緩存會(huì)提前進(jìn)行擴(kuò)容防止溢出。
  • 方法緩存是為了最大化的提高程序的執(zhí)行效率。
  • 蘋(píng)果在方法緩存這里用的是開(kāi)放尋址法來(lái)解決哈希沖突。
  • 通過(guò) cache_t 我們可以進(jìn)一步延伸去探究 objc_msgSend,因?yàn)椴檎曳椒ň彺媸菍儆?objc_msgSend 查找方法實(shí)現(xiàn)的快速流程。

我們下一篇將開(kāi)始探索 iOS 中方法的底層原理,敬請(qǐng)期待~

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