在之前系列文章的总结中(详情见文末),我们介绍了Android系统中ANR的设计原理以及在实际操作中监控ANR得到的解决方案。传统的监控与治理虽然ANR通过解决方案,问题已得到有效抑制,部分系统在实际使用过程中,由于组件的最初设计意图与开发人员的实际使用不一致而引发冲突,需要立即解决。如何从系统层面跳过Google在实际开发过程中因SP滥用而造成的设计缺陷,避免ANR问题。
为了让开发人员在设计之初更容易,Google 实现了一个轻量级数据持久化解决方案,——SharedPreference (sp)。由于其API简单、使用方便,受到开发者的青睐和依赖。越来越重了。当我们继续迭代应用程序的版本时,我们意识到Google 谈论轻量级数据存储是有原因的。应用越重,ANR问题越严重。本文从源码层面分析加载和写入过程中出现ANR问题的原因以及相关优化方案。
在分析SP引起ANR的原因时,经常会出现两类SharedPreference问题。下面介绍这两类ANR问题的原因和优化方案。
问题1:sp文件创建后,使用另一个线程加载并解析对应的sp文件。但是,如果当UI 线程尝试访问sp 文件的内容时sp 文件没有完全加载到内存中并进行解析,那么UI 线程将会被阻塞,直到SP 文件完全加载到内存中。具体ANR线程栈为:
主要原因是SP文件没有加载或解析到内存中,此时无法直接使用sp提供的接口。一旦创建了sp,就会通过加载相应的sp文件并运行startLoadFromDisk()来同时启动一个线程。
对于startLoadFromDisk(),将sp 标记为不可用。如果稍后尝试读取或写入数据,则读写线程将被阻塞,直到所有sp 文件都被读取和解析。
当线程读取或写入时,它会进入awaitLoadedLocked()逻辑。上图中,mLoaded为false,表示sp文件没有加载到内存中并进行解析。读写时,写入线程直接阻塞mLock,直到执行loadFromDisk()方法。
sp文件完全加载到内存并解析,所有等待当前sp的读写线程都直接启动。
问题2:为了保证Google系统内数据的跨进程一致性,早期的应用程序可以使用sp进行进程间通信。当一个组件被销毁或者其他生命周期被销毁时,任务必须在当前组件的生命周期内完成以保证当前的写入。写。此时,主线程在组件销毁或组件挂起生命周期中一直等待,直到sp完全写入相应文件。在下面的示例中,UI 线程在QueuedWork.waitToFinish() 处被阻塞,如图所示。然后,将源码应用到原文中,从头到尾整理一下写文件的整个过程,找到问题的根源。
具体需要等待文件写入的消息在ActiveThread H中,具体消息类型为:
publicstaticfinalintSERVICE_ARGS=115; publicstaticfinalintSTOP_SERVICE=116; publicstaticfinalintPAUSE_ACTIVITY=101; publicstaticfinalintSTOP_ACTIVITY_SHOW=103; publicstaticfinalintSLEEPING=137; 这种等待行为并不会造成问题,因为Google官方设计最初是作为轻量级数据存储方式来设计的,但在实践中的使用会导致过度等待行为。对于SP,这种延迟会不受控制地增加,直到最终发生ANR,并且对于业务操作繁重的应用程序,此问题更为明显。具体问题栈如下,均为系统栈。接下来分析这个ANR的根本原因,首先从waitToFinish开始。具体ANR堆栈为:
早期的sp接口只有一个同步写文件的commit接口。由于该接口直接影响开发者使用,因此Google官方提供了异步执行接口。开发者认为这个异步是真正的异步,所以使用了spappy接口,大流量的APP会因为这个应用设计缺陷导致的ANR问题而受到严重影响。
适用界面整体详细设计思路如下(基于Android 8.0及更早版本分析):
总体思路可以简单概括如下。
1、sp.apply()获取需要写入内存的数据集,并同步写入文件。内存提交结果:
2. 将MemoryCommitResult封装成Runnable,扔给子线程的queued-work-looper; 3. 将MemoryCommitResult的mapToWriteToDisk对应的key和value写入子线程的文件; 4. 完成后,执行MemoryCommitResult的setDiskWriteResult方法其中重要的一步是writeToDiskLatch.countDown(); 5. 当主线执行QueuedWork.waitToFinish() 时,如下所示:
6. 主线程是做什么的?现在我们需要从QueuedWork.add(Runnablefinisher)开始。具体的Runnable是:这里什么也没做。直接等待mcr.writeToDiskLatch.await() )。这里你应该有这样的印象:锁是在第4步写完文件后直接被neutron线程释放的。
结论:整个API的流程分析还是比较复杂的,但是runnables是一层层封装的,从这个线程抛出到那个线程,当子线程写完文件后,锁被释放,主线程整体思路比较很简单,尽管它在特定位置运行并且需要等待子线程完成写入文件。此问题的根本原因是未将挂起的应用操作写入文件。主线程等待指定的消息执行。如果等待时间过长,就会出现ANR。
Google官方在Android 8.0及以后版本中优化了SP写入逻辑,但希望上面的第6步能够帮助子线程一起写入,而不是UI线程愚蠢地等待。但是,这是一个保守的辅助,所以请解决这个问题优雅地发出。
问题一的解决方案:对于加载缓慢的问题,我们通常使用preload的方式来触发sp文件的加载和解析。所以当实际使用的时候,sp很可能已经被加载和解析了;你真正需要解决的是核心场景的SP不要太大。您仍然应该遵循谷歌的官方声明。这是一种轻量级的数据持久化存储方式。不要存储太多数据。避免文件大小过大。更快的加载和解析。这需要太多时间。
问题2:对于Google 为何这样设计,我提出了一些我自己的猜测。
Google 希望数据尽快写入文件,等待是没有意义的。直接由主线程等待并不能提高写入效率。我希望SP 实时写入文件。这种异步写入方式本身在方便跨进程实时性能的sp中访问文件时无法保证实时性能。可能是组件切换状态时。如果此时进程中没有任何组件,那么当系统资源较低时,其优先级就不太可能被系统杀死,几乎可以忽略不计。最有可能的是,谷歌的人希望从提交切换到无缝应用。模拟原始提交行为。原文件改为多次commit操作,每次写入一次。最终的应用在主线程上完成,并等待所有写操作一次写入。从上面的假设我们可以看出,主线程等待子线程写入是完全没有意义的,所以我希望能够以某种方式跳过这种浪费的等待行为。对于SharedPreference相关逻辑,我找到了以下起点:以下是8.0之前版本的优化策略。 8.0 及更高版本的处理方式类似。
如果需要在waitToFinish期间直接跳转到主线程来完成执行toTinish.run(),这显然是不可能的。如果可以让sPendingWorkFinishers.poll() 返回null ,那么这里的等待行为将被直接跳过。对于ConcurrentLinkedQueue集合,您可以直接动态代理该集合并重写轮询方法以始终返回null。在这种情况下,UI 不会等待子线程完成写入文件。事实证明该方法简单有效。
为了解决这种写等待ANR问题,还有另一种方法可以全局替换write方法。通过检测,所有API实现都被替换,并使用其他存储方法。这种方式修复成本和风险较高,但会稍后修复。您可以随意改变存储方式,使用更加灵活。
该解决方案的优势已得到多种Byte Systems 产品的验证。该方案稳定有效,并且消除了相应堆栈带来的ANR问题。 ANR的优势很明显,相应的界面跳转及其流畅度也很明显。其他场景也得到了显着改善。
展望未来,Google 增加了一个新的Jetpack 成员:DataStore。这主要用于替换SharedPreferences。 DataStore应该是开发者期待已久的库。 DataStore基于Flow,一种新的数据存储解决方案。网上有很多参考资料,有详细的介绍。
优化实践补充参考: 今日头条ANR优化实践系列-设计原理与影响因素今日头条ANR优化实践系列-监控工具与分析思路分享今日头条ANR优化实践系列-案例分析集今日头条ANR优化实践系列-Barrier Lead主线程暂停动画
Android 平台架构团队我们是字节跳动的Android 平台架构团队。主要服务今日头条,面向GIP,也服务公司其他产品。我们在用户体验、研发流程、架构方向等方面不断优化和探索。产品性能稳定性:满足产品快速迭代的需求,同时也追求极致的用户体验。如果你对技术充满热情,想要接受更大的挑战和舞台,请加入我们。我们在北京和深圳都有职位空缺。如果您有兴趣,请发送电子邮件至tech@bytedance.com。电子邮件标题:名称- GIP - Android 平台架构。欢迎关注“字节跳动技术团队”
标题:今日头条内容优化,今日头条seo如何优化?
链接:https://www.7kxz.com/news/gl/23713.html
版权:文章转载自网络,如有侵权,请联系删除!