Android-持久化技术一之SharedPreferences

5次阅读

共计 9314 个字符,预计需要花费 24 分钟才能阅读完成。

由于在自己练手的 App 项目中使用了 SharedPreferences 技术,所以对其进行一定探究。

首先我们先总结一下,Android 的数据持久化方式:SharedPrefences、SQLite、文件储存、ContentProvider、网络储存。其余四种,留后再进一步探究。

SharedPreferences

老规矩看看类注释是怎么介绍的

由 Context.getSharedPreferences 方法放回的,可以访问和修改参数数据的接口。对于任一一组数据,都有一个所有客户机共享的该类实例。对参数数据的修改,必须通过 Editor 对象,该对象确保了参数数值的一致性和控制客户端将数值提交到储存。由各种 get 方法得到的对象必须是不可变得对象。
该类保证了强一致性,但是不支持跨进程使用。

从这段注释中,我们不难发现:SharedPreferences 需要通过 Context 创建,该类与 Editor 对象密切相关,在应用内可以数据共享。

我们在 SharedPreferences 类中往下寻找,就找到 Editor 接口

    /**
     * Interface used for modifying values in a {@link SharedPreferences}
     * object.  All changes you make in an editor are batched, and not copied
     * back to the original {@link SharedPreferences} until you call {@link #commit}
     * or {@link #apply}
     */
    public interface Editor {Editor putString(String key, @Nullable String value);
      
        Editor putLong(String key, long value);
       
        Editor putFloat(String key, float value);
       
        Editor putBoolean(String key, boolean value);

        Editor remove(String key);
        
        Editor clear();

        boolean commit();

        void apply();}

从这个 Editor 接口中,我们可以得到几个信息。首先 SharedPreferences 只能储存 4 类数据,String,Long,Float,Boolean;其次 SharedPreferences 是使用 key-value 键值对的方式进行储存的;最后,有两种提交方式 apply(),commit()。

  • 从前两个信息不难推出,SharedPreferences 只是一种轻量级的储存方式,所以最好不要使用这个去储存一些过大,或者复杂数据类型的数据。
  • apply()和 commit()的区别:

    • 从上面可以看出,commit 是有返回值的,而 apply 是没有放回值的。当我们需要知道一个数据是否修改成功时,就需要调用 commit 方法。
    • commit 是直接将修改的数据同步提交到硬件硬盘,会阻塞调用它的线程。apply 则是将修改数据原子提交到内存,而后异步真正提交到硬件硬盘,所以后面调用 apply 会直接覆盖前面的数据,使用 apply 会提高效率。
    • apply 方法不会提示任何失败的提示。由于在一个进程中,sharedPreference 是单实例,一般不会出现并发冲突,如果对提交的结果不关心的话,建议使用 apply,当然需要确保提交成功且有后续操作的话,还是需要用 commit 的。
    • 此外 SharedPreferences 有一个接口,可以实现对键值变化的监听。
  • 如果需要储存复杂数据(图片或对象)时,就需要对将其转化为 Base64 编码。

如何使用 SharedPreferences?

1、获取 SharedPreferences

ContextImpl.getSharePreferences() 该类在 AS 上不显示。根据当前应用名称获取 ArrayMap(存储 sp 容器),并根据文件名获取 SharedPreferencesImpl 对象(实现 SharedPreferences 接口)。

  • 缓存未命中, 才构造 SharedPreferences 对象,也就是说,多次调用 getSharedPreferences 方法并不会对性能造成多大影响,因为又缓存机制
  • SharedPreferences对象的创建过程是线程安全的,因为使用了synchronize` 关键字
  • 如果命中了缓存,并且参数 mode 使用了 Context.MODE_MULTI_PROCESS,那么将会调用sp.startReloadIfChangedUnexpectedly() 方法,在 startReloadIfChangedUnexpectedly 方法中,会判断是否由其他进程修改过这个文件,如果有,会重新从磁盘中读取文件加载数据
class ContextImpl extends Context {
    // 静态存储类,缓存所有应用的 SP 容器, 该容器 key 对应应用名称,value 则为每个应用存储所有 sp 的容器
    private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
    
     @Override
    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);
    }

    @Override
    public SharedPreferences getSharedPreferences(File file, int mode) {
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
        // 从 ArrayMap 中获取到应用储存的 value
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
            // 从当前的 Map 中获取一个,如果没有则直接新建一个并且放回
            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;
    }
    
}

sharedPreferences 的对象实例 sharedPreferencesImpl 类

用 SP 存储的静态变量键值数据在内存中是一直存在(文件存储), 通过 SharedPreferencesImpl 构造器开启一个线程对文件进行读取。SharedPreferencesImpl 主要是对文件进行操作。

  • 将传进来的参数 file 以及 mode 分别保存在 mFile 以及 mMode
  • 创建一个 .bak 备份文件,当用户写入失败的时候会根据这个备份文件进行恢复工作
  • 将存放键值对的 mMap 初始化为null
  • 调用 startLoadFromDisk() 方法加载数据

startLoadFromDisk()

  • 如果有备份文件,直接使用备份文件进行回滚
  • 第一次调用 getSharedPreferences 方法的时候,会从磁盘中加载数据,而数据的加载时通过开启一个子线程调用 loadFromDisk 方法进行异步读取的
  • 将解析得到的键值对数据保存在 mMap
  • 将文件的修改时间戳以及大小分别保存在 mStatTimestamp 以及 mStatSize 中(保存这两个值有什么用呢?我们在分析 getSharedPreferences 方法时说过,如果有其他进程修改了文件,并且 modeMODE_MULTI_PROCESS,将会判断重新加载文件。如何判断文件是否被其他进程修改过,没错,根据文件修改时间以及文件大小即可知道)
  • 调用 notifyAll() 方法通知唤醒其他等待线程,数据已经加载完毕
    SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        // 文件备份
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        mMap = null;
        mThrowable = null;
        
        
        // 开启一个线程读取文件
        startLoadFromDisk();}
    
    
    private void startLoadFromDisk() {synchronized (mLock) {mLoaded = false;}
        new Thread("SharedPreferencesImpl-load") {public void run() {loadFromDisk();
            }
        }.start();}

    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;
        try {stat = Os.stat(mFile.getPath());
            if (mFile.canRead()) {
             // 读取文件
                BufferedInputStream str = null;
                try {
                    str = new BufferedInputStream(new FileInputStream(mFile), 16 * 1024);
                    // 使用 XmlUtils 工具类读取 xml 文件数据
                    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 = 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;/ 如果有数据,将数据已经赋值给类成员变量 mMap(将从文件读取的数据赋值给 mMap)mStatTimestamp = stat.st_mtim;
                        mStatSize = stat.st_size;
                    } else {
                    // 没有数据直接创建一个 hashmap 对象
                        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();}
        }
    }

2、获取数据

sharedPreferencesImpl 重写了 SharedPreferences 的方法,基本上结构都一致,这里拿 String 类举例。

  • getXxx方法是线程安全的,因为使用了 synchronize 关键字
  • getXxx方法是直接操作内存的,直接从内存中的 mMap 中根据传入的 key 读取value
  • getXxx方法有可能会卡在 awaitLoadedLocked 方法,从而导致线程阻塞等待(什么时候会出现这种阻塞现象呢?前面我们分析过,第一次调用 getSharedPreferences 方法时,会创建一个线程去异步加载数据,那么假如在调用完 getSharedPreferences 方法之后立即调用 getXxx 方法,此时的 mLoaded 很有可能为 false,这就会导致awaiteLoadedLocked 方法阻塞等待,直到 loadFromDisk 方法加载完数据并且调用 notifyAll 来唤醒所有等待线程
 public String getString(String key, @Nullable String defValue) {synchronized (mLock) {
        // 此处会阻塞当前线程,直到文件加载完毕,第一次使用的时候可能会阻塞主线程
            awaitLoadedLocked();
        // 从类成员变量 mMap 中直接读取数据,没有直接返回默认值
            String v = (String)mMap.get(key);
            return v != null ? v : defValue;
        }
    }

3、提交数据

3.1 获取 editor 对象

public Editor edit() {synchronized (mLock) {awaitLoadedLocked();// 如果文件未加载完毕,会一直阻塞当前线程,直到加载完成为止
       }
       return new EditorImpl();}

3.2 对数据进行修改

  • SharedPreferences的写操作是线程安全的,因为使用了 synchronize 关键字
  • 对键值对数据的增删记录保存在 mModified 中,而并不是直接对 SharedPreferences.mMap 进行操作(mModified会在 commit/apply 方法中起到同步内存 SharedPreferences.mMap 以及磁盘数据的作用)
 public final class EditorImpl implements Editor {
    // 先存储在 Editor 的 map 中
    private final Map<String, Object> mModified = new HashMap<>();
    
   // 各种修改方法依旧类似
    public Editor putString(String key, @Nullable String value) {synchronized (mEditorLock) {mModified.put(key, value);
                return this;
            }
        }
    ...
 }

3.3 提交数据

  • commit()方法
 public boolean commit() {
            long startTime = 0;

            if (DEBUG) {startTime = System.currentTimeMillis();
            }
            // 第一步  commitToMemory 方法可以理解为对 SP 中的 mMap 对象同步到最新数据状态
            //mcr 对象就是最终需要写入磁盘的 mMap
            MemoryCommitResult mcr = commitToMemory();
            
            // 第二步 写文件;注意第二个参数为 null,写文件操作会运行在当前线程
            // 当前只有一个 commit 线程时。会直接在当前线程执行
            // 如果是 UI 线程 则可能会造成阻塞
            // 会判断有无 备份文件,一定要有备份文件,防止写入错误
            // 将 mcr 写入磁盘
            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;
        }
  • apply 和 commit 主要区别就是 apply 的写文件操作会在一个线程中执行,不会阻塞 UI 线程
        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);

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

注意点

  • SharedPreferences 是线程安全的,但是不是进程安全的。
  • SharedPreferences 不要存放特别大的数据

    • 第一次加载时,需要将整个 SP 加载到内存当中,如果过于大,会导致阻塞,甚至会导致 ANR
    • 每次 apply 或者commit,都会把全部的数据一次性写入磁盘, 所以 SP 文件不应该过大, 影响整体性能
    • SharedPreference的文件存储性能与文件大小相关,我们不要将毫无关联的配置项保存在同一个文件中,同时考虑将频繁修改的条目单独隔离出来
  • 不适宜存储 JSON 等特殊符号很多的数据
  • 所有的 getXxx 都是从内存中取的数据,数据来源于SharedPreferences.mMap
  • apply同步回写(commitToMemory())内存 SharedPreferences.mMap,然后把异步回写磁盘的任务放到一个单线程的线程池队列中等待调度。apply 不需要等待写入磁盘完成,而是马上返回
  • ommit同步回写(commitToMemory())内存 SharedPreferences.mMap,然后如果mDiskWritesInFlight(此时需要将数据写入磁盘,但还未处理或未处理完成的次数)的值等于 1,那么直接在调用commit 的线程执行回写磁盘的操作,否则把异步回写磁盘的任务放到一个单线程的线程池队列中等待调度。commit会阻塞调用线程,知道写入磁盘完成才返回
  • MODE_MULTI_PROCESS是在每次 getSharedPreferences 时检查磁盘上配置文件上次修改时间和文件大小,一旦所有修改则会重新从磁盘加载文件,所以并不能保证多进程数据的实时同步
  • 多次 edit 多次 commit/apply

    • 多次 edit 会产生很多 editor 对象
    • 多次 apply 和 commit App 的 stop 方法会等待写完为止
正文完
 0