[Kotlin] Android bitmap 최적화
저번에 겪었던 OutOfMemory가 단순 빌드 이슈가 아닌 성능 이슈로 다가왔다.
내 쪽에서 테스트는 문제가 없었는데 다른 기기들에서 간헐적으로 발생한다는 것.
아마 맨 처음에 만들었을 때 4개 정도 사진 찍을거로 생각해 이미지 최적화까지는 하지 않았었는데
그게 20개로?왜?늘어서 캐시가 감당할 수 없을 정도의 메모리를 사용하고 있던 것.
그래서 저번에 만들었던 custom capture pad를 kotlin으로 마이그레이션 하면서 동시에 최적화 작업도 해보았다.
https://minggu92.tistory.com/11
(2년 전에 만든 건데...)
일단 OutOfMemory 이슈는 try catch로 잡힌다는 걸 알게 되었고... (심지어 app crash가 발생하지도 않음)
그에 따른 예외처리를 따로 해줘야 할 필요가 느껴진다.
아무리 자바엔 GC가 있다지만 사진을 이미지 축소하지 않고 사용하면 캐시 메모리는 금방 차 버린다. 명시적으로 메모리 해제할 일이 생길지도..
그래서 BitmapFactory의 isSampleSize Options을 줘서 크기를 1/4로 줄였다.
동적으로 ImageView의 크기를 계산해 isSampleSize option을 할당하는 방법도 있지만
그렇게까지 할 필요가 없어 코드는 사용하지 않았다.
Bitmap을 recycle 하는 방법도 있는데 동적으로 생성된 캡처 패드 객체라서 사용하기 어려워 보인다.
안드로이드를 손에서 놓은 지 좀 되었지만 최신 버전들을 사용해서 깔끔하게 작성해 보고 싶은 욕구가 생겼다.
package com.ming.gu.function
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Matrix
import android.graphics.drawable.BitmapDrawable
import android.media.ExifInterface
import android.os.Build
import android.provider.MediaStore
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewManager
import android.view.ViewTreeObserver
import android.widget.*
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.content.res.ResourcesCompat
import androidx.fragment.app.Fragment
import com.gun0912.tedpermission.util.ObjectUtils
import com.ming.gu.R
import com.ming.gu.model.CaptureItem
import java.io.File
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.*
class CustomCapturePad {
private var bnCaptureSave: Button? = null
private var bnCaptureClear: Button? = null
private var bnCaptureClose: ImageButton? = null
private var mCurrentPhotoPath: String? = null
private var ivCapture: ImageView? = null
private var inputBitmap: Bitmap? = null
private var outputBitmap: Bitmap? = null
private var mFragment: Fragment? = null
private var imgWidth: Int = 0
private var imgHeight: Int = 0
@SuppressLint("InflateParams", "QueryPermissionsNeeded", "SimpleDateFormat")
fun makeCapturePad(inputFragment: Fragment, inputCaptureItem: CaptureItem?) {
mFragment = inputFragment
inputBitmap = inputCaptureItem!!.capture
//새 inflater 생성
val inflater = mFragment!!.activity
?.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
//새 레이아웃 객체생성
val linearLayout = inflater.inflate(R.layout.popup_capture, null) as LinearLayout
//레이아웃 배경 투명도 주기
val myColor = ContextCompat.getColor(mFragment!!.requireContext(), R.color.o60)
linearLayout.setBackgroundColor(myColor)
//레이아웃 위에 겹치기
val params = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT
)
//바깥 터치 안되게
linearLayout.isClickable = true
linearLayout.isFocusable = true
mFragment!!.requireActivity().addContentView(linearLayout, params)
//버튼
ivCapture = linearLayout.findViewById(R.id.iv_capture_view)
bnCaptureSave = linearLayout.findViewById(R.id.btn_capture_save)
bnCaptureClear = linearLayout.findViewById(R.id.btn_capture_clear)
bnCaptureClose = linearLayout.findViewById(R.id.btn_capture_close)
ivCapture?.setImageBitmap(inputBitmap) //최초 로드시 기존꺼 세팅
//disable both buttons at start
bnCaptureSave?.isEnabled = false
bnCaptureSave?.backgroundTintList = mFragment?.requireContext()
?.let { ContextCompat.getColor(it, R.color.disable) }
?.let { ColorStateList.valueOf(it) }
//get ImageView size
ivCapture?.viewTreeObserver?.addOnPreDrawListener(object :
ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
imgWidth = ivCapture?.measuredHeight!!
imgHeight = ivCapture?.measuredWidth!!
ivCapture?.viewTreeObserver?.removeOnPreDrawListener(this)
return true
}
})
ivCapture?.setOnClickListener {
val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
// Ensure that there's a camera activity to handle the intent
if (takePictureIntent.resolveActivity(mFragment!!.requireActivity().packageManager) != null) {
// Create the File where the photo should go
var photoFile: File? = null
try {
//set Temp FileDirectory
val tempDir = mFragment!!.requireActivity().cacheDir
// File tempDir = new File(mFragment.getContext().getFilesDir() + "/temp");
//set Temp File Name
val timeStamp = SimpleDateFormat("yyyyMMdd").format(Date())
val imageFileName = "JPEG_" + timeStamp + "_"
val tempImage = File.createTempFile(
imageFileName, /* prefix */
".jpg", /* suffix */
tempDir /* directory */
)
// Save a file: path for use with ACTION_VIEW intents
mCurrentPhotoPath = tempImage.absolutePath
Log.i(TAG, "temp mCurrentPhotoPath : $mCurrentPhotoPath")
photoFile = tempImage
} catch (ex: IOException) {
// Error occurred while creating the File
}
// Continue only if the File was successfully created
if (photoFile != null) {
val photoURI = FileProvider.getUriForFile(
mFragment!!.requireActivity(),
mFragment!!.requireContext().packageName + ".fileprovider",
photoFile
)
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI)
mFragment!!.startActivityForResult(
takePictureIntent,
10
)
}
}
}
//CAPTURE SAVED
bnCaptureSave?.setOnClickListener {
try {
val drawable = ivCapture?.drawable as BitmapDrawable
val bitmap = drawable.bitmap
inputCaptureItem.capture = outputBitmap
outputBitmap = null
if (bitmap != null) {
inputCaptureItem.status = true
} else {
inputCaptureItem.status = false
inputCaptureItem.capture = (ResourcesCompat.getDrawable(
mFragment!!.requireActivity().resources,
R.drawable.icon_camera_999,
null
) as BitmapDrawable?)!!.bitmap //default image
}
mCurrentPhotoPath = "" //initialize
mFragment!!.onActivityResult(501, 10, null) //update item
if (linearLayout.parent != null) (linearLayout.parent as ViewManager).removeView(
linearLayout
)
} catch (e: Exception) {
Log.w(TAG, "SAVE ERROR!", e)
}
}
bnCaptureClear?.setOnClickListener {
Toast.makeText(mFragment!!.activity, "clear", Toast.LENGTH_LONG).show()
ivCapture?.setImageBitmap(null)
//disable both buttons at start
bnCaptureSave?.isEnabled = true
// bnCaptureClear.setEnabled(false);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
bnCaptureSave?.backgroundTintList = mFragment?.requireContext()
?.let { it1 -> ContextCompat.getColor(it1, R.color.button_default_color) }
?.let { it2 -> ColorStateList.valueOf(it2) }
// bnCaptureClear.setBackgroundTintList(ColorStateList.valueOf(mFragment.getActivity().getResources().getColor(R.color.disable)));
}
}
bnCaptureClose?.setOnClickListener {
if (linearLayout.parent != null) (linearLayout.parent as ViewManager).removeView(
linearLayout
)
}
}
fun onActivityResult(requestCode: Int, resultCode: Int) {
try {
//after capture
when (requestCode) {
10 -> {
if (resultCode == Activity.RESULT_OK) {
// val file = mCurrentPhotoPath?.let { File(it) }
// var bitmap = MediaStore.Images.Media
// .getBitmap(
// mFragment!!.requireActivity().contentResolver,
// Uri.fromFile(file)
// )
val options: BitmapFactory.Options = BitmapFactory.Options()
options.inSampleSize = 4
var bitmap = BitmapFactory.decodeFile(mCurrentPhotoPath, options)
if (!ObjectUtils.isEmpty(bitmap)) {
val ei = ExifInterface(mCurrentPhotoPath!!)
val orientation = ei.getAttributeInt(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_UNDEFINED
)
try {
bitmap = when (orientation) {
ExifInterface.ORIENTATION_ROTATE_90 -> rotateImage(bitmap, 90f)
ExifInterface.ORIENTATION_ROTATE_180 -> rotateImage(bitmap, 180f)
ExifInterface.ORIENTATION_ROTATE_270 -> rotateImage(bitmap, 270f)
ExifInterface.ORIENTATION_NORMAL -> bitmap
else -> bitmap
}
outputBitmap = bitmap //원본은 회전시켜 저장
// options.inSampleSize = calculateInSampleSize(options, imgWidth, imgHeight)
// bitmap = Bitmap.createScaledBitmap(bitmap, imgWidth, imgHeight, true)
ivCapture?.setImageBitmap(bitmap)
bnCaptureSave!!.isEnabled = true
bnCaptureClear!!.isEnabled = true
bnCaptureSave!!.backgroundTintList = mFragment?.requireContext()
?.let {
ContextCompat.getColor(it, R.color.button_default_color)
}?.let { ColorStateList.valueOf(it) }
} catch (e: OutOfMemoryError) {
Log.e(TAG, "OutOfMemoryError!", e)
} finally {
// if (!bitmap.isRecycled) bitmap.recycle();
}
}
}
}
}
} catch (e: Exception) {
Log.w(TAG, "onActivityResult Error !", e)
}
}
fun decodeSampledBitmapFromResource(
res: Resources,
resId: Int,
reqWidth: Int,
reqHeight: Int
): Bitmap {
// First decode with inJustDecodeBounds=true to check dimensions
return BitmapFactory.Options().run {
inJustDecodeBounds = true
BitmapFactory.decodeResource(res, resId, this)
// Calculate inSampleSize
inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight)
// Decode bitmap with inSampleSize set
inJustDecodeBounds = false
BitmapFactory.decodeResource(res, resId, this)
}
}
//축소 버전을 메모리로 로드
private fun calculateInSampleSize(
options: BitmapFactory.Options,
reqWidth: Int,
reqHeight: Int
): Int {
// Raw height and width of image
val (height: Int, width: Int) = options.run { outHeight to outWidth }
var inSampleSize = 1
if (height > reqHeight || width > reqWidth) {
val halfHeight: Int = height / 2
val halfWidth: Int = width / 2
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
inSampleSize *= 2
}
}
return inSampleSize
}
companion object {
val TAG = CustomCapturePad::class.java.simpleName.trim() //CustomCapturePad"
//카메라에 맞게 이미지 로테이션
fun rotateImage(source: Bitmap, angle: Float): Bitmap {
val matrix = Matrix()
matrix.postRotate(angle)
return Bitmap.createBitmap(
source, 0, 0, source.width, source.height,
matrix, true
)
}
}
}
import android.graphics.Bitmap
class CaptureItem {
var title: String? = null
var realPath: String? = null
var category: String? = null
var capture: Bitmap? = null
var status: Boolean? = null
override fun toString(): String {
return "CaptureItem [" +
" title : $title"+
", category : $category"+
", realPath : $realPath"+
", capture : $capture"+
", status : $status"+
"]"
}
}
<참조>
https://developer.android.com/topic/performance/graphics/load-bitmap?hl=ko
https://www.androidpub.com/android_dev_qna/1282821
https://mainia.tistory.com/468
https://leveloper.tistory.com/167
https://marlboroyw.tistory.com/481
'Andorid' 카테고리의 다른 글
[Kotlin] Android 네트워크 연결 상태 확인 (1) | 2022.09.11 |
---|---|
[Kotlin] Android OutOfMemoryError (0) | 2022.08.27 |
[Kotlin] Local Storage에 logcat파일 만들기 (0) | 2022.08.25 |
[Kotlin] Firebase 연동 & Realtime Database 조작 (0) | 2022.04.03 |
[Android / Java] Cloud Firestore 연동을 통한 Data 조작(feat. RecyclerView) (0) | 2022.03.27 |