воскресенье, 26 декабря 2010 г.

Синглетоны на Android. Как не стукнуться клювом об дерево...

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

Синглетон можно реализовать разными способами. Обычно его реализуют так, чтобы он был доступен в любой точке приложения. Например, в виде класса, в котором все функции статические. Или в виде обычного класса со статичной функцией GetInstance() и приватным конструктором. Главное, учесть при реализации синглетона особенности языка программирования и платформы. Иначе ваше приложение рискует погрязнуть в ворохе непонятных и трудноуловимых багов. Я уже как-то писал об одной такой нетривиальной особенности реализации синглетона на CLI/C++. Оказалось, что и в реализации синглетона под Android есть подобная нетривиальная особенность.

Простейшая и вроде бы очевидная реализация синглетона под Android неверна. Пример:
public final class MySingleton {
   private static MyClass m_A = new MyClass();
   private static MyClass getA() {
      return m_A;
   }
}
Такой синглетон заработает. И может работать очень долго. Но в один прекрасный момент, вы рискуете получить null в getA(). Со всеми вытекающими последствиями...

Причина проста. Вы инициализируете статический класс синглетона в контексте Activity. Если в вашем приложении несколько activity, то синглетон доступен в каждой из них. Но если первоначальная activity, в которой создавался синглетон, будет уничтожена, то и синглетон тоже будет уничтожен. После этого, в других activity вы будете получать null вместо A. А ведь под андроидом подобное уничтожение activity дело обычное..

Проблема эта известна. Имеются решения:
На мой взгляд, второй способ проще. Если нет каких-либо особых противопоказаний, можно использовать именно его. Синглетон, унаследованный от Application, будет выглядеть так:
public final class MySingleton extends Application {
   private MyClass m_A = new MyClass();

  @Override
  public void onCreate() {
     super.onCreate();
     //инициализация объектов синглетона
     Resources r = this.getResources();      
     ...
  }

  private MyClass getA() {
    return m_A;
  } 
}
Нужно не забыть немножко подправить манифест, указав класс синглетона в качестве класса приложения:
...
   
   ...
MySingleton стал нестатическим классом. Тем не менее, обращаться к нему можно отовсюду, где имеется context - из виджета, из activity и т.п.
//виджет:
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
  MySingleton s = (MySingleton)context.getApplication(); 
}
//Activity:
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  MySingleton s = (MySingleton)this.getApplication(); 
}
Не так удобно, как раньше, но вполне терпимо.

Выводы

Если ваш приложение под Android стало вдруг временами зависать или виджет стал отваливаться через некоторое время после начала работы, проверьте: а правильно ли у вас реализованы синглетоны?

6 комментариев:

  1. "Но если первоначальная activity, в которой создавался синглетон, будет уничтожена, то и синглетон тоже будет уничтожен."

    Вы не могли бы пояснить, почему так?

    ОтветитьУдалить
  2. Это архитектурная особенность Android OS. Подробнее можно почитать здесь. Цитирую: "Lifetime of a static variable: A static variable comes into existence when a class is loaded by the JVM and dies when the class is unloaded.

    So if you create an android application and initialize a static variable, it will remain in the JVM until one of the following happens:
    1. the class is unloaded
    2. the JVM shuts down
    3. the process dies

    Note that the value of the static variable will persist when you switch to a different activity of another application and none of the above three happens. Should any of the above three happen the static will lose its value."

    и далее

    "Well, the Singleton pattern is also based on using static variables so actually you would be in the same position. While the static approach may work most of the times, it may happen that in some cases when memory is full and another activity takes the foreground before your application moves to its next screen, your activity's process could be killed and you lose the static values."

    ОтветитьУдалить
  3. Подскажите, что может быть не так, сделал все по инструкции, возникает
    java.lang.ClassCastException: android.app.Application

    В строке
    MySingleton s = (MySingleton)this.getApplication();

    ОтветитьУдалить
  4. Вы точно в манифесте не забыли android:name=".MySingleton" в application прописать?

    Важный момент: тег application в манифесте должен быть один единственный.

    Проверьте, что возвращают:

    this.getApplicationContext() instanceof MySingleton
    и
    this.getApplication() instanceof MySingleton

    возможно следует использовать getApplicationContext.

    Вот здесь
    http://stackoverflow.com/questions/708012/android-how-to-declare-global-variables
    в комментариях пишут, что иногда имя с точкой не срабатывает, и приходится писать
    android:name="MySingleton"

    Лично я пишу всегда с точкой.

    Больше идей нет, надо исходники смотреть.

    ОтветитьУдалить
  5. Здесь сказано, что "унаследовать класс синглетона от класса Application" не выход.
    http://habrahabr.ru/qa/12794/

    ОтветитьУдалить
  6. Автор не приводит подробностей реализации, а просто констатирует факт - "не работает". Разобраться, чем вызваны описанные им проблемы без кода не возможно. Подозреваю, дело там совсем не в классе Application.

    Лично мой опыт показывает, что метод прекрасно работает на практике.

    Возможно, автор не учитывает возможность перезапуска приложения. Вот что написано в
    Android Application Framework FAQ: Even while an application appears to continue running, the system may choose to kill its process and restart it later. If you have data that you need to persist from one activity invocation to the next, you need to represent that data as state that gets saved by an activity when it is informed that it might go away.

    For sharing complex persistent user-defined objects, the following approaches are recommended:...


    Другими словами, не стоит использовать глобальный синглетон для хранения данных. Синглетон идеально подходит для хранения объектов, использующих данные (всякие кеши, менеджеры и т.д.). При пере-запуске приложения эти объекты будут пере-создаваться. И у них должна быть возможность загрузить необходимые для работы данные из какого-то внешнего источника.

    ОтветитьУдалить