Android 조각. 화면 회전 또는 구성 변경 중 AsyncTask 유지


86

저는 스마트 폰 / 태블릿 앱에서 작업 중이며 하나의 APK 만 사용하고 화면 크기에 따라 필요한 리소스를로드하는 중입니다. 최상의 디자인 선택은 ACL을 통해 프래그먼트를 사용하는 것 같습니다.

이 앱은 지금까지 활동 기반으로 만 잘 작동했습니다. 이것은 화면이 회전하거나 통신 중에 구성 변경이 발생하는 경우에도 작동하도록 활동에서 AsyncTasks 및 ProgressDialogs를 처리하는 방법에 대한 모의 클래스입니다.

나는 활동의 재현을 피하기 위해 매니페스트를 변경하지 않을 것입니다. 내가 원하지 않는 이유는 여러 가지가 있지만 주로 공식 문서가 권장하지 않는다고 말하고 지금까지 그것을 관리하지 않았기 때문에 권장하지 마십시오 노선.

public class Login extends Activity {

    static ProgressDialog pd;
    AsyncTask<String, Void, Boolean> asyncLoginThread;

    @Override
    public void onCreate(Bundle icicle) {
        super.onCreate(icicle);
        setContentView(R.layout.login);
        //SETUP UI OBJECTS
        restoreAsyncTask();
    }

    @Override
    public Object onRetainNonConfigurationInstance() {
        if (pd != null) pd.dismiss();
        if (asyncLoginThread != null) return (asyncLoginThread);
        return super.onRetainNonConfigurationInstance();
    }

    private void restoreAsyncTask();() {
        pd = new ProgressDialog(Login.this);
        if (getLastNonConfigurationInstance() != null) {
            asyncLoginThread = (AsyncTask<String, Void, Boolean>) getLastNonConfigurationInstance();
            if (asyncLoginThread != null) {
                if (!(asyncLoginThread.getStatus()
                        .equals(AsyncTask.Status.FINISHED))) {
                    showProgressDialog();
                }
            }
        }
    }

    public class LoginThread extends AsyncTask<String, Void, Boolean> {
        @Override
        protected Boolean doInBackground(String... args) {
            try {
                //Connect to WS, recieve a JSON/XML Response
                //Place it somewhere I can use it.
            } catch (Exception e) {
                return true;
            }
            return true;
        }

        protected void onPostExecute(Boolean result) {
            if (result) {
                pd.dismiss();
                //Handle the response. Either deny entry or launch new Login Succesful Activity
            }
        }
    }
}

이 코드는 잘 작동하고 있으며 불만없이 약 10.000 명의 사용자가 있으므로이 로직을 새로운 Fragment Based Design에 복사하는 것이 논리적으로 보였지만 물론 작동하지 않습니다.

LoginFragment는 다음과 같습니다.

public class LoginFragment extends Fragment {

    FragmentActivity parentActivity;
    static ProgressDialog pd;
    AsyncTask<String, Void, Boolean> asyncLoginThread;

    public interface OnLoginSuccessfulListener {
        public void onLoginSuccessful(GlobalContainer globalContainer);
    }

    public void onSaveInstanceState(Bundle outState){
        super.onSaveInstanceState(outState);
        //Save some stuff for the UI State
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //setRetainInstance(true);
        //If I setRetainInstance(true), savedInstanceState is always null. Besides that, when loading UI State, a NPE is thrown when looking for UI Objects.
        parentActivity = getActivity();
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        try {
            loginSuccessfulListener = (OnLoginSuccessfulListener) activity;
        } catch (ClassCastException e) {
            throw new ClassCastException(activity.toString() + " must implement OnLoginSuccessfulListener");
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        RelativeLayout loginLayout = (RelativeLayout) inflater.inflate(R.layout.login, container, false);
        return loginLayout;
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        //SETUP UI OBJECTS
        if(savedInstanceState != null){
            //Reload UI state. Im doing this properly, keeping the content of the UI objects, not the object it self to avoid memory leaks.
        }
    }

    public class LoginThread extends AsyncTask<String, Void, Boolean> {
            @Override
            protected Boolean doInBackground(String... args) {
                try {
                    //Connect to WS, recieve a JSON/XML Response
                    //Place it somewhere I can use it.
                } catch (Exception e) {
                    return true;
                }
                return true;
            }

            protected void onPostExecute(Boolean result) {
                if (result) {
                    pd.dismiss();
                    //Handle the response. Either deny entry or launch new Login Succesful Activity
                }
            }
        }
    }
}

onRetainNonConfigurationInstance()Fragment가 아닌 Activity에서 호출해야하므로 사용할 수 없습니다 getLastNonConfigurationInstance(). 나는 여기에서 답이없는 유사한 질문을 읽었습니다.

나는이 물건을 조각으로 올바르게 구성하기 위해 약간의 작업이 필요할 수 있음을 이해합니다. 즉, 동일한 기본 디자인 논리를 유지하고 싶습니다.

구성 변경 중에 AsyncTask를 유지하는 적절한 방법은 무엇이며, 여전히 실행중인 경우 AsyncTask가 Fragment의 내부 클래스이고 AsyncTask.execute를 호출하는 것은 Fragment 자체임을 고려하여 progressDialog를 표시합니다. ()?


1
AsyncTask로 구성 변경을 처리하는 방법에 대한 스레드 가 도움이 될 수 있습니다.
rds

응용 프로그램 생명 cycle..thus와 연관 AsyncTask를 때 활동를 재생성을 재개 할 수 있습니다
프레드 Grott에게

이 주제에 대한 내 게시물을 체크 아웃 : 로 구성 변경 처리 Fragment
알렉스 록우드에게

답변:


75

조각은 실제로 이것을 훨씬 쉽게 만들 수 있습니다. Fragment.setRetainInstance (boolean) 메서드를 사용하여 구성 변경시에도 조각 인스턴스를 유지합니다. 이것은 문서에서 Activity.onRetainnonConfigurationInstance () 에 대한 권장 대체입니다 .

어떤 이유로 유지 된 조각을 사용하고 싶지 않은 경우 취할 수있는 다른 접근 방식이 있습니다. 각 조각에는 Fragment.getId ()에 의해 반환 된 고유 식별자가 있습니다. 또한 Fragment.getActivity (). isChangingConfigurations ()를 통해 구성 변경을 위해 조각이 해체되고 있는지 확인할 수 있습니다 . 따라서 AsyncTask (onStop () 또는 onDestroy ()에서)를 중지하기로 결정한 시점에서 예를 들어 구성이 변경되는지 확인하고, 그렇다면 조각의 식별자 아래에있는 정적 SparseArray에 고정 할 수 있습니다. 그런 다음 onCreate () 또는 onStart ()에서 사용 가능한 희소 배열에 AsyncTask가 있는지 확인하십시오.


setRetainInstance는 백 스택을 사용하지 않는 경우에만 사용됩니다.
Neil

4
AsyncTask가 유지 된 Fragment의 onCreateView가 실행되기 전에 결과를 다시 보낼 수 있습니까?
jakk

6
@jakk 액티비티, 프래그먼트 등에 대한 라이프 사이클 메소드는 메인 GUI 스레드의 메시지 큐에 의해 순차적으로 호출되므로 이러한 라이프 사이클 메소드가 완료되기 전에 (또는 호출되기까지) 작업이 백그라운드에서 동시에 완료 되더라도 onPostExecute메소드는 계속해야합니다. 마지막으로 메인 스레드의 메시지 큐에 의해 처리되기 전에 기다립니다.
Alex Lockwood

이 방법 (RetainInstance = true)은 각 방향에 대해 다른 레이아웃 파일을로드하려는 경우 작동하지 않습니다.
Justin

MainActivity의 onCreate 메서드 내에서 asynctask를 시작하는 것은 "worker"조각 내에서 asynctask가 명시적인 사용자 작업에 의해 시작된 경우에만 작동하는 것처럼 보입니다. 메인 스레드와 사용자 인터페이스를 사용할 수 있기 때문입니다. 그러나 버튼 클릭과 같은 사용자 작업없이 앱을 시작한 직후 asynctask를 시작하면 예외가 발생합니다. 이 경우 asynctask는 onCreate 메서드가 아닌 MainActivity의 onStart 메서드에서 호출 될 수 있습니다.
ʕ ᵔᴥᵔ ʔ

66

아래에 자세히 설명 된 매우 포괄적이고 실제적인 예제를 즐길 수있을 것입니다.

  1. 회전이 작동하고 대화가 유지됩니다.
  2. 뒤로 버튼을 눌러 작업 및 대화 상자를 취소 할 수 있습니다 (이 동작을 원할 경우).
  3. 조각을 사용합니다.
  4. 활동 아래에있는 조각의 레이아웃은 장치가 회전 할 때 올바르게 변경됩니다.
  5. 완전한 소스 코드 다운로드와 사전 컴파일 된 APK가 있으므로 원하는 동작인지 확인할 수 있습니다.

편집하다

Brad Larson의 요청에 따라 아래 링크 된 솔루션의 대부분을 재현했습니다. 또한 게시 한 이후로 지적되었습니다 AsyncTaskLoader. 동일한 문제에 완전히 적용 할 수 있을지 모르겠지만 어쨌든 확인해야합니다.

사용 AsyncTask진행률 대화 상자 및 장치 회전과 함께 .

작동하는 솔루션!

드디어 일할 모든 것을 얻었습니다. 내 코드에는 다음과 같은 기능이 있습니다.

  1. Fragment방향에 따라 레이아웃이 변경되는 입니다.
  2. AsyncTask 을 할 수있는 곳.
  3. DialogFragment진행률 표시 줄에서 작업의 진행 (다만 불확실한 회)를 표시한다.
  4. 회전은 작업을 중단하거나 대화 상자를 닫지 않고 작동합니다.
  5. 뒤로 버튼은 대화 상자를 닫고 작업을 취소합니다 (이 동작은 매우 쉽게 변경할 수 있음).

일 함의 조합은 다른 곳에서는 찾을 수 없다고 생각합니다.

기본 아이디어는 다음과 같습니다. MainActivity단일 조각을 포함 하는 클래스가 있습니다 MainFragment. MainFragment가로 및 세로 방향에 대한 레이아웃이 다르며 레이아웃 setRetainInstance()이 변경 될 수 있도록 false입니다. 즉, 장치 방향이 변경되면 MainActivityMainFragment 완전히 파괴되고 다시 생성됩니다.

별도로 모든 작업을 수행하는 MyTask(에서 확장 AsyncTask)이 있습니다. 그것은 MainFragment파괴 될 것이기 때문에 우리는 그것을 저장할 수 없으며 Google은 setRetainNonInstanceConfiguration(). 어쨌든 항상 사용할 수있는 것은 아니며 기껏해야 추악한 해킹입니다. 대신 라는 MyTask다른 조각에 저장 합니다. 단편 것이다 한 참으로 설정되므로 장치의 회전으로이 단편은 파괴되지 않고,DialogFragmentTaskFragmentsetRetainInstance()MyTask 유지된다.

마지막으로 우리 TaskFragment는 그것이 끝났을 때 누구에게 알릴 것인지를 알려줄 필요 setTargetFragment(<the MainFragment>)가 있습니다. 그리고 우리는 그것을 만들 때 그것을 사용하여 그렇게 합니다. 장치가 회전되고 MainFragment가 파괴되고 새 인스턴스가 생성되면을 사용 FragmentManager하여 대화 상자 (태그 기반)를 찾고setTargetFragment(<the new MainFragment>) . 그게 다야.

다른 두 가지 작업이 필요했습니다. 먼저 대화 상자가 닫힐 때 작업을 취소하고 두 번째로 닫기 메시지를 null로 설정합니다. 그렇지 않으면 장치가 회전 할 때 대화 상자가 이상하게 닫힙니다.

코드

나는 레이아웃을 나열하지 않을 것입니다. 그들은 매우 분명하며 아래 프로젝트 다운로드에서 찾을 수 있습니다.

주요 활동

이것은 매우 간단합니다. 이 활동에 콜백을 추가하여 작업이 언제 완료되는지 알 수 있지만 필요하지 않을 수도 있습니다. 주로 단편 활동 콜백 메커니즘을 보여주고 싶었습니다. 매우 깔끔하고 이전에는 보지 못했을 수도 있기 때문입니다.

public class MainActivity extends Activity implements MainFragment.Callbacks
{
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
    @Override
    public void onTaskFinished()
    {
        // Hooray. A toast to our success.
        Toast.makeText(this, "Task finished!", Toast.LENGTH_LONG).show();
        // NB: I'm going to blow your mind again: the "int duration" parameter of makeText *isn't*
        // the duration in milliseconds. ANDROID Y U NO ENUM? 
    }
}

MainFragment

길지만 그만한 가치가 있습니다!

public class MainFragment extends Fragment implements OnClickListener
{
    // This code up to onDetach() is all to get easy callbacks to the Activity. 
    private Callbacks mCallbacks = sDummyCallbacks;

    public interface Callbacks
    {
        public void onTaskFinished();
    }
    private static Callbacks sDummyCallbacks = new Callbacks()
    {
        public void onTaskFinished() { }
    };

    @Override
    public void onAttach(Activity activity)
    {
        super.onAttach(activity);
        if (!(activity instanceof Callbacks))
        {
            throw new IllegalStateException("Activity must implement fragment's callbacks.");
        }
        mCallbacks = (Callbacks) activity;
    }

    @Override
    public void onDetach()
    {
        super.onDetach();
        mCallbacks = sDummyCallbacks;
    }

    // Save a reference to the fragment manager. This is initialised in onCreate().
    private FragmentManager mFM;

    // Code to identify the fragment that is calling onActivityResult(). We don't really need
    // this since we only have one fragment to deal with.
    static final int TASK_FRAGMENT = 0;

    // Tag so we can find the task fragment again, in another instance of this fragment after rotation.
    static final String TASK_FRAGMENT_TAG = "task";

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

        // At this point the fragment may have been recreated due to a rotation,
        // and there may be a TaskFragment lying around. So see if we can find it.
        mFM = getFragmentManager();
        // Check to see if we have retained the worker fragment.
        TaskFragment taskFragment = (TaskFragment)mFM.findFragmentByTag(TASK_FRAGMENT_TAG);

        if (taskFragment != null)
        {
            // Update the target fragment so it goes to this fragment instead of the old one.
            // This will also allow the GC to reclaim the old MainFragment, which the TaskFragment
            // keeps a reference to. Note that I looked in the code and setTargetFragment() doesn't
            // use weak references. To be sure you aren't leaking, you may wish to make your own
            // setTargetFragment() which does.
            taskFragment.setTargetFragment(this, TASK_FRAGMENT);
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState)
    {
        return inflater.inflate(R.layout.fragment_main, container, false);
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState)
    {
        super.onViewCreated(view, savedInstanceState);

        // Callback for the "start task" button. I originally used the XML onClick()
        // but it goes to the Activity instead.
        view.findViewById(R.id.taskButton).setOnClickListener(this);
    }

    @Override
    public void onClick(View v)
    {
        // We only have one click listener so we know it is the "Start Task" button.

        // We will create a new TaskFragment.
        TaskFragment taskFragment = new TaskFragment();
        // And create a task for it to monitor. In this implementation the taskFragment
        // executes the task, but you could change it so that it is started here.
        taskFragment.setTask(new MyTask());
        // And tell it to call onActivityResult() on this fragment.
        taskFragment.setTargetFragment(this, TASK_FRAGMENT);

        // Show the fragment.
        // I'm not sure which of the following two lines is best to use but this one works well.
        taskFragment.show(mFM, TASK_FRAGMENT_TAG);
//      mFM.beginTransaction().add(taskFragment, TASK_FRAGMENT_TAG).commit();
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data)
    {
        if (requestCode == TASK_FRAGMENT && resultCode == Activity.RESULT_OK)
        {
            // Inform the activity. 
            mCallbacks.onTaskFinished();
        }
    }

TaskFragment

    // This and the other inner class can be in separate files if you like.
    // There's no reason they need to be inner classes other than keeping everything together.
    public static class TaskFragment extends DialogFragment
    {
        // The task we are running.
        MyTask mTask;
        ProgressBar mProgressBar;

        public void setTask(MyTask task)
        {
            mTask = task;

            // Tell the AsyncTask to call updateProgress() and taskFinished() on this fragment.
            mTask.setFragment(this);
        }

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

            // Retain this instance so it isn't destroyed when MainActivity and
            // MainFragment change configuration.
            setRetainInstance(true);

            // Start the task! You could move this outside this activity if you want.
            if (mTask != null)
                mTask.execute();
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                Bundle savedInstanceState)
        {
            View view = inflater.inflate(R.layout.fragment_task, container);
            mProgressBar = (ProgressBar)view.findViewById(R.id.progressBar);

            getDialog().setTitle("Progress Dialog");

            // If you're doing a long task, you probably don't want people to cancel
            // it just by tapping the screen!
            getDialog().setCanceledOnTouchOutside(false);

            return view;
        }

        // This is to work around what is apparently a bug. If you don't have it
        // here the dialog will be dismissed on rotation, so tell it not to dismiss.
        @Override
        public void onDestroyView()
        {
            if (getDialog() != null && getRetainInstance())
                getDialog().setDismissMessage(null);
            super.onDestroyView();
        }

        // Also when we are dismissed we need to cancel the task.
        @Override
        public void onDismiss(DialogInterface dialog)
        {
            super.onDismiss(dialog);
            // If true, the thread is interrupted immediately, which may do bad things.
            // If false, it guarantees a result is never returned (onPostExecute() isn't called)
            // but you have to repeatedly call isCancelled() in your doInBackground()
            // function to check if it should exit. For some tasks that might not be feasible.
            if (mTask != null) {
                mTask.cancel(false);
            }

            // You don't really need this if you don't want.
            if (getTargetFragment() != null)
                getTargetFragment().onActivityResult(TASK_FRAGMENT, Activity.RESULT_CANCELED, null);
        }

        @Override
        public void onResume()
        {
            super.onResume();
            // This is a little hacky, but we will see if the task has finished while we weren't
            // in this activity, and then we can dismiss ourselves.
            if (mTask == null)
                dismiss();
        }

        // This is called by the AsyncTask.
        public void updateProgress(int percent)
        {
            mProgressBar.setProgress(percent);
        }

        // This is also called by the AsyncTask.
        public void taskFinished()
        {
            // Make sure we check if it is resumed because we will crash if trying to dismiss the dialog
            // after the user has switched to another app.
            if (isResumed())
                dismiss();

            // If we aren't resumed, setting the task to null will allow us to dimiss ourselves in
            // onResume().
            mTask = null;

            // Tell the fragment that we are done.
            if (getTargetFragment() != null)
                getTargetFragment().onActivityResult(TASK_FRAGMENT, Activity.RESULT_OK, null);
        }
    }

MyTask

    // This is a fairly standard AsyncTask that does some dummy work.
    public static class MyTask extends AsyncTask<Void, Void, Void>
    {
        TaskFragment mFragment;
        int mProgress = 0;

        void setFragment(TaskFragment fragment)
        {
            mFragment = fragment;
        }

        @Override
        protected Void doInBackground(Void... params)
        {
            // Do some longish task. This should be a task that we don't really
            // care about continuing
            // if the user exits the app.
            // Examples of these things:
            // * Logging in to an app.
            // * Downloading something for the user to view.
            // * Calculating something for the user to view.
            // Examples of where you should probably use a service instead:
            // * Downloading files for the user to save (like the browser does).
            // * Sending messages to people.
            // * Uploading data to a server.
            for (int i = 0; i < 10; i++)
            {
                // Check if this has been cancelled, e.g. when the dialog is dismissed.
                if (isCancelled())
                    return null;

                SystemClock.sleep(500);
                mProgress = i * 10;
                publishProgress();
            }
            return null;
        }

        @Override
        protected void onProgressUpdate(Void... unused)
        {
            if (mFragment == null)
                return;
            mFragment.updateProgress(mProgress);
        }

        @Override
        protected void onPostExecute(Void unused)
        {
            if (mFragment == null)
                return;
            mFragment.taskFinished();
        }
    }
}

예제 프로젝트 다운로드

다음은 소스 코드APK 입니다. 죄송합니다. ADT는 제가 프로젝트를 만들 수 있기 전에 지원 라이브러리를 추가해야한다고 주장했습니다. 제거 할 수 있다고 확신합니다.


4
DialogFragment이전 컨텍스트에 대한 참조를 보유하는 UI 요소가 있기 때문에 진행률 표시 줄을 유지하지 않을 것 입니다. 대신 AsyncTask다른 빈 조각에 저장 DialogFragment하고 대상으로 설정 합니다.
SD

장치를 회전하고 onCreateView()다시 호출 하면 해당 참조가 지워지지 않습니까? 이전 mProgressBar은 최소한 새 것으로 덮어 씁니다.
Timmmm

명시 적으로는 아니지만 나는 그것에 대해 꽤 확신합니다. 당신은 추가 할 수 있습니다 mProgressBar = null;onDestroyView()당신이 추가로 확인하려는 경우. Singularity의 방법은 좋은 생각 일 수 있지만 코드 복잡성을 더욱 증가시킬 것입니다!
Timmmm 2012

1
asynctask에 대한 참조는 progressdialog 조각입니다. 그래서 2 개의 질문 : 1- 만약 내가 progressdialog를 호출하는 실제 조각을 바꾸고 싶다면? 2- params를 asynctask에 전달하려면 어떻게해야합니까? 감사합니다.
Maxrunner

1
@Maxrunner는 매개 변수를 전달하는 가장 쉬운 것은 이동에 아마 mTask.execute()MainFragment.onClick(). 또는 매개 변수가 전달되도록 허용 setTask()하거나 MyTask자체적으로 저장할 수도 있습니다. 첫 번째 질문이 무엇을 의미하는지 정확히 모르겠지만 사용할 수 TaskFragment.getTargetFragment()있습니까? 나는 그것이 ViewPager. 그러나 ViewPagers잘 이해되거나 문서화되지 않았으므로 행운을 빕니다! 조각이 처음 표시 될 때까지 생성되지 않습니다.
Timmmm

16

최근 유지 된 Fragments를 사용하여 구성 변경을 처리하는 방법을 설명하는 기사게시했습니다 . 유지 문제를 해결합니다.AsyncTask회전 변경 멋지게 .

TL; DR은 AsyncTask내부에서 호스트 를 사용하고을 Fragment호출 setRetainInstance(true)하고 의 진행 상황 / 결과를 다시 Fragment보고 (또는 @Timmmm에서 설명하는 접근 방식을 사용하기로 선택한 경우 대상 )를 통해 .AsyncTaskActivityFragmentFragment


5
중첩 된 프래그먼트는 어떻게 하시겠습니까? AsyncTask가 다른 Fragment (Tab) 내부의 RetainedFragment에서 시작된 것과 같습니다.
Rekin

조각이 이미 유지 된 경우 유지 된 조각 내에서 비동기 작업을 실행하지 않는 이유는 무엇입니까? 이미 유지 된 경우 구성 변경이 발생하더라도 비동기 작업이 다시보고 할 수 있습니다.
Alex Lockwood

@AlexLockwood 블로그에 감사드립니다. onAttachand 를 처리하는 대신 onDetach, 내부 TaskFragment에 있으면 더 getActivity좋을까요? 우리는 콜백을 실행해야 할 때마다 호출합니다. (Instaceof 확인 TaskCallbacks)
Cheok Yan Cheng 2014

1
어느 쪽이든 할 수 있습니다. 난 그냥 그것을했다 onAttach()onDetach()그래서 난에 활동을 주조 지속적으로 피할 수있는 TaskCallbacks내가 그것을 사용하고자 할 때마다.
Alex Lockwood 2014

1
@AlexLockwood 애플리케이션이 단일 활동 (다중 조각 디자인)을 따르는 경우 각 UI 조각에 대해 별도의 작업 조각이 있어야합니까? 따라서 기본적으로 각 작업 조각의 수명주기는 대상 조각에 의해 관리되며 활동과의 통신이 없습니다.
Manas Bajaj 2015 년

13

내 첫 번째 제안은 내부 AsyncTasks피하는 것입니다. 이에 대해 질문 한 질문과 답변을 읽을 수 있습니다. Android : AsyncTask Recommendation : private class or public class?

그 후 나는 비 내부를 사용하기 시작했고 ... 이제 많은 이점을 볼 수 있습니다.

두 번째는 Application클래스 에서 실행중인 AsyncTask에 대한 참조를 유지하는 것입니다-http : //developer.android.com/reference/android/app/Application.html

AsyncTask를 시작할 때마다 응용 프로그램에서 설정하고 완료되면 null로 설정합니다.

프래그먼트 / 액티비티가 시작되면 AsyncTask가 실행 중인지 (응용 프로그램에서 null인지 아닌지 확인하여) 확인한 다음 원하는 항목 (액티비티, 프래그먼트 등)을 참조하여 콜백을 수행 할 수 있습니다.

이렇게하면 문제가 해결됩니다. 결정된 시간에 1 개의 AsyncTask 만 실행중인 경우 간단한 참조를 추가 할 수 있습니다.

AsyncTask<?,?,?> asyncTask = null;

그렇지 않으면 Aplication에서 HashMap을 참조하십시오.

진행률 대화 상자는 똑같은 원칙을 따를 수 있습니다.


2
AsyncTask의 수명주기를 부모에 바인딩하는 한 (AsyncTask를 Activity / Fragment의 내부 클래스로 정의하여) AsyncTask를 부모의 수명주기 레크리에이션에서 벗어나게 만드는 것이 상당히 어렵습니다. 솔루션, 너무 해키 보입니다.
yorkw

문제는 .. 더 나은 해결책이 있습니까?
neteinstein 2011

1
여기서 @yorkw에 동의해야합니다.이 솔루션은 얼마 전에 조각 (활동 기반 앱)을 사용하지 않고이 문제를 처리 할 때 나에게 제시된 aws입니다. 이 질문 : stackoverflow.com/questions/2620917/… 은 동일한 답을 가지고 있으며, "응용 프로그램 인스턴스에는 자체 수명주기가 있습니다. OS에 의해 종료 될 수 있으므로이 솔루션은 재현하기 어려운 버그 "
blindstuff

1
여전히 @yorkw가 말한 것처럼 "해키"가 덜한 다른 방법은 없습니다. 나는 여러 앱에서 그것을 사용하고 있으며 가능한 문제에 약간의주의를 기울이면 모두 훌륭하게 작동합니다.
neteinstein 2011

@hackbod 솔루션이 더 적합 할 수 있습니다.
neteinstein

4

이를 위해 AsyncTaskLoaders를 사용하는 방법을 생각해 냈습니다. 사용하기 매우 쉽고 오버 헤드가 적은 IMO가 필요합니다.

기본적으로 다음과 같이 AsyncTaskLoader를 만듭니다.

public class MyAsyncTaskLoader extends AsyncTaskLoader {
    Result mResult;
    public HttpAsyncTaskLoader(Context context) {
        super(context);
    }

    protected void onStartLoading() {
        super.onStartLoading();
        if (mResult != null) {
            deliverResult(mResult);
        }
        if (takeContentChanged() ||  mResult == null) {
            forceLoad();
        }
    }

    @Override
    public Result loadInBackground() {
        SystemClock.sleep(500);
        mResult = new Result();
        return mResult;
    }
}

그런 다음 버튼을 클릭 할 때 위의 AsyncTaskLoader를 사용하는 활동에서 다음을 수행합니다.

public class MyActivityWithBackgroundWork extends FragmentActivity implements LoaderManager.LoaderCallbacks<Result> {

    private String username,password;       
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // TODO Auto-generated method stub
        super.onCreate(savedInstanceState);
        setContentView(R.layout.mylayout);
        //this is only used to reconnect to the loader if it already started
        //before the orientation changed
        Loader loader = getSupportLoaderManager().getLoader(0);
        if (loader != null) {
            getSupportLoaderManager().initLoader(0, null, this);
        }
    }

    public void doBackgroundWorkOnClick(View button) {
        //might want to disable the button while you are doing work
        //to prevent user from pressing it again.

        //Call resetLoader because calling initLoader will return
        //the previous result if there was one and we may want to do new work
        //each time
        getSupportLoaderManager().resetLoader(0, null, this);
    }   


    @Override
    public Loader<Result> onCreateLoader(int i, Bundle bundle) {
        //might want to start a progress bar
        return new MyAsyncTaskLoader(this);
    }


    @Override
    public void onLoadFinished(Loader<LoginResponse> loginLoader,
                               LoginResponse loginResponse)
    {
        //handle result
    }

    @Override
    public void onLoaderReset(Loader<LoginResponse> responseAndJsonHolderLoader)
    {
        //remove references to previous loader resources

    }
}

이것은 방향 변경을 잘 처리하는 것으로 보이며 회전하는 동안 백그라운드 작업이 계속됩니다.

참고할 몇 가지 사항 :

  1. onCreate에서 asynctaskloader에 다시 연결하면 이전 결과와 함께 onLoadFinished ()에서 다시 호출됩니다 (이미 요청이 완료되었다고 말했더라도). 이것은 실제로 대부분의 경우 좋은 동작이지만 때로는 처리하기가 까다로울 수 있습니다. 나는 이것을 처리하는 많은 방법이 있다고 생각하지만 내가 한 일은 onLoadFinished에서 loader.abandon ()이라고 불렀습니다. 그런 다음 아직 포기하지 않은 경우에만 로더에 다시 연결하도록 onCreate 체크인을 추가했습니다. 결과 데이터가 다시 필요하면 그렇게하고 싶지 않을 것입니다. 대부분의 경우 데이터를 원합니다.

여기 에 http 호출에 이것을 사용하는 방법에 대한 자세한 내용이 있습니다.


확실히 그것은 getSupportLoaderManager().getLoader(0);null을 반환하지 않을 것입니다 (해당 ID가 0 인 로더가 아직 존재하지 않기 때문입니다)?
EmmanuelMess

1
그래, 구성 변경으로 인해 로더가 진행중인 동안 활동이 다시 시작되지 않는 한 null이됩니다. 그래서 null을 확인했습니다.
Matt Wolfe

3

저는 Marshmallow를 기반으로 AsyncTask하지만 다음과 같은 추가 기능이 있는 매우 작은 오픈 소스 백그라운드 작업 라이브러리를 만들었습니다 .

  1. 구성 변경시 작업을 자동으로 유지합니다.
  2. UI 콜백 (리스너)
  3. 장치가 회전 할 때 작업을 다시 시작하거나 취소하지 않습니다 (로더처럼).

라이브러리는 내부적으로 Fragment사용자 인터페이스없이를 사용하며 이는 구성 변경 ( setRetainInstance(true))에 걸쳐 유지 됩니다.

GitHub에서 찾을 수 있습니다 : https://github.com/NeoTech-Software/Android-Retainable-Tasks

가장 기본적인 예 (버전 0.2.0) :

이 예제는 매우 제한된 양의 코드를 사용하여 작업을 완전히 유지합니다.

직무:

private class ExampleTask extends Task<Integer, String> {

    public ExampleTask(String tag){
        super(tag);
    }

    protected String doInBackground() {
        for(int i = 0; i < 100; i++) {
            if(isCancelled()){
                break;
            }
            SystemClock.sleep(50);
            publishProgress(i);
        }
        return "Result";
    }
}

활동:

public class Main extends TaskActivityCompat implements Task.Callback {

    @Override
    public void onClick(View view){
        ExampleTask task = new ExampleTask("activity-unique-tag");
        getTaskManager().execute(task, this);
    }

    @Override
    public Task.Callback onPreAttach(Task<?, ?> task) {
        //Restore the user-interface based on the tasks state
        return this; //This Activity implements Task.Callback
    }

    @Override
    public void onPreExecute(Task<?, ?> task) {
        //Task started
    }

    @Override
    public void onPostExecute(Task<?, ?> task) {
        //Task finished
        Toast.makeText(this, "Task finished", Toast.LENGTH_SHORT).show();
    }
}

1

내 접근 방식은 위임 디자인 패턴을 사용하는 것입니다. 일반적으로 AysncTask.doInBackground () 메서드에서 AsyncTask (위임자)에서 BusinessDAO (대리자)로 실제 비즈니스 논리 (인터넷 또는 데이터베이스에서 데이터 읽기 등)를 분리 할 수 ​​있습니다. , 실제 작업을 BusinessDAO에 위임 한 다음 BusinessDAO에서 단일 프로세스 메커니즘을 구현하여 BusinessDAO.doSomething ()을 여러 번 호출하면 매번 실행되고 작업 결과를 기다리는 하나의 실제 작업이 트리거됩니다. 아이디어는 위임자 (예 : AsyncTask) 대신 구성 변경 중에 위임 (예 : BusinessDAO)을 유지하는 것입니다.

  1. 자체 애플리케이션을 생성 / 구현합니다. 목적은 여기에서 BusinessDAO를 생성 / 초기화하는 것이므로 BusinessDAO의 수명주기가 활동 범위가 아닌 애플리케이션 범위가되도록합니다. MyApplication을 사용하려면 AndroidManifest.xml을 변경해야합니다.

    public class MyApplication extends android.app.Application {
      private BusinessDAO businessDAO;
    
      @Override
      public void onCreate() {
        super.onCreate();
        businessDAO = new BusinessDAO();
      }
    
      pubilc BusinessDAO getBusinessDAO() {
        return businessDAO;
      }
    
    }
    
  2. 기존 Activity / Fragement는 대부분 변경되지 않았으며 여전히 AsyncTask를 내부 클래스로 구현하고 Activity / Fragement의 AsyncTask.execute ()를 포함합니다. 차이점은 이제 AsyncTask가 실제 작업을 BusinessDAO에 위임하므로 구성 변경 중에 두 번째 AsyncTask 초기화 및 실행되고 BusinessDAO.doSomething ()을 두 번째로 호출하지만 BusinessDAO.doSomething ()에 대한 두 번째 호출은 새 실행 작업을 트리거하지 않고 대신 현재 실행중인 작업이 완료 될 때까지 기다립니다.

    public class LoginFragment extends Fragment {
      ... ...
    
      public class LoginAsyncTask extends AsyncTask<String, Void, Boolean> {
        // get a reference of BusinessDAO from application scope.
        BusinessDAO businessDAO = ((MyApplication) getApplication()).getBusinessDAO();
    
        @Override
        protected Boolean doInBackground(String... args) {
            businessDAO.doSomething();
            return true;
        }
    
        protected void onPostExecute(Boolean result) {
          //Handle task result and update UI stuff.
        }
      }
    
      ... ...
    }
    
  3. BusinessDAO 내부에서 단일 프로세스 메커니즘을 구현합니다. 예를 들면 다음과 같습니다.

    public class BusinessDAO {
      ExecutorCompletionService<MyTask> completionExecutor = new ExecutorCompletionService<MyTask(Executors.newFixedThreadPool(1));
      Future<MyTask> myFutureTask = null;
    
      public void doSomething() {
        if (myFutureTask == null) {
          // nothing running at the moment, submit a new callable task to run.
          MyTask myTask = new MyTask();
          myFutureTask = completionExecutor.submit(myTask);
        }
        // Task already submitted and running, waiting for the running task to finish.
        myFutureTask.get();
      }
    
      // If you've never used this before, Callable is similar with Runnable, with ability to return result and throw exception.
      private class MyTask extends Callable<MyTask> {
        public MyAsyncTask call() {
          // do your job here.
          return this;
        }
      }
    
    }
    

이것이 작동하는지 100 % 확신하지 못합니다. 또한 샘플 코드 스 니펫을 의사 코드로 간주해야합니다. 디자인 수준에서 약간의 단서를 제공하려고합니다. 피드백이나 제안을 환영합니다.


아주 좋은 해결책 인 것 같습니다. 약 2 년 반 전에이 질문에 답 하셨기 때문에 그 이후로 테스트 했습니까?! 당신은 그것이 작동하는지 확실하지 않다고 말하는 것입니다. 이 문제에 대해 잘 테스트 된 솔루션을 위해 잠그고 있습니다. 의견 있으십니까?
알리레자 A. 아마디

1

AsyncTask를 정적 필드로 만들 수 있습니다. 컨텍스트가 필요하면 애플리케이션 컨텍스트를 제공해야합니다. 이렇게하면 메모리 누수가 방지됩니다. 그렇지 않으면 전체 활동에 대한 참조가 유지됩니다.


1

누군가가이 스레드로가는 길을 찾으면 깨끗한 접근 방식이 app.Service(START_STICKY로 시작된) 비동기 작업을 실행 한 다음 실행중인 서비스를 반복하여 서비스 (및 비동기 작업)가 여전히 있는지 확인하는 것임을 발견했습니다. 달리는;

    public boolean isServiceRunning(String serviceClassName) {
    final ActivityManager activityManager = (ActivityManager) Application.getContext().getSystemService(Context.ACTIVITY_SERVICE);
    final List<RunningServiceInfo> services = activityManager.getRunningServices(Integer.MAX_VALUE);

    for (RunningServiceInfo runningServiceInfo : services) {
        if (runningServiceInfo.service.getClassName().equals(serviceClassName)){
            return true;
        }
    }
    return false;
 }

그렇다면 DialogFragment(또는 무엇이든) 다시 추가 하고 대화 상자가 닫혔는지 확인하십시오.

v4.support.*라이브러리를 사용하는 경우 (작성 당시) setRetainInstance메소드 및 뷰 페이징에 대한 문제를 알고 있기 때문에 특히 적절합니다 . 또한 인스턴스를 유지하지 않으면 다른 리소스 세트 (예 : 새 방향에 대해 다른 뷰 레이아웃)를 사용하여 활동을 다시 만들 수 있습니다.


AsyncTask를 유지하기 위해 서비스를 실행하는 것이 과도하지 않습니까? 서비스는 자체 프로세스에서 실행되며 추가 비용 없이는 제공되지 않습니다.
WeNeigh

흥미로운 Vinay. 앱이 더 많은 리소스를 차지하는 것을 알아 차리지 못했습니다 (현재로서는 매우 가볍습니다). 무엇을 찾았습니까? 나는 서비스를 UI 상태에 관계없이 시스템이 무거운 작업이나 I / O를 수행 할 수 있도록하는 예측 가능한 환경으로 봅니다. 어떤 것이 완료되었는지 확인하기 위해 서비스와 통신하는 것이 '올바른'것처럼 보였습니다. 약간의 작업을 실행하기 시작하는 서비스는 작업이 완료되면 중지되므로 일반적으로 약 10-30 초 동안 유지됩니다.
BrantApps 2011

Commonsware의 대답은 여기에 서비스가 나쁜 생각이다 제안 할 것으로 보인다. 이제 AsyncTaskLoaders를 고려하고 있지만 자체 문제가있는 것 같습니다 (유연성, 데이터로드에만 해당)
WeNeigh

1
내가 참조. 눈에 띄게, 귀하가 연결 한이 서비스는 자체 프로세스에서 실행되도록 명시 적으로 설정되었습니다. 마스터는이 패턴이 자주 사용되는 것을 좋아하지 않는 것 같습니다. 나는 "매번 새로운 프로세스에서 실행"속성을 명시 적으로 제공하지 않았으므로 비판의 해당 부분에서 격리 되었기를 바랍니다. 나는 그 효과를 정량화하기 위해 노력할 것입니다. 물론 개념으로서의 서비스는 '나쁜 아이디어'가 아니며 원격으로 흥미롭고 의도 된 말장난을하는 모든 앱의 기본입니다. 그들의 JDoc은 여전히 ​​확실하지 않은 경우 사용에 대한 더 많은 지침을 제공합니다.
BrantApps 2011

0

이 문제를 해결하기 위해 samepl 코드를 작성합니다.

첫 번째 단계는 애플리케이션 클래스를 만드는 것입니다.

public class TheApp extends Application {

private static TheApp sTheApp;
private HashMap<String, AsyncTask<?,?,?>> tasks = new HashMap<String, AsyncTask<?,?,?>>();

@Override
public void onCreate() {
    super.onCreate();
    sTheApp = this;
}

public static TheApp get() {
    return sTheApp;
}

public void registerTask(String tag, AsyncTask<?,?,?> task) {
    tasks.put(tag, task);
}

public void unregisterTask(String tag) {
    tasks.remove(tag);
}

public AsyncTask<?,?,?> getTask(String tag) {
    return tasks.get(tag);
}
}

AndroidManifest.xml에서

<application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme"
        android:name="com.example.tasktest.TheApp">

활동 코드 :

public class MainActivity extends Activity {

private Task1 mTask1;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    mTask1 = (Task1)TheApp.get().getTask("task1");

}

/*
 * start task is not running jet
 */
public void handletask1(View v) {
    if (mTask1 == null) {
        mTask1 = new Task1();
        TheApp.get().registerTask("task1", mTask1);
        mTask1.execute();
    } else
        Toast.makeText(this, "Task is running...", Toast.LENGTH_SHORT).show();

}

/*
 * cancel task if is not finished
 */
public void handelCancel(View v) {
    if (mTask1 != null)
        mTask1.cancel(false);
}

public class Task1 extends AsyncTask<Void, Void, Void>{

    @Override
    protected Void doInBackground(Void... params) {
        try {
            for(int i=0; i<120; i++) {
                Thread.sleep(1000);
                Log.i("tests", "loop=" + i);
                if (this.isCancelled()) {
                    Log.e("tests", "tssk cancelled");
                    break;
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    protected void onCancelled(Void result) {
        TheApp.get().unregisterTask("task1");
        mTask1 = null;
    }

    @Override
    protected void onPostExecute(Void result) {
        TheApp.get().unregisterTask("task1");
        mTask1 = null;
    }
}

}

활동 방향이 변경되면 변수 mTask가 앱 컨텍스트에서 초기화됩니다. 작업이 완료되면 변수가 null로 설정되고 메모리에서 제거됩니다.

나를 위해 충분합니다.


0

백그라운드 작업을 유지하기 위해 유지 된 조각을 사용하는 방법에 대한 아래 예제를 살펴보십시오.

public class NetworkRequestFragment extends Fragment {

    // Declare some sort of interface that your AsyncTask will use to communicate with the Activity
    public interface NetworkRequestListener {
        void onRequestStarted();
        void onRequestProgressUpdate(int progress);
        void onRequestFinished(SomeObject result);
    }

    private NetworkTask mTask;
    private NetworkRequestListener mListener;

    private SomeObject mResult;

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);

        // Try to use the Activity as a listener
        if (activity instanceof NetworkRequestListener) {
            mListener = (NetworkRequestListener) activity;
        } else {
            // You can decide if you want to mandate that the Activity implements your callback interface
            // in which case you should throw an exception if it doesn't:
            throw new IllegalStateException("Parent activity must implement NetworkRequestListener");
            // or you could just swallow it and allow a state where nobody is listening
        }
    }

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

        // Retain this Fragment so that it will not be destroyed when an orientation
        // change happens and we can keep our AsyncTask running
        setRetainInstance(true);
    }

    /**
     * The Activity can call this when it wants to start the task
     */
    public void startTask(String url) {
        mTask = new NetworkTask(url);
        mTask.execute();
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        // If the AsyncTask finished when we didn't have a listener we can
        // deliver the result here
        if ((mResult != null) && (mListener != null)) {
            mListener.onRequestFinished(mResult);
            mResult = null;
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();

        // We still have to cancel the task in onDestroy because if the user exits the app or
        // finishes the Activity, we don't want the task to keep running
        // Since we are retaining the Fragment, onDestroy won't be called for an orientation change
        // so this won't affect our ability to keep the task running when the user rotates the device
        if ((mTask != null) && (mTask.getStatus == AsyncTask.Status.RUNNING)) {
            mTask.cancel(true);
        }
    }

    @Override
    public void onDetach() {
        super.onDetach();

        // This is VERY important to avoid a memory leak (because mListener is really a reference to an Activity)
        // When the orientation change occurs, onDetach will be called and since the Activity is being destroyed
        // we don't want to keep any references to it
        // When the Activity is being re-created, onAttach will be called and we will get our listener back
        mListener = null;
    }

    private class NetworkTask extends AsyncTask<String, Integer, SomeObject> {

        @Override
        protected void onPreExecute() {
            if (mListener != null) {
                mListener.onRequestStarted();
            }
        }

        @Override
        protected SomeObject doInBackground(String... urls) {
           // Make the network request
           ...
           // Whenever we want to update our progress:
           publishProgress(progress);
           ...
           return result;
        }

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

        @Override
        protected void onPostExecute(SomeObject result) {
            if (mListener != null) {
                mListener.onRequestFinished(result);
            } else {
                // If the task finishes while the orientation change is happening and while
                // the Fragment is not attached to an Activity, our mListener might be null
                // If you need to make sure that the result eventually gets to the Activity
                // you could save the result here, then in onActivityCreated you can pass it back
                // to the Activity
                mResult = result;
            }
        }

    }
}

-1

봐 가지고 여기를 .

Timmmm의 솔루션을 기반으로 한 솔루션 있습니다.

하지만 개선했습니다.

  • 이제 솔루션을 확장 할 수 있습니다. 확장 만하면됩니다. FragmentAbleToStartTask

  • 동시에 여러 작업을 계속 실행할 수 있습니다.

    그리고 제 생각에는 startActivityForResult만큼 쉽고 결과를받습니다.

  • 실행중인 작업을 중지하고 특정 작업이 실행 중인지 확인할 수도 있습니다.

내 영어 죄송합니다


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