【问题标题】:Swift 3.1 deprecates initialize(). How can I achieve the same thing?Swift 3.1 弃用了 initialize()。我怎样才能达到同样的目的?
【发布时间】:2017-08-07 01:32:47
【问题描述】:

Objective-C 声明了一个类函数initialize(),它在使用之前为每个类运行一次。它通常用作交换方法实现(swizzling)等的入口点。

Swift 3.1 弃用此函数并发出警告:

方法'initialize()'定义了Objective-C类方法'initialize', 不能保证被 Swift 调用并且将被禁止 在未来的版本中

如何解决这个问题,同时仍保持我目前使用 initialize() 入口点实现的相同行为和功能?

【问题讨论】:

  • 更准确地说,方法 +initialize 在使用类中的任何内容之前执行。实际上它是执行的,当包含该类的包被加载时,应用启动后会发生什么。
  • @AminNegm-Awad:我不确定这是否正确。也许您正在考虑 load 方法(在 Swift 中不可用)?
  • +initialize 也一样。 运行时向程序中的每个类发送初始化,就在类或任何继承自它的类从程序中发送其第一条消息之前。 不能保证这是在程序中完成的开始。这样的保证会导致立即加载所有捆绑包以获取+initialize 中的代码。 – 与+load 的区别在于,它在类 和类别 上执行。 (在类和类别中具有相同选择器的方法!)但是,当然,对于类别而言,可能的延迟调用这一事实比类更重要。
  • @AminNegm-Awad:也许我误解了你的第一条评论。你说实际上,initialize 在 bundle ... 加载时被调用。这不是我所经历的。
  • 是的,正如我几小时前所说的。但这有时很重要,我。 e.在我提到的情况下。但是,不能保证在应用启动时将其发送到类对象(在 Objective-C 中,没有像 initialize() 这样的成员函数,它们不会被调用,但会发送消息)。

标签: ios objective-c swift


【解决方案1】:

简单/简单的解决方案

常见的应用程序入口点是应用程序委托的applicationDidFinishLaunching。我们可以简单地为每个要在初始化时通知的类添加一个静态函数,然后从这里调用它。

第一个解决方案简单易懂。在大多数情况下,这是我推荐的。尽管下一个解决方案提供的结果与原始 initialize() 函数更相似,但它也会导致应用程序启动时间稍长。我不再想 在大多数情况下,付出努力、性能下降或代码复杂性都是值得的。简单的代码就是好代码。

继续阅读以了解另一种选择。您可能有理由需要它(或者可能是其中的一部分)。


不是那么简单的解决方案

第一个解决方案不一定能很好地扩展。如果您正在构建一个框架,您希望您的代码在不需要任何人从应用程序委托中调用它的情况下运行?

第一步

定义以下 Swift 代码。目的是为您希望灌输类似于initialize() 的行为的任何类提供一个简单的入口点——现在只需遵循SelfAware 即可完成。它还提供了一个函数来为每个符合要求的类运行此行为。

protocol SelfAware: class {
    static func awake()
}

class NothingToSeeHere {

    static func harmlessFunction() {

        let typeCount = Int(objc_getClassList(nil, 0))
        let types = UnsafeMutablePointer<AnyClass>.allocate(capacity: typeCount)
        let autoreleasingTypes = AutoreleasingUnsafeMutablePointer<AnyClass>(types)
        objc_getClassList(autoreleasingTypes, Int32(typeCount))
        for index in 0 ..< typeCount { (types[index] as? SelfAware.Type)?.awake() }
        types.deallocate(capacity: typeCount)

    }

}

第二步

这一切都很好,但是我们仍然需要一种方法来在应用程序启动时实际运行我们定义的函数,即NothingToSeeHere.harmlessFunction()。以前,此答案建议使用 Objective-C 代码来执行此操作。然而,似乎我们可以只使用 Swift 来做我们需要的事情。对于 macOS 或其他 UIApplication 不可用的平台,将需要以下变体。

extension UIApplication {

    private static let runOnce: Void = {
        NothingToSeeHere.harmlessFunction()
    }()

    override open var next: UIResponder? {
        // Called before applicationDidFinishLaunching
        UIApplication.runOnce
        return super.next
    }

}

第三步

我们现在在应用程序启动时有了一个入口点,并且有一种方法可以从您选择的类中挂钩。剩下要做的就是:与其实现initialize(),不如遵循SelfAware 并实现定义的方法awake()

【讨论】:

  • Swift中的优雅解决方案是使用Objective-C的机制?
  • 嗯,+initialize 的本质是它是自包含的。这就是它的用途。任何其他方法都不是解决方案,而是一个不雅的解决方案。而且我不认为它是一个端口。如果它是来自 Objective-C 的一个端口,那么它可以在没有 Objective-C 的情况下工作。但事实并非如此。不要误会我的意思,我赞成你的回答。然而,这不是一个优雅的解决方案,而是一个 hack,因为在 Swift 中没有办法做到这一点。
  • @AminNegm-Awad 我建议优雅来自外部用户的不变行为。对于依赖于initialize() 魔法的框架或类似的东西,这允许行为保持不变。您对我之前的评论是正确的,这样做不符合软件“端口”的定义。也许“桥”是一个更好的词。
  • 我认为重新使用 Objective-C 的想法非常令人遗憾。如果我想这样做,我就不会完成将我的应用程序转换为 Swift 的所有工作。
  • @AminNegm-Awad 你漏掉了 Cocoa Bindings。我刚刚将一个大量使用绑定的桌面应用程序从 Objective-C 转换为 Swift,差点把自己逼疯了。 :)
【解决方案2】:

您还可以使用自 those are already lazy 以来的静态变量,并在顶级对象的初始化程序中引用它们。这对于没有应用程序委托的应用程序扩展等很有用。

class Foo {
    static let classInit : () = {
        // do your global initialization here
    }()

    init() {
        // just reference it so that the variable is initialized
        Foo.classInit
    }
}

【讨论】:

  • 如果您必须在顶级对象的初始化程序中引用它们。,为什么不直接调用classInit()?有什么优势吗?这根本不是独立的解决方案。
  • 无论如何classInit 应该是类内部的。
  • @AminNegm-Awad 直接调用classInit() 每次创建类Foo 的实例时都会执行代码。这不是我们想要的。
  • 实际上对那个类私有应该更好。
  • @EricChai 对classInit() 的显式调用将在调用时执行。
【解决方案3】:

如果您更喜欢 Pure Swift™! 那么我对这种事情的解决方案是在 _UIApplicationMainPreparations 开始运行:

@UIApplicationMain
private final class OurAppDelegate: FunctionalApplicationDelegate {
    // OurAppDelegate() constructs these at _UIApplicationMainPreparations time
    private let allHandlers: [ApplicationDelegateHandler] = [
        WindowHandler(),
        FeedbackHandler(),
        ...

这里的模式是我通过将UIApplicationDelegate 分解为各个处理程序可以采用的各种协议来避免大规模应用程序委托问题,以防您想知道。但重要的一点是,尽早开始工作的纯 Swift 方法是在 @UIApplicationMain 类的初始化中调度 +initialize 类型的任务,就像这里的 allHandlers 的构造一样。 _UIApplicationMainPreparations 时间应该足够早了!

【讨论】:

  • 有什么方法可以为框架做到这一点? (没有应用委托)。
  • 据我所知,下面提到的静态类成员是您在框架中可以做的最好的事情
【解决方案4】:

对@JordanSmith 的优秀课程的一个小补充,确保每个awake() 只被调用一次:

protocol SelfAware: class {
    static func awake()
}

@objc class NothingToSeeHere: NSObject {

    private static let doOnce: Any? = {
        _harmlessFunction()
    }()

    static func harmlessFunction() {
        _ = NothingToSeeHere.doOnce
    }

    private static func _harmlessFunction() {
        let typeCount = Int(objc_getClassList(nil, 0))
        let types = UnsafeMutablePointer<AnyClass>.allocate(capacity: typeCount)
        let autoreleasingTypes = AutoreleasingUnsafeMutablePointer<AnyClass>(types)
        objc_getClassList(autoreleasingTypes, Int32(typeCount))
        for index in 0 ..< typeCount { (types[index] as? SelfAware.Type)?.awake() }
        types.deallocate(capacity: typeCount)
    }
}

【讨论】:

    【解决方案5】:

    我的方法与 adib 的方法基本相同。这是一个使用 Core Data 的桌面应用程序的示例;这里的目标是在任何代码提到它之前注册我们的自定义转换器:

    @NSApplicationMain
    class AppDelegate: NSObject, NSApplicationDelegate {
    
        override init() {
            super.init()
            AppDelegate.doInitialize
        }
    
        static let doInitialize : Void = {
            // set up transformer
            ValueTransformer.setValueTransformer(DateToDayOfWeekTransformer(), forName: .DateToDayOfWeekTransformer)
        }()
    
        // ...
    }
    

    好消息是它适用于任何类,就像initialize 所做的那样,只要你覆盖了所有的基础——也就是说,你必须实现每个初始化器。这是一个文本视图的示例,它在任何实例有机会出现在屏幕上之前配置自己的外观代理一次;这个例子是人为的,但封装非常好:

    class CustomTextView : UITextView {
    
        override init(frame: CGRect, textContainer: NSTextContainer?) {
            super.init(frame:frame, textContainer: textContainer)
            CustomTextView.doInitialize
        }
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder:aDecoder)
            CustomTextView.doInitialize
        }
    
        static let doInitialize : Void = {
            CustomTextView.appearance().backgroundColor = .green
        }()
    
    }
    

    这比应用程序委托更好地证明了这种方法的优势。只有一个应用程序委托实例,所以问题不是很有趣;但可以有很多 CustomTextView 实例。然而,CustomTextView.appearance().backgroundColor = .green 行将在 第一个 实例创建时被执行一次,因为它是静态属性初始化程序的一部分。这与类方法initialize 的行为非常相似。

    【讨论】:

    • 我想知道是否允许优化编译器初始化静态属性,因为 CustomTextView.doInitialize 评估为未使用的表达式 - 只是一个想法,我不知道这是否是不是一个问题。
    • @MartinR 好吧,如果是这样的话,标准的一次性替换模式会不会有同样的担忧?
    • dispatch_once 只执行一次块。这里的“一次”部分是静态属性初始化的副作用。 – Swift 迁移器将 dispatch_once 转换为 _ = MyClass.__once(例如这里:stackoverflow.com/q/39576848/1187415)。是否需要分配给_
    • @MartinR 我只能告诉你,我的价值转换器初始化实际上从未失败过。这来自我每天启动的一个真实应用程序。
    • 好的,谢谢。这只是一个想法,我可能完全走错了路。
    【解决方案6】:

    我认为这是一种解决方法。

    我们也可以在objective-c代码中编写initialize()函数,然后通过桥引用使用它

    希望最好的方式.....

    【讨论】:

      【解决方案7】:

      如果您想以 Pure Swift 方式修复 Method Swizzling:

      public protocol SwizzlingInjection: class {
          static func inject()
      }
      
      class SwizzlingHelper {
      
          private static let doOnce: Any? = {
              UILabel.inject()
              return nil
          }()
      
          static func enableInjection() {
              _ = SwizzlingHelper.doOnce
          }
      }
      
      extension UIApplication {
      
          override open var next: UIResponder? {
              // Called before applicationDidFinishLaunching
              SwizzlingHelper.enableInjection()
              return super.next
          }
      
      }
      
      extension UILabel: SwizzlingInjection
      {
          public static func inject() {
              // make sure this isn't a subclass
              guard self === UILabel.self else { return }
      
              // Do your own method_exchangeImplementations(originalMethod, swizzledMethod) here
      
          }
      }
      

      由于objc_getClassList 是Objective-C,它无法获取超类(例如UILabel),只能获取所有子类,但对于与UIKit 相关的swizzling,我们只想在超类上运行一次。只需在每个目标类上运行 inject(),而不是循环整个项目类。

      【讨论】:

      • 对我来说同样的问题:objc_getClassList 告诉我 UILabel 不符合我的 SwizzlingInjection 协议。只有(内部)UILabel 子类似乎符合此协议......太糟糕了......无法在运行时类上循环以对符合的类执行自动 swizzling 注入,需要为每个需要的类手动注入 swizzling。这就是为什么,对我来说,这个答案应该是被接受的^^
      【解决方案8】:
      1. 将您的班级标记为@objc
      2. NSObject继承
      3. 将 ObjC 类别添加到您的课程中
      4. 在类别中实现initialize

      例子

      Swift 文件:

      //MyClass.swift
      @objc class MyClass : NSObject
      {
      }
      

      Objc 文件:

      //MyClass+ObjC.h
      #import "MyClass-Swift.h"
      
      @interface MyClass (ObjC)
      
      @end
      
      //MyClass+ObjC.m
      #import "MyClass+ObjC.h"
      
      @implement MyClass (ObjC)
      
      + (void)initialize {
          [super initialize];
      }
      
      @end
      

      【讨论】:

        【解决方案9】:

        这是一个适用于 swift 3.1+

        的解决方案
        @objc func newViewWillAppear(_ animated: Bool) {
            self.newViewWillAppear(animated) //Incase we need to override this method
            let viewControllerName = String(describing: type(of: self)).replacingOccurrences(of: "ViewController", with: "", options: .literal, range: nil)
            print("VIEW APPEAR", viewControllerName)
        }
        
        static func swizzleViewWillAppear() {
            //Make sure This isn't a subclass of UIViewController, So that It applies to all UIViewController childs
            if self != UIViewController.self {
                return
            }
            let _: () = {
                let originalSelector = #selector(UIViewController.viewWillAppear(_:))
                let swizzledSelector = #selector(UIViewController.newViewWillAppear(_:))
                let originalMethod = class_getInstanceMethod(self, originalSelector)
                let swizzledMethod = class_getInstanceMethod(self, swizzledSelector)
                method_exchangeImplementations(originalMethod!, swizzledMethod!);
            }()
        }
        

        然后在 AppDelegate 上:

        UIViewController.swizzleViewWillAppear()
        

        Taking from the following post

        【讨论】:

          【解决方案10】:

          使用静态 closure stored property[About]

          执行某事的另一个示例
          extension MyClass {
          
              static let shared: MyClass = {
                  //closure
          
                  //create an instance and setup it
                  let myClass = MyClass(parameter: "parameter")
                  myClass.initialize()
          
                  return myClass
              }()
              //() to execute the closure.
          
              func initialize() {
                  //is called once
              }
          }
          
          //using
          let myClass = MyClass.shared
          
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2015-03-26
            • 2020-06-19
            • 2017-05-28
            • 1970-01-01
            • 1970-01-01
            • 2020-07-10
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多