如何巧用Android多进程,你不知道的点全总结!微信,微博等主流App都在用



Java和Android架构

欢迎关注我们,一起讨论技术,扫描和长按下方的二维码可快速关注我们。搜索微信公众号:JANiubility。

公众号:JANiubility






目录

  1. 前言

  2. 为什么要使用多进程?

  3. 为什么需要“跨进程通讯”?

  4. 跨进程通讯的方式有哪些?

  5. 使用AIDL实现一个多进程消息推送

  6. 实现思路

  7. 例子具体实现

  8. 知其然,知其所以然。

  9. 跨进程的回调接口

  10. DeathRecipient

  11. 权限验证

  12. 根据不同进程,做不同的初始化工作

  13. 总结

  14. 结语


1

前言

对于进程的概念,就不再啰嗦了,相信大家都能背出来。


2

为什么要使用多进程?

相信很多同学在实际开发中,基本都不会去给app划分进程,而且,在Android中使用多进程,还可能需要编写额外的进程通讯代码,还可能带来额外的Bug,这无疑加大了开发的工作量,在很多创业公司中工期也不允许,这导致了整个app都在一个进程中。


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


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


微信,微博等主流app是如何解决这些问题的?


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


下面我们使用adb查看一下微信和微博的进程信息(Android 5.0以下版本可直接在“设置 -> 应用程序”相关条目中查看):

进入adb shell后,使用 “ps | grep 条目名称” 可以过滤出想要查看的进程。


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


可见,合理使用多进程不仅仅是有多大好处的问题,我个人认为而且是很有必要的。


所以说,我们最好还是根据自身情况,考虑一下是否需要拆分进程。这也是本文的初衷:给大家提供一个多进程的参考思路,在遇到上述问题和场景的时候,可以考虑用多进程的方法来解决问题,又或者,在面试的时候,跟面试官聊到这方面的知识时候也不至于尴尬。


3

为什么需要“跨进程通讯”?

Android的进程与进程之间通讯,有些不需要我们额外编写通讯代码,例如:把选择图片模块放到独立的进程,我们仍可以使用startActivityForResult方法,将选中的图片放到Bundle中,使用Intent传递即可。(看到这里,你还不打算把你项目的图片选择弄到独立进程么?)


但是对于把“消息推送Service”放到独立的进程,这个业务就稍微复杂点了,这个时候可能会发生Activity跟Service传递对象,调用Service方法等一系列复杂操作。


由于各个进程运行在相对独立的内存空间,所以它们是不能直接通讯的,因为程序里的变量、对象等初始化后都是具有内存地址的,举个简单的例子,读取一个变量的值,本质是找到变量的内存地址,取出存放的值。不同的进程,运行在相互独立的内存(其实就可以理解为两个不同的应用程序),显然不能直接得知对方变量、对象的内存地址,这样的话也自然不能访问对方的变量,对象等。此时两个进程进行交互,就需要使用跨进程通讯的方式去实现。简单说,跨进程通讯就是一种让进程与进程之间可以进行交互的技术。


4

跨进程通讯的方式有哪些?

  1. 四大组件间传递Bundle;


  2. 使用文件共享方式,多进程读写一个相同的文件,获取文件内容进行交互;


  3. 使用Messenger,一种轻量级的跨进程通讯方案,底层使用AIDL实现(实现比较简单,博主开始本文前也想了一下是否要说一下这个东西,最后还是觉得没有这个必要,Google一下就能解决的问题,就不啰嗦了);


  4. 使用AIDL(Android Interface Definition Language),Android接口定义语言,用于定义跨进程通讯的接口;


  5. 使用ContentProvider,常用于多进程共享数据,比如系统的相册,音乐等,我们也可以通过ContentProvider访问到;


  6. 使用Socket传输数据。


接下来本文将重点介绍使用AIDL进行多进程通讯,因为AIDL是Android提供给我们的标准跨进程通讯API,非常灵活且强大(貌似面试也经常会问到,但是真正用到的也不多…)。上面所说的Messenger也是使用AIDL实现的一种跨进程方式,Messenger顾名思义,就像是一种串行的消息机制,它是一种轻量级的IPC方案,可以在不同进程中传递Message对象,我们在Message中放入需要传递的数据即可轻松实现进程间通讯。但是当我们需要调用服务端方法,或者存在并发请求,那么Messenger就不合适了。而四大组件传递Bundle,这个就不需要解释了,把需要传递的数据,用Intent封装起来传递即可,其它方式不在本文的讨论范围。


下面开始对AIDL的讲解,各位道友准备好渡劫了吗?


5

使用AIDL实现一个多进程消息推送

像图片选择这样的多进程需求,可能并不需要我们额外编写进程通讯的代码,使用四大组件传输Bundle就行了,但是像推送服务这种需求,进程与进程之间需要高度的交互,此时就绕不过进程通讯这一步了。
下面我们就用即时聊天软件为例,手动去实现一个多进程的推送例子,具体需求如下:


  1. UI和消息推送的Service分两个进程;


  2. UI进程用于展示具体的消息数据,把用户发送的消息,传递到消息Service,然后发送到远程服务器;


  3. Service负责收发消息,并和远程服务器保持长连接,UI进程可通过Service发送消息到远程服务器,Service收到远程服务器消息通知UI进程;


  4. 即使UI进程退出了,Service仍需要保持运行,收取服务器消息。


6

实现思路

先来整理一下实现思路:

  1. 创建UI进程(下文统称为客户端);


  2. 创建消息Service(下文统称为服务端);


  3. 把服务端配置到独立的进程(AndroidManifest.xml中指定process标签);


  4. 客户端和服务端进行绑定(bindService);


  5. 让客户端和服务端具备交互的能力。(AIDL使用);


7

例子具体实现

为了阅读方便,下文中代码将省略非重点部分,可以把本文完整代码Clone到本地再看文章:

https://github.com/V1sk/AIDL


Step0. AIDL调用流程概览


开始之前,我们先来概括一下使用AIDL进行多进程调用的整个流程:


  1. 客户端使用bindService方法绑定服务端;


  2. 服务端在onBind方法返回Binder对象;


  3. 客户端拿到服务端返回的Binder对象进行跨进程方法调用;

整个AIDL调用过程概括起来就以上3个步骤,下文中我们使用上面描述的例子,来逐步分解这些步骤,并讲述其中的细节。


Step1.客户端使用bindService方法绑定服务端


1.1 创建客户端和服务端,把服务端配置到另外的进程


  1. 创建客户端 -> MainActivity;


  2. 创建服务端 -> MessageService;


  3. 把服务端配置到另外的进程 -> android:process=”:remote”


上面描述的客户端、服务端、以及把服务端配置到另外进程,体现在AndroidManifest.xml中,如下所示:

开启多进程的方法很简单,只需要给四大组件指定android:process标签。


1.2 绑定MessageService到MainActivity


创建MessageService:

此时的MessageService就是刚创建的模样,onBind中返回了null,下一步中我们将返回一个可操作的对象给客户端。

客户端MainActivity调用bindService方法绑定MessageService


这一步其实是属于Service组件相关的知识,在这里就比较简单地说一下,启动服务可以通过以下两种方式:


  1. 使用bindService方法 -> bindService(Intent service, ServiceConnection conn, int flags);


  2. 使用startService方法 -> startService(Intent service);


bindService & startService区别:


Stpe2.服务端在onBind方法返回Binder对象


2.1 首先,什么是Binder?


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


2.2 其次,这个需要在onBind方法返回的Binder对象从何而来?


在这里就要引出本文中的主题了——AIDL
多进程中使用的Binder对象,一般通过我们定义好的 .adil 接口文件自动生成,当然你可以走野路子,直接手动编写这个跨进程通讯所需的Binder类,其本质无非就是一个继承了Binder的类,鉴于野路子走起来麻烦,而且都是重复步骤的工作,Google提供了 AIDL 接口来帮我们自动生成Binder这条正路,下文中我们围绕 AIDL 这条正路继续展开讨论(可不能把人给带偏了是吧)


2.3 定义AIDL接口


很明显,接下来我们需要搞一波上面说的Binder,让客户端可以调用到服务端的方法,而这个Binder又是通过AIDL接口自动生成,那我们就先从AIDL搞起,搞之前先看看注意事项,以免出事故:


AIDL支持的数据类型:


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


  • String和CharSequence


  • Parcelable:实现了Parcelable接口的对象


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


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


其他注意事项:


  • 在AIDL中传递的对象,必须实现Parcelable序列化接口;


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


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


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


下面继续我们的例子,开始对AIDL的讲解~


2.4 创建一个AIDL接口,接口中提供发送消息的方法(Android Studio创建AIDL:项目右键 -> New -> AIDL -> AIDL File),代码如下:

一个比较尴尬的事情,看了很多文章,从来没有一篇能说清楚in、out、inout这三个参数方向的意义,后来在stackoverflow上找到能理解答案(https://stackoverflow.com/questions/4700225/in-out-inout-in-a-aidl-interface-parameter-value),我翻译一下大概意思:


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


上述的MessageModel为消息的实体类,该类在AIDL中传递,实现了Parcelable序列化接口,代码如下:

手动实现Parcelable接口比较麻烦,安利一款AS自动生成插件android-parcelable-intellij-plugin
创建完MessageModel这个实体类,别忘了还有一件事要做:”在AIDL中传递的对象,需要在类文件相同路径下,创建同名、但是后缀为.aidl的文件,并在文件中使用parcelable关键字声明这个类“。代码如下:

对于没有接触过aidl的同学,光说就能让人懵逼,来看看此时的项目结构压压惊:

我们刚刚新增的3个文件:

  • MessageSender.aidl -> 定义了发送消息的方法,会自动生成名为MessageSender.Stub的Binder类,在服务端实现,返回给客户端调用


  • MessageModel.java -> 消息实体类,由客户端传递到服务端,实现了Parcelable序列化


  • MessageModel.aidl -> 声明了MessageModel可在AIDL中传递,放在跟MessageModel.java相同的包路径下


OK,相信此时懵逼已解除~


2.5 在服务端创建MessageSender.aidl这个AIDL接口自动生成的Binder对象,并返回给客户端调用,服务端MessageService代码如下:

MessageSender.Stub是Android Studio根据我们MessageSender.aidl文件自动生成的Binder对象(至于是怎样生成的,下文会有答案),我们需要把这个Binder对象返回给客户端。


2.6 客户端拿到Binder对象后调用远程方法


调用步骤如下:


  1. 在客户端的onServiceConnected方法中,拿到服务端返回的Binder对象;


  2. 使用MessageSender.Stub.asInterface方法,取得MessageSender.aidl对应的操作接口;


  3. 取得MessageSender对象后,像普通接口一样调用方法即可。


此时客户端代码如下:

在客户端中我们调用了MessageSender的sendMessage方法,向服务端发送了一条消息,并把生成的MessageModel对象作为参数传递到了服务端,最终服务端打印的结果如下:

这里有两点要说:


  1. 服务端已经接收到客户端发送过来的消息,并正确打印;


  2. 服务端和客户端区分两个进程,PID不一样,进程名也不一样;


到这里,我们已经完成了最基本的使用AIDL进行跨进程方法调用,也是Step.0的整个细化过程,可以再回顾一下Step.0,既然已经学会使用了,接下来…全剧终。。。

如果写到这里全剧终,那跟咸鱼有什么区别…


8

知其然,知其所以然。

我们通过上述的调用流程,看看从客户端到服务端,都经历了些什么事,看看Binder的上层是如何工作的,至于Binder的底层,这是一个非常复杂的话题,本文不深究。(如果看到这里你又想问什么是Binder的话,请手动倒带往上看…)


我们先来回顾一下从客户端发起的调用流程:


  1. MessageSender messageSender = MessageSender.Stub.asInterface(service);


  2. messageSender.sendMessage(messageModel);


抛开其它无关代码,客户端调跨进程方法就这两个步骤,而这两个步骤都封装在 MessageSender.aidl 最终生成的 MessageSender.java 源码(具体路径为:build目录下某个子目录,自己找,不爽你来打我啊

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值