Android: how to communicate from worker thread to

2019-06-09 18:01发布

问题:

I've created a service class and a worker class that is executed in a separate thread. I would like to setup a communication between them, so the worker could send some status back to the service.

I've tried to convert my worker Thread to HandlerThread and setup a Handler on the service side, but then I don't know how to actually send a message from the worker. It looks like I can't grasp the concept.

Here's my classes (without communication logic):

Service class

package com.example.app;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;


public class ConnectionService extends Service {

    protected ConnectionWorker thread;

    @Override
    public void onCreate() {

        super.onCreate();

        // Creating a connection worker thread instance.
        // Not starting it yet.
        this.thread = new ConnectionWorker();

        Log.d(this.getClass().getName(), "Service created");
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        Log.d(this.getClass().getName(), "Service started");

        // Checking if worker thread is already running.
        if (!this.thread.isAlive()) {

            Log.d(this.getClass().getName(), "Starting working thread");

            // Starting the worker thread.
            this.thread.start();
        }

        return Service.START_STICKY;
    }

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

    @Override
    public void onDestroy() {

        Log.d(this.getClass().getName(), "Stopping thread");

        // Stopping the thread.
        this.thread.interrupt();

        Log.d(this.getClass().getName(), "Stopping service");

        super.onDestroy();

        Log.d(this.getClass().getName(), "Service destroyed");
    }
}

Worker class

package com.example.app;

import android.os.SystemClock;
import android.util.Log;


public class ConnectionWorker extends Thread {

    public ConnectionWorker() {
        super(ConnectionWorker.class.getName());
    }

    @Override
    public void run() {

        super.run();

        Log.d(this.getClass().getName(), "Thread started");

        // Doing the work indefinitely.
        while (true) {

            if (this.isInterrupted()) {
                // Terminating this method when thread is interrupted.
                return;
            }

            // @todo: send a message to the service

            Log.d(this.getClass().getName(), "Doing some work for 3 seconds...");
            SystemClock.sleep(3 * 1000);
        }
    }
}

How do I implement a HandlerThread, send messages from the worker thread and receive them in my service?

I will highly appreciate a code example.

Update

Actually, it looks like I can't use Looper inside of my thread cause it will block the thread execution and I don't want that. Is it possible to send messages from the thread without using Looper?

回答1:

It looks like I've finally nailed it!

In order to pass data from thread back to a service you will need to do this:

  1. Subclass a Handler class inside of your service (call it e.g. a LocalHandler). You will have to make it static. Override a handleMessage method, it will receive messages from the thread.

  2. Add a Handler argument to your Thread constructor. Instantiate your LocalHandler class in a service and inject it to your thread via constructor.

  3. Save reference to the Handler inside of your thread and use it to send messages whenever appropriate.

Here's the complete example:

Service Class

package com.example.app;

import android.app.Service;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.util.Log;


public class ConnectionService extends Service {

    protected ConnectionWorker thread;

    static class LocalHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            Log.d(this.getClass().getName(), "Message received: " + (String) msg.obj);
        }
    }
    protected Handler handler;


    @Override
    public void onCreate() {

        super.onCreate();

        // Instantiating overloaded handler.
        this.handler = new LocalHandler();

        // Creating a connection worker thread instance.
        // Not starting it yet.
        // Injecting our handler.
        this.thread = new ConnectionWorker(this.handler);

        Log.d(this.getClass().getName(), "Service created");
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        Log.d(this.getClass().getName(), "Trying to start the service");

        // Checking if worker thread is already running.
        if (!this.thread.isAlive()) {

            Log.d(this.getClass().getName(), "Starting working thread");

            // Starting the worker thread.
            this.thread.start();

            Log.d(this.getClass().getName(), "Service started");
        }

        return Service.START_STICKY;
    }

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

    @Override
    public void onDestroy() {

        Log.d(this.getClass().getName(), "Stopping thread");

        // Stopping the thread.
        this.thread.interrupt();

        Log.d(this.getClass().getName(), "Stopping service");

        super.onDestroy();

        Log.d(this.getClass().getName(), "Service destroyed");
    }
}

Worker Class (Thread)

package com.example.app;

import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.util.Log;


public class ConnectionWorker extends Thread {

    // Reference to service's handler.
    protected Handler handler;

    public ConnectionWorker(Handler handler) {
        super(ConnectionWorker.class.getName());

        // Saving injected reference.
        this.handler = handler;
    }

    @Override
    public void run() {

        super.run();

        Log.d(this.getClass().getName(), "Thread started");

        // Doing the work indefinitely.
        while (true) {

            if (this.isInterrupted()) {
                // Terminating this method when thread is interrupted.
                return;
            }

            Log.d(this.getClass().getName(), "Doing some work for 3 seconds...");

            // Sending a message back to the service via handler.
            Message message = this.handler.obtainMessage();
            message.obj = "Waiting for 3 seconds";
            this.handler.sendMessage(message);

            SystemClock.sleep(3 * 1000);
        }
    }
}

I hope it's a valid implementation. If you now a better approach - please let me know.



回答2:

Slava, actually, your answer doesn't work. Let me explain why.

Why does Android have HandlerThread when Thread already exists?

This is because as the documentation says, HandleThread is a

Handy class for starting a new thread that has a looper. The looper can then be used to create handler classes. Note that start() must still be called.

How is a Looper used to create a Handler?

A Looper should be passed in the Handler's constructor to tell the Handler which Looper it is working with.

So each HandlerThread has one Looper and there can be many Handlers handling messages the Looper loops through.


When you created a new Handler in your Service class using the default constructor, the Handler gets associated to the current Looper, as explained in it's documentation.

Handler ()

Default constructor associates this handler with the Looper for the current thread. If this thread does not have a looper, this handler won't be able to receive messages so an exception is thrown.

You can confirm the problem still exists using by printing this

Thread.currentThread().getId()

at appropriate places - within the Service's onCreate(), within the Handler's handleMessage() and within the run() of ConnectionWorker. How to solve your problem then?

You can create a HandlerThread, even within the service, and when it's Looper is ready, create the Handler using that Looper.

new HandlerThread("ConnectionWorker") {
    @Override
    protected void onLooperPrepared() {
        new Handler(getLooper()).post(new Runnable() {
            @Override
            public void run() {
                // do your stuff

                getLooper().quitSafely();
            }
        });
    }
}.start();

Hope it isn't too late an answer :P