2012年11月22日木曜日

UIViewAnimationに渡したブロックはどうなるの?

UIViewAnimationを入れ子構造にしてselfをリークさせる、高度なヒドいコードを修正していたら疑問になったんだけど、UIViewAnimationに渡したanimationブロックはその後どうなるんだろうか。

view.layerに対してremoveAllAnimationsを実行すれば、UIViewアニメーションを中断できることから、animationブロック内部の処理は、CAAnimationに変換されて、viewのlayerに渡されるということは推測できる。

iOS View プログラミングガイドでは、UIViewのアニメーションメソッドが対象とするプロパティはview自身のものに限定されているような書き方をしているのに、実際にはview.layerに対する処理を書いても問題なく実行されることを不思議に思っていたんだけれど、layerにaddAnimationしているならば当然。

するとcompletionブロックはCABasinAnimationのdelegateインスタンスに保持されるんだろう。非形式プロトコルのdelegateメソッド、animationDidStop:finished:から実際のブロック実装が呼び出されるかたちかな。実際はメソッド実装の書き換えとかもっと高度なことをやってそうだけど。

このCore Animationのdelegateメソッドは、Cocoaのメモリ管理のレアな例外で、保持の向きが逆になっている。(Animationがdelegateを保持する)

まとめると、

  • アニメーションブロックはCAAnimationに変換されて、view.layerにaddAnimationされる。
  • 完了ブロックは、CAAnimationのdelegateメソッドを実装したインスタンスに保持されて、さらにdelegateがCAAnimationに保持される。
  • もし完了ブロックにviewを保持するインスタンスをキャプチャした場合、循環参照が発生する。

例えばUIViewControllerにアニメーション処理を記述したならば、UIViewController → view → layer → animation → animationDelegate → completationBlock → UIViewController…という循環が生じるわけだ。

そうなっても、完了ブロックが終了してUIViewControllerが解放されれば、循環は解消するので普通は心配いらない。

---

循環を起こすコードは例えばこんなの。

-(void)animation
{
    [UIView animateWithDuration:1.0f delay:0.0f options:UIViewAnimationCurveEaseInOut animations:^{
        self.theView.transform = CGAffineTransformMakeTranslation(0200);
    } completion:^(BOOL finished) {
        NSLog(@"%@ %@",selfself.theView);
        [UIView animateWithDuration:1.0f delay:0.0f options:UIViewAnimationCurveEaseInOut animations:^{
            self.theView.transform = CGAffineTransformIdentity;
        } completion:^(BOOL finished) {
            NSLog(@"%@ %@",selfself.theView);
            [self animation];
        }];
    }];
}
入れ子にしなくても循環するけど見た目がアニメーションにならないので。

これで別画面に行くとどうなるかというと、二つのcompletationブロックでNSLogが高速に吐き出され続ける。layerの実体を管理してるのはviewではなく、描画システム側だから描画処理自体はスキップされて、延々と呼び出しだけ繰り返される…という感じかな。

ちょっと考えれば再帰呼び出ししてるからアウトだと理解できそうなもんなんだけど、厄介なことに単なる再帰呼び出しと違ってスタックオーバーフローが起きないから発覚を遅らせる。完了ブロックが完了ブロックを呼び出す度に元のブロックは律儀に解放されてるのかな。

こういう変に手の込んだバグコードを書きそうになったら「もっとシンプルな方法があるんじゃない?」と自問すべき。大抵はもっとシンプルで、そしてその方が正しい実装法の書き方が存在する。

上のコードの場合、こう書き直せる。

-(void)animation
{
    [UIView animateWithDuration:1.0f
                          delay:0.0f
                        options:UIViewAnimationOptionCurveEaseInOut UIViewAnimationOptionAutoreverse UIViewAnimationOptionRepeat
                     animations:^{
        self.theView.transform = CGAffineTransformMakeTranslation(0200);
                        } completion:^(BOOL finished){
                            NSLog(@"finished");
    }];
}
UIViewAnimationOptionRepeatを指定すれば、そのアニメーションは繰り返され続けるから、それこそ無限ループを発生させているようで不安になるのかもしれないけど、画面遷移などでlayerが無効になると自動的にアニメーションが停止し、completionに記述された処理が実行される。

UIViewAnimationを入れ子にする処理はAppleのサンプルもやってるけど、Blocksの理解が甘いと変な罠に引っかかるから、真似しない方が良いと思う。

入れ子にしないと実現できないような、複雑なアニメーションをやりたければ、Core Animationを勉強してkey-frame animation辺りで実装した方がシンプルにできる。Core AnimationはCore系の中では最弱なので簡単に覚えられるはず。

----

最近修正してるヒドいコードの数々は、自分より立場の偉い人間が書いてるので、文句が言えない。せめて同じ轍を踏む人間が減りますように。

0 件のコメント:

コメントを投稿