关于Crash和ANR以及应用保活

来源:互联网 发布:bi工具竞品 知乎 编辑:程序博客网 时间:2024/05/02 05:04

如何记录应用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”的属性,安卓系统会在应用被强杀的时候尝试重新启动。
原创粉丝点击