美文网首页androidAndroid面试理论
Android开发二《IPC机制》

Android开发二《IPC机制》

作者: 独自闯天涯的码农 | 来源:发表于2022-03-22 12:28 被阅读0次

    一、Android的IPC简介

    IPC:Inter-process Communication的缩写,含义为进程间通信,指两个进程之间进行数据交换的过程.
    进程:指一个执行单元,在PC端或移动设备上指一个程序或者一个应用.一个进程可以包含多个线程;只包含一个为主线程(UI线程)
    线程:是CPU调度的最小单元,同时线程是一种有限的系统资源.
    ANR:Application Not Responding,即应用无响应,主线程中耗时操作导致;

    多进程使用场景:
    1、应用自身需要多进程模式实现某些功能;
    2、当前应用需要向其他应用获取数据;

    二、Android中的多进程模式

    1、开启多进程
    通过给四大组件指定:android:process属性,开启多进程模式
    1、android:process=":remote"    
    进程名以:开头为当前应用私有进程,
    2、android:process="com.alan.base.remote"
    进程名为完整的为全局进程,其他应用可通过ShareUID方式与它跑同一个进程(要求:两个应用有相同的ShareUID并且签名相同)
    
    Android系统为每个应用都会分配一个唯一的UID,具有相同的UID的应用才能共享数据,可以互相访问私有数据
    
    2、多进程模式运行机制

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

    多进程问题:
    1、静态成员和单例模式完全失效(备份)
    2、线程同步机制完全失效(不同进程不是同一个对象)
    3、SharedPreferences的可靠性下降(底层读写xml文件,并发写会出现问题)
    4、Application会多次创建(运行在同一个进程的组件属于同一个虚拟机和同一个Application)
    

    三、IPC基础概念介绍

    1、序列化

    序列化:将对象转换为可以传输的二进制流(二进制序列)的过程,从而进行传输数据;
    反序列化:就是从二进制流(序列)转化为对象的过程.

    1. Serializable接口

    Serializable是JAVA提供的一个序列化接口,它是一个空接口,为对象提供标准的序列化和反序列化操作,使用只需声明下面标识即可自动实现序列化过程:

    private static final long serialVersionUID = 112L;
    

    不声明serialVersionUID也可以,不影响序列化,但会影响反序列化;
    静态成员变量属于类不属于对象,不会参与序列化,用transient关键字修饰的成员变量也不会参与序列化;

    序列化过程
    User user = new User();
    ObjectOutputStream  out = new ObjectOutputStream(new FileOutputStream("cache.txt"));
    out.writeObject(user);
    out.close();
    
    反序列化过程
    ObjectInputStream  in = new ObjectInputStream(new FileInputStream("cache.txt"));
    User newUser = in.readObject();
    in.close();
    
    2. Parcelable接口

    Parcelable是Android提供的一个新序列化接口;
    Parcel内部包装了可序列化的数据,可以在Binder中自由传输;

    Parcel提供了一套机制,可以将序列化之后的数据写入到一个共享内存中,其他进程通过Parcel可以从这块共享内存中读出字节流,并反序列化成对象,下图是这个过程的模型。


    Parcelable
    public class DataBean implements Parcelable {
        private int mData;
    
        /**
         * 负责反序列化
         */
         public static final Parcelable.Creator<DataBean> CREATOR
                = new Parcelable.Creator<DataBean>() {
           /**
             * 从序列化对象中,获取原始的对象
             * @param source
             * @return
             */
            public DataBean createFromParcel(Parcel in) {
                return new DataBean(in);
            }
            
           /**
             * 创建指定长度的原始对象数组
             * @param size
             * @return
             */
            public DataBean[] newArray(int size) {
                return new DataBean[size];
            }
        };
    
    
        private DataBean(Parcel in) {
            mData = in.readInt();
        }
    
     /**
         * 描述
         * 返回的是内容的描述信息
         * 只针对一些特殊的需要描述信息的对象,需要返回1,其他情况返回0就可以
         *
         * @return
         */
        public int describeContents() {
            return 0;
        }
    
     /**
         * 序列化
         *
         * @param dest
         * @param flags
         */
        public void writeToParcel(Parcel out, int flags) {
            out.writeInt(mData);
        }
    }
    

    Parcelable实现过程主要分为序列化,反序列化,描述三个过程,下面分别介绍下这三个过程

    实现Parcelable的作用
    1)永久性保存对象,保存对象的字节序列到本地文件中;
    2)通过序列化对象在网络中传递对象;
    3)通过序列化在进程间传递对象。

    Parcelable和Serializable的区别和比较
    Parcelable和Serializable都是实现序列化并且都可以用于Intent间传递数据;
    Serializable是Java的实现方式,可能会频繁的IO操作,所以消耗比较大,但是实现方式简单; Parcelable是Android提供的方式,效率比较高,但是实现起来复杂一些;
    二者的选取规则是:内存序列化上选择Parcelable, 存储到设备或者网络传输上选择Serializable(当然Parcelable也可以但是稍显复杂)

    选择序列化方法的原则
    1)在使用内存的时候,Parcelable比Serializable性能高,所以推荐使用Parcelable。
    2)Serializable在序列化的时候会产生大量的临时变量,从而引起频繁的GC。
    3)Parcelable不能使用在要将数据存储在磁盘上的情况,因为Parcelable不能很好的保证数据的持续性在外界有变化的情况下。尽管Serializable效率低点,但此时还是建议使用Serializable 。

    3. 区别
    1、作用

    Serializable的作用是为了保存对象的属性到本地文件、数据库、网络流、rmi以方便数据传输,当然这种传输可以是程序内的也可以是两个程序间的。
    而Android的Parcelable的设计初衷是因为Serializable效率过慢,为了在程序内不同组件间以及不同Android程序间(AIDL)高效的传输数据而设计,这些数据仅在内存中存在,Parcelable是通过IBinder通信的消息的载体。

    2、效率及选择

    Parcelable的性能比Serializable好,在内存开销方面较小,所以在内存间数据传输时推荐使用Parcelable,如activity间传输数据;
    而Serializable可将数据持久化方便保存,所以在需要保存或网络传输数据时选择Serializable,因为android不同版本Parcelable可能不同,所以不推荐使用Parcelable进行数据持久化。

    3、编程实现

    对于Serializable,类只需要实现Serializable接口,并提供一个序列化版本id(serialVersionUID)即可。
    Parcelable则需要实现writeToParceldescribeContents函数以及静态的CREATOR变量,实际上就是将如何打包和解包的工作自己来定义,而序列化的这些操作完全由底层实现。

    2、Binder介绍

    1. 直观来说:Binder是Android中的一个类,实现了IBinder接口.
    2. 从IPC角度:Binder是Android中的一种跨进程通信方式;
    3. 硬件方面:Binder可以理解为一种虚拟的物理设备,他的设备驱动是/dev/binder,该通讯方式在Linux中没有;
    4. Android Framework角度:Binder是ServiceManager链接各种Manager和相应ManagerService的桥梁;
    5. Android应用层:Binder是客户端和服务端通讯的媒介,当bindService的时候,服务端会返回一个包含了服务端业务调用的Binder对象,通过这个Binder对象,客户端就可以获取服务端提供的服务或者数据,这里的服务包括普通服务和基于AIDL的服务.

    Android开发中,Binder主要用在Service中,包括AIDL和Messenger;其中Messenger的底层实现是AIDL.

    1. Binder概述

    Android系统中,涉及到多进程间的通信底层都是依赖于Binder IPC机制。

    2. Android使用Binder作为IPC方式原因
    • 性能方面:
      在移动设备上(性能受限制,比如耗电),广泛使用IPC对通讯机制性能有严格要求,Binder相对于传统IPC方式方式更加高效;Binder数据拷贝只需要一次,而管道、消息队列、Socket都需要两次,共享内存一次数据拷贝都不需要,但实现方式复杂。
    • 安全方面:
      传统的IPC方式对于通信双方的身份并没有做出严格的验证。Binder机制从协议本身就支持对通信双方做身份验证,从而大大提高安全性。
    3. IPC原理

    从进程角度看IPC机制


    IPC机制

    每个Android的进程,只能运行在自己进程所拥有的虛拟地址空间。例如,对应一个4GB的虛拟地址空间,其中3GB是用户空间,1GB是内核空间。当然内核空间的大小是可以通过参数配置调整的。对于用户空间,不同进程之间是不能共享的,而内核空间却是可共享的。Client进程向Server进程通信,恰怡是利用进程间可共享的内核内待空间来完成底层通信工作的。Client端与Server端进程往往采用ioctl等方法与内核空间的驱动进行交互。

    4. Binder原理

    Binder通信采用C/S架构,从组件视角来说,包含Client 、Server 、ServiceManager以及Binder驱动,其中ServiceManager用 于管理系统中的各种服务。


    Binder架构图
    Binder通信的四个角色
    • Client进程:使用服务的进程。
    • Server进程:提供服务的进程。
    • ServiceManager进程:ServiceManager的作用是将字符形式的Binder名字转化成Client中对该Binder的引用,使得Client能够通过Binder名字获得对Server中Binder实体的引用。
    • Binder驱动:驱动负责进程之间Binder通信的建立,Binder在进程之问的传递,Binder引用计数管理,数据包在进程之间的传递和交互等一系列底层支持。
    Binder运行机制

    Client/Server/ServiceManage之间的相互通信都是基于Binder机制。既然基于
    Binder机制通信,那么同样也是C/S架构,则图中的3大步骤都有相应的Client端与Server端。

    • 注册服务(addService):
      Server进程要先注册Service 到Serviceanager。该过程:Server是客户端,ServiceManager是服务端。(Android开机启动过程中,会初始化系统的各个Service,并将Service向ServiceManager注册,这一步系统自动完成)

    • 获取服务(getService):
      Client进程使用某个Service前,须先向ServiceManager中获取相应的Service。该过程:Client是客户端,ServiceManager是服务端。(例如:WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);)

    • 使用服务:
      Client根据得到的Service信息建立与Service所在的Server进程通信的通路,然后就可以直接与Service交互。该过程:Client是客户端,Server是服务端。

    图中的Client,Server, Service Manager之间交 互都是虚线表示,是由于它们彼此之问不是直接交互的,而是都通过与Binder驱动进行交互的,从而实现IPC通信(Interprocess Communication )方式。其中Binder驱动位于内核空间,Client,Server , Service Manager位于用户空间。Binder驱动和Service Manager可以看做是Android平台的基础架构,而Client和Server是 Android的应用层,开发人员只需自定义实现Client、Server端,借助Android的基本平台架构便可以直接进行IPC通信。

    Binder通信的实质

    Binder通信的实质是利用内存映射,将用户进程的内存地址和内核的内存地址映射为同一块物理地址,也就是说他们使用的同一块物理空间,每次创建Binder的时候大概分配128的空间。数据进行传输的时候,从这个内存空间分配一点,用完了再释放即可。

    5. Binder传输大文件

    Android中的匿名共享内存(Ashmem)是基于Linux共享内存的,借助Binder+文件描述符(FileDescriptor)实现了共享内存的传递。它可以让多个进程操作同一块内存区域,并且除了物理内存限制,没有其他大小限制。相对于Linux的共享内存,Ashmem对内存的管理更加精细化,并且添加了互斥锁。Java层在使用时需要用到MemoryFile,它封装了native代码。Android平台上共享内存通常的做法如下:

    1. 进程A通过MemoryFile创建共享内存,得到fd(FileDescriptor);
    2. 进程A通过fd将数据写入共享内存;
    3. 进程A将fd封装成实现Parcelable接口的ParcelFileDescriptor对象,通过Binder将ParcelFileDescriptor对象发送给进程B;
    4. 进程B获从ParcelFileDescriptor对象中获取fd,从fd中读取数据。
    1、通过AIDL传输FileDescriptor方式;
    2、通过Bundler传输;

    创建继承Binder的类

    class ImageBinder extends Binder {
        private Bitmap bitmap;
        public ImageBinder(Bitmap bitmap) {
            this.bitmap = bitmap;
        }
        Bitmap getBitmap() {
            return bitmap;
        }
    }
    

    发送大图

    Intent intent = new Intent(this, SecondActivity.class);
    Bundle bundle = new Bundle();
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
        bundle.putBinder("bitmap", new ImageBinder(mBitmap));
    }
    intent.putExtras(bundle);
    startActivity(intent);
    

    接收大图

    Bundle bundle = getIntent().getExtras();
    if (bundle != null) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
                    ImageBinder imageBinder = (ImageBinder) bundle.getBinder("bitmap");
                    Bitmap bitmap = imageBinder.getBitmap();
                    mIv.setImageBitmap(bitmap);
        }
     }
    

    较大的bitmap直接通过Intent传递报错TransactionTooLargeException; 是因为Intent启动组件时,系统禁掉了文件描述符fd,bitmap无法利用共享内存,只能采用拷贝到缓冲区的方式,导致缓冲区超限,触发异常;putBinder 的方式,避免了intent 禁用描述符的影响,bitmap 写parcel时的fd 默认是true,可以利用到共享内存,所以能高效传输图片。

    注意:概念了解
    1、MemoryFile
    MemoryFile是android在最开始就引入的一套框架,其内部实际上是封装了Android特有的内存共享机制Ashmem匿名共享内存;
    2、mmap
    内存映射

    3、FileDescriptor
    Linux 系统中,把一切都看做是文件,当进程打开现有文件或创建新文件时,内核向进程返回一个文件描述符,文件描述符就是内核为了高效管理已被打开的文件所创建的索引,用来指向被打开的文件,所有执行I/O操作的系统调用都会通过文件描述符。
    4、ParcelFileDescriptor
    ParcelFileDescriptor是可以用于进程间Binder通信的FileDescriptor,其实就是实现了Parcelable并能够通过流获取数据。

    参考:使用 AIDL 实现跨进程传输一个2M大小的文件

    四、Android中的IPC方式

    1、使用Bundle

    四大组件中的三大组件(Activity,Service,Receiver)都是支持在Intent中传递Bundle数据;

    2、使用文件共享

    两个进程通过读写同一个文件来交换数据;

    3、使用Messenger

    Messenger:信使,通过它可以在不同进程中传递Message对象,只能串行执行;
    使用步骤:

    1. 服务端进程
      在服务端创建一个Service来处理客户端的连接请求,同时创建Handler并通过它来创建一个Messenger对象,然后在Service的onBind中返回这个Messenger对象底层的Binder;
    2. 客户端进程
      绑定服务端的Service,绑定成功后用服务端返回的IBinder对象创建一个Messenger,通过这个Messenger就可以向服务端发送消息了

    4、使用AIDL

    AIDL (Android Interface Definition Language) 是一种接口定义语言,用于生成可以在Android设备上两个进程之间进行进程间通信(Interprocess Communication, IPC)的代码。如果在一个进程中(例如Activity)要调用另一个进程中(例如Service)对象的操作,就可以使用AIDL生成可序列化的参数,来完成进程问通信。

    Messenger底层实现是AIDL,Messenger只能传递消息Message,Messenger只能一个个处理消息,如果有大量并发请求获取调用服务端方法,则需要使用AIDL;
    使用步骤:

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

    注意:
    1.AIDL 找不到符号 方法 readFromParcel(Parcel)
    重写bean文件添加方法readFromParcel;
    2.aidl 找不到符号
    把Java的Bean文件放到Java文件夹下,aidl文件放到aidl文件夹下,重新编译。

    AIDL原理
    Binder通信过程
    1. Binder对象的获取
      Binder是实现跨进程通信的基础,Binder在服务端和客户端实现共享,是同一个对象,通过Binder对象获取实现了IInterface接口的对象来调用远程服务;

    服务端通过binder保存和获取IInterface两个关键方法: attachInterface和queryLocalInterface。

    public class Binder implements IBinder {
        private IInterface mOwner;
    
        public void attachInterface(@Nullable IInterface owner, @Nullable String descriptor) {
            mOwner = owner;
            mDescriptor = descriptor;
        }
    
        public @Nullable IInterface queryLocalInterface(@NonNull String descriptor) {
            if (mDescriptor != null && mDescriptor.equals(descriptor)) {
                return mOwner;
            }
            return null;
        }
        //第一个参数:识别调用哪一方法的id
        //第二个参数:序列化传入的数据
        //第三个参数:调用方法后返回的数据
        public final boolean transact(int code, @NonNull Parcel data, @Nullable Parcel reply,int flags) throws RemoteException {
            if (false) Log.v("Binder", "Transact: " + code + " to " + this);
    
            if (data != null) {
                data.setDataPosition(0);
            }
            boolean r = onTransact(code, data, reply, flags);
            if (reply != null) {
                reply.setDataPosition(0);
            }
            return r;
        }
    
        protected boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags) throws RemoteException {
            ...
        }
    }
    

    Binder具有跨进程传输的能力是因为实现了IBinder接口。系统会为每个实现了该接口的对象提供跨进程传输。
    Binder具有完成特定任务的能力是通过它的IInterface对象获取的。

    1. 调用服务端方法
      Proxy对象中的transact调用发生后,会引起系统的注意,系统意识到Proxy对象想找它的真身Binder对象(系统其实一直存着Binder和Proxy的对应关系)。于是系统将这个请求中的数据转发给Binder对象,Binder对象将会在onTransact中收到Proxy对象传来的数据,于是它从data中取出客户端进程传来的数据,又根据第一个参数确定想让它执行添加书本操作,于是它就执行了响应操作,并把结果写回reply。

    参考:AIDL使用详解

    5、使用ContentProvider

    ContentProvider是Android中提供的专门用于不同应用间进行数据共享的方式,和Messenger一样,ContentProvider的底层实现同样也是Binder;

    1、创建ContentProvider
    public class BankProvider extends ContentProvider {
    
        //ContentProvider的创建,初始化工作,运行在主线程中;
        @Override
        public boolean onCreate() {
            return false;
        }
    
        //用于向指定uri的ContentProvider中添加数据,返回该行的Uri,运行在ContentProvider线程中;
        @Override
        public Uri insert(Uri uri, ContentValues values) {
            return null;
        }
    
        //用于删除指定uri的数据,返回成功删除的行数,运行在ContentProvider线程中;
        @Override
        public int delete(Uri uri, String selection, String[] selectionArgs) {
            return 0;
        }
    
        //用户更新指定uri的数据,返回成功修改的行数,运行在ContentProvider线程中;
        @Override
        public int update(Uri uri, ContentValues values, String selection,
                          String[] selectionArgs) {
            return 0;
        }
    
        //用于查询指定uri的数据返回一个Cursor,运行在ContentProvider线程中;
        @Override
        public Cursor query(Uri uri, String[] projection, String selection,
                            String[] selectionArgs, String sortOrder) {
            return null;
    
        }
    
        //返回一个Uri请求对应的MIME类型,运行在ContentProvider线程中;
        @Override
        public String getType(Uri uri) {
            return null;
        }
    }
    

    ContentProvider的主要方法:

    public boolean onCreate():在创建 ContentProvider 时使用
    public Cursor query():用于查询指定 uri 的数据返回一个 Cursor
    public Uri insert():用于向指定uri的 ContentProvider 中添加数据
    public int delete():用于删除指定 uri 的数据
    public int update():用户更新指定 uri 的数据
    public String getType():用于返回指定的 Uri 中的数据 MIME 类型
    注:数据访问的方法 insert,delete 和 update 可能被多个线程同时调用,此时必须是线程安全的。
    

    注意:除了onCreate由系统回调运行在主进程中,其余5个方法均由外界回调并运行在Binder线程池中.

    2、注册ContentProvider
    <provider
                android:name=".ui.fragment.BankProvider"
                android:authorities="com.alan.user.bank"
                android:permission="com.alan.PROVIDER"
                android:readPermission="com.alan.PROVIDER"
                android:writePermission="com.alan.PROVIDER"
                android:enabled="true"
                android:exported="true"/>
    

    android:authorities:是ContentProvider的唯一标识,通过这个属性外部应用访问;
    android:permission:外界想要访问这个ContentProvider,必须添加的权限.
    android:readPermission:读权限
    android:writePermission:写权限

    3、访问ContentProvider
    Uri uri =Uri.parse("com.alan.user.bank");
    getContentResolver().query(uri,null, null, null, null);
    
    4、ContentProvider 核心类

    1、UriMatcher:操作 Uri 的工具类,用于匹配 Uri;
    2、ContentUris:操作 Uri 的工具类,用于操作 Uri 路径后面的 ID 部分;
    3、ContentObserver:用于监听 ContentProvider 中指定 Uri 标识数据的变化(增 / 删 / 改)

    参考:ContentProvider基本使用

    6、使用Socket

    Socket:也称为套接字,是网络通信中的概念,它分为流式套接字和用户数据报套接字两种,分别对应于网络的传输控制层中TCP和UDP协议.Socket可以支持传输任意字节流.
    使用步骤:
    1、声明网络权限INTERNET和ACCESS_NETWORK_STATE
    2、创建服务端Service,并在子线程(不能在主进程中访问网络)中创建ServerSocket监听端口号;
    3、创建客户端,开启线程去连接服务端,通过Socket发送消息;

    五、Binder连接池

    AIDL的流程:
    1、创建一个Service和一个AIDL接口;
    2、创建一个类继承自AIDL接口中的Stub类并实现Stub中的抽象方法;
    3、在Service的onBind方法中返回这个类的对象;
    4、然后客户端就可以绑定服务端的Service,建立连接后就可以访问远程服务端的方法.

    AIDL是一种常用的进程间通讯方式,每次都要创建Service,如果100个就会创建100个service,这会浪费系统资源.所以需要减少Service数量,将所有的AIDL放在同一个Service中去管理.

    服务端提供一个queryBinder接口,根据业务模块的特征来返回相应的Binder对象客户端,不同业务模块拿到所需的Binder对象后就可以进行远程方法调用了.
    Binder连接池的主要作用就是将每个业务模块的Binder请求统一转发到远程Service中去执行,从而避免了重复创建Service的过程.

    六、选用合适的IPC方式

    1.png

    相关文章

      网友评论

        本文标题:Android开发二《IPC机制》

        本文链接:https://www.haomeiwen.com/subject/uiozlrtx.html