如何在AsyncTask回调中获取当前savedInstanceState?

时间:2015-12-17 15:32:18

标签: android multithreading android-asynctask

我遇到了一个有趣的问题,我不确定如何修复它。请考虑以下代码:

public class MainActivity extends AppCompatActivity {

    private Bundle savedState;

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

        savedState = savedInstanceState;
        Log.d("ON CREATE", "savedState is null: "+(savedState==null));
        new CustomTask().execute();
    }

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

        protected Void doInBackground(Void... voids) {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return null;
        }

        protected void onPostExecute(Void v) {
            Log.d("POST EXECUTE", "savedState is null: "+(savedState==null));
        }
    }
}

此代码保存对savedInstanceState的引用,然后运行AsyncTask,它会在5秒后尝试访问该变量。如果用户在AsyncTask完成其工作之前更改了设备的方向,则会生成以下输出:

ON CREATE: savedState is null: true //initial run
ON CREATE: savedState is null: false //re-created after orientation change
POST EXECUTE: savedState is null: true //first instance
POST EXECUTE: savedState is null: false //second instance

两个onPostExecute()方法在方向更改后触发,但它们似乎正在访问相同的savedState变量,在两种情况下我都希望它是非空的,因为它是在方向改变。

显然,在方向更改之前启动的第一个AsyncTask仍然引用了更改之前的savedState变量。所以我的问题是:

为什么这样做?应用程序状态恢复后,我希望AsyncTask只是访问当前状态的所有类成员,这意味着savedState将为非null。

如何从变量更改之前启动的AsyncTask的回调中访问当前的savedInstanceState变量?

2 个答案:

答案 0 :(得分:1)

方向更改后,有两个MainActivity个实例。旧的事件经历了拆除生命周期事件(onStop()onDestroy()等)但是没有被垃圾收集,因为内部类CustomTask线程仍在运行并保持对它的引用。 CustomTask的该实例看到空savedState。它对重新启动后创建的MainActivity新实例及其非空savedState一无所知。

您真的希望CustomTask的原始实例在重启后继续运行吗?也许你应该在活动被销毁时取消它。如果您确实需要它继续运行并且可以访问您现在在活动中声明的状态数据,则需要将该状态数据从活动中移出到其他位置,例如单个对象,{{1}的子类或持久存储。

使用retained fragment可能是在重新启动时保留状态和后台处理的另一种选择。

答案 1 :(得分:0)

经过一个月的处理,我终于找到了解决方案。我一直在努力的关键概念是如何更新(已经运行的)AsyncTask,并引用它正在使用的Activity的当前实例。这是怎么做的。

第一步是将AsyncTask分离到自己的类文件中,并通过构造函数或setter传递对它所使用的Activity的引用。因此,为了使用我在原始问题中使用的相同示例,它看起来像这样:

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

    //This could also be a reference to a callback interface
    //implemented in an Activity
    private Activity activity;  

    public CustomTask(Activity activity) {
        this.activity = activity;
    }       

    protected Void doInBackground(Void... voids) {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return null;
    }

    protected void onPostExecute(Void v) {
        Log.d("POST EXECUTE", "savedState is null: "+(savedState==null));
    }

    //newly added method
    public void setActivity(Activity activity) {
        this.activity = activity;
    }

    //newly added method
    public void detachFromActivity() {
        activity = null;
    }
}  

下一步是将对正在运行的AsyncTask的引用保存为Activity的数据成员。这只是在Activity中创建私有变量private CustomTask customTask;的问题。

接下来,这是重要的部分,您需要在Activity中覆盖onRetainCustomNonConfigurationInstance(),并在此方法中,将AsyncTask与其旧的Activity(在屏幕旋转时销毁)分离并保留引用到任务,所以我们可以在活动的新实例中使用它,当屏幕完成旋转时将重新创建。所以这个方法看起来像这样:

@Override
public Object onRetainCustomNonConfigurationInstance() {
    if(customTask != null) {
        customTask.detachFromActivity();
    }
    return customTask;
}

现在,在屏幕旋转完成并重新创建Activity之后,我们需要获取对传递的任务的引用。所以我们调用getLastCustomNonConfigurationInstance()并将它返回的对象转换为我们特定的AsyncTask类类型。我们可以在这里进行空检查,看看我们是否有一个通过的任务。如果有一个,我们将其侦听器设置为CURRENT Activity引用,以便回调到达正确的Activity实例(因此避免了NullPointerExceptions,IllegalStateExceptions和其他恶意)。这个位应该是这样的:

customTask = (CustomTask) getLastCustomNonConfigurationInstance();
if(customTask != null) {
    customTask.setListener(this);
}

现在,我们正在运行的AsyncTask具有对当前Activity实例的正确引用,并且它将正确地将其回调传递给最新的Activity。

有关此解决方案的使用和限制的详细信息,请参阅此处的Android文档:http://developer.android.com/reference/android/app/Activity.html#onRetainNonConfigurationInstance()