Android跨進程通信IPC之16——Binder之native層C++篇--獲取服務

移步系列Android跨進程通信IPC系列

1 源碼位置

/frameworks/av/media/libmedia/
  - IMediaDeathNotifier.cpp

framework/native/libs/binder/
  - Binder.cpp
  - BpBinder.cpp
  - IPCThreadState.cpp
  - ProcessState.cpp
  - IServiceManager.cpp

對應的鏈接為

在Native層的服務注冊,我們依舊選擇media為例展開講解,先來看看media類關系圖。

2類圖

5713484-1048ad1cfa1b87af.png

圖解:

  • 藍色:代表獲取MediaPlayerService服務相關的類
  • 綠色:代表Binder架構中與Binder驅動通信過程中的最為核心的兩個類
  • 紫色:代表注冊服務獲取服務的公共接口/父類

3 獲取服務流程

3.1 getMediaPlayerService()函數

//frameworks/av/media/libmedia/IMediaDeathNotifier.cpp   35行
sp<IMediaPlayerService>&
IMediaDeathNotifier::getMediaPlayerService()
{
    Mutex::Autolock _l(sServiceLock);
    if (sMediaPlayerService == 0) {
         // 獲取 ServiceManager
        sp<IServiceManager> sm = defaultServiceManager(); 
        sp<IBinder> binder;
        do {
            //獲取名為"media.player"的服務
            binder = sm->getService(String16("media.player"));
            if (binder != 0) {
                break;
            }
            usleep(500000); // 0.5s
        } while (true);

        if (sDeathNotifier == NULL) {
            // 創建死亡通知對象
            sDeathNotifier = new DeathNotifier(); 
        }

        //將死亡通知連接到binder
        binder->linkToDeath(sDeathNotifier);
        sMediaPlayerService = interface_cast<IMediaPlayerService>(binder);
    }
    return sMediaPlayerService;
}

其中defaultServiceManager()過程在上面已經說了,返回的是BpServiceManager

在請求獲取名為"media.player"的服務過程中,采用不斷循環獲取的方法。由于MediaPlayerService服務可能還沒向ServiceManager注冊完成或者尚未啟動完成等情況,故則binder返回NULL,休眠0.5s后繼續請求,知道獲取服務為止。

3.2 BpServiceManager.getService()函數

//frameworks/native/libs/binder/IServiceManager.cpp       134行
virtual sp<IBinder> getService(const String16& name) const
    {
        unsigned n;
        for (n = 0; n < 5; n++){
            sp<IBinder> svc = checkService(name); 
            if (svc != NULL) return svc;
            sleep(1);
        }
        return NULL;
    }
  • 通過BpServiceManager來獲取MediaPlayer服務:檢索服務是否存在,當服務存在則返回相應的服務,當服務不存在則休眠1s再繼續檢索服務。
  • 該循環進行5次。為什么循環5次?這估計和Android的ANR的時間為5s相關。如果每次都無法獲取服務,循環5次,每次循環休眠1s,忽略checkService()的時間,差不多是5s的時間。

3.3 BpSeriveManager.checkService()函數

//frameworks/native/libs/binder/IServiceManager.cpp    146行
virtual sp<IBinder> checkService( const String16& name) const
{
    Parcel data, reply;
    //寫入RPC頭
    data.writeInterfaceToken(IServiceManager::getInterfaceDescriptor());
    //寫入服務名
    data.writeString16(name);
    remote()->transact(CHECK_SERVICE_TRANSACTION, data, &reply); 
    return reply.readStrongBinder(); 
}

檢索制定服務是否存在,其中remote()為BpBinder

3.4 BpBinder::transact()函數

// /frameworks/native/libs/binder/BpBinder.cpp    159行
status_t BpBinder::transact(
    uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags)
{
    if (mAlive) {
        status_t status = IPCThreadState::self()->transact(
            mHandle, code, data, reply, flags);
        if (status == DEAD_OBJECT) mAlive = 0;
        return status;
    }
    return DEAD_OBJECT;
}

Binder代理類調用transact()方法,真正工作還是交給IPCThreadState來進行transact工作。

3.4.1 IPCThreadState::self()函數

IPCThreadState* IPCThreadState::self()
{
    if (gHaveTLS) {
restart:
        const pthread_key_t k = gTLS;
        IPCThreadState* st = (IPCThreadState*)pthread_getspecific(k);
        if (st) return st;
        //初始化 IPCThreadState
        return new IPCThreadState;  
    }

    if (gShutdown) return NULL;
    pthread_mutex_lock(&gTLSMutex);
     //首次進入gHaveTLS為false
    if (!gHaveTLS) {
         //創建線程的TLS
        if (pthread_key_create(&gTLS, threadDestructor) != 0) { 
            pthread_mutex_unlock(&gTLSMutex);
            return NULL;
        }
        gHaveTLS = true;
    }
    pthread_mutex_unlock(&gTLSMutex);
    goto restart;
}
  • TLS是指Thread local storage(線程本地存儲空間),每個線程都擁有自己的TLS,并且是私有空間,線程之間不會共享。
  • 通過pthread_getspecific()/pthread_setspecific()函數可以獲取/設置這些空間中的內容。從線程本地存儲空間獲的保存期中的IPCThreadState對象。
    以后面的流程和上面的注冊流程大致相同,主要流程也是 IPCThreadState:: transact()函數、IPCThreadState::writeTransactionData()函數、IPCThreadState::waitForResponse()函數和IPCThreadState.talkWithDriver()函數,Android跨進程通信IPC之15——Binder之Framework層C++篇--注冊服務已經講解過了,這里就不詳細說明了。我們從IPCThreadState.talkWithDriver() 開始繼講解

3.4.2 IPCThreadState:: talkWithDriver()函數

status_t IPCThreadState::talkWithDriver(bool doReceive)
{
    ...
    binder_write_read bwr;
    const bool needRead = mIn.dataPosition() >= mIn.dataSize();
    const size_t outAvail = (!doReceive || needRead) ? mOut.dataSize() : 0;
    bwr.write_size = outAvail;
    bwr.write_buffer = (uintptr_t)mOut.data();
     //接收數據緩沖區信息的填充。如果以后收到數據,就直接填在mIn中了。
    if (doReceive && needRead) {
        bwr.read_size = mIn.dataCapacity();
        bwr.read_buffer = (uintptr_t)mIn.data();
    } else {
        bwr.read_size = 0;
        bwr.read_buffer = 0;
    }
    //當讀緩沖和寫緩沖都為空,則直接返回
    if ((bwr.write_size == 0) && (bwr.read_size == 0)) return NO_ERROR;

    bwr.write_consumed = 0;
    bwr.read_consumed = 0;
    status_t err;
    do {
        //通過ioctl不停的讀寫操作,跟Binder Driver進行通信
        if (ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr) >= 0)
            err = NO_ERROR;
        ...
       //當被中斷,則繼續執行
    } while (err == -EINTR); 
    ...
    return err;
}

binder_write_read結構體 用來與Binder設備交換數據的結構,通過ioctl與mDriverFD通信,是真正的與Binder驅動進行數據讀寫交互的過程。先向service manager進程發送查詢服務的請求(BR_TRANSACTION)。當service manager 進程收到帶命令后,會執行do_find_service()查詢服務所對應的handle,然后再binder_send_reply()應發送者,發送BC_REPLY協議,然后再調用binder_transaction(),再向服務請求者的todo隊列插入事務。接下來,再看看binder_transaction過程。

3.4.2.1 binder_transaction()函數

//kernel/drivers/android/binder.c     1827行
static void binder_transaction(struct binder_proc *proc,
               struct binder_thread *thread,
               struct binder_transaction_data *tr, int reply){
    //根據各種判定,獲取以下信息:
    // 目標線程
    struct binder_thread *target_thread; 
    // 目標進程
    struct binder_proc *target_proc; 
     /// 目標binder節點   
    struct binder_node *target_node;    
    // 目標 TODO隊列
    struct list_head *target_list;    
     // 目標等待隊列
    wait_queue_head_t *target_wait;    
    ...
    
    //分配兩個結構體內存
    struct binder_transaction *t = kzalloc(sizeof(*t), GFP_KERNEL);
    struct binder_work *tcomplete = kzalloc(sizeof(*tcomplete), GFP_KERNEL);
    //從target_proc分配一塊buffer
    t->buffer = binder_alloc_buf(target_proc, tr->data_size,

    for (; offp < off_end; offp++) {
        switch (fp->type) {
        case BINDER_TYPE_BINDER: ...
        case BINDER_TYPE_WEAK_BINDER: ...
        
        case BINDER_TYPE_HANDLE: 
        case BINDER_TYPE_WEAK_HANDLE: {
          struct binder_ref *ref = binder_get_ref(proc, fp->handle,
                fp->type == BINDER_TYPE_HANDLE);
          ...
          //此時運行在servicemanager進程,故ref->node是指向服務所在進程的binder實體,
          //而target_proc為請求服務所在的進程,此時并不相等。
          if (ref->node->proc == target_proc) {
            if (fp->type == BINDER_TYPE_HANDLE)
              fp->type = BINDER_TYPE_BINDER;
            else
              fp->type = BINDER_TYPE_WEAK_BINDER;
            fp->binder = ref->node->ptr;
             // BBinder服務的地址
            fp->cookie = ref->node->cookie; 
            binder_inc_node(ref->node, fp->type == BINDER_TYPE_BINDER, 0, NULL);
            
          } else {
            struct binder_ref *new_ref;
            //請求服務所在進程并非服務所在進程,則為請求服務所在進程創建binder_ref
            new_ref = binder_get_ref_for_node(target_proc, ref->node);
            fp->binder = 0;
             //重新給handle賦值
            fp->handle = new_ref->desc; 
            fp->cookie = 0;
            binder_inc_ref(new_ref, fp->type == BINDER_TYPE_HANDLE, NULL);
          }
        } break;
        
        case BINDER_TYPE_FD: ...
        }
    }
    //分別target_list和當前線程TODO隊列插入事務
    t->work.type = BINDER_WORK_TRANSACTION;
    list_add_tail(&t->work.entry, target_list);
    tcomplete->type = BINDER_WORK_TRANSACTION_COMPLETE;
    list_add_tail(&tcomplete->entry, &thread->todo);
    if (target_wait)
        wake_up_interruptible(target_wait);
    return;
}

這個過程非常重要,分兩種情況來說:

  • 情況1 當請求服務的進程與服務屬于不同的進程,則為請求服務所在進程創建binder_ref對象,指向服務進程中的binder_node
  • 當請求服務的進程與服務屬于同一進程,則不再創建新對象,只是引用計數+1,并且修改type為BINDER_TYPE_BINER或BINDER_TYPE_WEAK_BINDER。

3.4.2.2 binder_thread_read()函數

//kernel/drivers/android/binder.c    2650行
binder_thread_read(struct binder_proc *proc,struct binder_thread *thread,binder_uintptr_t binder_buffer, size_t size,binder_size_t *consumed, int non_block){
    ...
    //當線程todo隊列有數據則執行往下執行;當線程todo隊列沒有數據,則進入休眠等待狀態
    ret = wait_event_freezable(thread->wait, binder_has_thread_work(thread));
    ...
    while (1) {
        uint32_t cmd;
        struct binder_transaction_data tr;
        struct binder_work *w;
        struct binder_transaction *t = NULL;
        //先從線程todo隊列獲取事務數據
        if (!list_empty(&thread->todo)) {
            w = list_first_entry(&thread->todo, struct binder_work, entry);
        // 線程todo隊列沒有數據, 則從進程todo對獲取事務數據
        } else if (!list_empty(&proc->todo) && wait_for_proc_work) {
            ...
        }
        switch (w->type) {
            case BINDER_WORK_TRANSACTION:
                //獲取transaction數據
                t = container_of(w, struct binder_transaction, work);
                break;
                
            case : ...  
        }

        //只有BINDER_WORK_TRANSACTION命令才能繼續往下執行
        if (!t) continue;

        if (t->buffer->target_node) {
            ...
        } else {
            tr.target.ptr = NULL;
            tr.cookie = NULL;
            //設置命令為BR_REPLY
            cmd = BR_REPLY; 
        }
        tr.code = t->code;
        tr.flags = t->flags;
        tr.sender_euid = t->sender_euid;

        if (t->from) {
            struct task_struct *sender = t->from->proc->tsk;
            //當非oneway的情況下,將調用者進程的pid保存到sender_pid
            tr.sender_pid = task_tgid_nr_ns(sender, current->nsproxy->pid_ns);
        } else {
            ...
        }

        tr.data_size = t->buffer->data_size;
        tr.offsets_size = t->buffer->offsets_size;
        tr.data.ptr.buffer = (void *)t->buffer->data +
                    proc->user_buffer_offset;
        tr.data.ptr.offsets = tr.data.ptr.buffer +
                    ALIGN(t->buffer->data_size,
                        sizeof(void *));

        //將cmd和數據寫回用戶空間
        put_user(cmd, (uint32_t __user *)ptr);
        ptr += sizeof(uint32_t);
        copy_to_user(ptr, &tr, sizeof(tr));
        ptr += sizeof(tr);

        list_del(&t->work.entry);
        t->buffer->allow_user_free = 1;
        if (cmd == BR_TRANSACTION && !(t->flags & TF_ONE_WAY)) {
            ...
        } else {
            t->buffer->transaction = NULL;
            //通信完成則運行釋放
            kfree(t); 
        }
        break;
    }
done:
    *consumed = ptr - buffer;
    if (proc->requested_threads + proc->ready_threads == 0 &&
        proc->requested_threads_started < proc->max_threads &&
        (thread->looper & (BINDER_LOOPER_STATE_REGISTERED |
         BINDER_LOOPER_STATE_ENTERED))) {
        proc->requested_threads++;
        // 生成BR_SPAWN_LOOPER命令,用于創建新的線程
        put_user(BR_SPAWN_LOOPER, (uint32_t __user *)buffer);
    }
    return 0;
}

3.4.3 readStrongBinder()函數

//frameworks/native/libs/binder/Parcel.cpp   1334行
sp<IBinder> Parcel::readStrongBinder() const
{
    sp<IBinder> val;
    unflatten_binder(ProcessState::self(), *this, &val);
    return val;
}

里面主要是調用unflatten_binder()函數
那我們就來詳細看下

3.4.3.1 unflatten_binder()函數

status_t unflatten_binder(const sp<ProcessState>& proc,
    const Parcel& in, sp<IBinder>* out)
{
    const flat_binder_object* flat = in.readObject(false);
    if (flat) {
        switch (flat->type) {
            case BINDER_TYPE_BINDER:
                // 當請求服務的進程與服務屬于同一進程
                *out = reinterpret_cast<IBinder*>(flat->cookie);
                return finish_unflatten_binder(NULL, *flat, in);
            case BINDER_TYPE_HANDLE:
                //請求服務的進程與服務屬于不同進程
                *out = proc->getStrongProxyForHandle(flat->handle);
                //創建BpBinder對象
                return finish_unflatten_binder(
                    static_cast<BpBinder*>(out->get()), *flat, in);
        }
    }
    return BAD_TYPE;
}

如果服務的進程與服務屬于不同的進程會調用getStrongProxyForHandle()函數,那我們就好好研究下

3.4.3.2 getStrongProxyForHandle()函數

sp<IBinder> ProcessState::getStrongProxyForHandle(int32_t handle)
{
    sp<IBinder> result;

    AutoMutex _l(mLock);
    //查找handle對應的資源項[2.9.3]
    handle_entry* e = lookupHandleLocked(handle);

    if (e != NULL) {
        IBinder* b = e->binder;
        if (b == NULL || !e->refs->attemptIncWeak(this)) {
            ...
            //當handle值所對應的IBinder不存在或弱引用無效時,則創建BpBinder對象
            b = new BpBinder(handle);
            e->binder = b;
            if (b) e->refs = b->getWeakRefs();
            result = b;
        } else {
            result.force_set(b);
            e->refs->decWeak(this);
        }
    }
    return result;
}

readStrong的功能是flat_binder_object解析并創建BpBinder對象

3.4.3.3 lookupHandleLocked函數

ProcessState::handle_entry* ProcessState::lookupHandleLocked(int32_t handle)
{
    const size_t N=mHandleToObject.size();
    //當handle大于mHandleToObject的長度時,進入該分支
    if (N <= (size_t)handle) {
        handle_entry e;
        e.binder = NULL;
        e.refs = NULL;
        //從mHandleToObject的第N個位置開始,插入(handle+1-N)個e到隊列中
        status_t err = mHandleToObject.insertAt(e, N, handle+1-N);
        if (err < NO_ERROR) return NULL;
    }
    return &mHandleToObject.editItemAt(handle);
}

根據handle值來查找對應的handle_entry。

4 死亡通知

死亡通知時為了讓Bp端知道Bn端的生死情況

  • DeathNotifier是繼承IBinder::DeathRecipient類,主要需要實現其binderDied()來進行死亡通告。
  • 注冊:binder->linkToDeath(sDeathNotifier)是為了將sDeathNotifier死亡通知注冊到Binder上。

Bp端只需要覆寫binderDied()方法,實現一些后尾清楚類的工作,則在Bn端死掉后,會回調binderDied()進行相應處理

4.1 linkToDeath()函數

// frameworks/native/libs/binder/BpBinder.cpp   173行
status_t BpBinder::linkToDeath(
    const sp<DeathRecipient>& recipient, void* cookie, uint32_t flags)
{
    Obituary ob;
    ob.recipient = recipient;
    ob.cookie = cookie;
    ob.flags = flags;

    {
        AutoMutex _l(mLock);
        if (!mObitsSent) {
            if (!mObituaries) {
                mObituaries = new Vector<Obituary>;
                if (!mObituaries) {
                    return NO_MEMORY;
                }
                getWeakRefs()->incWeak(this);
                IPCThreadState* self = IPCThreadState::self();
                self->requestDeathNotification(mHandle, this);
                self->flushCommands();
            }
            ssize_t res = mObituaries->add(ob);
            return res >= (ssize_t)NO_ERROR ? (status_t)NO_ERROR : res;
        }
    }
    return DEAD_OBJECT;
}

里面調用了requestDeathNotification()函數

4.2 requestDeathNotification()函數

//frameworks/native/libs/binder/IPCThreadState.cpp    670行
status_t IPCThreadState::requestDeathNotification(int32_t handle, BpBinder* proxy)
{
    mOut.writeInt32(BC_REQUEST_DEATH_NOTIFICATION);
    mOut.writeInt32((int32_t)handle);
    mOut.writePointer((uintptr_t)proxy);
    return NO_ERROR;
}

向binder driver發送 BC_REQUEST_DEATH_NOTIFICATION命令。后面的流程和 Service Manager 里面的 ** binder_link_to_death() ** 的過程。

4.3 binderDied()函數

//frameworks/av/media/libmedia/IMediaDeathNotifier.cpp    78行
void IMediaDeathNotifier::DeathNotifier::binderDied(const wp<IBinder>& who __unused) {
    SortedVector< wp<IMediaDeathNotifier> > list;
    {
        Mutex::Autolock _l(sServiceLock);
        // 把Bp端的MediaPlayerService清除掉
        sMediaPlayerService.clear();   
        list = sObitRecipients;
    }

    size_t count = list.size();
    for (size_t iter = 0; iter < count; ++iter) {
        sp<IMediaDeathNotifier> notifier = list[iter].promote();
        if (notifier != 0) {
            //當MediaServer掛了則通知應用程序,應用程序回調該方法
            notifier->died(); 
        }
    }
}

客戶端進程通過Binder驅動獲得Binder的代理(BpBinder),死亡通知注冊的過程就是客戶端進程向Binder驅動注冊的一個死亡通知,該死亡通知關聯BBinder,即與BpBinder所對應的服務端。

4.4 unlinkToDeath()函數

當Bp在收到服務端的死亡通知之前先掛了,那么需要在對象的銷毀方法內,調用unlinkToDeath()來取消死亡通知;

//frameworks/av/media/libmedia/IMediaDeathNotifier.cpp    101行
IMediaDeathNotifier::DeathNotifier::~DeathNotifier()
{
    Mutex::Autolock _l(sServiceLock);
    sObitRecipients.clear();
    if (sMediaPlayerService != 0) {
        IInterface::asBinder(sMediaPlayerService)->unlinkToDeath(this);
    }
}

4.5 觸發時機

  • 每當service進程退出時,service manager 會收到來自Binder驅動的死亡通知。
  • 這項工作在啟動Service Manager時通過 binder_link_to_death(bs, ptr, &si->death)完成。
  • 另外,每個Bp端也可以自己注冊死亡通知,能獲取Binder的死亡消息,比如前面的IMediaDeathNotifier。

那Binder的死亡通知時如何被出發的?對于Binder的IPC進程都會打開/dev/binder文件,當進程異常退出的時候,Binder驅動會保證釋放將要退出的進程中沒有正常關閉的/dev/binder文件,實現機制是binder驅動通過調用/dev/binder文件所對應的release回調函數,執行清理工作,并且檢查BBinder是否有注冊死亡通知,當發現存在死亡通知時,那么久向其對應的BpBinder端發送死亡通知消息。

5 總結

在請求服務(getService)的過程,當執行到binder_transaction()時,會區分請求服務所屬進程情況。

  • 當請求服務的進程與服務屬于不同進程,則為請求服務所在進程創binder_ref對象,指向服務進程的binder_noder
  • 當請求服務的進程與服務屬于同一進程, 則不再創建新對象,只是引用計數+1,并且修改type為BINDER_TYPE_BINDER或BINDER_TYPE_WEAK_BINDER。
  • 最終readStrongBinder(),返回的是BB對象的真實子類

參考

Android跨進程通信IPC之9——Binder之Framework層C++篇2

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

推薦閱讀更多精彩內容