7日目を担当させていただきます、むろほしです。本投稿は、2015年12月5(土)に行われた【年末だよ】Unity お・と・な のLT大会 2015【ポロりしてもいいのよ】の自分のLTの関連投稿となります。
忙しい人のためのポイント!
以前、ある広告会社様が提供しているAndroid向けUnity広告SDK(Androidネイティブプラグイン)は、単独では正しく動作するけれど、他のネイティブプラグインと混ぜて使うとAndroid実機での実行時にクラッシュしました。
下記にそのような不具合を起こす広告SDK(Androidネイティブプラグイン)のコード例を示します。
例えば、全画面広告を表示するActivity(Androidにおける画面を司るクラス)でこのようなコードを書かれている可能性があります。(下記のコードは、実在の広告会社様のコードではありません。)
package com.xxx_adcompany.ad_plugin;
import android.app.Activity;
/*
中略
*/
/// xxx社様(架空の会社)が提供する全画面広告を表示するActivity例
public class AdActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.xxx_ad_activity); // 具体的にはこの行がダメ!!!!
/*
中略
*/
}
}
ポイントは、上記コード中のR.layout.xxx_ad_activity
のような定数を使ってはいけない、ということです。複数のネイティブプラグインが存在した場合、実機実行時にクラッシュもしくは予期せぬ挙動をする場合があります。(場合によっては、単独でもそのネイティブプラグインがエラーになることもあるかもしれません)
画像・レイアウトXMLなどのリソースを含むネイティブプラグインを作る際は、Javaコード中にRクラスの定数をベタ書きしないでください。
ただしaarという形式を用いるのであれば、Javaコード中にRクラスの定数をベタ書きすることが可能です。Unity5のみを対象としていいのであれば、aarへの移行をお勧めします。
aarを使えないのであれば、問題の箇所をRクラスの定数のを用いる代わりに
// layoutのIDをRクラスの定数で設定しちゃダメ
// これ書いちゃダメ、絶対!
setContentView(R.layout.xxx_ad_activity);
次のようにすることで、不具合を起こさないようにすることもできます。
// getIdentifierメソッドでlayoutのIDを取得して設定
int layoutId = getResources().getIdentifier("xxx_ad_activity", "layout", getPackageName());
setContentView(layoutId);
はじめに
順序が前後しましたが、「はじめに」です。
モバイルゲーム開発において、広告SDK(ネイティブプラグイン)を入れ込むことに対して、「辛い」、「しんどい」というイメージを持つ方もいるのではないでしょうか?
例えば
- iOSで、ある会社のSDKがはいっているとリジェクトされる
- iOSで、アイコン型広告でリジェクトされる
- iOSで、アダルトな広告が審査中に配信されてリジェクトされる
などなどを経験された方も多いのではないでしょうか?iOSでのリジェクト辛いですよね。それ以外にも私は、
- ドキュメントがめちゃめちゃ少ない or テキトー
- Unityプログラマにフレンドリーでない記述のドキュメント
- ドキュメントに記載されているファイルが無い or ファイル名が違う
- あきらかにたりないビルド手順
- 確実に足りないiOSのビルドオプション
- そもそもunitypackageが開けない!
なども体験しました。
一見簡単そうな広告SDKをゲームに入れるという行為、苦労された方も多いのではないでしょうか?
非エンジニアの方、もしエンジニアが広告SDKに苦戦していたら、それは広告SDKのバグやドキュメントの間違いが原因かもしれません。ぜひ、苦労しているエンジニアに優しく接してあげてください。
やっぱりUnityAds最高!
さてそのような自分の体験した広告SDK(ネイティブプラグイン)にまつわる苦労の中で、一番たちが悪く、一番時間を取られ、一番記憶に残っているものを紹介します。それはAndroidにおいて、「単独では動作するのだけれども、他の広告SDKと混ぜると、実機でクラッシュする広告SDK」というものです。
今後本投稿では、広告SDKの代わりにネイティブプラグインという言葉をメインに使います。エラー・クラッシュの原因は広告だけに限定されません。Androidのネイティブプラグイン中のJavaコードのある記述に原因があります。
いつエラー?どういうエラー?
エラーが起こるのは、ActivityのサブクラスなどJavaで記述したネイティブプラグインのコードを呼び出した時です。
どのようなエラーになるかは、どのようなAndroidネイティブプラグイン同士を混ぜるかによります。一例を挙げると、Javaのキャスト失敗例外によるクラッシュです。
大抵はアプリケーションがクラッシュすることがほとんどですが、ただし理論上は、想定と違う画像やテキストが表示される」という不具合も起こり得ると思います。(自分はクラッシュでした。)
ざっくり原因は?
ざっくりとした原因は、
「複数のAndroidネイティブプラグインが存在する場合、レイアウトXMLなどの各種リソースを示すID(定数)が、ネイティブプラグインのjar生成時とUnityのapkビルド時でずれが生じるため」
です。
しっかり原因は?
しっかりと原因を説明します。
この節の説明は、aarを使わないネイティブプラグインでUnity4系を前提としています。
ネイティブプラグインの構成のおさらい
まず、Xxx社という架空の会社が提供するネイティブプラグインがあるとします。
この会社から提供された、ネイティブプラグインは、
- xxx_ad_jar : ActivityなどのJavaコードをビルドしたjarファイル
- xxx_ad_layout.xml など: レイアウトXML、画像などのリソースファイル
- その他、Javaのメソッドを呼び出すC#のラッパー
で構成されているとします。jarファイル、リソースファイルは、Assets/Plugins/Android以下に配置します。
また、AndroidManifest.xmlに、ネイティブプラグイン内のActivityなどを適切に追加・編集し、Assets/Plugins/Androidに置く必要があります。
つぎの画像のような感じですね。
resディレクトリ以下に、layout/xxx_ad_activity.xmlなどのレイアウトファイルやdrawable/header.pngなどの画像が入っています。
ネイティブプラグインの作者様がjarをビルドする時にね
先ほど紹介した、Assets/Plugins/Android以下に置くxxx_ad_pluginというjarファイル。このネイティブプラグイン(広告SDK)の開発者のXxx社が、このjarの元になったJavaコードを次のように書いていた場合、不具合の原因になってしまいます。
package com.xxx_adcompany.ad_plugin;
import android.app.Activity;
/*
中略
*/
/// xxx社様(架空の会社)が提供する全画面広告を表示するActivity例
public class AdActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.xxx_ad_activity); // 具体的にはこの行がダメ!!!!
/*
中略
*/
}
}
ダメなのはR.layout.xxx_ad_activity
という定数を使っている箇所です。
AndroidのRクラスについて補足します。RクラスはAndroidプロジェクト中の画像、文字列、レイアウトXMLなど様々なリソースのIDを持つクラスです。R.drawable、R.string、R.layoutなど、Rクラスのstaticな内部クラスには、public static final int
な定数が定義されています。定数はそれぞれのリソースを対応します。例えば、R.layout.xxx_ad_activityという定数は、xxx_ad_activity.xmlというレイアウトXMLを示すIDです。またRクラスは、Androidプロジェクトのパッケージ名の名前空間に属します。com.xxx_adcompany.ad_plugin
といプロジェクト名ならば、com.xxx_adcompany.ad_plugin.R
です。Rクラスは、自動生成されます。
さて、このRクラスの定数を使っている箇所、具体的には次のコードがいけません。
setContentView(R.layout.xxx_ad_activity); // 具体的にはこの行がダメ!!!!ダメ絶対!
ここで仮に、R.layout.xxx_ad_activity
定数の値が0x7f030000
という値だったとしましょう。(16進整数リテラルです) AdActivity
をビルドしたclassには、Javaの仕様上0x7f030000
が直接埋め込まれます。そしてclassをまとめたjarにも0x7f030000
が直接埋め込まれるのです。
ポイントは、「xxx_ad_plugin.jar
中のxxx_ad_activity.xml
を参照する・必要とするコードでは、0x7f030000
という値が埋め込まれている」ということですよ!
Unityがapkをビルドする時にね
さて話はUnityに戻ります。
UnityはAndroidのビルド成果物であるapkを作る際に一度Androidプロジェクトを作ります。このAndroidプロジェクトの中には、Plugins/Android以下のjarや画像にレイアウトXMLも含まれます。そして、Rクラスも新たに作られます。
さて、ここで複数のネイティブプラグインがある場合を考えます。
今まで、Xxx社の提供している
- xxx_ad_plugin.jar
- xxx_ad_layout.xml
- 他
で構成されているネイティブプラグインがありましたね。ここに
- abc_ad_plugin.jar
- abc_ad_layout.xml
- 他
で、構成されているABC社のネイティブプラグインが加わったとしましょう。
この状況でUnityプロジェクトをビルドした場合、Unityが新たにつくるAndroidプロジェクトのRクラスは、次のようになります。
package com.mrstar.my_game;
public final class R {
/* 中略 */
public static final class layout {
public static final int abc_ad_lyout=0x7f030000;
public static final int xxx_ad_layout=0x7f030001;
}
/* 中略 */
}
気がつきましたか?
xxx_ad_layout
というレイアウトXMLには、0x7f030001
という値が振られましたね。(16進請数リテラル)
前の節のポイントを思い出してください。 「xxx_ad_plugin.jar
中のxxx_ad_activity.xml
を参照する・必要とするコードでは、0x7f030000
という値が埋め込まれている」でしたね。
このように、xxx_ad_layout.xmlというレイアウトファイルに対して、jarの中では0x7f030000
という値が使われていますが、実際のUnityでは0x7f030001
が割り振られてしまいました。0x7f030000
にはabc_ad_lyout.xml
という違うプラグインのレイアウトファイルが割り振られてしまいました。
こんな感じでIDがずれるのけど
「複数のAndroidネイティブプラグインが存在する場合、レイアウトXMLなどの各種リソースを示すID(定数)が、ネイティブプラグインのjar生成時とUnityのapkビルド時でずれが生じるため」
の、意味を理解していただけたでしょうか?このずれが原因となり、クラッシュを引き起こします。xxx_ad_activityというレイアウトを期待している箇所で、全く違うレイアウトが来てしまうのですから、クラッシュするのも当然ですよね。キャスト失敗などが起こるのが想像つきますでしょうか。
何が悪いのか?それはネイティブプラグインのjarの元になるJavaコード中で、
setContentView(R.layout.xxx_ad_activity); // 具体的にはこの行がダメ!!!!ダメ絶対!
のように、Rクラスの定数をベタ書きして、定数リテラルを埋め込むことです。jar生成時とUnityビルド時でリソースのIDのずれが生じて、それがエラー・クラッシュにつながります。
さて、今まで「複数のネイティブプラグインがある場合」といってきましたが、このようにRの定数をJavaコードにベタ書きしたのであれば、場合によっては単独でもエラーになりえることに注意してください。
また、aarを使わずこのようなjar・リソースファイルを用いる方法は、Unity5では次のような警告が出ます。
OBSOLETE - Providing Android resources in Assets/Plugins/Android/res is deprecated, please move your resources to an Android Library. See "Building Plugins for Android" section of the Manual.
UnityEditor.BuildPlayerWindow:BuildPlayerAndRun()
じゃあどうすればいいの?
Rクラスで定数をベタ書きしないで、リソースを用いるコードを記述することができます。ネイティブプラグインのjarを生成するためのJavaコードでは、
package com.xxx_adcompany.ad_plugin;
import android.app.Activity;
/*
中略
*/
/// xxx社様(架空の会社)が提供する全画面広告を表示するActivity例
public class AdActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// ポイントはここ
int layoutId = getResources().getIdentifier("xxx_ad_activity", "layout", getPackageName());
setContentView(layoutId);
/*
中略
*/
}
}
ようにすればOKです。
ResourcesクラスのgetIdentifierメソッドを用いて、idを取得しています。このメソッドを用いれば、複数のネイティブプラグインが統合された後のリソースIDも正しく取得することが可能です。
また、Unity5以降では、aarを使うことがオススメです。
さいごに
もし今後、あなたがUnityのAndroidネイティブプラグインを作ることになったら、まずaarを使って作ることを検討してください。
aarは使うことができず、jar+リソースファイルで作る必要があるのならば、ネイティブプラグインのJavaのコードにはRクラスの定数はベタ書きしないでください。ResourcesクラスのgetIdentifierメソッドを用いて、idを取得してください。
おまけ
自分は、ゲームを作ることが好きです。ゲームで遊んでもらうのが大好きです。
ただ、それだけではご飯はたべられません。お金を稼がなくてはいけません。そのために広告が大切なのはわかっています。
だけれども、どうせ徹夜するなら広告SDK(ネイティブプラグイン)の不具合に対処するためでなく、ゲームを楽しくするために徹夜をしたいです。
今回紹介した原因のネイティブプラグインの不具合この世からなくなりますように。
それからJavaでSDKを提供する人だけでなく、Javaを書く人はEffective Java読もうな!約束だぜ!!!