-
Notifications
You must be signed in to change notification settings - Fork 28.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make iOS Flutter framework extension-safe #165346
base: master
Are you sure you want to change the base?
Make iOS Flutter framework extension-safe #165346
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mostly minor nits except for the comment about keeping the old artifact for a release cycle, even if it's identical to Flutter.framework.
'artifacts/engine/ios-profile/extension_safe/Flutter.xcframework/ios-arm64/Flutter.framework/Flutter', | ||
'artifacts/engine/ios-profile/extension_safe/Flutter.xcframework/ios-arm64_x86_64-simulator/Flutter.framework/Flutter', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sooo nice we can make this change atomically with the engine now.
@@ -248,253 +248,6 @@ | |||
"sdk_version": "16c5032a" | |||
} | |||
} | |||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we should keep building these artifacts for a release (even though it doesn't really work) in case someone is using it, and then put the deprecation in the tech blog with instructions for migrating? I bet people have prototypes that "work" locally and I don't want them to be mysteriously busted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm fine either way as I expect this is a relatively quick and painless fix for users, but agreed, it seems like a good idea to give people a release worth of heads-up in the release notes.
if (@available(iOS 13.0, *)) { | ||
[self setUpSceneLifecycleNotifications:center]; | ||
} else { | ||
if ([FlutterSharedApplication isAvailable]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dot notation for properties, including class properties.
if ([FlutterSharedApplication isAvailable]) { | |
if (FlutterSharedApplication.isAvailable) { |
if (@available(iOS 13.0, *)) { | ||
[self setUpSceneLifecycleNotifications:center]; | ||
} else { | ||
if ([FlutterSharedApplication isAvailable]) { | ||
[self setUpApplicationLifecycleNotifications:center]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of the caller (in two places) needing to understand the extension and API limitations of this call, can the logic be pushed down into one setUpLifecycleNotifications:
method?
/** | ||
* Check whether the main bundle is an iOS App Extension. | ||
*/ | ||
+ (BOOL)isAppExtension; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be a readonly class property.
+ (BOOL)isAppExtension; | |
@property(class, nonatomic, readonly) BOOL isAppExtension; |
/** | ||
* Check whether the UIApplication is available. UIApplication is not available for App Extensions. | ||
*/ | ||
+ (BOOL)isAvailable; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same.
/** | ||
* Returns the `UIApplication.sharedApplication` is available. Otherwise returns nil. | ||
*/ | ||
+ (UIApplication*)uiApplication; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
uiApplication
with the prefix is kind of odd looking, how about sharedApplication
or just application
?
Also a class property.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed just application
.
@interface FlutterSharedApplication () | ||
|
||
+ (UIApplication*)sharedApplication; | ||
|
||
@end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this category necessary?
@implementation FlutterSharedApplication | ||
|
||
+ (BOOL)isAppExtension { | ||
NSDictionary* nsExtension = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSExtension"]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NSDictionary* nsExtension = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSExtension"]; | |
NSDictionary* nsExtension = [NSBundle.mainBundle objectForInfoDictionaryKey:@"NSExtension"]; |
@@ -53,6 +54,12 @@ - (MTLPixelFormat)pixelFormat { | |||
- (BOOL)isWideGamutSupported { | |||
FML_DCHECK(self.screen); | |||
|
|||
// Wide Gamut is not supported for iOS Extensions due to memory limitations | |||
// (see https://github.com/flutter/flutter/issues/165086). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch figuring out that would help with memory.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is exciting! Overall looks good!
userInfo:nil]; | ||
id mockEngine = OCMPartialMock(engine); | ||
[[NSNotificationCenter defaultCenter] postNotification:sceneNotification]; | ||
[[NSNotificationCenter defaultCenter] postNotification:applicationNotification]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NSNotificationCenter.defaultCenter
userInfo:nil]; | ||
id mockEngine = OCMPartialMock(engine); | ||
[[NSNotificationCenter defaultCenter] postNotification:sceneNotification]; | ||
[[NSNotificationCenter defaultCenter] postNotification:applicationNotification]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto.
UISceneActivationStateBackground; | ||
UIApplication* flutterApplication = [FlutterSharedApplication uiApplication]; | ||
if (flutterApplication != nil) { | ||
_isGpuDisabled = flutterApplication.applicationState == UIApplicationStateBackground; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is consistent with surrounding code, but this method is about half and half usage of self.foo
and _foo
. I did passes on a few of the classes during ARC migration, but didn't hit the engine. This is defined as a BOOL property in the public header and we're not in an initializer -- this should only be called via [engine run.*]
methods. i.e. go for self.isGpuDisabled
.
_isGpuDisabled = NO; | ||
if (@available(iOS 13.0, *)) { | ||
_isGpuDisabled = self.viewController.flutterWindowSceneIfViewLoaded.activationState == | ||
UISceneActivationStateBackground; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Feels like an opportunity to encapsulate lifecycle state somewhere so we don't need long chains of lookups. e.g. self.viewController.activationState
or self.viewController.isBackground
or similar.
[[NSNotificationCenter defaultCenter] postNotification:applicationNotification]; | ||
OCMVerify(times(1), [mockEngine sceneDidEnterBackground:[OCMArg any]]); | ||
XCTAssertTrue(engine.isGpuDisabled); | ||
bool switch_value = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bool switch_value = false; | |
BOOL gpuDisabled = NO; |
}]]; | ||
#endif | ||
UIApplication* flutterApplication = [FlutterSharedApplication uiApplication]; | ||
NSSet<UIScene*>* scenes; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion to guarantee we never hand a nil scenes
to requestGeometryUpdateForWindowScenes:
:
NSSet<UIScene*>* scenes; | |
NSSet<UIScene*>* scenes = [NSSet set]; |
id scene, NSDictionary* bindings) { | ||
return [scene isKindOfClass:[UIWindowScene class]]; | ||
}]]; | ||
} else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
} else { | |
} else if (self.flutterWindowSceneIfViewLoaded) { |
scenes = self.flutterWindowSceneIfViewLoaded | ||
? [NSSet setWithObject:self.flutterWindowSceneIfViewLoaded] | ||
: [NSSet set]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
scenes = self.flutterWindowSceneIfViewLoaded | |
? [NSSet setWithObject:self.flutterWindowSceneIfViewLoaded] | |
: [NSSet set]; | |
scenes = [NSSet setWithObject:self.flutterWindowSceneIfViewLoaded]; |
userInfo:nil]; | ||
id mockVC = OCMPartialMock(flutterViewController); | ||
[[NSNotificationCenter defaultCenter] postNotification:sceneNotification]; | ||
[[NSNotificationCenter defaultCenter] postNotification:applicationNotification]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here and below: NSNotificationCenter.defaultCenter
OCMStub([mockBundle objectForInfoDictionaryKey:@"NSExtension"]).andReturn(@{ | ||
@"NSExtensionPointIdentifier" : @"com.apple.share-services" | ||
}); | ||
XCTAssertTrue([FlutterSharedApplication isAppExtension]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here and below: dot notation.
Our current adoption/documentation for iOS Extensions does not currently work because it's disallowed to nest bundles (see #142531).
As of Xcode 13, linking to frameworks that contain non-extension-safe code no longer gives warnings (or blocks from App Store it seems). Therefore, it has become a runtime issue to ensure non-extension-safe code is not used.
Previously, we were building and shipping 2 separate Flutter.xcframeworks. One that was extension-safe and one that was not.
This PR removes the "extension_safe" framework and instead makes the entire framework extension-safe by annotating non-extension-safe code with
NS_EXTENSION_UNAVAILABLE_IOS
and ensuring that code is not used at runtime if the bundle is for an app extension.This PR also disables wide gamut for app extensions to decrease the chances of crashes (see #165086).
Fixes #142531.
For reference:
App extensions were first evaluated in https://flutter.dev/go/app-extensions. Here is the reasoning why neither method described there is opportune.
Option 1 -
I did look into splitting the framework into 2 frameworks (one with all extension-safe code and one with the non-extension-safe code). However, the original idea was to use objc Categories/Extensions - this doesn’t quite fit our needs. Categories/Extensions only allow you to add new methods/properties, not override existing ones. We could hypothetically add new methods, but that would require the user to change their code to use the new methods.
I also looked into subclasses which does allow overrides, but it would also require the user to change their code to use the new class.
We could do method swizzling, but opinion of that on the internet is that it's not very safe.
I’m of the opinion that anything that requires the user to change code isn’t super feasible due to plugins.
Option 2 -
We could still do the 2 frameworks but rename one to
FlutterExtentionSafe
. This works without users needing to change any code (including imports like@import Flutter
/#import <Flutter/Flutter.h>
). I believe the reason this works is because at compile time, it finds theFlutter
framework on the framework search path and it imports in the headers. Then at link time,FlutterExtentionSafe
is explicitly linked so it uses that framework first when checking for symbols and since it finds all the symbols inFlutterExtentionSafe
, it doesn’t need/try to auto-link theFlutter
framework (despiteFlutter
being the framework imported).This seems precarious to me since we’re relying on Xcode to not auto-link the
Flutter
framework. If for some reasonFlutter
framework did get auto-linked (such as the user using a symbol that’s not in theFlutterExtensionSafe
framework but is in theFlutter
framework - this is unlikely though), we’d get name collision issuesPre-launch Checklist
///
).If you need help, consider asking for advice on the #hackers-new channel on Discord.