ANR:QueuedWork.waitToFinish(), SharedPreferences

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

java.lang.Object.wait(Native Method)
java.lang.Thread.parkFor$(Thread.java:1220)
sun.misc.Unsafe.park(Unsafe.java:299)
java.util.concurrent.locks.LockSupport.park(LockSupport.java:158)
java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:810)
java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedInterruptibly(AbstractQueuedSynchronizer.java:970)
java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(AbstractQueuedSynchronizer.java:1278)
java.util.concurrent.CountDownLatch.await(CountDownLatch.java:203)
android.app.SharedPreferencesImpl$EditorImpl$1.run(SharedPreferencesImpl.java:366)
android.app.QueuedWork.waitToFinish(QueuedWork.java:88)
android.app.ActivityThread.handleStopActivity(ActivityThread.java:3679)
android.app.ActivityThread.access$1300(ActivityThread.java:166)
android.app.ActivityThread$H.handleMessage(ActivityThread.java:1438)
android.os.Handler.dispatchMessage(Handler.java:102)
android.os.Looper.loop(Looper.java:148)
android.app.ActivityThread.main(ActivityThread.java:5628)
java.lang.reflect.Method.invoke(Native Method)
com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:853)
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:737)

这个ANR日志Google官方也有关注到:https://issuetracker.google.com/issues/62206685

SP 的 apply 到底做了什么?

这是源码分析 https://gityuan.com/2017/06/18/SharedPreferences/

首先从问题的源头开始,SP 的 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 the
        // 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 == 1;
        }

        if (wasEmpty) {
            writeToDiskRunnable.run();
            return;
        }
    }

    QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}

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

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

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

如何解决,清空等待队列

从上述分析来看,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;

    @Override
    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);
        }
    }
}

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

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

还有一种解决思路就是用MMKV代替SP,具体参考如下:
腾讯持久化框架MMKV原理探究

Tencent/MMKV


以下回答来自ChatGPT 4.0,中规中矩,但未能一针见血的挖掘问题或者给出建设性修复方案


这段ANR(Application Not Responding)报错信息显示了一个涉及线程同步的问题。ANR发生时,通常是因为应用程序在主线程上执行了一个耗时操作,超过了系统设定的时间限制(如5秒),导致未能响应用户输入或系统事件。

从提供的堆栈跟踪中,我们可以看到以下一些关键点:

  1. SharedPreferencesImpl$EditorImpl$1.run: 表明正在运行与SharedPreferences实现相关的一段代码。SharedPreferences允许Android应用存储和检索键值对数据。

  2. QueuedWork.waitToFinish: 这个方法通常在应用程序的生命周期事件(例如onStop())调用期间被调用,以确保所有后台工作都已完成。在这里,它等待与SharedPreferences的异步提交相关的工作完成。

  3. CountDownLatch.await: 此方法调用指示线程正在等待直到计数器达到零。CountDownLatch是一个同步辅助类,用于处理像这样的等待/通知场景。

  4. AbstractQueuedSynchronizer: 这是一个用于构建锁和其他同步类的框架。这表明可能存在一个线程同步问题,线程正在等待获取某种形式的共享资源。

可能的原因:

  1. 耗时的IO操作SharedPreferences的提交默认是异步的,但如果有大量数据写入磁盘,或者commit()(同步提交)被调用,可能会导致主线程阻塞。

  2. 线程同步问题:其他线程正在使用SharedPreferences,当前主线程通过CountDownLatch等待其他线程完成操作,若这些线程被阻塞或执行缓慢,就会导致ANR。

修改方案:

  1. 避免在主线程上进行耗时操作:将所有可能耗时的操作移到后台线程上,特别是IO操作。

  2. 优化SharedPreferences的使用:减少不必要的读写操作,尽量使用apply()而非commit(),因为apply()是异步的。

  3. 优化线程管理:如果应用使用多线程来访问共享资源,确保这些资源的访问是高效且正确同步的。

  4. 监控和日志记录:增加详细的日志记录,在出现性能问题时能够快速定位问题源头。

  5. 分析和测试:使用Android Profiler等性能分析工具来识别并修复性能瓶颈。

  6. 代码审查:仔细检查与线程同步相关的代码,确保没有死锁、竞争条件或其他并发问题。

请记住,解决ANR需要详细分析应用程序的行为以及与之相关的系统日志。每个ANR的具体原因都可能不同,依据具体情况采取适当的解决策略。


文章来源:
https://www.jianshu.com/p/3f64caa567e5?utm_medium=timeline
https://blog.csdn.net/u013700502/article/details/125320325
https://mp.weixin.qq.com/s/IFgXvPdiEYDs5cDriApkxQ

  • 10
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值