天天看点

第二章 IPC机制

2.1 Android IPC

IPC 为进程间通信或跨进程通信,是指两个进程数据交换的过程。

按照操作系统的描述,线程是CPU调度的最小单元,同时线程是一种有限的系统资源;而进程一般是指一个执行单元,在pc和移动设备上指一个程序或者一个应用。

多进程的情况:

1:一个应用因为某些原因自身需要采用多进程模式来实现,原因有很多:有些模块由于特殊原因需要运行在单独的进程(音乐播放器);webView崩溃不能影响主应用;为了加大一个应用的可使用内存所以需要通过多进程来获取多份内存空间。

2:当前应用需要向其他应用获取数据。通过系统提供的 ContentProvider 去查询数据的时候也是进程间通信。

2.2.1 开启多进程模式

在Android中使用多进程只有一种方法,给四大组件在 AndroidMenifest 中指定 android:process 属性。非常规方法:通过JNI在native层fork出新的进程。

没有给四大组件设置 android:process 属性,那么运行在默认进程中,默认进程名是包名;

设置 android:process 属性的时候,如果加上“:”,属于当前的私有进程,含义是指要在当前的进程名前面附加上当前的包名,其它应用的组件不能和它跑在同一个进程中。

而进程名不以“:”开头的进程属于全局进程,其它应用可以通过ShareUID方式和它跑在同一个进程。

2.2.2 多进程模式的运行机制

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

多进程的影响:所有运行在不同进程的四大组件,只要它们通过内存来共享数据,都会共享失败。

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

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

(3) SharedPreferences 的可靠性下降。

(4) Application 会多次创建。

当一个组件跑在一个新的进程的时候,由于系统在创建新的进程同时分配独立的虚拟机,所以这个过程其实就是启动一个应用的过程,相当于系统又把应用重新启动了一次,自然会创建新的 Application 。理解:运行在同一个进程中的组件是属于同一个虚拟机和同一个 Application 的,运行在不同进程中的组件属于不同的虚拟机和 Application 的。

2.3.1 Serializable 接口

实现了 Serializable 接口的对象写到文件中就可以快速恢复,恢复后的对象和之前的对象内容一样,但是两者不是同一个对象。

原则:序列化后的数据中的 serialVersionUID 只有和当前类的 serialVersionUID 相同才能够正常地被反序列化。

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

如果不手动指定 serialVersionUID 的值,反序列化时如果类有所改变,系统会重新计算当前类的 hash 值并把它赋值给 serialVersionUID,此时当前类的 serialVersionUID 就和序列化的数据中的 serialVersionUID 不一致,反序列化失败,程序出现crash;手动指定 serialVersionUID 的值,能够很大程度上避免反序列化的失败。比如版本升级后,可能删除或增加了某个成员变量,此时反序列化仍能够成功,程序仍然能够最大限度地恢复数据。不指定的话,程序会挂掉;如果当前类结构发生了非常规性改变,比如修改了类名,修改了成员变量的类型,此时尽管 serialVersionUID 验证通过了,反序列化还是会失败,因为类结构有了毁灭性的改变。

特别:1:静态成员变量属于类不属于对象,不会参与序列化过程。2:用 transient 关键字标记的成员变量不参与序列化过程。

2.3.2 Parcelable 接口

Parcelable 也是一个接口,只要实现了这个接口,一个类的对象就可以实现序列化并可以通过 Intent 和 Binder 传递。

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;
    }

    /**
     * 通过Parcel的一系列read方法完成反序列化过程,从序列化后的对象中创建原始对象
     * @param in
     */
    protected User(Parcel in) {
        userId = in.readInt();
        userName = in.readString();
        isMale = in.readByte() != ;
        book = in.readParcelable(Book.class.getClassLoader());
    }

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

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

    /**
     * 内容描述功能由该方法来完成,几乎所有情况下该方法都返回0;仅当当前对象中存在文件描述符的时候,返回1.
     * @return
     */
    @Override
    public int describeContents() {
        return ;
    }

    /**
     * Parcel 内部包装了可序列化的数据,可以在Binder中自由传输
     * writeToParcel:实现序列化功能,通过调用Parcel的一系列write方法来完成,将当前对象写入到序列化结构中
     * flags为1:标识当前对象需要作为返回值返回,不能立即释放资源;
     *      几乎所有情况下为0
     */
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(userId);
        dest.writeString(userName);
        dest.writeByte((byte) (isMale ?  : ));
        dest.writeParcelable(book, flags);
    }
}
           

Parcelable 和 Serializable 都能实现序列化并且都可用于 Intent 间的数据传递,二者怎么选取?

2.3.3 Binder

Binder 是 Android 中的一个类,实现了 IBinder 接口。从 Android Framework 的角度来说,Binder 是 ServiceMannager 连接各种 Mannager (ActivityMannager、WindowMannager等)和 MannagerService 的桥梁;从 Android 的应用层来说,Binder 是客户端和服务端进行通信的媒介,当 bindService 的时候,服务端会返回一个包含了服务端业务调用的 Binder 对象,通过这个对象,客户端就可以获取服务端提供的服务或者数据,这里的服务包括普通服务和基于AIDL的服务。

Android 开发中,Binder 主要用在 Service 中,包括 AIDL 和 Message 。

/**
 * 首先:当客户端发起远程请求时,由于当前线程会被挂起,直至服务端进程返回数据,所以如果某一个远程方法是很耗时的,就不能在UI线程发起此远程请求。
 * 其次:由于服务端的Binder方法运行在Binder的线程池中,,所以Binder方法不管是否耗时,都应该采用同步的方式去实现,因为它已经运行在一个线程了。
 */
public interface IBookMannager extends android.os.IInterface {
    /**
     * 声明一个内部类Stub,这个Stub就是一个Binder类
     */
    public static abstract class Stub extends android.os.Binder implements com.why.a2_ipc.IBookMannager {
        /**
         * Binder的唯一标识,一般用当前的类名表示
         */
        private static final java.lang.String DESCRIPTOR = "com.why.a2_ipc.IBookMannager";
        /** Construct the stub at attach it to the interface. */
        public Stub(){
            this.attachInterface(this, DESCRIPTOR);
        }
        /**
         * 用于将服务端的Binder对象转换成客户端所需的AIDL接口类型的对象,这种转换过程是区分进程的。
         * 如果客户端和服务端位于同一进程,那么此方法返回的就是服务端的Stub对象本身,否则返回的是系统封装后的Stub.proxy对象。
         */
        public static com.why.a2_ipc.IBookMannager asInterface(android.os.IBinder obj) {
            if ((obj==null)) {
                return null;
            }
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            if (((iin!=null)&&(iin instanceof com.why.a2_ipc.IBookMannager))) {
                return ((com.why.a2_ipc.IBookMannager)iin);
            }
            return new com.why.a2_ipc.IBookMannager.Stub.Proxy(obj);
        }

        /**
         * 用于返回当前的Binder对象
         * @return
         */
        @Override
        public android.os.IBinder asBinder() {
            return this;
        }

        /**
         * 这个方法运行在Binder线程池中,当客户端发起跨进程请求时,远程请求会通过这个方法来处理。
         * @param code 可以确定客户端所请求的目标方法是什么
         * @param data 接着从data中取出目标方法所需的参数(如果目标方法有参数的话),然后执行目标方法
         * @param reply 当目标方法执行完毕后,就向reply写入返回值(如果目标方法有返回值的话)
         * @param flags
         * @return 如果此方法返回false,那么客户端的请求会失败。因此可以利用这个特性来做权限验证。
         * @throws android.os.RemoteException
         */
        @Override
        public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
            switch (code) {
                case INTERFACE_TRANSACTION: {
                    reply.writeString(DESCRIPTOR);
                    return true;
                }
                case TRANSACTION_getBookList: {
                    data.enforceInterface(DESCRIPTOR);
                    java.util.List<com.why.a2_ipc.Book> _result = this.getBookList();
                    reply.writeNoException();
                    reply.writeTypedList(_result);
                    return true;
                }
                case TRANSACTION_addBook: {
                    data.enforceInterface(DESCRIPTOR);
                    com.why.a2_ipc.Book _arg0;
                    if ((!=data.readInt())) {
                        _arg0 = com.why.a2_ipc.Book.CREATOR.createFromParcel(data);
                    }
                    else {
                        _arg0 = null;
                    }
                    this.addBook(_arg0);
                    reply.writeNoException();
                    return true;
                }
            }
            return super.onTransact(code, data, reply, flags);
        }

        /**
         * 当服务端和客户端位于不同的进程,方法调用需要走transact过程,这个逻辑由Stub的内部代理类Proxy来完成。
         */
        private static class Proxy implements com.why.a2_ipc.IBookMannager {
            private android.os.IBinder mRemote;
            Proxy(android.os.IBinder remote) {
                mRemote = remote;
            }
            @Override
            public android.os.IBinder asBinder() {
                return mRemote;
            }
            public java.lang.String getInterfaceDescriptor() {
                return DESCRIPTOR;
            }

            /**
             * 这个方法运行在客户端,当客户端调用该方法的时候,内部实现如下:
             * @return
             * @throws android.os.RemoteException
             */
            @Override
            public java.util.List<com.why.a2_ipc.Book> getBookList() throws android.os.RemoteException {
                /**
                 * 首先创建该方法的输入型对象_data、输出型对象_reply和返回值对象_result。
                 * 然后把该方法的参数信息写入_data(如果有参数的话)。
                 */
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                java.util.List<com.why.a2_ipc.Book> _result;
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    /**
                     * 接着调用transact()方法来完成RPC(远程过程调用)请求,同时当前线程挂起。然后服务端的onTransact方法会被调用,
                     * 直到RPC过程返回,当前线程继续执行
                     */
                    mRemote.transact(Stub.TRANSACTION_getBookList, _data, _reply, );
                    _reply.readException();
                    /**
                     * 并从_reply中取出RPC过程的返回结果;
                     */
                    _result = _reply.createTypedArrayList(com.why.a2_ipc.Book.CREATOR);
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
                /**
                 * 最后返回_reply中的数据
                 */
                return _result;
            }

            /**
             * 运行在客户端,执行过程和getBookList类似。
             * @param book
             * @throws android.os.RemoteException
             */
            @Override
            public void addBook(com.why.a2_ipc.Book book) throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    if ((book!=null)) {
                        _data.writeInt();
                        book.writeToParcel(_data, );
                    }
                    else {
                        _data.writeInt();
                    }
                    mRemote.transact(Stub.TRANSACTION_addBook, _data, _reply, );
                    _reply.readException();
                }
                finally {
                    _reply.recycle();
                    _data.recycle();
                }
            }
        }

        /**
         * 用来标识在transact过程中客户端所请求的是哪个方法
         */
        static final int TRANSACTION_getBookList = (android.os.IBinder.FIRST_CALL_TRANSACTION + );
        static final int TRANSACTION_addBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + );
    }
    public java.util.List<com.why.a2_ipc.Book> getBookList() throws android.os.RemoteException;
    public void addBook(com.why.a2_ipc.Book book) throws android.os.RemoteException;
}
           

个人理解:当客户端向binder池发起请求的时候,

IBookMannager.Stub.asInterface(service)

,首先在Binder中,此时还是在客户端进程,通过

binder.queryLocalInterface

查询接口并返回 IInterface 对象,如果该对象非空,说明server位于当前进程,返回当前对象;如果为空,则Server位于不同进程,则新建接口的代理类对象并返回。

如果客户端和服务端位于同一个进程就很容易了,直接调用即可,这里不加描述。如果位于不同进程,则在代理类中调用接口中的方法,在该方法中,会获取Parcel对象data、reply,并给data设置文件描述符,在Binder线程池调用的时候用来判断是不是该方法的参数。然后调用

transact()

,在该方法中调用Binder线程池中的onTransact()方法,根据不同方法的id判断走switch中的哪一个模块,不同的模块对应不同的方法调用。在

onTransact

中,将返回结果写入了reply中,最后在代理类中对应的方法中通过reply取出返回结果。

Binder死亡重新绑定:Binder的两个重要的方法:

linkToDeath

unlinkToDeath

,Binder运行在服务端,如果服务端进程由于某种原因异常终止,此时客户端到服务端的Binder连接断裂(称之为Binder死亡),会导致远程调用失败。如果我们不知道已经断裂,会影响客户端的功能。解决?

通过

linkToDeath

给Binder设置一个死亡代理,当Binder死亡的时候,我们会收到通知,此时就可以重新发起连接请求恢复连接。详细做法:在客户端绑定远程服务成功后,给Binder设置死亡代理:

bookMannager = IBookMannager.Stub.asInterface(binder);
binder.linkToDeath(deathRecipient, );
           

这样当Binder死亡的时候我们就可以收到通知了。

声明一个 DeathRecipient 对象,内部只有一个方法 binderDied ,在该方法中移出之前绑定的binder代理并重新绑定远程服务。

private IBinder.DeathRecipient deathRecipient = new IBinder.DeathRecipient() {
        /**
         * 当binder死亡的时候调用该接口
         */
        @Override
        public void binderDied() {
            Log.i(TAG, "binderDied: ");
            if (bookMannager == null){
                return;
            }
            /**
             * 移除之前的binder代理并重新绑定远程服务
             */
            bookMannager.asBinder().unlinkToDeath(deathRecipient, );
            bookMannager = null;
            bindService(new Intent(getApplicationContext(), MyService.class), new MyConnectService(), Context.BIND_AUTO_CREATE);
        }
    };
           

2.4 Android中的IPC方式

跨进程的通信方式:通过在Intent中附加extras来传递信息;通过共享文件的方式来共享数据;采用Binder的方式来跨进程通信;ContentProvider天生就是跨进程访问的;通过网络传输实现数据传递,所以Socket也可以实现IPC。

2.4.1 使用AIDL

可以使用AIDL实现跨进程的方法调用

1:服务端

服务端首先创建一个Service用来监听客户端的连接请求,然后创建一个AIDL文件,将暴露给客户端的接口在这个AIDL中声明,最后在这个Service中实现这个AIDL接口。

2:客户端

首先绑定服务端的Service,绑定成功后,将服务端返回的Binder对象转换成AIDL所属的接口类型,就可以调用接口中的方法了。

3:AIDL接口的创建

AIDL文件支持的数据类型:

1:基本数据类型(int、long、char、boolean、double等);

2:String 和 CharSequence

3:List:只支持 ArrayList,里面每个元素都必须能够被 AIDL 支持

4:Map:只支持HashMap,里面的每个元素都必须能够被AIDL支持,包括 key 和 value

5:Parcelable:所有实现了Parcelable 接口的对象

6:AIDL:所有的 AIDL 接口本身也可以在AIDL文件中使用

规范:

1:自定义的Parcelable 对象必须要显示import进来。

import com.why.a2_ipc.Book;

2:如果AIDL文件中用到了自定义的Parcelable对象,那么必须新建一个和它同名的AIDL文件,并在其中声明了Parcelable类型。

parcelable Book;

3:AIDL中除了基本数据类型,其他类型的参数必须标上参数方向:in、out或者inout。

4:AIDL只支持方法,不支持声明静态常量。

5:AIDL的包结构在服务端和客户端要保持一致,否则会运行出错,这是因为客户端需要反序列化服务端中和AIDL相关的所有类,如果类的完整路径不一样,就无法成功反序列化,程序也就无法正常运行。

注意:

Service中最好使用

CopyOnWriteArrayList

ConcurrentHashMap

,支持并发读写,由于AIDL方法是在服务端的Binder线程池中执行的,因此多个客户端同时连接的时候,会有多个线程同时访问的情况,因此在AIDL中要处理线程同步。

前面提到AIDL能够使用的List只有ArrayList,为什么使用CopyOnWriteArrayList还可以正常工作?

由于AIDL所支持的是抽象的List,而List只是一个接口,因此虽然服务端返回的是CopyOnWriteArrayList`,但是在Binder中会按照List的规范去访问数据并最终形成新的ArrayList传递给客户端。

在客户端注册监听和解注册监听的时候,传到服务端的是同一个对象,为什么到了服务端就是两个对象了?
           

因为 Binder 会把客户端传递过来的对象重新转化为一个新的对象,虽然注册和解注册过程中使用的是同一个客户端对象,但是通过Binder传递到服务端后,却会产生两个全新的对象。对象是不能跨进程直接传输的,对象的跨进程传输本质上都是反序列化的过程,反序列化的时候会重新生成一个对象。

删除跨进程Listener

RemoteCallbackList 是系统专门提供的删除跨进程的 listener 的接口。是一个泛型,支持管理任意的AIDL接口,从声明就可以看出,因为所有的AIDL接口都继承自IInterface接口。

RemoteCallbackList 原理:内部有一个Map结构专门用来保存所有的AIDL回调,key是IBinder类型,value是CallBack类型。

ArrayMap<IBinder, Callback> mCallbacks = new ArrayMap<IBinder, Callback>();
           

CallBack中封装了listener对象,注册的时候会把listener的信息存入ArrayMap对象mCallbacks 中:

IBinder binder = callback.asBinder();
Callback cb = new Callback(callback, cookie);
mCallbacks.put(binder, cb);
           

虽说多次跨进程传输客户端的同一个对象会在服务端生成不同的对象,但是它们底层的Binder对象是同一个。客户端解注册时,只要遍历服务端所有的 listener ,找出那个和解注册listener具有相同Binder对象的服务端 listener 并把它删除即可。

RemoteCallbackList 内部自动实现了线程同步的功能,注册和解注册时不用做线程同步。

注意

客户端调用远程服务端的方法,被调用的方法运行在服务端的Binder线程池中,同时客户端线程会被挂起,此时如果服务端方法执行比较耗时,会导致客户端线程长时间阻塞,如果客户端线程是UI线程,会导致客户端产生ANR。客户端的

onServiceConnected、onServiceDisconnected

运行在UI线程,

IBinder.DeathRecipient

中的

binderDied

方法运行在客户端的Binder线程池中,访问UI需要使用Handler切换到UI线程。由于服务端本身就运行在服务端Binder线程池中,所以服务端方法本身就可以执行大量耗时操作,不建议在服务端方法中开线程进行异步任务。同理,当远程服务端需要调用客户端中 listener 的方法时,被调用的方法也运行在Binder线程池中,只不过是客户端的Binder线程池中,所以同样不能在服务端中的UI线程中调用客户端的耗时方法,否则会导致服务端无法响应。

重新连接服务的两种方式

1:设置

DeathRecipient

监听,在客户端的Binder线程池中被回调。

2:在

onServiceDisconnected

中重连远程服务,在UI线程中被回调。

权限验证

1:在Service的

onBinder

中进行验证,不通过返回null。

@Override
    public IBinder onBind(Intent intent) {
        int permission = checkCallingOrSelfPermission("com.why.a2_ipc.permission.ACCESS_BOOK_SERVICE");
        if (permission == PackageManager.PERMISSION_DENIED){
            Log.i(TAG, "onBind: 权限验证失败");
            return null;
        }
        return myServiceBinder;
    }
           

验证的时候在AndroidMenifest.xml中声明权限,亲测以下两个都要写,否则不会验证成功。

<uses-permission android:name="com.why.a2_ipc.permission.ACCESS_BOOK_SERVICE"/>
    <permission android:name="com.why.a2_ipc.permission.ACCESS_BOOK_SERVICE" android:protectionLevel="normal"/>
           

2:在服务端的

onTransact

方法中进行权限验证。可以采用permission验证,类似第一种方法,还可以采用Uid和Pid做验证。

String packageName = null;
            String[] packagesForUid = getPackageManager().getPackagesForUid(getCallingUid());
            if (packagesForUid != null && packagesForUid.length > ){
                packageName = packagesForUid[];
            }
            if (!packageName.startsWith("com.why")){
                Log.i(TAG, "onTransact: 权限验证失败");
                return false;
            }
           

2.4.2 使用Bundle

四大组件中的Activity、Service、Receiver支持在 Intent 中传递 Bundle 数据,Bundle 实现了 Parcelable 接口,可以方便地在进程间传输,传输的数据必须能够被序列化。

2.4.3 使用文件共享

两个进程通过读/写同一个文件来交换数据。Android基于Linux,其并发读写可以无限制的进行,甚至两个线程同时对一个文件进行写操作都是可以的。还可以序列化一个对象到文件系统的同时从另一个进程中恢复对象,这两个对象不是同一个。

SharedPreferences是个特例:由于系统对它的读写有一定的缓存策略,在内存中会有一份文件缓存,当面对高并发读写访问,SharedPreferences 有很大可能会丢失数据。

2.2.4 使用Messenger

Messenger底层实现是AIDL,由于它一次处理一个请求,因此在服务器我们不用考虑线程同步的问题,因为服务端不存在并发执行的功能。

2.4.5 使用ContentProvider

2.4.6 使用 Socket

Socket 也成为“套接字”,是网络通信中的概念,分为流式套接字和用户数据报套接字两种,分别对应于网络的传输控制层中的TCP和UDP协议。TCP协议是面向连接的协议,提供稳定的双向通信功能,TCP的连接的建立需要通过三次握手,为了提供稳定的数据传输功能,本身提供了超时重传机制,因此具有很高的稳定性;而UDP是无连接的,提供不稳定的单向通信功能,当然UDP也可以实现双向通信功能。

2.5 Binder 连接池

当项目越来越庞大的时候,随着AIDL数量的增加,Service 数量也是不断增加。Service是四大组件之一,太多的 Service 使得应用看起来很重量级。

Binder 连接池的工作机制:每个业务模块创建自己的 ADIL 接口并实现此接口,此时不同业务模块之间是不能耦合的,实现细节要单独开来,然后向服务端提供自己的唯一标识和其对应的 Binder 对象;对于服务端来说,只需要一个 Service 就可以了,服务端提供一个 queryBinder 接口,该接口能够根据业务模块的特征来返回相应的 Binder 对象给它们,不同的业务模块拿到所需的 Binder 对象后就可以进行远程的方法调用了。由此可见, Binder 的主要作用就是将每个业务模块的 Binder 请求统一转发到远程 Service 中去执行,从而避免重复创建 Service 的过程。

实现:提供一个接口:

interface IBinderPool {
    IBinder queryBinder(int binderCode);
}
           

在 Service 中返回该接口的实现类对象给客户端,而该接口的实现类在 BinderPool 中。

2.6 选用合适的 IPC 方式

第二章 IPC机制

继续阅读