【问题标题】:AsyncTaskLoader onLoadFinished with a pending task and config changeAsyncTaskLoader onLoadFinished 有一个挂起的任务和配置更改
【发布时间】:2012-07-15 23:41:11
【问题描述】:

我正在尝试使用AsyncTaskLoader 在后台加载数据以填充详细视图以响应选择的列表项。我已经让它大部分工作了,但我仍然有一个问题。如果我在列表中选择第二个项目,然后在第一个选定项目的加载完成之前旋转设备,那么onLoadFinished() 调用将报告正在停止的活动而不是新活动.当只选择一个项目然后旋转时,这很好用。

这是我正在使用的代码。活动:

public final class DemoActivity extends Activity
        implements NumberListFragment.RowTappedListener,
                   LoaderManager.LoaderCallbacks<String> {

    private static final AtomicInteger activityCounter = new AtomicInteger(0);

    private int myActivityId;

    private ResultFragment resultFragment;

    private Integer selectedNumber;

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

        myActivityId = activityCounter.incrementAndGet();
        Log.d("DemoActivity", "onCreate for " + myActivityId);

        setContentView(R.layout.demo);

        resultFragment = (ResultFragment) getFragmentManager().findFragmentById(R.id.result_fragment);

        getLoaderManager().initLoader(0, null, this);

    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.d("DemoActivity", "onDestroy for " + myActivityId);
    }

    @Override
    public void onRowTapped(Integer number) {
        selectedNumber = number;
        resultFragment.setResultText("Fetching details for item " + number + "...");
        getLoaderManager().restartLoader(0, null, this);
    }

    @Override
    public Loader<String> onCreateLoader(int id, Bundle args) {
        return new ResultLoader(this, selectedNumber);
    }

    @Override
    public void onLoadFinished(Loader<String> loader, String data) {
        Log.d("DemoActivity", "onLoadFinished reporting to activity " + myActivityId);
        resultFragment.setResultText(data);
    }

    @Override
    public void onLoaderReset(Loader<String> loader) {

    }

    static final class ResultLoader extends AsyncTaskLoader<String> {

        private static final Random random = new Random();

        private final Integer number;

        private String result;

        ResultLoader(Context context, Integer number) {
            super(context);
            this.number = number;
        }

        @Override
        public String loadInBackground() {
            // Simulate expensive Web call
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            return "Item " + number + " - Price: $" + random.nextInt(500) + ".00, Number in stock: " + random.nextInt(10000);
        }

        @Override
        public void deliverResult(String data) {
            if (isReset()) {
                // An async query came in while the loader is stopped
                return;
            }

            result = data;

            if (isStarted()) {
                super.deliverResult(data);
            }
        }

        @Override
        protected void onStartLoading() {
            if (result != null) {
                deliverResult(result);
            }

            // Only do a load if we have a source to load from
            if (number != null) {
                forceLoad();
            }
        }

        @Override
        protected void onStopLoading() {
            // Attempt to cancel the current load task if possible.
            cancelLoad();
        }

        @Override
        protected void onReset() {
            super.onReset();

            // Ensure the loader is stopped
            onStopLoading();

            result = null;
        }

    }

}

列表片段:

public final class NumberListFragment extends ListFragment {

    interface RowTappedListener {

        void onRowTapped(Integer number);

    }

    private RowTappedListener rowTappedListener;

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

        rowTappedListener = (RowTappedListener) activity;
    }

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

        ArrayAdapter<Integer> adapter = new ArrayAdapter<Integer>(getActivity(),
                                                                  R.layout.simple_list_item_1,
                                                                  Arrays.asList(1, 2, 3, 4, 5, 6));
        setListAdapter(adapter);

    }

    @Override
    public void onListItemClick(ListView l, View v, int position, long id) {
        ArrayAdapter<Integer> adapter = (ArrayAdapter<Integer>) getListAdapter();
        rowTappedListener.onRowTapped(adapter.getItem(position));
    }

}

结果片段:

public final class ResultFragment extends Fragment {

    private TextView resultLabel;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View root = inflater.inflate(R.layout.result_fragment, container, false);

        resultLabel = (TextView) root.findViewById(R.id.result_label);
        if (savedInstanceState != null) {
            resultLabel.setText(savedInstanceState.getString("labelText", ""));
        }

        return root;
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);

        outState.putString("labelText", resultLabel.getText().toString());
    }

    void setResultText(String resultText) {
        resultLabel.setText(resultText);
    }

}

我已经能够使用普通的 AsyncTasks 来完成这项工作,但我正在尝试了解更多关于 Loaders 的信息,因为它们会自动处理配置更改。


编辑:我想我可能已经通过查看LoaderManager 的来源找到了问题所在。在配置更改后调用initLoader 时,LoaderInfo 对象的mCallbacks 字段将使用新活动更新为LoaderCallbacks 的实现,正如我所料。

public <D> Loader<D> initLoader(int id, Bundle args, LoaderManager.LoaderCallbacks<D> callback) {
    if (mCreatingLoader) {
        throw new IllegalStateException("Called while creating a loader");
    }

    LoaderInfo info = mLoaders.get(id);

    if (DEBUG) Log.v(TAG, "initLoader in " + this + ": args=" + args);

    if (info == null) {
        // Loader doesn't already exist; create.
        info = createAndInstallLoader(id, args,  (LoaderManager.LoaderCallbacks<Object>)callback);
        if (DEBUG) Log.v(TAG, "  Created new loader " + info);
    } else {
        if (DEBUG) Log.v(TAG, "  Re-using existing loader " + info);
        info.mCallbacks = (LoaderManager.LoaderCallbacks<Object>)callback;
    }

    if (info.mHaveData && mStarted) {
        // If the loader has already generated its data, report it now.
        info.callOnLoadFinished(info.mLoader, info.mData);
    }

    return (Loader<D>)info.mLoader;
}

但是,当有一个挂起的加载器时,主 LoaderInfo 对象也有一个 mPendingLoader 字段并引用了 LoaderCallbacks,并且这个对象永远不会随着 @987654339 中的新活动而更新@ 场地。我希望看到代码看起来像这样:

// This line was already there
info.mCallbacks = (LoaderManager.LoaderCallbacks<Object>)callback;
// This line is not currently there
info.mPendingLoader.mCallbacks = (LoaderManager.LoaderCallbacks<Object>)callback;

似乎正因为如此,挂起的加载程序在旧的活动实例上调用onLoadFinished。如果我在此方法中设置断点并使用调试器进行我觉得缺少的调用,那么一切都会按我的预期进行。

新问题是:我是否发现了错误,或者这是预期的行为?

【问题讨论】:

  • 所以事实证明onLoadFinished() 正在 实际被调用 - 它只是向旧活动(配置更改之前的活动)报告,而不是向新活动报告.问题已编辑,代码已更新。
  • 为了这个例子,我们可以假设列表中的整数是返回每个产品详细信息的服务器调用的产品 ID。这是否有助于使示例变得不那么琐碎?
  • 更新了问题以包含通过阅读源代码获得的新信息LoaderManager。代码示例也已更新,以模拟在选择每个产品 ID 时加载产品详细信息的更真实场景。
  • 我最近在Loaders 上写了一篇博文...也许你会觉得它有帮助:) androiddesignpatterns.com/2012/08/implementing-loaders.html

标签: android android-loadermanager asynctaskloader android-loader


【解决方案1】:

在大多数情况下,如果 Activity 已经销毁,您应该忽略此类报告。

public void onLoadFinished(Loader<String> loader, String data) {
    Log.d("DemoActivity", "onLoadFinished reporting to activity " + myActivityId);
    if (isDestroyed()) {
       Log.i("DemoActivity", "Activity already destroyed, report ignored: " + data);
       return;
    }
    resultFragment.setResultText(data);
}

您还应该在任何内部类中插入检查isDestroyed()。 Runnable - 是最常用的情况。

例如:

// UI thread
final Handler handler = new Handler();
Executor someExecutorService = ... ;
someExecutorService.execute(new Runnable() {
    public void run() {
        // some heavy operations
        ...
        // notification to UI thread
        handler.post(new Runnable() {
            // this runnable can link to 'dead' activity or any outer instance
            if (isDestroyed()) {
                return;
            }

            // we are alive
            onSomeHeavyOperationFinished();
        });
    }
});

但在这种情况下最好的方法是避免将 Activity 的强引用传递给另一个线程(AsynkTask、Loader、Executor 等)。

最可靠的解决方案在这里:

// BackgroundExecutor.java
public class BackgroundExecutor {
    private static final Executor instance = Executors.newSingleThreadExecutor();

    public static void execute(Runnable command) {
        instance.execute(command);
    }
}

// MyActivity.java
public class MyActivity extends Activity {
    // Some callback method from any button you want
    public void onSomeButtonClicked() {
        // Show toast or progress bar if needed

        // Start your heavy operation
        BackgroundExecutor.execute(new SomeHeavyOperation(this));
    }

    public void onSomeHeavyOperationFinished() {
        if (isDestroyed()) {
            return;
        }

        // Hide progress bar, update UI
    }
}

// SomeHeavyOperation.java
public class SomeHeavyOperation implements Runnable {
    private final WeakReference<MyActivity> ref;

    public SomeHeavyOperation(MyActivity owner) {
        // Unlike inner class we do not store strong reference to Activity here
        this.ref = new WeakReference<MyActivity>(owner);
    }

    public void run() {
        // Perform your heavy operation
        // ...
        // Done!

        // It's time to notify Activity
        final MyActivity owner = ref.get();
        // Already died reference
        if (owner == null) return;

        // Perform notification in UI thread
        owner.runOnUiThread(new Runnable() {
            public void run() {
                owner.onSomeHeavyOperationFinished();
            }
        });
    }
}

【讨论】:

    【解决方案2】:

    也许不是最好的解决方案,但...... 此代码每次都重新启动加载器,这很糟糕,但只能解决这个问题 - 如果您想使用加载器。

    Loader l = getLoaderManager().getLoader(MY_LOADER);
    if (l != null) {
        getLoaderManager().restartLoader(MY_LOADER, null, this);
    } else {
        getLoaderManager().initLoader(MY_LOADER, null, this);
    }
    

    顺便说一句。我正在使用 Cursorloader ...

    【讨论】:

      【解决方案3】:

      一种可能的解决方案是在自定义单例对象中启动 AsyncTask,并从 Activity 中的单例访问 onFinished() 结果。每次旋转屏幕时,执行 onPause() 或 onResume(),都会使用/访问最新的结果。如果您的单例对象中仍然没有结果,则您知道它仍然很忙,或者您可以重新启动任务。

      另一种方法是使用像 Otto 这样的服务总线,或者使用 Service。

      【讨论】:

        【解决方案4】:

        好的,如果我误解了任何内容,请原谅我试图理解这一点,但是当设备旋转时您会丢失对某些内容的引用。

        试一试……

        会添加

        android:configChanges="orientation|keyboardHidden|screenSize"
        

        在该活动的清单中修复您的错误?或阻止onLoadFinished() 说活动已停止?

        【讨论】:

        • 这可能是一种解决方法,但这很可能是 Android 中的真正错误。
        • @StevePomeroy 不一定,当设备旋转时;重新创建活动,从而重新初始化对对象或其他同类变量的任何引用。这可以防止系统通过将活动传递给活动本身来重新创建活动,从而允许它处理/覆盖方向更改。如果设备进入横向模式,某些布局元素可能会重新调整大小或重新定位。
        • @StevePomeroy 您的 asyncloader 从未真正完成,因为它在活动的破坏和重建中被处置
        • 查看上面列出的错误 (code.google.com/p/android/issues/detail?id=36778) 并试用示例代码。这种行为非常不寻常,而且肯定不是所希望的(除非它打算不起作用)。
        • 另外:你是对的 - 自己处理配置更改是解决问题的一种方法。但是,如果 Android 出于某种原因决定清理您的活动,而配置更改调用未处理呢?修复自己不处理配置更改时出现的错误仍然很重要。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-08-10
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多