likes
comments
collection
share

「谈谈设计模式」之享元模式:优化内存

作者站长头像
站长
· 阅读数 19

「谈谈设计模式」之享元模式:优化内存

定义

  享元模式是一种结构型设计模式, 它摒弃了在每个对象中保存所有数据的方式, 通过共享多个对象所共有的相同状态, 让你能在有限的内存容量中载入更多对象。

实现

实例

  上面的定义看着有点懵?没关系,我们用一个例子来逐渐向这个模式进行演化。这里我们以一个游戏中的基础物体对象的表示为例:

游戏中有大片草坪需要渲染,假如一片草坪中有1w个单位的草块对象,每个草坪的渲染最终由 position:位置,vector:表示角度的三维向量,texture:渲染的纹理(占用大量内存)等决定。下面是草块对象的定义:

// GrassBlock.kt
data class GrassBlock(
    val position: Pair<Float, Float>, // 24 bytes
    val vector: Triple<Float, Float, Float>, // 32bytes
    val texture: Texture // big object: 100k bytes
)

  每个属性后面分别标注了其占用的内存大小,分别为 24,32,100k bytes,Texuture 并未真实实现,所以这里为预期占用。回到我们的例子,如果我们有 1w 个单位的草块需要同时渲染,则占用的内存在 100k * 1w (bytes) = 1g,光是草坪就需要如此庞大的内存占用。

// FlyingWeight.kt
private fun getGrassBlocks() {
    val grassBlocks = mutableListOf<GrassBlock>()
    repeat(10_000) {
        grassBlocks.add(
            GrassBlock(
                Pair(Random.nextFloat(), Random.nextFloat()),
                Triple(Random.nextFloat(), Random.nextFloat(), Random.nextFloat()),
                Texture()
            )
        )
    }
}

// cost about 1GB
// random represent a specific real value

  但草坪的样式其实并非无穷多种,在我们的示例中,只有 10 种不同的样式,在10 种之间相互组合变换,下面我们就利用这点来做优化。

优化

// FlyingWeight.kt
private fun getGrassBlockBySharedTexture() {
    // 1. prepare all texture
    val textures = mutableListOf<Texture>()
    repeat(10) {
        textures.add(Texture(it.toString()))
    }

    val grassBlocks = mutableListOf<GrassBlock>()
    repeat(10_000) {
        grassBlocks.add(
            GrassBlock(
                Pair(Random.nextFloat(), Random.nextFloat()),
                Triple(Random.nextFloat(), Random.nextFloat(), Random.nextFloat()),
                textures.random() // 2. get texture from cache
            )
        )
    }
}

// cost about (32 + 24) * 1w + (16 * 1w) + (100k * 10) = 1720k ~= 1.7MB

  上面这种在不同的外部 GrassBlock 对象中共享相同的内部 Texture 对象的方式使得我们的内存占用从 1 G 直接降到了 1.7MB,巨大的进步。这种方式已经使得我们获得了巨大的内存占用优化,但我们还是可以让这个过程更加规范,从而形成一种模式

模式

// FlyingWeight.kt
private fun getGrassBlockByFlyingWeight() {
    // 1. prepare all texture
    val flyingWeightGrassWeightFactory = FlyingWeightGrassWeightFactory()

    val grassBlocks = mutableListOf<GrassBlock>()
    repeat(10_000) {
        grassBlocks.add(
            flyingWeightGrassWeightFactory.getGrossWeight(
                Pair(Random.nextFloat(), Random.nextFloat()),
                Triple(Random.nextFloat(), Random.nextFloat(), Random.nextFloat()),
                "${it % 10}" // 2. pass the name of texture
            )
        )
    }
}

// FlyingWeightGrassWeightFactory.kt
class FlyingWeightGrassWeightFactory {
    private val textureMap = mutableMapOf<String, Texture>()

    init {
        repeat(10) { textureMap[it.toString()] = Texture() }
    }

    fun getGrossWeight(
        position: Pair<Float, Float>,
        vector: Triple<Float, Float, Float>,
        nameOfTexture: String
    ): GrassBlock =
        GrassBlock(position, vector, textureMap[nameOfTexture]!!)
}

  在上面的示例代码中,我们把关于 texture 的 cache 挪到了 FlyingWeightGrassWeightFactory 中。并且不再直接创建 GrassBlock,而是利用 FlyingWeightGrassWeightFactory 来创建,并且传入的参数也发生了变化,对于 Texure,我们只需传入一个 name。

  事情貌似变得更复杂了,那么为此我们得到了什么好处?好处便是我们屏蔽了用户手动创建 GrassBlock 而带来的问题,我们不再可能会忘记需要从一个缓存处获取 Texure 。上面的模式便是享元模式,可以看到这个模式跟池化技术并不相同,那它们之间有什么关系呢?

与池化的关系

  如果读者熟知 Android 中的 Handler 机制,那么可能会认为其中的 Message 复用机制就是享元模式。但其实两者并不相同,Message 的复用机制可以理解为一种池化复用机制,复用的基础单位是整个独立对象,而享元模式则不同,其复用的单位是对象内部的部分属性(这个属性本身可能是一个对象)。

  Message 复用的关键在于其内部属性的可变性(池化技术的实现方式各不相同,这里以Message的复用为例),当 Message 对象被回收时,其内部的所有属性会被恢复出厂设置以保证不会之前的对象属性不会干扰到后续的复用。实际上,早期 View 组件 ListView 列表的条目状态错乱问题即是由于状态擦除环节的问题引起的。

  而享元模式则是利用对象的不可变性,利用 Factory 控制,在不同对象之间复用了内部的属性,这些属性的特点便是不可变性,同时往往也占用大量内存,这也是复用的原因。

总结

享元模式和池化技术都是通过复用来优化内存消耗的技术,但两者使用场景有所不同。

池化技术的复用单位是整个独立对象。适用于频繁使用,但在同一时间段内不会大量同时出现的对象。

享元模式的复用单位是内部的属性。适用于优化必须同时大量出现的对象所占用的内存。

练习

上面模式部分的例子并不能完全保证内存利用率最优,改造上面的示例,保证内存利用率最优。提示:单例模式。

笔者的其它文章:
转载自:https://juejin.cn/post/7399273700103847986
评论
请登录