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



: Servie의 모든 콜백은 메인 스레드에서 직접 실행 된다. 그래서 서비스에서 오래 걸리는 작업을 하면 안 된다. 이에 대한 방안으로 이전 포스트에서 HandlerThread 단일 백그라운드에서 작업을 하는 IntentService를 배웠다. IntentService는 단일큐에서 처리하기 때문에 동시성을 보장하지 못 한다.(멀티스레드가 안 된다.) 따라서 이에 대한 보안책을 배워볼 예정이다. 


※ Executor를 이용한 동시성 Service

: IntentService 대안으로 Executor을 사용하는 Service 추상 클래스를 만들 것이다. 따라서 실제 서비스를 동작시키려면 구현한 추상 클래스를 상속해야 한다. 상속한 서브 클래스에서 실제 동작을 구현하기 위해서 onHandlerIntent를 추상 메서드로 만든다. 그리고 Service 클래스를 상속받기에 onBind를 재정의한다. 해당 클래스에서 동시성을 구현하기 때문에 Executor 객체를 외부에서 받아야 하기에 생성자를 별도로 만든다. 이들 Executor 객체들에게 onHandleIntent 작업을 부여해 줘야 하기 때문에 startService()를 호출할 때 콜백하는 onStartCommand를 재정의해야 한다.

public abstract class ConcurrentIntentService extends Service{
	private final Executor executor;
	public ConcurrentIntentService(Executor executor){
		this.executor= executor;
	}
	protected abstract void onHandleIntent(Intent intent);
	
	@Override
	public IBinder onBind(Intent intent){
		return null;
	}
	
	@Override
	public int onStartCommand(final Intent intent, int flags, int startId){
		executor.execute(new Runnable(){
			@Override
			public void run(){
				onHandleIntent(intent);
			}
		});
		return START_REDELIVER_INTENT;
	}
}

onStartCommand의 리턴값으로는 다음과 같이 3가지 종류가 있다.

- START_STICKY : 메모리 공간 부족으로 서비스가 종료되었을 때 인텐트 없이 서비스를 재시작한다. 인텐트 없이 시작하기 때문에 추천하지 않는 반환값이다.

- START_NOT_STICKY : 메모리 공간 부족으로 서비스가 종료되었을 때 서비스를 재시작하지 않고 재전달하지 않는다.

- START_REDELIVER_INTENT : 메모리 공간 부족으로 서비스가 종료되었을 때 종료 전 마지막 인텐트 객체를 담아 서비스를 재시작한다.


위와 같이 하면 동시성이 제공되는 서비스를 만들 수 있다. 하지만 서비스에서 작업을 하지 않을 때 서비스를 중지시켜야 하는 데 중지할 수 있는 매개가 없다. 따라서 현재 하고 있는 작업을 추적해서 최종 작업이 끝났을 때 stopSelf를 호출하도록 해야 한다. 간단하게 int count 속성을 클래스에 추가해 작업을 추적하고 작업이 끝나면 stopSelf 메서드를 호출한다. stopSelf 메서드는 메인 스레드에서 실행 되야 한다. 메인 스레드로 메시지를 보내기 위한 메인 스레드에서 생성한 핸들러를 사용한다. 

    @Override
    public final int onStartCommand(final Intent intent, int flags, int startId) {
        counter++;
        executor.execute(new Runnable(){
            @Override
            public void run() {
                try {
                    onHandleIntent(intent);
                } finally {
                    handler.sendMessage(Message.obtain(handler));
                }
            }
        });
        return START_NOT_STICKY;
    }


    private class CompletionHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            if (--counter == 0) {
                Log.i(LaunchActivity.TAG, "no more tasks, stopping");
                stopSelf();
            } else {
                Log.i(LaunchActivity.TAG, counter + " active tasks");
            }
        }
    }

위와 같이 하면 동시성을 제공하는 서비스 추상 클래스는 완성됬다. 실제로 사용하려면 해당 클래스를 상속하고 executor 생성과 onHandleIntent 메서드만 구현하면 된다.

public class PrimesConcurrentIntentService extends ConcurrentIntentService { public PrimesConcurrentIntentService() { super(Executors.newFixedThreadPool( MAX_CONCURRENT_CALCULATIONS, new ThreadFactory(){ @Override public Thread newThread(Runnable r) { Thread t = new Thread(r); t.setPriority(Thread.MIN_PRIORITY); t.setName("download"); return t; } })); } @Override protected void onHandleIntent(Intent intent) {

.......작업 } }

Executor을 생성할 때 ThreadFactory를 이용했는데 스레드 우선순위에 유의해야 한다. 백그라운드 스레드가 메인 스레드를 기아 상태로 만들지 않도록 하는 게 중요하다. 

그리고 해당 서비스를 실행할 때는 당연히 액티비티에서 startService(intent); 호출하면 된다. 


※ 메신저를 통한 결과 반환

: 서비스에서 작업 결과값을 액티비티에 전달하고 싶을 때 액티비티 속성값 참조를 통해 결과값을 전달하게 되면 액티비티의 구성이 변경되 재시작 되었을 때 문제가 발생한다. 따라서 다른 방법이 필요한 데 가장 이상적인 방법은 메시지를 핸들러로 전송하게 해주는 메신저(Messenger) 클래스를 이용하는 것이다. 먼저 메인 액티비티에 핸들러 정적 내부 클래스를 정의하자. 이를 통해 외부 클래스에 대한 암시적 참조를 하지 않아 메모리 누수에 안전할 수 있다. 

private static class PrimesHandler extends Handler { private LinearLayout view; @Override public void handleMessage(Message message) { if (message.what == PrimesConcurrentIntentService.RESULT) { if (view != null) { TextView text = new TextView(view.getContext()); text.setText(message.arg1 + "th prime: " + message.obj.toString()); view.addView(text); } else { Log.i(LaunchActivity.TAG, "received a result, ignoring because we're detached"); } } else if (message.what == PrimesConcurrentIntentService.INVALID) { if (view != null) { TextView text = new TextView(view.getContext()); text.setText("Invalid request"); view.addView(text); } } else { super.handleMessage(message); } } public void attach(LinearLayout view) { this.view = view; } public void detach() { this.view = null; } }

attach와 detach 메서드를 정의해 뷰에 대한 참조 누수를 하지 않도록 한다. 

private static PrimesHandler handler = new PrimesHandler();
private static Messenger messenger = new Messenger(handler);
@Override
protected void onResume() {
    super.onResume();
    handler.attach((LinearLayout)findViewById(R.id.results));
}

@Override
protected void onPause() {
    super.onPause();
    handler.detach();
}  

Intent intent = new Intent(this, PrimesConcurrentIntentService.class);
intent.putExtra(PrimesConcurrentIntentService.PARAM, primeToFind);
intent.putExtra(PrimesConcurrentIntentService.MSNGR, messenger);
startService(intent);

intent를 통해 서비스에 messenger 객체를 보낸다. 또한 handler와 messenger 객체를 정적으로 선언해서 액티비티가 재시작해도 똑같은 handler와 messenger를 사용하게 한다. 

    @Override
    protected void onHandleIntent(Intent intent) {
        int primeToFind = intent.getIntExtra(PARAM, -1);
        Messenger messenger = intent.getParcelableExtra(MSNGR);
        try {
            if (primeToFind < 2) {
                messenger.send(Message.obtain(null, INVALID));
            } else {
                messenger.send(Message.obtain(null, RESULT, primeToFind, 0, calculateNthPrime(primeToFind)));
            }
        } catch (RemoteException anExc) {
            Log.e(LaunchActivity.TAG, "Unable to send message", anExc);
        }
    }

결과값을 액티비티 핸들러로 전송하기 위해 messenger.send 메서드를 이용한다.


※ 로컬 서비스

: 애플리케이션 프로세스 내부에서만 서비스를 작동하고 싶다면 로컬 서비스를 사용하는 게 좋다. 먼저 액티비티와 직접 상호작용하는 가장 기초적인 로컬 서비스를 볼 것이다. 일단 onBind 메서드를 구현해야 한다.

    public class Access extends Binder {
        public LocalPrimesService getService() {
            return LocalPrimesService.this;
        }
    };

    private final Access binder = new Access();

    @Override
    public IBinder onBind(Intent intent) {
        return binder;
    }

그 다음 직접 상호작용하는 액티비티나 프래그먼트에 bindService 메서드로 바인드 해야한다. 그 전에 바인드하고 바인드를 풀 때 콜백하는 ServiceConnection을 구현해야 한다.

public class LocalPrimesActivity extends Activity
implements LocalPrimesService.Callback {

    private LocalPrimesService service;
    private ServiceConnection connection;
    @Override
    protected void onStart() {
        super.onStart();
bindService( new Intent(this, LocalPrimesService.class), connection = new Connection(), Context.BIND_AUTO_CREATE); } @Override protected void onStop() { super.onStop();
service = null; unbindService(connection); } private class Connection implements ServiceConnection { @Override public void onServiceConnected(ComponentName name, IBinder binder) { service = ((LocalPrimesService.Access)binder).getService(); } @Override public void onServiceDisconnected(ComponentName name) { service = null; } } }

바운드 서비스는 클라이언트가 서비스를 바인딩하면 바로 시작되고 모든 클라이언트가 언바인드되면 자동으로 서비스를 중지한다. 위와 같이 바인딩하면 service 객체가 직접 참조를 가지기 때문에 서비스 인스턴스 안 메서드의 호출이 가능하다. 바인딩된 서비스 클래스 내부에 다음의 함수가 있다고 가정하자.  

    public void calculateNthPrime(final int n) {
 
        new AsyncTask<Void,Void,BigInteger>(){
            @Override
            protected BigInteger doInBackground(Void... params) {
                BigInteger prime = new BigInteger("2");
                for (int i=0; i<n; i++) {
                    prime = prime.nextProbablePrime();
                }
                return prime;
            }

            @Override
            protected void onPostExecute(BigInteger result) {
            		// 사용자에게 결과 통신
            }
        }.execute();
    }

그러면 액티비티에서 다음 코드와 같이 바로 호출이 가능하다. 참고로 서비스가 바인드됐는 지 확인도 해야 한다.

    if (service == null) 
    	service.calculateNthPrime(110);

위와 같이 하면 서비스에 작업 요청이 편리하고 인텐트를 생성할 필요 없어서 과도한 객체 생성이나 통신 과부하가 없다. 하지만 위 서비스 작업 calculateNthPrime은 미동기이므로 결과를 직접 반환이 불가능하고 서비스에서 UI에 대한 참조를 하지 않는다. 따라서 사용자에게 결과를 보여줄 방법이 있어야 한다. 이에 대한 방법으로 백그라운드 작업이 끝나면 액티비티의 메서드를 호출할 수 있도록 하는 것이다. 따라서 서비스에서는 액티비티에 대한 참조가 필요하다. 하지만 액티비티와 서비스의 생명주기는 서로 일치하지 않기 때문에 강한 참조를 하지 않고 액티비티가 가비지 컬렉션을 할 수 있도록 약한 참조를 한다. 단, 약한 참조로 액티비티에 접근할 때 해당 액티비티가 null인지 아닌지 검사를 해야한다. 추가로 통지를 통해서 결과값을 볼 수 있도록 구현했다.  다음은 이를 구현한 서비스 클래스와 액티비티 클래스의 일부 소스이다.

public class LocalPrimesService extends Service { public interface Callback { public boolean onResult(BigInteger result); } public class Access extends Binder { public LocalPrimesService getService() { return LocalPrimesService.this; } }; private final Access binder = new Access(); @Override public IBinder onBind(Intent intent) { return binder; } @Override public int onStartCommand(Intent intent, int flags, int startId) { return START_STICKY; } public void calculateNthPrime(final int n, Callback activity) { final WeakReference<Callback> maybeCallback = new WeakReference<Callback>(activity); new AsyncTask<Void,Void,BigInteger>(){ @Override protected BigInteger doInBackground(Void... params) { BigInteger prime = new BigInteger("2"); for (int i=0; i<n; i++) { prime = prime.nextProbablePrime(); } return prime; } @Override protected void onPostExecute(BigInteger result) { Callback callback = maybeCallback.get(); if (callback != null) { if (!callback.onResult(result)) { notifyUser(n, result.toString()); } } else { notifyUser(n, result.toString()); } } }.execute(); } private void notifyUser(int primeToFind, String result) { String msg = String.format("The %sth prime is %s", primeToFind, result); NotificationCompat.Builder builder = new NotificationCompat.Builder(this) .setSmallIcon(android.R.drawable.stat_notify_chat) .setContentTitle(getString(R.string.primes_service)) .setContentText(msg); NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); nm.notify(primeToFind, builder.build()); } }

액티비티....

@Override public boolean onResult(BigInteger result) { if (service == null) { return false; } else { LinearLayout results = (LinearLayout)findViewById(R.id.results); TextView view = new TextView(this); view.setText(result.toString()); results.addView(view); return true; } }

위 로컬 서비스는 액티비티와 서비스 사이에 상호작용이 쉽고 가벼워 작업이 용이하지만 디바이스 회전 같은 구성 변경이 일어나 액티비티가 재시작된다면 WeakReference는 가비지 컬렉션되어 액티비티로 결과를 전송할 수 없다. 이를 해결하려면 재시작한 액티비티와 서비스가 다시 연결되야 하는 데 이에 대한 방법으로 전에 메신저를 사용했었다. 바운드한 로컬 서비스는 서비스 메서드 호출이 가능하기 때문에 메신저 대신 핸들러를 사용할 수 있다.
    public void calculateNthPrime(final int n, Handler handler);

핸들러나 액티비티 참조 방법은 서비스를 호출한 액티비티와 연관될 때만 효율적이다. 앱의 다른 부분에서도 결과를 받길 원한다면 브로드캐스트를 하면 된다. 


※ 로컬 브로드 캐스트

: 로컬 브로드 캐스트를 이용하면 다른 앱에 브로드캐스트가 가지 않고 앱 내부에서 브로드캐스팅 된다. 따라서 프로세스 간 통신 과부하를 일으키지도 않아 효율과 보안 측면에서 유용하다. 로컬 브로드 캐스트를 이용하려면 LocalBroadcastManager을 이용하면 된다. LocalBroadcastManager는 Handler에 Message를 보내서 메인 스레드에서 가능한 시점에 처리한다. 따라서 메인 Looper에 쌓인 게 많다면 처리가 늦어질 수도 있다.
   private void broadcastResult(String result) {
        Intent intent = new Intent(PRIMES_BROADCAST);
        intent.putExtra(RESULT, result);
        LocalBroadcastManager.getInstance(this).
            sendBroadcast(intent);
    }

sendBroadcast는 비동기고 리시버가 처리하는 작업을 기다릴 필요 없이 즉시 반환한다. 마지막으로 서비스의 AsyncTask의 doInBackground 메서드 내부에서 broadcastResult 위 메서드를 호출하면 된다. 다음은 결과를 처리하는 리시버 클래스이다.

   private static class NthPrimeReceiver extends BroadcastReceiver {
        private LinearLayout view;

        @Override
        public void onReceive(Context context, Intent intent) {
            String result = intent.getStringExtra(
                BroadcastingPrimesService.RESULT);
            if (view != null) {
                TextView text = new TextView(view.getContext());
                text.setText(result);
                view.addView(text);
            } else {
                Log.i(LaunchActivity.TAG, "received a result, ignoring because we're detached");
            }
        }

        public void attach(LinearLayout view) {
            this.view = view;
        }

        public void detach() {
            this.view = null;
        }
    }

그리고 액티비티에서 인텐트 필터와 브로드캐스트 등록/해지를 해주면 된다.

    @Override
    protected void onStart() {
        super.onStart();

        receiver.attach((LinearLayout)findViewById(R.id.results));

        IntentFilter filter = new IntentFilter(
            BroadcastingPrimesService.PRIMES_BROADCAST);
        LocalBroadcastManager.getInstance(this).
            registerReceiver(receiver, filter);

        bindService(
            new Intent(this, BroadcastingPrimesService.class),
            connection = new Connection(),
            Context.BIND_AUTO_CREATE);
    }

    @Override
    protected void onStop() {
        super.onStop();
        service = null;
        unbindService(connection);

        LocalBroadcastManager.getInstance(this).
            unregisterReceiver(receiver);

        receiver.detach();
    }

위 로컬 브로드캐스트를 이용한 방법은 BroadcastReceiver을 등록하지 않는 액티비티로 이동하거나 애플리케이션이 종료된다면 계산 결과를 볼 수 없다. 즉, 처리하지 못한 브로드캐스트가 생기게 된다. 처리하지 못한 브로드캐스트를 처리하는 방법을 알아보자. 

해당 방법은 앱이 포그라운드에 있으면 결과를 표시하고 그렇지 않으면 통지를 한다. 따라서 결과를 브로드캐스팅한다면 브로드캐스트를 처리했는 지 여부를 알아내야 하고, 처리하지 않았다면 통지를 전송해야 한다. 이를 위해 sendBroadcastSync를 사용한다. 해당 메서드는 sendBroadcast와 달리 리시버의 처리를 기다리고 메인스레드에서 호출되야 한다. 따라서 브로드캐스팅을 할 때 이용하는 인텐트를 통해 처리 여부를 나타내는 데이터를 넣어서 처리 유무를 알 수 있다. 메인스레드에서 처리 되기 위해서 AsyncTask의 onPostExecute 메서드를 이용한다. 이를 구현한 코드는 다음과 같다.

 public void calculateNthPrime(final int n) {
        new AsyncTask<Void,Void,BigInteger>(){
            @Override
            protected BigInteger doInBackground(Void... params) {
                BigInteger prime = new BigInteger("2");
                for (int i=0; i<n; i++)
                    prime = prime.nextProbablePrime();
                return prime;
            }

            @Override
            protected void onPostExecute(BigInteger result) {
                if (!broadcastResult(result.toString()))
                    notifyUser(n, result.toString());
            }
        }.execute(); // remember, execute() operates off a single-threaded executor in API levels >= 11
    }

    private boolean broadcastResult(String result) {
        Intent intent = new Intent(PRIMES_BROADCAST);
        intent.putExtra(RESULT, result);
        LocalBroadcastManager.getInstance(this).
            sendBroadcastSync(intent);
        return intent.getBooleanExtra(HANDLED, false);
    }


※ 서비스 응용

: 서비스의 이상적인 사례는 다음과 같다.

- 오래 걸린다(몇 백 밀리초 이상)

- 단일 액티비티 클래스나 단일 프래그먼트 클래스에 한정하지 않는다.

- 사용자가 앱을 떠나도 반드시 완료되야 한다.

- IntentService가 제공하는 동시성 보다 더 많은 동시성을 요구한다.

=> 가장 좋은 사례는 웹 서비스로부터 다운로드를 동시에 처리하는 것이다.




+ Recent posts