美文网首页
SharedPreference用法及源码分析

SharedPreference用法及源码分析

作者: 咸鱼0907 | 来源:发表于2020-03-27 10:14 被阅读0次

    什么是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的读取速度。

    相关文章

      网友评论

          本文标题:SharedPreference用法及源码分析

          本文链接:https://www.haomeiwen.com/subject/qqmoehtx.html