Tistory View

Android Develop

안드로이드 Java ByteBuffer의 allocateDirect

God Dangchy What should I do? 2019. 12. 1. 02:41

서론

플랫폼이나 Java 버전마다 다르겠지만 Java heap을 이용하는 것보다 Direct방식을 이용하는 것이 대부분의 상황에서 더빠르다.(보통 이 방식을 전송[네트웍이든 파일이든 메모리데이터 교환이든]을 위해 사용한다는 기준에서 빠르다는 뜻이다.)

필자는 이 방식을 Native(NDK)와 Java에서 메모리를 공유하는 데 주로 사용한다.(Native에서 빠르게 읽어내려고.,,)

생성법

생성하는 법은 두가지가 있다. 하나는 Java에서, 다른 하나는 Native에서 만드는 것이다. 같은 것 같지만 다른 점이 있다.

Java에서 생성

생성하는 함수의 원형은 다음과 같다.

static ByteBuffer allocateDirect( int capacity );

 

필요한 용량을 바이트단위로 넘기면 ByteBuffer 객체를 생성하게 된다. 이 때 만들어진 메모리는 실제 네이티브에 생성되게 되며, GC에서 해제를 하지 않는다(ByteBuffer객체는 해제되지만 내부의 메모리는 해제되지 않는다). 따라서 프로그램(앱)이 완전히 종료되기 전에는 계속 존재하게 된다. 또한 Java Heap이 아니므로, Java의 메모리사용량에 포함되지 않아 OOM을 예방할 수도 있다.

이 때 생성된 메모리는 언제 Native에서 사용하고 있을지 모르기 때문에,
메모리를 계속 유지하는 것은 당연한 일이지만,
혹시나 java버전이나 플랫폼에따라 없어질지도 모른다는 기우가 있기는 하다.

 

Native에서 생성

Native코드에서 ByteBuffer를 만들려면 다음과 같이 한다. (코드 1)

// NATIVE PART
//public static native ByteBuffer allocDirectByteBuffer( int byteSize )
extern "C" JNIEXPORT void JNICALL
Java_com_...._YourClassName_allocDirectByteBuffer( JNIEnv* env, jclass cls, jint byteSize )
{
    void* p = malloc( byteSize );
    return env->NewDirectByteBuffer( p, byteSize );
}

// JAVA PART
ByteBuffer buf = YourClassName.allocDirectByteBuffer( 1024 );

NewDirectByteBuffer는 이미 생성된 주소가 필요하기 때문에, 실제 필요한 크기의 메모리를 할당하여 넘겨줘야 한다.

또한 이 메모리는 사라지면 안되기 때문에 스택에 만들지 않고 Native heap에 만들어야 한다.

NewDirectByteBuffer의 리턴값이 바로 ByteBuffer 객체라 자바로 다시 돌려 주면 바로 쓸 수가 있다.

이 객체를 Native에서 계속 유지하려면 GLobal Ref로 변경해줘야 할 것이다.(테스트 안함)
하지만 실제 Native Part에서는 메모리 주소만 있으면 되니 그렇게 쓸 일은 거의 없을 것이다.

"Natve에서 생성"하는 방식을 이용하면 좋은 점이 있는 데, Java[allocateDirect()]에서 실제 할당된 메모리는 없앨 방법이 없지만, Native방식은 더 이상 필요가 없으면 "해제할 수 있다"는 것이다.

예를 들어, Activity의 onCreate에 allocateDirect()를 사용하여 ByteBuffer를 만들 경우, Activity가 다시 생성될 때마다 메모리는 계속 다시 할당되어 지속적으로 메모리 사용량이 늘어나도 해제할 방법이 없지만, "Native에서 생성"방법을 쓰면, onDestroy등 해제를 하는 코드를 만들어 넣을 수 있다.

 

Native에서 메모리 주소 가져오기

allocateDirect()로 할당한 메모리를 Native에서 접근하려면 다음과 같은 코드로 메모리 주소를 가지고 올 수 있다.

 

(코드 2)

char* p = (char*)env->GetDirectBufferAddress(env, ByteBuffer 객체 );

Java에서 ByteBuffer를 파라미터로 넘겨주고, 그 파라미터를 GetDirectBufferAddress에 넘겨주면 된다. Native영역에 들어왔으니, 위의 예제에서는 char*로 형변환을 했지만, 원하는 형태로 그냥 형변환만 해서 사용하면 된다.

만약 위에서 만든 allocateDirectBuffer함수를 사용했다면 p값은 malloc으로 만들어진 값과 같은 값이다.

 

Java에서 Native를 사용해본 사람이라면 느끼겠지만, GetDirectBufferAddress를 썻으니 그에 상응하는 ReleaseDirectBufferAddress같은 것이 있을 거라 생각할 지도 모르지만, 그런 것은 없다. GetDirectBufferAddress에서 가져오는 메모리번지는 늘 같은 값이고 그냥 정해진 메모리번지일 뿐이니 상응하는 해제코드도 없고, 할 필요도 없다.

 

 

Native에서 만들었다면 실제 메모리를 해제하자.

위에서 (코드 1)을 이용하여 ByteBuffer를 만들었다면, 해제하는 코드는 다음과 같게 된다.

 

(코드 3)

// NATIVE PART
//public static native ByteBuffer freeDirectByteBuffer( ByteBuffer buf )
extern "C" JNIEXPORT void JNICALL
Java_com_...._YourClassName_freeDirectByteBuffer( JNIEnv* env, jclass cls, jobject oBuf )
{
    free( env->GetDirectBufferAddress( oBuf ) );
}

// JAVA PART // oBuf는 allocDirectByteBuffer의 리턴값
YourClassName.freeDirectByteBuffer( oBuf );

(코드 1)이 Activity의 onCreate에 있다면 (코드 3)은 onDestroy에 넣어두면 적당할 것이다. 물론 이제 절대로 이 메모리에 쓰거나 읽으면 안된다.

 

 

ByteBuffer를 FloatBuffer등 다른 형태로

allocate()를 쓰든 allocateDirect()를 쓰든 native에서 만들든, ByteBuffer를 (Float, Double, Int, Long, Short Char)Buffer로 다음과 같이 변환을 할 수가 있다.

 

(코드 4)

[형태]Buffer buf = ByteBuffer.allocate( 바이트크기 ).as[형태]Buffer();

 

Java는 기본적으로 BIG-ENDIAN을 사용하기 때문에 위에서 열거한 (Float, Double..등등)은 2바이트 이상을 쓰게 되므로 Native에서 접근할 경우 Endian을 고려해야 한다. 가장 쉬운 방법은 다음과 같이 Native에서 쓰는 Endian과 동일하게 만들어 주는 것이다.

 

(코드 5)

[형태]Buffer buf = Buffer.allocateDirect( 바이트크기 ).order( ByteOrder.nativeOrder() ).as[형태]Buffer();

이제 Native든 Java든 ENDIAN을 고려하지 않고 데이터를 쓰고 읽으면 된다.

 

위에서 작성(코드 1)한 할당함수를 사용한다면 다음과 같은 식의 코드가 될 것이다.

 

(코드 6)

FloatBuffer buf = YourClassName.allocDirectByteBuffer(바이트크기)
                     .order( ByteOrder.nativeOrder() )
                     .asFloatBuffer();

 

Multithread를 지원하는가?

아니다. 만약 Multithread용으로 만들어야 한다면 필요한 곳에 직접 Synchronizing코드를 넣어줘야 한다.

만약 자체적으로 Multithread를 지원했다면, 오히려 속도에서 더 떨어져서 사용가치가 없었을 것이다.

 

 

allocateDirect와 hasArray(), array(), UnsupportedOperationException

위의 내용은 Java와 NDK를 이용하는 상황에서 모두 동작을 하지만, array()는 사용하는 환경에서 지원을 해줘야 쓸 수가 있다.

다음의 코드는 Android에서 UnsupportedOperationException을 발생시킨다.

ByteBuffer b = ByteBuffer.allocateDirect(1024);

byte[] a = b.array(); // UnsupportedOperationException
a[1] = 3;

안드로이드 버전이 올라가며 지원이 될지는 모르지만 필자기 테스트한 결과는 지원하지 않는 것으로 보인다.

편리하게 사용하려 했지만, 이 방식을 안드로이드에서는 사용할 수 없으니, put, get을 이용하여 작업을 해야한다.

호환성이 높은 코드를 위해서도 어쩔 수 없이 put,get를 써야 할 듯하다.

(확인해 보지는 않았지만, allocate()로 할당된 것은 array()를 쓸 수 있는 듯한데, 이 글의 주제는 Direct를 다루는 것이다)

 

put이나 get은 ByteBuffer의 기본 사용법이니 다른 예제와 설명을 인터넷에서 쉽게 구할 수 있을 것이다.

 

'Android Develop' 카테고리의 다른 글

안드로이드 Java ByteBuffer의 allocateDirect  (2) 2019.12.01
Replies
Reply Write