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


114

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

当我的应用程序创建未处理的异常(而不是简单终止)时,我想首先给用户提供发送日志文件的机会。我意识到在遇到随机异常后进行更多工作是有风险的,但是,最糟糕的是,应用程序崩溃了,日志文件也没有发送出去。事实证明这比我预期的要棘手:)

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

尚无法解决的问题:(3)开始一项活动来发送电子邮件。最终,我将进行另一项活动来征求用户的许可。如果我的电子邮件活动正常进行,则不会给其他人带来麻烦。

问题的症结在于未处理的异常在我的Application类中被捕获。由于这不是活动,因此如何使用Intent.ACTION_SEND启动活动尚不清楚。也就是说,通常要启动一个活动,就调用startActivity并以onActivityResult继续。这些方法受“活动”支持,但不受“应用程序”支持。

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

以下是一些代码片段作为入门指南:

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

4
我个人只是使​​用ACRA,尽管它是开源的,所以您可以检查一下它们是如何做的……
David O'Meara

Answers:


240

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

您需要做很多事情:

  1. 处理Application子类中的uncaughtException。
  2. 捕获异常后,开始一个新的活动,要求用户发送日志。
  3. 从logcat的文件中提取日志信息,然后写入您自己的文件。
  4. 启动电子邮件应用程序,将您的文件作为附件提供。
  5. 清单:过滤活动以供异常处理程序识别。
  6. (可选)设置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活动中):

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

而已!如果您对此有任何改进的建议,请告诉我,我可能会对此进行更新。


10
请注意:永远不要调用System.exit。您将打破未捕获的异常处理程序链。只是将其转发到下一个。您已经从getDefaultUncaughtExceptionHandler获得了“ defaultUncaughtHandler”,只需在完成后将调用传递给它即可。
gilm 2014年

2
@gilm,您能否提供一个示例,说明如何“将其转发到下一个”以及处理程序链中可能发生的其他情况?已经有一段时间了,但是我测试了许多场景,并且调用System.exit()似乎是最好的解决方案。毕竟,该应用已崩溃,需要终止。
佩里·哈特曼2014年

我想我记得如果让未捕获的异常继续发生会发生什么情况:系统会发出“应用程序终止”通知。通常,那很好。但是由于新活动(发送带有日志的电子邮件)已经发出了自己的消息,因此让系统发出另一个消息令人感到困惑。所以我强迫它通过System.exit()悄悄中止。
佩里·哈特曼2014年

8
@PeriHartman当然:只有一个默认处理程序。在调用setDefaultUncaughtExceptionHandler()之前,必须调用getDefaultUncaughtExceptionHandler()并保留该引用。因此,假设您有bugsense,crashlytics,并且您的处理程序是最后安装的,则系统只会调用您的处理程序。您的任务是调用您通过getDefaultUncaughtExceptionHandler()收到的引用,并将该线程和可抛出的对象传递给下一链。如果您只是System.exit(),则不会调用其他人。
gilm 2014年

4
您还需要在清单中使用Application标签中的属性注册您的Application实现,例如:<application android:name =“ com.mydomain.MyApplication” other attrs ... />
Matt

9

如今,有许多崩溃预防工具可以轻松地做到这一点。

  1. crashlytics-一个崩溃报告工具,免费,但可以为您提供基本报告优点:免费

  2. Gryphonet-更高级的报告工具,需要某种费用。优点:容易恢复崩溃,ANR,缓慢...

如果您是私人开发人员,我会建议您使用Crashlytics,但如果它是一个大型组织,那么我会推荐Gryphonet。

祝好运!



5

当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
}

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

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


我相信我尝试过(或类似的尝试)。如果我记得,我遇到了许可问题。您是在有根设备上执行此操作吗?
Peri Hartman'4

哦,如果您感到困惑,很抱歉,但是到目前为止,我正在尝试使用我的模拟器。我尚未将其移植到设备上(但是,是的,我用于测试的设备是有根的设备)。
schow 2014年

2

处理未捕获的异常: 正如@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”库进行错误跟踪。


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.