Service远程服务

案例解析

需求场景:client和server建立远程连接后,client主动调用server方法增加消息事件,server定时推送给client消息事件。

服务端server

创建AIDL文件,并在其中定义与客户端通信的内容(即方法),并编译(Make Project)。

TestMathAidl.aidl类

package com.jinzifu.myserver;

import com.jinzifu.myserver.MediumEvent;
import com.jinzifu.myserver.INewEventArrivedListener;

interface TestMathAidl {

    List<MediumEvent> getMediumList();

    boolean addEvent(in MediumEvent mediumEvent);

    void registerListener(INewEventArrivedListener listener);

    void unRegisterListener(INewEventArrivedListener listener);
}

INewEventArrivedListener.aidl类

package com.jinzifu.myserver;
import com.jinzifu.myserver.MediumEvent;

interface INewEventArrivedListener {
    void onNewEventArrieved(in MediumEvent mediumEvent);
}

MediumEvent.aidl类

package com.jinzifu.myserver;

import com.jinzifu.myserver.MediumEvent;

parcelable MediumEvent;

注意:MediumEvent.java是属于Java类,应放在Java包下,其他aidl类应放在aidl包下。

AndroidManifest.xml类

//自定义服务权限
<permission android:name="com.jinzifu.myserver.permission.ACCESS_EVENT_SERVICE"
        android:protectionLevel="normal"/>

<service
      android:name=".TestServerService">
      <intent-filter>
          <action android:name="com.jinzifu.myserver.TestMathAidl" />
      </intent-filter>
</service>

注意:宜将AIDL相关的文件都放到同一个包中,便于从server端复制到client端。
因为客户端需要反序列化服务端中和AIDL接口相关的所有文件,如果类的完整路径不一致,就无法反序列化成功。

TestServerService:通过IBinder的实现类实现远程业务方法。

public class TestServerService extends Service {
    //① CopyOnWriteArrayList 队列支持并发读写,且最终以ArrayList的类型数据通信,同理ConcurrentHashMap。
    CopyOnWriteArrayList<MediumEvent> mediumEvents =
            new CopyOnWriteArrayList<>();
    //② RemoteCallbackList 是系统专门提供的用于管理跨进程接口回调实例的,且在客户端进程终止后,能自动移除客户端注册的Listener,内部还实现了线程同步。
    RemoteCallbackList<INewEventArrivedListener> INewEventArrivedListeners
            = new RemoteCallbackList<>();

    @Override
    public void onCreate() {
        super.onCreate();
        //③ 模拟远程服务定时推送消息给客户端,会遍历RemoteCallbackList的listener接口,参考观察者模式。
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    while (true) {
                        Thread.sleep(5000);
                        MediumEvent mediumEvent = new MediumEvent("" + System.currentTimeMillis(), 10);
                        //beginBroadcast与finishBroadcast要配对使用
                        final int size = INewEventArrivedListeners.beginBroadcast();
                        for (int i = 0; i < size; i++) {
                            INewEventArrivedListener iNewEventArrivedListener = INewEventArrivedListeners.getBroadcastItem(i);
                            iNewEventArrivedListener.onNewEventArrieved(mediumEvent);
                        }
                        INewEventArrivedListeners.finishBroadcast();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return new TestMathAidl.Stub() {
            @Override
            public List<MediumEvent> getMediumList() throws RemoteException {
                return mediumEvents;
            }

            //④ 重写onTransact方法,对多进程/多应用的客户端连接申请权限验证
            @Override
            public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
                    throws RemoteException {
                //⑤ server端自定义权限,client端注册权限后才能绑定成功
                int result = checkCallingPermission(
                        "com.jinzifu.myserver.permission.ACCESS_EVENT_SERVICE");
                if (result == PackageManager.PERMISSION_DENIED) {
                    Log.e("MainActivity", "远程服务权限校验=" + result);
                    return false;
                }
                return super.onTransact(code, data, reply, flags);
            }

            @Override
            public boolean addEvent(MediumEvent mediumEvent) throws RemoteException {
                return mediumEvents.add(mediumEvent);
            }

            @Override
            public void registerListener(INewEventArrivedListener listener)
                    throws RemoteException {
                INewEventArrivedListeners.register(listener);
            }

            @Override
            public void unRegisterListener(INewEventArrivedListener listener)
                    throws RemoteException {
                INewEventArrivedListeners.unregister(listener);
            }
        };
    }
}

核心知识点:如上源码中注释位置

  • CopyOnWriteArrayList 队列支持并发读写,且最终以ArrayList的类型数据通信,同理ConcurrentHashMap。
  • RemoteCallbackList 是系统专门提供的用于管理跨进程接口回调实例的,且在客户端进程终止后,能自动移除客户端注册的Listener,内部还实现了线程同步。
  • 模拟远程服务定时推送消息给客户端,会遍历RemoteCallbackList的listener接口,参考观察者模式。
  • 重写onTransact方法,对多进程/多应用的客户端连接申请权限验证。
  • server端自定义权限,client端注册权限后才能绑定成功。

客户端client

MainActivity类

...
ServiceConnection serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
            //① onServiceConnected和onServiceDisconnected回调是在client的UI线程中的。
            testMathAidl = TestMathAidl.Stub.asInterface(iBinder);
            if (testMathAidl == null || !iBinder.isBinderAlive()) return;
            Log.e("MainActivity", "远程服务连接成功");

            //② 调用远程服务service的方法会阻塞client的UI线程,此须在子线程中调用。
            new Thread() {
                @Override
                public void run() {
                    super.run();
                    try {
                        testMathAidl.registerListener(mINewEventArrivedListener);
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                }
            }.start();
        }

        @Override
        public void onServiceDisconnected(ComponentName componentName) {
            //③ 正常解绑服务不会回调此方法,远程服务service失联后会回调此方法,可在此重连服务
            Log.e("MainActivity", "远程服务失联");
        }
    };

    INewEventArrivedListener mINewEventArrivedListener = new INewEventArrivedListener.Stub() {

        @Override
        public void onNewEventArrieved(MediumEvent mediumEvent) throws RemoteException {
            //④ 接收远程服务端的回调,此处是在binder线程池中,更新UI需异步消息机制
            Log.e("MainActivity", "新事件名字=" + mediumEvent.name);
        }
    };

    @Override
    protected void onDestroy() {
        super.onDestroy();
        unbindService();
    }

    private void unbindService() {
        if (testMathAidl != null) {
            try {
                //⑤ 断开服务时,需要解注册listener并解绑service。
                testMathAidl.unRegisterListener(mINewEventArrivedListener);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
            unbindService(serviceConnection);
            testMathAidl = null;
        }
    }
...    

核心知识点:如上源码中注释位置

  • onServiceConnected和onServiceDisconnected回调是在client的UI线程中的。
  • 调用远程服务service的方法会阻塞client的UI线程,此须在子线程中调用。
  • 正常解绑服务不会回调此方法,远程服务service失联后会回调此方法,可在此重连服务。
  • 接收远程服务端的回调,此处是在binder线程池中,更新UI需异步消息机制。
  • 断开服务时,需要解注册listener并解绑service。

Android Manifest.xml类

 <uses-permission android:name="com.jinzifu.myserver.permission.ACCESS_EVENT_SERVICE"/>

LOG日志:

com.jinzifu.myclient E/MainActivity: 远程服务连接成功
com.jinzifu.myclient E/MainActivity: 事件个数=1
com.jinzifu.myclient E/MainActivity: 新事件名字=1567330942898
com.jinzifu.myclient E/MainActivity: 新事件名字=1567330947903
com.jinzifu.myclient E/MainActivity: 新事件名字=1567330952911
com.jinzifu.myclient E/MainActivity: 新事件名字=1567330957915
com.jinzifu.myclient E/MainActivity: 新事件名字=1567330962919

基于AIDL的IPC问题汇总

  • 重复实例问题

Android为每个应用分配一个独立的虚拟机,或者为每个进程分配独立的虚拟机,不同的虚拟机在内存分配上有不同的地址,导致不同进程中,访问同一个类会有多个对象实例。

  • 静态成员变量,单例模式失效;
  • 依附于进程的线程同步机制失效;
  • SharedPreferences持久化失效(同理读写文件并发);
  • Application实例重复创建;

在进程之间通过共享内存的方式通信,都会失败的。

  • 并发问题

AIDL方法是在服务端的Binder线程池中执行的,当多个客户端同时连接服务端时,会有并发读写的问题。CopyOnWriteArrayList 队列支持并发读写,且最终以ArrayList的类型数据通信,同理ConcurrentHashMap。

  • 接口回调问题

客户端注册Listener对象实参传到服务端时,服务端收到的是一个新的Listener对象,导致在解注册时无法成功移除客户端Listener。
RemoteCallbackList是系统专门提供的用于管理跨进程接口回调实例的,且在客户端进程终止后,能自动移除客户端注册的Listener,内部还实现了线程同步。

对象实例是不能通过跨进程直接传输的,对象的跨进程本质是反序列的过程,传递的是值不是引用。

  • 权限验证问题
  1. onTransact:onTransact方法返回值来做权限验证,避免任意进程调用远程服务。【推荐】
  2. 自定义Permission:在服务端的onBind()中校验自定义的permission,如果通过了校验,正常返回Binder对象,否则返回null。

常用的权限验证都要在server的清单文件中注册自定义权限。

  • 线程挂起引起的ANR问题

客户端需在工作线程中调用服务端方法。客户端listener的回调方法是在客户端的binder线程池中,若更新UI需hander异步通信机制。

  • onServiceConnnected和onServiceDisconnected方法运行在客户端UI线程中。
  • 客户端调用远程服务方法时,客户端线程会被挂起,进入阻塞状态。
  • 通过binder 调用对方的方法,对方的方法都是运行在自身的binder线程池中,耗时操作无需另开线程。
//判断是否是UI线程
Log.d("MainActivity", "" + (Looper.myLooper() == getMainLooper()));//true
  • Binder死亡问题

Binder运行在服务端进程中,如果服务端异常终止,客户端与服务端的连接就会中断,即Binder死亡。

两种监听binder死亡的方法:

  • Binder的两个方法linkToDeath和unlinkToDeath,通过linkToDeath为Binder设置死亡代理,当连接中断时客户端就会收到通知。
  • 远程service异常关闭时会调用onServiceDisconnected()方法,在此可执行服务的重连机制。【推荐】

linkToDeath的binderDied方法是在Binder线程池中被调用的,onServiceDisconnected()在客户端的UI线程中回调的。

linkToDeath会在service异常关闭时,移除连接信息,并回调onServiceDisconnected方法。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,324评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,356评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,328评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,147评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,160评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,115评论 1 296
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,025评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,867评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,307评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,528评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,688评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,409评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,001评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,657评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,811评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,685评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,573评论 2 353

推荐阅读更多精彩内容