Android 持久化技術(一)之SharedPreferences

因爲在本身練手的App項目中使用了SharedPreferences技術,因此對其進行必定探究。java

首先咱們先總結一下,Android的數據持久化方式:SharedPrefences、SQLite、文件儲存、ContentProvider、網絡儲存。其他四種,留後再進一步探究。android

SharedPreferences

老規矩看看類註釋是怎麼介紹的
image.png緩存

由Context.getSharedPreferences方法放回的,能夠訪問和修改參數數據的接口。對於任一一組數據,都有一個全部客戶機共享的該類實例。對參數數據的修改,必須經過Editor對象,該對象確保了參數數值的一致性和控制客戶端將數值提交到儲存。由各類get方法獲得的對象必須是不可變得對象。
該類保證了強一致性,可是不支持跨進程使用。安全

從這段註釋中,咱們不難發現:SharedPreferences須要經過Context建立,該類與Editor對象密切相關,在應用內能夠數據共享。網絡

咱們在SharedPreferences類中往下尋找,就找到Editor接口併發

/**
     * Interface used for modifying values in a {@link SharedPreferences}
     * object.  All changes you make in an editor are batched, and not copied
     * back to the original {@link SharedPreferences} until you call {@link #commit}
     * or {@link #apply}
     */
    public interface Editor {
      
        Editor putString(String key, @Nullable String value);
      
        Editor putLong(String key, long value);
       
        Editor putFloat(String key, float value);
       
        Editor putBoolean(String key, boolean value);

        Editor remove(String key);
        
        Editor clear();

        boolean commit();

        void apply();
    }

從這個Editor接口中,咱們能夠獲得幾個信息。首先SharedPreferences只能儲存4類數據,String,Long,Float,Boolean;其次SharedPreferences是使用key-value鍵值對的方式進行儲存的;最後,有兩種提交方式apply(),commit()。app

  • 從前兩個信息不難推出,SharedPreferences只是一種輕量級的儲存方式,因此最好不要使用這個去儲存一些過大,或者複雜數據類型的數據。
  • apply()和commit()的區別:異步

    • 從上面能夠看出,commit是有返回值的,而apply是沒有放回值的。當咱們須要知道一個數據是否修改爲功時,就須要調用commit方法。
    • commit是直接將修改的數據同步提交到硬件硬盤,會阻塞調用它的線程。apply則是將修改數據原子提交到內存,然後異步真正提交到硬件硬盤,因此後面調用apply會直接覆蓋前面的數據,使用apply會提升效率。
    • apply方法不會提示任何失敗的提示。因爲在一個進程中,sharedPreference是單實例,通常不會出現併發衝突,若是對提交的結果不關心的話,建議使用apply,固然須要確保提交成功且有後續操做的話,仍是須要用commit的。
    • 此外SharedPreferences有一個接口,能夠實現對鍵值變化的監聽。
  • 若是須要儲存複雜數據(圖片或對象)時,就須要對將其轉化爲Base64編碼。

如何使用SharedPreferences?

一、獲取SharedPreferences

ContextImpl.getSharePreferences() 該類在AS上不顯示。根據當前應用名稱獲取ArrayMap(存儲sp容器),並根據文件名獲取SharedPreferencesImpl對象(實現SharedPreferences接口)。
  • 緩存未命中, 才構造SharedPreferences對象,也就是說,屢次調用getSharedPreferences方法並不會對性能形成多大影響,由於又緩存機制
  • SharedPreferences對象的建立過程是線程安全的,由於使用了synchronize`關鍵字
  • 若是命中了緩存,而且參數mode使用了Context.MODE_MULTI_PROCESS,那麼將會調用sp.startReloadIfChangedUnexpectedly()方法,在startReloadIfChangedUnexpectedly方法中,會判斷是否由其餘進程修改過這個文件,若是有,會從新從磁盤中讀取文件加載數據
class ContextImpl extends Context {
    //靜態存儲類,緩存全部應用的SP容器,該容器key對應應用名稱,value則爲每一個應用存儲全部sp的容器
    private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
    
     @Override
    public SharedPreferences getSharedPreferences(String name, int mode) {
        ......
        
        // 根據 名字獲取相對應的文件名。
        // 若是沒有則直接新建一個
        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);
    }

    @Override
    public SharedPreferences getSharedPreferences(File file, int mode) {
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
        // 從ArrayMap中獲取到應用儲存的value
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
            // 從當前的Map中獲取一個,若是沒有則直接新建一個而且放回
            sp = cache.get(file);
            if (sp == null) {
                checkMode(mode);
                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);
                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();
        }
        return sp;
    }
    
}
sharedPreferences的對象實例 sharedPreferencesImpl類

用SP存儲的靜態變量鍵值數據在內存中是一直存在(文件存儲),經過SharedPreferencesImpl構造器開啓一個線程對文件進行讀取。SharedPreferencesImpl主要是對文件進行操做。ide

  • 將傳進來的參數file以及mode分別保存在mFile以及mMode
  • 建立一個.bak備份文件,當用戶寫入失敗的時候會根據這個備份文件進行恢復工做
  • 將存放鍵值對的mMap初始化爲null
  • 調用startLoadFromDisk()方法加載數據
startLoadFromDisk()
  • 若是有備份文件,直接使用備份文件進行回滾
  • 第一次調用getSharedPreferences方法的時候,會從磁盤中加載數據,而數據的加載時經過開啓一個子線程調用loadFromDisk方法進行異步讀取的
  • 將解析獲得的鍵值對數據保存在mMap
  • 將文件的修改時間戳以及大小分別保存在mStatTimestamp以及mStatSize中(保存這兩個值有什麼用呢?咱們在分析getSharedPreferences方法時說過,若是有其餘進程修改了文件,而且modeMODE_MULTI_PROCESS,將會判斷從新加載文件。如何判斷文件是否被其餘進程修改過,沒錯,根據文件修改時間以及文件大小便可知道)
  • 調用notifyAll()方法通知喚醒其餘等待線程,數據已經加載完畢
SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        // 文件備份
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        mMap = null;
        mThrowable = 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<String, Object> map = null;
        StructStat stat = null;
        Throwable thrown = null;
        try {
            stat = Os.stat(mFile.getPath());
            if (mFile.canRead()) {
             //讀取文件
                BufferedInputStream str = null;
                try {
                    str = new BufferedInputStream(
                            new FileInputStream(mFile), 16 * 1024);
                    //使用XmlUtils工具類讀取xml文件數據
                    map = (Map<String, Object>) XmlUtils.readMapXml(str);
                } catch (Exception e) {
                    Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
                } finally {
                    IoUtils.closeQuietly(str);
                }
            }
        } catch (ErrnoException e) {
            // An errno exception means the stat failed. Treat as empty/non-existing by
            // ignoring.
        } catch (Throwable t) {
            thrown = t;
        }

        synchronized (mLock) {
            //修改文件加載完成標誌
            mLoaded = true;
            mThrowable = thrown;

            // It's important that we always signal waiters, even if we'll make
            // them fail with an exception. The try-finally is pretty wide, but
            // better safe than sorry.
            try {
                if (thrown == null) {
                    if (map != null) {
                        mMap = map;/若是有數據,將數據已經賦值給類成員變量mMap(將從文件讀取的數據賦值給mMap)
                        mStatTimestamp = stat.st_mtim;
                        mStatSize = stat.st_size;
                    } else {
                    //沒有數據直接建立一個hashmap對象
                        mMap = new HashMap<>();
                    }
                }
                // In case of a thrown exception, we retain the old map. That allows
                // any open editors to commit and store updates.
            } catch (Throwable t) {
                mThrowable = t;
            } finally {
                //此處很是關鍵是爲了通知其餘線程文件已讀取完畢,大家能夠執行讀/寫操做了
                mLock.notifyAll();
            }
        }
    }

二、獲取數據

sharedPreferencesImpl重寫了SharedPreferences的方法,基本上結構都一致,這裏拿String類舉例。工具

  • getXxx方法是線程安全的,由於使用了synchronize關鍵字
  • getXxx方法是直接操做內存的,直接從內存中的mMap中根據傳入的key讀取value
  • getXxx方法有可能會卡在awaitLoadedLocked方法,從而致使線程阻塞等待(何時會出現這種阻塞現象呢?前面咱們分析過,第一次調用getSharedPreferences方法時,會建立一個線程去異步加載數據,那麼假如在調用完getSharedPreferences方法以後當即調用getXxx方法,此時的mLoaded頗有可能爲false,這就會致使awaiteLoadedLocked方法阻塞等待,直到loadFromDisk方法加載完數據而且調用notifyAll來喚醒全部等待線程
public String getString(String key, @Nullable String defValue) {
        synchronized (mLock) {
        //此處會阻塞當前線程,直到文件加載完畢,第一次使用的時候可能會阻塞主線程
            awaitLoadedLocked();
        //從類成員變量mMap中直接讀取數據,沒有直接返回默認值
            String v = (String)mMap.get(key);
            return v != null ? v : defValue;
        }
    }

三、提交數據

3.1 獲取editor對象
public Editor edit() {
       synchronized (mLock) {
           awaitLoadedLocked();//若是文件未加載完畢,會一直阻塞當前線程,直到加載完成爲止
       }
       return new EditorImpl();
   }
3.2 對數據進行修改
  • SharedPreferences的寫操做是線程安全的,由於使用了synchronize關鍵字
  • 對鍵值對數據的增刪記錄保存在mModified中,而並非直接對SharedPreferences.mMap進行操做(mModified會在commit/apply方法中起到同步內存SharedPreferences.mMap以及磁盤數據的做用)
public final class EditorImpl implements Editor {
    //先存儲在Editor的map中
    private final Map<String, Object> mModified = new HashMap<>();
    
   //各類修改方法依舊相似
    public Editor putString(String key, @Nullable String value) {
            synchronized (mEditorLock) {
                mModified.put(key, value);
                return this;
            }
        }
    ...
 }
3.3 提交數據
  • commit()方法
public boolean commit() {
            long startTime = 0;

            if (DEBUG) {
                startTime = System.currentTimeMillis();
            }
            //第一步  commitToMemory方法能夠理解爲對SP中的mMap對象同步到最新數據狀態
            //mcr對象就是最終須要寫入磁盤的mMap
            MemoryCommitResult mcr = commitToMemory();
            
            //第二步 寫文件;注意第二個參數爲null,寫文件操做會運行在當前線程
            //當前只有一個commit線程時。會直接在當前線程執行
            //若是是UI線程 則可能會形成阻塞
            //會判斷有無 備份文件,必定要有備份文件,防止寫入錯誤
            //將mcr寫入磁盤
            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;
        }
  • apply和commit主要區別就是apply的寫文件操做會在一個線程中執行,不會阻塞UI線程
public void apply() {
            final long startTime = System.currentTimeMillis();
            
            final MemoryCommitResult mcr = commitToMemory();
            final Runnable awaitCommit = new Runnable() {
                    @Override
                    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() {
                    @Override
                    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);
        }

注意點

  • SharedPreferences是線程安全的,可是不是進程安全的。
  • SharedPreferences 不要存放特別大的數據

    • 第一次加載時,須要將整個SP加載到內存當中,若是過於大,會致使阻塞,甚至會致使 ANR
    • 每次apply或者commit,都會把所有的數據一次性寫入磁盤, 因此 SP 文件不該該過大, 影響總體性能
    • SharedPreference的文件存儲性能與文件大小相關,咱們不要將毫無關聯的配置項保存在同一個文件中,同時考慮將頻繁修改的條目單獨隔離出來
  • 不適宜存儲JSON等特殊符號不少的數據
  • 全部的getXxx都是從內存中取的數據,數據來源於SharedPreferences.mMap
  • apply同步回寫(commitToMemory())內存SharedPreferences.mMap,而後把異步回寫磁盤的任務放到一個單線程的線程池隊列中等待調度。apply不須要等待寫入磁盤完成,而是立刻返回
  • ommit同步回寫(commitToMemory())內存SharedPreferences.mMap,而後若是mDiskWritesInFlight(此時須要將數據寫入磁盤,但還未處理或未處理完成的次數)的值等於1,那麼直接在調用commit的線程執行回寫磁盤的操做,不然把異步回寫磁盤的任務放到一個單線程的線程池隊列中等待調度。commit會阻塞調用線程,知道寫入磁盤完成才返回
  • MODE_MULTI_PROCESS是在每次getSharedPreferences時檢查磁盤上配置文件上次修改時間和文件大小,一旦全部修改則會從新從磁盤加載文件,因此並不能保證多進程數據的實時同步
  • 屢次edit屢次commit/apply

    • 屢次edit會產生不少editor對象
    • 屢次apply和commit App的stop方法會等待寫完爲止
相關文章
相關標籤/搜索