cocos2d-x

効率的なzipファイルのダウンロードと展開

More than 1 year has passed since last update.

はじめまして。
文章力を付ける為、Qiitaを始めてみる事にしました。
宜しくお願い致します。

Zipファイルのダウンロードと展開

ある程度、規模の大きなアプリケーションになってくると、
バンドルに含みきれないファイルを
おそらくzip形式にしてダウンロードする必要が出てきます。

この時にやってしまうと勿体無い事が、
ダウンロードしたzipファイルデータを、一度HDDへ保存してしまう事
です。

一般的にHDDへの読み書きはメモリへのそれよりも圧倒的に遅いので、
メモリ上にあるzipファイルを直接展開する方が
速度的に有利です。

cocos2d では、cocos2d::ZipFile::createWithBuffer
というメソッドが用意されているので、これを活用します。

と、その前に必要なzipファイルをダウンロードします。

GETリクエスト発行
void Class::downloadZip(const std::string& url){
    network::HttpRequest* req = new network::HttpRequest();
    req->setRequestType( network::HttpRequest::Type::GET );
    req->setUrl( url.c_str() );
    req->setResponseCallback( CC_CALLBACK_2(Class::httpRequestCallback, this) );

    network::HttpClient::getInstance()->send( req );
}
GETリクエストのレスポンス・コールバック
void Class::httpRequestCallback(network::HttpClient* client, network::HttpResponse* response)
{
    if( response->getResponseCode() == 200 ){
        // ファイルへ保存せずに、そのまま展開する
        unzip(&response->getResponseData()->at(0), response->getResponseData()->size());
    }else{
        CCLOG("%s", response->getHttpRequest()->getUrl());
        CC_ASSERT(0);
    }
}

cocos2d::ZipFile::createWithBufferを使ってzipを展開します。

Zip展開処理
void Class::unzip(const void* data, ssize_t datasize){

    // 出力先のルートパスを取得
    const std::string writablePath( cocos2d::FileUtils::getInstance()->getWritablePath() );

    // zipに含まれるファイル情報リストを取得
    cocos2d::ZipFile* zipfile = cocos2d::ZipFile::createWithBuffer( data, datasize );
    for( std::string filename = zipfile->getFirstFilename(); !filename.empty(); filename = zipfile->getNextFilename() ){
        if( *filename.rbegin() == '/' ){

            // It's a directory.
            cocos2d::FileUtils::getInstance()->createDirectory( writablePath + filename );

        }else{

            // It's a file.
            ssize_t filesize;
            unsigned char* filedata = zipfile->getFileData( filename, &filesize );
            {
                const std::string fullPath( writablePath + filename );
                FILE* file = fopen( fullPath.c_str(), "wb" );
                fwrite( filedata, filesize, 1, file );
                fclose( file );
            }
            free( filedata );

        }
    }
    delete zipfile;
}    

cocos2d::ZipFile::createWithBufferとはまた便利ですね。
zipに含まれるファイルの数だけzipfile->getFileDataの際に
文字列検索が発生してしまう事が残念ですが。

ただ、効率化の面で言えばまだ全然足らなくて、
このまま使ってしまうとローディング画面とかで
UIがカチカチ固まってしまう現象が起きやすいです。
※zipの展開処理でUIスレッドが止められてしまう為

zipを展開する処理を別スレッドへ逃す必要があるのですが、
いずれまた。

UIスレッドを止まらないようにする

※ "cocos zip"で検索すると上位に現れるようになっていたので、
cocos自体寂しくなってきましたが追記することにしました

時が経ちましたので、C++11でダウンロード処理を書き直してみます。

#include "network/HttpClient.h"
using onFinishedDownloadZip = std::function<void(bool succeeded)>;
static void downloadZip(const std::string& url, const std::string& outdir, onFinishedDownloadZip onFinished){
    auto req = new (std::nothrow) cocos2d::network::HttpRequest();
    req->setRequestType( cocos2d::network::HttpRequest::Type::GET );
    req->setUrl( url );
    req->setResponseCallback([onFinished, outdir](cocos2d::network::HttpClient* client, cocos2d::network::HttpResponse* response){
        if( response->isSucceed() ){
            // ダウンロードしたzipファイルを展開スレッドへ送る
            pushToUnzip(onFinished, outdir, response);
        }else{
            onFinished(false);
        }
    });
    cocos2d::network::HttpClient::getInstance()->send( req );
    req->release();
}

Zipの展開処理を別スレッドで行います。

非同期タスクを定義する際、cocosにAsyncTaskPoolというクラスが定義されているので
これを使ってみます。

static void pushToUnzip(onFinishedDownloadZip onFinished, const std::string& outdir, cocos2d::network::HttpResponse* response){
    // 別スレッドで実行されるタスク
    auto task = [onFinished, outdir, response](){

        // zipに含まれるファイル情報リストを取得
        cocos2d::ZipFile* zipfile = cocos2d::ZipFile::createWithBuffer(response->getResponseData()->data(),
                                                                       response->getResponseData()->size());
        for( std::string filename = zipfile->getFirstFilename(); !filename.empty(); filename = zipfile->getNextFilename() ){
            if( *filename.rbegin() == '/' ){

                // It's a directory.
                cocos2d::FileUtils::getInstance()->createDirectory( outdir + filename );

            }else{

                // It's a file.
                ssize_t filesize;
                unsigned char* filedata = zipfile->getFileData( filename, &filesize );
                // ファイルデータが取得できたので、書き込みを行うスレッドへ送る
                pushToWriteFile(nullptr, outdir + filename, filedata, filesize);

            }
        }
        delete zipfile;

        // 終了判定用に空データを送る
        pushToWriteFile(onFinished, "", nullptr, 0);
    };
    // 最後にUIスレッドで実行されるタスク
    auto finished = [response](void*){
        response->release();
    };
    // unzipタスク実行中に破棄されないよう保護する
    response->retain();
    // 非同期タスクの開始 (TASK_OTHERのスレッドキューへ積まれる)
    cocos2d::AsyncTaskPool::getInstance()->enqueue(cocos2d::AsyncTaskPool::TaskType::TASK_OTHER, finished, nullptr, task);
}

AsyncTaskPoolは、各タスクをキューとして扱うので登録された順に実行されます。
登録されたタスクは別スレッドで実行され、完了後にUIスレッドで終了コールバックが呼ばれます。

AsyncTaskPoolはいくつかスレッドを管理していますが、
cocos2d::AsyncTaskPool::TaskTypeで登録されるスレッドを指定します。

cocos2d::ZipFileでは、保持しているファイルデータを一つずつ取得していきますが、
ファイルデータの取得(getFileData)取得したデータの書き込み(writeFile)
関連するハードが異なるので非同期で行うほうが効率が良さそうです。

static void pushToWriteFile(onFinishedDownloadZip onFinished, const std::string& path, unsigned char* data, ssize_t size){
    CCLOG("pushToWriteFile: %s", path.c_str());
    // 別スレッドで実行されるタスク
    auto task = [path, data, size](){
        if( data ){
            FILE* file = fopen( path.c_str(), "wb" );
            fwrite( data, size, 1, file );
            fclose( file );
            CCLOG("endOfWriteFile: %s", path.c_str());

            // zipfile->getFileData で確保されたメモリを開放する
            free( data );
        }
    };
    // 最後にUIスレッドで実行されるタスク
    auto finished = [onFinished](void*){
        if(onFinished){ onFinished(true); }
    };
    // 非同期タスクの開始 (TASK_IOのスレッドキューへ積まれる)
    cocos2d::AsyncTaskPool::getInstance()->enqueue(cocos2d::AsyncTaskPool::TaskType::TASK_IO, finished, nullptr, task);
}

こんな感じで使えるかと思います。

exsample
downloadZip("http://127.0.0.1/test.zip",
            cocos2d::FileUtils::getInstance()->getWritablePath(),
            [](bool successed){
                CCLOG("*result %d", successed);
            });