如何使用Messenger(IPC)-實作

如何使用Messenger(IPC)-實作

情境

如何使用Messenger(IPC)中簡單介紹怎麼透過Messenger來進行IPC(跨行程溝通),
這邊就實際做個範例來呈現。

如何使用AIDL-非同步實作一樣, 會有兩個app被安裝,
一個用來遠端啟動Service, 另外一邊就是Client端。

程式碼說明

Remote Side

只需要建立一個Service即可。

public class RemoteService extends Service {
    private Messenger mMessenger;
    private HandlerThread mHandlerThread;
    private Handler mHandler;
    @Override
    public void onCreate() {
        super.onCreate();
        mHandlerThread = new HandlerThread("remote service");
        mHandlerThread.start();
        mHandler = new Handler(mHandlerThread.getLooper()){
            @Override
            public void handleMessage(Message msg) {
                try {
                    int rand = (int) Math.random()*3 + 1;
                    Thread.sleep(rand * 1000);
                    int what = msg.what;
                    Bundle b = new Bundle();
                    b.putString("remote", "task " + what + " is done\n");
                    Message m = new Message();
                    m.setData(b);
                    msg.replyTo.send(m);
                } catch (RemoteException e) {
                    e.printStackTrace();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        mMessenger = new Messenger(mHandler);
    }

    @Override
    public void onDestroy() {
        mHandlerThread.quit();
        super.onDestroy();
    }

    @Override
    public IBinder onBind(Intent intent) {
        return mMessenger.getBinder();
    }
}

Service做的事情非常簡單, 利用HandlerThread以及Looper跟Messenger進行聯繫。

mHandlerThread = new HandlerThread("remote service");
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper());

聯繫的時候, 將Messenger的Binder回傳出去。

@Override
public IBinder onBind(Intent intent) {
    return mMessenger.getBinder();
}

而當結束聯繫的時候, 則關閉Thread。

mHandlerThread.quit();

而假設你需要雙向溝通的時候, 其實可以透過Message.replyTo這個方法再將Message回傳。

int what = msg.what;
Bundle b = new Bundle();
b.putString("remote", "task " + what + " is done\n");
Message m = new Message();
m.setData(b);
msg.replyTo.send(m);

記得在AndroidMenifest加入process

<service
    android:name=".RemoteService"
    android:process=":remote">
    <intent-filter>
        <action android:name="service.remote" />
    </intent-filter>
</service>

這樣Remote Side就講解的差不多了。

Client Side

一開始宣告兩個Button用來處理連接跟中斷Remote端的Service,
TextView用來呈現連接以及中斷的結果。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="example.givemepass.messengerclientdemo.MainActivity">
    <Button
        android:id="@+id/connect_remote"
        android:text="connect to remote"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    <Button
        android:id="@+id/disconnect_remote"
        android:text="disconnect"
        android:layout_toRightOf="@id/connect_remote"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    <TextView
        android:id="@+id/result"
        android:layout_below="@id/connect_remote"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!" />
</RelativeLayout>

主程式

public class MainActivity extends AppCompatActivity {
    private Button connectRemote;
    private Button disconnectRemote;
    private ServiceConnection mServiceConnection;
    private Messenger messenger;
    private boolean isBound;
    private TextView result;
    private StringBuffer sb;
    private int index = 1;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initData();
        initView();
    }

    private void clearText(String msg){
        sb.delete(0, sb.length());
        if(msg != null) {
            sb.append(msg);
            sb.append("\n");
        }
        result.setText(sb);
    }

    private void initData(){
        sb = new StringBuffer();
    }

    private void initView(){
        result = (TextView) findViewById(R.id.result);
        connectRemote = (Button) findViewById(R.id.connect_remote);
        disconnectRemote = (Button) findViewById(R.id.disconnect_remote);
        connectRemote.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(!isBound) {
                    clearText(null);
                    mServiceConnection = new ServiceConnection() {
                        @Override
                        public void onServiceConnected(ComponentName name, IBinder service) {
                            messenger = new Messenger(service);
                            isBound = true;
                            clearText("remote is connected");
                        }

                        @Override
                        public void onServiceDisconnected(ComponentName name) {
                            messenger = null;
                            isBound = false;
                            clearText("remote is disconnected.");
                        }
                    };
                    Intent intent = new Intent();
                    intent.setAction("service.remote");
                    intent.setPackage("example.givemepass.messengerremotedemo");
                    bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
                } else{
                    try {
                        Message m = new Message();
                        m.what = index;
                        m.replyTo = new Messenger(new Handler(getMainLooper()){
                            @Override
                            public void handleMessage(Message msg) {
                                String msgStr = msg.getData().getString("remote");
                                if(msgStr != null){
                                    sb.append(msgStr);
                                    result.setText(sb);
                                }
                            }
                        });
                        messenger.send(m);
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                    index++;
                }
            }
        });
        disconnectRemote.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                messenger = null;
                isBound = false;
                index = 1;
                clearText("remote is disconnected.");
            }
        });
    }
}

雖然看起來很長, 但是其實處理的事情很單純,
一開始會透過ServiceConnection來判斷是否已經連接,
如果連接就會顯示已連結,
接著把傳過來的IBinder物件丟進Messenger就完成初始化了。

mServiceConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        messenger = new Messenger(service);
        isBound = true;
        clearText("remote is connected");
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {
        messenger = null;
        isBound = false;
        clearText("remote is disconnected.");
    }
};

接著對Remote的Service真實連接。

Intent intent = new Intent();
intent.setAction("service.remote");
intent.setPackage("example.givemepass.messengerremotedemo");
bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);

如果已經連接了,
由於可以透過replyTo雙向溝通,
我們就可以透過Messenger來取得Handler傳過來的Message物件,
將物件打開塞進我們的TextView。

try {
    Message m = new Message();
    m.what = index;
    m.replyTo = new Messenger(new Handler(getMainLooper()){
        @Override
        public void handleMessage(Message msg) {
            String msgStr = msg.getData().getString("remote");
            if(msgStr != null){
                sb.append(msgStr);
                result.setText(sb);
            }
        }
    });
    messenger.send(m);
} catch (RemoteException e) {
    e.printStackTrace();
}
index++;

那當中斷連線, 我們就可以把數值再次的全部初始化, 並且顯示已斷線。

messenger = null;
isBound = false;
index = 1;
clearText("remote is disconnected.");

這樣就完成IPC的串接了。
我們來看結果,
連續按下connect to remote的Button, 第一次會進行連接,
如果已經連接remote則會送出任務,
而按下disconnect則會中斷連接,
然後就重複以上的行為。

程式碼下載

Remote程式碼
Client程式碼