一篇文章彻底理解SharedPreferences

一、概述

SharedPreferences简称Sp(后面都会称Sp),是一种轻量级的数据存储方式,采用Key/value的方式
进行映射,最终会在手机的/data/data/package_name/shared_prefs/目录下以xml的格式存在。
Sp通常用于记录一些参数配置、行为标记等!因为其使用简单,所以大多数开发者用起来很爽!但是
请注意:千万不要使用Sp去存储量大的数据,也千万不要去让你的Sp文件超级大,否则会大大影响应用性能,
甚至出现ANR,没错是ANR(下面会分析)。

二、使用分析

1.获取一个SharedPreferences
/**
*param:name在/data/data/package_name/shared_prefs/目录下生成的文件的名字(如果该文件不存在就会创建,如果存在则更新)
*param:mode该文件的访问模式(Context.MODE_PRIVATE:默认的创建模式,只能由创建它的或者UID相同的应用程序访问,其余三种已经废弃,故不介绍)
*传送门:https://developer.android.com/reference/android/content/Context.html#MODE_PRIVATE
*/
SharedPreferences sharedPreferences = getSharedPreferences(NAME_SP_TEST, Context.MODE_PRIVATE);

2.通过Eidt更新数据

传送门:https://developer.android.com/reference/android/content/SharedPreferences.Editor
/**
*获取一个Edit对象,所有对数据的操作都需要经过Edit
*/
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putBoolean(“Lbjfan”,true);

/**
*apply和commit是两种不同的写入方式(源码会说到),这时候更新才会生效(有则添加,无则更新)
*/
editor.apply();//editor.commit();

三.源码分析

1.getSharedPreferences();

 public SharedPreferences getSharedPreferences(String name, int mode) {
     Class var4 = ContextImpl.class;
     SharedPreferencesImpl sp;
     synchronized(ContextImpl.class) {
         //一个全局的静态对象,定义如下:private static ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>> sSharedPrefs;
         if (sSharedPrefs == null) {
             sSharedPrefs = new ArrayMap();
         }
         //根据应用程序的包名在程序的执行环境中取到该应用程序对应的SharedPreferences文件组
         String packageName = this.getPackageName();
         ArrayMap<String, SharedPreferencesImpl> packagePrefs = (ArrayMap)sSharedPrefs.get(packageName);
         //如果不存在就创建,并缓存到全局的ArrayMap(sSharedPrefs)中
         if (packagePrefs == null) {
             packagePrefs = new ArrayMap();
             sSharedPrefs.put(packageName, packagePrefs);
         }

         if (this.mPackageInfo.getApplicationInfo().targetSdkVersion < 19 && name == null) {
             name = "null";
         }
        //根据文件的Name去拿到对应的SharedPreferencesImpl对象,如果没有就创建,并放入到自己的上一级缓存packagePrefs中
         sp = (SharedPreferencesImpl)packagePrefs.get(name);
         if (sp == null) {
             File prefsFile = this.getSharedPrefsFile(name);
             sp = new SharedPreferencesImpl(prefsFile, mode);
             packagePrefs.put(name, sp);
             return sp;
         }
     }

     if ((mode & 4) != 0 || this.getApplicationInfo().targetSdkVersion < 11) {
         sp.startReloadIfChangedUnexpectedly();
     }

     return sp;
 }

总结起来就是:由于ContextImpl是应用程序的执行环境,每个应用程序里面可以包含有多个SharedPreference文件。因此,为了更好的定位SharedPreference文件,首先根据应用程序进行筛选,得到ArrayMap 然后再通过SharedPreference文件名进行筛选,得到SharedPreferencesImpl。可以看到,SharedPreferencesImpl只会被创建一次,之后会被保存在缓存中,后续的获取操作都是从缓存中获取SharedPreferencesImpl实例对象。

获取文件目录的函数,注意到shared_prefs这个关键词

 private File getPreferencesDir() {
    Object var1 = this.mSync;
    synchronized(this.mSync) {
        if (this.mPreferencesDir == null) {
            this.mPreferencesDir = new File(this.getDataDirFile(), "shared_prefs");
        }
        return this.mPreferencesDir;
    }
}

2.SharedPreferencesImpl的构造函数
构造函数主要进行文件备份,加载状态初始化等准备操作,并开始将文件从磁盘加载到内存

 SharedPreferencesImpl(File file, int mode) {
    this.mFile = file;
    //同名的.bak文件用于当发生异常时,用于恢复数据
    this.mBackupFile = makeBackupFile(file);
    this.mMode = mode;
    this.mLoaded = false;
    this.mMap = null;
    this.startLoadFromDisk();
}

从磁盘读取文件

private void startLoadFromDisk() {
    //同步一个标记位,是否已经从磁盘读取文件,读取到以后就不用再去读取
    synchronized (mLock) {
        mLoaded = false;
    }
    //开启一个子线程去从磁盘加载文件,文件读取属于耗时操作,主线程容易ANR
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            loadFromDisk();
        }
    }.start();
}

3.具体读文件的操作

private void loadFromDisk() {
        synchronized (mLock) {
            //保证只加载一次
            if (mLoaded) {
                return;
            }
            //如果备份文件已经存在了,则删除当前文件,用备份文件来代替
            if (mBackupFile.exists()) {
                mFile.delete();
                mBackupFile.renameTo(mFile);
            }
        }

        // Debugging
        if (mFile.exists() && !mFile.canRead()) {
            Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
        }

        Map<String, Object> map = null;
        StructStat stat = null;
        Throwable thrown = null;
        //具体的文件读取操作,然后解析XML,存储在Map中!这也是为何SharedPrefernces不是线程安全的,因为使用的是Map数据结构
        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);
                } 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置为空,让等待加载完成的线程被唤醒
            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中
                        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();
            }
        }
    }

4.数据获取

public String getString(String key, @Nullable String defValue) {
    synchronized (mLock) {
        //等待文件加载到内存
        awaitLoadedLocked();
        //直接从内存中的Map中读取
        String v = (String)mMap.get(key);
        return v != null ? v : defValue;
    }
}
private void awaitLoadedLocked() {
    if (!mLoaded) {
        // Raise an explicit StrictMode onReadFromDisk for this
        // thread, since the real read will be in a different
        // thread and otherwise ignored by StrictMode.
        BlockGuard.getThreadPolicy().onReadFromDisk();
    }
    //等待文件记载到内存
    while (!mLoaded) {
        try {
            //线程Wait阻塞
            mLock.wait();
        } catch (InterruptedException unused) {
        }
    }
    if (mThrowable != null) {
        throw new IllegalStateException(mThrowable);
    }
}

小结:一般情况下使用Sp获取数据操作的操作都是在主线程进行的,如果第一次获取数据的时候,Sp文件过大,那么子线程从磁盘获取文件的操作比较耗时,导致主线程等待的时间大于5秒,此时就会发生ANR

5.数据更新

SharedPreferences.Editor editor = sharedPreferences.edit();//Edit的实现类EditorImpl
editor.putString();
editor.apply() or editor.commit()

构造方法

public Editor edit() {
        //
        synchronized (mLock) {
            //同样使调用Edit的线程等待,如果我们在主线程中切Sp文件没有被加载到内存中也会造成阻塞,引发ANR
            awaitLoadedLocked();
        }

        return new EditorImpl();
}

put方法

public Editor putString(String key, @Nullable String value) {
    synchronized (this) {
        //更新到内存中
        mModified.put(key, value);
        return this;
    }
}

apply和commit方法
commit方法(有返回值,告诉开发者成功还是失败)

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 {
        //等待写入结果,主线程调用,写入时间过长会导致阻塞,发生ANR
        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;
}

构建需要写入的数据

private MemoryCommitResult commitToMemory() {
            long memoryStateGeneration;
            List<String> keysModified = null;
            Set<OnSharedPreferenceChangeListener> listeners = null;
            Map<String, Object> mapToWriteToDisk;

            synchronized (SharedPreferencesImpl.this.mLock) {
                // We optimistically don't make a deep copy until
                // a memory commit comes in when we're already
                // writing to disk.
                if (mDiskWritesInFlight > 0) {
                    // We can't modify our mMap as a currently
                    // in-flight write owns it. Clone it before
                    // modifying it.
                    // noinspection unchecked
                    //将内存中的数据进行深拷贝
                    mMap = new HashMap<String, Object>(mMap);
                }
                //讲内存中Map的值赋给需要写入文件的Map
                mapToWriteToDisk = mMap;
                //写入文件的次数
                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) {
                        if (!mapToWriteToDisk.isEmpty()) {
                            changesMade = true;
                            mapToWriteToDisk.clear();
                        }
                        mClear = false;
                    }
                    //对Key/Value的处理,有则更新,无则添加
                    for (Map.Entry<String, Object> e : mModified.entrySet()) {
                        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) {
                            //如果已经存在相同的key/value,则直接返回,否则添加到需要写入磁盘的map中
                            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);
                        }

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

                    mModified.clear();

                    if (changesMade) {
                        mCurrentMemoryStateGeneration++;
                    }

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

创建并开启一个线程写入磁盘

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) {
                               //同时写入磁盘的计数-1
                                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);
            }

写入文件的操作

private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
                long startTime = 0;
                long existsTime = 0;
                long backupExistsTime = 0;
                long outputStreamCreateTime = 0;
                long writeTime = 0;
                long fsyncTime = 0;
                long setPermTime = 0;
                long fstatTime = 0;
                long deleteTime = 0;

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

                boolean fileExists = mFile.exists();

                if (DEBUG) {
                    existsTime = System.currentTimeMillis();

                    // Might not be set, hence init them to a default value
                    backupExistsTime = existsTime;
                }

                // Rename the current file so it may be used as a backup during the next read
                if (fileExists) {
                    boolean needsWrite = false;

                    // Only need to write if the disk state is older than this commit
                    // 只有磁盘上文件状态比当前文件更旧时,才执行更新操作
                    if (mDiskStateGeneration < mcr.memoryStateGeneration) {
                        if (isFromSyncCommit) {
                            needsWrite = true;
                        } else {
                            synchronized (mLock) {
                                // No need to persist intermediate states. Just wait for the latest state to
                                // be persisted.
                                if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
                                    needsWrite = true;
                                }
                            }
                        }
                    }
                    // 不需要立即写入,则在MemoryCommitResult中记录该结果,然后直接返回
                    if (!needsWrite) {
                        mcr.setDiskWriteResult(false, true);
                        return;
                    }

                    boolean backupFileExists = mBackupFile.exists();

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

                    if (!backupFileExists) {
                        if (!mFile.renameTo(mBackupFile)) {
                            Log.e(TAG, "Couldn't rename file " + mFile
                                  + " to backup file " + mBackupFile);
                            mcr.setDiskWriteResult(false, false);
                            return;
                        }
                    } else {
                        mFile.delete();
                    }
                }

                // 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.
                // 当尝试写入文件时,删除备份文件,并返回true。如果在写入过程中发生了异常,则删除新的文件,下一次从备份文件中恢复。
                try {
                    FileOutputStream str = createFileOutputStream(mFile);

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

                    if (str == null) {
                        mcr.setDiskWriteResult(false, false);
                        return;
                    }
                    XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);

                    writeTime = System.currentTimeMillis();

                    FileUtils.sync(str);

                    fsyncTime = System.currentTimeMillis();

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

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

                    try {
                        final StructStat stat = Os.stat(mFile.getPath());
                        synchronized (mLock) {
                            mStatTimestamp = stat.st_mtim;
                            mStatSize = stat.st_size;
                        }
                    } catch (ErrnoException e) {
                        // Do nothing
                    }

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

                    // Writing was successful, delete the backup file if there is one.
                    mBackupFile.delete();

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

                    mDiskStateGeneration = mcr.memoryStateGeneration;
                    //记录写入成功了,唤醒等待写入结果的线程
                    mcr.setDiskWriteResult(true, true);

                    if (DEBUG) {
                        Log.d(TAG, "write: " + (existsTime - startTime) + "/"
                                + (backupExistsTime - startTime) + "/"
                                + (outputStreamCreateTime - startTime) + "/"
                                + (writeTime - startTime) + "/"
                                + (fsyncTime - startTime) + "/"
                                + (setPermTime - startTime) + "/"
                                + (fstatTime - startTime) + "/"
                                + (deleteTime - startTime));
                    }

                    long fsyncDuration = fsyncTime - writeTime;
                    mSyncTimes.add((int) fsyncDuration);
                    mNumSync++;

                    if (DEBUG || mNumSync % 1024 == 0 || fsyncDuration > MAX_FSYNC_DURATION_MILLIS) {
                        mSyncTimes.log(TAG, "Time required to fsync " + mFile + ": ");
                    }

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

                // Clean up an unsuccessfully written file
                if (mFile.exists()) {
                    if (!mFile.delete()) {
                        Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
                    }
                }
                mcr.setDiskWriteResult(false, false);
            }

在将writeToFile()方法中,首先将当前文件重命名为备份文件,然后从当前文件中获取文件输出流,并将MemoryCommitResult中保存的备份数据,写入到文件输出流中。如果写入成功,则删除备份文件,返回真。如果写入失败,则删除当前文件,下一次从备份文件中恢复过来。

通知调用者是否写入成功是通过setDiskWriteResult()方法来完成的,在该方法中,通过MemoryCommitResult的writeToDiskResult变量来保存写入结果,写入成功为真,写入失败为假。不管写入成功还是失败,都会让writtenToDiskLatch闭锁计数减1,唤醒在闭锁上等待的线程。

因此,整个commit操作可以概括为:
1.创建写入数据
2.如果当前没有写入操作,则通过线程直接写入,如果有则添加到单任务队列中等待
3.保存写入结果,并唤醒等待写入的线程
4.通知所有的监听者写入完成
5.返回给调用这写入结果

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

                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);
            }
        };
    //与commit方法不同,通过postWriteRunnable异步等待
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

    // Okay to notify the listeners before it's hit disk
    // because the listeners should always get the same
    // SharedPreferences instance back, which has the
    // changes reflected in memory.
    notifyListeners(mcr);
}

综上:commit同步写入,有返回值,但是会造成调用它的线程阻塞,apply异步写入,无返回值!

四.Sp滥用带来的性能问题

1.第一次读取时,造成主线程阻塞(文件过大,主线程等待时间过长),引起ANR
2.文件太大,加载到内存中一直存在,占用大量内存
3.直接在主线程进行commit操作,造成阻塞,引发ANR
4.多次apply,造成锁竞争,浪费系统资源
5.Sp文件过大,每次更新都更新整个文件

五.Sp最佳实践方案

1.对Sp进行合适的拆分
2.在合适的时机进行异步初始化
3.批量修改一次提交
4.在主线程谨慎使用commit

SharedPreferences作为一种轻量级的数据存储方式,极大的方便了我们的操作!但请一定谨慎的使用!

    原文作者:LBJFxd
    原文地址: https://blog.csdn.net/fanxudonggreat/article/details/80877469
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞