Android多进程

一:了解多进程

问题:整个app都在一个进程有什么弊端?

     在Android中,虚拟机分配给各个进程的运行内存是有限制值的(这个值可以是32M,48M,64M等,根据机型而定),试想一下,如果在app中,增加了一个很常用的图片选择模块用于上传图片或者头像,加载大量Bitmap会使app的内存占用迅速增加,如果你还把查看过的图片缓存在了内存中,那么OOM的风险将会大大增加,如果此时还需要使用WebView加载一波网页,内存就更加紧张了。

      微信移动开发团队在《Android内存优化杂谈》一文中就说到:“对于webview,图库等,由于存在内存系统泄露或者占用内存过多的问题,我们可以采用单独的进程,微信当前也会把它们放在单独的tools进程中”。

   多进程app可以在系统中申请多份内存,但应合理使用,建议把一些高消耗但不常用的模块放到独立的进程,不使用的进程可及时手动关闭。

进程生命周期与优先级:

      在大多数情况下,每个Android应用都在各自的Linux进程中运行,当需要运行应用的一些代码时,系统会为应用创建此进程,并使其保持运行,直到不再需要它且系统需要回收其内存以供其他应用使用。

      应用进程的生命周期并不由应用本身直接控制,而是由系统综合多种因素来确定的,比如系统所知道的正在运行的应用部分、这些内容对用户的重要程度,以及系统中可用的总内存量,这是Android非常独特的一个基本功能。

     应用开发者必须了解不同的应用组件(特别是Activity、Service和BroadcastReceiver)对应用进程的生命周期有何影响,这些组件使用不当会导致系统在应用进程正执行重要任务时将它终止,Android利用重要性层次结构,就是将最重要的保留,杀掉不重要的进程。

Android将重要性层次结构分为5个层级,分为了:

前台进程:用户当前操作所必需的进程,如果一个进程满足以下任一条件,即视为前台进程。

1、托管用户正在交互的Activity(已调用Activity的onResume()方法)。

2、托管某个Service,后者绑定到用户正在交互的Activity。

3、托管正在“前台”运行的Service(服务已调用startForeground())。

4、托管正执行一个生命周期回调的Service(onCreate()、onStart()或onDestroy())。

5、托管正执行其onReceive()方法的BroadcastReceiver。

可见进程:没有任何前台组件、但仍会影响用户在屏幕上所见内容的进程,如果一个进程满足以下任一条件,即视为可见进程。

1、托管不在前台、但仍对用户可见的Activity(已调用其onPause()方法)。例如,如果前台Activity启动了一个对话框,允许在其后显示上一Activity,则有可能会发生这种情况。

2、托管绑定到可见(或前台)Activity的Service。

服务进程:正在运行已使用startService()方法启动的服务且不属于上述两个更高类别进程的进程。尽管服务进程与用户所见内容没有直接关联,但是它们通常在执行一些用户关心的操作(例如,在后台播放音乐或从网络下载数据)。因此,除非内存不足以维持所有前台进程和可见进程同时运行,否则系统会让服务进程保持运行状态。

后台进程:包含目前对用户不可见的Activity的进程(已调用Activity的onStop()方法)。这些进程对用户体验没有直接影响,系统可能随时终止它们,以回收内存供前台进程、可见进程或服务进程使用。 通常会有很多后台进程在运行,因此它们会保存在LRU(最近最少使用)列表中,以确保包含用户最近查看的Activity的进程最后一个被终止。如果某个Activity正确实现了生命周期方法,并保存了其当前状态,则终止其进程不会对用户体验产生明显影响,因为当用户导航回该Activity时,Activity会恢复其所有可见状态。

空进程:不含任何活动应用组件的进程。保留这种进程的的唯一目的是用作缓存,以缩短下次在其中运行组件所需的启动时间。为使总体系统资源在进程缓存和底层内核缓存之间保持平衡,系统往往会终止这些进程。

系统实例:

Android多进程_第1张图片

Low Memory Killer:

      进程按照状态分完重要性之后,就要开始杀进程了。Android的Low Memory Killer基于Linux的OOM机制,在Linux中,内存是以页面(page)为单位,当申请页面分配不足的时候,系统会通过Low Memory Killer来杀掉bad进程,释放内存。Low Memory Killer会根据进程的adj级别以及所占的内存,来决定是否杀掉该进程,adj越大,占用内存越多,进程越容易被杀掉。

参考一下其他应用的多进程使用情况:

微信:

微博:

      可以看到,微信的确有一个tools进程,而新浪微博也有image相关的进程,而且它们当中还有好些其它的进程,比如微信的push进程,微博的remote进程等,这里可以看出,他们不单单只是把上述的WebView、图库等放到单独的进程,还有推送服务等也是运行在独立的进程中的。一个消息推送服务,为了保证稳定性,可能需要和UI进程分离,分离后即使UI进程退出、Crash或者出现内存消耗过高等情况,仍不影响消息推送服务。

二:项目中多进程的实现

       各种组件元素的清单文件条目activity、service、receiver和provider均支持android:process属性,此属性能够指定该组件应在哪一个进程运行。您能够设置此属性,使每一个组件均在各自的进程中运行,或者使一些组件共享一个进程,而其余组件则不共享,此外,您还能够设置android:process,使不一样应用的组件在相同的进程中运行,但前提是这些应用共享相同的Linux用户ID(shareUID)并使用相同的证书进行签署。

       此外,application元素还支持android:process属性,以设置适用于全部组件的默认值。

私有进程和全局进程:

1、进程名以“:”开头的进程属于当前应用的私有进程,其他应用的组件不可以和它跑在同一个进程当中。

2、不以“:”开头的进程属于全局进程,其他应用通过shareUID方式可以和它跑在同一个进程中。

3、Android会为每个应用分配唯一的一个UID,具有相同UID的应用才能共享数据。

三:多进程的优缺点与使用场景

多进程优点:

优点一:为应用解决了OOM问题,Android对内存的限制是针对于进程的,这个阈值能够是48M、24M、16M等,视机型而定,因此,当咱们须要加载大图之类的操做,能够在新的进程中去执行,避免主进程OOM。

优点二:更有效、合理的利用内存。咱们能够在适当的时候生成新的进程,在不须要的时候及时杀掉,合理分配,提高用户体验。减小系统被杀掉的风险。

优点三:单一进程崩溃并不影响总体应用的使用。例如我在图片浏览进程打开了一个过大的图片,java heap申请内存失败,可是不影响我主进程的使用,并且,还能经过监控进程,将这个错误上报给系统,告知他在什么机型、环境下、产生了什么样的Bug,提高用户体验。

优点四:当咱们的应用开发愈来愈大,模块愈来愈多,团队规模也愈来愈大,协做开发也是个很麻烦的事情。项目解耦,模块化,是这阶段的目标。经过模块解耦,开辟新的进程,独立的JVM,来达到数据解耦目的。模块之间互不干预,团队并行开发,责任分工也明确,多模块应用,好比应用大而全,里面确定会有不少模块,假若有地图模块、大图浏览、自定义WebView等等(这些都是吃内存大户),还会有一些诸以下载服务,监控服务等等,一个成熟的应用必定是多模块化的。

多进程缺点:

缺点一:静态成员和单例模式完全失效。

缺点二:线程同步机制完全消失:因为不是同一个内存,那么无论是锁对象还是全局类都无法保证线程同步,因为不同进程锁的不是同一个对象。

缺点三:SharePreference可靠性下降,SharePreference不支持两个进程同时执行写操作,因为会导致数据丢失,因为SharedPreferences底层是通过读写XML文件实现的,并发写显然会出现问题,甚至读/写多可能出问题。

缺点四:Application会多次创建,系统在创建进程的同时分配独立的虚拟机(即代表会多次创建Application),当一个组件运行在一个新的进程中,由于系统要创建新的进程同时分配独立的虚拟机,因此就是一个启动应用的过程,既然重新启动则会创建新的Application。

四:Android跨进程通讯实现

       Android中追根溯源只有两种进程间通信方式,其他的方式都是通过封装这两种方式而得到的:Binder与Socket。

方式一:Android—Binder机制

(https://blog.csdn.net/ly0724ok/article/details/117566381/)

图例:

Android多进程_第2张图片

Binder通信的四个角色:

Client进程:使用服务的进程。

Server进程:提供服务的进程。

ServiceManager进程:ServiceManager的作用是将字符形式的Binder名字转化成Client中对该Binder的引用,使得Client能够通过Binder名字获得对Server中Binder实体的引用。

Binder驱动:驱动负责进程之间Binder通信的建立,Binder在进程之间的传递,Binder引用计数管理,数据包在进程之间的传递和交互等一系列底层支持。

优点:

Android多进程_第3张图片

方式二:Android中Socket通信的简单实现

解析:Android Framework 层代码中大量使用了 Binder IPC 通信方式,除此之外,Socket也是一种重要的IPC通信方式,比如StorageManagerService(8.0 之前叫 MountService)与 Vold 之前的通信,SystemServer 和 Zygote 之间也是通过 Socket 进行通信的。

图例:

Android多进程_第4张图片

Socket通信的实现步骤:

步骤一:建立一个服务器Socket。

步骤二:通过监听获取一个用于通信的Socket对象。

步骤三:在一个新线程中,通过对Socket对象进行封装,分别得到输入、输出流的引用对象,通过这两个对象向Client端发送或者从Client端接收数据,进而实现Socket通信。

步骤四:在适当的时机关闭Socket连接。

多进程实现中的相关概念了解:

概念一:RPC

1、Remote Procedure Call(远程过程调用)是一种计算机通讯协议,为我们定义了计算机C中的程序如何调用另外一台计算机S的程序。

2、RPC是Client/Server模式,客户端对服务器发出请求,服务器收到请求并且根据客户端提供的参数进行操作,然后结果返回给客户端。

概念二:IDL

1、Interface Definition Language (接口定义语言):通过一种中立的方式来描述接口,使得在不同平台上运行的对象和用不同语言编写的程序可以相互通信交流。

2、RPC只是一种协议,规定了通信的规则。

3、因为客户端与服务端平台的差异性,为了统一处理不同的实现,需要定义一个共同的接口,即就是IDL。

概念三:IPC(Inter-Process Communication进程间通信)

Android基于Linux,而Linux出于安全考虑,不同进程间不能之间操作对方的数据,这叫做“进程隔离”。

Android进程间通信对比:

Android多进程_第5张图片

多进程通信方式的选择:

1、只有允许不同应用的客户端用IPC方式调用远程方法,并且想要在服务中处理多线程时,才有必要使用AIDL。

2、如果需要调用远程方法,但不需要处理并发IPC,就应该通过实现一个Binder创建接口。(AIDL实现)

3、如果您想执行IPC,但只是传递数据,不涉及方法调用,也不需要高并发,就使用Messenger来实现接口。

4、如果需要处理一对多的进程间数据共享(主要是数据的CRUD),就使用ContentProvider。

5、如果要实现一对多的并发实时通信,就使用Socket。

Messenger与AIDL的区别:

1、底层都是通过Binder实现通信。

2、AIDL支持RTC,Messenger不支持RTC。

3、Messenger是以串行的方式处理客户端发来的消息,如果大量消息同时发送到服务端,服务端只能一个一个处理,所以大量并发请求就不适合用Messenger,而且Messenger只适合传递消息,不能跨进程调用服务端的方法,AIDL可以解决并发和跨进程调用方法的问题,要知道Messenger本质上也是AIDL,只不过系统做了封装方便上层的调用而已。

Messenger实现进程间通信:

介绍:Messenger是一种轻量级的IPC方案,它的底层实现是AIDL,可以在不同进程中传递Message对象,它一次只处理一个请求,在服务端不需要考虑线程同步的问题,服务端不存在并发执行的情形。

实现进程间通信流程:

1、在AndroidManifest.xml中设置Service的Android.process属性,把组件设置为多进程。(私域进程与全局进程的设置)

2、全局Application级别绑定服务Service,实现主要接口:ServiceConnection,实现连接远程服务函数。

3、创建Messenger对象,通过IBinder作为参数创建Messenger对象,再通过创建的Messenger对象发送消息到另外一个进程。

4、创建多进程服务Service,通过Handler作为参数创建Messenger对象,在使用创建的Messenger对象调用getBinder()函数到Service的onBinder重载函数中。

5、创建多进程后,Application的onCreate()函数会重新触发,这时需要通过进程的名字,决定重新初始化什么信息。

注意:客户端和服务端是通过拿到对方的Messenger来发送Message的,只不过客户端通过bindService onServiceConnected而服务端通过message.replyTo来获得对方的Messenger,Messenger中有一个Hanlder以串行的方式处理队列中的消息,不存在并发执行,因此我们不用考虑线程同步的问题。

AIDL实现进程间通信流程:

1、在AIDL文件夹中定义进程间通信接口文件,build一下,生成通信接口类(回调接口、序列化对象、返回对象、回调接口注册、Stub是根据我们MessageSender.aidl文件自动生成的Binder对象)

2、应用全局绑定服务(bindService),实现ServiceConnection接口,创建通信接口对象,并初始化对象。

3、通过通信接口和注册回调接口,实现进程间的相互通信。(在AIDL中传递的接口,不能是普通的接口,只能是AIDL接口,所以我们需要新建一个AIDL接口传到服务端,作为回调接口)

4、多进程远程服务,创建AIDL生成的接口对象,并实现onBinder()函数。

5、这里的远程服务需要通过注册回调函数实现向客户端的通信(观察者模式)

图例:

Android多进程_第6张图片

备注:Android中实现多进程通讯,建议使用系统提供的Binder类,该类已经实现了多进程通讯而不需要我们做底层工作。

.aidl文件生成进程间通信接口解析:

Android多进程_第7张图片

解析:把IBinder对象转换为com.example.aidl.MessageSender接口,判断IBinder是否处于相同进程,相同进程返回Stud实现com.example.aidl.MessageSender接口,不同进程,则返回Stud.Proxy实现的com.example.aidl.MessageSender接口。

结合流程图理解:

Android多进程_第8张图片

解析:从客户端的sendMessage开始,整个AIDL的调用过程如图所示,asInterface方法,将会判断onBind方法返回的Binder是否存处于同一进程,在同一进程中,则进行常规的方法调用,若处于不同进程,整个数据传递的过程则需要通过Binder底层去进行编组(序列化,传输,接收和反序列化),得到最终的数据后再进行常规的方法调用。    

    对象跨进程传输的本质就是序列化,传输,接收和反序列化这样一个过程,这也是为什么跨进程传输的对象必须实现Parcelable接口。

注意事项:

一:AIDL支持的数据类型:

1.1、Java编程语言中的所有基本数据类型(如int、long、char、boolean等等)

1.2、String和CharSequence

1.3、Parcelable:实现了Parcelable接口的对象

1.4、List:其中的元素需要被AIDL支持,另一端实际接收的具体类始终是ArrayList,但生成的方法使用的是List接口。

1.5、Map:其中的元素需要被AIDL支持,包括key和value,另一端实际接收的具体类始终是HashMap,但生成的方法使用的是Map接口。

二:在AIDL中传递的对象,必须实现Parcelable序列化接口。

三:在AIDL中传递的对象,需要在类文件相同路径下,创建同名、但是后缀为.aidl的文件,并在文件中使用parcelable关键字声明这个类。

四:跟普通接口的区别,只能声明方法,不能声明变量。

五:所有非基础数据类型参数都需要标出数据走向的方向标记,可以是in、out或inout,基础数据类型默认只能是in,不能是其他方向。

六:RemoteCallbackList专门用来管理多进程的远程回调接口,实现代码如下。

RemoteCallbackList listenerList = new RemoteCallbackList<>();

public void registerReceiveListener(MessageReceiver messageReceiver){

     listenerList.register(messageReceiver);

}

最后看一下AIDL项目的结构:

Android多进程_第9张图片

项目地址:https://github.com/agxxxx/AIDLMusicPlayer

五:多进程实现中遇到的问题汇总

问题一:多进程应用如何调试?

辅助进程的onCreate方法进行调试时,添加Debug.waitForDebugger(),Debug.waitForDebugger方法会让手机进程保持阻塞状态,直到连上调试器后,才会继续执行后续的代码。

问题二:多进程间的通信,数据传输的最大值是多少?(Binder传递数据大小限制)

解析:数据以Parcel对象的形式存放在Binder传递缓存中,如果数据或返回值比传递buffer大,则此次传递调用失败并抛出TransactionTooLargeException异常,Binder传递缓存有一个限定大小,通常是1Mb。但同一个进程中所有的传输共享缓存空间,多个地方在进行传输时,即使它们各自传输的数据不超出大小限制,TransactionTooLargeException异常也可能会被抛出,在使用Intent传递数据时,1Mb并不是安全上限。因为Binder中可能正在处理其它的传输工作,不同的机型和系统版本,这个上限值也可能会不同。

问题三:多进程导致的内存泄漏问题与解决方案?

解析:AIDL多进程中,如何防止内存泄漏的出现,并在合适的时候解除回调绑定接口。

实现一:

@Override

protected void onDestroy() {

unbindService(mServiceConnection);

super.onDestroy();

}

实现二:

if (mIMyAidlInterface != null && mIMyAidlInterface.asBinder().isBinderAlive()) {

    try {

        mIMyAidlInterface.getWorker(new Worker("worker_1", 0, "get"));

    } catch (Exception e) {

        Log.e(TAG, "Exception");

        //

        e.printStackTrace();

    }

}

问题四:启动多进程时,导致多进程启动失败的原因?

解析:Application的onCreate()会再次执行,如果Application的onCreate()发生初始化异常,导致多进程启动失败,为了避免上面的情况,也为了避免不必要的初始化行为,可以根据不同进程,做不同的初始化工作。

Android多进程_第10张图片

问题五:如何实现多进程操作安全?

解析:可以把所有的多进程操作的行为放在一个进程中操作,如果其他进程需要相关的操作,可以发送一个消息给到相关的操作进程,相关的操作进程再做统一的操作行为,例如添加、删除、修改、查询等。

文件共享问题:多进程情况下会出现两个进程在同一时刻访问同一个数据库文件的情况。这就可能造成资源的竞争访问,导致诸如数据库损坏、数据丢失等。在多线程的情况下我们有锁机制控制资源的共享。

     但是在多进程中比较难,虽然有文件锁、排队等机制,但是在Android里很难实现。解决办法就是多进程的时候不并发访问同一个文件,比如子进程涉及到操作数据库,就可以考虑调用主进程进行数据库的操作。

问题六:使用bindService与startService启动多进程服务的区别?

解析:使用bindService方式,多个Client可以同时bind一个Service,但是当所有Client unbind后,Service会退出,通常情况下,如果希望和Service交互,一般使用bindService方法,使用onServiceConnected中的IBinder对象可以和Service进行交互,不需要和Service交互的情况下,使用startService方法即可。

   我们是要和Service交互的,所以我们需要使用bindService方法,但是我们希望unbind后Service仍保持运行,这样的情况下,可以同时调用bindService和startService。

示例代码:

Android多进程_第11张图片

问题七:什么是Binder?

解析:IBinder是远程对象的基础接口,轻量级的远程过程调用机制的核心部分,该接口描述了与远程对象交互的抽象协议,而Binder实现了IBinder接口,简单说,Binder就是Android SDK中内置的一个多进程通讯实现类,在使用的时候,我们不用也不要去实现IBinder,而是继承Binder这个类即可实现多进程通讯。

问题八:in、out、inout这三个参数方向的意义?

解析:被“in”标记的参数,就是接收实际数据的参数,这个跟我们普通参数传递一样的含义。在AIDL中,“out” 指定了一个仅用于输出的参数,换而言之,这个参数不关心调用方传递了什么数据过来,但是这个参数的值可以在方法被调用后填充(无论调用方传递了什么值过来,在方法执行的时候,这个参数的初始值总是空的),这就是“out”的含义,仅用于输出。而“inout”显然就是“in”和“out”的合体了,输入和输出的参数。区分“in”、“out”有什么用?这是非常重要的,因为每个参数的内容必须编组(序列化,传输,接收和反序列化)。in/out标签允许Binder跳过编组步骤以获得更好的性能。

Android多进程_第12张图片

问题九:为什么要用RemoteCallbackList

解析:registerReceiveListener和unregisterReceiveListener在客户端传输过来的对象,经过Binder处理,在服务端接收到的时候其实是一个新的对象,这样导致在unregisterReceiveListener的时候,普通的ArrayList是无法找到在registerReceiveListener时候添加到List的那个对象的,但是它们底层使用的Binder对象是同一个,RemoteCallbackList利用这个特性做到了可以找到同一个对象,这样我们就可以顺利反注册客户端传递过来的接口对象了。RemoteCallbackList在客户端进程终止后,它能自动移除客户端所注册的listener,它内部还实现了线程同步,所以我们在注册和反注册都不需要考虑线程同步。

问题十:如何知道远程服务是否挂掉?

解析:服务端进程Crash了,而客户端进程想要调用服务端方法,这样就调用不到了。此时我们可以给Binder设置一个DeathRecipient对象,当Binder意外挂了的时候,我们可以在DeathRecipient接口的回调方法中收到通知,并作出相应的操作,比如重连服务等等。

方法一:使用DeathRecipient

1、声明DeathRecipient对象,实现其binderDied方法,当binder死亡时,会回调binderDied方法。

2、给Binder对象设置DeathRecipient对象。

      在客户端MainActivity声明DeathRecipient,Binder中两个重要方法:

1、linkToDeath ->设置死亡代理DeathRecipient对象。

2、unlinkToDeath -> Binder死亡的情况下,解除该代理。

代码示例:

Android多进程_第13张图片

Android多进程_第14张图片

方法二:Binder中的isBinderAlive也可以判断Binder是否死亡。

问题十一:远程服务如何做权限验证?

方法一:在服务端的onBind中校验自定义permission,如果通过了我们的校验,正常返回Binder对象,校验不通过返回null,返回null的情况下客户端无法绑定到我们的服务,自定义permission,在Androidmanifest.xml中增加自定义的权限:

Android多进程_第15张图片

Android多进程_第16张图片

方法二:在服务端的onTransact方法校验客户端包名,不通过校验直接return false,校验通过执行正常的流程,服务端检查权限的方法。

Android多进程_第17张图片

问题十二:为什么AIDL实现的进程间通信会导致ANR?

解析:绑定服务端的Service,绑定成功后,将服务端返回的Binder对象转成AIDL接口所属的类型,然后就可以调用AIDL中的方法了,客户端调用远程服务的方法,被调用的方法运行在服务端的Binder线程池中,同时客户端的线程会被挂起,如果服务端方法执行比较耗时,就会导致客户端线程长时间阻塞,导致ANR,客户端的onServiceConnected和onServiceDisconnected方法都在UI线程中。

你可能感兴趣的:(android)