Efficient Android Threading #12 로더를 이용한 자동 백그라운드 실행
이 글은 Efficient Android Threading 의 일부 내용만 발췌한 내용입니다.
자세한 내용은 책을 구입해서 보세용.
-
로더(Loader) 프레임워크는 콘텐트 프로바이더 또는 다른 데이터 소스에 대해 비동기 작업을 실행하기 위한 강력한 방법을 제공한다.
콘텐츠가 변하거나 데이터 소스에 추가될 때 로더 프레임워크는 비동기적으로 데이터를 불러오고 앱으로 전달할 수 있다.
API Level 11(허니콤)에서 추가되었다.
Loader 는 액티비티 또는 프래그먼트와 연결하는 것이 가능하다.
콘텐트 프로바이더에 연결하는 경우 CursorLoader 를 사용하면 되고,
다른 데이터 소스의 경우 커스텀 로더 객체로 구현하면 된다.
어떤 로더 유형이든 새로운 로더를 생성하는 콜백, 로더가 새로운 데이터를 전달할 때마다 실행되는 콜백, 로더가 재설정될 때 실행되는 콜백 등 총 세 개의 콜백을 정의해야 한다.
-
로더는 다음의 기능들을 제공한다.
비동기 데이터 관리
데이터 소스에 대해 백그라운드에서 반응하며, 데이터 소스에 새로운 데이터가 생길 때 앱에 콜백을 호출한다.
생명주기 관리
액티비티 또는 프레그먼트가 멈추면 내부 로더도 정지한다.
또한 백그라운드에서 실행 중인 로더는 방향 변경과 같은 설정 변경 후에도 작업을 계속한다.
데이터 캐시
비동기 데이터 로드의 결과가 전달될 수 없는 경우, 전달받는 수신자가 준비될 때 (예를 들어 설정 변경으로 액티비티가 재생성될 때) 전달될 수 있도록 데이터를 캐시한다.
누수 보호
액티비티의 설정 변경이 일어나면 로더 프레임워크는 Context 객체가 누수 손실되지 않도록 보장한다.
-
액티비티가 설정 변경될 때 실행 중인 로더는 새로운 액티비티에서 다시 실행되지 않도록 계속 살아 있다.
로더가 살아서 보존되므로 메모리 누수가 발생할 수 있다.
-
모든 콜백은 UI 스레드에서 호출된다.
14.1. 로더 프레임워크
-
로더 프레임워크는 LoaderManager, Loader, AsyncTaskLoader, CursorLoader 를 포함한다.
-
안드로이드 플랫폼에서 구체적으로 구현된 로더는 CursorLoader 뿐이다.
반면 커스텀 로더는 AsyncTaskLoader 를 확장하여 구현될 수 있다.
** 14.1.1. LoaderManager
-
LoaderManager 는 액티비티 또는 프래그먼트에 의해 사용되는 모든 로더를 관리하는 추상 클래스다.
LoaderManager 는 클라이언트와 클라이언트의 로더 사이에서 중개자 역할을 한다.
Activity 와 Fragment 는 LoaderManager 인스턴스를 보유하며, getLoaderManager() 를 통해 해당 인스턴스를 얻을 수 있다.
-
LoaderManager 는 아래의 메인 함수들을 갖는다.
Loader<D> initLoader(int id, Bundle args, LoaderCallbacks<D> callback)
Loader<D> restartLoader(int id, Bundle args, LoaderCallbacks<D> callback)
Loader<D> getLoader(int id)
void destroyLoader(int id)
모든 로더는 고유한 식별자를 갖는다.
일반적으로 앱은 로더를 시작하기 위해 initLoader 또는 restartLoader 를 호출하기만 하면 된다.
-
LoaderManager.Callbacks 는 다음 메소드들을 갖는다.
public Loader<D> onCreateLoader(int id, Bundle args)
public void onLoadFinished(Loader<D> loader, D data)
public void onLoaderReset(Loader<D> loader)
-
로더가 더 이상 필요 없을 때는 destroyLoader(id) 를 통해 명시적으로 로더를 파괴할 수 있다.
-
initLoader 는 식별자가 일치하는 경우, 가능한 로더를 재사용한다.
명시한 식별자를 가지고 있는 로더가 존재하지 않으면 onCreateLoader 는 새로운 로더를 요청한다.
그러면 데이터 로딩이 시작되고 결과가 onLoadFinished 에 전달된다.
로더 식별자가 이미 존재하는 경우 최신 데이터 로딩 결과는 onLoadFinished 에 직접 전달된다.
-
하부 데이터 소스가 클라이언트 생명주기 전반에 걸쳐 동일할 때는 initLoader 를 호출해주지만, 생명주기 동안 하부 데이터 소스가 바뀔 수 있다면 restartLoader 를 사용해야 한다.
** 14.1.2. LoaderCallbacks
-
클라이언트에서 Loader.forceLoad() 를 호출하여 새로운 데이터 로딩을 강제할 수 있다.
Loader.cancelLoad() 로 초기화된 로딩을 취소할 수도 있다.
** 14.1.3. AsyncTaskLoader
-
AsyncTaskLoader 는 내부적으로 AsyncTask.executeOnExecutor 를 사용하기 때문에 버전별 동작 차이 문제가 없다.
호환성 패키지의 AsyncTaskLoader 는 순차적으로 실행되므로 안드로이드의 기본 AsyncTask 에 의존하지 않는다.
-
AsyncTaskLoader 는 동시 작업(즉, 활성 스레드)의 수를 최소로 유지하려고 한다.
forceLoad() 로 연속적인 로딩을 강제하는 클라이언트는 모든 호출의 결과를 전달받지 못할 수 있다.
그 이유는 AsyncTaskLoader 가 새로운 것을 초기화하기 전에 이전의 로딩을 취소하기 때문이다.
-
로딩은 설정변경 때문에 시작될 수도 있고, 하부 데이터 세트가 변경되었음을 알림 받아 시작될 수도 있다.
즉 경우에 따라 UI 스레드에서 onLoadFinished 가 여러 차례 불릴 수 있고, 이로 인해 반응성이 안 좋아 질 수 있다.
이 경우 연속적인 데이터 로딩을 특정 지연시간 이후에만 일어나도록 AsyncTaskLoader 에 데이터 전달을 조절할 수 있다.
setUpdateThrottle(long delayMs) 를 통해 지연시간을 설정하면 된다.
-
cf) support lib 에는 API 레벨에 걸쳐 일관된 실행을 유지하는 자체 ModernAsyncTask 구현이 들어 있다.
14.2. CursorLoader 를 이용한 쉬운 데이터 로딩
-
CursorLoader 는 SQLite DB 에서 온 Cursor 객체가 아닌 콘텐트 프로바이더에서 전달된 Cursor 객체하고만 사용할 수 있다.
** 14.2.1. CursorLoader 사용하기
-
CursorLoader 는 Activity 클래스에서 사용되지 않는 managedQuery 와 startManagingCursor 메서드를 대체했다.
결과적으로 클라이언트는 내부 생명주기 관리를 방해하거나 커서 자체를 닫으려 해서는 안 된다.
** 14.2.2. 예제 : 연락처 리스트
-
새로운 데이터 세트가 로드되면 CursorLoader 는 이전 Cursor 를 닫는다.
따라서 CursorAdapter 를 사용할 때에는 changeCursor 가 아닌 swapCursor 를 사용해야 한다.
** 14.2.3. CRUD 지원 추가
14.3. 커스텀 로더 구현
-
로더는 플랫폼에 의해 지원되기 때문에 일반적으로는 CP 와 함께 사용되지만, 다른 데이터 소스는 커스텀 로더로 처리할 수 있다.
이를 위해서는 더 많은 작업과 프레임워크에 대한 더 깊은 이해가 필요하다.
모든 자격을 갖춘 로더는 다음과 같은 기능을 지원해야 한다.
로더 생명주기
백그라운드 로딩
콘텐츠 관리
캐시된 결과 전달
-
커스텀 로더는 정적 또는 외부 클래스로 선언해야 한다.
만약 그렇지 않다면 onCreateLoader 에서 로더가 반환될 때 RuntimeException 이 반환된다.
** 14.3.1. 로더 생명주기
-
-
Reset
캐시된 데이터가 해제된 로더의 초기 및 최종 상태
Started
비동기 데이터 로딩을 시작하고 LoaderCallback.onLoadFinished 콜백 호출을 통해 결과를 전달한다.
Stopped
로더는 클라이언트로의 데이터 전달을 멈춘다.
콘텐츠 변경 시 여전히 백그라운드에서 데이터를 로드할 수 있지만, 새로운 데이터 로딩 초기화 없이 최근 데이터를 쉽게 가져올 수 있도록 데이터가 로더에 캐시된다.
Abandoned
새로운 로더가 데이터 소스에 연결되기 전에 데이터가 저장되는 재설정 전의 중간 상태, 이 상태는 거의 사용되지 않는다.
-
로더의 생명주기는 LoaderManager 에 의해 제어되고, 일반적으로 클라이언트는 로더 메서드로 직접 상태를 수정해서는 안 된다.
대신 클라이언트는 initLoader 와 restartLoader 를 실행하여 시작 로더가 존재하는 것을 확인하고 로더와 상호작용하는 일은 LoaderManager 에 맡겨야 한다.
** 14.3.2. 백그라운드 로딩
-
AsyncTaskLoader 를 사용하면 쉽다.
public D loadInBackground() 를 오버라이드해서 D 만 넘겨주면 된다.
** 14.3.3. 콘텐츠 관리
-
Observable, Observer, BroadcastReceiver’s Intent, FileObserver 등을 통해 하부 데이터의 변경을 감지할 수 있다.
-
커스텀 로더는 로더가 시작할 때 takeContentChanged() 를 통해 로드한 콘텐츠가 있는지 확인해야 한다.
@Override
protected void onStartLoading(){
super.onStartLoading();
if (takeContentChanged()){
forceLoad();
}
}
takeContentChanged() 가 불리면 컨텐츠가 변경되었다는 flag 를 꺼준다.
그리고 onContentChanged() 가 불리면 flag 가 켜진다.
** 14.3.4. 캐시된 결과를 전달
** 14.3.5. 예제 : 커스텀 파일 로더
-
public class FileLoader extends AsyncTaskLoader<List<String>> {
// Cache the list of file names.
private List<String> mFileNames;
private class SdCardObserver extends FileObserver {
public SdCardObserver(String path) {
super(path, FileObserver.CREATE|FileObserver.DELETE);
}
@Override
public void onEvent(int event, String path) {
// This call will force a new asynchronous data load
// if the loader is started otherwise it will keep
// a reference that the data has changed for future loads.
onContentChanged();
}
}
private SdCardObserver mSdCardObserver;
public FileLoader(Context context) {
super(context);
String path = context.getFilesDir().getPath();
mSdCardObserver = new SdCardObserver(path);
}
@Override
protected void onStartLoading() {
super.onStartLoading();
// Start observing the content.
mSdCardObserver.startWatching();
if (mFileNames != null) {
// Return the cache
deliverResult(mFileNames);
}
if (fileNames == null || takeContentChanged()) {
forceLoad();
}
}
@Override
public List<String> loadInBackground() {
File directory = getContext().getFilesDir();
return Arrays.asList(directory.list());
}
@Override
public void deliverResult(List<String> data) {
if (isReset()) {
return;
}
// Cache the data
mFileNames = data;
// Only deliver result if the loader is started.
if (isStarted()) {
super.deliverResult(data);
}
}
@Override
protected void onStopLoading() {
super.onStopLoading();
cancelLoad();
}
@Override
protected void onReset() {
super.onReset();
mSdCardObserver.stopWatching();
clearResources();
}
private void clearResources() {
mFileNames = null;
}
}
** 14.3.6. 여러 개의 로더 처리
'프로그래밍 놀이터 > 안드로이드, Java' 카테고리의 다른 글
[android] ripple mask 의 정체를 알아보자.. (0) | 2018.03.31 |
---|---|
[android] Ripple 에 대해 알아보자 (0) | 2018.03.30 |
Efficient Android Threading #11 AsyncQueryHandler 를 이용한 콘텐트 프로바이더 접근 (0) | 2018.03.27 |
Efficient Android Threading #10 인텐트 서비스 (0) | 2018.03.26 |
Efficient Android Threading #9 서비스 (0) | 2018.03.25 |
댓글