(RunWithMe 리팩토링) 2. bindService 적용하여 메모리 누수 방지하기
안녕하세요
RunWithMe 리팩토링 프로젝트에서 팀장과 안드로이드 개발을 맡은 서경원입니다.
RunWithMe 프로젝트를 리팩토링하면서 배우게 된 내용에 대해서 설명해보려합니다.
이해가 안되는 내용이나 제가 잘못 적은 부분이 있다면 꼭 댓글 남겨주세요.
startService와 bindService
Service를 제대로 적용하기 위해 Service에 대해 공부했고 그 중 startService와 bindService의 차이점을 명확히 알고자했습니다.
아래는 startService와 bindService에 대한 설명입니다.
안드로이드에서 startService()
와 bindService()
는 서비스를 시작하는 두 가지 다른 메서드입니다.
startService()
: 이 메서드는 서비스를 시작하고 백그라운드에서 동작하도록 합니다.startService()
를 호출하면 서비스의onCreate()
메서드가 호출되고, 서비스는 백그라운드에서 동작하며 독립적으로 실행됩니다.startService()
로 시작된 서비스는 앱의 다른 구성 요소와 독립적으로 실행되므로, 서비스가 완료되거나stopService()
가 호출되기 전까지 계속 실행됩니다.bindService()
: 이 메서드는 액티비티와 서비스를 연결(bind)하여 상호 작용할 수 있도록 합니다.bindService()
를 호출하면 서비스의onBind()
메서드가 호출되며, 서비스와 액티비티 간의 인터페이스를 제공합니다. 이를 통해 액티비티는 서비스의 메서드를 직접 호출하고, 서비스의 데이터나 상태에 접근할 수 있습니다.bindService()
로 연결된 서비스는 액티비티와의 바인딩이 해제되거나,unbindService()
가 호출되면 종료됩니다.
주요 차이점:
startService()
는 백그라운드에서 동작하며 독립적으로 실행되는 서비스를 시작합니다.bindService()
는 액티비티와 서비스를 연결하여 상호 작용하고 데이터를 공유하는데 사용됩니다.
따라서, startService()
는 서비스를 백그라운드에서 실행하고자 할 때 사용하며, bindService()
는 서비스와 액티비티 간의 상호 작용이 필요할 때 사용합니다. 사용 목적과 필요한 상호 작용의 종류에 따라 두 가지 메서드 중 적합한 것을 선택하면 됩니다.
기존에는 startService만을 사용하고 있었습니다.
‘백그라운드에서 독립적으로 실행되기 때문에 startService를 사용하는 것은 맞는 것 같은데 bindService를 사용하면 상호 작용하고 데이터를 공유할 수 있네? 기존에는 companion object를 사용했는데 bindService가 더 효율적이지 않을까?’
라는 생각으로 기존 코드를 다시 살펴보았습니다.
// 기존 코드
class RunningService : LifecycleService() {
companion object{
val isTracking = MutableLiveData<Boolean>() // 위치 추적 상태 여부
val pathPoints = MutableLiveData<Polylines>() // LatLng = 위도,경도
val timeRunInMillis = MutableLiveData<Long>() // 뷰에 표시될 시간
var isFirstRun = false // 처음 실행 여부 (false = 실행되지않음)
val sumDistance = MutableLiveData<Float>(0f)
val defaultLatLng = MutableLiveData<LatLng>()
}
...
...
}
class RuninngActivity {
fun initObserve() {
// 위치 추적 여부 관찰하여 updateTracking 호출
RunningService.isTracking.observe(this){
updateTracking(it)
}
// 거리 텍스트 변경
RunningService.sumDistance.observe(this){
sumDistance = it
changeDistanceText()
// 프로그래스바 진행도 변경
if(sumDistance > 0 && type == GOAL_TYPE_DISTANCE){
binding.progressBarGoal.progress = if ((sumDistance / (goal / 100)).toInt() >= 100) 100 else (sumDistance / (goal / 100)).toInt()
}
}
}
}
기존 코드는 RunningService의 companion object를 통해 RunningActivity에서 LiveData를 observe하고 이를 통해 상호 작용하는 코드였습니다.
companion object 중 pathPoints라는 좌표 List를 가지고 있습니다. 5초마다 한 번씩 위도, 경도 값을 리스트에 추가하기 때문에 오랜 시간 뛰는 경우에는 좌표 리스트를 엉청나게 많이 가지고 있을 것 입니다. 좌표를 구성하는 LatLng 속성인 latitude와 longitude의 경우, double로 각각, 8바이트 값을 가지고 있습니다.
companion object이기 때문에 gc의 대상이 되지 않아 계속 무거운 데이터를 가지고 있을 가능성이 있고 이것 때문에 메모리 누수가 발생할 가능성 또한 있다고 생각되었습니다.
그렇기 때문에 bindService를 통해 RunningService와 상호 작용하고자 했습니다.
bindService를 사용하면 실행 중인 Service의 객체 값을 가지고 올 수 있고 이를 통해 RunningService와 상호 작용할 수 있었습니다.
아래는 변경된 코드 입니다.
// 변경 코드
class RunningService : LifecycleService() {
// Binder
private val binder = LocalBinder()
val isRunning = MutableLiveData<Boolean>() // 위치 추적 상태 여부
val pathPoints = MutableLiveData<PolyLine>() // LatLng = 위도, 경도
val timeRunInMillis = MutableLiveData<Long>() // 뷰에 표시될 시간
var isFirstRun = true // 처음 실행 여부 (true = 실행되지않음)
val sumDistance = MutableLiveData<Float>(0f)
inner class LocalBinder : Binder() {
fun getService(): RunningService = this@RunningService
}
override fun onBind(intent: Intent): IBinder {
super.onBind(intent)
return binder
}
...
...
}
class RunningActivity {
private lateinit var runningService: RunningService
private fun initObserve(){
// 좌표 observe
runningService.pathPoints.observe(this){
if(it.isNotEmpty()){
val lat = it.last().latitude
val lng = it.last().longitude
val latlng = LatLng(lat, lng)
naverLatLng.add(latlng)
latLngBoundsBuilder.include(latlng)
if(it.size >= 2){
drawPolyline()
}
// 경로 최적화
if(it.size >= 4){
optimizationPolyLine()
}
}
}
// 시간(타이머) 경과 observe
runningService.timeRunInMillis.observe(this){
currentTimeInMillis = it
val formattedTime = TrackingUtility.getFormattedStopWatchTimeSummery(it)
changeTimeText(formattedTime)
// 프로그래스바 진행도 변경
if(it > 0 && type == GOAL_TYPE_TIME) {
binding.progressBarGoal.progress = if ((it / (goal / 100)).toInt() >= 100) 100 else (it / (goal / 100)).toInt()
}
}
// 거리 observe
runningService.sumDistance.observe(this){
sumDistance = it
changeDistanceText(sumDistance)
changeCalorie(sumDistance)
// 프로그래스바 진행도 변경
if(sumDistance > 0 && type == GOAL_TYPE_DISTANCE) {
binding.progressBarGoal.progress = if ((sumDistance / (goal / 100)).toInt() >= 100) 100 else (sumDistance / (goal / 100)).toInt()
}
}
// 러닝 뛰고 있는 지 observe
runningService.isRunning.observe(this){
if(it){ // 러닝을 뛰고 있는 경우
binding.apply {
ibStart.visibility = View.GONE
ibStop.visibility = View.GONE
ibPause.visibility = View.VISIBLE
}
}else{ // 일시 정지 된 경우
binding.apply {
ibStart.visibility = View.VISIBLE
ibStop.visibility = View.VISIBLE
ibPause.visibility = View.GONE
}
}
}
}
private fun firstStart() {
if(firstRun){
lifecycleScope.launch(Dispatchers.Main) {
sendCommandToService(ACTION_SHOW_RUNNING_ACTIVITY)
delay(3000L)
sendCommandToService(ACTION_START_OR_RESUME_SERVICE)
// bindService
Intent(this@RunningActivity, RunningService::class.java).also { intent ->
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
}
delay(1000L)
}
}
}
private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val bind = service as RunningService.LocalBinder
runningService = bind.getService()
initObserve()
}
override fun onServiceDisconnected(name: ComponentName?) {
TODO("Not yet implemented")
}
}
}
경로 최적화를 통해 좌표 데이터를 줄여주기 때문에 메모리 누수가 발생할 상황은 거의 없을 것이지만 최악의 상황을 막아주고 리팩토링의 목적인 최적화를 잘 해내고 있는 것 같아 뿌듯했습니다!
'Android 일지 > 리팩토링' 카테고리의 다른 글
3. 경로 최적화로 좌표 데이터 약 73% 감소 (2) | 2023.10.17 |
---|---|
6. SharedPreference에서 DataStore로 변경하여 데이터 일관성 문제 해결하기 (0) | 2023.05.30 |
5. CustomView로 재사용성 향상 (0) | 2023.05.27 |
4. EventFlow 도입 (0) | 2023.05.26 |
1. LifecycleService와 Service의 차이점 (0) | 2023.05.25 |