android crash log收集总结

作者: 潇潇凤儿 | 来源:发表于2017-03-05 13:20 被阅读4799次

    一般应用在运行时,不可避免的会发生crash。一般应用crash分为三种:未捕获的异常、ANR(Application Not Responding)、闪退(NDK引发错误)。

    1. 未捕获的异常:一般系统会将crash log输入出控制台,开发人员可以及时查看。对于发布的版本,则可以通过一些方式收集crash log, 这样方便开发人员分析程序崩溃原因,更好的改进代码。同时当程序崩溃时,表现出闪退或已停止,可能会给用户带来很不好的体验,这需要我们重新定义程序崩溃时发出一些友情提示。
    2. ANR:系统一般会弹出提示框,让用户选择waiting还是close应用,在/data/anr目录下还生成一个traces.txt文件,方便程序员查看系统产生anr异常的堆栈和线程信息。
    3. 应用闪退,很可能是由于应用中的NDK引发错误,logcat打印出来的那堆日志根据看不懂,很难从日志当中定位错误的根源。如果是上线产品,一般很难收集到这些crash log,但也不是没有办法。

    下面就我们常遇到的crash log收集进行总结:

    一、java层崩溃:

    对于java层的crash,android中有处理这类问题的方法,在Thread类中的一个方法#setDefaultUncaughtExceptionHandler

    public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler handler) {  
        Thread.defaultUncaughtHandler = handler;  
    }
    

    通过该方法我们可以设置系统的默认异常处理器。其实UncaughtExceptionHandler是一个接口,

     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);
        }
    

    我们可以实现uncaughtException方法,自定义crash收集方法,然后将crash信息保存到本地的文件中就好了。
    具体实现步骤:

    1、 建立异常处理的CrashHandler,实现系统的UncaughtExceptionHandler

    当crash发生的时候,我们可以捕获到异常信息,把异常信息存储到SD卡中,然后在合适的时机通过网络将crash信息上传到服务器上,这样开发人员就可以分析用户crash的场景从而在后面的版本中修复此类crash。具体实现如下:

    public class MyCrashHandler implements UncaughtExceptionHandler {
    
        private static MyCrashHandler crashHandler;
        private static SimpleDateFormat formatter = new          SimpleDateFormat("yyyyMMddHHmmss");
        private static Date curDate = new Date(System.currentTimeMillis());//获取当前时间
        private static String str = formatter.format(curDate);
        private static String TAG = "MyCrashHandler";
        //系统默认的UncaughtException处理类
        private UncaughtExceptionHandler mDefaultHandler;
        //程序的Context对象
        private Context mContext;
        //用来存储设备信息和异常信息
        private Map<String, String> infos = new HashMap<String, String>();
    
    
        @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();//准备发消息的MessageQueue
                    Toast.makeText(mContext, "很抱歉,程序出现异常,即将退出.", Toast.LENGTH_LONG).show();
                    Looper.loop();
                }
            }.start();
            //收集设备参数信息
            collectDeviceInfo(mContext);
            //保存日志文件
            saveCrashInfo2File(ex);
            return true;
        }
    
    
        /**
         * 保存错误信息到文件中
         *
         * @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 {
                long timestamp = System.currentTimeMillis();
    
                String fileName = "crash-" + str + "-" + timestamp + ".log";
                if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
                    String path = Environment.getExternalStorageDirectory().getPath()+"/crash/";
                    File dir = new File(path);
                    if (!dir.exists()) {
                        dir.mkdirs();
                    }
                    FileOutputStream fos = new FileOutputStream(path + fileName);
                    fos.write(sb.toString().getBytes());
                    System.out.println(sb.toString());
                    fos.close();
                }
                return fileName;
            } catch (Exception e) {
                Log.e(TAG, "an error occured while writing file...", e);
            }
            return null;
        }
    
        /**
         * 初始化
         *
         * @param context
         */
        public void init(Context context) {
            mContext = context;
            //获取系统默认的UncaughtException处理器
            mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
            //设置该CrashHandler为程序的默认处理器
            Thread.setDefaultUncaughtExceptionHandler(this);
        }
    
        /**
         * 收集设备参数信息
         *
         * @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 (PackageManager.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);
                }
            }
        }
    
        private MyCrashHandler() {
        }
    
        //单例
        public static MyCrashHandler instance() {
            if (crashHandler == null) {
                crashHandler = new MyCrashHandler();
            }
            return crashHandler;
        }
    }
    

    2、 为ui线程添加默认异常事件Handler

    在ui线程中添加异常处理Handler,在应用的Application中添加而不是在Activity中添加。应用启动时是先走Application,故只需要在Application的onCreate方法里进行CrashHandler的初始化即可。

       public class CrashApplication extends MultiDexApplication  {
            private static final String TAG ="CrashApplication" ;
            private static Context mContext;
            @Override
            public void onCreate() {
                super.onCreate();
                MyCrashHandler crashHandler = MyCrashHandler.instance();
                crashHandler.init(getApplicationContext());
            }
        }
    

    3、方法2

    当然也可以集成第三方SDK,如友盟统计分析(http://dev.umeng.com/analytics/android-doc/integration),不仅能自动帮助应用收集crash log,还可以统计应用用户使用情况,总用户数、新增用户数、日活、月活、用户手手机版本、每个应用版本信息等情况。

    具体集成方式也很简单,只需要在友盟平台上注册开发者账号,申请应用,并在AndroidMainfest.xml里配置权限

    <meta-data android:value="YOUR_APP_KEY" android:name="UMENG_APPKEY"/>
    <meta-data android:value="Channel ID" android:name="UMENG_CHANNEL"/>
    

    在每个Activity的onResume方法中调用 MobclickAgent.onResume(Context) ,
    onPause方法中调用 MobclickAgent.onPause(Context)

       public void onResume() {
            super.onResume();
            MobclickAgent.onResume(this);
        }
        public void onPause() {
            super.onPause();
            MobclickAgent.onPause(this);
        }
    

    当然这可以写在基类里,省去每个类都去写。

    二、NDK的崩溃:

    由于jni代码不支持backtrace,故不可通过实现UncaughtExceptionHandler来捕获异常。楼主没有具体深入研究过,但大致可归纲以下方法:

    1、 通过api sigaction来捕捉对应的异常信号

    #include <signal.h> 
    int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));
    

    参考资料:
    1)【Android平台的崩溃捕获机制及实现】http://www.uml.org.cn/mobiledev/2016011510.asp

    1. stackOverFlow上提出的:http://stackoverflow.com/questions/1083154/how-can-i-catch-sigsegv-segmentation-fault-and-get-a-stack-trace-under-jni-on

    2、 借助第三方SDK:如腾讯 Bugly

    腾讯 Bugly不权可以抓取到JAVA的崩溃,同样也可以抓取到NDK代码的崩溃。

    3、 借助google的google-breakpad

    google-breakpad(https://github.com/svn2github/google-breakpad)可以捕获ndk的异常信息,它会将堆栈信息写入用户指定的本地MiniDump文件中。下次启动应用程序的时候,便可以读取该MiniDump文件进行相应的操作,比如上传到后台服务器。
    参考资料:1) [Android崩溃日志获取与解析](http://openflight.blog.51cto.com/11047400/1728909

    1. [Android端线上NativeCrash收集的两种方法(上)](http://www.jianshu.com/p/c65186af3089

    三、总结及注意事项

    1、 如果代码被proguad混淆后,输出的crash log,出错的行号看不出来(Unknown Source)

    需要在代码混淆的时候,加上在混淆文件里面记得加上这句:
    -keepattributes SourceFile,LineNumberTable # keep住源文件以及行号

    2、java层crash log收集

    1. 自己收集,可以实现系统的UncaughtExceptionHandler类,并重写uncaughtException方法,将crash log写到手机本地,在适时的时候,上传到我们的服务器,优点是,所需类和方法少,可以自定义log输出,缺点是没有第三方平台强大,只能收集crash log,统计信息等需要自己去做;
    2. 借助第三方平台,如友盟、腾讯 Bugly等,这些平台不仅能收集crash log,还添加了丰富的统计信息,可以很方便看出各版本、各平台等出现的crash,缺点是,导入第三库,代码量肯定比自己重写的类大,无法自定义应用崩溃时弹框;

    3、ndk层crash log收集

    1)在自己写的jni方法里,用sigaction来铺获,

    1. 采用google的google-breakpad的NativeCrash日志收集方法,优点是:可以很方便google-breakpad是夸平台开源工具,体量较大,在其基础上生成的通用SO和dmp日志也都较大,对于sdk大小有严格要求的APP,可能不是很方便。

    相关文章

      网友评论

        本文标题:android crash log收集总结

        本文链接:https://www.haomeiwen.com/subject/dibsgttx.html