SharedPreferences采用什么方式存储数据_sharedpreferences使用方法

2022-09-22 14:19:09 浏览数 (1)

大家好,又见面了,我是你们的朋友全栈君。

SharedPreferences(简称sp)Android平台上一个轻量级的存储辅助类,它提供了key-value键值对的接口,用来保存应用的一些常用配置,在应用中通常做一些简单数据的持久化缓存。本文将详细的分析SharedPreferences的实现方式、存储机制、如何正确使用它以及sp的性能问题等方面。

SharedPreferences实现详解


我们在Android开发中,如果想要保存一个相对较小的键值对集合,则应使用SharedPreferences API。SharedPreferences对象指向包含键值对的文件,并提供读写这些键值对的简单方法。SharedPreferences API提供了string,set,int,long,float,boolean六种数据类型的数据访问接口。sp文件在存储区最终数据是以xml形式进行存储。

获取SharedPreferences对象

想要使用sp来存取数据,我们首先要了解如何去获取它,Android的Context类为我们提供了获取SharedPreferences对象的抽象接口。

Context对象的getSharedPreferences()方法可以获取一个SharedPreferences对象,之后我们就可以通过SharedPreferences来管理我们的键值对数据了。

Context中的方法定义:
代码语言:javascript复制
    public abstract SharedPreferences getSharedPreferences(File file, @PreferencesMode int mode);

context类为我们提供了访问sp的抽象接口,真正的而实现实在ContextImpl类中。

ContextImpl中的方法实现:

我们来看源码实现:

代码语言:javascript复制
    @Override
    public SharedPreferences getSharedPreferences(File file, int mode) {
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
            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;
    }
参数:
  • file参数用于指定SharedPreferences文件的名称,如果指定的文件不存在则会创建一个,SharedPreferences文件都是存放在“/data/data/<包名>/shared_prefs/”目录下。
  • mode参数用于指定操作模式,它的可选值有2种,MODE_PRIVATE(默认值,指定文件是私有的,只可被当前应用访问或者相同user ID的进程)和MODE_MULTI_PROCESS(多进程共享模式)。
逻辑解析:
  1. getSharedPreferences首先会从内存缓存中查找是否已经存在我们想要获取的SharedPreferences实例。
  2. 如果不存在,则会为相应的file创建一个SharedPreferencesImpl实例,并且将它添加到缓存中。
  3. 判断如果参数mode的值是Context.MODE_MULTI_PROCESS或者App的targetSdkVersion小于Android 3.0,则执行调用sp的startReloadIfChangedUnexpectedly方法执行sp文件的重新加载工作,我们稍后分析。
MODE_MULTI_PROCESS模式

我们通常调用getSharedPreferences方法时,使用默认模式即可,也是google推荐的方式。Context.MODE_MULTI_PROCESS是多线程共享模式,理论上可以做到多进程数据共享功能,但是,此功能已废弃,不建议使用了。Context.MODE_MULTI_PROCESS模式是在Android 3.0之前版本的遗留功能,在之后某些版本的Android中无法可靠工作,而且也不提供任何协调跨进程并发修改的机制。我们不应该尝试使用该模式,如果我们有跨进程数据传输的需求,应该使用明确的跨进行数据共享机制,例如ContentProvider等来实现。

另外,MODE_MULTI_PROCESS模式需要进程在每次访问数据时都要进行io操作,以检查数据是否被其他进程改变,这样io操作造成了很大的性能消耗,如果我们在开发中误使用了该模式,应立即改成默认模式!

sp数据文件从存储分区加载到内存过程分析

SharedPreferences工具类为我们提供了管理sp数据的接口,从而简化了数据存取操作。sp数据文件最终是以.xml文件的格式,存储到App数据私有目录:/data/data/<app包名>/shared_prefs/目录下,那么sp文件是如何从存储区加载到内存中的呢?

在Context的getSharedPreferences方法获取SharedPreferences对象时,我们发现如果参数mode的值是Context.MODE_MULTI_PROCESS或者App的targetSdkVersion小于Android 3.0,则执行调用sp的startReloadIfChangedUnexpectedly方法执行sp文件的重新加载工作,这里就涉及到了sp文件加载过程,同样SharedPreferencesImpl类的构造过程也会涉及到sp文件的加载。

android.app.SharedPreferencesImpl:
代码语言:javascript复制
    @UnsupportedAppUsage
    SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file);//这里注意,创建了一个备份文件。
        mMode = mode;
        mLoaded = false;
        mMap = null;
        mThrowable = null;
        startLoadFromDisk(); //加载xml文件到内存中
    }
SharedPreferencesImpl类的startReloadIfChangedUnexpectedly方法:
代码语言:javascript复制
    @UnsupportedAppUsage
    void startReloadIfChangedUnexpectedly() {
        synchronized (mLock) {
            // TODO: wait for any pending writes to disk?
            if (!hasFileChangedUnexpectedly()) {
                return;
            }
            startLoadFromDisk();
        }
    }

SharedPreferencesImpl的构造方法和startReloadIfChangedUnexpectedly都调用到了startLoadFromDisk()方法。

startLoadFromDisk()方法:
代码语言:javascript复制
    @UnsupportedAppUsage
    private void startLoadFromDisk() {
        synchronized (mLock) {
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk();
            }
        }.start();
    }
逻辑解析:
  1. startLoadFromDisk方法中,先重置mLoaded状态。
  2. 创建一个工作线程,线程名为SharedPreferencesImpl-load。
  3. 线程调用loadFromDisk()方法执行sp文件从存储分区到内存的加载工作。
loadFromDisk()方法:
代码语言:javascript复制
    private void loadFromDisk() {
        synchronized (mLock) {
            if (mLoaded) { //判断mLoaded状态
                return;
            }
            if (mBackupFile.exists()) { //如果备份文件存在,则代表原始文件数据出现错误,使用备份文件,替换掉原始文件。
                mFile.delete();
                mBackupFile.renameTo(mFile);
            }
        }
        ……

        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); //获取输入流
                    map = (Map<String, Object>) XmlUtils.readMapXml(str);//解析xml文件到map中
                } 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;
                        mStatTimestamp = stat.st_mtim;
                        mStatSize = stat.st_size;
                    } else {
                        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();
            }
        }
    }
逻辑解析:
  1. 判断备份文件是否存在,如果存在则代表原始文件数据出现错误,使用备份文件,替换掉原始文件。
  2. 如果文件存在并且可读取,则把字节流读取到内存中,并且使用XmlUtils.readMapXml工具方法对原始数据进行解析。
  3. 数据解析后得到一个Map对象,它保存了该sp文件中存储的所有键值对的信息。
  4. 最后把得到的Map对象赋值给mMap属性,mStatTimestamp和mStatTimestamp用于判断数据是否更新。

SharedPreferences数据在内存中的存储结构

SharedPreferences文件都是存放在“/data/data/<app包名>/shared_prefs/”目录下。sp文件的存储格式是.xml文件,当SharedPreferences文件创建时,就会在相应目录新建一个本地文件。

我们可以从ContextImpl中看到sp文件是如何管理的:

代码语言:javascript复制
    private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;

这里会定义一个ArrayMap成员变量,存储了当前应用的所有sp对象。这样做是系统为了性能考虑,在每个sp文件读取之后,都会把sp对象存储到一个map中作为缓存。

ArrayMap对象的创建及赋值过程:
代码语言:javascript复制
    @GuardedBy("ContextImpl.class")
    private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
        if (sSharedPrefsCache == null) {
            sSharedPrefsCache = new ArrayMap<>();
        }

        final String packageName = getPackageName();
        ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
        if (packagePrefs == null) {
            packagePrefs = new ArrayMap<>();
            sSharedPrefsCache.put(packageName, packagePrefs);
        }

        return packagePrefs;
    }

从代码中我们可以看出,系统对SharedPreferences对象数据的存储结构是什么。

SharedPreferences对象数据的存储结构:
  • 首先以包名为key,存储一个名为sSharedPrefsCache的ArrayMap队列中。
  • 在ArrayMap中,又是以文件名为key,SharedPreferencesImpl对象(sp的实例对象)为value存储的。
  • 每一个SharedPreferencesImpl对象都对应一个物理位置在的sp文件。
  • SharedPreferencesImpl对象在创建时,或者sp文件有更新时,都会去存储区同步数据文件,并且把数据文件存储到一个map中(key/vaule分别对应我们存储时的key/value),该map就是上文中提到的SharedPreferencesImpl对象的mMap属性。

SharedPreferences的数据读取过程分析

我们以获取int值为例,来看sp的数据读取过程。

SharedPreferencesImpl类的getInt方法:
代码语言:javascript复制
    @Override
    public int getInt(String key, int defValue) {
        synchronized (mLock) {
            awaitLoadedLocked();
            Integer v = (Integer)mMap.get(key);
            return v != null ? v : defValue;
        }
    }

SharedPreferencesImpl类的getInt方法可以获取一个在sp文件中存储的int值。这里可以看到,源码中是直接从mMap中读取的,而这个mMap是SharedPreferencesImpl在创建时初始化的。这种做法,可以避免每次读取时,系统和存储分区的交互,从而大幅度提升了性能。

其他几种类型的数据读取逻辑类似,这里可以看到,读取过程相对来说非常简单,当SharedPreferencesImpl实例创建完成后,sp的xml文件中的数据已经加载到内存中,所以这里获取时,只需要简单的内存查询即可。

SharedPreferences数据存储过程分析

数据存储过程相对来说比较复杂,我们先来看如何使用sp来实现存储。

SharedPreferences数据存储示例

如果我们想要通过SharedPreferences存储数据,代码如下:

代码语言:javascript复制
        SharedPreferences.Editor editor = getSharedPreferences("person", MODE_PRIVATE).edit();
        editor.putString("name","budaye");
        editor.putInt("age",18);
        editor.apply();

代码执行之后,系统会在/data/data/<app包名>/shared_prefs/目录下,创建一个名为person.xml的文件:

代码语言:javascript复制
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="name">budaye</string>
    <int name="age" value="18" />
</map>

sp的数据存储,需要借助SharedPreferences的内部类Editor来实现,并且最后要使用apply()或commit()来保存更改。

我们来看源码。

SharedPreferences.Editor的putInt方法分析

我们以putInt为例,来分析sp数据的存储过程。

SharedPreferences的edit()方法:
代码语言:javascript复制
    public Editor edit() {
        synchronized (mLock) {
            awaitLoadedLocked();//等待锁释放
        }

        return new EditorImpl();
    }

该方法new一个EditorImpl对象并返回,所以SharedPreferences.Editor的实现类是EditorImpl。

EditorImpl的putInt方法
代码语言:javascript复制
        @GuardedBy("mEditorLock")
        private final Map<String, Object> mModified = new HashMap<>();
        
        @Override
        public Editor putInt(String key, int value) {
            synchronized (mEditorLock) {
                mModified.put(key, value);
                return this;
            }
        }

这里只是简单的将key和value的值put到了mModified中,mModified是一个Map,它存储者一次事务提交的所有将要变更的数据列表。

EditorImpl的apply方法

EditorImpl的apply方法:

代码语言:javascript复制
        @Override
        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);
            ……
            notifyListeners(mcr);
        }
逻辑解析:
  1. 调用commitToMemory()来把数据更新至内存中。
  2. apply()方法使用异步的方式来实现数据的写入过程(写入存储分区)。
  3. 调用SharedPreferencesImpl的enqueueDiskWrite方法来执行数据写入工作,注意这里的第二个参数,如果不为null,则代表异步写入。

我们再来看commit()方法。

EditorImpl的commit方法
代码语言:javascript复制
        @Override
        public boolean commit() {
            long startTime = 0;

            if (DEBUG) {
                startTime = System.currentTimeMillis();
            }

            MemoryCommitResult mcr = commitToMemory();

            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;
        }
逻辑解析:
  1. commit方法同样先调用了commitToMemory()来把数据更新至内存中。
  2. 然后调用SharedPreferencesImpl的enqueueDiskWrite方法来执行数据写入工作,注意这里是同步写入,第二个参数为null。

apply和commit方法都调用了两个关键方法:commitToMemory和SharedPreferencesImpl的enqueueDiskWrite方法,我们逐个分析。

EditorImpl的commitToMemory方法
代码语言:javascript复制
        private MemoryCommitResult commitToMemory() {
            long memoryStateGeneration;
            List<String> keysModified = null;
            Set<OnSharedPreferenceChangeListener> listeners = null;
            Map<String, Object> mapToWriteToDisk;

            synchronized (SharedPreferencesImpl.this.mLock) {
                if (mDiskWritesInFlight > 0) {
                    mMap = new HashMap<String, Object>(mMap); //如果其他线程也正在进行写入操作,我们先把mMap的键值对复制出一份。
                }
                mapToWriteToDisk = mMap; //直接在mapToWriteToDisk上进行操作
                mDiskWritesInFlight  ;

                boolean hasListeners = mListeners.size() > 0;
                if (hasListeners) {
                    keysModified = new ArrayList<String>();
                    listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
                }

                synchronized (mEditorLock) {
                    boolean changesMade = false;

                    if (mClear) {//如果调用了Editor的clear,则先将map中的数据进行清除。
                        if (!mapToWriteToDisk.isEmpty()) {
                            changesMade = true;
                            mapToWriteToDisk.clear();
                        }
                        mClear = false;
                    }

                    for (Map.Entry<String, Object> e : mModified.entrySet()) { //遍历mModified中的数据
                        String k = e.getKey();
                        Object v = e.getValue();
                        // "this" is the magic value for a removal mutation. In addition,
                        // setting a value to "null" for a given key is specified to be
                        // equivalent to calling remove on that key.
                        if (v == this || v == null) {
                            if (!mapToWriteToDisk.containsKey(k)) {//key和value都是空值,则跳过该条数据
                                continue;
                            }
                            mapToWriteToDisk.remove(k);//key值存在,value为null,则将数据删除。
                        } else {
                            if (mapToWriteToDisk.containsKey(k)) {
                                Object existingValue = mapToWriteToDisk.get(k);
                                if (existingValue != null && existingValue.equals(v)) {//key在map中已经存在,并且value没有改变,则跳过
                                    continue;
                                }
                            }
                            mapToWriteToDisk.put(k, v);//将key/value写入map中
                        }

                        changesMade = true;
                        if (hasListeners) {
                            keysModified.add(k);
                        }
                    }

                    mModified.clear();

                    if (changesMade) {
                        mCurrentMemoryStateGeneration  ;
                    }

                    memoryStateGeneration = mCurrentMemoryStateGeneration;
                }
            }
            return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
                    mapToWriteToDisk);//返回结果
        }
逻辑解析:
  1. mMap中存储的是该sp文件的所有数据,是sp文件在内存中的映射。
  2. 判断其他线程是否也正在进行写入操作,如果是,则把mMap的键值对复制出一份。
  3. 将mMap赋值给mapToWriteToDisk变量,后面直接在mapToWriteToDisk上进行操作。
  4. 如果调用了Editor的clear,则将map中的数据进行清除。
  5. 遍历mModified中的数据,mModified保存了本次事务提交的所有修改,上文中的putInt的数据,就存在该Map中。
  6. 判断,key和value都是空值,则跳过该条数据。
  7. key值存在,value为null,则将数据删除。
  8. key在map中已经存在,并且value没有改变,则跳过。
  9. 最后将key和value写入mapToWriteToDisk中。
  10. 如果数据已经改变,则设置changesMade变量为true。
  11. 最后返回已修改的内存数据对象MemoryCommitResult。
SharedPreferencesImpl的enqueueDiskWrite方法

数据在内存中更新之后,最后一步就是写入存储分区了,我们来看它对应的方法。

SharedPreferencesImpl的enqueueDiskWrite方法:

代码语言:javascript复制
    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);
    }
逻辑解析:

首先判断是否需要同步,这里可以看到apply是异步的,commit是同步的(有条件的同步)。 如果只有一个线程在执行写入操作mDiskWritesInFlight的值是1,则直接调用writeToFile方法执行写入工作。 否则,调用QueuedWork.queue方法添加到任务队列中执行,等待执行。 这里commit同步提交也是有条件的,如果commit时,该sp文件正在被其他线程执行数据写入,则执行异步写入。

Queued类的queue方法

我们来看异步写入的执行:

代码语言:javascript复制
    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);//如果是applay提交,这里直接delay了100毫秒再执行
            } else {
                handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
            }
        }
    }

异步任务是通过HandlerThread来实现的,我们来看它的初始化过程:

代码语言:javascript复制
    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;
        }
    }

这里创建了一个HandlerThread来执行异步,也就是任务队列是单线程的,并且线程优先级是前台线程优先级。

SharedPreferencesImpl的writeToFile方法

写入存储分区真正的执行方法是SharedPreferencesImpl的writeToFile方法:

代码语言:javascript复制
    private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
        ……
        boolean fileExists = mFile.exists(); //sp文件是否存在

        ……
        if (fileExists) {
            boolean needsWrite = false; //本次是否需要执行写入操作

            // Only need to write if the disk state is older than this commit
            if (mDiskStateGeneration < mcr.memoryStateGeneration) { //mDiskStateGeneration是一个long类型的值,记录着最后一次数据提交的状态值。
                if (isFromSyncCommit) {//如果是同步写入操作,这里指commit提交的,直接执行写入存储分区操作
                    needsWrite = true;
                } else {
                    synchronized (mLock) {
                        // No need to persist intermediate states. Just wait for the latest state to
                        // be persisted.
                        if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) { //如果是异步写入,这里指apply提交的,则判断是否是最新的一次写入请求,如果不是,则不执行写入存储分区操作,以优化性能。
                            needsWrite = true;
                        }
                    }
                }
            }

            if (!needsWrite) { //不需要执行数据写入
                mcr.setDiskWriteResult(false, true);
                return;
            }

            boolean backupFileExists = mBackupFile.exists();//判断备份是否存在

            if (DEBUG) {
                backupExistsTime = System.currentTimeMillis();
            }

            if (!backupFileExists) {//如果备份不存在,则把sp原始文件重命名为备份文件。
                if (!mFile.renameTo(mBackupFile)) {
                    Log.e(TAG, "Couldn't rename file "   mFile
                            " to backup file "   mBackupFile);
                    mcr.setDiskWriteResult(false, false);
                    return;
                }
            } else {//备份存在,则将file文件删除,因为它可能是错误数据。
                mFile.delete();
            }
        }
        //到了这里,sp对应的原始文件已经被删除了,只存在备份文件了。
        // Attempt to write the file, delete the backup and return true as atomically as
        // possible.  If any exception occurs, delete the new file; next time we will restore
        // from the backup.
        try {
            FileOutputStream str = createFileOutputStream(mFile);//创建一个空的原始文件,以存储sp数据。

            if (DEBUG) {
                outputStreamCreateTime = System.currentTimeMillis();
            }

            if (str == null) {
                mcr.setDiskWriteResult(false, false);
                return;
            }
            XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);//执行xml数据解析,将内存中的key-value键值对存储到str的数据流中。

            writeTime = System.currentTimeMillis();

            FileUtils.sync(str);//将数据流写入到存储分区中。

            fsyncTime = System.currentTimeMillis();

            str.close();
            ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);

            ……
            mBackupFile.delete();//写入完成后,将备份文件删除。

            ……

            mDiskStateGeneration = mcr.memoryStateGeneration;//更新mDiskStateGeneration的值,代表了最后一次写入时的值

            mcr.setDiskWriteResult(true, true);

            ……

            return;
        } catch (XmlPullParserException e) {
            Log.w(TAG, "writeToFile: Got exception:", e);
        } catch (IOException e) {
            Log.w(TAG, "writeToFile: Got exception:", e);
        }

        // 如果磁盘写入失败,则删除原始sp文件。
        if (mFile.exists()) {
            if (!mFile.delete()) {
                Log.e(TAG, "Couldn't clean up partially-written file "   mFile);
            }
        }
        mcr.setDiskWriteResult(false, false);
    }
逻辑解析:

sp文件写入存储分区(磁盘)的工作是由SharedPreferencesImpl的writeToFile方法来完成的,逻辑较多,根据代码逻辑,我们简单总结一些它的过程:

  1. 首先使用fileExists变量来判断sp文件是否存在。
  2. 如果存在,判断本次是否需要执行写入操作,用变量needsWrite表示。mDiskStateGeneration是一个long类型的值,记录着最后一次数据提交的状态值,用来判断是否存在更新。
  3. 如果是commit提交的,则needsWrite赋值为true,执行数据写入。
  4. 如果是apply提交的,则判断是否是最新的一次写入请求如果是,则needsWrite赋值为true,如果不是(是一个中间提交值,之后还有提交),则不执行写入存储分区操作,以优化性能。
  5. 如果needsWrite的值为false则不需要执行数据写入,结束本次任务。
  6. 接下来判断sp文件备份是否存在,如果备份不存在,则把sp原始文件重命名为备份文件。如果备份存在,则将file文件删除,因为它可能是错误数据。
  7. 2~6是sp文件存在时的处理逻辑。到了这里,sp对应的原始文件已经被删除了,只存在备份文件了(如果存在的话)。
  8. 接下来执行写入存储分区,首先创建一个空文件,以存储sp数据。
  9. 执行xml数据解析,将内存中的key-value键值对存储到str的数据流中。
  10. 将数据流写入到存储分区中。
  11. 写入完成后,将备份文件删除。
  12. 更新mDiskStateGeneration的值,代表了最后一次写入时的状态值。
  13. 最后,如果磁盘写入失败,则删除原始sp文件。

好了,到了这里,SharedPreferences的实现原理我们也就分析完了,那么在使用过程时,你是否也了解了SharedPreferences的正确打开方式呢?

SharedPreferences性能问题及最佳实践


sp文件的io操作

  • sp文件存储在“/data/data/<app包名>/shared_prefs/”目录下,存储格式是以.xml文件的形式存在。
  • sp文件在创建SharedPreferencesImpl对象时,会把文件从磁盘分区加载到内存中,并且存储到Map中。
  • sp文件读取是在子线程中进行的,子线程的优先级等同于它的父线程优先级。
  • sp文件在更新时,首先会更新到内存的Map对象中,根据提交的方式,commit通常会强制、同步写入,apply会执行异步写入工作,并且会等待100ms。
  • sp在异步提交时,使用的是ThreadHandler,线程优先级为前台任务的单线程操作,如果任务很多,怎会等待。
  • sp文件在写入时,会删除旧文件,新建新的文件,重新执行写入。
  • sp文件的写入更新方式是,全文件内容替换。
  • Context.MODE_MULTI_PROCESS模式时,每次调用getSharedPreferences获取SharedPreferences对象时,都会检查数据是否更新,如果更新,则从磁盘重新加载文件到内存中。

SharedPreferences的性能及最佳实践

sp的性能问题:
  1. sp文件存储在App私有目录,所以会随着App卸载而删除。
  2. sp文件存储的数据格式是.xml,每次从磁盘读取和写入操作,都需要解析xml,效率不高。
  3. sp的大量使用会占用大量的内存,因为它会把所有用到的sp文件内容都同步到内存中。
  4. sp错误使用,会导致大量的io操作,影响系统性能,例如,频繁的commit或apply。
  5. Context.MODE_MULTI_PROCESS模式的误用,会产生大量的io操作,严重影响性能。
  6. sp是在新建线程执行初始化工作,如果App启动时,在主线程执行大量的sp初始化工作,会创建大量的线程,且线程优先级同UI线程,这样会造成sp线程抢占UI线程资源,造成启动过慢等问题。
  7. sp如果是在优先级较低子线程中执行sp的初始化工作,则sp加载过程可能会变的很长。
  8. sp在提交时,如果在ui线程中使用commit同步提交,则可能会导致因等待而产生的ANR问题。
  9. sp每次更新到磁盘都是整体写入,性能影响较大。
  10. sp在执行数据写入时,都会创建EditorImpl对象,大量的提交操作会创建大量的EditorImpl对象,占用大量内存。
  11. sp跨进程访问模式,不可靠,已废弃。
  12. 当单个sp文件大于50k时(经验值,不同机器差别较大),io会变的非常缓慢。
  13. sp文件在执行apply写入时,至少要等到100ms以上。
sp的最佳实践
  1. 推荐使用sp存储一些数据量较小的应用配置类信息。
  2. 不要使用sp的Context.MODE_MULTI_PROCESS模式;不要指望使用sp来进行跨进程数据操作。
  3. 单个sp文件大小最好保持在10kb以内,最大不要超过50kb。
  4. 将不同的业务数据保存在不同的sp文件内,不要一个文件存储所有数据。
  5. sp数据更新时,最好多次修改后,统一执行一次commit或apply,以减少io次数。
  6. sp文件数量也要进行控制,以减少线程数量和内存使用。
  7. ui线程中使用sp数据要注意时效性,最好在使用之前,预加载到内存。
  8. sp加载时,会在子线程执行,子线程的优先级等同于父线程,一定要注意加载的时间。

发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/169755.html原文链接:https://javaforall.cn

0 人点赞