AsyncTask sẽ không dừng ngay cả khi hoạt động đã bị phá hủy


76

Tôi có một đối tượng AsyncTask bắt đầu thực thi khi hoạt động được tạo và thực hiện nội dung trong nền (tải xuống tối đa 100 hình ảnh). Mọi thứ hoạt động tốt nhưng có một hành vi đặc biệt này mà tôi không thể hiểu được.

Ví dụ: khi hướng của màn hình android thay đổi thì hoạt động sẽ bị hủy và tạo lại. Vì vậy, tôi ghi đè phương thức onRetainNonConfigurationInstance () và lưu tất cả dữ liệu đã tải xuống được thực thi trong AsyncTask. Mục đích của tôi khi làm việc này là không để AsyncTask chạy mỗi khi hoạt động bị phá hủy được tạo trong quá trình thay đổi hướng, nhưng như tôi có thể thấy trong nhật ký của mình, AsyncTask trước đó vẫn đang thực thi. (Mặc dù vậy, dữ liệu được lưu đúng cách)

Tôi thậm chí đã cố gắng hủy AsyncTask trong phương thức onDestroy () của hoạt động nhưng các bản ghi vẫn hiển thị AsyncTask đang chạy.

Đây thực sự là một hành vi kỳ lạ và thực sự sẽ rất biết ơn nếu ai đó có thể cho tôi biết quy trình chính xác để dừng / hủy AsyncTask.

Cảm ơn

Câu trả lời:


144

Câu trả lời được đưa ra bởi @Romain Guy là chính xác. Tuy nhiên, tôi muốn thêm phần bổ sung thông tin và cung cấp một con trỏ cho thư viện hoặc 2 có thể được sử dụng cho AsyncTask chạy lâu dài và thậm chí nhiều hơn nữa cho các asynctasks hướng mạng.

AsyncTasks đã được thiết kế để thực hiện mọi thứ trong nền. Và có, bạn có thể dừng nó bằng cách sử dụng cancelphương pháp này. Khi bạn tải xuống nội dung từ Internet, tôi thực sự khuyên bạn nên quan tâm đến chuỗi của mình khi nó ở trạng thái chặn IO . Bạn nên tổ chức tải xuống của mình như sau:

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
}

Sử dụng Thread.interruptedcờ sẽ giúp chuỗi của bạn thoát khỏi trạng thái io chặn đúng cách. Luồng của bạn sẽ phản hồi nhanh hơn với một lệnh gọi cancelphương thức.

Lỗ hổng thiết kế AsyncTask

Nhưng nếu AsyncTask của bạn tồn tại quá lâu, thì bạn sẽ gặp phải 2 vấn đề khác nhau:

  1. Các hoạt động có mối liên hệ chặt chẽ với vòng đời hoạt động và bạn sẽ không nhận được kết quả của AsyncTask nếu hoạt động của bạn không hoạt động. Thật vậy, có, bạn có thể nhưng đó sẽ là con đường khó khăn.
  2. AsyncTask không được ghi chép đầy đủ. Việc triển khai và sử dụng một asynctask ngây thơ, mặc dù trực quan, có thể nhanh chóng dẫn đến rò rỉ bộ nhớ.

RoboSpice , thư viện mà tôi muốn giới thiệu, sử dụng một dịch vụ nền để thực hiện loại yêu cầu này. Nó đã được thiết kế cho các yêu cầu mạng. Nó cung cấp các tính năng bổ sung như lưu trữ tự động các kết quả của yêu cầu.

Đây là lý do tại sao AsyncTasks không tốt cho các tác vụ chạy lâu. Lý do sau đây là sự chuyển thể từ các đoạn trích của động cơ RoboSpice : ứng dụng giải thích tại sao việc sử dụng RoboSpice đang đáp ứng nhu cầu trên nền tảng Android.

Vòng đời của AsyncTask và Activity

AsyncTasks không tuân theo vòng đời của phiên bản Activity. Nếu bạn khởi động AsyncTask bên trong Hoạt động và bạn xoay thiết bị, Hoạt động sẽ bị hủy và một phiên bản mới sẽ được tạo. Nhưng AsyncTask sẽ không chết. Nó sẽ tiếp tục sống cho đến khi hoàn thành.

Và khi hoàn tất, AsyncTask sẽ không cập nhật giao diện người dùng của Hoạt động mới. Thật vậy, nó cập nhật phiên bản cũ của hoạt động không được hiển thị nữa. Điều này có thể dẫn đến Ngoại lệ của kiểu java.lang.IllegalArgumentException: Chế độ xem không được đính kèm với trình quản lý cửa sổ nếu bạn sử dụng, chẳng hạn, findViewById để truy xuất chế độ xem bên trong Hoạt động.

Sự cố rò rỉ bộ nhớ

Rất thuận tiện để tạo AsyncTasks làm lớp bên trong Hoạt động của bạn. Vì AsyncTask sẽ cần thao tác các khung nhìn của Activity khi nhiệm vụ đã hoàn thành hoặc đang diễn ra, việc sử dụng lớp bên trong của Activity có vẻ thuận tiện: các lớp bên trong có thể truy cập trực tiếp vào bất kỳ trường nào của lớp bên ngoài.

Tuy nhiên, nó có nghĩa là lớp bên trong sẽ giữ một tham chiếu vô hình trên cá thể lớp bên ngoài của nó: Activity.

Về lâu dài, điều này dẫn đến rò rỉ bộ nhớ: nếu AsyncTask tồn tại lâu, nó sẽ giữ cho hoạt động "tồn tại" trong khi Android muốn loại bỏ nó vì nó không thể hiển thị được nữa. Hoạt động không thể được thu thập rác và đó là cơ chế trung tâm để Android duy trì tài nguyên trên thiết bị.

Tiến độ nhiệm vụ của bạn sẽ bị mất

Bạn có thể sử dụng một số giải pháp thay thế để tạo một asynctask hoạt động lâu dài và quản lý vòng đời của nó tương ứng với vòng đời của hoạt động. Bạn có thể hủy AsyncTask trong phương pháp onStop của hoạt động của mình hoặc bạn có thể để tác vụ không đồng bộ của mình kết thúc và không làm mất tiến trình của nó và liên kết lại nó với phiên bản hoạt động tiếp theo của bạn .

Điều này là có thể và chúng tôi chỉ ra cách trong RobopSpice động lực, nhưng nó trở nên phức tạp và mã không thực sự chung chung. Hơn nữa, bạn vẫn sẽ mất tiến độ thực hiện nhiệm vụ của mình nếu người dùng rời khỏi hoạt động và quay lại. Vấn đề tương tự này cũng xuất hiện với Loaders, mặc dù nó sẽ đơn giản hơn tương đương với AsyncTask với cách giải quyết liên tục được đề cập ở trên.

Sử dụng dịch vụ Android

Tùy chọn tốt nhất là sử dụng một dịch vụ để thực hiện các tác vụ chạy nền lâu dài của bạn. Và đó chính xác là giải pháp do RoboSpice đề xuất. Một lần nữa, nó được thiết kế cho mạng nhưng có thể được mở rộng cho những thứ không liên quan đến mạng. Thư viện này có một số lượng lớn các tính năng .

Bạn thậm chí có thể có được ý tưởng về nó trong vòng chưa đầy 30 giây nhờ vào đồ họa thông tin .


Thực sự là một ý tưởng rất tồi khi sử dụng AsyncTasks cho các hoạt động chạy dài. Tuy nhiên, chúng phù hợp với những hoạt động ngắn hạn như cập nhật Chế độ xem sau 1 hoặc 2 giây.

Tôi khuyến khích bạn tải xuống ứng dụng RoboSpice Motivations , ứng dụng này thực sự giải thích chuyên sâu về vấn đề này và cung cấp các mẫu và minh họa về các cách khác nhau để thực hiện một số nội dung liên quan đến mạng.


Nếu bạn đang tìm kiếm một giải pháp thay thế cho RoboSpice cho các tác vụ không liên quan đến mạng (ví dụ như không có bộ nhớ đệm), bạn cũng có thể xem Tape .


1
@Snicolas, bạn có thể thêm droidQuery làm giải pháp thay thế cho RoboSpice trên trang Github không? Tôi đã phát triển nó vào năm ngoái và cung cấp, trong số các tính năng khác, khả năng thực hiện các tác vụ mạng bằng jQuery -style Ajax (được viết bằng Android Java thuần túy).
Phil

@Phil, xong rồi. Thx cho liên kết. BTW, cú pháp của bạn thực sự gần giống với Ion (sau đó đã xuất hiện).
Snicolas

@Snicolas, cảm ơn! Có vẻ như cả droidQueryIon đều lấy một số thiết kế cú pháp từ Picasso . github.com/koush/ion/search?q=picasso&ref=cmdform ;)
Phil

"Thực sự là một ý tưởng rất tồi khi sử dụng AsyncTasks cho các hoạt động chạy lâu dài. Tuy nhiên, chúng vẫn tốt cho những hoạt động ngắn hạn như cập nhật Chế độ xem sau 1 hoặc 2 giây." . Tôi không nghĩ nó nhất thiết phải đúng. Lý do để Android đề xuất điều này là thực tế là AsyncTask sử dụng trình thực thi nhóm luồng toàn cầu. Bạn có thể chỉ cần chuyển trình thực thi nhóm luồng tùy chỉnh của mình thông qua phương thức thực thi ans để loại bỏ các vấn đề chặn có thể xảy ra. Mặc dù AsyncTask không phải là một viên đạn bạc như bạn đã đề cập "rò rỉ".
stdout,

Vấn đề không phải là hồ bơi, nó thực sự là rò rỉ.
Snicolas

16

Romain Guy đã đúng. Trên thực tế, một tác vụ không đồng bộ có trách nhiệm hoàn thành công việc của chính nó trong mọi trường hợp. Gián đoạn không phải là cách tốt nhất, vì vậy bạn nên liên tục kiểm tra xem ai đó muốn bạn hủy hoặc dừng nhiệm vụ của bạn.

Giả sử bạn AsyncTasklàm điều gì đó trong một vòng lặp nhiều lần. Sau đó, bạn nên kiểm tra isCancelled()trong mọi vòng lặp.

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

doTheTask() là công việc thực sự của bạn và trước khi thực hiện trong mỗi vòng lặp, bạn hãy kiểm tra xem có nên hủy bỏ nhiệm vụ của mình hay không.

Nói chung, bạn nên đặt một lá cờ trong AsyncTasklớp của mình hoặc trả về một kết quả thích hợp từ của bạn doInBackground()để onPostExecute()bạn có thể kiểm tra xem mình có thể hoàn thành những gì bạn muốn hay không hoặc công việc của bạn bị hủy giữa chừng.


1

Những điều sau đây không giải quyết được vấn đề của bạn, nhưng ngăn chặn nó: Trong tệp kê khai ứng dụng, hãy làm như sau:

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

Khi bạn thêm điều này, hoạt động của bạn không tải lại khi thay đổi cấu hình và nếu bạn muốn thực hiện một số thay đổi khi hướng thay đổi, bạn chỉ cần ghi đè phương pháp sau của hoạt động:

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

        //your code here
    }

o, ... phương thức này sẽ được gọi khi hướng của thiết bị thay đổi? @FranePoljak ... ic, ic, ...
gumuruh

1

hoạt động được tạo lại trên sự thay đổi định hướng, đúng vậy. nhưng bạn có thể tiếp tục asynctask bất cứ khi nào sự kiện này xảy ra.

bạn kiểm tra nó trên

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

-cheers


-1

Theo quan điểm của MVC , Activity là Controller ; việc Controller thực hiện các hoạt động tồn tại lâu hơn View (bắt nguồn từ android.view.View, thường là bạn chỉ sử dụng lại các lớp hiện có) là sai. Do đó, Model phải có trách nhiệm khởi động AsyncTasks.


-4

Bạn có thể dùng class MagicAppRestart từ bài đăng này để kết thúc quá trình cùng với tất cả các AsyncTasks; Android sẽ khôi phục ngăn xếp hoạt động (người dùng sẽ không đề cập đến bất cứ điều gì). Điều quan trọng cần lưu ý là thông báo duy nhất trước khi khởi động lại quy trình đang gọi onPause(); theo logic vòng đời ứng dụng Android , dù sao thì ứng dụng của bạn cũng phải sẵn sàng để chấm dứt.

Tôi đã thử nó, và nó có vẻ hoạt động. Tuy nhiên, hiện tại tôi dự định sử dụng các phương thức "văn minh hơn" như các tham chiếu yếu từ lớp Ứng dụng (các AsyncTasks của tôi khá ngắn và hy vọng không tốn nhiều bộ nhớ).

Đây là một số mã bạn có thể chơi với:

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));
    }
}

Phần còn lại là những gì Eclipse đã tạo cho một dự án Android mới cho 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>

và một phần có liên quan của nhật ký (lưu ý rằng chỉ onPauseđược gọi ):

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

Đã trả lời vào năm 2013 (thời của Android 2.x), bị phản đối vào năm 2017 (thời của Android 6 và 7). Rất nhiều phải đã thay đổi kể từ đó, tôi biết rằng các ứng dụng khởi động lại xe cho thuê đã không làm việc kể từ khi Android 4.
18446744073709551615
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.