Android SharedPreferences详解

前言

       Sharedpreferences是Android平台上一个轻量级的存储类,用来保存应用程序的各种配置信息,其本质是一个以“key-value”键值对的方式保存数据的xml文件,其文件保存在/data/data/com.xx.xxx/shared_prefs目录下,Android其它四种存储方式为:
       a. ContentProvider:将应用的私有数据提供给其他应用使用;
       b. 文件存储:以IO流形式存放,可分为手机内部和手机外部(sd卡等)存储,可存放较大数据;
       c. SQLiteDataBase:轻量级、跨平台数据库,将所有数据都是存放在手机上的单一文件内,占用内存小;
       d. 网络存储:数据存储在服务器上,通过连接网络获取数据;

       由于使用方式比较简单,平常的Android应用开发中经常用到Sharedpreferences,本文结合源码对平常使用及可能会出现的问题进行一一分析。

一.获取Sharedpreferences

       要想使用 SharedPreferences 来存储数据,首先需要获取到 SharedPreferences 对象,平常最常用的是通过以下方式来获取Sharedpreferences对象,通过调用context的getSharedPreferences()方法:

SharedPreferences mPreference = mContext.getSharedPreferences(SharePreferenceUtils.PREFERENCE_NAME,
                Context.MODE_PRIVATE);

       还有另外两种方式,一种是在Activity内部直接通过getPreferences()来获取该Activity类名对应name的SharedPreferences xml文件;一种是通过 PreferenceManager中的getDefaultSharedPreferences()静态方法,它接收一个 Context 参数,并自动使用当前应用程序的包名作为前缀来命名SharedPreferences xml文件;

二.使用方式

       SharedPreferences对象本身只能获取数据而不支持存储和修改,存储修改是通过SharedPreferences.edit()获取的内部接口Editor对象实现。使用Editor来存取数据,用到了SharedPreferences接口和SharedPreferences的一个内部接口SharedPreferences.Editor,使用方式如下:

a.写入数据:
//1.创建一个SharedPreferences对象
SharedPreferences sharedPreferences= getSharedPreferences(SharePreferenceUtils.PREFERENCE_NAME,
                Context.MODE_PRIVATE);
//2.实例化SharedPreferences.Editor对象
SharedPreferences.Editor editor = sharedPreferences.edit();
//3.将获取过来的值放入文件
editor.putString("xxx", “xxx”);
editor.putInt("xxx", 10);
editor.putBoolean("xxx",false);
//4.提交               
editor.commit();
editor.apply();
b.读取数据:
SharedPreferences sharedPreferences = getSharedPreferences(SharePreferenceUtils.PREFERENCE_NAME,
                Context.MODE_PRIVATE);
String name = sharedPreferences.getString("xxx","");
c.删除指定数据
editor.remove("xxx");
editor.commit();
d.清空数据
editor.clear();
editor.commit();

三.源码分析

       接下来根据调用关系,深入源码一起来详细的了解一下,看一下平时使用中可能遇到的问题及问题原因,本文主要对通过context.getSharedPreferences()方式来获取时的调用顺序及涉及到的类进行一一分析:

a.ContextImpl
private ArrayMap mSharedPrefsPaths;
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);
}

public File getSharedPreferencesPath(String name) {
    return makeFilename(getPreferencesDir(), name + ".xml");
}

       以上可以看到,在getSharedPreferences()内部会有一个Map缓存,如果Map缓存中有对应name的File,直接获取;否则的话,会通过getSharedPreferencesPath()来创建File,然后存入Map;最后通过getSharedPreferences()来返回SharedPreferences。

@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
        final ArrayMap cache = getSharedPreferencesCacheLocked();
        sp = cache.get(file);
        if (sp == null) {
            checkMode(mode);
            ......
            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) {
        sp.startReloadIfChangedUnexpectedly();
    }
    return sp;
}

       在getSharedPreferences内部返回的是一个SharedPreferencesImpl对象,SharedPreferences是一个接口,SharedPreferencesImpl是接口的实现类。

b.SharedPreferencesImpl

       通过获取方式可以看到,最终返回的是SharedPreferencesImpl对象,那么所有的操作都是通过该类来实现的,一起看一下:

private final File mFile;
private final File mBackupFile;
private final int mMode;
private Map mMap;

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

static File makeBackupFile(File prefsFile) {
    return new File(prefsFile.getPath() + ".bak");
}

       在获取SharedPreferencesImpl对象时,主要做了对应的赋值操作,执行了makeBackupFile()来备份文件,重点来看一下startLoadFromDisk():

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

       在startLoadFromDisk()内部现将mLoaded置为false,然后启动异步线程来执行loadFromDisk(),再看一下loadFromDisk():

private void loadFromDisk() {
    synchronized (mLock) {
        if (mLoaded) {
            return;
        }
        //检查源文件的备份文件是否存在:mBackupFile.exists(),如果存在,则将源文件删除:mFile.delete(),
        //然后将备份文件修改为源文件:mBackupFile.renameTo(mFile)。
        //后续操作就是从备份文件加载相关数据到内存 mMap 容器中了
        if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }
    }

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

    synchronized (mLock) {
        mLoaded = true;
        if (map != null) {
            mMap = map;
            ......
        } else {
            mMap = new HashMap<>();
        }
        mLock.notifyAll();
    }
}

       在loadFromDisk()内部主要干了五件事
       1.如果备份文件存在,那么就先删除mFile文件,然后将备份文件命名为mFile;
       2.通过XmlUtils.readMapXml将xml文件内的键值对读取为Map形式存在;
       3.加锁,将mLoaded赋值为true,表示已经加载完毕;
       4.如果map不为null,将读取的内容赋值给mMap(构造方法内置为null);
       5.调用mLock.notifyAll()来通知;
       以上就是通过getSharedPreferences()来获取SharedPreferences对象的过程,上述的3、4、5是简单的赋值或通知操作,为什么要标注出来呢,接着往下看,看一下读取操作:

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

       以上可以看到,在获取sp内的值时,先进行加锁操作,然后执行了awaitLoadedLocked(),从字面意思来看,是等待加载完成,那看一下内部实现:

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

       我们可以看到,如果mLoaded为false,那么会执行mLock.wait(),即进行等待,接下来就用到了上述讲的在loadFromDisk()内部最后赋值和通知操作了,如果在get()时mLoaded为false,那么表示没有加载完毕,就需要一直等待,等在loadFromDisk()内部加载完毕后才会结束等待,那么问题来了:为什么SharedPreference会造成卡顿甚至ANR?
       第一次从SharedPreference获取值的时候,可能阻塞主线程,造成卡顿/丢帧。获取SharedPreference可以立刻返回,耗时操作是在异步线程,但是去获取时如果异步线程没有加载完毕,会一直等待,造成卡顿,如果等待时间超过5s,甚至会造成ANR。注意:只有第一次才会,后面不会,因为加载文件成功后会在内存缓存数据,下次就不需要等待了。
       上面讲到了获取SharedPreference对象及获取sp存储的值,那么如何写入值呢?前面讲到通过edit()获取到Editor来进行写操作:

public Editor edit() {
    synchronized (mLock) {
        awaitLoadedLocked();
    }
    return new EditorImpl();
}

       每次执行edit()也需要等待锁,然后new一个EditorImpl对象,Editor是一个接口,内部的逻辑最终是通过其实现类EditorImpl来完成的,一起看一下:

c.EditorImpl
public final class EditorImpl implements Editor {
    private final Object mLock = new Object();

    @GuardedBy("mLock")
    private final Map mModified = Maps.newHashMap();

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

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

    public Editor clear() {
        synchronized (mLock) {
            mClear = true;
            return this;
        }
    }
}

       在向sp存数据时,先执行put相关的操作,接下来需要执行apply()及commit()来进行写入操作,那apply()和commit()有什么区别呢?
       1.apply()是异步操作,commit()是同步操作;
       2.apply()无返回值,commit()有boolean返回值;
       先看一下apply()

public void apply() {
   final MemoryCommitResult mcr = commitToMemory();
   final Runnable awaitCommit = new Runnable() {
            public void run() {
                try {
                    //阻塞等待写入文件完成,否则阻塞在这
                    //利用CountDownLatch来等待任务的完成
                    //后面执行enqueueDiskWrite写入文件成功后会把writtenToDiskLatch多线程计数器减1,
                    //这样的话下面的阻塞代码就可以通过了.
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException ignored) {
                }
                .......
            }
        };
    //QueuedWork是用来确保SharedPrefenced的写操作在Activity销毁前执行完的一个全局队列. 
    QueuedWork.addFinisher(awaitCommit);

    Runnable postWriteRunnable = new Runnable() {
            public void run() {
                //执行阻塞任务
                awaitCommit.run();
                //阻塞完成之后,从队列中移除任务
                QueuedWork.removeFinisher(awaitCommit);
            }
       };

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

       以上可以看到,在apply()中执行的操作为: 
       1.通过commitToMemory()创建MemoryCommitResult;
       2.创建一个Runnable awaitCommit;
       3.将awaitCommit执行addFinisher()[此处会有问题];
       4.创建一个Runnable postWriteRunnable;
       5.执行enqueueDiskWrite(),真正的写入操作;
       6.执行notifyListeners()通知sp数据变化;
       接下来看一下commit()

public boolean commit() {
    .......
    MemoryCommitResult mcr = commitToMemory();

    SharedPreferencesImpl.this.enqueueDiskWrite( mcr, null /* sync write on this thread okay */);
    try {
        mcr.writtenToDiskLatch.await();
    }.......
    notifyListeners(mcr);
    return mcr.writeToDiskResult;
}

       以上可以看到,在commit()中执行的操作为: 
       1.通过commitToMemory()创建MemoryCommitResult;
       2.执行enqueueDiskWrite();
       3.执行mcr.writtenToDiskLatch.await()[apply是在子线程中完成的]
       4.执行notifyListeners()通知sp数据变化;
       5.最后返回writeToDiskResult;
       上述可以看到,在apply()及commit()中都通过commitToMemory()创建了MemoryCommitResult,看一下commitToMemory()内部执行逻辑:

private MemoryCommitResult commitToMemory() {
    ......
    Map mapToWriteToDisk;

    synchronized (SharedPreferencesImpl.this.mLock) {
        if (mDiskWritesInFlight > 0) {
            mMap = new HashMap(mMap);
        }
        mapToWriteToDisk = mMap;
        mDiskWritesInFlight++;
        .........

        synchronized (mLock) {
            boolean changesMade = false;

            if (mClear) {
                if (!mMap.isEmpty()) {
                    changesMade = true;
                    mMap.clear();
                }
                mClear = false;
            }

            for (Map.Entry e : mModified.entrySet()) {
                String k = e.getKey();
                Object v = e.getValue();
                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;
                changesMade = true;
               ....
            }

            mModified.clear();
            if (changesMade) {
                //将该值+1,后续writeToFile()更新会用到
                mCurrentMemoryStateGeneration++;
             }

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

       以上可以看到,在commitToMemory()中执行的操作为: 
       1.将mMap赋值给局部变量mapToWriteToDisk[执行文件写入时会用到],mDiskWritesInFlight++[commit操作会调用到]
       2.如果clear为true,即当执行了clear()操作,会将mMap清空;
       3.遍历mModified,当v==this,说明执行了remove()操作,则将该值在mMap中移除,其他情况,即执行put()操作,先判断是否包含key,然后对比value,最后确定是否需要将值put到mMap中;
       4.将mModified清空,准备下次put操作;
       5.最后new MemoryCommitResult()返回;
       通过以上我们可以看到,当执行put()、remove()、clear()操作时,mMap是实时更新的,那么什么时候写入xml中的呢?在apply()及commit()中都执行了SharedPreferencesImpl.this.enqueueDiskWrite(mcr,postWriteRunnable),commit()时传入的postWriteRunnable为NULL,一起看一下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();
                }
            }
        };

    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (mLock) {
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            writeToDiskRunnable.run();
            return;
        }
    }

    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

       以上可以看到,在enqueueDiskWrite()中执行的操作为: 
       1.先判断postWriteRunnable是否为null,如果为null,说明是同步操作,即执行的commit()操作;
       2.创建了writeToDiskRunnable,在内部执行了writeToFile(),然后执行了postWriteRunnable;

private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
        .......
        if (fileExists) {
            boolean needsWrite = false;
            //判断是否需要写入文件,如果值没有变化的话,就不需要写入,在commitToMemory()中有对
          //memoryStateGeneration相关的操作
            if (mDiskStateGeneration < mcr.memoryStateGeneration) {
                if (isFromSyncCommit) {
                    needsWrite = true;
                } else {
                    synchronized (mLock) {
                        if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
                            needsWrite = true;
                        }
                    }
                }
            }

            if (!needsWrite) {
                //不需要wirte,直接setDiskWriteResult(),减少IO操作,commit()时会用来返回值,正常下返回true;
                mcr.setDiskWriteResult(false, true);
                return;
            }
            //判断备份文件是否存在
            boolean backupFileExists = mBackupFile.exists();
            //备份文件不存在,则在写入前需要先备份文件
            if (!backupFileExists) {
                if (!mFile.renameTo(mBackupFile)) {
                    //备份失败,直接返回
                    mcr.setDiskWriteResult(false, false);
                    return;
                }
            } else {
                //如果备份文件存在,将源文件删除[正常下备份文件是不应该存在的]
                mFile.delete();
            }
        }

        try {
            //创建mFile文件的输入流
            FileOutputStream str = createFileOutputStream(mFile);

            if (str == null) {
                mcr.setDiskWriteResult(false, false);
                return;
            }
            //向xml中写入值
            XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
            //强制更新到xml文件
            FileUtils.sync(str);
            str.close();
            ........
            //写入成功,将备份文件删除
            mBackupFile.delete();
            //赋值,为了下次的判断
            mDiskStateGeneration = mcr.memoryStateGeneration;
            mcr.setDiskWriteResult(true, true);
            mNumSync++;
            return;
        } ......
        //写入失败,将源文件删除
        if (mFile.exists()) {
            if (!mFile.delete()) {
                Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
            }
        }
        mcr.setDiskWriteResult(false, false);
    }

       3.如果是同步操作,直接在主线程执行postWriteRunnable,返回;
       4.如果是异步操作,即apply(),会将postWriteRunnable加入到QueuedWork队列中;

d.QueuedWork
private static final LinkedList sWork = new LinkedList<>();
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);
        }
    }
}

       以上可以看到,在执行QueuedWork的queue()操作: 
       1.通过getHandler()获取Handler;
       2.将Runnable work加入到sWork链表中;
       3.delay 100ms执行MSG_RUN消息;
       此时还没有看到什么时候执行的Runnable,接着往下看:

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

private static class QueuedWorkHandler extends Handler {
    static final int MSG_RUN = 1;

    QueuedWorkHandler(Looper looper) {
        super(looper);
    }

    public void handleMessage(Message msg) {
        if (msg.what == MSG_RUN) {
            processPendingWork();
        }
    }
}

private static void processPendingWork() {
    ......

    synchronized (sProcessingWork) {
        LinkedList work;

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

            // Remove all msg-s as all work will be processed now
            getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
        }

        if (work.size() > 0) {
            for (Runnable w : work) {
                w.run();
            }

        }
    }
}

       以上可以看到,在getHandler()内部会创建HandlerThread,通过消息队列,最终会执行Runnable的run()方法,然后里面执行耗时的WriteToFile()操作。
       至此,SharedPreferences相关的原理就分析完了。

四.总结

a.加载缓慢

       SharedPreferences文件的加载使用了异步线程,而且加载线程并没有设置优先级,如果在加载时读取数据就需要等待文件加载线程的结束。这就导致主线程等待低优先线程锁的问题,建议提前用预加载启动过程用到的SP文件。

b.commit和apply有什么区别?

       commit()是同步且有返回值;apply()方法是异步没有返回值;
       commit()在主线程写入文件,会造成UI卡顿;apply()在子线程写入文件,也有可能卡UI;
       apply()是在子线程写文件并不会造成UI线程卡顿,但是在ActivityThread的handlePauseActivity()、handleStopActivity()等方法中都会调用到:

QueuedWork.waitToFinish();

       看一下QueuedWork的waitToFinish()方法:

public static void waitToFinish() {
.......
    try {
        while (true) {
            Runnable finisher;

            synchronized (sLock) {
                finisher = sFinishers.poll();
            }

            if (finisher == null) {
                break;
            }

            finisher.run();
        }
    } finally {
         sCanDelay = true;
    }
}
......

       以上可以看到,循环地从sFinishers这个队列中取任务执行,直到任务为空。这个任务就是之前apply()中的awaitCommit,它是用来等待写入文件的线程执行完毕的。如果因为多次执行apply(),在onPause()时,那就意味着写入任务会在这里排队,但是写入文件那里只有一个HandlerThread在串行的执行,那是不是就卡顿了?

c.合并操作

       每次执行put()都会通过edit()创建一个EditorImpl对象,然后都会执行writeToFile()写文件操作,所以如果执行多次操作的话,建议合并为一次。

SharedPreferences.Editor editor = mPreference.edit();
editor.putInt(key,status).putString(key1,status1).putboolean(key2,status1).apply();
d.SharedPreference如何跨进程通信?
* @see #getSharedPreferences
*
* @deprecated MODE_MULTI_PROCESS does not work reliably in
* some versions of Android, and furthermore does not provide any
* mechanism for reconciling concurrent modifications across
* processes.  Applications should not attempt to use it.  Instead,
* they should use an explicit cross-process data management
* approach such as {@link android.content.ContentProvider ContentProvider}.
*/
@Deprecated
public static final int MODE_MULTI_PROCESS = 0x0004;
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();
}

       在初始化sp的时候,设置flag为MODE_MULTI_PROCESS来跨进程通信,但是很遗憾,这种方式已经被废弃。

e.全量写入

       无论是 commit() 还是 apply(),即使我们只改动其中一个条目,都会把整个内容全部写到文件。而且即使我们多次写同一个文件,SP也没有将多次修改合并为一次,这也是性能差的重要原因之一。

       总而言之,SharedPreferences是用来存储一些非常简单、轻量的数据。我们不要使用它存储过于复杂的数据,例如 HTML、JSON 等。而且 SharedPreferences 的文件存储性能与文件大小有关,每个 SP 文件不能过大,不要将毫无关联的配置项保存在同一个文件中,同时考虑将频繁修改的条目单独隔离出来。

你可能感兴趣的:(Android SharedPreferences详解)