반응형
[Android/Refactoring] Memory Leak - 2. 자주 발생하는 안드로이드 메모리 누수 방지하기
이어지는 글
2023.07.21 - [IT/Android] - [Android/Refactoring] Memory Leak - 1. 안드로이드 앱에서의 메모리 누수 찾기
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에 대한 정보를 얻기 위한 방법을 제공한다.
- 그러나, 이처럼
this
context를 사용해 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 전달
- 위와 비슷한 이유로,
FragmentStateAdapter
Constructor
에서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 = false
onPause
swipeRefresh.isEnabled = true
기타
- 모든 listeners, broadcast receiver 등에 대해 적절한 lifecycle 메소드에서 리소스 누수를 방지하기 위해 unregister 처리를 해줘야 한다.
- 예를 들어,
onViewCreated()
에서 receiver를 설정한 경우,onViewDestroyed()
에서 unregister처리를 해줘야 한다.
- 예를 들어,
반응형