일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 큐 자바 코드
- 안드로이드 os 구조
- rxjava cold observable
- 플러터 설치 2022
- 멤버변수
- 자바 다형성
- 안드로이드 레트로핏 crud
- jvm이란
- android retrofit login
- 안드로이드 라이선스
- 안드로이드 유닛 테스트 예시
- rxjava hot observable
- 객체
- 서비스 vs 쓰레드
- 안드로이드 라이선스 종류
- Rxjava Observable
- 클래스
- rxjava disposable
- 안드로이드 유닛테스트란
- android ar 개발
- 스택 큐 차이
- 서비스 쓰레드 차이
- jvm 작동 원리
- 2022 플러터 안드로이드 스튜디오
- 2022 플러터 설치
- 안드로이드 유닛 테스트
- ar vr 차이
- 안드로이드 레트로핏 사용법
- ANR이란
- 스택 자바 코드
- Today
- Total
나만을 위한 블로그
[Android] CameraX 코드랩 뜯어보기 - 1 - 본문
카메라는 내게 많이 생소한 영역이기도 하고 예전에 CameraX인지 뭔지가 새로 나왔다고 들었어서 최근에 코드랩을 따라 쳐보고 공부하긴 했었는데, 블로그에 남겨두면 나중에 찾아보기 더 좋을 것 같아서 남겨둔다.
시작하기 전에 내가 본 코드랩 주소는 아래에 남겨둔다.
https://developer.android.com/codelabs/camerax-getting-started#0
먼저 앱 수준 gradle에 의존성 몇 개를 추가한다. 22.06.05 기준으로 최신 버전은 1.2.0-alpha02지만 코드랩 기준으로 진행한다.
def camerax_version = "1.1.0-beta01"
implementation "androidx.camera:camera-core:$camerax_version"
implementation "androidx.camera:camera-camera2:$camerax_version"
implementation "androidx.camera:camera-lifecycle:$camerax_version"
implementation "androidx.camera:camera-video:$camerax_version"
implementation "androidx.camera:camera-view:$camerax_version"
implementation "androidx.camera:camera-extensions:$camerax_version"
그리고 카메라를 사용할 것이기 때문에 관련 권한들도 매니페스트에 까먹지 않고 정의해둔다. 영상 녹화까지 다루기 때문에 RECORD_AUDIO 퍼미션이 있는 게 보인다.
<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
그리고 자바 1.8 이상을 사용하기 위해 아래 코드를 넣어야 하는데, 범블비 이상이라면 기본으로 설정돼 있을 것이다. 만약 없다면 추가한다. 그리고 뷰 바인딩도 추가해준다.
buildFeatures {
viewBinding true
}
.
.
.
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
그리고 strings.xml에 문자열들을 몇 가지 넣어준다.
<resources>
<string name="app_name">CameraXApp</string>
<string name="take_photo">Take Photo</string>
<string name="start_capture">Start Capture</string>
<string name="stop_capture">Stop Capture</string>
</resources>
그리고 메인 액티비티의 XML 코드를 복붙한다. 다른 이름의 액티비티에서 쓸 거라면 필요한 부분만 복붙한다.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.camera.view.PreviewView
android:id="@+id/viewFinder"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<Button
android:id="@+id/image_capture_button"
android:layout_width="110dp"
android:layout_height="110dp"
android:layout_marginBottom="50dp"
android:layout_marginEnd="50dp"
android:elevation="2dp"
android:text="@string/take_photo"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintEnd_toStartOf="@id/vertical_centerline" />
<Button
android:id="@+id/video_capture_button"
android:layout_width="110dp"
android:layout_height="110dp"
android:layout_marginBottom="50dp"
android:layout_marginStart="50dp"
android:elevation="2dp"
android:text="@string/start_capture"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/vertical_centerline" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/vertical_centerline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent=".50" />
</androidx.constraintlayout.widget.ConstraintLayout>
그리고 메인 액티비티 소스코드를 붙여넣는다.
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.Manifest
import android.content.pm.PackageManager
import android.util.Log
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import java.util.concurrent.Executors
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.video.*
import androidx.camera.video.VideoCapture
import androidx.core.content.PermissionChecker
import com.android.example.cameraxapp.databinding.ActivityMainBinding
import java.nio.ByteBuffer
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ExecutorService
import android.provider.MediaStore
import android.content.ContentValues
import android.os.Build
typealias LumaListener = (luma: Double) -> Unit
class MainActivity : AppCompatActivity() {
private lateinit var viewBinding: ActivityMainBinding
private var imageCapture: ImageCapture? = null
private var videoCapture: VideoCapture<Recorder>? = null
private var recording: Recording? = null
private lateinit var cameraExecutor: ExecutorService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(viewBinding.root)
// Request camera permissions
if (allPermissionsGranted()) {
startCamera()
} else {
ActivityCompat.requestPermissions(
this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
}
// Set up the listeners for take photo and video capture buttons
viewBinding.imageCaptureButton.setOnClickListener { takePhoto() }
viewBinding.videoCaptureButton.setOnClickListener { captureVideo() }
cameraExecutor = Executors.newSingleThreadExecutor()
}
private fun takePhoto() {}
private fun captureVideo() {}
private fun startCamera() {}
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(baseContext, it)
== PackageManager.PERMISSION_GRANTED
}
override fun onDestroy() {
super.onDestroy()
cameraExecutor.shutdown()
}
companion object {
private const val TAG = "CameraXApp"
private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
private const val REQUEST_CODE_PERMISSIONS = 10
private val REQUIRED_PERMISSIONS =
mutableListOf (
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO
).apply {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
}.toTypedArray()
}
}
먼저 allPermissionsGranted()부터 보자. REQUIRED_PERMISSIONS는 CAMERA, RECORD_AUDIO 퍼미션이 담긴 수정 가능한 리스트에 안드로이드 파이 이하 버전인 경우 WRITE_EXTERNAL_STORAGE 퍼미션을 추가한 다음 toTypedArray()를 적용한 결과값이 담긴 프로퍼티다.
toTypedArray()는 List를 Array로 바꿔주는 함수인데, 이 함수를 써야 하는 이유는 아래와 같다.
ActivityCompat.requestPermissions() 안에서 빨간 줄이 생기며 컴파일 에러가 발생하는데, 함수가 요구하는 파라미터를 넣지 않았기 때문이다. 이 함수의 시그니처를 확인해 보면 toTypedArray()를 쓴 이유를 더 잘 이해할 수 있다. 자바로 돼 있지만 이해하는 덴 문제없을 것이다.
public static void requestPermissions(final @NonNull Activity activity,
final @NonNull String[] permissions, final @IntRange(from = 0) int requestCode)
이렇게 생겨먹었기 때문에 toTypedArray()를 썼지만, 애초에 처음부터 배열로 만들었다면 이렇게 할 이유가 없지 않았을까 생각된다. 아무튼 이렇게 권한을 요청하는 방법도 있단 걸 알았으니 넘어간다.
다시 onCreate() 안으로 돌아와서 allPermissionsGranted()로 모든 권한이 승낙됐다면 카메라를 시작하고 아니면 다시 사용자에게 권한을 요청하는데 생소한 코드가 보인다.
private lateinit var cameraExecutor: ExecutorService
cameraExecutor = Executors.newSingleThreadExecutor()
ExecutorService와 newSingleThreadExecutor()는 뭘까? 먼저 ExecutorService부터 확인해본다.
https://www.javatpoint.com/java-executorservice
쓰레드에서 작업을 비동기적으로 실행할 수 있게 해주는 인터페이스다. java.util.concurrent 패키지에 있다. 쓰레드풀을 유지관리하고 작업을 할당하는 데 도움이 된다. 작업 수가 사용 가능한 쓰레드보다 많은 경우 사용 가능한 쓰레드가 있을 때까지 작업을 대기열(queue)에 추가하는 기능을 제공한다
https://jaeryo2357.tistory.com/50
구조로만 보면 Runnable 객체를 실행하는 단순한 구조다. 클래스 자체로 어떤 백그라운드 프로세스가 생성되는 게 아닌 틀만 제공해준다. 따라서 대부분의 사용자는 Executor를 구현해서 백그라운드 작업을 수행하는 클래스를 정의한다
Executor의 구조를 보면 별도 쓰레드를 만들지 않는 걸 볼 수 있다. 이 말은 해당 객체를 호출한 쓰레드에서 Runnable을 실행한다는 뜻이다. 따라서 네트워크 I/O 작업을 메인 쓰레드에서 진행할 수 없고 UI가 멈춘 채 Runnable이 실행되면 사용자에게 불친절하기 때문에 별도의 쓰레드 풀을 갖고 있어야 한다. Executors 클래스는 ExecutorService라는 Executor 인터페이스를 확장하고 쓰레드 사이클을 관리하는 쓰레드 풀 객체를 만들어주는 여러 팩토리 함수를 갖고 있다
쓰레드 개수가 무분별하게 늘어나다 보면 그걸 관리하는 CPU에 과부하가 생긴다. Executors.newFixedThreadPool(int poolsize)는 동시 실행되는 쓰레드 개수를 매개변수로 받은 size만큼 조정한다. 2를 받았다면 3개 작업이 들어왔을 때 마지막으로 들어온 작업 1개는 앞의 2개 작업이 끝날 때까지 대기한다
ExecutorService는 쓰레드 풀을 관리하고 비동기 작업을 수행할 때 사용하는 인터페이스로 작업 개수가 사용 가능한 쓰레드보다 많은 경우 어떤 큐에 작업을 추가한다. 이 큐는 작업을 순서대로 갖고 있다가 메인 쓰레드에 순서대로 할당한다. onDestroy()에서 이 객체를 통해 shutDown()을 명시적으로 호출한 걸 보니 한 번 사용했다면 반드시 닫아줘야 하는 객체인 듯하다.
newSingleThreadExecutor()는 쓰레드가 1개인 ExecutorService를 리턴하는 메서드로 싱글 쓰레드에서 동작해야 하는 작업을 처리할 때 사용한다. 이 메서드의 정보는 아래 사이트에서 확인할 수 있다.
https://developer.android.com/reference/java/util/concurrent/Executors#newSingleThreadExecutor()
무제한 큐에서 작동하는 단일 워커 쓰레드를 쓰는 실행기를 만든다. 그러나 이 단일 쓰레드가 종료 전 실행 중 실패로 인해 종료되면 후속 작업을 실행하는 데 필요한 경우 새 쓰레드가 그 자리를 차지한다. 작업은 순차 실행되고 하나 이상의 작업이 활성화되지 않는다. newFixedThreadPool(1)과 달리 반환된 실행자는 추가 쓰레드를 사용하도록 재구성되지 않게 보장된다
그 외에는 시그니처만 있고 구현부는 없는 메서드거나, 간단한 상수 뿐이라서 넘겨도 될 듯하다.
'Android' 카테고리의 다른 글
[Android] 안드로이드 스튜디오 범블비에서 SHA-1 키 빨리 확인하는 법 (0) | 2022.06.09 |
---|---|
[Android] 코틀린과 ViewPager2로 미리보기 + 자동 무한 스크롤 기능 구현하는 법 (0) | 2022.06.08 |
[Android] Apostrophe not preceded by \ 에러 해결 (0) | 2022.06.03 |
[Android] Hilt에 대한 고찰 (0) | 2022.06.03 |
[Android] minSdkVersion이 26 미만일 때 코틀린으로 UTC 시간을 원하는 형식으로 바꾸는 법 (0) | 2022.06.01 |