해당 포스트는 "안드로이드 비동기 프로그래밍" 책의 내용을 요약한 것이다.




※ 루퍼

: 루퍼는 자신이 속한 스레드의 메시지큐에 추가되는 메시지를 기다리다가 꺼내서 이를 처리할 핸들러에 디스패치한다. 스레드에 루퍼를 설정하려면 정적 메서드 prepare과 loop를 호출해야 한다. 단, 안드로이드 메인 스레드는 루퍼가 기본적으로 설정되 있다. 

class SimpleThread extends Thread{
	public void run(){
		Looper.prepare();
		Looper.loop();
	}
}

그 다음 루퍼의 메시지 큐에 메시지를 보내고 루퍼가 메시지를 디스패치 했을 때 처리할 콜백 메서드를 구현해야 한다. 이를 핸들러가 해준다.

class SimpleThread extends Thread{
	public Handler handler;
	public void run(){
		Looper.prepare();
		handler = new Handler();
		Looper.loop();
	}
}

Handler 생성자를 사용하면 자동적으로 스레드와 설정된 메시지 큐에 연결된다. 위 SimpleThread는 Looper.loop() 안에서 큐에 추가되어야 하는 메시지를 기다린다. 다른 스레드에서 메시지를 큐에 Handler 객체를 사용해 추가하면 대기하고 있던 SimpleThread는 핸들러의 dispatchMessage 메서드를 호출해 해당 핸들러에게 메시지를 디스패치한다. 참고로 메인 스레드에는 루퍼가 기본적으로 생성되 있는 데 액티비티 생명주기 모든 작업은 메인 루퍼가 호출한 dispachMessage 호출에 의해 동작한다. 또한 다른 스레드에서 메인 스레드로 메시지를 보낼 수 있는 데 이를 통해 UI를 백그라운드 처리 결과와 함께 갱신 가능하다. 



 ※ 핸들러 사용

- 초기화

protected void onCreate(Bundle savedInstanceState){
	super.onCreate(savedInstanceState);
	Handler handler = new Handler();
	//Handler handler = new Handler(Looper.getMainLooper());
	...
}

위 핸들러는 액티비티의 onCreate()에서 초기화했기에 메인 스레드의 메시지 큐에 작업을 제출한다. 주석에 있는 것처럼 메인 루퍼 인스턴스를 전달해 명시적으로 초기화할 수 있다. 메시지 큐에 제출할 작업은 Message 객체나 Runnable 객체로 정의할 수 있다. 


- Runnable 

handler.post(new Runnable(){
	public void run(){
		// 메인 스레드에서 작업 수행
	}
});

위와 같이 익명 Runnable 객체를 생성해 post 메서드를 호출하면 메시지 큐의 끝에 Runnable 작업을 추가한다. 만약 Runnable 작업을 우선 실행해야 한다면 큐의 맨 앞에 작업을 추가하는 postAtFrontofQueue를 사용할 수 있다.

10초 안에 어떠한 작업을 하고 싶다면 Thread.sleep을 사용할 수 있지만 메인 스레드가 10초동안 아무 일도 할 수 없다. 메인 스레드를 절대로 봉쇄해서는 안 된다. 이에 대한 대안으로 다음의 메서드를 사용해 지연된 Runnable 작업을 큐에 보낼 수 있다. 

handler.postDelayed(new Runnable(){
	public void run(){
		// 10초 안에 메인 스레드에서 작업 수행
	}
}, TimeUnit.SECONDS.toMillis(10));

핸들러를 메인 스레드에서 생성했으므로 핸들러를 통해 제출한 작업은 메인 스레드에서 수행된다. 따라서 핸들러에서 긴 작업을 메시지 큐에 보내서는 안 된다. 하지만 핸들러를 통해서 UI와 안전하게 상호작용할 수 있다.  


메시지 큐에 들어있는 Runnable 객체는 다음의 메서드로 제거해 작업을 취소할 수 있다. 단 무엇을 제거할 지 지정해야 되 Runnable 인스턴스에 대한 참조가 유지되야 하고 메시지 큐에서 해당 작업이 기달리고 있는 경우에만 작업 취소가 가능하다. 이미 Runnable이 실행중이라면 최소되지 않는다.

final Runnable runnable = new Runnable(){
	public void run(){
		// 작업 수행
	}
}
handler.postDelayed(runnable, TimeUnit.SECONDS.toMillis(10));
handler.removeCallbacks(runnable); // 작업을 취소한다.


ex)해당 예제는 "안드로이드 비동기 프로그래밍" 책에서 가져왔다.

public class ExplicitHandlerPrimesActivity extends Activity {

    private Handler handler;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.ch3_example1a_layout);
        handler = new Handler();

        final TextView resultView = (TextView) findViewById(R.id.result);

        Button goButton = (Button) findViewById(R.id.go);
        goButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                resultView.setText(R.string.calculating);
                new PrimeCalculator(500, resultView, handler).start();
            }
        });
    }

    private static class PrimeCalculator extends Thread {

        private int primeToFind;
        private TextView resultView;
        private Handler handler;

        public PrimeCalculator(int primeToFind, TextView resultView, Handler handler) {
            this.handler = handler;
            this.resultView = resultView;
            this.primeToFind = primeToFind;
            setPriority(Thread.MIN_PRIORITY);
        }

        @Override
        public void run() {
            BigInteger prime = new BigInteger("2");
            for (int i=0; i<primeToFind; i++) {
                prime = prime.nextProbablePrime();
            }
            postResultToMainThread(prime);
        }

        private void postResultToMainThread(final BigInteger result) {
            handler.post(new Runnable(){
                @Override
                public void run() {
                    resultView.setText(result.toString());
                }
            });
        }
    }
}

PrimeCalculator의 run 함수를 보면 연산을 포함해서 runnable 객체를 만들지 않고 최대한 연산을 따로 분리한 것을 알 수 있다. 메인 스레드에서의 작업을 최소화하기 위해서다. 또한 백그라운드 작업을 수행할 때 setPriority(Thread.MIN_PRIORITY); 메서드를 호출했다. 이를 통해 스레드의 우선순위를 낮춰서 메인 스레드를 기아 상태로 만드는 것을 피한다. 추가적으로 핸들러는 뷰 클래스 계층에서도 호출할 수 있다. 따라서 handler.post가 아닌 resultView.post도 가능하다.


- Message

: 위에서 메시지 큐에 작업을 보낼 때 익명 Runnable 객체를 사용했다. 만약 여러 군데에서 같은 Runnable 작업을 보내야 한다면 일일히 익명 Runnable 객체를 생성하기에는 부담이다. 또한 정적 클래스나 여러 위치에서 인스턴스화할 수 있는 최상위 수준 Runnable 클래스를 정의하면 여러 개의 Runnable 클래스를 작성해야 할 시 부담이 된다. 이에 대한 대안으로 핸들러에게 메시지를 보내고 다른 메시지에 따라 반응하는 핸들러를 정의할 수 있다.

public static class SpeakHandler extends Handler {
    public static final int SAY_HELLO = 0;
    public static final int SAY_BYE = 1;
    public static final int SAY_WORD = 2;
    
    @Override
    public void handleMessage(Message msg) {
        switch(msg.what) {
            case SAY_HELLO:
                sayWord("hello"); break;
            case SAY_BYE:
                sayWord("goodbye"); break;
            case SAY_WORD:
                sayWord((String)msg.obj); break;
            default:
            	super.handleMessage(msg);
                	
        }
    }
    private void sayWord(String word) { … }
}

Handler handler = new SpeakHandler();

위와 같이 핸들러 클래스를 상속하여 handleMessage 메서드를 재정의한다. SpeakHandler가 루퍼에 의해 메시지를 받으면 handleMessage를 호출해 msg.what 값에 따라 다른 동작을 한다. 핸들러에 메시지를 보내는 방법은 다음의 방법들이 있다.

handler.sendEmptyMessage(SAY_HELLO);
handler.sendMessage(Message.obtain(handler,SpeakerHandler.SAY_WORD, "aaaa"));
handler.sendMessageAtFrontOfQueue(msg);
handler.sendMessageDelayed(msg, delay);

// 메시지 취소
handler.removemessages(SpeakHandler.SAY_WORD);

sendEmptyMessage 함수는 msg.what을 매개변수로 보낸다. sendMessage는 Message 객체를 보낸다. 세 번째 인자가 msg.obj이다. 참고로 여기서 new 연산자를 사용하지 않았는 데 메시지는 잠시 동안만 사용한다. 따라서 인스턴스를 생성할 때마다 가비지 컬렉팅을 하면 효율성이 떨어져 풀에서 재사용 가능한 메시지 인스턴스를 얻는 방식을 사용하는 게 좋다. 

메시지를 취소는 msg.what을 통해서 한다. 따라서 Runnable 작업의 취소와 달리 message 객체에 대한 참조를 유지 안 해도 된다.



- 핸들러 조합

: 위에서 Handler 클래스를 상속해서 별도의 핸들러를 만들었다. 이 방법 말고 Handler.Callback을 구현한 인스턴스를 전달함으로써 조합 방식을 통해 핸들러를 만들 수 있다. 

public static class Speak implements Handler.Callback {
    public static final int SAY_HELLO = 0;
    public static final int SAY_BYE = 1;
    public static final int SAY_WORD = 2;
    
    @Override
    public boolean handleMessage(Message msg) {
        switch(msg.what) {
            case SAY_HELLO:
                sayWord("hello"); break;
            case SAY_BYE:
                sayWord("goodbye"); break;
            case SAY_WORD:
                sayWord((String)msg.obj); break;
            default:
            	super.handleMessage(msg);
                	
        }
    }
    private void sayWord(String word) { … }
}

Handler handler = new Handler(new Speaker());

위와 같이 구현 가능하다. Handler 클래스를 상속 받는 것과 다른 점이 handleMessage 메서드의 반환 타입이다. handleMessage에서 메시지를 처리 못하면 false를 반환하고 처리하면 true를 반환한다. false가 반환될 시 자신의 handleMessage를 호출한다. 따라서 핸들러를 초기화 할 때 new SpeakerHandler(new Speaker()); 로 한다면 Speaker에서 처리 못 해 false를 반환한 메시지는 SpeakerHandler의 handleMessage에서 처리된다.


- Runnable vs Message

: Runnable은 작업을 보낼 때마다 새로운 인스턴스를 생성해 가비지 컬렉션에 과부하를 일으킨다. 개발 시간이 빠르고 쉽지만 코드가 여러 군데 분산된다. Message는 풀을 통해 재사용 가능한 객체를 받고 개발 시간은 Runnable 보다는 느릴 수 있다. 하지만 코드는 한 곳에 작성한다. 따라서 Runnable은 소규모의 단발성 애플리케이션에 사용하기 좋다. Message의 장점은 애플리케이션이 커질 수록 많아진다. 따라서 가비지를 최소화하고 앱을 부드럽게 실행하기 위해서 주로 Message 방식을 사용하는 게 좋다.



 ※ HandlerThread

: 스레드에 루퍼와 핸들러를 연결하면 해당 스레드로 다른 스레드에서 작업을 보낼 수 있다. 즉, 메인 스레드에서 백그라운드 스레드로 또는 백그라운드 스레드에서 백그라운드 스레드로 작업을 보낼 수 있다. 이를 루퍼를 사용하지 않고 편하게 하는 방법이 HandlerThread를 사용하는 것이다. HandlerThread는 Thread를 상속한 클래스로 별도의 루퍼와 메시지큐를 가지고 있다. 
HandlerThread thread= 
    new HandlerThread("bg", Process.THREAD_PRIORITY_BACKGROUND);
thread.start();

Handler handler = new Handler(thread.getLooper());
handler.post(new Runnable(){
	public void run(){
		// 백그라운드 작업
	}
});

Handler.Callback callback = new Handler.Callback();
Handler handler = new Handler(thread.getLooper(), callback);

위와 같이 하면 별도의 스레드에 작업을 보낼 수 있다. 만약 HandlerThread가 특정 액티비티에서 백그라운드 작업을 한다면 액티비티 생명주기와 비슷하게 하면 좋다. HandlerThread는 quit를 통해 종료시킬 수 있고 quit를 하면 큐에 있던 작업들이 모두 취소된다. quitSafely 메서드는 큐에 남아있는 작업을 수행한 후에 HandlerThread를 종료한다. HandlerThread를 종료할 때 액티비티 생명주기에 맞추어 다음과 같이 할 수 있다.

protected void onPause(){
	super.onPause();
	if((thread!=null) && (isFinishing()))
		thread.quit();
}



 ※ 주의할 점

- 암시적 참조 누수

final Runnable runnable = new Runnable(){
	public void run(){
		//...작업 수행
	}
};
handler.postDelayed(runnable, TimeUnit.SECONDS.toMillis(10));

위와 같은 코드가 있다고 가정하자. 만약 handler.postDelayed가 호출되고 10초가 되기 전에 액티비티가 종료되면 액티비티가 가비지 컬렉팅 되지 않는다. runnable가 액티비티 안 익명 내부 클래스로 선언됨으로써 액티비티를 암시적으로 참조하기 때문이다. 이 문제를 최소화하기 위해서는 비정적 내부 클래스 사용을 피해야 한다. Runnable 클래스나 Handler 클래스를 최상위 클래스로 선언하거나 정적 클래스나 정적 내부 클래스로 선언해야 한다. 그러면 참조를 매개변수로 받아야 하기 때문에 명시적이 된다. 또한 onPause 같은 생명주기에서 기다리는 작업을 취소할 수 있다. 여기서 만약 message를 사용했다면 msg.what을 통해 작업 취소가 가능해 참조를 유지 안해도 된다. handleThread 인스턴스의 경우에는 액티비티를 종료할 때 quit 메서드를 통해 작업을 취소해야 한다.



- 명시적 참조 누수
static class MyRunnable implements Runnable{
	private View view;
	public MyRunnable(View view){
		this.view = view;
	}
	public void run(){
		
	}
}

위와 같이 정적 클래스로 만들고 명시적 참조를 하더라도 Runnable 객체가 액티비티보다 오래 살아남으면 메모리 누수가 된다. 이에 대한 해결책으로 약한 참조를 사용하되 참조된 뷰를 사용하기 전에 널을 확인하는 것이다. WeakReference는 강한 참조를 갖는 객체가 가비지 컬렉션할 때 참조를 잃고 get()은 널을 반환한다.

static class MyRunnable implements Runnable{
	private WeakReference<View> view;
	public MyRunnable(View view){
		this.view = new WeakReference<View>(view);
	}
	public void run(){
		View v = view.get();
		if(v!=null){
			
		}
	}
}

또한 onResume과 onPause에서 view에 대한 참조를 붙이고 떼는 방법도 괜찮다.

static class MyHandler implements Handler{
	private View view;
	public void attach(View view){
		this.view = view;
	}
	public void detach(){
		view = null;
	}
	....
}


 ※ 응용

: 핸들러도 AsyncTask와 마찬가지로 오래 걸리는 작업(1초 이상)을 하면 안 된다. 메인스레드를 봉쇄하기 때문이다. 따라서 계산, 문자열 처리, 파일 시스템에 작은 파일 읽고 쓰기, 로컬 데이터베이스에 읽고 쓰기 수행을 하면 좋다. 단발성 또는 진행 표시나 작업 취소를 하는 방법을 원한다면 handler가 아닌 AsyncTask를 사용하는 게 좋다.





+ Recent posts