Android ANR(App Not Responding) 분석

2019. 12. 9. 20:15

 

ANR(Application Not Responding)

 

  • 어플리케이션이 응답하지 않는 경우 안드로이드 시스템에서 보여주는 에러. 애플리케이션 응답 없음.
  • 일정 시간 충분한 속도로 응답하지 않는 경우 발생. Main Thread(일명, UI Thread)가 일정시간 동안 Hold되면 발생함.
  • 언제 발생하는가?
    • 입력 이벤트(Key, Touch)를 통한 사용자 입력이 5초 내에 처리되지 않았을 때
    • BroadcastReceiver가 10초 내에 처리하지 못했을 때
    • Service가 20초 내로 처리되지 않았을 때
  • Activity Manager 및 Window Manager 시스템 서비스에서 애플리케이션 응답성을 모니터링 함.

 

ANR을 유발하는 원인

 

  • Main Thread에서 I/O 작업
  • Main Thread에서 복잡한 동작 구현(오랜 시간을 소비하는)

 

ANR을 피하는 방법

 

  • 오랜 시간이 걸리는 작업을 수행하는 경우 Main Thread를 대신하는 Worker Thread를 활용
  • 사용자에게 프로그래스바 등을 이용해 작업의 진행 과정을 안내

 

ANR 분석

 

  • Easy to get and hard to fix
  • Strict mode with debugging: 개발자 모드에서 Strict Mode enabled하면 애플리케이션에서 긴 작업을 Main Thread에서 진행할 경우 Flash Screen 현상이 발생

    

 

  • Google Play: 구글 플레이에 Real-time ANRs를 통해 ANR을 확인

  • Systrace

  • Traceview: Android Studio 3.2의 경우 CPU Profiler

  • dmtracedump with Graphviz
  • Dumpstate:
    • "ANR in" Filter
    • SIG:9 시스템에서 ANR을 발생시키는 프로세스를 종료하기 위해 SIGNAL_KILL을 호출하면서 남기는 로그. 해당 로그의 전후를 확인
    • "am_anr" Filter
  • Detecting ANRs by Runnable
public class AnrSupervisor {
    private ExecutorService anrExecutor = ExecutorService.newSingleThreadExecutor();
             
    private final AnrSupervisorRunnable anrSupervisor = new AnrSupervisorRunnable();

    public synchronized void start() {
        synchronized (this.anrSupervisor) {
            if (this.anrSupervisor.isStopped()) {
                this.anrExecutor.execute(this.anrSupervisor);
            } else {
                this.anrSupervisor.unstop();
            }
        }
    }
    
    public synchronized void stop() {
        this.anrSupervisor.stop();
    }
}
private class AnrSupervisorRunnable implements Runnable {
    private Handler handler = new Handler(Looper.getMainLooper());
    private boolean isStopped;
    private boolean isSupervisionStopped = true;

    @Override
    public void run() {
        this.isRunnableStopped = false;

        while (!Thread.interrupted()) {
            try {
                Log.d(AnrSupervisor.class.getSimpleName(), "Check for ANR...");
                AnrSupervisorCallback callback = new AnrSupervisorCallback();

                synchronized (callback) {
                    this.mHandler.post(callback);
                    callback.wait(1000);

                    if (!callback.isCalled()) {
                        AnrException e = new AnrException(this.mHandler.getLooper().getThread());
                        FirebaseCrash.report(e);
                        e.logProcessMap();

                        callback.wait();
                    } else {
                        Log.d(AnrSupervisor.class.getSimpleName(), "UI Thread responded within 1s");
                    }
                }

                this.checkStopped();

                Thread.sleep(5000);
            } catch (InterruptedException e) {
                break;
            }
        }

        Log.d(AnrSupervisor.class.getSimpleName(), "ANR supervision stopped");
        this.isSupervisionStopped = true;
    }

    private synchronized void checkStopped() throws InterruptedException {
        if (this.mStopped) {
            Thread.sleep(1000);

            if (this.mStopped) {
                throw new InterruptedException();
            }
        }
    }

    synchronized void stop() {
        Log.d(AnrSupervisor.class.getSimpleName(), "Stopping...");
        this.isStopped = true;

    }

    synchronized void unstop() {
        Log.d(AnrSupervisor.class.getSimpleName(), "Revert stopping...");
        this.isStopped = false;

    }

    synchronized boolean isStopped() {
        return this.isSupervisionStopped;
    }
}
public class AnrException extends Exception {
    public AnrException(Thread thread) {
        super("ANR detected");
        this.setStackTrace(thread.getStackTrace());

    }

    public void logProcessMap() {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        PrintStream ps = new PrintStream(bos);
        this.printProcessMap(ps);
        Log.i(this.getClass().getSimpleName(), new String(bos.toByteArray()));
    }

    public void printProcessMap(PrintStream ps) {
        Map<Thread, StackTraceElement[]> stackTraces = Thread.getAllStackTraces();

        ps.println("Process map:");
        for (Thread thread : stackTraces.keySet()) {
            if (stackTraces.get(thread).length > 0) {
                this.printThread(ps, Locale.getDefault(), thread, stackTraces.get(thread));
                ps.println();
            }
        }
    }

    private void printThread(PrintStream ps, Locale l, Thread thread, StackTraceElement[] stack) {
        ps.println(String.format(l, "\t%s (%s)", thread.getName(), thread.getState()));

        for (StackTraceElement element : stack) {
            ps.println(String.format(l, "\t\t%s.%s(%s:%d)",
                    element.getClassName(),
                    element.getMethodName(),
                    element.getFileName(), 
                    element.getLineNumber()));
        }
    }
}
private class AnrSupervisorCallback implements Runnable {
    private boolean isCalled;

    public AnrSupervisorCallback() {
        super();
    }

    @Override
    public synchronized void run() {
        this.isCalled = true;
        this.notifyAll();

    }
  
    synchronized boolean isCalled() {
        return this.isCalled;
    }
}
public class AnrSupervisedActivity extends AppCompatActivity {
    static final AnrSupervisor anrSupervisor = new AnrSupervisor();
    
    public void onStart() {
       super.onStart();
       anrSupervisor.start();
    }
    
    public void onStop() {
        super.onStop();
        anrSupervisor.stop();
    }
}

 

 

 

참고 문헌