handler.postDelayed vs. AlarmManager vs

2019-04-17 21:11发布

问题:

I have a minor problem in one of my apps. It uses a BroadCastReceiver to detect when a call finishes and then performs some minor housekeeping tasks. These have to be delayed for a few seconds, to allow the user to see some data and to ensure that the call log has been updated. I'm currently using handler.postDelayed() for this purpose:

public class CallEndReceiver extends BroadcastReceiver {

@Override
public void onReceive(final Context context, final Intent intent) {
    if (DebugFlags.LOG_OUTGOING)
        Log.v("CallState changed "
                + intent.getStringExtra(TelephonyManager.EXTRA_STATE));
    if (intent.getStringExtra(TelephonyManager.EXTRA_STATE)
            .equalsIgnoreCase(TelephonyManager.EXTRA_STATE_IDLE)) {
        SharedPreferences prefs = Utils.getPreferences(context);
        if (prefs.getBoolean("auto_cancel_notification", true)) {
            if (DebugFlags.LOG_OUTGOING)
                Log.v("Posting Handler to remove Notification ");
            final Handler mHandler = new Handler();
             final Runnable mCancelNotification = new Runnable() {
                   public void run() {
                        NotificationManager notificationMgr = (NotificationManager) context
                        .getSystemService(Service.NOTIFICATION_SERVICE);
                notificationMgr.cancel(12443);
                if (DebugFlags.LOG_OUTGOING)
                    Log.v("Removing Notification ");
                   }
                };
                mHandler.postDelayed(mCancelNotification, 4000);


        }
        final Handler updateHandler = new Handler();
         final Runnable mUpdate = new Runnable() {
               public void run() {
        if (DebugFlags.LOG_OUTGOING)
            Log.v("Starting updateService");
        Intent newBackgroundService = new Intent(context,
                CallLogUpdateService.class);
        context.startService(newBackgroundService);
               }
               };
               updateHandler.postDelayed(mUpdate, 5000);

        if (DebugFlags.TRACE_OUTGOING)
            Debug.stopMethodTracing();
        try
        {
        // Stopping old Service
        Intent backgroundService = new Intent(context,
                NetworkCheckService.class);
        context.stopService(backgroundService);
        context.unregisterReceiver(this);
        }
        catch(Exception e)
        {
            Log.e("Fehler beim Entfernen des Receivers", e);
        }
    }

}

}

Now I have the problem, that this setup works about 90% of the time. In about 10% of cases, the notification isn't removed. I suspect, that the thread dies before the message queue processes the message/runnable.

I'm now thinking about alternatives to postDelayed() and one of my choices is obviously the AlarmManager. However, I'm not sure about the performance impact (or the resources it uses).

Maybe there is a better way to ensure that all messages have been processed before a thread dies or another way to delay the execution of those two bits of code.

Thank you

回答1:

I'm currently using handler.postDelayed() for this purpose:

That's not a good idea, assuming the BroadcastReceiver is being triggered by a filter in the manifest.

Now I have the problem, that this setup works about 90% of the time. In about 10% of cases, the notification isn't removed. I suspect, that the thread dies before the message queue processes the message/runnable.

More accurately, the process is terminated, taking everything with it.

I'm now thinking about alternatives to postDelayed() and one of my choices is obviously the AlarmManager. However, I'm not sure about the performance impact (or the resources it uses).

It's not that bad. Another possibility is to do your delayed work in an IntentService -- triggered via a call to startService() -- and have it sleep on its background thread for a couple of seconds.



回答2:

Let's try a new way of doing this. Using RxJava. It's much simpler to prototype and easier to manage lots of threads if you want to ever run hundreds of such delayed tasks concurrently, sequentially, coupled with async tasks, chained with synchronous chained async calls etc.

Firstly, set up the Subscriber. Remember new on Subscriber should be done only once to avoid memory leaks.

// Set up a subscriber once
private Subscuber<Long> delaySubscriber = new Subscuber<Long> () {
    @Override
    public void onCompleted() {
        //Wrap up things as onCompleted is called once onNext() is over
    }
    @Override
    public void onError(Throwable e) {
        //Keep an eye open for this. If onCompleted is not called, it means onError has been called. Make sure to override this method
    }
    @Override
    public void onNext(Long aLong) {
        // aLong will be from 0 to 1000
        // Yuor code logic goes here

        // If you want to run this code just once, just add a counter and call onComplete when the counter runs the first time

    }
} 

The snippet below will just emit the 1 in the onNext() of the subscriber. Note that this is done on the Computation Threadpool created and managed by the RxJava library.

//Now when you want to start running your piece of cade, define an Observable interval that'll emit every second
private Observable<Long> runThisAfterDelay = Observable.just(1).delay(1000, TimeUnit.MILLISECONDS, Schedulers.computation());
// Subscribe to begin the emissions.
runThisAfterDelay.subscribe(delaySubscriber);

If you want to run a code after every one second, say, then you can do this:

private Observable<Long> runThisOnInterval = Observable.interval(1000, TimeUnit.MILLISECONDS, Schedulers.computation());


回答3:

In addition to the first answer, you might want to consider what the API documentation says for the onReceive method:

[...] The function is normally called within the main thread of its process, so you should never perform long-running operations in it [...]

So it looks like generally it is not a good idea to start something that waits a couple of time within onReceive (even though, in your case it's less than the 10s limit).

I had a similar timinig problem with the BroadcastReceiver. I couldn't get my results processed even though I onReceive had been called with exactly what I was exepcting. It seemed that the thread the BroadastReceiver was running in, got killed before my result processing could finish. My solutuion was to kick off a new thread to perform all processing.



回答4:

AlarmManager seems not to work very well for short periods of time like 10 seconds and according to user reports the behaviour heavily depends on the firmware.

At the end I decided to use Handler and Runnable in my service.

When creating the Handler, be sure to create it inside the Service class, not inside the BroadcastReceiver since in the last case you'll get Can't create Handler inside thread that has not called Looper.prepare()

public class NLService extends NotificationListenerService {
    private NLServiceReceiver nlservicereciver;
    Handler delayUpdateHandler = new Handler();
    private Runnable runBroadcastUpdate;

    public void triggerViewUpdate() {
        /* Accumulate view updates for faster, resource saving operation.
        Delay the update by some milliseconds.
        And if there was pending update, remove it and plan new update.
         */
        if (runBroadcastUpdate != null) {
            delayUpdateHandler.removeCallbacks(runBroadcastUpdate);
        }
        runBroadcastUpdate = new Runnable() {
            public void run() {
                // Do the work here; execution is delayed
            }
        };
        delayUpdateHandler.postDelayed(runBroadcastUpdate, 300);
    }

    class NLServiceReceiver extends BroadcastReceiver{
        @Override
        public void onReceive(Context context, Intent intent) {
            triggerViewUpdate();
        }
    }

}