DBILITY

android ANR, Thread, Handler, runOnUiThread, View Handler, AsyncTask, RxJava, Callback Interface 본문

android/java

android ANR, Thread, Handler, runOnUiThread, View Handler, AsyncTask, RxJava, Callback Interface

DBILITY 2024. 5. 23. 18:33
반응형
ANR ( Application Not Response )
Main Thread Blocking 상태로 사용자 키입력 , 터치 등에 반응을 할 수 없게 된다.
판단하는 시간은 다음과 같다.
// How long we allow a receiver to run before giving up on it.
static final int BROADCAST_FG_TIMEOUT = 10*1000;
static final int BROADCAST_BG_TIMEOUT = 60*1000;
    
// How long we wait until we timeout on key dispatching.
static final int KEY_DISPATCHING_TIMEOUT = 5*1000;
Main Thread ( UI Thread, Foregroud Thread )
UI갱신 등 눈에 보이는 작업은 Main Thread에서 실행
Worker Thread ( Background Thread )
눈에 보이지 않는 작업. 즉, 시간이 오래 걸리는 네크워크를 통한 파일다운로드, 파일저장, DB Transaction 등
Worker Thread가  UI갱신을 처리하지 못하도록 하는 안드로이드 제약이 있다.

https://developer.android.com/topic/performance/vitals/anr?hl=ko

 

ANR  |  App quality  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. ANR 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Android 앱의 UI 스레드가 너무 오랫동안 차단되면 'ANR

developer.android.com

 

시험삼아 Main Thread에서  100번 loop를 돌려 Thread.sleep(100L)으로 0.1마다 멈추고 TextView에 loop횟수를 표시하는  코드를 실행 버튼(Shift+F10)을 누르고 난 후 다시 열심히 클릭해도 클릭이 되지 않는다. 그후 다음과 같은 오류가 발생한다. 맨 앞줄 ANR이 보인다. Reason을 읽어보면 Wait queue head age: 5526.0ms
최소한 버튼의 클릭 등 사용자 화면 작업은 두번 이상이어야 오류가 발생한다.

..............
private static final String TAG = MainActivity.class.getSimpleName();
private ActivityMainBinding binding;
..............   

private void loop(View v) {
  for (int i = 1; i <= 100; i++) {
     try {
         Thread.sleep(100);
         binding.tvProgress.setText(i + "");
     } catch (InterruptedException e) {
         throw new RuntimeException(e);
     }       
  }
}
ANR in com.dbility.android.threadexam (com.dbility.android.threadexam/.MainActivity)
 PID: 9896
 Reason: Input dispatching timed out (Waiting to send non-key event because the touched window has not finished processing certain input events that were delivered to it over 500.0ms ago.  Wait queue length: 6.  Wait queue head age: 5526.0ms.)
 Parent: com.dbility.android.threadexam/.MainActivity
 Load: 0.38 / 0.25 / 0.22
 CPU usage from 72503ms to 0ms ago (2024-05-23 18:50:59.580 to 2024-05-23 18:52:12.083):
   5.8% 1793/surfaceflinger: 0.9% user + 4.9% kernel / faults: 55 minor
   5.3% 2023/system_server: 1.8% user + 3.5% kernel / faults: 6028 minor 7 major
   3.3% 2212/com.android.systemui: 0.8% user + 2.5% kernel / faults: 594 minor
   0.9% 8084/com.google.android.gms.persistent: 0.4% user + 0.5% kernel / faults: 3258 minor
   0.7% 1782/android.hardware.sensors@1.0-service: 0% user + 0.7% kernel
   0.7% 3349/com.google.android.googlequicksearchbox:search: 0.3% user + 0.3% kernel / faults: 4595 minor 11 major
   0.6% 1766/android.hardware.graphics.composer@2.1-service: 0% user + 0.6% kernel / faults: 7 minor
   0.6% 8162/com.google.android.gms: 0.4% user + 0.2% kernel / faults: 7398 minor
   0.4% 1751/android.system.suspend@1.0-service: 0.2% user + 0.2% kernel
   0% 1753/android.hardware.audio@2.0-service: 0% user + 0% kernel
   0.2% 1801/adbd: 0% user + 0.2% kernel / faults: 794 minor
   0% 1788/audioserver: 0% user + 0% kernel
   0.2% 2594/com.android.phone: 0% user + 0.1% kernel / faults: 373 minor
   0.1% 2821/com.google.android.apps.nexuslauncher: 0% user + 0% kernel / faults: 775 minor 3 major
   0% 3425/com.google.process.gapps: 0% user + 0% kernel / faults: 773 minor
   0% 1765/android.hardware.graphics.allocator@2.0-service: 0% user + 0% kernel / faults: 10 minor
   0% 1845/llkd: 0% user + 0% kernel
   0% 9118/installer: 0% user + 0% kernel / faults: 642 minor
   0% 1996/process-tracker: 0% user + 0% kernel / faults: 2 minor
   0% 1646/logd: 0% user + 0% kernel / faults: 5 minor
   0% 1822/statsd: 0% user + 0% kernel
   0% 9452/kworker/u8:1: 0% user + 0% kernel
   0% 1764/android.hardware.gnss@1.0-service: 0% user + 0% kernel
   0% 1812/idmap2d: 0% user + 0% kernel / faults: 2959 minor
   0% 1815/installd: 0% user + 0% kernel / faults: 22 minor
   0% 9066/kworker/u8:0: 0% user + 0% kernel
   0% 1791/lmkd: 0% user + 0% kernel
   0% 1794/logcat: 0% user + 0% kernel
   0% 3702/android.process.acore: 0% user + 0% kernel / faults: 21 minor
   0% 7691/com.google.android.gms.unstable: 0% user + 0% kernel / faults: 9 minor
   0% 8/rcu_preempt: 0% user + 0% kernel
   0% 9/rcu_sched: 0% user + 0% kernel
   0% 12/watchdog/0: 0% user + 0% kernel
   0% 27/watchdog/3: 0% user + 0% kernel
   0% 841/kworker/0:1H: 0% user + 0% kernel
   0% 1647/servicemanager: 0% user + 0% kernel
   0% 1655/vold: 0% user + 0% kernel
   0% 1669/jbd2/vdc-8: 0% user + 0% kernel
   0% 1747/zygote: 0% user + 0% kernel / faults: 164 minor
   0% 1758/android.hardware.configstore@1.1-service: 0% user + 0% kernel / faults: 27 minor 34 major
   0% 1787/ashmemd: 0% user + 0% kernel
   0% 1826/wificond: 0% user + 0% kernel / faults: 1 minor
   0% 2780/com.google.android.inputmethod.latin: 0% user + 0% kernel
   0% 3032/com.google.process.gservices: 0% user + 0% kernel / faults: 374 minor
   0% 4539/logcat: 0% user + 0% kernel
   0% 6241/com.google.android.inputmethod.korean: 0% user + 0% kernel / faults: 1 minor
   0% 9302/com.android.keychain: 0% user + 0% kernel / faults: 20 minor
  +0% 9896/com.dbility.android.threadexam: 0% user + 0% kernel
  +0% 9937/kworker/u8:3: 0% user + 0% kernel
  +0% 9938/kworker/u8:4: 0% user + 0% kernel
 3.7% TOTAL: 1.3% user + 2.3% kernel + 0% iowait + 0% softirq
 CPU usage from 11ms to 306ms later (2024-05-23 18:52:12.094 to 2024-05-23 18:52:12.389):
   33% 2023/system_server: 29% user + 3.7% kernel / faults: 580 minor
     22% 2140/InputDispatcher: 11% user + 11% kernel
     7.4% 2038/HeapTaskDaemon: 7.4% user + 0% kernel
     7.4% 2141/InputReader: 3.7% user + 3.7% kernel
     3.7% 2039/ReferenceQueueD: 3.7% user + 0% kernel
   7.1% 1793/surfa

Activity는 물론이고, 많이 사용하는 BroadcastReceiver, Service 조차도 모두 ANR이 발생할 수 있다.  아마도 모바일 기기의 물리적, 환경적 요인으로 인한 제약을 피하기 위해 그렇게 설계가 되었을 것이다.

https://developer.android.com/topic/performance/anrs/diagnose-and-fix-anrs?hl=ko

 

ANR 진단 및 해결  |  App quality  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. ANR 진단 및 해결 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Android 앱의 UI 스레드가 너무 오랫동안

developer.android.com

 

오류 발생을 피하기 위해 Main Thread가 아닌 별도의 Worker new Thread(new Runnable(){.................}).start()로 감싸서 실행했다.

..............
private static final String TAG = MainActivity.class.getSimpleName();
private ActivityMainBinding binding;
..............   

private void loop(View v) {
    new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 1; i <= 100; i++) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                binding.tvProgress.setText(i + "");
            }
        }
    }).start();
}

다음과 같이 오류가 발생했다. Only the original thread that created a view hierarchy can touch its views
대충 이해해 보자면 UI인 tvProgress(TextView)를 생성한 Main Thread가 아닌 Worker Thread에서 접근했다는 것.
오직 Main Thread에서만 View에 접근하고 갱신 할 수 있다.

FATAL EXCEPTION: Thread-2 (Ask Gemini)
 Process: com.dbility.android.threadexam, PID: 10385
 android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
 	at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8191)
 	at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1420)
 	at android.view.View.requestLayout(View.java:24454)
 	at android.view.View.requestLayout(View.java:24454)
 	at android.view.View.requestLayout(View.java:24454)
 	at android.view.View.requestLayout(View.java:24454)
 	at android.view.View.requestLayout(View.java:24454)
 	at android.view.View.requestLayout(View.java:24454)
 	at androidx.constraintlayout.widget.ConstraintLayout.requestLayout(ConstraintLayout.java:3605)
 	at android.view.View.requestLayout(View.java:24454)
 	at android.widget.TextView.checkForRelayout(TextView.java:9681)
 	at android.widget.TextView.setText(TextView.java:6269)
 	at android.widget.TextView.setText(TextView.java:6097)
 	at android.widget.TextView.setText(TextView.java:6049)
 	at com.dbility.android.threadexam.MainActivity$1.run(MainActivity.java:61)
 	at java.lang.Thread.run(Thread.java:919)

첫번째로 다음과 같이 Handler를 통해 해결 할 수 있다.
Handler는 Main Thread로 동작하므로 TextView가 변경이 된다.

..............
private static final String TAG = MainActivity.class.getSimpleName();
private ActivityMainBinding binding;
private Thread mThread;
private Handler mHandler = new Handler();
private final AtomicBoolean isRunning = new AtomicBoolean(false);
..............    

private void loop(View v) {
    mThread = new Thread(new Runnable() {
        @Override
        public void run() {
        	isRunning.set(true);
            for (int i = 1; i <= 100; i++) {
                try {
                    Thread.sleep(100);
                    
                    Log.d(TAG, Thread.currentThread().getName() + " : " + i + "");
                    final int finalI = i;
                    mHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            binding.tvProgress.setText(finalI + "");
                        }
                	});
                
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally {
                    isRunning.set(false);
                }               

            }
        }
    });
    if (!isRunning.get())
    	mThread.start();
}
Handler의 대표 메서드는 다음과 같다. 이름으로 유추 가능하다.
post(Runnable) , postAtTime(Runnable, long) ,  postDelayed(Runnable , long)
---------------------------------------------------------------------------------------------------------
sendEmptyMessage(int) , sendMessage(Message) ,
sendMessageAtTime(Message, long), sendMessageDelayed(Message, Long)

send시작하는 메서드에서 사용하는 Message Object는 다음과 같이 사용한다.

Message message= Message.obtain();
message.what = 1;
message.arg1 = 2;
message.arg2 = 3;
message.obj = "hello android";

Handler.sendMessage(Message)형태로 전달하면 Handler의 handlerMessage(Message) 메서드가 실행된다.
여기에서 Message를 이용해 UI를 갱신 할 수 있다.

..............
private static final String TAG = MainActivity.class.getSimpleName();
private ActivityMainBinding binding;

private final int PROGRESS = 2024;
private final int COMPLETE = 2023;

private Handler mHandler = new Handler(Looper.getMainLooper()){
    @Override
    public void handleMessage(@NonNull Message msg) {
        binding.tvProgress.setText(msg.arg1+"");
        binding.progressBar.setProgress(msg.arg1);
    }
};

private Handler cHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
    @Override
    public boolean handleMessage(@NonNull Message msg) {
        switch (msg.what) {
            case PROGRESS:
                binding.tvProgress.setText(msg.arg1 + "%");
                binding.progressBar.setProgress(msg.arg1);
                break;
            case COMPLETE:
                Toast.makeText(MainActivity.this,"완료",Toast.LENGTH_SHORT).show();
                break;
        }
        return false; // True if no further handling is desired
    }
});
..............

private void loop(View v) {
    new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 1; i <= 100; i++) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                Log.d(TAG, Thread.currentThread().getName() + " : " + i );
                
//                Message message = Message.obtain();
                Message message = cHandler.obtainMessage(); //recommended usage              
                message.what = i < 100 ? PROGRESS : COMPLETE;
                message.arg1 = i;
                //pHandler.sendMessage(message);
                cHandler.sendMessage(message);
            }
        }
    }).start();
}

Activity에는 runOnUiThread가 있다. Main Thread에서 동작한다. Activity가 Main Thread에서 동작하니 당연한 것이겠다.

..............
private static final String TAG = MainActivity.class.getSimpleName();
private ActivityMainBinding binding;
..............

private void loop(View v) {

    new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 1; i <= 100; i++) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                Log.d(TAG, Thread.currentThread().getName() + " : " + i);
                final int finalI = i;
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        binding.tvProgress.setText(finalI + "");
                    }
                });
            }
        }
    }).start();
}

모든 View는 Handler를 내장하고 있다. 알고 있으면 사용처도 생기겠다.

..............
private static final String TAG = MainActivity.class.getSimpleName();
private ActivityMainBinding binding;
..............

private void loop(View v) {

    new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 1; i <= 100; i++) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                Log.d(TAG, Thread.currentThread().getName() + " : " + i);
                final int finalI = i;
                
                v.post(new Runnable() {
                    @Override
                    public void run() {
                        binding.tvProgress.setText(finalI + "");
                    }
                });
            }
        }
    }).start();
}

안드로이드 개발에 자바를 쓰게 되면 Thread, Runnable을 통해 Worker를 구현한다. 제일 싫은 것은 너무나 많은 block brace..코드 가독성이 극혐이다.
이러한 작업을 하나의 Task로 처리하게 해주는 AsyncTask를 제공하고 있다. 
API level 30 (Android 11)부터 Deprecated. memory leak, 예외처리 없음, 순서보장없음 등등의 사유라고..그러나 알아야 한다.

AsyncTask는 Main Thread에서 시작할 수 있고, Worker Thread로 하던 Long Time, Heavy Task를 처리. Thread와 Handler없이도 UI를 갱신할 수 있다. 이보다 더 좋을 순 없었다....

AsyncTask를 상속 받아 작성해야 한다. 3가지 Generic Type을 정의 해야한다.

AsyncTask<Params, Progress, Result> 형태로
Params는 입력값, Progress는 Background Worker가 수행되는 동안 발행되는 Progress type(진행률 등 )
Result는 Background Working의 결과 Type이다.
사용할 필요가 없는 Generic은 Void로 입력하면 된다.
그냥 입으로 먹고, 소화되는 과정을 나타내며 응가가 나온다로 이해하니 쉽다. 응가가 Void라는 건 완벽한 에너지 효율이겠다. 나는 원초적이다.


AsyncTask는 다음의 4가지 Callback Method를 작성한다. 명칭 그대로 이해하면 된다. 실행 Thread만 구분하면 된다.

  1. onPreExecute() 
    Main Thread에서 실행되고 Task실행 직후에 호출된다. 초기화에 적절하다.
  2. doInBackground(Params...)
    Worker Thread에서 실행되고, 1번 직후에 호출된다.
    Long Time, Heavy Task를 여기서 처리한다.
    UI갱신이 필요할 때 publishProgress(Progress...)를 호출하면
    3번 onProgressUpdate(Progress...) 이 호출된다. 3번을 직접 호출하면 안된다.
    return value는 4번 onPostExecute(Result)로 전달된다.
  3. onProgressUpdate(Progress...)
    Main Thread에서 실행되며 , 2번에서 publishProgress(Progress...)가 실행되면 호출된다.
    2번 실행 중 UI를 변경하고 싶을 때 여기서 처리하면 된다.
  4. onPostExecute(Result)
    Main Thread에서 실행되며 2번 doInBackground가 종료된 직후에 호출된다.
    Task종료에 대한 처리는 여기서 작성한다.

AsyncTask는 다음과 같은 Threading 규칙을 따라야 한다.

  1. AsyncTask instance는 Main Thread에서 생성
  2. execute메서드는 Main Tread에서 생성
  3. 위 1~4 callback method는 직접 호출하면 안됨
  4. AysncTask instance는 한번만 실행할 수 있다. 다시 생성하고 실행하면 된다.
    java.lang.IllegalStateException: Cannot execute task: the task has already been executed (a task can be executed only once)

상속을 받아 작성된 AsyncTask는 AsyncTask.execute(Params..)로 실행하면 된다.
deprecated되어서 경고가 뜨게 된다.@SuppressWarnings("deprecation")로 제거해야 보기 좋다.

...........
private static final String TAG = MainActivity.class.getSimpleName();
private ActivityMainBinding binding;
private AsyncLoop asyncLoop;
..............

@SuppressWarnings("deprecation")
private void loop(View v) {
   asyncLoop = new AsyncLoop();
   asyncLoop.execute();
}

@SuppressWarnings("deprecation")
class AsyncLoop extends AsyncTask <Void, Integer, Void> {
    @Override
    protected void onPreExecute() {
        Log.e(TAG, Thread.currentThread().getName()+" onPreExecute");
    }

    @Override
    protected Void doInBackground(Void... voids) {
        for (int i = 0; i <= 100; i++) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Log.e(TAG, Thread.currentThread().getName()+" doInBackground : "+ i + "%");
            final int finalI = i;
            publishProgress(finalI);
            
            if(isCancelled())
               break;
        }
        return null;
    }

    @Override
    protected void onProgressUpdate(Integer... values) {
        Log.e(TAG, Thread.currentThread().getName()+" onProgressUpdate");
        binding.tvProgress.setText(values[0] + "%");
    }

    @Override
    protected void onPostExecute(Void unused) {
        Log.e(TAG, Thread.currentThread().getName()+" onPostExecute");
    }
    
    @Override
    protected void onCancelled(Void unused) {
        Log.e(TAG, Thread.currentThread().getName()+" onCancelled");
    }
}

AsyncTask는 호출 순서대로 실행이 된다. 동시(병렬) 실행이 필요한 경우 execute()대신 executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)를 사용해야 한다.

RxJava ( Reactive Extensions for the JVM )

reactive programming을 java에서 지원하는 library다. 반응형 프로그래밍은 asynchronous data stream을 이용하는 방식이라고 한다. observable, observer로 구분된다니 Observer Pattern이다. (경제공황처럼) Data Stream의 Produce(생산)를 Consuming(소비)이 따라 가지 못해 OOM(Out Of Memory)이 발생하게 되는(Backpressure) 문제를 해결하는 Flowable도 있다.

https://github.com/ReactiveX/RxJava

 

GitHub - ReactiveX/RxJava: RxJava – Reactive Extensions for the JVM – a library for composing asynchronous and event-based p

RxJava – Reactive Extensions for the JVM – a library for composing asynchronous and event-based programs using observable sequences for the Java VM. - ReactiveX/RxJava

github.com

dependency를 추가한다.

[versions]
~~~~
rxjava = "3.1.8"
rxAndroid = "3.0.2"

[libraries]
~~~~
rxjava = { group = "io.reactivex.rxjava3", name = "rxjava", version.ref = "rxjava" }
rxandroid = { group = "io.reactivex.rxjava3", name = "rxandroid", version.ref = "rxAndroid" }
dependencies {
~~~~
    implementation libs.rxandroid
    implementation libs.rxjava
~~~~
}
RxJava는 method chaining을 사용할 수 있다. 너무 좋다.
Observable(publish)에는 subscribeOn, observeOn이 있는데,
subScribeOnObservable 동작을 시작하는 Thread이고,
observeOnObservable이 Observer에게 알림을 보내는 스레드(다음 처리를 진행할 때 사용할 Thread )를 지정하는 것이다.
subscribeOn은 여러번 호출되더라도 맨 처음의 호출만 영향을 주며 어디에 위치하든 상관없다.
지금 생각에는 가독성 차원에서 chaining의 첫번째에 하는게 좋겠다.
observeOn은 여러개가 호출될 수 있으며 이후에 실행되는 연산에 영향을 주기 때문에 위치가 중요하다.
observer(subscribe)는 observable이 발행한 이벤트를 구독(수신)하는 객체다.

observeOn에 지정할 Schedulers 종류와 용도는 다음과 같다.

  • Schedulers.computation()  :  Event Loop에서 간단한 연산이나 콜백 처리를 위해 사용하며, I/O 처리는 하면 안됨
  • Schedulers.from(executor) : 특정 Executor를 Scheduler로 지정
  • Schedulers.immediate()  : Current Thread에서 즉시 수행
  • Schedulers.io()  :  Synchronous I/O를 별도로 처리시켜 Asynchronous 효율을 얻기 위한 Scheduler로 자체적인 ThreadPool에 의존
  • Schedulers.newThread() : 항상 새로운 Thread를 만드는 Scheduler
  • Schedulers.trampoline() :  Queue에 있는 Job이 끝나면 이어서 Current Thread에서 수행하는 Scheduler
  • AndroidSchedulers.mainThread() : Android Main Thread ( UI Thread )에서 수행
  • HandlerScheduler.from(handler)  : 특정 핸들러 handler에 의존하여 동작.
..............
private static final String TAG = MainActivity.class.getSimpleName();
private ActivityMainBinding binding;
..............

private final Observable observable = Observable.create(new ObservableOnSubscribe<Integer>() {
        @Override
        public void subscribe(@io.reactivex.rxjava3.annotations.NonNull ObservableEmitter<Integer> emitter) throws Throwable {

            for (int i = 1; i <= 20; i++) {
                try {
                    Thread.sleep(100);
                    if (i == 10) { // force stop condition
                        emitter.onComplete();//1
                        //Thread.currentThread().interrupt();//2
                    }
                    if (i <= 100) {
                        emitter.onNext(i);
                        if (i == 100)
                            emitter.onComplete();
                    }
                    Log.d(TAG, "Observable " + Thread.currentThread().getName() + " : emitter.onNext  " + i + "");
                } catch (InterruptedException e) {
                    throw new RuntimeException(e); //2                  
                }
            }
        }
    }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
    
    private final Observer<Integer> observer = new Observer<Integer>() {
        @Override
        public void onSubscribe(@io.reactivex.rxjava3.annotations.NonNull Disposable d) {
            Log.d(TAG, "Observer onSubscribe : " + Thread.currentThread().getName());
        }

        @Override
        public void onNext(@io.reactivex.rxjava3.annotations.NonNull Integer integer) {
            Log.d(TAG, "Observer onNext : " + Thread.currentThread().getName() + " , " + integer.toString());
            binding.tvProgress.setText(integer + "");
        }

        @Override
        public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) {
            Log.d(TAG, "Observer onError : " + Thread.currentThread().getName() + ", " + e.getMessage());
        }

        @Override
        public void onComplete() {
            Log.d(TAG, "Observer onComplete : " + Thread.currentThread().getName());
        }
    };
    
    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_start:
                Log.d(TAG, "onClick " + Thread.currentThread().getName());
                observable.subscribe(observer);
                break;
        }
    }

 

Logcat 결과

--------- beginning of main
12:16:49.399 MainActivity             D  onClick main
12:16:49.401 MainActivity             D  Observer onSubscribe : main
12:16:49.504 MainActivity             D  Observable RxCachedThreadScheduler-3 : emitter.onNext  1%
12:16:49.507 MainActivity             D  Observer onNext : main , 1
12:16:49.607 MainActivity             D  Observable RxCachedThreadScheduler-3 : emitter.onNext  2%
12:16:49.607 MainActivity             D  Observer onNext : main , 2
12:16:49.708 MainActivity             D  Observable RxCachedThreadScheduler-3 : emitter.onNext  3%
12:16:49.708 MainActivity             D  Observer onNext : main , 3
12:16:49.810 MainActivity             D  Observable RxCachedThreadScheduler-3 : emitter.onNext  4%
12:16:49.810 MainActivity             D  Observer onNext : main , 4
12:16:49.911 MainActivity             D  Observable RxCachedThreadScheduler-3 : emitter.onNext  5%
12:16:49.911 MainActivity             D  Observer onNext : main , 5
12:16:50.012 MainActivity             D  Observable RxCachedThreadScheduler-3 : emitter.onNext  6%
12:16:50.012 MainActivity             D  Observer onNext : main , 6
12:16:50.113 MainActivity             D  Observable RxCachedThreadScheduler-3 : emitter.onNext  7%
12:16:50.113 MainActivity             D  Observer onNext : main , 7
12:16:50.214 MainActivity             D  Observable RxCachedThreadScheduler-3 : emitter.onNext  8%
12:16:50.214 MainActivity             D  Observer onNext : main , 8
12:16:50.316 MainActivity             D  Observable RxCachedThreadScheduler-3 : emitter.onNext  9%
12:16:50.316 MainActivity             D  Observer onNext : main , 9
12:16:50.417 MainActivity             D  Observable RxCachedThreadScheduler-3 : emitter.onNext  10%
12:16:50.417 MainActivity             D  Observer onNext : main , 10
12:16:50.419 MainActivity             D  Observer onError : main, java.lang.InterruptedException

traditional? callback interface를 써보자.

우선 헷갈리지 않게 다음과 같이 정리된다. call by reference는 알고 있어야 함
1) Callback : event가 발생했을때 특정 메서드를 호출하는 개념 ( 1개 )
2) Listener : event가 발생했을때 연결된 event listener(event handler)에 event를 전달 ( N개 가능 )
3) Observer : property나 data value의 변경을 감지하고 subscriber에 변경 사항 전달

..............
private static final String TAG = MainActivity.class.getSimpleName();
private ActivityMainBinding binding;

public interface ICallback {
    public void success();
    public void progress(int progress);
    public void failure();
}
..............

public void fn_loop (View v, ICallback callback) {
     new Thread(new Runnable() {
         @Override
         public void run() {
             for (int i = 1; i <= 10; i++) {
                 try {
                     Thread.sleep(100);
                     Log.d(TAG, Thread.currentThread().getName() + " : " + i + "%");
                     final int finalI = i;
                     callback.progress(i);
                     if (i == 10)
                         callback.success();
                     if (i == 5)
                         throw new InterruptedException("cancel");
                 } catch (InterruptedException ie) {
                     //ie.printStackTrace();
                     Log.e(TAG, ie.getMessage()+"");
                     callback.failure();
                     break;
                 }
             }
         }
     }).start();
 }
 
 @Override
 public void onClick(View v) {
     switch (v.getId()) {
         case R.id.btn_start:
             Log.d(TAG, "onClick " + Thread.currentThread().getName());
             fn_loop(v, new ICallback() {
                 @Override
                 public void success() {
                     Log.d(TAG, Thread.currentThread().getName() + " ICallback.success()");
                 }

                 @Override
                 public void progress(int progress) {
                     Log.d(TAG, Thread.currentThread().getName() + " ICallback.progress() " + progress);
                     runOnUiThread(new Runnable() {
                         @Override
                         public void run() {
                             binding.tvProgress.setText(progress + "%");
                         }
                     });

                 }

                 @Override
                 public void failure() {
                     Log.d(TAG, Thread.currentThread().getName() + " ICallback.failure()");
                 }
             });
             break;           
     }
 }

 

반응형
Comments