AnyClass is NSObjectProtocol… sometimes?

試著忘記壹切 提交于 2020-01-05 04:58:28

问题


Following on this question I got very curious about the described behavior and I did some investigation that left me quite puzzled.

The problem

Checking the is NSObjectProtocol for the return of NSClassFromString returns true in any case except for the return of NSClassFromString("WKNSURLRequest"). The fact that all the results are true is a bit surprising for me also for PureClass and SwiftObject.

import UIKit
import WebKit
import ObjectiveC

class Sigh: NSObject { }
class PureClass { }

let sighClass = NSClassFromString(NSStringFromClass(Sigh.self))!
let pureClass = NSClassFromString(NSStringFromClass(PureClass.self))!
let nsObject = NSClassFromString("NSObject")!
let wkRequestClass = NSClassFromString("WKNSURLRequest")!
let swiftObject = NSClassFromString("SwiftObject")!

print("\n*NSObjectProtocol CONFORMANCE*")
print("NSObject: ", nsObject is NSObjectProtocol)
//print("WkRequestClass: ", wkRequestClass is NSObjectProtocol)
print("WkRequestClass: This would crash")
print("SighClass: ", sighClass is NSObjectProtocol)
print("PureClass: ", pureClass is NSObjectProtocol)
print("SwiftObject: ", swiftObject is NSObjectProtocol)

We are checking not an instance of those classes, but the return of NSClassFromString which is AnyClass?.

AnyClass is a typedef to AnyObject.Type. Why is it NSObjectProtocol? Why not for WkRequestClass?

What mecki said is true and we can check it by reading the webkit source code: WKNSURLRequest inherits from WKObject which is a root class BUT conforming the NSObjectProtocol, because WKObject conforms WKObject(protocol) that extends NSObject(protocol).

@protocol WKObject <NSObject>

@property (readonly) API::Object& _apiObject;

@end

NS_ROOT_CLASS
@interface WKObject <WKObject>

- (NSObject *)_web_createTarget NS_RETURNS_RETAINED;

@end

source: https://github.com/WebKit/webkit/blob/master/Source/WebKit2/Shared/Cocoa/WKObject.h

Mecki best guess to this kind of crash is a runtime error so I tried to explain it somehow. Here is my playground:

//: Playground - noun: a place where people can play

import UIKit
import WebKit
import ObjectiveC

class Sigh: NSObject { }
class PureClass { }

let sighClass: AnyClass = NSClassFromString(NSStringFromClass(Sigh.self))!
let pureClass: AnyClass = NSClassFromString(NSStringFromClass(PureClass.self))!
let nsObject: AnyClass = NSClassFromString("NSObject")!
let wkRequestClass: AnyClass = NSClassFromString("WKNSURLRequest")!
let swiftObject: AnyClass = NSClassFromString("SwiftObject")!

print("\n*NSObjectProtocol CONFORMANCE*")
print("NSObject: ", nsObject is NSObjectProtocol)
//print("WkRequestClass: ", wkRequestClass is NSObjectProtocol)
print("WkRequestClass: This would crash")
print("SighClass: ", sighClass is NSObjectProtocol)
print("PureClass: ", pureClass is NSObjectProtocol)
print("SwiftObject: ", swiftObject is NSObjectProtocol)

print("\n*ANYCLASS PRINT*")
print("NSObject: ", nsObject)
print("WkRequestClass: ", wkRequestClass)
print("SighClass: ", sighClass)
print("PureClass: ", pureClass)
print("SwiftObject: ", swiftObject)

print("\n*TYPE PRINT*")

print("Type of NSObject: ", type(of: nsObject))
print("Type of WkRequestClass: ", type(of: wkRequestClass))
print("Type of SighClass: ", type(of: sighClass))
print("Type of PureClass: ", type(of: pureClass))
print("Type of SwiftObject: ", type(of: swiftObject))

print("\n*.SELF PRINT*")

print("NSObject.self: ", nsObject.self)
print("WkRequestClass.self: ", wkRequestClass.self)
print("SighClass.self: ", sighClass.self)
print("PureClass.self: ", pureClass.self)
print("SwiftObject.self: ", swiftObject.self)

print("\n*SUPERCLASS PRINT*")

print("NSObject superClass: ", nsObject.superclass() ?? "nil")
//print("WkRequestClass superClass: ", wkRequestClass.superclass())
print("WkRequestClass superClass: This would crash")
print("SighClass superClass: ", sighClass.superclass() ?? "nil")
print("PureClass superClass: ", pureClass.superclass() ?? "nil")
print("SwiftObject superClass: ", swiftObject.superclass() ?? "nil")

print("\n*INTROSPECTION*\n")

var count: UInt32 = 0
var protocols = class_copyProtocolList(wkRequestClass, &count);

for i: Int in 0..<Int(count) {
    print("WkRequestClass implements", protocols![i]!)
}

print("WkRequestClass superClass is", class_getSuperclass(wkRequestClass))
print("Its super super class is", class_getSuperclass(class_getSuperclass(wkRequestClass)))

//Introspecting WKObject
protocols = class_copyProtocolList(class_getSuperclass(wkRequestClass), &count);

for i: Int in 0..<Int(count) {
    print("WKObject implements", protocols![i]!)
}

print("WKObject conforms the NSObjectProtocol? ", class_conformsToProtocol(class_getSuperclass(wkRequestClass), NSObjectProtocol.self))

In this easy playground I play a bit with different class types, and at the end I try to introspect WKNSURLRequest and WKObject using objective-c runtime.

If the crash is due to a runtime bug I was expecting a crash in the introspection section as well, but nothing. No problems at all.

This is the output:

**NSObjectProtocol CONFORMANCE**

 - NSObject:  true 
 - WkRequestClass: This would crash 
 - SighClass:  true
 - PureClass:  true 
 - SwiftObject:  true

**ANYCLASS PRINT**

 - NSObject:  NSObject
 - WkRequestClass:  WKNSURLRequest
 - SighClass:  Sigh
 - PureClass:  PureClass
 - SwiftObject:  SwiftObject

**TYPE PRINT**

 - Type of NSObject:  NSObject.Type
 - Type of WkRequestClass:  WKNSURLRequest.Type
 - Type of SighClass:  Sigh.Type
 - Type of PureClass:  PureClass.Type
 - Type of SwiftObject:  SwiftObject.Type

**.SELF PRINT**

 - NSObject.self:  NSObject
 - WkRequestClass.self:  WKNSURLRequest
 - SighClass.self:  Sigh
 - PureClass.self:  PureClass
 - SwiftObject.self:  SwiftObject

**SUPERCLASS PRINT**

 - NSObject superClass:  nil
 - WkRequestClass superClass: This would crash
 - SighClass superClass:  NSObject
 - PureClass superClass:  SwiftObject
 - SwiftObject superClass:  nil

**INTROSPECTION**

 - WkRequestClass implements ``
 - WkRequestClass superClass is WKObject
 - Its super super class is nil
 - WKObject implements ``
 - WKObject conforms the NSObjectProtocol?  true

Funny fact, if I do

wkRequestClass.isSubclass(of: class_getSuperclass(wkRequestClass))

I get a crash, which is absurd.

Does this proves that the objective c runtime is broken/doesn't handle correctly this case? The answer doesn't look easy (that0s why I'm posting this question) because, as expected, WKObject is conforming to NSObjectProtocol, and it is a root class as its superClass is nil. All worked for this kind of introspections.

What remains to check is the swift runtime. Is there any way to check it? Is there anything I missed that would explain this crash? I'm curious to know your opinion about that.


回答1:


You are correct that WKObject implements the NSObject protocol as it implements the WKObject protocol and this protocol inherits from the NSObject protocol. But that plays no role here.

Certain methods like +isSubclassOfClass: or +instancesRespondToSelector: are not declared in the NSObject protocol, these are just normal class methods of the NSObject class and thus inherited by all sub-classes of NSObject but only by sub-classes of NSObject. Other root classes must implement these themselves if they want to be NSObject compatible, the NSObject protocol won't force them to do so.

Now check out this code from a unit test class:

SEL issubclasssel = @selector(isSubclassOfClass:);
Protocol * nsobjp = @protocol(NSObject);
Class c1 = NSClassFromString(@"NSObject");
XCTAssert(c1);
XCTAssert([c1 conformsToProtocol:nsobjp]);
XCTAssert([c1 instancesRespondToSelector:issubclasssel]);
XCTAssert([c1 isSubclassOfClass:[NSObject class]]);

Class c2 = NSClassFromString(@"WKNSURLRequest");
XCTAssert(c2);
XCTAssert([c2 conformsToProtocol:nsobjp]); // Line 1
XCTAssert([c2 instancesRespondToSelector:issubclasssel]); // Line 2
XCTAssert([c2 isSubclassOfClass:[NSObject class]]); // Line 3

This code crashes at Line 2:

Thread 1: EXC_BAD_ACCESS (code=1, address=0x18)

And if I comment out Line 2, the code still crashes at Line 3 with exactly the same error. Note that this is not Swift code, nor in any way Swift related, this is pure Objective-C code. It's just wrong Objective-C code, as you will see below.

So WKObject does implement +conformsToProtocol: (Line 1 does not crash), it has to, as this is a requirement of the NSObject protocol, but it doesn't implement +instancesRespondToSelector: or +isSubclassOfClass:, and it doesn't have to, so this is perfectly okay. It is a root class, it doesn't inherit from NSObject and there is no protocol that would require it to implement any of these. It was my mistake above to call these methods; calling not-existing methods on objects is "undefined behavior" and this allows the runtime pretty much anything: ignoring the call, just logging an error, throwing an exception, or just crashing right away; and as objc_msgSend() is a highly optimized function (it has no security checks, that would too expensive for every call), it just crashes.

But apparently Swift sometimes doesn't seem to care. When dealing with Obj-C objects, Swift seems to assume that it can always call one of theses methods that NSObject and any sub-class of it implement, even though no protocol would promise that. And that's why certain Swift code constructs cause a crash with Objective-C root objects that don't inherit from NSObject. And as this assumption is simply wrong. Swift must never call any methods on root objects where it doesn't know for sure that these methods are also implemented. Thus I called this a bug in the Swift-Objc-Bridge at the other question.

Update 1:

Giuseppe Lanza asked:

Why then when I have a pure swift class, I get the class from string and then I test is NSObjectProtocol I get true?

Personally I think this is also a bug in the Swift Runtime. A pure Swift class does not conform to the NSObjectProtocol. Actually it cannot even conform to it, see answer below.

Giuseppe Lanza asked:

Please note that if I create a protocol that inherits from NSObjectProtocol and then I try to make PureClass conformance to that protocol the compiler will complain that PureClass is not NSObjectProtocol compliant

That's because PureClass would have to implement all required NSObjectProtocol methods to conform to that protocol; see this answer https://stackoverflow.com/a/24650406/15809

However, it cannot even satisfy that requirement, as one requirement of NSObjectProtocol is to implement this method

func `self`() -> Self

and that's simply not possible for a pure Swift class, as when you try to do that, the compiler will complain:

error: method cannot be an implementation of an @objc requirement
because its result type cannot be represented in Objective-C

which is correct, a pure Swift class cannot be represented in Obj-C, so it cannot return the required type.

The Swift documentation also says:

Note that @objc protocols can be adopted only by classes that inherit from Objective-C classes or other @objc classes.

And currently @objc forces you to inherit from NSObject, which a pure Swift class does not.



来源:https://stackoverflow.com/questions/43280615/anyclass-is-nsobjectprotocol-sometimes

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!