AsyncTask가 실제로 개념적으로 결함이 있습니까? 아니면 뭔가 빠졌습니까?


264

나는이 문제를 몇 달 동안 조사했으며 다른 해결책을 생각해 냈습니다. 모두 큰 해킹이기 때문에 만족스럽지 않습니다. 디자인에 결함이있는 클래스가 프레임 워크에 만들었고 아무도 그것에 대해 이야기하고 있지 않다는 것을 여전히 믿을 수 없으므로 뭔가 빠진 것 같아요.

문제는입니다 AsyncTask. 문서에 따르면

"스레드 및 / 또는 핸들러를 조작 할 필요없이 백그라운드 작업을 수행하고 UI 스레드에 결과를 게시 할 수 있습니다."

그런 다음 예제는에서 예제 showDialog()메소드가 어떻게 호출 되는지 계속 보여줍니다 onPostExecute(). 그러나 대화 상자를 표시하려면 항상 유효한 참조가 필요하고 AsyncTask 는 컨텍스트 객체에 대한 강력한 참조를 보유해서는 안되기 때문에 이것은 전적으로 나에게 부여 된 것처럼 보입니다 .Context

그 이유는 분명합니다. 활동이 파괴되어 작업을 시작하면 어떻게됩니까? 예를 들어 화면을 뒤집었기 때문에 항상 이런 일이 발생할 수 있습니다. 태스크를 만든 컨텍스트에 대한 참조를 유지한다면, 당신은 단지 쓸모 컨텍스트 개체 (윈도우가 파괴 된 것이며에 들고하지 않는 모든 UI 상호 작용 예외와 함께 실패합니다!), 당신은도를 만드는 위험 메모리 누수.

내 논리가 여기에 결함이 없다면, 이것은 다음과 같이 번역됩니다. onPostExecute()문맥에 액세스 할 수없는 경우이 메소드가 UI 스레드에서 실행되는 것이 좋기 때문에 전혀 쓸모가 없습니다. 여기서 의미있는 일은 할 수 없습니다.

한 가지 해결 방법은 컨텍스트 인스턴스를 AsyncTask가 아니라 인스턴스로 전달하는 것 Handler입니다. 작동 : 처리기는 컨텍스트와 작업을 느슨하게 바인딩하므로 누출 위험없이 메시지를 교환 할 수 있습니다 (오른쪽?). 그러나 그것은 AsyncTask의 전제, 즉 처리기를 귀찮게 할 필요가 없다는 것이 잘못되었음을 의미합니다. 동일한 스레드에서 메시지를 보내고 받고 있기 때문에 Handler를 학대하는 것처럼 보입니다 (UI 스레드에서 메시지를 작성하고 UI 스레드에서 실행되는 onPostExecute ()에서 메시지를 전송 함).

그 해결 방법을 사용하더라도 상황이 파괴 될 때 발생하는 작업에 대한 기록없다는 문제가 여전히 있습니다 . 즉, 컨텍스트를 다시 만들 때 (예 : 화면 방향 변경 후) 작업을 다시 시작해야합니다. 이것은 느리고 낭비입니다.

이것에 대한 나의 해결책 ( Droid-Fu 라이브러리에서 구현 됨 )은 WeakReference고유 한 응용 프로그램 객체의 구성 요소 이름에서 현재 인스턴스 로 의 매핑을 유지하는 것입니다 . AsyncTask가 시작될 때마다 해당 맵에 호출 컨텍스트를 기록하고 모든 콜백에서 해당 맵핑에서 현재 컨텍스트 인스턴스를 가져옵니다. 당신은 오래된 컨텍스트 인스턴스를 참조하지 않을 것을이 보장하지만 그리고 당신이 거기에 의미있는 UI 작업을 할 수 있도록 항상 콜백에 유효한 컨텍스트에 액세스 할 수 있습니다. 참조가 약하고 주어진 구성 요소의 인스턴스가 더 이상 존재하지 않으면 지워지기 때문에 누출되지 않습니다.

여전히 복잡한 해결 방법이며 일부 Droid-Fu 라이브러리 클래스를 서브 클래스 화해야하므로이 방법이 상당히 방해가됩니다.

이제 나는 단순히 알고 싶어합니다. 방금 큰 무언가가 누락되었거나 AsyncTask가 실제로 완전히 결함이 있습니까? 당신의 경험은 어떻게 작동합니까? 이 문제를 어떻게 해결 했습니까?

입력 해 주셔서 감사합니다.


1
궁금한 점이 있다면 최근에 IgnitedAsyncTask라는 점화 코어 라이브러리에 클래스를 추가했습니다.이 클래스는 아래 Dianne에 의해 요약 된 연결 / 연결 끊기 패턴을 사용하여 모든 콜백에서 유형 안전 컨텍스트 액세스를 지원합니다. 또한 예외를 발생시키고 별도의 콜백에서 처리 할 수 ​​있습니다. 참조 github.com/kaeppler/ignition-core/blob/master/src/com/github/...
마티아스

: 이것 좀 봐 가지고 gist.github.com/1393552
마티아스

1
질문 도 관련이 있습니다.
Alex Lockwood

비동기 작업을 arraylist에 추가하고 특정 지점에서 모두 닫습니다.
NightSkyCode

답변:


86

이런 식으로 어떻습니까 :

class MyActivity extends Activity {
    Worker mWorker;

    static class Worker extends AsyncTask<URL, Integer, Long> {
        MyActivity mActivity;

        Worker(MyActivity activity) {
            mActivity = activity;
        }

        @Override
        protected Long doInBackground(URL... urls) {
            int count = urls.length;
            long totalSize = 0;
            for (int i = 0; i < count; i++) {
                totalSize += Downloader.downloadFile(urls[i]);
                publishProgress((int) ((i / (float) count) * 100));
            }
            return totalSize;
        }

        @Override
        protected void onProgressUpdate(Integer... progress) {
            if (mActivity != null) {
                mActivity.setProgressPercent(progress[0]);
            }
        }

        @Override
        protected void onPostExecute(Long result) {
            if (mActivity != null) {
                mActivity.showDialog("Downloaded " + result + " bytes");
            }
        }
    }

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

        mWorker = (Worker)getLastNonConfigurationInstance();
        if (mWorker != null) {
            mWorker.mActivity = this;
        }

        ...
    }

    @Override
    public Object onRetainNonConfigurationInstance() {
        return mWorker;
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mWorker != null) {
            mWorker.mActivity = null;
        }
    }

    void startWork() {
        mWorker = new Worker(this);
        mWorker.execute(...);
    }
}

5
예, mActivity는! = null이지만 Worker 인스턴스에 대한 참조가 없으면 해당 인스턴스에 대한 모든 참조도 가비지 제거됩니다. 작업이 영원히 실행되면 어쨌든 메모리 누수가 발생합니다 (작업). 전화 배터리를 방전한다는 것은 말할 것도 없습니다. 또한 다른 곳에서 언급했듯이 onDestroy에서 mActivity를 null로 설정할 수 있습니다.
EboMike

13
onDestroy () 메소드는 mActivity를 널로 설정합니다. 활동이 아직 실행 중이므로 누가 활동에 대한 참조를 보유하고 있는지는 중요하지 않습니다. 그리고 활동 창은 항상 onDestroy ()가 호출 될 때까지 유효합니다. 여기에서 null로 설정하면 비동기 작업은 활동이 더 이상 유효하지 않음을 알게됩니다. (그리고 구성이 변경되면 이전 활동의 onDestroy ()가 호출되고 다음 활동의 onCreate ()가 그들 사이에서 처리 된 메인 루프에서 메시지없이 실행되므로 AsyncTask는 일관성이없는 상태를 보지 못합니다.
hackbod

8
사실이지만 여전히 언급 한 마지막 문제를 해결하지 못합니다. 작업이 인터넷에서 무언가를 다운로드한다고 상상해보십시오. 이 방법을 사용하면 작업이 실행되는 동안 화면을 3 번 뒤집 으면 화면이 회전 할 때마다 화면이 다시 시작되고 마지막을 제외한 모든 작업은 활동 참조가 null이기 때문에 결과를 버립니다.
Matthias

11
백그라운드에서 액세스하려면 mActivity를 중심으로 적절한 동기화를 수행 하고 null 일 때 실행을 처리하거나 백그라운드 스레드가 앱의 단일 글로벌 인스턴스 인 Context.getApplicationContext ()를 가져 오도록 해야합니다. 응용 프로그램 컨텍스트는 할 수있는 작업 (예 : 대화 상자와 같은 UI 없음)으로 제한되며 약간의주의가 필요합니다 (등록 된 수신기와 서비스 바인딩은 정리하지 않으면 영원히 유지됩니다). 특정 구성 요소의 컨텍스트와 관련이 없습니다.
hackbod

4
Dianne에게 감사합니다. 나는 문서가 처음부터 좋기를 바랍니다.
Matthias

20

그 이유는 분명합니다. 활동이 파괴되어 작업을 시작하면 어떻게됩니까?

수동에서 활동 해제 AsyncTask에서을 onDestroy(). 새 활동을 AsyncTaskin에 수동으로 다시 연결하십시오 onCreate(). 정적 내부 클래스 또는 표준 Java 클래스와 10 줄의 코드가 필요합니다.


정적 참조에주의하십시오. 정적 강력한 참조가 있지만 가비지 수집되는 객체를 보았습니다. 안드로이드 클래스 로더의 부작용 또는 버그 일 수도 있지만 정적 참조는 활동 라이프 사이클에서 상태를 교환하는 안전한 방법이 아닙니다. 그러나 앱 객체는 내가 사용하는 이유입니다.
Matthias

10
@ Matthias : 정적 참조를 사용한다고 말하지 않았습니다. 정적 내부 클래스를 사용한다고 말했습니다. 이름에 "정적"이 있음에도 불구하고 상당한 차이가 있습니다.
CommonsWare


5
알다시피 여기에서 핵심은 정적 내부 클래스가 아닌 getLastNonConfigurationInstance ()입니다. 정적 내부 클래스는 외부 클래스에 대한 암시 적 참조를 유지하지 않으므로 일반 공개 클래스와 의미가 동일합니다. 경고 : onRetainNonConfigurationInstance ()는 활동이 중단 될 때 호출이 보장되지 않으므로 (중단도 전화 통화 일 수 있음) onSaveInstanceState ()에서 작업을 소포해야합니다. 해결책. 그러나 여전히 좋은 생각입니다.
Matthias

7
음 ... onRetainNonConfigurationInstance ()는 활동이 소멸되고 다시 작성되는 중일 때 항상 호출됩니다. 다른 시간에 전화하는 것은 의미가 없습니다. 다른 활동으로 전환하면 현재 활동이 일시 중지 / 중지되지만 소멸되지 않으므로 비동기 작업이 동일한 활동 인스턴스를 계속 실행하여 사용할 수 있습니다. 완료되고 대화 상자가 표시되면 대화 상자가 해당 활동의 일부로 올바르게 표시되므로 활동으로 돌아갈 때까지 사용자에게 표시되지 않습니다. 번들에 AsyncTask를 넣을 수 없습니다.
hackbod

15

것 같습니다 AsyncTask조금있다 만보다 개념적으로 결함 . 호환성 문제로도 사용할 수 없습니다. Android 문서는 다음과 같습니다.

처음 소개되었을 때 AsyncTasks는 단일 백그라운드 스레드에서 순차적으로 실행되었습니다. DONUT부터는 여러 작업을 병렬로 수행 할 수있는 스레드 풀로 변경되었습니다. HONEYCOMB을 시작하면 병렬 실행으로 인한 일반적인 응용 프로그램 오류를 피하기 위해 단일 스레드에서 작업이 다시 실행됩니다. 병렬 실행을 정말로 원한다면 executeOnExecutor(Executor, Params...) 이 메소드 버전을THREAD_POOL_EXECUTOR ; 그러나 사용에 대한 경고는 주석을 참조하십시오.

모두 executeOnExecutor()하고 THREAD_POOL_EXECUTOR있습니다 API 레벨 11에 추가 (안드로이드 3.0.x의, 벌집).

AsyncTask, 두 개의 파일을 다운로드하기 위해 두 개의 파일 을 만들면 첫 번째 파일이 완료 될 때까지 두 번째 다운로드가 시작되지 않습니다. 두 서버를 통해 채팅하고 첫 번째 서버가 다운 된 경우 첫 번째 서버에 대한 연결 시간이 초과되기 전에 두 번째 서버에 연결하지 않습니다. 물론 새로운 API11 기능을 사용하지 않으면 코드가 2.x와 호환되지 않습니다.

그리고 2.x와 3.0+를 모두 목표로 삼으려면 정말 까다로워집니다.

또한 문서 는 다음 과 같이 말합니다.

주의 : 작업자 스레드를 사용할 때 발생할 수있는 또 다른 문제는 런타임 구성 변경 (예 : 사용자가 화면 방향을 변경하는 경우)으로 인해 활동이 예기치 않게 다시 시작된다는 것입니다. 될 수 있습니다 . 이러한 재시작 중 하나를 수행하는 동안 작업을 유지하는 방법과 활동이 소멸 될 때 작업을 올바르게 취소하는 방법을 보려면 선반 샘플 응용 프로그램의 소스 코드를 참조하십시오.


12

아마 우리는 구글을 포함한 모든이, 오용되는 AsyncTask으로부터 MVC 관점 입니다.

활동은 컨트롤러 이므로 컨트롤러가 보기 보다 오래 지속될 수있는 작업을 시작해서는 안됩니다 . 즉, AsyncTasks는 Activity 라이프 사이클에 바인딩되지 않은 클래스의 Model 에서 사용해야합니다 . 활동은 회전시 파괴됩니다. ( View 와 관련 하여 일반적으로 android.widget.Button에서 파생 된 클래스를 프로그래밍하지는 않지만 가능합니다. 일반적으로 View 에 대해 수행하는 유일한 작업 은 xml입니다.)

다시 말해, 활동의 메소드에 AsyncTask 파생어를 배치하는 것은 잘못입니다. OTOH, 활동에서 AsyncTasks를 사용하지 않아야하는 경우 AsyncTask는 매력을 잃습니다. 예전에는 빠르고 쉬운 해결책으로 광고되었습니다.


5

AsyncTask의 컨텍스트에 대한 참조로 메모리 누수 위험이 있다는 것은 확실하지 않습니다.

그것들을 구현하는 일반적인 방법은 Activity의 메소드 중 하나의 범위 내에서 새로운 AsyncTask 인스턴스를 만드는 것입니다. 따라서 활동이 파괴되면 AsyncTask가 완료되면 도달 할 수 없으며 가비지 수집이 가능합니까? 따라서 AsyncTask 자체가 멈추지 않기 때문에 활동에 대한 참조는 중요하지 않습니다.


2
사실-그러나 작업이 무기한 차단되면 어떻게됩니까? 작업은 차단 작업을 수행하기위한 것으로, 종료되지 않는 작업 일 수도 있습니다. 거기에 메모리 누수가 있습니다.
Matthias

1
무한 루프에서 작업을 수행하는 작업자 또는 I / O 작업과 같이 잠기는 작업
Matthias

2

활동에 대한 WeekReference를 유지하는 것이 더 강력합니다.

public class WeakReferenceAsyncTaskTestActivity extends Activity {
    private static final int MAX_COUNT = 100;

    private ProgressBar progressBar;

    private AsyncTaskCounter mWorker;

    @SuppressWarnings("deprecation")
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_async_task_test);

        mWorker = (AsyncTaskCounter) getLastNonConfigurationInstance();
        if (mWorker != null) {
            mWorker.mActivity = new WeakReference<WeakReferenceAsyncTaskTestActivity>(this);
        }

        progressBar = (ProgressBar) findViewById(R.id.progressBar1);
        progressBar.setMax(MAX_COUNT);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.activity_async_task_test, menu);
        return true;
    }

    public void onStartButtonClick(View v) {
        startWork();
    }

    @Override
    public Object onRetainNonConfigurationInstance() {
        return mWorker;
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mWorker != null) {
            mWorker.mActivity = null;
        }
    }

    void startWork() {
        mWorker = new AsyncTaskCounter(this);
        mWorker.execute();
    }

    static class AsyncTaskCounter extends AsyncTask<Void, Integer, Void> {
        WeakReference<WeakReferenceAsyncTaskTestActivity> mActivity;

        AsyncTaskCounter(WeakReferenceAsyncTaskTestActivity activity) {
            mActivity = new WeakReference<WeakReferenceAsyncTaskTestActivity>(activity);
        }

        private static final int SLEEP_TIME = 200;

        @Override
        protected Void doInBackground(Void... params) {
            for (int i = 0; i < MAX_COUNT; i++) {
                try {
                    Thread.sleep(SLEEP_TIME);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Log.d(getClass().getSimpleName(), "Progress value is " + i);
                Log.d(getClass().getSimpleName(), "getActivity is " + mActivity);
                Log.d(getClass().getSimpleName(), "this is " + this);

                publishProgress(i);
            }
            return null;
        }

        @Override
        protected void onProgressUpdate(Integer... values) {
            super.onProgressUpdate(values);
            if (mActivity != null) {
                mActivity.get().progressBar.setProgress(values[0]);
            }
        }
    }

}

이것은 우리가 Droid-Fu로 처음 한 것과 유사합니다. 컨텍스트 객체에 대한 약한 참조의 맵을 유지하고 콜백을 실행할 가장 최근의 참조 (사용 가능한 경우)를 얻기 위해 작업 콜백을 검색합니다. 그러나 우리의 접근 방식은이 매핑을 유지하는 단일 엔티티가 있음을 의미했지만 귀하의 접근 방식은 그렇지 않으므로 실제로 더 좋습니다.
Matthias

1
RoboSpice를 보셨습니까? github.com/octo-online/robospice . 나는이 시스템이 더 낫다고 믿는다.
Snicolas

프론트 페이지의 샘플 코드는 컨텍스트 참조를 유출하는 것처럼 보입니다 (내부 클래스는 외부 클래스에 대한 암시 적 참조를 유지합니다).
Matthias

@ Matthias, 맞습니다. 그래서 활동에 대한 약한 참조를 보유 할 정적 내부 클래스를 제안합니다.
Snicolas

1
@ Matthias, 나는 이것이 주제에서 벗어나기 시작한다고 생각합니다. 그러나 로더는 우리가하는 것보다 더 많은 캐싱을 제공하지 않습니다. 로더는 우리 라이브러리보다 더 장황한 경향이 있습니다. 실제로 이들은 커서를 잘 처리하지만 네트워킹의 경우 캐싱 및 서비스를 기반으로 한 다른 접근 방식이 더 적합합니다. neilgoodman.net/2011/12/26/… 1 부 & 2 부 참조
Snicolas

1

onPause()소유 활동 의 메소드를 대체하고 AsyncTask거기에서 메소드를 취소 하지 않는 이유는 무엇 입니까?


작업이 수행하는 작업에 따라 다릅니다. 데이터를로드 / 읽기하면 괜찮습니다. 그러나 원격 서버의 일부 데이터 상태가 변경되면 작업에 끝까지 실행할 수있는 기능을 제공하는 것이 좋습니다.
Vit Khudenko

@Arhimed와 UI 스레드 onPause를 잡고 있으면 다른 곳에서 잡고있는 것만 큼 나쁘다는 견해를 가져야 합니까? 즉, ANR을 얻을 수 있습니까?
Jeff Axelrod 님이

바로 그거죠. 우리 onPause는 ANR을 얻을 위험이 있기 때문에 UI 스레드를 막을 수 없다
Vit Khudenko

1

당신은 절대적으로 옳습니다-그래서 데이터를 가져 오기 위해 활동에서 비동기 작업 / 로더를 사용하지 않는 운동이 추진력을 얻는 이유입니다. 새로운 방법 중 하나는 데이터가 준비되면 본질적으로 콜백을 제공 하는 Volley 프레임 워크 를 사용하는 것 입니다. MVC 모델과 훨씬 더 일관됩니다. Volley는 Google I / O 2013에서 인구가 증가했습니다. 왜 더 많은 사람들이이를 알지 못하는지 잘 모르겠습니다.


그것에 대해 감사합니다 ... 나는 그것을 조사 할 것입니다 ... AsyncTask를 좋아하지 않는 나의 이유는 그것이 PostPostute에 대한 하나의 지침 세트와 함께 붙어 있기 때문입니다 ... 인터페이스를 사용하거나 매번 재정의하는 것처럼 해킹하지 않는 한 필요합니다.
carinlynchin

0

개인적으로 Thread를 확장하고 콜백 인터페이스를 사용하여 UI를 업데이트합니다. FC 문제없이 AsyncTask를 제대로 작동시킬 수 없었습니다. 또한 비 블로킹 큐를 사용하여 실행 풀을 관리합니다.


1
글쎄, 당신의 힘은 아마도 내가 언급 한 문제 때문일 것입니다 : 당신은 범위를 벗어난 컨텍스트를 참조하려고 시도했습니다 (즉, 창이 파괴되었습니다).
Matthias

아니요. 실제로 대기열이 AsyncTask에 내장되어 있기 때문에 발생했습니다. 항상 getApplicationContext ()를 사용합니다. 몇 가지 작업 만하면 AsyncTask에 문제가 없습니다 ...하지만 백그라운드에서 앨범 아트를 업데이트하는 미디어 플레이어를 작성하고 있습니다 ... 내 테스트에는 아트가없는 120 개의 앨범이 있습니다 ... 내 응용 프로그램이 완전히 닫히지 않고 asynctask에서 오류가 발생했습니다 ... 대신 프로세스를 관리하는 대기열이있는 싱글 톤 클래스를 작성했으며 지금까지는 훌륭하게 작동했습니다.
androidworkz

0

취소가 효과가 있다고 생각했지만 그렇지 않습니다.

여기 그들은 그것에 대해 RTFMing :

""작업이 이미 시작된 경우 mayInterruptIfRunning 매개 변수는 작업을 중지하기 위해이 작업을 실행하는 스레드를 중단해야하는지 여부를 결정합니다. "

그러나 스레드가 인터럽트 가능하다는 것을 의미하지는 않습니다. "AsyncTask가 아닌 Java입니다."

http://groups.google.com/group/android-developers/browse_thread/thread/dcadb1bc7705f1bb/add136eb4949359d?show_docid=add136eb4949359d


0

AsyncTask를 Activity, Context, ContextWrapper 등과 더 밀접하게 결합 된 것으로 생각하는 것이 좋습니다. 범위가 완전히 이해되면 더 편리합니다.

수명주기에 취소 정책이 있어야 결국 가비지 수집되고 더 이상 활동에 대한 참조를 유지하지 않으며 가비지 수집 될 수 있습니다.

컨텍스트를 통과하는 동안 AsyncTask를 취소하지 않으면 메모리 누수와 NullPointerException이 발생합니다. 토스트 간단한 대화 상자와 같은 피드백을 제공 해야하는 경우 애플리케이션 컨텍스트의 단일 톤이 NPE 문제를 피하는 데 도움이됩니다.

AsyncTask는 모두 나쁘지는 않지만 예기치 않은 함정을 일으킬 수있는 많은 마술이 진행되고 있습니다.


-1

"함께 작업 경험"에 관해서는 : 그것이 가능 하도록 프로세스를 종료 모든 AsyncTasks와 함께 사용자가 아무것도 언급하지 않도록, 안드로이드는 활동 스택을 다시 만듭니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.