Android SharedPreferences的理解與使用

Android 五種數據存儲的方式分別為:

  1. SharedPreferences:以Map形式存放簡單的配置參數;
  2. ContentProvider:將應用的私有數據提供給其他應用使用;
  3. 文件存儲:以IO流形式存放,可分為手機內部和手機外部(sd卡等)存儲,可存放較大數據;
  4. SQLite:輕量級、跨平臺數據庫,將所有數據都是存放在手機上的單一文件內,占用內存?。?/li>
  5. 網絡存儲 :數據存儲在服務器上,通過連接網絡獲取數據;

Sharedpreferences是Android平臺上一個輕量級的存儲類,用來保存應用程序的各種配置信息,其本質是一個以“鍵-值”對的方式保存數據的xml文件,其文件保存在/data/data/<package name>/shared_prefs目錄下。在全局變量上看,其優點是不會產生Application 、 靜態變量的OOM(out of memory)和空指針問題,其缺點是效率沒有上面的兩種方法高。

1.獲取SharedPreferences

要想使用 SharedPreferences 來存儲數據,首先需要獲取到 SharedPreferences 對象。Android中主要提供了三種方法用于得到 SharedPreferences 對象。
?1. Context 類中的 getSharedPreferences()方法:
?此方法接收兩個參數,第一個參數用于指定 SharedPreferences 文件的名稱,如果指定的文件不存在則會創建一個,第二個參數用于指定操作模式,主要有以下幾種模式可以選擇。MODE_PRIVATE 是默認的操作模式,和直接傳入 0 效果是相同的。
?MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE 這兩種模式已在 Android 4.2 版本中被廢棄。

Context.MODE_PRIVATE: 指定該SharedPreferences數據只能被本應用程序讀、寫;
Context.MODE_WORLD_READABLE:  指定該SharedPreferences數據能被其他應用程序讀,但不能寫;
Context.MODE_WORLD_WRITEABLE:  指定該SharedPreferences數據能被其他應用程序讀;
Context.MODE_APPEND:該模式會檢查文件是否存在,存在就往文件追加內容,否則就創建新文件;

2. Activity 類中的 getPreferences()方法:
?這個方法和 Context 中的 getSharedPreferences()方法很相似,不過它只接收一個操作模式參數,因為使用這個方法時會自動將當前活動的類名作為 SharedPreferences 的文件名。

3. PreferenceManager 類中的 getDefaultSharedPreferences()方法:
?這是一個靜態方法,它接收一個 Context 參數,并自動使用當前應用程序的包名作為前綴來命名 SharedPreferences 文件。

2.SharedPreferences的使用

SharedPreferences對象本身只能獲取數據而不支持存儲和修改,存儲修改是通過SharedPreferences.edit()獲取的內部接口Editor對象實現。使用Preference來存取數據,用到了SharedPreferences接口和SharedPreferences的一個內部接口SharedPreferences.Editor,這兩個接口在android.content包中;

 1)寫入數據:
     //步驟1:創建一個SharedPreferences對象
     SharedPreferences sharedPreferences= getSharedPreferences("data",Context.MODE_PRIVATE);
     //步驟2: 實例化SharedPreferences.Editor對象
     SharedPreferences.Editor editor = sharedPreferences.edit();
     //步驟3:將獲取過來的值放入文件
     editor.putString("name", “Tom”);
     editor.putInt("age", 28);
     editor.putBoolean("marrid",false);
     //步驟4:提交               
     editor.commit();

 2)讀取數據:
     SharedPreferences sharedPreferences= getSharedPreferences("data", Context .MODE_PRIVATE);
     String userId=sharedPreferences.getString("name","");
  
3)刪除指定數據
     editor.remove("name");
     editor.commit();

4)清空數據
     editor.clear();
     editor.commit();

注意:如果在 Fragment 中使用SharedPreferences 時,需要放在onAttach(Activity activity)里面進行SharedPreferences的初始化,否則會報空指針 即 getActivity()會可能返回null !

讀寫其他應用的SharedPreferences 步驟如下(未實踐):
?1. 在創建SharedPreferences時,指定MODE_WORLD_READABLE模式,表明該SharedPreferences數據可以被其他程序讀??;
?2. 創建其他應用程序對應的Context;
?3. 使用其他程序的Context獲取對應的SharedPreferences;
?4. 如果是寫入數據,使用Editor接口即可,所有其他操作均和前面一致;

try {
//這里的com.example.mpreferences 就是應用的包名
 Context mcontext = createPackageContext("com.example.mpreferences", CONTEXT_IGNORE_SECURITY);

 SharedPreferences msharedpreferences = mcontext.getSharedPreferences("name_preference", MODE_PRIVATE);
 int count = msharedpreferences.getInt("count", 0);

 } catch (PackageManager.NameNotFoundException e) {
       e.printStackTrace();
 }

3. SharedPreferences 的源碼分析(API 25)

先從Context的getSharedPreferences開始:

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

我們知道Android中的Context類其實是使用了裝飾者模式,而被裝飾對象其實就是一個ContextImpl對象,ContextImpl的getSharedPreferences方法:

    /**
     * Map from preference name to generated path.
     * 從preference名稱到生成路徑的映射;
     */
    @GuardedBy("ContextImpl.class")
    private ArrayMap<String, File> mSharedPrefsPaths;

    /**
     * Map from package name, to preference name, to cached preferences.
     * 從包名映射到preferences,以緩存preferences,這是個靜態變量;
     */
    @GuardedBy("ContextImpl.class")
    private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;

    @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);
    }


    @Override
    public SharedPreferences getSharedPreferences(File file, int mode) {
        checkMode(mode);
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
            sp = cache.get(file);
            if (sp == null) {
                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;
    }

    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);
        }

        return packagePrefs;
    }

從上面我們可以看出他們之間的關系:
mSharedPrefsPaths存放的是名稱與文件夾的映射(ArrayMap<String, File>),這里的名稱就是我們使用getSharedPreferences時傳入的name,如果mSharedPrefsPaths為null則初始化,如果file為null則新建一個File并將其加入mSharedPrefsPaths中;

( ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> )
sSharedPrefsCache 存放包名與ArrayMap鍵值對
初始化時會默認以包名作為鍵值對中的Key,注意這是個static變量;

(ArrayMap<File, SharedPreferencesImpl>)sSharedPrefs
packagePrefs存放文件name與SharedPreferencesImpl鍵值對

image.png

注意:

  1. 對于一個相同的SharedPreferences name,獲取到的都是同一個SharedPreferences對象,它其實是SharedPreferencesImpl對象。
  2. sSharedPrefs在程序中是靜態的,如果退出了程序但Context沒有被清掉,那么下次進入程序仍然可能取到本應被刪除掉的值。而換了另一種清除SharedPreferences的方式:使用SharedPreferences.Editor的commit方法能夠起作用,調用后不退出程序都馬上生效。

SharedPreferencesImpl對象

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

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

    private void loadFromDisk() {
        synchronized (SharedPreferencesImpl.this) {
            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 map = null;
        StructStat stat = null;
        try {
            stat = Os.stat(mFile.getPath());
            if (mFile.canRead()) {
                BufferedInputStream str = null;
                try {
                    str = new BufferedInputStream(
                            new FileInputStream(mFile), 16*1024);
                    map = XmlUtils.readMapXml(str);
                } catch (XmlPullParserException | IOException e) {
                    Log.w(TAG, "getSharedPreferences", e);
                } finally {
                    IoUtils.closeQuietly(str);
                }
            }
        } catch (ErrnoException e) {
            /* ignore */
        }

        synchronized (SharedPreferencesImpl.this) {
            mLoaded = true;
            if (map != null) {
                mMap = map;
                mStatTimestamp = stat.st_mtime;
                mStatSize = stat.st_size;
            } else {
                mMap = new HashMap<>();
            }
            notifyAll();
        }
    }

可以看到對于一個SharedPreferences文件name,第一次調用getSharedPreferences時會去創建一個SharedPreferencesImpl對象,它會開啟一個子線程,然后去把指定的SharedPreferences文件中的鍵值對全部讀取出來,存放在一個Map中。

調用getString時那個SharedPreferencesImpl構造方法開啟的子線程可能還沒執行完(比如文件比較大時全部讀取會比較久),這時getString當然還不能獲取到相應的值,必須阻塞到那個子線程讀取完為止,如getString方法:

    @Nullable
    public String getString(String key, @Nullable String defValue) {
        synchronized (this) {
            awaitLoadedLocked();
            String v = (String)mMap.get(key);
            return v != null ? v : defValue;
        }
    }

   private void awaitLoadedLocked() {
        if (!mLoaded) {
            // Raise an explicit StrictMode onReadFromDisk for this
            // thread, since the real read will be in a different
            // thread and otherwise ignored by StrictMode.
            BlockGuard.getThreadPolicy().onReadFromDisk();
        }
        while (!mLoaded) {
            try {
                wait();
            } catch (InterruptedException unused) {
            }
        }
    }

顯然這個awaitLoadedLocked方法就是用來等this這個鎖的,在loadFromDiskLocked方法的最后我們也可以看到它調用了notifyAll方法,這時如果getString之前阻塞了就會被喚醒。那么這里會存在一個問題,我們的getString是寫在UI線程中,如果那個getString被阻塞太久了,比如60s,這時就會出現ANR,所以要根據具體情況考慮是否需要把SharedPreferences的讀寫放在子線程中。

關于mBackupFile,SharedPreferences在寫入時會先把之前的xml文件改成名成一個備份文件,然后再將要寫入的數據寫到一個新的文件中,如果這個過程執行成功的話,就會把備份文件刪除。由此可見每次即使只是添加一個鍵值對,也會重新寫入整個文件的數據,這也說明SharedPreferences只適合保存少量數據,文件太大會有性能問題。

注意:

  1. 在UI線程中調用getXXX可能會導致ANR。
  2. 我們在初始化SharedPreferencesImpl對象時會加SharedPreferencesImpl對應的xml文件中的所有數據都加載到內存中,如果xml文件很大,將會占用大量的內存,我們只想讀取xml文件中某個key的值,但我們獲取它的時候是會加載整個文件。
  3. 每添加一個鍵值對,都會重新寫入整個文件的數據,不是增量寫入;
    綜上原因能說明Sharedpreferences只適合做輕量級的存儲。

SharedPreferences的內部類Editor

   SharedPreferences sharedPreferences= getSharedPreferences("data",Context.MODE_PRIVATE);
   SharedPreferences.Editor editor = sharedPreferences.edit();
   editor.putString("name", “Tom”);

   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 (this) {
            awaitLoadedLocked();
        }

        return new EditorImpl();
    }

其實拿到的是一個EditorImpl對象,它是SharedPreferencesImpl的內部類:

  public final class EditorImpl implements Editor {
        private final Map<String, Object> mModified = Maps.newHashMap();
        private boolean mClear = false;

        public Editor putString(String key, @Nullable String value) {
            synchronized (this) {
                mModified.put(key, value);
                return this;
            }
        }
          .....
}

可以看到它有一個Map對象mModified,用來保存“修改的數據”,也就是你每次put的時候其實只是把那個鍵值對放到這個mModified 中,最后調用apply或者commit才會真正把數據寫入文件中,如上面的putString方法,其它putXXX代碼基本也是一樣的。

commit方法和apply方法的不同

    public void apply() {
            final MemoryCommitResult mcr = commitToMemory();
            final Runnable awaitCommit = new Runnable() {
                    public void run() {
                        try {
                            mcr.writtenToDiskLatch.await();
                        } catch (InterruptedException ignored) {
                        }
                    }
                };

            QueuedWork.add(awaitCommit);

            Runnable postWriteRunnable = new Runnable() {
                    public void run() {
                        awaitCommit.run();
                        QueuedWork.remove(awaitCommit);
                    }
                };

            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);
        }

    public boolean commit() {
            MemoryCommitResult mcr = commitToMemory();
            SharedPreferencesImpl.this.enqueueDiskWrite(
                mcr, null /* sync write on this thread okay */);
            try {
                mcr.writtenToDiskLatch.await();
            } catch (InterruptedException e) {
                return false;
            }
            notifyListeners(mcr);
            return mcr.writeToDiskResult;
        }

    // Return value from EditorImpl#commitToMemory()
    private static class MemoryCommitResult {
        public boolean changesMade;  // any keys different?
        public List<String> keysModified;  // may be null
        public Set<OnSharedPreferenceChangeListener> listeners;  // may be null
        public Map<?, ?> mapToWriteToDisk;
        public final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
        public volatile boolean writeToDiskResult = false;

        public void setDiskWriteResult(boolean result) {
            writeToDiskResult = result;
            writtenToDiskLatch.countDown();
        }
    }

兩種方式首先都會先使用commitTomemory函數將修改的內容寫入到SharedPreferencesImpl當中,再調用enqueueDiskWrite寫磁盤操作,commitToMemory就是產生一個“合適”的MemoryCommitResult對象mcr,然后調用enqueueDiskWrite時需要把這個對象傳進去,commitToMemory方法:

        // Returns true if any changes were made
        private MemoryCommitResult commitToMemory() {
            MemoryCommitResult mcr = new MemoryCommitResult();
            synchronized (SharedPreferencesImpl.this) {
                // We optimistically don't make a deep copy until
                // a memory commit comes in when we're already
                // writing to disk.
                if (mDiskWritesInFlight > 0) {
                    // 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);
                }
                mcr.mapToWriteToDisk = mMap;
                mDiskWritesInFlight++;

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

                synchronized (this) {
                    if (mClear) {
                        if (!mMap.isEmpty()) {
                            mcr.changesMade = true;
                            mMap.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) {
                            if (!mMap.containsKey(k)) {
                                continue;
                            }
                            mMap.remove(k);
                        } else {
                            if (mMap.containsKey(k)) {
                                Object existingValue = mMap.get(k);
                                if (existingValue != null && existingValue.equals(v)) {
                                    continue;
                                }
                            }
                            mMap.put(k, v);
                        }

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

                    mModified.clear();
                }
            }
            return mcr;
        }

這里需要弄清楚兩個對象mMap和mModified,mMap是存放當前SharedPreferences文件中的鍵值對,而mModified是存放此時edit時put進去的鍵值對。mDiskWritesInFlight表示正在等待寫的操作數量。
可以看到這個方法中首先處理了clear標志,它調用的是mMap.clear(),然后再遍歷mModified將新的鍵值對put進mMap,也就是說在一次commit事務中,如果同時put一些鍵值對和調用clear后再commit,那么clear掉的只是之前的鍵值對,這次put進去的鍵值對還是會被寫入的。
遍歷mModified時,需要處理一個特殊情況,就是如果一個鍵值對的value是this(SharedPreferencesImpl)或者是null那么表示將此鍵值對刪除,這個在remove方法中可以看到,如果之前有同樣的key且value不同則用新的valu覆蓋舊的value,如果沒有存在同樣的key則完整寫入。需要注意的是這里使用了同步鎖住edtor對象,保證了當前數據正確存入。

 public Editor remove(String key) {
            synchronized (this) {
                mModified.put(key, this);
                return this;
            }
        }

        public Editor clear() {
            synchronized (this) {
                mClear = true;
                return this;
            }
        }

commit接下來就是調用enqueueDiskWrite方法:

private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
        final Runnable writeToDiskRunnable = new Runnable() {
                public void run() {
                    synchronized (mWritingToDiskLock) {
                        writeToFile(mcr);
                    }
                    synchronized (SharedPreferencesImpl.this) {
                        mDiskWritesInFlight--;
                    }
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
            };

        final boolean isFromSyncCommit = (postWriteRunnable == null);

        // Typical #commit() path with fewer allocations, doing a write on
        // the current thread.
        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            synchronized (SharedPreferencesImpl.this) {
                wasEmpty = mDiskWritesInFlight == 1;
            }
            if (wasEmpty) {
                writeToDiskRunnable.run();
                return;
            }
        }

        QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
    }

定義一個Runnable任務,在Runnable中先調用writeToFile進行寫操作,寫操作需要先獲得mWritingToDiskLock,也就是寫鎖。然后執行mDiskWritesInFlight–,表示正在等待寫的操作減少1。
判斷postWriteRunnable是否為null,調用commit時它為null,而調用apply時它不為null。isFromSyncCommit為true,而且有1個寫操作需要執行,那么就調用writeToDiskRunnable.run(),注意這個調用是在當前線程中進行的。如果不是commit,那就是apply,這時調用QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable),這個QueuedWork類其實很簡單,里面有一個SingleThreadExecutor,用于異步執行這個writeToDiskRunnable,commit的寫操作是在調用線程中執行的,而apply內部是用一個單線程的線程池實現的,因此寫操作是在子線程中執行的。

commit和apply的總結:

  1. apply沒有返回值而commit返回boolean表明修改是否提交成功 ;
  2. commit是把內容同步提交到硬盤的,而apply先立即把修改提交到內存,然后開啟一個異步的線程提交到硬盤,并且如果提交失敗,你不會收到任何通知。
  3. 所有commit提交是同步過程,效率會比apply異步提交的速度慢,在不關心提交結果是否成功的情況下,優先考慮apply方法。
  4. apply是使用異步線程寫入磁盤,commit是同步寫入磁盤。所以我們在主線程使用的commit的時候,需要考慮是否會出現ANR問題。(不適合大量數據存儲)

4. 查看Sharedpreferencesd 保存數據的xml文件

要想查看data文件首先要獲取手機root權限,成功root后,修改data權限即可查看data里面的數據庫。由于在xml文件內可以很清楚的查看到各個鍵-值”對數據,所以用Sharedpreferencesd保存比較重要的數據的時候最好先加密再保存。成功查看如下圖所示:

Paste_Image.png
Paste_Image.png

data權限修改辦法:

1. 打開cmd;
2. 輸入’adb shell’;
3. 輸入su,回車 ;
4. 輸入chmod 777 /data/data/<package name>/shared_prefs 回車
該步驟設置data文件夾權限為777(drwxrwxrwx,也即administrators、power users和users組都有對該文件夾的讀、寫、運行權限) ;

當你在Linux下用命令ll 或者ls -la的時候會看到類似drwxr-xr-x這樣標識,具體代表什么意思呢?
?這段標識總長度為10位(10個‘-’),第一位表示文件類型,如該文件是文件(用-表示),如該文件是文件夾(用d表示),如該文件是連接文件(用l表示),后面9個按照三個一組分,第一組:用戶權限,第二組:組權限,第三組:其他權限。 每一組是三位,分別是讀 r ,寫 w,執行 x,這些權限都可以用數字來表示:r 4, w 2 , x 1。如果沒有其中的某個權限則用‘-’表示。例如:
?1. -rwxrwx---,第一位‘-’代表的是文件,第二位到第四位rwx代表此文件的擁有者有讀、寫、執行的權限,同組用戶也有讀、寫、及執行權限,其他用戶組沒任何權限。用數字來表示的話則是770.
?2. drwx------,第一位‘d’代表的是文件夾,第二位到第四位rwx代表此文件夾的擁有者有讀、寫、執行的權限,第五位到第七位代表的是擁有者同組用戶的權限,同組用戶沒有任何權限,第八位到第十位代表的是其他用戶的權限,其他用戶也沒有任何權限。用數字來表示的話則是700.

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

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,662評論 25 708
  • ¥開啟¥ 【iAPP實現進入界面執行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個線程,因...
    小菜c閱讀 6,483評論 0 17
  • 面試題總結 通用 安卓學習途徑, 尋找資料學習的博客網站 AndroidStudio使用, 插件使用 安卓和蘋果的...
    JingBeibei閱讀 1,706評論 2 21
  • 簡書上的寫作者,每天都在花大量的時間寫作,每天耗費的用戶總時間估計有幾年。 簡書上每天產生著成千上萬的文字,連首頁...
    安和然閱讀 953評論 3 11
  • 最近又刮起了寵物當店長的風潮,這只柯基犬也不例外。位于日本東京市的一個茶屋里,其店長便是由這只四歲大的柯基Haru...
    小葵圖文同步閱讀 504評論 0 0