Help us understand the problem. What is going on with this article?

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

More than 3 years have 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);
            });
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした