* 이 개념 확실히 해야 한다. 동작 방식/프로세스가 복잡하니 직접 그려보면서 확실히 이해 ㄱㄱ
IPC
- Activity에서 실행 중인 Service를 제어하거나 데이터를 사용하는 등의 작업이 필요할 때 사용
- Activity 와 Service는 별개의 요소이다. 액티비티에서 서비스를 실행시켰다 하더라고 서로 별개로 동작하기 때문에 액티비티에서 실행중인 서비스에 접속해 서비스가 가지고 있는 메서드를 호출하는 개념 (액티비티 종료되더라도 서비스는 계속 동작하고 필요시 액티비티를 다시 만들고 서비스에 접속이 가능, 서비스의 결과를 액티비티에 반환 받아 사용할 수도 있음)
class TestService : Service() {
var isRunning = false
var value = 0
val binder = LocalBinder()
// 외부에서 서비스에 접속하면 호출되는 메서드
override fun onBind(intent: Intent): IBinder {
/* 외부에서 서비스에 접속 -> 해당 메서드 호출 -> 만들어놨던 LocalBinder 객체를 반환
-> 액티비티에서는 LocalBinder 객체의 getService() 메서드 사용 -> TestService 의 변수 받아옴 */
return binder
}
// 서비스가 가동될 때 호출되는 메서드 // 해당 서비스클래스의 인스턴스 객체 생성
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d("test","서비스 가동")
// 안드 8.0 이상부터
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val channel = NotificationChannel("Sevice","service",NotificationManager.IMPORTANCE_HIGH)
channel.enableLights(true)
channel.lightColor =Color.RED
channel.enableVibration(true)
manager.createNotificationChannel(channel)
val builder = NotificationCompat.Builder(this, "Sevice")
builder.setSmallIcon(android.R.drawable.ic_menu_search)
builder.setContentTitle("서비스 가동")
builder.setContentText("서비스가 가동중입니다.")
val notification = builder.build()
// 알림 메시지를 foreground 서비스를 위해 표시한다.
// targetSdk 28 이상에서는 permission 등록 필요
startForeground(10, notification) // 서비스 가동시 알림 뜨고, 서비스 종료시 자동으로 사라짐 (종료 전엔 못 없앰)
}
// 스레드 운영 // 액티비티를 종료하더라도 서비스는 계속 가동 (별개로 동작)
isRunning = true
thread {
while (isRunning) {
SystemClock.sleep(500)
val now = System.currentTimeMillis()
Log.d("test", "value : ${value}")
value++
}
}
return super.onStartCommand(intent, flags, startId)
}
// 서비스가 중지되고 소멸될 때 호출 (액티비티와 다르게 소멸만 존재 -> 백그라운드 실행이라 일시중지의 개념 X, 시작/종료 2가지뿐!)
override fun onDestroy() {
super.onDestroy()
isRunning = false
Log.d("test","서비스 증지")
}
// 변수의 값을 반환 하는 메서드 -> 액티비티에서 서비스에 접속하면 서비스 객체 자체를 받아올 수 있음. 하지만 변수는 private 하게 되어 있기에 메서드를 통해 가져옴
fun getNumber() : Int {
return value
}
// 접속하는 Activity에서 해당 서비스를 추출하기 위해 사용하는 이너 클래스 (액티비티에서 이 클래스 인스턴스 객체를 만들어서 이 전체 서비스 객체를 추출)
inner class LocalBinder : Binder() { // Binder 상속받음
fun getService() : TestService {
return this@TestService // 현재 서비스 객체의 주소값을 반환 // LocalBinder 객체가 가지고 있는 TestService 객체를 반환하는 것
}
}
}
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
var ipcService : TestService? = null // 접속한 서비스 객체
// 서비스 접속을 관리하는 객체
val connection = object : ServiceConnection {
// 서비스 접속 성공 했을 때 (서비스 접속시 onBind() 메서드 호출해 리턴 객체를 OS가 받고, 그다음 이 메서드를 자동 호출)
override fun onServiceConnected(p0: ComponentName?, p1: IBinder?) {
// p0 : 서비스의 이름
// p1 : binder 객체
// 서비스 추출
val binder = p1 as TestService.LocalBinder // LocalBinder 객체
ipcService = binder.getService()
Log.d("test2", "서비스 접속")
}
// 서비스 접속 해제 했을 때
override fun onServiceDisconnected(p0: ComponentName?) {
ipcService = null
Log.d("test2", "서비스 해제")
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater) // 뷰바인딩 객체
val view = binding.root //뷰바인딩을 통해 레이아웃과 뷰가 결합 -> .root 를 통해 View 객체만를 뽑아내는(?)
setContentView(view)
// 현재 가동시키고 싶은 서비스가 가동 X 상태면 가동시킨다
val chk = isServiceRunning("com.example.androidstudy_kotlin.TestService")
val serviceIntent = Intent(this, TestService::class.java)
if (chk == false) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // 안드 8.0 이상부터 서비스가 자동 중단 방지 위해 foreground
startForegroundService(serviceIntent)
} else {
startService(serviceIntent)
}
}
// 서비스에 접속한다
bindService(serviceIntent, connection, Context.BIND_AUTO_CREATE)
// 기본 리스트 다이얼로그 (다이얼로그에 리트스뷰를 넣어 커스텀 한다고 생각하면 됨)
binding.button.setOnClickListener {
var value = ipcService?.getNumber()
binding.textView.text = "value : ${value}"
}
}
// 서비스 실행 여부를 검사하는 메서드
fun isServiceRunning(name : String) : Boolean { // 검사하고자 하는 서비스의 이름이 매개변수로 들어오면 실행중인 서비스 리스트를 가져와 비교
val manager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
// 현재 실행중인 서비스들을 가져옴
val serviceList = manager.getRunningServices(Int.MAX_VALUE) // 최대 몇개의 서비스를 가져올지 정함 -> 최댓값으로 설정
/* 단말기 내부에 존재하는 모든 서비스를 가져올 수 있으나 보안상의 문제로 버전 올라가면서 deprecated -> 현재 앱에서 동작시킨 서비스들의 목록만 가져옴(다른 앱의 서비스는 X) */
for (serviceInfo in serviceList) {
// 서비스의 이름이 원하는 이름인가...
if (serviceInfo.service.className == name) {
return true
}
}
return false
}
override fun onDestroy() {
super.onDestroy()
// 접속한 서비스에 접속을 해제
unbindService(connection)
}
}
✔️ 서비스가 동작하고 있는지 확인하고 서비스에서 운영하고 있는 변수값을 화면에 출력하고 싶다 뿅
=> 서비스가 동작하고 있는지 확인하고 (동작 X 면) 동작시키는 메서드 -> 서비스에 접속 -> 외부에서 서비스 접속이 감지되면 서비스 클래스에서 onBind() 메서드 자동 호출 -> onBind() 메서드는 사용자가 정의해놓은 LocalBinder 이너 클래스 객체를 반환 -> 해당 객체는 서비스 객체를 반환하는 메서드를 갖고 있기 때문에 서비스 객체를 추출 -> 반환된 서비스 객체를 이용해 서비스 변수에 접근 (private 변수에 바로 접근할 수 없기 때문에 메서드 통해서 접근)
❓ 서비스에 접속하면 무조건 onBind() 메서드가 호출된다고 했는데 왜 여기서 바로 서비스 객체를 반환하지 않나요?
=> 나도 모른다!!! onBind() 메서드의 반환 타입이 IBinder 이기 때문에 사용자가 일단 Binder 를 상속받는 클래스를 만들고 그 안에 서비스 객체를 반환하는 메서드르르 만들어서 사용하는 것!!!
이렇게 앱을 실행하면 스레드가 동작하면서 value값은 계속 변하게 된다. 앱을 완전히 종료하지 않는 이상 스레드는 계속 돌아가는 것은 저번 시간에 배운 것이다. 그리고 서비스에 접속하여 서비스 객체를 추출하고 객체의 변수값인 value 데이터를 받아와 화면에 표시할 수 있다.
앱을 나가서 홈스레드에서 현재 실행중인 앱을 위로 올려 제거하고 다시 앱을 켜면 서비스 접속이 해제되었다가 다시 접속될 것 이라고 생각했다. 그리서 아래 onServiceDisconnected() 메서드가 호출되면서 로그가 뜰줄 알았는데 뜨지 않았다.
공식문서와 스택오버플로우를 뒤져본 결과 해다아 메서드는 비정상적인 종료일때만 호출된다고 한다. 그리고 사실 unbindService 하지 않는 이상 서비스는 해제되지 않는다고 한다.
// 일반적으로 클라이언트가 서비스 접속을 해제 했을 때는?.. unbindService() 를 하지 않는이상 진짜 해제되는 것이 아니다.
// 그냥 ipcService 객체를 Null로 만들어서 객체/변수 추출을 중단하는 정도이고 실제로는
override fun onServiceDisconnected(p0: ComponentName?) {
ipcService = null
Log.d("test3", "서비스 해제")
}
하지만 그렇게 앱을 나갔다가 다시 실행해 들어올 때마다 '서비스 접속' 이라는 로그는 뜨는 것을 확인할 수 있었다. 그것은 즉, ipcService 변수에 null 값이 다시 세팅되었다가 TestService 객체를 할당해줬다는 것이다. 그러면 도대체 어디서 ipcService는 null 로 변하는 것일까?...
안드로이드 OS에서 따로 메서드를 호출해 ipcService 변수값을 null 값으로 변화 시키는줄 알고 이걸 추적해보려고 아래와 같이 디버깅을 시도했다. 어디에 breakpoint를 걸어야할지 잘 몰라서 일단 냅다 ipcService 변수에 걸고 디버깅 모드로 실행해봤는데 이렇게 이 변수에 어떤 값들이 들어가는데 표시해준다. (당연한건가...ㅋ) 아무튼 들이다보고 있었는데....
갑자기 생각난 기본 개념...!!!! 액티비티는 화면이 안 보이는 순간 onPause() -> onStop() 메서드를 호출한다. 그리고 앱을 나갔다가 다시 들어오게 되면 onResume()이 실행된다. 이때 onPause() -> onStop() 과정에서 불필요한 리소스는 해제시켜 leak 현상 등을 방지 한다고 한다. 그렇기 때문에 그 안의 변수인 ipcService도 초기값인 null 값을 갖게 되는 것이 당연하다. (이런 당연한 것을 고민하고 있었다니....하지만 배운걸 토대로 문제를 해결해서 뿌듯했다)
[참고 문험]
https://developer.android.com/guide/components/bound-services?hl=ko
'Android' 카테고리의 다른 글
[안드로이드/Fragment] Fragment 생명주기 (1) | 2023.04.21 |
---|---|
[안드로이드/Fragment] Fragment 란? (0) | 2023.04.21 |
[안드로이드/Service] Service(서비스) 란? (0) | 2023.04.20 |
[안드로이드/Recevier] 시스템 메시지 (System Message) (0) | 2023.04.19 |
[안드로이드/Recevier] BroadCastReceiver 란? (0) | 2023.04.17 |