LeakCanary 內(nèi)存泄露監(jiān)測(cè)原理研究

"Read the fucking source code" -- linus一句名言體現(xiàn)出了閱讀源碼的重要性,學(xué)習(xí)別人得代碼是提升自己的重要途徑覆糟。最近用到了LeakCanary,順便看一下其代碼,學(xué)習(xí)一下。
LeakCanary是安卓中用來(lái)檢測(cè)內(nèi)存泄露的小工具掸绞,它能幫助我們提早發(fā)現(xiàn)代碼中隱藏的bug, 降低應(yīng)用中內(nèi)存泄露以及OOM產(chǎn)生的概率。

廢話不多說(shuō)耕捞,關(guān)于LeakCanary的使用方法衔掸,其實(shí)很簡(jiǎn)單,如果我們只想檢測(cè)Activity的內(nèi)存泄露俺抽,而且只想使用其默認(rèn)的報(bào)告方式具篇,我們只需要在Application中加一行代碼,

LeakCanary.install(this);

那我們今天閱讀源碼的切入點(diǎn)凌埂,就從這個(gè)靜態(tài)方法開(kāi)始驱显。

 /**
   * Creates a {@link RefWatcher} that works out of the box, and starts watching activity
   * references (on ICS+).
   */
  public static RefWatcher install(Application application) {
    return install(application, DisplayLeakService.class,
        AndroidExcludedRefs.createAppDefaults().build());
  }

這個(gè)函數(shù)內(nèi)部直接調(diào)用了另外一個(gè)重載的函數(shù)

/**
   * Creates a {@link RefWatcher} that reports results to the provided service, and starts watching
   * activity references (on ICS+).
   */
  public static RefWatcher install(Application application,
      Class<? extends AbstractAnalysisResultService> listenerServiceClass,
      ExcludedRefs excludedRefs) {
    //判斷是否在Analyzer進(jìn)程里
    if (isInAnalyzerProcess(application)) {
      return RefWatcher.DISABLED;
    }
    enableDisplayLeakActivity(application);
    HeapDump.Listener heapDumpListener =
        new ServiceHeapDumpListener(application, listenerServiceClass);
    RefWatcher refWatcher = androidWatcher(application, heapDumpListener, excludedRefs);
    ActivityRefWatcher.installOnIcsPlus(application, refWatcher);
    return refWatcher;
  }

因?yàn)閘eakcanay會(huì)開(kāi)啟一個(gè)遠(yuǎn)程service用來(lái)分析每次產(chǎn)生的內(nèi)存泄露,而安卓的應(yīng)用每次開(kāi)啟進(jìn)程都會(huì)調(diào)用Applicaiton的onCreate方法瞳抓,因此我們有必要預(yù)先判斷此次Application啟動(dòng)是不是在analyze service啟動(dòng)時(shí)埃疫,

public static boolean isInServiceProcess(Context context, Class<? extends Service> serviceClass) {
    PackageManager packageManager = context.getPackageManager();
    PackageInfo packageInfo;
    try {
      packageInfo = packageManager.getPackageInfo(context.getPackageName(), GET_SERVICES);
    } catch (Exception e) {
      Log.e("AndroidUtils", "Could not get package info for " + context.getPackageName(), e);
      return false;
    }
    String mainProcess = packageInfo.applicationInfo.processName;

    ComponentName component = new ComponentName(context, serviceClass);
    ServiceInfo serviceInfo;
    try {
      serviceInfo = packageManager.getServiceInfo(component, 0);
    } catch (PackageManager.NameNotFoundException ignored) {
      // Service is disabled.
      return false;
    }

    if (serviceInfo.processName.equals(mainProcess)) {
      Log.e("AndroidUtils",
          "Did not expect service " + serviceClass + " to run in main process " + mainProcess);
      // Technically we are in the service process, but we're not in the service dedicated process.
      return false;
    }

    //查找當(dāng)前進(jìn)程名
    int myPid = android.os.Process.myPid();
    ActivityManager activityManager =
        (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
    ActivityManager.RunningAppProcessInfo myProcess = null;
    for (ActivityManager.RunningAppProcessInfo process : activityManager.getRunningAppProcesses()) {
      if (process.pid == myPid) {
        myProcess = process;
        break;
      }
    }
    if (myProcess == null) {
      Log.e("AndroidUtils", "Could not find running process for " + myPid);
      return false;
    }

    return myProcess.processName.equals(serviceInfo.processName);
  }

判斷Application是否是在service進(jìn)程里面啟動(dòng),最直接的方法就是判斷當(dāng)前進(jìn)程名和service所屬的進(jìn)程是否相同孩哑。當(dāng)前進(jìn)程名的獲取方式是使用ActivityManager的getRunningAppProcessInfo方法栓霜,找到進(jìn)程pid與當(dāng)前進(jìn)程pid相同的進(jìn)程,然后從中拿到processName. service所屬進(jìn)程名横蜒。獲取service應(yīng)處進(jìn)程的方法是用PackageManager的getPackageInfo方法胳蛮。

RefWatcher

ReftWatcher是leakcancay檢測(cè)內(nèi)存泄露的發(fā)起點(diǎn)。使用方法為丛晌,在對(duì)象生命周期即將結(jié)束的時(shí)候仅炊,調(diào)用

RefWatcher.watch(Object object)

為了達(dá)到檢測(cè)內(nèi)存泄露的目的,RefWatcher需要

  private final Executor watchExecutor;
  private final DebuggerControl debuggerControl;
  private final GcTrigger gcTrigger;
  private final HeapDumper heapDumper;
  private final Set<String> retainedKeys;
  private final ReferenceQueue<Object> queue;
  private final HeapDump.Listener heapdumpListener;
  private final ExcludedRefs excludedRefs;
  • watchExecutor: 執(zhí)行內(nèi)存泄露檢測(cè)的executor
  • debuggerControl :用于查詢(xún)是否正在調(diào)試中澎蛛,調(diào)試中不會(huì)執(zhí)行內(nèi)存泄露檢測(cè)
  • queue : 用于判斷弱引用所持有的對(duì)象是否已被GC抚垄。
  • gcTrigger: 用于在判斷內(nèi)存泄露之前,再給一次GC的機(jī)會(huì)
  • headDumper: 用于在產(chǎn)生內(nèi)存泄露室執(zhí)行dump 內(nèi)存heap
  • heapdumpListener: 用于分析前面產(chǎn)生的dump文件谋逻,找到內(nèi)存泄露的原因
  • excludedRefs: 用于排除某些系統(tǒng)bug導(dǎo)致的內(nèi)存泄露
  • retainedKeys: 持有那些呆檢測(cè)以及產(chǎn)生內(nèi)存泄露的引用的key呆馁。

接下來(lái),我們來(lái)看看watch函數(shù)背后是如何利用這些工具毁兆,生成內(nèi)存泄露分析報(bào)告的浙滤。

public void watch(Object watchedReference, String referenceName) {
    checkNotNull(watchedReference, "watchedReference");
    checkNotNull(referenceName, "referenceName");
    //如果處于debug模式,則直接返回
    if (debuggerControl.isDebuggerAttached()) {
      return;
    }
    //記住開(kāi)始觀測(cè)的時(shí)間
    final long watchStartNanoTime = System.nanoTime();
    //生成一個(gè)隨機(jī)的key气堕,并加入set中
    String key = UUID.randomUUID().toString();
    retainedKeys.add(key);
    //生成一個(gè)KeyedWeakReference
    final KeyedWeakReference reference =
        new KeyedWeakReference(watchedReference, key, referenceName, queue);
    //調(diào)用watchExecutor纺腊,執(zhí)行內(nèi)存泄露的檢測(cè)
    watchExecutor.execute(new Runnable() {
      @Override public void run() {
        ensureGone(reference, watchStartNanoTime);
      }
    });
  }

所以最后的核心函數(shù)是在ensureGone這個(gè)runnable里面畔咧。要理解其工作原理,就得從keyedWeakReference說(shuō)起

WeakReference與ReferenceQueue

從watch函數(shù)中摹菠,可以看到盒卸,每次檢測(cè)對(duì)象內(nèi)存是否泄露時(shí)骗爆,我們都會(huì)生成一個(gè)KeyedReferenceQueue次氨,這個(gè)類(lèi)其實(shí)就是一個(gè)WeakReference,只不過(guò)其額外附帶了一個(gè)key和一個(gè)name

final class KeyedWeakReference extends WeakReference<Object> {
  public final String key;
  public final String name;

  KeyedWeakReference(Object referent, String key, String name,
      ReferenceQueue<Object> referenceQueue) {
    super(checkNotNull(referent, "referent"), checkNotNull(referenceQueue, "referenceQueue"));
    this.key = checkNotNull(key, "key");
    this.name = checkNotNull(name, "name");
  }
}

在構(gòu)造時(shí)我們需要傳入一個(gè)ReferenceQueue摘投,這個(gè)ReferenceQueue是直接傳入了WeakReference中煮寡,關(guān)于這個(gè)類(lèi),有興趣的可以直接看Reference的源碼犀呼。我們這里需要知道的是幸撕,每次WeakReference所指向的對(duì)象被GC后,這個(gè)弱引用都會(huì)被放入這個(gè)與之相關(guān)聯(lián)的ReferenceQueue隊(duì)列中外臂。

我們這里可以貼下其核心代碼

private static class ReferenceHandler extends Thread {

        ReferenceHandler(ThreadGroup g, String name) {
            super(g, name);
        }

        public void run() {
            for (;;) {
                Reference<Object> r;
                synchronized (lock) {
                    if (pending != null) {
                        r = pending;
                        pending = r.discovered;
                        r.discovered = null;
                    } else {
                        //....
                        try {
                            try {
                                lock.wait();
                            } catch (OutOfMemoryError x) { }
                        } catch (InterruptedException x) { }
                        continue;
                    }
                }

                // Fast path for cleaners
                if (r instanceof Cleaner) {
                    ((Cleaner)r).clean();
                    continue;
                }

                ReferenceQueue<Object> q = r.queue;
                if (q != ReferenceQueue.NULL) q.enqueue(r);
            }
        }
    }

    static {
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        for (ThreadGroup tgn = tg;
             tgn != null;
             tg = tgn, tgn = tg.getParent());
        Thread handler = new ReferenceHandler(tg, "Reference Handler");
        /* If there were a special system-only priority greater than
         * MAX_PRIORITY, it would be used here
         */
        handler.setPriority(Thread.MAX_PRIORITY);
        handler.setDaemon(true);
        handler.start();
    }

在reference類(lèi)加載的時(shí)候坐儿,java虛擬機(jī)會(huì)創(chuàng)建一個(gè)最大優(yōu)先級(jí)的后臺(tái)線程,這個(gè)線程的工作原理就是不斷檢測(cè)pending是否為null宋光,如果不為null貌矿,就將其放入ReferenceQueue中,pending不為null的情況就是罪佳,引用所指向的對(duì)象已被GC逛漫,變?yōu)椴豢蛇_(dá)。

那么只要我們?cè)跇?gòu)造弱引用的時(shí)候指定了ReferenceQueue赘艳,每當(dāng)弱引用所指向的對(duì)象被內(nèi)存回收的時(shí)候酌毡,我們就可以在queue中找到這個(gè)引用。如果我們期望一個(gè)對(duì)象被回收蕾管,那如果在接下來(lái)的預(yù)期時(shí)間之后枷踏,我們發(fā)現(xiàn)它依然沒(méi)有出現(xiàn)在ReferenceQueue中,那就可以判定它的內(nèi)存泄露了掰曾。LeakCanary檢測(cè)內(nèi)存泄露的核心原理就在這里呕寝。

其實(shí)Java里面的WeakHashMap里也用到了這種方法,來(lái)判斷hash表里的某個(gè)鍵值是否還有效婴梧。在構(gòu)造WeakReference的時(shí)候給其指定了ReferenceQueue.

監(jiān)測(cè)時(shí)機(jī)

什么時(shí)候去檢測(cè)能判定內(nèi)存泄露呢下梢?這個(gè)可以看AndroidWatchExecutor的實(shí)現(xiàn)


public final class AndroidWatchExecutor implements Executor {

    //....
    
    private void executeDelayedAfterIdleUnsafe(final Runnable runnable) {
        // This needs to be called from the main thread.
        Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
          @Override public boolean queueIdle() {
            backgroundHandler.postDelayed(runnable, DELAY_MILLIS);
            return false;
          }
        });
      }
  }

這里又看到一個(gè)比較少的用法,IdleHandler塞蹭,IdleHandler的原理就是在messageQueue因?yàn)榭臻e等待消息時(shí)給使用者一個(gè)hook孽江。那AndroidWatchExecutor會(huì)在主線程空閑的時(shí)候,派發(fā)一個(gè)后臺(tái)任務(wù)番电,這個(gè)后臺(tái)任務(wù)會(huì)在DELAY_MILLIS時(shí)間之后執(zhí)行岗屏。LeakCanary設(shè)置的是5秒辆琅。

二次確認(rèn)保證內(nèi)存泄露準(zhǔn)確性

為了避免因?yàn)間c不及時(shí)帶來(lái)的誤判,leakcanay會(huì)進(jìn)行二次確認(rèn)進(jìn)行保證这刷。

void ensureGone(KeyedWeakReference reference, long watchStartNanoTime) {
    long gcStartNanoTime = System.nanoTime();
    //計(jì)算從調(diào)用watch到進(jìn)行檢測(cè)的時(shí)間段
    long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
    //根據(jù)queue移除已被GC的對(duì)象的弱引用
    removeWeaklyReachableReferences();
    //如果內(nèi)存已被回收或者處于debug模式婉烟,直接返回
    if (gone(reference) || debuggerControl.isDebuggerAttached()) {
      return;
    }
    //如果內(nèi)存依舊沒(méi)被釋放,則再給一次gc的機(jī)會(huì)
    gcTrigger.runGc();
    //再次移除
    removeWeaklyReachableReferences();
    if (!gone(reference)) {
      //走到這里暇屋,認(rèn)為內(nèi)存確實(shí)泄露了
      long startDumpHeap = System.nanoTime();
      long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
      //dump出heap報(bào)告
      File heapDumpFile = heapDumper.dumpHeap();

      if (heapDumpFile == HeapDumper.NO_DUMP) {
        // Could not dump the heap, abort.
        return;
      }
      long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
      heapdumpListener.analyze(
          new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs,
              gcDurationMs, heapDumpDurationMs));
    }
  }

  private boolean gone(KeyedWeakReference reference) {
    return !retainedKeys.contains(reference.key);
  }

  private void removeWeaklyReachableReferences() {
    // WeakReferences are enqueued as soon as the object to which they point to becomes weakly
    // reachable. This is before finalization or garbage collection has actually happened.
    KeyedWeakReference ref;
    while ((ref = (KeyedWeakReference) queue.poll()) != null) {
      retainedKeys.remove(ref.key);
    }
  }

Dump Heap

監(jiān)測(cè)到內(nèi)存泄露后似袁,首先做的就是dump出當(dāng)前的heap,默認(rèn)的AndroidHeapDumper調(diào)用的是

Debug.dumpHprofData(filePath);

到處當(dāng)前內(nèi)存的hprof分析文件咐刨,一般我們?cè)贒eviceMonitor中也可以dump出hprof文件昙衅,然后將其從dalvik格式轉(zhuǎn)成標(biāo)準(zhǔn)jvm格式,然后使用MAT進(jìn)行分析定鸟。

那么LeakCanary是如何分析內(nèi)存泄露的呢而涉?

HaHa

LeakCanary 分析內(nèi)存泄露用到了一個(gè)和Mat類(lèi)似的工具叫做HaHa,使用HaHa的方法如下:

public AnalysisResult checkForLeak(File heapDumpFile, String referenceKey) {
    long analysisStartNanoTime = System.nanoTime();

    if (!heapDumpFile.exists()) {
      Exception exception = new IllegalArgumentException("File does not exist: " + heapDumpFile);
      return failure(exception, since(analysisStartNanoTime));
    }

    try {
      HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
      HprofParser parser = new HprofParser(buffer);
      Snapshot snapshot = parser.parse();

      Instance leakingRef = findLeakingReference(referenceKey, snapshot);

      // False alarm, weak reference was cleared in between key check and heap dump.
      if (leakingRef == null) {
        return noLeak(since(analysisStartNanoTime));
      }

      return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef);
    } catch (Throwable e) {
      return failure(e, since(analysisStartNanoTime));
    }
  }

關(guān)于HaHa的原理联予,感興趣的同學(xué)可以深究啼县,這里就不深入介紹了。

返回的ActivityResult對(duì)象中包含了對(duì)象到GC root的最短路徑沸久。LeakCanary在dump出hprof文件后季眷,會(huì)啟動(dòng)一個(gè)IntentService進(jìn)行分析:HeapAnalyzerService在分析出結(jié)果之后會(huì)啟動(dòng)DisplayLeakService用來(lái)發(fā)起Notification 以及將結(jié)果記錄下來(lái)寫(xiě)在文件里面。以后每次啟動(dòng)LeakAnalyzerActivity就從文件里讀取歷史結(jié)果麦向。

ExcludedRef

由于某些系統(tǒng)的bug瘟裸,以及某些廠商rom的bug,Activity在finish之后仍然會(huì)被某些系統(tǒng)組件給hold住诵竭。LeakCanary列出了一些很常見(jiàn)的话告,比如三星的手機(jī)activity會(huì)被audioManager給hold住,試了一下huawei的系統(tǒng)貌似也會(huì)出現(xiàn)卵慰,還有比如activity中如果有會(huì)獲取鍵盤(pán)焦點(diǎn)的view沙郭,在activity finish之后view會(huì)被InputMethodManager給hold住,因?yàn)関iew會(huì)持有activity 造成activity泄漏裳朋,除非有新的view獲取鍵盤(pán)焦點(diǎn)病线。

LeakCanary中有一個(gè)AndroidExcludedRefs枚舉類(lèi),其中枚舉了很多特定版本系統(tǒng)issue引起的內(nèi)存泄漏鲤嫡,因?yàn)檫@種問(wèn)題 不是開(kāi)發(fā)者導(dǎo)致的送挑,因此HeapAnalyzerService在分析內(nèi)存泄露時(shí),會(huì)將這些GC Root排除在外暖眼。而且每個(gè)ExcludedRef通常都跟特定廠商或者Android版本有關(guān)惕耕,這些枚舉類(lèi)都加了一個(gè)適用條件。

AndroidExcludedRefs(boolean applies) {  this.applies = applies;}


   AUDIO_MANAGER__MCONTEXT_STATIC(SAMSUNG.equals(MANUFACTURER) && SDK_INT == KITKAT) {
    @Override void add(ExcludedRefs.Builder excluded) {
      // Samsung added a static mContext_static field to AudioManager, holds a reference to the
      // activity.
      // Observed here: https://github.com/square/leakcanary/issues/32
      excluded.staticField("android.media.AudioManager", "mContext_static");
    }
  },

比如上面這個(gè)AudioManager引起的問(wèn)題诫肠,只有在Build中的MANUFACTURER表明是三星以及sdk版本是KITKAT(4.4, 19)時(shí)才適用司澎。

手動(dòng)釋放資源

然后并不是leakCanary不報(bào)錯(cuò)我們就不用管欺缘,activity內(nèi)存泄露了,大部分情況下沒(méi)多大事挤安,但是有些占用內(nèi)存很多的頁(yè)面谚殊,比如圖庫(kù),webview頁(yè)面蛤铜,因?yàn)閍citivity不能回收嫩絮,它所指向的view以及view下面的bitmap都不能被回收,這是會(huì)造成很不好的后果的昂羡,很可能會(huì)導(dǎo)致OOM絮记,因此我們需要手動(dòng)在Activity結(jié)束時(shí)回收資源摔踱。

Under 4.0 & Fragment

LeakCanary只支持4.0以上虐先,原因是其中在watch 每個(gè)Activity時(shí)適用了Application的registerActivityLifecycleCallback函數(shù),這個(gè)函數(shù)只在4.0上才支持派敷,但是在4.0以下也是可以用的蛹批,可以在Application中將返回的RefWatcher存下來(lái),然后在基類(lèi)Activity的onDestroy函數(shù)中調(diào)用篮愉。

同理腐芍,如果我們想檢測(cè)Fragment的內(nèi)存的話,我們也闊以在Fragment的onDestroy中watch它试躏。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末猪勇,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子颠蕴,更是在濱河造成了極大的恐慌泣刹,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件犀被,死亡現(xiàn)場(chǎng)離奇詭異椅您,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)寡键,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)掀泳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人西轩,你說(shuō)我怎么就攤上這事员舵。” “怎么了藕畔?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵马僻,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我劫流,道長(zhǎng)巫玻,這世上最難降的妖魔是什么丛忆? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮仍秤,結(jié)果婚禮上熄诡,老公的妹妹穿的比我還像新娘。我一直安慰自己诗力,他們只是感情好凰浮,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著苇本,像睡著了一般袜茧。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上瓣窄,一...
    開(kāi)封第一講書(shū)人閱讀 49,007評(píng)論 1 284
  • 那天笛厦,我揣著相機(jī)與錄音,去河邊找鬼俺夕。 笑死裳凸,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的劝贸。 我是一名探鬼主播姨谷,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼映九!你這毒婦竟也來(lái)了梦湘?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤件甥,失蹤者是張志新(化名)和其女友劉穎捌议,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體嚼蚀,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡禁灼,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了轿曙。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片弄捕。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖导帝,靈堂內(nèi)的尸體忽然破棺而出守谓,到底是詐尸還是另有隱情,我是刑警寧澤您单,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布斋荞,位于F島的核電站,受9級(jí)特大地震影響虐秦,放射性物質(zhì)發(fā)生泄漏平酿。R本人自食惡果不足惜凤优,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蜈彼。 院中可真熱鬧筑辨,春花似錦、人聲如沸幸逆。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)还绘。三九已至楚昭,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間拍顷,已是汗流浹背抚太。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留菇怀,地道東北人凭舶。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓晌块,卻偏偏與公主長(zhǎng)得像爱沟,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子匆背,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

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