Objective-C中的load方法執(zhí)行的來龍去脈

引子

我們都知道: Objective-C中類Class+load方法會在類第一次加載到內(nèi)存時, 并且APP的整個生命周期只會執(zhí)行一次. 但是知其然最好知其所以然, 今天來分析一下+load方法執(zhí)行的來龍去脈.

準(zhǔn)備工作, 本文涉及到的Apple 開源源碼如下:

  • dyld-635.2
  • objc4-750

上一篇文章<<iOS APP啟動前后發(fā)生了什么?>>開篇, 有如下的調(diào)用棧:

0 +[AppDelegate load]
1 call_load_methods
2 load_images
// 這里是一個斷層
3 dyld::notifySingle(dyld_image_states, ImageLoader const*)
4 ImageLoader::recursiveInitialization(...)
5 ImageLoader::processInitializers(...)
6 ImageLoader::runInitializers(...)
7 dyld::_main(...)
8 dyldbootstrap::start(...)
9 _dyld_start

我們能看到實際最后會調(diào)用+[Class load]類方法, 我們發(fā)現(xiàn)從調(diào)用棧那里有一個斷層, 3 dyld::notifySingle(dyld_image_states, ImageLoader const*) -> +[AppDelegate load]的過程, 明顯不是在dyld, ImageLoader庫中, 而是在runtime中的方法, 重要的原因就是dyld::notifySingle是對外發(fā)送兩個一個通知, 而loadImage是針對通知注冊的handler.

而前文在講到, 當(dāng)運行到后面會在runtime初始化時調(diào)用_objc_init, 這個方法最后會調(diào)用dyld::_dyld_objc_notify_register方法注冊三個hanlder, 其中有一個方法就是runtimeload_images, 因此dyld::notifySingle實際是發(fā)出了某個通知, 觸發(fā)load_images.

void _objc_init(void) {
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    environ_init();
    tls_init();
    static_init();
    lock_init();
    exception_init();

    // 這里是在dyld中加入一個監(jiān)聽器, 一旦dyld監(jiān)聽到有新的鏡像加載到runtime時, 就調(diào)用 load_images 方法, 并傳入最新鏡像的信息類別 infoList
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

dyld_image的state的監(jiān)聽與通知

為了證明我們前面的內(nèi)容, 我們需要在源碼中去找到線索.

我們打開dyld的源碼dyld_priv.h, 中的關(guān)于dyld_image_states的定義:

// DEPRECATED 
// dyld_image 整個生命周期中會經(jīng)歷的狀態(tài)
enum dyld_image_states {
    dyld_image_state_mapped                 = 10,       // No batch notification for this - 是否已經(jīng)映射
    dyld_image_state_dependents_mapped      = 20,       // Only batch notification for this - 依賴是否映射
    dyld_image_state_rebased                = 30,       // rebase
    dyld_image_state_bound                  = 40,       // 已經(jīng)bound
    dyld_image_state_dependents_initialized = 45,       // Only single notification for this
    dyld_image_state_initialized            = 50,       // -- 已經(jīng)初始化!!!!! 重要的狀態(tài)
    dyld_image_state_terminated             = 60        // Only single notification for this
};

而且dyld_image.state的狀態(tài)切換都是在dyld::_main(...)方法中進行的, 我們將該方法簡寫如下:

uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, int argc, const char* argv[], const char* envp[], const char* apple[], uintptr_t* startGlue){
    
    ...

    // dyld::instantiateFromLoadedImage ->  ImageLoaderMachOClassic::instantiateMainExecutable(create image for main executable) -> setMapped -> dyld_state = dyld_image_state_mapped-> 發(fā)出notification
    // 初始化完成以后, sMainExecutable被push到 sAllImages, 并且將它的關(guān)鍵信息插入到MappedRanges鏈表(這個鏈表中的內(nèi)容已經(jīng)mapped完畢)
    sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath); // dyld_image_state_mapped 并通知

    ...

    /*
    注意, 這里執(zhí)行 link(...) 時, linkingMainExecutable = true!!!
    1. recursiveLoadLibraries -> dyld_image_state_dependents_mapped 并通知
    2. recursiveRebase -> dyld_image_state_rebased 并通知
    (不會執(zhí)行: 3. recursiveBindWithAccounting -> recursiveBind -> dyld_image_state_bound)
    (不會執(zhí)行: 4. weakBind -> 通知 dyld_image_state_bound)
    */
    link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);

    ...

    gLinkContext.linkingMainExecutable = false;

    // 從這里開始 linkingMainExecutable = false, 也就是 MainImageLoader完成link操作!!! 切換 MainImageLoader.fstate = dyld_image_state_bound, 并發(fā)送 dyld_image_state_bound_notify 通知
    // Bind and notify for the main executable now that interposing has been registered
    uint64_t bindMainExecutableStartTime = mach_absolute_time();
    sMainExecutable->recursiveBindWithAccounting(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true);
    uint64_t bindMainExecutableEndTime = mach_absolute_time();
    ImageLoaderMachO::fgTotalBindTime += bindMainExecutableEndTime - bindMainExecutableStartTime;
    gLinkContext.notifyBatch(dyld_image_state_bound, false);

    ...

    /*
    這里開始執(zhí)行各個dyld_image的initializers方法, 當(dāng)初始化完成以后, 就MainImageLoader.fstate = dyld_image_state_initialized, 并發(fā)送 dyld_image_state_initialized_notify 通知
    1. initializeMainExecutable
    2. sMainExecutable->runInitializers
    3. sMainExecutable->processInitializers
    4. context.notifyBatch(dyld_image_state_initialized, false);
    */
    initializeMainExecutable(); 

    ...

    return Main函數(shù)的入口
}

在梳理之前, 我們需要有一個簡單的概念, 關(guān)于link(..)過程中的rebasebind.

當(dāng)mach-odyld_image二進制文件被加載到內(nèi)存中以后, 由于地址空間加載隨機化(ASLR, Address Space Layout Randomization)的緣故, 二進制文件最終的加載地址與預(yù)期地址之間會存在偏移, 所以需要進行rebase操作, 對那些指向文件內(nèi)部符號的指針進行修正, 在 link 函數(shù)中該項操作由 recursiveRebase 函數(shù)執(zhí)行. rebase 完成之后, 就會進行 bind 操作, 修正那些指向其他二進制文件所包含的符號的指針, 由 recursiveBind函數(shù)執(zhí)行呜魄。 當(dāng)rebase以及bind結(jié)束時, link函數(shù)就完成了它的使命.

我們能看到在dyld::_main(...)函數(shù)中dyld_image會隨著過程切換自己的state狀態(tài), 并且對外發(fā)出相關(guān)狀態(tài)的通知.

同時我們在源碼中有如下代碼:

// DEPRECATED -- 當(dāng) dyld_image 的state狀態(tài)變化以后, 調(diào)用的回調(diào)函數(shù)callback格式如下
typedef const char* (*dyld_image_state_change_handler)(enum dyld_image_states state, uint32_t infoCount, const struct dyld_image_info info[]);

// 注冊的方法的 函數(shù)指針當(dāng)  mapped/ init/ unmapped 狀態(tài)時, 分別調(diào)用的callback格式如下
typedef void (*_dyld_objc_notify_mapped)(unsigned count, const char* const paths[], const struct mach_header* const mh[]);
typedef void (*_dyld_objc_notify_init)(const char* path, const struct mach_header* mh);
typedef void (*_dyld_objc_notify_unmapped)(const char* path, const struct mach_header* mh);

// 
// Note: only for use by objc runtime 
// 這個方法只有在 runtime 的 _objc_init 方法中調(diào)用
// Register handlers to be called when objc images are mapped, unmapped, and initialized.
// Dyld will call back the "mapped" function with an array of images that contain an objc-image-info section.
// Those images that are dylibs will have the ref-counts automatically bumped, so objc will no longer need to
// call dlopen() on them to keep them from being unloaded.  

// 1. During the call to _dyld_objc_notify_register(), dyld will call the "mapped" function with already loaded objc images.  
// 2. During any later dlopen() call, dyld will also call the "mapped" function.  (每一次調(diào)用dlopen(), 都會調(diào)用'mapped' function)
// 3. Dyld will call the "init" function when dyld would be called initializers in that image.  This is when objc calls any +load methods in that image. - 當(dāng) image狀態(tài)變化成 initializer 時候, 會調(diào)用`init` callback, 這個callback在實際代碼中是調(diào)用的 objc4.750 的 `loadImages` 方法, 這個方法內(nèi)部會調(diào)用這個`image`中的每個`Class`的`+load`方法

void _dyld_objc_notify_register(_dyld_objc_notify_mapped    mapped,
                                _dyld_objc_notify_init      init,
                                _dyld_objc_notify_unmapped  unmapped) {
    dyld::registerObjCNotifiers(mapped, init, unmapped);
}

注意上面這個方法_dyld_objc_notify_register是在dyld::_main方法中的mainImageLoaderlink(...)方法結(jié)束以后, 由于依賴的庫中有libSystemlibCloure從而加載runtime_objc_init(...)方法結(jié)束時候才調(diào)用, 因此在執(zhí)行_dyld_objc_notify_register以后, 相當(dāng)于runtime就會監(jiān)聽所有在runtime之后被加載的dyld_image, 根據(jù)他們的的狀態(tài), 去調(diào)用注冊的3個回調(diào)函數(shù), 這里我們重點關(guān)注load_images方法.

load_imagesruntime_objc_init(...)被注冊以后,一旦dyld中有新的image狀態(tài)成為init(也就是dyld_image_state_initialized, 此時表示該image已經(jīng)完成link), 就會調(diào)用load_images方法, 對這個完全初始化成功的image中的內(nèi)容做一些處理.

objc中的load_images

load_images方法的源碼如下:

/***********************************************************************
* load_images
* Process +load in the given images which are being mapped in by dyld.
*
* Locking: write-locks runtimeLock and loadMethodLock

 有新的鏡像image被加載到 runtime 時娇澎,調(diào)用 load_images 方法睹晒,并傳入最新鏡像image的信息列表 infoList:

  images 是鏡像的意思: 這里就會遇到一個問題:鏡像到底是什么,我們用一個斷點打印出所有加載的鏡, 從控制臺輸出的結(jié)果大概就是這樣的岔激,我們可以看到鏡像并不是一個 Objective-C 的代碼文件是掰,它應(yīng)該是一個 target 的編譯產(chǎn)物辱匿。這里面有很多的動態(tài)鏈接庫,還有一些蘋果為我們提供的框架匾七,比如 Foundation、 CoreServices 等等丁频,都是在這個 load_images 中加載進來的,而這些 imageFilePath 都是對應(yīng)的二進制文件的地址席里。

 +load 的應(yīng)用:

 +load 可以說我們在日常開發(fā)中可以接觸到的調(diào)用時間最靠前的方法,在主函數(shù)運行之前改基,load 方法就會調(diào)用咖为。

 由于它的調(diào)用不是惰性的,且其只會在程序調(diào)用期間調(diào)用一次躁染,最最重要的是,如果在類與分類中都實現(xiàn)了 load 方法诺舔,它們都會被調(diào)用备畦,不像其它的在分類中實現(xiàn)的方法會被覆蓋,這就使 load 方法成為了方法調(diào)劑的絕佳時機懂盐。

 但是由于 load 方法的運行時間過早,所以這里可能不是一個理想的環(huán)境拌喉,因為某些類可能需要在在其它類之前加載俐银,但是這是我們無法保證的。不過在這個時間點捶惜,所有的 framework 都已經(jīng)加載到了運行時中,所以調(diào)用 framework 中的方法都是安全的汽久。
**********************************************************************/
extern bool hasLoadMethods(const headerType *mhdr);
extern void prepare_load_methods(const headerType *mhdr);

void load_images(const char *path __unused, const struct mach_header *mh) {
    // Return without taking locks if there are no +load methods here.
    // 如果 沒有 +load 方法, 直接返回
    if (!hasLoadMethods((const headerType *)mh)) return;

    // 此時表示 mh中有 +load 方法

    // 上鎖, 不能同時多個線程執(zhí)行 loadMethod, 鎖1
    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    {
        // runtimeLock 兩個鎖, 鎖2
        // 這里 write-locks 需要兩個鎖
        mutex_locker_t lock2(runtimeLock);
        //調(diào)用 prepare_load_methods 對 load 方法的調(diào)用進行準(zhǔn)備, 主要工作就是將Class的所有方法都加載到一個叫l(wèi)oadable_classes的數(shù)組中
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    // 在將鏡像加載到運行時, 對 load 方法的準(zhǔn)備就緒之后踊餐,執(zhí)行 call_load_methods,開始調(diào)用 load 方法
    call_load_methods();
}

load_images中的源碼游走以后, 我們主要看到兩個重要的步驟 -- 準(zhǔn)備load和調(diào)用load:

  1. 當(dāng)有新的鏡像被dyld加載, runtime就會去該鏡像中對所有的 class/category 進行準(zhǔn)備操作.
  2. 準(zhǔn)備操作是prepare_load_methods
  3. 調(diào)用操作是call_load_methods

objc中如何準(zhǔn)備 -- prepare_load_methods解析

/**
 準(zhǔn)備load methods
 */
void prepare_load_methods(const headerType *mhdr) {
    size_t count, i;

    // 調(diào)用 load_method 時, 必須是 runtimeLock已經(jīng)上鎖
    runtimeLock.assertLocked();

    //處理mach-o中的class:
    // 通過 _getObjc2NonlazyClassList 獲取二進制文件中所有的類的列表之后三痰,會通過 remapClass 獲取類對應(yīng)的指針,然后調(diào)用 schedule_class_load 遞歸地安排當(dāng)前類的父類和當(dāng)前類加入到一個 loadable_list中
    classref_t *classlist =
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        // 內(nèi)部處理以后調(diào)用 add_class_to_loadable_list方法
        schedule_class_load(remapClass(classlist[i]));
    }

    //處理mach-o中的categorys:
    // 通過 _getObjc2NonlazyCategoryList 方法獲取二進制文件中所有的category的列表, 然后遞歸處理每個單獨的category.
    // 單獨處理Category的過程如下: 首先獲取每個category的Class, 然后先調(diào)用一個關(guān)鍵的方法`realizeClass`, 這個方法能夠保證每個類已經(jīng)被runtime進行了`realize`過, 這個過程很重要(后面有專門的文章來解釋整個realize的過程), 然后調(diào)用`add_category_to_loadable_list`將category方法加入loadable_list
    category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
    for (i = 0; i < count; i++) {
        category_t *cat = categorylist[i];
        Class cls = remapClass(cat->cls);
        if (!cls) continue;  // category for ignored weak-linked class
        // realizeClass 做的工作就是Class第一次 initiail
        realizeClass(cls);
        assert(cls->ISA()->isRealized());
        // 獲取 category中的+load方法, 然后按照一定順序?qū)?load方法加入到一個loadabel_list中
        add_category_to_loadable_list(cat);
    }
}

通過源碼注釋, 我們可以看出準(zhǔn)備過程會處理兩塊內(nèi)容, 分別是鏡像中的class以及category.

如果處理鏡像中的class, 過程是:

  1. _getObjc2NonlazyClassList獲取二進制文件中所有的類, 放到一個鏈表中, 然后遞歸處理每個class
  2. 遍歷這個鏈表, 取出每個節(jié)點, 先調(diào)用remapClass, 然后調(diào)用schedule_class_load
  3. schedule_class_load主要是將入?yún)⒌?code>class的繼承鏈的每個+load方法都加入到loadable_classes鏈表中. 注意這里的添加+load方法到鏈表的順序是, 先父類, 然后自己.

如果處理鏡像中的category, 過程有點不一樣:

  1. _getObjc2NonlazyCategoryList方法獲取二進制中所有的category, 放到一個鏈表中, 然后遞歸處理每個category
  2. 單獨category的過程是: 首先獲取每個category對應(yīng)的class, 先對class進行remapClass,調(diào)用一個關(guān)鍵的方法realizeClass(我們可以認(rèn)為這個方法是Class類對象在內(nèi)存中的初始化創(chuàng)建方法),最后調(diào)用add_category_to_loadable_list方法
  3. add_category_to_loadable_list是將category按照一定順序?qū)?code>+load方法加入到一個叫做loadable_categories鏈表中.

realizeClass方法我們后面專門分析, 這里我們只簡單了解一下. 我們直到Class在編譯期間, 有很多方法是我們自己在代碼里面定義的, 這些方法在編譯器編譯期間就搞定了, 當(dāng)它加載到內(nèi)存時候, 它的方法列表里面都是編譯期間確定的方法, 我們稱為只讀方法, 但是還有一些方法例如在category中的方法, 也是與Class有關(guān)的, 但是并沒有與這個Class關(guān)聯(lián), 通過realizeClass來調(diào)整類在內(nèi)存中的結(jié)構(gòu), 例如將category中的方法都關(guān)聯(lián)到class上去, 添加到class的方法列表中, 當(dāng)然, 還有一些其他的作用, 后面再講.

objc中正式調(diào)用每個類的+load方法 -- call_load_methods解析

void call_load_methods(void) {
    static bool loading = NO;
    bool more_categories;

    loadMethodLock.assertLocked();

    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush();

    do {
        // 1. Repeatedly call class +loads until there aren't any more
        while (loadable_classes_used > 0) {
            call_class_loads(); // 這里會調(diào)用  load 方法
        }

        // 2. Call category +loads ONCE
        more_categories = call_category_loads();

        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}

static void call_class_loads(void) {
    int i;
    
    // Detach current loadable list.
    // 用一個便利結(jié)構(gòu)體, 內(nèi)部持有Class對應(yīng)方法的+load IMP
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;
    
    // Call all +loads for the detached list.
    // 遍歷所有的 loadable_classes 中的每個 loadable Class, 從中按照順序取出+load方法, 按照 loadable_list 的順序執(zhí)行!!!
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
        load_method_t load_method = (load_method_t)classes[i].method;
        if (!cls) continue; 

        if (PrintLoading) {
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
        // 某個類的 +load 方法會執(zhí)行!!!
        (*load_method)(cls, SEL_load);
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}

簡單來說, 就是將loadable_classes中之前存儲的class+load調(diào)用, 然后清理, 最后調(diào)用loadable_categories緩存的category相關(guān)的+load方法.

小總結(jié)

  1. +load方法是如何被調(diào)用的舷丹?

runtime在初始化時, 會注冊一個回調(diào), 去監(jiān)聽鏡像加載, 每次有新的鏡像加載時, 就會調(diào)用注冊的load_images回調(diào), 這個方法會將鏡像中所有類和分類的+load方法按照一定順序, 放到loadable_classesloadable_categories兩個鏈表中, 然后遍歷執(zhí)行兩個鏈表中的每個節(jié)點的+load方法.

  1. +load方法的調(diào)用順序如何?
    1. 父類的+load會先調(diào)用, 然后才調(diào)用子類+load
    2. 類的+load會先調(diào)用, 然后調(diào)用分類的+load
    3. 總得來說, 會先調(diào)用super類的+load方法, 然后調(diào)用自身的+load方法, 最后調(diào)用分類重寫的+load方法.

并且結(jié)合前面文章我們知道: +load方法會先于app的啟動方法main執(zhí)行, 并且它在全局只會調(diào)用一次等特性, +load方法是讓我們實現(xiàn)的method swizzling最佳位置!!!!

就算分類重寫了+load方法, 通過上面分析, 仍然會按照父類, 本類, 分類的順序執(zhí)行+load方法. 需要注意這點比較特殊, 與其他方法的執(zhí)行不一樣!!!

參考

iOS程序啟動->dyld加載->runtime初始化(初識)
你真的了解load方法么?
http://www.cocoachina.com/ios/20170716/19876.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末颜凯,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子症概,更是在濱河造成了極大的恐慌,老刑警劉巖诅蝶,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件募壕,死亡現(xiàn)場離奇詭異,居然都是意外死亡舱馅,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進店門棘钞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來干毅,“玉大人,你說我怎么就攤上這事硝逢。” “怎么了垫毙?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵拱绑,是天一觀的道長丽蝎。 經(jīng)常有香客問我膀藐,道長红省,這世上最難降的妖魔是什么额各? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任虾啦,我火速辦了婚禮,結(jié)果婚禮上傲醉,老公的妹妹穿的比我還像新娘呻率。我一直安慰自己,他們只是感情好礼仗,可當(dāng)我...
    茶點故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著韭脊,像睡著了一般单旁。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上慎恒,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天,我揣著相機與錄音死嗦,去河邊找鬼粒氧。 笑死越除,一個胖子當(dāng)著我的面吹牛外盯,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播孩擂,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼箱熬,長吁一口氣:“原來是場噩夢啊……” “哼狈邑!你這毒婦竟也來了蚤认?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤砰琢,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后亏较,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡雪情,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年你辣,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片宴凉。...
    茶點故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡表悬,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蟆沫,到底是詐尸還是另有隱情,我是刑警寧澤饭庞,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布舟山,位于F島的核電站绸狐,受9級特大地震影響累盗,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜若债,卻給世界環(huán)境...
    茶點故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一拆座、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧挪凑,春花似錦、人聲如沸躏碳。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽咬最。三九已至翎嫡,卻和暖如春永乌,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背翅雏。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工望几, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留绩脆,地道東北人橄抹。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像矢劲,于是被迫代替她去往敵國和親慌随。 傳聞我的和親對象是個殘疾皇子芬沉,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,435評論 2 359

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