1、概述

想当初在第一次拜读《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,在此之前来看看处理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时,系统会默认使用当前类的Hash值最为UID,当java对象改变时其Hash值也改变了,所以反序列化时就找不到对应的Java类了,因此serialVersionUID是辅助序列化和反序列化的,只有两者的serialVersionUID一致才可实现反序列化;

  1. Parcelable

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

  1. 二者对比

对于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运行于用户空间,Binder驱动运行于内核空间;

  1. Server:服务的真正提供者,它会先向ServiceManager注册自己Binder表明自己可以提供服务,驱动会为这个Binder创建位于内核中的实体Binder和ServiceManager中的引用,并将名字以及新建的实体引用打包传给 ServiceManager,ServiceManger 将其填入查找表;
  2. Client:服务的需求者和使用者,它向ServiceManager申请需要的服务;ServiceManager将表中的引用Binder返回Client,Client拿到服务后即可调用服务中的方法;
  3. ServiceManager:Binder实体和引用的中转站,保存并分发Binder的引用,负责整个系统Binder的调度;
  4. 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因为自己有金刚钻才敢接这瓷器活;

当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的封装是我们可以更简单的使用进程通信,它的构造函数如下,从构造函数中看出Message提供了两种创建的方式,分别传入Handler和IBinder对象,其实这也正对应这消息的接收和发送;

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

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

  1. 服务端

首先创建一个Service来连接和接收客户端的请求,在Service中创建Handler实例,并使用此Handler的实例创建一个Messenger实例,由此可见此Handler会处理接收客户端发送的数据,并在Service的onBind()中返回Messenger中的Binder;

//创建Handlerclass 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实例,这也就是Message的另一个构造函数,创建Messenger对象后就可以和服务端进行通信了,发送Message信息服务端就会收到;

private var messenger: Messenger? = nullprivate 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// 创建Messagemessenger?.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实例发送给服务端,告诉服务段要回信息的话使用此Messager,服务端就可以使用此实例回应客户端信息;

//客户端发送Messenger到Servicemsg.replyTo = mGetReplyMessenger;// 在Service端接收客户端的Messenger,然后同样使用Messager发送消息给客户端Messenger msg = msg.replyTo;

AIDL

对于进程通信来说,可能实际在项目中使用的可能更多的还是AIDL,所以作为本文的最后也是重点讲解,并结合实际的代码分析多进程的使用,AIDL其实是Android为了方便开发者使用进程通信而设置的,使用AIDL后只需声明Server需要提供的方法即可,所有关于进程通信的代码系统会自动生成,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文件的包,这是AIDL的使用细节,虽然二者可能在统一包内,但还是要使用import导入;

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对象,若为其他进程则返回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("======","name = $name ; pasd = $pasd")            user = User(name)    }    override fun isLogin(): Boolean {       return user != null    }    override fun getUser(): User? {        return user    }    }
  1. 创建Service端并在onBind方法中返回Stub的子类
class BinderMangerService : Service() {    val binder = LoginBinder()     override fun onBind(intent: Intent) : IBinder?{     return  binder    }  }

设置Service的进程,使用process设定服务所在的进程名称

 <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()        }
  • AIDL Binder接口回调

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

interface ILoginCallback {  void  loginSuccess();  void loginFailed();}

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

void registerListener(ILoginCallback iLoginCallback); void unregisterListener(ILoginCallback iLoginCallback);

在ILoginBinder的实现类Stub中实现这两个方法,这里需要说明的是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 = 111112018-12-08 22:46:48.367 747-747/com.alex.kotlin.myapplication:login E/======: 登陆成功

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

  • Binder权限验证

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

  1. onBind()方法中,根据一些标志确定是返回Binder还是null
  2. 服务端的onTransact()中,根据标志返回true或false

验证的方式也有两种:

  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("com.alex.kotlin.myapplication.permissions.BINDER_SERVICE")           if (check == PackageManager.PERMISSION_DENIED){ // 检验权限               return false           }          val packages = packageManager.getPackagesForUid(Binder.getCallingUid())           if (packages != null && !packages.isEmpty()){ // 检验包名               val packageName = packages[0]               if (!packageName.startsWith("com.alex")){                   return false               }           }           return 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地址

更多相关文章

  1. android解析XML文件的三方法之DOM
  2. android里的线程和进程
  3. Android(安卓)界面滑动实现---Scroller类 从源码和开发文档中学
  4. Android中线程与进程的理解
  5. Android的事件处理机制之基于监听的事件处理
  6. 手机管理应用研究【4】—— 手机加速篇
  7. Android(安卓)对View的一些理解
  8. 如何使用Android中的OpenGL ES媒体效果
  9. Android中通过反射来设置Toast的显示时间

随机推荐

  1. 急找工作,想用java语言做一个小的软件,请高
  2. 开发网站相关知识html和javascript
  3. 为内存密集型应用程序增加JVM最大堆大小
  4. Ldap 修改用户密码及安装证书
  5. java写入文件的几种方法小结
  6. 使用webmagic编写Java爬虫获取博客园文章
  7. java 百度地图判断两点距离1
  8. 我的程序员成长之路
  9. java实现能计算10道基本运算的计算器
  10. 黑马程序员_Java基础_异常