阅读 1901

Android 程序崩溃之快速锁定!

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

前言

从刚开始接触Android开发,第一次发版,遇到程序崩溃,那就一个慌张。好几年过去了,现在的听到程序崩溃?嗯,稍等我看看什么问题,然后该锁定该锁定该解决解决。

发版前减少bug、崩溃等,发版后遇到bug、崩溃也不要慌张,毕竟 bug不 会因为你的慌张而自动修复对吧?要以最快的速度解决(解决问题同样是能力的体现),并说明问题轻重,看看是直接发版还是坐等下次。同时,吸取教训避免同样问题发生。

今天咱们就聊聊Android程序闪退。一个应用的崩溃率高低,决定了这个应用的质量。

为了解决崩溃问题,Android 系统会输出各种相应的 log 日志,当然还各式各样的三方库,大程度上降低了工程师锁定崩溃问题的难度。

如果要给 crash 日志进行分类,可以分成 2 大类

  • JVM 异常(Exception)堆栈信息,如下:

  • native 代码崩溃日志,如下:

JVM 异常堆栈信息

Java 中异常(Exception)分两种:

  • 检查异常 checked Exception
  • 非检查异常 unchecked Exception

检查异常:就是在代码编译时期,Android Studio 就会提示代码有错误,无法通过编译,比如 InterruptedException。如果我们没有在代码中将这些异常 catch,而是直接抛出,最终也有可能导致程序崩溃。

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
复制代码

非检查异常:包括 error 和运行时异常(RuntimeException),Android Studio 并不会在编译时期提示这些异常信息,而是在程序运行时期因为代码错误而直接导致程序崩溃,比如 OOM 或者空指针异常(NullPointerException)。

2021-09-13 11:50:27.327 19984-19984/com.scc.demo E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.scc.demo, PID: 19984
    java.lang.RuntimeException: Unable to start activity ComponentInfo{com.scc.demo/com.scc.demo.actvitiy.HandlerActivity}: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.widget.Button.setOnClickListener(android.view.View$OnClickListener)' on a null object reference
        ...
     Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.widget.Button.setOnClickListener(android.view.View$OnClickListener)' on a null object reference
        at com.scc.demo.actvitiy.HandlerActivity.onCreate(HandlerActivity.java:41)
        at android.app.Activity.performCreate(Activity.java:8000)
        ...
复制代码

Java 异常

对于上述两种异常我们都可以使用 UncaughtExceptionHandler 来进行捕获操作,它是 Thread 的一个内部接口,定义如下:

    public interface UncaughtExceptionHandler {
        /**
         * 当给定Thread由于给定的Throwable而终止时调用的方法。
         * 此方法抛出的任何异常都将被 Java 虚拟机忽略。
         * @param t Thread
         * @param e Throwable
         */
        void uncaughtException(Thread t, Throwable e);
    }
复制代码

从官方对其介绍能够看出,对于传入的 Thread,如果因为"未捕获"异常而导致被终止,uncaughtException 则会被调用。我们可以借助它来间接捕获程序异常,并进行异常信息的记录工作,或者给出更友好的异常提示信息。

自定义异常处理类

  • 1.收集 crash 现场的相关信息,如当前 App 的版本信息,设备的相关信息以及异常信息。

  • 2.日志的记录工作(如保存在本地),等开发人员排查问题或等下次启动APP上传至服务器。

自定义异常处理类

自定义类实现 UncaughtExceptionHandler 接口,并实现 uncaughtException 方法:

public class SccExceptionHandler implements Thread.UncaughtExceptionHandler {
    private Thread.UncaughtExceptionHandler mDefaultHandler;
    private static SccExceptionHandler sccExceptionHandler;
    private Context mContext;

    public static SccExceptionHandler getInstence() {
        if (sccExceptionHandler == null) {
            synchronized (SccExceptionHandler.class) {
                sccExceptionHandler = new SccExceptionHandler();
            }
        }
        return sccExceptionHandler;
    }

    public void init(Context context) {
        mContext = context;
        //系统默认未捕获异常handler
        //the default uncaught exception handler for all threads
        mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
        //将当前Handler设为系统默认
        Thread.setDefaultUncaughtExceptionHandler(this);

    }

    @Override
    public void uncaughtException(@NonNull @NotNull Thread t, @NonNull @NotNull Throwable e) {
        if (!handlerUncaughtException(e) && mDefaultHandler != null) {
            //注释1:系统处理
            mDefaultHandler.uncaughtException(t, e);
        } else {
            //注释2:自己处理
            Intent intent = new Intent(mContext, ImageViewActivity.class);
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_CLEAR_TASK);
            mContext.startActivity(intent);
            //终止进程
            android.os.Process.killProcess(android.os.Process.myPid());
            //终止当前运行的 Java 虚拟机。
            //参数用作状态代码; 按照惯例,非零状态代码表示异常终止。
            System.exit(0);
        }
    }

    //处理程序未捕获异常
    private boolean handlerUncaughtException(Throwable e) {
        //1.收集 crash 现场的相关信息,如当前 App 的版本信息,设备的相关信息以及异常信息。
        //2.日志的记录工作(如保存在本地),等开发人员排查问题或等下次启动APP上传至服务器。
        return true;
        //不想处理 return false;
    }
}
复制代码

注释1:在自定义异常处理类中需要持有线程默认异常处理类。这样做的目的是在自定义异常处理类无法处理或者处理异常失败时,还可以将异常交给系统做默认处理。

注释2:如果自定义异常处理类成功处理异常,需要进行页面跳转,或者将程序进程"杀死"。否则程序会一直卡死在崩溃界面,并弹出无响应对话框。

android.os.Process.myPid():返回此进程的标识符,可与 killProcess 和 sendSignal 一起使用。

android.os.Process.killProcess(android.os.Process.myPid()):使用给定的 PID 终止进程。 请注意,尽管此 API 允许我们根据其 PID 请求终止任何进程,但内核仍会对您实际能够终止的 PID 施加标准限制。 通常这意味着只有运行调用者的包/应用程序的进程和由该应用程序创建的任何其他进程; 共享一个通用 UID 的包也将能够杀死彼此的进程。

使用自定义异常处理类

SccExceptionHandler 定义好之后,就可以将其初始化,并将主线程注册到 SccExceptionHandler 中。如下:

public class SccApp extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        SccExceptionHandler.getInstence().init(this);
    }
}
复制代码

native 异常

当程序中的 native 代码发生崩溃时,系统会在 /data/tombstones/ 目录下保存一份详细的崩溃日志信息。由于对 native 还不是很熟悉就不误导大家,感兴趣的自己玩玩。

对于程序崩溃信号机制的介绍,可以参考腾讯的这篇文章:Android 平台 Native 代码的崩溃捕获机制及实现

线上崩溃日志获取

上面介绍的 Java 和 Native 崩溃的捕获都是基于自己能够复现 bug 的前提下。但是对于线上的用户,这种操作方式是不太现实的。

对于大多数公司来说,针对线上版本,没有必要自己实现一个抓取 log 的平台系统。最快速的实现方式就是集成第三方 SDK。目前比较成熟,采用也比较多的就是腾讯的 Bugly。

Bugly

Bugly 基本能够满足线上版本捕获 crash 的所有需求,包括 Java 层和 Native 层的 crash 都可以获取相应的日志。并且每天 Bugly 都会邮件通知上一天的崩溃日志,方便测试和开发统计 bug 的分布以及崩溃率。

接入文档

异常概括

崩溃分析

程序崩溃分析这块我没做调整,这个是bugly自动抓取的。

错误分析

具体内容

这里我用来存放去服务端请求接口时的参数和返回的数据。,下面看看具体效果。

使用起来相当方便,而且错误还提供解决方案,美滋滋。

xCrash

xCrash 能为安卓 app 提供捕获 java 崩溃,native 崩溃和 ANR 的能力。不需要 root 权限或任何系统权限。

xCrash 能在 app 进程崩溃或 ANR 时,在你指定的目录中生成一个 tombstone 文件(格式与安卓系统的 tombstone 文件类似)。

xCrash 已经在 爱奇艺 的不同平台(手机,平板,电视)的很多安卓 app(包括爱奇艺视频)中被使用了很多年。

xCrash传送门

Sentry

Sentry 是一项可帮助您实时监控和修复崩溃的服务。 服务器使用 Python,但它包含一个完整的 API,用于在任何应用程序中从任何语言发送事件。

Sentry传送门

XCrash 和 Sentry,这两者比 Bugly 好的地方就是除了自动拦截界面崩溃事件,还可以主动上报错误信息。

可以看出 XCrash 的使用更加灵活,工程师的掌控性更高。可以通过设置不同的过滤方式,针对性地上报相应的 crash 日志。并且在捕获到 crash 之后,可以加入自定义的操作,比如本地保存日志或者直接进行网络上传等。

另外:Sentry 还有一个好处就是可以通过设置过滤,来判断是否上报 crash 日志。这对于 SDK 的开发人员是很有用的。比如一些 SDK 的开发商只是想收集自身 SDK 引入的 crash,对于用户的其他操作导致的 crash 进行过滤,这种情况就可以考虑集成 Sentry。

Bugly 简单使用

感觉教程乱的可以自己去上文找Buyle文档自己集成,很简单的。

库文件导入

自动集成(推荐)

plugins {
    id 'com.android.application'
}
android {
    compileSdkVersion 30//项目的编译版本
    defaultConfig {
        applicationId "com.scc.demo"//包名
        minSdkVersion 23//最低的兼容的Android系统版本
        targetSdkVersion 30//目标版本,表示你在该Android系统版本已经做过充分的测试
        versionCode 1//版本号
        versionName "1.0.0"//版本名称
        ndk {
            abiFilters 'armeabi-v7a', 'arm64-v8a','x86'
            //运行环境,要上传Google Play必须兼容64位,这里仅兼容ARM架构
            //对于ARM架构,32 位库位于armeabi-v7a 中。64 位等效项是arm64-v8a。
            //对于x86体系结构,查找x86(用于 32 位)和 x86_64(用于 64 位)。
        }
    }
}

dependencies {
    implementation 'com.tencent.bugly:crashreport:3.4.4'
    //集成Bugly NDK时,需要同时集成Bugly SDK。
    implementation 'com.tencent.bugly:nativecrashreport:3.9.2'

}
复制代码

注意:自动集成时会自动包含Bugly SO库,建议在Module的build.gradle文件中使用NDK的"abiFilter"配置,设置支持的SO库架构。

如果在添加"abiFilter"之后Android Studio出现以下提示:

NDK integration is deprecated in the current plugin. Consider trying the new experimental plugin.

则在项目根目录的gradle.properties文件中添加:

android.useDeprecatedNdk=true

参数配置

  • 在AndroidManifest.xml中添加权限:
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_LOGS" />
复制代码

注意:如果您的App需要上传到Google Play Store,您需要将READ_PHONE_STATE权限屏蔽掉或者移除,否则可能会被下架。

  • 请避免混淆Bugly,在Proguard混淆文件中增加以下配置:
-dontwarn com.tencent.bugly.**
-keep public class com.tencent.bugly.**{*;}
复制代码

初始化

public class SccApp extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        //70594a1ff8 Bugly新建产品的 App ID
        CrashReport.initCrashReport(getApplicationContext(), "70594a1ff8", false);
    }
}
复制代码

错误分析

设置

    private void setCrashReport(String url, String name, Map<String, String> params, String message) {
        try {
            if (params != null && !MStringUtils.isNullOrEmpty(url) && !MStringUtils.isNullOrEmpty(name) && !MStringUtils.isNullOrEmpty(params.toString()) && !MStringUtils.isNullOrEmpty(message)) {
                CrashReport.putUserData(AppGlobalUtils.getApplication(), "SccParams", params.toString());
                CrashReport.putUserData(AppGlobalUtils.getApplication(), "Data",   "LoginName-Scc001:" + message);
                CrashReport.postCatchedException(new RuntimeException(name + ":" + url + ":" + message));
            }
        } catch (Exception e) {
        }
    }
复制代码

调用

        HashMap<String,String> hashMap = new HashMap<>();
        hashMap.put("name","scc001");
        hashMap.put("pass","111111");
        String returnData = "哈哈哈哈哈";
        setCrashReport("loin/register","Main",hashMap,returnData);
复制代码

效果

错误列表

错误详情

出错堆栈

跟踪数据

崩溃分析

这个不用咱自己设置,Bugly自动抓取,下面提供跟错误分析类似功能这里就不多描述了。

本文内容到这里就算结束了。希望能帮你快速锁定 bug 并解决,让应用更完美,让你的老板更放心,票票来的更多一些。

往期推荐

❤️Android 安装包打包过程 ❤️

❤️Android 安装包体积优化❤️

❤️ Android 源码解读-应用是如何启动的❤️

❤️ Android 源码解读-startActivity()❤️

文章分类
Android
文章标签