SharedPreference是Android上一种很是易用的轻量级存储方式,因为其API及其友好,获得了不少不少开发者的青睐。可是,SharedPreference并非万能的,若是把它用在不合适的使用场景,那么将会带来灾难性的后果android
第一次看到下面这个sp的时候,个人心里是崩溃的:json
一个默认的sp有90K,当我打开它的时候,我都快哭了:除了零零星星的几个很小的key以外,存储了一个炒鸡大的key,这一个key至少占了其中的89K。知道这是什么概念吗?缓存
在小米1S这种手机上,就算获取这个sp里面一个很小的key,会花费120+ms!!那个绝不相干的key拖慢了其余全部key的读取速度!固然,在性能稍好的手机上,这个问题不是特别严重。可是要知道,120ms这个是彻底不能忍的!app
之因此说SharedPreference(下文简称sp)是一种轻量级的存储方式,是它的设计所决定的:sp在建立的时候会把整个文件所有加载进内存,若是你的sp文件比较大,那么会带来几个严重问题:ide
也许有童鞋会说,sp的加载不是在子线程么,怎么会卡住主线程?子线程IO就必定不会阻塞主线程吗?函数
下面是默认的sp实现SharedPreferenceImpl这个类的getString函数:post
public String getString(String key, @Nullable String defValue) { synchronized (this) { awaitLoadedLocked(); String v = (String)mMap.get(key); return v != null ? v : defValue; } }
继续看看这个awaitLoadedLocked:性能
private void awaitLoadedLocked() { while (!mLoaded) { try { wait(); } catch (InterruptedException unused) { } } }
一把锁就是挂在那里!!这意味着,若是你直接调用getString,主线程会等待加载sp的那么线程加载完毕!这不就把主线程卡住了么?测试
另外,有一个叫诀窍能够节省一下等待的时间:既然getString之类的操做会等待sp加载完成,而加载是在另一个线程执行的,咱们可让sp先去加载,作一堆事情,而后再getString!以下:优化
// 先让sp去另一个线程加载 SharedPreferences sp = getSharedPreferences("test", MODE_PRIVATE); // 作一堆别的事情 setContentView(testSpJson); // ... // OK,这时候估计已经加载完了吧,就算没完,咱们在本来应该等待的时间也作了一些事! String testValue = sp.getString("testKey", null);
更为严重的是,被加载进来的这些大对象,会永远存在于内存之中,不会被释放。咱们看看ContextImpl这个类,在getSharedPreference的时候会把全部的sp放到一个静态变量里面缓存起来:
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; }
注意这个static的sSharedPrefsCache,它保存了你全部使用的sp,而后sp里面有一个成员mMap保存了全部的键值对;这样,你程序中使用到的那些个sp永远就呆在内存中,是否是毛骨悚然?!
因此,请不要在sp里面存储超级大的key碰到这样的猪队友,请让他自行反省!!赶忙把自家App检查一下!!
还有一些童鞋,他在sp里面存json或者HTML;这么作不是不能够,可是,若是这个json相对较大,那么也会引发sp读取速度的急剧降低。
JSON或者HTML格式存放在sp里面的时候,须要转义,这样会带来不少 & 这种特殊符号,sp在解析碰到这个特殊符号的时候会进行特殊的处理,引起额外的字符串拼接以及函数调用开销。而JSON原本就是能够用来作配置文件的,你干吗又把它放在sp里面呢?画蛇添足。下面我写个demo验证一下。
下面这个sp是某个app的换肤配置:
咱们先用sp进行读取,而后用直接把它丢json文件,直接读取而且解析;json使用的代码以下:
public int getValueByJson(Context context, String key) { File jsonFile = new File(context.getFilesDir().getParent() + File.separator + SP_DIR_NAME, "skin_beta2.json"); FileInputStream fis = null; ByteArrayOutputStream bao = new ByteArrayOutputStream(); try { fis = new FileInputStream(jsonFile); FileChannel channel = fis.getChannel(); ByteBuffer buffer = ByteBuffer.allocate(1 << 13); // 8K int i1; while ((i1 = channel.read(buffer)) != -1) { buffer.flip(); bao.write(buffer.array(), 0, i1); buffer.clear(); } String content = bao.toString(); JSONObject jsonObject = new JSONObject(content); return jsonObject.getInt(key); } catch (IOException e) { e.printStackTrace(); } catch (JSONException e) { throw new RuntimeException("not a json file"); } finally { close(fis); close(bao); } return 0; }
而后个人测试结果是:直接解析JSON比在xml里面要快一倍!在小米1S上结果以下:
时间jsonspMi 1S8038Nexus5X6.53.5这个JSON的读取尚未作任何的优化,提高潜力巨大!所以,若是你须要用JSON作配置,请不要把它存放在sp里面!!
我见过这样的使用代码:
SharedPreferences sp = getSharedPreferences("test", MODE_PRIVATE); sp.edit().putString("test1", "sss").apply(); sp.edit().putString("test2", "sss").apply(); sp.edit().putString("test3", "sss").apply(); sp.edit().putString("test4", "sss").apply();
每次edit都会建立一个Editor对象,额外占用内存;固然多建立几个对象也影响不了多少;可是,屡次apply也会卡界面你造吗?
有童鞋会说,apply不是在别的线程些磁盘的吗,怎么可能卡界面?我带你仔细看一下源码。
public void apply() { final MemoryCommitResult mcr = commitToMemory(); final Runnable awaitCommit = new Runnable() { public void run() { try { mcr.writtenToDiskLatch.await(); } catch (InterruptedException ignored) { } } }; QueuedWork.add(awaitCommit); Runnable postWriteRunnable = new Runnable() { public void run() { awaitCommit.run(); QueuedWork.remove(awaitCommit); } }; SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable); notifyListeners(mcr); }
注意两点,第一,把一个带有await的runnable添加进了QueueWork类的一个队列;第二,把这个写入任务经过enqueueDiskWrite丢给了一个只有单个线程的线程池执行。
到这里一切都OK,在子线程里面写入不会卡UI。可是,你去ActivityThread类的handleStopActivity里看一看:
private void handleStopActivity(IBinder token, boolean show, int configChanges, int seq) { // 省略无关。。 // Make sure any pending writes are now committed. if (!r.isPreHoneycomb()) { QueuedWork.waitToFinish(); } // 省略无关。。 }
waitToFinish?? 又要等?源码以下:
public static void waitToFinish() { Runnable toFinish; while ((toFinish = sPendingWorkFinishers.poll()) != null) { toFinish.run(); } }
还记得这个toFinish的Runnable是啥吗?就是上面那个awaitCommit它里面就一句话,等待写入线程!!若是在Activity Stop的时候,已经写入完毕了,那么万事大吉,不会有任何等待,这个函数会立马返回。可是,若是你使用了太屡次的apply,那么意味着写入队列会有不少写入任务,而那里就只有一个线程在写。当App规模很大的时候,这种状况简直就太常见了!
所以,虽然apply是在子线程执行的,可是请不要无节制地apply;commit我就很少说了吧?直接在当前线程写入,若是你在主线程干这个,当心挨揍。
还有童鞋发现sp有一个貌似能够提供「跨进程」功能的FLAG——MODE_MULTI_PROCESS,咱们看看这个FLAG的文档:
@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}.
文档也说了,这玩意在某些Android版本上不可靠,而且将来也不会提供任何支持,要是用跨进程数据传输须要使用相似ContentProvider的东西。并且,SharedPreference的文档也特别说明:
Note: This class does not support use across multiple processes.
那么咱们姑且看一看,设置了这个Flag到底干了啥;在SharedPreferenceImpl里面,没有发现任何对这个Flag的使用;而后咱们去ContextImpl类里面找找getSharedPreference的时候作了什么:
@Override public SharedPreferences getSharedPreferences(File file, int mode) { checkMode(mode); SharedPreferencesImpl sp; synchronized (ContextImpl.class) { final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked(); sp = cache.get(file); if (sp == null) { 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; }
这个flag保证了啥?保证了在API 11之前的系统上,若是sp已经被读取进内存,再次获取这个sp的时候,若是有这个flag,会从新读一遍文件,仅此而已!因此,若是仰仗这个Flag作跨进程存取,简直就是丢人现眼。
总价一下,sp是一种轻量级的存储方式,使用方便,可是也有它适用的场景。要优雅滴使用sp,要注意如下几点: