アクセシビリティ
デベロッパーリソース
吉川和巳氏

スティルハウス
吉川和巳氏

作成日:
2009年9月10日
ユーザレベル:
すべて
製品:
Adobe Flex
Adobe AIR

Flex/AIRハマり帳 ~第3回 Flash Playerのシングルスレッドモデルでハマらない方法~

はじめに

Flex/AIR開発には、ところどころに「落とし穴」があります。過去3年間のFlex/AIR開発を通じて、筆者はそうした「落とし穴」にことごとくハマり、そのたびに「二度と繰り返すまい」との思いで要点をメモしてきました。本連載では、その筆者の「Flex/AIRハマり帳」をもとに、これからFlex/AIR開発を始める方が同じ過ちを繰り返さないためのささやかなtipsを紹介していきたいと思います(なお、ここで紹介するtipsは、あくまで筆者の経験に基づいて得られたものであり、アドビシステムズの公式な提供情報ではないことにご留意ください)。

ComboBoxの「ひっかかり」でハマる

まずは、以下のサンプルを試してみてください(クリックすると動作します)。ごらんの通り2つのComboBoxが並んでおり、それぞれ選択すると、選んだ文字列が画面下のTextAreaに大量にコピーされるというサンプルです。

ComboBoxのサンプル(サンプル表示後、右クリック→「ソースの表示」でソースが見られます)

図:ComboBoxのサンプル(サンプル表示後、右クリック→「ソースの表示」でソースが見られます)

左と右のComboBoxは、機能的には何ら違う点はありません。しかしお試しいただくと分かるとおり、左のComboBoxは何か動きが最後に「ひっかかる感じ」です。文字列を選択したあと、ドロップダウンリストが上に引っ込むときの動作が若干スムーズではありません。一方、右のComboBoxは通常どおりスムーズに引っ込んでくれます。

実は筆者は、ある開発案件でこの問題に悩まされました。お客様から「ComboBoxの動きがどうもぎくしゃくする」という指摘を受けたものの、どう対処すればよいか分からなかったのです。

原因については、ある程度の予測はつきました。ComboBoxの選択にともなって実行される処理が重い(時間がかかる)のです。例えば上記サンプルの左のComboBoxは、以下のようなMXMLコードで記述されています。

<mx:ArrayCollection id="ac">
    <mx:String>Hello, </mx:String>
    <mx:String>This is a </mx:String>
    <mx:String>sample code. </mx:String>
</mx:ArrayCollection>

<mx:ComboBox id="cbKeyword1" change="onKeywordChange1(event)" dataProvider="{ac}"/>

このように、ComboBoxの選択にともなうchangeイベントによってonKeywordChange1()関数で呼び出す仕組みです。同関数は、以下のように定義されています。

private function onKeywordChange1(e:Event):void {
    doSlowTask(cbKeyword1.selectedLabel);
}

// なにか重い処理
private function doSlowTask(text:String):void {
    var s:String = "";
    for (var i:int = 0; i < 10000; i++) {
        s += text;
    }
    taKeywords.text = s;
}

このサンプルでは、「選択された文字列を1万回コピーしてTextAreaにセットする」という処理(doSlowTask関数)を実行していますが、要するにこの部分は「時間がかかる処理」ならなんでもかまいません。筆者の開発案件の例では、AIRクライアントが内蔵するSQLiteデータベースに対する検索処理(同期呼び出し)でした。ComboBoxのchangeイベントを受けてこうした処理を実行すると、ComboBoxの動きがぎくしゃくするのです。

Flashは「シングルスレッド」で動く

なぜこのようなことが起きるのか。それを調べる過程で学んだのは、「Flash Playerはシングルスレッドで動作する」という点が重要な意味を持っていることです。参考になったのは、以下のWebページです。

Adobe MAX 2006 フラッシュレポート/Improving ActionScript 3 Performance

このページでは、以下のように説明されています。

「Flash Playerはシングルスレッドで動作し、そのなかでグラフィックのレンダリングとActionScriptの実行が交代で行われます。ActionScript実行ステージが完了しないと、次のレンダリングステージには行きません」

「シングルスレッド」とは、「同時に1つのプログラムだけを逐次実行する」というモデルです。例えるなら、たくさんある仕事をすべて1人でこなすようなものです。いろいろな仕事を切り替えながら「並行(concurrent)」に処理することはできますが、2つの仕事を同時に「並列(parallel)」に処理することはできません。一方、シングルスレッドと対比される「マルチスレッド」とは、「1回に複数のプログラムを並列処理する」というモデルです。たくさんある仕事を複数人でこなすようなものなので、誰か1人が仕事でハマっていても、もう1人が他の仕事を並列して進めることができます。

シングルスレッドとマルチスレッド

図:シングルスレッドとマルチスレッド

一般に、GUIアプリケーションはマルチスレッドモデルで実装されている例が少なくありません。そのため、例えばボタンをクリックしたあとに実行される処理に長い時間がかかったとしても、UIの描画や他のイベント処理は並列処理されるため、GUIアプリケーション全体が「固まってしまう」ことはありません。

これに対し、FlashおよびFlex/AIRはシングルスレッドで実装されたGUIアプリケーションです。上記ページで説明されているように、Flash Playerは以下の2つの処理を短い時間で切り替えながら「並行」処理しています。

●ActionScriptの実行
●UIの描画

この仕組みがスムーズに動作する前提条件は、このどちらの処理も短時間で終了する(もしくは中断できる)という「譲り合いの精神」です。上記例のComboBoxのように、changeイベントで起動されたActionScriptコードが長時間にわたって処理を続けてしまうと、ComboBoxのドロップダウンリストを引っ込めるUI描画が滞ってしまい、引っかかるような動きとなってしまうのです。

callLater関数を使うべし

こうした罠にはまった筆者を救ったのが、上記ページでも紹介されている「callLater関数」です。この関数は、mx.core.UIComponentインタフェースで定義されている関数であり、MXMLコンポーネント内のActionScriptコードから呼び出しが可能です。上記サンプルの右のComboBox(スムーズに動く方)では、以下のようにcallLater関数を利用するイベントハンドラを使用しています。

private function onKeywordChange2(e:Event):void {
    callLater(function():void {
        doSlowTask(cbKeyword2.selectedLabel);
    });
}

callLater関数は、その名が示すとおり「後回しにする」関数です。上記の例で分かる通り、callLater関数の引数には関数(Functionオブジェクト)を渡します。時間がかかる処理は、この引数の関数内で呼び出すように記述しておくことで、UI描画がすべて終了してから実行されます。これにより、ComboBoxの動きがぎこちなく見える現象が解消される仕組みです。

非同期で行こう!

Flex/AIRのシングルスレッドモデルをスムーズに動かすためのtipsは、このcallLater関数の利用の他にもあります。基本ルールは、「時間がかかる処理は非同期で実行すべし」です。

「非同期(asynchronous)処理」とは、「呼び出し元が待たされない処理方式」のことです。これを日常業務でのコミュニケーションになぞらえると、例えば「電子メール」では、メールの発信元はメールを送りっぱなしにして次の作業に取りかかれるため、非同期型のコミュニケーションが可能です。一方「電話」では、相手が返事してくれるまで発信元は待たされます。この「呼び出し元が待たされる処理方式」は「同期(synchronous)」処理と呼ばれます。

Flex/AIRでは、この非同期処理のためのAPIがいくつか用意されています。例えばAIRの内蔵データベースSQLiteでは、データベース・アクセスのためのAPIとして、一般的な同期型(DB検索が終わるまで待たされる)と非同期型(DB検索が終わったらイベントでコールバックしてくれる)の両方を提供しています。また同様に、AIRのファイル・アクセスAPIでも非同期型と同期型の2種類を用意しており、ファイルのコピーといった時間のかかる処理でもアプリケーションがフリーズしないような実装が可能です。

Flex/AIRを非同期式に実装する簡単な手段として、flash.utils.Timerクラスを活用することも可能です。例えば、UIコンポーネントのイベントをトリガーとしてTimerをスタートさせ、Timerのtimerイベントを受けて実際の処理を実行するように記述しておきます。これにより、イベントハンドラと実際の処理を切り離した非同期式の実装が可能です。以下のサンプルの動作をお試しください。

Timerによる非同期処理のサンプル(サンプル表示後、右クリック→「ソースの表示」でソースが見られます)

図:Timerによる非同期処理のサンプル(サンプル表示後、右クリック→「ソースの表示」でソースが見られます)

このサンプルを使うと分かるように、左側のTextInputでは文字を入力するたび(=changeイベントが発生するたび)に重い文字列コピー処理が実行されるため、TextInputのUI描画がぎこちなくなってしまいます。これに対し、右側のTextInputでは文字入力の後にワンクッション(正確には500ms)おいてから処理が実行されるため、UIはスムーズに動作しています。

この右側のTextInputのchangeイベントでは、以下のようなイベントハンドラを呼び出しています。

private function onKeywordChange2(e:Event):void {
    timer.reset();
    timer.start();
}

このように、イベントハンドラの中ではTimerをリセットして再スタートさせているだけであり、これらの関数呼び出しによってUI描画が妨げられることはありません。一方、Timerの方は以下のように初期化されています。

private function init():void {
    timer = new Timer(500);
    timer.addEventListener(TimerEvent.TIMER, function(e:Event):void {
        timer.stop();
        doSlowTask(tiKeyword2.text);
    });
}

上記例のように、timerイベントのイベントハンドラ内で重い処理を呼び出すことで、非同期動作が可能です。また、短時間で多数発生するchangeイベントによるチャタリングを吸収して1回の処理にまとめるという効果も得られています。

以上、今回はFlash Playerのシングルスレッドモデルに注目し、それがもたらす問題と対策について紹介しました。ここで取り上げたような「UIの微妙なぎこちなさ」は、要件定義書や仕様書に明記されるようなものではありません。しかし発注元やエンドユーザーがアプリケーションに触れた際に与える印象(エクスペリエンス)を損なう要因ともなり、それを解消するために思ったよりも大きな工数が取られることもしばしばあります。Flex/AIR開発における顧客満足と生産性を高めるためにも、こうした「小技」を覚えておくことが意外に有効だと言えるでしょう。

著者について

スティルハウス 吉川和巳氏

テクニカルライター。
Adobe FlexおよびAIRなどのリッチ・インターネット・アプリケーション分野をはじめ、Javaサーバサイド・プログラミング、データベース開発、仮想化技術などを中心に執筆活動を行っている。また技術文書や書籍の翻訳も手がけており、翻訳書に「XML構築ガイド」(ピアソンエデュケーション)がある。