1,组件化和ARouter原理

组件化定义:由若干独立的子模块,组合成一个整体,降低模块间的耦合,这些子模块在补足一定的条件下,都可独立运行。主模块也不会因为缺少任意子模块而无法运行。组件之间可以灵活的组建。

这里面有一个主要的问题就是组件之间的通信和页面跳转。

通信原理

总所周知,Android提供了很多不同的信息的传递方式,比如在四大组件中本地广播、进程间的AIDL、匿名间的内存共享、Intent Bundle传递等等,那么在这么多传递方式,哪种类型是比较适合组件与组件直接的传递呢。

 1、本地广播,也就是LoacalBroadcastRecevier。更多是用在同一个应用内的不同系统规定的组件进行通信,好处在于:发送的广播只会在自己的APP内传播,不会泄漏给其他的APP,其他APP无法向自己的APP发送广播,不用被其他APP干扰。本地广播好比对讲通信,成本低,效率高,但有个缺点就是两者通信机制全部委托与系统负责,我们无法干预传输途中的任何步骤,不可控制,一般在组件化通信过程中采用比例不高。
 2、进程间的AIDL。这个粒度在于进程,而我们组件化通信过程往往是在线程中,况且AIDL通信也是属于系统级通信,底层以Binder机制,虽说Android提供模板供我们实现,但往往使用者不好理解,交互比较复杂,往往也不适用应用于组件化通信过程中。
 3、匿名的内存共享。比如用Sharedpreferences,在处于多线程场景下,往往会线程不安全,这种更多是存储一一些变化很少的信息,比如说组件里的配置信息等等。
 4、Intent Bundle传递。包括显性和隐性传递,显性传递需要明确包名路径,组件与组件往往是需要互相依赖,这背离组件化中SOP(关注点分离原则),如果走隐性的话,不仅包名路径不能重复,需要定义一套规则,只有一个包名路径出错,排查起来也稍显麻烦,这个方式往往在组件间内部传递会比较合适,组件外与其他组件打交道则使用场景不多。

那组件化通信什么机制比较适合呢?既然组件层中的模块是相互独立的,它们之间并不存在任何依赖。没有依赖就无法产生关系,没有关系,就无法传递消息,那要如何才能完成这种交流?

目前主流做法之一就是引入第三者,比如图中的Base Module,比较有代表性的是就是ARouter。

Android 通用组件封装 android组件化通信原理_数据

实现思路(接口+路由)是专门抽取一个LibModule作为路由服务,每个组件声明自己提供的服务 Service API,这些 Service 都是一些接口,组件负责将这些 Service 实现并注册到一个统一的路由 Router 中去,如果要使用某个组件的功能,只需要向Router 请求这个 Service 的实现,具体的实现细节我们全然不关心,只要能返回我们需要的结果就可以了。

除了这种以通过引入第三者方式,还有一种解决方式是以事件总线方式,但这种方式目前开源的框架中使用比例不高。

Android 通用组件封装 android组件化通信原理_android_02

事件总线通过记录对象,使用监听者模式来通知对象各种事件,比如在现实生活中,我们要去找房子,一般都去看小区的公告栏,因为那边会经常发布一些出租信息,我们去查看的过程中就形成了订阅的关系,只不过这种是被动去订阅,因为只有自己需要找房子了才去看,平时一般不会去看。小区中的公告栏可以想象成一个事件总线发布点,监听者则是哪些想要找房子的人,当有房东在公告栏上贴上出租房信息时,如果公告栏有订阅信息功能,比如引入门卫保安,已经把之前来这个公告栏要查看的找房子人一一进行电话登记,那么一旦有新出租消息产生,则门卫会把这条消息一一进行短信群发,那么找房子人则会收到这条消息进行后续的操作,是马上过来看,还是延迟过来,则根据自己的实际情况进行处理。在目前开源库中,有EventBus、RxBus就是采用这种发布/订阅模式,优点是简化了Android组件之间的通信方式,实现解耦,让业务代码更加简洁,可以动态设置事件处理线程和优先级,缺点则是每个事件需要维护一个事件类,造成事件类太多,无形中加大了维护成本。那么在组件化开源框架中有ModuleBusCC  等等。

页面跳转

ARouter

ARouter 核心实现思路是,我们在代码里加入的@Route注解,会在编译时期通过apt生成一些存储path和activityClass映射关系的类文件,然后app进程启动的时候会拿到这些类文件,把保存这些映射关系的数据读到内存里(保存在map里),然后在进行路由跳转的时候,通过build()方法传入要到达页面的路由地址,ARouter会通过它自己存储的路由表找到路由地址对应的Activity.class(activity.class = map.get(path)),然后new Intent(),当调用ARouter的withString()方法它的内部会调用intent.putExtra(String name, String value),调用navigation()方法,它的内部会调用startActivity(intent)进行跳转,这样便可以实现两个相互没有依赖的module顺利的启动对方的Activity了。

2,recyclerview和listview区别

  • ViewHolder 的编写规范化,ListView 是需要自己定义的,而RecyclerView是规范好的;
  • RecyclerView复用item全部搞定,不需要想ListView那样setTag()与getTag();
  • RecyclerView多了一些LayoutManager工作,但实现了布局效果多样化;

布局效果:

  • ListView 的布局比较单一,只有一个纵向效果;
  • RecyclerView 的布局效果丰富, 可以在 LayoutMananger 中设置:线性布局(纵向,横向),表格布局,瀑布流布局
  • 在RecyclerView 中,如果存在的 LayoutManager 不能满足需求,可以自定义 LayoutManager

空数据处理:

在ListView中有个setEmptyView() 用来处理Adapter中数据为空的情况;但是在RecyclerView中没有这个API,所以在RecyclerView中需要进行一些数据判断来实现数据为空的情况;

HeaderView 与 FooterView:

  • 在ListView中可以通过addHeaderView() 与 addFooterView()来添加头部item与底部item,来当我们需要实现下拉刷新或者上拉加载的情况;而且这两个API不会影响Adapter的编写;
  • 但是RecyclerView中并没有这两个API,所以当我们需要在RecyclerView添加头部item或者底部item的时候,我们可以在Adapter中自己编写,根据ViewHolder的Type与View来实现自己的Header,Footter与普通的item,但是这样就会影响到Adapter的数据,比如position,添加了Header与Footter后,实际的position将大于数据的position;

局部刷新:

  • 在ListView中通常刷新数据是用notifyDataSetChanged() ,但是这种刷新数据是全局刷新的(每个item的数据都会重新加载一遍),这样一来就会非常消耗资源;
  • RecyclerView中可以实现局部刷新,例如:notifyItemChanged();
  • 如果要在ListView实现局部刷新,依然是可以实现的,当一个item数据刷新时,我们可以在Adapter中,实现一个notifyItemChanged()方法,在方法里面通过这个 item 的 position,刷新这个item的数据

缓存区别:

  • 层级不同

ListView有两级缓存,在屏幕与非屏幕内。mActivityViews + mScrapViews

RecyclerView比ListView多两级缓存:支持开发者自定义缓存处理逻辑,RecyclerViewPool(缓存池)。并且支持多个离屏ItemView缓存(缓存屏幕外2个 itemView)。 mAttachedScrap + mCacheViews + mViewCacheExtension + mRecyclerPool

  • 缓存内容不同

ListView缓存View。 RecyclerView缓存RecyclerView.ViewHolder

  • RV优势

a.mCacheViews的使用,可以做到屏幕外的列表项ItemView进入屏幕内时也无须bindView快速重用;

b.mRecyclerPool可以供多个RecyclerView共同使用,在特定场景下,如viewpaper+多个列表页下有优势.

客观来说,RecyclerView在特定场景下对ListView的缓存机制做了补强和完善。

详细的解释看这里:https://zhuanlan.zhihu.com/p/23339185

动画效果:

  • 在RecyclerView中,已经封装好API来实现自己的动画效果;并且如果我们需要实现自己的动画效果,我们可以通过相应的接口实现自定义的动画效果(RecyclerView.ItemAnimator类),然后调用RecyclerView.setItemAnimator() (默认的有SimpleItemAnimator与DefaultItemAnimator);
  • 但是ListView并没有实现动画效果,但我们可以在Adapter自己实现item的动画效果;

嵌套滑动:

  • 在事件分发机制中,Touch事件在进行分发的时候,由父View向子View传递,一旦子View消费这个事件的话,那么接下来的事件分发的时候,父View将不接受,由子View进行处理;但是与Android的事件分发机制不同,嵌套滚动机制(Nested Scrolling)可以弥补这个不足,能让子View与父View同时处理这个Touch事件,主要实现在于NestedScrollingChild与NestedScrollingParent这两个接口;而在RecyclerView中,实现的是NestedScrollingChild,所以能实现嵌套滚动机制;
  • ListView就没有实现嵌套滚动机制;

3,APP的启动流程:

Android 通用组件封装 android组件化通信原理_缓存_03

 

 启动流程

  • 点击桌面App图标,Launcher进程采用Binder IPC向system_server进程发起startActivity请求;
  • system_server进程接收到请求后,向zygote进程发送创建进程的请求;
  • Zygote进程fork出新的子进程,即App进程;
  • App进程,通过Binder IPC向sytem_server进程发起attachApplication请求;
  • system_server进程在收到请求后,进行一系列准备工作后,再通过binder IPC向App进程发送scheduleLaunchActivity请求;
  • App进程的binder线程(ApplicationThread)在收到请求后,通过handler向主线程发送LAUNCH_ACTIVITY消息;
  • 主线程在收到Message后,通过发射机制创建目标Activity,并回调Activity.onCreate()等方法。

参考文章:http://gityuan.com/2016/03/12/start-activity/ ,https://zhuanlan.zhihu.com/p/67451239

4、Binder机制

Binder是Android系统提供的一种IPC机制。

IPC原理

Android 通用组件封装 android组件化通信原理_android_04

每个Android的进程,只能运行在自己进程所拥有的虚拟地址空间。对应一个4GB的虚拟地址空间,其中3GB是用户空间,1GB是内核空间,当然内核空间的大小是可以通过参数配置调整的。对于用户空间,不同进程之间彼此是不能共享的,而内核空间却是可共享的。Client进程向Server进程通信,恰恰是利用进程间可共享的内核内存空间来完成底层通信工作的,Client端与Server端进程往往采用ioctl等方法跟内核空间的驱动进行交互。 

Binder原理

Android 通用组件封装 android组件化通信原理_数据_05

一次完整的 Binder IPC 通信过程通常是这样:

首先 Binder 驱动在内核空间创建一个数据接收缓存区;
接着在内核空间开辟一块内核缓存区,建立内核缓存区和内核中数据接收缓存区之间的映射关系,以及内核中数据接收缓存区和接收进程用户空间地址的映射关系;
发送方进程通过系统调用 copyfromuser() 将数据 copy 到内核中的内核缓存区,由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通信。
所以Binder IPC通信只需要拷贝一次,这也是它高效的原因。

Binder模型

Android 通用组件封装 android组件化通信原理_android_06

Binder 是基于 C/S 架构的。由一系列的组件组成,包括 Client、Server、ServiceManager、Binder 驱动。其中 Client、Server、Service Manager 运行在用户空间,Binder 驱动运行在内核空间。其中 Service Manager 和 Binder 驱动由系统提供,而 Client、Server 由应用程序来实现。Client、Server 和 ServiceManager 均是通过系统调用 open、mmap 和 ioctl 来访问设备文件 /dev/binder,从而实现与 Binder 驱动的交互来间接的实现跨进程通信。

Client、Server、ServiceManager、Binder 驱动这几个组件在通信过程中扮演的角色就如同互联网中服务器(Server)、客户端(Client)、DNS域名服务器(ServiceManager)以及路由器(Binder 驱动)之间的关系。

通常我们访问一个网页的步骤是这样的:首先在浏览器输入一个地址,如 http://www.google.com 然后按下回车键。但是并没有办法通过域名地址直接找到我们要访问的服务器,因此需要首先访问 DNS 域名服务器,域名服务器中保存了 http://www.google.com 对应的 ip 地址 10.249.23.13,然后通过这个 ip 地址才能放到到 http://www.google.com 对应的服务器。

Binder 通信过程

首先,一个进程使用 BINDER_SET_CONTEXT_MGR命令通过 Binder 驱动将自己注册成为 ServiceManager。
Server 通过驱动向 ServiceManager 中注册 Binder(Server 中的 Binder 实体),表明可以对外提供服务。驱动为这个 Binder 创建位于内核中的实体节点以及 ServiceManager 对实体的引用,将名字以及新建的引用打包传给 ServiceManager,ServiceManger 将其填入查找表。
Client 通过名字,在 Binder 驱动的帮助下从 ServiceManager 中获取到对 Binder 实体的引用,通过这个引用就能实现和 Server 进程的通信。

Binder完整定义

  • 从进程间通信的角度看,Binder 是一种进程间通信的机制;
  • 从 Server 进程的角度看,Binder 指的是 Server 中的 Binder 实体对象;
  • 从 Client 进程的角度看,Binder 指的是对 Binder 代理对象,是 Binder 实体对象的一个远程代理
  • 从传输过程的角度看,Binder 是一个可以跨进程传输的对象;Binder 驱动会对这个跨越进程边界的对象对一点点特殊处理,自动完成代理对象和本地对象之间的转换

可能被问的问题:

1,service注册的鸡蛋问题

ServierManager 是一个进程,Server 是另一个进程,Server 向 ServiceManager 中注册 Binder 必然涉及到进程间通信。当前实现进程间通信又要用到进程间通信,这就好像蛋可以孵出鸡的前提却是要先找只鸡下蛋!Binder 的实现比较巧妙,就是预先创造一只鸡来下蛋。ServiceManager 和其他进程同样采用 Bidner 通信,ServiceManager 是 Server 端,有自己的 Binder 实体,其他进程都是 Client,需要通过这个 Binder 的引用来实现 Binder 的注册,查询和获取。ServiceManager 提供的 Binder 比较特殊,它没有名字也不需要注册。当一个进程使用 BINDER_SET_CONTEXT_MGR命令将自己注册成 ServiceManager 时 Binder 驱动会自动为它创建 Binder 实体(这就是那只预先造好的那只鸡)。其次这个 Binder 实体的引用在所有 Client 中都固定为 0 而无需通过其它手段获得。也就是说,一个 Server 想要向 ServiceManager 注册自己的 Binder 就必须通过这个 0 号引用和 ServiceManager 的 Binder 通信。类比互联网,0 号引用就好比是域名服务器的地址,你必须预先动态或者手工配置好。要注意的是,这里说的 Client 是相对于 ServiceManager 而言的,一个进程或者应用程序可能是提供服务的 Server,但对于 ServiceManager 来说它仍然是个 Client。

2,为什么选用Binder

现有的Linux通讯的不足:

  • 管道:在创建时分配一个page大小的内存,缓存区大小比较有限;
  • 消息队列:信息复制两次,额外的CPU消耗;不合适频繁或信息量大的通信;
  •  共享内存:无须复制,共享缓冲区直接付附加到进程虚拟地址空间,速度快;但进程间的同步问题操作系统无法实现,必须各进程利用同步工具解决;
  •  套接字:作为更通用的接口,传输效率低,主要用于不通机器或跨网络的通信;
  •  信号量:常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  •  信号: 不适用于信息交换,更适用于进程中断控制,比如非法内存访问,杀死某个进程等;

Binder的优势:

从性能的角度 数据拷贝次数:Binder数据拷贝只需要一次,而管道、消息队列、Socket都需要2次,但共享内存方式一次内存拷贝都不需要;从性能角度看,Binder性能仅次于共享内存。

从稳定性的角度:Binder是基于C/S架构的,简单解释下C/S架构,是指客户端(Client)和服务端(Server)组成的架构,Client端有什么需求,直接发送给Server端去完成,架构清晰明朗,Server端与Client端相对独立,稳定性较好;而共享内存实现方式复杂,没有客户与服务端之别, 需要充分考虑到访问临界资源的并发同步问题,否则可能会出现死锁等问题;从这稳定性角度看,Binder架构优越于共享内存。
从安全的角度:传统Linux IPC的接收方无法获得对方进程可靠的UID/PID,从而无法鉴别对方身份。Android为每个安装好的应用程序分配了自己的UID,故进程的UID是鉴别进程身份的重要标志,前面提到C/S架构,Android系统中对外只暴露Client端,Client端将任务发送给Server端,Server端会根据权限控制策略,判断UID/PID是否满足访问权限,目前权限控制很多时候是通过弹出权限询问对话框,让用户选择是否运行传统IPC只能由用户在数据包里填入UID/PID;另外,可靠的身份标记只有由IPC机制本身在内核中添加。其次传统IPC访问接入点是开放的,无法建立私有通道。从安全角度,Binder的安全性更高。

Binder一次拷贝发生在哪里?

Android 通用组件封装 android组件化通信原理_数据_07

可以看出是发送方进程通过系统调用 copyfromuser() 将数据 copy 到内核中的内核缓存区,由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通信。

Binder传输的数据大小?

Binder对CPU和内存的需求比较低,效率比较高,从而进一步说明Binder适合于移动系统Android,但是,也有一定缺点,就是不同利用Binder输出大数据,比如利用Binder传输几M大小的图片,便会出现异常,虽然有厂商会增加Binder内存,但是也不可能比系统默认内存大很多,否则整个系统的可用内存大幅度降低。

ProcessState::ProcessState()
    : mDriverFD(open_driver()) // 打开Binder驱动【见小节2.3】
    , mVMStart(MAP_FAILED)
    , mThreadCountLock(PTHREAD_MUTEX_INITIALIZER)
    , mThreadCountDecrement(PTHREAD_COND_INITIALIZER)
    , mExecutingThreadsCount(0)
    , mMaxThreads(DEFAULT_MAX_BINDER_THREADS)
    , mManagesContexts(false)
    , mBinderContextCheckFunc(NULL)
    , mBinderContextUserData(NULL)
    , mThreadPoolStarted(false)
    , mThreadPoolSeq(1)
{
    if (mDriverFD >= 0) {
        //采用内存映射函数mmap,给binder分配一块虚拟地址空间,用来接收事务
        mVMStart = mmap(0, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, mDriverFD, 0);
        if (mVMStart == MAP_FAILED) {
            close(mDriverFD); //没有足够空间分配给/dev/binder,则关闭驱动
            mDriverFD = -1;
        }
    }
}
  • BINDER_VM_SIZE = (1*1024*1024) - (4096 *2), binder分配的默认内存大小为1M-8k。
  • DEFAULT_MAX_BINDER_THREADS = 15,binder默认的最大可并发访问的线程数为16。

所以可以看出限制就是1M-8k,超过这个大小就会报错。

参考文章:

http://gityuan.com/2015/11/08/binder-get-sm/