首页 > 解决方案 > Bundle.classNamed(_ className: String) 失败但 NSClassFromString() 工作

问题描述

我正在研究从 Swift 中的主机应用程序加载插件的概念证明。我能够在 Bundle 类上加载框架或包,NSClassFromString并且Bundle.principalClass似乎工作正常,但我无法从Bundle.classNamed函数中获得任何结果。

这是我正在使用的一段代码:

for fullName in bundles {
    print("Loading framework: \(fullName)")

    if let bundle = Bundle(path: fullName), bundle.load(),
        let name = fullName.split(separator: ".").first {

        let typeNamed = bundle.classNamed(name + ".Plugin") as? NSObject.Type
        let typeNS = NSClassFromString(name + ".Plugin") as? NSObject.Type
        let typeNamedV2 = bundle.classNamed(name + ".PluginV2") as? NSObject.Type
        let typeNSV2 = NSClassFromString(name + ".PluginV2") as? NSObject.Type
        let typePrincipal = bundle.principalClass as? NSObject.Type

        print("From bundle.classNamed: \(initPlugin(from: typeNamed)?.description ?? "")" )
        print("From NSClassFromString: \(initPlugin(from: typeNS)?.description ?? "")" )

        print("From bundle.classNamed: \(initPlugin(from: typeNamedV2)?.description ?? "")" )
        print("From NSClassFromString: \(initPlugin(from: typeNSV2)?.description ?? "")" )

        print("From bundle.principalClass: \(initPlugin(from: typePrincipal)?.description ?? "")" )

        bundle.unload()
    }
}

以下是输出:

Loading framework: One.framework
From bundle.classNamed: 
From NSClassFromString: <One.Plugin: 0x102802060>
From bundle.classNamed: 
From NSClassFromString: <One.PluginV2: 0x102802060>
From bundle.principalClass: <One.Plugin: 0x102801650>
Loading framework: CommonInterface.framework
From bundle.classNamed: 
From NSClassFromString: 
From bundle.classNamed: 
From NSClassFromString: 
From bundle.principalClass: 
Loading framework: Bundle.bundle
From bundle.classNamed: 
From NSClassFromString: <Bundle.Plugin: 0x102b08040>
From bundle.classNamed: 
From NSClassFromString: <Bundle.PluginV2: 0x102b08f80>
From bundle.principalClass: <Bundle.Plugin: 0x102801ea0>

项目配置为 Swift 5 和 Xcode 11 构建。

在这里您可以找到 POC 的完整源代码:

https://github.com/lechuckcaptain/SwiftPluginArchitectureExample

欢迎任何提示/反馈!

标签: swiftpluginsreflectionframeworksbundle

解决方案


如果捆绑包已通过相对路径加载,这似乎bundle.classNamed() 不起作用。这是一个演示问题的最小独立示例:Bundle(path:)

// This works:

let b1 = Bundle(path: "/System/Library/Frameworks/Foundation.framework")!
print(b1.load()) // true
let c1: AnyClass? = b1.classNamed("NSString")
print(c1 as Any) // Optional(NSString)

// This does not work

FileManager.default.changeCurrentDirectoryPath("/System/Library/Frameworks")
let b2 = Bundle(path: "Foundation.framework")!
print(b2.load()) // true
let c2: AnyClass? = b2.classNamed("NSString")
print(c2 as Any) // nil

这对我来说就像一个错误。在您的情况下,快速解决方法是通过绝对路径加载包:

if let bundle = Bundle(path: "\(path)/\(fullName)")

或者使用URLs,这里是必要的修改:

// main.swift:

let pluginHost = PluginHost()
pluginHost.loadPlugins(at: Bundle.main.bundleURL)

// PluginHost.swift:

func loadPlugins(at url: URL) {
    let fileManager = FileManager.default

    let bundles = (try? fileManager.contentsOfDirectory(at: url, includingPropertiesForKeys: nil)
        .filter { $0.pathExtension == "bundle"
            || $0.pathExtension == "framework"
        }) ?? []

    for bundleURL in bundles {
        print("Loading framework: \(bundleURL)")
        if let bundle = Bundle(url: bundleURL) {
            let bundleName = bundleURL.deletingPathExtension().lastPathComponent
            print("Name:", bundleName)

            let typeNamed = bundle.classNamed(bundleName + ".Plugin") as? NSObject.Type
            let typeNamedV2 = bundle.classNamed(bundleName + ".PluginV2") as? NSObject.Type

            print("From bundle.classNamed: \(initPlugin(from: typeNamed)?.description ?? "")" )
            print("From bundle.classNamed V2: \(initPlugin(from: typeNamedV2)?.description ?? "")" )

        }
    }
}

输出:

Start plugin loading
Loading framework: file:///.../One.framework/
Name: One
From bundle.classNamed: <One.Plugin: 0x100537f90>
From bundle.classNamed V2: <One.PluginV2: 0x1005388a0>
Loading framework: file:///.../CommonInterface.framework/
Name: CommonInterface
From bundle.classNamed: 
From bundle.classNamed V2: 
Loading framework: file:///.../Bundle.bundle/
Name: Bundle
From bundle.classNamed: <Bundle.Plugin: 0x10053a1d0>
From bundle.classNamed V2: <Bundle.PluginV2: 0x10053a1d0>
End plugin loading
Program ended with exit code: 0

评论:

NSObject如果您需要一个init()方法(并在所有插件中protocol PluginInterface实现一个)并像这样加载插件,则插件类不必继承required init()

if let cls = bundle.classNamed(bundleName + ".Plugin") as? PluginInterface.Type {
    let plugin = cls.init()
    plugin.doSomething()
}
if let cls = bundle.classNamed(bundleName + ".PluginV2") as? PluginInterface.Type {
    let plugin = cls.init()
    plugin.doSomething()
}

推荐阅读