SharedPreferences 源碼分析及踩坑指南

SharedPreferences 源碼分析及使用事項

作爲Android 輕量級的存儲工具,SharedPreferences被廣泛使用,API 簡潔明瞭,易學易用,爲廣大程序小哥哥們喜聞樂見。殊不知,一片和諧的環境下,蘊藏着不少危機,本文將從源碼角度進行解析,並附上踩過的一些坑。


一般用法

SharedPreferences pref = mAppContext.getSharedPreferences(prefName, Context.MODE_PRIVATE);

源碼分析

Conext#getSharedPreferences的內部實現,具體實現在

ContextImpl.getSharedPreferences()
 @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);//一個context可以有N多SharedPreferences文件
                mSharedPrefsPaths.put(name, file);
            }
        }
        return getSharedPreferences(file, mode);//初始化後,以後從緩存讀取file,再構建SharedPreference,mSharedPrefsPaths沒有做銷燬處理.如果SharedPreferences很多,map會很大,會佔用更多內存。
再看getSharedPreferences(file, mode)方法
@Override
    public SharedPreferences getSharedPreferences(File file, int mode) {
        checkMode(mode);
        if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {//8.0新特性,用戶加鎖時,會拋異常,實測暫未開啓
            if (isCredentialProtectedStorage()
                    && !getSystemService(StorageManager.class).isUserKeyUnlocked(
                            UserHandle.myUserId())
                    && !isBuggy()) {
                throw new IllegalStateException("SharedPreferences in credential encrypted "
                        + "storage are not available until after user is unlocked");
            }
        }
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();//SharedPreferencesImpl緩存池
            sp = cache.get(file);
            if (sp == null) {
                sp = new SharedPreferencesImpl(file, mode);//一個name對應一個SharedPreferences,可理解爲單例
                cache.put(file, sp);
                return sp;
            }
        }
        if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||//跨進程,Google不推薦
            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;
    }
具體分析SharePerferenceImpl
	 // Lock ordering rules:        //一共3把鎖,注意加鎖順序
	 //  - acquire SharedPreferencesImpl.mLock before EditorImpl.mLock
	 //  - acquire mWritingToDiskLock before EditorImpl.mLock
構造器
SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file);//災備文件
        mMode = mode;
        mLoaded = false;
        mMap = null;
        startLoadFromDisk();//核心方法
    }

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

    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 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 (Exception e) {
                    Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
                } finally {
                    IoUtils.closeQuietly(str);
                }
            }
        } catch (ErrnoException e) {
            /* ignore */
        }

        synchronized (mLock) {
            mLoaded = true;
            if (map != null) {
                mMap = map;//mMap 是該SP文件,所包含的所有Key,Value
                mStatTimestamp = stat.st_mtime;
                mStatSize = stat.st_size;
            } else {
                mMap = new HashMap<>();
            }
            mLock.notifyAll();//Load完成,發一個notifyAll,表示已經準備好SP文件,阻塞結束
        }
    }

####get方法

    @Nullable
    public String getString(String key, @Nullable String defValue) {
        synchronized (mLock) { //線程安全   如果同時有多個方法,操作SP,這裏可能會阻塞,給我們一個啓示,優先級高的SP文件,最好單獨保存
            awaitLoadedLocked();// 對於單進程來說,get方法,應該不會在這裏阻塞
            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 {
                mLock.wait();
            } catch (InterruptedException unused) {
            }
        }
    }

####Put方法
put方法,實際通過EditorImpl 完成,Editor是專爲SharedPreferences 私人定製

 @GuardedBy("mLock")
 private final Map<String, Object> mModified = Maps.newHashMap();
        
public Editor putString(String key, @Nullable String value) {
            synchronized (mLock) {
                mModified.put(key, value);
                return this;
            }
        }

從上面可以看出,put方法,僅僅是把key,value存入內存mModified,並沒有保存至磁盤。有點類似於事務處理,必須是最後一步提交事務,纔算是正式生效了。

####最重要的兩個方法

 public void apply() {//異步回寫磁盤
            final long startTime = System.currentTimeMillis();

            final MemoryCommitResult mcr = commitToMemory();//回寫內存
            final Runnable awaitCommit = new Runnable() {
                    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() {
                    public void run() {
                        awaitCommit.run();
                        QueuedWork.removeFinisher(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);
        }

這次有出比較要命的地方, QueuedWork.addFinisher(awaitCommit);雖然是異步操作,但也可能會阻塞主線程

要點在ActivityThread handleXXActivity方法中

  private void handleStopActivity(IBinder token, boolean show, int configChanges, int seq) {
        ActivityClientRecord r = mActivities.get(token);
        if (!checkAndUpdateLifecycleSeq(seq, r, "stopActivity")) {
            return;
        }
        r.activity.mConfigChangeFlags |= configChanges;

        StopInfo info = new StopInfo();
        performStopActivityInner(r, info, show, true, "handleStopActivity");

        if (localLOGV) Slog.v(
            TAG, "Finishing stop of " + r + ": show=" + show
            + " win=" + r.window);

        updateVisibility(r, show);

        // Make sure any pending writes are now committed.
        if (!r.isPreHoneycomb()) {
            QueuedWork.waitToFinish();
        }
       .........

再看 這個 QueuedWork.waitToFinish();

 public static void waitToFinish() {
     .........
        try {
            while (true) {
                Runnable finisher;

                synchronized (sLock) {  //加鎖,輪詢,同步,忐忑不?
                    finisher = sFinishers.poll();
                }

                if (finisher == null) {
                    break;
                }

                finisher.run();
            }
        } finally {
            sCanDelay = true;
        }
		......
        
    }
    
從上面能看出來,Activity Service 一些操作,是需要等到SP操作結束的。所以即使是異步的apply操作也是有可能阻塞主線程的。使用要慎重。

再來看commit方法

 public boolean commit() {
            long startTime = 0;

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

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

SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */); 是異步的關鍵方法

幾經輾轉,會調用

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

##總結一下:
####1、 apply 方法也有可能阻塞主線程,儘量保證SP操作時間可控,不要存儲過大文件
以我們工程爲例,一個key,value就有18k之巨,直接導致後臺監控到大量的SP相關的卡頓
####2、不建議通過SP,實現進程共享,完全質量無保障
####3、getXXX方法,本身也有可能阻塞(別的地方也在操作SP),所以高優先級SP文件,建議獨立開來
####4、apply 異步不需要等待執行結果,但也使用到了線程池,因此儘量合併提交
####5、commit 操作,同步操作,除非希望立即獲取返回結果,否則儘量使用apply

####6、如何解決恐怖的apply 方法阻塞主線程的問題???
有不少開發規範都規定,儘量使用apply方法,除非想理解獲得提交狀態,否則不要用commit方法。

對於小型SP存儲來說,是沒有問題的。 但大型項目,通常SP文件品種繁多,且單個SP文件,所包含的各色key,value(單個可能都不大,但好虎架不住狼多),會導致另一個致命問題,ANR。

那麼解決之道是什麼呢?
答案是爲了性能計,棄用apply。
替代方案爲:仿照apply源碼的做法,需要apply的地方,採用子線程,異步提交commit。這樣,既避免了apply方法,影響ActivityThread重要方法問題,也避免了同步操作阻塞主線程。

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