【cocos2d-x v3.1】cocos2dxでzipファイルを扱う場合のまとめ。load timeの計測もあるよ。

# ipaファイルサイズ、apkファイルサイズについて
ご存知の方も多いかと思うのですが、
非wifi環境ではiOSアプリのipaファイルサイズは
**100MB**まで、
Androidアプリのapkファイルサイズは
**50MB**までとなっています。

Androidはapkとは別に拡張ファイルという物を登録すると最大4GBまでいけます。
アプリをマーケットに登録する際にファイル追加の手順が必要だったり、
拡張ファイルを更新する時にアプリバージョンも更新しないとならないため、
ちょっと使いにくいです

そのため、大体どのアプリもマーケット上では50MB以下に抑えておいて、
インストールして、起動した時に必要なファイルを追加でDLするような
仕組みを持っているのではないでしょうか。
今回はその時に有用(かもしれない)zipファイル周りのTipsを投稿します。

**ソースコードはcocos2d-x v3.1.1で記載していますが、3.0でも動作するのは確認済みです。**

必要な所だけの抜粋で、多少省略していることをお許し下さい。

# zipファイルを通信で取得後、保存

```cpp:HelloWorldScene.h

#include "cocos2d.h"
#import "network/HttpClient.h"

USING_NS_CC;
using namespace cocos2d::network;

class HelloWorld : public cocos2d::Layer
{
public:
    // there's no 'id' in cpp, so we recommend returning the class instance pointer
    static cocos2d::Scene* createScene();

    // Here's a difference. Method 'init' in cocos2d-x returns bool, instead of returning 'id' in cocos2d-iphone
    virtual bool init();  
    
    // a selector callback
    void menuCloseCallback(cocos2d::Ref* pSender);
    
    // implement the "static create()" method manually
    CREATE_FUNC(HelloWorld);
    
    void httpCallBackResponse(HttpClient* client , HttpResponse* response);
};

```

```cpp:HelloWorldScene.cpp

#include "HelloWorldScene.h"
#import "external/unzip/unzip.h"

Scene* HelloWorld::createScene(){ // 略 }

// on "init" you need to initialize your instance
bool HelloWorld::init()
{
    // 略
    
    auto request = new HttpRequest();
    request->setUrl("http://*****.***.amazonaws.com/img_100.zip");
    request->setRequestType(cocos2d::network::HttpRequest::Type::GET);
    // cocos v3.0 はこう
    //request->setResponseCallback(this, httpresponse_selector(HelloWorld::httpCallBackResponse));
    
    // cocos v3.1 はこう
    request->setResponseCallback(CC_CALLBACK_2(HelloWorld::httpCallBackResponse ,this));
    network::HttpClient::getInstance()->send(request);
    
    return true;
}

void HelloWorld::httpCallBackResponse(HttpClient* client , HttpResponse* response)
{
    if (response->isSucceed()) {
        std::vector<char>* buffer = response->getResponseData();
        
        CCLOG("buffet size :: %lu" , buffer->size());
        
        std::string filePath = FileUtils::getInstance()->getWritablePath();
        std::string zipFile = "img_100.zip";
        std::string path = filePath.c_str() + zipFile;
        
        // zipファイルの保存
        FILE* fp = fopen(path.c_str() , "wb");
        CCLOG("debug writtern :: %zu" , fwrite(buffer->data() , 1, buffer->size() , fp));
        fclose(fp);
    }
}    


void HelloWorld::menuCloseCallback(Ref* pSender){ // 略 }

```

以上のコードで保存が可能です。


# zipファイルの中にあるファイル名一覧を取り出す
これも以下のようなコードで可能です。

```cpp:HelloWorldScene.cpp

#import "external/unzip/unzip.h"

void readInZipFileName()
{
        std::string filePath = FileUtils::getInstance()->getWritablePath();
        std::string zipFile = "img_100.zip";
        std::string path = filePath.c_str() + zipFile;
        
        // zipファイルをopenし、ファイル数を取得します。
        unzFile zipfile = unzOpen(path.c_str());

        unz_global_info global_info;
        if ( unzGetGlobalInfo( zipfile, &global_info ) != UNZ_OK )
        {
            printf( "could not read file global infon" );
            unzClose( zipfile );
            return;
        }
        
        // ファイルの先頭にカーソルを合わせます
        unzGoToFirstFile(zipfile);
        uLong i;
        for ( i = 0; i < global_info.number_entry; ++i )
        {
            // Get info about current file.
            char filename[ 100 ];
            if ( unzGetCurrentFileInfo64(zipfile, 
                                         NULL, 
                                         filename, 
                                         100, 
                                         NULL, 0, NULL, 0)

                != UNZ_OK )
            {
                CCLOG( "could not read file" );
                unzClose( zipfile );
                return;
            }
            std::string str(filename);
            
            CCLOG("file[%lu] name == %s" , i , str.c_str());
            // ここでstd::vectorにでも詰めればOK

            // 次にカーソルを進める
            unzGoToNextFile(zipfile);
        }
        // 終わったらcloseを忘れずに。
        unzClose(zipfile);
}

```

# zipファイルの中にある画像をSpriteで表示する
これはお馴染みですね。まとめって事で記載しておきます。

```cpp:HelloWorld.cpp
        std::string filePath = FileUtils::getInstance()->getWritablePath();
        std::string zipFile = "img_100.zip";
        std::string imgFile = "img_100/f001.png"
        std::string path = filePath.c_str() + zipFile;
        
        // zip fileから指定した画像を読み出して表示
        unsigned long size = 0;
        unsigned char* buf = FileUtils::getInstance()->getFileDataFromZip(
                                                     path.c_str() , 
                                                     zipFile.c_str(), 
                                                     (ssize_t*)&size);
        
        auto img = new Image();
        img->initWithImageData(buf , size);
        
        auto texture = new Texture2D();
        texture->initWithImage(img);
        
        auto sprite = Sprite::createWithTexture(texture);
        
        this->addChild(sprite);

```

# 気になるread time。気になるよね？

これこれ、一際怪しい光を放つこれ。

```cpp:HelloWorld.cpp

unsigned char* buf = FileUtils::getInstance()->getFileDataFromZip(
                                                     path.c_str() , 
                                                     zipFile.c_str(), 
                                                     (ssize_t*)&size);

```

これ気になりますよねぇ。zipファイルから読み出すって単純に考えて遅いでしょう。
で、どのぐらい遅いのか、計測してみました。

* 画像を100枚用意。(全て別画像、一枚辺り、600x800 600kb前後)
* zipファイルに圧縮後のサイズ：47.3MB
* zipファイルに圧縮しないと：58MB
* 100枚読み出してaddChildするまでの時間を比較します。
* 一度読みだすとTextureCacheに載るため、二回目以降は高速になります。
* そのため、念を入れて片方ずつ計測します。

ソースはこんな感じです。

```cpp:HelloWorldScene.cpp

// 片方ずつ行います。(コメントアウトで簡単に)
void exec(){
    // resourceから読み出す
    const auto resourceStartTime = std::chrono::system_clock::now();
    for(int i = 1 ; i < 101 ; i++){
        sprintf(str, "f%03d.png" , i);
        readFromResource(str);
    }
    const auto resourceEndTime = std::chrono::system_clock::now();
    const auto resourceExecTime = resourceEndTime - resourceStartTime;

    CCLOG("resourceExecTime : %lld ms" , std::chrono::duration_cast<std::chrono::milliseconds>(resourceExecTime).count());
    
    // zipから読み出す
    const auto zipStartTime = std::chrono::system_clock::now();
    for(int i = 1 ; i < 101 ; i++){
        sprintf(str, "f%03d.png" , i);
        readFromZip(str);
    }
    const auto zipEndTime = std::chrono::system_clock::now();
    const auto zipExecTime = zipEndTime - zipStartTime;
    
    CCLOG("zipExecTime : %lld ms" , std::chrono::duration_cast<std::chrono::milliseconds>(zipExecTime).count());
}

void HelloWorld::readFromResource(const char* _fileName){
    auto sprite = Sprite::create(_fileName);
    this->addChild(sprite);
}

void HelloWorld::readFromZip(const char* _fileName){
    
    std::string fileName = "img_100/";
    fileName = fileName + _fileName;
    // zip fileから指定した画像を読み出して表示
    unsigned long size = 0;
    unsigned char* buf = FileUtils::getInstance()->getFileDataFromZip(path.c_str() , fileName.c_str(), (ssize_t*)&size);
    
    auto img = new Image();
    img->initWithImageData(buf , size);
    
    auto texture = new Texture2D();
    texture->initWithImage(img);
    
    auto sprite = Sprite::createWithTexture(texture);
    
    this->addChild(sprite);
}

```

# 結果

**cocos2d: resourceExecTime : 4592 ms**

**cocos2d: zipExecTime : 4757 ms**



という結果になりました。
当初の予想通りzipの方が遅い！って事がわかりましたが、
思ったよりもそこまで差が付きませんでした。
一度読んでしまえば、textureCacheに載せる事が出来るので、
全く使えないって事は無さそうです。

# その他
zipについて色々とまとめてみましたが、
実運用上では、
* バージョン管理
* 差分更新
* Androidの挙動問題(端末やOSverによってはApplication.mkにunzip.h入れないとダメかも。未確認)
などなど、まだまだ考える事がありそうです。

あ、削除。remove(writablePath + "xxx.zip")で出来るよ。
あ、zipから読みだした後にtextureCacheに明示的に入れないとダメかも？
あ、zipからファイル読み出す時に、NULLになっちゃうんだけど…って人は、external/unzip/unzip.h 1300行目辺りint ZEXPORT unzLocateFile()の辺りにブレークポイント貼ると分かりやすいよ。

とまだまだネタはあるんですが、とりあえずここまで！