안드로이드 NDK 네이티브 프로그래밍

94
  • date post

    22-Jul-2016
  • Category

    Documents

  • view

    333
  • download

    23

description

데무라 나리카즈 지음 | 이해란 옮김 | 임베디드 & 모바일 시리즈 _ 025 | ISBN: 9788992939089 | 25,000원 | 2012년 08월 30일 발행 | 312쪽

Transcript of 안드로이드 NDK 네이티브 프로그래밍

Page 1: 안드로이드 NDK 네이티브 프로그래밍
Page 2: 안드로이드 NDK 네이티브 프로그래밍
Page 3: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK네이티브 프로그래밍

Page 4: 안드로이드 NDK 네이티브 프로그래밍

iv v

목 차

Chapter 01

안드로이드 NDK란?

01.01 이 책에 대해 ........................................................................................................ 2

01.02 안드로이드의 개발 라이브러리 ........................................................................ 3

01.03 안드로이도 NDK에서 이용할 수 있는 기능 ..................................................... 5

01.04 안드로이드의 내부 구조(계층) .......................................................................... 7

01.05 애플리케이션 실행환경 ...................................................................................... 10

01.06 실행파일이 생성되기까지 ................................................................................... 13

01.07 안드로이드 NDK의 장단점 ............................................................................... 14

01.08 설치 ...................................................................................................................... 18

01.09 정리 ...................................................................................................................... 20

Chapter 02

자바와 안드로이드 NDK

02.01 JNI란? .................................................................................................................. 22

02.02 JNI의 규약 ........................................................................................................... 23

02.03 log 출력하기 ........................................................................................................ 39

02.04 자바에서 C 함수 호출하기 ................................................................................. 41

02.05 JNIHelp 이용하기 .............................................................................................. 47

02.06 정리 ...................................................................................................................... 53

Page 5: 안드로이드 NDK 네이티브 프로그래밍

iv v

Chapter 03

NativeActivity

03.01 NativeActivity란? .............................................................................................. 56

03.02 NativeActivity와 게임 ....................................................................................... 56

03.03 이벤트 .................................................................................................................. 59

03.04 NativeActivity의 제약 ....................................................................................... 60

03.05 논블록에 대해 ..................................................................................................... 61

03.06 NativeActivityGlue ........................................................................................... 65

03.07 정리 ...................................................................................................................... 81

Chapter 04

OpenGL|ES

04.01 OpenGL와 OpenGL|ES .................................................................................... 84

04.02 SurfaceView란? .................................................................................................. 86

04.03 안드로이드의 OpenGL|ES ................................................................................ 87

04.04 예제 프로그램(OpenGL|ES 1.1) ........................................................................ 88

04.05 OpenGL|ES 2.1 .................................................................................................. 106

04.06 AndroidBitmap ................................................................................................. 135

04.07 정리 ...................................................................................................................... 140

Page 6: 안드로이드 NDK 네이티브 프로그래밍

vi vii

Chapter 05

사운드

05.01 OpenSL|ES에 대해 ............................................................................................. 144

05.02 사운드 포맷 ......................................................................................................... 148

05.03 재생과 녹음 ......................................................................................................... 149

05.04 데이터 소스 ......................................................................................................... 151

05.05 안드로이드 확장기능 .......................................................................................... 152

05.06 이펙트에 대해 ...................................................................................................... 155

05.07 예제 코드 ............................................................................................................. 158

05.08 설정 ...................................................................................................................... 171

05.09 제약 사항 ............................................................................................................. 172

05.10 정리 ...................................................................................................................... 174

Chapter 06

입출력(센서, 키보드, 파일)

06.0 안드로이드 NDK에서 센서 이용하기 .............................................................. 176

06.02 터치패널 .............................................................................................................. 176

06.03 키 입력 ................................................................................................................. 180

06.04 센서 ...................................................................................................................... 182

Page 7: 안드로이드 NDK 네이티브 프로그래밍

vi vii

06.05 Con�guration .................................................................................................... 190

06.06 Assets .................................................................................................................. 191

06.07 정리 ...................................................................................................................... 194

Chapter 07

툴(컴파일러, 디버거)

07.01 툴체인 .................................................................................................................. 196

07.02 arm-linux-androideabi-4.4.3의 새로운 기능 ................................................. 197

07.03 STL ...................................................................................................................... 198

07.04 gcc의 확장 기능 .................................................................................................. 201

07.05 외부 프로젝트 참조 ............................................................................................ 205

07.06 정수 ...................................................................................................................... 212

07.07 ndk-gdb ............................................................................................................... 213

07.08 ndk-build ............................................................................................................ 215

07.09 정리 ...................................................................................................................... 216

Page 8: 안드로이드 NDK 네이티브 프로그래밍

viii ix

Chapter 08

아키텍처

08.01 ARM 프로세서 ................................................................................................... 218

08.02 Cortex-A8 ........................................................................................................... 219

08.03 아키텍처의 구성 ................................................................................................. 222

08.04 메모리 .................................................................................................................. 228

08.05 L1 캐시와 L2 캐시 .............................................................................................. 229

08.06 ABI....................................................................................................................... 230

08.07 cpu-features ........................................................................................................ 234

08.08 정리 ...................................................................................................................... 237

Chapter 09

최적화

09.01 최적화의 순서 ..................................................................................................... 240

09.02 문제가 되는 부분 파악하기 ............................................................................... 242

09.03 최적화 방법 ......................................................................................................... 246

09.04 실행 바이너리의 실행 효율 향상시키기 ............................................................ 248

09.05 캐시의 적중률 향상시키기 ................................................................................. 251

09.06 gcc에 의한 최적화 ............................................................................................... 253

Page 9: 안드로이드 NDK 네이티브 프로그래밍

viii ix

09.07 그래픽스의 최적화 .............................................................................................. 256

09.08 소수 연산 ............................................................................................................. 258

09.09 정리 ...................................................................................................................... 261

Chapter 10

NEON

10.0 ARM 명령과 NEON 명령의 차이 ..................................................................... 264

10.02 NEON 명령에 대한 상세 설명 ........................................................................... 266

10.03 NEON 명령 출력하기 ........................................................................................ 273

10.04 벡터화 .................................................................................................................. 276

10.05 정리 ...................................................................................................................... 282

Chapter 11

레퍼런스

11.01 ndk-build에 대해 ............................................................................................... 284

11.02 Android.mk ....................................................................................................... 285

11.03 Application.mk 파일 ......................................................................................... 292

Page 10: 안드로이드 NDK 네이티브 프로그래밍

x xi

안드로이드 애플리케이션의 개발환경인 안드로이드 NDK를 학습하고자 이 책을 접한 모든 분

을 환영합니다. 안드로이드 NDK를 사용함으로써 C/C++로도 애플리케이션을 개발할 수 있게

됐습니다. 이 책에서는 이러한 안드로이드 NDK의 모든 것을 설명할 것입니다.

안드로이드 NDK는 2009년 6월에 처음 출시됐지만 그 당시에는 지원하는 기능이 그다지 많

지 않았습니다. 하지만 현재에 이르기까지 3D 그래픽스 라이브러리가 추가되는 등 점차 발전을

거듭해 왔으며, 최신판인 안드로이드 NDK, 리비전 5(이하 안드로이드 NDKr5)(옮긴이: 2012년

7월 현재 최신 버전은 리비전 8인 NDKr8입니다)에서는 사운드 및 터치패널에 의한 입력 등 지

원하는 라이브러리가 대폭 추가됐습니다. 그 덕분에 안드로이드 NDK의 이용 가치는 이전보다

월등히 높아졌습니다.

그럼, 이러한 안드로이드 NDKr5를 이용하는 이유는 뭘까요?

일반적으로 자바만으로도 애플리케이션을 개발할 수 있는데, 굳이 C/C++로 안드로이드 애

플리케이션을 개발하려고 하는 데는 몇 가지 이유가 있습니다. 과거의 개발 자산을 활용하기

위해서라든지, 인터넷에서 유통되고 있는 C 라이브러리를 이용하기 위해서라든지, 여러 가지

이유를 들 수 있겠지만 그중에서 가장 큰 이유는 처리 속도를 빠르게할 수 있다는 점입니다.

자바로 개발하고 있지만 생각보다 처리 시간이 오래 걸리는 부분이 있는 분도 분명 있을 것입

니다. 이때도 안드로이드 NDK가 매우 유용합니다. 작업의 일부를 C/C++로 바꾸면 처리 속도

를 향상할 수 있습니다.

또, 처음부터 빠른 속도가 필요한 애플리케이션도 적지 않습니다. 대표적인 예가 바로 게임

입니다. 안드로이드 NDKr5로 업그레이드되면서 지금까지의 안드로이드 NDK와 비교했을

때 사운드 및 터치패널의 지원으로 게임 개발이 훨씬 쉬워졌습니다. 이 부분 역시 안드로이드

NDKr5의 큰 장점이라고 할 수 있습니다.

이 책에서는 안드로이드 NDK를 이용해 안드로이드 단말 본래의 성능을 극대화하는 방법에

주안점을 두고 설명합니다. 그러므로 안드로이드 NDKr5에서 지원하는 각종 라이브러리 및 컴

지은이의 말

Page 11: 안드로이드 NDK 네이티브 프로그래밍

x xi

파일러와 같은 툴의 사용법과 CPU의 구조 및 컴파일러의 활용법 등도 설명합니다1.

안드로이드 NDK를 처음 접하는 분에게는 조금 어려운 부분이 있을지도 모르겠습니다만 어

려운 부분만 극복한다면 자바만으로는 도저히 해결할 수 없었던 작업을 처리할 수 있을 것입

니다.

여러분이 안드로이드 NDK를 학습하는 데 이 책이 조금이나마 도움되기를 바랍니다.

마지막으로 이 책은 저 혼자의 힘으로는 절대 완성할 수가 없었을 것입니다. 필자에게 이 책

을 집필할 기회를 주신 이마무라노리츠나 님, 그리고 바쁘신 와중에도 이 책의 리뷰를 맡아주

시고 많은 조언을 해주신 츠카다쇼우야 님, 코시노마코토 님, 그리고 마지막까지 포기하지 않고

편집을 도와주신 슈우와 시스템 관계자분에게도 감사의 마음을 전합니다. 그 밖에도 많은 분

의 도움이 있었기에 이 책을 완성할 수 있었습니다.

끝으로 집필 중에 아이들과 많이 놀아 주지 못해서 가족에게 많은 부담을 지웠습니다. 특히

집필에 크게 협조해준 아내에게 감사하다는 말을 전하고 싶습니다.

2011년 7월

데무라 나리카즈

1  참고로 이 책에서는 맥 OS X 10.6(스노우 레오파드)를 개발환경으로 사용합니다. 소스코드의 동작 검증에는 “넥서스 S”(안드로이드 2.3.4)를 사용했습니다.

Page 12: 안드로이드 NDK 네이티브 프로그래밍

xii 1

Page 13: 안드로이드 NDK 네이티브 프로그래밍

xii 1

Chapter

안드로이드 NDK란?

C/C++를 함께 사용하는 안드로이드 애플리케이션의 개발은 안드로이드

1.5부터 할 수 있었습니다. 하지만 안드로이드 NDK의 등장으로 더

다양한 안드로이드 애플리케이션을 개발할 수 있게 됐습니다. 특히 게임

개발자에게는 꿈에 그리던 지원이라고 할 수 있을 것입니다.

이 책에서는 안드로이드 NDK란 무엇이고 어떤 기능이 있는지 11장에 걸쳐

설명합니다.

01

Page 14: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

2

01.01 이 책에 대해

이 책은 “안드로이드 NDK를 이용한 개발에 관심이 있다”, “안드로이드 NDK를 이용해 애플리

케이션을 개발해 보긴 했지만 사실은 아직도 어떻게 해야 할지 잘 모르겠다”, “ 안드로이드 NDK

를 이용해 하드웨어의 성능을 좀 더 높이고 싶다”는 고민에 답하기 위해 쓰인 책입니다.

또 이 책에서는 자바나 C/C++ 프로그래밍의 기본적인 문법에 대한 지식 및 자바를 이용해 안

드로이드 애플리케이션을 개발해본 경험이 있다는 것을 전제로 하고 있으므로 이 부분에 대해

자세히 설명하지 않습니다.

그 이유는 안드로이드 NDK를 이용할 때 C/C++에 대한 지식이 전제되는 것은 물론이거니와

안드로이드 NDK에 포함된 라이브러리 역시 안드로이드의 애플리케이션 프레임워크에 대한 지

식을 전제로 설계된 것도 있기 때문입니다.

그러므로 안드로이드 SDK로 애플리케이션을 개발한 경험이 없는 분이 이 책을 읽으려면 애

플리케이션 프레임워크에 대해서도 함께 공부해야 해서 어려움이 있을 수도 있습니다. 이 책을

학습하기 전에 안드로이드 SDK를 이용해 애플리케이션을 개발해 볼 것을 권합니다.

또 안드로이드 NDK를 가장 효과적으로 활용할 수 있는 애플리케이션으로는 게임을 들 수 있

습니다.1 그래서 이 책에서는 “게임 개발에서는”이라는 문구를 자주 사용합니다.

물론 안드로이드 NDK는 게임 개발 외에도 매우 유용하므로 이 책은 안드로이드 NDK를 이

용하는 모든 개발자에게 도움이 되는 책이라고 확신합니다.

또 이 책에서는 안드로이드 NDK에서 이용할 수 있는 라이브러리(기능)에 대한 설명 말고도

ARM 아키텍처에 대한 세부적인 설명 및 컴파일러와 같은 툴을 이용하는 방법도 설명합니다.

그 이유는 안드로이드 NDK를 이용하려면 안드로이드 NDK에 포함된 라이브러리 및 컴파일러

와 같은 툴에 대한 지식도 필요하기 때문입니다. 하지만 안드로이드 NDK를 이용하는 것만으로

는 하드웨어를 충분히 활용할 수 없습니다.2

스마트폰과 같이 하드웨어의 스펙이 그다지 높지 않은 기기의 성능을 향상시키려면 라이브러

리를 이해하고 있는 것만큼 하드웨어의 아키텍처도 충분히 이해해야 합니다. 그리고 툴을 이용

해 하드웨어 본래의 성능을 얼마만큼 이끌어낼 것인가를 이해하는 것 역시 중요합니다. 하드웨

1  이 책의 중간에도 언급하겠지만 안드로이드 NDKr5 자체가 게임 개발을 목적으로 개발된 듯한 흔적이 있습니다.2  이는 게임 애플리케이션을 개발할 때는 치명적인 문제가 될 수 있습니다.

Page 15: 안드로이드 NDK 네이티브 프로그래밍

1장 ┃ 안드로이드 NDK란?

3

어의 성능이 향상되면 더 많은 캐릭터를 화면에 표시할 수 있으며, 게임을 제작할 때도 좀 더 자

유로운 표현이 가능해집니다.

안드로이드 NDK는 API 레벨 3(안드로이드 1.6)부터 API 레벨 9(안드로이드 2.3)까지 지원

하지만 이 책에서는 API 레벨 9에서 이용할 수 있는 기능을 중점적으로 설명합니다. 그 이유는

API 레벨 9부터 안드로이드 NDK에서 다룰 수 있는 기능이 현저히 늘어났기 때문입니다(2012

년 6월 현재 안드로이드 SDK에서 지원하는 버전은 API 레벨 20입니다).

01.02 안드로이드의 개발 라이브러리

이번에는 안드로이드 애플리케이션 개발에 이용되는 개발 라이브러리에 대해 살펴보겠습니다.

현재 안드로이드 애플리케이션을 개발하는 데 이용되는 라이브러리는 2가지가 있습니다.

≑ 안드로이드 SDK(Android So�ware Development Kit)

≑ 안드로이드 NDK(Android Native Development Kit)

지금부터 안드로이드 SDK와 안드로이드 NDK의 차이점을 설명하겠습니다.

01.02.01 안드로이드 SDK란?

먼저 안드로이드 SDK는 안드로이드 애플리케이션을 개발하는 데 필수적인 개발 키트입니다.

안드로이드 SDK에는 안드로이드의 실행파일(apk 파일)을 만드는 데 필요한 개발 툴과 안드

로이드 에뮬레이터, 문서 등이 포함돼 있으며, 안드로이드 SDK 없이는 애플리케이션을 개발하

거나 실행할 수 없습니다.

또 애플리케이션을 개발할 때는 안드로이드 SDK와 함께 통합개발환경(IDE, Integrated

Development Environment)인 이클립스(Eclipse)와 안드로이드 애플리케이션 개발용 플러그인인

ADT(Android Development Tool)를 이용해 개발하는 방법이 일반적입니다.

Page 16: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

4

01.02.02 안드로이드 NDK란?

다음으로 안드로이드 NDK는 안드로이드 SDK와 함께 이용되는 개발 키트(서브셋)입니다. 안

드로이드 NDK를 이용하면 안드로이드 애플리케이션의 일부 또는 전부를 C/C++로 만들 수 있

습니다. Native Development Kit라는 이름대로, 안드로이드 NDK로 만들어진 실행 바이너리

는 네이티브 코드(CPU가 직접 이해할 수 있는 코드, 기계어)가 됩니다.

안드로이드 NDK에는 컴파일러 및 링커가 포함된 툴체인(toolchain), 헤더 파일, 라이브러리,

문서, 예제 프로그램 등 크로스 개발3에 필요한 툴이 포함돼 있습니다.

이처럼 안드로이드 SDK와 안드로이드 NDK는 둘 중의 하나를 선택할 수 있는 것이 아니라

안드로이드 SDK가 기본으로 사용되는 툴이며, 안드로이드 NDK는 SDK를 보조하기 위해 사용

하는 툴입니다.

안드로이드 NDK안드로이드 SDK

애플리케이션프레임워크

툴체인, 라이브러리 등 C/C++에 필요한 파일만 있다.

애플리케이션을 실행하려면 SDK의 개발 툴이 필요하다.

컴파일러

링커

헤더 라이브러리

도큐멘트

개발 툴

기타 등등...

에뮬레이터

개발 툴(dx, adb 등)

자바 개발환경, 에뮬레이터, 개발 툴 등 개발에 필요한 모든 요소가 구비돼 있다.

[그림 01-01] 안드로이드 SDK와 안드로이드 NDK의 관계

3  크로스 개발이란 하드웨어 및 운영체제의 환경이 서로 다른 호스트와 타겟을 대상으로 개발하는 것을 말합니다. 안드로이드 애플리케이션을 개발할 때도 이러한 크로스개발 환경을 구축한 뒤 개발합니다.

Page 17: 안드로이드 NDK 네이티브 프로그래밍

1장 ┃ 안드로이드 NDK란?

5

01.03 안드로이드 NDK에서 이용할 수 있는 기능

안드로이드 NDK에서는 이제까지 애플리케이션 프레임워크 없이는 사용할 수 없었던 여러 가

지 기능을 비롯해 안드로이드 NDK에 새롭게 추가된 기능을 C/C++ 코드에서 직접 사용할 수

있게 됐습니다.

그럼, NDK를 이용해 직접 사용할 수 있게 된 기능에 대해 설명하겠습니다.

안드로이드 NDK에서 사용할 수 있는 기능은 안드로이드의 버전(API 레벨)에 따라 다릅니

다. 여기서는 사용할 수 있는 기능이 가장 많은 API 레벨 9(안드로이드 2.3)를 기준으로 설명하

겠습니다. 이 버전에서는 다음과 같은 기능을 C/C++에서 사용할 수 있습니다(실제로 NDK r5

이후부터 NDK의 기능이 비약적으로 발전했습니다).

≑ 3D 그래픽스 라이브러리(OpenGL|ES 1.1/2.0)

≑ 사운드 재생(OpenSL|ES)

≑ 센서 입력, 터치패널 입력

≑ NativeActivity

≑ Assets 폴더로부터 데이터 입력

≑ 리눅스의 시스템 콜

각 기능에 대한 자세한 내용은 다음 장부터 하나씩 차례대로 설명하겠습니다.

01.03.01 OpenGL|ES

OpenGL|ES를 이용할 수 있습니다. OpenGL|ES는 스마트폰이나 가정용 게임 등에 이용되는 임

베디드용 3D 그래픽 라이브러리입니다.

3D 그래픽스 라이브러리의 연산 처리는 대량의 데이터를 고속으로 처리해야 하는 대표적인

처리 가운데 하나입니다. 이러한 고속 연산이 필요할 때 자바 코드보다 네이티브 코드로 처리하

는 편이 단시간에 더 많은 작업을 처리할 수 있고, 그로 인해 더 정교한 3D 모델을 보여줄 수 있

습니다.4

4  더 정교한 3D 모델을 보여줄 수 있다는 말은 게임의 표현력이 더 폭넓어질 수 있다는 의미입니다.

Page 18: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

6

또 OpenGL|ES에는 ver 1.1과 ver 2.0으로 두 가지 버전이 있고, API 레벨에 따라 이용할 수

있는 버전이 다릅니다.

01.03.02 OpenSL|ES

OpenSL|ES5를 이용하면 사운드의 녹음 및 재생을 할 수 있습니다. 즉, C/C++에서 여러 가지 음

원(PCM 음원, MP3 등)을 BGM으로 재생하거나 효과음을 재생할 수 있습니다.

음원을 재생할 때 잔향 음과 같은 효과를 섞어서 재생할 수도 있습니다. 예를 들면, 동굴이나

작은 방 등의 게임 장면에 잔향을 설정하면 더 현장감 있는 음을 재생할 수 있습니다.

01.03.03 센서, 터치패널

센서나 터치패널을 이용해 사용자에게서 입력을 받습니다. 센서나 터치패널을 통한 입력은 애

플리케이션을 조작하는 데 반드시 필요한 요소입니다.

아울러 프레임워크를 거치지 않으므로 더 높은(일정 주기로) 정밀도로 데이터를 받을 수 있습

니다.

01.03.04 NativeActivity

C/C++에서 직접 Activity의 이벤트를 수신하고 설정할 수 있는 NativeActivity를 이용할 수 있

습니다. C/C++에서 Activity를 제어할 수 있게 되면서 애플리케이션 프레임워크를 이용하지 않

고 C/C++만으로도 안드로이드 애플리케이션을 만들 수 있게 됐습니다.

01.03.05 Assets 폴더로부터 데이터 입력

Assets 폴더에 있는 데이터를 가져올 수 있습니다. Assets 폴더는 리소스 폴더와는 별개로 여러

종류의 데이터(캐릭터 데이터, 텍스처 데이터, 음악 데이터 등)를 넣어두고, 애플리케이션에 포

함할 수 있습니다.

5  임베디드용으로 개발된 사운드 라이브러리

Page 19: 안드로이드 NDK 네이티브 프로그래밍

1장 ┃ 안드로이드 NDK란?

7

Assets 폴더의 데이터는 리눅스에서 파일을 관리하는 방법과 거의 같으므로 리소스 데이터처

럼 파일을 추가할 때마다 빌드할 필요가 없고, 대량의 데이터를 관리해야 할 때 매우 유용한 방

법입니다.

01.03.06 리눅스의 시스템 콜

리눅스의 시스템 콜을 사용할 수 있습니다. 원래 안드로이드는 리눅스 커널을 기반으로 하며,

그 위에 애플리케이션 프레임워크와 달빅(Dalvik) VM이 있습니다. 그리고 안드로이드에서는

하드웨어를 제어할 때 거의 리눅스 커널을 이용합니다.

그러나 안드로이드 SDK에서는 이러한 리눅스 커널의 API는 거의 애플리케이션 프레임워크

에 의해 은폐돼 있으므로 애플리케이션 개발자가 직접 시스템 콜을 사용할 수 없습니다. 하지만

NDK에서는 리눅스 커널의 시스템 콜을 직접 사용할 수 있습니다. 이 시스템 콜을 이용할 수 있

게 되면서 애플리케이션 프레임워크만으로는 구현할 수 없었던 제어도 할 수 있게 됐고, 만들 수

있는 애플리케이션의 폭도 넓어졌습니다.

지금까지는 API 레벨 9(안드로이드 2.3)에서 이용할 수 있는 기능을 설명했지만 API 레벨에

따라 이용할 수 있는 기능은 서로 다릅니다. 안드로이드 2.3 이전 버전에서 사용할 수 있는 기능

은 표 01-01을 참고하기 바랍니다.

[표 01-01] API 레벨과 안드로이드 운영체제에 따른 기능

API

레벨

안드로이드

버전JNI

OpenGL|ES비트맵

Open

SL|ES

Native

Activity입력

버전 1.1 버전 2.0

4 1.6 ○ ○

5 2.1 ○ ○ ○

8 2.2 ○ ○ ○ ○

9 2.3.1 ○ ○ ○ ○ ○ ○ ○

01.04 안드로이드의 내부 구조(계층)

앞에서도 설명했지만 안드로이드 NDK에 포함된 라이브러리를 이용하면 자바로 애플리케이션

을 개발하는 것과는 달리 애플리케이션 프레임워크를 거치지 않고도 안드로이드의 라이브러리

를 이용할 수 있습니다.

Page 20: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

8

같은 기능을 이용한다고 가정했을 때 자바로 만들었을 때와 안드로이드 NDK로 만들었을 때

를 비교해보면 이용하는 라이브러리의 종류와 수가 다릅니다.

보통 안드로이드 NDK를 이용해 만든 쪽이 이용하는 라이브러리의 수가 적습니다(즉, 빠른

속도로 동작할 가능성이 높습니다).

이것은 무슨 뜻일까요? 이를 이해하려면 안드로이드의 내부 구조를 알아야 합니다. 안드로이

드는 다음과 같은 계층으로 구성돼 있습니다.

애플리케이션

홈 다이얼러 SMS/MMS I M 브라우저 카메라 알람 계산기

액티비티 매니저 윈도우 매니저 컨텐츠 프로바이더 뷰 시스템 알람 매니저

주소록 음성 다이얼 Email Calendar Media Player Photo Album Clock ···

애플리케이션 프레임워크

라이브러리 안드로이드 런타임

하드웨어 추상 계층

그래픽스 오디오 카메라 블루투스 GPS 라디오(RIL) Wifi ···

리눅스 커널

패키지 매니저 텔레포니 매니저 리소스 매니저 로케이션 매니저 ···

SQLite 웹킷 Libc서피스매니저

미디어프레임워크

Open GLIES

오디오매니저

FreeType SSL ···

코어 라이브러리

달빅 가상머신

디스플레이 드라이버 카메라 드라이버 블루투스 드라이버 공유 메모리 바인더(IPC) 드라이버

USB 드라이버 키패드 드라이버 Wifi 드라이버 오디오 드라이버 파워 매니지먼트

[그림 01-02] 안드로이드의 내부 구조

그림 01-02를 하드웨어에 가까운 층(그림 01-02의 아래층)부터 순서대로 설명하겠습니다.

Page 21: 안드로이드 NDK 네이티브 프로그래밍

1장 ┃ 안드로이드 NDK란?

9

01.04.01 리눅스 커널

하드웨어의 바로 위에 있는 계층이 그림 01-02의 맨 아래에 있는 리눅스 커널입니다. 이 리눅스

커널은 PC에서 이용하는 일반적인 리눅스 커널에 안드로이드 환경에 맞게 커스터마이징된 형

태입니다.6 안드로이드 환경에 맞게 제작된 커널은 전원 관리와 메모리 관리의 확장 등 일부 기

능에 국한돼 있으므로 대부분의 기능은 일반 리눅스 커널과 똑같이 동작합니다.

01.04.02 HAL

다음은 HAL(Hardware Abstraction Layer)입니다. HAL은 하드웨어를 추상화한 계층으로 HAL

에 정의된 API를 통해 정보를 가져와 안드로이드를 탑재한 하드웨어를 제어할 수 있게 돼 있습

니다.

그리고 HAL 내부에서는 리눅스 커널에 있는 디바이스 드라이버(디바이스를 제어하는 프로

그램)에 접근해 데이터를 가져옵니다(예를 들면, 가속도 센서만 하더라도 다양한 칩이 있으며,

데이터를 가져오는 방법 또한 서로 다릅니다).

이런 방법을 이용하면 안드로이드 하드웨어의 차이를 HAL의 하위 계층에 국한할 수 있으며,

HAL보다 상위 계층에서는 하드웨어에 의존하지 않고 코드를 작성할 수 있습니다.

01.04.03 라이브러리와 안드로이드 런타임

HAL 위에는 라이브러리와 안드로이드 런타임이 있습니다. 먼저 라이브러리에는 다양한 미들

웨어가 들어 있습니다. 예를 들면, 앞에서도 소개한 OpenGL|ES와 데이터베이스 라이브러리인

SQLite 등이 포함돼 있습니다.

그리고 안드로이드 런타임은 안드로이드 애플리케이션을 동작시키기 위한 실행환경입니

다. 안드로이드 런타임에는 기본 라이브러리가 포함된 코어 라이브러리와 달빅 VM(VM이란

Virtual Machine, 즉 가상 머신을 말함)이 있습니다. 달빅 VM은 안드로이드의 커다란 특징 가

운데 하나입니다.

이 달빅 VM 덕분에 자바로 만든 안드로이드 애플리케이션이 CPU의 종류와 상관없이 동작

할 수 있습니다.

6 (옮긴이) 이 말은 본래의 리눅스 커널을 임베디드 환경인 안드로이드에 맞게 수정했다는 의미입니다.

Page 22: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

10

01.04.04 애플리케이션 프레임워크

라이브러리와 안드로이드 런타임 위에 있는 것이 애플리케이션 프레임워크입니다. 애플리케이

션 프레임워크에는 버튼이나 텍스트 박스와 같은 안드로이드 애플리케이션을 동작시키는 데 필

요한 라이브러리가 포함돼 있습니다. 앞에서 언급한 라이브러리를 자바에서 조작할 때 사용되

는 래퍼 라이브러리도 포함돼 있습니다.

01.04.05 애플리케이션

그림 01-02의 가장 위에 있는 계층이 애플리케이션입니다. 이 계층에서 안드로이드의 각종 애플

리케이션이 동작합니다. 안드로이드 NDK를 사용해 만든 안드로이드 애플리케이션을 포함한

모든 애플리케이션이 애플리케이션 계층에서 동작합니다.

01.05 애플리케이션 실행환경

그럼 앞에서 설명한 계층에서 자바로 만든 애플리케이션과 안드로이드 NDK를 이용해 만든 애

플리케이션이 안드로이드 내부에서는 어떤 차이를 보이며 실행되는지 설명하겠습니다.

자바로 만든 애플리케이션은 달빅 VM이 해석할 수 있는 바이트코드가 포함된 실행파일입니

다. 그리고 애플리케이션이 실행될 때 이 실행파일은 달빅 VM 안에 있는 컴파일러(JIT)에 의해

네이티브 코드로 변환된 후 CPU에 의해 실행됩니다.7

달빅 VM을 이용한 실행파일의 장점은 달빅 VM이 동작하는 환경이라면 ARM이나 x86 같은

CPU의 아키텍처와 관계없이 애플리케이션을 실행할 수 있다는 점입니다.

‘JIT 컴파일러에서 실행한다’라는 말만 들으면 애플리케이션의 실행 속도가 느려진다고 생각

할 수도 있지만 현시점에서는 무조건 그렇다고 할 수는 없습니다. 달빅 VM은 JIT에 의해 네이티

브 코드로 변환된 실행파일을 캐시하는 등 애플리케이션이 고속으로 실행될 수 있는 구조를 갖

추고 있습니다.

7  이것은 안드로이드 2.2부터 가능해진 기능입니다. 이전 버전에서는 달빅 VM에 JIT 컴파일러가 탑재돼 있지 않았습니다.

Page 23: 안드로이드 NDK 네이티브 프로그래밍

1장 ┃ 안드로이드 NDK란?

11

이 내용을 앞서 보여준 계층 그림(그림 01-02)으로 바꿔 설명하면 그림 01-03으로 나타낼 수

있습니다.

애플리케이션

홈 다이얼러 SMS/MMS I M 브라우저 카메라 알람 계산기

액티비티 매니저 윈도우 매니저 컨텐츠 프로바이더 뷰 시스템 알람 매니저

주소록 음성 다이얼 Email Calendar Media Player Photo Album Clock ···

애플리케이션 프레임워크

라이브러리 안드로이드 런타임

하드웨어 추상 계층

그래픽스 오디오 카메라 블루투스 GPS 라디오(RIL) Wifi ···

리눅스 커널

패키지 매니저 텔레포니 매니저 리소스 매니저 로케이션 매니저 ···

SQLite 웹킷 Libc서피스매니저

미디어프레임워크

Open GLIES

오디오매니저

FreeType SSL ···

코어 라이브러리

달빅 가상머신

디스플레이 드라이버 카메라 드라이버 블루투스 드라이버 공유 메모리 바인더(IPC) 드라이버

USB 드라이버 키패드 드라이버 Wifi 드라이버 오디오 드라이버 파워 매니지먼트

자바 애플리케이션

[그림 01-03] 자바로 만들었을 때

자바로 실행하기

먼저 애플리케이션 계층에서 애플리케이션이 시작한 후에 실행됩니다. 애플리케이션은 달빅

VM의 바이트코드로 구성돼 있으므로 달빅 VM을 거친 후에 실행됩니다. 애플리케이션 내부

에는 애플리케이션 프레임워크의 기능을 사용하는 부분도 있고 이 애플리케이션 프레임워크의

기능에는 SQLite 같은 라이브러리를 이용하는 기능도 있습니다.

이렇듯 애플리케이션 프레임워크는 달빅 VM을 거쳐 실행됩니다.

Page 24: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

12

NDK를 이용해 실행하기

이번에는 안드로이드 NDK를 이용한 애플리케이션을 설명하겠습니다. 여기서는 자바를 메인으

로 개발한 애플리케이션의 일부에서 네이티브 코드를 이용하고 있다고 가정합니다.

자바로 만들어진 부분은 앞서 설명했듯이 달빅 VM을 통해 실행되지만 안드로이드 NDK에

의해 만들어진 라이브러리는 네이티브 코드이므로 달빅 VM을 거치지 않고 CPU가 직접 실행

합니다. 또 네이티브 코드에서 직접 안드로이드의 기능(OpenGL|ES)을 사용하면 애플리케이션

프레임워크(=자바 코드)를 거치지 않고 직접 라이브러리를 호출합니다. 네이티브 코드로 만들

면 애플리케이션 프레임워크를 거치지 않고 직접 안드로이드의 기능에 접근하므로 실행 효율이

매우 높아집니다.

애플리케이션

홈 다이얼러 SMS/MMS I M 브라우저 카메라 알람 계산기

액티비티 매니저 윈도우 매니저 컨텐츠 프로바이더 뷰 시스템 알람 매니저

주소록 음성 다이얼 Email Calendar Media Player Photo Album Clock ···

애플리케이션 프레임워크

라이브러리 안드로이드 런타임

하드웨어 추상 계층

그래픽스 오디오 카메라 블루투스 GPS 라디오(RIL) Wifi ···

리눅스 커널

패키지 매니저 텔레포니 매니저 리소스 매니저 로케이션 매니저 ···

SQLite 웹킷 Libc서피스매니저

미디어프레임워크

Open GLIES

오디오매니저

FreeType SSL ···

코어 라이브러리

달빅 가상머신

디스플레이 드라이버 카메라 드라이버 블루투스 드라이버 공유 메모리 바인더(IPC) 드라이버

USB 드라이버 키패드 드라이버 Wifi 드라이버 오디오 드라이버 파워 매니지먼트

자바 애플리케이션

NDK 코드

[그림 01-04] 자바 코드와 네이티브 코드를 함께 사용했을 때

Page 25: 안드로이드 NDK 네이티브 프로그래밍

1장 ┃ 안드로이드 NDK란?

13

또 바이트코드의 실행이나 네이티브 코드의 실행에 상관없이 안드로이드 애플리케이션은 모

두 달빅 VM에서 생성된 샌드박스(sandbox) 위에서 실행되므로 애플리케이션의 버그 또는 악의

적인 애플리케이션이 운영체제나 다른 애플리케이션에 영향을 미치는 것을 막을 수 있습니다.

01.06 실행파일이 생성되기까지

안드로이드 SDK와 안드로이드 NDK를 모두 이용한 애플리케이션은 달빅 VM의 바이트코드

로 구성된 실행파일과 CPU에서 직접 실행할 수 있는 네이티브 코드를 포함한 실행파일이 하나

의 실행파일에 공존합니다. 그럼 이 두 개의 파일은 어떤 관계를 이루며 실행될까요?

안드로이드 NDK를 이용할 경우 C/C++로 만든 코드는 컴파일러와 링커를 거쳐 최종적으로

는 모듈(.so 파일)로 만들어집니다. 공유 라이브러리이므로 단독으로 동작할 수는 없습니다. 그

리고 이 모듈은 자바 코드(.dex 파일)로부터 호출돼 이용됩니다. 이렇듯 안드로이드 애플리케이

션에서는 자바 코드가 주(主)가 되고, 네이티브 코드가 종(從)이 되어 실행됩니다.

또 실제로 개발할 때는 C/C++ 코드를 먼저 빌드한 후 자바 코드를 빌드해야 합니다. 이는 자

바 코드를 빌드할 때 모듈(.so)을 이용해 실행파일(.apk)을 생성하기 때문입니다.

리스소

・메시지・아이콘

자바 소스코드

안드로이드 애플리케이션

리스소

개발툴(dx、adb 등)

애플리케이션 프레임워크

안드로이드 SDK

개발툴(gcc 등)

헤더 파일, 라이브러리

안드로이드 NDK

실행파일 네이티브 라이브러리

C/C++ 소스코드

[그림 01-05] 개발 언어, 툴, 실행파일

Page 26: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

14

01.07 안드로이드 NDK의 장단점

어떤 기술이든 장점만 있다는 건 드문 일일 것입니다. 장점이 있으면 단점도 있게 마련입니다. 안

드로이드 NDK 역시 마찬가지입니다.

안드로이드 NDK를 이용함으로써 이제까지 자바로 개발할 때는 얻을 수 없었던 장점을 얻을

수 있게 됐지만 안드로이드 NDK에 의해 제약을 받는 부분도 있습니다.

안드로이드 NDK를 이용할 때는 이러한 장단점을 잘 파악해서 만들고자 하는 애플리케이션

의 특성과 잘 비교한 후 안드로이드 NDK를 도입합시다.

01.07.01 장점

안드로이드 NDK의 가장 큰 장점은 안드로이드 애플리케이션에서 네이티브 코드(CPU가 직

접 이해할 수 있는 코드, 기계어)를 이용할 수 있게 됐다는 점입니다. 네이티브 코드를 이용하면

CPU를 직접 제어할 수 있으므로 CPU의 특성에 맞춰 변경하거나 하드웨어를 섬세하게 제어할

수 있습니다.

프로그램을 어떻게 만드느냐에 따라 자바보다 네이티브 코드로 만드는 편이 훨씬 빠른 속도

로 데이터를 처리할 수 있으므로 이미지 처리나 게임 등 고속 처리가 필요한 부분에서는 네이티

브 코드를 사용하는 것도 좋은 방법입니다.

C/C++로 만들어진 과거의 자산이나 라이브러리를 이용할 수 있다는 장점도 있습니다. C/

C++는 자바보다 훨씬 이전부터 있던 언어로 업계나 기업에 따라서는 과거의 소프트웨어 자산

을 대량으로 보유하고 있기도 합니다.

이런 소프트웨어 자산을 안드로이드 애플리케이션을 개발하는 데 이용할 수 있게 되면서 C/

C++로 만들어진 소프트웨어 자산을 활용할 수 있게 됐습니다.

01.07.02 단점

이번에는 단점을 설명하겠습니다.

가장 큰 단점은 실행파일에 네이티브 코드가 포함돼 있으므로 실행파일이 하드웨어(CPU)에

Page 27: 안드로이드 NDK 네이티브 프로그래밍

1장 ┃ 안드로이드 NDK란?

15

의존한다는 점입니다.

안드로이드도 최근에는 ARM, x86(Intel), MIPS, SH 등 다양한 아키텍처의 CPU에서 동작

할 수 있게 됐습니다. 자바로 만들면 앞에서도 설명했듯이 달빅 VM이 CPU 아키텍처의 차이를

해결해 주므로 개발자나 사용자는 하드웨어의 차이에 신경 쓸 필요가 없었습니다.

하지만 안드로이드 NDK를 이용한 애플리케이션은 아키텍처에 의존한 코드가 포함돼 있으

므로 실행파일을 CPU마다 준비해야 합니다. 그리고 사용자도 자신이 가진 하드에 맞춰 실행파

일을 선택해야 합니다. 앞으로는 복수의 하드웨어 아키텍처를 하나의 실행 바이너리(.apk 파일)

로 해결할 수 있게 되겠지만 그때도 역시 각 아키텍처에는 실행파일이 존재하므로 실행파일의

크기가 커질 수밖에 없습니다.

달빅 VM

안드로이드 애플리케이션

네이티브 코트

자바로 만든 코드

C/C++로 만든 코드

바이트코드(달빅)

ARM용안드로이드

자바 코드

MIPS용 안드로이드

X86용 안드로이드

ARM용 모듈

X86용 모듈

각 CPU에서실행할 수 있게 변환

CPU별로 실행 바이너리를 준비해야 한다.

MIPS용 실행파일이 없으므로, MIPS용 안드로이드에서는 실행할 수 없다.

[그림 01-06] NDK, 아키텍처, 플랫폼의 관계

또 자바의 디버그 환경과 비교해보면 NDK로 작성한 코드를 디버깅하기가 더 어렵습니다. 그

이유는 네이티브 바이너리가 실행되고 있을 때는 달빅 VM에 의해 엄격하게 관리되지 않기 때

문입니다. 예를 들어, 버그 때문에 애플리케이션이 접근해서는 안 될 메모리 영역에 접근했다고

합시다. 이때 자바 애플리케이션이라면 “Out Of Range” 등의 예외가 발생해 대화상자가 나타나

면서 애플리케이션이 강제 종료됩니다.

Page 28: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

16

이와 같은 상황이 네이티브 코드에서 일어나면 자바와 같은 예외는 전혀 발생하지 않

고 그 자리에서 애플리케이션이 종료됩니다. 그리고 이런 종류의 에러는 세그먼테이션 폴트

(Segmentation fault)라는 에러 메시지와 함께 그 당시의 레지스터 값이 표시된 상태에서 강제 종

료하는 경우가 대부분이며, 소스코드의 어느 행에서 에러가 발생했는지는 전혀 표시되지 않습

니다. 그런 의미에서 자바 코드를 디버그할 때처럼 쉽게 디버그되지는 않는다고 생각하는 것이

좋습니다.

자바 코드에서의 잘못된 종료 C/C++ 코드에서의 잘못된 종료

다이얼로그가 표시되고 강제 종료한다 강제 종료 시에는 홈 화면으로 돌아간다

죄송합니다

calcVal(com.example.calcval)이

예상치 않게 중지되었습니다.

다시 시도하세요.

강제로 닫기

[그림 01-07] 자바와 안드로이드 NDK의 에러의 차이

01.07.03 NDK의 파일구성

안드로이드 NDK의 파일 구성에 대해 설명하겠습니다. 안드로이드 NDK에는 앞에서도 설명했듯

이 개발 툴, 헤더파일, 라이브러리, 소스코드, 예제코드 등이 포함돼 있습니다. 안드로이드 NDK

에 어떤 파일과 툴이 포함돼 있는지 파악해 두면 나중에 기능을 학습할 때 편리할 것입니다.

Page 29: 안드로이드 NDK 네이티브 프로그래밍

1장 ┃ 안드로이드 NDK란?

17

. GNUmakefile ———————————————————————————————// makefile build —————————————————————————————————————// 빌드에 사용되는 툴

docs ——————————————————————————————————————// 도큐멘트

ndk-build —————————————————————————————————// 빌드 코멘트

ndk-gdb ———————————————————————————————————// 디버그 코멘트

platforms —————————————————————————————————// API 레벨별 헤더, 라이브러리 파일

android-3 arch-arm usr include lib android-4 arch-arm usr include lib android-5 arch-arm usr include lib android-8 arch-arm usr include lib android-9 arch-arm usr include lib

samples ———————————————————————————————————// 예제 프로그램

bitmap-plasma hello-gl2 hello-jni hello-neon module-exports native-activity native-audio native-plasma san-angeles test-libstdc++ two-libs

sources ———————————————————————————————————// 툴 등의 소스코드

android cpufeatures libthread_db native_app_glue cpufeatures cxx-stl stlport

Page 30: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

18

tests —————————————————————————————————————// 테스트 코드

README build prebuild-stlport device test-stlport run-standalone-tests.sh run-tests.sh standalone

toolchains arm-eabi-4.4.0 arm-linux-androideabi-4.4.3 x86-4.2.1

[그림 01-08] 안드로이드 NDK의 파일 구성

툴은 API 레벨에 상관없이 공통으로 사용할 수 있고 헤더 파일과 라이브러리는 API 레벨별

로 나뉘어 있습니다. 그 이유는 앞에서도 설명했지만 API 레벨에 따라 이용할 수 있는 기능이

다르기 때문입니다. 또 API 레벨에서 지원하는 기능은 기본적으로 상위 호환이 가능합니다.

01.08 설치

안드로이드 NDK를 설치해 봅시다. 설치 프로그램이 별도로 있지는 않습니다. 압축 파일 형태로

배포되며 압축을 푸는 것만으로 설치가 완료됩니다. 이때 이클립스를 이용한 자바의 안드로이

드 애플리케이션 개발환경(이클립스, 안드로이드 SDK, ADT)은 모두 설정된 상태여야 합니다.

참고로 이 책에서는 맥 OS X을 기준으로 설정하겠습니다.

01.08.01 내려받기

다음 웹사이트에서 안드로이드 NDK를 내려받습니다.

http://developer.android.com/sdk/ndk/index.html

맥 OS X(Intel)에서 내려받을 파일은 android-ndk-r5c-darwin-x86.tar.bz2로 bz2 형식으로

압축된 파일입니다. 즉, 설치 프로그램이 별도로 존재하지 않습니다(현재 버전인 ndk-r8에서는

android-ndk-r8-darwin-x86.tar.bz2입니다).

Page 31: 안드로이드 NDK 네이티브 프로그래밍

1장 ┃ 안드로이드 NDK란?

19

01.08.02 설치

내려받은 android-ndk-r5c-darwin-x86.tar.bz2를 임시 폴더에 풉니다. 여기서는 /Developer/

SDKs 폴더에 풀겠습니다.

[그림 01-09] 안드로이드 NDK 내려받기

$ tar xvf android-ndk-r5c-darwin-x86.tar.bz2 $ sudo mv android-ndk-r5c /Developer/SDKs/

다음으로 android-ndk-r5c 폴더에 포함된 명령어를 실행할 수 있게 패스를 추가합니다.

$HOME/.bashrc 파일의 맨 밑에 다음 한 줄을 추가합니다.

export $PATH=/Developer/SDKs/android-ndk-r5c:$PATH

이것으로 안드로이드 NDK를 이용할 수 있게 됐습니다. 안드로이드 NDK는 기본적으로 명령

행(command line)을 사용해 빌드합니다. 이것으로 안드로이드 NDK를 이용한 개발환경의 설정

이 끝났습니다.

Page 32: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

20

01.08.03 윈도우에서의 안드로이드 NDK 설치

여기서는 맥을 사용한다는 가정하에 안드로이드 NDK를 이용한 애플리케이션의 개발환경을

설정했습니다. 이는 리눅스에서 개발환경을 설정하는 것과 같습니다. 단, 윈도우에서 안드로이

드 NDK를 사용할 때는 안드로이드 NDK를 설치하기 전에 시그윈(Cygwin, www.cygwin.com)을

설치해야 합니다(GNU make가 필요하기 때문). 시그윈이란 리눅스에서 자주 사용하는 명령어

를 윈도우에서도 실행할 수 있게 해주는 실행 환경 소프트웨어입니다.

01.09 정리

이 장에서는 안드로이드 NDK의 개요와 장단점, 그리고 개발환경을 구축하는 방법을 설명했습

니다. 이렇듯 안드로이드 NDK는 자바나 안드로이드 SDK로 애플리케이션을 개발하는 것과는

많이 다릅니다.

일반적인 자바를 이용한 애플리케이션 개발과 크게 다른 점이라면 안드로이드 NDK로 개발

할 때는 터미널에서 명령어를 사용해서 빌드해야 한다는 점입니다. 이 점을 비롯해 지금까지 해

온 자바를 이용한 애플리케이션 개발의 흐름과는 다른 점이 있다는 것을 기억합시다.

그럼 다음 장부터는 안드로이드 NDK를 이용한 개발 방법을 소개하겠습니다.

Page 33: 안드로이드 NDK 네이티브 프로그래밍

Chapter

자바와 안드로이드 NDK

서점에 가면 안드로이드용 애플리케이션을 개발하기 위한 많은 책이 진열돼

있지만 대부분 자바로 애플리케이션을 만드는 방법을 설명한 책입니다.

그럼 안드로이드용 이외의 애플리케이션을 한 번 생각해 봅시다.

아직도 C/C++로 만들어진 프로젝트는 많이 있습니다. 영상 처리와 같이

고속 처리를 해야 하는 애플리케이션 역시 C/C++로 만들 때가 많습니다.

iOS용 애플리케이션 개발에 사용되는 언어는 오브젝티브-C인데, 이

언어는 오브젝티브-C는 물론 C/C++ 코드도 직접 호출할 수 있습니다.

안드로이드의 경우 자바로는 C/C++ 코드를 직접 호출할 수 없지만 JNI(Java

Native Interface)를 이용하면 이 문제를 해결할 수 있습니다.

즉, JNI를 이용함으로써 자바와 C/C++를 용도에 맞게 선택해 개발할 수

있는 것입니다. 그래서 이 장에서는 JNI에 대해 자세히 알아보겠습니다.

02

Page 34: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

22

02.01 JNI란?

JNI란 Java Native Interface의 약자로, 자바에서 C/C++의 함수를 호출하거나, 반대로 C/C++

에서 자바의 클래스와 메서드를 사용할 수 있게 해주는 인터페이스입니다.

호출하는 자바의 라이브러리는 개발자가 직접 개발한 라이브러리나 애플리케이션 프레임워

크처럼 이미 만들어진 라이브러리여도 상관없습니다.

애플리케이션(자바)

애플리케이션(C/C++)

JNI

애플리케이션 프레임워크

자바 측

C측

[그림 02-01] JNI의 역할

JNI를 이용하면 자바에서 처리하는 특정 부분을 C/C++로 만들 수 있습니다. 예를 들면, 영상

처리를 하는 애플리케이션을 한번 생각해 봅시다. 이 애플리케이션의 사용자 인터페이스는 자

바로 만들고, 실제 이미지를 처리하는 부분은 C/C++로 만들면 모든 과정을 자바로 처리할 때보

다 훨씬 빠른 속도로 영상 처리를 하는 애플리케이션을 만들 수 있습니다.

또 C/C++에서 애플리케이션 프레임워크를 호출할 수도 있습니다. 예를 들면, 데이터를 로딩

하는 경우, C/C++ 측의 데이터 로딩 진행 상황에 맞춰 애플리케이션 프레임워크의 프로그레스

다이얼로그를 호출하면 C/C++ 측의 데이터 로딩 진행 상태를 표시할 수 있습니다.

자바 코드를 통해 가져온 카메라 영상 데이터

처리 전 데이터 처리 후 데이터 중도 경과

JNI

영상 변환(C/C++)

프로그레스 다이얼로그 표시(자바)

카메라 애플리케이션

[그림 02-02] JNI 이용 패턴의 예

Page 35: 안드로이드 NDK 네이티브 프로그래밍

2장 ┃ 자바와 안드로이드 NDK

23

이렇듯 JNI는 자바와 C/C++를 상호 연결하는 데 사용합니다. 이때 한 가지 주의해야 할 점

이 있습니다. JNI는 ‘라이브러리가 아니라 인터페이스’라는 점입니다. JNI의 주안점은 자바와 C/

C++가 문제없이 서로의 함수와 메서드를 호출할 수 있게 인터페이스를 정의하는 것이며, 이를

보완하기 위한 라이브러리도 준비돼 있습니다. 이 인터페이스에 따라 자바의 메서드 및 C/C++

의 함수를 정의함으로써 비로소 자바와 C/C++가 서로의 함수를 호출할 수 있게 됩니다.

JNI를 이용한다고 해서 C/C++와 자바의 기능 차이를 완전히 메울 수 있는 건 아닙니다. 그래

서 JNI를 사용하기 위한 제약이 존재합니다.

02.02 JNI의 규약

앞에서도 설명했듯이 자바와 C/C++의 문법은 비슷한 듯하지만 다릅니다. 또 동작 환경도 달빅

VM에서 동작한다거나 CPU에서 직접 동작하는 등 전혀 다릅니다. 이러한 차이를 JNI가 어느

정도는 해결해 주지만 이처럼 서로 다른 환경을 상호 연결하려면 프로그램을 만들 때 여러 가지

규약을 지켜야 합니다. 지금부터 JNI 규약에 대해 자세히 알아보겠습니다.

02.02.01 메모리 관리

자바와 C/C++의 큰 차이점 가운데 하나는 메모리를 관리하는 방법입니다. 자바로 만든 애플리

케이션의 메모리는 전적으로 달빅 VM이 일괄적으로 관리하며, 개발자는 메모리 관리에 전혀

신경 쓸 필요가 없습니다. 하지만 C/C++에서는 할당한 메모리를 모두 개발자가 책임지고 관리

해야 합니다. 그러므로 메모리를 관리할 때는 C/C++로 프로그래밍할 때처럼 메모리의 할당과

해제를 해야 합니다.

특히, 자바와 C/C++의 메모리 해제는 큰 차이가 있습니다. 자바는 사용하지 않는 메모리를

해제하는 가비지 컬렉션이 달빅 VM에 준비돼 있으므로 메모리를 해제하는 코드를 작성할 필

요가 없습니다.1

한편, C/C++는 할당한 메모리 영역을 해제하지 않았을 경우, 메모리 누수(메모리 영역을 두번

다시 사용할 수 없게 되는 현상)가 발생합니다. 이러한 메모리 누수가 늘어나면 당연히 할당할

수 있는 메모리 영역이 줄어들고, 결국 메모리 할당의 실패로 애플리케이션이 강제 종료됩니다.

1  단, 안드로이드는 임베디드 기기용 자바이기도 하므로 명시적으로 메모리 해제를 해야 할 때도 있습니다.

Page 36: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

24

메모리 누수는 골치 아픈 버그(발생 조건이 명확하지 않은 애플리케이션의 강제 종료 등)를 일

으킬 수 있는 원인이 되므로 메모리를 할당하거나 해제할 때는 세심한 주의를 기울여야 합니다.

또 C/C++에서 하는 일반적인 메모리 할당(malloc 또는 free, new 또는 delete)과 별개로 JNI

에서는 자바에 의해 할당된 메모리도 C/C++에서 접근할 수 있도록 메모리를 할당한 후 해제할

때도 있습니다. 이러한 메모리는 반드시 해제해야 합니다.

가비지 컬렉션(GC)

해제

해제

할당 해제

감시

JNI

C/C++ 코드

메모리1 메모리1

메모리2 해제된 메모리

자바코드

달빅 VM

메모리

[그림 02-03] JNI의 메모리 관리

02.02.02 타입 선언

자바에서 건네받은 변수를 C/C++에서 사용할 때 자바에서 선언한 변수의 타입에 따라 C/C++

에서 변수를 사용하는 방법이 달라집니다.

int나 long 같은 자바의 원시 타입을 C/C++에서 사용할 때는 자바의 타입 앞에 j를 붙여서 선

언합니다. 예를 들면, 자바의 int 타입을 JNI에서 사용하려면 jint로 선언합니다.

그리고 자바의 원시 타입으로 선언한 배열을 다룰 때는 선언한 타입 뒤에 Array를 붙여서 선

언합니다. 예를 들면, 자바의 byte[](바이트 타입의 배열 선언)가 C/C++에서는 jbyteArray가 됩

니다.

또 jbyteArray 같은 자바의 배열을 C/C++에서 사용할 때, JNI에 준비된 배열 변환 함수를 이

용하면 자바의 배열 변수에 접근할 수 있습니다(이 내용은 뒤에서 자세히 설명하겠습니다).

또 이런 j~와 같은 타입명은 헤더 파일(JNIHELP.h)에 int 타입의 별명으로 정의됩니다. 물론,

별명으로 정의되기 이전의 타입으로 선언(예를 들면, jint 타입이 아니라 int 타입으로 선언)해

도 됩니다. 하지만 jni를 거쳐 만들어진 변수나 앞으로 자바에서도 사용할 가능성이 있는 변수

Page 37: 안드로이드 NDK 네이티브 프로그래밍

2장 ┃ 자바와 안드로이드 NDK

25

는 프로그램의 이해를 돕기 위해서라도 jint처럼 JNI에서 준비한 타입명으로 선언하는 게 좋습

니다.

[표 02-01] 자바와 JNI의 타입

자바 JNI(C) JNI(C, 배열)

boolean jboolean jbooleanArray

byte jbyte jbyteArray

char jchar jcharArray

short jshort jshortArray

int jint jintArray

long jlong jlongArray

float jfloat jfloatArray

double jdouble jdoubleArray

object jobject jObjectArray

이 밖에도 자바의 인스턴스를 사용할 수 있습니다. 원시 타입이 아닌 인스턴스를 사용할 때는

jobject 타입(자바의 Object 타입)을 선언합니다. jobject 타입으로 건네받은 인스턴스는 그대로

사용할 수 없으므로 C/C++ 코드에서는 리플렉션을 이용해 대상 C++ 클래스로 변환한 후 메

서드를 호출하거나 멤버 변수의 값을 구합니다(29페이지에서 설명).

02.02.03 시그니처

JNI에는 시그니처라는 독특한 형태가 있습니다. 시그니처는 호출하는 메서드명, 인수, 반환값

을 지정해 유일한 메서드를 만들고자 할 때 사용하며, 원시 타입 및 클래스에 따라 기술하는 방

법이 다릅니다. 먼저, 원시 타입은 알파벳 한 글자로 표현합니다(byte인 경우 B, long인 경우 J

등). 또한 클래스(자바의 String 클래스, 자바의 표준 클래스, 애플리케이션 프레임워크의 클래

스 등)를 지정할 때는 클래스명 앞에는 L을, 끝에는 세미콜론을 붙이고, 클래스명은 패키지명을

포함한 문자열로 지정합니다. 예를 들면, 자바의 String 타입을 시그니처로 표기할 때는 ‘Ljava/

lang/String;’으로 표기합니다.

또한 배열은 선두에 ‘[’을 붙여서 지정합니다. 예를 들면, int[](int 타입 배열)은 ‘[l;’로 표기하

고, String 타입 배열은 ‘[Ljava/lang/String;’으로 표기합니다.

Page 38: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

26

메서드의 인수와 반환값은 소괄호 ‘(’와 ‘)’로 구별하며, 괄호 안에는 인수의 타입을 순서대로

정의하고, 괄호의 바깥 즉, ‘)’의 다음에는 반환값의 타입을 정의합니다. 반환값이 void 타입이라

면 ‘V’로 표기합니다. 다음 표는 원시 타입의 시그니처를 나열한 목록입니다.

[표 02-02] 원시 타입의 시그니처 목록

시그니처 원시 타입

B byte

C char

D double

F float

I int

J long

S short

V void

Z boolean

L클래스명; 완전 수식 클래스명* 지정

[클래스명 배열

(인수)반환값의 타입 인수와 반환값의 정의

* (옮긴이) ‘ Ljava/lang/String;’과 같이 패키지를 포함한 클래스의 경로를 모두 적어주는 것을 완전 수식 클래스명 또는 완전 수식명이라고 합니다.

그럼, 시그니처를 사용해 함수를 선언하는 예를 살펴보겠습니다.

[표 02-03] 시그니처를 사용해 인수와 반환값을 선언한 예

함수 선언 시그니처

void foo(int val) (I)V

String bar(int val) (I)Ljava/lang/String;

void buzz() ()V

02.02.04 C와 C++의 차이

JNI에서는 C와 C++를 모두 사용할 수 있습니다. 그러나 C와 C++ 중 어느 언어를 사용하느냐

에 따라 JNI의 함수(멤버 함수)를 호출하는 방법이 달라집니다. C에서는 env 구조체의 함수 포

인터를 이용해 호출하고, C++에서는 env 인스턴스의 멤버 함수를 호출합니다.

Page 39: 안드로이드 NDK 네이티브 프로그래밍

2장 ┃ 자바와 안드로이드 NDK

27

C와 C++의 차이점을 예제를 이용해 설명하겠습니다.

다음은 JNI의 FindClass 함수를 호출하는 예입니다. 이 FindClass 함수는 인수로 설정한 클

래스를 구하는 함수입니다.

jclass jklass = (*env)->FindClass(env, "java/lang/Integer");

C 언어에서는 위와 같이 코드를 작성합니다. C 언어에서는 함수 포인터를 호출하므로 첫 번

째 인수에 JNIEnv 타입의 변수를 설정하고, 두 번째 이후에는 FindClass 함수에 필요한 인수를

설정합니다.

이와 같은 내용을 C++에서는 다음과 같이 작성합니다.

jclass jklass = env->FindClass("java/lang/Integer");

여기서는 env 인스턴스의 멤버 함수를 호출하므로 앞에서처럼 env 구조체를 인수로 설정할

필요 없이 멤버 함수인 FindClass에 필요한 인수만 설정합니다.

또 C++로 만든 경우 자바에서 호출되는 함수는 extern "C"를 선언해야 합니다.

앞으로 계속되는 예제에서는 C 언어를 사용해 설명하겠습니다.

02.02.05 메서드의 정의

JNI를 이용해 C의 함수를 호출하려면 호출하는 쪽인 자바와 호출되는 쪽인 C/C++는 JNI의 규

정에 따라 정의해야 합니다. 먼저 자바 측에서는 다음과 같은 규정을 지켜야 합니다.

≑ 호출하는 함수를 선언할 때는 native를 추가한다.

≑ loadLibrary()를 이용해 호출할 라이브러리를 지정한다.

그리고 C/C++ 측에서는 다음 규정을 지켜야 합니다.

≑ 함수명은 Java로 시작해야 하며, 패키지명, 클래스명, 메서드명을 연결해서 기술해야 한다.

Page 40: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

28

≑ 선두에 설정하는 2개의 인수는 JNIEnv 타입과 jboject 타입으로 한다.

다음은 2개의 값을 더하는 addVals 함수가 포함된 libcalcvals.so 파일을 로드한 후, 자바에

서 호출하는 예제입니다.

먼저, 자바 측(MainActivity.java)에서 addValues 메서드를 선언합니다.

package com.example.calcval;

public class MainActivity extends Activity {

...

public native int addVals(int a, int b); // native를 붙인다.

static { System.loadLibrary("calcvals"); // libcalcvals.so를 로드한다. } }

여기서 addVals 메서드는 JNI를 이용해 호출되는 네이티브 코드이므로 선언할 때 ‘native’를

지정합니다. 그리고 addVals 메서드는 libcalcvals.so 파일에 포함돼 있으므로 미리 loadLibrary

메서드를 이용해 libcalcvals.so 파일을 로드합니다.

다음은 C 언어로 구현된 부분의 코드입니다.

#include <jni.h>

jint Java_com_example_calcval_MainActivity_addVals(JNIEnv* env, jobject thiz, jint a, jint b) { return a + b; }

먼저 jni.h를 인클루드합니다. jni.h에는 JNI를 이용하는 데 필요한 함수와 구조체가 정의돼

있습니다.

함수명은 ‘Java_패키지명_클래스명_메서드명’의 순서로 나열하고, 밑줄(_)를 이용해 함수명

을 선언합니다.

자바 측 코드에서는 com.example.calcval 패키지에 들어있는 MainActivity 클래스의

Page 41: 안드로이드 NDK 네이티브 프로그래밍

2장 ┃ 자바와 안드로이드 NDK

29

addVals 메서드를 호출하므로 함수명은 Java_com_example_calcval_MainActivity_addVals

가 됩니다.

함수의 인수 중 선두의 2개(JNIEnv 타입의 변수 env와 jobject 타입의 변수 thiz)는 JNI로부

터 건네받는 변수로, JNI에서 선언한 멤버 함수는 반드시 이 2개의 변수를 인수로 건네받아야

합니다.

첫 번째는 JNIEnv 타입의 변수 env이며, JNI의 환경정보가 들어 있습니다. 이 env 변수를 통

해 자바 측 실행 정보를 취득해 설정합니다. 두 번째는 jobject 타입의 변수인 thiz를 정의합니다.

이 변수에는 이 함수가 포함된 클래스의 정보가 들어 있습니다(이 예제에서는 MainActivity의

인스턴스 정보가 포함돼 있습니다).

세 번째 이후의 인수는 자바 측에서 선언한 인수를 통해 이 메서드에 전달한 변수입니다(타입

명은 자바와 C를 통일해야 합니다).

위 예제에서는 두 개의 정수형 숫자입니다. 반환값 설정은 C 언어와 같습니다.

02.02.06 클래스와 메서드

계속해서 C/C++에서 자바의 클래스와 메서드에 접근하는 방법을 설명하겠습니다. 앞에서도

설명했듯이 C/C++에서 직접 자바의 클래스나 메서드에 접근하는 것은 불가능합니다. 그러나

JNI는 자바의 리플렉션을 이용할 수 있는 기능을 지원하므로 이 기능을 이용하면 C/C++에서

자바의 클래스를 호출할 수 있습니다.

리플렉션이란?

리플렉션이란 프로그램을 실행할 때 자바의 클래스에 접근할 수 있는 기능입니다. 그 덕분에 빌

드가 아닌 실행을 할 때 호출할 메서드를 지정하는 등 프로그램의 유연성을 높일 수 있습니다.

다음은 리플렉션을 이용해 C/C++에서 자바 메서드를 호출하는 순서입니다.

1. FindClass 함수를 이용해 사용할 함수를 지정합니다.

2. 1의 클래스에서 메서드명과 인수를 지정해 해당 메서드의 ID를 구합니다.

3. 클래스와 메서드 ID를 지정해 메서드를 호출합니다(반환값의 타입과 정적(static) 메

서드 여부에 따라 호출하는 메서드가 다름).

Page 42: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

30

JNI 코드에서 자바의 Integer 클래스에 있는 parseInt 메서드를 호출해 문자열을 int 타입의

숫자로 변환하는 프로그램을 예로 들어 설명하겠습니다.

// jstring strInt;

jclass jklass = (*env)->FindClass(env, "java/lang/Integer"); ————————————

jmethodID jmethod = (*env)->GetStaticMethodID(env, jklass, "parseInt", "(Ljava/lang/String;)I"); —————————

if (jmethod == NULL) return; ———————————————————————————

jint value = (*env)->CallStaticIntMethod(env, jklass, jmethod, strInt); ————————

FindClass 함수를 이용해 java.lang.Integer 클래스를 가져옵니다. 클래스는 완전 수식 클래스명으로 지정합니다.

GetStaticMethodID 함수에 메서드("parseInt")와 인수, 그리고 반환값의 타입("(Ljava/lang/String;)I")을 전달하고 해당 메서드의 ID를 구합니다. 해당하는 메서드가 없으면 NULL이 반환됩니다. 이 메서드에서는 정적 메서드를 취득했지만 일반적인 메서드에서 메서드 ID를 가져올 때는 GetMethodID 함수를 지정합니다.

메서드를 실행할 수 있는 형태인지 NULL 체크를 합니다.

과 에서 구한 클래스와 메서드를 실행합니다. 여기서 지정한 메서드명은 정적 메서드의 여부와 반환값의 타입에 따라 달라집니다. parseInt는 int 타입을 반환값으로 하는 정적 메서드이므로 CallIntStaticMethod 함수를 실행합니다.

메서드를 실행하는 함수(위의 예제에서는 CallIntStaticMethod)는 반환값이나 Static 메서드

인지 아닌지에 따라 명칭이 달라집니다.

[표 02-04] 리플렉션을 실행하는 메서드

반환값 일반 메서드 정적 메서드

Object CallObjectMethod CallStaticObjectMethod

Boolean CallBooleanMethod CallStaticBooleanMethod

Byte CallBytemethod CallStaticBytemethod

Char CallCharMethod CallStaticCharMethod

Int CallIntMethod CallStaticIntMethod

Long CallLongMethod CallStaticLongMethod

Float CallFloatMethod CallStaticFloatMethod

Double CallDoubleMethod CallStaticDoubleMethod

Void CallVoidMethod CallStaticVoidMethod

일반 메서드는 CallXXXMethod(XXX는 반환값의 타입명)를, 정적 메서드는 CallXXXStatic

Method(XXX는 반환값의 타입명)를 이용해 호출합니다.

또 이 밖에도 인수를 구하는 방법이 다른 메서드로는 CallXXXMethodV(XXX는 반환값의

타입명)와 CallXXXMethodA(XXX는 반환값의 타입명)가 있습니다.

Page 43: 안드로이드 NDK 네이티브 프로그래밍

2장 ┃ 자바와 안드로이드 NDK

31

02.02.07 멤버 변수

자바의 멤버 변수도 앞서 설명한 메서드와 마찬가지로 리플렉션을 이용해 가져올 수 있습니

다. 순서는 앞에서 설명한 자바의 클래스와 메서드를 이용하는 방법과 같습니다. 다른 점은

GetMethodID 함수 대신 GetFieldID 함수를 사용한다는 점입니다. 또 값을 가져올 때는 메서

드를 호출할 때 사용했던 CallIntMethod 함수 대신 GetIntField 함수와 같이 GetXXXField

함수(XXX는 반환값의 타입명)를 사용합니다.

intValue라는 int 타입의 멤버 변수를 가져오는 코드를 이용해 설명하겠습니다.

jfieldID jfield = (*env)->GetFieldID(env, instance, "intValue", "I"); ————————

if (jfield == NULL) return; ———————————————————————————

jint val = (*env)->GetIntField(env, instance, jfield); —————————————————

GetFieldID 함수에서 가져올 멤버 변수와 타입을 지정합니다. 첫 번째 인수는 JNI의 환경정보이고, 두 번째 인수는 클래스(인스턴스)의 변수입니다. 세 번째 인수에는 호출할 변수명을, 네 번째 인수에는 멤버 변수의 타입을 지정합니다. 이 함수에서는 j�eld 타입의 멤버 변수를 가져옵니다. 해당하는 멤버 변수가 없으면 NULL을 반환합니다.

멤버 변수가 존재하는지 확인합니다.

GetIntField 함수에서 해당하는 멤버 변수를 int 타입으로 가져옵니다. 첫 번째 인수에는 JNI의 환경정보를, 두 번째 인수에는 클래스의 인스턴스 변수를, 세 번째 인수에는 호출할 멤버 변수의 ID를 지정합니다. 이번에도 마찬가지로 가져올 멤버 변수의 타입에 따라 호출할 함수명이 GetXXXField(XXX는 멤버 변수의 타입명)로 바뀝니다.

02.02.08 배열

계속해서 C/C++에서 자바의 배열을 사용하는 방법을 설명하겠습니다.

자바의 배열과 C/C++의 배열은 문법적으로는 대괄호 []에 첨자를 지정해 배열 안의 데이터를

가져오거나 설정합니다. 그러나 자바와 C/C++의 배열은 내부적으로 배열을 이용하는 구조가

다릅니다.

자바의 배열은 배열 클래스로 취급되며, 각 요소가 실제 메모리상에서 반드시 연속으로 배치

돼 있지는 않습니다(또 이런 사실을 확인할 수도 없습니다). 그러나 C/C++의 배열은 배열의 요

소가 실제 메모리상에서도 연속으로 배치돼 있습니다.

Page 44: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

32

메모리

3000

3004

3008

A[0]

C/C++의 배열

연속돼 있다

A[1]

A[2]

:

메모리

주소주소

4000

4004

4100

B[0]

자바의 배열

반드시 연속돼있는 건 아니다

B[1]

B[2]

[그림 02-04] 자바와 C의 배열 메모리의 이미지

이렇듯 자바와 C의 배열은 서로 다르므로 C에서 자바의 배열을 사용하려면 먼저 변환을 해

야 합니다.

C/C++에서 접근할 수 있는 자바의 배열을 가져오려면 먼저 GetXXXArrayElements(XXX

는 타입명) 함수를 사용합니다. 이 함수는 C/C++에서 자바의 배열에 접근할 수 있는 배열의 포

인터를 반환해 줍니다.

또 이 Get X X X ArrayElements(X X X는 타입명)로 확보한 메모리 영역은 사용 후

releaseArrayElements 함수를 이용해 해제해야 합니다.

다음은 int 타입의 배열을 이용하는 코드를 예로 들어 설명하겠습니다.

// 새로운 배열 생성

jintArray ary = (*env)->NewIntArray(env, 10); ———————————————————

// 배열의 길이를 구한다. int length = (*env)->GetArrayLength(env, ary); ——————————————————

// C의 배열로 변환한다. jint* arraylist = (*env)->GetIntArrayElements(env, ary, NULL); ———————————

: (다른 처리 부분) :

// 자원을 해제한다. (*)->ReleaseIntArrayElements(env, ary, arraylist,0); ————————————————

자바에서 사용할 수 있는 새로운 배열을 생성합니다. 첫 번째 인수는 JNI의 환경정보를 전달하고, 두 번째 인수는 새로 생성하는 배열의 크기를 지정합니다.

배열의 길이를 구합니다. 첫 번째 인수로는 JNI의 환경정보를 전달하고, 두 번째 인수로는 배열의 길이를 jIntArray 타입으로 지정합니다. 반환값은 배열의 길이입니다.

C/C++에서 사용할 수 있는 배열로 변환합니다. jIntArray 타입으로는 C/C++에서 접근할 수 없습니다. 따라서

Page 45: 안드로이드 NDK 네이티브 프로그래밍

2장 ┃ 자바와 안드로이드 NDK

33

GetIntArrayElements 함수로 변환한 후, 그 반환값(포인터)을 이용하면 C/C++의 배열로서 사용할 수 있습니다.

C/C++에서 접근할 수 있게 확보한 메모리를 해제합니다. GetIntArrayElements로 확보한 영역은 ReleaseIntArray Elements를 사용해 해제해야 합니다. 첫 번째 인수로는 JNI의 환경정보를 전달하고, 두 번째 인수로는 해제하고자 하는 jIntArray 타입의 변수를 지정합니다. 세 번째 인수는 해제하고자 하는 int 타입의 포인터를 지정합니다.

02.02.09 예외

JNI를 이용하면 C/C++의 함수 안에서도 자바의 예외를 발생시키거나 가져올 수 있습니다.

그러므로 C/C++ 코드 안에서 발생한 에러라 할지라도 함수의 반환값을 이용해 상태를 알리

는 것 말고도 예외를 발생시켜 자바 측에서 예외 처리를 할 수 있습니다.

예외를 발생시키려면 �rowNew 함수를 사용합니다.

다음 예제는 JNI의 함수를 이용해 IOException을 발생시킨 예입니다.

jclass klass= (*env)->FindClass(env, "java/io/IOException"); if (klass == NULL) return; (*env)->ThrowNew(env, klass, "io error"); return;

예외가 발생했는지 확인하려면 ExceptionCheck 함수를 이용합니다. 이 함수는 예외가 발생

했다면 true를 반환합니다. 발생한 예외는 ExceptionOccurred 함수로 확인할 수 있습니다. 발

생한 예외를 다른 코드에서 처리하고자 할 때는 그대로 return 하면 됩니다.

if ((*env)->ExceptionCheck(env)) return;

발생한 예외를 가져와 내용에 따라 처리 방법을 나누고자 할 때는 ExceptionOccurred 함수

를 이용합니다. 이 함수를 이용해 예외를 확인하고 IsInstanceOf 메서드를 이용해 발생한 예외

가 얻고자 하는 클래스의 인스턴스인지 확인한 후 다음 처리를 계속합니다.

Jclass jklass = (*env)->FindClass(env, "java/lang/RuntimeException"); jthrowable jexception = (*env)->ExceptionOccured(env); if (jexception != NULL) { if ((*env)->IsInstanceOf(env, jexception, jklass) { // 발생한 예외는 RuntimeException을 상속한 것임. ...

Page 46: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

34

} }

발생한 예외를 정리하려면 ExceptionClear 함수를 사용합니다.

02.02.10 문자열

다음은 JNI에서 문자열을 다루는 방법을 설명하겠습니다.

자바와 C/C++는 문자열을 다루는 방법이 서로 다릅니다. C/C++에서는 개발자의 구현에 의

존(개발자가 자유롭게 문자코드를 결정할 수 있다)하지만 자바에서는 문자코드로 유니코드만

사용합니다.

유니코드는 인코딩 방법에 따라 UTF-8, UTF-16 등으로 나뉘며, JNI에서는 UTF-8과 UTF-8

이외의 문자코드에 따라 처리 방법이 달라집니다.

[표 02-05] 유니코드(UTF-16 등)로 다루는 경우

함수명 개요

NewString 새로운 유니코드(UTF-8, UTF-16 등) 문자열을 생성한다.

GetStringLength 유니코드 문자열의 길이를 반환한다.

GetStringChars 유니코드 문자의 배열을 참조하는 포인터를 반환한다.

ReleaseStringChars GetStringChars로 취득한 포인터를 해제한다.

[표 02-06] 유니코드(UTF-8)로 다루는 경우

함수명 개요

NewStringUTF 새로운 유니코드(UTF-8) 문자열을 생성한다.

GetStringUTFLength UTF-8의 바이트 수를 반환한다.

GetStringUTFChars UTF-8 인코딩 문자열을 나타내는 바이트 배열의 포인터를 반환한다.

ReleaseStringUTFChars GetStringUTFChars로 취득한 포인터를 해제한다.

유니코드로 문자코드를 취급할 때는 UTF-8로 인코딩된 문자열을 취급하는 경우가 많습니다.

계속해서 UTF-8에서 문자열을 다루는 예를 설명하겠습니다.

Page 47: 안드로이드 NDK 네이티브 프로그래밍

2장 ┃ 자바와 안드로이드 NDK

35

// 새로운 문자열을 생성한다. jstring jstr = (*env)->NewStringUTF(env, "펭귄"); ——————————————

// 문자열을 반환한다. jsize jlen = (*env)->GetStringLength(env, jstr); ——————————————————

// 문자열의 바이트 배열의 포인터를 반환한다. const char* bytes = (*env)->GetStringUTFChars(env, jstr, NULL); —————————

// 자원을 해제한다. (*env)->ReleaseStringUTFChars(env, jstr, bytes); ————————————————

UTF-8의 형태로 자바의 String 타입의 새로운 변수를 생성합니다. 이 경우 문자열은 UTF-8로 인코딩돼 반환됩니다.

문자열의 길이를 반환합니다. GetStringLength가 아닌 GetStringUTFLength를 사용하면 문자열의 바이트 수를 구할 수 있습니다.

C/C++에서 다루기 쉬운 const char*형으로 변환합니다.

NewStringUTF로 확보한 메모리 영역은 ReleaseStringUTFChar 함수를 이용해 해제해야 합니다.

02.02.11 대규모 메모리

지금까지는 int 타입이나 문자열 등 비교적 소규모의 데이터를 C/C++에서 다루는 방법을 설명

했습니다. 그러나 항상 소규모 데이터만 다룰 수는 없습니다. 이미지 데이터와 같은 대량의 데이

터를 다룰 일도 있을 것입니다.

이번에는 이미지 데이터를 C/C++에서 처리할 때처럼 대량의 데이터를 자바에서 C/C++로 전

달하는 방법을 설명하겠습니다. 이런 대량의 데이터를 C/C++의 함수에 전달하는 방법은 두 가

지가 있습니다.

1. 자바의 배열을 이용한다.

2. java.nio.ByteBu�er 클래스를 이용한다.

첫 번째는 자바의 배열을 인수로 전달하는 방법입니다. 자바에서 설정한 배열은 한 번의 변

환으로 C/C++에서 이용할 수 있습니다. 그러므로 자바의 배열을 이용하려면 배열 데이터를 C/

C++용으로 변환해서 저장할 영역과 변환하는 시간이 필요합니다.

두 번째는 바이너리 데이터를 이용하기에 적합한 java.nio.ByteBu�er 클래스를 이용하는 방

법입니다. jni.nio.ByteBu�er 클래스를 이용하면 자바의 배열을 이용할 때와 같은 데이터 변환

과정이 필요없습니다. 그러므로 필요한 메모리의 용량도 적게 들며, 처리 시간도 단축됩니다.

Page 48: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

36

또 jni.nio.ByteBu�er 클래스를 이용하면 이미지 데이터와 같은 바이너리 데이터 말고도 문자

열이나 구조체와 같은 각종 데이터를 담아서 전달할 수도 있습니다.

java.nio.ByteBu�er 클래스를 이용하면 다음과 같은 함수를 사용할 수 있습니다.

NewDirectByteBuffer(JNIEnv* env, void* address, jlong capacity)

메모리 주소와 크기를 설정하고 java.nio.ByteBu�er의 새로운 인스턴스를 생성한다.

GetDirectBufferAddress(JNIEnv *env, jobject buf)

java.nio.ByteBu�er의 인스턴스를 전달하고 시작 주소를 반환한다.

GetDirectBufferCapacity(JNIEnv *env, jobject buf)

java.nio.ByteBu�er의 인스턴스를 전달하고 크기를 반환한다.

ByteBu�er를 이용해 데이터를 주고받는 예제를 예로 들어 설명하겠습니다. 이 예제는 자바

측에서 ByteBu�er를 이용해 C의 구조체 형태로 데이터를 배치한 후, C/C++에서 구조체로 데

이터를 가져와 덧셈을 하고 그 결과를 다시 자바 측에서 가져오는 식으로 동작합니다.

ByteBuffer byteBuffer = ByteBuffer.allocateDirect(10); ———————————————

byteBuffer.order(ByteOrder.LITTLE_ENDIAN); ———————————————————

int a = 30; short b = 20; byte c = 10;

byteBuffer.putInt(0, a); ————————————————————— byteBuffer.putShort(4, b); ————— byteBuffer.put(6, c); (3) ————————————————————— setByteBuffer(byteBuffer); ———————————————————————————

TextView tv = new TextView(this);

StringBuffer strBuf = new StringBuffer();

strBuf.append("a="+byteBuffer.getInt(0) + "\n"); ——————————————— strBuf.append("b="+byteBuffer.getShort(4) + "\n"); —————

strBuf.append("c="+byteBuffer.get(6) + "\n"); ——————————————————

ByteBu�er의 영역을 10바이트 확보합니다. 또 allocateDirect 메서드를 이용해 확보한 영역을 변경할 수 없게 합니다.

byteBu�er의 바이트 순서를 리틀 엔디언으로 설정합니다. 자바는 빅 엔디언이 기본값이지만 달빅에서는 String 이외의 모든 수치는 리틀 엔디언으로 취급합니다.

Page 49: 안드로이드 NDK 네이티브 프로그래밍

2장 ┃ 자바와 안드로이드 NDK

37

확보한 ByteBu�er에 구조체 형태로 숫자를 대입합니다. 자바에서는 int는 4바이트, short는 2바이트, byte는 1바이트로 정해져 있습니다. 이 예제에서는 0번부터 4바이트까지는 long 타입의 변수 영역이고, 4번부터 2바이트까지는 short 타입의 변수 영역입니다. 그리고 6번부터는 byte 타입의 영역입니다. 각 자리에 맞춰 수치를 대입합니다.

jni에서 정의한 setByteBu�er 메서드를 호출합니다.

byteBu�er에서 데이터를 꺼내 표시합니다.

typedef struct _DataInfo { int a; short b; char c; } DataInfo;

jint setByteBuffer(JNIEnv* env, jobject thiz, jobject buf) { DataInfo *dataInfo;

dataInfo = (*env)->GetDirectBufferAddress(env, buf); ————————————————

dataInfo->a += 10; ————————————————————————————

dataInfo->b += 10; dataInfo->c += 10;

return 0; }

GetDirectBu�erAddress 함수를 이용해 인수로 구한 ByteBu�er의 시작 주소를 DataInfo 구조체에 넣어둡니다.

DataInfo 구조체의 변수를 계산합니다. 이로써 ByteBu�er의 버퍼 데이터는 수정됐습니다.

이후 자바 측에서 같은 ByteBu�er의 데이터에 접근하면 변경 후의 데이터를 얻게 됩니다.

02.02.12 리틀 엔디언과 빅 엔디언

앞에서 리틀 엔디언과 빅 엔디언이라는 용어가 언급됐기에 여기서 좀 더 자세하게 설명하겠습

니다.

C나 자바 같은 컴파일형 언어는 변수에 타입을 선언함으로써 변수를 사용할 수 있게 됩니다.

C/C++나 자바에서 사용하는 데이터는 여러 바이트로 나타내는 경우가 있습니다. 예를 들면,

자바에서 long 타입은 8바이트의 데이터를, int 타입은 4바이트의 데이터를 저장할 수 있습니다.

한편, 컴퓨터의 메모리는 1바이트 단위로 데이터가 저장되므로 int 타입과 같이 여러 바이트

로 구성된 변수를 저장하는 방법으로 상위 바이트에서 하위 바이트의 순서로 저장하는 방법과

하위 바이트에서 상위 바이트의 순서로 저장하는 두 가지 방법이 있습니다. 좀 더 구체적으로

살펴봅시다.

Page 50: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

38

0x12345678라는 int 타입의 데이터를 메모리에 저장한다고 했을 때 0x12345678을 메모리에

저장하기 위해서는 상위 바이트(0x12345678에서는 0x12)부터 저장하는 방법과 하위 바이트

(0x12345678에서는 0x78)부터 저장하는 방법이 있습니다.

이때 상위 바이트부터 순서대로 메모리에 저장하는 방법을 빅 엔디언(big endian)이라 하고,

하위 바이트부터 저장하는 방법을 리틀 엔디언(little endian)이라고 합니다.

리틀 엔디언

←상위 주소(n+3번지) 하위 주소(n번지)→

0x12 0x34 0x56 0x78

빅 엔디언

←상위 주소(n+3번지) 하위 주소(n번지)→

0x78 0x56 0x34 0x12

[그림 02-05] 0x12345678을 메모리에 저장

이처럼 수치가 리틀 엔디언과 빅 엔디언 중 어떤 방식으로 저장되느냐는 각 시스템에 따라 달

라집니다. 인텔 아키텍처에서는 리틀 엔디언을, ARM 프로세서에서는 빅 엔디언과 리틀 엔디언

을 둘 다 사용할 수 있습니다(바이 엔디언이라고도 합니다).

또 달빅 VM의 내부에서는 데이터가 리틀 엔디언으로 취급됩니다. 일반적으로 데이터는 주로

리틀 엔디언으로 취급하지만 빅 엔디언으로 데이터를 저장하는 시스템도 있습니다. C 언어로 데

이터를 다룰 때는 CPU 및 시스템 사양서를 통해 빅 엔디언으로 할 것인지 리틀 엔디언으로 할

것인지 미리 파악한 후에 프로그램을 만들어야 합니다.

02.02.13 Android.mk 파일

안드로이드 NDK를 이용해 빌드하려면 Android.mk 파일이 필요합니다.

C/C++ 코드를 빌드할 때 Make�le을 만들어서 빌드하는 경우가 종종 있습니다. 하지만 안드

로이드 NDK에서는 Make�le 대신 Android.mk 파일을 만들어야 합니다.

Android.mk 파일은 모듈을 만드는 데 필요한 소스코드 및 링크할 라이브러리를 지정하거나,

Page 51: 안드로이드 NDK 네이티브 프로그래밍

2장 ┃ 자바와 안드로이드 NDK

39

라이브러리의 이름을 지정하는 등 빌드에 필요한 최소한의 정의를 합니다.2 문법은 GNU make

에 따라 작성합니다.

그럼 가장 기본적인 Android.mk 파일을 한번 보겠습니다. 이 파일에서는 C의 소스코드인

calcvals.c에서 모듈인 libcalcvals.so 파일을 생성합니다. 이 libcalcs.so 파일이 안드로이드의 실

행파일(.apk 파일)에 포함되어 애플리케이션이 실행되는 것입니다.

LOCAL_PATH := $(call my-dir) —————————————————————————

include $(CLEAR_VARS) ———————————————————————————

LOCAL_MODULE := calcvals —————————————————————————

LOCAL_SRC_FILES := calvals.c ————————————————————————

include $(BUILD_SHARED_LIBRARY) ——————————————————————

필수 설정입니다. 이 설정으로 LOAD_PATH 변수에 현재 디렉터리를 전달하게 됩니다.

필수 설정입니다. 이전에 했던 모든 설정을 정리하고, 이 행 이후의 변수에 영향을 주지 않게 합니다.

생성할 모듈명을 지정합니다. 이 예제에서는 calcvals이므로 libcalcvals.so라는 공유 라이브러리가 생성됩니다.

에서 생성된 모듈을 빌드하는 데 필요한 소스파일을 지정합니다.

이 라이브러리를 공유 라이브러리로 생성합니다.

02.03 로그 출력하기

안드로이드 NDK를 이용할 때도 자바에서 Log 클래스를 이용하는 것처럼 logcat을 이용해 로

그를 출력할 수 있습니다. 이처럼 로그를 출력해 봄으로써 현재 어떤 동작이 이뤄지고 있는지 확

인할 수도 있고, 파라미터를 로그에 출력해 버그의 원인을 찾아내는 데 이용할 수도 있습니다.

로그를 출력하려면 log.h에 정의된 android_log_print 함수를 이용합니다. 하지만 이 함수를

그대로 사용하기에는 불편함이 있으므로 레벨별로 매크로를 정의합니다.

#include <jni.h> #include <android/log.h>

#define LOG_TAG "tagname" #define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)

2  Make�le에서는 보통 컴파일러를 지정하지만 Android.mk 파일은 그럴 필요가 없습니다.

Page 52: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

40

로그를 표시하기 위해 android/log.h를 인클루드합니다. android/log.h에는 ANDROID_

LOG_INFO 등을 이용해 로그 출력의 레벨을 정의할 수 있는 __android_log_print와 같은 로

그 출력 함수가 정의돼 있습니다.

로그 레벨의 종류는 다음과 같습니다.

[표 02-06] 지정 가능한 로그 레벨

ANDROID_LOG_VERBOSE ↑ 가벼운 레벨

↓ 무거운 레벨

ANDROID_LOG_DEBUG

ANDROID_LOG_INFO

ANDROID_LOG_WARN

ANDROID_LOG_ERROR

ANDROID_LOG_FATAL

로그를 출력하려면 앞서 정의한 LOGE 함수를 이용하며, 인수로는 printf 함수를 이용할 때

와 같은 문법을 사용해 표시하고자 하는 문자열을 전달함으로써 로그에 표시할 수가 있습니다.

LOGE("test passed"); // 메시지만 표시한다. LOGE("values=%d\n", values); // 메시지와 변수를 함께 표시한다.

로그를 출력하려면 liblog.so 라이브러리를 링크해야 합니다.

LOCAL_LDLIBS 변수에 링크할 라이브러리를 설정합니다.

Android.mk

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := foo LOCAL_SRC_FILES := foo.c LOCAL_LDLIBS := -llog # 추가

include $(BUILD_SHARED_LIBRARY)

Page 53: 안드로이드 NDK 네이티브 프로그래밍

2장 ┃ 자바와 안드로이드 NDK

41

로그를 출력할 때도 주의해야 할 점이 있습니다. 로그 출력은 비교적 부하가 큰 작업입니다.

로그를 대량으로 출력하면 애플리케이션의 동작이 느려지는 등 애플리케이션 이용에 지장을

줄 가능성이 있습니다. 그러므로 로그 출력은 필요한 부분만 간추려서 출력해야 합니다.

또 애플리케이션을 배포할 때는 로그가 출력되지 않게 해야 합니다.

02.04 자바에서 C 함수 호출하기

지금까지 JNI의 기능을 살펴봤습니다. 이제 실제로 JNI를 이용한 프로그램을 만들겠습니다. 여

기서는 C로 사칙연산을 하는 함수를 만들고 이 함수를 자바에서 호출하겠습니다. 이번 예제에

서는 int 타입만을 사용해 간단하게 덧셈, 뺄셈, 곱셈, 나눗셈하는 메서드만 만들 것입니다.

02.04.01 프로젝트 만들기

먼저 이클립스에서 안드로이드 애플리케이

션의 프로젝트를 만듭니다. 여기에서는 API

레벨9(안드로이드 2.3)를 기준으로 합니다.

이클립스에서 프로젝트를 만든 후, C/C++

소스코드와 Android.mk 파일을 배치하기

위해 jni 폴더를 추가합니다.

그러면 프로젝트는 다음과 같은 폴더 구조

가 됩니다.

이 jni 폴더 아래에 C/C++ 코드와 Android.

mk 파일을 준비합니다.

[그림 02-06] 프로젝트 구성

Page 54: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

42

02.04.02 예제 코드

앞에서도 설명했듯이 사칙연산을 하는 함수를 준비합니다.

먼저, Android.mk 파일을 만듭니다.

이 파일에 의해 calcvals.c가 빌드되고, libcalcvals.so 파일이 만들어집니다.

Android.mk

LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS)

LOCAL_SRC_FILES := calcvals.c ————————————————————————

LOCAL_MODULE := calcvals —————————————————————————

include $(BUILD_SHARED_LIBRARY) ———————————————————————

LOCAL_SRC_FILES에서 빌드할 소스파일을 지정합니다. 모듈을 생성하는 데 필요한 모든 소스 파일을 이 파일에 지정합니다.

에서 지정한 소스 파일을 토대로 생성할 모듈의 이름을 지정합니다. 여기서 지정한 모듈 이름이 나중에 자바에서 사용하는 이름이 됩니다.

이 라이브러리는 공유 라이브러리로 작성합니다.

계속해서 소스코드에 대해 설명하겠습니다.

여기서는 두 개의 값을 더하는 addVals 함수를 중심으로 실행되는 순서대로 자바 측 코드부

터 설명하겠습니다.

MainActivity.java

package com.example.calcval;

import android.app.Activity; import android.os.Bundle; import android.widget.TextView;

public class MainActivity extends Activity { @Override

public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);

Page 55: 안드로이드 NDK 네이티브 프로그래밍

2장 ┃ 자바와 안드로이드 NDK

43

StringBuffer buf = new StringBuffer(); TextView tv = new TextView(this);

int a = 10; int b = 2;

int val = addVals(a, b); ——————————————————————————————

buf.append("a+b="+String.valueOf(val)+"\n"); val = subVals(a, b); buf.append("a-b="+String.valueOf(val)+"\n"); val = mulVals(a, b); buf.append("a*b="+String.valueOf(val)+"\n"); val = divVals(a, b); buf.append("a/b="+String.valueOf(val)+"\n");

tv.setText( buf.toString() ); setContentView(tv); }

public native int addVals(int a, int b); ————————————————

public native int subVals(int a, int b); public native int mulVals(int a, int b); public native int divVals(int a, int b);

static { System.loadLibrary("calcvals"); ———————————————————————

}

MainActivity 클래스가 사용될 때 ‘calcvals’ 라이브러리를 로드합니다. 이 ‘calcvals’ 라이브러리에는 addVals와 같이 native 선언을 할 때 호출되는 함수가 등록돼 있습니다.

calcvals 라이브러리에 등록된 함수를 메서드로 선언합니다. native로 선언함으로써 이 메서드는 JNI를 통해 호출된다는 의미가 됩니다.

addVals 함수를 호출합니다. 호출하는 방법은 일반적인 메서드를 호출하는 방법과 같습니다.

계속해서 C 코드를 설명하겠습니다.

calcvals.h

#include <jni.h>

#ifndef _addvals_h #define _addvals_h #ifdef __cplusplus

extern "C" { #endif

jint Java_com_example_calcval_MainActivity_addVals( JNIEnv* env, jobject thiz,

Page 56: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

44

jint a, jint b ); jint Java_com_example_calcval_MainActivity_subVals( JNIEnv* env, jobject thiz, jint a, jint b ); jint Java_com_example_calcval_MainActivity_mulVals( JNIEnv* env, jobject thiz, jint a, jint b ); jint Java_com_example_calcval_MainActivity_divVals( JNIEnv* env, jobject thiz, jint a, jint b ); #ifdef __cplusplus } #endif #endif

calcvals.c

#include "calcvals.h"

jint Java_com_example_calcval_MainActivity_addVals( JNIEnv* env, jobject thiz, ————— jint a, jint b ) ———————

{ return a + b; }

jint Java_com_example_calcval_MainActivity_subVals( JNIEnv* env, jobject thiz, jint a, jint b ) { return a - b; }

jint Java_com_example_calcval_MainActivity_mulVals( JNIEnv* env, jobject thiz, jint a, jint b ) { return a * b; }

jint Java_com_example_calcval_MainActivity_divVals( JNIEnv* env, jobject thiz, jint a, jint b ) { return a / b; }

Page 57: 안드로이드 NDK 네이티브 프로그래밍

2장 ┃ 자바와 안드로이드 NDK

45

자바에서 호출된 addVals 메서드는 이 함수에서 실행됩니다. 인수로는 jni의 환경, MainActivity의 인스턴스, 더하기를 할 데이터 A와 B로 4개입니다. 그리고 인수에서 전달된 데이터 A와 B를 더한 값을 반환합니다.

마지막으로 자바의 addVals 메서드로 결과를 가져와 표시합니다.

02.04.03 빌드

필요한 파일을 모두 만들었으니 빌드해 봅시다. 빌드는 이클립스에서 하지 않고, 터미널(커맨드

프롬프트)에서 ndk-build 명령을 이용합니다.

먼저, 프로젝트 폴더로 이동해서 ndk-build 명령을 실행합니다.

% cd <프로젝트 폴더>% ndk-build

이로써 현재 디렉터리에 있는 JNI 폴더 아래의 소스코드가 빌드됩니다. 소스코드에 에러가

있다면 터미널에 에러 개수가 표시됩니다.

“ndk-build를 실행할 수 없다”는 에러가 표시된다면 19페이지의 Android-ndk 폴더의 경로가

제대로 설정돼 있지 않을 가능성이 있으니 경로 설정을 다시 한번 확인합니다.

빌드에 성공했다면 libs/armeabi 폴더 아래에 calcvals 라이브러리 파일(libcalcvals.so)이 생

성될 것입니다. 계속해서 이클립스에서 자바 코드를 빌드합니다. 그러면 조금 전에 생성된 라이

브러리 파일을 포함한 실행파일이 생성됩니다.

칼럼

자바 파일의 변경 시각에 주의!

이때 자바 코드가 바뀌지 않으면 이클립스에서 빌드를 실행해도 새로 빌드가 되지 않습니다.

이는 calcvals 라이브러리가 생성된 시간과는 상관이 없습니다. 이클립스가 실행파일의 생성

시간과 비교해 빌드를 할 것인지 판단하기 때문입니다.

이 문제를 해결하려면 ndk-build로 공유 라이브러리를 만든 다음, 일단 이클립스에서 프로

젝트를 정리(clean)합니다. 그리고 모든 오브젝트 파일과 실행파일을 삭제한 뒤, 다시 한번

빌드합니다. 그러면 조금 전에 만든 라이브러리가 포함된 실행파일이 생성될 것입니다.

Page 58: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

46

실행파일의 실행 결과는 다음과 같습니다.

[그림 02-07] 실행 결과

칼럼

이클립스만으로 프로젝트를 만든다!

C/C++의 소스코드는 ndk-build 명령을 이용해 빌드하는데, 이렇게 되면 이클립스와 터미

널을 번갈아 가며 조작해야 하는 번거로움이 있습니다. 그러나 프로젝트를 설정하면 이클립

스만으로도 C/C++ 코드와 자바 코드를 순서대로 빌드해 실행파일을 만들 수 있습니다. 그러

려면 이클립스에 CDT를 설치해 C/C++의 소스코드를 make가 아닌 ndk-build 명령으로 빌

드할 수 있게 프로젝트의 프로퍼티에서 설정하면 됩니다.

Page 59: 안드로이드 NDK 네이티브 프로그래밍

2장 ┃ 자바와 안드로이드 NDK

47

02.05 JNIHelp 이용하기

이로써 JNI를 이용해 C의 함수를 호출할 수 있게 됐습니다. 그러나 이 방법에는 몇 가지 불편한

점이 있습니다.

먼저, 자바 측에서 네이티브로 선언된 메서드에 맞춰 C/C++ 측에서는 패키지명, 클래스명, 메

서드명을 연결한 함수명을 쓰지 않으면 안 됩니다. 이렇게 되면 함수명이 너무 길어져서 소스코

드의 가독성이 떨어집니다.

또 함수명을 잘못 작성했을 때 잘못된 부분을 찾기 어렵습니다.

JNI는 애플리케이션 프레임워크에서 각종 미들웨어(OpenGL|ES, sqlite 등)를 호출할 때도 이

용됩니다. 이때는 앞에서의 긴 함수명을 사용하지 않고 JNI에서 호출되는 함수를 패키지 단위

로 등록할 수 있는 jniRegisterNativeMethods 함수를 이용해 등록합니다.

그럼 조금 전의 calcvals.c를 jniRegisterNativeMethods 함수3를 이용해 등록해 보겠습니다.

다음은 앞에서 학습한 예제코드를 jniRegisterNativeMethods 함수를 이용해 변환한 코드입

니다.

#include <stdio.h> #include <android/log.h> #include "calcvals.h"

#define EXPORT __attribute__((visibility("default"))) #define LOG_TAG ("addVals")

#define LOGD(...) ((void)__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)) #define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__))

jint addVals(JNIEnv* env, jobject thiz, jint a, jint b) { return a + b; } jint subVals(JNIEnv* env, jobject thiz, jint a, jint b) { return a - b; }

3  jniRegisterNativeMethod 함수는 안드로이드 2.3의 소스코드에 있는 dalvik/libnativehelper 디렉터리의 JNIHelp.c 파일에 포함돼 있습니다.

Page 60: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

48

jint mulVals(JNIEnv* env, jobject thiz, jint a, jint b) { return a * b; }

jint divVals(JNIEnv* env, jobject thiz, jint a, jint b) { return a / b; }

int jniRegisterNativeMethods(JNIEnv* env, const char* className, —————— const JNINativeMethod* gMethods, int numMethods) ——————

———

{ jclass clazz;

LOGD("Registering %s natives\n", className); clazz = (*env)->FindClass(env, className); if (clazz == NULL) { LOGD("Native registration unable to find class '%s'\n", className); return -1; } if ((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0) { LOGD("RegisterNatives failed for '%s'\n", className); return -1; } return 0; }

static JNINativeMethod sMethods[] = { ————————————————————————————————— /* name, signature, funcPtr */ {"addVals", "(II)I", (void*)addVals}, {"subVals", "(II)I", (void*)subVals}, ———

{"mulVals", "(II)I", (void*)mulVals}, {"divVals", "(II)I", (void*)divVals}, }; ————————————————————————————————————————————————————————————————————

EXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) —————————————————————

{ JNIEnv* env = NULL; jint result = -1;

if ((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_6) != JNI_OK) { return result; }

jniRegisterNativeMethods(env, "com/example/calcval/MainActivity", sMethods, NELEM(sMethods)); return JNI_VERSION_1_6; }

jniRegisterNativeMethods를 이용해 JNI에 메서드를 등록합니다.

자바에서 호출하고자 하는 함수를 등록합니다. 왼쪽부터 순서대로 메서드명, 메서드의 인수와 반환값(시그니처로 정의), 함수 포인터를 함께 정의합니다.

JNI_OnLoad 함수는 JNI의 라이브러리가 로드된 후에 호출되는 함수입니다.

Page 61: 안드로이드 NDK 네이티브 프로그래밍

2장 ┃ 자바와 안드로이드 NDK

49

여기서는 jniRegisterNativeMethods 함수를 이용해 자바에서 호출되는 네이티브 메서드를

등록합니다.

02.05.01 JNI에서 애플리케이션 프레임워크 호출하기

지금까지 자바 측에서 C/C++의 함수를 호출하는 방법을 설명했습니다. 이번에는 반대로 C/

C++에서 애플리케이션 프레임워크 같은 자바의 클래스에 접근하는 방법을 설명하겠습니다. 이

때는 앞서 설명한 리플렉션을 이용해 자바의 라이브러리에 접근합니다.

계속해서 Button을 누르면 애플리케이션 프레임워크의 Toast를 표시하는 프로그램을 예로

들어 설명하겠습니다. C 측에서 Toast 클래스를 호출하면 Toast가 show 메서드에 의해 표시됩

니다.

먼저 MainActivity.java를 설명하겠습니다.

MainActivity.java

package com.example.jnitoast;

import android.app.Activity; import android.content.Context; import android.os.Bundle; import android.view.View; import android.widget.Button;

public class MainActivity extends Activity {

@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);

setContentView(R.layout.main);

Button btn = (Button)findViewById(R.id.btn); btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { CharSequence text = "JNI에서 Toast를 표시"; displayToast(text); } }); }

Page 62: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

50

public native void displayToast(CharSequence charseq);

static { System.loadLibrary("jnitoast"); } }

MainActivity.java에서는 버튼 표시와 함께 클릭 이벤트를 이용해 jnitoast.c에 준비된

displayToast 메서드를 호출합니다. Toast의 표시는 jnitoast.c에서 이뤄집니다.

C 측(jnitoast.h)의 코드는 다음과 같습니다.

jnitoast.h

#include <jni.h>

#ifndef _addvals_h #define _addvals_h #ifdef __cplusplus extern "C" { #endif

#ifndef NELEM #define NELEM(x) ((int) (sizeof(x) / sizeof((x)[0]))) #endif

void displayToast(JNIEnv* env, jobject thiz, jobject charseq);

#ifdef __cplusplus }

#endif #endif

jnitoast.c

#include <stdio.h> #include <android/log.h> #include "jnitoast.h"

#define EXPORT __attribute__((visibility("default"))) #define LOG_TAG ("jnitoast")

#define LOGD(...)((void)__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__))

Page 63: 안드로이드 NDK 네이티브 프로그래밍

2장 ┃ 자바와 안드로이드 NDK

51

#define LOGE(...)((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__))

void displayToast(JNIEnv* env, jobject thiz, jobject charseq) { // Java: Toast toastobj = Toast.makeText(context, charseq, 0); ———————————————

// Toast 클래스를 가져온다. jclass toast = (*env)->FindClass(env, "android/widget/Toast"); // Toast 클래스의 makeText 정적 메서드의 메서드 ID를 가져온다. jmethodID methodMakeText = (*env)->GetStaticMethodID(env, toast, "makeText", "(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;"); if(methodMakeText == NULL){ LOGE("toast.makeText not Found"); return; } —— // Toast.makeText(this, charseq, 0)를 실행한다. jobject toastobj = (*env)->CallStaticObjectMethod(env, toast, methodMakeText, thiz, charseq, 0);

// toast.show를 실행한다. // Java: toastobj.show(); jmethodID methodShow = (*env)->GetMethodID(env, toast, "show", "()V"); (*env)->CallVoidMethod(env, toastobj, methodShow); ———————————————————————————

return; }

int jniRegisterNativeMethods(JNIEnv* env, const char* className, const JNINativeMethod* gMethods, int numMethods) { jclass clazz;

LOGD("Registering %s natives\n", className); clazz = (*env)->FindClass(env, className); if (clazz == NULL) { LOGD("Native registration unable to find class '%s'\n", className); return -1; } if ((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0) { LOGD("RegisterNatives failed for '%s'\n", className); return -1; } return 0; }

static JNINativeMethod sMethods[] = { /* name, signature, funcPtr */ {"displayToast", "(Ljava/lang/CharSequence;)V", (void*)displayToast}, };

Page 64: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

52

EXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { JNIEnv* env = NULL; jint result = -1;

if ((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_6) != JNI_OK) { return result; }

jniRegisterNativeMethods(env, "com/example/jnitoast/MainActivity", sMethods, NELEM(sMethods)); return JNI_VERSION_1_6; }

여기서는 앞서 소개한 jniRegisterNativeMethods 함수를 이용해 메서드를 등록합니다.

MainActivity.java에서 호출되는 displayToast 함수에서는 자바의 리플렉션을 호출해 Toast를

표시하고 있습니다.

코드 중간의 에 해당하는 네이티브 측에서 작성한 displayToast 메서드를 자바로 작성하면

다음과 같습니다.

void displayToast(CharSequence charseq){ Toast toastobj = Toast.makeText(this, charseq, 0); toastobj.show(); }

빌드에 필요한 Android.mk는 다음과 같습니다.

Android.mk

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS) LOCAL_MODULE := jnitoast LOCAL_SRC_FILES := jnitoast.c LOCAL_LDLIBS := -llog

include $(BUILD_SHARED_LIBRARY)

jnitoast.c를 빌드해 libjnitoast.so 파일을 모듈로 출력합니다. 또 logcat을 이용해 로그를 출력

하고 있으므로 로그 라이브러리를 링크하고 있습니다.

Page 65: 안드로이드 NDK 네이티브 프로그래밍

2장 ┃ 자바와 안드로이드 NDK

53

02.06 정리

이 장에서 설명한 대로 JNI를 이용하면 자바에서 C/C++의 함수를 호출하거나, 반대로 C/C++

에서 자바의 클래스 및 메서드를 정의한 후 호출할 수 있습니다. 또 과거의 C/C++ 코드를 이용

하는 방법도 이해했으리라 생각합니다. JNI를 이용하면 이제까지 사용하던 C/C++의 라이브러

리 자산이나 인터넷상의 다양한 라이브러리를 이용할 수 있습니다.

칼럼

안드로이드의 소스코드

안드로이드의 거의 모든 소스코드는 오픈소스로 공개돼 있습니다. 안드로이드의 소스코드를

내려받는 방법은 몇 가지가 있지만 가장 간편한 방법은 버전 관리 시스템인 git를 이용해 깃

헙(github)에서 OESF(Open Embedded Software Foundation)가 공개하고 있는 안드로

이드 2.3의 소스코드를 내려받는 방법입니다. 이 경우 다음과 같은 방법으로 소스코드를 내

려받습니다.

% git clone https://github.com/OESF/OHA-Android-2.3_r1.0.git

이 책에서는 안드로이드의 소스코드에서 JNI의 네이티브 메서드를 등록하는 방법으로

JNIHelp.c를 소개했습니다. 하지만 그 밖에도 여러 가지 유용한 코드가 많이 공개돼 있고 안

드로이드 애플리케이션을 만드는 데 여러모로 참고할 만한 부분이 많으니 한번쯤 둘러보면

좋을 것입니다.

Page 66: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

54

Page 67: 안드로이드 NDK 네이티브 프로그래밍

Chapter

NativeActivity

안드로이드 애플리케이션을 만들 때 반드시 이용되는 클래스 중 하나가 바로

Activity 클래스입니다. 이 Activity 클래스를 이용해 애플리케이션의 화면

표시 및 터치패널에서 입력을 하거나 메뉴를 표시하는 등 애플리케이션의

기본이 되는 동작을 제어합니다. 이러한 Activity를 여러 개 만들어 서로

호출하게 하는 것이 안드로이드 애플리케이션의 구성 원리입니다.

Android NDKr5부터는 이처럼 Activity에서 이뤄지는 이벤트를 C/C++

코드로도 구현할 수 있습니다.

03

Page 68: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

56

03.01 NativeActivity란?

안드로이드 NDK에서 API 레벨 9(안드로이드 2.3)를 설치하면 C/C++로도 Activity를 구현할

수 있는 NativeActivity가 지원됩니다. 이번에는 이 NativeActivity에 대해 설명하겠습니다.

애플리케이션

Activity 목록화면

Activity 상세화면

Activity 설정화면

[그림 03-01] 애플리케이션과 Activity의 관계

간단히 말하면 “C/C++에서도 Activity를 만들 수 있게 됐다”라는 한마디로 정의할 수 있습

니다. 이 NativeActivity가 지원되기 이전(API 레벨 9 이전)에는 Activity를 만들기 위해 애플

리케이션 프레임워크의 Activity 클래스를 이용할 수밖에 없었습니다. 이것이 바로 안드로이드

NDK를 메인으로 사용하더라도 자바 코드를 포함하지 않으면 안 되는 원인 중 하나였습니다.

하지만 안드로이드 NDKr5로 업그레이드되면서 API 레벨 9부터는 NativeActivity를 이용할

수 있게 됐으며, 그 덕분에 C/C++만으로도 안드로이드 애플리케이션을 만들 수 있게 됐습니다.

애플리케이션 프레임워크의 Activity와 NativeActivity의 차이점을 자바나 C/C++와 같

은 언어 이 외의 부분에서 찾아본다면 애플리케이션 프레임워크의 Activity 클래스보다

NativeActivity가 배터리 소모가 적다는 점 등 시스템상에서 얻을 수 있는 장점이 많아졌습니

다(자세한 내용은 뒤에서 설명하겠습니다).

03.02 NativeActivity와 게임

NativeActivity를 이용하면 C/C++만으로도 안드로이드 애플리케이션을 만들 수 있습니다. 그

러나 C/C++만으로 유틸리티 계열의 애플리케이션을 만든다는 것은 자바로 만드는 것 이상으로

어렵습니다. 그 이유는 Button이나 TextBox처럼 애플리케이션 프레임워크에 준비돼 있는 위젯

을 표시하기 위해서는 JNI의 리플렉션을 이용해야 하기 때문입니다(코드 분량이 늘어나고, 가

독성도 떨어집니다).

Page 69: 안드로이드 NDK 네이티브 프로그래밍

3장 ┃ NativeActivity

57

그럼 NativeActivity는 어떤 애플리케이션의 개발에 적합할까요? 바로 게임과 같이 안드로이

드 표준 위젯을 많이 사용하지 않는 애플리케이션에 적합합니다. 여기서는 특히 게임 애플리케

이션에 초점을 맞춰 설명하고 있지만 NativeActivity의 장점으로는 아래와 같은 것이 있습니다.

≑ GC의 의존도를 줄일 수 있다.

≑ 자바 코드가 줄어든다.

그럼 이러한 장점에 대해 순서대로 설명하겠습니다.

03.02.01 GC의 의존도를 줄일 수 있다.

안드로이드 2.2까지는 게임 애플리케이션을 만들 때 자바로만 만들거나 자바와 C/C++를 병용

해서 만들었습니다. 이때 문제가 되기 쉬운 것 중 하나가 달빅 VM의 가비지 컬렉션(Garbage

Collection, 이하 GC)에 의한 애플리케이션의 일시 정지 현상입니다. 자바(달빅 VM)를 이용하

는 이점 중 하나가 메모리 해제를 명시적으로 하지 않아도 된다는 점입니다. 이 부분에 대해서는

2장에서 설명했습니다. 자바(달빅 VM)에서는 어느 영역의 메모리가 참조됐는지, 아니면 이미

참조가 끝났는지 등 메모리 이용 상황은 전적으로 달빅 VM에서 관리합니다. 그리고 참조하지

않는 메모리는 달빅 VM의 GC 기능에 의해 자동적으로 해제되고, 그 결과 자바에서 이용할 수

있는 메모리 영역이 늘어나게 됩니다.

변수A

참조 중인 영역

참조가 끝난 영역

데이터C

변수D

데이터E

미사용 영역

데이터B

GC 실행 전

변수A

데이터C

데이터E

미사용 영역

GC 실행 후

[그림 03-02] GC

단, GC가 실행되고 있는 동안(아주 잠깐이지만)에는 화면을 포함해서 모든 동작이 멈추는 경

우도 있습니다(안드로이드 2.3 이후부터 개선됐습니다).

Page 70: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

58

GC는 일반적인 유틸리티 계열 애플리케이션이 동작할 때는 크게 문제가 되지 않습니다. 하지

만 게임에서는 이 같은 잠깐 동안의 정지 현상이 문제가 되기도 합니다. 예를 들면, ‘슈퍼마리오

브라더스’ 같은 액션 게임의 경우, 캐릭터가 자유자재로 움직여야 게임을 하는 재미가 있을 것입

니다. 하지만 게임을 하는 도중에 GC가 실행되면 0.02초 정도 게임이 정지하게 되는데, 게임을

하는 입장에서 보면 이것조차도 큰 방해가 될 수 있습니다. 이래서는 게임이 재미있을 리가 없습

니다.

그리고 개발자가 임의로 GC를 실행할 수만 있다면 더할 나위가 없겠지만 GC를 실행하는 타

이밍은 애플리케이션의 상태에 상관없이 전적으로 시스템(달빅 VM)에 의존합니다.1

이런 문제가 있기 때문에 안드로이드용 게임 애플리케이션 개발자는 게임 중에 GC가 실행되

지 않게 하기 위해 여러 가지 방책을 모색해 왔습니다. 하지만 이런 고민은 재미있는 게임을 만

들어야 하는 개발자 입장에서 보면 근본적으로 불필요한 과정일지도 모릅니다. 이런 기술적인

문제를 해결할 시간을 게임 개발에 투자해야 하는 것이 맞겠지요.

GC의 영향이 적다.

GC의 영향이 크다.

타이틀스테이지선택

GC의 실행으로 인한 영향이 적다

GC의 실행으로 인한 영향이 크다

여기서 한꺼번에GC를 실행시키고 싶지만GC의 실행은 시스템이알아서 하기 때문에

개발자가 실행할 수 없다.

스테이지변경

게임 중 게임 중

시간의 흐름

[그림 03-03] GC의 실행 타이밍

안드로이드 NDKr5 이후 버전에서는 API 레벨 9부터 NativeActivity가 지원됩니다. 그 덕분

에 자바 코드를 전혀 사용하지 않고도 애플리케이션을 개발할 수 있게 됐고, GC의 발생을 걱정

하지 않고 개발할 수 있게 됐습니다.

1  자바 코드에서 System.gc() 메서드를 사용해 GC를 실행할 수도 있지만 실행과 동시에 GC가 실행된다는 보장은 없습니다.

Page 71: 안드로이드 NDK 네이티브 프로그래밍

3장 ┃ NativeActivity

59

03.02.02 자바 코드가 줄어든다.

안드로이드를 탑재한 최근의 스마트폰은 해마다 하드웨어의 성능이 향상되어 과거의 플레이스

테이션 수준의 게임도 동작할 수 있게 됐습니다.

과거의 컨슈머용 애플리케이션2을 안드로이드에 이식한다고 생각해봅시다.

안드로이드 NDKr5가 등장하기 이전의 환경에서는 자바로만 만들어야 하는 부분이 있었기

때문에 자바와 안드로이드 NDK 기반의 C/C++ 코드까지 두 가지 언어를 사용한 소스코드를

작성해야 했습니다. 그러나 NativeActivity의 지원 덕분에 자바 코드 한 줄 없이도 개발이 가능

해졌습니다. 또 이제까지 컨슈머용 게임을 만들어 오던 프로그래머의 입장에서 보면 익숙하지

않은 자바를 사용하지 않고도 지금까지 다뤄왔던 익숙한 C/C++만으로 안드로이드 애플리케이

션을 만들 수 있기 때문에 정신적인 부담도 적어졌습니다.

이렇듯 안드로이드 NDKr5 덕분에 안드로이드에서도 한층 더 다양한 게임을 즐길 수 있게 됐

습니다.

03.03 이벤트

NativeActivity는 Activity의 생성이나 사용자 입력 등 운영체제에서 전달되는 이벤트를 이용합

니다. 여기서는 NativeActivity를 이용할 때 가장 중요한 사항들을 설명하겠습니다.

앞서 설명했듯이 NatvieActivty는 애플리케이션 프레임워크의 Activity 클래스와 비교해보면

콜백 처리에 의해 좀 더 많은 이벤트를 처리할 수 있으며, 이러한 이벤트는 지금까지의 Activity

클래스와 동일하게 동작합니다. NativeActivity의 이벤트 처리 방법에 대해서는 뒤에서 예제 코

드를 이용해 설명하겠습니다.

2  (옮긴이) 컨슈머용 애플리케이션이란 가정용 또는 개인용으로 만들어진 게임기나 게임 소프트웨어(TV 게임, 휴대용 게임)를 이용한 컴퓨터 게임(가정용 게임)을 말합니다. 게임 센터 등에서 접할 수 있는 아케이드 게임과 반대 개념으로 이해하면 됩니다.

Page 72: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

60

[표 03-01] NativeActivity에서 처리 가능한 이벤트 목록

NativeActivity(C/C++) 개요

onStart Activity.onStart()와 동일

onResume Activity.onResume()와 동일

onSaveInstanceState Activity.onSaveInstanceState()와 동일

onPause Activity.onPause()와 동일

onStop Activity.onStop()와 동일

onDestroy Activity.onDestroy()와 동일

onWindowFocusChanged Window의 포커스가 바뀌었다.

onNativeWindowCreated NativeWindow가 생성됐다.

onNativeWindowResized NativeWindow의 크기가 바뀌었다.

onNativeWindowRedrawNeeded NativeWindows 다시 그리기를 요구한다.

onNativeWindowDestroyed NativeWindow가 삭제됐다.

onInputQueueCreated InputQueue가 생성됐다.

onInputQueueDestroyed InputQueue가 삭제됐다.

onContentRectChanged (소프트 키보드에서)그리기 영역이 바뀌었다.

onConfigurationChanged 시스템 설정이 바뀌었다.

onLowMemory 시스템의 메모리가 부족하다.

NativeActivity에서 이벤트 처리를 할 때 한 가지 주의해야 할 점이 있습니다. NativeActivity

에서 이벤트 처리를 할 때는 다른 코드의 진행을 기다리지 않고 종료해야(논블록 처리) 한다는

점입니다.

03.04 NativeActivity의 제약

NativeActivity는 애플리케이션 프레임워크의 Activity 클래스와 동일한 역할을 합니다. 하지만

네이티브 코드에 직접 접근하기 때문에 코드에는 많은 차이가 있습니다. 아울러 그렇기 때문에

여러 가지 제약도 있습니다. NativeActivity를 이용하려면 이러한 제약을 충분히 이해하고 코드

를 작성해야 합니다. 그럼 어떠한 제약이 있는지 알아보겠습니다.

1. Callback은 모두 논블록 함수로 만들어야 한다.

2. android_main() 함수에서 처리를 시작하도록 만들어야 한다.

3. 입력 이벤트는 Activity에 전달하는 AInputQueue를 이용한다.

Page 73: 안드로이드 NDK 네이티브 프로그래밍

3장 ┃ NativeActivity

61

03.05 논블록에 대해

NativeActivity의 시스템에서 이뤄지는 콜백(onStart 등) 중인 처리는 모두 논블록 방식으로 코

드를 작성해야 합니다. 여기서 말하는 논블록 방식이란 “처리가 완료되는 것을 기다리지 않고,

프로그램을 계속해서 진행하는” 것을 의미합니다.

이 논블록 방식의 반대 의미로 “처리가 완료할 때까지 기다리는” 블록 방식이 있습니다. 여기

서는 NativeActivity로부터 멀리 떨어진 클라이언트와 서버 간의 데이터 통신을 예로 들어 논블

록 방식과 블록 방식을 비교해서 설명하겠습니다. 인터넷에 접속돼 있는 PC가 서버로부터 특정

데이터를 가져올 경우, 다음과 같은 절차를 거칩니다.

1. 클라이언트에서 서버로 요청 메시지를 보낸다.

2. 서버가 요청 메시지를 이해한 후 요청한 데이터를 보낸다.

3. 클라이언트는 서버로부터 보내온 데이터를 수신하고 저장한다.

이때 클라이언트가 서버로부터 보내온 데이터를 수신 완료한 타이밍을 파악하는 데는 두 가

지 방법이 있습니다.

1. 데이터의 수신 완료 여부를 확인한다(블록 방식).

2. 데이터를 수신한 후 콜백 함수를 호출한다(논블록 방식).

그럼, 이 두 처리 방식에 대해 자세하게 알아보겠습니다.

03.05.01 데이터의 수신 완료 여부를 체크한다(블록 방식)

클라이언트가 서버에서 “데이터를 수신 완료했는지 계속해서 체크”하는 방법을 생각해봅시다.

클라이언트는 데이터가 수신 완료됐는지 항상 확인합니다. 이렇게 확인을 계속하는 동안에는

클라이언트는 다른 어떤 작업도 처리할 수 없습니다.

Page 74: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

62

처리 진행 중

처리 정지 중

서버로부터 응답이오기를 기다린다

요청에 대한 처리

클라이언트

요청

응답

서버

다른 어떤 처리도진행할 수 없다

[그림 03-04] 응답이 올 때까지 기다린다.

코드로 작성하면 다음과 같습니다.

void *imageData sendRequest(GET_IMAGE) ——————————————————————————

while(response){ response = receiveData(GET_IMAGE, imageData) ———————————————

}

이 예제 코드에서는 서버에 이미지 데이터를 요청하는 메시지를 보낸 후, 서버로부터 보

낸 데이터를 수신 완료할 때까지 계속해서 확인하고 있으며, 수신 완료 후에 다른 작업을 처리합

니다.

블록 방식으로 코드를 작성하면 코드는 단순해질 수 있습니다. 하지만 결점도 있습니다. “처리

를 계속할지 말지를 외적 요인(서버 및 네트워크)에 의존한다”라는 점입니다. 이 예제 코드에서

는 의 while에서 서버로부터의 응답을 기다리고 있습니다. 즉, 서버로부터 응답이 없으면 그다

음 작업을 진행할 수 없습니다.3

3  원래는 서버로부터 응답이 없으면 타임아웃 처리를 하게 돼 있지만 여기서는 이 부분은 생략합니다.

Page 75: 안드로이드 NDK 네이티브 프로그래밍

3장 ┃ NativeActivity

63

03.05.02 콜백 함수 이용하기(논블록 방식)

알람 시계는 미리 설정해 놓은 시간에 소리가 울리게 해서 잠을 깨우는 역할을 합니다. 알람 시

계 덕분에 기상 시간이 빠듯할 때까지 잠을 잘 수가 있는 것이죠.

콜백 함수는 이 알람 시계와 비슷한 역할을 합니다. 어떤 처리가 완료되면 사전에 설정해 놓

은 함수(콜백 함수)가 호출되는 방식입니다.

이 예를 좀 전의 클라이언트와 서버 간의 데이터 통신에 대입해서 생각해 봅시다.

1. 데이터가 수신 완료된 후 호출될 함수를 미리 등록해 둔다.

2. 서버에 요청 메시지를 보낸다.

3. 수신을 완료하고 나면 등록해 둔 함수가 호출된다.

콜백 함수를 이용하면 클라이언트가 서버를 계속해서 체크할 필요가 없습니다. 서버에 요청

메시지를 보낸 후에도 마음 놓고 다른 처리를 할 수가 있습니다. 그리고 서버로부터 데이터를 수

신한 후에는 콜백 함수 안에 정의해 놓은 대로 계속해서 처리를 진행할 수 있습니다. 이처럼 코

드 안에서 다른 코드의 진행을 방해하지 않는 함수를 논블록 함수라고 합니다.

처리 진행중 다른 처리 진행중

처리 정지중

다른 처리가진행되고 있다

요청에 대한 처리

클라이언트

함수A

콜백함수

요청

응답

서버

처리를 계속한다

[그림 03-05] 콜백 함수로 처리한다.

Page 76: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

64

func(){ setCallback(onReceiveData) ——————————————————————————

... sendRequest(GET_IMAGE) ——————————————————————————

}

onReceiveData(int param, void *data){ —————————————— switch(param){ case GET_IMAGE: ... —— break } } ———————————————————————————————————————————————

이 방식에서는 “요청을 처리하는 부분(func)”과 “수신한 데이터를 처리하는 부분(onReceiveData)”

으로 나뉩니다.

예제 코드에서는 데이터를 수신한 후에 호출되는 함수(콜백 함수)를 정의하는 setCallback

함수가 시스템에 준비돼 있는 것으로 가정합니다. 이 코드에서는 데이터를 수신한 후에 호출

될 함수를 정의하고, 서버에 이미지 전송을 요청합니다. 그리고 데이터를 수신한 후 에서 등

록한 함수(위의 코드에서는 의 함수)를 호출합니다.

앞서 설명한 프로그램과 다른 점은 클라이언트는 서버에 요청을 보낸 후 프로그램를 완료하고

있으며, 서버로부터의 응답은 코드에 포함돼 있지 않다는 것입니다. 그리고 클라이언트가 서버로

부터 데이터를 수신했을 때 setCallback 함수에서 정의한 onReceiveData 함수가 실행됩니다.

03.05.03 ANR

이처럼 NativeActivity의 이벤트 안에 블록 처리를 포함해서는 안 됩니다. 그럼 만일 Native

Activity에서 이벤트를 실행하는 도중에 블록 처리가 포함돼 있을 경우에는 어떻게 될까요?

그때는 블록 처리에 의해 메인 스레드가 멈추게 되고, 메인 스레드가 멈추고 나서 얼마 후에

ANR(Android Not Responding) 대화상자가 표시됩니다.

Page 77: 안드로이드 NDK 네이티브 프로그래밍

3장 ┃ NativeActivity

65

[그림 03-06] ANR(Android Not Responding) 다이얼로그

이 ANR(Android Not Responding) 대화상자는 메인 스레드(UI스레드)를 처리하는 도중에 어

떤 원인으로 일정 시간 동안 애플리케이션이 반응하지 않을 때 이를 시스템이 감지하고 표시하

는 대화상자입니다.

ANR은 시스템이 애플리케이션의 상태를 감지해서 표시합니다. 이 대화상자에서는 사용자가

애플리케이션의 진행 여부를 선택할 수 있습니다. 또 ANR은 애플리케이션의 에러를 나타내는

대화상자의 하나이기도 합니다.

03.06 NativeActivityGlue

안드로이드 NDKr5에는 NativeActivity를 이용한 예제 코드가 있습니다. 이 예제 코드는

NativeActivity만으로 만들어져 있지 않고, NativeActivityGlue(NativeActivity+Glue(접착제

라는 뜻))라는 래퍼 라이브러리를 이용해 NativeActivity를 컨트롤합니다.

NativeActivity를 직접 이용하지 않고 NativeActivityGlue를 이용하면 NativeActivity의 처

리, 특히 이벤트 관련 처리가 용이해집니다.

Page 78: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

66

이 NativeActivityGlue는 NativeActivity를 감싸고 있는 라이브러리(래퍼 라이브러리)라고

생각하면 됩니다. 그렇기 때문에 NativeActivity보다 사용하긴 쉽지만 NativeActivity의 제약을

모두 해결해 주는 것은 아니기 때문에 NativeActivity의 제약에 대해서는 주의를 해야 합니다.

이 NativeActivityGlue가 제공되는 이유가 안드로이드 공식 문서에는 명확히 설명돼 있지 않

지만 NativeActivityGlue의 소스코드(안드로이드 NDKr5의 /sources/android/native_app_

glue 디렉터리에 있습니다)를 읽어보면 다음과 같이 추측할 수 있습니다.

NativeActivity를 이용하려면 애플리케이션 프레임워크의 Activity 클래스에서는 은폐돼 있

는 여러 가지 이벤트도 개발자가 직접 제어해야 합니다. 즉, NativeActivity를 바르게 사용하려

면 애플리케이션 프레임워크의 Activity 클래스를 제어하는 방법을 반드시 숙지해야 합니다.

그러나 NativeActvity를 사용하는 모든 개발자가 애플리케이션 프레임워크의 Activity 클

래스에 대해 모두 파악하고 있는 것은 아닙니다. 그래서 Activity 클래스를 잘 모르더라도

NativeActivity를 이용하기 쉽도록 제공된 것이 NativeActivityGlue입니다.

이처럼 NativeActivity를 이용해 애플리케이션을 만들 때 실질적으로는 NativeActivityGlue

를 이용하게 됩니다. 이 책에서도 NativeActivity를 이용하는 애플리케이션에서는 Native

ActivityGlue를 이용해서 설명하겠습니다.

03.06.01 android_main 함수

먼저 MainActivityGlue를 이용했을 때 NativeActivity의 흐름을 설명하겠습니다.

MainActivityGlue에서는 android_main 함수가 가장 먼저 호출됩니다(C 언어의 main 함수

와 같습니다). 이 함수의 내용은 반복 처리를 하기 전과 후로 나눌 수 있습니다.

Page 79: 안드로이드 NDK 네이티브 프로그래밍

3장 ┃ NativeActivity

67

void android_main(){ // 매개변수를 초기화한다. // 센서, 터치패널의 입력을 초기화한다. while(1){ // 센서, 터치패널로부터 입력을 받는다. // 게임 처리

// 그리기 처리

if (finish == TRUE){ // 게임 종료 처리

return; } } }

이처럼 android_main 안에서 반복 처리를 하기 전에 애플리케이션을 초기화합니다. 여기서

말하는 초기화는 게임을 시작해 한 번만 처리되는 작업을 말합니다. 예를 들면, 그림을 그리는

데 필요한 화면의 초기화나 센서 및 터치패널의 초기화를 말합니다.

그리고 while에 의한 반복 처리에서는 애플리케이션 안에서 반복해서 이용되는 처리나, 각 프

레임마다 필요한 처리를 합니다. 즉, 게임에 필요한 데이터의 로딩, 센서 및 터치패널로부터 입력

받기, 게임 본체의 처리, 화면 그리기 등을 말합니다. 그리고 게임을 중단할 경우에는 애플리케

이션의 종료 처리를 한 후, 이 루프를 빠져나옵니다.

03.06.02 초기화 처리

초기화 처리에서는 시스템으로부터 전달받은 이벤트를 처리하는 함수를 설정하거나, 애플리케

이션에 필요한 초기화 작업을 합니다. 먼저, 시스템 설정이나 키 입력 및 이벤트 관련 설정을 합

니다.

Page 80: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

68

void android_main(struct android_app* state) { struct engine engine;

// glue가 삭제되지 않도록

app_dummy();

memset(&engine, 0, sizeof(engine)); state->userData = &engine; —————————————————————————————————————————————————

state->onAppCmd = engine_handle_cmd; ————————————————————————————————————

state->onInputEvent = engine_handle_input; ————————————————————————————————

engine.app = state; // 센서 감시 준비

engine.sensorManager = ASensorManager_getInstance(); ———————————————— engine.accelerometerSensor = ASensorManager_getDefaultSensor(engine.sensorManager, ASENSOR_TYPE_ACCELEROMETER); —————

engine.sensorEventQueue = ASensorManager_createEventQueue(engine.sensorManager, state->looper, LOOPER_ID_USER, NULL, NULL); ——————

if (state->savedState != NULL) { —————————————————————————————————————— // We are starting with a previous saved state; restore from it. —— engine.state = *(struct saved_state*)state->savedState; ————————————————— }

android_main 함수는 NativeActivityGlue에서 호출되며, 함수가 호출될 때 android_

app 구조체를 인수로 전달받습니다. 이 인수에는 NativeActiv ity에서 계승된 정보와

NativeActivityGlue의 라이브러리에서 구한 하드웨어 정보(화면 해상도 등)가 들어 있습니다.

android_main 함수는 가장 먼저 state 변수를 초기화합니다.

에 대해

userData 멤버에는 애플리케이션이 포커스를 잃었을 때(애플리케이션이 활성화 상태가 아닐

때), 일시적으로 저장할 정보(변수)를 지정합니다. 그리고 애플리케이션이 포커스를 잃었을 때,

그 정보를 일시적으로 옮겨 놓습니다(별도로 코드를 작성할 필요는 없습니다). 그리고 다시 포

커스를 찾았을 때(애플리케이션이 활성화 상태가 됐을 때) 조금 전에 변수에 옮겨 둔 정보가 다

시 재설정됩니다. 이 userData에 저장돼 있는 정보를 이용하면 포커스를 잃어버린 직후의 상태

에서 다시 애플리케이션을 재개할 수 있습니다.

Page 81: 안드로이드 NDK 네이티브 프로그래밍

3장 ┃ NativeActivity

69

에 대해

onAppCmd 멤버에는 시스템(Android)으로부터 전달되는 메시지를 처리하는 함수의 포인터

를 지정합니다. 이 예제에서는 engine_handle_cmd 함수가 지정돼 있고, 이 함수 안에서는 아

래와 같은 이벤트 처리가 이뤄집니다.

static void engine_handle_cmd(struct android_app* app, int32_t cmd) { struct engine* engine = (struct engine*)app->userData; switch (cmd) { case APP_CMD_SAVE_STATE: // 시스템의 현재 상태를 저장하도록 요청한다. ... break; case APP_CMD_INIT_WINDOW: // 윈도우 표시 직후의 상태를 초기화한다. ... engine_init_display(); // 표시를 초기화한다. ... break; case APP_CMD_TERM_WINDOW: // 윈도우를 감춘다. ... break; case ... } }

이 engine_handle_cmd 함수(onAppCmd에 지정하는 함수)에서는 인수로 android_app 구

조체의 변수(시스템의 상태가 저장돼 있음)와 cmd 변수(시스템으로부터 수신한 이벤트의 종류

가 저장돼 있음)를 설정합니다. cmd 변수를 통해 확인할 수 있는 이벤트는 다음과 같습니다.

[표 03-02] cmd 변수가 가지고 있는 이벤트

이벤트 의미

APP_CMD_INPUT_CHANGED AInputQueue에 변경사항이 생김

APP_CMD_INIT_WINDOW ANativeWindow를 이용할 수 있게 됨

APP_CMD_TERM_WINDOW ANativeWindow가 종료를 요청

APP_CMD_WINDOW_RESIZED ANativeWindow가 사이즈를 요청

APP_CMD_WINDOW_ REDRAW_NEEDED ANativeWindows가 다시 그리기를 요청

APP_CMD_CONTENT_RECT_ CHANGED Window 내부의 표시영역이 변경됨. 예를 들면, 소

프트웨어의 키보드가 표시된 경우에 발생한다.

APP_CMD_GAINED_FOCUS Activity가 활성화 상태가 됨(포커스를 얻음)

APP_CMD_LOST_FOCUS Activity가 비활성화 상태가 됨(포커스를 잃음)

APP_CMD_CONFIG_CHANGED 기기의 설정이 변경됨

Page 82: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

70

이벤트 의미

APP_CMD_LOW_MEMORY 시스템의 메모리가 부족

APP_CMD_START Activity가 시작됨(onStart)

APP_CMD_RESUME Activity가 재개됨(onResume)

APP_CMD_SAVE_STATE 상태 저장이 시작됨

APP_CMD_PAUSE Activity가 일시 정지됨(onPause)

APP_CMD_STOP Activity가 정지됨(onStop)

APP_CMD_DESTROY Activity가 삭제됨(onDestroy)

APP_CMD_INIT_WINDOW를 수신한 후 화면을 초기화합니다.

에 대해

키보드, 터치패널, 센서 등으로부터 입력이 있을 경우, 이를 처리하는 함수 포인터를 지정합니

다. 이 예제에서는 engine_handle_input 함수를 지정하고 있으며, 이 함수 안에서는 다음과 같

은 처리를 합니다.

static int32_t engine_handle_input(struct android_app* app, AInputEvent* event) { struct engine* engine = (struct engine*)app->userData; if (AInputEvent_getType(event) == AINPUT_EVENT_TYPE_MOTION) { engine->animating = 1; engine->state.x = AMotionEvent_getX(event, 0); ————————————————————

—— engine->state.y = AMotionEvent_getY(event, 0); ———————————————————— return 1; } return 0; }

터치된 포인터(X좌표와 Y좌표)를 구합니다(터치패널의 입력 디바이스로부터 구한 정보에 대해서는 6장에서 자세하게 설명하겠습니다).

engine_handle_input 함수(onInputEvent에 지정한 함수)에는 2개의 인수를 지정합니다.

≑ android_app 구조체의 변수(시스템 상태가 저장돼 있다.)

≑ AInputEvent 구조체의 변수(시스템으로부터 수신한 입력 이벤트에 관한 정보가 저장돼 있다.)

Page 83: 안드로이드 NDK 네이티브 프로그래밍

3장 ┃ NativeActivity

71

engine_handle_input 함수에서는 AInputEvent_getType(event) 함수로부터 발생한 이벤트

의 종류를 가져옵니다. 여기서는 다음과 같은 이벤트가 발생합니다.

[표 03-03] AInputEvent_getType(event) 함수에서 발생한 이벤트의 종류

이벤트 종류 의미

AINPUT_EVENT_TYPE_KEY 키 입력 이벤트

AINPUT_EVENT_TYPE_MOTION 터치패널 이벤트

에 대해

센서로부터 정보를 가져오기 위해 SensorManager의 설정과 센서값을 구하는 데 필요한 이벤트

를 설정합니다. 여기서는 가속 센서(ASENSOR_TYPE_ACCELEROMETER)를 설정합니다.

에 대해

Activity가 포커스를 잃기 직전에 저장한 정보가 있을 경우 여기서 복원합니다. “(1)에 대해”에서

설명했듯이 포커스를 잃기 직전에 저장한 데이터를 복원하면 포커스를 잃기 직전의 상태에서부

터 애플리케이션을 다시 이용할 수 있습니다.

03.06.03 루프 처리

초기화가 끝나면 루프 안에서 애플리케이션의 다음 처리를 계속합니다. 이 루프는 애플리케이

션이 종료하면(중단하지 않고) 반복 처리도 함께 종료합니다. 루프 안에서는 애플리케이션에 필

요한 일반적인 처리 및 키 입력 처리, 화면 그리기 처리 등이 이뤄집니다.

계속해서 예제 코드를 이용해서 설명하겠습니다.

while(1) { // 저장돼 있는 모든 이벤트를 읽는다(Read all pending events). int ident; int events; struct android_poll_source* source;

while ((ident = ALooper_pollAll(engine.animating ? 0 : -1, NULL, &events, ————— (void**)&source)) >= 0) {

Page 84: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

72

// 이벤트를 처리

if (source != NULL) { source->process(state, source); }

if (ident == LOOPER_ID_USER) { ———————————————————————————————————————————— if (engine.accelerometerSensor != NULL) { ASensorEvent event; while (ASensorEventQueue_getEvents(engine.sensorEventQueue, &event, 1) > 0) { LOGI("accelerometer: x=%f y=%f z=%f ", event.acceleration.x, ——

event.acceleration.y, event.acceleration.z); } } } —————————————————————————————————————————————————————————————————————————

// 애플리케이션을 종료한다. if (state->destroyRequested != 0) { ——————————————————————————————————————————

engine_term_display(&engine); return; } }

if (engine.animating) { engine.state.angle += .01f; ——————————————————————————————————————————————— if (engine.state.angle > 1) { engine.state.angle = 0; ——

} engine_draw_frame(&engine); ——————————————————————————————————————————————— } }

ALooper_pollAll 함수에서 입력(센서, 키보드, 터치패널) 이벤트를 가져옵니다. 이벤트가 발생하지 않을 때는 이 while 안의 처리 내용은 실행되지 않습니다. Animating의 내용에 따라 가져온 모든 이벤트를 처리할 것인지(animating이 1인 경우), 아니면 다른 어떤 이벤트가 발생할 때까지 기다릴 것인지(animating이 0인 경우)가 결정됩니다. ALooper_pollAll 함수의 인수에 대한 설명은 android/looper.h 파일 안에 있습니다.

수신한 센서 이벤트를 처리합니다. LOOPER_ID_USER에는 초기화 당시에 키보드 및 센서로부터 입력이 수신됐을 때 LOOPER_ID를 구해 대입해 두었습니다. ASensorEventQueue_getEvents 함수에서는 수신한 이벤트의 종류를 판별합니다. 여기서는 가속도 센서의 값을 가져온 경우, 그 값을 표시합니다. 이 부분에 대한 자세한 설명은 6장에서 하겠습니다.

애플리케이션 종료 요청이 오면 애플리케이션을 종료합니다. engine_term_display 함수에서 종료 처리를 한 후, 루프를 빠져 나옵니다.

애플리케이션의 주된 작업을 처리합니다. 일반적인 게임의 주요 로직 및 그리기(drawing)가 여기에 해당합니다. 이 예제 코드에서는 표시할 사각형의 각도의 연산과 그리기를 하고 있습니다.

Page 85: 안드로이드 NDK 네이티브 프로그래밍

3장 ┃ NativeActivity

73

03.06.04 AndroidManifest.xml 정의

애플리케이션 프레임워크의 Activity를 설정하는 것과 마찬가지로 NativeActivity를 이용하는

경우에도 AndroidManifest.xml에서 Activity를 정의해야 합니다.

Intent-�lter의 설정은 동일하지만 NativeActivity를 이용할 경우에는 지금까지 Activity 클래

스를 이용할 때와는 다른 설정이 필요합니다.

NativeActivity를 이용한 AndroidManifest.xml의 예를 보시겠습니다.

AndroidManifest.xml의 예

<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.native_activity" android:versionCode="1" android:versionName="1.0">

<uses-sdk android:minSdkVersion="8" />

<application android:label="@string/app_name" android:hasCode="false">

<activity android:name="android.app.NativeActivity" ———————————————

android:label="@string/app_name" android:configChanges="orientation|keyboardHidden">

<meta-data android:name="android.app.lib_name" ——————————————

android:value="native-activity" /> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>

Activity 태그의 android:name 속성은 패키지명과 상관없이 “android.app.NativeActivty”로 지정합니다.

meta-data 태그에 NativeActivity를 포함한 공유 모듈명을 설정합니다. android:name 속성에는 “android.app.lib_name”을, android:value 속성에는 모듈명을 지정합니다. 모듈명은 파일명을 그대로 사용하지 않고, 선두에 “lib”와 확장자의 “.so”를 삭제한 명칭으로 합니다. 위의 경우, “native-activity”는 “libnative-activity.so”를 가리키는 것입니다.

Page 86: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

74

03.06.05 Android.mk 파일

NativeActvityGlue는 다른 모듈로 구축돼 있기 때문에 애플리케이션에 링크를 설정해야 합니

다. 일반적인 라이브러리는 LOCAL_LDLIBS 변수를 링크하지만 NativeActivityGlue는 안드

로이드 NDK에서 소스코드로 제공되는 외부 모듈입니다.

따라서 LOCAL_STATIC_LIBRARIES 변수에 외부 모듈로 링크 설정을 해야 합니다. 다음

예제 코드를 살펴보겠습니다.

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := native-activity LOCAL_SRC_FILES := main.c LOCAL_LDLIBS := -llog -landroid -lEGL -lGLESv1_CM LOCAL_STATIC_LIBRARIES := android_native_app_glue ——————————————

include $(BUILD_SHARED_LIBRARY)

$(call import-module,android/native_app_glue) —————————————————

LOCAL_STATIC_LIBRARIES 변수에 링크할 모듈(android_native_app_glue)을 지정합니다.

링크할 모듈이 있는 곳을 지정합니다. call import-module의 기본경로는 안드로이드 NDK가 설치된 폴더입니다. 여기서는 안드로이드 NDK 폴더의 android/native_app_glue 폴더에 있는 모듈을 설치합니다(자세한 내용은 7장에서 설명하겠습니다).

03.06.06 예제 코드

그럼 NativeActivityGlue를 이용해 애플리케이션을 만들어 보겠습니다. 여기서는 OpenGL|ES

를 사용해 사각형이 회전하는 애플리케이션을 만들어 보겠습니다.

OpenGL|ES의 표시에 대해서는 4장에서 자세히 설명하겠습니다.

main.c

#include <jni.h> #include <errno.h>

#include <EGL/egl.h>

Page 87: 안드로이드 NDK 네이티브 프로그래밍

3장 ┃ NativeActivity

75

#include <GLES/gl.h> #include <math.h>

#include <android/sensor.h> #include <android/log.h> #include <android_native_app_glue.h>

// 디버그용 메시지

#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, "native-activity", __VA_ARGS__)) #define LOGW(...) ((void)__android_log_print(ANDROID_LOG_WARN, "native-activity", __VA_ARGS__))

/** * 재시작에 필요한 데이터를 저장한다. */ struct saved_state { float angle; int32_t x; int32_t y; }; —————————————————————————————————————(1)

/** * 애플리케이션에서 공유하는 정보

*/ struct engine { struct android_app* app; ASensorManager* sensorManager; const ASensor* accelerometerSensor; ASensorEventQueue* sensorEventQueue; int animating; EGLDisplay display; EGLSurface surface; EGLContext context; int32_t width; int32_t height; struct saved_state state; };

void gluPerspective(double fovy ,double aspect , double zNear , double zFar) { GLfloat xmin, xmax, ymin, ymax; ymax = zNear * tan(fovy * M_PI / 360.0); ymin = -ymax; xmin = ymin * aspect; xmax = ymax * aspect; glFrustumf(xmin, xmax, ymin, ymax, zNear, zFar); }

Page 88: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

76

#define LENGTH(15) short triangleBuffer[] = { /* X Y Z */ -LENGTH/2, -LENGTH/2, 0, LENGTH-LENGTH/2, -LENGTH/2, 0, -LENGTH/2, LENGTH-LENGTH/2, 0, LENGTH-LENGTH/2, LENGTH-LENGTH/2, 0, };

float colorBuffer[] = { /* R G B A */ 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.5, 0.5, 0.0, 1.0, };

/** * 표시를 초기화한다. */ void initBox(struct engine* engine){ glDisable(GL_LIGHTING); glDisable(GL_CULL_FACE); glDisable(GL_DEPTH_BUFFER_BIT); glDisable(GL_DEPTH_TEST); glClearColor(.7f, .7f, .9f, 1.f); glShadeModel(GL_SMOOTH);

float ratio = (float)engine->width / (float)engine->height; glViewport(0, 0, (int)engine->width, (int)engine->height);

glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(40.0, ratio, 0.1,100); }

static float angle = 0; void drawBox(void) { // 각 프레임마다 표시를 초기화한다. glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glTranslatef(0.0, 0.0, -50.0); glRotatef(angle, 0,0,1.0f); // 그리기

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glEnableClientState(GL_VERTEX_ARRAY); glEnableClientState(GL_COLOR_ARRAY); glVertexPointer(3, GL_SHORT, 0, triangleBuffer); glColorPointer(4, GL_FLOAT, 0, colorBuffer); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

Page 89: 안드로이드 NDK 네이티브 프로그래밍

3장 ┃ NativeActivity

77

glDisableClientState(GL_VERTEX_ARRAY); }

/** * 디바이스의 EGL 컨텍스트를 초기화한다. */ static int engine_init_display(struct engine* engine) { // OepGL ES와 EGL을 초기화한다.

const EGLint attribs[] = { EGL_SURFACE_TYPE, EGL_WINDOW_BIT, EGL_BLUE_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_RED_SIZE, 8, EGL_NONE }; EGLint w, h, dummy, format; EGLint numConfigs; EGLConfig config; EGLSurface surface; EGLContext context; EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); eglInitialize(display, 0, 0); eglChooseConfig(display, attribs, &config, 1, &numConfigs); eglGetConfigAttrib(display, config, EGL_NATIVE_VISUAL_ID, &format); ANativeWindow_setBuffersGeometry(engine->app->window, 0, 0, format); surface = eglCreateWindowSurface(display, config, engine->app->window, NULL); context = eglCreateContext(display, config, NULL, NULL); if (eglMakeCurrent(display, surface, surface, context) == EGL_FALSE) { LOGW("Unable to eglMakeCurrent"); return -1; }

eglQuerySurface(display, surface, EGL_WIDTH, &w); eglQuerySurface(display, surface, EGL_HEIGHT, &h); engine->display = display; engine->context = context; engine->surface = surface; engine->width = w; engine->height = h; engine->state.angle = 0;

// Box 표시를 초기화한다. initBox(engine);

Page 90: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

78

return 0; } /* * 각 프레임 그리기

*/ static void engine_draw_frame(struct engine* engine) { if (engine->display == NULL) { // display가 없을 경우

return; }

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); drawBox(); eglSwapBuffers(engine->display, engine->surface); }

/** * EGL 컨텍스트를 파기한다. */ static void engine_term_display(struct engine* engine) { if (engine->display != EGL_NO_DISPLAY) { eglMakeCurrent(engine->display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); if (engine->context != EGL_NO_CONTEXT) { eglDestroyContext(engine->display, engine->context); } if (engine->surface != EGL_NO_SURFACE) { eglDestroySurface(engine->display, engine->surface); } eglTerminate(engine->display); } engine->animating = 0; engine->display = EGL_NO_DISPLAY; engine->context = EGL_NO_CONTEXT; engine->surface = EGL_NO_SURFACE; }

/** * 입력 이벤트를 처리한다. */ static int32_t engine_handle_input(struct android_app* app, AInputEvent* event) { struct engine* engine = (struct engine*)app->userData; if (AInputEvent_getType(event) == AINPUT_EVENT_TYPE_MOTION) { engine->animating = 1; engine->state.x = AMotionEvent_getX(event, 0); engine->state.y = AMotionEvent_getY(event, 0); return 1; } return 0; }

/**

Page 91: 안드로이드 NDK 네이티브 프로그래밍

3장 ┃ NativeActivity

79

* 주요 명령어를 처리한다. */ static void engine_handle_cmd(struct android_app* app, int32_t cmd) { struct engine* engine = (struct engine*)app->userData; switch (cmd) { case APP_CMD_SAVE_STATE: engine->app->savedState = malloc(sizeof(struct saved_state)); *((struct saved_state*)engine->app->savedState) = engine->state; engine->app->savedStateSize = sizeof(struct saved_state); break; case APP_CMD_INIT_WINDOW: if (engine->app->window != NULL) { engine_init_display(engine); engine_draw_frame(engine); } break; case APP_CMD_TERM_WINDOW: engine_term_display(engine); break; case APP_CMD_GAINED_FOCUS: if (engine->accelerometerSensor != NULL) { ASensorEventQueue_enableSensor(engine->sensorEventQueue, engine->accelerometerSensor); ASensorEventQueue_setEventRate(engine->sensorEventQueue, engine->accelerometerSensor, (1000L/60)*1000); } break; case APP_CMD_LOST_FOCUS: if (engine->accelerometerSensor != NULL) { ASensorEventQueue_disableSensor(engine->sensorEventQueue, engine->accelerometerSensor); } engine->animating = 0; engine_draw_frame(engine); break; } }

/** * 애플리케이션을 시작한다. */ void android_main(struct android_app* state) { struct engine engine;

// glue가 삭제되지 않도록

app_dummy();

memset(&engine, 0, sizeof(engine)); state->userData = &engine; state->onAppCmd = engine_handle_cmd; state->onInputEvent = engine_handle_input; engine.app = state;

Page 92: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

80

// 센서로부터 데이터를 취득하기 위해서 필요한 초기화를 한다. engine.sensorManager = ASensorManager_getInstance(); engine.accelerometerSensor = ASensorManager_getDefaultSensor(engine.sensorManager, ASENSOR_TYPE_ACCELEROMETER); engine.sensorEventQueue = ASensorManager_createEventQueue(engine.sensorManager, state->looper, LOOPER_ID_USER, NULL, NULL); if (state->savedState != NULL) { // 이전 상태로 돌아간다. engine.state = *(struct saved_state*)state->savedState; }

while(1) {

int ident; int events; struct android_poll_source* source;

// 센서로부터 입력되는 내용을 처리한다. while((ident=ALooper_pollAll(engine.animating ? 0 : -1, NULL, &events, (void**)&source)) >= 0) { // 이벤트를 처리한다. if (source != NULL) { source->process(state, source); }

// 센서에 데이터가 존재할 경우, 처리를 진행한다. if (ident == LOOPER_ID_USER) { if (engine.accelerometerSensor != NULL) { ASensorEvent event; while(ASensorEventQueue_getEvents(engine.sensorEventQueue, &event, 1) > 0){ LOGI("accelerometer: x=%f y=%f z=%f", event.acceleration.x, event.acceleration.y, event.acceleration.z); } } }

// 파기 요청이 있었는지 체크한다. if (state->destroyRequested != 0) { engine_term_display(&engine); return; } }

if (engine.animating) { // 다음 프레임을 그리기 위해 필요한 처리를 한다. // 사각형을 회전한다. angle = (angle+2); if (angle > 360){ angle = 0;

Page 93: 안드로이드 NDK 네이티브 프로그래밍

3장 ┃ NativeActivity

81

} // 화면 그리기

engine_draw_frame(&engine); } } }

03.07 정리

NativeActivity를 이용하면 C/C++만으로도 안드로이드 애플리케이션을 만들 수 있습니다.

특히, 게임을 만드는 데 유용합니다. 또 NativeActivity를 이용해 애플리케이션을 만들 때는

NativeActivityGlue도 함께 이용하는 것이 좋습니다.

NativeActivityGlue는 NativeActivity의 번거로운 부분을 처리해주기 때문에 NativeActi

vity만으로 애플리케이션을 만드는 것보다 훨씬 편리합니다.

아울러 NativeActivityGlue는 안드로이드 NDK 안에 소스코드가 있어서 자유롭게 응용할

수도 있습니다.

Page 94: 안드로이드 NDK 네이티브 프로그래밍

안드로이드 NDK 네이티브 프로그래밍

82

칼럼

사용성

안드로이드에서는 하드웨어의 키 배치나 화면 해상도가 기종마다 다릅니다. 터치패널을 이용

하는 게임을 만들 경우에는 이러한 화면 해상도의 차이를 고려해서 만들어야 합니다.

소니 에릭슨에서 발매한 엑스페리아 플레이(Xperia PLAY)의 경우, 풀 키보드 대신 십자키,

LR 키, 그리고 4개의 버튼처럼 플레이스테이션의 컨트롤과 닮은 하드웨어 키를 갖추고 있습

니다. 앞으로는 이처럼 개성 있는 입력 방법을 탑재한 단말도 많아질 것입니다.

[그림 03-07] 엑스페리아 플레이

안드로이드 3.1에서는 USB의 호스트 기능을 지원합니다. 이 기능을 이용하면 안드로이드 단

말기에 엑스박스(XBOX)와 같은 컨트롤러를 연결해서 조작할 수 있습니다. 이처럼 앞으로 안

드로이드에서는 다양한 하드웨어의 접속이 가능해질 것입니다. 특히, 애플리케이션 게임이라

면 터치패널보다 USB 컨트롤러를 이용하는 편이 조작하는 데 훨씬 편리하다는 것은 명백한

사실입니다.

그러나 이것은 미래의 이야기도 포함돼 있기에 현재는 터치패널만으로 조작할 수 있게 만들

어 놓고, 안드로이드 운영체제의 보급 정도를 고려해 가면서 USB가 연결된 컨트롤러를 지원

하는 방법을 택하는 것도 좋은 방법일 것입니다.