LIFULL Creators Blog

LIFULL Creators Blogとは、株式会社LIFULLの社員が記事を共有するブログです。自分の役立つ経験や知識を広めることで世界をもっとFULLにしていきます。

泡を食いながらAsyncDisplayKitを使ってみる

Apple原理主義者であり、Paper原理主義者とも名乗ろうかと考えている大坪です。

Facebook paperの開発舞台裏の記事を読み「がびーん」となったのがほぼ半年前。アニメーションエンジンのpopにも感動したのですが、一番重要な非同期UI部品がなかなか公開されない。最初は是非使いたいと思っていたけどこれ以上待ちきれない、と見切りをつけて開発し始めたアプリが表にでた頃いきなりニュースが飛び込んできました。


The case for AsyncDisplayKit

引用元:Introducing AsyncDisplayKit: For smooth and responsive apps on iOS | Engineering Blog | Facebook Code

Facebook paperを触るとすぐに気がつくことですがあの動きの滑らかさは普通ではない。裏で大量の画像をダウンロードし、デコードを行っている。であればCPUは忙しくなり、ユーザは反応の悪さにいらいらしてしまう。

For an app to maintain the gold standard of 60 frames per second, it can only use the main thread for milliseconds at a time. But main-thread-only UIKit views like UIImageView and UITextView can take tens to hundreds of milliseconds to size and display themselves. And while the main thread is decoding an image or rendering text, it can’t respond to user input or keep up with scrolling.

引用元:Introducing AsyncDisplayKit: For smooth and responsive apps on iOS | Engineering Blog | Facebook Code

適当な訳:秒間60フレームという黄金律を守ろうと思えば、メインスレッドを一度に数ミリ秒しか使うことができない。しかしメインスレッドでしか動かないUIImageViewやUITextViewなどのUIKit部品はサイズを変更し表示するのに数百ミリ秒を使ってしまう。メインスレッドが画像をデコードしたり文字を描画している間、アプリはユーザの入力を受け付けたりスクロールしたりすることができない。

しかしFacebook paperはほとんどの場合ユーザ操作に即座に反応する。なぜそういうことが可能なのか?

AsyncDisplayKitはメインスレッドでしか操作できないUIViewの代わりにNodeを導入しました。

If you know how to use views, you know how to use nodes. ASImageNode and the Text Kit-powered ASTextNode can be used just like their UIKit counterparts. Unlike UIKit view hierarchies, node hierarchies for entire screenfuls of content can be initialized and laid out on background threads — and nodes make it easy to take advantage of the multicore CPUs in all current iOS devices.

引用元:Introducing AsyncDisplayKit: For smooth and responsive apps on iOS | Engineering Blog | Facebook Code

適ry)訳:Viewの使い方がわかっていれば、Nodeを使うことができる。ASImageNodeやText Kitを使ったASTextNodeはUIKitの同等部品(訳注:UIImageViewとUILabel)と同じように使うことができる。UIKitの階層と違って、Nodeの階層はバックグラウンドスレッドで作成、配置することができる。そのためNodeは最近のiOSデバイスのマルチコアCPUの利点を生かすことができる。

なるほど、わからん。

実際に動かしてみないことにはなにがなんだか、というわけでさっそくサンプルプログラムを作ったわけです。参考にしたのは本家のExampleプロジェクトとStartガイドです。

ーーー

最初に言い訳をいくつか。今回文字通り泡食って作ったサンプルなので正しい使い方をしているという保証はありません。おまけにSwiftを使うのも初めて、とあっていろいろおかしなところもあるかもしれませんが私はこの記事を読んでくれる方の心の広さを信じています。

さて今回作ったのはこんなアプリです。

f:id:nextdeveloper:20141021140553g:plain

立ち上げると二つのボタンが置かれた画面がでてくる。それぞれ押すと似たような画面に遷移する。それがどうした、と言いたくなるのをぐっと堪えましょう。遷移先の画面には子猫の写真とテキストがずらっと並んでいます。個数はそれぞれ100個。

ソースはGithubにおきました。Cocoapodsを使っているので、実行前に"pod install"を実行してください。

このプログラムを例えばiPhone4Sで動かすとレスポンスの違いがわかると思います。UIViewと書かれたボタンを押すと、遷移まで「ぐっ」と一呼吸間があります。AsyncNodeを押すと一瞬で遷移する。なぜこのような違いが生じているのか。

ーーー

UIViewとかかれたボタンを押すと、普通のUIViewを使った画面へ遷移します。遷移先のUIViewControllerであるところのStdViewControllerを見てもらうと、loadView()の中で必要なビューを作成し、UIScrollView上にずらずら並べ、addSubviewしている。これらの処理は全てメインスレッドで行われるので、ここで時間をとってしまい画面遷移のアニメーションが「もっさり」になるわけです。

AsyncNodeと書かれたボタンを押すと、AsyncDisplayKitを使ったAsyncViewControllerに遷移する。ソースを見てみるとloadViewはこんな風になっています。

ベースとなるUIScrollViewをつけた後にGCDを使い、バックグラウンドスレッドで画面部品の作成、配置を行い、その後にメインスレッドでそれらをUIScrollViewにaddSubviewする。これによって重たい処理をメインスレッドで行うことなく画面遷移のアニメーションがスムーズに動くわけです。

StdViewControllerとAsyncViewControllerを比べてもらうとわかるのですが、処理の流れはほとんど同一です。(GCDを使っているところを除けば)しかし神とか悪魔は常に細部に宿っている。というわけで「細かいところ」を見ていきましょう。

違いその1 UIImageView,UILabelの代わりにASImageNode,ASTextNodeが使われている。

違いその2 文字列を画面幅に収める時、高さがどれだけになるかを計算する必要が有ります。 StdViewControllerでは

let tSize = textView.sizeThatFits(vSize)

としていますがAsyncViewControllerでは

let tSize = textNode.measure(vSize)

としています。このmeasureという関数は、sizeThatFitsと似ていますが、計算したサイズの値をASTextNodeのプロパティとして保存してくれる、といううれしさがあります。後で参照するのが簡単ですね。

違いその3

SDWebImageDownloaderを使って画像をダウンロードしています。そのcompletionBlockがAsyncViewControllerのほうが複雑になっています。

データが返ってきたときに、対象となるASImageNodeがロードされていた場合には、メインスレッドでイメージをセットする必要が有ります。これを抜かすとクラッシュです(何度もやったので確信を持って断言できます)

逆に言えば、これだけの修正で時間のかかる処理をバックグラウンドで行うことができる。やれありがたや。

というわけでなんとかこのライブラリをあちこちで使っていきたいと思っている今日この頃。ちょっと待て、UIControlはどうしてくれるとかStoryboardはどうするんだとかAutolayoutはこれとどう共存するのか、とかあれこれ面白い話があるのですが今回はここまで。