Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

[google_maps_flutter] Fix iOS crash by observing map frame change only once #3426

Merged
merged 19 commits into from
Mar 21, 2022
Merged

[google_maps_flutter] Fix iOS crash by observing map frame change only once #3426

merged 19 commits into from
Mar 21, 2022

Conversation

guykogus
Copy link
Contributor

I cannot to tell you the stress this bug has caused me 😅

The view method gets called a lot. Every draw cycle, in fact. And if the frame should change, for example when the keyboard appears, every single one of those observeValueForKeyPath: observations will be called, which crashes an app with the following exception: EXC_BAD_ACCESS KERN_PROTECTION_FAILURE

Screen Shot 2021-01-15 at 00 17 58

Pre-launch Checklist

  • The title of the PR starts with the name of the plugin surrounded by square brackets, e.g. [shared_preferences]
  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the Flutter Style Guide and the C++, Objective-C, Java style guides.
  • I listed at least one issue that this PR fixes in the description above.
  • I added new tests to check the change I am making or feature I am adding, or Hixie said the PR is test exempt.
  • I updated pubspec.yaml with an appropriate new version according to the pub versioning philosophy.
  • I updated CHANGELOG.md to add a description of the change.
  • I updated/added relevant documentation (doc comments with ///).
  • I signed the CLA.
  • All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel on Discord.

@guykogus guykogus requested a review from cyanglaz as a code owner January 14, 2021 23:22
@google-cla google-cla bot added the cla: yes label Jan 14, 2021
@MarceLifeBites
Copy link

Thx @guykogus , we are having de same bug here, and fix it with help a lot with our platform stability, so i wish they can release the solution as soon as possible

@pawlowskim
Copy link

Exactly the same happens for us.

Crashed: com.apple.main-thread
EXC_BAD_ACCESS KERN_PROTECTION_FAILURE 0x000000016f713ff8
-[GMSMapView internalSetCamera:]

Crashed: com.apple.main-thread
0  libsystem_pthread.dylib        0x1cfd358c4 ___chkstk_darwin + 64
1  libsystem_pthread.dylib        0x1cfd358c4 thread_chkstk_darwin + 64
2  Foundation                     0x18a9b36cc NSKeyValueWillChange + 128
3  Foundation                     0x18a9b0d24 NSKeyValueWillChangeWithPerThreadPendingNotifications + 384
4  Runner                         0x10096123c -[GMSMapView internalSetCamera:] + 32268
5  Runner                         0x10095e808 -[GMSMapView updateWithCamera:] + 21464
6  Foundation                     0x18aa19614 __NSThreadPerformPerform + 184
7  CoreFoundation                 0x1896a5be0 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 24
8  CoreFoundation                 0x1896a5ae0 __CFRunLoopDoSource0 + 204
9  CoreFoundation                 0x1896a4e28 __CFRunLoopDoSources0 + 256
10 CoreFoundation                 0x18969f3d0 __CFRunLoopRun + 776
11 CoreFoundation                 0x18969eb90 CFRunLoopRunSpecific + 572
12 GraphicsServices               0x19f9c1598 GSEventRunModal + 160
13 UIKitCore                      0x18bf88638 -[UIApplication _run] + 1052
14 UIKitCore                      0x18bf8dbb8 UIApplicationMain + 164
15 Runner                         0x1005f7948 main + 10 (AppDelegate.swift:10)
16 libdyld.dylib                  0x18937d588 start + 4

Flutter 1.22.5, google_maps_flutter: 1.0.6. For us it happens when user stays on a map for ~30 minutes (app in foreground, screen is on) and we are tracking his position and update his position marker (custom one, so we are re-drawing circle on a map). To simulate it, I just use more or less this piece of code:

int a = 0;
    Timer.periodic(const Duration(milliseconds: 10), (timer) {
      if (_mapController != null && position != null && mounted) {
        a++;
        double b = a > 50000 ? -0.0003 : 0.0003;
        setState(() {
          position = Position(
              longitude: position.longitude,
              latitude: position.latitude + b);
        });
        _mapController.animateCamera(CameraUpdate.newLatLng(
            LatLng(position.latitude, position.longitude)));
      }
    });

I can confirm changes from this pull seems to resolves the problem. Beside, it should be released with version lower than 2.0.0 because plenty of projects depends on more libraries than only google maps and won't be migrated to flutter 2.0.0 for a while (dependency hell 😢 )

@guykogus
Copy link
Contributor Author

guykogus commented Mar 9, 2021

I can confirm changes from this pull seems to resolves the problem. Beside, it should be released with version lower than 2.0.0 because plenty of projects depends on more libraries than only google maps and won't be migrated to flutter 2.0.0 for a while (dependency hell 😢 )

Unfortunately there's no branch in the original repo to merge with to keep it at version < 2.0 ☹️

@pawlowskim
Copy link

I can confirm changes from this pull seems to resolves the problem. Beside, it should be released with version lower than 2.0.0 because plenty of projects depends on more libraries than only google maps and won't be migrated to flutter 2.0.0 for a while (dependency hell 😢 )

Unfortunately there's no branch in the original repo to merge with to keep it at version < 2.0 ☹️

I'm aware, but the fix won't be usable for old projects that have dozens of non flutter / dart team libraries dependencies. Until then, guess we will use your fork:

  google_maps_flutter:
      git:
          url: git://github.com/guykogus/flutter_plugins
          path: packages/google_maps_flutter/google_maps_flutter
          ref: 6156ea3fa2ecf4b0e7ff35ba9a5fb1414e181d6d

@guykogus
Copy link
Contributor Author

guykogus commented Mar 9, 2021

I'm aware, but the fix won't be usable for old projects that have dozens of non flutter / dart team libraries dependencies. Until then, guess we will use your fork:

  google_maps_flutter:
      git:
          url: git://github.com/guykogus/flutter_plugins
          path: packages/google_maps_flutter/google_maps_flutter
          ref: 6156ea3fa2ecf4b0e7ff35ba9a5fb1414e181d6d

You can use ref: via if you want. I've got another fix there for Android, which can crash if the native Android map loads after the Flutter map is unmounted (i.e. the StatefulWidget is disposed). Unfortunately the PR was rejected cos they couldn't reproduce it.

@sangTroo
Copy link

sangTroo commented Mar 9, 2021

Flutter 1.22.5, google_maps_flutter: 1.0.6. For us it happens when user stays on a map for ~30 minutes (app in foreground, screen is on) and we are tracking his position and update his position marker (custom one, so we are re-drawing circle on a map). To simulate it, I just use more or less this piece of code:

We have the same problem. Will try this branch to see if it fixes the issue. The app that we're building is heavily using maps. And hasn't been stable since this bug keeps the app crashing

@carlosmobile
Copy link

Hi! We have the same problem. We are using a fork with this changes and fixed the crash. Thanks!

Copy link
Contributor

@cyanglaz cyanglaz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for providing the fix, the change looks good.

It's not clear to me in what scenario the view is called multiple times for a single webview.
It looks like it's only called once per controller, see: https://github.com/flutter/engine/blob/master/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm#L161

Again, this is a good fix nonetheless, the old code is pretty confusing and didn't do what it says it wants to do.

However, we should add some tests before landing this. I wonder If you know a way to reproduce this consistently.

@via-guy
Copy link

via-guy commented Apr 29, 2021

Thanks for providing the fix, the change looks good.

It's not clear to me in what scenario the view is called multiple times for a single webview.
It looks like it's only called once per controller, see: https://github.com/flutter/engine/blob/master/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm#L161

Again, this is a good fix nonetheless, the old code is pretty confusing and didn't do what it says it wants to do.

However, we should add some tests before landing this. I wonder If you know a way to reproduce this consistently.

I couldn't come up with one when I made this fix. It happens in our project, and clearly it does for others too. Do any of you have a simple sample test that can be provided here, @pawlowskim @sangTroo @carlosmobile ?

@sangTroo
Copy link

Any update when this fix will be merged?

@sangTroo
Copy link

Thanks for providing the fix, the change looks good.
It's not clear to me in what scenario the view is called multiple times for a single webview.
It looks like it's only called once per controller, see: https://github.com/flutter/engine/blob/master/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm#L161
Again, this is a good fix nonetheless, the old code is pretty confusing and didn't do what it says it wants to do.
However, we should add some tests before landing this. I wonder If you know a way to reproduce this consistently.

I couldn't come up with one when I made this fix. It happens in our project, and clearly it does for others too. Do any of you have a simple sample test that can be provided here, @pawlowskim @sangTroo @carlosmobile ?

Hi,
What kind of simple sample test do you want, sorry I don't get what you mean

Copy link
Contributor

@stuartmorgan-g stuartmorgan-g left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this should be easily testable with a native unit test:

  • Allow injecting a mock GMSMapView
  • Call view several times
  • Ensure that addObserver:... is not called on the mock repeatedly.

@via-guy
Copy link

via-guy commented Oct 6, 2021

@stuartmorgan I've been playing a bit with doing something like that but I don't really know how to, at least not without breaking the current code structure/API. Any extra guide would be greatly appreciated.

@stuartmorgan-g
Copy link
Contributor

stuartmorgan-g commented Oct 25, 2021

@stuartmorgan I've been playing a bit with doing something like that but I don't really know how to, at least not without breaking the current code structure/API. Any extra guide would be greatly appreciated.

Can you elaborate on the issue you are having?

If it's the mock injection, for something that's created in init like the map view you generally add a new init method with the thing you want to inject added, which becomes the main initializer, and the real one delegates to that. E.g.:

- (instancetype)initWithFrame:(CGRect)frame
               viewIdentifier:(int64_t)viewId
                    arguments:(id _Nullable)args
                    registrar:(NSObject<FlutterPluginRegistrar>*)registrar {
    GMSCameraPosition* camera = ToOptionalCameraPosition(args[@"initialCameraPosition"]);
    GMSMapView* mapView = [GMSMapView mapWithFrame:frame camera:camera];
    return [self initWithMapView:mapView ...];
};

- (instancetype)initWithMapView:(GMSMapView)mapView
               viewIdentifier:(int64_t)viewId
                    arguments:(id _Nullable)args
                    registrar:(NSObject<FlutterPluginRegistrar>*)registrar {
  if (self = [super init]) {
    _mapView = mapView;
    _viewId = viewId;

    _mapView.accessibilityElementsHidden = NO;
    ...

You expose the new init method in a ...-Test.h header file, and use it in the unit test.

@godofredoc godofredoc changed the base branch from master to main January 6, 2022 22:57
@flutter-dashboard
Copy link

It looks like this pull request may not have tests. Please make sure to add tests before merging. If you need an exemption to this rule, contact Hixie on the #hackers channel in Chat.

If you are not sure if you need tests, consider this rule of thumb: the purpose of a test is to make sure someone doesn't accidentally revert the fix. Ask yourself, is there anything in your PR that you feel it is important we not accidentally revert back to how it was before your fix?

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing.

@stuartmorgan-g
Copy link
Contributor

@guykogus Are you planning on adding tests per #3426 (review) so that this can move forward?

@guykogus
Copy link
Contributor Author

@stuartmorgan yes, I really want to because this change is essential, I just haven't had a chance to do it...

@via-guy
Copy link

via-guy commented Feb 14, 2022

@stuartmorgan I've added some tests, but I'm not sure if I've really done it correctly. Basically I just tested to see if it doesn't lag a long time to loop over view. On the master branch the test fails, but it passes on mine.

@@ -2,6 +2,7 @@

* Removes dependencies from `pubspec.yaml` that are only needed in `example/pubspec.yaml`
* Updates Android compileSdkVersion to 31.
* Fix iOS crash on `EXC_BAD_ACCESS KERN_PROTECTION_FAILURE` if the map frame changes long after creation.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixes

(Per the repo CHANGELOG style guide.)


NS_ASSUME_NONNULL_BEGIN

@interface MockRegistrar : NSObject <FlutterPluginRegistrar>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you mocking these manually instead of with OCMock?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've never used OCMock. Thanks for the tip!

}
[[controller view] setValue:[NSValue valueWithCGRect:CGRectMake(0, 0, 0, 0)] forKey:@"frame"];

[viewExpectation fulfill];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what this test is supposed to be doing. It's not asserting what this PR is actually intended to fix.

I described what a test should look like above; I'm not sure why you've done something completely different.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point. Got distracted with seeing if I could actually test for the crash itself to happen. I'll update it.

@via-guy
Copy link

via-guy commented Feb 14, 2022

@stuartmorgan I've updated according to your suggestions, but I'm not sure about the placement of GoogleMapController-Test.h. I added it into the example project itself, because otherwise it would be exported with the pod. Is that correct, or is there some way in the podspec to specify target test files?

@stuartmorgan-g
Copy link
Contributor

@stuartmorgan I've updated according to your suggestions, but I'm not sure about the placement of GoogleMapController-Test.h. I added it into the example project itself, because otherwise it would be exported with the pod. Is that correct, or is there some way in the podspec to specify target test files?

It should live next to the source file; you can use a module map to control how it's exposed. See #4430 for an example of setting that up.

@@ -31,6 +31,8 @@ target 'Runner' do
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths

pod 'OCMock'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should have a version. See other uses for examples.


@interface FLTGoogleMapController (Test)

- (instancetype)initWithMapView:(GMSMapView *)mapView
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All methods need declaration comments, per the ObjC style guide.


@import GoogleMaps;

@interface MockGMSMapView : GMSMapView
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs a declaration comment.


@interface MockGMSMapView : GMSMapView

@property(nonatomic, assign, readonly) NSInteger frameObserverCount;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs a declaration comment.


@import GoogleMaps;

@interface MockGMSMapView : GMSMapView
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should probably call this PartiallyMockedMapView or something along those lines; the assumption for a mock object is not that it is the real object but with some overridden behavior, but that it has no behavior other than what it is given in test setup.

@@ -3,6 +3,7 @@
* Removes dependencies from `pubspec.yaml` that are only needed in `example/pubspec.yaml`
* Updates Android compileSdkVersion to 31.
* Internal code cleanup for stricter analysis options.
* Fixes iOS crash on `EXC_BAD_ACCESS KERN_PROTECTION_FAILURE` if the map frame changes long after creation.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can't add changes to a version that has already been published. It needs a new version.


@import GoogleMaps;

// Defines a map view used for testing key-value observing.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// Defines a map view used for testing key-value observing.
@interface PartiallyMockedMapView : GMSMapView

/// Counts the number of times that `frame` KVO has been added
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same. You're also missing the period; comments should be properly punctuated, per the style guide.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, this is a property, so should describe a noun rather than a verb; methods do things, properties are things. I.e., "The number of times that [...]".

@interface FLTGoogleMapController (Test)

/**
* Initialize a map controller with a concrete map view.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Initializes

(Per style guide.)

@via-guy
Copy link

via-guy commented Mar 8, 2022

@stuartmorgan done

@jmagman
Copy link
Member

jmagman commented Mar 18, 2022

There's a format error, please run:

patch -p1 <<DONE
diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/google_maps_flutter-umbrella.h b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/google_maps_flutter-umbrella.h
index 5af927f61..50880a2b9 100644
--- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/google_maps_flutter-umbrella.h
+++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/google_maps_flutter-umbrella.h
@@ -3,8 +3,8 @@
 // found in the LICENSE file.
 
 #import <Foundation/Foundation.h>
-#import <google_maps_flutter/FLTGoogleMapsPlugin.h>
 #import <google_maps_flutter/FLTGoogleMapTileOverlayController.h>
+#import <google_maps_flutter/FLTGoogleMapsPlugin.h>
 #import <google_maps_flutter/JsonConversions.h>
 
 FOUNDATION_EXPORT double google_maps_flutterVersionNumber;

DONE

@via-guy
Copy link

via-guy commented Mar 20, 2022

@jmagman done

Copy link
Contributor

@stuartmorgan-g stuartmorgan-g left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@stuartmorgan-g stuartmorgan-g added waiting for tree to go green (Use "autosubmit") This PR is approved and tested, but waiting for the tree to be green to land. and removed needs tests labels Mar 21, 2022
@fluttergithubbot fluttergithubbot merged commit 5b49d0f into flutter:main Mar 21, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request Mar 22, 2022
mauricioluz pushed a commit to mauricioluz/plugins that referenced this pull request Jan 26, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
cla: yes p: google_maps_flutter platform-ios waiting for tree to go green (Use "autosubmit") This PR is approved and tested, but waiting for the tree to be green to land.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants