日期: 2021 年 9 月 7 日

分享一下完全不依赖 asyncio 也支持异步语法的库

异步语法的支持完全不依赖 asyncio,当然并没有说可以替代 asyncio 或者更好啥的,只是一种实现,如果有对异步 io 或者 python 异步语法实现感兴趣的可以一看吧。

只要是用于代理流量转发这样的场景,所以接口毕竟简单,当然支持范围也就没 asyncio 那么广了,从 echo 测试来看,性能还是要好于 asyncio 一些的,helpers 中也简单实现了几个工具。

HTTP 请求测试

import sevent

async def http_test():
s = sevent.tcp.Socket()
await s.connectof((‘www.baidu.com’, 80))
await s.send(b’GET / HTTP/1.1\r\nHost: www.baidu.com\r\nConnection: Close\r\nUser-Agent: curl/7.58.0\r\nAccept: */*\r\n\r\n’)

data = b”
while True:
try:
data += (await s.recv()).read()
except sevent.tcp.SocketClosed:
break
print(data.decode(“utf-8”))
await s.closeof()

sevent.run( http_test)
TCP 端口转发

import sys
import sevent

async def tcp_port_forward_server():
server = sevent.tcp.Server()
server.listen((“0.0.0.0”, int(sys.argv[1])))

while True:
conn = await server.accept()
pconn = sevent.tcp.Socket()
pconn.connect((sys.argv[2], int(sys.argv[3])))
conn.link(pconn)

sevent.run(tcp_port_forward_server)
await asyncio 异步 Python25 条回复 • 2021-09-07 16:10:23 +08:00
abersheeran 1
abersheeran 6 小时 32 分钟前
看到了 greenlet,我之前就有个想法,用 greenlet 把同步函数包装起来变成一个 awaitable 的对象,但是一直没空去做。你可以试试看做一下。
sujin190 2
sujin190 6 小时 19 分钟前
@abersheeran #1 这个很简单,很早就搞过了

https://github.com/snower/TorMySQL
封装的 pymysql 可在 asyncio 下用

https://github.com/mongodb/motor
封装的 pymongo
youngce 3
youngce 5 小时 55 分钟前
Twisted 的出现比 asyncio 还早,本质就是函数回调+事件循环。后来 Twisted 一部分核心贡献者转到 asyncio 去了,毕竟 asyncio 是未来
abersheeran 4
abersheeran 5 小时 50 分钟前
motor 用的线程池。你这个 TorMySQL 写的一言难尽……不过 https://github.com/snower/TorMySQL/blob/ad583aadc2844c4b4e32e948b1f3252582832022/tormysql/util.py 这个挺符合我之前的设想。测试结果怎么样?比 asyncio.to_thread 快吗?
sujin190 5
sujin190 5 小时 47 分钟前
@youngce #3 你说的对,但是 Twisted 要能用 async 和 await 语法底层 ioloop 必须是 asyncio,我分享这个并没有说比 asyncio 更好,只是分享下对不使用 asyncio 的情况下如何使用 async 和 await 语法,感兴趣的话可以看看一看,毕竟 python 的 async 和 await 语法可是在解释器层和 asyncio 耦合在一起的,异步 io 相关的实现就更多了,也不复杂

而且吧其实 asyncio 为了使用更广,接口实现太复杂了,想简单搞个小工具啥的太麻烦了
Kilerd 6
Kilerd 5 小时 46 分钟前
可以看看这个 https://github.com/dabeaz/curio
sujin190 7
sujin190 5 小时 42 分钟前
@abersheeran #4 motor 并没有用线程池,你没仔细看吧,greenlet 的切换肯定比线程切换快,款且还有同步锁的问题,实现要更复杂,性能肯定是 greenlet 更好了

TorMySQL 我们自己用了很久了,并没有啥问题啊,只是为了让在 python2 的 tornado 上也能正常运行,并不是全都是 python3 语法的
abersheeran 8
abersheeran 5 小时 36 分钟前
@sujin190 Motor 的线程池代码: https://github.com/mongodb/motor/blob/master/motor/frameworks/asyncio/__init__.py#L73

我还想问个 Greenlet 的问题:一个带系统中断操作的函数丢到 greenlet 里,它会自动在中断时 switch 到其他 greenlet 吗?
sujin190 9
sujin190 5 小时 15 分钟前
@abersheeran #8 好吧,我错了,没想到 0.5 版本之后这货就改成线程池了,只是不知道为啥这样改

应该不会吧,greenlet 切换的是栈帧,系统中断打断的是底层线程调用栈,python 的栈帧似乎是分配在堆上的,线程切换并不会影响 python 栈帧,自然也就不会导致 greenlet 切换了吧
abersheeran 10
abersheeran 5 小时 12 分钟前
@sujin190 中断不会导致 greenlet 切换?那你这个封装甚至不如 asyncio.to_thread 快啊。我明白 motor 怎么换成线程池了。看来我还得看看 gevent 去
sujin190 11
sujin190 5 小时 11 分钟前
@abersheeran #8 如果是 signal 信号打断,signal 信号处理器结束的时候会恢复原来的栈帧,所以这种估计也不会导致 greenlet 切换吧
sujin190 12
sujin190 5 小时 6 分钟前
@abersheeran #10 你说的是系统 io 会不会导致 greenlet 切换吧?这个系统 io 处理都是运行在主 greenlet 里的,是你需要写数据读数据的时候,你主动切换的主 greenlet 去,不像线程池一样当系统 io 产生的时候由操作系统调度线程切换
ysc3839 13
ysc3839 4 小时 51 分钟前 via Android
借楼问一下,Python 里 async function 能否当成回调函数使用?
比如说已经有一个队列了,在 await 某个对象时得到一个回调函数加入队列,后续执行这个函数恢复 async function 执行。
abersheeran 14
abersheeran 4 小时 49 分钟前
@sujin190 那只要一个 SQL 查询卡在那儿,后续所有 SQL 查询不都被阻塞了吗?
sujin190 15
sujin190 4 小时 42 分钟前
@abersheeran #14 不啊,主 greenlet 使用 epoll 可以同时处理很多个连接的,连接 io 事件产生的时候主 greenlet 处理完事件后会依次切换到对应的子 greenlet 做业务处理,子 greenlet 又可以产生更多的 io 操作主动切回主 greenlet 处理了啊,这样不就异步并行可以同时处理无数个 sql 查询请求了
abersheeran 16
abersheeran 4 小时 28 分钟前
@sujin190 ?好的我去看看
sujin190 17
sujin190 4 小时 23 分钟前 via Android
@ysc3839 应该是可以的吧,只不过普通队列并不能让这个函数运行起来,你需要从队列取出来后扔到 asyncio 中去运行起来
ysc3839 18
ysc3839 3 小时 53 分钟前 via Android
@sujin190 想问不用 asyncio 的情况能否实现。
sujin190 19
sujin190 3 小时 17 分钟前
@ysc3839 #18 也是可以的,只是不用 asyncio 这样的异步 io,那么异步方法似乎没啥用了吧,没啥用的必要了
tmac010sjh 20
tmac010sjh 1 小时 39 分钟前
苏神依旧这么牛逼,还在 mg 么?
wamson 21
wamson 1 小时 32 分钟前
正好对这个有兴趣,话说这个有什么比较有名的方案吗?比如底层是 c++,io_loop 是 lib_event,通过 pybind11 来驱动 python 脚本层级,那么 python 层该怎么使用 async 语法糖呢。眼馋 python 的 async 语法糖好久了。
haoliang 22
haoliang 1 小时 2 分钟前
没有文档,粗略爬过代码;我的理解中,`sevent.go(callback)` 对应 `asyncio.create_task`, 在 sevent 中如何等待这个 callback 的完成呢? callback 运行过程中抛错,该如何处理?
sujin190 23
sujin190 50 分钟前
@wamson #21 async 语法糖本质就是一个迭代器,用迭代器的方法驱动运行就好了,搞 c++的话,去瞅一下 asyncio 的源码很快就知道了吧,而且你可以把 asyncio 的那部分源码搬过来放到你的 c++代码里啊
sujin190 24
sujin190 48 分钟前
@haoliang #22 只是一看,没啥动力写文档啊,callback 出错只是单纯用 logging 输出调用栈信息了,其实一般来说如果你关注 callback 出的错,那么你应该在一进入 callback 时就自己加 try 就是了呀
sujin190 25
sujin190 48 分钟前
@tmac010sjh #20 跑人了

请问 Ubuntu 升级软件包和內核会有兼容性问题吗?

Ubuntu 是可以跨版本升级的。请问 如果 apt-get upgrade 升级 OS 系统版本 16 到 18,已安装的软件版本也会升级到*新的 stable 版本吗? 如果会升级的话,如何保证能够兼容旧的配置文件? 。谢谢指点?

Ubuntu 升级 版本 请问13 条回复 • 2021-09-05 12:26:42 +08:00
Judoon 1
Judoon 3 天前
跨版本升级需要 do-release-upgrade
直接 apt-get upgrade 不会跨版本升级。

升级的时候会让你确认配置文件是使用新版的默认值,还是保持旧的配置文件,可以做比对,这时候就需要你对配置有一定了解才行了。
programV2 2
programV2 3 天前 via iPhone
@Judoon
@Judoon
@Judoon 谢谢 v 友. 命令打错了。sudo apt-get dist-upgrade 也可升级系统。那如果 sudo apt-get upgrade 更新已安装的包,也会让我确认配置文件是使用新版的默认值还是保持旧的配置文吗?我记得没有出现确认的提示呀。

@Judoon
ByteCat 3
ByteCat 2 天前
可能有些软件会依赖内核提供的某些功能,目前遇到的有 ufw 、wireguard 之类的,可能会报错,通常来说没什么问题
programV2 4
programV2 2 天前 via iPhone
@ByteCat
@Judoon 谢谢两位? 所以只要不是跨版本升级,如 Ubuntu 16.01 到 16.04 这种小版本之间的升级就不会出现兼容问题?
@ByteCat
hsfzxjy 5
hsfzxjy 2 天前 via Android
ubuntu 16.01 是什么,不是只有 04 和 10 吗
jackmod 6
jackmod 2 天前
ubuntu 的跨版本升级基本就是把 source.list 里的发行版代号改掉了。
官方仓库的包有连续性,很少会冲突。但也不建议升级时跨越 LTS 版本。
如果现存的配置文件和新版的不同,apt 会暂停并询问操作。
cheng6563 7
cheng6563 2 天前
内核一般和上层功能的兼容性基本没啥问题,比如我 centos6 加新内核跑各种容器跑的飞起。倒是和硬件驱动可能会有大问题,可能升到新内核直接上不了网。
blueboyggh 8
blueboyggh 2 天前
apt dist-upgrade 也不会升级大版本号吧
jim9606 9
jim9606 2 天前
内核通常可以放心升,但建议同步升级 linux-libc-dev 和 linux-firmware 避免找不到固件之类的问题。
do-release-upgrade 的问题是如果你改过软件包的配置,升级过程会卡住并询问是否保留修改。这个安装过程好像是没法回退和中断的。要是遇上空间不足、意外关机、SSH 断线就不好收拾。而且这个过程可能会破坏不在包管理器管理下的软件的 ABI 兼容性。
adoal 10
adoal 2 天前
“如何保证能够兼容旧的配置文件”?

理论上并不能保证……实际上遇到的不兼容情况并不多。

另外,如果安装 deb 包时初始版本的配置文件被用户修改过了,而新版的 deb 包自带的初始文件又有变化,dpkg 会问你怎么处理,是用新的、保留旧的、尝试做三路归并(成功率并不高),也可以用 diff 格式显示新老版本的差异,以及临时开一个 shell 让你人工检查。
triptipstop 11
triptipstop 2 天前
就算不跨版本,平时升级下内核,有些软件就不行了,比如 vbox 。
msg7086 12
msg7086 2 天前
@programV2 @hsfzxjy
倒是有 16.04.1 和 16.04.4 。

这里 16.04 是大版本(年.月),后面的.1 和.4 是小版本。
wangbenjun5 13
wangbenjun5 2 天前
@triptipstop 虚拟机确实是依赖内核的一些东西,每次升级都得重新搞一下。但是大部分软件其实对 Linux 内核版本没什么要求,升级完全不影响,我使用的是 ubuntu16.04 ,默认内核是 4.4 版本,我现在已经升级到 5.10 版本。

为什么你要用 Linux 当桌面呢?

今天逛王垠的博客,想起之前他写的关于 Linux 的文章。

我大学那会,也是受他影响,把桌面换成了 Ubuntu,Linux 的技能也是那个时候学会的。

一直使用 Ubuntu 到大学毕业参加工作,工作中感觉自己是个另类,因为只有我一个人用 Linux 。

开发过程中的东西,Win 上有的,Linux 下也有,这点没让我觉得 Linux 有什么优势。公司使用微信和 QQ,这个 Linux 反而是硬伤,Wine 又不稳定,只有开个 Win 虚拟机。

后来想通了,为什么不用 Win 呢,该有的都有。工作和娱乐两不误,而 Linux 只能工作。

当时 QQ,网易云音乐,微信,钉钉,这些都只有 Win 才有。不过现在也有 Linux 的网易云音乐了。

使用 Linux 的过程中,我并没有感到效率的提升,而是一大堆破事。

系统可能因为一个升级就出问题,轻则软件包依赖异常,重则进不了桌面。

那会 Ubuntu 还是用的 gnome,这个东西也会卡死,我都遇到好几次,鼠标根本动不了。只能切换到终端杀进程,重启桌面服务。

Linux 桌面的体验真心没有 Windows 好,就很折腾。

我想唯一的好处,就是让我对操作系统的本质更了解。让我在学习操作系统课程的时候,知道什么是进程,内存管理,文件系统。而不是像其他同学那样是陌生的概念。

但我不得不说,Linux 的优势是在服务器上。除了 Windows 专属的域,其他的任何服务都能用 Linux 搭建,不仅免费,还占用资源少。

以上是我的经历,我曾经是 Linux 的狂热粉丝,但我*终从 Linux 叛逃到 Windows 上了。

我想论坛里肯定还有坚持使用 Linux 的小伙伴,你们为什么要用 Linux 当桌面呢?

第 1 条附言 · 2 天前
感谢大家花时间回复。看到大家都使用没问题,我觉得也许是我太菜的缘故,导致使用 Linux 总是出问题。
好久有空尝试一些其他发行版试试,Ubuntu 可能不是那么稳,总是弹出来内部错误的报错窗口。
Linux 桌面 Ubuntu Win156 条回复 • 2021-09-07 16:26:47 +08:00
1 2
2
❮ ❯
fatigue 101
fatigue 1 天前
开发用不到桌面,vim 好多年了,有个终端就行,很少在本地开发

需要用到桌面的地方,Linux 桌面又不行了,比如打游戏,所以还得是 win
CheckTime 102
CheckTime 1 天前
@superhxl 昨天看 Deepin 社区的时候看到了,Deepin 系统可以直接在商店里面下,Arch 的话有人打包上传了 AUR,包名 wemeet-bin
@hackyuan 可以直接从 AUR 下哦,包名叫 wemeet-bin
cassyfar 103
cassyfar 1 天前
@felixcode 早看过了。还是有各种小问题。
noparking188 104
noparking188 1 天前 ❤️ 1
非得只有一台电脑嘛
Windows 、Linux 、MacOS 都有,该用啥用啥,虚拟机也行
daoqiongsi1101 105
daoqiongsi1101 1 天前
@nekoyaki 有一点比较好奇,你为什么不用微信?那你用什么社交软件?
jones2000 106
jones2000 1 天前
开发用什么不能写代码呢,有个记事本就可以,能 ctrl+c, ctrl+v 就行。, 但 windows 你可以摸鱼呀。
harde 107
harde 1 天前 ❤️ 2
任何的为了用而用,*后都没有好下场。

PS:这不是抖机灵~
wangfeng3769 108
wangfeng3769 1 天前
自从当了老师之后就不用 Linux 了,很怀念 linux 的日子
Xusually 109
Xusually 1 天前
主力机 MacbookPro Intel,干活、联络该有的软件都有,界面一致性好,brew 很好用,和手机、手表、平板绑定,家里人共享存储也方便。
辅机 Dell 的一台旧 laptop,Ubuntu,一直升级,目前 20.04.03 LTS 。
休眠工具机一台 ThinkPad X220i,Windows 一直升级,目前 Win10 。内存加到满,硬盘换了 ssd,偶尔车上接个 ODB,拿到哪里测试个东西什么的还是挺好用而且便携的。

还是得看用途,没有一概而论的。
defunct9 110
defunct9 1 天前 via iPhone
自由啊,想做什么做什么
ysn2233 111
ysn2233 1 天前
win 有 wsl1 之后就直接转 win 了,后面有 wsl2+xserver 转发之后就基本不会再需要用到 linux 桌面了,毕竟只论桌面水平和 win 差距太大了
gnawll 112
gnawll 1 天前 via iPhone
Linux 用了有一两年。*让我舒服的是终端的使用和出现问题能找到愿意,win 上啥都不会 只能点点点
psirnull 113
psirnull 1 天前
不用 linux,用 macos
imycc 114
imycc 1 天前 via iPhone
不用勉强自己。桌面系统只是一个工具,选择工具主要看顺不顺手。
如果你是做后台开发,且懒得配置远程编辑或者工具链,又不想踩跨平台的坑,直接用 linux 的桌面系统就好。
如果你经常打游戏,或者对 QQ,音乐甚至其他软件在 linux 上没有合适的替代,那么用 Win/MacOS+( wsl/虚拟机 /开发机)的方案比较合适。

条件允许的话,还有一种解决方案。自带笔记本用于工作聊天,把公司的台式机重装成 linux,双屏幕+笔记本三个屏幕,按自己的需求分配。两个系统之间用文件共享或者其他方案传输文件、剪贴板、图片之类的。
Dragonphy 115
Dragonphy 1 天前
日经贴,想用啥用啥,觉得 Linux 用得不糟心的可能没有触及到他的痛点,我 Linux 桌面使用不下去的
silkriver 116
silkriver 1 天前
Linux 当桌面的*佳选择是用 Arch 系,对于桌面用户来说 Linux 发行版只分有 AUR 和没有 AUR 这两种
zzzmh 117
zzzmh 1 天前
一开始是为了学习 后来喜欢 Linux 的效率 比如同一句 sql linux 版本能查更快 maven / webpack 打包更快。。。。。其余没了 也不是非用不可。。。。。就是玩
huangmingyou 118
huangmingyou 1 天前
周末刚遇到的情况。在家用 win10 系统想连阿里云上的 win7, 结果远程桌面协议不对应,让我改组策略,但是组策略用不了,要先弄一个 bat 处理下。处理完了系统卡死重启。还是没用。然后搜了一下,又要改注册表。
刚好家里还有一个 mac,想用 mac 连一下 windows 的远程桌面,搜了一下,搜到一个 samba 共享的文档。
事情比较急,也没来得及继续找方案。
然后把一台安装了 linux 的老笔记本跑起来,用 redesktop 远程连上了。

对我来说,作为一个运维,能稳定的提供一个工作环境,和一个安全的环境,比啥都重要。
huangmingyou 119
huangmingyou 1 天前
补充一下,从 2001 年开始用 linux,20 年都是 linux 做主力,除了需要安装虚拟 windows 聊天意外。用 linux 工作完全没问题。
现在的 linux 已经比之前好用多了。早期用 redhat 7.1 的时候,还要自己汉化,安装字体。然后联想的幸福 linux 不知道为啥不持续做了,那可是国内很早的发行版。做的其实还阔以。
cco 120
cco 1 天前
日常开发,啥系统都行。win 肯定是*稳定的,*通用的。但是那个命令行真心用不惯。
dfkjgklfdjg 121
dfkjgklfdjg 1 天前
因为树莓派上装 Win 有点卡,本来就是娱乐用的,主力还是 Win,
说 Win 卡和中毒的都是钞能力不够,买好一点的配置,从官方渠道购入正版软件,
如果还会中毒的好好想想每个夜深人静的时候你都干了些什么。

平心而论,适合当时的业务场景的系统就是好的系统,以一套应万变的东西肯定会出现意外的问题。
yuancoder 122
yuancoder 1 天前
很多人根本就不会用 linux,基本命令和概念都不懂。而你可以很容易的使用 windows 。
20015jjw 123
20015jjw 1 天前 via Android
mac 用户:huh ?
leeyuzhe 124
leeyuzhe 1 天前
之前 chrome 某个版本下 linux 输入法 flitcx 有个 bug,折腾了半下午,后来想想简直是找罪受就换成了 windows
lagoon 125
lagoon 1 天前
因为 windows 贵
LokiSharp 126
LokiSharp 1 天前
就不能都用么 Emmmm
Nich0la5 127
Nich0la5 1 天前
没必要吵 有人觉得开箱即用才算不折腾,有人觉得社区有成熟方案照着做一遍就算不折腾。看你心态了。
linux 桌面的折腾大部分都是一劳永逸的,升级带来的问题其实不多,有也会在下个小版本修掉(除非再来一次 qt4 到 qt5 )。
本懒货工作机用的 win+wsl 很多 Linux 比较爽的地方 win 也在学 比如 windows terminal,chocolate 。家里一台 win 打游戏一台折腾着玩现在装的 fyde os
raptor 128
raptor 1 天前
首先,因为没什么电脑娱乐的需求。
其次,命令行用得多,Windows 的命令行太垃圾。
第三,也没什么需要用的应用没有 LINUX 版。
第四,十几年不用 Windows 不太会用——上一次用的主力 windows 版本还是 XP 。
第五,相比之下还是 Mac 好用一点。
janda 129
janda 1 天前
### 你们 linux 都用的哪个虚拟机?

宿主机通过虚拟机聊天、会不会不是很方便啊,主要是来信息了、不打开虚拟机就看不到,可以做到虚拟机里面的消息通知在宿主机任务栏提示么!
ww2000e 130
ww2000e 1 天前
桌面可以自己定制,还可以用平铺器 ,win 默认\r\n 换行和 gbk 环境感觉很麻烦,ntfs 分区保存代码里面有链接也会碰到麻烦
chenmobuys 131
chenmobuys 1 天前
什么样的工具,就做什么样的事情。
zonyitoo 132
zonyitoo 1 天前
是的,所以毕业后就换 mac 了。
cyspy 133
cyspy 1 天前
gnome3 差,emacs 好
857681664 134
857681664 1 天前 via Android
linux 装软件比 windows 舒服多了,尤其是开发环境需要的软件,windows 你得找安装程序,然后点好多下才能装好,linux 无论是 aptget 还是 pacman,都是一行代码的事,中间没有什么别的步骤就装好了。我现在不得不用 windows 的原因是 linux 没有好用的跨平台远程控制软件。有大佬如果知道哪个跨平台的远程控制软件可以推荐一下。
popil1987 135
popil1987 1 天前
用 archlinux + i3wm
去年固态硬盘坏了,重装了一次系统,如果不算这次已经三年没重装系统了
貌似好几个月没关机了
不玩游戏
比如前一阵别人让我给压缩个视频,打开终端 ffmpeg 就开干
遇上一次 halt,重启,打开日志*后几段粘贴到 google 搜索,解决!
当然,windows 也离不了,微信开发者工具
MacOS 也离不了,Flutter 的 ios 编译还得在 MacOS 上执行
另外,tensorflow gpu 驱动那套还是 windows 上容易安装,在 linux 还得区分 nvidia 啥版本,我根本记不住
就像一楼说的,不要坚持,既然你适应 Windows 那就用 Windows,适应 MacOS 就用 MacOS
uniqueding 136
uniqueding 1 天前 via Android
对我来说 pc 就是 browser 加 terminal,什么都一样
greatx 137
greatx 1 天前
我三个电脑,一个 windows,一个 linux,还有一个 macos,根据实际需求遇到什么情况用什么。
guochao 138
guochao 1 天前
Linux 下你对自己负责,任何商业软件下你希望其他人负责。抛开特别的生态需求,系统之间没啥差别。我选择 Linux 只是因为习惯了,很多人选择 Windows 也是类似的原因。另一方面 Windows 用户多可能和商业原因有关,硬件厂商很多会天然亲近 Wintel,我*近遇到的问题是主板有一天只能找到 Windows 的 bootloader 了( UEFI Shell 可以直接引导起来其他的 bootloader,说明包括 Secure Boot 在内的其他东西没问题),我自己感觉和厂商有关系,但是毕竟没有实锤,我不做 BIOS 给我代码我也锤不了。

非要说问题的话。

Linux 的主要问题在于碎片化,不同的环境满足的条件不一样,进而导致不同的应用表现差异有大有小。安卓也是类似的问题。再有就是权限放开以后大家认为法无禁止皆可为,可是 Linux 什么都没有禁止,就有很多人以为什么事情都可以做,做了也没关系。我自己平时也搞一些 Linux 桌面中间件(就是老看更新的 dbus 的各种服务),所以现在的工作也和 Linux 桌面生态有关。桌面环境一般够好了,不管是操作还是渲染效率。出问题八成是没有理解逻辑做了没法负责的事情。有的应用或者硬件不成熟是一定的,缺少(行为随机的普通)用户使用的产品都是缺少验证的,但是作为开发者不觉得是大问题,只是什么时候生态够大,大到能逐渐容纳用户、验证产品。那当然产品也要分情况,普通用户就限制死,不要做自己负责不了的傻事儿,需要开发东西了再考虑放开权限,其实就是 UOS/ChromeOS 的思路。啥?你说 configure 、make 、sudo make 不是正常操作么,凭啥不让我做?这还真不是正常操作,毕竟楼上大多出问题的连 ld 都没搞清楚过,Linux 对大部分人只是一个平台,一个环境,但是不是研究对象,知道 epoll 知道内核的某些模块知道一些原理不代表理解了 Linux 。就像是你很熟悉飞机的面板制造工艺,能说自己熟悉飞机整体么?能在家目录里面装上包配置环境,让二进制能被找到,这对于大部分人来说已经足够了,包括大部分的研发。

Windows 不熟悉,但是遇到过中文路径的问题,anaconda 很难和其他工具集成,conda 没法用系统代理(每次直接报错,必须关了代理再跑 conda ),环境变量管理复杂,桌面卡死,复制时系统失去响应,dotnet 为啥 udp 还要报端口拒*,QQ+虚拟机+游戏有概率会死,构建的时候索引和桌面占资源太多,音频有驱动有输出没声音,某些版本的一些软件终端渲染很慢( tty 的水准),硬件 passthrough 没搞懂怎么搞,应用之间不怎么好隔离,Windows 容器占的内存也太夸张了。但是赢就赢在有生态,另外 WSL2 的效率勉勉强强及格了,只是桌面还是很怪异,没法日常用。尝试了几年的微软栈以后,*后放弃了 Windows 。

回到一开始,我不用 Windows 只是因为我习惯了 Linux,而且 Linux 的生态可以满足我的需要,3D 建模、文档、简单的 CAD 、简单的电路设计,这些在 Linux 上面对我来说足够了。反过来说,如果没有这些生态,那我推荐你用 Windows,毕竟讨论独占生态没有意义,XBbox 没有地平线零之曙光就是不好了么,只是零之曙光选择了 PS4 独占而已。
guochao 139
guochao 1 天前
@skiy 我是觉得国内现在首先就是缺生态,缺应用,普通用户用不起来。其实底层的东西国内做了很多了,lvs 、tidb 、nginx 、linux 内核模块(比如说华为),甚至有些差不多算是正常用的服务器用的国产的 CPU (有些网站就跑在这些服务器上)。有了用户才知道脖子被别人卡在了什么地方,知道下来要做什么优化什么,没有用户就空想只能给别人养用户,或者做了东西有问题但是发现不了。

碎片化这个是个问题,但是不只是版本的问题,版本其实还好,snap 之类的工具就是解决这些。主要是桌面环境本身的能力,有的环境有 A 有 B 没 C,有的有 A 有 C 没 B 。鸿蒙、Fuchsia 都是把能力细化以后,应用按照能力分发,比如说现在 Android 本地 apk 只是一个形式,内部都是分解开的,分发的不一定是一个完整的应用。
skiy 140
skiy 1 天前
@guochao 不说国内, 国外的生态也一样. 比如 dropbox 就没有 linux 版的.
这几年, 中国这边的也慢慢开始做底层的了, 但跟国外大厂真的还有差距.

我前段时间整理了下各大公司的 opensource , 国外的厂商基本都有一个独立的二级域名挂靠他们的开源项目, 国内的只有 BAT 和华为. 他们有些公司还把自己吃饭的家伙开源出来了. 对待开源, 他们是认真的. 国内的厂商巴不得捂着, 生怕别人用他们的技术超过他们. 如果国内的厂商也能够像老外那样, 估计能把中国科技行业提升一个台阶.

我希望的是一些流行的软件, 大厂花点心思去做适配 (钉钉的 LINUX 版还是第三方个人基于 API 开发的, 所以肯定不存在技术难度)
froz 141
froz 1 天前
毛坯房和精装房的区别,即使我的毛坯房( arch )总有点小毛病,但不影响我爱她,比较是我自己装修的。
skies457 142
skies457 1 天前
Linux 下没有好用的 office 就足以劝退大多数人了….
MatDK 143
MatDK 1 天前
其实两边都有不好的地方 但同时也都在进步
Linux 对开发更友好
1. 环境配置 /包管理简单 可能你准备一个 bash 等着就完事了
2. 以前没有 WSL 的时候,Linux 开发确实更省心
3. git docker 等一些辅助工具都是 linux 友好的
4. 同 3,很多 CML 工具没有 GUI, windows 的 CML 之前又很烂[出了 powershell 和 windows terminal 之后有进步一点]
ipwx 144
ipwx 1 天前
@guochao 理想都是好的,现实都是骨干的。

Nvidia 显卡升级驱动或者内核就崩掉 X 也屡见不鲜。我甚至在我的 联想笔记本(集显) 上升级了内核就无法正常显示了。这我也负责不了啊。。。
nekoyaki 145
nekoyaki 1 天前
@skiy 不说国内, 国外的生态也一样. 比如 dropbox 就没有 linux 版的.
==================
dropbox 是有 linux 版,我前几年用过很久,后来是因为自己家里搭了 owncloud,dropbox 容量比较贵才弃用它。
但 evernote 确实没有 linux 版本,我是买了一个 crossover ( wine 的商业版)跑 evernote 的。
nekoyaki 146
nekoyaki 1 天前
@skies457
skies457 51 分钟前
Linux 下没有好用的 office 就足以劝退大多数人了….
========
其实 linux 版本的 WPS 蛮好用,也没有广告,稳定性也很好,足够满足大多数普通人对 office 的功能需求了
skiy 147
skiy 23 小时 59 分钟前
@nekoyaki 刚看了,dropbox 确实有. 是我记错了. evernote 确实是没有, 所以我现在用的是为知自建版.
nekoyaki 148
nekoyaki 23 小时 50 分钟前 ❤️ 1
@daoqiongsi1101
daoqiongsi1101 21 小时 30 分钟前
@nekoyaki 有一点比较好奇,你为什么不用微信?那你用什么社交软件?
=======

假如你是真的好奇而不是反问和质问的话,那么长话短说,就是我不需要微信。至于我用什么,我也说了,我用 docker+deepin 容器里面运行的 wine 版本 QQ 。

如果你还进一步好奇为什么我不需要的话,那就从各个功能上分开说。

聊天功能上,其实聊天软件的功能、性能并不*终决定用户的选择,用户到底选用什么聊天软件,万千归一,还是“自己需要沟通的人都用什么”。从这个角度而言,虽然大部分人可能觉得微信不可或缺,但我需要进行沟通的人,基本都是“也”使用 QQ 、电话、短信的。至于同事之间,我们使用自研的通讯软件沟通。

支付功能上我使用支付宝比较多,现金少一些,不用微信支付。因为我不信任一个做本职聊天功能都做不好的软件插手管我的钱包。而且微信支付没有快捷入口,但支付宝的付款码和扫码付都有快捷入口,非常方便。

朋友圈、公众号我没有需求。

而且我不用微信还有一个原因,就是 2020 年新冠疫情*肆虐的时候,我出门不得不使用微信扫码,但在这个紧要关头,微信却不由分说前后封了我新注册、什么都没做、没有好友的两个微信号,导致我日常生活处处受限。
有过这个经历,我不可能对这个软件有什么好感。
essicaj 149
essicaj 23 小时 39 分钟前
不是你太菜,我用 ubuntu 的桌面环境也是各种崩。编译个东西也是各种不兼容,很多编译问题一旦网上没有找到别人也有过的类似情况,根本就排查不出来到底哪里有问题。搞得我现在基本都是开 docker 重新部署环境来区分编译的。
azuresoar 150
azuresoar 23 小时 31 分钟前
王垠*近还在说阿波罗登月是骗局。。。
ETONG 151
ETONG 23 小时 22 分钟前
mac 才是*优解
miv 152
miv 22 小时 38 分钟前 ❤️ 1
过了爱折腾的年纪了。
以前折腾 win 、ubuntu 等好几个 Linux 版本。
在安装系统以及装完系统那一瞬间是很爽的。
特别是在大学时代,可以炫耀自己。
后来工作了就没那个心思了。
不管是 win 、linux 、Mac 都用过了,总的来说,重要的不是操作系统,而是操作系统之上的 开发套件。
比如 idea,用来写 Java,navicat 用来连数据库。
使用不同的操作系统,都还逃不过类似的干活工具。
所以,也就懒去折腾啥操作系统了。
现在备用机一个 win 、还有一个 nuc 的 mac 黑果。
内存够用、硬盘够用,可以出活就行了。
干完活去舒服溜达或者思考人生才是有意义的事情。
*后,无论是怎么样的操作系统,自己用顺心,可以出活*重要,而不是东施效颦。
ThatYear 153
ThatYear 9 小时 31 分钟前
以前装了一段时间,后来还是装回了 Windows,把 Linux 又装虚拟机了
zrpain 154
zrpain 7 小时 51 分钟前
@skiy dropbox 有啊 我用的 archlinux 前段时间才 yay -S dropbox.。用的好好的
cloudfstrife 155
cloudfstrife 5 小时 4 分钟前
debian + KDE , 稳定,酷炫,日常开发很 OK,KDE-Connect 真好用。
聊天用手机,PC 常挂 IRC 。
ptrees 156
ptrees 27 分钟前
我还以为是为了省钱,打破微软垄断之类的…

CentOS 要停止维护了,下一个要选择哪个发行版本? Debian 还是 Ubuntu?还是国产?

RT,CentOS 要停止维护了,下一个要选择哪个发行版本? Debian 还是 Ubuntu?还是国产?

centos debian Ubuntu 停止91 条回复 • 2021-09-07 15:55:29 +08:00
XiLingHost 1
XiLingHost 1 天前
如果不折腾,建议 Debian,稳定,但是包旧
如果喜欢新包,Ubuntu 的 server 版本
如果喜欢折腾,考虑 archlinux
LokiSharp 2
LokiSharp 1 天前
能和 CentOS 对标的也只有 OpenSUSE 了吧
AoEiuV020 3
AoEiuV020 1 天前
我觉得 ubuntu 可以,这阵子接触过一些服务器端项目,官方文档和脚本就只支持 centos 和 ubuntu,
不过服务器还是 docker 靠谱,宿主差不多就好,
libook 4
libook 1 天前
Debian 、Ubuntu LTS 、OpenSUSE 都可以,或者花钱买 RedHat 。
skiy 5
skiy 1 天前
看你是用在桌面还是服务器了.

如果是服务器的话, 可以用一些大厂的如:
CloudLinux 的 AlmaLinux
Oracle 的 Oracle Linux
或者
Rocky Linux
但国外的话, 我只看到 rocky 的有几个中国镜像源.

如果用国内的阿里云自己搞了个 anolios, 或者考虑 openeuler. 两者我更看好 openeuler. 不过 anolios 是 rhel 的完全克隆版(官方说可以直接迁移), 所以我觉得应该只是去除了里面的相关商标而已.
CodeCodeStudy 6
CodeCodeStudy 1 天前
https://www.centos.org/centos-linux/

8 的 End-of-life 是 2021-12-31
7 的 End-of-life 是 2024-06-30

所以用 7 吧,还可以用好几年呢
nbweb 7
nbweb 1 天前 ❤️ 9
用了十年的 debian 用户路过,不管是树莓派,还是家里 nas,还是其它的折腾中的虚拟机,都是 debian 。如果包旧的话,就编译,关键是使用久了,各种命令也熟了,折腾起来不费事。
我的工作跟程序沾不半边,完全是业余爱好,现在已能解决一些 debian 中出现的问题。
EastLord 8
EastLord 23 小时 45 分钟前
麒麟
afirefish 9
afirefish 23 小时 36 分钟前 ❤️ 2
debian,就一个字,稳!
felixcode 10
felixcode 23 小时 35 分钟前 via Android
debian,新一点的用 testing,求稳定的话用 stable
raysonlu 11
raysonlu 23 小时 25 分钟前
像阿里这样的大厂不会维护 centos?
Illusionary 12
Illusionary 23 小时 20 分钟前
centos 在国内的保有量非常大啊,好像国外用的不多?
skiy 13
skiy 23 小时 15 分钟前
@Illusionary 国内用 ubuntu 比较多. 我有个国外 VPS 被攻击,然后 VPS 厂商帮我重装的系统是 ubuntu 的…后来我 top 后发现, ubuntu 占的资源比 centos 少好多.
ETONG 14
ETONG 23 小时 13 分钟前
debian 11 yyds
wolfie 15
wolfie 23 小时 9 分钟前
@raysonlu #11
腾讯有 TencentOS
chust 16
chust 23 小时 5 分钟前 via iPhone
我们实验室要用 ansys 这类工程软件,不想用 centos,只有 opensuse 可用了
nbweb 17
nbweb 23 小时 0 分钟前
@skiy debian 可以安装在 128m 的内存的 vps 上面,有时 64m 都能装,哈哈哈。。。。
Aluhao 18
Aluhao 23 小时 0 分钟前
freebsd +1
zengxs 19
zengxs 23 小时 0 分钟前 ❤️ 4
整理了一些服务器操作系统的支持周期,可供参考

RedHat 系: (RH 系的系统都完全兼容 CentOS)
* RedHat 10 年 商业支持(付费)
* OracleLinux 10 年 商业支持(免费+付费)
* AlmaLinux 10 年 商业支持(免费+付费)
* Rocky Linux 10 年 社区支持

Debian 系:
* Debian 5 年 社区支持
* Ubuntu Server 10 年 商业支持(免费+付费)

SUSE 系:
* SUSE Linux Enterprise Server 10 年 商业支持(付费) (LTSS 额外 3 年支持)
* openSUSE Leap 5 年 社区支持

其他:
* FreeBSD 5 年 社区支持
* M$ Windows Server 10 年 商业支持(付费)
zengxs 20
zengxs 22 小时 55 分钟前
@zengxs 我个人是倾向于选择 OracleLinux,当然 M$ Windows 也不是不行 ?
salmon5 21
salmon5 22 小时 40 分钟前
@skiy #5
AlmaLinux 有 2 个中国镜像源 https://mirrors.almalinux.org/isos/x86_64/8.4.html
salmon5 22
salmon5 22 小时 38 分钟前
@zengxs #19
AlmaLinux 是完全免费的; CloudLinux OS 是收费的
kingfalse 23
kingfalse 22 小时 35 分钟前 via Android
debian 才是王道
yejinmo 24
yejinmo 22 小时 30 分钟前
openEuler
yinjiayi 25
yinjiayi 22 小时 28 分钟前
openEuler +1
whcoding 26
whcoding 22 小时 26 分钟前
有人用 deepin 么?
xlsepiphone 27
xlsepiphone 22 小时 26 分钟前
debian + homebrew
fpure 28
fpure 22 小时 22 分钟前
好像国外都是用 ubuntu
xiaoz 29
xiaoz 22 小时 21 分钟前
@XiLingHost #1,Debian 相对 CentOS 来说,包已经算新的了,CentOS 的包才是真的旧,默认内核也低(虽然可以升级)。下一个系统我也会选 Debian
julyclyde 30
julyclyde 22 小时 15 分钟前
@wolfie saltstack 和 ansible 都不能正确识别 TencentOS 哦
Loku 31
Loku 22 小时 11 分钟前
服务器一直用 Debian
桌面端使用 Xubuntu
qiaoka 32
qiaoka 21 小时 36 分钟前
近些年一直在用 debian,稳定性挺好,大版本升级也很丝滑。
*近在考虑是不是要将一部分服务器改为 suse,但还没有下定决心。
LxnChan 33
LxnChan 21 小时 28 分钟前
必然是 Ubuntu,因为确实好用。
另外 Ubuntu 就类似于 Debian 的傻瓜版(只是形容,没有侮辱人的意思,我自己也用 Ubuntu ),况且 Ubuntu 也是基于 Debian 的。
不过我不是很懂 cent 系的操作流程,感觉上来说迁移到 Ubuntu 会更好上手一些。
locoz 34
locoz 21 小时 14 分钟前 via Android
Ubuntu
msg7086 35
msg7086 20 小时 52 分钟前 via Android
如果你对 RH 系有需求的话,商业水平的可以用 Oracle 和 alma,社区维护的可以用 rocky 。
salmon5 36
salmon5 20 小时 51 分钟前
AlmaLinux/Rocky Linux
Huelse 37
Huelse 20 小时 48 分钟前
debian 11,爽
linzianplay 38
linzianplay 20 小时 17 分钟前
https://access.redhat.com/support/policy/updates/errata#Full_Support_Phase
CentOS Stream 的支持计划和 RedHat 保持一致,因为现在 CentOS Stream 是 RedHat 的上游分支
RedHat 目前已经计划至少支持到 2029 年
linzianplay 39
linzianplay 20 小时 16 分钟前
所以你直接用 CentOS Stream 就行了,他的 EOL 时间和会随着时间滚动加上去
skiy 40
skiy 20 小时 13 分钟前
@linzianplay 为什么当初改用”流”发布后, 老外骂得这么惨? 这不是维护不维护的问题, 而是 “小白鼠” 的问题.
KKLeon 41
KKLeon 20 小时 13 分钟前 via Android
@nbweb 我也是,之前因为 nas 折腾 debian,,现在回归 win nas
linzianplay 42
linzianplay 20 小时 8 分钟前
@skiy 这根本不是小白鼠的问题
你先别看当初包括创始人都在骂
现在都香起来了
linzianplay 43
linzianplay 20 小时 2 分钟前
人家用一样的代码打包出来 RedHat,怎么到你们嘴里就变成了 beta 呢.jpg
没发布正式版的时候也没让你升呀
lx0758 44
lx0758 19 小时 58 分钟前
用了 5 年 CentOS 了, 上周 5 台玩具全部上了 Debian 11
lx0758 45
lx0758 19 小时 56 分钟前
以前是服务器 CentOS7, 桌面 Ubuntu, 现在 Debian 一把梭
honmaple 46
honmaple 19 小时 27 分钟前
debian 包太旧,还是用 ubuntu lts 吧,千万不要像我司,选择丧心病狂的 gentoo 。。。
Henry529 47
Henry529 19 小时 25 分钟前
ubuntu 好点, 不过反正有 docker
cdlnls 48
cdlnls 19 小时 22 分钟前
自己选的话,大概率是 openEuler,实在用不惯 apt
anouser 49
anouser 19 小时 20 分钟前
我们公司的服务之前是 centos6 的,现在都升级到 debian buster 了,debian 升级到 11 也相对平滑。如果是服务器我觉得 debian 挺好的。
felixcode 50
felixcode 19 小时 19 分钟前
说 Debian 包太旧的,大概不知道 Debian 的稳定版 Debian 11 内核已经是 5.10, 而 Ubuntu 20.04LTS 的内核还在 5.04
felixcode 51
felixcode 19 小时 16 分钟前
@felixcode
5.05 -> 5.4
zro 52
zro 19 小时 13 分钟前
没人推 Fedora 么。。?
herozzm 53
herozzm 19 小时 4 分钟前
debian,内核和里面的包都很新啊,纯正,占用内存低,centos 可以无缝切过来,不会有坑
loading 54
loading 19 小时 2 分钟前
时过境迁啊,我还有一阵子认为我没用 RH 系会错失很多就业机会,没想到 Debian 系能看到 CentOS 停止支持。
raysonx 55
raysonx 18 小时 58 分钟前
关于 CentOS 问题我发过一个帖子,有兴趣的可以看我的发帖记录。
dorothyREN 56
dorothyREN 18 小时 36 分钟前
rhel 开发者订阅就行了
skiy 57
skiy 18 小时 22 分钟前 ❤️ 1
@zro 我怀疑只有我一个人用 Fedora 桌面版.
之前服务器全系 CentOS 8 (当初从 Ubuntu 切到 Fedora 的原因就是因为服务器用的是 CentOS), 后来发现资源占得多, 以后机子全部换 Ubuntu Server.

不过我发现国内的服务器, 真的离不开 RHEL 了. 看看 openEuler, AnoliOS, TententOS 全是 RHEL 系的. 然后那些桌面版全是 Debian 系的, 比如统信桌面版 (deepin) 就是 debian 系的, 然后服务器版是基于 OpenEuler (RHEL) 系的…这操作有点迷.
ik2h 58
ik2h 17 小时 44 分钟前 via Android
centos 都能用这么久,感觉其他发行版怎么选都无所谓了,找包管理器一样的,基本上都能无痛切换。桌面 linux 搞来搞去,能用出大区别的就包管理器罢了,想折腾的倒是可以玩玩 gentoo
ik2h 59
ik2h 17 小时 44 分钟前 via Android
centos 都能用这么久,感觉其他发行版怎么选都无所谓了,找包管理器一样的,基本上都能无痛切换。桌面 linux 搞来搞去,能用出大区别的就包管理器罢了,想折腾的倒是可以玩玩 gentoo 。
ajaxfunction 60
ajaxfunction 17 小时 42 分钟前
不会吧,
Felldeadbird 61
Felldeadbird 17 小时 36 分钟前
因为我不是专业的运维,c6 开始用,到慢慢习惯 c7 .结果发布 c8 又要重新学习。我就换回去 ubuntu 了。没想到官方放弃了?
cubecube 62
cubecube 17 小时 25 分钟前
debian 基本上可以跨版本升级,这个就赢了很多发行版了。
suxiaoxiann 63
suxiaoxiann 17 小时 23 分钟前
CentOS Stream
yanzhiling2001 64
yanzhiling2001 17 小时 10 分钟前
当时连夜投奔 debian,才发现 debian 真香
Maboroshii 65
Maboroshii 16 小时 6 分钟前
我司*近才把所有云服务上 ecs 的系统全部换成了 CentOS,就要停止维护了? 心疼运维 1 秒钟
tril 66
tril 16 小时 4 分钟前
个人体验:ubuntu 用起来会比 debian 适合 centos 用户。
centos 用户一般默认 root 是吧,debian 的 root 用户连 bashrc 都没有,就算直接复制了也会经常碰到 bash_history 没保存的情况。这在 ubuntu 上就从没遇到过,而且 root 用户也是配置好了的,相比 centos 就多一步 sudo su 。(不过 ubuntu 正在抛弃 netboot,如果喜欢自己手动 dd 系统的话,还是用 debian 比较好)
kksco 67
kksco 15 小时 45 分钟前
debian
hushao 68
hushao 13 小时 40 分钟前
Debian 真的是太稳了
ysicing 69
ysicing 8 小时 41 分钟前
Debian 11 稳
justrand 70
justrand 8 小时 31 分钟前
debian
idragonet 71
idragonet 8 小时 24 分钟前
服务器一直是 Ubuntu,现在是 20LTS.
liuxu 72
liuxu 8 小时 1 分钟前 via Android
看到大家都选 debian 和 ubuntu,我露出了老父亲的笑容
helllkz 73
helllkz 7 小时 56 分钟前
PVE 都是基于 Debian 的,所以 D11,老哥稳
ragnaroks 74
ragnaroks 7 小时 53 分钟前
debian 和 ubuntu 二选一那当然是 debian
xuanbg 75
xuanbg 7 小时 50 分钟前
服务器随便哪个发行版都没有太大区别,反正 docker 能稳定跑就成。桌面没怎么用过,没有资格评论。
CatCode 76
CatCode 7 小时 26 分钟前
选国产甚至不如用 windows server ( doge
Dragonphy 77
Dragonphy 7 小时 3 分钟前
国产系统是哪个?都是桌面环境吧
gBurnX 78
gBurnX 7 小时 0 分钟前
建议继续用 CentOS,新版本的结束周期还有很多年。

原因是,VMware 对其他平台的支持太差了,这样测试就成了问题。
jay4497 79
jay4497 6 小时 58 分钟前
我还以为这是个坟贴。。。
raptor 80
raptor 6 小时 45 分钟前
OMV 和 PVE 都是基于 Debian,所以我选 Debian
visonnn 81
visonnn 5 小时 8 分钟前
我公司已经全面转 Debian
LostPrayers 82
LostPrayers 5 小时 7 分钟前
Ubuntu Server 吧, 开发阶段当然是*新,这个追新方便。
开发完之后部署,Ubuntu Server 也有 10 年的 LTS 支持,不会错过各种安全补丁更新
cat9life 83
cat9life 5 小时 2 分钟前
小白的话建议 Ubuntu 资料多
polyang 84
polyang 4 小时 50 分钟前
我自己虚拟机用的是 centos,我会考虑 Ubuntu,公司服务器就是用的 Ubuntu
bipy 85
bipy 4 小时 12 分钟前
一直首选 Debian
calmzhu 86
calmzhu 1 小时 35 分钟前
个人的话用 Windows 吧…
企业,说的好像你改的了似的?
back0893 87
back0893 1 小时 31 分钟前
debian 或者 ubuntu
Kasumi20 88
Kasumi20 1 小时 17 分钟前
CentOS 这种垃圾终于要毁灭了吗?真棒!
linunix 89
linunix 1 小时 4 分钟前
CentOS Stream 或者 Rocky Linux
snuglove 90
snuglove 56 分钟前
CentOS LINUX 肯定是不能用了,维护时间已经缩短到 2021-12-31*
CentOS Stream 虽然维护时间到 2024-05-31,但是是不稳定的系统.也不建议用
不想有大的变化,可以用 Rocky Linux,想尝试其他的可以看看楼里建议.
Mogamigawa 91
Mogamigawa 47 分钟前 via iPhone
不要国内,无关爱国,只是因为国内容易跑路。

大厂的对象存储都是基于什么实现的呢?

腾讯云 COS,阿里云 OSS,HW OBS 这些对象存储服务是独立自主实现的,还是基于什么开源项目(比如 :MinIO )实现的呢?
OSS minio 存储 对象30 条回复 • 2021-09-05 05:25:09 +08:00
Ianchen 1
Ianchen 6 天前
盲猜一个 Ceph
acbot 2
acbot 6 天前
@Ianchen *早我感觉也是 Ceph 但是后来我发现 MinIO 更像 特别是 Rest Api 和 微服务结合方面
ccde8259 3
ccde8259 5 天前 via iPhone
Ceph……
自研一套 API 不用几个钱
dynastysea 4
dynastysea 5 天前 ❤️ 1
明确的告诉你,这三家都是自研的,ceph 这东西当当玩具可以,真正大规模的实践坑很多。存储是云计算的根基,在这块上各家都是重金投入,而且不存在兼容性的问题,自研好处多多。
moult 5
moult 5 天前 via iPhone
腾讯和阿里有历史技术沉淀在,肯定自研的。像 HW,七牛这类厂商就不好说了。。。不过,感觉 ceph 和 minio 自用还可以,拿来卖服务还差口气儿。
tongz 6
tongz 5 天前
各位大佬, OSS 的话, 海量小文件存储靠谱吗, 单个文件平均在 1KB-10KB 之间, 每天产生 1 千万个左右, 还在持续增长, 写多读少
locoz 7
locoz 5 天前 via Android
底层可能部分用了 ceph,但是主体肯定是自己搞的,大厂又不差那点钱。
plko345 8
plko345 5 天前 via Android
@acbot 对象存储好像都是有标准的,api 和协议方面都差不太多,aws s3 也是,底层不一样,但提供给用户的接口一样,所以会有这种错觉
gstqc 9
gstqc 5 天前 via Android
主要是 s3 协议已经是事实上的业界标准了,不兼容就没人用
ospider 10
ospider 5 天前
ceph 确实支撑不了多大的场景,但是也不至于到玩具的地步。头条 2017 年才从 ceph 切换到自研的。
boyhailong 11
boyhailong 5 天前
了解的成都好几个做云存储的都是 ceph 。。。。 而且也都商用了
dongqihong 12
dongqihong 5 天前
大体都分三层,接入层( API 、各种业务逻辑),索引层(基于 LSM ),持久化存储层(分部署存储,类似 HDFS )
阿里腾讯华为都大致如此,S3 早期也一样
WebKit 13
WebKit 5 天前 via Android
@moult 七牛也是 go 开发,自研的
gstqc 14
gstqc 5 天前
AWS S3 这种,一个 bucket 放百亿量的文件,如何做检索的
opengps 15
opengps 5 天前
参考 aws 的对象存储
crowdwei 16
crowdwei 5 天前
Azure Storage:
https://azure.microsoft.com/en-us/blog/sosp-paper-windows-azure-storage-a-highly-available-cloud-storage-service-with-strong-consistency/
henvm 17
henvm 5 天前 via Android
对象存储不是基于对象的吗?
imbushuo 18
imbushuo 5 天前
https://dl.acm.org/doi/10.1145/2043556.2043571

scalable table as index + scalable persistent layer (stamp),跟 SSD 的内部存储组织类似但是 scale 被放大了
acbot 19
acbot 5 天前
@boyhailong 曾经我看一个 私有云的商用产品存储方案也是用的 ceph
acbot 20
acbot 5 天前
@moult 也就是 一个中型公司自己服务用的话 ceph 和 minio 都没有问题是不?
acbot 21
acbot 5 天前
@ospider 是什么原因造成 ceph 不能支撑大的场景呢?如果自用只是存储量比较大的情况 ceph 能胜任不呢?
swulling 22
swulling 5 天前 via iPhone
说底层用 ceph 的可能对数据量缺乏想象力
dynastysea 23
dynastysea 5 天前 ❤️ 1
@moult 如果你是个存储行业的,你就知道 HW 的存储实力有多强了(软硬层面全栈都有自研,腾讯现在还停留在软件层面,阿里也在涉及存储硬件开发),互联网公司在华为面前就是跟玩似的,特别是腾讯,虽然是自研但基本也是跟着开源的节奏来(华为在国外都有专门的研究所,比如俄罗斯有专门的存储算法研究团队,存储部门内的博士更是一大堆,阿里也有美国研发团队,博士也不少。腾讯投入小很多,基本都是国内招本科生、研究生,存储行业好的博士基本不会选择腾讯,多数都是选择华为、阿里,很多在华为的挖都挖不动,对于那些深研技术的,目前国内差不多只有华为、阿里有更高的平台可以让他们接触到*前沿的存储技术)。华为存储做的很早,腾讯、阿里存储组里面很多华为跳槽过来的。
zmxnv123 24
zmxnv123 5 天前 via iPhone
hdfs+hbase 目前对象数大概几十万亿。
armyHcz 25
armyHcz 5 天前
@moult 七牛用的是阿里云包了一层
hemingway 26
hemingway 4 天前
除了 ceph 和 minio 还有其他选择么?对于小厂来说,不自研的话
henvm 27
henvm 4 天前
@dynastysea 这点比较赞同,我记得我公司之前一个客户买了一台华为的存储设备 15 万吧,他们用起来很舒服,
kerro1990 28
kerro1990 4 天前
用 AWS S3
yuyuko 29
yuyuko 2 天前 via iPhone
对象存储是存储中难度*小的了吧。。。协议层召点人怼一下,元数据层用开源方案,存储层随便搞搞就行,反正不要 iops,不要时延,吞吐大力出奇迹
yuyuko 30
yuyuko 2 天前 via iPhone
@dynastysea 哈哈,国内不还有 emc 嘛,我们这边好多 emc 出来的人?

aws s3 国内的使用体验如何?

请问有人使用过 aws s3 吗?本人有 10TB 数据需要做长期云端备份。对比了一下目前提供商的成本,aws 是*低的(虽然流量费用很高,但是不下载就没有问题)。但是 aws 服务器全部位于海外,上传和下载速度可能很慢,想问一下有这种情况吗?

要是用国内的云的话,tx 和 aliyun 会有内容审查吗?传上去的东西不知道会不会被和谐,可能要额外加密…

AWS aliyun 下载 备份52 条回复 • 2021-09-07 15:26:11 +08:00
skiy 1
skiy 1 天前
国内无体验. 因为全被墙了. 我记得 GitHub 的 release 走的就是 S3 (不知道现在改了没)
yunochi 2
yunochi 23 小时 50 分钟前
@skiy 谢谢,那只能用国内的了
Yi23 3
Yi23 23 小时 36 分钟前
aws 来说,早上还行,下午变卡,晚上更卡。
aliyun OSS 会有内容审查机制,违规文件无法访问,能不能下载看心情
wei745359223 4
wei745359223 23 小时 33 分钟前
aws 国内也有服务的,有宁夏和北京 2 个区域。
ivyliner 5
ivyliner 23 小时 28 分钟前
@wei745359223 AWS 国内不允许个人注册使用. 满足不了个人备份的需求.
skiy 6
skiy 23 小时 25 分钟前
用国内的吧, 可以看看七牛云.
国内的也是走流量, 但是存储包是有包年的, 另外, 可以自己搞个对应区域的云主机, 然后走内网.
ck65 7
ck65 23 小时 17 分钟前
数据备份建议克服连接困难用 s3,毕竟审查环境里的服务等同于 zero guarantee for long-term data integrity 。
yunochi 8
yunochi 23 小时 14 分钟前
@Yi23 谢谢,不过加一层加密压缩应该可以规避吧(虽然不想加)
Rheinmetal 9
Rheinmetal 23 小时 13 分钟前
@ck65 加密呢
jackleeforce3615 10
jackleeforce3615 23 小时 13 分钟前
天翼云盘 我可能注册的早 居然有 12t 的空间
wellsc 11
wellsc 23 小时 11 分钟前
@skiy 七牛还是算了吧
yunochi 12
yunochi 23 小时 11 分钟前
@wei745359223 谢谢,不过国内只允许企业服务

@skiy 谢谢,七牛云不是很了解,之后我查一下
@ck65 谢谢,请问如何克服连接困难呢?挂 * 吗?那样上传的开销就大了
stanchenxxx2015 13
stanchenxxx2015 23 小时 9 分钟前
楼主,aws/阿里云 /腾讯云我们这边都有代理,可以给您专业的技术支持和折扣价,欢迎联系呀:MTczNDYyNjUwNzA= ( base64 )

aws 国内两个 region,北京 /宁夏,单看存储的话性能是还 ok 的。
yunochi 14
yunochi 23 小时 8 分钟前
@jackleeforce3615 谢谢,不过国内网盘都有审核,而且本人还遇到过文件乱码的情况,不是很信任
skiy 15
skiy 23 小时 7 分钟前
@wellsc 普通存储方面价格比阿里有点优势, 其它的不了解.
相比之下, 腾讯云的 COS 便宜很多. 但看楼主存的东西好像要避规, 所以厂商越小,技术层面越菜,越做不到这方面的需要. 当然也有相关的技术风险.
不过,按我的理解,我觉得还是自建 NAS 成本更低,更安全和稳定.
yunochi 16
yunochi 23 小时 6 分钟前
@stanchenxxx2015 aws 国内有个人服务了?
yunochi 17
yunochi 23 小时 3 分钟前
@skiy 谢谢,nas 和磁带我都考虑过,放弃的原因是家里地方太小
stanchenxxx2015 18
stanchenxxx2015 23 小时 2 分钟前
暂时还没有,只能挂在企业下。楼主如果没有企业账号的话我们这边可以用我们的企业账号直接给您开个子账号。但数据安全性这块只能靠相互信任了。。。
stanchenxxx2015 19
stanchenxxx2015 23 小时 1 分钟前
@yunochi 暂时还没有,只能挂在企业下。楼主如果没有企业账号的话我们这边可以用我们的企业账号直接给您开个子账号。但数据安全性这块只能靠相互信任了。。。
yunochi 20
yunochi 23 小时 0 分钟前
@stanchenxxx2015 谢谢,那还是算了吧
stanchenxxx2015 21
stanchenxxx2015 22 小时 57 分钟前
@yunochi 海外 aws 也可以考虑下,可以用个人账号,数据安全性就不用担心了。可以开个香港 region 测测速度,我们这边可以提供免费测试金。
zengxs 22
zengxs 22 小时 28 分钟前
我印象中 Azure 是比 AWS S3 要便宜的,而且可以直接通过 samba 挂载到本地
Mithril 23
Mithril 22 小时 27 分钟前
只是备份的话,有专门的冷数据存储服务,比 S3 便宜。
审核需要规避的话,自己加密分卷。不要相信任何一家。
huangzxx 24
huangzxx 19 小时 23 分钟前
可以从香港阿里云中转一下
bullfrog 25
bullfrog 18 小时 30 分钟前 via iPhone
加密的话一定要确保能解开啊。。
PrinceofInj 26
PrinceofInj 18 小时 22 分钟前
目前再用国外的 blackbaze 备份照片,直连速度还不错
w7938940 27
w7938940 18 小时 18 分钟前
blackbaze b2 应该是*便宜的,国内也能直连
idblife 28
idblife 18 小时 16 分钟前 via iPhone
@stanchenxxx2015
香港速度不行,用东京吧
Cipool 29
Cipool 18 小时 8 分钟前 via Android
或者尝试一下 Office 365 中国版 (由世纪互联运营),多开几个企业订阅
bazingaterry 30
bazingaterry 17 小时 54 分钟前
blackbaze+1
ck65 31
ck65 17 小时 29 分钟前
@Rheinmetal 何必呢
ck65 32
ck65 17 小时 21 分钟前
@yunochi 淘低价机场呗,Google 「低价大流量机场」我就不推荐链接了。当然,我不知道你的后续增量有多大,如果只考虑初始的 10TB 看起来价格并不夸张。
wtks1 33
wtks1 15 小时 44 分钟前 via Android
或许可以试试 wasabi 的对象储存? 1t 每月 5.99 美元
linzianplay 34
linzianplay 14 小时 49 分钟前
https://www.alibabacloud.com/zh/product/oss/pricing
价格基本对标 aws,部分区域也有对标 S3 Glacier 的东西
冷归档型存储 存储空间 US$0.00099
流量经过 cloudflare 的话不计算流量费用
linzianplay 35
linzianplay 14 小时 43 分钟前
如果不在意墙的问题的话还可以上 Google Workspace,比冷存储贵点但是可以随意读写
qwertqwert12345 36
qwertqwert12345 14 小时 0 分钟前
azure 倒是没被墙吧
zzfra 37
zzfra 8 小时 10 分钟前 via Android
原来不只一个人不知道 b2 是 backblaze
knightdf 38
knightdf 7 小时 27 分钟前
@skiy S3 没有被墙啊,我们就是用的国际版 AWS,s3 我在家里都随便用
hewiefsociety 39
hewiefsociety 7 小时 23 分钟前
S3 挺强的 只能这么说
yalin 40
yalin 7 小时 8 分钟前
AWS 有国内版
zfy2014 41
zfy2014 6 小时 55 分钟前
@yunochi nas 也不大呀,感觉还是 nas 靠谱点
JingW 42
JingW 6 小时 39 分钟前
我尝试过,上传速度不错,下载还没有大量使用过,但是猜想可以通过 cloudfront 加速应该还可以
Rheinmetal 43
Rheinmetal 6 小时 4 分钟前
@ck65 任何存储提供商都不值得相信
反正上传的网速瓶颈摆在那里 加密成本也不高吧 aes-gcm 好像有硬件加速的 费一点电 让量子计算机实用或者暴力破解能力足够之前能保密还是挺划算的
担心量子计算机可以看看 PQC
mikong0911 44
mikong0911 5 小时 42 分钟前
存上去不要钱,下载是要钱的,个人用还是要注意下
th00000 45
th00000 5 小时 41 分钟前
如果是上传比较多而不经常下载的话, 不推荐使用 AWS S3
推荐使用 Amazon S3 Glacier https://aws.amazon.com/s3/glacier/
如果要下载的话, 检索时间会比较长(十几个小时才能检索完成), 并且检索要花钱
如果是要存储的话, 可以理解为非常非常的便宜, 每 G 每月只需要 3 分钱
https://www.amazonaws.cn/glacier/pricing/
hulala1021 46
hulala1021 5 小时 40 分钟前
前公司做 GIS 的,大量业务依赖 S3,使用的是宁夏节点,就速度来说取决你的带宽
LnTrx 47
LnTrx 3 小时 32 分钟前
就长期云端备份的需求而言(一年以上,几乎不下载),谷歌家的 Archive Storage 应该是*便宜的($1.2/TB ),还没有解冻时间

用境外的就是有可访问性的风险(国内厂商的国外地域也不例外)
用境内的就是有合规性的风险,部分价格可能稍贵

关键看自己对成本、可访问性、安全性的取舍
ck65 48
ck65 3 小时 30 分钟前
@Rheinmetal 加密是件有价值的事,但我们不是在讨论楼主的传输问题吗。。
Fantaoranges 49
Fantaoranges 1 小时 36 分钟前
搜了一下我们公司提的 case,没有什么 s3 相关的问题,当然用的是 aws-cn 的,s3 可以转冷存很便宜。
ch2 50
ch2 1 小时 31 分钟前
国内存储你只能加密
linzianplay 51
linzianplay 1 小时 3 分钟前
@LnTrx aws 和 alibabacloud 对标产品是$0.99/TB
skiy 52
skiy 58 分钟前
@knightdf 也可能跟运营商和地域有关吧.
我刚刚又试了一下,确实不行.

我之前有用过 brave, 就是没办法正常更新.
`https://brave.com/linux/#linux`

“`bash
Errors during downloading metadata for repository ‘brave-browser-rpm-release.s3.brave.com_x86_64_’:
– Curl error (28): Timeout was reached for https://brave-browser-rpm-release.s3.brave.com/x86_64/repodata/repomd.xml [Failed to connect to brave-browser-rpm-release.s3.brave.com port 443: Connection timed out]

Android Binder 设计与实现 – 设计篇

Android Binder 设计与实现 – 设计篇

关键词

Binder Android IPC Linux 内核 驱动

摘要

Binder是Android系统进程间通信(IPC)方式之一。Linux已经拥有管道,system V IPC,socket等IPC手段,却还要倚赖Binder来实现进程间通信,说明Binder具有无可比拟的优势。深入了解Binder并将之与传统IPC做对比有助于我们深入领会进程间通信的实现和性能优化。本文将对Binder的设计细节做一个全面的阐述,首先通过介绍Binder通信模型和Binder通信协议了解Binder的设计需求;然后分别阐述Binder在系统不同部分的表述方式和起的作用;*后还会解释Binder在数据接收端的设计考虑,包括线程池管理,内存映射和等待队列管理等。通过本文对Binder的详细介绍以及与其它IPC通信方式的对比,读者将对Binder的优势和使用Binder作为Android主要IPC方式的原因有深入了解。

1 引言

基于Client-Server的通信方式广泛应用于从互联网和数据库访问到嵌入式手持设备内部通信等各个领域。智能手机平台特别是Android系统中,为了向应用开发者提供丰富多样的功能,这种通信方式更是无处不在,诸如媒体播放,视音频频捕获,到各种让手机更智能的传感器(加速度,方位,温度,光亮度等)都由不同的Server负责管理,应用程序只需做为Client与这些Server建立连接便可以使用这些服务,花很少的时间和精力就能开发出令人眩目的功能。Client-Server方式的广泛采用对进程间通信(IPC)机制是一个挑战。目前linux支持的IPC包括传统的管道,System V IPC,即消息队列/共享内存/信号量,以及socket中只有socket支持Client-Server的通信方式。当然也可以在这些底层机制上架设一套协议来实现Client-Server通信,但这样增加了系统的复杂性,在手机这种条件复杂,资源稀缺的环境下可靠性也难以保证。

另一方面是传输性能。socket作为一款通用接口,其传输效率低,开销大,主要用在跨网络的进程间通信和本机上进程间的低速通信。消息队列和管道采用存储-转发方式,即数据先从发送方缓存区拷贝到内核开辟的缓存区中,然后再从内核缓存区拷贝到接收方缓存区,至少有两次拷贝过程。共享内存虽然无需拷贝,但控制复杂,难以使用。

表 1 各种IPC方式数据拷贝次数

IPC 数据拷贝次数
共享内存 0
Binder 1
Socket/管道/消息队列 2

还有一点是出于安全性考虑。Android作为一个开放式,拥有众多开发者的的平台,应用程序的来源广泛,确保智能终端的安全是非常重要的。终端用户不希望从网上下载的程序在不知情的情况下偷窥隐私数据,连接无线网络,长期操作底层设备导致电池很快耗尽等等。传统IPC没有任何安全措施,完全依赖上层协议来确保。首先传统IPC的接收方无法获得对方进程可靠的UID/PID(用户ID/进程ID),从而无法鉴别对方身份。Android为每个安装好的应用程序分配了自己的UID,故进程的UID是鉴别进程身份的重要标志。使用传统IPC只能由用户在数据包里填入UID/PID,但这样不可靠,容易被恶意程序利用。可靠的身份标记只有由IPC机制本身在内核中添加。其次传统IPC访问接入点是开放的,无法建立私有通道。比如命名管道的名称,system V的键值,socket的ip地址或文件名都是开放的,只要知道这些接入点的程序都可以和对端建立连接,不管怎样都无法阻止恶意程序通过猜测接收方地址获得连接。

基于以上原因,Android需要建立一套新的IPC机制来满足系统对通信方式,传输性能和安全性的要求,这就是Binder。Binder基于Client-Server通信模式,传输过程只需一次拷贝,为发送发添加UID/PID身份,既支持实名Binder也支持匿名Binder,安全性高。

2 面向对象的 Binder IPC

Binder使用Client-Server通信方式:一个进程作为Server提供诸如视频/音频解码,视频捕获,地址本查询,网络连接等服务;多个进程作为Client向Server发起服务请求,获得所需要的服务。要想实现Client-Server通信据必须实现以下两点:一是server必须有确定的访问接入点或者说地址来接受Client的请求,并且Client可以通过某种途径获知Server的地址;二是制定Command-Reply协议来传输数据。例如在网络通信中Server的访问接入点就是Server主机的IP地址+端口号,传输协议为TCP协议。对Binder而言,Binder可以看成Server提供的实现某个特定服务的访问接入点, Client通过这个‘地址’向Server发送请求来使用该服务;对Client而言,Binder可以看成是通向Server的管道入口,要想和某个Server通信首先必须建立这个管道并获得管道入口。

与其它IPC不同,Binder使用了面向对象的思想来描述作为访问接入点的Binder及其在Client中的入口:Binder是一个实体位于Server中的对象,该对象提供了一套方法用以实现对服务的请求,就象类的成员函数。遍布于client中的入口可以看成指向这个binder对象的‘指针’,一旦获得了这个‘指针’就可以调用该对象的方法访问server。在Client看来,通过Binder‘指针’调用其提供的方法和通过指针调用其它任何本地对象的方法并无区别,尽管前者的实体位于远端Server中,而后者实体位于本地内存中。‘指针’是C++的术语,而更通常的说法是引用,即Client通过Binder的引用访问Server。而软件领域另一个术语‘句柄’也可以用来表述Binder在Client中的存在方式。从通信的角度看,Client中的Binder也可以看作是Server Binder的‘代理’,在本地代表远端Server为Client提供服务。本文中会使用‘引用’或‘句柄’这个两广泛使用的术语。

面向对象思想的引入将进程间通信转化为通过对某个Binder对象的引用调用该对象的方法,而其独特之处在于Binder对象是一个可以跨进程引用的对象,它的实体位于一个进程中,而它的引用却遍布于系统的各个进程之中。*诱人的是,这个引用和java里引用一样既可以是强类型,也可以是弱类型,而且可以从一个进程传给其它进程,让大家都能访问同一Server,就象将一个对象或引用赋值给另一个引用一样。Binder模糊了进程边界,淡化了进程间通信过程,整个系统仿佛运行于同一个面向对象的程序之中。形形色色的Binder对象以及星罗棋布的引用仿佛粘接各个应用程序的胶水,这也是Binder在英文里的原意。

当然面向对象只是针对应用程序而言,对于Binder驱动和内核其它模块一样使用C语言实现,没有类和对象的概念。Binder驱动为面向对象的进程间通信提供底层支持。

3 Binder 通信模型

Binder框架定义了四个角色:Server,Client,ServiceManager(以后简称SMgr)以及Binder驱动。其中Server,Client,SMgr运行于用户空间,驱动运行于内核空间。这四个角色的关系和互联网类似:Server是服务器,Client是客户终端,SMgr是域名服务器(DNS),驱动是路由器。

3.1 Binder 驱动

和路由器一样,Binder驱动虽然默默无闻,却是通信的核心。尽管名叫‘驱动’,实际上和硬件设备没有任何关系,只是实现方式和设备驱动程序是一样的:它工作于内核态,提供open(),mmap(),poll(),ioctl()等标准文件操作,以字符驱动设备中的misc设备注册在设备目录/dev下,用户通过/dev/binder访问该它。驱动负责进程之间Binder通信的建立,Binder在进程之间的传递,Binder引用计数管理,数据包在进程之间的传递和交互等一系列底层支持。驱动和应用程序之间定义了一套接口协议,主要功能由ioctl()接口实现,不提供read(),write()接口,因为ioctl()灵活方便,且能够一次调用实现先写后读以满足同步交互,而不必分别调用write()和read()。Binder驱动的代码位于linux目录的drivers/misc/binder.c中。

3.2 ServiceManager 与实名Binder

和DNS类似,SMgr的作用是将字符形式的Binder名字转化成Client中对该Binder的引用,使得Client能够通过Binder名字获得对Server中Binder实体的引用。注册了名字的Binder叫实名Binder,就象每个网站除了有IP地址外还有自己的网址。Server创建了Binder实体,为其取一个字符形式,可读易记的名字,将这个Binder连同名字以数据包的形式通过Binder驱动发送给SMgr,通知SMgr注册一个名叫张三的Binder,它位于某个Server中。驱动为这个穿过进程边界的Binder创建位于内核中的实体节点以及SMgr对实体的引用,将名字及新建的引用打包传递给SMgr。SMgr收数据包后,从中取出名字和引用填入一张查找表中。

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

3.3 Client 获得实名Binder的引用

Server向SMgr注册了Binder实体及其名字后,Client就可以通过名字获得该Binder的引用了。Client也利用保留的0号引用向SMgr请求访问某个Binder:我申请获得名字叫张三的Binder的引用。SMgr收到这个连接请求,从请求数据包里获得Binder的名字,在查找表里找到该名字对应的条目,从条目中取出Binder的引用,将该引用作为回复发送给发起请求的Client。从面向对象的角度,这个Binder对象现在有了两个引用:一个位于SMgr中,一个位于发起请求的Client中。如果接下来有更多的Client请求该Binder,系统中就会有更多的引用指向该Binder,就象java里一个对象存在多个引用一样。而且类似的这些指向Binder的引用是强类型,从而确保只要有引用Binder实体就不会被释放掉。通过以上过程可以看出,SMgr象个火车票代售点,收集了所有火车的车票,可以通过它购买到乘坐各趟火车的票-得到某个Binder的引用。

3.4 匿名 Binder

并不是所有Binder都需要注册给SMgr广而告之的。Server端可以通过已经建立的Binder连接将创建的Binder实体传给Client,当然这条已经建立的Binder连接必须是通过实名Binder实现。由于这个Binder没有向SMgr注册名字,所以是个匿名Binder。Client将会收到这个匿名Binder的引用,通过这个引用向位于Server中的实体发送请求。匿名Binder为通信双方建立一条私密通道,只要Server没有把匿名Binder发给别的进程,别的进程就无法通过穷举或猜测等任何方式获得该Binder的引用,向该Binder发送请求。

下图展示了参与Binder通信的所有角色,将在以后章节中一一提到。

binder_overview

图 1 Binder通信示例

4 Binder 协议

Binder协议基本格式是(命令+数据),使用ioctl(fd, cmd, arg)函数实现交互。命令由参数cmd承载,数据由参数arg承载,随cmd不同而不同。下表列举了所有命令及其所对应的数据:

表 2 Binder通信命令字

命令 含义 arg
BINDER_WRITE_READ 该命令向Binder写入或读取数据。参数分为两段:写部分和读部分。如果write_size不为0就先将write_buffer里的数据写入Binder;如果read_size不为0再从Binder中读取数据存入read_buffer中。write_consumed和read_consumed表示操作完成时Binder驱动实际写入或读出的数据个数。 struct binder_write_read {

signed long write_size;

signed long write_consumed;

unsigned long write_buffer;

signed long read_size;

signed long read_consumed;

unsigned long read_buffer;

};

BINDER_SET_MAX_THREADS 该命令告知Binder驱动接收方(通常是Server端)线程池中*大的线程数。由于Client是并发向Server端发送请求的,Server端必须开辟线程池为这些并发请求提供服务。告知驱动线程池的*大值是为了让驱动发现线程数达到该值时不要再命令接收端启动新的线程。 int max_threads;
BINDER_SET_CONTEXT_MGR 将当前进程注册为SMgr。系统中同时只能存在一个SMgr。只要当前的SMgr没有调用close()关闭Binder驱动就不能有别的进程可以成为SMgr。
BINDER_THREAD_EXIT 通知Binder驱动当前线程退出了。Binder会为所有参与Binder通信的线程(包括Server线程池中的线程和Client发出请求的线程)建立相应的数据结构。这些线程在退出时必须通知驱动释放相应的数据结构。
BINDER_VERSION 获得Binder驱动的版本号。

这其中*常用的命令是BINDER_WRITE_READ。该命令的参数包括两部分数据:一部分是向Binder写入的数据,一部分是要从Binder读出的数据,驱动程序先处理写部分再处理读部分。这样安排的好处是应用程序可以很灵活地处理命令的同步或异步。例如若要发送异步命令可以只填入写部分而将read_size置成0;若要只从Binder获得数据可以将写部分置空即write_size置成0;若要发送请求并同步等待返回数据可以将两部分都置上。

4.1 BINDER_WRITE_READ 之写操作

Binder写操作的数据时格式同样也是(命令+数据)。这时候命令和数据都存放在binder_write_read 结构write_buffer域指向的内存空间里,多条命令可以连续存放。数据紧接着存放在命令后面,格式根据命令不同而不同。下表列举了Binder写操作支持的命令:

表 3 Binder写操作命令字

cmd 含义 arg
BC_TRANSACTION
BC_REPLY
BC_TRANSACTION用于Client向Server发送请求数据;BC_REPLY用于Server向Client发送回复(应答)数据。其后面紧接着一个binder_transaction_data结构体表明要写入的数据。 struct binder_transaction_data
BC_ACQUIRE_RESULT
BC_ATTEMPT_ACQUIRE
暂未实现
BC_FREE_BUFFER 释放一块映射的内存。Binder接收方通过mmap()映射一块较大的内存空间,Binder驱动基于这片内存采用*佳匹配算法实现接收数据缓存的动态分配和释放,满足并发请求对接收缓存区的需求。应用程序处理完这片数据后必须尽快使用该命令释放缓存区,否则会因为缓存区耗尽而无法接收新数据。 指向需要释放的缓存区的指针;该指针位于收到的Binder数据包中
BC_INCREFS
BC_ACQUIRE
BC_RELEASE
BC_DECREFS
这组命令增加或减少Binder的引用计数,用以实现强指针或弱指针的功能。 32位Binder引用号
BC_INCREFS_DONE
BC_ACQUIRE_DONE
*次增加Binder实体引用计数时,驱动向Binder实体所在的进程发送BR_INCREFS, BR_ACQUIRE消息;Binder实体所在的进程处理完毕回馈BC_INCREFS_DONE,BC_ACQUIRE_DONE void *ptr:Binder实体在用户空间中的指针

void *cookie:与该实体相关的附加数据

BC_REGISTER_LOOPER
BC_ENTER_LOOPER
BC_EXIT_LOOPER
这组命令同BINDER_SET_MAX_THREADS一道实现Binder驱动对接收方线程池管理。BC_REGISTER_LOOPER通知驱动线程池中一个线程已经创建了;BC_ENTER_LOOPER通知驱动该线程已经进入主循环,可以接收数据;BC_EXIT_LOOPER通知驱动该线程退出主循环,不再接收数据。
BC_REQUEST_DEATH_NOTIFICATION 获得Binder引用的进程通过该命令要求驱动在Binder实体销毁得到通知。虽说强指针可以确保只要有引用就不会销毁实体,但这毕竟是个跨进程的引用,谁也无法保证实体由于所在的Server关闭Binder驱动或异常退出而消失,引用者能做的是要求Server在此刻给出通知。 uint32 *ptr; 需要得到死亡通知的Binder引用

void **cookie: 与死亡通知相关的信息,驱动会在发出死亡通知时返回给发出请求的进程。

BC_DEAD_BINDER_DONE 收到实体死亡通知书的进程在删除引用后用本命令告知驱动。 void **cookie

在这些命令中,*常用的是BC_TRANSACTION/BC_REPLY命令对,Binder请求和应答数据就是通过这对命令发送给接收方。这对命令所承载的数据包由结构体struct binder_transaction_data定义。Binder交互有同步和异步之分,利用binder_transaction_data中flag域区分。如果flag域的TF_ONE_WAY位为1则为异步交互,即Client端发送完请求交互即结束, Server端不再返回BC_REPLY数据包;否则Server会返回BC_REPLY数据包,Client端必须等待接收完该数据包方才完成一次交互。

4.2 BINDER_WRITE_READ :从Binder读出数据

从Binder里读出的数据格式和向Binder中写入的数据格式一样,采用(消息ID+数据)形式,并且多条消息可以连续存放。下表列举了从Binder读出的命令字及其相应的参数:

表 4 Binder读操作消息ID

消息 含义 参数
BR_ERROR 发生内部错误(如内存分配失败)
BR_OK
BR_NOOP
操作完成
BR_SPAWN_LOOPER 该消息用于接收方线程池管理。当驱动发现接收方所有线程都处于忙碌状态且线程池里的线程总数没有超过BINDER_SET_MAX_THREADS设置的*大线程数时,向接收方发送该命令要求创建更多线程以备接收数据。
BR_TRANSACTION
BR_REPLY
这两条消息分别对应发送方的BC_TRANSACTION和BC_REPLY,表示当前接收的数据是请求还是回复。 binder_transaction_data
BR_ACQUIRE_RESULT
BR_ATTEMPT_ACQUIRE
BR_FINISHED
尚未实现
BR_DEAD_REPLY 交互过程中如果发现对方进程或线程已经死亡则返回该消息
BR_TRANSACTION_COMPLETE 发送方通过BC_TRANSACTION或BC_REPLY发送完一个数据包后,都能收到该消息做为成功发送的反馈。这和BR_REPLY不一样,是驱动告知发送方已经发送成功,而不是Server端返回请求数据。所以不管同步还是异步交互接收方都能获得本消息。
BR_INCREFS
BR_ACQUIRE
BR_RELEASE
BR_DECREFS
这一组消息用于管理强/弱指针的引用计数。只有提供Binder实体的进程才能收到这组消息。 void *ptr:Binder实体在用户空间中的指针

void *cookie:与该实体相关的附加数据

BR_DEAD_BINDER
BR_CLEAR_DEATH_NOTIFICATION_DONE
向获得Binder引用的进程发送Binder实体死亡通知书;收到死亡通知书的进程接下来会返回BC_DEAD_BINDER_DONE做确认。 void **cookie:在使用BC_REQUEST_DEATH_NOTIFICATION注册死亡通知时的附加参数。
BR_FAILED_REPLY 如果发送非法引用号则返回该消息

和写数据一样,其中*重要的消息是BR_TRANSACTION 或BR_REPLY,表明收到了一个格式为binder_transaction_data的请求数据包(BR_TRANSACTION)或返回数据包(BR_REPLY)。

4.3 struct binder_transaction_data :收发数据包结构

该结构是Binder接收/发送数据包的标准格式,每个成员定义如下:

表 5 Binder收发数据包结构:binder_transaction_data

成员 含义
union {

size_t handle;

void *ptr;

} target;

对于发送数据包的一方,该成员指明发送目的地。由于目的是在远端,所以这里填入的是对Binder实体的引用,存放在target.handle中。如前述,Binder的引用在代码中也叫句柄(handle)。

当数据包到达接收方时,驱动已将该成员修改成Binder实体,即指向Binder对象内存的指针,使用target.ptr来获得。该指针是接收方在将Binder实体传输给其它进程时提交给驱动的,驱动程序能够自动将发送方填入的引用转换成接收方Binder对象的指针,故接收方可以直接将其当做对象指针来使用(通常是将其reinterpret_cast成相应类)。

void *cookie; 发送方忽略该成员;接收方收到数据包时,该成员存放的是创建Binder实体时由该接收方自定义的任意数值,做为与Binder指针相关的额外信息存放在驱动中。驱动基本上不关心该成员。
unsigned int code; 该成员存放收发双方约定的命令码,驱动完全不关心该成员的内容。通常是Server端定义的公共接口函数的编号。
unsigned int flags; 与交互相关的标志位,其中*重要的是TF_ONE_WAY位。如果该位置上表明这次交互是异步的,Server端不会返回任何数据。驱动利用该位来决定是否构建与返回有关的数据结构。另外一位TF_ACCEPT_FDS是出于安全考虑,如果发起请求的一方不希望在收到的回复中接收文件形式的Binder可以将该位置上。因为收到一个文件形式的Binder会自动为数据接收方打开一个文件,使用该位可以防止打开文件过多。
pid_t sender_pid;

uid_t sender_euid;

该成员存放发送方的进程ID和用户ID,由驱动负责填入,接收方可以读取该成员获知发送方的身份。
size_t data_size; 该成员表示data.buffer指向的缓冲区存放的数据长度。发送数据时由发送方填入,表示即将发送的数据长度;在接收方用来告知接收到数据的长度。
size_t offsets_size; 驱动一般情况下不关心data.buffer里存放什么数据,但如果有Binder在其中传输则需要将其相对data.buffer的偏移位置指出来让驱动知道。有可能存在多个Binder同时在数据中传递,所以须用数组表示所有偏移位置。本成员表示该数组的大小。
union {

struct {

const void *buffer;

const void *offsets;

} ptr;

uint8_t buf[8];

} data;

data.bufer存放要发送或接收到的数据;data.offsets指向Binder偏移位置数组,该数组可以位于data.buffer中,也可以在另外的内存空间中,并无限制。buf[8]是为了无论保证32位还是64位平台,成员data的大小都是8个字节。

这里有必要再强调一下offsets_size和data.offsets两个成员,这是Binder通信有别于其它IPC的地方。如前述,Binder采用面向对象的设计思想,一个Binder实体可以发送给其它进程从而建立许多跨进程的引用;另外这些引用也可以在进程之间传递,就象java里将一个引用赋给另一个引用一样。为Binder在不同进程中建立引用必须有驱动的参与,由驱动在内核创建并注册相关的数据结构后接收方才能使用该引用。而且这些引用可以是强类型,需要驱动为其维护引用计数。然而这些跨进程传递的Binder混杂在应用程序发送的数据包里,数据格式由用户定义,如果不把它们一一标记出来告知驱动,驱动将无法从数据中将它们提取出来。于是就使用数组data.offsets存放用户数据中每个Binder相对data.buffer的偏移量,用offsets_size表示这个数组的大小。驱动在发送数据包时会根据data.offsets和offset_size将散落于data.buffer中的Binder找出来并一一为它们创建相关的数据结构。在数据包中传输的Binder是类型为struct flat_binder_object的结构体,详见后文。

对于接收方来说,该结构只相当于一个定长的消息头,真正的用户数据存放在data.buffer所指向的缓存区中。如果发送方在数据中内嵌了一个或多个Binder,接收到的数据包中同样会用data.offsets和offset_size指出每个Binder的位置和总个数。不过通常接收方可以忽略这些信息,因为接收方是知道数据格式的,参考双方约定的格式定义就能知道这些Binder在什么位置。

binder_proto

图 2 BINDER_WRITE_READ数据包实例

5 Binder 的表述

考察一次Binder通信的全过程会发现,Binder存在于系统以下几个部分中:

· 应用程序进程:分别位于Server进程和Client进程中

· Binder驱动:分别管理为Server端的Binder实体和Client端的引用

· 传输数据:由于Binder可以跨进程传递,需要在传输数据中予以表述

在系统不同部分,Binder实现的功能不同,表现形式也不一样。接下来逐一探讨Binder在各部分所扮演的角色和使用的数据结构。

5.1 Binder 在应用程序中的表述

虽然Binder用到了面向对象的思想,但并不限制应用程序一定要使用面向对象的语言,无论是C语言还是C++语言都可以很容易的使用Binder来通信。例如尽管Android主要使用java/C++,象SMgr这么重要的进程就是用C语言实现的。不过面向对象的方式表述起来更方便,所以本文假设应用程序是用面向对象语言实现的。

Binder本质上只是一种底层通信方式,和具体服务没有关系。为了提供具体服务,Server必须提供一套接口函数以便Client通过远程访问使用各种服务。这时通常采用Proxy设计模式:将接口函数定义在一个抽象类中,Server和Client都会以该抽象类为基类实现所有接口函数,所不同的是Server端是真正的功能实现,而Client端是对这些函数远程调用请求的包装。如何将Binder和Proxy设计模式结合起来是应用程序实现面向对象Binder通信的根本问题。

5.1.1 Binder 在Server端的表述 – Binder实体

做为Proxy设计模式的基础,首先定义一个抽象接口类封装Server所有功能,其中包含一系列纯虚函数留待Server和Proxy各自实现。由于这些函数需要跨进程调用,须为其一一编号,从而Server可以根据收到的编号决定调用哪个函数。其次就要引入Binder了。Server端定义另一个Binder抽象类处理来自Client的Binder请求数据包,其中*重要的成员是虚函数onTransact()。该函数分析收到的数据包,调用相应的接口函数处理请求。

接下来采用继承方式以接口类和Binder抽象类为基类构建Binder在Server中的实体,实现基类里所有的虚函数,包括公共接口函数以及数据包处理函数:onTransact()。这个函数的输入是来自Client的binder_transaction_data结构的数据包。前面提到,该结构里有个成员code,包含这次请求的接口函数编号。onTransact()将case-by-case地解析code值,从数据包里取出函数参数,调用接口类中相应的,已经实现的公共接口函数。函数执行完毕,如果需要返回数据就再构建一个binder_transaction_data包将返回数据包填入其中。

那么各个Binder实体的onTransact()又是什么时候调用呢?这就需要驱动参与了。前面说过,Binder实体须要以Binde传输结构flat_binder_object形式发送给其它进程才能建立Binder通信,而Binder实体指针就存放在该结构的handle域中。驱动根据Binder位置数组从传输数据中获取该Binder的传输结构,为它创建位于内核中的Binder节点,将Binder实体指针记录在该节点中。如果接下来有其它进程向该Binder发送数据,驱动会根据节点中记录的信息将Binder实体指针填入binder_transaction_data的target.ptr中返回给接收线程。接收线程从数据包中取出该指针,reinterpret_cast成Binder抽象类并调用onTransact()函数。由于这是个虚函数,不同的Binder实体中有各自的实现,从而可以调用到不同Binder实体提供的onTransact()。

5.1.2 Binder 在Client端的表述 – Binder引用

做为Proxy设计模式的一部分,Client端的Binder同样要继承Server提供的公共接口类并实现公共函数。但这不是真正的实现,而是对远程函数调用的包装:将函数参数打包,通过Binder向Server发送申请并等待返回值。为此Client端的Binder还要知道Binder实体的相关信息,即对Binder实体的引用。该引用或是由SMgr转发过来的,对实名Binder的引用或是由另一个进程直接发送过来的,对匿名Binder的引用。

由于继承了同样的公共接口类,Client Binder提供了与Server Binder一样的函数原型,使用户感觉不出Server是运行在本地还是远端。Client Binder中,公共接口函数的包装方式是:创建一个binder_transaction_data数据包,将其对应的编码填入code域,将调用该函数所需的参数填入data.buffer指向的缓存中,并指明数据包的目的地,那就是已经获得的对Binder实体的引用,填入数据包的target.handle中。注意这里和Server的区别:实际上target域是个联合体,包括ptr和handle两个成员,前者用于接收数据包的Server,指向 Binder实体对应的内存空间;后者用于作为请求方的Client,存放Binder实体的引用,告知驱动数据包将路由给哪个实体。数据包准备好后,通过驱动接口发送出去。经过BC_TRANSACTION/BC_REPLY回合完成函数的远程调用并得到返回值。

5.2 Binder 在传输数据中的表述

Binder可以塞在数据包的有效数据中越进程边界从一个进程传递给另一个进程,这些传输中的Binder用结构flat_binder_object表示,如下表所示:

表 6 Binder传输结构:flat_binder_object

成员 含义
unsigned long type 表明该Binder的类型,包括以下几种:

BINDER_TYPE_BINDER:表示传递的是Binder实体,并且指向该实体的引用都是强类型;

BINDER_TYPE_WEAK_BINDER:表示传递的是Binder实体,并且指向该实体的引用都是弱类型;

BINDER_TYPE_HANDLE:表示传递的是Binder强类型的引用

BINDER_TYPE_WEAK_HANDLE:表示传递的是Binder弱类型的引用

BINDER_TYPE_FD:表示传递的是文件形式的Binder,详见下节

unsigned long flags 该域只对*次传递Binder实体时有效,因为此刻驱动需要在内核中创建相应的实体节点,有些参数需要从该域取出:

第0-7位:代码中用FLAT_BINDER_FLAG_PRIORITY_MASK取得,表示处理本实体请求数据包的线程的*低优先级。当一个应用程序提供多个实体时,可以通过该参数调整分配给各个实体的处理能力。

第8位:代码中用FLAT_BINDER_FLAG_ACCEPTS_FDS取得,置1表示该实体可以接收其它进程发过来的文件形式的Binder。由于接收文件形式的Binder会在本进程中自动打开文件,有些Server可以用该标志禁止该功能,以防打开过多文件。

union {

void *binder;

signed long handle;

};

当传递的是Binder实体时使用binder域,指向Binder实体在应用程序中的地址。

当传递的是Binder引用时使用handle域,存放Binder在进程中的引用号。

void *cookie; 该域只对Binder实体有效,存放与该Binder有关的附加信息。

无论是Binder实体还是对实体的引用都从属与某个进程,所以该结构不能透明地在进程之间传输,必须经过驱动翻译。例如当Server把Binder实体传递给Client时,在发送数据流中,flat_binder_object中的type是BINDER_TYPE_BINDER,binder指向Server进程用户空间地址。如果透传给接收端将毫无用处,驱动必须对数据流中的这个Binder做修改:将type该成BINDER_TYPE_HANDLE;为这个Binder在接收进程中创建位于内核中的引用并将引用号填入handle中。对于发生数据流中引用类型的Binder也要做同样转换。经过处理后接收进程从数据流中取得的Binder引用才是有效的,才可以将其填入数据包binder_transaction_data的target.handle域,向Binder实体发送请求。

这样做也是出于安全性考虑:应用程序不能随便猜测一个引用号填入target.handle中就可以向Server请求服务了,因为驱动并没有为你在内核中创建该引用,必定会被驱动拒*。唯有经过身份认证确认合法后,由‘权威机构’(Binder驱动)亲手授予你的Binder才能使用,因为这时驱动已经在内核中为你使用该Binder做了注册,交给你的引用号是合法的。

下表总结了当flat_binder_object结构穿过驱动时驱动所做的操作:

表 7 驱动对flat_binder_object的操作

Binder 类型( type域) 在发送方的操作 在接收方的操作
BINDER_TYPE_BINDER

BINDER_TYPE_WEAK_BINDER

只有实体所在的进程能发送该类型的Binder。如果是*次发送驱动将创建实体在内核中的节点,并保存binder,cookie,flag域。 如果是*次接收该Binder则创建实体在内核中的引用;将handle域替换为新建的引用号;将type域替换为BINDER_TYPE_(WEAK_)HANDLE
BINDER_TYPE_HANDLE

BINDER_TYPE_WEAK_HANDLE

获得Binder引用的进程都能发送该类型Binder。驱动根据handle域提供的引用号查找建立在内核的引用。如果找到说明引用号合法,否则拒*该发送请求。 如果收到的Binder实体位于接收进程中:将ptr域替换为保存在节点中的binder值;cookie替换为保存在节点中的cookie值;type替换为BINDER_TYPE_(WEAK_)BINDER。

如果收到的Binder实体不在接收进程中:如果是*次接收则创建实体在内核中的引用;将handle域替换为新建的引用号

BINDER_TYPE_FD 验证handle域中提供的打开文件号是否有效,无效则拒*该发送请求。 在接收方创建新的打开文件号并将其与提供的打开文件描述结构绑定。
5.2.1 文件形式的 Binder

除了通常意义上用来通信的Binder,还有一种特殊的Binder:文件Binder。这种Binder的基本思想是:将文件看成Binder实体,进程打开的文件号看成Binder的引用。一个进程可以将它打开文件的文件号传递给另一个进程,从而另一个进程也打开了同一个文件,就象Binder的引用在进程之间传递一样。

一个进程打开一个文件,就获得与该文件绑定的打开文件号。从Binder的角度,linux在内核创建的打开文件描述结构struct file是Binder的实体,打开文件号是该进程对该实体的引用。既然是Binder那么就可以在进程之间传递,故也可以用flat_binder_object结构将文件Binder通过数据包发送至其它进程,只是结构中type域的值为BINDER_TYPE_FD,表明该Binder是文件Binder。而结构中的handle域则存放文件在发送方进程中的打开文件号。我们知道打开文件号是个局限于某个进程的值,一旦跨进程就没有意义了。这一点和Binder实体用户指针或Binder引用号是一样的,若要跨进程同样需要驱动做转换。驱动在接收Binder的进程空间创建一个新的打开文件号,将它与已有的打开文件描述结构struct file勾连上,从此该Binder实体又多了一个引用。新建的打开文件号覆盖flat_binder_object中原来的文件号交给接收进程。接收进程利用它可以执行read(),write()等文件操作。

传个文件为啥要这么麻烦,直接将文件名用Binder传过去,接收方用open()打开不就行了吗?其实这还是有区别的。首先对同一个打开文件共享的层次不同:使用文件Binder打开的文件共享linux VFS中的struct file,struct dentry,struct inode结构,这意味着一个进程使用read()/write()/seek()改变了文件指针,另一个进程的文件指针也会改变;而如果两个进程分别使用同一文件名打开文件则有各自的struct file结构,从而各自独立维护文件指针,互不干扰。其次是一些特殊设备文件要求在struct file一级共享才能使用,例如android的另一个驱动ashmem,它和Binder一样也是misc设备,用以实现进程间的共享内存。一个进程打开的ashmem文件只有通过文件Binder发送到另一个进程才能实现内存共享,这大大提高了内存共享的安全性,道理和Binder增强了IPC的安全性是一样的。

5.3 Binder 在驱动中的表述

驱动是Binder通信的核心,系统中所有的Binder实体以及每个实体在各个进程中的引用都登记在驱动中;驱动需要记录Binder引用->实体之间多对一的关系;为引用找到对应的实体;在某个进程中为实体创建或查找到对应的引用;记录Binder的归属地(位于哪个进程中);通过管理Binder的强/弱引用创建/销毁Binder实体等等。

驱动里的Binder是什么时候创建的呢?前面提到过,为了实现实名Binder的注册,系统必须创建*只鸡–为SMgr创建的,用于注册实名Binder的Binder实体,负责实名Binder注册过程中的进程间通信。既然创建了实体就要有对应的引用:驱动将所有进程中的0号引用都预留给该Binder实体,即所有进程的0号引用天然地都指向注册实名Binder专用的Binder,无须特殊操作即可以使用0号引用来注册实名Binder。接下来随着应用程序不断地注册实名Binder,不断向SMgr索要Binder的引用,不断将Binder从一个进程传递给另一个进程,越来越多的Binder以传输结构 – flat_binder_object的形式穿越驱动做跨进程的迁徙。由于binder_transaction_data中data.offset数组的存在,所有流经驱动的Binder都逃不过驱动的眼睛。Binder将对这些穿越进程边界的Binder做如下操作:检查传输结构的type域,如果是BINDER_TYPE_BINDER或BINDER_TYPE_WEAK_BINDER则创建Binder的实体;如果是BINDER_TYPE_HANDLE或BINDER_TYPE_WEAK_HANDLE则创建Binder的引用;如果是BINDER_TYPE_HANDLE则为进程打开文件,无须创建任何数据结构。详细过程可参考表7。随着越来越多的Binder实体或引用在进程间传递,驱动会在内核里创建越来越多的节点或引用,当然这个过程对用户来说是透明的。

5.3.1 Binder 实体在驱动中的表述

驱动中的Binder实体也叫‘节点’,隶属于提供实体的进程,由struct binder_node结构来表示:

表 8 Binder节点描述结构:binder_node

成员 含义
int debug_id; 用于调试
struct binder_work work; 当本节点引用计数发生改变,需要通知所属进程时,通过该成员挂入所属进程的to-do队列里,唤醒所属进程执行Binder实体引用计数的修改。
union {

struct rb_node rb_node;

struct hlist_node dead_node;

};

每个进程都维护一棵红黑树,以Binder实体在用户空间的指针,即本结构的ptr成员为索引存放该进程所有的Binder实体。这样驱动可以根据Binder实体在用户空间的指针很快找到其位于内核的节点。rb_node用于将本节点链入该红黑树中。

销毁节点时须将rb_node从红黑树中摘除,但如果本节点还有引用没有切断,就用dead_node将节点隔离到另一个链表中,直到通知所有进程切断与该节点的引用后,该节点才可能被销毁。

struct binder_proc *proc; 本成员指向节点所属的进程,即提供该节点的进程。
struct hlist_head refs; 本成员是队列头,所有指向本节点的引用都链接在该队列里。这些引用可能隶属于不同的进程。通过该队列可以遍历指向该节点的所有引用。
int internal_strong_refs; 用以实现强指针的计数器:产生一个指向本节点的强引用该计数就会加1。
int local_weak_refs; 驱动为传输中的Binder设置的弱引用计数。如果一个Binder打包在数据包中从一个进程发送到另一个进程,驱动会为该Binder增加引用计数,直到接收进程通过BC_FREE_BUFFER通知驱动释放该数据包的数据区为止。
int local_strong_refs; 驱动为传输中的Binder设置的强引用计数。同上。
void __user *ptr; 指向用户空间Binder实体的指针,来自于flat_binder_object的binder成员
void __user *cookie; 指向用户空间的附加指针,来自于flat_binder_object的cookie成员
unsigned has_strong_ref;

unsigned pending_strong_ref;

unsigned has_weak_ref;

unsigned pending_weak_ref

这一组标志用于控制驱动与Binder实体所在进程交互式修改引用计数
unsigned has_async_transaction; 该成员表明该节点在to-do队列中有异步交互尚未完成。驱动将所有发送往接收端的数据包暂存在接收进程或线程开辟的to-do队列里。对于异步交互,驱动做了适当流控:如果to-do队列里有异步交互尚待处理则该成员置1,这将导致新到的异步交互存放在本结构成员 – asynch_todo队列中,而不直接送到to-do队列里。目的是为同步交互让路,避免长时间阻塞发送端。
unsigned accept_fds 表明节点是否同意接受文件方式的Binder,来自flat_binder_object中flags成员的FLAT_BINDER_FLAG_ACCEPTS_FDS位。由于接收文件Binder会为进程自动打开一个文件,占用有限的文件描述符,节点可以设置该位拒*这种行为。
int min_priority 设置处理Binder请求的线程的*低优先级。发送线程将数据提交给接收线程处理时,驱动会将发送线程的优先级也赋予接收线程,使得数据即使跨了进程也能以同样优先级得到处理。不过如果发送线程优先级过低,接收线程将以预设的*小值运行。

该域的值来自于flat_binder_object中flags成员。

struct list_head async_todo 异步交互等待队列;用于分流发往本节点的异步交互包

每个进程都有一棵红黑树用于存放创建好的节点,以Binder在用户空间的指针作为索引。每当在传输数据中侦测到一个代表Binder实体的flat_binder_object,先以该结构的binder指针为索引搜索红黑树;如果没找到就创建一个新节点添加到树中。由于对于同一个进程来说内存地址是唯一的,所以不会重复建设造成混乱。

5.3.2 Binder 引用在驱动中的表述

和实体一样,Binder的引用也是驱动根据传输数据中的flat_binder_object创建的,隶属于获得该引用的进程,用struct binder_ref结构体表示:

表 9 Binder引用描述结构:binder_ref

成员 含义
int debug_id; 调试用
struct rb_node rb_node_desc; 每个进程有一棵红黑树,进程所有引用以引用号(即本结构的desc域)为索引添入该树中。本成员用做链接到该树的一个节点。
struct rb_node rb_node_node; 每个进程又有一棵红黑树,进程所有引用以节点实体在驱动中的内存地址(即本结构的node域)为所引添入该树中。本成员用做链接到该树的一个节点。
struct hlist_node node_entry; 该域将本引用做为节点链入所指向的Binder实体结构binder_node中的refs队列
struct binder_proc *proc; 本引用所属的进程
struct binder_node *node; 本引用所指向的节点(Binder实体)
uint32_t desc; 本结构的引用号
int strong; 强引用计数
int weak; 弱引用计数
struct binder_ref_death *death; 应用程序向驱动发送BC_REQUEST_DEATH_NOTIFICATION或BC_CLEAR_DEATH_NOTIFICATION命令从而当Binder实体销毁时能够收到来自驱动的提醒。该域不为空表明用户订阅了对应实体销毁的‘噩耗’。

就象一个对象有很多指针一样,同一个Binder实体可能有很多引用,不同的是这些引用可能分布在不同的进程中。和实体一样,每个进程使用红黑树存放所有正在使用的引用。不同的是Binder的引用可以通过两个键值索引:

· 对应实体在内核中的地址。注意这里指的是驱动创建于内核中的binder_node结构的地址,而不是Binder实体在用户进程中的地址。实体在内核中的地址是唯一的,用做索引不会产生二义性;但实体可能来自不同用户进程,而实体在不同用户进程中的地址可能重合,不能用来做索引。驱动利用该红黑树在一个进程中快速查找某个Binder实体所对应的引用(一个实体在一个进程中只建立一个引用)。

· 引用号。引用号是驱动为引用分配的一个32位标识,在一个进程内是唯一的,而在不同进程中可能会有同样的值,这和进程的打开文件号很类似。引用号将返回给应用程序,可以看作Binder引用在用户进程中的句柄。除了0号引用在所有进程里都固定保留给SMgr,其它值由驱动动态分配。向Binder发送数据包时,应用程序将引用号填入binder_transaction_data结构的target.handle域中表明该数据包的目的Binder。驱动根据该引用号在红黑树中找到引用的binder_ref结构,进而通过其node域知道目标Binder实体所在的进程及其它相关信息,实现数据包的路由。

6 Binder 内存映射和接收缓存区管理

暂且撇开Binder,考虑一下传统的IPC方式中,数据是怎样从发送端到达接收端的呢?通常的做法是,发送方将准备好的数据存放在缓存区中,调用API通过系统调用进入内核中。内核服务程序在内核空间分配内存,将数据从发送方缓存区复制到内核缓存区中。接收方读数据时也要提供一块缓存区,内核将数据从内核缓存区拷贝到接收方提供的缓存区中并唤醒接收线程,完成一次数据发送。这种存储-转发机制有两个缺陷:首先是效率低下,需要做两次拷贝:用户空间->内核空间->用户空间。Linux使用copy_from_user()和copy_to_user()实现这两个跨空间拷贝,在此过程中如果使用了高端内存(high memory),这种拷贝需要临时建立/取消页面映射,造成性能损失。其次是接收数据的缓存要由接收方提供,可接收方不知道到底要多大的缓存才够用,只能开辟尽量大的空间或先调用API接收消息头获得消息体大小,再开辟适当的空间接收消息体。两种做法都有不足,不是浪费空间就是浪费时间。

Binder采用一种全新策略:由Binder驱动负责管理数据接收缓存。我们注意到Binder驱动实现了mmap()系统调用,这对字符设备是比较特殊的,因为mmap()通常用在有物理存储介质的文件系统上,而象Binder这样没有物理介质,纯粹用来通信的字符设备没必要支持mmap()。Binder驱动当然不是为了在物理介质和用户空间做映射,而是用来创建数据接收的缓存空间。先看mmap()是如何使用的:

fd = open(“/dev/binder”, O_RDWR);

mmap(NULL, MAP_SIZE, PROT_READ, MAP_PRIVATE, fd, 0);

这样Binder的接收方就有了一片大小为MAP_SIZE的接收缓存区。mmap()的返回值是内存映射在用户空间的地址,不过这段空间是由驱动管理,用户不必也不能直接访问(映射类型为PROT_READ,只读映射)。

接收缓存区映射好后就可以做为缓存池接收和存放数据了。前面说过,接收数据包的结构为binder_transaction_data,但这只是消息头,真正的有效负荷位于data.buffer所指向的内存中。这片内存不需要接收方提供,恰恰是来自mmap()映射的这片缓存池。在数据从发送方向接收方拷贝时,驱动会根据发送数据包的大小,使用*佳匹配算法从缓存池中找到一块大小合适的空间,将数据从发送缓存区复制过来。要注意的是,存放binder_transaction_data结构本身以及表4中所有消息的内存空间还是得由接收者提供,但这些数据大小固定,数量也不多,不会给接收方造成不便。映射的缓存池要足够大,因为接收方的线程池可能会同时处理多条并发的交互,每条交互都需要从缓存池中获取目的存储区,一旦缓存池耗竭将产生导致无法预期的后果。

有分配必然有释放。接收方在处理完数据包后,就要通知驱动释放data.buffer所指向的内存区。在介绍Binder协议时已经提到,这是由命令BC_FREE_BUFFER完成的。

通过上面介绍可以看到,驱动为接收方分担了*为繁琐的任务:分配/释放大小不等,难以预测的有效负荷缓存区,而接收方只需要提供缓存来存放大小固定,*大空间可以预测的消息头即可。在效率上,由于mmap()分配的内存是映射在接收方用户空间里的,所有总体效果就相当于对有效负荷数据做了一次从发送方用户空间到接收方用户空间的直接数据拷贝,省去了内核中暂存这个步骤,提升了一倍的性能。顺便再提一点,Linux内核实际上没有从一个用户空间到另一个用户空间直接拷贝的函数,需要先用copy_from_user()拷贝到内核空间,再用copy_to_user()拷贝到另一个用户空间。为了实现用户空间到用户空间的拷贝,mmap()分配的内存除了映射进了接收方进程里,还映射进了内核空间。所以调用copy_from_user()将数据拷贝进内核空间也相当于拷贝进了接收方的用户空间,这就是Binder只需一次拷贝的‘秘密’。

7 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不是线程池中的线程还可以为线程池分担部分工作,减少线程池使用率。

8 数据包接收队列与(线程)等待队列管理

通常数据传输的接收端有两个队列:数据包接收队列和(线程)等待队列,用以缓解供需矛盾。当超市里的进货(数据包)太多,货物会堆积在仓库里;购物的人(线程)太多,会排队等待在收银台,道理是一样的。在驱动中,每个进程有一个全局的接收队列,也叫to-do队列,存放不是发往特定线程的数据包;相应地有一个全局等待队列,所有等待从全局接收队列里收数据的线程在该队列里排队。每个线程有自己私有的to-do队列,存放发送给该线程的数据包;相应的每个线程都有各自私有等待队列,专门用于本线程等待接收自己to-do队列里的数据。虽然名叫队列,其实线程私有等待队列中*多只有一个线程,即它自己。

由于发送时没有特别标记,驱动怎么判断哪些数据包该送入全局to-do队列,哪些数据包该送入特定线程的to-do队列呢?这里有两条规则。规则1:Client发给Server的请求数据包都提交到Server进程的全局to-do队列。不过有个特例,就是上节谈到的Binder对工作线程启动的优化。经过优化,来自T1的请求不是提交给P2的全局to-do队列,而是送入了T2的私有to-do队列。规则2:对同步请求的返回数据包(由BC_REPLY发送的包)都发送到发起请求的线程的私有to-do队列中。如上面的例子,如果进程P1的线程T1发给进程P2的线程T2的是同步请求,那么T2返回的数据包将送进T1的私有to-do队列而不会提交到P1的全局to-do队列。

数据包进入接收队列的潜规则也就决定了线程进入等待队列的潜规则,即一个线程只要不接收返回数据包则应该在全局等待队列中等待新任务,否则就应该在其私有等待队列中等待Server的返回数据。还是上面的例子,T1在向T2发送同步请求后就必须等待在它私有等待队列中,而不是在P1的全局等待队列中排队,否则将得不到T2的返回的数据包。

这些潜规则是驱动对Binder通信双方施加的限制条件,体现在应用程序上就是同步请求交互过程中的线程一致性:1) Client端,等待返回包的线程必须是发送请求的线程,而不能由一个线程发送请求包,另一个线程等待接收包,否则将收不到返回包;2) Server端,发送对应返回数据包的线程必须是收到请求数据包的线程,否则返回的数据包将无法送交发送请求的线程。这是因为返回数据包的目的Binder不是用户指定的,而是驱动记录在收到请求数据包的线程里,如果发送返回包的线程不是收到请求包的线程驱动将无从知晓返回包将送往何处。

接下来探讨一下Binder驱动是如何递交同步交互和异步交互的。我们知道,同步交互和异步交互的区别是同步交互的请求端(client)在发出请求数据包后须要等待应答端(Server)的返回数据包,而异步交互的发送端发出请求数据包后交互即结束。对于这两种交互的请求数据包,驱动可以不管三七二十一,统统丢到接收端的to-do队列中一个个处理。但驱动并没有这样做,而是对异步交互做了限流,令其为同步交互让路,具体做法是:对于某个Binder实体,只要有一个异步交互没有处理完毕,例如正在被某个线程处理或还在任意一条to-do队列中排队,那么接下来发给该实体的异步交互包将不再投递到to-do队列中,而是阻塞在驱动为该实体开辟的异步交互接收队列(Binder节点的async_todo域)中,但这期间同步交互依旧不受限制直接进入to-do队列获得处理。一直到该异步交互处理完毕下一个异步交互方可以脱离异步交互队列进入to-do队列中。之所以要这么做是因为同步交互的请求端需要等待返回包,必须迅速处理完毕以免影响请求端的响应速度,而异步交互属于‘发射后不管’,稍微延时一点不会阻塞其它线程。所以用专门队列将过多的异步交互暂存起来,以免突发大量异步交互挤占Server端的处理能力或耗尽线程池里的线程,进而阻塞同步交互。

9 总结

Binder使用Client-Server通信方式,安全性好,简单高效,再加上其面向对象的设计思想,独特的接收缓存管理和线程池管理方式,成为Android进程间通信的中流砥柱。

图文详解 Android Binder跨进程通信机制 原理

图文详解 Android Binder跨进程通信机制 原理

目录

%title插图%num
目录


1. Binder到底是什么?

  • 中文即 粘合剂,意思为粘合了两个不同的进程
  • 网上有很多对Binder的定义,但都说不清楚:Binder是跨进程通信方式、它实现了IBinder接口,是连接 ServiceManager的桥梁blabla,估计大家都看晕了,没法很好的理解
  • 我认为:对于Binder的定义,在不同场景下其定义不同

%title插图%num
定义

在本文的讲解中,按照 大角度 -> 小角度 去分析Binder,即:

  • 先从 机制、模型的角度 去分析 整个Binder跨进程通信机制的模型

    其中,会详细分析模型组成中的 Binder驱动

  • 再 从源码实现角度,分析 Binder在 Android中的具体实现

从而全方位地介绍 Binder,希望你们会喜欢。


2. 知识储备

在讲解Binder前,我们先了解一些基础知识

2.1 进程空间分配

  • 一个进程空间分为 用户空间 & 内核空间(Kernel),即把进程内 用户 & 内核 隔离开来
  • 二者区别:
    1. 进程间,用户空间的数据不可共享,所以用户空间 = 不可共享空间
    2. 进程间,内核空间的数据可共享,所以内核空间 = 可共享空间
  • 进程内 用户 与 内核 进行交互 称为系统调用

%title插图%num
示意图

2.2 进程隔离

为了保证 安全性 & 独立性,一个进程 不能直接操作或者访问另一个进程,即Android的进程是相互独立、隔离的

2.3 跨进程通信( IPC )

  • 隔离后,由于某些需求,进程间 需要合作 / 交互
  • 跨进程间通信的原理
    1. 先通过 进程间 的内核空间进行 数据交互
    2. 再通过 进程内 的用户空间 & 内核空间进行 数据交互,从而实现 进程间的用户空间 的数据交互

%title插图%num
示意图

Binder,就是充当 连接 两个进程(内核空间)的通道。


3. Binder 跨进程通信机制 模型

3.1 模型原理

Binder 跨进程通信机制 模型 基于 Client - Server 模式,模型原理图如下:

相信我,一张图就能解决问题

%title插图%num
示意图

3.2 额外说明

说明1:Client进程、Server进程 & Service Manager 进程之间的交互都必须通过Binder驱动(使用 open 和 ioctl文件操作函数),而非直接交互 **

原因:

  1. Client进程、Server进程 & Service Manager进程属于进程空间的用户空间,不可进行进程间交互
  2. Binder驱动 属于 进程空间的 内核空间,可进行进程间 & 进程内交互

所以,原理图可表示为以下:

虚线表示并非直接交互

%title插图%num
示意图

说明2: Binder驱动 & Service Manager进程 属于 Android基础架构(即系统已经实现好了);而Client 进程 和 Server 进程 属于Android应用层(需要开发者自己实现)

所以,在进行跨进程通信时,开发者只需自定义Client & Server 进程 并 显式使用上述3个步骤,*终借助 Android的基本架构功能就可完成进程间通信

%title插图%num
示意图

说明3:Binder请求的线程管理
  • Server进程会创建很多线程来处理Binder请求
  • 管理Binder模型的线程是采用Binder驱动的线程池,并由Binder驱动自身进行管理

    而不是由Server进程来管理的

  • 一个进程的Binder线程数默认*大是16,超过的请求会被阻塞等待空闲的Binder线程。

    所以,在进程间通信时处理并发问题时,如使用ContentProvider时,它的CRUD(创建、检索、更新和删除)方法只能同时有16个线程同时工作


  • 至此,我相信大家对Binder 跨进程通信机制 模型 已经有了一个非常清晰的定性认识
  • 下面,我将通过一个实例,分析Binder跨进程通信机制 模型在 Android中的具体代码实现方式

    即分析 上述步骤在Android中具体是用代码如何实现的


4. Binder机制 在Android中的具体实现原理

  • Binder机制在 Android中的实现主要依靠 Binder类,其实现了IBinder 接口

    下面会详细说明

  • 实例说明:Client进程 需要调用 Server进程的加法函数(将整数a和b相加)

    即:

    1. Client进程 需要传两个整数给 Server进程
    2. Server进程 需要把相加后的结果 返回给Client进程
  • 具体步骤
    下面,我会根据Binder 跨进程通信机制 模型的步骤进行分析

步骤1:注册服务

  • 过程描述
    Server进程 通过Binder驱动 向 Service Manager进程 注册服务
  • 代码实现
    Server进程 创建 一个 Binder 对象

    1. Binder 实体是 Server进程 在 Binder 驱动中的存在形式
    2. 该对象保存 Server 和 ServiceManager 的信息(保存在内核空间中)
    3. Binder 驱动通过 内核空间的Binder 实体 找到用户空间的Server对象
  • 代码分析
    Binder binder = new Stub();
    // 步骤1:创建Binder对象 ->>分析1

    // 步骤2:创建 IInterface 接口类 的匿名类
    // 创建前,需要预先定义 继承了IInterface 接口的接口 -->分析3
    IInterface plus = new IPlus(){

          // 确定Client进程需要调用的方法
          public int add(int a,int b) {
               return a+b;
         }

          // 实现IInterface接口中唯一的方法
          public IBinder asBinder(){ 
                return null ;
           }
};
          // 步骤3
          binder.attachInterface(plus,"add two int");
         // 1. 将(add two int,plus)作为(key,value)对存入到Binder对象中的一个Map<String,IInterface>对象中
         // 2. 之后,Binder对象 可根据add two int通过queryLocalIInterface()获得对应IInterface对象(即plus)的引用,可依靠该引用完成对请求方法的调用
        // 分析完毕,跳出


<-- 分析1:Stub类 -->
    public class Stub extends Binder {
    // 继承自Binder类 ->>分析2

          // 复写onTransact()
          @Override
          boolean onTransact(int code, Parcel data, Parcel reply, int flags){
          // 具体逻辑等到步骤3再具体讲解,此处先跳过
          switch (code) { 
                case Stub.add: { 

                       data.enforceInterface("add two int"); 

                       int  arg0  = data.readInt();
                       int  arg1  = data.readInt();

                       int  result = this.queryLocalIInterface("add two int") .add( arg0,  arg1); 

                        reply.writeInt(result); 

                        return true; 
                  }
           } 
      return super.onTransact(code, data, reply, flags); 

}
// 回到上面的步骤1,继续看步骤2

<-- 分析2:Binder 类 -->
 public class Binder implement IBinder{
    // Binder机制在Android中的实现主要依靠的是Binder类,其实现了IBinder接口
    // IBinder接口:定义了远程操作对象的基本接口,代表了一种跨进程传输的能力
    // 系统会为每个实现了IBinder接口的对象提供跨进程传输能力
    // 即Binder类对象具备了跨进程传输的能力

        void attachInterface(IInterface plus, String descriptor);
        // 作用:
          // 1. 将(descriptor,plus)作为(key,value)对存入到Binder对象中的一个Map<String,IInterface>对象中
          // 2. 之后,Binder对象 可根据descriptor通过queryLocalIInterface()获得对应IInterface对象(即plus)的引用,可依靠该引用完成对请求方法的调用

        IInterface queryLocalInterface(Stringdescriptor) ;
        // 作用:根据 参数 descriptor 查找相应的IInterface对象(即plus引用)

        boolean onTransact(int code, Parcel data, Parcel reply, int flags);
        // 定义:继承自IBinder接口的
        // 作用:执行Client进程所请求的目标方法(子类需要复写)
        // 参数说明:
        // code:Client进程请求方法标识符。即Server进程根据该标识确定所请求的目标方法
        // data:目标方法的参数。(Client进程传进来的,此处就是整数a和b)
        // reply:目标方法执行后的结果(返回给Client进程)
         // 注:运行在Server进程的Binder线程池中;当Client进程发起远程请求时,远程请求会要求系统底层执行回调该方法

        final class BinderProxy implements IBinder {
         // 即Server进程创建的Binder对象的代理对象类
         // 该类属于Binder的内部类
        }
        // 回到分析1原处
}

<-- 分析3:IInterface接口实现类 -->

 public interface IPlus extends IInterface {
          // 继承自IInterface接口->>分析4
          // 定义需要实现的接口方法,即Client进程需要调用的方法
         public int add(int a,int b);
// 返回步骤2
}

<-- 分析4:IInterface接口类 -->
// 进程间通信定义的通用接口
// 通过定义接口,然后再服务端实现接口、客户端调用接口,就可实现跨进程通信。
public interface IInterface
{
    // 只有一个方法:返回当前接口关联的 Binder 对象。
    public IBinder asBinder();
}
  // 回到分析3原处

注册服务后,Binder驱动持有 Server进程创建的Binder实体

步骤2:获取服务

  • Client进程 使用 某个 service前(此处是 相加函数),须 通过Binder驱动 向 ServiceManager进程 获取相应的Service信息
  • 具体代码实现过程如下:

%title插图%num
示意图

此时,Client进程与 Server进程已经建立了连接

步骤3:使用服务

Client进程 根据获取到的 Service信息(Binder代理对象),通过Binder驱动 建立与 该Service所在Server进程通信的链路,并开始使用服务

  • 过程描述
    1. Client进程 将参数(整数a和b)发送到Server进程
    2. Server进程 根据Client进程要求调用 目标方法(即加法函数)
    3. Server进程 将目标方法的结果(即加法后的结果)返回给Client进程
  • 代码实现过程

步骤1: Client进程 将参数(整数a和b)发送到Server进程

// 1. Client进程 将需要传送的数据写入到Parcel对象中
// data = 数据 = 目标方法的参数(Client进程传进来的,此处就是整数a和b) + IInterface接口对象的标识符descriptor
  android.os.Parcel data = android.os.Parcel.obtain();
  data.writeInt(a); 
  data.writeInt(b); 

  data.writeInterfaceToken("add two int");;
  // 方法对象标识符让Server进程在Binder对象中根据"add two int"通过queryLocalIInterface()查找相应的IInterface对象(即Server创建的plus),Client进程需要调用的相加方法就在该对象中

  android.os.Parcel reply = android.os.Parcel.obtain();
  // reply:目标方法执行后的结果(此处是相加后的结果)

// 2. 通过 调用代理对象的transact() 将 上述数据发送到Binder驱动
  binderproxy.transact(Stub.add, data, reply, 0)
  // 参数说明:
    // 1. Stub.add:目标方法的标识符(Client进程 和 Server进程 自身约定,可为任意)
    // 2. data :上述的Parcel对象
    // 3. reply:返回结果
    // 0:可不管

// 注:在发送数据后,Client进程的该线程会暂时被挂起
// 所以,若Server进程执行的耗时操作,请不要使用主线程,以防止ANR


// 3. Binder驱动根据 代理对象 找到对应的真身Binder对象所在的Server 进程(系统自动执行)
// 4. Binder驱动把 数据 发送到Server 进程中,并通知Server 进程执行解包(系统自动执行)

步骤2:Server进程根据Client进要求 调用 目标方法(即加法函数)

// 1. 收到Binder驱动通知后,Server 进程通过回调Binder对象onTransact()进行数据解包 & 调用目标方法
  public class Stub extends Binder {

          // 复写onTransact()
          @Override
          boolean onTransact(int code, Parcel data, Parcel reply, int flags){
          // code即在transact()中约定的目标方法的标识符

          switch (code) { 
                case Stub.add: { 
                  // a. 解包Parcel中的数据
                       data.enforceInterface("add two int"); 
                        // a1. 解析目标方法对象的标识符

                       int  arg0  = data.readInt();
                       int  arg1  = data.readInt();
                       // a2. 获得目标方法的参数

                       // b. 根据"add two int"通过queryLocalIInterface()获取相应的IInterface对象(即Server创建的plus)的引用,通过该对象引用调用方法
                       int  result = this.queryLocalIInterface("add two int") .add( arg0,  arg1); 

                        // c. 将计算结果写入到reply
                        reply.writeInt(result); 

                        return true; 
                  }
           } 
      return super.onTransact(code, data, reply, flags); 
      // 2. 将结算结果返回 到Binder驱动

步骤3:Server进程 将目标方法的结果(即加法后的结果)返回给Client进程

  // 1. Binder驱动根据 代理对象 沿原路 将结果返回 并通知Client进程获取返回结果
  // 2. 通过代理对象 接收结果(之前被挂起的线程被唤醒)

    binderproxy.transact(Stub.ADD, data, reply, 0);
    reply.readException();;
    result = reply.readInt();
          }
}
  • 总结
    下面,我用一个原理图 & 流程图来总结步骤3的内容

%title插图%num
原理图%title插图%num
流程图


5. 优点

对比 Linux (Android基于Linux)上的其他进程通信方式(管道/消息队列/共享内存/信号量/Socket),Binder 机制的优点有:

  • 高效
    1. Binder数据拷贝只需要一次,而管道、消息队列、Socket都需要2次
    2. 通过驱动在内核空间拷贝数据,不需要额外的同步处理
  • 安全性高
    Binder 机制为每个进程分配了 UID/PID 来作为鉴别身份的标示,并且在 Binder 通信时会根据 UID/PID 进行有效性检测

    1. 传统的进程通信方式对于通信双方的身份并没有做出严格的验证
    2. 如,Socket通信 ip地址是客户端手动填入,容易出现伪造
  • 使用简单
    1. 采用Client/Server 架构
    2. 实现 面向对象 的调用方式,即在使用Binder时就和调用一个本地对象实例一样

6. 总结

  • 本文主要详细讲解 跨进程通信模型 Binder机制 ,总结如下:

%title插图%num
定义%title插图%num
原理图

Android aidl Binder框架浅析

Android aidl Binder框架浅析

1、概述

Binder能干什么?Binder可以提供系统中任何程序都可以访问的全局服务。这个功能当然是任何系统都应该提供的,下面我们简单看一下Android的Binder的框架

Android Binder框架分为服务器接口、Binder驱动、以及客户端接口;简单想一下,需要提供一个全局服务,那么全局服务那端即是服务器接口,任何程序即客户端接口,它们之间通过一个Binder驱动访问。

服务器端接口:实际上是Binder类的对象,该对象一旦创建,内部则会启动一个隐藏线程,会接收Binder驱动发送的消息,收到消息后,会执行Binder对象中的onTransact()函数,并按照该函数的参数执行不同的服务器端代码。

Binder驱动:该对象也为Binder类的实例,客户端通过该对象访问远程服务。

客户端接口:获得Binder驱动,调用其transact()发送消息至服务器

如果大家对上述不了解,没关系,下面会通过例子来更好的说明,实践是检验真理的唯一标准嘛

2、AIDL的使用

如果对Android比较熟悉,那么一定使用过AIDL,如果你还不了解,那么也没关系,下面会使用一个例子展示AIDL的用法。

我们使用AIDL实现一个跨进程的加减法调用

1、服务端

新建一个项目,创建一个包名:com.zhy.calc.aidl,在包内创建一个ICalcAIDL文件:

  1. package com.zhy.calc.aidl;  
  2. interface ICalcAIDL  
  3. {
  4.     int add(int x , int y);  
  5.     int min(int x , int y );  
  6. }

注意,文件名为ICalcAIDL.aidl

然后在项目的gen目录下会生成一个ICalcAIDL.Java文件,暂时不贴这个文件的代码了,后面会详细说明

然后我们在项目中新建一个Service,代码如下:

  1. package com.example.zhy_binder;  
  2. import com.zhy.calc.aidl.ICalcAIDL;  
  3. import android.app.Service;  
  4. import android.content.Intent;  
  5. import android.os.IBinder;  
  6. import android.os.RemoteException;  
  7. import android.util.Log;  
  8. public class CalcService extends Service  
  9. {
  10.     private static final String TAG = “server”;  
  11.     public void onCreate()  
  12.     {
  13.         Log.e(TAG, “onCreate”);  
  14.     }
  15.     public IBinder onBind(Intent t)  
  16.     {
  17.         Log.e(TAG, “onBind”);  
  18.         return mBinder;  
  19.     }
  20.     public void onDestroy()  
  21.     {
  22.         Log.e(TAG, “onDestroy”);  
  23.         super.onDestroy();  
  24.     }
  25.     public boolean onUnbind(Intent intent)  
  26.     {
  27.         Log.e(TAG, “onUnbind”);  
  28.         return super.onUnbind(intent);  
  29.     }
  30.     public void onRebind(Intent intent)  
  31.     {
  32.         Log.e(TAG, “onRebind”);  
  33.         super.onRebind(intent);  
  34.     }
  35.     private final ICalcAIDL.Stub mBinder = new ICalcAIDL.Stub()  
  36.     {
  37.         @Override  
  38.         public int add(int x, int y) throws RemoteException  
  39.         {
  40.             return x + y;  
  41.         }
  42.         @Override  
  43.         public int min(int x, int y) throws RemoteException  
  44.         {
  45.             return x – y;  
  46.         }
  47.     };
  48. }

在此Service中,使用生成的ICalcAIDL创建了一个mBinder的对象,并在Service的onBind方法中返回

*后记得在AndroidManifest中注册

  1. <service android:name=“com.example.zhy_binder.CalcService” >  
  2.            <intent-filter>  
  3.                <action android:name=“com.zhy.aidl.calc” />  
  4.                <category android:name=“android.intent.category.DEFAULT” />  
  5.            </intent-filter>  
  6.        </service>  

这里我们指定了一个name,因为我们一会会在别的应用程序中通过Intent来查找此Service;这个不需要Activity,所以我也就没写Activity,安装完成也看不到安装图标,悄悄在后台运行着。

到此,服务端编写完毕。下面开始编写客户端

2、客户端

客户端的代码比较简单,创建一个布局,里面包含4个按钮,分别为绑定服务,解除绑定,调用加法,调用减法

布局文件:

  1. <LinearLayout xmlns:android=“http://schemas.android.com/apk/res/android”  
  2.     xmlns:tools=“http://schemas.android.com/tools”  
  3.     android:layout_width=“match_parent”  
  4.     android:layout_height=“match_parent”  
  5.     android:orientation=“vertical” >  
  6.     <Button  
  7.         android:layout_width=“fill_parent”  
  8.         android:layout_height=“wrap_content”  
  9.         android:onClick=“bindService”  
  10.         android:text=“BindService” />  
  11.     <Button  
  12.         android:layout_width=“fill_parent”  
  13.         android:layout_height=“wrap_content”  
  14.         android:onClick=“unbindService”  
  15.         android:text=“UnbindService” />  
  16.     <Button  
  17.         android:layout_width=“fill_parent”  
  18.         android:layout_height=“wrap_content”  
  19.         android:onClick=“addInvoked”  
  20.         android:text=“12+12” />  
  21.     <Button  
  22.         android:layout_width=“fill_parent”  
  23.         android:layout_height=“wrap_content”  
  24.         android:onClick=“minInvoked”  
  25.         android:text=“50-12” />  
  26. </LinearLayout>  

主Activity

  1. package com.example.zhy_binder_client;  
  2. import android.app.Activity;  
  3. import android.content.ComponentName;  
  4. import android.content.Context;  
  5. import android.content.Intent;  
  6. import android.content.ServiceConnection;  
  7. import android.os.Bundle;  
  8. import android.os.IBinder;  
  9. import android.util.Log;  
  10. import android.view.View;  
  11. import android.widget.Toast;  
  12. import com.zhy.calc.aidl.ICalcAIDL;  
  13. public class MainActivity extends Activity  
  14. {
  15.     private ICalcAIDL mCalcAidl;  
  16.     private ServiceConnection mServiceConn = new ServiceConnection()  
  17.     {
  18.         @Override  
  19.         public void onServiceDisconnected(ComponentName name)  
  20.         {
  21.             Log.e(“client”, “onServiceDisconnected”);  
  22.             mCalcAidl = null;  
  23.         }
  24.         @Override  
  25.         public void onServiceConnected(ComponentName name, IBinder service)  
  26.         {
  27.             Log.e(“client”, “onServiceConnected”);  
  28.             mCalcAidl = ICalcAIDL.Stub.asInterface(service);
  29.         }
  30.     };
  31.     @Override  
  32.     protected void onCreate(Bundle savedInstanceState)  
  33.     {
  34.         super.onCreate(savedInstanceState);  
  35.         setContentView(R.layout.activity_main);
  36.     }
  37.     /** 
  38.      * 点击BindService按钮时调用 
  39.      * @param view 
  40.      */  
  41.     public void bindService(View view)  
  42.     {
  43.         Intent intent = new Intent();  
  44.         intent.setAction(“com.zhy.aidl.calc”);  
  45.         bindService(intent, mServiceConn, Context.BIND_AUTO_CREATE);
  46.     }
  47.     /** 
  48.      * 点击unBindService按钮时调用 
  49.      * @param view 
  50.      */  
  51.     public void unbindService(View view)  
  52.     {
  53.         unbindService(mServiceConn);
  54.     }
  55.     /** 
  56.      * 点击12+12按钮时调用 
  57.      * @param view 
  58.      */  
  59.     public void addInvoked(View view) throws Exception  
  60.     {
  61.         if (mCalcAidl != null)  
  62.         {
  63.             int addRes = mCalcAidl.add(12, 12);  
  64.             Toast.makeText(this, addRes + “”, Toast.LENGTH_SHORT).show();  
  65.         } else  
  66.         {
  67.             Toast.makeText(this, “服务器被异常杀死,请重新绑定服务端”, Toast.LENGTH_SHORT)  
  68.                     .show();
  69.         }
  70.     }
  71.     /** 
  72.      * 点击50-12按钮时调用 
  73.      * @param view 
  74.      */  
  75.     public void minInvoked(View view) throws Exception  
  76.     {
  77.         if (mCalcAidl != null)  
  78.         {
  79.             int addRes = mCalcAidl.min(58, 12);  
  80.             Toast.makeText(this, addRes + “”, Toast.LENGTH_SHORT).show();  
  81.         } else  
  82.         {
  83.             Toast.makeText(this, “服务端未绑定或被异常杀死,请重新绑定服务端”, Toast.LENGTH_SHORT)  
  84.                     .show();
  85.         }
  86.     }
  87. }

很标准的绑定服务的代码。

直接看运行结果:

%title插图%num

我们首先点击BindService按钮,查看log

  1. 08-09 22:56:38.959: E/server(29692): onCreate
  2. 08-09 22:56:38.959: E/server(29692): onBind
  3. 08-09 22:56:38.959: E/client(29477): onServiceConnected

可以看到,点击BindService之后,服务端执行了onCreate和onBind的方法,并且客户端执行了onServiceConnected方法,标明服务器与客户端已经联通

然后点击12+12,50-12可以成功的调用服务端的代码并返回正确的结果

下面我们再点击unBindService

  1. 08-09 22:59:25.567: E/server(29692): onUnbind
  2. 08-09 22:59:25.567: E/server(29692): onDestroy

由于我们当前只有一个客户端绑定了此Service,所以Service调用了onUnbind和onDestory

然后我们继续点击12+12,50-12,通过上图可以看到,依然可以正确执行,也就是说即使onUnbind被调用,连接也是不会断开的,那么什么时候会端口呢?

即当服务端被异常终止的时候,比如我们现在在手机的正在执行的程序中找到该服务:

%title插图%num

点击停止,此时查看log

  1. 08-09 23:04:21.433: E/client(30146): onServiceDisconnected

可以看到调用了onServiceDisconnected方法,此时连接被断开,现在点击12+12,50-12的按钮,则会弹出Toast服务端断开的提示。

说了这么多,似乎和Binder框架没什么关系,下面我们来具体看一看AIDL为什么做了些什么。

3、分析AIDL生成的代码

1、服务端

先看服务端的代码,可以看到我们服务端提供的服务是由

  1. private final ICalcAIDL.Stub mBinder = new ICalcAIDL.Stub()  
  2.     {
  3.         @Override  
  4.         public int add(int x, int y) throws RemoteException  
  5.         {
  6.             return x + y;  
  7.         }
  8.         @Override  
  9.         public int min(int x, int y) throws RemoteException  
  10.         {
  11.             return x – y;  
  12.         }
  13.     };

ICalcAILD.Stub来执行的,让我们来看看Stub这个类的声明:

  1. public static abstract class Stub extends android.os.Binder implements com.zhy.calc.aidl.ICalcAIDL  

清楚的看到这个类是Binder的子类,是不是符合我们文章开通所说的服务端其实是一个Binder类的实例

接下来看它的onTransact()方法:

  1. @Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException  
  2. {
  3. switch (code)  
  4. {
  5. case INTERFACE_TRANSACTION:  
  6. {
  7. reply.writeString(DESCRIPTOR);
  8. return true;  
  9. }
  10. case TRANSACTION_add:  
  11. {
  12. data.enforceInterface(DESCRIPTOR);
  13. int _arg0;  
  14. _arg0 = data.readInt();
  15. int _arg1;  
  16. _arg1 = data.readInt();
  17. int _result = this.add(_arg0, _arg1);  
  18. reply.writeNoException();
  19. reply.writeInt(_result);
  20. return true;  
  21. }
  22. case TRANSACTION_min:  
  23. {
  24. data.enforceInterface(DESCRIPTOR);
  25. int _arg0;  
  26. _arg0 = data.readInt();
  27. int _arg1;  
  28. _arg1 = data.readInt();
  29. int _result = this.min(_arg0, _arg1);  
  30. reply.writeNoException();
  31. reply.writeInt(_result);
  32. return true;  
  33. }
  34. }
  35. return super.onTransact(code, data, reply, flags);  
  36. }

文章开头也说到服务端的Binder实例会根据客户端依靠Binder驱动发来的消息,执行onTransact方法,然后由其参数决定执行服务端的代码。

可以看到onTransact有四个参数

code , data ,replay , flags

code 是一个整形的唯一标识,用于区分执行哪个方法,客户端会传递此参数,告诉服务端执行哪个方法

data客户端传递过来的参数

replay服务器返回回去的值

flags标明是否有返回值,0为有(双向),1为没有(单向)

我们仔细看case TRANSACTION_min中的代码

data.enforceInterface(DESCRIPTOR);与客户端的writeInterfaceToken对用,标识远程服务的名称

int _arg0;
_arg0 = data.readInt();
int _arg1;
_arg1 = data.readInt();

接下来分别读取了客户端传入的两个参数

int _result = this.min(_arg0, _arg1);
reply.writeNoException();
reply.writeInt(_result);

然后执行this.min,即我们实现的min方法;返回result由reply写回。

add同理,可以看到服务端通过AIDL生成Stub的类,封装了服务端本来需要写的代码。

2、客户端

客户端主要通过ServiceConnected与服务端连接

  1. private ServiceConnection mServiceConn = new ServiceConnection()  
  2.     {
  3.         @Override  
  4.         public void onServiceDisconnected(ComponentName name)  
  5.         {
  6.             Log.e(“client”, “onServiceDisconnected”);  
  7.             mCalcAidl = null;  
  8.         }
  9.         @Override  
  10.         public void onServiceConnected(ComponentName name, IBinder service)  
  11.         {
  12.             Log.e(“client”, “onServiceConnected”);  
  13.             mCalcAidl = ICalcAIDL.Stub.asInterface(service);
  14.         }
  15.     };

如果你比较敏锐,应该会猜到这个onServiceConnected中的IBinder实例,其实就是我们文章开通所说的Binder驱动,也是一个Binder实例

在ICalcAIDL.Stub.asInterface中*终调用了:

  1. return new com.zhy.calc.aidl.ICalcAIDL.Stub.Proxy(obj);  

这个Proxy实例传入了我们的Binder驱动,并且封装了我们调用服务端的代码,文章开头说,客户端会通过Binder驱动的transact()方法调用服务端代码

直接看Proxy中的add方法

  1. @Override public int add(int x, int y) throws android.os.RemoteException  
  2. {
  3. android.os.Parcel _data = android.os.Parcel.obtain();
  4. android.os.Parcel _reply = android.os.Parcel.obtain();
  5. int _result;  
  6. try {  
  7. _data.writeInterfaceToken(DESCRIPTOR);
  8. _data.writeInt(x);
  9. _data.writeInt(y);
  10. mRemote.transact(Stub.TRANSACTION_add, _data, _reply, 0);  
  11. _reply.readException();
  12. _result = _reply.readInt();
  13. }
  14. finally {  
  15. _reply.recycle();
  16. _data.recycle();
  17. }
  18. return _result;  
  19. }

首先声明两个Parcel对象,一个用于传递数据,一个用户接收返回的数据

_data.writeInterfaceToken(DESCRIPTOR);与服务器端的enforceInterfac对应

_data.writeInt(x);
_data.writeInt(y);写入需要传递的参数

mRemote.transact(Stub.TRANSACTION_add, _data, _reply, 0);

终于看到了我们的transact方法,*个对应服务端的code,_data,_repay分别对应服务端的data,reply,0表示是双向的

_reply.readException();
_result = _reply.readInt();

*后读出我们服务端返回的数据,然后return。可以看到和服务端的onTransact基本是一行一行对应的。

到此,我们已经通过AIDL生成的代码解释了Android Binder框架的工作原理。Service的作用其实就是为我们创建Binder驱动,即服务端与客户端连接的桥梁。

AIDL其实通过我们写的aidl文件,帮助我们生成了一个接口,一个Stub类用于服务端,一个Proxy类用于客户端调用。那么我们是否可以不通过写AIDL来实现远程的通信呢?下面向大家展示如何完全不依赖AIDL来实现客户端与服务端的通信。

4、不依赖AIDL实现程序间通讯

1、服务端代码

我们新建一个CalcPlusService.java用于实现两个数的乘和除

  1. package com.example.zhy_binder;  
  2. import android.app.Service;  
  3. import android.content.Intent;  
  4. import android.os.Binder;  
  5. import android.os.IBinder;  
  6. import android.os.Parcel;  
  7. import android.os.RemoteException;  
  8. import android.util.Log;  
  9. public class CalcPlusService extends Service  
  10. {
  11.     private static final String DESCRIPTOR = “CalcPlusService”;  
  12.     private static final String TAG = “CalcPlusService”;  
  13.     public void onCreate()  
  14.     {
  15.         Log.e(TAG, “onCreate”);  
  16.     }
  17.     @Override  
  18.     public int onStartCommand(Intent intent, int flags, int startId)  
  19.     {
  20.         Log.e(TAG, “onStartCommand”);  
  21.         return super.onStartCommand(intent, flags, startId);  
  22.     }
  23.     public IBinder onBind(Intent t)  
  24.     {
  25.         Log.e(TAG, “onBind”);  
  26.         return mBinder;  
  27.     }
  28.     public void onDestroy()  
  29.     {
  30.         Log.e(TAG, “onDestroy”);  
  31.         super.onDestroy();  
  32.     }
  33.     public boolean onUnbind(Intent intent)  
  34.     {
  35.         Log.e(TAG, “onUnbind”);  
  36.         return super.onUnbind(intent);  
  37.     }
  38.     public void onRebind(Intent intent)  
  39.     {
  40.         Log.e(TAG, “onRebind”);  
  41.         super.onRebind(intent);  
  42.     }
  43.     private MyBinder mBinder = new MyBinder();  
  44.     private class MyBinder extends Binder  
  45.     {
  46.         @Override  
  47.         protected boolean onTransact(int code, Parcel data, Parcel reply,  
  48.                 int flags) throws RemoteException  
  49.         {
  50.             switch (code)  
  51.             {
  52.             case 0x110:  
  53.             {
  54.                 data.enforceInterface(DESCRIPTOR);
  55.                 int _arg0;  
  56.                 _arg0 = data.readInt();
  57.                 int _arg1;  
  58.                 _arg1 = data.readInt();
  59.                 int _result = _arg0 * _arg1;  
  60.                 reply.writeNoException();
  61.                 reply.writeInt(_result);
  62.                 return true;  
  63.             }
  64.             case 0x111:  
  65.             {
  66.                 data.enforceInterface(DESCRIPTOR);
  67.                 int _arg0;  
  68.                 _arg0 = data.readInt();
  69.                 int _arg1;  
  70.                 _arg1 = data.readInt();
  71.                 int _result = _arg0 / _arg1;  
  72.                 reply.writeNoException();
  73.                 reply.writeInt(_result);
  74.                 return true;  
  75.             }
  76.             }
  77.             return super.onTransact(code, data, reply, flags);  
  78.         }
  79.     };
  80. }

我们自己实现服务端,所以我们自定义了一个Binder子类,然后复写了其onTransact方法,我们指定服务的标识为CalcPlusService,然后0x110为乘,0x111为除;

记得在AndroidMenifest中注册

  1. <service android:name=“com.example.zhy_binder.CalcPlusService” >  
  2.            <intent-filter>  
  3.                <action android:name=“com.zhy.aidl.calcplus” />  
  4.                <category android:name=“android.intent.category.DEFAULT” />  
  5.            </intent-filter>  
  6.        </service>  

服务端代码结束。

2、客户端代码

单独新建了一个项目,代码和上例很类似

首先布局文件:

  1. <LinearLayout xmlns:android=“http://schemas.android.com/apk/res/android”  
  2.     xmlns:tools=“http://schemas.android.com/tools”  
  3.     android:layout_width=“match_parent”  
  4.     android:layout_height=“match_parent”  
  5.     android:orientation=“vertical” >  
  6.     <Button  
  7.         android:layout_width=“fill_parent”  
  8.         android:layout_height=“wrap_content”  
  9.         android:onClick=“bindService”  
  10.         android:text=“BindService” />  
  11.     <Button  
  12.         android:layout_width=“fill_parent”  
  13.         android:layout_height=“wrap_content”  
  14.         android:onClick=“unbindService”  
  15.         android:text=“UnbindService” />  
  16.     <Button  
  17.         android:layout_width=“fill_parent”  
  18.         android:layout_height=“wrap_content”  
  19.         android:onClick=“mulInvoked”  
  20.         android:text=“50*12” />  
  21.     <Button  
  22.         android:layout_width=“fill_parent”  
  23.         android:layout_height=“wrap_content”  
  24.         android:onClick=“divInvoked”  
  25.         android:text=“36/12” />  
  26. </LinearLayout>  

可以看到加入了乘和除

然后是Activity的代码

  1. package com.example.zhy_binder_client03;  
  2. import android.app.Activity;  
  3. import android.content.ComponentName;  
  4. import android.content.Context;  
  5. import android.content.Intent;  
  6. import android.content.ServiceConnection;  
  7. import android.os.Bundle;  
  8. import android.os.IBinder;  
  9. import android.os.RemoteException;  
  10. import android.util.Log;  
  11. import android.view.View;  
  12. import android.widget.Toast;  
  13. public class MainActivity extends Activity  
  14. {
  15.     private IBinder mPlusBinder;  
  16.     private ServiceConnection mServiceConnPlus = new ServiceConnection()  
  17.     {
  18.         @Override  
  19.         public void onServiceDisconnected(ComponentName name)  
  20.         {
  21.             Log.e(“client”, “mServiceConnPlus onServiceDisconnected”);  
  22.         }
  23.         @Override  
  24.         public void onServiceConnected(ComponentName name, IBinder service)  
  25.         {
  26.             Log.e(“client”, ” mServiceConnPlus onServiceConnected”);  
  27.             mPlusBinder = service;
  28.         }
  29.     };
  30.     @Override  
  31.     protected void onCreate(Bundle savedInstanceState)  
  32.     {
  33.         super.onCreate(savedInstanceState);  
  34.         setContentView(R.layout.activity_main);
  35.     }
  36.     public void bindService(View view)  
  37.     {
  38.         Intent intentPlus = new Intent();  
  39.         intentPlus.setAction(“com.zhy.aidl.calcplus”);  
  40.         boolean plus = bindService(intentPlus, mServiceConnPlus,  
  41.                 Context.BIND_AUTO_CREATE);
  42.         Log.e(“plus”, plus + “”);  
  43.     }
  44.     public void unbindService(View view)  
  45.     {
  46.         unbindService(mServiceConnPlus);
  47.     }
  48.     public void mulInvoked(View view)  
  49.     {
  50.         if (mPlusBinder == null)  
  51.         {
  52.             Toast.makeText(this, “未连接服务端或服务端被异常杀死”, Toast.LENGTH_SHORT).show();  
  53.         } else  
  54.         {
  55.             android.os.Parcel _data = android.os.Parcel.obtain();
  56.             android.os.Parcel _reply = android.os.Parcel.obtain();
  57.             int _result;  
  58.             try  
  59.             {
  60.                 _data.writeInterfaceToken(“CalcPlusService”);  
  61.                 _data.writeInt(50);  
  62.                 _data.writeInt(12);  
  63.                 mPlusBinder.transact(0x110, _data, _reply, 0);  
  64.                 _reply.readException();
  65.                 _result = _reply.readInt();
  66.                 Toast.makeText(this, _result + “”, Toast.LENGTH_SHORT).show();  
  67.             } catch (RemoteException e)  
  68.             {
  69.                 e.printStackTrace();
  70.             } finally  
  71.             {
  72.                 _reply.recycle();
  73.                 _data.recycle();
  74.             }
  75.         }
  76.     }
  77.     public void divInvoked(View view)  
  78.     {
  79.         if (mPlusBinder == null)  
  80.         {
  81.             Toast.makeText(this, “未连接服务端或服务端被异常杀死”, Toast.LENGTH_SHORT).show();  
  82.         } else  
  83.         {
  84.             android.os.Parcel _data = android.os.Parcel.obtain();
  85.             android.os.Parcel _reply = android.os.Parcel.obtain();
  86.             int _result;  
  87.             try  
  88.             {
  89.                 _data.writeInterfaceToken(“CalcPlusService”);  
  90.                 _data.writeInt(36);  
  91.                 _data.writeInt(12);  
  92.                 mPlusBinder.transact(0x111, _data, _reply, 0);  
  93.                 _reply.readException();
  94.                 _result = _reply.readInt();
  95.                 Toast.makeText(this, _result + “”, Toast.LENGTH_SHORT).show();  
  96.             } catch (RemoteException e)  
  97.             {
  98.                 e.printStackTrace();
  99.             } finally  
  100.             {
  101.                 _reply.recycle();
  102.                 _data.recycle();
  103.             }
  104.         }
  105.     }
  106. }

为了明了,我直接在mulInvoked里面写了代码,和服务端都没有抽象出一个接口。首先绑定服务时,通过onServiceConnected得到Binder驱动即mPlusBinder;

然后准备数据,调用transact方法,通过code指定执行服务端哪个方法,代码和上面的分析一致。

下面看运行结果:

%title插图%num

是不是很好的实现了我们两个应用程序间的通讯,并没有使用aidl文件,也从侧面分析了我们上述分析是正确的。

 

好了,就到这里,相信大家看完这篇博文,对aidl和Binder的理解也会更加深刻。

Context都没弄明白,还怎么做Android开发?

Context都没弄明白,还怎么做Android开发?

Activity mActivity =new Activity()

作为Android开发者,不知道你有没有思考过这个问题,Activity可以new吗?Android的应用程序开发采用JAVA语言,Activity本质上也是一个对象,那上面的写法有什么问题呢?估计很多人说不清道不明。Android程序不像Java程序一样,随便创建一个类,写个main()方法就能运行,Android应用模型是基于组件的应用设计模式,组件的运行要有一个完整的Android工程环境,在这个环境下,Activity、Service等系统组件才能够正常工作,而这些组件并不能采用普通的Java对象创建方式,new一下就能创建实例了,而是要有它们各自的上下文环境,也就是我们这里讨论的Context。可以这样讲,Context是维持Android程序中各组件能够正常工作的一个核心功能类。

Context到底是什么

Context的中文翻译为:语境; 上下文; 背景; 环境,在开发中我们经常说称之为“上下文”,那么这个“上下文”到底是指什么意思呢?在语文中,我们可以理解为语境,在程序中,我们可以理解为当前对象在程序中所处的一个环境,一个与系统交互的过程。比如微信聊天,此时的“环境”是指聊天的界面以及相关的数据请求与传输,Context在加载资源、启动Activity、获取系统服务、创建View等操作都要参与。

那Context到底是什么呢?一个Activity就是一个Context,一个Service也是一个Context。Android程序员把“场景”抽象为Context类,他们认为用户和操作系统的每一次交互都是一个场景,比如打电话、发短信,这些都是一个有界面的场景,还有一些没有界面的场景,比如后台运行的服务(Service)。一个应用程序可以认为是一个工作环境,用户在这个环境中会切换到不同的场景,这就像一个前台秘书,她可能需要接待客人,可能要打印文件,还可能要接听客户电话,而这些就称之为不同的场景,前台秘书可以称之为一个应用程序。

如何生动形象的理解Context

上面的概念中采用了通俗的理解方式,将Context理解为“上下文”或者“场景”,如果你仍然觉得很抽象,不好理解。在这里我给出一个可能不是很恰当的比喻,希望有助于大家的理解:一个Android应用程序,可以理解为一部电影或者一部电视剧,Activity,Service,Broadcast Receiver,Content Provider这四大组件就好比是这部戏里的四个主角:胡歌,霍建华,诗诗,Baby。他们是由剧组(系统)一开始就定好了的,整部戏就是由这四位主演领衔担纲的,所以这四位主角并不是大街上随随便便拉个人(new 一个对象)都能演的。有了演员当然也得有摄像机拍摄啊,他们必须通过镜头(Context)才能将戏传递给观众,这也就正对应说四大组件(四位主角)必须工作在Context环境下(摄像机镜头)。那Button,TextView,LinearLayout这些控件呢,就好比是这部戏里的配角或者说群众演员,他们显然没有这么重用,随便一个路人甲路人乙都能演(可以new一个对象),但是他们也必须要面对镜头(工作在Context环境下),所以Button mButton=new Button(Context)是可以的。虽然不很恰当,但还是很容易理解的,希望有帮助。

源码中的Context

/**
 * Interface to global information about an application environment.  This is
 * an abstract class whose implementation is provided by
 * the Android system.  It
 * allows access to application-specific resources and classes, as well as
 * up-calls for application-level operations such as launching activities,
 * broadcasting and receiving intents, etc.
 */
public abstract class Context {
    /**
     * File creation mode: the default mode, where the created file can only
     * be accessed by the calling application (or all applications sharing the
     * same user ID).
     * @see #MODE_WORLD_READABLE
     * @see #MODE_WORLD_WRITEABLE
     */
    public static final int MODE_PRIVATE = 0x0000;

    public static final int MODE_WORLD_WRITEABLE = 0x0002;

    public static final int MODE_APPEND = 0x8000;

    public static final int MODE_MULTI_PROCESS = 0x0004;

    .
    .
    .
    }

源码中的注释是这么来解释Context的:Context提供了关于应用环境全局信息的接口。它是一个抽象类,它的执行被Android系统所提供。它允许获取以应用为特征的资源和类型,是一个统领一些资源(应用程序环境变量等)的上下文。就是说,它描述一个应用程序环境的信息(即上下文);是一个抽象类,Android提供了该抽象类的具体实现类;通过它我们可以获取应用程序的资源和类(包括应用级别操作,如启动Activity,发广播,接受Intent等)。既然上面Context是一个抽象类,那么肯定有他的实现类咯,我们在Context的源码中通过IDE可以查看到他的子类*终可以得到如下关系图:

%title插图%num

Context.png

Context类本身是一个纯abstract类,它有两个具体的实现子类:ContextImpl和ContextWrapper。其中ContextWrapper类,如其名所言,这只是一个包装而已,ContextWrapper构造函数中必须包含一个真正的Context引用,同时ContextWrapper中提供了attachBaseContext()用于给ContextWrapper对象中指定真正的Context对象,调用ContextWrapper的方法都会被转向其所包含的真正的Context对象。ContextThemeWrapper类,如其名所言,其内部包含了与主题(Theme)相关的接口,这里所说的主题就是指在AndroidManifest.xml中通过android:theme为Application元素或者Activity元素指定的主题。当然,只有Activity才需要主题,Service是不需要主题的,因为Service是没有界面的后台场景,所以Service直接继承于ContextWrapper,Application同理。而ContextImpl类则真正实现了Context中的所以函数,应用程序中所调用的各种Context类的方法,其实现均来自于该类。一句话总结:Context的两个子类分工明确,其中ContextImpl是Context的具体实现类,ContextWrapper是Context的包装类。Activity,Application,Service虽都继承自ContextWrapper(Activity继承自ContextWrapper的子类ContextThemeWrapper),但它们初始化的过程中都会创建ContextImpl对象,由ContextImpl实现Context中的方法。

一个应用程序有几个Context

其实这个问题本身并没有什么意义,关键还是在于对Context的理解,从上面的关系图我们已经可以得出答案了,在应用程序中Context的具体实现子类就是:Activity,Service,Application。那么Context数量=Activity数量+Service数量+1。当然如果你足够细心,可能会有疑问:我们常说四大组件,这里怎么只有Activity,Service持有Context,那Broadcast Receiver,Content Provider呢?Broadcast Receiver,Content Provider并不是Context的子类,他们所持有的Context都是其他地方传过去的,所以并不计入Context总数。上面的关系图也从另外一个侧面告诉我们Context类在整个Android系统中的地位是多么的崇高,因为很显然Activity,Service,Application都是其子类,其地位和作用不言而喻。

Context能干什么

Context到底可以实现哪些功能呢?这个就实在是太多了,弹出Toast、启动Activity、启动Service、发送广播、操作数据库等等都需要用到Context。

TextView tv = new TextView(getContext());

ListAdapter adapter = new SimpleCursorAdapter(getApplicationContext(), ...);

AudioManager am = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);getApplicationContext().getSharedPreferences(name, mode);

getApplicationContext().getContentResolver().query(uri, ...);

getContext().getResources().getDisplayMetrics().widthPixels * 5 / 8;

getContext().startActivity(intent);

getContext().startService(intent);

getContext().sendBroadcast(intent);

Context作用域

虽然Context神通广大,但并不是随便拿到一个Context实例就可以为所欲为,它的使用还是有一些规则限制的。由于Context的具体实例是由ContextImpl类去实现的,因此在*大多数场景下,Activity、Service和Application这三种类型的Context都是可以通用的。不过有几种场景比较特殊,比如启动Activity,还有弹出Dialog。出于安全原因的考虑,Android是不允许Activity或Dialog凭空出现的,一个Activity的启动必须要建立在另一个Activity的基础之上,也就是以此形成的返回栈。而Dialog则必须在一个Activity上面弹出(除非是System Alert类型的Dialog),因此在这种场景下,我们只能使用Activity类型的Context,否则将会出错。

%title插图%num

Context作用域.png

从上图我们可以发现Activity所持有的Context的作用域*广,无所不能。因为Activity继承自ContextThemeWrapper,而Application和Service继承自ContextWrapper,很显然ContextThemeWrapper在ContextWrapper的基础上又做了一些操作使得Activity变得更强大,这里我就不再贴源码给大家分析了,有兴趣的童鞋可以自己查查源码。上图中的YES和NO我也不再做过多的解释了,这里我说一下上图中Application和Service所不推荐的两种使用情况。
1:如果我们用ApplicationContext去启动一个LaunchMode为standard的Activity的时候会报错android.util.AndroidRuntimeException: Calling startActivity from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?这是因为非Activity类型的Context并没有所谓的任务栈,所以待启动的Activity就找不到栈了。解决这个问题的方法就是为待启动的Activity指定FLAG_ACTIVITY_NEW_TASK标记位,这样启动的时候就为它创建一个新的任务栈,而此时Activity是以singleTask模式启动的。所有这种用Application启动Activity的方式不推荐使用,Service同Application。
2:在Application和Service中去layout inflate也是合法的,但是会使用系统默认的主题样式,如果你自定义了某些样式可能不会被使用。所以这种方式也不推荐使用。
一句话总结:凡是跟UI相关的,都应该使用Activity做为Context来处理;其他的一些操作,Service,Activity,Application等实例都可以,当然了,注意Context引用的持有,防止内存泄漏。

如何获取Context

通常我们想要获取Context对象,主要有以下四种方法
1:View.getContext,返回当前View对象的Context对象,通常是当前正在展示的Activity对象。
2:Activity.getApplicationContext,获取当前Activity所在的(应用)进程的Context对象,通常我们使用Context对象时,要优先考虑这个全局的进程Context。
3:ContextWrapper.getBaseContext():用来获取一个ContextWrapper进行装饰之前的Context,可以使用这个方法,这个方法在实际开发中使用并不多,也不建议使用。
4:Activity.this 返回当前的Activity实例,如果是UI控件需要使用Activity作为Context对象,但是默认的Toast实际上使用ApplicationContext也可以。

getApplication()和getApplicationContext()

上面说到获取当前Application对象用getApplicationContext,不知道你有没有联想到getApplication(),这两个方法有什么区别?相信这个问题会难倒不少开发者。

%title插图%num

getApplication()&getApplicationContext().png

程序是不会骗人的,我们通过上面的代码,打印得出两者的内存地址都是相同的,看来它们是同一个对象。其实这个结果也很好理解,因为前面已经说过了,Application本身就是一个Context,所以这里获取getApplicationContext()得到的结果就是Application本身的实例。那么问题来了,既然这两个方法得到的结果都是相同的,那么Android为什么要提供两个功能重复的方法呢?实际上这两个方法在作用域上有比较大的区别。getApplication()方法的语义性非常强,一看就知道是用来获取Application实例的,但是这个方法只有在Activity和Service中才能调用的到。那么也许在*大多数情况下我们都是在Activity或者Service中使用Application的,但是如果在一些其它的场景,比如BroadcastReceiver中也想获得Application的实例,这时就可以借助getApplicationContext()方法了。

publicclassMyReceiverextendsBroadcastReceiver{

@Override
publicvoidonReceive(Contextcontext,Intentintent){
ApplicationmyApp=(Application)context.getApplicationContext();

}

}

Context引起的内存泄露

但Context并不能随便乱用,用的不好有可能会引起内存泄露的问题,下面就示例两种错误的引用方式。

错误的单例模式

public class Singleton {
    private static Singleton instance;
    private Context mContext;

    private Singleton(Context context) {
        this.mContext = context;
    }

    public static Singleton getInstance(Context context) {
        if (instance == null) {
            instance = new Singleton(context);
        }
        return instance;
    }
}

这是一个非线程安全的单例模式,instance作为静态对象,其生命周期要长于普通的对象,其中也包含Activity,假如Activity A去getInstance获得instance对象,传入this,常驻内存的Singleton保存了你传入的Activity A对象,并一直持有,即使Activity被销毁掉,但因为它的引用还存在于一个Singleton中,就不可能被GC掉,这样就导致了内存泄漏。

View持有Activity引用

public class MainActivity extends Activity {
    private static Drawable mDrawable;

    @Override
    protected void onCreate(Bundle saveInstanceState) {
        super.onCreate(saveInstanceState);
        setContentView(R.layout.activity_main);
        ImageView iv = new ImageView(this);
        mDrawable = getResources().getDrawable(R.drawable.ic_launcher);
        iv.setImageDrawable(mDrawable);
    }
}
就是Drawable被设置到ImageView上的时候,ImageView会将自身设置到Drawable的callback上去,因为View是实现了Drawable.Callback接口, 这样当Drawable需要刷新的时候,可以调用.这个Callback,然后通知View重新绘制该Drawable. 所以引用的正确顺序应该Drawable->View->Context, 是这样导致的泄

有一个静态的Drawable对象当ImageView设置这个Drawable时,ImageView保存了mDrawable的引用,而ImageView传入的this是MainActivity的mContext,因为被static修饰的mDrawable是常驻内存的,MainActivity是它的间接引用,MainActivity被销毁时,也不能被GC掉,所以造成内存泄漏。

正确使用Context

一般Context造成的内存泄漏,几乎都是当Context销毁的时候,却因为被引用导致销毁失败,而Application的Context对象可以理解为随着进程存在的,所以我们总结出使用Context的正确姿势:
1:当Application的Context能搞定的情况下,并且生命周期长的对象,优先使用Application的Context。
2:不要让生命周期长于Activity的对象持有到Activity的引用。
3:尽量不要在Activity中使用非静态内部类,因为非静态内部类会隐式持有外部类实例的引用,如果使用静态内部类,将外部实例引用作为弱引用持有。

总结

总之Context在Android系统中的地位很重要,它几乎无所不能,但它也不是你想用就能随便用的,谨防使用不当引起的内存问题。

友情链接: SITEMAP | 旋风加速器官网 | 旋风软件中心 | textarea | 黑洞加速器 | jiaohess | 老王加速器 | 烧饼哥加速器 | 小蓝鸟 | tiktok加速器 | 旋风加速度器 | 旋风加速 | quickq加速器 | 飞驰加速器 | 飞鸟加速器 | 狗急加速器 | hammer加速器 | trafficace | 原子加速器 | 葫芦加速器 | 麦旋风 | 油管加速器 | anycastly | INS加速器 | INS加速器免费版 | 免费vqn加速外网 | 旋风加速器 | 快橙加速器 | 啊哈加速器 | 迷雾通 | 优途加速器 | 海外播 | 坚果加速器 | 海外vqn加速 | 蘑菇加速器 | 毛豆加速器 | 接码平台 | 接码S | 西柚加速器 | 快柠檬加速器 | 黑洞加速 | falemon | 快橙加速器 | anycast加速器 | ibaidu | moneytreeblog | 坚果加速器 | 派币加速器 | 飞鸟加速器 | 毛豆APP | PIKPAK | 安卓vqn免费 | 一元机场加速器 | 一元机场 | 老王加速器 | 黑洞加速器 | 白石山 | 小牛加速器 | 黑洞加速 | 迷雾通官网 | 迷雾通 | 迷雾通加速器 | 十大免费加速神器 | 猎豹加速器 | 蚂蚁加速器 | 坚果加速器 | 黑洞加速 | 银河加速器 | 猎豹加速器 | 海鸥加速器 | 芒果加速器 | 小牛加速器 | 极光加速器 | 黑洞加速 | movabletype中文网 | 猎豹加速器官网 | 烧饼哥加速器官网 | 旋风加速器度器 | 哔咔漫画 | PicACG | 雷霆加速