8 minute read

해당 시리즈는 실제 깊은 코드 수준보단 간단한 이론을 바탕으로 글을 작성했기 때문에, 내용에 구현방식이 존재하지 않거나 누락된 내용이 존재할 수 있습니다. 하지만 Android 그래픽 파이프라인에 대한 이해에 도움이 될 수 있기에 참고하시길 바랍니다.

Surface

Surface는 Android 그래픽 시스템의 주요 요소 중 하나로 Producer(생산자)Consumer(소비자)사이 버퍼를 교환할 수 있도록 도와주는 인터페이스입니다.

Surface는 데이터를 그릴 수 있도록 버퍼를 제공 해주며 반대로 화면 렌더링과 같은 케이스에서 버퍼 소비를 도와주는 역할을 수행합니다. 여기서 Surface의 버퍼 내에 데이터를 제공해주는 주체를 Producer(생산자)라고 하며 반대로 채워진 버퍼를 소비하는 주체를 Consumer(소비자)라고 합니다.

여기서 언급한 버퍼는 그래픽 버퍼(Graphic Buffer) 라고 불리며 이는 BufferQueue라는 구조에 저장됩니다. Surface는 이러한 BufferQueue와 밀접하게 연동되어 작동합니다.

정리하자면, Surface는 내부적으로 BufferQueue와 연결되어 있습니다. 이때 Producer는 데이터를 BufferQueue로부터 제공 받은 그래픽 버퍼에 작성하고 반대로 Consumer는 데이터를 가져가 최종 화면을 구성하는 역할을 수행합니다.

핵심 개념

앞서 Surface에 대해 설명하면서 Producer, Consumer…와 같은 용어들이 등장하였습니다. Surface에 대해 더욱 자세하게 이해하기 전 해당 용어들에 대해서 확인하겠습니다.

Producer(생산자)

Surface의 구조를 더욱 자세하게 알기 위해서는 ProducerConsumer가 정확히 무엇을 이해해야만 합니다.

Producer(생산자)Surface와 연결된 Buffer Queue를 요청하고 픽셀 데이터를 직접 그리는 역할을 하는 주체입니다. 이때 그려진 데이터는 최종적으로 화면에 출력되기 위해 Consumer에게 전달됩니다.

Producer의 대표적인 예시는 Canvas, Camera, OpenGL ES 등이 존재합니다. 흔히 애플리케이션 개발에 사용되는 View는 Canavs에 그리는 과정을 통해 Producer의 역할을 수행하게 됩니다. 카메라 하드웨어 센서로 받게 되는 프레임 데이터들 또한 Surface로 부터 제공받은 그래픽 버퍼를 통해 프레임을 그릴 수 있게 됩니다. OpenGL ES는 그래픽 API로 최종적으로 렌더링을 수행하기 위해서는 마찬가지로 Surface의 도움이 필요합니다.

Consumer(소비자)

Consumer(소비자)는 반대로 Producer가 생성한 버퍼 데이터를 소비하는 역할을 수행합니다. Consumer는 버퍼 데이터를 소비하여 처리하거나 화면에 출력될 수 있도록 도와줍니다.

Consumer의 대표적인 예시는 SurfaceFlinger입니다. SurfaceFlinger는 Android에서 여러 애플리케이션들이 생성한 그래픽 데이터들을 최종적으로 합성하여 화면에 출력하는 네이티브 시스템 서비스입니다. (SurfaceFlinger에 대한 내용은 후속 포스트에서 집중적으로 다루게 될 것입니다.)

네이티브 시스템 서비스(Native System Service)란 운영체제가 제공하는 기능 또는 시스템 자원에 접근할 수 있게 도와주는 컴포넌트로 해당 컴포넌트를 사용하여 앱이 직접 하드웨어 OS 내부와 통신하지 않고 시스템 기능을 사용할 수 있게 됩니다. 네이티브 시스템 서비스는 대표적으로 CameraService, MediaServer 등이 있습니다.

BufferQueue

앞서 Surface 그리고 ProducerConsumer에 관한 설명을 하면서 BufferQueue에 대해 언급하였습니다. BufferQueueProducerConsumer가 공유하여 사용하는 버퍼의 풀을 관리하는 구조입니다. 이때 Producer는 버퍼에 그림을 그리고 Consumer는 버퍼를 읽습니다. 다른 말로 풀어쓰자면, BufferQueue 내부에서는 한쪽에서 데이터를 생산하고(In)하고 반대쪽은 데이터를 소비(Out) 하는 구조로 동작하게 됩니다.

BufferQueue는 여러 개의 버퍼를 두어 버퍼를 순환시키는 구조로 사용됩니다. 예를 들어, Producer가 어떤 하나의 버퍼에 데이터를 쓴다면 또 다른 버퍼에 존재하는 데이터는 Consumer가 읽어 그래픽 데이터를 화면에 표시하게 됩니다. 이어서 화면에 표시하는 과정을 통해 소비된 버퍼에는 다시 Producer가 새로운 데이터를 쓰게 되고, 데이터 쓰기가 완료된 버퍼는 다시 Consumer에 의해 사용됩니다.

해당 방식은 실제 디스플레이에서 화면 깜빡임 없이 부드러운 애니메이션을 구현할 수 있도록 도와줍니다. 또한 BuffferQueue는 해당 구조상 고정된 버퍼 풀을 유지하고 있으며, 메모리를 할당하고 해제하기 보단 재사용할 수 있도록 관리하기 때문에 메모리 비용을 줄이주는 역할을 합니다.

Triple Buffering

구체적으로 Android BufferQueue에서는 Triple Buffering 기법을 통해 ProducerConsumer간 버퍼 교환을 수행하는 방식을 사용합니다. Tripple Buffering 기법 이전에는 Double Buffering이라는 기법이 있지만, 특정 주기마다 프레임을 전달해줄 때 버퍼가 준비되어있지 않아 렌더링 요청을 완료하지 못할 경우, 디스플레이 출력에 지연이 발생할 수 있습니다.

Tripple Buffering기법은 총 3개의 버퍼를 사용하며, 프론트 버퍼 1개와 백 버퍼 2개의 역할을 부여합니다. 디스플레이는 프론트 버퍼에 있는 데이터를 바탕으로 화면에 표시하며, 백 버퍼는 다음 프레임 렌더링을 수행합니다. 이 때, 백 버퍼 하나의 렌더링이 완료되었는데, 디스플레이가 아직 프레임을 표시중 이라면, 대기하는 것이 아닌 또 다른 백 버퍼에 다음 프레임을 렌더링 합니다.

해당 기법을 사용할 경우 연속적으로 렌더링 작업을 수행할 수 있으며, 프레임을 안정적으로 유지시킬 수 있습니다.실제로 앱이 BufferQueue내 하나에 버퍼에 Surface를 통해 그림을 그리면(그래픽 버퍼를 생산하면) SurfaceFlingerBufferQueue에 그려진 버퍼의 데이터를 가져와 화면 합성을 수행합니다.

TripleBuffering과 같은 MultipleBuffering 관련 내용은 [해당 링크]를 통해 더욱 많은 정보를 획득하실 수 있습니다.

UI 컴포넌트

우리가 앞서 살펴본 Surface는 앱이 화면에 그린 데이터들을 주고받도록 도와주는 인터페이스 역할을 합니다. 하지만 실제 안드로이드 프로젝트 내 특정 시나리오를 구현하는 과정에서는 해당 Surface에 직접적으로 접근하여 사용하는 경우는 드물며, 대신 Android에서 제공되는 SurfaceHolder, SurfaceView, TextureView와 같은 추상화된 UI 컴포넌트들을 통해 Surface의 기능을 활용합니다.

SurfaceHolder

SurfaceHolderSurface를 관리하는 추상 인터페이스 입니다. 개발자가 Surface의 생명주기를 파악하고, Surface의 속성을 제어하며 Surface에 직접적으로 그리기 위한 Canvas를 얻을 수 있도록 도와주는 역할을 수행할 수 있는 인터페이스를 제공해줍니다 . 이때 SurfaceHolder는 Surface와 1:1로 연결됩니다.

SurfaceHolder는 Surface의 상태 변화를 알려주는 콜백 인터페이스를 제공합니다. 콜백을 등록하기 위해서는 addCallback(Callback callback) 을 사용하여야 하며, 구현할 수 있는 콜백은 아래와 같습니다.

surfaceCreated(SurfaceHolder holder)

Surface가 처음 생성되었을 때 호출됩니다. 해당 시점 부터 그리기 작업을 수행할 수 있습니다.

surfaceChanged(SurfaceHolder holder, int format, int width, int height)

Surface의 크기나 픽셀 포맷이 변경될 때 호출됩니다.

surfaceDestroyed(SurfaceHolder holder)

Surface가 파괴되기 직전에 호출됩니다. 해당 콜백이 호출된 이후부터는 더 이상 Surface에 그리기 작업을 수행할 수 없습니다.

SurfaceHolder는 또한 lockCanvas() 메서드를 제공하여 Surface의 버퍼를 잠그고, 그릴 수 있는 Canvas객체를 반환할 수 있도록 도와줍니다. 반대로 그리기 작업이 완료되면 버퍼의 락을 해제할 수 있도록 도와주는 unlockCanvasAndPost() 메서드를 제공합니다.

lockCanvas()는 동시성 문제를 해결하기 위해 사용됩니다. 예를 들어, 여러 스레드나 다른 시스템 구성요소가 해당 Surface의 버퍼를 접근하여 쓰거나 읽기 작업을 시도한다면, 데이터가 손상되거나 충돌이 발생할 수 있습니다. 이를 해결하기 위해 Surface가 BufferQueue 를 통해 버퍼를 가져올 때 lock을 걸고 lock을 걸어둔 상태에서 Canvas를 통해 그림 작업을 할 수 있도록 합니다. 반대로 그리기 작업을 완료했으면 lock 을 해제하면서 queue에 버퍼를 전달합니다. 이러한 방식을 통해 Surface를 더욱 안전하게 사용할 수 있게 됩니다.

SurfaceHolder에 대한 더욱 자세한 내용은 [해당 소스]를 통해 확인하실 수 있습니다.

SurfaceView

SurfaceView는 안드로이드의 일반적인 View계층 구조와는 별개의 Surface을 사용하여 렌더링을 수행하는 특수한 UI 컴포넌트입니다. SurfaceView는 고성능 미디어 및 그래픽을 위한 도구로 일반 View보다 뛰어난 성능과 유연성을 제공합니다. 대표적인 사례로 카메라 Preview, 게임, 비디오 재생등이 있습니다.

일반적으로 Android의 View는 Window에서 그려지게 되며, Window는 하나의 Surface를 보유하고 있습니다. View는 제공받은 Surface 위에 Canavs를 통한 View를 그리면서 그래픽 버퍼를 채워넣는 반면, SurfaceView는 자신만의 고유한 Surface를 생성하고 관리합니다. SurfaceView는 별도의 렌더링 스레드를 통해 직접 그림을 그리며, UI 스레드의 방해를 받지않으면서 부드럽게 표시할 수 있게됩니다.

물론 SurfaceView는 일반적인 View 계층 구조 내부에 배치됩니다. 하지만, 실제 렌더링 작업에서는별도의 독립적인 Surface에서 작업을 수행하며, 다른 배치된 View들과는 관계 없이 직접적으로 소비자에게 그래픽 버퍼를 전달합니다. 해당 현상은 마치 메인 UI 화면에 구멍을 뚫고 그리는 것과 유사합니다(punch-through).

물론 위와 같이 SurfaceView는 독립적으로 동작하기 때문에 일반 View처럼 확대,축소 및 회전등 복잡한 View 애니메이션이나 변형을 직접 적용하기 어려우며, SurfaceView 위와 밑에 있는 View들은 가려질 수 있습니다.

앞서 Surface는 별도의 렌더링 스레드(백그라운드) 스레드에서 그림을 그린다고 언급하였습니다. 실제 카메라 Preview와 게임과 같은 작업에서는 실시간으로 데이터를 처리하여야하는데, 해당 작업을 메인 UI스레드에서 처리할 경우 UI스레드가 블록되어 ANR이 발생할 수 있습니다.

추가적으로 SurfaceView의 Surface는 직접 접근하는 것이 아닌 구현된 SurfaceHolder를 통해 Surface를 제어할 수 있습니다.


ANR

ANR은 Application Not Responding의 줄임말로 Android 운영체제가 특정 조건 하에 애플리케이션이 사용자 입력에 응답하지 않는다고 판단할 때 발생하는 메시지입니다. ANR의 일반적인 주된 원인은 메인 UI 스레드에서 시간이 오래걸리는 작업(네트워크처리,파일처리)을 수행할 때 오랫동안 블록되는 케이스입니다. ANR이 발생할 수 있는 대표적인 조건들은 아래와 같습니다.

  • 앱이 입력 이벤트를 5초이내로 응답하지 못한 경우
  • 앱에서 선언한 서비스가 몇 초 이내로 Service.onCreate() Service.onStartedCommand() Service.onBind() 실행을 완료할 수 없는 경우
  • 포그라운드 서비스가 5초내로 startForeground()를 호출하지 못한경우
  • BroadcaseReceiver가 설정된 시간 내에 실행을 완료하지 못한경우

ANR의 더욱 자세한 내용들은 [해당 문서]를 참고하시길 바랍니다.


SurfaceTexture

SurfaceTextureSurface와 마찬가지로 픽셀 데이터를 위한 버퍼들을 관리합니다. 하지만 해당 컴포넌트는 Surface로 그려지는 내용을 오직 OpenGL ES 택스처로 변환하여 GPU에서 사용할 수 있도록 전달합니다. SurfaceTexture는 Surface가 제공하는 데이터를 OpenGL ES 와 연결하기위한 UI Component입니다.

SurfaceTexture에 대해 조금 더 구체적으로 정리하겠습니다. 먼저 SurfaceTexture는 내부적으로 그래픽 데이터를 받기 위해 자신만의 BufferQueueSurface를 생성하여 제공합니다.

예를 들어, 카메라 Preview와 같은 Producer가 제공하는 데이터들은 SurfaceTexture가 제공하는 Surface를 통해 전달하게 될 것이며, 해당 Surface 객체는 내부적으로 보유하고 있는 BufferQueue를 통해 OpenGL ES 텍스처로 변환되어 GPU가 사용할 수 있도록 파이프라인이 구성될 것입니다

SurfaceTexture를 설명하는 과정에서 몇몇 새로운 용어의 등장하였습니다. 잠깐 새로운 용어들에 대해서 정리한 후 내용을 이어나가겠습니다.

OpenGL ES

OpenGL ES는 Open Graphics Library for Embedded Systems)의 약자이며, OpenGL의 임베디드 시스템 전용 라이브러리입니다. 이는 스마트폰, 태블릿등 리소스가 제한적인 모바일 및 임베디드 기기에서 고성능 2D 및 3D 그래픽을 렌더링하기 위한 API입니다.

Texture

GPU는 자신만의 메모리 공간에서 특정 구조로 최적화된 데이터를 사용해야 효율적으로 빠르게 처리할 수 있습니다. 이때 GPU가 직접 읽고 그리는데 최적화된 이미지 데이터를 텍스처라고 합니다. 텍스처는 시스템 메모리가 아닌 GPU가 직접 접근하고 처리할 수 있는 GPU 메모리(VRAM)에 로드되며, 기본적으로 픽셀 데이터로 이루어진 단일 또는 다차원 배열로 구성되어있습니다.

TextureView

TextureView는 Android에서 비디오나 카메라 Preview와 같은 실시간 그래픽 콘텐츠를 화면에 표시해주는 특수한 View 컴포넌트 입니다. SurfaceView와 유사한 목적을 가지지만, TextureView는 View계층 구조 내에서 다른 View 요소들과 자연스럽게 상호작용하며 렌더링 된다는 특징이 존재합니다.

TextureView는 앞서 언급된 SurfaceTexture와 연관이 깊은 컴포넌트 입니다. TextureViewSurfaceTexture 객체를 활용하여 그래픽 데이터를 화면에 표시합니다.

앞서 SurfaceTexture는 픽셀 데이터를 최종적으로 OpenGL ES 텍스처로 변환한다고 하였는데, TextureView는 이 SurfaceTexture가 만든 텍스처를 가져와 그립니다. TextureView가 화면에 나타날 준비가 되었을 때 내부적으로 SurfaceTexture 객체를 생성하고, 해당 SurfaceTexture를 관리합니다. 해당 SurfaceTexture는 Producer로 부터 픽셀 데이터를 받으며, 받은 데이터는 OpenGL ES 텍스처로 변환 후, TextureView에게 전달되어 표시됩니다.

다시 한번 종합적으로 정리하자면, 먼저 카메라 Preview와 같은 프레임 데이터는 TextureView 가 생성한 SurfaceTexture 내에 있는 Surface 객체로 전달됩니다. 그 후 Surface로 전달된 데이터는 SurfaceTexture로 이동되어 OpenGL ES 텍스처로 변환합니다. 변환된 데이터는 곧이어 TextureView가 받게되며 해당 텍스처를 렌더링에 사용합니다. 그 후 TextureView는 해당 OpenGL ES 텍스처를 사용하여 자신이 속한 Window의 Surface에 다른 View들과 함께 그려지게 됩니다.

하지만 여전히 의문점이 남아있습니다. 일반적인 View시스템은 최종적으로 Surface를 통해 픽셀 데이터를 넘기지만, 같은 View 시스템에 속해있는 TextureView는 OpenGL ES 텍스처를 넘기게 됩니다. 이 2가지의 다른 유형의 데이터가 어떻게 통합되어 렌더링을 할 수 있을까에 대한 부분은 우선 TextureView과 View를 상속받는 컴포넌트임을 알아야합니다.

TextureView는 일반 View를 상속받는 컴포넌트로 다른View들과 마찬가지로 onDraw(Canvas canvas) 메서드를 가집니다. 해당 onDraw() 메서드가 호출된다면 TextureView는 SurfaceTexture에서 최신 상태로 업데이트 된 OpenGL ES 텍스처를 가져옵니다. TextureView는 Canvas를 통해 GPU로 텍스처를 그리는 명령을 전달하고, GPU는 전달받은 텍스처를 사용하여 해당 View가 속해있는 Window의 Surface에 영역에 맞도록 픽셀 데이터를 씁니다. 물론 나머지 Window의 Surface 영역들은 다른 View들이 기존 방식대로 픽셀을 채워나갈것입니다. 최종적으로 모든 영역이 채워진 Surface의 데이터는 소비자(SurfaceFlinger)에게 제출되어 디스플레이를 출력하게 됩니다.

TextureView는 다른 View들과 통합되어 보이게 할 수 있다는 장점이 존재합니다. 또한 View의 모든 속성(위치,크기,회전,스케일, …)등을 자유롭게 적용하여 유연하게 카메라 Preview나 비디오 화면을 보여줄 수 있습니다.

하지만 SurfaceView에 비해 성능이 떨어진다는 단점이 존재합니다. 앞서 TextureView는 데이터를 OpenGL ES 텍스처로 변환하고 그 텍스처를 다시 View에 속하는 Window의 Surface에 그리는 추가적인 단계를 거친다고 언급하였씁니다. 이로 인해 SurfaceView 대비 약간의 렌더링 지연과 성능 부하가 발생할 수 있습니다. 따라서 향상된 프레임 속도와 최소 지연 시간이 요구되는 프로젝트의 경우 TextureView대신 SurfaceView를 사용해야만 합니다.

GLSurfaceView

GLSurfaceView는 SurfaceView와 OpenGL ES를 함께 사용하는 경우 사용되는 컴포넌트로 OpenGL ES를 사용하여 그래픽을 렌더링하는 데 특화된 SurfaceView의 확장된 클래스입니다.

GLSurfaceView는 개발시에 OpenGL ES 환경을 직접 설정하고 관리해야 하는 복잡성을 줄여주며, 고성능 커스텀 렌더링을 더 쉽게 구현할 수 있도록 도와줍니다. GLSurfaceView를 사용하려면, GLSurfaceView.Renderer 인터페이스를 구현하여 OpenGL ES를 바탕으로 그리는 코드를 작성하고, 이를 GLSurfaceView에 설정하면 됩니다.

GLSurfaceView는 별도의 렌더링 스레드를 자동으로 생성하고 관리하여 UI 스레드의 부하를 줄여줍니다. 개발자는 스레딩 로직에 신경 쓸 필요 없이 Renderer 콜백에 그리는 코드만 작성해주면 되지만, SurfaceView의 Punch Through 인한 단점들을 그대로 가져가게 된다는 단점이 존재합니다.



지금까지 Surface를 통해 그래픽 데이터가 어떻게 생산되고 BufferQueue를 거쳐 Consumer에게 전달되는지 그 흐름을 이해했습니다. 다음 포스팅에서는 Consumer의 대표적인 예시로 소개되었던 SurfaceFlinger에 대해 집중적으로 다루어보겠습니다.

참고자료