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




※ 로더(Loader)

: 로더는 안드로이드 메인 스레드와 상관없이 비동기적으로 데이터를 적재하고 액티비티와 프래그먼트에 걸쳐 활용할 수 있는 데이터를 만드는 것이다. 로더를 사용함으로써의 장점은 다음과 같다.

- 백그라운드 스레드에서 작업을 해 결과값을 메인 스레드에 안전하게 전달할 수 있다.

- 데이터를 캐시해 효율적이다.

- 별도의 생명주기를 가지고 있어 액티비티/프래그먼트 생명주기에 얽매이지 않는다. => 액티비티가 재생성되도 자동으로 원래 있던 로더와 연결할 수 있다.

- 데이터를 감시하여 데이터가 변경되면 새 결과를 전달한다.


로더에 적재된 데이터를 사용하려면 해당 액티비티에 로더를 생성하고 LoaderCallbacks 인터페이스를 구현해야 한다. 다음은 로더를 생성하는 메서드이다.

private static final int MEDIA_STORE_LOADER = "image_loader".hashCode();
getSupportLoaderManager().initLoader(MEDIA_STORE_LOADER, null, this);

LoaderManager은 여러 개의 로더를 관리하는 객체로 액티비티/프래그먼트당 하나만 존재한다. initLoader을 호출하면 "MEDIA_STORE_LOADER" id를 가진 로더가 있는 지 찾아보고 있다면 해당 로더를 반환하고 없으면 새로 생성한다. 세 번째 인자로 LoaderCallbacks 인터페이스를 구현한 클래스를 넣는다. 로더는 해당 인터페이스에 구현된 콜백 메서드를 이용한다. LoaderCallbacks 인터페이스를 구현한 클래스에서 재정의해야할 메서드는 다음과 같다.

CursorLoader onCreateLoader(int id, Bundle bundle);
void onLoadFinished(Loader<Cursor> loader, Cursor media);
void onLoaderReset(Loader<Cursor> loader);

initLoader을 호출할 때 id에 해당하는 로더가 없다면 onCreateLoader가 호출되고 해당 메서드 안에서 로더 객체를 생성 후 반환한다. onLoadFinished 메서드는 백그라운드 작업을 완료되면 호출된다. 백그라운드 결과로 적재된 데이터에 접근할 수 있다. initLoader 호출 후 id에 해당하는 값이 없다면 onCreateLoader에서 객체가 생성되는 데 그 후 바로 백그라운드 작업으로 데이터가 적재되고 완료되면 onLoadFinished 메서드가 호출된다. 만약 initLoader를 호출했을 때 id에 해당하는 로더가 있다면 이미 해당 로더에는 데이터 적재가 완료되었기 때문에 바로 onLoadFinished 메서드가 호출된다. onLoaderReset은 로더를 버릴 때 필요한 정리를 수행하는 곳이다. 

로더는 주로 로더의 서브 클래스인 AsyncTaskLoader와 CursorLoader이 많이 사용된다. CursorLoader는 AsyncTaskLoader의 서브 클래스로 로컬 DB에 있는 원시 데이터에 접근하는 데 주로 쓰이고 관련된 DB Cursor을 사용한다. AsyncTaskLoader는 작업을 수행할 AsyncTask를 제공하는 클래스이다.


- AsyncTaskLoader

ex) "안드로이드 비동기 프로그래밍" 예제. 백그라운드에서 MediaStore로부터 비트맵을 적재하는 예

public class ThumbnailLoader extends AsyncTaskLoader<Bitmap> {

    private Bitmap data;
    private Integer mediaId;

    public ThumbnailLoader(Context context, Integer mediaId) {
        super(context);
        this.mediaId = mediaId;
    }

    @Override
    public Bitmap loadInBackground() {
        if (mediaId != null) {
            Log.i(LaunchActivity.TAG, "loading from mediastore");
            ContentResolver res = getContext().getContentResolver();
            return MediaStore.Images.Thumbnails.getThumbnail(
                res, mediaId, MediaStore.Images.Thumbnails.MINI_KIND, null);
        } else {
            Log.i(LaunchActivity.TAG, "mediaId was null!");
            return null;
        }
    }

    @Override
    protected void onStartLoading() {
        if (data != null){
            deliverResult(data);
        }
        else{
            forceLoad();
        }
    }

    @Override
    protected void onForceLoad() {
        super.onForceLoad();
    }

    @Override
    public void deliverResult(Bitmap data) {
        super.deliverResult(this.data = data);
    }

    @Override
    protected void onStopLoading() {
        cancelLoad();
    }

    @Override
    public void onCanceled(Bitmap unneeded) {
        if ((unneeded != null) && (unneeded != data))
            unneeded.recycle();
    }

    @Override
    protected void onReset() {
        if (data != null) {
            data.recycle();
            data = null;
        }
    }
}

AsyncTaskLoader를 구현할 때 AsyncTaskLoader<T>을 상속해야 하고 적재하려는 객체의 타입을 T에 명시한다. 위에서는 비트맵을 적재하기 때문에 제네릭 인자로 Bitmap을 넣었다. 로더 클래스는 생성자로 Context를 전달해야 한다. 따라서 생성자에 Context를 매개변수로 받되 로더는 액티비티보다 오래 생존할 수 있기에 getContext()가 아닌 ApplicationContext를 참조해야 한다. 이에 관해서 잘 모르겠다면 해당 문장에 걸려있는 url 포스트를 보면 된다.  로더에서 재정의해야할 가장 중요한 메서드는 loadInBackground이다. loadInBackground에서 백그라운드로 데이터를 적재하며 유일하게 메인 스레드에서 실행되지 않는다. 여기서 리턴 되는 값은 onLoadFinished 콜백 메서드의 매개변수로 간다.

여기서 로더는 별도의 생명주기를 가지는 데 생명주기와 함께 위 예제 코드를 추가적으로 알아보겠다.

먼저 로더가 onCreateLoader에 의해 생성되면 onStartLoading() 메서드가 자동으로 호출된다. 여기서 적재된 데이터에 대한 캐시 여부를 확인한다. 위 예에서 보듯이 data에 이전에 적재된 데이터를 가지고 있는 지 확인하고 있고 data 캐시가 있다면 deliverResult(data)를 호출하고 없다면 forceLoad()를 호출한다. forceLoad()는 백그라운드 스레드를 작동시킨다. 만약 data 캐시가 있으면 다시 백그라운드 작업으로 데이터를 적재할 필요가 없으므로 deliverResult가 호출되 onLoadFinished 콜백 메서드가 호출된다. 

로더를 버릴 때는 onReset이 자동 호출된다. 여기서 로더가 참조했던 데이터를 정리해야 한다. 위 예제에는 data.recycle() 메서드를 통해 비트맵을 정리한다. 액티비티나 프래그먼트가 중지되면 onStopLoading 메서드가 호출된다. 만약 여기서 백그라운드 작업을 취소하고 싶다면 onStopLoading 메서드 안에서 슈퍼 클래스인 cancelLoad()를 호출하면 된다. 하지만 백그라운드에서 적재하고 있는 데이터가 계속 필요하다면 백그라운드 작업을 계속 진행하기 위해서 cancelLoad 메서드 호출을 하지 않아도 된다. 만약 cancelLoad 메서드로 인해서 백그라운드 취소 동작이 시작 되면 취소한 후에 백그라운드에서 적재했던 데이터를 정리하기 위해 onCanceled 메서드를 구현해야 한다. 똑같이 위 예제에서는 data.recycle()로 데이터를 정리한다. 다음은 로더와 연결되는 액티비티 코드이다.

public class ThumbnailActivity extends FragmentActivity implements LoaderManager.LoaderCallbacks<Bitmap> { private static final int LOADER_ID = "thumb_loader".hashCode(); private Integer mediaId; private ImageView thumb; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.ch4_example1_layout); thumb = (ImageView) findViewById(R.id.thumb); mediaId = getMediaIdFromIntent(getIntent()); if (mediaId != null) getSupportLoaderManager().initLoader(LOADER_ID, null, this); } @Override protected void onDestroy() { super.onDestroy(); if (isFinishing()) { getSupportLoaderManager().destroyLoader(LOADER_ID); } } @Override public Loader<Bitmap> onCreateLoader(int i, Bundle bundle) { return new ThumbnailLoader(getApplicationContext(), mediaId); } @Override public void onLoadFinished(Loader<Bitmap> loader, Bitmap bitmap) { thumb.setImageBitmap(bitmap); } @Override public void onLoaderReset(Loader<Bitmap> loader) { // we don't need to do anything here. } private Integer getMediaIdFromIntent(Intent intent) { if (Intent.ACTION_SEND.equals(intent.getAction())) { Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM); return new Integer(uri.getLastPathSegment()); } else { return null; } } }

AsyncTask와 비교할 때 코드가 복잡하긴 하지만 액티비티 재시작에도 데이터를 사용하기 위해 캐싱하며 다른 프래그먼트나 액티비티에서 데이터를 이용할 수 있다는 장점이 있다.

- CursorLoader

: CursorLoader는 AsyncTaskLoader의 서브 클래스로 DB 커서를 이용한다. 커서는 iterator와 거의 같고 데이터 집합을 자유롭게 순회 가능하다. 

ex) "안드로이드 비동기 프로그래밍" 예제. MediaStore에서 모든 이미지 목록 정보를 불러오는 예

public class MediaStoreActivity extends CompatibleActivity
implements LoaderManager.LoaderCallbacks<Cursor> {

    public static final int MS_LOADER = "ms_crsr".hashCode();

    private MediaCursorAdapter adapter;

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

        setContentView(R.layout.ch4_example2_layout);

        adapter = new MediaCursorAdapter(getApplicationContext());

        GridView grid = (GridView)findViewById(R.id.grid);
        grid.setAdapter(adapter);

        getSupportLoaderManager()
            .initLoader(MS_LOADER, null, this);
    }

    @Override
    public CursorLoader onCreateLoader(int id, Bundle bundle) {
        return new CursorLoader(this,
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            new String[]{
                MediaStore.Images.Media._ID,
                MediaStore.Images.Media.DISPLAY_NAME
            }, "", null, null);
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor media) {
        adapter.changeCursor(media);
    }

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
        adapter.changeCursor(null);
    }

    @Override
    protected void onStop() {
        super.onStop();
        if (isFinishing()) {
            getSupportLoaderManager().destroyLoader(MS_LOADER);
        }
    }
}

public class MediaCursorAdapter extends SimpleCursorAdapter {
    private static String[] FIELDS = new String[]{
        MediaStore.Images.Media._ID,
        MediaStore.Images.Media.DISPLAY_NAME
    };

    private static int[] VIEWS = new int[]{
        R.id.media_id, R.id.display_name
    };

    public MediaCursorAdapter(Context context) {
        super(context, R.layout.ch4_example2_cell, null, FIELDS, VIEWS, 0);
    }
}


- 로더 조합

: 두 예제 AsyncTaskLoader과 CursorLoader을 조합해서 디바이스에 있는 모든 영상의 썸네일을 백그라운드로 전부 적재하고 그리드 뷰로 보여주는 코드를 작성해보자. 위에서 구현한 CursorLoader는 영상의 ID를 얻었다. 이 ID를 위 예제 ThumbnailLoader에 전달하여 이미지를 적재하면 된다. 먼저 별도의 생성자를 만들고 mediaId를 설정하고 얻는 메서드를 추가한다.  

    public ThumbnailLoader(Context context) {
        super(context);
    }

    public Integer getMediaId() {
        return mediaId;
    }

    
    public void setMediaId(Integer newMediaId) {
        if(mediaId!=null && !mediaId.equals(newMediaId)){
        	this.mediaId = newMediaId;
            onContentChanged();	
        }
    }

onContentChanged()는 로더가 현재 시작 상태이면 forceload()를 호출해 백그라운드 적재를 강제로 시킨다. 중지 상태이면 takeContentChanged() 리턴 값을 true로 만든다. takeContentChanged()에 대해서는 조금 뒤에 알 수 있다. 따라서 위 코드는 로더에 새로운 mediaId 값이 들어오면 다시 백그라운드 적재가 이루어지고 onLoadFinished가 호출되 새로운 데이터로 refresh된다. 그리고 takeContentChanged()를 처리하기 위해서 onStartLoading() 메서드도 바꿔야 한다. 

    @Override
    protected void onStartLoading() {
        if (data != null)
            deliverResult(data);
        if ((takeContentChanged() || data == null)) {
            forceLoad();
        }
    }

takeContentChanged()가 true이면 즉, 새로운 데이터로 재적재해야 한다면 다시 백그라운드 적재를 하도록 한다.

다음은 GridView에 연결되고 로더를 통해 영상 데이터를 사용할 MediaCursorAdapter 코드를 보자

public class MediaCursorAdapter extends CursorAdapter {

    private LoaderManager mgr;
    private LayoutInflater inf;
    private int count;
    private List<Integer> ids;

    public MediaCursorAdapter(Context ctx, LoaderManager mgr) {
        super(ctx.getApplicationContext(), null, true);
        this.mgr = mgr;
        inf = (LayoutInflater) ctx.
            getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        ids = new ArrayList<Integer>();
    }

    @Override
    public View newView(final Context ctx, Cursor crsr, ViewGroup parent) {
        ImageView view = (ImageView) inf.
            inflate(R.layout.ch4_example3_cell, parent, false);
        view.setId(MediaCursorAdapter.class.hashCode() + ++count);
        mgr.initLoader(
            view.getId(), null, new ThumbnailCallbacks(ctx, view));
        ids.add(view.getId()); // remember the id, so we can clean it up later
        return view;
    }

    @Override
    public void bindView(View view, final Context context, Cursor cursor) {
        Loader<?> l = mgr.getLoader(view.getId());
        ThumbnailLoader loader = (ThumbnailLoader) l;

        Integer mediaId = cursor.getInt(cursor.getColumnIndex(MediaStore.Images.Media._ID));

        // optimisation for orientation changes
        // that would otherwise do unnecessary work
        if (!mediaId.equals(loader.getMediaId())) {
            ImageView image = (ImageView) view;
            image.setImageBitmap(null);
            loader.setMediaId(mediaId);
        }
    }

    public void destroyLoaders() {
        for (Integer id : ids)
            mgr.destroyLoader(id);
    }
}

다음은 Activity와 ThumbnailLoader에 대한 LoaderCallbacks 인터페이스 구현 코드이다.

public class ThumbnailCallbacks implements LoaderManager.LoaderCallbacks<Bitmap> {
    private Context context;
    private ImageView image;

    public ThumbnailCallbacks(Context context, ImageView image) {
        // ensuring that we always only keep a handle on the Application Context
        // so that we don't leak references to the Activity!
        this.context = context.getApplicationContext();
        this.image = image;
    }

    @Override
    public Loader<Bitmap> onCreateLoader(int i, Bundle bundle) {
        return new ThumbnailLoader(context);
    }

    @Override
    public void onLoadFinished(Loader<Bitmap> loader, Bitmap b) {
        // we could just set the bitmap to the imageview, but
        // lets create a drawable that fades from transparent to
        // our bitmap, for a bit of extra swankiness...
        final TransitionDrawable drawable =
                new TransitionDrawable(new Drawable[] {
                        new ColorDrawable(Color.TRANSPARENT),
                        new BitmapDrawable(context.getResources(), b)
                });

        // set our fade-in drawable to the image view
        image.setImageDrawable(drawable);

        // fade in over 0.2 seconds
        drawable.startTransition(200);
    }

    @Override
    public void onLoaderReset(Loader<Bitmap> loader) {
        // nothing much for us to do here.
    }
}

public class MediaStoreActivity extends CompatibleActivity
implements LoaderManager.LoaderCallbacks<Cursor> {

    private static final String TAG = "asyncandroid";
    public static final int MS_LOADER = "ms_crsr".hashCode();

    private MediaCursorAdapter adapter;

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

        setContentView(R.layout.ch4_example3_layout);

        adapter = new MediaCursorAdapter(
            getApplicationContext(),
            getSupportLoaderManager());

        GridView grid = (GridView)findViewById(R.id.grid);
        grid.setAdapter(adapter);

        getSupportLoaderManager().initLoader(MS_LOADER, null, this);
    }

    @Override
    public CursorLoader onCreateLoader(int id, Bundle bundle) {
        return new CursorLoader(this,
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            new String[]{
                MediaStore.Images.Media._ID,
                MediaStore.Images.Media.DISPLAY_NAME
            }, "", null, null);
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor media) {
        adapter.changeCursor(media);
    }

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
        adapter.changeCursor(null);
    }

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

        if (isFinishing()) {
            // if we don't want to use these loaders in other activities
            // this is our last chance to clean them up.
            getSupportLoaderManager().destroyLoader(MS_LOADER);
            adapter.destroyLoaders();
        }
    }
}


- 응용

: 로더는 파일이나 로컬 DB로부터 데이터를 읽는 곳에 사용하면 좋다. 또한 AsyncTask를 직접 사용해 액티비티 생명주기에 유연하게 대처할 수 있다. 별다른 코드 없이 방향 바꿈 같은 변경 구성을 잘 처리할 수 있다. 액티비티 생명주기와 분리되 있어 HTTP 다운로드 같은 네트워크 전송을 수행하는 데는 AsyncTask나 Handler보다 좋지만 다음에 배울 IntentService나 Service를 이용하는 게 더 좋다.




+ Recent posts