我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Android Crash 治理之道

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

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可以很轻松的看到在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卡,上传到服务器等逻辑。



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);
    }
    
    @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());
        }
    }
    
    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();
        }
    }
    
    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();
        }
    }
    
    private void zip(String class="lazy" data-src, String dest) throws IOException {
        ZipOutputStream out = null;
        File outFile = new File(dest);
        File fileOrDirectory = new File(class="lazy" data-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便成为了重中之重。

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

 

总结:

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


作者:地球很小


免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Android Crash 治理之道

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Android Crash 治理之道

Crash知道: Crash是指由于未处理的异常或者信号导致的意外退出,使得Android应用崩溃。当应用崩溃时,Android会杀死应用的进程并显示一个对话框来告知用户,他的应用由于未知的意外而停止了。当然现在的国内厂商自定义的系统大多取
2022-06-06

技术解读:美团外卖Android Crash治理之路!

  【本文转载自美团技术团队微信公众号,作者:维康 少杰 晓飞,转载和授权请联系原作者】  Crash率是衡量一个App好坏的重要指标之一。如果你忽略了它的存在,它就会得寸进尺,愈演愈烈,最后造成大量用户的流失,进而给公司带来无法估量的损失
2023-06-05

Android 性能优化(四)Crash治理之路,拦截并处理Exception

前言 Crash率是衡量一个App好坏的重要指标之一。如果你忽略了它的存在,它就会得寸进尺,愈演愈烈,最后造成大量用户的流失,进而给公司带来无法估量的损失。 上一篇(Android 性能优化(三)认识异常Exception和错误Error)
2022-06-06

Android Java crash 处理流程详解

这篇文章主要为大家介绍了Android Java crash 处理流程详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2022-11-13

一块肉,一把剑!深析360大数据安全治理之道

无论是对于个人还是企业,数据的重要性是不言而喻的!不少用户将数据形象的比喻为信息时代的石油和金矿,这一观点小编深表赞同。由此一来数据安全的防护也成为企业业务中极为重要的一环。但近年来数据泄露事件却屡禁不止,从雅虎数亿用户的数据泄露到Face
2023-06-04
数据库审计:从零到一的进阶指南,掌握数据治理之道
2024-03-10

SpringCloud Eureka服务治理之服务注册服务发现

这篇文章主要介绍了SpringCloud Eureka服务治理服务注册和服务发现概念详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2022-11-13

双黄连与“阿飞的剑”:疫情之下的信息治理

此刻的你,一定记得这件事:1月31日晚上,有媒体发微博称:记者从中国科学院上海药物所获悉,该所和武汉病毒所联合研究初步发现,中成药双黄连口服液可抑制新型冠状病毒。这个消息一出,15分钟之后全网电商双黄连口服液全部断货,有些地区百姓不顾疫情彻
2023-06-06

Android开发分渠道打包之友盟篇

第一步:在清单文件AndroidManifest.xml中application模块中加入:第二步:在app的build.gradle中的android部分加入:flavorDimensions ""productFlavors{wando
2022-06-06

Android线程管理之ActivityThread

ActivityThread功能 它管理应用进程的主线程的执行(相当于普通Java程序的main入口函数),并根据AMS的要求(通过IApplicationThread接口,AMS为Client、ActivityThread.Applica
2022-06-06

Android Mms之:深入理解Compose

Mms中的ComposeMessageActivity(以下简称Composer)是整个Mms中最重要的一个组件,它负责编辑信息,发送信息,管理信息,接收信息,与外部应用接口。在Mms内部与Composer关联的类和组件特别多,几乎所有的类
2022-06-06

编程热搜

  • Android:VolumeShaper
    VolumeShaper(支持版本改一下,minsdkversion:26,android8.0(api26)进一步学习对声音的编辑,可以让音频的声音有变化的播放 VolumeShaper.Configuration的三个参数 durati
    Android:VolumeShaper
  • Android崩溃异常捕获方法
    开发中最让人头疼的是应用突然爆炸,然后跳回到桌面。而且我们常常不知道这种状况会何时出现,在应用调试阶段还好,还可以通过调试工具的日志查看错误出现在哪里。但平时使用的时候给你闹崩溃,那你就欲哭无泪了。 那么今天主要讲一下如何去捕捉系统出现的U
    Android崩溃异常捕获方法
  • android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
    系统的设置–>电池–>使用情况中,统计的能耗的使用情况也是以power_profile.xml的value作为基础参数的1、我的手机中power_profile.xml的内容: HTC t328w代码如下:
    android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
  • Android SQLite数据库基本操作方法
    程序的最主要的功能在于对数据进行操作,通过对数据进行操作来实现某个功能。而数据库就是很重要的一个方面的,Android中内置了小巧轻便,功能却很强的一个数据库–SQLite数据库。那么就来看一下在Android程序中怎么去操作SQLite数
    Android SQLite数据库基本操作方法
  • ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
    工作的时候为了方便直接打开编辑文件,一些常用的软件或者文件我们会放在桌面,但是在ubuntu20.04下直接直接拖拽文件到桌面根本没有效果,在进入桌面后发现软件列表中的软件只能收藏到面板,无法复制到桌面使用,不知道为什么会这样,似乎并不是很
    ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
  • android获取当前手机号示例程序
    代码如下: public String getLocalNumber() { TelephonyManager tManager =
    android获取当前手机号示例程序
  • Android音视频开发(三)TextureView
    简介 TextureView与SurfaceView类似,可用于显示视频或OpenGL场景。 与SurfaceView的区别 SurfaceView不能使用变换和缩放等操作,不能叠加(Overlay)两个SurfaceView。 Textu
    Android音视频开发(三)TextureView
  • android获取屏幕高度和宽度的实现方法
    本文实例讲述了android获取屏幕高度和宽度的实现方法。分享给大家供大家参考。具体分析如下: 我们需要获取Android手机或Pad的屏幕的物理尺寸,以便于界面的设计或是其他功能的实现。下面就介绍讲一讲如何获取屏幕的物理尺寸 下面的代码即
    android获取屏幕高度和宽度的实现方法
  • Android自定义popupwindow实例代码
    先来看看效果图:一、布局
  • Android第一次实验
    一、实验原理 1.1实验目标 编程实现用户名与密码的存储与调用。 1.2实验要求 设计用户登录界面、登录成功界面、用户注册界面,用户注册时,将其用户名、密码保存到SharedPreference中,登录时输入用户名、密码,读取SharedP
    Android第一次实验

目录