포착되지 않은 예외를 처리하고 로그 파일을 보내야합니다.


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를 가 오픈 소스가 있지만 당신이 ... 그들이 그것을 어떻게 확인할 수 있도록,
데이비드 O'Meara는

답변:


240

여기에 완전한 솔루션이 있습니다 (거의 : UI 레이아웃과 버튼 처리를 생략했습니다)-많은 실험과 그 과정에서 발생한 문제와 관련된 다른 사람들의 다양한 게시물에서 파생되었습니다.

다음과 같은 여러 가지 작업을 수행해야합니다.

  1. Application 하위 클래스에서 uncaughtException을 처리합니다.
  2. 예외를 포착 한 후 새 활동을 시작하여 사용자에게 로그를 보내도록 요청하십시오.
  3. logcat의 파일에서 로그 정보를 추출하고 자신의 파일에 씁니다.
  4. 이메일 앱을 시작하여 파일을 첨부 파일로 제공합니다.
  5. 매니페스트 : 예외 처리기가 인식하도록 활동을 필터링합니다.
  6. 선택적으로 Log.d () 및 Log.v ()를 제거하도록 Proguard를 설정합니다.

이제 세부 정보는 다음과 같습니다.

(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에서 구성 줄을 변경합니다. "optimize"를 지정해야합니다. 그렇지 않으면 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

2
@gilm, "다음으로 전달"하는 방법과 핸들러 체인에서 다른 일이 발생할 수있는 방법에 대한 예제를 제공 할 수 있습니까? 오래되었지만 여러 시나리오를 테스트했으며 System.exit ()를 호출하는 것이 최상의 솔루션 인 것 같습니다. 결국 앱이 다운되어 종료해야합니다.
Peri Hartman 2014 년

잡히지 않은 예외를 계속하면 시스템이 "앱 종료 됨"알림을 표시하면 어떻게되는지 기억합니다. 일반적으로 괜찮습니다. 그러나 새로운 활동 (로그와 함께 이메일을 보내는)이 이미 자체 메시지를 게시하고 있기 때문에 시스템이 다른 메시지를 게시하는 것은 혼란 스럽습니다. 그래서 System.exit ()를 사용하여 조용히 강제로 중단합니다.
Peri Hartman

8
@PeriHartman 확실히 : 기본 핸들러는 하나뿐입니다. setDefaultUncaughtExceptionHandler ()를 호출하기 전에 getDefaultUncaughtExceptionHandler ()를 호출하고 해당 참조를 유지해야합니다. 따라서 bugsense, crashlytics가 있고 핸들러가 마지막으로 설치되었다고 가정하면 시스템은 귀하의 핸들러 만 호출합니다. getDefaultUncaughtExceptionHandler ()를 통해받은 참조를 호출하고 스레드를 전달하고 다음 체인에 throwable을 전달하는 것이 작업입니다. 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

@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 2014

오, 혼란스러워서 미안하지만 지금까지 내 에뮬레이터로 시도하고 있습니다. 아직 장치에 이식하지 않았습니다 (하지만 테스트에 사용하는 장치는 루팅 된 장치입니다).
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.