SharedPreference用法及源码分析

什么是SharedPreference

SharedPreference(以下简称SP)是Android提供的一个轻量级的持久化存储框架,主要用于保存一些比较小的数据,例如配置数据。SP是以“健-值”对的形式来保存数据,其实质是把这些数据保存到XML文件中,每个“健-值”对就是XML文件的一个节点,通过调用SP生成的文件最终会放在手机内部存储的/data/data//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来实现一个简单的记住登陆密码的功能。代码很简单,不做过多解释来。




    
        

        
    

    

        

        

    

    

        
        

    

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





    


//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 map = null;
        try {
            if (mFile.canRead()) {
                BufferedInputStream str = null;
                try {
                    str = new BufferedInputStream(new FileInputStream(mFile), 16 * 1024);
                    map = (Map) 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 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 keysModified = null;
            Set listeners = null;
            Map mapToWriteToDisk;
               mapToWriteToDisk = mMap; 
                synchronized (mEditorLock) {
                    boolean changesMade = false;
                    for (Map.Entry 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的读取速度。

你可能感兴趣的:(SharedPreference用法及源码分析)