解脱してAutoLayoutのエラーログをスラスラ読もう

解脱してAutoLayoutのエラーログをスラスラ読もう

解脱(Gedatsu)をすればAutoLayoutのエラーログもスラスラ読めるようになります

解脱前 解脱後

解脱を入れるには

CocoapodsでもSwiftPackageManagerでもCarthageでもマニュアルインストールでもいいですが、まずはGedatsuのInstallセクションを見て、Gedatsuをプロジェクトにインストールしていきましょう

解脱するには

解脱するのは簡単です。AppDelegate.application:didFinishLaunchingWithOptions:Gedatsu.open をして悟りを開くだけです。 DEBUG フラグで囲むのを忘れないように

#if DEBUG
import Gedatsu
#endif

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    #if DEBUG
    Gedatsu.open()
    #endif
    return true
}

解脱の仕組み

どうやって私が解脱まで到達できたのか少し紹介します。まず Gedatsu のエントリポイントとなる関数を見てみましょう。ここでは標準エラー出力までに途中に処理を挟んでいます。次にUIKitのPrivate APIをフックしている関数を見ます。

Gedatsu.open

コードはここです

internal func open() {
    _ = dup2(STDERR_FILENO, writer.writingFileDescriptor)
    _ = dup2(reader.writingFileDescriptor, STDERR_FILENO)
    source = DispatchSource.makeReadSource(fileDescriptor: reader.readingFileDescriptor, queue: .init(label: "com.bannzai.gedatsu"))
    source.setEventHandler { ... }
    source.activate()

普段iOS開発たちでは使わない関数たちばかりですね。僕も初めて使いました。ここらへんの関数たち説明を書こうと思ったのですが思ったよりボリューミーだったので途中から挫折しちゃいました。(てへ

なので簡易的な説明だけ書いておきます。参考のリンクも貼っておくので興味があったら調べてみてください。簡易的な説明すると 1つめの dup2writer.writingFileDescriptor を通して標準エラー出力に書き込めるように。2つ目の dup2標準エラー出力の書き込み先を reader.writingFileDescriptor にしています。DispatchSource.makeReadSource では監視したいファイルディスクリプターを渡して、setEventHandler で監視対象のイベントのハンドリング、activate で監視の開始をしています。

あと途中まで詳しく書こうとして断念した文章をおいておきます。情報量はあまり変わりませんがもう少し丁寧な言葉づかいになっています。

途中まで詳しく書こうとして断念した文章

dup2はファイルディスクリプタを複製するシステムコールの関数です。duplicateの略ですね。2はきっと引数の数です。ファイルディスクリプタはファイルに対して割り当てられている識別子です。参照先のファイルを表すポインタみたいなやつです。少し説明を省略しますが、1つめのdup2writer.writingFileDescriptorを通して標準エラー出力に書き込めるように。2つ目のdup2で 標準エラー出力の書き込み先をreader.writingFileDescriptor` にしています。

readerやらwriterといったものは Pipeのラッパーです。例えばReaderの中身だとこんな具合です。Pipeで用意されたファイルに書き込まれると(2つ目のdup2を思い出してください)ファイルが読み込めるようになります。

internal class ReaderImpl: Reader {
    let pipe: Pipe = Pipe()
    var writingFileDescriptor: Int32 { pipe.fileHandleForWriting.fileDescriptor }
    var readingFileDescriptor: Int32 { pipe.fileHandleForReading.fileDescriptor }

    func read() -> Data {
        pipe.fileHandleForReading.availableData
    }
}

次に DispatchSource.makeReadSource で行っていることは reader.readingFileDescriptor を監視するために渡しています。きっと指定したファイルディスクリプタに向いているファイルに変更がある場合にデータを読み込むように割当られる仕組みなんでしょう(雑な理解。source.setEventHandler で変更を検知したときに実行したい処理内容を書きます。この場合はAutoLayoutでエラーが起きた時点のデータからログを整形してコンソールに流す。それ以外は整形せずにコンソールに流す。といったことをしています。最後の source.activate() で監視を始める流れになっています。

通常何かしらのエラーのログや出力は標準エラー出力書かれてコンソール上に出てきます。もっと噛み砕くと 1. Process X < 標準エラーにログ書くぞ 1. 標準エラー < Process X から書かれるぞ!書かれてるぞ! 1. 標準エラー < 書かれたからコンソール上に出してくれ みたいな流れです。

UIView.engine:willBreakConstraint:dueToMutuallyExclusiveConstraints:

2つ目のUIKitのPrivate APIのフックです。コードはここです こちらはシンプルにObjective-CのRuntime APIを Swiftから呼び出してメソッドを入れ替えることで途中の処理を挟んでいます。

internal static func swizzle() {
    guard let from = class_getInstanceMethod(UIView.classForCoder(), NSSelectorFromString("engine:willBreakConstraint:dueToMutuallyExclusiveConstraints:")) else {
        fatalError("Could not get instance method for UIView.engine:willBreakConstraint:dueToMutuallyExclusiveConstraints:")
    }
    guard let to = class_getInstanceMethod(UIView.classForCoder(), #selector(UIView._engine(engine:constraint:exclusiveConstraints:))) else {
        fatalError("Could not get instance method for UIView.\(#selector(UIView._engine(engine:constraint:exclusiveConstraints:)))")
    }
    method_exchangeImplementations(from, to)
}

このPrivate API UIKitCoreの中のシンボルに含まれていました。 下記のコマンドで確認ができます。

$ nm /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore | grep engine:willBreakConstraint:dueToMutuallyExclusiveConstraints:

上記の2つを組み合わせて AutoLeyoutの制約のエラーが発生したときにコンソールに任意のエラーメッセージを出力する仕組みを実現しています。

解脱した人の声

まとめ

Gedatsuを望む方リポジトリはこちらです。
https://github.com/bannzai/Gedatsu/

そして解脱をした人もこれからする人も

スターください 🌟

おしまい\(^o^)/