モジュール 3 タスク 3: より多くのデータの取得と読み込み


JavaFX メディアブラウザアプリケーションを起動する NetBeans プロジェクトをダウンロードする


注: 複数の検索を行うとパフォーマンスに悪影響があるという既知の問題があります。この問題が発生した場合、アプリケーションを再起動してください。



はじめに

これまで、モジュール 3 では、検索結果の数は 50 に制限されていました。これは、Yahoo API から返される検索結果の最大数です。このモジュールでは、検索の開始番号を指定する API のフィールドを活用することにより、より多くの結果を取得します。検索結果を多く取得すると、サムネイルをすべて読み込むのに問題が生じます。このタスクでは、同時に読み込むサムネイル数を制御するメカニズムの作成方法について説明します。

プロジェクトの実行

  1. モジュール 3 タスク 3 NetBeans プロジェクトをダウンロードして、NetBeans IDE を開きます。

  2. プロジェクトを実行します。

    画像がより多く読み込まれるため、スクロールバーの動作も変わります。図 1 に示すように、表示可能なサムネイルを表すスクロールオーバーレイが、水平方向の中央に配置されます。右にスクロールすると、サムネイルの列を表すリッジが左に移動し、オーバーレイは中央に配置し直されます。検索結果が多数ある場合、スクロールバーはウィンドウの両端でフェードアウトして表示されます。

    高速でスクロールすると、「事前読み込み」されていないサムネイルが発生し、そのサムネイルが読み込まれるまではプレースホルダーが表示されます。「サムネイル読み込みの抑制」で説明するように、読み込みは ThumbnailController で管理されます。

検索とスクロールが拡張された壁 図 1

アーキテクチャー

このタスクでは、新しいクラス ThumbnailController.fx を 1 つ追加して、図 2 で青く示した既存のクラスを変更します。

モジュール 3 タスク 3 のアーキテクチャー 図 2

より多くの検索結果の取得

WebSearch.searchLocationHttpRequest が使用する URL です。YahooAPI では、Yahoo 画像 REST クエリー仕様にしたがって、定義されています。モジュール 2 タスク 1 では、searchLocation は次のように、YahooAPI で初期化されています。

ソースコード
    override var searchLocation = bind
           "http://search.yahooapis.com/ImageSearchService/V1/imageSearch"
           "?appid=={yahooSearchAppID}&query={yahooSearchQuery}"
           "&results={numberOfYahooResults}";

Yahoo 画像検索 API ごとに、このクエリーは最大 50 の結果を返します。Yahoo API には、結果の開始位置を設定できる start パラメータがあります。そのため、より多くの結果を取得するには、次の結果ブロックの位置を start に指定して、別の HTTP GET 要求を送信する必要があります。

コードでは、HttpRequest のシーケンスを用意するというアプローチを取っています。最初の要求が完了したら次の要求を送信する、ということをシーケンスの最後に到達するまで続けます。WebSearch.fx では、searchLocation がシーケンスに変更されます。

ソースコード
    package var searchLocation : String[];

YahooAPI.fx では、WebSearch が拡張され、searchLocation シーケンスが次のように初期化されます。

ソースコード
    def yahooSearchLocation = bind
           "http://search.yahooapis.com/ImageSearchService/V1/imageSearch"
           "?appid=={yahooSearchAppID}&query={yahooSearchQuery}"
           "&results={numberOfYahooResults}"
       on replace {
            searchLocation = for (i in [1..500 step numberOfYahooResults]) {
                "{yahooSearchLocation}&start={i}"
            }
        };

これで、yahooSearchQuery が変更されると必ず、新しい searchLocation シーケンスが作成されます。このようなことが発生するイベントのチェーンは、SearchTextBox.fx で開始されます。ここでは、WebSearch インスタンスの searchQuery 変数がテキストボックスの値に設定されます。YahooAPI の searchQuery にあるトリガーによって、「語句を含む」という意味を表すため空白文字は「+」に置換され、ローカル変数 yahooSearchQuery が設定されます。yahooSearchQuery が変更されるため、yahooSearchLocation のバインドが評価され、on-replace ブロックでは新しい searchLocation のシーケンスが作成されます。

WebSearch では、searchLocation のシーケンスを作成するために、一部を変更する必要がありました。

ソースコード
def httpRequest : HttpRequest[] = bind for (loc in [0..<sizeof searchLocation])
 {
    HttpRequest {

    method: HttpRequest.GET

        location: bind searchLocation[loc]

         onResponseMessage: function(msg:String) {
            if ( httpRequest[loc].responseCode != 200 ) {
                println("HTTP response 
                         {httpRequest[loc].responseCode}: {msg}");
            }
        }

        onException: function(ex: Exception) {
            ex.printStackTrace();
        }

        onDone: function() {
            (httpRequest[loc].context as HttpRequest).enqueue();
        }

        onInput: function(is: InputStream) {
            try {
                pullParser.input = is;
                pullParser.parse();
            } finally {
               is.close();
           }
        }
    }
 }

このコードで、HttpRequest のシーケンスが作成されるようになったことに注意してください。この for ループは、metaData シーケンスから thumbnails シーケンスを作成する Wall のループと非常によく似ています。for ループの範囲式でシーケンススライスが使用されていることにも注意してください。これにより、ループの上限が sizeof searchLocation - 1 に制限されています。

シーケンス以外のこのコードブロックの主な相違点は、HttpRequest.onDone 関数です。HttpRequest.context 変数の使用方法に注意してください。HttpRequest 自体はコンテキストに何の影響も与えません。そのため、変数は任意の値に割り当てることができます。コードのこの部分では、コンテキストを使用して、次に送信する httpRequest への参照を保持しています。このように、ある要求が完了したら、チェーンの次の要求が送信されます。

次のブロックでは要求チェーンが設定され、インスタンスの初期化後に実行されます。

ソースコード
    postinit {
        // Chain the Requests}
        for (i in [1..<sizeof httpRequest]) {
            httpRequest[i-1].context = httpRequest[i];
        }
    }

サムネイル読み込みの抑制

各要求からデータが返されると、そのデータが結果シークエンスに追加され、新しい Thumbnail インスタンスが Wall に作成されます。ただし、これらのサムネイルに対する画像のすべてを読み込む必要はありません。すべてを読み込むと、処理やメモリ消費の点で相当なオーバーヘッドが生じます。この問題に対処するため、このコードではレート制限機能を設け、すぐ「隣り」のシーンにあるサムネイルだけを読み込むようにしています。ここからは、この機能の動作について説明します。

ここでは、Thumbnail、新しいクラスの ThumbnailController、および Wall (使用頻度は低い) の、3 つのクラスが使用されます。ThumbnailController からの指示があるまで、Thumbnail では画像を読み込まないというのが、基本的なアーキテクチャーです。Thumbnail では、boundsInScene 変数のトリガーを起動することで、読み込みを行うかどうかを決定します。Thumbnail が表示可能な隣接する画面にある場合、Thumbnail 自体が読み込み要求とともに ThumbnailController のキューに登録されます。同様に、Thumbnail が表示可能な隣接する画面になくなった場合、Thumbnail 自体を読み込みを解除するためにキューに登録します。次に、ThumbnailController はキューに対し、Thumbnail に画像の読み込みまたは読み込みの解除を行うように指示を出します。

Thumbnail.fx の次のコードは、サムネイル画像を読み込むもので、これまでのコードとある程度似ています。これまでのタスクとの相違は、画像が読み込まれるのは、loadStatus 変数が Constants.THUMB_LOAD に設定されている場合だけということです。それ以外の場合、画像はプレースホルダ画像の 1 つになります。

ソースコード
    function checkLoadStatus() : Void {
        if (loadStatus == Constants.THUMB_UNLOAD) {
            if (metaData.media_type == MetaData.type_image) {
                image = Constants.PHOTO_PLACEHOLDER;
            } else {
                image = Constants.VIDEO_PLACEHOLDER;
            }
        } else {
            if (metaData != null) {
                image = Image {
                    url: metaData.thumb.url
                    placeholder:
                        if (metaData.media_type == MetaData.type_image) {
                            Constants.PHOTO_PLACEHOLDER
                        } else {
                            Constants.VIDEO_PLACEHOLDER
                        }

                    width : Constants.THUMB_WIDTH * Constants.EXPANDED_THUMB_SCALE
                    height: Constants.THUMB_HEIGHT * 
                            Constants.EXPANDED_THUMB_SCALE
                    preserveRatio : true
                    backgroundLoading: true
                };
            }
        }
    };

loadStatus フラグの値が変更されると、checkLoadStatus() 関数が呼び出されます。後で説明しますが、loadStatus フラグは、ThumbnailController から送信されます。ただし、最初に Thumbnail のインスタンスは ThumbnailController キューにあることが必要です。Thumbnail 自身をキューに追加するかどうかは、boundsInScene 値によるトリガーで決まります。

ソースコード
    var desiredLoadStatus = bind {
        if (boundsInScene.maxX >= -Constants.STAGE_WIDTH and
            boundsInScene.minX <= 2 * Constants.STAGE_WIDTH) {

            true;
        } else {
            false;
        }
    }

Thumbnail が表示可能な隣接する画面 (ここではステージ幅 3 つ分と定義されています) にある場合、desiredLoadStatus は true になります。ただし、Thumbnail が既にキューに追加されている可能性があるため、実行する内容は、キューに追加する、キューから削除する、何もしないのいずれかに決定する必要があります。この決定は、次のトリガーによって行われます。

ソースコード
    var loadStatusTrigger = bind (not controller.scrolling) and
                                 (desiredLoadStatus != requestedLoadStatus) on replace {
        if (loadStatusTrigger) {
            requestedLoadStatus = desiredLoadStatus;
            controller.queueAction(this,
                                   if (requestedLoadStatus) {
                                       Constants.THUMB_LOAD
                                   } else {
                                       Constants.THUMB_UNLOAD
                                   },
                                   priority);
        }
    }

controller.queueAction の呼び出しによって、Thumbnail は ThumbnailController キューに格納されます。Thumbnail が表示可能な場合、priority フラグは true に設定されます。

ThumbnailController にキューが用意され、一連のローダーによって、キューが処理されます。LoadSpec は、Thumbnail とアクション (読み込みまたは読み込み解除) への参照が保持されるコンテナです。ThumbnailLoader はさらに興味深い内容で、次に詳しく説明します。LoadSpec と ThumbnailLoader はどちらも、ThumbnailController.fx に定義されています。

ソースコード
    var queue: LoadSpec[];
    var loaders: ThumbnailLoader[];

queueAction() 関数が、キューの管理を処理しています。該当するコードにはコメントが多く添えられていますが、最終的には、LoadSpec がキューから削除されるか、キューに追加されるという結果になります。キューを処理するメインループは、serveQueue() という名前です。

ソースコード
    function serveQueue() {
        while (sizeof queue > 0 and sizeof loaders < 7) {
            var spec = queue[0];
            delete queue[0];

            var loader = ThumbnailLoader {
                thumbController: this
                thumbnail: spec.thumbnail
            }
            insert loader into loaders;
            loader.load(spec.action);
        }
    }
}

一度に実行できる loader は、最大 7 つまでであることに注意してください。つまり、最大で 7 つの画像を一度に読み込めるということです。これにより、画像がすべてバックグラウンドで読み込まれるために UE が応答できなくなるような Image を頻繁に呼び出すループを防ぎます。serveQueue() 関数は、キューの先頭の参照先を保存してからキューの最初の要素を削除するという方法で、キューの先頭の登録を通常の方法で解除します。新しい ThumbnailLoader が作成されます。load() 関数から serveQueue() が呼び出される可能性があるため、thumbController の後方参照が必要になります。最後に、loader が loaders シーケンスに挿入され、loader の load 関数が呼び出されます。

ソースコード
    function load(action: Integer) {
        if (thumbnail.loadStatus != action) {
            if (action == Constants.THUMB_LOAD) {
                loading = true;
            }
            thumbnail.loadStatus = action;
        }
        if (not loading) {
            delete this from loaders;
        }
    }

Thumbnail.loadStatus は最終的に、ThumbnailLoader.load に設定されます。これにより、action の値に応じて、サムネイル画像が読み込まれるか、プレースホルダ画像に戻されます。Thumbnail の loadStatus をトリガーとして、画像の読み込みが解除された場合、loader は一連の loaders から削除されます。画像の読み込み解除は、Thumbnail の画像を静的なプレースホルダ画像に置き換えるだけで、特にオーバーヘッドは生じないことを覚えておいてください。

しかし、Thumbnail の画像が読み込まれると、画像が読み込まれるまで ThumbnailLoader はキューの中に残り続けます。そのため、この処理 (同時に 7 画像の読み込みが可能です) を実行している間は、他の画像の読み込みを防ぎます。ThumbnailLoader は Thumbnail の画像読み込みの進行状況を監視しています。進行状況が 100% に達したら、ThumbnailLoader はキューから削除され、別の画像の読み込みが可能になります。

ソースコード
    var progress = bind thumbnail.image.progress on replace {
        if (loading and progress == 100) {
            delete this from loaders;
            thumbController.serveQueue();
        }
    }

このコードはすべて同じスレッドで実行されるため、serveQueue()を呼び出して、キューを起動することが必要になります。

試してみましょう

  • action 内の要求チェーンを参照するため、WebSearch.fx を変更して、onDone 関数を次のように変更します。
ソースコード
    onDone: function() {
        var next = httpRequest[loc].context as HttpRequest;
        println("{next}");
        next.enqueue();
    }

 

English
日本語
한국어
简体中文
Português do Brasil
русский