2013年

10月

21日

Android NDKでファイル読み込みをやりたかっただけ

Javaで開発されたAndroidアプリは「Dalvik」というAndroid OSに搭載されてる仮想マシンを経て解析され、実行します。

 

対してAndroid NDKはこの仮想マシンを通らず、ハードウェア上で直接実行できるコードを組むことが出来ます。加えてネイティブコードはC/C++言語、Java言語だけで開発されたアプリよりも断然速いです。

 

私は以前、テクスチャ・ポリゴンの描画機能だけをネイティブ化していました。しかし折角高速化出来るのであれば、描画部分だけでなく処理に時間の掛かる他のコードもネイティブ化したいところです。ということで思いついたのが今回のファイル読み込みです。

 

以前MMDモデルをAndroidで読み込んで描画するプログラムをJavaだけで作ったことがあるのですが、1ファイルに付きデバッグで1分以上、リリースで5秒も読み込みに掛かりました。この処理をネイティブ化すればかなりの処理速度向上が期待できそうです。

 

今回はNDKからファイルを読み込む方法を幾つか紹介していこうと思います。開発環境は私の実機端末に合わせてAndroid SDK 2.2でやっていきます。

ネイティブでローカルフォルダ・SDカード内のファイルを読み込む

Android NDKはC/C++言語なので、fopen、fread、fgets等のお馴染みのC標準関数が使えます。なのでSDカード・ローカルフォルダ下にあるファイルは簡単に読み込めます。

 

SDカードのフォルダパスは端末によって変わるらしいですが、AndroidでSDカードのディレクトリパスが取得出来るようになっています。

 

実際にやってみましょう。今回はSDカード下にあるファイルを読み込んで中の文字列を画面に描画するプログラムを作ります。まずはSDカード下に適当なファイルを作成します。

 

まずはJava側のソースコード。

NDKライブラリをJavaから呼び出せるよう準備をします。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package com.androidproject;

public class NativeCall {

    // NDLライブラリの読み込み
    static {
        
        // NDKライブラリ名を指定
        System.loadLibrary("AndroidNativeProject");
    }
    
    /**
     * ファイル読み込み
     * @return
     */
    public static native String loadFile();
}

今回はネイティブコードを呼び出すためだけのクラスを用意しました。

System.loadLibraryにはAndroid.mkで設定したモジュール名を指定します。

 

アクセス修飾子(+static)の後にnativeをつけた関数がNDK側で実装する関数になります。

 

次にネイティブコードでloadFileメソッドの中身を実装していきます。

NDK側での関数名は「Java_ +パッケージ名を含んだJavaクラス名 + nativeを付けた関数名」になります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include <jni.h>
#include <stdio.h>
#include <string.h>
#include "NativeCall.hpp"

/**
 * ファイルサイズの取得
 */
fpos_t fgetsize(FILE* stream) {

    // 現在のファイル位置を取得
    fpos_t current = 0;
    fgetpos(stream, &current);

    // ファイルの終端へ移動
    fseek(stream, 0, SEEK_END);

    // シーク位置を取得
    fpos_t size = 0;
    fgetpos(stream, &size);

    // ファイルの元の位置へ戻る
    fseek(stream, current, SEEK_SET);

    return size;
}

/**
 * ファイル読み込み
 */
JNIEXPORT jstring JNICALL Java_com_androidproject_NativeCall_loadFile
  (JNIEnv *env, jclass kc, jstring filename) {

    jstring result = NULL;

    // ファイルパス取得
    jboolean copy = false;
    char path[256] = {0};
    strcpy(path, env->GetStringUTFChars(filename, &copy));

    // ファイルオープン
    FILE* fp = fopen(path, "r");

    if(fp != NULL)
    {
        // ファイルサイズの取得
        fpos_t size = fgetsize(fp);

        // データを保存するバッファを用意
        char* buf = new char[size + 1];
        memset(buf, 0, size + 1);

        // ファイルデータを全て読み込む
        fread(buf, size, 1, fp);

        // ファイルを閉じる
        fclose(fp);

        // jstring変換
        result = env->NewStringUTF(buf);

        // バッファ削除
        delete[] buf;
    }
    else
        result = env->NewStringUTF("ファイル読み込みに失敗");

    return     result;
}

fseekを使ってファイル容量を取得し、全てのファイルデータをバイト配列で受け取ってJava文字列に変換しています。最後に読み取ったデータをViewクラスで表示してみましょう。

 

SDカードから文字列を読み取り表示する自作ビュークラスのコードです。SDカードのファイル情報はEnvironment.getExternalStorageDirectoryメソッドで取得できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package com.androidproject;

import java.io.File;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Environment;
import android.view.View;

public class MyView extends View {

    Paint paint;
    String sdcard_data = "";
    
    /**
     * コンストラクタ
     * @param context
     */
    public MyView(Context context){
        super(context);
        
        // 背景色
        setBackgroundColor(Color.argb(255, 0,  128, 255));
        
        // ペイントクラス作成
        paint = new Paint();
        
        // 文字サイズを30に
        paint.setTextSize(30);
        
        // 文字色を白に
        paint.setColor(Color.WHITE);
        
        // アンチエイリアスを有効に
        paint.setAntiAlias(true);

        // SDカードディレクトリの取得
        File sd_card = Environment.getExternalStorageDirectory();
        String load_file = "テストファイル";
        
        // SDカード内のファイル探索
        for (File file : sd_card.listFiles()) {
            
            if(file.isFile()) {
                String path = file.getAbsolutePath();
                
                if(path.indexOf(load_file) >= 0){
                    
                    // SDカードからファイル読み込み
                    sdcard_data = NativeCall.loadFile(path);
                    break;
                }
            }
            
        } 
        
        
    }
    
    @Override 
    protected void onDraw(Canvas canvas) {
        
        // 文字列描画
        canvas.drawText(sdcard_data,48, 48, paint);
    }
}

 

端末のローカルフォルダにあるファイルも同じ方法で読み取ることが出来ます。

次はアプリ下のAssetsフォルダに設置したファイルをネイティブで読み取ってみましょう。

ネイティブでAssetsフォルダのファイルを読み込む AssetManager編

Android OS 2.3以降から、NDKでAssetManagerを使えるようサポートされました。

この機能は android ndkのplatformsフォルダにある「android/asset_manager.h」と「android /asset_manager_jni.h」をインクルードし、ライブラリ「libandroid」を追加すれば使用出来るようになります。

 

私 はまだSDK2.3以降のアプリを開発したことが無いので詳細な使い方は知りませんが、android ndkのサンプルに「native-audio」という「ネイティブからassetsフォルダのオーディオファイルを読み込む」プロジェクトがあるので ソースを覗いてみてください。

 

使い方は大体こんな感じです。

 

①Java側からNDKへAssetManagerオブジェクトを送り込む

AssetManager assets = context.getAssets();

NativeCall.loadFileFromAssets(assets, "assetsフォルダ下の読み込みたいファイル名");

 

②Javaから送られたassetsオブジェクトをネイティブ用のAAssetManagerに変換する
Java_com_androidproject_NativeCall_loadFileFromAssets(JNIEnv* env, jclass clazz, jobject assetManager, jstring filename) {

 

// 変換関数 asset_manager_jni.hで定義

AAssetManager* mgr = AAssetManager_fromJava(env, assetManager);

 

③変換したAAssetManagerを開く

jboolean copy = false;

const char *utf8 = env->GetStringUTFChars(filename, &copy);

AAsset* asset = AAssetManager_open(mgr, utf8, AASSET_MODE_UNKNOWN);

 

こんな感じです。あとはAAsset_read関数でデータを読み取っていきます。使い方はfreadと同じです。他にもAssetの操作に関する関数が用意されてるので簡単にデータを読み込めると思います。

 

最後に、AAssetManager_openを使用したら必ずAAsset_closeで閉じましょう。

 

参考元一覧:

Tips/NDKでassets下のファイルを直接弄る方法

ネイティブでAssetsフォルダのファイルを読み込む バイト配列編

Andoird OS2.2以下ではAssetManagerがネイティブでサポートされておらず、Java側からしか操作することが出来ません。

 

ではどうやってファイル読み込みするかというと、話は簡単。Java側で全てのファイルデータをバイト配列に格納し、それをNDK側へ送り込んでデータを読み取らせます。

 

①Java側でAssetsフォルダ下のファイルのストリームデータを取得する

AssetManager assets = context.getAssets();

InputStream stream = assets.open("assetsフォルダ下の読み込みたいファイル名");

 

②ファイルデータをバイト配列へ格納する

int size = stream.available(); // ファイル容量取得

byte[] buffer = new byte[size];
stream.read(buffer); // ファイルデータを全て読み取る

stream.close();

 

このバイト配列をネイティブコードへ渡してデータを読み取っていきます。

 

バイト配列から値を読み込むには以下のような自作クラスを使います。

読み取り自体は非常に簡単です。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
#include <string.h>
#include <jni.h>

class BinaryReader {
    unsigned int offset, size;
    jbyte* buffer;

public:
    /**
     * コンストラクタ
     */
    BinaryReader(JNIEnv *env, const jbyteArray *jarray) {

        // 初期化
        offset = size = 0;
        buffer = 0;

        // バイト配列の先頭ポインタ取得
        jboolean copy = false;
        jbyte* pbuf = env->GetByteArrayElements(*jarray, &copy);

        // バッファ領域確保
        size = env->GetArrayLength(*jarray);
        buffer = new jbyte[size];

        // データコピー
        memcpy(buffer, pbuf, size);

        // 開放処理
        env->ReleaseByteArrayElements(*jarray, pbuf, 0);
    }

    /**
     * コピーコンストラクタ
     */
    BinaryReader(const BinaryReader &read) {

        // データコピー
        this->offset = read.offset;
        this->size = read.size;

        // 新たに領域確保
        this->buffer = new jbyte[this->size];

        // バッファコピー
        memcpy(this->buffer, read.buffer, this->size);
    }

    /**
     * デストラクタ
     */
    ~BinaryReader() {
        Close();
    }

    /**
     * 閉じる
     */
    void Close() {

        // バッファ開放
        delete[] buffer;
        buffer = 0;

        // 初期化
        offset = size = 0;
    }

    /**
     * 現在のストリーム位置取得
     */
    unsigned int GetPosition() {
        return offset;
    }

    /**
     * バッファサイズ取得
     */
    unsigned int GetSize() {
        return size;
    }

    /**
     * ストリーム位置の移動
     */
    void Seek(unsigned int position) {
        if(size < position)
            position = size;

        offset = position;
    }

    /**
     * データ読み込み(テンプレート)
     */
    template <typename T> T Read() {

        // 初期化
        T value = 0;

        if(offset < size) {

            // 現在位置のデータポインタを指定型のポインタに変換
            T *p = (T*)&buffer[offset];

            // 実数値を取得
            value = (*p);

            // 型サイズ分だけ移動
            offset += sizeof(T);
        }

        return value;
    }

    /**
     * データ配列読み込み(テンプレート)
     */
    template <typename T> void ReadArray(T* _buffer, int _size) {

        for(int i = 0; i < _size && offset < size; i++) {
            T *p = (T*)&buffer[offset];
            _buffer[i] = (*p);

            offset += sizeof(T);
        }
    }

    /**
     * バイト読み込み
     */
    jbyte ReadByte() {
        return Read<jbyte>();
    }

    /**
     * バイト配列読み込み
     */
    void ReadBytes(jbyte* _buffer, int _size) {
        ReadArray<jbyte>(_buffer, _size);
    }

    /**
     * Short読み込み
     */
    short ReadShort() {
        return Read<short>();
    }

    /**
     * Short配列読み込み
     */
    void ReadShorts(short* _buffer, int _size) {
        ReadArray<short>(_buffer, _size);
    }

    /**
     * Int読み込み
     */
    int ReadInt() {
        return Read<int>();
    }

    /**
     * Int配列読み込み
     */
    void ReadInts(int* _buffer, int _size) {
        ReadArray<int>(_buffer, _size);
    }

    /**
     * Long読み込み
     */
    long ReadLong() {
        return Read<long>();
    }

    /**
     * Long配列読み込み
     */
    void ReadLongs(long* _buffer, int _size) {
        ReadArray<long>(_buffer, _size);
    }

    /**
     * Float読み込み
     */
    float ReadFloat() {
        return Read<float>();
    }

    /**
     * Float配列読み込み
     */
    void ReadFloats(float* _buffer, int _size) {
        ReadArray<float>(_buffer, _size);
    }

    /**
     * Double読み込み
     */
    double ReadDouble() {
        return Read<double>();
    }

    /**
     * DOuble配列読み込み
     */
    void ReadDoubles(double* _buffer, int _size) {
        ReadArray<double>(_buffer, _size);
    }
};

ネイティブでAssetsフォルダのファイルを読み込む ZLib編

Androidアプリの実行形式「.apk」ファイルはzip形式で圧縮されているので、zlibで中身を解析することが出来ます。この処理方法だとAssetManagerを使わずにファイルを読み取れ、assetsフォルダ以外のフォルダにある任意のファイルも動的に読み取れます。

 

この手法ではzlibに搭載されている「minizip」を使います。zlib自体はAndroid NDKに標準装備されていますが、minizipが無いのでzlibの公式から最新版のソースコードをダウンロードしましょう。2013/10/21現在で最新版はzlib1.2.8です。

 

zlib Home Site ← コチラからダウンロード

 

解凍をしたら、「contrib\minizip」フォルダにある「unzip.h」、「unzip.c」、「ioapi.h」、「ioapi.c」の4つのファイルを自分のAndroidプロジェクトのjniフォルダにコピーします。

 

このままだとエラーが出るのでソースコードに一部追記をします。ioapi.hに「USE_FILE32API」のマクロ宣言と、ioapi.cに「size_t型」の型宣言を追加します。

 

あと忘れずに、Android.mkにminizipのソースファイルとandroid ndkに標準搭載されてるzlibライブラリを追加しましょう。

 

では実際に使っていきましょう。

まずassetsフォルダにこのようなテスト用のファイルを仕込みました。

 

Java側のソースコード。

起動している自分自身のアプリケーションパスを取得します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package com.androidproject;

import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.View;

public class MyView extends View {

    Paint paint;
    String assets_data = "";

    /**
     * コンストラクタ
     * @param context
     */
    public MyView(Context context){
        super(context);

        // 背景色
        setBackgroundColor(Color.argb(255, 0,  128, 255));

        // ペイントクラス作成
        paint = new Paint();

        // 文字サイズを30に
        paint.setTextSize(30);

        // 文字色を白に
        paint.setColor(Color.WHITE);

        // アンチエイリアスを有効に
        paint.setAntiAlias(true);

        // パッケージマネージャーを取得
        PackageManager manager = context.getPackageManager();
        ApplicationInfo info = null;
        String apk_path = null;

        try {

            // APKファイルのパスを取得
            info = manager.getApplicationInfo(
                    "com.androidproject",
                    PackageManager.GET_META_DATA);

            apk_path = info.sourceDir;

        } catch(NameNotFoundException ex) {}

        // ネイティブからファイル読み込み
        if(apk_path != null)
            assets_data = NativeCall.loadAssetsFile(apk_path, "data.txt");
    }

    @Override 
    protected void onDraw(Canvas canvas) {

        // 文字列描画
        canvas.drawText(assets_data, 48, 48, paint);
    }
}

ContextからPackageManagerを取得し、そこからアプリケーション情報を取得します。PackageManager.getApplicationInfoの第一引数にはプロジェクトのパッケージ名を指定し、第二引数にはPackageManagerのフラグを渡します。

 

PackageManager.GET_META_DETA

・PackageManager.GET_SHARED_LIBRARY_FILES

PackageManager. GET_UNINSTALLED_PACKAGES

このいずれかを指定します。

 

リファレンス

public abstract ApplicationInfo getApplicationInfo (String packageName, int flags)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package com.androidproject;

public class NativeCall {

    // NDLライブラリの読み込み
    static {
        
        // NDKライブラリ名を指定
        System.loadLibrary("AndroidNativeProject");
    }
    
    /**
     * Assets読み込み
     * @param apkfilename
     * @param datafilename
     * @return
     */
    public static native String loadAssetsFile(
            String apkfilename,
            String datafilename);
}

 

次はNDK側。

まず「unzOpen」メソッドにapkファイル名を指定してapkファイルを開きます。

unzFile fp = unzOpen(apkpath);

 

読み込みに成功すると、unzFile(voidポインタ)が返ってきます。

失敗するとNULLが渡されます。

 

次に「unzGetCurrentFileInfo」メソッドでapkに含まれるファイル情報を取得します。

処理に成功するとUNZ_OK(0)を返します。

unz_file_info info;

char currentfile[256] = {0}; // ファイルパスを格納するバッファ
int hr = unzGetCurrentFileInfo(fp, &info, currentfile, 256, NULL, 0, NULL, 0);

次のファイル情報へ移動するには「unzGoToNextFile」メソッドを呼び出します。

処理に成功するとUNZ_OK(0)を返します。

hr = unzGoToNextFile(fp);


お目当てのファイルが見つかったら「unzOpenCurrentFile」メソッドでカレントファイルを開きます。処理に成功するとUNZ_OK(0)を返します。

hr = unzOpenCurrentFile(fp);


カレントファイルからデータを読み取るには「unzReadCurrentFile」メソッドを呼び出します。使い方はfreadやAAsset_readと同じです。

// int型の値を読み取る場合

int value = 0;

hr = unzReadCurrentFile(fp, (void*)&value, sizeof(int));

 

// float型の配列を読み取る場合

float values[3] = {0.0f};

hr = unzReadCurrentFile(fp, (void*)values, sizeof(float) * 3);


unzOpenCurrentFileメソッドを呼び出したら必ず「unzCloseCurrentFile」メソッドでカレントファイルを閉じましょう。処理に成功するとUNZ_OK(0)を返します。

hr = unzCloseCurrentFile(fp);


最後に全ての処理が終わったら「unzClose」メソッドでファイルを閉じましょう。

hr = unzClose(fp);

 


今回は文字列だけなので、ファイル内のデータを全てバイト配列に格納してからJava文字列に変換してJava側にデータを渡します。未圧縮ファイルのサイズ、圧縮時のファイルサイズはunz_file_info構造体から取得できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#include <jni.h>
#include <stdio.h>
#include <string.h>
#include "NativeCall.hpp"
#include "unzip.h"

/**
 * Assetsファイル読み込み
 */
JNIEXPORT jstring JNICALL Java_com_androidproject_NativeCall_loadAssetsFile
  (JNIEnv *env, jclass jc, jstring apkfilename, jstring datafilename) {

    jstring result = NULL;

    // APKファイルパス取得
    jboolean copy = false;
    char apkpath[256] = {0};
    strcpy(apkpath, env->GetStringUTFChars(apkfilename, &copy));

    // データファイルパス取得
    char datapath[256] = {0};
    strcpy(datapath, env->GetStringUTFChars(datafilename, &copy));

    // APKファイルを開く
    unzFile fp = unzOpen(apkpath);

    if(fp != NULL)
    {
        unz_file_info info;
        int hr = UNZ_OK;

        while(UNZ_OK == hr)
        {
            // 現在のファイル情報を取得
            char currentfile[256] = {0};
            hr = unzGetCurrentFileInfo(fp, &info, currentfile, 256, NULL, 0, NULL, 0);

            if(UNZ_OK == hr)
            {
                // 読み込みたいファイルが見つかった場合
                if(strstr(currentfile, datapath) != NULL && strstr(currentfile, "assets") != NULL)
                {
                    // カレントファイルを開く
                    hr = unzOpenCurrentFile(fp);

                    // データを保存するバッファを用意
                    char* buf = new char[info.uncompressed_size + 1];
                    memset(buf, 0, info.uncompressed_size + 1);

                    // ファイルデータを全て読み込む
                    unzReadCurrentFile(fp, buf, info.uncompressed_size);

                    // カレントファイルを閉じる
                    hr = unzCloseCurrentFile(fp);

                    // jstring変換
                    result = env->NewStringUTF(buf);

                    // バッファ削除
                    delete[] buf;

                    // ループ終了
                    break;
                }
            }

            // 次のファイルへうつる
            hr = unzGoToNextFile(fp);
        }

        // APKファイルを閉じる
        unzClose(fp);

        if(UNZ_OK != hr)
            result = env->NewStringUTF("ファイル読み込みに失敗");
    }

    return result;
}

アプリの実行結果

Javaで宣言した自作クラスにファイルデータを格納する

これまでは文字列データばかりを扱ってきましたが、ネイティブコードからJavaクラスを取得したり、作成することも出来ます。

 

以下のサンプルは、ファイルから数値を読み取って自作の3次元ベクトルクラスに値を代入するコードです。ファイル読み込みはzlibの手法をとっています。ファイルにはバイナリ形式で「0.5」、「2」、「36」の数値が入っています。

 

Javaでベクトルクラス宣言

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.androidproject;

public class Vector3 {

    public float X, Y, Z;
    
    /**
     * コンストラクタ
     */
    public Vector3() {
        X = Y = Z = 0;
    }
    
    /**
     * コンストラクタ
     * @param x
     * @param y
     * @param z
     */
    public Vector3(float x, float y, float z) {
        this.X = x;
        this.Y = y;
        this.Z = z;
    }
    
    @Override
    public String toString() {
        return String.format("Vector3(%f, %f, %f)", this.X, this.Y, this.Z);
    }
}

読み込んだベクトルデータを表示

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package com.androidproject;

import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.View;

public class MyView extends View {

    Paint paint;
    String assets_data = "";
    Vector3 vec;
    
    /**
     * コンストラクタ
     * @param context
     */
    public MyView(Context context){
        super(context);
        
        // 背景色
        setBackgroundColor(Color.argb(255, 0,  128, 255));
        
        // ペイントクラス作成
        paint = new Paint();
        
        // 文字サイズを30に
        paint.setTextSize(30);
        
        // 文字色を白に
        paint.setColor(Color.WHITE);
        
        // アンチエイリアスを有効に
        paint.setAntiAlias(true);
        
        // パッケージマネージャーを取得
        PackageManager manager = context.getPackageManager();
        ApplicationInfo info = null;
        String apk_path = null;
        
        try {
            
            // APKファイルのパスを取得
            info = manager.getApplicationInfo("com.androidproject", 0);
            apk_path = info.sourceDir;
            
        } catch(NameNotFoundException ex) {}
        
        if(apk_path != null)
            vec = NativeCall.loadAssetsFile(apk_path, "vector.txt");
    }
    
    @Override 
    protected void onDraw(Canvas canvas) {
        
        // Vector3文字列描画
        if(vec != null)
            canvas.drawText(vec.toString(), 48, 48, paint);
    }
}

ベクトルデータの読み込み

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#include <jni.h>
#include <stdio.h>
#include <string.h>
#include "NativeCall.hpp"
#include "unzip.h"

/**
 * Assetsファイル読み込み
 */
JNIEXPORT jobject JNICALL Java_com_androidproject_NativeCall_loadAssetsFile
  (JNIEnv *env, jclass jc, jstring apkfilename, jstring datafilename) {

    jobject vec_obj = NULL;

    // APKファイルパス取得
    jboolean copy = false;
    char apkpath[256] = {0};
    strcpy(apkpath, env->GetStringUTFChars(apkfilename, &copy));

    // データファイルパス取得
    char datapath[256] = {0};
    strcpy(datapath, env->GetStringUTFChars(datafilename, &copy));

    // APKファイルを開く
    unzFile fp = unzOpen(apkpath);

    if(fp != NULL)
    {
        unz_file_info info;
        int hr = UNZ_OK;

        while(UNZ_OK == hr)
        {
            // 現在のファイル情報を取得
            char currentfile[256] = {0};
            hr = unzGetCurrentFileInfo(
                    fp, &info,
                    currentfile, 256,
                    NULL, 0, NULL, 0);

            if(UNZ_OK == hr)
            {
                // 読み込みたいファイルが見つかった場合
                if(strstr(currentfile, datapath) != NULL &&
                        strstr(currentfile, "assets") != NULL) {

                    // カレントファイルを開く
                    hr = unzOpenCurrentFile(fp);

                    // パッケージ名を含めたクラス名を指定
                    jclass vec_class = env->FindClass(
                            "com.androidproject.Vector3");

                    // Vector3の引数付コンストラクタハンドル取得
                    jmethodID vec_const = env->GetMethodID(
                            vec_class,
                            "<init>",
                            "(FFF)V");

                    // ファイル読み込み
                    float values[3] = {0};
                    unzReadCurrentFile(fp, values, sizeof(float) * 3);

                    // Vector3オブジェクトの作成
                    vec_obj = env->NewObject(
                            vec_class,
                            vec_const,
                            values[0], values[1], values[2]);

                    // カレントファイルを閉じる
                    hr = unzCloseCurrentFile(fp);

                    // ループ終了
                    break;
                }
            }

            // 次のファイルへうつる
            hr = unzGoToNextFile(fp);
        }

        // APKファイルを閉じる
        unzClose(fp);
    }

    // 作成したオブジェクトをJavaへ渡す
    return vec_obj;
}

 

Javaクラスを呼び出すにはJNIEnv::FindClassメソッドを呼び出します。この引数には呼び出したいクラスの名前を渡すのですが、クラス名だけではなくパッケージ名も含める必要があります。成功するとJavaクラスを示すIDが作成されます。

// Vector3クラスのクラスID取得

jclassID jc = env->FindClass("com.androidproject.Vector3");

 

次にJNIEnv::GetMethodIDメソッドでコンストラクタメソッドを用意します。

第一引数:メソッドを所持するJavaクラスのID

第二引数:メソッド名 コンストラクタを呼び出す場合、どのクラスでも<init>になります

第三引数:メソッドの引数と戻り値を表すシグネチャ 引数と戻り値がない場合は"()V"

 

今回はfloat値を3つ渡せるコンストラクタのメソッドIDを取得します。

// コンストラクタメソッドを取得(コンストラクタ名は一律で<init>)
jmethodID jm = env->GetMethodID(jc, "<init>", "(FFF)V");


シグネチャ一覧
シグネチャ
byte B
char C
float F
double D
short S
int I
long J
void V
boolean Z
クラス L + クラス修飾名 + ;
配列 [ + それぞれの型のシグネチャ
メソッド (引数のシグネチャのリスト) + 戻り値のシグネチャ

 

int型配列

[I

 

Stringクラス

Ljava/lang/String;

 

Stringクラス配列

[Ljava/lang/String;

 

第一引数がfloat、戻り値がdoubleのメソッド

(F)D


第一引数がfloat、第2引数がint、戻り値が無いメソッド

(FI)V

 

第一引数がbyte配列、第2引数がString、戻り値が無いメソッド

([BLjava/lang/String;)V

 

クラスのシグネチャはFindClassに渡すクラス名とは値が異なるので注意しましょう。

 

最後にJNIEnv::NewObjectメソッドで実際にクラスオブジェクトを作成します。引数にクラスIDとメソッドIDを渡し、さらにメソッドに渡す引数を指定します。

// X=0.1 Y=0.2 Z=0.3の3次元ベクトルを作成

jobject  vector_obj = env->NewObject(jc, jm, 0.1f, 0.2f, 0.3f);

 

サンプルコードの実行結果

Zlibの方法で、pmdモデルを材質データまで読み込んでみました。

 

モデルは、ままま氏のDIVAっぽいど「DIVA風ミク」を使用しました。

頂点数9,331個

ポリゴン数12,083枚

マテリアル数5個

テクスチャ数8枚のモデルです。

 

ファイル読み込み→描画専用モデル変換→OpenGLのテクスチャ・頂点バッファ領域の獲得と設定 この4つの作業を全てネイティブ側で行い、結果はこのようになりました。

処理にDebugでは1.47秒、Releaseでは1.17秒掛かっています。

以前と比べるとかなり速くなりました。

 

そういえばAndroid OS2.3以降のAssetManagerがサポートされた環境で読み込むと、このZLib形式とどちらのほうが処理が速いのでしょうか?2.3以上の端末は持っていないので、今度SDKのエミュレータで比較してみようと思います。

 

比較結果