《Android开发艺术探索》读书笔记——IPC机制

一、Android IPC简介

(1)IPC是Inter-Process Communication的缩写,含义为进程间通信或者跨进程通信,是指两个进程之间进行数据交换的过程。

(2)线程是CPU调度的最小单位,进程一般指一个执行单元,在PC和移动设备上指一个程序或者一个应用。一个进程可以包含多个线程,因此进程和线程是包含与被包含的关系。

(3)一个进程中可以只有一个线程,即主线程,在Android里面主线程也叫UI线程,在UI线程里才能操作界面元素。

(4)ANR:如果在Android应用的主线程执行大量耗时任务,会造成界面无法响应,系统会抛出ANR(Application Not Responding),即应用无响应。

二、Android中的多线程模式


开启多线程

(1)在Android中开启多进程普遍使用的方法,那就是给四大组件在AndroidMenifest中指定android:process属性。

<span style="font-size:14px;">        <activity
            android:name=".SecondActivity"
            android:configChanges="screenLayout"
            android:label="@string/app_name"
            android:process=":remote" />
        <activity
            android:name=".ThirdActivity"
            android:configChanges="screenLayout"
            android:label="@string/app_name"
            android:process="com.ryg.chapter_2.remote" /></span>

SecondActivity和ThirdActivity的android:process属性分别为“:remote”和“com.ryg.chapter_2.remote”。其中“:”是一种简写的方法,指要在当前的进程名前面附加上当前的包名,SecondActivity的完整进程名为 com.ryg.chapter_2:remote,而ThirdActivity的声明方式是一种完整的命名方式。其次,进程名以“:”开头的进程属于当前应用的私有进程,其他应用的组件不可以和它跑在同一个进程中,而进程名不以“:”开头的进程属于全局进程,其他应用可以通过ShareUID的方式和它跑在同一个进程中。

(2)Android系统会为每个应用分配一个唯一的UID,具有相同UID的应用才能共享数据。两个应用通过ShareUID的方式跑在同一个进程是有要求的,需要这两个应用有相同的ShareUID并且签名相同才可以。


多进程模式的运行机制

一般来说,使用多进程会造成如下几个方面的问题:

(1)静态成员和单例模式完全失效。

Android为每一个应用分配了一个独立的虚拟机,或者说为每一个进程都分配了一个独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间,这就导致在不同的虚拟机中访问同一个类的对象会产生多份副本。

(2)线程同步机制完全失效。

第2个问题本质上和第1个问题是类似的,既然都不是一块内存了,那么不管是锁对象还是锁全局类都无法保证线程同步,因为不同进程锁的不是同一个对象。

(3)SharePreferences的可靠性下降。

SharedPreferences不支持两个进程同时去执行写操作,否则会导致一定几率的数据丢失,这是因为SharedPreferences底层是通过读/写XML文件来实现的,并发写显然是可能出问题的,甚至并发读/写都有可能出问题。

(4)Application会多次创建。

当一个组件跑在一个新的进程中的时候,由于系统要在创建新的进程同时分配独立的虚拟机,所以这个过程其实就是启动一个应用的过程。因此,相当于系统又把这个应用重新启动了一遍,自然会创建新的Application。


IPC基础概念介绍

本节主要介绍IPC中的一些基础概念,主要包含三方面内容:Serializable接口、Parcelable接口以及Binder。

Serializable接口

(1)Serializable是Java所提供的一个序列化接口,它是一个空接口,为对象提供标准的序列化和反序列化操作。

(2)serialVersionUID是一个long类型的数字,是用来辅助序列化和反序列化过程的,原则上序列化后的数据中的serialVersionUID只有和当前类的serialVersionUID相同才能够正常地被序列化。一般来说,我们应该手动指定serialVersionUID的值。

(3)serialVersionUID的详细工作机制:序列化的时候系统会把当前类的serialVersionUID写入序列化的文件中(也可能是其他中介),当反序列化的时候系统会检测文件中的serialVersionUID,看它是否和当前类的serialVersionUID一致,如果一致就说明序列化的类的版本和当前类的版本是相同的,这个时候可以成功反序列化。

(4)通过Serializable方式实现对象的序列化和反序列化很简单,只需要采用ObjectOutputStream和ObjectInputStream即可轻松实现。

<span style="font-size:14px;">//User类已经实现了Serializable接口
//序列化过程
    	User user = new User(0, "jake", true);
    	ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cache.txt"));
    	out.writeObject(user);
    	out.close();
    	
    	//反序列过程
    	ObjectInputStream in = new ObjectInputStream(new FileInputStream("cache.txt"));
    	User newUser = (User) in.readObject();
    	in.close();</span>
(5)静态成员变量属于类不属于对象,所以不会参与序列化过程;其次用transient关键字标记的成员变量不参与序列化过程。

Parcelable接口

(1)Parcelable接口是Android中提供的新的序列化接口。

(2)一个类的对象只要实现Parcelable接口,就可以实现序列化并可以通过Intent和Binder传递。下面的示例是典型用法:

<span style="font-size:14px;">public class User implements Parcelable {

    public int userId;
    public String userName;
    public boolean isMale;

    public Book book;

    public User(int userId, String userName, boolean isMale) {
        this.userId = userId;
        this.userName = userName;
        this.isMale = isMale;
    }

    //返回当前对象的内容描述。如果含有文件描述符,返回1,否则返回0,几乎所有情况都返回0
    public int describeContents() {
        return 0;
    }

    //将当前对象写入序列化结构中,其中flags标示为1时标识当前对象需要作为返回值,不能立即释放资源,几乎所有情况都为0
    public void writeToParcel(Parcel out, int flags) {
        out.writeInt(userId);
        out.writeString(userName);
        out.writeInt(isMale ? 1 : 0);
        out.writeParcelable(book, 0);
    }

    public static final Parcelable.Creator<User> CREATOR = new Parcelable.Creator<User>() {
        //从序列化后的对象中创建原始对象
        public User createFromParcel(Parcel in) {
            return new User(in);
        }

        //创建制定长度的原始对象数组
        public User[] newArray(int size) {
            return new User[size];
        }
    };

    private User(Parcel in) {
        userId = in.readInt();
        userName = in.readString();
        isMale = in.readInt() == 1;
        book = in.readParcelable(Thread.currentThread().getContextClassLoader());
    }
}
</span>
(3)Parcelable是Android中的序列化方式,因此更适合用在Android平台上,它的缺点是使用起来稍微麻烦,但是它的效率很高;Serializable比较适合将对象序列化到存储设备中或者将对象序列化后通过网络传输。

Binder

(1)Binder是Android中的一个类,它实现了IBinder接口。从IPC角度来说,Binder是Android中的一种跨进程通信方式,Binder还可以理解为一种虚拟的物理设备,它的设备驱动是/dev/binder;从Android Framework角度来说,Binder是ServiceManager连接各种Manager(ActivityManager、WindowManager,等等)和ManagerService的桥梁;从Android应用层来说,Binder是客户端和服务端进行通信的媒介,当bindService的时候,服务端会返回一个包含了服务端业务调用的Binder对象,通过这个Binder对象,客户端就可以获取服务端提供的服务或者数据,这里的服务包括普通服务和基于AIDL的服务。

(2)aidl工具根据aidl文件自动生成的java接口的解析:首先,它声明了我们在aidl文件中所声明的方法,并声明了整型的id分别用于标识这些方法,id用于标识在transact过程中客户端所请求的是哪个方法。接着,它声明了一个内部类Stub,这个Stub就是一个Binder类,当客户端和服务端都位于同一个进程时,方法调用不会走跨进程的transact过程,而当两者位于不同进程时,方法调用需要走transact过程,这个逻辑有Stub的内部代理类Proxy来完成。所以,这个接口的核心实现就是它的内部类Stub和Stub的内部代理类Proxy,下面将详细介绍针对这两个类的每个方法的含义:

  • DESCRIPTOR:Binder的唯一标识,一般用当前Binder的类名表示。
  • asInterface(android.os.IBinder obj):用于将服务端的Binder对象转换成客户端所需的AIDl接口类型的对象,这种转换过程是区分进程的,如果客户端和服务端位于同一进程,那么此方法返回的就是服务端的Stub对象本身,否则返回的是系统封装后的Stub.proxy对象。
  • asBinder:此方法用于返回当前Binder对象。
  • onTransact:这个方法运行在服务端中的Binder线程中,当客户端发起跨进程请求时,远程请求会通过系统底层封装后交由此方法处理。该方法的原型为
    <span style="font-size:14px;">public Boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags)</span>
    服务端通过code可以确定客户端所请求的目标方法是什么,接着从data中取出目标方法所需的参数(如果目标方法有参数的话),然后执行目标方法。当目标方法执行完毕后,就向reply中写入返回值(如果目标方法有返回值的话)。需要注意的是,如果此方法返回false,那么客户端的请求就会失败,因此我们可以利用这个特性来做权限验证。
  • Proxy#[method]:代理类的接口方法,这些方法运行在客户端,当客户端远程调用这些方法时,首先创建方法所需要的输入型Parcel对象_data、输出型Parcel对象_reply和返回值对象List;然后把该方法的参数信息写入_data中(如果有参数的话);接着调用transact方法来发起RPC(远程过程调用)请求,同时当前线程挂起;然后服务端的onTransact方法会被调用,直到RPC过程返回后,当前线程继续执行,并从_reply中取出RPC过程额返回结果;最后返回_reply中的数据。

(3)当客户端发起远程请求时,由于当前线程会被挂起直至服务端进程返回数据,所以如果一个远程方法是很耗时的,那么不能在UI线程中发起此远程请求。

(4)AIDL文件并不是必要的,我们完全可以不提供AIDL文件即可实现Binder,之所以提供AIDL文件,是为了方便系统为我们生成代码。

(5)给Binder设置死亡代理(Binder的两个很重要的方法linkToDeath和unlinkToDeath):

<span style="font-size:14px;">    //当Binder死亡的时候,系统就会回调binderDied方法,然后我们就可以移出之前绑定的binder代理并重新绑定远程服务
    private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
        @Override
        public void binderDied() {
            Log.d(TAG, "binder died. tname:" + Thread.currentThread().getName());
            if (mRemoteBookManager == null)
                return;
            mRemoteBookManager.asBinder().unlinkToDeath(mDeathRecipient, 0);
            mRemoteBookManager = null;
            // TODO:这里重新绑定远程Service
        }
    };

    //在客户端绑定远程服务成功后,给binder设置死亡代理
    mService = IMessageBoxManager.stub.asInterface(binder);
    binder.linkToDeath(mDeathRecipient, 0);
</span>


Android中的IPC方式

本节开始详细分析各种跨进程通信方式

使用Bundle

(1)Activity、Service、Receiver都支持在Intent中传递Bundle数据,由于Bundle实现了Parcelable接口,所以它可以方便地在不同的进程间传输。

(2)使用Bundle传输的数据必须能够被序列化,比如基本类型、实现了Parcelable接口的对象、实现了Serializable接口的对象以及一些Android支持的特殊对象。

使用文件共享

(1)文件共享是两个进程通过读/写同一个文件来交换数据,实现不同进程间的通信。

(2)文件共享方式适合在对数据同步要求不高的进程间进行通信,并且要妥善处理并发读/写的问题。

(3)SharedPreferences是个特例,由于系统对它的读/写有一定的缓存策略,即在内存中会有一份SharedPreferences文件的缓存,因此在多线程模式下,系统对它的读/写就变得不可靠,当面对高并发的读/写访问,SharedPreferences有很大几率会丢失数据,因此,不建议在进程间通信中使用SharedPreferences。

使用Messenger

(1)Messenger是一种轻量级的IPC方案,它的底层实现是AIDL,通过它可以在不同进程中传递Message对象,在Message中放入我们需要传递的数据,就可以轻松实现数据的进程间传递了。

(2)Messenger是以串行的方式处理客户端发来的消息,Messenger的作用主要是为了传递消息。

(3)实现一个Messenger有如下几个步骤:

  1. 服务端进程:首先,我们需要在服务端创建一个Servicee来处理客户端的连接请求,同时创建一个Handler并通过它来创建一个Messenger对象,然后在Service的onBind中返回这个Messenger对象底层的Binder即可。
  2. 客户端进程:客户端进程中,首先要绑定服务端的Service,绑定成功后用服务端返回的IBinder对象创建一个Messenger,通过这个Messenger对象就可以向服务端发送消息了,发消息类型为Message对象。如果需要服务端能够回应客户端,就和服务端一样需要创建一个Handler并创建一个新的Messenger,并把这个Messenger对象通过Message的replyTo参数传递给服务端,服务端通过这个replyTo参数就可以回应客户端。

使用AIDL

(1)使用AIDL进行进程间通信的流程:

  1. 服务端:服务端首先要创建一个Service用来监听客户端的连接请求,然后创建一个AIDL文件,将暴露给客户端的接口在这个AIDL文件中声明,最后在Service中实现这个AIDL接口即可。
  2. 客户端:首先需要绑定服务端的Service,绑定成功后,将服务端返回的Binder对象转成AIDL接口所属的类型,接着就就可以调用AIDL中的方法了。

(2)AIDL文件支持的数据类型:

  • 基本数据类型(char、int、long、double、boolean等);
  • String和CharSequence;
  • List:只支持ArrayList,里面每个元素都必须能够被AIDL支持;
  • Map:只支持HashMap,里面的每个元素都必须被AIDL支持,包括key和value;
  • Parcelable:所有实现了Parcelable接口的对象;
  • AIDL:所有的AIDL接口本身也可以在AIDL文件中使用。

(3)自定义的Parcelable对象和AIDL对象必须要显示import进来,不管它们是否和当前的AIDL文件位于同一个包内。

(4)AIDL文件中用到了自定义的Parcelable对象,那么必须新建一个和它同名的AIDL文件,并在其中声明他为Parcelable类型。

(5)AIDL中除了基本数据类型,其他类型的参数必须标上方向:in、out或者inout,in表示输入型参数,out表示输出型参数,inout表示输入输出型参数。

(6)RemoteCallbackList是系统专门提供的用于删除跨进程listener的接口。RemoteCallbackList是一个泛型,支持管理任意的AIDL接口,因为所有的AIDL接口都继承自IInterface接口。

(7)遍历RemoteCallbackList,必须要按照下面的方式进行,其中beginBroadcast和finishBroadcast必须要

<span style="font-size:14px;">final int N = mListenerList.beginBroadcast();
        for (int i = 0; i < N; i++) {
            IOnNewBookArrivedListener l = mListenerList.getBroadcastItem(i);
            if (l != null) {
                //TODO 
            }
        }
        mListenerList.finishBroadcast();</span>

(8)客户端的onServiceConnected和onServiceDisconnected方法都运行在UI线程中,所以不可以在它们里面直接调用服务端的耗时方法。

(9)服务端进程意外停止,Binder意外死亡时,有以下方法可以重新连接服务:

  1. 第一种方法是给Binder设置DeathRecipient监听,当Binder死亡时,binderDied方法会被回调,在binderDied方法中就可以重连远程服务。
  2. 第二种方法是在onServiceDisconnected中重连远程服务。

这两种方法的区别在于:onServiceDisconnected方法在客户端的UI线程中被回调,而binderDied方法在客户端的Binder线程池中被回调。

(10)给服务加入权限验证功能:

  1. 第一种方法是在onBind中进行验证,验证不通过就直接返回null,这样验证失败的客户端直接无法绑定服务。
  2. 第二种方法是在服务端的onTransact方法中进行验证,如果验证失败就直接返回false,这样服务端就会终止执行AIDL中的方法从而达到保护服务端的效果。

使用ContentProvider

(1)ContentProvider是Android中提供的专门用于不同应用间进行数据共享的方式,ContentProvider的底层实现是Binder。

(2)ContentProvider主要以表格的形式来组织数据,并且可以包含多个表,ContentProvider还支持文件数据,比如图片、视频等。

(3)ContentProvider对底层的数据存储方式没有任何要求,级可以使用SQLite数据库,也可以使用普通的文件,甚至可以采用内存中的一个对象来进行数据的存储。

使用Socket

(1)Socket也称为“套接字”,是网络通信中的概念,它分为流式套接字和用户数据报套接字两种,分别对应于网络的传输控制层中的TCP和UDP协议。

(2)通过Socket不仅仅能实现进程间的通信,还可以实现设备间的通信。


Binder连接池

(1)当项目越来越庞大时,随着AIDL数量的增加,我们不能无限制地增加Service,因为Service本身就是一种系统资源,而且太多的Service会使得我们的应用看起来很重量级,所以我们应该将所有的AIDL放在同一个Service中去管理。

(2)Binder连接池的作用:将每个业务模块的Binder请求统一转发到远程Service中去执行,从而避免重复创建Service的过程。

(3)工作机制:每个业务模块创建自己的AIDL接口并实现此接口,这个时候不同业务模块之间是不能有耦合的,所有实现细节要单独开来,然后向服务端提供自己的唯一标识和对应的Binder对象;对于服务端来说,只需要一个Service就可以了,服务端提供一个queryBinder接口,这个接口能够根据业务模块的特征来返回相应的Binder对象给它们,不同的业务模块拿到所需的Binder对象后就可以进行远程方法调用了。


选用合适的IPC方式


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值