Android 開発の落とし穴

今回は Android の話です。
Java プログラマが Android アプリを開発する際に陥りやすい事象を 4 つご紹介します。

※本記事は Java 習得済みの方向けです。
 Java の基本や、ちょっとしたテクニック等を知りたい、という方は
 弊社 HP 「Java 講座」をどうぞ!
 (↓ に出てくるシングルトンの解説もありますよ)

シングルトンのはずがいなくなる件

Java 界隈では常識な、
生存し続けるはずのシングルトンなインスタンス。
OutOfMemory 例外さえ気をつけていれば良いと思いきや!

Android では、
バックグラウンドに引っ込んでいてメモリがきつくなったら、
たとえ static final なインスタンスでも
メモリから無言で跡形もなくいなくなります。(えー
消えるときに自動でどこかに永続化もしてくれません。(えー!

つまり、いつ消えてもいいように自前で永続化する必要があります。

ダメな例


public class Singleton {
    private static final instance = new Singleton();

    private int value;

    private Singleton() {
        // NOP
    }

    public static Singleton getInstance() {
        return instance;
    }

    public static int getValue() {
        return value;
    }

    public static void setValue(int val) {
        value = val;
    }
}

この書き方だと、value の値が 0 になってしまうかもしれません。
なんてこった。

大丈夫な例

解決策がこちら。
アプリケーションクラスを拡張して Application インスタンスを取得できるようにします。


public class MyApplication extends Application {
    private static Application app;

    public static Application getInstance() {
        return app;
    }

    @Override
    public void onCreate() {
        app = this;
    }
}

シングルトンクラスを変更。


public class Singleton {
    private static final instance = new Singleton();

    private int value;

    private Singleton() {
        // プリファレンスから復元しよう!
        MyApplication.getInstance().getApplicationContext().getSharedPreferences(
            "appname", Context.MODE_PRIVATE).getInt("value", 0);
    }

    public static Singleton getInstance() {
        return instance;
    }

    public static int getValue() {
        return value;
    }

    public static void setValue(int val) {
        value = val;
        // ついでにプリファレンスに保存しよう!
        MyApplication.getInstance().getApplicationContext().getSharedPreferences(
            "appname", Context.MODE_PRIVATE).edit().putInt("value", value).commit();
    }
}

これなら、プリファレンスから復元できるので
メモリから消えてしまっても大丈夫です。
プリファレンスに読み書きするオーバーヘッドが生じてしまうのは
仕方なし…ということで。

どこへいったアクティビティ

Android では、アクティビティがバックグラウンドに引っ込んでいて
メモリがきつくなった場合、
メモリからアクティビティがいなくなります。(えー
書きかけのテキストなどが消えてしまうわけです。困ります。
こういう事態は、カメラやアルバムを開く際等に、画像展開でメモリがカツカツになってよく起きます。

ちなみに、Android の「設定」-「開発者向けオプション」の下の方にある
「アクティビティを保持しない」をオンにすると、
バックグラウンドになったら問答無用でいなくなってくれるので再現に重宝します。
※開発者向けオプションは Android のバージョンにより項目が異なります。
 また、デフォルトでは「設定」に表示されていません。
 開発者向けオプションについてはこちらなどをご参考に。

幸いにして、 Activity クラスには、

onSaveInstanceState(Bundle bundle) :インスタンスの保存
onCreate(Bundle savedInstanceState) :インスタンスの復帰

がある!

インスタンスの保存・復帰


EditText editTextMessage;

@override
public void onSaveInstanceState(Bundle state) {
    super.onSaveInstanceState(state);

    state.putString(KEY, editTextMessage.getText().toString());
}

@override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (savedInstanceState != null) {
        String messageText = savedInstanceState.getString(KEY);
        editTextMessage.setText(messageText);
    }
}

カメラ、アルバム画像の怪

1000 万画素の写真をそのまま読み込むと、
どれだけメモリを消費するでしょう?
JPEG ファイルとしては、1MB くらいかもしれませんが……。

プログラム上では JPEG や PNG のまま扱いません。
BMP にして画像処理をします。
BMP はビットマップファイル、読んで字のごとく。
1 画素あたり 4 バイト消費するので、

4 バイト * 1000 万画素 = 4000 万バイト(40MB)

これはキツいです。
一応、ロードするときに ARGB8888 以外を選べばサイズは縮小できますが、
色数が減るので汚くなります。(RGB565, ARGB4444 など)

なので、原寸大のまま読み込まないこと。
縦横サイズを 1/n にして読み込む方法があるので、
それを使いましょう。

画像を 1/n サイズで読み込む


// uri にカメラ画像の Uri が設定されているとします

Bitmap scaledImage = null;

// 端末の画面サイズ取得
Display display = getWindowManager().getDefaultDisplay();
DisplayMetrics metrics = new DisplayMetrics();
display.getMetrics(metrics);
Point outSize = new Point();
display.getSize(outSize);
int maxPicSize = outSize.x > outSize.y ? outSize.x : outSize.y;

try {
    // 読み込み用のオプション生成
    BitmapFactory.Options options = new BitmapFactory.Options();
    // ほかのオプションを適宜設定しつつ...
    // 画像のサイズ情報だけを取得するフラグを有効に
    options.inJustDecodeBounds = true;

    // ファイルパス取得する
    String path = uri.getPath();

    // サイズだけ読み込み、画面に収まるよう縮尺値を計算
    BitmapFactory.decodeFile(path, options);
    float sd = metrics.scaledDensity;
    int sw = Math.round((options.outWidth * sd) / maxPicSize) + 1;
    int sh = Math.round((options.outHeight * sd) / maxPicSize) + 1;
    int scale = Math.max(sw, sh);

    // 今度は予め小さくした画像を読み込みたいのでfalseを指定
    options.inJustDecodeBounds = false;
    // 縮尺値を指定
    options.inSampleSize = scale;

    // 指定されたパスの画像を取得する。
    InputStream is = getContentResolver().openInputStream(uri);
    scaledImage = BitmapFactory.decodeStream(is, new Rect(-1, -1, -1, -1), options);
} catch (Exception e) {
    // TODO エラー処理してください
    e.printStackTrace();
    return;
}

画像リソースをケチると、低スペック端末で落ちる件

Drawable リソース(画像リソース)を用意する際、
大は小を兼ねると思ってリソースの準備を怠ると痛い目を見るよ、というお話。

Android はディスプレイの Density(密度)に合わせて、
読み込む画像リソースを自動的に切り替えたり、拡大縮小したりするようになっています。
Density は 160dpi(dot per inch)を基準として下記の表のように分類されていて、
res/drawable-XXXX (XXXX は Density の種類)以下に
画像リソースを定義できます。

画像リソースの種類

Density dpi 比率 備考
ldpi ~120 x0.75 絶滅種
mdpi ~160 x1.0 主に Android 2.x 時代の機種
hdpi ~240 x1.5 Nexus S
xhdpi ~320 x2.0 Galaxy Nexus, Nexus 4, Nexus 9
xxhdpi ~480 x3.0 Nexus 5
xxxhdpi ~640 x4.0 Nexus 6

 

こうした画像リソースを定義する際、
たとえば drawable-xxxhdpi だけ用意したとします。

Nexus S のような
drawable-hdpi が本来のリソースである場合、
drawable-hdpi を用意していなくても
それ以外の解像度のリソースを引っ張ってきます。
スケールダウンで見た目は小さすぎず、
大きすぎずちょうど良く表示してくれます。

が、しかし、
この場合メモリのフットプリントはガッチリ持っていかれてしまいます。
面積比で 7 倍以上違うので、
drawable-hdpi を使うよりもメモリ 7 倍以上食う訳です。

というわけで結論。
大きい素材からリサイズして
低解像度用のリソースも用意しましょう。

Windows だったら Ralpha (窓の杜 / Vector )で一括リサイズがおすすめです。
Mac, Linux は……良い方法がありましたらぜひコメント欄まで!

また、 PNGGauntlet を使うと
一括で PNG サイズを極限まで小さくできるので
APK のサイズを縮めるのに役立ちます。

 

以上、Java プログラマが Android アプリ開発で陥りやすい落とし穴まとめでした。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

*
*
*