剖析 SharedPreferences apply 引起的 ANR 问题

导读

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

发布时间: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 已经全量开启清理等待锁策略,上线至今没有发现此策略产生的用户反馈。

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
SharedPreferencesAndroid 中一种轻量级的数据存储方式,用于存储简单的键值对。它可以用来存储用户的偏好设置、应用程序的状态等等。SharedPreferences 存储的数据是以 XML 文件的形式保存在设备上的,它的数据是应用程序私有的,只能被本应用程序读取和修改。 使用 SharedPreferences 可以通过以下步骤实现: 1. 获取 SharedPreferences 对象:通过 Context 的 getSharedPreferences() 方法获取 SharedPreferences 对象。 2. 存储数据:通过 SharedPreferences.Editor 对象的 putXXX() 方法存储数据,其中 XXX 表示不同的数据类型。 3. 提交数据:通过 SharedPreferences.Editor 对象的 commit() 方法或 apply() 方法提交数据。 4. 读取数据:通过 SharedPreferences 对象的 getXXX() 方法读取数据,其中 XXX 表示不同的数据类型。 下面是一个简单的示例代码: ```java // 获取 SharedPreferences 对象 SharedPreferences preferences = getSharedPreferences("my_data", MODE_PRIVATE); // 存储数据 SharedPreferences.Editor editor = preferences.edit(); editor.putString("username", "张三"); editor.putInt("age", 20); editor.commit(); // 读取数据 String username = preferences.getString("username", ""); int age = preferences.getInt("age", 0); ``` 在这个示例中,我们通过 getSharedPreferences() 方法获取名为 "my_data" 的 SharedPreferences 对象,然后通过 SharedPreferences.Editor 对象存储了一个字符串和一个整数,最后通过 getString() 和 getInt() 方法读取了这些数据。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值