레이블이 백그라운드인 게시물을 표시합니다. 모든 게시물 표시
레이블이 백그라운드인 게시물을 표시합니다. 모든 게시물 표시

2018년 6월 20일 수요일

[안드로이드] WorkManager에 관하여

Services. The life with/without. And WorkManager.

백그라운드를 다루는 안드로이드의 새 버전들의 출시로 인해, 백그라운드 다루기는 이전보다 더 복잡해졌습니다. 그래서 구글은 JetPack의 일부로, 이러한 백그라운드 다루는 작업을 도와주기 위해 WorkManager를 출시했습니다.
WorkManager가 무엇인지 배우기 전에,이것이 왜 만들어지게 되었고, 왜 필요한지를 알 필요가 있습니다. 어떤 컴포넌트 / 라이브러리든지 그 내부에서 어떤일이 일어나고 있고, 왜 그것을 사용해야 하는지 정확히 아는 개발자가 정말 좋은 개발자입니다.
이 글은 3개의 파트로 구성됩니다.
  1. 메모리 기본
  2. 현재 존재하는 백그라운드 방법
  3. WorkManager
먼저, 백그라운드에 관련된 것을 시작하기 전에, 우리는 안드로이드 프로세스의 메모리 관리의 기본에 대해 이해할 필요가 있습니다. 바로 이것이 첫번째 파트가 될 것입니다.

1. 메모리 기본

아주 오래전에, 안드로이드 커널은 리눅스 커널을 기반으로 해서 개발되었습니다. 안드로이드 커널과 모든 리눅스 커널들의 가장 핵심이 되는 차이점은 바로 스왑공간(Swap Space)가 없다는 것입니다.
리눅스의 스왑공간은 램이 꽉 찼을 때 사용됩니다. 시스템은 더 많은 메모리 리소스를 필요로 하지만 램이 꽉 찼을 때, 메모리상의 비활성 페이지를 스왑공간으로 이동시킵니다. 스왑공간은 램의 용량이 작은 디바이스에 도움이 되긴 하지만, 램의 용량을 늘리는것을 대체할 수는 없습니다.스왑공간은 램보다도 엑세스타임이 느린 하드 드라이브에 위치하기 때문입니다.


안드로이드에서는, 스왑공간 같은 것은 없습니다. 시스템의 메모리가 다 소진되었을 때, OOM 킬러를 이용해 프로세스를 강제종료 시켜버립니다. OOM 킬러는 Visible 상태와 소모된 메모리의 양에 기반하여 프로세시를 정리하여 여유 메모리를 확보합니다.
모든 프로세스는 엑티비티 매니저가 부여한 자신의 oom_adj 점수를 가지고 있습니다. 이 점수는 애플리케이션의 상태(Foreground, Background, Background Service 등등...)의 조합입니다. 다음은 모든 oom_adj값을 보여주고 있습니다.
# Define the oom_adj values for the classes of processes that can be
# killed by the kernel.  These are used in ActivityManagerService.
    setprop ro.FOREGROUND_APP_ADJ 0
    setprop ro.VISIBLE_APP_ADJ 1
    setprop ro.SECONDARY_SERVER_ADJ 2
    setprop ro.BACKUP_APP_ADJ 2
    setprop ro.HOME_APP_ADJ 4
    setprop ro.HIDDEN_APP_MIN_ADJ 7
    setprop ro.CONTENT_PROVIDER_ADJ 14
    setprop ro.EMPTY_APP_ADJ 15
프로세스의 oom_adj의 값이 클수록 커널의 OOM 킬러에게 정리당하기 쉽습니다. OOM킬러는 현재 사용가능한 여유메모리 크기와 oom_adj 임계값을 기반으로 구성한 규칙을 사용합니다. 즉, OOM 킬러의 조건은 다음과 같습니다.
여유 메모리의 공간이 X 보다 작을 때, oom_adj값이 Y보다 큰 프로세스를 정리하라!




즉, 가장 중요한 내용은, 앱이 메모리를 적게 소비할수록, 프로세스가 정리되지 않고 중요한 내용을 다룰 기회가 더 많아진다는 것입니다. 그리고, 두번째로 중요한 내용은 애플리케이션의 상태에 대해 이해하는 것입니다. 그래서, 앱이 백그라운드에 진입했을 때에도 뭔가를 지속적으로 하고싶으면, 서비스 컴포넌트를 사용해야 합니다.
  • 서비스는 UI를 제공하지 않고, 백그라운드에서 오래 걸리는 동작을 수행할 수 있도록 하는 앱 컴포넌트입니다.
다음과 같이 서비스를 사용해야 했던 몇가지 이유가 있습니다.
  1. 시스템에게 이 프로세스가 오래 걸리는 작업이 있음을 알려주고, 그에 맞는 oom_adj 점수를 얻도록 하기 위함입니다.
  2. 안드로이드 애플리케이션의 4대 진입점 중 하나입니다.
  3. 서비스를 별개의 프로세스에서 실행시킬 수 있습니다.
그러나!! 서비스를 사용하는 것의 단점이 있습니다. 프로세스가 계속 실행되고 있는 것이므로 배터리를 엄청나게 소비합니다.
아마 모든 개발자들이 아무런 제한없이 백그라운드에서, 원하는 모든 동작들을 수행했을 것입니다. 그래서, 구글은 마시멜로에서 처음 도즈(Doze)모드를 도입하고 누가에서 더 발전시켰습니다.
만약 도즈모드에 익숙하지 않다면, 반드시 익숙해지길 권합니다. 유저가 디바이스의 스크린을 끄고나면, 도즈모드가 시작되어 네트워크 통신, Sync, GPS, 알람, 와이파이 스캔 등을 비활성화 시켜버립니다. 이 도즈모드는 사용자가 스크린을 켜거나, 디바이스를 충전기에 연결할 때까지 유지됩니다. 중요하지 않은 일을 수행하는 앱의 개수를 줄임으로써 디바이스의 배터리를 절약하도록 합니다.
구글은 도즈모드를 통해 시작한 백그라운드 제한을 오레오에서 더더욱 강화했습니다. API 26으로 타게팅된 앱이 백그라운드 서비스 생성을 허가받지 않은 채로 startService() 메서드를 호출하려고 하면 IllegalStateException 예외를 던집니다.
혹자는 API 26으로 타게팅을 안하면 된다고 생각할 수도 있지만, 구글에서 다음과 같은 정책을 발표했습니다. (정책확인) 요약하자면 다음과 같습니다.
  • 2018년 8월 : 새로 출시되는 앱들은 반드시 API 26(Oreo 8.0) 이상
  • 2018년 11월 : 기존 앱들도 API 26(Oreo 8.0) 이상
  • 2019년 이후 : 매년 targetSdkVersion 요구사항이 향상될 것입니다. 안드로이드가 매년 새로운 버전을 낼 때마다, 모든 앱들은 해당 API 레벨 이상을 타겟팅해야 합니다.
이를 통해 알 수 있는 것은....
백그라운드에서 오랜시간 동작할 수 있다는 서비스의 기본목적을 수행하는 것이 허용되지 않게 되었다. 그러므로, 백그라운드 작업을 위해 더 이상 서비스를 사용하지 않게 될 것이다.
서비스를 대체하여 백그라운드에서 동작을 수행하려면, job API를 사용해야 합니다.

2. 현재 존재하는 백그라운드 방법

약간의 KB를 다운로드하는 간단한 네트워크 예제를 살펴봅시다.
가장 직접적인 방법(그리고 부정확한...)은 해당 동작을 수행하는 별도의 스레드를 가지는 것입니다.
int threads = Runtime.getRuntime().availableProcessors();
ExecutorService executor = Executors.newFixedThreadPool(threads);
executor.submit(myWork);
어떤 유저가 매우 느린 3G환경을 가지고 있고, 엘리베이터를 타면서 로그인 동작을 수행했다고 생각해 봅시다. 그리고, 네트워크 작업 중에 유저가 전화를 받게됩니다.
OkHttp의 기본 타임아웃은 꽤 큰 편입니다.
  • connectTimeout = 10_000;
  • readTimeout = 10_000;
  • writeTimeout = 10_000;
게다가, 기본적으로 네트워크의 재시도 회수는 3번입니다.
그러므로, 가장 최악의 경우 90초 (30 * 3)가 걸리게 됩니다.
자 그러면 다음의 문제를 생각해 봅시다.

로그인은 성공했을까요???

앱이 백그라운드에 있는 동안, 우리는 모릅니다. 우리가 위에서 살펴봤듯이, 프로세스가 계속 살아있어서 로그인 요청에 대한 응답을 받아서 유저정보를 잘 저장했을지를 보장할 수 없습니다. 혹, 사용자의 전화가 오프라인 상태가 되어 인터넷 연결이 끊길 수도 있습니다
유저 입장에서, "나는 정보를 입력하고 버튼을 눌렀으니 로그인이 잘 되었을 꺼야" 라고 생각할 수 있습니다. 그렇지만 만약에 로그인이 되어있지 않다면, 유저는 앱의 안좋은 UX를 탓하게 될 것입니다. 그러나, 이는 UX의 문제가 아니라 기술적인 문제입니다.
그래서 JobScheduler가 이 상황을 해결하기 위해 등장했습니다.
ComponentName service = new ComponentName(this, MyJobService.class);
JobScheduler mJobScheduler = (JobScheduler)getSystemService(Context.JOB_SCHEDULER_SERVICE);
JobInfo.Builder builder = new JobInfo.Builder(jobId, serviceComponent)
 .setRequiredNetworkType(jobInfoNetworkType)
 .setRequiresCharging(false)
 .setRequiresDeviceIdle(false)
 .setExtras(extras).build();
mJobScheduler.schedule(jobInfo);
JobScheduler는 시작된 Job을 스케쥴링합니다. 적땅한 시간이 되면, 시스템은 MyJobService를 시작하고, onStartJob() 메서드 안의 내용들을 수행할 것입니다. 꽤 좋은 이론이지만, JobScheduler는 최소 API21 이상에서만 사용할 수 있습니다. 게다가... API 21, 22에서의 JobScheduler는 버그덩어리 컴포넌트 였습니다. 즉, JobScheduler를 제대로 사용하려면 최소 API 레벨은 23 이여야 한다는 것입니다.
만약 API21, 22에서도 제대로 사용하려면 JobDispatcher를 사용할 수 있습니다.
Job myJob = firebaseJobDispatcher.newJobBuilder()
 .setService(SmartService.class)
 .setTag(SmartService.LOCATION_SMART_JOB)
 .setReplaceCurrent(false)
 .setConstraints(ON_ANY_NETWORK)
 .build();
firebaseJobDispatcher.mustSchedule(myJob);
그러나.... 이것은 구글플레이서비스에 의존성을 가지고 있습니다. 그러므로, 아마존기반의 디바이스, 중극 제조사에서는 사용할 수 없는 방법입니다. 그러므로 JobDispatcher도 좋은 선택은 아닙니다. 그러면, 오레오 이전 디바이스에서, 기존 서비스의 이점을을 취하려면 어떻게 해야 할까요??
바로 서포트 라이브러리의 JobIntentService를 이용하면 됩니다. 이 API는 기존의 인텐트서비스를 job으로 실행할 수 있습니다. 그러나, 이것도 오레오에서 가능한 빨리 호출을 수행하는데 도움이 되지 않습니다. 그러므로, 다시 원점으로 돌아갑니다. 현재 실행중인 기기의 안드로이드 버전에 따른 관리, 백그라운드에서 작업을 실행하고, 적절한 스케쥴러에게 작업 등록 등...

3. WorkManager

위의 내용을 종합했을 때, 디바이스의 안드로이드 버전 / 구글플레이서비스의 여부에 따라 다른 해결책들이 존재합니다. 그러므로, 여러분은 이걸 상황에 맞게 모두 구현해야 한다는 고민에 빠지게 될 것입니다. 안드로이드 팀은 이러한 불만사항을 수용하여 Google I/O에서 WorkManager라는 해결책을 제시했습니다.
WorkManager는 시스템 기반의 백그라운드 프로세싱 API를 제공하여, 구현을 단순화하도록 도와줍니다. 이 클래스는 앱이 포그라운드에 없어도, 백그라운드 Job에서 작업들이 수행되도록 합니다. JobScheduler나 Firebase의 JobDispatcher를 사용할 수 있다면 사용하고, 앱이 포그라운드라면 그 프로세스에서 가급적 처리할 수 있도록 합니다. 개발자가 백그라운드 처리과정을 깊게 신경쓰지 않아도 되도록 간단하게 래핑해놓았습니다.
WorkManager 라이브러리는 몇개의 컴포넌트를 가지고 있습니다.
  • WorkManager : 인수 / 제약조건을 가진 작업을 받아서, 큐에 추가합니다.
  • Worker : 백그라운드 스레드에서 동작하는 doWork() 메서드 하나만 가지고 있습니다. 모든 백그라운드 태스크들은 이 메서드 안에서 수행되어야 합니다. 가능한한 심플하도록 구성해야 합니다.
  • WorkRequest : Worker가 어떤 인수와 제약조건(ex : 인터넷, 충전 등)와 함께 큐에 추가되어야할지를 명시하는 역할입니다.
  • WorkResult : 성공, 실패, 재시도
  • Data : Worker로 주고받는 영구적인 키-벨류 쌍의 값 입니다.
먼저, Worker 클래스를 상속받아 새로운 클래스를 만들고 doWork()메서드를 구현합니다.
public class LocationUploadWorker extends Worker {
    ...
     //Upload last passed location to the server
    public WorkerResult doWork() {
        ServerReport serverReport = new ServerReport(getInputData().getDouble(LOCATION_LONG, 0),
                getInputData().getDouble(LOCATION_LAT, 0), getInputData().getLong(LOCATION_TIME,
                0));
        FirebaseDatabase database = FirebaseDatabase.getInstance();
        DatabaseReference myRef =
                database.getReference("WorkerReport v" + android.os.Build.VERSION.SDK_INT);
        myRef.push().setValue(serverReport);
        return WorkerResult.SUCCESS;
    }
}
다음으로, 생성한 Worker를 실행하기 위해 WorkManager를 호출합니다.
Constraints constraints = new Constraints.Builder().setRequiredNetworkType(NetworkType
            .CONNECTED).build();
Data inputData = new Data.Builder()
            .putDouble(LocationUploadWorker.LOCATION_LAT, location.getLatitude())
            .putDouble(LocationUploadWorker.LOCATION_LONG, location.getLongitude())
            .putLong(LocationUploadWorker.LOCATION_TIME, location.getTime())
            .build();
OneTimeWorkRequest uploadWork = new OneTimeWorkRequest.Builder(LocationUploadWorker.class)
            .setConstraints(constraints).setInputData(inputData).build();
WorkManager.getInstance().enqueue(uploadWork);
WorkManager가 남은 과정들을 모두 처리할 것입니다. WorkManager는 가장 최적의 스케쥴을 선택하여 작업을 큐에 추가할 것입니다. 또한, 작업의 인수, 세부사항, 변경되는 상태를 저장할 것입니다. 심지어, LiveData를 이용하여 작업상태를 구독할 수도 있습니다.
WorkManager.getInstance().getStatusById(locationWork.getId()).observe(this,
        workStatus -> {
    if(workStatus!=null && workStatus.getState().isFinished()){
         ...
    }
});
WorkManager 라이브러리의 구조는 다음의 그림을 참고할 수 있습니다.


  1. 백그라운드로 수행할 작업을 Worker의 doWork() 메서드를 구현하여 정의합니다.
    2.생성한 Worker 인스턴스를 가지고 WorkRequest를 생성합니다. 필요하다면WorkRequest를 생성할 때 DataConstraints를 추가합니다.
  2. WorkManager의 enqueue() 메서드를 이용해 생성한 WorkRequest를 큐에 추가합니다. 이 떄, 내부적으로 Room에 작업을 저장합니다.
  3. 내부적으로 적절한 스케쥴러(JobScheduler, JobDispatcher, Executor, AlarmManager)중 하나를 골라 doWork() 메서드를 호출합니다.
  4. 작업의 결과는 LiveData를 이용해 구독할 수 있고, 해당 작업의 산출물은 Data로 이용할 수 있습니다.
이것은 어디까지나 기본동작으로, 여러 추가기능들이 있습니다. 이 추가기능을 따로 써도 되고, 조합해서도 쓸 수 있습니다.
  • 주기적으로 실행하기
Constraints constraints = new Constraints.Builder().setRequiredNetworkType
        (NetworkType.CONNECTED).build();
PeriodicWorkRequest locationWork = new PeriodicWorkRequest.Builder(LocationWork
        .class, 15, TimeUnit.MINUTES).addTag(LocationWork.TAG)
        .setConstraints(constraints).build();
WorkManager.getInstance().enqueue(locationWork);
  • 두개 이상의 job을 순차적으로 실행하기
WorkManager.getInstance(this)
        .beginWith(Work.from(LocationWork.class))
        .then(Work.from(LocationUploadWorker.class))
        .enqueue();
  • 두개 이상의 job을 병렬로 실행하기
WorkManager.getInstance(this).enqueue(Work.from(LocationWork.class,  LocationUploadWorker.class));
  • Note : 주기적 job과 일회성 job을 함께 조합할 수 없습니다.

이 외에도 WorkManager를 이용해 할 수 있는 많은 상황들이 있습니다. Work 취소하기, 결합하기, 연결하기, 한 Work의 Data를 다른 Work로 합치기 등등... 이 다른 기능들을 더 확인하려면 문서를 찾아보길 권합니다.

원문

https://medium.com/google-developer-experts/services-the-life-with-without-and-worker-6933111d62a6

2018년 1월 15일 월요일

[안드로이드] 스레드3 - 기본 스레드의 생명주기 관리

기본 스레드의 관리

  • 안드로이드에서도 자바와 마찬가지로 java.lang.Thread 클래스가 모든 스레드의 근간이 된다.

1.기본개념

생명주기

  • Thread.State enum에 정의되어 있으며, Thread 인스턴스의 getStatus() 메서드를 이용해 확인할 수 있다.

NEW

  • Thread 클래스의 인스턴스를 생성한 직후의 상태
  • Thread 인스턴스 생성이 다른 클래스의 인스턴스 생성보다 무겁지는 않다.
  • 생성된 스레드는 현재 스레드와 동일한 우선순위의 스레드그룹으로 할당된다.
    • ex) UI스레드에서 생성하면 UI스레드와 동일한 우선순위의 스레드그룹이 된다.

RUNNABLE

  • Thread.start() 메서드가 호출되어 실행환경이 설정된 직후부터 스레드가 실행되는 중의 상태
  • 이후에 OS의 스케쥴러가 스레드를 선택하면 run() 메서드가 호출되어 태스크가 실행된다.

BLOCK / WAITING / TIMED_WAITING

  • I/O 동작, 다른스레드와의 자원동기화, 블로킹 API호출 등 스레드의 실행이 멈출 때의 상태들
  • 스레드를 명시적으로 실행을 멈추는 방법
    • Thread.sleep() : 스레드를 일정시간 동안 자도록 만든후 다시 실행하도록 스케쥴되도록 함
    • Thread.yield() : 현재 실행을 포기하고 스케줄러가 어떤 스레드를 실행할지 결정하도록 함

TERMINATED

  • run() 메서드가 실행을 완료하면 스레드가 종료되고 스레드의 자원이 해제된 후의 상태
  • 스레드가 종료되고 나면 해당 스레드의 인스턴스와 실행환경은 재사용할 수 없다.
  • 실행환경을 설정/해제하는 것은 매우 무거운 동작
  • 스레드가 동작을 명시적으로 종료하도록 요청할 수도 있다. - interrupt() 메서드 호출

인터럽트

  • 스레드를 명시적으로 종료하고 싶을 때, 스레드에 interrupt() 메서드를 이용해 인터럽트를 요청
  • 어디까지나 요청일 뿐, 인터럽트 호출에 대한 실행여부는 스레드 자체가 결정한다.
  • 일반적으로 스레드의 인터럽트는 공동으로(Collaboratively) 구현되어야 한다.
class TestThread extends Thread {
    @Override
    public void run() {
        //isInterrupt() 메서드를 수시로 검사하여 true를 반환하면 태스크를 완료한 것으로 처리
        while(!isInterrunpted()) {
            //스레드가 살아있음
        }
        //태스크 완료되어 스레드 종료됨
    }
}
  • 스레드 내에서 블로킹 API를 사용중이라 스레드가 차단되어있는 중에 인터럽트 신호를 받으면 블로킹 API는 InterruptException 을 던진다.
    • InterruptException이 던져지면 인터럽트 플래그가 리셋되므로 주의한다.
class TestThread extends Thread {
    @Override
    public void run() {
        //isInterrupt() 메서드를 수시로 검사하여 true를 반환하면 태스크를 완료한 것으로 처리
        while(!isInterrunpted()) {
            doBlockingAction();
        }
    }
    
    public void doBlockingAction() {
        try {
            //Blocking API 호출
        } catch (InterruptException e) {
            //1.자원 정리
            //2.isInterrupt() 플래그가 초기화되었으므로, run() 메서드가 isInterrupt()를 제대로 인식하도록 자신을 다시 인터럽트
            Thread.currentThread().interrupt();
        }
    }
}

잡히지 않는 예외

  • 모든 스레드는 RuntimeException이 발생했을 때, 이 예외를 처리해주지 않으면 스레드가 비정상적으로 종료된다.
  • 스레드 내에서 발생하는 RuntimeException을 잡으려면, 스레드가 비정상 종료되기 전에 호출되는 UncaughtExceptionHandler를 부착한다.
    • 모든 스레드에 전역 핸들러를 부착할 경우 : Thread 클래스의 setDefaultUncaughtExceptionHandler() 정적 메서드 호출
    • 특정 스레드에 지역 핸들러를 부착할 경우 : Thread 인스턴스의 setUncaughtExceptionHandler() 메서드 호출
    • 위의 두가지 경우가 모두 부착되어 있을 경우에는 특정 스레드마다 붙은 핸들러가 우선시되어 모든스레드에 부착된 핸들러는 호출되지 않는다.
  • 안드로이드 런타임은 앱이 시작될 때 전역 핸들러를 부착한다. 이 전역 핸들러의 기본동작은 잡히지 않은 예외가 발생했을 때, 프로세스를 죽이는 것으로 모든 스레드가 동등하게 처리된다.
  • Ex - 앱이 크래시되기 직전에 로그남기기
// 앱 시작시 실행되는 아무 클래스
public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate(0;
        //새로운 전역 핸들러를 설정
        Thread.setDefaultUncaughtExceptionHandler(new LogExceptionHandler());
    }
}

//새로 정의한 예외핸들러 클래스
public class LogExceptionHandler implements Thread.UncaughtExceptionHandler {
    private final Thread.UncaughtExceptionHandler defaultHandler;
    
    public LogExceptionHandler() {
        defaultHandler = Thread.getDefaultUncaughtExceptionHandler();
    }
    
    @Override
    public void uncaughtException(Thread thread, Throwable throwable) {
        //로그를 파일로 남기거나 서버로 전송
        
        //안드로이드 런타임이 붙였던 기존 핸들러로 작업 위임
        default.Handler.uncaughtException(thread, throwable);
    }
}

2.스레드 관리

스레드 정의(Define)와 시작

  • 스레드 정의 방법에 따라, 그 스레드의 생명주기가 다르다. 그래서 때때로 안드로이드의 컴포넌트(Activity, Service, BroadcastReceiver, ContentProvider)와 생명주기가 달라서 메모리 누수를 일으키기도 한다.

익명 내부클래스로 정의

  • 익명 내부클래스는 구현이 간단하지만, 외부클래스의 인스턴스에 대한 참조를 내부적으로 유지하고 있다.
public class AnyObject {
    //UI 스레드에서 호출되는 메서드
    @UiThread
    public void anyMethod() {
        new Thread() {
            @Override
            public void run() {
                // 긴 태스크를 실행한다고 가정
                // 해당 스레드가 살아있는 동안 이 스레드는 AnyObject 인스턴스의 참조를 유지하고 있다.
                doLongRunningTask();
            }
        }.start();
    }
}

공개된 독립 클래스로 정의

  • 스레드를 실행하는 인스턴스에 대한 잠재적인 참조를 유지하지는 않지만, 클래스의 개수가 많아진다.
class TestThread extends Thread {
    @Override
    public void run() {
        // 긴 태스크를 실행한다고 가정
        doLongRunningTask();
    }
}

public class AnyObject {
    //UI 스레드에서 호출되는 메서드
    private TestThread testThread;
    
    @UiThread
    public void anyMethod() {
        testThread = new TestThread();
        testThread.start();
    }
}

정적 내부 클래스로 정의

  • 외부 클래스의 클래스 객체에 대한 내부참조를 유지하고 있다. 그렇지만 클래스객체의 참조는 메모리 누수의 원인이 되지 않는다.
public class AnyObject {
    static class TestThread extends Thread {
        @Override
        public void run() {
            // 긴 태스크를 실행한다고 가정
            doLongRunningTask();
        }
    }

    private TestThread testThread;

    //UI 스레드에서 호출되는 메서드
    @UiThread
    public void anyMethod() {
        testThread = new TestThread();
        testThread.start();
    }
}

스레드 유지

  • Activity의 설정이 변경되면 기본적으로 Activity는 재시작되며 가지고있던 멤버변수들도 초기화된다. - 당연히 스레드도 초기화되어 새로운 객체로 생성되어 다시 시작된다.
  • 해당 증상을 방지하기 위해 Activity와 Fragment는 설정의 변경에도 객체를 유지할 수 있는 방법을 제공한다.

Activity에서 스레드 유지

  • pulbic Object onRetainNonConfigurationInstance()
    • 설정 변경이 일어나기 전에 플랫폼에 의해 호출되는 콜백메서드
    • 새로 생성되는 Activity 객체에 전달할 객체를 반환하도록 구현해야 한다.
  • public Object getLastNonConfigurationInstance()
    • 설정 변경이 이루어진 후, 새로 생성된 Activity 객체에서 호출하는 메서드
    • onRetainNonConfigurationInstance() 메서드에서 반환한 객체를 반환한다.
    • 설정 변경이 아닌 다른이유로 Activity가 재시작 될 경우 null을 반환한다.
  • 예제
public class TestActivity extends Activity {
    private static class TestThread extends Thread {
        @Override
        public void run() {
            //TODO::긴 작업
        }
    }

    private static TestThread thread;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout_activity_test);
        Object retainedObject = getLastNonConfigurationInstance();
        if (retainedObject != null) {
            thread = retainedObject;
        } else {
            thread = new TestThread();
            thread.start();
        }
    }
    
    @Override
    public Object onRetainNonConfigurationInstance() {
        if (thread != null && thread.isAlive()) {
            return thread;
        }
        return null;
    }
}

Fragment에서 스레드 유지

  • Fragment의 상태 유지요청 메서드인 setRetainInstance(true)를 이용하여 프레그먼트를 유지한다.
  • 예제
public class TestFragment extends Fragment {
    private static class TestThread extends Thread {
        @Override
        public void run() {
            //TODO::긴 작업
        }
    }

    private TestThread thread;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (thread == null) {
            thread = new Thread();
            thread.start();
        }
        // 이 메서드만 설정해주면 프레그먼트는 엑티비티의 설정이 변경되는 동안 객체가 유지된다.
        setRetainInstance(true);
    }
}

public class TestActivity extends Activity {
    TestFragment fragment;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout_activity_test);
        FragmentManager manager = getFragmentManager();
        fragment = manager.findFragmentByTag("TestFragment");
        if (fragment == null) {
            FragmentTransaction transaction = manager.beginTransaction();
            fragment = new Fragment();
            transaction.add(fragment, "TestFragment");
            transaction.commit();
           
        }
    }
}