Android Java - Joda Date работает медленно

Использование Joda 1.6.2 с Android

Следующий код зависает около 15 секунд.

DateTime dt = new DateTime();

Первоначально опубликовано это сообщение Android Java - Joda Date работает медленно в Eclipse/Emulator -

Просто попробовал еще раз, и все равно не лучше. У кого-нибудь еще есть такая проблема или знаете как ее исправить?

10 ответов

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

Тем не менее, просто позвонив DateTimeZone.setProvider([custom provider instance ...]) недостаточно, потому что по причинам, которые не имеют смысла для меня, DateTimeZone имеет статический инициализатор, где он вызывает getDefaultProvider(),

Чтобы быть в полной безопасности, вы можете переопределить это значение по умолчанию, установив это системное свойство перед тем, как вызывать что-либо в joda.

Например, добавьте в свою деятельность следующее:

@Override
public void onCreate(Bundle savedInstanceState) {
    System.setProperty("org.joda.time.DateTimeZone.Provider", 
    "com.your.package.FastDateTimeZoneProvider");
}

Тогда все, что вам нужно сделать, это определить FastDateTimeZoneProvider, Я написал следующее:

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;
    }
}

Я проверил это, и он работает на Android SDK 2.1+ с версией joda 1.6.2. Конечно, его можно оптимизировать и дальше, но при профилировании моего приложения ( mogwee) это уменьшило время инициализации DateTimeZone с ~500 мс до ~18 мс.

Если вы используете proguard для сборки своего приложения, вам нужно добавить эту строку в proguard.cfg, потому что Joda ожидает, что имя класса будет точно таким, как вы укажете:

-keep class com.your.package.FastDateTimeZoneProvider

Я сильно подозреваю, что это связано с необходимостью построения хронологии ISO для часового пояса по умолчанию, что, вероятно, предполагает чтение всей информации о часовом поясе.

Вы можете проверить это, позвонив ISOChronology.getInstance() сначала - время, а затем время последующего вызова new DateTime(), Я подозреваю, что это будет быстро.

Знаете ли вы, какие часовые пояса будут актуальны в вашем приложении? Вы можете обнаружить, что можете сделать все это намного быстрее, перестроив Joda Time с очень сокращенной базой данных часовых поясов. Или позвоните DateTimeZone.setProvider() с вашей собственной реализацией Provider который не делает так много работы.

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

Мне нужно только UTC в моем заявлении. Итак, следуя совету Унчека, я использовал

System.setProperty("org.joda.time.DateTimeZone.Provider", "org.joda.time.tz.UTCProvider");

org.joda.time.tz.UTCProvider на самом деле используется JodaTime в качестве вторичной резервной копии, поэтому я подумал, почему бы не использовать его для основного использования? Все идет нормально. Он загружается быстро.

Лучший ответ, предоставленный пахотчиком, не является надежным, если вы должны иметь точные расчеты часового пояса для ваших дат. Вот пример проблемы, которая может произойти:

Предположим, ваш DateTime Объект установлен на 4:00 утра, через час после начала перехода на летнее время. Когда Йода проверяет FastDateTimeZoneProvider до 3:00 (т.е. до перехода на летнее время) он получит DateTimeZone объект с неправильным смещением, потому что tz.inDaylightTime(new Date()) проверка вернет false.

Мое решение состояло в том, чтобы принять недавно опубликованную библиотеку joda-time-android. Он использует ядро ​​Joda, но обеспечивает загрузку часового пояса только по мере необходимости из папки raw. Настройка легко с Gradle. В вашем проекте продлите Application класс и добавить следующее на его onCreate():

public class MyApp extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        JodaTimeAndroid.init(this);
    }
}

Автор написал пост в блоге об этом в прошлом году.

Я только что выполнил тест, который @"Name is carl" опубликовал на нескольких устройствах. Я должен отметить, что тест не полностью действителен и результаты вводят в заблуждение (в том смысле, что он отражает только один экземпляр DateTime).

  1. Из его теста, при сравнении DateTime с Date, DateTime вынужден анализировать строковые значения ts, где Date ничего не анализирует.

  2. Хотя первоначальное создание DateTime было точным, оно ТОЛЬКО занимает столько времени на самом ПЕРВОМ создании... каждый экземпляр после этого составлял 0 мс (или очень близко к 0 мс)

Чтобы убедиться в этом, я использовал следующий код и создал 1000 новых экземпляров DateTime на старом устройстве 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));

Мои результаты показали:

Дата: 264
Дата: 0
Дата: 0
Дата: 0
Дата: 0
Дата: 0
Дата: 0...
Дата: 0
Дата: 0
Дата: 1
Дата: 0...
Дата: 0
Дата: 0
Дата: 0

Таким образом, результатом было то, что первый случай был 264 мс, и более 95% из следующих были 0 мс (у меня иногда был 1 мс, но никогда не имел значение больше 1 мс).

Надеюсь, что это дает более четкое представление о стоимости использования Joda.

ПРИМЕЧАНИЕ: я использовал joda-time версии 2.1

При использовании зависимости gradle от dlew / joda-time-android требуется всего 22,82 мс (миллисекунды). Поэтому я рекомендую вам использовать его вместо того, чтобы переопределять что-либо.

Я могу подтвердить эту проблему с версией 1, 1.5 и 1.62 от joda. Date4J работает хорошо для меня в качестве альтернативы.

http://www.date4j.net/

Я нашел решение для меня. Я загружаю UTC и часовой пояс по умолчанию. Так что загружается очень быстро. И я думаю, что в этом случае мне нужно перехватить трансляцию TIME ZONE CHANGE и перезагрузить часовой пояс по умолчанию.

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;
    }
}

Это быстрое примечание, чтобы завершить ответ о date4j от @Steven

Я провел быстрый и грязный тест сравнения java.util.Date, jodatime а также date4j на самом слабом андроид устройстве у меня (HTC Dream/Sapphire 2.3.5).

Детали: нормальная сборка (без защиты), реализация FastDateTimeZoneProvider для йодатима.

Вот код:

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));

Вот результаты:

           debug       | normal 
joda     : 3s (3577ms) | 0s (284ms)
date     : 0s (0)      | 0s (0s)
date4j   : 0s (55ms)   | 0s (2ms) 

И последнее, размеры банок:

jodatime 2.1 : 558 kb 
date4j       : 35 kb

Я думаю, что попробую date4j.

Вы также можете проверить бэкпорт JSR-310 Джейка Уортона для пакетов java.time.*.

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

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

  • Как уже упоминалось, вы можете использовать библиотеку joda-time-android.
  • Не использовать FastDateTimeZoneProvider предложено @ElijahSh и @plowman. Потому что это обрабатывать смещение DST как стандартное смещение для выбранного часового пояса. Так как это даст "правильные" результаты на сегодня и на оставшуюся половину года, прежде чем произойдет следующий переход на летнее время. Но это определенно даст неверный результат за день до перехода на летнее время и на следующий день после следующего перехода на летнее время.
  • Правильный способ использования часовых поясов системы с JodaTime:

    открытый класс AndroidDateTimeZoneProvider реализует 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);
    }
    

    }

Где 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 такой же, как "Старый", но основанный на android.icu.util.TimeZone вместо.

  • Специально для этого я создал форк Joda Time. Он загружается всего ~29 мс в режиме отладки и ~ 2 мс в режиме выпуска. Также он имеет меньший вес, поскольку не включает базу данных часовых поясов.
Другие вопросы по тегам