AsyncTaskLoader onLoadFinished带有待执行任务和配置更改

问题描述:

我试图使用AsyncTaskLoader在后台加载数据以填充详细视图以响应所选列表项。我已经得到它主要工作,但我仍然有一个问题。如果我在列表中选择第二个项目,然后在第一个选定项目的加载完成之前旋转设备,则onLoadFinished()调用将报告正在停止的活动而不是新活动。当选择一个项目然后旋转时,这可以正常工作。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); 
    } 

} 

我已经能够得到这个使用普通AsyncTask s工作,但我想了解更多约为Loader,因为它们会自动处理配置更改。


编辑:我想我可能已经通过查看源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的参考,以及,这个目的是从不与在mCallbacks领域的新的活动更新。我希望看到的代码看起来像这个:

// 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在老年活动实例。如果我在此方法中断点并使用调试器使我感觉缺少的调用一切正常,则我所期望的一切都可以正常工作。

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

+0

看一看['CursorLoader.java'](http://grepcode.com/file_/repository.grepcode.com/java/ext/com.google.android/android/4.0.1_r1/android/ content/CursorLoader.java /?v = source)源代码。尝试实现'onStartLoading','onStopLoading','onCanceled','onReset'和'deliverResult'类似于源代码的做法......'LoaderManager'假定所有这些方法都正确实现。这可能就是为什么你的实现只能部分处理配置更改。 – 2012-07-17 05:24:10

+1

所以事实证明'onLoadFinished()* *实际上是被调用的 - 它只是报告给旧的活动(配置改变之前的活动),而不是新的活动。问题已编辑,代码已更新。 – 2012-07-17 13:50:52

+0

老实说,我认为这里的问题是这个例子本身的微不足道......在现实生活中,'Loader'不包含实际的数据源(即private final int字段是您的实际数据源,是不是?)。 'Loader's也应该监视他们的数据源并在发生变化时进行报告。在大多数情况下,由于Loader足够聪明以保留其旧数据,在配置更改之后,您的新Activity上不会调用onLoadFinished,只有在它看到对其后备数据所做的更改时才会重新加载制作。 – 2012-07-17 14:05:08

好吧我试图理解这个原谅我,如果我误解了任何东西,但是当设备旋转时失去对某些东西的引用。

以刺...

会增加

android:configChanges="orientation|keyboardHidden|screenSize" 

在您的清单该活动改正错误?或阻止onLoadFinished()说活动停止?

+0

这可能是一种解决方法,但这很可能是Android中的一个真正的bug。 – 2012-11-13 21:24:54

+0

@StevePomeroy不一定,当设备旋转时;该活动被重新创建,因此重新初始化任何对该对象或其他变量的引用。这会阻止系统通过将活动传递给活动本身来重新创建活动,从而允许它处理/覆盖方向更改。如果设备进入横向模式,某些布局元素可能会重新调整大小或重新定位。 – StrikeForceZero 2012-11-14 05:59:15

+0

@StevePomeroy您的异步加载程序从未被破坏和重建活动 – StrikeForceZero 2012-11-14 06:05:33

在大多数情况下,如果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()。可运行 - 是最常用的情况。

例如:

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

但在这样的情况下最好的方法是,以避免传递活性强引用另一个线程(AsynkTask,装载,执行器等)。

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

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

也许不是最好的解决办法,但... 这每一次代码重新启动加载器,这是不好的,但只是解决的作品 - 如果你想二手装载机。

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

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

一个可能的解决方案是在自定义单例对象中启动AsyncTask并从Activity中的单例访问onFinished()结果。每当你旋转你的屏幕,去onPause()或onResume(),最新的结果将被使用/访问。如果您的单身物件中仍然没有结果,则说明它仍然很忙或者您可以重新启动任务。

另一种方法是与像Otto这样的服务公司一起工作,或使用服务工作。