Utiliser Joda 1.6.2 avec Android
Le code suivant se bloque pendant environ 15 secondes.
DateTime dt = new DateTime();
Initialement posté ce post Android Java - Joda Date est lent dans Eclipse/Emulator -
Je viens de l'essayer à nouveau et ce n'est toujours pas mieux. Est-ce que quelqu'un d'autre a ce problème ou sait comment le résoudre?
J'ai également rencontré ce problème. Les soupçons de Jon Skeet étaient corrects, le problème est que les fuseaux horaires sont chargés de manière vraiment inefficace, ouvrant un fichier jar puis lisant le manifeste pour essayer d'obtenir ces informations.
Cependant, le simple fait d'appeler DateTimeZone.setProvider([custom provider instance ...])
n'est pas suffisant car, pour des raisons qui n'ont pas de sens pour moi, DateTimeZone possède un initialiseur statique où il appelle getDefaultProvider()
.
Pour être complètement sûr, vous pouvez remplacer cette valeur par défaut en définissant cette propriété système avant d'appeler quoi que ce soit dans le joda.
Dans votre activité, par exemple, ajoutez ceci:
@Override
public void onCreate(Bundle savedInstanceState) {
System.setProperty("org.joda.time.DateTimeZone.Provider",
"com.your.package.FastDateTimeZoneProvider");
}
Il vous suffit alors de définir FastDateTimeZoneProvider
. J'ai écrit ce qui suit:
package com.your.package;
public class FastDateTimeZoneProvider implements Provider {
public static final Set<String> AVAILABLE_IDS = new HashSet<String>();
static {
AVAILABLE_IDS.addAll(Arrays.asList(TimeZone.getAvailableIDs()));
}
public DateTimeZone getZone(String id) {
if (id == null) {
return DateTimeZone.UTC;
}
TimeZone tz = TimeZone.getTimeZone(id);
if (tz == null) {
return DateTimeZone.UTC;
}
int rawOffset = tz.getRawOffset();
//sub-optimal. could be improved to only create a new Date every few minutes
if (tz.inDaylightTime(new Date())) {
rawOffset += tz.getDSTSavings();
}
return DateTimeZone.forOffsetMillis(rawOffset);
}
public Set getAvailableIDs() {
return AVAILABLE_IDS;
}
}
J'ai testé cela et cela semble fonctionner sur Android SDK 2.1+ avec joda version 1.6.2. Il peut bien sûr être optimisé davantage, mais lors du profilage de mon application ( mogwee ), cela a réduit le temps d'initialisation de DateTimeZone de ~ 500 ms à ~ 18 ms.
Si vous utilisez proguard pour créer votre application, vous devrez ajouter cette ligne à proguard.cfg car Joda s'attend à ce que le nom de la classe soit exactement comme vous le spécifiez:
-keep class com.your.package.FastDateTimeZoneProvider
Je fortement suspect c'est parce qu'il doit construire la chronologie ISO pour le fuseau horaire par défaut, ce qui implique probablement de lire toutes les informations de fuseau horaire dans.
Vous pouvez le vérifier en appelant ISOChronology.getInstance()
pour la première fois, puis chronométrez un appel ultérieur à new DateTime()
. Je suspect ça va être rapide.
Savez-vous quels fuseaux horaires seront pertinents dans votre application? Vous mai trouvez que vous pouvez rendre le tout beaucoup plus rapide en reconstruisant Joda Time avec une base de données de fuseaux horaires très réduite. Sinon, appelez DateTimeZone.setProvider()
avec votre propre implémentation de Provider
qui ne fait pas autant de travail.
Cela vaut la peine de vérifier si c'est vraiment le problème d'abord, bien sûr :) Vous pouvez aussi vouloir essayer de passer explicitement dans le fuseau horaire UTC, ce qui ne nécessitera pas de lecture dans la base de données de fuseau horaire ... bien que vous ne savez jamais quand vous déclencherez accidentellement un appel qui fait nécessite le fuseau horaire par défaut, auquel cas vous encourrez le même coût.
Je n'ai besoin que d'UTC dans ma demande. Donc, suivant les conseils de Unchek, j'ai utilisé
System.setProperty("org.joda.time.DateTimeZone.Provider", "org.joda.time.tz.UTCProvider");
org.joda.time.tz.UTCProvider
est en fait utilisé par JodaTime comme sauvegarde secondaire, alors j'ai pensé pourquoi ne pas l'utiliser pour une utilisation principale? Jusqu'ici tout va bien. Il se charge rapidement.
La meilleure réponse fournie par ploughman n'est pas fiable si vous devez avoir des calculs de fuseau horaire précis pour vos dates. Voici un exemple de problème pouvant survenir:
Supposons que votre objet DateTime
soit défini à 4h00, une heure après le début de l'heure d'été ce jour-là. Lorsque Joda vérifie le fournisseur FastDateTimeZoneProvider
avant 3 h du matin (c'est-à-dire avant l'heure d'été), il obtiendra un objet DateTimeZone
avec le mauvais décalage car la vérification tz.inDaylightTime(new Date())
renverra false .
Ma solution a été d'adopter la bibliothèque joda-time-Android récemment publiée . Il utilise le cœur de Joda mais s'assure de charger un fuseau horaire uniquement selon les besoins à partir du dossier brut. La mise en place est facile avec gradle. Dans votre projet, étendez la classe Application
et ajoutez ce qui suit sur sa onCreate()
:
public class MyApp extends Application {
@Override
public void onCreate() {
super.onCreate();
JodaTimeAndroid.init(this);
}
}
L'auteur a écrit un article de blog à ce sujet l'année dernière.
Je peux confirmer ce problème avec les versions 1, 1.5 et 1.62 de joda. Date4J fonctionne bien pour moi comme alternative.
Je viens de réaliser le test que @ "Name is carl" a posté, sur plusieurs appareils. Je dois noter que le test n'est pas complètement valide et que les résultats sont trompeurs (en ce sens qu'il ne reflète qu'une seule instance de DateTime).
D'après son test, lors de la comparaison de DateTime à Date, DateTime est obligé d'analyser la chaîne ts, où Date n'analyse rien.
Alors que la création initiale de DateTime était précise, cela prend SEULEMENT autant de temps sur la toute première création ... chaque instance après cela était de 0 ms (ou très proche de 0 ms)
Pour vérifier cela, j'ai utilisé le code suivant et créé 1000 nouvelles instances de DateTime sur un ancien appareil Android 2.3
int iterations = 1000;
long totalTime = 0;
// Test Joda Date
for (int i = 0; i < iterations; i++) {
long d1 = System.currentTimeMillis();
DateTime d = new DateTime();
long d2 = System.currentTimeMillis();
long duration = (d2 - d1);
totalTime += duration;
log.i(TAG, "datetime : " + duration);
}
log.i(TAG, "Average datetime : " + ((double) totalTime/ (double) iterations));
Mes résultats ont montré:
datetime: 264 datetime: 0 datetime: 0 datetime: 0 datetime: 0 datetime: 0 datetime: 0 ... datetime: 0 datetime: 0 datetime: 1 datetime: 0 ... datetime: 0 datetime: 0 datetime: 0
Ainsi, le résultat a été que la première instance était de 264 ms et plus de 95% des éléments suivants étaient de 0 ms (j'ai parfois eu un 1 ms, mais jamais une valeur supérieure à 1 ms).
J'espère que cela donne une image plus claire du coût d'utilisation de Joda.
REMARQUE: j'utilisais joda-time version 2.1
En utilisant dlew/joda-time-Android la dépendance gradle, cela ne prend que 22,82 ms (millisecondes). Je vous recommande donc de l'utiliser au lieu de remplacer quoi que ce soit.
J'ai trouvé une solution pour moi. Je charge UTC et le fuseau horaire par défaut. C'est donc des charges très rapides. Et je pense que dans ce cas, j'ai besoin de rattraper le changement de fuseau horaire et de recharger le fuseau horaire par défaut.
public class FastDateTimeZoneProvider implements Provider {
public static final Set<String> AVAILABLE_IDS = new HashSet<String>();
static {
AVAILABLE_IDS.add("UTC");
AVAILABLE_IDS.add(TimeZone.getDefault().getID());
}
public DateTimeZone getZone(String id) {
int rawOffset = 0;
if (id == null) {
return DateTimeZone.getDefault();
}
TimeZone tz = TimeZone.getTimeZone(id);
if (tz == null) {
return DateTimeZone.getDefault();
}
rawOffset = tz.getRawOffset();
//sub-optimal. could be improved to only create a new Date every few minutes
if (tz.inDaylightTime(new Date())) {
rawOffset += tz.getDSTSavings();
}
return DateTimeZone.forOffsetMillis(rawOffset);
}
public Set getAvailableIDs() {
return AVAILABLE_IDS;
}
}
Vous pouvez également commander le backport JSR-310 de Jake Wharton des packages Java.time. *.
Cette bibliothèque place les informations de fuseau horaire en tant qu'élément standard Android et fournit un chargeur personnalisé pour les analyser efficacement. [Elle] propose les API standard dans Java 8 as un package beaucoup plus petit non seulement en taille binaire et en nombre de méthodes, mais aussi en taille API.
Ainsi, cette solution fournit une bibliothèque de taille binaire plus petite avec une empreinte de comptage de méthode plus petite, combinée à un chargeur efficace pour les données de fuseau horaire.
Cette note rapide pour compléter la réponse sur date4j de @Steven
J'ai exécuté un benchmark rapide et sale en comparant Java.util.Date
, jodatime
et date4j
sur le plus faible Android que j'ai (HTC Dream/Sapphire 2.3.5).
Détails: construction normale (pas de proguard), implémentation du FastDateTimeZoneProvider
pour jodatime.
Voici le code:
String ts = "2010-01-19T23:59:59.123456789";
long d1 = System.currentTimeMillis();
DateTime d = new DateTime(ts);
long d2 = System.currentTimeMillis();
System.err.println("datetime : " + dateUtils.durationtoString(d2 - d1));
d1 = System.currentTimeMillis();
Date dd = new Date();
d2 = System.currentTimeMillis();
System.err.println("date : " + dateUtils.durationtoString(d2 - d1));
d1 = System.currentTimeMillis();
hirondelle.date4j.DateTime ddd = new hirondelle.date4j.DateTime(ts);
d2 = System.currentTimeMillis();
System.err.println("date4j : " + dateUtils.durationtoString(d2 - d1));
Voici les résultats :
debug | normal
joda : 3s (3577ms) | 0s (284ms)
date : 0s (0) | 0s (0s)
date4j : 0s (55ms) | 0s (2ms)
Une dernière chose, les tailles des pots:
jodatime 2.1 : 558 kb
date4j : 35 kb
Je pense que je vais essayer date4j.
FastDateTimeZoneProvider
proposé par @ElijahSh et @plowman. Parce qu'il traite l'offset DST comme un décalage standard pour le fuseau horaire sélectionné. Car cela donnera de "bons" résultats pour aujourd'hui et pour le reste d'un semestre avant la prochaine transition à l'heure d'été. Mais cela donnera certainement un mauvais résultat pour la veille de la transition DST et pour le lendemain de la prochaine transition DST.La bonne façon d'utiliser les fuseaux horaires du système avec JodaTime:
classe publique AndroidDateTimeZoneProvider implémente org.joda.time.tz.Provider {
@Override
public Set<String> getAvailableIDs() {
return new HashSet<>(Arrays.asList(TimeZone.getAvailableIDs()));
}
@Override
public DateTimeZone getZone(String id) {
return id == null
? null
: id.equals("UTC")
? DateTimeZone.UTC
: Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
? new AndroidNewDateTimeZone(id)
: new AndroidOldDateTimeZone(id);
}
}
Où AndroidOldDateTimeZone:
public class AndroidOldDateTimeZone extends DateTimeZone {
private final TimeZone mTz;
private final Calendar mCalendar;
private long[] mTransition;
public AndroidOldDateTimeZone(final String id) {
super(id);
mTz = TimeZone.getTimeZone(id);
mCalendar = GregorianCalendar.getInstance(mTz);
mTransition = new long[0];
try {
final Class tzClass = mTz.getClass();
final Field field = tzClass.getDeclaredField("mTransitions");
field.setAccessible(true);
final Object transitions = field.get(mTz);
if (transitions instanceof long[]) {
mTransition = (long[]) transitions;
} else if (transitions instanceof int[]) {
final int[] intArray = (int[]) transitions;
final int size = intArray.length;
mTransition = new long[size];
for (int i = 0; i < size; i++) {
mTransition[i] = intArray[i];
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
public TimeZone getTz() {
return mTz;
}
@Override
public long previousTransition(final long instant) {
if (mTransition.length == 0) {
return instant;
}
final int index = findTransitionIndex(instant, false);
if (index <= 0) {
return instant;
}
return mTransition[index - 1] * 1000;
}
@Override
public long nextTransition(final long instant) {
if (mTransition.length == 0) {
return instant;
}
final int index = findTransitionIndex(instant, true);
if (index > mTransition.length - 2) {
return instant;
}
return mTransition[index + 1] * 1000;
}
@Override
public boolean isFixed() {
return mTransition.length > 0 &&
mCalendar.getMinimum(Calendar.DST_OFFSET) == mCalendar.getMaximum(Calendar.DST_OFFSET) &&
mCalendar.getMinimum(Calendar.ZONE_OFFSET) == mCalendar.getMaximum(Calendar.ZONE_OFFSET);
}
@Override
public boolean isStandardOffset(final long instant) {
mCalendar.setTimeInMillis(instant);
return mCalendar.get(Calendar.DST_OFFSET) == 0;
}
@Override
public int getStandardOffset(final long instant) {
mCalendar.setTimeInMillis(instant);
return mCalendar.get(Calendar.ZONE_OFFSET);
}
@Override
public int getOffset(final long instant) {
return mTz.getOffset(instant);
}
@Override
public String getShortName(final long instant, final Locale locale) {
return getName(instant, locale, true);
}
@Override
public String getName(final long instant, final Locale locale) {
return getName(instant, locale, false);
}
private String getName(final long instant, final Locale locale, final boolean isShort) {
return mTz.getDisplayName(!isStandardOffset(instant),
isShort ? TimeZone.SHORT : TimeZone.LONG,
locale == null ? Locale.getDefault() : locale);
}
@Override
public String getNameKey(final long instant) {
return null;
}
@Override
public TimeZone toTimeZone() {
return (TimeZone) mTz.clone();
}
@Override
public String toString() {
return mTz.getClass().getSimpleName();
}
@Override
public boolean equals(final Object o) {
return (o instanceof AndroidOldDateTimeZone) && mTz == ((AndroidOldDateTimeZone) o).getTz();
}
@Override
public int hashCode() {
return 31 * super.hashCode() + mTz.hashCode();
}
private long roundDownMillisToSeconds(final long millis) {
return millis < 0 ? (millis - 999) / 1000 : millis / 1000;
}
private int findTransitionIndex(final long millis, final boolean isNext) {
final long seconds = roundDownMillisToSeconds(millis);
int index = isNext ? mTransition.length : -1;
for (int i = 0; i < mTransition.length; i++) {
if (mTransition[i] == seconds) {
index = i;
}
}
return index;
}
}
AndroidNewDateTimeZone.Java identique à "Old" mais basé sur Android.icu.util.TimeZone
au lieu.