Crash率是掂量一个App好坏的重要指标之一,如果你疏忽了它的存在,它就会愈演愈烈,最初造成大量用户的散失,进而给公司带来无法估量的损失。本文讲述美团外卖Android客户端团队在将App的Crash率从千分之三做到万分之二过程中所做的大量实际工作,抛砖引玉,心愿可能为其余团队提供一些教训和启发。

面临的挑战和成绩

面对用户应用频率高,外卖业务增长快,Android碎片化重大这些问题,美团外卖Android App如何继续的升高Crash率,是一项极具挑战的事件。通过团队的全力全策,美团外卖Android App的均匀Crash率从千分之三降到了万分之二,最优值万一左右(Crash率统计形式:Crash次数/DAU)。

美团外卖自2013年创立以来,业务就以指数级的速度倒退。美团外卖承载的业务,从繁多的餐饮业务,倒退到餐饮、超市、生鲜、果蔬、药品、鲜花、蛋糕、跑腿等十多个大品类业务。目前美团外卖日实现订单量已冲破2000万,成为美团点评最重要的业务之一。美团外卖客户端所承载的业务模块越来越多,产品复杂度越来越高,团队开发人员日益减少,这些都给App升高Crash率带来了微小的挑战。

Crash的治理实际

对于Crash的治理,咱们尽量恪守以下三点准则:

  • 由点到面。一个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进行了分片,如下图所示:

通过对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文件,之后通过dex2jar和jd-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。

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。路由设计示意图如下:

网络层对立解决API脏数据 客户端的很大一部分的Crash是因为API返回的脏数据。比方当API返回空值、空数组或返回不是约定类型的数据,App收到这些数据,就极有可能产生空指针、数组越界和类型转换谬误等Crash。而且这样的脏数据,特地容易引起线上大面积的解体。 最早咱们的工程的网络层用法是:页面监听网络胜利和失败的回调,网络胜利后,将JSON数据传递给页面,页面解析Model,初始化View,如图所示。这样的问题就是,网络尽管申请胜利了,然而JSON解析Model这个过程可能存在问题,例如没有返回数据或者返回了类型不对的数据,而这个脏数据导致问题会呈现在UI层,间接反馈给用户。

依据上图,咱们能够看到因为网络层只承当了申请网络的职责,没有承当数据解析的职责,数据解析的职责交给了页面去解决。这样使得咱们一旦发现脏数据导致的Crash,就只能在网络申请的回调外面减少各种判断去兼容脏数据。咱们有几百个页面,补漏齐全补不过去。通过几个版本的重构,咱们从新划分了网络层的职责,如图所示:

从图上能够看出,重构后的网络层负责申请网络和数据解析,如果存在脏数据的话,在网络层就会发现问题,不会影响到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环境下能够将报警信息上报到服务器,实时察看数据,有问题及时处理。

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代码查看实际》。

资源重复查看

在之前的文章《美团外卖Android平台化架构演进实际》中讲述了咱们的平台化演进过程,在这个过程中大家很大的一部分工作是下沉,然而下沉不齐全就会导致一些类和资源的反复,类因为有包名的限度不会呈现问题。然而一些资源文件如layout、drawable等如果同名则上层会被下层笼罩,这时layout里view的id产生了变动就可能导致空指针的问题。为了防止这种问题,咱们写了一个Gradle插件通过hook MergeResource这个Task,拿到所有library和主库的资源文件,如果查看到反复则会中断编译过程,输入反复的资源名及对应的library name,同时防止有些资源因为款式等起因的确须要笼罩,因而咱们设置了白名单。同时在这个过程中咱们也拿到了所有的的图片资源,能够棘手做图片大小的本地监控,如下图所示:

Crash的监控&止损的实际

监控

在通过后面提到的各种检查和测试之后,利用便开始公布了。咱们建设了如下图的监控流程,来保障异样产生时可能及时失去反馈并解决。首先是灰度监控,灰度阶段是增量Crash最容易裸露的阶段,如果这个阶段没有很好的把握住,会使得增量变存量,从而导致Crash率回升。如果条件容许的话,能够在灰度期间制订一些灰度策略去进步这个阶段Crash的裸露。例如分渠道灰度、分城市灰度、分业务场景灰度、新装用户的灰度等等,尽量笼罩所有的分支。灰度完结之后便开始全量,在全量的过程中咱们还须要一些日常Crash监控和Crash率的异样报警来避免突发状况的产生,例如因为后盾上线或者经营配置谬误导致的线上Crash。除此之外还须要一些其余的监控,例如,之前提到的大图监控,来防止因为大图导致的OOM。具体的输入模式次要有邮件告诉、IM告诉、报表。

止损

只管咱们在后面做了那么多,然而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治理之路上离指标越来越近。

本文转自 https://juejin.cn/post/6844903620920492046,如有侵权,请分割删除。

相干视频举荐:

【2021最新版】Android studio装置教程+Android(安卓)零基础教程视频(适宜Android 0根底,Android初学入门)含音视频_哔哩哔哩_bilibili

Android进阶零碎学习——高级UI卡顿性能优化_哔哩哔哩_bilibili