SharedPreference用法及源碼分析

什么是SharedPreference

SharedPreference(以下簡稱SP)是Android提供的一個輕量級的持久化存儲框架,主要用于保存一些比較小的數據,例如配置數據。SP是以“健-值”對的形式來保存數據,其實質是把這些數據保存到XML文件中,每個“健-值”對就是XML文件的一個節點,通過調用SP生成的文件最終會放在手機內部存儲的/data/data/<package name>/shared_prefs目錄下。

如何使用SharedPreference

獲取SharedPreference

使用SP的第一步是獲取SP對象。在Android中,我們可以通過以下三種方式來獲取SP對象。

1.Context類中的getSharedPreferences方法
    public SharedPreferences getSharedPreferences(String name, int mode) {
          ...
    }

這個方法接收兩個參數,第一個參數是SP的文件名,我們知道SP是以XML文件的形式進行存儲的,每一個SharedPreference實例都對應了一個XML文件,這里的name就是XML文件的名字。第二個參數用于指定文件的操作模式,最開始的時候SP是可以跨進程訪問的,所以SP有MODE_PRIVATE,MODE_WORLD_READABLE,MODE_MULTI_PROCESS等多種操作模式,只不過出于安全性考慮,谷歌目前只保留了MODE_PRIVATE這一種模式,其他模式均已被廢棄。在MODE_PRIVATE模式下,只有應用本身可以訪問SharedPreference文件,其他應用無權訪問。

2.Activity的getPreferences方法
   public SharedPreferences getPreferences(int mode) {
       return getSharedPreferences(getLocalClassName(), mode);
   }

這個方法只接收一個參數,即SP文件的操作模式,那SharedPreference的名字是啥呢,通過源碼可以看到,這里使用了當前類的類名來作為SP的文件名。例如,當前類名為MainActivity,那么對應SP的文件名就是MainActivity.xml。

3.PreferenceManager的getDefaultSharedPreferences方法
    public static SharedPreferences getDefaultSharedPreferences(Context context) {
        return context.getSharedPreferences(getDefaultSharedPreferencesName(context),
       getDefaultSharedPreferencesMode());
    }

    private static int getDefaultSharedPreferencesMode() {
        return Context.MODE_PRIVATE;
    }

    public static String getDefaultSharedPreferencesName(Context context) {
        return context.getPackageName() + "_preferences";
    }

這個方法接收Context參數,并使用當前包名_preferences作為SP的文件名。使用MODE_PRIVATE作為操作模式。
以上三個方法其實大同小異,主要區別在于最終生成的SP的文件名有差異。

使用SharedPreference進行讀寫數據
1.讀取數據

使用SharedPreference讀取數據很簡單,分為兩個步驟:
(1) 獲取SharedPreference對象(使用上述的三種方式)
(2) 調用SharedPreference對象的get方法讀取對應類型的數據。

2.寫入數據

使用SharedPreference寫入數據分為四步:
(1) 獲取SharedPreference對象
(2) 獲取SharedPreferences.Editor對象
(3) 調用SharedPreferences.Editor對象的put方法寫入數據
(4) 調用SharedPreferences.Editor對象的apply/commit方法提交更改

示例

我們平常在登陸賬號的時候一般都會有一個記住密碼的功能,下面就用SP來實現一個簡單的記住登陸密碼的功能。代碼很簡單,不做過多解釋來。

<!--activity_main.xml-->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <TextView
            android:layout_width="90dp"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:textSize="18sp"
            android:text="Account" />

        <EditText
            android:id="@+id/account"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_gravity="center_vertical" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <TextView
            android:layout_width="90dp"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:textSize="18sp"
            android:text="Password" />

        <EditText
            android:id="@+id/password"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight = "1"
            android:layout_gravity = "center_vertical"
            android:inputType="textPassword"/>

    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <CheckBox
            android:id="@+id/remember_password"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="18sp"
            android:text="remember password"/>

    </LinearLayout>

    <Button
        android:id="@+id/login"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:text="login"/>
</LinearLayout>
//MainActivity.java
import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Toast;

public class MainActivity extends Activity {
    private EditText mAccountEdit;
    private EditText mPasswordEdit;
    private Button mLoginBtn;
    private CheckBox mRememberPasswordCbx;
    private SharedPreferences mSharedPreferences;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mAccountEdit = findViewById(R.id.account);
        mPasswordEdit = findViewById(R.id.password);
        mLoginBtn = findViewById(R.id.login);
        mRememberPasswordCbx = findViewById(R.id.remember_password);

        mSharedPreferences = getSharedPreferences("admin",MODE_PRIVATE);
        boolean isRememberPassword = mSharedPreferences.getBoolean("RememberPassword",false);
        if(isRememberPassword){
            String account = mSharedPreferences.getString("Account","");
            String password = mSharedPreferences.getString("Password","");
            mAccountEdit.setText(account);
            mPasswordEdit.setText(password);
            mRememberPasswordCbx.setChecked(true);
        }

        mLoginBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String account = mAccountEdit.getText().toString();
                String password = mPasswordEdit.getText().toString();
                SharedPreferences.Editor edit = mSharedPreferences.edit();
                if (account.equals("admin") && password.equals("888888")) {
                    if(mRememberPasswordCbx.isChecked()){
                        edit.putString("Account",account);
                        edit.putString("Password",password);
                        edit.putBoolean("RememberPassword",true);
                    }else {
                        edit.clear();
                    }
                    edit.apply();
                    Intent intent = new Intent(MainActivity.this, UserActivity.class);
                    startActivity(intent);
                }else {
                    Toast.makeText(MainActivity.this,"賬號或者密碼錯誤",Toast.LENGTH_LONG).show();
                }
            }
        });
    }
}

<!--activity_user.xml-->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_horizontal"
        android:text="login success"/>

</LinearLayout>
//UserActivity
import android.app.Activity;
import android.os.Bundle;

public class UserActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_user);
    }

}
SharedPreference源碼分析
1.獲取對象

首先看下獲取SP對象的源碼。無論使用哪種方式來獲取SP對象,最終都是通過調用SharedPreferencesImpl來構建SP對象的。創建SP對象代碼如下。

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

這個方法主要就是定義了一個備份文件對象,然后調用了startLoadFromDisk方法,繼續來看startLoadFromDisk方法。

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

這里調用了loadFromDisk方法,開啟了一個異步線程,因為加載SP文件是IO耗時操作,不能放在主線程,否則會導致主線程阻塞。繼續看loadFromDisk方法。

    private void loadFromDisk() {
        synchronized (mLock) {
            if (mLoaded) {
                return;
            }
            if (mBackupFile.exists()) {
                mFile.delete();
                mBackupFile.renameTo(mFile);
            }
        }
        Map<String, Object> map = null;
        try {
            if (mFile.canRead()) {
                BufferedInputStream str = null;
                try {
                    str = new BufferedInputStream(new FileInputStream(mFile), 16 * 1024);
                    map = (Map<String, Object>) XmlUtils.readMapXml(str);
                } 
                ...
            }
        } 
        ...
        synchronized (mLock) {
            mLoaded = true;
            try {   
                if (map != null) {
                    mMap = map;
                } else {
                    mMap = new HashMap<>();
                }              
            } catch (Throwable t) {
       
            } finally {
                mLock.notifyAll();
            }
        }
    }

這里省略了部分代碼,可以看到如果有備份文件,SP會優先使用備份文件,然后就是讀取并解析XML文件,通過 XmlUtils.readMapXml方法讀取XML文件并解析成Map對象,這里就是創建SP對象的關鍵,也就是說創建SP對象的過程其實就是把SP文件加載到Map(內存)中的過程。加載完成之后,會調用mLock同步鎖的notifyAll方法,來使其他阻塞在這個同步鎖的線程解除阻塞。同時,把mLoaded置為true,表示加載文件完成。到此,創建SP對象的過程就結束了,我們最終得到了一個Map,后續的讀取操作都會基于這個Map來進行。
從上面過程可以看到SharedPreference最終會以Map的形式加載到內存中,所以SharedPreference適合用于存儲小數據,并不適合存儲較大的數據。否則一方面會消耗內存,一方面在加載文件的過程可能導致主線程阻塞。

2.讀取數據

創建SP對象完成后,我們實際上獲得來一個裝載SP數據的Map,讀取數據的過程實際就是從Map取數據的過程,以getString方法為例。

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

這里讀取數據不難理解,就是Map的get操作,有一個地方需要注意的,就是awaitLoadedLocked方法。我們看一下這個方法。

    private void awaitLoadedLocked() {
        while (!mLoaded) {
            try {
                mLock.wait();
            } catch (InterruptedException unused) {
            }
        }
    }

從上面分析我們可以知道,mLoaded表示SP文件是加載完成,如果沒有加載完成,這個方法就會進入while循環,并調用mLock.wait()來阻塞當前線程。在loadFromDisk方法中我們可以看到,當加載文件完成后,會調用 mLock.notifyAll()來使其他阻塞在mLock同步鎖的線程解除阻塞。所以,等到SP文件加載完成后,這個方法就會解除阻塞,如果沒有讀取完成,調用getString的線程會阻塞在這個同步鎖上。這也解釋來為什么在第一次從SP讀取數據的時候有可能會耗時比較久,后面讀取數據幾乎不耗時。就是因為SP文件沒有加載完成,導致線程阻塞引起的,后續讀取因為都是直接從內存中(mMap)中讀取,所以幾乎不會耗時。

2.寫入數據

在向SP寫入數據的時候,我們首先獲取了一個Editor對象,這個Editor對象的作用是什么呢?來看下獲取Editor對象的源碼。

    public Editor edit() {
        synchronized (mLock) {
            awaitLoadedLocked();
        }

        return new EditorImpl();
    }

這里和讀取數據一樣,首先也是調用awaitLoadedLocked方法來等待SP文件加載完成。然后就是調用EditorImpl來創建editor對象。看一下EditorImpl類的定義。

public final class EditorImpl implements Editor {
        private final Object mEditorLock = new Object();
        
        private final Map<String, Object> mModified = new HashMap<>();

        @Override
        public Editor putString(String key, @Nullable String value) {
            synchronized (mEditorLock) {
                mModified.put(key, value);
                return this;
            }
        }
        ...
       @Override
        public void apply() {
          ...
        }

       @Override
        public void commit() {
          ...
        }
}

這個類很簡單,主要就是創建來一個Map(mModified),并定義來一些put方法,還有就是定義來一個apply方法和一個commit方法。
在向SharedPreference寫入數據的時候,我們是調用editor的put方法來寫入數據的,以putString方法為例。

        public Editor putString(String key, @Nullable String value) {
            synchronized (mEditorLock) {
                mModified.put(key, value);
                return this;
            }
        }

這里可以看到,寫入數據時,并沒有把數據直接寫如文件,而是把數據放在了mModified這個表里邊,這個表是在內存里的。
執行寫入數據的最后一步是調用editor的supply/commit方法來提交變更,那么這兩個方法有什么區別呢?首先來看一下commit方法。

        public boolean commit() {
            ...
            MemoryCommitResult mcr = commitToMemory();
            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null /* sync write on this thread okay */);
            ...
        }

commit方法首先是調用commitToMemory構造存儲對象,然后調用enqueueDiskWrite將進行持久化存儲。首先來看一下commitToMemory方法

        private MemoryCommitResult commitToMemory() {
            long memoryStateGeneration;
            List<String> keysModified = null;
            Set<OnSharedPreferenceChangeListener> listeners = null;
            Map<String, Object> mapToWriteToDisk;
               mapToWriteToDisk = mMap; 
                synchronized (mEditorLock) {
                    boolean changesMade = false;
                    for (Map.Entry<String, Object> e : mModified.entrySet()) {
                        String k = e.getKey();
                        Object v = e.getValue();
                        if (v == this || v == null) {
                            if (!mapToWriteToDisk.containsKey(k)) {
                                continue;
                            }
                            mapToWriteToDisk.remove(k);
                        } else {
                            if (mapToWriteToDisk.containsKey(k)) {
                                Object existingValue = mapToWriteToDisk.get(k);
                                if (existingValue != null && existingValue.equals(v)) {
                                    continue;
                                }
                            }
                            mapToWriteToDisk.put(k, v);
                        }
                 }
        }

我們最終向文件寫入的內容是mapToWriteToDisk,這個map包含兩部分,第一部分是創建SP對象時從文件加載到內存的map,第二部分是創建editor對象的時創建的mModified,editor的所有put操作都是放在了這個map里邊,把兩個map合并之后就得到了最終要向文件寫入的map,所以SP每次提交數據修改并不是增量寫入數據,而是將新增數據和原有數據合并之后全量寫入。
后面接著看enqueueDiskWrite方法。

    private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) {
        final boolean isFromSyncCommit = (postWriteRunnable == null);
        final Runnable writeToDiskRunnable = new Runnable() {
                @Override
                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);
    }

這里首先創建了一個Runnable對象writeToDiskRunnable,在這個對象的run方法里邊執行文件寫入操作。然后如果isFromSyncCommit為true且當前只有一個寫入操作,就直接在當前線程執行writeToDiskRunnable的run方法,也就是說在當前線程執行寫入文件操作。否則就傳入QueuedWork進行異步寫入。那么isFromSyncCommit什么時候為true呢,就是在postWriteRunnable=null的時候,這時再回頭看commit方法,這個方法在調用enqueueDiskWrite方法時,postWriteRunnable參數傳入的是null,看到這里也就明白了,commit是同步IO操作,也就是在調用commit方法的線程直接執行寫入操作。
再來看apply方法。

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

            QueuedWork.addFinisher(awaitCommit);

            Runnable postWriteRunnable = new Runnable() {
                    @Override
                    public void run() {
                        awaitCommit.run();
                        QueuedWork.removeFinisher(awaitCommit);
                    }
                };

            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
        }

分析過commit方法之后,apply方法就很簡單了,首先apply方法也是構建了一個mcr對象,然后定義了一個postWriteRunnable對象并調用了enqueueDiskWrite方法,根據上面對enqueueDiskWrite方法的分析,postWriteRunnable!=null會使isFromSyncCommit為false,進而在異步線程執行文件寫入操作。
所以在使用SharedPreference存儲數據的時候,最好使用apply方法提交修改,而不是commit,因為commit是在當前線程執行IO操作,有可能會導致線程卡頓甚至出現ANR。而apply是異步寫入的,不會阻塞當前線程執行。

使用SharedPreference的建議
  • 不要使用SP存儲大文件及存儲大量的key和value,因為最終SharedPreference是會把所有數據加載到內存的,存儲大數據或者大量數據會造成界面卡頓或者ANR,SP是輕量級存儲框架,如果要存儲較大數據,請考慮數據庫或者文件存儲方式。
  • apply進行存儲,而不是commit方法,因為apply是異步寫入磁盤的,所以效率上會比commit好,但是如果需要即存即用的話還是盡量使用commit。
  • 如果修改數據,盡量批量寫入后再調用apply或者commit,從源碼分析可以看到,無論是apply或者是commit,都是將修改的數據和原有數據合并,并執行全量寫入操作。多次調用apply或者commit不僅會發起多次IO操作,還會導致不必要的數據寫入。
  • 不要把所有數據都存儲在一個SP文件里邊,SP文件越大,讀寫速度越慢。因此,不同功能模塊的數據最好用不同的文件存儲,這樣可以提高SP的加載和寫入速度。。
  • 盡量不要存儲json或者html數據,因為json或者html在存儲時會引來額外的字符轉義開銷,如果數據比較大,會大大降低sp的讀取速度。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容