Узнайте, как работает JobSchedulers внутри. В этой статье мы рассмотрим внутреннюю реализацию планировщиков заданий Android. как они могут творить всю магию и даже больше? Если это те вопросы, которые вы задаете себе, поздравляем, ваши поиски завершены сегодня! Но будьте готовы, это будет долгая поездка.

Итак, прежде чем мы начнем, я хотел бы отметить, что эта статья будет полезна тем, кто хотя бы раз использовал JobSchedulers и знаком с предлагаемыми API.

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



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

Пока я объясняю, я также опубликую код из фреймворка с ограниченными деталями для облегчения понимания. Без лишних слов приступим.

Ключевые компоненты JobScheduler

  1. JobSchedulerService
  2. JobStatus
  3. JobStore и JobSet
  4. Controllers и StatusChangedListeners
  5. JobServiceContext
  6. JobConcurrencyManager
  7. Работа Restriction

JobSchedulerService

Это фасад, с которым взаимодействует API JobScheduler, когда вы вызываете getSystemService(Context.JobSchedulerService).

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

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

Рабочий статус

Это внутреннее представление вашей работы. Как пользователь, мы создаем объект JobInfo, который содержит важную информацию о нашей работе, такую ​​как компонент, который необходимо запустить (JobService реализация), ограничения для работы, сведения о периодичности и т. д.

В тот момент, когда мы отправляем JobInfo в фреймворк с помощью schedule()API, вся информация обрабатывается, создается новый объект JobStatus с более подробной информацией о задании, и все внутренние классы совместно используют этот класс друг с другом. Они не знают об информации о работе.

JobStatus имеет 2 внутренних списка, один из которых называется ожидающим списком, а другой — исполняемым списком.

Мы знаем, что можем добавить несколько JobWorkItems к информации о задании, используя enqueue(), и извлечь рабочий элемент задания в службе заданий, используя param.dequeue(). Таким образом, говоря простым языком, всякий раз, когда работа добавляется с помощью enqueue(), она добавляется в список ожидающих выполнения, а всякий раз, когда она удаляется из очереди, она удаляется из списка ожидающих выполнения и добавляется в список выполнения. Как только работа помечается как завершенная с помощью JobParameter.completeWork(JobWorkItem) API, элемент также удаляется из списка выполнения.

Поставить в очередь

public void enqueueWorkLocked(IActivityManager am, JobWorkItem work) {
    if (pendingWork == null) {
        pendingWork = new ArrayList<>();
    }
    work.setWorkId(nextPendingWorkId);
    nextPendingWorkId++;
    // .. some framework code stripped.
    pendingWork.add(work);
}

Удалить из очереди

public JobWorkItem dequeueWorkLocked() {
    if (pendingWork != null && pendingWork.size() > 0) {
        JobWorkItem work = pendingWork.remove(0);
        if (work != null) {
            if (executingWork == null) {
                executingWork = new ArrayList<>();
            }
            executingWork.add(work);
        }
        return work;
    }
    return null;
}

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

Все выполняемые работы старого задания переносятся в ожидающее задание входящего задания, а ожидающие работы копируются как есть. См. код ниже:

public void stopTrackingJobLocked(IActivityManager am, JobStatus incomingJob) {
    if (incomingJob != null) {
        // We are replacing with a new job -- transfer the work!  We do any executing
        // work first, since that was originally at the front of the pending work.
        if (executingWork != null && executingWork.size() > 0) {
            incomingJob.pendingWork = executingWork;
        }
        if (incomingJob.pendingWork == null) {
            incomingJob.pendingWork = pendingWork;
        } else if (pendingWork != null && pendingWork.size() > 0) {
            incomingJob.pendingWork.addAll(pendingWork);
        }
        pendingWork = null;
        executingWork = null;
        incomingJob.nextPendingWorkId = nextPendingWorkId;
        incomingJob.updateEstimatedNetworkBytesLocked();
    } else {
        // We are completely stopping the job...  need to clean up work.
       
        pendingWork = null;
        executingWork = null;
    }
   
}

Просто не правда ли.

Попробуем разобраться со следующим набором ответственности этого класса. Каждое задание имеет определенный набор ограничений, при которых оно должно выполняться, например, запускать только при подключенной батарее, запускать только при активной сети, не запускать, если сеть не соответствует заданным критериям, или запускать только при неиспользованном хранилище. не низкий и многое другое. Таким образом, всякий раз, когда изменяется состояние системы, этот объект обновляется текущим состоянием соответствующего ограничения. Мы поговорим о том, как он будет обновляться в будущих компонентах.

Он использует целое число с битовой маской для отслеживания состояния ограничений. Каждое ограничение представлено с помощью бита в этом целочисленном значении: всякий раз, когда состояние изменяется, большое значение добавляется и соответственно очищается. И объект предоставляет методы получения/установки для опроса текущего состояния отдельных ограничений. И способ проверки общего состояния задания, т.е. isReady().

С точки зрения непрофессионала, у него есть целое число, представляющее ограничения, необходимые для запуска задания (из предоставленной нами JobInfo), и целое число, представляющее текущий статус ограничений. Если 2 равны, то isReady() возвращает true.

private boolean isConstraintsSatisfied(int satisfiedConstraints) {
    if (overrideState == OVERRIDE_FULL) {
        // force override: the job is always runnable
        return true;
    }

    int sat = satisfiedConstraints;
    if (overrideState == OVERRIDE_SOFT) {
        // override: pretend all 'soft' requirements are satisfied
        sat |= (requiredConstraints & SOFT_OVERRIDE_CONSTRAINTS);
    }

    return (sat & mRequiredConstraintsOfInterest) ==   mRequiredConstraintsOfInterest;
}

Как только вы посмотрите на класс, вы увидите, что этот класс применяет ограничения периодичности к вашим заданиям, т.е. максимальная периодичность не может превышать 1 год, а минимальная не может быть менее 15 минут.

Магазин заданий и набор заданий

Этот компонент поддерживает список всех ваших заданий, запущенных в вашей системе (да, в вашей системе). Он хранится в виде XML-файла jobs.xml в каталоге /system/jobs внутри каталога системных данных. Всякий раз, когда задание создается, оно записывается внутри этого класса, и когда задание удаляется, оно должно быть удалено из этого класса. Он предоставляет простые API, такие как add(JobStatus), remove(JobStatus), contains(JobStatus), removeAllForUid(userID) и т. д.

Когда система загружается, этот компонент создается, и все задания, записанные в XML, считываются и преобразуются в JobStatus объекты, а JobSchedulerService запускает команду запуска для запуска любых применимых заданий.

Если мы посмотрим на его внутренности, то увидим, что задания хранятся и группируются по идентификаторам пользователей. Это сделано потому, что если пользователь удаляется, он может опросить все задания и отменить их. Аналогичные вещи можно сделать, если он должен запускать задания для пользователя. Итак, данные выглядят как Map<UserID, Set<JobStatus> >.

private JobStore(Context context, Object lock, File dataDir) {
    mLock = lock;
    mWriteScheduleLock = new Object();
    mContext = context;

    File systemDir = new File(dataDir, "system");
    File jobDir = new File(systemDir, "job");
    jobDir.mkdirs();
    mJobsFile = new AtomicFile(new File(jobDir, "jobs.xml"), "jobs");

    mJobSet = new JobSet();
    readJobMapFromDisk(mJobSet, mRtcGood);
}

Итак, это память вашего планировщика заданий. Вот как он запоминает ваши задания, и они переустанавливаются, если устройство перезагружается несколько раз.

Контроллеры и StateChangedListeners

Особая возможность JS заключается в том, что он может запускать задания и вытеснять их по мере выполнения/несоблюдения ограничений. Контроллеры — это компоненты, которые включают эту функцию.

Контроллеры — это органы чувств инфраструктуры JobScheduler, которые отслеживают состояние системы в реальном времени.

В идеале для каждого ограничения, которое вы указали в своем JobInfo, есть по крайней мере один сопоставленный с ним контроллер. Некоторые из них BatteryController, ChargingController, ConnectivityController, IdleController, QuotaController и многие другие.

Каждый контроллер придерживается контракта контроллера, который имеет 2 важных метода: maybeStartTracking(JobStatus), maybeStopTracking(JobStatus).

Способ, которым эти API помогают, заключается в том, что всякий раз, когда задание запланировано, JobSchedulerService имеет список всех контроллеров (List<Controllers>), оно запускается в цикле for и запускает maybeStartTracking(). И внутри каждого контроллера есть проверка, которая проверяет, есть ли у этого задания ограничение, которое контроллер может помочь прослушивать. Если да, то задание добавляется в список отслеживания статуса задания или игнорируется.

@Override
public void maybeStartTrackingJobLocked(JobStatus taskStatus, JobStatus lastJob) {
    if (taskStatus.hasPowerConstraint()) { // use job status API 
        mTrackedTasks.add(taskStatus);
        taskStatus.setTrackingController(JobStatus.TRACKING_BATTERY);
        taskStatus.setChargingConstraintSatisfied(mChargeTracker.isOnStablePower());
        taskStatus.setBatteryNotLowConstraintSatisfied(mChargeTracker.isBatteryNotLow());
    }
}

Теперь вопрос, как он прослушивает состояние системы?

Большинство разработчиков Android знают, что такие варианты использования могут быть реализованы с помощью широковещательных приемников. Так что и здесь ваша догадка верна. Каждый контроллер построен поверх широковещательного приемника. Таким образом, он обеспечивает поддержку как по запросу, так и по запросу.

Вытягивание требуется, потому что служба планировщика заданий иногда хочет знать, выполняется ли конкретное ограничение перед запуском задания (обычно, когда задание отправлено и его необходимо запустить немедленно). API на основе push требуется для обновления объектов jobStatus для состояния удовлетворенных ограничений в реальном времени.

Коммуникация на основе push происходит через контракт под названием State Change Listener. JobSchedulerService реализует этот интерфейс, и когда каждый контроллер изменяет состояние, он уведомляет службу JS, используя следующие методы обратного вызова:

public interface StateChangedListener {
    /**
     * Called by the controller to notify the JobManager that it should check on the state of a
     * task.
     */
    public void onControllerStateChanged();

    /**
     * Called by the controller to notify the JobManager that regardless of the state of the task,
     * it must be run immediately.
     * @param jobStatus The state of the task which is to be run immediately. <strong>null
     *                  indicates to the scheduler that any ready jobs should be flushed.</strong>
     */
    public void onRunJobNow(JobStatus jobStatus);

    public void onDeviceIdleStateChanged(boolean deviceIdle);
}

Ниже представлен общий поток данных:

JobServiceContext

Платформа ограничивает максимальное количество заданий, которые могут выполняться вместе, до 16. И это ограничение не для всего приложения, а для всей системы для разных пользователей.

/** The maximum number of concurrent jobs we run at one time. */
static final int MAX_JOB_CONTEXTS_COUNT = 16;

/** The maximum number of jobs that we allow an unprivileged app to schedule */
private static final int MAX_JOBS_PER_APP = 100;

Для запуска 16 заданий фреймворк создает 16 оболочек, внутри которых выполняются задания. Эти снаряды называются JobServiceContext. Они инициализируются при загрузке системы и вызываются onBootPhase(int phase) с фазой == THIRD_PARTY_APPS_CAN_START. Ниже приведен урезанный стартовый код:

else if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) {
    synchronized (mLock) {
        // Let's go!
        mReadyToRock = true;
       
        // Create the "runners".
        for (int i = 0; i < MAX_JOB_CONTEXTS_COUNT; i++) {
            mActiveServices.add(
                    new JobServiceContext(this, mBatteryStats, mJobPackageTracker,
                            getContext().getMainLooper()));
        }
        // Attach jobs to their controllers.
        mJobs.forEachJob((job) -> {
            for (int controller = 0; controller < mControllers.size(); controller++) {
                final StateController sc = mControllers.get(controller);
                sc.maybeStartTrackingJobLocked(job, null);
            }
        });
        // GO GO GO! Fire the jobs if they are ready
        mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget();
    }

Все эти 16 оболочек являются повторно используемыми оболочками, что означает, что когда задание вытесняется из него, его можно использовать для запуска другого задания. Итак, теперь возникает вопрос, JobService приложения запускается в процессе приложения, а JobServiceContext запускается в системном процессе — но как они могут работать вместе?

Как вы уже догадались, на сцену выходит AIDL.

JobServiceContext — это хост, на котором размещаются процессы, а взаимодействие осуществляется через интерфейсы AIDL. Взаимодействие двустороннее. Внутри JobServiceContext есть ссылка на интерфейс IJobService, которая указывает на JobService пользователя, и когда обратный вызов передается JobService, например onStartJob(JobParameters), onStopJob(JobParameters),этот JobParameter содержит нечто, называемое JobCallback это связующая реализация небольшого набора методов, которые можно вызывать из процесса приложения. Это выглядит следующим образом:

final class JobCallback extends IJobCallback.Stub {
    public String mStoppedReason;
    public long mStoppedTime;

    @Override
    public void acknowledgeStartMessage(int jobId, boolean ongoing) {
        doAcknowledgeStartMessage(this, jobId, ongoing);
    }

    @Override
    public void acknowledgeStopMessage(int jobId, boolean reschedule) {
        doAcknowledgeStopMessage(this, jobId, reschedule);
    }

    @Override
    public JobWorkItem dequeueWork(int jobId) {
        return doDequeueWork(this, jobId);
    }

    @Override
    public boolean completeWork(int jobId, int workId) {
        return doCompleteWork(this, jobId, workId);
    }

    @Override
    public void jobFinished(int jobId, boolean reschedule) {
        doJobFinished(this, jobId, reschedule);
    }
}

Методы обратного вызова задания те же, что и при вызове из JobService. Вы можете использовать только completeWork и jobFinished, но если мы увидим реализацию класса JobService.java, мы также увидим использование всех этих методов.

Теперь мы знаем, что существуют механизмы для связи в обоих направлениях, IJobService связующая реализация для JobServiceContext -> User JobService и JobCallback внутри JobParameters для наоборот.

На приведенной ниже диаграмме показано то, что мы обсуждали ранее:

Следует помнить, что JobServiceContext является реализацией ServiceConnection, поэтому, когда контекст связывается с вашей службой заданий, он получает обратный вызов onServiceConnected(), а внутри него получает связыватель AIDL, указывающий на вашу службу заданий, и именно здесь находится экземпляр JobParameter. также создается. Давайте посмотрим на урезанный код:

boolean executeRunnableJob(JobStatus job) {
    synchronized (mLock) {
       
        mRunningJob = job;
        mRunningCallback = new JobCallback();
 
        mParams = new JobParameters(mRunningCallback, job.getJobId(), ji.getExtras(),
                ji.getTransientExtras(), ji.getClipData(), ji.getClipGrantFlags(),
                isDeadlineExpired, triggeredUris, triggeredAuthorities, job.network);


        final Intent intent = new Intent().setComponent(job.getServiceComponent());
        boolean binding = false;
        try {
            binding = mContext.bindServiceAsUser(intent, this,
                    Context.BIND_AUTO_CREATE | Context.BIND_NOT_FOREGROUND
                    | Context.BIND_NOT_PERCEPTIBLE,
                    new UserHandle(job.getUserId()));
        } catch (SecurityException e) {
         //. some code
        }
        return true;
    }
}

Здесь мы видим запуск привязки задания к службе, теперь, когда ваша служба создается, этот класс получает onServiceConnected(), это место, где привязка к вашей службе заданий приходит к этому классу и запускает IJobService.startJob(params).

JobServiceContext отвечает за все виды связи с/от пользователя JobService. Любой компонент, которому необходимо запускать обратные вызовы внутри вашей службы, должен запросить этот компонент.

JobConcurrencyManager

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

Таким образом, он предоставляет API assignJobsToContextsLocked, который используется JobSchedulerService. Служба JS никогда не запускает задание напрямую, так как никогда не узнает, какие задания уже запущены, и также необходимо выполнить много вытеснения. Таким образом, вы всегда увидите, что он всегда будет вызывать assignJobsToContextsLocked.

Я постараюсь объяснить это очень простыми словами.

Этот компонент отслеживает все 16 запущенных заданий и, если приходит новое задание, сканирует все слоты и проверяет, доступен ли слот. Если он доступен, он назначает задание слоту и запускает сервис, используя JobServiceContext, который мы только что исследовали.

Если слоты недоступны, он будет проходить через все слоты и смотреть, не ниже ли приоритет запущенной службы, чем приоритет службы, запуск которой запрашивается, если приоритет запущенной службы низкий, то запрашивается onStopJob() на этом и он вытесняется, и новый сервис назначается этому слоту. Если все службы важны, запрошенная служба ожидает, пока слот не станет пустым.

В целом поток выглядит следующим образом:

JobSchedulerService (продолжение, как и было обещано)

Мы знаем, что служба JS до сих пор является главным оркестратором, здесь мы увидим, что происходит, когда задание запланировано с использованием schedule(JobInfo) и enqueue(JobInfo, JobWorkItem). Оба вызова перенаправляются на внутренний метод,

public int scheduleAsPackage(JobInfo job, JobWorkItem work, int uId, String packageName, int userId, String tag)

Что происходит внутри него?

Лучше всего это будет продемонстрировано с помощью блок-схемы:

  1. Когда приходит задание, система видит, есть ли уже запланированное там задание с таким же идентификатором, если нет, то создается объект JobStatus. Если задание с этим идентификатором уже существует, то оно проверяет, является ли задание таким же или нет (изменился ли JobInfo?). Если это одно и то же задание, то он ставит входящую работу в очередь на существующее задание, в противном случае он запрашивает отмену существующего задания и перенос всей работы на новое задание.

2. После создания JobStatus в случае его отсутствия или получения существующего статуса задания для запланированного задания этот класс запускает запрос на отслеживание с использованием maybeStartTracking(JobStatus) API, который мы обсуждали, и соответствующие контроллеры начинают отслеживать задание.

3. Теперь он вызывает isReady(), чтобы узнать, можно ли его запустить прямо сейчас. Если NO, то он сохраняется в списке ожидающих выполнения (он запускается, когда ограничения удовлетворяются, о чем сообщает контроллер). Если isReady() возвращает YES, он помещает запрос к JobConcurrencyManager, используя assignJobsToContextsLocked().

4. JobConcurrencyManager находит свободный слот и запрашивает JobServiceContext запуск JobService приложения с помощью executeRunnableJob API. Он связывает службу, а затем вызывается onServiceConnected(). Оттуда запускается обратный вызов onStartJob с параметрами задания.

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