Разница между initLoader и restartLoader в LoaderManager

129

Я полностью потерялся в различиях между функциями initLoaderи restartLoaderфункциями LoaderManager:

  • У них обоих одинаковые подписи.
  • restartLoader также создает загрузчик, если он не существует («Запускает новый или перезапускает существующий Загрузчик в этом диспетчере»).

Есть ли какая-то связь между двумя методами? Есть ли вызов restartLoaderвсегда позвонить initLoader? Могу ли я позвонить, restartLoaderне звоня initLoader? Безопасно ли initLoaderдважды звонить для обновления данных? Когда мне следует использовать один из двух и почему ?

theomega
источник

Ответы:

202

Чтобы ответить на этот вопрос, вам нужно покопаться в LoaderManagerкоде. Хотя документация для самого LoaderManager недостаточно ясна (иначе бы не было этого вопроса), документация для LoaderManagerImpl, подкласса абстрактного LoaderManager, гораздо более информативна.

initLoader

Вызов для инициализации конкретного идентификатора с помощью загрузчика. Если с этим идентификатором уже связан загрузчик, он остается неизменным, а все предыдущие обратные вызовы заменяются вновь предоставленными. Если в настоящее время загрузчика для идентификатора нет, создается и запускается новый.

Эту функцию обычно следует использовать при инициализации компонента, чтобы гарантировать создание загрузчика, на который он полагается. Это позволяет повторно использовать данные существующего загрузчика, если он уже есть, так что, например, когда действие создается повторно после изменения конфигурации, ему не нужно повторно создавать свои загрузчики.

restartLoader

Вызов для воссоздания загрузчика, связанного с определенным идентификатором. Если в настоящее время с этим идентификатором связан загрузчик, он будет отменен / остановлен / уничтожен соответствующим образом. Будет создан новый загрузчик с заданными аргументами, и его данные будут доставлены вам, когда они станут доступны.

[...] После вызова этой функции любые предыдущие загрузчики, связанные с этим идентификатором, будут считаться недействительными, и вы не будете получать от них дальнейшие обновления данных.

В основном есть два случая:

  1. Загрузчика с идентификатором не существует: оба метода создадут новый загрузчик, поэтому разницы нет.
  2. Загрузчик с идентификатором уже существует: initLoaderзаменит только обратные вызовы, переданные в качестве параметра, но не отменит или остановит загрузчик. Для a CursorLoaderэто означает, что курсор остается открытым и активным (если это было до initLoaderвызова). `restartLoader, с другой стороны, отменит, остановит и уничтожит загрузчик (и закроет базовый источник данных, например, курсор) и создаст новый загрузчик (который также создаст новый курсор и повторно запустит запрос, если загрузчик CursorLoader).

Вот упрощенный код для обоих методов:

initLoader

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используется для проверки наличия инициализированного загрузчика. Если такового не существует, создается новый, если он уже существует, он используется повторно. Мы всегда используем этот метод, ЕСЛИ нам не нужен новый загрузчик, потому что запрос для выполнения изменился (не базовые данные, а фактический запрос, как в операторе SQL для CursorLoader), и в этом случае мы будем вызывать restartLoader.

Деятельность / Фрагмент жизненного цикла не имеет ничего общего с решением использовать один или другой метод (и нет необходимости отслеживать звонки , используя флаг один выстрел , как предположил Симон)! Это решение принято исключительно исходя из «необходимости» нового погрузчика. Если мы хотим выполнить тот же запрос, который мы используем initLoader, если мы хотим выполнить другой запрос, который мы используем restartLoader.

Мы всегда могли использовать, restartLoaderно это было бы неэффективно. После поворота экрана или если пользователь уходит от приложения и позже возвращается к тому же самому Activity, мы обычно хотим показать тот же результат запроса, и поэтому restartLoaderбез необходимости повторно создаст загрузчик и отклонит базовый (потенциально дорогой) результат запроса.

Очень важно понимать разницу между загружаемыми данными и «запросом» для загрузки этих данных. Предположим, мы используем CursorLoader, запрашивающий у таблицы заказы. Если в эту таблицу добавляется новый порядок, CursorLoader использует onContentChanged (), чтобы информировать пользовательский интерфейс об обновлении и отображении нового порядка ( restartLoaderв этом случае нет необходимости ). Если мы хотим отображать только открытые ордера, нам нужен новый запрос, и мы должны использовать его restartLoaderдля возврата нового CursorLoader, отражающего новый запрос.


Есть ли какая-то связь между двумя методами?

Они совместно используют код для создания нового загрузчика, но они делают разные вещи, когда загрузчик уже существует.

Есть ли вызов restartLoaderвсегда позвонить initLoader?

Нет, никогда не бывает.

Могу ли я позвонить, restartLoaderне звоня initLoader?

Да.

Безопасно ли initLoaderдважды звонить для обновления данных?

initLoaderДважды позвонить безопасно, но данные не будут обновлены.

Когда мне следует использовать один из двух и почему ?


Это должно (надеюсь) быть ясным после моих объяснений выше.

Изменения конфигурации

LoaderManager сохраняет свое состояние при изменении конфигурации (включая изменение ориентации), поэтому вы можете подумать, что нам больше нечего делать. Подумай еще раз ...

Во-первых, LoaderManager не сохраняет обратные вызовы, поэтому, если вы ничего не сделаете, вы не получите вызовов к своим методам обратного вызова onLoadFinished()и т. Д., И это, скорее всего, сломает ваше приложение.

Поэтому мы ДОЛЖНЫ вызвать хотя бы initLoaderдля восстановления методы обратного вызова (это restartLoader, конечно, тоже возможно). В документации указано:

Если в момент вызова вызывающая сторона находится в своем запущенном состоянии, а запрошенный загрузчик уже существует и сгенерировал свои данные, то обратный вызов onLoadFinished(Loader, D)будет вызван немедленно (внутри этой функции) [...].

Это означает, что если мы позвоним initLoaderпосле изменения ориентации, мы сразу же получим onLoadFinishedвызов, потому что данные уже загружены (при условии, что это было до изменения). Хотя это звучит прямо, но может быть сложно (разве мы не все любим Android ...).

Мы должны различать два случая:

  1. Самостоятельно обрабатывает изменения конфигурации: это относится к фрагментам, использующим setRetainInstance (true), или к Activity с соответствующим android:configChangesтегом в манифесте. Эти компоненты не получат вызов onCreate, например, после поворота экрана, поэтому не забывайте вызывать initLoader/restartLoaderдругой метод обратного вызова (например, in onActivityCreated(Bundle)). Чтобы иметь возможность инициализировать загрузчик (-ы), идентификаторы загрузчика должны быть сохранены (например, в списке). Поскольку компонент сохраняется при изменении конфигурации, мы можем просто перебрать существующие идентификаторы загрузчика и вызвать initLoader(loaderid, ...).
  2. Сам не обрабатывает изменения конфигурации: в этом случае загрузчики могут быть инициализированы в onCreate, но нам нужно вручную сохранить идентификаторы загрузчиков, иначе мы не сможем сделать необходимые вызовы initLoader / restartLoader. Если идентификаторы хранятся в ArrayList, мы должны выполнить
    outState.putIntegerArrayList(loaderIdsKey, loaderIdsArray)onSaveInstanceState и восстановить идентификаторы в onCreate: loaderIdsArray = savedInstanceState.getIntegerArrayList(loaderIdsKey)перед тем, как сделать вызов (ы) initLoader.
Эмануэль Моеклин
источник
: +1: И последнее. Если вы используете initLoader(и все обратные вызовы завершены, загрузчик простаивает) после ротации, вы не получите onLoadFinishedобратный вызов, но если вы используете, restartLoaderвы получите?
Blundell
Неправильно. Метод initLoader вызывает метод onLoadFinished () перед возвратом (если загрузчик запущен и имеет данные). Я добавил параграф об изменениях конфигурации, чтобы объяснить это более подробно.
Emanuel Moecklin
6
ах, конечно, комбинация вашего ответа и @ alexlockwood дает полную картину. Я полагаю, что ответ для других: используйте initLoader, если ваш запрос статический, и перезапустите загрузчик, если вы хотите изменить запрос,
Бланделл,
1
Это красиво вызывает: «используйте initLoader, если ваш запрос статичен, и перезапуститеLoader, если вы хотите изменить запрос»
Эмануэль Моеклин
1
@Mhd. Tahawi, вы не меняете обратные вызовы, вы только устанавливаете их туда, куда они должны идти. После поворота экрана их необходимо повторно установить, потому что Android не будет их держать, чтобы предотвратить утечку памяти. Вы можете устанавливать для них все, что захотите, если они поступают правильно.
Emanuel Moecklin
46

Вызов, initLoaderкогда загрузчик уже был создан (обычно это происходит после изменения конфигурации, например), сообщает LoaderManager о необходимости немедленной доставки самых последних данных загрузчика onLoadFinished. Если загрузчик еще не был создан (например, при первом запуске действия / фрагмента), вызов to initLoaderсообщает LoaderManager о необходимости вызвать onCreateLoaderдля создания нового загрузчика.

Вызов restartLoaderуничтожает уже существующий загрузчик (а также любые существующие данные, связанные с ним) и сообщает LoaderManager вызвать onCreateLoaderдля создания нового загрузчика и инициировать новую загрузку.


В документации об этом тоже довольно ясно:

  • initLoaderобеспечивает инициализацию и активность загрузчика. Если загрузчик еще не существует, он создается и (если действие / фрагмент в данный момент запущен) запускает загрузчик. В противном случае повторно используется последний созданный загрузчик.

  • restartLoaderзапускает новый или перезапускает существующий загрузчик в этом диспетчере, регистрирует обратные вызовы для него и (если действие / фрагмент в настоящее время запущено) начинает его загрузку. Если ранее был запущен загрузчик с таким же идентификатором, он будет автоматически уничтожен, когда новый загрузчик завершит свою работу. Обратный вызов будет доставлен до того, как старый загрузчик будет уничтожен.

Алекс Локвуд
источник
@TomanMoney В своем ответе я объяснил, что это значит. Что вас смущает?
Alex Lockwood
вы только что перефразировали документ. Но в документе не указано, где следует использовать каждый метод и почему плохо его испортить. По моему опыту, просто вызов restartLoader и никогда не вызывающий initLoader работает нормально. Так что это все еще сбивает с толку.
Том anMoney
3
@TomanMoney Обычно вы используете initLoader()in onCreate()/ onActivityCreated()при первом запуске активности / фрагмента. Таким образом, когда пользователь впервые открывает действие, загрузчик будет создан в первый раз ... но при любых последующих изменениях конфигурации, когда все действие / фрагмент должны быть уничтожены, следующий вызов initLoader()просто вернет старый Loaderвместо создание нового. Обычно вы используете, restartLoader()когда вам нужно изменить Loaderзапрос (например, вы хотите получить отфильтрованные / отсортированные данные и т. Д.).
Alex Lockwood
4
Я все еще не уверен в решении API использовать оба метода, поскольку у них одинаковая сигнатура. Почему API не может быть единственным методом startLoader (), который каждый раз делает «правильные вещи»? Я думаю, что это сбивает с толку многих.
Tom anMoney
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 выполняется на 2 - й и последующих проходах через onResume )

Кроме того, не забудьте назначить разные идентификаторы для каждого из ваших загрузчиков в Activity (что может быть проблемой с фрагментами внутри этого действия, если вы не будете осторожны с нумерацией)


Я пробовал использовать только initLoader .... похоже, не работает эффективно.

Пробовал initLoader на onCreate с нулевыми аргументами (документы говорят, что это нормально) и restartLoader (с допустимыми аргументами) в onResume .... документы неверны, и initLoader выдает исключение нулевого указателя.

Пробовал только restartLoader ... какое-то время работает, но срабатывает при 5 или 6 переориентации экрана.

Пробовал initLoader в onResume ; снова работает какое-то время, а затем дует. (в частности, ошибка «Вызывается doRetain, когда не запускается:» ...)

Пробовал следующее: (отрывок из класса обложки, у которого идентификатор загрузчика передан в конструктор)

/**
 * 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 для действий, но это оставляет фрагменты с использованием другого шаблона, и у меня не было времени исследовать, действительно ли это стабильно. Может ли кто-нибудь еще прокомментировать, успешно ли он использует этот шаблон для занятий?

Саймон
источник
/ @ Саймон на 100% прав, и это должен быть принятый ответ. Я не совсем поверил его ответу и потратил несколько часов, пытаясь найти разные способы заставить эту работу работать. Как только я переместил вызов initLoader в onCreate, все заработало. Затем вам понадобится одноразовый флаг, чтобы учесть время вызова onStart, но не onCreate
CjS
2
«Пробовал только restartLoader ... работает какое-то время, но срабатывает при 5-м или 6-м переориентации экрана». Оно делает? Я просто попробовал, сотню раз повернул экран, и взрыва не получилось. Какого рода исключение вы получаете?
Tom anMoney
-1 Я ценю усилия по исследованию этого ответа, но большинство результатов неверны.
Эмануэль Моеклин
1
@IgorGanapolsky почти все. Если вы прочитаете и поймете мой ответ, вы поймете, что делают initLoader и restartLoader и когда их использовать, а также поймете, почему почти все выводы Саймона неверны. Нет связи между жизненным циклом фрагмента / действия и решением, когда использовать initLoader / restartLoader (с оговоркой, которую я объясняю в разделе изменения конфигурации). На основе проб и ошибок Саймон делает вывод, что жизненный цикл - ключ к пониманию двух методов, но это не так.
Эмануэль Моеклин
@IgorGanapolsky Я не пытаюсь афишировать свой ответ. Я просто пытаюсь помочь другим разработчикам и помешать им использовать результаты Саймона в своих собственных приложениях. Как только вы поймете, для чего предназначены эти два метода, все станет довольно очевидным и простым для реализации.
Эмануэль Моеклин
0

initLoaderбудет повторно использовать те же параметры, если загрузчик уже существует. Он немедленно возвращается, если старые данные уже загружены, даже если вы вызываете его с новыми параметрами. В идеале загрузчик должен автоматически уведомлять об активности новых данных. Если бы экран повернулся, initLoaderбудет вызываться снова, и сразу же отобразятся старые данные.

restartLoaderпредназначен для случаев, когда вы хотите принудительно перезагрузить и изменить параметры. Если бы вы создавали экран входа в систему с помощью загрузчиков, вы бы вызывали только restartLoaderкаждый раз, когда нажимали кнопку. (Кнопка может быть нажата несколько раз из-за неправильных учетных данных и т. Д.). Вы будете звонить только initLoaderпри восстановлении сохраненного состояния экземпляра действия в случае поворота экрана во время входа в систему.

Monstieur
источник
-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 в первый раз и перезапустите его в следующие раз, хотя я не могу с уверенностью сказать, что именно каждый из них будет делать.

koljaTM
источник
И что initLoaderв этом случае делать?
theomega
-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 () позволяет завершить предыдущий поток загрузки без обратного вызова (так что все транзакции db будут завершены правильно) и снова запускает новый поток. Да, требует дополнительного времени, но работает более стабильно. Обратный вызов примет только последний запущенный поток. Таким образом, если вы хотите провести тесты с прерыванием ваших транзакций db - добро пожаловать, попробуйте перезапуститьLoader (...), иначе forceLoad (). Единственное удобство restartLoader (...) - это доставка новых исходных данных, я имею в виду параметры. И не забудьте в этом случае уничтожить загрузчик в методе onDetach () подходящего фрагмента. Также имейте в виду, что иногда, когда вы занимаетесь чем-то, и пусть говорят, 2 фрагмента с загрузчиком, каждый из которых включает действие - вы получите доступ только к 2 диспетчерам загрузчиков, поэтому Activity разделяет свой LoaderManager с фрагментом (ами), который сначала отображается на экране во время загрузки. Попробуйте LoaderManager.enableDebugLogging (true); чтобы увидеть подробности в каждом конкретном случае.

user1700099
источник
2
-1 для обертывания вызова getLoader(0)в try { ... } catch (Exception e) { ... }.
Alex Lockwood