hyoromoのブログ

iOS/AndroidもしくはCocos2dxネタを書いています

なんでAppWidgetすぐ死んでしまうん?

Android端末に対して、一定以上の負荷を掛けると ActivityManager が悲鳴を上げて各Activity/Service が落ちる場合があります。もちろんAppWidgetも例外なく落とされるのですが、落とされたアプリをもう一度起動しようと ActivityManager は頑張ってくれます。
あまり ActivityManager を追ってないので分かりませんが、どうやら設定の仕方次第で無事動作し始める場合と、動作しない場合があるようです。今回はActivityManagerに殺されても復活してくれるようコードを組んでみました。

負荷の掛け方

Android端末から世界一重いページ!?サイトにアクセスしてLogCatを眺めて下さい。
ActivityManager が大量のログを吐き出していたら間違いなくアナタのAndroid端末瀕死です。

ActivityManagerの吐くLog

一定以上の負荷を掛けた際、ActivityManager は Low Memory: No more background processes. とLogCatに吐き始めます。
その際に、以下のようにプロセスが殺されたり生き返ったりを繰り返します。そして、なんらかの理由で二度とStartされなくなります...

05-07 00:01:01.532: INFO/ActivityManager(74): Process jp.hyoromo.alarmmanagersample (pid 16268) has died.

05-07 00:01:08.282: INFO/ActivityManager(74): Start proc jp.hyoromo.alarmmanagersample for broadcast jp.hyoromo.alarmmanagersample/.AlarmManagerSample: pid=16327 uid=10071 gids={}

対策

以下の対策を講じると落ちても必ず復活するようになりました。*1

  • Alarm設定時に使うContextをAppWidgetProviderのものに変更。
  • オーバライドしたonUpdateメソッドを使うのではなく、onReceive時にハンドリングして処理するように変更。

ソース

@ITで公開されていたデジタル時計ウィジェットを改良したサンプルが以下のようになります。

public class AlarmManagerSample extends AppWidgetProvider {
    private static final long INTERVAL = 1000;

    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)) {
            setAlarm(context);
            Intent serviceIntent = new Intent(context, MyService.class);
            context.startService(serviceIntent);
        }
        super.onReceive(context, intent);
    }

    /**
     * PendingAlarmIntentを作成
     */
    private PendingIntent getPendingAlarmIntent(Context context) {
        Intent intent = new Intent(context, AlarmManagerSample.class);
        intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
        PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0);

        return pendingIntent;
    }

    /**
     * アラームを設定
     */
    private void setAlarm(Context context) {
        AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
        long now = System.currentTimeMillis() + 1;
        long oneHourAfter = now + INTERVAL - now % (INTERVAL);
        alarmManager.set(AlarmManager.RTC_WAKEUP, oneHourAfter, getPendingAlarmIntent(context));
    }

    /**
     * 時計更新サービス処理
     */
    public static class MyService extends Service {
        @Override
        public void onStart(Intent intent, int startId) {
            Context context = getApplicationContext();
            RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.alarm_manager_sample);
            remoteViews.setTextViewText(R.id.TextView01, new Date().toLocaleString());

            ComponentName thisWidget = new ComponentName(context, AlarmManagerSample.class);
            AppWidgetManager manager = AppWidgetManager.getInstance(context);
            manager.updateAppWidget(thisWidget, remoteViews);
        }

        @Override
        public IBinder onBind(Intent intent) {
            return null;
        }
    }
}

また、私がリリースしているVocaloidClockWidgetでも同様の対応を行っています。GitHubで公開していますので、参考にしたい人はどうぞ。

余談

Task Killer でプロセスを殺された場合はどうしようもありません。
なんでプロセスが死んでもまた復活するかと言うと ActivityManager 管理下の元、ActivityManagerに殺されたからです。他のアプリから殺された場合はActivityManagerの知らぬ所なのでプロセスをスタートさせてくれません。

*1:後者はあまり関係ないかも