2013年2月7日木曜日

iOSはRemoteViewControllerで進化を遂げるか

Ole Begemann さんのブログで3回に渡り "Remote View Controller" に関するハックが解説されている。

Part1:


Part2:
More on Remote View Controllers – Ole Begemann

Part3:
Update on Remote View Controllers – Ole Begemann


乱暴に要点をまとめると
・iOS6でXPCが導入された
・XPCをベースにした Remote View Controller な仕組みが非公開APIで提供されている
・アプリから iOSが提供するサービス(例えばメッセージ、Facebookなど)を呼び出す時に使われている

XPCとは OS X Lion から導入された "lightweight helper tools"。launchd管理下にあって、アプリからのリクエストに応じてプロセス(Sandbox)を起こし各種サービスを提供する仕組みのこと。

(参考)

XPC Services を使う用途として次の2点が挙げられている。
安定性の向上 〜 XPC Services が落ちてもアプリには影響を与えない。
セキュリティの向上 〜 XPC Services毎にアクセス制御を細かく設定できて、アクセス可能な範囲を本体のアプリとは分離できる。Sandboxでの利用が想定されている。

OS X ではセキュリティの向上を主に目的に導入されたと思われるが、iOS6ではこれを外部プロセス(サービス)利用時のプロセス間通信の仕組みとして利用しているようだ。

そしてこのXPCを利用した RemoteViewController は、その名の通りリモート版ViewControllerの役割を果たす(※ここでいうリモートとはアプリプロセスの外部のプロセスを指す。ネットワーク越しの外部のことではない)。具体的には、サービスが提供するViewController(例えばFacebookならメッセージ編集ダイアログのViewController)のプロキシとして働く。どうも RemoteViewControllerとそのビューは、アプリ側のViewController階層、ビュー階層に透過的に組み込まれるようだ。ViewControllerとして振舞うということはアプリ側は今までのViewControllerの作法で使えるし、サービスを提供するリモート側もアプリとのインターフェイスに使い慣れたViewControllerが利用できるということだと思われる。

*イメージ*
   +−−−−−−−−−−−−−−−−−−−−−+
   |アプリのViewController|
   |   ↓         |
   |RemoteViewController|
   |(プロキシ)       |
   +−−−−−−−−−−−−−−−−−−−−−+
         ↓
        XPC
         ↓
   +−−−−−−−−−−−−−−−−−−−−−+
   | サービスプロセスの   |
   |  ViewController   |
   +−−−−−−−−−−−−−−−−−−−−−+
まだはっきりとわからないがイメージとしてはリモートのViewControllerを透過的扱えるということらしい。ということは描画もユーザのインタラクションもXPCを通じて透過的に外部プロセスとやりとりできるのかもしれない。そうだとしたら凄い。


検証記事によれば、この仕組は iOS5には無かったもので実際メッセージ作成のUIを呼び出したときの挙動が明らかに変わっていることが指摘されている。例えばMFMailComposeViewControllerを呼び出した時のビュー階層はiOS6の場合:
(lldb) po [controller.view recursiveDescription]
(id) $2 = 0x1e05c2e0 'MFMailComposeViewController:0x1e04f6d0' 1 child[MFMailComposeInternalViewController:0x1e02dfd0 ] <UILayoutContainerView: 0x1e04ffe0; frame = (0 0; 320 480); autoresize = W+H; layer = <CALayer: 0x1e0500a0>>
   | <UINavigationTransitionView: 0x1d57f6a0; frame = (0 0; 320 480); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x1d57f770>>
   |    | <UIViewControllerWrapperView: 0x1e04f600; frame = (0 20; 320 460); autoresize = W+H; layer = <CALayer: 0x1e0241f0>>
   |    |    | 'MFMailComposeInternalViewController:0x1e02dfd0' 1 child[MFMailComposeRemoteViewController:0x1e055230 ] <UIView: 0x1e05f9a0; frame = (0 0; 320 460); autoresize = W+H; layer = <CALayer: 0x1e05fa00>>
   |    |    |    | 'MFMailComposeRemoteViewController:0x1e055230' <_UISizeTrackingView: 0x1e05c030; frame = (0 0; 320 460); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x1e05c110>>
   |    |    |    |    | <_UIRemoteView: 0x1e05c300; frame = (0 0; 320 480); transform = [0.5, -0, 0, 0.5, -0, 0]; userInteractionEnabled = NO; layer = <CALayerHost: 0x1e05c460>>
最下層に *RemoteViewController と _UIRemoteView が組み込まれているのがわかる。

次はiOS5の場合
(lldb) po controller
(MFMailComposeViewController *) $1 = 0x07ea0420 <MFMailComposeViewController: 0x7ea0420>
(lldb) po [controller.view recursiveDescription]
(id) $2 = 0x08bb4d50 'MFMailComposeViewController:0x7ea0420' 1 child[MFMailComposeController:0x89b0220 ] <UILayoutContainerView: 0x8b97e70; frame = (0 0; 320 480); autoresize = W+H; layer = <CALayer: 0x8b97ec0>>
   | <UINavigationTransitionView: 0x89b1460; frame = (0 0; 320 480); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x89b1500>>
   |    | <UIViewControllerWrapperView: 0x7e77990; frame = (0 64; 320 416); autoresize = W+H; layer = <CALayer: 0x7e98af0>>
   |    |    | 'MFMailComposeController:0x89b0220' <MFMailComposeView: 0x89b4410; baseClass = UITransitionView; frame = (0 0; 320 416); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x89b4530>>
  :
と、かなり長いので割愛するが、iOS5の場合は通常のローカルなUIViewControllerのサブクラス、そしてUILabelやUIButtonがビュー階層に配置されている。そう、iOS6の方では画面上に配置されているUILabelやUIButtonがビュー階層に現れない(!)。

検証記事では色々な方法を駆使して最終的にはRemoteViewControllerとXPCの接点になると思われるメソッドを突き止める。
+ (id)requestViewController:(id)arg1
    fromServiceWithBundleIdentifier:(id)arg2 connectionHandler:(id)arg3;
こんな感じで使われる。
[MFMailComposeRemoteViewController
                      requestViewController:@"ComposeServiceRemoteViewController"
            fromServiceWithBundleIdentifier:@"com.apple.MailCompositionService"
                          connectionHandler:handler];
第一引数がRemoteViewControllerのクラス名、第二引数がサービスのバンドルID。この例の場合、/Applications/MailCompositionService.appに対応するようだ。
そして第三引数は2つの引数を取るブロック。こんな感じ。
typedef void (^ConnectionHandler)(id blockArg1, id blockArg2);
blockArg1にはリモートと接続されたRemoteViewControllerのインスタンス、blockArg2はNSError*と推察されている。ここでこのブロックを意図的に置き換え(swizzledBlock)、さらにRemoteViewControllerになりすますプロキシ(loggerProxy)を用意して、RemoteViewControllerにどんなメッセージが送られているかを調べている。結果はこう:
Logger Proxy: [<MFMailComposeRemoteViewController: 0x1c576600> serviceViewControllerProxy]
  return value: @: <_UIViewServiceImplicitAnimationEncodingProxy: 0x1d83e4a0; target: <_XPCProxyReplyHandlerQueueRedirectingProxy: 0x1d8701d0; target: <_UIViewServiceXPCProxy: 0x1d8613c0>>>

Logger Proxy: [<MFMailComposeRemoteViewController: 0x1c576600> setDelegate:]
  called with arguments: (
    "@: <MFMailComposeInternalViewController: 0x1c56f9c0>"
  )
  return value: v: (void)

Logger Proxy: [<MFMailComposeRemoteViewController: 0x1c576600> parentViewController]
  return value: @: (null)

Logger Proxy: [<MFMailComposeRemoteViewController: 0x1c576600> view]
  return value: @: XX ('MFMailComposeRemoteViewController:0x1c576600' <_UISizeTrackingView: 0x1d855440; frame = (0 0; 0 0); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x1d865e90>>)

Logger Proxy: [<MFMailComposeRemoteViewController: 0x1c576600> parentViewController]
  return value: @: (null)

Logger Proxy: [<MFMailComposeRemoteViewController: 0x1c576600> _existingView]
  return value: @: 'MFMailComposeRemoteViewController:0x1c576600' <_UISizeTrackingView: 0x1d855440; frame = (0 0; 320 416); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x1d865e90>>

Logger Proxy: [<MFMailComposeRemoteViewController: 0x1c576600> _existingView]
  return value: @: 'MFMailComposeRemoteViewController:0x1c576600' <_UISizeTrackingView: 0x1d855440; frame = (0 0; 320 416); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x1d865e90>>
   :
setParentViewController: や willMoveToParentViewController: など通常のViewControllerの初期化で呼び出されるメッセージが送られている。リモートのViewControllerが、透過的にローカルViewControllerとして働くことが推察される。

Part2では発見されたRemoteViewControllerが表でまとめられている。
More on Remote View Controllers – Ole Begemann

これをみると既にいくつものサービスがこの仕組で提供されているのがわかる。またPart3では_UIWebViewControllerも紹介されている。WebブラウザはSandbox化が望まれるコンポーネントなのでもし今後XPC経由で安全に扱えるようになればセキュリティ上大きなメリットになると思われる。


- - - -
Appleが一般開発者にXPCサービスの開発を開放することはこれまでの方針から考えにくいが、Appleが提供するXPCサービスが今後iOS7以降、さらに増えることは間違いない。それらは現状と同じように各種フレームワークのAPIとして提供されると思われる。ただそうだとしてもサービスが増えるメリット、XPCの特性を生かしたセキュアなコンポーネントが利用できるメリットなど地味ながらも大きな改善になるだろう。そして将来もしXPCサービスの開発を一般開発者へ開放することがあれば、iOSは今とはかなり違った体験を提供できるスマートフォンになるかも。これはちょっとワクワクする。

なお今回は(というか毎回ですが)勢いで書いたので間違い・勘違いがあれば遠慮無く指摘して下さい。


2 件のコメント:

  1. (Android 知らないので想像ですが)C の argc, argv で何でもプロセスに渡す密結合であろう Android と比べると、この仕組みはモダンですね!!

    返信削除
  2. Matsugaki さん、こんにちは。

    私もAndroidを知らないのでこの辺り比較してみたい気もしています。
    XPCの仕組みは従来のDLL方式と異なりミニサーバともいえる仕組みなのでセキュリティと安定性の面でメリットが期待できますね。

    返信削除