十年网站开发经验 + 多家企业客户 + 靠谱的建站团队
量身定制 + 运营维护+专业推广+无忧售后,网站问题一站解决
在某些业务场景下,我们需要在应用中单独开启一个进程进行一些操作。比如性能监控,如果让原始业务和性能监控本身的业务跑在同一个进程下,那么就会导致性能统计的数据的失真。
创新互联长期为成百上千客户提供的网站建设服务,团队从业经验10年,关注不同地域、不同群体,并针对不同对象提供差异化的产品和服务;打造开放共赢平台,与合作伙伴共同营造健康的互联网生态环境。为吉隆企业提供专业的成都网站建设、做网站,吉隆网站改版等技术服务。拥有10年丰富建站经验和众多成功案例,为您定制开发。
而进程间通信,一般采用AIDL机制的客户端与服务端通信。
AIDL只能传递如下几类数据:
当传递自定义 Parcelable 时,有三处地方需要注意:
当传递其他 aidl 接口时,同样必须要 import 这个 aidl 文件
编写完 aidl 文件后,make一下工程,会在 build 下的 generated 下的 source 下的 aidl 目录生成对应的接口类文件。aidl 接口其实就是 API 接口,通过实现对应接口类的 Stub 子类来实现具体的 API 逻辑;通过对应接口类的 Stub 子类的 asInterface 方法得到具体的实现类,调用具体的 API 方法。
一个基本的客户端服务端的通信结构一般包括如下功能
客户端的功能
服务端的功能
客户端的相关功能实现比较简单,麻烦的是服务端的功能。因为 AIDL 接口定义的都是服务端的接口,是由客户端来调用的。而想要实现服务端反向调用客户端则需要通过其他手段实现。
想要实现服务端主动连接客户端,最好的办法就是 服务端发送广播,客户端收到广播后再主动连接服务端 ,通过这种方式变相地实现服务端主动连接客户端的功能
想要实现服务端主动断开客户端,除了上面 发送广播是一种实现方式外,还可以通过 android 的系统API RemoteCallbackList,用包名作为key值来注册远程回调接口的方式,让服务端持有客户端的回调接口,服务端调用回调接口,客户端在回调接口中实现主动断开服务端 ,通过这种方式变量地实现服务端主动断开客户端的功能。而采用后者会显得更加优雅
既然所有的操作归根结底都是由客户端来完成的,那么客户端必须得有如下的功能模块:
服务端必须得有的功能模块:
那么,整体的通信流程就是如下的步骤:
首先是通信的 aidl 接口定义
然后是客户端的连接操作与断开连接操作,包括广播接收者的注册以及回调接口的实现
然后是客户端的拉取数据和推送数据操作
接着是服务端的 iBinder 接口的实现,完成回调接口的注册、业务子线程的开启和关闭、数据的推送和数据的拉取操作
然后是服务端的主动连接和主动断开连接操作
最后是服务端的 onUnbind 方法的实现,对回调接口进行反注册
服务端模仿 FloatViewPlugin 自定义插件,实现 IServicePlugin 接口,定制个性化的悬浮窗插件
客户端在 Appliaction 的 onCreate方法中初始化
在 MainActivity 上实现连接、断开、数据通信
一个通信通道,实现跨进程的的Socket网络通信。
具体的通信通道的图如下。
android进程间通信是使用Binder来传数据,而Binder传输的数据,有一个最为基本的要求,就是要实现Parcelable接口。
ParcelFileDescriptor是android提供的一个数据结构。
ParcelFileDescriptor是可以用于进程间Binder通信的FileDescriptor。支持stream 写入和stream 读出
我们可以使用
来将PacecelFileDescriptor 与File对应起来,以实现进程间的文件共享。
我们也可以使用
来建立一个pipe通信通道,ParcelFileDescriptor数组第一个元素是read端,第二个元素是write端,通过write端的AutoCloseOutputStream和read端的AutoCloseInputStream,我们就可以实现进程见的数据流传输了。
发送端:
1. 业务层调用getOutputStream向通信层发起请求
2. 通信层通过creatPipe 建立一个ParcelFileDescriptor数组,并将write端的pipe[1]返回给业务层
3. 业务层得到pipe[1](ParcelFileDescriptor)后,可以通过AutoCloseOutputStream写入数据
4. 从通信层的pipe[0]的AutoCloseInputStream中读出数据通过socket发送出去
接收端:
1. 业务层调用getInputStream向通信层发起请求
2. 通信层通过creatPipe 建立一个ParcelFileDescriptor数组,并将read端的pipe[0]返回给业务层
3. 业务层得到pipe 0 后,可以通过AutoCloseInputStream读取数据。(如没有数据,则阻塞,一直等到有数据为止)
4. socket中读取数据,写入到通信层的pipe[1]的AutoCloseOutputStream。(pipe[1]一旦写入,第三步中pipe[2]就可以读取出数据)
还是先看共享内存的使用方法,我主要介绍两个函数:
通过 shmget() 函数申请共享内存,它的入参如下
通过 shmat() 函数将我们申请到的共享内存映射到自己的用户空间,映射成功会返回地址,有了这个地址,我们就可以随意的读写数据了,我们继续看一下这个函数的入参
共享内存的原理是在内存中单独开辟的一段内存空间,这段内存空间其实就是一个tempfs(临时虚拟文件),tempfs是VFS的一种文件系统,挂载在/dev/shm上,前面提到的管道pipefs也是VFS的一种文件系统。
由于共享的内存空间对使用和接收进程来讲,完全无感知,就像是在自己的内存上读写数据一样,所以也是 效率最高 的一种IPC方式。
上面提到的IPC的方式都是 在内核空间中开辟内存来存储数据 ,写数据时,需要将数据从用户空间拷贝到内核空间,读数据时,需要从内核空间拷贝到自己的用户空间,
共享内存就只需要一次拷贝 ,而且共享内存不是在内核开辟空间,所以可以 传输的数据量大 。
但是 共享内存最大的缺点就是没有并发的控制,我们一般通过信号量配合共享内存使用,进行同步和并发的控制 。
共享内存在Android系统中主要的使用场景是 用来传输大数据 ,并且 Android并没有直接使用Linux原生的共享内存方式,而是设计了Ashmem匿名共享内存 。
之前说到有名管道和匿名管道的区别在于有名管道可以在vfs目录树中查看到这个管道的文件,但是匿名管道不行, 所以匿名共享内存同样也是无法在vfs目录中查看到 的, Android之所以要设计匿名共享内存 ,我觉得主要是为了安全性的考虑吧。
我们来看看共享内存的一个使用场景,在Android中,如果我们想要将当前的界面显示出来,需要将当前界面的图元数据传递Surfaceflinger去做图层混合,图层混合之后的数据会直接送入帧缓存,送入帧缓存后,显卡就会直接取出帧缓存里的图元数据显示了。
那么我们如何将应用的Activity的图元数据传递给SurfaceFlinger呢?想要将图像数据这样比较大的数据跨进程传输,靠binder是不行的,所以这儿便用到匿名共享内存。
从谷歌官方提供的架构图可以看到,图元数据是通过BufferQueue传递到SurfaceFlinger去的,当我们想要绘制图像的时候, 需要从BufferQueue中申请一个Buffer,Buffer会调用Gralloc模块来分配共享内存 当作图元缓冲区存放我们的图元数据。
可以看到Android的匿名共享内存是通过 ashmem_create_region() 函数来申请共享内存的,它会在/dev/ashmem下创建一个虚拟文件,Linux原生共享内存是通过shmget()函数,并会在/dev/shm下创建虚拟文件。
匿名共享内存是通过 mmap() 函数将申请到的内存映射到自己的进程空间,而Linux是通过*shmat()函数。
虽然函数不一样,但是Android的匿名共享内存和Linux的共享内存在本质上是大同小异的。
要使用一块共享内存
本文整理和引用他人的笔记,旨在个人复习使用。
参考链接:
;depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.edu_weight
默认情况下,一个app只会运行在一个进程中,进程名为app的包名。
1. 分散内存的占用
Android系统对每个应用进程的内存占用是有限制的,占用内存越大的进程,被系统杀死的可能性就越大。使用多进程可以减少主进程占用的内存,避免OOM问题,降低被系统杀死的概率。
2. 实现多模块
一个成熟的应用一定是多模块化的。项目解耦,模块化,意味着开辟新的进程,有独立的JVM,带来数据解耦。模块之间互不干预,团队并行开发,同时责任分工也很明确。
3. 降低程序奔溃率
子进程崩溃不会影响主进程的运行,能降低程序的崩溃率。
4. 实现一些特殊功能
比如可以实现推送进程,使得主进程退出后,能离线完成消息推送服务。还可以实现守护进程,来唤醒主进程达到保活目的。还可以实现监控进程专门负责上报bug,进而提升用户体验。
android:process 属性的值以冒号开头的就是 私有进程 ,否则就是 公有进程 。当然命名还需要符合规范,不能以数字开头等等。
1. 前台进程
2. 可见进程
3. 服务进程
4. 后台进程
5. 空进程
Android 会将进程评定为它可能达到的最高级别。另外服务于另一进程的进程其级别永远不会低于其所服务的进程。
创建新的进程时会创建新的Application对象,而我们通常在Application的onCreate方法中只是完成一些全局的初始化操作,不需要多次执行。
解决思路:获取当前进程名,判断是否为主进程,只有主进程的时候才执行初始化操作
获取当前进程名的两种方法:
Application中判断是否是主进程(方法1例子):
Serializable 和 Parcelable是数据序列化的两种方式,Android中只有进行序列化过后的对象才能通过intent和Binder传递。
通常序列化后的对象完成传输后,通过反序列化获得的是一个新对象,而不是原来的对象。
Serializable是java接口,位于java.io的路径下。Serializable的原理就是把Java对象序列化为二进制文件后进行传递。Serializable使用起来非常简单,只需直接实现该接口就可以了。
Parcelable是Google为了解决Serializable效率低下的问题,为Android特意设计的一个接口。Parcelable的原理是将一个对象完全分解,分解成可以传输的数据类型(如基本数据类型)再进行传递。
通常需要存到本地磁盘的数据就使用Serializable,其他情况就使用效率更高的Parcelable。
IPC 即 Inter-Process Communication (进程间通信)。Android 基于 Linux,而 Linux 出于安全考虑,不同进程间不能之间操作对方的数据,这叫做“进程隔离”。
每个进程的虚拟内存空间(进程空间)又被分为了 用户空间和内核空间 , 进程只能访问自身用户空间,只有操作系统能访问内核空间。
由于进程只能访问自身用户空间,因此在传统的IPC中,发送进程需要通过copy_from_user(系统调用)将数据从自身用户空间拷贝到内核空间,再由接受进程通过copy_to_user从内核空间复拷贝到自身用户空间,共需要拷贝2次,效率十分低下。Android采用的是Binder作为IPC的机制,只需复制一次。
Binder翻译过来是粘合剂,是进程之间的粘合剂。
Binder IPC通信的底层原理是 通过内存映射(mmap),将接收进程的用户空间映射到内核空间 ,有了这个映射关系,接收进程就能通过用户空间的地址获得内核空间的数据,这样只需发送进程将数据拷贝到内核空间就可完成通讯。
一次完整的Binder IPC通信:
从IPC的角度看,Binder是一种跨进程通信机制(一种模型),Binder 是基于 C/S 架构的,这个通信机制中主要涉及四个角色:Client、Server、ServiceManager和Binder驱动。
Client、Server、ServiceManager都是运行在用户空间的进程,他们通过系统调用(open、mmap 和 ioctl)来访问设备文件/dev/binder,从而实现与Binder驱动的交互。Binder驱动提供进程间通信的能力(负责完成一些底层操作,比如开辟数据接受缓存区等),是Client、Server和ServiceManager之间的桥梁。
Client、Server就是需要进行通信两个的进程,通信流程:
细心的你一定发现了,注册服务和获得服务本身就是和ServiceManager进行跨进程通信。其实和ServiceManager的通信的过程也是获取Binder对象(早已创建在Binder驱动中,携带了注册和查询服务等接口方法)来使用,所有需要和ServiceManager通信的进程,只需通过0号引用,就可以获得这个Binder对象了。
AIDL内部原理就是基于Binder的,可以借此来分析Binder的使用。
AIDL是接口定义语言,简短的几句话就能定义好一个复杂的、内部有一定功能的java接口。
先看看ICallBack.aidl文件,这里定义了一个接口,表示了服务端提供的功能。
被定义出来的java接口继承了IInterface接口,并且内部提供了一个Stub抽象类给服务端(相当于封装了一下,服务端只需继承这个类,然后完成功能的里面具体的实现)。
参考:
(以下是添加了回调的最终实现,可以看参考链接一步一步来)
为需要使用的类,创建aidl文件。
系统会自动在main文件下生成aidl文件夹,并在该文件夹下创建相应目录。
在java相同路径下创建Student类,这里不能使用@Parcelize注解,否则会报错
创建IStudentService.aidl,定义了一个接口,该接口定义了服务端提供的功能。创建完后rebuild一下项目 (每次创建和修改定义接口文件都要rebuild一下)
创建在服务端的StudentService
可以看见有回调,说明客户端也提供了接口给服务端来回调(双向通信,此时客户端的变成了服务端),即ICallBack.aidl
客户端是通过Binder驱动返回的Binder调用StudentService里的具体实现方法
AIDL使用注意:
Messenger可以在不同进程中传递 Message 对象,在Message中放入我们需要传递的数据,就可以轻松地实现数据的进程间传递了。Messenger 是一种轻量级的 IPC 方案,是对AIDL的封装,底层实现是 AIDL。
使用详见:
进程:是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。
线程:是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一些在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
区别:
(1)、一个程序至少有一个进程,一个进程至少有一个线程;
(2)、线程的划分尺度小于进程,使得多线程程序的并发性高;
(3)、进程在执行过程中拥有独立的内存单元,而多个线程共享内存,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉。
---------------------
一、Android进程间通信方式
1.Bundle
由于Activity,Service,Receiver都是可以通过Intent来携带Bundle传输数据的,所以我们可以在一个进程中通过Intent将携带数据的Bundle发送到另一个进程的组件。
缺点:无法传输Bundle不支持的数据类型。
2.ContentProvider
ContentProvider是Android四大组件之一,以表格的方式来储存数据,提供给外界,即Content Provider可以跨进程访问其他应用程序中的数据。用法是继承ContentProvider,实现onCreate,query,update,insert,delete和getType方法,onCreate是负责创建时做一些初始化的工作,增删查改的方法就是对数据的查询和修改,getType是返回一个String,表示Uri请求的类型。注册完后就可以使用ContentResolver去请求指定的Uri。
3.文件
两个进程可以到同一个文件去交换数据,我们不仅可以保存文本文件,还可以将对象持久化到文件,从另一个文件恢复。要注意的是,当并发读/写时可能会出现并发的问题。
4.Broadcast
Broadcast可以向android系统中所有应用程序发送广播,而需要跨进程通讯的应用程序可以监听这些广播。
5.AIDL方式
Service和Content Provider类似,也可以访问其他应用程序中的数据,Content Provider返回的是Cursor对象,而Service返回的是Java对象,这种可以跨进程通讯的服务叫AIDL服务。
AIDL通过定义服务端暴露的接口,以提供给客户端来调用,AIDL使服务器可以并行处理,而Messenger封装了AIDL之后只能串行运行,所以Messenger一般用作消息传递。
6.Messenger
Messenger是基于AIDL实现的,服务端(被动方)提供一个Service来处理客户端(主动方)连接,维护一个Handler来创建Messenger,在onBind时返回Messenger的binder。
双方用Messenger来发送数据,用Handler来处理数据。Messenger处理数据依靠Handler,所以是串行的,也就是说,Handler接到多个message时,就要排队依次处理。
7.Socket
Socket方法是通过网络来进行数据交换,注意的是要在子线程请求,不然会堵塞主线程。客户端和服务端建立连接之后即可不断传输数据,比较适合实时的数据传输
二、Android线程间通信方式
一般说线程间通信主要是指主线程(也叫UI线程)和子线程之间的通信,主要有以下两种方式:
1.AsyncTask机制
AsyncTask,异步任务,也就是说在UI线程运行的时候,可以在后台的执行一些异步的操作;AsyncTask可以很容易且正确地使用UI线程,AsyncTask允许进行后台操作,并在不显示使用工作线程或Handler机制的情况下,将结果反馈给UI线程。但是AsyncTask只能用于短时间的操作(最多几秒就应该结束的操作),如果需要长时间运行在后台,就不适合使用AsyncTask了,只能去使用Java提供的其他API来实现。
2.Handler机制
Handler,继承自Object类,用来发送和处理Message对象或Runnable对象;Handler在创建时会与当前所在的线程的Looper对象相关联(如果当前线程的Looper为空或不存在,则会抛出异常,此时需要在线程中主动调用Looper.prepare()来创建一个Looper对象)。使用Handler的主要作用就是在后面的过程中发送和处理Message对象和让其他的线程完成某一个动作(如在工作线程中通过Handler对象发送一个Message对象,让UI线程进行UI的更新,然后UI线程就会在MessageQueue中得到这个Message对象(取出Message对象是由其相关联的Looper对象完成的),并作出相应的响应)。
三、Android两个子线程之间通信
面试的过程中,有些面试官可能会问Android子线程之间的通信方式,由于绝大部分程序员主要关注的是Android主线程和子线程之间的通信,所以这个问题很容易让人懵逼。
主线程和子线程之间的通信可以通过主线程中的handler把子线程中的message发给主线程中的looper,或者,主线程中的handler通过post向looper中发送一个runnable。但looper默认存在于main线程中,子线程中没有Looper,该怎么办呢?其实原理很简单,把looper绑定到子线程中,并且创建一个handler。在另一个线程中通过这个handler发送消息,就可以实现子线程之间的通信了。
子线程创建handler的两种方式:
方式一:给子线程创建Looper对象:
new Thread(new Runnable() {
public void run() {
Looper.prepare(); // 给这个Thread创建Looper对象,一个Thead只有一个Looper对象
Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
Toast.makeText(getApplicationContext(), "handleMessage", Toast.LENGTH_LONG).show();
}
};
handler.sendEmptyMessage(1);
Looper.loop(); // 不断遍历MessageQueue中是否有消息
};
}).start();
---------------------
方式二:获取主线程的looper,或者说是UI线程的looper:
new Thread(new Runnable() {
public void run() {
Handler handler = new Handler(Looper.getMainLooper()){ // 区别在这!!!
@Override
public void handleMessage(Message msg) {
Toast.makeText(getApplicationContext(), "handleMessage", Toast.LENGTH_LONG).show();
}
};
handler.sendEmptyMessage(1);
};
}).start();
---------------------