SharedPreferences源碼解析

起因

在項(xiàng)目開(kāi)發(fā)中發(fā)現(xiàn)很多數(shù)據(jù)都是用SharedPreferences做本地保存的,操作SharedPreferences只需要建立Editor阴绢,然后這個(gè)向這個(gè)Editor對(duì)象put各種各樣的鍵值對(duì)吧享,最后調(diào)用它的commit或者apply保存信息即可甲葬,非常簡(jiǎn)單方便鸡挠。同時(shí)注釋文檔中說(shuō)明commit是同步的而apply是異步的辉饱,說(shuō)明SharedPreferences是支持多線程的,那就有些疑惑了:

  • 假設(shè)要保存的數(shù)據(jù)很多拣展,apply異步線程保存彭沼,同時(shí)主線程再去讀取數(shù)據(jù),讀取數(shù)據(jù)會(huì)不會(huì)等待备埃?
  • 在異步保存時(shí)退出Activity姓惑,退出應(yīng)用有影響嗎?

下面將帶著這兩個(gè)問(wèn)題去學(xué)習(xí)SharedPreferences的實(shí)現(xiàn)

分析

1.獲取SP對(duì)象

調(diào)用Activity的getSharedPreferences方法,發(fā)現(xiàn)這是父類ContextWrapper的方法按脚,如下:

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

這個(gè)方法只是再次調(diào)用mBase的相應(yīng)方法于毙,mBase是ContextImpl對(duì)象,而每一個(gè)ContextWrapper都會(huì)被綁定ContextImpl辅搬,ContextWarpper子類有Application望众、Service、Activity伞辛,所以在這三個(gè)常見(jiàn)的Context中都可以使用SP烂翰。

接著看看ContextImpl中的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) {//用的是類鎖,不是對(duì)象鎖
            if (mSharedPrefsPaths == null) {
                mSharedPrefsPaths = new ArrayMap<>();
            }
            file = mSharedPrefsPaths.get(name);
            if (file == null) {
                file = getSharedPreferencesPath(name);//創(chuàng)建File對(duì)象蚤氏,name + ".xml"
                mSharedPrefsPaths.put(name, file);
            }
        }
        return getSharedPreferences(file, mode);
    }

上面的mSharedPrefsPaths是一個(gè)Map對(duì)象甘耿,它保存的是SP的名字與文件的對(duì)應(yīng)關(guān)系,如果沒(méi)有對(duì)應(yīng)的File就創(chuàng)建一個(gè)File對(duì)象竿滨,注意mSharedPrefsPaths是對(duì)象的成員佳恬,但是在操作這個(gè)對(duì)象時(shí)用的不是對(duì)象鎖而是類鎖,為什么于游?接著看最后調(diào)用的它的重載方法毁葱,如下:


    /**
     * Map from package name, to preference name, to cached preferences.
      靜態(tài)的sSharedPrefsCache
     */
    private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;

    @Override
    public SharedPreferences getSharedPreferences(File file, int mode) {
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
        //file -> SP 的map
            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;
            }
        }

    //獲取靜態(tài)的sSharedPrefsCache
    private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
        if (sSharedPrefsCache == null) {
            sSharedPrefsCache = new ArrayMap<>();
        }

        final String packageName = getPackageName();
        ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
        if (packagePrefs == null) {
            packagePrefs = new ArrayMap<>();
            sSharedPrefsCache.put(packageName, packagePrefs);
        }

       

在這里創(chuàng)建、返回了正兒八經(jīng)干活的SharedPreferencesImpl對(duì)象贰剥,以上的分析可以總結(jié)為:獲取SP時(shí)存在這樣的映射關(guān)系: (對(duì)象中sp name) -> (對(duì)象 file) -> (全局的 sp)的關(guān)系倾剿,這個(gè)映射關(guān)系為了確保萬(wàn)無(wú)一失在操作的過(guò)程中全部使用類鎖,究竟在什么情況會(huì)出錯(cuò)蚌成,我的單線程腦子還沒(méi)想出來(lái)前痘。。 担忧。

2. 操作SP對(duì)象
2.1 新建與get方法

首先要明確這個(gè)SP對(duì)象它是static的芹缔,任意線程只要姿勢(shì)對(duì)了都可以通過(guò)Context操作它,所以對(duì)它的操作全部都加了對(duì)象鎖瓶盛,構(gòu)造方法如下:

    SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        mMap = null;
        mThrowable = null;
      //加載數(shù)據(jù)
        startLoadFromDisk();
    }

    private void startLoadFromDisk() {
        synchronized (mLock) {
            mLoaded = false;
        }

        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk();
            }
        }.start();
    }
    //開(kāi)啟新的線程加載數(shù)據(jù)
    private void loadFromDisk() {
        synchronized (mLock) {
            if (mLoaded) {
                return;
            }
            if (mBackupFile.exists()) {
                mFile.delete();
                mBackupFile.renameTo(mFile);
            }
        }

        // Debugging
        if (mFile.exists() && !mFile.canRead()) {
            Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
        }

        Map<String, Object> map = null;
        StructStat stat = null;
        Throwable thrown = null;
        try {
            stat = Os.stat(mFile.getPath());
            if (mFile.canRead()) {
                BufferedInputStream str = null;
                try {
                    str = new BufferedInputStream(
                            new FileInputStream(mFile), 16 * 1024);
                  //將XML文件轉(zhuǎn)化為內(nèi)存中的鍵值對(duì)
                    map = (Map<String, Object>) XmlUtils.readMapXml(str);
                } catch (Exception e) {
                    Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
                } finally {
                    IoUtils.closeQuietly(str);
                }
            }
        } catch (ErrnoException e) {
            // An errno exception means the stat failed. Treat as empty/non-existing by
            // ignoring.
        } catch (Throwable t) {//比較豪放最欠,任意問(wèn)題都catch住
            thrown = t;
        }
    
        synchronized (mLock) {
            mLoaded = true;
            mThrowable = thrown;

            // It's important that we always signal waiters, even if we'll make
            // them fail with an exception. The try-finally is pretty wide, but
            // better safe than sorry.
            try {
                if (thrown == null) {
                    if (map != null) {
                        mMap = map;
                        mStatTimestamp = stat.st_mtim;
                        mStatSize = stat.st_size;
                    } else {
                        mMap = new HashMap<>();
                    }
                }
                // In case of a thrown exception, we retain the old map. That allows
                // any open editors to commit and store updates.
            } catch (Throwable t) {
                mThrowable = t;
            } finally {
                mLock.notifyAll();//通知其他線程加載完成了
            }
        }
    }

上面的代碼中將所有保存在本地的數(shù)據(jù)都讀取到了mMap中示罗,接下來(lái)的getXXX方法都是從這個(gè)mMap中獲取值

    public Map<String, ?> getAll() {
        synchronized (mLock) {
            awaitLoadedLocked();//等待鎖釋放
            //noinspection unchecked
            return new HashMap<String, Object>(mMap);
        }
    }

2.2 edit方法
    @Override
    public Editor edit() {
        // TODO: remove the need to call awaitLoadedLocked() when
        // requesting an editor.  will require some work on the
        // Editor, but then we should be able to do:
        //
        //      context.getSharedPreferences(..).edit().putString(..).apply()
        //
        // ... all without blocking.
        synchronized (mLock) {
            awaitLoadedLocked();
        }

        return new EditorImpl();
    }

直接返回了EditorImpl對(duì)象,它是SharedPreferences.Editor的實(shí)現(xiàn)類芝硬。

        //保存更改的鍵值對(duì)
        private final Map<String, Object> mModified = new HashMap<>();
        //
        @Override
        public Editor putBoolean(String key, boolean value) {
            synchronized (mEditorLock) {
                mModified.put(key, value);
                return this;
            }
        }

調(diào)用putXXX方法時(shí)數(shù)據(jù)被保存在了mModified鍵值對(duì)中蚜点,在commit或者apply的時(shí)候提交,下面先看commit方法:

        @Override
        public boolean commit() {
            long startTime = 0;

            if (DEBUG) {
                startTime = System.currentTimeMillis();
            }
            //將mModified的變化同步給mMap
            MemoryCommitResult mcr = commitToMemory();

            SharedPreferencesImpl.this.enqueueDiskWrite(
                mcr, null /* sync write on this thread okay */);
            try {
                mcr.writtenToDiskLatch.await();
            } catch (InterruptedException e) {
                return false;
            } finally {
                if (DEBUG) {
                    Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                            + " committed after " + (System.currentTimeMillis() - startTime)
                            + " ms");
                }
            }
            notifyListeners(mcr);
            return mcr.writeToDiskResult;
        }

  private MemoryCommitResult commitToMemory() {
            long memoryStateGeneration;
            List<String> keysModified = null;
            Set<OnSharedPreferenceChangeListener> listeners = null;
            Map<String, Object> mapToWriteToDisk;

            synchronized (SharedPreferencesImpl.this.mLock) {
                // We optimistically don't make a deep copy until
                // a memory commit comes in when we're already
                // writing to disk.
                if (mDiskWritesInFlight > 0) {//此刻有其他數(shù)據(jù)正在提交
                    // We can't modify our mMap as a currently
                    // in-flight write owns it.  Clone it before
                    // modifying it.
                    // noinspection unchecked
                    mMap = new HashMap<String, Object>(mMap);
                }
                mapToWriteToDisk = mMap;
                mDiskWritesInFlight++;//正在同步+1

                boolean hasListeners = mListeners.size() > 0;
                if (hasListeners) {
                    keysModified = new ArrayList<String>();
                    listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
                }

                synchronized (mEditorLock) {
                    boolean changesMade = false;

                    if (mClear) {
                        if (!mapToWriteToDisk.isEmpty()) {
                            changesMade = true;
                            mapToWriteToDisk.clear();
                        }
                        mClear = false;
                    }

                    for (Map.Entry<String, Object> e : mModified.entrySet()) {
                        String k = e.getKey();
                        Object v = e.getValue();
                        // "this" is the magic value for a removal mutation. In addition,
                        // setting a value to "null" for a given key is specified to be
                        // equivalent to calling remove on that key.
                        if (v == this || v == null) {//為null刪除mMap的數(shù)據(jù)
                            if (!mapToWriteToDisk.containsKey(k)) {
                                continue;
                            }
                            mapToWriteToDisk.remove(k);
                        } else {//更新mMap中的數(shù)據(jù)
                            if (mapToWriteToDisk.containsKey(k)) {
                                Object existingValue = mapToWriteToDisk.get(k);
                                if (existingValue != null && existingValue.equals(v)) {
                                    continue;
                                }
                            }
                            mapToWriteToDisk.put(k, v);
                        }

                        changesMade = true;
                        if (hasListeners) {
                            keysModified.add(k);
                        }
                    }

                    mModified.clear();

                    if (changesMade) {
                        mCurrentMemoryStateGeneration++;
                    }

                    memoryStateGeneration = mCurrentMemoryStateGeneration;
                }
            }
            return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
                    mapToWriteToDisk);
        }

上面將mModified的鍵值同步給了mMap吵取,并且返回了MemoryCommitResult對(duì)象,顧名思義它是同步的結(jié)果锯厢,它的構(gòu)造方法的最后一個(gè)參數(shù)其實(shí)就是mMap對(duì)象皮官,此刻mMap中已經(jīng)同步了mModified的值,接下來(lái)就順理成章地要將mMap轉(zhuǎn)換成xml文件了实辑,看commit()中的調(diào)用的SharedPreferencesImpl.this.enqueueDiskWrite方法:

    private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
        //是否是同步commit捺氢,commit方法中postWriteRunnable為null
        final boolean isFromSyncCommit = (postWriteRunnable == null);
        //寫文件的runnable
        final Runnable writeToDiskRunnable = new Runnable() {
                @Override
                public void run() {
                    synchronized (mWritingToDiskLock) {
                        writeToFile(mcr, isFromSyncCommit);
                    }
                    synchronized (mLock) {
                        mDiskWritesInFlight--;
                    }
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
            };

        // Typical #commit() path with fewer allocations, doing a write on
        // the current thread.
        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            synchronized (mLock) {//此刻只有這一個(gè)commit
                wasEmpty = mDiskWritesInFlight == 1;
            }
            
            if (wasEmpty) {//只有一個(gè)提交,就在主線程寫文件了
                writeToDiskRunnable.run();
                return;
            }
        }

        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    }

接下來(lái)看apply方法:

        public void apply() {
            final long startTime = System.currentTimeMillis();
            //和commit一樣剪撬,也調(diào)用了commitToMemory同步mModified
            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);
                    }
                };
            //也調(diào)用了enqueueDiskWrite將數(shù)據(jù)寫到文件中
            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

            // Okay to notify the listeners before it's hit disk
            // because the listeners should always get the same
            // SharedPreferences instance back, which has the
            // changes reflected in memory.
             notifyListeners(mcr);
        } 

apply的調(diào)用邏輯和commit是一樣的都是先將mModified的數(shù)據(jù)同步給mMap摄乒。
到這里開(kāi)頭的第一個(gè)問(wèn)題就有了答案:
不管是commit還是apply,最后都是將Editor的修改的數(shù)據(jù)同步賦值給SP的mMap對(duì)象之后才會(huì)做寫文件的操作残黑,所以apply時(shí)立即讀數(shù)據(jù)馍佑,讀的是內(nèi)存mMap的數(shù)據(jù),不會(huì)等待寫完了然后再讀文件梨水,要等待也只是等待同步的過(guò)程拭荤。

apply調(diào)用enqueueDiskWrite寫文件時(shí)postWriteRunnable不為null,直接跳到 enqueueDiskWrite方法的QueuedWork.queue處疫诽,將任務(wù)進(jìn)隊(duì)列了舅世,而QueuedWork是什么?結(jié)合apply的異步寫功能奇徒,很容易想到它是一個(gè)排隊(duì)執(zhí)行的異步線程雏亚,QueuedWork如下:

public class QueuedWork {

    private static final Object sLock = new Object();

    private static Object sProcessingWork = new Object();

    private static Handler sHandler = null;

    private static final LinkedList<Runnable> sWork = new LinkedList<>();

   //新建HandlerThread 線程以及一個(gè)對(duì)應(yīng)的QueuedWorkHandler
    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;
        }
    }

 

  //進(jìn)隊(duì)列方法,將work保存到sWork中摩钙,并通知使用handler去通知handlerThread執(zhí)行
    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);
            }
        }
    }

    /**
     * @return True iff there is any {@link #queue async work queued}.
     */
    public static boolean hasPendingWork() {
        synchronized (sLock) {
            return !sWork.isEmpty();
        }
    }
    //取出sWork中的runnable挨個(gè)執(zhí)行
    private static void processPendingWork() {
        long startTime = 0;

        if (DEBUG) {
            startTime = System.currentTimeMillis();
        }

        synchronized (sProcessingWork) {
            LinkedList<Runnable> work;

            synchronized (sLock) {
                work = (LinkedList<Runnable>) sWork.clone();
                sWork.clear();

                // Remove all msg-s as all work will be processed now
                getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
            }

            if (work.size() > 0) {
                for (Runnable w : work) {
                    w.run();
                }

                if (DEBUG) {
                    Log.d(LOG_TAG, "processing " + work.size() + " items took " +
                            +(System.currentTimeMillis() - startTime) + " ms");
                }
            }
        }
    }


  /**
     * Trigger queued work to be processed immediately. The queued work is processed on a separate
     * thread asynchronous. While doing that run and process all finishers on this thread. The
     * finishers can be implemented in a way to check weather the queued work is finished.
     *
     * Is called from the Activity base class's onPause(), after BroadcastReceiver's onReceive,
     * after Service command handling, etc. (so async work is never lost)
     */
    public static void waitToFinish() {
        long startTime = System.currentTimeMillis();
        boolean hadMessages = false;

        Handler handler = getHandler();

        synchronized (sLock) {
            if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
                // Delayed work will be processed at processPendingWork() below
                handler.removeMessages(QueuedWorkHandler.MSG_RUN);

                if (DEBUG) {
                    hadMessages = true;
                    Log.d(LOG_TAG, "waiting");
                }
            }

            // We should not delay any work as this might delay the finishers
            sCanDelay = false;
        }

        StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
        try {
            processPendingWork();
        } finally {
            StrictMode.setThreadPolicy(oldPolicy);
        }

    }


    private static class QueuedWorkHandler extends Handler {
        static final int MSG_RUN = 1;

        QueuedWorkHandler(Looper looper) {
            super(looper);
        }

        public void handleMessage(Message msg) {
            if (msg.what == MSG_RUN) {
                processPendingWork();
            }
        }
    }
}

QueuedWork封裝了HandlerThread罢低,queue方法將runnable保存到sWork中,并通知使用handler去通知handlerThread取sWork中的runnable執(zhí)行胖笛,最后在子線程調(diào)用processPendingWork取出sWork中的runnable挨個(gè)執(zhí)行奕短,這時(shí)退出應(yīng)用會(huì)怎么樣?
QueuedWork有一個(gè)waitToFinish方法匀钧,看這個(gè)方法名大致知道它是退出前的被調(diào)用的方法翎碑,它被調(diào)用地方如下圖:


調(diào)用waitToFinish的地方

一目了然,在Activity之斯、Service stop的時(shí)候會(huì)調(diào)用這個(gè)方法日杈。再看看這個(gè)方法的實(shí)現(xiàn)遣铝,等待HandlerThread中的上一個(gè)runnable結(jié)束后,判斷這個(gè)線程的Looper的隊(duì)列中是否有消息莉擒,如果有就接管這個(gè)事件酿炸,取sWork中所有的Runnable在主線程執(zhí)行。

所以涨冀,退出Activity填硕、退出應(yīng)用不會(huì)導(dǎo)致apply的數(shù)據(jù)丟失,它會(huì)在退出時(shí)將異步線程切換到主線程來(lái)執(zhí)行鹿鳖,等待數(shù)據(jù)都保存了才會(huì)退出扁眯。

參考 : SharedPreferences的使用及源碼淺析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市翅帜,隨后出現(xiàn)的幾起案子姻檀,更是在濱河造成了極大的恐慌,老刑警劉巖涝滴,帶你破解...
    沈念sama閱讀 211,561評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件绣版,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡歼疮,警方通過(guò)查閱死者的電腦和手機(jī)杂抽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)韩脏,“玉大人默怨,你說(shuō)我怎么就攤上這事≈杷兀” “怎么了匙睹?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,162評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)济竹。 經(jīng)常有香客問(wèn)我痕檬,道長(zhǎng),這世上最難降的妖魔是什么送浊? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,470評(píng)論 1 283
  • 正文 為了忘掉前任梦谜,我火速辦了婚禮,結(jié)果婚禮上袭景,老公的妹妹穿的比我還像新娘唁桩。我一直安慰自己,他們只是感情好耸棒,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,550評(píng)論 6 385
  • 文/花漫 我一把揭開(kāi)白布荒澡。 她就那樣靜靜地躺著,像睡著了一般与殃。 火紅的嫁衣襯著肌膚如雪单山。 梳的紋絲不亂的頭發(fā)上碍现,一...
    開(kāi)封第一講書(shū)人閱讀 49,806評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音米奸,去河邊找鬼昼接。 笑死,一個(gè)胖子當(dāng)著我的面吹牛悴晰,可吹牛的內(nèi)容都是我干的慢睡。 我是一名探鬼主播,決...
    沈念sama閱讀 38,951評(píng)論 3 407
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼铡溪,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼漂辐!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起佃却,我...
    開(kāi)封第一講書(shū)人閱讀 37,712評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤者吁,失蹤者是張志新(化名)和其女友劉穎窘俺,沒(méi)想到半個(gè)月后饲帅,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,166評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡瘤泪,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,510評(píng)論 2 327
  • 正文 我和宋清朗相戀三年灶泵,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片对途。...
    茶點(diǎn)故事閱讀 38,643評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡赦邻,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出实檀,到底是詐尸還是另有隱情惶洲,我是刑警寧澤,帶...
    沈念sama閱讀 34,306評(píng)論 4 330
  • 正文 年R本政府宣布膳犹,位于F島的核電站恬吕,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏须床。R本人自食惡果不足惜铐料,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,930評(píng)論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望豺旬。 院中可真熱鬧钠惩,春花似錦、人聲如沸族阅。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,745評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)坦刀。三九已至举塔,卻和暖如春绑警,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背央渣。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,983評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工计盒, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人芽丹。 一個(gè)月前我還...
    沈念sama閱讀 46,351評(píng)論 2 360
  • 正文 我出身青樓北启,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親拔第。 傳聞我的和親對(duì)象是個(gè)殘疾皇子咕村,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,509評(píng)論 2 348

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

  • Android 存儲(chǔ)優(yōu)化系列專題 SharedPreferences 系列 《Android 之不要濫用 Shar...
    godliness閱讀 75,918評(píng)論 12 78
  • Android開(kāi)發(fā)中,我們經(jīng)常會(huì)用到SharedPreferences蚊俺,它是一種輕量的數(shù)據(jù)存儲(chǔ)方式懈涛,通常用...
    viclee_d9af閱讀 561評(píng)論 0 0
  • 昨天已經(jīng)看到NioEventLoop的執(zhí)行是在SingleThreadEventExecutor.this.run...
    Terminalist閱讀 301評(píng)論 0 0
  • Android 五種數(shù)據(jù)存儲(chǔ)的方式分別為: SharedPreferences:以Map形式存放簡(jiǎn)單的配置參數(shù); ...
    ghroost閱讀 12,578評(píng)論 0 23
  • 談到外包泳猬,就想到了現(xiàn)如今各大企業(yè)紛紛外包事務(wù)性工作批钠,用以節(jié)省成本。我是個(gè)財(cái)務(wù)工作者得封,財(cái)務(wù)領(lǐng)域很常見(jiàn)的現(xiàn)象便是一些大...
    抱貓碼字閱讀 144評(píng)論 3 5