写点什么

androidjetpack 视频,扔物线五期

用户头像
极客开源
关注
发布于: 刚刚

为什么要使用多进程


对于进程的概念,来到这里的都是编程修仙之人,就不再啰嗦了,相信大家倒着、跳着、躺着、各种姿势都能背出来。


相信很多同学在实际开发中,基本都不会去给 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 或者出现内存消耗过高等情况,仍不影响消息推送服务。


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


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


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


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

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

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


跨进程的通讯方式有哪些


  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 的讲解,各位道友准备好渡劫了吗?


使用 AIDL 使用一个跨进程消息推送


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


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

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

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

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


实现思路


先来整理一下实现思路:


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

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

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

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

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


例子具体实现


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






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


正如上面所说,我们是要和 Service 交互的,所以我们需要使用 bindService 方法,但是我们希望 unbind 后 Service 仍保持运行,这样的情况下,可以同时调用 bindService 和 startService(比如像本例子中的消息服务,退出 UI 进程,Service 仍需要接收到消息),代码如下:



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,既然已经学会使用了,接下来…全剧终。。。



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

知其然,知其所以然


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


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


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

  2. messageSender.sendMessage(messageModel);


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


请看下方代码和注释,前方高能预警…






只看代码的话,可能会有点懵逼,相信结合代码再看下方的流程图会更好理解:



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


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


跨进程的回调接口


在上面我们已经实现了从客户端发送消息到跨进程服务端的功能,接下来我们还需要将服务端接收到的远程服务器消息,传递到客户端。有同学估计会说:“这不就是一个回调接口的事情嘛”,设置回调接口思路是对的,但是在这里使用的回调接口有点不一样,在 AIDL 中传递的接口,不能是普通的接口,只能是 AIDL 接口,所以我们需要新建一个 AIDL 接口传到服务端,作为回调接口。


新建消息收取的 AIDL 接口 MessageReceiver.aidl:



接下来我们把回调接口注册到服务端去,修改我们的 MessageSender.aidl:



以上就是我们最终修改好的 aidl 接口,接下来我们需要做出对应的变更:


  1. 在服务端中增加 MessageSender 的注册和反注册接口的方法;

  2. 在客户端中实现 MessageReceiver 接口,并通过 MessageSender 注册到服务端。


客户端变更,修改 MainActivity:




客户端主要有 3 个变更:


  1. 增加了 messageReceiver 对象,用于监听服务端的消息通知;

  2. onServiceConnected 方法中,把 messageReceiver 注册到 Service 中去;

  3. onDestroy 时候解除 messageReceiver 的注册。


下面对服务端 MessageServie 进行变更:





服务端主要变更:


  1. MessageSender.Stub 实现了注册和反注册回调接口的方法;

  2. 增加了 RemoteCallbackList 来管理 AIDL 远程接口;

  3. FakeTCPTask 模拟了长连接通知客户端有新消息到达。(这里的长连接可以是 XMPP,Mina,Mars,Netty 等,这里弄个假的意思意思,有时间的话咱开个帖子聊聊 XMPP)


  • 这里还有一个需要讲一下的,就是 RemoteCallbackList,为什么要用 RemoteCallbackList,普通 ArrayList 不行吗?当然不行,不然干嘛又整一个 RemoteCallbackList ,registerReceiveListener 和 unregisterReceiveListener 在客户端传输过来的对象,经过 Binder 处理,在服务端接收到的时候其实是一个新的对象,这样导致在 unregisterReceiveListener 的时候,普通的 ArrayList 是无法找到在 registerReceiveListener 时候添加到 List 的那个对象的,但是它们底层使用的 Binder 对象是同一个,RemoteCallbackList 利用这个特性做到了可以找到同一个对象,这样我们就可以顺利反注册客户端传递过来的接口对象了。

  • RemoteCallbackList 在客户端进程终止后,它能自动移除客户端所注册的 listener,它内部还实现了线程同步,所以我们在注册和反注册都不需要考虑线程同步,的确是个 666 的类。(至于使用 ArrayList 的幺蛾子现象,大家可以自己试试,篇幅问题,这里就不演示了)


到此,服务端通知客户端相关的代码也写完了,运行结果无非就是正确打印就不贴图了,可以自己 Run 一下,打印的时候注意去选择不同的进程,不然瞪坏屏幕也没有日志。


DeathRecipient


你以为这样就完了?too young too simple…


不知道你有没有感觉到,两个进程交互总是觉得缺乏那么一点安全感…比如说服务端进程 Crash 了,而客户端进程想要调用服务端方法,这样就调用不到了。此时我们可以给 Binder 设置一个 DeathRecipient 对象,当 Binder 意外挂了的时候,我们可以在 DeathRecipient 接口的回调方法中收到通知,并作出相应的操作,比如重连服务等等。


DeathRecipient 的使用如下:


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

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


在客户端 MainActivity 声明 DeathRecipient:



Binder 中两个重要方法:


  1. linkToDeath -> 设置死亡代理 DeathRecipient 对象;

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


此外,Binder 中的 isBinderAlive 也可以判断 Binder 是否死亡


权限验证


就算是公交车,上车也得嘀卡对不,如果希望我们的服务进程不想像公交车一样谁想上就上,那么我们可以加入权限验证。


介绍两种常用验证方法:


  1. 在服务端的 onBind 中校验自定义 permission,如果通过了我们的校验,正常返回 Binder 对象,校验不通过返回 null,返回 null 的情况下客户端无法绑定到我们的服务;

  2. 在服务端的 onTransact 方法校验客户端包名,不通过校验直接 return false,校验通过执行正常的流程。


自定义 permission,在 Androidmanifest.xml 中增加自定义的权限:



服务端检查权限的方法:



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


相信前一两年很多朋友还在使用 Android-Universal-Image-Loader 来加载图片,它是需要在 Application 类进行初始化的。打个比如,我们用它来加载图片,而且还有一个图片选择进程,那么我们希望分配更多的缓存给图片选择进程,又或者是一些其他的初始化工作,不需要在图片选择进程初始化怎么办?


这里提供一个简单粗暴的方法,博主也是这么干的…直接拿到进程名判断,作出相应操作即可:



每个进程创建,都会调用 Application 的 onCreate 方法,这是一个需要注意的地方,我们也可以根据当前进程的 pid,拿到当前进程的名字去做判断,然后做一些我们需要的逻辑,我们这个例子,拿到的两个进程名分别是:


  1. 客户端进程:com.example.aidl

  2. 服务端进程:com.example.aidl:remote


总结


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

  2. 实现多进程的方式有多种:四大组件传递 Bundle、Messenger、AIDL 等,选择适合自己的使用场景;

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

  4. 多进程应用,Application 将会被创建多次;

学习福利

【Android 详细知识点思维脑图(技能树)】



其实 Android 开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。


虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。


这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司 19 年的面试题。把技术点整理成了视频和 PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。


由于篇幅有限,这里以图片的形式给大家展示一小部分。



网上学习 Android 的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。


本文已被CODING开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》收录

用户头像

极客开源

关注

还未添加个人签名 2021.03.18 加入

还未添加个人简介

评论

发布
暂无评论
androidjetpack视频,扔物线五期