ต้องจัดการข้อยกเว้นที่ไม่ถูกจับและส่งไฟล์บันทึก


115

อัปเดต: โปรดดูโซลูชัน "ยอมรับ" ด้านล่าง

เมื่อแอปของฉันสร้างข้อยกเว้นที่ไม่สามารถจัดการได้แทนที่จะยุติเพียงแค่ฉันต้องการเปิดโอกาสให้ผู้ใช้ส่งไฟล์บันทึกก่อน ฉันตระหนักดีว่าการทำงานมากขึ้นหลังจากได้รับข้อยกเว้นแบบสุ่มนั้นมีความเสี่ยง แต่เดี๋ยวก่อนสิ่งที่แย่ที่สุดคือแอปขัดข้องและไฟล์บันทึกไม่ได้รับการส่ง นี่กลายเป็นเรื่องยากกว่าที่ฉันคาดไว้ :)

สิ่งที่ได้ผล: (1) การดักจับข้อยกเว้นที่ไม่ถูกจับ (2) การแยกข้อมูลบันทึกและการเขียนลงในไฟล์

สิ่งที่ยังใช้ไม่ได้: (3) เริ่มต้นกิจกรรมเพื่อส่งอีเมล ท้ายที่สุดฉันจะมีกิจกรรมอื่นเพื่อขออนุญาตของผู้ใช้ หากฉันได้รับกิจกรรมอีเมลที่ใช้งานได้ฉันไม่คาดหวังว่าอีกฝ่ายจะมีปัญหามากนัก

ปมของปัญหาคือข้อยกเว้นที่ไม่สามารถจัดการได้ถูกจับในคลาสแอปพลิเคชันของฉัน เนื่องจากนั่นไม่ใช่กิจกรรมจึงไม่ชัดเจนว่าจะเริ่มกิจกรรมด้วย 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);
  }
}

4
โดยส่วนตัวแล้วฉันใช้ACRAแม้ว่าจะเป็นโอเพ่นซอร์สดังนั้นคุณสามารถตรวจสอบได้ว่าพวกเขาทำอย่างไร ...
David O'Meara

คำตอบ:


241

นี่คือวิธีแก้ปัญหาที่สมบูรณ์ (เกือบ: ฉันละเค้าโครง UI และการจัดการปุ่ม) - ได้มาจากการทดลองจำนวนมากและโพสต์ต่างๆจากผู้อื่นที่เกี่ยวข้องกับปัญหาที่เกิดขึ้นระหว่างทาง

มีหลายสิ่งที่คุณต้องทำ:

  1. จัดการกับ uncaughtException ในคลาสย่อยแอปพลิเคชันของคุณ
  2. หลังจากตรวจพบข้อยกเว้นให้เริ่มกิจกรรมใหม่เพื่อขอให้ผู้ใช้ส่งบันทึก
  3. แยกข้อมูลบันทึกจากไฟล์ของ logcat และเขียนลงในไฟล์ของคุณเอง
  4. เริ่มแอปอีเมลโดยให้ไฟล์ของคุณเป็นไฟล์แนบ
  5. Manifest: กรองกิจกรรมของคุณให้รับรู้โดยตัวจัดการข้อยกเว้นของคุณ
  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 คุณจะทำลายห่วงโซ่ตัวจัดการข้อยกเว้นที่ไม่ถูกจับ เพียงแค่ส่งต่อไปยังรายการถัดไป คุณมี "defaultUncaughtHandler" จาก getDefaultUncaughtExceptionHandler อยู่แล้วเพียงแค่ส่งสายไปเมื่อคุณทำเสร็จแล้ว
gilm

2
@gilm คุณช่วยยกตัวอย่างวิธี "ส่งต่อไปยังรุ่นถัดไป" ได้หรือไม่และอาจเกิดอะไรขึ้นอีกในเครือข่ายตัวจัดการ เป็นเวลานานแล้ว แต่ฉันได้ทดสอบสถานการณ์หลายอย่างและการเรียก System.exit () ดูเหมือนจะเป็นทางออกที่ดีที่สุด ท้ายที่สุดแอปขัดข้องและจำเป็นต้องยุติ
Peri Hartman

ฉันคิดว่าฉันจำได้ว่าจะเกิดอะไรขึ้นหากคุณปล่อยให้ข้อยกเว้นที่ไม่ถูกตรวจจับดำเนินต่อไป: ระบบแจ้งว่า "แอปถูกยุติ" โดยปกตินั่นจะดี แต่เนื่องจากกิจกรรมใหม่ (ที่ส่งอีเมลพร้อมบันทึก) กำลังวางข้อความของตัวเองอยู่แล้วการที่ระบบวางระบบอื่นจึงทำให้เกิดความสับสน ดังนั้นฉันบังคับให้ยกเลิกอย่างเงียบ ๆ ด้วย System.exit ()
Peri Hartman

8
@PeriHartman แน่ใจว่ามีตัวจัดการเริ่มต้นเพียงตัวเดียว ก่อนที่จะเรียก setDefaultUncaughtExceptionHandler () คุณต้องเรียกใช้ getDefaultUncaughtExceptionHandler () และเก็บข้อมูลอ้างอิงนั้นไว้ ดังนั้นสมมติว่าคุณมีข้อบกพร่อง crashlytics และตัวจัดการของคุณได้รับการติดตั้งครั้งล่าสุดระบบจะโทรหาคุณเท่านั้น เป็นหน้าที่ของคุณที่จะเรียกการอ้างอิงที่คุณได้รับผ่าน getDefaultUncaughtExceptionHandler () และส่งผ่านเธรดและโยนไปยังลำดับถัดไปได้ ถ้าคุณแค่ System.exit () ระบบจะไม่เรียกคนอื่น ๆ
gilm

4
คุณต้องลงทะเบียนการใช้งานแอปพลิเคชันของคุณในรายการด้วยแอตทริบิวต์ในแท็กแอปพลิเคชันเช่น: <application android: name = "com.mydomain.MyApplication" อื่น ๆ ... />
แมตต์

9

ปัจจุบันมีเครื่องมือซ่อมแซมข้อผิดพลาดมากมายที่ทำได้อย่างง่ายดาย

  1. crashlytics - เครื่องมือรายงานข้อขัดข้องฟรี แต่ให้รายงานพื้นฐานข้อดี: ฟรี

  2. Gryphonet - เครื่องมือการรายงานขั้นสูงที่ต้องเสียค่าธรรมเนียมบางประเภท ข้อดี: เกิดข้อขัดข้องง่าย ANR ความช้า ...

หากคุณเป็นนักพัฒนาส่วนตัวฉันขอแนะนำ Crashlytics แต่ถ้าเป็นองค์กรขนาดใหญ่ฉันจะใช้ Gryphonet

โชคดี!


5

ลองใช้ ACRA แทนซึ่งจะจัดการการส่งการติดตามสแต็กรวมถึงข้อมูลการดีบักที่มีประโยชน์อื่น ๆ มากมายไปยังแบ็กเอนด์ของคุณหรือไปยังเอกสาร Google เอกสารที่คุณตั้งค่าไว้

https://github.com/ACRA/acra


5

คำตอบของ @ 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
}

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

โอ้ฉันขอโทษถ้าฉันทำให้คุณสับสน แต่ฉันก็พยายามกับอีมูเลเตอร์ของฉัน ฉันยังไม่ได้พอร์ตไปยังอุปกรณ์ (แต่ใช่อุปกรณ์ที่ฉันใช้ทดสอบเป็นอุปกรณ์ที่รูทแล้ว)
schow

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' สำหรับการติดตามข้อผิดพลาด


โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.