美团外卖Android Crash治理之路

面试中经常问到的是Android的性能优化以及Crash处理。 今天咱们来学习一下啊美团App的Crash处理。更多参考《Android性能优化:手把手带你全面实现内存优化html

原为地址: https://blog.csdn.net/MeituanTech/article/details/80701773java

Crash率是衡量一个App好坏的重要指标之一,若是你忽略了它的存在,它就会愈演愈烈,最后形成大量用户的流失,进而给公司带来没法估量的损失。本文讲述美团外卖Android客户端团队在将App的Crash率从千分之三作到万分之二过程当中所作的大量实践工做,抛砖引玉,但愿可以为其余团队提供一些经验和启发。android

面临的挑战和成果

面对用户使用频率高,外卖业务增加快,Android碎片化严重这些问题,美团外卖Android App如何持续的下降Crash率,是一项极具挑战的事情。经过团队的全力全策,美团外卖Android App的平均Crash率从千分之三降到了万分之二,最优值万一左右(Crash率统计方式:Crash次数/DAU)。git

美团外卖自2013年建立以来,业务就以指数级的速度发展。美团外卖承载的业务,从单一的餐饮业务,发展到餐饮、超市、生鲜、果蔬、药品、鲜花、蛋糕、跑腿等十多个大品类业务。目前美团外卖日完成订单量已突破2000万,成为美团点评最重要的业务之一。美团外卖客户端所承载的业务模块愈来愈多,产品复杂度愈来愈高,团队开发人员日益增长,这些都给App下降Crash率带来了巨大的挑战。程序员

Crash的治理实践

对于Crash的治理,咱们尽可能遵照如下三点原则:github

  • 由点到面。一个Crash发生了,咱们不能只针对这个Crash的去解决,而要去考虑这一类Crash怎么去解决和预防。只有这样才能使得这一类Crash真正被解决。
  • 异常不能随便吃掉。随意的使用try-catch,只会增长业务的分支和隐蔽真正的问题,要了解Crash的本质缘由,根据本质缘由去解决。catch的分支,更要根据业务场景去兜底,保证后续的流程正常。
  • 预防胜于治理。当Crash发生的时候,损失已经形成了,咱们再怎么治理也只是减小损失。尽量的提早预防Crash的发生,能够将Crash消灭在萌芽阶段。

常规的Crash治理

常规Crash发生的缘由主要是因为开发人员编写代码不当心致使的。解决这类Crash须要由点到面,根据Crash引起的缘由和业务自己,统一集中解决。常见的Crash类型包括:空节点、角标越界、类型转换异常、实体对象没有序列化、数字转换异常、Activity或Service找不到等。这类Crash是App中最为常见的Crash,也是最容易反复出现的。在获取Crash堆栈信息后,解决这类Crash通常比较简单,更多考虑的应该是如何避免。下面介绍两个咱们治理的量比较大的Crash。面试

NullPointerException

NullPointerException是咱们遇到最频繁的,形成这种Crash通常有两种状况:正则表达式

  • 对象自己没有进行初始化就进行操做。
  • 对象已经初始化过,可是被回收或者手动置为null,而后对其进行操做。

针对第一种状况致使的缘由有不少,多是开发人员的失误、API返回数据解析异常、进程被杀死后静态变量没初始化致使,咱们能够作的有:算法

  • 对可能为空的对象作判空处理。
  • 养成使用@NonNull和@Nullable注解的习惯。
  • 尽可能不使用静态变量,万不得已使用SharedPreferences来存储。
  • 考虑使用Kotlin语言。

针对第二种状况大部分是因为Activity/Fragment销毁或被移除后,在Message、Runnable、网络等回调中执行了一些代码致使的,咱们能够作的有:编程

  • Message、Runnable回调时,判断Activity/Fragment是否销毁或被移除;加try-catch保护;Activity/Fragment销毁时移除全部已发送的Runnable。
  • 封装LifecycleMessage/Runnable基础组件,并自定义Lint检查,提示使用封装好的基础组件。
  • 在BaseActivity、BaseFragment的onDestory()里把当前Activity所发的全部请求取消掉。
IndexOutOfBoundsException

这类Crash常见于对ListView的操做和多线程下对容器的操做。

针对ListView中形成的IndexOutOfBoundsException,常常是由于外部也持有了Adapter里数据的引用(如在Adapter的构造函数里直接赋值),这时若是外部引用对数据更改了,但没有及时调用notifyDataSetChanged(),则有可能形成Crash,对此咱们封装了一个BaseAdapter,数据统一由Adapter本身维护通知, 同时也极大的避免了The content of the adapter has changed but ListView did not receive a notification,这两类Crash目前获得了统一的解决。

另外,不少容器是线程不安全的,因此若是在多线程下对其操做就容易引起IndexOutOfBoundsException。经常使用的如JDK里的ArrayList和Android里的SparseArray、ArrayMap,同时也要注意有一些类的内部实现也是用的线程不安全的容器,如Bundle里用的就是ArrayMap。

系统级Crash治理

众所周知,Android的机型众多,碎片化严重,各个硬件厂商可能会定制本身的ROM,更改系统方法,致使特定机型的崩溃。发现这类Crash,主要靠云测平台配合自动化测试,以及线上监控,这种状况下的Crash堆栈信息很难直接定位问题。下面是常见的解决思路:

  1. 尝试找到形成Crash的可疑代码,看是否有特异的API或者调用方式不当致使的,尝试修改代码逻辑来进行规避。
  2. 经过Hook来解决,Hook分为Java Hook和Native Hook。Java Hook主要靠反射或者动态代理来更改相应API的行为,须要尝试找到能够Hook的点,通常Hook的点多为静态变量,同时须要注意Android不一样版本的API,类名、方法名和成员变量名均可能不同,因此要作好兼容工做;Native Hook原理上是用更改后方法把旧方法在内存地址上进行替换,须要考虑到Dalvik和ART的差别;相对来讲Native Hook的兼容性更差一点,因此用Native Hook的时候须要配合降级策略。
  3. 若是经过前两种方式都没法解决的话,咱们只能尝试反编译ROM,寻找解决的办法。

咱们举一个定制系统ROM致使Crash的例子,根据Crash平台统计数据发现该Crash只发生在vivo V3Max这类机型上,Crash堆栈以下:

java.lang.RuntimeException: An error occured while executing doInBackground()
  at android.os.AsyncTask$3.done(AsyncTask.java:304)
  at java.util.concurrent.FutureTask.finishCompletion(FutureTask.java:355)
  at java.util.concurrent.FutureTask.setException(FutureTask.java:222)
  at java.util.concurrent.FutureTask.run(FutureTask.java:242)
  at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:231)
  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112)
  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587)
  at java.lang.Thread.run(Thread.java:818)
Caused by: java.lang.NullPointerException: Attempt to invoke interface method 'int java.util.List.size()' on a null object reference
  at android.widget.AbsListView$UpdateBottomFlagTask.isSuperFloatViewServiceRunning(AbsListView.java:7689)
  at android.widget.AbsListView$UpdateBottomFlagTask.doInBackground(AbsListView.java:7665)
  at android.os.AsyncTask$2.call(AsyncTask.java:292)
  at java.util.concurrent.FutureTask.run(FutureTask.java:237)
  ... 4 more
复制代码

咱们发现原生系统上对应系统版本的AbsListView里并无UpdateBottomFlagTask类,所以能够判定是vivo该版本定制的ROM修改了系统的实现。咱们在定位这个Crash的可疑点无果后决定经过Hook的方式解决,经过源码发现AsyncTask$SerialExecutor是静态变量,是一个很好的Hook的点,经过反射添加try-catch解决。由于修改的是final对象因此须要先反射修改accessFlags,须要注意ART和Dalvik下对应的Class不一样,代码以下:

public static void setFinalStatic(Field field, Object newValue) throws Exception {
        field.setAccessible(true);
        Field artField = Field.class.getDeclaredField("artField");
        artField.setAccessible(true);
        Object artFieldValue = artField.get(field);
        Field accessFlagsFiled = artFieldValue.getClass().getDeclaredField("accessFlags");
        accessFlagsFiled.setAccessible(true);
        accessFlagsFiled.setInt(artFieldValue, field.getModifiers() & ~Modifier.FINAL);
        field.set(null, newValue);
    }

复制代码
private void initVivoV3MaxCrashHander() {
    if (!isVivoV3()) {
        return;
    }
    try {
        setFinalStatic(AsyncTask.class.getDeclaredField("SERIAL_EXECUTOR"), new SafeSerialExecutor());
        Field defaultfield = AsyncTask.class.getDeclaredField("sDefaultExecutor");
        defaultfield.setAccessible(true);
        defaultfield.set(null, AsyncTask.SERIAL_EXECUTOR);
    } catch (Exception e) {
        L.e(e);
    }
}
复制代码

美团外卖App用上述方法解决了对应的Crash,可是美团App里的外卖频道由于平台的限制没法经过这种方式,因而咱们尝试反编译ROM。  Android ROM编译时会将framework、app、bin等目录打入system.img中,system.img是Android系统中用来存放系统文件的镜像 (image),文件格式通常为yaffs2或ext。但Android 5.0开始支持dm-verity后,system.img再也不提供,而是提供了三个文件system.new.dat,system.patch.dat,system.transfer.list,所以咱们首先须要经过上述的三个文件获得system.img。但咱们将vivo ROM解压后发现厂商将system.new.dat进行了分片,以下图所示:

image

通过对system.transfer.list中的信息和system.new.dat 1 2 3 … 文件大小对比研究,发现一些共同点,system.transfer.list中的每个block数*4KB 与对应的分片文件的大小大体相同,故大胆猜想,vivo ROM对system.patch.dat分片也只是单纯的按block前后顺序进行了分片处理。因此咱们只须要在转化img前将这些分片文件合成一个system.patch.dat文件就能够了。最后根据system.img的文件系统格式进行解包,拿到framework目录,其中有framework.jar和boot.oat等文件,由于Android4.4以后引入了ART虚拟机,会预先把system/framework中的一些jar包转换为oat格式,因此咱们还须要将对应的oat文件经过ota2dex将其解包得到dex文件,以后经过dex2jarjd-gui查看源码。

OOM

OOM是OutOfMemoryError的简称,在常见的Crash疑难排行榜上,OOM绝对能够名列前茅而且经久不衰。由于它发生时的Crash堆栈信息每每不是致使问题的根本缘由,而只是压死骆驼的最后一根稻草。  致使OOM的缘由大部分以下:

  • 内存泄漏,大量无用对象没有被及时回收致使后续申请内存失败。
  • 大内存对象过多,最多见的大对象就是Bitmap,几个大图同时加载很容易触发OOM。

内存泄漏  内存泄漏指系统未能及时释放已经再也不使用的内存对象,通常是由错误的程序代码逻辑引发的。在Android平台上,最多见也是最严重的内存泄漏就是Activity对象泄漏。Activity承载了App的整个界面功能,Activity的泄漏同时也意味着它持有的大量资源对象都没法被回收,极其容易形成OOM。  常见的可能会形成Activity泄漏的缘由有:

  • 匿名内部类实现Handler处理消息,可能致使隐式持有的Activity对象没法回收。
  • Activity和Context对象被混淆和滥用,在许多只须要Application Context而不须要使用Activity对象的地方使用了Activity对象,好比注册各种Receiver、计算屏幕密度等等。
  • View对象处理不当,使用Activity的LayoutInflater建立的View自身持有的Context对象其实就是Activity,这点常常被忽略,在本身实现View重用等场景下也会致使Activity泄漏。

对于Activity泄漏,目前已经有了一个很是好用的检测工具:LeakCanary,它能够自动检测到全部Activity的泄漏状况,而且在发生泄漏时给出十分友好的界面提示,同时为了防止开发人员的疏漏,咱们也会将其上报到服务器,统一检查解决。另外咱们能够在debug下使用StrictMode来检查Activity的泄露、Closeable对象没有被关闭等问题。

大对象  在Android平台上,咱们分析任一应用的内存信息,几乎均可以得出一样的结论:占用内存最多的对象大都是Bitmap对象。随着手机屏幕尺寸愈来愈大,屏幕分辨率也愈来愈高,1080p和更高的2k屏已经占了大半份额,为了达到更好的视觉效果,咱们每每须要使用大量高清图片,同时也为OOM埋下了祸根。  对于图片内存优化,咱们有几个经常使用的思路:

  • 尽可能使用成熟的图片库,好比Glide,图片库会提供不少通用方面的保障,减小没必要要的人为失误。
  • 根据实际须要,也就是View尺寸来加载图片,能够在分辨率较低的机型上尽量少地占用内存。除了经常使用的BitmapFactory.Options#inSampleSize和Glide提供的BitmapRequestBuilder#override以外,咱们的图片CDN服务器也支持图片的实时缩放,能够在服务端进行图片缩放处理,从而减轻客户端的内存压力。  分析App内存的详细状况是解决问题的第一步,咱们须要对App运行时到底占用了多少内存、哪些类型的对象有多少个有大体了解,并根据实际状况作出预测,这样才能在分析时作到有的放矢。Android Studio也提供了很是好用的Memory Profiler堆转储分配跟踪器功能能够帮咱们迅速定位问题。

AOP加强辅助

AOP是面向切面编程的简称,在Android的Gradle插件1.5.0中新增了Transform API以后,编译时修改字节码来实现AOP也由于有了官方支持而变得很是方便。  在一些特定状况下,能够经过AOP的方式自动处理未捕获的异常:

  • 抛异常的方法很是明确,调用方式比较固定。
  • 异常处理方式比较统一。
  • 和业务逻辑无关,即自动处理异常后不会影响正常的业务逻辑。典型的例子有读取Intent Extras参数、读取SharedPreferences、解析颜色字符串值和显示隐藏Window等等。

这类问题的解决原理大体相同,咱们以Intent Extras为例详细介绍一下。读取Intent Extras的问题在于咱们很是经常使用的方法 Intent#getStringExtra 在代码逻辑出错或者恶意攻击的状况下可能会抛出ClassNotFoundException异常,而咱们平时在写代码时又不太可能给全部调用都加上try-catch语句,因而一个更安全的Intent工具类应运而生,理论上只要全部人都使用这个工具类来访问Intent Extras参数就能够防止此类型的Crash。可是面对庞大的旧代码仓库和诸多的业务部门,修改现有代码须要极大成本,还有更多的外部依赖SDK基本不可能使用咱们本身的工具类,此时就须要AOP大展身手了。  咱们专门制做了一个Gradle插件,只须要配置一下参数就能够将某个特定方法的调用替换成另外一个方法:

WaimaiBytecodeManipulator {
     replacements(
         "android/content/Intent.getIntExtra(Ljava/lang/String;I)I=com/waimai/IntentUtil.getInt(Landroid/content/Intent;Ljava/lang/String;I)I",
         "android/content/Intent.getStringExtra(Ljava/lang/String;)Ljava/lang/String;=com/waimai/IntentUtil.getString(Landroid/content/Intent;Ljava/lang/String;)Ljava/lang/String;",
         "android/content/Intent.getBooleanExtra(Ljava/lang/String;Z)Z=com/waimai/IntentUtil.getBoolean(Landroid/content/Intent;Ljava/lang/String;Z)Z",
         ...)
    }
}
复制代码

上面的配置就能够将App代码(包括第三方库)里全部的Intent.getXXXExtra调用替换成IntentUtil类中的安全版实现。固然,并非全部的异常都只须要catch住就万事大吉,若是真的有逻辑错误确定须要在开发和测试阶段及时暴露出来,因此在IntentUtil中会对App的运行环境作判断,Debug下会将异常直接抛出,开发同窗能够根据Crash堆栈分析问题,Release环境下则在捕获到异常时返回对应的默认值而后将异常上报到服务器。

依赖库的问题

Android App常常会依赖不少AAR, 每一个AAR可能有多个版本,打包时Gradle会根据规则肯定使用的最终版本号(默认选择最高版本或者强制指定的版本),而其余版本的AAR将被丢弃。若是互相依赖的AAR中有不兼容的版本,存在的问题在打包时是不能发现的,只有在相关代码执行时才会出现,会形成NoClassDefFoundError、NoSuchFieldError、NoSuchMethodError等异常。如图所示,order和store两个业务库都依赖了platform.aar,一个是1.0版本,一个是2.0版本,默认最终打进APK的只有platform 2.0版本,这时若是order库里用到的platform库里的某个类或者方法在2.0版本中被删除了,运行时就可能发生异常,虽然SDK在升级时会尽可能作到向下兼容,但不少时候尤为是第三方SDK是无法获得保证的,在美团外卖Android App v6.0版本时由于这个缘由致使热修复功能丧失,所以为了提早发现问题,咱们接入了依赖检查插件Defensor。

image

Defensor在编译时经过DexTask获取到全部的输入文件(也就是被编译过的class文件),而后检查每一个文件里引用的类、字段、方法等是否存在。

除此以外咱们写了一个Gradle插件SVD(strict version dependencies)来对那些重要的SDK的版本进行统一管理。插件会在编译时检查Gradle最终使用的SDK版本是否和配置中的一致,若是不一致插件会终止编译并报错,并同时会打印出发生冲突的SDK的全部依赖关系。

Crash的预防实践

单纯的靠约定或规范去减小Crash的发生是不现实的。约定和规范受限于组织架构和具体执行的我的,很容易被忽略,只有靠工程架构和工具才能保证Crash的预防长久的执行下去。

工程架构对Crash率的影响

在治理Crash的实践中,咱们每每忽略了工程架构对Crash率的影响。Crash的发生大部分缘由是源于程序员的不合理的代码,而程序员工做中最直接的接触的就是工程架构。对于一个边界模糊,层级混乱的架构,程序员是更加容易写出引发Crash的代码。在这样的架构里面,即便程序员意识到致使某种写法存在问题,想要去改善这样不合理的代码,也是很是困难的。相反,一个层级清晰,边界明确的架构,是可以大大减小Crash发生的几率,治理和预防Crash也是相对更容易。这里咱们能够举几个咱们实践过的例子阐述。

业务模块的划分  原来咱们的Crash基本上都是由个别同窗关注解决的,团队里的每一个同窗都会提交可能引发Crash的代码,若是负责Crash的同窗由于某些事情,暂时没有关注App的Crash率,那么形成Crash的同窗也不会知道他的代码引发了Crash。

对于这个问题,咱们的作法是App的业务模块化。业务模块化后,每一个业务都有都有惟一包名和对应的负责人。当某个模块发生了Crash,能够根据包名提交问题给这个模块的负责人,让他第一时间进行处理。业务模块化自己也是工程架构优先须要考虑的事情之一。

页面跳转路由统一处理页面跳转  对外卖App而言,使用过程当中最多的就是页面间的跳转,而页面间跳转常常会形成ActivityNotFoundException,例如咱们配了一个scheme,但对方的scheme路径已经发生了变化;又例如,咱们调用手机上相册的功能,而相册应用已被用户本身禁用或移除了。解决这一类Crash,其实也很简单,只须要在startActivity增长ActivityNotFoundException异常捕获便可。但一个App里,启动Activity的地方,几乎是随处可见,没法预测哪一处会形成ActivityNotFoundException。  咱们的作法是将页面的跳转,都经过咱们封装的scheme路由去分发。这样的好处是,经过scheme路由,在工程架构上全部业务都是解耦,模块间不须要相互依赖就能够实现页面的跳转和基本类型参数的传递;同时,因为全部的页面跳转都会走scheme路由,咱们只须要在scheme路由里一处加上ActivityNotFoundException异常捕获便可解决这种类型的Crash。路由设计示意图以下:

image

网络层统一处理API脏数据  客户端的很大一部分的Crash是由于API返回的脏数据。好比当API返回空值、空数组或返回不是约定类型的数据,App收到这些数据,就极有可能发生空指针、数组越界和类型转换错误等Crash。并且这样的脏数据,特别容易引发线上大面积的崩溃。  最先咱们的工程的网络层用法是:页面监听网络成功和失败的回调,网络成功后,将JSON数据传递给页面,页面解析Model,初始化View,如图所示。这样的问题就是,网络虽然请求成功了,可是JSON解析Model这个过程可能存在问题,例如没有返回数据或者返回了类型不对的数据,而这个脏数据致使问题会出如今UI层,直接反应给用户。

image

根据上图,咱们能够看到因为网络层只承担了请求网络的职责,没有承担数据解析的职责,数据解析的职责交给了页面去处理。这样使得咱们一旦发现脏数据致使的Crash,就只能在网络请求的回调里面增长各类判断去兼容脏数据。咱们有几百个页面,补漏彻底补不过来。经过几个版本的重构,咱们从新划分了网络层的职责,如图所示:

image

从图上能够看出,重构后的网络层负责请求网络和数据解析,若是存在脏数据的话,在网络层就会发现问题,不会影响到UI层,返回给UI层的都是校验成功的数据。这样改造后,咱们发现这类的Crash率有了极大的改善。

大图监控

上面讲到大对象是致使OOM的主要缘由之一,而Bitmap是App里最多见的大对象类型,所以对占用内存过大的Bitmap对象的监控就颇有必要了。  咱们用AOP方式Hook了三种常见图片库的加载图片回调方法,同时监控图片库加载图片时的两个维度:  1. 加载图片使用的URL。外卖App中除静态资源外,全部图片都要求发布到专用的图片CDN服务器上,加载图片时使用正则表达式匹配URL,除了限定CDN域名以外还要求全部图片加载时都要添加对应的动态缩放参数。  2. 最终加载出的图片结果(也就是Bitmap对象)。咱们知道Bitmap对象所占内存和其分辨率大小成正比,而通常状况下在ImageView上设置超过自身尺寸的图片是没有意义的,因此咱们要求显示在ImageView中的Bitmap分辨率不容许超过View自身的尺寸(为了下降误报率也能够设定一个报警阈值)。

开发过程当中,在App里检测到不合规的图片时会当即高亮出错的ImageView所在的位置并弹出对话框提示ImageView所在的Activity、XPath和加载图片使用的URL等信息,以下图,辅助开发同窗定位并解决问题。在Release环境下能够将报警信息上报到服务器,实时观察数据,有问题及时处理。 

image

Lint检查

咱们发现线上的不少Crash其实能够在开发过程当中经过Lint检查来避免。Lint是Google提供的Android静态代码检查工具,能够扫描并发现代码中潜在的问题,提醒开发人员及早修正,提升代码质量。

可是Android原生提供的Lint规则(如是否使用了高版本API)远远不够,缺乏一些咱们认为有必要的检测,也不能检查代码规范。所以咱们开始开发自定义Lint,目前咱们经过自定义Lint规则已经实现了Crash预防、Bug预防、提高性能/安全和代码规范检查这些功能。如检查实现了Serializable接口的类,其成员变量(包括从父类继承的)所声明的类型都要实现Serializable接口,能够有效的避免NotSerializableException;强制使用封装好的工具类如ColorUtil、WindowUtil等能够有效的避免由于参数不正确产生的IllegalArgumentException和由于Activity已经finish致使的BadTokenException。

Lint检查能够在多个阶段执行,包括在本地手动检查、编码实时检查、编译时检查、commit时检查,以及在CI系统中提Pull Request时检查、打包时检查等,以下图所示。更详细的内容可参考《美团外卖Android Lint代码检查实践》

image

资源重复检查

在以前的文章《美团外卖Android平台化架构演进实践》中讲述了咱们的平台化演进过程,在这个过程当中你们很大的一部分工做是下沉,可是下沉不彻底就会致使一些类和资源的重复,类由于有包名的限制不会出现问题。可是一些资源文件如layout、drawable等若是同名则下层会被上层覆盖,这时layout里view的id发生了变化就可能致使空指针的问题。为了不这种问题,咱们写了一个Gradle插件经过hook MergeResource这个Task,拿到全部library和主库的资源文件,若是检查到重复则会中断编译过程,输出重复的资源名及对应的library name,同时避免有些资源由于样式等缘由确实须要覆盖,所以咱们设置了白名单。同时在这个过程当中咱们也拿到了全部的的图片资源,能够顺手作图片大小的本地监控,以下图所示: 

image

Crash的监控&止损的实践

监控

在通过前面提到的各类检查和测试以后,应用便开始发布了。咱们创建了以下图的监控流程,来保证异常发生时可以及时获得反馈并处理。首先是灰度监控,灰度阶段是增量Crash最容易暴露的阶段,若是这个阶段没有很好的把握住,会使得增量变存量,从而致使Crash率上升。若是条件容许的话,能够在灰度期间制定一些灰度策略去提升这个阶段Crash的暴露。例如分渠道灰度、分城市灰度、分业务场景灰度、新装用户的灰度等等,尽可能覆盖全部的分支。灰度结束以后便开始全量,在全量的过程当中咱们还须要一些平常Crash监控和Crash率的异常报警来防止突发状况的发生,例如由于后台上线或者运营配置错误致使的线上Crash。除此以外还须要一些其余的监控,例如,以前提到的大图监控,来避免由于大图致使的OOM。具体的输出形式主要有邮件通知、IM通知、报表。

image

止损

尽管咱们在前面作了那么多,可是Crash仍是没法避免的,例如,在灰度阶段由于量级不够,有些Crash没有被暴露出来;又或者某些功能客户端比后台更早上线,而这些功能在灰度阶段没有被覆盖到;这些状况下,若是出现问题就须要考虑如何止损了。

问题发生时首先须要评估重要性,若是问题不是很严重并且修复成本较高能够考虑在下个版本再修复,相反若是问题比较严重,对用户体验或下单有影响时就必需要修复。修复时首先考虑业务降级,主要看该部分异常的业务是否有兜底或者A/B策略,这样是最稳妥也是最有效的方式。若是业务不能降级就须要考虑热修复了,目前美团外卖Android App接入的热修复框架是自研的Robust,能够修复90%以上的场景,热修成功率也达到了99%以上。若是问题发生在热修复没法覆盖的场景,就只能强制用户升级。强制升级由于覆盖周期长,同时影响用户的体验,只在万不得已的状况下才会使用。

展望

Crash的自我修复

咱们在作新技术选型时除了要考虑是否能知足业务需求、是否比现有技术更优秀和团队学习成本等因素以外,兼容性和稳定性也很是重要。但面对国内非富多彩的Android系统环境,在体量百万级以上的的App中几乎不可能实现毫无瑕疵的技术方案和组件,因此通常状况下若是某个技术实现方案能够达到0.01‰如下的崩溃率,而其余方案也没有更好的表现,咱们就认为它是能够接受的。可是哪怕仅仅十万分之一的崩溃率,也表明还有用户受到影响,而咱们认为Crash对用户来讲是最糟糕的体验,尤为是涉及到交易的场景,因此咱们必须本着每一单都很重要的原则,尽最大努力保证用户顺利执行流程。

实际状况中有一些技术方案在兼容性和稳定性上作了必定妥协的场景,每每是由于考虑到性能或扩展性等方面的优点。这种状况下咱们其实能够再多作一些,进一步提升App的可用性。就像不少操做系统都有“兼容模式”或者“安全模式”,不少自动化机械机器都配套有手动操做模式同样,App里也能够实现备用的降级方案,而后设置特定条件的触发策略,从而达到自动修复Crash的目的。

举例来说,Android 3.0中引入了硬件加速机制,虽然能够提升绘制帧率而且下降CPU占用率,可是在某些机型上仍是会有绘制错乱甚至Crash的状况,这时咱们就能够在App中记录硬件加速相关的Crash问题或者使用检测代码主动检测硬件加速功能是否正常工做,而后主动选择是否开启硬件加速,这样既可让绝大部分用户享受硬件加速带来的优点,也能够保障硬件加速功能不完善的机型不受影响。  还有一些相似的能够作自动降级的场景,好比:

  • 部分使用JNI实现的模块,在SO加载失败或者运行时发生异常则能够降级为Java版实现。
  • RenderScript实现的图片模糊效果,也能够在失败后降级为普通的Java版高斯模糊算法。
  • 在使用Retrofit网络库时发现OkHttp3或者HttpURLConnection网络通道失败率高,能够主动切换到另外一种通道。

这类问题都须要根据具体状况具体分析,若是能够找到准确的断定条件和稳定的修复方案,就可让App稳定性再上一个台阶。

特定Crash类型日志自动回捞

外卖业务发展迅速,即便咱们在开发时使用各类工具、措施来避免Crash的发生,但Crash仍是不可避免。线上某些怪异的Crash发生后,咱们除了分析Crash堆栈信息以外,还可使用离线日志回捞、下发动态日志等工具来还原Crash发生时的场景,帮助开发同窗定位问题,可是这两种方式都有它们各自的问题。离线日志顾名思义,它的内容都是预先记录好的,有时候可能会漏掉一些关键信息,由于在代码中加日志通常只是在业务关键点,在大量的普通方法中不可能都加上日志。动态日志(Holmes)存在的问题是每次下发只能针对已知UUID的一个用户的一台设备,对于大量线上Crash的状况这种操做并不合适,由于咱们并不能知道哪一个发生Crash的用户还会再次复现此次操做,下发配置充满了不肯定性。

咱们能够改造Holmes使其支持批量甚至全量下发动态日志,记录的日志等到发生特定类型的Crash时才上报,这样一来能够减小日志服务器压力,同时也能够极大提升定位问题的效率,由于咱们能够肯定上报日志的设备最后都真正发生了该类型Crash,再来分析日志就能够作到事半功倍。

总结

业务的快速发展,每每不可能给团队充足的时间去治理Crash,而Crash又是App最重要的指标之一。团队须要由一个个Crash个例,去探究每个Crash发生的最本质缘由,找到最合理解决这类Crash的方案,创建解决这一类Crash的长效机制,而不能饮鸩止渴。只有这样,随着版本的不断迭代,咱们才能在Crash治理之路上离目标愈来愈近。

参考资料

  1. Crash率从2.2%降至0.2%,这个团队是怎么作到的?
  2. Android运行时ART加载OAT文件的过程分析
  3. Android动态日志系统Holmes
  4. Android Hook技术防范漫谈
  5. 美团外卖Android Lint代码检查实践

面试必备之UI刷新大解密

Flutter基础-环境搭建及demo运行

个人Android重构之旅:框架篇

MVC,MVP 和 MVVM 模式如何选择?

微信公众号:终端研发部

技术
相关文章
相关标签/搜索