Android连续闪退优化

android手机中,应该大都遇到过,app使用中,突然出现崩溃,然后接着再次打开,
依然闪退,多次打开之后也是闪退的问题。
如果你是用户,是不是针对这种app,直接卸载掉呢?
作为开发,有什么方法避免这种情况发生么


背景

应用启动时会执行一系列的初始化操作,比如:

  • 数据库更新
  • 初始化一系列第三方sdk
  • 读取热修复补丁包
  • 读取 React Native 更新包
  • 读取本地缓存文件
  • 读取数据库
  • 读取服务端数据
  • 等等
    如果其中有环节出现异常,就会导致应用启动时发生连续闪退。
    如果闪退发生在热修复之前,那应用将得不到修复。

解决方案

UncaughtExceptionHandler 类介绍

Thread.UncaughtExceptionHandler 接口,提供了监听java线程异常的功能。

1
2
3
4
5
6
7
8
9
10
11
public interface UncaughtExceptionHandler {
/**
* Method invoked when the given thread terminates due to the
* given uncaught exception.
* <p>Any exception thrown by this method will be ignored by the
* Java Virtual Machine.
* @param t the thread
* @param e the exception
*/
void uncaughtException(Thread t, Throwable e);
}

以下代码是简单的一种保存错误日志的写法,可用于开发中,测试出现bug,开发直接通过保存的异常文件来查找该异常。

  • 以下是网络上开源的CrashHandler用于保存错误日志的一个处理示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    public class CrashHandler implements UncaughtExceptionHandler {

    public static final String TAG = "CrashHandler";

    // 系统默认的UncaughtException处理类
    private Thread.UncaughtExceptionHandler mDefaultHandler;
    // CrashHandler实例
    private static CrashHandler INSTANCE = new CrashHandler();
    // 程序的Context对象
    private Context mContext;
    // 用来存储设备信息和异常信息
    private Map<String, String> infos = new HashMap<String, String>();

    // 用于格式化日期,作为日志文件名的一部分
    private DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss SSS");

    private CrashHandler() {
    }

    /** 获取CrashHandler实例 ,单例模式 */
    public static CrashHandler getInstance() {
    return INSTANCE;
    }

    /**
    * 初始化
    *
    * @param context
    */
    public void init(Context context) {
    mContext = context;
    // 获取系统默认的UncaughtException处理器
    mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
    // 设置该CrashHandler为程序的默认处理器
    Thread.setDefaultUncaughtExceptionHandler(this);
    }

    /**
    * 当UncaughtException发生时会转入该函数来处理
    */
    @Override
    public void uncaughtException(Thread thread, Throwable ex) {
    if (!handleException(ex) && mDefaultHandler != null) {
    // 如果用户没有处理则让系统默认的异常处理器来处理
    mDefaultHandler.uncaughtException(thread, ex);
    } else {
    try {
    Thread.sleep(3000);
    } catch (InterruptedException e) {
    Log.e(TAG, "error : ", e);
    }
    // 退出程序
    android.os.Process.killProcess(android.os.Process.myPid());
    System.exit(1);
    }
    }

    /**
    * 自定义错误处理,收集错误信息 发送错误报告等操作均在此完成.
    *
    * @param ex
    * @return true:如果处理了该异常信息;否则返回false.
    */
    private boolean handleException(Throwable ex) {
    if (ex == null) {
    return false;
    }
    // 使用Toast来显示异常信息
    new Thread() {
    @Override
    public void run() {
    Looper.prepare();
    Toast.makeText(mContext, "很抱歉,程序出现异常即将退出.", Toast.LENGTH_LONG).show();
    Looper.loop();
    }
    }.start();
    // 收集设备参数信息
    collectDeviceInfo(mContext);
    // 保存日志文件
    saveCrashInfo2File(ex);
    return true;
    }

    /**
    * 收集设备参数信息
    *
    * @param ctx
    */
    public void collectDeviceInfo(Context ctx) {
    try {
    PackageManager pm = ctx.getPackageManager();
    PackageInfo pi = pm.getPackageInfo(ctx.getPackageName(), PackageManager.GET_ACTIVITIES);
    if (pi != null) {
    String versionName = pi.versionName == null ? "null" : pi.versionName;
    String versionCode = pi.versionCode + "";
    infos.put("versionName", versionName);
    infos.put("versionCode", versionCode);
    }
    } catch (NameNotFoundException e) {
    Log.e(TAG, "an error occured when collect package info", e);
    }
    Field[] fields = Build.class.getDeclaredFields();
    for (Field field : fields) {
    try {
    field.setAccessible(true);
    infos.put(field.getName(), field.get(null).toString());
    Log.d(TAG, field.getName() + " : " + field.get(null));
    } catch (Exception e) {
    Log.e(TAG, "an error occured when collect crash info", e);
    }
    }
    }

    /**
    * 保存错误信息到文件中
    *
    * @param ex
    * @return 返回文件名称,便于将文件传送到服务器
    */
    private String saveCrashInfo2File(Throwable ex) {

    StringBuffer sb = new StringBuffer();
    for (Map.Entry<String, String> entry : infos.entrySet()) {
    String key = entry.getKey();
    String value = entry.getValue();
    sb.append(key + "=" + value + "\n");
    }

    Writer writer = new StringWriter();
    PrintWriter printWriter = new PrintWriter(writer);
    ex.printStackTrace(printWriter);
    Throwable cause = ex.getCause();
    while (cause != null) {
    cause.printStackTrace(printWriter);
    cause = cause.getCause();
    }
    printWriter.close();
    String result = writer.toString();
    sb.append(result);
    try {
    String time = formatter.format(new Date());
    String fileName = time + ".txt";
    if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
    String path = AppSettings.CrashLogPath;
    File dir = new File(path);
    if (!dir.exists()) {
    dir.mkdirs();
    }
    FileOutputStream fos = new FileOutputStream(path + fileName);
    fos.write(sb.toString().getBytes("UTF-8"));
    fos.close();
    }
    return fileName;
    } catch (Exception e) {
    Log.e(TAG, "an error occured while writing file...", e);
    }
    return null;
    }
    }
    在app的applacation中初始化这个类即可
    1
    2
    CrashHandler crashHandler = CrashHandler.getInstance();
    crashHandler.init(this);

闪退解决思路

UncaughtExceptionHandler 类的出现,我们可以获取到异常的次数。
具体怎么计数,我建议是:

  • 每次打开记录当前时间,出现异常的时间和记录的时间小于1分钟,可算一次打开闪退。
  • 正常退出时,计数归零。

分级保护

  • 当app连续2次以上闪退,开启第一级保护机制

    • 可以清理一些,非重要问题。即数据来源于网络的数据,可再下载的数据。
  • 当app连续3次以上闪退,开启第二级保护机制

    • 可尝试清理所有保存在本地的数据,即恢复到才安装的状态。(最后提示一个弹框,让用户作选择,温馨提示会导致重新登陆等操作)
    • 这个时候,可以将崩溃的日志信息,保存上传到服务器。
  • 担心用户不再打开app,可尝试异常后重启

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public void restartApp() {  
    Intent intent = new Intent(getApplicationContext(), FlashActivity.class); //FlashActivity 打开app的第一个界面
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    startActivity(intent);

    //把你退出APP的代码放在这

    android.os.Process.killProcess(android.os.Process.myPid());
    }

    注意

  • 设置了Thread.setDefaultUncaughtExceptionHandler可能无法捕获子线程异常

其他

有的异常,可能并非我们正规app导致的;比如,第三方开发者,反编译后重新打包的app。我们可以在捕获异常的时候作签名校验。

第三方解决方案