安全性好:为发送方添加UID/PID身份信息 性能更佳:传输过程只要一次数据拷贝,而Socket、管道等传统IPC手段都至少需要两次数据拷贝 复制代码
Binder四大模块:
- Binder Driver位于内核空间中,主要负责Binder通信的建立,以及其在进程间的传递和Binder引用计数管理/数据包的传输等。而Client与Server之间的跨进程通信则统一通过Binder Driver处理转发。
- 对于Client来说,只需要知道自己要使用的Binder的名字,然后通过0号引用去访问ServerManager获取目标Binder的引用,得到引用后就可以像普通方法那样调用Binder实体的方法。
- Server在生成一个Binder实体的同时会为其绑定一个别名并将别名传递给Binder Driver,Binder Driver接收后如果发现是新增的Binder,那么就会为其在内核空间中创建相应的Binder实体节点,然后Binder Driver将该节点的引用传递给ServerManager,ServerManager收到后再将该Binder的别名和引用插入到一张数据表中,这跟DNS中存储的域名到IP地址的映射原理类似。
- ServerManager也是个标准的Server,并且在Android中约定其在Binder通信的过程中唯一标识永远是0,也就是前面提到的0号引用。
Android系统启动过程中SystemServer会向BinderDriver注册ServiceManager,BinderDriver自动为ServiceManager创建Binder实体。所有在这之后启动的应用进程都会持有这个Binder的句柄,为0号引用,即所有用户进程的0号引用都指向该Binder。ActivityManagerService、PackageManagerService等系统服务都是通过Binder机制与应用进行双向通信。
传统的IPC方式:
- 发送方先将准备好的数据存放在缓存区中
- 然后通过系统调用进入内核中,内核服务程序在内核空间分配内存,将数据从发送方缓存区复制到内核缓存区中。
- 接收方读数据时也要提供一块缓存区,内核将数据从内核缓存区拷贝到接收方提供的缓存区中。
- 这种存储-转发机制有两个缺陷:
- 首先是效率低下,需要做两次拷贝:用户空间->内核空间->用户空间。Linux使用copy_from_user()和copy_to_user()实现这两个跨空间拷贝,在此过程中如果使用了高端内存(high memory),这种拷贝需要临时建立/取消页面映射,造成性能损失。
- 其次是接收数据的缓存要由接收方提供,可接收方不知道到底要多大的缓存才够用,只能开辟尽量大的空间或先调用API接收消息头获得消息体大小,再开辟适当的空间接收消息体。两种做法都有不足,不是浪费空间就是浪费时间。
显然,Linux上的跨进程通信需要内核空间做支持。传统的跨进程通信方式有Socket、信号量、管道、内存共享等,他们都属于Linux内核,但Android上的Binder并不属于Linux内核,那么Binder如何实现IPC呢?答案是 Loadable Kernel Module查看wiki, Android利用Linux的动态内核可加载模块机制(Loadable Kernel Module,LKM),建立Binder Driver挂载为动态内核,然后通过Binder Driver以mmap的方式将内核空间与接收方的用户空间进行内存映射,于是只需要从发送方的用户空间拷贝数据到内核空间中,就实现了一次数据拷贝完成进程间通信。使用mmap建立内核空间跟用户空间的映射后,同一份物理内存,既可以在用户空间用虚拟地址访问,也可以在内核空间用虚拟地址访问。所以mmap的本质是让用户空间中的一块虚拟地址与内核空间中的一块虚拟地址指向同一块物理地址。
Android应用在进程启动之初会创建一个单例的ProcessState对象,其构造函数执行时会同时完成binder mmap,为进程分配一块内存,专门用于Binder通信。
匿名Binder
在ServiceManager中注册过的Binder都叫实名Binder。当Client与Server通过实名Binder建立好Binder连接后,Server还可以通过这个连接将新的Binder实体封装进数据包传递给Client,这个被传递的就叫做匿名Binder,匿名Binder依然会在Binder Driver中生成实体节点,但不会在ServiceManager中注册。
匿名Binder为通信双方建立起一条私密通道,只要Server没有把匿名Binder发给别的进程,别的进程就无法通过穷举或猜测等任何方式获得该Binder的引用,向该Binder发送请求。
Binder线程
Binder通信实际上是位于不同进程中的线程之间的通信。假如进程S是Server端,提供Binder实体,线程T1从Client进程C1中通过Binder的引用向进程S发送请求。S为了处理这个请求需要启动线程T2,而此时线程T1处于接收返回数据的等待状态。T2处理完请求就会将处理结果返回给T1,T1被唤醒得到处理结果。在这过程中,T2仿佛T1在进程S中的代理,代表T1执行远程任务,而给T1的感觉就是象穿越到S中执行一段代码又回到了C1。为了使这种穿越更加真实,驱动会将T1的一些属性赋给T2,特别是T1的优先级nice,这样T2会使用和T1类似的时间完成任务。很多资料会用‘线程迁移’来形容这种现象,容易让人产生误解。一来线程根本不可能在进程之间跳来跳去,二来T2除了和T1优先级一样,其它没有相同之处,包括身份,打开文件,栈大小,信号处理,私有数据等。
对于Server进程S,可能会有许多Client同时发起请求,为了提高效率往往开辟线程池并发处理收到的请求。怎样使用线程池实现并发处理呢?这和具体的IPC机制有关。拿socket举例,Server端的socket设置为侦听模式,有一个专门的线程使用该socket侦听来自Client的连接请求,即阻塞在accept()上。这个socket就象一只会生蛋的鸡,一旦收到来自Client的请求就会生一个蛋 – 创建新socket并从accept()返回。侦听线程从线程池中启动一个工作线程并将刚下的蛋交给该线程。后续业务处理就由该线程完成并通过这个单与Client实现交互。
可是对于Binder来说,既没有侦听模式也不会下蛋,怎样管理线程池呢?一种简单的做法是,不管三七二十一,先创建一堆线程,每个线程都用BINDER_WRITE_READ命令读Binder。这些线程会阻塞在驱动为该Binder设置的等待队列上,一旦有来自Client的数据驱动会从队列中唤醒一个线程来处理。这样做简单直观,省去了线程池,但一开始就创建一堆线程有点浪费资源。于是Binder协议引入了专门命令或消息帮助用户管理线程池,包括:
· INDER_SET_MAX_THREADS
· BC_REGISTER_LOOP
· BC_ENTER_LOOP
· BC_EXIT_LOOP
· BR_SPAWN_LOOPER
首先要管理线程池就要知道池子有多大,应用程序通过INDER_SET_MAX_THREADS告诉驱动最多可以创建几个线程。以后每个线程在创建,进入主循环,退出主循环时都要分别使用BC_REGISTER_LOOP,BC_ENTER_LOOP,BC_EXIT_LOOP告知驱动,以便驱动收集和记录当前线程池的状态。每当驱动接收完数据包返回读Binder的线程时,都要检查一下是不是已经没有闲置线程了。如果是,而且线程总数不会超出线程池最大线程数,就会在当前读出的数据包后面再追加一条BR_SPAWN_LOOPER消息,告诉用户线程即将不够用了,请再启动一些,否则下一个请求可能不能及时响应。新线程一启动又会通过BC_xxx_LOOP告知驱动更新状态。这样只要线程没有耗尽,总是有空闲线程在等待队列中随时待命,及时处理请求。
关于工作线程的启动,Binder驱动还做了一点小小的优化。当进程P1的线程T1向进程P2发送请求时,驱动会先查看一下线程T1是否也正在处理来自P2某个线程请求但尚未完成(没有发送回复)。这种情况通常发生在两个进程都有Binder实体并互相对发时请求时。假如驱动在进程P2中发现了这样的线程,比如说T2,就会要求T2来处理T1的这次请求。因为T2既然向T1发送了请求尚未得到返回包,说明T2肯定(或将会)阻塞在读取返回包的状态。这时候可以让T2顺便做点事情,总比等在那里闲着好。而且如果T2不是线程池中的线程还可以为线程池分担部分工作,减少线程池使用率。
三、mmap扩展
Linux的三种IO方式:标准IO、直接IO、mmap。
标准IO
应用程序平时使用的read()、write()都属于标准IO,在发起读写操作后其实是往内核空间的页缓存读写数据。对于写操作,系统默认是延迟写入机制,页缓存的数据会由内核在合适的时机写入磁盘。
- 用户发起 write 操作
- 操作系统查找页缓存
a.若未命中,则产生缺页异常,然后创建页缓存,将用户传入的内容写入页缓存
b.若命中,则直接将用户传入的内容写入页缓存- 用户 write 调用完成
- 页被修改后成为脏页,操作系统有两种机制将脏页写回磁盘
a.用户手动调用 fsync()
b.由 pdflush 进程定时将脏页写回磁盘
可以看出write过程中有两次数据拷贝,第一次是从内存空间写入内核空间,第二次是内核将页缓存数据写入磁盘。
知识扩展:
相对于机械硬盘,SSD 存储还有一个“写入放大”的问题。这个问题主要和 SSD 存储的物理结构有关。
当 SSD 被全部写过一遍之后,再写入的数据是不可以直接更新,只可以通过覆盖重写,在覆盖之前需要先擦除数据。
但写入的最小单位是 Page,擦除的最小单位是 Block,而 Block 远大于 Page,所以在写入新数据时就需要先把 Block 上的数据读出来和要写入的数据合并在一起,再把 Block 擦除,最后把读出来的数据重新写入到存储上,这样导致实际写入的数据可能远远大于最开始需要写入的数据。
直接IO
应用程序直接读写磁盘。Android并没有提供直接IO的JAVA API。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
![](https://i-blog.csdnimg.cn/blog_migrate/70d92756e10a69790159caabe84718b2.jpeg)
结语
- 现在随着短视频,抖音,快手的流行NDK模块开发也显得越发重要,需要这块人才的企业也越来越多,随之学习这块的人也变多了,音视频的开发,往往是比较难的,而这个比较难的技术就是NDK里面的技术。
- 音视频/高清大图片/人工智能/直播/抖音等等这年与用户最紧密,与我们生活最相关的技术一直都在寻找最终的技术落地平台,以前是windows系统,而现在则是移动系统了,移动系统中又是以Android占比绝大部分为前提,所以AndroidNDK技术已经是我们必备技能了。
- 要学习好NDK,其中的关于C/C++,jni,Linux基础都是需要学习的,除此之外,音视频的编解码技术,流媒体协议,ffmpeg这些都是音视频开发必备技能,而且
- OpenCV/OpenGl/这些又是图像处理必备知识,下面这些我都是当年自己搜集的资料和做的一些图,因为当年我就感觉视频这块会是一个大的趋势。所以提前做了一些准备。现在拿出来分享给大家。
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!*