SwiftのクラスをObjective-CのClass型に渡してinitしたときに落ちるパターン

(2020-06-28)

Objective-Cのクラスは基本的にNSObjectをルートクラスに持ち、そのinit()が継承またはオーバーライドされるが、 SwiftのクラスはNSObjectを継承していなかったり他のdesignated initializerが存在することでinit()が存在しないことや、default initializerのために明示的なinit()が存在しない場合がある。

Swiftのdesignated/convenience/required/default initializerと継承 - sambaiz-net

そんなクラスのMetatypeをObjective-CのClass型に代入してinitするとどうなるか確認する。

SwiftのMetatypeとMetadata - sambaiz-net

#import <Foundation/Foundation.h>

@interface Hoge: NSObject

@property (weak, nonatomic) id <HogeDelegate> delegate;
@property Class klass;
- (void)fuga;
@end
#import "Hoge.h"
#import <UIKit/UIKit.h>

@implementation Hoge

- (void)fuga {
    dispatch_async(dispatch_get_main_queue(), ^{
        [[_klass alloc] init];
    });
}

@end
func f() {
    var hoge = Hoge()
    hoge.klass = B.self
    hoge.fuga()
}
  • init()を実装したクラス: 実行時にUnrecognized selector -[***.B init]で落ちる

init()を実装しないでdefault initializerが存在する場合も同様。

class B{
    init() {}
}
  • NSObjectを継承したクラス: 落ちない
class B: NSObject{
}
  • NSObjectを継承し、init()以外のdesignated initializerを実装したクラス: 実行時にUse of unimplemented initializer 'init()' for class '***.B'で落ちる
class B: NSObject{
    init(a: String) {
    }
}
  • Objective-Cのクラスを継承したクラス: 落ちない
class B: Hoge{
}
  • Objective-Cのクラスを継承し、init()以外のdesignated initializerを実装したクラス: Use of unimplemented initializer...で落ちる
class B: Hoge {
    init(a: String) {
    }
}

ここまでの結果からNSObjectを間接的にも継承してないかinit()が呼べないと落ちることが分かる。


だが、いずれの条件も満たしていそうな次のクラスはなぜか落ちない。

class B: UIView {
    override init(frame: CGRect) {
        super.init(frame: frame)
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
}

ブレークポイントを設定してコールスタックを見るとまず[UIView init]が呼ばれ、そこからinit(frame:.zero)が呼ばれている。 実際init(frame:)を消すと Fatal error: Use of unimplemented initializer 'init(frame:)' for class '***.B' で落ちる。

コールスタック

UIKitCore`-[UIView init]:
    0x111add74b <+0>:  pushq  %rbp
    0x111add74c <+1>:  movq   %rsp, %rbp
    0x111add74f <+4>:  movq   0x7e699a(%rip), %rsi      ; "initWithFrame:"
    0x111add756 <+11>: movq   0x3c598b(%rip), %rax      ; (void *)0x0000000113a2bd00: CGRectZero
    0x111add75d <+18>: movq   0x18(%rax), %rcx
    0x111add761 <+22>: movq   0x10(%rax), %rdx
    0x111add765 <+26>: movq   (%rax), %r8
    0x111add768 <+29>: movq   0x8(%rax), %rax
    0x111add76c <+33>: pushq  %rcx
    0x111add76d <+34>: pushq  %rdx
    0x111add76e <+35>: pushq  %rax
    0x111add76f <+36>: pushq  %r8
    0x111add771 <+38>: callq  *0x3c6d39(%rip)           ; (void *)0x000000010d3b1640: objc_msgSend
->  0x111add777 <+44>: addq   $0x20, %rsp
    0x111add77b <+48>: popq   %rbp
    0x111add77c <+49>: retq   

そもそもUIViewにはinit()がないはずだが、UIView.init() は確かに呼べて、コールスタック上はエイリアスのように直接init(frame:.zero)が呼ばれているように見える。 それも他のdesignated initializerを定義して継承してないのでB.init()は呼べないはずだが呼べる。ただしinit(frame:)を継承しないで呼ぼうとすると実行時ではなくビルド時にエラーになる。 試しにObjective-CのクラスHogeにも- (instancetype)initを追加してみたがやはり継承されない。さっぱり分からない。分かる人がいたら教えて欲しい。