Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android-SharedPreferences源碼學習與最佳實踐

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

編輯:關於Android編程

最近有個任務是要做應用啟動時間優化,然後記錄系統啟動的各個步驟所占用的時間,發現有一個方法是操作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;//在內存的一份緩存

@Override
public 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-v
private 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 made
private 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
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved