腾讯一面:请你分别谈谈SharedPreferences 和MMKV 2022-12-15 14:13 81阅读 0赞 **SharedPreferences** 作为轻量级存储在 **Android** 应用中是必不可少的,但依旧存在较大的优化空间,小菜在做性能优化时尝试了新的利器 **腾讯 MMKV**,小菜今天按如下脑图顺序尝试学习和简单分析一下; > ![8f4c034a776beb5f1e3be96c670920a1.png][] ### SharedPreferences ### #### 1. SharedPreferences 基本介绍 #### **SharedPreferences** 是一种轻量级存储方式,以 **key-value** 方式存储在本地 **xml** 文件中;其持久化的本质就是在在本地磁盘记录一个 **xml** 文件; public interface SharedPreferences { public interface OnSharedPreferenceChangeListener { void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key); } void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener); void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener); Editor edit(); public interface Editor { Editor putString(String key, @Nullable String value); Editor putStringSet(String key, @Nullable Set<String> values); ... Editor remove(String key); Editor clear(); boolean commit(); void apply(); } } 简单分析源码可得,**SharedPreferences** 只是一个接口,**SharedPreferencesImpl** 为具体的实现类,通过 **ContextImpl** 中 **getSharedPreferences()** 获取对象; #### 2. SharedPreferences 初始化 #### SharedPreferences sp = getSharedPreferences(Constants.SP_APP_CONFIG, MODE_PRIVATE); **SharedPreferences** 的通过 **getSharedPreferences()** 初始化创建一个对象;其中 **MODE** 为文件操作类型;**MODE\_PRIVATE** 为本应用私有的,其他 **app** 不可访问的;**MODE\_APPEND** 也为应用私有,但是新保存的数据放置在文件最后,不会替换之前已有的 **key-value**;**MODE\_WORLD\_READABLE/WRITEABLE** 为其他文件是否可以支持读写操作;常用的还是 **MODE\_PRIVATE** 方式; ![8c41bef0b70acd880d917b4ec5acf777.png][] @Override public SharedPreferences getSharedPreferences(String name, int mode) { if (mPackageInfo.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.KITKAT) { if (name == null) { name = "null"; } } File file; synchronized (ContextImpl.class) { if (mSharedPrefsPaths == null) { // TAG 01 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) { // TAG 02 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"); } } // TAG 03 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) { sp.startReloadIfChangedUnexpectedly(); } return sp; } 小菜在源码处注明了几个 **TAG** 需要注意的地方; **TAG 01:** 在根据 **name** 查询文件时,**SharedPreferences** 使用了 **ArrayMap**,相较于 **HashMap** 更便捷,更节省空间; ![25c4a30f444a6f1533a01856cd13ab67.png][] **TAG 02:** 在创建生成 **SharedPreferences** 时,通过 **cache** 来防止同一个 **SharedPreferences** 被重复创建; **TAG 03:** **SharedPreferencesImapl** 为具体的实现类,初始化时开启新的 **I/O** 线程读取整个文件 **startLoadFromDisk()**,进行 **xml** 解析,存入内存 **Map** 集合中; 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(); } #### 3. SharedPreferences 编辑提交 #### // 编辑数据 Editor editor = sp.edit(); editor.putString("name", "阿策小和尚"); // 提交数据 editor.apply(); // 获取数据 Editor editor = sp.edit(); editor.getString("name", ""); **Editor** 是用于编辑 **SharedPreferences** 内容的接口,**EditorImpl** 为具体的实现类;**putXXX()** 编辑后的数据保存在 **Editor** 中,**commit()/apply()** 后才会更新到 **SharedPreferences**; @Nullable public String getString(String key, @Nullable String defValue) { synchronized (mLock) { awaitLoadedLocked(); String v = (String)mMap.get(key); return v != null ? v : defValue; } } @GuardedBy("mLock") private void awaitLoadedLocked() { if (!mLoaded) { BlockGuard.getThreadPolicy().onReadFromDisk(); } while (!mLoaded) { try { mLock.wait(); } catch (InterruptedException unused) { } } if (mThrowable != null) { throw new IllegalStateException(mThrowable); } } **getXXX()** 获取数据时根据 **mLoaded** 文件是否读取完成判断,若未读取完成 **awaitLoadedLocked()** 会被阻塞,此时在 **UI** 主线程中进行使用时就可有可能会造成 **ANR**; @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) { } } }; QueuedWork.addFinisher(awaitCommit); Runnable postWriteRunnable = new Runnable() { @Override public void run() { awaitCommit.run(); QueuedWork.removeFinisher(awaitCommit); } }; SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable); notifyListeners(mcr); } **Editor** 通过 **commit()** 和 **apply()** 提交更新到 **SharedPrefenences**;两者的区别很明显,**apply()** 通过线程进行异步处理,如果任务完成则从队列中移除 **QueuedWork.removeFinisher**,无法获取提交的结果;**commit** 是同步更新,使用时会阻塞主线程,因为是同步提交,可以获取 **Boolean** 状态的提交状态,进而判断是否提交成功; #### 4. SharedPreferences 问题与优化 #### **SharedPreferences** 虽因其便利性而应用广泛,但也存在一些弊端; ##### Q1: 编辑 get()/put() 时均会涉及到互斥锁和写入锁,并发操作时影响性能; ##### ##### A1: 读写操作都是针对的 **SharedPreferences** 对象,可适当拆分文件或降低访问频率等; ##### ##### Q2: 使用时出现卡顿引发 GC 或 ANR; ##### ##### A2: ##### 1. 不要存放大数据类型的 **key-value** 避免导致一直在内存中无法释放; 2. 尽量避免频繁读写操作; 3. 尽量减少 **apply()** 次数,每次都会新建一个 **EditorImpl** 对象,可以批量处理统一提交; ##### Q3: 不能跨进程通信,不能保证更新本地数据后被另一个进程所知; ##### ##### A3: 可以借助 ContentProvider 来在多进程中更新数据; ##### ### MMKV ### #### 1. MMKV 基本介绍 #### 正因为 **SharedPreferences** 还有很大的优化空间,因为我们才会尝试其他存储框架;其中 [**腾讯 MMKV**][MMKV] 得到很多人的支持; **MMKV** 分别代表的是 **Memory Mapping Key Value**,是基于 **mmap** 内存映射的 **key-value** 组件,底层序列化/反序列化使用 **protobuf** 实现,性能高,稳定性强;官网 **Wiki** 介绍的优势很明显,是目前微信正在使用的轻量级存储框架;在 **Android / macOS / Win32 / POSIX** 多个平台一并开源; #### 2. MMKV 优势 #### 小菜从如下几个角度简单分析一下 **MMKV** 的优势; **a. 数据格式及更新范围优化**; **SharedPreferences** 采用 **xml** 数据存储,每次读写操作都会全局更新;**MMKV** 采用 **protobuf** 数据存储,更紧密,支持局部更新 **b. 文件耗时操作优化**; **MMKV** 采用 **MMap** 内存映射的方式取代 **I/O** 操作,使用 **0**拷贝技术提高更新速度; **c. 跨进程状态同步**; **SharedPreferences** 为了线程安全不支持跨进程状态同步;**MMKV** 通过 **CRC** 校验 和文件锁 **flock** 实现跨进程状态更新; **d. 应用便捷性,较好的兼容性**; **MMKV** 使用方式便捷,与 **SharedPreferences** 基本一致,迁移成本低; 如图: ##### 2.1 Memory Mapping 内存映射 ##### **Memory Mapping** 简称 **MMap** 是一种将磁盘上文件的一部分或整个文件映射到应用程序地址空间的一系列地址机制,从而应用程序可以用访问内存的方式访问磁盘文件; > ![b3ded286007429b9e41796ac1f154667.png][] 由此可见,**MMap** 的优势很明显了,因为进行了内存映射,操作内存相当于操作文件,无需开启新的线程,相较于 **I/O** 对文件的读写操作只需要从磁盘到用户主存的一次数据拷贝过程,减少了数据的拷贝次数,提高了文件的操作效率;同时 **MMap** 只需要提供一段内存,只需要关注往内存文件中读写操作即可,在操作系统内存不足或进程退出时自动写入文件中; 当然,**MMap** 也有自身的劣势,因为 **MMap** 需要提供一度长度的内存块,其映射区的长度默认是一页,即 **4kb**,当存储的文件内容较少时可能会造成空间的浪费; ##### 2.2 Protocol Buffers 编码结构 ##### **Protocol Buffers** 简称 **protobuf**,是 **Google** 出品的一种可扩展的序列化数据的编码格式,主要用于通信协议和数据存储等;利用 **varint** 原理(一种变长的编码方式,值越小的数字,使用的字节越少)压缩数据以后,二进制数据非常紧凑; **protobuf** 采用了 **TLV(TAG-Length-Value)** 的编码格式,减少了分隔符的使用,编码更为紧凑; > ![7b51f9d5617e24ebfa894f1d18986899.png][] **protobuf** 在更新文件时,虽然也不方便局部更新,但是可以做增量更新,即不管之前是否有相同的 **key**,一旦有新的数据便添加到文件最后,待最终文件读取时,后面新的数据会覆盖之前老旧的数据; 当添加新的数据时文件大小不够了,需要全量更新,此时需要将 **Map** 中数据按照 **MMKV** 方式序列化,滤重后保存需要的字节数,根据获取的字节数与文件大小进行比较;若保存后的文件大小可以添加新的数据时直接添加在最后面,若保存后的文件大小还是不足以添加新的数据时,此时需要对 **protobuf \* 2** 扩容; **protobuf** 功能简单,作为二进制存储,可读性较差;同时无法表示复杂的概念,通用性相较于 **xml** 较差;这也是 **protobuf** 的不足之处; ##### 2.3 flock 文件锁 + CRC 校验 ##### **SharedPreferences** 因为线程安全不支持在多进程中进行数据更新;而 **MMKV** 通过 **flock** 文件锁和 **CRC** 校验支持多进程的读写操作; 小菜简单理解,**MMKV** 在进程 **A** 中更新了数据,在进程 **B** 中获取当前数据时会先通过 **CRC** 文件校验看文件是否有过更新,若没更新直接读取,若已更新则重新获取文件内容在进行读取; 而为了防止多个进程同时对文件进行写操作,**MMKV** 采用了文件锁 **flock** 方式来保证同一时间只有一个进程对文件进行写操作; #### 3. MMKV 应用与注意 #### **MMKV** 的应用非常简单,根据官网集成即可: 1. **Maven** 仓库引入 **mmkv**; implementation 'com.tencent:mmkv-static:1.2.2' 1. 初始化; MMKV.initialize(this); 1. 根据文件名称创建对应存储文件;建议设置 **MMKV** 为全局实例,方便统一处理; // 默认文件名 MMKV kv = MMKV.defaultMMKV(); // 指定文件名 MMKV kv = MMKV.mmkvWithID(Constants.SP_APP_CONFIG); 1. 可以通过 **encode()** 方式存储数据也可以使用和 **SharedPreferences** 相同的 **put()** 方式存储数据; kv.encode("name", "阿策小和尚"); kv.encode("age", 18); kv.putString("address", "北京市海淀区"); kv.putInt("sex", 0); 1. 同样可以采用 **decodeXXX()** 或 **getXXX()** 获取数据; kv.decodeString("name", ""); kv.decodeInt("age", -1); kv.getString("address", ""); kv.getInt("sex", -1); 1. 与 **SharedPreferences** 一样,**remove()** 清除一条数据,**clear()** 清空全部数据; kv.remove(); kv.clear(); 1. 对于应用中已存在 **SharedPreferences** 时,**MMKV** 提供了一键转换为 **MMKV** 方式; MMKV mmkv = MMKV.mmkvWithID(Constants.SP_APP_CONFIG); SharedPreferences sp = context.getSharedPreferences(mid, Context.MODE_PRIVATE); mmkv.importFromSharedPreferences(sp); sp.edit().clear().commit(); > ![6da68367f6f67d39747a5995c2ead4ca.png][] -------------------- 小菜对于 **SharedPreferences** 和 **MMKV** 的底层源码还不够深入,如有错误,请多多指导! > 本文在开源项目:[https://github.com/Android-Alvin/Android-LearningNotes][https_github.com_Android-Alvin_Android-LearningNotes] 中已收录,里面包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中… 作者:老菜和尚 [8f4c034a776beb5f1e3be96c670920a1.png]: /images/20221123/09037075d107438bb8d6217028f8fc45.png [8c41bef0b70acd880d917b4ec5acf777.png]: /images/20221123/e96bb5f8d0a04594bc7af50e333772a3.png [25c4a30f444a6f1533a01856cd13ab67.png]: /images/20221123/6645871ac54049d4830b51c5c9a38c45.png [MMKV]: https://github.com/Tencent/MMKV [b3ded286007429b9e41796ac1f154667.png]: /images/20221123/d7e2ef876aba444193b8dfd9ed31dbbd.png [7b51f9d5617e24ebfa894f1d18986899.png]: /images/20221123/81eb5daea31f4bb5aebb5cb0f22979ba.png [6da68367f6f67d39747a5995c2ead4ca.png]: /images/20221123/a1a21c310b684c4ab9d814044d5df99b.png [https_github.com_Android-Alvin_Android-LearningNotes]: https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2FAndroid-Alvin%2FAndroid-P7%2Fblob%2Fmaster%2FAndroid%25E5%25BC%2580%25E5%258F%2591%25E8%25BF%2598%25E4%25B8%258D%25E4%25BC%259A%25E8%25BF%2599%25E4%25BA%259B%25EF%25BC%259F%25E5%25A6%2582%25E4%25BD%2595%25E9%259D%25A2%25E8%25AF%2595%25E6%258B%25BF%25E9%25AB%2598%25E8%2596%25AA%25EF%25BC%2581.md
相关 腾讯一面:请你分别谈谈SharedPreferences 和MMKV SharedPreferences 作为轻量级存储在 Android 应用中是必不可少的,但依旧存在较大的优化空间,小菜在做性能优化时尝试了新的利器 腾讯 MMKV,小菜今天按 本是古典 何须时尚/ 2022年12月15日 14:13/ 0 赞/ 82 阅读
相关 腾讯一面面经(前端) 我之前在京东实习过,目前在滴滴实习。 一、自我介绍 了解了一些做的项目的细节。 二、一些问题 1. 元素的隐藏与显示 2. cookie localstora 一时失言乱红尘/ 2022年11月26日 09:52/ 0 赞/ 189 阅读
相关 面试官:请你谈谈ConcurrentHashMap ![268a46f60f8556572a4292507f133e47.gif][] 点击上方蓝字,关注我们 ![c666f8b2da76f3a843068e3ea1828e 电玩女神/ 2022年11月12日 11:59/ 0 赞/ 102 阅读
相关 腾讯业务运维一面凉面 感受 腾讯与之前的二线三线公司给人的感觉完全不一样,偏向于技术底层,还有职业发展的愿景,更多看的是潜力 问题 1 自我介绍 2 职业规划? 傷城~/ 2022年10月22日 02:54/ 0 赞/ 130 阅读
相关 Android 安卓告别SharedPreFerences,你好MMKV 文章目录 你好 MMKV! 为什么要使用 MMKV 代码使用 引入 最简单的代码栗子 曾经终败给现在/ 2022年10月16日 15:28/ 0 赞/ 163 阅读
相关 腾讯2016校招面试经验分享(一面) 今天去参加腾讯公司的面试,我投的后台C++的岗位,今天就一面,总共80分钟。(面试官问了50分钟左右 + 30分钟做个一道编程题)。 面试如下: M (面试官) W 布满荆棘的人生/ 2022年08月08日 14:44/ 0 赞/ 312 阅读
相关 腾讯一面 4月2号腾讯实习招聘机试,抱着做练习题的态度做了一下,结果10天后通知我去面试,如下为面试官问的问题,最后一题编程题,附上答案。 1. 自我介绍 2. TCP/IP三次 小鱼儿/ 2022年07月28日 11:48/ 0 赞/ 253 阅读
相关 请你谈谈cookie的弊端 cookie虽然在持久保持客户端数据提供了方便,分担了服务器存储的负担,但还是有很多局限。 1、IE6或更低版本最多20个cookie; 2、IE7和之后的版本最后可以有5 痛定思痛。/ 2022年06月08日 05:29/ 0 赞/ 119 阅读
相关 腾讯一面 2018年4月20日春招,最近较忙,才整理一下。 1. 项目经历 2. 一个字符数组a\[\], 一个字符数组b\[\],求a与b的差集c。 3. 64匹马,8个跑道,至 待我称王封你为后i/ 2022年05月26日 00:53/ 0 赞/ 207 阅读
相关 腾讯一面,焉知喜凉 > 个人技术博客:http://www.zhenganwen.top 有用过Java的并发包吗,里面有哪些并发的数据结构 是指JUC包吗?里面有 深藏阁楼爱情的钟/ 2022年01月20日 03:47/ 0 赞/ 183 阅读
还没有评论,来说两句吧...