Let’s imagine we have a simple Android application, which connects to a remote service via IPC, schedules a relatively long task, then continues working while awaiting for callback with some results. AIDL interfaces:
IRemoteService.aidl
package com.var.testservice;
import com.var.testservice.IServCallback;
interface IRemoteService {
void scheduleHeavyTask(IServCallback callback);
}
IRemoteService.aidl
package com.var.testservice;
interface IServCallback {
void onResult(int result);
}
Code for activity:
package com.var.testclient;
import com.var.testservice.IServCallback;
import com.var.testservice.IRemoteService;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import android.view.View;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
public class MainActivity extends Activity {
private static final String TAG = "TestClientActivity";
private IServCallback.Stub servCallbackListener = new IServCallback.Stub(){
@Override
public void onResult(int result) throws RemoteException {
Log.d(TAG, "Got value: " + result);
}
};
private ServiceConnection servConnection = new ServiceConnection(){
@Override
public void onServiceConnected(ComponentName name, IBinder binder) {
service = IRemoteService.Stub.asInterface(binder);
}
@Override
public void onServiceDisconnected(ComponentName name) {
service = null;
}
};
private IRemoteService service;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if(!bindService(new Intent(IRemoteService.class.getName()), servConnection, Context.BIND_AUTO_CREATE)){
Log.d(TAG, "Service binding failed");
} else {
Log.d(TAG, "Service binding successful");
}
}
@Override
protected void onDestroy() {
if(service != null) {
unbindService(servConnection);
}
super.onDestroy();
}
public void onButtonClick(View view){
Log.d(TAG, "Button click");
if(service != null){
try {
service.scheduleHeavyTask(servCallbackListener);
} catch (RemoteException e) {
Log.d(TAG, "Oops! Can't schedule task!");
e.printStackTrace();
}
}
}
}
Code for service:
package com.var.testservice;
import android.app.Service;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.util.Log;
public class TestService extends Service {
private static final String TAG = "TestService";
class TestServiceStub extends IRemoteService.Stub {
private IServCallback servCallback;
//These 2 fields will be used a bit later
private Handler handler;
private int result;
//The simpliest implementation. In next snippets I will replace it with
//other version
@Override
public void scheduleHeavyTask(IServCallback callback)
throws RemoteException {
servCallback = callback;
result = doSomethingLong();
callback.onResult(result);
}
private int doSomethingLong(){
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
return 42;
}
}
}
@Override
public IBinder onBind(Intent intent) {
return new TestServiceStub();
}
}
This version, while being really dumb (it makes UI thread from application hang for 5 seconds, causing ANR), it successfully executes all calls via IPC, delivering result to activity.
Problems start if I try to put calculations into separate thread:
@Override
public void scheduleHeavyTask(IServCallback callback)
throws RemoteException {
servCallback = callback;
Runnable task = new Runnable(){
@Override
public void run() {
result = doSomethingLong();
try {
Log.d(TAG, "Sending result!");
servCallback.onResult(result);
} catch (RemoteException e) {
e.printStackTrace();
}
}
};
new Thread(task).start();
}
In this case the callback is just not delivered to activity: service successfully calls servCallback.onResult(result);, but nothing is called within activity. No exceptions, no clues, no survivors: perfect invocation murder. I couldn’t find any information about possible cause of such behavior, so I’d be grateful if someone could clarify what happens here. My suggestion’s that there’s some kind of security mechanism, tracking which exact threads were bound, and ignoring “unsafe” calls from other threads (something similar happens when we try to mess with UI elements from non-UI thread), but I can’t be sure.
The most obvious solution is to post callback invocation to the bound thread, so I made this:
@Override
public void scheduleHeavyTask(IServCallback callback)
throws RemoteException {
Log.d(TAG, "Schedule request received.");
servCallback = callback;
if(Looper.myLooper() == null) {
Looper.prepare();
}
handler = new Handler();
Runnable task = new Runnable(){
@Override
public void run() {
result = doSomethingLong();
Log.d(TAG, "Posting result sender");
handler.post(new Runnable(){
@Override
public void run() {
try {
Log.d(TAG, "Sending result!");
servCallback.onResult(result);
} catch (RemoteException e) {
e.printStackTrace();
}
Looper.myLooper().quit();
Log.d(TAG, "Looper stopped");
}
});
}
};
new Thread(task).start();
Looper.loop();
}
Here I faced 2 more problems:
- I had to call
Looper.loop()to enable processing of callback runnables, but it blocks IPC, so I have the same result as in the beginning – no actual multithreading; -
Registering for callback second time (after first cycle finished and returned value) results in exception:
java.lang.RuntimeException: Handler (android.os.Handler) sending message to a Handler on a dead thread at android.os.MessageQueue.enqueueMessage at android.os.Handler.sendMessageAtTime at android.os.Handler.sendMessageDelayed at android.os.Handler.post at com.var.testservice.TestService$TestServiceStub$1.run at java.lang.Thread.run
This lefts me completely puzzled: I make a fresh instance from actual Looper, how can it point to dead thread?
The whole idea of service being able of queueing tasks and making callbacks when they finish doesn’t sound insane to me, so I hope someone more experienced could explain me:
- Why can’t I actually make IPC calls from different threads?
- What’s wrong with my
Handler? - What instruments/architecture should I use to make a clean, proper queue mechanism, so it could call IPC methods on the right thread without constantly calling
Looper.loop()/Looper.quit()?
Thank you.
I can’t explain why your program isn’t working. But the version involving threads and an asynchronous callback:
should work just fine.
Here’s how Android arranges threads for AIDL and other types of Binder transaction.
scheduleHeavyTaskis called from the very same thread asonButtonClick. Similarly, you should fine that the call toonResultshould be a simple method call, from the thread running the task.onButtonClickcalledscheduleHeavyTaskwhich called back toonResultfrom the same thread, thenonButtonClickwould appear directly to callonResultwithin the caller process.There are absolutely no mechanisms to avoid calls from “unsafe” or “unbound” threads – so this simple approach you posted should work. As you say, it’s a common pattern, and I’ve used it lots of times. I’d therefore recommend proceeding with this rather than fiddling with extra loopers and handlers.
Here are some ideas:
ddmsfrom the command line, or show the Devices and Threads views within Eclipse. This will give you a view of exactly what each thread is doing – you can get a callstack. It’s normally possible to use this even in cases where a full-on debugger would be inconvenient.synchronizedanywhere? What could be happening is thatonResultwants to go ahead and run, but it’s blocked on some monitor. As soon as it becomes unblocked, it might well run.