Diseño de software

Swift: patrón Object Pool, guía completa

Al crear una pantalla con muchos efectos de juego, llega un momento en que los fotogramas empiezan a entrecortarse.

4 min de lectura
Imagen de portada de Swift: patrón Object Pool, guía completa

Al crear una pantalla con muchos efectos de juego, llega un momento en que los fotogramas empiezan a entrecortarse.

Si creas continuamente objetos de vida corta, como balas o partículas, mediante init, verás que el gráfico de memoria salta como una sierra en Instruments.

En estos casos resulta útil el patrón Object Pool de Swift.

La idea es sencilla: un object pool evita crear objetos cada vez; los prepara, los presta y los recupera. A diferencia del patrón Flyweight, su propósito es distinto.

En este artículo veremos qué es un object pool, en qué se diferencia de Flyweight y cómo implementarlo en Swift.


¿Qué es el patrón Object Pool?

Un object pool crea por adelantado varios objetos cuyo coste de creación es alto y los guarda en un «pool».

En lugar de crear uno nuevo cuando se necesita, se toma uno del pool y se devuelve al terminar.

La clave es la reutilización. El objetivo es reducir el coste de asignar y liberar memoria al repetir init y deinit.

En una frase: «no lo crees; pídelo prestado y devuélvelo».

Resulta especialmente útil en situaciones como estas.

  • Objetos que se crean y destruyen masivamente en poco tiempo, como balas y partículas
  • Recursos cuya creación es pesada, como conexiones de red o hilos
  • Vistas que se sustituyen continuamente en pantalla, como la cola de reutilización de UITableViewCell

De hecho, si desarrollas para iOS, probablemente ya usabas object pools. dequeueReusableCell es precisamente el object pool que Apple creó.


¿En qué se diferencia de Flyweight?

Es fácil confundirlos porque ambos dan la impresión de «usar los objetos con moderación».

Pero su propósito es completamente distinto.

Object Pool reutiliza objetos en distintos momentos. Cuando devuelves esta bala, la siguiente ocupa su lugar.

Flyweight permite que varios lugares referencien simultáneamente un único objeto compartido. Por ejemplo, al dibujar 10 000 árboles, los datos comunes —color y textura— se almacenan una sola vez y solo se pasa aparte la posición.

La diferencia puede resumirse así.

Categoría Object Pool Flyweight
Objetivo Reducir el coste de creación y liberación Reducir el uso de memoria
Forma de reutilización Prestar y devolver (división temporal) Compartir simultáneamente (división espacial)
Estado Cada objeto conserva su estado propio Separar el estado compartido del externo
Ejemplos representativos Reutilización de celdas, partículas Glifos de fuentes, iconos de mapas

En resumen: Object Pool significa «reutilizar por turnos» y Flyweight, «compartir entre todos».

Como se presta y se devuelve, la estructura es más sencilla de lo esperado
Como se presta y se devuelve, la estructura es más sencilla de lo esperado

Cómo crear un Object Pool en Swift

La estructura es bastante simple: un array para los objetos disponibles y dos métodos para prestarlos y recuperarlos.

Este es un pool genérico sencillo.

final class ObjectPool<T> {
    private var available: [T] = []
    private let factory: () -> T

    init(factory: @escaping () -> T) { self.factory = factory }

    func acquire() -> T { available.popLast() ?? factory() }  // Crear uno nuevo si no existe
    func release(_ item: T) { available.append(item) }        // Devolverlo al terminar
}

acquire() extrae un objeto del pool si queda alguno; solo crea uno nuevo cuando está vacío.

Al devolverlo mediante release(), la siguiente solicitud reutiliza ese objeto.

Pedirlo, usarlo y devolverlo: eso es todo el ciclo
Pedirlo, usarlo y devolverlo: eso es todo el ciclo

Al aplicarlo a partículas, el flujo es el siguiente.

let pool = ObjectPool<Particle> { Particle() }

let p = pool.acquire()  // Pedirlo prestado del pool
p.reset(at: point)      // Es importante reiniciar el estado!
// ...Después de usarlo en pantalla
pool.release(p)         // Devolverlo

Hay algo que debes recordar: el objeto devuelto conserva su estado anterior.

Por eso, antes de prestarlo de nuevo, debes pasar por una inicialización como reset() para evitar datos fantasma.


¿Cuándo usar y cuándo evitar un object pool?

Usarlo indiscriminadamente puede resultar contraproducente.

Estos son los criterios que reuní tras probarlo directamente.

Se recomienda en estos casos.

  1. Cuando se crean y destruyen varias decenas de objetos por segundo o más
  2. Cuando crear un solo objeto tiene un coste claramente apreciable
  3. Cuando el gráfico de memoria presenta picos de sierra y se aprecia la carga de GC (recolección de basura) o ARC (Automatic Reference Counting, conteo automático de referencias)

En estos casos, piénsalo dos veces.

  • Si son objetos ligeros que solo se crean una o dos veces ocasionalmente, gestionar el pool cuesta más
  • Si olvidas devolverlos, el pool se vacía y acabarás creándolos de nuevo cada vez
  • En entornos multihilo necesitas un bloqueo o sincronización de la cola para acceder al pool

Swift usa muchos tipos por valor (struct) y ARC es bastante eficiente, así que no hace falta introducir un pool siempre.

Recomiendo medir primero con Instruments e introducirlo solo cuando se confirme el cuello de botella.

Tras medirlo, el gráfico de memoria quedó claramente más estable
Tras medirlo, el gráfico de memoria quedó claramente más estable

Object Pool es un patrón de reutilización que ahorra costes de creación; Flyweight es un patrón de compartición que ahorra memoria.

Si tienes clara la diferencia, podrás elegir el patrón adecuado para cada situación.

Mide primero e introdúcelo cuando haga falta. Espero que experimentes ese momento en que los fotogramas se vuelven mucho más fluidos.

Artículos relacionados