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

吼啦啦Android稳定性治理实践!

时间:2023-03-14 16:52:55 科技观察

作者|Kelvin,货拉拉高级客户端工程师,目前负责货拉拉AppAndroid的性能优化和APM相关工作。AppCrash是用户最糟糕的体验。会导致进程中断、应用口碑差、应用卸载、用户流失、订单流失等。相关数据显示,当AndroidApp的崩溃率超过0.4%时,活跃用户数明显下降。据统计,我们在2021年初的崩溃率为5%。大量的研发时间花在了定位和解决用户的反馈和投诉上。崩溃治理刻不容缓。通过去年的大量实践治理,我们应用的崩溃率已经降低并稳定在0.02%。在此做一些总结和分享,希望能为其他团队提供经验和启发。根据行业标准《2020移动应用性能管理白皮书 | 基调听云》,我们对Crash率的治理目标定为0.03%。治理前的整体情况App的崩溃率高达5+%。随着产品的不断迭代,复杂度不断增加,这给降低崩溃率带来了巨大的挑战。我们必须处理新的Crash,同时针对遗留的Crahs进行清理。Crash部分分为两部分。Native部分崩溃占比高达72%。这部分的Crash主要来自于Flutter、第三方地图和第三方SDK,解决难度较大。Java部分的很多crash并没有及时解决,还遗留了大量的历史欠账,大部分都有通用的解决方案。Crash治理方法常见的Crash处理方法是根据Crash统计平台的栈、用户日志、操作路径定位及解决方案,寻找共性、型号、品牌、系统版本、所在页面、用户操作等辅助solvingproblemrecurrentscenarios,可以复现的,通常很容易解决。可以通过离线复现或云真机复现的方式,对crash率高的模块进行业务梳理、排查、重构等。与第三方SDK沟通升级解决问题,修改SDK使用方式。崩溃治理实践由于内部统计平台建设时间相对较晚,我们只能获取崩溃率的近期变化,可以看到崩溃率已经明显收敛。1、代码重构,应对需求频繁变更,提升研发效率。货拉拉首页和订单确认页面是使用Flutter和小程序实现的。这部分是用户使用率最高的页面,代码量庞大复杂。大量崩溃发生在在线环境中。大多数崩溃是零星的,无法在离线环境中重现。从栈的情况很难定位到问题的根源。修改可能涉及多个终端,每个终端都需要进行回归验证。.综合分析后,我们决定梳理逻辑,让最重要的部分代码回归本源。重构上线后,崩溃率明显下降,从原来的5%下降到0.5%,稳定性也大大提高。2、第三方SDK中的Crash处理和Native的crash处理,将我们的crash率从千分位降低到0.05%。前期我们轻松解决了java部分的crash。但是第三方sdk和Native的crash居高不下。很长一段时间,我们都无法通过提交工单和咨询对方来解决。对于一些crash,我们升级了sdk,发现crash的数量反而增加了,需要降级回老版本。对于一些crash,堆栈信息与这个sdk无关。这些部分的crash通常通过以下两种方式解决:检查Api调用中的问题(调用时机、调用顺序、传入参数、调用线程或进程等),沟通解决,(如没有一对一的开发对接接下来尝试提交工单咨询),将Crash栈发送给第三方SDK处理。我们Crash量最高的两组就是例子:2.1第三方VMP线程监控内存情况,关闭App,导致Crash统计平台出现大量Native。CrashSIGSEGV(SEGV_MAPERR)、SIGABRT、SIGSEGV(SEGV_ACCERR)等。堆栈通常在系统级so内部(libgui.so、libGLES_mali.so、libc.so等),我们无法知道其来源来自捕获堆栈的问题。我们知道的信息是,vivo手机Android10的死机率非常高,Android11的死机率非常高。为此,我们孤注一掷进行了几次尝试:错误堆栈中有硬件加速相关的sos,我们也曾尝试过关闭整个硬件加速功能。进行了大量的内存泄漏管理和线程管理来降低手机的帧率。多次联系手机厂商无果,在一次用户日志对比中发现同一个日志存在一些bug。/proc/pid/mem和/proc/self/task/pid/mem文件被监控。如果文件被操作,应用程序将退出。exit方法可能有问题,在mapsdk中捕获nativecrash时可能会导致二次crash。这部分bug处理后,我们的崩溃率降低到0.1%左右。native部分的crash栈没有和这部分crash混在一起,栈信息更清晰,更容易定位问题。2.2Map的使用Map是货拉拉应用中必不可少的组件,下单过程中都会用到它,贯穿于整个主流程。地图部分的崩溃大部分是原生的,有些是特定品牌机型特有的。我们将crash反馈给SDK端,开发者根据堆栈定位问题。经过多方分析,导致崩溃的原因一部分是SDK导致的,另一部分是我们调用不合理造成的。SDK内部原因通过升级解决,crash呈现收敛趋势。调用不合理的部分主要是销毁时机不正确造成的。我们检查并修复了每个地图使用场景,经过多次版本修复,整体崩溃率降至0.05%。3、OOM治理OutOfMemoryError是指内存溢出,也是一种比较难解决的crash。通常,报告的堆栈信息、用户日志等没有太大的参考价值,往往内存已经增加到其他地方内存溢出的临界点,而报告的位置很可能没有内存问题,但是内存溢出的主要原因有:内存泄漏、大对象引用、内存抖动、线程使用不合理等。3.1内存泄漏主要通过LeakCanary检测。通常,Activity的泄露会比较严重,因为它承载了整个页面的控件和数据。如果Activity不能被回收,那么这部分对象就不能被回收。Activity泄露的常见原因Handler、Thread等匿名内部类隐式持有外部类,导致注册的监听器没有及时注销:Activity对象,还有一些页面添加后没有移除,造成大量内存泄漏。网络请求、异步任务:由于前期网络部分封装不好,网络请求没有绑定到页面的生命周期。在弱网络或用户快速关闭页面时会发生内存泄漏。Activity在Context可以使用Application的时候使用:比如Toast,第三方SDK3.2线程管理当线程超过系统设置的上限时,也会造成OOM,报错:pthread_create(1040KBstack)失败:内存不足。通常的处理方式如下:统一App内部线程池,对于随机使用的异步任务,使用线程池或者RxJava去检查App中的TimerApp端的线程池,避免随机使用线程并分析合理的可替换线程或线程池以进行检测和替换处理。.这里的处理,我们先使用Glide进行图片加载,然后将App中原生加载的图片替换为Glide。另外,使用MAT工具对大对象进行筛选排序,对头部大对象进行优化:将原本在同一个SharePreference中的数据拆分成多个,使用后回收。一个类保存的数据量非常大,不经常使用,可以放在数据库或文件中。3.4内存抖动其实对于这部分的优化,我们只是针对检测到的、比较严重的、不合理的内存增长进行了优化。通过AndroidStudio的Profiler发现内存呈锯齿状增长,或者GC频率极高,分析解决了一些不合理的对象分配。这里有两个例子:在自定义View的onDraw方法中创建一个对象b.设置监控布局变化,然后改变控件的大小和位置。当这个视图的显示状态发生变化时,又会重新开始监听,一直在这里执行,导致内存疯狂增长,GC频率增加。在一些测试机器上,GC会在十秒内执行一次。View.getViewTreeObserver().addOnGlobalLayoutListener(()->{//更改视图大小、位置等int[]view1Location=newint[2];view1.getLocationOnScreen(view1Location);int[]view2Location=newint[2];view2.getLocationOnScreen(view2Location);});解决方案主要有两种:1、对象复用:对于onDraw中的使用场景,可以将对象作为成员变量。其他情况考虑是否可以建立对象池2.增加条件:只有满足一定条件才允许创建对象的逻辑。比如第二个例子,由于业务原因无法移除监听器。我们添加了一些条件,创建逻辑只会在少数情况下执行。4.经常crash的问题通常根据堆栈和用户手机的相关情况很容易定位到问题,但是不能只修改引起问题的那行代码,更不能执行trycatchat将要。你需要找到问题的本质,也许这个问题还会导致其他一系列的问题。例如:点击事件的一行出现空指针,发现数据为空。数据是从上一页的网络接口传过来的,接口数据是空的,因为传入的deviceid是空的。所以这个时候,我们仅仅在点击事件中去判断是否为空肯定是不够的。我们需要找出deviceid为空的原因,否则会导致整个链路崩溃或者业务不正常。4.1空指针NullPointerException虽然报告的空指针数量并不多,但根据我们的统计,它是出现次数最多的错误。通常是在对象本身没有初始化或者对象被回收的时候使用对象造成的:代码逻辑分支导致,走的是对应的调用逻辑,而不是初始化位置。服务器接口返回数据异常,数据库查询数据异常。一些postDelay或者异步的回调在回调的时候也会被回收,这可能会导致空指针的静态变量被回收。通常,基于空判断过程,不能在代码中直接添加空指针。应该找到真正要规避对象为空的原因,常见的解决办法是:做好null的判断或者使用kotlin语言使用注解@NonNull和@Nullable。有异步任务,适时停止异步任务或者去掉监听回调(比如销毁页面停止网络Request,去掉页面中的postDelay任务)静态变量管理好,可以在本地持久化,当Adapter数据发生变化时,在多线程的情况下使用不安全的集合。解决方案:String,SpannableString对于索引操作,首先要确定字符串的长度bug产生的原因对于这类bug,很难找出触发的原因。我们需要在栈、系统版本、型号中找到关键词进行分析。解决方案通常是避免调用或挂钩。比如下面这个bug,关键字“autofill”,Android10版本,对应Android10的自动填充功能。错误名称和堆栈信息android.os.RemoteExceptionRemotestacktrace:atcom.android.internal.util.Preconditions.checkCollectionElementsNotNull简单分析一下上面的错误日志涉及到跨进程,分成三个会更清楚部分。App进程中ActivityThread的mH收到Message处理handleRequestAssistContextExtras,然后调用ActivityTaskManagerService的aidl方法reportAssistContextExtras进入ActivityTaskManagerService.reportAssistContextExtras。在构造新的FillRequest时,调用Preconditions.checkCollectionElementsNotNull抛出异常,异常信息返回给我们的App进程。Preconditions类异常抛出的位置调用验证。查看源码,mH中只有一次handleRequestAssistContextExtras的调用,发送这条消息的地方也只有一处。通过hook判断用户在使用autofill功能时会调用这个。解决方法设置关闭EditText控件的自动填充功能Edittext自动填充导致的Bug其他bug很多崩溃android.app.RemoteServiceExceptionContext.startForegroundService()didn'tcallService.startForeground()Android8.0启动服务适配问题,app内部和SDK中均有crash,检查app中所有服务是否进行适配处理。android.content.ActivityNotFoundExceptionNoActivityfoundtohandleIntent这是我们app中回调Android跳转页面的js方法,因为找不到页面导致崩溃。另外,我们发现在业务中,页面的一些操作以及JavaScriptInterface回调发送给Android端的Eventbus事件导致了很多莫名其妙的小bug。因此,我们尽可能将JavaScriptInterface中的回调丢回主线程执行,减少因线程操作UI和多线程执行不安全的问题。com.google.gson.stream.MalformedJsonException后端数据或本地数据库问题导致数据解析异常。统一使用Json解析工具来捕获和处理异常。支持持续集成、灰度发布、构建统计、配置分发、强升级、普升级弱升级、内测分发等。灰度发布支持设置设备ID、数量、百分比、版本号、品牌型号、网络系统、时间、系统版本、商家城市、定位城市等非常灵活。灰度系统的主要作用:第一时间获取用户反馈,完善产品功能,暴露存在的崩溃问题和产品设计问题,及时修正,降低影响。业务需要时,会增加灰度的范围设置,例如:城市、品牌型号、版本号等。对于高风险的需求,我们可能会专门安排一个灰度版本发布,将影响范围降到最低。应用配置系统通过货拉拉的打包发布系统发布配置。同时该配置支持灰度发布,可以像App发布灰度一样进行发布。对于一些业务,可以通过配置进行分布式控制功能参数和功能。切换,进行业务降级,修改AB实验等。比如我们线上环境部分H5页面使用了离线包,其他部门切换了网关,导致离线包出现跨域问题,页面无法访问访问过。我们通过配置下发及时关闭了离线包功能,业务降级为线上网页。代码质量相关措施建立CodeReview体系,保证代码可读性和风格一致性:方便其他成员理解代码,增强可维护性发现设计问题:检查代码中设计不合理,避免后续维护和迭代困难代码的健壮性,是否存在潜在的安全问题,性能风险等相互学习:阅读别人的代码也可以学习经验查找错误:代码中的错误是很难避免的,这些错误可能就在眼前另一个人不难发现,添加的模块Owner模块中的代码主要是Owner自己开发实现的。Owner会很熟悉,可以保证代码的一致性。所有者可以更好地审查其他开发人员的代码。业务会谈和思维导图让团队成员熟悉业务模块和流程。促进团队内部沟通,学会扫码:不符合团队规范的代码不能提交,保证代码规范。保证代码一致性Hotfix当应用已经大规模发布,Crash会导致大量用户无法使用,闪退,主进程无法执行等,我们会考虑hotfix。如果没有热修复技术,那么我们只能重新发布App版本,甚至要求强制升级,这会造成非常差的用户体验。日志系统货拉拉的日志系统分为两大类:实时日志:用户在使用APP时,通过上传策略将日志上报至平台,支持日志级别过滤、TAG过滤、搜索等常用功能。离线日志:根据用户ID等,推送用户举报,或用户主动举报。通过日志系统,我们可以根据日志级别、日志时间等了解当前用户的使用场景和操作路径,方便场景复现,是解决Crash的利器之一。综上所述,在过去的一年里,我们对APP关键模块进行了重构,对第三方SDK进行了管理,并进行了多次专项治理。崩溃率已从5+%降低到0.02%。稳定性的提升,让我们的用户反馈问题和用户投诉明显。Androidapp相关的投诉数量从去年4月的12起减少到今年4月的2起,让我们留住了更多的用户,保证了用户能够顺利完成订单。我们总结了一些治理原则:举一反三,由点到面,从一个问题中引入或归类为一类问题,进行专项治理。找到问题的本质,找到问题的根源,而不是盲目地尝试捕捉和绕过判断。及时解决。崩溃问题应该在开发和测试过程中尽快及时解决,而不是在灰度和全量之后。以预防为主,将Crash杜绝在萌芽状态,采取代码审查、模块划分、技术讲座等措施。控制准入,严格控制模块、sdk、新技术的引入,降低引入风险Crash治理不是一劳永逸的,需要长期治理,建立长效机制。另一方面,对于新技术和跨平台技术,在业务允许的情况下,应该大胆尝试,利用已有的预防措施做好预防,而不是直接放弃这些技术的应用。