乐趣区

关于android:解读-美团外卖Android-Crash治理之路

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

退出移动版