본문 바로가기
Programming/Android

Avoiding Android memory leak

by ciwhiz 2020. 5. 11.

아래 글을 정리한 것

www.android.jlelse.eu/9-ways-to-avoid-memory-leaks-in-android-b6d81648e35e

- Memory Leak 을 유발하는 것들과 Solutions.

원인 1:  Broadcast Receivers

Activity 내에 local Broadcast Receiver를 register 해두었는데, unregister를 하지 않으면,
Activity가 종료된 후에도 Broadcast receiver가 activity 에 대한 참조를 가지고 있어서 leak 발생.

해결책) 항상 onStop에서 unregister()를 call 해라.

**주의할점: Broadcast receiver를 onCreate()에서 register했다면 앱이 background에서 onResume()으로 돌아왔을때,

정상적으로 register 되지않으므로 항상 register는 onStart() 또는 onResume()에서 해주자. onStop에서는 unregister()!

(참조코드)

더보기
public class BroadcastReceiverLeakActivity extends AppCompatActivity {

    private BroadcastReceiver broadcastReceiver;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_first);
    }

    private void registerBroadCastReceiver() {
        broadcastReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                //your receiver code goes here!
            }
        };
        registerReceiver(broadcastReceiver, new IntentFilter("SmsMessage.intent.MAIN"));
    }
    
    
    @Override
    protected void onStart() {
        super.onStart();
        registerBroadCastReceiver();
    }    


    @Override
    protected void onStop() {
        super.onStop();

        /*
         * Uncomment this line in order to avoid memory leak.
         * You need to unregister the broadcast receiver since the broadcast receiver keeps a reference of the activity.
         * Now when its time for your Activity to die, the Android framework will call onDestroy() on it
         * but the garbage collector will not be able to remove the instance from memory because the broadcastReceiver
         * is still holding a strong reference to it.
         * */

        if(broadcastReceiver != null) {
            unregisterReceiver(broadcastReceiver);
        }
    }
}

 

원인 2: Static Activity or View Reference

만약 textview를 static으로 선언했다면(이유야 어쨌든..) 이 static field가 activity또는 view를 직간접적으로 참조한다면,
역시 leak 발생.
해결책) activity, view, context 에 대해서는 절대 static 변수를 사용하지 마라.

더보기
public class StaticReferenceLeakActivity extends AppCompatActivity {

    /*  
     * This is a bad idea! 
     */
    private static TextView textView;
    private static Activity activity;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_first);
        
        
        textView = findViewById(R.id.activity_text);
        textView.setText("Bad Idea!");
           
        activity = this;
    }
}

 

원인 3: Singleton Class Reference

아래와 같은 Singleton Class를 정의하고, local storage로부터 파일 몇개를 fetch하기 위해 context를 전달할 필요가 있다고 가정해보자. 
해결책)
option 1 : singleton class 에 activity context 대신 application context()를 넘겨라.
option 2 : 그래도 activity context 를 넘겨야 한다면, activity 가 destroy 될 때,
              확실하게 single class에 넘긴 context가 null로 set 되었는지 확인해라.

더보기
public class SingletonLeakExampleActivity extends AppCompatActivity {

    private SingletonSampleClass singletonSampleClass;
    
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
    /* 
     * Option 1: Do not pass activity context to the Singleton class. 
     * Instead pass application Context
     */      
        singletonSampleClass = SingletonSampleClass.getInstance(this);
    }
  
    
   @Override
   protected void onDestroy() {
        super.onDestroy();

    /* 
     * Option 2: Unregister the singleton class here 
     * i.e. if you pass activity context to the Singleton class, 
     * then ensure that when the activity is destroyed, 
     *  the context in the singleton class is set to null.
     */
     singletonSampleClass.onDestroy();
   }
}
public class SingletonSampleClass {
  
    private Context context;
    private static SingletonSampleClass instance;
  
    private SingletonSampleClass(Context context) {
        this.context = context;
    }

    public synchronized static SingletonSampleClass getInstance(Context context) {
        if (instance == null) instance = new SingletonSampleClass(context);
        return instance;
    }
  
    public void onDestroy() {
       if(context != null) {
          context = null; 
       }
    }
}

 

원인 4: Inner Class Reference

아래와 같이 LeakyClass.java 라는 inner class를 정의했고, 다른 activity 에 redirect 하기위해 activity를 넘겨야할 필요가 있다고 가정해보자. 
아래코드에서 첫번째 실수 inner class 변수 의 static 선언. -> non-static class로 변경해라.

두번째 실수 1) inner class 에서는 class 변수를 생성하지 마라, 2) inner class에 activity instance를 전달하지 마라.

더보기
public class InnerClassReferenceLeakActivity extends AppCompatActivity {

  /* 
   * Mistake Number 1: 
   * Never create a static variable of an inner class
   * Fix I:
   * private LeakyClass leakyClass;
   */
  private static LeakyClass leakyClass;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_first);
        
        new LeakyClass(this).redirectToSecondScreen();

        /*
         * Inner class is defined here
         * */
         leakyClass = new LeakyClass(this);
         leakyClass.redirectToSecondScreen();
    }
    
  /* 
   * Mistake Number 2: 
   * 1. Never create a inner variable of an inner class
   * 2. Never pass an instance of the activity to the inner class
   */       
    private class LeakyClass {
        
        private Activity activity;
        public LeakyClass(Activity activity) {
            this.activity = activity;
        }
        
        public void redirectToSecondScreen() {
            this.activity.startActivity(new Intent(activity, SecondActivity.class));
        }
    }
}

해결책) 
option 1: 이미 얘기한 적있지만 절대 inner class 에 static 변수 생성하지 마라.

option 2: Class 는 static 으로 set 해야한다. anonymous class instance는 "static"으로 선언되면,  outer class에 대해 암묵적인 참조를 갖지 않는다. 

option 3: 어떤 View/Activity 에 대해서 WeakReference를 사용해라. Garbage Collector는 WeakReference로 가리키는 객체에 대해서만 collect 할 수 있다. 
아래는 솔루션 코드 

더보기
public class InnerClassReferenceLeakActivity extends AppCompatActivity {

  /* 
   * Mistake Number 1: 
   * Never create a static variable of an inner class
   * Fix I:
   */
  private LeakyClass leakyClass;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_first);
        
        new LeakyClass(this).redirectToSecondScreen();

        /*
         * Inner class is defined here
         * */
         leakyClass = new LeakyClass(this);
         leakyClass.redirectToSecondScreen();
    }
  
  
    /*  
     * How to fix the above class:
     * Fix memory leaks:
     * Option 1: The class should be set to static
     * Explanation: Instances of anonymous classes do not hold an implicit reference to their outer class 
     * when they are "static".
     *
     * Option 2: Use a weakReference of the textview or any view/activity for that matter
     * Explanation: Weak References: Garbage collector can collect an object if only weak references 
     * are pointing towards it.
     * */
    private static class LeakyClass {
        
        private final WeakReference<Activity> messageViewReference;
        public LeakyClass(Activity activity) {
            this.activity = new WeakReference<>(activity);
        }
        
        public void redirectToSecondScreen() {
            Activity activity = messageViewReference.get();
            if(activity != null) {
               activity.startActivity(new Intent(activity, SecondActivity.class));
            }
        }
    }  
}

 

원인 5: Anonymous Class Reference

위의 원인4와 똑같다. Anonymous Class라는 점만 참고해서 보자. 아래는 Solution Code
해결책) 1. inner class에 static 변수 안쓰기. 2. inner class는 static class로 3. weakReference로 activity 참조하기.

더보기
public class AnonymousClassReferenceLeakActivity extends AppCompatActivity {

    private TextView textView;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_first);


        textView = findViewById(R.id.activity_text);
        textView.setText(getString(R.string.text_inner_class_1));
        findViewById(R.id.activity_dialog_btn).setVisibility(View.INVISIBLE);

        /*
         * Runnable class is defined here
         * */
         new Thread(new LeakyRunnable(textView)).start();
    }



    private static class LeakyRunnable implements Runnable {

        private final WeakReference<TextView> messageViewReference;
        private LeakyRunnable(TextView textView) {
            this.messageViewReference = new WeakReference<>(textView);
        }

        @Override
        public void run() {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            TextView textView = messageViewReference.get();
            if(textView != null) {
                textView.setText("Runnable class has completed its work");
            }
        }
    }
}

 

원인 6: AsyncTask Reference

onPostExecute()에서 textView를 업데이트하기 위해 사용된 string 변수를 asyncTask를 사용해서 get 해오고 있다고 

가정해보자.
3가지 문제점과 해결책 :
1) activity 안에서 class를 참조하지 마라. 만약 꼭 해야한다면, inner class를 static으로 set 해야하며 그 inner class의 부모 activity class에 대해 어떤 암묵적 참조도 하지 않도록 static inner class로 해줘야한다. 

2) activity 가 destory될 때 항상 asyncTask를 cancel 해줘라. 안그러면 activity가 destroy되어도 asyncTask는 계속 실행중일 것이기 때문이다.

3) 절대 asyncTask 내에서 activity로 부터 View에대한 직접적인 reference를 하지 마라. weakReference를 사용해라.

더보기

<문제코드>

public class AsyncTaskReferenceLeakActivity extends AppCompatActivity {

    private TextView textView;
    private BackgroundTask backgroundTask;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_first);

        /*
         * Executing AsyncTask here!
         * */
        backgroundTask = new BackgroundTask(textView);
        backgroundTask.execute();
    }

    /*
     * Couple of things we should NEVER do here:
     * Mistake number 1. NEVER reference a class inside the activity. 
     * If we definitely need to, 
     * we should set the class as static as static inner classes don’t hold
     *    any implicit reference to its parent activity class
     * Mistake number 2. We should always cancel the asyncTask when activity is destroyed. 
     * This is because the asyncTask will still be executing even if the activity
     *    is destroyed.
     * Mistake number 3. Never use a direct reference of a view from acitivty 
     * inside an asynctask.
     * */
     private class BackgroundTask extends AsyncTask<Void, Void, String> {    
        @Override
        protected String doInBackground(Void... voids) {

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "The task is completed!";
        }

        @Override
        protected void onPostExecute(String s) {
            super.onPostExecute(s);
            textView.setText(s);
        }
    }
}
더보기

Solution 코드

public class AsyncTaskReferenceLeakActivity extends AppCompatActivity {

    private TextView textView;
    private BackgroundTask backgroundTask;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_first);

        /*
         * Executing AsyncTask here!
         * */
        backgroundTask = new BackgroundTask(textView);
        backgroundTask.execute();
    }


    /*
     * Fix number 1
     * */
    private static class BackgroundTask extends AsyncTask<Void, Void, String> {

        private final WeakReference<TextView> messageViewReference;
        private BackgroundTask(TextView textView) {
            this.messageViewReference = new WeakReference<>(textView);
        }


        @Override
        protected String doInBackground(Void... voids) {

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "The task is completed!";
        }

        @Override
        protected void onPostExecute(String s) {
            super.onPostExecute(s);
          /*
           * Fix number 3
           * */          
            TextView textView = messageViewReference.get();
            if(textView != null) {
                textView.setText(s);
            }
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        /*
         * Fix number 2
         * */
        if(backgroundTask != null) {
            backgroundTask.cancel(true);
        }
    }
}

 

원인 7: Handler Reference

5초 후에 새로운 스크린으로 redirect 하는 Handler를 사용하고 있다고 가정해보자.

문제점과 해결책
1) activity 내에서 class를 참조하지 말자. 정 필요하면, static 으로 class를 선언하자. 왜냐면, Handler 는 main Thread에서 인스턴스화되어서  Looper의 message queue와 연관되어있기 때문이다. message queue의 message들은 Handler 에 참조를 걸고 있고, 그래서 마침내 Looper가 message를 처리할 때 framework은 Handler의 handleMessage(msg)를 call할 수 있는 것이다. 

2) 바로 참조하지 말고 activity 의 weak reference를 사용하자!

더보기
public class HandlersReferenceLeakActivity extends AppCompatActivity {

    private TextView textView;

    /*
     * Mistake Number 1
     * */
     private Handler leakyHandler = new Handler();


    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_first);

        /*
         * Mistake Number 2
         * */
        leakyHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                textView.setText(getString(R.string.text_handler_1));
            }
        }, 5000);
    }

Solution code

더보기
public class HandlersReferenceLeakActivity extends AppCompatActivity {

    private TextView textView;

    /*
     * Fix number I
     * */
    private final LeakyHandler leakyHandler = new LeakyHandler(this);

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_first);

        leakyHandler.postDelayed(leakyRunnable, 5000);
    }

    /*
     * Fix number II - define as static
     * */
    private static class LeakyHandler extends Handler {
      
    /*
     * Fix number III - Use WeakReferences
     * */      
        private WeakReference<HandlersReferenceLeakActivity> weakReference;
        public LeakyHandler(HandlersReferenceLeakActivity activity) {
            weakReference = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            HandlersReferenceLeakActivity activity = weakReference.get();
            if (activity != null) {
                activity.textView.setText(activity.getString(R.string.text_handler_2));
            }
        }
    }

    private static final Runnable leakyRunnable = new Runnable() {
        @Override
        public void run() { /* ... */ }
    }

 

 

원인 8: Thread Reference

Thread와 TimerTask class 양쪽에서 우리는 이같은 실수를 반복할수 있다. 
문제점 1: static 변수 사용

문제점 2: non-static anonymous class 사용.

더보기
public class ThreadReferenceLeakActivity extends AppCompatActivity {

    /*  
     * Mistake Number 1: Do not use static variables
     * */    
    private static LeakyThread thread;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_first);

        createThread();
        redirectToNewScreen();
    }


    private void createThread() {
        thread = new LeakyThread();
        thread.start();
    }

    private void redirectToNewScreen() {
        startActivity(new Intent(this, SecondActivity.class));
    }


    /*
     * Mistake Number 2: Non-static anonymous classes hold an 
     * implicit reference to their enclosing class.
     * */
    private class LeakyThread extends Thread {
        @Override
        public void run() {
            while (true) {
            }
        }
    }

해결책) 1. static 변수 제거. 2. activity destroy 할 때 thread 를 kill 해준다. 3. static inner class로 Thread class 사용

더보기
public class ThreadReferenceLeakActivity extends AppCompatActivity {

    /*
     * FIX I: make variable non static
     * */
    private LeakyThread leakyThread = new LeakyThread();

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_first);

        createThread();
        redirectToNewScreen();
    }


    private void createThread() {
        leakyThread.start();
    }

    private void redirectToNewScreen() {
        startActivity(new Intent(this, SecondActivity.class));
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // FIX II: kill the thread
        leakyThread.interrupt();
    }


    /*
     * Fix III: Make thread static
     * */
    private static class LeakyThread extends Thread {
        @Override
        public void run() {
            while (!isInterrupted()) {
            }
        }
    }
}

 

원인 9: TimerTask Reference

위의 Thread를 사용한 timer와 동일한 원리를 적용하면 된다.

더보기
public class TimerTaskReferenceLeakActivity extends Activity {

    private CountDownTimer countDownTimer;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_first);

        startTimer();
    }

    /*  
     * Mistake 1: Cancel Timer is never called 
     * even though activity might be completed
     * */
    public void cancelTimer() {
        if(countDownTimer != null) countDownTimer.cancel();
    }

    
    private void startTimer() {
        countDownTimer = new CountDownTimer(1000, 1000) {
            @Override
            public void onTick(long millisUntilFinished) {
                final int secondsRemaining = (int) (millisUntilFinished / 1000);
                //update UI
            }

            @Override
            public void onFinish() {
                //handle onFinish
            }
        };
        countDownTimer.start();
    }
}

Solution Code : onDestroy()에서 timer를 cancel 해줘라.

더보기
public class TimerTaskReferenceLeakActivity extends Activity {

    private CountDownTimer countDownTimer;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_first);

        startTimer();
    }


    public void cancelTimer() {
        if(countDownTimer != null) countDownTimer.cancel();
    }


    private void startTimer() {
        countDownTimer = new CountDownTimer(1000, 1000) {
            @Override
            public void onTick(long millisUntilFinished) {
                final int secondsRemaining = (int) (millisUntilFinished / 1000);
                //update UI
            }

            @Override
            public void onFinish() {
                //handle onFinish
            }
        };
        countDownTimer.start();
    }
  
  
    /*
     * Fix 1: Cancel Timer when 
     * activity might be completed
     * */  
   @Override
    protected void onDestroy() {
        super.onDestroy();
        cancelTimer();
    }
}

 

기본적으로 요약을 해보자

1. 가능하면 activity context 말고 applicationContext()를 사용해라 정 activity context를 사용해야하거들랑,

    activity destroy() 시점에 class로 전달한 context가 null로 set 되었는지 확인시켜줘라.

2. activity context 또는 view 를 static 변수로 사용하지 마라.

3. activity 안에서 class를 참조하지 마라. 정 필요하면 그 class를 static으로 선언해서 써라.       

   thread/handler/timer/asyncTask 얘들 전부다 !!

4. 항상 Broadcast Receiver와 timer는 activity 종료 시점에 unregister 해줘야한다.
   asyncTask나 Thread는 cancel()시켜줘야한다.

5. activity 또는 view를 참조할 때는 항상 weakReference를 사용해라.