이전에 관련된 글을 작성한 적 있으므로,
중복된 항목에 대해서 이유를 자세하게 적지는 않겠다.
Inner class와 익명 class
- 밖의 클래스보다 inner class의 주기가 더 길면 생기는 문제
- 암시적 참조를 주의할것
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_my)
val button = findViewById<Button>(R.id.myButton)
button.setOnClickListener {
// This is an anonymous inner class
// It holds an implicit reference to MyActivity
// Long-running operations or delayed execution here can cause leaks
}
}
}
- onCreate 안에서 click listener가 설정되었으므로, 저 리스너는 activity에 대해서 암시적 참조를 가지게된다.
→ 리스너의 행위는 정해진게 없으므로, 만약 리스너가 작업을 계속 진행하는 동안 activty가 삭제되거나 하면 메모리 누수가 발생함.
해결 방안
- static inner class를 사용할것
- weakReference를 사용할것
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_my)
MyStaticAsyncTask(this).execute()
}
private class MyStaticAsyncTask(activity: MyActivity) : AsyncTask<Void, Void, String>() {
private val activityReference = WeakReference(activity)
override fun doInBackground(vararg params: Void?): String {
// Perform a long-running task
return "Result"
}
override fun onPostExecute(result: String) {
activityReference.get()?.let { activity ->
// Safely update the Activity's UI
}
}
}
}
- Activity에 대한 weakReference를 가지고 있는게 포인트!
Handler & Runnable
- 마찬가지로, 스레드에서 뷰에 대한 암시적인 참조가 있을 때 문제가 됨.
- 딜레이가 있는 실행동안 뷰가 삭제되면 GC안될 수 있음
class MyActivity : AppCompatActivity () {
private val handler = Handler()
private val updateRunnable = object : Runnable {
override fun run () {
// 해당 Runnable은 MyActivity에 대한 암시적 참조를 보유
// 여기서 장기 실행 작업이나 지연 실행으로 인해 Leaks
}
}
override fun onCreate (savedInstanceState: Bundle ?) {
super .onCreate(savedInstanceState)
setContentView(R.layout.activity_my)
// 지연된 작업 게시
handler.postDelayed(updateRunnable, 60000 ) // 60초 지연
}
}
해결 방안
- static inner class 혹은 별도의 클래스 사용할 것
- onDestroy()에서 알아서 주울것
class MyActivity : AppCompatActivity() {
private val handler = Handler()
private val updateRunnable = UpdateRunnable(this)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_my)
handler.postDelayed(updateRunnable, 60000) // 60-second delay
}
override fun onDestroy() {
super.onDestroy()
handler.removeCallbacks(updateRunnable) // Remove pending callbacks
}
private class UpdateRunnable(activity: MyActivity) : Runnable {
private val weakActivity = WeakReference(activity)
override fun run() {
weakActivity.get()?.let {
// Perform actions without risking a memory leak
}
}
}
}
- 마찬가지로 weakReference를 가지고 있는게 포인트~
- 추가로, destroy 시점에 적절하게 callback을 삭제해줌
익명 listener
- 리스너가 뷰 내부에 선언되면서 암시적인 참조를 보유하게 될 때 생기는 문제
- 1번 예시가 곧 예제가 됨
해결 방안
- 리스너에서 장기적으로 실행되는 작업이 없도록 방지할 것
- 마찬가지로 WeakReference를 사용할것!
- static 중첩 클래스나 람다를 활용할 것 → 약한 참조를 가능하게 해줌!
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_my)
val button = findViewById<Button>(R.id.myButton)
button.setOnClickListener { view ->
// Lambda expression for click listener
// Handle click event without risking a memory leak
}
}
}
static view / context
- 정적인 필드가 뷰에 대한 참조를 보유할 때 발생함..
- 컨텍스트가 사용되지 않더라도 GC가 메모리를 회수하는것을 방지하게됨
class MyActivity : AppCompatActivity () {
companion object {
// 뷰에 대한 정적 참조
var staticView: View? = null
}
override fun onCreate (savedInstanceState: Bundle ?) {
super .onCreate(savedInstanceState)
setContentView(R.layout.activity_my)
staticView = findViewById(R.id.myView) // 정적 필드에 뷰 할당
}
}
- companion object는 인스턴스가 아닌 속한 클래스에 연결됨
- 위의 object는 activity가 삭제 된 후에도 참조를 유지함
해결 방안
- static한 친구에게 view의 참조를 넘기지말것…
- 뭔가 데이터 및 상태를 유지해야 하는 상황이라면
- → viewmodel / savedInstanceState를 사용할것!
- 뭔가 데이터 및 상태를 유지해야 하는 상황이라면
LiveData의 부적절한 사용
- LiveData의 수명 주기가 소유자보다 오래 지속되는 경우 발생
class MyActivity : AppCompatActivity () {
private val viewModel: MyViewModel by viewModels()
override fun onCreate (savedInstanceState: Bundle ?) {
super .onCreate(savedInstanceState)
setContentView(R.layout.activity_my)
// 부적절한 관찰
viewModel.myLiveData.observe( this , Observer { data ->
// UI 업데이트 - 올바르게 처리되지 않으면 메모리 누수가 발생할 수 있습니다.
})
}
}
해결 방안
- viewLifeCycleOwner 사용
- destroy에서 적절하게 옵저버 제거
class MyFragment : Fragment () {
private val viewModel: MyViewModel by viewModels()
override fun onViewCreated (view: View , saveInstanceState: Bundle ?) {
super .onViewCreated(view, selectedInstanceState)
viewModel.myLiveData.observe(viewLifecycleOwner, Observer { data - >
// UI 업데이트 - viewLifecycleOwner로 올바르게 관찰
})
}
}
Context 가 있는 Singleton
- 당연히 안됨..
- singleton은 전체 생애주기 중 단 하나만 존재하도록 설계됨
→ 당연히 context가 있음 안된ㄷ ㅏ!
class MySingleton private constructor(context: Context) {
init {
// Initialization code that uses the context
}
companion object {
private var instance: MySingleton? = null
fun getInstance(context: Context): MySingleton {
if (instance == null) {
instance = MySingleton(context)
}
return instance!!
}
}
}
해결방안
- 굳이 context가 필요하다면 application context를 사용하자
class MySingleton private constructor(context: Context) {
init {
// Use the application context to avoid memory leaks
val applicationContext = context.applicationContext
}
companion object {
private var instance: MySingleton? = null
fun getInstance(context: Context): MySingleton {
if (instance == null) {
instance = MySingleton(context.applicationContext)
}
return instance!!
}
}
}
- 가능하다면 그냥 쓰지말자
비트맵
- 고해상도 이미지를 처리 할 때 해당 부분이 문제가 될 수 있음
- OutOfMemoryError가 발생 할 수 있음(처리 과정 중에)
- 명시적으로 재활용하지 않을 경우 메모리가 해제되지 않아 문제가 있을 수 있음
class MyActivity : AppCompatActivity() {
private var myBitmap: Bitmap? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_my)
val imageView = findViewById<ImageView>(R.id.myImageView)
myBitmap = BitmapFactory.decodeResource(resources, R.drawable.large_image)
imageView.setImageBitmap(myBitmap)
}
}
해결방안
- 적절한 사이즈의 비트맵을 사용할 것. (표시에 필요한 사이즈로만)
- onDestroy에 recycle을 해줄것
- 라이브러리 사용할것 (글라이드 / 피카소 / 코일)
class MyActivity : AppCompatActivity() {
private var myBitmap: Bitmap? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_my)
val imageView = findViewById<ImageView>(R.id.myImageView)
myBitmap = BitmapFactory.decodeResource(resources, R.drawable.large_image)
// Resize bitmap as needed before setting it to ImageView
imageView.setImageBitmap(myBitmap)
}
override fun onDestroy() {
super.onDestroy()
myBitmap?.recycle() // Recycle the bitmap
myBitmap = null
}
}
- 보면 알겠지만 recycle을 해주고있다~
웹뷰
- 앱 내에서 웹 콘텐츠를 처리하는 방식 때문에 문제가 생길 수 있다~
- 웹뷰의 주기는 속해있는 액티비티 / 프래그먼트와 다르다. 제대로 관리되지 않으면 상위 뷰의 주기가 끝났는데도 리소스를 계속 소비할 수 있다
class MyActivity : AppCompatActivity() {
private lateinit var myWebView: WebView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_my)
myWebView = findViewById(R.id.myWebView)
myWebView.loadUrl("https://example.com")
}
}
- 현재 웹뷰가 뷰의 컨텍스트를 암시적으로 참조하는 부분에서 초기화되고있음
해결 방안
- 적절히 해제할것 : webview.destroy()를 뷰가 해제되는 시점에 호출할 수 있게 할것
- application context를 사용할 것 : 만약 컨텍스트의 참조가 필요한 상황이라면 application context를 사용할 것
- 앱 컨텍스트가 항상 나쁜것만은 아니다!
class MyActivity : AppCompatActivity() {
private lateinit var myWebView: WebView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_my)
myWebView = findViewById(R.id.myWebView)
myWebView.loadUrl("https://example.com")
}
override fun onDestroy() {
myWebView.destroy() // Properly destroy the WebView
super.onDestroy()
}
}
BroadCast Receiver
- 얘 또한.. 참조를 보유할 수 있기때문에, 뷰가 사라지는 시점에 적절히 취소해주지 않으면 메모리에 혼백이 되어서 떠돌게된다
class MyActivity : AppCompatActivity () {
private val myReceiver = object : BroadcastReceiver() {
override fun onReceive (context: Context ?,intent: Intent ?) {
// 브로드캐스트 처리
}
}
override fun onCreate (savedInstanceState: Bundle ?) {
super .onCreate(savedInstanceState)
setContentView(R.layout.activity_my)
// 수신자 등록
IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED). also { filter ->
registerReceiver(myReceiver, filter)
}
}
}
- 해당 경우에서는 액티비티 삭제되어도 계속 참조를 보유하게 된다
해결 방안
- 알아서 destroy 적절히 해주기
- (익명 클래스 피하기)
class MyActivity : AppCompatActivity () {
private val myReceiver = object : BroadcastReceiver() {
override fun onReceive (context: Context ?,intent: Intent ?) {
// 브로드캐스트 처리
}
}
override fun onCreate (savedInstanceState: Bundle ?) {
super .onCreate(savedInstanceState)
setContentView(R.layout.activity_my)
IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED). also { filter ->
registerReceiver(myReceiver, filter)
}
}
override fun onDestroy () {
super .onDestroy()
unregisterReceiver(myReceiver) // 수신자 등록 취소
}
}
RecyclerView Adapter의 event listener
- 어댑터나 뷰홀더가 뷰에 대한 참조를 보유할 때 발생함
class MyAdapter(private val items: List<Item>, private val context: Context) : RecyclerView.Adapter<MyAdapter.ViewHolder>() {
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val button: Button = view.findViewById(R.id.myButton)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.item_view, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.button.setOnClickListener {
// This listener holds a reference to 'context' which can cause a memory leak
}
}
}
해결 방안
- 어댑터에 컨텍스트 전달하지말것.
- parent.context
- onCreateView에서는 holder.itemview.context를 사용하세요~
- 리스너에 weakReference를 사용하세용
- 리스너를 잘 분리하세용
class MyAdapter(private val items: List<Item>) : RecyclerView.Adapter<MyAdapter.ViewHolder>() {
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val button: Button = view.findViewById(R.id.myButton)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_view, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.button.setOnClickListener {
// Listener that doesn't hold a strong reference to the context
}
}
override fun onViewRecycled(holder: ViewHolder) {
holder.button.setOnClickListener(null) // Detach listener
}
}
결론은
- 항상 뷰에 대한 약한 참조를 사용하자
- 웬만하면 context는 넘기지 말자
Uploaded by N2T