Используем static Handler

Ответить
Аватара пользователя
damager82
Администратор
Сообщения: 1383
Зарегистрирован: 07 янв 2012, 11:32
Контактная информация:

Используем static Handler

Сообщение damager82 » 18 апр 2013, 10:46

В уроках используется обычный Handler. Eclipse на это ругается: "This Handler class should be static or leaks might occur " ("Класс Handler должен быть static, иначе могут быть утечки памяти") и он абсолютно прав. Пофиксим это.

Приведу пример static Handler.

[syntax=java5]public class MainActivity extends Activity {

Handler handler;
TextView tvTest;
int cnt = 0;

@Override
protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);
setContentView(R.layout.main);

tvTest = (TextView) findViewById(R.id.tvTest);

handler = new MyHandler(this);
handler.sendEmptyMessageDelayed(0, 1000);
}

void someMethod() {
tvTest.setText("Count = " + cnt++);
handler.sendEmptyMessageDelayed(0, 1000);
}

@Override
protected void onDestroy() {
if (handler != null)
handler.removeCallbacksAndMessages(null);
super.onDestroy();
}

static class MyHandler extends Handler {

WeakReference<MainActivity> wrActivity;

public MyHandler(MainActivity activity) {
wrActivity = new WeakReference<MainActivity>(activity);
}

@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
MainActivity activity = wrActivity.get();
if (activity != null)
activity.someMethod();
}
}
}[/syntax]

Код в целом несложен: на экране просто идет счетчик, который работает через отложенные сообщения Handler.

В чем разница между обычным Handler и static? Чтобы понять это, необходимо знать две вещи:

1) Не static Handler содержит скрытую ссылку на Activity.
2) Сообщения предназначенные для Handler содержат ссылку на сам Handler (а следовательно и на Activity).

Два этих факта могут вызвать утечку памяти. Например, вы в системную очередь на выполнение помещаете (с помощью Handler) сообщение, которое должно выполниться через час. Затем закрываете приложение. Система хочет освободить память и поудалять из нее неиспользуемые объекты. И она удалила бы закрытое Activity и все его содержимое, но не может. Потому что сообщение, которое будет висеть еще час, хранит ссылку на Handler, а Handler хранит ссылку на Activity. В итоге одно сообщение (которое, скорее всего, уже никому не нужно) держит зря целое Activity и не дает системе удалить его из памяти.

Объявляя Handler как static, мы разрубаем цепь ссылок и Handler больше не хранит ссылку на Activity. Но! Handler обычно используется чтобы взаимодействовать с UI, и он должен уметь работать с Activity и его методами. А для этого он должен иметь ссылку на Activity.

Получается легкое противоречие. Handler должен хранить ссылку на Activity, чтобы работать с ним. Но не должен хранить ссылку на Activity, чтобы не вызвать утечку памяти.

Тут выручает Java-механизм слабых ссылок - WeakReference. Слабая ссылка не учитывается системой при очистке памяти. Если система видит, что Activity больше не нужно, но есть Handler, который хранит слабую ссылку на Activity, - слабая ссылка будет проигнорена, Activity будет удалено и утечек памяти не будет.

Мы в конструктор Handler-а передаем Activity и для хранения используем контейнер WeakReference. Метод get позволяет нам получить ссылку обратно из контейнера. А если WeakReference возвращает null, значит объект был удален из памяти, т.е. Activity было закрыто.

Это очень полезный механизм и применим не только к Handler. Рекомендую понять его и использовать для предотвращения утечек.
Добро пожаловать на форум сайта StartAndroid
ИзображениеИзображение

argamidon
Сообщения: 6
Зарегистрирован: 28 окт 2013, 11:13

Re: Используем static Handler

Сообщение argamidon » 27 янв 2014, 10:24

очень полезно. спасибо. вы меня спровоцировали почитать о рефлексии

Sikambr
Сообщения: 2
Зарегистрирован: 28 июл 2014, 14:16

Re: Используем static Handler

Сообщение Sikambr » 28 июл 2014, 14:44

А как восстановить ссылку в MyHandler на MainActivity?
Например, при смене ориентации экрана, Activity пересоздается, а используя wrActivity.get() в MyHandler мы будем ссылаться на null или старый Activity.
А как найти ссылку на вновь созданный MainActivity?

Аватара пользователя
klblk
Сообщения: 1097
Зарегистрирован: 18 окт 2012, 11:17
Откуда: г. Красноярск

Re: Используем static Handler

Сообщение klblk » 29 июл 2014, 14:13

Sikambr писал(а):А как восстановить ссылку в MyHandler на MainActivity?
Например, при смене ориентации экрана, Activity пересоздается, а используя wrActivity.get() в MyHandler мы будем ссылаться на null или старый Activity.
А как найти ссылку на вновь созданный MainActivity?
не должно быть так.
после переворота создается новый MyHandler в onCreate():
[syntax=java]handler = new MyHandler(this);[/syntax]

Sikambr
Сообщения: 2
Зарегистрирован: 28 июл 2014, 14:16

Re: Используем static Handler

Сообщение Sikambr » 30 июл 2014, 12:04

klblk писал(а):не должно быть так.
Вы правы!
Но только не по причине
klblk писал(а):после переворота создается новый MyHandler в onCreate():
handler = new MyHandler(this);
а вот из за этого

Код: Выделить всё

if (handler != null)
    handler.removeCallbacksAndMessages(null);
У меня немного другая ситуация, Handler оборачивается в Message и всё это передается куда-то. Примерно так:

Код: Выделить всё

dialog.setButton(DialogInterface.BUTTON_POSITIVE, "text", Message.obtain(new MyHandler());
а ссылку на MyHandler хранить не хотелось бы.
Единственное, что в голову лезет, так это то, что MainActivity может быть только один, вот его как-то и найти.

Maugli
Сообщения: 1
Зарегистрирован: 18 янв 2015, 17:09

Re: Используем static Handler

Сообщение Maugli » 18 янв 2015, 17:32

У меня обратный вопрос - как сохранить состояние MainActivity и хэндлера при перевороте экрана ?
На примере того же счётчика, т.е. чтобы работа счётчика не прерывалась.
Или же такой подход будет неверным, и функцию счётчика следует возложить на тот же сервис, а из MainActivity при пересоздании - обращаться к сервису ?
Или же всё-таки можно сделать, например через свою реализацию onSaveInstanceState ?

Вопрос именно в идеологии.

Dzam
Сообщения: 6
Зарегистрирован: 27 янв 2015, 23:17

Re: Используем static Handler

Сообщение Dzam » 04 мар 2015, 14:16

Доброго всем дня.
Я реализую Handler в созданном мной классе. Применрно так:

Код: Выделить всё

public class MyClass
{
 private void someMethod1()
 {
 }

 private void someMethod2()
 {
 }

 void processMessage(Message msg)
    {
        switch (msg.what)
        {
            case myclass.MSG1:
            {
                ...
                someMethod1();
                break;
            }

            case myclass.MSG2:
            {
               ...
                someMethod2();
               break;
            }
        }
    }
    
    private final int MSG1    = 1;
    private final int MSG2    = 2;
    private final int MSG3    = 3;

    static class wHandler extends Handler
    {
        WeakReference<MyClass> wrMyClass;
        public wHandler(MyClass myclass)
        {
            wrWheel = new WeakReference<MyClass>(myclass);
        }

        @Override
        public void handleMessage(Message msg)
        {
            super.handleMessage(msg);
            
            MyClass myclass = wrMyClass.get();
            
            myclass.processMessage(msg);
        }
    }
}
Но ведь в таком случае метод processMessage() становится доступным любому, кто создал эксземпляр класса MyClass(). Как от этого защититься?

Аватара пользователя
klblk
Сообщения: 1097
Зарегистрирован: 18 окт 2012, 11:17
Откуда: г. Красноярск

Re: Используем static Handler

Сообщение klblk » 04 мар 2015, 14:45

private void processMessage(Message msg)?

Dzam
Сообщения: 6
Зарегистрирован: 27 янв 2015, 23:17

Re: Используем static Handler

Сообщение Dzam » 04 мар 2015, 23:43

klblk писал(а):private void processMessage(Message msg)?
Да, конечно. Спасибо. Надо же мне было так ступить... :)

Аватара пользователя
klblk
Сообщения: 1097
Зарегистрирован: 18 окт 2012, 11:17
Откуда: г. Красноярск

Re: Используем static Handler

Сообщение klblk » 05 мар 2015, 08:15

У меня тоже вопрос возник по Handler'у. Что если бы его будем создавать не в onCreate, а сразу инициализируем его:
[syntax=java]public class MainActivity extends Activity {
Handler handler = new MyHandler(this);
...
}[/syntax]

Будут ли в данном случае неприятные последствия? Насколько я помню Java в данном случае он создастся в конструкторе MainActivity.

Аватара пользователя
doter.ua
Сообщения: 1106
Зарегистрирован: 23 ноя 2013, 16:08
Откуда: Ukraine

Re: Используем static Handler

Сообщение doter.ua » 05 мар 2015, 11:30

klblk писал(а):У меня тоже вопрос возник по Handler'у. Что если бы его будем создавать не в onCreate, а сразу инициализируем его:
[syntax=java]public class MainActivity extends Activity {
Handler handler = new MyHandler(this);
...
}[/syntax]

Будут ли в данном случае неприятные последствия? Насколько я помню Java в данном случае он создастся в конструкторе MainActivity.
http://stackoverflow.com/questions/2079 ... ng-handler
Семь раз отмерь - поставь студию.
Эклипс не студия, ошибка вылетит - не исправишь.
Скажи мне кто твой друг, и оба поставили студию.
Студия - свет, а эклипс - тьма.

onesoft
Сообщения: 4
Зарегистрирован: 27 фев 2015, 15:11

Re: Используем static Handler

Сообщение onesoft » 25 мар 2015, 17:02

Жаль, что вы не привели пример кода с использованием мягких ссылок.
На всякий случай приведу код урока, который у меня получился в итоге.

Код: Выделить всё

package ru.mytest.p0801_handler;

import android.os.Handler;
import android.os.Message;
import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import java.lang.ref.WeakReference;
import java.util.concurrent.TimeUnit;

public class MainActivity extends ActionBarActivity {
    final String LOG_TAG = "myLog";
    TextView tvInfo;
    Button btnStart;
    static myHandler mHandler;

    private static class myHandler extends Handler {
        private WeakReference<MainActivity> mTarget;

        myHandler(MainActivity target) {
            mTarget = new WeakReference<MainActivity>(target);
        }

        public void setTarget(MainActivity target) {
            mTarget.clear();
            mTarget = new WeakReference<MainActivity>(target);
        }

        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = mTarget.get();
            // обновляем TextView
            activity.tvInfo.setText("Закачано файлов: " + msg.what);
            if (msg.what == 10) activity.btnStart.setEnabled(true);
        }
    }

    /** Called when the activity is first created. */
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        tvInfo = (TextView) findViewById(R.id.tvInfo);
        btnStart = (Button) findViewById(R.id.btnStart);
        if(mHandler == null) mHandler = new myHandler(this);
        else mHandler.setTarget(this);
    }

    public void onclick(View v) {
        switch (v.getId()) {
            case R.id.btnStart:
                btnStart.setEnabled(false);
                Thread t = new Thread(new Runnable() {
                    public void run() {
                        for (int i = 1; i <= 10; i++) {
                            // долгий процесс
                            downloadFile();
                            mHandler.sendEmptyMessage(i);
                            // пишем лог
                            Log.d(LOG_TAG, "i = " + i);
                        }
                    }
                });
                t.start();
                break;
            case R.id.btnTest:
                Log.d(LOG_TAG, "test");
                break;
            default:
                break;
        }
    }

    void downloadFile() {
        // пауза - 1 секунда
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Аватара пользователя
Mikhail_dev
Сообщения: 2386
Зарегистрирован: 09 янв 2012, 14:45
Откуда: Самара

Re: Используем static Handler

Сообщение Mikhail_dev » 25 мар 2015, 22:05

Мягкие ссылки? Вы серьёзно? Тут народ кнопочку боится в XML написать, а базовую Java знает процентов 30 от силы, а тут мягкие ссылки. К сожалению (хотя наверное к счастью =) ), их никто особо и не знает. Об этой теме я даже заметку написал, в подписи. Первая.

Аватара пользователя
Atetc
Сообщения: 45
Зарегистрирован: 02 май 2014, 13:13
Откуда: Уфа
Контактная информация:

Re: Используем static Handler

Сообщение Atetc » 12 май 2015, 07:38

"This Handler class should be static or leaks might occur "
Обхожу это так:

Код: Выделить всё

private Handler mHandler = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            if (msg.what == MSG) {
                // your code
                return true;
            }
            return false;
        }
    });
Русскоязычный чат Android разработчиков https://gitter.im/rus-speaking/android

K_Vladimir
Сообщения: 36
Зарегистрирован: 28 июн 2015, 03:13

Re: Используем static Handler

Сообщение K_Vladimir » 24 июл 2015, 18:57

onesoft писал(а):Жаль, что вы не привели пример кода с использованием мягких ссылок.
На всякий случай приведу код урока, который у меня получился в итоге.

Код: Выделить всё

package ru.mytest.p0801_handler;

import android.os.Handler;
import android.os.Message;
import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import java.lang.ref.WeakReference;
import java.util.concurrent.TimeUnit;

public class MainActivity extends ActionBarActivity {
    final String LOG_TAG = "myLog";
    TextView tvInfo;
    Button btnStart;
    static myHandler mHandler;

    private static class myHandler extends Handler {
        private WeakReference<MainActivity> mTarget;

        myHandler(MainActivity target) {
            mTarget = new WeakReference<MainActivity>(target);
        }

        public void setTarget(MainActivity target) {
            mTarget.clear();
            mTarget = new WeakReference<MainActivity>(target);
        }

        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = mTarget.get();
            // обновляем TextView
            activity.tvInfo.setText("Закачано файлов: " + msg.what);
            if (msg.what == 10) activity.btnStart.setEnabled(true);
        }
    }

    /** Called when the activity is first created. */
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        tvInfo = (TextView) findViewById(R.id.tvInfo);
        btnStart = (Button) findViewById(R.id.btnStart);
        if(mHandler == null) mHandler = new myHandler(this);
        else mHandler.setTarget(this);
    }

    public void onclick(View v) {
        switch (v.getId()) {
            case R.id.btnStart:
                btnStart.setEnabled(false);
                Thread t = new Thread(new Runnable() {
                    public void run() {
                        for (int i = 1; i <= 10; i++) {
                            // долгий процесс
                            downloadFile();
                            mHandler.sendEmptyMessage(i);
                            // пишем лог
                            Log.d(LOG_TAG, "i = " + i);
                        }
                    }
                });
                t.start();
                break;
            case R.id.btnTest:
                Log.d(LOG_TAG, "test");
                break;
            default:
                break;
        }
    }

    void downloadFile() {
        // пауза - 1 секунда
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
Спасибо. Ваш код очень помог.
Но не совсем понятно:
1. В каком случае произойдёт вызов mHandler.setTarget(this); ??? Может ли вообще такое случиться и в какой ситуации?

Код: Выделить всё

if(mHandler == null) mHandler = new myHandler(this);
        else mHandler.setTarget(this);
2. Здесь на форуме, в главном примере, есть переопределение метода onDestroy:
@Override
protected void onDestroy() {
if (handler != null)
handler.removeCallbacksAndMessages(null);
super.onDestroy();
}
Нужно ли дописать его в данном случае?
Я так полагаю, что метод handler.removeCallbacksAndMessages(null); позволяет нам удалять только сообщения, но это не позволяет решить всю проблему утечки памяти, т.к. ссылки на объекты остаются??

SDN
Сообщения: 1
Зарегистрирован: 26 июл 2015, 13:43

Re: Используем static Handler

Сообщение SDN » 26 июл 2015, 14:24

Друзья, извините, если вопрос глупый. Пытаюсь работать с потоками из фрагмента. Нужны ли в таком случае слабые ссылки? Если да, то нужно ли передавать в статический handler Activity (и если тоже "да", то как?).

Заранее спасибо.

Аватара пользователя
Kirill
Сообщения: 19
Зарегистрирован: 09 сен 2015, 13:53

Re: Используем static Handler

Сообщение Kirill » 16 сен 2015, 00:45

K_Vladimir писал(а): 2. Здесь на форуме, в главном примере, есть переопределение метода onDestroy:
@Override
protected void onDestroy() {
if (handler != null)
handler.removeCallbacksAndMessages(null);
super.onDestroy();
}
Нужно ли дописать его в данном случае?
Я так полагаю, что метод handler.removeCallbacksAndMessages(null); позволяет нам удалять только сообщения, но это не позволяет решить всю проблему утечки памяти, т.к. ссылки на объекты остаются??
Мы удалили все сообщения из очереди в хендлере - таким образом хендлер готов чтобы его очистил GC.
Если хендлер создавать в статическом внутреннем классе или в отдельном классе тогда он не имеет ссылки на Активити, а значит не будет удерживать его от очистки GC.

Аватара пользователя
Kirill
Сообщения: 19
Зарегистрирован: 09 сен 2015, 13:53

Re: Используем static Handler

Сообщение Kirill » 16 сен 2015, 00:56

итак я для себя выделил такие утечки:

1. Статическая переменная класса Activity содержит в себе ссылку на контекст.
Пример: http://android-developers.blogspot.ru/2 ... leaks.html Допустим у вас в onCreate() создается некий Drawable, который достаточно большой и вы хотите чтобы в случае смены ориентации экрана (при смене вызывается onDestroy() и onCreate() каждый раз) его не создавать заново, а взять из статической переменной класса. Все ок, но если вы связали этот Drawable например с каким-то View, то в статическом поле будет сидеть drawable c уже ссылкой на весь Активити, т.к. в каждом View есть ссылка на Контекст. И этот весь старый Активити сборщик мусора не уберет.
Решение: http://stackoverflow.com/questions/6567 ... on-android Решается это тем, что в методе оnDestroy делается unbind, то есть обнуляется связь у Drawable, чтобы он не тянул за собой весь предыдущий активити.

2. Утечка в случае с хендлером. Если хендлер объявить как внутренний класс в Активити, то может образоваться утечка, т.к. внутренний класс содержит скрытую ссылку на внешний класс (Активити) по этому Активити не очистится GC*, а так как в Хендлере будут сообщения которые ожидают своей отработки (а они не могут потому что Активити давно уже закрыта) то и Хендлер не очистится GC.
Пример и решение здесь: http://www.androiddesignpatterns.com/20 ... -leak.html Если коротко то, надо хендлер инициализировать как вложенный статический класс или как отдельный класс. А если необходимо в него передать все таки ссылку на контекст - передать ее через класс-переходник WeakReference что позволит удалить Активити даже при наличии слабой ссылки.

3. внутренние классы - все то же самое как и в п. 2 (внутренние классы содержат ссылки на внешние классы)
Здесь как еще один пример могут послужить анонимные внутренние классы ...new Runnable(){... или new Thread(){ они тоже содержат ссылку на внешний класс и если этот поток явно не потушить то он будет держать Активити пока ОС не вырубит все приложение.
http://www.androiddesignpatterns.com/20 ... leaks.html Здесь в статье показывается на диаграмме как забивается память при таких открываниях потока если десять раз поменять ориентацию экрана (покрутить телефон).
Решение: в этой же статье. Если коротко - определйте свои методы Thread и устанавливайте метод close() котторый бы убивал этот поток. В методе onDestroy() вызывайе myThread.close();

Единственное что мне здесь не понятно: в статье сказано что
After each configuration change, the Android system creates a new Activity and leaves the old one behind to be garbage collected. However, the thread holds an implicit reference to the old Activity and prevents it from ever being reclaimed. As a result, each new Activity is leaked and all resources associated with them are never able to be reclaimed. то есть каждый раз создается новое Активити, а старые из-за скрытой ссылки никогда уже не будут очищенны (разумеется пока работает приложение).
Так вот вопрос: когда поток отработает - он должен сам обнулиться, а значит и ссылка на Активити пропадет - следовательно Активити сможет быть очищен GC, так ли это или уже все, поток завис и не закончится никогда, а значит и Активити будет висеть до последнего?

* - GC - Garbage Collection ( http://www.oracle.com/webfolder/technet ... index.html )

stoptalking
Сообщения: 1
Зарегистрирован: 10 окт 2017, 14:05

Re: Используем static Handler

Сообщение stoptalking » 10 окт 2017, 14:28

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

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

Ответить