10.1、消息机制
当我们的服务器与启动端不是在统一项目内时,即服务器与页面属于不同的进程,
那么我们按照之前的方法来启动绑定服务时,会崩溃,因为找不到对应的本地服务器
所以我们要实现跨进程获取service,通过绑定模式
将服务器端设置为非本地
<service android:process=":remote"/>
设置后,会把服务器端定义为远程服务器,此时若进行绑定则会崩溃,
所以此时如果想要向服务器端发送消息,需要用到消息机制
-
安卓的消息机制:
Message消息
,Messenger信使
,Handler消息处理
,一般处理消息(线程)的同步 -
安卓app的进程,进程名称就是该app的包名
-
在安卓组件注册时,可通过process属性设置组件进程的相关状态
1、客户端向服务器端发送消息
由于安卓的消息机制是基于绑定模式的,因此我们要先编写绑定模式框架
跨进程服务必须要借助消息机制,即包装成消息进行通信
服务器端:建立信使
步骤:
-
准备Handler对象,指定消息处理方式:
编写自定义类继承Handler
,重写handleMessage
回调函数 handleMessage
函数在收到消息对象后回调。每当收到一条消息,都会回调一次,收到的消息对象即该函数的形参
-
准备一个信使
Messenger
对象,替换Binder对象Messenger
对象需要传入一个Handler
对象,用作消息处理,即我们步骤一创建的handler对象 -
从创建的
Messenger
对象中通过getBinder函数提取出消息机制中的Binder对象,用作onbind函数返回
public class srv extends Service {
class handler extends Handler{
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
}
}
handler handler;
Messenger srv_receive_msger;
@Override
public void onCreate() {
super.onCreate();
handler=new handler();
srv_receive_msger=new Messenger(handler);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return srv_receive_msger.getBinder();
}
}
这样我们就用Messenger对象替换Binder对象,用作消息传递
启动端:发送消息
步骤:
-
连接成功后,将返回的Binder对象转换为Messenger对象 创建Messenger对象,将获取的Binder对象作为参数来转换
-
准备消息对象,由于每次所传递的消息不一样,所以我们在获取对象时创建消息
即每次获取服务时,将消息通过messenger传递到启动端 在创建Message对象时,需要使用obtain函数来获取
public static Message obtain ()
//Return a new Message instance from the global pool. Allows us to avoid allocating new objects in many cases.
-
通过Messenger对象的send函数向服务器端传递消息
Message对象内定了一个数据传输的函数:
setData()
、getData()
public void setData (Bundle data) //Sets a Bundle of arbitrary data values. Messenger act_send_msger; //重构自定的Connection内部类 class Connection implements ServiceConnection{ @Override public void onServiceConnected(ComponentName componentName, IBinder iBinder) { act_send_msger=new Messenger(iBinder); } @Override public void onServiceDisconnected(ComponentName componentName) { if(it!=null) it=null; } } //获取服务事件监听器 case R.id.btn_get_service:{ Message msg=Message.obtain(); msg.what=101;//标识发送方 Bundle bd=new Bundle(); bd.putString("act_msg","send by activity:hello"); msg.setData(bd); if(msg!=null) try { act_send_msger.send(msg); } catch (RemoteException e) { e.printStackTrace(); } break; }
我们通过按钮事件实现消息传递,
首先我们启动服务,建立启动端与服务器端的连接
当我们点击按钮时,就通过信使对象将我们创建的消息传递通过链接传递给服务器端
服务器端:接受消息
handleMessage
函数在收到消息对象后回调。每当收到一条消息,都会回调一次,收到的消息对象即该函数的形参
class handler extends Handler{
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
switch (msg.what){
case 101:{
Bundle bd=msg.getData();
String str=bd.getString("act_msg");
Log.i("mytag",str);
break;
}
}
}
}
通过msg.what
成员变量,来区分是由那个启动端发送来的消息
Message对象的成员变量:
public int arg1;//int 参数
public int arg2;//int 参数
public Object obj;
public Messenger replyTo;//消息回复
public int sendingUid = -1;
public int what;//消息标记:标记该信息是由谁传递的
测试:
android:process
:
一个进程的名称,应用的所有组件都应在该进程中运行。 每个组件都可以通过设置自己的 process
属性来替换此默认值。
默认情况下,当应用的第一个组件需要运行时,Android 会为该应用创建一个进程。所有组件随后都在该进程中运行。 默认进程的名称与由 <manifest>
元素设置的软件包名称一致。
<application>
元素的 process
属性可以为所有组件设置不同的默认值。不过,组件可以使用其自己的 process
属性替换默认属性,从而允许您跨多个进程分布应用。
如果为此属性分配的名称以冒号(“:”)开头,则会在需要时创建一个应用专用的新进程。 如果进程名称以小写字符开头,则会创建一个采用该名称的全局进程。一个应用可与其他应用共享全局进程,从而减少资源使用量。
由于服务器端我们在清单文件中配置了以(“:”)开头的process名,所以Android创建了一个应用专用的新进程
我们在启动端向服务器端传递的消息,则在:remote进程中收到。
而我们的本地进程是收不到的
2、服务器端向客户端回传消息
服务器端的消息发送一般是被动的发送响应消息
当启动端向服务器端发送消息后,服务器端给予启动端一个响应
服务器端:回复响应
步骤:
-
准备消息对象
-
接受启动端消息后,从消息对象的replyto中获取启动端的信使对象
-
调用信使对象的send函数实现消息传递
switch (msg.what){
case 101:{
//消息接受
Bundle bd=msg.getData();
String str=bd.getString("act_msg");
Log.i("mytag",str)
//消息响应
Messenger srv_send_msger=msg.replyTo;
Message srv_msg=Message.obtain();
srv_msg.what=999;//999为服务器端
Bundle bd1=new Bundle();
bd1.putString("reply","service's reply:hello");
srv_msg.setData(bd1);
if(srv_send_msger!=null) {
try {
srv_send_msger.send(srv_msg);
} catch (RemoteException e) {
e.printStackTrace();
}
}
break;
}
}
一般服务器的消息属于响应消息,即启动端向服务器端发送消息后,立马得到一个来自服务器的响应
所以服务器端在收到启动端的消息时,就立马响应
启动端:接受响应
步骤:
-
准备新的messenger对象
-
在启动向服务器端发送消息时,通过消息对象的replyto成员设置启动端的信使对象, 从而服务端可以获取启动端的信使对象,以实现服务端向启动端的响应
//定义信使对象
Messenger act_receive_msger=new Messenger(new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
switch (msg.what){
case 999:{
Bundle bd=new Bundle();
bd=msg.getData();
Log.i("mytag",bd.getString("reply"));
break;
}
}
}
});//在获取服务的事件监听器上设置reply对象
//在发送消息前设置
msg.replyTo=act_receive_msger;java
replyTo
变量:
public Messenger replyTo
//Optional Messenger where replies to this message can be sent.
3.流程
启动端——>服务器端:
-
启动端建立
Activity-Message
消息对象用来存放即将发送的消息 -
将消息对象交给
Activity
的发送信使Activity_send_Messenger
-
Activity_send_Messenger
的本质是对Service_receive_Messenger
的一个引用,这两个对象其实是同一个对象-
当我们在启动端启动服务器服务时,会调用服务器端的onbind回调函数,而该回调函数的返回对象就是
Service_receive_Messenger
-
我们在启动端设置的
Activity_send_Messenger
就在连接成功时,建立起了引用关系
-
-
Service_receive_Messenger
服务器的接受收到消息后交给Service_Handler
来处理 -
我们要实现的对消息的相关操作就在Handler里编写
服务器端——>启动端
-
服务器端建立
Service-Message
消息对象用来存放用来响应的消息内容 -
将消息对象交给
Service_send_Messenger
来实现消息发送 -
Service_send_Messenger
该信使对象,该信使将消息回传给发送方-
在该信使对象创建时,是将发送方的replyTo变量赋值给了该信使
-
在发送方发送消息时,就把发送方的接受信使通过replyTo变量传递给了服务器
-
服务器的发送信使,通过replyTo找到了目标信使,从而实现消息的回传
-
-
发送方收到消息后,交给发送方的Handler对象来处理
-
我们要实现的对服务器响应消息的接受就在这里编写
4.消息机制对线程的关系:ui进程
例如,我们想要在UI界面上显示实时时间
通过一个死循环:
findViewById(R.id.btn_get_time).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
flag=true;
while(flag)
Log.i("mytag",""+System.currentTimeMillis());
}
});
findViewById(R.id.btn_stop).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
flag=false;
}
});
两个按钮控制,启停
可以发现,当程序在执行死循环时,UI界面无法互动,因为在后台程序运行完之前,前台时一致保持未响应的状态
这样就导致了UI线程的阻塞
解决:
将其运行在线程中,即可实现启停
new Thread(){
public void run() {
flag=true;
while(flag)
Log.i("mytag",""+System.currentTimeMillis());
}
}.start();
这是在Logcat窗口来显示时间值,那么如果我们想要在UI页面上显示
设置一个Textview来放置时间
new Thread(){
public void run() {
flag=true;
while(flag)
textView.setText("Time:"+System.currentTimeMillis());
}
}.start();
发现在运行时,程序崩溃,报错:
安卓UI中的各种界面元素,不支持异步操作,只能由UI线程来操作
意味着需要做线程同步,安卓使用消息机制(安卓线程同步)
通过消息传递,将消息传递给自己
将消息传递给Handler,UI中的元素需要在Handler中进行处理
public class Amain extends Activity {
boolean flag;
Messenger msger;
TextView textView;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.ly_main);
class handler extends Handler{
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
switch (msg.what){
case 111:{
Bundle bd=new Bundle();
bd=msg.getData();
textView.setText(bd.getString("key"));
}
}
}
}
handler handler=new handler();
msger=new Messenger(handler);
textView=(TextView)findViewById(R.id.tv_time);
findViewById(R.id.btn_get_time).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
new Thread(){
public void run() {
flag=true;
while(flag){
Message msg=Message.obtain();
msg.what=111;
Bundle bd=new Bundle();
bd.putString("key","Time:"+System.currentTimeMillis());
msg.setData(bd);
if(msger!=null)
try {
msger.send(msg);
} catch (RemoteException e) {
e.printStackTrace();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
});
findViewById(R.id.btn_stop).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
textView.setText("stop");
flag=false;
}
});
}
}
10.2、AIDL
Android 接口定义语言 (AIDL) 与您可能使用过的其他接口语言 (IDL) 类似。您可以利用它定义客户端与服务均认可的编程接口,以便二者使用进程间通信 (IPC) 进行相互通信。在 Android 中,一个进程通常无法访问另一个进程的内存。因此,为进行通信,进程需将其对象分解成可供操作系统理解的原语,并将其编组为可供您操作的对象。编写执行该编组操作的代码较为繁琐,因此 Android 会使用 AIDL 为您处理此问题。
官方文档:Android 接口定义语言 (AIDL) | Android 开发者 | Android Developers
AIDL接口文件的声明
步骤:
-
直接使用Android Studio直接创建文件 src\main\aidl
-
编写服务
interface Srv_aidl {
//接口中的函数为服务端提供服务
int add(int a,int b);
}
服务器端:
步骤:
-
创建AIDL接口的存根对象(Stub)
-
通过onBind回调函数返回存根对象
-
设置隐式跳转路径
import com.example.aidl.Srv_aidl;
public class MyService extends Service {
Srv_aidl.Stub istub=new Srv_aidl.Stub() {
@Override
public int add(int a, int b) throws RemoteException {
return a+b;
}
};
@Override
public IBinder onBind(Intent intent) {
return istub;
}
}
<action android:name="myaction"/>
aidl接口在安卓默认实现时,会自动添加一个Stub静态抽象内部类 该类继承了aidl接口(并没有实现接口中的函数),同时继承了Binder类
//源码
public static abstract class Stub extends android.os.Binder implements com.example.aidl.Srv_aidl
启动端
步骤:
-
双进程创建
构造跨进程通讯环境
-
创建aidl文件,保证与服务端的一致(推荐文件拷贝),复制到启动进程的main文件夹下
注意:启动端的aidl包名须与服务端的一致
-
启动端绑定模式框架
-
绑定成功后,使用存根对象
Stub
的asinterface
函数,将binder对象转换为接口对象 -
通过接口对象调用接口的服务函数
-
通过页面文本框显示结果
import com.example.aidl.Srv_aidl;
public class Amain extends Activity {
Intent it;
Srv_aidl srv_aidl;
connection connection;
TextView tv;
boolean isbound;//用于健壮性检测
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.ly_main);
it=new Intent();
it.setAction("myaction");
it.setPackage("com.example.aidl");//完全包名
connection=new connection();
tv=(TextView) findViewById(R.id.tv_view);
findViewById(R.id.btn_startservice).setOnClickListener(listener);
findViewById(R.id.btn_getservice).setOnClickListener(listener);
findViewById(R.id.btn_stopservice).setOnClickListener(listener);
}
class connection implements ServiceConnection{
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
srv_aidl=Srv_aidl.Stub.asInterface(iBinder);
if(srv_aidl!=null) isbound=true;
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
if(srv_aidl!=null) srv_aidl=null;
isbound=false;
}
}
View.OnClickListener listener=new View.OnClickListener() {
@Override
public void onClick(View view) {
switch(view.getId()){
case R.id.btn_startservice:{
if(it!=null && connection!=null &&!isbound)
bindService(it,connection,BIND_AUTO_CREATE);
break;
}
case R.id.btn_getservice:{
if(srv_aidl!=null && isbound)
try {
int num=srv_aidl.add(10,20);
tv.setText("10+20="+num);
} catch (RemoteException e) {
e.printStackTrace();
}
break;
}
case R.id.btn_stopservice:{
if(connection!=null && isbound) {
unbindService(connection);
srv_aidl=null;
isbound=false;
}
break;
}
}
}
};
}
测试:
首先开启服务器端
由于服务器端是没有页面显示的,我们要在运行配置中将运行页面设置为nothing
然后开启启动端
结果:
同样,我们可以设置随机两个数进行加法运算,通过每次点击获取服务,获得任意两个数的和
case R.id.btn_getservice:{
if(srv_aidl!=null && isbound)
try {
int x= (int) (Math.random()*100);
int y= (int) (Math.random()*100);
int num=srv_aidl.add(x,y);
tv.setText(x+"+"+y+"="+num);
} catch (RemoteException e) {
e.printStackTrace();
}
break;
}