UIMenuController 在 macOS Monterey 上崩溃
UIMenuController crashes on macOS Monterey
我的 iOS 应用程序使用 UIMenuController 显示 Copy/Paste 上下文菜单。当我在 macOS 12.0 上启动应用程序并使用鼠标或触控板按住 control 单击(右键单击)时,应用程序在显示菜单时崩溃并带有此崩溃日志:
Application Specific Information:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason:
'*** -[__NSDictionaryM setObject:forKey:]: object cannot be nil (key: title)'
Last Exception Backtrace:
0 CoreFoundation 0x1be758118 __exceptionPreprocess + 220
1 libobjc.A.dylib 0x1be4a9808 objc_exception_throw + 60
2 CoreFoundation 0x1be828464 -[__NSCFString characterAtIndex:].cold.1 + 0
3 CoreFoundation 0x1be835270 -[__NSDictionaryM setObject:forKey:].cold.3 + 0
4 CoreFoundation 0x1be691590 -[__NSDictionaryM setObject:forKey:] + 904
5 UIKitCore 0x1e5b85998 -[_UIMenuBarItem properties] + 124
6 UIKitMacHelper 0x1d3bc7058 UINSNSMenuItemFromUINSMenuItem + 96
7 UIKitMacHelper 0x1d3bc6d60 _insertUINSMenuItemsIntoNSMenu + 844
8 UIKitMacHelper 0x1d3bc67c0 UINSNSMenuFromUINSMenu + 152
9 UIKitMacHelper 0x1d3bc6690 -[UINSMenuController _createNSMenu:forContextMenu:] + 92
10 UIKitMacHelper 0x1d3c3505c -[UINSMenuController _prepareToShowContextMenu:activityItemsConfiguration:] + 144
11 UIKitMacHelper 0x1d3c349c0 -[UINSMenuController showContextMenu:inWindow:atLocationInWindow:activityItemsConfiguration:] + 312
12 libdispatch.dylib 0x1be44ce60 _dispatch_call_block_and_release + 32
13 libdispatch.dylib 0x1be44ebac _dispatch_client_callout + 20
14 libdispatch.dylib 0x1be45d0ac _dispatch_main_queue_callback_4CF + 944
15 CoreFoundation 0x1be719e60 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 16
我对其他开发人员的几个 iOS 应用程序进行了相同的尝试,当我用鼠标右键单击时,它们都在 macOS 上崩溃了。
有没有人找到解决方法?
可以通过在私有 _UIMenuBarItem
class 上调整 properties
方法来解决这个问题。显然,这附带了通常的免责声明,这可能会让你被 Apple 拒绝(但实际上,这似乎并不经常导致拒绝)。
解决方法如下:基本思路是将原始方法调用包装在 @try/@catch
块中。发生崩溃是因为最初的实现有时会尝试将 title
键的 nil
值插入到 NSDictionary 中。此解决方法捕获该异常,然后 returns 一个虚拟字典以满足调用者的需求。
UIMenuBarItemMontereyCrashFix.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/// Helper class to apply a fix to prevent a crash on macOS Monterey when a user right-clicks in a text field
@interface UIMenuBarItemMontereyCrashFix : NSObject
/// Apply the crash fix. It will only be applied the first time it's called, subsequent calls are no-ops.
/// It will only have an effect when called on macOS Monterey or later.
+ (void)applyCrashFixIfNeeded;
@end
NS_ASSUME_NONNULL_END
UIMenuBarItemMontereyCrashFix.m
#import "UIMenuBarItemMontereyCrashFix.h"
#import <objc/runtime.h>
static BOOL hasCrashFixBeenApplied = NO;
@implementation UIMenuBarItemMontereyCrashFix
/// Apply the crash fix. It will only be applied the first time it's called, subsequent calls are no-ops.
+ (void)applyCrashFixIfNeeded
{
if (@available(macOS 12.0, *)) {} else {
// Bail if we are not running on Monterey
return;
}
if (!hasCrashFixBeenApplied) {
Class UnderscoreUIMenuBarItem = NSClassFromString(@"_UIMenuBarItem");
SEL selector = sel_getUid("properties");
Method method = class_getInstanceMethod(UnderscoreUIMenuBarItem, selector);
IMP original = method_getImplementation(method);
// The crash happens because in some instances the original implementation
// tries to insert `nil` as a value for the key `title` into a dictionary.
// This is how the fix works:
// We wrap the original implementation call in a @try/@catch block. When the
// exception happens, we catch it, and then return a dummy dictionary to
// satisfy the caller. The dummy has `isEnabled` set to NO, and `isHidden` set
// to YES.
IMP override = imp_implementationWithBlock(^id(id me) {
@try {
id res = ((id (*)(id))original)(me);
return res;
}
@catch(NSException *exception) {
return @{
@"allowsAutomaticKeyEquivalentLocalization" : @0,
@"allowsAutomaticKeyEquivalentMirroring" : @0,
@"defaultCommand" : @0,
@"identifier":@"com.apple.menu.application",
@"imageAlwaysVisible" : @0,
@"isAlternate" : @0,
@"isEnabled" : @0,
@"isHidden" : @1,
@"isSeparatorItem" : @0,
@"keyEquivalent" : @"",
@"keyEquivalentModifiers" : @0,
@"remainsVisibleWhenDisabled" : @0,
@"state" : @0,
@"title" : @""
};
}
});
method_setImplementation(method, override);
hasCrashFixBeenApplied = YES;
}
}
@end
记得将 UIMenuBarItemMontereyCrashFix.h
添加到您的桥接 header 中,以便您可以从 Swift 调用它。然后只需在您的应用启动序列中的某处调用 UIMenuBarItemMontereyCrashFix.applyIfNeeded()
(例如在您的 AppDelegate 中)。
我的 iOS 应用程序使用 UIMenuController 显示 Copy/Paste 上下文菜单。当我在 macOS 12.0 上启动应用程序并使用鼠标或触控板按住 control 单击(右键单击)时,应用程序在显示菜单时崩溃并带有此崩溃日志:
Application Specific Information:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason:
'*** -[__NSDictionaryM setObject:forKey:]: object cannot be nil (key: title)'
Last Exception Backtrace:
0 CoreFoundation 0x1be758118 __exceptionPreprocess + 220
1 libobjc.A.dylib 0x1be4a9808 objc_exception_throw + 60
2 CoreFoundation 0x1be828464 -[__NSCFString characterAtIndex:].cold.1 + 0
3 CoreFoundation 0x1be835270 -[__NSDictionaryM setObject:forKey:].cold.3 + 0
4 CoreFoundation 0x1be691590 -[__NSDictionaryM setObject:forKey:] + 904
5 UIKitCore 0x1e5b85998 -[_UIMenuBarItem properties] + 124
6 UIKitMacHelper 0x1d3bc7058 UINSNSMenuItemFromUINSMenuItem + 96
7 UIKitMacHelper 0x1d3bc6d60 _insertUINSMenuItemsIntoNSMenu + 844
8 UIKitMacHelper 0x1d3bc67c0 UINSNSMenuFromUINSMenu + 152
9 UIKitMacHelper 0x1d3bc6690 -[UINSMenuController _createNSMenu:forContextMenu:] + 92
10 UIKitMacHelper 0x1d3c3505c -[UINSMenuController _prepareToShowContextMenu:activityItemsConfiguration:] + 144
11 UIKitMacHelper 0x1d3c349c0 -[UINSMenuController showContextMenu:inWindow:atLocationInWindow:activityItemsConfiguration:] + 312
12 libdispatch.dylib 0x1be44ce60 _dispatch_call_block_and_release + 32
13 libdispatch.dylib 0x1be44ebac _dispatch_client_callout + 20
14 libdispatch.dylib 0x1be45d0ac _dispatch_main_queue_callback_4CF + 944
15 CoreFoundation 0x1be719e60 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 16
我对其他开发人员的几个 iOS 应用程序进行了相同的尝试,当我用鼠标右键单击时,它们都在 macOS 上崩溃了。
有没有人找到解决方法?
可以通过在私有 _UIMenuBarItem
class 上调整 properties
方法来解决这个问题。显然,这附带了通常的免责声明,这可能会让你被 Apple 拒绝(但实际上,这似乎并不经常导致拒绝)。
解决方法如下:基本思路是将原始方法调用包装在 @try/@catch
块中。发生崩溃是因为最初的实现有时会尝试将 title
键的 nil
值插入到 NSDictionary 中。此解决方法捕获该异常,然后 returns 一个虚拟字典以满足调用者的需求。
UIMenuBarItemMontereyCrashFix.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/// Helper class to apply a fix to prevent a crash on macOS Monterey when a user right-clicks in a text field
@interface UIMenuBarItemMontereyCrashFix : NSObject
/// Apply the crash fix. It will only be applied the first time it's called, subsequent calls are no-ops.
/// It will only have an effect when called on macOS Monterey or later.
+ (void)applyCrashFixIfNeeded;
@end
NS_ASSUME_NONNULL_END
UIMenuBarItemMontereyCrashFix.m
#import "UIMenuBarItemMontereyCrashFix.h"
#import <objc/runtime.h>
static BOOL hasCrashFixBeenApplied = NO;
@implementation UIMenuBarItemMontereyCrashFix
/// Apply the crash fix. It will only be applied the first time it's called, subsequent calls are no-ops.
+ (void)applyCrashFixIfNeeded
{
if (@available(macOS 12.0, *)) {} else {
// Bail if we are not running on Monterey
return;
}
if (!hasCrashFixBeenApplied) {
Class UnderscoreUIMenuBarItem = NSClassFromString(@"_UIMenuBarItem");
SEL selector = sel_getUid("properties");
Method method = class_getInstanceMethod(UnderscoreUIMenuBarItem, selector);
IMP original = method_getImplementation(method);
// The crash happens because in some instances the original implementation
// tries to insert `nil` as a value for the key `title` into a dictionary.
// This is how the fix works:
// We wrap the original implementation call in a @try/@catch block. When the
// exception happens, we catch it, and then return a dummy dictionary to
// satisfy the caller. The dummy has `isEnabled` set to NO, and `isHidden` set
// to YES.
IMP override = imp_implementationWithBlock(^id(id me) {
@try {
id res = ((id (*)(id))original)(me);
return res;
}
@catch(NSException *exception) {
return @{
@"allowsAutomaticKeyEquivalentLocalization" : @0,
@"allowsAutomaticKeyEquivalentMirroring" : @0,
@"defaultCommand" : @0,
@"identifier":@"com.apple.menu.application",
@"imageAlwaysVisible" : @0,
@"isAlternate" : @0,
@"isEnabled" : @0,
@"isHidden" : @1,
@"isSeparatorItem" : @0,
@"keyEquivalent" : @"",
@"keyEquivalentModifiers" : @0,
@"remainsVisibleWhenDisabled" : @0,
@"state" : @0,
@"title" : @""
};
}
});
method_setImplementation(method, override);
hasCrashFixBeenApplied = YES;
}
}
@end
记得将 UIMenuBarItemMontereyCrashFix.h
添加到您的桥接 header 中,以便您可以从 Swift 调用它。然后只需在您的应用启动序列中的某处调用 UIMenuBarItemMontereyCrashFix.applyIfNeeded()
(例如在您的 AppDelegate 中)。