Cần xử lý ngoại lệ không cần thiết và gửi tệp nhật ký


114

CẬP NHẬT: Vui lòng xem giải pháp "được chấp nhận" bên dưới

Khi ứng dụng của tôi tạo ra một ngoại lệ chưa được xử lý, thay vì chỉ đơn giản là chấm dứt, trước tiên tôi muốn cho người dùng cơ hội gửi tệp nhật ký. Tôi nhận ra rằng làm thêm công việc sau khi nhận được một ngoại lệ ngẫu nhiên là rất rủi ro nhưng, này, điều tồi tệ nhất là ứng dụng kết thúc sự cố và tệp nhật ký không được gửi. Điều này hóa ra phức tạp hơn tôi mong đợi :)

Những gì hoạt động: (1) bẫy ngoại lệ không cần thiết, (2) trích xuất thông tin nhật ký và ghi vào tệp.

Những gì chưa hoạt động: (3) bắt đầu một hoạt động để gửi email. Cuối cùng, tôi sẽ có một hoạt động khác để yêu cầu sự cho phép của người dùng. Nếu tôi nhận được hoạt động email hoạt động, tôi không mong đợi nhiều rắc rối cho hoạt động khác.

Điểm mấu chốt của vấn đề là ngoại lệ chưa xử lý được mắc trong lớp Ứng dụng của tôi. Vì đó không phải là Hoạt động nên không rõ ràng về cách bắt đầu một hoạt động với Intent.ACTION_SEND. Tức là, thông thường để bắt đầu một hoạt động, người ta gọi startActivity và tiếp tục với onActivityResult. Các phương pháp này được hỗ trợ bởi Hoạt động nhưng không được Ứng dụng hỗ trợ.

Có bất cứ đề nghị nào cho việc làm như thế này hả?

Dưới đây là một số đoạn mã làm hướng dẫn bắt đầu:

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
Cá nhân tôi chỉ sử dụng ACRA , mặc dù nó là mã nguồn mở, do đó bạn có thể kiểm tra như thế nào họ làm điều đó ...
David O'Meara

Câu trả lời:


240

Đây là giải pháp hoàn chỉnh (hầu như: Tôi đã bỏ qua bố cục giao diện người dùng và xử lý nút) - có được từ rất nhiều thử nghiệm và nhiều bài đăng khác nhau của những người khác liên quan đến các vấn đề nảy sinh trong quá trình này.

Có một số điều bạn cần làm:

  1. Xử lý ngoại lệ uncaughtException trong lớp con Ứng dụng của bạn.
  2. Sau khi bắt được một ngoại lệ, hãy bắt đầu một hoạt động mới để yêu cầu người dùng gửi nhật ký.
  3. Trích xuất thông tin nhật ký từ các tệp của logcat và ghi vào tệp của riêng bạn.
  4. Khởi động ứng dụng email, cung cấp tệp của bạn dưới dạng tệp đính kèm.
  5. Tệp kê khai: lọc hoạt động của bạn để trình xử lý ngoại lệ của bạn nhận ra.
  6. Theo tùy chọn, thiết lập Proguard để loại bỏ Log.d () và Log.v ().

Bây giờ, đây là chi tiết:

(1 & 2) Xử lý ngoại lệ, bắt đầu hoạt động nhật ký gửi:

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) Trích xuất nhật ký (Tôi đặt đây là Hoạt động SendLog của tôi):

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) Bắt đầu một ứng dụng email (cũng trong Hoạt động SendLog của tôi):

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) Đây là giao diện của SendLog (mặc dù vậy, bạn sẽ phải thêm giao diện người dùng):

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) Bản kê khai:

<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) Thiết lập Proguard:

Trong project.properties, hãy thay đổi dòng cấu hình. Bạn phải chỉ định "tối ưu hóa" nếu không Proguard sẽ không loại bỏ các lệnh gọi Log.v () và Log.d ().

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

Trong proguard-project.txt, hãy thêm phần sau. Điều này yêu cầu Proguard giả sử Log.v và Log.d không có tác dụng phụ (mặc dù chúng có tác dụng phụ vì chúng ghi vào nhật ký) và do đó có thể bị xóa trong quá trình tối ưu hóa:

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

Đó là nó! Nếu bạn có bất kỳ đề xuất nào để cải thiện điều này, vui lòng cho tôi biết và tôi có thể cập nhật điều này.


10
Chỉ cần một lưu ý: không bao giờ gọi System.exit. Bạn sẽ phá vỡ chuỗi trình xử lý ngoại lệ không cần thiết. Chỉ cần chuyển tiếp nó đến tiếp theo. Bạn đã có "defaultUncaughtHandler" từ getDefaultUncaughtExceptionHandler, chỉ cần chuyển lệnh gọi đến nó khi bạn hoàn tất.
gilm

2
@gilm, bạn có thể cung cấp ví dụ về cách "chuyển tiếp nó sang thứ tiếp theo" và điều gì khác có thể xảy ra trong chuỗi xử lý không? Đã lâu rồi, nhưng tôi đã thử nghiệm một số tình huống và gọi System.exit () có vẻ là giải pháp tốt nhất. Sau cùng, ứng dụng đã bị lỗi và nó cần phải chấm dứt.
Peri Hartman

Tôi nghĩ rằng tôi nhớ lại những gì sẽ xảy ra nếu bạn để ngoại lệ không cần thiết tiếp tục: hệ thống đưa ra thông báo "ứng dụng đã chấm dứt". Thông thường, điều đó sẽ ổn. Nhưng vì hoạt động mới (gửi email kèm theo nhật ký) đã đưa ra thông điệp của riêng nó, nên việc hệ thống đưa ra một hoạt động khác là điều khó hiểu. Vì vậy, tôi buộc nó phải hủy bỏ một cách lặng lẽ với System.exit ().
Peri Hartman

8
@PeriHartman chắc chắn: chỉ có một trình xử lý mặc định. trước khi gọi setDefaultUncaughtExceptionHandler (), bạn phải gọi getDefaultUncaughtExceptionHandler () và giữ tham chiếu đó. Vì vậy, giả sử bạn có bugsense, crashlytics và trình xử lý của bạn được cài đặt lần cuối, hệ thống sẽ chỉ gọi của bạn. nhiệm vụ của bạn là gọi tham chiếu mà bạn đã nhận được qua getDefaultUncaughtExceptionExceptionHandler () và chuyển luồng và có thể ném cho chuỗi tiếp theo. nếu bạn chỉ System.exit (), những cái khác sẽ không được gọi.
gilm

4
Bạn cũng cần phải đăng ký thi lại ứng dụng trong biểu hiện với một thuộc tính trong thẻ Application, ví dụ: <ứng dụng Android: name = "com.mydomain.MyApplication" attrs khác ... />
Matt

9

Ngày nay có rất nhiều công cụ khắc phục sự cố có thể thực hiện điều này một cách dễ dàng.

  1. crashlytics - Một công cụ báo cáo sự cố, miễn phí nhưng cung cấp cho bạn các báo cáo cơ bản Ưu điểm: Miễn phí

  2. Gryphonet - Một công cụ báo cáo tiên tiến hơn, yêu cầu một số loại phí. Ưu điểm: Dễ dàng tái tạo các sự cố, ANR, độ chậm ...

Nếu bạn là một nhà phát triển tư nhân, tôi sẽ đề xuất Crashlytics, nhưng nếu đó là một tổ chức lớn, tôi sẽ chọn Gryphonet.

Chúc may mắn!


5

Thay vào đó, hãy thử sử dụng ACRA - nó xử lý việc gửi dấu vết ngăn xếp cũng như rất nhiều thông tin gỡ lỗi hữu ích khác tới phần phụ trợ của bạn hoặc tới tài liệu Google Documents mà bạn đã thiết lập.

https://github.com/ACRA/acra


5

Câu trả lời của @ PeriHartman hoạt động tốt khi chuỗi giao diện người dùng ném ra ngoại lệ không cần thiết. Tôi đã thực hiện một số cải tiến khi một chuỗi không phải giao diện người dùng đưa ra ngoại lệ không cần thiết.

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

Giải thích độc đáo. Nhưng một nhận xét ở đây, thay vì ghi vào tệp bằng File Writer và Streaming, tôi đã sử dụng trực tiếp tùy chọn logcat -f. Đây là mã

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

Điều này đã giúp tôi gửi thông tin bộ đệm mới nhất. Sử dụng tính năng Phát trực tuyến tệp đã cho tôi một vấn đề là nó không xóa nhật ký mới nhất khỏi bộ đệm. Nhưng dù sao, đây là hướng dẫn thực sự hữu ích. Cảm ơn bạn.


Tôi tin rằng tôi đã thử điều đó (hoặc một cái gì đó tương tự). Nếu tôi nhớ lại, tôi đã gặp vấn đề về quyền. Bạn có đang làm điều này trên thiết bị đã root không?
Peri Hartman

Ồ, tôi xin lỗi nếu tôi làm bạn bối rối, nhưng tôi vẫn đang cố gắng với trình giả lập của mình. Tôi vẫn chưa chuyển nó vào một thiết bị (nhưng có thiết bị tôi sử dụng để kiểm tra là thiết bị đã được root).
schow

2

Xử lý các Ngoại lệ chưa suy nghĩ: như @gilm đã giải thích, chỉ cần làm điều này, (kotlin):

private val defaultUncaughtHandler = Thread.getDefaultUncaughtExceptionHandler();

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

Tôi hy vọng nó sẽ hữu ích, nó đã làm việc cho tôi .. (: y). Trong trường hợp của tôi, tôi đã sử dụng thư viện 'com.microsoft.appcenter.crashes.Crashes' để theo dõi lỗi.


Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.