[google_maps_flutter] share marker icons between identical bitmaps - #12957
jopmiddelkamp wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Code Review
This pull request implements caching of UIImage instances created from BitmapDescriptor.bytes across several iOS Google Maps packages to prevent exhausting the Maps SDK's texture atlases. It introduces NSCache storage keyed by SHA256 hashes of the image data and scaling parameters, and adds corresponding unit tests. Feedback suggests adding a defensive check for nil or empty NSData before executing CC_SHA256 in the Objective-C implementation to avoid potential undefined behavior.
| static NSString *FGMBytesMapIconCacheKey(FGMPlatformBitmapBytesMap *bitmap, CGFloat screenScale) { | ||
| NSData *data = bitmap.byteData.data; | ||
| unsigned char digest[CC_SHA256_DIGEST_LENGTH]; | ||
| CC_SHA256(data.bytes, (CC_LONG)data.length, digest); | ||
| NSData *digestData = [NSData dataWithBytes:digest length:CC_SHA256_DIGEST_LENGTH]; | ||
| NSString *contentHash = [digestData base64EncodedStringWithOptions:0]; | ||
| long scaling = (long)bitmap.bitmapScaling; | ||
| return | ||
| [NSString stringWithFormat:@"%@|%ld|%f|%@|%@|%f", contentHash, scaling, | ||
| bitmap.imagePixelRatio, bitmap.width, bitmap.height, screenScale]; | ||
| } |
There was a problem hiding this comment.
To ensure robust defensive programming, we should guard against potential nil or empty NSData before calling CC_SHA256. Passing a NULL pointer to CC_SHA256 (which happens if data is nil or empty) can trigger static analysis warnings or undefined behavior depending on the environment. Checking data.length > 0 before hashing is a safer approach.
static NSString *FGMBytesMapIconCacheKey(FGMPlatformBitmapBytesMap *bitmap, CGFloat screenScale) {
NSData *data = bitmap.byteData.data;
NSString *contentHash = @"";
if (data.length > 0) {
unsigned char digest[CC_SHA256_DIGEST_LENGTH];
CC_SHA256(data.bytes, (CC_LONG)data.length, digest);
NSData *digestData = [NSData dataWithBytes:digest length:CC_SHA256_DIGEST_LENGTH];
contentHash = [digestData base64EncodedStringWithOptions:0];
}
long scaling = (long)bitmap.bitmapScaling;
return
[NSString stringWithFormat:@"%@|%ld|%f|%@|%@|%f", contentHash, scaling,
bitmap.imagePixelRatio, bitmap.width, bitmap.height, screenScale];
}
On iOS the plugin builds a new
UIImagefor every marker whose icon comes fromBitmapDescriptor.bytes, even when two markers hand it identical bytes and identical scaling. That isPlatformBitmap.createIconin the Swift packages andFGMIconFromBitmapin the Objective-C one.The Google Maps SDK allocates marker texture space per
UIImageinstance. It does not look at the image content. So a map with 4000 markers that share 22 images still asks for 4000 texture slots, the atlases run out, and the SDK starts loggingAn app-level cache does not get you out of this. The app I hit it in already creates each
BitmapDescriptoronce and reuses it across markers, and the plugin still creates oneUIImageper marker.This change caches the finished icon in a process-wide
NSCache. The key is the SHA-256 of the bytes plusbitmapScaling,imagePixelRatio,width,heightandscreenScale, which is every input that code path reads, so two bitmaps only share an instance when they would have produced the same image.UIImageis immutable, so sharing one between markers is safe.NSCacheis thread-safe and drops its contents when the system is short on memory. Only the bytes-map branch changes. No Dart, no platform interface.I measured it on an iPhone 17 simulator (iOS 26.4) with GoogleMaps SDK 9.4.0, using a sample app with 4000 markers, 22 distinct images and the marker geometry from a production fleet app, which averages 24753 physical pixels per marker on a 3x screen. Same seed, same 12 pans, only the plugin swapped:
google_maps_flutter_iosThree measurements point at the instance count rather than the images themselves. Going from 110 distinct images to 22 changed nothing, 928 errors either way. Image size does matter: at 1500 markers over the same area, 29 x 29 px icons gave 0 errors and 96 x 96 px icons gave 300. But shrinking them stops helping once you are past the limit, and halving the pixels per marker only moved the count from 3722 to 3585. Sharing the
UIImagetakes it to 0. That is why this tends to bite apps with large labelled markers and stays invisible in a small test.The same change is in all three iOS implementations, so it reaches everyone. That includes
google_maps_flutter_ios, which is still the iOSdefault_packageofgoogle_maps_flutter. The README there says the package will not receive new feature updates, and this is a bug fix, so I read that as in scope. Say if you would rather have it as a separate PR.One thing I should be straight about: I reproduce the atlas exhaustion, not the visual corruption. In production we see markers drawn with pieces of other markers' images, or missing entirely. On the simulator the SDK logs the failure and still draws. The linked issue keeps those two apart.
Fixes flutter/flutter#193105
Pre-Review Checklist
[shared_preferences]///).If you need help, consider asking for advice on the #hackers-new channel on Discord.
Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the
gemini-code-assistbot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.Footnotes
Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling. ↩ ↩2