[Android/Flutter 교육] 26일차

MSU·2024년 2월 1일

Android-Flutter

목록 보기
27/85
post-thumbnail

Thread

cpu core 하나 당 1~10개의 작업 처리
process : 하나의 컴퓨터 안에서 실행되는 프로그램의 한 단위
thread : 하나의 프로그램 안에서 작업의 한 단위
thread 하나 당 하나의 작업을 처리할 수 있다.

동기 : 순차적으로 처리
비동기 : 동시에 처리, 동시에 작업할 일을 각각 쓰레드를 만들어 실행시키면 동시에 실행이 가능하다.

안드로이드 4대 구성요소(Activity, Service, BR, CP)가 실행되면 그 안에 작성된 작업을 처리하기 위해 안드로이드 OS가 쓰레드를 발생시킨다.
만약 다른 작업을 동시에 처리하고자 한다면 Thread를 발생시켜 주면 된다.
Thread에서 처리하는 작업에 오류가 발생하면 Thread가 중단된다.
Thread는 동시에 처리하고자 하는 작업이 있거나 오류가 발생할 가능성이 있는 작업을 처리할 때 사용한다.

자바에서는 Thread클래스를 상속받은 클래스를 정의하거나 Runnable 인터페이스를 구현해야 하는데 코틀린에서는 init블럭처럼 thread블럭이 제공된다.

onCreate메서드가 끝나면 화면이 정상적으로 출력되어야 하는데
onCreate메서드 안에 while문이 무한히 반복되면 onCreate메서드가 끝이 나지 않기 때문에 화면이 출력되지 않는다.

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        activityMainBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(activityMainBinding.root)

        while(true){
            SystemClock.sleep(100)
            val now = System.currentTimeMillis()
            activityMainBinding.textView.text = "현재 시간 : $now"
        }
    }

하나의 쓰레드가 while문 안에 있는 코드를 처리하고 있기 때문

while문을 thread 블럭에 넣어주면 비동기로 실행이 가능하다.

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        activityMainBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(activityMainBinding.root)

        // 쓰래드를 발생시킨다.
        thread {
            while(true){
                SystemClock.sleep(100)
                val now = System.currentTimeMillis()
                Log.d("test1234", "Thread1 - $now")
                activityMainBinding.textView.text = "Thread1 - $now"
            }
        }

        thread{
            while(true){
                SystemClock.sleep(100)
                val now = System.currentTimeMillis()
                Log.d("test1234", "Thread2 - $now")
                activityMainBinding.textView2.text = "Thread2 - $now"
            }
        }

        activityMainBinding.apply {
            button.setOnClickListener {
                val now = System.currentTimeMillis()
                textView3.text = "버튼 클릭 : $now"
            }
        }

    }

Ui Thread, original Thread는 안드로이드 OS가 발생시킨 쓰레드, 화면 갱신, 생명주기와 관련된 메서드 호출, 리스너 처리 등의 일을 담당하게 된다. 이 쓰레드가 호출하는 메서드 내부의 코드는 무조건 금방 끝나야 한다.

사용자 쓰레드는 개발자가 thread 코드 블럭을 이용해 발생시키는 쓰레드. 오래걸리는 작업이나, 오류가 발생될 가능성이 있는 작업을 담당하게 한다.

위에 작성한 코드는 낮은 안드로이드 버전에서는 에러가 난다. 버전 업데이트 이후 화면 갱신과 관련된 코드는 사용자 쓰레드가 아닌 UI Thread가 담당을 해야하기 때문이다.

runOnUiThread 코드 블럭

        thread {
            while(true){
                SystemClock.sleep(100)
                val now = System.currentTimeMillis()
                Log.d("test1234", "Thread1 - $now")
                // 화면과 관련된 작업을 UI Thread가 처리할 수 있도록 한다.
                runOnUiThread {
                    activityMainBinding.textView.text = "Thread1 - $now"
                }
            }
        }

        thread{
            while(true){
                SystemClock.sleep(100)
                val now = System.currentTimeMillis()
                Log.d("test1234", "Thread2 - $now")
                runOnUiThread {
                    activityMainBinding.textView2.text = "Thread2 - $now"
                }
            }
        }

Ui Thread가 작업중인 경우에는 runOnUiThread블럭에 있는 코드는 대기 상태에 있다가 기존 작업이 끝나면 그때 실행된다.
최소버전에서 그냥 thread블럭에서 실행시켜보고 실행이 가능하면 runOnUiThread없어도 된다.

액티비티가 끝나도 발생한 사용자 thread는 계속 동작한다.
OS가 판단했을 때 시스템 메모리가 부족할 경우 사용자 thread가 모두 종료된다.(이를 방지하기 위해서는 Service를 사용해야 함)
액티비티가 끝날 때 thread도 같이 종료시켜야 한다.

class MainActivity : AppCompatActivity() {

    lateinit var activityMainBinding: ActivityMainBinding

    // 사용자 쓰래드는 Activity가 종료되었다고 하더라도 계속 동작을 한다.
    // OS가 판단했을 때 시스템 메모리가 부족한 경우 사용자 쓰래드들이 모두 종료된다.(이를 방지하기 위해서는 Service를 사용해야 한다)
    // 따라서 Activity가 종료될 때 발생시킨 쓰래드들도 종료되게 해야 한다.
    // Thread 중지 여부를 처리할 변수
    var threadRunFlag1 = false
    var threadRunFlag2 = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        activityMainBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(activityMainBinding.root)

        // 쓰래드를 발생시킨다.
        thread {

            threadRunFlag1 = true

            while(threadRunFlag1){
                SystemClock.sleep(100)
                val now = System.currentTimeMillis()
                Log.d("test1234", "Thread1 - $now")
                // 화면과 관련된 작업을 UI Thread가 처리할 수 있도록 한다.
                runOnUiThread {
                    activityMainBinding.textView.text = "Thread1 - $now"
                }
            }
        }

        thread{

            threadRunFlag2 = true

            while(threadRunFlag2){
                SystemClock.sleep(100)
                val now = System.currentTimeMillis()
                Log.d("test1234", "Thread2 - $now")
                runOnUiThread {
                    activityMainBinding.textView2.text = "Thread2 - $now"
                }
            }
        }

        activityMainBinding.apply {
            button.setOnClickListener {
                val now = System.currentTimeMillis()
                textView3.text = "버튼 클릭 : $now"
            }
        }

    }

    override fun onDestroy() {
        super.onDestroy()
        
        // Activity가 종료될 때 쓰레드 내부의 코드가 종료될 수 있도록
        // while문 조건식에 있는 변수의 값을 false로 설정해준다.
        threadRunFlag1 = false
        threadRunFlag2 = false
        
    }
}

BroadCastReciever

시스템 사용을 감지
매시간 계속 시스템을 감지를 하는것보다 BR을 이용해서
사건과 관련된 이름을 찾아서 관련 메서드를 호출시키는 것

문자수신을 위해
AndroidManifest.xml에 내용 추가(BR때문이 아니라 문자메시지 받으려고)

    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
    <uses-permission android:name="android.permission.RECEIVE_SMS"/>
    <uses-permission android:name="android.permission.READ_PHONE_STATE"/>

    <uses-feature
        android:name="android.hardware.telephony"
        android:required="false" />

        <receiver
            android:name=".BootReceiver"
            android:enabled="true"
            android:exported="true" >
        </receiver>

AndroidManifest.xml에 추가된 receiver 내용을 수정

        <receiver
            android:name=".BootReceiver"
            android:enabled="true"
            android:exported="true" >
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED"/>
            </intent-filter>
        </receiver>
// BootReceiver.kt

package kr.co.lion.android31_br

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.widget.Toast

// AndroidManifest.xml에 android.intent.action.BOOT_COMPLETED 라는 이름으로 등록된 BR
// 부팅이 완료되면 동작한다.

class BootReceiver : BroadcastReceiver() {

    // 사건이 발생했을 때 안드로이드 OS가 호출하는 메서드
    override fun onReceive(context: Context, intent: Intent) {
        // This method is called when the BroadcastReceiver is receiving an Intent broadcast.
        val t1 = Toast.makeText(context, "부팅이 완료되었습니다.", Toast.LENGTH_SHORT)
        t1.show()
    }
}

앱을 깔고 한번 실행해준다음에 재부팅을 하면 토스트메시지가 뜬다

        <receiver
            android:name=".SmsReceiver"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="android.provider.Telephony.SMS_RECEIVED"/>
            </intent-filter>
        </receiver>
// SmsReceiver.kt

package kr.co.lion.android31_br

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.telephony.SmsMessage
import android.util.Log
import android.widget.Toast

// AndroidManifest.xml 에 android.provider.Telephony.SMS_RECEIVED 라는
// 이름으로 등록된 BR
class SmsReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        // This method is called when the BroadcastReceiver is receiving an Intent broadcast.
        // 수신된 문자 정보를 가지고 있는 객체가 있는지 확인한다.
        if(intent.extras != null){
            // 문자 메시지 정보 객체를 추출한다.
            val objs = intent.extras?.get("pdus") as Array<Any?>
            // Array형인 이유는 장문메시지를 보낼때 여러개로 발송되기 때문에
            // 수신된 문자들이 있다면
            if(objs != null){
                // 수신된 문자의 수 만큼 반복한다
                objs.forEach {
                    // 문자 메시지 객체를 추출한다.
                    val format = intent.extras?.getString("format")
                    // 문자 메시지 객체를 생성한다.
                    val currentSMS = SmsMessage.createFromPdu(it as ByteArray?, format)

                    // 전화번호
                    val str1 = """전화번호 : ${currentSMS.displayOriginatingAddress}
                        |내용 : ${currentSMS.displayMessageBody}
                    """.trimMargin()

                    Toast.makeText(context, str1, Toast.LENGTH_LONG).show()
                    Log.d("test1234", str1)

                }
            }
        }
    }
}

서비스 Service

안드로이드 4대 구성 요소 중 하나
백그라운드 프로세서

액티비티를 통해 쓰레드를 발생시키면 액티비티가 종료될 때 쓰레드도 종료된다.
쓰레드를 종료시키지 않게 하려면 액티비티가 아니라 서비스를 통해 쓰레드를 발생시키면 됨

onStartCommand : 서비스가 가동되면 자동으로 호출되는 메서드

onDestroy : 서비스가 중지되면 호출되는 메서드

안드로이드 8.0 이상 부터는 서비스 가동시 알림 메시지를 띄워야 한다.
이때, AndroidManifest.xml에 FOREGROUND_SERVICE 권한을 등록해야 한다.

<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
// TestService.kt
package kr.co.lion.android32_service

import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Binder
import android.os.Build
import android.os.IBinder
import android.os.SystemClock
import android.util.Log
import androidx.core.app.NotificationCompat
import kotlin.concurrent.thread

// 안드로이드 8.0 이상 부터는 서비스 가동시 알림 메시지를 띄워야 한다.
// 이때, AndroidManifest.xml에 FOREGROUND_SERVICE 권한을 등록해야 한다.

class TestService : Service() {

    // 쓰레드의 반복문의 조건식으로 사용할 변수
    var isRunning = false

    var value = 0

    // Activity가 서비스에 접속하면 Activity로 전달될 객체
    val binder = LocalBinder()

    // 외부에서 서비스에 접속하면 호출되는 메서드
    // 여기에서 Binder객체를 반환하면 Binder 객체를 Activity에서 받을 수 있다.
    override fun onBind(intent: Intent): IBinder {
        return binder
    }

    // 서비스가 가동되면 자동으로 호출되는 메서드
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {

        // 안드로이드 8.0 부터는 알림 메시지를 띄워줘야 한다.
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
            // 알림 채널 등록
            addNotificationChannel("Service", "Service")
            // 알림 메시지 구성
            val builder = getNotificationBuilder("Service")
            builder.setSmallIcon(android.R.drawable.ic_btn_speak_now)
            builder.setContentTitle("서비스 가동")
            builder.setContentText("서비스가 가동 중입니다")
            // 알림 메시지를 띄운다
            // 안드로이드 8.0 부터는 서비스 가동 시 알림 메시지를 띄워야 한다.
            // 안드로이드 10.0 에서 서비스에 대한 용도를 지정하는 개념이 추가되었다.
            // 예) startForeground(10,notification,ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
            // 용도를 지정하게 되면 구글에서 용도에 맞는지 확인하고 용도에 맞으면 검수를 통과시켜준다.
            // 이 때, 용도를 명시했고 구글로부터 인증을 받았기 때문에 알림 메시지를 띄우지 않는다.
            // 안드로이드 14버전 부터 서비스의 용도를 명시하는 것이 의무화 되었다.
            // 따라서 안드로이드 14버전으로 테스트 할때는 알림 메시지가 나타나지 않는다.
            // https://developer.android.com/about/versions/14/changes/fgs-types-required?hl=ko
            val notification = builder.build()
            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE){
                startForeground(10,notification,ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
            }else{
                // 안드로이드 13까지는 용도를 명시하지 않음
                startForeground(10,notification)
            }
        }

        // 쓰레드를 가동한다.
        isRunning = true
        thread{
            while(isRunning){
                SystemClock.sleep(500)
                val now = System.currentTimeMillis()
                Log.d("test1234","현재시간 : $now")
                value++
            }
        }

        return super.onStartCommand(intent, flags, startId)
    }

    // 서비스가 중지되면 호출되는 메서드
    override fun onDestroy() {
        super.onDestroy()

        // 쓰레드 중단을 위해 변수에 false를 넣어준다.
        isRunning = false
    }

    // Notification Channel 등록
    // 첫 번째 : 코드에서 채널을 관리하기 위한 이름
    // 두 번째 : 사용자에게 보여줄 채널의 이름
    fun addNotificationChannel(id:String, name:String){
        // 안드로이드 8.0 이상일 때만 동작하게 한다.
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
            // 알림 메시지를 관라하는 객체를 가져온다.
            val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
            // 해당 채널이 등록되어 있는지 확인한다.
            // 채널이 등록되어 있지 않으면 null을 반환한다.
            val channel = notificationManager.getNotificationChannel(id)
            // 등록된 채널이 없다면
            if(channel == null){
                // 채널 객체를 생성한다.
                val newChannel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_HIGH)
                // 진동을 사용할 것인가.
                newChannel.enableVibration(true)
                // 채널을 등록한다.
                notificationManager.createNotificationChannel(newChannel)
            }
        }
    }

    // Notification 메새지를 생성하기 위한 객체를 반환하는 메서드
    fun getNotificationBuilder(id:String) : NotificationCompat.Builder{
        // 안드로이드 8.0 이상이면 마지막 매개변수에 채널 id를 설정해줘야 한다.
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
            val builder = NotificationCompat.Builder(this, id)
            return builder
        }  else {
            val builder = NotificationCompat.Builder(this)
            return builder
        }
    }

    // 프로퍼티의 값을 반환하는 메서드
    fun getNumber():Int{
        return value
    }

    // Activity에서 서비스에 접속하고 서비스 객체를 반환 받기 위한 클래스
    inner class LocalBinder : Binder(){
        fun getService() : TestService{
            return this@TestService
        }
    }
}
// MainActivity.kt
package kr.co.lion.android32_service

import android.app.ActivityManager
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.IBinder
import kr.co.lion.android32_service.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

    lateinit var activityMainBinding: ActivityMainBinding

    // 서비스 가동을 위해 사용할 Intent
    lateinit var serviceIntent:Intent

    // 서비스 객체의 주소값을 담을 프로퍼티
    var testService: TestService? = null

    // 서비스 접속을 관리하는 매니저
    lateinit var serviceConnectionClass: ServiceConnectionClass

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        activityMainBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(activityMainBinding.root)

        activityMainBinding.apply {
            button.setOnClickListener {
                // 현재 서비스가 실행중인지 파악한다.
                // 패키지를 포함한 서비스명을 넣어줌
                val chk = isServiceRunning("kr.co.lion.android32_service.TestService")
                // 서비스를 실행하기 위한 Intent 생성
                serviceIntent = Intent(this@MainActivity, TestService::class.java)

                // 서비스가 가동중이 아니라면
                if(chk == false){
                    // 서비스를 가동한다.
                    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
                        startForegroundService(serviceIntent)
                    }else{
                        startService(serviceIntent)
                    }
                }


                // 서비스 접속에 성공하면 Service가 가지고 있는 onBind 메서드가 호출된다.
                //     override fun onBind(intent: Intent): IBinder {
                //        return binder
                //    }

                // Service가 가지고 있는 onBind 메서드에서 반환하는 객체를 OS가 받아둔다.

                // Activity에서 서비스에 접속했을 때 지정한 ServiceConnection 객체에 접근한다.
                // 아래의 두 번째 매개변수
                // bindService(serviceIntent, serviceConnectionClass, BIND_AUTO_CREATE)

                // OS가 ServiceConnection이 가지고 있는 onServiceConnected 메서드를 호출한다.
                // 이때, 두 번째 매개변수에 Service가 전달한 객체를 담아준다.
                // override fun onServiceConnected(name: ComponentName?, service: IBinder?)

                // onServiceConnected 메서드에서 Binder 객체를 통해 서비스 객체의 주소 값을 받아서
                // 서비스 객체에 접근할 수 있다.
                //    val binder = service as TestService.LocalBinder
                //    testService = binder.getService()


                // 서비스에 접속한다.
                serviceConnectionClass = ServiceConnectionClass()
                // BIND_AUTO_CREATE : 서비스가 가동 중이 아닐 때 서비스를 가동시키라는 옵션임. 현재는 동작하지 않음.(명시적으로 동작시키도록 해줘야 함) 매개변수로 요구하는 옵션이기때문에 일단 넣어줌
                bindService(serviceIntent, serviceConnectionClass, BIND_AUTO_CREATE)
            }

            button2.setOnClickListener {
                // 실행중인 서비스를 중단시킨다.
                if(::serviceIntent.isInitialized){
                    stopService(serviceIntent)
                }
            }

            button3.setOnClickListener {
                // 서비스에서 값을 가져온다.
                if(testService != null){
                    val value = testService?.getNumber()
                    textView.text = "value : $value"
                }
            }
        }
    }

    // 서비스가 가동중인지 확인하는 메서드
    fun isServiceRunning(name:String) : Boolean{
        // 서비스 관리자를 추출한다.
        val activityManager = getSystemService(ACTIVITY_SERVICE) as ActivityManager
        // 현재 실행 중인 서비스들을 가져온다.
        val serviceList = activityManager.getRunningServices(Int.MAX_VALUE)
        // 가져온 서비스의 수 만큼 반복한다.
        serviceList.forEach {
            // 현재 서비스의 이름이 동일한지 확인한다.
            if(it.service.className == name){
                return true
            }
        }
        return false
    }

    // 서비스에 접속을 관리하는 클래스
    inner class ServiceConnectionClass : ServiceConnection{
        // 서비스 접속이 성공하게 되면 호출되는 메서드
        override fun onServiceConnected(p0: ComponentName?, service: IBinder?) {
            // TestService에서 정의한 onBind메서드가 리턴하는 서비스 객체 binder를 매개변수 service로 받아옴
            // binder는 TestService에서 LocalBinder클래스로 생성된 객체인데 getService() 메서드를 호출하면 TestClass객체의 주소를 받아옴
            // 따라서 TestClass 클래스 내부에 정의한 getNumber 메서드로 value프로퍼티 값을 가져올 수 있다.
            val binder = service as TestService.LocalBinder
            // 서비스 객체를 추출한다.
            testService = binder.getService()

        }

        override fun onServiceDisconnected(p0: ComponentName?) {
            testService = null
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        // 서비스가 접속 중이라면 접속을 해제한다.
        if(::serviceConnectionClass.isInitialized == true){
            unbindService(serviceConnectionClass)
        }
    }
}

알림 메시지가 뜨지 않는 이유

안드로이드 8.0 부터는 서비스 가동 시 알림 메시지를 띄워야 한다.
안드로이드 10.0 에서 서비스에 대한 용도를 지정하는 개념이 추가되었다.
예) startForeground(10,notification,ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)

용도를 지정하게 되면 구글에서 용도에 맞는지 확인하고 용도에 맞으면 검수를 통과시켜준다.
이 때, 용도를 명시했고 구글로부터 인증을 받았기 때문에 알림 메시지를 띄우지 않는다.

안드로이드 14버전 부터 서비스의 용도를 명시하는 것이 의무화 되었다.
따라서 안드로이드 14버전으로 테스트 할때는 알림 메시지가 나타나지 않는다.

https://developer.android.com/about/versions/14/changes/fgs-types-required?hl=ko

안드로이드 13버전까지는 용도를 명시하지 않아도 된다.




※ 출처 : 멋쟁이사자 앱스쿨 2기, 소프트캠퍼스 
profile
안드로이드공부

0개의 댓글