Android开发艺术(2)——IPC机制

代码地址:https://github.com/kaikaixue/DevelopArtistical

IPC简介

IPC就是进程间通讯的简称

线程是一种有限的资源

Windows:剪切板、管道、油槽等进行IPC

Linux:命名管道、共享内容、信号量等

Android:继承自Linux,但是通讯方式不完全继承Linux,最有特色的是Binder

android一个应用可以开启多进程模式,为什么要开启?

1.android对每个应用的内存使用做了限制,比如16GB(当然现在版本不是了)用多进程可以扩大这个内存

2.可能某些模块需要运行在单独的进程等

Android中的多进程

  • 如果两个应用跑在同一个进程中,那么可以共享data目录、组件信息、内存等,或者说他们就像是同一个应用的两部分
  • 清单文件中process可以指定全名也可以指定“:名称”,前者是全局进程,其他应用可以通过shareUID和他跑在同一个进程,后者是应用下的私有进程其他应用的组件不可以和他跑在同一进程
  • P39这块理解有太多问题,有时间回来再看看,看是否可以理解

android会为每个应用程序,或者说每个进程分配一个独立的虚拟机,每个之间互不干扰,比如现在有com.xk.demo和com.xk.demo:remote,两个进程,他们同时访问Demo.class这个类,其实访问的是两个不同的对象,互不干扰的。

不同进程中的组件,通过内存共享数据就会失败

通常会有以下几种问题:

  • 静态成员、单例模式失效
  • 线程同步机制失效
  • SharedPreferences可靠性下降
  • Application多次创建

同一应用间的多进程其实就是相当于两个不同的应用使用shareUID的模式(毕竟Application创建了两次,所以说是两个应用也不为过)

共享内存不可行了,那么就要有一些别的方法来实现进程间通讯

IPC基础概念

Serializable

  • 静态成员不属于类成员,不参与序列化
  • transient修饰的成员变量不参与序列化
  • 序列化后,会在文件中存储serializableid,反序列化的时候会和他进行比对,如果不一致,就crash,这就是为什么要写serializable的原因,防止类的结构改变后反序列化失败,当然修改了类名,字段类型等之后,无法反序列化了

Parcelable

User实现了parcelable,User中有Book也实现了parcelable,那么在序列化User中的Book的时候需要传入当前线程上下文的classloader,否则会报找不到类的错误

二者的区别:Parcelable是android提供的,效率高,更适合android平台,但是使用起来麻烦,主要用于内存序列化中,在网络上传输之类的(进程间利用binder通信貌似需要的是Parcelable)。Serializable是java提供的,序列化的时候要做大量的io操作,会产生大量的临时变量,引起频繁的GC。在存储到本地的情况下,最好用serializable。

Binder

简单的实现了一个aidl代码(查看此处)

5.0之后需要显示调用service,不可以使用action、filiter的形式隐式启动

最恶心的binder来了。。。

 /**
  * ICalculate是用来做计算功能的,那么他是在服务端的,他的实现(继承自Stub)也是在服务端的,而客户端需要调用它里面的方法,需要的就是这个接口对象,该方法的作用就是把Stub转换成可以在binder中传播的接口
  * 如果客户端和服务的在同一进程中,那么可以共享数据,所以直接返回这个Stub就好了,如果不在同一个进程中,就创建一个代理对象Stub.Proxy,并返回
  * Cast an IBinder object into an com.xk.chapter2.ICalculate interface,
  * generating a proxy if needed.
  */
public static com.xk.chapter2.ICalculate asInterface(android.os.IBinder obj) {
    //...Stub里面的asInterface方法
}
/**
 * 该方法是服务端的方法(书上的话来说就是运行在Binder线程池,反正跟客户端不在一个进程中),只有在客户端、服务端不在一个进程的情况下才会走这里(如果在同一进程,asInterface会直接返回Stub对象,直接调用Stub里面的实现)
 * 如果不在同一进程,asInterface会返回代理对象(Stub.Proxy),当客户端调用方法的时候,内部会像下面一样,把客户端的参数写入到_data,它是一个parcel(parcelable可知,这个东西可以binder中传输),然后还有一个code(code标记了调用的是接口中的哪个方法)
 * 同时还有一个_reply,用来承载返回结果。然后_data、code、_reply等,这几个参数调用一个方法mRemote.transact(),很明显,这个是个远程方法,通过一系列底层实现,最后就会回调到服务端的这个onTransact方法(前面是在客户端,现在到了服务端了),然后服务端调用真实的方法实现,得到结果,写入
 * 到_reply中,这里有个返回值,返回false,那么客户端的请求就会失败,可以用此来对方法权限做限制,毕竟不是所有的客户端都可以调用服务端的
 * @param code
 * @param data
 * @param reply
 * @param flags
 * @return
 * @throws android.os.RemoteException
 */
@Override
public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
    //...Stub.onTransact()
}

说明一下:客户端线程中发起一个服务端请求,客户端线程就会被挂起,直到服务端返回数据,客户端才会继续执行,所以如果服务端的操作比较耗时,客户端应该放在子线程中去做。对于服务端,无论是否耗时,都不需要再开线程,而是要写成同步的,因为它本来就是运行在binder线程池中的。

[图片上传失败...(image-f4724-1510575485006)]

疑问:Proxy是如何实现两个进程通讯的 没看出来

在bindService的地方会创建一个ServiceConnection

ServiceConnection conn = new ServiceConnection() {

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
        //这里的service,如果是在同一进程中,为Service的onBind中返回的那个类型,如果不是同一进程,这里返回的就是BindProxy,然后调用asInterface,拿到的就是对应的IBookManager了(Service的onBind返回的对象或者根据这个BindProxy对象创建的Proxy)
            iBookManager = IBookManagerStub.asInterface(service);
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }

binder死亡代理

ServiceConnection conn = new ServiceConnection() {

    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        iBookManager = IBookManagerStub.asInterface(service);
        service.linkToDeath(recipient,0);
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {

    }
};

如上,在binder连接成功后,为它设置死亡代理,第一个参数是一个接口,当binder断开后,会回调这个接口,可以在接口里面做解除绑定的操作,然后继续重新bindService即可

Android中的IPC方式

Bundle

这个很简单,intent中可以传递它,它可以承载基本数据类型、Serializable、Parcellabled等

文件共享

Windows中读写文件会给文件加上排斥锁,使得其他线程无法访问。Linux不会,android基于Linux所以也一样,所以可以同时读写一个文件,但是很明显,这样会有问题发生。

记得大学时做一个App的数据缓存就用到了这个,一个进程中把一个对象序列化到本地,另一个进程中直接反序列化成对象即可。需要注意的是,这两个对象只是内容一样,本质上是两个不同的对象。

对于同步性要求不太高的情况,我们是可以通过共享文件来实现多进程通讯。SharedPreferences也是文件,但是他比较特殊,他是存储在包名目录下的,而且他会在内存中有缓存策略,所以在多进程并发操作的时候就不靠谱了,很大几率会丢失数据(多进程内存是不可以共享的),所以虽然他也是文件,但是极不推荐使用。

Messenger

其实就是封装的AIDL,不过使用起来比较方便,传递的是Message对象,可以承载what、arg1、arg2、Bundle、replyTo等,他的obj属性在android2.2之前不支持跨进程传输,2.2之后也仅支持系统提供的Parcelable对象。Bundle可以支持很多数据类型,不过测试发现自定义的Parcelable好像也不行。具体看代码。

AIDL

Messenger就是封装的AIDL,他只适合简单的消息传递,如果有大量的并发请求,或者调用函数,就不合适了。

AIDL支持:

  • 基本数据类型
  • String和CharSequence
  • List:支持吃ArrayList,并且每个元素需要被AIDL支持(注意,没了良好的控制并发读写,在服务端可以用CopyOnWriteArrayList代替ArrayList,他不是继承自ArrayList,但是不会报错,因为AIDL接受的是List接口,在传到客户端的时候,CopyOnWriteArrayList会被以ArrayList的形式转换出去)
  • Map:支持HashMap,key、value都需要被AIDL支持
  • Parcelable:实现了Parcelable的对象(使用的时候不论是否在同一个包内,都需要显示的import,而且还要新建一个同样名字的aidl文件)
  • AIDL自身的接口(这个在demo中用了,可以看代码)

除了基本数据类型,都要加上in、out、inout

aidl接口只支持方法,不支持生命就静态变量

客户端调用服务端方法后,会等待服务端方法执行完,服务端方法在Binder线程池中,所以如果服务端方法耗时的话,客户端就会被挂起,所以客户端要注意时间太长要开线程

关于断开重连:

  • 设置死亡代理,前面讲过(UI线程中回调)
  • 在ServiceConnection的回调中设置(客户端的binder线程池中回调)

关于监听器:

对象在多进程中是无法传输的(内存不共享),对象的传输本质是序列化和反序列化(实现parcelable的原因),所以当客户端设置一个监听器对象给服务端的时候,服务端解除这个监听时,对象不是一个,所以可以用系统提供的类RemoteCallbackList代替普通的List去维护监听器的集合

关于权限控制

  • service的onbind中校验权限,然后返回binder对象或者null(onbind运行在服务端的UI线程中,不是一个binder调用,只能验证服务端的权限,所以没意义,所以不用了)
  • Stub.onTransact中返回false表示拒绝客户端请求,具体看demo

以上任何疑问直接看demo

ContentProvider

关于清单文件中的注册






关于Provider的定义

注意,内容观察者底层也是通过binder实现的,但是比aidl用起来简单,他仅仅是帮助我们实现了进程间通讯,具体的数据存储、查询之类的 还是需要我们自己完成的,可以使用sqlite、sp、甚至文件 各种都无所谓,但是可能被多个进程、多个线程同时调用(想想通讯录),所以对于线程同步需要考虑,如果用的是一个sqlitehelper的话,系统以及实现了,总之视情况而定吧

oncreate是被系统调用的,运行在主线程。其他几个方法是运行在binder线程池中,打印日志即可知道,代码中已经写好,详情见代码

在静态代码块中指定一些匹配规则,如下代码,然后在增删改查方法中就可以匹配了

static UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

static {
    //在匹配规则中添加两条规则:uri后面的path是book的时候,可以匹配为0,user时可以匹配为1,然后在增删改查中把uri转换成对应的0、1
    uriMatcher.addURI("com.xk.chapter2.provider.BookProvider", "book", 0);
    uriMatcher.addURI("com.xk.chapter2.provider.BookProvider", "user", 1);
}

@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
    int uriCode = uriMatcher.match(uri);
    String tableName = getTableName(uriCode);//根据匹配的code,返回对应的文件名、数据库名等
    // TODO: by xk 2017/9/17 15:37 这里操作数据库、文件等查询操作
    Log.d("BookProvider", "query-->" + Thread.currentThread());
    return null;
}

观察者模式

数据内容改变之后,可以利用notifyChange通知观察者,这个想想手机通讯录即可

@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
    Log.d("BookProvider", "insert-->" + Thread.currentThread());
    // TODO: by xk 2017/9/17 15:37 参照query,找到操作的表,然后存数数据
    getContext().getContentResolver().notifyChange(uri, null);//通知观察者,这个uri下的数据被改变了
    return null;
}

自定义调用方法

使用call可以实现自定义方法调用,具体的使用方式是很灵活的,我随便写个demo,不一定准确,就是一种思路,不过一般也不这么写,随意发挥即可

//除了增删改查,还可以通过call方法实现远程进程中方法的调用,这么一来不就是跟aidl实现的效果一样了吗,还很简单,来试一试(这里只是我瞎猜的写的一种使用办法,随机应变喽)(注意该方法是在binder线程池中,阻塞没关系,但是他的调用者——客户端就需要注意了)
@Nullable
@Override
public Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) {
    Bundle bundle = new Bundle();

    String[] as = arg.split("a");
    if (method.equals("add")) {
        int add = add(Integer.parseInt(as[0]), Integer.parseInt(as[1]));
        bundle.putInt("result", add);

    } else if (method.equals("sub")) {
        int add = sub(Integer.parseInt(as[0]), Integer.parseInt(as[1]));
        bundle.putInt("result", add);
    }
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return bundle;
}


private int add(int a, int b) {
    return a + b;
}

private int sub(int a, int b) {
    return a - b;
}

这是provider中的代码 ,在客户端直接像调用增删改查一样,调用call即可,三个参数的含义直接看上面这个方法就行

关于Provider的使用

以下是在客户端的代码,如果provider中声明了权限,记得在清单文件中声明,测试发现,如果客户端、服务端是同一个app的不同进程,不需要声明权限(猜测:毕竟是同一个app,权限展示也是以app为基准的吧)

  • 查询

    Uri uri = Uri.parse("content://com.xk.chapter2.provider.BookProvider");
    Log.d("ProviderActivity", "onClick-->" + Thread.currentThread());
    getContentResolver().query(uri, null, null, null, null);
    
  • call

    Uri uri3 = Uri.parse("根据需求写");
    Log.d("ProviderActivity", "onClick-->" + Thread.currentThread());
    Bundle add = getContentResolver().call(uri3, "sub", "1a2", new Bundle());
    Toast.makeText(this, add.getInt("result")+"", Toast.LENGTH_SHORT).show();
    

uri的话,后面加path,然后在provider中解析后可知要访问的数据(是book还是user),path后面还可以加别的参数,其实这只是一个约定的规范而已,随便传,然后字符串解析应该也没问题吧,不过估计没人那么干,会被同事骂的。

Socket

服务的readLine,那么客户端需要writeLine,或者末尾加\n,这个折腾了好久,之前一直发不过去数据

Binder连接池

一个aidl对应一个service,很明显是不好的,毕竟service是重量级的,在设置的app详情中也是可以看到的,所以如果写一个Binder连接池,通过code直接返回对应的IBinder,最好了,详情见代码

选择合适的IPC方式

[图片上传失败...(image-e48dc5-1510575485007)]

你可能感兴趣的:(Android开发艺术(2)——IPC机制)