ナノカ技術メモ

ただのエンジニア。何でも屋みたいな扱い受けてます。

Unityでアプリ容量削減。700MB→179MB

はじめに

先日、Unityで簡単なiOSアプリを作りリリースしたのですが、Archiveでipaを作成したら130MB近くあったのでびっくり...
ちなみにiOSではAppStoreに公開して100MBを超えているとWifiに接続しないとダウンロードできません。
簡単なゲームのアプリなのでWifiに繋いでまで入れるものではないし、このままではインストールの壁が高い...
なんとしても容量削減しないといけないのでそのときやったことを書きます。

ipaのサイズ:130MB
iPhoneでの容量:700MB
iOSではインストール時はipaに圧縮されており、インストール後はappとして展開されます。

検証結果

Textureサイズを適切なものに変更

・容量にはほぼ変化なしだがメモリ削減に効果あり。
docs.unity3d.com


UnityのProjectから画像素材を選択するとInspectorに下のような画面が表示されます。
そこで画像のサイズとクオリティを設定できます。

デフォルトでは2048という無駄に大きいサイズになっているはずなので画像素材のサイズより大きい適切なものに変更しましょう。(下では200x200pxの素材を256に変更)
f:id:nanokanato:20170605142542p:plain:w300

素材の容量を削減

・容量にやや効果ありだが、やりすぎると劣化します
  ・ipaのサイズ:130MB→124MB
  ・iPhoneでの容量:700MB→680MB
tinyjpg.com

今回の場合、容量の原因は主に画像素材でした。
なので画像素材自体を軽くしてみました。

素材によっては大幅に削減されます。
劣化した場合はそのまま使いました。

AssetBundle

iPhoneでの容量に大幅に効果あり
  ・ipaのサイズ:124MB→116MB
  ・iPhoneでの容量:680MB→179MB
docs.unity3d.com

今回の場合、連番画像などが多くなっており、それをResources.Load()で取得しておりました。
しかし、Resourcesフォルダの多用はビルド時に使用している使用していないに関係なく無圧縮でビルドされることがわかりました。
また、画像素材の他にもビルド時に生成されたものがあり容量的によくないようです。
AssetBundleはUnityで使う素材を圧縮したもので、アプリで使用する時だけ解凍されます。

Resourcesをやめ、フォルダ名をResourceに変更しました。(フォルダ名はなんでも良い)
Resourceフォルダを選択すると、下にAssetBundle名を登録できますので自由につけました。(デフォルトはNone)
※グループごとにフォルダを分けている場合でもフォルダ以下をAssetBundleにできますが、個別の方がいいでしょう。
f:id:nanokanato:20170605145759p:plain:w300

以下のコードをUnityのProjectの「Assets/Editor」に追加します。

using UnityEngine;
using System.Collections;
using UnityEditor;
using System.IO;

public class ExportAssetbundle  {

	[MenuItem("Export/AssetBundle/iOS")]
	static void iOS_Export() {
		Directory.CreateDirectory (Application.streamingAssetsPath);
		BuildPipeline.BuildAssetBundles(Application.streamingAssetsPath+"/iOS", BuildAssetBundleOptions.ChunkBasedCompression, BuildTarget.iOS);
	}

	[MenuItem("Export/AssetBundle/Android")]
	static void Android_Export() {
		Directory.CreateDirectory (Application.streamingAssetsPath);
		BuildPipeline.BuildAssetBundles(Application.streamingAssetsPath+"/Android", BuildAssetBundleOptions.ChunkBasedCompression, BuildTarget.Android);
	}
}

追加するとUnityのメニューに「Export/AssetBundle」が追加されます。
f:id:nanokanato:20170605162212p:plain:w300

UnityのProjectのAssetsに「StreamingAssets/iOS」を追加してメニューのiOSを選択するとAssetBundleの作成が開始されます。
作成されたAssetBundleは「Assets/StreamingAssets/iOS」に追加されます。

AssetBundleの読み込みはAssetBundle.LoadFromFile()メソッドで読み込めます。
しかし、AssetBundleは1度しか読み込めません(読み込むとエラーが出ます)
なので自分は以下のようにstaticのメソッドで一度読み込んだらキャッシュから読み出すようにしています。
AssetBundleが読み込めない時は、上の手順では「Assets/StreamingAssets/iOS」にAssetBundleを作成したので問題はないはずですがLoadFromFile()内のパスを確認してください。

private static Dictionary<string, AssetBundle> assetBundleCache = new Dictionary<string, AssetBundle>();
private static AssetBundle readAssetBundleAssetBundle(string key) {
	AssetBundle assetBundle = null;
	if (!string.IsNullOrEmpty(key)) {
		if (assetBundleCache != null) {
			if (assetBundleCache.ContainsKey (key)) {
				assetBundle = assetBundleCache [key];
			}
		}
		if (assetBundle == null) {
			assetBundle = AssetBundle.LoadFromFile(Application.streamingAssetsPath+"/iOS/"+key);
			if (assetBundleCache.ContainsKey(key)) {
				assetBundleCache[key] = assetBundle;
			} else {
				assetBundleCache.Add(key, assetBundle);
			}
		}
	}
	return assetBundle;
}

読み込んだAssetBundleから素材を取り出すにはLoadAsset()を使用します。
上のreadAssetBundleAssetBundleメソッドを使ってAssetBundleを取得した後、LoadAssetでSpriteを取得しています。
Resouces.Loadでは「Resouces/Chara/chara_1.png」を読み込む場合、"Chara/chara_1.png"を引数としていましたが、
LoadAssetではフォルダ階層を無視し、拡張子が必要です。"chara_1.png"が引数になります。

AssetBundle assetBundle = readAssetBundleAssetBundle(AssetBundle名);
if (assetBundle != null) {
	sprite = assetBundle.LoadAsset<Sprite>(素材ファイル名);
}

AssetBundleで素材を読み込むのならAssetBundle化した素材たちはプロジェクトの外で管理しておきましょう。

ダウンロードで素材を取得

・ダウンロード時のサイズに効果あり
  ・ipaのサイズ:124MB→116MB
  ・iPhoneでの容量:680MB→179MB

上の3つの対策をしてiPhoneでの容量は大幅に減らせましたが、ipaのサイズが100MBを超えているため素材は初回にダウンロードすることにしました。
このアプリでは一応、設定値などを起動時に通信していたのでその通信にAssetBundleも含めたいと思います。
サーバーにはAssetBundleのパスと最終更新日を用意し、最終更新日がアプリ内の最終更新日付より新しくなったら再取得するようにしました。

作成したAssetBundleにはフォルダに登録したAssetBundle名のファイル以外に「.manifest」や「iOS」「iOS.manifest」などが作成されますが削除してビルドしても実機で動かす際には問題なかったので使用しませんでした。

AssetBundleの取得はWWWクラスで行います。
WWWクラスの引数assetBundleがあれば取得成功なので通信結果であるByte配列をローカルに保存します。
※引数assetBundleはAssetBundle.LoadFromFile()と同じく1度しか取得されないようです。
 取得したらキャッシュとしてどこかに保持しましょう。

using (WWW data_www = new WWW (サーバーに設置したAssetBundleのURL)) {
	yield return data_www;
	
	//通信結果を取得
	if (string.IsNullOrEmpty (data_www.error)) {
		//通信成功
		AssetBundle assetBundle = data_www.assetBundle;
		if (assetBundle != null) {
			//AssetBundleの取得成功、保存
			System.IO.File.Delete(AssetBundleを保存するローカルのパス);
			System.IO.File.WriteAllBytes(AssetBundleを保存するローカルのパス, data_www.bytes);
		}
	}
}

前回起動時にサーバー通信でByte配列を保存した後はローカルパスをWWWで通信することで取得できます。
サーバー通信後にローカルパスからも取得しようとすると同じAssetBundleを2回取得しようとしていることになるためエラーになります。

結論

・画像は256色にする
・Resourcesフォルダの多用はしない
・それでも重い時はAssetBundle化
ipaの容量が100MB超えたらサーバーからダウンロードさせる

AssetBundleはPrefabなども可能なのでサーバーがあればアプリを更新せずにレイアウトの変更も可能に!