如何使用 Swift 中的协议公开 Objective-C object 的私有 class 方法?

How do I expose a private class method of an Objective-C object using protocols in Swift?

考虑 UIColor 上的两个私有方法:

  1. 实例方法styleString其中returns颜色的RGB字符串
  2. class 方法 _systemDestructiveTintColor 其中 returns 破坏性按钮使用的红色。

UIColor.h private header for reference

对于实例方法,我可以创建一个 @objc 协议并使用 unsafeBitCast 公开私有方法:

@objc protocol  UIColorPrivate {
    func styleString() -> UIColor
}

let white = UIColor.whiteColor()
let whitePrivate = unsafeBitCast(white, UIColorPrivate.self)
whitePrivate.styleString() // rgb(255,255,255)

但是,我不确定这对 class 方法有何影响。

第一次尝试:

@objc protocol UIColorPrivate {
    class func _systemDestructiveTintColor() -> String // Error: Class methods are only allowed within classes
}

有道理,我改成static:

@objc protocol UIColorPrivate {
    static func _systemDestructiveTintColor() -> String
}

let colorClass = UIColor.self
let privateClass = unsafeBitCast(colorClass, UIColorPrivate.self) // EXC_BAD_ACCESS

这会导致崩溃。好吧,这不会很快。我可以使用桥接 header 并将 class 方法公开为 @interface,但是有没有办法以纯 Swift 公开这些私有 class 方法?

我可以用 performSelector 做到这一点,但我宁愿将该方法公开为接口或协议:

if UIColor.respondsToSelector("_systemDestructiveTintColor") {
    if let red = UIColor.performSelector("_systemDestructiveTintColor").takeUnretainedValue() as? UIColor {
        // use the color
    }
}

but is there a way to expose these private class methods

这就像说你想要一顿由牛排组成的素食餐。你可以吃牛排,但那不是纯素餐。 "private"和"expose"这两个词是相反的。

使用Objective-C的动态消息解决了这个问题。您可以通过 performSelector 及其方法系列来使用它。您已经知道这一点,所以很难看出还需要什么。

如果你更喜欢使用 #selector 语法,你可以创建一个虚拟的 class 协议,其中包含一个包含你的目标函数的 static 方法,给你自己一个方法来引用方法。但是,您的整个 unsafeBitCast 路线无处可去。

EDIT 您可以通过转换为 AnyObject 将任何已知消息发送到任何 Objective-C 对象,并且您可以使用我的虚拟 class 协议来制作已知消息:

@objc protocol Dummy {
    func hiddenMethod()
}
(someObject as AnyObject).hiddenMethod()

但我不明白为什么这比协议和 #selector 语法更好。

unsafeBitCast() 是访问私有 API.

的糟糕方式

is there a way to expose these private class methods in pure Swift?

问题有一个答案 - 使用带有计算 属性 的扩展。您仍然必须使用执行选择器,但您可以获得类型安全。

extension UIColor {
    static var systemDestructiveTintColor: UIColor {
        let privateColor = Selector("_systemDestructiveTintColor")
        if UIColor.respondsToSelector(privateColor),
            let red = UIColor.performSelector(privateColor).takeUnretainedValue() as? UIColor {
                return red
            }
        return UIColor.redColor()
    }
}

用法

let color = UIColor.systemDestructiveTintColor
print(color)

更新 - 打破 unsafebit 转换

解决评论中的问题:

then why would they expose the unsafeBitCast API in the first place?

unsafeBitCast() 的文档说明如下:

  • Warning: Breaks the guarantees of Swift's type system; use with extreme care. There's almost always a better way to do anything.

unsafeBitCastswift/stdlib/public/core/Builtin.swift中定义为

@_transparent
public func unsafeBitCast<T, U>(_ x: T, to: U.Type) -> U {
  _precondition(sizeof(T.self) == sizeof(U.self),
    "can't unsafeBitCast between types of different sizes")
  return Builtin.reinterpretCast(x)
}

Builtin.reinterpretCastswift/include/swift/AST/Builtins.def中定义为

/// reinterpretCast has type T -> U.
BUILTIN_SIL_OPERATION(ReinterpretCast, "reinterpretCast", Special)

ReinterpretCast 是 C++ 标识符,"reinterpretCast" 是 Swift 中的字符串名称,Special 是函数的一个属性,表示以下 (source):

The builtin has custom proccessing

那么自定义处理在哪里?在 swift/lib/AST/Builtins.cpp

case BuiltinValueKind::ReinterpretCast:
  if (!Types.empty()) return nullptr;
  return getReinterpretCastOperation(Context, Id);
...
...
static ValueDecl *getReinterpretCastOperation(ASTContext &ctx,
                                              Identifier name) {
  // <T, U> T -> U
  // SILGen and IRGen check additional constraints during lowering.
  GenericSignatureBuilder builder(ctx, 2);
  builder.addParameter(makeGenericParam(0));
  builder.setResult(makeGenericParam(1));
  return builder.build(name);
}

总结

不安全位转换的目的是盲目地将 1 对象的类型更改为另一种对象。协议实例方法恰好可以工作,因为如果你像对待 UIColor.styleString() -> UIColor 一样对待 @objc protocol UIColorPrivate { func styleString() -> UIColor } 的位,则会调用正确的方法。

它不适用于 class 方法一点也不奇怪;事实上,我会说它适用于实例方法真是太神奇了。

通过协议实现所需功能的一种方法是为静态方法使用单独的协议。 Objective-C 中的静态方法实际上是 class 的元 class 上的实例方法,因此您可以安全地采用如下方法:

@objc protocol UIColorPrivateStatic {
    func _systemDestructiveTintColor() -> UIColor
}

let privateClass = UIColor.self as! UIColorPrivateStatic
privateClass._systemDestructiveTintColor() // UIDeviceRGBColorSpace 1 0.231373 0.188235 1

这会让你同时暴露私有方法和协议的使用,并且你可以摆脱丑陋的东西unsafeBitCast(并不是说强制转换会更漂亮)。

请注意,如果您使用的是私有 API,那么如果 Apple 决定更改 class 的某些内部结构,您的代码随时可能会中断。