128

我完全迷失了initLoader和 的restartLoader功能之间的差异LoaderManager

  • 他们都有相同的签名。
  • restartLoader如果加载器不存在,还会创建一个加载器(“在此管理器中启动一个新的或重新启动一个现有的加载器”)。

这两种方法之间有什么关系吗?打电话restartLoader总是打电话initLoader吗?我可以restartLoader不打电话就打电话initLoader吗?调用initLoader两次刷新数据是否安全?我什么时候应该使用两者之一,为什么

4

6 回答 6

201

要回答这个问题,您需要深入研究LoaderManager代码。虽然 LoaderManager 本身的文档不够清晰(或者不会有这个问题),但抽象 LoaderManager 的子类 LoaderManagerImpl 的文档更具启发性。

初始化加载器

调用以使用 Loader 初始化特定 ID。如果这个 ID 已经有一个与之关联的 Loader,它保持不变,并且任何以前的回调都替换为新提供的回调。如果当前没有该 ID 的 Loader,则会创建并启动一个新的 Loader。

这个函数一般应该在组件初始化时使用,以确保创建它所依赖的 Loader。这允许它重新使用现有加载器的数据(如果已经存在),因此例如在配置更改后重新创建 Activity 时,它不需要重新创建其加载器。

重启加载器

调用以重新创建与特定 ID 关联的加载程序。如果当前有与此 ID 关联的 Loader,它将酌情取消/停止/销毁。将创建一个具有给定参数的新加载器,并且一旦可用,它的数据就会交付给您。

[...] 调用此函数后,任何与此 ID 关联的先前加载程序都将被视为无效,您将不会收到来自它们的进一步数据更新。

基本上有两种情况:

  1. id 的加载器不存在:两种方法都会创建一个新的加载器,所以没有区别
  2. id 的加载器已经存在:initLoader只会替换作为参数传递的回调,但不会取消或停止加载器。对于CursorLoader这意味着光标保持打开和活动状态(如果在调用之前是这种情况initLoader)。另一方面,`restartLoader 将取消、停止和销毁加载器(并像游标一样关闭底层数据源)并创建一个新的加载器(如果加载器是,它也会创建一个新的游标并重新运行查询游标加载器)。

以下是这两种方法的简化代码:

初始化加载器

LoaderInfo info = mLoaders.get(id);
if (info == null) {
    // Loader doesn't already exist -> create new one
    info = createAndInstallLoader(id, args, LoaderManager.LoaderCallbacks<Object>)callback);
} else {
   // Loader exists -> only replace callbacks   
   info.mCallbacks = (LoaderManager.LoaderCallbacks<Object>)callback;
}

重启加载器

LoaderInfo info = mLoaders.get(id);
if (info != null) {
    LoaderInfo inactive = mInactiveLoaders.get(id);
    if (inactive != null) {
        // does a lot of stuff to deal with already inactive loaders
    } else {
        // Keep track of the previous instance of this loader so we can destroy
        // it when the new one completes.
        info.mLoader.abandon();
        mInactiveLoaders.put(id, info);
    }
}
info = createAndInstallLoader(id, args,  (LoaderManager.LoaderCallbacks<Object>)callback);

正如我们所看到的,如果加载器不存在(info == null),两种方法都会创建一个新的加载器(info = createAndInstallLoader(...))。如果加载器已经存在,initLoader则只替换回调(info.mCallbacks = ...),同时restartLoader停用旧加载器(当新加载器完成其工作时它将被销毁),然后创建一个新加载器。

因此,现在很清楚何时使用initLoader和何时使用restartLoader以及为什么使用这两种方法是有意义的。 initLoader用于确保有一个初始化的加载器。如果不存在,则创建一个新的,如果已经存在,则重新使用它。我们总是使用这种方法,除非我们需要一个新的加载器,因为要运行的查询已经改变(不是底层数据,而是像 CursorLoader 的 SQL 语句中的实际查询),在这种情况下我们将调用restartLoader.

Activity/Fragment 生命周期与使用一种或另一种方法的决定无关(并且没有必要像 Simon 建议的那样使用一次性标志来跟踪调用)!该决定完全基于对新装载机的“需求”。如果我们想运行我们使用的相同查询initLoader,如果我们想运行我们使用的不同查询restartLoader

我们总是可以使用restartLoader,但这将是低效的。在屏幕旋转之后,或者如果用户离开应用程序并稍后返回到同一个 Activity,我们通常希望显示相同的查询结果,因此restartLoader会不必要地重新创建加载程序并关闭底层(可能很昂贵)的查询结果。

了解加载的数据与加载该数据的“查询”之间的区别非常重要。假设我们使用 CursorLoader 查询订单表。restartLoader如果向该表添加新订单,CursorLoader 使用 onContentChanged() 通知 UI 更新并显示新订单(在这种情况下无需使用)。如果我们只想显示未结订单,我们需要一个新查询,我们将使用restartLoader它返回一个反映新查询的新 CursorLoader。


这两种方法之间有什么关系吗?

他们共享代码来创建一个新的加载器,但是当加载器已经存在时,他们会做不同的事情。

打电话restartLoader总是打电话initLoader吗?

不,它永远不会。

我可以restartLoader不打电话就打电话initLoader吗?

是的。

调用initLoader两次刷新数据是否安全?

调用initLoader两次是安全的,但不会刷新数据。

我什么时候应该使用两者之一,为什么


在我上面的解释之后,这应该(希望)很清楚。

配置更改

LoaderManager 在配置更改(包括方向更改)时保持其状态,因此您会认为我们无事可做。再想想...

首先,LoaderManager 不保留回调,因此如果您什么都不做,您将不会收到对您的回调方法onLoadFinished()之类的调用,这很可能会破坏您的应用程序。

因此,我们至少必须调用initLoader来恢复回调方法(restartLoader当然,a 也是可能的)。该文档指出:

如果在调用点调用者处于启动状态,并且请求的加载器已经存在并且已经生成了它的数据,那么回调onLoadFinished(Loader, D)将立即被调用(在这个函数内部)[...]。

这意味着如果我们initLoader在方向更改后调用,我们将立即收到onLoadFinished调用,因为数据已经加载(假设是更改之前的情况)。虽然这听起来很简单,但它可能很棘手(我们不都喜欢 Android...)。

我们必须区分两种情况:

  1. 处理配置本身的变化:对于使用 setRetainInstance(true) 的 Fragment 或android:configChanges清单中具有相应标签的 Activity 就是这种情况。这些组件在屏幕旋转后不会收到 onCreate 调用,因此请记住调用 initLoader/restartLoader另一个回调方法(例如 in onActivityCreated(Bundle))。为了能够初始化加载器,加载器ID需要被存储(例如在一个列表中)。因为组件在配置更改中保留,所以我们可以遍历现有的加载器 id 并调用initLoader(loaderid, ...).
  2. 本身不处理配置更改:在这种情况下,可以在 onCreate 中初始化加载器,但我们需要手动保留加载器 ID,否则我们将无法进行所需的 initLoader/restartLoader 调用。如果 id 存储在 ArrayList 中,我们将
    outState.putIntegerArrayList(loaderIdsKey, loaderIdsArray)在 onSaveInstanceState 中执行并在 onCreate: 中恢复 id: loaderIdsArray = savedInstanceState.getIntegerArrayList(loaderIdsKey)在我们进行 initLoader 调用之前。
于 2014-01-04T03:33:31.073 回答
46

initLoader在 Loader 已经创建时调用(例如,这通常发生在配置更改之后)告诉 LoaderManager 立即传递 Loader 的最新数据onLoadFinished。如果尚未创建 Loader(例如,当 Activity/Fragment 首次启动时),则调用initLoader告诉 LoaderManager 调用onCreateLoader以创建新的 Loader。

调用restartLoader会破坏已经存在的 Loader(以及与之关联的任何现有数据)并告诉 LoaderManager 调用onCreateLoader以创建新的 Loader 并启动新的加载。


文档对此也很清楚:

  • initLoader确保加载器已初始化并处于活动状态。如果加载器尚不存在,则创建一个并(如果活动/片段当前已启动)启动加载器。否则,最后创建的加载器将被重新使用。

  • restartLoader在这个管理器中启动一个新的或重新启动一个现有的加载器,向它注册回调,并且(如果活动/片段当前已启动)开始加载它。如果之前已经启动了具有相同 id 的加载器,它将在新加载器完成其工作时自动销毁。回调将在旧加载器被销毁之前传递。

于 2013-05-23T04:47:27.803 回答
16

我最近遇到了多个加载器管理器和屏​​幕方向更改的问题,我想说,经过大量的反复试验,以下模式在活动和片段中都适用于我:

onCreate: call initLoader(s)
          set a one-shot flag
onResume: call restartLoader (or later, as applicable) if the one-shot is not set.
          unset the one-shot in either case.

(换句话说,设置一些标志,以便initLoader始终运行一次,并且 restartLoader 在第二次运行,随后通过onResume

此外,请记住为 Activity 中的每个加载程序分配不同的 id(如果您不小心编号,这可能会导致该 Activity 中的片段出现问题)


我尝试只使用initLoader .... 似乎没有有效地工作。

onCreate上尝试使用 null args 的initLoader (文档说这没问题)和onResume中的restartLoader(带有有效 args)......docs 是错误的,并且 initLoader抛出了一个空指针异常。

仅尝试了restartLoader ...工作了一段时间,但在第5个或第6个屏幕重新定向时会爆炸。

onResume中尝试了initLoader;再次工作一段时间然后吹。(特别是“未启动时调用doRetain:”......错误)

尝试了以下方法:(摘自将加载程序 id 传递给构造函数的封面类)

/**
 * start or restart the loader (why bother with 2 separate functions ?) (now I know why)
 * 
 * @param manager
 * @param args
 * @deprecated use {@link #restart(LoaderManager, Bundle)} in onResume (as appropriate) and {@link #initialise(LoaderManager, Bundle)} in onCreate 
 */
@Deprecated 
public void start(LoaderManager manager, Bundle args) {
    if (manager.getLoader(this.id) == null) {
        manager.initLoader(this.id, args, this);
    } else {
        manager.restartLoader(this.id, args, this);
    }
}

(我在 Stack-Overflow 某处发现)

同样,这工作了一段时间,但仍然偶尔会出现故障。


从我在调试时可以得出的结论来看,我认为与保存/恢复实例状态有关,这需要initLoader (/s) 在生命周期的onCreate部分运行,如果它们要在循环中存活下来. (我可能错了。)

对于在结果从另一个管理器或任务返回之前无法启动的管理器(即无法在onCreate中初始化),我只使用initLoader。(我可能不正确,但它似乎有效。这些辅助加载器不是直接实例状态的一部分,因此在这种情况下使用initLoader实际上可能是正确的)

生命周期


查看图表和文档,我会认为 initLoader 应该在 onCreate & restartLoader 在 onRestart for Activities 中使用,但这会使 Fragments 使用一些不同的模式,我没有时间调查这是否真的稳定。其他人可以评论他们是否在这种活动模式上取得了成功?

于 2013-05-22T23:51:33.150 回答
0

initLoader如果加载器已经存在,将重用相同的参数。如果旧数据已经加载,它会立即返回,即使您使用新参数调用它也是如此。加载器应该理想地自动通知新数据的活动。如果屏幕旋转,initLoader将再次调用并立即显示旧数据。

restartLoader当您想要强制重新加载并更改参数时。如果您要使用加载程序制作登录屏幕,您只需restartLoader在每次单击按钮时调用。(由于凭据不正确等原因,可能会多次单击该按钮)。如果在登录过程中屏幕旋转,您只会initLoader在恢复活动的已保存实例状态时调用。

于 2014-01-21T14:21:13.910 回答
0

第一次启动时的初始化加载器使用 loadInBackground() 方法,在第二次启动时它将被省略。所以,我认为,更好的解决方案是:

Loader<?> loa; 
try {
    loa = getLoaderManager().getLoader(0);
} catch (Exception e) {
    loa = null;
}
if (loa == null) {
    getLoaderManager().initLoader(0, null, this);
} else {
    loa.forceLoad();
}

///////////////////////////////////////// ///////////////////////

protected SimpleCursorAdapter mAdapter;

private abstract class SimpleCursorAdapterLoader 
    extends AsyncTaskLoader <Cursor> {

    public SimpleCursorAdapterLoader(Context context) {
        super(context);
    }

    @Override
    protected void onStartLoading() {
        if (takeContentChanged() || mAdapter.isEmpty()) {
            forceLoad();
        }
    }

    @Override
    protected void onStopLoading() {
        cancelLoad();
    }

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

我花了很多时间来找到这个解决方案——restartLoader(...) 在我的情况下无法正常工作。唯一的 forceLoad() 允许在没有回调的情况下完成先前的加载线程(因此您将正确完成所有数据库事务)并再次启动新线程。是的,它需要一些额外的时间,但更稳定。只有最后启动的线程才会进行回调。因此,如果您想通过中断数据库事务进行测试——欢迎您,尝试重新启动Loader(...),否则 forceLoad()。restartLoader(...) 唯一方便的是传递新的初始数据,我的意思是参数。在这种情况下,请不要忘记在合适的 Fragment 的 onDetach() 方法中销毁 loader。还要记住,有时候,当你有活动时,让他们说,2 个带有 Loader 的 Fragment,每个包含一个 Activity——你只会到达 2 个 Loader Manager,因此 Activity 与 Fragment 共享它的 LoaderManager,在加载过程中首先显示在屏幕上。试试 LoaderManager.enableDebugLogging(true); 查看每个特定案例的详细信息。

于 2013-03-19T14:21:53.567 回答
-1

如果加载器已经存在,restartLoader 将停止/取消/销毁旧加载器,而 initLoader 将使用给定的回调初始化它。我不知道旧回调在这些情况下做了什么,但我想它们会被放弃。

我扫描了http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.0.1_r1/android/app/LoaderManager.java但我无法找出确切的不同的是,除了方法做不同的事情。所以我会说,第一次使用 initLoader 并在接下来的时间重新启动,尽管我不能肯定地说他们每个人会做什么。

于 2013-01-21T18:43:55.553 回答