2016/02/14

AdView は動的追加にしないとメモリリークする

私が Google Play で公開している Dive! Log! というアプリに少し大きめの画像を扱う機能を追加したところ、どうもメモリリークしているような挙動を示すようになりました。 Heap Dump を見てみたところ、Google AdMob の AdView まわりが参照を握っている様子。
こういう時の常套手段として、AdView しかないアプリを作って動作を確認してみることにしました。

2016/02/11

Android 6.0 で EXTERNAL_STORAGE へアクセス許可しても即時反映されない

Android 6.0 以降から個別のパーミッション設定が可能になった

Android 6.0 Marshmallow 以降では各パーミッションを個別に設定できるようになりました。 ユーザにとっては有り難い機能ですが、開発者としてはエラーの原因が増えるので辛いところです。

Storage 関連のパーミッションはすぐに反映されない

特に android.permission.WRITE_EXTERNAL_STORAGEandroid.permission.READ_EXTERNAL_STORAGE は許可を動的に取得してもすぐに反映されないので注意が必要です。
というのも、External Storage へのアクセスは Linux の基本機能であるファイルへのアクセス制限を用いて実現されているからです。
具体的には sdcard_rw というグループへのアクセス許可があるかどうかを Linux Kernel が判断しています。そして、実行中のプロセスに関しては設定が反映されませんAndroid OS の動作が今後変わる可能性はありますが

反映させるためにはアプリを再起動する

少なくとも現時点では、この問題を回避する方法はアプリごと再起動するしか解が無いように思います。

具体例

実際のコードを載せておきます最低限のものなので、細かいお作法に関しては公式のドキュメントを参考にしてください
まず、Activity の onResume など使用する直前にパーミッションを確認します。

@Override
protected void onResume() {
    super.onResume();

    if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
        ActivityCompat.requestPermissions(this,
                new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
                REQUEST_PERMISSIONS_WRITE_EXTERNAL_STORAGE);
    }
}

WRITE_EXTERNAL_STORAGE への許可がない場合、以下のようにユーザへ許可を求めるダイアログが表示されます。

ユーザの対応が onRequestPermissionsResult で返ってくるので、対応を決めます。一般的なパーミッションだとそのまま処理を続行することになりますが、Storage 系のパーミッションの場合、以下のように アプリの再起動をして設定を反映させますActivity ではなくアプリプロセスの再起動が必要です

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
        @NonNull int[] grantResults) {
    switch (requestCode) {
        case REQUEST_PERMISSIONS_WRITE_EXTERNAL_STORAGE:
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // 許可された場合
                // 設定を反映させるためにアプリを再起動
                Intent activityIntent = new Intent(this, ThisActivity.class); // 自Activity を指定
                int pendingIntentId = 1;
                PendingIntent pendingIntent = PendingIntent.getActivity(this, pendingIntentId, activityIntent, PendingIntent.FLAG_CANCEL_CURRENT);
                AlarmManager mgr = (AlarmManager)getSystemService(ALARM_SERVICE);
                mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, pendingIntent);
                System.exit(0);
            } else {
                // 許可されなかった場合
                // TODO エラーメッセージを表示して Activity を終了させる等
            }
            break;
        default:
            // Do nothing
            break;
    }
}

参考