KLabGames Tech Blog

KLabは、多くのスマートフォン向けゲームを開発・提供しています。 スマートフォン向けゲームの開発と運用は、Webソーシャルゲームと比べて格段に複雑で、またコンシューマゲーム開発とも異なったノウハウが求められる領域もあります。 このブログでは、KLabのゲーム開発・運用の中で培われた様々な技術や挑戦とそのノウハウについて、広く紹介していきます。

カテゴリ: Android

このエントリーは、KLab Advent Calendar 2016 の12/22の記事です。

22番手も緊張しますね。KLabGamesの基盤エンジニアのkenseiです。よろしくお願いします。

プロローグ

ある日突然事は起こります。それまで順調にビルドされていたjenkinsのandroidビルドが突然のエラー。ログを見るとstderrの項目に

trouble writing output:
Too many field references: 131000; max is 65536.
You may try using --multi-dex option.

の文字が。そう、Androidはファイル内でコードによって呼び出すことができる参照の総数で65535を超える事ができないのです。

64K を超えるメソッドを使用するアプリの設定

いつかは戦わないといけないのですが、いざその日が来ると憂鬱ですね。ちなみに宗教上の理由でmulti dexはご法度です。

まずは計測

dexのメソッド数を計測してみます。ありがたいことにツールが公開されているので、それを使用します。

dex-method-counts

$ ./gradlew assemble
$ ./dex-method-counts path/to/target.apk > method-count.txt

吐かれたログを見ると

.
.
android: 16833
com: 19806
  facebook: 3263
  google: 14115
    android: 13911
    gms: 13911 <= gpgs
.
.

androidはしょうがないとして、うむ、、GPGSでかいな。。てことで、ここを重点的に潰す事にします。

aarとの戦い

最近のandroid libraryはAAR(Android Archive)で提供されています。ただのzipですが、jarは中に入ってしまってるので解凍しないといけません。という事でみんな大好き、shellの出番です。

本当はgradleでスマートにやりたかったのですが、aarをzipTreeするとpermissionエラーを吐いてgradleが死にました。googleさん、中をゴニョゴニョしたい人の事考えてassembleして下さい。。

準備

unzipするので、Assetsと同じフォルダにディレクトリを作ります。そこにshellを作っていきます。

$ mkdir -p Target/shrink-jar-task && touch shrink-jar.sh && chmod u+x shrink-jar.sh

まずはおまじない

#!/bin/bash

# 変数の準備
WORKSPACE=$(pwd)
WORK_DIR="${WORKSPACE}/tmp"
GRADLE_WORK_DIR="${WORKSPACE}/lib"
PLUGIN_DIR="${WORKSPACE}/../Assets/Plugins/Android"

# 優しさ
while getopts v OPT
do
  case $OPT in
    v ) DEBUG="true"
        ;;
  esac
done

# おしおき
if [ "${JAVA_HOME-undefined}" = "undefined" ]; then
    echo "JAVA_HOME not set"
    echo "alias emacs='vim'" >> .bash_profile
    exit 1
fi
if [ "${ANDROID_HOME-undefined}" = "undefined" ]; then
    echo "ANDROID_HOME not set"
    echo "alias emacs='vim'" >> .bash_profile
    exit 1
fi

# ワーク作る
if [ -d $WORK_DIR ]; then
  rm -rf $WORK_DIR
fi
mkdir $WORK_DIR
if [ -d $GRADLE_WORK_DIR ]; then
  rm -rf $GRADLE_WORK_DIR
fi
mkdir $GRADLE_WORK_DIR

だいたいコメントの通りです。途中でJAVA_HOMEとANDROID_HOMEを設定せずにビルドをしようとする不届き者にemacsを使えなくするお仕置きをしています。vimを使ってればお仕置きされずに済んだのに。。

準備その2

shellでやっていますが、一部の処理はgradleを使用するのでhomebrew等でインストールを済ませておいて下さい。あ、言うの遅くなりましたが、動作はmacでしか確認していません。

上記で用意したフォルダにbuild.gradleファイルを作成し、下記の内容を設定します。

$ vim build.gradle
task wrapper(type: Wrapper) {
    gradleVersion = '2.14.1'
}

設定後にbuild.gradleファイルのある場所で

$ gradle wrapper

を実行します。

aarを解凍

Assets/Plugins/Androidの下にあるaarを解凍していきます。

echo "unziping..."
find $PLUGIN_DIR -type f -name "*.aar" -exec basename {} \; | sed -e "s/.aar//" | xargs -I{} unzip ${PLUGIN_DIR}/{}.aar -d ${WORK_DIR}/{} >/dev/null 2>&1
find $WORK_DIR -type d -maxdepth 1 -mindepth 1 -exec basename {} \; | xargs -I{} rm -rf ${PLUGIN_DIR}/{}

ちなみにmaxdepthオプション付けてディレクトリをfindすると、rootが入って来てしまうのですが、mindepthを重ねがけするとrootを除外できるのを今回発見しました。他のスマートなやり方あるかもしれないので、もっと綺麗なshellを書けるようになりたい。。

aarを削除

もう使用しないので、aarを消していきます。

echo "remove plugin aar..."
if [ "$DEBUG" == "true" ] ; then
    find $PLUGIN_DIR -type f -name "*.aar*" -not -name 'appcompat-v7*.aar' -not -name 'support-v4-*.aar' -exec echo "  "{} \;
fi
find $PLUGIN_DIR -type f -name "*.aar*" -not -name 'appcompat-v7*.aar' -not -name 'support-v4-*.aar' -exec rm {} \;

この時にappcompatとsupportはいったん除きます。理由は後で面倒くさいからです!

classes.jarを取り出す

解凍したaarの中からjarを取り出していきます。

echo "move classes.jar"
find $WORK_DIR -type d -maxdepth 1 -mindepth 1 -not -name 'appcompat-v7*' -not -name 'support-v4-*' -exec basename {} \; | xargs -I{} mv ${WORK_DIR}/{}/classes.jar ${GRADLE_WORK_DIR}/{}.jar
find $PLUGIN_DIR -type d -name "firebase*" -exec basename {} \; | xargs -I{} mv ${PLUGIN_DIR}/{}/libs/classes.jar ${GRADLE_WORK_DIR}/{}.jar
if [ "$DEBUG" == "true" ] ; then
  find $GRADLE_WORK_DIR -type f -name "*.jar"  -exec echo "  "{} \;
fi

ファイルが同名なので、元のフォルダ名+"jar"に名前を変更しています。また、AARファイルになっていないAndroidLibraryのjarも持ってきています。今回はfirebaseを指定しています。

classes.jarを一つのjarにまとめる

renameしたlibフォルダのjarファイル達をunzipして一つのjarファイルにまとめてしまいます。build.gradleを編集します。

apply plugin: 'java'

dependencies {
    compile fileTree(dir: 'lib', include: ['*.jar'])
}

jar {
    from configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
    archiveName = 'google.jar'
}

task wrapper(type: Wrapper) {
    gradleVersion = '2.14.1'
}

shellにgradleのjarタスクを実行させます。

echo -n "make aggregation jar"
./gradlew -b build.gradle jar

-b でgradle 設定ファイルを指定しています。

full-gpgs-jar

jd-guiで生成されたjarを確認した所 一個のjarファイルにまとまりました。

proguard ruleを生成

使用していないメソッドをstripするためにproguardをかけていきます。一からproguardのruleファイルを書いていくのは面倒なので、aarの中に入ってるproguard.txtを再利用します。また、アプリ独自の設定を書いていくようにproguard-rules.proファイルを用意します。

$ touch proguard.base

shellにproguard ruleファイルを生成させます。

echo "generate proguard rule"
cat $ANDROID_HOME/tools/proguard/proguard-android.txt > proguard-rules.pro
find $WORK_DIR -type f -name "proguard.txt" | grep -v appcompat-v7 | grep -v support-v4 | xargs -I{} cat {} >> proguard-rules.pro
cat proguard.base >> proguard-rules.pro

ちなみにデフォルトのproguard-android.txtもまとめて1ファイルにしてしまってます。gradleのproguardタスクでgetDefaultProguardFile関数が使えなかったので。。

jarファイルダイエット

proguardを使用して、未使用のメソッドをjarから抹殺します。androidのpluginとjavaのpuluginでタスクがかち合うので、別名のgradle設定ファイルにします。今回はproguard.gradleとします。

まずはandroidプラグインを使ってビルドを通すためにダミーのAndroidManifest.xmlを作成します。

$ mkdir -p src/main && vim src/main/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.klab.tekitou" >
    <uses-sdk android:minSdkVersion="7" />
</manifest>

次にgradle設定ファイルを作成します。

$ vim proguard.gradle
buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.1.0'
    }
}
allprojects {
    repositories {
        jcenter()
    }
}

apply plugin: 'com.android.library'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.3"

    defaultConfig {
        minSdkVersion 14
        targetSdkVersion 23
    }
}

task clearJar(type: Delete) {
    delete '../Assets/Plugins/Android/google.jar'
}
task proguard(type: proguard.gradle.ProGuardTask) {
    configuration 'proguard-rules.pro'
    injars 'build/libs/google.jar'
    outjars '../Assets/Plugins/Android/google.jar'
    libraryjars System.getenv("JAVA_HOME") + '/lib/rt.jar'
    libraryjars System.getenv("JAVA_HOME") + '/lib/jce.jar'
    libraryjars System.getenv("ANDROID_HOME") + '/platforms/android-23/android.jar'
    libraryjars System.getenv("ANDROID_HOME") + '/extras/android/m2repository/com/android/support/support-annotations/23.4.0/support-annotations-23.4.0.jar'
    libraryjars 'tmp/appcompat-v7-23.1.1/classes.jar'
    libraryjars 'tmp/support-v4-24.0.0/classes.jar'
    libraryjars '../Assets/Plugins/Android/firebase-common-9.8.0/libs/classes.jar'
}
proguard.dependsOn(clearJar)

injarsにclasses.jarをまとめたgoogle.jar、libraryjarsにjarが依存している物を指定しています。

※適当に必要そうなのを足していったので、結局どれが本当に必要なのか分かってない

appcompatとsupportは最初に作業フォルダに持ってきたけど、excludeしていたjarを使用します。support-annotationsはversionがpathに入っていますが、target sdk versionの中で一番新しいのを適当に設定しています。android.jarはproguard.gradleで指定したcompileSdkVersionを指定しています。

shellにgradleのjarタスクを実行させます。

echo "shurink jar"
./gradlew -b proguard.gradle proguard

ここはビルドに必要なlibraryjars不足によるエラーとの戦いになるので、何回もgradlewコマンドを叩いて通るまで頑張ります。

minimam-gpgs-jar

できあがったミニマムなjarファイルです。

aarの中のresourceを戻す

解凍したaarの中のリソースを戻していきます。Assets/Plugins/Androidフォルダ直下に置いて、packagingをUnityに任せます。

echo "copy unziped android resources"
if [ "$DEBUG" == "true" ] ; then
  find $WORK_DIR -type d -maxdepth 1 -mindepth 1 -exec basename {} \; | grep -v appcompat-v7 | grep -v support-v4 | xargs -I{} echo "  "${WORK_DIR}/{}
fi
cat <<EOS > project.properties
target=android-14
android.library=true
generated=true
EOS
find $WORK_DIR -type d -maxdepth 1 -mindepth 1 -exec basename {} \; | grep -v appcompat-v7 | grep -v support-v4 | xargs -I{} cp project.properties ${WORK_DIR}/{}/
find $WORK_DIR -type d -maxdepth 1 -mindepth 1 -exec basename {} \; | grep -v appcompat-v7 | grep -v support-v4 | xargs -I{} cp -r ${WORK_DIR}/{} ${PLUGIN_DIR}/{}
if [ "$DEBUG" == "true" ] ; then
  find $WORK_DIR -type d -maxdepth 1 -mindepth 1 -exec basename {} \; | grep -v appcompat-v7 | grep -v support-v4 | xargs -I{} echo "  "${PLUGIN_DIR}/{}
fi

project.propertiesを追加しないとUnityにAndroidResourceとして認識されないので、追加しています。targetはPlayer SettingsのMinimum API Levelを設定すれば大丈夫だと思います。generated=trueとしてるのは、aarのバージョンアップ時にフォルダを消すためのマーキングです。

きちんとAndroidResourceとして認識されているかはUnityAppPath/Temp/StagingArea/android-librariesを見ると確認できます。

proguardファイルの設定

上記で生成したjarファイルはミニマム過ぎて動きません。試しにfirebaseを全部stripから覗いてみます。

$ vim proguard.base
-dontwarn android.support.v4.app.**
-dontwarn com.google.firebase.**
-dontwarn com.google.android.gms.**

-keep public class com.google.firebase.**

ついでにwarningがうるさいので切っています。

  • aarはresourceが未コンパイルなので、R.classがないエラーが出るので。
  • R.$のエラーだけ出るようになるまではdontwarnは設定しない

実行してみると

add-firebase

firebaseが復活しているのが分かります。

ここからはUnityでAndroidビルド&&実機で動かしながら動作確認 => 落ちた所でログを確認して、必要なクラスをproguard.baseに足してshellを流す の作業の繰り返しになります。

エピローグ

vimを使いましょう

明日はTech Blogなのに、泣かせるので評判な@hhattoです。

この記事は KLab Advent Calendar 2016 の14日目の記事で @kakkun61 が担当します。

compile SDK version や build-tools version を Unity アプリで指定するにはどうすればいいの?という話をします。

この記事の内容は OS X 10.11.6 で確認しています。

Android アプリケーションのビルド時に指定するバージョンについて

Android アプリケーションのビルド時に指定するバージョンについては「基本をおさえる!Androidアプリで指定するバージョンについて(compileSdk,buildToolsVersion,....) - Qiita」にまとまっています。簡単に引用・要約します。

Android アプリケーションのビルド時に指定するバージョンは下記7つあります。

  • SDK バージョン
    • コンパイル SDK バージョン
    • 最小 SDK バージョン
    • 最大 SDK バージョン
    • ターゲット SDK バージョン
  • build-tools バージョン
  • Gradle の Android プラグインのバージョン
  • Gradle のバージョン
SDK バージョン
コンパイル SDK バージョン
コンパイル時に使用される Android.jar のバージョンでここにない API はコンパイルできません。build.gradle に記載します。値としては API レベルです。次の3つも同様です。
最小 SDK バージョン
インストールできる OS の下限で、条件付きコンパイル等しない限りこれより古い API は利用できません。build.gradle に記載します。
最大 SDK バージョン
インストールできる OS の上限です。build.gradle に記載します。
ターゲット SDK バージョン
OS がこの値を見て処理をし分けます。例えば UI テーマの変更や実質メニューキーの表示非表示があります。引用元記事にスクリーンショットがあります。build.gradle に記載します。
build-tools バージョン
その名の通り build-tools のバージョンで aaptaidldxjack.jarjill.jar などが含まれます。build.gradle に記載します。
Gradle の Android プラグインのバージョン
プロジェクトの build.gradle に記載します。
Gradle のバージョン
gradle/wrapper/gradle-wrapper.properties にダウンロード元 URL として記載します。

Unity 3D から生成した Android プロジェクトを Gradle でビルドする

Unity 3D を使って直接 APK を生成する場合、コンパイル SDK バージョンと build-tools バージョンを指定することができず、ビルドするコンピューターにインストールされている最新版が使用されます。これでは、ビルドサーバーを複数案件で共用している場合、案件ごとでこれらバージョンを固定してビルドすることができなくなります。なので、Unity 3D から Android プロジェクトを生成し、Gradle でビルドすることを考えます。

ただ、これではまだ問題があります。Unity 3D の生成する Android プロジェクトの形式が ADT 形式なのです。Google による ADT のサポートは終了しています。

これを Gradle でビルドするために Gradle ビルドスクリプトに少し工夫を加えます。

処理の流れ

  1. Unity エディターから Android プロジェクト(ADT 形式)を出力
  2. Gradle でビルドできるよう Gradle 関連ファイルのリンクを作成
  3. Gradle でビルドして APK 生成

ディレクトリー構成

下記のようなディレクトリー構成とします。サンプルリポジトリーはこちら

  • Assets/
    • Editor/
      • BatchBuild.cs
  • ProjectSettings/
  • gradle/
    • gradle/wrapper
    • build.gradle
    • gradlew
  • make-gradle-buildable.bash

BatchBuild.cs

バッチビルドでは Build() 関数を呼び出せば build-adt ディレクトリーに Android プロジェクトが生成されます(処理の流れ 1)。BuildOptions.AcceptExternalModificationsToPlayer を指定することで Android プロジェクトの生成を指定しています。その他のパラメーターは適宜変更してください。

using UnityEngine;
using UnityEditor;
using System.Linq;

public static class BatchBuild
{
    public static void Build()
    {
        EditorApplication.Exit(DoBuild());
    }

    static int DoBuild()
    {
        Debug.Log("Build Start");
        var options = BuildOptions.SymlinkLibraries
                      | BuildOptions.Development
                      | BuildOptions.AllowDebugging
                      | BuildOptions.AcceptExternalModificationsToPlayer; // このオプションでプラットフォームのプロジェクトが生成される

        var scenes = EditorBuildSettings.scenes.Where(s => s.enabled).ToArray();

        PlayerSettings.bundleIdentifier = "com.example.gradletest";
        PlayerSettings.bundleVersion = "1";
        PlayerSettings.productName = "Gradle Test";
        PlayerSettings.Android.bundleVersionCode = 1;
        PlayerSettings.Android.minSdkVersion = AndroidSdkVersions.AndroidApiLevel22;

        var errorMessage = BuildPipeline.BuildPlayer(scenes, "build-adt", BuildTarget.Android, options);
        if (string.IsNullOrEmpty(errorMessage))
        {
            Debug.Log("Build Success");
            return 0;
        }
        Debug.LogError("Build Failure");
        Debug.LogError(errorMessage);
        return 1;
    }

    [MenuItem("File/Export Android Project #%a")]
    public static void MenuBuild()
    {
        DoBuild();
    }
}

make-gradle-buildable.bash

処理の流れ 1 で、build-adt ディレクトリーに Android プロジェクトが生成されるので、Gradle でのビルドができるようにそこにファイルのリンクを置きます(処理の流れ 2)。prod_nameBatchBuild.csPlayerSettings.productName に指定されたものです。

#!/usr/bin/env bash -ex

prod_name=$(ls -1 build-adt)
ln -sFv "$(pwd)/gradle/"* "build-adt/$prod_name"

gradle/build.gradle

処理の流れ 3 のADT 形式のプロジェクトを Gradle でビルドするためのビルドスクリプトです。sourceSets の部分がキモで、各ソースファイルの位置を ADT 形式に合うように指定しています。他の部分は一般的な Android のビルドスクリプト同様に適宜変更してください。

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.1.0'
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.2"

    defaultConfig {
        applicationId "com.example.gradletest"
        minSdkVersion 15
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }

    // Lint エラーが出てもビルドを継続するように指示
    // Unity の生成物が Lint にひっかかるので必須
    lintOptions {
        abortOnError false
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-unity.txt'), 'proguard-rules.pro'
        }
    }

    // ADT 形式のディレクトリー構成に対応するための記述
    sourceSets {
        main {
            manifest.srcFile 'AndroidManifest.xml'
            java.srcDirs = ['src']
            resources.srcDirs = ['src']
            aidl.srcDirs = ['src']
            renderscript.srcDirs = ['src']
            res.srcDirs = ['res']
            assets.srcDirs = ['assets']
            jniLibs.srcDirs = ['libs']
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:23.4.0'
}

ビルド手順

以上の準備をすると、下記コマンドでビルドができるようになります。生成された APK は build-adt/$prod_name/build/outputs/apk 以下にあります。./make-gradle-buildable.bash./gradlew build の実行は、Unity の post process build にしてしまうのもありだと思います。

/Applications/Unity/Unity.app/Contents/MacOS/Unity -batchmode -quit -executeMethod BatchBuild.Build
./make-gradle-buildable.bash
pushd "build-adt/$(ls -1 build-adt)"
  ./gradlew build
popd

こんにちは、makki_dです。 今回はKLabでも利用しているSQLCipherについて、自前ビルドすることでapkファイル内のライブラリサイズを50%(当社比)削減する方法を紹介したいと思います。

暗号化機能付きSQLite、SQLCipher

SQLiteはファイルベースの軽量なRDBで、クライアント(iPhone/Android)内にステージや敵の基本情報(マスタデータ)等を保持するのにとても便利です。 ですが、SQLiteのデータファイルを平文のまま保持していると、改ざんされたり非公開のデータが覗き見られたりしてしまうので、暗号化しておきたいものです。 そんな時には、SQLiteに暗号化機能を加えたSQLCipher(※)がとても手軽で、KLabでも活用しています。

この記事では、Unity製のAndroid向けアプリで、SQLCipherを依存ファイルを少なくシンプルに使うための方法を紹介します。

※ここで扱うSQLCipherは、オープンソースなCommunity Editionを利用しています。

SQLCipherの普通の使い方 (Android)

SQLCipherのサイトにはAndroidへの導入手順のページもありますし、 Community Editionのバイナリパッケージも用意されています。 Unity5以降であれば、aarファイルをAssets/Plugins/Android以下に入れることでライブラリ自体は簡単に導入できます。

しかしこのライブラリはJavaから利用することを前提とするものですので、Javaのクラスに依存していますし、Unityから使う時もJavaの世界を経由して呼び出すことになってしまいます。

さらに、3.4.0まではICU (International Compornents for Unicode) という文字コード変換ライブラリが組み込まれているため、 icudt46.zipという2.2MBのファイルがもれなく含まれていました。 GooglePlayのAPKファイルサイズ制限的にも苦しいので、依存をなくしてしまいたいところです。

ネイティブ共有ライブラリとして使う方法

さて、ここでUnityでSQLiteを使う時を思い出したいと思います。

C#には、DllImport属性を使ってネイティブ共有ライブラリの関数を直接呼び出すことができます。 (MSDN, Mono)

Unityでもこの機能を使って、Plugins以下に配置したlibsqlite3.soの関数を呼びだすことでSQLiteが使えます。 SQLCipherもネイティブ共有ライブラリとしてビルドすれば、同様にDllImportできるはずです。

ネイティブ共有ライブラリのビルド方法

実際にARMv7向けのネイティブ共有ライブラリをビルドしてみたいと思います。 ビルド環境は Ubuntu 14.04、Android NDK r12b を想定してます。

前準備として次のように環境変数を設定します。 コマンド中のパスやAPI Levelは適宜読み替えてください。

export ANDROID_NDK_ROOT=$HOME/Android/android-ndk-r12b
export SYSROOT=$ANDROID_NDK_ROOT/platforms/android-14/arch-arm
export TOOLCHAIN=$ANDROID_NDK_ROOT/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin
export PATH=$TOOLCHAIN:$PATH

SQLiteの場合

ここでまたSQLiteを思い出したいと思います。 SQLiteのビルドは、amalgamationされた単一のcソース(sqlite3.c)からコンパイルするのが簡単です。 SQLiteのDownloadsページよりソースを入手した後、 Android NDKを用いて次のようにビルドします。

unzip sqlite-amalgamation-3140100.zip
cd sqlite-amalgamation-3140100
arm-linux-androideabi-gcc --sysroot=$SYSROOT -march=armv7-a -shared sqlite3.c -o sqlite3.so

sqlite3.cは公式サイトからも入手できますが、生のソースコードツリーから生成することもできます。 configureしたあと、make sqlite3.c とするだけです。

unzip sqlite-src-3140100.zip
cd sqlite-src-3140100
./configure
make sqlite3.c

SQLCipherの場合

さて、SQLCipherではどうでしょう。 configureオプションで頑張ってクロスコンパイルすることもできるかもしれませんが、 SQLiteと同じようにamalgamationされたsqlite3.cからコンパイルするのが簡単です。

SQLCipherではsqlite3.c単体では提供されていないので、自分で生成します。 ソースをcloneしてきたら、configureしてmake sqlite3.cです。sqliteと同じですね。

git clone https://github.com/sqlcipher/sqlcipher.git
cd sqlcipher
./configure
make sqlite3.c

続いてNDKでビルドするのですが、その前にOpenSSLも用意する必要があります。 SQLCipherは暗号化にOpenSSLのlibcryptoを利用しているのですが、Androidが持つlibcrypto.soは使えませんので静的リンクします。 (Android6以降のlibcrypto.soはOpenSSLではなくBoringSSLのもので、ドキュメントにもリンクしてはならないと書かれてます。)

同じNDKでlibcrypto.aをビルドします。 (実際には使っていない機能を組み込まないために、このあたりを参考にConfigureオプションを設定します)

git clone -b openssl_1_0_2-stable https://github.com/openssl/openssl.git
cd openssl
./Configure dist
./Configure --cross-compile-prefix=$TOOLCHAIN/arm-linux-androideabi- --sysroot=$SYSROOT android-armv7
make build_libcrypto 

libcrypto.aがビルドされました。いよいよ生成したsqlite3.cからsqlcipherをビルドします。

cd sqlcipher
arm-linux-androideabi-gcc --sysroot=$SYSROOT -march=armv7-a -shared -DSQLITE_HAS_CODEC -I../openssl/include sqlite3.c -o libsqlcipher.so ../openssl/libcrypto.a

できあがったlibsqlcipher.soの使い方

こうしてAndroid用のlibsqlcipher.soを作ることが出来ました。 これは普通のネイティブ共有ライブラリなので、DllImportで直接呼び出すことができます。 Javaやicudt46.zipへの依存もありません。 既存のSQLite向けの実装のDllImportだけ書き換えれば使えるはずです。

またaarファイルでSQLCipherを導入した場合、Javaとのブリッジのためのコードの他、ARMv5向けのバイナリもapkファイルに含まれてしまいます。 Unity本体がARMv5をサポートしていないので、ARMv7とx86のネイティブ共有ライブラリのみに絞ることでapkファイルのサイズを節約できます。 空プロジェクトにライブラリのみ追加して試してみたところ、apkファイルサイズは次のようになりました。

ライブラリ導入方法 apkファイルサイズ ライブラリサイズ(差分)
ライブラリ無し 17.6 MB --
aar ver 3.4.0 24.7 MB 7.1 MB
aar ver 3.5.3 20.0 MB 2.4 MB
自前ビルド 18.8 MB 1.2 MB

この記事ではARMv7のAndroid向けバイナリのビルド方法を掲載しましたが、x86のAndroidやWindows向けのクロスコンパイルも基本的には同じ方法でできるはずです。 ぜひ挑戦してみてください。

プロモーションコード(promo code)とは

今年(2016年)の1月くらいから、Androidではプロモーションコードの機能が使えるようになりました。プロモーションコードとは利用者に対して無料でアプリそのものやアプリ内アイテム、機能のアンロック等を提供できるもので、開発者が作成したコードは利用規約の下で自由に配布が可能なため、強力なプロモーション効果が期待できます。

今回はAndroid DevelopersのIn-app Promotionsの翻訳をご紹介します。

注意事項:

  • このページは Google Inc. とは無関係であり、内容は無保証です
  • 参照した文書 は 2016/5/17時点のものです。これ以降にドキュメントが改訂されていた場合、最新の仕様と異なる可能性があります

アプリ内プロモーション(In-app Promotions 和訳)

プロモーションコードを利用することで、開発者は配布数限定の無料コンテンツ・無料の追加機能をユーザに提供できるようになります。作成したプロモーションコードはサービス利用条件の下で配布することが可能で、ユーザはアプリ中またはPlay Storeアプリでコードを入力して無料でアイテムを入手することができます。プロモーションコードを様々な場面で活用することで、開発者はユーザとのふれあいの機会をさらに広げることができます。
例として次のような利用法が考えられます。

  • ゲームアプリであれば、とあるイベントへ招待された出席者向けの特典として、専用の特別アイテム(例としてキャラクターや装飾アイテムなど)を作ることができます。会場でプロモーションコードがプリントされた紙のカードを出席者に配布すれば、コードを入力したユーザだけにその特別アイテムをアンロックできます。
  • プロモーションコードを地元の商店や企業で配布すれば、アプリの試用を促すことができます。これにより潜在的なユーザの獲得につなげることができます。
  • 「友人と家族」用の専用コードを開発者の社内で配り、友人たちにシェアするといったことができます。

プロモーションコードは必ず何らかの product ID (または SKU)と関連づけられます。作成済みの in-app プロダクトからプロモーションコードを作成できます。そのプロダクトをPlay Storeで販売しないようにすれば、プロモーションコードでのみ入手可能なアイテムとすることができます。Play Storeやアプリ内でプロモーションコードを入力すると、ユーザには通常通り決済した時と同じようにアイテムが付与されます。すでにアプリ内課金としてIn-app Billing version3 を利用中であれば、プロモーションコード対応を追加するのは簡単です。

プロモーションコードの作成と利用

プロモーションコードは Google Play Developer Console で作成します。各コードはDeveloper Consoleで登録済みの商品ひとつに紐付きます。

入手したプロモーションコードを商品引き替えに利用するには、次の二つの方法があります。

  • In-app Billing の実装 で説明されているとおり、アプリ内の通常の購入フローの中でプロモーションコードを入力します。これはアプリから見れば通常の購入と全く同様で、ユーザにとっては代金の代わりにプロモーションコードを利用することだけが異なることになります。
  • Google Play Storeアプリ内からコードを入力します。ユーザがここでコードを入力すると、Play Storeはそのユーザに対して対象アプリを起動するよう通知する(最新バージョンのアプリがインストール済みの場合)か、または対象アプリのダウンロード、もしくはアップデートを促します。(現在のところGoogle Play Webストア上でのプロモーションコード入力は非対応です)

プロモーションコードが消費型プロダクト用の場合、その商品の別のプロモーションコードが再度利用できるのは初めのものが消費された になります。例えば、とあるゲームでプロモーションコードを使ってライフ追加のアイテムを提供していたとします。ベティはそのアイテムのプロモーションコードを二つ持っており、プロモーションコードを一つ利用してからゲームを起動します。するとアイテムが消費されてベティの持つキャラクターには追加ライフが付与された状態になります。ここで初めて、もうひとつのプロモーションコードを利用してさらにライフを追加できるようになります。
(最初に入力したプロモーションコードで入手したアイテムが消費されるまでは、別のコードを利用することはできません)

アプリでのプロモーションコード対応

アプリにプロモーションコード対応を組み込むには、アプリの起動(start)時または復帰(resume)時に必ず getPurchases() メソッドを呼び出す必要があります。このメソッドが返すものは現在未消費の購入商品全てであり、これにはプロモーションコードで引き替えられた購入内容も含まれます。これを実現する一番簡単な方法は、アクティビティの onResume() メソッド内で getPurchases() を呼び出すことです。こうすることで、アクティビティが生成されるタイミングだけでなく復帰時にもコールバックが呼ばれるようになります。起動時や復帰時に getPurchases() を呼び出せば、ユーザが購入したもの、およびアプリが起動していない間にプロモーションコードで引き替えた内容も含めて全ての内容を取得することが保証できます。この方法が良いのは、さらにユーザがアプリ内で購入を行ったが何らかの理由で処理が失敗した場合でも、次回アクティビティが復帰し getPurchases() が呼び出されば、アプリが購入に関する情報を再度見つけられるようになることです。

上記に加えて、アプリ内でユーザがプロモーションコードを入力できるようにしておく必要があります。すでにアプリ内課金の処理(In-app Billingリクエストの作成 に記載)に対応済みであれば、自動的にアプリ内でのプロモーションコードの引き替えにも対応できていることになります。アプリ内課金のUIが起動されるとユーザにはプロモーションコードで支払いをするオプションが示され、その操作の結果として onActivityResult() メソッドは購入が完了したかどうかを示すインテントを受け取ります。この場合でも、購入や消費処理が正しく終了しなかった場合に備えてアプリ起動時や復帰時に getPurchase() を呼び出す必要はあります。たとえばプロモーションコードでのアイテム引き替えには成功したものの、それを利用する前にアプリが強制終了したとします。それでも次の起動時にアプリは getPurchases() を呼び出すため、購入に関する情報を取得できることになります。

アプリ起動中にPlay Storeアプリ内でプロモーションコードが利用された場合についても想定しておかなくてはなりません。アプリに PURCHASES_UPDATED インテントのリスナを登録しておけば、ユーザがコードを利用したことはアプリから即座に検知することができます。Play Storeはユーザがプロモーションコードを利用するとこのインテントを送信します。

PURCHASES_UPDATEDインテントを検知するには、"com.android.vending.billing.PURCHASES_UPDATED" を処理するBroadcastReceiverオブジェクトを動的に生成して登録するようにしてください。次のようなコードを記述し、アクティビティの onResume() メソッド内でレシーバーの登録を行います。

IntentFilter promoFilter =
    new IntentFilter("com.android.vending.billing.PURCHASES_UPDATED");
registerReceiver(myPromoReceiver, promoFilter);

ユーザが購入処理を行うとシステムは登録されたレシーバーの onReceive() メソッドを実行します。このメソッド内では getPurchases() を呼び出し、ユーザがどの購入を行ったかを精査する必要があります。

アクティビティの onPause() メソッドでは、アプリ起動中でない時のシステム負荷を下げるため次のようにBroadcastReceiverを削除するようにしてください。

unRegisterReceiver(myPromoReceiver);

注意:アプリのマニフェストでのBroadcastReceiverの登録は行わないでください。マニフェストでreceiverを宣言してしまうと、アプリが起動していない状態でユーザが何らかの購入を行った際、システムはインテント処理のためにそのアプリを起動してしまうことになります。この動作は必要でないばかりか、むしろユーザを苛立たせることになりかねません。これを避けるため、ユーザがアプリを起動したタイミングで getPurchases() を呼び出して、アプリ非起動時に作成された購入情報を取得するようにすると良いでしょう。

アプリ内プロモーションのテスト

アプリ内プロモーションに対応した際は、次のユースケースをテストするようにして下さい。

ユーザが対象アプリ内からプロモーションコードを入力する

プロモーションコードが対象アプリ内における課金フローの中で利用された場合、In-app Billingリクエストの作成で示すように、システムは購入処理のためアクティビティの onActivityResult() メソッドを起動します。ユーザが金銭、プロモーションコードどちらで購入を行った場合でも onActivityResult() にて購入結果が正しく処理されることを確認してください。

ユーザがPlay Store内からプロモーションコードを入力する

プロモーションコードがPlay Store内で利用された場合には、いくつか想定しておくべきケースがあります。それぞれのパターンについて検証を行うようにしてください。

対象アプリがインストールされていない場合

対象アプリがデバイスにインストールされていない状態でユーザがプロモーションコードを入力すると、Play Storeはそのユーザに対してアプリのインストールを促します。(対象アプリがインストールされているがそれが最新のバージョンでない場合は、Play Storeはアプリのアップデートを促します) 対象アプリがインストールされていない場合のテストとして、次の手順をデバイスから実施するようにしてください。

  1. ユーザが Play Store上でプロモーションコードを入力する。Play Storeはユーザに対して対象アプリのインストールを促す
  2. ユーザが対象アプリをインストールして起動する。ここでアプリの起動処理を確認し、アプリが getPurchases() を呼び出していること、プロモーションコードによる購入が正しく検出できていることを検証する
対象アプリがインストール済みだが、起動中でない場合

対象アプリがデバイスにインストールされている状態でプロモーションコードを入力すると、Play Storeはそのユーザに対象アプリに切り替わることを通知します。デバイスに対象アプリがインストール済みだが起動状態ではない場合のテストとして、次の手順を実施してください。

  1. ユーザが Play Store上でプロモーションコードを入力する。Play Storeはユーザに対して対象アプリに切り替わることを通知する
  2. ユーザが対象アプリを起動する。ここでアプリの起動処理を確認し、アプリが getPurchases() を呼び出していること、プロモーションコードによる購入が正しく検出できていることを検証する
対象アプリがインストール済みで、起動中の場合

デバイス上でアプリが起動中の時にプロモーションコードが入力された場合には、Play Storeは対象アプリに対して PURCHASES_UPDATED インテントで通知を行います。次の手順でテストを行ってください。

  1. ユーザが対象アプリを起動する。アプリ自体が正しく登録されており、 PURCHASES_UPDATED インテントを処理できる状態になっていることを確認する
  2. ユーザがPlay Storeアプリを起動しプロモーションコードを入力すると、Play Storeは PURCHASES_UPDATED インテントを送信する。ここでそのインテントを処理するため、アプリの BroadcastReceiver.onReceive() コールバックが呼ばれていることを確認する
  3. 上記の onReceive() メソッドでは getPurchases() を呼び出してインテントに応答する必要がある。ここでは上記のメソッドの呼び出し、およびユーザが入力したプロモーションコードにより作成された購入情報が正しく検出できていることを確認する
  4. ユーザがタスクの切り替えを行い対象アプリに戻る。ここで、購入されたアイテムが付与されていることを確認する

実際にプロモーションコードを実装してみると?

'In-app Promotions'の翻訳は以上ですが、最後に実際にプロモーションコードを利用する際のポイントを簡単に記載しておきます。

まず、プロモーションコードはIn-app Billingが利用できる地域全てで利用できるわけではありません。2016年5月現在ではアメリカや日本など16カ国となっており、プロモーション対象ユーザがこれらの地域に含まれているか確認しておく必要があります。
またKLabで実施したテストから、 少なくとも現状の振る舞いからは いくつか注意すべき点があることが分かっています。

  1. 購入情報のうち、'Order Id'(orderId)および'Developer Payload'(developerPayload)は設定されない
  2. 発行済みのプロモーションコードのうち、どのコードが利用されたかを知る手段は(恐らくは)無い

1.については、独自サーバでコンテンツを付与するようなアプリを提供している場合、一意性のチェックをOrder Idで行っていたり、セキュリティチェックをDeveloper Payloadで行っているような場合に影響するでしょう。(後者の属性が省かれる理由は、Google Play Storeアプリから直接プロモーションコードが入力された場合、ペイロードを対象アプリ中から設定する機会がないことによる制限ではないかと思われます)
このため、すでに実装済みのIn-app Billing処理を改修して提供する場合にはこの点に注意が必要です。
また 2.についてですが、購入情報の中には利用されたプロモーションコードの情報は含まれていない(と思われる)ため、コード毎の使用記録等の追跡は不可能ということになりそうです。
ちなみにプロモーションコードの利用に関する情報はDeveloper Consoleでも少なくとも現時点では確認ができないようです。

また、上記の資料ではWeb版のプレイストアではプロモーションコードに未対応の旨が記載されていますが、現在Web版TOPからプロモコードを入力するUIへの導線が存在する(TOP左下の「コードを利用」のリンク)ようで、ヘルプにもその記述があります。

プロモーションコード利用に関するベストプラクティスは今後もこちらでご案内する予定です。

KG SDKについて

KG SDK は、Androidのプロモーションコードといった新しい機能にもいち早く対応し、ゲーム開発を最適化することを目指しています。
KG SDKの概要についてはこちら を、お問い合わせにつきましてはこちら をご覧下さい。


Shimanuki

こんにちは、今日は少しAndroidの開発ツールのお話をしたいと思います。

Visual Studio 2015を使ってAndroidの実機デバッグをおこなう方法とハマりがちなポイントを紹介します。

もともと社内でVisual Studio 2015でのAndroid開発環境を評価し始めたのは、Visual Studio 2015がリリースされる少し前のことでした。その時には最新のPreview版(CTP6)を使い、Android端末の実機デバッグをテストしました。

当時の個人的な感想としては、まだサポートしていない機能が幾つかあるものの、VSの操作に慣れている人にとっては大変使いやすい環境だと感じました。

Visual Studio 2015の正式リリースから時間がある程度経ち、Update 1リリースも提供されています。以前のプレビュー版で作っていた環境よりもかなり安定した環境を整えられたので、VSの新機能と共に設定上のコツなどを紹介します。

この文章で想定している対象読者

  • Windows上で主にNDKを利用するAndroidプロジェクトの開発、ビルド、実行、デバッグなど一連を全部完結したい方
  • Visual StudioでAndroidもしくはクロスプラットフォームのプロジェクトを開発したい方

なぜVisual Studio?

  • VisualStudioはインテリセンスを含めてエディターが使いやすい
  • Android実機に繋いでアプリを実行し、ブレークポイントが置かれた所に処理が止まる。その時点での変数値やメモリダンプなどが確認できたり、VisualStudioの便利な機能が当たり前に使える。

image04

  • SDK、NDKの適切なバージョンをインストール時に追加取得できる

VSでのAndroidアプリ開発を始める前にある程度理解が必要なポイント

  • Visual Studioの基本操作
  • Visual Studioのプロジェクトの基本構成

使いこなすためのポイント

  • Visual Studioのプロジェクトプロパティの設定
  • Visual Studioのビルドイベント

今回筆者が使った環境について

  • VSバージョン : Visual Studio Enterprise 2015
  • OS : Windows7 Professional
  • 端末 : Nexus5(Android 5.0)

ゼロベースからサンプルをステップ実行してみよう

まず、導入はとても簡単です。

  • ライセンス条件に従って、Visual Studio Enterprise 2015もしくはVisual Studio Community 2015のどちらかをダウンロード(Community版は企業内での利用などに制限があります。適切に判断してください)
  • Visual C++ for cross-platformのSDKやAndroid NDKなどのパッケージにチェックを付けてインストール

これだけで最低限の環境が整います。

インストールを待っている間についでに、端末の準備もしておくことをお勧めします。

  • 端末の設定画面で開発者モードをオン(開発者モードの他の設定は必要に応じてオン/オフ)
  • PCに該当するUSB Driverをインストール

次はプロジェクトのテンプレートを作って実行します。

  • 新しいプロジェクトの作成画面を開き、図1のようにプロジェクトのテンプレートを選んで作成

image01
(図1)

  • 図2のようなプロジェクトが出来上がる

image06
(図2)

ここで気を付ける点が二つあります。

一点目:
今まで通りソリューションへプロジェクトを自由に追加/削除できますが、AndroidApp.Packagingというパッケージング用プロジェクトは単体で作成することができません。このため、一旦追加したものを削除しないように注意してください。

二点目:
メニューバーの真ん中あたりに[Debug][ARM(チップ)][端末名]と表示されます。端末名がちゃんと表示されるかどうかを確認しておいてください。チップのデフォルトはx86なので、デフォルトのままでは端末名にNo Deviceと表示される場合があります。このままでは実機への転送とデバッグがおこなえません。

作ったプロジェクトをビルドします。

Ctrl + Shift + Bでビルドし、出力ウインドウを見たら、先頭にはこんなログが出力されます。

1>------ Build started: Project: AndroidApp.NativeActivity, Configuration: Debug ARM ------
1> ANDROID_HOME=C:\Program Files (x86)\Android\android-sdk
1> ANT_HOME=C:\Program Files (x86)\Microsoft Visual Studio 14.0\Apps\apache-ant-1.9.3\
1> JAVA_HOME=C:\Program Files (x86)\Java\jdk1.7.0_55
1> NDK_ROOT=C:\ProgramData\Microsoft\AndroidNDK\android-ndk-r10e\

いくつかのパスがすでに内部で設定されていることがわかります。

NDKのビルドが終わったら、自動的にantが実行され、パッケージングなどの作業を完了させます。

ここではサンプルコード中の図3の位置へブレークポイントを貼って、F5で実行してみましょう。

image03
(図3)

ブレークポイントのところで実機上の処理が中止され、ウォッチ機能でパラメータの内容を確認できます。

その後にF5で再開し、サンプルプログラムの実行が継続されることを確認できます。

既存プロジェクトをVS2015に移行する時の注意点

ここまでで出来たテンプレートプロジェクトを拡張していくと、より複雑なアプリを開発できます。一方で既に存在するプロジェクトをVS2015で開発することもできます。

が、どちらにしても単純なサンプルを超えると何かとハマるところが出てきます。

ここではAndroidAppプロジェクトを例として、よく遭遇する注意点を列挙します。

1、AndroidApp.Packagingを改造する場合

操作自体はさほど難しくありませんが、パッケージング用プロジェクトにファイルを入れる場合、参照をリンクするのではなく、ファイルがコピーされる点に注意してください。古いファイルを一度削除し、新しいファイルをAndroidApp.Packaging以下にドラッグ&ドロップするだけでokです

AndroidManifest.xmlを入れ替えた場合のみ、プロパティの中身を確認する必要があります。Package Actionの所には必ずAndroid Manifest Xmlを選んでください(図4)。

image02
(図4)

AndroidApp.Pacagkingのプロパティには幾つかデバッグに関するパラメータがあるため、事前に確認しておくことをお勧めします。

2、antに関して

antを単体で実行することも可能です。AndroidApp\ARM\Debug\Packageに対してファイルの追加/削除処理をおこないたい場合にはant debugでant単体を起動することもあるでしょう。

ただし、少し手間は発生します。

  • まず事前にantのパスを通します。
  • もしbuild.xmlがないというエラーが表示された場合、VSのビルドを一回走らせると自動的に生成されます。
  • そのbuild.xml先頭のほうにを追加し、local.propertiesを作って、sdk.dir=path/to/your/android-sdkを記入します。

image05
(図5)

  • 補足:もしjavaのソースをコンパイルする必要がある場合、ant.propertiesを作って、source.dir=absolute/path/to/java/folderを記入します。
  • ※VS2015 Update 1ではJavaのビルドやデバッグがサポートされるそうです。(2015.11.16時点でRC状態)

3、デバッグに関して

  • Androidツール(Android Device Monitorなど)が起動している最中は、VSのデバッガーを起動できません。VS2015の正式版ではちゃんとlogcatを見れるので、通常は特に他のツールを起動する必要はないはずです。
  • 既に同じパッケージ名のアプリがインストールされている場合、インストール中に出力ウインドウでUnknown Errorが出力されます。その場合はインストール済みのアプリを端末上の全ユーザで一旦アンインストールしたうえでVSから再度転送します。
  • ブレークポイントで処理を止めた際に、変数がoptimized outとして表示される場合があります。コンパイル時のoptimization levelを-O0にすれば、これらはきちんと表示されますが、当然バイナリにリリース版との差分が生じるため、追いかけているバグが発生しなくなるなどの懸念もあります。
  • デバッグシンボルに関して、AndroidApp\ARM\Debug\.gdbにデバッグシンボルが入った.soファイルが存在しないとブレークポイントが効きません。過去のプロジェクトをVS2015に移植する時によく発生します。ビルドイベントを使ってファイルをコピーすれば解決できます。

4.プロジェクトプロパティの設定

  • 一度プロジェクトプロパティを変えるとリビルドが発生します。少しミスするだけでも大きな時間の無駄につながります。
  • 新規プロジェクトを追加した時にConfiguration Typeは.soになっているかどうかを確認します。また、Use of STLに関しては必要に応じて変えるのが無難です。

image00
(図6)

ここまでを一通りおさえておけば、Visual Studio
2015を使ってAndroidアプリ(NDKメイン)を快適に開発できるでしょう。

もし記述に間違いなどを見つけられたら、是非ご指摘をください。

Kyo Shinyuu

↑このページのトップヘ