当前位置:   article > 正文

Android sharepreference槽点及改进方案_sharedpreferences in credential encrypted storage

sharedpreferences in credential encrypted storage are not available until af

1 概述简介

1.1 简介

众所周知,SharedPreferences是一种轻型的Android数据存储方式,它的本质是基于XML文件存储key-value键值对数据,通常用来存储一些简单的配置信息。它的存储位置是在/data/data/<包名>/shared_prefs目录下。SharedPreferences对象本身只能获取数据而不支持存储和修改,存储修改是通过Editor对象实现。比较经典的使用方式例如用户输入框对过往登录账户的存储。

1.2 使用方式

实现SharedPreferences存储的步骤如下:

1、根据Context获取SharedPreferences对象

2、利用edit()方法获取Editor对象。

3、通过Editor对象存储key-value键值对数据。

4、通过commit()或apply()方法提交数据。

1.3 commit和apply方法的区别

1、apply没有返回值而commit返回boolean表明修改是否提交成功

2、apply是将修改数据原子提交到内存,而后异步真正提交到硬件磁盘;而commit是同步的提交到硬件磁盘,因此,在多个并发的提交commit的时候,他们会等待正在处理的commit保存到磁盘后在操作,从而降低了效率。而apply只是原子的提交到内存,后面有调用apply的函数的将会直接覆盖前面的内存数据,这样从一定程度上提高了很多效率。

3、apply方法不会提示任何失败的提示。

一般的话,建议使用apply,当然,如果是需要确保数据提交成功,且有后续操作的话,则需要用commit方法。

1.4 优缺点及使用建议

优点:

  • 轻量级,以键值对的方式进行存储,使用方便,易于理解
  • 采用的是xml文件形式存储在本地,程序卸载后会也会一并被清除,不会残留信息

缺点:

  • 由于是对文件IO读取,因此在IO上的瓶颈是个大问题,因为在每次进行get和commit时都要将数据从内存写入到文件中,或从文件中读取
  • 多线程场景下效率较低,在get操作时,会锁定SharedPreferences对象,互斥其他操作,而当put,commit时,则会锁定Editor对象,使用写入锁进行互斥,在这种情况下,效率会降低
  • 不支持跨进程通讯
  • 由于每次都会把整个文件加载到内存中,因此,如果SharedPreferences文件过大,或者在其中的键值对是大对象的json数据则会占用大量内存,读取较慢是一方面,同时也会引发程序频繁GC,导致的界面卡顿。

建议:

  • 不要存储较大数据或者较多数据到SharedPreferences中:SharedPreferences支持6种数据类型,String set int float long boolean,如果需要存取比较复杂的数据类型比如类或者图像,则需要对这些数据进行编码,通常将其转换成Base64编码,然后将转换后的数据以字符串的形式保存在XML文件中(强烈不建议这么干)。
  • 频繁修改的数据修改后统一提交,而不是修改过后马上提交,示例如下:
  •  

    /**

    * 错误示例

    * */

    private void errorExample() {

    SharedPreferences sharedPreferences = getSharedPreferences("MyID", MODE_PRIVATE);

    sharedPreferences.edit().putInt("intId", 1).apply();

    sharedPreferences.edit().putString("stringId", "stringId").apply();

    Set<String> stringSet = new HashSet<>();

    stringSet.add("stringSetTest");

    sharedPreferences.edit().putStringSet("stringSetId", stringSet).apply();

    sharedPreferences.edit().putBoolean("booleanId", true).apply();

    sharedPreferences.edit().putLong("longId", 1).apply();

    sharedPreferences.edit().putFloat("floatId", 1).apply();

    }

    /**

    * 正确示例

    * */

    private void rightExample() {

    SharedPreferences sharedPreferences = getSharedPreferences("MyID", MODE_PRIVATE);

    SharedPreferences.Editor editor = sharedPreferences.edit();

    editor.putInt("intId", 1);

    editor.putString("stringId", "stringId");

    Set<String> stringSet = new HashSet<>();

    stringSet.add("stringSetTest");

    editor.putStringSet("stringSetId", stringSet);

    editor.putBoolean("booleanId", true);

    editor.putLong("longId", 1);

    editor.putFloat("floatId", 1);

    editor.commit();

    }

    在跨进程通讯中不去使用SharedPreferences
  • 键值对不宜过多

1.5 替代方案

1.5.1 MMKV

诞生背景

它的最早的诞生,主要是因为在微信iOS端有一个重大的bug,一个特殊的文本可以导致微信的iOS端闪退,而且还出现了不止一次。为了统计这种闪退的字符出现频率以及过滤,但是由于出现的次数,发现原来的键值对存储组件NSUserDefaults根本达不到要求,会导致cell的滑动卡顿。因此iOS端就开始创造一个高新性能的键值对存储组件。Android端因为SharePreference的跨进程读写问题及性能问题,也开始复用iOS的MMKV,并进行了改进。

MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。从 2015 年中至今在微信上使用,其性能和稳定性经过了时间的验证。

性能对比

官方性能比较图:

1、Android端1000次的读写性能比较:

iOS端10000次的读写性能比较:

能看到mmkv比起我们开发常用的组件要快上数百倍。

1.5.2 Tray

GitHub地址:https://github.com/grandcentrix/tray

一个外国Android开发开源的一款Android SharePreference替代者,最早的版本是14年9月17号,最后一个版本是17年2月7号,GitHub2.3kstar,目前已经不再维护,网上资料也相对较少,不做进一步研究。

1.5.3 PreferencesProvider

GitHub地址:https://github.com/mengdd/PreferencesProvider

Tray的使用者,APP被一个偶现的未解决的issue:https://github.com/grandcentrix/tray/issues/50影响,作者迟迟不发版解决,自己团队根据Tray进一步优化而诞生,只有一个版本,发布于四年前,GitHub46star,基本也是处于不维护的状态,网上资料也相对很少,不做进一步研究。

2 SharedPreferences源码分析

2.1 简单说明

SharedPreferences是Android提供的数据持久化的一种手段,适合单进程、小批量的数据存储与访问。为什么这么说呢?因为SharedPreferences的实现是基于单个xml文件实现的,并且所有持久化数据都是一次性加载到内存,如果数据过大,是不合适采用SharedPreferences存放的。而适用的场景是单进程的原因同样如此,由于Android原生的文件访问并不支持多进程互斥,所以SharePreferences也不支持,如果多个进程更新同一个xml文件,就可能存在不互斥问题。

2.2 getSharedPreferences

首先,从基本使用简单看下SharedPreferences的实现原理:

 

mSharedPreferences = context.getSharedPreferences("test", Context.MODE_PRIVATE);

SharedPreferences.Editor editor = mSharedPreferences.edit();

editor.putString(key, value);

editor.apply();

context.getSharedPreferences其实就是简单的调用ContextImpl的getSharedPreferences,具体实现如下:

 

@Override

public SharedPreferences getSharedPreferences(String name, int mode) {

// At least one application in the world actually passes in a null

// name. This happened to work because when we generated the file name

// we would stringify it to "null.xml". Nice.

if (mPackageInfo.getApplicationInfo().targetSdkVersion <

Build.VERSION_CODES.KITKAT) {

if (name == null) {

name = "null";

}

}

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

}

ContextImpl有一个成员变量mSharedPrefsPaths,保存sp的名字与对应的文件的映射,这个很好理解,当我们通过context拿sp的实例的时候,肯定先要找到sp对应文件,然后再对该文件进行读写操作。

值得注意的是这里对于mSharedPrefsPaths的操作时加锁了,锁的对象是ContextImpl.class,所以不论是从哪个Context的子类来获取sp,都能保证mSharedPrefsPaths的线程安全。

 

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

}

上述代码有几个重要的点,下面单独分析一下:

1、getSharedPreferencesCacheLocked方法:

 

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;

}

这里主要涉及两个映射关系,一个是应用包名与sp之间的映射,因为一个应用可能创建多个sp文件来存储不同的业务配置项。第二个是sp文件与sp实现类SharedPreferencesImpl之间的映射关系。

值得注意的是它们使用的都是ArrayMap而不是HashMap,估计主要是因为ArrayMap比HashMap更省内存,后续看看哪位大佬比较擅长这个,给大家普及分享一下各种array及map的原理源码及使用场景。

2、通过file拿到对应的sp的实现类实例

3、检查操作模式,看一下实现:

 

private void checkMode(int mode) {

if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.N) {

if ((mode & MODE_WORLD_READABLE) != 0) {

throw new SecurityException("MODE_WORLD_READABLE no longer supported");

}

if ((mode & MODE_WORLD_WRITEABLE) != 0) {

throw new SecurityException("MODE_WORLD_WRITEABLE no longer supported");

}

}

}

Android N及以上版本跨进程的读写模式直接抛出安全异常,可见Google粑粑对应用安全方面的限制越来越严格了。

4、创建sp的实现类的实例,并加入到缓存中,以便下次能够快速的拿到。

5、当操作模式设置为Context.MODE_MULTI_PROCESS或者目标sdk版本小于3.2时,调用sp.startReloadIfChangedUnexpectedly()

 

void startReloadIfChangedUnexpectedly() {

synchronized (mLock) {

// TODO: wait for any pending writes to disk?

if (!hasFileChangedUnexpectedly()) {

return;

}

startLoadFromDisk();

}

}

该方法先去检查文件状态是否改变,如果有的话就重新读取文件数据到内存。这里我们知道MODE_MULTI_PROCESS是不靠谱的,它并不能支持数据跨进程共享,只是getSharePreference时会去检查文件状态是否改变,改变就重新加载数据到内存。

2.3 SharedPreferencesImpl

上面了解到getSharedPreferences返回的是SharedPreferencesImpl的实例,现在重点看一下SharedPreferencesImpl的实现:

2.3.1 构造函数

 

SharedPreferencesImpl(File file, int mode) {

mFile = file;

mBackupFile = makeBackupFile(file);

mMode = mode;

mLoaded = false;

mMap = null;

mThrowable = null;

startLoadFromDisk();

}

都是一些常规操作,初始化一些值,创建备份文件,重点看一下startLoadFromDisk

2.3.2 startLoadFromDisk

 

private void startLoadFromDisk() {

synchronized (mLock) {

mLoaded = false;

}

new Thread("SharedPreferencesImpl-load") {

public void run() {

loadFromDisk();

}

}.start();

}

将变量mLoaded置为false,表示数据还没有加载成功,然后开启一个线程调用loadFromDisk

2.3.3 loadFromDisk

 

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

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;

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、然后创建文件输出流读取文件内存并转化为Map,注意这里创建带缓存的输出流时,指定的buffer大小为16k。

3、将读取到的Map赋值给mMap成员变量,如果map为空就创建一个空的HashMap,这里又是用到HashMap了,因为这里设计频繁查找或插入操作,而hashMap的查询和插入操作的效率是优于ArrayMap的

4、通知唤醒线程,有唤醒就有阻塞,看一下哪里阻塞了,全局搜索一下mLock.wait

 

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 {

mLock.wait();

} catch (InterruptedException unused) {

}

}

if (mThrowable != null) {

throw new IllegalStateException(mThrowable);

}

}

该方法在mLoaded为false的时候一直阻塞,而之前的notifyAll唤醒的就是此处的阻塞。再看一下awaitLoadedLocked在哪里被调用了。

 

@Override

public Map<String, ?> getAll() {

synchronized (mLock) {

awaitLoadedLocked();

//noinspection unchecked

return new HashMap<String, Object>(mMap);

}

}

@Override

@Nullable

public String getString(String key, @Nullable String defValue) {

synchronized (mLock) {

awaitLoadedLocked();

String v = (String)mMap.get(key);

return v != null ? v : defValue;

}

}

@Override

@Nullable

public Set<String> getStringSet(String key, @Nullable Set<String> defValues) {

synchronized (mLock) {

awaitLoadedLocked();

Set<String> v = (Set<String>) mMap.get(key);

return v != null ? v : defValues;

}

}

@Override

public int getInt(String key, int defValue) {

synchronized (mLock) {

awaitLoadedLocked();

Integer v = (Integer)mMap.get(key);

return v != null ? v : defValue;

}

}

@Override

public long getLong(String key, long defValue) {

synchronized (mLock) {

awaitLoadedLocked();

Long v = (Long)mMap.get(key);

return v != null ? v : defValue;

}

}

@Override

public float getFloat(String key, float defValue) {

synchronized (mLock) {

awaitLoadedLocked();

Float v = (Float)mMap.get(key);

return v != null ? v : defValue;

}

}

@Override

public boolean getBoolean(String key, boolean defValue) {

synchronized (mLock) {

awaitLoadedLocked();

Boolean v = (Boolean)mMap.get(key);

return v != null ? v : defValue;

}

}

@Override

public boolean contains(String key) {

synchronized (mLock) {

awaitLoadedLocked();

return mMap.containsKey(key);

}

}

所有的get相关方法都被阻塞,直到完成数据从文件加载到内存的过程。因此当第一次调用sp的get相关

函数时是比较慢的,需要等待数据从文件被读取到内存,之后会比较快,因为是直接在内存中读取。

接下来看看put相关方法

 

@Override

public Editor edit() {

// TODO: remove the need to call awaitLoadedLocked() when

// requesting an editor. will require some work on the

// Editor, but then we should be able to do:

//

// context.getSharedPreferences(..).edit().putString(..).apply()

//

// ... all without blocking.

synchronized (mLock) {

awaitLoadedLocked();

}

return new EditorImpl();

}

调用put相关方法之前需要调用edit方法,此处也是需要等待的,返回的是EditorImpl的实例。

2.4 EditorImpl

 

@GuardedBy("mEditorLock")

private final Map<String, Object> mModified = new HashMap<>();

EditorImpl是SharedPreferencesImpl的内部类,内部有一个HashMap保存被更改的键值对。

2.4.1 put & remove

 

@Override

public Editor putBoolean(String key, boolean value) {

synchronized (mEditorLock) {

mModified.put(key, value);

return this;

}

}

@Override

public Editor remove(String key) {

synchronized (mEditorLock) {

mModified.put(key, this);

return this;

}

}

从以上两个方法可以知道,put方法就是向mModified添加一个键值对,remove方法添加的value为当前editor实例。它们都是被mEditorLock加锁保护的,有两个原因:

  • HashMap不是线程安全的
  • 需要和其他的get方法互斥

2.4.2 commit

 

@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、commitToMemory实现

 

// Returns true if any changes were made

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

}

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;

}

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

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

}

就是把更改的键值对提交到内存中,即把mModified中的键值对更新到mapToWriteToDisk中,顺便获取被更新的键的集合以及外部设置监听器列表

2、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);

}

主要工作就是把最新的数据写入到文件

2.4.3 apply

 

@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);

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

}

关键代码enqueueDiskWrite上面已经分析过,核心代码如下:

 

final boolean isFromSyncCommit = (postWriteRunnable == null);

QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);

此处可知isFromSyncCommit为false,queue参数取非为true,

 

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

}

}

}

首先获取一个Handler的实例,然后再通过Handler发送一个消息,先看一下getHandler

 

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;

}

}

这是一个典型的单例模式写法,Handler构造方法的Looper来自HandlerThread,这是一个内部维护消息机制的线程,任务是按照时间顺序依次执行的。

接下来看一下QueuedWorkHandler里面的handleMessage的方法实现:

 

public void handleMessage(Message msg) {

if (msg.what == MSG_RUN) {

processPendingWork();

}

}

 

private static void processPendingWork() {

long startTime = 0;

if (DEBUG) {

startTime = System.currentTimeMillis();

}

synchronized (sProcessingWork) {

LinkedList<Runnable> work;

synchronized (sLock) {

work = (LinkedList<Runnable>) 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();

}

if (DEBUG) {

Log.d(LOG_TAG, "processing " + work.size() + " items took " +

+(System.currentTimeMillis() - startTime) + " ms");

}

}

}

}

其实到这里apply方法也基本上分析完毕,该方法是在子线程被调用的,为了线程安全考虑,使用的是HandlerThread来依次执行写文件任务。当我们需要依次提交更改多个键值对时,只需要保留最后一个commit或apply方法既可。

2.5 简单总结

  • SharedPreferences不适合存储过大的数据,因为它一直保存在内存中,数据过大容易造成内存溢出。
  • SharedPreferences并不支持跨进程,因为它不能保证更新本地数据后被另一个进程所知道,而且跨进程的操作标记已经被弃用。
  • SharedPreferences的commit方法是直接在当前线程执行文件写入操作,而apply方法是在工作线程执行文件写入,尽可能使用apply,因为不会阻塞当前线程。
  • SharedPreferences批量更改数据时,只需要保留最后一个apply即可,避免添加多余的写文件任务。
  • 每个SharedPreferences存储的键值对不宜过多,否则在加载文件数据到内存时会耗时过长,而阻塞SharedPreferences的相关get或put方法,造成ui卡顿。
  • 频繁更改的配置项和不常更改的配置项应该分开为不同的SharedPreferences存放,避免不必要的io操作。

注:1、SharedPreferences有大约万分之一的损坏率(网络数据)

2、一个 100KB 的 SharedPreferences 文件读取等待时间大约需要 50~100ms(网络数据),建议提前用异步线程预加载启动过程中用到的 SharedPreferences 文件。

3 MMKV简单使用

3.1 前置知识

mmkv其实和SharePrefences一样,有增删查改四种操作。MMKV作为一个键值对存储组件,也对了存储对象的序列化方式进行了优化。常用的方式比如有json,Twitter的Serial。而MMKV使用的是Google开源的序列化方案:Protocol Buffers。

Protocol Buffers & json对比:

  • 从体积上,使用了二进制的压缩,比起json小上不少。
  • 兼容性上,Protocol Buffers有自己的语法,可以跨语言跨平台。
  • 使用成本上,比起json就要高上不少。需要定义.proto 文件,并用工具生成对应的辅助类。辅助类特有一些序列化的辅助方法,所有要序列化的对象,都需要先转化为辅助类的对象,这让序列化代码跟业务代码大量耦合,是侵入性较强的一种方式。

下面进行比较几个对象序列化之间的要素比较:

要素

Serial

JSON

Protocol Buffers

正确性

时间开销

良(Json>性能>Serializable)

良(性能<Protocol Buffers)

空间开销

良(对象序列化,空间较大)

良 (数据序列化,保留可读性牺牲空间)

优(二进制压缩)

开发成本

良(比起Serializable麻烦需要额外接入)

良(对引用,继承支持有限)

差(不支持对象之间引用和继承)

兼容性

良(和平台相关)

优(跨平台跨语言支持)

优(跨平台跨语言支持)

MMKV就是看重了Protocol Buffers的时间开销小,选择Protocol Buffers进行对象缓存的核心。

3.2 MMKV的使用

3.2.1 依赖注入

在 App 模块的 build.gradle 文件里添加:

 

dependencies {

implementation 'com.tencent:mmkv:1.0.22'

// replace "1.0.22" with any available version

}

3.2.2 初始化

MMKV 的使用非常简单,所有变更立马生效,无需调用 sync 、apply 。 在 App 启动时初始化 MMKV,设定 MMKV 的根目录(files/mmkv/),也可以直接使用默认目录:

 

MMKV.initialize(this);

public static String initialize(Context context) {

//默认根目录

String root = context.getFilesDir().getAbsolutePath() + "/mmkv";

return initialize(root, (MMKV.LibLoader)null);

}

String dir = getFilesDir().getAbsolutePath() + "/mmkv_2";

MMKV.initialize(dir);

3.2.3 获取实例

 

// 获取默认的全局实例

MMKV mmkv = MMKV.defaultMMKV();

// 根据业务区别存储, 附带一个自己的 ID

MMKV mmkv = MMKV.mmkvWithID("MyID");

// 多进程同步支持

MMKV mmkv = MMKV.mmkvWithID("MyID", MMKV.MULTI_PROCESS_MODE);

3.2.4 CURD

 

// 添加/更新数据

mmkv.encode(key, value);

// 获取数据

int tmp = mmkv.decodeInt(key);

// 删除数据

mmkv.removeValueForKey(key);

3.2.5 SharedPreferences迁移到MMKV

 

MMKV mmkv = MMKV.mmkvWithID("MyID");

SharedPreferences sharedPreferences = getSharedPreferences("MyID", MODE_PRIVATE);

// 迁移旧数据

mmkv.importFromSharedPreferences(sharedPreferences);

// 清空旧数据

sharedPreferences.edit().clear().commit();

3.3 美菜商城实际应用

美菜商城跟美菜大客户目前使用的自己开发的storage SDK,内部使用的存储组件就是MMKV,下面我们来简单看一下使用情况。

3.3.1 注解SharedPreferences的table名称跟key名称

 

/**

* Desc: 用来标注接口内的方法,定义sp存储字段的key

*/

@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.METHOD)

public @interface SpKey {

String spKey();

}

 

/**

* Desc: 用来标注sharedPreference接口, tableName 为表名

*/

@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.TYPE)

public @interface SpTable {

String tableName() default "";

}

3.3.2 SharedPreferences接口的返回操作

 

public interface Option <SpType>{

SpType get(SpType defValue);

SpType get();

void set(SpType value);

}

3.3.3 获取 SpTable interface 的实例

 

public static <T> T provideInstance(Context context, Class<T> spInterface) {

if (!spInterface.isInterface()) {

throw new IllegalStateException("spInterface must be an interface");

}

String spTableName = getTableNameFromInterface(spInterface);

final SpUtil spUtil = new SpUtil(context, spTableName);

@SuppressWarnings("unchecked")

T instance = (T) Proxy.newProxyInstance(spInterface.getClassLoader(), new Class<?>[]{spInterface}, new InvocationHandler() {

@Override

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

if (method.getReturnType() != Option.class) {

throw new IllegalStateException("spInterface method return type must be Option.class but now it is " + method.getReturnType().getName());

}

final String spKey;

SpKey keyAnn = method.getAnnotation(SpKey.class);

if (keyAnn != null && !keyAnn.spKey().isEmpty()) {

spKey = keyAnn.spKey();

} else {

spKey = method.getName();

}

ParameterizedType optionType = ((ParameterizedType) method.getGenericReturnType());

Type optionGenericType = optionType.getActualTypeArguments()[0];

if (!(optionGenericType instanceof Class)) {

if (optionGenericType.toString().equals("java.util.Set<java.lang.String>")) { //Set<String>类型

return new Option<Set<String>>() {

@Override

public Set<String> get(Set<String> defValue) {

return spUtil.getStringSet(spKey, defValue);

}

@Override

public Set<String> get() {

return spUtil.getStringSet(spKey, Collections.<String>emptySet());

}

@Override

public void set(Set<String> value) {

spUtil.putStringSet(spKey, value);

}

};

}

throw new IllegalStateException("the saving type " + optionGenericType + " is not Shared preferences supported");

}

Class optionGenericClass = (Class)optionGenericType;

if (optionGenericClass == String.class) {

return new Option<String>() {

@Override

public String get(String defValue) {

return spUtil.getString(spKey, defValue);

}

@Override

public String get() {

return spUtil.getString(spKey, "");

}

@Override

public void set(String value) {

spUtil.putString(spKey, value);

}

};

} else if (optionGenericClass == Integer.class) {

return new Option<Integer>() {

@Override

public Integer get(Integer defValue) {

return spUtil.getInt(spKey, defValue);

}

@Override

public Integer get() {

return spUtil.getInt(spKey, 0);

}

@Override

public void set(Integer value) {

spUtil.putInt(spKey, value);

}

};

} else if (optionGenericClass == Long.class) {

return new Option<Long>() {

@Override

public Long get(Long defValue) {

return spUtil.getLong(spKey, defValue);

}

@Override

public Long get() {

return spUtil.getLong(spKey, 0);

}

@Override

public void set(Long value) {

spUtil.putLong(spKey, value);

}

};

} else if (optionGenericClass == Float.class) {

return new Option<Float>() {

@Override

public Float get(Float defValue) {

return spUtil.getFloat(spKey, defValue);

}

@Override

public Float get() {

return spUtil.getFloat(spKey, 0);

}

@Override

public void set(Float value) {

spUtil.putFloat(spKey, value);

}

};

} else if (optionGenericClass == Boolean.class) {

return new Option<Boolean>() {

@Override

public Boolean get(Boolean defValue) {

return spUtil.getBoolean(spKey, defValue);

}

@Override

public Boolean get() {

return spUtil.getBoolean(spKey, false);

}

@Override

public void set(Boolean value) {

spUtil.putBoolean(spKey, value);

}

};

} else {

throw new IllegalStateException("the saving type " + optionGenericClass + " is not Shared preferences supported");

}

}

});

return instance;

}

3.3.4 获取 SpTable interface的table名称

 

private static <T> String getTableNameFromInterface(Class<T> spInterface) {

SpTable tableAnn = spInterface.getAnnotation(SpTable.class);

if (tableAnn == null) {

throw new IllegalStateException("spInterface must have a SpTable annotation");

}

String spTableName = tableAnn.tableName();

if (spTableName.isEmpty()) {

spTableName = spInterface.getSimpleName();

}

return spTableName;

}

3.3.5 清空SharedPreferences&清除某一个key

 

public static void clearSpTable(Context context, String tableName) {

new SpUtil(context, tableName).clear();

}

public static <T> void clearSpTable(Context context, Class<T> spInterface) {

clearSpTable(context, getTableNameFromInterface(spInterface));

}

public static void remove(Context context, String tableName, String key) {

new SpUtil(context, tableName).remove(key);

}

public static <T> void remove(Context context, Class<T> spInterface, String key) {

remove(context, getTableNameFromInterface(spInterface), key);

}

3.3.6 保存跟清除的核心代码

 

public class SpUtil {

private SharedPreferences mSp;

private MMKV mmkv;

public SpUtil(Context context) {

this(context, context.getPackageName() + "_sp");

}

public SpUtil(Context context, String spTableName) {

Log.d("MyTag","--SpUtil spTableName : "+spTableName);

mSp = context.getSharedPreferences(spTableName, Context.MODE_PRIVATE);

mmkv = MMKV.mmkvWithID(spTableName);

mmkv.importFromSharedPreferences(mSp);

mSp.edit().clear().apply();

}

public void putString(String key, String value) {

// mmkv.edit().putString(key, value).apply();

mmkv.encode(key,value);

}

public String getString(String key, String defValue) {

// return mmkv.getString(key, defValue);

return mmkv.decodeString(key,defValue);

}

public void putInt(String key, int value) {

mSp.edit().putInt(key, value).apply();

mmkv.encode(key,value);

}

public int getInt(String key, int defValue) {

// return mmkv.getInt(key, defValue);

return mmkv.decodeInt(key,defValue);

}

public void putFloat(String key, float value) {

// mmkv.edit().putFloat(key, value).apply();

mmkv.encode(key,value);

}

public float getFloat(String key, float defValue) {

// return mmkv.getFloat(key, defValue);

return mmkv.decodeFloat(key,defValue);

}

public void putLong(String key, long value) {

// mmkv.edit().putLong(key, value).apply();

mmkv.encode(key,value);

}

public long getLong(String key, long defValue) {

// return mmkv.getLong(key, defValue);

return mmkv.decodeLong(key,defValue);

}

public void putBoolean(String key, boolean value) {

// mmkv.edit().putBoolean(key, value).apply();

mmkv.encode(key,value);

}

public boolean getBoolean(String key, boolean defValue) {

// return mmkv.getBoolean(key, defValue);

return mmkv.decodeBool(key,defValue);

}

public void putStringSet(String key, Set<String> value) {

// mmkv.edit().putStringSet(key, value).apply();

mmkv.encode(key,value);

}

public Set<String> getStringSet(String key, Set<String> defValue) {

// return mmkv.getStringSet(key, defValue);

return mmkv.decodeStringSet(key,defValue);

}

public void clear() {

// mmkv.edit().clear().apply();

mmkv.clearAll();

}

public void remove(String key) {

// mmkv.edit().remove(key).apply();

mmkv.remove(key).apply();

}

}

3.3.7 商城代码的实际使用

  • 添加依赖
  •  

    "mc_storage" : 'com.meicai.android.sdk:storage:0.0.5'

    api rootProject.ext.dependencies["mc_storage"]

    初始化
  •  

    MMKV.initialize(this);

    创建具体的SpTable interface
  •  

    @Keep

    @SpTable(tableName = "PurchaseSearchHistory")

    public interface PurchaseSearchHistorySp {

    Option<String> purchaseSearchHistory();

    }

    获取SpTable interface的实例
  •  

    private PurchaseSearchHistorySp mPurchaseSearchHistorySp = SpManager.provideInstance(MainApp.getInstance(), PurchaseSearchHistorySp.class);

    读&写
 

mPurchaseSearchHistorySp.purchaseSearchHistory().get("[]")

mPurchaseSearchHistorySp.purchaseSearchHistory().set(history);

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/数据挖掘灵魂/article/detail/60749
推荐阅读
相关标签
  

闽ICP备14008679号