이 글은 기존 운영했던 WordPress 블로그인 PyxisPub: Development Life (pyxispub.uzuki.live) 에서 가져온 글 입니다. 모든 글을 가져오지는 않으며, 작성 시점과 현재 시점에는 차이가 많이 존재합니다.
작성 시점: 2018-12-04
안드로이드에서 String나 ByteArray를 Base64 로 인코딩 / 디코딩 하는 라이브러리는 많다. 기본적으로 안드로이드가 제공하는 Base64도 있고, Apache의 Commons-codec에도 있고, JDK 1.8에 (이제야) 구현된 Base64가 있다.
그러면 '어떤 것을 사용하는 것이 더 성능이 나올까?' 라는 단순한 질문이 나올 수 있는데, 이미 자바로는 2014년에 비교한 Base64 encoding and decoding performance 라는 글이 있다.
다만 해당 글에는 안드로이드에선 사용하지 못하는 DataTypeConverter 나 sun 패키지가 있어 참고로 하기에는 어려운 점이 많다.
그래서 이번 기회로 안드로이드에서 사용할 수 있는 Base64 라이브러리 6종에 대해 성능 비교를 하려고 한다.
비교 방법으로는 크게 3가지가 있다.
fun testBytes(bufferSize: Int): HashMap<String, TestResult> {
val r = Random(125)
val buffers = ArrayList<ByteArray>()
for (i in 0 until bufferSize) {
val buf = ByteArray(bufferSize)
r.nextBytes(buf)
buffers.add(buf)
}
val results = HashMap<String, TestResult>()
for (codec in byteCodecList) {
val name = codec.javaClass.simpleName
results[name] = testByteCodec(codec, buffers)
}
return results
}
@Throws(IOException::class)
private fun testByteCodec(codec: Base64ByteCodec, buffers: List<ByteArray>): TestResult {
val encoded = ArrayList<ByteArray>()
val result = ArrayList<ByteArray>()
val encodeTime =
measureTimeStopWatch { for (buf in buffers) encoded.add(codec.encodeBytes(buf)) }
val decodeTime =
measureTimeStopWatch { for (ar in encoded) result.add(codec.decodeBytes(ar)) }
return TestResult(encodeTime.toDouble(), decodeTime.toDouble())
}
buffersize 만큼의 사이즈를 가지는 ByteArray를 bufferSize개 만큼 생성하고, 이를 각 라이브러리마다 인코딩과 디코딩 시간을 측정한다. 예를 들어 1024로 지정했다면 length가 1024인 ByteArray를 1024개를 생성하여 인코딩 시간과 디코딩 시간을 측정하는 방식이다.
여기서 Base64ByteCodec 는 인터페이스로 이 인터페이스를 구현하는 클래스는 AndroidImpl, ApacheImpl, IHarderImpl, Java8Impl, MiGImpl 총 5개로 Guava는 아쉽게도 지원하지 않는다.
class AndroidImpl : Base64Codec, Base64ByteCodec {
private val flag = Base64.DEFAULT
override fun decodeBytes(base64: ByteArray): ByteArray {
return Base64.decode(base64, flag)
}
override fun encodeBytes(data: ByteArray): ByteArray {
return Base64.encode(data, flag)
}
override fun encode(data: ByteArray): String {
return Base64.encodeToString(data, flag)
}
override fun decode(base64: String): ByteArray {
return Base64.decode(base64, flag)
}
}
내부 구현체는 이런 식으로 testByteCodec 메서드에서 decodeBytes, encodeBytes 등의 메서드를 호출하면 각 라이브러리의 코드가 호출되는 형식이다.
위 ByteArray <-> String 와 비슷한 방식이나 호출되는 메서드만 encode, decode가 불린다.
fun testFile(file: File): HashMap<String, TestResult> {
val results = HashMap<String, TestResult>()
val fileBytes = file.readBytes()
for (codec in byteCodecList) {
val name = codec.javaClass.simpleName
results[name] = testByteCodecFile(codec, fileBytes)
}
// Guava doesn't support ByteArray -> ByteArray. so we ignore them.
return results
}
@Throws(IOException::class)
private fun testByteCodecFile(codec: Base64ByteCodec, buffer: ByteArray): TestResult {
val encodeTime = measureTimeStopWatch { codec.encodeBytes(buffer) }
return TestResult(encodeTime.toDouble(), 0.0)
}
파일을 객체로 받아서 각 라이브러리마다 인코딩 시간만을 측정한다. (디코딩은 측정하지 않았다.)
테스트 기기는 Galaxy S8, 데이터의 정확성을 위해서 전체 테스트 셋트를 3번 반복하여 나온 평균치로 산정했다.
전체적으로 Java8Impl 와 MiGImpl 가 비슷한 속도를 보이고, AndroidImpl는 디코딩에서는 강점을 보이는 반면 인코딩에서는 Apache와 다를바가 없는 속도를 보여주었다.
ByteArray <-> ByteArray를 지원하지 않는 Guava가 추가되었는데, 수치가 많이 튄 것을 볼 수 있다. 어떤 한번만 그런 것이 아닌 지속해서 발생하는 것으로 봐서는 테스트 방법이 적절치 않았다고 판단하거나 Guava가 다른 라이브러리와는 조금 다른 구현체를 가지고 있는 것 같다.
ByteArray <-> ByteArray때와 마찬가지로 인코딩 때는 Java8Impl, MiGImpl가 강점을 보이고 디코딩 때에는 Java8Impl 와 AndroidImpl가 강점을 보였다.
1.5MB에서는 5개 라이브러리가 큰 차이를 보이고 있지는 않았으나, 5MB와 10MB에는 Java8Impl - MiGImpl 가 서로 비슷하고, AndroidImpl - IHarderImpl > ApacheImpl 가 서로 비슷했다.
이미지 인코딩의 5MB, 10MB 이미지와 큰 차이는 보이지 않았다. 마찬가지로 Java8Impl 와 MiGImpl가 서로 비슷했다.
결론은 아래와 같이 정리된다.
벤치마크시에 사용된 샘플 앱은 Github에 공개되어 있다.