记录并通过邮件上传App崩溃日志

1.引子
最近在做一个社交app的过程中,用户总是反映app在跳转到分享页面的时候App无故退出。
我在我的手机上实验了几下,都能成功,神奇的安卓啊,最后想到了一个办法,
记录用户app的崩溃日志来解决。
可是用户就是用户,提交错误日志,总不能给用户说,你到xx路径下面,把xx文件发给我吧。
所以想到了这种方法:
1.如果app退出,则将app的崩溃日志记录在某个文件下面;
2.当用户再次打开app的时候,提示,用户是否上传错误日志;
3.如果用户选择是,就将错误日志以附件的形式,添加到发送的邮件中;
4.选择否,就直接删除错误日志;
2.知识点讲解
1.如何记录App的崩溃日志

/**
 * UncaughtException处理类,当程序发生Uncaught异常的时候
 * 
 * @author user 注意修改文件的路径和文件名,在Manifest中添加文件读写权限;
 * 
 */
public class CrashHandler implements UncaughtExceptionHandler {
    // 错误日志文件夹的位置
    private String mCrashLogDirPath = "";
    public static final String TAG = CrashHandler.class.getSimpleName();
    // 系统默认的UncaughtException处理类
    private Thread.UncaughtExceptionHandler mDefaultHandler;
    // CrashHandler实例
    private static CrashHandler INSTANCE = new CrashHandler();
    private Context mContext;
    // 用来存储设备信息和异常信息
    private Map<String, String> infos = new HashMap<String, String>();
    // 用于格式化日期,作为日志文件名的一部分
    private DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss",
            Locale.CHINA);

    private CrashHandler() {
    }

    public static CrashHandler getInstance() {
        return INSTANCE;
    }

    public void init(Context context) {
        mContext = context;
        mCrashLogDirPath = context.getExternalCacheDir() + File.separator
                + MainActivity.Error_DIR_NAME + File.separator;
        // 获取系统默认的UncaughtException处理器
        mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
        // 设置该CrashHandler为程序的默认处理器
        Thread.setDefaultUncaughtExceptionHandler(this);
    }

    /**
     * 当UncaughtException发生时会转入该函数来处理 如果导入项目@Override报错,请修改project编译的jdk版本到1.5以上
     */
    @Override
    public void uncaughtException(Thread thread, Throwable ex) {
        if (!handleException(ex) && mDefaultHandler != null) {
            // 如果用户没有处理则让系统默认的异常处理器来处理
            mDefaultHandler.uncaughtException(thread, ex);
        } else {
            // 退出程序
            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;
        }
        // 收集设备参数信息
        collectDeviceInfo(mContext);
        saveCrashInfo2File(ex);
        // 向配置文件中写入标识位
        flagCrash();
        return true;
    }

    public void flagCrash() {
        SharedPreferences.Editor editor = PreferenceManager
                .getDefaultSharedPreferences(mContext).edit();
        editor.putBoolean(MainActivity.TAG_OCCURRED_ERROR, true);
        editor.commit();
    }

    /**
     * 收集设备参数信息
     * 
     * @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 Boolean 判断文件保存到本地是否成功
     */
    private Boolean saveCrashInfo2File(Throwable ex) {
        Boolean saveFlag = false;
        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();
        // TODO
        System.err.println("*****下面打印错误信息*****");
        System.err.println(result);
        sb.append(result);
        try {
            long timestamp = System.currentTimeMillis();
            String time = formatter.format(new Date());
            String fileName = "crash-" + time + "-" + timestamp + ".log";
            if (Environment.getExternalStorageState().equals(
                    Environment.MEDIA_MOUNTED)) {

                File dir = new File(mCrashLogDirPath);
                if (!dir.exists()) {
                    dir.mkdirs();
                }
                FileOutputStream fos = new FileOutputStream(mCrashLogDirPath
                        + fileName);
                fos.write(sb.toString().getBytes());
                fos.close();
            }
            saveFlag = true;
        } catch (Exception e) {
            Log.e(TAG, "an error occured while writing file...", e);
        }
        return saveFlag;
    }

和这片相似的代码,只要稍微搜索一下随处可见。注意相似不是雷同。在这里阐述一下我的小聪明:
1.在这里我使用的路径为this.getCacheDir(),在这个路径下面创建和删除文件是不需要任何权限的;
2.在将app崩溃信息保存到文件之前,我对错误信息进行了控制台打印。因为如果仅仅将错误信息保存到本地文件的话,对于程序员来说,每一次向在控制台打印错误日志,都必须在Application中将手机错误信息的日志功能关闭掉,但是下一次打包的时候,就很有可能把这件事情忘了(亲身经历啊);
3.app崩溃字段的保存。这里使用 PreferenceManager
.getDefaultSharedPreferences(mContext),获取App默认的sharedPreference文件,省略了自定义文件的麻烦,当然还需要自定义一个字段。当用户下次进入app后,对app之前是否发生过崩溃进行判断。
3.大家注意一下错误信息的差别:
1.在没有添加错误日志的时候,打印的错误信息如下:
这里写图片描述
打印信息为鲜红色,这个大家应该都比较熟悉。
2.看一下添加记录App崩溃日志后的效果:
这里写图片描述
这种红的颜色就淡了好多,就不是太惹眼了。
3.看一下报错的错误日志在手机上的效果吧:
这里写图片描述

下面贴上打开App检测是否发生过崩溃,上传崩溃(通过调用手机的邮件系统),清除崩溃字段和崩溃日志的完整代码


/**
 * 
 * @author guchuanhang 
 * 
 */
public class MainActivity extends Activity implements
        android.view.View.OnClickListener {
    public static final int TAG_ERROR_CODE = 10;
    /**
     * 错误日志在cachedir下面的crash文件夹下面
     */
    public static final String Error_DIR_NAME = "crash";
    /**
     * 通过该字段,进行判断,程序是否发生过crash ,使用getDefaultSharedPreferences
     */
    public static final String TAG_OCCURRED_ERROR = "crashed";
    private Dialog upErrorDialog;
    private Button mbtnMakeError = null;
    private String errorString;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        mbtnMakeError = (Button) findViewById(R.id.btn_make_error);
        mbtnMakeError.setOnClickListener(this);

        // 判断上次是否发生过崩溃
        SharedPreferences preferences = PreferenceManager
                .getDefaultSharedPreferences(this);
        if (preferences.getBoolean(TAG_OCCURRED_ERROR, false)) {
            uploadErrorDialog();
        }

    }

    public void onClick(View v) {
        switch (v.getId()) {
        case R.id.btn_make_error:
            System.out.println(errorString.equals("xxx"));
            break;

        default:
            break;
        }
    }

    // 第一上传错误日志的对话框样式
    protected void uploadErrorDialog() {
        AlertDialog.Builder builder = new Builder(MainActivity.this);
        builder.setMessage("上次程序异常退出,\n上传错误日志?");
        builder.setTitle("提示");
        builder.setPositiveButton("确定", new OnClickListener() {
            public void onClick(DialogInterface dialog, int which) {
                setAttachment(MainActivity.this);
                dialog.dismiss();
            }
        });
        builder.setNegativeButton("取消", new OnClickListener() {
            public void onClick(DialogInterface dialog, int which) {
                deleteErrorLogAndRemoveTag();
                dialog.dismiss();
            }
        });
        upErrorDialog = builder.create();
        upErrorDialog.show();
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (upErrorDialog != null && upErrorDialog.isShowing()) {
            upErrorDialog.dismiss();
        }
        if (requestCode == TAG_ERROR_CODE) {
            deleteErrorLogAndRemoveTag();
        } else {
            super.onActivityResult(requestCode, resultCode, data);
        }
    }
    public void deleteErrorLogAndRemoveTag(){
        File file = new File(getExternalCacheDir(), Error_DIR_NAME);
        File[] errorFiles = file.listFiles();
        for (int i = 0; i < errorFiles.length; i++) {
            errorFiles[i].delete();
        }
        SharedPreferences.Editor editor = PreferenceManager
                .getDefaultSharedPreferences(this).edit();
        editor.remove(TAG_OCCURRED_ERROR);
        editor.commit();
    }

    public void setAttachment(Context conext) {
        Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
        String[] tos = { "guchuanhang@qq.com" };
         String[] ccs = { "1162834643@qq.com" };
        intent.putExtra(Intent.EXTRA_EMAIL, tos);
         intent.putExtra(Intent.EXTRA_CC, ccs);
        intent.putExtra(Intent.EXTRA_TEXT, "我很生气,你要尽快解决。\n 否则后果不堪设想!");
        intent.putExtra(Intent.EXTRA_SUBJECT, "叮咚FM崩溃日志");

        ArrayList<Uri> imageUris = new ArrayList<Uri>();
        File srcDir = new File(this.getExternalCacheDir(), "crash");
        File[] logFiles = srcDir.listFiles();
        for (int i = 0; i < logFiles.length; i++) {
            imageUris.add(Uri.parse("file://" + logFiles[i].getAbsolutePath()));
        }

        intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, imageUris);
        intent.setType("image/*");
        intent.setType("message/rfc882");
        Intent.createChooser(intent, "Choose Email Client");
        ((Activity) conext).startActivityForResult(intent, TAG_ERROR_CODE);
    }

}

下面贴上Application中打开错误日志的方法:


import pan.gch.demo.util.CrashHandler;
import android.app.Application;
/**
 * @author guchuanhang
 *
 */
public class MyApplication extends Application {  

    @Override  
    public void onCreate() {  
        super.onCreate();  
        CrashHandler crashHandler = CrashHandler.getInstance();  
        crashHandler.init(getApplicationContext());  
    }  
}  

3.附上Demo
我啰嗦了折磨多,相信聪明的你,一定一看就会。Demo下载地址:
http://download.csdn.net/detail/guchuanhang/9148729

展开阅读全文

没有更多推荐了,返回首页