[Kotlin] 8장. 앱 개발 - 카메라, 갤러리, 쓰레드

Hwichan Ji·2020년 12월 26일
0

Kotlin

목록 보기
8/11
post-thumbnail

이것이 안드로이드다 with 코틀린(고돈호 지음) 으로 공부한 내용을 정리한 글입니다.

카메라와 갤러리

카메라

권한

기기의 카메라 기능을 이용하려고 할 경우 카메라에 대한 권한과 카메라로 촬영한 사진에 대한 접근 권한을 얻어야 합니다.

// AndroidManifest.xml
<uses-permission android:name="android.permission.CAMERA"/>
// kotlin
if(ContextCompat.checkSelfPermission(this, permission) !=
        PackageManager.PERMISSION_GRANTED) {
    ActivityCompat.requestPermissions(this, permissions, flag)
    return false
}

// ...

override fun onRequestPermissionsResult(requestCode: Int,
                                        permissions: Array<out String>,
                                        grantResults: IntArray) {
    // ...                            
}

카메라 앱 호출

카메라 앱을 호출하는 것은 카메라 액티비티를 실행하는 것과 같으므로 Intent를 통해 카메라 앱을 호출합니다.

val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
startActivityForResult(intent, FLAG)

카메라로 촬영한 사진 정보는 onActivityReult 메서드로 전달됩니다.

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(reqeustCode, resultCode, data)
    if(resultCode == Activity.RESULT_OK) {
        when (requestCode) {
            FLAG -> {
                if(data?.extras?.get("data") != null) {
                    val bitmap = data?.extras?.get("data") as Bitmap
                    imagePreview.setImageBitmmap(bitmap)
                }
            }
        }
    }
}

갤러리

촬영한 사진 저장

외부 저장소에 촬영한 사진을 저장하려면 MediaStore를 이용해야 합니다.

MediaStore
안드로에드에서 외부 저장소를 관리하는 데이터베이스

권한

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

이미지 파일 저장 메서드 정의

fun saveImageFile(filename: String, mimeType: String, bitmap: Bitmap) : Uri? {
    var values = ContentValues()
    values.put(MediaStore.Images.Media.DISPLAY_NAME, filename)
    values.put(MediaStore.Images.MIME_TYPE, mimeType)
    
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        // 파일 저장을 완료하기 전까지 다른 곳에서 해당 데이터를 요청하는 것을 무시
       values.put(MediaStore.Images.Media.IS_PENDING, 1)
    }
    
    // MediaStore에 파일 등록
    val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
    try {
        if (uri != null) {
            // 파일 디스크립터 획득
            var descriptor = contentResolver.openFileDescriptor(uri, "w")
            if (descriptor != null) {
                // FileOutputStream으로 비트맵 파일 저장. 숫자는 압축률
                val fos = FileOutputStream(descriptor.fileDescriptor)
                bimap.compress(Bitmap.CompressFormat.JPEG, 100, fos)
                fos.close()
                
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                    // 데이터 요청 무시 해제
                    values.clear()
                    values.put(MediaStore.Images.Media.IS_PENDING, 0)
                    contentResolver.update(uri, values, null, null)
                }
            }
        }
    } catch (e:java.lang.Exception) {
        Log.e("File", "error=${e.locatizedMessage}")
    }
    return uri
}

URI
통합 자원 식별자는 특정 리소스 자원을 고유하게 실벽할 수 잇는 식별자를 의미

갤러리에서 사진 가져오기

권한

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

갤러리 앱 호출

카메라 앱을 호출할 때와 마찬가지로 Intent를 통해 갤러리 앱을 호출합니다.

val intent = Intent(Intent.ACTION_PICK)
intent.type = MediaStore.Images.Media.CONTENT_TYPE
startActivtyForResult(intent, FLAG_REQ_STORAGE)

갤러리 앱에서 선택한 이미지

갤러리 앱에서 선택한 이미지에 대한 정보가 onActivityResult 메서드로 전달됩니다.

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    //...
    when (reqeustCode) {
        FLAG -> {
            val uri = data?.data
            imagePreview.setImageURI(uri)
        }
    }
}

쓰레드

쓰레드와 루퍼

안드로이드의 쓰레드는 크게 1개의 메인 쓰레드와 여러개의 워커 쓰레드로 나눌 수 있습니다.

메인 쓰레드

기본적으로 모든 컴포넌트는 단일 프로세스 및 메인 쓰레드에서 실행됩니다. 메인 쓰레드는 다음과 같은 특징을 같습니다.

  • 화면 UI 그리기 담당
  • UI 툴킷의 구성요소와 상호작용하고 UI 이벤트를 사용자에게 응답
  • UI 쓰레드라고도 불림

안드로이드 시스템은 UI 이벤트 및 작업에 대해 수 초 내에 응답하지 않으면 앱을 종료시키고 ANR 팝업창을 띄웁니다. 따라서 시간이 오래 걸리는 작업은 워커 쓰레드를 생성해서 처리해야 합니다.

ANR: Application Not Responding

워커 쓰레드

네트워크 작업, 시간이 오래 걸리는 계산, 파일 업로드와 다운로드, 이미치 처리, 데이터 로딩 등은 처리 시간을 미리 계산할 수 없습니다. 이러한 작업들은 워커 쓰레드를 통해 백그라운드에서 처리하는 것을 권장합니다. 따라서 워커 쓰레드는 백그라운드 쓰레드라고도 불립니다.

Thread 클래스

Thread 클래스를 상속받아 쓰레드를 생성할 수 있습니다.

class WorkerThread : Thread() {
    override fun run() {
        // 워커 쓰레드에서 수행할 작업 정의
    }
}

// ...

override fun onCreate(savedInstanceState: Bundle?) {
    // ...
    var thread = WorkerThread()
    thread.start() // 쓰레드 실행
}

Runnable 인터페이스

Runnable 인터페이스를 구현해 쓰레드를 생성할 수 있습니다.

class WorkerRunnable: Runnable {
    override fun run() {
        // 워커 쓰레드에서 수행할 작업 정의
    }
}

Runnable 인터페이스를 구현한 객체는 Thread 클래스의 생성자로 전달하고 Thread 클래스의 start() 메서드를 호출해야 쓰레드가 생성됩니다.

override fun onCreate(savedInstanceState: Bundle?) {
    // ...
    var thread = Thread(WorkerRunnable())
    thread.start() // 쓰레드 실행
}

Runnable 인터페이스는 자바나 코틀린에서 다중 상속을 허용하지 않기 때문에 이미 다른 클래스를 상속받은 클래스도 쓰레드를 구현할 수 있도록 하기 위해 사용됩니다.

람다식으로 Runnable 익명 객체 구현

코틀린에서 인터페이스 내부에 메서드가 하나만 있는 경우 람다식으로 변환이 가능합니다.

Thread {
    // run() 메소드에 정의했던 작업 작성
}.start()

람다식
익명함수를 생성하기 위한 식으로 객체 지향적이기 보다 함수 지향적임

코틀린에서 제공하는 thread() 구현

코틀린에서는 다음과 같이 백그라운드를 사용할 수 있습니다.

thread(start=true) {
    // 백그라운드 작업 정의
}

핸들러와 루퍼

UI는 메인 쓰레드에서만 접근할 수 있으며 백그라운드 쓰레드에서 UI에 접근하는 경우 예외가 발생하고 앱이 종료됩니다. 따라서 백그라운드에서 UI에 접근해야 하는 경우 UI 작업을 메인 쓰레드에 의뢰해야 하는데, 이때 사용하는 것이 핸들러와 루퍼입니다.

루퍼

루퍼는 지속적으로 루프를 돌아 Message Queue에 메시지가 들어오는 것을 감지하고 메시지를 꺼내서 해당 메시지와 연결된 핸들러를 호출합니다.

메인 쓰레드는 기본적으로 루퍼를 가지고 있으나 워커 쓰레드는 루퍼를 생성해야 합니다. 루퍼는 하나의 쓰레드만 담당할 수 있고 하나의 쓰레드도 오직 하나의 루퍼만 가질 수 있습니다.

핸들러

핸들러는 루퍼와 연결된 메시지 큐로 메시지나 Runnable 객체를 보내고 메시지 큐에서 꺼내진 메시지를 처리합니다. 핸들러는 생성과 동시에 해당 핸들러를 생성한 쓰레드에 연결되어 그 쓰레드의 루퍼 및 Message Queue에 대한 참조를 갖게 됩니다. 쓰레드 하나에 여러 개의 핸들러를 생성할 수 있습니다.

메시지 전달 과정

  1. 메인 쓰레드에 Handler 객체 생성
  2. 수신한 메시지를 처리하기 위한 handleMessage 메소드 override
  3. 유저 쓰레드에서 메인 쓰레드의 Handler 객체에 대한 참조 획득
  4. 유저 쓰레드에서 obtainMessage 메소드로 메시지를 생성
  5. sendMessage 메소드를 통해 유저 쓰레드에서 메인 쓰레드의 Message Queue로 메시지 전달

AsyncTask

AsyncTask 클래스는 비동기 처리를 할 수 있도록 쓰레드와 핸들러 기능을 하나의 클래스에 합쳐놓은 것입니다.

AsyncTask 구조

AsyncTask 클래스 내부에 3가지 인터페이스를 구현하는 것만으로 쓰레드와 핸들러 생성을 대체할 수 있습니다.

  • doInBackground(): 백그라운드에서 수행할 작업 정의
  • onProgressUpdate(): doInBackground() 블럭에서 publishProgress()가 호출될 때마다 실행되는 메서드로 메인 쓰레드에서 동작하고 파일 다운로드 시에 현재 진행률을 보여주는 형태로 많이 사용됨
  • onPostExecute(): doInBackground()가 완료된 후에 호출되는 메서드로 메인 쓰레드에서 동작합니다. 따라서 작업의 결과를 UI에 업데이트할 수 있습니다.
val asyncTask = object : AsyncTask<Void, Int, String>() {
    override fun doInBackground(vararg params: Void?): String {
    
    }
    override fun onProgressUpdate(vararg values: Int?) {
        
    }
    override fun onPostExecute(result: String?) {
    
    }
}

asyncTask.execute()

가변인자 vararg
파라미터의 개수가 가변적일 경우 vararg로 선언함. 파라미터는 temp[0]과 같이 배열처럼 사용

AsyncTask 제약사항

  • 한 번 실행한 AsyncTask는 다시 실행할 수 없고 새로운 AsyncTask를 생성해 실행해야 함
  • AsyncTask를 사용해서 스케쥴링 할 수 있는 작업 수의 제한이 있음
  • 몇 초정도의 짧은 작업에서만 이상적으로 동작함
profile
안드로이드 개발자를 꿈꾸는 사람

0개의 댓글