iOS 11からUIDebuggingInformationOverlayを使えるようにする

iOS 10から UIKit に UIDebuggingInformationOverlay が追加されました。 記憶にも新しいことでしょう。 これを使うと次のようなデバッグ用の画面を表示することができます。

f:id:dealforest:20171213194316p:plain

private APIではありますが最低限のデバッグできる機能が提供されていました。 どういうことができるかは「UIDebuggingInformationOverlay - Low Level」が参考になります。

これが残念ながら iOS 11 からは利用できなくなっていました。

iOS 10での表示方法

private APIなのでビルドすると動的に呼び出す必要がありますが、 今回は本質的な箇所を見てもらいたいのでLLDBで実行しています。

// Objective-C
(lldb) po [UIDebuggingInformationOverlay prepareDebuggingOverlay]
(lldb) po [[UIDebuggingInformationOverlay overlay] toggleVisibility]

この2行で表示することができます。

こういう場合、動的に実行できるObjective-Cは便利ですね。 Swift で書くと次のようになります。見辛いですね。。。

// Swift
(lldb) po ((NSClassFromString("UIDebuggingInformationOverlay") as! NSObject.Type).perform(Selector("prepareDebuggingOverlay"))
(lldb) po ((NSClassFromString("UIDebuggingInformationOverlay") as! NSObject.Type).perform(Selector("overlay")).takeUnretainedValue() as! UIWindow).perform(NSSelectorFromString("toggleVisibility"))

iOS 11からの表示方法

さて、これからが本題です。 残念ながら iOS 11 からは利用できなくなりました。

// iOS 11
(lldb) po [UIDebuggingInformationOverlay prepareDebuggingOverlay]
(lldb) po [UIDebuggingInformationOverlay overlay]
nil
(lldb) po [UIDebuggingInformationOverlay new]
nil

iOS 11では UIDebuggingInformationOverlayインスタンス生成ができなくなっています。

しかし、世の中にはそれでも諦めずにどうにかしてしまう人がいます。 「Swizzling in iOS 11 with UIDebuggingInformationOverlay」で紹介されています。 下の方に実際に動作するサンプルコードのDLリンクがあります。

Method Swizzlingを使って動作するようにハックをしていますが、メモリ上で処理を書き換えています。 ただそれを調査するプロセスがとにかく凄いので必見です!!

NSObject+UIDebuggingInformationOverlayInjector.mをプロジェクトに同梱した上で、次のコマンドを実行することで表示されます。

(lldb) po @import UIKit
(lldb) po [UIDebuggingInformationOverlay prepareDebuggingOverlay]
(lldb) po UITapGestureRecognizer *$gesture = [UITapGestureRecognizer new]
(lldb) po [$gesture setValue:@(UIGestureRecognizerStateEnded) forKey:@"state"]
(lldb) po [[UIDebuggingInformationOverlayInvokeGestureHandler mainHandler] 

LLDB コマンド

毎回打つのはだるいのでLLDBコマンドにしてみましょう。

/path/to/debug_overlay.py

#!/usr/bin/env python

import lldb

def process(debugger, command, result, internal_dict):
    lldb.debugger.HandleCommand('expr -lobjc -O -- @import UIKit')
    lldb.debugger.HandleCommand('expr -lobjc -O -- [UIDebuggingInformationOverlay prepareDebuggingOverlay]')
    lldb.debugger.HandleCommand('expr -lobjc -O -- UITapGestureRecognizer *$gesture = [UITapGestureRecognizer new]')
    lldb.debugger.HandleCommand('expr -lobjc -O -- [$gesture setValue:@(UIGestureRecognizerStateEnded) forKey:@"state"]')
    lldb.debugger.HandleCommand('expr -lobjc -O -- [[UIDebuggingInformationOverlayInvokeGestureHandler mainHandler] _handleActivationGesture: $gesture]')

def __lldb_init_module(debugger,internal_dict):
    debugger.HandleCommand("command script add -f debug_overlay.process debug_overlay")
    print "debug_overlay command enabled."

~/.lldbinit

...

command script import /path/to/debug_overlay.py

これで (lldb) debug_overlay とすると表示されるようになりました。 とはいえ NSObject+UIDebuggingInformationOverlayInjector.m が必要であったり、iOS 10 で動作するようにするためには分岐が必要になったりと微妙な点は多々あります。

まとめ

現時点ではここまでして使うかどうかは微妙なところがありますね。。。 いつの日か解放される日が来ることを心待ちにするのがいいかもしれません。

自分でも色々と試していたのですが動作させれずでしたが、ハックして使えるようにするまでのプロセスに感動したものがあったので紹介しました。

Enjoy バイナリハック