App Crash对于用户来讲是一种最糟糕的体验,它会导致流程中断、app口碑变差、app卸载、用户流失、订单流失等。
Crash治理方法
常见Crash的处理方式:
• 在开发提交代码过程中,让同事之间codeReview检测潜在问题。
• 在功能上线之前,周期性地跑monkey,提前发现crash问题。
• 根据Crash统计平台的堆栈,用户日志,操作路径定位和解决。
• 寻找共性,机型、品牌、系统版本、所在页面、用户操作等辅助解决问题。
• 复现场景,能够复现通常就很容易解决,可以线下复现或者云真机复现。
• 对crash率较高的模块进行业务梳理,排查,重构等。
• 与第三方sdk沟通升级解决问题,修改SDK的使用方式。
Crash治理实践
1.预防Crash
对于稳定性来说,如果App已经到了线上才发现异常,那其实已经造成了损失,所以,对于稳定性的优化,其重点在于预防。Gerrit 是一个免费、开放源代码的代码审查软件,使用网页界面。我们公司大多数项目都使用Gerrit进行CodeReview,从项目开发阶段就会审查各种潜在的crash或者逻辑上的问题,严格按照代码+1、+2之后才能提交入库。
2.长效保持需要科学流程
应用稳定性的建设过程是一个细活,所以很容易出现这个版本优化好了,但是在接下来的版本中如果我们不管它,它就会发生持续恶化的情况,因此,我们必须从项目研发的每一个流程入手,建立科学完善的相关规范,才能保证长效的优化效果。在每个版本都要追踪崩溃监测平台,将对应的崩溃分发给对应业务的开发人员处理。
Crash率评价
性能指标 | 优秀值 | 及格值 | 极差值 | 行业参考值 |
---|---|---|---|---|
崩溃率(%) | <=0.1 | 0.6 | >=1 | 0.5 |
那么,我们App的Crash率降低多少才能算是一个正常水平或优秀的水平呢?
Java与Native的总崩溃率必须在千分之二以下。
Crash率万分位为优秀:需要注意90%的Crash都是比较容易解决的,但是要解决最后的10%需要付出巨大的努力。
Crash关键问题
如果应用发生了Crash,我们应该尽可能还原Crash现场。因此,我们需要全面地采集应用发生Crash时的相关信息,如下所示:
- 堆栈、设备、OS版本、进程、线程名、Logcat
- 前后台、使用时长、App版本、小版本、渠道
- CPU架构、内存信息、线程数、资源包信息、用户行为日志
Android中常见异常类型
在Android应用开发中,常见的异常崩溃情况有很多,以下是一些常见的异常类型:
- 空指针异常(NullPointerException):
当试图调用一个对象的方法或访问其属性,而该对象为空时,就会抛出空指针异常。
- 数组越界异常(ArrayIndexOutOfBoundsException):
当尝试访问数组中不存在的索引位置时,会引发数组越界异常。
- 类型转换异常(ClassCastException):
当试图将一个对象转换为不兼容的类型时,会抛出类型转换异常。
并发修改异常(ConcurrentModificationException):
在使用迭代器遍历集合的同时,对集合进行了结构性修改(增加、删除元素)时,会抛出并发修改异常。
- 资源未找到异常(NotFoundException):
在尝试获取某个资源(如布局文件、字符串等)而该资源不存在时,会抛出资源未找到异常。
- SQLite异常:
包括但不限于SQLiteConstraintException(违反约束异常)、SQLiteException(SQLite数据库相关的通用异常)等。
- 网络异常(IOException):
在网络请求、文件操作等场景中,由于网络不可用或文件不可访问等原因,可能会抛出IOException。
- 线程异常(ThreadException):
在多线程编程中,可能会遇到一些线程异常,如IllegalThreadStateException、InterruptedException等。
视图相关异常:
包括但不限于InflateException(布局文件解析异常)、IllegalStateException(视图状态异常)等。
- 运行时异常(RuntimeException):
运行时异常是一类不需要显式捕获的异常,通常是由程序员的错误导致的,如算术异常(ArithmeticException)、索引越界异常(IndexOutOfBoundsException)等。
- SecurityException:
当应用尝试执行受系统安全性保护的敏感操作时,可能会引发SecurityException。
那么常见是如上崩溃类型,定位到了崩溃,要解决这些崩溃,就需要自己的实力基础了。空指针异常做判空容错处理,数组越界做判断处理,类型转换异常先做类型判断处理,并发修改用线程安全的集合如CopyOnWriteArrayList,IOException,SqliteException相关操作做try catch等等处理。
异常发生的崩溃是谁处理的
当发生了Exception或throwable时,如果我们手动在代码中做了try catch处理,那么异常会由catch捕获机制来处理异常。那如果没有自己try catch呢?那就会由Thread类的dispatchUncaughtException方法处理,在当前线程处理未捕获异常,打印崩溃日志,然后由系统类调用了killProcess退出程序。
public final void dispatchUncaughtException(Throwable e) {
// BEGIN Android-added: uncaughtExceptionPreHandler for use by platform.
Thread.UncaughtExceptionHandler initialUeh =
Thread.getUncaughtExceptionPreHandler();
if (initialUeh != null) {
try {
initialUeh.uncaughtException(this, e);
} catch (RuntimeException | Error ignored) {
// Throwables thrown by the initial handler are ignored
}
}
// END Android-added: uncaughtExceptionPreHandler for use by platform.
getUncaughtExceptionHandler().uncaughtException(this, e);
}
Crash监控方案
Java中的Thread定义了一个接口: UncaughtExceptionHandler ;用于处理未捕获的异常导致线程的终止(注意:catch了的是捕获不到的),当我们的应用crash的时候,就会走 UncaughtExceptionHandler.uncaughtException ,在该方法中可以获取到异常的信息,我们通过 Thread.setDefaultUncaughtExceptionHandler 该方法来设置线程的默认异常处理器,我们可以将异常信息保存到本地或者是上传到服务器,方便我们快速的定位问题。
class MyExceptionHandler implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) {
File file = dealException(thread, throwable);
//上传服务器
...
}
/*** 导出异常信息到SD卡 ** @param e */
private File dealException(Thread thread, Throwable throwable) {
String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
File crashFolder = new File(mContext.getExternalCacheDir().getAbsoluteFile(), CrashMonitor.DEFAULT_JAVA_CRASH_FOLDER_NAME);
if (!crashFolder.exists()) {
crashFolder.mkdirs();
}
File crashFile = new File(crashFolder, time + FILE_NAME_SUFFIX);
try {
// 往文件中写入数据
PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(crashFile)));
pw.println(time);
pw.println(thread);
pw.println(getPhoneInfo());
throwable.printStackTrace(pw); //将异常信息堆栈写入文件
// 写入crash堆栈
pw.close();
} catch (IOException ex) {
ex.printStackTrace();
}
return crashFile;
}
private String getPhoneInfo() {
PackageManager pm = mContext.getPackageManager();
PackageInfo pi = null;
StringBuilder sb = new StringBuilder();
try {
pi = pm.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES);
// App版本
sb.append("App Version: ");
sb.append(pi.versionName);
sb.append("_");
sb.append(pi.versionCode + "\n");
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
// Android版本号
sb.append("OS Version: ");
sb.append(Build.VERSION.RELEASE);
sb.append("_");
sb.append(Build.VERSION.SDK_INT + "\n");
// 手机制造商
sb.append("Vendor: ");
sb.append(Build.MANUFACTURER + "\n");
// 手机型号
sb.append("Model: ");
sb.append(Build.MODEL + "\n");
// CPU架构
sb.append("CPU: ");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
sb.append(Arrays.toString(Build.SUPPORTED_ABIS));
} else {
sb.append(Build.CPU_ABI);
}
return sb.toString();
}
}
然后在application中设置自定义的UncaughtExceptionHandler。
Thread.setDefaultUncaughtExceptionHandler(MyExceptionHandler())
ANR问题分析
ANR发生的原因总结和解决办法
本质就是消息机制的message处理超过了5s消息还无法进行处理,因为前面有消息一直卡住,就会报 AppNotResponding。
最终由ActivityManager报出:
/**
* Method for the app to tell system that it's wedged and would like to trigger an ANR.
*
* @param reason The description of that what happened
*/
public void appNotResponding(@NonNull final String reason) {
try {
getService().appNotResponding(reason);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
1.在主线程中,进行了触屏点击滑动等操作,在5秒之内对该事件没有响应,就会导致ANR(例如,按键按下,屏幕触摸)
2.BroadcastReceiver在10秒内没有执行完毕
3.service是20秒
根本原因是在主线程进行了耗时操作,导致后面的消息无法处理,比如:
1.耗时的网络访问
2.大量的数据读写
3.数据库操作
4.在主线程中调用thread的join()方法、sleep()方法、wait()方法
5.其他线程持有锁,导致主线程等待超时
6.子线程终止或崩溃导致主线程一直等待
解决的主要办法就是开启子线程去处理这些耗时操作。修改主线程去等待子线程的锁。
ANR排查流程
ANR定位解决实战:https://www.jianshu.com/p/f59bc75782f3
使用FileObserver监控data/anr目录,当文件状态发生改变、创建或删除时,说明当前发生了anr事件。
创建类继承FileObserver,监控data/anr文件:
class AnrFileObserver extends FileObserver {
@Override
public void onEvent(int event, @Nullable String path) {
switch (event) {
case FileObserver.ACCESS:
break;
case FileObserver.CREATE:
break;
case FileObserver.MODIFY:
break;
}
}
}
线上ANR监控:
接入 bugly,根据App版本,手机型号,发生时间,次数,产生位置进行位置问题定位并解决。
ANR异常我们可分为线上监测和线下监测两个方向
线上监测主要是利用FileObserver进行ANR目录文件变化监听,以ANR-WatchDog进行补充。
FileObserver在使用过程中应注意高版本程序不可用以及预防死锁出现。
线下监测主要是在报错之后利用ADB命令将错误的日志导出并找到错误的类进行分析。
参考:
https://www.jianshu.com/p/8abce3ff4687
如何降低App的Crash率?
崩溃是由于消息机制里面的Looer.loop方法像外抛的异常,然后系统的CrashHandler类,最未捕获异常会打印堆栈日志,然后做killprocess。我们可以定义自己的UncatchExceptionHandler,然后hook掉系统的这个类,自定义处理日志,然后做重启looper,或者做退出当前activity,做程序崩溃处理。
将未捕获异常自行处理:
public class CrashHandler implements Thread.UncaughtExceptionHandler {
private static CrashHandler instance;
private Thread.UncaughtExceptionHandler defaultExceptionHandler;
private CrashHandler() {
// 获取默认的异常处理器
defaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
// 设置当前实例为默认的异常处理器
Thread.setDefaultUncaughtExceptionHandler(this);
}
public static synchronized CrashHandler getInstance() {
if (instance == null) {
instance = new CrashHandler();
}
return instance;
}
@Override
public void uncaughtException(Thread t, Throwable e) {
// 在这里处理崩溃逻辑,例如记录日志、上传崩溃信息等
// 默认的异常处理器处理崩溃
// if (defaultExceptionHandler != null) {
// defaultExceptionHandler.uncaughtException(t, e);
// }
}
}
OOM
Out of Memory(OOM)错误通常发生在应用程序尝试分配比其可用内存更多的内存时。
以下是一些可能导致OOM崩溃的常见场景的示例:
1.Bitmap操作:
// 创建过大的Bitmap
Bitmap largeBitmap = Bitmap.createBitmap(5000, 5000, Bitmap.Config.ARGB_8888);
解决方案:
对于大图,可以使用inSampleSize进行缩放。
使用BitmapFactory.Options.inJustDecodeBounds来获取图片大小信息,然后再根据需要进行合适的缩放。
2.大量资源未释放:
// 大量创建未释放的资源
for (int i = 0; i < 1000; i++) {
Bitmap bitmap = Bitmap.createBitmap(1000, 1000, Bitmap.Config.ARGB_8888);
// 使用bitmap...
}
解决方案:
及时释放不再需要的资源,例如使用bitmap.recycle()。
3.频繁创建对象:
// 在循环中频繁创建对象
for (int i = 0; i < 1000000; i++) {
Object obj = new Object();
// 使用obj...
}
解决方案:
避免在循环中频繁创建对象,考虑对象池的使用,获取使用内存缓存。
4.大量数据加载:
// 一次性加载大量数据到内存
List<SomeData> dataList = loadDataFromNetwork();
解决方案:使用分页加载或流式处理数据,而不是一次性加载所有数据。
- 内存泄漏:
解决方案:
避免在长时间生命周期对象中持有引用,使用弱引用或及时释放引用。
一些预防OOM的措施包括:
1.优化内存使用: 确保及时释放不再需要的对象,避免创建过多的临时对象。
2.使用内存分析工具: 使用工具如Android Profiler或MAT(Memory Analyzer Tool)来检测内存泄漏和分析内存使用情况。
3.合理使用Bitmap: 对于大图,使用合适的缩放技术,及时回收不再需要的Bitmap对象。
4.分页加载: 对于大量数据,采用分页加载或流式处理,而不是一次性加载所有数据。
5.使用内存缓存: 对于频繁使用的资源,考虑使用内存缓存来避免重复加载。
如果发生了异常情况,怎么快速止损?
1.功能开关
首先,需要让App具备一些高级的能力,我们对于任何要上线的新功能,要加上一个功能的开关,通过配置中心下发的开关呢,来决定是否要显示新功能的入口。如果有异常情况,可以紧急关闭新功能的入口,那就可以让这个App处于可控的状态了。
2.关闭灰度发布
如果问题更严重,范围更广,可以立即关闭灰度发布版本。
3.动态修复:热修复、资源包更新
目前热修复的方案其实已经比较成熟了,我们完全可以低成本地在我们的项目中添加热修复的能力,当然,如果有些功能是由RN或WeeX来实现就更好了,那就可以通过更新资源包的方式来实现动态更新。