2011/12/24

Titanium の R.java と special platform files (Androidのみ)

だいぶ前に TITANIUM BBS(JP UNOFFICIAL) で投稿があった Titanium Mobile のアーキテクチャ(ROOT/platform/ ディレクトリの処理) の件がわかったのでまとめておきます。

Android アプリケーションのビルドは、Google の公式のドキュメントでは A Detailed Look at the Build Process にあるようなデータフローになっています。Titanium のビルドシステムはこのフローを土台にしていますが、 R.java の取り扱い方が若干特殊になっています。というのも、 Titanium では、 R.java に含めるべきリソースと含めないリソースが存在するからです。前者は主にスプラッシュスクリーンやアイコン用の画像で、 Android アプリとして必須のもので、特に Android OS から利用できるようにするため、 R.java に含めなくてはなりません。それに対して、 Titanium の JavaScript からしか参照されないリソースは、 R.java に含める必要がありません。ということで、 Titanium において、ユーザ定義のリソースは、プロジェクト直下の Resouces/android/ 内に保存しておくと、出来上がった apk の所定の場所にコピーされるのみの処理がされます。


普通はこれで十分なのですが、用途によっては、Titanium のユーザが R.java にリソースを追加したい場合があります(実際これで四苦八苦してましたw)。

そこで登場するのが "special platform files" と呼ばれるものですが、例によって、これについてのドキュメントはありません。android 用の build.py の中に登場します。
special_resources_dir = os.path.join(self.top_dir,'platform','android')
if os.path.exists(special_resources_dir):
debug("found special platform files dir = %s" % special_resources_dir)
ignore_files = ignoreFiles
ignore_files.extend(['AndroidManifest.xml']) # don't want to overwrite build/android/AndroidManifest.xml yet
self.copy_project_platform_folder(ignoreDirs, ignore_files)
要するに、プロジェクト直下の
/platform/android/res/
以下にリソースとなるファイルを置いておくと、 Titanium が R.java を生成する際に使う
/build/android/res/
以下にコピーされます。

注意点としては R.java 生成のお約束として、リソースファイルはユーザが作成したディレクトリに入れておかないといけないことです。 /platform/android/ res/直下にファイルを置くと、Android ツールの aapt がエラーをなり、 Titanium のビルドが停止します。

これで、心置きなくR.javaにリソースを追加できます。


Titanium pluginの開発

module ではなくて、 plugin についての話です。
TitaniumSDKのディレクトリをのぞいていると、plugin というようなディレクトリやplugin.py というような Python ファイルが見つかります。 Appcelerator の Web ページを見ても特にこれについて説明がないようです。ほとんど情報がありません。.... ただ CoffeeScript の Titanium 用 plugin というのがあって、実際この仕組みで作られています。現状 Titanium の plugin は CoffeeScript のためだけにあるということができます。


module 作成と同じように、titanium.py で plugin プロジェクトが作成できます。 module を作成したことがあれば、自分の .bash_profile に titanium.py のエイリアスの設定をしていると思います。これがすんでいるものとして話を進めます。
$ titanium help create
Appcelerator Titanium
Copyright (c) 2010-2011 by Appcelerator, Inc.


Usage: titanium.py create [--platform=p] [--type=t] [--dir=d] [--name=n] [--id=i] [--ver=v]
--platform=p1,p2 platform: iphone, ipad, android, blackberry, etc.
--type=t type of project: project, module, plugin
--dir=d directory to create the new project
--name=n project name
--id=i project id (ie com.companyName.project
--ver=i platform version
--android=sdk_folder For android module - the Android SDK folder
help で create コマンドの情報を見ると、確かに type に plugin を指定できることがわかります。
最小限必要なオプションで、作成してみます。
$ titanium create --type=plugin --name=myplugin --id=jp.isisredirect.myplugin
Created plugin project
確かに plugin プロジェクトが jp.isisredirect.myplugin ディレクトリとして作成されました。
$ ls -l jp.isisredirect.myplugin
total 40
-rw-r--r-- 1 <username> staff 77 12 24 11:56 LICENSE
-rw-r--r-- 1 <username> staff 681 12 24 11:56 README
-rwxr-xr-x 1 <username> staff 3034 12 24 11:56 build.py
-rw-r--r-- 1 <username> staff 363 12 24 11:56 manifest
-rwxr-xr-x 1<username> staff 185 12 24 11:56 plugin.py
生成された build.py が plugin を作成するスクリプトになります。 jp.isisredirect.mypluginディレクトリにcdして、build.py を実行してみます。
$ ./build.py
[WARN] please update the manifest key: 'copyright' to a non-default value
[WARN] please update the manifest key: 'license' to a non-default value
[WARN] please update the LICENSE file with your license text before distributingPlugin
packaged at jp.isisredirect.myplugin-0.1.zip

manifst や LICENSE ファイルを修正していないのでしかられますが、ともかく jp.isisredirect.myplugin-0.1.zip として、plugin が作成されました。

README ファイルによれば、この zip ファイルを、module と同様、TitaniumSDK ディレクトリに置くか、plugin.py を Titanium プロジェクトディレクトリの plugins/jp.isisredirect. myplugin/ におけば、ビルドシステムに認識されます。

そして Titanium プロジェクトで実行されるようにするには、module と同じように、tiapp.xml に
<plugins>
   <plugin version="0.1">jp.isisredirect.myplugin</plugin>
</plugins>
というpluginsタグとpluginタグを追加してやります。

これで、Titanium プロジェクトをビルドするたびに、plugin.py が実行されることになります。

さて、実行されるといっても、plugin.py になにも修正を加えていないので、以前と何の変わりもありません。しかし、どういう修正が必要なのか、何が許されるのか、はたまたどのタイミングで実行されるか、どこにも情報がありません。ということで、モバイルアプリ向けの build.py の中を探ることにします( iPhone 用と Android 用の build.py は当然違ったものになっていますが、plugin 周りの実装はほぼ共通です)。Titanium プロジェクトのビルドのはじめの方で、以下のように plugin.py が使われています。
for plugin in ti.properties['plugins']:
local_plugin_file = os.path.join(local_compiler_dir,plugin['name'],'plugin.py')
plugin_file = os.path.join(tp_compiler_dir,plugin['name'],plugin['version'],'plugin.py')
if not os.path.exists(local_plugin_file) and not os.path.exists(plugin_file):
o.write("+ Missing plugin at %s (checked %s also)\n" % (plugin_file,local_plugin_file))
print "[ERROR] Build Failed (Missing plugin for %s). Please see output for more details" % plugin['name']
sys.stdout.flush()
sys.exit(1)
o.write("+ Detected plugin: %s/%s\n" % (plugin['name'],plugin['version']))
print "[INFO] Detected compiler plugin: %s/%s" % (plugin['name'],plugin['version'])
code_path = plugin_file
if os.path.exists(local_plugin_file):
code_path = local_plugin_file
o.write("+ Loading compiler plugin at %s\n" % code_path)
compiler_config['plugin']=plugin
fin = open(code_path, 'rb')
m = hashlib.md5()
m.update(open(code_path,'rb').read())
code_hash = m.hexdigest()
p = imp.load_source(code_hash, code_path, fin)
p.compile(compiler_config)
fin.close()
tiapp.xml にある <plugins> の <plugin> の情報をもとに、起動すべき plugin.py を選び、起動可能でなければ、”Build Failed (Missing plugin” のエラーをはき、起動可能であれば、plugin.py 内の compile 関数を呼びます。
これ以外の箇所で、plugin.py の参照はないので、Titanium の plugin は完全にビルドのプリプロセスとして働くことがわかります。

titanium.py で作成直後の plugin.py は
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Titanium Compiler plugin
# jp.isisredirect.myplugin
#
def compile(config):
 print "[INFO] jp.isisredirect.myplugin plugin loaded"
というように、ほとんど空の compile 関数が生成されています。ここに必要な処理を書いていくことになります。
引数の config には、呼び出し元で
(ios用を引用。android用とは構成も若干異なっています)
compiler_config = {
 'platform':'ios',
'devicefamily':devicefamily,
'simtype':simtype,
'tiapp':ti,
'project_dir':project_dir,
'titanium_dir':titanium_dir,
'appid':appid,
'iphone_version':iphone_version,
'template_dir':template_dir,
'project_name':name,
'command':command,
'deploytype':deploytype,
'build_dir':build_dir,
'app_name':app_name,
'app_dir':app_dir,
'iphone_dir':iphone_dir
}
という具合に設定されています。それぞれの値の意味は、build.py をじっくり読んでいただくことにして、ともかくこの引数の値で、ビルドのいろいろな状況に応じた処理が可能なのがわかります。


Titanium の plugin は、ビルドのプリプロセスとして働くので、ビルド前に独自のファイルの整合性チェックや、コードやリソースの前処理などを行うことができるでしょう。CoffeeScript の plugin が今のところ最大の例ですので、これを読みながら研究するのがよいと思われます。



2011/12/17

【Titanium Advent Calendar 2011:十七日目】ダイエットしてますか?iOS編

このエントリーは、@astronaughtsさんの企画された「Titanium Advent Calendar 2011」の17日目エントリーとして参加しています。

Titanium Mobileで作ったアプリの問題に、ファイルサイズが大きいことがあります。iOS の場合ちょっとしたアプリでも3MB越えが普通です。これをなんとかしようというのが今回の話です。
いつものように、D.I.Y. & On Your Risk な話なので、実施にあたっては、各自の責任で行ってください。以下の実施例は、Titanium SDK 1.7.5 、XCode 4.2、iOS SDK 5を使っています。


Ti のビルドシステムそれなりに賢いことをしてくれていることを最初にいっておきましょう。キーになるのは、Ti SDK ディレクトリ /Classes/ に含まれる defines.h です。
このファイルは、Simulatorでビルドする場合には、そのままコピーされますが、"Install - iOS Device"や"Deploy Distribute - App Store"の場合、JavaScriptのソースファイルから使用された内部モジュールの情報を収集して、生成されます。


元のdefines.hファイル
(前略)
#define USE_TI_STREAM
#define USE_TI_CODEC
#define USE_TI_UTILS
#define USE_TI_XML
#define USE_TI_ACCELEROMETER
#define USE_TI_API
#define USE_TI_APP
#define USE_TI_CONTACTS
#define USE_TI_DATABASE
(以下略)


Tiビルドシステムが生成したdefines.hファイル
// Warning: this is generated file. Do not modify!
#define TI_VERSION 1.7.5
#define USE_TI_ANALYTICS 1
#define USE_TI_NETWORK 1
#define USE_TI_PLATFORM 1
#define USE_TI_UI 1
#define USE_TI_API 1
#define USE_TI_UISETBACKGROUNDCOLOR 1
#define USE_TI_UITABGROUP 1
#define USE_TI_UIWINDOW 1
#define USE_TI_UITAB 1
#define USE_TI_UILABEL 1 
上の例は、TiStudioがプロジェクト生成時に自動生成されるapp.jsだけの状況でビルドした場合の例です。対応するObjective-Cのソースで、これらのマクロで展開されるコードを切り替える仕組みになっています。よく整理されているようです。
#define TI_VERSION 1.7.5
から
#define USE_TI_API 1
までの行は、有無をいわさず生成されますが、それ以降の部分は、JavaScriptによって何が生成されるか決まってきます。


ビルドするアーキテクチャが違っているので、一概に比較できませんが、"Run As > iPhone Simulator" "Debug As > Debug Simulator" で作られる Debug-iphonesimulatorが 8M であるのに対して、"Deploy Distribute - App Store" で作られるRelease-iphoneosが 5.5M 程度になっています。(もちろん、オーガナイザーでipaに変換した後は、zip圧縮されるのでさらに小さくなります。概ね3.5M)。


Tiはdefines.h をつかって開発者の書いたJavaScriptソースファイルが使っているモジュールを特定して、C言語マクロ USE_TI_XXXXXXX を通じて、コードの削減を行っています。Tiの生成するXCodeプロジェクトはコードに関しては、呼ばれないコードは除外するオプションがついているので、使用する機能が少なければより小さなコードを生成することとなります。
そして、賢いことに、リソースに関しても、必要なものだけ含むようにしてくれているようです。シミュレータ用には、 facebook 用の画像ファイルが、必ず含まれてしまいますが、実機用に関しては、facebook関連のコードがなければ、含まれません。

Tiが賢いことをやってくれていても、まだ大きい、どうにかしたいと思うのが人情です。

Tiの生成するXCodeプロジェクトのビルド対象のアーキテクチャはARMv6+ARMv7となっていて、2つの種類のコードが含まれています。
アーキテクチャによって機種を分類すると
ARMv6:
    1st & 2nd Generation iPod Touch
    iPhone,
    iPhone 3G
ARMv7:
    iPhone 3GS
    iPhone 4
    iPhone 4S
    iPad
    iPad2
    3rd & 4th Generation iPod Touch
となりますが、Tiはそれぞれのアーキテクチャに対して最適なコードを別々に生成しているため、全体のアプリのサイズを大きくしています。そこで
  1. 対象をARMv6アーキテクチャにして、ARMv6,ARMv7で動くようにする(ただしARMv7での最適化がなされない)
  2. 対象をARMv7アーキテクチャに限る
のいずれかの選択をすると、1アーキテクチャになる分アプリサイズが小さくなります。
ビルド後にlipoなどのコマンドラインツールを使ってやる方法もありますが、よりお手軽にするには、XCodeのプロジェクト設定を変える方法があります(細かい手順は省略。実施にあたってはあらかじめオリジナルのバックアップをお忘れなく)。
実際試してみると、
ARMv6のみを選択の*.appサイズ
Debug-iphoneos 3.8M
Release-iphoneos 3.6M
ARMv7のみを選択の*.appサイズ
Debug-iphoneos 3.8M
Release-iphoneos 3.6M
という具合に、かなりサイズ削減できます(ともに同じくらいのサイズになります)。
この削減方法は、Mac OS のユニーバーサルバイナリにまつわるアプリサイズ削減方法と原理は同じです。
さらに、これ以上サイズを削減するには、Titaniumの中心 TiCore(libTiCore.a)に手を入れる必要がでてきそうですが、これは大事(ダイエットどころか、大手術)になりそうなので、今日はここまで...


無関係な小ネタ
PS
最後に、本当はAndroidのほうがアプリケーションサイズについて問題になるケースがあるので、こちらについても書きたかったのですが、V8ベースのv1.8リリースを間近にひかえて、状況が変わってしまう可能性があるので、こちらについてはまた後日ということで...