반응형
[Android/Refactoring] Memory Leak - 2. 자주 발생하는 안드로이드 메모리 누수 방지하기
이어지는 글
2023.07.21 - [IT/Android] - [Android/Refactoring] Memory Leak - 1. 안드로이드 앱에서의 메모리 누수 찾기
[Android/Refactoring] Memory Leak - 1. 안드로이드 앱에서의 메모리 누수 찾기
배경 개발을 해놓고 나니 메모리가 이미지를 로딩하는 등 앱을 지속적으로 실행시키면서 메모리가 슬금슬금 증가하는 모습을 안드로이드 프로파일러를 통해서 발견했다. 앱을 사용하면서 지속
hodie.tistory.com
Static References
Activities,Fragments,views,context에 대해static references를 과도하게 사용하지 않아야 한다.Static 객체는Application이 실행되는 동안 살아있지만,View(또는Context)에 대한Static Reference는 제 때 지워지지 않을 확률이 높기 때문이다.Application이Class를JVM에 로드하면 해당static member들이 메모리 할당되고 증가된 lifespan에 의해Class가garbage collection의 대상이 될 때 까지 메모리에 남아있게 된다.
예시
Static variable인TextView를 선언하고, 다음과 같이TextView의 값을 업데이트하는 클래스가 있고 이를onCreate() 메소드에서 실행한다고 하자
private static TextView textView;
private void changeText() {
textView = (TextView) findViewById(R.id.testview);
textView.setText("Update Hello World greetings!");
}
- 해당
Static View는changeText()클래스를 실행하는 동안Activity의 일부분이다. 따라서,특정 Activity에 대한static reference가 hold된다. 해당Static View는Activity의Lifecycle이후에도 실행되게 된다. - 따라서, 이경우 여전히
Activity에 대한 참조를view가 가지고 있기 때문에Activity가garbage collected되지 않는다. 이는 곧메모리 누수가 발생함을 의미한다. Static은 주어진 클래스에서 동일한variable를 모든 객체에서 사용하기 위해 사용된다. 따라서,View가 statically 유지 되어야만 하는 경우에는 onDestroy()에서 해당 reference를 null 처리하면 메모리 누수를 막을 수 있다.- 이 경우,
activity가destroy되면,static reference도destroyed되기 때문에,activity가 garbage collected될 수 있다.
- 이 경우,
@Override
protected void onDestroy() {
super.onDestroy();
textView = null;
}
- 위처럼
Destroy할 때null 처리하여 메모리 누수를 막을 수도 있지만 가장 좋은 것은, 가능한 static keyword를 사용하지 않고 선언하는 것이다.
예시 2 (Static Context)
private static Context mContext;
...
.. onCreate() {
mContext = this;
}
- 위처럼
onCreate()메소드 안에서Activity Context를Static reference하려는 경우acitivty에 대한leak을 발생시킬 수 있으며, 안드로이드 스튜디오도 이를 경고할 것이다. - 그러나,
static field에 두어야만 한다면,virtual/weak reference를 사용해hold할 수 있다.
private static WeakReference<Context> mContext;
- 따라서
onCreate()메소드 내부에서
mContext = new WeakReference<> (this);
를 사용할 수 있고,onDestroy()메소드 내부에서null 처리해 사용할 수 있다.
mContext = new WeakReference<> (this);
Binding leaks
ViewBinding을 사용하는 경우에onDestroyView()lifecycle 메소드에서binding = null처리한다.
Context
Context는Application에서 다른 components간에 communicate가 가능하도록 allow해준다.- 이를 통해, 새로운 객체를 만들거나, 리소스(layout, image, string 등)에 접근할 수 있도록 해주며, activity, database와 기기의 내부 저장소를 launch할 수 있도록 해준다.
- 따라서
Context에 Access할 수 있는 방법에도 여러 방법이 있는데,this와getApplicationContext가 있다. Context는 다른component에 대한 참조를 유지한다. 따라서,context를 사용하는 방법은application에서 중요하다.
예시
public class SingletonClass {
private static SingletonClass singletonClassInstance;
private Context context;
private SingletonClass(Context context){
this.context = context;
}
public static void singletonClassInstance(Context context){
if (singletonClassInstance == null){
singletonClassInstance = new SingletonClass(context);
}
}
}
- 해당 예시에서,
SingletonClass.singletonClassInstance(this)를 사용해MainActivity의SingletonClass클래스에 접근하게 된다.- 이때, SingletonClass 데이터를 가져오기 위해, parameter this를 사용해 해당 클래스의 context를 얻게 된다.
- 이 경우, context는 Java 클래스이다. 이는, Application components나 다른 OS feature에 대한 정보를 얻기 위한 방법을 제공한다.
- 그러나, 이처럼
thiscontext를 사용해 MainActivity에서 SingletonClass를 exceuting하면 activity가 누수(leak)될 수 있다.
Context의 사용법
- Context는 전체 Application의 lifecycle에 묶여있다. 따라서, Context를 잘못 사용하는 경우 메모리 누수로 이어질 수 있는 것이다.
- 따라서, 다른 Context를 사용하는 경우 위치와 사용시기를 확인해야 한다.
- 예를 들어,
getApplicationContext는 객체가 Activity Lifecycle을 넘어서까지 살아있어야 하는 경우 사용할 수 있다. 그러나, UI와 관련된 component들에선 사용할 수 없다.Singleton이 있는 경우, 항상 ApplicationContext를 사용해야 한다는 것이다.
- 또한 this는 객체가 activity lifecycle의 과거에 살아 있지 않은 경우에만 사용할 수 있다. 다만, UI component들을 참조하는데는 사용 할 수 있다.
- UI component들은 장시간 실행되는 작업이 아니고, activity lifecycle을 초과해 살아있을 수 없다.
- this Context는 XML 레이아웃이나, dialogue, getting resources, starting activity를 하는 여러 operation에 사용될 수 있다.
예시 2 (ApplicationContext - Singleton)
- SingletonClass를 사용하는 경우, Context-implementing 객체는 1개만 있을 수 있으므로,
getApplicationContext를 사용하는 것이 적절하다. getApplicationContext는singleton Context이다. 이는, 얼마나 많이 context에 접근하는 지 중요하게 생각하지 않으며 항상 같은 instance를 얻게 된다.- 따라서 해당 instance는 새로운 context를 만들지 않는다. 따라서 SingletonClass를 사용하는 경우 아래와 같이 사용하면 memory leak을 방지할 수 있다.
SingletonClass.singletonClassInstance(getApplicationContext());
Fragment Lifecycle vs ViewLifecycleOwner Lifecycle
Fragment에서 LiveData를 observing하는 경우, this 대신 viewLifecycleOwner 전달
- viewLifecycleOwner은 UI에 있는 한(
onCreateVew()~onDestroyView()), fragment에 묶여있는데(tied to fragment UI) 반해 - 반면에
this는 fragment의 전체 lifecycle (onCreate()~onDestroy())에 묶여있기 때문에 아래의 그림처럼 viewLifecycleOwner보다 this를 사용하는 경우 더 오랜 시간 observe하게 된다.
출처: https://write.agrevolution.in/memory-leaks-in-android-apps-45a27c6ac35d
- 이것은 곧
this를lifecycle owner로 전달(pass)하는 경우 메모리 누수를 일으키는 원인이 될 수 있음을 의미한다. - 예를 들어,
view가destroyed되었으나, (onDestroyView()is called),fragment는destroy되지 않은 경우 (onDestroy()is not called),LiveData 객체의 모든 변경사항이 여전히 observed되기 때문에 Crush가 발생할 수 있다.
FragmentStateAdapter Constructor에서 lifecycle 객체 대신 viewLifecycleOwner.lifecycle 전달
- 위와 비슷한 이유로,
FragmentStateAdapterConstructor에서Adapter의 범위를Fragment가 아닌,Fragment의 View의 lifecycle로 Scope를 지정해야 하기 때문에viewLifecycleOwner의 Lifecycle을 전달해야 한다.FragmentPagerStateAdapter(fragment)혹은FragmentPagerStateAdapter(fragmentActivity)에서FragmentPagerStateAdapter(fragmentManger, lifecycle)로 변경한다- 1번째와 2번째는
fragmentActivity.getLifecycle()과fragment.getLifecycle()를 이용해LifecycleEvent를 관찰한다. 따라서, previous memory leak이 일어나게 된다. - 3번째를 사용해
fragment의 viewLifecycleOwner를 2번째 인자로 넣어줘 사용할 수 있다. - 위의 식으로 인해,
View가 destryoed되고 나서, 다른 fragment로 이동한 후,onDetach를 calling할 때 까지 response를 받는다. viewModel.orders.collectLatest { adapter?.submitData(it) }- 이때 위와 같은 식이 있다면, fragment의 view가 destroy됐음에도 불구하고, recyclerview에 액세스하려고할 것이며 이는, adapter에 대한 참조를 가지고 있으면서 memory leak을 일으키는 원인이 된다.
Threaded Code
Threaded code는 메모리 누수를 일으킬 가능성이 매우 높다. Thread들은 실행 logic을 여러개의 동시 tasks로 decompose한다.- 안드로이드는 스레드를 사용해, 동시에 실행되는 여러 task들을 처리한다. 스레드에는 고유한 실행 환경이 없기 때문에, 부모 task로부터 실행 환경을 상속한다. 따라서 스레드는 단일 프로세스 범위 내에서 서로 데이터를 쉽게 통신하고 교환할 수 있다.
예시
- Thread Task를 초기화하고, threaded task를 set up한 뒤, onCreate() 메소드에서 task를 실행한다
private final ThreadedTask thread = new ThreadedTask();
private class ThreadedTask extends Thread {
@Override
public void run() {
// Run the ThreadedTask for some time
SystemClock.sleep(1000 * 20);
}
}
thread.start();
- Thread Task가 시작되면 실행이 완료될 때까지 시간이 걸리게 된다.
- 만약, task 실행이 끝나기 이전에 activity를 닫게되면, 실행되고 있는
ThreadedTask는 Activity가 garbage collected되는 것을 막을 것이다. Background에서 발생하는 view, activity또는 context에 대한 참조를 가지고 있는 것은 메모리 누수를 발생할 확률이 있다.- 이러한 누수를 해결하기 위해
static class를 사용할 수 있다.static class는 static class를 둘러싼activity class에 대한 참조를 가지고 있지 않기 때문이다.
// make ThreadedTask static to remove reference to the containing activity private static class ThreadedTask extends Thread { @Override public void run() { // check if the thread is interrupted while (!isInterrupted()) { // Run the ThreadedTask for some time SystemClock.sleep(1000 * 20); } } }- 또는 Activity가 destroy되는
onDestroy()에서 해당 스레드를 중단시킬수 있다.
// If the activity is destroyed, isInterrupted() will return true, and the thread will be stopped: @Override protected void onDestroy() { super.onDestroy(); //kill the thread in activity onDestroy thread.interrupt(); } - 이러한 누수를 해결하기 위해
Handler Threads
Handler는Java background Thread이다. 이것은 background에서 계속 실행되며,Appplication이 스레드 실행을 종료할 때 까지 순차적으로 다른 task들을 실행한다.- Handler는 주로 Application UI와 통신하고, 실행 스레드를 기반으로 다른 component들을 업데이트 한다.
- 가장 대표적인 handler application 예시가 progress bar
Handler는 메시지큐를 만드는데loopers를 사용하기에, 이를 이용해 메시지를 예약하고, 다른 반복 tasks를 기반으로 UI를 업데이트 할 수 있다.- 다만, Handler는 스레드이며, 여러번 반복해 실행되기 때문에, 어떻게 사용하느냐에 따라 메모리 누수가 발생할 가능성이 존재한다
예시
- handler task를 선언하고,
onCreate()메소드에서 task를 실행한다.
handler.postDelayed(new Runnable() {
@Override
public void run() {
textView.setText("Handler execution done");
}
// delay its execution.
}, 1000 * 10);
private final Handler handler = new Handler(Looper.getMainLooper());
- 해당 handler가 실행되었을 때, 해당
handler는activity에 callback을 register한다. 이것은activity가garbage collecting되는 것을 막고, 이는 곧 메모리 누수를 일으키게 된다.
@Override
protected void onDestroy() {
super.onDestroy();
//remove the handler references and callbacks.
handler.removeCallbacksAndMessages(null);
}
- 따라서, 메모리 누수를 해결하기 위해, 모든 callback들을 삭제해야 한다.
- 스레드는 단일 프로세스 범위 내에서 서로 데이터를 통신하고 교환하기 때문에,
onDestroy()메소드가 호출될 때 관련 callback을 제거해야 한다. - 이렇게 하면
Handler references가 제거되고, 메모리 누수가 해결된다.
- 스레드는 단일 프로세스 범위 내에서 서로 데이터를 통신하고 교환하기 때문에,
- Application에서 스레드가 누수 될수 있는 가능성은 많다. 따라서 Threaded 실행이 잘 되도록 하려면, 스레드가 생성된 시점부터 제거된 시점까지 Thread Lifecycle이 완전히 실행될 수 있도록 해야 한다.
- 또한, inner class에서 outer(parent) class에 대한 모든 implicit references를 관찰해야 한다.
Viewpager
ViewPager를 사용하는 경우 관련된 누수를 방지하기 위해onDestroyView()메소드 에서 다음과 같이 처리해주어야 한다.- viewPager.adapter = null
- tabLayout이 존재하는 경우 tabs에 적용한 listener들 remove 처리
- TabLayoutMediator Detach
override fun onDestroyView() { super.onDestroyView() binding.apply { viewPager.adapter = null tabLayout.removeOnTabSelectedListener(tabSelectedListener) } _binding = null tabLayoutMediator?.detach() tabLayoutMediator = null }
RecyclerView
RecyclerView를 사용하는 경우onDestroyView()Lifecycle 메소드에서 RecyclerView 객체에 대한 참조를 holding하는 것을 방지하기 위해RecyclerView.adapter = null처리를 해줘야 한다.- RecyclerView가 onCreateView() 에서 만들어 진뒤 해당 RecyclerView는, RecyclerViewAdapter에 대한 reference를 hold하고 있게 된다. 만약 fragment가 adapter의 reference를 hold하게 된다면, 이것은 Adapter를 통해 직접 및 간접적으로 RecyclerView를 hold하게 된다.
- RecyclerView is created inside
onCreateView(), and it holds a reference to RecyclerViewAdapter. If the fragment also holds the reference of the adapter, it holds RecyclerView both directly and indirectly through adapter.
- RecyclerView is created inside
- onDestyoView()가 호출됐을 때, fragment는 RecyclerView에 대한 직접적인 reference는 해제할 수 있지만, 간접적인(indircet) reference는 해제할 수 없기 때문에, RecyclerView가 leak되게 된다.

- 만약 Fragment가 Adapter의 reference를 hold해야 하는 경우, reference를 null로 set해줘야 하고 이것이 onDestroyView에서 null로 set해주는 것이다.
출처: https://weidianhuang.medium.com/how-to-prevent-common-memory-leaks-inside-android-fragment-c243ed7074d6
- null로 Set 해줌에 따라, reference를 break 해줄수 있다.
- 이를 통해서, RecyclerView → Adapter로의 reference를 break해주는 것을 이해할 수 있는 있지만, Adapter → RecyclerView의 reference를 break하는 것에 대해선 부족한 설명일 수 있다. 이는 Android RecyclerView source code를 통해 확인할 수 있다.
setAdapter()코드를 확인해보면 다음과 같다
public void setAdapter(@Nullable Adapter adapter) { // bail out if layout is frozen setLayoutFrozen(false); setAdapterInternal(adapter, false, true); processDataSetCompletelyChanged(false); requestLayout(); }- 위에서
setAdapterInternal를 호출하는 것을 알 수 있다.private void setAdapterInternal(@Nullable Adapter adapter, boolean compatibleWithPrevious, boolean removeAndRecycleViews ) { if (mAdapter != null) { mAdapter.unregisterAdapterDataObserver(mObserver); mAdapter.onDetachedFromRecyclerView(this); } ...... final Adapter oldAdapter = mAdapter; mAdapter = adapter; ...... }- 해당 메소드에서 mAdapter는 previous reference이고, new reference가 업데이트 되기 이전에, 이것은 unregisterAdapterDataObserver를 호출해 adapter 내부의 observable한 항목을 제거하도록 한다.
→ 이를 통해 Adapter에서 Adapter → recyclerView의 reference가 break됨을 알 수 있다.
- 해당 메소드에서 mAdapter는 previous reference이고, new reference가 업데이트 되기 이전에, 이것은 unregisterAdapterDataObserver를 호출해 adapter 내부의 observable한 항목을 제거하도록 한다.
- Destroy하는 경우에 null 처리하는 것이 싫다면, RecyclerViewAdapter의 reference를 Fragment에서 유지하지 않고, 항상 recyclerView.adapter를 이용해 RecyclerViewAdapter에 acces 하는 것도 하나의 방법이다.
SwipeRefreshLayout
- SwipeRefreshLayout의 경우 라이브러리 자체적으로 Leak을 가지고 있는 것이 Google Issue Tracker에서도 이미 보고되었다. 문제는 여전히 고쳐지지 않고 있다는 보고가 지속적으로 올라오고 있기 때문에 이에 대한 대비를 해둘 필요가 있다고 판단했다.
- 이를 위해 onResume ~ onPause 동안만 사용할 수 있도록 코드 내부에서 자체적으로 enable를 true/false 처리 해줄필요가 있다.
InOnResume
InswipeRefresh.isEnabled = falseonPauseswipeRefresh.isEnabled = true
기타
- 모든 listeners, broadcast receiver 등에 대해 적절한 lifecycle 메소드에서 리소스 누수를 방지하기 위해 unregister 처리를 해줘야 한다.
- 예를 들어,
onViewCreated()에서 receiver를 설정한 경우,onViewDestroyed()에서 unregister처리를 해줘야 한다.
- 예를 들어,
반응형