Skip to content

Latest commit

Β 

History

History
203 lines (153 loc) Β· 7.96 KB

Loading Large Bitmaps Efficiently.md

File metadata and controls

203 lines (153 loc) Β· 7.96 KB

효율적인 큰 λΉ„νŠΈλ§΅ λ‘œλ”©

1. 잘 될 거라고 μƒκ°ν–ˆμ–΄μš”...

μš”λ²ˆμ— 폰에 μžˆλŠ” 사진듀을 λͺ¨λ‘ 가져와 μ‚¬μš©μžμ—κ²Œ 리슀트둜 λ³΄μ—¬μ€˜μ•Όν•˜λŠ” κΈ°λŠ₯을 λ§Œλ“€κ²Œ λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

μ €λŠ” 'νΌλ―Έμ…˜μ— μ €μž₯μ†Œ μ ‘κ·Ό κΆŒν•œμ„ μ£Όκ³  λ―Έλ””μ–΄ μ €μž₯μ†Œμ˜ λͺ¨λ“  μ‚¬μ§„λ“€μ˜ PATH URI λ₯Ό 가져와 ArrayList 에 λͺ¨λ‘ μΆ”κ°€ν•˜μ—¬ RecyclerView 둜 보여주면 λ˜κ² λ‹€.' 라고 μƒκ°ν–ˆμŠ΅λ‹ˆλ‹€.

μ €λŠ” λ°”λ‘œ μž‘μ—…μ— λ“€μ–΄κ°”μŠ΅λ‹ˆλ‹€.

λ¨Όμ € AndroidManifest 에

<uses-permissionandroid:name="android.permission.READ_EXTERNAL_STORAGE" /> 

μ €μž₯μ†Œ μ ‘κ·Ό κΆŒν•œμ„ μ£Όμ—ˆμŠ΅λ‹ˆλ‹€.
그리고 λͺ¨λ“  μ‚¬μ§„λ“€μ˜ PATH λ₯Ό λ½‘λŠ” ν•¨μˆ˜λ₯Ό λ§Œλ“€μ—ˆμŠ΅λ‹ˆλ‹€.

@SuppressLint("Recycle")
private fun getAllImages(activity: Activity){
    val uri: Uri = android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI
    val cursor: Cursor?
    val columnIndexData: Int
    val albumList = ArrayList<String>()

    val projection = arrayOf(MediaStore.MediaColumns.DATA, MediaStore.Images.Media.BUCKET_DISPLAY_NAME)

    cursor = activity.contentResolver.query(uri, projection, null, null, null)

    columnIndexData = cursor!!.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)
    while (cursor!!.moveToNext()) {
        albumList.add(cursor!!.getString(columnIndexData))
    }
    albumList.reverse()
    albumAdapter = AlbumAdapter(context!!, albumList)
    albumAdapter.notifyDataSetChanged()
}

λ§ˆμ§€λ§‰μœΌλ‘œ 사진을 보여쀄 RecyclerView 에 쓰일 item view 와 Adapter 을 λ§Œλ“€μ—ˆμŠ΅λ‹ˆλ‹€.

import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import coloring.com.ccb.R
import coloring.com.ccb.ui.palette.AddPaletteColorActivity
import kotlinx.android.synthetic.main.item_photo.view.*

class AlbumAdapter(private val context: Context,
                   private val albumList: ArrayList<String>) : RecyclerView.Adapter<AlbumAdapter.AlbumViewHolder>() {

    override fun onBindViewHolder(holder: AlbumViewHolder, position: Int) {
        if (img != null) 
            holder.itemView.photo.imageURI = Uri.parse(albumList[position])
        else 
            holder.itemView.photo.setImageResource(R.drawable.ic_launcher_background)

        holder.itemView.photo.setOnClickListener {
            val intent = Intent(context, AddPaletteColorActivity::class.java)
            if(img != null) intent.putExtra("path", albumList[position])
            else intent.putExtra("path", "noPath")
            context.startActivity(intent)
        }
    }

    override fun getItemCount() = albumList.size

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AlbumViewHolder = AlbumViewHolder(parent)

    class AlbumViewHolder(parentView : View) : RecyclerView.ViewHolder(
            LayoutInflater.from(parentView.context).inflate(R.layout.item_photo, null, false))
}

μ €λŠ” μ •λ§λ‘œ 잘 μž‘λ™λ  것이라고 μƒκ°ν–ˆμŠ΅λ‹ˆλ‹€.
κ·ΈλŸ¬λ‚˜ μ €μ˜ κΈ°λŒ€λ₯Ό 져버리지 μ•Šκ³  정말 λ†€λžκ²Œλ„ 치λͺ…적인 λ¬Έμ œκ°€ 생기고 λ§™λ‹ˆλ‹€.

앱이 μ™„μ „νžˆ μΌœμ§€κΈ° κΉŒμ§€ 10 초 이상 걸리고,
사진 λ¦¬μŠ€νŠΈλ“€μ„ 보여쀄 λ•Œ, μ—„μ²­ 많이 폰의 λ©”λͺ¨λ¦¬λ₯Ό λ¨ΉμœΌλ©΄μ„œ 폰의 ꡬ동이 λ©ˆμΆ”λ©΄μ„œ 움직이지 μ•ŠλŠ” κ²ƒμ΄μ—ˆμŠ΅λ‹ˆλ‹€...

νœ΄λŒ€ν°λ„ 감당λͺ»ν•˜λŠ” 치λͺ…적인 μ €μ˜ λ―ΈμŠ€λŠ” λ¬΄μ—‡μ΄μ—ˆμ„κΉŒμš”?

2. ꡬ동을 μœ„ν•œ 문제 원인 μ°ΎκΈ°(μ‚½μ§ˆ ν•˜κΈ°)

μ €λŠ” RecylclerView 의 ꡬ쑰에 λ¬Έμ œκ°€ μžˆμ—ˆλ˜κ²Œ μ•„λ‹κΉŒ μƒκ°ν–ˆμŠ΅λ‹ˆλ‹€.
RecyclerView κ΄€λ ¨ λ¬Έμ„œλ“€μ„ ν•΄μ„ν•˜λ©΄μ„œ κΉŒμ§€ μ½μ–΄λ³΄μ•˜μ§€λ§Œ 별 μ†Œλ“μ΄ μ—†μ—ˆμŠ΅λ‹ˆλ‹€.

λ‹€μ‹œ μƒκ°ν•΄λ³΄λ‹ˆ, RecyclerView 에 item 으둜 사진듀이 잘 λ³΄μΈλ‹€λŠ” 것은 RecyclerView 의 잘λͺ»μ΄ μ•„λ‹Œ 것을 μ•Œκ²Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.

μ €λŠ” item 에 λ¬Έμ œκ°€ μžˆλŠ”κ²Œ μ•„λ‹κΉŒ μΆ”μΈ‘ν–ˆμŠ΅λ‹ˆλ‹€.

<?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/photo"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:scaleType="fitCenter"
    android:adjustViewBounds="true" />

μœ„μ—λŠ” 사진을 λ³΄μ—¬μ£ΌλŠ” item의 xml 파일 μž…λ‹ˆλ‹€.
μ •λ§λ‘œ 별거 없이 ImageView 만으둜 이루어져 μžˆμŠ΅λ‹ˆλ‹€.

if (img != null) 
    holder.itemView.photo.imageURI = Uri.parse(albumList[position])
else 
    holder.itemView.photo.setImageResource(R.drawable.ic_launcher_background)

μœ„μ— μ½”λ“œλŠ” item 에 이미지λ₯Ό μ„€μ •ν•˜λŠ” μ½”λ“œμž…λ‹ˆλ‹€.
μ—¬κΈ°μ„œ img λŠ” μ΄λ―Έμ§€μ˜ PATH μž…λ‹ˆλ‹€. λ§Œμ•½ img κ°€ null 이 μ•„λ‹ˆλ©΄ uri λ₯Ό 톡해 item 이미지λ₯Ό μ„€μ •ν•©λ‹ˆλ‹€.

else 일 λ•ŒλŠ” μž„μ˜ 이미지 λ¦¬μ†ŒμŠ€λ₯Ό μ„€μ •ν•΄μ€λ‹ˆλ‹€.

μ €λŠ” 이 μ½”λ“œλ₯Ό

 holder.itemView.photo.setImageResource(R.drawable.ic_launcher_background)

둜 λ°”κΎΈκ³  ν…ŒμŠ€νŠΈλ₯Ό ν•΄λ³΄μ•˜μŠ΅λ‹ˆλ‹€.
μ½”λ“œμƒμœΌλ‘œ 보면 κ·Έλƒ₯ μž„μ˜ μ‚¬μ§„λ“€λ‘œ 이루어진 RecyclerView λ¦¬μŠ€νŠΈκ°€ λ§Œλ“€μ–΄μ§ˆ κ²ƒμž…λ‹ˆλ‹€.

κ²°κ³ΌλŠ” 앱이 5μ΄ˆμ•ˆμ— μΌœμ§€κ³  리슀트λ₯Ό λ³Ό λ•Œλ„ 앱이 잘 ꡬ동 λ˜λŠ” κ²ƒμ΄μ—ˆμŠ΅λ‹ˆλ‹€!

holder.itemView.photo.imageURI = Uri.parse(albumList[position])

문제의 μ½”λ“œλŠ” μœ„μ—μ˜ μ½”λ“œμ˜€μŠ΅λ‹ˆλ‹€. uri 둜만 이미지λ₯Ό μ„€μ •ν•΄μ„œλŠ” μ•ˆλ˜κ³ 
λ¬΄μ—‡μΈκ°€μ˜ μ²˜λ¦¬κ°€ ν•„μš”ν•˜λ‹€λŠ” 것을 μ•Œκ²Œ λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

μ €λŠ” ImageView κ°€ 이미지λ₯Ό 보여쀄 λ•Œ, Bitmap 을 ν†΅ν•΄μ„œ λ³΄μ—¬μ€€λ‹€λŠ” 것을 λ– μ˜¬λ ΈμŠ΅λ‹ˆλ‹€.
μ €λŠ” μ œκ°€ 가지고 μžˆλŠ” μ‚¬μ§„λ“€μ˜ 파일 μš©λŸ‰μ„ λ³΄μ•˜μŠ΅λ‹ˆλ‹€.
ν™”μ§ˆμ΄ μ’‹μ•„μ„œ κ·ΈλŸ°μ§€ 70 MB 이상 μ΄μ—ˆμŠ΅λ‹ˆλ‹€.

μ €λŠ” μ‚¬μ§„λ“€μ˜ μš©λŸ‰μ„ 보고 ν™•μ‹  ν–ˆμŠ΅λ‹ˆλ‹€.
'이 λ¬Έμ œλŠ” ImageView κ°€ 사진듀을 보여쀄 λ•Œ, μ‚¬μ§„λ“€μ˜ Bitmap 크기가 λ„ˆλ¬΄ μ»€μ„œ λ©”λͺ¨λ¦¬λ₯Ό 많이 λ¨ΉλŠ” κ²ƒμ΄κ΅¬λ‚˜.'

μ €λŠ” μ‚¬μ§„λ“€μ˜ Bitmap 크기λ₯Ό μ΅œμ ν™” μ‹œν‚¬ 방법을 찾기둜 ν–ˆμŠ΅λ‹ˆλ‹€.

3. λΉ„νŠΈλ§΅ μ΅œμ ν™” ν•˜κΈ°

μ €μ—κ²Œ 방법은 2κ°€μ§€λ‘œ λ‚˜λ‰˜μ—ˆμŠ΅λ‹ˆλ‹€.

  • Glide λ‚˜ Picaso 같은 라이브러리둜 λΉ„νŠΈλ§΅ 크기 쀄이기
  • Android λ¬Έμ„œ λ³΄λ©΄μ„œ 직접 λΉ„νŠΈλ§΅ 크기 쀄이기

μ €λŠ” λΉ„νŠΈλ§΅μ„ 쀄이기 μœ„ν•΄ 라이브러리λ₯Ό μ“°λŠ” 것은 λΉ„νš¨μšΈμ μ΄λΌκ³  μƒκ°ν•˜μ—¬ 직접 λΉ„νŠΈλ§΅ 크기λ₯Ό μ€„μ΄κΈ°λ‘œ ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

    private fun decodeSampledBitmapFromURI(path: String, reqWidth: Int, reqHeight: Int): Bitmap? {
        return BitmapFactory.Options().run {
            inJustDecodeBounds = true
            BitmapFactory.decodeFile(path, this)

            inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight)
            inJustDecodeBounds = false
            BitmapFactory.decodeFile(path, this)
        }
    }

μ €λŠ” μœ„μ— μ½”λ“œ 처럼 μ‚¬μ§„μ˜ PATH URI λ₯Ό BitmapFactory λ₯Ό 톡해 Bitmap 으둜 λ³€ν™˜κ³Ό λ™μ‹œμ— Bitmap 의 크기λ₯Ό μ΅œμ ν™” μ‹œν‚€λŠ” ν•¨μˆ˜λ₯Ό λ§Œλ“€μ—ˆμŠ΅λ‹ˆλ‹€.

이 μ½”λ“œμ—μ„œ μ΅œμ ν™”λ₯Ό λ‹΄λ‹Ήν•œ ν•¨μˆ˜λŠ” calculateInSampleSize μž…λ‹ˆλ‹€.
μ½”λ“œλŠ” μ•„λž˜μ™€ κ°™μŠ΅λ‹ˆλ‹€.

    private fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
        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

            while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
                inSampleSize *= 2
            }
        }
        return inSampleSize
    }

μ•„λž˜ μ½”λ“œλŠ” 적용 μ‹œν‚¨ κ²ƒμž…λ‹ˆλ‹€.

val img = decodeSampledBitmapFromURI(albumList[position], 200, 200)

if (img != null) holder.itemView.photo.setImageBitmap(img)
else holder.itemView.photo.setImageResource(R.drawable.ic_launcher_background)

μ μš©ν•˜λ‹ˆ 정말 잘 λŒμ•„κ°‘λ‹ˆλ‹€!