【Android应用开发】 Android 崩溃日志 本地存储 与 远程保存(一)

简介: 【Android应用开发】 Android 崩溃日志 本地存储 与 远程保存(一)

一. 崩溃日志本地存储




1. 保存原理解析



崩溃信息本地保存步骤 :


-- 1. 自定义类实现 UncaughtExceptionHandler : public class CrashHandler implements UncaughtExceptionHandler;


-- 2. 设置该自定义的 CrashHandler 类为单例模式 :


// 单例模式
  private static CrashHandler INSTANCE = new CrashHandler();
  private CrashHandler() {
  }
  public static CrashHandler getInstance() {
  return INSTANCE;
  }

-- 重写 uncaughtException 方法 :

@Override
  public void uncaughtException(Thread thread, Throwable ex)

-- 自定义 handleException 方法处理异常信息 : 在该方法中进行设备信息收集, 以及将信息保存到文件中;




(1) UncaughtExceptionHandler 类解析


UncaughtExceptionHandler 作用 : 该类处理以下情况, 如果有未捕获的异常发生, 出现了程序崩溃闪退的情况, 此时会回调该类的 uncaughtException 方法;





(2) 线程相关


线程相关 : 每个线程都对应有响应的默认的未捕获异常处理器;


-- 获取线程默认的未捕获异常处理器 : Thread.getDefaultUncaughtExceptionHandler();


-- 设置线程默认的未捕获异常处理器 : Thread.setDefaultUncaughtExceptionHandler(this);





(3) uncaughtException 方法


uncaughtException 方法解析 :


-- 回调时机 : 出现未定义的异常时;


-- 回调参数 : 回调时会传入 线程对象 和 要抛出的异常信息, 我们可以在程序中拿到这两个信息;



public void uncaughtException(Thread thread, Throwable ex)




(4) 手机设备信息


手机设备信息手机步骤 :


-- 1. 获取包信息 :



//获取包管理器
    PackageManager pm = ctx.getPackageManager();
    //获取包信息
    PackageInfo pi = pm.getPackageInfo(ctx.getPackageName(),
      PackageManager.GET_ACTIVITIES);

-- 2. 获取版本号信息 :


if (pi != null) {
    //版本号
    String versionName = pi.versionName == null ? "null"
      : pi.versionName;
    //版本代码
    String versionCode = pi.versionCode + "";
    //将版本信息存放到 成员变量 Map<String, String> mInfos 中
    this.mInfos.put("versionName", versionName);
    this.mInfos.put("versionCode", versionCode);
    }


-- 3. 使用反射获取 Build 类成员变量变量 , 并遍历获取这些变量内容:


//获取 Build 中定义的变量, 使用反射方式获取, 该类中定义了设备相关的变量信息
  Field[] fields = Build.class.getDeclaredFields();
  //遍历获取额变量, 将这些信息存放到成员变量 Map<String, String> mInfos 中
  for (Field field : fields) {
    try {
    //设置 Build 成员变量可访问
    field.setAccessible(true);
    //将 设备相关的信息存放到 mInfos 成员变量中
    mInfos.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);
    }
  }


(4) 保存崩溃信息到文件



保存文件步骤 : 这些步骤就很简单了, 使用 IO流即可;


-- 1. 将之前获取的 Build 设备信息, 版本信息, 崩溃信息转为字符串 :



//存储相关的字符串信息
  StringBuffer sb = new StringBuffer();
  //将成员变量 Map<String, String> mInfos  中的数据 存储到 StringBuffer sb 中
  for (Map.Entry<String, String> entry : this.mInfos.entrySet()) {
    String key = entry.getKey();
    String value = entry.getValue();
    sb.append(key + "=" + value + "\n");
  }

-- 2. 在 Logcat 中打印崩溃信息 : 之前的默认操作就是打印崩溃信息到 Logcat 中, 我们在这里继续执行完这个步骤, 否则Logcat 中没有数据的;


//将 StringBuffer sb 中的字符串写出到文件中
  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();

-- 3. 写出数据到文件中 : IO 流知识点, 不再做过多赘述;


 

String result = writer.toString();
  sb.append(result);
  try {
    long timestamp = System.currentTimeMillis();
    String time = formatter.format(new Date());
    String fileName = "crash-" + time + "-" + timestamp + ".txt";
    if (Environment.getExternalStorageState().equals(
      Environment.MEDIA_MOUNTED)) {
    //获取文件输出路径
    String path = Environment.getExternalStorageDirectory()
      + "/crashinfo/";
    //创建文件夹和文件
    File dir = new File(path);
    if (!dir.exists()) {
      dir.mkdirs();
    }
    //创建输出流
    FileOutputStream fos = new FileOutputStream(path + fileName);
    //向文件中写出数据
    fos.write(sb.toString().getBytes());
    fos.close();
    }
    return fileName;
  } catch (Exception e) {
    Log.e(TAG, "an error occured while writing file...", e);
  }






2. 代码及示例





(1) 相关代码示例



故意发生错误的代码 :



public class MainActivity extends Activity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  }
  public void onClick(View view) {
  int i = 3;
  i = i / 0;
  }
}



CrashHandler 注册代码 : 在 Activity 或者 Application 中注册该代码;



CrashHandler.getInstance().init(getApplicationContext());


CrashHandler 代码 :



package cn.org.octpus.crash;
import java.io.File;
import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.Thread.UncaughtExceptionHandler;
import java.lang.reflect.Field;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Build;
import android.os.Environment;
import android.os.Looper;
import android.util.Log;
import android.widget.Toast;
/**
 * UncaughtExceptionHanlder 作用 : 处理 线程被未捕获的异常终止 的情况, 一旦出现了未捕获异常崩溃, 系统就会回调该类的
 * uncaughtException 方法;
 */
public class CrashHandler implements UncaughtExceptionHandler {
  // 用于打印日志的 TAG 标识符
  public static final String TAG = "octopus.CrashHandler";
  // 系统默认的UncaughtException处理类
  private Thread.UncaughtExceptionHandler mDefaultHandler;
  // 程序的Context对象
  private Context mContext;
  // 用来存储设备信息和异常信息
  private Map<String, String> mInfos = new HashMap<String, String>();
  // 用于格式化日期,作为日志文件名的一部分
  private DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
  // 单例模式
  private static CrashHandler INSTANCE = new CrashHandler();
  private CrashHandler() {
  }
  public static CrashHandler getInstance() {
  return INSTANCE;
  }
  /**
  * 初始化该类, 向系统中注册
  * @param context
  */
  public void init(Context context) {
  mContext = context;
  // 获取系统默认的 UncaughtException 处理器
  mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
  // 设置该 CrashHandler 为程序的默认处理器
  Thread.setDefaultUncaughtExceptionHandler(this);
  }
  /*
  * 出现未捕获的异常时, 会自动回调该方法
  * (non-Javadoc)
  * @see java.lang.Thread.UncaughtExceptionHandler#uncaughtException(java.lang.Thread, java.lang.Throwable)
  */
  @Override
  public void uncaughtException(Thread thread, Throwable ex) {
  /*
   * 调用 handleException() 方法处理该线程
   * 如果返回 true 说明处理成功, 如果返回 false 则调用默认的异常处理器来处理
   * 一般情况下该方法都会成功处理
   */
  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来显示异常信息, 
   * 由于在主线程会阻塞, 
   * 不能实时出现 Toast 信息, 
   * 这里我们在子线程中处理 Toast 信息
   */
  new Thread() {
    @Override
    public void run() {
    Looper.prepare();
    Toast.makeText(mContext, "很抱歉,程序出现异常,即将退出.", Toast.LENGTH_LONG)
      .show();
    Looper.loop();
    }
  }.start();
  // 收集设备参数信息
  collectDeviceInfo(mContext);
  // 保存日志文件
  saveCrashInfo2File(ex);
  return true;
  }
  /**
  * 收集设备参数信息, 将手机到的信息存储到
  * @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 + "";
    //将版本信息存放到 成员变量 Map<String, String> mInfos 中
    this.mInfos.put("versionName", versionName);
    this.mInfos.put("versionCode", versionCode);
    }
  } catch (NameNotFoundException e) {
    Log.e(TAG, "an error occured when collect package info", e);
  }
  //获取 Build 中定义的变量, 使用反射方式获取, 该类中定义了设备相关的变量信息
  Field[] fields = Build.class.getDeclaredFields();
  //遍历获取额变量, 将这些信息存放到成员变量 Map<String, String> mInfos 中
  for (Field field : fields) {
    try {
    //设置 Build 成员变量可访问
    field.setAccessible(true);
    //将 设备相关的信息存放到 mInfos 成员变量中
    mInfos.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 返回文件名称,便于将文件传送到服务器
  */
  private String saveCrashInfo2File(Throwable ex) {
  //存储相关的字符串信息
  StringBuffer sb = new StringBuffer();
  //将成员变量 Map<String, String> mInfos  中的数据 存储到 StringBuffer sb 中
  for (Map.Entry<String, String> entry : this.mInfos.entrySet()) {
    String key = entry.getKey();
    String value = entry.getValue();
    sb.append(key + "=" + value + "\n");
  }
  //将 StringBuffer sb 中的字符串写出到文件中
  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 time = formatter.format(new Date());
    String fileName = "crash-" + time + "-" + timestamp + ".txt";
    if (Environment.getExternalStorageState().equals(
      Environment.MEDIA_MOUNTED)) {
    //获取文件输出路径
    String path = Environment.getExternalStorageDirectory()
      + "/crashinfo/";
    //创建文件夹和文件
    File dir = new File(path);
    if (!dir.exists()) {
      dir.mkdirs();
    }
    //创建输出流
    FileOutputStream fos = new FileOutputStream(path + fileName);
    //向文件中写出数据
    fos.write(sb.toString().getBytes());
    fos.close();
    }
    return fileName;
  } catch (Exception e) {
    Log.e(TAG, "an error occured while writing file...", e);
  }
  return null;
  }
}





(2) 结果示例




崩溃日志存放文件路径 : /storage/sdcard0/crashinfo/crash-2015-04-27-19-31-41-1430134301642.txt;


-- 说明 : 其中 /storage/sdcard0/ 是系统默认的 SD 卡路径, crashinfo/crash-2015-04-27-19-31-41-1430134301642.txt 是我们创建的文件;




崩溃日志内容 :



1430134301642.txt                                                             <
HARDWARE=pxa1088
RADIO=unknown
versionCode=1
HOST=SWDA2601
TAGS=release-keys
ID=JDQ39
MANUFACTURER=samsung
TYPE=user
IS_TRANSLATION_ASSISTANT_ENABLED=false
IS_SECURE=false
TIME=1416298944000
FINGERPRINT=samsung/wilcoxdszn/wilcoxds:4.2.2/JDQ39/G3812ZNUANK1:user/release-keys
UNKNOWN=unknown
BOARD=PXA1088
PRODUCT=wilcoxdszn
versionName=1.0
DISPLAY=JDQ39.G3812ZNUANK1
USER=se.infra
DEVICE=wilcoxds
MODEL=SM-G3812
BOOTLOADER=unknown
CPU_ABI=armeabi-v7a
CPU_ABI2=armeabi
IS_SYSTEM_SECURE=false
IS_DEBUGGABLE=false
SERIAL=5202889565301100
BRAND=samsung
java.lang.IllegalStateException: Could not execute method of the activity
  at android.view.View$1.onClick(View.java:3804)
  at android.view.View.performClick(View.java:4439)
  at android.widget.Button.performClick(Button.java:142)
  at android.view.View$PerformClick.run(View.java:18395)
  at android.os.Handler.handleCallback(Handler.java:725)
  at android.os.Handler.dispatchMessage(Handler.java:92)
  at android.os.Looper.loop(Looper.java:176)
  at android.app.ActivityThread.main(ActivityThread.java:5319)
  at java.lang.reflect.Method.invokeNative(Native Method)
  at java.lang.reflect.Method.invoke(Method.java:511)
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1102)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:869)
  at dalvik.system.NativeStart.main(Native Method)
Caused by: java.lang.reflect.InvocationTargetException
  at java.lang.reflect.Method.invokeNative(Native Method)
  at java.lang.reflect.Method.invoke(Method.java:511)
  at android.view.View$1.onClick(View.java:3799)
  ... 12 more
Caused by: java.lang.ArithmeticException: divide by zero
  at cn.org.octpus.crash.MainActivity.onClick(MainActivity.java:20)
  ... 15 more
java.lang.reflect.InvocationTargetException
  at java.lang.reflect.Method.invokeNative(Native Method)
  at java.lang.reflect.Method.invoke(Method.java:511)
  at android.view.View$1.onClick(View.java:3799)
  at android.view.View.performClick(View.java:4439)
  at android.widget.Button.performClick(Button.java:142)
  at android.view.View$PerformClick.run(View.java:18395)
  at android.os.Handler.handleCallback(Handler.java:725)
  at android.os.Handler.dispatchMessage(Handler.java:92)
  at android.os.Looper.loop(Looper.java:176)
  at android.app.ActivityThread.main(ActivityThread.java:5319)
  at java.lang.reflect.Method.invokeNative(Native Method)
  at java.lang.reflect.Method.invoke(Method.java:511)
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1102)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:869)
  at dalvik.system.NativeStart.main(Native Method)
Caused by: java.lang.ArithmeticException: divide by zero
  at cn.org.octpus.crash.MainActivity.onClick(MainActivity.java:20)
  ... 15 more
java.lang.ArithmeticException: divide by zero
  at cn.org.octpus.crash.MainActivity.onClick(MainActivity.java:20)
  at java.lang.reflect.Method.invokeNative(Native Method)
  at java.lang.reflect.Method.invoke(Method.java:511)
  at android.view.View$1.onClick(View.java:3799)
  at android.view.View.performClick(View.java:4439)
  at android.widget.Button.performClick(Button.java:142)
  at android.view.View$PerformClick.run(View.java:18395)
  at android.os.Handler.handleCallback(Handler.java:725)
  at android.os.Handler.dispatchMessage(Handler.java:92)
  at android.os.Looper.loop(Looper.java:176)
  at android.app.ActivityThread.main(ActivityThread.java:5319)
  at java.lang.reflect.Method.invokeNative(Native Method)
  at java.lang.reflect.Method.invoke(Method.java:511)
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1102)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:869)
  at dalvik.system.NativeStart.main(Native Method)







相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
7月前
|
Android开发
Android应用开发权限
Android应用开发权限
44 1
|
9月前
|
XML Java 测试技术
车载Android应用开发与分析 - SystemUI 「功能」与「源码结构」分析
本期内容开始,我们将介绍原生Android Automotive中车载应用的实现方式和它的原理。首先要介绍的就是车载应用开发中非常重要的一个系统应用,Android系统的UI - SystemUI。
534 1
车载Android应用开发与分析 - SystemUI 「功能」与「源码结构」分析