プロプロセッサの使い方
現在のJavaでは資源の節約があまり意識されなくなってきていますが、
今でも少ない資源(メモリ・CPU・VRAM等のリソース)の下でJavaを使ったプログラムのニーズはあります。
例えば、今では誰もが持っている携帯電話や家電製品等の組み込み系がそれに当たります。
ここでは、携帯電話でのJavaアプリケーションを例にとって、リソースが少ない環境下で、以前に私が行ったJavaとは言いがたいドケチ・プログラミングの一例を紹介します。
関連記事:
携帯電話のスペック
下記のURLに、NTTDoCoMoから発売されている携帯電話のスペックが載っています。
下部へ行くにつれて新しくなるのですが、この記事を書いている時点での最高は、アプリサイズが1MB(ストレージエリア込み)でJavaで使用可能なヒープは15MBです。
これをハイスペックと思うか、ロースペックと思うかは人それぞれだと思いますが、私はかなりハイスペックだと思います。
逆に下記URLの上部は古い機種で、最低はアプリサイズが10KB(ストレージエリアが別に10K存在)でJavaで使用可能なヒープは96KBです。
Javaの実行環境において、これをハイスペックと思う人は恐らくいないと思います。
http://www.nttdocomo.co.jp/service/imode/make/content/spec/iappli/index.html
プログラムサイズについて考える
では、このロースペックの環境の時にどうやってコーディングしていけばいいのでしょうか?
ヒープの少なさは何とかなるかもしれませんが、プログラムとストレージのサイズ問題は、一般的なJavaプログラミングのコーディング規約を守って記述していると、
要件を満たす前にプログラムサイズがオーバーして、どうにもならなくなってしまいます。深刻な問題です。
現在のように携帯アプリサイズが1MBあっても、この問題は出てくると思いますが、10KB以内という環境下だと、ほぼ間違いなく遭遇する問題です。
携帯に限らずJavaアプリは、最終的にはclassファイルをjarファイルにします。
例えば、以下のような文字列をプログラムの中に埋め込んで作成していくと、いくらjar圧縮がかかっても文字列ベースのアプリだと、
あっという間にファイルサイズが膨れ上がります。
String messageUnsupport = "ご利用の端末ではこの機能を使用することができません。n"; message += "大変申し訳ございませんが、買い換えてください。"; String messageException = "システムエラーが発生しました。n"; message += "もう一度最初からお試しください。";
当たり前ですが、デバックメッセージも最後は消さないとサイズが膨れ上がります。こういうのは、コンパイラの最適化で上手く処理されるかもしれません。
しかし処理されない場合は、消さないとそのままclassファイルにデバッグメッセージが残ってしまいます。
if(debug) { System.out.println("[DEBUG] ****************************"); }
サイズがギリギリのラインで開発してると、デバック文すら埋め込めなくなります。厄介なことです。
他にも
- クラス名が長い
- クラスが複数個存在する
- メソッドが多い
- 変数が多い
- Stringクラスを+で結合する
このようなことが原因となり、プログラムファイルサイズはどんどん大きくなります。
では、上記のような肥大要因に対処するために
- クラス名はAやBやCなど1文字
- クラスは2個(最低2個は必要)
- メソッドは1文字
- メソッドは極力作らない
- 変数は極力使わずマジックナンバーだけ
- アクセス修飾子はメンバ変数も全てpublicのみ
- Stringクラスを+で結合しない
といったルールでコーディングをしたらどうなるでしょうか?書いている時は理解できても、後から修正しようと思ったときに自分自身が理解できなくなりそうです。
Javaで使うCプリプロセッサ
そこで、当時恐らく殆どのアプリ開発者が使っていたのがC言語のプリプロセッサです。
メジャーなのが、以下のプリプロセッサでしょうか。
- cpp32(Borland)
- cl(Microsoft)
- gcc(GNU)
- ※cpp32以外はプリプロセッサではないのですが、clは/EP、gccは-E -Pオプションでプリプロセスを行うことができます
これらのツールを使うと、JavaソースコードにC言語構文のマクロや定数を埋め込み、それを展開できます。
例えば、以下のようなコードの一部分があるとします。
final int MAX_DATA_LENGTH = 1024 * 32; for(int i = 0; i < MAX_DATA_LENGTH; i++) { // 処理 }
このように記述したくても、final intの宣言で、クラスファイルに変数1つ分の領域が含まれるのが嫌だ!ということで通常は次のように記述することになります。
for(int i = 0; i < (1024 * 32); i++) { // 処理 }
しかし、これではマジックナンバーがソースに含まれてしまいます。もし複数個所にこの定数値を埋め込んだ場合には、値を変更するときが大変です。変更漏れもあるでしょう。
このように書かざるを得ない場合にでも、プリプロセッサを使えば次のように記述することができます。
#define MAX_DATA_LENGTH (1024 * 32) for(int i = 0; i < MAX_DATA_LENGTH; i++) { // 処理 }
このソースにプリプロセッサを通した後は次のようになります。
for(int i = 0; i < (1024 * 32); i++) { // 処理 }
こうすることで、どのプログラム言語でも忌み嫌われるマジックナンバーを使うこと無く、さらに変数を使わずに定数を表現できます。
同じ型の変数が大量にある場合は、それを配列にすることでプログラムサイズは小さくなります。
例えば以下のようなソースがあったとします。
SomethingObject.java
class SomethingObject { private int offsetX1; private int offsetY1; private int offsetX2; private int offsetY2; private int drawX1; private int drawX2; private int drawX3; private int drawX4; private int drawY1; private int drawY2; private int drawY3; private int drawY4; public void init() { drawX1 = 100; drawY1 = 200; offsetX1 = 21; offsetY2 = 15; .... } }
これを、次のようなヘッダファイルを準備してプリプロセッサを使ったコードに直します。
SomethingObject.h
#define MAX_INT_VAR (12)
#define INT_VAR_ARRAY iva
#define offsetX1 INT_VAR_ARRAY[0]
#define offsetY1 INT_VAR_ARRAY[1]
#define offsetX2 INT_VAR_ARRAY[2]
#define offsetY2 INT_VAR_ARRAY[3]
#define drawX1 INT_VAR_ARRAY[4]
#define drawX2 INT_VAR_ARRAY[5]
#define drawX3 INT_VAR_ARRAY[6]
#define drawX4 INT_VAR_ARRAY[7]
#define drawY1 INT_VAR_ARRAY[8]
#define drawY2 INT_VAR_ARRAY[9]
#define drawY3 INT_VAR_ARRAY[10]
#define drawY4 INT_VAR_ARRAY[11]
SomethingObject.java
#include "SomethingObject.h" class SomethingObject { int [] INT_VAR_ARRAY = new int[MAX_INT_VAR]; public void init() { drawX1 = 100; drawY1 = 200; offsetX1 = 21; offsetY2 = 15; .... } }
こうしてしまえば、プリプロセッサを通った後は次のようになります。
SomethingObject.java
class SomethingObject { int [] iva = new int[(12)]; public void init() { iva[4] = 100; iva[8] = 200; iva[0] = 21; iva[3] = 15; .... } }
これなら、javacでコンパイル可能な状態になります。
変数が全て1つの配列になって、変数の数が一気に減りました。この方法は、同じ型の変数が多ければ多いほど効果を発揮します。
これで、数十バイト~数キロ縮みます。
また、ソースの見栄えも殆ど変わらないので可読性は失われません。
また、プリプロセッサを使うと特定の機種向けモジュールを作る際にもソースを分けることなく、共通のヘッダファイルで
以下のように記述することで、1ソースで管理することも可能です。(これがいいことか悪いことかは別として)
#ifdef F504 #define SLEEP(s) try {Thread.sleep(s);}catch(Exception) {} #else #define SLEEP(s) #endif #ifdef QVGA #define WIDTH 240 #else #define WIDTH 120 #endif
プリプロセッサによる弊害
プリプロセッサを使用するということは、ピュアなJavaソースコードではありません。
そうすると、Java開発でよく使われるEclipseではコーディングできなくなってしまいます。
使おうとしても、以下の画像のように、エラーの嵐となってしまいます。
また、プリプロセッサを通してコンパイルしていると、実際のJavaソースになったものを読まなければいけないときに大変になります。
例えば、以下の似非Javaソースコードをプリプロセッサを通すと・・・
#define A_FLG (1) #define B_FLG (A_FLG<<1) #define C_FLG (B_FLG<<1) #define D_FLG (C_FLG<<1) #define E_FLG (D_FLG<<1) #define F_FLG (E_FLG<<1) #define G_FLG (F_FLG<<1) #define H_FLG (G_FLG<<1) #define FLAG_ON(i,flg) i|=flg #define FLAG_OFF(i,flg) i&=~flg #define CHECK_FLAG(i,flg) ((i&flg)!=0) #define MAKE_RGB(r,g,b) (((r&0xFF)<<16)|((g&0xFF)<<8)|((b&0xFF))) public class test { public static void main(String[] args){ int flag = 0; int rgb = 0; int r=0,g=0,b=0; FLAG_ON(flag, A_FLG); FLAG_ON(flag, D_FLG); FLAG_ON(flag, H_FLG); FLAG_OFF(flag, D_FLG); if(CHECK_FLAG(flag, A_FLG)) { System.out.println("A bit on"); } if(CHECK_FLAG(flag, B_FLG)) { System.out.println("B bit on"); } if(CHECK_FLAG(flag, C_FLG)) { System.out.println("C bit on"); } if(CHECK_FLAG(flag, D_FLG)) { System.out.println("D bit on"); } if(CHECK_FLAG(flag, E_FLG)) { System.out.println("E bit on"); } if(CHECK_FLAG(flag, F_FLG)) { System.out.println("F bit on"); } if(CHECK_FLAG(flag, G_FLG)) { System.out.println("G bit on"); } if(CHECK_FLAG(flag, H_FLG)) { System.out.println("H bit on"); } // RGB作成 rgb = MAKE_RGB(0xFF, 0xCF, 0x73); System.out.println(Integer.toHexString(rgb).toUpperCase()); } }
次のようになります。
public class test { public static void main(String[] args){ int flag = 0; int rgb = 0; int r=0,g=0,b=0; flag|=(1); flag|=((((1)<<1)<<1)<<1); flag|=((((((((1)<<1)<<1)<<1)<<1)<<1)<<1)<<1); flag&=~((((1)<<1)<<1)<<1); if(((flag&(1))!=0)) { System.out.println("A bit on"); } if(((flag&((1)<<1))!=0)) { System.out.println("B bit on"); } if(((flag&(((1)<<1)<<1))!=0)) { System.out.println("C bit on"); } if(((flag&((((1)<<1)<<1)<<1))!=0)) { System.out.println("D bit on"); } if(((flag&(((((1)<<1)<<1)<<1)<<1))!=0)) { System.out.println("E bit on"); } if(((flag&((((((1)<<1)<<1)<<1)<<1)<<1))!=0)) { System.out.println("F bit on"); } if(((flag&(((((((1)<<1)<<1)<<1)<<1)<<1)<<1))!=0)) { System.out.println("G bit on"); } if(((flag&((((((((1)<<1)<<1)<<1)<<1)<<1)<<1)<<1))!=0)) { System.out.println("H bit on"); } rgb = (((0xFF&0xFF)<<16)|((0xCF&0xFF)<<8)|((0x73&0xFF))); System.out.println(Integer.toHexString(rgb).toUpperCase()); } }
他のツールと方法
他にも有名どころのソースコード難読化ツールとして次のものがあります。
- Jarg
- Progurd
- Retro Guard
このようなツールで変数名・クラス名を縮める副作用を利用してプログラムサイズを縮めることや
- 7Zip
- WinRAR
といった圧縮ツール利用して、jar形式にする際の圧縮ツールを変えて、プログラムサイズを減らすことも可能です。
ちなみに、難読化ツールは数%~数十%縮みますが、圧縮ツールを変えたところで数バイト~数十バイトくらいしか縮みません。
まとめ
通常のWEBアプリケーションを作る際には、プログラムサイズはさほど気にしなくてもいい問題です。
メモリサイズもあまり気にしないで組んで、いざ足りなくなったとしてもハードウェアのメモリ増設でカバーできます。
しかし、今回の記事の例で出したような携帯電話などでは、メモリの増設は通常できません。
さらに、機種毎に搭載しているメモリの大きさもばらばらです。
もしこのような環境に遭遇した場合は、ソースコードの可読性を落とすような記述をするのではなく、
他のツール(今回でいえばCプリプロセッサ)を使って、極力可読性を保ちつつ、メモリ・プログラムサイズを節約していくことをお勧めします。
関連記事: