iOS內存管理-深入解析自動釋放池

主要內容:

  1. AutoreleasePool簡介
  2. AutoreleasePool底層原理
  3. AutoreleaseNSThreadNSRunLoop的關系
  4. AutoreleasePool在主線程上的釋放時機
  5. AutoreleasePool在子線程上的釋放時機
  6. AutoreleasePool需要手動添加的情況

一、Autorelease簡介

iOS開發中的Autorelease機制是為了延時釋放對象。自動釋放的概念看上去很像ARC,但實際上這更類似于C語言中自動變量的特性。

  • 自動變量:在超出變量作用域后將被廢棄;
  • 自動釋放池:在超出釋放池生命周期后,向其管理的對象實例的發送release消息;
1.1 MRC下使用自動釋放池

在MRC環境中使用自動釋放池需要用到NSAutoreleasePool對象,其生命周期就相當于C語言變量的作用域。對于所有調用過autorelease方法的對象,在廢棄NSAutoreleasePool對象時,都將調用release實例方法。用源代碼表示如下:

//MRC環境下的測試:
//第一步:生成并持有釋放池NSAutoreleasePool對象;
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

//第二步:調用對象的autorelease實例方法;
id obj = [[NSObject alloc] init];
[obj autorelease];

//第三步:廢棄NSAutoreleasePool對象;
[pool drain];   //向pool管理的所有對象發送消息,相當于[obj release]

//obi已經釋放,再次調用會崩潰(Thread 1: EXC_BAD_ACCESS (code=EXC_I386_GPFLT))
NSLog(@"打印obj:%@", obj); 

理解NSAutoreleasePool對象的生命周期,如下圖所示:

NSAutoreleasePool對象的生命周期.png
1.2 ARC下使用自動釋放池

ARC環境不能使用NSAutoreleasePool類也不能調用autorelease方法,代替它們實現對象自動釋放的是@autoreleasepool塊和__autoreleasing修飾符。比較兩種環境下的代碼差異如下圖:

對比MRC與ARC的自動釋放池使用.png

如圖所示,@autoreleasepool塊替換了NSAutoreleasePoool類對象的生成、持有及廢棄這一過程。而附有__autoreleasing修飾符的變量替代了autorelease方法,將對象注冊到了Autoreleasepool;由于ARC的優化,__autorelease是可以被省略的,所以簡化后的ARC代碼如下:

//ARC環境下的測試:
@autoreleasepool {
    id obj = [[NSObject alloc] init];
    NSLog(@"打印obj:%@", obj); 
}

顯式使用__autoreleasing修飾符的情況非常少見,這是因為ARC的很多情況下,即使是不顯式的使用__autoreleasing,也能實現對象被注冊到釋放池中。主要包括以下幾種情況:

  1. 編譯器會進行優化,檢查方法名是否以alloc/new/copy/mutableCopy開始,如果不是則自動將返回對象注冊到Autoreleasepool;
  2. 訪問附有__weak修飾符的變量時,實際上必定要訪問注冊到Autoreleasepool的對象,即會自動加入Autoreleasepool;
  3. id的指針或對象的指針(id*,NSError **),在沒有顯式地指定修飾符時候,會被默認附加上__autoreleasing修飾符,加入Autoreleasepool

注意:如果編譯器版本為LLVM.3.0以上,即使ARC無效@autoreleasepool塊也能夠使用;如下源碼所示:

//MRC環境下的測試:
@autoreleasepool{
    id obj = [[NSObject alloc] init];
    [obj autorelease];
}

二、AutoRelease原理

2.1 使用@autoreleasepool{}

我們在main函數中寫入自動釋放池相關的測試代碼如下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"Hello, World!");
    }
    return 0;
}

為了探究釋放池的底層實現,我們在終端使用clang -rewrite-objc + 文件名命令將上述OC代碼轉化為C++源碼:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */
    {
        __AtAutoreleasePool __autoreleasepool;
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_3f_crl5bnj956d806cp7d3ctqhm0000gn_T_main_d37e0d_mi_0);
     }//大括號對應釋放池的作用域
     
     return 0;
}

在經過編譯器clang命令轉化后,我們看到的所謂的@autoreleasePool塊,其實對應著__AtAutoreleasePool的結構體。

2.2 分析結構體__AtAutoreleasePool的具體實現

在源碼中找到__AtAutoreleasePool結構體的實現代碼,具體如下:

extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

__AtAutoreleasePool結構體包含了:構造函數、析構函數和一個邊界對象;

  • 構造函數內部調用:objc_autoreleasePoolPush()方法,返回邊界對象atautoreleasepoolobj
  • 析構函數內部調用:objc_autoreleasePoolPop()方法,傳入邊界對象atautoreleasepoolobj

分析main函數中__autoreleasepool結構體實例的生命周期是這樣的:
__autoreleasepool是一個自動變量,其構造函數是在程序執行到聲明這個對象的位置時調用的,而其析構函數則是在程序執行到離開這個對象的作用域時調用。所以,我們可以將上面main函數的代碼簡化如下:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ {
        void *atautoreleasepoolobj = objc_autoreleasePoolPush();
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_kb_06b822gn59df4d1zt99361xw0000gn_T_main_d39a79_mi_0);
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
    return 0;
}
2.3 objc_autoreleasePoolPush與objc_autoreleasePoolPop

進一步觀察自動釋放池構造函數與析構函數的實現,其實它們都只是對AutoreleasePoolPage對應靜態方法pushpop的封裝

void *objc_autoreleasePoolPush(void) {
    return AutoreleasePoolPage::push();
}

void objc_autoreleasePoolPop(void *ctxt) {
    AutoreleasePoolPage::pop(ctxt);
}
2.4 理解AutoreleasePoolPage

AutoreleasePoolPage是一個C++中的類,打開Runtime的源碼工程,在NSObject.mm文件中可以找到它的定義,摘取其中的關鍵代碼如下:

//大致在641行代碼開始
class AutoreleasePoolPage {
#   define EMPTY_POOL_PLACEHOLDER ((id*)1)  //空池占位
#   define POOL_BOUNDARY nil                //邊界對象(即哨兵對象)
    static pthread_key_t const key = AUTORELEASE_POOL_KEY;
    static uint8_t const SCRIBBLE = 0xA3;  // 0xA3A3A3A3 after releasing
    static size_t const SIZE = 
#if PROTECT_AUTORELEASEPOOL
        PAGE_MAX_SIZE;  // must be multiple of vm page size
#else
        PAGE_MAX_SIZE;  // size and alignment, power of 2
#endif
    static size_t const COUNT = SIZE / sizeof(id);
    magic_t const magic;                  //校驗AutoreleasePagePoolPage結構是否完整
    id *next;                             //指向新加入的autorelease對象的下一個位置,初始化時指向begin()
    pthread_t const thread;               //當前所在線程,AutoreleasePool是和線程一一對應的
    AutoreleasePoolPage * const parent;   //指向父節點page,第一個結點的parent值為nil
    AutoreleasePoolPage *child;           //指向子節點page,最后一個結點的child值為nil
    uint32_t const depth;                 //鏈表深度,節點個數
    uint32_t hiwat;                       //數據容納的一個上限
    //......
};

其實,每個自動釋放池都是是由若干個AutoreleasePoolPage組成的雙向鏈表結構,如下圖所示:

AutoreleasePool.png

AutoreleasePoolPage中擁有parentchild指針,分別指向上一個和下一個page;當前一個page的空間被占滿(每個AutorelePoolPage的大小為4096字節)時,就會新建一個AutorelePoolPage對象并連接到鏈表中,后來的 Autorelease對象也會添加到新的page中;

另外,當next== begin()時,表示AutoreleasePoolPage為空;當next == end(),表示AutoreleasePoolPage已滿。

2.5 理解哨兵對象/邊界對象(POOL_BOUNDARY)的作用

AutoreleasePoolPage的源碼中,我們很容易找到邊界對象(哨兵對象)的定義:

#define POOL_BOUNDARY nil

邊界對象其實就是nil的別名,而它的作用事實上也就是為了起到一個標識的作用。

每當自動釋放池初始化調用objc_autoreleasePoolPush方法時,總會通過AutoreleasePoolPagepush方法,將POOL_BOUNDARY放到當前page的棧頂,并且返回這個邊界對象;

而在自動釋放池釋放調用objc_autoreleasePoolPop方法時,又會將邊界對象以參數傳入,這樣自動釋放池就會向釋放池中對象發送release消息,直至找到第一個邊界對象為止。

2.6 理解objc_autoreleasePoolPush方法

經過前面的分析,objc_autoreleasePoolPush最終調用的是 AutoreleasePoolPagepush方法,該方法的具體實現如下:

static inline void *push() {
   return autoreleaseFast(POOL_BOUNDARY);
}

static inline id *autoreleaseFast(id obj)
{
   AutoreleasePoolPage *page = hotPage();
   if (page && !page->full()) {
       return page->add(obj);
   } else if (page) {
       return autoreleaseFullPage(obj, page);
   } else {
1.        return autoreleaseNoPage(obj);
   }
}

//壓棧操作:將對象加入AutoreleaseNoPage并移動棧頂的指針
id *add(id obj) {
    id *ret = next;
    *next = obj;
    next++;
    return ret;
}

//當前hotPage已滿時調用
static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) {
    do {
        if (page->child) page = page->child;
        else page = new AutoreleasePoolPage(page);
    } while (page->full());

    setHotPage(page);
    return page->add(obj);
}

//當前hotpage不存在時調用
static id *autoreleaseNoPage(id obj) {
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    setHotPage(page);

    if (obj != POOL_SENTINEL) {
        page->add(POOL_SENTINEL);
    }

    return page->add(obj);
}

觀察上述代碼,每次調用push其實就是創建一個新的AutoreleasePool,在對應的AutoreleasePoolPage中插入一個POOL_BOUNDARY ,并且返回插入的POOL_BOUNDARY 的內存地址。push方法內部調用的是autoreleaseFast方法,并傳入邊界對象(POOL_BOUNDARY)。hotPage可以理解為當前正在使用的AutoreleasePoolPage

自動釋放池最終都會通過page->add(obj)方法將邊界對象添加到釋放池中,而這一過程在autoreleaseFast方法中被分為三種情況:

  1. 當前page存在且不滿,調用page->add(obj)方法將對象添加至page的棧中,即next指向的位置
  2. 當前page存在但是已滿,調用autoreleaseFullPage初始化一個新的page,調用page->add(obj)方法將對象添加至page的棧中
  3. 當前page不存在時,調用autoreleaseNoPage創建一個hotPage,再調用page->add(obj) 方法將對象添加至page的棧中
2.7 objc_autoreleasePoolPop方法

AutoreleasePool的釋放調用的是objc_autoreleasePoolPop方法,此時需要傳入邊界對象作為參數。這個邊界對象正是每次執行objc_autoreleasePoolPush方法返回的對象atautoreleasepoolobj

同理,我們找到objc_autoreleasePoolPop最終調用的方法,即AutoreleasePoolPagepop方法,該方法的具體實現如下:

static inline void pop(void *token)   //POOL_BOUNDARY的地址
{
    AutoreleasePoolPage *page;
    id *stop;

    page = pageForPointer(token);   //通過POOL_BOUNDARY找到對應的page
    stop = (id *)token;
    if (DebugPoolAllocation  &&  *stop != POOL_SENTINEL) {
        // This check is not valid with DebugPoolAllocation off
        // after an autorelease with a pool page but no pool in place.
        _objc_fatal("invalid or prematurely-freed autorelease pool %p; ", 
                    token);
    }

    if (PrintPoolHiwat) printHiwat();   // 記錄最高水位標記

    page->releaseUntil(stop);   //向棧中的對象發送release消息,直到遇到第一個哨兵對象

    // memory: delete empty children
    // 刪除空掉的節點
    if (DebugPoolAllocation  &&  page->empty()) {
        // special case: delete everything during page-per-pool debugging
        AutoreleasePoolPage *parent = page->parent;
        page->kill();
        setHotPage(parent);
    } else if (DebugMissingPools  &&  page->empty()  &&  !page->parent) {
        // special case: delete everything for pop(top) 
        // when debugging missing autorelease pools
        page->kill();
        setHotPage(nil);
    } 
    else if (page->child) {
        // hysteresis: keep one empty child if page is more than half full
        if (page->lessThanHalfFull()) {
            page->child->kill();
        }
        else if (page->child->child) {
            page->child->child->kill();
        }
    }
}

上述代碼中,首先根據傳入的邊界對象地址找到邊界對象所處的page;然后選擇當前page中最新加入的對象一直向前清理,可以向前跨越若干個page,直到邊界所在的位置;清理的方式是向這些對象發送一次release消息,使其引用計數減一;

另外,清空page對象還會遵循一些原則:

  1. 如果當前的page中存放的對象少于一半,則子page全部刪除;
  2. 如果當前當前的page存放的多余一半(意味著馬上將要滿),則保留一個子page,節省創建新page的開銷;
2.8 autorelease方法

上述是對自動釋放池整個生命周期的分析,現在我們來理解延時釋放對象autorelease方法的實現,首先查看該方法的調用棧:

- [NSObject autorelease]
└── id objc_object::rootAutorelease()
    └── id objc_object::rootAutorelease2()
        └── static id AutoreleasePoolPage::autorelease(id obj)
            └── static id AutoreleasePoolPage::autoreleaseFast(id obj)
                ├── id *add(id obj)
                ├── static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
                │   ├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
                │   └── id *add(id obj)
                └── static id *autoreleaseNoPage(id obj)
                    ├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
                    └── id *add(id obj)

如上所示,autorelease方法最終也會調用上面提到的 autoreleaseFast方法,將當前對象加到AutoreleasePoolPage中。關于autoreleaseFast的分析這里不再累述,我們主要來考慮一下兩次調用的區別:

autorelease函數和push函數一樣,關鍵代碼都是調用autoreleaseFast函數向自動釋放池的鏈表棧中添加一個對象,不過push函數入棧的是一個邊界對象,而autorelease函數入棧的是一個具體的Autorelease的對象。

三、AutoreleasePool與NSThread、NSRunLoop的關系

由于AppKitUIKit框架的優化,我們很少需要顯式的創建一個自動釋放池塊。這其中就涉及到AutoreleasePoolNSThreadNSRunLoop的關系。

3.1 RunLoop和NSThread的關系

RunLoop是用于控制線程生命周期并接收事件進行處理的機制,其實質是一個do-While循環。在蘋果文檔找到關于NSRunLoop的介紹如下:

Your application neither creates or explicitly manages NSRunLoop objects. Each NSThread object—including the application’s main thread—has an NSRunLoop object automatically created for it as needed. If you need to access the current thread’s run loop, you do so with the class method currentRunLoop.

總結RunLoopNSThread(線程)之間的關系如下:

  1. RunLoop與線程是一一對應關系,每個線程(包括主線程)都有一個對應的RunLoop對象;其對應關系保存在一個全局的Dictionary里;
  2. 主線程的RunLoop默認由系統自動創建并啟動;而其他線程在創建時并沒有RunLoop,若該線程一直不主動獲取,就一直不會有RunLoop
  3. 蘋果不提供直接創建RunLoop的方法;所謂其他線程Runloop的創建其實是發生在第一次獲取的時候,系統判斷當前線程沒有RunLoop就會自動創建;
  4. 當前線程結束時,其對應的Runloop也被銷毀;
3.2 RunLoop和AutoreleasePool的關系

蘋果文檔中找到兩者關系的介紹如下:

The Application Kit creates an autorelease pool on the main thread at the beginning of every cycle of the event loop, and drains it at the end, thereby releasing any autoreleased objects generated while processing an event.

如上所述,主線程的NSRunLoop在監測到事件響應開啟每一次event loop之前,會自動創建一個autorelease pool,并且會在event loop結束的時候執行drain操作,釋放其中的對象。

3.3 Thread和AutoreleasePool的關系

蘋果文檔中找到兩者關系的介紹如下:

Each thread (including the main thread) maintains its own stack of NSAutoreleasePool objects (see Threads). As new pools are created, they get added to the top of the stack. When pools are deallocated, they are removed from the stack. Autoreleased objects are placed into the top autorelease pool for the current thread. When a thread terminates, it automatically drains all of the autorelease pools associated with itself.

如上所述, 包括主線程在內的所有線程都維護有它自己的自動釋放池的堆棧結構。新的自動釋放池被創建的時候,它們會被添加到棧的頂部,而當池子銷毀的時候,會從棧移除。對于當前線程來說,Autoreleased對象會被放到棧頂的自動釋放池中。當一個線程線程停止,它會自動釋放掉與其關聯的所有自動釋放池。

四、AutoreleasePool在主線程上的釋放時機

4.1 理解主線程上的自動釋放過程

分析主線程RunLoop管理自動釋放池并釋放對象的詳細過程,我們在如下Demo中的主線程中設置斷點,并執行lldb命令:po [NSRunLoop currentRunLoop],具體效果如下:

autoreleasepool在主線程的釋放時機.png

我們看到主線程RunLoop中有兩個與自動釋放池相關的Observer,它們的 activities分別為0x10xa0這兩個十六進制的數,轉為二進制分別為110100000,對應CFRunLoopActivity的類型如下:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),          //0x1,啟動Runloop循環
    kCFRunLoopBeforeTimers = (1UL << 1),            
    kCFRunLoopBeforeSources = (1UL << 2),        
    kCFRunLoopBeforeWaiting = (1UL << 5),  //0xa0,即將進入休眠     
    kCFRunLoopAfterWaiting = (1UL << 6),   
    kCFRunLoopExit = (1UL << 7),           //0xa0,退出RunLoop循環  
    kCFRunLoopAllActivities = 0x0FFFFFFFU
    };

結合RunLoop監聽的事件類型,分析主線程上自動釋放池的使用過程如下:
1.App啟動后,蘋果在主線程RunLoop里注冊了兩個Observer,其回調都是 _wrapRunLoopWithAutoreleasePoolHandler();

  1. 第一個Observer監視的事件是Entry(即將進入Loop),其回調內會調用 _objc_autoreleasePoolPush()創建自動釋放池。order = -2147483647(即 32`位整數最小值)表示其優先級最高,可以保證創建釋放池發生在其他所有回調之前;
  2. 第二個Observer監視了兩個事件BeforeWaiting(準備進入休眠)時調用_objc_autoreleasePoolPop()_objc_autoreleasePoolPush()釋放舊的池并創建新池;Exit(即將退出Loop) 時調用 _objc_autoreleasePoolPop()來釋放自動釋放池。order = 2147483647(即32位整數的最大值)表示其優先級最低,保證其釋放池子發生在其他所有回調之后;
  3. 在主線程執行的代碼,通常是寫在諸如事件回調、Timer回調內的。這些回調會被 RunLoop創建好的AutoreleasePool環繞著,所以不會出現內存泄漏,開發者也不必顯示創建AutoreleasePool了;

最后,也可以結合圖示理解主線程上自動釋放對象的具體流程:

自動釋放池系統釋放原理圖.png
  1. 程序啟動到加載完成后,主線程對應的RunLoop會停下來等待用戶交互
  2. 用戶的每一次交互都會啟動一次運行循環,來處理用戶所有的點擊事件、觸摸事件。
  3. RunLoop檢測到事件后,就會創建自動釋放池;
  4. 所有的延遲釋放對象都會被添加到這個池子中;
  5. 在一次完整的運行循環結束之前,會向池中所有對象發送release消息,然后自動釋放池被銷毀;
4.2 測試主線程上的對象自動釋放過程

下面的代碼創建了一個Autorelease對象string,并且通過weakString進行弱引用(不增加引用計數,所以不會影響對象的生命周期),具體如下:

@interface TestMemoryVC ()
@property (nonatomic,weak)NSString *weakString;
@end

@implementation TestMemoryVC
- (void)viewDidLoad {
    [super viewDidLoad];
    NSString *string = [NSString stringWithFormat:@"%@",@"WUYUBEICHEN"];
    self.weakString = string;
}

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    NSLog(@"viewWillAppear:%@", self.weakString);
}

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    NSLog(@"viewDidAppear:%@", self.weakString);
}

@end

//打印結果:
//viewWillAppear:WUYUBEICHEN
//viewDidAppear:(null)

代碼分析:自動變量的string在離開viewDidLoad的作用域后,會依靠當前主線程上的RunLoop迭代自動釋放。最終string對象在viewDidAppear方法執行前被釋放(RunLoop完成此次迭代)。

五、AutoreleasePool子線程上的釋放時機

子線程默認不開啟RunLoop,那么其中的延時對象該如何釋放呢?其實這依然要從ThreadAutoreleasePool的關系來考慮:

Each thread (including the main thread) maintains its own stack of NSAutoreleasePool objects.

也就是說,每一個線程都會維護自己的 Autoreleasepool棧,所以子線程雖然默認沒有開啟RunLoop,但是依然存在AutoreleasePool,在子線程退出的時候會去釋放autorelease對象。

前面講到過,ARC會根據一些情況進行優化,添加__autoreleasing修飾符,其實這就相當于對需要延時釋放的對象調用了autorelease方法。從源碼分析的角度來看,如果子線程中沒有創建AutoreleasePool ,而一旦產生了Autorelease對象,就會調用autoreleaseNoPage方法自動創建hotpage,并將對象加入到其棧中。所以,一般情況下,子線程中即使我們不手動添加自動釋放池,也不會產生內存泄漏。

六、AutoreleasePool需要手動添加的情況

盡管ARC已經做了諸多優化,但是有些情況我們必須手動創建AutoreleasePool,而其中的延時對象將在當前釋放池的作用域結束時釋放。蘋果文檔中說明了三種情況,我們可能會需要手動添加自動釋放池:

  1. 編寫的不是基于UI框架的程序,例如命令行工具;
  2. 通過循環方式創建大量臨時對象;
  3. 使用非Cocoa程序創建的子線程;

而在ARC環境下的實際開發中,我們最常遇到的也是第二種情況,以下面的代碼為例:

- (void)viewDidLoad {
    [super viewDidLoad];
    for (int i = 0; i < 1000000; i++) {
        NSObject *obj = [[NSObject alloc] init];
        NSLog(@"打印obj:%@", obj);
    }
 }

上述代碼中,obj因為離開作用域所以會被加入最近一次創建的自動釋放池中,而這個釋放池就是主線程上的RunLoop管理的;因為for循環在當前線程沒有執行完畢,Runloop也就沒有完成當前這一次的迭代,所以導致大量對象被延時釋放。釋放池中的對象將會在viewDidAppear方法執行前就被銷毀。在此情況下,我們就有必要通過手動干預的方式及時釋放不需要的對象,減少內存消耗;優化的代碼如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    for (int i = 0; i < 1000000; i++) {
        @autoreleasepool{
             NSObject *obj = [[NSObject alloc] init];
             NSLog(@"打印obj:%@", obj);
        }
    }
 }

參考鏈接

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