IPC机制与面试题精选《Android开发艺术探索》笔记

参考链接

本文图片与总结习题均来自厘米姑娘

IPC简介

IPC的概念

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

进程与线程的区别

  • 线程是CPU调度的最小单元,同时线程是一种有限的资源。
  • 进程一般指一个执行单元,一个进程可以包含多个线程,所以他们之间是包含与被包含的关系。(比如Android中有主线程,而如果要执行耗时操作(网络请求),就需要开一个新的线程去完成)

多进程的情况

  • 第一种情况是一个应用因为某些原因自身需要采用多线程模式来实现。
  • 另一种情况是当前应用需要向其他应用获取数据

开启多进程模式的方法

多进程模式是指一个应用中存在多个进程

在AndroidMainfest中为四大组件指定属性:android:process。

没有指定该属性

没有指定该属性则运行在默认进程,其进程名就是包名。

以:命名

省略包名,如android:process=":remote",其全称com.example.myapplication:remote

这是一种属于当前应用的私有进程,其他进程的组件不能和它跑在同一进程。

完整命名的进程

android:process = "com.example.myapplication.remote"

这是一种全局进程,其他应用可以通过ShareUID方式和他跑在用一个进程中。

UID&ShareUID:

Android系统为每个应用分配一个唯一的UID,具有相同UID的应用才能共享数据。
两个应用通过ShareUID跑在同一进程的条件:ShareUID相同且签名也相同。

满足上述条件的两个应用,无论是否跑在同一进程,它们可共享data目录,组件信息。
若跑在同一进程,它们除了可共享data目录、组件信息,还可共享内存数据。它们就像是一个应用的两个部分。

查看进程的方法

通过DDMS视图查看进程信息。
通过shell查看,命令为:adb shell ps|grep 包名。

使用进程间通信的重要性(重点)

所有运行在不同进程的四大组件,想要通过内存来共享数据,就会共享失败。

原因由于Android系统为每个进程(应用)都分配了一个独立的虚拟机,不同的虚拟机在内存上就有不同的地址空间,当对一个公共类进行修改时,就会造成在不同的虚拟机中访问相同的类产生多份备份。

举例说明:两个进程中都有一个类,实际上这两个类是互相不影响的,当你在进程1中更改了类中的属性,只会影响进程1,而不会影响进程2。

多进程造成的影响有四点(重要)

  • 1.静态成员和单例模式失效
    原因:因为同一个类在多个进程中会多次加载。
  • 2.线程同步机制失效
    原因:不是一块内存的话,无论锁对象还是锁全局类,都无法保证线程同步,因为不同进程锁的不是同一个对象。
  • 3.Sharedpreferences的可靠性下降
    原因:SharedPreferences不支持两个进程同时进行读写操作,即不支持并发读写,有一定机率导致数据丢失。(SharedPreferences底层是通过读写XML文件实现的,不支持多个进程的并发操作)
  • 4.Application多次创建
    原因:Android系统会为新的进程分配独立虚拟机,相当于系统又把这个应用重新启动了一次,就相当于创建了新的Application。

序列化

序列化的介绍

含义:序列化表示将一个对象转换成可存储或可传输的状态。序列化后的对象可以在网络上进行传输,也可以存储到本地。

场景:需要通过Intent和Binder传输对象,就必须完成对象的序列化过程

两种方式:实现Serializable/Parcelable接口。

Serializable与Parcelable的比较

在这里插入图片描述

Serialzable的例子

Serializable使用简单,我们举一个将Person对象从FirstActivity传递到SecondActivity的例子。

首先让我们要用的类实现该接口,并给予一个serialVersionUID。

public class Person implements Serializable{
    private static final long serialVersionUID = 7382351359868556980L;
    ...
}

在FirstActivity中使用Intent的putExtra传递即可。

 				Intent open = new Intent(MainActivity.this,SecondActivity.class);
                Person person = new Person();
                person.setName("一去二三里");
                person.setAge(18);
                // 传输方式一,intent直接调用putExtra
                // public Intent putExtra(String name, Serializable value)
                open.putExtra("put_ser_test", person);
                // 传输方式二,intent利用putExtras(注意s)传入bundle
                /**
                Bundle bundle = new Bundle();
                bundle.putSerializable("bundle_ser",person);
                open.putExtras(bundle);
                 */
                startActivity(open);

在跳转的Activity中可以通过如下方式获取并转化为Person对象(反序列化过程)。

Intent intent = getIntent();
        // 关键方法:getSerializableExtra ,我们的类是实现了Serializable接口的,所以写这个方法获得对象
        // public class Person implements Serializable
        Person per = (Person)intent.getSerializableExtra("put_ser_test");
        //Person per = (Person)intent.getSerializableExtra("bundle_ser");
        mTvDate.setText("名字:"+per.getName()+"\\n"
                +"年龄:"+per.getAge());

serialVersionUID的作用

理论上来说serialVersionUID可以不赋予,一样可以完成序列化与反序列化。但由于原则上序列化后的数据中的serialVersionUID要和当前类的serialVersionUID 相同才能正常的反序列化,还是要在类中赋予
serialVersionUID,防止当前类的serialVersionUID因类中成员变量更改而改变。

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

因此,如果我们对当前类进行修改,比如增加或者删除了某些成员变量,那么系统就会重新计算当前类的hash值并把它赋值给serialversionUID,这个时候当前类的serialversionUID就和序列化数据中的serialversionUID不一致,于是反序列化失败。

Binder机制

概念

从API角度:是一个类,实现了IBinder接口。
从IPC角度:是Android用于实现多进程通信的方式
从Framework角度:是ServiceManager连接各种Manager和相应ManagerService的桥梁。
从应用层:是客户端和服务端进行通信的媒介。客户端通过它可获取服务端提供的服务或者数据。

优点

Linux常用的IPC方式包括管道、Socket、共享内存、消息队列

(1)传输速率快,拷贝次数少
相比管道、Socket与消息队列的2次拷贝,Binder机制只有一次拷贝。这是因为其他方式的拷贝是首先将数据从传输方的缓存区拷贝到内核的缓存区中,之后再从内核的缓存区中将数据拷贝至接收方的缓存区
在这里插入图片描述
而Binder机制中,传输方将数据从缓存区拷贝到内核的缓存区后,不需要再次拷贝,因为内核中的缓存区与接收方的缓存区映射到同一块物理地址。
在这里插入图片描述
(2)实现C/S架构方便
Linux常用IPC方式只有Socket是基于C/S架构,但通常用于网络中,且传输速率慢。而binder机制具有C/S架构,且Server与Cilent相对独立,稳定性好。

(3)较为安全
传统linux的IPC方式接收都无法获得发送方的UID/PID,从而无法获得对方的消息。而Binder机制为每个进程配备了UID/PID,在通信时会根据UID/PID进行检测。

Binder框架

其中包含Client、Server、ServiceManager、Binder驱动设备。其中Client、Server、ServiceManager处于用户空间,而Binder驱动设备处于内核空间。
在这里插入图片描述
Service、Client:服务端与客户端,在ServiceManager与驱动设备的支持下,完成进程间通信。

Service Manager:服务的管理者,将Service的Binder转换为Client对Binder的引用,使得Client可以通过Binder名字获得Server中Binder实体的引用,进而完成Server与Client的通信。
在这里插入图片描述
Binder驱动设备

  • 与硬件设备没有关系,其工作方式与设备驱动程序是一样的,工作于内核态。
  • 提供open()、mmap()、poll()、ioctl()等标准文件操作。
  • 以字符驱动设备中的misc设备注册在设备目录/dev下,用户通过/dev/binder访问该它。
  • 负责进程之间binder通信的建立,传递,计数管理以及数据的传递交互等底层支持
  • 驱动和应用程序之间定义了一套接口协议,主要功能由ioctl()接口实现,由于ioctl()灵活、方便且能够一次调用实现先写后读以满足同步交互,因此不必分别调用write()和read()接口。
  • 其代码位于linux目录的drivers/misc/binder.c中。

Android中的IPC机制

1.Bundle机制

Bundle实现了Parcelable接口,可以在不同进程间传递信息,这是最常用的方法。

调用Bundle的putString放入key与value的对应,并最终使用intent.putExtra方法传递Bundle对象。在想获取Bundle对象的位置,可以通过getIntent()获取intent对象,并通过getExtra()方法获取Bundle,进而获取Bundle其中存入的数据。

注意传入的value需要是实现了Serialzable或者Paracelable。

        Intent intent = new Intent(MainActivity.this,SecondActivity.class);
        Bundle data = new Bundle();
        data.putString("name","zhangsan");//将数据放入bundle
        intent.putExtra(data);
        startActivity(intent);
        //在SecondActivity中,将传递的数据取出
        Bundle data = getIntent().getExtra();//从bundle中取出数据
        String name = data.getString("name");

我们也可以省略Bundle,直接putExtra传递我们的的数据的key与value。获取也是通过getIntent获取Intent对象,之后直接通过intent.getStringExtra(key)来获取我们想传递的value。

        //在MainActivity中存入数据:
        intent.putExtra(NAME_KEY,"zhangsan");
        intent.putExtra(AGE_KEY,25);
        intent.putExtra(IS_FEMALE_KEY,false);
        //在SecondActivity中取出数据:
        Intent intent = getIntent();
        String name = intent.getStringExtra(MainActivity.NAME_KEY);
        int age = intent.getIntExtra(MainActivity.AGE_KEY,30);
        boolean isfemale = intent.getBooleanExtra(MainActivity.IS_FEMALE_KEY,true);

2.使用文件共享

两个进程通过读/写同一个文件来交换数据。比如A把数据写入一个文件,B通过读写文件来获取数据。

适用于对数据同步要求不太高的进程,同时要妥善处理并发读和并发写的问题。

3.使用AIDL

AIDL内部是实现的Binder机制。如果在一个进程中,需要调用另一个进程中对象的方法,可以使用AIDL生成可序列化的参数,AIDL会生成一个服务器端对象的代理类,通过它来实现客户端间接调用服务端对象的方法。

AIDL所支持的数据类型
基本类型:int,boolean,float,double,byte,long,char;
CharSequence类型;
String类型;
List、Map中的数据类型都是AIDL所支持的类型;
实现Parcelable接口的对象;

注意其参数除了有数据类型外,还必须标有tag,tag包括:in 数据只允许从客户端发往服务端;out数据只允许从服务端发送客户端;inout:两种方式都可以。

in Student student

AIDL的本质

这里先放一个AIDL的实例:创建一个AIDL文件,IBookManager接口中包含一个getBookList方法,一个addBook方法

import com.ryg.chapter_2.aidl.Book;
interface IBookManager {
     List<Book> getBookList();
     void addBook(in Book book);
     }

本质是实现了Binder机制,其中包括以下几个关键类与关键方法:

(1)Stub类Binder的实现类,服务端由它来提供方法。

(2)Proxy类:**服务端的本地代理,客户端通过这个类来调用服务端的方法。**在本例子中该类包括两个方法:Proxy#getBookList与Proxy#addList,他们都运行在客户端。

(3)asInterface方法:**客户端调用,将服务器返回的Binder对象转换为AIDL接口的对象,以供客户端使用。**返回对象:如果服务端和客户端时同一个进程,则直接返回Stu对象;否则返回的是封装后的Stu.proxy对象。其中的DESCRIPTOR是AIDL的唯一标识。
在这里插入图片描述(4)asBinder方法:根据当前调用情况返回代理Proxy的Binder对象

(5) onTransact方法:该方法运行在服务端的Binder线程池中,当客户端与服务端发起跨进程请求时会调用。public Boolean onTransact (int code,android.os.Pareel data,android.os.Pareel reply,int fiags)首先通过code来判断客户端请求的是哪个方法并通过data来获取该方法所需要传入的参数。然后执行目标方法,执行完毕后,会将结果(如果有的话)写入reply中。如果该方法返回false,则说明客户端请求失败。

@Override
    protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
        switch (code){
            case INTERFACE_TRANSACTION:
                reply.writeString(DESCRIPTORS);
                return true;
                //getBookList方法(没有data有reply)
            case TRANSACTION_getBookList:
                data.enforceInterface(DESCRIPTORS);
                List<com.liuguilin.multiprocesssample.Book> result = this.getBookList();
                reply.writeNoException();
                reply.writeTypedList(result);
                return true;
                //addBook方法(有data没有reply)
            case TRANSACTION_addBook:
                data.enforceInterface(DESCRIPTORS);
                Book book;
                if(0!=data.readInt()){
                    book =  Book.CREATOR.createFromParcel(data);
                }else {
                    book = null;
                }
                this.addBook(book);
                reply.writeNoException();
                return true;
        }
        return super.onTransact(code, data, reply, flags);
    }

(6)transact()方法:当客户端发起跨进程请求时,会首先调用transact方法,并将当前线程挂起。之后调用服务端的onTransact方法,直到返回结果,线程才继续执行。

调用方法的流程以Proxy#getBookList为例:当客户端运行该方法后,会创建所需要的输入型对象data,输出值对象reply,以及返回值对象List,然后把该方法的参数信息写入data中,接着调用transact方法来发起与服务端通信的请求,之后线程挂起,服务端会回调onTransact方法,当方法执行结束后,线程进行执行,并从reply中取出结果。

AIDL的使用

服务端

(1)创建一个AIDL文件

import com.ryg.chapter_2.aidl.Book;
interface IBookManager {
     List<Book> getBookList();
     void addBook(in Book book);
     }

(2)在Service中实现AIDL接口中的方法,并在onBind方法中返回Binder对象(AIDL接口的Stub对象该对象是个Binder,也是实现它内部的方法)。

服务端代码:

public class BookManagerService extends Service {

    private static final String TAG = "BookManagerService";

    private CopyOnWriteArrayList<Book> mBookList = new CopyOnWriteArrayList<>();

    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(1,"IOS"));
    }

    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }
}

客户端

(1)创建ServiceConnection,并在onServiceConnected方法中,将从服务端获取的Binder对象转化为我们的AIDL接口asInterface(binder),进而调用接口中的方法

(2)调用bindService将服务端与客户端绑定起来

可以看到我们使用Service的方法很类似。整体的思路为:首先创建一个ServiceConnection,并重写onServiceConnected方法,在该方法中要先将binder对象(service)转化为AIDL接口(IBookManager),然后就可以调用该接口中的方法了

之后在创建一个Intent并传入当前Activity与Service类,使用bindService方法传入intent,ServiceConnection,标识符 以建立服务端与客户端的绑定。最后在onDestroy()方法中调用unbindService方法,断开绑定。


public class BookManagerActivity extends AppCompatActivity {
    private static final String TAG = "BookManagerActivity";
    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            IBookManager bookManager = IBookManager.Stub.asInterface(service);
            try {
                List<Book> list = bookManager.getBookList();
                Log.i(TAG, "list Type :" + list.getClass());
                getCanonicalName();
                Log.i(TAG, "list string : " + list.toString());
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
        @Override
        public void onServiceDisconnected(ComponentName name) {
        }
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
       ...
        Intent intent = new Intent(this, BookManagerService.class);
        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);

    }

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


IPC的整体流程(借助AIDL来谈)

(1)服务端将Binder对象发送至与其绑定的客户端,客户端通过使用AIDL的asInterface将Binder对象转化为AIDL接口,进而去调用接口中的方法。asInterface返回值根据服务端和客户端是否在同一个进程,如果在的话就直接返回Stu对象,否则返回Stu.porxy。(Stu的代理)

(2)当调用接口方法后,会调用transact方法,并传入data(包括输入参数,输出参数,返回值)并将线程挂起;

(3)之后会调用服务端的onTransact方法,当方法执行完成后,会将返回值reply返回客户端,并唤醒线程
在这里插入图片描述
一个调用方法实例。
在这里插入图片描述

Binder连接池

当多个模块都需要使用AIDL进行远程通信,如果我们都为其创建Service,就会导致消耗系统资源过多。

解决方法是使用Binder连接池。具体逻辑如下每个模块都创建自己的AIDL文件并实现其中的接口。服务端创建一个AIDL文件,接口中包含一个binderQuery方法。创建一个BinderPool类,在该类中创建一个Binder内部类继承自接口的Stub(实质是一个Binder类),内部类中重写binderQuery方法,根据标识符创建对应的Binder对象并返回。在服务端onBind中返回这个Binder对象。当客户端需要使用Binder时,会调用连接池(BindPool)的bindQuery方法(其实是调用的(内部类)Binder对象的binderQuery方法)并传入标识符,就会返回Binder对象。最后对其使用asinstance方法转化为对应接口。
在这里插入图片描述
具体代码:

服务端的AIDL文件,要实现它的queryBinder方法,

interface IBinderPool {
    IBinder queryBinder(int binderCode);
}

下面还剩下Binder连接池(BinderPool类)的具体实现了,在他的内部具体做以下两个事情:

(1)要实现IBinderPool接口
新建一个BinderPoolImpl类并继承自IBinderPool.Stub,在queryBinder方法中根据传入的标识符来判断返回模块需要的Binder对象

(2)绑定远程服务
在绑定成功后,客户端就可以通过Binder连接池的queryBinder方法来获取对应的Binder,拿到所需的Binder之后,不同业务模块就可以操作了。**

public class BinderPool {
    private IBinderPool mIBinderPool;
	public IBinder queryBinder(int binderCode) {
        	IBinder binder = null;
        		try {
            		if (mIBinderPool != null) {
                	binder = mIBinderPool.queryBinder(binderCode);
           	 		}
        	} catch (RemoteException e) {
            	e.printStackTrace();
        	}
	        return binder;
    }
	
	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_SECURUITY_CENTER:
                    binder = new SecurityCenterImpl();
                    break;
                case BINDER_COMPUTE:
                    binder = new ComputeImpl();
                    break;
            }
            return binder;
        }
    }
}

在Service中onBind方法返回BinderPool类的BinderPoolImpl对象。

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) {
        return mBinderPool;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
    }
}

最后在客户端,需要获取Binder时,需要传入标识符。首先获取之前BinderPool类,之后调用它的queryBinder方法并传入标识符获取当前模块所需的Binder,最后通过asInterface将其转换为接口,以使用接口的方法。

BinderPool binderPool = BinderPool.getInstance(BinderActivity.this);
IBinder securityBinder = binderPool.queryBinder(BinderPool.BINDER_SECURUITY_CENTER);
mSecurityCenter = SecurityCenterImpl.asInterface(securityBinder);

总结(复习必看)

1.Android中进程和线程的关系?区别?

线程是CPU的最小调度,比如Android的UI线程不能执行耗时操作,我们就需要再开一个线程执行耗时操作。

而进程是一个执行单元,可以包含多个线程。四大组件可以通过在AndroidManifest中通过更改android:process来开启进程。

2.为何需要进行IPC?多进程通信可能会出现什么问题?

3.什么是序列化?Serializable接口和Parcelable接口的区别?为何推荐使用后者?

4.Android中为何新增Binder来作为主要的IPC方式?

5.使用Binder进行数据传输的具体过程?

6.Binder框架中ServiceManager的作用?

7.Android中有哪些基于Binder的IPC方式?简单对比下?

8.是否了解AIDL?原理是什么?如何优化多模块都使用AIDL的情况?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章