Skip to content

Commit 2f20864

Browse files
fix: Avoid keeping strong reference to self instance in delegates
1 parent 6aaf755 commit 2f20864

6 files changed

Lines changed: 135 additions & 34 deletions

File tree

WebDriverAgentLib/Routing/FBTCPSocket.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ NS_ASSUME_NONNULL_BEGIN
3838

3939
@interface FBTCPSocket : NSObject
4040

41-
@property (nullable, nonatomic) id<FBTCPSocketDelegate> delegate;
41+
#if __has_feature(objc_arc_weak)
42+
@property (nullable, nonatomic, weak) id<FBTCPSocketDelegate> delegate;
43+
#else
44+
@property (nullable, nonatomic, assign) id<FBTCPSocketDelegate> delegate;
45+
#endif
4246

4347
/**
4448
Creates TCP socket isntance which is going to be started on the specified port

WebDriverAgentLib/Routing/FBTCPSocket.m

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,13 @@ - (BOOL)startWithError:(NSError **)error
4848
- (void)stop
4949
{
5050
@synchronized(self.connectedClients) {
51-
for (NSUInteger i = 0; i < [self.connectedClients count]; i++) {
52-
[[self.connectedClients objectAtIndex:i] disconnect];
51+
NSArray *clients = self.connectedClients.copy;
52+
for (GCDAsyncSocket *client in clients) {
53+
[client disconnect];
5354
}
5455
}
5556

57+
self.delegate = nil;
5658
[self.listeningSocket disconnect];
5759
}
5860

@@ -66,20 +68,29 @@ - (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSo
6668
@synchronized(self.connectedClients) {
6769
[self.connectedClients addObject:newSocket];
6870
}
69-
[self.delegate didClientConnect:newSocket];
71+
id<FBTCPSocketDelegate> delegate = self.delegate;
72+
if (nil != delegate) {
73+
[delegate didClientConnect:newSocket];
74+
}
7075
}
7176

7277
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
7378
{
74-
[self.delegate didClientSendData:sock];
79+
id<FBTCPSocketDelegate> delegate = self.delegate;
80+
if (nil != delegate) {
81+
[delegate didClientSendData:sock];
82+
}
7583
}
7684

7785
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err
7886
{
7987
@synchronized(self.connectedClients) {
8088
[self.connectedClients removeObject:sock];
8189
}
82-
[self.delegate didClientDisconnect:sock];
90+
id<FBTCPSocketDelegate> delegate = self.delegate;
91+
if (nil != delegate) {
92+
[delegate didClientDisconnect:sock];
93+
}
8394
}
8495

8596
@end

WebDriverAgentLib/Routing/FBWebServer.m

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ @interface FBWebServer ()
5151

5252
@implementation FBWebServer
5353

54+
- (void)dealloc
55+
{
56+
[self stopScreenshotsBroadcaster];
57+
}
58+
5459
+ (NSArray<Class<FBCommandHandler>> *)collectCommandHandlerClasses
5560
{
5661
NSArray *handlersClasses = FBClassesThatConformsToProtocol(@protocol(FBCommandHandler));
@@ -125,12 +130,14 @@ - (void)startHTTPServer
125130
- (void)initScreenshotsBroadcaster
126131
{
127132
[self readMjpegSettingsFromEnv];
133+
FBMjpegServer *mjpegServer = [[FBMjpegServer alloc] init];
128134
self.screenshotsBroadcaster = [[FBTCPSocket alloc]
129135
initWithPort:(uint16_t)FBConfiguration.mjpegServerPort];
130-
self.screenshotsBroadcaster.delegate = [[FBMjpegServer alloc] init];
136+
self.screenshotsBroadcaster.delegate = mjpegServer;
131137
NSError *error;
132138
if (![self.screenshotsBroadcaster startWithError:&error]) {
133139
[FBLogger logFmt:@"Cannot init screenshots broadcaster service on port %@. Original error: %@", @(FBConfiguration.mjpegServerPort), error.description];
140+
[mjpegServer stopStreaming];
134141
self.screenshotsBroadcaster = nil;
135142
}
136143
}
@@ -141,7 +148,13 @@ - (void)stopScreenshotsBroadcaster
141148
return;
142149
}
143150

151+
id<FBTCPSocketDelegate> delegate = self.screenshotsBroadcaster.delegate;
152+
if ([delegate respondsToSelector:@selector(stopStreaming)]) {
153+
[(FBMjpegServer *)delegate stopStreaming];
154+
}
155+
self.screenshotsBroadcaster.delegate = nil;
144156
[self.screenshotsBroadcaster stop];
157+
self.screenshotsBroadcaster = nil;
145158
}
146159

147160
- (void)readMjpegSettingsFromEnv
@@ -164,6 +177,8 @@ - (void)stopServing
164177
if (self.server.isRunning) {
165178
[self.server stop:NO];
166179
}
180+
self.server = nil;
181+
self.exceptionHandler = nil;
167182
self.keepAlive = NO;
168183
}
169184

@@ -192,10 +207,15 @@ - (BOOL)attemptToStartServer:(RoutingHTTPServer *)server onPort:(NSInteger)port
192207

193208
- (void)registerRouteHandlers:(NSArray *)commandHandlerClasses
194209
{
210+
__weak typeof(self) weakSelf = self;
195211
for (Class<FBCommandHandler> commandHandler in commandHandlerClasses) {
196212
NSArray *routes = [commandHandler routes];
197213
for (FBRoute *route in routes) {
198214
[self.server handleMethod:route.verb withPath:route.path block:^(RouteRequest *request, RouteResponse *response) {
215+
__strong typeof(weakSelf) strongSelf = weakSelf;
216+
if (nil == strongSelf) {
217+
return;
218+
}
199219
NSDictionary *arguments = [NSJSONSerialization JSONObjectWithData:request.body options:NSJSONReadingMutableContainers error:NULL];
200220
FBRouteRequest *routeParams = [FBRouteRequest
201221
routeRequestWithURL:request.url
@@ -209,7 +229,7 @@ - (void)registerRouteHandlers:(NSArray *)commandHandlerClasses
209229
[route mountRequest:routeParams intoResponse:response];
210230
}
211231
@catch (NSException *exception) {
212-
[self handleException:exception forResponse:response];
232+
[strongSelf handleException:exception forResponse:response];
213233
}
214234
}];
215235
}
@@ -237,9 +257,14 @@ - (void)registerServerKeyRouteHandlers
237257
[response respondWithString:calibrationPage];
238258
}];
239259

260+
__weak typeof(self) weakSelf = self;
240261
[self.server get:@"/wda/shutdown" withBlock:^(RouteRequest *request, RouteResponse *response) {
262+
__strong typeof(weakSelf) strongSelf = weakSelf;
263+
if (nil == strongSelf) {
264+
return;
265+
}
241266
[response respondWithString:@"Shutting down"];
242-
[self.delegate webServerDidRequestShutdown:self];
267+
[strongSelf.delegate webServerDidRequestShutdown:strongSelf];
243268
}];
244269

245270
[self registerRouteHandlers:@[FBUnknownCommands.class]];

WebDriverAgentLib/Utilities/FBImageProcessor.m

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ @interface FBImageProcessor ()
2727
@property (nonatomic) NSData *nextImage;
2828
@property (nonatomic, readonly) NSLock *nextImageLock;
2929
@property (nonatomic, readonly) dispatch_queue_t scalingQueue;
30+
@property (atomic, assign) BOOL isScalingScheduled;
3031

3132
@end
3233

@@ -38,6 +39,7 @@ - (id)init
3839
if (self) {
3940
_nextImageLock = [[NSLock alloc] init];
4041
_scalingQueue = dispatch_queue_create("image.scaling.queue", NULL);
42+
_isScalingScheduled = NO;
4143
}
4244
return self;
4345
}
@@ -51,32 +53,43 @@ - (void)submitImageData:(NSData *)image
5153
[FBLogger verboseLog:@"Discarding screenshot"];
5254
}
5355
self.nextImage = image;
56+
BOOL shouldSchedule = !self.isScalingScheduled;
57+
if (shouldSchedule) {
58+
self.isScalingScheduled = YES;
59+
}
5460
[self.nextImageLock unlock];
61+
if (!shouldSchedule) {
62+
return;
63+
}
5564

5665
#pragma clang diagnostic push
5766
#pragma clang diagnostic ignored "-Wcompletion-handler"
5867
dispatch_async(self.scalingQueue, ^{
59-
[self.nextImageLock lock];
60-
NSData *nextImageData = self.nextImage;
61-
self.nextImage = nil;
62-
[self.nextImageLock unlock];
63-
if (nextImageData == nil) {
64-
return;
65-
}
68+
while (YES) {
69+
[self.nextImageLock lock];
70+
NSData *nextImageData = self.nextImage;
71+
self.nextImage = nil;
72+
if (nextImageData == nil) {
73+
self.isScalingScheduled = NO;
74+
[self.nextImageLock unlock];
75+
return;
76+
}
77+
[self.nextImageLock unlock];
6678

67-
// We do not want this value to be too high because then we get images larger in size than original ones
68-
// Although, we also don't want to lose too much of the quality on recompression
69-
CGFloat recompressionQuality = MAX(0.9,
70-
MIN(FBMaxCompressionQuality, FBConfiguration.mjpegServerScreenshotQuality / 100.0));
71-
NSData *thumbnailData = [self.class fixedImageDataWithImageData:nextImageData
72-
scalingFactor:scalingFactor
73-
uti:UTTypeJPEG
74-
compressionQuality:recompressionQuality
75-
// iOS always returns screnshots in portrait orientation, but puts the real value into the metadata
76-
// Use it with care. See https://github.com/appium/WebDriverAgent/pull/812
77-
fixOrientation:FBConfiguration.mjpegShouldFixOrientation
78-
desiredOrientation:nil];
79-
completionHandler(thumbnailData ?: nextImageData);
79+
// We do not want this value to be too high because then we get images larger in size than original ones
80+
// Although, we also don't want to lose too much of the quality on recompression
81+
CGFloat recompressionQuality = MAX(0.9,
82+
MIN(FBMaxCompressionQuality, FBConfiguration.mjpegServerScreenshotQuality / 100.0));
83+
NSData *thumbnailData = [self.class fixedImageDataWithImageData:nextImageData
84+
scalingFactor:scalingFactor
85+
uti:UTTypeJPEG
86+
compressionQuality:recompressionQuality
87+
// iOS always returns screnshots in portrait orientation, but puts the real value into the metadata
88+
// Use it with care. See https://github.com/appium/WebDriverAgent/pull/812
89+
fixOrientation:FBConfiguration.mjpegShouldFixOrientation
90+
desiredOrientation:nil];
91+
completionHandler(thumbnailData ?: nextImageData);
92+
}
8093
});
8194
#pragma clang diagnostic pop
8295
}

WebDriverAgentLib/Utilities/FBMjpegServer.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ NS_ASSUME_NONNULL_BEGIN
1919
*/
2020
- (instancetype)init;
2121

22+
/**
23+
Stops screenshot broadcasting and prevents future scheduling.
24+
*/
25+
- (void)stopStreaming;
26+
2227
@end
2328

2429
NS_ASSUME_NONNULL_END

WebDriverAgentLib/Utilities/FBMjpegServer.m

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ @interface FBMjpegServer()
3535
@property (nonatomic, readonly) FBImageProcessor *imageProcessor;
3636
@property (nonatomic, readonly) long long mainScreenID;
3737
@property (nonatomic, assign) NSUInteger consecutiveScreenshotFailures;
38+
@property (atomic, assign) BOOL isStreaming;
39+
@property (nonatomic, assign) NSUInteger sentFramesCount;
40+
@property (nonatomic, assign) NSUInteger sentBytesCount;
3841

3942
@end
4043

@@ -45,11 +48,15 @@ - (instancetype)init
4548
{
4649
if ((self = [super init])) {
4750
_consecutiveScreenshotFailures = 0;
51+
_isStreaming = YES;
52+
_sentFramesCount = 0;
53+
_sentBytesCount = 0;
4854
_listeningClients = [NSMutableArray array];
4955
dispatch_queue_attr_t queueAttributes = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, 0);
5056
_backgroundQueue = dispatch_queue_create(QUEUE_NAME, queueAttributes);
57+
__weak typeof(self) weakSelf = self;
5158
dispatch_async(_backgroundQueue, ^{
52-
[self streamScreenshot];
59+
[weakSelf streamScreenshot];
5360
});
5461
_imageProcessor = [[FBImageProcessor alloc] init];
5562
_mainScreenID = [XCUIScreen.mainScreen displayID];
@@ -59,22 +66,29 @@ - (instancetype)init
5966

6067
- (void)scheduleNextScreenshotWithInterval:(uint64_t)timerInterval timeStarted:(uint64_t)timeStarted
6168
{
69+
if (!self.isStreaming) {
70+
return;
71+
}
6272
uint64_t timeElapsed = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW) - timeStarted;
6373
int64_t nextTickDelta = timerInterval - timeElapsed;
74+
__weak typeof(self) weakSelf = self;
6475
if (nextTickDelta > 0) {
6576
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, nextTickDelta), self.backgroundQueue, ^{
66-
[self streamScreenshot];
77+
[weakSelf streamScreenshot];
6778
});
6879
} else {
6980
// Try to do our best to keep the FPS at a decent level
7081
dispatch_async(self.backgroundQueue, ^{
71-
[self streamScreenshot];
82+
[weakSelf streamScreenshot];
7283
});
7384
}
7485
}
7586

7687
- (void)streamScreenshot
7788
{
89+
if (!self.isStreaming) {
90+
return;
91+
}
7892
NSUInteger framerate = FBConfiguration.mjpegServerFramerate;
7993
uint64_t timerInterval = (uint64_t)(1.0 / ((0 == framerate || framerate > MAX_FPS) ? MAX_FPS : framerate) * NSEC_PER_SEC);
8094
uint64_t timeStarted = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW);
@@ -106,10 +120,11 @@ - (void)streamScreenshot
106120
self.consecutiveScreenshotFailures = 0;
107121

108122
CGFloat scalingFactor = FBConfiguration.mjpegScalingFactor / 100.0;
123+
__weak typeof(self) weakSelf = self;
109124
[self.imageProcessor submitImageData:screenshotData
110125
scalingFactor:scalingFactor
111126
completionHandler:^(NSData * _Nonnull scaled) {
112-
[self sendScreenshot:scaled];
127+
[weakSelf sendScreenshot:scaled];
113128
}];
114129

115130
[self scheduleNextScreenshotWithInterval:timerInterval timeStarted:timeStarted];
@@ -122,7 +137,17 @@ - (void)sendScreenshot:(NSData *)screenshotData {
122137
[chunk appendData:(id)[@"\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
123138
@synchronized (self.listeningClients) {
124139
for (GCDAsyncSocket *client in self.listeningClients) {
125-
[client writeData:chunk withTimeout:-1 tag:0];
140+
// Slow clients should fail/close instead of buffering indefinitely.
141+
[client writeData:chunk withTimeout:FRAME_TIMEOUT tag:0];
142+
}
143+
self.sentFramesCount++;
144+
self.sentBytesCount += chunk.length * self.listeningClients.count;
145+
NSUInteger framerate = MAX(1, MIN(MAX_FPS, FBConfiguration.mjpegServerFramerate));
146+
if (0 == self.sentFramesCount % framerate) {
147+
[FBLogger verboseLog:[NSString stringWithFormat:@"MJPEG stats: clients=%@ sentFrames=%@ sentBytes=%@",
148+
@(self.listeningClients.count),
149+
@(self.sentFramesCount),
150+
@(self.sentBytesCount)]];
126151
}
127152
}
128153
}
@@ -158,4 +183,22 @@ - (void)didClientDisconnect:(GCDAsyncSocket *)client
158183
[FBLogger log:@"Disconnected a client from screenshots broadcast"];
159184
}
160185

186+
- (void)stopStreaming
187+
{
188+
self.isStreaming = NO;
189+
@synchronized (self.listeningClients) {
190+
NSArray<GCDAsyncSocket *> *clients = self.listeningClients.copy;
191+
[self.listeningClients removeAllObjects];
192+
for (GCDAsyncSocket *client in clients) {
193+
[client disconnect];
194+
}
195+
}
196+
}
197+
198+
- (void)dealloc
199+
{
200+
[self stopStreaming];
201+
[FBLogger verboseLog:@"FBMjpegServer deallocated"];
202+
}
203+
161204
@end

0 commit comments

Comments
 (0)