Evolution du planificateur de tâches



L'application iFunny sur laquelle nous travaillons est disponible en magasin depuis plus de cinq ans. Pendant ce temps, l'équipe mobile a dû passer par de nombreuses approches et migrations différentes entre les outils, et il y a un an, il était temps de passer d'une solution auto-écrite et de regarder vers quelque chose de plus «à la mode» et répandu. Cet article est un petit aperçu de ce qui a été étudié, des solutions qui ont été envisagées et de ce qu'elles ont abouti.

Pourquoi avons-nous besoin de tout cela?

Décidons immédiatement en l'honneur de cet article et pourquoi ce sujet s'est avéré important pour l'équipe de développement Android:

  1. Il existe de nombreux scénarios lorsque vous devez exécuter des tâches en dehors du cadre de l'interface utilisateur active;
  2. le système impose un grand nombre de restrictions au lancement de telles tâches;
  3. Il s'est avéré assez difficile de choisir entre les solutions existantes, car chaque outil a ses avantages et ses inconvénients.

Chronologie du développement des événements


Android 0

AlarmManager, gestionnaire, service


Initialement, leurs solutions ont été mises en œuvre pour lancer des tâches en arrière-plan basées sur des services. Il existe également un mécanisme qui relie les tâches au cycle de vie et peut les annuler et les restaurer. Cela convenait à l'équipe depuis longtemps, car la plate-forme n'imposait aucune restriction à ces tâches.
Google a conseillé de le faire sur la base du diagramme suivant:



Fin 2018, inutile de comprendre cela, il suffit d'évaluer l'ampleur de la catastrophe.
En fait, personne ne se souciait du volume de travail en arrière-plan. Les candidatures ont fait ce qu'elles voulaient et quand elles le voulaient.

Avantages :
disponible partout;
accessible à tous.

Inconvénients :
le système restreint le travail dans tous les sens;
aucun lancement par condition;
L'API est minimale et vous devez écrire beaucoup de code.

Android 5. Lollipop

Jobcheduler


Après 5 (!) Ans, plus près de 2015, Google a remarqué que les tâches étaient lancées de manière inefficace. Les utilisateurs ont commencé à se plaindre régulièrement que leurs téléphones étaient bas simplement en se couchant sur une table ou dans leur poche.

Avec la sortie d'Android 5, un outil comme JobScheduler est apparu. Il s'agit d'un mécanisme avec l'aide duquel il est possible d'effectuer divers travaux en arrière-plan, dont le début a été optimisé et simplifié en raison du système centralisé de lancement de ces tâches et de la possibilité de fixer les conditions de ce lancement même.

Dans le code, tout cela semble assez simple: un service est annoncé dans lequel viennent les événements de début et de fin.
À partir des nuances: si vous souhaitez effectuer un travail de manière asynchrone, à partir de onStartJob, vous devez démarrer le flux; L'essentiel est de ne pas oublier d'appeler la méthode jobFinished à la fin du travail, sinon le système ne lâchera pas WakeLock, votre tâche ne sera pas considérée comme terminée et sera perdue.

public class JobSchedulerService extends JobService { @Override public boolean onStartJob(JobParameters params) { doWork(params); return false; } @Override public boolean onStopJob(JobParameters params) { return false; } } 

De n'importe où dans l'application, vous pouvez lancer ce travail. Les tâches sont effectuées dans notre processus, mais sont lancées au niveau IPC. Il existe un mécanisme centralisé qui contrôle leur exécution et ne réveille l'application qu'aux moments nécessaires. Vous pouvez également définir diverses conditions de déclenchement et transférer des données via le bundle.

 JobInfo task = new JobInfo.Builder(JOB_ID, serviceName) .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) .setRequiresDeviceIdle(true) .setRequiresCharging(true) .build(); JobScheduler scheduler = (JobScheduler) context.getSystemService(JOB_SCHEDULER_SERVICE); scheduler.schedule(task); 

En général, par rapport à rien, c'était déjà quelque chose. Mais ce mécanisme n'est disponible qu'avec l'API 21, et au moment de la sortie d'Android 5.0, il serait étrange d'arrêter de prendre en charge tous les anciens appareils (3 ans se sont écoulés et nous prenons toujours en charge les quatre).

Avantages :
L'API est simple;
conditions de lancement.

Inconvénients :
Disponible à partir de l'API 21
en fait, uniquement avec l'API 23;
facile de se tromper.

Android 5. Lollipop

Gcm network manager


Un analogue de JobScheduler - GCM Network Manager a également été présenté. Il s'agit d'une bibliothèque qui offrait des fonctionnalités similaires, mais fonctionnait déjà avec l'API 9. Vrai, elle nécessitait en retour les services Google Play. Apparemment, les fonctionnalités nécessaires au travail de JobScheduler ont commencé à être fournies non seulement via la version Android, mais également au niveau GPS. Il est à noter que les développeurs du framework ont ​​rapidement changé d'avis et décidé de ne pas connecter leur futur au GPS. Merci à eux pour ça.

Tout semble absolument identique. Même service:

 public class GcmNetworkManagerService extends GcmTaskService { @Override public int onRunTask(TaskParams taskParams) { doWork(taskParams); return 0; } } 

Le même lancement de tâche:

 OneoffTask task = new OneoffTask.Builder() .setService(GcmNetworkManagerService.class) .setTag(TAG) .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) .setRequiresCharging(true) .build(); GcmNetworkManager mGcmNetworkManager = GcmNetworkManager.getInstance(this); mGcmNetworkManager.schedule(task); 

Cette similitude d'architecture a été dictée par la fonctionnalité héritée et le désir d'obtenir une migration simple entre les outils.

Avantages :
API similaire à JobScheduler;
Disponible à partir de l'API 9.

Inconvénients :
Vous devez avoir les services Google Play
facile de se tromper.

Android 5. Lollipop

WakefulBroadcastReceiver


Ensuite, j'écrirai quelques mots sur l'un des mécanismes de base utilisés dans JobScheduler et disponibles directement pour les développeurs. Il s'agit de WakeLock et de son WakefulBroadcastReceiver basé.

À l'aide de WakeLock, vous pouvez empêcher le système de rester en veille, c'est-à-dire garder l'appareil dans un état actif. Cela est nécessaire si nous voulons faire un travail important.
Lors de la création de WakeLock, vous pouvez spécifier ses paramètres: maintenez le CPU, l'écran ou le clavier.

 PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE) PowerManager.WakeLock wl = pm.newWakeLock(PARTIAL_WAKE_LOCK, "name") wl.acquire(timeout); 

Sur la base de ce mécanisme, le WakefulBroadcastReceiver fonctionne. Nous démarrons le service et maintenons WakeLock.

 public class SimpleWakefulReceiver extends WakefulBroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { Intent service = new Intent(context, SimpleWakefulService.class); startWakefulService(context, service); } } 

Une fois que le service a terminé les travaux nécessaires, nous le diffusons par des méthodes similaires.

À travers 4 versions, ce BroadcastReceiver deviendra obsolète, et les alternatives suivantes seront décrites sur developer.android.com:

  • JobScheduler;
  • Syncadapter
  • DownloadManager
  • FLAG_KEEP_SCREEN_ON pour Windows.

Android 6. Guimauve

DozeMode: Dormez en déplacement


Ensuite, Google a commencé à appliquer diverses optimisations pour les applications s'exécutant sur l'appareil. Mais ce qui est l'optimisation pour l'utilisateur est une limitation pour le développeur.

La première étape a été DozeMode, qui met l'appareil en mode veille s'il reste inactif pendant un certain temps. Dans les premières versions, cela durait une heure, dans les versions suivantes, la durée du sommeil était réduite à 30 minutes. Périodiquement, le téléphone se réveille, exécute toutes les tâches en attente et s'endort à nouveau. La fenêtre DozeMode se développe de façon exponentielle. Toutes les transitions entre les modes peuvent être suivies via adb.

Lorsque DozeMode se produit, les restrictions suivantes sont imposées sur l'application:

  • le système ignore tous les WakeLock;
  • AlarmManager est retardé;
  • JobScheduler ne fonctionne pas;
  • SyncAdapter ne fonctionne pas;
  • l'accès au réseau est limité.

Vous pouvez également ajouter votre application à la liste blanche afin qu'elle ne tombe pas dans les limites de DozeMode, mais au moins Samsung a complètement ignoré cette liste.

Android 6. Guimauve

AppStandby: Applications inactives


Le système identifie les applications inactives et leur impose les mêmes restrictions que dans DozeMode.
Une demande est envoyée en isolement si:

  • n'a pas de processus au premier plan;
  • n'a pas de notification active;
  • non ajouté à la liste d'exclusion.

Android 7. Nougat

Optimisations en arrière-plan. Svelte


Svelte est un projet dans lequel Google essaie d'optimiser la consommation de RAM par les applications et le système lui-même.
Dans Android 7, dans le cadre de ce projet, il a été décidé que les diffusions implicites ne sont pas très efficaces, car elles sont écoutées par un grand nombre d'applications et le système dépense une grande quantité de ressources lorsque ces événements se produisent. Par conséquent, les types d'événements suivants ont été interdits de déclaration dans le manifeste:

  • CONNECTIVITY_ACTION;
  • ACTION_NEW_PICTURE;
  • ACTION_NEW_VIDEO.

Android 7. Nougat

FirebaseJobDispatcher


Dans le même temps, une nouvelle version du cadre de lancement des tâches a été publiée - FirebaseJobDispatcher. En fait, c'était le GCM NetworkManager terminé, qui a été un peu rangé et rendu un peu plus flexible.

Visuellement, tout était exactement pareil. Même service:

 public class JobSchedulerService extends JobService { @Override public boolean onStartJob(JobParameters params) { doWork(params); return false; } @Override public boolean onStopJob(JobParameters params) { return false; } } 

La seule différence entre lui était la possibilité d'installer son pilote. Un pilote est la classe qui était responsable de la stratégie de lancement des tâches.

Le lancement des tâches lui-même n'a pas changé au fil du temps.

 FirebaseJobDispatcher dispatcher = new FirebaseJobDispatcher(new GooglePlayDriver(context)); Job task = dispatcher.newJobBuilder() .setService(FirebaseJobDispatcherService.class) .setTag(TAG) .setConstraints(Constraint.ON_UNMETERED_NETWORK, Constraint.DEVICE_IDLE) .build(); dispatcher.mustSchedule(task); 

Avantages :
API similaire à JobScheduler;
Disponible à partir de l'API 9.

Inconvénients :
Vous devez avoir les services Google Play
facile de se tromper.

C'était encourageant d'installer mon pilote pour se débarrasser du GPS. Nous avons même cherché, mais avons finalement trouvé ce qui suit:





Google le sait, mais ces tâches restent ouvertes pendant plusieurs années.

Android 7. Nougat

Job Android par Evernote


En conséquence, la communauté ne pouvait pas le supporter, et une solution self-made est apparue sous la forme d'une bibliothèque d'Evernote. Ce n'était pas le seul, mais c'était la solution d'Evernote qui a pu s'établir et «entrer dans le peuple».

En termes architecturaux, cette bibliothèque était plus pratique que ses prédécesseurs.
L'entité chargée de créer les tâches est apparue. Dans le cas de JobScheduler, ils ont été créés par réflexion.

 class SendLogsJobCreator : JobCreator { override fun create(tag: String): Job? { when (tag) { SendLogsJob.TAG -> return SendLogsJob() } return null } } 

Il existe une classe distincte, qui est la tâche elle-même. Dans JobScheduler, tout cela a été vidé dans un commutateur à l'intérieur de onStartJob.

 class SendLogsJob : Job() { override fun onRunJob(params: Params): Result { return doWork(params) } } 

Le lancement des tâches est identique, mais en plus des événements hérités, Evernote a également ajouté les siens, tels que le lancement de tâches quotidiennes, des tâches uniques et le lancement dans la fenêtre.

 new JobRequest.Builder(JOB_ID) .setRequiresDeviceIdle(true) .setRequiresCharging(true) .setRequiredNetworkType(JobRequest.NetworkType.UNMETERED) .build() .scheduleAsync(); 

Avantages :
API pratique;
pris en charge sur toutes les versions;
Vous n'avez pas besoin des services Google Play.

Inconvénients :
solution tierce.

Les gars ont activement soutenu leur bibliothèque. Bien qu'il y ait eu quelques problèmes critiques, cela a fonctionné sur toutes les versions et sur tous les appareils. En conséquence, l'année dernière, notre équipe Android a choisi une solution d'Evernote, car les bibliothèques de Google ont coupé une grande couche d'appareils qu'elles ne peuvent pas prendre en charge.
À l'intérieur, elle a travaillé sur des solutions de Google, dans des cas extrêmes - avec AlarmManager.

Android 8. Oreo

Limites d'exécution en arrière-plan


Revenons à nos limites. Avec l'avènement du nouvel Android, de nouvelles optimisations sont venues. Les gars de Google ont trouvé un autre problème. Cette fois, le tout s'est avéré être dans les services et les émissions (oui, rien de nouveau).

  • startService si les applications en arrière-plan
  • diffusion implicite dans le manifeste

Premièrement, il était interdit de démarrer des services à partir de l'arrière-plan. Dans le «cadre de la loi» ne restaient que des services de premier plan. Les services peuvent désormais être déclarés obsolètes.
La deuxième limitation est la même diffusion. Cette fois, il est devenu interdit d'enregistrer TOUTES les diffusions implicites dans le manifeste. La diffusion implicite est une diffusion, qui est destinée non seulement à notre application. Par exemple, il y a Action ACTION_PACKAGE_REPLACED et il y a ACTION_MY_PACKAGE_REPLACED. Donc, le premier est implicite.

Mais toute diffusion peut toujours être enregistrée via Context.registerBroadcast.

Android 9. Pie

Workmanager


Sur cette optimisation s'est encore arrêtée. Peut-être que les appareils ont commencé à fonctionner rapidement et soigneusement en termes de consommation d'énergie; peut-être que les utilisateurs s'en sont plaints moins.
Dans Android 9, les développeurs du framework ont ​​minutieusement abordé l'outil de lancement des tâches. Afin de résoudre tous les problèmes urgents, une bibliothèque a été introduite sur Google I / O pour lancer les tâches d'arrière-plan de WorkManager.

Google a récemment tenté de façonner sa vision de l'architecture de l'application Android et donne aux développeurs les outils nécessaires à cet effet. Il y avait donc des composants architecturaux avec LiveData, ViewModel et Room. WorkManager ressemble à un complément raisonnable à leur approche et à leur paradigme.

Si nous parlons de la façon dont le WorkManager est organisé à l'intérieur, il n'y a pas de percée technologique. En fait, il s'agit d'un wrapper de solutions existantes: JobScheduler, FirebaseJobDispatcher et AlarmManager.

createBestAvailableBackgroundScheduler
 static Scheduler createBestAvailableBackgroundScheduler(Context, WorkManager) { if (Build.VERSION.SDK_INT >= MIN_JOB_SCHEDULER_API_LEVEL) { return new SystemJobScheduler(context, workManager); } try { return tryCreateFirebaseJobScheduler(context); } catch (Exception e) { return new SystemAlarmScheduler(context); } } 


Le code de sélection est assez simple. Mais il convient de noter que JobScheduler est disponible à partir de l'API 21, mais ils ne l'utilisent qu'avec l'API 23, car les premières versions étaient plutôt instables.

Si la version est inférieure à 23, alors par réflexion, nous essayons de trouver FirebaseJobDispatcher, sinon nous utilisons AlarmManager.

Il est à noter que le wrapper est sorti assez flexible. Cette fois, les développeurs ont tout divisé en entités distinctes et, d'un point de vue architectural, cela semble pratique:

  • Travailleur - logique de travail;
  • WorkRequest - logique de lancement de la tâche;
  • WorkRequest.Builder - paramètres;
  • Contraintes - conditions;
  • WorkManager - un gestionnaire qui gère les tâches;
  • WorkStatus - état de la tâche.




Les conditions de lancement ont été héritées de JobScheduler.
Il peut être noté que le déclencheur pour changer l'URI est apparu uniquement avec l'API 23. En outre, vous pouvez vous abonner au changement non seulement d'un URI spécifique, mais aussi de tous les imbriqués en utilisant l'indicateur dans la méthode.

Si nous parlons de nous, alors au stade alpha, il a été décidé de passer à WorkManager.
Il y a plusieurs raisons à cela. Evernote a quelques bogues critiques que les développeurs de la bibliothèque promettent de corriger avec la transition vers une version avec WorkManager intégré. Et ils conviennent eux-mêmes que la décision de Google annule les avantages d'Evernote. De plus, cette solution cadre bien avec notre architecture, car nous utilisons des composants d'architecture.

De plus, je voudrais montrer avec un exemple simple comment nous essayons d'utiliser cette approche. Dans le même temps, il n'est pas très important que vous ayez un WorkManager ou un JobScheduler.



Regardons un exemple avec un cas très simple: cliquer sur republier ou aimer.

Maintenant, toutes les applications essaient de s'éloigner du blocage des demandes sur le réseau, car cela rend l'utilisateur nerveux et le fait attendre, bien qu'il puisse à présent effectuer des achats dans l'application ou regarder des publicités.

Dans de tels cas, les données locales changent d'abord - l'utilisateur voit immédiatement le résultat de son action. Ensuite, en arrière-plan, il y a une demande au serveur, si elle échoue, les données sont réinitialisées à leur état initial.

Ensuite, je vais montrer un exemple de la façon dont il ressemble à nous.

JobRunner contient la logique de lancement des tâches. Ses méthodes décrivent la configuration des tâches et passent les paramètres.

JobRunner.java
 fun likePost(content: IFunnyContent) { val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() val input = Data.Builder() .putString(LikeContentJob.ID, content.id) .build() val request = OneTimeWorkRequest.Builder(LikeContentJob::class.java) .setInputData(input) .setConstraints(constraints) .build() WorkManager.getInstance().enqueue(request) } 


La tâche elle-même dans le WorkManager est la suivante: nous prenons l'id des paramètres et appelons la méthode sur le serveur pour aimer ce contenu.

Nous avons une classe de base qui contient la logique suivante:

 abstract class BaseJob : Worker() { final override fun doWork(): Result { val workerInjector = WorkerInjectorProvider.injector() workerInjector.inject(this) return performJob(inputData) } abstract fun performJob(params: Data): Result } 

Tout d'abord, cela vous permet de vous éloigner un peu de la connaissance explicite de Worker. Il contient également la logique d'injection de dépendances via WorkerInjector.

WorkerInjectorImpl.java
 @Singleton public class WorkerInjectorImpl implements WorkerInjector { @Inject public WorkerInjectorImpl() {} @Ovierride public void inject(Worker job) { if (worker instanceof AppCrashedEventSendJob) { Injector.getAppComponent().inject((AppCrashedEventSendJob) job); } else if (worker instanceof CheckNativeCrashesJob) { Injector.getAppComponent().inject((CheckNativeCrashesJob) job); } } } 


Il sert simplement de proxy aux appels à Dagger, mais il nous aide dans les tests: nous remplaçons les implémentations d'injecteurs et implémentons l'environnement nécessaire dans les tâches.

 fun void testRegisterPushProvider() { WorkManagerTestInitHelper.initializeTestWorkManager(context) val testDriver = WorkManagerTestInitHelper.getTestDriver() WorkerInjectorProvider.setInjector(TestInjector()) // mock dependencies val id = jobRunner.runPushRegisterJob() testDriver.setAllConstraintsMet(id) Assert.assertTrue(…) } 

 class LikePostInteractor @Inject constructor( val iFunnyContentDao: IFunnyContentDao, val jobRunner: JobRunner) : Interactor { fun execute() { iFunnyContentDao.like(getContent().id) jobRunner.likePost(getContent()) } } 

L'interacteur est l'entité que le ViewController tire pour initier le passage du script (dans ce cas, comme lui). Nous marquons le contenu localement comme «téléchargé» et soumettons la tâche pour exécution. Si la tâche échoue, l'élément similaire est supprimé.

 class IFunnyContentViewModel(val iFunnyContentDao: IFunnyContentDao) : ViewModel() { val likeState = MediatorLiveData<Boolean>() var iFunnyContentId = MutableLiveData<String>() private var iFunnyContentState: LiveData<IFunnyContent> = attachLiveDataToContentId(); init { likeState.addSource(iFunnyContentState) { likeState.postValue(it!!.hasLike) } } } 

Nous utilisons les composants d'architecture de Google: ViewModel et LiveData. Voici à quoi ressemble notre ViewModel. Ici, nous connectons la mise à jour de l'objet dans le DAO avec le statut de like.

IFunnyContentViewController.java
 class IFunnyContentViewController @Inject constructor( private val likePostInteractor: LikePostInteractor, val viewModel: IFunnyContentViewModel) : ViewController { override fun attach(view: View) { viewModel.likeState.observe(lifecycleOwner, { updateLikeView(it!!) }) } fun onLikePost() { likePostInteractor.setContent(getContent()) likePostInteractor.execute() } } 


ViewController, d'une part, souscrit à la modification du statut de similaires, d'autre part, initie le passage du script dont nous avons besoin.

Et c'est pratiquement tout le code dont nous avons besoin. Il reste à ajouter le comportement de la vue elle-même avec le même et la mise en œuvre de votre DAO; si vous utilisez Room, enregistrez simplement les champs dans l'objet. Cela semble assez simple et efficace.

Pour résumer


JobScheduler, GCM Network Manager, FirebaseJobDispatcher:

  • ne les utilise pas
  • ne lisez plus d'articles à leur sujet
  • ne regarde pas les rapports
  • ne pensez pas lequel choisir.

Job Android par Evernote:

  • À l'intérieur, ils utiliseront le WorkManager;
  • les bogues critiques sont flous entre les solutions.

WorkManager:

  • API LEVEL 9+;
  • indépendant des services Google Play;
  • Chaînage / Fusions d'entrée;
  • approche réactive;
  • le soutien de Google (je veux le croire).

Source: https://habr.com/ru/post/fr434680/


All Articles