其實(shí)我只是想看看SharedPreferences是如何實(shí)現(xiàn)的

那天我突然看到有人說(shuō)使用SharedPreferences會(huì)出現(xiàn)ANR豌鸡,要知道ANR可是個(gè)大問(wèn)題啊氢卡,于是我就想看看SharedPreferences是如何實(shí)現(xiàn)的,然后記錄于此,還請(qǐng)各位指點(diǎn)建邓!

我們都知道,我們是通過(guò)Context來(lái)獲取SharedPreferences的春缕,于是盗胀,我們就在Context中看到這么一個(gè)抽象方法,比較Context本身就是個(gè)抽象類(lèi)嘛锄贼!

public abstract SharedPreferences getSharedPreferences(String name, @PreferencesMode int mode);

然后票灰,很顯然,我們得找到Context的具體實(shí)現(xiàn)類(lèi)宅荤,才能知道getSharedPreferences這個(gè)方法里面的邏輯屑迂,我們很快就注意到ContextWrapper,而且我們的Activity和Application都繼承于ContextWrapper冯键,然后我們就找到這樣的一個(gè)實(shí)現(xiàn)方法惹盼,如下:

    @Override
    public SharedPreferences getSharedPreferences(String name, int mode) {
        return mBase.getSharedPreferences(name, mode);
    }

其中mBase是申明的一個(gè)Context成員變量,而我們想看getSharedPreferences的具體實(shí)現(xiàn)惫确,找到mBase就是關(guān)鍵逻锐,ContextWrapper中相關(guān)代碼如下:

    Context mBase;
    public ContextWrapper(Context base) {
        mBase = base;
    }
    protected void attachBaseContext(Context base) {
        if (mBase != null) {
            throw new IllegalStateException("Base context already set");
        }
        mBase = base;
    }

然后我們注意到mBase有兩種初始化方法,其中attachBaseContext值得我們注意雕薪,畢竟我們記得在Activity的源碼中就有attachBaseContext方法昧诱,如下:

    @Override
    protected void attachBaseContext(Context newBase) {
        super.attachBaseContext(newBase);
        if (newBase != null) {
            newBase.setAutofillClient(this);
        }
    }

其中 super.attachBaseContext(newBase)這個(gè)調(diào)用的就是ContextWrapper中的attachBaseContext方法。同樣的所袁,Application中的attach方法中也有調(diào)用到該方法盏档。
我們繼續(xù)查看,發(fā)現(xiàn)Activity的attachBaseContext是由attach方法調(diào)用的燥爷,我們來(lái)看看這個(gè)方法:

 final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback) {

        attachBaseContext(context);
        ......
}

我們很自然地注意到第二個(gè)參數(shù)ActivityThread蜈亩,畢竟總所周知,ActivityThread才是應(yīng)用真正的入口前翎,所以稚配,我們的關(guān)注點(diǎn)應(yīng)該轉(zhuǎn)到ActivityThread上來(lái),我們來(lái)看看其入口的代碼:

public static void main(String[] args) {
        .......
        Looper.prepareMainLooper();
        .......
        ActivityThread thread = new ActivityThread();
        thread.attach(false, startSeq);

        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }

        if (false) {
            Looper.myLooper().setMessageLogging(new
                    LogPrinter(Log.DEBUG, "ActivityThread"));
        }

        // End of event ActivityThreadMain.
        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
        Looper.loop();

        throw new RuntimeException("Main thread loop unexpectedly exited");
    }

我們摘取了關(guān)鍵代碼港华,很顯然道川,這就是android的消息分發(fā)機(jī)制,其中我們用的是主線(xiàn)程的Looper立宜,所以其Handler也默認(rèn)綁定到主線(xiàn)程中冒萄,于是我們的四大組件默認(rèn)都運(yùn)行在主線(xiàn)程中的,這一點(diǎn)必須明確橙数!
另外尊流,注意到最后拋出的異常沒(méi),說(shuō)明Looper死循環(huán)必須貫穿APP整個(gè)生命周期灯帮,除非APP退出了崖技,否則必須一直進(jìn)行Looper死循環(huán)逻住,否則就會(huì)拋出異常。
好了迎献,上面這些屬于順帶一提的瞎访,說(shuō)這個(gè)入口主要是想說(shuō)下面這兩行代碼:

        ActivityThread thread = new ActivityThread();
        thread.attach(false, startSeq);

這里會(huì)進(jìn)行什么操作呢?attach傳false忿晕,說(shuō)明不是系統(tǒng)應(yīng)用装诡,會(huì)直接綁定到AMS(ActivityManagerService)银受,如下attach中的關(guān)鍵代碼:

           final IActivityManager mgr = ActivityManager.getService();
            try {
                mgr.attachApplication(mAppThread, startSeq);
            } catch (RemoteException ex) {
                throw ex.rethrowFromSystemServer();
            }

正如大家所知道的践盼,其實(shí)ActivityThread擁有與AMS交互并且管理Activity和Service等組件的重要作用。
至于ActivityThread與AMS間的交互宾巍,其實(shí)都是進(jìn)行著一個(gè)個(gè)IPC交互咕幻,你看到上面代碼中的RemoteException沒(méi)?在這里我們就不對(duì)IPC調(diào)用作過(guò)多的說(shuō)明和深究了顶霞,我們只需知道一點(diǎn):其實(shí)Activity的每個(gè)生命周期調(diào)用肄程,其實(shí)是由ActivityThread與AMS進(jìn)行IPC交互來(lái)完成的。

我們明白了上述過(guò)程后选浑,發(fā)現(xiàn)了ActivityThread中有很多handle開(kāi)頭的方法蓝厌,如下圖所示:

消息分發(fā)機(jī)制調(diào)用的方法

沒(méi)錯(cuò),這些就是IPC后通過(guò)消息分發(fā)機(jī)制調(diào)用的方法古徒,我們重點(diǎn)關(guān)注的是Activity的啟動(dòng)拓提,如上圖紅圈中的方法:

    /**
     * Extended implementation of activity launch. Used when server requests a launch or relaunch.
     */
    @Override
    public Activity handleLaunchActivity(ActivityClientRecord r,
            PendingTransactionActions pendingActions, Intent customIntent) {
        .......
        final Activity a = performLaunchActivity(r, customIntent);
        .......
    }

handleLaunchActivity中我們注意到performLaunchActivity這個(gè)方法,關(guān)鍵代碼如下:

    /**  Core implementation of activity launch. */
    private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
              .......
              ContextImpl appContext = createBaseContextForActivity(r);
              .......
              activity.attach(appContext, this, getInstrumentation(), r.token,
                        r.ident, app, r.intent, r.activityInfo, title, r.parent,
                        r.embeddedID, r.lastNonConfigurationInstances, config,
                        r.referrer, r.voiceInteractor, window, r.configCallback);
              .......
    }

終于到這里來(lái)了隧膘,看到?jīng)]代态?在performLaunchActivity方法里,我們創(chuàng)建了ContextImpl疹吃,然后將其傳給了activity蹦疑,于是,我們苦苦追尋的實(shí)現(xiàn)類(lèi)就是ContextImpl萨驶,當(dāng)然歉摧,光憑名字我就知道它是Context的真正實(shí)現(xiàn)類(lèi)了,只是腔呜,我們還是必須要這樣有憑有據(jù)判莉、明明白白滴追蹤一下代碼!

當(dāng)然育谬,我們回顧一下上面ContextWrapper券盅、ContextImpl和Context三者的關(guān)系,從設(shè)計(jì)模式的角度上來(lái)說(shuō)膛檀,這里應(yīng)用了代理模式锰镀。
其中娘侍,Context為抽象主題類(lèi),ContextImpl為真實(shí)主題類(lèi)泳炉,ContextWrapper為代理類(lèi)憾筏,ContextImpl和ContextWrapper都繼承于Context,然后ContextWrapper持有ContextImpl的引用花鹅,完成了對(duì)ContextImpl的代理操作氧腰。

接下來(lái)我們重點(diǎn)就放在ContextImpl來(lái)了,我們可以看到getSharedPreferences的真正實(shí)現(xiàn)刨肃,如下:

 @Override
    public SharedPreferences getSharedPreferences(String name, int mode) {
        // At least one application in the world actually passes in a null
        // name.  This happened to work because when we generated the file name
        // we would stringify it to "null.xml".  Nice.
        if (mPackageInfo.getApplicationInfo().targetSdkVersion <
                Build.VERSION_CODES.KITKAT) {
            if (name == null) {
                name = "null";
            }
        }

        File file;
        synchronized (ContextImpl.class) {
            if (mSharedPrefsPaths == null) {
                mSharedPrefsPaths = new ArrayMap<>();
            }
            file = mSharedPrefsPaths.get(name);
            if (file == null) {
                file = getSharedPreferencesPath(name);
                mSharedPrefsPaths.put(name, file);
            }
        }
        return getSharedPreferences(file, mode);
    }

我們可以看到古拴,最終會(huì)調(diào)用的是getSharedPreferences(File file, int mode),注意到其中創(chuàng)建文件的方法:

   @Override
    public File getSharedPreferencesPath(String name) {
        return makeFilename(getPreferencesDir(), name + ".xml");
    }

   private File getPreferencesDir() {
        synchronized (mSync) {
            if (mPreferencesDir == null) {
                mPreferencesDir = new File(getDataDir(), "shared_prefs");
            }
            return ensurePrivateDirExists(mPreferencesDir);
        }
    }

   private File makeFilename(File base, String name) {
        if (name.indexOf(File.separatorChar) < 0) {
            return new File(base, name);
        }
        throw new IllegalArgumentException(
                "File " + name + " contains a path separator");
    }

從上面這3個(gè)方法真友,我們可以知道:
1)SharedPreferences其實(shí)存儲(chǔ)為一個(gè)xml文件
2)SharedPreferences存儲(chǔ)的位置在這個(gè)目錄下:/data/data/你APP包名/shared_prefs/
3)創(chuàng)建SharedPreferences的名字中不能含有“/”這樣的分隔符黄痪,否則會(huì)報(bào)錯(cuò)

接著,我們來(lái)看看最終創(chuàng)建的方法:getSharedPreferences(File file, int mode):

    @Override
    public SharedPreferences getSharedPreferences(File file, int mode) {
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
            sp = cache.get(file);
            if (sp == null) {
                checkMode(mode);
                if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {
                    if (isCredentialProtectedStorage()
                            && !getSystemService(UserManager.class)
                                    .isUserUnlockingOrUnlocked(UserHandle.myUserId())) {
                        throw new IllegalStateException("SharedPreferences in credential encrypted "
                                + "storage are not available until after user is unlocked");
                    }
                }
                sp = new SharedPreferencesImpl(file, mode);
                cache.put(file, sp);
                return sp;
            }
        }
        if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
            getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
            // If somebody else (some other process) changed the prefs
            // file behind our back, we reload it.  This has been the
            // historical (if undocumented) behavior.
            sp.startReloadIfChangedUnexpectedly();
        }
        return sp;
    }

代碼也很簡(jiǎn)單盔然,我們最終返回的是SharedPreferencesImpl對(duì)象桅打,它是SharedPreferences的實(shí)現(xiàn)類(lèi),然后通過(guò)一個(gè)ArrayMap進(jìn)行了緩存愈案,一個(gè)文件對(duì)應(yīng)一個(gè)SharedPreferencesImpl挺尾,然后再追其根源,其實(shí)就是一個(gè)文件名對(duì)應(yīng)一個(gè)SharedPreferencesImpl站绪,為什么這么說(shuō)呢遭铺?因?yàn)槲覀儫o(wú)法直接調(diào)用這個(gè)方法:getSharedPreferences(File file, int mode),源碼也以注釋說(shuō)明清楚了崇众,不信你可以去試試看O(∩_∩)O

我們回到方法的最后掂僵,意思就是當(dāng)mode為Context.MODE_MULTI_PROCESS時(shí),會(huì)執(zhí)行:sp.startReloadIfChangedUnexpectedly();
這個(gè)就是說(shuō)當(dāng)在多進(jìn)程時(shí)顷歌,每次獲取SharedPreferences都會(huì)嘗試去重新加載數(shù)據(jù)锰蓬,以防數(shù)據(jù)發(fā)生變化而不一致。這就是SharedPreferences在多進(jìn)程中保證數(shù)據(jù)正確性的方法眯漩,當(dāng)然芹扭,Context.MODE_MULTI_PROCESS這個(gè)已經(jīng)是被廢棄掉了,谷歌推薦使用ContentProivder來(lái)完成多進(jìn)程文件的共享赦抖,而不是SharedPreferences舱卡,至于原因,后面加以說(shuō)明队萤。

接下來(lái)我們看看SharedPreferencesImpl的構(gòu)造函數(shù):

   SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        mMap = null;
        mThrowable = null;
        startLoadFromDisk();
    }

這里嘛轮锥,注意到makeBackupFile,這個(gè)是搞了一個(gè)備份文件“.bak”要尔,以防在保存數(shù)據(jù)過(guò)程中存現(xiàn)中斷舍杜,下次進(jìn)來(lái)時(shí)可以通過(guò)備份文件進(jìn)行恢復(fù)新娜,算是一種保險(xiǎn)措施吧,當(dāng)然既绩,它也僅僅只能恢復(fù)保存進(jìn)“.bak”文件中的數(shù)據(jù)而已概龄。
我們重點(diǎn)要看的是最后一個(gè)方法:

   private void startLoadFromDisk() {
        synchronized (mLock) {
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk();
            }
        }.start();
    }

原來(lái),這里直接創(chuàng)建一個(gè)線(xiàn)程饲握,將SharedPreferences文件中的數(shù)據(jù)直接加載到內(nèi)存中去私杜,這里需要說(shuō)明兩點(diǎn):
1)不用使用SharedPreferences存儲(chǔ)大量的數(shù)據(jù),不然你想啊救欧,那么大的數(shù)據(jù)直接load進(jìn)內(nèi)存衰粹,簡(jiǎn)直了···
2)這里也解釋了上面說(shuō)到的不建議使用Context.MODE_MULTI_PROCESS的原因:因?yàn)樯厦娴膕tartReloadIfChangedUnexpectedly會(huì)調(diào)用到startLoadFromDisk這個(gè)方法,這樣一來(lái)颜矿,在多進(jìn)程環(huán)境中寄猩,很多時(shí)候獲取SharedPreferences會(huì)多次load數(shù)據(jù)進(jìn)內(nèi)存嫉晶,這浪費(fèi)了內(nèi)存的緩存作用骑疆,同時(shí)讀寫(xiě)IO也會(huì)影響性能。

上面說(shuō)到的替废,將SharedPreferences文件中的數(shù)據(jù)load進(jìn)內(nèi)存箍铭,根據(jù)源碼會(huì)保存為一個(gè) Map<String, Object>鍵值對(duì)對(duì)象,然后之后的所有讀操作都從這個(gè)Map對(duì)象中讀取椎镣,這也解釋了為什么SharedPreferences在第一次讀會(huì)較慢诈火,而后面就很快了?那是因?yàn)榈谝淮巫x時(shí)需要花時(shí)間將數(shù)據(jù)load進(jìn)內(nèi)存状答,之后都從Map讀就很快了冷守。

說(shuō)完讀,我們接著說(shuō)寫(xiě)惊科,說(shuō)寫(xiě)就離不開(kāi)要說(shuō)commit 和 apply拍摇。
我們都知道一般建議使用apply,就算你用了commit 馆截,AndroidStudio也會(huì)給出這樣的提醒:

Consider using apply() instead of commit on shared preferences. Whereas commit blocks and writes its data to persistent storage immediately, apply will handle it in the background.

當(dāng)然充活,如果你使用commit然后用變量接收commit 的結(jié)果的話(huà),就沒(méi)有上面的提醒蜡娶,這也說(shuō)明了commit 和 apply的第一個(gè)不同:commit有一個(gè)boolean返回值混卵,而apply沒(méi)有。

我們直接貼出commit 和 apply兩方法的關(guān)鍵代碼:

        @Override
        public boolean commit() {
             ......
            MemoryCommitResult mcr = commitToMemory();
            SharedPreferencesImpl.this.enqueueDiskWrite(
                mcr, null /* sync write on this thread okay */);
             ......
            return mcr.writeToDiskResult;
        }
        @Override
        public void apply() {
             ......
            final MemoryCommitResult mcr = commitToMemory();
            final Runnable awaitCommit = new Runnable() {
                    @Override
                    public void run() {
                        try {
                            mcr.writtenToDiskLatch.await();
                        } catch (InterruptedException ignored) {
                        }

                        if (DEBUG && mcr.wasWritten) {
                            Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                                    + " applied after " + (System.currentTimeMillis() - startTime)
                                    + " ms");
                        }
                    }
                };

            QueuedWork.addFinisher(awaitCommit);

            Runnable postWriteRunnable = new Runnable() {
                    @Override
                    public void run() {
                        awaitCommit.run();
                        QueuedWork.removeFinisher(awaitCommit);
                    }
                };

            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
           ......
        }

很明顯窖张,
1)commit 和 apply都調(diào)用了commitToMemory幕随,該方法從名字就知道提交到內(nèi)存,也就說(shuō)兩個(gè)方法都先更新了內(nèi)存Map的數(shù)據(jù)宿接。
2)commit 和 apply兩個(gè)方法也都調(diào)用了enqueueDiskWrite赘淮,該方法從名字也能知道就是保存到本地磁盤(pán)的枢赔,而主要區(qū)別在于第二個(gè)參數(shù),commit中傳的是null拥知,而且源碼還給出了注釋?zhuān)簊ync write on this thread okay踏拜,意思就是在當(dāng)前線(xiàn)程同步寫(xiě)進(jìn)磁盤(pán);而apply則傳了一個(gè)Runnable對(duì)象低剔,然后使用了QueuedWork.queue方法加入了任務(wù)速梗,很顯然它屬于一個(gè)異步操作。
于是襟齿,這里就有了commit 和 apply的第二個(gè)不同:保存到本地磁盤(pán)姻锁,commit是同步、阻塞的猜欺,apply是異步位隶、非阻塞的。

上面說(shuō)到了QueuedWork.queue开皿,這里插一下QueuedWork相關(guān)的東西涧黄,它屬于android的一個(gè)內(nèi)部工具類(lèi),用于跟蹤那些未完成的或尚未結(jié)束的全局任務(wù)赋荆,我們也同樣來(lái)看一下其相關(guān)源碼:

   public static void queue(Runnable work, boolean shouldDelay) {
        Handler handler = getHandler();

        synchronized (sLock) {
            sWork.add(work);

            if (shouldDelay && sCanDelay) {
                handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
            } else {
                handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
            }
        }
    }

    private static Handler getHandler() {
        synchronized (sLock) {
            if (sHandler == null) {
                HandlerThread handlerThread = new HandlerThread("queued-work-looper",
                        Process.THREAD_PRIORITY_FOREGROUND);
                handlerThread.start();

                sHandler = new QueuedWorkHandler(handlerThread.getLooper());
            }
            return sHandler;
        }
    }

很明顯了笋妥,內(nèi)部使用了HandlerThread,然后用Handler進(jìn)行消息的分發(fā)窄潭,再次證明了apply是異步春宣、非阻塞的。
當(dāng)然嫉你,我們上面apply方法中有這么一行代碼:

QueuedWork.addFinisher(awaitCommit);

這里再說(shuō)明一下月帝,QueuedWork有waitToFinish方法來(lái)保證addFinisher中的Runnable得以執(zhí)行,那么幽污,讓我們來(lái)看看waitToFinish都在哪里被調(diào)用的:

waitToFinish調(diào)用之處

原來(lái)繞了一大圈嚷辅,我們又回到ActivityThread中來(lái)了,比如handlePauseActivity油挥,意思就是在activity暫停時(shí)就會(huì)調(diào)用waitToFinish潦蝇,該方法的目的就是確保之前提交的異步任務(wù)能被執(zhí)行完畢,而由于在ActivityThread中調(diào)用深寥,也就是在主線(xiàn)程中攘乒,所以,如果使用apply方法而出現(xiàn)ANR的話(huà)惋鹅,一般就是出現(xiàn)在調(diào)用waitToFinish這個(gè)過(guò)程则酝。

好啦,我們總算可以回到最開(kāi)始說(shuō)的問(wèn)題了:使用SharedPreferences會(huì)出現(xiàn)ANR,經(jīng)過(guò)上面一系列分析沽讹,我們得出了以下出現(xiàn)ANR的情況:
1)首次getXXX般卑,如果你的SharedPreferences中存儲(chǔ)了大量數(shù)據(jù),那么在首次獲取數(shù)據(jù)時(shí)爽雄,會(huì)將文件中的數(shù)據(jù)load進(jìn)內(nèi)存蝠检,我們得在主線(xiàn)程中等其load完畢后才能get,如果load時(shí)間很長(zhǎng)就有可能造成ANR
2)commit挚瘟,這個(gè)就很容易理解了叹谁,在主線(xiàn)程中進(jìn)行保存到本地磁盤(pán)的操作,該操作有可能出現(xiàn)ANR
3)apply乘盖,就如上面所分析的焰檩,在調(diào)用waitToFinish時(shí)有可能出現(xiàn)ANR

說(shuō)了這么多,我們是不是得寫(xiě)一個(gè)ANR出來(lái)岸┛颉析苫?沒(méi)問(wèn)題,請(qǐng)看如下代碼:

 SharedPreferences sp= getSharedPreferences("mysp", 0);
 SharedPreferences.Editor editorA = sp.edit();
 for(int i=0;i<300000;i++){
        editorA.putString("A" + i, "a" + i);
 }
 editorA.apply();
 SharedPreferences.Editor editorB = sp.edit();
 for(int i=0;i<300000;i++){
       editorB.putString("B"+i,"b"+i);
 }
 editorB.commit();

簡(jiǎn)單粗暴穿扳,這樣就是一個(gè)因commit造成的ANR衩侥,同時(shí),我們接著看看讀取數(shù)據(jù)的情況纵揍,因?yàn)樯厦娴臄?shù)據(jù)量較大了顿乒,所以筆者在首次讀取時(shí)發(fā)現(xiàn)直接黑屏了好幾秒议街,倒是還沒(méi)出現(xiàn)ANR泽谨。

最后,我們針對(duì)上面3種出現(xiàn)ANR的情況給出如下的建議
1)請(qǐng)別往SharedPreferences中存入大量的數(shù)據(jù)特漩,數(shù)據(jù)量大時(shí)請(qǐng)考慮使用本地?cái)?shù)據(jù)庫(kù)
2)如果擔(dān)心commit在主線(xiàn)程保存數(shù)據(jù)會(huì)導(dǎo)致ANR吧雹,其實(shí)有一種做法就是直接新建一個(gè)子線(xiàn)程來(lái)執(zhí)行,可以考慮用一個(gè)單線(xiàn)程池來(lái)進(jìn)行封裝
3)apply的話(huà)可以考慮使用清理等待鎖涂身,據(jù)頭條app開(kāi)發(fā)團(tuán)隊(duì)的測(cè)試驗(yàn)證雄卷,效果還是很OK的,有空筆者再進(jìn)行上手驗(yàn)證蛤售,暫且收錄于此丁鹉。

參考鏈接:
https://blog.csdn.net/shifuhetudi/article/details/52089562
http://www.reibang.com/p/875d13458538

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市悴能,隨后出現(xiàn)的幾起案子揣钦,更是在濱河造成了極大的恐慌,老刑警劉巖漠酿,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件冯凹,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡炒嘲,警方通過(guò)查閱死者的電腦和手機(jī)宇姚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門(mén)匈庭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人浑劳,你說(shuō)我怎么就攤上這事阱持。” “怎么了魔熏?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵紊选,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我道逗,道長(zhǎng)兵罢,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任滓窍,我火速辦了婚禮卖词,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘吏夯。我一直安慰自己此蜈,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布噪生。 她就那樣靜靜地躺著裆赵,像睡著了一般。 火紅的嫁衣襯著肌膚如雪跺嗽。 梳的紋絲不亂的頭發(fā)上战授,一...
    開(kāi)封第一講書(shū)人閱讀 49,166評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音桨嫁,去河邊找鬼植兰。 笑死璃吧,一個(gè)胖子當(dāng)著我的面吹牛楣导,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播畜挨,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼筒繁,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了巴元?” 一聲冷哼從身側(cè)響起毡咏,我...
    開(kāi)封第一講書(shū)人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎务冕,沒(méi)想到半個(gè)月后血当,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年臊旭,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了落恼。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡离熏,死狀恐怖佳谦,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情滋戳,我是刑警寧澤钻蔑,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站奸鸯,受9級(jí)特大地震影響咪笑,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜娄涩,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一钻蹬、第九天 我趴在偏房一處隱蔽的房頂上張望谤职。 院中可真熱鬧,春花似錦默责、人聲如沸能曾。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)剿配。三九已至,卻和暖如春咽斧,著一層夾襖步出監(jiān)牢的瞬間堪置,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工收厨, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留晋柱,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓诵叁,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親钦椭。 傳聞我的和親對(duì)象是個(gè)殘疾皇子拧额,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

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