Shared Preference를 사용해 데이터를 읽고 저장하는 Data Model을 만들려고 했더니, Shared Preference를 얻기 위해 Application Context가 필요했다. 보통 context는 필요할 때 매번 값을 인자로 넘겨주는 사용법이 권장된다. 그런데 말이지. 언제든 사용할 수 있게 singleton으로 만들었는데, 만들다보니 lifecycle이 Application과 동일하고 데이터 저장, 읽기를 하는데 굳이 외부에서 context를 매번넘겨줘야 하는건가 생각이 들었다. Data Model에서 알아서 읽고 쓰고 하면 되는건데.
일단, Singleton 구현은 static으로 만들어진다.
class TimerModel {
companion object {
@Volatile private var instance: TimerModel? = null
fun getInstance(): TimerModel {
return instance ?: synchronized(this) {
instance ?: TimerModel().also { instance = it }
}
}
}
}
getInstance(context: Context) 형태로 context를 넘겨야하나? 이러면 매번 넘겨주는게 되는데. 일단, 해보자…는 엄중한 경고가 뜸.
Do not place Android context classes in static fields (static reference to TimerModel which has field context pointing to Context); this is a memory leak
Inspection info:A static field will leak contexts. Non-static inner classes have an implicit reference to their outer class. If that outer class is for example a Fragment or Activity, then this reference means that the long-running handler/loader/task will hold a reference to the activity which prevents it from getting garbage collected. Similarly, direct field references to activities and fragments from these longer running instances can cause leaks. ViewModel classes should never point to Views or non-application Contexts.
간단히 말하면, static field에 context 저장하지마라, 메모리 누수가 생긴다.
이런 문제 때문인지, Android 오픈소스 앱인 DeskClock을 보면, singleton에서 static으로 저장하지않고, 최초 인스턴스 생성시 init() 함수를 따로 만들어서 전달해주고 있다.
class DeskClockApplication : Application() {
override fun onCreate() {
super.onCreate()
val applicationContext = applicationContext
val prefs = getDefaultSharedPreferences(applicationContext)
DataModel.dataModel.init(applicationContext, prefs) // <-- 요기
...
이럴거면 singleton으로 왜만들지? 라는 생각이 들어서 뭔가 아닌거 같았어.
잘 모르는 상태에서 이런저런거 찾아보다가, Application context의 경우, custom Application 클래스에서 저장해놓고 참조하는 방법을 찾았다.
class MyApplication : Application() {
init{
instance = this
}
companion object {
lateinit var instance: MyApplication
fun ApplicationContext() : Context {
return instance.applicationContext
}
}
}
아니 뭐야, 이것도 static으로 context를 저장하는건데? 실제로 검색을 해봐도 이게 된다 안된다 말들이 너무 많더라고. 아무래도 왜 메모리 릭이 생기는지 이해를 해야 정답을 알거같아서 좀 더 찾아봤다.
Context에 대해 간단히 알아보면, context는 시스템에서 제공하는 서비스에 대한 인터페이스를 제공하고, 앱의 리소스에 접근, Activity간 인텐트를 주고받는 인터페이스 역할등을 한다. 또한, 두가지의 context가 존재하는데, Application context가 있고 Activity context가 있다. 각, context는 해당하는 lifecycle을 갖는다. 그러니까, 특정 Activity의 context는 해당하는 Activity가 종료되면 같이 사라진다는 얘기.
결국, 하나의 앱에서 여러 Activity가 사용되면서 각각 붙어있는 context들이 사용되는데, 이 context를 static으로 저장해놓으면, Activity가 사라져도 context는 gabage collection 대상이 되지 않아 메모리 누수가 생긴다는 얘기다.
그런데 말이지. application context는 해당사항이 없잖아? 위처럼 Application 인스턴스에서 저장해도 문제가 안된다는 얘기. 또한, Dagger를 사용하는 경우 다음과 같은 더 간단한 솔루션도 있는데,
@Inject Context context;
이것도 결국, Dagger 내에서 context를 저장하고 그걸 활용한단 얘기거든. 그러니까, 아 난 모르겠으니까 싶으면 Dagger를 써도 되고, 저 위에서처럼 custom Application 인스턴스에 저장해놓고 읽어가게 해도 괜찮다는 거다. 정리 끝!