iOS刨根問底-深入理解RunLoop

零:前言

聲明:本文非原創(chuàng)邪锌,是我在整理自己iOS知識體系時勉躺,閱讀到這篇文章,感覺作者整理的非常好觅丰,就轉(zhuǎn)載到這里方便自己學習饵溅、備忘。

感謝作者崔江濤妇萄,如有侵權(quán)蜕企,請聯(lián)系刪除咬荷。

原文鏈接:https://www.cnblogs.com/kenshincui/p/6823841.html
原文作者:崔江濤KenshinCui

一:概述

RunLoop作為 iOS 中一個基礎組件和線程有著千絲萬縷的關(guān)系,同時也是很多常見技術(shù)的幕后功臣糖赔。盡管在平時多數(shù)開發(fā)者很少直接使用RunLoop萍丐,但是理解RunLoop可以幫助開發(fā)者更好的利用多線程編程模型轩端,同時也可以幫助開發(fā)者解答日常開發(fā)中的一些疑惑放典。本文將從RunLoop源碼著手,結(jié)合RunLoop的實際應用來逐步解開它的神秘面紗基茵。

二: 開源的RunloopRef

通常所說的RunLoop指的是NSRunloop或者CFRunloopRef奋构,CFRunloopRef是純C的函數(shù),而NSRunloop僅僅是CFRunloopRef的OC封裝拱层,并未提供額外的其他功能弥臼,因此下面主要分析CFRunloopRef,蘋果已經(jīng)開源了CoreFoundation源代碼根灯,因此很容易找到CFRunloop源代碼??径缅。

https://opensource.apple.com/source/CF

從代碼可以看出CFRunloopRef其實就是__CFRunloop這個結(jié)構(gòu)體指針(按照OC的思路我們可以將RunLoop看成一個對象),這個對象的運行才是我們通常意義上說的運行循環(huán)烙肺,核心方法是__CFRunloopRun()纳猪,為了便于閱讀就不再直接貼源代碼,放一段偽代碼方便大家閱讀:

int32_t __CFRunLoopRun( /** 5個參數(shù) */ )
{
    // 通知即將進入runloop
    __CFRunLoopDoObservers(KCFRunLoopEntry);
    
    do
    {
        // 通知將要處理timer和source
        __CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
        __CFRunLoopDoObservers(kCFRunLoopBeforeSources);
        
        // 處理非延遲的主線程調(diào)用
        __CFRunLoopDoBlocks();
        // 處理Source0事件
        __CFRunLoopDoSource0();
        
        if (sourceHandledThisLoop) {
            __CFRunLoopDoBlocks();
         }
        // 如果有 Source1 (基于port) 處于 ready 狀態(tài)桃笙,直接處理這個 Source1 然后跳轉(zhuǎn)去處理消息氏堤。
        if (__Source0DidDispatchPortLastTime) {
            Boolean hasMsg = __CFRunLoopServiceMachPort();
            if (hasMsg) goto handle_msg;
        }
            
        // 通知 Observers: RunLoop 的線程即將進入休眠(sleep)。
        if (!sourceHandledThisLoop) {
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
        }
            
        // GCD dispatch main queue
        CheckIfExistMessagesInMainDispatchQueue();
        
        // 即將進入休眠
        __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
        
        // 等待內(nèi)核mach_msg事件
        mach_port_t wakeUpPort = SleepAndWaitForWakingUpPorts();
        
        // 等待搏明。鼠锈。。
        
        // 從等待中醒來
        __CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
        
        // 處理因timer的喚醒
        if (wakeUpPort == timerPort)
            __CFRunLoopDoTimers();
        
        // 處理異步方法喚醒,如dispatch_async
        else if (wakeUpPort == mainDispatchQueuePort)
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
            
        // 處理Source1
        else
            __CFRunLoopDoSource1();
        
        // 再次確保是否有同步的方法需要調(diào)用
        __CFRunLoopDoBlocks();
        
    } while (!stop && !timeout);
    
    // 通知即將退出runloop
    __CFRunLoopDoObservers(CFRunLoopExit);
}

源代碼盡管不算太長星著,但是如果不太熟悉的話面對這么一堆不知道做什么的函數(shù)調(diào)用還是會給人一種神秘感购笆。但是現(xiàn)在可以不用逐行閱讀,后面慢慢解開這層神秘面紗⌒檠現(xiàn)在只要了解上面的偽代碼知道核心的方法__CFRunLoopRun()內(nèi)部其實是一個do-while循環(huán)同欠,這也正是Runloop運行的本質(zhì)。執(zhí)行了這個函數(shù)以后就一直處于“等待-處理”的循環(huán)之中邮丰,直到循環(huán)結(jié)束行您。只是不同于我們自己寫的循環(huán)它在休眠時幾乎不會占用系統(tǒng)資源,當然這是由于系統(tǒng)內(nèi)核負責實現(xiàn)的剪廉,也是Runloop精華所在娃循。

隨著Swift的開源蘋果也維護了一個Swift版本的跨平臺CoreFoundation版本,除了mac平臺它還是適配了 Linux 和 Windows 平臺斗蒋。但是鑒于目前很多關(guān)于 Runloop 的討論都是以OC版展開的捌斧,所以這里也主要分析OC版本笛质。
https://github.com/apple/swift-corelibs-foundation/

下圖描述了Runloop運行流程±搪欤基本描述了上面Runloop的核心流程妇押,當然可以查看官方 The RunLoop Sequence of Events描述

001.png

整個流程并不復雜(需要注意的就是黃色區(qū)域的消息處理中并不包含source0姓迅,因為它在循環(huán)開始之初就會處理)敲霍,整個流程其實就是一種Event Loop的實現(xiàn),其他平臺均有類似的實現(xiàn)丁存,只是這里叫做Runloop肩杈。但是既然RunLoop是一個消息循環(huán),誰來管理和運行Runloop解寝?那么它接收什么類型的消息扩然?休眠過程是怎么樣的?如何保證休眠時不占用系統(tǒng)資源聋伦?如何處理這些消息以及何時退出循環(huán)夫偶?還有一系列問題需要解開。

注意的是盡管CFRunLoopPerformBlock在上圖中作為喚醒機制有所體現(xiàn)觉增,但事實上執(zhí)行CFRunLoopPerformBlock只是入隊兵拢,下次RunLoop運行才會執(zhí)行,而如果需要立即執(zhí)行則必須調(diào)用CFRunLoopWakeUp抑片。

上面那個圖很大卵佛,也可以看下面這個精簡的 RunLoop 流程圖:

0022.jpg

三: Runloop Mode

從源碼很容易看出,Runloop總是運行在某種特定的CFRunLoopModeRef下(每次運行__CFRunLoopRun()函數(shù)時必須指定Mode)敞斋。而通過CFRunloopRef對應結(jié)構(gòu)體的定義可以很容易知道每種Runloop都可以包含若干個Mode截汪,每個Mode又包含Source/Timer/Observer。每次調(diào)用Runloop的主函數(shù)__CFRunLoopRun()時必須指定一種Mode植捎,這個Mode稱為 _currentMode 衙解,當切換Mode時必須退出當前Mode,然后重新進入Runloop以保證不同ModeSource/Timer/Observer互不影響焰枢。

struct __CFRunLoop {  // 部分
    CFRuntimeBase _base;
    pthread_mutex_t _lock; /* locked for accessing mode list */
    __CFPort _wakeUpPort; // used for CFRunLoopWakeUp 
    Boolean _unused;
    pthread_t _pthread;
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    CFAbsoluteTime _runTime;
    CFAbsoluteTime _sleepTime;
    CFTypeRef _counterpart;
};

// ----------------------------------------

struct __CFRunLoopMode {  // 部分
    CFRuntimeBase _base;
    /* must have the run loop locked before locking this */
    pthread_mutex_t _lock;
    CFStringRef _name;
    Boolean _stopped;
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
    CFMutableDictionaryRef _portToV1SourceMap;
    __CFPortSet _portSet;
    CFIndex _observerMask;
};

系統(tǒng)默認提供的 Run Loop Modes 有kCFRunLoopDefaultMode(NSDefaultRunLoopMode)和UITrackingRunLoopMode蚓峦,需要切換到對應的Mode 時只需要傳入對應的名稱即可。前者是系統(tǒng)默認的 Runloop Mode济锄,例如進入iOS程序默認不做任何操作就處于這種 Mode 中暑椰,此時滑動UIScrollView,主線程就切換 Runloop 到UITrackingRunLoopMode荐绝,不再接受其他事件操作(除非你將其他 Source/Timer 設置到UITrackingRunLoopMode下)一汽。

但是對于開發(fā)者而言經(jīng)常用到的 Mode 還有一個kCFRunLoopCommonModesNSRunLoopCommonModes),其實這個并不是某種具體的 Mode,而是一種模式組合低滩,在iOS系統(tǒng)中默認包含了 NSDefaultRunLoopModeUITrackingRunLoopMode召夹;注意:并不是說 Runloop 會運行在kCFRunLoopCommonModes這種模式下岩喷,而是相當于分別注冊了 NSDefaultRunLoopModeUITrackingRunLoopMode。當然你也可以通過調(diào)用CFRunLoopAddCommonMode()方法將自定義 Mode 放到 kCFRunLoopCommonModes組合监憎。

注意:我們常常還會碰到一些系統(tǒng)框架自定義 Mode纱意,例如FoundationNSConnectionReplyMode。還有一些系統(tǒng)私有 Mode鲸阔,例如:GSEventReceiveRunLoopMode接受系統(tǒng)事件偷霉,UIInitializationRunLoopMode是App啟動過程中初始化Mode。更多系統(tǒng)或框架Mode查看這里??
https://iphonedev.wiki/index.php/CFRunLoop

CFRunLoopRefCFRunloopMode隶债、CFRunLoopSourceRef/CFRunloopTimerRef/CFRunLoopObserverRef關(guān)系如下圖:

002.png

一個RunLoop對象(CFRunLoop)中包含若干個運行模式(CFRunLoopMode)腾它。而每一個運行模式下又包含若干個輸入源(CFRunLoopSource)、定時源(CFRunLoopTimer)死讹、觀察者(CFRunLoopObserver)。

那么CFRunLoopSourceRef赞警、CFRunLoopTimerRefCFRunLoopObserverRef究竟是什么?它們在 Runloop 運行流程中起到什么作用呢虏两?

四: Source

首先看一下官方 Runloop 結(jié)構(gòu)圖(注意下圖的 Input Source Port 和前面流程圖中的Source0并不對應愧旦,而是對應Source1Source1Timer都屬于端口事件源定罢,不同的是所有的Timer都共用一個端口“Mode Timer Port”笤虫,而每個Source1都有不同的對應端口):

003.jpeg

再結(jié)合前面 RunLoop 核心運行流程可以看出Source0(負責App內(nèi)部事件,由App負責管理觸發(fā)祖凫,例如UITouch事件)和Timer(又叫Timer Source琼蚯,基于時間的觸發(fā)器,上層對應NSTimer)是兩個不同的 Runloop 事件源(當然Source0Input Source中的一類惠况,Input Source 還包括 Custom Input Source遭庶,由其他線程手動發(fā)出),RunLoop 被這些事件喚醒之后就會處理并調(diào)用事件處理方法(CFRunLoopTimerRef的回調(diào)指針和CFRunLoopSourceRef均包含對應的回調(diào)指針)稠屠。

但是對于CFRunLoopSourceRef除了Source0之外還有另一個版本就是Source1峦睡,Source1除了包含回調(diào)指針外包含一個mach port,和Source0需要手動觸發(fā)不同权埠,Source1可以監(jiān)聽系統(tǒng)端口和其他線程相互發(fā)送消息榨了,它能夠主動喚醒 RunLoop (由操作系統(tǒng)內(nèi)核進行管理,例如CFMessagePort消息)攘蔽。官方也指出可以自定義 Source龙屉,因此對于CFRunLoopSourceRef來說它更像一種協(xié)議,框架已經(jīng)默認定義了兩種實現(xiàn)秩彤,如果有必要開發(fā)人員也可以自定義叔扼,詳細情況可以查看官方文檔事哭。

五: Observer

struct __CFRunLoopObserver {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;
    CFIndex _rlCount;
    CFOptionFlags _activities; /* immutable */
    CFIndex _order; /* immutable */
    CFRunLoopObserverCallBack _callout; /* immutable */
};

相對來說CFRunloopObserverRef理解起來并不復雜,它相當于消息循環(huán)中的一個監(jiān)聽器瓜富,隨時通知外部當前 RunLoop 的運行狀態(tài)(它包含一個函數(shù)指針_callout_將當前狀態(tài)及時告訴觀察者)鳍咱。具體的 Observer 狀態(tài)如下:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0), // 進入RunLoop 
    kCFRunLoopBeforeTimers = (1UL << 1), // 即將開始Timer處理
    kCFRunLoopBeforeSources = (1UL << 2), // 即將開始Source處理
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進入休眠
    kCFRunLoopAfterWaiting = (1UL << 6), // 從休眠狀態(tài)喚醒
    kCFRunLoopExit = (1UL << 7), // 退出RunLoop
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

六: Call out

RunLoop 幾乎所有的操作都是通過Call out進行回調(diào)的(無論是 Observer 的狀態(tài)通知還是 Timer、Source 的處理)与柑,而系統(tǒng)在回調(diào)時通常使用如下幾個函數(shù)進行回調(diào)(換句話說你的代碼其實最終都是通過下面幾個函數(shù)來負責調(diào)用的谤辜,即使你自己監(jiān)聽Observer 也會先調(diào)用下面的函數(shù)然后間接通知你,所以在調(diào)用堆棧中經(jīng)臣叟酰看到這些函數(shù)):

static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();

例如在控制器的touchBegin中打入斷點查看堆棧(由于UIEventSource0丑念,所以可以看到一個Source0Call out函數(shù)CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION調(diào)用):

004.png

七: RunLoop休眠

其實對于 Event Loop 而言 RunLoop 最核心的事情就是保證線程在沒有消息時休眠以避免占用系統(tǒng)資源,有消息時能夠及時喚醒结蟋。RunLoop 的這個機制完全依靠系統(tǒng)內(nèi)核來完成脯倚,具體來說是蘋果操作系統(tǒng)核心組件Darwin中的Mach來完成的(Darwin是開源的)∏妒海可以從下圖最底層Kernel中找到Mach

005.png

MachDarwin的核心推正,可以說是內(nèi)核的核心,提供了進程間通信(IPC)宝惰、處理器調(diào)度等基礎服務植榕。在Mach中,進程尼夺、線程間的通信是以消息的方式來完成的尊残,消息在兩個Port之間進行傳遞(這也正是Source1之所以稱之為 Port-based Source 的原因,因為它就是依靠系統(tǒng)發(fā)送消息到指定的Port來觸發(fā)的)淤堵。消息的發(fā)送和接收使用<mach/message.h>中的mach_msg()函數(shù)(事實上蘋果提供的 Mach API 很少寝衫,并不鼓勵我們直接調(diào)用這些API):

006.jpg

mach_msg()的本質(zhì)是一個調(diào)用mach_msg_trap(),這相當于一個系統(tǒng)調(diào)用,會觸發(fā)內(nèi)核狀態(tài)切換粘勒。當程序靜止時竞端,RunLoop 停留在

__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy)

而這個函數(shù)內(nèi)部就是調(diào)用了mach_msg讓程序處于休眠狀態(tài)。

RunLoop 這種有事做事庙睡,沒事休息的機制其實就是用戶態(tài)內(nèi)核態(tài)的互相轉(zhuǎn)化事富。用戶態(tài)內(nèi)核態(tài)在 Linux 和 Unix 系統(tǒng)中,是基本概念乘陪,是操作系統(tǒng)的兩種運行級別统台,他們的權(quán)限不一樣,由于系統(tǒng)的資源是有限的啡邑,比如網(wǎng)絡贱勃、內(nèi)存等,所以為了優(yōu)化性能,降低電量消耗贵扰,提高資源利用率仇穗,所以內(nèi)核底層就這么設計了。

八: Runloop和線程的關(guān)系

Runloop 是基于pthread進行管理的戚绕,pthread是基于 C 的跨平臺多線程操作底層API纹坐。它是 mach thread 的上層封裝(可以參見Kernel Programming Guide),和NSThread一一對應(而NSThread是一套面向?qū)ο蟮腁PI舞丛,所以在iOS開發(fā)中我們也幾乎不用直接使用pthread)耘子。

007.gif

蘋果開發(fā)的接口中并沒有直接創(chuàng)建 Runloop 的接口,如果需要使用 Runloop 通常CFRunLoopGetMain()CFRunLoopGetCurrent()兩個方法來獲惹蚯小(通過上面的源代碼也可以看到谷誓,核心邏輯在_CFRunLoopGet_當中),通過代碼并不難發(fā)現(xiàn)其實只有當我們使用線程的方法主動 get Runloop 時才會在第一次創(chuàng)建該線程的Runloop,同時將它保存在全局的Dictionary中(線程和Runloop是一一對應)吨凑,默認情況下線程并不會創(chuàng)建 Runloop(主線程的Runloop比較特殊捍歪,任何線程創(chuàng)建之前都會保證主線程已經(jīng)存在 Runloop),同時在線程結(jié)束的時候也會銷毀對應的Runloop怀骤。

iOS開發(fā)過程中對于開發(fā)者而言更多的使用的是NSRunloop,它默認提供了三個常用的run方法:

- (void)run; 
- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;
- (void)runUntilDate:(NSDate *)limitDate;
  • run:方法對應上面CFRunloopRef中的CFRunLoopRun并不會退出费封,除非調(diào)用CFRunLoopStop();通常如果想要永遠不會退出 RunLoop 才會使用此方法,否則可以使用runUntilDate:蒋伦。
  • runMode:beforeDate:則對應CFRunLoopRunInMode(mode,limiteDate,true)方法,只執(zhí)行一次,執(zhí)行完就退出焚鹊;通常用于手動控制 RunLoop(例如在while循環(huán)中)痕届。
  • runUntilDate:方法其實是CFRunLoopRunInMode(kCFRunLoopDefaultMode,limiteDate,false),執(zhí)行完并不會退出末患,繼續(xù)下一次RunLoop直到timeout研叫。

九: RunLoop應用

9.1 NSTimer

前面一直提到 Timer Source 作為事件源,事實上它的上層對應就是NSTimer(其實就是CFRunloopTimerRef)這個開發(fā)者經(jīng)常用到的定時器(底層基于使用mk_timer實現(xiàn))璧针,甚至很多開發(fā)者接觸 RunLoop 還是從NSTimer開始的嚷炉。其實NSTimer定時器的觸發(fā)正是基于 RunLoop 運行的,所以使用NSTimer之前必須注冊到 RunLoop探橱,但是 RunLoop 為了節(jié)省資源并不會在非常準確的時間點調(diào)用定時器申屹,如果一個任務執(zhí)行時間較長,那么當錯過一個時間點后只能等到下一個時間點執(zhí)行隧膏,并不會延后執(zhí)行(NSTimer提供了一個tolerance屬性用于設置寬容度哗讥,如果確實想要使用NSTimer并且希望盡可能的準確,則可以設置此屬性)胞枕。

NSTimer的創(chuàng)建通常有兩種方式杆煞,盡管都是類方法,一種是timerWithXXX,另一種scheduedTimerWithXXX决乎。

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

二者最大的區(qū)別就是后者除了創(chuàng)建一個定時器外會自動以NSDefaultRunLoopModeMode添加到當前線程 RunLoop 中队询,不添加到 RunLoop中的NSTimer是無法正常工作的。例如下面的代碼中如果timer2不加入到RunLoop 中是無法正常工作的构诚。同時注意如果滾動UIScrollView(UITableView蚌斩、UICollectionview是類似的)二者是無法正常工作的,但是如果將NSDefaultRunLoopMode改為NSRunLoopCommonModes則可以正常工作唤反,這也解釋了前面介紹的 Mode 內(nèi)容凳寺。

@interface BViewController ()
@property (nonatomic, weak) NSTimer *timer1;
@property (nonatomic, weak) NSTimer *timer2;
@end

@implementation BViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor blueColor];

    // timer1創(chuàng)建后會自動以NSDefaultRunLoopMode默認模式添加到當前RunLoop中,所以可以正常工作
    self.timer1 = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timeInterval:) userInfo:nil repeats:YES];
    NSTimer *tempTimer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timeInterval:) userInfo:nil repeats:YES];
    
    // 如果不把timer2添加到RunLoop中是無法正常工作的(注意如果想要在滾動UIScrollView時timer2可以正常工作可以將NSDefaultRunLoopMode改為NSRunLoopCommonModes)
    [[NSRunLoop currentRunLoop] addTimer:tempTimer forMode:NSDefaultRunLoopMode];
    self.timer2 = tempTimer;
    
    CGRect rect = [UIScreen mainScreen].bounds;
    UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:CGRectInset(rect, 0, 200)];
    [self.view addSubview:scrollView];
    
    UIView *contentView = [[UIView alloc] initWithFrame:CGRectInset(scrollView.bounds, -100, -100)];
    contentView.backgroundColor = [UIColor redColor];
    [scrollView addSubview:contentView];
    scrollView.contentSize = contentView.frame.size;
}

- (void)timeInterval:(NSTimer *)timer {
    if (self.timer1 == timer) {
        NSLog(@"timer1...");
    } else {
        NSLog(@"timer2...");
    }
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self dismissViewControllerAnimated:true completion:nil];
}

- (void)dealloc {
    NSLog(@"BViewController dealloc...");
}
@end

注意上面代碼中UIViewControllertimer1timer2并沒有強引用彤侍,對于普通的對象而言肠缨,執(zhí)行完viewDidLoad方法之后(準確的說應該是執(zhí)行完viewDidLoad方法后的的一個 RunLoop 運行結(jié)束)二者應該會被釋放,但事實上二者并沒有被釋放盏阶。原因是:為了確保定時器正常運轉(zhuǎn)晒奕,當加入到 RunLoop 以后系統(tǒng)會對NSTimer執(zhí)行一次retain操作(特別注意??:timer2創(chuàng)建時并沒直接賦值給timer2,原因是timer2weak屬性名斟,如果直接賦值給timer2會被立即釋放脑慧,因為timerWithXXX方法創(chuàng)建的NSTimer默認并沒有加入 RunLoop,只有后面加入 RunLoop 以后才可以將引用指向timer2)砰盐。

但是即使使用了弱引用闷袒,上面的代碼中BViewController也無法正常釋放,原因是在創(chuàng)建NSTimer2時指定了targetself岩梳,這樣一來造成了timer1timer2BViewController有一個強引用囊骤。

解決這個問題的方法通常有兩種:一種是將target分離出來獨立成一個對象(在這個對象中創(chuàng)建NSTimer并將對象本身作為NSTimertarget),控制器通過這個對象間接使用NSTimer冀值;另一種方式的思路仍然是轉(zhuǎn)移target也物,只是可以直接增加NSTimer擴展(分類),讓NSTimer自身做為target列疗,同時可以將操作selector封裝到block中滑蚯。后者相對優(yōu)雅,也是目前使用較多的方案抵栈,如果你可以確保代碼只在 iOS 10 后運行就可以使用 iOS 10 新增的系統(tǒng)級block方案(下面的代碼中已經(jīng)貼出這種方法)告材。

當然使用上面第二種方法可以解決控制器無法釋放的問題,但是會發(fā)現(xiàn)即使控制器被釋放了兩個定時器仍然正常運行竭讳,要解決這個問題就需要調(diào)用NSTimerinvalidate方法(注意:無論是重復執(zhí)行的定時器還是一次性的定時器只要調(diào)用invalidate方法則會變得無效创葡,只是一次性的定時器執(zhí)行完操作后會自動調(diào)用invalidate方法)。修改后的代碼如下:

@implementation BViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor blueColor];

    self.timer1 = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"timer1...");
    }];
    NSTimer *tempTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"timer2...");
    }];
    [[NSRunLoop currentRunLoop] addTimer:tempTimer forMode:NSDefaultRunLoopMode];
    self.timer2 = tempTimer;
    
    CGRect rect = [UIScreen mainScreen].bounds;
    UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:CGRectInset(rect, 0, 200)];
    [self.view addSubview:scrollView];
    
    UIView *contentView = [[UIView alloc] initWithFrame:CGRectInset(scrollView.bounds, -100, -100)];
    contentView.backgroundColor = [UIColor redColor];
    [scrollView addSubview:contentView];
    scrollView.contentSize = contentView.frame.size;
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self dismissViewControllerAnimated:true completion:nil];
}

- (void)dealloc {
    [self.timer1 invalidate];
    [self.timer2 invalidate];
    NSLog(@"BViewController dealloc...");
}
@end

其實和定時器相關(guān)的另一個問題大家也經(jīng)常碰到绢慢,那就是NSTimer不是一種實時機制灿渴,官方文檔明確說明在一個循環(huán)中如果 RunLoop 沒有被識別(這個時間大概在50-100ms)或者說當前 RunLoop 在執(zhí)行一個長的call out(例如執(zhí)行某個循環(huán)操作)則NSTimer可能就會存在誤差洛波,RunLoop 在下一次循環(huán)中繼續(xù)檢查并根據(jù)情況確定是否執(zhí)行(NSTimer的執(zhí)行時間總是固定在一定的時間間隔,例如1:00:00骚露、1:00:01蹬挤、1:00:02、1:00:05則跳過了第4棘幸、5次運行循環(huán))焰扳。

要演示這個問題請看下面的例子(注意:有些示例中可能會讓一個線程中啟動一個定時器,再在主線程啟動一個耗時任務來演示這個問误续,如果實際測試可能效果不會太明顯吨悍,因為現(xiàn)在的iPhone都是多核運算的,這樣一來這個問題會變得相對復雜蹋嵌,因此下面的例子選擇在同一個 RunLoop 中即加入定時器和執(zhí)行耗時任務)

@interface ViewController ()
@property (nonatomic, weak) NSTimer *timer1;
@property (nonatomic, strong) NSThread *thread1;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor redColor];
    
    // 由于下面的方法無法拿到NSThread的引用育瓜,也就無法控制線程的狀態(tài)
    // [NSThread detachNewThreadSelector:@selector(performTask) toTarget:self withObject:nil];
    self.thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(performTask) object:nil];
    [self.thread1 start];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self.thread1 cancel];
    [self dismissViewControllerAnimated:YES completion:nil];
}

- (void)dealloc {
    [self.timer1 invalidate];
    NSLog(@"ViewController dealloc.");
}

- (void)performTask {
    // 使用下面的方式創(chuàng)建定時器雖然會自動加入到當前線程的RunLoop中,但是除了主線程外其他線程的RunLoop默認是不會運行的栽烂,必須手動調(diào)用
    __weak typeof(self) weakSelf = self;
    self.timer1 = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        if ([NSThread currentThread].isCancelled) {
            //[NSObject cancelPreviousPerformRequestsWithTarget:weakSelf selector:@selector(caculate) object:nil];
            //[NSThread exit];
            [weakSelf.timer1 invalidate];
        }
        NSLog(@"timer1...");
    }];
    
    NSLog(@"runloop before performSelector: %@", [NSRunLoop currentRunLoop]);
    
    // 區(qū)分直接調(diào)用和「performSelector:withObject:afterDelay:」區(qū)別,下面的直接調(diào)用無論是否運行RunLoop一樣可以執(zhí)行躏仇,但是后者則不行。
    // [self caculate];
    [self performSelector:@selector(caculate) withObject:nil afterDelay:2.0];

    // 取消當前RunLoop中注冊selector(注意:只是當前RunLoop腺办,所以也只能在當前RunLoop中取消)
    // [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(caculate) object:nil];
    NSLog(@"runloop after performSelector:%@", [NSRunLoop currentRunLoop]);
    
    // 非主線程RunLoop必須手動調(diào)用
    [[NSRunLoop currentRunLoop] run];
    
    NSLog(@"注意:如果RunLoop不退出(運行中)焰手,這里的代碼并不會執(zhí)行,RunLoop本身就是一個循環(huán).");
}

- (void)caculate {
    for (int i = 0;i < 9999;++i) {
        NSLog(@"%i, %@",i, [NSThread currentThread]);
        if ([NSThread currentThread].isCancelled) {
            return;
        }
    }
}
@end

如果運行并且不退出上面的程序會發(fā)現(xiàn)怀喉,前兩秒NSTimer可以正常執(zhí)行书妻,但是兩秒后由于同一個RunLoop中循環(huán)操作的執(zhí)行造成定時器跳過了中間執(zhí)行的機會一直到caculator循環(huán)完畢,這也正說明了NSTimer不是實時系統(tǒng)機制的原因躬拢。

但是以上程序還有幾點需要說明一下:

  • 1.NSTimer會對Target進行強引用直到任務結(jié)束或exit之后才會釋放驻子。如果上面的程序沒有進行線程cancel而終止任務則及時關(guān)閉控制器也無法正確釋放。
  • 2.非主線程的 RunLoop 并不會自動運行(同時注意默認情況下非主線程的RunLoop并不會自動創(chuàng)建估灿,直到第一次使用),RunLoop 運行必須要在加入NSTimerSource0缤剧、Sourc1馅袁、Observer輸入后運行否則會直接退出。例如上面代碼如果run放到NSTimer創(chuàng)建之前則既不會執(zhí)行定時任務也不會執(zhí)行循環(huán)運算荒辕。
  • 3.performSelector:withObject:afterDelay:執(zhí)行的本質(zhì)還是通過創(chuàng)建一個NSTimer然后加入到當前線程RunLoop(通而過前后兩次打印 RunLoop 信息可以看到此方法執(zhí)行之后RunLoop的 timer 會增加1個汗销。類似的還有performSelector:onThread:withObject:afterDelay:,只是它會在另一個線程的RunLoop中創(chuàng)建一個Timer)抵窒,所以此方法事實上在任務執(zhí)行完之前會對觸發(fā)對象形成引用弛针,任務執(zhí)行完進行釋放(例如上面會對ViewController形成引用,注意:performSelector:withObject:等方法則等同于直接調(diào)用李皇,原理與此不同)削茁。
  • 4.同時上面的代碼也充分說明了RunLoop是一個循環(huán)事實宙枷,run方法之后的代碼不會立即執(zhí)行,直到RunLoop退出茧跋。
  • 5.上面程序的運行過程中如果突然dismiss慰丛,則程序的實際執(zhí)行過程要分為兩種情況考慮:如果循環(huán)任務caculate還沒有開始則會在timer1中停止timer1運行(停止了線程中第一個任務),然后等待caculate執(zhí)行并break(停止線程中第二個任務)后線程任務執(zhí)行結(jié)束釋放對控制器的引用瘾杭;如果循環(huán)任務caculate執(zhí)行過程中dismisscaculate任務執(zhí)行結(jié)束诅病,等待timer1下個周期運行(因為當前線程的RunLoop并沒有退出,timer1引用計數(shù)器并不為0)時檢測到線程取消狀態(tài)則執(zhí)行invalidate方法(第二個任務也結(jié)束了)粥烁,此時線程釋放對于控制器的引用贤笆。

CADisplayLink是一個執(zhí)行頻率(FPS)和屏幕刷新相同(可以修改preferredFramesPerSecond改變刷新頻率)的定時器,它也需要加入到RunLoop才能執(zhí)行讨阻。與NSTimer類似芥永,CADisplayLink同樣是基于CFRunloopTimerRef實現(xiàn),底層使用mk_timer(可以比較加入到 RunLoop 前后 RunLoop 中 timer 的變化)变勇。和NSTimer相比它精度更高(盡管NSTimer也可以修改精度)恤左,不過和NStimer類似的是如果遇到大任務它仍然存在丟幀現(xiàn)象。通常情況下CADisaplayLink用于構(gòu)建幀動畫搀绣,看起來相對更加流暢飞袋,而NSTimer則有更廣泛的用處。

9.2 AutoreleasePool

AutoreleasePool是另一個與 RunLoop 相關(guān)討論較多的話題链患。其實從 RunLoop 源代碼分析巧鸭,AutoreleasePool與 RunLoop 并沒有直接的關(guān)系,之所以將兩個話題放到一起討論最主要的原因是因為在iOS應用啟動后會注冊兩個 Observer 管理和維護AutoreleasePool麻捻。不妨在應用程序剛剛啟動時打印currentRunLoop可以看到系統(tǒng)默認注冊了很多個 Observer纲仍,其中有兩個 Observer 的callout都是_ wrapRunLoopWithAutoreleasePoolHandler,這兩個是和自動釋放池相關(guān)的兩個監(jiān)聽贸毕。

<CFRunLoopObserver 0x6080001246a0 [0x101f81df0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1020e07ce), context = <CFArray 0x60800004cae0 [0x101f81df0]>{type = mutable-small, count = 0, values = ()}}
<CFRunLoopObserver 0x608000124420 [0x101f81df0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1020e07ce), context = <CFArray 0x60800004cae0 [0x101f81df0]>{type = mutable-small, count = 0, values = ()}}

第一個 Observer 會監(jiān)聽 RunLoop 的進入郑叠,它會回調(diào)objc_autoreleasePoolPush()向當前的AutoreleasePoolPage增加一個哨兵對象標志創(chuàng)建自動釋放池。這個 Observer 的order是-2147483647優(yōu)先級最高明棍,確保發(fā)生在所有回調(diào)操作之前乡革。

第二個 Observer 會監(jiān)聽 RunLoop 的進入休眠和即將退出 RunLoop 兩種狀態(tài),在即將進入休眠時會調(diào)用objc_autoreleasePoolPop()objc_autoreleasePoolPush()根據(jù)情況從最新加入的對象一直往前清理直到遇到哨兵對象摊腋。而在即將退出 RunLoop 時會調(diào)用objc_autoreleasePoolPop()釋放自動自動釋放池內(nèi)對象沸版。這個 Observer 的order是2147483647,優(yōu)先級最低兴蒸,確保發(fā)生在所有回調(diào)操作之后视粮。

主線程的其他操作通常均在這個AutoreleasePool之內(nèi)(main函數(shù)中)咐熙,以盡可能減少內(nèi)存維護操作(當然你如果需要顯式釋放【例如循環(huán)】時可以自己創(chuàng)建AutoreleasePool否則一般不需要自己創(chuàng)建)加矛。

其實在應用程序啟動后系統(tǒng)還注冊了其他 Observer(例如即將進入休眠時執(zhí)行注冊回調(diào)_UIGestureRecognizerUpdateObserver用于手勢處理、回調(diào)為_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv的Observer用于界面實時繪制更新)和多個Source1(例如context為CFMachPortSource1用于接收硬件事件響應進而分發(fā)到應用程序一直到UIEvent),這里不再一一詳述默终。

9.3 UI更新

如果打印App啟動之后的主線程 RunLoop 可以發(fā)現(xiàn)另外一個 callout 為_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv的 Observer遭商,這個監(jiān)聽專門負責UI變化后的更新泌豆,比如修改了frame毅舆、調(diào)整了UI層級(UIView/CALayer)或者手動設置了setNeedsDisplay:/setNeedsLayout:之后就會將這些操作提交到全局容器。而這個 Observer 監(jiān)聽了主線程 RunLoop 的即將進入休眠和退出狀態(tài)议谷,一旦進入這兩種狀態(tài)則會遍歷所有的UI更新并提交進行實際繪制更新炉爆。

通常情況下這種方式是完美的,因為除了系統(tǒng)的更新卧晓,還可以利用setNeedsDisplay等方法手動觸發(fā)下一次 RunLoop 運行的更新芬首。但是如果當前正在執(zhí)行大量的邏輯運算可能UI的更新就會比較卡,因此facebook推出了Texture來解決這個問題逼裆。

Texture其實是將UI排版和繪制運算盡可能放到后臺郁稍,將UI的最終更新操作放到主線程(這一步也必須在主線程完成),同時提供一套類UIViewCALayer的相關(guān)屬性胜宇,盡可能保證開發(fā)者的開發(fā)習慣耀怜。這個過程中Texture在主線程 RunLoop 中增加了一個Observer 監(jiān)聽即將進入休眠和退出 RunLoop 兩種狀態(tài),收到回調(diào)時遍歷隊列中的待處理任務一一執(zhí)行。

9.4 NSURLConnection

一旦啟動NSURLConnection以后就會不斷調(diào)用delegate方法接收數(shù)據(jù)桐愉,這樣一個連續(xù)的的動作正是基于 RunLoop 來運行财破。

一旦NSURLConnection設置了delegate會立即創(chuàng)建一個線程com.apple.NSURLConnectionLoader,同時內(nèi)部啟動 RunLoop 并在NSDefaultMode模式下添加4個Source0从诲。其中CFHTTPCookieStorage用于處理cookie;CFMultiplexerSource負責各種delegate回調(diào)并在回調(diào)中喚醒delegate內(nèi)部的RunLoop(通常是主線程)來執(zhí)行實際操作左痢。

早期版本的AFNetworking庫也是基于NSURLConnection實現(xiàn),為了能夠在后臺接收delegate回調(diào)AFNetworking內(nèi)部創(chuàng)建了一個空的線程并啟動了RunLoop系洛,當需要使用這個后臺線程執(zhí)行任務時AFNetworking通過performSelector:onThread:將這個任務放到后臺線程的 RunLoop 中俊性。

十: GCD和RunLoop的關(guān)系

在 RunLoop 的源代碼中可以看到用到了GCD的相關(guān)內(nèi)容,但是 RunLoop 本身和GCD并沒有直接的關(guān)系描扯。當調(diào)用了dispatch_async(dispatch_get_main_queue(), ^(void)block)libDispatch會向主線程 RunLoop 發(fā)送消息喚醒 RunLoop定页,RunLoop 從消息中獲取block,并且在__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__回調(diào)里執(zhí)行這個block绽诚。不過這個操作僅限于主線程拯勉,其他線程dispatch操作是全部由libDispatch驅(qū)動的。

十一: 更多RunLoop使用

寫到了這里憔购,那么除了這些究竟在實際開發(fā)過程中我們自己能不能適當?shù)氖褂?RunLoop 幫我們做一些事情呢?

思考這個問題其實只要看RunLoopRef的包含關(guān)系就知道了岔帽,RunLoop包含多個Mode玫鸟,而它的Mode又是可以自定義的,這么推斷下來其實無論是Source1犀勒、Timer還是Observer開發(fā)者都可以利用屎飘,但是通常情況下不會自定義Timer妥曲,更不會自定義一個完整的Mode,利用更多的其實是Observer和Mode的切換钦购。

例如很多人都熟悉的使用perfromSelector在默認模式下設置圖片檐盟,防止UITableView滾動卡頓:

[imgView performSelector:@selector(setImage:) withObject:myImage afterDelay:0.0 inModes:@NSDefaultRunLoopMode]

還有sunnyxx的UITableView+FDTemplateLayoutCell利用 Observer 在界面空閑狀態(tài)下計算出UITableViewCell的高度并進行緩存。

關(guān)于如何自定義一個Custom Input Source官網(wǎng)給出了詳細的流程押桃。

11.1: 自定義常駐線程

// 1.全局的葵萎,或者static的,目的是保持`NSThread`
@property (nonatomic, strong) NSThread *myThread;

// 2.初始化線程并啟動
self.myThread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
self.myThread.name = @"myThread";
[self.myThread start];

// 3.啟動RunLoop唱凯,子線程的RunLoop默認是停止的
- (void)run {
    // 只要往RunLoop中添加了 timer羡忘、source或者observer就會繼續(xù)執(zhí)行
    // 一個RunLoop通常必須包含一個輸入源或者定時器來監(jiān)聽事件
    // 如果一個都沒有,RunLoop啟動后立即退出磕昼。
    @autoreleasepool {
        // 添加一個input source
        NSRunLoop *rl = [NSRunLoop currentRunLoop];
        [rl addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [rl run];
// 2卷雕、添加一個定時器
// NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
// [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// [[NSRunLoop currentRunLoop] run];
    }
}

這樣myThread這個線程就會一直存在,當需要使用此線程在處理一些事情的時候就這么調(diào)用:

[self performSelector:@selector(act) onThread:self.thread withObject:nil waitUntilDone:NO];

- (void)act {
    NSLog(@"111");
    NSLog(@"%@", [NSThread currentThread]);
}

END票从。
我是小侯爺漫雕。
在帝都艱苦奮斗,白天是上班族峰鄙,晚上是知識服務工作者浸间。
如果讀完覺得有收獲的話,記得關(guān)注和點贊哦先馆。
非要打賞的話发框,我也是不會拒絕的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末煤墙,一起剝皮案震驚了整個濱河市梅惯,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌仿野,老刑警劉巖铣减,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異脚作,居然都是意外死亡葫哗,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門球涛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來劣针,“玉大人,你說我怎么就攤上這事亿扁∞嗟洌” “怎么了?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵从祝,是天一觀的道長襟己。 經(jīng)常有香客問我引谜,道長,這世上最難降的妖魔是什么擎浴? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任员咽,我火速辦了婚禮,結(jié)果婚禮上贮预,老公的妹妹穿的比我還像新娘贝室。我一直安慰自己,他們只是感情好萌狂,可當我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布档玻。 她就那樣靜靜地躺著,像睡著了一般茫藏。 火紅的嫁衣襯著肌膚如雪误趴。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天务傲,我揣著相機與錄音凉当,去河邊找鬼。 笑死售葡,一個胖子當著我的面吹牛看杭,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播挟伙,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼楼雹,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了尖阔?” 一聲冷哼從身側(cè)響起贮缅,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎介却,沒想到半個月后谴供,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡齿坷,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年桂肌,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片永淌。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡崎场,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出遂蛀,到底是詐尸還是另有隱情照雁,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站饺蚊,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏悬嗓。R本人自食惡果不足惜污呼,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望包竹。 院中可真熱鬧燕酷,春花似錦、人聲如沸周瞎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽声诸。三九已至酱讶,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間彼乌,已是汗流浹背泻肯。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留慰照,地道東北人灶挟。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像毒租,于是被迫代替她去往敵國和親稚铣。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,037評論 2 355

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