AsyncTask가 실제로 개념적으로 결함이 있습니까? 아니면 뭔가 빠졌습니까?
나는이 문제를 몇 달 동안 조사했으며 다른 해결책을 생각해 냈습니다. 모두 큰 해킹이기 때문에 만족스럽지 않습니다. 나는 여전히 디자인에 결함이있는 클래스가 프레임 워크에 그것을 만들었고 아무도 그것에 대해 이야기하고 있지 않다고 믿을 수 없으므로 나는 뭔가를 놓치고 있어야한다고 생각합니다.
문제는입니다 AsyncTask
. 문서에 따르면
"스레드 및 / 또는 핸들러를 조작하지 않고도 백그라운드 작업을 수행하고 UI 스레드에 결과를 게시 할 수 있습니다."
그런 다음 예제는에서 예제 showDialog()
메소드가 어떻게 호출 되는지 계속 보여줍니다 onPostExecute()
. 그러나 대화 상자를 표시하려면 항상 유효한 참조가 필요하고 AsyncTask 는 컨텍스트 객체에 대한 강력한 참조를 보유해서는 안되기 때문에 이것은 전적으로 나에게 부여 된 것처럼 보입니다 .Context
그 이유는 명백합니다. 활동이 파괴되어 작업을 시작하면 어떻게됩니까? 예를 들어 화면을 뒤집었기 때문에 항상 이런 일이 발생할 수 있습니다. 태스크를 만든 컨텍스트에 대한 참조를 유지한다면, 당신은 단지 쓸모 컨텍스트 개체 (윈도우가 파괴 된 것이며에 들고하지 않는 모든 UI 상호 작용 예외와 함께 실패합니다!), 당신은도를 만드는 위험 메모리 누수.
내 논리에 결함이 없으면 다음과 같이 해석됩니다. onPostExecute()
컨텍스트에 액세스 할 수없는 경우이 메소드가 UI 스레드에서 실행하는 것이 어떤 이점이 있습니까? 여기서 의미있는 일은 할 수 없습니다.
한 가지 해결 방법은 컨텍스트 인스턴스를 AsyncTask가 아닌 인스턴스로 전달하는 것 Handler
입니다. 작동 : 처리기는 컨텍스트와 작업을 느슨하게 바인딩하므로 누출 위험없이 메시지를 교환 할 수 있습니다 (오른쪽?). 그러나 그것은 AsyncTask의 전제, 즉 처리기를 귀찮게 할 필요가 없다는 것이 잘못되었음을 의미합니다. 동일한 스레드에서 메시지를 보내고 받고 있기 때문에 Handler를 학대하는 것처럼 보입니다 (UI 스레드에서 메시지를 작성하고 UI 스레드에서 실행되는 onPostExecute ()로 메시지를 전송 함).
그 해결 방법을 사용하더라도 상황이 파괴 될 때 발생하는 작업에 대한 기록 이 없다는 문제가 여전히 있습니다 . 즉, 컨텍스트를 다시 만들 때 (예 : 화면 방향 변경 후) 작업을 다시 시작해야합니다. 이것은 느리고 낭비입니다.
이것에 대한 나의 해결책 ( Droid-Fu 라이브러리에서 구현 됨 )은 WeakReference
고유 한 응용 프로그램 객체의 구성 요소 이름에서 현재 인스턴스 로 의 매핑을 유지하는 것입니다 . AsyncTask가 시작될 때마다 해당 맵에 호출 컨텍스트를 기록하고 모든 콜백에서 해당 맵핑에서 현재 컨텍스트 인스턴스를 가져옵니다. 당신은 오래된 컨텍스트 인스턴스를 참조하지 않을 것을이 보장하지만 그리고 당신이 거기에 의미있는 UI 작업을 할 수 있도록 항상 콜백에 유효한 컨텍스트에 액세스 할 수 있습니다. 참조가 약하고 주어진 구성 요소의 인스턴스가 더 이상 존재하지 않으면 지워지기 때문에 누출되지 않습니다.
여전히 복잡한 해결 방법이며 일부 Droid-Fu 라이브러리 클래스를 서브 클래스 화해야하므로이 방법이 상당히 방해가됩니다.
이제 나는 단순히 알고 싶어합니다. 방금 큰 무언가가 누락되었거나 AsyncTask가 실제로 완전히 결함이 있습니까? 당신의 경험은 어떻게 작동합니까? 이 문제를 어떻게 해결 했습니까?
입력 해 주셔서 감사합니다.
이런 식으로 어떻습니까 :
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(...);
}
}
그 이유는 명백합니다. 활동이 파괴되어 작업을 시작하면 어떻게됩니까?
수동에서 활동 해제 AsyncTask
에서을 onDestroy()
. 새 활동을 AsyncTask
in에 수동으로 다시 연결하십시오 onCreate()
. 정적 내부 클래스 또는 표준 Java 클래스와 10 줄의 코드가 필요합니다.
그것은 단지 개념적으로 결함이있는 것보다 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+를 모두 목표로 삼으려면 정말 까다로워집니다.
또한 문서 는 다음 과 같이 말합니다.
주의 : 작업자 스레드를 사용할 때 발생할 수있는 또 다른 문제는 런타임 구성 변경 (예 : 사용자가 화면 방향을 변경하는 경우)으로 인해 활동이 예기치 않게 다시 시작되어 작업자 스레드가 손상 될 수 있습니다 . 이러한 재시작 중 하나를 수행하는 동안 작업을 유지하는 방법과 활동이 소멸 될 때 작업을 올바르게 취소하는 방법을 보려면 선반 샘플 응용 프로그램의 소스 코드를 참조하십시오.
아마도 구글을 포함한 우리 모두 AsyncTask
는 MVC 관점 에서 오용 하고있을 것 입니다.
활동은 컨트롤러 이므로 컨트롤러가 보기 보다 오래 지속될 수있는 작업을 시작해서는 안됩니다 . 즉, AsyncTasks는 Activity 라이프 사이클에 바인딩되지 않은 클래스의 Model 에서 사용해야합니다 . 활동은 회전시 파괴됩니다. ( View 와 관련 하여 일반적으로 android.widget.Button에서 파생 된 클래스를 프로그래밍하지는 않지만 가능합니다. 일반적으로 View 에 대해 수행하는 유일한 작업 은 xml입니다.)
즉, AsyncTask 파생 상품을 활동 메소드에 배치하는 것은 잘못입니다. OTOH, 활동에서 AsyncTasks를 사용하지 않아야하는 경우 AsyncTask는 매력을 잃습니다. 예전에는 빠르고 쉬운 해결책으로 광고되었습니다.
AsyncTask의 컨텍스트를 참조하여 메모리 누수 위험이 있다는 것은 확실하지 않습니다.
그것들을 구현하는 일반적인 방법은 Activity의 메소드 중 하나의 범위 내에서 새로운 AsyncTask 인스턴스를 만드는 것입니다. 따라서 활동이 파괴되면 AsyncTask가 완료되면 도달 할 수 없으며 가비지 수집이 가능합니까? 따라서 AsyncTask 자체가 멈추지 않기 때문에 활동에 대한 참조는 중요하지 않습니다.
활동에 대한 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]);
}
}
}
}
onPause()
소유 활동 의 메소드를 대체하고 AsyncTask
거기에서 취소하는 것이 어떻습니까?
당신이 절대적으로 옳습니다. 그래서 데이터를 가져 오기 위해 활동에서 비동기 작업 / 로더를 사용하지 않는 운동이 추진력을 얻는 이유입니다. 새로운 방법 중 하나는 데이터가 준비되면 본질적으로 콜백을 제공 하는 Volley 프레임 워크 를 사용하는 것 입니다. MVC 모델과 훨씬 더 일관됩니다. Volley는 Google I / O 2013에서 인구가 증가했습니다. 왜 더 많은 사람들이이를 알지 못하는지 잘 모르겠습니다.
개인적으로 Thread를 확장하고 콜백 인터페이스를 사용하여 UI를 업데이트합니다. FC 문제 없이는 AsyncTask를 제대로 작동시킬 수 없었습니다. 또한 비 블로킹 큐를 사용하여 실행 풀을 관리합니다.
취소가 효과가 있다고 생각했지만 그렇지 않습니다.
여기 그들은 그것에 대해 RTFMing :
""작업이 이미 시작된 경우 mayInterruptIfRunning 매개 변수는 작업을 중지하기 위해이 작업을 실행하는 스레드를 중단해야하는지 여부를 결정합니다. "
그러나 스레드가 인터럽트 가능하다는 것을 의미하지는 않습니다. "AsyncTask가 아닌 Java입니다."
AsyncTask는 Activity, Context, ContextWrapper 등과 더 밀접하게 결합 된 것으로 생각하는 것이 좋습니다. 범위가 완전히 이해되면 더 편리합니다.
수명주기에 취소 정책이 있어야 결국 가비지 수집되고 더 이상 활동에 대한 참조를 유지하지 않으며 가비지 수집 될 수 있습니다.
컨텍스트에서 순회하는 동안 AsyncTask를 취소하지 않으면 메모리 누수와 NullPointerExceptions가 발생합니다. 토스트 간단한 대화 상자와 같은 피드백을 제공 해야하는 경우 애플리케이션 컨텍스트의 단일 톤으로 NPE 문제를 피할 수 있습니다.
AsyncTask가 모두 나쁘지는 않지만 예기치 않은 함정을 일으킬 수있는 많은 마술이 진행되고 있습니다.
"함께 작업 경험"에 관해서는 : 그것이 가능 하도록 프로세스를 종료 모든 AsyncTasks와 함께 사용자가 아무것도 언급하지 않도록, 안드로이드는 활동 스택을 다시 만듭니다.
'Programing' 카테고리의 다른 글
루프가 반대로 더 빠릅니까? (0) | 2020.04.01 |
---|---|
Android 및 iOS 용 2D 플랫폼 간 게임 엔진? (0) | 2020.04.01 |
ViewPager 내부의 조각 교체 (0) | 2020.04.01 |
Spring의 ApplicationContext.getBean이 왜 나쁜 것으로 간주됩니까? (0) | 2020.04.01 |
maven-shade-plugin은 무엇이며 Java 패키지를 재배치하려는 이유는 무엇입니까? (0) | 2020.04.01 |