Android-SharedPreferences源碼學習與最佳實務

來源:互聯網
上載者:User

最近有個任務是要做應用啟動時間最佳化,然後記錄系統啟動的各個步驟所佔用的時間,發現有一個方法是操作SharedPreferences的,裡面僅僅是讀了2個key,然後更新一下值,然後再寫回去,耗時竟然在500ms以上(應用初次安裝的時候),感到非常吃驚。以前只是隱約的知道SharedPreferences是跟硬碟上的一個xml檔案對應的,具體的實現還真沒研究過,下面我們就來看看SharedPreferences到底是個什麼玩意,為什麼效率會這麼低?

SharedPreferences是存放在ContextImpl裡面的,所以先看寫ContextImpl這個類:

ContextImpl.java(https://github.com/android/platform_frameworks_base/blob/master/core/java/android/app/ContextImpl.java):

/**   * Map from package name, to preference name, to cached preferences.   */private static ArrayMap> sSharedPrefs;//在記憶體的一份緩衝@Overridepublic SharedPreferences getSharedPreferences(String name, int mode) {        SharedPreferencesImpl sp;        synchronized (ContextImpl.class) {//同步的            if (sSharedPrefs == null) {                sSharedPrefs = new ArrayMap>();            }            final String packageName = getPackageName();            ArrayMap packagePrefs = sSharedPrefs.get(packageName);            if (packagePrefs == null) {                packagePrefs = new ArrayMap();                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;    }
getSharedPreferences()做的事情很簡單,一目瞭然,我們重點看下SharedPreferencesImpl.java這個類:
SharedPreferencesImpl.java(https://github.com/android/platform_frameworks_base/blob/master/core/java/android/app/SharedPreferencesImpl.java)
首先是建構函式:

SharedPreferencesImpl(File file, int mode) {        mFile = file;//這個是硬碟上的檔案        mBackupFile = makeBackupFile(file);//這個是備份檔案,當mFile出現crash的時候,會使用mBackupFile來替換        mMode = mode;//這個是開啟檔案        mLoaded = false;//這個是一個標誌位,檔案是否載入完成,因為檔案的載入是一個非同步過程        mMap = null;//儲存資料用        startLoadFromDisk();//開始從硬碟非同步載入}//還兩個很重要的成員:private int mDiskWritesInFlight = 0;  //有多少批次沒有commit到disk的寫操作,每個批次可能會對應多個k-vprivate final Object mWritingToDiskLock = new Object();//寫硬碟檔案時候加鎖
//從硬碟載入private void startLoadFromDisk() {        synchronized (this) {//先把狀態置為未載入            mLoaded = false;        }        new Thread("SharedPreferencesImpl-load") {//開了一個線程,非同步載入            public void run() {                synchronized (SharedPreferencesImpl.this) {                    loadFromDiskLocked();//由SharedPreferencesImpl.this鎖保護                }            }        }.start();    }
//從硬碟載入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 = Libcore.os.stat(mFile.getPath());            if (mFile.canRead()) {                BufferedInputStream str = null;                try {                    str = new BufferedInputStream(new FileInputStream(mFile), 16*1024);//從硬碟把資料讀出來                    map = XmlUtils.readMapXml(str);//做xml解析                } 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;  //儲存到mMap            mStatTimestamp = stat.st_mtime;//記錄檔案的時間戳記            mStatSize = stat.st_size;//記錄檔案的大小        } else {            mMap = new HashMap();        }        notifyAll();//喚醒等待線程    }
然後我們隨便看一個讀請求:

 public int getInt(String key, int defValue) {        synchronized (this) {//還是得首先擷取this鎖            awaitLoadedLocked(); //這一步完成以後,說明肯定已經載入完了            Integer v = (Integer)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) {            }        }    }
看一下寫操作,寫是通過Editor來做的:

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.//注釋很有意思,擷取edit的時候,可以把這個同步去掉,但是如果去掉就需要在Editor上做一些工作(???)。//但是,好處是context.getSharedPreferences(..).edit().putString(..).apply()整個過程都不阻塞        synchronized (this) {//還是先等待載入完成            awaitLoadedLocked();        }        return new EditorImpl();//返回一個EditorImpl,它是一個內部類    }
public final class EditorImpl implements Editor {//寫操作暫時會把資料放在這裡面        private final Map mModified = Maps.newHashMap();//由this鎖保護//是否要清空所有的preferences private boolean mClear = false;public Editor putInt(String key, int value) {            synchronized (this) {//首先擷取this鎖                mModified.put(key, value);//並不是直接修改mMap,而是放到mModified裡面                return this;            }        }}
看一下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()這個方法主要是用來更新記憶體緩衝的mMap:

// Returns true if any changes were madeprivate MemoryCommitResult commitToMemory() {    MemoryCommitResult mcr = new MemoryCommitResult();    synchronized (SharedPreferencesImpl.this) { //加SharedPreferencesImpl鎖,寫記憶體的時候不允許讀        // We optimistically don't make a deep copy until a memory commit comes in when we're already writing to disk.        if (mDiskWritesInFlight > 0) {//如果存在沒有提交的寫, mDiskWritesInFlight是SharedPreferences的成員變數             // We can't modify our mMap as a currently in-flight write owns it.  Clone it before modifying it.            // noinspection unchecked            mMap = new HashMap(mMap);//clone一個mMap,沒明白!        }        mcr.mapToWriteToDisk = mMap;        mDiskWritesInFlight++;//批次數目加1        boolean hasListeners = mListeners.size() > 0;        if (hasListeners) {            mcr.keysModified = new ArrayList();            mcr.listeners = new HashSet(mListeners.keySet());        }        synchronized (this) {//對當前的Editor加鎖            if (mClear) {//只有當調用了clear()才會把這個值置為true                if (!mMap.isEmpty()) {//如果mMap不是空                    mcr.changesMade = true;                    mMap.clear();//清空mMap。mMap裡面存的是整個的Preferences                }                mClear = false;            }            for (Map.Entry e : mModified.entrySet()) {//遍曆所有要commit的entry                String k = e.getKey();                Object v = e.getValue();                if (v == this) {  // magic value for a removal mutation                    if (!mMap.containsKey(k)) {                        continue;                    }                    mMap.remove(k);                } else {                    boolean isSame = false;                    if (mMap.containsKey(k)) {                        Object existingValue = mMap.get(k);                        if (existingValue != null && existingValue.equals(v)) {                            continue;                        }                    }                    mMap.put(k, v);//這裡是往裡面放,因為最外層有對SharedPreferencesImpl.this加鎖,寫是沒問題的                }                mcr.changesMade = true;                if (hasListeners) {                    mcr.keysModified.add(k);                }            }            mModified.clear();//清空editor        }    }    return mcr;}
//這是隨後的寫硬碟 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);//如果是commit,postWriteRunnable是null        // Typical #commit() path with fewer allocations, doing a write on        // the current thread.        if (isFromSyncCommit) {//如果是調用的commit            boolean wasEmpty = false;            synchronized (SharedPreferencesImpl.this) {                wasEmpty = mDiskWritesInFlight == 1;//如果只有一個批次等待寫入            }            if (wasEmpty) {                writeToDiskRunnable.run();//不用另起線程,直接在當前線程執行,很nice的最佳化!                return;            }        }//如果不是調用的commit,會走下面的分支//如或有多個批次等待寫入,另起線程來寫,從方法名可以看出來也是串列的寫,寫檔案本來就應該串列!        QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);    }
看下writeToDiskRunnable都幹了些什麼:

final Runnable writeToDiskRunnable = new Runnable() {//這是工作在另一個線程        public void run() {            synchronized (mWritingToDiskLock) {//mWritingToDiskLock是SharedPreferencesImpl的成員變數,保證單線程寫檔案,       //不能用this鎖是因為editor上可能會存在多個commit或者apply       //也不能用SharedPreferences鎖,因為會阻塞讀,不錯!                writeToFile(mcr);//寫到檔案            }            synchronized (SharedPreferencesImpl.this) {                mDiskWritesInFlight--;//批次減1            }            if (postWriteRunnable != null) {                postWriteRunnable.run();//這個是寫完以後的回調            }        }    };
下面是真正要寫硬碟了:

// Note: must hold mWritingToDiskLock    private void writeToFile(MemoryCommitResult mcr) {        // Rename the current file so it may be used as a backup during the next read        if (mFile.exists()) {            if (!mcr.changesMade) {//如果沒有修改,直接返回                // If the file already exists, but no changes were                // made to the underlying map, it's wasteful to                // re-write the file.  Return as if we wrote it                // out.                mcr.setDiskWriteResult(true);                return;            }            if (!mBackupFile.exists()) {//先備份                if (!mFile.renameTo(mBackupFile)) {                    Log.e(TAG, "Couldn't rename file " + mFile                          + " to backup file " + mBackupFile);                    mcr.setDiskWriteResult(false);                    return;                }            } else {//刪除重建                mFile.delete();            }        }        // Attempt to write the file, delete the backup and return true as atomically as        // possible.  If any exception occurs, delete the new file; next time we will restore        // from the backup.        try {            FileOutputStream str = createFileOutputStream(mFile);            if (str == null) {                mcr.setDiskWriteResult(false);                return;            }            XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);            FileUtils.sync(str);//強制寫到硬碟            str.close();            ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);            try {                final StructStat stat = Libcore.os.stat(mFile.getPath());                synchronized (this) {                    mStatTimestamp = stat.st_mtime;//更新檔案時間戳記                    mStatSize = stat.st_size;//更新檔案大小                }            } catch (ErrnoException e) {                // Do nothing            }            // Writing was successful, delete the backup file if there is one.            mBackupFile.delete();            mcr.setDiskWriteResult(true);            return;        } catch (XmlPullParserException e) {            Log.w(TAG, "writeToFile: Got exception:", e);        } catch (IOException e) {            Log.w(TAG, "writeToFile: Got exception:", e);        }        // Clean up an unsuccessfully written file        if (mFile.exists()) {            if (!mFile.delete()) {                Log.e(TAG, "Couldn't clean up partially-written file " + mFile);            }        }        mcr.setDiskWriteResult(false);    }
public static boolean sync(FileOutputStream stream) {        try {            if (stream != null) {                stream.getFD().sync();//強制寫硬碟            }            return true;        } catch (IOException e) {        }        return false;}
這裡面還有一個跟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);//這個地方傳遞的postWriteRunnable不再是null    // 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);}
我們已經看過enqueueDiskWrite()這個方法了,因為參數postWriteRunnable不是null,最終會執行:
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
這是在單獨的線程上做寫硬碟的操作,寫完以後會回調postWriteRunnable,等待寫硬碟完成!

從上面的代碼可以得出以下結論:
(1)SharedPreferences在第一次載入的時候,會從硬碟非同步讀檔案,然後會在記憶體做緩衝。
(2)SharedPreferences的讀都是讀的記憶體緩衝。
(3)如果是commmit()寫,是先把資料更新到記憶體,然後同步到硬碟,整個過程是在同一個線程中同步來做的。
(4)如果是apply()寫,首先也是寫到記憶體,但是會另起一個線程非同步來寫硬碟。因為我們在讀的時候,是直接從記憶體讀取的,因此,用apply()而不是commit()會提高效能。
(5)如果有多個key要寫入,不要每次都commit或者apply,因為這裡面會存在很多的加鎖操作,更高效的使用方式是這樣:editor.putInt("","").putString("","").putBoolean("","").apply();並且所有的putXXX()的結尾都會返回this,方便鏈式編程。
(6)這裡面有三級的鎖:SharedPreferences,Editor, mWritingToDiskLock。
mWritingToDiskLock是對應硬碟上的檔案,Editor是保護mModified的,SharedPreferences是保護mMap的。
參考:
http://stackoverflow.com/questions/19148282/read-speed-of-sharedpreferences
http://stackoverflow.com/questions/12567077/is-sharedpreferences-access-time-consuming

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.