SDL2のAndroidアプリケーションでseekが遅くなる

なかなか気づきにくい現象ですが、SDL2のAndroidアプリケーションで特定の拡張子以外のファイルを開いた場合に、seek処理が遅くなります。

30MByte程度のファイルポインタを末尾に移動するのに数秒掛かります。

これはAssetManagerの仕様に伴うものです。

回避方法を記載します。


1 SDL2のI/O処理

SDL_RWops構造体経由で実行します。以下のコードはfoo.barというファイルを開いて、ファイルポインタを末尾に移動します。

SDL_RWops *rwops;
static char buffer[1024];

rwops = SDL_RWFromFile("foo.bar", "rt");
if (rwops == nullptr)
  return 1;

SDL_RWseek(rwops, 0, RW_SEEK_END);
SDL_RWclose(rwops);

return 0;

SDL_RWops構造体でI/O処理の実装は隠蔽されています。

2 AndroidのSDL_RWops構造体のメンバ

Androidの場合、SDL_RWops構造体はAssetManagerを使おうと試みます。assetFileDescriptorRefにAssetManager.openFdのJNIグローバル参照を設定します。

Sint64 Android_JNI_FileSeek(SDL_RWops* ctx, Sint64 offset, int whence)
{
    if (ctx->hidden.androidio.assetFileDescriptorRef) {
        /** AssetManager. */
    } else {
        /** Not AssetManager. */
    }

    return ctx->hidden.androidio.position;

}

AssetManager.openFdは独自形式のファイルの場合にnullを返します(1MByteを超える場合?)。

 

AssetManager.openFdが成功した場合、Android_JNI_FileSeekのif文が真となり、seek関数を呼び出します。

 

失敗した場合、else文が実行されます。

else文ではseek関数の代わりにread関数を何度も呼び、ファイルポインタを進めることで代用します。

read関数はコピーが発生する為、seek関数に比べて相当に重くなります。

これがseek処理が重い原因です。

3 aapt

assetsディレクトリのファイルをパッケージングするツールです。

jpgやmp3等の登録された拡張子はそのままパッケージングされます。

登録されていない拡張子の場合は圧縮されます。

1MByteを超えるような大きいファイルの場合は展開されないようです。

4 custom_rules.xml

aaptはAndroid SDKのtools/ant/build.xmlのタスクとして定義されています。

各プロジェクトのcustom_rules.xmlでaaptのタスクを上書きし、登録されていない拡張子を非圧縮の対象として登録することで、1MByte以上の登録されていなかった拡張子のファイルをAssetManager.openFdで開くことができるようになります。

 

以下は-pre-buildでjniディレクトリをndk-buildでビルドし、-package-resourcesでaaptを実行するcustom_rules.xmlです。

<?xml version="1.0" encoding="UTF-8"?>
<project>
  <target name="-pre-build">
    <exec executable="ndk-build" failonerror="true"/>
  </target>
  <target name="-package-resources" depends="-crunch">
    <!-- only package resources if *not* a library project -->
    <do-only-if-not-library elseText="Library project: do not package resources..." >
      <aapt executable="${aapt}"
            command="package"
            versioncode="${version.code}"
            versionname="${version.name}"
            debug="${build.is.packaging.debug}"
            manifest="${out.manifest.abs.file}"
            assets="${asset.absolute.dir}"
            androidjar="${project.target.android.jar}"
            apkfolder="${out.absolute.dir}"
            nocrunch="${build.packaging.nocrunch}"
            resourcefilename="${resource.package.file.name}"
            resourcefilter="${aapt.resource.filter}"
            libraryResFolderPathRefid="project.library.res.folder.path"
            libraryPackagesRefid="project.library.packages"
            libraryRFileRefid="project.library.bin.r.file.path"
            previousBuildType="${build.last.target}"
            buildType="${build.target}"
            ignoreAssets="${aapt.ignore.assets}">
        <res path="${out.res.absolute.dir}" />
        <res path="${resource.absolute.dir}" />
        <nocompress extension="bar" />
        <!-- <nocompress /> forces no compression on any files in assets or res/raw -->
        <!-- <nocompress extension="xml" /> forces no compression on specific file extensions in assets and res/raw -->
      </aapt>
    </do-only-if-not-library>
  </target>
</project>

上の例では<nocompress extension="bar" />で.barファイルを非圧縮にしています。

複数の拡張子を登録する場合はnocompress属性を複数記述します。