How do I expose a private class method of an Objec

2019-06-21 20:06发布

问题:

Consider two private methods on UIColor:

  1. The instance method styleString which returns the RGB string of the color
  2. The class method _systemDestructiveTintColor which returns the red color used by destructive buttons.

UIColor.h private header for reference

For instance methods, I can create an @objc protocol and use unsafeBitCast to expose the private method:

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

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

However, I'm not sure how this would work for class methods.

First attempt:

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

Makes sense, I'll change it to static:

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

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

This causes a crash. Well this is going nowhere fast. I could use a bridging header and just expose the class methods as an @interface, but is there a way to expose these private class methods in pure Swift?

I could do this with performSelector, but I'd rather expose the method as an interface or protocol:

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

回答1:

One way to achieve what you want via protocols is to use a separate protocol for the static method. Static methods in Objective-C are actually instance methods on the metaclass of the class, so you can safely take an approach like below:

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

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

This will give you both exposure of the private method and usage of protocols, and you get rid of the ugly unsafeBitCast (not that a forced cast would be more beautiful).

Just note that as always if you are working with private API's your code can break at any time if Apple decides to change some of the internals of the class.



回答2:

unsafeBitCast() is a terrible way to access private API.

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

There is an answer to that question - use an extension with a computed property. You still have to use perform selector but you get the type safety.

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()
    }
}

Usage

let color = UIColor.systemDestructiveTintColor
print(color)

Update - breaking down unsafebit cast

To address the question in the comments:

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

The documentation for unsafeBitCast() says the following:

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

unsafeBitCast is defined in swift/stdlib/public/core/Builtin.swift as

@_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.reinterpretCast is defined in swift/include/swift/AST/Builtins.def as

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

ReinterpretCast is the C++ identifier, "reinterpretCast" is the string name in Swift and Special is an attribute of the function which means the following (source):

The builtin has custom proccessing

So where is that custom processing? In 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);
}

Summary

The purpose of unsafe bit cast is to blindly change a type of 1 object to another. The protocol instance method just happens to work because if you treat the bits of @objc protocol UIColorPrivate { func styleString() -> UIColor } like it was UIColor.styleString() -> UIColor the correct method is called.

It's not at all strange that it doesn't work with class methods; in fact i'd say it's miraculous that it works for instance methods.



回答3:

but is there a way to expose these private class methods

That's like saying you want a vegan meal consisting of steak. You can have the steak, but that would not be a vegan meal. The words "private" and "expose" are opposites.

Using Objective-C's dynamic messaging solves the problem. You can use it by way of performSelector and its family of methods. You already know this, so it's hard to see what more can be wanted.

If you prefer to use #selector syntax, you can create a dummy class protocol with a static method containing your target function, to give yourself a way to refer to the method. Your whole unsafeBitCast route, however, is going nowhere.

EDIT You can send any known message to any Objective-C object by casting to AnyObject, and you can use my dummy class protocol to make the message known:

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

But I don't see why this is better than the protocol and #selector syntax.