iOS多線程——你要知道的RunLoop都在這里

你要知道的iOS多線程NSThread、GCD渣刷、NSOperation、RunLoop都在這里

轉(zhuǎn)載請注明出處 http://www.reibang.com/p/cfe5132e975f

本系列文章主要講解iOS中多線程的使用,包括:NSThread防楷、GCD、NSOperation以及RunLoop的使用方法詳解则涯,本系列文章不涉及基礎(chǔ)的線程/進程复局、同步/異步、阻塞/非阻塞粟判、串行/并行亿昏,這些基礎(chǔ)概念,有不明白的讀者還請自行查閱档礁。本系列文章將分以下幾篇文章進行講解角钩,讀者可按需查閱。

RunLoop 基本概念

前面幾篇文章詳細講解了創(chuàng)建多線程的方法和多線程編程的相關(guān)知識递礼,當我們使用NSThread進行多線程編程時惨险,只要任務(wù)結(jié)束,線程也就退出了脊髓,每次執(zhí)行一個任務(wù)都需要創(chuàng)建一個線程非常浪費資源平道,所以需要一種能夠使線程常駐內(nèi)存不退出d,當有任務(wù)來臨時能隨時執(zhí)行的方法供炼,這就是RunLoop的作用一屋。類似于javascriptEvent Loop模型,大致類似于如下代碼:

int retVal = Running;
do {
     // 執(zhí)行各種任務(wù)袋哼,處理各種事件
     // ......
} while (retVal != Stop && retVal != Timeout);

上述循環(huán)只有在特定條件才才會退出冀墨,否則就會一直在循環(huán)中處理各種任務(wù)或事件,諸如觸摸屏幕事件涛贯、手勢事件诽嘉、定時器事件、用戶提交的任務(wù)弟翘、各種方法的執(zhí)行等虫腋。

RunLoop與線程關(guān)聯(lián)的,是一種事件處理環(huán)稀余,用來安排和協(xié)調(diào)到來的事件悦冀,目的就是讓其關(guān)聯(lián)的線程在有事件到達時時刻保持運行狀態(tài),而當沒有事件需要處理時進入睡眠狀態(tài)從而節(jié)約資源睛琳,每一個線程都可以有一個RunLoop對象與之對應(yīng)盒蟆,并且是在第一次獲取它是系統(tǒng)自動創(chuàng)建的,比如主線程關(guān)聯(lián)的RunLoop师骗,我們都知道程序的入口函數(shù)是main函數(shù)历等,下面是創(chuàng)建工程后Xcode自動生成的main.m文件的main函數(shù)代碼:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

該方法執(zhí)行體被autoreleasepool包圍,所以程序可以使用ARC來管理內(nèi)存辟癌,后面會講解RunLoopautoreleasepool的關(guān)系寒屯,main函數(shù)直接返回了UIApplicationMain函數(shù),該函數(shù)內(nèi)部就會第一次獲取RunLoop對象黍少,所以系統(tǒng)就會創(chuàng)建這樣一個RunLoop對象寡夹,因此在沒有滿足特定條件的時候該主線程不會退出,應(yīng)用就可以持續(xù)運行而不會退出仍侥。

在官方文檔中使用下圖描述RunLoop模型:

官方RunLoop模型圖

從上圖可以看出一個線程會關(guān)聯(lián)一個RunLoop對象要出,RunLoop對象會一直循環(huán),直到超時或收到退出指令农渊。在無限循環(huán)的過程中會一直處理到來的事件,右側(cè)將事件分為了兩類,一類是Input sources這部分包括基于端口的source1事件砸紊,開發(fā)者提交的各種source0事件传于,調(diào)用performSelector:onThread:方法事件,還有一類Timer sources這個就是常用的定時器事件醉顽,這些事件在程序運行期間會不斷產(chǎn)生之后會由RunLoop對象檢測并負責處理相關(guān)事件沼溜。

RunLoop 源碼解析

RunLoop有兩個對象,NSRunLoopCFRunLoopRef游添,區(qū)別在于由Core Foundation框架提供的CFRunLoopRef是純C語言編寫的系草,提供的也是C語言接口,這些接口都是線程安全的唆涝,由Foundation框架提供的NSRunLoop是面向?qū)ο蟮恼叶迹腔?code>CFRunLoopRef的封裝,提供的都是面向?qū)ο蟮慕涌诶群ǎ@些接口不是線程安全的能耻,Core Foudation框架是開源的,可以在這個地址下載:Core Foundation開源代碼亡驰,本文接下來的內(nèi)容主要是針對該開源代碼進行講解晓猛。

首先,看一下在代碼中如何獲取RunLoop對象,在Foundation框架中的NSRunLoop類提供了如下兩個類屬性:

//獲取當前線程關(guān)聯(lián)的RunLoop對象
@property (class, readonly, strong) NSRunLoop *currentRunLoop;
//獲取主線程關(guān)聯(lián)的RunLoop對象
@property (class, readonly, strong) NSRunLoop *mainRunLoop

對應(yīng)的Core Foundation框架中提供了如下兩個函數(shù)來獲取RunLoop對象:

//獲得當前線程關(guān)聯(lián)的RunLoop對象
CFRunLoopGetCurrent(); 
// 獲得主線程關(guān)聯(lián)的RunLoop對象
CFRunLoopGetMain();

前面一直講每一個線程都會關(guān)聯(lián)一個RunLoop對象,并且不能通過手動創(chuàng)建該對象方援,只能在第一次獲取時系統(tǒng)自動創(chuàng)建送丰,看一下Core Foundation框架是如何實現(xiàn)的:

//CFRunLoopGetMain函數(shù)用于獲取主線程關(guān)聯(lián)的RunLoop對象
CFRunLoopRef CFRunLoopGetMain(void) {
    CHECK_FOR_FORK();
    //靜態(tài)變量保存主線程關(guān)聯(lián)的RunLoop對象
    static CFRunLoopRef __main = NULL; // no retain needed
    //如果主線程關(guān)聯(lián)的RunLoop對象為NULL就調(diào)用_CFRunLoopGet0函數(shù)獲取一個
    if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
    return __main
}

//獲取當前線程關(guān)聯(lián)的RunLoop對象
CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    //這一段沒找到對應(yīng)的函數(shù)...猜測是和上面的函數(shù)用意一樣
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    //如果上面沒找到就調(diào)用_CFRunLoopGet0函數(shù)去獲取一個
    return _CFRunLoopGet0(pthread_self());
}


//全局的可變字典數(shù)據(jù)結(jié)構(gòu),key為thread_t即線程珠叔,value為RunLoop對象
static CFMutableDictionaryRef __CFRunLoops = NULL;
//全局的一個鎖
static CFLock_t loopsLock = CFLockInit;

//_CFRunLoopGet0接收一個pthread_t對象,即線程對象,返回一個與之關(guān)聯(lián)的RunLoop對象
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    //判斷是否為主線程
    if (pthread_equal(t, kNilPthreadT)) {
    //pthread_main_thread_np()函數(shù)用來獲取主線程
    t = pthread_main_thread_np();
    }
    //加鎖蚓曼,防止產(chǎn)生競爭創(chuàng)建多個RunLoop對象
    __CFLock(&loopsLock);
    //如果全局的保存線程和runloop對象的字典為空
    if (!__CFRunLoops) {
        __CFUnlock(&loopsLock);
    //創(chuàng)建一個字典
    CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
    /*
    根據(jù)主線程創(chuàng)建RunLoop對象
    所以,當?shù)谝淮潍@取RunLoop對象時就會自動創(chuàng)建主線程關(guān)聯(lián)的RunLoop對象
    */
    CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
    //設(shè)置全局的字典钦扭,key為主線程纫版,value為主線程關(guān)聯(lián)的RunLoop對象
    CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
    if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
        CFRelease(dict);
    }
    CFRelease(mainLoop);
        __CFLock(&loopsLock);
    }
    //通過線程在字典中獲取RunLoop對象
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    //如果沒有獲取到
    if (!loop) {
    //沒有獲取到就根據(jù)線程創(chuàng)建一個RunLoop對象
    CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
    //再次獲取
    loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    if (!loop) {
        //字典中仍然沒有線程關(guān)聯(lián)的RunLoop對象就將剛才新創(chuàng)建加入到字典照中
        CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
        loop = newLoop;
    }
        // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
        __CFUnlock(&loopsLock);
    CFRelease(newLoop);
    }
    if (pthread_equal(t, pthread_self())) {
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
            //設(shè)置銷毀時的回調(diào)
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    //返回線程關(guān)聯(lián)的RunLoop對象
    return loop;
}

/*
真正的用于創(chuàng)建RunLoop對象的靜態(tài)函數(shù),形參為線程對象
該函數(shù)主要用于分配存儲空間客情,并進行RunLoop對象相關(guān)初始化操作
*/
static CFRunLoopRef __CFRunLoopCreate(pthread_t t) {
    CFRunLoopRef loop = NULL;
    CFRunLoopModeRef rlm;
    uint32_t size = sizeof(struct __CFRunLoop) - sizeof(CFRuntimeBase);
    loop = (CFRunLoopRef)_CFRuntimeCreateInstance(kCFAllocatorSystemDefault, CFRunLoopGetTypeID(), size, NULL);
    if (NULL == loop) {
    return NULL;
    }
    (void)__CFRunLoopPushPerRunData(loop);
    __CFRunLoopLockInit(&loop->_lock);
    loop->_wakeUpPort = __CFPortAllocate();
    if (CFPORT_NULL == loop->_wakeUpPort) HALT;
    __CFRunLoopSetIgnoreWakeUps(loop);
    loop->_commonModes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
    CFSetAddValue(loop->_commonModes, kCFRunLoopDefaultMode);
    loop->_commonModeItems = NULL;
    loop->_currentMode = NULL;
    loop->_modes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
    loop->_blocks_head = NULL;
    loop->_blocks_tail = NULL;
    loop->_counterpart = NULL;
    loop->_pthread = t;
#if DEPLOYMENT_TARGET_WINDOWS
    loop->_winthread = GetCurrentThreadId();
#else
    loop->_winthread = 0;
#endif
    rlm = __CFRunLoopFindMode(loop, kCFRunLoopDefaultMode, true);
    if (NULL != rlm) __CFRunLoopModeUnlock(rlm);
    return loop;
}

通過上面源碼不難發(fā)現(xiàn)其弊,RunLoop對象保存在一個全局的字典中,該字典以線程對象pthread_tkey膀斋,以RunLoop對象為value梭伐,并且,在第一次獲取RunLoop對象時總會先把主線程關(guān)聯(lián)的RunLoop對象創(chuàng)建好仰担,在獲取其他線程關(guān)聯(lián)的RunLoop對象時都從這個全局的字典中獲取糊识,如果沒有獲取到就創(chuàng)建一個并且添加進字典中,所以每一個線程有且僅有一個與之關(guān)聯(lián)的RunLoop對象,重要的是赂苗,如果不獲取線程關(guān)聯(lián)的RunLoop對象愉耙,那么這個RunLoop對象就不會被創(chuàng)建。當線程退出時拌滋,也會將RunLoop對象銷毀朴沿。

接下來查看一下CFRunLoopRef具體的數(shù)據(jù)結(jié)構(gòu)如下:

struct __CFRunLoop {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;          /* locked for accessing mode list */
    __CFPort _wakeUpPort;           // used for CFRunLoopWakeUp 
    Boolean _unused;
    volatile _per_run_data *_perRunData;              // reset for runs of the run loop
    pthread_t _pthread;
    uint32_t _winthread;
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFAbsoluteTime _runTime;
    CFAbsoluteTime _sleepTime;
    CFTypeRef _counterpart;
};

typedef struct __CFRunLoop * CFRunLoopRef;

上述數(shù)據(jù)結(jié)構(gòu)中比較重要的就是_commonModes_commonModeItems败砂、_currentMode以及_modes赌渣,具體關(guān)系如下圖所示,該圖取自文章深入理解RunLoop https://blog.ibireme.com/2015/05/18/runloop/下面講的內(nèi)容也有參考該博客中的內(nèi)容昌犹,建議讀者閱讀原文:

RunLoop數(shù)據(jù)結(jié)構(gòu)

上圖很好的描述了struct __CFRunLoop數(shù)據(jù)結(jié)構(gòu)相關(guān)成員變量的關(guān)系坚芜,每一個__CFRunLoop對象可以包含數(shù)個不同的Mode,而每一個Mode又包含了數(shù)個Source祭隔、ObserverTimer货岭,當一個RunLoop運行時只能選擇其中的某一個Mode來執(zhí)行,如果要切換Mode則需要退出運行后指定一個新的Mode后重新執(zhí)行運行疾渴。通過這樣的方式千贯,可以在不同Mode中設(shè)置不同的Source/Observer/Timer而不同的Mode中間的這三部分互不影響,也就是說搞坝,有些Source/Observer/Timer只能在某一個Mode中運行搔谴,當RunLoop運行在其他Mode中,該事件得不到處理桩撮。

Source CFRunLoopSourceRef

SourceCFRunLoopSourceRef類的對象敦第,指代事件源,即前文官方結(jié)構(gòu)圖中的Input Source店量,在官方文檔中該事件源Source分為三類:

  • Port-Based Sources 基于端口的芜果,也稱為source1事件,通過內(nèi)核和其他線程通信融师,接收到事件后包裝為source0事件后分發(fā)給其他線程處理右钾。

  • Custom Input Sources 用戶自定義

  • Cocoa Perform Selector Sources 調(diào)用諸如perfromSelector:onThread:這樣的方法產(chǎn)生的事件

按照調(diào)用棧來說其實只分成兩類,Source0不基于端口的和Source1基于端口的旱爆,分類方式并不是很重要舀射,了解即可。

Timer CFRunLoopTimerRef

Timer可以理解為定時器即NSTimer怀伦,因為CFRunLoopTimerRefNSTimertoll-free bridged脆烟,所以可以互相轉(zhuǎn)換,將其理解為NSTimer即可房待,RunLoop對象會在注冊的定時器時間到達時喚醒關(guān)聯(lián)的線程對象來執(zhí)行定時器的回調(diào)邢羔。

Observer CFRunLoopObserverRef

Observer就是監(jiān)聽器驼抹,用來監(jiān)聽RunLoop的各種狀態(tài),在源碼中有如下監(jiān)聽狀態(tài)的枚舉定義:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    //即將進入RunLoop的執(zhí)行循環(huán)
    kCFRunLoopEntry = (1UL << 0),
    //即將處理Timer事件
    kCFRunLoopBeforeTimers = (1UL << 1),
    //即將處理Source事件
    kCFRunLoopBeforeSources = (1UL << 2),
    //RunLoop即將進入休眠狀態(tài)
    kCFRunLoopBeforeWaiting = (1UL << 5),
    //RunLoop即將被喚醒
    kCFRunLoopAfterWaiting = (1UL << 6),
    //RunLoop即將退出
    kCFRunLoopExit = (1UL << 7),
    //監(jiān)聽RunLoop的全部狀態(tài)
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

Observer中定義了一系列的監(jiān)聽器张抄,開發(fā)者也可以使用監(jiān)聽器來監(jiān)聽具體的狀態(tài)改變砂蔽,具體栗子后文會介紹洼怔。

Mode CFRunLoopModeRef

ModeRunLoop中比較重要的部分署惯,系統(tǒng)默認為我們提供了五種Mode:

  • kCFRunLoopDefaultMode 即 NSDefaultRunLoopMode,默認運行模式

  • UITrackingRunLoopMode 跟蹤UIScrollView滑動時使用的運行模式镣隶,保證滑動時不受其他事件處理的影響极谊,保證絲滑

  • UIInitializationRunLoopMode 啟動應(yīng)用時的運行模式,應(yīng)用啟動完成后就不會再使用

  • GSEventReceiveRunLoopMode 事件接收運行模式

  • kCFRunLoopCommonModes 即 NSRunLoopCommonModes 是一種標記的模式安岂,還需要上述四種模式的支持

UITrackingRunLoopMode只有當用戶滑動屏幕時轻猖,即滑動UIScrollView時才會執(zhí)行的模式,此時域那,不在該模式內(nèi)的Source/Timer/Observer都不會得到執(zhí)行咙边,它僅僅專注于滑動時產(chǎn)生的各種事件,通過這樣的方式就可以保證用戶在滑動頁面時的流暢性次员,這也是分不同Mode的優(yōu)點败许。

具體數(shù)據(jù)結(jié)構(gòu)如下:

typedef struct __CFRunLoopMode *CFRunLoopModeRef;

struct __CFRunLoopMode {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;  /* must have the run loop locked before locking this */
    CFStringRef _name;
    Boolean _stopped;
    char _padding[3];
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
    CFMutableDictionaryRef _portToV1SourceMap;
    __CFPortSet _portSet;
    CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    dispatch_source_t _timerSource;
    dispatch_queue_t _queue;
    Boolean _timerFired; // set to true by the source when a timer has fired
    Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
    mach_port_t _timerPort;
    Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
    DWORD _msgQMask;
    void (*_msgPump)(void);
#endif
    uint64_t _timerSoftDeadline; /* TSR */
    uint64_t _timerHardDeadline; /* TSR */
};

從上述數(shù)據(jù)結(jié)構(gòu)中可以看出,Mode內(nèi)部管理了一個_source0的事件集合淑蔚,一個_source1的事件集合市殷,一個_observers的數(shù)組以及_timers的數(shù)組,這也印證了前文中關(guān)于Mode的圖例刹衫,再結(jié)合之前講的__CFRunLoop中比較重要的幾個成員變量:

struct __CFRunLoop {
    ...
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    ...
}

其中_currentMode即代表當前RunLoop對象正在執(zhí)行的ModeCFRunLoopModeRef類的對象醋寝。

_mode是一個Set集合保存了所有該RunLoop對象可以執(zhí)行的Mode

_commonModes保存的是具有Common屬性的Mode的名稱带迟,前文__CFRunLoopMode的結(jié)構(gòu)體定義中可以看到音羞,每個Mode管理自己的Source/Timer/Observer,而被標記為Common屬性的Mode還有一個特性就是當RunLoop對象在執(zhí)Common屬性的Mode時仓犬,會自動將_commonModeItems中保存的Source/Observer/Timer同步添加該Mode中嗅绰,標識Common屬性只需要將__CFRunLoopModeRef_name成員變量的值添加進_commonModes集合中即可。被標記為Common屬性的Mode就是前文講的kCFRunLoopCommonModes模式婶肩,可以看出這種模式不是一種真正的模式办陷,僅僅是標識其他模式是否需要同步添加_commonModeItems中的Source/Timer/Observer

_commonModeItems中保存的就是那些需要同步添加到具有Common屬性的Mode中的Source/Timer/Observer集合律歼。

系統(tǒng)默認將kCFRunLoopDefaultModeUITrackingRunLoopMode添加到了_commonModes中民镜,即標識為Common屬性,所以當RunLoop運行在這兩種模式中會自動同步添加_commonModeItems中的Source/Timer/Observer险毁。

舉個常見的栗子:

- (void) viewWillAppear:(BOOL)animate
{
    [super viewWilAppear:YES];
    //創(chuàng)建一個NSTimer的對象制圈,從當前時間開始每1s輸出一次Hello们童,World
    NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"Hello, World");
    }];
    //將timer加入到當前線程關(guān)聯(lián)的RunLoop對象的NSDefaultRunLoopMode中
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    
    //類方法,創(chuàng)建一個timer并添加到當前線程關(guān)聯(lián)的RunLoop的NSDefaultRunLoopMode中
    [NSTimer scheduledTimerWithTimeInterval:2 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"Hello, World222");
    }];
}

上面的栗子創(chuàng)建了兩個NSTimer鲸鹦,這兩個定時器執(zhí)行效果相同慧库,但如果頁面中有一個UIScrollView或其子類的對象在滑動時,NSTimer就不會再有任何輸出馋嗜,當停下滑動時又會有輸出齐板,因為上述代碼創(chuàng)建的兩個NSTimer都加入到了RunLoop對象的NSDefaultRunLoopMode中,在滑動時RunLoop會切換到UITrackingRunLoopMode模式下執(zhí)行葛菇,而UITrackingRunLoopMode中沒有上述定時器甘磨,所以不會執(zhí)行,當停止滑動時RunLoop對象又切換到了NSDefaultRunLoopMode模式眯停,所以可以繼續(xù)執(zhí)行定時器的回調(diào)济舆。

為了解決這個問題,可以將NSTimer即加入到NSDefaultRunLoopMode中莺债,又加入到UITrackingRunLoopMode中滋觉,同一個Source/Timer/Observer可以添加到不同的Mode中,但同一個Source/Timer/Observer不能添加到同一個Mode中齐邦,這樣不會有任何效果椎侠,但添加到兩個Mode中并不是最好的解決方案,還有一個方案就是利用前面的Common屬性侄旬,NSDefaultRunLoopModeUITrackingRunLoopMode都被添加進了_commonModes集合中被標識了具有Common屬性肺蔚,所以在運行時就會自動將_commonModeItems中的Source/Timer/Observer同步添加到其中,因此儡羔,只需要將創(chuàng)建的NSTimer加入到_commonModeItems中即可宣羊,此時只需要使用NSRunLoopCommonModes即可,代碼如下:

- (void) viewWillAppear:(BOOL)animate
{
    [super viewWilAppear:YES];
    NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:2 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"Hello, World");
    }];
    
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}

NSTimer加入到NSRunLoopCommonModes中就是把其加入到_commonModeItems集合中汰蜘,這樣在滑動時就會自動同步添加NSTimerUITrackingRunLoopMode模式下仇冯,所以定時器也可會得到執(zhí)行。如果需要注意使用類方法scheduledTimerWithTimeInterval:repeats:block時要注意該方法默認是加入到NSDefaultRunLoopMode模式中的族操。

通過上述講解苛坚,可以發(fā)現(xiàn),NSTimer其實是不那么精確的色难,首先泼舱,在使用時需要加入到RunLoop中,如果加在CommonMode在普通情況或滑動時都可以執(zhí)行回調(diào)方法枷莉,這個時候的誤差就來自于RunLoop一次循環(huán)的執(zhí)行延遲娇昙,最壞情況下,RunLoop一次循環(huán)需要執(zhí)行的任務(wù)較多笤妙,NSTimer回調(diào)執(zhí)行的延遲就會加大冒掌。如果加在其他模式下噪裕,當模式切換時就不會再執(zhí)行NSTimer的回調(diào)方法了,所以股毫,在使用時需要根據(jù)情況選擇不同的定時器以滿足項目需求膳音。

在查看RunLoop運行機制前,做一個小實驗铃诬,創(chuàng)建一個視圖控制器祭陷,并添加一個按鈕,在按鈕點擊事件的回調(diào)函數(shù)中打一個斷點氧急,然后運行程序點擊按鈕颗胡,之后查看調(diào)用棧如下圖所示:

按鈕點擊的調(diào)用棧

從上圖中可以看到程序在18處執(zhí)行main函數(shù)毫深,17執(zhí)行UIApplicationMain函數(shù)吩坝,這就是程序啟動過程,16是系統(tǒng)內(nèi)部事件哑蔫,15調(diào)用CFRunLoopRunSpecific后文會詳細講解該函數(shù)钉寝,14開始執(zhí)行RunLoop進入循環(huán),13開始處理source0這個source0就是點擊按鈕的事件闸迷,11是真正執(zhí)行source0的函數(shù)嵌纲,10-0就是點擊事件的整個轉(zhuǎn)發(fā)處理過程,最終交由我們自定義的回調(diào)方法進行處理腥沽。

RunLoop 執(zhí)行邏輯

在官方文檔中描述的RunLoop循環(huán)中的執(zhí)行邏輯如下:

  1. 通知監(jiān)聽器RunLoop進入循環(huán)

  2. 通知監(jiān)聽器即將處理Timer事件

  3. 通知監(jiān)聽器即將處理source0(不是基于端口的)事件

  4. 執(zhí)行source0事件

  5. 如果有source1(基于端口的)事件則立即執(zhí)行跳轉(zhuǎn)到第九步

  6. 通知監(jiān)聽器RunLoop即將進入休眠狀態(tài)

  7. 將線程休眠逮走,直到以下事件發(fā)生才會被喚醒:

    • 有source1事件到達
    • 定時器觸發(fā)時間到達
    • RunLoop對象的超時時間過期
    • 被外部顯示喚醒
  8. 通知監(jiān)聽器RunLoop對象即將被喚醒

  9. 處理添加進來的事件,包括:

    • 如果用戶定義的定時器時間到達今阳,執(zhí)行定時器時間并重啟循環(huán)师溅,跳轉(zhuǎn)到第二步
    • 如果有source1事件,傳遞這個事件
    • 如果RunLoop被顯示喚醒并且沒有超時則重啟RunLoop盾舌,跳轉(zhuǎn)到第二步
  10. 通知監(jiān)聽器RunLoop退出循環(huán)

為了驗證上述執(zhí)行順序墓臭,可以自行編寫監(jiān)聽器來監(jiān)聽RunLoop對象狀態(tài)的改變,具體栗子如下:

- (void)viewWillAppear:(BOOL)animated
{
    // 創(chuàng)建觀察者
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        NSLog(@"Status has changed into: %zd", activity);
    });    
    
    /*
    將監(jiān)聽器添加到當前RunLoop對象中妖谴,在RunLoop循環(huán)中就會執(zhí)行上述回調(diào)塊
    監(jiān)聽的是kCFRunLoopDefaultMode即默認狀態(tài)
    也可以使用kCFRunLoopCommonModes窿锉,同時監(jiān)聽默認狀態(tài)以及滑動視圖的狀態(tài)
    */
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    //CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopCommonModes);

    //Core Foundation需要手動釋放observer
    CFRelease(observer);
    
    //添加一個textView,它是UIScrollView的子類
    UITextView *textView = [[UITextView alloc] initWithFrame:CGRectMake(0, 20, ScreenWidth, 300)];
    textView.text = @"Hello, World";
    [textView setBackgroundColor:[UIColor redColor]];
    [self.view addSubview:textView];
}

為了減少輸出選擇監(jiān)聽kCFRunLoopDefaultMode模式膝舅,啟動程序后不做任何操作發(fā)現(xiàn)其輸出如下:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    //即將進入RunLoop的執(zhí)行循環(huán) 1
    kCFRunLoopEntry = (1UL << 0),
    //即將處理Timer事件 2
    kCFRunLoopBeforeTimers = (1UL << 1),
    //即將處理Source事件 4
    kCFRunLoopBeforeSources = (1UL << 2),
    //RunLoop即將進入休眠狀態(tài) 32
    kCFRunLoopBeforeWaiting = (1UL << 5),
    //RunLoop即將被喚醒 64
    kCFRunLoopAfterWaiting = (1UL << 6),
    //RunLoop即將退出 128
    kCFRunLoopExit = (1UL << 7),
    //監(jiān)聽RunLoop的全部狀態(tài)
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

Status has changed into: 2
Status has changed into: 4
Status has changed into: 2
Status has changed into: 4
Status has changed into: 2
Status has changed into: 4
Status has changed into: 2
Status has changed into: 4
Status has changed into: 2
Status has changed into: 4
Status has changed into: 2
Status has changed into: 4
Status has changed into: 2
Status has changed into: 4
Status has changed into: 32
Status has changed into: 64
Status has changed into: 2
Status has changed into: 4
Status has changed into: 32
Status has changed into: 64

從輸出中不難發(fā)現(xiàn)嗡载,不做任何操作時程序處于NSDefaultRunLoopMode模式下,一直在2-9步間循環(huán)仍稀,當沒有事件要處理時就轉(zhuǎn)入了休眠狀態(tài)洼滚,之后又被喚醒繼續(xù)處理,可能有讀者疑惑為什么連續(xù)那么多次都是2 4的輸出琳轿,狀態(tài)2表示即將處理Timer判沟,狀態(tài)4表示即將處理Source耿芹,接著就會處理Source,但如果有source1的存在(基于端口的事件)就不會休眠直接跳轉(zhuǎn)到第九步處理相關(guān)事件挪哄,處理完成之后又回到第二步吧秕,所以產(chǎn)生上述輸出。

此時當我們將UITextView中添加多個換行符直到滾動條出現(xiàn)后迹炼,滑動UItextView會發(fā)現(xiàn)有如下輸出:

Status has changed into: 2
Status has changed into: 4
Status has changed into: 32
Status has changed into: 64
Status has changed into: 2
Status has changed into: 4
Status has changed into: 32
Status has changed into: 64
Status has changed into: 2
Status has changed into: 4
Status has changed into: 128

128代表RunLoop對象退出循環(huán)了砸彬,因為當我們滑動UItextView時,RunLoop對象切換到了UITrackingRunLoopMode斯入,前文講過砂碉,RunLoop對象每次執(zhí)行時只能執(zhí)行在一個模式下,如果要切換模式只能退出后重新進入循環(huán)刻两,從上述輸出就證明了這一點增蹭。

接下來看一下RunLoop的執(zhí)行源碼。

RunLoop執(zhí)行的入口函數(shù)

RunLoop對外只提供了兩個入口函數(shù)

/*
RunLoop對外提供的入口函數(shù)
用戶可以顯示調(diào)用后使當前線程關(guān)聯(lián)的RunLoop對象以默認模式運行
*/
void CFRunLoopRun(void) {   /* DOES CALLOUT */
    //返回值
    int32_t result;
    //循環(huán)體磅摹,直到RunLoop停止或者結(jié)束時才會終止循環(huán)
    do {
        /*
        調(diào)用CFRunLoopRunSpecific啟動RunLoop
        執(zhí)行的RunLoop就是當前線程關(guān)聯(lián)的RunLoop對象
        超時時間100億秒滋迈,317.098年,永不超時
        */
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

/*
RunLoop對外提供的入口函數(shù)
用戶可以顯示調(diào)用后使當前線程關(guān)聯(lián)的RunLoop對象以指定模式户誓、超時時間運行
*/
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

CFRunLoopRunSpecific

/*
按照指定的模式饼灿、超時時間以及條件運行RunLoop
rl: 要運行的RunLoop對象
modeName: RunLoop對象要執(zhí)行的模式名稱
seconds: RunLoop循環(huán)的超時時間
returnAfterSourceHandled: 是否在處理完source后就退出RunLoop循環(huán)
*/
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
    __CFRunLoopLock(rl);
    //通過Mode的名稱查找CFRunLoopModeRef對象
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    /*
    如果沒有獲取到Mode
    或Mode的內(nèi)容為空,內(nèi)容為空即Mode的Source/Timer/Observer集合都沒有數(shù)據(jù)
    為空就直接返回帝美,并不真正執(zhí)行RunLoop的循環(huán)
    */
    if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
    Boolean did = false;
    if (currentMode) __CFRunLoopModeUnlock(currentMode);
    __CFRunLoopUnlock(rl);
    return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
    }
    volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
    CFRunLoopModeRef previousMode = rl->_currentMode;
    rl->_currentMode = currentMode;
    int32_t result = kCFRunLoopRunFinished;

    /*
    調(diào)用__CFRunLoopDoObservers觸發(fā)監(jiān)聽器碍彭,RunLoop即將進入循環(huán)
    */
    if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    //調(diào)用真正執(zhí)行RunLoop循環(huán)的函數(shù)
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    /*
    調(diào)用__CFRunLoopDoObservers觸發(fā)監(jiān)聽器,RunLoop退出循環(huán)
    */
    if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

        __CFRunLoopModeUnlock(currentMode);
        __CFRunLoopPopPerRunData(rl, previousPerRun);
    rl->_currentMode = previousMode;
    __CFRunLoopUnlock(rl);
    return result;
}

上述代碼做了一系列的處理悼潭,比如通過名稱查找Mode庇忌,判斷Mode是否為空,即判斷Mode中是否還有Source/Timer/Observer女责,其中比較重要的函數(shù)有__CFRunLoopFindMode函數(shù)漆枚,該函數(shù)在查找根據(jù)Mode名稱查找時,如果沒有找到會嘗試創(chuàng)建一個新的Mode抵知,如果創(chuàng)建失敗才會返回NULL墙基。__CFRunLoopModeIsEmpty函數(shù)用來判斷Mode中的Source/Timer/Observer是否為空,如果集合中沒有對象就返回true刷喜。__CFRunLoopDoObservers用來觸發(fā)監(jiān)聽器的回調(diào)函數(shù)或回調(diào)塊残制,前文舉的栗子在創(chuàng)建監(jiān)聽器并加入到RunLoop對象后,其實是將這個監(jiān)聽器加入到了Mode_observers數(shù)組中掖疮,所以該函數(shù)內(nèi)部會遍歷對應(yīng)數(shù)組并調(diào)用回調(diào)函數(shù)或回調(diào)塊來進行通知初茶。

接下來就要查看__CFRunLoopRun函數(shù)的實現(xiàn),但該函數(shù)源碼太長有三百多行浊闪,而且包含了不少跨平臺的預(yù)編譯指令恼布,由于篇幅的問題螺戳,這里不直接分析了,有興趣的讀者可以參考本系列文章第五篇iOS多線程——RunLoop與GCD折汞、AutoreleasePool倔幼,在這篇文章中會詳細講解該函數(shù)的源碼,那這里直接使用深入理解RunLoop https://blog.ibireme.com/2015/05/18/runloop/中作者簡化整理版本爽待,代碼如下:

__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
    
    Boolean sourceHandledThisLoop = NO;
    int retVal = 0;
    do {
        
        /// 2. 通知 Observers: RunLoop 即將觸發(fā) Timer 回調(diào)损同。
        __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
        /// 3. 通知 Observers: RunLoop 即將觸發(fā) Source0 (非port) 回調(diào)。
        __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
        /// 執(zhí)行被加入的block
        __CFRunLoopDoBlocks(runloop, currentMode);
        
        /// 4. RunLoop 觸發(fā) Source0 (非port) 回調(diào)鸟款。
        sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
        /// 執(zhí)行被加入的block
        __CFRunLoopDoBlocks(runloop, currentMode);
        
        /// 5. 如果有 Source1 (基于port) 處于 ready 狀態(tài)膏燃,直接處理這個 Source1 然后跳轉(zhuǎn)去處理消息。
        if (__Source0DidDispatchPortLastTime) {
            Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
            if (hasMsg) goto handle_msg;
        }
        
        /// 通知 Observers: RunLoop 的線程即將進入休眠(sleep)何什。
        if (!sourceHandledThisLoop) {
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
        }
        
        /// 7. 調(diào)用 mach_msg 等待接受 mach_port 的消息组哩。線程將進入休眠, 直到被下面某一個事件喚醒。
        /// ? 一個基于 port 的Source 的事件富俄。
        /// ? 一個 Timer 到時間了
        /// ? RunLoop 自身的超時時間到了
        /// ? 被其他什么調(diào)用者手動喚醒
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
            mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
        }
        
        /// 8. 通知 Observers: RunLoop 的線程剛剛被喚醒了禁炒。
        __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
        
        /// 收到消息,處理消息霍比。
    handle_msg:
        
        /// 9.1 如果一個 Timer 到時間了,觸發(fā)這個Timer的回調(diào)暴备。
        if (msg_is_timer) {
            __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
        }
        
        /// 9.2 如果有dispatch到main_queue的block悠瞬,執(zhí)行block。
        else if (msg_is_dispatch) {
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
        }
        
        /// 9.3 如果一個 Source1 (基于port) 發(fā)出事件了涯捻,處理這個事件
        else {
            CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
            sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
            if (sourceHandledThisLoop) {
                mach_msg(reply, MACH_SEND_MSG, reply);
            }
        }
        
        /// 執(zhí)行加入到Loop的block
        __CFRunLoopDoBlocks(runloop, currentMode);
        
        
        if (sourceHandledThisLoop && stopAfterHandle) {
            /// 進入loop時參數(shù)說處理完事件就返回浅妆。
            retVal = kCFRunLoopRunHandledSource;
        } else if (timeout) {
            /// 超出傳入?yún)?shù)標記的超時時間了
            retVal = kCFRunLoopRunTimedOut;
        } else if (__CFRunLoopIsStopped(runloop)) {
            /// 被外部調(diào)用者強制停止了
            retVal = kCFRunLoopRunStopped;
        } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
            /// source/timer/observer一個都沒有了
            retVal = kCFRunLoopRunFinished;
        }
        
        /// 如果沒超時,mode里沒空障癌,loop也沒被停止凌外,那繼續(xù)loop。
    } while (retVal == 0);
}

上面執(zhí)行代碼就和前面官方文檔中講解的順序一致涛浙,不再贅述康辑。

在前文給了一個點擊按鈕的調(diào)用棧運行圖,可以發(fā)現(xiàn)執(zhí)行source0事件時是調(diào)用了一個非常長的函數(shù)來處理轿亮,為了方便查看調(diào)用棧執(zhí)行的順序疮薇,深入理解RunLoop https://blog.ibireme.com/2015/05/18/runloop/一文中,作者將整個RunLoop響應(yīng)函數(shù)按執(zhí)行順序列了下來我注,如下:

{
    /// 1. 通知Observers按咒,即將進入RunLoop
    /// 此處有Observer會創(chuàng)建AutoreleasePool: _objc_autoreleasePoolPush();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
    do {
 
        /// 2. 通知 Observers: 即將觸發(fā) Timer 回調(diào)。
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
        /// 3. 通知 Observers: 即將觸發(fā) Source (非基于port的,Source0) 回調(diào)但骨。
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
 
        /// 4. 觸發(fā) Source0 (非基于port的) 回調(diào)励七。
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
 
        /// 6. 通知Observers智袭,即將進入休眠
        /// 此處有Observer釋放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
 
        /// 7. sleep to wait msg.
        mach_msg() -> mach_msg_trap();
        
 
        /// 8. 通知Observers,線程被喚醒
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
 
        /// 9. 如果是被Timer喚醒的掠抬,回調(diào)Timer
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
 
        /// 9. 如果是被dispatch喚醒的补履,執(zhí)行所有調(diào)用 dispatch_async 等方法放入main queue 的 block
        __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
 
        /// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件喚醒了,處理這個事件
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
 
 
    } while (...);
 
    /// 10. 通知Observers剿另,即將退出RunLoop
    /// 此處有Observer釋放AutoreleasePool: _objc_autoreleasePoolPop();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}

參考文章

深入理解RunLoop https://blog.ibireme.com/2015/05/18/runloop/

備注

由于作者水平有限箫锤,難免出現(xiàn)紕漏,如有問題還請不吝賜教雨女。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末谤饭,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子榛丢,更是在濱河造成了極大的恐慌边坤,老刑警劉巖,帶你破解...
    沈念sama閱讀 224,764評論 6 522
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件讼稚,死亡現(xiàn)場離奇詭異括儒,居然都是意外死亡,警方通過查閱死者的電腦和手機锐想,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 96,235評論 3 402
  • 文/潘曉璐 我一進店門帮寻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人赠摇,你說我怎么就攤上這事固逗。” “怎么了藕帜?”我有些...
    開封第一講書人閱讀 171,965評論 0 366
  • 文/不壞的土叔 我叫張陵烫罩,是天一觀的道長。 經(jīng)常有香客問我洽故,道長贝攒,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,984評論 1 300
  • 正文 為了忘掉前任时甚,我火速辦了婚禮隘弊,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘撞秋。我一直安慰自己长捧,他們只是感情好,可當我...
    茶點故事閱讀 69,984評論 6 399
  • 文/花漫 我一把揭開白布吻贿。 她就那樣靜靜地躺著串结,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上肌割,一...
    開封第一講書人閱讀 53,471評論 1 314
  • 那天卧蜓,我揣著相機與錄音,去河邊找鬼把敞。 笑死弥奸,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的奋早。 我是一名探鬼主播盛霎,決...
    沈念sama閱讀 41,844評論 3 428
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼耽装!你這毒婦竟也來了愤炸?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,818評論 0 279
  • 序言:老撾萬榮一對情侶失蹤掉奄,失蹤者是張志新(化名)和其女友劉穎规个,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體姓建,經(jīng)...
    沈念sama閱讀 47,359評論 1 324
  • 正文 獨居荒郊野嶺守林人離奇死亡诞仓,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 39,385評論 3 346
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了速兔。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片墅拭。...
    茶點故事閱讀 41,515評論 1 354
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖憨栽,靈堂內(nèi)的尸體忽然破棺而出帜矾,到底是詐尸還是另有隱情,我是刑警寧澤屑柔,帶...
    沈念sama閱讀 37,114評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站珍剑,受9級特大地震影響掸宛,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜招拙,卻給世界環(huán)境...
    茶點故事閱讀 42,836評論 3 338
  • 文/蒙蒙 一唧瘾、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧别凤,春花似錦饰序、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 33,291評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春蝠嘉,著一層夾襖步出監(jiān)牢的瞬間最疆,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 34,422評論 1 275
  • 我被黑心中介騙來泰國打工蚤告, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留努酸,地道東北人。 一個月前我還...
    沈念sama閱讀 50,064評論 3 381
  • 正文 我出身青樓杜恰,卻偏偏與公主長得像获诈,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子心褐,可洞房花燭夜當晚...
    茶點故事閱讀 46,581評論 2 365

推薦閱讀更多精彩內(nèi)容