Description
On iOS, registerSessionReadyListener() trips the Main Thread Checker: the native
-[AppsFlyerLib registerSessionReadyListener:] reads -[UIApplication applicationState]
while running on a Swift concurrency cooperative thread, not on the main thread.
The plugin's own AFRPCBridge already hops to the main thread for event delivery
(DispatchQueue.main.async { handler(jsonEvent) }, with a comment explaining why
Task { @MainActor in } is not used there). The request path does not do the same:
AFRPCCoreHandler.handle is async, so it executes on the cooperative pool and calls
straight into the native SDK from there.
Environment
|
|
| appsflyer_sdk |
7.0.2 |
| AppsFlyerFramework |
7.0.2 |
| Flutter |
3.47.2 (stable), Dart 3.13.2 |
| Xcode |
26.6 (17F113) |
| Platform |
iOS only app, UIScene lifecycle (FlutterSceneDelegate), SPM (no CocoaPods) |
Steps to reproduce
final af = AppsFlyerSdk.instance;
await af.enableDebug(true);
await af.init(devKey: '<key>', appId: '<id>');
await af.setCustomerUserId('<id>');
await af.registerSessionReadyListener(() => af.start()); // <- warning fires here
Run a debug build on a device/simulator with the Main Thread Checker enabled (default).
Actual result
Main Thread Checker: UI API called on a background thread: -[UIApplication applicationState]
PID: 62283, TID: 3850498, Thread name: (none), Queue name: com.apple.root.user-initiated-qos.cooperative, QoS: 25
Backtrace:
4 Runner.debug.dylib -[AppsFlyerLib registerSessionReadyListener:] + 164
5 Runner.debug.dylib $s12AppsFlyerRPC16AFRPCCoreHandlerV34handleRegisterSessionReadyListener33_...
6 Runner.debug.dylib $s12AppsFlyerRPC16AFRPCCoreHandlerV6handleys6ResultOyAA10SDKSuccessVyAA10AnyCodableVGAA10SDKFailureVGAA0D7RequestOYaFTY0_ + 780
7 libswift_Concurrency.dylib
9 libdispatch.dylib _dispatch_root_queue_drain + 404
10 libdispatch.dylib _dispatch_worker_thread2 + 188
11 libsystem_pthread.dylib _pthread_wqthread + 228
Expected result
No Main Thread Checker violation: RPC requests that reach UIKit-touching SDK APIs should
be dispatched on the main thread, the way AFRPCBridge already does for events.
Notes
Reading UIApplication.applicationState off the main thread is a data race by Apple's
own contract, and this one decides whether a session is sent, so the value it reads
matters. There is no way to work around it from the Dart side, since the queue is chosen
inside the plugin: the only alternative is to drop registerSessionReadyListener
entirely and call start() directly, which per the v7 docs would lose every session
after the first foreground cycle.
Description
On iOS,
registerSessionReadyListener()trips the Main Thread Checker: the native-[AppsFlyerLib registerSessionReadyListener:]reads-[UIApplication applicationState]while running on a Swift concurrency cooperative thread, not on the main thread.
The plugin's own
AFRPCBridgealready hops to the main thread for event delivery(
DispatchQueue.main.async { handler(jsonEvent) }, with a comment explaining whyTask { @MainActor in }is not used there). The request path does not do the same:AFRPCCoreHandler.handleisasync, so it executes on the cooperative pool and callsstraight into the native SDK from there.
Environment
FlutterSceneDelegate), SPM (no CocoaPods)Steps to reproduce
Run a debug build on a device/simulator with the Main Thread Checker enabled (default).
Actual result
Expected result
No Main Thread Checker violation: RPC requests that reach UIKit-touching SDK APIs should
be dispatched on the main thread, the way
AFRPCBridgealready does for events.Notes
Reading
UIApplication.applicationStateoff the main thread is a data race by Apple'sown contract, and this one decides whether a session is sent, so the value it reads
matters. There is no way to work around it from the Dart side, since the queue is chosen
inside the plugin: the only alternative is to drop
registerSessionReadyListenerentirely and call
start()directly, which per the v7 docs would lose every sessionafter the first foreground cycle.