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

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

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

Существует ли какая-то связь между этими двумя методами? Вызывает ли вызов restartLoader вызов initLoader? Могу ли я позвонить restartLoader без вызова initLoader? Сохраняется ли вызов initLoader дважды, чтобы обновить данные? Когда следует использовать один из двух и (важно!) Почему?

Ответ 1

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

initLoader

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

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

restartLoader

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

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

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

  • Загрузчик с идентификатором не существует: оба метода создадут новый загрузчик, чтобы там не было разницы.
  • Загрузчик с идентификатором уже существует: initLoader заменяет только обратные вызовы, переданные как параметры, но не отменяет или останавливает загрузчик. Для 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, но это было бы неэффективно. После поворота экрана или если пользователь переходит от приложения и возвращается позже к той же самой Деятельности, мы обычно хотим показать тот же результат запроса, и поэтому перезагружаемый загрузчик излишне повторного создания загрузчика и отклонения базового (потенциально дорогого) результата запроса.

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

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

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

Всегда ли вызов restartLoader всегда вызывает initLoader?

Нет, он никогда этого не делает.

Можно ли вызвать restartLoader без вызова initLoader?

Да.

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

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

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

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

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

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

Прежде всего, LoaderManager не сохраняет обратные вызовы, поэтому, если вы ничего не делаете, вы не будете получать вызовы к вашим методам обратного вызова, например onLoadFinished() и тому подобное, и это, скорее всего, сломает ваше приложение. Поэтому мы должны вызвать хотя бы initLoader для восстановления методов обратного вызова (возможно, также возможен перезапускLoader). В документации указано:

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

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

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

  • Обрабатывает сами изменения конфигурации: это относится к фрагментам которые используют setRetainInstance (true) или для Activity с в соответствии с android: configChanges тег в манифесте. Эти компоненты не получат вызов onCreate после, например, поворот экрана, поэтому имейте в виду позвонить initLoader/restartLoader в другом методе обратного вызова (например, в onActivityCreated (Bundle)). Чтобы иметь возможность инициализировать загрузчик (-ы), идентификаторы загрузчика необходимо сохранить (например, в списке). Потому как компонент сохраняется при изменении конфигурации, мы можем просто перейдите по существующим идентификаторам загрузчика и вызовите initLoader (loaderid, ...).
  • Не обрабатывает сами изменения конфигурации: в этом случае Погрузчики могут быть инициализированы в onCreate, но нам нужно вручную сохраните идентификаторы загрузчика или мы не сможем сделать необходимые вызовы initLoader/restartLoader. Если идентификаторы хранятся в ArrayList, мы будем делать outState.putIntegerArrayList(loaderIdsKey, loaderIdsArray) в onSaveInstanceState и восстановить идентификаторы в onCreate: loaderIdsArray = savedInstanceState.getIntegerArrayList(loaderIdsKey), прежде чем мы сделаем вызов initLoader.

Ответ 2

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

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


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

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

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

Ответ 3

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

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 выполнял всегда один раз, и этот перезапуск запускается на втором и последующих проходах через onResume)

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


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

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

Пробовал перезапускLoader только... работает некоторое время, но дует на 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)

Опять же, это сработало некоторое время, но все равно бросало случайный сбой.


Из того, что я могу понять во время отладки, я думаю, что есть что-то делать с состоянием экземпляра save/restore, которое требует, чтобы initLoader (/s) запускались в компоненте onCreate жизненного цикла, если они вынуждены пережить вращение цикла. (Возможно, я ошибаюсь.)

в случае менеджеров, которые не могут быть запущены до тех пор, пока результаты не вернутся из другого менеджера или задачи (т.е. не могут быть инициализированы в onCreate), я использую только initLoader. (Возможно, это неверно, но, похоже, это работает. Эти вторичные загрузчики не являются частью непосредственного состояния экземпляра, поэтому использование initLoader в этом случае может быть правильным)

lifecycle


Взглянув на диаграммы и документы, Я бы подумал, что initLoader должен зайти в onCreate и restartLoader в onRestart для действий, но это оставляет Фрагменты, используя какой-то другой шаблон, и у меня не было времени, чтобы исследовать, действительно ли это стабильно. Может ли кто-нибудь еще прокомментировать, если у них есть успех с этим шаблоном для действий?

Ответ 4

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

Я просмотрел http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.0.1_r1/android/app/LoaderManager.java, но я не могу понять, что такое точная разница, кроме того, что методы делают разные вещи. Поэтому я бы сказал, сначала используйте initLoader и перезагрузитесь в следующие моменты времени, хотя я не могу с уверенностью сказать, что каждый из них будет делать точно.

Ответ 5

Инициализатор загрузки при первом запуске использует метод 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); для просмотра деталей в каждом конкретном случае.

Ответ 6

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

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