剖析 SharedPreferences apply 引起的 ANR 问题

本文分析了Android开发中SharedPreferences使用不当可能导致的ANR问题,特别是apply方法在组件生命周期变化时可能引发的队列等待超时。通过分析源码,揭示了commit和apply的区别,并提出了通过清理等待锁队列来避免ANR的解决方案。实验结果显示,清理策略对数据持久化影响较小,已在实际应用中得到验证。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

导读

本文作者:字节跳动技术团队

发布时间:2018-01-16

原文地址:https://mp.weixin.qq.com/s/IFgXvPdiEYDs5cDriApkxQ

对于 Android 开发来说,SharedPreferences 是再熟悉不过了,经常被用来存放一些轻量数据。不过其实其缺点蛮突出的,随便用的,可是会出麻烦的。我就遇到过由 SharedPreferences 引起的 ANR。在网上搜到了字节跳动的一篇技术文章,跟我遇到的情况是一样的,所以转发一下。


项目中 ANR 率居高不下,从统计上来看排在前面的有几个都是 SharedPreferences(以下简称 SP)引起的。接下来我们抽丝剥茧地来分析其产生原因及如何解决。

crash 堆栈信息如下。从 crash 收集平台上来看,有几个类似的堆栈信息。唯一的区别就是 ActivityThread 的入口方法。除了 ActivityThread 的 handleSleeping 方法之外,还有 handleServiceArgs、handleStopService、handleStopActivity。

在这里插入图片描述

ActivityThread 的这几个方法是 Activity 或 Service 的生命周期变化的时候调用的。从堆栈信息来看,组件生命周期变化,导致调用 QueueWork 中的队列处于等待状态,等待超时则发生 ANR。那么 QueuedWork 的工作机制是什么样的呢,我们从源码入手来进行分析。

1. SP 的 apply 到底做了什么

首先从问题的源头开始,SP 的 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);
    
    // Okay to notify the listeners before it's hit disk
    // because the listeners should always get the same
    // SharedPreferences instance back, which has thel
    // changes reflected in memory.
    notifyListeners(mcr) ;
}

apply 方法,首先创建了一个 awaitCommit 的 Runnable,然后加入到 QueuedWork 中,awaitCommit 中包含了一个等待锁,需要在其它地方释放。我们在上面看到的 QueuedWork.waitToFinish() 其实就是等待这个队列中的 awaitCommit 全部释放。

然后通过 SharedPreferencesImpl.this.enqueueDiskWrite 创建了一个任务来执行真正的 SP 持久化。

private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) {
    final Runnable writeToDiskRunnable = new Runnable() {
        public void run() {
            synchronized (mWritingToDiskLock) {
                writeToFile(mcr);
            }
            synchronized (SharedPreferencesImpl.this) {
                mDiskWritesInFlight--;
            }
            if (postWriteRunnable != null) {
                postWriteRunnable.run();
            }
    }};
    final boolean isFromSyncCommit = (postWriteRunnable == null);

    // Typical #commit() path with fewer allocations,doing a write on
    // the current thread.
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (SharedPreferencesImpl.this) {
            wasEmpty = mDiskWritesInFlight == l;
        }
        if (wasEmpty) {
            writeToDiskRunnable.run();
            return;
        }
    }
    QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}

其实无论是 SP 的 commit 还是 apply 最终都会调用 enqueueDiskWrite 方法,区别是 commit 方法调用传递的第二个参数为 null。此方法内部也是根据第二个参数来区分 commit 和 apply 的,如果是 commit 则会同步的执行 writeToFile,apply 则会将 writeToFile 加入到一个任务队列中异步的执行,从这里也可以看出 commit 和 apply 的真正区别。

writeToFile 执行完成会释放等待锁,之后会回调传递进来的第二个参数 Runnable 的 run 方法,并将 QueuedWork 中的这个等待任务移除。

总结来看,SP 调用 apply 方法,会创建一个等待锁放到 QueuedWork 中,并将真正数据持久化封装成一个任务放到异步队列中执行,任务执行结束会释放锁。Activity onStop 以及 Service 处理 onStop,onStartCommand 时,执行 QueuedWork.waitToFinish() 等待所有的等待锁释放。

2. 如何解决,清空等待队列

从上述分析来看,SP 操作仅仅把 commit 替换为 apply 不是万能的,apply 调用次数过多容易引起 ANR。所有此类 ANR 都是经由 QueuedWork.waitToFinish() 触发的,如果在调用此函数之前,将其中保存的队列手动清空,那么是不是能解决问题呢,答案是肯定的。

Activity 的 onStop,以及 Service 的 onStop 和 onStartCommand 都是通过 ActivityThread 触发的,ActivityThread 中有一个 Handler 变量,我们通过 Hook 拿到此变量,给此 Handler 设置一个 callback,Handler 的 dispatchMessage 中会先处理 callback。

    
public static void tryHackActivityThreadH() {
    try {
        if ((Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT)) {
            Reflect activityThreadRef = Reflect.on(Class.forName("android.app.ActivityThread")).call("currentActivityThread");
            if (activityThreadRef != null) {
                Handler h = activityThreadRef.field("mH", Class.forName("android.app.ActivityThread$H")).<Handler>get();
                if (h != null) {
                    Reflect hRef = Reflect.on(h);
                    Handler.Callback hCallBack = hRef.field("mCallback", Handler.Callback.class)<Handler.Callback>get();
                    ActivityThreadHCallBack activityThreadHCallBack = new ActivityThreadHCallBack(h, hCallBack);
                    hRef.set("mCallback", activityThreadHCallBack);
                }
            }
        }
    } catch (Throwable t) {
        t.printStackTrace();
        // ignore
    }     
}

在 Callback 中调用队列的清理工作

public class ActivityThreadHCallBack implements Handler.Callback {
    private static final int SERVICE_ARGS = 115;
    private static final int STOP_SERVICE = 116;
    private static final int SLEEPING = 137;
    private static final int STOP_ACTIVITY_SHOW = 103;
    private static final int STOP_ACTIVITY_HIDE = 104;
    private static final int ENTER_ANIMATION_COMPLETE = 149;

    @0verride
    public boolean handleMessage(Message msg) {
        final int what = msg.what;
        switch (what) {
            case SERVICE_ARGS:
                SpBlockHelper.beforeSPBlock("SERVICE_ARGS");
                break;
            case STOP_SERVICE:
                SpBlockHelper.beforeSPBlock("STOP_SERVICE");
                break;
            case SLEEPING:
                SpBlockHelper. beforeSPBlock("SLEEPING");
                break;
            case STOP_ACTIVITY_SHOW:
            case STOP_ACTIVITY_HIDE:
                SpBlockHelper.beforeSPBlock("STOP_ACTIVITY");
                break;
        }
        return false;
    }
}

队列清理需要反射调用 QueuedWork。

public class SpBlockHelper {
    static final String TAG= "SpBlockHelper";
    static boolean init = false;
    static String CLASS_QUEUED_WORK = "android.app.QueuedWork";
    static String FIELD_PENDING_FINISHERS = "sPendingWorkFinishers";
    static ConcurrentLinkedQueue<Runnable> sPendingWorkFinishers = null;

    public static void beforeSPBlock(String tag) {
        if (!init) {
            getPendingWorkFinishers();
            init = true;
        }
        Logger.d(TAG, "beforeSPBlock " + tag);
        if (sPendingWorkFinishers != null) {
            sPendingWorkFinishers.clear();
        }
    }
    
    static void getPendingWorkFinishers() {
        try {
            Class clazz = Class.forName(CLASS_QUEUED_WORK);
            Field field = clazz.getDeclaredField(FIELD_PENDING_FINISHERS);
            if (field != null) {
                field.setAccessible(true);
                sPendingWorkFinishers = (ConcurrentLinkedQueue<Runnable>) field.get(null);
            }
        } catch (Exception e) {
            MiraLogger.e(TAG, "getPendingWorkFinishers", e);
        }
    }
}

3. 清理等待锁会产生什么问题

SP 无论是 commit 还是 apply 都会产生 ANR,但从 Android 之初到目前 Android8.0,Google 一直没有修复此 bug,我们贸然处理会产生什么问题呢。Google 在 Activity 和 Service 调用 onStop 之前阻塞主线程来处理 SP,我们能猜到的唯一原因是尽可能的保证数据的持久化。因为如果在运行过程中产生了 crash,也会导致 SP 未持久化,持久化本身是 IO 操作,也会失败。我们清理了等待锁队列,会对数据持久化造成什么影响呢,下面我们通过一组实验来验证。

进程启动的时候,产生一个随机数字。用 commit 和 apply 两种方式来存此变量。第二次进程启动,获取以两种方式存取的值并做比较,如果相同表示 apply 持久化成功,如果不相同表示 apply 持久化失败。

  • 实验一:开启等待锁队列的清理
  • 实验二:关闭等待锁队列的清理

线上同时开启两个实验,在实验规模相同的情况下,统计 apply 失败率。

  • 实验一:失败率为 1.84%
  • 实验二:失败率为为 1.79%

可见,apply 机制本身的失败率就比较高,清理等待锁队列对持久化造成的影响不大。

目前头条 app 已经全量开启清理等待锁策略,上线至今没有发现此策略产生的用户反馈。

<think>首先,用户的问题是:“我想查找过时代码的替代方案 请问SharedPreferences PreferenceManager.getDefaultSharedPreferences 替代方案”。翻译成英文是:"I want to find alternatives to deprecated code. What are the alternatives to SharedPreferences PreferenceManager.getDefaultSharedPreferences?" 从系统级指令中,我需要: - 所有行内数学表达式使用$...$格式,但这里没有数学内容,所以可能不需要。 - 独立公式使用$$...$$,但同样,没有数学内容。 - LaTeX语法正确:确保任何数学部分正确,但这里没有。 - 使用中文回答:整个响应必须用中文。 - 生成相关问题:在回答末尾添加相关问题。 - 在引用的段落末尾添加引用标识:我需要引用提供的引用内容。 用户提供了引用内容: - 引用[1]: 关于SharedPreferences获取方法的区别。 - 引用[2]: getPreferences()、getSharedPreferences()和getDefaultSharedPreferences()的区别。 - 引用[3]: Context类中的getSharedPreferences()方法,解释了模式和废弃的模式。 - 引用[4]: PreferenceManager.getDefaultSharedPreferences()的实现和细节。 - 引用[5]: SharedPreferences使用,包括getDefaultSharedPreferences()。 用户的问题是针对PreferenceManager.getDefaultSharedPreferences()的替代方案,因为它是过时的或废弃的。从引用[3]中,我们知道MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE在Android 4.2中被废弃,MODE_MULTI_PROCESS在Android 6.0中被废弃。但getDefaultSharedPreferences()本身没有被废弃;它是获取SharedPreferences的一种方式。 在引用[4]中,它说PreferenceManager.getDefaultSharedPreferences()返回一个SharedPreferences实例,使用默认的文件名和私有模式。 在引用[5]中,它提到getDefaultSharedPreferences()使用MODE_PRIVATE。 所以,用户可能误解了。PreferenceManager.getDefaultSharedPreferences() 没有被废弃。但SharedPreferences API本身在较新的Android版本中被推荐使用DataStore或其它替代方案,因为SharedPreferences有一些限制。 在Android开发中,SharedPreferences的某些常量被废弃了,但getDefaultSharedPreferences()方法本身没有被废弃。然而,Google推荐使用Jetpack DataStore作为更现代的替代方案。 从我的知识: - SharedPreferences是用于存储键值对数据的API。 - PreferenceManager.getDefaultSharedPreferences()是获取默认SharedPreferences实例的方法。 - 在Android开发中,SharedPreferences API没有被废弃,但Google建议使用Jetpack DataStore(如Preferences DataStore或Proto DataStore)作为更好的替代方案,因为它支持异步操作、类型安全等。 所以,我应该解释替代方案。 用户的问题具体是“SharedPreferences PreferenceManager.getDefaultSharedPreferences 替代方案”,意思是替代PreferenceManager.getDefaultSharedPreferences()的方案。 但PreferenceManager.getDefaultSharedPreferences()没有被废弃;它是标准的获取方式。然而,整个SharedPreferences API在较新项目中可能被替代。 参考引用: - 引用[3]提到废弃的模式,但getSharedPreferences()本身没有被废弃。 - 引用[4]和[5]描述getDefaultSharedPreferences()。 所以,替代方案不是针对这个方法本身,而是针对SharedPreferences。 用户说“过时代码”,可能指的是废弃的模式或整个API的现代替代。 在响应中,我需要: 1. 回答结构清晰,逐步解决问题。 2. 使用中文。 3. 添加相关问题。 4. 在引用段落末尾添加引用标识。 结构响应: - 先确认问题。 - 解释PreferenceManager.getDefaultSharedPreferences()没有被废弃,但SharedPreferences有更好的替代。 - 推荐Jetpack DataStore作为替代方案。 - 提供代码示例。 - 添加相关问题。 引用提供的引用内容: - 引用[3]:解释模式。 - 引用[4]:getDefaultSharedPreferences()的实现。 - 引用[5]:默认设置。 但我需要自然地添加引用标识。 例如,在解释时,说“如引用[3]所述,...”。 最终,添加相关问题。 响应大纲: 1. 介绍:确认用户查询过时代码的替代方案。 2. 解释:PreferenceManager.getDefaultSharedPreferences()没有被废弃,但SharedPreferences API在现代Android开发中推荐使用Jetpack DataStore。 3. 替代方案:介绍Jetpack DataStore。 4. 代码示例:展示如何使用DataStore替代SharedPreferences。 5. 相关问题:生成几个相关问题。 使用中文。</think>### SharedPreferences 中 PreferenceManager.getDefaultSharedPreferences 的替代方案 您好!您的查询是关于 Android 开发中 `PreferenceManager.getDefaultSharedPreferences()` 方法的替代方案。您提到这是“过时代码”,但实际上,`PreferenceManager.getDefaultSharedPreferences()` 方法本身在 Android API 中 **并未被废弃**,它仍然是获取默认 SharedPreferences 实例的标准方式。不过,SharedPreferences API 整体在较新 Android 版本中(尤其是针对某些模式)存在局限性,Google 推荐使用更现代的替代方案,如 Jetpack DataStore。这是因为 SharedPreferences 存在线程阻塞、缺乏类型安全等问题,而 DataStore 提供了异步操作、更好的错误处理等优势[^3][^5]。 下面我将逐步解释当前情况,并推荐替代方案,确保回答清晰可靠。 #### 1. **当前状态分析:为什么需要替代方案?** - `PreferenceManager.getDefaultSharedPreferences()` 方法用于获取默认的 SharedPreferences 文件,文件名通常为应用包名加 `_preferences`(例如 `com.example.app_preferences`),并使用 `MODE_PRIVATE` 模式(仅当前应用可访问)。这在引用[4]和[5]中有详细说明:它通过 `context.getSharedPreferences()` 实现,默认模式为 `MODE_PRIVATE`[^4][^5]。 - **问题根源**:SharedPreferences API 中的某些常量(如 `MODE_WORLD_READABLE` 和 `MODE_WORLD_WRITEABLE`)在 Android 4.2 中被废弃,`MODE_MULTI_PROCESS` 在 Android 6.0 中被废弃,因为它们存在安全风险和多进程问题[^3]。虽然 `getDefaultSharedPreferences()` 本身未废弃,但整个 SharedPreferences 机制在性能、可靠性和现代特性上不如新方案。 - **官方推荐**:Google 在 Android Jetpack 中引入了 **DataStore** 作为 SharedPreferences 的替代品,特别适合新项目或重构旧代码。DataStore 有两种实现: - **Preferences DataStore**:类似 SharedPreferences,但支持异步操作和协程。 - **Proto DataStore**:使用 Protocol Buffers 定义数据结构,提供类型安全。 #### 2. **推荐替代方案:Jetpack DataStore** 如果您想避免 SharedPreferences 的潜在问题,Jetpack DataStore 是最佳选择。它解决了 SharedPreferences 的阻塞主线程问题,并提供了更健壮的 API。以下是迁移步骤: - **添加依赖**:在 `build.gradle` 文件中添加 DataStore 依赖。 ```gradle dependencies { implementation "androidx.datastore:datastore-preferences:1.0.0" // 或者使用 Proto DataStore:implementation "androidx.datastore:datastore-core:1.0.0" } ``` - **基本用法示例**(使用 Preferences DataStore 替代 `getDefaultSharedPreferences()`): - 创建 DataStore 实例: ```kotlin // 在 Application 类或 Activity 中初始化 val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings") ``` - 读写数据(使用 Kotlin 协程,避免主线程阻塞): ```kotlin // 写入数据 suspend fun saveValue(key: String, value: Int) { context.dataStore.edit { preferences -> preferences[intPreferencesKey(key)] = value } } // 读取数据 val valueFlow: Flow<Int> = context.dataStore.data .map { preferences -> preferences[intPreferencesKey("my_key")] ?: 0 // 默认值 } ``` - **优势**: - **异步操作**:读写不会阻塞 UI 线程。 - **类型安全**:使用 `intPreferencesKey` 等类型化键,减少错误。 - **迁移支持**:DataStore 提供了从 SharedPreferences 迁移的工具,可以平滑过渡。 - **为什么不是直接替换方法?** `getDefaultSharedPreferences()` 是 SharedPreferences 的快捷方式,而 DataStore 是一个全新的 API。如果您必须使用 SharedPreferences,可以直接用 `context.getSharedPreferences("default_name", Context.MODE_PRIVATE)` 替代,但这不解决根本问题。DataStore 才是长期推荐的方案[^3][^5]。 #### 3. **注意事项** - **兼容性**:DataStore 需要 Android 5.0+(API 21)和 Kotlin 协程支持。对于旧项目,如果您暂时无法迁移,可以继续使用 `getDefaultSharedPreferences()`,但确保模式为 `MODE_PRIVATE`(唯一未被废弃的模式)[^3]。 - **性能对比**:DataStore 在大型数据集上表现更好,避免了 SharedPreferences 的 `commit()` 或 `apply()` 导致的 ANR 风险。 - **引用说明**:如引用[3]所述,SharedPreferences 的废弃模式主要涉及多进程和全局访问,而 `getDefaultSharedPreferences()` 默认使用安全模式,因此它本身仍可用,但整体生态已转向 DataStore[^3]。引用[5]也提到,默认文件名基于包名,这在 DataStore 中可通过 `name` 参数自定义[^5]。 #### 4. **迁移建议** - 评估项目需求:如果只是简单键值存储,Preferences DataStore 足够;如果需要复杂数据结构,选择 Proto DataStore。 - 测试:使用 Android Studio 的 DataStore API 测试工具,确保数据迁移无误。 - 官方资源:参考 [Android Developers 文档](https://developer.android.com/topic/libraries/architecture/datastore) 获取更多示例。 ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值