blockcanary源碼學(xué)習(xí)隨筆

blockcanary是什么撩荣?

blockcanary是國(guó)內(nèi)開(kāi)發(fā)者MarkZhai開(kāi)發(fā)的一套性能監(jiān)控組件,它對(duì)主線程操作進(jìn)行了完全透明的監(jiān)控灸叼,并能輸出有效的信息汉额,幫助開(kāi)發(fā)分析、定位到問(wèn)題所在被冒,迅速優(yōu)化應(yīng)用

下圖為官方原理介紹示例圖:

image.png

簡(jiǎn)介

Github地址:blockcanary

特點(diǎn)

  • 非侵入式
  • 使用簡(jiǎn)單
  • 實(shí)時(shí)監(jiān)控
  • 提供完善的堆棧及內(nèi)存信息

Android渲染機(jī)制

Android系統(tǒng)每隔16ms發(fā)出VSYNC信號(hào)军掂,觸發(fā)對(duì)UI進(jìn)行渲染, 如果每次渲染都成功昨悼,這樣就能夠達(dá)到流暢的畫(huà)面所需要的60fps蝗锥,為了能夠?qū)崿F(xiàn)60fps,這意味著程序的大多數(shù)操作都必須在16ms內(nèi)完成率触。如果超過(guò)了16ms那么可能就出現(xiàn)丟幀的情況终议。

本文主要對(duì)blockcanary的原理進(jìn)行分析,關(guān)于渲染的詳細(xì)機(jī)制及優(yōu)化,推薦參考如下文章:

Android性能優(yōu)化-渲染優(yōu)化

blockcanary怎么用穴张?

1细燎、gradle引入庫(kù)

 debugImplementation 'com.github.markzhai:blockcanary-android:1.5.0'
 releaseImplementation 'com.github.markzhai:blockcanary-no-op:1.5.0'
 

2、自定義Application并且在onCreate中進(jìn)行初始化

public class ExampleApplication extends Application {

    @Override public void onCreate() {
        super.onCreate();
        BlockCanary.install(this, new BlockCanaryContext()).start();
    }
}

blockcanary核心執(zhí)行流程是怎樣皂甘?

blockcanary的核心原理是通過(guò)自定義一個(gè)Printer玻驻,設(shè)置到主線程ActivityThread的MainLooper中。MainLooper在dispatch消息前后都會(huì)調(diào)用Printer進(jìn)行打印偿枕。從而獲取前后執(zhí)行的時(shí)間差值击狮,判斷是否超過(guò)設(shè)置的閾值。如果超過(guò)益老,則會(huì)將記錄的棧信息及cpu信息發(fā)通知到前臺(tái)彪蓬。

關(guān)鍵類功能說(shuō)明

說(shuō)明
BlockCanary 外觀類,提供初始化及開(kāi)始捺萌、停止監(jiān)聽(tīng)
BlockCanaryContext 配置上下文档冬,可配置id、當(dāng)前網(wǎng)絡(luò)信息桃纯、卡頓閾值酷誓、log保存路徑等
BlockCanaryInternals blockcanary核心的調(diào)度類,內(nèi)部包含了monitor(設(shè)置到MainLooper的printer)态坦、stackSampler(棧信息處理器)盐数、cpuSampler(cpu信息處理器)、mInterceptorChain(注冊(cè)的攔截器)伞梯、以及onBlockEvent的回調(diào)及攔截器的分發(fā)
LooperMonitor 繼承了Printer接口玫氢,用于設(shè)置到MainLooper中。通過(guò)復(fù)寫(xiě)println的方法來(lái)獲取MainLooper的dispatch前后的執(zhí)行時(shí)間差谜诫,并控制stackSampler和cpuSampler的信息采集漾峡。
StackSampler 用于獲取線程的棧信息,將采集的棧信息存儲(chǔ)到一個(gè)以key為時(shí)間戳的LinkHashMap中喻旷。通過(guò)mCurrentThread.getStackTrace()獲取當(dāng)前線程的StackTraceElement
CpuSampler 用于獲取cpu信息生逸,將采集的cpu信息存儲(chǔ)到一個(gè)以key為時(shí)間戳的LinkHashMap中。通過(guò)讀取/proc/stat文件獲取cpu的信息
DisplayService 繼承了BlockInterceptor攔截器且预,onBlock回調(diào)會(huì)觸發(fā)發(fā)送前臺(tái)通知
DisplayActivity 用于顯示記錄的異常信息Activity

代碼執(zhí)行流程

leakcanary的核心流程主要包含3個(gè)步驟槽袄。

1、init-初始化

2锋谐、monitor-監(jiān)聽(tīng)MainLooper的dispatch時(shí)間差遍尺,推送前臺(tái)通知

3、dump-采集線程棧信息及cpu信息

這里先上一下整體的流程圖怀估,建議結(jié)合源碼進(jìn)行查看狮鸭。

image

下面我們通過(guò)上述3個(gè)步驟相關(guān)的源碼來(lái)進(jìn)行分析。

1多搀、init

根據(jù)Application中的使用歧蕉,我們首先看install方法

  public static BlockCanary install(Context context, BlockCanaryContext blockCanaryContext) {
        //BlockCanaryContext.init會(huì)將保存應(yīng)用的applicationContext和用戶設(shè)置的配置參數(shù)
        BlockCanaryContext.init(context, blockCanaryContext);
        //etEnabled將根據(jù)用戶的通知欄消息配置開(kāi)啟
        setEnabled(context, DisplayActivity.class, BlockCanaryContext.get().displayNotification());
        return get();
    }
    

接著看get方法的實(shí)現(xiàn)如下:

    //使用單例創(chuàng)建了一個(gè)BlockCanary對(duì)象
    public static BlockCanary get() {
        if (sInstance == null) {
            synchronized (BlockCanary.class) {
                if (sInstance == null) {
                    sInstance = new BlockCanary();
                }
            }
        }
        return sInstance;
    }

接著我們看BlockCanary的對(duì)象的構(gòu)造方法實(shí)現(xiàn)如下:

private BlockCanary() {
        //初始化lockCanaryInternals調(diào)度類
        BlockCanaryInternals.setContext(BlockCanaryContext.get());
        mBlockCanaryCore = BlockCanaryInternals.getInstance();
        //為BlockCanaryInternals添加攔截器(責(zé)任鏈)BlockCanaryContext對(duì)BlockInterceptor是空實(shí)現(xiàn)
        mBlockCanaryCore.addBlockInterceptor(BlockCanaryContext.get());
        if (!BlockCanaryContext.get().displayNotification()) {
            return;
        }
        //DisplayService只在開(kāi)啟通知欄消息的時(shí)候添加,當(dāng)卡頓發(fā)生時(shí)將通過(guò)DisplayService發(fā)起通知欄消息
        mBlockCanaryCore.addBlockInterceptor(new DisplayService());

    }

接著我們看BlockCanaryInternals的構(gòu)造方法康铭,實(shí)現(xiàn)如下:

public BlockCanaryInternals() {
        //初始化棧采集器
        stackSampler = new StackSampler(
                Looper.getMainLooper().getThread(),
                sContext.provideDumpInterval());
        //初始化cpu采集器
        cpuSampler = new CpuSampler(sContext.provideDumpInterval());

        //初始化LooperMonitor惯退,并實(shí)現(xiàn)了onBlockEvent的回調(diào),該回調(diào)會(huì)在觸發(fā)閾值后被調(diào)用
        setMonitor(new LooperMonitor(new LooperMonitor.BlockListener() {

            @Override
            public void onBlockEvent(long realTimeStart, long realTimeEnd,
                                     long threadTimeStart, long threadTimeEnd) {
                ArrayList<String> threadStackEntries = stackSampler
                        .getThreadStackEntries(realTimeStart, realTimeEnd);
                if (!threadStackEntries.isEmpty()) {
                    BlockInfo blockInfo = BlockInfo.newInstance()
                            .setMainThreadTimeCost(realTimeStart, realTimeEnd, threadTimeStart, threadTimeEnd)
                            .setCpuBusyFlag(cpuSampler.isCpuBusy(realTimeStart, realTimeEnd))
                            .setRecentCpuRate(cpuSampler.getCpuRateInfo())
                            .setThreadStackEntries(threadStackEntries)
                            .flushString();
                    LogWriter.save(blockInfo.toString());

                    if (mInterceptorChain.size() != 0) {
                        for (BlockInterceptor interceptor : mInterceptorChain) {
                            interceptor.onBlock(getContext().provideContext(), blockInfo);
                        }
                    }
                }
            }
        }, getContext().provideBlockThreshold(), getContext().stopWhenDebugging()));

        LogWriter.cleanObsolete();
    }

2从藤、monitor

首先我們先看下系統(tǒng)的Looper的loop()方法中對(duì)于printer的使用催跪,如下:

   for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }

            // 執(zhí)行dispatchMessage前,執(zhí)行Printer的println方法
            final Printer logging = me.mLogging;
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }

            final long traceTag = me.mTraceTag;
            long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
            long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;
            if (thresholdOverride > 0) {
                slowDispatchThresholdMs = thresholdOverride;
                slowDeliveryThresholdMs = thresholdOverride;
            }
            final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0);
            final boolean logSlowDispatch = (slowDispatchThresholdMs > 0);

            final boolean needStartTime = logSlowDelivery || logSlowDispatch;
            final boolean needEndTime = logSlowDispatch;

            if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
                Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
            }

            final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
            final long dispatchEnd;
            try {
                msg.target.dispatchMessage(msg);
                dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
            if (logSlowDelivery) {
                if (slowDeliveryDetected) {
                    if ((dispatchStart - msg.when) <= 10) {
                        Slog.w(TAG, "Drained");
                        slowDeliveryDetected = false;
                    }
                } else {
                    if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery",
                            msg)) {
                        // Once we write a slow delivery log, suppress until the queue drains.
                        slowDeliveryDetected = true;
                    }
                }
            }
            if (logSlowDispatch) {
                showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", msg);
            }
          // 執(zhí)行dispatchMessage后夷野,執(zhí)行Printer的println方法
            if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }

            // Make sure that during the course of dispatching the
            // identity of the thread wasn't corrupted.
            final long newIdent = Binder.clearCallingIdentity();
            if (ident != newIdent) {
                Log.wtf(TAG, "Thread identity changed from 0x"
                        + Long.toHexString(ident) + " to 0x"
                        + Long.toHexString(newIdent) + " while dispatching to "
                        + msg.target.getClass().getName() + " "
                        + msg.callback + " what=" + msg.what);
            }

            msg.recycleUnchecked();
        }

當(dāng)install進(jìn)行初始化完成后懊蒸,接著會(huì)調(diào)用start()方法,實(shí)現(xiàn)如下:

  public void start() {
        if (!mMonitorStarted) {
            mMonitorStarted = true;
            //把mBlockCanaryCore中的monitor設(shè)置MainLooper中進(jìn)行監(jiān)聽(tīng)
            Looper.getMainLooper().setMessageLogging(mBlockCanaryCore.monitor);
        }
    }

當(dāng)MainLooper執(zhí)行dispatch的前后會(huì)調(diào)用printer的println方法悯搔,所以這里我們看LooperMonitor對(duì)println方法的實(shí)現(xiàn)如下:

 @Override
    public void println(String x) {
        //如果再debug模式骑丸,不執(zhí)行監(jiān)聽(tīng)
        if (mStopWhenDebugging && Debug.isDebuggerConnected()) {
            return;
        }
        if (!mPrintingStarted) {//dispatchMesage前執(zhí)行的println
            //記錄開(kāi)始時(shí)間
            mStartTimestamp = System.currentTimeMillis();
            mStartThreadTimestamp = SystemClock.currentThreadTimeMillis();
            mPrintingStarted = true;
            //開(kāi)始采集棧及cpu信息
            startDump();
        } else {//dispatchMesage后執(zhí)行的println
            //獲取結(jié)束時(shí)間
            final long endTime = System.currentTimeMillis();
            mPrintingStarted = false;
            //判斷耗時(shí)是否超過(guò)閾值
            if (isBlock(endTime)) {
                notifyBlockEvent(endTime);
            }
            stopDump();
        }
    }
 //判斷是否超過(guò)閾值
 private boolean isBlock(long endTime) {
        return endTime - mStartTimestamp > mBlockThresholdMillis;
    }
//回調(diào)監(jiān)聽(tīng)
 private void notifyBlockEvent(final long endTime) {
        final long startTime = mStartTimestamp;
        final long startThreadTime = mStartThreadTimestamp;
        final long endThreadTime = SystemClock.currentThreadTimeMillis();
        HandlerThreadFactory.getWriteLogThreadHandler().post(new Runnable() {
            @Override
            public void run() {
                mBlockListener.onBlockEvent(startTime, endTime, startThreadTime, endThreadTime);
            }
        });
    }

當(dāng)發(fā)現(xiàn)時(shí)間差超過(guò)閾值后,會(huì)回調(diào)onBlockEvent妒貌。具體的實(shí)現(xiàn)在BlockCanaryInternals的構(gòu)造方法中通危,如下:

 setMonitor(new LooperMonitor(new LooperMonitor.BlockListener() {

            @Override
            public void onBlockEvent(long realTimeStart, long realTimeEnd,
                                     long threadTimeStart, long threadTimeEnd) {
                //根據(jù)開(kāi)始及結(jié)束時(shí)間,從棧的map當(dāng)中獲取記錄信息
                ArrayList<String> threadStackEntries = stackSampler
                        .getThreadStackEntries(realTimeStart, realTimeEnd);
                if (!threadStackEntries.isEmpty()) {
                    //構(gòu)建 BlockInfo對(duì)象灌曙,設(shè)置相關(guān)的信息
                    BlockInfo blockInfo = BlockInfo.newInstance()
                            .setMainThreadTimeCost(realTimeStart, realTimeEnd, threadTimeStart, threadTimeEnd)
                            .setCpuBusyFlag(cpuSampler.isCpuBusy(realTimeStart, realTimeEnd))
                            .setRecentCpuRate(cpuSampler.getCpuRateInfo())
                            .setThreadStackEntries(threadStackEntries)
                            .flushString();
                    //記錄信息
                    LogWriter.save(blockInfo.toString());
                    //遍歷攔截器菊碟,通知
                    if (mInterceptorChain.size() != 0) {
                        for (BlockInterceptor interceptor : mInterceptorChain) {
                            interceptor.onBlock(getContext().provideContext(), blockInfo);
                        }
                    }
                }
            }
        }, getContext().provideBlockThreshold(), getContext().stopWhenDebugging()));

最后我們看攔截器的實(shí)現(xiàn)DisplayService,會(huì)發(fā)送前臺(tái)的通知在刺,代碼如下:

  @Override
    public void onBlock(Context context, BlockInfo blockInfo) {
        Intent intent = new Intent(context, DisplayActivity.class);
        intent.putExtra("show_latest", blockInfo.timeStart);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
        PendingIntent pendingIntent = PendingIntent.getActivity(context, 1, intent, FLAG_UPDATE_CURRENT);
        String contentTitle = context.getString(R.string.block_canary_class_has_blocked, blockInfo.timeStart);
        String contentText = context.getString(R.string.block_canary_notification_message);
        show(context, contentTitle, contentText, pendingIntent);
    }

3逆害、dump

從上面的流程我們可以知道,當(dāng)dispatchMessage前的println觸發(fā)時(shí)蚣驼,會(huì)執(zhí)行dump的start方法忍燥,當(dāng)dispatchMessage后的println觸發(fā)時(shí),會(huì)執(zhí)行dump的stop方法隙姿。

 private void startDump() {
        if (null != BlockCanaryInternals.getInstance().stackSampler) {
            BlockCanaryInternals.getInstance().stackSampler.start();
        }

        if (null != BlockCanaryInternals.getInstance().cpuSampler) {
            BlockCanaryInternals.getInstance().cpuSampler.start();
        }
    }

    private void stopDump() {
        if (null != BlockCanaryInternals.getInstance().stackSampler) {
            BlockCanaryInternals.getInstance().stackSampler.stop();
        }

        if (null != BlockCanaryInternals.getInstance().cpuSampler) {
            BlockCanaryInternals.getInstance().cpuSampler.stop();
        }
    }

下面我們分Stacksampler和CpuSampler進(jìn)行介紹梅垄。

1、Stacksampler

start()的執(zhí)行流程如下:

 public void start() {
        if (mShouldSample.get()) {
            return;
        }
        mShouldSample.set(true);

        HandlerThreadFactory.getTimerThreadHandler().removeCallbacks(mRunnable);
        //通過(guò)一個(gè)HandlerThread延時(shí)執(zhí)行了mRunnable
        HandlerThreadFactory.getTimerThreadHandler().postDelayed(mRunnable,
                BlockCanaryInternals.getInstance().getSampleDelay());
    }
   //mRunnable在基類AbstractSampler中定義
  private Runnable mRunnable = new Runnable() {
        @Override
        public void run() {
            //抽象方法
            doSample();
            //繼續(xù)執(zhí)行采集
            if (mShouldSample.get()) {
                HandlerThreadFactory.getTimerThreadHandler()
                        .postDelayed(mRunnable, mSampleInterval);
            }
        }
    };
 //Stacksampler的doSample()實(shí)現(xiàn)
  @Override
    protected void doSample() {
        StringBuilder stringBuilder = new StringBuilder();
        //通過(guò)mCurrentThread.getStackTrace()獲取StackTraceElement输玷,加入到StringBuilder
        for (StackTraceElement stackTraceElement : mCurrentThread.getStackTrace()) {
            stringBuilder
                    .append(stackTraceElement.toString())
                    .append(BlockInfo.SEPARATOR);
        }

        synchronized (sStackMap) {
        //Lru算法队丝,控制LinkHashMap的長(zhǎng)度
            if (sStackMap.size() == mMaxEntryCount && mMaxEntryCount > 0) {
                sStackMap.remove(sStackMap.keySet().iterator().next());
            }
            //加入到map中
            sStackMap.put(System.currentTimeMillis(), stringBuilder.toString());
        }
    }

stop()的執(zhí)行流程如下:

 public void stop() {
        if (!mShouldSample.get()) {
            return;
        }
        //設(shè)置控制變量
        mShouldSample.set(false);
        //取消handler消息
        HandlerThreadFactory.getTimerThreadHandler().removeCallbacks(mRunnable);
    }

2、CpuSampler

其他執(zhí)行流程均與StackSampler一致欲鹏,這里主要分析doSample的實(shí)現(xiàn)机久,如下:

 //主要通過(guò)獲取/proc/stat文件 去獲取cpu的信息
  protected void doSample() {
        BufferedReader cpuReader = null;
        BufferedReader pidReader = null;

        try {
            cpuReader = new BufferedReader(new InputStreamReader(
                    new FileInputStream("/proc/stat")), BUFFER_SIZE);
            String cpuRate = cpuReader.readLine();
            if (cpuRate == null) {
                cpuRate = "";
            }

            if (mPid == 0) {
                mPid = android.os.Process.myPid();
            }
            pidReader = new BufferedReader(new InputStreamReader(
                    new FileInputStream("/proc/" + mPid + "/stat")), BUFFER_SIZE);
            String pidCpuRate = pidReader.readLine();
            if (pidCpuRate == null) {
                pidCpuRate = "";
            }

            parse(cpuRate, pidCpuRate);
        } catch (Throwable throwable) {
            Log.e(TAG, "doSample: ", throwable);
        } finally {
            try {
                if (cpuReader != null) {
                    cpuReader.close();
                }
                if (pidReader != null) {
                    pidReader.close();
                }
            } catch (IOException exception) {
                Log.e(TAG, "doSample: ", exception);
            }
        }
    }

blockcanary是如何進(jìn)行卡頓的判定?

blockcanary的核心原理是通過(guò)自定義一個(gè)Printer赔嚎,設(shè)置到主線程ActivityThread的MainLooper中膘盖。MainLooper在dispatch消息前后都會(huì)調(diào)用Printer進(jìn)行打印胧弛。從而獲取前后執(zhí)行的時(shí)間差值,判斷是否超過(guò)設(shè)置的閾值侠畔。如果超過(guò)结缚,則判定為卡頓。

leakcanary是如何獲取線程的堆棧信息软棺?

通過(guò)mCurrentThread.getStackTrace()方法红竭,遍歷獲取StackTraceElement,轉(zhuǎn)化為一個(gè)StringBuilder的value喘落,并存儲(chǔ)到一個(gè)key為時(shí)間戳的LinkHashMap中茵宪。

leakcanary是如何獲取cpu的信息?

通過(guò)讀取/proc/stat文件瘦棋,獲取所有CPU活動(dòng)的信息來(lái)計(jì)算CPU使用率稀火。解析出信息后,轉(zhuǎn)化為一個(gè)StringBuilder的value赌朋,并存儲(chǔ)到一個(gè)key為時(shí)間戳的LinkHashMap中憾股。

總結(jié)

思考

blockcanary充分的利用了Loop的機(jī)制,在MainLooper的loop方法中執(zhí)行dispatchMessage前后都會(huì)執(zhí)行printer的println進(jìn)行輸出箕慧,并且提供了方法設(shè)置printer服球。通過(guò)分析前后打印的時(shí)差與閾值進(jìn)行比對(duì),從而判定是否卡頓颠焦。

參考資料

Android性能優(yōu)化-渲染優(yōu)化

Android UI卡頓監(jiān)測(cè)框架BlockCanary原理分析

關(guān)于

歡迎關(guān)注我的個(gè)人公眾號(hào)

微信搜索:一碼一浮生斩熊,或者搜索公眾號(hào)ID:life2code

image
最后編輯于
?著作權(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)容

  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語(yǔ)法,類相關(guān)的語(yǔ)法跟继,內(nèi)部類的語(yǔ)法种冬,繼承相關(guān)的語(yǔ)法,異常的語(yǔ)法舔糖,線程的語(yǔ)...
    子非魚(yú)_t_閱讀 31,581評(píng)論 18 399
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,501評(píng)論 25 707
  • 最近除了踐行007之外娱两,我在踐行這五個(gè)字:保持饑餓感。 為什么我突然有這樣的想法呢金吗?故事還得從去年下半...
    良晨goodmorning閱讀 435評(píng)論 0 2
  • 南瓜是日常生活中常見(jiàn)的一種食材十兢,做法非常多,同時(shí)也具有很高的營(yíng)養(yǎng)價(jià)值摇庙。南瓜的食療功效解毒旱物,保護(hù)胃黏膜,幫助消化卫袒,并...
    我在黑暗中跳舞閱讀 423評(píng)論 0 0