关于Crash和ANR以及应用保活

如何记录应用crash

首先,引起应用crash的可能是我们对于NullPointer等未进行合理的处理导致的,所以这些抛出的异常没有被很好的处理,那么如果我们的应用发生了这样的crash,应该怎么去监听呢?

解决办法:

设置默认的未处理异常处理机制,例如:

public class CrashHandler implements Thread.UncaughtExceptionHandler {
    private static final String TAG = "CrashHandler";
    private static CrashHandler instance = new CrashHandler();

    public static CrashHandler getInstance() {
        return instance;
    }

    @Override
    public void uncaughtException(Thread thread, Throwable throwable) {
        Calendar calendar = null;
        calendar = Calendar.getInstance();
        calendar.setTimeInMillis(SystemClock.currentThreadTimeMillis());
        Log.d(TAG, "uncaughtException: here will show crash!");
        Log.d(TAG, "uncaughtException: time = " + calendar.get(Calendar.HOUR_OF_DAY)
                + ":" + calendar.get(Calendar.MINUTE)
                + ":" + calendar.get(Calendar.SECOND)
                + ":" + calendar.get(Calendar.MILLISECOND));
        Log.d(TAG, "uncaughtException: thread = " + thread + " =  " + thread.getName());
        Log.e(TAG, "uncaughtException: ", throwable);
    }
}

在Application中设置

   Thread.setDefaultUncaughtExceptionHandler(CrashHandler.getInstance());

这种方式下,当出现了未处理的异常的时候,我们可以在这里尝试处理,如果可以搞定或者是偶然性的异常,我们可以在这里尝试重新启动我们的应用,而且经过我们的处理之后并不会弹出crash窗口。

通过广播监听crash和anr

Android系统运行中,也会去捕获应用的crash和anr,如果捕获到这种情况就会调用AMS的handleApplicationCrash的函数:

[-->ActivityManagerService.java::handleApplicationCrash]
public void handleApplicationCrash(IBinder app,
                   ApplicationErrorReport.CrashInfo crashInfo) {
 ProcessRecord r = findAppProcess(app, "Crash");
 ......
 //调用addErrorToDropBox函数,第一个参数是一个字符串,为“crash”
 addErrorToDropBox("crash", r, null, null, null, null, null, crashInfo);
 ......
}

接下来看addErrorToDropBox:

[-->ActivityManagerService.java::addErrorToDropBox]
public void addErrorToDropBox(String eventType,
          ProcessRecord process, ActivityRecord activity,
          ActivityRecord parent, String subject,
          final String report, final File logFile,
          final ApplicationErrorReport.CrashInfo crashInfo) {

  /*
  dropbox日志文件的命名有一定的规则,其前缀都是一个特定的tag(标签),
  tag由两部分组成,合起来是“进程类型_事件类型”。
  下边代码中的processClass函数返回该进程的类型,包括“system_server”、“system_app”
  和“data_app”3种。eventType用于指定事件类型,目前也有3种类型:“crash”、“wtf”
  (what a terrible failure)和“anr”
  */
  final String dropboxTag = processClass(process) + "_" + eventType;
  //获取DBMS Bn端的对象DropBoxManager
      final DropBoxManager dbox = (DropBoxManager)
              mContext.getSystemService(Context.DROPBOX_SERVICE);
    /*
    对于DBMS,不仅通过tag标识文件名,还可以根据配置的情况,允许或禁止特定tag日志
    文件的记录。isTagEnable将判断DBMS是否禁止该标签,如果该tag已被禁止,则不允许记
    录日志文件
    */
      if (dbox == null || !dbox.isTagEnabled(dropboxTag)) return;
      //创建一个StringBuilder,用于保存日志信息
      final StringBuilder sb = new StringBuilder(1024);
      appendDropBoxProcessHeaders(process, sb);
      ......//将信息保存到字符串sb中
       //单独启动一个线程用于向DBMS添加信息
      Thread worker = new Thread("Error dump: " + dropboxTag) {
          @Override
          public void run() {
              if (report != null) {
                  sb.append(report);
              }
              if (logFile != null) {
                  try {//如果有log文件,那么把log文件内容读到sb中
                      sb.append(FileUtils.readTextFile(logFile,
                              128 * 1024, "\n\n[[TRUNCATED]]"));
                  } ......
              }
              //读取crashInfo信息,一般记录的是调用堆栈信息
              if (crashInfo != null && crashInfo.stackTrace != null) {
                  sb.append(crashInfo.stackTrace);
              }
              String setting = Settings.Secure.ERROR_LOGCAT_PREFIX + dropboxTag;
             //查询Settings数据库,判断该tag类型的日志是否对所记录的信息有行数限制,例如某些tag的日志文件只准记录1000行的信息
            int lines = Settings.Secure.getInt(mContext.getContentResolver(),
                                                      setting, 0);
              if (lines > 0) {
                  sb.append("\n");
                   InputStreamReader input = null;
                  try {
                      //创建一个新进程以运行logcat,后面的参数都是logcat常用的参数
                      java.lang.Process logcat = new
                        ProcessBuilder("/system/bin/logcat",
                      "-v", "time", "-b", "events", "-b", "system", "-b",
                           "main", "-t", String.valueOf(lines))
                              .redirectErrorStream(true).start();
                   //由于新进程的输出已经重定向,因此这里可以获取最后lines行的信息,不熟悉ProcessBuidler的读者可以查看SDK中关于它的用法说明
                   ......
                 }
              }
             //调用DBMS的addText
              dbox.addText(dropboxTag, sb.toString());
          }
      };
      if (process == null || process.pid == MY_PID) {
          worker.run(); //如果是SystemServer进程崩溃了,则不能在别的线程执行
      } else {
          worker.start();
   }
}

很容易可以看出,上边主要是在生成错误日志,最后调用了addText将函数的内容传给了DBMS:

[-->DropBoxManager.java::addText]
public void addText(String tag, String data) {
/*
 mService和DBMS交互。DBMS对外只提供一个add函数用于日志添加,而DBM提供了3个函数,
 分别是addText、addData、addFile,以方便使用
*/
 try { mService.add(new Entry(tag, 0, data)); } ......
}

这里和service进行了交互,尝试去添加一个entry:

[-->DropBoxManagerService.java::add]
public void add(DropBoxManager.Entry entry) {
      File temp = null;
      OutputStream output = null;
      final String tag = entry.getTag();//先取出这个Entry的tag
      try {
          int flags = entry.getFlags();
          ......
          //做一些初始化工作,包括生成dropbox目录、统计当前已有的dropbox文件信息等
          init();
          if (!isTagEnabled(tag)) return;//如果该tag被禁止,则不能生成日志文件
          long max = trimToFit();
          long lastTrim = System.currentTimeMillis();
          //BlockSize一般是4KB
          byte[] buffer = new byte[mBlockSize];
          //从Entry中得到一个输入流。与Java I/O相关的类比较多,且用法非常灵活,建议读者阅读《Java编程思想》中“Java I/O系统”一章
          InputStream input = entry.getInputStream();
          ......
          int read = 0;
          while (read < buffer.length) {
              int n = input.read(buffer, read, buffer.length - read);
              if (n <= 0) break;
              read += n;
          }
          //先生成一个临时文件,命名方式为“drop线程id.tmp”
          temp = new File(mDropBoxDir, "drop" +
                       Thread.currentThread().getId() + ".tmp");
          int bufferSize = mBlockSize;
          if (bufferSize > 4096) bufferSize = 4096;
          if (bufferSize < 512) bufferSize = 512;
          FileOutputStream foutput = new FileOutputStream(temp);
          output = new BufferedOutputStream(foutput, bufferSize);
           //生成GZIP压缩文件
          if (read == buffer.length &&
               ((flags & DropBoxManager.IS_GZIPPED) == 0)) {
              output = new GZIPOutputStream(output);
              flags = flags | DropBoxManager.IS_GZIPPED;
          }
           /*
               DBMS很珍惜/data分区,若所生成文件的size大于一个BlockSize,
               则一定要先压缩
          */
          ...... //写文件,这段代码非常烦琐,其主要目的是尽量节省存储空间
          /*
          生成一个EntryFile对象,并保存到DBMS内部的一个数据容器中。
           DBMS除了负责生成文件外,还提供查询的功能,这个功能由getNextEntry完成。
           另外,刚才生成的临时文件在createEntry函数中会重命为带标签的名字,
           读者可自行分析createEntry函数
           */
          long time = createEntry(temp, tag, flags);
          temp = null;
          Intent dropboxIntent = new
                       Intent(DropBoxManager.ACTION_DROPBOX_ENTRY_ADDED);
          dropboxIntent.putExtra(DropBoxManager.EXTRA_TAG, tag);
          dropboxIntent.putExtra(DropBoxManager.EXTRA_TIME, time);
          if (!mBooted) {
              dropboxIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
          }
          //发送DROPBOX_ENTRY_ADDED广播。系统中目前还没有程序接收该广播
          mContext.sendBroadcast(dropboxIntent,
                          android.Manifest.permission.READ_LOGS);
      }......
}

可以看到这里会尝试去发送一个广播android.intent.action.DROPBOX_ENTRY_ADDED

所以我们如果定义一个广播接收器,接收广播即可捕获广播,不过这样子需要权限

    <uses-permission android:name="android.permission.READ_LOGS"/>

在安卓4.1的版本之上,为了LogCat的安全性,想要拿到此项权限必须是System签名或者Root的App或者adb才能看到这些东西,所以这种方式现在很难做到!

在Android4.4的版本上可以尝试使用如下代码(估计之后的版本都需要Root):

    public void requestPermission(){
        String pname = getPackageName();
        String[] CMDLINE_GRANTPERMS = { "su", "-c", null };
        if (getPackageManager().checkPermission(android.Manifest.permission.READ_LOGS, pname) != 0) {
            if (android.os.Build.VERSION.SDK_INT >= 16) {
                try {
                    // format the commandline parameter
                    CMDLINE_GRANTPERMS[2] = String.format("pm grant %s android.permission.READ_LOGS", pname);
                    java.lang.Process p = Runtime.getRuntime().exec(CMDLINE_GRANTPERMS);
                    int res = p.waitFor();
                    if (res != 0)
                        throw new Exception("failed to become root");
                } catch (Exception e) {
                }
            }
        }
    }

在Android6.0以上发现,动态申请权限毫无用处,尝试了各种手段后都无法申请,所以如果你搞定了这个,请联系我,联系方式:im.noclay@gmail.com。 谢谢Thanks♪(・ω・)ノ

如何对自己的应用保活

  1. 采用双进程保活机制,利用另一个进程的service对自己的应用判断存活与否,否则则重新启动自己的应用
  2. 将应用设为system应用,并添加persistent=”true”的属性,安卓系统会在应用被强杀的时候尝试重新启动。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值