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
는 Window
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
가 제어한다.)
이렇게 SurfaceHolder
는 Surface
와 SurfaceView
를 제어하기 위해 다음과 같은 API를 제공하고 있다.
SurfaceView
Lifecycle 관련 Callback APIfun surfaceCreated(holder: SurfaceHolder)
fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int)
fun surfaceDestroyed(holder: SurfaceHolder)
Surface
제어 관련 APIfun getSurface() : Surface
fun lockCanvas() : Canvas
Canvas
를 통해서 Surface의 pixel 들을 조작할 수 있다.fun unlockCanvasAndPost()
fun setFixedSize(width: Int, height: Int)
fun setFormat(format: Int)
그리고 해당 API를 이용하여, 다음과 같이 하나의 파란공이 위치를 랜덤하게 변경하는 Custom 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
}
}
}
이에 좀 더 나아가서, Surface
의 내용이 실제 Display에 표시되는 과정은 어떻게 될까?
이를 알기위해선 가장 먼저 BufferQueue
와 SurfaceFlinger
의 개념에 대해서 알고 있어야 한다.
BufferQueue는 그래픽 데이터 버퍼들을 생성하는 컴포넌트 ("생산자")와 이 데이터를 받아서 디스플레이 하거나 추가 프로세싱을 하려는 컴포넌트 ("소비자")를 연결시켜주는 역할을 갖는다. 시스템 전반에 걸쳐 Android 그래픽 데이터 버퍼를 이동시키는 거의 모든 작업들은 BufferQueue를 통해 처리된다.
SurfaceFlinger는 모든 Surface를 추적하고 화면에 그려질 surface 순서를 관장하며, 여러 App 및 System UI 요소에서 오는 그래픽 데이터 버퍼들을 받아 그것들을 최종 Frame Buffer로 합성하는 HW composer에게 전달하는 역할을 담당한다.
그리고 이러한 BufferQueue
와 SurfaceFlinger
의 관계는 아래 그림에서 잘 표현되고 있다.
위의 그림과 같이 App의 Surface와 System UI의 Surface 정보들은 BufferQueue에게 전달하며, SurfaceFlinger는 이를 전달 받아 HW Composer에게 전달한다는 모습을 볼 수 있다. 조금 더 자세하겐 HW Composer는 Display spec에 따라 합성을 지원 할수도 안할 수도 있으며, 안할 경우에는 SurfaceFilger가 해당 역할을 맡게 된다.
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
위의 내용을 보면, App은 언제든지 그래픽 버퍼를 전달할 수 있는 것처럼 보인다. 하지만 SurfaceFlinger는 장치에 따라 다를 수 있는 디스플레이 refersh 사이에만 buffer를 받기 위해 깨어난다. 이러한 동작은 메모리 사용을 최소화 하며, refersh 중 화면을 업데이트할때 발생할 수 있는 tearing현상을 피할 수 있게 한다. 참고로, Display 갱신 주기가 60Hz로 이뤄지고 있는 스마트폰 Camera 역시 Vsync를 적용하여 Lagging 현상을 최소화할 수 있었다. (preview frame이 drop될 수 있을지언정)
마지막으로, 이러한 Surface ↔ BufferQueue ↔ SurfaceFlinger에 대한 관계는 다음과 같이 정의될 수 있다.
dequeueBuffer()
를 호출하여 BufferQueue에 Free Buffer를 요청하여 Buffer의 너비, 높이, 픽셀 형식과 사용 플래그를 지정한다. queueBuffer()
를 호출하여 버퍼를 채우고 버퍼를 대기열에 반환한다. acquireBuffer()
로 버퍼를 획득하고 버퍼 콘텐츠를 활용한다. releaseBuffer()
를 호출하여 버퍼를 대기열에 반환한다. 이렇게 Android SurfaceView
의 특성과 동작 방식에 대해서 알아보았다.
해당 내용은 정말 빙산의 일각일 뿐이며.. 정리하면 할수록 Android 개발자 입장에서 SurfaceView
를 쉽게 사용하고 있는 것에 대해 감사함을 느낄 수 있었다.
1. How did Android SurfaceView Works?
2. [Android] SurfaceView 개념
3. Android Graphics 번역 4편 - SurfaceFlinger와 Hardware composer