当前位置: 首页 > 科技观察

线上“杀”APP的滞后问题!_0

时间:2023-03-21 11:32:53 科技观察

ANR(ApplicationNotResponse)是Android开发团队经常遇到的无响应问题,但很难定位和根除。尤其是线上问题,由于难以复现,开发者很难有效快速解决。为此,本文将与大家分享作者如何在一个月内将ANR在线问题发生率降低50%的探索与实践,希望对开发者有所帮助或启发。谷歌内部研究表明,过多的崩溃和ANR会直接影响应用的评分,商店用户难以积累,严重影响应用在商店的排名。这一系列的连锁反应会给应用带来很大的损失,可能会失去在应用商店被谷歌推荐的资格。所以ANR问题对于大部分Android团队来说都是非常头疼的问题,尤其是线上问题更是让人头疼。因为局部问题可以重现,在线ANR是有难度的。因此,探索在线ANR问题的治理方案更有意义。原因及过程分析1、关于ANR从用户的角度来看,ANR问题是指用户在使用应用过程中遇到严重卡顿或卡顿时,系统给出的无响应提示弹窗。从系统端来看,ANR问题是AMS执行特定方法时出现的超时错误。触发点有四个:InputDispatchingTimeoutBroadcastQueueTimeoutServiceTimeoutContentProviderTimeout系统的ANR触发过程大致可以分为两部分:用户可以直观的ANR弹窗,这部分由AMS处理。同时AMS也会发送一个SIGQUIT信号:SignalCatcher线程会收到这个信号,处理后续的dump逻辑;市面上大部分ANR错误采集SDK都是基于这个原理。2、现状与挑战根据美图秀秀安卓端在线监测数据,ANR问题几乎是死机问题的两倍。在这种情况下,首先要考虑的是如何减少问题的数量,然后再考虑后续的计划。面对大量的问题,首先要做的就是对数据进行分析、分类和整理,以找到头部问题。美图秀秀在线header问题分布如下:经过对这些问题的简单分析,得出以下结论:根据已有的日志信息暂时无法得出nativePollOnce问题。但占用量较大,应优先处理。processPendingWork问题已经有了可靠的解决方案。比例不低,应该优先处理。剩下的题数差不多,所以处理顺序不固定。分析与实践在分析具体问题之前,首先跟大家分享一下笔者在处理问题过程中总结的一些经验。我们遇到的大部分问题可以分为两类:主动问题:指可以直接追踪定位的问题;主动问题通常可以直接解决,处理结果直接影响线上某项指标(如:对应问题的出现次数、发生率等)被动问题:问题产生的原因不确定且存在没有明确的线索;面对被动问题更多时候需要“大胆假设,谨慎求证”。通常被动问题需要更多的现场信息和旁证来追根溯源。溯源成功后的被动问题可以转化为一个或多个主动问题。当一系列问题有了明确的优先级和分类后,我们就可以开始分析单个案例了。1、网上反映的MessageQueue.nativePollOnce问题的堆栈如上图所示。这是棘手的部分:如果您只查看报告的堆栈和错误日志,则很难排除问题的根本原因。上面说了,处理这类问题需要“大胆假设”。可能的原因是:主线程状态异常导致“暂停”,堆栈漂移。A:假设主线程异常,为什么会这样呢?假设jankstack漂移了,那么真正的jankstack在哪里呢?1.1主线程卡死在Android应用的启动过程中有这样一个逻辑:zygote初始化→RuntimeInit初始化。在RuntimeInit的初始化过程中,会注册一个默认的errorhandler来响应异常,如下所示:默认的异常处理机制会在线程崩溃时同步到ActivityThread和ActivityManagerService,然后“杀死”自己。那么如果主线程出现异常,没有使用系统的处理环节,或者异常处理过程耗时过长,就会出现ANR。当主线程崩溃时,它实际上处于终止状态。此时主线程Looper的MessageQueue组件无法继续添加新的消息,Android应用的运行仅仅依赖于主线程的消息轮询——线上的错误栈也指向MessageQueue组件等待新消息的到来。因此,当主线程出现异常,无法及时杀掉进程时,系统就会触发ANR超时机制。根据上面的逻辑推理,我们通过埋点的方式找出了线上异常处理链中各个方法的耗时数据:通过比较各个异常处理方法的耗时,我们可以发现Firebase异常分析SDK是在异常处理链中耗时最长。删除此组件有望减少此问题的发生次数。最后,去除Firebase异常分析SDK的版本发布后,线上数据显示整体ANR率有所下降。接下来分析另一种情况:1.2Stuckstackdrift在这种情况下,错误报告中的stuckstack已经失真,无法反映当时现场的真实情况。所以加入在线慢功能监控可以更准确的分析这个问题。在线慢函数监控原理:查看Looper源码可知,主线程执行的所有任务都是在Looper.loop()方法中的msg.target.dispatchMessage中调度执行的。这里有一个Printer组件,在执行消息之前和之后都会调用打印方法。可以通过Looper.setMessageLogging方法设置一个Printer来监控每??条Message的执行时间:监控逻辑关键代码:后期我们通过这种方式获取到如下慢函数数据:排序后得到的分布数据:是不难得出结论:[QueuedWork.processPendingWork]和[首页创建]类问题占比最大,必须优先处理。下面将对这两个问题进行详细分析。回到上面错误处理的思路:在这个问题的处理中,采用了被动问题的通用解决方案。通过【假设->验证->上线->数据变更】的过程,最终减少或转化为活跃问题的发生。当然,造成这个问题的原因并不仅限于上述两种猜想,还有更多的可能性需要进一步探讨。2、QueuedWork.processPendingWork问题处理问题调用栈:接触AndroidFrameWork源码不多的同学可能不太了解这个类的功能。这时候可以使用Android源码搜索引擎查找:通过对源码的分析,可以得到一个大概的流程:SharedPreferencesImpl.apply()方法调用QueuedWork.add()添加写入任务SharedPreferences进入QueuedWork的任务队列,然后ActivityThread在一些组件生命周期方法中执行QueuedWork.waitToFinish→QueuedWork.processPendingWork的过程。这些生命周期方法是:handleStopServicehandlePauseActivityhandleStopActivityhandleSleeping及以上生命周期方法会先等待QueuedWork中的异步队列执行完成,然后再执行后续流程。不难得出结论,SharedPreferences的apply方法本身就是设计成异步写入的,Android系统为了保证数据有效性,会在特定的生命周期方法中等待异步写入任务完成。如果这个任务的处理时间过长,就会出现ANR问题。美图秀秀内部开发了代码编织工具MtAjx。它类似于著名的AspectJ。与AspectJ方案相比,具有更好的兼容性(公司大量项目编译遇到AspectJ问题)和更加人性化的API设计。在处理这个问题的时候,我们使用MtAjx来拦截SharedPreferences的创建,并返回带有日志输出功能的SharedPreferencesWrapper。简化流程如下:完成以上功能,只需要写一些简单的规则,使用MtAjx即可实现:接下来,可以使用自动化测试模拟在线用户的真实操作,通过SharedPreferencesWrapper的日志分析写入SharedPreferences的频率。最终输出数据如下:根据目前搜集到的线索,可以推测:GMS组件的某个操作大量调用了SharedPreferences的apply方法。此操作可能会为SharedPreferences创建过多的异步写入任务,从而导致ANR。后面处理这个问题的时候,我们修改了这个组件:使用MtAjx拦截GMS中SharedPreferences的创建和获取,返回一个安全的SharedPreferences实现。不过上线后得到的数据和预估的有些出入:自身问题减少的比例很小,线上整体ANR波动不大。猜测出现这个问题时系统的IO负载已经很严重了,或许可以应对一些场景。收获不大。然后,推出了完整共享的SharedPreferences替换来避免这个问题。请注意,这里的“全额”并不是真正的全额更换,而是不包括部分可能受影响的来电。大部分只涉及业务,这里不作为核心讨论;使用“安全”的SharedPreferences实现只是为了避免问题,它仍然需要减少系统负载。全SharedPreferences替换的处理方法与日志输出的过程类似。同样采用MtAjx的方案拦截SharedPreferences的创建,返回一个安全的实现。在这里,容灾时,将在线切换在线作为全量替换的整体控制策略,规避未知风险。最终这个策略上线后,上述问题整体减少了60%-70%,如下图:3.首页创建问题处理这个问题是通过慢函数数据发现的上面提到的分析。经过stack聚类分析,关联上线了两个指标:slowfunction-关联了四个问题。ANR——N道题关联,题目分散。其中,函数慢的问题多发生在某些View的初始化过程中。下面是网上一些触发这个问题的点:ViewPagerFix.()MainTabItemLayout.()MainActivity.onCreate()HomeTopHeaderLayout。过滤掉上述问题后的最终调用栈如下:所有的卡点都落在了Runtime.loadLibrary0()这个调用上。在线ANR或者慢功能的数据表现也差不多:都是高通机型,低端机型较多。经过分析,发现高通机型中有一个BoostFramework组件可以加快应用程序的响应速度:类似的机制可以在特定情况下提高调度优先级和CPU频率,从而加快应用程序的响应速度。但在某些情况下,这种机制会导致应用程序卡死:BoostFramework的初始化依赖于一个so的加载。但是Runtime.loadlibrary0是synchronize修饰的函数,多线程调用难免有锁竞争。美图秀秀在启动过程中,有大量的“异步”加载so操作。如果子线程先于主线程进入Runtime.loadLibrary0方法,那么获取不到锁的主线程会等待子线程释放锁,然后继续执行。也就是说,如果子线程中有耗时较长的so加载行为,就会阻塞主线程的so加载。目前so的加载时间还是一个盲点,无法针对性的处理这个问题。思路和前面的SharedPreferences问题是一样的:使用MtAjx拦截System.loadLibrary()方法并输出其耗时,最终得到如下数据(部分):从数据上看,somesoloading确实消耗大量时间。针对这个问题,目前初步的解决方案是:尽量减少异步so加载对主线程so加载的影响。尝试Hack高通平台的BoostFramework,让它延迟加载或者提前加载,列出可执行方案如下:网上最后的解决方案:在异步任务中延迟执行so,加载高通平台的BoostFramewok,加速在合理的时间启动此系统黑客攻击,必要的灾难恢复仍有待完成。这里和前面的解决方案一样,BoostFramework加载的时机由在线开关决定。采用最终方案上线后,得到一些线上数据:相关ANR问题数量下降50%,低端机启动速度提升13%。问题2和3都经历了类似的过程:分析:包括对问题的根源、原理、发生场景的完整分析和建模:指的是问题的直观“数据”。比如SharedPreferences的读写频率分析,so加载中的加载时间数据分析,以及在线慢功能和ANR数据变化等。预估:列出现有解决方案的优缺点,以及可能产生的影响、容灾措施等。验证:将最终结果与预估时的结果进行对比,是否影响线上相关问题的核心指标。4.BinderProxy问题处理网上的Binder问题有多种堆栈表示,但最终调用点是android.os.BinderProxy.transactNative()方法。经过分析查阅资料,得出初步结论。出现此类问题时,系统或应用基本上处于以下状态:然后分析数据,评估是否存在不合理的场景。与前面的例子不同的是:Binder问题最终完全发生在系统层面,无法被MtAjx拦截。因此,另一种方式:通过NativeHook,可以拦截所有的Binder调用,从而获取Binder调用的运行时数据。这里选择BinderProxy.transact()作为拦截点:依然是使用自动化测试来模拟在线用户的使用,输出日志。经过分析,得到如下数据(部分):数据反映了一系列问题:频繁获取自身包信息:如versionName、versionCode频繁获取设备信息:如IMEI、IMSI、网卡地址,频繁网络类型检测、网络状态检测、频繁访问本地目录、频繁权限检测,这些方法的调整使用频率都存在异常(有的甚至超过View绘图)。总结后输出最终的修改结论和建议,供内部或第三方修改:以上问题都得到了处理,一定程度上减少了整体ANR问题的发生。因此,大多数ANR问题都不是单体问题。通常,降低整个应用程序的负载也可以降低整体ANR问题的发生率。反思经过整整一个月的问题处理,通过复习得到以下一系列思考。1.风险控制当风险存在时,通常有以下方法:当本地容灾策略异常时,自适应断线上报部分功能。这种方式在效率和用户体验方面是最好的。但是,本地化意味着通常需要各种规则和条件来约束。所以在大多数情况下这不是最好的选择。在线开关控制观察到异常(人为、报警)次数增加后,通过在线开关关闭相应功能。目前最常用的方法适用于大多数场景。热修复补丁观察到线上异常后,可以通过下载热修复补丁来避免问题的扩大。但是,某些设备和通道可能会发生故障。不得已发布紧急版本:紧急版本从修复效率和用户体验上来说都是一个糟糕的选择。2.保持数据准确完整基础数据建设更有利于发现问题其中一些在当时完全是盲点,无法评估用户流失有多少与此相关。单维数据只能反映部分事实,能够相互印证的多维数据才更可信。例如,在性能问题上:ANR数据、慢功能数据,甚至启动时间都可以相互印证。3、不要忽视“蚂蚁窝”中小问题的积累,爆发后再填坑,这样往往会消耗更多的精力。之前在公司内部的AOP处理中一直使用AspectJ,前期遇到的问题一直都是通过添加各种规则来避免的。后期出问题的时候,会比开发一个AOP方案消耗更多的人力。没有外力约束,稳定的系统会逐渐增加熵,从而陷入混沌状态,代码也是一样。低质量的代码不仅会降低研发效率,还会增加各种稳定性风险。平时不被注意的细节,会在数百万或数千万用户面前成倍增加。比如主线程的一次IO操作:很多人认为“几Kb的数据不过是大事”,但事实真的如此吗?未来探索无论过去、现在还是未来,ANR问题的处理始终是一项具有挑战性的任务。依靠过去的经验可以避免一些共性问题,但面向未来,我们需要探索更多定位和解决问题的方法。1、自研性能监控平台,对Bug进行发现、预警、跟进,将整个开发交付流程串联起来。这将是未来性能优化工作的绝佳工具。2、采样上报的详细ANR日志通过对比Bugly和XCrash上报的ANR日志,可以看出XCrash上报的信息更适合开发者定位问题。后期我们使用XCrash接入作为采样上报方案,补充ANR数据,为定位问题提供更多有价值的信息。3.业务异常关联数据分析通过将核心业务的运行路径与线上异常数据关联起来,我们可以找到一些通常被忽略的问题定位方法。我们将图像处理业务与ANR或Crash的发生时间节点关联起来,定位ANR或Crash是否与某个业务组件相关联。结论ANR问题不是一个单一的问题。大多数ANR问题只是结果,原因千奇百怪,文章只是冰山一角。后续团队也需要不断解决新的问题,对处理过的问题不断总结、归纳、规范,以促进其使用,更好地管理ANR问题。注:文中描述的MtAjx尚未公开。如果需要,可以使用AspectJ代替。文章中的大量经验来自于之前的投稿;水平有限,难免有错误——如有疑问,欢迎随时与我交流:i@zhangyanwei.com专家介绍作者张彦伟,美图秀秀安卓专家。2019年加入美图,目前负责美图秀秀的Android优化。美图秀秀是厦门美图科技有限公司于2008年10月8日开发推出的一款免费图像处理软件,全球累计用户超过10亿,长期保持图像应用排行榜领先地位时间。