Android性能優化(五)--更快--穩定--響應速度ANR

1 ANR簡介

  • ANR,是“Application Not Responding”的縮寫,即“應用程序無響應”。
  • 在Android中,ActivityManagerService(簡稱AMS)和WindowManagerService(簡稱WMS)會監測應用程序的響應時間,如果應用程序主線程(即UI線程)在超時時間內對輸入事件沒有處理完畢(或沒有處理),或者對特定操作沒有執行完畢,就會出現ANR。
  • 對于輸入事件沒有處理完畢產生的ANR,Android會顯示一個對話框,提示用戶當前應用程序沒有響應,用戶可以選擇繼續等待或者關閉這個應用程序(也就是殺掉這個應用程序的進程)。

2 為什么會有ANR

ANR的產生需要同時滿足三個條件:

  • 主線程:只有應用程序進程的主線程響應超時才會產生ANR;
  • 超時時間:產生ANR的上下文不同,超時時間也會不同,但只要在這個時間上限內沒有響應就會ANR;
  • 輸入事件/特定操作:輸入事件是指按鍵、觸屏等設備輸入事件,特定操作是指BroadcastReceiver和Service的生命周期中的各個函數,產生ANR的上下文不同,導致ANR的原因也會不同;

3 三種情況會觸發ANR

3.1 主線程對輸入事件在5秒內沒有處理完畢

  • Android的事件系統從2.3開始做了完全不同的實現,原先2.2中是在Java層實現的,但在2.3中整體轉移到了C++層,本書基于2.3以后的版本進行說明。我們先簡單了解一下產生這種ANR的整個流程。
  • 當應用程序的Window處于Active狀態并且能夠接收輸入事件(例如按鍵事件、觸摸事件等)時,系統底層上報的事件就會被InputDispatcher分發給這個應用程序,應用程序的主線程通過InputChannel讀取輸入事件并交給界面視圖處理,界面視圖是一個樹狀結構,DecorView是視圖樹的根,事件從樹根開始一層一層向端點(例如一個Button)傳遞。
  • 我們通常會注冊一個監聽器來接收并處理事件,或者創建自定義的視圖控件來處理事件。
  • InputDispatcher運行在系統進程(進程名為system_server)的一個單獨的線程中,應用程序的主線程在處理事件的過程中,InputDispatcher會不斷的檢測處理過程是否超時,一旦超時,會通過一系列的回調通知WMS的notifyANR函數,最終會調用到AMS中mHandler對象里的SHOW_NOT_RESPONDING_MSG這個case,此時界面上就顯示系統提示對話框了,同時使用logcat命令查看log(日志信息)也可以看到關于ANR的信息。
  • Window:具體指的是PhoneWindow對象,表示一個能夠顯示的窗口,它能夠接收系統分發的各種輸入事件;
  • InputDispatcher:將系統上報的輸入事件分發給當前活動的窗口;
  • InputChannel:InputDispatcher和應用程序分別運行在兩個不同的進程中,InputDispatcher就是通過InputChannel將事件對象傳遞給應用進程的。
  • 注意:產生這種ANR的前提是要有輸入事件,如果用戶沒有觸發任何輸入事件,即便是主線程阻塞了,也不會產生ANR,因為InputDispatcher沒有分發事件給應用程序,當然也不會檢測處理超時和報告ANR了。

3.2 主線程在執行BroadcastReceiver的onReceive函數時10秒內沒有執行完畢

  • BroadcastReceiver(簡稱BR)的onReceive函數運行在主線程中,當這個函數超過10秒鐘沒有返回就會觸發ANR。不過對這種情況的ANR系統不會顯示對話框提示,僅是輸出log而已。

3.3 主線程在執行Service的各個生命周期函數時20秒內沒有執行完畢

  • Service的各個生命周期函數也運行在主線程中,當這些函數超過20秒鐘沒有返回就會觸發ANR。同樣對對這種情況的ANR系統不會顯示對話框提示,僅是輸出log而已。

3.4 小結

  • 1.三種ANR中只有第1種會顯示系統提示對話框,因為用戶正在做界面交互操作,如果長時間沒有任何響應,會讓用戶懷疑設備死機了,大多數人此時會開始亂按,甚至拔出電池重啟,給用戶的體驗肯定是非常糟糕的。
  • 2.三種ANR發生時都會在log中輸出錯誤信息,你會發現各個應用進程和系統進程的函數堆棧信息都輸出到了一個/data/anr/traces.txt的文件中,這個文件是分析ANR原因的關鍵文件,同時在日志中還會看到當時的CPU使用率,這也是重要信息,可以利用它們分析ANR問題。
  • 3.這三種ANR不是孤立的,有可能會相互影響。例如一個應用程序進程中同時有一個正在顯示的Activity和一個正在處理消息的BroadcastReceiver,它們都運行在這個進程的主線程中。如果BR的onReceive函數沒有返回,此時用戶點擊屏幕,而onReceive超過5秒仍然沒有返回,主線程無法處理用戶輸入事件,就會引起第1種ANR。如果繼續超過10秒沒有返回,又會引起第2種ANR。

4 traces.txt--ANR分析

4.1 獲取ANR產生的trace文件

ANR產生時, 系統會生成一個traces.txt的文件放在/data/anr/下. 可以通過adb命令將其導出到本地:

$adb pull data/anr/traces.txt .

4.2 分析traces.txt

4.2.1 普通阻塞導致的ANR

強行sleep thread產生的一個ANR
獲取到的tracs.txt文件一般如下:

----- pid 2976 at 2016-09-08 23:02:47 -----
Cmd line: com.anly.githubapp  // 最新的ANR發生的進程(包名)

...

DALVIK THREADS (41):
"main" prio=5 tid=1 Sleeping
  | group="main" sCount=1 dsCount=0 obj=0x73467fa8 self=0x7fbf66c95000
  | sysTid=2976 nice=0 cgrp=default sched=0/0 handle=0x7fbf6a8953e0
  | state=S schedstat=( 0 0 0 ) utm=60 stm=37 core=1 HZ=100
  | stack=0x7ffff4ffd000-0x7ffff4fff000 stackSize=8MB
  | held mutexes=
  at java.lang.Thread.sleep!(Native method)
  - sleeping on <0x35fc9e33> (a java.lang.Object)
  at java.lang.Thread.sleep(Thread.java:1031)
  - locked <0x35fc9e33> (a java.lang.Object)
  at java.lang.Thread.sleep(Thread.java:985) // 主線程中sleep過長時間, 阻塞導致無響應.
  at com.tencent.bugly.crashreport.crash.c.l(BUGLY:258)
  - locked <@addr=0x12dadc70> (a com.tencent.bugly.crashreport.crash.c)
  at com.tencent.bugly.crashreport.CrashReport.testANRCrash(BUGLY:166)  // 產生ANR的那個函數調用
  - locked <@addr=0x12d1e840> (a java.lang.Class<com.tencent.bugly.crashreport.CrashReport>)
  at com.anly.githubapp.common.wrapper.CrashHelper.testAnr(CrashHelper.java:23)
  at com.anly.githubapp.ui.module.main.MineFragment.onClick(MineFragment.java:80) // ANR的起點
  at com.anly.githubapp.ui.module.main.MineFragment_ViewBinding$2.doClick(MineFragment_ViewBinding.java:47)
  at butterknife.internal.DebouncingOnClickListener.onClick(DebouncingOnClickListener.java:22)
  at android.view.View.performClick(View.java:4780)
  at android.view.View$PerformClick.run(View.java:19866)
  at android.os.Handler.handleCallback(Handler.java:739)
  at android.os.Handler.dispatchMessage(Handler.java:95)
  at android.os.Looper.loop(Looper.java:135)
  at android.app.ActivityThread.main(ActivityThread.java:5254)
  at java.lang.reflect.Method.invoke!(Native method)
  at java.lang.reflect.Method.invoke(Method.java:372)
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:698)

如上trace信息中的添加的中文注釋已基本說明了trace文件該怎么分析:

  • 1.文件最上的即為最新產生的ANR的trace信息.
  • 2.前面兩行表明ANR發生的進程pid, 時間, 以及進程名字(包名).
  • 3.尋找我們的代碼點, 然后往前推, 看方法調用棧, 追溯到問題產生的根源.

4.2.2 CPU滿負荷

race信息可能會包含這樣的信息:

Process:com.anly.githubapp
...
CPU usage from 3330ms to 814ms ago:
6% 178/system_server: 3.5% user + 1.4% kernel / faults: 86 minor 20 major
4.6% 2976/com.anly.githubapp: 0.7% user + 3.7% kernel /faults: 52 minor 19 major
0.9% 252/com.android.systemui: 0.9% user + 0% kernel
...

100%TOTAL: 5.9% user + 4.1% kernel + 89% iowait
  • 最后一句表明了:
  • 1.當是CPU占用100%, 滿負荷了.
  • 2.其中絕大數是被iowait即I/O操作占用了.

4.2.3 內存原因

  • 其實內存原因有可能會導致ANR,
    例如如果由于內存泄露, App可使用內存所剩無幾, 我們點擊按鈕啟動一個大圖片作為背景的activity, 就可能會產生ANR, 這時trace信息可能是這樣的:
// 以下trace信息來自網絡, 用來做個示例
Cmdline: android.process.acore

DALVIK THREADS:
"main"prio=5 tid=3 VMWAIT
|group="main" sCount=1 dsCount=0 s=N obj=0x40026240self=0xbda8
| sysTid=1815 nice=0 sched=0/0 cgrp=unknownhandle=-1344001376
atdalvik.system.VMRuntime.trackExternalAllocation(NativeMethod)
atandroid.graphics.Bitmap.nativeCreate(Native Method)
atandroid.graphics.Bitmap.createBitmap(Bitmap.java:468)
atandroid.view.View.buildDrawingCache(View.java:6324)
atandroid.view.View.getDrawingCache(View.java:6178)

...

MEMINFO in pid 1360 [android.process.acore] **
native dalvik other total
size: 17036 23111 N/A 40147
allocated: 16484 20675 N/A 37159
free: 296 2436 N/A 2732

可以看到free的內存已所剩無幾
當然這種情況可能更多的是會產生OOM的異常...

5 ANR的避免和檢測

5.1 切記不要在主線程中作耗時操作

針對三種不同的情況, 一般的處理情況如下

  • 1.主線程阻塞的
    開辟單獨的子線程來處理耗時阻塞事務.
  • 2.CPU滿負荷, I/O阻塞的
    I/O阻塞一般來說就是文件讀寫或數據庫操作執行在主線程了, 也可以通過開辟子線程的方式異步執行.
  • 3.內存不夠用的
    增大VM內存, 使用largeHeap屬性, 排查內存泄露。

5.2 借助于一些工具來進行檢測,從而更有效的避免ANR的引入。

5.2.1 StrictMode

嚴格模式StrictMode是Android SDK提供的一個用來檢測代碼中是否存在違規操作的工具類,StrictMode主要檢測兩大類問題。

  • 線程策略 ThreadPolicy
    1.detectCustomSlowCalls:檢測自定義耗時操作
    2.detectDiskReads:檢測是否存在磁盤讀取操作
    3.detectDiskWrites:檢測是否存在磁盤寫入操作
    4.detectNetWork:檢測是否存在網絡操作
  • 虛擬機策略VmPolicy
    1.detectActivityLeaks:檢測是否存在Activity泄露
    2.detectLeakedClosableObjects:檢測是否存在未關閉的Closeable對象泄露
    3.detectLeakedSqlLiteObjects:檢測是否存在Sqlite對象泄露
    4.setClassInstanceLimit:檢測類實例個數是否超過限制
    Debug版本中使用
    ThreadPolicy可以用來檢測可能催在的主線程耗時操作,需要注意的是我們只能在Debug版本中使用它,發布到市場上的版本要關閉掉。StrictMode的使用很簡單,我們只需要在應用初始化的地方例如Application或者MainActivity類的onCreate方法中執行如下代碼:
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);

        // 開啟線程模式
        StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                .detectAll()//調用detectAll方法表示啟動所有的檢測策略
                .penaltyLog()
                .penaltyDialog() ////打印logcat,當然也可以定位到dropbox,通過文件保存相應的log
                .build());
        // 開啟虛擬機模式
        StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                .detectAll()//調用detectAll方法表示啟動所有的檢測策略
                .penaltyLog()//調用penaltyLog表示在Logcat中打印日志
                .build());
    }

也可以根據應用的具體要求只開啟某些策略

         StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                 .detectDiskReads()
                 .detectDiskWrites()
                 .detectNetwork()  
                 .penaltyLog()
                 .build());

         StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                 .detectLeakedSqlLiteObjects()
                 .detectLeakedClosableObjects()
                 .detectActivityLeaks()
                 .penaltyLog()
                 .build());

5.2.2 BlockCanary監控應用主線程的卡頓

  • BlockCanary是一個非侵入式式的性能監控函數庫,它的用法和leakCanary類似,只不過后者監控應用的內存泄露,而BlockCanary主要用來監控應用主線程的卡頓。
  • 它的基本原理是利用主線程的消息隊列處理機制,通過對比消息分發開始和結束的時間點來判斷是否超過設定的時間,如果是,則判斷為主線程卡頓。
  • 它的集成很簡單,首先在build.gradle中添加依賴一般選取以下其中一個 case 引入即可
dependencies {
    compile 'com.github.markzhai:blockcanary-android:1.5.0'

    // 僅在debug包啟用BlockCanary進行卡頓監控和提示的話,可以這么用
    debugCompile 'com.github.markzhai:blockcanary-android:1.5.0'
    releaseCompile 'com.github.markzhai:blockcanary-no-op:1.5.0'
}

然后在Application類中進行配置和初始化即可

public class AnrDemoApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        // 在主進程初始化調用哈
        BlockCanary.install(this, new AppBlockCanaryContext()).start();

    }
}

實現自己監控的上下文

public class AppBlockCanaryContext extends BlockCanaryContext {
    // 實現各種上下文,包括應用標示符,用戶uid,網絡類型,卡慢判斷闕值,Log保存位置等

    /**
     * Implement in your project.
     *
     * @return Qualifier which can specify this installation, like version + flavor.
     */
    public String provideQualifier() {
        return "unknown";
    }


    /**
     * Implement in your project.
     *
     * @return user id
     */
    public String provideUid() {
        return "uid";
    }

    /**
     * Network type
     *網絡類型
     * @return {@link String} like 2G, 3G, 4G, wifi, etc.
     */
    public String provideNetworkType() {
        return "unknown";
    }

    /**
     * Config monitor duration, after this time BlockCanary will stop, use
     * with {@code BlockCanary}'s isMonitorDurationEnd
     * 配置監視器持續時間,此時BlockCanary將停止,并使用{@Code BlockCanary}的isMonitorDurationEnd
     *
     * @return monitor last duration (in hour)
     */
    public int provideMonitorDuration() {
        return -1;
    }

    /**
     * Config block threshold (in millis), dispatch over this duration is regarded as a BLOCK. You may set it
     * from performance of device.
     * 配置塊閾值(以毫為單位),在此持續時間內的調度被視為BLOCK。 您可以根據設備的性能進行設置。
     * @return threshold in mills
     */
    public int provideBlockThreshold() {
        return 1000;
    }

    /**
     * Thread stack dump interval, use when block happens, BlockCanary will dump on main thread
     * stack according to current sample cycle.
     * <p>
     * Because the implementation mechanism of Looper, real dump interval would be longer than
     * the period specified here (especially when cpu is busier).
     * </p>
     *
     * @return dump interval (in millis)
     */
    public int provideDumpInterval() {
        return provideBlockThreshold();
    }

    /**
     * Path to save log, like "/blockcanary/", will save to sdcard if can.
     *
     * @return path of log files
     * 日志文件的路徑
     */
    public String providePath() {
        return "/blockcanary/";
    }

    /**
     * If need notification to notice block.
     * 是否需要通知
     * @return true if need, else if not need.
     */
    public boolean displayNotification() {
        return true;
    }

    /**
     * Implement in your project, bundle files into a zip file.
     * 將文件捆綁到一個zip文件中。
     * @param src  files before compress
     * @param dest files compressed
     * @return true if compression is successful
     */
    public boolean zip(File[] src, File dest) {
        return false;
    }

    /**
     * Implement in your project, bundled log files.
     *
     * @param zippedFile zipped file
     */
    public void upload(File zippedFile) {
        throw new UnsupportedOperationException();
    }


    /**
     * Packages that developer concern, by default it uses process name,
     * put high priority one in pre-order.
     * 開發人員關注的軟件包,默認情況下它使用進程名稱,并在預訂中放置高優先級的軟件包。
     * @return null if simply concern only package with process name.
     * 如果僅僅關注包含進程名稱的包,則返回null。
     */
    public List<String> concernPackages() {
        return null;
    }

    /**
     * Filter stack without any in concern package, used with @{code concernPackages}.
     * 無需關注包的過濾器堆棧,與@ {code concernPackages}一起使用。
     * @return true if filter, false it not.
     */
    public boolean filterNonConcernStack() {
        return false;
    }

    /**
     * Provide white list, entry in white list will not be shown in ui list.
     * 提供白名單,白名單中的條目不會顯示在用戶名單中。
     * @return return null if you don't need white-list filter.
     */
    public List<String> provideWhiteList() {
        LinkedList<String> whiteList = new LinkedList<>();
        whiteList.add("org.chromium");
        return whiteList;
    }

    /**
     * Whether to delete files whose stack is in white list, used with white-list.
     * 是否刪除堆棧位于白名單中的文件,與白名單一起使用。
     * @return true if delete, false it not.
     */
    public boolean deleteFilesInWhiteList() {
        return true;
    }

    /**
     * Block interceptor, developer may provide their own actions.
     * Block攔截器,開發者可以提供自己的操作。
     */
    public void onBlock(Context context, BlockInfo blockInfo) {

    }
}

6 使用子線程的方式

6.1 啟Thread方式

這個其實也是Java實現多線程的方式. 有兩種實現方法, 繼承Thread 或 實現Runnable接口:

繼承Thread

class PrimeThread extends Thread {
    long minPrime;
    PrimeThread(long minPrime) {
        this.minPrime = minPrime;
    }

    public void run() {
        // compute primes larger than minPrime
         . . .
    }
}

PrimeThread p = new PrimeThread(143);
p.start();

實現Runnable接口

class PrimeRun implements Runnable {
    long minPrime;
    PrimeRun(long minPrime) {
        this.minPrime = minPrime;
    }

    public void run() {
        // compute primes larger than minPrime
         . . .
    }
}

PrimeRun p = new PrimeRun(143);
new Thread(p).start();

6.2 使用AsyncTask

這個是Android特有的方式, AsyncTask顧名思義, 就是異步任務的意思.

private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
    // Do the long-running work in here
    // 執行在子線程
    protected Long doInBackground(URL... urls) {
        int count = urls.length;
        long totalSize = 0;
        for (int i = 0; i < count; i++) {
            totalSize += Downloader.downloadFile(urls[i]);
            publishProgress((int) ((i / (float) count) * 100));
            // Escape early if cancel() is called
            if (isCancelled()) break;
        }
        return totalSize;
    }

    // This is called each time you call publishProgress()
    // 執行在主線程
    protected void onProgressUpdate(Integer... progress) {
        setProgressPercent(progress[0]);
    }

    // This is called when doInBackground() is finished
    // 執行在主線程
    protected void onPostExecute(Long result) {
        showNotification("Downloaded " + result + " bytes");
    }
}

// 啟動方式
new DownloadFilesTask().execute(url1, url2, url3);

6.3 HandlerThread

Android中結合Handler和Thread的一種方式. 前面有云, 默認情況下Handler的handleMessage是執行在主線程的, 但是如果我給這個Handler傳入了子線程的looper, handleMessage就會執行在這個子線程中的. HandlerThread正是這樣的一個結合體:

// 啟動一個名為new_thread的子線程
HandlerThread thread = new HandlerThread("new_thread");
thread.start();

// 取new_thread賦值給ServiceHandler
private ServiceHandler mServiceHandler;
mServiceLooper = thread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper);

private final class ServiceHandler extends Handler {
    public ServiceHandler(Looper looper) {
      super(looper);
    }
    
    @Override
    public void handleMessage(Message msg) {
      // 此時handleMessage是運行在new_thread這個子線程中了.
    }
}

6.4 IntentService

Service是運行在主線程的, 然而IntentService是運行在子線程的.
實際上IntentService就是實現了一個HandlerThread + ServiceHandler的模式.

6.5 Loader

Android 3.0引入的數據加載器, 可以在Activity/Fragment中使用. 支持異步加載數據, 并可監控數據源在數據發生變化時傳遞新結果. 常用的有CursorLoader, 用來加載數據庫數據.

// 創建一個Loader
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
    // This is called when a new Loader needs to be created.  This
    // sample only has one Loader, so we don't care about the ID.
    // First, pick the base URI to use depending on whether we are
    // currently filtering.
    Uri baseUri;
    if (mCurFilter != null) {
        baseUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI,
                  Uri.encode(mCurFilter));
    } else {
        baseUri = Contacts.CONTENT_URI;
    }

    // Now create and return a CursorLoader that will take care of
    // creating a Cursor for the data being displayed.
    String select = "((" + Contacts.DISPLAY_NAME + " NOTNULL) AND ("
            + Contacts.HAS_PHONE_NUMBER + "=1) AND ("
            + Contacts.DISPLAY_NAME + " != '' ))";
    return new CursorLoader(getActivity(), baseUri,
            CONTACTS_SUMMARY_PROJECTION, select, null,
            Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC");
}

// 加載完成
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
    // Swap the new cursor in.  (The framework will take care of closing the
    // old cursor once we return.)
    mAdapter.swapCursor(data);
}

6.6 特別注意

使用Thread和HandlerThread時, 為了使效果更好, 建議設置Thread的優先級偏低一點:

Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND);

因為如果沒有做任何優先級設置的話, 你創建的Thread默認和UI Thread是具有同樣的優先級的, 你懂的. 同樣的優先級的Thread, CPU調度上還是可能會阻塞掉你的UI Thread, 導致ANR的.

參考

http://www.lxweimin.com/p/c8c15608c68e

http://www.lxweimin.com/p/6d855e984b99

https://blog.csdn.net/sinat_36668731/article/details/74924432

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

推薦閱讀更多精彩內容