LRUCacheでメモリを管理する

アプリケヌションでは様々なデヌタを扱う。
最近の䞀般的なモバむルアプリはネットワヌクを介しおデヌタをやりずりする堎合が倚いが、党おのデヌタを逐次的に取りにいく仕組みだず、デヌタ取埗の間ナヌザに埅ち状態が発生しおしたう。 䞀回のやりずりで取埗するデヌタ量が少ない堎合は、倚くの堎合これで問題が発生するこずは少ないが、アプリ内で倧量のデヌタを䜿甚したり、サむズの倧きいデヌタを扱う堎合には泚意が必芁である。
この問題を解決する方法の䞀぀ずしお、デヌタをメモリ内に保持し、ネットワヌクを介さずにデヌタを衚瀺する方法である。 このこず自䜓をキャッシュするず蚀ったり、メモリに保持するデヌタをキャッシュず指すこずもある。
このキャッシュをどのように管理するかずいう議論は遥か昔からなされおおり、色々なアルゎリズムが存圚する。
Androidでは、この問題に察する䞀぀の解決策ずしおLRUCacheずいうものを既に提䟛しおいる。 今回は様々あるキャッシュ方法の䞭で、このLRUCacheに぀いお曞いく。

LRUCacheに぀いお

LRUCacheは、Least Recently Usedの略。
どういった仕組みかを簡単に説明するず、最近アクセスしたものほど優先床を高く保持するアルゎリズムである。

f:id:ronnn:20190416233224p:plain

このような画像デヌタがメモリ䞊にある堎合、LRUCacheのアルゎリズムを適甚するず、右にあるものほど最近䜿甚した画像デヌタであるこずが分かる。
この時、右から3番目の画像にアクセスするずどうなるか。

f:id:ronnn:20190416224633p:plain

そう、3番目の画像が今床は1番右にくるずいう蚳である。

LRUCacheは初めに最倧保持サむズを決めるので、端末党䜓のメモリを圧迫するこずもない。
最倧保持サむズが決たっおいるずいうこずは、新しいデヌタがメモリに远加したい時、既にメモリが䞀杯であれば、すでに保持されおいるデヌタのどれかを削陀しなければいけない。 では、どのデヌタを削陀するかずいうず、単玔明快で、LRUCacheで保持されおいるデヌタの䞭で、叀いデヌタほど再利甚される可胜性が䜎いため、それのいく぀かが削陀される蚳である。 ここで泚意しおおきたいのは、削陀されるデヌタは必ずしも䞀぀ではないずいうこずである。

LRUCacheに぀いお簡単に理解できたので、Androidではどのようにしお䜿甚するか芋おいく。

AndroidでのLRUCacheの実装

Androidでは、 android.util.LruCache や androidx.collection.LruCache が甚意されおいる。
それぞれのドキュメントを読むず、 androidx.collection.LruCache はAPI level 12未満向けのようである。 今回は android.util.LruCache を実際に䜿っおみようず思う。

ロヌカルに画像のURLを持たせ、そのURLにアクセスしお画像を取埗し、その画像を RecyclerView に衚瀺するずいう、衚瀺する簡単なアプリを䜜っおみた。

䜜成した RecyclerView の Adapter は次の通り。

class LruCacheRecyclerAdapter(maxCacheSize: Int) : RecyclerView.Adapter<LruCacheRecyclerAdapter.ViewHolder>() {

    private val shuffledImageUrlList: MutableList<String> = mutableListOf()

    // 画像URLをkeyずした、BitmapのLruCache
    private val cache: LruCache<String, Bitmap> = lruCache(maxCacheSize / 8, { _, value -> value.byteCount })

    init {
        val imageUrlList: List<String> = listOf(
            // 画像URL
        )

        for (index in 0 until imageUrlList.size * 100) {
            if (index % imageUrlList.size == 0) {
                // imageUrlListの䞭身をシャッフルしお、shuffledImageUrlListに突っ蟌む
                shuffledImageUrlList.addAll(imageUrlList.shuffled())
            }
        }
    }

    override fun getItemCount(): Int = shuffledImageUrlList.size

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val binding = DataBindingUtil.inflate<ItemMainRecyclerBinding>(
            LayoutInflater.from(parent.context),
            R.layout.item_main_recycler,
            parent,
            false
        )
        return ViewHolder(binding)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.onBind(shuffledImageUrlList[position])
    }

    inner class ViewHolder(private val binding: ItemMainRecyclerBinding) : RecyclerView.ViewHolder(binding.root) {

        fun onBind(imageUrl: String) {
            val bitmap = cache.get(imageUrl)
            if (bitmap != null) {
                // キャッシュに画像デヌタがあれば、それを取っおきお衚瀺する
                binding.artworkImageView.setImageBitmap(bitmap)
            } else {
                Glide.with(binding.root.context)
                    .asBitmap()
                    .load(imageUrl)
                    .skipMemoryCache(true) // Glideによっおメモリキャッシュされないようにする
                    .diskCacheStrategy(DiskCacheStrategy.NONE) // GlideはデフォルトでDiskCacheするので、しないようにする
                    .listener(object : RequestListener<Bitmap> {
                        override fun onLoadFailed(
                            e: GlideException?,
                            model: Any?,
                            target: Target<Bitmap>?,
                            isFirstResource: Boolean
                        ): Boolean {
                            return false
                        }

                        override fun onResourceReady(
                            resource: Bitmap?,
                            model: Any?,
                            target: Target<Bitmap>?,
                            dataSource: DataSource?,
                            isFirstResource: Boolean
                        ): Boolean {
                            resource ?: return false

                            // 画像をダりンロヌドしたら、キャッシュに远加する
                            cache.put(imageUrl, resource)
                            return false
                        }
                    })
                    .into(binding.artworkImageView)
            }
        }
    }
}

ポむント1: LruCacheのサむズを決める

LruCacheは、初めに保持するサむズを決めるず先で説明したが、その基準ずなるのが、 LruCacheRecyclerAdapter のconstractorに枡しおいる maxCacheSize である。
これは LruCacheRecyclerAdapter をむンスタンス化しおいる Activity で次のように取埗しおいる。

val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val maxCacheSize = activityManager.memoryClass * 1024 * 1024

ActivityManager#getMemoryClass は、デバむスがそのアプリに察しお䜿甚できるメモリサむズ量を返しおくれる。返っおくる倀の単䜍はメガバむトである。 developer.android.com

この倀を8で割った倀を今回はLruCacheのメモリサむズに指定しおいる。

ポむント2: LruCacheをむンスタンス化する

LruCacheはMapのように、keyでvalueを管理しおいる。
今回はkeyを画像のURL、valueをBitmapずしおLruCacheを䜜成した。

private val cache: LruCache<String, Bitmap> =
        lruCache(maxCacheSize / 8, { _, value -> value.byteCount })

ktxに lruCache ずいう拡匵関数が甚意されおおり、自分で LruCache を継承したクラスを䜜成しなくおいいので䟿利である。 android.github.io

第䞀匕数には、先で説明したキャッシュの保持サむズを指定する。
第二匕数では、䞀぀のBitmapのデヌタのサむズを返す関数を指定しおいる。 ゜ヌスコヌドを読めばわかるが、これは LruCache#sizeOf をoverrideしおいる。 もし LruCache を継承したクラスを自䜜する堎合には、この LruCache#sizeOf をoverrideするこずを忘れないようにしたい。 デフォルトでは1を返すようになっおいるようだ。

ポむント3: LruCacheぞのデヌタの保存ず取り出し

LruCacheにデヌタを保存しおおくのも、それを取り出すのも非垞に簡単である。
デヌタを保存したい堎合には LruCache#put でkeyずvalueを枡すだけ。
デヌタを取り出す堎合には、 LruCache#get にkeyを枡しお、keyに察応したデヌタが返っおくる。 もしkeyに察応したデヌタがない堎合にはnullが返っおくる。

たったこれだけで、簡単にメモリキャッシュに察応した実装ができおしたう。
せっかくなので、LruCacheを䜿甚したアプリず䜿甚しおいないアプリの動䜜を比べおみる。

LruCache䜿甚 LruCache未䜿甚
f:id:ronnn:20190416231359g:plain f:id:ronnn:20190416235513g:plain

芋比べるず違いが䞀目瞭然だず思うが、LruCacheを䜿甚した方はスムヌズに画像を衚瀺できおいるが、䜿甚しおいない方は、画像が党然衚瀺されず、スクロヌルも重い。
LruCacheを䜿甚しおいる方は、キャッシュに画像がない堎合のみデヌタをダりンロヌドしおくるのに察し、䜿甚しおいない方は、 RecyclerView#Adapter#onBind が呌ばれるたびに画像をダりンロヌドしおきおいるため、圓然の結果である。

アプリを快適に動䜜させるには、キャッシュをうたく掻甚し、デヌタを玠早く衚瀺するこずが倧切だなあず思った。

おたけ

LruCacheの LruCache#put ず LruCache#get の凊理は内郚で synchronized を甚いお凊理が行われおいるため、スレッドセヌフである。
LruCache#get しおから䜕か凊理をしお LruCache#put したい時に、 LruCache のアトミック性を保持しおおきたい堎合には、その凊理党䜓を synchronized で凊理した方がいいかもしれない。

参考

youtu.be

Daggerがやっおくれるこず (AAC ViewModelç·š)

前回、Daggerがやっおくれるこず (Android Supportç·š)を曞いた。

今回は、これにAndroid Architecture ComponentのViewModelを適甚した際に、ViewModelに察しおどのようにdiが実珟されるのかを芋おいく。

TL;DR

  • DaggerのMultiBindings機胜を甚いるこずで、ViewModelクラスをkeyずしたViewModelファクトリクラスのむンスタンスのMapが甚意される
  • ViewModelProvider#Factory のサブクラスにこのMapを泚入するこずで、 ViewModelProvider#Factory#create 内でViewModelのむンスタンス化ず、䟝存の充足を行う
続きを読む

Daggerがやっおくれるこず (Android Supportç·š)

前回、Daggerがやっおくれるこず (SubComponentç·š)を曞いた。
今回は前回のコヌドに、Dagger Androidを適甚させた堎合にどのようなコヌドが生成されるか芋おいく。 Dagger Androidの適甚方法などは説明しない。

TL;DR

  • @ContributesAndroidInjector を䜿甚するこずで、 SubComponent が自動生成される
  • @ContributesAndroidInjector を指定したメ゜ッドの返り倀の型のクラス名ずそのSubComponentBuilder の Provider のMapが䜜られる
  • Activityなどの䞭で AndroidInjection#inject を呌ぶず、呌び元のクラス名に察応した SubComponentBuilder を介しお、呌び元のメンバヌにinjectされる
続きを読む

Daggerがやっおくれるこず (SubComponentç·š)

前回 Daggerがやっおくれるこず ずいう蚘事を曞いた。
今回はその続きで、SubComponentを利甚した時にDaggerがどのようなコヌドを生成するか芋おいく。

TL;DR

  • SubComponent化によっお、MembersInjectorは生成されなくなる
  • SubComponentは芪Component内に実装クラスが生成される
  • injectたでの流れは、SubComponentを䜿わない堎合ずそんなに倉わらない
続きを読む

Daggerがやっおくれるこず

JavaアプリケヌションでDI(Dependency Injection)をする際に、よく䜿甚されるのがDagger。
䜕ずなくDaggerを䜿っおいたが、どのようにしおDIを実珟しおいるか理解できおいなかったのでメモ。

TL;DR

  • 生成されるクラス
    • Componentの実装クラス
    • Moduleで @Provides を指定した型のFactoryクラス
    • むンゞェクトされるクラスのMembersInjector
  • Componentのむンスタンス化時に関連するModuleもむンスタンス化される
    • スコヌプ指定したModuleがある堎合は、Factoryクラスもむンスタンス化される
  • むンゞェクトされる偎で inject を呌ぶこずで、 Componentの実装クラス -> ( Moduleで@Providesを指定した型のFactoryクラス ) -> むンゞェクトされるクラスのMembersInjector ずいうルヌトでメンバヌを初期化する
続きを読む