找回密码
 立即注册
查看: 298|回复: 0

Android 性能优化篇之SharedPreferences使用优化

[复制链接]
发表于 2022-6-23 16:46 | 显示全部楼层 |阅读模式
作者:小马快跑
转载地址:https://juejin.cn/post/7110618131000721416
SP的使用及存在的问题


SharedPreferences(以下简称SP)是Android本地存储的一种方式,是以key-value的形式存储在/data/data/项目包名/shared_prefs/sp_name.xml里,SP的使用示例及源码解析参见:Android本地存储之SharedPreferences源码解析。以下是SP的一些结论:
    SharedPreferences读取xml文件时,会以DOM方式解析(把整个xml文件直接加载到内存中解析),在调用getXXX()方法时取到的是内存中的数据,方法执行时会有个锁来阻塞,目的是等待文件加载完毕,没加载完成之前会wait()。SP第一次初始化到读取到数据存在一定延迟,因为需要到文件中读取数据,因此可能会对UI线程流畅度造成一定影响,严重情况下会产生ANR。SharedPreferences写文件时,如果调用的commit(),会将数据同步写入内存中,内存数据更新,再同步写入磁盘中; 如果调用的apply(),会将数据同步写入内存中,内存数据更新,然后异步写人磁盘,也就是说可能写磁盘操作还没有完成就直接返回了。在UI线程中建议使用apply(),因为同步写磁盘,当文件较大时,commit()会等到写磁盘完成再返回,可能会有ANR问题。写文件时即使用的是apply()方法,依然有可能会造成ANR问题,这是为什么呢?先看下apply()的流程。
apply()流程分析

写文件流程( 8.0以上)


apply()为什么还会出现ANR呢?我们来看下apply()的逻辑(这里源码是看的API30的):
//SharedPreferencesImpl.EditorImpl.java@Overridepublic void apply() {    final long startTime = System.currentTimeMillis();    //写入内存    final MemoryCommitResult mcr = commitToMemory();    //这里的操作使用CountDownLatch实现等待效果    final Runnable awaitCommit = new Runnable() {            @Override            public void run() {                try {                    //writtenToDiskLatch类型是CountDownLatch(1)                    mcr.writtenToDiskLatch.await();                } catch (InterruptedException ignored) {                }               }            }        };    //将awaitCommit加入队列中,后续Activity的onStop()中即会执行这个Runnable等待    QueuedWork.addFinisher(awaitCommit);    Runnable postWriteRunnable = new Runnable() {            @Override            public void run() {                awaitCommit.run();                QueuedWork.removeFinisher(awaitCommit);            }        };    //文件写入操作    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);}
QueuedWork.addFinisher(awaitCommit)将awaitCommit加入到队列中,awaitCommit在执行时利用CountDownLatch机制可以实现对当前线程的阻塞效果,后续Activity的onStop()中会将这里的awaitCommit取出来执行,即UI线程会阻塞等待sp文件写入磁盘。
//SharedPreferencesImpl.javaprivate 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();                }            }        };    //commit()会在当前线程进行写入操作    if (isFromSyncCommit) {        boolean wasEmpty = false;        synchronized (mLock) {            wasEmpty = mDiskWritesInFlight == 1;        }        if (wasEmpty) {            writeToDiskRunnable.run();            return;        }    }    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);}<hr>//QueuedWork.javapublic 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并传入HandlerThread的Looper,即Handler会在子线程中处理消息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;    }}private static class QueuedWorkHandler extends Handler {    static final int MSG_RUN = 1;    QueuedWorkHandler(Looper looper) {        super(looper);    }    public void handleMessage(Message msg) {        if (msg.what == MSG_RUN) {            processPendingWork();        }    }}private static void processPendingWork() {    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) {            //取出Runnable并执行            for (Runnable w : work) {                w.run();            }        }    }}
apply()写入操作是通过SharedPreferencesImpl#enqueueDiskWrite()在HandlerThread构造的子线程中完成的,写入成功后会通过writtenToDiskLatch.countDown()释放awaitCommit中的锁,使得UI线程恢复执行。
QueuedWork.waitToFinish ( 8.0以上)


Activity的onStop()、Service的onDestroy()执行时,都会调用到QueuedWork.waitToFinish()方法:
//ActivityThread.javaprivate void handleStopService(IBinder token) {    Service s = mServices.remove(token);    if (s != null) {        try {            if (localLOGV) Slog.v(TAG, "Destroying service " + s);            s.onDestroy();            s.detachAndCleanUp();            //看这里 看这里!!!            QueuedWork.waitToFinish();            //...其他代码...        } catch (Exception e) {        }    }}@Overridepublic void handleStopActivity(IBinder token, int configChanges,        PendingTransactionActions pendingActions, boolean finalStateRequest, String reason) {    final ActivityClientRecord r = mActivities.get(token);    r.activity.mConfigChangeFlags |= configChanges;    final StopInfo stopInfo = new StopInfo();    performStopActivityInner(r, stopInfo, true /* saveState */, finalStateRequest,            reason);    //大于API11的时候执行    if (!r.isPreHoneycomb()) {        //看这里 看这里!!!        QueuedWork.waitToFinish();    }   //......}
Activity的onStop()、Service中的onDestroy()都是间接在ActivityThread中的handleStopService()、handleStopActivity()执行的,这两个方法里都会执行到QueuedWork.waitToFinish():
public static void waitToFinish() {    long startTime = System.currentTimeMillis();    boolean hadMessages = false;    Handler handler = getHandler();    synchronized (sLock) {        if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {            // Delayed work will be processed at processPendingWork() below            handler.removeMessages(QueuedWorkHandler.MSG_RUN);        }        // We should not delay any work as this might delay the finishers        sCanDelay = false;    }    StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();    try {        //把任务取出来,直接在当前线程处理 8.0之后才有的逻辑        processPendingWork();    } finally {        StrictMode.setThreadPolicy(oldPolicy);    }    try {        while (true) {            Runnable finisher;            synchronized (sLock) {                //重点 看这里 看这里!!!                finisher = sFinishers.poll();            }            if (finisher == null) {                break;            }            finisher.run();        }    } finally {        sCanDelay = true;    }   }}
这里的sFinishers中取的Runnable就是在写文件之前通过QueuedWork.addFinisher(awaitCommit)添加的,当取出awaitCommit执行时即会阻塞当前线程,如果apply()中写入磁盘时间过长导致awaitCommit的锁没有及时释放,UI线程就会因为长时间被阻塞得不到执行而出现ANR了。

用一张图来总结:

图片来自:今日头条 ANR 优化实践系列 - 告别 SharedPreference 等待
写文件流程( 8.0以下)

public void apply() {            final MemoryCommitResult mcr = commitToMemory();            //这里的操作时为了CountDownLatch实现等待效果            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);        }QueuedWork.waitToFinish ( 8.0以下)

    public static void waitToFinish() {        Runnable toFinish;        while ((toFinish = sPendingWorkFinishers.poll()) != null) {            toFinish.run();        }    }
8.0以下的流程相对更简单一些,但核心流程是一样的,当在UI线程中调用到QueuedWork.waitToFinish()时,如果写入磁盘的操作还未完成且耗时比较长,就会引起UI线程的ANR。

综述,使用apply()依然有可能会造成ANR问题。
如何优化

Jetpack DataStore替代


Jetpack DataStore 是一种改进的新数据存储解决方案,允许使用协议缓冲区存储键值对或类型化对象。DataStore以异步、一致的事务方式存储数据,克服了 SharedPreferences(以下统称为SP)的一些缺点。DataStore基于Kotlin协程和Flow实现,并且可以对SP数据进行迁移,旨在取代SP。

DataStore提供了两种不同的实现:Preferences DataStore与Proto DataStore,其中Preferences DataStore用于存储键值对;Proto DataStore用于存储类型化对象,DataStore更详细的介绍参见:Android Jetpack系列之DataStore
MMKV替代


MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。从 2015 年中至今在微信上使用,其性能和稳定性经过了时间的验证。近期也已移植到 Android / macOS / Win32 / POSIX 平台,一并开源。

注:mmap 内存映射,可以提供一段可供随时写入的内存块,App 只管往里面写数据,由操作系统负责将内存回写到文件,不必担心 crash 导致数据丢失。

MMKV地址:https://github.com/tencent/mmkv
SP使用优化

    SP文件尽量按分类去加载存储,如果文件里存储的K-V数据过多,会导致第一次加载时间过长;另外新增一个K-V时,写入磁盘是全量更新,即会把之前的文件再次更新一遍,所以也要求SP使用时尽量分类加载存储。主要是优化UI线程中执行QueuedWork.waitToFinish(),当队列执行poll()时,通过反射修改poll()的返回值,将其设为null,这样UI线程会继续往下执行而不会原地阻塞等待了。示例如下(注意8.0以上与8.0以下处理不一样):
object SPHook {    fun optimizeSpTask() {        if (Build.VERSION.SDK_INT < 26) {            reflectSPendingWorkFinishers()        } else {            reflectSFinishers()        }    }    /**     * 8.0以上 Reflect finishers     *     */    private fun reflectSFinishers() {        try {            val clz = Class.forName("android.app.QueuedWork")            val field = clz.getDeclaredField("sFinishers")            field.isAccessible = true            val queue = field.get(clz) as? LinkedList<Runnable>            if (queue != null) {                val linkedListProxy = LinkedListProxy(queue)                field.set(queue, linkedListProxy)                log("hook success")            }        } catch (ex: Exception) {            log("hook error:${ex}")        }    }    /**     * 8.0以下 Reflect pending work finishers     */    private fun reflectSPendingWorkFinishers() {        try {            val clz = Class.forName("android.app.QueuedWork")            val field = clz.getDeclaredField("sPendingWorkFinishers")            field.isAccessible = true            val queue = field.get(clz) as? ConcurrentLinkedQueue<Runnable>            if (queue != null) {                val proxy = ConcurrentLinkedQueueProxy(queue)                field.set(queue, proxy)                log("hook success")            }        } catch (ex: Exception) {            log("hook error:${ex}")        }    }    /**     * 在8.0以上apply()中QueuedWork.addFinisher(awaitCommit), 需要代理的是LinkedList,如下:     * # private static final LinkedList<Runnable> sFinishers = new LinkedList<>()     */    private class LinkedListProxy(private val sFinishers: LinkedList<Runnable>) :        LinkedList<Runnable>() {        override fun add(element: Runnable): Boolean {            return sFinishers.add(element)        }        override fun remove(element: Runnable): Boolean {            return sFinishers.remove(element)        }        override fun isEmpty(): Boolean = true        /**         * 代理的poll()方法,永远返回空,这样UI线程就可以避免被阻塞,继续执行了         */        override fun poll(): Runnable? {            return null        }    }    /**     * 在8.0以下代理     * // The set of Runnables that will finish or wait on any async activities started by the application.     * private static final ConcurrentLinkedQueue<Runnable> sPendingWorkFinishers = new ConcurrentLinkedQueue<Runnable>();     */    private class ConcurrentLinkedQueueProxy(private val sPendingWorkFinishers: ConcurrentLinkedQueue<Runnable>) :        ConcurrentLinkedQueue<Runnable>() {        override fun add(element: Runnable?): Boolean {            return sPendingWorkFinishers.add(element)        }        override fun remove(element: Runnable?): Boolean {            return sPendingWorkFinishers.remove(element)        }        override fun isEmpty(): Boolean = true        /**         * 代理的poll()方法,永远返回空,这样UI线程就可以避免被阻塞,继续执行了         */        override fun poll(): Runnable? {            return null        }    }}
注: Android P(9.0) 中引入了对隐藏API的限制,因为这里是hook的源码,所以如果后续版本如果也被标记为隐藏API,可能会导致反射失败。找到一个绕过隐藏API限制的库:https://github.com/LSPosed/AndroidHiddenApiBypass,原理是通过Unsafe去操作的,后续可以考虑通过这种方式去突破限制。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-26 08:57 , Processed in 0.092827 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表