深入理解Android中的SharedPreferences

前言

SharePreferences每個開發都一直在用,在用法上沒什麼好說的。但是今天看到一篇總結SharePreferences的文章,覺得總結的挺全的。一些平時大家不怎麼關注的點都講到了。

提出問題

SharedPreferences作爲Android中數據存儲方式的一種,我們經常會用到,它適合用來保存那些少量的數據,特別是鍵值對數據,比如配置信息,登錄信息等。不過要想做到正確使用SharedPreferences,就需要弄清楚下面幾個問題:
(1)每次調用getSharedPreferences時都會新建一個SharedPreferences對象嗎?
(2)在UI線程中調用getXXX有可能導致ANR嗎?
(3)爲什麼SharedPreferences只適合用來存放少量數據,SharedPreferences對應的就是普通的xml文件,爲什麼不能存放大量數據?
(4)commit和apply有什麼區別?
(5)SharedPreferences每次寫入時是增量寫入嗎?

源碼分析

要想弄清楚上面幾個問題,需要查看SharedPreferences的源碼。先從Context的getSharedPreferences開始:

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

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

@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
        if (sSharedPrefs == null) {
            sSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>();
        }

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

        // 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";
            }
        }

        sp = packagePrefs.get(name);
        if (sp == null) {
            File prefsFile = getSharedPrefsFile(name);
            sp = new SharedPreferencesImpl(prefsFile, mode);
            packagePrefs.put(name, 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;
}

可以看到這裏使用到了單例模式,sSharedPrefs 是一個ArrayMap,packagePrefs也是一個ArrayMap,它們的關係是這樣的:
packagePrefs存放文件name與SharedPreferencesImpl鍵值對,sSharedPrefs存放包名與ArrayMap鍵值對。注意sSharedPrefs是static變量,也就是一個類只有一個實例,因此你每次getSharedPreferences其實拿到的都是同一個SharedPreferences對象。
這裏回答第一個問題,對於一個相同的SharedPreferences name,獲取到的都是同一個SharedPreferences對象,它其實是SharedPreferencesImpl對象。

SharedPreferencesImpl構造方法:

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

與mBackupFile有關的等後面說,看startLoadFromDisk方法:

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

實際上是調用loadFromDiskLocked方法:

private void loadFromDiskLocked() {
    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 e) {
                Log.w(TAG, "getSharedPreferences", e);
            } catch (FileNotFoundException e) {
                Log.w(TAG, "getSharedPreferences", e);
            } catch (IOException e) {
                Log.w(TAG, "getSharedPreferences", e);
            } finally {
                IoUtils.closeQuietly(str);
            }
        }
    } catch (ErrnoException e) {
    }
    mLoaded = true;
    if (map != null) {
        mMap = map;
        mStatTimestamp = stat.st_mtime;
        mStatSize = stat.st_size;
    } else {
        mMap = new HashMap<String, Object>();
    }
    notifyAll();
}

可以看到對於一個SharedPreferences文件name,第一次調用getSharedPreferences時會去創建一個SharedPreferencesImpl對象,它會開啓一個子線程,然後去把指定的SharedPreferences文件中的鍵值對全部讀取出來,存放在一個Map中。如果我們在UI線程中這樣子寫:

SharedPreferences sp = getSharedPreferences("test", Context.MODE_PRIVATE);
String name = sp.getString("name", null);

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

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

顯然這個awaitLoadedLocked方法就是用來等this這個鎖的,在loadFromDiskLocked方法的最後我們也可以看到它調用了notifyAll方法,這時如果getString之前阻塞了就會被喚醒。那麼現在這裏有一個問題,我們的getString是寫在UI線程中,如果那個getString被阻塞太久了,這時就會出現ANR,因此要根據具體情況考慮是否需要把SharedPreferences的讀寫放在子線程中。
這裏回答第二個問題,在UI線程中調用getXXX可能會導致ANR。同時可以回答第三個問題,SharedPreferences只能用來存放少量數據,如果一個SharedPreferences對應的xml文件很大的話,在初始化時會把這個文件的所有數據都加載到內存中,這樣就會佔用大量的內存,有時我們只是想讀取某個xml文件中一個key的value,結果它把整個文件都加載進來了,顯然如果必要的話這裏需要進行相關優化處理。

SharedPreferences的getXXX的實現基本都是一樣,這裏就不逐個分析了。

SharedPreferences的初始化和讀取比較簡單,寫操作就相對複雜了點,我們知道寫一個SharedPreferences文件都是先要調用edit方法獲取到一個Editor對象:

public Editor edit() {
    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;
    ......
}

可以看到它有一個Map對象mModified,用來保存“髒數據”,也就是你每次put的時候其實是把那個鍵值對放到這個mModified 中,最後調用apply或者commit纔會真正把數據寫入文件中,比如看putString:

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

其它putXXX代碼基本也是一樣的。EditorImpl類的關鍵就是apply和commit,不過它們有一些區別,先看commit方法:

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

關鍵有兩步,先調用commitToMemory,再調用enqueueDiskWrite,commitToMemory就是產生一個“合適”的MemoryCommitResult對象mcr,然後調用enqueueDiskWrite時需要把這個對象傳進去,commitToMemory方法:

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,那麼clear掉的只是之前的鍵值對,這次put進去的鍵值對還是會被寫入的。遍歷mModified時,需要處理一個特殊情況,就是如果一個鍵值對的value是this(SharedPreferencesImpl)或者是null那麼表示將此鍵值對刪除,這個在remove方法中可以看到:

public Editor remove(String key) {
    synchronized (this) {
        mModified.put(key, this);
        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與繼承Thread的區別,Runnable表示一個任務,不一定要在子線程中執行,一般優先考慮使用Runnable。這個Runnable中先調用writeToFile進行寫操作,寫操作需要先獲得mWritingToDiskLock,也就是寫鎖。然後執行mDiskWritesInFlight–,表示正在等待寫的操作減少1。最後判斷postWriteRunnable是否爲null,調用commit時它爲null,而調用apply時它不爲null。
Runnable定義完,就判斷這次是commit還是apply,如果是commit,即isFromSyncCommit爲true,而且有1個寫操作需要執行,那麼就調用writeToDiskRunnable.run(),注意這個調用是在當前線程中進行的。如果不是commit,那就是apply,這時調用QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable),這個QueuedWork類其實很簡單,裏面有一個SingleThreadExecutor,用於異步執行這個writeToDiskRunnable。
這裏就可以回答第四個問題了,commit的寫操作是在調用線程中執行的,而apply內部是用一個單線程的線程池實現的,因此寫操作是在子線程中執行的。

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

SharedPreferences幾種模式:

Context.MODE_PRIVATE:應用私有,只有相同的UID才能進行讀寫
Context.MODE_MULTI_PROCESS:多進程安全標誌,Android2.3之前該標誌是默認被設置的,Android2.3開始需要自己設置。
MODE_APPEND:首次創建時如果文件存在不會刪除文件。
注意這些模式可以使用位與進行設置,比如MODE_PRIVATE | MODE_APPEND。

http://blog.csdn.net/u012619640/article/details/50940074

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章