메모리 누수를 해결하는 조금 더 자세한 방법

2024. 1. 3. 10:55개발/Android

이전에 관련된 글을 작성한 적 있으므로,

중복된 항목에 대해서 이유를 자세하게 적지는 않겠다.

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가 삭제되거나 하면 메모리 누수가 발생함.

해결 방안

  1. static inner class를 사용할것
  1. 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초 지연
     } 
}

해결 방안

  1. static inner class 혹은 별도의 클래스 사용할 것
  1. 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번 예시가 곧 예제가 됨

해결 방안

  1. 리스너에서 장기적으로 실행되는 작업이 없도록 방지할 것
  1. 마찬가지로 WeakReference를 사용할것!
  1. 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가 삭제 된 후에도 참조를 유지함

해결 방안

  1. static한 친구에게 view의 참조를 넘기지말것…
    1. 뭔가 데이터 및 상태를 유지해야 하는 상황이라면
      1. → 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 업데이트 - 올바르게 처리되지 않으면 메모리 누수가 발생할 수 있습니다.
         }) 
    } 
}

해결 방안

  1. viewLifeCycleOwner 사용
  1. 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!!
        }
    }
}

해결방안

  1. 굳이 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!!
        }
    }
}
  1. 가능하다면 그냥 쓰지말자

비트맵

  1. 고해상도 이미지를 처리 할 때 해당 부분이 문제가 될 수 있음
    • OutOfMemoryError가 발생 할 수 있음(처리 과정 중에)
  1. 명시적으로 재활용하지 않을 경우 메모리가 해제되지 않아 문제가 있을 수 있음
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)
    }
}

해결방안

  1. 적절한 사이즈의 비트맵을 사용할 것. (표시에 필요한 사이즈로만)
  1. onDestroy에 recycle을 해줄것
  1. 라이브러리 사용할것 (글라이드 / 피카소 / 코일)
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")
    }
}
  • 현재 웹뷰가 뷰의 컨텍스트를 암시적으로 참조하는 부분에서 초기화되고있음

해결 방안

  1. 적절히 해제할것 : webview.destroy()를 뷰가 해제되는 시점에 호출할 수 있게 할것
  1. application context를 사용할 것 : 만약 컨텍스트의 참조가 필요한 상황이라면 application context를 사용할 것
    1. 앱 컨텍스트가 항상 나쁜것만은 아니다!
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) 
        } 
    } 
}
  • 해당 경우에서는 액티비티 삭제되어도 계속 참조를 보유하게 된다

해결 방안

  1. 알아서 destroy 적절히 해주기
  1. (익명 클래스 피하기)
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
        }
    }
}

해결 방안

  1. 어댑터에 컨텍스트 전달하지말것.
    1. parent.context
    1. onCreateView에서는 holder.itemview.context를 사용하세요~
  1. 리스너에 weakReference를 사용하세용
  1. 리스너를 잘 분리하세용
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
    }
}

결론은

  1. 항상 뷰에 대한 약한 참조를 사용하자
  1. 웬만하면 context는 넘기지 말자

Uploaded by N2T

'개발 > Android' 카테고리의 다른 글

Retrofit API Test  (1) 2024.01.03
Android SSL Pinning  (0) 2023.12.15
Android의 모듈식 접근 방식  (0) 2023.12.15
MenuProvider  (0) 2023.12.15
메모리 누수의 원인 10가지  (0) 2023.12.15