概述

想当初在第一次拜读《Android 艺术开发探索》时,深感真的是一本很“艺术”的书(因为当初菜的看不懂..),随着自己的成长和多次阅读,从开始的完全不懂到现在的有所理解、使用和总结,才体会到其中探索的奥妙,现在跟着安卓高级开发的学习路线,进一步学习、总结和梳理知识。

多进程作为 Android 开发者迈向高级开发者的第一关,也使许多初级开发者望而却步,这也是每个开发者必经阶段,正好笔者在公司的开发项目中也一直使用了多进程,之前只是在使用阶段、和平时零散的知识点,而对 Binder 的原理和理解并不深入,本文结合最近所看的文章和实际使用,从开发者的角度总结多进程和 Binder 的使用,即为自己梳理知识也希望帮助有需要的人。

在这里插入图片描述

  • 定义 提到多进程就会想到多线程,这也是很多初级的面试问题,二者对比着可能更好理解:
  1. 线程:线程是 CPU 最小的调度单元,是有限的系统资源,也是处理任务的地方
  2. 进程:是一个执行单元,一般指设备上的一个程序或一个应用
  3. 理解:进程和线程是包含和被包含的关系;一个进程可以包含多个线程
  • 开启方式 Android 开启多进程只有一个方式:注册清单文件中,在 Android 四大组件中指定 process 属性,命名方式如下:
  1. 以“:”命名方式:最终的进程名为在当前的命名前面添加默认的包名
  2. 完整命名方式:最终的进程名就为设定的名称
android:process=":consume"
android:process="com.alex.kotlin.myapplication.consume"
复制代码
  • 多进程问题 因为进程开启时 Application 都会重新创建,所以很多数据和对象都会产生副本,因此在多进程模式下数据共享就会变得不稳定,多进程模式下会造车如下的问题:
  1. 静态成员和单例模式完全失效
  2. 线程同步机制完全失效
  3. SharePreference 可靠性下降
  4. Application 会多次创建

进程间通信

关于进程间的通信首先想到的是 Binder 机制,当然开发中如果使用多进程,那 Binder 自当是首当其冲要了解和学习的,下文也会重点介绍 Binder,在此之前来看看我们实际开发中使用的、或者可以跨进程通信的机制

  • 序列化
  1. Serializable Serializable 序列的使用很简单,只需要实现在 Java 类中实现 Serializable 接口,设置 serialVersionUID 即可
public class Book implements Serializable {
    private static final long serialVersionUID = 871136882801008L;
    String name;
    int age;
public Book(String name, int age) {
    this.name = name;
    this.age = age;
}

}

复制代码

在储存数据时只需将对象序列化在磁盘中,在需要使用的地方反序列化即可获取 Java 实例,使用过程如下:

//序列化
val book = Book("Android",20)
        val file = File(cacheDir,"f.txt")
        val out = ObjectOutputStream(FileOutputStream(file))
        out.writeObject(book)
        out.close()
//反序列化
val file = File(cacheDir,"f.txt")
        val input = ObjectInputStream(FileInputStream(file))
        val book: Book = input.readObject() as Book
        input.close()
复制代码

针对上面的 serialVersionUID 可能有的认为不设置也可以使用,但如果不设置 serialVersionUID 值,Java 对象同样可以序列化,但是当 Java 类改变时,这时如果去反序列化的化就会报错,因为 serialVersionUID 是辅助序列化和反序列化的,只有两者的 serialVersionUID 一致才可实现反序列化,所以你不指定 serialVersionUID 时,系统会默认使用当前类的 Hash 值,当 java 对象改变时其 Hash 值也改变了,所以反序列化时就找不到对应的 Java 类了。

  1. Parcelable Parcelable 也是一个接口,他是 Android 提供的在内存中更高效的序列化方式,使用方法是实现接口,重写其中方法即可,当然也可使用插件自动生成。

对于 Parcelable 和 Serializable 的选择使用:Serializable 是 Java 的序列化接口,使用时开销大,需要大量的 IO 操作,Parcelable 是 Android 提供的序列化接口,适合 Android 效率更高,对于两者的选择,如果只是在内存上序列化使用 Parcelable,如果需要在磁盘上序列化使用 Serializable 即可。

Binder

在网上看了需对关于 Binder 的文章,有的深入 Binder 源码和底层去分析 Binder 的源码和实现,当然这里面的代码我是看不懂,本文主要从 Android 开发的角度,对 Binder 的通信的模型和方式做一个介绍,

  • Binder 模型 Binder 框架定义了四个角色:Server,Client,ServiceManager(简称 SMgr)以及 Binder 驱动。其中 Server,Client,SMgr 运行于用户空间,驱动运行于内核空间
  1. Server:服务的真正提供者,不过它会先向 ServiceManager 注册自己 Binder 表明自己可以提供服务,驱动会为这个 BInder 创建位于内核中的实体和 ServiceManager 中的引用,并将名字以及新建的引用打包传给 ServiceManager,ServiceManger 将其填入查找表
  2. Client:服务的需求者和使用者,它向 ServiceManager 申请需要的服务;ServiceManager 将表中的引用返回 Client,Client 拿到服务后即可调用服务中的方法;
  3. ServiceManager:Binder 实体和引用的中转站,保存并分发 Binder 的引用;
  4. Binder 驱动:Binder 驱动默默无闻付出,却是通信的核心,驱动负责进程之间 Binder 通信的建立,Binder 在进程之间的传递,Binder 引用计数管理,数据包在进程之间的传递和交互等一系列底层支持,借用网上的一张图片展示 Binder 的通信模型;

在这里插入图片描述 如果上面的四个功能难以理解,我们以打电话为例,将整个电话系统的程序比做 Binder 驱动,通讯录比作 ServiceManager,你本人为 Client,现在你要打电话给叫 Server 人求助:

  1. Server:Server 表示你要打电话找的人,它会首先给你留一个手机号,你为了可以找到他,将号码保存到通讯录中,通讯录相当于 ServiceManager(Server 向 ServiceManager 注册服务)
  2. client:相当于你本人,发起打电话请求
  3. ServiceManager:通讯录保存电话号码,你需要的时候首先向通讯录去查找号码,它会返回 Server 手机号
  4. Binder 驱动:打电话的系统,根据你输入的号码呼叫对应的人 对于 Binder 的通信模型如上述所述,简单的说就是 Server 先注册并登记表示可以提供服务功能,当有需求时向登记处查找可以提供服务的 Service,登记处会给你详细的地址,然后你就可以和服务商之间合作,只是整个过程在 Binder 驱动作用下完成;
  • Binder 代理机制 通过上面的 Binder 通信机制的理解,相信已经了解 Binder 是如何跨进程通信的,可是具体的数据和对象都存在不同的进程中,那么进程间是如何相互获取的呢?比如 A 进程要获取 B 进程中的对象,它是如何实现的呢?此时就需要 Binder 的代理机制;

当 Binder 收到 A 进程的请求后,Binder 驱动并不会真的把 object 返回给 A,而是返回了一个跟 object 看起来一模一样的代理对象 objectProxy,这个 objectProxy 具有和 object 一摸一样的方法,但是这些方法并没有 B 进程中 object 对象那些方法的能力,这些方法只需要把把请求参数交给驱动即可;

而对于进程 A 却傻傻不知道它以为拿到了 B 进程中 object 对象,所以直接调用了 Object 的方法,当 Binder 驱动接收到 A 进程的消息后,发现这是个 objectProxy 就去查询自己维护的表单,一查发现这是 B 进程 object 的代理对象。于是就会去通知 B 进程调用 object 的方法,并要求 B 进程把返回结果发给自己。当驱动拿到 B 进程的返回结果后就会转发给 A 进程,一次通信就完成了,所以中间的代理就只是一个面具和传输的媒介。

在这里插入图片描述

  • Binder 使用 Messenger 一种轻量级的 IPC 方案,它的底层实现是 AIDL,Messenger 通过对 AIDL 的封装是我们可以更简单的使用进程通信,它的构造函数如下:
public Messenger(Handler target) {
        mTarget = target.getIMessenger();
    }

public Messenger(IBinder target) {
mTarget = IMessenger.Stub.asInterface(target);
}

复制代码

实现一个 Messenger 分为两步,即服务端和客户端的实现

  1. 服务端 首先创建一个 Service 来连接客户端的请求,在 Service 中创建 Handler 实例,并使用此 Handler 的实例创建一个 Messenger 实例,并在 Service 的 onBind() 中返回 Messenger 实例。
//创建Handler
class HandlerService : Handler() {
    override fun handleMessage(msg: Message?) {
        when (msg?.what) {
            MSG_WHAT -> {
                Log.e("MyService", "MyServer")
            }
            else -> super.handleMessage(msg)
        }
    }
}
//使用Handler实例创建Messenger实例
private val messenger = Messenger(HandlerService())

// 服务通过 onBind() 使其返回客户端
override fun onBind(intent: Intent): IBinder {
return messenger.binder
}

复制代码
  1. 客户端 客户端在绑定 Service 后,会在 onServiceConnected 中获取 IBinder 的实例,客户端使用此实例创建 Messenger 实例,这个 Messenger 就可以和服务端进行通信了,发送 Message 信息服务端就会收到;
private var messenger: Messenger? = null

private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(p0: ComponentName?, iBinder: IBinder?) {
messenger = Messenger(iBinder) // 绑定 service 后初始化 Messenger
}
override fun onServiceDisconnected(p0: ComponentName?) {
messenger = null
}
}

var message = Message.obtain(null, MSG_WHAT, 100,0) // 创建 Message
messenger?.send(message) // 发送 Message

// 输出结果
07-24 14:00:38.604 18962-18962/com.example.administrator.memory E/MyService:MyServer 100

复制代码

若服务端想回应客户端,那客户端就要像服务端一样创建一个接受信息的 Handler 和 Messenger 实例,在发送 Message 时使用 msg.replyTo 将 Messenger 实例发送给服务端,服务端就可以使用此实例回应客户端信息;

//客户端发送Messenger到Service
msg.replyTo = mGetReplyMessenger;

// 在 Service 端接收客户端的 Messenger
Messenger msg = msg.replyTo;

复制代码

AIDL 对于进程通信来说,可能实际在项目中使用的可能更多的还是 AIDL,所以作为本文的最后也是重点讲解,并结合实际的代码分析多进程的使用,Aidl 支持的数据类型:

  1. 基本数据类型
  2. String 和 CharSequence
  3. List:只支持 ArrayList
  4. Map:只支持 HashMap
  5. Parcelable:所有实现 Parcelable 接口的实例
  6. AIDL:所有声明的 AIDL 文件

AIDL 的使用分为三步:AIDL 接口创建、服务端、客户端实现,下面实际代码分析,我们做一个简单的 Demo,在主进程中输入账户密码,然后在服务进程中验证登陆,并将结果返回调用进程;

  • AIDL 接口创建 创建登陆 ILoginBinder 的 AIDL 接口文件,并声明登陆方法:
import com.alex.kotlin.myapplication.User;
interface ILoginBinder {
 void login(String name ,String pasd);
 boolean isLogin();
 User getUser();
}
复制代码

上面的 Aidl 文件中使用了 User 类,所以在 Java 代码中创建 User 类,但初次之外也要创建 User.aidl 文件且包名要和 Java 中的一样,并在 ILoginBinder 中导入 User 文件的包;

package com.alex.kotlin.myapplication;
parcelable User ;
复制代码

此时点击 MakePeoject 系统会自动编译出 AIDL 文件对应的 java 代码 ILoginBinder 类,可以在 build 包相应的路径下可以查看此类,代码结构如下:

public interface ILoginBinder extends android.os.IInterface{
.....
public static abstract class Stub extends android.os.Binder implements com.alex.kotlin.myapplication.binder.ILoginBinder{
......
private static class Proxy implements com.alex.kotlin.myapplication.binder.ILoginBinder{
.....
}
......
}
复制代码
  1. ILoginBinder:继承 android.os.IInterface 的接口,并声明了 AIDL 文件中的方法
  2. Stub:编译 AIdl 文件后自动生成的文件,继承 Binder 并实现 ILoginBinder 接口,Stub 是一个抽象类,所以它的子类要实现 AIDL 文件中的方法;Stub 中有个重要的方法 asInterface(android.os.IBinder obj),它的传入参数是 Binder 实例,根据判断 Binder 是否为当前进程,若为当前线程返回 BInder 的实例,若为其他进程则返回 Stub.Proxy(obj) 的代理类
  3. Proxy:它是 Stub 中的一个内部类,也实现了 ILoginBinder 接口和所有方法,不过它的方法最后的执行还是交给传入的 mRemote 中执行,而 mRemote 就是 IBinder 的实例,所以方法的最终调用还是在 Stub 的子类中
  • 服务端的实现 服务端的实现也分为两步:
  1. 创建 Stub 类的子类并实现方法
class LoginBinder : ILoginBinder.Stub() {
override fun login(name: String?, pasd: String?) {

 Log.e(<span class="hljs-string">"======"</span>,<span class="hljs-string">"name = <span class="hljs-variable">$name</span> ; pasd = <span class="hljs-variable">$pasd</span>"</span>)
        user = User(name)
}

override fun isLogin(): Boolean {
   <span class="hljs-built_in">return</span> user != null
}

override fun getUser(): User? {
    <span class="hljs-built_in">return</span> user
}
}
复制代码
  1. 创建 Service 端并在 onBind 方法中返回 Stub 的子类
class BinderMangerService : Service() {
val binder = LoginBinder()

 override fun onBind(intent: Intent) : IBinder?{
 <span class="hljs-built_in">return</span>  binder
}

}

复制代码

设置 Service 的进程

 <service
                android:name=".binder.BinderMangerService"
                android:process=":service">
</service>
复制代码
  • 客户端的实现 客户端的实现和服务端一样遵循者 Service 的使用方式,首先绑定 Service 服务在后的回调中获取 IBinder 实例,也是 Stub 的实现类的实例,客户端拿到此类后调用 Stub 中的 asInterface() 方法获取代理类,到此即可实现进程间的通信
runOnThread {
       val intent = Intent(contextWrapper, BinderMangerService::class.java)
       contextWrapper.bindService(intent, serviceConnect,Context.BIND_AUTO_CREATE)
       binderSuccessCallback?.success()
}
......
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
        iBinderManger = IBinderManger.Stub.asInterface(service)
}
复制代码

此时获取到 IBinderManger 的代理类后即可调用方法,下面我们调用 login() 方法登陆, 查看输出信息:

2018-12-08 22:24:26.675 349-363/com.alex.kotlin.myapplication:service E/======: name = AAAAA ; pasd = 11111
复制代码
  • AIDL 的断开监听

此时在 service 进程中收到了默认进成发送的登陆信息,即二者之间的通信完成,但服务的连接会在某个时机因为某种原因时断开,为了获取断开的时间或保持连接的稳定性 Android 提供了 Binder 连接的死亡监听类 IBinder.DeathRecipient,在绑定成功时给获取的 Ibinder 绑定 IBinder.DeathRecipient 实例,在连接断开时会收到死亡回调,我们可以断开连接后继续重连,使用如下:

  //创建IBinder.DeathRecipient实例
  var deathRecipient : IBinder.DeathRecipient? = null
  deathRecipient = IBinder.DeathRecipient {
           //断开连接
            iBinderManger?.asBinder()?.unlinkToDeath(deathRecipient,0)
            iBinderManger = null
            //重新连接
        }

override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
iBinderManger = IBinderManger.Stub.asInterface(service)
// 设置死亡监听
service?.linkToDeath(deathRecipient,0)
countDownLatch.countDown()
}

复制代码
  • AIDLBinder 接口回调

但此时的通信是单向的,如果想在登陆成功或失败的时候通知默认进程,即进程间的回调,以为二者处于不同进程间,所以普通的接口回调不能满足,此时的接口也必须是跨进程的 AIDl 接口,所以创建 ILoginCallback 文件:

interface ILoginCallback {
  void  loginSuccess();
  void loginFailed();
}
复制代码

在 ILoginBinder 的文件中添加注册和解除监听的方法:

void registerListener(ILoginCallback iLoginCallback);

void unregisterListener(ILoginCallback iLoginCallback);

复制代码

在 ILoginBinder 的实现类中实现这两个方法,这里需要说明的是 Android 为多进程中的接口注册问题提供了专门的类:RemoteCallbackList,所以在 Stub 的实现类中创建 RemoteCallbackList, 并在两个方法中添加和删除 ILoginCallback 的实例

private val remoteCallbackList = RemoteCallbackList<ILoginCallback>()
override fun registerListener(iLoginCallback: ILoginCallback?) {
     remoteCallbackList.register(iLoginCallback)
}

override fun unregisterListener(iLoginCallback: ILoginCallback?) {
     remoteCallbackList.unregister(iLoginCallback)
}
复制代码

对于 RemoteCallbackList 的遍历也有所不同,必须 beginBroadcast()和 finishBroadcast() 的成对使用,下面在登陆成功或失败后回调接口:

f (name != null && pasd != null){
            user = User(name)
            val number = remoteCallbackList.beginBroadcast()
            for (i in 0 until number){
                remoteCallbackList.getBroadcastItem(i).loginSuccess()
            }
            remoteCallbackList.finishBroadcast()
        }else{
            val number = remoteCallbackList.beginBroadcast()
            for (i in 0 until number){
                remoteCallbackList.getBroadcastItem(i).loginFailed()
            }
            remoteCallbackList.finishBroadcast()
}
复制代码

在 LoginActivity 中创建 ILoginCallback.Stub 的子类,并调用方法注册接口,

 private val loginCallback = object : ILoginCallback.Stub(){
        override fun loginSuccess() {
            Log.e("======","登陆成功")
        }
        override fun loginFailed() {
        }
    }
  loginBinder?.registerListener(loginCallback)
复制代码

此时再次运行结果:

2018-12-08 22:46:48.366 792-810/com.alex.kotlin.myapplication:service E/======: name = AAAAA ; pasd = 11111
2018-12-08 22:46:48.367 747-747/com.alex.kotlin.myapplication:login E/======: 登陆成功
复制代码

到这里进程间的相互通信已经完成了,现在可以在二者之间实现数据或逻辑的相互调用,是不是很 happy,但是你可以调用别人也可以调用,那怎么让只有自己才能调用呢?那就用到最后的一点就是 Binder 的权限验证

  • Binder 权限验证

默认情况下远程服务任何人都可以连接,权限验证也就是阻拦那些不想让他连接的人,验证的地方有两处:

  1. onBind()方法中
  2. 服务端的 onTransact()

验证的方式也有两种:

  1. 自定义权限验证
  2. 包名验证

下面分别使用两者进行服务端的验证,首先在清单文件中添加自定义权限,并默认声明此权限

<uses-permission android:name="com.alex.kotlin.myapplication.permissions.BINDER_SERVICE"/>

<permission android:name=“com.alex.kotlin.myapplication.permissions.BINDER_SERVICE”
android:protectionLevel=“normal”/>

复制代码

在 onBind()中判断此权限,如果通过则返回 Binder 实例,否则返回 null

override fun onBind(intent: Intent) : IBinder?{
        val check = checkCallingOrSelfPermission("com.alex.kotlin.myapplication.permissions.BINDER_SERVICE")
        if (check == PackageManager.PERMISSION_DENIED){
            return  null
        }
      return  binder
 }
复制代码

另一中就是在服务端的 onTransact()中验证权限和包名,只有二者都通过返回 true,否则返回 false

override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
       val check = checkCallingOrSelfPermission(<span class="hljs-string">"com.alex.kotlin.myapplication.permissions.BINDER_SERVICE"</span>)
       <span class="hljs-keyword">if</span> (check == PackageManager.PERMISSION_DENIED){
           <span class="hljs-built_in">return</span> <span class="hljs-literal">false</span>
       }

      val packages = packageManager.getPackagesForUid(Binder.getCallingUid())
       <span class="hljs-keyword">if</span> (packages != null &amp;&amp; !packages.isEmpty()){
           val packageName = packages[0]
           <span class="hljs-keyword">if</span> (!packageName.startsWith(<span class="hljs-string">"com.alex"</span>)){
               <span class="hljs-built_in">return</span> <span class="hljs-literal">false</span>
           }
       }
       <span class="hljs-built_in">return</span> super.onTransact(code, data, reply, flags)

}

复制代码
  • Binder 连接池 上面过程只使用了一个 Aidl 文件,那如果 10 个呢?不可能创建和绑定 10 个 Service,所以此时就休要使用 Binder 连接池,在 Binder 连接池中提供查询 Binder 功能,根据传入参数的不同获取响应 Stub 子类的实例,只需创建一个用于绑定和返回 Binder 连接池的 Service 即可,详细使用见文末的 Demo;

到此本文的所有内容都介绍完毕了,从安卓开发和使用来说已能满足工作中的需求,文末附上一个 Aidl 的 Demo,以商店购买商品为例,使用 Binder 连接池实现登陆、售货员、商店、和消费者四个进程的通信;

<activity android:name=".ConsumeActivity"
        android:process=":consume">
        </activity>
        <activity
                android:name=".LoginActivity"
                android:process=":login">
        </activity>
        <service
                android:name=".binder.BinderMangerService"
                android:process=":service">
        </service>
        <activity
                android:name=".ProfuctActivity"
                android:process=":product">
</activity>
复制代码

AIdlDemo 地址

  • Android

    开放手机联盟(一个由 30 多家科技公司和手机公司组成的团体)已开发出 Android,Android 是第一个完整、开放、免费的手机平台。

    293 引用
感谢    赞同    分享    收藏    关注    反对    举报    ...