[Anrdroid] SurfaceView 기본 + 심화

mhyun, Park·2023년 10월 31일
2

SurfaceView는 Android View 클래스를 상속하고 있는 class로써 Main UI Thread에서 동작되는 일반 Android View들과 달리, 별도 Thread에서 동작하는 View 클래스이다.

SurfaceView가 Main Thread가 아닌 Thread에서 동작한다는 말을 듣자마자 어렴풋이 알 수 있듯이 비용이 높은 작업들이 동작하게 된다. 예를 들어, Camera Peview나 Video 재생 그리고 3D 작업같은 처리들은 1초에 수십프레임이 그려져야하는데 이러한 무거운 작업들이 Android UI(Main) Thread 혼자 감당하게 되면, 결국 자원이 부족하여 Application의 정상적인 동작을 보장하기 어렵기 때문이다.

즉, SurfaceView는 고성능이 요구되는 환경 속에서도 View를 원할하게 업데이트 해줄 수 있으며 그리기 Task를 Android 시스템에 맡기는 것이 아니라 추가적인 Thread를 이용해 원하는 시점에 강제로 화면에 그릴 수 있다.

SurfaceView 특성 및 구조

다음은 SurfaceView의 특성을 간단하게 표현한 그림이다.

위의 그림처럼 SurfaceViewWindow layer 아래에 존재하며 Window를 뚫어서(punched) 자신이 보여지게끔 한다. 만약, 해당 Window 위에 다른 View가 있는 경우는 함께 Blending 되어 보여지게 되며 화면 변화마다 전체적인 합성이 실행되기 때문에 성능에 영향을 끼칠 수 있다.

그렇다면, 2개의 SurfaceView를 조금이라도 겹친 상태로 동시에 운용하는 경우에는 어떻게 동작될까?

앞서 말한대로, 최종적인 화면은 Blending된 하나의 View가 화면에 뿌려지기 때문에 같은 layer에 위치한 SurfaceView들 간의 Z-order에 따른 우선 순위에 따라 화면에 보여진다. 우선 순위가 높은 SurfaceView는 우선 순위가 낮은 SurfaceView를 덮게 되며, SurfaceView.setZOrderMediaOverlay(true or false) 를 통해 두 SurfaceView간의 Z-order를 동적으로 설정/변경할 수 있다.

다음은 SurfaceView의 내부 구조이다.
SurfaceView는 실제 Content가 저장되는 Buffer인 (1) Surface 그리고 Surface와 SurfaceView 중간에서 상태 제어를 하는 (2) SurfaceHolder로 이루어져 있다.

다들 알다시피 Android 정책 상 Main Thread 이외의 Thread는 UI 구성요소에 대한 직접 접근은 허락되지 않는듯이 SurfaceView 또한 오로지 SurfaceHolder를 통해서만 Surface를 업데이트하거나 Event를 처리할 수 있다. (+ Multi-Thread 접근또한 SurfaceHolder가 제어한다.)

이렇게 SurfaceHolderSurfaceSurfaceView를 제어하기 위해 다음과 같은 API를 제공하고 있다.

SurfaceView Lifecycle 관련 Callback API

  • fun surfaceCreated(holder: SurfaceHolder)
  • fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int)
  • fun surfaceDestroyed(holder: SurfaceHolder)

Surface 제어 관련 API

  • fun getSurface() : Surface
  • fun lockCanvas() : Canvas
    : surface update를 위한 함수로, 반환 된 Canvas를 통해서 Surface의 pixel 들을 조작할 수 있다.
  • fun unlockCanvasAndPost()
    : surface update가 끝나면 호출해야하는 함수로, 해당 함수가 호출되면, 현재 Surface content가 view에 보여지게 된다.
  • fun setFixedSize(width: Int, height: Int)
  • fun setFormat(format: Int)

그리고 해당 API를 이용하여, 다음과 같이 하나의 파란공이 위치를 랜덤하게 변경하는 Custom SurfaceView를 작성할 수 있다.

SurfaceView 예제

ChatGPT 감사합니다^^7

class DrawingSurfaceView(context: Context) : SurfaceView(context), SurfaceHolder.Callback {
    private var drawingThread: DrawingThread? = null

    init {
        surfaceHolder.addCallback(this)
    }

    override fun surfaceCreated(holder: SurfaceHolder) {
        drawingThread = DrawingThread(holder)
        drawingThread?.start()
    }

    override fun surfaceChanged(surfaceHolder: SurfaceHolder, format: Int, width: Int, height: Int) {}
    
    override fun surfaceDestroyed(surfaceHolder: SurfaceHolder) {
        var retry = true
        drawingThread?.requestStop()
        while (retry) {
            try {
                drawingThread?.join()
                retry = false
            } catch (e: InterruptedException) {
                // Retry until the thread is successfully joined
            }
        }
    }

    private inner class DrawingThread(private val surfaceHolder: SurfaceHolder) : Thread() {
        private var isRunning = true

        private var ballX = 200f
        private var ballY = 200f
        private var ballRadius = 50f

        fun requestStop() {
            isRunning = false
        }

        override fun run() {
            while (isRunning) {
                val canvas = surfaceHolder.lockCanvas()
                try {
                    synchronized(surfaceHolder) {
                        // Update the ball position
                        updateBallPosition()

                        // Draw on the canvas
                        canvas.drawColor(Color.WHITE)
                        canvas.drawCircle(ballX, ballY, 100f, Paint().apply { color = Color.BLUE })
                    }
                } finally {
                    // Unlock the canvas and post it to the SurfaceView
                    surfaceHolder.unlockCanvasAndPost(canvas)
                }
            }
        }


        private fun updateBallPosition() {
            // Move the ball in a random direction
            when (Random.nextInt(0, 4)) {
                0 -> ballX += 10f
                1 -> ballX -= 10f
                2 -> ballY += 10f
                3 -> ballY -= 10f
            }

            // Ensure the ball stays within the bounds of the SurfaceView
            if (ballX < ballRadius) ballX = ballRadius
            if (ballX > width - ballRadius) ballX = width - ballRadius
            if (ballY < ballRadius) ballY = ballRadius
            if (ballY > height - ballRadius) ballY = height - ballRadius
        }
    }
}

SurfaceView 심화

이에 좀 더 나아가서, Surface의 내용이 실제 Display에 표시되는 과정은 어떻게 될까?
이를 알기위해선 가장 먼저 BufferQueueSurfaceFlinger의 개념에 대해서 알고 있어야 한다.

BufferQueue

BufferQueue는 그래픽 데이터 버퍼들을 생성하는 컴포넌트 ("생산자")와 이 데이터를 받아서 디스플레이 하거나 추가 프로세싱을 하려는 컴포넌트 ("소비자")를 연결시켜주는 역할을 갖는다. 시스템 전반에 걸쳐 Android 그래픽 데이터 버퍼를 이동시키는 거의 모든 작업들은 BufferQueue를 통해 처리된다.

SurfaceFlinger

SurfaceFlinger모든 Surface를 추적하고 화면에 그려질 surface 순서를 관장하며, 여러 App 및 System UI 요소에서 오는 그래픽 데이터 버퍼들을 받아 그것들을 최종 Frame Buffer로 합성하는 HW composer에게 전달하는 역할을 담당한다.

그리고 이러한 BufferQueueSurfaceFlinger의 관계는 아래 그림에서 잘 표현되고 있다.

위의 그림과 같이 App의 Surface와 System UI의 Surface 정보들은 BufferQueue에게 전달하며, SurfaceFlinger는 이를 전달 받아 HW Composer에게 전달한다는 모습을 볼 수 있다. 조금 더 자세하겐 HW Composer는 Display spec에 따라 합성을 지원 할수도 안할 수도 있으며, 안할 경우에는 SurfaceFilger가 해당 역할을 맡게 된다.

SurfaceFligner ↔ HW Composer

  • SurfaceFlingerHWC(Hardware Composer)에게 전체 Layer list를 제공하고 어떻게 처리하고 싶은지 물어본다.
  • HWC는 이러한 질문에 대해 각 layer에 overlay 또는 GLES 합성 이라고 표시해서 응답한다.
  • SurfaceFlinger는 GLES 합성을 처리하여 출력 버퍼를 HWC에 전달하고 나머지 작업을 HWC에 맡긴다.
    type    |          source crop              |           frame           name
------------+-----------------------------------+-----------------------------------------------------------------------
        HWC | [    0.0,    0.0,  320.0,  240.0] | [   48,  411, 1032, 1149] SurfaceView
        HWC | [    0.0,   75.0, 1080.0, 1776.0] | [    0,   75, 1080, 1776] com.android.sample.PlayMovieSurfaceActivity
        HWC | [    0.0,    0.0, 1080.0,   75.0] | [    0,    0, 1080,   75] StatusBar
        HWC | [    0.0,    0.0, 1080.0,  144.0] | [    0, 1776, 1080, 1920] NavigationBar
  FB TARGET | [    0.0,    0.0, 1080.0, 1920.0] | [    0,    0, 1080, 1920] HWC_FRAMEBUFFER_TARGET

VSYNC

위의 내용을 보면, App은 언제든지 그래픽 버퍼를 전달할 수 있는 것처럼 보인다. 하지만 SurfaceFlinger는 장치에 따라 다를 수 있는 디스플레이 refersh 사이에만 buffer를 받기 위해 깨어난다. 이러한 동작은 메모리 사용을 최소화 하며, refersh 중 화면을 업데이트할때 발생할 수 있는 tearing현상을 피할 수 있게 한다. 참고로, Display 갱신 주기가 60Hz로 이뤄지고 있는 스마트폰 Camera 역시 Vsync를 적용하여 Lagging 현상을 최소화할 수 있었다. (preview frame이 drop될 수 있을지언정)

마지막으로, 이러한 Surface ↔ BufferQueue ↔ SurfaceFlinger에 대한 관계는 다음과 같이 정의될 수 있다.

  • Surface(생산자)dequeueBuffer()를 호출하여 BufferQueue에 Free Buffer를 요청하여 Buffer의 너비, 높이, 픽셀 형식과 사용 플래그를 지정한다.
  • Surface(생산자)queueBuffer()를 호출하여 버퍼를 채우고 버퍼를 대기열에 반환한다.
  • SurfaceFlinger(소비자)acquireBuffer()로 버퍼를 획득하고 버퍼 콘텐츠를 활용한다.
  • 이를 완료한 SurfaceFlinger(소비자)releaseBuffer()를 호출하여 버퍼를 대기열에 반환한다.

이렇게 Android SurfaceView의 특성과 동작 방식에 대해서 알아보았다.
해당 내용은 정말 빙산의 일각일 뿐이며.. 정리하면 할수록 Android 개발자 입장에서 SurfaceView를 쉽게 사용하고 있는 것에 대해 감사함을 느낄 수 있었다.

1. How did Android SurfaceView Works?
2. [Android] SurfaceView 개념
3. Android Graphics 번역 4편 - SurfaceFlinger와 Hardware composer

profile
Android Framework Developer

0개의 댓글