Android進程管理三部曲[3]-內存的回收

作者: 強波 (阿里云OS平臺部-Cloud Engine)
博客: http://qiangbo.space/

本文是Android系統進程管理的第三篇文章。進程管理的前面兩篇文章,請參見這里:

本文適合Android平臺的應用程序開發者,也適合對于Android系統內部實現感興趣的讀者。

前言

內存是系統中非常寶貴的資源,即便如今的移動設備上,內存已經達到4G甚至6G的級別,但對于內存的回收也依然重要,因為在Android系統上,同時運行的進程有可能會有幾十甚至上百個之多。

如何將系統內存合理的分配給每個進程,以及如何進行內存回收,便是操作系統需要處理的問題之一。

本文會講解Android系統中內存回收相關的知識。

對于內存回收,主要可以分為兩個層次:

  • 進程內的內存回收:通過釋放進程中的資源進行內存回收
  • 進程級的內存回收:通過殺死進程來進行內存回收

這其中,進程內的內存回收主要分為兩個方面:

  • 虛擬機自身的垃圾回收機制
  • 在系統內存狀態發生變化時,通知應用程序,讓開發者進行內存回收

進程級的內存回收主要是依靠系統中的兩個模塊,它們是:

  • Linux OOM Killer
  • LowMemoryKiller

在特定場景下,他們都會通過殺死進程來進行內存回收。

Android系統的內存管理簡介

在Android系統中,進程可以大致分為系統進程應用進程兩大類。

系統進程是系統內置的(例如:initzygotesystem_server進程),屬于操作系統必不可少的一部分。系統進程的作用在于:

  • 管理硬件設備
  • 提供訪問設備的基本能力
  • 管理應用進程

應用進程是指應用程序運行的進程。這些應用程序可能是系統出廠自帶的(例如Launcher,電話,短信等應用),也可能是用戶自己安裝的(例如:微信,支付寶等)。

Android中應用進程通常都運行在Java虛擬機中。在Android 5.0之前的版本,這個虛擬機是Dalvik,5.0及之后版本,Android引入了新的虛擬機,稱作Android Runtime,簡稱“ART”。

關于ART和Dalvik可以參見這里:ART and Dalvik。無論是Dalvik還是ART,本身都具有垃圾回收的能力,關于這一點,我們在后面專門講解。

Android的應用程序都會依賴一些公共的資源,例如:Android SDK提供的類和接口,以及Framework公開的圖片,字符串等。為了達到節省內存的目的,這些資源在內存中并不是每個應用進程單獨一份拷貝。而是會在所有應用之間共享,因為所有應用進程都是作為Zygote進程fork出來的子進程。關于這部分內容,我們已經在Android系統中的進程管理:進程的創建一文中講解過。

在Java語言中,通過new創建的對象都會在堆中分配內存。應用程序堆的大小是有限的。系統會根據設備的物理內存大小來確定每個應用程序所允許使用的內存大小,一旦應用程序使用的內存超過這個大小,便會發生OutOfMemoryError

因此開發者需要關心應用的內存使用狀況。關于如何監測應用程序的內存使用,可以參見這里:Investigating Your RAM Usage

開發者相關的API

下面是一些與內存相關的開發者API,它們是Android SDK的一部分。

ComponentCallbacks2

Android系統會根據當前的系統內存狀態和應用的自身狀態對應用進行通知。這種通知的目的是希望應用能夠感知到系統和自身的狀態變化,以便開發者可以更準確的把握應用的運行。

例如:在系統內存充足時,為了提升響應性能,應用可以緩存更多的資源。但是當系統內存緊張時,開發者應當釋放一定的資源來緩解內存緊張的狀態。

ComponentCallbacks2接口中的void onTrimMemory(int level) 回調函數用來接收這個通知。關于這一點,在“開發者的內存回收”一節,我們會詳細講解。

ActivityManager

ActivityManager,從名稱中就可以看出,這個類是用來管理Activity的系統服務。但這個類中也包含了很多運行時狀態查詢的接口,這其中就包括與內存相關的幾個:

  • int getMemoryClass () 獲取當前設備上,單個應用的內存大小限制,單位是M。注意,這個函數的返回值只是一個大致的值。
  • void getMemoryInfo (ActivityManager.MemoryInfo outInfo) 獲取系統的內存信息,具體結構可以查看ActivityManager.MemoryInfo,開發者最關心的可能就是availMem以及totalMem
  • void getMyMemoryState (ActivityManager.RunningAppProcessInfo outState) 獲取調用進程的內存信息
  • MemoryInfo[] getProcessMemoryInfo (int[] pids) 通過pid獲取指定進程的內存信息
  • boolean isLowRamDevice() 查詢當前設備是否是低內存設備

Runtime

Java應用程序都會有一個Runtime接口的實例,通過這個實例可以查詢運行時的一些狀態,與內存相關的接口有:

  • freeMemory() 獲取Java虛擬機的剩余內存
  • maxMemory() 獲取Java虛擬機所能使用的最大內存
  • totalMemory() 獲取Java虛擬機擁有的最大內存

虛擬機的垃圾回收

垃圾回收是指:虛擬機會監測應用程序的對象創建和使用,并在一些特定的時候銷毀無用的對象以回收內存。

垃圾回收的基本想法是要找出虛擬機中哪些對象已經不會再被使用然后將其釋放。其最常用的算法有下面兩種:

引用計數算法

引用計數算法是為每個對象維護一個被引用的次數:對象剛創建時的初始引用計數為0,每次被一個對象引用時,引用計數加1,反之減1。當一個對象的引用計數重新回到0時便可以認為是不會被使用的,這些對象便可以被垃圾回收。

讀者可能馬上會想到,當有兩個對象互相引用時,這時引用計數該如何計算。關于這部分內容,這里不再展開講解。有興趣的讀者可以查詢Google或者維基百科:Garbage collection

對象追蹤算法

對象追蹤算法是通過GC root類型的對象為起點,追蹤所有被這些對象所引用的對象,并順著這些被引用的對象繼續往下追蹤,在追蹤的過程中,對所有被追蹤到的對象打上標記。

而剩下的那些沒有被打過標記的對象便可以認為是沒有被使用的,因此這些對象可以將其釋放。

這里提到的的GC root類型的對象有四類:

  • 棧中的local變量,即方法中的局部變量
  • 活動的線程(例如主線程或者開發者創建的線程)
  • static變量
  • JNI中的引用

下面這幅圖描述了這種算法:


a)表示算法開始時,所有對象的標記為false,然后以GC root為起點開始追蹤和打標記,b)中被追蹤到的對象打上了標記。剩下的沒有打上標記的對象便可以釋放了。算法結束之后,c)中將所有對象的標記全部置為false。下一輪計算時,重新以GC root開始追蹤。

Dalvik虛擬機主要用的就是垃圾回收算法,這里是其Source:MarkSweep.cpp

開發者的內存回收

內存回收并不是僅僅是系統的事情,作為開發者,也需要在合適的場合下進行內存釋放。無節制的消耗內存將導致應用程序OutOfMemoryError

上文中提到,虛擬機的垃圾回收會回收那些不會再被使用到的對象。因此,開發者所需要做的就是:當確定某些對象不會再被使用時,要主動釋放對其引用,這樣虛擬機才能將其回收。對于不再被用到對象,仍然保持對其引用導致其無法釋放,將導致內存泄漏的發生。

為了更好的進行內存回收,系統會一些場景下會通知應用,希望應用能夠配合進行一些內存的釋放。

ComponentCallbacks2接口中的 void onTrimMemory(int level)回調就是用來接收這個事件的。

Activity, Service, ContentProviderApplication都實現了這個接口,因此這些類的子類都可以接收這個事件。

onTrimMemory回調的參數是一個級別,系統會根據應用本身的狀態以及系統的內存狀態發送不同的級別,具體的包括:

  • 應用處于Runnig狀態可能收到的級別

    • TRIM_MEMORY_RUNNING_MODERATE 表示系統內存已經稍低
    • TRIM_MEMORY_RUNNING_LOW 表示系統內存已經相當低
    • TRIM_MEMORY_RUNNING_CRITICAL 表示系統內存已經非常低,你的應用程序應當考慮釋放部分資源
  • 應用的可見性發生變化時收到的級別

    • TRIM_MEMORY_UI_HIDDEN 表示應用已經處于不可見狀態,可以考慮釋放一些與顯示相關的資源
  • 應用處于后臺時可能收到的級別

    • TRIM_MEMORY_BACKGROUND 表示系統內存稍低,你的應用被殺的可能性不大。但可以考慮適當釋放資源
    • TRIM_MEMORY_MODERATE 表示系統內存已經較低,當內存持續減少,你的應用可能會被殺死
    • TRIM_MEMORY_COMPLETE 表示系統內存已經非常低,你的應用即將被殺死,請釋放所有可能釋放的資源

這里是這個方法實現的示例代碼:Release memory in response to events

在前面的文章中我們提到過:ActivityManagerService負責管理所有的應用進程。

而這里的通知也是來自ActivityManagerService。在updateOomAdjLocked的時候,ActivityManagerService會根據系統內存以及應用的狀態通過app.thread.scheduleTrimMemory發送通知給應用程序。

這里的appProcessRecord,即描述應用進程的對象,thread是應用的主線程。而scheduleTrimMemory是通過Binder IPC的方式將消息發送到應用進程上。這些內容在前面的文章中已經介紹過,如果覺得陌生,可以閱讀一下前面兩篇文章。

ActivityThread中(這個是應用程序的主線程),接受到這個通知之后,便會遍歷應用進程中所有能接受這個通知的組件,然后逐個回調通知。

相關代碼如下:

final void handleTrimMemory(int level) {
   if (DEBUG_MEMORY_TRIM) Slog.v(TAG, "Trimming memory to level: " + level);

   ArrayList<ComponentCallbacks2> callbacks = collectComponentCallbacks(true, null);

   final int N = callbacks.size();
   for (int i = 0; i < N; i++) {
       callbacks.get(i).onTrimMemory(level);
   }

   WindowManagerGlobal.getInstance().trimMemory(level);
}

Linux OOM Killer

前面提到的機制都是在進程內部通過釋放對象來進行內存回收。

而實際上,系統中運行的進程數量,以及每個進程所消耗的內存都是不確定的。

在極端的情況下,系統的內存可能處于非常嚴峻的狀態,假設這個時候所有進程都不愿意釋放內存,系統將會卡死。

為了使系統能夠繼續運轉不至于卡死,系統會嘗試殺死一些不重要的進程來進行內存回收,這其中涉及的模塊主要是:Linux OOM Killer和LowMemoryKiller。

Linux OOM Killer是Linux內核的一部分,其源碼可以在這里查看:/mm/oom_kill.c

Linux OOM Killer的基本想法是:

當系統已經沒法再分配內存的時候,內核會遍歷所有的進程,對每個進程計算badness值,得分(badness)最高的進程將會被殺死

即:badness得分越低表示進程越重要,反之表示不重要。

Linux OOM Killer的執行流程如下:

_alloc_pages -> out_of_memory() -> select_bad_process() -> oom_badness()

這其中,_alloc_pages 是內核在分配內存時調用的函數。當內核發現無法再分配內存時,便會計算每個進程的badness值,然后選擇最大的(系統認為最不重要的)將其殺死。

那么,內核是如何計算進程的badness值的呢?請看下面的代碼:

unsigned long oom_badness(struct task_struct *p, struct mem_cgroup *memcg,
              const nodemask_t *nodemask, unsigned long totalpages)
{
    long points;
    long adj;

    ...

    points = get_mm_rss(p->mm) + p->mm->nr_ptes + get_mm_counter(p->mm, MM_SWAPENTS);
    task_unlock(p);

    if (has_capability_noaudit(p, CAP_SYS_ADMIN))
        points -= (points * 3) / 100;

    adj *= totalpages / 1000;
    points += adj;

    return points > 0 ? points : 1;
}

從這段代碼中,我們可以看到,影響進程badness值的因素主要有三個:

  • 進程的oom_score_adj
  • 進程的內存占用大小
  • 進程是否是root用戶的進程

即,oom_score_adj(關于oom_score_adj,在Android系統中的進程管理:進程的優先級一文中我們專門講解過。)值越小,進程占用的內存越小,并且如果是root用戶的進程,系統就認為這個進程越重要。

反之則被認為越不重要,越容易被殺死。

LowMemoryKiller

OOM Killer是在系統內存使用情況非常嚴峻的時候才會起作用。但直到這個時候才開始殺死進程來回收內存是有點晚的。因為在進程被殺死之前,其他進程都無法再申請內存了。

因此,Google在Android上新增了一個LowMemoryKiller模塊。LowMemoryKiller通常會在Linux OOM Killer工作之前,就開始殺死進程。

LowMemoryKiller的做法是:

提供6個可以設置的內存級別,當系統內存每低于一個級別時,將oom_score_adj大于某個指定值的進程全部殺死。

這么說會有些抽象,但具體看一下LowMemoryKiller的配置文件我們就好理解了。

LowMemoryKiller在sysfs上暴露了兩個文件來供系統調整參數,這兩個文件的路徑是:

  • /sys/module/lowmemorykiller/parameters/minfree
  • /sys/module/lowmemorykiller/parameters/adj

如果你手上有一個Android設備,你可以通過adb shell連上去之后,通過cat命令查看這兩個文件的內容。

這兩個文件是配對使用的,每個文件中都是由逗號分隔的6個整數值。

在某個設備上,這兩個文件的值可能分別是下面這樣:

  • 18432,23040,27648,32256,55296,80640
  • 0,100,200,300,900,906

這組配置的含義是;當系統內存低于80640k時,將oom_score_adj值大于906的進程全部殺死;當系統內存低于55296k時,將oom_score_adj值大于900的進程全部殺死,其他類推。

LowMemoryKiller殺死進程的時候會在內核留下日志,你可以通過dmesg
命令中看到。這個日志可能是這樣的:

lowmemorykiller: Killing 'gnunet-service-' (service adj 0,
to free 327224kB on behalf of 'kswapd0' (21) because
cache 6064kB is below limit 6144kB for oom_score_adj 0

從這個日志中,我們可以看到被殺死進程的名稱,進程pid和oom_score_adj值。另外還有系統在殺死這個進程之前系統內存還剩多少,以及殺死這個進程釋放了多少。

LowMemoryKiller的源碼也在內核中,路徑是:kernel/drivers/staging/android/lowmemorykiller.c

lowmemorykiller.c中定義了如下幾個函數:

  • lowmem_shrink
  • lowmem_init
  • lowmem_exit
  • lowmem_oom_adj_to_oom_score_adj
  • lowmem_autodetect_oom_adj_values
  • lowmem_adj_array_set
  • lowmem_adj_array_get
  • lowmem_adj_array_free

LowMemoryKiller本身是一個內核驅動程序的形式存在,lowmem_initlowmem_exit
分別負責模塊的初始化和退出清理工作。

lowmem_init函數中,就是通過register_shrinker向內核中注冊了register_shrinker 函數:

static int __init lowmem_init(void)
{
    register_shrinker(&lowmem_shrinker);
    return 0;
}

register_shrinker函數就是LowMemoryKiller的算法核心,這個函數的代碼和說明如下:

static int lowmem_shrink(struct shrinker *s, struct shrink_control *sc)
{
    struct task_struct *tsk;
    struct task_struct *selected = NULL;
    int rem = 0;
    int tasksize;
    int i;
    short min_score_adj = OOM_SCORE_ADJ_MAX + 1;
    int minfree = 0;
    int selected_tasksize = 0;
    short selected_oom_score_adj;
    int array_size = ARRAY_SIZE(lowmem_adj);
    int other_free = global_page_state(NR_FREE_PAGES) - totalreserve_pages;
    int other_file = global_page_state(NR_FILE_PAGES) -
                        global_page_state(NR_SHMEM) -
                        total_swapcache_pages();
   
    if (lowmem_adj_size < array_size)
        array_size = lowmem_adj_size;
    if (lowmem_minfree_size < array_size)
        array_size = lowmem_minfree_size;
    // lowmem_minfree 和lowmem_adj 記錄了兩個配置文件中配置的數據
    for (i = 0; i < array_size; i++) {
        minfree = lowmem_minfree[i];
        // 確定當前系統處于低內存的第幾檔
        if (other_free < minfree && other_file < minfree) {
           // 確定需要殺死的進程的oom_score_adj的上限
            min_score_adj = lowmem_adj[i];
            break;
        }
    }
    if (sc->nr_to_scan > 0)
        lowmem_print(3, "lowmem_shrink %lu, %x, ofree %d %d, ma %hd\n",
                sc->nr_to_scan, sc->gfp_mask, other_free,
                other_file, min_score_adj);
    rem = global_page_state(NR_ACTIVE_ANON) +
        global_page_state(NR_ACTIVE_FILE) +
        global_page_state(NR_INACTIVE_ANON) +
        global_page_state(NR_INACTIVE_FILE);
    if (sc->nr_to_scan <= 0 || min_score_adj == OOM_SCORE_ADJ_MAX + 1) {
        lowmem_print(5, "lowmem_shrink %lu, %x, return %d\n",
                 sc->nr_to_scan, sc->gfp_mask, rem);
        return rem;
    }
    selected_oom_score_adj = min_score_adj;

    rcu_read_lock();
    // 遍歷所有進程
    for_each_process(tsk) {
        struct task_struct *p;
        short oom_score_adj;

        if (tsk->flags & PF_KTHREAD)
            continue;

        p = find_lock_task_mm(tsk);
        if (!p)
            continue;

        if (test_tsk_thread_flag(p, TIF_MEMDIE) &&
            time_before_eq(jiffies, lowmem_deathpending_timeout)) {
            task_unlock(p);
            rcu_read_unlock();
            return 0;
        }
        oom_score_adj = p->signal->oom_score_adj;
        // 跳過那些oom_score_adj值比目標值小的
        if (oom_score_adj < min_score_adj) {
            task_unlock(p);
            continue;
        }
        tasksize = get_mm_rss(p->mm);
        task_unlock(p);
        if (tasksize <= 0)
            continue;
        // selected 是將要殺死的備選進程
        if (selected) {
           // 跳過那些oom_score_adj比備選的小的
            if (oom_score_adj < selected_oom_score_adj)
                continue;
           // 如果oom_score_adj一樣,跳過那些內存消耗更小的
            if (oom_score_adj == selected_oom_score_adj &&
                tasksize <= selected_tasksize)
                continue;
        }
        // 更換備選的目標,因為又發現了一個oom_score_adj更大,
        // 或者內存消耗更大的進程
        selected = p;
        selected_tasksize = tasksize;
        selected_oom_score_adj = oom_score_adj;
        lowmem_print(2, "select '%s' (%d), adj %hd, size %d, to kill\n",
                 p->comm, p->pid, oom_score_adj, tasksize);
    }
    
    // 已經選中目標,記錄日志并殺死進程
    if (selected) {
        long cache_size = other_file * (long)(PAGE_SIZE / 1024);
        long cache_limit = minfree * (long)(PAGE_SIZE / 1024);
        long free = other_free * (long)(PAGE_SIZE / 1024);
        trace_lowmemory_kill(selected, cache_size, cache_limit, free);
        lowmem_print(1, "Killing '%s' (%d), adj %hd,\n" \
                "   to free %ldkB on behalf of '%s' (%d) because\n" \
                "   cache %ldkB is below limit %ldkB for oom_score_adj %hd\n" \
                "   Free memory is %ldkB above reserved\n",
                 selected->comm, selected->pid,
                 selected_oom_score_adj,
                 selected_tasksize * (long)(PAGE_SIZE / 1024),
                 current->comm, current->pid,
                 cache_size, cache_limit,
                 min_score_adj,
                 free);

        lowmem_deathpending_timeout = jiffies + HZ;
        set_tsk_thread_flag(selected, TIF_MEMDIE);
        send_sig(SIGKILL, selected, 0);
        rem -= selected_tasksize;
    }
    lowmem_print(4, "lowmem_shrink %lu, %x, return %d\n",
             sc->nr_to_scan, sc->gfp_mask, rem);
    rcu_read_unlock();
    return rem;
}

進程的死亡處理

在任何時候,應用進程都可能死亡,例如被OOM Killer或者LowMemoryKiller殺死,自身crash死亡又或者被用戶手動殺死。無論哪種情況,作為應用進程的管理者ActivityManagerService都需要知道。

在應用進程死亡之后,ActivityManagerService需要執行如下工作:

  • 執行清理工作 ActivityManagerService內部的ProcessRecord以及可能存在的四大組件的相關結構需要全部清理干凈
  • 重新計算進程的優先級 上文已經提到過,進程的優先級是有關聯性的,有其中一個進程死亡了,可能會連到影響到其他進程的優先級需要調整。

ActivityManagerService是利用Binder提供的死亡通知機制來進行進程的死亡處理的。關于Binder請參閱其他資料,限于篇幅關系,這里不再展開講解。

簡單來說,死亡通知機制就提供了進程間的一種死亡監聽的能力:當目標進程死亡的時候,監聽回調會執行。

ActivityManagerService中的AppDeathRecipient監聽了應用進程的死亡消息,該類代碼如下:

private final class AppDeathRecipient implements IBinder.DeathRecipient {
   final ProcessRecord mApp;
   final int mPid;
   final IApplicationThread mAppThread;

   AppDeathRecipient(ProcessRecord app, int pid,
           IApplicationThread thread) {
       mApp = app;
       mPid = pid;
       mAppThread = thread;
   }

   @Override
   public void binderDied() {
       synchronized(ActivityManagerService.this) {
           appDiedLocked(mApp, mPid, mAppThread, true);
       }
   }
}

每一個應用進程在啟動之后,都會attach到ActivityManagerService上通知它自己的進程已經啟動完成了。這時ActivityManagerService便會為其創建一個死亡通知的監聽器。在這之后如果進程死亡了,ActivityManagerService便會收到通知。

private final boolean attachApplicationLocked(IApplicationThread thread,
       int pid) {
    ...
        try {
            AppDeathRecipient adr = new AppDeathRecipient(
                    app, pid, thread);
            thread.asBinder().linkToDeath(adr, 0);
            app.deathRecipient = adr;
        } catch (RemoteException e) {
            app.resetPackageList(mProcessStats);
            startProcessLocked(app, "link fail", processName);
            return false;
        }
    ...
}

進程死亡之后的處理工作是appDiedLocked這個方法中處理的,這部分還是比較容易理解的,這里就不過多講解了。

結束語

這三篇文章,我們詳細講解了Android系統中進程的創建,優先級的管理和內存回收。這些內容對于所有運行在Android系統中的應用進程都是適用的。

優秀的開發者應該充分了解這些內容,因為這是與應用的生命周期密切相關的。

由于篇幅所限,這其中有些知識我們沒有詳細展開討論,但有些內容會在今后的文章中專門講解。

由于筆者水平有限,文章中不免有所錯漏,歡迎讀者指出。

參考資料與推薦讀物

Overview of Android Memory Management

Understanding Java Garbage Collection

Processes and Threads

Java Memory Management

Debugging ART Garbage Collection

Android Runtime

Taming the OOM killer

OOM Killer

Out Of Memory Management

更多文章請關注公眾號

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

推薦閱讀更多精彩內容