Skip to content

[Bug]: iOS - Permission.location.status / .isGranted can hang the main thread for 2+ seconds #1557

Description

@pfraunlobEurofunk

Please check the following before submitting a new issue.

Please select affected platform(s)

  • Android
  • iOS
  • Windows

Steps to reproduce

We collect production crash/ANR data via Sentry (App Hang detection, watchdog
threshold 2000ms). We see repeated "App Hanging" events on iOS whose main
thread stack trace is:

App Hanging: App hanging for at least 2000 ms.

  Runner              0x100794114  +[LocationPermissionStrategy permissionStatus:] (LocationPermissionStrategy.m:166)
  Runner              0x100792ec0  +[PermissionManager checkPermissionStatus:result:] (PermissionManager.m:23)
  Runner              0x100792a70  -[PermissionHandlerPlugin handleMethodCall:result:] (PermissionHandlerPlugin.m:30)
  Flutter             0x101a7dce4  __45-[FlutterMethodChannel setMethodCallHandler:]_block_invoke (FlutterChannels.mm:315)
  Flutter             0x1015dbdf0  flutter::PlatformMessageHandlerIos::HandlePlatformMessage (platform_message_handler_ios.mm:70)

This happens most frequently right at applicationDidBecomeActive /
AppLifecycleState.resumed, when app code calls Permission.location.status
or Permission.location.isGranted to detect whether the user changed the
location permission in Settings while the app was backgrounded.

Root cause: LocationPermissionStrategy.checkPermissionStatus: calls
+[CLLocationManager authorizationStatus] synchronously on whatever thread
invoked the method channel (the main/platform thread by default), with no
dispatch_async off-thread hop.

This is the same class of problem as #1002, which was fixed for
checkServiceStatus (CLLocationManager.locationServicesEnabled) in #1329 /
permission_handler_apple 9.4.5:

- (void)checkServiceStatus:(PermissionGroup)permission completionHandler:(ServiceStatusHandler)completionHandler {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        BOOL isEnabled = [CLLocationManager locationServicesEnabled];
        dispatch_async(dispatch_get_main_queue(), ^(void) {
            completionHandler(isEnabled ? ServiceStatusEnabled : ServiceStatusDisabled);
        });
    });
}

However, checkPermissionStatus: (used by .status / .isGranted) was never given the same treatment and is still synchronous today in the latest published version (9.6.1):

- (PermissionStatus)checkPermissionStatus:(PermissionGroup)permission {
    return [LocationPermissionStrategy permissionStatus:permission];
}

+ (PermissionStatus)permissionStatus:(PermissionGroup)permission {
    CLAuthorizationStatus authorizationStatus = [CLLocationManager authorizationStatus]; // synchronous, can block
    ...
}

Since PermissionManager.checkPermissionStatus:result: calls this synchronously and immediately passes the result to FlutterResult, the main thread is blocked for however long authorizationStatus takes internally (observed 2000ms+ on affected devices/iOS versions in production).

Possible fix

Mirroring the checkServiceStatus fix, checkPermissionStatus could dispatch the location-specific call off the main thread and hop back before invoking the result callback, e.g. (scoped to location permission groups only, to avoid touching the other ~10 permission strategies that don't have this issue):

+ (void)checkPermissionStatus:(enum PermissionGroup)permission result:(FlutterResult)result {
    id<PermissionStrategy> permissionStrategy = [PermissionManager permissionStrategy:permission];

    if (permission == PermissionGroupLocation || permission == PermissionGroupLocationAlways ||
        permission == PermissionGroupLocationWhenInUse) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            PermissionStatus status = [permissionStrategy checkPermissionStatus:permission];
            dispatch_async(dispatch_get_main_queue(), ^{ result(@(status)); });
        });
        return;
    }

    PermissionStatus status = [permissionStrategy checkPermissionStatus:permission];
    result(@(status));
}

Expected results

Permission.location.status / .isGranted should never be able to block the calling (main) thread for multiple seconds, consistent with how serviceStatus already behaves after #1329.

Actual results

Main thread hangs 2000ms+ inside [CLLocationManager authorizationStatus], reported by Sentry's App Hang detector on iOS, primarily right after the app becomes active again.

Code sample

Code sample
[Paste your code here]

Screenshots or video

Screenshots or video demonstration

[Upload media here]

Version

permission_handler: 13.0.1 / permission_handler_apple: 9.6.1

Flutter Doctor output

Doctor output
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.44.8, on macOS 26.5.2 25F84 darwin-arm64, locale de-AT)
[✓] Android toolchain - develop for Android devices (Android SDK version 37.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 26.6)
[✓] Chrome - develop for the web
[✓] Connected device (2 available)
[✓] Network resources

• No issues found!

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions