美团外卖Android 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. d o n e ( A s y n c T a s k . j a v a : 304 ) a t j a v a . u t i l . c o n c u r r e n t . F u t u r e T a s k . f i n i s h C o m p l e t i o n ( F u t u r e T a s k . j a v a : 355 ) a t j a v a . u t i l . c o n c u r r e n t . F u t u r e T a s k . s e t E x c e p t i o n ( F u t u r e T a s k . j a v a : 222 ) a t j a v a . u t i l . c o n c u r r e n t . F u t u r e T a s k . r u n ( F u t u r e T a s k . j a v a : 242 ) a t a n d r o i d . o s . A s y n c T a s k 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 3.done(AsyncTask.java:304)atjava.util.concurrent.FutureTask.finishCompletion(FutureTask.java:355)atjava.util.concurrent.FutureTask.setException(FutureTask.java:222)atjava.util.concurrent.FutureTask.run(FutureTask.java:242)atandroid.os.AsyncTaskSerialExecutor 1. r u n ( A s y n c T a s k . j a v a : 231 ) a t j a v a . u t i l . c o n c u r r e n t . T h r e a d P o o l E x e c u t o r . r u n W o r k e r ( T h r e a d P o o l E x e c u t o r . j a v a : 1112 ) a t j a v a . u t i l . c o n c u r r e n t . T h r e a d P o o l E x e c u t o r 1.run(AsyncTask.java:231) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112) at java.util.concurrent.ThreadPoolExecutor 1.run(AsyncTask.java:231)atjava.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112)atjava.util.concurrent.ThreadPoolExecutorWorker.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 U p d a t e B o t t o m F l a g T a s k . i s S u p e r F l o a t V i e w S e r v i c e R u n n i n g ( A b s L i s t V i e w . j a v a : 7689 ) a t a n d r o i d . w i d g e t . A b s L i s t V i e w UpdateBottomFlagTask.isSuperFloatViewServiceRunning(AbsListView.java:7689) at android.widget.AbsListView UpdateBottomFlagTask.isSuperFloatViewServiceRunning(AbsListView.java:7689)atandroid.widget.AbsListViewUpdateBottomFlagTask.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文件,之后通过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”,
…)
}
}

分享读者

作者2013年java转到Android开发,在小厂待过,也去过华为,OPPO等大厂待过,18年四月份进了阿里一直到现在。

被人面试过,也面试过很多人。深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长,而且极易碰到天花板技术停滞不前!

我们整理了一份阿里P7级别的Android架构师全套学习资料,特别适合有3-5年以上经验的小伙伴深入学习提升。

主要包括腾讯,以及字节跳动,阿里,华为,小米,等一线互联网公司主流架构技术。如果你有需要,尽管拿走好了。

腾讯T3架构师学习专题资料

如果你觉得自己学习效率低,缺乏正确的指导,可以点击加入资源丰富,学习氛围浓厚的技术圈一起学习交流吧

群内有许多来自一线的技术大牛,也有在小厂或外包公司奋斗的码农,我们致力打造一个平等,高质量的Android交流圈子,不一定能短期就让每个人的技术突飞猛进,但从长远来说,眼光,格局,长远发展的方向才是最重要的。

35岁中年危机大多是因为被短期的利益牵着走,过早压榨掉了价值,如果能一开始就树立一个正确的长远的职业规划。35岁后的你只会比周围的人更值钱。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
img

t5u9x4-1711025585953)]

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
[外链图片转存中…(img-PLQfJvvA-1711025585953)]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值