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

Posted

技术标签:

【中文标题】需要处理未捕获的异常并发送日志文件【英文标题】:Need to handle uncaught exception and send log file 【发布时间】:2013-11-22 17:45:11 【问题描述】:

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

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

工作原理:(1) 捕获未捕获的异常,(2) 提取日志信息并写入文件。

什么还不起作用:(3)开始一个活动来发送电子邮件。最终,我将进行另一项活动来征求用户的许可。如果我让电子邮件活动正常工作,我预计对方不会有太多麻烦。

问题的症结在于我的 Application 类中捕获了未处理的异常。由于那不是 Activity,因此如何使用 Intent.ACTION_SEND 启动 Activity 并不明显。也就是说,通常要启动一项活动,需要调用 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);
  

【问题讨论】:

我个人只使用ACRA,虽然它是开源的,所以你可以检查他们是如何做到的...... 【参考方案1】:

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

你需要做很多事情:

    在您的应用程序子类中处理 uncaughtException。 捕获异常后,启动一个新的活动来要求用户发送 一个日志。 从 logcat 的文件中提取日志信息并写入您的 自己的文件。 启动电子邮件应用程序,将您的文件作为附件提供。 清单:过滤您的活动以被您的异常处理程序识别。 (可选)设置 Proguard 以去除 Log.d() 和 Log.v()。

现在,这里是详细信息:

(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 Activity 中):

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 & 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(...);

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

【讨论】:

请注意:永远不要调用 System.exit。您将破坏未捕获的异常处理程序链。只是转发到下一个。您已经从 getDefaultUncaughtExceptionHandler 获得了“defaultUncaughtHandler”,完成后只需将调用传递给它。 @gilm,您能否提供一个有关如何“将其转发到下一个”的示例以及处理程序链中可能发生的其他情况?已经有一段时间了,但我测试了许多场景,调用 System.exit() 似乎是最好的解决方案。毕竟,应用已经崩溃,需要终止。 @PeriHartman 确定:只有一个默认处理程序。在调用 setDefaultUncaughtExceptionHandler() 之前,您必须调用 getDefaultUncaughtExceptionHandler() 并保留该引用。因此,假设您有 bugsense、crashlytics 并且您的处理程序是最后安装的,系统只会调用您的处理程序。您的任务是调用您通过 getDefaultUncaughtExceptionHandler() 收到的引用并将线程和可抛出的线程传递给链中的下一个。如果你只是 System.exit(),其他的都不会被调用。 您还需要在清单中使用Application标签中的属性注册您的Application实现,例如: @gilm - 我重新访问了这段代码并尝试转发到之前的默认处理程序。效果是显示系统“应用程序崩溃”对话框以及我的自定义对话框。似乎唯一可行的解​​决方案是调用 System.exit()。【参考方案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();
        

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

【讨论】:

我相信我尝试过(或类似的)。如果我记得,我遇到了权限问题。您是在有根设备上执行此操作吗? 哦,如果我让你感到困惑,我很抱歉,但到目前为止我正在尝试使用我的模拟器。我还没有把它移植到设备上(但是我用来测试的设备是有根的)。【参考方案3】:

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

https://github.com/ACRA/acra

【讨论】:

【参考方案4】:

@PeriHartman 的答案在 UI 线程抛出未捕获的异常时效果很好。当非 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

【讨论】:

【参考方案5】:

今天有许多崩溃报告工具可以轻松完成此操作。

    crashlytics - 崩溃报告工具,免费但为您提供基本报告 优点:免费

    Gryphonet - 更高级的报告工具,需要某种费用。 优点:轻松重现崩溃、ANR、缓慢...

如果您是私人开发人员,我会推荐 Crashlytics,但如果是大型组织,我会选择 Gryphonet。

祝你好运!

【讨论】:

【参考方案6】:

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

你可以在这个Medium Article了解更多关于图书馆的信息

【讨论】:

【参考方案7】:

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

private val defaultUncaughtHandler = Thread.getDefaultUncaughtExceptionHandler();

override fun onCreate() 
  //...
    Thread.setDefaultUncaughtExceptionHandler  t, e ->
        Crashlytics.logException(e)
        defaultUncaughtHandler?.uncaughtException(t, e)
    

我希望它有所帮助,它对我有用.. (:y)。 在我的例子中,我使用 'com.microsoft.appcenter.crashes.Crashes' 库进行错误跟踪。

【讨论】:

【参考方案8】:

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

创建以下 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()))

【讨论】:

有谁知道为什么在代码崩溃时附加调试器时这个解决方案不起作用? 看起来又漂亮又简单。自从我发布原始解决方案以来,发生了很多变化。感谢您的更新。【参考方案9】:

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

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

应用

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);
    

发送日志

public class SendLog extends AppCompatActivity 
    public Throwable thrownexception;

    @Override
    public void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);

        if (getIntent().hasExtra("com.mydomain.thrownexception")) 
            thrownexception = (Throwable) getIntent().getExtras().get("com.mydomain.thrownexception");
        
    

    private String getLogString() 
        String logToUpload = "";
        PackageManager manager = context.getPackageManager();
        PackageInfo info = null;
        try 
            info = manager.getPackageInfo(context.getPackageName(), 0);
         catch (PackageManager.NameNotFoundException e2) 
            e2.printStackTrace();
        
        String model = Build.MODEL;
        if (!model.startsWith(Build.MANUFACTURER))
            model = Build.MANUFACTURER + " " + model;

        try 
            logToUpload = logToUpload + "Android version | " + Build.VERSION.SDK_INT + "\n";
            logToUpload = logToUpload + "Device | " + model + "\n";
            logToUpload = logToUpload + "App version | " + (info == null ? "(null)" : info.versionCode) + "\n";
            logToUpload = logToUpload + "Exception | " + thrownException.toString();
         catch (Exception e) 
            e.printStackTrace();
            return null;
        
        logToUpload = logToUpload + stackTraceToString(thrownException);

        return logToUpload;


     public static String stackTraceToString(Throwable throwable) 
        String strStackTrace = "";
        strStackTrace = strStackTrace + "\nStack Trace |";
        strStackTrace = strStackTrace + throwable.toString();
        try 
            for (StackTraceElement element : throwable.getStackTrace()) 
                strStackTrace = strStackTrace + "\n" + element.toString();
            
            //  Suppressed exceptions 
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) 
                Throwable[] suppressed = throwable.getSuppressed();
                if (suppressed.length > 0) 
                    strStackTrace = strStackTrace + "\nSuppressed |";
                    for (Throwable elementThrowable : suppressed) 
                        strStackTrace = strStackTrace + "\n" + elementThrowable.toString();
                        for (StackTraceElement element : elementThrowable.getStackTrace()) 
                            strStackTrace = strStackTrace + "\n" + element.toString();
                        
                    
                
            
            //   Recursively call method to include stack traces of exceptions that caused the throwable
            if (throwable.getCause() != null) 
                strStackTrace = strStackTrace + "\nCaused By:";
                strStackTrace = strStackTrace + stackTraceToString(throwable.getCause());
            
         catch (Exception e) 
            e.printStackTrace();
        

    return strStackTrace;

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

【讨论】:

以上是关于需要处理未捕获的异常并发送日志文件的主要内容,如果未能解决你的问题,请参考以下文章

Celery 任务未捕获的异常未发送到 Sentry

获取Android崩溃crash信息并写入日志发送邮件

lua中怎么捕获错误异常信息

weblogicwar包,捕获的异常在哪看

lua中怎么捕获错误异常信息

TestFlight 崩溃报告