Android Crash 治理之道

Crash知道:

Crash是指由于未处理的异常或者信号导致的意外退出,使得Android应用崩溃。当应用崩溃时,Android会杀死应用的进程并显示一个对话框来告知用户,他的应用由于未知的意外而停止了。当然现在的国内厂商自定义的系统大多取消了这个通知应用终止的对话框。他们认为系统所提供的对话框没有意义,如果开发者希望在自身应用崩溃时,弹出对话框告知,也可以通过Android系统提供的API自行定制。

一般常规的Android Crash主要是由于开发者代码编写不规范导致的。比如一些常见异常没有捕获处理,通常在android应用开发中NullPointerException(空指针异常)是最常见的,一个小小的初始化、网络获取的不规范的数据、解析出错等都有可能导致空指针异常。其次是IndexOutOfBoundsException(数组角标越界异常),由于android应用中一般都会大量使用ListView,因此这类异常导致的Crash也是较多的。

还有些其他情况导致的Crash,比如Out of Memory(俗称OOM),内存溢出是一个大课题,这里就不多做介绍,只要知道这种Crash是由于开发者代码编写不规范导致使用的内存超过了该应用申请的内存的最大阈值。简单来说就是内存不够用了,手机甩锅了。

众所周知国内的手机厂家百花齐放,导致android机型各种各样,碎片化严重,有时候同一个应用在某些特定的机型上就会出现Crash。这类也是最难搞的一种,没有相同机型问题很难重现,底层代码不同又必须去阅读底层代码,找到问题了又没法修改,还得想办法绕过它。

总之,能够导致Crash的原因有很多,而一个优秀的应用则应该尽量降低Crash,甚至是零Crash(这是美好的愿望~也只是个愿望),因此如何去降低应用的Crash率就显得尤为重要了。

 

Crash检测:

一般在开发过程中所遇到的问题都能够通过logcat查看到,比如:

logcat日志

从logcat可以很轻松的看到在ActivityThread运行时导致了RuntimeException异常,而导致异常的原因是ArrayIndexOutofBoundsExce-ption,即数组角标越界,发生在代码CrashActivity类中第60行。由此我们可以很方便的检测出Crash。

而大多数情况下,开发者并不能保证应用在正式上线后不存在崩溃,那么这个时候又如何去检测呢?如果你的应用发布在Google应用商店上面的话,那么恭喜你,当你的应用崩溃数过多的时候,Android Vitals就会通过Play管理中心来提醒你,在Android Vitals中有一个指标叫Crash rates(崩溃率)。它认为当每天至少有1.09%的工作时段出现了至少一次崩溃或者每天至少有0.18%的工作时段出现了两次或者两次以上的崩溃则为不正常。

当然,如果你的应用没有发布到Google应用商店,那么也不用担心,我们可以通过CrashHandler在应用Crash时进行捕获,然后保存并上传日志信息,之后我们就可以通过日志进行分析了。CrashHandler原理是通过Thread.UncaughtExceptionHandler接口中的uncaughtException方法来实现,当应用发生未捕获的异常时,会回调此方法。我们可以在其中大做文章。

接下来我们就通过代码讲讲CrashHandler是如何实现的,首先需要创建一个CrashHandler类并让他继承Thread.Uncaught-ExceptionHandler接口,然后在其中完成保存异常信息到sd卡,上传到服务器等逻辑。

/**
 * 当应用意外崩溃时,捕获并处理
 * Created by ledding on 2020/4/14.
 */
public class CrashHandler implements Thread.UncaughtExceptionHandler {
    private static final String TAG = "CrashHandler";
    private static final boolean DEBUG = true;

    private static final String PATH = Environment.getExternalStorageDirectory() + "/Crash/log";

    private static CrashHandler INSTANCE = new CrashHandler();
    private Context mContext;
    private Thread.UncaughtExceptionHandler mDefaultExceptionHandler;

    private CrashHandler(){

    }

    public static CrashHandler getInstance(){
        return INSTANCE;
    }

    public void init(Context context){
        this.mContext = context;
        //获取当前默认ExceptionHandler,保存在全局对象
        mDefaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
        //替换默认对象为当前对象
        Thread.setDefaultUncaughtExceptionHandler(this);
    }

    /**
     * 当应用发生未捕获的异常时,会回调此方法
     * @param t
     * @param e
     */
    @Override
        public void uncaughtException(Thread t, Throwable e) {
        //保存trace信息到sd卡
        dumpToSDCard(t,e);
        //TODO 上传到服务器,也可以选择在其他时间上传
        e.printStackTrace();
        if (mDefaultExceptionHandler!=null){
            mDefaultExceptionHandler.uncaughtException(t,e);
        }else {
            //主动杀死进程
            Process.killProcess(Process.myPid());
        }

    }

    /**
     * dump trace信息到sd卡
     * @param t
     * @param e
     */
    private void dumpToSDCard(final Thread t,final Throwable e){
        if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
            Log.i(TAG, "no sdcard skip dump");
            return;
        }

        //判断文件夹路径是否存在
        File file = new File(PATH);
        if (!file.exists()){
            file.mkdirs();
        }

        //将当前时间作为文件名命名
        Date nowDate = new Date(System.currentTimeMillis());
        String time = new SimpleDateFormat("yyyy-MM-dd_HH:mm:ss", Locale.CHINA).format(nowDate);
        File logFile = new File(PATH,time+".trace");
        //注意实际保存的地址可能与Environment.getExternalStorageDirectory()获取的地址有所区别
        //可以通过adb命名查看实际位置
        Log.i(TAG, logFile.getAbsolutePath());
        try {
            //写入手机信息和异常日志信息
            PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(logFile)));
            if (pw.checkError()) {
                pw.println(time);
                dumpPhoneInfo(pw);
                pw.println();
                e.printStackTrace();
            }
            pw.close();
        } catch (Exception e1) {
            e1.printStackTrace();
        }
    }

    /**
     * 保存手机信息
     * @param pw
     */
    private void dumpPhoneInfo(PrintWriter pw){
        PackageManager pm = mContext.getPackageManager();
        PackageInfo pi = null;

        try {
            pi = pm.getPackageInfo(mContext.getPackageName(),PackageManager.GET_ACTIVITIES);
            if (pi != null){
                pw.print("APP Version:");
                pw.print(pi.versionName);
                pw.print('_');
                pw.print(pi.versionCode);

                //android版本号
                pw.print("OS Version: ");
                pw.print(Build.VERSION.RELEASE);
                pw.print("_");
                pw.println(Build.VERSION.SDK_INT);

                //手机制造商
                pw.print("Vendor: ");
                pw.println(Build.MANUFACTURER);

                //手机型号
                pw.print("Model: ");
                pw.println(Build.MODEL);

                //cpu架构
                pw.print("CPU ABI: ");
                pw.println(Build.CPU_ABI);
            }
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
    }

    /**
     * 压缩文件夹,为上传做准备。节省流量。
     * @param src
     * @param dest
     * @throws IOException
     */
    private void zip(String src, String dest) throws IOException {
        ZipOutputStream out = null;
        File outFile = new File(dest);
        File fileOrDirectory = new File(src);
        out = new ZipOutputStream(new FileOutputStream(outFile));
        if (fileOrDirectory.isFile()) {
            zipFileOrDirectory(out, fileOrDirectory, "");
        }else {
            File[] entries = fileOrDirectory.listFiles();
            for (int i = 0; i < entries.length; i++) {
                zipFileOrDirectory(out, entries[i], "");
            }
        }
        if(null != out){
            out.close();
        }
    }

    private static void zipFileOrDirectory(ZipOutputStream out,File fileOrDirectory, String curPath) throws IOException {
        FileInputStream in = null;
        if (!fileOrDirectory.isDirectory()){
            byte[] buffer = new byte[4096];
            int bytes_read;
            in = new FileInputStream(fileOrDirectory);
            ZipEntry entry = new ZipEntry(curPath + fileOrDirectory.getName());
            out.putNextEntry(entry);
            while ((bytes_read = in.read(buffer)) != -1) {
                out.write(buffer, 0, bytes_read);
            }
            out.closeEntry();
        }else{
            File[] entries = fileOrDirectory.listFiles();
            for (int i = 0; i < entries.length; i++) {
                zipFileOrDirectory(out, entries[i], curPath + fileOrDirectory.getName() + "/");
            }
        }
        if (null != in){
            in.close();
        }
    }
}

接下来在创建一个MyApplication继承自Application,并在onCreate中初始化CrashHandler。这样CrashHandler的生命周期就随着应用的生命周期而改变了。

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        //捕获异常
        CrashHandler.getInstance().init(getApplicationContext());
    }
}

最后再手动添加未捕获的异常代码,然后运行一下。

        int[] numbs = new int[5];
        numbs[5] = 0;

正常情况下,这个时候手机sd卡里应该已经存在trace文件了,文件路径是Environment.getExternalStorageDirectory() + "/Crash/log",这里要说明一下通过Environment.getExternalStorageDirectory()获取的路径不一定是手机存在的真实路径,所以我们可以直接在手机文件管理器中搜索文件名(这里的文件是以时间来保存的)。

 接下来你可以直接打开trace文件,当你连接adb时也可以通过adb shell命令查找并导出到电脑上打开。

可以看到日志中写入了时间、app版本、os版本、手机型号和错误日志等信息,我们可以很轻松的检测问题。当然如果是用户在使用应用是发生了崩溃,你不可能让用户提供他手机中的日志文件给你,但是你却可以在uncaughtException方法中上传日志到服务器,以便你分析和解决问题。

 

Crash预防:

就用户体验而言,你的应用发生了崩溃就是不好的体验,及时你能及时的检测和修复,因此,合理的预防Crash便成为了重中之重。

  1. 避免NullPointException,尽量做到对可能为空的对象做判空处理,也要养成使用@NonNull注解的习惯。
  2. 避免IndexOutOfBoundsException,一般情况下封装BaseAdapter,数据统一让Adapter管理,尽量使用线程安全的容器集合。
  3. 如果是由Android碎片化所引起的系统级的异常Crash,我们是没法提前预防的,只能在自身的日积月累中,根据自身经验来提前预防一些以前遇到过的问题,或通过网络积累。
  4. 避免OutOfMemoryError,导致内存泄漏的原因有多种,比如单例模式引用了某个Activity的Context、非静态内部类默认是持有外部类的引用,一旦生命周期长于外部类生命周期,则外部类很难被杀死、Bitmap处理过后没有及时回收等都是我们所必须去注意的问题,具体如何去预防内存泄漏网上也有很多,后续我也会进行整理。

 

总结:

在Android性能优化中Crash是没法绕开的话题,我们经常在微博热搜上看到某某app又崩了,一个app的口碑往往会因为几次Crash而一落千丈。因此Crash治理至关重要,本文也只是粗浅的总结了一下,Crash治理之路任重而道远。

发布了17 篇原创文章 · 获赞 6 · 访问量 4338

猜你喜欢

转载自blog.csdn.net/ledding/article/details/105508591