LoaderManager中的initLoader和restartLoader之间的区别


129

我对initLoaderrestartLoader功能之间的差异完全迷失了LoaderManager

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

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

Answers:


202

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

初始化加载器

调用以使用Loader初始化特定的ID。如果此ID已经有一个与之关联的Loader,则将其保持不变,并且所有以前的回调都将被新提供的回调代替。如果当前没有用于该ID的加载程序,则会创建并启动一个新的ID。

通常,在初始化组件时应使用此功能,以确保创建了依赖它的Loader。这样,如果已有的装载程序数据已经存在,它便可以重新使用它,例如,当在更改配置后重新创建活动时,不需要重新创建其装载程序。

restartLoader

调用以重新创建与特定ID关联的Loader。如果当前有一个与此ID关联的装载程序,则将视情况取消/停止/销毁它。将创建一个具有给定参数的新Loader,并将其数据传递给您。

[...]调用此函数后,与该ID关联的所有以前的装载程序都将被视为无效,并且您将不会再从中收到任何数据更新。

基本上有两种情况:

  1. 带有ID的加载程序不存在:这两种方法都会创建一个新的加载程序,因此两者之间没有区别
  2. 具有ID的加载程序已经存在:initLoader将仅替换作为参数传递的回调,而不会取消或停止加载程序。对于a CursorLoader表示光标保持打开状态并处于活动状态(如果在initLoader调用之前是这种情况)。另一方面,`restartLoader将取消,停止和销毁加载器(并像游标一样关闭底层数据源)并创建新的加载器(如果加载器为一个CursorLoader)。

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

初始化加载器

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;
}

restartLoader

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

活动/片段生命周期无关的决定使用一种或另一种方法(和有没有必要使用一次性标志来跟踪调用的西蒙建议)!该决定仅基于对新装载机的“需求”来做出。如果我们要运行使用的相同查询initLoader,如果我们要运行不同的查询,则使用restartLoader

我们可以一直使用,restartLoader但是那样效率很低。屏幕旋转后,或者如果用户离开应用程序并稍后返回相同的Activity,我们通常希望显示相同的查询结果,因此这restartLoader将不必要地重新创建加载程序并取消基础(可能昂贵)的查询结果。

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


两种方法之间有关系吗?

他们共享代码来创建新的Loader,但是当已有Loader时,它们会做不同的事情。

打电话restartLoader总是打initLoader吗?

不,永远不会。

我可以打电话restartLoader而不必打电话initLoader吗?

是。

拨打initLoader两次刷新数据是否安全?

可以安全拨打initLoader两次,但不会刷新任何数据。

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


经过我上面的解释,这(希望)应该清楚。

配置变更

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

首先,LoaderManager不会保留回调,因此,如果您不执行任何操作,将不会收到对诸如like onLoadFinished()之类的回调方法的调用,这很可能会破坏您的应用程序。

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

如果在调用时调用者处于启动状态,并且所请求的加载程序已经存在并已生成其数据,则将onLoadFinished(Loader, D)立即(在此函数内部)调用回调。[...]

这意味着,如果我们initLoader在方向更改后致电,我们将立即onLoadFinished接到电话,因为数据已经加载(假设更改之前就是这种情况)。虽然听起来很简单,但可能会有些棘手(我们都不要喜欢Android吗?)。

我们必须区分两种情况:

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

:+1:最后一点。如果您initLoader在轮换后使用(并且所有回调均已完成,Loader处于空闲状态),您将不会获得onLoadFinished回调,但是如果使用,restartLoader您将获得回调吗?
Blundell 2014年

不正确 initLoader方法在返回之前会调用onLoadFinished()方法(如果加载程序已启动且具有数据)。我添加了一段有关配置更改的内容,以更详细地说明这一点。
伊曼纽尔·莫克林

6
啊,当然,您的答案与@alexlockwood的结合可以提供完整的图像。我想其他人的答案是,如果您的查询是静态的,请使用initLoader;如果您想更改查询,请使用restartLoader
Blundell

1
很好地说明了这一点:“如果您的查询是静态的,请使用initLoader,如果您要更改查询,请
重新启动Loader

1
@Mhd。塔哈维(Tahawi)您无需更改回调,只需将它们设置为应到达的位置。屏幕旋转后需要重新设置它们,因为Android不会保留它们以防止内存泄漏。只要他们做正确的事,您就可以随意将它们设置为您想要的任何东西。
伊曼纽尔·莫克林

46

initLoader在已经创建Loader时调用(例如,通常在配置更改后发生),它告诉LoaderManager立即将Loader的最新数据传送到onLoadFinished。如果尚未创建Loader(例如,在首次启动活动/片段时),则的调用会initLoader告诉LoaderManager调用onCreateLoader以创建新的Loader。

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


文档对此也很清楚:

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

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


@TomanMoney我解释了答案中的含义。您对哪一部分感到困惑?
Alex Lockwood

您只是重新整理了文档。但是该文档没有指出应在何处使用每种方法以及为什么将其弄乱是不好的。以我的经验,仅调用restartLoader而从不调用initLoader效果很好。因此,这仍然令人困惑。
Tom anMoney

3
@TomanMoney通常在活动/片段首次启动时initLoader()onCreate()/中使用onActivityCreated()。这样,当用户首次打开活动时,将首次创建加载程序...但是在随后的配置更改中,必须销毁整个活动/片段,以下调用initLoader()将仅返回旧的Loader而不是创建一个新的。通常restartLoader()在需要更改Loader查询时使用(例如,您要获取过滤/排序的数据等)。
Alex Lockwood

4
我仍然对API决定同时使用这两种方法感到困惑,因为它们具有相同的签名。为什么API不能是每次都会执行“正确的事情”的单个startLoader()方法?我认为这是使很多人感到困惑的部分。
Tom anMoney 2013年

1
@TomanMoney这里的文档说:developer.android.com/guide/components/loaders.html。“在更改配置后重新创建时,它们会自动重新连接到最后一个加载器的光标。因此,它们不需要重新查询其数据。”
IgorGanapolsky

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

另外,请记住为一个活动中的每个加载程序分配不同的ID(如果您不小心编号,则该活动中的片段可能会有点问题)


我尝试仅使用initLoader ....似乎无法有效工作。

试图initLoader的onCreate与空ARGS(文档说这是确定)restartLoader(标有有效参数)的onResume ....文档是错误的&initLoader抛出一个空指针异常。

仅尝试过restartLoader ...可以工作一段时间,但在第5或第6屏幕重新定向时会失败

onResume中尝试过initLoader ; 再次工作一会儿再吹。(特别是“未启动时称为doRetain:” ...错误)

尝试了以下操作:(摘自具有loader ID传递给构造函数的Cover类)

/**
 * 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应该进入onRestart for Activities中的onCreate&restartLoader中,但是这会使Fragment使用某些不同的模式,而且我还没有时间研究这种情况是否稳定。任何人都可以评论这种模式是否成功吗?


/ @ Simon是100%正确的,这应该是可接受的答案。我不太相信他的回答,并花了几个小时试图找到不同的方法来完成这项工作。我将initLoader调用移至onCreate之后,一切就开始起作用。然后,您需要使用一键式标记来说明调用onStart的时间,但不考虑onCreate的时间
CjS 2013年

2
“尝试过的restartLoader ...可以工作一段时间,但是在第5或第6屏幕重新定向时会失败。” 是吗 我只是尝试了一下,然后将屏幕旋转了一百次,没有炸毁。你会得到什么样的例外?
Tom anMoney 2013年

-1我很欣赏此答案背后的研究工作,但大多数结果都是错误的。
伊曼纽尔·莫克林

1
@IgorGanapolsky几乎所有内容。如果您阅读并理解了我的答案,您将了解initLoader和restartLoader的功能以及何时使用它们,并且您还将了解为什么几乎所有Simon的结论都是错误的。片段/活动的生命周期与何时使用initLoader / restartLoader的决定之间没有任何联系(需要说明的是我在配置更改下说明)。Simon从反复试验中得出结论,生命周期是理解这两种方法的线索,但事实并非如此。
伊曼纽尔·莫克林

@IgorGanapolsky我不是要宣传自己的答案。我只是在尝试帮助其他开发人员,并阻止他们将Simon的结果用于自己的应用程序。一旦了解了这两种方法的含义,整个过程就会变得很明显并且很容易实现。
伊曼纽尔·莫克林

0

initLoader如果加载程序已经存在,将重用相同的参数。如果已经加载了旧数据,即使使用新参数调用它,它也会立即返回。理想情况下,加载程序应自动通知新数据活动。如果屏幕旋转,initLoader将再次被调用,并且旧数据将立即显示。

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


-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并在随后的几次重新启动,尽管我不能确定地说它们中的每一个将做什么。


initLoader在这种情况下该怎么办?
theomega 2013年

-1

首次启动时的初始化加载程序使用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的片段-您将仅到达2个Loader Manager,因此Activity与片段共享其LoaderManager,片段在加载过程中首先显示在屏幕上。尝试LoaderManager.enableDebugLogging(true); 查看每种情况下的详细信息。


2
-1,用于将呼叫包装到getLoader(0)try { ... } catch (Exception e) { ... }
Alex Lockwood
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.