深度剖析Android SharePreferences


前言

提到Sp(后面都用這個簡稱),相信Android開發者都不會陌生,基本上寫項目都會用到,但是可能沒有深究實現細節,因此當面試時
被面試官問到相關問題,往往不知所措.

先提幾個問題:

  • q1:Sp可以跨進程么?為什么?
  • q2:有什么方法可以讓Sp實現跨進程?
  • q3:commit和apply有什么區別?使用場景?
  • q4:為什么Sp不適合存放占用內存較大的內容?如bitmap

使用

Sp的簡單使用如下

  SharedPreferences preferences = this.getSharedPreferences("sp_name", Context.MODE_PRIVATE);
        String key = preferences.getString("key", "");
        preferences.edit().putString("my_key", "hell").apply();

調用Context.getSharedPreferences,傳遞sp的名稱和操作模式既可獲取Sp的實例

默認的操作模式是Context.MODE_PRIVATE,也是官方推薦的,其他幾種模式基本都被棄用了,即官方不推薦使用.

源碼分析

getSharedPreferences

我們知道Context的唯一實現是ContextImpl,不管是Activity的context還是Application的context,context.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.
        //目標sdk小於4.4時,支持sp的名字為null
        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);
    }

ContextImpl有一個成員變量mSharedPrefsPaths,保存sp的名字與對應的文件的映射,這個很好理解,當我們通過context拿sp的實例

的時候,肯定先要找到sp對應文件,然后再對該文件進行讀寫操作.

值得注意的是這里對於mSharedPrefsPaths的操作時加鎖了,鎖的對象是ContextImpl.class,所以不論是從哪個Context的子類來獲取sp,都能保證

mSharedPrefsPaths的線程安全.

 @Override
    public SharedPreferences getSharedPreferences(File file, int mode) {
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();//1
            sp = cache.get(file);//2
            if (sp == null) {
                checkMode(mode);//3
                if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {
                    if (isCredentialProtectedStorage()
                            && !getSystemService(UserManager.class)
                                    .isUserUnlockingOrUnlocked(UserHandle.myUserId())) {
                        throw new IllegalStateException("SharedPreferences in credential encrypted "
                                + "storage are not available until after user is unlocked");
                    }
                }
                sp = new SharedPreferencesImpl(file, mode);//4
                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();
        }//5
        return sp;
    }

這個方法大概涉及5個比較重要的點,上面都有標注,接下來一一分析

  1. 首先看getSharedPreferencesCacheLocked()方法的實現
    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;
    }

這里主要涉及兩個映射關系,一個是應用包名與sp之間的映射,因為一個應用可能創建多個sp文件來存儲不同的業務配置項

第二個是sp文件與sp實現類SharedPreferencesImpl之間的映射關系,這個之前有提到.

值得注意的是它們使用的都是ArrayMap而不是HashMap,主要是因為ArrayMapHashMap更省內存,這個以后單獨寫一篇.

  1. 通過file拿到對應的sp的實現類實例.

  2. 檢查操作模式,看一下實現

    private void checkMode(int mode) {
        if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.N) {
            if ((mode & MODE_WORLD_READABLE) != 0) {
                throw new SecurityException("MODE_WORLD_READABLE no longer supported");
            }
            if ((mode & MODE_WORLD_WRITEABLE) != 0) {
                throw new SecurityException("MODE_WORLD_WRITEABLE no longer supported");
            }
        }
    }

當目標sdk版本大於N的時候,如果操作模式設置為MODE_WORLD_READABLEMODE_WORLD_WRITEABLE話,即允許其他應用讀寫sp的話,就會拋出

安全異常,可見Google對應用安全方面的限制越來越嚴格了.

  1. 創建sp的實現類的實例,並加入到緩存中,以便下次能夠快速的拿到.

  2. 當操作模式設置為Context.MODE_MULTI_PROCESS或者目標sdk版本小於3.2時,調用sp.startReloadIfChangedUnexpectedly()

void startReloadIfChangedUnexpectedly() {
        synchronized (mLock) {
            // TODO: wait for any pending writes to disk?
            if (!hasFileChangedUnexpectedly()) {
                return;
            }
            startLoadFromDisk();
        }
    }

該方法先去檢查文件狀態是否改變,如果有的話就重新讀取文件數據到內存.這里我們知道MODE_MULTI_PROCESS是不靠譜的,它並不能支持數據

跨進程共享,只是getSharePreference時回去檢查文件狀態是否改變,改變就重新加載數據到內存.

SharedPreferencesImpl

上面了解到getSharePreference返回的其實是SharedPreferencesImpl的實例,現在重點看一下SharedPreferencesImpl的實現.

構造函數

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

都是一些常規操作,初始化一些值,創建備份文件,重點看一下startLoadFromDisk

startLoadFromDisk

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

mLoaded變量置為false,表示數據還沒有加載成功,然后開啟了一個線程,並調用loadFromDisk

loadFromDisk

private void loadFromDisk() {
        synchronized (mLock) {
            if (mLoaded) {
                return;
            }
            if (mBackupFile.exists()) {//1
                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);//2
                } 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;//3
                mStatTimestamp = stat.st_mtim;
                mStatSize = stat.st_size;
            } else {
                mMap = new HashMap<>();
            }
            mLock.notifyAll();//4
        }
    }

  1. 先判斷備份文件是否存在,如果存在就刪除當前文件,將備份文件重命名為正式文件.

  2. 然后創建文件輸出流讀取文件內存並轉化為Map,注意這里創建帶緩存的輸出流時,指定的buffer大小為16k.可以借鑒.

  3. 將讀取到的Map賦值給mMap成員變量,如果map為空就創建一個空的HashMap,這里又是用到HashMap了,因為這里

設計頻繁查找或插入操作,而hashMap的查詢和插入操作的效率是優於ArrayMap的.

  1. 通知喚醒線程,有喚醒就有阻塞,看一下哪里阻塞了,全局搜索一下
 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) {
            }
        }
    }

該方法在mLoaded為false的時候一直阻塞,而之前的notifyAll喚醒的就是此處的阻塞.再看一下awaitLoadedLocked

在哪里被調用了.

 public Map<String, ?> getAll() {
        synchronized (mLock) {
            awaitLoadedLocked();
            //noinspection unchecked
            return new HashMap<String, Object>(mMap);
        }
    }

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

    @Nullable
    public Set<String> getStringSet(String key, @Nullable Set<String> defValues) {
        synchronized (mLock) {
            awaitLoadedLocked();
            Set<String> v = (Set<String>) mMap.get(key);
            return v != null ? v : defValues;
        }
    }

    public int getInt(String key, int defValue) {
        synchronized (mLock) {
            awaitLoadedLocked();
            Integer v = (Integer)mMap.get(key);
            return v != null ? v : defValue;
        }
    }
    public long getLong(String key, long defValue) {
        synchronized (mLock) {
            awaitLoadedLocked();
            Long v = (Long)mMap.get(key);
            return v != null ? v : defValue;
        }
    }
    public float getFloat(String key, float defValue) {
        synchronized (mLock) {
            awaitLoadedLocked();
            Float v = (Float)mMap.get(key);
            return v != null ? v : defValue;
        }
    }
    public boolean getBoolean(String key, boolean defValue) {
        synchronized (mLock) {
            awaitLoadedLocked();
            Boolean v = (Boolean)mMap.get(key);
            return v != null ? v : defValue;
        }
    }

    public boolean contains(String key) {
        synchronized (mLock) {
            awaitLoadedLocked();
            return mMap.containsKey(key);
        }
    }

這里可以知道,所有的get相關方法都被阻塞,直到完成數據從文件加載到內存的過程.因此當第一次調用sp的get相關

函數時是比較慢的,需要等待數據從文件被讀取到內存,之后會比較快,因為是直接在內存中讀取.

至此,get相關方法已經分析完畢,原理也比較容易理解,接下來看看put相關方法.

edit

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

        return new EditorImpl();
    }

調用put相關方法之前需要調用edit方法,此處也是需要等待的,返回的是EditorImpl的實例.

EditorImpl

        @GuardedBy("mLock")
        private final Map<String, Object> mModified = Maps.newHashMap();

EditorImpl是SharePreferenceImpl的內部類,內部有一個HashMap保存被更改的鍵值對.

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

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

從以上兩個方法可以知道,put方法就是向mModified添加一個鍵值對,remove方法添加的value為當前editor實例.

它們都是被mLock加鎖保護的,有兩個原因

  • HashMap不是線程安全的
  • 需要和其他的get方法互斥

commit

 public boolean commit() {
            long startTime = 0;

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

            MemoryCommitResult mcr = commitToMemory();   //1

            SharedPreferencesImpl.this.enqueueDiskWrite(      //2
                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;
        }
  1. commitToMemory實現
  private MemoryCommitResult commitToMemory() {
            long memoryStateGeneration;
            List<String> keysModified = null;
            Set<OnSharedPreferenceChangeListener> listeners = null;
            Map<String, Object> mapToWriteToDisk;

            synchronized (SharedPreferencesImpl.this.mLock) {
                // 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);
                }
                mapToWriteToDisk = mMap;
                mDiskWritesInFlight++;

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

                synchronized (mLock) {
                    boolean changesMade = false;

                    if (mClear) {
                        if (!mMap.isEmpty()) {
                            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);
                        }

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

                    mModified.clear();

                    if (changesMade) {
                        mCurrentMemoryStateGeneration++;
                    }

                    memoryStateGeneration = mCurrentMemoryStateGeneration;
                }
            }
            return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
                    mapToWriteToDisk);
        }

顧名思義,就是把更改的鍵值對提交到內存中,即把mModified中的鍵值對更新到mMap中,順便獲取被更新

的鍵的集合以及外部設置監聽器列表(基於深拷貝)

  1. enqueueDiskWrite
 private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
        //判斷是同步任務還是異步任務
        final boolean isFromSyncCommit = (postWriteRunnable == null);

        final Runnable writeToDiskRunnable = new Runnable() {
                public void run() {
                    synchronized (mWritingToDiskLock) {
                        writeToFile(mcr, isFromSyncCommit);//寫到文件中
                    }
                    synchronized (mLock) {
                        mDiskWritesInFlight--;//寫入操作完畢,計數減一
                    }
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
            };

        // Typical #commit() path with fewer allocations, doing a write on
        // the current thread.
        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            synchronized (mLock) {
                wasEmpty = mDiskWritesInFlight == 1;
            }
            if (wasEmpty) {
                writeToDiskRunnable.run();//如果當前沒有正在執行的同步任務,就直接執行
                return;
            }
        }
        //異步提交任務
        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    }

commit方法分析完畢,其實就是將最新的mMap寫入到文件中.

apply

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

enqueueDiskWrite之前看過了,關鍵代碼如下

QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
 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);
            }
        }
    }

首先獲取一個Handler的實例,然后再通過Handler發送一個消息,先看一下getHandler

   private static Handler getHandler() {
        synchronized (sLock) {
            if (sHandler == null) {
                HandlerThread handlerThread = new HandlerThread("queued-work-looper",
                        Process.THREAD_PRIORITY_FOREGROUND);
                handlerThread.start();

                sHandler = new QueuedWorkHandler(handlerThread.getLooper());
            }
            return sHandler;
        }
    }

這是一個典型的單例模式寫法,Handler構造方法的Looper來自HandlerThread,這是一個內部維護消息機制

的線程,任務是按照時間順序依次執行的,不了解的可以去看一下源碼.

接下來看一下handleMessage方法實現

        public void handleMessage(Message msg) {
            if (msg.what == MSG_RUN) {
                processPendingWork();
            }
        }
 private static void processPendingWork() {
        long startTime = 0;

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

        synchronized (sProcessingWork) {
            LinkedList<Runnable> work;

            synchronized (sLock) {
                work = (LinkedList<Runnable>) sWork.clone();
                sWork.clear();

                // Remove all msg-s as all work will be processed now
                getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
            }
            //直接循環調用runnable的run方法執行任務
            if (work.size() > 0) {
                for (Runnable w : work) {
                    w.run();
                }

                if (DEBUG) {
                    Log.d(LOG_TAG, "processing " + work.size() + " items took " +
                            +(System.currentTimeMillis() - startTime) + " ms");
                }
            }
        }
    }

其實到這里apply方法也基本上分析完畢,該方法是在子線程被調用的,為了線程安全考慮,使用的是HandlerThread

來依次執行寫文件任務.

但我們需要依次提交更改多個鍵值對時,只需要保留最后一個commit或apply方法既可.

跨進程的Sp

結合ContentProvider並重寫call方法

總結

  • sp不適合存儲過大的數據,因為它一直保存在內存中,數據過大容易造成內存溢出.

  • sp並不支持跨進程,因為它不能保證更新本地數據后被另一個進程所知道,而且跨進程的操作標記已經被棄用.

  • sp的commit方法是直接在當前線程執行文件寫入操作,而apply方法是在工作線程執行文件寫入,盡可能使用apply,因為不會阻塞當前線程.

  • sp批量更改數據時,只需要保留最后一個apply即可,避免添加多余的寫文件任務.

  • 每個sp存儲的鍵值對不宜過多,否則在加載文件數據到內存時會耗時過長,而阻塞sp的相關getput方法,造成ui卡頓.

  • 頻繁更改的配置項和不常更改的配置項應該分開為不同的sp存放,避免不必要的io操作.

感覺寫的有些亂,不過一些重要的點基本都有,如果有理解錯誤的地方還請大佬指正.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM