fix: Improve Android resource efficiency/cleanup (use class members for CoroutineScope and FrameProcessorThread) (#335)

* fix: Use custom CoroutineScope

* fix: Use custom `CoroutineScope`

* Make `frameProcessorThread` and `coroutineScope` instance variables

* Update VisionCameraScheduler.java

* Remove `HybridData::resetNative()` calls

they're called by a Java GC destructor anyways.

* Update CameraViewManager.kt

* Update CameraView.kt
This commit is contained in:
Marc Rousavy 2021-08-25 11:33:57 +02:00 committed by GitHub
parent c7fb89170e
commit ff5a8b8900
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 107 additions and 96 deletions

View File

@ -11,6 +11,6 @@ class CameraPackage : ReactPackage {
} }
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> { override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
return listOf(CameraViewManager()) return listOf(CameraViewManager(reactContext))
} }
} }

View File

@ -27,6 +27,7 @@ import com.mrousavy.camera.utils.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.guava.await import kotlinx.coroutines.guava.await
import java.lang.IllegalArgumentException import java.lang.IllegalArgumentException
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors import java.util.concurrent.Executors
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@ -60,8 +61,16 @@ import kotlin.math.min
// TODO: takePhoto() return with jsi::Value Image reference for faster capture // TODO: takePhoto() return with jsi::Value Image reference for faster capture
@Suppress("KotlinJniMissingFunction") // I use fbjni, Android Studio is not smart enough to realize that. @Suppress("KotlinJniMissingFunction") // I use fbjni, Android Studio is not smart enough to realize that.
@SuppressLint("ClickableViewAccessibility") // suppresses the warning that the pinch to zoom gesture is not accessible @SuppressLint("ClickableViewAccessibility", "ViewConstructor")
class CameraView(context: Context) : FrameLayout(context), LifecycleOwner { class CameraView(context: Context, private val frameProcessorThread: ExecutorService) : FrameLayout(context), LifecycleOwner {
companion object {
const val TAG = "CameraView"
const val TAG_PERF = "CameraView.performance"
private val propsThatRequireSessionReconfiguration = arrayListOf("cameraId", "format", "fps", "hdr", "lowLightBoost", "photo", "video", "enableFrameProcessor")
private val arrayListOfZoom = arrayListOf("zoom")
}
// react properties // react properties
// props that require reconfiguring // props that require reconfiguring
var cameraId: String? = null // this is actually not a react prop directly, but the result of setting device={} var cameraId: String? = null // this is actually not a react prop directly, but the result of setting device={}
@ -95,6 +104,7 @@ class CameraView(context: Context) : FrameLayout(context), LifecycleOwner {
private val cameraExecutor = Executors.newSingleThreadExecutor() private val cameraExecutor = Executors.newSingleThreadExecutor()
internal val takePhotoExecutor = Executors.newSingleThreadExecutor() internal val takePhotoExecutor = Executors.newSingleThreadExecutor()
internal val recordVideoExecutor = Executors.newSingleThreadExecutor() internal val recordVideoExecutor = Executors.newSingleThreadExecutor()
private var coroutineScope = CoroutineScope(Dispatchers.Main)
internal var camera: Camera? = null internal var camera: Camera? = null
internal var imageCapture: ImageCapture? = null internal var imageCapture: ImageCapture? = null
@ -172,7 +182,6 @@ class CameraView(context: Context) : FrameLayout(context), LifecycleOwner {
scaleGestureDetector = ScaleGestureDetector(context, scaleGestureListener) scaleGestureDetector = ScaleGestureDetector(context, scaleGestureListener)
touchEventListener = OnTouchListener { _, event -> return@OnTouchListener scaleGestureDetector.onTouchEvent(event) } touchEventListener = OnTouchListener { _, event -> return@OnTouchListener scaleGestureDetector.onTouchEvent(event) }
hostLifecycleState = Lifecycle.State.INITIALIZED hostLifecycleState = Lifecycle.State.INITIALIZED
lifecycleRegistry = LifecycleRegistry(this) lifecycleRegistry = LifecycleRegistry(this)
reactContext.addLifecycleEventListener(object : LifecycleEventListener { reactContext.addLifecycleEventListener(object : LifecycleEventListener {
@ -206,10 +215,6 @@ class CameraView(context: Context) : FrameLayout(context), LifecycleOwner {
} }
} }
fun finalize() {
mHybridData.resetNative()
}
private external fun initHybrid(): HybridData private external fun initHybrid(): HybridData
private external fun frameProcessorCallback(frame: ImageProxy) private external fun frameProcessorCallback(frame: ImageProxy)
@ -252,8 +257,8 @@ class CameraView(context: Context) : FrameLayout(context), LifecycleOwner {
fun update(changedProps: ArrayList<String>) = previewView.post { fun update(changedProps: ArrayList<String>) = previewView.post {
// TODO: Does this introduce too much overhead? // TODO: Does this introduce too much overhead?
// I need to .post on the previewView because it might've not been initialized yet // I need to .post on the previewView because it might've not been initialized yet
// I need to use GlobalScope.launch because of the suspend fun [configureSession] // I need to use CoroutineScope.launch because of the suspend fun [configureSession]
GlobalScope.launch(Dispatchers.Main) { coroutineScope.launch {
try { try {
val shouldReconfigureSession = changedProps.containsAny(propsThatRequireSessionReconfiguration) val shouldReconfigureSession = changedProps.containsAny(propsThatRequireSessionReconfiguration)
val shouldReconfigureZoom = shouldReconfigureSession || changedProps.contains("zoom") val shouldReconfigureZoom = shouldReconfigureSession || changedProps.contains("zoom")
@ -334,7 +339,7 @@ class CameraView(context: Context) : FrameLayout(context), LifecycleOwner {
val imageAnalysisBuilder = ImageAnalysis.Builder() val imageAnalysisBuilder = ImageAnalysis.Builder()
.setTargetRotation(rotation) .setTargetRotation(rotation)
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.setBackgroundExecutor(CameraViewModule.FrameProcessorThread) .setBackgroundExecutor(frameProcessorThread)
if (format == null) { if (format == null) {
// let CameraX automatically find best resolution for the target aspect ratio // let CameraX automatically find best resolution for the target aspect ratio
@ -473,12 +478,4 @@ class CameraView(context: Context) : FrameLayout(context), LifecycleOwner {
} }
return map return map
} }
companion object {
const val TAG = "CameraView"
const val TAG_PERF = "CameraView.performance"
private val propsThatRequireSessionReconfiguration = arrayListOf("cameraId", "format", "fps", "hdr", "lowLightBoost", "photo", "video", "enableFrameProcessor")
private val arrayListOfZoom = arrayListOf("zoom")
}
} }

View File

@ -1,18 +1,67 @@
package com.mrousavy.camera package com.mrousavy.camera
import android.util.Log import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.ReadableMap
import com.facebook.react.common.MapBuilder import com.facebook.react.common.MapBuilder
import com.facebook.react.uimanager.SimpleViewManager import com.facebook.react.uimanager.SimpleViewManager
import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.annotations.ReactProp import com.facebook.react.uimanager.annotations.ReactProp
import com.mrousavy.camera.frameprocessor.FrameProcessorRuntimeManager
import java.util.concurrent.Executors
class CameraViewManager : SimpleViewManager<CameraView>() { @Suppress("unused")
private fun addChangedPropToTransaction(view: CameraView, changedProp: String) { class CameraViewManager(reactContext: ReactApplicationContext) : SimpleViewManager<CameraView>() {
if (cameraViewTransactions[view] == null) { private val frameProcessorThread = Executors.newSingleThreadExecutor()
cameraViewTransactions[view] = ArrayList() private var frameProcessorManager: FrameProcessorRuntimeManager? = null
init {
if (frameProcessorManager == null) {
frameProcessorThread.execute {
frameProcessorManager = FrameProcessorRuntimeManager(reactContext, frameProcessorThread)
reactContext.runOnJSQueueThread {
frameProcessorManager!!.installJSIBindings()
} }
cameraViewTransactions[view]!!.add(changedProp) }
}
}
private fun destroy() {
frameProcessorManager = null
frameProcessorThread.shutdown()
}
override fun onCatalystInstanceDestroy() {
super.onCatalystInstanceDestroy()
destroy()
}
override fun invalidate() {
super.invalidate()
destroy()
}
public override fun createViewInstance(context: ThemedReactContext): CameraView {
return CameraView(context, frameProcessorThread)
}
override fun onAfterUpdateTransaction(view: CameraView) {
super.onAfterUpdateTransaction(view)
val changedProps = cameraViewTransactions[view] ?: ArrayList()
view.update(changedProps)
cameraViewTransactions.remove(view)
}
override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any>? {
return MapBuilder.builder<String, Any>()
.put("cameraInitialized", MapBuilder.of("registrationName", "onInitialized"))
.put("cameraError", MapBuilder.of("registrationName", "onError"))
.build()
}
override fun getName(): String {
return TAG
} }
@ReactProp(name = "cameraId") @ReactProp(name = "cameraId")
@ -78,6 +127,7 @@ class CameraViewManager : SimpleViewManager<CameraView>() {
view.format = format view.format = format
} }
// TODO: Change when TurboModules release.
// We're treating -1 as "null" here, because when I make the fps parameter // We're treating -1 as "null" here, because when I make the fps parameter
// of type "Int?" the react bridge throws an error. // of type "Int?" the react bridge throws an error.
@ReactProp(name = "fps", defaultInt = -1) @ReactProp(name = "fps", defaultInt = -1)
@ -144,36 +194,16 @@ class CameraViewManager : SimpleViewManager<CameraView>() {
view.enableZoomGesture = enableZoomGesture view.enableZoomGesture = enableZoomGesture
} }
override fun onAfterUpdateTransaction(view: CameraView) {
super.onAfterUpdateTransaction(view)
val changedProps = cameraViewTransactions[view] ?: ArrayList()
view.update(changedProps)
cameraViewTransactions.remove(view)
}
public override fun createViewInstance(context: ThemedReactContext): CameraView {
return CameraView(context)
}
override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any>? {
return MapBuilder.builder<String, Any>()
.put("cameraInitialized", MapBuilder.of("registrationName", "onInitialized"))
.put("cameraError", MapBuilder.of("registrationName", "onError"))
.build()
}
override fun onDropViewInstance(view: CameraView) {
Log.d(TAG, "onDropViewInstance() called!")
super.onDropViewInstance(view)
}
override fun getName(): String {
return TAG
}
companion object { companion object {
const val TAG = "CameraView" const val TAG = "CameraView"
val cameraViewTransactions: HashMap<CameraView, ArrayList<String>> = HashMap() val cameraViewTransactions: HashMap<CameraView, ArrayList<String>> = HashMap()
private fun addChangedPropToTransaction(view: CameraView, changedProp: String) {
if (cameraViewTransactions[view] == null) {
cameraViewTransactions[view] = ArrayList()
}
cameraViewTransactions[view]!!.add(changedProp)
}
} }
} }

View File

@ -15,19 +15,16 @@ import androidx.core.content.ContextCompat
import com.facebook.react.bridge.* import com.facebook.react.bridge.*
import com.facebook.react.modules.core.PermissionAwareActivity import com.facebook.react.modules.core.PermissionAwareActivity
import com.facebook.react.modules.core.PermissionListener import com.facebook.react.modules.core.PermissionListener
import com.mrousavy.camera.frameprocessor.FrameProcessorRuntimeManager
import com.mrousavy.camera.parsers.* import com.mrousavy.camera.parsers.*
import com.mrousavy.camera.utils.* import com.mrousavy.camera.utils.*
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.guava.await import kotlinx.coroutines.guava.await
@Suppress("unused")
class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
companion object { companion object {
const val TAG = "CameraView" const val TAG = "CameraView"
var RequestCode = 10 var RequestCode = 10
val FrameProcessorThread: ExecutorService = Executors.newSingleThreadExecutor()
fun parsePermissionStatus(status: Int): String { fun parsePermissionStatus(status: Int): String {
return when (status) { return when (status) {
@ -38,24 +35,22 @@ class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBase
} }
} }
private var frameProcessorManager: FrameProcessorRuntimeManager? = null private val coroutineScope = CoroutineScope(Dispatchers.Default) // TODO: or Dispatchers.Main?
override fun initialize() { private fun cleanup() {
super.initialize() if (coroutineScope.isActive) {
if (frameProcessorManager == null) { coroutineScope.cancel("CameraViewModule has been destroyed.")
FrameProcessorThread.execute {
frameProcessorManager = FrameProcessorRuntimeManager(reactApplicationContext)
reactApplicationContext.runOnJSQueueThread {
frameProcessorManager!!.installJSIBindings()
}
}
} }
} }
override fun onCatalystInstanceDestroy() { override fun onCatalystInstanceDestroy() {
super.onCatalystInstanceDestroy() super.onCatalystInstanceDestroy()
frameProcessorManager?.destroy() cleanup()
frameProcessorManager = null }
override fun invalidate() {
super.invalidate()
cleanup()
} }
override fun getName(): String { override fun getName(): String {
@ -66,7 +61,7 @@ class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBase
@ReactMethod @ReactMethod
fun takePhoto(viewTag: Int, options: ReadableMap, promise: Promise) { fun takePhoto(viewTag: Int, options: ReadableMap, promise: Promise) {
GlobalScope.launch(Dispatchers.Main) { coroutineScope.launch {
withPromise(promise) { withPromise(promise) {
val view = findCameraView(viewTag) val view = findCameraView(viewTag)
view.takePhoto(options) view.takePhoto(options)
@ -74,9 +69,10 @@ class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBase
} }
} }
@Suppress("unused")
@ReactMethod @ReactMethod
fun takeSnapshot(viewTag: Int, options: ReadableMap, promise: Promise) { fun takeSnapshot(viewTag: Int, options: ReadableMap, promise: Promise) {
GlobalScope.launch(Dispatchers.Main) { coroutineScope.launch {
withPromise(promise) { withPromise(promise) {
val view = findCameraView(viewTag) val view = findCameraView(viewTag)
view.takeSnapshot(options) view.takeSnapshot(options)
@ -87,7 +83,7 @@ class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBase
// TODO: startRecording() cannot be awaited, because I can't have a Promise and a onRecordedCallback in the same function. Hopefully TurboModules allows that // TODO: startRecording() cannot be awaited, because I can't have a Promise and a onRecordedCallback in the same function. Hopefully TurboModules allows that
@ReactMethod @ReactMethod
fun startRecording(viewTag: Int, options: ReadableMap, onRecordCallback: Callback) { fun startRecording(viewTag: Int, options: ReadableMap, onRecordCallback: Callback) {
GlobalScope.launch(Dispatchers.Main) { coroutineScope.launch {
val view = findCameraView(viewTag) val view = findCameraView(viewTag)
try { try {
view.startRecording(options, onRecordCallback) view.startRecording(options, onRecordCallback)
@ -112,7 +108,7 @@ class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBase
@ReactMethod @ReactMethod
fun focus(viewTag: Int, point: ReadableMap, promise: Promise) { fun focus(viewTag: Int, point: ReadableMap, promise: Promise) {
GlobalScope.launch(Dispatchers.Main) { coroutineScope.launch {
withPromise(promise) { withPromise(promise) {
val view = findCameraView(viewTag) val view = findCameraView(viewTag)
view.focus(point) view.focus(point)
@ -126,7 +122,7 @@ class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBase
@ReactMethod @ReactMethod
fun getAvailableCameraDevices(promise: Promise) { fun getAvailableCameraDevices(promise: Promise) {
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
GlobalScope.launch(Dispatchers.Main) { coroutineScope.launch {
withPromise(promise) { withPromise(promise) {
val extensionsManager = ExtensionsManager.getInstance(reactApplicationContext).await() val extensionsManager = ExtensionsManager.getInstance(reactApplicationContext).await()
val cameraProvider = ProcessCameraProvider.getInstance(reactApplicationContext).await() val cameraProvider = ProcessCameraProvider.getInstance(reactApplicationContext).await()

View File

@ -39,14 +39,6 @@ public abstract class FrameProcessorPlugin {
mHybridData = initHybrid(name); mHybridData = initHybrid(name);
} }
@Override
protected void finalize() throws Throwable {
super.finalize();
if (mHybridData != null) {
mHybridData.resetNative();
}
}
private native @NonNull HybridData initHybrid(@NonNull String name); private native @NonNull HybridData initHybrid(@NonNull String name);
/** /**

View File

@ -10,9 +10,10 @@ import com.mrousavy.camera.CameraView
import com.mrousavy.camera.ViewNotFoundError import com.mrousavy.camera.ViewNotFoundError
import com.swmansion.reanimated.Scheduler import com.swmansion.reanimated.Scheduler
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.concurrent.ExecutorService
@Suppress("KotlinJniMissingFunction") // I use fbjni, Android Studio is not smart enough to realize that. @Suppress("KotlinJniMissingFunction") // I use fbjni, Android Studio is not smart enough to realize that.
class FrameProcessorRuntimeManager(context: ReactApplicationContext) { class FrameProcessorRuntimeManager(context: ReactApplicationContext, frameProcessorThread: ExecutorService) {
companion object { companion object {
const val TAG = "FrameProcessorRuntime" const val TAG = "FrameProcessorRuntime"
val Plugins: ArrayList<FrameProcessorPlugin> = ArrayList() val Plugins: ArrayList<FrameProcessorPlugin> = ArrayList()
@ -30,7 +31,7 @@ class FrameProcessorRuntimeManager(context: ReactApplicationContext) {
init { init {
val holder = context.catalystInstance.jsCallInvokerHolder as CallInvokerHolderImpl val holder = context.catalystInstance.jsCallInvokerHolder as CallInvokerHolderImpl
mScheduler = VisionCameraScheduler() mScheduler = VisionCameraScheduler(frameProcessorThread)
mContext = WeakReference(context) mContext = WeakReference(context)
mHybridData = initHybrid(context.javaScriptContextHolder.get(), holder, mScheduler) mHybridData = initHybrid(context.javaScriptContextHolder.get(), holder, mScheduler)
initializeRuntime() initializeRuntime()
@ -42,10 +43,7 @@ class FrameProcessorRuntimeManager(context: ReactApplicationContext) {
Log.i(TAG, "Successfully installed ${Plugins.count()} Frame Processor Plugins!") Log.i(TAG, "Successfully installed ${Plugins.count()} Frame Processor Plugins!")
} }
fun destroy() { @Suppress("unused")
mHybridData.resetNative()
}
@DoNotStrip @DoNotStrip
@Keep @Keep
fun findCameraViewById(viewId: Int): CameraView { fun findCameraViewById(viewId: Int): CameraView {

View File

@ -2,28 +2,26 @@ package com.mrousavy.camera.frameprocessor;
import com.facebook.jni.HybridData; import com.facebook.jni.HybridData;
import com.facebook.proguard.annotations.DoNotStrip; import com.facebook.proguard.annotations.DoNotStrip;
import com.mrousavy.camera.CameraViewModule; import java.util.concurrent.ExecutorService;
@SuppressWarnings("JavaJniMissingFunction") // using fbjni here @SuppressWarnings("JavaJniMissingFunction") // using fbjni here
public class VisionCameraScheduler { public class VisionCameraScheduler {
@SuppressWarnings({"unused", "FieldCanBeLocal"})
@DoNotStrip @DoNotStrip
private final HybridData mHybridData; private final HybridData mHybridData;
private final ExecutorService frameProcessorThread;
public VisionCameraScheduler() { public VisionCameraScheduler(ExecutorService frameProcessorThread) {
this.frameProcessorThread = frameProcessorThread;
mHybridData = initHybrid(); mHybridData = initHybrid();
} }
@Override
protected void finalize() throws Throwable {
mHybridData.resetNative();
super.finalize();
}
private native HybridData initHybrid(); private native HybridData initHybrid();
private native void triggerUI(); private native void triggerUI();
@SuppressWarnings("unused")
@DoNotStrip @DoNotStrip
private void scheduleTrigger() { private void scheduleTrigger() {
CameraViewModule.Companion.getFrameProcessorThread().submit(this::triggerUI); frameProcessorThread.submit(this::triggerUI);
} }
} }