需要处理未捕获的异常并发送日志文件

时间:2013-11-11 01:43:14

标签: android exception-handling logcat

更新:请参阅下面的“已接受”解决方案

当我的应用创建未处理的异常,而不是简单地终止时,我想首先让用户有机会发送日志文件。我意识到在获得随机异常后做更多的工作是有风险的,但是,嘿,最糟糕的是应用程序完成崩溃并且日志文件没有被发送。事实证明这比我预期的更棘手:)

什么有效:(1)捕获未捕获的异常,(2)提取日志信息并写入文件。

什么不起作用:(3)开始发送电子邮件的活动。最终,我还有另一项活动要求用户的许可。如果我让电子邮件活动正常工作,我不会对另一方感到麻烦。

问题的症结在于我的Application类中捕获了未处理的异常。由于这不是一个Activity,因此如何使用Intent.ACTION_SEND启动活动并不明显。也就是说,通常要启动一个活动,就会调用startActivity并使用onActivityResult恢复。这些方法受Activity支持,但不受Application支持。

有关如何执行此操作的任何建议吗?

以下是一些代码剪辑作为入门指南:

public class MyApplication extends Application
{
  defaultUncaughtHandler = Thread.getDefaultUncaughtExceptionHandler();
  public void onCreate ()
  {
    Thread.setDefaultUncaughtExceptionHandler (new Thread.UncaughtExceptionHandler()
    {
      @Override
      public void uncaughtException (Thread thread, Throwable e)
      {
        handleUncaughtException (thread, e);
      }
    });
  }

  private void handleUncaughtException (Thread thread, Throwable e)
  {
    String fullFileName = extractLogToFile(); // code not shown

    // The following shows what I'd like, though it won't work like this.
    Intent intent = new Intent (Intent.ACTION_SEND);
    intent.setType ("plain/text");
    intent.putExtra (Intent.EXTRA_EMAIL, new String[] {"me@mydomain.com"});
    intent.putExtra (Intent.EXTRA_SUBJECT, "log file");
    intent.putExtra (Intent.EXTRA_STREAM, Uri.parse ("file://" + fullFileName));
    startActivityForResult (intent, ACTIVITY_REQUEST_SEND_LOG);
  }

  public void onActivityResult (int requestCode, int resultCode, Intent data)
  {
    if (requestCode == ACTIVITY_REQUEST_SEND_LOG)
      System.exit(1);
  }
}

9 个答案:

答案 0 :(得分:226)

这是完整的解决方案(几乎:我省略了UI布局和按钮处理) - 来自大量实验以及其他与其他问题相关的帖子。

您需要做很多事情:

  1. 处理Application子类中的uncaughtException。
  2. 捕获异常后,启动新活动以要求用户发送 一个日志。
  3. 从logcat的文件中提取日志信息并写入您的 自己的文件。
  4. 启动电子邮件应用,将您的文件作为附件提供。
  5. Manifest:过滤您的活动,以便您的异常处理程序识别。
  6. 或者,设置Proguard以去除Log.d()和Log.v()。
  7. 现在,详情如下:

    (1& 2)处理uncaughtException,开始发送日志活动:

    public class MyApplication extends Application
    {
      public void onCreate ()
      {
        // Setup handler for uncaught exceptions.
        Thread.setDefaultUncaughtExceptionHandler (new Thread.UncaughtExceptionHandler()
        {
          @Override
          public void uncaughtException (Thread thread, Throwable e)
          {
            handleUncaughtException (thread, e);
          }
        });
      }
    
      public void handleUncaughtException (Thread thread, Throwable e)
      {
        e.printStackTrace(); // not all Android versions will print the stack trace automatically
    
        Intent intent = new Intent ();
        intent.setAction ("com.mydomain.SEND_LOG"); // see step 5.
        intent.setFlags (Intent.FLAG_ACTIVITY_NEW_TASK); // required when starting from Application
        startActivity (intent);
    
        System.exit(1); // kill off the crashed app
      }
    }
    

    (3)提取日志(我把它作为我的SendLog活动):

    private String extractLogToFile()
    {
      PackageManager manager = this.getPackageManager();
      PackageInfo info = null;
      try {
        info = manager.getPackageInfo (this.getPackageName(), 0);
      } catch (NameNotFoundException e2) {
      }
      String model = Build.MODEL;
      if (!model.startsWith(Build.MANUFACTURER))
        model = Build.MANUFACTURER + " " + model;
    
      // Make file name - file must be saved to external storage or it wont be readable by
      // the email app.
      String path = Environment.getExternalStorageDirectory() + "/" + "MyApp/";
      String fullName = path + <some name>;
    
      // Extract to file.
      File file = new File (fullName);
      InputStreamReader reader = null;
      FileWriter writer = null;
      try
      {
        // For Android 4.0 and earlier, you will get all app's log output, so filter it to
        // mostly limit it to your app's output.  In later versions, the filtering isn't needed.
        String cmd = (Build.VERSION.SDK_INT <= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) ?
                      "logcat -d -v time MyApp:v dalvikvm:v System.err:v *:s" :
                      "logcat -d -v time";
    
        // get input stream
        Process process = Runtime.getRuntime().exec(cmd);
        reader = new InputStreamReader (process.getInputStream());
    
        // write output stream
        writer = new FileWriter (file);
        writer.write ("Android version: " +  Build.VERSION.SDK_INT + "\n");
        writer.write ("Device: " + model + "\n");
        writer.write ("App version: " + (info == null ? "(null)" : info.versionCode) + "\n");
    
        char[] buffer = new char[10000];
        do 
        {
          int n = reader.read (buffer, 0, buffer.length);
          if (n == -1)
            break;
          writer.write (buffer, 0, n);
        } while (true);
    
        reader.close();
        writer.close();
      }
      catch (IOException e)
      {
        if (writer != null)
          try {
            writer.close();
          } catch (IOException e1) {
          }
        if (reader != null)
          try {
            reader.close();
          } catch (IOException e1) {
          }
    
        // You might want to write a failure message to the log here.
        return null;
      }
    
      return fullName;
    }
    

    (4)启动电子邮件应用程序(也在我的SendLog活动中):

    private void sendLogFile ()
    {
      String fullName = extractLogToFile();
      if (fullName == null)
        return;
    
      Intent intent = new Intent (Intent.ACTION_SEND);
      intent.setType ("plain/text");
      intent.putExtra (Intent.EXTRA_EMAIL, new String[] {"log@mydomain.com"});
      intent.putExtra (Intent.EXTRA_SUBJECT, "MyApp log file");
      intent.putExtra (Intent.EXTRA_STREAM, Uri.parse ("file://" + fullName));
      intent.putExtra (Intent.EXTRA_TEXT, "Log file attached."); // do this so some email clients don't complain about empty body.
      startActivity (intent);
    }
    

    (3&amp; 4)这就是SendLog的样子(不过你必须添加UI):

    public class SendLog extends Activity implements OnClickListener
    {
      @Override
      public void onCreate(Bundle savedInstanceState)
      {
        super.onCreate(savedInstanceState);
        requestWindowFeature (Window.FEATURE_NO_TITLE); // make a dialog without a titlebar
        setFinishOnTouchOutside (false); // prevent users from dismissing the dialog by tapping outside
        setContentView (R.layout.send_log);
      }
    
      @Override
      public void onClick (View v) 
      {
        // respond to button clicks in your UI
      }
    
      private void sendLogFile ()
      {
        // method as shown above
      }
    
      private String extractLogToFile()
      {
        // method as shown above
      }
    }
    

    (5)清单:

    <manifest xmlns:android="http://schemas.android.com/apk/res/android" ... >
        <!-- needed for Android 4.0.x and eariler -->
        <uses-permission android:name="android.permission.READ_LOGS" /> 
    
        <application ... >
            <activity
                android:name="com.mydomain.SendLog"
                android:theme="@android:style/Theme.Dialog"
                android:textAppearance="@android:style/TextAppearance.Large"
                android:windowSoftInputMode="stateHidden">
                <intent-filter>
                  <action android:name="com.mydomain.SEND_LOG" />
                  <category android:name="android.intent.category.DEFAULT" />
                </intent-filter>
            </activity>
         </application>
    </manifest>
    

    (6)设置Proguard:

    在project.properties中,更改配置行。您必须指定“优化”或Proguard将删除Log.v()和Log.d()调用。

    proguard.config=${sdk.dir}/tools/proguard/proguard-android-optimize.txt:proguard-project.txt
    

    在proguard-project.txt中,添加以下内容。这告诉Proguard假设Log.v和Log.d没有副作用(即使它们写入日志时也是如此)因此可以在优化期间删除:

    -assumenosideeffects class android.util.Log {
        public static int v(...);
        public static int d(...);
    }
    

    就是这样!如果您对此有任何改进建议,请告诉我,我可以更新。

答案 1 :(得分:8)

今天有很多崩溃转发工具可以轻松完成这项工作。

  1. crashlytics - 崩溃报告工具,免费提供基本报告 优点:免费

  2. Gryphonet - 更高级的报告工具,需要一些费用。 优点:轻松恢复崩溃,ANR,缓慢......

  3. 如果您是私人开发者,我会建议Crashlytics,但如果它是一个大型组织,我会去Gryphonet。

    祝你好运!

答案 2 :(得分:5)

尝试使用ACRA - 它会处理堆栈跟踪以及大量其他有用的调试信息到您的后端或您已设置的Google Docs文档。

https://github.com/ACRA/acra

答案 3 :(得分:4)

当UI线程抛出未捕获的异常时,@ PeriHartman的答案很有效。当非UI线程抛出未捕获的异常时,我做了一些改进。

public boolean isUIThread(){
    return Looper.getMainLooper().getThread() == Thread.currentThread();
}

public void handleUncaughtException(Thread thread, Throwable e) {
    e.printStackTrace(); // not all Android versions will print the stack trace automatically

    if(isUIThread()) {
        invokeLogActivity();
    }else{  //handle non UI thread throw uncaught exception

        new Handler(Looper.getMainLooper()).post(new Runnable() {
            @Override
            public void run() {
                invokeLogActivity();
            }
        });
    }
}

private void invokeLogActivity(){
    Intent intent = new Intent ();
    intent.setAction ("com.mydomain.SEND_LOG"); // see step 5.
    intent.setFlags (Intent.FLAG_ACTIVITY_NEW_TASK); // required when starting from Application
    startActivity (intent);

    System.exit(1); // kill off the crashed app
}

答案 4 :(得分:2)

很好地解释。 但是这里有一个观察,我没有使用File Writer和Streaming写入文件,而是直接使用了logcat -f选项。这是代码

String[] cmd = new String[] {"logcat","-f",filePath,"-v","time","<MyTagName>:D","*:S"};
        try {
            Runtime.getRuntime().exec(cmd);
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

这有助于我刷新最新的缓冲信息。使用文件流给我一个问题,它没有从缓冲区刷新最新的日志。 但无论如何,这是非常有用的指南。谢谢。

答案 5 :(得分:1)

处理未捕获的异常: 正如@gilm解释的那样,做到这一点,(kotlin):

private val defaultUncaughtHandler = Thread.getDefaultUncaughtExceptionHandler();

override fun onCreate() {
  ...
  Thread.setDefaultUncaughtExceptionHandler { t, e -> 
    CustomFuncCrashes.trackError(e);
    defaultUncaughtHandler(t, e);  // just pass the call to 'defaultUncaughtHandler' when you're done
  }
}

我希望它能对我有用。.(:y)。 就我而言,我使用了“ com.microsoft.appcenter.crashes.Crashes”库进行错误跟踪。

答案 6 :(得分:0)

您可以使用FireCrasher库处理未捕获的异常,并从中恢复。

您可以在此Medium Article

中进一步了解图书馆

答案 7 :(得分:0)

这是我根据 Peri 的回答和 gilm 的评论设计的解决方案。

创建以下 Kotlin UncaughtExceptionHandler 类:

import android.content.Context
import android.content.Intent
import com.you.website.presentation.activity.MainActivity
import java.io.StringWriter
import android.os.Process
import android.util.Log
import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlin.system.exitProcess

/**
 * When a crash occurs, Android attempts to restart the current and preceding activity by default.
 * This is not desired functionality because in most apps there is not enough data persisted to properly recreate recreate the activities.
 * This can result in restarting with "Welcome, null" at the top of the page.
 * Instead, when a crash occurs, we want the app to crash gracefully and restart from its initial screen.
 * This UncaughtExceptionHandler catches uncaught exceptions and returns to the its initial startup Activity.
 */
class UncaughtExceptionHandler(val context: Context, private val defaultUncaughtHandler: Thread.UncaughtExceptionHandler?) : Thread.UncaughtExceptionHandler {
    override fun uncaughtException(thread: Thread, exception: Throwable) {
        // Begin the main activity so when the app is killed, we return to it instead of the currently active activity.
        val intent = Intent(context, MainActivity::class.java)
        context.startActivity(intent)
        // Return to the normal flow of an uncaught exception
        if (defaultUncaughtHandler != null) {
            defaultUncaughtHandler.uncaughtException(thread, exception)
        } else {
            // This scenario should never occur. It can only happen if there was no defaultUncaughtHandler when the handler was set up.
            val stackTrace = StringWriter()
            System.err.println(stackTrace) // print exception in the 'run' tab.
            Log.e("UNCAUGHT_EXCEPTION", "exception", exception) // print exception in 'Logcat' tab.
            FirebaseCrashlytics.getInstance().recordException(exception) // Record exception in Firebase Crashlytics
            Process.killProcess(Process.myPid())
            exitProcess(0)
        }
    }
}

将以下内容添加到 MainActivity 的 onCreate() 方法(要打开的第一个活动):

Thread.setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler(this, Thread.getDefaultUncaughtExceptionHandler()))

答案 8 :(得分:0)

我修改了 Peri 的回答,这非常有帮助。不是使用 Logcat,而是通过意图 putExtra 将 Throwable 发送到 SEND_LOG。然后循环遍历 SEND_LOG 中的堆栈跟踪。字符串不是使用电子邮件,而是发布到网页上。

附加到他的答案的代码摘录:

申请

auto get() -> const char & {
  const char &c = *_s;
  ++_s;
  return c;
}

发送日志

public void handleUncaughtException (Thread thread, Throwable e)
    {
        e.printStackTrace();

        // Launch error handler
        Intent intent = new Intent ();
        intent.setAction ("com.mydomain.SEND_LOG");
        intent.setFlags (Intent.FLAG_ACTIVITY_NEW_TASK); from Application
        intent.putExtra("com.mydomain.thrownexception",e);
        startActivity (intent);

        Runtime.getRuntime().exit(1);
    }

(我没有足够的分数来评论他的回答)