Yakalanmamış istisnayı ele alıp günlük dosyası göndermeniz gerekiyor


114

GÜNCELLEME: Lütfen aşağıdaki "kabul edilen" çözüme bakın

Uygulamam basitçe sonlandırmak yerine işlenmeyen bir istisna oluşturduğunda, öncelikle kullanıcıya bir günlük dosyası gönderme fırsatı vermek istiyorum. Rastgele bir istisna aldıktan sonra daha fazla iş yapmanın riskli olduğunun farkındayım, ancak en kötüsü uygulamanın çökmesini bitirmesi ve günlük dosyasının gönderilmemesidir. Bu beklediğimden daha yanıltıcı oluyor :)

İşe yarayan şey: (1) yakalanmamış istisnayı yakalamak, (2) günlük bilgilerini çıkarmak ve bir dosyaya yazmak.

Henüz işe yaramayanlar: (3) e-posta göndermek için bir etkinlik başlatmak. Nihayetinde, kullanıcının iznini istemek için başka bir etkinliğim daha olacak. E-posta etkinliğini çalışır hale getirirsem, diğeri için fazla sorun beklemiyorum.

Sorunun özü, işlenmeyen istisnanın Uygulama sınıfımda yakalanmasıdır. Bu bir Etkinlik olmadığı için, Intent.ACTION_SEND ile bir etkinliğe nasıl başlayacağınız açık değildir. Yani, normalde bir etkinliği başlatmak için startActivity çağrılır ve onActivityResult ile devam edilir. Bu yöntemler Aktivite tarafından desteklenir ancak Uygulama tarafından desteklenmez.

Bunu nasıl yapacağımıza dair herhangi bir tavsiye var mı?

İşte başlangıç ​​kılavuzu olarak bazı kod parçaları:

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
Şahsen ben sadece ACRA kullanıyorum , açık kaynak olmasına rağmen nasıl yaptıklarını kontrol edebilirsiniz ...
David O'Meara

Yanıtlar:


240

İşte tam çözüm (neredeyse: UI düzenini ve düğme işlemeyi ihmal ettim) - birçok deneyden ve yol boyunca ortaya çıkan sorunlarla ilgili diğerlerinden çeşitli gönderilerden türetildi.

Yapmanız gereken birkaç şey var:

  1. Uygulama alt sınıfınızda uncaughtException işleyin.
  2. Bir istisna yakaladıktan sonra, kullanıcıdan bir günlük göndermesini istemek için yeni bir etkinlik başlatın.
  3. Günlük bilgilerini logcat dosyalarından çıkarın ve kendi dosyanıza yazın.
  4. Dosyanızı ek olarak sağlayarak bir e-posta uygulaması başlatın.
  5. Manifest: etkinliğinizi istisna işleyiciniz tarafından tanınacak şekilde filtreleyin.
  6. İsteğe bağlı olarak, Log.d () ve Log.v () 'yi çıkarmak için Proguard'ı kurun.

Şimdi, ayrıntılar:

(1 ve 2) uncaughtException işleyin, günlük etkinliği göndermeye başlayın:

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) Günlüğü çıkar (bunu SendLog Etkinliğim olarak koydum):

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) Bir e-posta uygulaması başlatın (ayrıca SendLog Etkinliğimde):

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 ve 4) SendLog şöyle görünüyor (yine de kullanıcı arayüzünü eklemeniz gerekecek):

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) Bildiri:

<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) Kurulum Proguard'ı:

Project.properties içinde yapılandırma satırını değiştirin. Sen "optimize" belirtmelidir veya Proguard olacak değil Log.v () ve Log.d () çağrıları kaldırın.

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

Proguard-project.txt dosyasına aşağıdakileri ekleyin. Bu, Proguard'a Log.v ve Log.d'nin hiçbir yan etkisinin olmadığını varsaymasını söyler (günlüklere yazdıkları için yapsalar bile) ve bu nedenle optimizasyon sırasında kaldırılabilir:

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

Bu kadar! Bu konuda iyileştirmeler yapmak için herhangi bir öneriniz varsa lütfen bize bildirin, bunu güncelleyebilirim.


10
Sadece bir not: System.exit'i asla çağırmayın. Yakalanmamış istisna işleyicileri zincirini kıracaksınız. Sadece bir sonrakine iletin. GetDefaultUncaughtExceptionHandler'dan "defaultUncaughtHandler" zaten var, işiniz bittiğinde çağrıyı ona iletin.
gilm

2
@gilm, "onu bir sonrakine iletmek" ve işleyici zincirinde başka neler olabileceği konusunda bir örnek verebilir misiniz? Uzun zaman oldu, ancak birkaç senaryoyu test ettim ve System.exit () 'i çağırmak en iyi çözüm gibi görünüyordu. Sonuçta, uygulama çöktü ve sonlandırılması gerekiyor.
Peri Hartman

Sanırım yakalanmayan istisnanın devam etmesine izin verirseniz ne olacağını hatırlıyorum: sistem "uygulama sonlandırıldı" bildirimini koyar. Normalde bu iyi olur. Ancak yeni etkinlik (günlükle e-posta gönderen) zaten kendi mesajını oluşturduğundan, sistemin başka bir mesaj göndermesi kafa karıştırıcıdır. Bu yüzden System.exit () ile sessizce iptal etmeye zorladım.
Peri Hartman

8
@PeriHartman emin: sadece bir varsayılan işleyici var. setDefaultUncaughtExceptionHandler () öğesini çağırmadan önce getDefaultUncaughtExceptionHandler () öğesini çağırmalı ve bu referansı korumalısınız. Öyleyse, hata algılamanız, crashlytics'iniz olduğunu ve işleyicinizin en son yüklenen olduğunu varsayalım, sistem yalnızca sizinkini arayacaktır. getDefaultUncaughtExceptionHandler () aracılığıyla aldığınız referansı çağırmak ve zincirde bir sonraki evre ve fırlatılabilirliği iletmek sizin görevinizdir. yalnızca System.exit () olursanız, diğerleri çağrılmaz.
gilm

4
Ayrıca, manifest'teki Uygulama uygulamanızı Uygulama etiketindeki bir öznitelikle kaydetmeniz gerekir, örneğin: <application android: name = "com.mydomain.MyApplication" other attrs ... />
Matta

9

Bugün bunu kolayca yapan birçok kilitlenme yeniden yazma aracı var.

  1. crashlytics - Ücretsiz olan ancak size temel raporlar veren bir kilitlenme raporlama aracı Avantajlar: Ücretsiz

  2. Gryphonet - Daha gelişmiş bir raporlama aracı, bir tür ücret gerektirir. Avantajlar: Çökmelerin kolay yeniden oluşturulması, ANR'ler, yavaşlık ...

Özel bir geliştiriciyseniz, Crashlytics'i öneririm, ancak büyük bir organizasyonsa, Gryphonet'e giderim.

İyi şanslar!


5

Bunun yerine ACRA kullanmayı deneyin - yığın izlemenin yanı sıra tonlarca başka yararlı hata ayıklama bilgisinin arka ucunuza veya kurduğunuz Google Dokümanlar belgesine gönderilmesini sağlar.

https://github.com/ACRA/acra


5

@ PeriHartman'ın cevabı, UI iş parçacığı yakalanmamış istisna attığında iyi çalışıyor. Yakalanmamış istisnanın UI olmayan bir iş parçacığı tarafından atılması için bazı iyileştirmeler yaptım.

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

Güzelce açıkladı. Ancak burada bir gözlem, File Writer ve Streaming kullanarak dosyaya yazmak yerine, doğrudan logcat -f seçeneğini kullandım. İşte kod

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

Bu, en son arabellek bilgisini temizlememe yardımcı oldu. Dosya akışını kullanmak bana en son günlükleri arabellekten temizlemediğine dair bir sorun verdi. Ama her neyse, bu gerçekten yardımcı bir rehberdi. Teşekkür ederim.


Bunu denediğime inanıyorum (veya benzer bir şey). Hatırlarsam izin sorunları yaşadım. Bunu köklü bir cihazda mı yapıyorsunuz?
Peri Hartman

Oh, kafanı karıştırdıysam özür dilerim, ama şimdiye kadar öykünücülerimi deniyorum. Henüz bir cihaza aktarmadım (ama evet test etmek için kullandığım cihaz köklü bir cihazdır).
Schow

2

Yakalanmayan İstisnaları Ele Alma : @gilm'in açıkladığı gibi sadece şunu yapın, (kotlin):

private val defaultUncaughtHandler = Thread.getDefaultUncaughtExceptionHandler();

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

Umarım yardımcı olur, benim için çalıştı .. (: y). Benim durumumda, hata izleme için 'com.microsoft.appcenter.crashes.Crashes' kitaplığını kullandım.


Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.