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

更新:请参阅下面的“接受”解决scheme

当我的应用程序创build一个未处理的exception而不是简单地终止时,我想先给用户一个发送日志文件的机会。 我意识到,得到一个随机exception后做更多的工作是有风险的,但是,嘿,最糟糕的是应用程序完成崩溃和日志文件不发送。 这是变得比我想象的更复杂:)

什么工作:(1)捕获未捕获的exception,(2)提取日志信息并写入文件。

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

问题的症结在于未处理的exception在我的Application类中被捕获。 由于这不是一个活动,所以如何使用Intent.ACTION_SEND启动一个活动并不明显。 也就是说,通常要启动一个调用startActivity的活动,并使用onActivityResult继续。 这些方法由Activity支持,而不是由Application支持。

任何build议如何做到这一点?

以下是一些代码片段作为起始指南:

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

下面是完整的解决scheme(差不多:我省略了UI布局和button处理) – 源于大量的实验和来自其他人的各种post,涉及到一路走来的问题。

有很多事情你需要做:

  1. 在你的Application子类中处理uncaughtException。
  2. 捕捉到exception后,开始一个新的活动,要求用户发送日志。
  3. 从logcat文件中提取日志信息并写入您自己的文件。
  4. 启动一个电子邮件应用程序,提供您的文件作为附件。
  5. 清单:筛选您的活动,以便您的exception处理程序识别。
  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中,更改configuration行。 您必须指定“优化”或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(...); } 

而已! 如果您有任何改进build议,请让我知道,我可能会更新这个。

今天有许多崩溃修复工具可以轻松完成这个任务。

  1. crashlytics – 一个崩溃报告工具,免费,但给你基本报告优点:免费

  2. Gryphonet – 一个更高级的报告工具,需要一些费用。 优点:易于娱乐的崩溃,ANR的,慢…

如果你是一个私人开发者,我会build议Crashlytics,但是如果它是一个大的组织,我会去Gryphonet。

祝你好运!

尝试使用ACRA,它可以处理将堆栈跟踪以及大量其他有用的debugging信息发送到您的后端或您已经设置的Google文档。

https://github.com/ACRA/acra

当UI线程抛出未捕获的exception时,@ PeriHartman的回答很好用。 当非UI线程抛出未捕获的exception时,我做了一些改进。

 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 } 

很好地解释说。 但是在这里有一个观察,而不是使用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(); } 

这帮助我冲洗最新的缓冲区信息。 使用文件stream给了我一个问题,它不是从缓冲区刷新最新的日志。 但无论如何,这是真正有帮助的指导。 谢谢。