Edited at

効率的な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);
});