androidAndroid面试理论

Android开发二《IPC机制》

2022-03-22  本文已影响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方式原因
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通信的四个角色
Binder运行机制

Client/Server/ServiceManage之间的相互通信都是基于Binder机制。既然基于
Binder机制通信,那么同样也是C/S架构,则图中的3大步骤都有相应的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
上一篇下一篇

猜你喜欢

热点阅读