AsyncTask не зупиняється, навіть коли діяльність знищена


76

У мене є об’єкт AsyncTask, який починає виконуватися, коли діяльність створюється, і робить речі у фоновому режимі (завантажує до 100 зображень). Все працює нормально, але є така особлива поведінка, яку я не можу зрозуміти.

Наприклад, коли орієнтація екрану Android змінюється, тоді діяльність знищується і створюється заново. Тому я перевизначаю метод onRetainNonConfigurationInstance () і зберігаю всі завантажені дані, що виконуються в AsyncTask. Моя мета робити це - не запускати AsyncTask кожного разу, коли діяльність знищується-створюється під час змін орієнтації, але, як я бачу в своїх журналах, попередня AsyncTask все ще виконується. (Хоча дані зберігаються правильно)

Я навіть намагався скасувати AsyncTask в методі onDestroy () діяльності, але журнали все ще показують AsyncTask як запущений.

Це дійсно дивна поведінка, і я був би дуже вдячний, якщо хтось може сказати мені правильну процедуру зупинки / скасування AsyncTask.

Дякую

Відповіді:


144

Відповідь, надана @Romain Guy, є правильною. Тим не менше, я хотів би додати доповнення до інформації та вказати вказівник на бібліотеку або 2, які можна використовувати для тривалої роботи AsyncTask і навіть більше для мережево орієнтованих асинктаз.

AsyncTasks розроблені для роботи у фоновому режимі. І так, ви можете зупинити це, використовуючи cancelметод. Завантажуючи матеріали з Інтернету, настійно рекомендую подбати про свою нитку, коли вона перебуває у стані блокування вводу- виводу . Ви повинні організувати завантаження таким чином:

public void download() {
    //get the InputStream from HttpUrlConnection or any other
    //network related stuff
    while( inputStream.read(buffer) != -1 && !Thread.interrupted() ) {
      //copy data to your destination, a file for instance
    }
    //close the stream and other resources
}

Використання Thread.interruptedпрапора допоможе вашому потоку правильно вийти із блокуючого стану io. Ваш потік буде більш реагувати на виклик cancelметоду.

Недолік дизайну AsyncTask

Але якщо ваш AsyncTask триває занадто довго, тоді ви зіткнетеся з двома різними проблемами:

  1. Діяльність погано прив’язана до життєвого циклу діяльності, і ви не отримаєте результату своєї AsyncTask, якщо ваша діяльність помре. Справді, так, ви можете, але це буде грубий шлях.
  2. AsyncTask не дуже добре задокументовані. Наївне, хоч і інтуїтивне, впровадження та використання асинктаску може швидко призвести до витоків пам'яті.

RoboSpice , бібліотека, яку я хотів би представити, використовує фонову службу для виконання такого роду запитів. Він розроблений для мережевих запитів. Він надає додаткові функції, такі як автоматичне кешування результатів запитів.

Ось причина, чому AsyncTasks погані для тривалих завдань. Наступне міркування - це адаптація до мотивацій RoboSpice : додаток, який пояснює, чому використання RoboSpice задовольняє потреби в платформі Android.

Життєвий цикл AsyncTask та Activity

AsyncTasks не відповідають життєвому циклу екземплярів Activity. Якщо ви запустите AsyncTask всередині Activity і обернете пристрій, Activity буде знищено та створено новий екземпляр. Але AsyncTask не помре. Він буде продовжувати жити, поки не завершиться.

І коли він завершиться, AsyncTask не буде оновлювати інтерфейс нової активності. Дійсно, він оновлює попередній екземпляр діяльності, який більше не відображається. Це може призвести до винятку типу java.lang.IllegalArgumentException: подання, не приєднане до диспетчера вікон, якщо ви використовуєте, наприклад, findViewById для отримання подання всередині Activity.

Проблема витоку пам'яті

Дуже зручно створювати AsyncTasks як внутрішні класи вашої діяльності. Оскільки AsyncTask потрібно буде маніпулювати поданнями Activity, коли завдання завершено або виконується, використання внутрішнього класу Activity здається зручним: внутрішні класи можуть отримувати безпосередній доступ до будь-якого поля зовнішнього класу.

Тим не менше, це означає, що внутрішній клас буде мати невидиму посилання на свій екземпляр зовнішнього класу: Activity.

У довгостроковій перспективі це спричиняє витік пам’яті: якщо AsyncTask триває довго, він зберігає активність «живою», тоді як Android хотів би позбутися від неї, оскільки вона більше не може відображатися. Діяльність не може бути зібрана сміття, і це центральний механізм для Android для збереження ресурсів на пристрої.

Прогрес вашого завдання буде втрачено

Ви можете використовувати деякі обхідні шляхи, щоб створити тривалий асинктаск та керувати його життєвим циклом відповідно до життєвого циклу діяльності. Ви можете або скасувати AsyncTask у методі onStop своєї діяльності, або дозволити асинхронному завданню закінчитись, а не втратити його прогрес і зв’язати його з наступним екземпляром вашої діяльності .

Це можливо, і ми показуємо, як у мотиваціях RobopSpice, але це ускладнюється, а код насправді не є загальним. Більше того, ви все одно втратите хід свого завдання, якщо користувач залишить діяльність і повернеться. Ця сама проблема виникає з Loaders, хоча це було б простішим еквівалентом AsyncTask із зазначеним вище обхідним способом обходу.

Використання служби Android

Найкращий варіант - скористатися послугою для виконання ваших тривалих фонових завдань. І це саме рішення, запропоноване RoboSpice. Знову ж таки, він призначений для роботи в мережі, але його можна поширити і на не пов’язані з мережею матеріали. Ця бібліотека має велику кількість функцій .

Ви навіть можете зрозуміти це менш ніж за 30 секунд завдяки інфографіці .


Насправді дуже погана ідея використовувати AsyncTasks для тривалих операцій. Тим не менше, вони чудово підходять для короткоживучих, таких як оновлення подання через 1 або 2 секунди.

Я закликаю вас завантажити додаток RoboSpice Motivations , він справді пояснює це поглиблено, а також надає зразки та демонстрації різних способів робити деякі речі, пов’язані з мережею.


Якщо ви шукаєте альтернативу RoboSpice для не пов'язаних з мережею завдань (наприклад, без кешування), ви також можете поглянути на Tape .


1
@Snicolas, ти можеш додати droidQuery як альтернативу RoboSpice на сторінці Github ? Я розробив його в минулому році і, серед інших можливостей, надає можливість виконувати мережеві завдання за допомогою jQuery- style Ajax (написаного на чистій Android Java).
Філ

@Phil, готово. Thx для посилання. До речі, ваш синтаксис дуже близький до Ion (що з’явилося згодом).
Сніколас

@Snicolas, дякую! Схоже, і droidQuery, і Ion взяли певний синтаксичний дизайн від Пікассо . github.com/koush/ion/search?q=picasso&ref=cmdform ;)
Філ

"Насправді дуже погана ідея використовувати AsyncTasks для тривалих операцій. Тим не менше, вони добре підходять для короткочасних, таких як оновлення подання через 1 або 2 секунди." . Я не думаю, що це обов’язково правда. Причиною того, що Android припускає це, є той факт, що AsyncTask використовує глобальний виконавець пулу потоків. Ви можете просто передати свій власний виконавець пулу потоків через метод executes і позбутися можливих проблем із блокуванням. Хоча AsyncTask не є срібною кулею, як ви вже згадували про "витік".
stdout

Проблема не в басейні, це насправді витік.
Snicolas

16

Ромен Гай має рацію. Насправді асинхронне завдання відповідає за закінчення власної роботи в будь-якому випадку. Переривання - не найкращий спосіб, тому вам слід постійно перевіряти, чи не хоче хтось скасувати або зупинити своє завдання.

Скажімо, ваш AsyncTaskбагато разів робить щось у циклі. Тоді вам слід перевірити isCancelled()кожен цикл.

while ( true ) {
    if ( isCancelled())
        break;
    doTheTask();
}

doTheTask() це ваша справжня робота, і перед тим, як робити це в кожному циклі, ви перевіряєте, чи слід скасувати ваше завдання.

Як правило, ви повинні встановити прапор у своєму AsyncTaskкласі або повернути відповідний результат зі свого, doInBackground()щоб у вашому onPostExecute()ви могли перевірити, чи зможете ви закінчити те, що хочете, чи вашу роботу було скасовано посередині.


1

Наступне не вирішує вашу проблему, але запобігає: У маніфесті програми зробіть це:

    <activity
        android:name=".(your activity name)"
        android:label="@string/app_name"
        android:configChanges="orientation|keyboardHidden|screenSize" > //this line here
    </activity>

Коли ви додаєте це, ваша активність не перезавантажується при зміні конфігурації, і якщо ви хочете внести деякі зміни при зміні орієнтації, ви просто перевизначаєте наступний спосіб діяльності:

@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);

        //your code here
    }

o, ... цей метод буде викликаний, як тільки орієнтація пристрою зміниться? @FranePoljak ... ic, ic, ...
gumuruh

1

діяльність відтворюється при зміні орієнтації, так, це правда. але ви можете продовжувати свою асинктацію щоразу, коли трапляється ця подія.

ви перевіряєте це

@Override
protected void onCreate(Bundle savedInstanceState) { 
     if ( savedInstanceState == null ) {
           startAsyncTask()
     } else {
           // ** Do Nothing async task will just continue.
     }
}

-очікує


-1

З точки зору MVC , Activity - це контролер ; неправильно, щоб Контролер виконував операції, які переживають View (похідне від android.view.View, зазвичай Ви просто повторно використовуєте існуючі класи). Отже, відповідальність Моделі повинна запустити AsyncTasks.


-4

Ви можете використовувати class MagicAppRestartз цим постом , щоб вбити процес разом з усім AsyncTasks; Android відновить стек активності (користувач нічого не згадає). Важливо зазначити, що єдиним повідомленням перед перезапуском процесу є дзвінок onPause(); відповідно до логіки життєвого циклу додатка Android , ваш додаток у будь-якому випадку повинен бути готовим до такого припинення.

Я спробував, і, здається, це працює. Тим не менше, на даний момент я планую використовувати "більш цивілізовані" методи, такі як слабкі посилання з класу Application (мої AsyncTasks досить короткочасні і, сподіваюся, не так багато вимагають пам'яті).

Ось код, з яким можна грати:

MagicAppRestart.java

package com.xyz;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;

/** This activity shows nothing; instead, it restarts the android process */
public class MagicAppRestart extends Activity {
    // Do not forget to add it to AndroidManifest.xml
    // <activity android:name="your.package.name.MagicAppRestart"/>
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        System.exit(0);
    }
    public static void doRestart(Activity anyActivity) {
        anyActivity.startActivity(new Intent(anyActivity.getApplicationContext(), MagicAppRestart.class));
    }
}

Решта - те, що Eclipse створив для нового проекту Android для com.xyz.AsyncTaskTestActivity :

AsyncTaskTestActivity.java

package com.xyz;

import android.app.Activity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.View;

public class AsyncTaskTestActivity extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        Log.d("~~~~","~~~onCreate ~~~ "+this);
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }
    public void onStartButton(View view) {
        Log.d("~~~~","~~~onStartButton {");
        class MyTask extends AsyncTask<Void, Void, Void> {

            @Override
            protected Void doInBackground(Void... params) {
                // TODO Auto-generated method stub
                Log.d("~~~~","~~~doInBackground started");
                try {
                    for (int i=0; i<10; i++) {
                        Log.d("~~~~","~~~sleep#"+i);
                        Thread.sleep(200);
                    }
                    Log.d("~~~~","~~~sleeping over");
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                Log.d("~~~~","~~~doInBackground ended");
                return null;
            }
            @Override
            protected void onPostExecute(Void result) {
                super.onPostExecute(result);
                taskDone();
            }
        }
        MyTask task = new MyTask();
        task.execute(null);
        Log.d("~~~~","~~~onStartButton }");
    }
    private void taskDone() {
        Log.d("~~~~","\n\n~~~taskDone ~~~ "+this+"\n\n");
    }
    public void onStopButton(View view) {
        Log.d("~~~~","~~~onStopButton {");
        MagicAppRestart.doRestart(this);
        Log.d("~~~~","~~~onStopButton }");
    }
    public void onPause() {   Log.d("~~~~","~~~onPause ~~~ "+this);   super.onPause(); }
    public void onStop() {    Log.d("~~~~","~~~onStop ~~~ "+this);    super.onPause(); }
    public void onDestroy() { Log.d("~~~~","~~~onDestroy ~~~ "+this); super.onDestroy(); }
}

main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <Button android:text="Start" android:onClick="onStartButton" android:layout_width="fill_parent" android:layout_height="wrap_content"/>
    <Button android:text="Stop" android:onClick="onStopButton" android:layout_width="fill_parent" android:layout_height="wrap_content"/>
    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="@string/hello" />

</LinearLayout>

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.xyz"
    android:versionCode="1"
    android:versionName="1.0" >
    <uses-sdk android:minSdkVersion="7" />
    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:name=".AsyncTaskTestActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name=".MagicAppRestart"/>
    </application>
</manifest>

та відповідна частина журналів (зверніть увагу, що називається тількиonPause ):

D/~~~~    (13667): ~~~onStartButton {
D/~~~~    (13667): ~~~onStartButton }
D/~~~~    (13667): ~~~doInBackground started
D/~~~~    (13667): ~~~sleep#0
D/~~~~    (13667): ~~~sleep#1
D/~~~~    (13667): ~~~sleep#2
D/~~~~    (13667): ~~~sleep#3
D/~~~~    (13667): ~~~sleep#4
D/~~~~    (13667): ~~~sleep#5
D/~~~~    (13667): ~~~sleep#6
D/~~~~    (13667): ~~~sleep#7
D/~~~~    (13667): ~~~sleep#8
D/~~~~    (13667): ~~~sleep#9
D/~~~~    (13667): ~~~sleeping over
D/~~~~    (13667): ~~~doInBackground ended
D/~~~~    (13667): 
D/~~~~    (13667): 
D/~~~~    (13667): ~~~taskDone ~~~ com.xyz.AsyncTaskTestActivity@40516988
D/~~~~    (13667): 




D/~~~~    (13667): ~~~onStartButton {
D/~~~~    (13667): ~~~onStartButton }
D/~~~~    (13667): ~~~doInBackground started
D/~~~~    (13667): ~~~sleep#0
D/~~~~    (13667): ~~~sleep#1
D/~~~~    (13667): ~~~sleep#2
D/~~~~    (13667): ~~~sleep#3
D/~~~~    (13667): ~~~sleep#4
D/~~~~    (13667): ~~~sleep#5
D/~~~~    (13667): ~~~onStopButton {
I/ActivityManager(   81): Starting: Intent { cmp=com.xyz/.MagicAppRestart } from pid 13667
D/~~~~    (13667): ~~~onStopButton }
D/~~~~    (13667): ~~~onPause ~~~ com.xyz.AsyncTaskTestActivity@40516988
I/ActivityManager(   81): Process com.xyz (pid 13667) has died.
I/WindowManager(   81): WIN DEATH: Window{4073ceb8 com.xyz/com.xyz.AsyncTaskTestActivity paused=false}
I/ActivityManager(   81): Start proc com.xyz for activity com.xyz/.AsyncTaskTestActivity: pid=13698 uid=10101 gids={}
I/ActivityManager(   81): Displayed com.xyz/.AsyncTaskTestActivity: +44ms (total +65ms)
D/~~~~    (13698): ~~~onCreate ~~~ com.xyz.AsyncTaskTestActivity@40517238

Відповідав у 2013 році (часи Android 2.x), проти - у 2017 році (часи Android 6 та 7). З тих пір мало що змінилося, я знаю, що хак перезапуску програми не працював з Android 4.
18446744073709551615
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.