《Android开发艺术探索》笔记总结——第二章:IPC机制

记录下之前自己面试的几个问题:

提问:Android底层的IPC机制是什么?

答:Binder

提问:Linux中还有那些进程间通讯的方式?

答:共享内存、消息队列、Socket、管道、共享文件、信号量

提问:Linux中还有很多进程间通讯的方式,问什么Android选择了Binder?

答:emmmm

毫无疑问最后跪了,虽然之前也了解了Binder的运行机制,但是从来没有想过这个问题,这准备这篇笔记的时候特意的去查了一下这个问题,参考链接放到最后。

一:Android中开启多进程

在Manifest文件中给组件添加process属性,这样此组件就会运行到单独的进程中,但是在设置的时候有个细节,process属性可以自定义一个字符串,有时候看到有这样的写法:

process = ":remote"

这样书写,这个进程的名字是:包名:remote。有冒号和没冒号的区别是:进程名以 “:” 开头的进程属于当前应用的私有进程,其他应用的组件不可以和它跑在同一个进程中,而进程名不以 “:” 开头的进程数据全局进程,其他应用通过 ShareUID 方式可以和它跑在同一个进程中,此时两个进程可以访问对方的私有数据,比如data目录等

多进程会造成的问题:

1)静态成员和单利模式完全失效
2)线程同步机制完全失效
3)SharedPreferences 可靠性下降
4)Application会多次创建

问题1 、问题2 是因为系统会为每个进程分配独立的虚拟机,同时分配不同的内存空间,这就导致了不同虚拟机中访问同一个类的对象会产生多个副本,所以在一个进程中修改了一个对象的值,在另一个进程中相同对象的值不会变化,同理均线呈锁对象也不是同一个对象,无法保证同步。

问题3 是因为SharedPreference 是不支持两个进程同时去执行写操作,否则会导致一定几率的数据丢失。

问题4 是因为系统在创建新进程的同时分配独立虚拟机的过程就是在启动一个应用的过程,所以相当于系统吧这个应用重新启动了一遍,会创建新的Application。

二:IPC的基础概念

数据在进程间传递的时候是需要序列化的,在传递以后需要反序列化拿到数据,需要注意的是,在数据进行序列化和反序列化传输到另一个进程以后,拿到的数据和之前虽然内容都相同,但是不是同一个对象。

使用Serializable 或者 Parcelable 来实现数据的序列化和反序列化,Serializable 的序列化比较简单,直接 实现这个接口就可以了,Parcelable 的使用要复杂一点,需要写几个方法,需要注意的是在实现Serializable 接口的时候尽量声明一个 serialVersionUID,而只有当这个字段在序列化之前和序列化之后都不变的时候才能够成功的反序列化,所以我们应该手动指定。

举例:

public class User implements Serializable {
         private static final long serialVersionUID = 519067123721295773L;
         public int userId;
         public String userName;
         public boolean isMale;
        ...
}

//序列化到文件中
    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();

Serializable 相对Parcelable来说在序列化和反序列化的过程需要大量 I/O 操作,所以Parcelable更加高效,但是代码实现上麻烦,同时它主要用在内存序列化上,如果把对象序列化到存储设备中或者将对象序列化后通过网络传输建议使用Serializable

Android中的IPC方法基本上都是使用Binder来实现的,最基本的是AIDL来实现进程间通信,在使用AIDL的时候需要注意一下几点:

1:在客户端访问远程进程的服务端的时候,客户端的进程会被挂起,当访问的服务端的进程返回数据的时候客户端的进程才会再次运行,所以进程间通信的逻辑不要防到客户端的UI线程

2:服务端的Binder方法运行在Binder的线程池中,所以Binder方法不管是否耗时都应该采用同步的方式去实现,因为它已经运行在一个线程中了,所以当有很多客户端同时访问服务端的时候,服务端的Binder方法要处理好线程同步的问题,下面的例子是使用 CopyOnWriteArrayList 自动实现线程同步

public class BookManagerService extends Service {
         private static final String TAG = "BMS";
         private CopyOnWriteArrayList<Book> mBookList = new CopyOnWriteArray-
         List<Book>();
         private Binder mBinder = new IBookManager.Stub() {
             @Override
             public List<Book> getBookList() throws RemoteException {
                 return mBookList;
             }
             @Override
             public void addBook(Book book) throws RemoteException {
                 mBookList.add(book);
             }
         };
         @Override
         public void onCreate() {
             super.onCreate();
             mBookList.add(new Book(1,"Android"));
             mBookList.add(new Book(2,"Ios"));
         }
         @Override
         public IBinder onBind(Intent intent) {
             return mBinder;
         }
    }

3:为了防止服务端由于某种原因异常终止造成的远程调用失败,我们可以在客户端给Binder设置死亡监听,来监听服务端的Binder的状态,当Binder死亡的时候,系统就会回调binderDied方法

//其中的mBookManager 即客户端通过 ServiceConnection 获取到的服务端的Binder     
private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient(){
        @Override
        public void binderDied() {
                if (mBookManager == null)
                        return;
        
                mBookManager.asBinder().unlinkToDeath(mDeathRecipient,0);
                mBookManager = null;
                // TODO:这里重新绑定远程Service
        }
};

然后在客户端绑定远程服务后,被binder设置死亡代理

mBookManager = IMessageBoxManager.Stub.asInterface(binder);
binder.linkToDeath(mDeathRecipient,0);

三:Android实现IPC的方式

1:使用Bundle

在开启其他进程的四大组件的时候可以通过Bundle来传递数据,传递的数据必须能够序列化

2:使用文件共享

使用共享文件的方式比较好理解,把文件存储到一个可以共享的位置,多个进程共享,但是有两个注意事项

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

2)系统提供的SharePreference 不要使用在进程间通信中。
因为系统对SP的读写有一定的缓存策略,即在内存中会有一份SP文件的缓存,所以在多进程模式下,系统对它的读写会不可靠。
3:使用Messenger对象

Messenger对象其实就是对AIDL的封装,具体是用方法和AIDL的使用方式类似

4:使用AIDL
服务端:具体实现代码见上一节
1)创建Service
2)创建AIDL文件,文件中定义要暴露的接口
3)在Service实现这个AIDL中的接口,在Service中 onBinde 方法中返回 Binder
客户端:
1)绑定服务端的Service,如果是不同应用使用隐式意图获取Intent,同一个应用直接使用类名
2)在ServiceConnection 对象实现的方法中获取服务端的Binder,转成AIDL接口所属的类型
3)调用AIDL中的方法
5:使用ContentProvider

由于项目中使用过ContentProvider,所以对ContentProvider比较熟悉,这里记录需要注意的几个点:

1)可以给Provider组件添加单独的权限来控制访问者,访问者如果想访问也需要声明对应的权限
<provider
        android:name=".provider.BookProvider"
        android:authorities="com.ryg.chapter_2.book.provider"
        android:permission="com.ryg.PROVIDER" //权限
        android:process=":provider" >
</provider>

访问者需要在Manifest文件中声明权限

<uses-permission android:name="com.ryg.PROVIDER" />
2)ContentProvider中的query、update、insert、delete四个方法是存在多线程并发访问的.

实际使用中如果存在并发的场景,内部要做好线程同步,如果使用SQLite数据库,数据库内部会对操作进行同步处理,但是如果通过多个SQLiteDatabase对象来操作数据库就无法保证线程同步了,因为SQLiteDatabase对象之间无法进行线程同步

6:使用Socket

使用Socket实现进程通信主要是使用网络来完成进程通信

四:Binder连接池

开始的时候看这部分内容简直就是懵逼,即使自己敲了一遍仍然懵逼,发现在总结笔记的时候突然看懂了。
在这里插入图片描述
传统的方法中每个客户端都要对应自己一个单独的Service来完成进程通信,因为需要在Service中返回服务端的Binder对象。

如上图,Binder连接池的作用是为了防止当多个业务模块也就是多个客户端都需要访问服务端的时候,要创建多个Service的情况,客户端要通过Binder连接池来访问服务端,也就是说这个Binder连接池即充当服务端也充当了客户端,在客户端访问Binder池的时候此时它是服务端,当连接池根据表示去访问Service拿对应的Binder对象的时候,它是客户端。通过这样的思想来分析 BinderPool 中的代码就会豁然开朗。

传统的方法是,服务端创建AIDL接口,然后创建Service,实现AIDL接口,然后在Service中返回;客户端绑定服务端Service,然后通过ServiceConnection 获取Binder对象,调用对应的AIDL接口方法。

根据上面的图片,咱们倒着看
1:每个客户端要实现的AIDL接口,都实现成对应的Binder类,放到服务端

2:现在Binder线程池访问服务端,服务端需要实现Binder线程池对应的AIDL接口,然后返回给Binder线程池的Binder对象,我们成为Binder0,Binder0中只有一个AIDL方法,就是根据标识返回步骤1中对应的Binder对象

3:Binder线程池作为客户端要绑定服务端Service,通过ServiceConnection获取服务端返回的Binder0

4:多个客户端访问Binder线程池,Binder作为服务端会根据客户端的标识去调用Binder0中的查询方法,返回步骤1中对应的Binder对象

5:客户端通过对象调用服务端的方法。

下面是代码实现,来自刚哥的示例代码:

第一步:实现多个aidl文件接口
//aidl文件
interface ISecurityCenter {
         String encrypt(String content);
         String decrypt(String password);
}

//另一个aidl文件
interface ICompute {
         int add(int a,int b);
}

实现每个aild文件接口,实现以后当然都是Binder类

//第一个aidl文件接口实现类
public class SecurityCenterImpl extends ISecurityCenter.Stub {
         private static final char SECRET_CODE = '^';
         @Override
         public String encrypt(String content) throws RemoteException {
             char[] chars = content.toCharArray();
             for (int i = 0; i < chars.length; i++) {
                 chars[i] ^= SECRET_CODE;
             }
             return new String(chars);
         }
         @Override
         public String decrypt(String password) throws RemoteException {
             return encrypt(password);
         }
}
//实现第二个aidl文件接口类
public class ComputeImpl extends ICompute.Stub {
         @Override
         public int add(int a,int b) throws RemoteException {
             return a + b;
         }
}
第二步:服务端的Service实现

实现Binder线程池要调用的AIDL接口

interface IBinderPool {
         /**
          * @param binderCode,the unique token of specific Binder<br/>
          * @return specific Binder who's token is binderCode.
          */
         IBinder queryBinder(int binderCode);
 }

BinderPoolImpl 类中肯定要实现对应的AIDL接口方法,这个方法也是Binder线程池作为客户端要调用的远程方法,代码如下:

//这个类是Binder线程池(BinderPool.java)中的内部类,单独拿到这里

public static final int BINDER_COMPUTE = 0;
public static final int BINDER_SECURITY_CENTER = 1;

public static class BinderPoolImpl extends IBinderPool.Stub {
             public BinderPoolImpl() {
                 super();
             }
             @Override
             public IBinder queryBinder(int binderCode) throws RemoteException {
                 IBinder binder = null;
                 switch (binderCode) {
                     case BINDER_SECURITY_CENTER: {
                         binder = new SecurityCenterImpl();
                         break;
                     }
                     case BINDER_COMPUTE: {
                         binder = new ComputeImpl();
                         break;
                     }
                     default:
                         break;
                  }
                  return binder;
             }
}

创建Service

public class BinderPoolService extends Service {
         private static final String TAG = "BinderPoolService";
         private Binder mBinderPool = new BinderPool.BinderPoolImpl();
         @Override
         public void onCreate() {
             super.onCreate();
         }
         @Override
         public IBinder onBind(Intent intent) {
             Log.d(TAG,"onBind");
             return mBinderPool; //返回的是Binder线程池的Binder对象,它是Binder池中的一个内部类,当然可以把它单独抽取出来
         }
         @Override
    public void onDestroy() {
             super.onDestroy();
         }
}

到此第二步完成,服务端的已经返回给Binder线程池的Binder0对象

第三步:Binder线程池作为客户端获取Binder0
//这个对象也是Binder线程池(BinderPool.java)中的变量
private ServiceConnection mBinderPoolConnection = new ServiceConnection() {
             @Override
             public void onServiceDisconnected(ComponentName name) {
                 // ignored.
             }
             @Override
             public void onServiceConnected(ComponentName name,IBinder service) {
                 mBinderPool = IBinderPool.Stub.asInterface(service);
                 try {
                     //设置Binder死亡代理
                     mBinderPool.asBinder().linkToDeath(mBinderPoolDeathRecipient,0);
                 } catch (RemoteException e) {
                     e.printStackTrace();
                 }
                 //使用CountDownLatch对象来同步线程的唤醒代码
                 mConnectBinderPoolCountDownLatch.countDown();
             }
};
//绑定Service的方法
private synchronized void connectBinderPoolService() {
            //使用CountDownLatch来实现进程同步,
             mConnectBinderPoolCountDownLatch = new CountDownLatch(1);
             Intent service = new Intent(mContext,BinderPoolService.class);
            //绑定服务
             mContext.bindService(service,mBinderPoolConnection,Context.BIND_AUTO_CREATE);
             try {
                     //给集成加锁
                     mConnectBinderPoolCountDownLatch.await();
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }

使用Binder0的对象调用对应的AIDL接口,也就是上面第二步中实现的方法

public IBinder queryBinder(int binderCode) {
             IBinder binder = null;
             try {
                 if (mBinderPool != null) {
                     binder = mBinderPool.queryBinder(binderCode);
                 }
             } catch (RemoteException e) {
                 e.printStackTrace();
             }
             return binder;
 }

到此,Binder线程池和服务端的交互就完成了,接下来就是多个客户端访问线程池的流程

第四步:多个客户端访问Binder线程池,Binder作为服务端会根据客户端的标识去调用Binder0中的查询方法,返回步骤1中对应的Binder对象

这一步Binder线程池作为服务端和多个客户端交互,Binder线程池首先要返回所有的Binder,其实在上面的流程中已经完成了,当Binder线程池拿到从最后服务端获取的Binder调用 queryBinder 方法的时候,返回的Binder就是根据对应标识找到的IBinder对象,只是需要在转换一下,使用 asInterface 方法转换成Binder对象

第五步:客户端通过对象调用服务端的方法。

客户端只需要直接创建Binder线程池的对象,调用 queryBinder 方法,输入对应的标识即可获取到对应的Binder对象,然后就能调用远程方法,至于什么标识对应哪个Binder,当然是和服务端协商好的,调用代码:

private void doWork() {
         BinderPool binderPool = BinderPool.getInsance(BinderPoolActivity.this);
    
         IBinder securityBinder = binderPool.queryBinder(BinderPool.BINDER_SECURITY_CENTER);
       
         mSecurityCenter = (ISecurityCenter) SecurityCenterImpl.asInterface(securityBinder);
         Log.d(TAG,"visit ISecurityCenter");
         String msg = "helloworld-安卓";
         try {
             String password = mSecurityCenter.encrypt(msg);
             System.out.println("encrypt:" + password);
             System.out.println("decrypt:" + mSecurityCenter.decrypt(password));
         } catch (RemoteException e) {
             e.printStackTrace();
         }
         Log.d(TAG,"visit ICompute");
         IBinder computeBinder = binderPool
                 .queryBinder(BinderPool.BINDER_COMPUTE);
         mCompute = ComputeImpl.asInterface(computeBinder);
         try {
             System.out.println("3+5=" + mCompute.add(3,5));
         } catch (RemoteException e) {
             e.printStackTrace();
         }
 }

具体的Binder线程池的全部代码就不贴了,可以直接看刚哥的书了。

五:几种IPC方式的对比

在这里插入图片描述

为什么 Android 要采用 Binder 作为 IPC 机制?

猜你喜欢

转载自blog.csdn.net/static_zh/article/details/85097457