在 Swift 中访问 object-c NSDictionary 值很慢
Posted
技术标签:
【中文标题】在 Swift 中访问 object-c NSDictionary 值很慢【英文标题】:Accessing object-c NSDictionary values in Swift is slow 【发布时间】:2019-08-19 11:02:42 【问题描述】:我遇到了 swift 和 Objective-c 之间的性能问题。我试图完全了解幕后发生的事情,以便将来避免它。
我有一个 objc 类型 Car
@interface Car
@property (nonatomic,readonly) NSDictionary<NSString *, Part *> *parts;
@end
然后我迅速列举了大量的Car
对象并从每辆汽车parts
字典中查找Part
。
for(var car in allCarsInTheWorld)
let part = car.parts["partidstring"] //This is super slow.
在我的工作应用程序中,上面的整个循环大约需要 5-10 秒。我可以通过修改上面的代码来解决这个问题,如下所示,这会导致相同的循环在毫秒内运行:
固定 obj-c 文件
@interface Car
@property (nonatomic,readonly) NSDictionary<NSString *, Part *> *parts;
// Look up the part from the parts dictionary above
// in obj-c implementation and return it
-(Part *)partFor:(NSString *)partIdString;
@end
固定的 Swift 文件
for(var car in allCarsInTheWorld)
let part = car.partFor("partidstring") //This is fast, performance issue gone.
性能下降的原因是什么?我唯一能想到的是,当我从 swift 访问 obj-c 字典时,它正在被复制。
编辑:添加了分析堆栈的图片。这些是我在字典中调用的框架。似乎它与字符串而不是字典本身有关。
这似乎是我能找到的最接近问题的匹配:https://forums.swift.org/t/string-performance-how-to-ensure-fast-path-for-comparison-hashing/27436
【问题讨论】:
您是否尝试过使用 Instruments (Time Profiler) 分析代码? – 这里也有类似的观察:***.com/q/46833459. 【参考方案1】:部分问题在于将NSDictionary<NSString *, Part *>
桥接到[String: Part]
涉及对字典的所有键和值进行运行时检查。这是必需的,因为 NSDictionary
的 Objective-C 泛型参数不保证字典不会包含不兼容的键/值(例如,Objective-C 代码可以将非字符串键或非 Part 值添加到字典)。对于大量的字典,这可能会变得很耗时。
另一方面是 Swift 也可能会创建一个相应的字典以使其不可变,因为 Objective-C 的字典也可能是一个 `NSMutableDictionary'。这涉及额外的分配和解除分配。
您添加 partFor()
函数的方法通过将字典隐藏在 Swift 世界中来避免上述两种情况。而且它在架构上也更好,因为您隐藏了汽车零件存储的实现细节(假设您还将字典设为私有)。
【讨论】:
感谢您的解释。如此简单,完全有道理。我学到了一些新东西:)【参考方案2】:问题似乎是在您的第一个版本中,在循环的每次迭代中,整个字典都被转换为 swift 字典。
代码的行为如下:
for(var car in allCarsInTheWorld)
let car_parts = car.parts as [String: Part] // This is super slow.
let part = car_parts["partidstring"]
这是 Swift 桥接在这里工作方式的一种错误特征。如果 Swift 编译器只调用 Objective C 方法 -objectForKeyedSubscript:
会快得多。
在此之前,如果您关心性能,那么实现像 -partFor:
这样的自定义 objc 方法是一个很好的解决方案。
【讨论】:
【参考方案3】:假设你展示了一个 ObjC 类:
@interface MCADictionaryHolder : NSObject
@property (nonatomic) NSDictionary<NSString *, id> * _Nonnull objects;
- (void)randomise:(NSInteger)upperBound;
- (id _Nullable)itemForKey:(NSString * _Nonnull)key;
@end
@implementation MCADictionaryHolder
- (instancetype)init
self = [super init];
if (self)
self.objects = [[NSDictionary alloc] init];
return self;
-(void)randomise:(NSInteger)upperBound
NSMutableDictionary * d = [[NSMutableDictionary alloc] initWithCapacity:upperBound];
for (NSInteger i = 0; i < upperBound; i++)
NSString *inStr = [@(i) stringValue];
[d setObject:inStr forKey:inStr];
self.objects = [[NSDictionary alloc] initWithDictionary:d];
-(id)itemForKey:(NSString *)key
id value = [self.objects objectForKey:key];
return value;
@end
假设您启动了类似于如下所示的性能测试:
class NSDictionaryToDictionaryBridgingTests: LogicTestCase
func test_default()
let bound = 2000
let keys = (0 ..< bound).map "\($0)"
var mutableDict: [String: Any] = [:]
for key in keys
mutableDict[key] = key
let dict = mutableDict
let holder = MCADictionaryHolder()
holder.randomise(bound)
benchmark("Access NSDictionary via Swift API")
for key in keys
let value = holder.objects[key]
_ = value
benchmark("Access NSDictionary via NSDictionary API")
let nsDict = holder.objects as NSDictionary
for key in keys
let value = nsDict.object(forKey: key)
_ = value
benchmark("Access NSDictionary via dedicated method")
for key in keys
let value = holder.item(forKey: key)
_ = value
benchmark("Access to Swift Dictionary via Swift API")
for key in keys
let value = dict[key]
_ = value
然后性能测试将显示类似于下图的结果:
Access NSDictionary via Swift API:
.......... 1103.655ms ± 9.358ms (mean ± SD)
Access NSDictionary via NSDictionary API:
.......... 0.263ms ± 0.001ms (mean ± SD)
Access NSDictionary via dedicated method:
.......... 0.335ms ± 0.002ms (mean ± SD)
Access to Swift Dictionary via Swift API:
.......... 0.174ms ± 0.001ms (mean ± SD)
从结果可以看出:
通过 Swift API 访问 Swift Dictionary 可以获得最快的结果。 在 Swift 端通过 NSDictionary API 访问 NSDictionary 有点慢,但可以接受。因此,无需创建专用方法来对NSDictionary
执行操作,只需将[AnyHashable: Any]
转换为NSDictionary
并执行所需的操作即可。
更新
在某些情况下,通过便捷的方法或属性访问 ObjC 是值得的,以最小化 ObjC Swift 之间的跨界成本。
假设你有一个 ObjC 扩展,如下所示:
@interface NSAppearance (MCA)
-(BOOL)mca_isDark;
@end
-(BOOL)mca_isDark
if ([self.name isEqualToString:NSAppearanceNameDarkAqua])
return true;
if ([self.name isEqualToString:NSAppearanceNameVibrantDark])
return true;
if ([self.name isEqualToString:NSAppearanceNameAccessibilityHighContrastDarkAqua])
return true;
if ([self.name isEqualToString:NSAppearanceNameAccessibilityHighContrastVibrantDark])
return true;
return false;
@end
假设您启动了类似于如下所示的性能测试:
class NSStringComparisonTests: LogicTestCase
func isDarkUsingSwiftAPI(_ a: NSAppearance) -> Bool
switch a.name
case .darkAqua, .vibrantDark, .accessibilityHighContrastDarkAqua, .accessibilityHighContrastVibrantDark:
return true
default:
return false
func isDarkUsingObjCAPI(_ a: NSAppearance) -> Bool
let nsName = a.name.rawValue as NSString
if nsName.isEqual(to: NSAppearance.Name.darkAqua)
return true
if nsName.isEqual(to: NSAppearance.Name.vibrantDark)
return true
if nsName.isEqual(to: NSAppearance.Name.accessibilityHighContrastDarkAqua)
return true
if nsName.isEqual(to: NSAppearance.Name.accessibilityHighContrastVibrantDark)
return true
return false
func test_default()
let appearance = NSAppearance.current!
let numIterations = 1000000
benchmark("Compare using Swift API", numberOfIterations: numIterations)
let value = isDarkUsingSwiftAPI(appearance)
_ = value
benchmark("Compare using ObjC API", numberOfIterations: numIterations)
let value = isDarkUsingObjCAPI(appearance)
_ = value
benchmark("Compare using ObjC convenience property", numberOfIterations: numIterations)
let value = appearance.mca_isDark()
_ = value
然后性能测试将显示类似于下图的结果:
Compare using Swift API:
.......... 813.347ms ± 7.560ms (mean ± SD)
Compare using ObjC API:
.......... 534.337ms ± 1.065ms (mean ± SD)
Compare using ObjC convenience property:
.......... 142.729ms ± 0.197ms (mean ± SD)
从结果可以看出,通过便捷方式从 ObjC 世界中获取信息是最快的解决方案。
【讨论】:
以上是关于在 Swift 中访问 object-c NSDictionary 值很慢的主要内容,如果未能解决你的问题,请参考以下文章
Object-C 中的 Swift 代码:“ClassName”没有可见的@interface 声明选择器“alloc”