[camera]use weak self to fix a crash due to orientation change (#6277)
diff --git a/packages/camera/camera_avfoundation/CHANGELOG.md b/packages/camera/camera_avfoundation/CHANGELOG.md
index 2726309..4df497b 100644
--- a/packages/camera/camera_avfoundation/CHANGELOG.md
+++ b/packages/camera/camera_avfoundation/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.9.8+4
+
+* Fixes a crash due to sending orientation change events when the engine is torn down.
+
## 0.9.8+3
* Fixes avoid_redundant_argument_values lint warnings and minor typos.
diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraOrientationTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraOrientationTests.m
index 29fc325..60e88ff 100644
--- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraOrientationTests.m
+++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraOrientationTests.m
@@ -92,6 +92,39 @@
[self waitForExpectationsWithTimeout:30.0 handler:nil];
}
+- (void)testOrientationChanged_noRetainCycle {
+ dispatch_queue_t captureSessionQueue = dispatch_queue_create("capture_session_queue", NULL);
+ FLTCam *mockCam = OCMClassMock([FLTCam class]);
+ FLTThreadSafeMethodChannel *mockChannel = OCMClassMock([FLTThreadSafeMethodChannel class]);
+
+ __weak CameraPlugin *weakCamera;
+
+ @autoreleasepool {
+ CameraPlugin *camera = [[CameraPlugin alloc] initWithRegistry:nil messenger:nil];
+ weakCamera = camera;
+ camera.captureSessionQueue = captureSessionQueue;
+ camera.camera = mockCam;
+ camera.deviceEventMethodChannel = mockChannel;
+
+ [camera orientationChanged:
+ [self createMockNotificationForOrientation:UIDeviceOrientationLandscapeLeft]];
+ }
+
+ // Sanity check
+ XCTAssertNil(weakCamera, @"Camera must have been deallocated.");
+
+ // Must check in captureSessionQueue since orientationChanged dispatches to this queue.
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"Dispatched to capture session queue"];
+ dispatch_async(captureSessionQueue, ^{
+ OCMVerify(never(), [mockCam setDeviceOrientation:UIDeviceOrientationLandscapeLeft]);
+ OCMVerify(never(), [mockChannel invokeMethod:@"orientation_changed" arguments:OCMOCK_ANY]);
+ [expectation fulfill];
+ });
+
+ [self waitForExpectationsWithTimeout:1 handler:nil];
+}
+
- (NSNotification *)createMockNotificationForOrientation:(UIDeviceOrientation)deviceOrientation {
UIDevice *mockDevice = OCMClassMock([UIDevice class]);
OCMStub([mockDevice orientation]).andReturn(deviceOrientation);
diff --git a/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin.m b/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin.m
index cb19c09..628211a 100644
--- a/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin.m
+++ b/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin.m
@@ -19,7 +19,6 @@
@interface CameraPlugin ()
@property(readonly, nonatomic) FLTThreadSafeTextureRegistry *registry;
@property(readonly, nonatomic) NSObject<FlutterBinaryMessenger> *messenger;
-@property(readonly, nonatomic) FLTThreadSafeMethodChannel *deviceEventMethodChannel;
@end
@implementation CameraPlugin
@@ -56,6 +55,10 @@
[[FLTThreadSafeMethodChannel alloc] initWithMethodChannel:methodChannel];
}
+- (void)detachFromEngineForRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
+ [UIDevice.currentDevice endGeneratingDeviceOrientationNotifications];
+}
+
- (void)startOrientationListener {
[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
[[NSNotificationCenter defaultCenter] addObserver:self
@@ -73,11 +76,12 @@
return;
}
+ __weak typeof(self) weakSelf = self;
dispatch_async(self.captureSessionQueue, ^{
// `FLTCam::setDeviceOrientation` must be called on capture session queue.
- [self.camera setDeviceOrientation:orientation];
+ [weakSelf.camera setDeviceOrientation:orientation];
// `CameraPlugin::sendDeviceOrientation` can be called on any queue.
- [self sendDeviceOrientation:orientation];
+ [weakSelf sendDeviceOrientation:orientation];
});
}
@@ -89,11 +93,11 @@
- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result {
// Invoke the plugin on another dispatch queue to avoid blocking the UI.
- dispatch_async(_captureSessionQueue, ^{
+ __weak typeof(self) weakSelf = self;
+ dispatch_async(self.captureSessionQueue, ^{
FLTThreadSafeFlutterResult *threadSafeResult =
[[FLTThreadSafeFlutterResult alloc] initWithResult:result];
-
- [self handleMethodCallAsync:call result:threadSafeResult];
+ [weakSelf handleMethodCallAsync:call result:threadSafeResult];
});
}
@@ -261,7 +265,11 @@
- (void)handleCreateMethodCall:(FlutterMethodCall *)call
result:(FLTThreadSafeFlutterResult *)result {
// Create FLTCam only if granted camera access (and audio access if audio is enabled)
+ __weak typeof(self) weakSelf = self;
FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) {
+ typeof(self) strongSelf = weakSelf;
+ if (!strongSelf) return;
+
if (error) {
[result sendFlutterError:error];
} else {
@@ -272,14 +280,17 @@
if (audioEnabled) {
// Setup audio capture session only if granted audio access.
FLTRequestAudioPermissionWithCompletionHandler(^(FlutterError *error) {
+ // cannot use the outter `strongSelf`
+ typeof(self) strongSelf = weakSelf;
+ if (!strongSelf) return;
if (error) {
[result sendFlutterError:error];
} else {
- [self createCameraOnSessionQueueWithCreateMethodCall:call result:result];
+ [strongSelf createCameraOnSessionQueueWithCreateMethodCall:call result:result];
}
});
} else {
- [self createCameraOnSessionQueueWithCreateMethodCall:call result:result];
+ [strongSelf createCameraOnSessionQueueWithCreateMethodCall:call result:result];
}
}
});
@@ -287,7 +298,11 @@
- (void)createCameraOnSessionQueueWithCreateMethodCall:(FlutterMethodCall *)createMethodCall
result:(FLTThreadSafeFlutterResult *)result {
+ __weak typeof(self) weakSelf = self;
dispatch_async(self.captureSessionQueue, ^{
+ typeof(self) strongSelf = weakSelf;
+ if (!strongSelf) return;
+
NSString *cameraName = createMethodCall.arguments[@"cameraName"];
NSString *resolutionPreset = createMethodCall.arguments[@"resolutionPreset"];
NSNumber *enableAudio = createMethodCall.arguments[@"enableAudio"];
@@ -296,22 +311,22 @@
resolutionPreset:resolutionPreset
enableAudio:[enableAudio boolValue]
orientation:[[UIDevice currentDevice] orientation]
- captureSessionQueue:self.captureSessionQueue
+ captureSessionQueue:strongSelf.captureSessionQueue
error:&error];
if (error) {
[result sendError:error];
} else {
- if (self.camera) {
- [self.camera close];
+ if (strongSelf.camera) {
+ [strongSelf.camera close];
}
- self.camera = cam;
- [self.registry registerTexture:cam
- completion:^(int64_t textureId) {
- [result sendSuccessWithData:@{
- @"cameraId" : @(textureId),
- }];
- }];
+ strongSelf.camera = cam;
+ [strongSelf.registry registerTexture:cam
+ completion:^(int64_t textureId) {
+ [result sendSuccessWithData:@{
+ @"cameraId" : @(textureId),
+ }];
+ }];
}
});
}
diff --git a/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin_Test.h b/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin_Test.h
index 77a758d..f6c97da 100644
--- a/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin_Test.h
+++ b/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin_Test.h
@@ -8,7 +8,7 @@
#import "FLTCam.h"
#import "FLTThreadSafeFlutterResult.h"
-/// Methods exposed for unit testing.
+/// APIs exposed for unit testing.
@interface CameraPlugin ()
/// All FLTCam's state access and capture session related operations should be on run on this queue.
@@ -17,6 +17,10 @@
/// An internal camera object that manages camera's state and performs camera operations.
@property(nonatomic, strong) FLTCam *camera;
+/// A thread safe wrapper of the method channel used to send device events such as orientation
+/// changes.
+@property(nonatomic, strong) FLTThreadSafeMethodChannel *deviceEventMethodChannel;
+
/// Inject @p FlutterTextureRegistry and @p FlutterBinaryMessenger for unit testing.
- (instancetype)initWithRegistry:(NSObject<FlutterTextureRegistry> *)registry
messenger:(NSObject<FlutterBinaryMessenger> *)messenger
diff --git a/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m b/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m
index f267604..90b81ad 100644
--- a/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m
+++ b/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m
@@ -20,16 +20,18 @@
}
- (FlutterError *_Nullable)onCancelWithArguments:(id _Nullable)arguments {
+ __weak typeof(self) weakSelf = self;
dispatch_async(self.captureSessionQueue, ^{
- self.eventSink = nil;
+ weakSelf.eventSink = nil;
});
return nil;
}
- (FlutterError *_Nullable)onListenWithArguments:(id _Nullable)arguments
eventSink:(nonnull FlutterEventSink)events {
+ __weak typeof(self) weakSelf = self;
dispatch_async(self.captureSessionQueue, ^{
- self.eventSink = events;
+ weakSelf.eventSink = events;
});
return nil;
}
@@ -247,16 +249,18 @@
return;
}
+ __weak typeof(self) weakSelf = self;
FLTSavePhotoDelegate *savePhotoDelegate = [[FLTSavePhotoDelegate alloc]
initWithPath:path
ioQueue:self.photoIOQueue
completionHandler:^(NSString *_Nullable path, NSError *_Nullable error) {
- dispatch_async(self.captureSessionQueue, ^{
- // Dispatch back to capture session queue to delete reference.
- // Retain cycle is broken after the dictionary entry is cleared.
- // This is to keep the behavior with the previous `selfReference` approach in the
- // FLTSavePhotoDelegate, where delegate is released only after capture completion.
- [self.inProgressSavePhotoDelegates removeObjectForKey:@(settings.uniqueID)];
+ typeof(self) strongSelf = weakSelf;
+ if (!strongSelf) return;
+ dispatch_async(strongSelf.captureSessionQueue, ^{
+ // cannot use the outter `strongSelf`
+ typeof(self) strongSelf = weakSelf;
+ if (!strongSelf) return;
+ [strongSelf.inProgressSavePhotoDelegates removeObjectForKey:@(settings.uniqueID)];
});
if (error) {
@@ -387,6 +391,7 @@
// Under rare contest scenarios, it will not block for too long since the critical section is
// quite lightweight.
dispatch_sync(self.pixelBufferSynchronizationQueue, ^{
+ // No need weak self because it's dispatch_sync.
previousPixelBuffer = self.latestPixelBuffer;
self.latestPixelBuffer = newBuffer;
});
@@ -610,6 +615,7 @@
__block CVPixelBufferRef pixelBuffer = nil;
// Use `dispatch_sync` because `copyPixelBuffer` API requires synchronous return.
dispatch_sync(self.pixelBufferSynchronizationQueue, ^{
+ // No need weak self because it's dispatch_sync.
pixelBuffer = self.latestPixelBuffer;
self.latestPixelBuffer = nil;
});
@@ -917,11 +923,19 @@
[[FLTThreadSafeEventChannel alloc] initWithEventChannel:eventChannel];
_imageStreamHandler = imageStreamHandler;
+ __weak typeof(self) weakSelf = self;
[threadSafeEventChannel setStreamHandler:_imageStreamHandler
completion:^{
- dispatch_async(self->_captureSessionQueue, ^{
- self.isStreamingImages = YES;
- self.streamingPendingFramesCount = 0;
+ typeof(self) strongSelf = weakSelf;
+ if (!strongSelf) return;
+
+ dispatch_async(strongSelf.captureSessionQueue, ^{
+ // cannot use the outter strongSelf
+ typeof(self) strongSelf = weakSelf;
+ if (!strongSelf) return;
+
+ strongSelf.isStreamingImages = YES;
+ strongSelf.streamingPendingFramesCount = 0;
});
}];
} else {
diff --git a/packages/camera/camera_avfoundation/ios/Classes/FLTSavePhotoDelegate.m b/packages/camera/camera_avfoundation/ios/Classes/FLTSavePhotoDelegate.m
index 1df1708..617890c 100644
--- a/packages/camera/camera_avfoundation/ios/Classes/FLTSavePhotoDelegate.m
+++ b/packages/camera/camera_avfoundation/ios/Classes/FLTSavePhotoDelegate.m
@@ -31,13 +31,17 @@
self.completionHandler(nil, error);
return;
}
+ __weak typeof(self) weakSelf = self;
dispatch_async(self.ioQueue, ^{
+ typeof(self) strongSelf = weakSelf;
+ if (!strongSelf) return;
+
NSData *data = photoDataProvider();
NSError *ioError;
- if ([data writeToFile:self.path options:NSDataWritingAtomic error:&ioError]) {
- self.completionHandler(self.path, nil);
+ if ([data writeToFile:strongSelf.path options:NSDataWritingAtomic error:&ioError]) {
+ strongSelf.completionHandler(self.path, nil);
} else {
- self.completionHandler(nil, ioError);
+ strongSelf.completionHandler(nil, ioError);
}
});
}
diff --git a/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeEventChannel.m b/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeEventChannel.m
index 46941bb..ed749a3 100644
--- a/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeEventChannel.m
+++ b/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeEventChannel.m
@@ -21,8 +21,11 @@
- (void)setStreamHandler:(NSObject<FlutterStreamHandler> *)handler
completion:(void (^)(void))completion {
+ __weak typeof(self) weakSelf = self;
FLTEnsureToRunOnMainQueue(^{
- [self.channel setStreamHandler:handler];
+ typeof(self) strongSelf = weakSelf;
+ if (!strongSelf) return;
+ [strongSelf.channel setStreamHandler:handler];
completion();
});
}
diff --git a/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeFlutterResult.m b/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeFlutterResult.m
index ad125f7..283a0d6 100644
--- a/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeFlutterResult.m
+++ b/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeFlutterResult.m
@@ -52,6 +52,11 @@
*/
- (void)send:(id _Nullable)result {
FLTEnsureToRunOnMainQueue(^{
+ // WARNING: Should not use weak self, because `FlutterResult`s are passed as arguments
+ // (retained within call stack, but not in the heap). FLTEnsureToRunOnMainQueue may trigger a
+ // context switch (when calling from background thread), in which case using weak self will
+ // always result in a nil self. Alternative to using strong self, we can also create a local
+ // strong variable to be captured by this block.
self.flutterResult(result);
});
}
diff --git a/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeMethodChannel.m b/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeMethodChannel.m
index 5b29b70..df7c169 100644
--- a/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeMethodChannel.m
+++ b/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeMethodChannel.m
@@ -20,8 +20,9 @@
}
- (void)invokeMethod:(NSString *)method arguments:(id)arguments {
+ __weak typeof(self) weakSelf = self;
FLTEnsureToRunOnMainQueue(^{
- [self.channel invokeMethod:method arguments:arguments];
+ [weakSelf.channel invokeMethod:method arguments:arguments];
});
}
diff --git a/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeTextureRegistry.m b/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeTextureRegistry.m
index 349b89f..b82d566 100644
--- a/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeTextureRegistry.m
+++ b/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeTextureRegistry.m
@@ -21,20 +21,25 @@
- (void)registerTexture:(NSObject<FlutterTexture> *)texture
completion:(void (^)(int64_t))completion {
+ __weak typeof(self) weakSelf = self;
FLTEnsureToRunOnMainQueue(^{
- completion([self.registry registerTexture:texture]);
+ typeof(self) strongSelf = weakSelf;
+ if (!strongSelf) return;
+ completion([strongSelf.registry registerTexture:texture]);
});
}
- (void)textureFrameAvailable:(int64_t)textureId {
+ __weak typeof(self) weakSelf = self;
FLTEnsureToRunOnMainQueue(^{
- [self.registry textureFrameAvailable:textureId];
+ [weakSelf.registry textureFrameAvailable:textureId];
});
}
- (void)unregisterTexture:(int64_t)textureId {
+ __weak typeof(self) weakSelf = self;
FLTEnsureToRunOnMainQueue(^{
- [self.registry unregisterTexture:textureId];
+ [weakSelf.registry unregisterTexture:textureId];
});
}
diff --git a/packages/camera/camera_avfoundation/pubspec.yaml b/packages/camera/camera_avfoundation/pubspec.yaml
index c2b9dcb..3d1b7c6 100644
--- a/packages/camera/camera_avfoundation/pubspec.yaml
+++ b/packages/camera/camera_avfoundation/pubspec.yaml
@@ -2,7 +2,7 @@
description: iOS implementation of the camera plugin.
repository: https://github.com/flutter/plugins/tree/main/packages/camera/camera_avfoundation
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
-version: 0.9.8+3
+version: 0.9.8+4
environment:
sdk: ">=2.14.0 <3.0.0"