Single bitmap pool with view size changes support

Summary:
This change aims to reduce the number of bitmap pools by definiting a default pool which can contain bitmaps of different sizes.

A side benefit is that this single pool facilitates handling the case where a view gets resized too.

So overall, this should be both more generic and efficient compared to the previous approach.

Reviewed By: LukeDefeo

Differential Revision: D39815821

fbshipit-source-id: e0aa17ba55db07b74d8f22ea16e0c864288fb169
This commit is contained in:
Lorenzo Blasa
2022-09-27 13:00:04 -07:00
committed by Facebook GitHub Bot
parent 945e26d0f1
commit fadaf26f1e
6 changed files with 52 additions and 38 deletions

View File

@@ -45,6 +45,7 @@ class UIDebuggerFlipperPlugin(
override fun onConnect(connection: FlipperConnection) { override fun onConnect(connection: FlipperConnection) {
Log.i(LogTag, "Connected") Log.i(LogTag, "Connected")
this.context.connectionRef.connection = connection this.context.connectionRef.connection = connection
this.context.bitmapPool.makeReady()
connection.send( connection.send(
InitEvent.name, InitEvent.name,
@@ -59,6 +60,7 @@ class UIDebuggerFlipperPlugin(
Log.i(LogTag, "Disconnected") Log.i(LogTag, "Disconnected")
context.treeObserverManager.stop() context.treeObserverManager.stop()
context.bitmapPool.recycleAll()
} }
override fun runInBackground(): Boolean { override fun runInBackground(): Boolean {

View File

@@ -11,42 +11,58 @@ import android.graphics.Bitmap
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
class BitmapPool( /** BitmapPool is intended to be used on the main thread. In other words, it is not thread-safe. */
private val width: Int, class BitmapPool(private val config: Bitmap.Config = Bitmap.Config.RGB_565) {
private val height: Int,
private val config: Bitmap.Config = Bitmap.Config.RGB_565
) {
interface RecyclableBitmap { interface ReusableBitmap {
val bitmap: Bitmap? val bitmap: Bitmap?
fun recycle() fun readyForReuse()
} }
private var handler: Handler = Handler(Looper.getMainLooper()) private var handler: Handler = Handler(Looper.getMainLooper())
private val bitmaps: MutableList<Bitmap> = mutableListOf() private val container: MutableMap<String, MutableList<Bitmap>> = mutableMapOf()
private var isRecycled = false private var isRecycled = false
fun recycle() { private fun generateKey(width: Int, height: Int): String = "$width,$height"
fun recycleAll() {
isRecycled = true isRecycled = true
container.forEach { (_, bitmaps) ->
bitmaps.forEach { bitmap -> bitmap.recycle() } bitmaps.forEach { bitmap -> bitmap.recycle() }
bitmaps.clear() bitmaps.clear()
} }
fun getBitmap(): RecyclableBitmap { container.clear()
return if (bitmaps.isEmpty()) { }
fun makeReady() {
isRecycled = false
}
fun getBitmap(width: Int, height: Int): ReusableBitmap {
val key = generateKey(width, height)
val bitmaps = container[key]
return if (bitmaps == null || bitmaps.isEmpty()) {
LeasedBitmap(Bitmap.createBitmap(width, height, config)) LeasedBitmap(Bitmap.createBitmap(width, height, config))
} else { } else {
LeasedBitmap(bitmaps.removeLast()) LeasedBitmap(bitmaps.removeLast())
} }
} }
inner class LeasedBitmap(override val bitmap: Bitmap) : RecyclableBitmap { inner class LeasedBitmap(override val bitmap: Bitmap) : ReusableBitmap {
override fun recycle() { override fun readyForReuse() {
val key = generateKey(bitmap.width, bitmap.height)
handler.post { handler.post {
if (isRecycled) { if (isRecycled) {
bitmap.recycle() bitmap.recycle()
} else { } else {
var bitmaps = container[key]
if (bitmaps == null) {
bitmaps = mutableListOf()
container[key] = bitmaps
}
bitmaps.add(bitmap) bitmaps.add(bitmap)
} }
} }
@@ -54,10 +70,6 @@ class BitmapPool(
} }
companion object { companion object {
fun createBitmap(width: Int, height: Int, config: Bitmap.Config): Bitmap {
return Bitmap.createBitmap(width, height, config)
}
fun createBitmapWithDefaultConfig(width: Int, height: Int): Bitmap { fun createBitmapWithDefaultConfig(width: Int, height: Int): Bitmap {
return Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565) return Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)
} }

View File

@@ -8,6 +8,7 @@
package com.facebook.flipper.plugins.uidebugger.core package com.facebook.flipper.plugins.uidebugger.core
import com.facebook.flipper.core.FlipperConnection import com.facebook.flipper.core.FlipperConnection
import com.facebook.flipper.plugins.uidebugger.common.BitmapPool
import com.facebook.flipper.plugins.uidebugger.descriptors.DescriptorRegister import com.facebook.flipper.plugins.uidebugger.descriptors.DescriptorRegister
import com.facebook.flipper.plugins.uidebugger.observers.TreeObserverFactory import com.facebook.flipper.plugins.uidebugger.observers.TreeObserverFactory
import com.facebook.flipper.plugins.uidebugger.observers.TreeObserverManager import com.facebook.flipper.plugins.uidebugger.observers.TreeObserverManager
@@ -23,6 +24,8 @@ data class Context(
PartialLayoutTraversal(descriptorRegister, observerFactory) PartialLayoutTraversal(descriptorRegister, observerFactory)
val treeObserverManager = TreeObserverManager(this) val treeObserverManager = TreeObserverManager(this)
val bitmapPool = BitmapPool()
} }
data class ConnectionRef(var connection: FlipperConnection?) data class ConnectionRef(var connection: FlipperConnection?)

View File

@@ -40,10 +40,13 @@ class DecorViewObserver(val context: Context) : TreeObserver<DecorView>() {
val throttledUpdate = val throttledUpdate =
throttleLatest<WeakReference<View>?>(throttleTimeMs, waitScope, mainScope) { weakView -> throttleLatest<WeakReference<View>?>(throttleTimeMs, waitScope, mainScope) { weakView ->
if (node.width > 0 && node.height > 0) { weakView?.get()?.let { view ->
bitmapPool = BitmapPool(node.width, node.height) var snapshotBitmap: BitmapPool.ReusableBitmap? = null
if (view.width > 0 && view.height > 0) {
snapshotBitmap = context.bitmapPool.getBitmap(node.width, node.height)
}
processUpdate(context, view, snapshotBitmap)
} }
weakView?.get()?.let { view -> processUpdateWithSnapshot(context, view) }
} }
listener = listener =
@@ -68,9 +71,6 @@ class DecorViewObserver(val context: Context) : TreeObserver<DecorView>() {
} }
nodeRef = null nodeRef = null
bitmapPool?.recycle()
bitmapPool = null
} }
} }

View File

@@ -31,7 +31,6 @@ import com.facebook.flipper.plugins.uidebugger.descriptors.nodeId
abstract class TreeObserver<T> { abstract class TreeObserver<T> {
protected val children: MutableMap<Int, TreeObserver<*>> = mutableMapOf() protected val children: MutableMap<Int, TreeObserver<*>> = mutableMapOf()
protected var bitmapPool: BitmapPool? = null
abstract val type: String abstract val type: String
@@ -39,12 +38,12 @@ abstract class TreeObserver<T> {
abstract fun unsubscribe() abstract fun unsubscribe()
fun processUpdate(context: Context, root: Any) = this.processUpdate(context, root, false)
fun processUpdateWithSnapshot(context: Context, root: Any) =
this.processUpdate(context, root, true)
/** Traverses the layout hierarchy while managing any encountered child observers. */ /** Traverses the layout hierarchy while managing any encountered child observers. */
fun processUpdate(context: Context, root: Any, takeSnapshot: Boolean) { fun processUpdate(
context: Context,
root: Any,
snapshotBitmap: BitmapPool.ReusableBitmap? = null
) {
val startTimestamp = System.currentTimeMillis() val startTimestamp = System.currentTimeMillis()
val (visitedNodes, observableRoots) = context.layoutTraversal.traverse(root) val (visitedNodes, observableRoots) = context.layoutTraversal.traverse(root)
@@ -82,20 +81,18 @@ abstract class TreeObserver<T> {
Log.d(LogTag, "For Observer ${this.type} Sending ${visitedNodes.size}") Log.d(LogTag, "For Observer ${this.type} Sending ${visitedNodes.size}")
var recyclableBitmap: BitmapPool.RecyclableBitmap? = null if (snapshotBitmap != null) {
if (takeSnapshot && bitmapPool != null) {
@Suppress("unchecked_cast") @Suppress("unchecked_cast")
val descriptor = val descriptor =
context.descriptorRegister.descriptorForClassUnsafe(root::class.java) context.descriptorRegister.descriptorForClassUnsafe(root::class.java)
as NodeDescriptor<Any> as NodeDescriptor<Any>
recyclableBitmap = bitmapPool?.getBitmap() descriptor.getSnapshot(root, snapshotBitmap.bitmap)
descriptor.getSnapshot(root, recyclableBitmap?.bitmap)
} }
val endTimestamp = System.currentTimeMillis() val endTimestamp = System.currentTimeMillis()
context.treeObserverManager.enqueueUpdate( context.treeObserverManager.enqueueUpdate(
SubtreeUpdate( SubtreeUpdate(
type, root.nodeId(), visitedNodes, startTimestamp, endTimestamp, recyclableBitmap)) type, root.nodeId(), visitedNodes, startTimestamp, endTimestamp, snapshotBitmap))
} }
fun cleanUpRecursive() { fun cleanUpRecursive() {

View File

@@ -31,7 +31,7 @@ data class SubtreeUpdate(
val nodes: List<Node>, val nodes: List<Node>,
val startTime: Long, val startTime: Long,
val traversalCompleteTime: Long, val traversalCompleteTime: Long,
val snapshot: BitmapPool.RecyclableBitmap? val snapshot: BitmapPool.ReusableBitmap?
) )
/** Holds the root observer and manages sending updates to desktop */ /** Holds the root observer and manages sending updates to desktop */
@@ -87,7 +87,7 @@ class TreeObserverManager(val context: Context) {
treeUpdate.nodes, treeUpdate.nodes,
snapshot)) snapshot))
treeUpdate.snapshot.recycle() treeUpdate.snapshot.readyForReuse()
} }
val serializationEnd = System.currentTimeMillis() val serializationEnd = System.currentTimeMillis()