普通视图

发现新文章,点击刷新页面。
昨天以前首页

【教程】在青龙面板上使用Server酱³发送支持标签的通知

2025年5月3日 23:07

虽然青龙面板现在已经支持使用Server酱来进行通知的推送了,也支持了Server酱³,但是却没给我们自定义标签的选项,所以青龙面板发过来的消息会被丢到“无Tag消息”中,并不方面我们分类。

所以我们就需要使用自定义通知来实现这个功能,打开Server酱³,添加一个新的 APPKey,输入应用备注,然后复制 URL(不是Key)。

Server酱³后台

回到青龙面板,将通知方式改成自定义通知,请求方式改成 “POST” ,webhookContentType 改成 “application/json”,在 webhookUrl 处粘贴刚刚复制的URL(https://<uid>.push.ft07.com/send/<sendkey>.send),并在末尾加上?title=$title&desp=$content&tags=青龙(青龙可以改成任意字符串)。保存之后,应该就可以在手机上的Server酱³收到通知了。

青龙面板通知设置
通知样式

【教程】在宝塔面板上使用Server酱³发送通知

2025年5月3日 22:39

众所周知,宝塔面板的告警设置中的通知渠道目前只有微信公众号邮箱、钉钉、飞书、企业微信、短信通知和自定义消息通道。正常情况下这已经够用了,但是如果你就是想把所有的通知都整合到Server酱³,看上去就没办法了。但是我们可以使用自定义消息通道来配置。

这里就以Server酱³(不是Server酱·Turbo版)作为演示,因为Server酱³现在已经接入了大部分的手机的系统通),并不用保持 APP 的后台存活。而且,Server酱³的APP中,可以按照通知的标签进行分类,防止通知太多找不到。

在Server酱³后台中,我们找到“AppKey 管理”,添加一个新的 AppKey,输入对应的应用备注(如宝塔面板),便可以生成一个新的 AppKey URL。

Server酱³后台

然后复制这个URL(不是 Key ),打开宝塔面板的自定义消息通道配置(设置-告警通知-告警设置-自定义消息通道-配置),在名称处填入”Server酱³“,URL 处粘贴刚刚复制的URL(https://<uid>.push.ft07.com/send/<sendkey>.send),如果需要自定义标签,需在 URL 末尾加上“?&tags=宝塔 ”(宝塔可以替换成任意字符串)。

然后在自定义参数处填上 {"title":"$title","desp":"$msg"} ,然后保存。正常情况下,手机端上的Server酱³就会收到消息通道配置提醒,并且带有服务器的信息。

自定义消息通道配置
【教程】在宝塔面板上使用Server酱³发送通知插图2

我如何将资金从新加坡转到香港?反之亦然

2025年5月1日 00:00

因为偶尔会有一点新加坡的小收入——小到只能解决吃饭而已,我需要将它转到香港的银行账户里,这怎么办?

以前我用新加坡银行的电汇功能(其实就是国际汇款),这个办法最为传统,但速度要几天,关键是手续费不便宜,综合费率都是1%以上。

后来,我用了Wise的汇款功能,这个速度很快,但是有一定的手续费,虽然没有传统银行那么贵。

最近几个月,我一直在使用一个更好的办法,利用长桥券商的服务:资金划转(跨券商资金划转)。直接将资金通过资金划转,在香港和新加坡两地之间移动,只要开通两边的券商账户并授权即可——在同一个长桥APP里,使用它划转资金完全免费。速度方面,一般隔一个工作日就能到账。

券商自己还提供换汇功能,比如新加坡元可以兑成美元或者港元,反之亦然。如果资金闲置的话,长桥还有余额通理财功能,其实就是货币基金,美元产品的收益率目前年化是4%以上,好过香港银行的存款利率。

估计长桥没有想到我对它们产品是这么利用的,这也说明不同地点之间的资金流动便利性是多么重要,而我们的传统金融服务跟上这个时代了吗?

其实长桥券商还有更多的功能,只是一般人不知道而已,比如下图,这些便利就等着朋友们自己去开发了。

长桥证券

需要注册长桥开户(开户入金教程)的朋友,长桥官方给土木坛子博客读者配置了一个渠道专属注册优惠,推荐码是:783XQW,专门的注册开户网页链接:

香港长桥:https://app.longbridgehk.com/ac/oa?account_channel=lb&channel=HB100006&invite-code=783XQW

新加坡长桥(可买数字加密货币ETF):https://activity.lbmkt.ing/pages/longbridge/7415/index.html?appid=longbridge&orgid=1&account_channel=lb&lang=zh-CN&channel=HB100006&invite-code=783XQW

渠道专属活动,注册后(注册时填写土木坛子渠道邀请码:783XQW)通常每个月都有丰厚福利奖励(长桥新加坡账号入资对应等值新币,奖励相应免佣等福利)。

PS 你如果实在还有疑问(包括但不限于境外银行开户、券商账户开户等),可扫描二维码添加我的微信号( tumuhk ),请注明“美股”,我尽力解答你的相关问题。

土木坛子

我用 AI 做了两个 WordPress 插件 —— 好物/应用分享页

2025年5月1日 00:40

作为小白,我一直很羡慕各位大佬制作的好物分享页面。之前我也有尝试使用 WordPress 的古腾堡编辑器手搓好物页和应用页,但添加新的内容会变得十分麻烦,以至于我几乎没有更新过页面。于是,我用 AI 制作了两个 WordPress 插件,实现了好物和应用分享页面的搭建,并且操作非常简单,适合小白用户。

效果展示

https://veryjack.com/goods
https://veryjack.com/apps

项目地址

好物分享页 WordPress 插件

https://github.com/very-jack/wp_goods_exhibition

应用分享页 WordPress 插件

https://github.com/very-jack/wp_apps_exhibition

使用方法

两个插件的使用方法相似,均是先在 github releases 页面下载 zip 文件,在 WordPress 中安装插件,最后在后台页面添加信息即可,以好物分享页为例,具体步骤如下:

  1. 下载插件
  2. 在 WordPress 后台 – 插件添加插件 – 选择下载的 zip 文件进行安装;
  3. 在 WordPress 后台 – 好物页面 选项 – 添加新产品 – 填入产品名称产品描述跳转链接(可不填)产品图片即可;
  4. 创建一个页面或者新的文章,在文中输入段代码即可调用该分享页面。

注意事项

如果需要调整颜色等样式,需要自行下载插件后解压,修改 .css 文件。

AI 对话记录

应网友的建议,在此补充对 AI 提问的部分内容,或许能帮助同样想让 AI 制作小工具的朋友们:

我想制作一个 wordpress 插件,能够在页面中优雅地展示产品,如上传图所示,具体要求如下:
**设计方面**:
1. 效果要如上传图那样简洁美观,有点苹果地设计风格;
2. 每一行只展示两个产品,每个产品包含图片、名称和描述;
3. 对于更新日期不超过1个月的产品,会有一个 new 的标签;
4. 设计上一定要保持对齐,图片大小一致等符合基本设计要求
5. 需要完美适配电脑和手机;

**使用方面**:
1. 通过短链接 [goods_exhibition_page] 来调用该页面;
2. 在后台有添加和管理产品的页面,能够上传图片、填写名称、描述等信息;
3. 插件名为“好物页面插件”
如图,这是目前的效果,有几个需要调整的地方:
1. 产品卡片太大了,请缩小一些,请参考第二张图片的效果;
2. 因为我上传的图片都是没有背景的,所以我想图片显示区域的背景和文字部分背景的颜色一致,这样更加简洁,风格更加统一
还需要做调整:
1. 页面没有居中显示:
2. 卡片还是太大了,特别是宽度,请更窄一些
3. 添加一个点击开片跳转到指定网页的功能,这个跳转的网址也是在后台处填写,如果没填写网址,则不会跳转。
1. 当一个产品有超链接时,在该卡片上会有一个跳转的图标,但该图标和 new 标签重叠了,请做调整;
2. 当鼠标指向没有超链接的卡片时,鼠标样式也会发生变化,这很不合理,请做调整;
链接图标和new标签重叠的问题依然存在,现在的情况是:当鼠标不指向该卡片时,链接图标和new标签重叠;当鼠标指向该卡片时,new标签会向左移动一点,和链接图标错开位置。
请帮我调整为:
1. 将链接图标移至卡片最右下角的地方;
2. 鼠标指向该卡片时,new标签不要移动。

......

以上是最初几段对话,此后就是关于样式细节的微调,参考价值不大了,就省去了。

全程使用 Claude 3.7 和 Gemini 2.5 Pro 实现。

我用 AI 做了两个 WordPress 插件 —— 好物/应用分享页最先出现在Jack's Space

一些我自己用的还不错的 Chrome 插件

2025年3月11日 18:25

英语/日语翻译:沉浸式翻译

我的日常要看到很多英文文章和网站,因此,可以借助沉浸式翻译,帮助我快速翻译多种语言为中文,从而降低我在阅读不同语言内容的障碍。

1743588697 image

快速搜索:超级搜索

超级搜索支持我快速的选中词汇并进行搜索,对于一些特定的场景下,会非常有帮助。比如我在浏览网页的时候,发现了一部电影,想快速在豆瓣电影中找到并标记,就可以借助超级搜索,配置一个搜索关键词来实现。

1743588684 image

SEO 检查器:AITDK

AI TDK 是我用来检查自己的网页是否完成了一些基本的 SEO 设置的工具。当我上线了一个新的网站后,就会打开 AITDK,然后查看哪里的信息还不完整,需要补充的。就可以继续去补充相应的内容。

1743588868 image

JSON 查看:JSON Viewer

在开发的时候,经常会有要查看服务端返回的 JSON 的情况, 借助 JSON Viewer 可以将不容易看明白的 JSON 给格式化了,方便你快速定位要看的 JSON。

1743589100 image

广告拦截:AdGuard

广告拦截我选择了 AdGuard,有了它,我看 Youtube 再也没有广告了。。。

1743589301 image

复制为 Markdown:Copy as Markdown

因为经常要将部分内容复制为 Markdown,方便在我的其他工具中使用,所以我安装了这个 Copy as Markdown 插件,方便自己随意复制。

1743589434 image

页面增强:篡改猴

当我需要对一些网页做一些快速的改造,但同时又不想写成 Chrome 插件的时候,就会选择写成油猴脚本,然后放在篡改猴里来用,非常方便。

1743589508 image

灵感记录:Memos

我自己部署了一个 Memos ,用于记录自己的灵感和想法。因此,我使用了一个 Chrome 插件,来方便我记录。

1743589800 image

这个桌面宠物也太可爱了吧!

2025年3月30日 10:03

Mac 平台上时不时会冒出一些有趣的应用。前几天,我关注已久的一款应用终于上架了——它名叫 Docko,是一款非常可爱的应用,推荐给大家。

软件简介

  • Docko 是专为 macOS 打造的虚拟宠物应用,为 Mac 的 Dock 栏增添了趣味性和互动体验。
  • 它以像素风格呈现出可爱的猫咪、狗狗等虚拟宠物,这个小家伙不仅会跟随鼠标移动,在 Dock 上跑来跑去,偶尔还会打个盹儿,为日常电脑使用带来一丝轻松与乐趣。
  • 我甚至发现,当虚拟猫咪睡着时,还会发出真实猫咪那样的“咕噜”声,对于像我这样的养猫人来说,简直是种治愈。
  • 虚拟宠物有两种呈现形式,一种是在dock栏上方,另一种则是以图标形式存在于dock栏内,可以根据自己的使用情况选择。

软件下载

目前,Docko 已登陆 Mac App Store,用户可以免费下载体验。免费版提供一只黑色猫咪和一只黄色小狗,虽然选择不多,但依然十分讨喜,非常适合喜欢个性化桌面的用户。当然,如果你想解锁更多宠物款式,可以选择付费,价格为 18 元。

在 Mac App Store 搜索 “docko”,或通过以下链接即可下载: https://apps.apple.com/us/app/docko-pets/id6743445976?mt=12

这个桌面宠物也太可爱了吧!最先出现在Jack's Space

记一次发现小宇宙 iOS 版的跳转注入漏洞

2025年3月7日 14:42

漏洞风险:可以在其他应用借助小宇宙端内跳转任何网页。

此文章发布前,小宇宙已经修复了这个问题,所以你们可能不能复现这个问题了。

先看漏洞效果,这个漏洞的问题是你可以在小宇宙里跳转到任何网站,甚至是 PornHub。不过这个 Bug 不重要,重要的是发现 Bug 的过程

漏洞效果

0. 背景

在一天晚上,我在和朋友聊小宇宙的 URL Scheme,想要做个功能,可以实现一键打开小宇宙的节目页面。但卡在我面前的是,我不知道小宇宙的 URL Scheme 到底是什么。于是便开始了我的 Hack 之旅,也就找到了小宇宙的这个安全漏洞。

1. 找到小宇宙的 URL Scheme

由于 Scheme 是在 App 中定义的,所以当我想到要找 Scheme 之后,第一反应的是去拿 iOS App 的 Info.plist(因为 iOS 是将 Scheme 定义在 info.plist 当中)。

过去需要通过 IPA 备份、越狱等方式来获取到这个文件,不过得益于 M 系列支持在 macOS 上运行的原因,现在 IPA 的获得变得非常的简单。 先在 App Store 安装小宇宙,并在「应用程序」中找到小宇宙。

image

然后右键点击小宇宙,点击「显示包内容」

image

然后看到这样的内容,WrappedBundle 是一个假的应用程序,所以继续点击 Wrapper 往里跳转。

image

然后会发现里面还有一个 Podcast 应用,这个才是真正的小宇宙的 IPA 包。然后继续点击「显示包内容」

image

在包内容当中可以看到 info.plist 文件,使用 Xcode 或者 VSCode 打开 info.plist 文件。

image

info.plist 文件中,搜索 CFBundleURLSchemes ,找到了小宇宙的 URL Scheme(里面有很多个,但很多都是其他应用的,试一下就可以发现):cosmos://

image

2. 找到 URL Scheme 能够打开的页面

找到了 URL Scheme,只能通过 cosmos:// 打开应用首页,无法满足我的需求,于是开始继续寻找可能的 URL Scheme 。一般来说,这个时候就只能继续反编译 IPA 包或者 APK 包了。不过对于我来说,这些不是一个好的选项(成本太高)。

然后想到,小宇宙的网页似乎是提供了打开客户端的能力,所以可以从网页版找到突破口。通过简单的搜索,果然让我在网页前端找到了突破口。找到了 7 个 Scheme 。

image

当然,中间存在一些重复的 Scheme。所以最终梳理出来的 Scheme URL:

  • cosmos://page.cos/discover:打开发现页
  • cosmos://page.cos/shownotes/EPISODE_ID:打开节目的 Shownote 页面
  • cosmos://page.cos/episode/EPISODE_ID:打开节目的详情页
  • cosmos://page.cos/webView?url=:意义不明,看起来像是打开一个特定的 URL
  • cosmos://page.cos/web?url=:意义不明,看起来像是打开一个特定的 URL

3. 发现问题 URL Scheme

前面的三个很正常,但后面的两个带 URL 的引起我的注意 —— as a Hacker,你知道的,任何一个可能的输入框都可能成为我们的注入点,于是乎,我就构建了一个链接,来打开我的 Blog

cosmos://page.cos/web?url=https%3A%2F%2Fwww.ixiqin.com

将这个链接使用 Safari 打开,就会自动唤起小宇宙,并打开我的播客。

至此,我发现了小宇宙这个跳转注入漏洞,并快速将其反馈给小宇宙官方同学。

复盘:如何规避这样的问题

在这个 Case 当中,小宇宙因为没有设置 URL 跳转的白名单,导致实际上出现了跳转恶意网站的风险。理论上,作为应用提供商,出于安全合规的视角,最好是控制 URL 跳转的域名和空间,避免被恶意滥用。

或者也可以参考我们现在见到的很多网站,在外部跳转时加一个风险提醒

如果在这个 Case 当中,小宇宙在一开始就限制了可以打开有限域名,那也不会出现如今我这次的漏洞问题。

这个问题风险大么?

取决于如何定义和如何使用。如果只是跳转一些常规网站,自然是风险不大的。但如果不受限制的,比如跳转到一些诈骗网站,可能风险就是大的。

香港加密资产交易所的现状与未来

2025年3月18日 00:00

HashKey

这两年关注香港金融比较多,比如HashKey长桥证券。我其实是HashKey的推荐大使,但数据表明通过我的推荐链接注册的人数不多,通过验证使用的几乎没有,比起通过我的长桥注册链接的朋友数量,HashKey似乎存在感不强。

其实,香港的另一家加密资产交易所OSL一直让我去实名验证,发了数不清次数的邮件催促我,但我根本不愿意去体验(它比HashKey规模小)……

难道我的感觉是错觉?香港的加密资产交易市场现状是不是真的不行?这是为什么?

HashKey作为香港最大的合规加密货币交易所——一些香港证券公司甚至套壳使用它来交易加密资产,它每天的交易量非常小,通常BTC/USD交易对一天只有几百个BTC,香港的本位法币BTC/HKD交易对就更小了,一天甚至只有个位数的BTC交易量,多的时候也不过两位数。

比起Binance动辄上万个BTC的交易量,HashKey的交易深度完全不值一提,而事实上HashKey的交易体验非常好,完全支持法币的出入金,交易费用也不高,和Binance并无区别。

另一方面,香港的现货加密ETF资产交易量也不大,流动性较差,比起美国的也是不值一提,哪怕香港的ETH现货ETF已经实现质押收益(美国目前还没有实现)。

为什么没多少人去使用HashKey?其实原因不复杂。

香港本岛人口基数并不大,因此光靠香港本地人,用户数量自然不大,而且不少有钱的、年轻的香港人似乎去了其它国家。香港周边的东南亚国家,人家完全可以使用欧美的交易所,或者Binance之类的交易所。欧美用户正常情况下有大把的加密货币交易所,为什么要来香港交易?除非为了避税。

更为关键的是,数量最为庞大也是最有需求的内地大陆人,鉴于目前的合规要求,不得使用香港的加密资产相关服务,包括HashKey和加密货币ETF交易。

虽然HashKey目前的交易量很小,但不排除它在未来有很大的潜力。

首先,香港本身作为曾经的世界金融中心,如今极力拥抱加密资产,非常需要它来激活经济活力,推动香港发展为全球虚拟资产中心。而香港也能作为内地的一块试验田,在未来完成内地的一些想法,毕竟美国都把比特币作为国家战略储备了,其它国家不可能忽视它。

此外,还是有一些人群有着不可告人的目的,需要使用香港的加密交易服务,比如,我们的确看到香港有些人背着成袋的资金去香港的找换店交易,也发现交易所的USDT/USD交易对远大于其它交易对。

未来到底会怎么样?我们未来再来回看。

梯子/翻墙技巧: 一条命令就可以在服务器上设置一个Firefox/火狐浏览器代理

2025年3月7日 04:50

回国的时候最麻烦就是访问国外的网站:邮箱、油管视频等。在中国大陆,翻墙是违法的,但是好像在上海北京等大城市,可以申请许可,比如一些国际驻公司和政府机构因需要则可以申请绕过大墙网GFW/Great Firewall。

翻墙/建梯子的方式很多种,怕麻烦可以买现成的VPN,自己有服务器的可以建Wireguard等。这里再介绍一种简单的方式。

Docker容器里跑火狐FireFox浏览器

假设你的主机/服务器/VPS里已经装有Docker。只需要把下面的脚本存成一个BASH文件,比如 docker-firefox.sh 然后在命令行下跑:./docker-firefox.sh PASS 其中PASS是密码,这个我们下面要用到,PASS如果不写的话则要把 “-e VNC_PASSWORD=” 那行删掉即可。

#!/bin/bash
VNC_PASS=$1

docker run -itd \
    --restart always \
    --name=firefox \
    -p 5800:5800 \
    -v $(pwd)/config:/config:rw \
    --shm-size 1g \
    -e ENABLE_CJK_FONT=1 \
    -e VNC_PASSWORD=${VNC_PASS} \
    jlesage/firefox

等一会儿,这时候Docker会去拉取这个软件镜像,然后完事即可在浏览器里把你的 域名/IP地址 后加上5800端口, 5800是上面脚本里指定的,也可以改成你想要的端口。

docker-firefox-pull-image 梯子/翻墙技巧: 一条命令就可以在服务器上设置一个Firefox/火狐浏览器代理 I.T. 小技巧 技术 服务器 计算机 资讯 运维

输入这个脚本命令Docker就会加载容器 jlesage/firefox

这样跑起来了,你的主机就相当于一个中转/代理,你在国内的时候访问你的主机(如果你主机没被大墙网拉入黑名单的话),那你就可以在浏览器里的内嵌FireFox浏览器里打开其它国外网站了。

firefox-docker-on-browser 梯子/翻墙技巧: 一条命令就可以在服务器上设置一个Firefox/火狐浏览器代理 I.T. 小技巧 技术 服务器 计算机 资讯 运维

在浏览器里输入服务器IP/域名(HTTP)后加上 :5800 端口号 还需要输入刚刚指定的密码。

使用体验

可能是和我的主机配置有关,总觉得有点卡。不是很流畅。还有就是我想登陆我的GMAIL帐号,被GOOGLE认定有风险,要我改密码,吓得我只能作罢。

试了一下,看了一个油管广告,没有声音,所以通过这个技巧看油管视频是不现实的。

还有就是,目前只支持HTTP,而不支持HTTPS,如果域名开启了HSTS,那只能通过IP来访问HTTP。不过,你可以通过nginx/apache设置一个反代 reverse proxy 来访问HTTPS。

比如以下是在Nginx服务器配置443端口重新转向到这个FireFox代理:

server {
    listen 443 ssl;
    server_name domain_name

    ssl_certificate /root/fullchain.cer;
    ssl_certificate_key /root/key.key;

    location / {
        proxy_pass https://your-server-ip:5800;
        proxy_ssl_verify off;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

这玩意不适合日常工作,但是回国用于救急还是可以的。特别是不想在公共电脑上留下痕迹,只需要用强密码+HTTPS。等不需要用的时候记得把Docker容器删除即可。

docker stop firefox
docker rm -f firefox

这个没法支持多用户,也就是说多个用户同时登陆看到的是同一个窗口。

jlesage-firefox-in-docker-browser-scaled 梯子/翻墙技巧: 一条命令就可以在服务器上设置一个Firefox/火狐浏览器代理 I.T. 小技巧 技术 服务器 计算机 资讯 运维

测试可以在浏览器里打开谷歌

可以使用:docker logs -f firefox 来查看窗口的相关信息:

[init ] container is starting…
[cont-env ] loading container environment variables…
[cont-env ] APP_NAME: loading…
[cont-env ] APP_VERSION: loading…
[cont-env ] DISPLAY: executing…
[cont-env ] DISPLAY: terminated successfully.
[cont-env ] DISPLAY: loading…
[cont-env ] DOCKER_IMAGE_PLATFORM: loading…
[cont-env ] DOCKER_IMAGE_VERSION: loading…
[cont-env ] EGL_LOG_LEVEL: executing…
[cont-env ] EGL_LOG_LEVEL: terminated successfully.
[cont-env ] EGL_LOG_LEVEL: loading…
[cont-env ] GSK_RENDERER: executing…
[cont-env ] GSK_RENDERER: terminated successfully.
[cont-env ] GSK_RENDERER: loading…
[cont-env ] GTK2_RC_FILES: executing…
[cont-env ] GTK2_RC_FILES: terminated successfully.
[cont-env ] GTK2_RC_FILES: not setting variable.
[cont-env ] GTK_THEME: executing…
[cont-env ] GTK_THEME: terminated successfully.
[cont-env ] GTK_THEME: not setting variable.
[cont-env ] HOME: loading…
[cont-env ] LIBGL_DRIVERS_PATH: executing…
[cont-env ] LIBGL_DRIVERS_PATH: terminated successfully.
[cont-env ] LIBGL_DRIVERS_PATH: not setting variable.
[cont-env ] PULSE_CONFIG_PATH: executing…
[cont-env ] PULSE_CONFIG_PATH: terminated successfully.
[cont-env ] PULSE_CONFIG_PATH: not setting variable.
[cont-env ] PULSE_COOKIE: executing…
[cont-env ] PULSE_COOKIE: terminated successfully.
[cont-env ] PULSE_COOKIE: not setting variable.
[cont-env ] PULSE_SERVER: executing…
[cont-env ] PULSE_SERVER: terminated successfully.
[cont-env ] PULSE_SERVER: not setting variable.
[cont-env ] QT_STYLE_OVERRIDE: executing…
[cont-env ] QT_STYLE_OVERRIDE: terminated successfully.
[cont-env ] QT_STYLE_OVERRIDE: not setting variable.
[cont-env ] TAKE_CONFIG_OWNERSHIP: loading…
[cont-env ] XDG_CACHE_HOME: loading…
[cont-env ] XDG_CONFIG_HOME: loading…
[cont-env ] XDG_DATA_HOME: loading…
[cont-env ] XDG_RUNTIME_DIR: loading…
[cont-env ] XDG_STATE_HOME: loading…
[cont-env ] container environment variables initialized.
[cont-secrets] loading container secrets…
[cont-secrets] container secrets loaded.
[cont-init ] executing container initialization scripts…
[cont-init ] 10-certs.sh: executing…
[cont-init ] 10-certs.sh: terminated successfully.
[cont-init ] 10-check-app-niceness.sh: executing…
[cont-init ] 10-check-app-niceness.sh: terminated successfully.
[cont-init ] 10-clean-logmonitor-states.sh: executing…
[cont-init ] 10-clean-logmonitor-states.sh: terminated successfully.
[cont-init ] 10-clean-tmp-dir.sh: executing…
[cont-init ] 10-clean-tmp-dir.sh: terminated successfully.
[cont-init ] 10-fontconfig-cache-dir.sh: executing…
[cont-init ] 10-fontconfig-cache-dir.sh: terminated successfully.
[cont-init ] 10-init-users.sh: executing…
[cont-init ] 10-init-users.sh: terminated successfully.
[cont-init ] 10-nginx.sh: executing…
[cont-init ] 10-nginx.sh: terminated successfully.
[cont-init ] 10-openbox.sh: executing…
[cont-init ] 10-openbox.sh: terminated successfully.
[cont-init ] 10-pkgs-mirror.sh: executing…
[cont-init ] 10-pkgs-mirror.sh: terminated successfully.
[cont-init ] 10-pulse.sh: executing…
[cont-init ] 10-pulse.sh: terminated successfully.
[cont-init ] 10-set-tmp-dir-perms.sh: executing…
[cont-init ] 10-set-tmp-dir-perms.sh: terminated successfully.
[cont-init ] 10-vnc-password.sh: executing…
[cont-init ] 10-vnc-password.sh: creating VNC password file from environment variable…
[cont-init ] 10-vnc-password.sh: terminated successfully.
[cont-init ] 10-web-data.sh: executing…
[cont-init ] 10-web-data.sh: terminated successfully.
[cont-init ] 10-webauth.sh: executing…
[cont-init ] 10-webauth.sh: terminated successfully.
[cont-init ] 10-x11-unix.sh: executing…
[cont-init ] 10-x11-unix.sh: terminated successfully.
[cont-init ] 10-xdg-runtime-dir.sh: executing…
[cont-init ] 10-xdg-runtime-dir.sh: terminated successfully.
[cont-init ] 15-cjk-font.sh: executing…
[cont-init ] 15-cjk-font.sh: installing CJK font…
[cont-init ] 15-cjk-font.sh: fetch https://dl-cdn.alpinelinux.org/alpine/v3.21/main/x86_64/APKINDEX.tar.gz
[cont-init ] 15-cjk-font.sh: fetch https://dl-cdn.alpinelinux.org/alpine/v3.21/community/x86_64/APKINDEX.tar.gz
[cont-init ] 15-cjk-font.sh: (1/1) Installing font-wqy-zenhei (0.9.45-r3)
[cont-init ] 15-cjk-font.sh: Executing fontconfig-2.15.0-r1.trigger
[cont-init ] 15-cjk-font.sh: Executing mkfontscale-1.2.3-r1.trigger
[cont-init ] 15-cjk-font.sh: OK: 694 MiB in 168 packages
[cont-init ] 15-cjk-font.sh: terminated successfully.
[cont-init ] 15-install-pkgs.sh: executing…
[cont-init ] 15-install-pkgs.sh: terminated successfully.
[cont-init ] 55-check-snd.sh: executing…
[cont-init ] 55-check-snd.sh: sound not supported: device /dev/snd not exposed to the container.
[cont-init ] 55-check-snd.sh: terminated successfully.
[cont-init ] 55-firefox.sh: executing…
[cont-init ] 55-firefox.sh: terminated successfully.
[cont-init ] 56-firefox-set-prefs-from-env.sh: executing…
[cont-init ] 56-firefox-set-prefs-from-env.sh: terminated successfully.
[cont-init ] 85-take-config-ownership.sh: executing…
[cont-init ] 85-take-config-ownership.sh: terminated successfully.
[cont-init ] 89-info.sh: executing…
╭――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――╮
│ │
│ Application: Firefox │
│ Application Version: 135.0-r0 │
│ Docker Image Version: 25.02.2 │
│ Docker Image Platform: linux/amd64 │
│ │
╰――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――╯

有用简单的IT小技巧

英文:Using Docker to Run Firefox as a Proxy: A Simple Solution for Accessing Blocked Websites in China

本文一共 804 个汉字, 你数一下对不对.
梯子/翻墙技巧: 一条命令就可以在服务器上设置一个Firefox/火狐浏览器代理. (AMP 移动加速版本)

扫描二维码,分享本文到微信朋友圈
75a5a60b9cac61e5c8c71a96e17f2d9c 梯子/翻墙技巧: 一条命令就可以在服务器上设置一个Firefox/火狐浏览器代理 I.T. 小技巧 技术 服务器 计算机 资讯 运维
The post 梯子/翻墙技巧: 一条命令就可以在服务器上设置一个Firefox/火狐浏览器代理 first appeared on 小赖子的英国生活和资讯.

相关文章:

  1. 出租房子的教训: 让中介找人上门换一个水龙头竟然要204英镑! 我老婆在剑桥Bar Hill有一套出租房/Buy to Let,但她不想管理。每次房子出问题需要修理时,中介都会联系我。有房子出租闹心的事情真是多,真的印证了那句话:操着卖白粉的心,挣着卖白菜的钱!每次最怕的就是接到中介的电话,因为一有电话就意味着房子有问题要修/要花钱了。 几天前,中介打电话来说,租户反映厨房水龙头打开时有奇怪的声音,挺吓人的。我当时回复他们说我会找个水管工/Plumber第二天过去看看。但挂了电话后,我联系了我认识的中国装修师傅。他手艺很好,在英国生活了几十年,以前帮我家做过不少装修。不过他这次没时间,也不太愿意接这种小活。 前两年这个师傅给我们家换地板,翻新三个厕所,还有就是阳光房地板,门前加建了个Porch等,最近房子也在重新折腾装修,之后弄完再详细说说。 我懒得在网上(比如 MyBuilder.com)再找其他人,就让中介安排了他们自己的工人。 后来,中介回电说水管工已经在房子里了,并报价修理费为170英镑,加上VAT税后(20%)一共204英镑。如果不修,也要付140英镑的上门费,加税后差不多168英镑。觉得价格有点高,但也只能咬牙同意了。 其实让我不爽的是自己为了省事,最后只能任人宰割。老外只要上门就收钱,我当时虽让中介确认报价,但没问上门费的问题,现在骑虎难下。听说中介找的合同工都比较贵。 那个房子一共让中介找过四五次人修房子(各种问题:上次厨房水管堵了,被收了400多英镑,据说水管工花了三小时通下水道),主要是因为这次我认识的师傅没时间。其实我还认识另一个会修房子的中国人,这样看来,以后可能不会再让中介找人了。 去年冬天,因为恶劣天气,房子顶有一小块掉了,中介找人报价要1800英镑,还好我没同意。后来找了个中国师傅,才花了350英镑,而且当时有Landlord保险还全额报销了。 PS:我发了封邮件给中介小小抱怨了一下。 Hi there,...
  2. 真正意义上的鼓励优秀作品 – 优秀被错过文章 有奖励啦! 大家都知道我的日报第一项就是 《那些优秀可能被错过的文章》这个算法是通过我自己的认识选出一些比较 好的文章 但是收益却比较低, 那么, 通过 @dailychina 天天回复, 比如: 对于作者来说, 除了心理得到表扬之外 并没啥卵用, 是吧. 而且有些作者经常上榜啊, 于是,...
  3. 按揭贷款(房贷,车贷) 每月还贷计算器 去年给银行借了17万英镑 买了20万7500英镑的房子, 25年还清. 前2年是定率 Fix Rate 的合同 (年利率2.49%). 每个月大概是还 700多英镑. 有很多种还贷的计算方式, 定率/每月固定 是比较常用的. 简单来说就是 每个月交的钱是...
  4. 一行代码修改引起的血案 – (二) 上一篇说到乱改了一行代码引来的错误, 自己也检讨了一下. 以下已经同事同意: 看了XX在群里发的内容 http://coolshell.cn/articles/4576.html,然后到 http://thread.gmane.org/gmane.linux.kernel/1124982/focus=1126216 仔细看了Linus Torvalds发的邮件,受益匪浅,摘了几段内容附在了 < how to fix bug.xls > 中....
  5. 逻辑题 – 帽子是黑的还是白的? 老婆每天晚上都会打开一个 智力测试的 APP 然后 有一天就问我这么一道智力题. 有三顶白色帽子 两顶黑色帽子. 有三个人被蒙着眼睛 然后随机的被戴上帽子 其中剩下的两顶就放在隔壁屋 每个人只能看到其他两人帽子的颜色 并不知道自己头顶上帽子的颜色. 每个人只有一次猜对的机会 不能互相交谈 已知第一个人看到其他两个人头顶上的颜色是白色...
  6. 使用AWK来看见证人生成块的速度 每次见证人出块, 媳妇总我说 “又生了”. 每次出块我总会去算一下离上次出块多少时间, 这是可以通过当前块数和上次出块数算出来的. 首先, 我们可以通过 docker logs 来显示很多很多的记录: 有一个脚本 ./run.sh logs是显示最近几条记录 (tail) 我们可以通过管道...
  7. STEEM API 系列之获取货币转换 STEEMIT 有 SBD, STEEM, STEEM POWER 还有 VESTS这几个概念. 时不时, 我们就需要知道它们之间几个转换关系: 1个STEEM等于多少SBD? 1个SBD 等于多少STEEM? 1个SP等于多少VESTS? 1M的VESTS...
  8. 测试 Crontab 是否工作的PHP脚本 LINUX 下的 crontab 是用来定时运行 程序任务的工具. 当然你需要简单配置一下 一般是通过 crontab -e 命令 来编辑. 如果你想简单知道 是否配置正确 和任务实际上会被调用的频率 你可以通过下面简单的PHP脚本来实现....

Chromebook折腾之2025

2025年3月5日 14:40

最近淘了一台洋垃圾Chromebook,折腾了一段时间,目前已经基本在日常使用了。在折腾过程中查找了许多的网上资料,解决了不少中文环境使用ChromeOS的问题,这里就分享一下Chromebook的选购和软件安装。

ChromeOS是什么

ChromeOS是Google从2009年开始开发的项目,可以简单理解为在Linux内核上运行的,以Chrome浏览器作为桌面环境,主要功能也是运行Web应用程序的一个操作系统。在之后,该系统也支持了运行Android应用程序,以及通过容器运行Linux程序,这样一套组合下来,我们就获得了一个原生支持运行Android应用,Linux应用和Web应用的系统,这比在Windows下面折腾Linux子系统,Android子系统要流畅得多。

目前为止,想要运行ChromeOS有两种方式,第一种就是购买ChromeBook,也就是搭载了ChromeOS的笔记本电脑或者触屏电脑。第二种方式,Google在2022年发布了ChromeOS Flex,让用户可以在经过认证的设备上安装ChromeOS Flex,包括一些Mac电脑也是支持的。

而想要激活ChromeOS,你需要有可以顺畅访问Google服务的网络。如果你没有这个条件,来自中国的fydeOS它是一个本地化的ChromeOS,内置了本地化定制和国内可以使用的网络服务,感兴趣可以去他们的官网看看。

Chromebook适合谁

ChromeOS最初设计出来也主要是方便云端办公,提供简单、快速、安全的环境,因此它更适合于对于性能没有要求,而希望简单吗体验的人。比如说:使用在线文档的文字工作者,得益于Google doc,飞书文档,语雀等文字和表格类在线工具,Chromebook简单的功能以及比较长的续航是一个性价比比较高的选择。除此之外,对于性能没有要求的开发者和数码极客或许也会考虑由于一台自己的Chromebook。

最新的Chromebook有两档标准,普通的Chromebook,以及Chromebook Plus,普通的Chromebook可能只搭载Intel Celeron处理器以及4GB的ROM, Plus也只是它性能的两到三倍。目前Chromebook在国内没有销售,通过天猫国际等平台平台购买的新机器一般也都比较贵没有性价比。对于普通用户国内平台在销售的平板电脑或者笔记本都比它有性价比的多。

而对于我所说的极客用户来说,在闲鱼淘一台洋垃圾Chromebook可能是一个比较有性价比的选择。比如我这台Lenovo Duet5,骁龙7C,8GB内存,256GB存储,13寸的OLED屏幕,搭配触控笔加键盘,支持平板模式和桌面模式,只要不到1500块钱,相比于iPad,看起来还是有点性价比的。

Chromebook选购指南

再次强调一下选择Chromebook需要保证有能够激活Google服务的网络环境。不具备的可以考虑fydeos,以及他们的Fydetab Duo设备。

在淘设备的时候,因为我们可能买到的是2019年或者更早发布的设备,我们需要关注设备的自动更新到期时间(简称AUE),所有ChromeOS设备都能够借助于Chrome浏览器几乎同步的更新节奏收到Google的定期更新。因此决定购买之前可以在Google的这个页面看一下该产品型号的AUE日期。

其次,电池健康度也是选择二手Chromebook产品时候值得关注的信息。本身购买Chromebook就是为了优秀的能耗和续航体验,电池不行这些就没办法完全达成了。购买前最好和商家沟通让对方打开「关于 ChromeOS > 诊断」界面并提供截图,可以在这个界面中清楚地看到当前设备的电池寿命、循环计数等信息。从这里可以大概预估该设备之前的运行时长,并且电池寿命高于90%应该是比较好的。我在这里就踩到了坑,因为是专门的二手商家,说了是库存设备,并且说没法激活设备不愿意提供截图导致我收到的设备实际上电池已经循环过了300多次,电池寿命只有86%,同时因为运行时间过长oled屏幕也有一点烧屏了。

最后,屏幕这块OLE屏幕可以让卖家把屏幕亮度跳到最低拍照这样也能看到一部分屏幕的缺陷,以及全白页面拍照测试等。关于型号的话,考虑到Android应用的兼容性,我选择的是ARM芯片的Duet设备,如果更加关注Linux应用的兼容性或许可以考虑X86芯片的设备。设备的型号这块,除了我提到的Duet,Google推出的Pixelbook Go, Pixelbook也是可以考虑的型号。

最后的最后,实际购买之前可以考虑使用现有设备刷如ChromeOS Flex或者fydeOS体验一下再做决定。

ChromeOS 初始化

ChromeOS本身的内核是Linux,但是为了安全,我们是没办法在上面安装Linux应用的,同时Android应用的安装也必须通过Play store才能安装,因此如果想要获得系统的完全控制权是需要开启开发者模式的。开启开发者模式后可以直接安装Android APK文件,同时也拥有了Root权限,可以在系统做修改,比如安装类似Mac下面homebrew的chromebrew工具等。但是代价是,每次启动电脑都会先跳出一个60s的警告页面(可以跳过),而在普通模式和开发者模式之间切换,所有的系统数据都会被清除,需要提前做好备份。

在我体验完开发者模式之后,决定还是回到安全模式。对于大部分人也都不需要开发者模式,我们通过Linux子系统开启Linux子系统的开发者模式,也就可以通过ADB来安装Android应用。因此如果想要开启开发者模式可以查看网上的资料。 初始化,可以通过家庭的软路由,或者手机上面开启Clash作为代理服务,在连接完网络后,修改网络中的代理服务,把手机或者软路由作为Chromebook的代理服务器,从而可以激活服务。同时要系统更新和安装Linux子系统需要稳定的翻墙服务,不然可能会失败。

ChromeOS初体验

ChromeOS内已经内置了一部分Web应用,包括了Google全家桶和一些工具应用。在未连接键盘鼠标前是平板模式,连接了之后为桌面模式。

以上为桌面模式,打开两个应用平铺,左下角为应用列表。

以上为平板模式的桌面

很多场景也可以通过浏览器进行,对于一些提供了PWA的网站,可以点击地址栏的安装按钮,这样就会生成一个桌面图标方便下次使用。也可以在Chrome应用商店安装扩展程序。

因为登录了Google账号,Chrome浏览器上安装的扩展程序,一些设置,书签等也都会同步过来。

同时ChromeOS还支持与Android手机连接,能够对手机进行简单的控制,包括手机的静音,地理位置权限开关,控制手机的热点并连接上网,查看手机最近的照片,打开的Chrome标签页等,如下图所示。

对于中文输入,Chrome内置了拼音输入法,如果使用双拼的话可以考虑使用fydeos提供的真文韵输入法,不过真文韵输入法没有软键盘,在平板模式还是没法使用,另外真文韵在Linux应用也无法使用,解决方法后面再说。

配置Linux子系统

Linux系统模式是未开启的,需要首先到「关于 ChromeOS 」中开发者部分开启,最新版本默认安装的是Debian 12,因为需要到Google的服务器上下载Linux镜像文件,这个过程可能非常慢,我这里差不多半个小时才完成。

有了Linux系统,我们首先需要安装中文环境,执行如下命令安装中文字体:

1
sudo apt install fonts-wqy-microhei fonts-wqy-zenhei fonts-noto-cjk

Linux上面是没法使用系统的输入法的,我们需要安装Linux的中文输入法,我这里就是安装的fcitx5,可以使用如下命令安装:

1
sudo apt install zenity fcitx5 fcitx5-rime

安装之后在 /etc/environment.d/ 文件中创建一个im.conf文件,并且写入如下内容:

1
2
3
4
GTK_IM_MODULE=fcitx
QT_IM_MODULE=fcitx
XMODIFIERS=@im=fcitx
SDL_IM_MODULE=fcitx

之后手动打开fcitx5,并且配置好自己的输入法选项就可以在Linux中使用应用了。

除此之外,就跟正常使用linux一样,安装的Linux应用如果是有桌面图标的也会在Chrome的应用列表中展示,同样对于deb文件,也可以直接在chrome的文件管理器中直接点击安装。

现在ChromeOS也支持了安装多个容器,也就是说可以运行多个不同的Linux,感兴趣的可以看看这位博主的这篇安装ArchLinux的文章

安装微信

微信算是每个人都必须有的通信工具了,在ChromeOS中有两种方式可以安装,一个是安装到Android子系统,直接在Google play下载就行了,另一种则是安装Linux版本的桌面微信。

但既然有这么大的屏幕,当然是桌面版使用体验更好了。我这里介绍一下在我的Debian12下面安装arm版本的微信的过程吧,因为微信的有一些依赖系统内是没有的组件需要安装。

1
sudo apt install libatomic1 -y && wget -O libwebp6.deb https://security.debian.org/pool/updates/main/libw/libwebp/libwebp6_0.6.1-2.1+deb11u2_arm64.deb && sudo dpkg -i libwebp6.deb

除了这个之外还缺少一个libtiff5,debian12上面已经有libtiff6了,我们创建一个链接就可以了。

1
sudo ln -s /usr/lib/aarch64-linux-gnu/libtiff.so.6 /usr/lib/aarch64-linux-gnu/libtiff.so.5

之后我们应该就可以使用Linux版本的微信了。

另外还推荐在Linux子系统安装stalonetray,这样就可以展示Linux的软件的托盘,比如去查看输入法状态,和切换输入选项等。可以参考这篇文章

对于Linux直接在Chrome点击deb文件安装的应用,虽然安装完了但是有可能点击图标打开的时候总是在转圈却打不开,这可能是因为程序出错了,可以在命令行中手动运行,这样错误日志就可以查看了。

配置安装非Google play的Android应用

如果想要安装国内的应用,可能很多都无法在Google play商店下载,一种方式是打开ChromeOS的开发者模式,但是那样每次开机就要忍受开机警告。我这里选择通过Linux子系统来安装。

首先打开「关于 ChromeOS -> Linux开发环境 -> 开发Android应用」,将其中的启用ADB调试打开。

点击启用的时候会有如下提示:

并且如果停用的话也会将Chromebook恢复出厂设置,所有数据被清空,使用这个功能需要谨慎。

再打开Linux命令行,执行如下命令安装adb工具。

1
sudo apt install adb

之后打开「设置 -> 应用 -> 管理Google Play 偏好设置 -> Android设置」,这样就进入Android系统的设置应用中了,可以在关于设备中多次点击版本号,开启Android子系统的开发者模式,在然后到系统,开发者选项中打开ADB调试。之后在linux命令行执行如下命令并显示正常就说明配置好了。

1
adb devices

之后就可以通过ADB安装程序了,当然也可以选择使用adb安装一个国内的应用商店,之后通过应用商店安装应用。

ChromeOS的体验介绍

使用了一段时间之后来说,作为一个轻量化的Linux 本来说,这台设备还是符合期望的。Linux,Android子系统都和宿主系统有着很好的深度绑定,使用子系统的应用也和使用宿主一样有着很好的体验。而在我这里一大缺陷为,因为Linux子系统和Android子系统都被划分到了私有网络,因此它们实际上网络是和Chromeos宿主共享的,但是和局域网的其他设备不是在同一个子网里面的,因此类似LocalSend这种工具是不能使用的。这里目前我的解决办法是使用fydeOS提供的fyDrop工具和其他局域网进行文件传输。

这个设备同时还支持通过usb typec接口连接外接显示器,chromeos有着不错的窗口管理,桌面分屏,这些功能都为使用体验加分许多。

如果只是轻办公我感觉这是一台很棒的设备,但是得益于这个性能,想要在这上面做Android开发,去编译程序那就不敢想象了。而至于要不要入坑,还是要你自己决定。

最后照例来推荐一些可以参考的资料:

  1. fydeOS源于chromeOS,这边的中文资料都可以参考:https://community.fydeos.com/t/topic/40986
  2. Chrome 官方的文档: https://chromeos.dev/en/linux
  3. 解锁开发者模式和一些折腾,可以参考这边文章和博主的其他文章: 打造一台适合生产的Chromebook

看完评论一下吧

通过Service Worker‌ 缓存优化 Typecho 的首页访问加速

2025年2月19日 00:05

Service Worker‌ 是一种运行在浏览器背景的脚本,它可以在页面关闭后继续运行,主要用于处理网络请求、缓存资源、推送消息等功能。

认识

Service Worker 也是一个后台运行的脚本,充当一个代理服务器,拦截用户发出的网络请求,比如加载脚本和图片。Service Worker 可以修改用户的请求,或者直接向用户发出回应,不用联系服务器,这使得用户可以在离线情况下使用网络应用。它还可以在本地缓存资源文件,直接从缓存加载文件,因此可以加快访问速度。

条件

要使用 Service Worker‌ 确保网站支持 HTTPS 协议,这是必要条件。

注册

Typecho 使用的话,需要在主题的 header.php 或者 footer.php 里面添加如下代码,以完成 Service Worker 注册。

<script>
    if ('serviceWorker' in navigator) {
        navigator.serviceWorker.register('/serviceworker.js')
            .then(registration => {
                console.log('Service Worker 注册成功:', registration);
            })
            .catch(error => {
                console.log('Service Worker 注册失败:', error);
            });
    }
</script>

新建

在网站的根目录新建 serviceworker.jsoffline.html 两个独立文件,其中js为引入的必需文件,offline为非必需离线文件。

const CACHE_NAME = 'typecho-cache-v2';
const OFFLINE_URL = '/offline.html';

const urlsToCache = [
    '/',
    '/index.php',
    '/usr/themes/你的主题名字/css/style.min.css', /**css文件
    '/usr/themes/你的主题名字/js/script.min.js', /**js文件
    '/usr/uploads/ /**存储在服务器的图片或文件
    '/favicon.ico',
    OFFLINE_URL,
    'https://artalk.bosir.cn/dist/Artalk.js' /**第三方缓存文件
];

self.addEventListener('install', event => {
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then(cache => cache.addAll(urlsToCache))
            .then(() => self.skipWaiting())
    );
});

self.addEventListener('activate', event => {
    event.waitUntil(
        caches.keys().then(cacheNames => {
            return Promise.all(
                cacheNames.map(name => {
                    if (name !== CACHE_NAME) {
                        return caches.delete(name);
                    }
                })
            );
        }).then(() => self.clients.claim())
    );
});

self.addEventListener('fetch', event => {
    if (event.request.method !== 'GET') return;

    event.respondWith(
        caches.match(event.request).then(response => {
            if (response) return response;

            return fetch(event.request).then(response => {
                if (!response || response.status !== 200 || response.type !== 'basic') {
                    return response;
                }
                const responseToCache = response.clone();
                caches.open(CACHE_NAME).then(cache => cache.put(event.request, responseToCache));
                return response;
            }).catch(() => {
                return caches.match(OFFLINE_URL);
            });
        })
    );
});

offline模式,即离线模式,f12 打开开发者工具后找到 online选项,点击后会出现offine 字样,点击打开离线模式,刷新页面会发现首页依然可以访问,而没有缓存的页面就显示下面的离线页面。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>水清无鱼</title>
</head>
<body>
    <h1>离线模式</h1>
</body>
</html>

缓存

为了防止在浏览器需要请求新版本的 serviceworker.js 文件时,而文件自身被缓存,所以需要给 serviceworker.js 文件单独指定缓存头 Cache-control: no-storeno-cache 。然后在 Nainx 的 server { 区域内添加:

# serviceworker.js 不设置缓存
location ~* /(.*)/serviceworker\.js {
    add_header Cache-Control no-cache;
    add_header Pragma no-cache;
    add_header Expires 0;
}

添加完毕后,重启Nginx服务器。

更新

在更新 Service Worker 时,需要修改 CACHE_NAME 的版本号以触发缓存更新

const CACHE_NAME = 'typecho-cache-v1';
或
const CACHE_NAME = 'typecho-cache-v2';

总结

整体来说,只要不涉及复杂的主题和js,Typecho 已经足够快,且拥有良好的打开速度。可如果你像我一样比较追求极致性能,喜欢折腾,也可以尝试一下。我的目标是把博客优化到和静态博客一样丝滑,目前缓存过后进入首页只需要100ms以内,en...

题外话

目前 typecho 已经出现了 1.3.0测试版本,没错,就是目前我现在使用的这个版本。不过我并不建议大家进行升级,毕竟BUG有点多。我目前是全站 0 插件,测试版本的兼容性有待考量。且如果php像我一样为最新版,出现的问题可能更多...

参考资料

1.// cloud.tencent.com/developer/article/2005909

2.// www.luolt.cn/archives/1848.html

3.// www.bookstack.cn/read/webapi-tutorial/docs-service-worker.md

小白也能轻松上手!用 Cherry Studio 搭建你的专属 AI 知识库

2025年2月17日 22:54

在 AI 工具不断迭代更新的今天,不知道大家是否和我一样,希望有一款 AI 工具能够检索自己整理的笔记资料,并以对话的方式直接输出结果。这其实就是知识库(RAG)的核心理念。借助 Cherry Studio 软件,结合 Ollama 中的开源模型,以及 DeepSeek、Gemini 等提供的免费额度,即便像我这样的零基础小白,也能轻松搭建属于自己的 AI 知识库,无需任何专业背景。

效果图

AI 会检索知识库里的信息来回答你的问题。

原理

原理?我就一小白哪懂什么原理🤯,我只知道需要一个文本嵌入模型来预处理知识库里的素材,然后需要一个语言模型(DeekSeek、ChatGPT)来帮我输出结果,而两个模型质量会影响我们输出的结果。所以,文本嵌入模型和语言模型是我们需要准备的东西。

需要的工具

  • Ollama:通过 Ollama 安装 bge-m3 文本嵌入模型,用来预处理放入知识库的素材。
  • DeepSeek API Key:通过 DeepSeek官网、阿里云百炼、腾讯云、火山引擎等平台薅取免费的 token,通过 API 调用满血版的 DeepSeek,无需本地部署大模型。

因为本地只需要安装一个软件和一个文本嵌入模型,对配置要求不高,一般电脑就可以胜任。

具体步骤

  1. 通过官网地址下载免费的 Cherry Studio 软件,并安装。
  2. 官网下载 Ollama(https://ollama.com/)并安装,我们需要通过它来安装文本嵌入模型来预处理知识库里的材料。
  3. 安装好 Ollama 后,打开终端应用(Mac 用户搜索 “终端”,Windows 搜索“Terminal”),在黑黑的界面中输入 ollama pull bge-m3 来下载和安装文件嵌入模型,等待完成即可。完成后输入 ollama list 会显示出我们刚刚安装的模型。
  1. 选择一个平台去获取免费的 DeepSeek 使用额度,推荐:阿里云百炼腾讯云火山引擎
  2. 我这里以阿里云百炼为例,点击上方链接进入阿里云百炼,初次登陆需要进行实名认证(https://myaccount.console.aliyun.com/cert-info)。通过阿里云百炼链接进入后台后,点击右上角的“创建我的API-KEY”,生成一个即可。至此,所有需要准备的东西都完成了。
  1. 先配置文本嵌入模型:打开 Cherry Studio 软件,点击左下角 设置 图标,选择“模型服务”,在列表中选择 “Ollama”,点开右上角的 开关,“API 密钥”处空着,在“模型”部分点击“添加”,在窗口的“模型ID”处填入 bge-m3:latest 即可。
  1. 然后配置语言模型(DeepSeek):点击左下角设置图标,选择“模型服务”,在列表中选择 “阿里云百炼”,点开右上角的 开关,在“API 密钥”处填写后台申请的 Key,在“模型”部分点击“添加”,在窗口的“模型ID”处填入deepseek-r1 即可。(如果有需要也可以将其他模型添加上,比如 deepseek-r1等等。)
  1. 现在我们就可以配置知识库了,点击Cherry Studio 软件窗口左侧的“知识库”按钮,“添加”一个知识库,“名称”随便填,“嵌入模型”中选择我们配置的bge-m3:latest
  1. 现在只需要将你想添加到数据库中的文件拖入即可,当嵌入模型预处理完成后,会在相应的文件右侧显示出一个绿色的小勾
  1. 最后,你只需要创建一个对话,并勾选中“知识库”选项,就可以调用属于你自己的知识库啦,记得在对话框顶部选择你想使用的语言模型哦~

小结

还记得 ChatGPT 刚问世那会儿,网络上随处可见对 AI 的质疑声。很多人觉得它不过是会拼凑套话的”人工智障”,甚至戏称其为”高级复读机”。但谁也没想到,短短两三年间,人工智能竟以惊人的速度进化——从实时联网检索信息,到成为程序员的智能搭档,再到深度研究功能的突破性进展,这些技术突破不仅让普通职场人的工作效率翻倍,甚至开始替代部分传统岗位。身处这场智能革命的浪潮中,与其被动担忧被取代,不如主动掌握新工具。当 DeepSeek 被身边人津津乐道的时候,我们是否更应该思考:如何让这个聪明的数字助手,真正成为提升个人竞争力的加速器。

小白也能轻松上手!用 Cherry Studio 搭建你的专属 AI 知识库最先出现在Jack's Space

使用Leafletjs实现足迹地图功能

2025年2月9日 11:40

我的博客上面挂着一个使用Leaflet实现的足迹地图功能,最近又给他添加了一些功能并且进行了一些美化。之前也有人问题这个怎么实现的,趁着刚折腾完来分享一下。

代码库的选择

早前一直想要做一个足迹的功能,像是国内的百度地图和阿里地图都有js的sdk,但是他们的sdk使用不简单,并且他们的地图只有国内的。后来了解过google map以及mapbox,但是都没有深入研究。后来看到博主水八口记使用了leaflet还比较简单,就使用这个库来实现了我的足迹功能。

地图图层

使用leaflet的一大好处是,你可以自由使用你想要的地图图层,对于符合Leaflet的地图瓦片地址我们是可以直接使用的,通常是类似这种格式的地址: https://{s}.somedomain.com/{foo}/{z}/{x}/{y}.png,其中的{z}/{x}/{y}是必须要支持的,leaflet会在运行的时候替换具体的值,从而请求对应的放大级别(z,zoom), 对应坐标(x, y)的瓦片进行渲染。

一般使用cartocdn提供的openstreetmap的地图时,是可以直接使用的,但是我们如果想要使用mapbox地图或者其他地图供应商的时候,就需要借助插件了,可以在这个页面看看有没有Plugins - Leaflet - a JavaScript library for interactive maps

对于地图图层,leaflet是支持同时加载多个图层的,比如说我可以添加一层底图再添加一层天气卫星图。

我们这里先看一下如何创建一个地图并且设置我们的地图图层. 首先需要引入leaflet的css和js文件

1
2
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
<!-- js引入一定要放到css的后面 --> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>

之后,在我们需要显示地图的位置放一个div元素,并且设置一个id,这样我们在后面的js代码中才能控制它:

1
<div id="footprintmap"></div>

同时我们可以通过css设置这个容器的宽度高度:

1
2
3
4
#footprintmap {
width: 100%;
 height: 180px;
}

这些做完之后就可以在javascript中去创建地图对象,并且给它添加地图图层了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<script type="text/javascript">

 //地图的版权声明,使用三方地图数据出于对版权的尊重最好加一下
      var cartodbAttribution = '&copy; <a href="https://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a> contributors, &copy; <a href="https://carto.com/attribution" target="_blank">CARTO</a>';
      var map = L.map('map', {gestureHandling: true, minZoom: 1, maxZoom: 14}).setView([33.3007613,117.2345622], 4); //创建地图,设置最大最小放大级别,setView设置地图初始化时候的中心点坐标和放大级别
      map.zoomControl.setPosition('topright'); //设置放大控制按钮的位置
      map.createPane('labels');

      map.getPane('labels').style.zIndex = 650;

      map.getPane('labels').style.pointerEvents = 'none';

      L.tileLayer('https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png', {

    attribution: cartodbAttribution

}).addTo(map); //添加地图图层到map对象当中

</script>

添加足迹点到地图中

经过以上的步骤我们就可以在网页上展示一个地图了,而我们实现足迹功能一般会给我们去过的地点打上标记。一种方法是给去过的城市做一个蒙层,一种方式是加一些点标记。这里先看加点标记的方法。

标记在Leaflet中称为Marker, 我们可以使用以下代码添加默认的Market:

1
marker = new L.marker([33.3007613,117.2345622]).bindPopup("popup text").addTo(map);

效果如下:

在上面我们通过bindPopup来设置点击Marker之后弹出的内容,其中我们是可以设置HTML元素的,因此我们就可以显示图片或者超链接之类的内容了。

如果不喜欢这个默认的蓝色Marker,也可以替换为图片。比如我用如下的代码就设置类一个svg图片作为Market标记图:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
function colorMarker() {
  const svgTemplate = `
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" class="marker">
      <path fill-opacity=".25" d="M16 32s1.427-9.585 3.761-12.025c4.595-4.805 8.685-.99 8.685-.99s4.044 3.964-.526 8.743C25.514 30.245 16 32 16 32z"/>
      <path stroke="#fff" fill="#ff471a" d="M15.938 32S6 17.938 6 11.938C6 .125 15.938 0 15.938 0S26 .125 26 11.875C26 18.062 15.938 32 15.938 32zM16 6a4 4 0 100 8 4 4 0 000-8z"/>
    </svg>`;
  const icon = L.divIcon({
    className: "marker",
    html: svgTemplate,
    iconSize: [28, 28],
    iconAnchor: [12, 24],
    popupAnchor: [7, -16],
  });
  return icon;
}

marker = new L.marker([lat, lng], {
    icon: colorMarker(),
  }).bindPopup(popupText).addTo(map);

主要是在前面创建marker的时候传的这个icon,你也可以传普通的图片。

如果我们需要展示多个点的时候,我们可以把这些点的数据存储成一个json,并且把他作为一个JavaScript对象加载,再读取他把每个点添加到地图中。 我就创建了一个points.js的文件保存所有的点:

1
2
3
let points = [
    ["<b>北京</b><i>Beijing</i><a href='/2025-01-beijing/'><img src='https://img.isming.me/photo/IMG_20250101_133455.jpg' />北京游流水账</a>", 40.190632,116.412144],
    ["<b>广州</b><i>Guangzhou</i>", 23.1220615,113.3714803],];

内容大概如上:

1
2
<!--加载点数据这样我们在javascript环境中就可以拿到points这个数组-->
 <script type="text/javascript" src="/points.js"></script>

以上加载了点数据,通过下面的代码来读取并且添加点:

1
2
3
4
5
6
7
for (let i = 0; i < points.length; i++) {
//循环遍历所有点,并且保存到如下三个变量中
  const [popupText, lat, lng] = points[i];
  marker = new L.marker([lat, lng], {
    icon: colorMarker(),
  }).bindPopup(popupText).addTo(map);
}

到此为止就完成了足迹点功能的开发。

去过的区域图层开发

而我们要实现去过的城市标记,这个时候就不是一个一个的点了,我们可能给去过的城市添加遮罩,这个其实就是给地图上画一个新的图层。每一个城市本质上就是许多个点围成的多边形,我们可以使用Leaflet提供的polygon方法来绘制,但是我们需要给把每个城市的多边形的各个顶点找到并且组织成一个数组,工作量真的是巨大的。

这样的难题我们不是第一个遇到的,前人已经遇到并且帮我们解决了。在2015年就有了GeoJson这种用Json描述的地理空间数据交换格式,他支持描述点,线,多边形。而Leaflet对齐也有支持。因此,我们只需要找到我们所需要的城市的geojson数据的MultiPolygon或者Polygon数据,就可以在Leaflet中进行绘制了。

对于中国的数据,我们可以在阿里云的datev平台进行下载,你可以省份数据或者按照城市甚至更小的行政单位下载数据。对于国外的数据可以到github上面去查找,这里是一份国家数据: datasets/geo-countries: Country polygons as GeoJSON in a datapackage

对于我们下载的中国的geojson数据,因为比较详细,也就比较大,我们可以使用mapshaper这个工具来对数据进行一些处理,直接使用Simplify功能,使用它减少点的数量,从而减少我们的文件的大小。

按照geojson文件格式,我们对于多个城市需要组成一个类似如下的json:

1
2
3
4
5
6
{
"type": "FeatureCollection", features: [
{"type": "Feature", "geometry": {"type": "Polygon", "coordinates": [[[88.40590939643968,22.55522906690669],[88.36498482718275,22.494854169816982],[88.28898205570562,22.51497913551355],[88.2714429545955,22.55235407180718],[88.32990662496253,22.55235407180718],[88.36498482718275,22.60410398359836],[88.35913846014606,22.62997893949395],[88.38837029532957,22.62710394439444],[88.40590939643968,22.55522906690669]]]}},
...
]
}

对于这样的一个json对象,我们就可以直接使用Leaflet的geojson文件进行加载,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function onEachFeature(feature, layer) { // does this feature have a property named popupContent?
 if (feature.properties && feature.properties.popupContent) {
 layer.bindPopup(feature.properties.popupContent); //从json文件中读取属性进行popup展示
 }
}

var geojson = L.geoJSON(areas, {
 onEachFeature: onEachFeature,
  style: function (geoJsonFeature) {
    return {
      color: '#ffcc80', //设置遮罩的颜色
      fillOpacity: 0.4, //设置透明度
      stroke: false, //是否要显示边缘线
    };
  }
}).addTo(map);

对于geojson我们也可以在properties中设置弹框的内容进行展示。

总结

到这里我们就完成了基于leaflet的一个足迹地图,既包括足迹点,也包括去过的城市的遮罩。而geojson和Leaflet的功能远远不止这些,感兴趣的可以去看相关文档。另外因为我使用的地图是openstreetmap的数据,关于中国领土有争议的部分标记不正确,这个不在我的解决能力范围之内,只能暂且使用,但是不代表本人观点。

参考资料:

  1. Tutorials - Leaflet - a JavaScript library for interactive maps
  2. https://tomickigrzegorz.github.io/leaflet-examples/
  3. GeoJSON - 维基百科,自由的百科全书
  4. DataV.GeoAtlas地理小工具系列

看完评论一下吧

拒绝成为受害者!普通人必备的账号安全防护法则

2025年2月5日 00:54

最近,小红书经常给我推送关于苹果账号被盗刷 648 元用于游戏充值的笔记。我才发现,仍然有许多用户并不了解如何保护自己的账号安全。因此,这篇文章将分享一些普通用户可以采取的措施,以提高账号安全性,并尽可能避免损失。

避免使用简单密码

不要使用像 123456000000qwer123password 这类众所周知的简单密码,同时也不要使用基于个人信息的密码,如生日、姓名拼音等,以防被轻易猜出。建议使用单词加数字符号的组合,比纯乱码的密码输入起来更方便一些。

避免在多个网站使用相同的账号和密码

这是一个常被忽视的安全隐患。为了方便记忆,许多人习惯在不同网站使用相同的用户名(通常是邮箱)和密码,甚至终生只用一两个固定密码。然而,一旦某个网站发生数据泄露,所有使用相同密码的账户都会面临被盗的风险。事实上,数据泄露事件频繁发生,即便是阿里巴巴、Facebook 这样的互联网巨头都无法幸免,更不用说那些安全防护薄弱的小公司了。

考虑使用密码管理软件

在不同网站设置独立的账号和密码,必然需要妥善记录和管理,否则普通人根本无法记住如此多的信息。因此,推荐使用可靠的密码管理工具,如 1Password、Bitwarden 以及微软的 Authenticator。苹果用户也可以使用 iOS 18 上的 Passwords。

密码管理软件不仅能安全存储账号、密码、TOTP(双重认证)和网站信息,还能帮助生成随机密码,极大提升便利性。用户只需记住密码管理软件的主账号密码,即可高效管理所有登录信息。我自己就在使用自部署的 Bitwarden。

Bitwarden 界面

开启双重验证(多重验证)

开启双重认证可以为你的账户提供额外的一层保护。在新设备上登录时,除了输入正确的账号和密码,还需额外输入手机验证码、邮箱验证码或 TOTP(基于时间的一次性验证码)等。因此,即使账号密码泄露,攻击者也无法立即登录你的账户。而当你收到异常的手机验证码或邮件验证码时,这也能成为一种警示,提醒你账号可能已遭泄露。

以 Apple ID 为例,简单介绍开启双重认证的方法:进入设置 > 点击头像 > 登录与安全性,然后开启 双重认证 。启用后,若有人尝试在新设备上登录你的 Apple ID,你的所有受信任设备都会收到登录请求,并显示登录设备的位置信息。你需要点击“允许”,然后输入弹出的 6 位验证码,才能完成登录。

Apple ID 双重认证界面

设置 SIM PIN 码

在国内,大多数账号都可以通过手机验证码登录。如果手机丢失,不法分子可能会取出你的 SIM 卡并插入自己的设备,从而盗取你的账号信息。为了防止这种情况发生,你可以为 SIM 卡设置 PIN 码锁定。这样一来,每次手机开机时,都需要输入正确的 PIN 码才能解锁并激活 SIM 卡的使用。

以 iPhone 为例,设置 PIN 码的方法如下:进入 设置 > 蜂窝网络 > 选择相应的电话卡 > SIM 卡 PIN 码,然后进行设置。多数 SIM 卡的默认 PIN 码为 1234,具体密码请参考新卡上的说明。

SIM 卡 PIN 码设置界面

其他注意事项

  • 谨慎使用公共 Wi-Fi:公共 Wi-Fi 可能存在安全隐患,例如中间人攻击、数据窃听、钓鱼热点等。在连接公共 Wi-Fi 时,尽量避免进行涉及敏感信息的操作,以降低风险。
  • 警惕钓鱼网站:如果收到类似“账号泄露”的短信或邮件,务必提高警惕。对于无法辨别的邮箱、电话号码、网址等,可先联系官方客服核实,避免上当受骗。
  • 定期更新系统和软件:及时更新手机操作系统和应用程序,修复安全漏洞,减少被攻击的风险。
  • 检查账号安全:可以通过一些网站检测自己的账号密码是否已泄露,以便及时采取应对措施。苹果用户也可直接在自带的 Passwords 软件中查看泄漏的密码。
Have I Been Pwned (HIBP):https://haveibeenpwned.com
Have I Been Pwned - 密码检测:https://haveibeenpwned.com/Passwords
苹果 Passwords 软件内的信息
查询到泄漏了信息的网站

结语

以上是根据个人经验总结的一些安全建议,虽然操作上可能略显繁琐,但能够有效提升账号安全性,降低被盗号的风险,避免不必要的损失。希望大家都能远离盗号的烦恼!如果你有更好的安全建议,欢迎在评论区分享,说不定你的方法可以帮助到更多人。

拒绝成为受害者!普通人必备的账号安全防护法则最先出现在Jack's Space

部署和运行 DeepSeek R1-8b 大型语言模型 (LLM) 的两个简单命令

2025年2月3日 04:45

视频:油管/Youtube | B站/小破站 | 微博视频 | 西瓜视频 | 微信视频号 | X/推特 | 小红书

大型语言模型(LLMs)如 DeepSeek R1-8B 正在革新自然语言处理,使强大的 AI 驱动应用成为可能。然而,设置这些模型往往是一项艰巨的任务,需要复杂的配置。幸运的是,仅需两条简单的命令,您就可以使用 Ollama 在本地系统上轻松部署和运行 DeepSeek R1-8B——Ollama 是一个用于管理和运行开源 LLMs 的简化工具。

步骤 1:安装 Ollama

Ollama 让在本地运行大型语言模型变得更加简单。要安装它,请运行以下命令:

sudo apt install curl -y
curl -fsSL https://ollama.com/install.sh | sh

此命令执行以下操作:

  • 安装 curl,一个用于从互联网下载文件的命令行工具(如果本地没有安装的话)。
  • 下载并执行 Ollama 安装脚本,设置运行 LLMs 所需的一切。

步骤 2:运行 DeepSeek R1-8B

安装 Ollama 后,您可以立即运行 DeepSeek R1-8B 模型:

ollama run deepseek-r1:8b

此命令:

  • 从 Ollama 的存储库拉取 DeepSeek R1-8B 模型(如果尚未下载)。
  • 启动模型,使您可以通过命令行与其交互。

您还可以把deepseek-r1:8b换成llama3.2等,具体看模型列表

为什么使用 Ollama?

  • 易于使用:无需复杂的 Docker 设置或环境配置。
  • 本地推理优化:Ollama 旨在高效地在消费级硬件上运行。
  • 快速设置:整个过程仅需几分钟,让您专注于使用模型,而非配置它。

Ollama 是一个功能强大且用户友好的工具,专为在本地运行和管理开源大型语言模型(LLMs)而设计。它通过消除复杂的配置需求,简化了部署过程,使 AI 模型更易于开发者、研究人员和爱好者使用。借助 Ollama,用户只需使用简单的命令即可轻松下载、运行并与各种 LLMs 交互,确保流畅高效的使用体验。Ollama 针对本地推理进行了优化,能够在消费级硬件上高效运行,使用户无需依赖云端解决方案即可利用先进 AI 模型的强大能力。

结论

部署和运行像 DeepSeek R1-8B 这样的高级 LLM 并不复杂。只需两条简单的命令,您就可以在本地计算机上运行一个强大的 AI 模型。无论您是开发者、研究人员还是 AI 爱好者,这种简化的设置都让 LLMs 的实验变得更加轻松。

立即尝试,体验 DeepSeek R1-8B 的强大功能!

大语言模型 LLM = Large Language Model

英文:Two Simple Commands to Deploy and Run the DeepSeek R1-8b Large Language Model (LLM)

本文一共 714 个汉字, 你数一下对不对.
部署和运行 DeepSeek R1-8b 大型语言模型 (LLM) 的两个简单命令. (AMP 移动加速版本)

扫描二维码,分享本文到微信朋友圈
75a5a60b9cac61e5c8c71a96e17f2d9c 部署和运行 DeepSeek R1-8b 大型语言模型 (LLM) 的两个简单命令 DeepSeek 人工智能 (AI) 技术 教育 程序员 计算机 计算机
The post 部署和运行 DeepSeek R1-8b 大型语言模型 (LLM) 的两个简单命令 first appeared on 小赖子的英国生活和资讯.

相关文章:

  1. 按揭贷款(房贷,车贷) 每月还贷计算器 去年给银行借了17万英镑 买了20万7500英镑的房子, 25年还清. 前2年是定率 Fix Rate 的合同 (年利率2.49%). 每个月大概是还 700多英镑. 有很多种还贷的计算方式, 定率/每月固定 是比较常用的. 简单来说就是 每个月交的钱是...
  2. 智能手机 HTC One M9 使用测评 虽然我对手机要求不高, 远远没有像追求VPS服务器一样, 但是怎么算来两年内换了四个手机, 先是三星 S4 用了一年多, 然后 Nokia Lumia 635 Windows Phone, 后来又是 BLU, 半年多前换了...
  3. 力扣刷题获得一件衣服奖励(Leetcode DCC Winner) 我每天都在力扣上刷题。力扣有国服和美服,我两边都会打卡。每天打卡可以获得积分,而积分可以兑换各种礼物,比如T恤、卫衣、水壶,甚至可以用来抵扣一个月的会员费用。 我从2018年8月开始刷题找工作(当时去伦敦面试亚马逊),从那时起每年都会续费会员,费用是159美元,相当于每月13.25美元。我觉得这是对自己最值得的投资。买了力扣会员,就会有动力刷题、思考,通过不断练习让自己熟能生巧,保持一定的竞争力。 到目前为止,我已经用积分兑换了7-8件力扣的衣服,其中有2-3件是卫衣。国内的礼物我都寄到姐姐家。 前不久,我收到了力扣的邮件,说我获得了DCC奖励。我也不知道为什么会获得这个奖,随手回了邮件。没多久,就收到了一件新版的力扣衬衫。 英文:Leetcode DCC Winner T-shirt 本文一共 291 个汉字, 你数一下对不对. 力扣刷题获得一件衣服奖励(Leetcode DCC Winner)....
  4. 同一台服务器上多个WORDPRESS站点的一些设置可以移出去 我自从把所有网站都挪到一处VPS服务器上 就发现很多事情省事很多 可以同时管理多个网站 包括 WORDPRESS博客. 比如我有四个WORDPRESS博客 然后我就把通用的一些资料给移出去 移到 HTTP或者HTTPS都不能直接访问的文件夹里这样就更安全许多. 文件 wp-conn.php 存储了 相同的数据库资料. 1 2...
  5. 老婆的配偶签证被拒 郁闷死了, 601镑签证费打水漂,一去不回!费钱费力. 去年12月份我请了律师拿到了永居.老婆是T1G签证的陪工签 (DEPENDENT VISA) 2016年4月份到期. 然后我就想说得趁早把她的签证转成配偶签(SPOUSE)这样她就可以尽快走五年永居的路线. 今天收到拒签信,原因是我没有提供 有工资进帐的那份银行帐单,我提供了我和我老婆的联名帐户, 但是工资并不是直接打到这个帐单上的.所以就这一点被拒了.完全不给解释,不给补材料的机会.601镑就这样再见了. 英国的签证寄出之后是先由另一个部门先收费, 收完费才正式审理,而且不管结果如何是不退钱的.后悔没让律师弄,也不至于到现在浪费这么多时间和金钱,签证还没过.由于原签证还没到期,所以还不能上述.估计只能等搬完家后年底请律师搞定这事. 真是郁闷, 600镑, 我可以再买一个IPHONE6,或者给我的新买的车换四个轮胎....
  6. 公司给配了台高配DELL笔记本 早上例会结束的时候我顺便说了一句 我的笔记本有点慢, 当时我并不知道我的经理远程用电话也参加会议了(他全程在听), senior staff SE 对着电话说, “peter, you hear that? btw, my disks are...
  7. 花钱让人换汽车钥匙的电池真是个智商税 今天想不到我这么聪明的人也被人狠狠的收了一把智商税. 今天被收智商税了, 去 Tesco 换车钥匙的电池. . 才发现如此的简单, 那人直接2分钟搞定2个, 然后收了我25英镑. . 服了. . 我还以为很复杂…… 网友说 “1....
  8. 优化设计 个人主页 并且 PageSpeed Insights 双项 100分 坛子的个人主页 www.tanzhijun.com 不错 很适合个人主页的模板. 而且是手机友好. 于是我照着把 我的主页改了改. https://steakovercooked.com 并且做了几点修改: 0. 使用 google mod_pagespeed 把 JS,...

如何快速清空七牛的存储空间中的所有文件,并删除存储空间

2025年1月25日 14:45

如果你和我一样,有很多历史的文件存储在七牛上,但如今已经不再需要使用,那么就可以考虑删除七牛的存储空间,来节省费用。

但七牛为了保证安全,所以要求必须删除所有的文件后才能删除空间,以避免误删除,所以需要一个个删除所有的文件。为了快速删除七牛存储空间的文件,我写了个简单的脚本,帮助你快速删除七牛空间下的所有文件。

具体操作可参考如下脚本,你只需要

  1. 在本地安装七牛 SDK :pip install qiniu
  2. 创建一个新文件 run.py 并复制下方的代码,修改其中的访问密钥和存储空间名称
  3. 执行 python run.py 就可以了。
# -*- coding: utf-8 -*-
# 导入七牛云 SDK 所需的模块
from qiniu import Auth
from qiniu import BucketManager, build_batch_delete
# 七牛云账号的访问密钥
access_key = '你的 ACCESS Key'
secret_key = '你的 Secret Key'
# 要清理的存储空间名称
bucket_name = '你要清空的空间名称'


# 使用 AK、SK 初始化授权对象
q = Auth(access_key, secret_key)
# 初始化存储空间管理器
bucket = BucketManager(q)

# 设置每次列举的最大条目数
limit = 1000

# 循环列举并删除存储空间中的文件
while True:
    # 列举存储空间中的文件
    # ret: 包含文件信息的字典
    # eof: 是否已列举完所有文件
    # info: 请求的状态信息
    ret, eof, info = bucket.list(bucket=bucket_name)
    # 从返回结果中提取文件名列表
    keys = [item['key'] for item in ret['items']]
    # 构建批量删除操作
    ops = build_batch_delete(bucket_name, keys)
    # 执行批量删除操作
    ret, info = bucket.batch(ops)
    # 检查删除操作是否成功
    if info.status_code == 200:
        print(f"success delete {len(keys)} files!")
    
    # 判断是否已经列举完所有文件
    if eof:
        break
    else:
        continue
# 输出清理完成的提示信息
print(f"delete all files in {bucket_name}")

执行成功后,你会看到如下面这样的命令,接下来等他自动执行即可,你就不用做任何事情了。

image

当工具提示你 delete all files in 你的 kodo 名时,你就可以回到七牛控制台,删除掉空的 kodo 了。

image

DOGE: 政府效率部 – 狗狗币起飞了! 再一次FEMO! (狗币简介)

2025年1月22日 03:33

Dogecoin/狗狗币简介

狗狗币(Dogecoin)是一种基于区块链技术的去中心化数字货币,由Billy Markus和Jackson Palmer于2013年创建。它最初是为了调侃比特币和其他加密货币而诞生的,灵感来源于风靡一时的“Doge”表情包(柴犬头像搭配搞笑文字)。尽管起初带有玩笑性质,但狗狗币凭借其低交易成本、快速确认时间和活跃的社区支持,逐渐成为一种实用的数字货币。

狗狗币的特点在于其无限供应量和轻松的文化氛围,适合用于小额支付、打赏和慈善活动。近年来,特别是在Elon Musk等知名人士的支持下,狗狗币的知名度和市值迅速提升,从一个“互联网笑话”蜕变成全球关注的数字资产。

elon-musk-trump-doge-department-of-government-efficiency DOGE: 政府效率部 - 狗狗币起飞了! 再一次FEMO! (狗币简介) FEMO - Fear of Missing Out - 害怕错过 Meme Coin/土狗 加密货币 区块链 狗狗币/Dogecoin

DOGE美国政府效能部网站上线,是狗狗币,这一定不是巧合。

DOGE:效率新时代,狗狗币领航改革

2025年,历史性的一刻到来了!

唐纳德·特朗普第二次就职美国总统,成为第47任美国总统。这一次,他的执政团队中加入了一位备受瞩目的人物——他的好哥们,Elon Musk。这位以创新精神闻名于世的企业家,不再仅仅是特斯拉和SpaceX的掌舵人,而是正式加入了美国政府,出任全新部门 “Department of Government Efficiency”(DOGE,政府效率部) 的负责人。这英文的缩写就是DOGE,这绝对不是巧合。

今天,官方网站(doge.gov)闪亮登场,其标志性的页面赫然是经典的Doge头像,背景是象征财富的金色圆章,伴随口号:“The people voted for major reform.” (人民投票支持重大改革)。

doge-gov-html-code-doge-logo DOGE: 政府效率部 - 狗狗币起飞了! 再一次FEMO! (狗币简介) FEMO - Fear of Missing Out - 害怕错过 Meme Coin/土狗 加密货币 区块链 狗狗币/Dogecoin

DOGE.gov网站HTML源代码中也有个DOGE狗狗

老马说,现在400多个政府部门可以被砍到不到99个。

狗狗币的历史性飞跃

这一重大事件瞬间点燃了加密货币市场,狗狗币的价格飙升6%。许多投资者纷纷涌入市场,视其为未来数字货币应用的领导者。这也让更多的币圈圈外认识接触到虚拟货币

dogecoin-2025-01-21 DOGE: 政府效率部 - 狗狗币起飞了! 再一次FEMO! (狗币简介) FEMO - Fear of Missing Out - 害怕错过 Meme Coin/土狗 加密货币 区块链 狗狗币/Dogecoin

狗狗币应声大涨6%到0.38

SEC:SEC Crypto 2.0:代理主席 Uyeda 宣布成立新的加密工作组。

马斯克过分了,马斯克真是带货王,马斯克割韭菜老手了,看到帖子就立马多了进去。

太疯狂了,感觉新一波牛市马上就来了!再不上车就晚了,要FEMO了。

虚拟货币/Crypto/土狗币/Meme Coin

英文:Crypto News: Dogecoin’s Rise to Prominence in 2025 (FEMO?)

本文一共 718 个汉字, 你数一下对不对.
DOGE: 政府效率部 – 狗狗币起飞了! 再一次FEMO! (狗币简介). (AMP 移动加速版本)

扫描二维码,分享本文到微信朋友圈
75a5a60b9cac61e5c8c71a96e17f2d9c DOGE: 政府效率部 - 狗狗币起飞了! 再一次FEMO! (狗币简介) FEMO - Fear of Missing Out - 害怕错过 Meme Coin/土狗 加密货币 区块链 狗狗币/Dogecoin
The post DOGE: 政府效率部 – 狗狗币起飞了! 再一次FEMO! (狗币简介) first appeared on 小赖子的英国生活和资讯.

相关文章:

  1. 按揭贷款(房贷,车贷) 每月还贷计算器 去年给银行借了17万英镑 买了20万7500英镑的房子, 25年还清. 前2年是定率 Fix Rate 的合同 (年利率2.49%). 每个月大概是还 700多英镑. 有很多种还贷的计算方式, 定率/每月固定 是比较常用的. 简单来说就是 每个月交的钱是...
  2. 智能手机 HTC One M9 使用测评 虽然我对手机要求不高, 远远没有像追求VPS服务器一样, 但是怎么算来两年内换了四个手机, 先是三星 S4 用了一年多, 然后 Nokia Lumia 635 Windows Phone, 后来又是 BLU, 半年多前换了...
  3. 整合 ChatGPT 到微信公众号机器人 ChatGPT 很火也很适合整合到各种机器人上, 比如 Telegram, Discord, Slack 等, 我把它加到了我的微信公众号 justyyuk 上了. 微信公众号 justyyuk: 小赖子的英国生活和资讯 – 扫码就能关注微信公众号,...
  4. 在英国给孩子换学校的经历: 孩子离开了村里的小学 由于搬了家, 孩子上学得提前半小时出门了, 因为早上堵, 也得开车半小时才能到. 之前在 Fen Drayton 村庄上小学, 早上8:45学校门开, 9点敲钟孩子排队依次进入教室, 我们由于在村里, 只需要提前5分钟出门和孩子一起走路就可以了. 现在一下子早上变得很匆忙, 得叫孩子起床, 做早饭,...
  5. 同一台服务器上多个WORDPRESS站点的一些设置可以移出去 我自从把所有网站都挪到一处VPS服务器上 就发现很多事情省事很多 可以同时管理多个网站 包括 WORDPRESS博客. 比如我有四个WORDPRESS博客 然后我就把通用的一些资料给移出去 移到 HTTP或者HTTPS都不能直接访问的文件夹里这样就更安全许多. 文件 wp-conn.php 存储了 相同的数据库资料. 1 2...
  6. 公司请的专业摄影师 公司来了新的CEO管理之后,很多事情都不一样了, 特别是一些公司对外形象的事情就特别的在意, 比如公司网站用上SSL.现在公司还有空闲的位置,请速来(钱多人不傻). 一月份出差回LUTON,刚好公司请来摄影师给高层管理照像放网站上的,于是我也凑了凑热闹(但是却还不够资格被放在公司网站上),不过没关系,放这里也差不多. 人到中年, 沧桑感强了些. 更新更新: 同事用他NB的单反给谢菲尔得办公室的人也拍了一组这样的照片.看起来很不错, 很专业,灯光,道具应有尽有.我已经用在了LINKEDIN页面上,立马高大上. 本文一共 230 个汉字, 你数一下对不对. 公司请的专业摄影师. (AMP...
  7. 剑桥在河边的餐厅 The Galleria (vLog, 剑河上的餐厅) B站: https://www.bilibili.com/video/BV1Wg4y157EL/ 油管: https://www.youtube.com/watch?v=bA7eb4qZxY4 西瓜: https://www.ixigua.com/7230366503680344628 上周末和媳妇孩子步行到市中心, 走走停停, 就在剑桥有名的网红店 The Galleria (河边的餐厅) 下坐下来吃了, 这家店我几年前和当时的老板来过, 坐在河边很享受....
  8. 力扣 Leetcode 的刷题利器: 在线调试器和自动代码提示完成 力扣代码调试器 Debugger 最近 leetcode 刷题网站出了一个在线调试器. 个人感觉非常好用. 因为我平时是用 IPAD+蓝牙键盘来刷题, 而在 ipad 上是没有集成的IDE的, 对于调试来说, 只能很原始的让函数退出一个值, 然后尝试不同的输入来发现问题. leetcode在线调试器的好处...

小巧的 UPS — 瓦力 W120 开箱

2025年1月11日 18:07

械硬盘在非正常断电时存在损坏的风险,因此,UPS 对于每一位 NAS 用户来说都是必不可少的设备。前几天,我那台正常运行了半年的群晖 NAS 恰好遭遇了一次“异常关机”,这让我非常紧张,随即就下单购买了一台 UPS。今天就来和大家分享一下这款小巧的 UPS — 瓦力 W120。

小巧的 UPS — 瓦力 W120 开箱最先出现在Jack's Space

Typecho博客引入Pjax无刷新页面的两种方式

2025年1月11日 15:27

使用Pjax可以在不刷新整个页面的情况下加载新的内容,它利用 AJAX(XmlHttpRequest)和 pushState() 来提供网站的交互性和响应速度,从而提升流畅的浏览体验。Pjax和我使用的instant.page类似,都是为了提升用户体验,让页面更顺滑且加快一定的响应速度,两者并不冲突,可以共存。

Typecho中,有很多主题都集成了Pjax无刷新功能,例如迷你日志、handsome主题等。但也有很多主题没有集成这项功能,所以提供两种Pjax部署方式。

Jquery.pjax

传统的Pjax部署方式,应该前后引入Jquery和Pjax库,且版本需要匹配。

<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery.pjax/2.0.1/jquery.pjax.min.js"></script>

或者你也可以使用比较稳定的jquery2.1.4和pjax1.9.6版本。主要前后顺序不能出错。

<script>
$(document).pjax('a[href^="<?php Helper::options()->siteUrl()?>"]:not(a[target="_blank"], a[no-pjax])', {container: '#main',fragment: '#main',timeout: 10000})
</script>

这段js部署在 /body 之前。如果需要加载动画,可以访问我提供的参考资料。

Pjax

Pjax是一个独立的 JavaScript 模块,不依赖于像 jQuery 这样的其他库,它完全用原生 JavaScript 编写。并且体积更小,压缩后载入时间会更短。

<script src="//cdn.bosir.cn/js/pjax.min.js"></script>

我已经部署到阿里云CDN,有需要的可以直接复制过去。随后引入

<script> var pjax = new Pjax({elements: 'a',selectors: ["#main"],timeout: 10000, push: true, replace: false,maxCacheLength: 20, }); </script>

这是根据自己主题修改的,需要更新的部分 #mainbody,如果CSS结构中,没有使用 id 就使用 .mainbody 相反有的话就使用 # 号。

验证方式

以Chrome为例,F12 打开控制台,点击 Network 后,Ctrl+R 刷新,随便点击一个页面,如果出现 xhr 开头的文件,表明成功。 当然更详细的成功部署验证,大家可以自己查阅相关资料。

相关资料:

[1] Gitcode: //gitcode.com/gh_mirrors/pj/pjax?utm_source=artical_gitcode&index=top&type=href
[2] 万维易源: //www.showapi.com/news/article/66d5a0754ddd79f11a002a13
[3] cdnjs: //cdnjs.com/

使用 Cloudflare 配置 301 转发

2025年1月11日 13:09

我的 newsletter 一部分使用 Ghost.js 进行托管,还有的在使用 Quail 来进行托管,而对应的域名则放在 Cloudflare 上进行解析。最近给 Quail 的 newsletter 绑定了域名,因为其只支持绑定一个域名,所以我就在 Cloudflare 上配置了 301 转发,来确保我的 @ 和 www 域名都访问同一部分内容。

这里我设定是 www 域名是真实绑定了 Quail 的后台,并通过 Cloudflare 完成相应的 CNAME 配置,确保网页可以正常访问,并要实现在 Cloudflare 上配置 301 转发,能够将根域名转发到 www 域名上。

image

在 Cloudflare 上配置域名解析,并开启代理模式

在 Cloudflare 上新增一个 CNAME 域名配置,配置你要转发的域名,目标可以选择你的 www 域名,或者是你的 www 域名的对应的 cname 域名。然后再打开代理

image

配置规则

在 Cloudflare 左侧侧边栏找到「规则」,进入规则页面,以配置规则。

image

在规则页面,创建一个新的规则,这里模板你可以选择「从 www 重定向到根」(如果你和我相反,是先建设好的根域名,将 www 转发过来,那直接使用这个规则就行。)

image

在弹出的规则配置页面设置你的转发规则,比如下图这样的设置就行。

image

这样当你的规则生效后,如果用户访问 AIStarter.dev/xxx,就会自动转发到 www.aistarte.dev/xxx。从而帮助你完成相关的配置能力。配置完成后,保存稍等片刻就可以生效了。这个时候你就可以使用 curl -I xxxx.xxx 来看你的域名转发情况,是否能正常返回 301 response。

image

Win10 映射 Webdav

2025年1月7日 10:59

前一段时间把办公室一台服务器搬回了家里的书房
使用的是 win 的系统,开启了 webdav,使用手机的 CX 文件管理器访问,非常好用
今天想在办公室用电脑访问,发现添加网络位置失败
这个似乎是很早以前我就解决的问题,但是时间太久了又忘记怎么处理了
查了一下是因为 win 默认是只支持 https 的链接,需要修改一下,备忘如下:
1、先去计算机管理-服务-WebClient,把服务改为自动启动,并先暂停服务
2、修改注册表 HKEY_LOCAL_MACHINE-SYSTEM-CurrentControl-Ser-Services-WebClient-Parameters-BasicAuthLevel 值改成 2,这样就同时支持 http
3、回去计算机管理,打开 WebClient 服务
然后就可以在映射网络磁盘里直接添加自己的 Webdav 了

WordPress 使用 Caddy 完成静态化缓存

2025年1月5日 11:05

使用 Caddy 处理 WordPress 当中,我提到在用 Caddy 处理 WordPress,且为了性能做了很多优化。

我的博客经历了三重优化:最基础的 PHP OpCache + Redis 数据查询缓存 + 静态化缓存。

其中一个比较有效的,便是在整个站点上加入静态化缓存,绝大多数游客看到的其实是预先生成好的静态页面,从而减少了数据库加载、渲染、计算的成本。

而想要实现静态化,则需要借助于 Cache Enabler 插件和 Caddy 配置来完成。

安装插件并启用

安装 Cache Enabler 插件,并启用插件,启用后,在后台设置中,配置过期时间和对应的清除策略,并保存。这个时候,Cache Enabler 就会自动帮你去生成不同的页面了。

image 5

配置 Caddy 路由转发

首先,你应该在你的 php_fastcgi unix//run/php/php-fpm.sock 前面加入缓存的代码并重启 Caddy,具体如下

image 4

缓存配置如下

     @cache {
		not header_regexp Cookie "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_logged_in"
		not path_regexp "(/wp-admin/|/xmlrpc.php|/wp-(app|cron|login|register|mail).php|wp-.*.php|/feed/|index.php|wp-comments-popup.php|wp-links-opml.php|wp-locations.php|sitemap(index)?.xml|[a-z0-9-]+-sitemap([0-9]+)?.xml)"
		not method POST
		not expression {query} != ''
    }

    route @cache {
        try_files /wp-content/cache/cache-enabler/{host}{uri}/https-index.html /wp-content/cache/cache-enabler/{host}{uri}/index.html {path} {path}/index.php?{query}
    }
       

这部分配置先定义了一个 @cache 块,用于后续判断,并在其中加入了多种条件判断,说明了不使用缓存的情况:

  • 如果用户有以下 Cookie,就不使用缓存:
    • comment_author(评论作者)
    • wordpress_[a-f0-9]+ (WordPress 的会话 Cookie)
    • wp-postpass(密码保护文章的 Cookie)
    • wordpress_logged_in(登录状态的 Cookie)
  • 如果当前请求命中了以下路径则不缓存
    • /wp-admin/(后台管理页面)
    • /xmlrpc.php(XML-RPC 接口)
    • 所有 wp-*.php 文件(WordPress 系统文件)
    • /feed/(RSS 订阅)
    • sitemap 相关文件
  • POST 请求不缓存(比如评论)
  • 带查询参数的缓存不请求。

随后,通过 route @cache 定义了命中缓存部分的查找顺序:

  1. 先找 HTTPS 版本的缓存:/wp-content/cache/cache-enabler/{host}{uri}/https-index.html
  2. 再找普通缓存:/wp-content/cache/cache-enabler/{host}{uri}/index.html
  3. 如果找不到缓存,就尝试原始路径:{path}
  4. 最后尝试 PHP 文件:{path}/index.php?{query}

查看效果

打开一个无痕窗口,访问你的网站,如果在 html 底部看到 <!-- Cache Enabler by KeyCDN @ Sat, 04 Jan 2025 03:05:34 GMT (https-index.html) --> ,则说明你已经成功启用静态化缓存了!

参考资料

使用 Caddy 处理 WordPress

2025年1月4日 10:44

在用了很久的 Docker 托管 WordPress 后, 近期我把 Blog 迁移到了腾讯云的香港轻量云主机上,以获得更快的访问体验。在这次迁移后,出于 Hack 方便的目的,我将 Nginx 替换成了 Caddy。你目前访问的站点便是一个基于 Caddy 托管的 WordPress 站点。

安装 Caddy

安装 Caddy 的过程不需要太过赘述,遵循 Caddy 官方安装文档当中的指南安装即可。

安装 PHP

完成了 Caddy 的安装后,则是安装 PHP,这里我使用的是 ondrej 维护的仓库

sudo add-apt-repository ppa:ondrej/php
sudo apt update

执行上述命令安装 PPA 仓库,就可以继续执行 apt install php 来安装 php & 对应的版本。此外,记得安装相关的依赖包

apt-get install php-fpm php-mysql php-curl php-gd php-mbstring php-common php-xml php-xmlrpc -y

配置 Caddy

完成安装后,就可以正常来配置 Caddy 。得益于社区的集成和 Caddy 官方的支持,Caddy 配置 WordPress 的支持非常简单,可以直接使用 Caddyfile 格式来撰写。

example.ixiqin.com { # 这个配置是给 example.ixiqin.com

    root * /data/caddy/example.ixiqin.com # 我的网站文件都放在 /data/caddy/xxx 下,/data 是我挂载的数据盘

    log { #日志配置
        output file /var/log/caddy/example.ixiqin.com.log  # 日志路径
        format console # 日志格式
    }

    request_body { # 请求体大小
        max_size 20MB # 最大 20MB
    }

    encode zstd gzip # 支持 gzip 和 zstd 压缩
    file_server # 直接提供静态文件(比如图片啥的)
    php_fastcgi unix//run/php/php-fpm.sock # 使用 php_fastcgi 调用 php-fpm 来处理动态 php 文件。
}

只需要这样的配置,你就可以完成一个最基础的 WordPress 的站点的配置。

其他配置

对静态文件提供单独的 404 返回

按照上面的配置,其实所有的请求都会转发给 php-fpm 来处理,从而造成额外的 PHP 资源浪费。因此,可以在配置当中加入如下代码,来让 Caddy 直接返回,从而避免对 PHP 性能的浪费。

@static_404 {  
  path_regexp \.(jpg|jpeg|png|webp|gif|avif|ico|svg|css|js|gz|eot|ttf|otf|woff|woff2|pdf)$  
  not file  
}  

respond @static_404 "Not Found" 404 {  
  close  
}

配置缓存头

除了静态文件的 404 处理,你还可以在 Caddy 当中配置静态文件的缓存,从而让浏览器更多的应用缓存,减少服务器的流量,提升加载速度。

@static {  
  path_regexp \.(jpg|jpeg|png|webp|gif|avif|ico|svg|css|js|gz|eot|ttf|otf|woff|woff2|pdf)$  
}  
header @static Cache-Control "max-age=31536000,public,immutable"

Miniflux + AI,无痛阅读英文RSS订阅源

2024年12月29日 20:07

前言

Miniflux + AI,无痛阅读英文RSS订阅源

最近一直想为自己的RSS订阅加上翻译的功能,降低自己阅读国外Newsletter和RSS信息的难度。现在手上用的RSS阅读客户端都不具备这个功能,只能从服务端入手了。

我使用Miniflux来管理订阅,它具备完整的API接口,可以充分根据自己需求DIY插件。当然作为一个代码弱手,我还是选择现成的工具。Miniflux-ai是国人开发、官方认可的第三方插件,可以为Miniflux增加以下AI功能:

  • 文章摘要
  • 文章翻译
  • 每日新闻简报(基于已订阅的内容)
Miniflux + AI,无痛阅读英文RSS订阅源
Lire for mac

我接入的大模型是Gemini-Pro,实际使用下来,摘要和翻译功能都很好用,也可以根据自己实际的需求修改相关的prompt优化翻译和摘要结果。总之,确实满足了我的翻译需求。

💡
如果你使用Lire作为阅读器,默认的配置可能会导致文章列表出现乱码,可以通过调整配置解决这个问题(后附)。

不过新闻简报功能就差强人意了,会出现主语模糊的情况。不过毕竟不是我的主要需求,权当看个乐子吧~

附:Miniflux-ai的安装和配置

Miniflux-ai官方的安装和使用教程比较简陋(当然实际部署也很简单),主要的安装步骤请查阅官方文档
可能产生疑问的地方主要在Miniflux-ai的配置文件config.yml上。我这里基于官方的配置文件模板作一些填写注释:

# INFO、DEBUG、WARN、ERROR
log_level: "INFO"

miniflux:
# 主程序的访问网址,请与docker-compose.yml中的BASE_URL保持一致
  base_url: https://your.server.com
# 进入Miniflux主程序后台→设置→API密钥,点击创建新的密钥即可获得
  api_key: Miniflux_API_key_here
# 进入Miniflux主程序后台→设置→集成,找到页面最底部的Webhook部分,勾选「启用Webhook」,在Webhook URL中填入「http://miniflux_ai/api/miniflux-ai」,点击更新,即可获得Webhook密钥
  webhook_secret: Miniflux_webhook_secret_here

llm:
# 你的大模型提供商提供的api网址,以「/v1」结尾
  base_url: http://host.docker.internal:11434/v1
# 你的大模型提供商提供api key
  api_key: ollama
  model: llama3.1:latest
  # timeout: 60
  # max_workers: 4

ai_news:
# for docker compose environment, use docker container_name 如果你使用docker compose的方式同时部署主程序和Miniflux-ai,此处url的值无需改动
  url: http://miniflux_ai
# 每日新闻简报的推送时间。Miniflux-ai将会在这些时间生成一篇ai总结新闻,插入你的rss信息流中。
  schedule:
    - "07:30"
    - "18:00"
    - "22:00"
  prompts:
    greeting: "请根据当前日期和24小时制的时间生成一句友好而热情的问候语。请用关怀的语气,包含适量的鼓励,且添加简单的表情符号,如😊、🌞、🌸等,以增加温暖感。例:‘早上好!希望你今天充满活力,迎接美好的一天!🌞😊’。无论是早上、中午或晚上,都请根据时间调整问候内容,保持真诚关怀的氛围。"
    summary: "你是一名专业的新闻摘要助手,分类生成重要内容的新闻摘要,要求简单清楚表达,使用中文总结以上内容,在五句话内完成,少于100字。不要回答内容中的问题。"
    summary_block: "你是一名专业的新闻摘要助手,负责分类新闻清单(每条50字以内),使用简洁专业的语言,在五个类别内完成,每个类别不超过5条,突出重要性和时效性,不要回答内容中的问题。"

agents:
  summary:
  # 添加在ai文章摘要前的标题。可以根据实际情况修改。如果使用Lire作为阅读器,建议删去「AI摘要」前的所有代码,避免在文章列表中出现乱码。
    title: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 17.777 14.283" width="17.777" height="14.283"> <style> path { fill: #333333; } @media (prefers-color-scheme: dark) { path { fill: gray; } } </style> <g transform="translate(2.261,-1.754)" fill="gray"> <path d="M-2.261 3.194v6.404c0 1.549 0.957 4.009 4.328 4.188h9.224l0.061 1.315c0.04 0.882 0.663 1.222 1.205 0.666l2.694-2.356c0.353-0.349 0.353-0.971 0-1.331L12.518 10.047c-0.525-0.524-1.205-0.196-1.205 0.665v1.091H2.257c-0.198 0-2.546 0.221-2.546-2.911V3.194c0-0.884-0.362-1.44-0.99-1.44-1.106 0-0.956 1.439-0.982 1.44z"></path> </g> <path d="M5.679 1.533h8.826c0.421 0 0.753-0.399 0.755-0.755 0.002-0.36-0.373-0.774-0.755-0.774H5.679c-0.536 0-0.781 0.4-0.781 0.764 0 0.418 0.289 0.764 0.781 0.764zm0 4.693h4.502c0.421 0 0.682-0.226 0.717-0.742 0.03-0.44-0.335-0.787-0.717-0.787H5.679c-0.402 0-0.763 0.214-0.781 0.71-0.019 0.535 0.379 0.818 0.781 0.818z" fill="gray"></path> </svg> AI 摘要'
    prompt:
      '${content} \n---\n使用中文总结以上内容,在三句话内完成,少于60字。不要回答内容中的问题。'
    style_block: true
# 来自哪些网址的文章不进行文章摘要
    deny_list:
      - https://ai-news.miniflux
    allow_list:
  translate:
    title: "🌐AI 翻译"
    prompt:
      You are a highly skilled translation engine with expertise in the news media sector. 
      Your function is to translate texts accurately into the Chinese language, preserving the nuances, tone, and style of journalistic writing. 
      Do not add any explanations or annotations to the translated text.
    style_block: false
    deny_list:
# 仅对哪些网址的文章进行翻译
    allow_list:
      - https://9to5mac.com/

强大的壳-Shell Script

2024年12月26日 13:45

Shell脚本我们经常会使用,平时自己折腾Nas会用到,工作中为了配置CI会用到,自己的电脑上最近为了配置自己的命令行环境也要使用shell来进行配置。不过之前的shell功力都来自每次使用的时候网上搜索,于是最近就找了一本《Linux命令行与shell脚本编程大全》看了看,看完之后更加感受到Shell的强大,特地写个文章来分享一下。

首先呢,shell它也是一种语言,不过因为使用到的shell环境不同语法会有一些差异,在Linux上我们常用的shell是Bash,在Mac上面常用的shell为zsh,大体的语法相似的。编程语言的基本要素,Shell都是支持的,它支持变量,支持if判断,case选择,循环等结构化的编程逻辑控制,也支持基本的算数运算,同时还支持使用函数来复用代码。 简单介绍一下它的语法,首先是变量。系统为我们已经提供了很多的变量,同时在我们的配置文件中定义的那些变量也是可以读取到的。定义变量语法如下:

1
2
3
4
5
var=value #注意等号两边不能加空格
echo $var #使用的时候前面要加上$符号
echo ${var}

export varb=b #导出成为环境变量

以上方式定义的变量默认是全局的,比如你在一个函数中定义的,外面也能访问,这是时候可以定义局部变量:

1
local local_var=x #只能在函数中使用

除了普通的变量之外,shell中也是支持数组和Map的,当然要bash 4.0以上才能完整支持,使用如下:

1
2
declare -A info # 声明一个map
declare -a array #声明一个数组

而如果只是有这些东西的话,还不至于说Shell强大。而shell中可以直接调用命令以及Linux中的一些程序这才是它的强大之处。在python等其他语言中我们也是可以调用的,但是是都需要通过语言的系统调用才能调用,而shell中则是可以直接调用那些命令,只要这些程序的可执行文件在PATH环境变量中就可以。

而配合Shell的很多特性,又进一步强大了。第一大神器是重定向,重定向支持重定向输入和重定向输出,以下为一些示例:

1
2
3
4
5
6
7
date > test.txt #重定向输出到test.txt文件中,覆盖文件
ls >> test.txt #重定向,但是追加而不是覆盖文件
wc < test.txt #输入重定向
wc << EOF #内敛输入重定向
test a
test b
EOF

因为有了输入输出重定向,我们会有很多的玩法,可以方便的命令的输入写入到我们的文件中,而linux系统中,万物皆为文件,因此理论上可以写入或者读取所有东西。比如,有一个Null设备,我们可以通过以下的命令,来不展示任何运行输出。

1
2
ls >/dev/null 2>&1
ls 1>/dev/null 2>/dev/null

1为标准输出,2为错误输出,未指定的时候默认是把标准输出重定向,这里重定向到null则不会有任何输出,而第一行我们将错误输出又通过&绑定到了标准输出。当然除了这个还有更多的用法。

除了重定向之外的另一大特性则是 管道 。在某些场景重定向已经可以解决了很多功能,但是管道实现会更优雅。管道可以将前一个命令的输出直接传给另一个命令,并且管道的串联没有数量的限制,并且前一个命令产生输出就会传递到第二个命令,不用使用缓冲区或者文件。比如:

1
ls | sort | more

甚至我们还可以将刚刚的输出继续重定向保存到文件

1
ls | sort > files.txt

在很多命令的参数之类的都提供了正则表达式的支持,正则表达式能够让我们更加方便的进行数据匹配,Linux中常用正则为POSIX正则表达式,而它又有两种,基础正则表达式(BRE)和扩展正则表达式(ERE),大部分的Linux/Unix工具都支持BRE引擎规范,仅仅通过BRE就能完成大部分的文本过滤了,但是ERE提供了更强的功能,而有些工具为了速度,也仅仅实现了BRE的部分功能。

BRE支持的语法符号包括,.匹配任意一个字符,[]字符集匹配,[^]字符集否定匹配,^匹配开始位置, $匹配结束位置,()子表达式,*任意次数量匹配(0次或多次),而ERE在BRE的基础上,还支持?最多一次匹配,+匹配至少一次。而它们的更多功能可以参看这篇文章:https://en.wikibooks.org/wiki/Regular_Expressions/POSIX_Basic_Regular_Expressions

有了正则表达式以及许多的处理工具我们就可以做很多的事情了,比如说查找文件,我们可以使用find,查找某个文件夹下面为指定后缀的文件:

1
find . -type f -name "*.java" #find支持的只是通配符,非正则

而配合管道,又可以对find之后的结果进行进一步的处理,比如配合上grep可以进一步对文件的内容进行过滤。

1
2
find . -type f -name "*.sh" |xargs grep "bash" #find 不能通过管道直接传递可以使用xargs或者通过如下方式
find . -type f -name "*.sh" -exec grep "bash" {} \;

对于文本的处理,Linux中又有sed和awk两大杀器,而关于他们的使用已经可以被写成书了。sed全名为Stream editor,也就是流编辑器,通过它可以方便的查找文件内容并替换后输出,awk则是一种模式匹配和文字处理语言,通过他们可以方便的处理文本。比如说我们可以使用sed对一份CSV文件中的手机号码进行打码处理:

1
sed -E 's/([0-9]{3})[0-9]{4}([0-9]{4})/\1**\2/g' input.csv

以上关于命令的介绍只是抛砖引玉,关于他们的使用,我们的电脑中已经给我们提供了详细的介绍,只需要在命令行中输入man commandname就可以了,除此之外,很多的命令也也提供了简单的帮助,只需要输入commandname help, command --help之类的就可以看到。

如果仅仅是语言层面的功能的话,shell相比python是没什么优势的,但是它能够和其他的命令无缝的使用,并且被Mac,Linux,Unix内置可直接使用也是它的一大优势。此外我们还可以通过shell脚本来增强我们的Linux终端,比如说可以定义自己的函数,通过.bashrc引用,可以在终端中直接调用方法名执行。

通过Shell,在Linux下面的体验得到很好的提升,工作效率也可以获得很大的提高,本文只是略微提到其皮毛,希望能够引起你对Shell的兴趣,如果想要更加深入的了解,还是需要去阅读手册或者书籍。

以下是推荐的一些资料可供参考:

  1. Bash脚本编程入门 by阮一峰
  2. Bash脚本进阶指南
  3. Grep,Sek和awk的区别
  4. 《Linux命令行与Shell脚本编程大全》(可以在微信读书中看电子书)
  5. awesome-shell (值得看看的各种资料,也可以去看看别人写的shell脚本)

看完评论一下吧

PHP8.4开启opcache扩展加速和网站instant.page预加载脚本

2024年12月25日 16:25

本周对博客进行了许多精简和优化,诸如合并订阅、友人页面,摒弃下拉菜单,修改主题..当然,也有一些新添加的东西,预加载脚本和缓存等。本地测试首页打开耗时应该成功降到了300ms以下,各页面的打开也更加顺滑。

折腾是一种自我实现的过程,不管结果怎样,都是值得的,你越折腾就越精彩。

instant.page

这是一个网站预加载的js脚本,可以提升网站速度。如图,鼠标悬停超过65毫秒,自动加载页面文本信息,但是不加载图片资源。对于用户体验会有提升,具体提升多大,只有自己测试了。部署方式,直接引用js脚本即可。PS.推荐放在footer

<script src="//instant.page/5.2.0" type="module" integrity="sha384-jnZyxPjiipYXnSU0ygqeac2q7CVYMbh84q0uHVRRxEtvFPiQYbXWUorga2aqZJ0z"></script>

国内服务器可以直接本地部署,不推荐CDN部署,会增加请求数消耗资源。

<script src="https://你的存储位置/instantpage.js" type="module"></script>

官网脚本下载

php8.4开启 Opcache

想体验php8.0以上的终极奥妙,你就开启这个,也是一种缓存机制.

[Zend Opcache]
zend_extension="opcache.so" ;
opcache.enable = 1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=100000
opcache.revalidate_freq=60
opcache.fast_shutdown=1
opcache.enable_cli=1
opcache.file_cache=/tmp
opcache.jit_buffer_size=64M
opcache.jit=1205
[Zend Opcache]

测试指南

当然,以上折腾属于做好网站备份的,如果你没有做好准备,那就赶快准备起来吧。

NeoDB WordPress 插件优化

2024年12月24日 23:35

Dayu 前些天发布了一款借助 NeoDB 实现书影音页面展示的插件,这正是我一直想实现的功能。由于该插件还有一些需要完善的地方,我便在此基础上进行了一些优化和调整。本文将简单介绍一下经我改进后的 WordPress 插件——NeoDB Integration 的使用。

NeoDB WordPress 插件优化最先出现在Jack's Space

香港金融业玄机

2024年12月24日 00:00

又到年末,回首今年写的博客日志文章,目前只有45篇,比2023年的50篇又少了5篇,精准的数字表明我似乎有一种越来越不想说话的感觉。

回顾了一下今年的关注点,基本上是研究境外银行券商、U卡、加密货币ETF跨境汇款这些事情,帮朋友们研究那些搞钱的管道和工具。

我才发现有太多的人不知道这些信息差,更感慨太多普通人的学习能力、动手能力在走出学校后日渐下降,年龄越大越明显。中年的我不想这样子,适当折腾吧。

折腾过程中也发现一些很有意思的事情:香港银行、证券行业对中国大陆内地用户的依赖越来越强。

从目前香港银行几乎全面开放对中国内地用户开户的现象就可以看出,他们太需要内地用户去成为他们的客户了,这放在几年前简直不敢想象,那时他们也许有太多香港本地用户和国外用户,根本不屑得正眼看一下内地用户。真是此一时彼一时。

而香港证券业就更有意思了。一方面,香港证券商不允许内地新用户开户,另一方面,它允许某一时间点之前的存量用户开户,比如,你之前开过香港A券商的户,拿着开户证明可以新开香港B券商的账户。

这背后的原因我猜是来自内地监管的需要,大家都去玩港美股了,谁来“投资”内地股市?但又不能彻底把门关死,所以允许已经在境外券商开户的用户开新的香港券商账户,反正这一部分用户已经出去了。

但是,玄机出现了。

在开新的香港券商账户时,它所谓的存量用户证明不过是一个图片文件……连PDF(PDF可防伪)的要求都没有。重点来了,一张图片如何证明是真伪呢?聪明的人你一定想到了,不过是一张图片吗?懂的都懂了。

淘宝网上甚至有人利用这个特点来提供“包过”的服务,真是善于发现商机,而这所谓的“包过”难道不一种智商税?可也有不少人去花钱买……

难道券商和监管方不知道要求一张图片的证明太过儿戏?不是的,你能想到的他们也一定能想到。我猜它就是要来这么一个认认真真走过场的游戏。明面上的要求还是要有的,这是一种管理艺术,既不让太多的人来,又给有需要的人留一道口子……

何况,真要搞那么严,大家都去纯境外的券商(非香港券商)去注册新账户了,那资金岂不是连香港都待不住了?反而更不利于监管。

至此,这实际操作的过程是不是一种特色?着实有点意思。而这种信息差,懂的人心知肚明,谁也不说。

HashKey

而像HashKey这种香港头号持牌(交易量最大)加密货币交易所(像胜利证券和众安银行都是套个壳使用HashKey的底层服务),要求就明显严了,由于受有关方面的要求,中国内地用户不允许在香港开户从事所有加密相关的投资活动(ETF也不行)。

HashKey严格不允许内地用户注册(连内地网络IP都不能使用它的服务),除非你拥有香港的身份,或者海外其它地方的居留许可和地址证明(即留学生和海外工作、移民的人士),看得出来它对合规严格得多。

因此好些朋友问我如何在HashKey开户,这个确实很难绕过。就像上面所说,只有拥有香港身份ID(恭喜那些拥有香港优才卡等人才计划的朋友)或海外其它地方的居留许可和地址证明(即留学生和海外工作、移民的人士)的朋友才允许,因而HashKey账户的含金量要高得多,稀缺的资源才珍贵。

满足条件的朋友可以好好利用这个资源,我就不多说了,欢迎使用我的推荐注册地址(我是HashKey的推广大使Ambassador,推荐码:8OZ6lc):

https://www.hashkey.com/en-US/register/invite?invite_code=8OZ6lc

注册激活后有多达300个的HSK代币奖励(目前价格约为2美元一个)。

总体而言,目前的香港对于中国内地朋友来说是一个很特殊的离岸金融城市,值得好好研究利用。这就是我这一年来的一点感想。

最后,因为众所周知的原因,你如果实在还有疑问(包括但不限于境外银行开户、券商账户开户等),可扫描二维码添加我的微信号( tumuhk ),请注明“美股”,我尽力解答你的相关问题。

土木坛子

值此圣诞元旦佳节之季,祝朋友们收获智慧与财富。

CSS中添加伪元素,并移除特定a类标签的伪元素的样式

2024年12月17日 15:25

今天,在修改主题的时候,发现自己post-content中的a类元素,使用:after伪元素以后,同时作用于cl-offset且name属性以cl-开头的href属性包含#cl-且onclick属性为Catalogswith()的标签。

.post-content a:after{
        content: '↷';
    font-size: .9rem;
}

这两个代码共同性就是a类元素,我们需要做的事情就是,移除掉不需要:after伪元素的属性。

2024-12-17-15-41-44.png

移除特定a标签的 :after 伪元素的样式,需要使用CSS选择器来精确选中这个a标签,并设置 :after 伪元素的content属性为空。这样,:after伪元素就不会显示任何内容。

以下是CSS代码示例,它会移除所有.post-content下具有类cl-offset和a,并且name属性以cl-开头的a标签的:after伪元素:

.post-content a.cl-offset[name^="cl-"]:after {
    content: '' !important;
}</div>

这里的 ^="cl-" 是一个属性选择器,它会匹配所有name属性值以cl-开头的元素。 content: '' 会移除:after伪元素的内容,!important是一个强制性声明,确保这个规则会覆盖其他可能存在的规则。

把这段CSS代码应该放在CSS样式表中,它将确保所有符合条件的a标签的:after伪元素都不会显示任何内容。

同理如果想要为.post-content类下的所有a标签添加:after伪元素,并设置其内容为↷和字体大小为.9rem,但同时想要移除特定a标签的:after伪元素(即那些href属性包含#cl-且onclick属性为Catalogswith()的标签),可以使用以下CSS代码:

/* 为所有.post-content下的a标签添加:after伪元素 */
.post-content a:after {
    content: '↷';
    font-size: .9rem;
}

/* 移除具有特定href和onclick属性的a标签的:after伪元素 */
.post-content a[href^="#cl-"][onclick="Catalogswith()"]:after {
    content: none;
}

这段CSS代码起了两个作用:

  1. 它为.post-content类下的所有a标签添加了一个:after伪元素,内容为↷,字体大小为.9rem。然后,针对那些href属性以#cl-开头且onclick属性为Catalogswith()的a标签,将:after伪元素的内容设置为none,从而移除了这些特定标签的:after伪元素。
  2. 除了符合特定条件的a标签外,其他所有.post-content下的a标签都会在其后显示一个↷符号。

果然,学习才能使人进步啊,现在不就知道什么a类元素,什么伪元素了吗。不然连怎么问Ai都不知道。

参考资料:

[1]CSDN://blog.csdn.net/libol/article/details/139976586
[2]腾讯云://cloud.tencent.com/developer/article/1407981
[3]简书://www.jianshu.com/p/77f0efab3747
[4]php中文网://www.php.cn/code/894.html
[5]picprose://picprose.net/zh

Linux重装与dotfile整理分享

2024年12月15日 20:05

最近把电脑上面的Linux系统给重装了,同时呢也要配置新的MacBook,就整理了一个个人的dotfile,这里分享一下linux上的我主要使用的软件,以及我的dotfile内容。

什么是Dotfile

dotfile字面意思就是以.开头的文件,在Linux当中就是隐藏文件,我们大家说的一般指的就是配置文件,比如shell的.bashrc.profile文件等。我在整理自己的dotfile的时候参考了一些网上大神的dotfile文件,这里我主要是包含我的shell的一些配置文件,vimgitrime相关的文件。

我的 Dotfile

为了保持Linux和Mac系统的统一, 我将Linux的Shell也换成了zsh,同时为了简单并没有使用oh-my-zsh,只是添加了一些自己常用的aliases

而Vim则使用neovim,它相当于是重新开发的,我想比vim应该代码上面更加高效,毕竟少了很多的历史包袱。另外它的配置文件可以使用Lua进行编写,而不是使用vim script,这样也是它的一大优点。

除了配置之外,还增加了脚本用用于将这些配置文件自动拷贝到对应的目录,使用以下代码判断是Linux系统还是Mac系统:

1
2
3
4
5
if [ "$(uname -s)" == "Darwin" ]; then
 //action for mac
else
 //action for linux
fi

另外呢,对于Mac系统,在初始化脚本中还添加了homebrew的安装,并且通过使用Brewfile在定义需要安装的一些软件,这样在执行brew bundle的时候可以把这些软件都安装上去。

对于Linux的目前还没做啥,都是通过自己手动安装的,不过一些操作也记录到了shell文件当中了。

Linux上的软件

既然写了文章,就顺便分享一下我的Linux上面还在用的软件吧。 首先是Shell,为了跟Mac保持统一,也改用了zsh,如果你也想要设置zsh为你的默认shell,可以执行如下命令并重启(前提你已经安装的zsh):

1
 sudo chsh -s $(which zsh) $USER

编辑器目前在用的有三款,主要在用neovim,同时代码文件还会使用vscode,因为有些场景neovim操作比较麻烦(对于快捷键不太熟悉),最近也在使用阮一峰老师之前推荐过的zed,据说比vscode性能更高,目前体验是对于很多语言的支持是已经内置了,不需要在安装插件,这点是好评的。

输入法在使用Fcitx5,输入方案则是使用了Rime,Rime的配置则是参考了雾凇拼音,而我主要使用小鹤双拼。

其他还在使用的软件包括:

项目开发: Android studio

截图软件:Flameshot

启动器: ULauncher, 使用简单,支持的插件数量也比较多

文档搜索: Zeal doc,mac 上面dash的window和linux平台开源版本,支持dash的文档。

文件同步: Syncthing

局域网文件传输: LocalSend

聊天软件: Weixin, telegram

文档和博客编辑: Obsidian

网页浏览器: Edge

Linux 开启zram

我的电脑已经有32G的内存了,大部分时候是够用的,但是编译Android系统的时候就不够用了。因此需要想办法,一种方式是弄一个swap空间,但是swap的速度不是很快,经过查询资料了解到现在linux已经有了一种新的虚拟内存技术,也就是zram,它主要功能是虚拟内存压缩,它是通过在RAM中压缩设备的分页,避免在磁盘分页,从而提高性能。

而想要启用它其实很简单,在我们的Ubuntu中,只需要首先关闭原先的swap空间,编辑/etc/fstab文件,将其中的swapfile条目注释掉。之后调用如下命令:

1
sudo swapoff /swapfile

如果你本来就没有设置swap,那就不需要做上面的操作,直接安装zram-config:

1
2
3
sudo apt install zram-config
systemctl enable zram-config //设置开机启动开启zram的服务
systemctl start zram-config //启动zram服务

之后可以调用如下命令验证:

1
cat /proc/swaps

我们在系统监控里面也能看到,不过还是swap。 以上方式开启的zram为物理内存的一半大小,当然也是可以修改的。 修改/usr/bin/init-zram-swapping文件中的mem大小即可。

如果对于我的dotfile感兴趣,可以查看我的repo, 地址为: https://github.com/sangmingming/dotfiles,其中我提到的初始化脚本为script/bootstrap文件。

看完评论一下吧

宝塔正式版9.3.0更新和Typecho适配php8.4版本

2024年12月10日 10:49

今天,宝塔面板更新到正式版9.3.0,这意味着,网站可以正式部署php8.4版本了。先说一下,此次宝塔面板的更新,更新的内容还是比较多的,具体更新细节,点击官网查看。

其中,我注意到或者最关心的就两个,一个是支持php8.4,另一个是优化SSL自动续签成功率。当然,如果使用wordpress的朋友们也可以使用新增的 WP Tools工具,专门为wp配备的。同时,也逐渐提升了Docker的使用功能。

为什么说,我想要升级到php8.4呢?

1733796708984.jpg

于我个人而言,或者我这个博客而言,php7.4和php8.4,没有本质区别,但玩博客,不就是硬凑吗?每次更新,我都喜欢冲在前面,尝试一些新的东西,而且我又是个喜欢折腾的人,备份好数据,就是干!

PHP 8.4 对于网站的提升主要体现在性能优化、新特性以及安全性提升三个方面。

1.性能优化:PHP 8.4 引入了 JIT(Just-In-Time)编译器,这意味着一些代码块会被编译成机器码,从而提升执行速度。
2.新特性:PHP 8.4 带来了许多新特性,例如联合类型、nullsafe操作符、属性类型、数据投影等,这些可以帮助开发者写出更加清晰、类型安全的代码。
3.安全性提升:PHP 8.4 在安全性上有所增强,例如引入了新的错误处理机制、对参数进行验证等。

实际升级到php8.4以后,不知道是不是错觉,或者心理作用,感觉就是比php7.4丝滑,这东西就是让人身心愉快。

Typecho支持且兼容php8.4

注意,升级到php8.4前,做好备份工作,因为Ty1.2.1虽然兼容,但也需要做部分适配。且有的主题需要去调试才能完美兼容,所以一定要保护好自己的数据,以免造成不必要的损失。

1733795960873.jpg

Ty升级到php8.4以后,会出现以上错误,不包含(主题和插件)的错误,仅程序本身。以上错误参考如下:

public function getHeader(string $key, ?string $default = null): ?string

我们找到错误的文件对应位置,然后只需要删除getHeader(string $key, ?string $default = null)中的?string即可,当然,如果你的$default后面如果没有null,加上就可以了,有些参数也不是必须的。根据你升级后出现的问题,自行调整即可。

上两张图中,出现的错误都是一样的,所以我们只需要删除$default前面的标记即可,对应的哪一项出错,就改哪一项,其余的别改动就行。

如果是插件和主题的错误,则需要具体去看,我这个主题因为简单,所以未出现报错。插件也只是蓝河兄的RSS插件有一处报错,和上面的情况类似,都很容易处理。

SSL即将来到45天的时代?

缩短SSL证书有效期为90天是谷歌去年3月份提出的,一年半后,苹果公司提出的方案是45天。具体可查看《SSL证书有效期将缩短为45天》一文。

2025年9月15日 至 2026年9月14日,证书有效期缩短为200天
2026年9月15日 至 2027年4月14日,证书有效期缩短为100天
2027年4月15 日起,证书有效期缩短为45天

真假,目前自不可知,不过自动化SSL申请续签部署,应该是必不可少的环节了。

以后只存在两种情况,要么付费整年的证书,(这里给龙笑天下打个广告,他的证书一年只需50元,五年更便宜只用198元,具体的点击去看),要么就是自己部署工具。所以宝塔这次优化的自动续签成功率,对于我们这种小白来说也是一种好消息。

当然你也可以使用脚本工具或者相关工具,进行自动化续签。也可以参考前文我提到的工具《在宝塔面板上,用Docker安装部署Certimate可视化SSl证书管理工具》来部署。

本文参考资料:

[1]我爱水煮鱼://blog.wpjam.com/2024/11/23/php-8-4/
[2]PHP中文网://www.php.net/releases/8.4/zh.php
[3]宝塔面板://www.bt.cn/bbs/thread-19376-1-1.html
[4]KIMI://kimi.moonshot.cn/

用宝塔面板Docker安装部署Certimate可视化SSl证书管理工具

2024年12月5日 15:36

上次讲到阿里云的https加速网关服务,使用之后发现确实很贵,所以让我给停止了。但是我又没时间折腾自动续签的事儿,所以买了张年付128的证书。但是越想越不得劲儿,花了冤枉钱,还是解决不了根本问题。这不,最近偶然发现了一个很好用且部署相当简单的工具,Certimate

68747470733a2f2f692e696d6775722e636f6d2f38776974335a412e6a706567.jpg

Certimate介绍

Certimate 是一个由国人开发的 SSL 证书管理工具,提供可视化界面让我们可以用简单直观的方式来管理 SSL 证书,申请证书、部署证书,以及证书到期续签都是自动完成的,不需要人工接入,大大简化了证书的维护工作。

私有部署:部署简单,作者提供多种方法安装,几分钟内就可以完成;
数据安全:所有数据存储在本机,不保存在第三方服务器,数据安全有保障;
维护方便:简单配置自动申请并且部署证书,到期自动续签,无需人工干预。

部署Certimate

因为我使用的是宝塔面板,所以我就根据Docker安装的方式来部署了。

首先确保宝塔面板已经安装了Docker模块,最新版好像直接上线了。如果没有安装,在软件商店安装也是可以的。

1733382135069.jpg

点击Docker后选择创建容器,接着选择容器编排,在里面点击创建,即可创建Yaml模板,然后在添加Compose模板,里面输入名称certimate,内容里添加

version: "3.0"
services:
  certimate:
    image: registry.cn-shanghai.aliyuncs.com/usual2970/certimate:latest
    container_name: certimate_server
    ports:
      - 8090:8090
    volumes:
      - ./data:/app/pb_data
    restart: unless-stopped

最后,编排模板选择certimate,编排名称填写你要的名字,完成项目创建即可。

使用Certimate

此时,你已经拥有了Certimate,默认的访问端口是8090,所以你只需要在浏览器,输入访问http://你的服务器ip:8090

用户名:admin@certimate.fun  密码:1234567890

1733382748643.jpg

进入Certimate后,就比较简单了,可视化面板,依次添加相关内容。选择相应的服务商。创建以后会有一个授权,需要添加相关的授权内容,阿里云入口RAM访问控制,创建后会获得密匙,然后再Certimate填写即可。

因为我还是用这那张收费的证书,所以暂时还没有添加,不过有备无患,留着备用。

以下是本文参考资料:

[1]Github://github.com/usual2970/certimate?tab=readme-ov-file
[2]张洪HEO://blog.zhheo.com/p/88ab.html
[3]那些免费的砖://www.thosefree.com/certimate
[4]Certimate官网://docs.certimate.me/?from=thosefree.com

Kagi 搜索引擎美化分享

2024年12月4日 23:05

Kagi 是一款以用户隐私和高质量搜索结果为核心的付费搜索引擎,具有无广告、高度可定制、隐私保护、AI 功能等特点。今天通过链接 (文中有) 领取到了三个月的免费体验,整体体验不错。为优化使用 Kagi 时的体验,我按照博客色彩风格进行了 CSS 自定义,有兴趣的朋友可以参考一下。

Kagi 搜索引擎美化分享最先出现在Jack's Space

苹果快捷指令一键同步更新 Memos 和 X

2024年12月2日 22:41

一直以来,我都尽量在 X (以前的 Twitter) 和我自己的 说说页面 同步更新动态,但每一条消息都需要手动发布两次,非常麻烦。为了能同时在两个平台更新动态,我手搓了一个快捷指令,能一次性将文字内容和所选图片内容发布到两个平台。

苹果快捷指令一键同步更新 Memos 和 X最先出现在Jack's Space

最新境外银行和券商开户的坏消息与好消息

2024年11月19日 00:00

当上帝关上一扇门的同时,也会打开一扇窗。

出于某些众所周知的原因,那篇关于境外长桥券商开户教程的微信公众号文章被要求删除了。

此外,OCBC关闭了中国大陆用户开户的通道,堵住了往新加坡券商出入金的口子,因为像新加坡长桥这类券商和香港不一样的地方是可以投资购买加密ETF资产这是一般的普通人能接触加密资产最方便最便捷的途径

与此对应,香港汇丰倒是为了抢这波潜在客户,汇丰香港支持大陆身份在内地在线开户了!汇丰APP在线申请即可,在通过后的90天内前往香港境内,通过汇丰APP在线激活即可,无需去线下支行网点了。但是激活必须人在香港,因为需要出入境记录。总体来说比之前方便多了,可以不必再跑网点排队预约。还没开通香港银行卡的朋友可以提上日程了。

与此配套,还有一个非常大的好消息可以分享给大家。长桥证券现在支持香港账户和新加坡账户之间互转资金(免手续费)。意味着长桥香港账户里的资金可以划转到新加坡长桥账户里——反之亦然,这样就解决了没有新加坡银行卡也能完成资金出入新加坡长桥账户的问题,也就解决了借助新加坡长桥账户投资加密ETF的难题。

土木坛子

具体操作非常简单,直接在长桥的APP里申请授权(见上图),等待审核通过即可完成,一个APP里申请两个账户系统,资金互相划转,利用不同的政策实现不同的投资行为。

好在目前去香港办理银行账户非常方便,有了香港银行账号后,再申请长桥香港和新加坡两地的证券账号即可。先办理拥有这些管道再说,免得将来政策变化生变。

长桥官方给土木坛子博客读者配置了一个渠道专属注册优惠,推荐码是:783XQW,专门的注册开户网页链接:

香港长桥:https://app.longbridgehk.com/ac/oa?account_channel=lb&channel=HB100006&invite-code=783XQW

新加坡长桥(可买数字加密货币ETF):https://activity.lbmkt.ing/pages/longbridge/7415/index.html?appid=longbridge&orgid=1&account_channel=lb&lang=zh-CN&channel=HB100006&invite-code=783XQW

渠道专属活动,注册后(注册时填写土木坛子渠道邀请码:783XQW)通常每个月都有丰厚福利奖励(长桥新加坡账号入资对应等值新币,奖励相应免佣等福利)。

因为众所周知的原因,你如果实在还有疑问(包括但不限于境外银行开户、券商账户开户等),可扫描二维码添加我的微信号( tumuhk ),请注明“美股”,我尽力解答你的相关问题。

土木坛子

最后插播一个小福利,能用的U卡不多了,VCard目前一切正常,博主提供福利一枚:VCard黑五圣诞狂欢盛大来袭!半价开卡尽享超值优惠!本次活动将为土木坛子的读者提供10个VCard半价开卡名额,让更多粉丝能够以超值价格拥有专属VCard,享受购物狂欢!

  • 活动时间:2024年11月11日 – 2024年12月25日
  • 参与方式:读者加微信( tumuhk ),注明需要VCard福利,先到先得,每人限一次机会。

VCard注册安装推荐链接(邀请码110316):https://webapp.51vcard.com/#InviteRegisterPage?inviteCode=110316

改头换面 —— iOS 黑色极简风桌面分享

2024年11月15日 22:45

最近,各大手机厂商都在比谁更有果味,但作为一名 iPhone 的老用户,我反而觉得这些变化有些单调。恰好,前不久 阿尼欧 分享了一款名为 Smile 的极简 App 启动器。受到启发,我以这款软件为核心,打造了一个黑色极简风格的手机桌面。本文将简单介绍我如何借助这款软件实现这一效果。

改头换面 —— iOS 黑色极简风桌面分享最先出现在Jack's Space

Elon Musk的SpaceX在2024年下半年实现火箭回收用筷子夹住

2024年11月7日 05:08

不得不说,真是牛逼哄哄。梦想照进现实,这么有创意的想法可能来源于老马对中国筷子的深刻理解。美国的老马/伊隆·马斯克/Elon Musk真是人类之光,他的梦想是带人类上火星,看来这一辈子有希望能见到了。

这真是一个Engineer Masterpiece/工程师杰作。膜拜。中国的两马都在忙着搞钱呢。我们和美国的差距确实有点大。

很多人都有疯狂的想法,但唯独伊隆·马斯克可以将其变成现实!

本文一共 149 个汉字, 你数一下对不对.
Elon Musk的SpaceX在2024年下半年实现火箭回收用筷子夹住. (AMP 移动加速版本)

扫描二维码,分享本文到微信朋友圈
75a5a60b9cac61e5c8c71a96e17f2d9c Elon Musk的SpaceX在2024年下半年实现火箭回收用筷子夹住 技术 科技 见闻 视频 资讯
The post Elon Musk的SpaceX在2024年下半年实现火箭回收用筷子夹住 first appeared on 小赖子的英国生活和资讯.

相关文章:

  1. 按揭贷款(房贷,车贷) 每月还贷计算器 去年给银行借了17万英镑 买了20万7500英镑的房子, 25年还清. 前2年是定率 Fix Rate 的合同 (年利率2.49%). 每个月大概是还 700多英镑. 有很多种还贷的计算方式, 定率/每月固定 是比较常用的. 简单来说就是 每个月交的钱是...
  2. 智能手机 HTC One M9 使用测评 虽然我对手机要求不高, 远远没有像追求VPS服务器一样, 但是怎么算来两年内换了四个手机, 先是三星 S4 用了一年多, 然后 Nokia Lumia 635 Windows Phone, 后来又是 BLU, 半年多前换了...
  3. 在英国给孩子换学校的经历: 孩子离开了村里的小学 由于搬了家, 孩子上学得提前半小时出门了, 因为早上堵, 也得开车半小时才能到. 之前在 Fen Drayton 村庄上小学, 早上8:45学校门开, 9点敲钟孩子排队依次进入教室, 我们由于在村里, 只需要提前5分钟出门和孩子一起走路就可以了. 现在一下子早上变得很匆忙, 得叫孩子起床, 做早饭,...
  4. 同一台服务器上多个WORDPRESS站点的一些设置可以移出去 我自从把所有网站都挪到一处VPS服务器上 就发现很多事情省事很多 可以同时管理多个网站 包括 WORDPRESS博客. 比如我有四个WORDPRESS博客 然后我就把通用的一些资料给移出去 移到 HTTP或者HTTPS都不能直接访问的文件夹里这样就更安全许多. 文件 wp-conn.php 存储了 相同的数据库资料. 1 2...
  5. 公司请的专业摄影师 公司来了新的CEO管理之后,很多事情都不一样了, 特别是一些公司对外形象的事情就特别的在意, 比如公司网站用上SSL.现在公司还有空闲的位置,请速来(钱多人不傻). 一月份出差回LUTON,刚好公司请来摄影师给高层管理照像放网站上的,于是我也凑了凑热闹(但是却还不够资格被放在公司网站上),不过没关系,放这里也差不多. 人到中年, 沧桑感强了些. 更新更新: 同事用他NB的单反给谢菲尔得办公室的人也拍了一组这样的照片.看起来很不错, 很专业,灯光,道具应有尽有.我已经用在了LINKEDIN页面上,立马高大上. 本文一共 230 个汉字, 你数一下对不对. 公司请的专业摄影师. (AMP...
  6. Leetcode 的在线调试器 最近 leetcode 刷题网站出了一个在线调试器. 个人感觉非常好用. 因为我平时是用 IPAD+蓝牙键盘来刷题, 而在 ipad 上是没有集成的IDE的, 对于调试来说, 只能很原始的让函数退出一个值, 然后尝试不同的输入来发现问题. leetcode在线调试器的好处 理论上来说, 你可以直接在浏览器里解决任何一道...
  7. 在英国开车的简单介绍/英国开车上路需要准备什么? 在英国合法上路需要有: 有效的驾照; MOT 车的年检; 路税 (Road Tax);还有最重要的汽车保险; 四者缺一不可. 千万不要有侥幸心理, 因为警察现在都高科技, 都能扫描车牌就能知道你合不合法. 不合法直接拦下来轻则罚款, 重则扣车上述法庭. 驾照 在英国可以用欧盟的大部分驾照,...
  8. 公司给配了台高配DELL笔记本 早上例会结束的时候我顺便说了一句 我的笔记本有点慢, 当时我并不知道我的经理远程用电话也参加会议了(他全程在听), senior staff SE 对着电话说, “peter, you hear that? btw, my disks are...

香港汇丰银行借记卡申请试用经验

2024年11月3日 00:00

HSBC HK Debit Card
之前分享过中国银行香港万事达借记卡(扣账卡),没想到很多朋友对这类信息很感兴趣。这次分享一下与之对应的汇丰银行香港万事达借记卡。

我申请中银借记卡时,顺手就在香港汇丰银行APP里申请了它的万事达借记卡。几天时间后的周末收到了通过EMS邮寄过来的实体卡,一顿操作就绑定到了我的微信和支付宝上。使用下来,我觉得它和中银的卡有所区别。以下是汇丰香港官网对此卡的简单优点介绍:

  • 轻松使用全球主要货币:一卡在手,即可用12种主要货币消费及提取现金。我们会从您的汇丰综合理财账户中相应的货币账户直接扣款。12种货币包括港元、美元、英镑、日元、人民币、欧元、泰铢、澳元、新西兰元、新加坡元、加拿大元及瑞士法郎。其他货币的消费,则根据现行汇率以港元结算。
  • 所有消费免收手续费:无论您是在商店还是网上购物,我们都不会向您收取任何本地或海外交易费用。
  • 全球各地免费提款:在全球汇丰自动柜员机提取现金,手续费全免。
  • 签账可享现金回赠:所有合资格签账免收外币交易费,更可享0.4%现金回赠。
  • 为挚爱申请附属借记卡:与至亲一同享受一卡多币及零手续费的便利。

可以看出,汇丰和中银这家银行相同的是都是万事达借记卡,都是和各自储蓄账户关联,透过万事达网络可以在境内消费和提现人民币,免收外币交易费、不占国人外汇额度。这里主要说一下它们之间的差异。

汇丰申请后很快邮寄实体卡(APP中也可以查看到卡号信息),中银在APP中第一时间申请到了虚拟卡的账号,与虚拟卡信息一致的中银实体借记卡邮寄速度没有汇丰快。收到汇丰实体卡后,直接在APP上激活,然后拨打APP上的电话设置卡密码。

汇丰的借记卡也有消费现金返现,力度是千分之四,和中银是千分之五区别不大。汇丰的港币与人民币汇率和中银几乎一致,和市场中间价几乎持平。

实际使用中,汇丰借记卡在境内使用有一个巨大“BUG”。汇丰采用了欧洲银行通行的安全验证方法,它在支付的时候偶尔会使用网页启动APP二次确认,但此网页需要科学上网手段才能访问,一般情况下内地会因连不上网页导致支付失败。

相比之下,中银借记卡没有采用上述验证手段,因此不会遇到这种因特殊国情导致的问题,也许这就是中资银行的产品和服务更符合国情一些?

但汇丰有一个“致命”好处,它的借记卡可以在全球汇丰ATM柜员机上免费提取现金,真的像一台提款机。中银的借记卡也可以提现,但每次有50元手续费。

此外,两家银行的借记卡都支持申请附属卡,但汇丰只支持附属卡主使用护照作为身份证件,中银则除了护照之外,还可以使用身份证和港澳通行证的组合来申请附属卡。

总体而言,我觉得如果你拥有上述两家银行的香港账户,趁现在开放时机两家的万事达借记卡都去申请,卡的有效期长达10年,且不要年费。拿汇丰的借记卡在境内提现,中银的借记卡国内日常小额支付,组合起来更好用。

中国银行香港扣账卡申请试用经验

2024年11月1日 00:00

中国银行香港万事达卡扣账卡

由于某些原因,我的中国银行香港银行卡里有一点港币,之前是直接通过中银快汇汇到内地的中国银行账户后换成人民币在国内使用。

最近直接在中国银行香港APP里申请了它的万事达扣账卡(支持附属卡),操作路径:APP里“选单”、“账户”、“申请扣账卡”,简单操作后,两个工作日就获批准通过,直接获得虚拟卡的账号、CVV码和有效日期,之后还可申请邮寄实体卡。

中银香港的扣账卡其实就是借记卡Debit Card,因此很好申请——相比之下信用卡难以申请通过,但它由于是和信用卡一样的16位万事达卡号,相当于是没有额度直接使用银行账户余额的准信用卡(但不是信用卡),所以可以很方便地绑定内地微信支付、支付宝、苹果支付、Google支付后在内地消费使用。

绑定后在国内消费人民币很方便,速度很快,有即时短信和邮件提醒,汇率算下来和市场上的汇率几乎一致,直接在账户余额中扣除,并且还有0.5%的消费现金奖励,虽然不多,但好过没有。

和其它境外信用卡、借记卡相比,中银香港的扣账卡除了汇率公平、无年费、消费不占外汇额度外,最大的好处是没有任何额外的汇率转换费之类的费用,像香港众安VISA卡、新加坡OCBC的VISA卡、英国运通信用卡都明确有1.95-2.95%不等的外币转换费。

唯一的美中不足,每次微信、支付宝消费额度超过200元时,需要支付3%的手续费,200以内的小额消费免手续费。中银香港扣账卡默认每天消费额度是50000等值港币,取现额度30000等值港币——但在内地取现每次有50元手续费,额度支持自行APP修改。

香港汇丰银行也支持万事达借记卡实体卡申请(无虚拟卡),在全球汇丰银行提取现金不需要手续费,我还没有申请,有兴趣的朋友也可以申请试用。

和我需要在境内消费外币相反,有些朋友需要绑定境外一些支付场景——比如OpenAI之类的每月订阅服务费,当国内信用卡不被支持时,中银香港借记卡无疑是个不错的选择。

我的建议是趁现在开放申请,先申请为快吧,免得未来说不定就关上大门不让申请了。

YAML 入门篇教程

2020年8月31日 00:00

YAML 的语法和其它的高级语言类似,并且可以简单表达清单、散列表等数据形态。使用空白符号缩进和大量依赖外观的特色,特别适合用来表达或者编辑数据结构、各种配置文件、倾印调试内容、文件大纲。

基本语法

  1. 大小写较敏感;

  2. 使用缩进表示层级关系;

  3. 缩进不允许使用 tab,仅限空格;

  4. 缩进空格数不重要,只要相同层级的元素左对齐即可;

  5. 井号表示注释。

对象

对象:键值对的集合,又称映射、哈希、字典。

对象键值对使用冒号结构表示 key: value,冒号后面要加一个空格。

也可以使用 key:{key1: value1, key2: value2}。

还可使用缩进表示层级关系;

1
2
3
key:
child-key: value
child-key2: value2

较复杂的对象格式,可使用问号加一个空格代表一个复杂的 key,配合一个冒号加一个空格代表一个 value:

1
2
3
4
5
6
?
- complexkey1
- complexkey2
:
- complexvalue1
- complexvalue2

意思即对象的属性是一个数组[complexkey1,complexkey2],对应值也是一个数组[complexvalue1,complexvalue2]。

数组

数组:一组按照次序排列的值,又称序列、列表。以横杠开头的行表构成一个数组:

1
2
3
- A
- B
- C

支持多维数组,可以使用行内表示:

1
key: [value1, value2]

数据结构子成员是一个数组,则可以在该项下面缩进一个空格:

1
2
3
4
-
- A
- B
- C

一个相对复杂例子:

1
2
3
4
5
6
7
8
9
companies:
-
id: 1
name: company1
price: 200W
-
id: 2
name: company2
price: 500W

意思是 companies 的属性是一个数组,每一个数组元素又是由 id/name/price 三个属性构成的。

数组也可以使用流式的方式表示:

1
companies: [{id: 1,name: company1,price: 200W},{id: 2,name: company2,price: 500W}]

复合结构

数组和对象可构成复合结构,例如:

1
2
3
4
5
6
7
8
9
languages:
- Ruby
- Perl
- Python
websites:
YAML: yaml.org
Ruby: ruby-lang.org
Python: python.org
Perl: use.perl.org

转换为 JSON:

1
2
3
4
5
6
7
8
9
{ 
languages: [ 'Ruby', 'Perl', 'Python'],
websites: {
YAML: 'yaml.org',
Ruby: 'ruby-lang.org',
Python: 'python.org',
Perl: 'use.perl.org'
}
}

纯量

纯量是最基本,不可再分的值,包括:

  1. 字符;
  2. 布尔;
  3. 整数;
  4. 浮点;
  5. 空值;
  6. 时间;
  7. 日期。

使用一个例子来快速了解纯量的基本使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
boolean: 
- true
- false
float:
- 3.14
- 6.8523015e+5
int:
- 123
- 0b1010_0111_0100_1010_1110
null:
nodeName: 'node'
parent: ~
string:
- 'Hello world'
- newline
newline2
date:
- 2020-03-25
datetime:
- 2020-03-25T00:00:00+08:00

引用

AND 符用来建立锚点,双小于表示合并到当前数据,星号用来引用锚点:

1
2
3
4
5
6
7
8
9
defaults: &defaults
adapter: postgres
host: localhost
development:
database: myapp_development
<<: *defaults
test:
database: myapp_test
<<: *defaults

上面的代码相当于:

1
2
3
4
5
6
7
8
9
10
11
defaults:
adapter: postgres
host: localhost
development:
database: myapp_development
adapter: postgres
host: localhost
test:
database: myapp_test
adapter: postgres
host: localhost

下面是另一个例子:

1
2
3
4
5
- &showell Steve 
- Clark
- Brian
- Oren
- *showell

转换为 JavaScript 代码如下:

1
[ 'Steve', 'Clark', 'Brian', 'Oren', 'Steve' ]

Ansible Playbook 实战案例篇

2020年8月28日 00:00

Playbook 是一个不同于使用 Ansible 命令行执行方式的模式,其功能更强大灵活。Playbook 可定制配置,可按照指定的操作步骤有序执行,支持同步、异步方式。值得注意的是 Playbook 通过 YAML 格式来进行描述定义的。

案例拓扑

实战案例拓扑如下:

环境规划

网络环境规划如下:

角色NAT 外网 IPNAT 外网 IP部署软件
m01eth0:10.0.0.61eth1:172.16.1.61Ansible
backupeth0:10.0.0.41eth1:172.16.1.41rsync
nfseth0:10.0.0.31eth1:172.16.1.31NFS、sersync
web01eth0:10.0.0.7eth1:172.16.1.7httpd

配置 Ansible 对应的主机

1
2
3
4
5
6
7
[root@m01 ~]# vim /etc/ansible/hosts
[web]
172.16.1.7
[nfs]
172.16.1.31
[backup]
172.16.1.41

注意:上面为 Ansible 主机清单列表。

检查对应主机组和规划的 IP 是否一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@m01 ~]# ansible web --list-host  
hosts (1):
172.16.1.7
[root@m01 ~]# ansible backup --list-host
hosts (1):
172.16.1.41
[root@m01 ~]# ansible nfs --list-host
hosts (1):
172.16.1.31
[root@m01 ~]# ansible all --list-host
hosts (3):
172.16.1.31
172.16.1.41
172.16.1.7

注意:上面命令用于检测主机清单列表是否生效。

建立对应目录站点

1
[root@m01 ~]# mkdir -p /etc/ansible/ansible_playbook/file

注意:上面命令用于建立存放 ansible-playbook 文件的目录。

编写基础模块的 Playbook

实现如下功能:

  1. 基础仓库准备;
  2. 安装 rsync 服务端;
  3. 安装 nfs-utils 服务端;
  4. 创建 www 用户指定 UID/GID;
  5. 准备 rsync 客户端密码文件。

建立基础环境的 YAML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
[root@m01 ansible_playbook]# cat base.yml 
- hosts: all
remote_user: root
tasks:
- name: configure yum repos
yum_repository:
name: base
description: base yum repo
baseurl:
- http://mirrors.aliyun.com/centos/$releasever/os/$basearch/
- http://mirrors.aliyuncs.com/centos/$releasever/os/$basearch/
- http://mirrors.cloud.aliyuncs.com/centos/$releasever/os/$basearch/
gpgcheck: no
- name: configure yum repos
yum_repository:
name: epel
description: epel yum repo
baseurl: http://mirrors.aliyun.com/epel/7/$basearch
gpgcheck: no
- name: Create www Group
group: name=www gid=666
- name: Create www User
user: name=www uid=666 group=666 shell=/sbin/nologin create_home=no
- name: create rsync client pass
copy: content='123456' dest=/etc/rsync.pass mode=0600
- name: Push backup scripts
copy: src=./files/clinet_push_rsync.sh dest=/server/scripts/
when: (ansible_hostname != "backup")
- name: Cron Tasks
cron: name=Rsync_Backup minute=00 hour=01 job='/bin/bash /server/scripts/clinet_push_rsync.sh &>/dev/null'
when: (ansible_hostname != "backup")

使用 ansible-playbook 检测语法并进行模拟执行:

1
2
3
[root@m01 ansible_playbook]# ansible-playbook --syntax-check base.yaml
playbook: base.yaml
[root@m01 ansible_playbook]# ansible-playbook -C base.yaml

编写应用模块 rsync 的剧本:

  1. 安装 rsync 服务端;
  2. 配置 rsync 服务端;
  3. 启动 rsync 服务端;
  4. 准备对应的数据存储仓库/backup、/data 并授权为 www;
  5. 准备虚拟用户和密码文件权限 600;
  6. 变更配置,重载服务。

准备对应配置文件存放至/etc/ansible/ansible_playbook/files/:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[root@m01 conf]# cat /etc/ansible/ansible_playbook/files/rsyncd.conf 
uid = www
gid = www
port = 873
fake super = yes
use chroot = no
max connections = 200
timeout = 600
ignore errors
read only = false
list = false
auth users = rsync_backup
secrets file = /etc/rsync.passwd
log file = /var/log/rsyncd.log
#####################################
[backup]
path = /backup
[data]
path = /data

编写 rsync 服务端安装的 YAML 语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
[root@m01 ansible_playbook]# cat rsync.yml 
- hosts: backup
remote_user: root
tasks:
- name: Install Rsync Server
yum: name=rsync state=present
- name: Config Rsync Server
copy: src=./files/{{ item.src }} dest=/etc/{{ item.dest }} mode={{ item.mode }}
with_items:
- { src: "rsyncd.conf", dest: "rsyncd.conf", mode: "0644" }
- { src: "rsync.passwd", dest: "rsync.passwd", mode: "0600" }
notify:
- Restart Rsync Server
tags: conf_rsync
- name: Create Directory
file: name={{ item }} state=directory owner=www group=www recurse=yes
with_items:
- /data
- /backup
- name: Server Rsync Server
service: name=rsyncd state=started enabled=yes
- name: Check Rsync Status
shell: netstat -lntp|grep rsync
register: Rsync_Status
- name: Out Rsync Status
debug: msg={{ Rsync_Status.stdout_lines }}
handlers:
- name: Restart Rsync Server
service: name=rsyncd state=restarted

编写应用模块 NFS 的剧本:

  1. 安装 NFS 服务端;
  2. 配置 NFS 服务端;
  3. 启动 NFS 服务端;
  4. 准备对应数据存储仓库/data 并授权为 www;
  5. 变更配置,重载服务。

准备 NFS 配置文件 exports:

1
2
[root@m01 ansible_playbook]# cat /etc/ansible/ansible_playbook/files/exports 
{{ share_dir }} {{ share_ip }}(rw,sync,all_squash,anonuid=666,anongid=666)

编写 NFS 安装与配置的 YAML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[root@m01 ansible_playbook]# cat /etc/ansible/ansible_playbook/nfs.yml
- hosts: nfs
remote_user: root
vars:
share_dir: /data
share_ip: 172.16.1.0/24
tasks:
- name: Install NFS-Server
yum: name=nfs-utils state=present
- name: Configure NFS-Server
template: src=./files/exports dest=/etc/exports
notify: Restart Nfs Server
- name: Create Directory
file: name={{ share_dir }} state=directory owner=www group=www recurse=yes
- name: Start NFS-Server
service: name=nfs state=started enabled=yes
- name: Check Nfs Server
shell: cat /var/lib/nfs/etab
register: NFS_Status
- name: Out Nfs Server
debug: msg={{ NFS_Status.stdout_lines }}
handlers:
- name: Restart Nfs Server
service: name=nfs state=restarted

编写应用模块 sersync 的剧本:

  1. 安装 sersync 服务端;
  2. 配置 sersync 服务端;
  3. 启动 sersync 服务端。

下载 sersync 软件包:

1
2
[root@m01 ansible_playbook]# ll /etc/ansible/ansible_playbook/file/
-rw-r--r-- 1 root root 727290 Aug 1 12:04 sersync.tar.gz

准备 sersync 实时同步的配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
[root@m01 ansible_playbook]# cat /etc/ansible/ansible_playbook/conf/confxml.xml.nfs 
<?xml version="1.0" encoding="ISO-8859-1"?>
<head version="2.5">
<host hostip="localhost" port="8008"></host>
<debug start="false"/>
<fileSystem xfs="true"/>
<filter start="false">
<exclude expression="(.*)\.svn"></exclude>
<exclude expression="(.*)\.gz"></exclude>
<exclude expression="^info/*"></exclude>
<exclude expression="^static/*"></exclude>
</filter>
<inotify>
<delete start="true"/>
<createFolder start="true"/>
<createFile start="true"/>
<closeWrite start="true"/>
<moveFrom start="true"/>
<moveTo start="true"/>
<attrib start="false"/>
<modify start="false"/>
</inotify>
<sersync>
<localpath watch="/data">
<remote ip="172.16.1.41" name="data"/>
</localpath>
<rsync>
<commonParams params="-az"/>
<auth start="true" users="rsync_backup" passwordfile="/etc/rsync.pass"/>
<userDefinedPort start="false" port="874"/><!-- port=874 -->
<timeout start="true" time="100"/><!-- timeout=100 -->
<ssh start="false"/>
</rsync>
<failLog path="/tmp/rsync_fail_log.sh" timeToExecute="60"/><!--default every 60mins execute once-->
<crontab start="false" schedule="600"><!--600mins-->
<crontabfilter start="false">
<exclude expression="*.php"></exclude>
<exclude expression="info/*"></exclude>
</crontabfilter>
</crontab>
<plugin start="false" name="command"/>
</sersync>
<plugin name="command">
<param prefix="/bin/sh" suffix="" ignoreError="true"/> <!--prefix /opt/tongbu/mmm.sh suffix-->
<filter start="false">
<include expression="(.*)\.php"/>
<include expression="(.*)\.sh"/>
</filter>
</plugin>
<plugin name="socket">
<localpath watch="/opt/tongbu">
<deshost ip="192.168.138.20" port="8009"/>
</localpath>
</plugin>
<plugin name="refreshCDN">
<localpath watch="/data0/htdocs/cms.xoyo.com/site/">
<cdninfo domainname="ccms.chinacache.com" port="80" username="xxxx" passwd="xxxx"/>
<sendurl base="http://pic.xoyo.com/cms"/>
<regexurl regex="false" match="cms.xoyo.com/site([/a-zA-Z0-9]*).xoyo.com/images"/>
</localpath>
</plugin>
</head>

编写 sersync 应用 YAML:

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@m01 ansible_playbook]# cat sersync.yaml 
- hosts: nfs
tasks:
- name: Installed Sersync
copy: src=./file/sersync.tar.gz dest=/server/tools/
- name: Tar xf Sersync
shell: cd /server/tools/ && tar xf sersync.tar.gz && mv GNU-Linux-x86 /usr/local/sersync
args:
creates: /usr/local/sersync
- name: Config Sersync
copy: src=./conf/confxml.xml.nfs dest=/usr/local/sersync/confxml.xml
- name: Service Start Sersync
shell: /usr/local/sersync/sersync2 -dro /usr/local/sersync/confxml.xml

编写 Web 应用模块的剧本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[root@m01 ansible_playbook]# cat web.yml 
- hosts: web
remote_user: root
vars:
remote_nfs_ip: 172.16.1.31
local_dir: /var/www/html/
http_port: 80
tasks:
- name: Installed Httpd Server
yum: name=httpd,php state=present
- name: Configure Httpd Server
template: src=./files/httpd.conf dest=/etc/httpd/conf/httpd.conf
notify: Restart Httpd Server
- name: Start Httpd Server
service: name=httpd state=started enabled=yes
- name: Mount Nfs Server
mount: src={{remote_nfs_ip}}:/data path={{ local_dir }} fstype=nfs opts=defaults state=mounted
- name: Push kaoshi.zip
unarchive: src=./files/kaoshi.zip dest={{ local_dir }}
handlers:
- name: Restart Httpd Server
service: name=httpd state=restarted

将所有编写好的 YAML 引入至一个文件中, 这样便于一次执行:

1
2
3
4
5
6
[root@m01 ansible_playbook]# cat main.yaml 
- import_playbook: base.yaml
- import_playbook: rsync.yaml
- import_playbook: nfs.yaml
- import_playbook: sersync.yaml
- import_playbook: web.yaml

测试

  1. 测试 Web 是否能同步数据至 NFS 存储;
  2. NFS 是否实时同步至 rsync 的/data;
  3. 使用客户端测试能否推送数据至 rsync 的 backup。

Apache 的三种工作模式介绍「event 篇」

2020年8月25日 00:00

Apache 目前一共有三种稳定的 MPM 模式。它们分别是 prefork/worker 和 event,它们同时也代表 Apache 的演变和发展。今天杜老师就聊下 event 模式!

event 模式的工作原理

event 是 Apache 最新的工作模式,和 worker 模式很像,不同的是在于它解决了 keep-alive 长连接时候占用线程资源被浪费的问题。

event 工作模式中,会有一些专门的线程用来管理这些 keep-alive 类型线程,当真实请求过来的时候,将请求传递给服务器的线程,执行完毕之,又允许它释放。这样,一个线程就可以几个请求了,实现了异步非阻塞。这增强了在高并发场景下的请求处理。

event 工作模式在遇到某些不兼容模块时,如果,将会回退到 worker 模式,一个工作线程处理一个请求。官方自带模块全部支持 event 工作模式的。

event 工作模式需要 Linux 系统对 epoll 支持,才能启用。需要补充的是 HTTPS 的连接,它的运行模式仍然类似 worker 的方式,线程会被一直占用,知道连接关闭。

event 模式的装方法

在编译安装 Apache 的过程中,加入参数–with-mpm=event 即进行编译安装。

也可以使用–enable-mpms-shared=all,这样在编译的时候会在 modules 目录下自动编译出三个 MPM 文件的 so,然后通过修改 httpd.conf 配置文件更改 MPM 即可。

worker 模式的配置参数说明

配置说明
StartServers 3服务启动时初始进程数,默认为 3
MinSpareThreads 75最小空闲子进程数,默认为 75
MaxSpareThreads 250最大空闲子进程数,默认为 250
ThreadsPerChild 25每个子进程产生的线程数量,默认是 25
MaxRequestWorkers 400限定同一时间内客户端最大接入的请求数,默认是 400
MaxConnectionsPerChild 0每个子进程在其生命周期内允许最大请求数量,如果请求总数已经达到这个数值,子进程将结束,如果设置为 0,子进程将永远不会结束。将值设置为非 0 值,可以防止运行 PHP 导致的内存泄露

工作模式与 Web 应用选择

Apache 能更好的为有特殊要求的站点定制。例如,要求更高伸缩性的站点可以选择使用线程的 MPM,即 worker 或 event。

需要可靠性或者与旧软件兼容的站点则可以使用 prefork。就使用 PHP 而言,FastCGI 和 PHP-FPM 是更推荐的使用模式。

Apache 的三种工作模式介绍「worker 篇」

2020年8月22日 00:00

Apache 目前一共有三种稳定的 MPM 模式。它们分别是 prefork/worker 和 event,它们同时也代表 Apache 的演变和发展。今天杜老师就聊下 worker 模式!

worker 模式的工作原理

worker 模式和 prefork 模式相比较,worker 模式使用了多进程和多线程的混合模式,worker 模式也同样会先预派生一些子进程,然后每一个子进程创建一些线程,同时包括一个监听线程,每个请求过来会被分配到一个线程来服务。线程比起进程会更轻量,因为线程是通过共享父进程内存空间,因此,内存的占用会减少一些,在高并发场景下会比 prefork 有更多可用的线程,表现会更优秀一些。另外,如果一个线程出现问题也会导致同一进程下的线程出现问题,如果是多个线程出现了问题,也只是影响 Apache 的一部分,而不是全部的。由于用到多进程多线程,需要考虑到线程的安全,在使用 keep-alive 长连接的时候,某个线程会一直被占用,即使中间没有请求,需要等待到超时才会被释放。

Apache 总是试图维持一个备用或空闲的服务线程池。这样,客户端无须等待新线程或者新进程的建立即可得到处理。在 Unix 中为了能够绑定 80 端口,父进程一般都是以 root 的身份启动,随后,Apache 会以较低权限的用户建立子进程和线程。User 和 Group 指令用于配置 Apache 子进程的权限。虽然子进程必须对其提供内容拥有读权限,但应该尽可能给予它较少的特权。另外,除非使用了 suEXEC ,否则,这些指令配置的权限将被 CGI 脚本继承。

worker 模式的安装方法

在编译安装 Apache 的过程中,加入参数–with-mpm=worker 即进行编译安装。

也可以使用–enable-mpms-shared=all,这样在编译的时候会在 modules 目录下自动编译出三个 MPM 文件的 so,然后通过修改 httpd.conf 配置文件更改 MPM 即可。

worker 模式优缺点

优点:占据更少内存,高并发下表现优秀。

缺点:必须考虑线程安全问题,因为多个子线程是共享父进程内存地址的。如使用 keep-alive 的长连接方式,也许中间几乎没有请求,这时就会发生阻塞,线程会被挂起,需要一直等待到超时才会被释放。如果线程过多,被这样的占据,也会导致高并发场景下的无服务线程可用。

worker 模式配置参数说明

配置说明
StartServers 3服务启动时初始进程数,默认是 3
MinSpareThreads 75最小空闲子进程数,默认是 75
MaxSpareThreads 250最大空闲子进程数,默认是 250
ThreadsPerChild 25每个子进程产生的线程数量,默认是 25
MaxRequestWorkers 400限定同一时间内客户端最大接入请求数量,默认是 400
MaxConnectionsPerChild 0每个子进程在其生命周期内允许最大请求数量,如果请求总数已经达到这个数值,子进程将结束,如果设置为 0,子进程将永远不会结束。如该值设置为非 0 值,可以防止运行 PHP 导致的内存泄露

Apache 的三种工作模式介绍「prefork 篇」

2020年8月16日 00:00

Apache 目前一共有三种稳定的 MPM 模式。它们分别是 prefork/worker 和 event,它们同时也代表 Apache 的演变和发展。今天杜老师就聊下 prefork 模式!

prefork 模式的工作原理

prefork 模式是很古老但是非常稳定的模式。用的是多个子进程,Apache 在启动之初,控制进程会建立若干子进程,然后等待请求进来,并且总是视图保持一些备用的子进程。为不在请求到来时再生成子进程,所以需要根据需求不断的创建新的子进程,最大可以达到每秒 32 个直到满足需求为止。之所以这样做,是为减少频繁创建和销毁进程的开销。每个子进程中只有一个线程,在一个时间点,只能处理一个请求。

在 Unix 系统,父进程通常以 root 身份运行以便绑定 80 端口,而 Apache 产生的子进程通常以一个低特权用户运行。User 和 Group 指令用于配置子进程的低特权用户。运行子进程的用户必须要对它所服务的内容有读取权限,但对服务内容之外的其它资源必须拥有尽可能少的权限。

prefork 模式的安装方法

在编译安装 Apache 的过程中,加参数–with-mpm=prefork 即可,如不加也可以,因为 Apache 默认采用 prefork 模式进行编译安装。

也可以使用–enable-mpms-shared=all,这样在编译的时候会在 modules 目录下自动编译出三个 MPM 文件的 so,然后通过修改 httpd.conf 配置文件更改 MPM 即可。

prefork 模式优缺点

优点:成熟,兼容所有新老模块。进程之间完全独立,使它非常稳定。同时,不需要担心线程安全的问题。

缺点:一个进程相对占用更多系统资源,消耗更多内存。而且,它并不擅长处理高并发请求,在这种场景下,它会将请求放进队列中,一直等有可用进程,请求才被处理。

prefork 模式的配置参数说明

配置参数说明如下:

配置说明
StartServers 5服务启动时初始进程数,默认是 5
MinSpareServers 5最小空闲子进程数,默认是 5
MaxSpareServers 10最大空闲子进程数,默认是 10
MaxRequestWorkers 250限定同一时间内客户端最大接入请求数量,默认是 250
MaxConnectionsPerChild 0每个子进程在其生命周期内允许最大请求数量,如果请求总数已经达到这个数值,子进程将结束,如果设置为 0,子进程将永远不会结束。该值设置为非 0 值,可以防止运行 PHP 导致的内存泄露

MySQL 主键和唯一索引区别

2020年8月13日 00:00

主键的完整称呼是主键约束,是 MySQL 中使用最为频繁的约束。一般情况为了便于 DBMS 更快的查找到表中的记录,都会在表中设一个主键。MYSQL 索引用来快速地寻找那些具有特定值的记录,如果没有索引,执行查询时 MySQL 必须从第一个记录开始扫描整个表的所有记录,直至找到符合要求记录。

两者区别

主键是种约束,唯一索引是种索引,两者在本质上是不同的。

主键创建后一定包含一个唯一性索引,唯一性索引并不一定是主键。

唯一性索引列允许空值,而主键列不允许为空值。

主键列创建时,已经默认为非空值加唯一索引了。

主键可被其他表引用为外键,唯一索引不能。

一个表最多能创建一个主键,但可创建多个唯一索引。

主键和唯一索引都可有多列。

主键适合那些不容易更改的唯一标识,如自动递增列、身份证等。

在 RBO 模式,主键的执行计划优先级高于唯一索引。两者都可以提高查询的速度。

索引是一种特殊的文件,它们包含对数据表里所有记录的引用指针。

总体来说

主键相当于一本书籍的页码,索引相当于书籍的目录。

其实主键和索引都是键,不过主键是逻辑键,索引是物理键,意思是主键不实际存在,而索引实际存在在数据库中,主键一般都要创建,主要用来避免一张表中有相同的记录,索引一般可以不建,但如果需要对该表进行查询操作,则最好建,这样可以加快检索速度。

何为内网穿透

2020年8月10日 00:00

前两天杜老师提到在家里搭建了一台 NAS 服务器,并做了内网穿透可实现随时访问。有小伙伴问我何为内网穿透,今天就跟大家简单说说!

技术介绍

内网穿透简单来说就是将内网外网通过外网服务器隧道打通,内网的数据让外网可以获取。如常用的办公室软件等,一般在办公室或着家里,通过宽带上网,这样办公软件只在本地的局域网之内才能访问,那么问题来了,如果是手机上,或公司外地的办公人员,如何访问到办公软件呢?这时就需要内网穿透工具了。

运行隧道之后,绑定一个专属域名、端口,办公软件就已经在公网上了,在外地的办公人员可在任何地方愉快的访问办公软件了!

能做什么

  1. 外网可访问上文举例的办公软件;

  2. 放在家里的树莓派,服务器等,需远程 SSH 管理,这样打通服务器的 22 端口即可远程通过 SSH 操作服务器了;

  3. 微信、支付宝等本地开发。现在微信、支付宝等应用,需服务器接收微信、支付宝发送的回调信息,而在本地开发程序的话,还得实时上传到服务器,以便支持微信、支付宝的回调信息,如果用了内网穿透软件,将回调地址设置成绑定域名,回调数据可立即传递回本地,这样很方便的在本地就可以实时调试程序,无须再不断上传服务器等繁琐且无意义的步骤;

  4. 一些企业的内部数据库,由于安全性等原因,不愿意放到云服务器上,可将数据库放到办公室本地,然后通过内网穿透隧道映射,这样既可保证安全,又保证公网可正常访问;

  5. 一些开发板做的监控等信息,每台设备运行一条隧道,可以方便的管理监控各个设备的运行情况;

  6. NAS 上运行内网穿透之后,随时随地在任何地方可以访问到 NAS 上应用。

CDN 原理及使用

2020年4月9日 00:00

关于 CDN 的具体概念,可以参考百度百科,百度搜 CDN 即可,我们这里会用 “白话” 这种形式,讲述一下 CDN 的原理,以及使用步骤。

技术原理

先简单说一下原理。比如我们这里有一台服务器,随着用户体验需求越来越高,我们需要提升访客访问速度,最有效的方式,就是在访客家门口放一台服务器。但是每个访客都放一台,那成本太高了,所以我们可以按照访客地区分布,比如北京区的访客统一在北京放一台。服务器是有了,但访客想要获取的并不是服务器,而是服务器的数据。那如何同步所有服务器数据?

这里就可以使用 CDN,用户先去访问 CDN 服务器,CDN 服务器会查看下自身缓存,如果缓存中存在访客想要的数据,就会直接发给用户,这种直连的返回会提升访问速度;如果 CDN 中没有此项缓存,CDN 会去找原服务器请求数据,然后转发数据给用户并同时缓存一份到自身缓存中,下次如果再有任何用户请求相同数据,CDN 可直接从缓存中读取。

实现方式

使用分为两种,第一种为自己搭建 CDN 服务器。不过并不建议个人以及小型公司使用这种形式,因为我们需要购买的节点服务器成本很大,倒不如第二种实惠!

第二种为网上购买服务。通常使用这一种比较多,原因是 CDN 需要分布每个地区,如果在这些地区都购买服务器成本太大,而且搭建每个节点又太麻烦,购买服务可以节省时间、成本。

购买服务的步骤简单为:寻找 CDN 服务商,购买内容分发服务,将自己原服务器的 IP 提交给 CDN 服务商,等服务商解析后会给你一个域名,通过 CNAME 的形式替换掉原域名解析。

实现形式:客户端在访问域名之后,域名服务器会解析到 CDN 服务商的域名,服务商的域名解析服务会定位你的 IP,然后将距离最近的 CDN 服务器地址发送给你,你就可以直接与 CDN 服务器通信提高访问的速度。

基于端口、域名及 IP 的 Apache 虚拟主机

2020年3月22日 00:00

Apache 是世界使用排名第一的 Web 服务软件。它可运行在几乎所有广泛使用的计算机平台上,由于其跨平台和安全性被广泛的使用,是最流行的 Web 服务器端软件之一。

服务介绍

快速、可靠并可通过简单的 API 扩充,将 Perl/Python 等解释器编译到服务中。

Apache 安装后,默认为基于 IP,而且是单 IP 的形式,既直接可通过本机的 IP 访问,默认的 DocumentRoot 为/var/www/html,可通过配置文件/etc/httpd/conf/httpd.conf 修改。

基于 IP 的

1
2
3
4
5
6
<VirtualHost 192.168.18.251:80>
DocumentRoot /var/www/html/web1
</VirtualHost>
<VirtualHost 192.168.18.252:80>
DocumentRoot /var/www/html/web2
</VirtualHost>

注意:如果做多 IP 的虚拟主机,请为主机增加 IP 后按照上面的流程配置。

基于端口

1
2
3
4
5
6
7
8
Listen 81
Listen 82
<VirtualHost 192.168.18.250:81>
DocumentRoot /var/www/html/web1
</VirtualHost>
<VirtualHost 192.168.18.250:82>
DocumentRoot /var/www/html/web2
</VirtualHost>

注意:配置文件如上,大家可以自行更换端口及根目录。

基于域名

1
2
3
4
5
6
7
8
<VirtualHost *:80>  
ServerName a.dusays.com
DocumentRoot /var/www/html/web1
</VirtualHost>
<VirtualHost *:80>
ServerName b.dusays.com
DocumentRoot /var/www/html/web2
</VirtualHost>

注意:需首先将配置文件中 NameVirtualHost *:80 一项去掉注释,然后进行如上配置。

IIS 启用目录浏览功能项

2020年3月16日 00:00

做文件分享时,通过 Web 可能会方便一些,将文件都放在 Web 目录下,别人即可直接访问下载,那这项功能如何实现呢?这里就需要开启 IIS 目录浏览功能。

功能介绍

IIS 的工作原理是当有请求信息时,会将网站根目录中的网站源码解析成网页,最后再通过浏览器显示出来。如果根目录内没有网站源码则会提示权限错误。

如果开启目录浏览功能,当根目录内没有网站源码时,则会将目录内其它文件显示出来。

设置步骤

首先点击快速启动栏最左边服务器管理器:

然后依次点开角色,Web 服务器,选 Internet 信息服务:

在右侧选择已创建好的默认网站:

在选项中找到目录浏览:

进入后启用此功能:

启用后可修改显示信息:

然后进入网站所在目录,上传文件即可,这里需要注意的是,所在的目录不要有 html 等网页源码文件:

测试成功:

Windows2008 系统 IIS 服务安装

2020年3月13日 00:00

Windows2008 我们安装的是 Web Server 版本,主要就是用来搭建网站,今天杜老师为大家讲解一下 IIS 服务的安装流程。如遇其它问题,可在文章的评论去留言!

服务介绍

IIS 是由微软公司提供基于运行 Windows 的互联网基本服务,最初是 NT 版本的可选包,随后内置在之后的版本一起发行。

IIS 是一种网页服务组件,其中包括 Web 服务器、FTP 服务器、SMTP 服务器,分别用于网页浏览、文件传输、新闻服务和邮件发送等方面。

安装流程

这是一个欢迎界面,没有太大用途,每次添加角色都会显示,如果感觉麻烦,可以勾选默认情况下不显示此项:

因为系统版本原因,这里只显示 IIS 一个选项,勾选后会提示如下:

因为添加此项角色,需要依赖环境,所以要确认添加这些环境才可以安装:

确认后会显示勾选成功:

下图为 IIS 介绍:

这里需要选择组件功能,根据需要勾选即可,这里我们默认:

设置好后,就可以安装了:

安装成功,显示如图:

学习 Linux 的最佳环境

2020年2月27日 00:00

孟母三迁,只为让孩子有一个好的成长环境。环境对学习很重要,那么学习 Linux 的最佳环境是什么样的呢?与大家分享一下杜老师成长中的学习环境!

如何学习

好久没更新了,不知道写点什么好,最近忙着课改,下班又忙着回邮件,已无时间打点博客。不过时间就像乳沟,挤挤总会有的「当然不是挤我」今天我们就来聊聊新手们关心的问题,Linux 的最佳学习环境是啥?

孟母三迁,只为孟子有个好的成长环境。大家都知道环境对我们的影响力,如果你出生在米国,相信你的英语能力分分钟 GRE;如果你出生在日本,相信你能听懂的不仅仅呀咩蝶了!有个好的学习环境,我们可以更快掌握好 Linux。

什么样的环境才是最好的呢?你别问我,说实话我也不知道,毕竟最好的东西是不存在的,但我可以说说,我是如何学习 Linux 的!

先要装一个 Linux 系统,很多大牛包括不是大牛的我,都建议大家不要在实体机上安装,毕竟前期大家不会使用,很多操作不知如何下手,久而久之,你会感觉 Linux 上什么都做不成,渐渐就会对其失去兴趣。

建立兴趣

兴趣是最好的老师,那如何对 Linux 建立兴趣呢?相信每个男人都不会拒绝一个大美女,同样,他也无法拒绝超绚丽的界面,所以前期装个漂亮的图形界面还是有用的,毕竟你也不会太多命令,再不让你用鼠标点,估计你会拿刀子捅人了。这一点和某些大牛观点冲突,一些大牛建议不要安装图形界面,会有依赖,大家根据自身喜好,自行决定。

接下来是教材。鸟哥的私房菜一书,被很多人誉为 Linux 的圣经,它记录了菜鸟到老鸟的成长过程,在这里推荐给大家。不过需要注意一下,鸟哥是台湾人,说话的方式很特别「至少我看了会起鸡皮疙瘩的」最后说下,如果可以的话,尽量别看视频,太过浪费时间,有那时间,不如多做练习。或者来这里我们交流下!

MySQL 数据库调优知识分享

2020年2月15日 00:00

MySQL 是一个关系型数据库管理系统,由瑞典 AB 公司开发,属于 Oracle 旗下的产品。MySQL 是最流行的关系型数据库管理系统之一,在 Web 应用方面,MySQL 是最好的 RDBMS 应用软件之一。

MySQL 的介绍

MySQL 是一种关系型数据库管理系统,关系数据库将数据保存在不同的表中,而不是将所有数据放在一个大仓库内,这样就增加了速度并且提高了灵活性。

MySQL 所用的 SQL 语言是用于访问数据库的最常用标准化语言。MySQL 软件采用了双授权的政策,分为社区版以及商业版,由于其体积小、速度较快、总体拥有成本较低,尤其是开放源码的这一特点,一般中小型网站的开发都选择 MySQL 作为网站的数据库。

缓存命中

一是在配置时,客户端与服务器端要使用相同的字符集而不是兼容。

二是在客户端,要固化查询的语句,从而可提高应用系统的查询效率。

三是提高内存中缓存的配置,不过用户的并发数越多,这个设置的效果会越不明显。

四是做分区表以保证查询条件不变来提高缓存的命中率。

CPU 的瓶颈

对于过多依赖数据库进行逻辑运算的情况,最好优化方式是将运算尽可能从数据库端迁移到应用端,降低数据库主机计算量。

对数据库逻辑 I/O 太大的状况,可以选择增加 CPU 的数目或更换更强劲的 CPU,也可以从应用角度尽量降低非必要请求或者是减少每次请求的数据量,同时针对 Schema 结构以及索引进行相应的优化调整,尽可能让完成一次请求所需检索的数据量更小。

IO 的瓶颈

增加内存可缓存数据量,这个方案能否达到效果取决于系统热点数据的总量。

改善底层存储设备的 I/O 能力,包括单个磁盘本身的性能和磁盘数目,同时还受存储自身以及存储和主机之间的带宽限制,需要同时考虑到这 3 方面的因素,做好总体分析和局部的平衡。

如何在 Windows 系统中构建 RAID 磁盘阵列

2020年2月12日 00:00

RAID 全称 Redundant Array of Inexpensive Disks,廉价冗余磁盘阵列,通过多块磁盘组成一种模式,来提高吞吐量和可靠性。这篇就讲一下如何在 Windows 系统中构建 RAID!

注意事项

不过讲的这些构建方法,我们称为软 RAID,原因是通过系统软件实现的,而通过 RAID 卡构建的,才叫硬 RAID。

它们除了构建方式有区别外,还有一个区别就是资源耗损。因为硬 RAID 通过硬件构建,所以数据会在 RAID 卡进行处理,然后放入硬盘,对系统不会有消耗;而软 RAID 需要系统先处理数据,然后写入硬盘,在操作时,会占用系统大约 20% 资源。

构建步骤

首先讲下我们实验环境,在虚拟机里跑 Windows7 32 位系统,实验之前需要添加三块硬盘。在计算机点击右键,进入管理:

然后进入磁盘管理:

因为硬盘是刚刚添加的,所以会出现初始化硬盘这个窗口,直接点击确认即可。有人可能会问,下面两个选项分别什么意思,这里简单说下。MBR 主启动只能分 4 个主分区,其中扩展分区相当于主分区;GPT 分区表主要应用在 T 级以上的硬盘,几乎不限制主分区:

初始化后就会看到你的硬盘分区:

有几种分区形式呢?简单和普通的主分区一样的;跨区这个不属于 RAID,相当于一块硬盘不够用,在尾巴上接上另一块盘,容量相当于两个硬盘总容量;带区是 RAID0;镜像是 RAID1。我们这里只以 RAID1 镜像卷为例,演示创建过程,其它形式大同小异,不懂可以留言:

点击菜单栏的操作,所有任务,点新建镜像卷,就会出现新建向导:

左边框里是可以使用的硬盘,右边是已经选择的硬盘,需要点击左边框里硬盘,选择添加:

硬盘添加之后,可以设置硬盘大小:

这里是给硬盘分配盘符:

是否要格式化硬盘,以及格式化的参数:

配置完成,点击完成即可生效:

新添加的硬盘一般都是基本磁盘,而要实现卷的创建,需要转换成动态磁盘才可以,点是即可:

稍等一会,就可以在磁盘管理中看到了:

最后我们去计算机确认一下,创建成功:

在 Linux 上安装虚拟机

2020年2月9日 00:00

之前教大家如何在 Windows 安装虚拟机,那在 Linux 上如何安装虚拟机呢?Linux 一边会自带 KVM 虚拟机,不过本篇教程会教大家如何安装 VMware 虚拟机!

VMware 虚拟机

VMware 是一款功能强大的桌面虚拟计算机软件,提供用户可在单一的桌面上同时运行不同操作系统,进行开发、测试、部署新应用程序的最佳解决方案。VMware 可在一部实体机器上模拟完整网络环境,以及可便于携带的虚拟机器,其更好的灵活性与先进的技术胜过了市面上其它虚拟计算机软件。

对于企业的 IT 开发人员和系统管理员而言,VMware 在虚拟网路,实时快照,拖曳式共享文件夹,支持 PXE 等方面的特点使它成为必不可少的工具。

安装过程

首先需要下载 VMware for Linux 版,这里用的是 9.0.3 版本,建议用最新版:

打开一个终端,进入这个文件所在目录,然后输入如图内容,注意版本不同名字不同,不要照我输入,尽量使用 tab 键补全:

开始运行安装,首先同意协议:

然后选择是否在启动时检查更新:

是否匿名发送使用信息:

设置 Workstation Server 的用户,这个无视即可:

设置虚拟机的安装路径:

设置 HTTPS 的端口,默认即可:

这里用来设置虚拟机的最大文件打开数量:

开始安装:

安装完成:

在左上角应用程序,系统工具,点 VMware Workstation 启用:

首次启用需要同意协议:

开启成功,虽然界面是英文的,不过操作和布局与 Windows 版相同:

CDN 是什么与其优势

2020年2月6日 00:00

对于全国而言,南北方的网络运营商也不是一个,但是跨运营商的网络访问的速度就会大大降低。但对于网站的运营者或者开发者而言,总不能让南北方的童鞋们访问自己的网站的速度有明显的差异吧,为了解决这个问题,使用 CDN 技术就是个非常好的选择。

CDN 是什么

随着互联网的发展,用户在使用网络时对网站浏览速度和效果愈加重视,但由于网民的数量激增,网络访问路径过长,从而使用户的访问质量受到严重影响。特别是当用户与网站之间的链路被突发的大流量数据拥塞时,对于异地互联网用户急速增加的地区来说,访问质量不良更是个急待解决的问题。如何才能让各地的用户都能够进行高质量访问,并尽量减少由此而产生的费用和网站管理压力?内容发布网络 CDN 全称 Content Delivery Network 诞生了。

CDN 是一种通过互联网互相连接的电脑网络系统,利用最靠近每一位用户的服务器,更快、更可靠将音乐、图片、视频、应用程序及其他文件发送给用户,来提供高性能、可扩展性及低成本的网络内容传递给用户。

CDN 的优势

内容分发网络的总承载量可以比单一骨干最大的带宽还要大。这使得内容分发网络可以承载的用户数量比起传统单一服务器多。也就是说,若把有 100Gbps 处理能力的服务器放在只有 10Gbps 带宽的数据中心,则亦只能发挥出 10Gbps 的承载量。但如果放到十个有 10Gbps 的地点,整个系统承载量就可以到 10*10Gbps。同时,将服务器放到不同地点,可以减少互连流量,进而降低带宽成本。而节点的实际数量就会影响 CDN 的效果。

内容分发网络另外一个好处在于异地备援。当某一个服务器故障时,系统将会调用其他邻近地区的服务器服务,进而提供接近 100% 可靠度。除此之外,内容分发网络提供给服务提供者更多的控制权。提供服务的人可以针对客户、地区,或是其它因子调整。

为什么服务器要选择 Linux 系统

2020年1月28日 00:00

从最近的统计数据可以看到,全球大量数据中心的服务器已经开始向基于 Linux 平台转移。相比较 Windows 而言,Linux 提供了更多优势。包括 Google/Twitter/Facebook 和 Amazon 在内诸多国际互联网巨头,都是在基于 Linux 的服务器上运转他们的服务。

开源稳定

Linux 相较 Windows 领先的首要原因是完全免费且可用作开源用途。通过开源方式,可以轻松查看用于创建 Linux 内核的代码,也可以对代码进行修改和再创作。通过许多编程接口,您甚至可以开发自己的程序并将其添加到 Linux 操作系统中。还可以对 Linux 操作系统进行自定义,以满足使用的要求,这是 Windows 无法实现的。

Linux 系统一直以其稳定性闻名,它们可以连续运行多年而不发生任何重大问题。事实上很多 Linux 用户都从未在自己的环境中遇到过系统崩溃的情况。相对比 Windows 而言,挂起和崩溃完全是一种常态。

尽管 Windows 也可以很好地执行多任务处理,但 Linux 可以在处理各种任务的同时,能提供坚如磐石的性能。

当将 Linux 与 Windows 进行对比时,对每项系统配置的更改都需要重启 Windows,而 Linux 更改大多数配置时都无需重启服务器即可生效,这也确保了 Linux 服务器最短的停机时间。

安全灵活

Linux 由最初的多用户操作系统开发的 Unix 操作系统发展而来,在安全方面显然要比 Windows 更强。与 Windows 作为病毒和恶意软件攻击的首要目标不同,Linux 只有管理员或特定用户才有权访问 Linux 的内核,而且 Linux 服务器不会经常受到攻击,并且被发现的任何漏洞都会在第一时间由大批的 Linux 开发人员修复。

Linux 是世界最灵活的操作系统,没有之一,您可以根据需要自定义系统。使用 Linux 您可以随心所欲地安装 GUI 界面或仅使用终端来管理服务器;使用 Linux 您可选择各种工具和实用程序来管理所有与服务器相关的活动,例如:添加用户、管理服务、网络,安装新应用程序以及监控性能等。

而 Shell 是 Linux 系统最强大的组件,允许运行各种程序并允许与内核进行交互。

总的来说,Linux 为您提供对服务器的完全控制、掌控权利。

硬件软件

当将 Linux 与 Windows 在硬件方面进行比时,Windows 需要经常对硬件进行升级以满足各方面性能需求。而 Linux 对硬件的需求很低,不需要频繁对硬件进行升级更新,并且无论系统架构或处理器如何,都能表现非常出色。

Linux 为开源应用程序开辟一个新的世界,有数以千计的开源应用程序正在等待用户探索,您甚至可以使用特殊界面在 Linux 服务器上运行 Windows 应用程序。

自由变更

对 Linux 而言,您不会被商业供应商强加产品和服务,用户可自由选择适合需求的产品。正是这种自由使得像 Amazon 和 Google 这样的大公司选择基于 Linux 的服务器来提供服务。

与 Windows 不同,您可轻松对 Linux 进行变更,并无需重启服务器。对 Windows 而言,要使用微软发布的新功能就得购买下一个版本。

维护成本

总体拥有成本方面,由于 Linux 在使用上属于是完全免费。

即便您购买了针对企业或组织的 Linux 发行版,也会比 Windows 或者其他许可软件花费更少。

社区支持

Linux 社区在全球都十分活跃和使用广泛,总有数千名志愿者在线活动解决其他 Linux 用户的问题,所以几乎在任何 Linux 论坛上发布的任何问题都会得到即时响应。

而选择用 Linux 企业时,还会附有付费支持选项。

Linux 下 8 种查 xfs 等文件系统类型的实用方法总结

2020年1月25日 00:00

在对磁盘做挂载等操作之前,我们需要先知道该磁盘所对应的文件系统类型,下面杜老师为大家介绍 Linux 下 8 种查 xfs 等文件系统类型的实用方法!

First 种方法

命令是 blkid /dev/sda1,执行效果如下:

Second 种方法

命令是命令是 df -T,执行效果如下:

Third 种方法

命令是 file -sL /dev/sda1,执行效果如下:

Fourth 种方法

命令是 fsck -N /dev/sda1,执行效果如下:

Fifth 种方法

命令是 cat /etc/fstab,执行效果如下:

Sixth 种方法

命令是 lsblk -f,执行效果如下:

Seventh 种方法

命令是 mount | grep "^/dev",执行效果如下:

Eighth 种方法

命令是 cat /proc/mounts | grep "^/dev",执行效果如下:

负载状态 Load Average 中的数据代表了什么

2020年1月13日 00:00

当我们执行 top 或 uptime 等命令时,都会看到一组数值,这组数值代表了当前服务器负载状态,它是服务器运行状况的一项较重要的参考数值,被称为 Load Average「平均负载」今天杜老师为大家讲解一下!

负载状态

平均负载 Load Average 是指系统的运行队列的平均利用率,也可以认为是运行进程的平均数。这个一段时间一般取 1 分钟、5 分钟以及 15 分钟。显示如下:

状态讲解

下面我们以高速道路来例举服务器在不同负载状态时的表现。我们假设:CPU 核心数=车道数量;内存=车道宽度;磁盘的 I/O=车道限速。单核相当于单车道,双核相当于双车道;偶尔来个请求,相当于车辆少,服务器较流畅;请求较多,但在车道负载范围之内,服务器也没有太大压力;请求变的更多,已经达到服务器的负载临界,服务器会变的缓慢;服务器已经在忙于处理请求,但有大量请求还未流入,就会造成堵塞。服务器的压力效果如图:

公有、私有和联盟链区别

2019年11月16日 00:00

区块链的本质是一种开源分布式账本,它是比特币和其它虚拟货币核心技术,能高效记录买卖双方的交易,并保证这些记录是可查证而且永久保存的。相信有人也听说过区块链分为公有链、私有链以及联盟链,它们三者有何区别?

区块链之公有

从字面上理解,就是公有,它是指对全世界所有人开放,任何人都可以读取数据、发送交易而且交易能够获得有效确认的共识区块链。

公有链上各个节点均可自由加入、退出网络,并参加链上数据的读写,并且,网络中不存在任何中心化的服务节点。

比特币大家熟悉吧?采取的就是公有链。因为没有限制,任何人都可以参加。

公有链的特点:

  1. 访问门槛较低:只要有计算机,只要计算机能联网,就都能够访问;
  2. 数据公开透明并且无法篡改:公有链是高度去中心化的分布式账本,每个人在任何节点的行为都是可以查看的,且基于庞大的用户体系,想要篡改交易数据,几乎是不可能实现;
  3. 匿名:由于节点之间无需彼此信任,所有的操作都可以匿名进行,很好地保护使用者隐私;
  4. 免受开发者的影响:公有链数据的读写是不受任何人控制和篡改的,就连程序开发者也无权干涉用户。所以,极大程度上保护了用户免于程序开发者的影响。

当然,公链也存在着交易速度慢等问题,由此,我们产生了私有链概念。

区块链之私有

私有链也称专有链。它是条非公开的链,通常情况,需要授权才能加入节点。且私有链中各个节点的写入权限皆被严格控制,读取权限则可视需求有选择性地对外开放。

通常情况,私有链适用于企业内部应用,以及特定机构的内部数据管理与审计等金融场景的应用。特别在某些情况下,私有链上一些规则,可被机构修改,如还原交易流程等服务。

蚂蚁金服就是应用私有链的显著代表。

私有链的特点:

  1. 交易速度:私有链上节点只有少量,并且具有很高的信任度,交易不需要所有网络节点的确认,所以其交易速度比任何其他的区块链都快;
  2. 隐私保障良好:由于读取权限是由该组织决定的,参与者难以获得私有链上的数据,因此组织自身隐私保障更好;
  3. 交易成本大幅降低:私有链的交易只需几个受到普遍认可的高算力节点确认即可,其交易成本与公有链和联盟链相比较极低;
  4. 安全性高:链上成员都经过审核授权的,所以恶意攻击的可能性相对较小。

不过,私有链也存在问题:如权限被少数节点控制,不能根本解决作弊问题,背离了去中心化的初衷;此外,私有链上的数据可能被操纵,代码也可能被修改。

区块链之联盟

联盟链是指由多个机构共同参与管理的区块链,每个组织或机构管理一个或多个节点,其数据只允许系统内不同的机构进行读写、发送。

联盟链的各个节点通常有与之对应的实体机构组织,通过授权后才能加入与退出网络。各机构组织组成利益相关的联盟,共同维护区块链的健康运转。

联盟链适合组织机构间的交易和结算,目前国内比较有影响力的联盟链有中国分布式总账基础协议联盟、中国区块链研究联盟和微众银行等等。

联盟链的特点:从某种程度上来说,其实联盟链属于私有链,但是它私有的程度不同,其权限设计要求更复杂,可信程度更高。

区块链之总结

一般来说,公有链适用于对可信度、安全性有很高要求,而且对交易速度要求不高的场景。

私有链或者联盟链更适合对隐私保护、交易速度和内部监管等具有比较高要求的应用。

区块链是什么

2019年11月8日 00:00

区块链到底是什么?在网络上出现频率很高,看到这个词觉得很厉害,但不确切知道什么意思。而且最近区块链火了,不仅相关的概念股大涨,还被中央点名。今天杜老师给大家讲下:什么是区块链!

技术概念

从学术角度来解释,区块链是分布数据存储、多中心点对点传输、共识机制、加密算法等计算机技术新型应用模式。

区块链本质是一个去中心化的数据库。

白话解读

假如你们家里有个账本,让你记账。爸爸妈妈把工资交给你,你记到账本上。但如果你贪吃,想买点好吃的,可能账本上的记录会少个十几块。这就是集中式数据库的缺点,数据被管理者把控。

区块链是如何解决这个问题?我们使用全家总动员的方式记账,上述的问题就不会有了,因为你在记账,爸爸也在记账,妈妈也在记账,他们都能看到总账,你不能改,爸爸妈妈也不能改,这样想买烟抽的爸爸和想贪吃的你都没办法啦。这就是一个去中心化数据库,并没有一个权威的管理,所以数据库的内容互相同步、校验。

技术特点

  1. 安全:不同于公司或者政府机构拥有的集中化数据库,区块链不受任何人或实体的控制,数据在多台计算机上完整地复制。其与集中式数据库不同,攻击者没有一个单一的入口,数据的安全性更有保障;

  2. 不可篡改:一旦进入到区块链,任何的信息都无法更改,甚至管理员也无法修改信息。一个东西一旦出现就再没法改变,这种属性对于目前所处的可更改、瞬息万变的网上世界而言意义很重大;

  3. 可访问性:网络中所有节点都可以轻松访问信息;

  4. 无第三方:因为区块链的去中心化,它可以帮助点对点交易,因此,无论是在交易还是交换资金,都无需第三方批准。

技术应用

  1. 数字身份:我们的出生证、房产证以及婚姻证等等,需要一个中心节点,大家才能承认。一旦跨国,合同、证书可能就失效了,因为缺少全球性的中心节点。区块链技术不可篡改特性从根本上改变了这一情况,我们的出生证、房产证以及婚姻证都可以在区块链上公证,变成全球都信任的东西;

  2. 医疗健康:简单说就是利用区块链建立有时间戳的通用记录存储库,进而达到不同数据库都可提取数据信息的目的。例如你去看病,不用每换个医院就反复检查,也不用为报销医保反复折腾,可节省时间和开销;

  3. 旅行消费:例如我们经常会用携程、美团等来寻找并下单入住酒店和其他服务,各个平台从中获得提成。而区块链的应用正是除去中间商,并为服务提供商和客户创建安全和分散的方式,以达到直接进行连接和交易目的;

  4. 更便捷的交易:区块链可以让支付和交易变得更高效且更便捷。区块链平台允许用户创建在满足某些条件时变为活动的智能合约,意味着当交易双方同意满足其条件时,可以释放自动付款;

  5. 严把产品质量:假如你买了个苹果,在区块链的技术下,你可以知道从果农的生产到流通环节的全过程。在这其中有政府的监管信息、有专业的检测数据、有企业的质量检验数据等等。智慧供应链将使我们日常吃到的食物、用到的商品更安全,让我们更放心;

  6. 产权保护:艺术创作者把自己作品放在区块链上,有人用了他的作品,他就可以立刻知道。相应版税也会自动支付给创作者。区块链的技术既保护了版权,也有助于创作者更好更直接地向消费者售卖自己的作品,而不再需要发行公司的协助。

通过 asciinema 录制 Linux 命令

2019年10月12日 00:00

作为一枚贴心的杜老师,我需要经常给学生演示实验流程,而截图的形式太过繁琐,视频又太占用流量,有什么更好的方法?今天杜老师为大家分享一个工具,可以录制 Linux 的命令!

工具介绍

asciinema 一个强大的神器。它能够在 Linux 等终端上进行操作的录制,与常规的录屏软件不同的是,其录屏并不会生成视频文件,而是生成文字形式,我们可以通过命令自带的播放器进行回放。

如果没有安装这个工具如何播放?完全不用担心这个问题,它还能够将录制的结果上传到官网中进行播放,而且提供了强大的引用功能,方便大家将其引入到页面中,或存储到本地。

如何安装

1
pip3 install asciinema

注意:如果提示 pip3 命令未找到,可使用 yum -y install python3-pip 安装。

如何使用

开始录制:

1
asciinema rec

开始录制并指定本地保存的文件名:

1
asciinema rec dusays.cast

开始录制,并设置空闲间隔为 2 秒:

1
asciinema rec -i 2

播放录制好的文件:

1
asciinema play dusays.cast

重播录制好的文件,并指定播放的倍速:

1
asciinema play -s 2 dusays.cast

如果你想在网上观看和分享,请上传它:

1
asciinema upload dusays.cast

效果演示

点击播放:

asciicast

如何手动创建一个用户

2019年10月11日 00:00

我们可使用 useradd 这个命令快速创建一个用户,但如果这个命令不能使用时,我们如何手动创建一个用户?今天杜老师讲一下手动创建一个用户,让大家更好的了解 useradd 的运行机制!

演示动画

点击播放:

asciicast

操作命令

1
2
3
4
5
6
echo 'dusays:x:1001:1001:dusays.com:/home/dusays:/bin/bash' >> /etc/passwd
echo 'dusays:x:1001:' >> /etc/group
cp -r /etc/skel /home/dusays
chown -R dusays. /home/dusays
su - dusays
exit

注意:为了更好演示,这里使用了 echo 命令,大家可使用 vim 直接改写配置文件!

MySQL 在不同内存下的资源分配

2019年9月27日 00:00

数据库调优包含两个大方向,一个是 MySQL 参数优化,另一个是 SQL 语句的优化,后者常交给开发工程师处理。而 MySQL 需要经过大量尝试才能得出最优参数,今天给大家讲解一下参数的作用及相关推荐值!

参数作用

首先说一下各参数作用:

参数名称参数作用
key_buffer_size单位为 MB,用于索引的缓冲区大小
query_cache_size单位为 MB,查询缓存,不开启请设为 0
tmp_table_size单位为 MB,临时表缓存的大小
innodb_buffer_pool_size单位为 MB,InnoDB 缓冲区大小
innodb_log_buffer_size单位为 MB,InnoDB 日志缓冲区大小
sort_buffer_sizeKB 乘以连接数,每个线程排序的缓冲的大小
read_buffer_sizeKB 乘以连接数,读入缓冲区的大小
read_rnd_buffer_sizeKB 乘以连接数,随机读取缓冲区的大小
join_buffer_sizeKB 乘以连接数,关联表缓存的大小
thread_stackKB 乘以连接数,每个线程的堆栈的大小
binlog_cache_sizeKB 乘以连接数,二进制日志缓存的大小「4096 的倍数」
thread_cache_size线程池的大小
table_open_cache表的缓存「最大别超过 2048」
max_connections最大的连接数

推荐阈值

然后说一下不同内存的推荐阈值:

优化方案1-2GB2-4GB4-8GB8-16GB16-32GB
key_buffer_size1282683845121024
query_cache_size00000
tmp_table_size6438451210242048
innodb_buffer_pool_size25638451210244096
innodb_log_buffer_size88888
sort_buffer_size768768102420484096
read_buffer_size768768102420484096
read_rnd_buffer_size51251276810242048
join_buffer_size10242048204840968192
thread_stack256256256384512
binlog_cache_size6464128192256
thread_cache_size6496128192256
table_open_cache12819238410242048
max_connections100200300400500

特别声明

不同的程序对数据库的需求不同,以上数值仅供参考,具体阈值还要根据实例运行情况进行修改。

以上数值皆为云主机进行的尝试,如果是实体服务器,则阈值差异会更大!

补充说明

query_cache_size 为查询缓存,我习惯不启用,毕竟 WordPress 的查询语句实在惨不忍睹,这个对 WordPress 用户来说几乎无用。

如果想开启也可以,这个没有范围,只要内存够用开多大都可行!

MySQL 几种存储引擎的介绍

2019年9月26日 00:00

在数据库中存的就是一张张有着千丝万缕关系的表,所以表设计的好坏,直接影响着整个数据库。而在设计表的时候,我们都会关注一个问题,使用什么存储引擎。等下,存储引擎?什么是存储引擎呢?

存储引擎

关系数据库表用于存储和组织信息的数据结构,可以将表理解为由行和列组成的表格,类似于 Excel 的电子表格的形式。

有的表简单有的表复杂,有的表根本不用存储任何长期的数据,有的表读取非常快,但是插入数据时却很差;我们在实际开发过程中,可能需要各种各样的表,不同的表,意味着存储不同类型的数据,数据的处理上也会存在差异。

那么,对 MySQL 来说,它提供了很多种类型的存储引擎,我们可以根据对数据处理的需求,选择不同存储引擎,从而最大限度利用 MySQL 强大的功能。

这篇博文将总结和分析各个引擎特点,以及适用场合,并不会纠结更深层次的东西。我的学习方法是先学会用懂得怎么用,再去知道到底是如何能用的。下面就对 MySQL 支持的存储引擎进行简单介绍。

MyISAM 表

MyISAM 是独立于操作系统的,这说明可以轻松地将其从 Windows 服务器移植到 Linux 服务器;每当我们建立一个 MyISAM 引擎的表时,就会在本地磁盘上建立三个文件,文件名就是表名称。例如,我建立了一个 MyISAM 引擎的 tb_demo 表,那么就会生成以下三个文件:

  1. tb_demo.frm 存储表定义;
  2. tb_demo.MYD 存储表数据;
  3. tb_demo.MYI 存储表索引。

MyISAM 表无法处理事务,这就意味着有事务处理需求的表,不能用 MyISAM 存储引擎。MyISAM 存储引擎特别适合以下几种情况下使用:

  1. 选择密集型表。MyISAM 存储引擎在筛选大量数据时非常快,是它最突出的优点;
  2. 插入密集型表。MyISAM 的并发插入特性允许同时选择、插入数据。例如:MyISAM 存储引擎非常适合管理邮件或 Web 服务器日志数据。

InnoDB 表

InnoDB 是一个健壮的事务型存储引擎,这种存储引擎已经被很多互联网公司使用,为用户操作非常大的数据存储提供一个强大的解决方案。

我的电脑上安装的 MySQL 5.6.13 版,InnoDB 就是作为默认的存储引擎。InnoDB 还引入了行级锁定和外键约束,在以下场合下,使用 InnoDB 是最理想的选择:

  1. 更新密集的表。InnoDB 存储引擎特别适合处理多重并发的更新请求;
  2. 事务。InnoDB 存储引擎是支持事务的标准 MySQL 存储引擎;
  3. 自动灾难恢复。与其它的存储引擎不同,InnoDB 表能够自动从灾难中恢复;
  4. 外键约束。支持外键的存储引擎只有 InnoDB;
  5. 支持自动增加列 AUTO_INCREMENT 属性;

一般来说,如果需要事务支持,并且有较高的并发读取频率,InnoDB 是很不错的选择。

Memory 表

使用 MySQL Memory 存储引擎的出发点是速度。为得到最快的响应时间,采用的逻辑存储介质是系统内存。虽然在内存中存储表数据确实提供很高的性能,但当 mysqld 守护进程崩溃时,所有的 Memory 数据都会丢失。获得速度的同时也带来一些缺陷。它要求存储在 Memory 数据表里的数据用的是长度不变的格式,这意味着不能用 BLOB 和 TEXT 这样的长度可变的数据类型,VARCHAR 是种长度可变的类型,但因为它在 MySQL 内部当做长度固定不变的 CHAR 类型,所以可以使用。

一般在以下几种情况下用 Memory 存储引擎:

  1. 目标数据较小,而且被非常频繁地访问。在内存中存放数据,所以会造成内存的使用,可以通过参数 max_heap_table_size 控制 Memory 表的大小,设置此参数就可以限制 Memory 表最大大小;
  2. 如果数据是临时的,而且要求必须立即可用,那么就可以存放在内存表中;
  3. 存储在 Memory 表中的数据如突然丢失,不会对应用服务产生实质的负面影响。

Memory 同时支持散列索引和 B 树索引。B 树索引可以使用部分查询、通配查询,也可以使用操作符方便数据挖掘。散列索引进行“相等比较”时非常快,但对“范围比较”速度就慢多了,因此散列索引值适合使用在和操作符,不适合在或操作符,也同样不适合用在 order by 子句中。

Merge 表

Merge 存储引擎是一组 MyISAM 表组合,这些 MyISAM 表结构必须完全相同,尽管使用不如其它引擎突出,但是在某些情况下非常有用。说白了 Merge 表就是几个相同 MyISAM 表的聚合;Merge 表中并没有数据,对 Merge 类型的表可以进行查询、更新、删除操作,这些操作实际上是对内部的 MyISAM 表进行操作。

Merge 存储引擎使用场景对于服务器日志这种信息,一般常用的存储策略是将数据分成很多表,每个名称与特定时间端相关。例如:可以用 12 个相同的表来存储服务器的日志数据,每个表用对应各个月份的名字来命名。当有必要基于所有 12 个日志表的数据来生成报表时,这意味着需要编写、更新多表查询,以反映这些表中的信息。与其编写这些可能会出现错误的查询,不如将这些表合并起来使用一条查询,之后再删除 Merge 表而不影响原来的数据,删除 Merge 表只会删除掉 Merge 表的定义,对内部表没有任何影响。

Archive 表

Archive 就是归档的意思,在归档之后很多的高级功能就不再支持了,仅支持最基本的插入和查询两种功能。

在 MySQL 5.5 版前,Archive 不支持索引,但是在 MySQL 5.5 以后的版本中就开始支持索引了。Archive 拥有很好的压缩机制,使用 zlib 压缩库,在记录被请求时会实时压缩,所以它经常被用来当做仓库使用。

MySQL 主从复制的简介

2019年9月25日 00:00

MySQL 主从复制是指数据可以从一个数据库服务器主节点复制到一个或多个从节点。MySQL 数据库默认采用异步复制方式,这样从节点不用一直访问主节点来更新自己的数据,数据的更新也可以在远程连接上进行,从节点可以复制主数据库中的所有数据库或者特定的数据库,或者特定的表。

何为主从复制

主从同步是在一个交换节点设立高精度的基准时钟,通过传输链路把此基准时钟信号送到网中的各个从节点,各个从节点利用锁相环技术把本地的时钟频率锁定在基准时钟频率上,从而实现网内各节点之间的时钟信号同步。

主从同步网一般采用星状结构或树状结构,其中大部分的节点为从节点。从节点的时钟跟踪基准时钟状态,可以有效降低成本。因此,主从同步技术能够提高时钟系统的精确度,同时降低成本。

没有主从复制

没有主从复制的情况是什么样子:

有了主从复制

有了主从复制的情况是什么样子::

主从复制原理

主从复制原理图示:

主从复制结构

主从同步网中时钟的传输可以呈现星状结构或树状结构。当基准时钟到各个从节点均有直达链路时,传输链路呈现星状结构;当基准时钟到有些从节点没有直达链路时,传输采用逐级传递方式,传输链路呈现树状结构。

等级主从同步是为了改善可靠性而采用的一种同步方式。在这种方式中,交换局的时钟精度都有一个等级,当基准时钟失效时采用次一级时钟作为主时钟,传送到各个局的时钟信息都带有等级识别信息。显然,这是以复杂性换取可靠性的一种同步方式。

主从复制优点

在主从同步区域内形成一个全同步网,从而避免准同步网中固有的周期性滑动和波动;在主从同步网中绝大多数的节点是从节点,从节点的时钟处于跟踪基准时钟状态。因为对从节点的压控振荡器只要求有较低精度,并且级别越低,要求就会越低。

当系统采用单端控制时,定时信号传输链路上的扰动将会导致定时基准信号扰动,这会在一定程度上影响时钟同步的稳定性。

PHP 动态模式和静态模式区别

2019年9月24日 00:00

CGI 是通用网管协议,是用于让交互程序和 Web 服务器通信的协议。FastCGI 是基于 CGI 的增强版本协议,不同于创建新的进程来服务请求,使用持续的进程和创建子进程来处理一连串的进程,这些进程交由 FastCGI 服务器管理,开销更小,效率更高。php-fpm 是 PHP 实现的 FastCGI 进程管理器,用于替换 FastCGI 大部分附加功能,适用于高负载网站。

静态动态

  1. 静态:直接开启指定数量的 php-fpm 进程,不再增加或者减少;

  2. 动态:开始的时候开启一定数量 php-fpm 进程,当请求变大的时候,动态的增加 php-fpm 进程数到上限,当空闲的时候自动释放空闲进程数到一个下限。

涉及参数

这里先说一下涉及到这个的几个参数,他们分别是 pm/pm.max_children/pm.start_servers/pm.min_spare_servers 和 pm.max_spare_servers:

  1. pm 表示用哪种方式,有两个值可以选择,就是 static 静态或者 dynamic 动态。
  2. pm.max_children 静态方式下开启的 php-fpm 进程数量;
  3. pm.start_servers 动态方式下的起始 php-fpm 进程数量;
  4. pm.min_spare_servers 动态方式下的最小 php-fpm 进程数量;
  5. pm.max_spare_servers 动态方式下的最大 php-fpm 进程数量。

参数关系

如果将 pm 设置为 static,那么就只有 pm.max_children 这个参数生效。系统会开启设置的数量个 php-fpm 进程。

如果将 pm 设置为 dynamic,那么 pm.max_children 参数会失效,后面 3 个参数生效。系统会在 php-fpm 运行开始的时候启动 pm.start_servers 个 php-fpm 进程,然后根据系统需求动态在 pm.min_spare_servers 和 pm.max_spare_servers 之间调整 php-fpm 进程数。

参数选择

那么,对于我们的服务器,选择哪种执行方式比较好呢?事实和 Apache 一样,我们运行的 PHP 程序在执行完成后,或多或少都会有内存泄露的问题。这也是为什么开始的时候一个 php-fpm 进程只占用 3M 左右内存,运行一段时间后就会上升到 20 到 30M 的原因。所以,动态方式因为会结束掉多余进程,可以回收释放一些内存,所以推荐在内存较少的服务器上使用。具体最大数量根据物理内存总大小除以 20M 得到。比如说 512M 的 VPS,建议 pm.max_spare_servers 值设置为 20。而至于 pm.min_spare_servers,则建议根据服务器的负载情况来设置,比较合适的值在 5 到 10 之间。

对比较大内存的服务器来说,设置为静态的话会提高效率。因为频繁开关 php-fpm 进程也会有时滞,所以内存够大的情况下开静态效果会更好。数量可以根据物理内存总大小除以 30M 得到。比如说 2GB 内存的服务器,可以设置为 50;4GB 内存可以设置为 100 等。

IaaS/PaaS/SaaS 的区别

2019年9月22日 00:00

你一定听说过云计算中三个高大上的概念:IaaS/PaaS 以及 SaaS,这几个术语并不好理解。不过,如果你是一个吃货,还喜欢吃披萨,这个问题就容易解决了!一个 “吃货” 是怎样吃到披萨的?

自己做吧

这真是个麻烦的事,你的准备很多东西,发面、和面、进烤箱等,简单列举一下,需下图所示的一切:

速食披萨

只需要从披萨店里买回成品,回家热一下就好了,在自己的餐桌上吃。和自己在家做不同,你需要一个披萨供应商:

披萨外卖

打个电话,披萨就送到家门口:

在店里吃

什么都不需要准备,连餐桌也是 Pizza 店的:

总结一下

总结一下,吃货们可以通过如下途径吃披萨:

忘记披萨

假设你是一家超牛 X 的技术公司,根本不用别人提供服务,拥有基础设施、应用等等其它一切,你把它们分为三层:基础设施、平台、软件,如图:

三个分层

这其实是云计算的三个分层,基础设施在最下端,平台位于中间,软件位于顶端,分别是 IaaS/PaaS/SaaS,别的一些层可以在这些层基础上添加。

你的公司什么都有,现在所处的状态叫本地部署,就像在自己家做 Pizza 一样。几年前如果你想在办公室或者公司的网站上运行一些企业的应用,你需要去买服务器,或者别的高昂的硬件来控制本地应用,让你的业务都运行起来,这就叫做本地部署。

假如你家 boss 突然有一天想明白了,只是为了吃上披萨,为什么非要自己做?

于是,准备考虑一家云服务供应商,这个云服务供应商能提供哪些服务呢?其所能提供的云服务也就是云计算的三个分层:IaaS/PaaS 以及 SaaS,就像 Pizza 店提供三种服务:买成品回家做、外卖和到披萨店吃。

分层图示

用一张图来表示就是这样的:

IaaS 基础设施即服务

有了 IaaS 你就可以将硬件外包到别的地方去。IaaS 公司会提供场外的服务器、存储、网络硬件,你都可以租用。

节省了维护成本和办公场地,公司可以在任何时候利用这些硬件来运行应用。

PaaS 平台即服务

第二层就是所谓的 PaaS 了,某些时候也叫做中间件。公司所有的开发都可以在这一层进行,节省了时间和资源。

PaaS 公司在网上提供各种开发和分发应用的解决方案,比如虚拟服务器和操作系统。这节省了你在硬件上的费用,也让分散的工作室之间合作变得更加容易。网页应用管理,应用设计,应用虚拟主机,存储,安全以及应用开发协作工具等等。

SaaS 软件即服务

第三层也就是所谓 SaaS 了。这一层是和你的生活每天接触的一层,大多是通过网页浏览器来接入的。

任何一个远程服务器上的应用都可以通过网络运行,就是 SaaS 了。

概念扩展

IaaS 就是我们 IDC 机房,它为我们提供了服务器和相关的环境,包含网络、电源等等,我们只需要安装好系统、服务、程序即可使用。

各种云服务提供商应该都算是 PaaS,不仅提供相关硬件,而且还为我们准备好了系统,我们只需要上传程序就行了。

SaaS 最简单的理解就是网店了,比如淘宝,我们不需要买硬件,不需要安软件,直接可以在上面开通并使用。

看完这篇文章,你是否能分清它们之前的区别呢?有不同的看法,可以在页面下留言!

威联通NAS购入初体验以及设置记录

2024年10月29日 21:57

之前是用树莓派连个两盘位硬盘盒运行一些服务,由于它的稳定性加上容量不够,一直想弄一个NAS,趁着双十一到来,就入手了威联通的NAS,本文介绍 一下购入的抉择以及NAS的初始化和相关的设置。

缘起

NAS这个东西知道了很多年了,一直想要搞一个,迫于家里花费的紧张,之前一直是使用一台树莓派4B,其中刷了Openwrt系统,挂载了两块盘的硬盘盒,其中开启了Webdav, Samba,Jellyfin相关的东西。不过因为Jellyfin挂载阿里云盘速度不太理想,有不少视频还是下载到自己的硬盘里面的。同时内,硬盘也出现了拷贝大文件就出现问题,需要重启硬盘盒和系统的问题,这个后续会继续说。

DIY NAS硬件或者成品的NAS也关注了有挺长一段时间,迫于以上问题,以及文件越来越多,当时买的这两块2T的硬盘,容量已经不够用了,想要购买一个NAS的想法更加加强,终于决定今年双十一搞个NAS。

剁手

购买NAS是有两个选择,自己组装硬件,安装飞牛或者黑群晖等NAS系统,又或者购买群晖、威联通等成品NAS。在V2EX发帖求助,以及自己的纠结中,最终在性价比和稳定性等各种因素比较之后,选择入手了威联通TS464C2。

威联通的系统虽然被大家诟病许久,但是它也算是市场上除了群晖之外NAS系统做的最久的厂家了,考虑到文件的安全可靠在文件系统和系统稳定性上面,这两家还是要比国内的新起之辈更加值得信赖的。而我选择的这一款,支持内存扩展,如果以后服务比较多,可以再增加一根内存。4个3.5寸硬盘位加上两个NVME 硬盘位,对于容量的扩展应该很多年都不存在问题了。双十一这块机器只要2000块钱就拿下,而群晖同配置的4盘位差不多要四千,只能说高攀不起。

另外下单了一块国产的NVME 2T硬盘,加入Qtier存储池,希望能提高一些速度。为了拥有更大的容量,经过一些研究,淘宝购入了一块2手服务器硬盘,型号为HC550, 16TB,回来看Smart信息,已经运行了786天,不过其他信息看着都不错。

上电

收到机器,插上硬盘,参照指南开始初始化。威联通提供了比较友好的初始化方法,可以通过网页或者应用对它进行初始化,不过一定要插上硬盘才能开始这一切。

根据指南初始化之后,开始了硬盘的初始化和存储池的设置。之前使用的openwrt中对于硬盘的管理是比较简单的,基本就是实现了基础的Linux功能,把磁盘挂载到指定的目录,硬盘初始化之类的。而QNAP中,“存储与快照总管应用”中,对于硬盘和存储卷的设置则全面,可以设置各种raid的存储池,Qtier,快照,卷等等,也有硬盘的运行情况的显示。我想这就是选择大厂成品NAS的原因,毕竟docker之类的东西大家都很容易做,但是这种积累了很多年的东西不是那么快能够做出来的。

安装软件

在威联通NAS中安装软件可以选择从QNAP的应用中心安装应用,也可以安装Container Station之后通过docker来安装。不过官方的应用中心中的应用中主要是一些官方提供的应用,这个时候我们可以选择第三方的应用中心,这里我推荐一个: https://www.myqnap.org/repo.xml,官方应用商店没有的可以来这里试试。不过这个应用商店中的部分应用是收费的,比如Jellyfin,它提供的版本需要依赖Apache,这个时候你需要去它的网站上面购买,价格还不便宜,当然我是不会去购买的。

除了应用中心安装之外,我们还可以去网上找QPKG文件,比如Jellyfin,我就是使用的pdulvp为QNAP定制的版本,下载地址在:https://github.com/pdulvp/jellyfin-qnap/releases。Jellyfin我不使用官方的docker版本有两个原因,一是使用这个定制版本,可以方便的使用英特尔的集成显卡进行视频的硬解,另一方面是使用docker的化,默认只能映射一个媒体目录到Docker中,想要映射多个目录会麻烦一点,因此使用QPKG更方便。

对于其他的应用,比如FreshRss, VaultWarden则是选择了使用Docker进行部署,Container Station的Docker部署为先写一个compose文件,之后软件会帮助下载相关的容器进行启动,这个有个问题就是创建完compose之后,容器启动起来之后,在web界面上就没法编辑compose文件了,想要编辑的需要用ssh进终端去看,比如我创建的app-freshrss它的compose文件就在/share/Container/container-station-data/application/app-freshrss当中。

另外威联通自带的一些应用,文件管理,QuMagie,特别要说一下QuMagie,它已经可以帮我把相片按照人物,地点,物品等等分类好了,配合上手机App使用流畅很多,再也不用之前那样使用SMB手动同步了。

其他

目前用了这个二手服务其硬盘加上新买的固态硬盘组了一个Qtier池作为主要存储区,家里有块旧的sata固态硬盘就把他搞成高速缓存加速了。原来的两块酷狼硬盘都是EXT4格式,但是插到QNAP上却不能识别,只好放在原来的设备上,新NAS通过Samba访问原先的设备,把文件拷贝过来。

之后把旧的硬盘插上来使用才发现,其中一个硬盘出现了坏道,数量还不少,感觉它应该命不久矣,不敢放什么东西上来了。 而另一块好的硬盘,准备把它作为备份盘,相片,笔记和其他的一些重要文件都定期备份到这个盘上面。因为硬盘数量优先,并没有组RAID还是空间优先,只把重要的文件备份但愿以后不会踩坑。

以上就是这个新NAS的初体验了,后面还要继续增加新的应用,仍然需要摸索,外网访问仍然沿用家里的DDNS和端口转发。目前才用了不到一个星期,还有很多东西没有用到没有涉及,后面有新的体验来再继续写文章分享,也欢迎玩NAS网友一起交流分享,如果有好玩的应用也欢迎评论推荐给我。

看完评论一下吧

Android源码分析:广播接收器注册与发送广播流程解析

2024年10月17日 19:40

广播,顾名思义就是把一个信息传播出去,在Android中也提供了广播和广播接收器BroadcastReceiver,用来监听特定的事件和发送特定的消息。不过广播分为全局广播和本地广播,本地广播是在Android Jetpack库中所提供,其实现也是基于Handler和消息循环机制,并且这个类Android官方也不推荐使用了。我们这里就来看看Android全局的这个广播。

应用开发者可以自己发送特定的广播,而更多场景则是接收系统发送的广播。注册广播接收器有在AndroidManifest文件中声明和使用代码注册两种方式,在应用的target sdk大于等于Android 8.0(Api Version 26)之后,系统会限制在清单文件中注册。通过清单方式注册的广播,代码中没有注册逻辑,只有PMS中读取它的逻辑,我们这里不进行分析。

注册广播接收器

首先是注册广播接收器,一般注册一个广播接收器的代码如下:

1
2
3
val br: BroadcastReceiver = MyBroadcastReceiver()
val filter = IntentFilter(ACTION_CHARGING)
activity.registerReceiver(br, filter)

使用上面的代码就能注册一个广播接收器,当手机开始充电就会收到通知,会去执行MyBroadcastReceiveronReceive方法。

那我们就从这个registerReceiver来时往里面看,因为Activity是Context的子类,这个注册的方法的实现则是在ContextImpl当中,其中最终调用的方法为registerReceiverInternal,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
private Intent registerReceiverInternal(BroadcastReceiver receiver, int userId,
 IntentFilter filter, String broadcastPermission,
 Handler scheduler, Context context, int flags) {
 IIntentReceiver rd = null;
 if (receiver != null) {
 if (mPackageInfo != null && context != null) {
 if (scheduler == null) {
 scheduler = mMainThread.getHandler();
 }
 rd = mPackageInfo.getReceiverDispatcher(
 receiver, context, scheduler,
 mMainThread.getInstrumentation(), true);
 } else {
 ...
 }
 }
 try {
 ActivityThread thread = ActivityThread.currentActivityThread();
 Instrumentation instrumentation = thread.getInstrumentation();
 if (instrumentation.isInstrumenting()
 && ((flags & Context.RECEIVER_NOT_EXPORTED) == 0)) {
 flags = flags | Context.RECEIVER_EXPORTED;
 }
 final Intent intent = ActivityManager.getService().registerReceiverWithFeature(
 mMainThread.getApplicationThread(), mBasePackageName, getAttributionTag(),
 AppOpsManager.toReceiverId(receiver), rd, filter, broadcastPermission, userId,
 flags);
 if (intent != null) {
 intent.setExtrasClassLoader(getClassLoader());
 intent.prepareToEnterProcess(
 ActivityThread.isProtectedBroadcast(intent),
 getAttributionSource());
 }
 return intent;
 } catch (RemoteException e) {
 ...
 }
}

我们在注册广播的时候只传了两个参数,但是实际上它还可以传不少的参数,这里userId就是注册的用户id,会被自动 填充成当前进程的用户Id,broadcastPermission表示这个广播的权限,也就是说需要有该权限的应用发送的广播,这个接收者才能接收到。scheduler就是一个Handler,默认不传,在第8行可以看到,会拿当前进程的主线程的Handlerflag是广播的参数,这里比较重要的就是RECEIVER_NOT_EXPORTED,添加了它则广播不会公开暴露,其他应用发送的消息不会被接收。

在第10行,这里创建了一个广播的分发器,在24行,通过AMS去注册广播接收器,只有我们的broadcast会用到contentprovider或者有sticky广播的时候,30行才会执行到,这里跳过。

获取广播分发器

首先来看如何获取广播分发器,这块的代码在LoadedApk.java中,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public IIntentReceiver getReceiverDispatcher(BroadcastReceiver r,
 Context context, Handler handler,
 Instrumentation instrumentation, boolean registered) {
 synchronized (mReceivers) {
 LoadedApk.ReceiverDispatcher rd = null;
 ArrayMap<BroadcastReceiver, LoadedApk.ReceiverDispatcher> map = null;
 if (registered) {
 map = mReceivers.get(context);
 if (map != null) {
 rd = map.get(r);
 }
 }
 if (rd == null) {
 rd = new ReceiverDispatcher(r, context, handler,
 instrumentation, registered);
 if (registered) {
 if (map == null) {
 map = new ArrayMap<BroadcastReceiver, LoadedApk.ReceiverDispatcher>();
 mReceivers.put(context, map);
 }
 map.put(r, rd);
 }
 } else {
 rd.validate(context, handler);
 }
 rd.mForgotten = false;
 return rd.getIIntentReceiver();
 }
}

先来说一下mReceivers,它的结构为ArrayMap<Context, ArrayMap<BroadcastReceiver, ReceiverDispatcher>>,也就是嵌套了两层的ArrayMap,外层是以Context为key,内层以Receiver为key,实际存储的为ReceiverDispatcherReceiverDispatcher内部所放的IIntentReceiver比较重要,也就是我们这个方法所返回的值,它实际是IIntentReceiver.Stub,也就是它的Binder实体类。

这段代码的逻辑也比较清晰,就是根据ContextReceiver到map中去查找看是否之前注册过,如果注册过就已经有这个Dispatcher了,如果没有就创建一个,并且放到map中去,最后返回binder对象出去。

AMS注册广播接收器

在AMS注册的代码很长,我们这里主要研究正常的普通广播注册,关于黏性广播,instantApp的广播,以及广播是否导出等方面都省略不予研究。以下为我们关注的核心代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public Intent registerReceiverWithFeature(IApplicationThread caller, String callerPackage,
 String callerFeatureId, String receiverId, IIntentReceiver receiver,
 IntentFilter filter, String permission, int userId, int flags) {
 ...
 synchronized(this) {
 ReceiverList rl = mRegisteredReceivers.get(receiver.asBinder());
 if (rl == null) {
 rl = new ReceiverList(this, callerApp, callingPid, callingUid,
 userId, receiver);
 if (rl.app != null) {
 final int totalReceiversForApp = rl.app.mReceivers.numberOfReceivers();
 if (totalReceiversForApp >= MAX_RECEIVERS_ALLOWED_PER_APP) {
 throw new IllegalStateException("Too many receivers, total of "
 + totalReceiversForApp + ", registered for pid: "
 + rl.pid + ", callerPackage: " + callerPackage);
 }
 rl.app.mReceivers.addReceiver(rl);
 } else {
 try {
 receiver.asBinder().linkToDeath(rl, 0);
 } catch (RemoteException e) {
 return sticky;
 }
 rl.linkedToDeath = true;
 }
 mRegisteredReceivers.put(receiver.asBinder(), rl);
 } else {
 // 处理userId, uid,pid 等不同的错误
 }

 BroadcastFilter bf = new BroadcastFilter(filter, rl, callerPackage, callerFeatureId,
 receiverId, permission, callingUid, userId, instantApp, visibleToInstantApps,
 exported);
 if (rl.containsFilter(filter)) {
 } else {
 rl.add(bf);
 mReceiverResolver.addFilter(getPackageManagerInternal().snapshot(), bf);
 }
 }
 ...
}

在前面ContextImpl中调用AMS注册Reciever的地方,我们传的就是Receiver的Binder实体,这里拿到的是binder引用。在代码中我们可以看到,首先会以我们传过来的receiver的binder对象为key,到mRegisterReceivers当中去获取ReceiverList,这里我们就知道receiver在System_server中是怎样存储的了。如果AMS当中没有,会去创建一个ReceiverList并放置到这个map当中去,如果存在则不需要做什么事情。但是这一步只是放置了Receiver,而我们的Receiver对应的关心的IntentFilter还没使用,这里就需要继续看31行的代码了。在这里这是使用了我们传过来的IntentFilter创建了一个BroadcastFilter对象,并且把它放到了ReceiverList当中,同时还放到了mReceiverResolver当中,这个对象它不是一个Map而是一个IntentResolver,其中会存储我们的BroadcastFilter,具体这里先不分析了。 BroadcastReceiver 存放结构

到这里我们就看完了广播接收器的注册,在App进程和System_Server中分别将其存储,具体两边的数据结构如上图所示。这里可以继续看看发送广播的流程了。

发送广播

一般我们发送广播会调用如下的代码:

1
2
3
4
5
Intent().also { intent -> 
 intent.setAction("com.example.broadcast.MY_NOTIFICATION") 
 intent.putExtra("data", "Nothing to see here, move along.") 
 activity.sendBroadcast(intent)
}

我们通过设置Action来匹配对应的广播接收器,通过设置Data或者Extra,这样广播接收器中可以接收到对应的数据,最后调用sendBroadcast来发送。而sendBroadcast的实现也是在ContextImpl中,源码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Override
public void sendBroadcast(Intent intent) {
 warnIfCallingFromSystemProcess();
 String resolvedType = intent.resolveTypeIfNeeded(getContentResolver());
 try {
 intent.prepareToLeaveProcess(this);
 ActivityManager.getService().broadcastIntentWithFeature(
 mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType,
 null, Activity.RESULT_OK, null, null, null, null /*excludedPermissions=*/,
 null, AppOpsManager.OP_NONE, null, false, false, getUserId());
 } catch (RemoteException e) {
 throw e.rethrowFromSystemServer();
 }
}

这里代码比较简单,就是直接调用AMS的broadcastIntentWithFeature来发送广播。

AMS发送广播

这里我们可以直接看AMS中的broadcastIntentWithFeature的源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Override
public final int broadcastIntentWithFeature(IApplicationThread caller, String callingFeatureId,
 Intent intent, String resolvedType, IIntentReceiver resultTo,
 int resultCode, String resultData, Bundle resultExtras,
 String[] requiredPermissions, String[] excludedPermissions,
 String[] excludedPackages, int appOp, Bundle bOptions,
 boolean serialized, boolean sticky, int userId) {
 enforceNotIsolatedCaller("broadcastIntent");
 synchronized(this) {
 intent = verifyBroadcastLocked(intent);

 final ProcessRecord callerApp = getRecordForAppLOSP(caller);
 final int callingPid = Binder.getCallingPid();
 final int callingUid = Binder.getCallingUid();

 final long origId = Binder.clearCallingIdentity();
 try {
 return broadcastIntentLocked(callerApp,
 callerApp != null ? callerApp.info.packageName : null, callingFeatureId,
 intent, resolvedType, resultTo, resultCode, resultData, resultExtras,
 requiredPermissions, excludedPermissions, excludedPackages, appOp, bOptions,
 serialized, sticky, callingPid, callingUid, callingUid, callingPid, userId);
 } finally {
 Binder.restoreCallingIdentity(origId);
 }
 }
}

第10行代码,主要验证Intent,比如检查它的Flag,检查它是否传文件描述符之类的,里面的代码比较简单清晰,这里不单独看了。后面则是获取调用者的进程,uid,pid之类的,最后调用broadcastIntentLocked,这个方法的代码巨多,接近1000行代码,我们同样忽略sticky的广播,也忽略顺序广播,然后来一点一点的看:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
//ActivityManagerService.java 
//final int broadcastIntentLocked(...)
intent = new Intent(intent);
intent.addFlags(Intent.FLAG_EXCLUDE_STOPPED_PACKAGES);
if (!mProcessesReady && (intent.getFlags()&Intent.FLAG_RECEIVER_BOOT_UPGRADE) == 0) {
 intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
}
userId = mUserController.handleIncomingUser(callingPid, callingUid, userId, true,
 ALLOW_NON_FULL, "broadcast", callerPackage);
final String action = intent.getAction();

首先这里的代码是对Intent做一下封装,并且如果系统还在启动,不允许启动应用进程,以及获取当前的用户ID,大部分情况下,我们只需要考虑一个用户的情况。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
if (action != null) {
 ...
 switch (action) {
 ...
 case Intent.ACTION_PACKAGE_DATA_CLEARED:
 {
 Uri data = intent.getData();
 String ssp;
 if (data != null && (ssp = data.getSchemeSpecificPart()) != null) {
 mAtmInternal.onPackageDataCleared(ssp, userId);
 }
 break;
 }
 case Intent.ACTION_TIMEZONE_CHANGED:
 mHandler.sendEmptyMessage(UPDATE_TIME_ZONE);
 break;
 ...
 }
}

对于一些系统的广播事件,除了要发送广播给应用之外,在AMS中,还会根据其广播,来调用相关的服务或者执行相关的逻辑,也会在这里调用其代码。这里我罗列了清除应用数据和时区变化两个广播,其他的感兴趣的可以自行阅读相关代码。

1
2
3
4
5
6
int[] users;
if (userId == UserHandle.USER_ALL) {
 users = mUserController.getStartedUserArray();
} else {
 users = new int[] {userId};
}

以上代码为根据前面拿到的userId,来决定广播要发送给所有人还是仅仅发送给当前用户,并且把userId保存到users数组当中。

获取广播接收者

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
List receivers = null;
List<BroadcastFilter> registeredReceivers = null;
if ((intent.getFlags() & Intent.FLAG_RECEIVER_REGISTERED_ONLY) == 0) {
 receivers = collectReceiverComponents(
 intent, resolvedType, callingUid, users, broadcastAllowList);
}
if (intent.getComponent() == null) {
 final PackageDataSnapshot snapshot = getPackageManagerInternal().snapshot();
 if (userId == UserHandle.USER_ALL && callingUid == SHELL_UID) {
 ...
 } else {
 registeredReceivers = mReceiverResolver.queryIntent(snapshot, intent,
 resolvedType, false /*defaultOnly*/, userId);
 }
}

以上为获取我们注册的所有的接收器的代码,其中FLAG_RECEIVER_REGISTERED_ONLY意味着仅仅接收注册过的广播,前面在判断当前系统还未启动完成的时候有添加这个FLAG,其他情况一般不会有这个Flag,这里我们按照没有这个flag处理。那也就会执行第4行的代码。另外下面还有从mReceiverResolver从获取注册的接收器的代码,因为大部分情况不是从shell中执行的,因此也忽略了其代码。

首先看collectReceiverComponents的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
private List<ResolveInfo> collectReceiverComponents(Intent intent, String resolvedType,
 int callingUid, int[] users, int[] broadcastAllowList) {
 int pmFlags = STOCK_PM_FLAGS | MATCH_DEBUG_TRIAGED_MISSING;

 List<ResolveInfo> receivers = null;
 HashSet<ComponentName> singleUserReceivers = null;
 boolean scannedFirstReceivers = false;
 for (int user : users) {
 List<ResolveInfo> newReceivers = mPackageManagerInt.queryIntentReceivers(
 intent, resolvedType, pmFlags, callingUid, user, true /* forSend */); //通过PMS,根据intent和uid读取Manifest中注册的接收器
 if (user != UserHandle.USER_SYSTEM && newReceivers != null) {
 for (int i = 0; i < newReceivers.size(); i++) {
 ResolveInfo ri = newReceivers.get(i);
 //如果调用不是系统用户,移除只允许系统用户接收的接收器
 if ((ri.activityInfo.flags & ActivityInfo.FLAG_SYSTEM_USER_ONLY) != 0) {
 newReceivers.remove(i);
 i--;
 }
 }
 }
 // 把别名替换成真实的接收器 
 if (newReceivers != null) {
 for (int i = newReceivers.size() - 1; i >= 0; i--) {
 final ResolveInfo ri = newReceivers.get(i);
 final Resolution<ResolveInfo> resolution =
 mComponentAliasResolver.resolveReceiver(intent, ri, resolvedType,
 pmFlags, user, callingUid, true /* forSend */);
 if (resolution == null) {
 // 未找到对应的接收器,删除这个记录 
 newReceivers.remove(i);
 continue;
 }
 if (resolution.isAlias()) {
 //找到对应的真实的接收器,就把别名的记录替换成真实的目标
 newReceivers.set(i, resolution.getTarget());
 }
 }
 }
 if (newReceivers != null && newReceivers.size() == 0) {
 newReceivers = null;
 }

 if (receivers == null) {
 receivers = newReceivers;
 } else if (newReceivers != null) {
 if (!scannedFirstReceivers) {
 //查找单用户记录的接收器,并且保存
 scannedFirstReceivers = true;
 for (int i = 0; i < receivers.size(); i++) {
 ResolveInfo ri = receivers.get(i);
 if ((ri.activityInfo.flags&ActivityInfo.FLAG_SINGLE_USER) != 0) {
 ComponentName cn = new ComponentName(
 ri.activityInfo.packageName, ri.activityInfo.name);
 if (singleUserReceivers == null) {
 singleUserReceivers = new HashSet<ComponentName>();
 }
 singleUserReceivers.add(cn);
 }
 }
 }
 for (int i = 0; i < newReceivers.size(); i++) {
 ResolveInfo ri = newReceivers.get(i);
 if ((ri.activityInfo.flags & ActivityInfo.FLAG_SINGLE_USER) != 0) {
 ComponentName cn = new ComponentName(
 ri.activityInfo.packageName, ri.activityInfo.name);
 if (singleUserReceivers == null) {
 singleUserReceivers = new HashSet<ComponentName>();
 }
 if (!singleUserReceivers.contains(cn)) {
 //对于单用户的接收器,只存一次到返回结果中
 singleUserReceivers.add(cn);
 receivers.add(ri);
 }
 } else {
 receivers.add(ri);
 }
 }
 }
 }
 ...
 return receivers;
}

以上就根据信息通过PMS获取所有通过Manifest静态注册的广播接收器,对其有一些处理,详见上面的注释。

对于我们在代码中动态注册的接收器,则需要看mReceiverResolver.queryIntent的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
protected final List<R> queryIntent(@NonNull PackageDataSnapshot snapshot, Intent intent,
 String resolvedType, boolean defaultOnly, @UserIdInt int userId, long customFlags) {
 ArrayList<R> finalList = new ArrayList<R>();
 F[] firstTypeCut = null;
 F[] secondTypeCut = null;
 F[] thirdTypeCut = null;
 F[] schemeCut = null;

 if (resolvedType == null && scheme == null && intent.getAction() != null) {
 firstTypeCut = mActionToFilter.get(intent.getAction());
 }

 FastImmutableArraySet<String> categories = getFastIntentCategories(intent);
 Computer computer = (Computer) snapshot;
 if (firstTypeCut != null) {
 buildResolveList(computer, intent, categories, debug, defaultOnly, resolvedType,
 scheme, firstTypeCut, finalList, userId, customFlags);
 }
 sortResults(finalList); //按照IntentFilter的priority优先级降序排序
 return finalList;
}

以上代码中,这个mActionToFilter就是我们前面注册广播时候,将BroadcastFilter添加进去的一个ArrayMap,这里会根据Action去其中取出所有的BroadcastFilter,之后调用buildResolveList将其中的不符合本次广播接收要求的广播接收器给过滤掉,最后按照IntentFilter的优先级降序排列。

到这里我们就有两个列表receivers存放Manifest静态注册的将要本次广播接收者,和registeredReceivers通过代码手动注册的广播接收者。

广播入队列

首先来看通过代码注册的接收器不为空,并且不是有序广播的情况,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
int NR = registeredReceivers != null ? registeredReceivers.size() : 0;
if (!ordered && NR > 0) {
 ...
 final BroadcastQueue queue = broadcastQueueForIntent(intent);
 BroadcastRecord r = new BroadcastRecord(queue, intent, callerApp, callerPackage,
 callerFeatureId, callingPid, callingUid, callerInstantApp, resolvedType,
 requiredPermissions, excludedPermissions, excludedPackages, appOp, brOptions,
 registeredReceivers, resultTo, resultCode, resultData, resultExtras, ordered,
 sticky, false, userId, allowBackgroundActivityStarts,
 backgroundActivityStartsToken, timeoutExempt);
 ...
 final boolean replaced = replacePending
 && (queue.replaceParallelBroadcastLocked(r) != null);
 if (!replaced) {
 queue.enqueueParallelBroadcastLocked(r);
 queue.scheduleBroadcastsLocked();
 }
 registeredReceivers = null;
 NR = 0;
}

在这里,第4行会首先根据intent的flag获取对应的BroadcastQueue,这里有四个Queue,不看其代码了,不过逻辑如下:

  1. 如果有FLAG_RECEIVER_OFFLOAD_FOREGROUND 标记,则使用mFgOffloadBroadcastQueue
  2. 如果当前开启了offloadQueue,也就是mEnableOffloadQueue,并且有FLAG_RECEIVER_OFFLOAD标记,则使用mBgOffloadBroadcastQueue
  3. 如果有FLAG_RECEIVER_FOREGROUND,也就是前台时候才接收广播,则使用mFgBroadcastQueue
  4. 如果没有上述标记,则使用mBgBroadcastQueue。 拿到queue之后,会创建一条BroadcastRecord,其中会记录传入的参数,intent,以及接收的registeredReceivers,调用queue的入队方法,最后把registeredReceivers设置为null,计数也清零。具体入队的代码,我们随后再看,这里先看其他情况下的广播入队代码。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
int ir = 0;
if (receivers != null) {
 String skipPackages[] = null;
 //对于添加应用,删除应用数据之类的广播,不希望变化的应用能够接收到对应的广播
 //这里设置忽略它们
 if (Intent.ACTION_PACKAGE_ADDED.equals(intent.getAction())
 || Intent.ACTION_PACKAGE_RESTARTED.equals(intent.getAction())
 || Intent.ACTION_PACKAGE_DATA_CLEARED.equals(intent.getAction())) {
 Uri data = intent.getData();
 if (data != null) {
 String pkgName = data.getSchemeSpecificPart();
 if (pkgName != null) {
 skipPackages = new String[] { pkgName };
 }
 }
 } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(intent.getAction())) {
 skipPackages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST);
 }
 if (skipPackages != null && (skipPackages.length > 0)) {
 //如果Manifest注册的广播接收器的包名和skip的一样,那就移除它们
 for (String skipPackage : skipPackages) {
 if (skipPackage != null) {
 int NT = receivers.size();
 for (int it=0; it<NT; it++) {
 ResolveInfo curt = (ResolveInfo)receivers.get(it);
 if (curt.activityInfo.packageName.equals(skipPackage)) {
 receivers.remove(it);
 it--;
 NT--;
 }
 }
 }
 }
 }

 int NT = receivers != null ? receivers.size() : 0;
 int it = 0;
 ResolveInfo curt = null;
 BroadcastFilter curr = null;
 while (it < NT && ir < NR) {
 if (curt == null) {
 curt = (ResolveInfo)receivers.get(it);
 }
 if (curr == null) {
 curr = registeredReceivers.get(ir);
 }
 if (curr.getPriority() >= curt.priority) {
 //如果动态注册的广播优先级比静态注册的等级高,就把它添加到静态注册的前面。
 receivers.add(it, curr);
 ir++;
 curr = null;
 it++;
 NT++;
 } else {
 // 如果动态注册的广播优先级没有静态注册的等级高,那就移动静态注册的游标,下一轮在执行相关的判断。
 it++;
 curt = null;
 }
 }
}
while (ir < NR) { //如果registeredReceivers中的元素没有全部放到receivers里面,就一个一个的遍历并放进去。
 if (receivers == null) {
 receivers = new ArrayList();
 }
 receivers.add(registeredReceivers.get(ir));
 ir++;
}

以上的代码所做的事情就是首先移除静态注册的广播当中需要忽略的广播接收器,随后将静态注册和动态注册的广播接收器,按照优先级合并到同一个列表当中,当然如果动态注册的前面已经入队过了,这里实际上是不会在合并的。关于合并的代码,就是经典的两列表合并的算法,具体请看代码和注释。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
if ((receivers != null && receivers.size() > 0)
 || resultTo != null) {
 BroadcastQueue queue = broadcastQueueForIntent(intent);
 BroadcastRecord r = new BroadcastRecord(queue, intent, callerApp, callerPackage,
 callerFeatureId, callingPid, callingUid, callerInstantApp, resolvedType,
 requiredPermissions, excludedPermissions, excludedPackages, appOp, brOptions,
 receivers, resultTo, resultCode, resultData, resultExtras,
 ordered, sticky, false, userId, allowBackgroundActivityStarts,
 backgroundActivityStartsToken, timeoutExempt);

 final BroadcastRecord oldRecord =
 replacePending ? queue.replaceOrderedBroadcastLocked(r) : null;
 if (oldRecord != null) {
 if (oldRecord.resultTo != null) {
 final BroadcastQueue oldQueue = broadcastQueueForIntent(oldRecord.intent);
 try {
 oldRecord.mIsReceiverAppRunning = true;
 oldQueue.performReceiveLocked(oldRecord.callerApp, oldRecord.resultTo,
 oldRecord.intent,
 Activity.RESULT_CANCELED, null, null,
 false, false, oldRecord.userId, oldRecord.callingUid, callingUid,
 SystemClock.uptimeMillis() - oldRecord.enqueueTime, 0);
 } catch (RemoteException e) {

 }
 }
 } else {
 queue.enqueueOrderedBroadcastLocked(r);
 queue.scheduleBroadcastsLocked();
 }
}else {
 //对于无人关心的广播,也做一下记录
 if (intent.getComponent() == null && intent.getPackage() == null
 && (intent.getFlags()&Intent.FLAG_RECEIVER_REGISTERED_ONLY) == 0) {
 addBroadcastStatLocked(intent.getAction(), callerPackage, 0, 0, 0);
 }
}

以上的代码,跟前面入队的代码也差不多,不过这里如果采用的方法是enqueueOrderedBroadcastLocked,并且多了关于已经发送的广播的替换的逻辑,这里我们先不关注。如果receivers为空,并且符合条件的隐式广播,系统也会对其进行记录,具体,我们这里也不进行分析了。

BroadcastQueue 入队

我们知道前面入队的时候有两个方法,分别是enqueueParallelBroadcastLockedenqueueOrderedBroadcastLocked,我们先来分析前者。

1
2
3
4
5
6
7
public void enqueueParallelBroadcastLocked(BroadcastRecord r) {
 r.enqueueClockTime = System.currentTimeMillis();
 r.enqueueTime = SystemClock.uptimeMillis();
 r.enqueueRealTime = SystemClock.elapsedRealtime();
 mParallelBroadcasts.add(r);
 enqueueBroadcastHelper(r);
}

这里就是将BroadcastRecord放到mParallelBroadcasts列表中,随后执行enqueueBroadcastHelper,我们先看继续看一下enqueueOrderedBroadcastLocked方法。

1
2
3
4
5
6
7
public void enqueueOrderedBroadcastLocked(BroadcastRecord r) {
 r.enqueueClockTime = System.currentTimeMillis();
 r.enqueueTime = SystemClock.uptimeMillis();
 r.enqueueRealTime = SystemClock.elapsedRealtime();
 mDispatcher.enqueueOrderedBroadcastLocked(r);
 enqueueBroadcastHelper(r);
}

这里跟上面很类似,差别是这里把BroadcastRecord入队了mDispatcher,对于普通广播,其内部是把这个记录放到了mOrderedBroadcasts列表。 而enqueueBroadcastHelper方法仅仅用于trace,我们这里不需要关注。

到了这里,我们把广播放到对应的列表了,但是广播还是没有分发出去。

AMS端广播的分发

以上是代码入了BroadcastQueu,接下来就可以看看队列中如何处理它了。首先需要注意一下,记录在入队的同时还调用了BroadcastQueuescheduleBroadcastsLock方法,代码如下:

1
2
3
4
5
6
7
public void scheduleBroadcastsLocked() {
 if (mBroadcastsScheduled) {
 return;
 }
 mHandler.sendMessage(mHandler.obtainMessage(BROADCAST_INTENT_MSG, this));
 mBroadcastsScheduled = true;
}

这里使用了Handler发送了一条BROADCAST_INTENT_MSG消息,我们可以去看一下BroadcastHandlerhandleMessage方法。其中在处理这个消息的时候调用了processNextBroadcast方法,我们可以直接去看其实现:

1
2
3
4
5
private void processNextBroadcast(boolean fromMsg) {
 synchronized (mService) {
 processNextBroadcastLocked(fromMsg, false);
 }
}

这里开启了同步块调用了processNextBroadcastLocked方法,这个方法依然很长,其中涉及到广播的权限判断,对于静态注册的广播,可能还涉及到对应进程的启动等。

动态广播的分发

动态注册的无序广播相对比较简单,这里我们仅仅看一下其中无序广播的分发处理:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
if (fromMsg) {
 mBroadcastsScheduled = false; //通过handleMessage过来,把flag设置为false
}
while (mParallelBroadcasts.size() > 0) {
 r = mParallelBroadcasts.remove(0);
 r.dispatchTime = SystemClock.uptimeMillis();
 r.dispatchRealTime = SystemClock.elapsedRealtime();
 r.dispatchClockTime = System.currentTimeMillis();
 r.mIsReceiverAppRunning = true;
 final int N = r.receivers.size();

 for (int i=0; i<N; i++) {
 Object target = r.receivers.get(i);

 deliverToRegisteredReceiverLocked(r,
 (BroadcastFilter) target, false, i); //分发
 }
 addBroadcastToHistoryLocked(r); //把广播添加的历史记录中
}


这里就是遍历`ParallelBroadcasts`中的每一条`BroadcastRecord`,其中会再分别遍历每一个`BroadcastFilter`,调用`deliverToRegisteredReceiverLocked`来分发广播
```java
private void deliverToRegisteredReceiverLocked(BroadcastRecord r,
 BroadcastFilter filter, boolean ordered, int index) {
 boolean skip = false;
 ...

 if (filter.requiredPermission != null) {
 int perm = mService.checkComponentPermission(filter.requiredPermission,
 r.callingPid, r.callingUid, -1, true);
 if (perm != PackageManager.PERMISSION_GRANTED) {
 skip = true;
 } else {
 final int opCode = AppOpsManager.permissionToOpCode(filter.requiredPermission);
 if (opCode != AppOpsManager.OP_NONE
 && mService.getAppOpsManager().noteOpNoThrow(opCode, r.callingUid,
 r.callerPackage, r.callerFeatureId, "Broadcast sent to protected receiver")
 != AppOpsManager.MODE_ALLOWED) {
 skip = true;
 }
 }
 }
 ...
 if (skip) {
 r.delivery[index] = BroadcastRecord.DELIVERY_SKIPPED;
 return;
 }

 r.delivery[index] = BroadcastRecord.DELIVERY_DELIVERED;
 ...
 try {

 if (filter.receiverList.app != null && filter.receiverList.app.isInFullBackup()) {
 if (ordered) {
 skipReceiverLocked(r);
 }
 } else {
 r.receiverTime = SystemClock.uptimeMillis();
 maybeAddAllowBackgroundActivityStartsToken(filter.receiverList.app, r);
 maybeScheduleTempAllowlistLocked(filter.owningUid, r, r.options);
 maybeReportBroadcastDispatchedEventLocked(r, filter.owningUid);
 performReceiveLocked(filter.receiverList.app, filter.receiverList.receiver,
 new Intent(r.intent), r.resultCode, r.resultData,
 r.resultExtras, r.ordered, r.initialSticky, r.userId,
 filter.receiverList.uid, r.callingUid,
 r.dispatchTime - r.enqueueTime,
 r.receiverTime - r.dispatchTime);
 if (filter.receiverList.app != null
 && r.allowBackgroundActivityStarts && !r.ordered) {
 postActivityStartTokenRemoval(filter.receiverList.app, r);
 }
 }
 if (ordered) {
 r.state = BroadcastRecord.CALL_DONE_RECEIVE;
 }
 } catch (RemoteException e) {
 ...
 if (ordered) {
 r.receiver = null;
 r.curFilter = null;
 filter.receiverList.curBroadcast = null;
 }
 }
}

在这个方法中有大段的代码是判断是否需要跳过当前这个广播,我这里仅仅保留了几句权限检查的代码。对于跳过的记录会将其BroadcastRecorddelivery[index]值设置为DELIVERY_SKIPPED, 而成功分发的会设置为DELIVERY_DELIVERED。对于有序广播的分发我们这里也不予分析,直接看无序广播的分发,在分发之前会尝试给对应的接收进程添加后台启动Activity的权限,这个会在分发完成之后恢复原状,调用的是maybeAddAllowBackgroundActivityStartsToken,就不具体分析了。

之后会调用performReceiveLocked去进行真正的分发,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void performReceiveLocked(ProcessRecord app, IIntentReceiver receiver,
 Intent intent, int resultCode, String data, Bundle extras,
 boolean ordered, boolean sticky, int sendingUser,
 int receiverUid, int callingUid, long dispatchDelay,
 long receiveDelay) throws RemoteException {
 if (app != null) {
 final IApplicationThread thread = app.getThread();
 if (thread != null) {
 try {
 thread.scheduleRegisteredReceiver(receiver, intent, resultCode,
 data, extras, ordered, sticky, sendingUser,
 app.mState.getReportedProcState());
 } catch (RemoteException ex) {
 ...
 throw ex;
 }
 } else {
 ...
 throw new RemoteException("app.thread must not be null");
 }
 } else {
 receiver.performReceive(intent, resultCode, data, extras, ordered,
 sticky, sendingUser);
 }
 ...
}

在执行分发的代码中,如果我们的ProcessRecord不为空,并且ApplicationThread也存在的情况下,会调用它的scheduleRegisterReceiver方法。如果进程记录为空,则会直接使用IIntentReceiverperformReceiver方法。我们在App中动态注册的情况,ProcessRecord一定是不为空的,我们也以这种情况继续向下分析。

动态注册广播分发App进程逻辑

1
2
3
4
5
6
7
public void scheduleRegisteredReceiver(IIntentReceiver receiver, Intent intent,
 int resultCode, String dataStr, Bundle extras, boolean ordered,
 boolean sticky, int sendingUser, int processState) throws RemoteException {
 updateProcessState(processState, false);
 receiver.performReceive(intent, resultCode, dataStr, extras, ordered,
 sticky, sendingUser);
}

在应用进程中,首先也只是根据AMS传过来的processState更新一下进程的状态,随后还是调用了IIntentReceiverperformReceive方法,performReceiveLoadedApk当中,为内部类InnerReceiver的方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void performReceive(Intent intent, int resultCode, String data,
 Bundle extras, boolean ordered, boolean sticky, int sendingUser) {
 final LoadedApk.ReceiverDispatcher rd;
 if (intent == null) {
 rd = null;
 } else {
 rd = mDispatcher.get(); //获取ReceiverDispatcher
 }
 if (rd != null) {
 rd.performReceive(intent, resultCode, data, extras,
 ordered, sticky, sendingUser);
 } else {
 IActivityManager mgr = ActivityManager.getService();
 try {
 if (extras != null) {
 extras.setAllowFds(false);
 }
 mgr.finishReceiver(this, resultCode, data, extras, false, intent.getFlags());
 } catch (RemoteException e) {
 throw e.rethrowFromSystemServer();
 }
 }
}

在应用进程中,首先会获取ReceiverDisptcher,这个一般不会为空。但是系统代码比较严谨,也考虑了,不存在的情况会调用AMS的finishReceiver完成整个流程。

对于存在的情况,会调用ReceiverDispatcherperformReceive方法继续分发。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public void performReceive(Intent intent, int resultCode, String data,
 Bundle extras, boolean ordered, boolean sticky, int sendingUser) {
 final Args args = new Args(intent, resultCode, data, extras, ordered,
 sticky, sendingUser);
 ..
 if (intent == null || !mActivityThread.post(args.getRunnable())) {
 if (mRegistered && ordered) {
 IActivityManager mgr = ActivityManager.getService();
 ..
 args.sendFinished(mgr);
 }
 }
}

这里的代码有点绕,不过也还比较清晰,首先是创建了一个Args对象,之后根据java的语法,如果intent不为空的时候会执行如下代码:

1
mActivityThread.post(args.getRunnable())

当这个执行失败的时候,才会看情况执行8行到第10行的代码。而这个Runnable就是应用端真正分发的逻辑,其代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public final Runnable getRunnable() {
 return () -> {
 final BroadcastReceiver receiver = mReceiver;
 final boolean ordered = mOrdered;


 final IActivityManager mgr = ActivityManager.getService();
 final Intent intent = mCurIntent;

 mCurIntent = null;
 mDispatched = true;
 mRunCalled = true;
 if (receiver == null || intent == null || mForgotten) {
 ...
 return;
 }
 try {
 ClassLoader cl = mReceiver.getClass().getClassLoader();
 intent.setExtrasClassLoader(cl);
 intent.prepareToEnterProcess(ActivityThread.isProtectedBroadcast(intent),
 mContext.getAttributionSource());
 setExtrasClassLoader(cl);
 receiver.setPendingResult(this);
 receiver.onReceive(mContext, intent);
 } catch (Exception e) {
 if (mRegistered && ordered) {
 sendFinished(mgr);
 }
 if (mInstrumentation == null ||
 !mInstrumentation.onException(mReceiver, e)) {
 throw new RuntimeException(
 "Error receiving broadcast " + intent
 + " in " + mReceiver, e);
 }
 }

 if (receiver.getPendingResult() != null) {
 finish();
 }
 };
}

这里的receiver就是我们注册时候的那个BroadcastReceiver,这里将当前的Args对象作为它的PendingResult,在这里调用了它的onReceive方法 ,最后看pendingResult是否为空,不为空则调用PendingResultfinish()方法。当我们在onReceive中编写代码的时候,如果调用了goAsync的话,那这里的PendingResult就会为空。

另外就是我们这个Runnable是使用的mActivityThread的post方法投递出去的,它是一个Handler对象,它是在注册广播接收器的时候指定的,默认是应用的主线程Handler,也就是说广播的执行会在主线程。

但是即使是我们使用goAsync的话,处理完成之后也是需要手动调用finish的,我们后面在来看相关的逻辑。

静态广播的发送

在前面分析的BroadcastQueueprocessNextBroadcastLocked方法中,我们只分析了动态广播的发送,这里再看一下静态广播的发送,首先仍然是看processNextBroadcastLocked中的相关源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
BroadcastRecord r;
do {
 r = mDispatcher.getNextBroadcastLocked(now);
 if (r == null) {
 ...
 return;
 }
 ...

} while(r === null);
...
if (app != null && app.getThread() != null && !app.isKilled()) {
 try {
 app.addPackage(info.activityInfo.packageName,
 info.activityInfo.applicationInfo.longVersionCode, mService.mProcessStats);
 maybeAddAllowBackgroundActivityStartsToken(app, r);
 r.mIsReceiverAppRunning = true;
 processCurBroadcastLocked(r, app);
 return;
 } catch(RemoteException e) {
 ...
 }
}
...

在第3行,会从mDispatcher中拿BroadcastRecord的记录,我们之前在AMS端入队的代码,对于静态注册的广播和有序广播都是放在mDispatcher当中的,这里拿到动态注册的有序广播也会从这里拿,它的后续逻辑跟前面分析的是一样的,这里不再看了。对于静态注册的广播,在调用后续的方法之前,需要先获取对应进程的ProcessRecord,和ApplicationThread,并且进行广播权限的检查,进程是否存活检查这些在我们11行的位置,都省略不看了。如果App进程存活则会走到我们12行的部分,否则会去创建对应的进程,创建完进程会再去分发广播。

动态注册的广播,会传一个IIntentReceiver的Binder到AMS,而静态注册的广播,我们跟着第18行代码processCurBroadcastLocked方法进去一览究竟:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private final void processCurBroadcastLocked(BroadcastRecord r,
 ProcessRecord app) throws RemoteException {
 final IApplicationThread thread = app.getThread();
 ...
 r.receiver = thread.asBinder();
 r.curApp = app;
 final ProcessReceiverRecord prr = app.mReceivers;
 prr.addCurReceiver(r);
 app.mState.forceProcessStateUpTo(ActivityManager.PROCESS_STATE_RECEIVER);
 ...
 r.intent.setComponent(r.curComponent);

 boolean started = false;
 try {
 mService.notifyPackageUse(r.intent.getComponent().getPackageName(),
 PackageManager.NOTIFY_PACKAGE_USE_BROADCAST_RECEIVER);
 thread.scheduleReceiver(new Intent(r.intent), r.curReceiver,
 mService.compatibilityInfoForPackage(r.curReceiver.applicationInfo),
 r.resultCode, r.resultData, r.resultExtras, r.ordered, r.userId,
 app.mState.getReportedProcState());
 started = true;
 } finally {
 if (!started) {
 r.receiver = null;
 r.curApp = null;
 prr.removeCurReceiver(r);
 }
 }

}

在这个方法中,把App的ProcessRecord放到了BroadcastRecord当中,并且把ApplicationThread设置为receiver,最后是调用了ApplicationThreadscheduleReceiver,从而通过binder调用App进程。

静态注册广播分发App进程逻辑

通过Binder调用,在App的ApplicationThread代码中,调用的是如下方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public final void scheduleReceiver(Intent intent, ActivityInfo info,
 CompatibilityInfo compatInfo, int resultCode, String data, Bundle extras,
 boolean sync, int sendingUser, int processState) {
 updateProcessState(processState, false);
 ReceiverData r = new ReceiverData(intent, resultCode, data, extras,
 sync, false, mAppThread.asBinder(), sendingUser);
 r.info = info;
 r.compatInfo = compatInfo;
 sendMessage(H.RECEIVER, r);
}

这里是创建了一个ReceiverData把AMS传过来数据包裹其中,并且通过消息发出去,之后会调用ActivityThreadhandleReceiver方法, 代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
private void handleReceiver(ReceiverData data) {
 String component = data.intent.getComponent().getClassName();

 LoadedApk packageInfo = getPackageInfoNoCheck(
 data.info.applicationInfo, data.compatInfo);

 IActivityManager mgr = ActivityManager.getService();

 Application app;
 BroadcastReceiver receiver;
 ContextImpl context;
 try {
 app = packageInfo.makeApplicationInner(false, mInstrumentation);
 context = (ContextImpl) app.getBaseContext();
 if (data.info.splitName != null) {
 context = (ContextImpl) context.createContextForSplit(data.info.splitName);
 }
 if (data.info.attributionTags != null && data.info.attributionTags.length > 0) {
 final String attributionTag = data.info.attributionTags[0];
 context = (ContextImpl) context.createAttributionContext(attributionTag);
 }
 java.lang.ClassLoader cl = context.getClassLoader();
 data.intent.setExtrasClassLoader(cl);
 data.intent.prepareToEnterProcess(
 isProtectedComponent(data.info) || isProtectedBroadcast(data.intent),
 context.getAttributionSource());
 data.setExtrasClassLoader(cl);
 receiver = packageInfo.getAppFactory()
 .instantiateReceiver(cl, data.info.name, data.intent);
 } catch (Exception e) {
 data.sendFinished(mgr);
 ...
 }

 try {

 sCurrentBroadcastIntent.set(data.intent);
 receiver.setPendingResult(data);
 receiver.onReceive(context.getReceiverRestrictedContext(),
 data.intent);
 } catch (Exception e) {
 data.sendFinished(mgr);
 } finally {
 sCurrentBroadcastIntent.set(null);
 }

 if (receiver.getPendingResult() != null) {
 data.finish();
 }
}

这个代码中主要有两个try-catch的代码块,分别是两个主要的功能区。因为静态注册的广播,我们的广播接收器是没有构建的,AMS传过来的只是广播的类名,因此,第一块代码的功能就是创建广播接收器对象。第二块代码则是去调用广播接收器的onReceive方法,从而传递广播。另外这里会调用PendingResultfinish去执行广播处理完成之后的逻辑,以及告知AMS,不过这里的PendingResult就是前面创建的ReceiverData

完成广播的发送

在分析前面的动态注册广播分发和静态注册广播分发的时候,最终在App进程它们都有一个Data,静态为ReceiverData, 动态为Args,他们都继承了PendingResult,最终都会调用PendingResultfinish方法来完成后面的收尾工作,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public final void finish() {
 if (mType == TYPE_COMPONENT) {
 final IActivityManager mgr = ActivityManager.getService();
 if (QueuedWork.hasPendingWork()) {
 QueuedWork.queue(new Runnable() {
 @Override public void run() {
 sendFinished(mgr);
 }
 }, false);
 } else {
 sendFinished(mgr);
 }
 } else if (mOrderedHint && mType != TYPE_UNREGISTERED) {
 final IActivityManager mgr = ActivityManager.getService();
 sendFinished(mgr);
 }
}

这里的QueuedWork主要用于运行SharedPreferences写入数据到磁盘,当然这个如果其中有未运行的task则会添加一个Task到其中来运行sendFinished,这样做的目的是为了保证如果当前除了广播接收器没有别的界面或者Service运行的时候,AMS不会杀掉当前的进程。否则会直接运行sendFinished方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public void sendFinished(IActivityManager am) {
 synchronized (this) {
 if (mFinished) {
 throw new IllegalStateException("Broadcast already finished");
 }
 mFinished = true;
 try {
 if (mResultExtras != null) {
 mResultExtras.setAllowFds(false);
 }
 if (mOrderedHint) {
 am.finishReceiver(mToken, mResultCode, mResultData, mResultExtras,
 mAbortBroadcast, mFlags);
 } else {
 am.finishReceiver(mToken, 0, null, null, false, mFlags);
 }
 } catch (RemoteException ex) {
 }
 }
}

这里就是调用AMS的finishReceiver方法,来告诉AMS广播接收的处理已经执行完了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public void finishReceiver(IBinder who, int resultCode, String resultData,
 Bundle resultExtras, boolean resultAbort, int flags) {
 if (resultExtras != null && resultExtras.hasFileDescriptors()) {
 throw new IllegalArgumentException("File descriptors passed in Bundle");
 }

 final long origId = Binder.clearCallingIdentity();
 try {
 boolean doNext = false;
 BroadcastRecord r;
 BroadcastQueue queue;

 synchronized(this) {
 if (isOnFgOffloadQueue(flags)) {
 queue = mFgOffloadBroadcastQueue;
 } else if (isOnBgOffloadQueue(flags)) {
 queue = mBgOffloadBroadcastQueue;
 } else {
 queue = (flags & Intent.FLAG_RECEIVER_FOREGROUND) != 0
 ? mFgBroadcastQueue : mBgBroadcastQueue;
 }

 r = queue.getMatchingOrderedReceiver(who);
 if (r != null) {
 doNext = r.queue.finishReceiverLocked(r, resultCode,
 resultData, resultExtras, resultAbort, true);
 }
 if (doNext) {
 }
 trimApplicationsLocked(false, OomAdjuster.OOM_ADJ_REASON_FINISH_RECEIVER);
 }

 } finally {
 Binder.restoreCallingIdentity(origId);
 }
}

相关的逻辑从13行开始,首先仍然是根据广播的flag找到之前的BroadcastQueue,之后根据IBinder找到发送的这一条BroadcastRecord,调用Queue的finishReceiverLocked方法。根据它的返回值,再去处理队列中的下一个广播记录。最后的trimApplicationsLocked里面会视情况来决定是否停止App进程,我们这里就不进行分析了。

processNextBroadcastLocaked前面已经分析过了,这里只需要来看finishReceiverLocked方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public boolean finishReceiverLocked(BroadcastRecord r, int resultCode,
 String resultData, Bundle resultExtras, boolean resultAbort, boolean waitForServices) {
 final int state = r.state;
 final ActivityInfo receiver = r.curReceiver;
 final long finishTime = SystemClock.uptimeMillis();
 final long elapsed = finishTime - r.receiverTime;
 r.state = BroadcastRecord.IDLE;
 final int curIndex = r.nextReceiver - 1;
 if (curIndex >= 0 && curIndex < r.receivers.size() && r.curApp != null) {
 final Object curReceiver = r.receivers.get(curIndex);

 }
 ...

 r.receiver = null;
 r.intent.setComponent(null);
 if (r.curApp != null && r.curApp.mReceivers.hasCurReceiver(r)) {
 r.curApp.mReceivers.removeCurReceiver(r);
 mService.enqueueOomAdjTargetLocked(r.curApp);
 }
 if (r.curFilter != null) {
 r.curFilter.receiverList.curBroadcast = null;
 }
 r.curFilter = null;
 r.curReceiver = null;
 r.curApp = null;
 mPendingBroadcast = null;

 r.resultCode = resultCode;
 r.resultData = resultData;
 r.resultExtras = resultExtras;
 ....
 r.curComponent = null;

 return state == BroadcastRecord.APP_RECEIVE
 || state == BroadcastRecord.CALL_DONE_RECEIVE;
}

在这里,我们最关注的代码就是17行开是的代码,从mReceivers列表中移除BroadcastRecord,并且把ReceiverListcurBroadcast设置为空,并且其他几个参数也设置为空,这样才算完成了广播的分发和处理。

总结

以上就是广播接收器的注册,以及动态、静态广播分发的分析了。关于取消注册是跟注册相关的过程,理解了注册的逻辑,取消注册也可以很快的搞清楚。关于sticky的广播,限于篇幅先不分析了。而有序广播,它在AMS端其实和静态注册的广播是差不多,不过它在调用App进程的时候是有差别的。另外关于权限相关的逻辑,以后在权限代码的分析中可以再进行关注。

看完评论一下吧

一个小技巧,将 AI 设置为你的搜索引擎

2024年10月17日 18:55

随着人工智能的进步,越来越多的人在主动搜索信息前往往会先咨询 AI。其中,我个人偏爱具备联网能力的 AI 工具,例如 Perplexity。本文将阐述如何通过简单设置,将 AI 配置为搜索引擎,从而提升使用便利性。

一个小技巧,将 AI 设置为你的搜索引擎最先出现在Jack's Space

更优雅的RSS使用指南

2024年10月14日 21:32

最近因为Follow的爆火,RSS的内容也跟着一起火了一把。笔者最近也优化了一下自己博客的RSS输出,在这里写一下博客如何更加 优雅的输出RSS,以及在订阅RSS的时候如何更好的发现RSS源。

RSS2.0 与 ATOM

RSS是一种消息来源格式,用于方便的将一个站点的内容以一个指定的格式输出,方便订阅者聚合多个站点的内容。

目前RSS的版本为2.0,而我们大家在使用博客输出RSS文件的时候,除了常用的RSS2.0格式,目前还有一个ATOM格式,其目前的版本为1.0。Atom发展的动机为了解决RSS2.0的问题,它解决了如下问题(来源WikiPedia):

  • RSS 2.0可能包含文本或经过编码的HTML内容,同时却没有提供明确的区分办法;相比之下,Atom则提供了明确的标签(也就是typed)。
  • RSS 2.0的description标签可以包含全文或摘要(尽管该标签的英文含义为描述或摘要)。Atom则分别提供了summary和content标签,用以区分摘要和内容,同时Atom允许在summary中添加非文本内容。
  • RSS 2.0存在多种非标准形式的应用,而Atom具有统一的标准,这便于内容的聚合和发现。
  • Atom有符合XML标准的命名空间,RSS 2.0却没有。
  • Atom通过XML内置的xml:base标签来指示相对地址URI,RSS2.0则无相应的机制区分相对地址和绝对地址。
  • Atom通过XML内置的xml:lang,而RSS采用自己的language标签。
  • Atom强制为每个条目设定唯一的ID,这将便于内容的跟踪和更新。
  • Atom 1.0允许条目单独成为文档,RSS 2.0则只支持完整的种子文档,这可能产生不必要的复杂性和带宽消耗。
  • Atom按照RFC3339标准表示时间 ,而RSS2.0中没有指定统一的时间格式。
  • Atom 1.0具有在IANA注册了的MIME类型,而RSS 2.0所使用的application/rss+xml并未注册。
  • Atom 1.0标准包括一个XML schema,RSS 2.0却没有。
  • Atom是IETF组织标准化程序下的一个开放的发展中标准,RSS 2.0则不属于任何标准化组织,而且它不是开放版权的。

相比之下ATOM协议是有更多的有点,如果你RSS生成程序已经支持了Atom那肯定是优先使用Atom。不过现在基本上99%以上的Rss订阅器或者工具对于两者都有很好的支持,因此如果你现在已经使用了RSS2.0也没必要替换成Atom了。

RSS的自动发现

对于提供Rss订阅的网站,最好的方式是提供相应的连接或者使用Rss图标,告诉访客当前网站的Rss地址。

除了这样之外,我们还应该在网站的源码中添加RSS地址,这样对于一些浏览器插件或者订阅软件可以通过我们的网站页面自动发现RSS订阅地址。

对于RSS2.0的订阅地址可以添加如下代码:

1
<link rel="alternate" type="application/rss+xml" href="/feed.xml" />

对于ATOM的订阅地址可以添加如下代码:

1
<link rel="alternate" type="application/atom+xml" href="atom.xml" title="Site title" />

如果你同时提供了ATOM和RSS2.0两种订阅文件,可以上面两行代码都添加。当然现在一些博客程序的模板文件中已经添加了上面的代码,检查一下即可。

RSS输出的优化

因为我的博客是以RSS2.0格式输出的订阅文件,因此这里我就按照我的优化内容来介绍一下输出相关的优化,对于ATtom可以参考其规范文档。

首先区分介绍和全文的输出。对于只输出描述的网站只需要设置描述部分即可,对于输出了全部的博客,还是建议同时输出描述和全文的。 而RSS2.0不支持输出全文,我们可以用一下的标记来输出全文:

1
<content:encoded>全文内容</content:encoded>

其中的文章html,最好做一下转码。 (以上代码加的有问题,有的RSS识别失败,暂时回退了,有时间换Atom)

其次可以补充一下网站的内容的元数据,比如作者的信息,网站的标题简介等等。

对于文章,也可以在输出的时候输出相关的元数据,如标题,作者,标签等。标签支持设置多个,可以用如下的标记:

1
<category domain="{{ .Permalink }}">{{ .LinkTitle }}</category>

另外在我设置的过程,发现rss是提供了一个comments标记的,设置这个标记后,如果RSS阅读器对此支持,理论上可以直接从RSS阅读器点击跳转到文章的评论页面。

最后,我们可能想要检测要多少通过RSS点击跳转到我们博客的访问量,这个时候可以在输出的链接上面加上特定的参数,这样在我们的统计平台上面就可以看到有多少用户从这里打开页面的,我所加的参数如下:

?utm_source=rss

订阅RSS

目前最流行的订阅RSS的方式要属于Follow了,这里也推荐使用。

除了Follow之外,我还自建了一个FreshRss来订阅一些内容,这个的使用要更早于Follow的出现。现在还不能抛弃它的原因是Follow目前不支持移动端,我使用Android的手机,在移动推荐使用FeedMe来浏览FreshRss的订阅内容。

另外,我们在浏览一些内容或者博客的时候,也需要一个工具来帮助我们方便的查看和订阅RSS源,这个时候就要推荐一下DIYgod大佬开发的浏览器插件RSSHub-Radar,对于我们的博客,如果已经加了我前面说的html代码,它可以自己发现订阅地址,如下图所示:

它还支持配置规则,则一些拥有RSSHub订阅的站点,比如b站,微博,小红书等,可以嗅探到RSShub的订阅地址,如下图所示:

另外,看上面弹出的窗口中是可以直接去预览对应的RSS内的,还可以直接跳转到Follow、FreshRss等订阅源去添加这个订阅源,这些可以在插件的设置中进行设置,如下图所示:

除了上面的设置,这个插件还支持一些其他的设置,读者朋友可以自行探索。

总结

以上就是关于网站配置和rss订阅方面我的一些建议,而本文的标题也有一些标题党了,欢迎吐槽。

资料

如果读者需要查阅ATOM和RSS的维基百科,请查看英文版本,中文版本内容比较简略,很多发展相关的内容都没有。

看完评论一下吧

Android源码分析:再读消息循环源码

2024年10月10日 21:17

Android消息循环在应用开发中会经常涉及,我以前也分析过。不过那个时候分析的还是以很老的Android源码来进行的,并且只是分析了Java层的代码,当时的文章为:Android消息循环分析。而Native层,以及一些新增的功能,都没有涉及,今天再读源码,对其进行再次分析。

消息循环简化版本

对于应用层的开发者来说,虽然已经过了10年,java层的Api还是跟之前一样的,依然是通过Handler发送消息,Looper会中消息队列中取消息,消息会根据Handler中的callback或者消息自己的callback执,如上图所示。我之前分析的发送消息和处理消息已经比较清楚了,这块不再看了。这里主要分析一下从MessageQueue取消息,之前涉及的文件描述符的监控和Native层的一些实现等进行分析。

java层loop取消息

首先来看java层如何从消息队列取消息的,Looper中有如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public static void loop() {
 final Looper me = myLooper();
 ...
 me.mInLoop = true;
 Binder.clearCallingIdentity();
 final long ident = Binder.clearCallingIdentity();
 ...
 for (;;) {
 if (!loopOnce(me, ident, thresholdOverride)) {
 return;
 }
 }
}

以上代码核心就是拿到当前线程的Looper然后,在无限循环当中取调用loopOnceloopOnce代码很长,但是忽略错误处理和Log,核心代码如下:

1
2
3
4
5
6
7
8
9
private static boolean loopOnce(final Looper me,
 final long ident, final int thresholdOverride) {
 Message msg = me.mQueue.next(); //从消息队列中取消息
 ...
 msg.target.dispatchMessage(msg); //分发消息
 ...
 msg.recycleUnchecked(); //回收消息,方便下一次发送消息使用
 return true;
}

loopOnce中主要就是去通过MessageQueue取消息,之后在分发消息,并且回收消息。再来看MessageQueuenext方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
Message next() {
 final long ptr = mPtr;
 ...
 int nextPollTimeoutMillis = 0;
 for (;;) {
 nativePollOnce(ptr, nextPollTimeoutMillis);
 synchronized (this) {
 Message prevMsg = null;
 Message msg = mMessages;
 if (msg != null && msg.target == null) {
 do {
 prevMsg = msg;
 msg = msg.next;
 } while (msg != null && !msg.isAsynchronous());
 }
 if (msg != null) {
 if (now < msg.when) {
 nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
 } else {
 mBlock = false;
 if (preMsg != null) {
 prevMsg.next = msg.next;
 } else {
 mMessages = msg.next;
 }
 msg.next = null;
 msg.markInUse();
 return msg;
 }
 } else {
 nextPollTimeoutMillis = -1;
 }
 ...
 }
 ...
 }
}

以上为next方法的简化,在Java层的MessageQueue的实现就是一个链表,因此向其中发送消息或者取消息的过程就是链表添加或者删除的过程。在第21行到第26行就是从链表中删除msg的过程。其中这个链表它的头节点是存放在mMessages这个变量,Message在插入链表的时候,也是按照事件先后运行放到链表当中的。

在这个方法的开头,我们看到mPtr,它就是MessageQueue在native层对应的对象,不过Native的Message和Java层的Message是相互独立的,在读取next的时候,也会通过nativePollOnce来native层来读取一个消息,另外在这里还传了一个nextPollTimeoutMillis,用来告诉native需要等待的时间,具体后面在来具体分析相关代码。

因为我们的消息循环中除了放置我们通过Handler所发送的消息之外,还会存在同步信号的屏障,比如ViewRootImpl就会在每一次scheduleTraversals的时候发送一个屏障消息。屏障消息和普通消息的区别就是没有targetHandler。因此在第10行,当我们检查到是屏障消息的时候,会跳过它, 并且查找它之后的第一条异步消息。 另外就是在这个do-while的循环条件中,我们可以看到它还有判断消息是否为Asynchronous的,我们正常创建的Handler一般async都是false,也就是说消息的这个值也是为false。而异步的,一般会被IMS,WMS,Display,动画等系统组件使用,应用开发者无法使用。

这里我们只要知道,如果有异步消息,就会先执行异步消息。在第17行,这里还会判断消息的事件,如果消息的when比当前事件大的化,那么这个消息还不能够执行,这时候需要去等待,这里就会给nextPollTimeoutMillis去赋值。

Native层的MessageQueue和Looper

我们刚刚看MessageQueue的代码时候,看到mPtr,它对应native层的MessageQueue的指针。它的初始化在MessageQueue的构造方法中,也就是调用nativeInit,其内部源码为调用NativeMessageQueue的构造方法,源码在android_os_MessageQueue.cpp中:

1
2
3
4
5
6
7
8
NativeMessageQueue::NativeMessageQueue() :
 mPollEnv(NULL), mPollObj(NULL), mExceptionObj(NULL) {
 mLooper = Looper::getForThread();
 if (mLooper == NULL) {
 mLooper = new Looper(false);
 Looper::setForThread(mLooper);
 }
}

这里我们可以看到在Native层,创建MessageQueue的时候,也会创建Looper,当然如果当前线程存在Looper则会直接使用。Native层的Looper跟Jav层一样,是存放在ThreadLocal当中的,可以看如下代码:

1
2
3
4
5
sp<Looper> Looper::getForThread() {
 int result = pthread_once(& gTLSOnce, initTLSKey);
 Looper* looper = (Looper*)pthread_getspecific(gTLSKey);
 return sp<Looper>::fromExisting(looper);
}

到这里,我们知道对于一个启动了消息循环的线程,它在Java层和Native层分别会有各自的MessageQueue和Looper,java层通过mPtr来引用Native层的对象,从而使得两层能够产生联系。

Native层pollOnce

之前分析Java层获取消息的时候,会有一个地方调用nativePollOnce,它在native拿到NativeMessageQueue之后会调用它的pollOnce方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void NativeMessageQueue::pollOnce(JNIEnv* env, jobject pollObj, int timeoutMillis) {
 mPollEnv = env;
 mPollObj = pollObj;
 mLooper->pollOnce(timeoutMillis);
 mPollObj = NULL;
 mPollEnv = NULL;

 if (mExceptionObj) {
 env->Throw(mExceptionObj);
 env->DeleteLocalRef(mExceptionObj);
 mExceptionObj = NULL;
 }
}

这里的pollObj为我们java层的MessageQueue, 这里继续调用了native层的pollOnce,代码如下:

1
2
3
4
5
6
7
int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) { //我们的调用流程只会传timeoutMillis
 ...
 for (;;) {
 ...
 result = pollInner(timeoutMillis);
 }
}

这里省略了一些结果处理的代码,我们可以回头在看,这可以看到开启了一个无限循环,并调用pollInner, 这个方法比较长,我们先分块看其中的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
if (timeoutMillis != 0 && mNextMessageUptime != LLONG_MAX) {
 nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
 int messageTimeoutMillis = toMillisecondTimeoutDelay(now, mNextMessageUptime);
 if (messageTimeoutMillis >= 0
 && (timeoutMillis < 0 || messageTimeoutMillis < timeoutMillis)) {
 timeoutMillis = messageTimeoutMillis;
 }
}
int result = POLL_WAKE;
mResponses.clear(); //清除reponses列表和计数
mResponseIndex = 0;
mPolling = true;

struct epoll_event eventItems[EPOLL_MAX_EVENTS];
int eventCount = epoll_wait(mEpollFd.get(), eventItems, EPOLL_MAX_EVENTS, timeoutMillis);

mPolling = false;

这里timeoutMillis是我们从java层传过来的下一个消息的执行事件,而mNextMessageUptime是native层的最近一个消息的执行事件,这个根据这两个字段判断需要等待的事件。

在之后调用epoll_wait来等待I/O事件,或者到设置的超时时间结束等待,这样做可以避免Java层和Native层的循环空转。此处的epoll_wait除了避免循环空转还有另一个作用,我们之前在分析IMS也使用过LooperaddFd,这里如果对应的文件描述符有变化,这里就会拿到,并反应在eventCount上,这里我们先不具体分析,后面再看。

Native消息的读取和处理

当等待完成之后,就会去native的消息队列中取消息和处理,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Done: ;
 mNextMessageUptime = LLONG_MAX;
 while (mMessageEnvelopes.size() != 0) {
 nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
 const MessageEnvelope& messageEnvelope = mMessageEnvelopes.itemAt(0);
 if (messageEnvelope.uptime <= now) {
 {
 sp<MessageHandler> handler = messageEnvelope.handler;
 Message message = messageEnvelope.message;
 mMessageEnvelopes.removeAt(0);
 mSendingMessage = true;
 mLock.unlock();
 handler->handleMessage(message);
 }

 mLock.lock();
 mSendingMessage = false;
 result = POLL_CALLBACK;
 } else {
 mNextMessageUptime = messageEnvelope.uptime;
 break;
 }
 }

在Native中消息是放在mMessageEnvelope当中,这是一个verctor也就是一个动态大小的数组。不过不看这个的化,我们可以看到这里读取消息,以及读取它的执行时间uptime跟java层的代码是很像是的,甚至比java层还要简单许多,就是直接拿数组的第一条。之后使用MessageHandler执行handleMessage。这里的MessageHandler跟java层的也是很像,这里再列一下MessageEnvelopeMessage的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
struct MessageEnvelope {
 MessageEnvelope() : uptime(0) { }

 MessageEnvelope(nsecs_t u, sp<MessageHandler> h, const Message& m)
 : uptime(u), handler(std::move(h)), message(m) {}

 nsecs_t uptime;
 sp<MessageHandler> handler;
 Message message;
};

struct Message {
 Message() : what(0) { }
 Message(int w) : what(w) { }

 /* The message type. (interpretation is left up to the handler) */
 int what;
};

这里和java层的区别是,拆分成了两个结构体,但是呢比java层的还是要简单很多。到这里Native层和Java层对应的消息循环体系就分析完了。但是Native层除了这个消息循环还有一些其他东西,就是前面说到的文件描述符的消息传递。

文件描述符消息读取和处理

前面在pollOnce中还是有关于文件描述符消息的处理,这里继续分析。前面的epoll_wait就会读取相关的事件,读取完事件之后的处理如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
if (eventCount < 0) { //如果读出来的eventCount小于0,则说明有错误
 if (errno == EINTR) { //处理错误,并且跳转到Done去读取native层的消息
 goto Done;
 }
 result = POLL_ERROR;
 goto Done;
}

if (eventCount == 0) { //直接超时,没有读到事件
 result = POLL_TIMEOUT;
 goto Done;
}

for (int i = 0; i < eventCount; i++) { //根据返回的条数,来处理消息
 const SequenceNumber seq = eventItems[i].data.u64;
 uint32_t epollEvents = eventItems[i].events;
 if (seq == WAKE_EVENT_FD_SEQ) { //序列为这个序列被定义成为唤醒事件
 if (epollEvents & EPOLLIN) {
 awoken();
 } else {
 }
 } else {
 const auto& request_it = mRequests.find(seq);
 if (request_it != mRequests.end()) {
 const auto& request = request_it->second;
 int events = 0;
 if (epollEvents & EPOLLIN) events |= EVENT_INPUT;
 if (epollEvents & EPOLLOUT) events |= EVENT_OUTPUT;
 if (epollEvents & EPOLLERR) events |= EVENT_ERROR;
 if (epollEvents & EPOLLHUP) events |= EVENT_HANGUP;
 mResponses.push({.seq = seq, .events = events, .request = request});
 } else {
 ...
 }
 }
}

前面的错误处理我们直接看我的注释即可。后面会根据返回的eventCount来一次对每一个eventItem做处理,其他它的u64为序列号,这些为注册到LoopermRequests的序列号,其中1为WAKE_EVENT_FD_SEQ,也就是mWakeEventFd的序列,这里唤醒我们先不管了,直接看后面的正常的文件描述符事件监听。 这里首先会通过seq找到对应的Request,并根据epollEvents来设置他们的事件类型,之后封装成为Response放到mResponses当中。在这些做完,后面同样是跳转到Done后面的代码块,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Done: ;
 ...
 for (size_t i = 0; i < mResponses.size(); i++) {
 Response& response = mResponses.editItemAt(i);
 if (response.request.ident == POLL_CALLBACK) {
 int fd = response.request.fd;
 int events = response.events;
 void* data = response.request.data;
 int callbackResult = response.request.callback->handleEvent(fd, events, data);
 if (callbackResult == 0) {
 AutoMutex _l(mLock);
 removeSequenceNumberLocked(response.seq);
 }

 response.request.callback.clear(); //移除response对与callback的引用
 result = POLL_CALLBACK;
 }
 }

这里则是遍历刚刚我们填充的mResponses数组,从其中取出每一个Response,并调用它的Request的Callback回调的handleEvent方法,它的使用我们之前分析IMSServiceManager启动的时候已经见到过了。

以上说的是Java层会初始化Handler和Looper的情况,如果只是Native层使用的话,一般怎么用的呢。我们以BootAnimation中的使用为例,它是在BootAnimation.cpp当中,在初始化BootAnimation对象的时候,会创建一个Looper,代码如下:

1
new Looper(false)

readyToRun中添加文件描述符的监听:

1
2
3
4
5
status_t BootAnimation::readyToRun() {
 ...
 mLooper->addFd(mDisplayEventReceiver->getFd(), 0, Looper::EVENT_INPUT,
 new DisplayEventCallback(this), nullptr);
}

最后去循环调用pollOnce,来获取消息:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
bool BootAnimation::android() {
 do {
 processDisplayEvents();
 ...
 } while (!exitPending());
}

void BootAnimation::processDisplayEvents() {
 mLooper->pollOnce(0);
}

这就是Android Framework当中,大部分的Native场景使用消息循环的方式。而Native中,想要跟Java层一样发送消息,则是调用Looper的sendMessage方法。而Native层的Handler我们可以理解为只是一个Message的回调,和java层的Handler功能不可同日而语。

异步消息

在Java层的消息循环中,消息是有同步和异步之分的,异步消息一般都会伴随则屏障消息,我们之前分析的获取next消息中可以看到,如果第一个消息是屏障消息,会找后面的第一条异步消息来执行。

同时在enqueueMessage的代码中也有如下逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
//MessageQueue.java
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
for (;;) {
 prev = p;
 p = p.next;
 if (p == null || when < p.when) {
 break;
 }
 if (needWake && p.isAsynchronous()) {
 needWake = false;
 }
}
msg.next = p;
prev.next = msg;

插入异步消息会改变唤醒等待的状态,如果链表头是屏障消息,且之前调用next的时候mBlocked设置为了true,且当前是异步消息会设置成唤醒,但是如果当前的消息队列中已经有了比当前消息更早执行的消息,则不会唤醒。

到这就完成了消息循环的所有分析了。也欢迎读者朋友交流探讨。

看完评论一下吧

Android消息循环分析

2014年4月2日 19:14

我们的常用的系统中,程序的工作通常是有事件驱动和消息驱动两种方式,在Android系统中,Java应用程序是靠消息驱动来工作的。

消息驱动的原理就是:
1. 有一个消息队列,可以往这个队列中投递消息;
2. 有一个消息循环,不断从消息队列中取出消息,然后进行处理。
在Android中通过Looper来封装消息循环,同时在其中封装了一个消息队列MessageQueue。
另外Android给我们提供了一个封装类,来执行消息的投递,消息的处理,即Handler。

在我们的线程中实现消息循环时,需要创建Looper,如:

class LooperThread extends Thread {
public Handler mHandler;
public void run() {
Looper.prepare(); //1.调用prepare
......
Looper.loop(); //2.进入消息循环
}
}

看上面的代码,其实就是先准备Looper,然后进入消息循环。

  1. 在prepare的时候,创建一个Looper,同时在Looper的构造方法中创建一个消息队列MessageQueue,同时将Looper保存到TLV中(这个是关于ThreadLocal的,不太懂,以后研究了再说)
  2. 调用loop进入消息循环,此处其实就是不断到MessageQueue中取消息Message,进行处理。

然后再看我们如何借助Handler来发消息到队列和处理消息

Handler的成员(非全部):

final MessageQueue mQueue;
final Looper mLooper;
final Callback mCallback;

Message的成员(非全部):

Handler target;
Runnable callback;

可以看到Handler的成员包含Looper,通过查看源代码,我们可以发现这个Looper是有两种方式获得的,1是在构造函数传进来,2是使用当前线程的Looper(如果当前线程无Looper,则会报错。我们在Activity中创建Handler不需要传Handler是因为Activity本身已经有一个Looper了),MessageQueue也就是Looper中的消息队列。

然后我们看怎么向消息队列发送消息,Handler有很多方法发送队列(这个自己可以去查),比如我们看sendMessageDelayed(Message msg, long delayMillis)

public final boolean sendMessageDelayed(Message msg, long delayMillis) {
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
// SystemClock.uptimeMillis() 获取开机到现在的时间
}
//最终所有的消息是通过这个发,uptimeMillis是绝对时间(从开机那一秒算起)
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
boolean sent = false;
MessageQueue queue = mQueue;
if (queue != null) {
msg.target = this;
sent = queue.enqueueMessage(msg, uptimeMillis);
}
return sent;
}

看上面的的代码,可以看到Handler将自己设为Message的target,然后然后将msg放到队列中,并且指定执行时间。

消息处理

处理消息,即Looper从MessageQueue中取出队列后,调用msg.target的dispatchMessage方法进行处理,此时会按照消息处理的优先级来处理:

  1. 若msg本身有callback,则交其处理;
  2. 若Handler有全局callback,则交由其处理;
  3. 以上两种都没有,则交给Handler子类实现的handleMessage处理,此时需要重载handleMessage。

我们通常采用第三种方式进行处理。

注意!!!!我们一般是采用多线程,当创建Handler时,LooperThread中可能还未完成Looper的创建,此时,Handler中无Looper,操作会报错。

我们可以采用Android为我们提供的HandlerThread来解决,该类已经创建了Looper,并且通过wait/notifyAll来避免错误的发生,减少我们重复造车的事情。我们创建该对象后,调用getLooper()即可获得Looper(Looper未创建时会等待)。

补充

本文所属为Android中java层的消息循环机制,其在Native层还有消息循环,有单独的Looper。并且2.3以后MessageQueue的核心向Native层下移,native层java层均可以使用。这个我没有过多的研究了!哈哈

PS:本文参考《深入理解Android:卷I》

看完评论一下吧

现在做个人博客的最低成本是多少

2024年10月7日 22:44

距离上次在知乎回答这个问题已经过去一年多了,我决定重新修正一下这个答案。

先说我的成本,243.12元/年。

直接上清单:

如果不需要评论功能,这个成本可能会更低,但考虑到后期需要备案,还得有服务器,所以服务器的成本总也绕不过。

服务器我选择的是阿里云新人优惠,2022年初阿里云新人优惠,3年196元,我直接续费到了2028年。(这种大力度优惠不会再有了。)

最初博客静态文件和图床都使用了阿里云oss,不幸的是,随着博客流量越来越大,图床的存储桶从最初的一年十几块钱,到后来一天一两块钱,看得我很焦虑。用客户端备份了整个存储桶里的图片,居然有1.3G!

直到将图床整体迁移到cloudflare,配置好了uPic,虽然慢点,但也还能接受。至少不用再为每天那几块钱心惊肉跳了。

xyz溢价域名,一年6块8,10年也才68块钱,选那种纯数字域名即可。xyz域名支持在国内备案,虽然图床域名解析在了cloudflare,但博客静态文件还在阿里云,担心批量使用境外链接会被污染,就先在阿里云备案后,再解析到cloudflare。

cloudflare真是活菩萨,发现解析到cloudflare的域名可以直接带上SSL签名,还可以设置反代,省去了搭建面板设置反代的麻烦。

没准哪天心血来潮,把博客整个数据迁移到cloudflare,直接零成本,也不是不可能。但念到总有一天cloudflare可能会被墙,先这样吧。

搞机!给红米 K40 刷一个 PixelExperience

2024年10月5日 22:30

随着国产手机系统日益完善,各大厂商对 Bootloader(BL)解锁的限制也愈发严格。这导致越来越少的用户愿意尝试对安卓手机进行 root 或刷机等深度定制。我使用了几个月的澎湃 OS 后,最终还是决定将我的红米 K40 刷入 PixelExperience(简称 PE)系统。在这篇文章中,我将为大家详细记录这次"搞机"的全过程。

搞机!给红米 K40 刷一个 PixelExperience最先出现在Jack‘s Space

C/C++ 中的内存管理器(堆与栈)

2024年10月3日 04:48

最近面试的时候遇到这个问题。这个问题考你计算机的基本功。

在 C/C++ 中,内存管理是控制程序如何分配和管理其资源的关键方面。C/C++ 程序中的内存通常分为不同的区域:堆栈和堆是最主要的动态和自动内存分配区域。

ACM题解系列之 – 最小堆栈 (Min Stack)

stack C/C++ 中的内存管理器(堆与栈) 学习笔记 技术 程序员 程序设计 编程 计算机 软件工程 面试

Stack 栈

堆栈内存

  • 定义:堆栈内存用于静态(自动)内存分配。它是存储函数参数、本地变量和返回地址的地方。当调用一个函数时,一个新的内存块(称为堆栈帧)会被添加到堆栈的顶部。当函数返回时,该内存会被自动释放。
  • 分配:内存由系统自动管理——在变量超出作用域时自动分配和释放。无需人工干预。
  • 生命周期:受限于函数或代码块的作用域。一旦函数退出,内存将被释放。
  • 大小限制:堆栈的大小通常较小并由系统预定义,意味着大的分配可能导致堆栈溢出。
  • 访问速度:由于其后进先出(LIFO)的结构,堆栈内存访问速度更快。由于内存是连续的且可预测的,它允许快速访问。
  • 使用场景:局部变量、函数调用信息和固定大小的对象(数组、结构体)。

堆内存

  • 定义:堆内存用于动态内存分配,程序员使用 C 中的 malloc()、calloc()、free() 和 C++ 中的 new、delete 手动分配和释放内存。
  • 分配:内存在运行时分配,并且分配的生命周期由程序员手动控制。它可以持续存在,直到显式释放。
  • 生命周期:堆分配的对象的生命周期不受作用域的限制。内存将一直被使用,直到被释放为止。
  • 大小限制:堆通常比堆栈大,但取决于系统资源。不当处理可能导致内存泄漏(忘记释放分配的内存)或碎片化(内存使用效率低)。
  • 访问速度:堆内存的访问速度比堆栈慢,因为分配是分散的,动态分配涉及更多的开销。
  • 使用场景:如链表、等大数据结构,或在运行时确定大小的对象。

堆与栈的主要区别

特征 堆栈
内存大小 通常较小,预定义 通常较大,受系统资源限制
分配 自动,由编译器管理 手动,由程序员管理(使用 new、malloc 等)
释放 自动(函数退出时) 手动(使用 delete、free 等)
生命周期 限于函数/代码块作用域 可以持续,直到显式释放
速度 较快(连续内存) 较慢(分散内存,开销更大)
风险 堆栈溢出(如果超出大小限制) 内存泄漏和碎片化

堆栈分配示例

void function() {
    int x = 10; // 分配在堆栈上
} // x 会自动释放

堆分配示例

void function() {
    int* p = new int; // 分配在堆上
    *p = 10;
    delete p; // 必须手动释放
}

正确管理堆内存在 C/C++ 中非常重要,因为它可能导致与内存相关的错误,如内存泄漏或重复释放。理解堆和堆栈内存之间的差异有助于优化程序的性能和可靠性。

英文:The Memory Manager in C/C++ (Heap vs Stack)

面试经历

面试题

面试技巧

面试其它

本文一共 874 个汉字, 你数一下对不对.
C/C++ 中的内存管理器(堆与栈). (AMP 移动加速版本)

扫描二维码,分享本文到微信朋友圈
75a5a60b9cac61e5c8c71a96e17f2d9c C/C++ 中的内存管理器(堆与栈) 学习笔记 技术 程序员 程序设计 编程 计算机 软件工程 面试
The post C/C++ 中的内存管理器(堆与栈) first appeared on 小赖子的英国生活和资讯.

相关文章:

  1. Javascript 中 sleep 函数实现 Javascript 中并没有 built-in 的 sleep 函数支持, 在 async/await/Promise 的支持之前, 我们可以用 busy-waiting 的方式来模拟: 1 2 3...
  2. 按揭贷款(房贷,车贷) 每月还贷计算器 去年给银行借了17万英镑 买了20万7500英镑的房子, 25年还清. 前2年是定率 Fix Rate 的合同 (年利率2.49%). 每个月大概是还 700多英镑. 有很多种还贷的计算方式, 定率/每月固定 是比较常用的. 简单来说就是 每个月交的钱是...
  3. 《Steem 指南》之 justyy 在线工具与 API 系列 – 同时给多个帐号发送SBD或者STEEM 同时给多个帐号发送SBD或者STEEM STEEMIT 和 BUSY 的前端都有一个内置的钱包工具, 您可以一次给一个帐号发送 SBD 或者 STEEM. 当我们要给很多很多人发送钱的时候, 就显得有些不方便了. 这时候可以用这个在线工具: https://steemyy.com/wallet-tool/ 填写表单 只需要填上你的ID,...
  4. 智能手机 HTC One M9 使用测评 虽然我对手机要求不高, 远远没有像追求VPS服务器一样, 但是怎么算来两年内换了四个手机, 先是三星 S4 用了一年多, 然后 Nokia Lumia 635 Windows Phone, 后来又是 BLU, 半年多前换了...
  5. 拔牙后的注意事项(图, 慎入) Care of Mouth after Extraction 昨天又拔了两颗牙, 初步定在5月4号装牙套. 这是牙医诊所给的术后注意事项: 拔完后需要等3-4小时麻醉失效后才能吃喝. 稍微流点血是很正常的. 但是请不要漱口吐出, 因为这会加速流血. 你只要轻轻的含着口水并咽下即可. 如果一直流血, 请拿着纱布(并不是纸巾)放在拔牙处20分钟. 24小时内请不要运动, 术后几小时内回家静静坐着. 12小时内不要吸烟, 喝酒或者喝热饮, 因为这会让伤口流血....
  6. 同一台服务器上多个WORDPRESS站点的一些设置可以移出去 我自从把所有网站都挪到一处VPS服务器上 就发现很多事情省事很多 可以同时管理多个网站 包括 WORDPRESS博客. 比如我有四个WORDPRESS博客 然后我就把通用的一些资料给移出去 移到 HTTP或者HTTPS都不能直接访问的文件夹里这样就更安全许多. 文件 wp-conn.php 存储了 相同的数据库资料. 1 2...
  7. 最简单有效的过滤WordPress垃圾评论的方法 当你的Wordpress博客流量大的时候, 不免会收到很多垃圾评论. 本文介绍一种特别简单而且免费的过滤Wordpress垃圾评论的方法. 这种方法不需要你安装任何插件, 也不需要拥有修改Wordpress主题模板函数的能力, 只需要1分钟就可以搞定. 把这个列表拷贝下来 打开 WordPress 的控制面版, 到设置-讨论 拷贝上面的列表到 “评论审核” 或者 “评论黑名单”...
  8. 更改全站的评论名称 坛子给我建议说: 我觉得很有道理,但是别人网站上的留言我改不了, 自己的还是可以先改改的. 于是,我登陆 phpmyadmin (一个网页式的php mysql 管理平台) 然后输入以下命令: update `wp_comments` set `wp_comment_author` = 'JustYY.com...

Android源码分析:系统进程中事件的读取与分发

2024年9月26日 13:44

之前分析的是从InputChannel中读取Event,并且向后传递,进行消费和处理的过程。但是之前的部分呢,事件如何产生,事件怎么进入到InputChanel当中的,事件又是如何跨进程到达App进程,这里继续来分析。

以上为system进程的流程的简化图,这里我们可以看到几个重要的组件,这里以触摸事件来进行分析(后文的分析也将会以触摸事件为主进行分析)并且简单的描绘了事件从EventHub到服务端的InputChannel发送事件的全部过程。具体内容一起来看下面的代码。

InputManagerService的创建

因为事件的分发涉及到不少类,我们先从InputManagerService(IMS)的初始化出发,进行分析。入口代码在SystemServer.java中,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
private void startOtherServices(@NonNull TimingsTraceAndSlog t) {
WindowManagerService wm = null;
InputManagerService inputManager = null;
inputManager = new InputManagerService(context);
wm = WindowManagerService.main(context, inputManager, !mFirstBoot, mOnlyCore,
 new PhoneWindowManager(), mActivityManagerService.mActivityTaskManager);
ServiceManager.addService(Context.WINDOW_SERVICE, wm, /* allowIsolated= */ false,
 DUMP_FLAG_PRIORITY_CRITICAL | DUMP_FLAG_PROTO);
ServiceManager.addService(Context.INPUT_SERVICE, inputManager,
 /* allowIsolated= */ false, DUMP_FLAG_PRIORITY_CRITICAL);

inputManager.setWindowManagerCallbacks(wm.getInputManagerCallback());
inputManager.start();
}

这里我们可以看到WMS的创建我们传入了IMS,并且IMS也依赖WindowMnagerCallbacks,我们先看一下IMS的构造方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public InputManagerService(Context context) {
 this(new Injector(context, DisplayThread.get().getLooper()));
}

@VisibleForTesting
InputManagerService(Injector injector) {
 ...
 mHandler = new InputManagerHandler(injector.getLooper());
 mNative = injector.getNativeService(this);
 ...
}

我们主要关注这个mNative的构建,它是NativeImpl,它的创建过程如下:

1
new NativeInputManagerService.NativeImpl(service, mContext, mLooper.getQueue());

这里的Looper是前面传进来的DisplayThread的Looper。在NativeImpl的构造方法中调用了init方法,并获取到了它的native指针,这里需要看com_android_server_input_InputManagerService.cpp中的natvieInit方法,代码如下:

1
2
3
4
5
6
7
8
static jlong nativeInit(JNIEnv* env, jclass /* clazz */,
 jobject serviceObj, jobject contextObj, jobject messageQueueObj) {
 sp<MessageQueue> messageQueue = android_os_MessageQueue_getMessageQueue(env, messageQueueObj);
 NativeInputManager* im = new NativeInputManager(contextObj, serviceObj,
 messageQueue->getLooper());
 im->incStrong(0);
 return reinterpret_cast<jlong>(im);
}

这里创建了NativeInputManager

NativeInputManager初始化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
NativeInputManager::NativeInputManager(jobject contextObj,
 jobject serviceObj, const sp<Looper>& looper) :
 mLooper(looper), mInteractive(true) {
 JNIEnv* env = jniEnv();

 mServiceObj = env->NewGlobalRef(serviceObj);

 {
 AutoMutex _l(mLock);
 mLocked.systemUiLightsOut = false;
 mLocked.pointerSpeed = 0;
 mLocked.pointerAcceleration = android::os::IInputConstants::DEFAULT_POINTER_ACCELERATION;
 mLocked.pointerGesturesEnabled = true;
 mLocked.showTouches = false;
 mLocked.pointerDisplayId = ADISPLAY_ID_DEFAULT;
 }
 mInteractive = true;

 InputManager* im = new InputManager(this, this);
 mInputManager = im;
 defaultServiceManager()->addService(String16("inputflinger"), im);
}

这个构造方法中,传入的jobject为我们之前的NativeImpl,后面有需要调用java层的时候会用到它。除此之外我们看到又创建了一个InputManger,并且把它注册到了ServiceManger当中,名称为inputflinger

我们继续看InputManager的初始化代码,它传如的两个参数readerPolicydispatcherPolicy的实现都在NativeInputManager当中。它的代码如下:

1
2
3
4
5
6
7
8
InputManager::InputManager(
 const sp<InputReaderPolicyInterface>& readerPolicy,
 const sp<InputDispatcherPolicyInterface>& dispatcherPolicy) {
 mDispatcher = createInputDispatcher(dispatcherPolicy);
 mClassifier = std::make_unique<InputClassifier>(*mDispatcher);
 mBlocker = std::make_unique<UnwantedInteractionBlocker>(*mClassifier);
 mReader = createInputReader(readerPolicy, *mBlocker);
}

这里首先创建了InputDispatcher,之后创建的mClassifiermBlockerInputDispatcher一样都是继承自InputListenerInterface,它们的作用为在事件经过InputDispatcher分发之前,可以做一些预处理。最后创建InputReader,事件会经由它传递到InputDispatcher,最后再由InputDispatcher分到到InputChannel。下面来详细分析。

事件源的初始化

因为InputDispatcher初始化代码比较简单,我们从createInputReader的源码开始看起来:

1
2
3
4
std::unique_ptr<InputReaderInterface> createInputReader(
 const sp<InputReaderPolicyInterface>& policy, InputListenerInterface& listener) {
 return std::make_unique<InputReader>(std::make_unique<EventHub>(), policy, listener);
}

我们可以看到在创建InputReader之前首先创建了一个EventHub,看名字我们就知道它是一个事件的收集中心。我们看它的构造方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
EventHub::EventHub(void)
 : mBuiltInKeyboardId(NO_BUILT_IN_KEYBOARD),
 mNextDeviceId(1),
 ...
 mPendingINotify(false) {
 ensureProcessCanBlockSuspend();

 mEpollFd = epoll_create1(EPOLL_CLOEXEC); //创建epoll实例,flag表示执行新的exec时候会自动关闭

 mINotifyFd = inotify_init1(IN_CLOEXEC); //创建inotify实例,该实例用于监听文件的变化

 if (std::filesystem::exists(DEVICE_INPUT_PATH, errorCode)) {
 addDeviceInputInotify();
 } else {
 addDeviceInotify();
 isDeviceInotifyAdded = true;

 }

 struct epoll_event eventItem = {};
 eventItem.events = EPOLLIN | EPOLLWAKEUP;
 eventItem.data.fd = mINotifyFd;
 int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mINotifyFd, &eventItem);
 int wakeFds[2];
 result = pipe2(wakeFds, O_CLOEXEC);

 mWakeReadPipeFd = wakeFds[0];
 mWakeWritePipeFd = wakeFds[1];

 result = fcntl(mWakeReadPipeFd, F_SETFL, O_NONBLOCK);

 result = fcntl(mWakeWritePipeFd, F_SETFL, O_NONBLOCK);

 eventItem.data.fd = mWakeReadPipeFd;
 result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeReadPipeFd, &eventItem);
}

从上面的代码我们可以看到这里主要为创建inotify并且通过epoll去监听文件的变化,其中还是用管道创建了wakeReadPipewakeReadPipe的文件描述,用于接收回调。我们先看一下addDeviceInputInotify()方法:

1
2
3
4
void EventHub::addDeviceInputInotify() {
 mDeviceInputWd = inotify_add_watch(mINotifyFd, DEVICE_INPUT_PATH, IN_DELETE | IN_CREATE);

}

其中DEVICE_INPUT_PATH的值为/dev/input,也就是说把这个path放到mINofiyFd的监控当中。对于了解Linux的人应该知道,在Linux中万物结尾文件,因此我们的输入也是文件,当事件发生的时候便会写入到/dev/input下面,文件变化我们也会得到通知。我这里使用ls命令打印了一下我的手机,/dev/input下面有如下文件:

1
event0 event1 event2 event3 event4 event5

具体这些文件的写入,那就是内核和驱动相关的东西了,我们这里不再讨论。而事件的读取,我们后面再进行分析。

IMS的启动

各个对象都构建完成之后,IMS要进行启动,才能够对事件进行处理并且分发。SystemServer中已经调用了IMS的start方法,它其中又会调用NativeInputMangerstart方法,最终会调用 native层的InputManagerstart方法。而其中分别又调用了Dispatcher的start方法和Reader的start方法。我们分别分析。

InputDispater 调用start

1
2
3
4
5
6
7
8
status_t InputDispatcher::start() {
 if (mThread) {
 return ALREADY_EXISTS;
 }
 mThread = std::make_unique<InputThread>(
 "InputDispatcher", [this]() { dispatchOnce(); }, [this]() { mLooper->wake(); });
 return OK;
}

这个方法中主要创建了InputThread,并且给它传了两个lambda,分别执行InputDispatchdispatchOnce方法和执行Looper的wake方法。我们看InputThread的构造方法:

1
2
3
4
5
InputThread::InputThread(std::string name, std::function<void()> loop, std::function<void()> wake)
 : mName(name), mThreadWake(wake) {
 mThread = new InputThreadImpl(loop);
 mThread->run(mName.c_str(), ANDROID_PRIORITY_URGENT_DISPLAY);
}

可以看到其中创建了InputThreadImpl,这个类才是真的继承的系统的Thread类,这里构建完成它就继续调用了它的run方法,这样它就会启动了。这里我们需要注意这个 线程的优先级,为PRIORITY_URGEN_DISPLAY,可以看到优先级是非常高了。

1
2
3
4
bool threadLoop() override {
 mThreadLoop();
 return true;
}

另外就是我们传进来的loop传入了这个对象,并且在它的threadLoop中会执行它。对于native中的线程,我们在threadLoop中实现逻辑就可以了,并且这里我们返回值为true,它会继续循环执行 。而我们传入的另一个lambda,则是在线程推出的时候调用。这个线程循环中执行的就是我们的InputDispatch中 的dispatchOnce方法,也就是消息的投递,后面再来分析。

InputReader调用start方法

1
2
3
4
5
6
7
8
status_t InputReader::start() {
 if (mThread) {
 return ALREADY_EXISTS;
 }
 mThread = std::make_unique<InputThread>(
 "InputReader", [this]() { loopOnce(); }, [this]() { mEventHub->wake(); });
 return OK;
}

这里的初始化,我们可以看到跟前面的InputDispatch很类似,连InputThread用的都是同一个类,内部也就一样有InputThreadImpl了。这里则是调用了InputReader内部的loopOnce方法。到这里系统就完成了输入事件分发的初始化了。

我们在看事件的分发之前,先看一下应用中的接收和系统的InputDispatch进行连接的过程。

InputChannel的注册

我们之前分析应用层的事件传递的时候,只是谈到了InputChannel是在WMS调用如下代码生成的:

1
mInputChannel = mWmService.mInputManager.createInputChannel(name);

但是内部如何创建InputChannel的,以及 这个InputChannel是如何收到消息的我们都没有涉及,我们现在继续分析它一下。这个createInputChannel内部最终会调用到native层的InputDispatchercreateInputChannel方法, 代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Result<std::unique_ptr<InputChannel>> InputDispatcher::createInputChannel(const std::string& name) {
 std::unique_ptr<InputChannel> serverChannel;
 std::unique_ptr<InputChannel> clientChannel;
 status_t result = InputChannel::openInputChannelPair(name, serverChannel, clientChannel);
 ...
 { // acquire lock
 std::scoped_lock _l(mLock);
 const sp<IBinder>& token = serverChannel->getConnectionToken();
 int fd = serverChannel->getFd();
 sp<Connection> connection =
 n1ew Connection(std::move(serverChannel), false /*monitor*/, mIdGenerator);
 ...
 mConnectionsByToken.emplace(token, connection);

 std::function<int(int events)> callback = std::bind(&InputDispatcher::handleReceiveCallback, this, std::placeholders::_1, token);

 mLooper->addFd(fd, 0, ALOOPER_EVENT_INPUT, new LooperEventCallback(callback), nullptr);
 } // release lock

 // Wake the looper because some connections have changed.
 mLooper->wake();
 return clientChannel;
}

首先是第4行代码,这里创建了InputChannel,而它又分为serverChannelclientChannel,返回调用方的是`clientChannel。

我们先进去看看其源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
status_t InputChannel::openInputChannelPair(const std::string& name,
 std::unique_ptr<InputChannel>& outServerChannel,
 std::unique_ptr<InputChannel>& outClientChannel) {
 int sockets[2];
 if (socketpair(AF_UNIX, SOCK_SEQPACKET, 0, sockets)) { //创建socket对
 ..
 return result;
 }
 ..
 sp<IBinder> token = new BBinder();

 std::string serverChannelName = name + " (server)";
 android::base::unique_fd serverFd(sockets[0]); //获取server socket fd
 outServerChannel = InputChannel::create(serverChannelName, std::move(serverFd), token); //创建server InputChannel

 std::string clientChannelName = name + " (client)";
 android::base::unique_fd clientFd(sockets[1]);
 outClientChannel = InputChannel::create(clientChannelName, std::move(clientFd), token); //创建Client InputChannel
 return OK;
}

以上代码我们可以看到就是创建了一对socket,分别放到两个InputChannel当中,并且这里创建了一个BBinder作为两个InputChannel的token,具体用处我们后面会再提到。此时可以继续回看前面的createInputChannel方法,在11行,创建了一个 Connection对象,并且以前面创建的BBinder为key放到了mConnectionsByToken当中,Connection的用处留到后面继续讲。

在15行创建了一个callback,其中会执行InputDispatcherhandleReceiveCallback方法,并且这个callback被添加looper的addFd的时候设置进去了,这里的fd就是之前创建的ServerInputChannel的socket的文件描述符。到这里就完成了初始化,添加了服务端InputChannel的文件描述符监听。

事件触发

我们之前在分析InputManger的启动的时候,已经看到了事件是通过/dev/input来通知到EventHub,而InputReader通过Looper监听了/dev/input的文件描述符,从而让我们事件传递的系统动起来。那么我们首先就从InputReaderloopOnce开始看起来。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
void InputReader::loopOnce() {
 ...
 size_t count = mEventHub->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE);

 { // acquire lock
 std::scoped_lock _l(mLock);
 mReaderIsAliveCondition.notify_all();

 if (count) {
 processEventsLocked(mEventBuffer, count);
 }
 ...
 } // release lock
 ...
 mQueuedListener.flush();
}

我们这里省略了设备变化,超时等相关的代码,仅仅保留了事件读取相关的部分。我们看到,首先在第3行中,从EventHub中去获取新的事件,之后在第10行,去处理这些事件,第15行会清楚所有的事件,我们分别看看各个里面的逻辑。

从EventHub读取事件

首先是getEvents方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
size_t EventHub::getEvents(int timeoutMillis, RawEvent* buffer, size_t bufferSize) {
 std::scoped_lock _l(mLock);
 struct input_event readBuffer[bufferSize];
 RawEvent* event = buffer;
 size_t capacity = bufferSize;
 bool awoken = false;
 for (;;) {
 nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);

 bool deviceChanged = false;
 //pendingIndex小于PendingCount,说明之前有事件还为处理完
 while (mPendingEventIndex < mPendingEventCount) {
 const struct epoll_event& eventItem = mPendingEventItems[mPendingEventIndex++];
 ...
 Device* device = getDeviceByFdLocked(eventItem.data.fd);
 if (device == nullptr) { //未能找到device,报错跳出
 continue;
 }

 // EPOLLIN表示有事件可以处理
 if (eventItem.events & EPOLLIN) {
 int32_t readSize =
 read(device->fd, readBuffer, sizeof(struct input_event) * capacity);
 if (readSize == 0 || (readSize < 0 && errno == ENODEV)) {
 // 接收到通知之前,设备以及不见了
 deviceChanged = true;
 closeDeviceLocked(*device);
 } else if() { //其中的错误情况,忽略掉
 } else {
 int32_t deviceId = device->id == mBuiltInKeyboardId ? 0 : device->id;

 size_t count = size_t(readSize) / sizeof(struct input_event); //根据一个事件的大小,来算同一个设备上面读取到的事件的个数
 //以下为具体保存事件到event当中
 for (size_t i = 0; i < count; i++) {
 struct input_event& iev = readBuffer[i];
 event->when = processEventTimestamp(iev);
 event->readTime = systemTime(SYSTEM_TIME_MONOTONIC);
 event->deviceId = deviceId;
 event->type = iev.type;
 event->code = iev.code;
 event->value = iev.value;
 event += 1;
 capacity -= 1;
 }
 if (capacity == 0) { //缓冲区已经满了,无法在记录事件,跳出
 mPendingEventIndex -= 1;
 break;
 }
 }
 } else if (eventItem.events & EPOLLHUP) {
 ...
 } else {
 ...
 }
 }
 ...
 //event和buffer地址不同说明已经拿到事件了,可以跳出循环
 if (event != buffer || awoken) {
 break;
 }

 mPendingEventIndex = 0;
 mLock.unlock(); // poll之前先加锁
 int pollResult = epoll_wait(mEpollFd, mPendingEventItems, EPOLL_MAX_EVENTS, timeoutMillis);
 mLock.lock(); // poll完之后从新加锁

 if (pollResult == 0) {
 // Timed out.
 mPendingEventCount = 0;
 break;
 }

 if (pollResult < 0) {
 mPendingEventCount = 0;
 if (errno != EINTR) {
 usleep(100000);
 }
 } else {
 mPendingEventCount = size_t(pollResult);
 }
 }

 // event为填充之后的指针地址,而buffer为开始的地址,相减获得count
 return event - buffer;
}

这个方法是很复杂的,但是我们主要分析事件的分发,因此其中关于设备变化,设备响应,错误处理等等相关的代码都省略了。这个方法,我们传入了一个RawEvent的指针用来接收事件,另外传了bufferSize来表示我们所能接收的事件数量。这个方法使用了两层循环来进行逻辑的处理,外层的为无限循环。当我们第一次进入这个方法当中,mPendingEventCountmPendingEventIndex都是0,因此不会进入第二层的循环,这个时候会执行到64行,调用epoll_wait系统调用,去读取事件,读取的结果会放到mPendingEventItems当中,之后会算出pendingCount。这样继续循环,我们就可以进入内存循环当中了。 在刚刚的PendingEventItem中并没有存储具体的事件,而是存储的事件发生的设备文件描述符,在内存的循环中,首先会根据设备的描述符查找设备,并对其进行检查。之后再从设备当中读取事件,拼装成为需要向后分发的事件。 这里的count有点让人迷糊,我画了个图如下所示:

其中我们真正读取的事件的数量,是要看有几个设备,每个设备有多少个事件,对其进行计算。 到这里我们就获取到了事件,这里可以回到InputReader中继续往下看了。

InputReader对事件进行处理

在这里的处理调用的是processEventsLocked,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void InputReader::processEventsLocked(const RawEvent* rawEvents, size_t count) {
 for (const RawEvent* rawEvent = rawEvents; count;) {
 int32_t type = rawEvent->type;
 size_t batchSize = 1;
 //如果不是设备处理相关的事件,则执行。
 if (type < EventHubInterface::FIRST_SYNTHETIC_EVENT) {
 int32_t deviceId = rawEvent->deviceId;
 while (batchSize < count) {
 if (rawEvent[batchSize].type >= EventHubInterface::FIRST_SYNTHETIC_EVENT ||
 rawEvent[batchSize].deviceId != deviceId) {
 //当遇到设备整删除事件,或者不是当前设备的事件,就不能进行批量处理,跳过。
 break;
 }
 batchSize += 1;
 }
 processEventsForDeviceLocked(deviceId, rawEvent, batchSize);
 } else {
 //设备添加删除之类的事件处理,跳过
 }
 count -= batchSize;
 rawEvent += batchSize;
 }
}

这个方法中主要是对与设备增加删除事件和普通事件进行分别处理,如果是普通的事件,会对同一个设备上的事件进行批量处理,批量处理则会调用processEventsForDeviceLocked方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void InputReader::processEventsForDeviceLocked(int32_t eventHubId, const RawEvent* rawEvents,
 size_t count) {
 auto deviceIt = mDevices.find(eventHubId);
 if (deviceIt == mDevices.end()) {
 //没有找到设备,返回
 return;
 }

 std::shared_ptr<InputDevice>& device = deviceIt->second;
 if (device->isIgnored()) { //是被忽略的设备,跳过
 return;
 }

 device->process(rawEvents, count);
}

这个方法中主要是查找设备,找到未忽略的设备则会调用设备的process方法进行处理。 InputDevice只是设备的抽象,而其中的处理又会调用InputMapper的方法,InputMapper是抽象类,它有许多的实现,比如我们的触摸事件就会有TouchuInputMapperMultiTouchInputMapper,各种不同的InputMapper会对事件进行处理,拼装成符合相关类型的事件,其中逻辑我们就不继续进行追踪了。

对于touch事件,这个process处理完成,在TouchInputMapper中最终会调用dispatchMotion,这个方法代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
void TouchInputMapper::dispatchMotion(...) {
 PointerCoords pointerCoords[MAX_POINTERS];
 PointerProperties pointerProperties[MAX_POINTERS];
 uint32_t pointerCount = 0;
 ...
 const int32_t displayId = getAssociatedDisplayId().value_or(ADISPLAY_ID_NONE);
 const int32_t deviceId = getDeviceId();
 std::vector<TouchVideoFrame> frames = getDeviceContext().getVideoFrames();
 std::for_each(frames.begin(), frames.end(),
 [this](TouchVideoFrame& frame) { frame.rotate(this->mInputDeviceOrientation); });
 NotifyMotionArgs args(getContext()->getNextId(), when, readTime, deviceId, source, displayId,
 policyFlags, action, actionButton, flags, metaState, buttonState,
 MotionClassification::NONE, edgeFlags, pointerCount, pointerProperties,
 pointerCoords, xPrecision, yPrecision, xCursorPosition, yCursorPosition,
 downTime, std::move(frames));
 getListener().notifyMotion(&args);
}

其中有许多关于多点触控,事件处理的判断,这里只关注最后的部分,就是将事件组装成一个NotifyMotionArgs对象,并调用ListenernotifyMotion方法。这里的getListener()内部首先会调用getContenxt获取Context,而这个Context就是InputReader的内部成员mContext,这这个Listener也就是我们之前在初始化InputReader时候它的成员变量mQueuedListener,那我们下面继续去看它的notifyMotion

notifyMotion

1
2
3
4
void QueuedInputListener::notifyMotion(const NotifyMotionArgs* args) {
 traceEvent(__func__, args->id);
 mArgsQueue.emplace_back(std::make_unique<NotifyMotionArgs>(*args));
}

这里是直接把之前的那个变量放到mArgsQueue当中了。这个时候,我们需要留意一下之前InputReadeloopOnce的15行,这里调用的 flush方法,也是这个QueuedInputListener内部的:

1
2
3
4
5
6
void QueuedInputListener::flush() {
 for (const std::unique_ptr<NotifyArgs>& args : mArgsQueue) {
 args->notify(mInnerListener);
 }
 mArgsQueue.clear();
}

这里这是掉用了我们传进来的NotifyArgs的notify方法,并且传过来的参数mInnerListener是我们之前创建InputManager时候创建的,这里会有三层嵌套,首先是UnWantedInteractionBlocker先处理,之后它会按情况传递给InputClassifier处理,最后是在InputDispatcher当中处理。

我们先看看看notify当中做了什么,再继续往后看。

1
2
3
void NotifyMotionArgs::notify(InputListenerInterface& listener) const {
 listener.notifyMotion(this);
}

这里也是比较简单,就是直接调用了linster的notifyMotion方法,我们可以直接去看了。因为我们主要关注 传递,而不关注处理,这里就跳过,直接看InputDispatcher中的这个方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void InputDispatcher::notifyMotion(const NotifyMotionArgs* args) {
 if (!validateMotionEvent(args->action, args->actionButton, args->pointerCount,
 args->pointerProperties)) {
 return; //不合法的触摸事件直接返回
 }

 uint32_t policyFlags = args->policyFlags;
 policyFlags |= POLICY_FLAG_TRUSTED;

 android::base::Timer t;

 bool needWake = false;
 { // acquire lock
 mLock.lock();
 ...
 std::unique_ptr<MotionEntry> newEntry =
 std::make_unique<MotionEntry>(args->id, args->eventTime, args->deviceId,
 args->source, args->displayId, policyFlags,
 args->action, args->actionButton, args->flags,
 args->metaState, args->buttonState,
 args->classification, args->edgeFlags,
 args->xPrecision, args->yPrecision,
 args->xCursorPosition, args->yCursorPosition,
 args->downTime, args->pointerCount,
 args->pointerProperties, args->pointerCoords);
 ...
 needWake = enqueueInboundEventLocked(std::move(newEntry));
 mLock.unlock();
 } // release lock

 if (needWake) {
 mLooper->wake();
 }
}

在这里则是执行完一些检查之后,把事件封装成为MotionEntry,调用enqueueInboundEventLocked,最后调用looperwake方法。enqueueInboundEventLocked代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
bool InputDispatcher::enqueueInboundEventLocked(std::unique_ptr<EventEntry> newEntry) {
 bool needWake = mInboundQueue.empty();
 mInboundQueue.push_back(std::move(newEntry));
 EventEntry& entry = *(mInboundQueue.back());
 switch (entry.type) {
 case EventEntry::Type::MOTION: {
 if (shouldPruneInboundQueueLocked(static_cast<MotionEntry&>(entry))) { //返回true的时候,事件会被移除不处理
 mNextUnblockedEvent = mInboundQueue.back();
 needWake = true;
 }
 break;
 }
 ...
 }

 return needWake;
}

在这里,首先把事件放入mInboundQueue这个deque当中,最后根据事件的类型和信息要不要唤醒looper,如果事件不被移除needWake就为false,前面的wake也不会被调用。但是这个是否调用,不影响我们的后续分析,因为InputDispatch中的Thead会一直循环调用。

InputDispatcher分发消息

说到这里,我们就该来看看InputDispatcherdispatchOnce方法了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
void InputDispatcher::dispatchOnce() {
 nsecs_t nextWakeupTime = LONG_LONG_MAX;
 { // acquire lock
 std::scoped_lock _l(mLock);
 mDispatcherIsAlive.notify_all();

 if (!haveCommandsLocked()) {
 dispatchOnceInnerLocked(&nextWakeupTime);
 }

 if (runCommandsLockedInterruptable()) {
 nextWakeupTime = LONG_LONG_MIN;
 }
 ...
 } // release lock

 //等待下一次调用
 mLooper->pollOnce(timeoutMillis);
}

这里有不少处理下一次唤醒的逻辑,我们都跳过,主要就看一下第8行,进行这一次的实际执行内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) {
 ...
 if (!mPendingEvent) { //当前没有待处理的pending事件
 if (mInboundQueue.empty()) { //如果事件列表为空
 ...
 if (!mPendingEvent) {
 return;
 }
 } else {
 // 列表中拿一个事件
 mPendingEvent = mInboundQueue.front();
 mInboundQueue.pop_front();
 traceInboundQueueLengthLocked();
 }
 ...
 }

 bool done = false;
 ..
 switch (mPendingEvent->type) {
 ...
 case EventEntry::Type::MOTION: {
 std::shared_ptr<MotionEntry> motionEntry =
 std::static_pointer_cast<MotionEntry>(mPendingEvent);
 ...
 done = dispatchMotionLocked(currentTime, motionEntry, &dropReason, nextWakeupTime);
 break;
 }
 ...
 }

 if (done) {
 ...
 releasePendingEventLocked();
 *nextWakeupTime = LONG_LONG_MIN; // force next poll to wake up immediately
 }
}

我们将这个方法进行了简化,仅仅保留了触摸事件的部分代码。首先判断mPendingEvent是否为空,为空的时候我们需要到mPendingEvent中去拿一个,我们之前插入的是尾部,这里是从头部取的。拿到事件进行完种种处理和判断之后,会调用dispatchMotionLocked进行触摸事件的分发:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
bool InputDispatcher::dispatchMotionLocked(nsecs_t currentTime, std::shared_ptr<MotionEntry> entry,
 DropReason* dropReason, nsecs_t* nextWakeupTime) {
 if (!entry->dispatchInProgress) { //设置事件正在处理中
 entry->dispatchInProgress = true;

 }

 if (*dropReason != DropReason::NOT_DROPPED) {
 //对于要抛弃的事件这里进行处理,返回
 return true;
 }

 const bool isPointerEvent = isFromSource(entry->source, AINPUT_SOURCE_CLASS_POINTER); //读取是否为POINTER
 std::vector<InputTarget> inputTargets;

 bool conflictingPointerActions = false;
 InputEventInjectionResult injectionResult;
 if (isPointerEvent) {
 //如果屏幕触摸事件则去找到对应的window
 injectionResult =
 findTouchedWindowTargetsLocked(currentTime, *entry, inputTargets, nextWakeupTime,
 &conflictingPointerActions);
 } else {
 // Non touch event. (eg. trackball)
 injectionResult =
 findFocusedWindowTargetsLocked(currentTime, *entry, inputTargets, nextWakeupTime);
 }
 if (injectionResult == InputEventInjectionResult::PENDING) {
 return false;
 }

 setInjectionResult(*entry, injectionResult);
 ...

 // Dispatch the motion.
 dispatchEventLocked(currentTime, entry, inputTargets);
 return true;
}

这个方法中依然是对于事件做很多的处理和判断,比如否要抛弃等。但是其中最终要的是调用findFocusedWIndowTargetsLocked来找到我们的事件所对应的Window,并且保存相关信息到inputTargets当中,这里获取inputTargets的过程比较复杂,但是简单来说呢就是从之前我们保存在InputDispatcher中的mConnectionsByToken中查找到对应的条目,这里暂不深入分析。拿到这个之后就是调用dispatchEventLocked去分发,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void InputDispatcher::dispatchEventLocked(nsecs_t currentTime,
 std::shared_ptr<EventEntry> eventEntry,
 const std::vector<InputTarget>& inputTargets) {
 ...
 for (const InputTarget& inputTarget : inputTargets) {
 sp<Connection> connection =
 getConnectionLocked(inputTarget.inputChannel->getConnectionToken());
 if (connection != nullptr) {
 prepareDispatchCycleLocked(currentTime, connection, eventEntry, inputTarget);
 } else {

 }
 }
}

通过这里我们可以看到首先是通过inputTarget去拿到connectionToken,再通过它拿到Connection。最后通过调用prepareDispatchCycleLocked

1
2
3
4
5
6
7
void InputDispatcher::prepareDispatchCycleLocked(nsecs_t currentTime,
 const sp<Connection>& connection,
 std::shared_ptr<EventEntry> eventEntry,
 const InputTarget& inputTarget) {
 ...
 enqueueDispatchEntriesLocked(currentTime, connection, eventEntry, inputTarget);
}

这个方法简化的化,这是调用第6行的这个方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void InputDispatcher::enqueueDispatchEntriesLocked(nsecs_t currentTime,
 const sp<Connection>& connection,
 std::shared_ptr<EventEntry> eventEntry,
 const InputTarget& inputTarget) {

 bool wasEmpty = connection->outboundQueue.empty();

 // Enqueue dispatch entries for the requested modes.
 enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,
 InputTarget::FLAG_DISPATCH_AS_HOVER_EXIT);
 enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,
 InputTarget::FLAG_DISPATCH_AS_OUTSIDE);
 enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,
 InputTarget::FLAG_DISPATCH_AS_HOVER_ENTER);
 enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,
 InputTarget::FLAG_DISPATCH_AS_IS);
 enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,
 InputTarget::FLAG_DISPATCH_AS_SLIPPERY_EXIT);
 enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,
 InputTarget::FLAG_DISPATCH_AS_SLIPPERY_ENTER);

 // If the outbound queue was previously empty, start the dispatch cycle going.
 if (wasEmpty && !connection->outboundQueue.empty()) {
 startDispatchCycleLocked(currentTime, connection);
 }
}

其中对于消息会尝试按照每一种mode都调用enqueueDIspatchEntryLocked方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
void InputDispatcher::enqueueDispatchEntryLocked(const sp<Connection>& connection,
 std::shared_ptr<EventEntry> eventEntry,
 const InputTarget& inputTarget,
 int32_t dispatchMode) {

 int32_t inputTargetFlags = inputTarget.flags;
 if (!(inputTargetFlags & dispatchMode)) {
 return;
 }
 inputTargetFlags = (inputTargetFlags & ~InputTarget::FLAG_DISPATCH_MASK) | dispatchMode;

 std::unique_ptr<DispatchEntry> dispatchEntry =
 createDispatchEntry(inputTarget, eventEntry, inputTargetFlags);

 EventEntry& newEntry = *(dispatchEntry->eventEntry);
 // Apply target flags and update the connection's input state.
 switch (newEntry.type) {
 case EventEntry::Type::MOTION: {
 const MotionEntry& motionEntry = static_cast<const MotionEntry&>(newEntry);
 constexpr int32_t DEFAULT_RESOLVED_EVENT_ID =
 static_cast<int32_t>(IdGenerator::Source::OTHER);
 dispatchEntry->resolvedEventId = DEFAULT_RESOLVED_EVENT_ID;
 ...


 if ((motionEntry.flags & AMOTION_EVENT_FLAG_NO_FOCUS_CHANGE) &&
 (motionEntry.policyFlags & POLICY_FLAG_TRUSTED)) {
 break;
 }

 dispatchPointerDownOutsideFocus(motionEntry.source, dispatchEntry->resolvedAction,
 inputTarget.inputChannel->getConnectionToken());
 break;
 }
 ...
 }
 connection->outboundQueue.push_back(dispatchEntry.release());
 traceOutboundQueueLength(*connection);
}

在这个方法中,又把事件封装成为dispatchEntry,并放到Connection内部的outboundQueue这个队列当中。

到这里我们可以回看上面的enqueueDispatchEntriesLocked的最后一块代码,那里有判断了如果这个outboundQueue队列不为空,则会执行最后的startDispatchCycleLocked,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
void InputDispatcher::startDispatchCycleLocked(nsecs_t currentTime,
 const sp<Connection>& connection) {

 while (connection->status == Connection::Status::NORMAL && !connection->outboundQueue.empty()) {
 DispatchEntry* dispatchEntry = connection->outboundQueue.front();
 dispatchEntry->deliveryTime = currentTime;
 ...
 // Publish the event.
 status_t status;
 const EventEntry& eventEntry = *(dispatchEntry->eventEntry);
 switch (eventEntry.type) {
 ...
 case EventEntry::Type::MOTION: {
 const MotionEntry& motionEntry = static_cast<const MotionEntry&>(eventEntry);
 ...

 std::array<uint8_t, 32> hmac = getSignature(motionEntry, *dispatchEntry);

 status = connection->inputPublisher
 .publishMotionEvent(dispatchEntry->seq,
 dispatchEntry->resolvedEventId,
 motionEntry.deviceId, motionEntry.source,
 motionEntry.displayId, std::move(hmac),
 dispatchEntry->resolvedAction,
 motionEntry.actionButton,
 dispatchEntry->resolvedFlags,
 motionEntry.edgeFlags, motionEntry.metaState,
 motionEntry.buttonState,
 motionEntry.classification,
 dispatchEntry->transform,
 motionEntry.xPrecision, motionEntry.yPrecision,
 motionEntry.xCursorPosition,
 motionEntry.yCursorPosition,
 dispatchEntry->rawTransform,
 motionEntry.downTime, motionEntry.eventTime,
 motionEntry.pointerCount,
 motionEntry.pointerProperties, usingCoords);
 break;
 }
 }
 ...

 }
}

在这个方法中,则是从outboundQueue把所有的事件一条一条的取出来,解包成它要的类型,比如触摸事件就是MotionEntry,经过判断和一些处理之后,调用connection中的inputPublisherpublishMotionEvent方法,这里的inputPublisher我们之前分析创建InputChannel的时候有所了解,创建它所传的InputChannel为Server端的那个。 我们这里看一下它的publishMotionEvent方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
status_t InputPublisher::publishMotionEvent(...) {

 InputMessage msg;
 msg.header.type = InputMessage::Type::MOTION;
 msg.header.seq = seq;
 msg.body.motion.eventId = eventId;
 ...
 msg.body.motion.pointerCount = pointerCount;
 for (uint32_t i = 0; i < pointerCount; i++) {
 msg.body.motion.pointers[i].properties.copyFrom(pointerProperties[i]);
 msg.body.motion.pointers[i].coords.copyFrom(pointerCoords[i]);
 }

 return mChannel->sendMessage(&msg);
}

这里主要创建了InputMessage,将之前MotionEvent的所有参数放进去,通过ServerInputChannel调用sendMessage发送出去,sendMessage的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
status_t InputChannel::sendMessage(const InputMessage* msg) {
 const size_t msgLength = msg->size();
 InputMessage cleanMsg;
 msg->getSanitizedCopy(&cleanMsg);
 ssize_t nWrite;
 do {
 nWrite = ::send(getFd(), &cleanMsg, msgLength, MSG_DONTWAIT | MSG_NOSIGNAL);
 } while (nWrite == -1 && errno == EINTR);

 return OK;
}

我们知道,InputChannel内部的文件描述符为socket的标记,这里调用send方法,也就是通过socket把信息发送出去,这样的话,我们Client端的的socket也就会接收到,通过Looper,Client端的EventListener就可以接收到消息,我们的应用便可以接收到,到这里便把事件分发,完整的串起来了。

总结

以上就是事件在系统进程中的处理,包括它的事件获取,事件处理,最后通过socket发送,这样我们在客户端进程的InputChannel就能够接收到通知,客户端能够处理事件。配合我们之前分析过应用层的事件分发,到这里,不算事件的驱动相关的部分,事件分发的整个流程我们都有所了解了。

在这里事件从系统system_server通过Server的InputChannel发送的客户端的InputChannel,所采用的是unix的socket功能,而不是使用的binder或者其他的跨进程服务。这一块,结合我在网上查找的资料,以及我自己的想法,我想这里这样做的原因是,unix的sockt pair使用上很简单,并且运行效率很高效,不需要像binder一样涉及到进程和线程的切换。另外就是socket使用了fd,目标进程可以直接监听到事件的来临,而不是向binder一样需要有相应的接口涉及,可以更加实时的接收到事件,也不会因为binder线程阻塞而卡顿。

当然这是我的一些想法,也欢迎读者朋友说说你对于这块的想法。

看完评论一下吧

Android源码分析:从源头分析View事件的传递

2024年9月20日 17:20

对于应用开发者的我们来说,经常会处理按钮点击,键盘输入等事件,而我们的处理一般都是在Activity中或者View中去做的。我们在上一篇文章中分析了View和Activity与Window的关系,其中的ViewRootImpl和我们的事件传递息息相关,上文未能分析,本文将对其进行分析。

事件介绍

事件是什么呢,广义上事件的发生可能在软件也可能在硬件层,在Android设备当中,我们会有可能有键盘触发,触摸触发,鼠标触发的各种事件。我们关注的通常有两种事件: 按键事件(KeyEvent): 这种色包括物理的按键,Home键,音量键,也包括软键盘触发的事件。 触摸事件(TouchEvent): 手指在屏幕上触摸触发的事件,可能是点击,也可能是拖动。

对于按键事件,一般有ACTION_DOWNACTION_UP两种状态,对于KeyEvent所支持的所有keyCode,我们都可以在KeyEvent当中找到。

而对于触摸事件来说,除了DOWNUP两种状态之外,还有ACTION_MOVEACTION_CANCEL等状态。

应用层的事件类图如下图所示:

classDiagram
class Parcelable {
<<interface>>
}
class InputEvent {
<<abstract>>
}
class KeyEvent
class MotionEvent
InputEvent<|--KeyEvent
InputEvent<|--MotionEvent
Parcelable<|..KeyEvent
Parcelable<|..MotionEvent
Parcelable<|..InputEvent

事件传递到View

我们一般处理View的onClick事件,而这个事件是在View的onTouchEvent中进行处理并执行的,在View中我们可以向上追溯到dispatchPointerEvent方法当中,这个方法就是外部向View传递事件的调用。我们知道Android的UI界面中的所有View是一个树形的结构,因此这些事件也就会通过dispatchTouchEvent一层一层的往下传,从而每一个View都能够接收到事件,并决定是否处理。

dispatchPointerEvent是在ViewRootImpl当中调用,代码如下:

1
2
3
4
5
6
7
8
9
private int processPointerEvent(QueuedInputEvent q) {
 final MotionEvent event = (MotionEvent)q.mEvent;
 ...
 boolean handled = mView.dispatchPointerEvent(event);
 maybeUpdatePointerIcon(event);
 maybeUpdateTooltip(event);
 ...
 return handled ? FINISH_HANDLED : FORWARD;
}

在Activity中,它的根视图为DecorViewViewRootImpl在执行它的dispatchPointerEvent方法,它再向下把触摸事件依次向下传递。

除了触摸事件,按键事件也是类似,ViewRootImpl当中会调用View的dispatchKeyEvent方法,View当中会做相应的处理或者向下传递。

ViewRootImpl中对事件的处理

对于ViewRootImpl当中是如何获取事件,并且向后传递的,我们这里以触摸事件为主进行分析,其他事件也类似。

ViewRootImpl中,定义写一些内部类,大概如下:

classDiagram
class InputStage {
<<abstract>>
+InputStage mNext;
+deliver(QueuedInputEvent q)
#finish(QueuedInputEvent q, boolean handled)
#forward(QueuedInputEvent q)
#onDeliverToNext(QueuedInputEvent q)
#onProcess(QueuedInputEvent q)
}
class AsyncInputStage {
<<abstract>>
#defer(QueuedInputEvent q)
}
InputStage <|-- AsyncInputStage
AsyncInputStage <|--NativePreImeInputStage
InputStage <|-- ViewPreImeInputStage
AsyncInputStage <|-- ImeInputStage
InputStage <|-- EarlyPostImeInputStage
AsyncInputStage <|-- NativePostImeInputStage
InputStage <|-- ViewPostImeInputStage
InputStage <|--SyntheticInputStage

上面这几个类就ViewRootImpl中处理事件的类,其初始化代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
//ViewRootImpl.java
public void setView(...) {
 ...
 CharSequence counterSuffix = attrs.getTitle();
 mSyntheticInputStage = new SyntheticInputStage();
 InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage);
 InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStage,
 "aq:native-post-ime:" + counterSuffix);
 InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStage);
 InputStage imeStage = new ImeInputStage(earlyPostImeStage,
 "aq:ime:" + counterSuffix);
 InputStage viewPreImeStage = new ViewPreImeInputStage(imeStage);
 InputStage nativePreImeStage = new NativePreImeInputStage(viewPreImeStage,
 "aq:native-pre-ime:" + counterSuffix);

 mFirstInputStage = nativePreImeStage;
 mFirstPostImeInputStage = earlyPostImeStage;
}

以上代码创建了多个InputStage,它们一起组成了输入事件处理的流水线。其中ViewPostImeInputStage中就会处理与触摸相关的事件,它的onProcess方法代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
protected int onProcess(QueuedInputEvent q) {
 if (q.mEvent instanceof KeyEvent) {
 return processKeyEvent(q);
 } else {
 final int source = q.mEvent.getSource();
 if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
 return processPointerEvent(q);
 } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
 return processTrackballEvent(q);
 } else {
 return processGenericMotionEvent(q);
 }
 }
}

可以看到,当我们的输入源为POINTER,触摸屏和鼠标的触发都是这一类。这个时候就会执行上面我们提到的 processPointerEvent方法,之后事件也就会传递到View当中。

这里我们知道了是通过InputStage的流水线拿到的事件,但是这个事件从何处来的呢,我们需要继续向上溯源。

ViewRootImpl从何处获得事件

关于这一点,我们仍然需要关注ViewRootImplsetView方法中的如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
//VieRootImpl.java
InputChannel inputChannel = null;
if ((mWindowAttributes.inputFeatures
 & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
 inputChannel = new InputChannel();
}
...
res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes,
 getHostVisibility(), mDisplay.getDisplayId(), userId,
 mInsetsController.getRequestedVisibilities(), inputChannel, mTempInsets,
 mTempControls, attachedFrame, compatScale);
...
if (inputChannel != null) {

 mInputEventReceiver = new WindowInputEventReceiver(inputChannel,
 Looper.myLooper());

}

在这里,我们创建了一个InputChannel,但是我们创建的InputChannel仅仅是java层的一个类,没法去获取到事件,随后我们调用WindowSessionaddToDisplayAsUser他就会获得mPtr,也就是Native层的InputChannel,具体内容随后再看相关代码。在15行,这里创建了一个WindowInputEventReceiver,它的参数为inputChannelLooper,这里一起看一下InputEventReceiver的构造方法,代码如下:

1
2
3
4
5
6
7
8
public InputEventReceiver(InputChannel inputChannel, Looper looper) {
 mInputChannel = inputChannel;
 mMessageQueue = looper.getQueue();
 mReceiverPtr = nativeInit(new WeakReference<InputEventReceiver>(this),
 inputChannel, mMessageQueue);

 mCloseGuard.open("InputEventReceiver.dispose");
}

InputEventReceiver的初始化

这里主要是调用了nativeInit方法,并且获取到mReceivePtr,native的代码在android_view_InputEventReceiver.cpp当中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
static jlong nativeInit(JNIEnv* env, jclass clazz, jobject receiverWeak,
 jobject inputChannelObj, jobject messageQueueObj) {
 std::shared_ptr<InputChannel> inputChannel =
 android_view_InputChannel_getInputChannel(env, inputChannelObj); //获取Native成的InputChannel
 sp<MessageQueue> messageQueue = android_os_MessageQueue_getMessageQueue(env, messageQueueObj); //获取native层的消息队列

 sp<NativeInputEventReceiver> receiver = new NativeInputEventReceiver(env,
 receiverWeak, inputChannel, messageQueue);
 status_t status = receiver->initialize();
 receiver->incStrong(gInputEventReceiverClassInfo.clazz); // 增加引用计数
 return reinterpret_cast<jlong>(receiver.get());
}

在上面的代码中,先是分别获取了Native层的InputChannel和MessageQueue,之后创建了NativeInputEventReceiver,并且调用了它的initialize方法:

1
2
3
4
status_t NativeInputEventReceiver::initialize() {
 setFdEvents(ALOOPER_EVENT_INPUT);
 return OK;
}

内部调用了setFdEvents方法,参数ALOOPER_EVENT_INPUT,这个参数表示监听文件描述符的读操作,其内部代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void NativeInputEventReceiver::setFdEvents(int events) {
 if (mFdEvents != events) {
 mFdEvents = events;
 int fd = mInputConsumer.getChannel()->getFd();
 if (events) {
 mMessageQueue->getLooper()->addFd(fd, 0, events, this, nullptr);
 } else {
 mMessageQueue->getLooper()->removeFd(fd);
 }
 }
}

这里就是拿到InputChannel的文件描述符,并且添加到Looper中去监听它的输入事件。我们暂时不会去阅读硬件层面的触发,以及事件如何发送到InputChannel当中,这里就大胆的假设,InputChannel当中有一个文件描述符,当有事件发生时候,会写入到这个文件当中去。而文件变化,Looper就会收到通知,事件也就发送出来了。

NativeInputEventReceiver 接收事件并分发

这个时候我们可以看一下NativeInputEventReceiver所实现的LooperCallbackhandleEvent方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
int NativeInputEventReceiver::handleEvent(int receiveFd, int events, void* data) {

 constexpr int REMOVE_CALLBACK = 0;
 constexpr int KEEP_CALLBACK = 1;

 if (events & ALOOPER_EVENT_INPUT) {
 JNIEnv* env = AndroidRuntime::getJNIEnv();
 status_t status = consumeEvents(env, false /*consumeBatches*/, -1, nullptr);
 mMessageQueue->raiseAndClearException(env, "handleReceiveCallback");
 return status == OK || status == NO_MEMORY ? KEEP_CALLBACK : REMOVE_CALLBACK;
 }
 ...
 return KEEP_CALLBACK;
}

其中核心代码如上,就是判断如果事件为ALOOPER_EVENT_INPUT,则会调用consumeEvents方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
status_t NativeInputEventReceiver::consumeEvents(JNIEnv* env,
 bool consumeBatches, nsecs_t frameTime, bool* outConsumedBatch) {
 ...
 ScopedLocalRef<jobject> receiverObj(env, nullptr);
 bool skipCallbacks = false;
 for (;;) {
 uint32_t seq;
 InputEvent* inputEvent;

 status_t status = mInputConsumer.consume(&mInputEventFactory,
 consumeBatches, frameTime, &seq, &inputEvent);
 ...
 assert(inputEvent);

 if (!skipCallbacks) {
 if (!receiverObj.get()) {
 receiverObj.reset(jniGetReferent(env, mReceiverWeakGlobal));
 if (!receiverObj.get()) {
 ...
 return DEAD_OBJECT;
 }
 }

 jobject inputEventObj;
 switch (inputEvent->getType()) {
 case AINPUT_EVENT_TYPE_MOTION: {
 MotionEvent* motionEvent = static_cast<MotionEvent*>(inputEvent);
 if ((motionEvent->getAction() & AMOTION_EVENT_ACTION_MOVE) && outConsumedBatch) {
 *outConsumedBatch = true;
 }
 inputEventObj = android_view_MotionEvent_obtainAsCopy(env, motionEvent);
 break;
 }
 ...
 default:
 assert(false); // InputConsumer should prevent this from ever happening
 inputEventObj = nullptr;
 }

 if (inputEventObj) {
 env->CallVoidMethod(receiverObj.get(),
 gInputEventReceiverClassInfo.dispatchInputEvent, seq, inputEventObj);
 ...
 env->DeleteLocalRef(inputEventObj);
 } else {
 ...
 }
 }
 }
}

上面的代码做过简化,switch的case只保留了一个。首先在第10行,我们看到这里调用了mInputConsumerconsume方法。这个InputConsumer是在Receiver创建的时候创建它,它用于到InputChannel中获取消息,并且按照类型包装成InputEvent的具体子类,并写入到inputEvent当中。在后面的Switch判断处,就可以根据它的类型做处理,从而封装成java类型的InputEvent。而receiverObj在第17行,通过jniGetReferent拿到java层的InputEventReceiver的引用,在41行调用了它的dispatchInputEvent方法,从而调用了java层的同名方法,代码如下:

1
2
3
4
private void dispatchInputEvent(int seq, InputEvent event) {
 mSeqMap.put(event.getSequenceNumber(), seq);
 onInputEvent(event);
}

我们再到WindowInputEventReceiver中看onInputEvent方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public void onInputEvent(InputEvent event) {
 List<InputEvent> processedEvents;
 try {
 processedEvents =
 mInputCompatProcessor.processInputEventForCompatibility(event);
 } finally {
 }
 if (processedEvents != null) {
 if (processedEvents.isEmpty()) {
 finishInputEvent(event, true);
 } else {
 for (int i = 0; i < processedEvents.size(); i++) {
 enqueueInputEvent(
 processedEvents.get(i), this,
 QueuedInputEvent.FLAG_MODIFIED_FOR_COMPATIBILITY, true);
 }
 }
 } else {
 enqueueInputEvent(event, this, 0, true);
 }
}

其中第4行代码,是为了兼容低版本设计的,只有应用的TargetSDKVersion小于23才会生效,这里我们就不关注它了。因此这里就只会执行第19行的代码,其内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
void enqueueInputEvent(InputEvent event,
 InputEventReceiver receiver, int flags, boolean processImmediately) {
 QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags);
 if (event instanceof MotionEvent) {
 MotionEvent me = (MotionEvent) event;
 }
 QueuedInputEvent last = mPendingInputEventTail;
 if (last == null) {
 mPendingInputEventHead = q;
 mPendingInputEventTail = q;
 } else {
 last.mNext = q;
 mPendingInputEventTail = q;
 }
 mPendingInputEventCount += 1;
 if (processImmediately) {
 doProcessInputEvents();
 } else {
 scheduleProcessInputEvents();
 }
}

这里的代码,把我们的Event包装成一个QueuedInputEvent,并且放置到mQueuedInputEventPool这个链表中,具体可以自行看obtainQueuedInputEvent方法。而根据我们之前传递的参数,可以看到这里后面会调用到doProcessInputEvents方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
void doProcessInputEvents() {
 // Deliver all pending input events in the queue. 
 while (mPendingInputEventHead != null) {
 QueuedInputEvent q = mPendingInputEventHead;
 mPendingInputEventHead = q.mNext;
 if (mPendingInputEventHead == null) {
 mPendingInputEventTail = null;
 }
 q.mNext = null;

 mPendingInputEventCount -= 1; mViewFrameInfo.setInputEvent(mInputEventAssigner.processEvent(q.mEvent));

 deliverInputEvent(q);
 }
 //除了我们收到调用来把事件队列的所有事件消费,还有一些消息本来是准备通过Handler发送消息来处理的,既然我们已经手动把所有消息都处理掉了,那么如果有等待处理的消息事件,也就不需要了,下面的代码就是把他们删掉
 if (mProcessInputEventsScheduled) {
 mProcessInputEventsScheduled = false;
 mHandler.removeMessages(MSG_PROCESS_INPUT_EVENTS);
 }
}

这里的代码主要就是遍历之前的链表,把每一条消息都取出来,并且调用deliverInputEvent方法来把它分发掉,同时会把它从链表中删除。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private void deliverInputEvent(QueuedInputEvent q) {
 try {
 if (mInputEventConsistencyVerifier != null) {
 try { //事件一致性检查,避免外面传过来应用无法处理的事件
 mInputEventConsistencyVerifier.onInputEvent(q.mEvent, 0);
 } finally {
 }
 }

 InputStage stage; //选择要使用的入口InputStage
 if (q.shouldSendToSynthesizer()) {
 stage = mSyntheticInputStage;
 } else {
 stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage;
 }
 ...
 if (stage != null) {
 handleWindowFocusChanged();
 stage.deliver(q); //InputStage 开始分发事件
 } else {
 finishInputEvent(q);
 }
 } finally {

 }
}

在这个方法中,主要就是根据事件的属性选择入口的InputStage,之后调用它的deliver方法,在这个方法中就会按照链式调用,最终能够处理掉的一个InputStage会将它处理,也就是把事件分发到应用中去。

到这里我们就完成了从InputChannel中获取事件,并且通过InputEventReceiver传递到Java层,并且通过InputStage转发到应用的View当中。

InputChannel的初始化

刚刚我们已经基本把事件处理在ViewRootImpl中的部分看完了,而我们在其中创建的InputChannel只是一个壳,想要看看它的真正的初始化,我们沿着之前调用的addToDisplayAsUser继续往后看。IWindowSession是一个AIDL定义的Binder服务,在它的定义中InputChannel使用了out进行修饰,表示它会被binder服务端修改,并写入数据。而这个addToDisplayAsUser方法内部最终会调用WMS的addWindow方法,其中和InputChannel相关代码如下:

1
2
3
4
5
6
7
8
final WindowState win = new WindowState(this, session, client, token, parentWindow,
 appOp[0], attrs, viewVisibility, session.mUid, userId,
 session.mCanAddInternalSystemWindow);
 final boolean openInputChannels = (outInputChannel != null
 && (attrs.inputFeatures & INPUT_FEATURE_NO_INPUT_CHANNEL) == 0);
if (openInputChannels) {
 win.openInputChannel(outInputChannel);
}

这里outInputChannel就是我们从客户端传过来的那个InputChannel的壳,随后便调用了WindowStateopenInputChannel方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void openInputChannel(InputChannel outInputChannel) {
 String name = getName(); //获取window的name
 mInputChannel = mWmService.mInputManager.createInputChannel(name); //创建
 mInputChannelToken = mInputChannel.getToken();
 mInputWindowHandle.setToken(mInputChannelToken);
 mWmService.mInputToWindowMap.put(mInputChannelToken, this);
 if (outInputChannel != null) {
 mInputChannel.copyTo(outInputChannel); //将Native Channel写入我们传入的InputChannel
 } else {
 }
}

这里就是调用InputManager去创建InputChannel,并且把它和我们的WIndow关联,以及保存到我们传入的InputChannel当中,这样我们的View层面就可以通过InputChannel获取到事件了。InputManagerService创建InputChannel的部分这里就不讨论了,留待以后讨论。

总结

到此为止,就分析完了应用侧从WMS到View,如何初始化InputEventReceiver,InputEventReceiver和InputChannel关联起来,事件如何从InputChannel一直传递到我们的View的了。

sequenceDiagram
InputChannel-->>NativeInputEventReceiver: handleEvent
note right of InputChannel: notify has event via Looper
NativeInputEventReceiver->> NativeInputEventReceiver: consumeEvents
NativeInputEventReceiver->>+ InputChannel: consume
note right of InputChannel: get event from InputChannel
InputChannel-->>-NativeInputEventReceiver: return inputEvent
NativeInputEventReceiver->>InputEventReceiver: dispatchInputEvent
InputEventReceiver->>InputEventReceiver: onInputEvent
InputEventReceiver->>ViewRootImpl: enqueueInputEvent
ViewRootImpl->>ViewRootImpl: doProcessInputEvents
ViewRootImpl->>ViewRootImpl: deliverInputEvent
ViewRootImpl->>+ViewPostImeInputStage: deliver
ViewPostImeInputStage->>ViewPostImeInputStage:onProcess
ViewPostImeInputStage->>ViewPostImeInputStage: processPointerEvent
ViewPostImeInputStage-->>+View: dispatchPointerEvent
View->>View:dispatchTouchEvent
View->>View: onTouch
View-->>-ViewPostImeInputStage: return is consume it or not
ViewPostImeInputStage-->>-ViewRootImpl: finish deliver

之前的分析涉及到了InputChannel的初始化和InputEventReceiver的初始化,直接看可以会比较绕人,上面从事件分发角度画了一下事件从InputChannel一直流转到View的一个时序图,希望对于你理解这个流程有所理解。如果哪里存在疏漏,也欢迎读者朋友们评论指点。

看完评论一下吧

Android源码分析:Window与Activity与View的关联

2024年9月19日 21:30

Activity是四大组件中和UI相关的那个,应用开发过程中,我们所有的界面基本都需要使用Activity才能去渲染和绘制UI,即使是ReactNative,Flutter这种跨平台的方案,在Android中,也需要一个Activity来承载。但是我们的Activity内我们设置的View又是怎么渲染到屏幕上的呢,这背后又有WindowManager和SurfaceFlinger来进行工作。本文就来看看WindowManger如何管理Window,以及Window如何与Activity产生关系的呢。

Activity与Window的初见

Activity的创建是在ActivityThreadperformLaunchActivity中,这里会创建要启动的Activity,并且会调用Activity的attach方法,在这个方法当中就会创建Window,其中和Window相关的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(mWindowControllerCallback);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
 mWindow.setSoftInputMode(info.softInputMode);
}
if (info.uiOptions != 0) {
 mWindow.setUiOptions(info.uiOptions);
}
mWindow.setWindowManager(
 (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
 mToken, mComponent.flattenToString(),
 (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
if (mParent != null) {
 mWindow.setContainer(mParent.getWindow());
}
mWindowManager = mWindow.getWindowManager();

这里我们可以看到为Activity创建了Window,目前Android上面的Window实例为PhoneWindow,同时还给Window设置了WindowManager,不过这里的WindowManager仅仅是一个本地服务,它的实现为WindowManagerImpl,它的注册代码在SystemServiceRegister.java中,代码如下:

1
2
3
4
5
6
registerService(Context.WINDOW_SERVICE, WindowManager.class,
 new CachedServiceFetcher<WindowManager>() {
 @Override
 public WindowManager createService(ContextImpl ctx) {
 return new WindowManagerImpl(ctx);
 }});

而我们这个WindowManagerImpl内部持有持有了一个WindowManagerGlobal,看名字就知道它应该会涉及到跨进程通讯,去看它代码就知道它内部有两个成员,分别是sWindowManagerServicesWindowSession,这两个成员就用于跨进程通讯。这里我们先知道有这几个类,后面到用处再继续分析。

---
title: WindowManager相关类图
---
classDiagram
directioni TB
class ViewManager {
<<interface>>
addView(view, params)
updateViewLayout(view, params)
removeView(view)
}
class WindowManager {
<<interface>>
}
class WindowManagerImpl {
- WindowManagerGlobal mGlobal;
- IBinder mWindowContextToken;
- IBinder mDefaultToken;
}
ViewManager <|-- WindowManager
WindowManager <|.. WindowManagerImpl
class WindowManagerGlobal {
IwindowManager sWindowManagerService;
IWindowSession SwindowSession;
}
WindowManagerImpl ..> WindowManagerGlobal
class Window {
<<abstract>>
WindowManager mWindowManager;
}
Window <|.. PhoneWindow
Window ..> WindowManager
class IWindow {
<<Interface>>
}
IWindow <|--W
class ViewRootImpl {
W mWindow
IWindowSession mWindowSession
}
ViewRootImpl .. W
ViewRootImpl .. IWindowSession
IWindowSession .. W

这里只可出了App进程相关的一些类,System_Server相关未列出,后面涉及到相关部分的时候再进行分析。

Window与View的邂逅

我们一般情况下会在Activity的onCreate当中去调用setContentView,只有这样我们的View才能够显示出来。因此我们直接看这个方法的调用:

1
2
3
4
public void setContentView(@LayoutRes int layoutResID) {
 getWindow().setContentView(layoutResID);
 initWindowDecorActionBar();
}

其中就是调用了window的同名方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public void setContentView(int layoutResID) {
 if (mContentParent == null) {
 installDecor();
 } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
 mContentParent.removeAllViews();
 }

 if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
 final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
 getContext());
 transitionTo(newScene); //执行页面Transition动画
 } else {
 mLayoutInflater.inflate(layoutResID, mContentParent);
 }
 mContentParent.requestApplyInsets();
 final Callback cb = getCallback();
 if (cb != null && !isDestroyed()) {
 cb.onContentChanged();
 }
 mContentParentExplicitlySet = true;
}

这里我们主要是将我们的ContentView添加到mContentParent当中去,这个mContentParent有可能为空,需要我们通过installDecor来创建,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
private void installDecor() {
 mForceDecorInstall = false;
 if (mDecor == null) {
 mDecor = generateDecor(-1);
 mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
 mDecor.setIsRootNamespace(true);
 if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
 mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
 }
 } else {
 mDecor.setWindow(this);
 }
 if (mContentParent == null) {
 mContentParent = generateLayout(mDecor);

 // Set up decor part of UI to ignore fitsSystemWindows if appropriate. 
 mDecor.makeFrameworkOptionalFitsSystemWindows();

 final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
 R.id.decor_content_parent);

 if (decorContentParent != null) {
 mDecorContentParent = decorContentParent;
 mDecorContentParent.setWindowCallback(getCallback());
 if (mDecorContentParent.getTitle() == null) {
 mDecorContentParent.setWindowTitle(mTitle);
 }

 final int localFeatures = getLocalFeatures();
 for (int i = 0; i < FEATURE_MAX; i++) {
 if ((localFeatures & (1 << i)) != 0) {
 mDecorContentParent.initFeature(i);
 }
 }

 mDecorContentParent.setUiOptions(mUiOptions);

 ...

 PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false);
 if (!isDestroyed() && (st == null || st.menu == null) && !mIsStartingWindow) {
 invalidatePanelMenu(FEATURE_ACTION_BAR);
 }
 } else {
 mTitleView = findViewById(R.id.title);
 if (mTitleView != null) {
 //title view的设置
 }
 }

 if (mDecor.getBackground() == null && mBackgroundFallbackDrawable != null) {
 mDecor.setBackgroundFallback(mBackgroundFallbackDrawable);
 }

 if (hasFeature(FEATURE_ACTIVITY_TRANSITIONS)) {
 ...
 //页面动画的读取和设置 
 }
 }
}

这里我们可以看到主要做的就是创建了decorView和ContentParent,还有一些动画,标题之类的初始化我们这里就跳过了。DecorView就是App Activity页面最底层的容器,它为我们封装了状态栏,底部导航栏,App页面的内容的展示。而ContentParent的初始化,则是根据Activity的设置,根据是否展示状态栏,是否展示标题栏等,进行加载相应的布局,加载到DecorView当中,最后com.android.internal.R.id.content对应的FrameLayout就会成为ContentParent。 当这一切做完,我们的页面View就成功的添加到Window当中了,但是它是如何展示出来的呢,还需要继续往后看。我们需要前往ActivityThread的handleResumeActivity方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//调用Activity的onResume方法
if (!performResumeActivity(r, finalStateRequest, reason)) {
 return;
}
//r为ActivityClientRecord
final Activity a = r.activity;
//检查当前的Activity是否能显示
boolean willBeVisible = !a.mStartedActivity;
if (!willBeVisible) {
 willBeVisible = ActivityClient.getInstance().willActivityBeVisible(
 a.getActivityToken());
}
if (r.window == null && !a.mFinished && willBeVisible) {
 r.window = r.activity.getWindow(); //把activity的window保存到r.window中
 View decor = r.window.getDecorView();
 decor.setVisibility(View.INVISIBLE);
 ViewManager wm = a.getWindowManager();
 WindowManager.LayoutParams l = r.window.getAttributes();
 a.mDecor = decor;
 l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
 l.softInputMode |= forwardBit;
 if (r.mPreserveWindow) {
 a.mWindowAdded = true;
 r.mPreserveWindow = false;
 ViewRootImpl impl = decor.getViewRootImpl();
 if (impl != null) {
 impl.notifyChildRebuilt();
 }
 }
 if (a.mVisibleFromClient) {
 if (!a.mWindowAdded) {
 a.mWindowAdded = true;
 wm.addView(decor, l); //调用windowManager添加decorView
 } else {
 a.onWindowAttributesChanged(l);
 }
 }
} else if (!willBeVisible) {
 r.hideForNow = true;
}

可以看到上面的代码把window保存到了ActivityClientRecord当中,同时调用了WindowManager的addView方法,去添加view。我们继续往后看代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) {
 ViewRootImpl impl = r.window.getDecorView().getViewRootImpl();
 WindowManager.LayoutParams l = impl != null
 ? impl.mWindowAttributes : r.window.getAttributes();
 if ((l.softInputMode
 & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)
 != forwardBit) {
 l.softInputMode = (l.softInputMode
 & (~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION))
 | forwardBit;
 if (r.activity.mVisibleFromClient) {
 ViewManager wm = a.getWindowManager();
 View decor = r.window.getDecorView();
 wm.updateViewLayout(decor, l);
 }
 }

 r.activity.mVisibleFromServer = true;
 mNumVisibleActivities++;
 if (r.activity.mVisibleFromClient) {
 r.activity.makeVisible();
 }

 if (shouldSendCompatFakeFocus) {
 if (impl != null) {
 impl.dispatchCompatFakeFocus();
 } else {
 r.window.getDecorView().fakeFocusAfterAttachingToWindow();
 }
 }
}

上面的代码中,我们看到主要做了两件事情,一个是调用updateViewLayout去更新视图的属性,但是updateViewLayout也要属性发生变化,并且有输入法的时候才会执行,另外就是调用activity的makeVisible方法去展示View。

这个过程我们需要分析如下两步。

  1. 调用addView添加decorView
  2. 调用activity.makeVisible来显示 我们分别看一下这两个方法的实现

WMS与ViewRootImpl的遇见:调用WindowManger的addView

1
2
3
4
5
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
 applyTokens(params);
 mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
 mContext.getUserId());
}

这里就是调用mGlobaladdView方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public void addView(View view, ViewGroup.LayoutParams params,
 Display display, Window parentWindow, int userId) {

 final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
 ....

 ViewRootImpl root;
 View panelParentView = null;

 synchronized (mLock) {

 int index = findViewLocked(view, false);
 ...

 if (windowlessSession == null) {
 root = new ViewRootImpl(view.getContext(), display);
 } else {
 root = new ViewRootImpl(view.getContext(), display,
 windowlessSession, new WindowlessWindowLayout());
 }

 view.setLayoutParams(wparams);

 mViews.add(view);
 mRoots.add(root);
 mParams.add(wparams);

 try {
 root.setView(view, wparams, panelParentView, userId);
 } catch (RuntimeException e) {
 final int viewIndex = (index >= 0) ? index : (mViews.size() - 1);
 // BadTokenException or InvalidDisplayException, clean up. 
 if (viewIndex >= 0) {
 removeViewLocked(viewIndex, true);
 }
 throw e;
 }
 }
}

在正常的App页面,windowlessSession会一直为空,这里就会创建一个ViewRootImpl,并且把我们的DecorView以及WindowParams都传进去。并且viewrootwparams都会按照顺序存到List当中。这里我们需要去看ViewRootImpl的setView方法,其中和添加到屏幕相关的代码如下:

1
2
3
4
5
6
7
requestLayout(); //测量布局
res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes,
 getHostVisibility(), mDisplay.getDisplayId(), userId,
 mInsetsController.getRequestedVisibilities(), inputChannel, mTempInsets,
 mTempControls, attachedFrame, compatScale);
...
view.assignParent(this); //将ViewRootImpl设置为DecorView的parent

这里的mDisplay为外面从Context中所获取的,用于指定当前的UI要显示到哪一个显示器上去。这里的mWindowSession的获取代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//WindowManagerGlobal.java
@UnsupportedAppUsage
public static IWindowSession getWindowSession() {
 synchronized (WindowManagerGlobal.class) {
 if (sWindowSession == null) {
 try {
 @UnsupportedAppUsage
 InputMethodManager.ensureDefaultInstanceForDefaultDisplayIfNecessary();
 IWindowManager windowManager = getWindowManagerService();
 sWindowSession = windowManager.openSession(
 new IWindowSessionCallback.Stub() {
 @Override
 public void onAnimatorScaleChanged(float scale) {
 ValueAnimator.setDurationScale(scale);
 }
 });
 } catch (RemoteException e) {
 throw e.rethrowFromSystemServer();
 }
 }
 return sWindowSession;
 }
}

可以看到就是通过IWindowManger这个Binder服务调用了openSession来获取了一个WindowSession。其代码如下:

1
2
3
4
//WindowManagerService.java
public IWindowSession openSession(IWindowSessionCallback callback) {
 return new Session(this, callback);
}

在System_Server端,创建了一个Session对象来提供相关的服务。它的addToDisplayAsUser方法又调用了WMSaddWindow方法,这个方法比较长我们只看其中和UI展示相关的部分,并且UI类型不是App的普通UI的也都给省略掉。

1
2
3
int res = mPolicy.checkAddPermission(attrs.type, isRoundedCornerOverlay, attrs.packageName,
 appOp);
final DisplayContent displayContent = getDisplayContentOrCreate(displayId, attrs.token);

第一行代码首先是去检查我们当前要展示的view,它的类型是否支持去展示。第3行代码的内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
private DisplayContent getDisplayContentOrCreate(int displayId, IBinder token) {
 if (token != null) {
 final WindowToken wToken = mRoot.getWindowToken(token);
 if (wToken != null) {
 return wToken.getDisplayContent();
 }
 }

 return mRoot.getDisplayContentOrCreate(displayId);
}

mRoot为一个RootWindowContainer对象,之前我们在分析Activity的启动过程中已经见到了它,我们的ActivityRecord和Task都存在它当中。这里wToken初始情况一般为null因此会执行下面的getDisplayContentOrCreate方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
DisplayContent getDisplayContentOrCreate(int displayId) {
 DisplayContent displayContent = getDisplayContent(displayId);
 if (displayContent != null) {
 return displayContent;
 }
 ...
 final Display display = mDisplayManager.getDisplay(displayId);
 ...
 displayContent = new DisplayContent(display, this);
 addChild(displayContent, POSITION_BOTTOM);
 return displayContent;
}

这里就是根据displayId从列表中去拿DisplayContent如果不存在就去创建一个并且保存到列表中,方便下次使用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
//WindowManagerService.addWindow
WindowToken token = displayContent.getWindowToken(
 hasParent ? parentWindow.mAttrs.token : attrs.token);
if (token == null) {
{
 ...
 final IBinder binder = attrs.token != null ? attrs.token : client.asBinder();
 token = new WindowToken.Builder(this, binder, type)
 .setDisplayContent(displayContent)
 .setOwnerCanManageAppTokens(session.mCanAddInternalSystemWindow)
 .setRoundedCornerOverlay(isRoundedCornerOverlay)
 .build();
}

继续看addWindow的内容,如果displayContent是新创建的,那么这里拿到的token就会为空,因此这里调用了client.asBinder来获取IBinder,或者直接拿’attr’中的token,这个client为IWindow类型,它在应用侧为W的实例,它是ViewRootImpl的一个内部类。这里创建完WindowToken之后,我们可以继续往后看。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
//WindowManagerService.addWindow
final WindowState win = new WindowState(this, session, client, token, parentWindow,
 appOp[0], attrs, viewVisibility, session.mUid, userId,
 session.mCanAddInternalSystemWindow);
final DisplayPolicy displayPolicy = displayContent.getDisplayPolicy();
displayPolicy.adjustWindowParamsLw(win, win.mAttrs);
attrs.flags = sanitizeFlagSlippery(attrs.flags, win.getName(), callingUid, callingPid);
attrs.inputFeatures = sanitizeSpyWindow(attrs.inputFeatures, win.getName(), callingUid,
 callingPid);
win.setRequestedVisibilities(requestedVisibilities);

res = displayPolicy.validateAddingWindowLw(attrs, callingPid, callingUid);

这里创建的WindowState用于保存Window的状态,可以说是Window在WMS中存储的一个章台。随后从DisplayContent中拿到了DisplayPolicy这个类主要是用于控制显示的一些行为,比如状态栏,导航栏的显示状态之类的。这里会根据WindowParams来调整DisplayPolicy的参数,以及调用validateAddingWindowLw检查当前的window是否能够添加的系统界面中,这个app普通type不涉及。

1
2
3
4
//WindowManagerService.addWindow
win.attach();
mWindowMap.put(client.asBinder(), win);
win.initAppOpsState();

win.attach方法如下:

1
2
3
void attach() {
 mSession.windowAddedLocked();
}

其中就调用了SessionwindowAddedLocked方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
void windowAddedLocked() {
 if (mPackageName == null) {
 final WindowProcessController wpc = mService.mAtmService.mProcessMap.getProcess(mPid);
 if (wpc != null) {
 mPackageName = wpc.mInfo.packageName;
 mRelayoutTag = "relayoutWindow: " + mPackageName;
 } else {
 }
 }
 if (mSurfaceSession == null) {
 mSurfaceSession = new SurfaceSession();
 mService.mSessions.add(this);
 if (mLastReportedAnimatorScale != mService.getCurrentAnimatorScale()) {
 mService.dispatchNewAnimatorScaleLocked(this);
 }
 }
 mNumWindow++;
}

对于每个进程第一次使用openSession创建的Session这个地方都会执行,主要就是创建了SurfaceSession,并且保存到WMSmSessions当中去。之后又把client作为key,WindowState为value存放到mWindowMap当中。

1
2
3
//WindowManagerService.addWindow
win.mToken.addWindow(win);
displayPolicy.addWindowLw(win, attrs);

先看这个WindowToken.addWindow方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void addWindow(final WindowState win) {
 if (mSurfaceControl == null) {
 createSurfaceControl(true /* force */);

 reassignLayer(getSyncTransaction());
 }
 if (!mChildren.contains(win)) {
 addChild(win, mWindowComparator);
 mWmService.mWindowsChanged = true;
 }
}

这里创建了一个SurfaceControl,并且保存到了WindowList当中去。随后再看displayyPolicy.addWindowLw,其中主要用于处理inset相关的处理,这里也先跳过。到此位置addView的代码基本就看完了。

调用activity.makeVisible来显示

1
2
3
4
5
6
7
8
void makeVisible() {
 if (!mWindowAdded) {
 ViewManager wm = getWindowManager();
 wm.addView(mDecor, getWindow().getAttributes());
 mWindowAdded = true;
 }
 mDecor.setVisibility(View.VISIBLE);
}

我们之前已经分析过addView了,这里mWindowAdded也是为true,这里的addView因此是不会被执行的。我们看一下下面的setVisibility,这个就是我们的普通View的方法,还是直接看源码:

1
2
3
4
//View.java
public void setVisibility(@Visibility int visibility) {
 setFlags(visibility, VISIBILITY_MASK);
}

这里是直接调用了setFlags方法,其中和设置显示相关的部分如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
final int newVisibility = flags & VISIBILITY_MASK;
if (newVisibility == VISIBLE) {
 if ((changed & VISIBILITY_MASK) != 0) {
 mPrivateFlags |= PFLAG_DRAWN;
 invalidate(true);

 needGlobalAttributesUpdate(true);
 shouldNotifyFocusableAvailable = hasSize();
 }
}

if ((changed & VISIBILITY_MASK) != 0) {
 if (mParent instanceof ViewGroup) {
 ViewGroup parent = (ViewGroup) mParent;
 parent.onChildVisibilityChanged(this, (changed & VISIBILITY_MASK),
 newVisibility);
 parent.invalidate(true);
 } else if (mParent != null) {
 mParent.invalidateChild(this, null);
 }
}

DecorView的parent为ViewRootImpl,因此上面会调用ViewRootImplinvalidateChild方法,内部会调用如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
 checkThread();

 if (dirty == null) {
 invalidate();
 return null;
 } else if (dirty.isEmpty() && !mIsAnimating) {
 return null;
 }

 if (mCurScrollY != 0 || mTranslator != null) {
 mTempRect.set(dirty);
 dirty = mTempRect;
 if (mCurScrollY != 0) {
 dirty.offset(0, -mCurScrollY);
 }
 if (mTranslator != null) {
 mTranslator.translateRectInAppWindowToScreen(dirty);
 }
 if (mAttachInfo.mScalingRequired) {
 dirty.inset(-1, -1);
 }
 }

 invalidateRectOnScreen(dirty);

 return null;
}

这段代码会检查需要从新绘制的区域,并且放在dirty当中,最后调用invalidateRectOnScreen方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
private void invalidateRectOnScreen(Rect dirty) {
 final Rect localDirty = mDirty;

 // Add the new dirty rect to the current one 
 localDirty.union(dirty.left, dirty.top, dirty.right, dirty.bottom);
 final float appScale = mAttachInfo.mApplicationScale;
 final boolean intersected = localDirty.intersect(0, 0,
 (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
 if (!intersected) {
 localDirty.setEmpty();
 }
 if (!mWillDrawSoon && (intersected || mIsAnimating)) {
 scheduleTraversals();
 }
}

这里仍然检查dirty区域,并且去做Traversal。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void scheduleTraversals() {
 if (!mTraversalScheduled) {
 mTraversalScheduled = true;
 mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
 mChoreographer.postCallback(
 Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
 notifyRendererOfFramePending();
 pokeDrawLockIfNeeded();
 }
}

这里就是启动线程去不断的页面的刷新重绘,就不分析了。最终会执行到performTraversals方法,其中有如下代码我们比较关注:

1
2
3
4
if (mFirst || windowShouldResize || viewVisibilityChanged || params != null
 || mForceNextWindowRelayout) {
 relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
}

当首次执行这个方法的时候mFirst为true,除了这个条件之外,window需要从新计算size,view的可见性变化,windowParams变化等任一条件满足就会执行这里。我们在继续看里面的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
if (relayoutAsync) {
 mWindowSession.relayoutAsync(mWindow, params,
 requestedWidth, requestedHeight, viewVisibility,
 insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, mRelayoutSeq,
 mLastSyncSeqId);
} else {
 relayoutResult = mWindowSession.relayout(mWindow, params,
 requestedWidth, requestedHeight, viewVisibility,
 insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, mRelayoutSeq,
 mLastSyncSeqId, mTmpFrames, mPendingMergedConfiguration, mSurfaceControl,
 mTempInsets, mTempControls, mRelayoutBundle);
 ...
}

当view为本地进行Layout且一些其他的条件符合,并且它的位置大小没有变化的时候,才会是relayoutAsync,不过两个最终的在服务端都会调用relayout方法,区别就是这里relayout的时候传过去了一个mSurfaceControl,这个接口是AIDL定义的,这个参数定义的为out,服务端会传输值到这个对象里,我们随后会看到,因为非异步是大多数情况的调用,这里也对他进行分析。在Session的relayout方法中调用了如下代码:

1
2
3
4
int res = mService.relayoutWindow(this, window, attrs,
 requestedWidth, requestedHeight, viewFlags, flags, seq,
 lastSyncSeqId, outFrames, mergedConfiguration, outSurfaceControl, outInsetsState,
 outActiveControls, outSyncSeqIdBundle);

这里就是调用了WMSrelayoutWindow方法,其中我们关注的有一下代码:

1
2
3
4
5
6
7
8
9
final WindowState win = windowForClientLocked(session, client, false);
if (shouldRelayout && outSurfaceControl != null) {
 try {
 result = createSurfaceControl(outSurfaceControl, result, win, winAnimator);
 } catch (Exception e) {
 ...
 return 0;
 }
}

为应用提供画布容器

这里看一下这个createSurfaceControl的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
WindowSurfaceController surfaceController;
try {
 surfaceController = winAnimator.createSurfaceLocked();
} finally {

}
if (surfaceController != null) {
 surfaceController.getSurfaceControl(outSurfaceControl);

} else {
 outSurfaceControl.release();
}

第三行主要是创建一个WindowSurfaceController对象,第8行则是使用这个对象去获取SurfaceControl,我们看一下它的代码:

1
2
3
void getSurfaceControl(SurfaceControl outSurfaceControl) {
 outSurfaceControl.copyFrom(mSurfaceControl, "WindowSurfaceController.getSurfaceControl");
}

SurfaceControlcopyFrom方法代码如下:

1
2
3
4
5
6
7
public void copyFrom(@NonNull SurfaceControl other, String callsite) {
 mName = other.mName;
 mWidth = other.mWidth;
 mHeight = other.mHeight;
 mLocalOwnerView = other.mLocalOwnerView;
 assignNativeObject(nativeCopyFromSurfaceControl(other.mNativeObject), callsite);
}

最主要的是最后的assignNativeObject赋值到我们从app进程传过来的SurfaceControl当中。native层的SurfaceControl有如下几个成员变量:

1
2
3
4
5
6
sp<SurfaceComposerClient> mClient;
sp<IBinder> mHandle;
sp<IGraphicBufferProducer> mGraphicBufferProducer;
mutable Mutex mLock;
mutable sp<Surface> mSurfaceData;
mutable sp<BLASTBufferQueue> mBbq;

其中就有Surface,而我们在服务端拿到的这个SurfaceControl随后会写回客户端,这样App进程就可以把UI元素绘制到这个Surface上面了。

前面有列过客户端WindowManager相关的类,这里在列一下system_server进程中相关的类:

classDiagram
class IWindowManager {
<<interface>>
}
class Stub["IWindowManager.Stub"]
IWindowManager <|..Stub
class WindowManagerService {
WindowManagerPolicy mPolicy
ArraySet~Session~ mSessions
HashMap~IBinder, WindowState~ mWindowMap
RootWindowContainer mRoot
}
Stub <|--WindowManagerService
class IWindowSession {
<<Interface>>
}
class SessionStub["IWindowSession.Stub"] {
<<abstract>>
}
class Session {
WindowManagerService mService
}
IWindowSession <|..SessionStub
SessionStub<|--Session
WindowContainer<|--RootWindowContainer
WindowContainer <|-- WindowToken
WindowContainer <|--WindowState
WindowToken .. WindowManagerService
Session .. WindowManagerService
RootWindowContainer .. WindowManagerService

总结

我们在调用WMS的addWindow的时候,并没有把View直接传过来,所传过来的WindowLayoutParams当中,宽和高是比较重要的信息,因为在对调用这个方法之前,代码中先是执行了requestLayout去测量的布局的尺寸,并且在返回参数中通过Rect返回了画布的尺寸。我们也知道通过SurfaceControl为我们提供了Surface,这样客户端就能够把UI数据写上去了。而这样,这个Window与View就能够与系统的其他服务一起,把我们的UI显示到屏幕上了。

在与WMS初始通信的时候,WMS服务端为App创建了Session这个对象,App通过这个对象来与服务端进行Binder通讯。同时,App进程在创建ViewRootImpl的时候创建了W这个对象,它是IWindow的binder对象,服务端可以通过这个对象来与app进程通讯。为了方便理解,关于服务端和客户端,我又画了如下图,希望对你理解它们有所帮助。

以上就应用的窗口与Activity相关的分析,整体流程还是比较复杂的,如果哪里存在疏漏,也欢迎读者朋友们评论指点。另外关于应用的事件分发也会涉及到WMS和ViewRootImpl,为了使得文章不至于太长,就留到下次再进行分析。

看完评论一下吧

1Panel 反向代理网站并解决 502 错误

2024年9月16日 18:55

使用 1G 内存的阿里云轻量香港一段时间了,由于内存过小,机器内存占用长期超过 70%,时不时会卡到需要重启,于是又搞了台 2G 内存洛杉矶的 VPS,决定把将博客迁移过去,然后使用阿里云香港进行反向代理,做一个 CDN 节点。这篇文章会简单记录一下,在两台 VPS 均使用 1Panel 面板的情况下,如何实现发反向代理网站,并解决出现的 502 错误。

1Panel 反向代理网站并解决 502 错误最先出现在Jack‘s Space

C++的 map 当键(Key)不存在的时候会发生什么?

2024年9月14日 18:27

面试流程(例如筛选)的早期阶段,一位 Google 招聘人员曾向我问过这个问题。

在C++中,当你使用std::map访问一个不存在的键时,行为取决于你是如何访问它的。

使用下标操作符 [] 访问时

如果键不存在,std::map 会默认插入一个该键的元素,并为其赋值为类型的默认值。比如,如果 map 的值类型是 int,那么它会插入该键并赋值为 0。

例子:

std::map<int, int> myMap;
int value = myMap[10]; // 如果键10不存在,会插入myMap[10] = 0

使用 at() 方法访问时

如果键不存在,at() 会抛出 std::out_of_range 异常。

例子:

std::map<int, int> myMap;
try {
    int value = myMap.at(10); // 如果键10不存在,会抛出异常
} catch (const std::out_of_range& e) {
    std::cout << "Key not found!" << std::endl;
}

使用 find() 方法

find() 方法不会修改 map,它返回一个迭代器。如果键不存在,它会返回 map.end()。

例子:

std::map<int, int> myMap;
auto it = myMap.find(10);
if (it == myMap.end()) {
    std::cout << "Key not found!" << std::endl;
} else {
    std::cout << "Value: " << it->second << std::endl;
}

C++ std::map 和 std::unordered_map的比较

std::unordered_map 处理不存在的键与 std::map 类似,但有一些差异,主要是因为它们内部的数据结构不同。

map 和 unordered_map 的区别:

  • 顺序:std::map 是有序的(内部实现为平衡树),所以元素会按键的顺序排列。而 std::unordered_map 是无序的,使用哈希表存储元素,因此没有特定的顺序。
  • 性能:std::unordered_map 通常有更快的平均访问时间(由于哈希结构,平均时间复杂度为 O(1)),而 std::map 的访问时间复杂度为 O(log n),因为其内部实现为树结构。然而,如果发生大量哈希冲突,unordered_map 在最坏情况下的时间复杂度可能是 O(n)。

总的来说,std::unordered_map 和 std::map 在处理不存在的键时,对于 []、at() 和 find() 的行为相似,但它们在顺序和性能方面存在差异。

总结

  • 使用 [] 访问时,如果键不存在,map 会插入一个新元素并赋予默认值。
  • 使用 at() 访问时,如果键不存在,会抛出异常。
  • 使用 find() 可以检查键是否存在,而不会修改 map。

英文:C++: Access a Non-existent Key in std::map or std::unordered_map

面试经历

面试题

面试技巧

面试其它

本文一共 473 个汉字, 你数一下对不对.
C++的 map 当键(Key)不存在的时候会发生什么?. (AMP 移动加速版本)

扫描二维码,分享本文到微信朋友圈
75a5a60b9cac61e5c8c71a96e17f2d9c C++的 map 当键(Key)不存在的时候会发生什么? ACM题解 学习笔记 小技巧 技术 数据结构与算法 程序设计 编程 资讯 软件工程
The post C++的 map 当键(Key)不存在的时候会发生什么? first appeared on 小赖子的英国生活和资讯.

相关文章:

  1. 步步高学生电脑上 Basic 编程语言 peek 用法示例 步步高学生电脑 是8位FC机的经典之作.它上面的BASIC有三个版本 1.0, 2.0 和 2.1 2.1 版本有个在线帮助,实际上是 help.cmd 1.0 是用 Esc 键退回到 DOS 的,...
  2. 你给SteemIt中文微信群拖后腿了么? 这年头不缺算法, 就缺数据. 这两天花了很多时间在整API上, 整完之后自己用了一下还觉得真是挺方便的. 今天就突然想看一看自己是否给大家拖后腿了, 于是调用每日中文区微信群排行榜单的API, 刷刷拿着 NodeJs 练手: 1 2 3 4 5 6...
  3. Javascript 中 sleep 函数实现 Javascript 中并没有 built-in 的 sleep 函数支持, 在 async/await/Promise 的支持之前, 我们可以用 busy-waiting 的方式来模拟: 1 2 3...
  4. 按揭贷款(房贷,车贷) 每月还贷计算器 去年给银行借了17万英镑 买了20万7500英镑的房子, 25年还清. 前2年是定率 Fix Rate 的合同 (年利率2.49%). 每个月大概是还 700多英镑. 有很多种还贷的计算方式, 定率/每月固定 是比较常用的. 简单来说就是 每个月交的钱是...
  5. 《Steem 指南》之 justyy 在线工具与 API 系列 – 同时给多个帐号发送SBD或者STEEM 同时给多个帐号发送SBD或者STEEM STEEMIT 和 BUSY 的前端都有一个内置的钱包工具, 您可以一次给一个帐号发送 SBD 或者 STEEM. 当我们要给很多很多人发送钱的时候, 就显得有些不方便了. 这时候可以用这个在线工具: https://steemyy.com/wallet-tool/ 填写表单 只需要填上你的ID,...
  6. 智能手机 HTC One M9 使用测评 虽然我对手机要求不高, 远远没有像追求VPS服务器一样, 但是怎么算来两年内换了四个手机, 先是三星 S4 用了一年多, 然后 Nokia Lumia 635 Windows Phone, 后来又是 BLU, 半年多前换了...
  7. 试用 Linkedin (领英) 高级帐号 (Premium) Linkedin (领英) 算是比较靠谱的职业社交网站, 在上面有很多猎头, 很多知名公司的HR 无时无刻在招人. 特别领英在被微软收购之后, 名气就变得大了许多. 领英是免费使用的, 但也有付费用户, 有给猎头的, 也有给想找工作的. 价格并不便宜, 对于想找工作的 Job...
  8. 最简单有效的过滤WordPress垃圾评论的方法 当你的Wordpress博客流量大的时候, 不免会收到很多垃圾评论. 本文介绍一种特别简单而且免费的过滤Wordpress垃圾评论的方法. 这种方法不需要你安装任何插件, 也不需要拥有修改Wordpress主题模板函数的能力, 只需要1分钟就可以搞定. 把这个列表拷贝下来 打开 WordPress 的控制面版, 到设置-讨论 拷贝上面的列表到 “评论审核” 或者 “评论黑名单”...

Android源码分析:Activity启动流程Task相关分析

2024年9月13日 17:57

Activity的启动分析,很大一块需要了解的是Activity的Task管理,以及启动过程中Task的决策,在之前分析启动流程中,关于Task处理的部分,我这里是简化掉了很多的,今天再来分析一下。

入口与计算启动参数

在之前分析Activity的启动中,已经看到了关于处理Task的代码是在ActivityStart当中的startActivityInner方法当中,这个方法有不少入参,先捋一遍: resultTo为调用的Activity的mToken(IBinder)

ActivityRecord r, //新创建的Record,包含calling信息和要打开的ActivityInfo等
ActivityRecord sourceRecord, //resultTo不为空的时候才会去使用`ActivityRecord isInAnyTask`读取
IvoiceInteractionSession voiceSession, //startVoiceActivity的时候才会传
IvoiceInteractor voiceInteractor, //同上,一般为系统的语音助手界面
int startFlags, //客户端传过来的startFlags一般为0
boolean doResume, //是否需要去resume activity,对于启动Activity场景总是为true
ActivityOptions options, //Activity启动的一些参数,页面跳转动画等
Task inTask, //一般为通过AppTaskImpl启动Activity才会设置值,正常app启动不存在
TaskFragment inTaskFragment, //同上,一般情况为空
int balCode, //Activity后台启动的许可Code,默认为BAL_ALLOW_DEFAULT
NeededGrants intentGrants //Intent访问权限授权

有了所有的入参可以看看computeLaunchingTaskFlags,对于普通应用mInTask为空,mSourceRecord不为空,关注这个方法内的如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
if (mInTask == null) {
 if (mSourceRecord == null) {
 if ((mLaunchFlags & FLAG_ACTIVITY_NEW_TASK) == 0 && mInTask == null) {
 //如果获取不到启动来源的ActivityRecord,且当前要启动的Activity还没有设置NEW_TASK flag,则给他添加
 mLaunchFlags |= FLAG_ACTIVITY_NEW_TASK;
 }
 } else if (mSourceRecord.launchMode == LAUNCH_SINGLE_INSTANCE) {
 //如果来源ActivityRecord是SINGLE INSTANCE,也就是说它是自己独立的任务栈,新启动Activity必须设置NEW_TASK 
 mLaunchFlags |= FLAG_ACTIVITY_NEW_TASK;
 } else if (isLaunchModeOneOf(LAUNCH_SINGLE_INSTANCE, LAUNCH_SINGLE_TASK)) {
 //如果新启动的Activity是SingleInstance或者SingleTask,也要添加NEW_TASK flag
 mLaunchFlags |= FLAG_ACTIVITY_NEW_TASK;
 }
}

if ((mLaunchFlags & FLAG_ACTIVITY_LAUNCH_ADJACENT) != 0
 && ((mLaunchFlags & FLAG_ACTIVITY_NEW_TASK) == 0 || mSourceRecord == null)) {
 //如果要启动的Activity设置了分屏的FLAG,但是却没有设置NEW——FLAG或者没有源ActivityRecord,这个时候就需要忽略掉分屏的这个FLAG
 mLaunchFlags &= ~FLAG_ACTIVITY_LAUNCH_ADJACENT;
}

简化版本的流程图如下:

获取当前的顶部Task: getFocusedRootTask

以上是针对LaunchFlag的一部分处理,但并不是全部,暂时继续往后看。随后就是获取task

1
2
3
final Task prevTopRootTask = mPreferredTaskDisplayArea.getFocusedRootTask();
final Task prevTopTask = prevTopRootTask != null ? prevTopRootTask.getTopLeafTask() : null;
final Task reusedTask = getReusableTask();

首先看这个mPreferredTaskDisplayArea,这个表示倾向的Activity显示的TaskDisplay,它的赋值是在前面的setInitialState方法中:

1
2
3
4
5
mSupervisor.getLaunchParamsController().calculate(inTask, r.info.windowLayout, r,
 sourceRecord, options, mRequest, PHASE_DISPLAY, mLaunchParams);
mPreferredTaskDisplayArea = mLaunchParams.hasPreferredTaskDisplayArea()
 ? mLaunchParams.mPreferredTaskDisplayArea
 : mRootWindowContainer.getDefaultTaskDisplayArea();

我们这里就以它是拿的DefaultTaskDisplayArea为例来分析,继续就是看它的getFocusedRootTask,看代码之前先看看这些类的关系图,之前画过Task,WindowContainer相关的,但是还不够全,这里再补充完整一点。

classDiagram
class ConfigurationContainer {
<<abstract>>
}
class WindowContainer {
List<WindowContainer> mChildren
}
class TaskFragment
class Task
class ActivityRecord {
Task task
TaskDisplayArea mHandoverTaskDisplayArea
}
ConfigurationContainer <|--WindowContainer
WindowContainer <|-- TaskFragment
TaskFragment <|--Task
WindowContainer <|--RootWindowContainer
WindowContainer <|-- DisplayArea
DisplayArea <|-- TaskDisplayArea
WindowContainer"0..*" <-- "1*"WindowContainer
WindowContainer <|-- WindowToken
WindowToken <|--ActivityRecord
ActivityRecord --> Task
ActivityRecord --> TaskDisplayArea

当然WindowContainer的子类远不止这些,包括WindowState等等都是它的子类,但是暂时不需要讨论他们,这里暂时先不列出来了。 我们还是先看getFocusedRootTask方法的源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Task getFocusedRootTask() {
 if (mPreferredTopFocusableRootTask != null) {
 return mPreferredTopFocusableRootTask;
 }

 for (int i = mChildren.size() - 1; i >= 0; --i) {
 final WindowContainer child = mChildren.get(i);
 if (child.asTaskDisplayArea() != null) {
 final Task rootTask = child.asTaskDisplayArea().getFocusedRootTask();
 if (rootTask != null) {
 return rootTask;
 }
 continue;
 }

 final Task rootTask = mChildren.get(i).asTask();
 if (rootTask.isFocusableAndVisible()) {
 return rootTask;
 }
 }

 return null;
}

如果说当前的TaskDisplayArea中,preferredTopFocusableRoot存在就会直接使用,这个会在postionChildTaskAt的时候,如果child放置到顶部,并且它是可获得焦点的,会把他赋值给这个preferredTopFocusableRoot。 我们这里先看它为空的情况。如果它为空,这回到树状结构中查找,遍历树节点如果也是TaskDisplayArea,则会 看他们的focusedRootTask是否存在,如果就返回。如果节点是Task,就会检查这个Task是否为可获得焦点并且可见的,则返回它。否则就返回空。因为我们当前已经打了Activity,这里一般是可以获得值的。

如果拿到了prevTopRootTask,就会去调用getTopLeafTask去获取叶子节点的Task,代码如下:

1
2
3
4
5
6
7
8
public Task getTopLeafTask() {
 for (int i = mChildren.size() - 1; i >= 0; --i) { //从大数开始遍历
 final Task child = mChildren.get(i).asTask();
 if (child == null) continue; //如果不是Task就跳过
 return child.getTopLeafTask(); //继续看它的子节点
 }
 return this; //没有孩子节点,那就是一个叶子节点 
}

以上是获取叶子节点的代码,典型的树的遍历代码。到目前是拿的当前在展示的页面的任务栈。

获取可复用的Task:getReusableTask

而之后的getReusableTask则是获取可以使用的任务Task:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
private Task getReusableTask() {
 //一般是从最近任务打开的页面才会执行这里,我们可以跳过
 if (mOptions != null && mOptions.getLaunchTaskId() != INVALID_TASK_ID) {
 Task launchTask = mRootWindowContainer.anyTaskForId(mOptions.getLaunchTaskId());
 if (launchTask != null) {
 return launchTask;
 }
 return null;
 }
 //如果启动的FLAG是 Single Instance或者SingleTask;又或者是虽然设置了NEW_TASK但是没有设置MULTIPLE_TASK。这些情况都会把新的Activity放到已有的任务栈。
 boolean putIntoExistingTask = ((mLaunchFlags & FLAG_ACTIVITY_NEW_TASK) != 0 &&
 (mLaunchFlags & FLAG_ACTIVITY_MULTIPLE_TASK) == 0)
 || isLaunchModeOneOf(LAUNCH_SINGLE_INSTANCE, LAUNCH_SINGLE_TASK);
 //因为mInTask为空,后面的resultTo不为空,因此putIntoExistingTask结果为false。当通过startActivityForResult的且requestCode > 0 时候就不为空
 putIntoExistingTask &= mInTask == null && mStartActivity.resultTo == null;
 ActivityRecord intentActivity = null;
 if (putIntoExistingTask) {
 if (LAUNCH_SINGLE_INSTANCE == mLaunchMode) {
 //这种情况只有一个实例,就通过intent和activityInfo去找到它。
 intentActivity = mRootWindowContainer.findActivity(mIntent, mStartActivity.info,
 mStartActivity.isActivityTypeHome());
 } else if ((mLaunchFlags & FLAG_ACTIVITY_LAUNCH_ADJACENT) != 0) {
 //对于分屏的,如果历史栈中有才使用
 intentActivity = mRootWindowContainer.findActivity(mIntent, mStartActivity.info,
 !(LAUNCH_SINGLE_TASK == mLaunchMode));
 } else {
 // 查找最合适的Task给Activity用 
 intentActivity =
 mRootWindowContainer.findTask(mStartActivity, mPreferredTaskDisplayArea);
 }
 }

 if (intentActivity != null && mLaunchMode == LAUNCH_SINGLE_INSTANCE_PER_TASK
 && !intentActivity.getTask().getRootActivity().mActivityComponent.equals(
 mStartActivity.mActivityComponent)) {
 //对于singleInstancePreTask,如果Task的根Activity不是要启动的Activity那么还是不能够复用,因此需要把intentActivity设置为空。
 intentActivity = null;
 }

 if (intentActivity != null
 && (mStartActivity.isActivityTypeHome() || intentActivity.isActivityTypeHome())
 && intentActivity.getDisplayArea() != mPreferredTaskDisplayArea) {
 //
 intentActivity = null;
 }

 return intentActivity != null ? intentActivity.getTask() : null;
}

以上就是根据条件判断是否可以复用栈,如果可以会去拿已经存在的Activity,如果Activity存在,则回去拿它的Task。其中这里有一个singleInstancePreTask的启动模式,这个对于我们很多Android开发这是不熟悉的,它是Android12引入的,它可以说是加强版本的singleInstance,当它是Task栈的根Task的时候就复用,如果不是的就类似singleTask会去打开一个新的Task栈。

这里先来看一下这个findActivity,他也是到RootWindowContainer中去查找,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
ActivityRecord findActivity(Intent intent, ActivityInfo info, boolean compareIntentFilters) {
 ComponentName cls = intent.getComponent();
 if (info.targetActivity != null) {
 cls = new ComponentName(info.packageName, info.targetActivity);
 }
 final int userId = UserHandle.getUserId(info.applicationInfo.uid);

 final PooledPredicate p = PooledLambda.obtainPredicate(
 RootWindowContainer::matchesActivity, PooledLambda.__(ActivityRecord.class),
 userId, compareIntentFilters, intent, cls);
 final ActivityRecord r = getActivity(p);
 p.recycle();
 return r;
}

其中第8行就是创建了一个PooledPredicate,在我们调用test方法的时候就 会调用RootWindowContainer::matchesActivity这个方法,这个方法的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
private static boolean matchesActivity(ActivityRecord r, int userId,
 boolean compareIntentFilters, Intent intent, ComponentName cls) {
 if (!r.canBeTopRunning() || r.mUserId != userId) return false;

 if (compareIntentFilters) {
 if (r.intent.filterEquals(intent)) {
 return true;
 }
 } else {
 if (r.mActivityComponent.equals(cls)) {
 return true;
 }
 }
 return false;
}

首先检查,对应的ActivityRecord是否可以运行在topTask,是否与我们目标要启动的Activity是同样的用户Id,也就是在同一个进程。如果compareIntentFilters为true,还是检查他们的intent是否相同,之后会检查是否为同一个Activity类。对于这个有所了解,我们继续看getActivity的代码,它首先是会调用WindowContainer中的这个方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
ActivityRecord getActivity(Predicate<ActivityRecord> callback, boolean traverseTopToBottom,
 ActivityRecord boundary) {
 if (traverseTopToBottom) {
 for (int i = mChildren.size() - 1; i >= 0; --i) {
 final WindowContainer wc = mChildren.get(i);
 if (wc == boundary) return boundary;

 final ActivityRecord r = wc.getActivity(callback, traverseTopToBottom, boundary);
 if (r != null) {
 return r;
 }
 }
 } else {
 ...
 }

 return null;
}

如果单看上面的代码,我们似乎永远都拿不到ActivityRecord,但是呢ActivityRecord也是WindowContainer的子类,在它当中我们也有同名方法,代码如下:

1
2
3
4
5
@Override
ActivityRecord getActivity(Predicate<ActivityRecord> callback, boolean traverseTopToBottom,
 ActivityRecord boundary) {
 return callback.test(this) ? this : null;
}

这里可以看到,他就是调用了我们刚刚传入的那个PooledPredicate来测试自己是否符合要求,从而我们可以拿到对应的ActivityRecord

计算目标Task: computeTargetTask

到这里我们可以继续看startActivityInner方法中的如下代码:

1
2
3
final Task targetTask = reusedTask != null ? reusedTask : computeTargetTask();
final boolean newTask = targetTask == null;
mTargetTask = targetTask;

如果我们刚刚已经拿到reusedTask,那么目标的task就会使用它,如果拿不到则会调用computeTargetTask去获取Task,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private Task computeTargetTask() {
 if (mStartActivity.resultTo == null && mInTask == null && !mAddingToTask
 && (mLaunchFlags & FLAG_ACTIVITY_NEW_TASK) != 0) {
 // 同时满足这些条件的情况,不复用task,直接返回空
 return null;
 } else if (mSourceRecord != null) {
 //调用源ActivityRecord,直接复用调用源的Task
 return mSourceRecord.getTask();
 } else if (mInTask != null) {
 //inTask一般是AppTaskImpl指定的,就直接用它,它有可能还没创建这里去创建
 if (!mInTask.isAttached()) {
 getOrCreateRootTask(mStartActivity, mLaunchFlags, mInTask, mOptions);
 }
 return mInTask;
 } else {
 //获取或者创建Task
 final Task rootTask = getOrCreateRootTask(mStartActivity, mLaunchFlags, null /* task */,
 mOptions);
 final ActivityRecord top = rootTask.getTopNonFinishingActivity();
 if (top != null) {
 return top.getTask();
 } else {
 rootTask.removeIfPossible("computeTargetTask");
 }
 }
 return null;
}

这里我们继续去看一下getOrCreateRootTask,代码如下:

1
2
3
4
5
6
7
8
private Task getOrCreateRootTask(ActivityRecord r, int launchFlags, Task task,
 ActivityOptions aOptions) {
 final boolean onTop =
 (aOptions == null || !aOptions.getAvoidMoveToFront()) && !mLaunchTaskBehind;
 final Task sourceTask = mSourceRecord != null ? mSourceRecord.getTask() : null;
 return mRootWindowContainer.getOrCreateRootTask(r, aOptions, task, sourceTask, onTop,
 mLaunchParams, launchFlags);
}

这里还是先拿到调用端的sourceTask以及是否需要onTop,之后调用了RootWindowContainergetOrCreateRootTask方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
Task getOrCreateRootTask(@Nullable ActivityRecord r,
 @Nullable ActivityOptions options, @Nullable Task candidateTask,
 @Nullable Task sourceTask, boolean onTop,
 @Nullable LaunchParamsController.LaunchParams launchParams, int launchFlags) {
 ...
 TaskDisplayArea taskDisplayArea = null;

 final int activityType = resolveActivityType(r, options, candidateTask);
 Task rootTask = null;
 ...
 int windowingMode = launchParams != null ? launchParams.mWindowingMode
 : WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 ....
 if (taskDisplayArea == null) {
 taskDisplayArea = getDefaultTaskDisplayArea();
 }
 return taskDisplayArea.getOrCreateRootTask(r, options, candidateTask, sourceTask,
 launchParams, launchFlags, activityType, onTop);
}

因为我们没有设置什么参数,因此会执行到最后的fallback流程,我们只分析这一部分。默认我们拿到的activityTypeActivity_TYPE_STANDARDgetDefaultTaskDisplayArea会拿到默认的TaskDisplayArea这个之前已经分析过了,最后就是通过它去调用getOrCreateRootTask,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Task getOrCreateRootTask(int windowingMode, int activityType, boolean onTop,
 @Nullable Task candidateTask, @Nullable Task sourceTask,
 @Nullable ActivityOptions options, int launchFlags) {
 final int resolvedWindowingMode =
 windowingMode == WINDOWING_MODE_UNDEFINED ? getWindowingMode() : windowingMode;
 if (!alwaysCreateRootTask(resolvedWindowingMode, activityType)) {
 Task rootTask = getRootTask(resolvedWindowingMode, activityType);
 if (rootTask != null) {
 return rootTask;
 }
 } else if (candidateTask != null) {
 ....
 }
 return new Task.Builder(mAtmService)
 .setWindowingMode(windowingMode)
 .setActivityType(activityType)
 .setOnTop(onTop)
 .setParent(this)
 .setSourceTask(sourceTask)
 .setActivityOptions(options)
 .setLaunchFlags(launchFlags)
 .build();
}

因为我们传进来的windowingModeWINDOWING_MODE_UNDEFINED,因此这里会调用getWindowingMode来设置Mode,这里就是调用系统设置了,不需要看代码。

因为ActivityType是ACTIVITY_TYPE_STAND,所以这里alwaysCreateRootTask为true,因为我们传进来的candidateTask也是空,因此最后就是会创建一个新的Task。但是因为是创建的新task,这个Task里面没有运行中的Activity,因此computeTargetTask还是会返回空。

获取PriorAboveTask和task回收检查

继续回来看startActivityInner内部的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
if (targetTask != null) { //在DisplayArea中获取在targetTask Root上面的其他root task
 mPriorAboveTask = TaskDisplayArea.getRootTaskAbove(targetTask.getRootTask());
}
//如果newTask为false,则看看目标task 顶部的未finish的ActivityRecord
final ActivityRecord targetTaskTop = newTask
 ? null : targetTask.getTopNonFinishingActivity();
if (targetTaskTop != null) {
 startResult = recycleTask(targetTask, targetTaskTop, reusedTask, intentGrants);
 if (startResult != START_SUCCESS) {
 return startResult;
 }
} else {
 mAddingToTask = true;
}

在可以复用栈的情况下,targetTaskTop不为空,比如singleTask的模式,这个时候会去执行recycleTask。其他情况设置mAddingToTask,表示我们的ActivityRecord需要添加到Task。

1
2
3
4
5
6
7
final Task topRootTask = mPreferredTaskDisplayArea.getFocusedRootTask();
if (topRootTask != null) {
 startResult = deliverToCurrentTopIfNeeded(topRootTask, intentGrants);
 if (startResult != START_SUCCESS) {
 return startResult;
 }
}

如果我们检查topRootTask不为空的情况,这里如果我们的启动模式是singleTask,首先会检查task栈顶未启动的Activity是否与当前要启动的相同,如果相同,则不启动当前Activity,仅仅去执行它的newIntent,具体代码就不分析了。

创建RootTask,处理新Activity的Task

再往后看代码,之后就该创建RootTask了,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
if (mTargetRootTask == null) {
 mTargetRootTask = getOrCreateRootTask(mStartActivity, mLaunchFlags, targetTask,
 mOptions);
}
if (newTask) {
 final Task taskToAffiliate = (mLaunchTaskBehind && mSourceRecord != null)
 ? mSourceRecord.getTask() : null;
 setNewTask(taskToAffiliate);
} else if (mAddingToTask) {
 addOrReparentStartingActivity(targetTask, "adding to task");
}

上面调用了getOrCreateRootTask,来创建了新的RootTask,与我们之前分析的类似。同时因为我们之前没有成功创建targetTask,因此这里会执行到setNewTask,而taskToAffiliate没有特殊参数,默认我们先按照空来对待吧。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
private void setNewTask(Task taskToAffiliate) {
 final boolean toTop = !mLaunchTaskBehind && !mAvoidMoveToFront;
 final Task task = mTargetRootTask.reuseOrCreateTask(
 mStartActivity.info, mIntent, mVoiceSession,
 mVoiceInteractor, toTop, mStartActivity, mSourceRecord, mOptions);
 task.mTransitionController.collectExistenceChange(task);
 //把新的ActivityRecord放置到Task列表的顶部
 addOrReparentStartingActivity(task, "setTaskFromReuseOrCreateNewTask");
 if (taskToAffiliate != null) {
 mStartActivity.setTaskToAffiliateWith(taskToAffiliate);
 }
}

这里大多数情况,toTop会是true,我们去看一下这个reuseOrCreateTask方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
Task reuseOrCreateTask(ActivityInfo info, Intent intent, IVoiceInteractionSession voiceSession,
 IVoiceInteractor voiceInteractor, boolean toTop, ActivityRecord activity,
 ActivityRecord source, ActivityOptions options) {

 Task task;
 if (canReuseAsLeafTask()) { //如果没有Task子节点或者不是组织创建的
 task = reuseAsLeafTask(voiceSession, voiceInteractor, intent, info, activity);
 } else {
 // 创建taskId
 final int taskId = activity != null
 ? mTaskSupervisor.getNextTaskIdForUser(activity.mUserId)
 : mTaskSupervisor.getNextTaskIdForUser();
 final int activityType = getActivityType();
 //创建task,并且当前Task设置为这个Task的Parent,在build当中,把当前的Task放置到Parent的mChildren当中,根据toTop决定是否放置到顶部
 task = new Task.Builder(mAtmService)
 .setTaskId(taskId)
 .setActivityType(activityType != ACTIVITY_TYPE_UNDEFINED ? activityType
 : ACTIVITY_TYPE_STANDARD)
 .setActivityInfo(info)
 .setActivityOptions(options)
 .setIntent(intent)
 .setVoiceSession(voiceSession)
 .setVoiceInteractor(voiceInteractor)
 .setOnTop(toTop)
 .setParent(this)
 .build();
 }

 int displayId = getDisplayId();
 if (displayId == INVALID_DISPLAY) displayId = DEFAULT_DISPLAY;
 final boolean isLockscreenShown = mAtmService.mTaskSupervisor.getKeyguardController()
 .isKeyguardOrAodShowing(displayId);
 if (!mTaskSupervisor.getLaunchParamsController()
 .layoutTask(task, info.windowLayout, activity, source, options)
 && !getRequestedOverrideBounds().isEmpty()
 && task.isResizeable() && !isLockscreenShown) {
 //设置task的布局边界
 task.setBounds(getRequestedOverrideBounds());
 }

 return task;
}

上面代码我们就可以复用或者创建新的task,详见注释。拿到Task,或者我们之前已经有Task的情况下(mAddingToTask为true)的时候,还需要执行addOrReparentStartingActivity,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private void addOrReparentStartingActivity(@NonNull Task task, String reason) {
 TaskFragment newParent = task;
 if (mInTaskFragment != null) {
 //我们的场景不涉及InTaskFragment不为空,忽略
 ...
 } else {
 //当clearTop的时候,并且是可嵌入的,这个时候会保存TaskFragment到mAddingToTaskFragment
 TaskFragment candidateTf = mAddingToTaskFragment != null ? mAddingToTaskFragment : null;
 if (candidateTf == null) {
 //获取目标Task的topRunningActivity,新建的Task不存在
 final ActivityRecord top = task.topRunningActivity(false /* focusableOnly */,
 false /* includingEmbeddedTask */);
 if (top != null) {
 candidateTf = top.getTaskFragment();
 }
 }
 if (candidateTf != null && candidateTf.isEmbedded()
 && canEmbedActivity(candidateTf, mStartActivity, task) == EMBEDDING_ALLOWED) {
 //如果拿到了topTask,并且对应的Task是可嵌入的,并且要打开的ActivityRecord也可被嵌入,这把拿到的这个Task作为新的父Task
 newParent = candidateTf;
 }
 }
 //新的ActivityRecord的TaskFragment为空,或者和新的Parent一样,就把这个ActivityRecord放到Task的顶部
 if (mStartActivity.getTaskFragment() == null
 || mStartActivity.getTaskFragment() == newParent) {
 newParent.addChild(mStartActivity, POSITION_TOP);
 } else {
 mStartActivity.reparent(newParent, newParent.getChildCount() /* top */, reason);
 }
}

这里会检查如果新的父Task和我们可以复用的Task是否相同,如果相同,或者ActivityRecord中还没有parent,这个时候就把ActivityRecord添加到Task的孩子列表的顶部。而如果ActivityRecord已经存在了parent并且不是我们将要设置的这个,就需要做reparent,这个步骤代码比较复杂,前面调用检查判断的调用省略,直接看最后的调用,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//WindowContainer.java
void reparent(WindowContainer newParent, int position) {
 final DisplayContent prevDc = oldParent.getDisplayContent();
 final DisplayContent dc = newParent.getDisplayContent();

 mReparenting = true;
 //从旧的parent中移除自己,并把自己添加到新parent的指定位置
 oldParent.removeChild(this);
 newParent.addChild(this, position);
 mReparenting = false;

 // 重新布局layout
 dc.setLayoutNeeded();
 //如果新旧的DisplayContent不同,还需要做displayChange的处理
 if (prevDc != dc) {
 onDisplayChanged(dc);
 prevDc.setLayoutNeeded();
 }
 getDisplayContent().layoutAndAssignWindowLayersIfNeeded();

 onParentChanged(newParent, oldParent);
 onSyncReparent(oldParent, newParent);
}

以上的代码我们看到有做parent的替换,但是复杂点在后面的onParentChanged里面,这里会做SurfaceControl的创建或者reparent,这里就不深入了。除此之外,这里还涉及到动画的处理,我们这里也 不深入了。

继续往后看

1
2
3
4
if (!mAvoidMoveToFront && mDoResume) {
 mTargetRootTask.getRootTask().moveToFront("reuseOrNewTask", targetTask);
 ...
}

这里会在检查我们的TargetRootTask相比与它的RootTask如果不是在顶部的,需要把它移动到顶部。再往后面就是调用TargetRootTask去启动Activity,以及确认Activity显示出来。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
final boolean isTaskSwitch = startedTask != prevTopTask && !startedTask.isEmbedded();
//启动Activity
mTargetRootTask.startActivityLocked(mStartActivity, topRootTask, newTask, isTaskSwitch,
 mOptions, sourceRecord);
if (mDoResume) {
 final ActivityRecord topTaskActivity = startedTask.topRunningActivityLocked();
 if (!mTargetRootTask.isTopActivityFocusable()
 || (topTaskActivity != null && topTaskActivity.isTaskOverlay()
 && mStartActivity != topTaskActivity)) {
 //如果当前要启动的Activity还没有启动,没有在栈顶端,执行下面的代码
 mTargetRootTask.ensureActivitiesVisible(null /* starting */,
 0 /* configChanges */, !PRESERVE_WINDOWS);
 mTargetRootTask.mDisplayContent.executeAppTransition();
 } else {
 if (mTargetRootTask.isTopActivityFocusable()
 && !mRootWindowContainer.isTopDisplayFocusedRootTask(mTargetRootTask)) {
 mTargetRootTask.moveToFront("startActivityInner");
 }
 mRootWindowContainer.resumeFocusedTasksTopActivities(
 mTargetRootTask, mStartActivity, mOptions, mTransientLaunch);
 }
}
mRootWindowContainer.updateUserRootTask(mStartActivity.mUserId, mTargetRootTask); //更新用户的rootTask

// 更新系统的最近任务
mSupervisor.mRecentTasks.add(startedTask);

到此位置才算是完成了所有Task计算以及Activity的启动。

通过Adb shell看Activity Task栈

前面都是在解读Android的源码可能比较抽象,其中涉及到了挺多WindowContainer和Task等等相关的查找创建的,为了更加形象。我写了个小demo,主页面是普通的launchMode,另外一次打开了一个singleTask启动Mode的和一个singleInstance 启动Mode的页面,然后我们用一下命令进行Activity Task的dump:

1
adb shell dumpsys activity activities > ~/activitytasks.txt

我们就得到了如下的内容(为方便解读,做了删减):

Display #0 (activities from top to bottom):
* Task{8ed7532 #40 type=standard A=10116:com.example.myapplication U=0 visible=true visibleRequested=true mode=fullscreen translucent=false sz=1}
topResumedActivity=ActivityRecord{3653c00 u0 com.example.myapplication/.SimpleInstanceActivity} t40}
* Hist #0: ActivityRecord{3653c00 u0 com.example.myapplication/.SimpleInstanceActivity} t40}
* Task{ac77886 #39 type=standard A=10116:com.example.myapplication U=0 visible=false visibleRequested=false mode=fullscreen translucent=true sz=2}
mLastPausedActivity: ActivityRecord{6c019a5 u0 com.example.myapplication/.SingleTaskActivity} t39}
* Hist #1: ActivityRecord{6c019a5 u0 com.example.myapplication/.SingleTaskActivity} t39}
* Hist #0: ActivityRecord{ef92174 u0 com.example.myapplication/.MainActivity} t39}
* Task{d8527c1 #1 type=home U=0 visible=false visibleRequested=false mode=fullscreen translucent=true sz=1}
* Task{d60ff49 #33 type=home I=com.android.launcher3/.uioverrides.QuickstepLauncher U=0 rootTaskId=1 visible=false visibleRequested=false mode=fullscreen translucent=true sz=1}
mLastPausedActivity: ActivityRecord{868b56f u0 com.android.launcher3/.uioverrides.QuickstepLauncher} t33}
* Hist #0: ActivityRecord{868b56f u0 com.android.launcher3/.uioverrides.QuickstepLauncher} t33}
* Task{2c52978 #36 type=standard A=10044:com.android.documentsui U=0 visible=false visibleRequested=false mode=fullscreen translucent=true sz=1}
mLastPausedActivity: ActivityRecord{f9c48b6 u0 com.android.documentsui/.files.FilesActivity} t36}
mLastNonFullscreenBounds=Rect(338, 718 - 1103, 2158)
isSleeping=false
* Hist #0: ActivityRecord{f9c48b6 u0 com.android.documentsui/.files.FilesActivity} t36}
* Task{e38c1d6 #35 type=standard A=10044:com.android.documentsui U=0 visible=false visibleRequested=false mode=fullscreen translucent=true sz=1}
mLastPausedActivity: ActivityRecord{32a5344 u0 com.android.documentsui/.files.FilesActivity} t35}
mLastNonFullscreenBounds=Rect(338, 718 - 1103, 2158)
isSleeping=false
* Hist #0: ActivityRecord{32a5344 u0 com.android.documentsui/.files.FilesActivity} t35}
* Task{1d65c74 #3 type=undefined U=0 visible=false visibleRequested=false mode=fullscreen translucent=true sz=2}
mCreatedByOrganizer=true
* Task{41cb5e3 #5 type=undefined U=0 rootTaskId=3 visible=false visibleRequested=false mode=multi-window translucent=true sz=0}
mBounds=Rect(0, 2960 - 1440, 4440)
mCreatedByOrganizer=true
isSleeping=false
* Task{1cdca12 #4 type=undefined U=0 rootTaskId=3 visible=false visibleRequested=false mode=multi-window translucent=true sz=0}
mCreatedByOrganizer=true
isSleeping=false

这上面就是我们的mRootContainer它当中的的display下面的所有的Task记录,因为我的手机只有一块屏幕,这里只有一个display0, 并且展示了他们的存储关系,这里我们可以看到我们的SimpleInstanceActivity它是在独立的Task当中的。用图表简单描绘一下,结构如下所示:

总结

以上就是Activity Task管理的分析,因为这个流程真的是非常复杂,因此中间的很多步骤还是进行了部分省略。Android系统迭代了这么多年,作为UI展示的组件,Activity承载了太多东西,多屏幕,折叠屏什么的都要支持,因此引入的东西就越来越多。官方也是意识到了这一块的,Activity的管理从AMS抽出来单独的ATMS,ActivityTaskSupervisor的功能也在慢慢抽离到其他的代码中,当前代码里面也添加了很多注释,只要花时间还是能够给搞明白的。

本文仅为一家之言,因为个人疏忽,可能文中也会出现一些错误,欢迎大家指正。

看完评论一下吧

Android源码分析:Activity启动流程分析

2024年9月11日 22:20

Activity是Android中四大组件使用最多的一种,不准确的说,一个Activity就是一个独立页面的承载,因此看Android系统的源码,Activity的启动也是必须要去阅读的。今天的文章就来介绍Activity的启动。因为之前的文章已经分析了ClientTransaction,因此我们对于AMS调用Activity的生命周期和启动有所了解。并且我们也已经分析过了Binder,对于跨进程通讯我们也比较清楚了,不需要细看。我们也分析了应用进程的启动,我们分析Activity启动过程,就不需要去关注应用进程的启动了。有了这些基础,分析Activity的启动会容易一点点。

发起启动Activity

我们首先来看一下启动Activity的调用,我们通常会使用下面的显示调用来启动一个Activity。

1
2
Intent intent = new Intent(context, AActivity.class);
startActivity(intent);

当然也有可能会使用隐式调用来启动一个Activity,如下:

1
2
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://isming.me"));
startActivity(intent);

不过以上两者在启动过程中,仅仅是查找目标组件有区别,并且对于隐式调用,可能存在多个可以启动的Activity,这个时候需要让用户选择目标的页面。对于这一块,我们在后面这个地方会考虑部分略过。

Activity中最终会走到 startActivityForResult(intent, requestCode, options)方法中,这里传入的options我们可以用它设置一些东西,比如App跳转的动画等,我们前面的场景的options为空,并且手机上默认的parent activity也为空,因此会执行这一部分:

1
2
3
4
5
6
7
8
Instrumentation.ActivityResult ar = mInstrumentation.execStartActivity(
 this, mMainThread.getApplicationThread(), mToken, this,
 intent, requestCode, options);
if (ar != null) {
 mMainThread.sendActivityResult(
 mToken, mEmbeddedID, requestCode, ar.getResultCode(),
 ar.getResultData());
}

发起端的处理

在Instrumentation.execStartActivity中会调用如下代码:

1
2
3
4
5
6
int result = ActivityTaskManager.getService().startActivity(whoThread,
 who.getOpPackageName(), who.getAttributionTag(), intent,
 intent.resolveTypeIfNeeded(who.getContentResolver()), token,
 target != null ? target.mEmbeddedID : null, requestCode, 0, null, options);
 notifyStartActivityResult(result, options);
 checkStartActivityResult(result, intent);

早期是直接通过ActivityManagerService去启动新的页面的,在这个commit开始把Activity管理的拆分到ActivityTaskManagerService中去。这里我们看到是去获取ActivityTaskManagerService后面简称ATMS,获取ATMS的代码就不罗列了。

这里传到ATMS的参数,包括,发起应用的ApplicationThread,包名(对于普通应用来说opPackage和packageName是一样的),启动的Intent,token和target一般都是空。

ATMS执行startActivity

最终的执行实际是通过binder调用到ActivityTaskManagerService中的startActivity方法,这个方法中又直接调用了startActivityAsUser,其中会有一些检查,检查调用端的uid和packageName是否匹配和其他一些检查,这里不太关注,我们主要关注以下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
userId = getActivityStartController().checkTargetUser(userId, validateIncomingUser,
 Binder.getCallingPid(), Binder.getCallingUid(), "startActivityAsUser");
 return getActivityStartController().obtainStarter(intent, "startActivityAsUser")
 .setCaller(caller)
 .setCallingPackage(callingPackage)
 .setCallingFeatureId(callingFeatureId)
 .setResolvedType(resolvedType)
 .setResultTo(resultTo)
 .setResultWho(resultWho)
 .setRequestCode(requestCode)
 .setStartFlags(startFlags)
 .setProfilerInfo(profilerInfo)
 .setActivityOptions(opts)
 .setUserId(userId)
 .execute();

从上面的逻辑可以看到,控制Activity启动的代码都放到ActivityStartController中了,首先是获取用户uid,因为每个应用的都会有一个uid,其后就是获取一个ActivityStarter,再通过构建者模式把启动Activity的参数都传到ActivityStarter中去,最后在ActivityStarter的execute()方法中去执行启动的逻辑。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
...
if (mRequest.activityInfo == null) { //如果还没有activitgyInfo去填充
 mRequest.resolveActivity(mSupervisor);
}
...
synchronized (mService.mGlobalLock) {
 res = resolveToHeavyWeightSwitcherIfNeeded(); //检查是否为heavy-weight 进程,系统会限制同一时间只有一个heavy-weight进程
 if (res != START_SUCCESS) {
 return res;
 }
 res = executeRequest(mRequest);
}

其中第2行代码就是根据我们的Intent去查询我们将要打开的目标Activity信息。

解析ActivityInfo

resolveActivity中的核心代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
//ActivityStarter.Request
void resolveActivity(ActivityTaskSupervisor supervisor) {
 resolveInfo = supervisor.resolveIntent(intent, resolvedType, userId,
 0 /* matchFlags */,
 computeResolveFilterUid(callingUid, realCallingUid, filterCallingUid));
 activityInfo = supervisor.resolveActivity(intent, resolveInfo, startFlags,
 profilerInfo);
 if (activityInfo != null) {
 intentGrants = supervisor.mService.mUgmInternal.checkGrantUriPermissionFromIntent(
 intent, resolvedCallingUid, activityInfo.applicationInfo.packageName,
 UserHandle.getUserId(activityInfo.applicationInfo.uid));
 }
}

resolveActivity的工作主要由ActivityTaskSupervisor来完成,首先是resolveIntent来获取ResolveInfo,之后调用resolveActivity获取ActivityInfo,最后再去对Intent中的data Uri做权限检查,我们这里只需要分析前两步骤就可。

resolveIntent方法内部,我们看到是调用了PackageManagerServiceresolveIntent方法,代码如下,具体就不深入探究了。

1
2
3
4
//ActivityTaskSupervisor
return mService.getPackageManagerInternalLocked().resolveIntent(
 intent, resolvedType, modifiedFlags, privateResolveFlags, userId, true,
 filterCallingUid);

resolveActivity代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
ActivityInfo resolveActivity(Intent intent, ResolveInfo rInfo, int startFlags,
 ProfilerInfo profilerInfo) {
 final ActivityInfo aInfo = rInfo != null ? rInfo.activityInfo : null;
 if (aInfo != null) {
 intent.setComponent(new ComponentName(
 aInfo.applicationInfo.packageName, aInfo.name));
 ...
 }
 return aInfo;
}

这里所做的事情则比较简单,就是从前面拿到的ResolveInfo中拿到activityInfo,并且构建一个ComponentName放到Intent中去。到此为止就拿到了要打开的Activity信息。

ActivityStarter.executeRequest

在前面拿到ActivityInfo,并且我们还构建了一个Request,我们就会继续调用executeRequest方法,其中是有大段的代码 是检查权限,以及一些系统Activity逻辑的处理,不是我们流程关注的重点,重要的是以下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

mInterceptor.setStates(userId, realCallingPid, realCallingUid, startFlags, callingPackage,
 callingFeatureId);
if (mInterceptor.intercept(intent, rInfo, aInfo, resolvedType, inTask, inTaskFragment,
 callingPid, callingUid, checkedOptions)) { //拦截器对要启动的Activity做预处理
 intent = mInterceptor.mIntent;
 rInfo = mInterceptor.mRInfo;
 aInfo = mInterceptor.mAInfo;
 resolvedType = mInterceptor.mResolvedType;
 inTask = mInterceptor.mInTask;
 callingPid = mInterceptor.mCallingPid;
 callingUid = mInterceptor.mCallingUid;
 checkedOptions = mInterceptor.mActivityOptions;

 intentGrants = null;
}

final ActivityRecord r = new ActivityRecord.Builder(mService) //构建ActivityRecord
 .setCaller(callerApp)
 .setLaunchedFromPid(callingPid)
 .setLaunchedFromUid(callingUid)
 .setLaunchedFromPackage(callingPackage)
 .setLaunchedFromFeature(callingFeatureId)
 .setIntent(intent)
 .setResolvedType(resolvedType)
 .setActivityInfo(aInfo)
 .setConfiguration(mService.getGlobalConfiguration())
 ...
 .build();

 mLastStartActivityRecord = r; //保存构建的ActivityRecord

 ...

 mLastStartActivityResult = startActivityUnchecked(r, sourceRecord, voiceSession,
 request.voiceInteractor, startFlags, true /* doResume */, checkedOptions,
 inTask, inTaskFragment, balCode, intentGrants); //执行启动Activity,并保存结果到mLastStartActivityResult中,以及结果中返回这个result

 if (request.outActivity != null) {
 request.outActivity[0] = mLastStartActivityRecord;
 }
 ...

这里我们有一些权限检查和系统处理之类的没有贴,不过还是贴了一下intercept方法,这里就是给了系统的其他代码来修改Intent的机会。之后就会利用我们传进来的信息去创建ActivityRecord,并且调用startActivityUnchecked去进入下一步:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

private int startActivityUnchecked(final ActivityRecord r, ActivityRecord sourceRecord,...,NeededUriGrants intentGrants) {
 int result = START_CANCELED;
 final Task startedActivityRootTask;

 ......
 try {
 ......
 try {
 .....
 result = startActivityInner(r, sourceRecord, voiceSession, voiceInteractor,
 startFlags, doResume, options, inTask, inTaskFragment, balCode,
 intentGrants);
 } finally {
 startedActivityRootTask = handleStartResult(r, options, result, newTransition,
 remoteTransition); //处理启动Activity的结果
 }
 } finally {
 mService.continueWindowLayout(); //wms处理
 }
 postStartActivityProcessing(r, result, startedActivityRootTask);

 return result;
 }

这里又走到了startActivityInner(),startActivityInner()会去计算launch falgs,去判断是否开创建新的Task还是可以复用task,以及调用启动的后续代码,这个方法的代码比较长我们先一点一点的看。

Task的处理

首先来看其中关于flag的处理,首先就是其中调用的computeLaunchingTaskFlags方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
private void computeLaunchingTaskFlags() {
 ...
 if (mInTask == null) {
 if (mSourceRecord == null) {
 if ((mLaunchFlags & FLAG_ACTIVITY_NEW_TASK) == 0 && mInTask == null) {
 mLaunchFlags |= FLAG_ACTIVITY_NEW_TASK;
 }
 } else if (mSourceRecord.launchMode == LAUNCH_SINGLE_INSTANCE) {
 mLaunchFlags |= FLAG_ACTIVITY_NEW_TASK;
 } else if (isLaunchModeOneOf(LAUNCH_SINGLE_INSTANCE, LAUNCH_SINGLE_TASK)) {
 mLaunchFlags |= FLAG_ACTIVITY_NEW_TASK;
 }
 }

 if ((mLaunchFlags & FLAG_ACTIVITY_LAUNCH_ADJACENT) != 0
 && ((mLaunchFlags & FLAG_ACTIVITY_NEW_TASK) == 0 || mSourceRecord == null)) {
 mLaunchFlags &= ~FLAG_ACTIVITY_LAUNCH_ADJACENT;
 }
}

这里就是对于我们的启动的LaunchFlag做处理,比如说LAUNCH_SIGLE_INSTANCELAUNCH_SINGLE_TASK都给添加FLAG_ACTIVITY_NEW_TASK等。

随后则是计算Task:

1
2
3
4
5
6
7
final Task prevTopRootTask = mPreferredTaskDisplayArea.getFocusedRootTask();
final Task prevTopTask = prevTopRootTask != null ? prevTopRootTask.getTopLeafTask() : null;
final Task reusedTask = getReusableTask();

final Task targetTask = reusedTask != null ? reusedTask : computeTargetTask();
final boolean newTask = targetTask == null;
mTargetTask = targetTask;

getFocusedRootTask会尝试去获取首选的Task,如果不存在也会从当前显示屏获取获取最顶部的可触摸并且在展示的Task。而这个preTopTask如果能够获取到,它又会去获取的它叶子节点。叶子节点的规则就是没有只节点。Task相关类的继承结果如下:

classDiagram
class ConfigurationContainer {
<<abstract>>
}
class WindowContainer
class TaskFragment
class Task
ConfigurationContainer <|--WindowContainer
WindowContainer <|-- TaskFragment
TaskFragment <|--Task
WindowContainer <|--RootWindowContainer
1
2
3
4
if (mTargetRootTask == null) {
 mTargetRootTask = getOrCreateRootTask(mStartActivity, mLaunchFlags, targetTask,
 mOptions);
}

这里最终会拿到RootTask,如果没有也会创建,具体代码这里不分析了。

调用Task的 resumeFocusedTasksTopActivities

之后会调用如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//ActivityStarter
 ...
 mTargetRootTask.startActivityLocked(mStartActivity, topRootTask, newTask, isTaskSwitch,
 mOptions, sourceRecord); //这个名字是startActivityLock但并不是真的打开activity,而是把Activity对应的task放到列表的最前面,以及会展示window动画
 if (mDoResume) {
 if (!mTargetRootTask.isTopActivityFocusable()
 || (topTaskActivity != null && topTaskActivity.isTaskOverlay()
 && mStartActivity != topTaskActivity)) {
 //对样式pip页面或者其他一些情况的处理
 ...
 } else {
 ...
 //真正的启动Activity的代码这里是入口
 mRootWindowContainer.resumeFocusedTasksTopActivities(
 mTargetRootTask, mStartActivity, mOptions, mTransientLaunch);
 }
 ...

 }
 ...

 return START_SUCCESS;
 }

我们看到首先调用了startActivityLocked方法,这里主要做的就是把我们的ActivityReccord放到Task中去,并且展示Activity的启动动画。之后调用的RootContainerresumeFocsedTasksTopActivities才是真正的启动,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
//RootWindowContainer.java
boolean resumeFocusedTasksTopActivities(Task targetRootTask, ActivityRecord target, ActivityOptions targetOptions,
boolean deferPause) {
 ...
 boolean result = false;
 if (targetRootTask != null && (targetRootTask.isTopRootTaskInDisplayArea()
 || getTopDisplayFocusedRootTask() == targetRootTask)) {
 result = targetRootTask.resumeTopActivityUncheckedLocked(
 target,targetOptions, deferPause); //执行启动
 }
 ...
}

后面会走到Task的resumeTopActivityUnCheckedLocked方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
boolean resumeTopActivityUncheckedLocked(ActivityRecord prev, ActivityOptions options,
 boolean deferPause) {
 ...
 if (isLeafTask()) {
 if (isFocusableAndVisible()) { //可触摸可见
 someActivityResumed = resumeTopActivityInnerLocked(prev, options, deferPause);
 }
 } else {
 int idx = mChildren.size() - 1;
 while (idx >= 0) {
 final Task child = (Task) getChildAt(idx--);
 if (!child.isTopActivityFocusable()) {
 continue;
 }
 if (child.getVisibility(null /* starting */)
 != TASK_FRAGMENT_VISIBILITY_VISIBLE) {
 if (child.topRunningActivity() == null) {
 continue;
 }
 break;
 }

 someActivityResumed |= child.resumeTopActivityUncheckedLocked(prev, options,
 deferPause);
 if (idx >= mChildren.size()) {
 idx = mChildren.size() - 1;
 }
 }
 }


}

此处如果当前的Task本来就是叶子节点,那么会调用resumeTopActivityInnerLocked方法,否则会遍历子的task列表,在子task列表中找到符合条件的去执行resumeTopActivityUncheckedLocked方法,如此最后还是会调用到resumeTopActivityInnerLocked方法,而我们再跟进去看,可以看到其中的核心逻辑是调用topFragment的resumeTopActivity方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
final boolean resumeTopActivity(ActivityRecord prev, ActivityOptions options,
 boolean deferPause) {
 ...
 boolean pausing = !deferPause && taskDisplayArea.pauseBackTasks(next);
 if (mResumedActivity != null) {
 pausing |= startPausing(mTaskSupervisor.mUserLeaving, false /* uiSleeping */,
 next, "resumeTopActivity"); //首先把顶部处于Resumed状态的activity执行pausing
}
if (pausing) {
 //检查即将启动的Activity的Activity的进程有没有起来,如果没有进程去创建进程,创建进程的代码需要单独分析,此处略过
 if (next.attachedToProcess()) {
 next.app.updateProcessInfo(false /* updateServiceConnectionActivities */,
 true /* activityChange */, false /* updateOomAdj */,
 false /* addPendingTopUid */);
 } else if (!next.isProcessRunning()) {
 final boolean isTop = this == taskDisplayArea.getFocusedRootTask();
 mAtmService.startProcessAsync(next, false /* knownToBeDead */, isTop,
 isTop ? HostingRecord.HOSTING_TYPE_NEXT_TOP_ACTIVITY
 : HostingRecord.HOSTING_TYPE_NEXT_ACTIVITY);
 }
 ...
 return true;
 }
 if (next.attachedToProcess()) { //如何Activity已经在这个进程中了
 ...
 final ClientTransaction transaction =
 ClientTransaction.obtain(next.app.getThread(), next.token); //构建ClientTransaction,传入要打开的Activity对应的applicationThread和IBinder
 ...
 if (next.newIntents != null) { //把intent放进去,后面会把Activity吊起,并 调用onNewIntent()
 transaction.addCallback(
 NewIntentItem.obtain(next.newIntents, true /* resume */));
 }
 ...
 mAtmService.getLifecycleManager().scheduleTransaction(transaction);

 } else {
 ...
 mTaskSupervisor.startSpecificActivity(next, true, true); //启动Activity
 }


}

上面最后的代码可以看到,Activity已经存在的时候是走到onNewIntent, 调用的代码被包装成了ClientTransaction,通过ClientlifecycleManager 的scheduleTransaction方法,最终其实是调用了IApplicationThread的scheduleTransaction,最终通过binder调用到了app进程中的同名方法,这里要去看ActivityThread, ApplicationThread为它的内部类,看它的代码,它实际调用了ActivityThread的同名方法。而启动Activity,我们一路跟着startSpecificActivity()方法进去最终会看到也是通过ClientTransaction,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
clientTransaction.addCallback(LaunchActivityItem.obtain(new Intent(r.intent),
 System.identityHashCode(r), r.info,
 // TODO: Have this take the merged configuration instead of separate global 
 // and override configs. 
 mergedConfiguration.getGlobalConfiguration(),
 mergedConfiguration.getOverrideConfiguration(), r.compat,
 r.getFilteredReferrer(r.launchedFromPackage), task.voiceInteractor,
 proc.getReportedProcState(), r.getSavedState(), r.getPersistentSavedState(),
 results, newIntents, r.takeOptions(), isTransitionForward,
 proc.createProfilerInfoIfNeeded(), r.assistToken, activityClientController,
 r.shareableActivityToken, r.getLaunchedFromBubble(), fragmentToken));
// Set desired final state. 
final ActivityLifecycleItem lifecycleItem;
if (andResume) {
 lifecycleItem = ResumeActivityItem.obtain(isTransitionForward,
 r.shouldSendCompatFakeFocus());
} else {
 lifecycleItem = PauseActivityItem.obtain();
}
clientTransaction.setLifecycleStateRequest(lifecycleItem);
mService.getLifecycleManager().scheduleTransaction(clientTransaction);

目标进程执行performLaunchActivity

我们之前已经分析过ClientTransaction,我们知道这个LaunchActivityItem的callback,最后client就是我们的ActivityThread,会执行它的handleLaunchActivity方法,其中最核心的就是如下这一句:

1
final Activity a = performLaunchActivity(r, customIntent);

我们继续往里面看:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//content of performLaunchActivity() function
...
//为Activity创建Base Context
ContextImpl appContext = createBaseContextForActivity(r);
Activity activity = null;
...
//创建Activity实例,就是通过反射来实例化一个Activity实例
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
 cl, component.getClassName(), r.intent);
...
//拿到Application的实例,如果缓存中有就用,没有就创建一个新的,这里也不看具体代码了
Application app = r.packageInfo.makeApplicationInner(false, mInstrumentation);
...
//为Activity创建配置,如里面有语言,屏幕设置等等参数,不具体分析了
Configuration config =
 new Configuration(mConfigurationController.getCompatConfiguration());
if (r.overrideConfig != null) {
 config.updateFrom(r.overrideConfig);
}
...
//把Activity和baseContext绑定,并且把一些参数附加到Activity实例上去
appContext.setOuterContext(activity);
activity.attach(appContext, this, getInstrumentation(), r.token,
 r.ident, app, r.intent, r.activityInfo, title, r.parent,
 r.embeddedID, r.lastNonConfigurationInstances, config,
 r.referrer, r.voiceInteractor, window, r.activityConfigCallback,
 r.assistToken, r.shareableActivityToken);
...
mInstrumentation.callActivityOnCreate(activity, r.state); //这一步完成,Activity里面会执行完onCreate()
...
r.setState(ON_CREATE);

再来具体看一看Activity的attach方法中做了什么

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
attachBaseContext(context);
mFragments.attachHost(null /*parent*/);
mWindow = new PhoneWindow(this, window, activityConfigCallback);
....
mWindow.setWindowManager(
 (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
 mToken, mComponent.flattenToString(),
 (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
mWindowManager = mWindow.getWindowManager();
mCurrentConfig = config;

可以看到在里面绑定了baseContext,以及创建了我们的PhoneWindow,以及把window和windowManager进行绑定,还有其他一些Activity内部会用到的参数的传递。

而mInstrumentation里面最终是会调用Activity的performCreate,其中则会调用activity的onCreate。这里就不贴相关代码了。

这样我们的Activity才走完onCreate,而剩余步骤,我们之前还设置了LifecycleStateRequestResumeActivityItem,因此这是要让我们的Activity最终进入到Resume状态,具体的可以参看ClientTransaction分析。两篇文章配合着一起,就是完整的Activity启动流程了。

总结

从Activity调用startActivity,一直到A T M S调用ActivityStarter的调用时序图如下:

sequenceDiagram
Activity->>Activity: startActivity
Activity->>Instrumentation:execStartActivity
Instrumentation->>ATMS: startActivity
ATMS->>ActivityStarter: execute

从ActivityStarter调用到新的进程处理的时序图如下(省略了到Activity部分的流程):

sequenceDiagram
ActivityStarter->>ActivityStarter: resolveActivity
ActivityStarter->>ActivityStarter: executeRequest
ActivityStarter->>ActivityStarter: startActivityUnchecked
ActivityStarter->>RootWindowContainer: resumeFocusedTasksTopActivities
RootWindowContainer->>Task: resumeTopActivityUncheckedLocked
Task->>Task: resumeTopActivity
Task->>ActivityTaskSupervisor: startSpecificActivity
ActivityTaskSupervisor->>ActivityTaskSupervisor: realStartActivityLocked
ActivityTaskSupervisor->>ActivityThread: handleLaunchActivity
note right of ActivityTaskSupervisor: 通过ClientTransaction
ActivityThread->>ActivityThread: performLaunchActivity

以上就是一个较为精简的Activity启动的流程。其中省略了不少东西,关于startActivityForResult的情况需要获取到打开的Activity的结果的情况这里还没有讨论。

看代码可以发现Activity的启动过程是非常的复杂的,再加上新版本的Android支持多屏幕,折叠屏,分屏,画中画等等非常多的特性,因而Task的复用,新建就很复杂,因此本文这一部分暂时放下,等到以后在写。

如果你也对于Android系统源码感兴趣,欢迎与我交流。博文因为个人局限,也难免会出现差错,欢迎大家指正。

看完评论一下吧

跨境汇款神器:熊猫速汇

2024年9月11日 00:00

上次介绍跨境汇款服务Wise后,不少国内朋友向我反馈,国内用户无法通过Wise汇出人民币……由于我是用UK信息注册,所以没有遇到这个政策问题。

对于这种特殊情况,国内朋友完全可以使用熊猫速汇(支付宝、微信的国际汇款合作商之一),专门解决人民币汇出中国大陆和其它货币汇入中国大陆的难题

注册大陆地区熊猫速汇一般都是送10元汇全球手续费折扣券,熊猫速汇给通过土木坛子链接注册的朋友一些额外优惠:

  • 人民币汇出“随机派发10-30人民币手续费优惠券,限时:首笔汇款日本或新加坡的客户,由土木坛子联系渠道部的工作人员领取30元人民币优惠券”;
  • 港币回国“首笔免手续费,支付宝汇率提升券,内部会员汇率提升券(限时无次数),限时免手续费活动,8.88现金红包”;
  • 新币回国“首次免除手续费,支付宝、微信汇率提升券,不限时内部会员免除手续费,每个月不定期派发汇率提升券,8.88现金红包”;
  • 英镑/欧元回国“首笔免手续费,闪付收费4‰,老用户8‰,支付宝汇率提升券、微信汇率提升券”。

人民币汇出为什么有安全合规的保障?

熊猫速汇“Panda Remit”是一家专注于提供快速、安全和便捷的国际汇款解决方案的跨境汇款服务提供商。他们致力于为个人和企业客户提供高效、透明和经济实惠的跨境支付服务,以助用户轻松完成国际汇款。该服务支持资金出国和回国,操作简单,费用透明,具有诸多优势。

熊猫速汇是通过与天津金城银行合作来实现汇款,天津金城银行股份有限公司成立于2015年4月,是银保监会批准成立的全国首批五家民营银行之一。金城速汇产品(汇出中国服务),汇款人信息均由金城银行依法合规采集,且由金城银行提供中国境内全部相关汇出汇款服务,熊猫速汇仅向金城银行提供境外收款人清分服务及向境外收款人提供汇兑服务。

当前熊猫速汇人民币汇出服务汇款单笔限额最低为人民币100元,最高为人民币49000元;根据熊猫速汇合作银行规定与授权,给予每人每年最高30万元人民币汇款额度。

中国大陆人民币资金汇往国外熊猫速汇优惠注册

中国大陆人民币资金汇往国外熊猫速汇优惠注册链接,或者直接扫描上方二维码。

注意,一个内地手机号码可以同时注册:大陆、香港、新加坡等其他各个地区熊猫速汇的账户。比如:你可以使用同一个手机APP切换不同的地区登录,然后可以使用大陆账号从大陆汇款到新加坡、香港、英国,也可以使用香港账号汇款到大陆、新加坡、日本,或者使用新加坡账号汇款到澳洲、美国,随心所欲、一个账户汇往全球。

境外资金汇往中国大陆熊猫速汇优惠注册

境外资金汇往中国大陆熊猫速汇优惠注册链接,或者直接扫描上方二维码。

注册流程:

  1. 注册账户:使用邀请链接注册,享受优惠。
  2. 实名认证:使用正确证件进行认证。
  3. 添加收款信息:填写收款人详细信息。
  4. 确认汇款:提交申请并完成付款。
  5. 确认到账:通常一个小时内到账。

熊猫速汇优点

  • 低成本:手续费仅为80元人民币,相较传统银行可大幅节省。
  • 快速到账:通常一小时内到账,效率高。
  • 无外汇额度占用:人民币汇款不占用外汇额度,但每人每年最高不超过30万人民币
  • 优惠汇率:提供优惠汇率。
  • 操作简单:注册到汇款流程简单,支持在线实名认证。
  • 安全保证:持有金融牌照并接受监管,确保资金安全。
  • 汇款全球:支持40多个国家/地区的汇款,包括iFast英国、OCBC新加坡、Wise等。
  • 费用透明:费用清晰可见。
  • 无需换汇:可直接付款人民币,避免限额问题。

熊猫速汇缺点

  • 非同名汇款,但合法资金来源并提供汇款证明即可。
  • 不同地区汇款需单独实名,实名证件要求不同。
  • 不提供个人IBAN,到账显示的非个人名称,不适合像OCBC的首次同名入金验证需求。

欢迎大家扫码下面二维码加入微信群共同交流汇款经验、技巧和问题(或者添加微信tumuhk,请注明“熊猫”),并领取熊猫速汇给通过我的优惠注册链接不定时发布的内部福利(熊猫速汇工作人员也在群内)。
熊猫速汇内部汇款福利群

Android源码分析: 应用进程启动分析

2024年9月9日 20:11

Android应用进程的启动,简单来说就是从zygot进程fork出来一个新进程,并对其进行一些初始化。这样做系统的一些代码和资源等等就不需要重复加载,一些环境变量也都不需要重新设置,可以说是很巧妙的设置。下面就来具体分析一下其初始化过程。

启动时机

应用进程的启动,一般是在创建四大组件,比如说启动Activity,Service,使用ContentProvider,有广播需要处理,这些情况需要创建进程。在我们分析的代码当中,除了这几种情况,BackupAngent也会涉及到创建App进程。

启动进程调用的为AMS当中的startProcessLocked方法, 我们注意看的话,AMS当中还有另一个方法startIsolatedProcess也是用来启动进程的,但是这个方法它启动的进程一般是给系统使用的,我们这里不会分析。

AMS调用启动进程

我们就从AMSstartProcessLocked这个方法开始看起来:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
final ProcessRecord startProcessLocked(String processName,
 ApplicationInfo info, boolean knownToBeDead, int intentFlags,
 HostingRecord hostingRecord, int zygotePolicyFlags, boolean allowWhileBooting,
 boolean isolated) {
 return mProcessList.startProcessLocked(processName, info, knownToBeDead, intentFlags,
 hostingRecord, zygotePolicyFlags, allowWhileBooting, isolated, 0 /* isolatedUid */,
 false /* isSdkSandbox */, 0 /* sdkSandboxClientAppUid */,
 null /* sdkSandboxClientAppPackage */,
 null /* ABI override */, null /* entryPoint */,
 null /* entryPointArgs */, null /* crashHandler */);
}

这里我们传入的参数intentFlags为0,zygotePolicyFlagsZYGOTE_POLICY_FLAG_LATENCY_SENSITIVE, allowWhileBootingfalse, isolatedfalse。之后startProcessLocked方法内部用调用了ProcessList的同名方法,其中我们关注的核心语句如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
ProcessRecord app;
...
app = getProcessRecordLocked(processName, info.uid); //从缓存中获取ProcessRecord
...
if (app == null) {
 app = newProcessRecordLocked(info, processName, isolated, isolatedUid, isSdkSandbox,
 sdkSandboxUid, sdkSandboxClientAppPackage, hostingRecord);
} else {
 app.addPackage(info.packageName, info.longVersionCode, mService.mProcessStats);
}
...
final boolean success =
 startProcessLocked(app, hostingRecord, zygotePolicyFlags, abiOverride);

以上代码可以看到,会先去获取是否有现有的processRecord可用,有的话就拿出来使用,没有的话会创建新的,之后会调用startProcessLocked方法。ProcessList中使用mProcessNames来存储ProcessRecord与processName和uid的对应关系,查找的逻辑就是从map中查找不再关注。newProcessRecordLocked方法则是创建新的ProcessRecord,并且会把各种信息保存到这个record当中去。我们这里可以继续看startProcessLocked方法,,最终会调用这个方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
boolean startProcessLocked(ProcessRecord app, HostingRecord hostingRecord,
 int zygotePolicyFlags, boolean disableHiddenApiChecks, boolean disableTestApiChecks,
 String abiOverride) {
 //对于从缓存拿到的ProcessRecord,把原来的信息清掉
 if (app.getPid() > 0 && app.getPid() != ActivityManagerService.MY_PID) {
 mService.removePidLocked(app.getPid(), app);
 app.setBindMountPending(false);
 app.setPid(0);
 app.setStartSeq(0);
 }
 app.unlinkDeathRecipient();
 app.setDyingPid(0);
 ...
 final IPackageManager pm = AppGlobals.getPackageManager();
 permGids pm.getPackageGids(app.info.packageName, =
 MATCH_DIRECT_BOOT_AUTO, app.userId);
 StorageManagerInternal storageManagerInternal = LocalServices.getService(
 StorageManagerInternal.class);
 mountExternal = storageManagerInternal.getExternalStorageMountMode(uid,
 app.info.packageName); //检查外部存储访问权限
 externalStorageAccess = storageManagerInternal.hasExternalStorageAccess(uid,
 app.info.packageName);
 if (pm.checkPermission(Manifest.permission.INSTALL_PACKAGES,
 app.info.packageName, userId)
 == PackageManager.PERMISSION_GRANTED) { //检查安装应用的权限
 Slog.i(TAG, app.info.packageName + " is exempt from freezer");
 app.mOptRecord.setFreezeExempt(true);
 }
 if (app.processInfo != null && app.processInfo.deniedPermissions != null) {
 for (int i = app.processInfo.deniedPermissions.size() - 1; i >= 0; i--) {
 int[] denyGids = mService.mPackageManagerInt.getPermissionGids(
 app.processInfo.deniedPermissions.valueAt(i), app.userId);
 if (denyGids != null) {
 for (int gid : denyGids) {
 permGids = ArrayUtils.removeInt(permGids, gid);
 }
 }
 }
 }

 gids = computeGidsForProcess(mountExternal, uid, permGids, externalStorageAccess); //根据前面的权限和相关信息,计算新启动的进程 需要分配的用户
 ...
 //读取app的debuggable,profileable等标志位
 boolean debuggableFlag = (app.info.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
 boolean isProfileableByShell = app.info.isProfileableByShell();
 boolean isProfileable = app.info.isProfileable();
 if (debuggableFlag) {
 runtimeFlags |= Zygote.DEBUG_ENABLE_JDWP;
 runtimeFlags |= Zygote.DEBUG_JAVA_DEBUGGABLE;
 runtimeFlags |= Zygote.DEBUG_ENABLE_CHECKJNI;


 if (android.provider.Settings.Global.getInt(mService.mContext.getContentResolver(), android.provider.Settings.Global.ART_VERIFIER_VERIFY_DEBUGGABLE, 1) == 0) {
 runtimeFlags |= Zygote.DISABLE_VERIFIER;
 }
 }
 if (isProfileableByShell) {
 runtimeFlags |= Zygote.PROFILE_FROM_SHELL;
 }
 if (isProfileable) {
 runtimeFlags |= Zygote.PROFILEABLE;
 } //把标志位信息保存到runtimeFlags中
 ...//其他一些flag写入到runtimeFlags中去
 if (debuggableFlag) {
 //debuggable时候使用wrap.sh去fork进程
 String wrapperFileName = app.info.nativeLibraryDir + "/wrap.sh";
 StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
 try {
 if (new File(wrapperFileName).exists()) {
 invokeWith = "/system/bin/logwrapper " + wrapperFileName;
 }
 } finally {
 StrictMode.setThreadPolicy(oldPolicy);
 }
 }
 String requiredAbi = (abiOverride != null) ? abiOverride : app.info.primaryCpuAbi;
 if (requiredAbi == null) { //设置 app native 库使用的abi,如arm或者x86或者armv8等等
 requiredAbi = Build.SUPPORTED_ABIS[0];
 }
 String instructionSet = null;
 if (app.info.primaryCpuAbi != null) {
 instructionSet = VMRuntime.getInstructionSet(requiredAbi);
 }

 app.setGids(gids);
 app.setRequiredAbi(requiredAbi);
 app.setInstructionSet(instructionSet); //把信息都设置到ProcessRecord中
 final String seInfo = app.info.seInfo
 + (TextUtils.isEmpty(app.info.seInfoUser) ? "" : app.info.seInfoUser);
 final String entryPoint = "android.app.ActivityThread"; //设置进程入口位ActivityThread

 return startProcessLocked(hostingRecord, entryPoint, app, uid, gids,
 runtimeFlags, zygotePolicyFlags, mountExternal, seInfo, requiredAbi,
 instructionSet, invokeWith, startUptime, startElapsedTime);

}

以上代码主要是检查应用的各种权限,对其设置对应权限组的groupId,以及设置应用的Abi等信息。之后又会启动一个新的startProcessLocked方法,其中仍然是给ProcessRecord设置参数,其中很大篇幅的为设置debug和profilable相关的参数设置,这里就不列出参数设置的代码了,只列以下最后启动调用的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
if (mService.mConstants.FLAG_PROCESS_START_ASYNC) {
 mService.mProcStartHandler.post(() -> handleProcessStart(
 app, entryPoint, gids, runtimeFlags, zygotePolicyFlags, mountExternal,
 requiredAbi, instructionSet, invokeWith, startSeq));
 return true;
} else {
 final Process.ProcessStartResult startResult = startProcess(hostingRecord,
 entryPoint, app,
 uid, gids, runtimeFlags, zygotePolicyFlags, mountExternal, seInfo,
 requiredAbi, instructionSet, invokeWith, startUptime);
 handleProcessStartedLocked(app, startResult.pid, startResult.usingWrapper,
 startSeq, false);
 return app.getPid() > 0;
}

这里有两个分支,这个 FLAG_PROCESS_START_ASYNC 默认为True,是通过系统的Setting去设置的。第一个分支是通过Handle把任务抛出去执行,而直接返回了执行成功,另一个分支则是等待任务执行完成,在根据返回的UID检查是否成功。不过两个分支里面都是执行了startProcess方法,在这个方法中我们关注以下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
if (hostingRecord.usesWebviewZygote()) { //webview进程的创建
 startResult = startWebView(entryPoint,
 app.processName, uid, uid, gids, runtimeFlags, mountExternal,
 app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet,
 app.info.dataDir, null, app.info.packageName,
 app.getDisabledCompatChanges(),
 new String[]{PROC_START_SEQ_IDENT + app.getStartSeq()});
} else if (hostingRecord.usesAppZygote()) {
 final AppZygote appZygote = createAppZygoteForProcessIfNeeded(app);

 startResult = appZygote.getProcess().start(entryPoint,
 app.processName, uid, uid, gids, runtimeFlags, mountExternal,
 app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet,
 app.info.dataDir, null, app.info.packageName,
 /*zygotePolicyFlags=*/ ZYGOTE_POLICY_FLAG_EMPTY, isTopApp,
 app.getDisabledCompatChanges(), pkgDataInfoMap, allowlistedAppDataInfoMap,
 false, false,
 new String[]{PROC_START_SEQ_IDENT + app.getStartSeq()});
} else {
 regularZygote = true;
 startResult = Process.start(entryPoint,
 app.processName, uid, uid, gids, runtimeFlags, mountExternal,
 app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet,
 app.info.dataDir, invokeWith, app.info.packageName, zygotePolicyFlags,
 isTopApp, app.getDisabledCompatChanges(), pkgDataInfoMap,
 allowlistedAppDataInfoMap, bindMountAppsData, bindMountAppStorageDirs,
 new String[]{PROC_START_SEQ_IDENT + app.getStartSeq()});
}

以上可以看到我们在创建新的进程的时候,会有三个分支,我们回看我们创建HostingRecord 时候是调用的如下的构造方法:

1
2
3
4
5
public HostingRecord(@NonNull String hostingType, ComponentName hostingName, boolean isTopApp) {
 this(hostingType, hostingName.toShortString(), REGULAR_ZYGOTE,
 null /* definingPackageName */, -1 /* mDefiningUid */, isTopApp /* isTopApp */,
 null /* definingProcessName */, null /* action */, TRIGGER_TYPE_UNKNOWN);
}

Process启动进程调用

因此上面的代码是走到了regular分支,它调用了Processstart方法, Process中又调用了ZYGOTE_PROCESSstart方法, ZYGOTE_PROCESS为一个ZygoteProcess常量,其中又会调用startViaZygoate方法,我们来看看这个方法的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private Process.ProcessStartResult startViaZygote(@NonNull final String processClass, @Nullable final String niceName, final int uid, final int gid, @Nullable final int[] gids,
 int runtimeFlags, int mountExternal,
 int targetSdkVersion,
 @Nullable String seInfo,
 @NonNull String abi,
 @Nullable String instructionSet,
 @Nullable String appDataDir,
 @Nullable String invokeWith,
 boolean startChildZygote,
 @Nullable String packageName,
 int zygotePolicyFlags,
 boolean isTopApp,
 @Nullable long[] disabledCompatChanges,
 @Nullable Map<String, Pair<String, Long>> pkgDataInfoMap,
 @Nullable Map<String, Pair<String, Long>> allowlistedDataInfoList,
 boolean bindMountAppsData,
 boolean bindMountAppStorageDirs,
 @Nullable String[] extraArgs) throws ZygoteStartFailedEx {
 ArrayList<String> argsForZygote = new ArrayList<>();
 argsForZygote.add("--runtime-args");
 argsForZygote.add("--setuid=" + uid);
 argsForZygote.add("--setgid=" + gid);
 argsForZygote.add("--runtime-flags=" + runtimeFlags);
 ....
 synchronized(mLock) {
 return zygoteSendArgsAndGetResult(openZygoteSocketIfNeeded(abi),
 zygotePolicyFlags,
 argsForZygote);
}

上面的代码就是把我们之前所有的各种参数,都拼接起来放到一个字符数组中,后面的openZygoteSocketIfNeeded则是根据abi来于zygote进程建立socket连接,其他的我就要进入zygoteSendArgsAndGetResult方法中查看详情了。

1
2
3
4
5
if (shouldAttemptUsapLaunch(zygotePolicyFlags, args)) {
 return attemptUsapSendArgsAndGetResult(zygoteState, msgStr);
}

return attemptZygoteSendArgsAndGetResult(zygoteState, msgStr);

这里有一个判断是否要使用usap进程池(非专门app使用进程池),不过我看了这里mUsapPoolEnabled字段默认为false,那我们就不看这个分支了。而attemptZygoteSendArgsAndGetResult代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private Process.ProcessStartResult attemptZygoteSendArgsAndGetResult(
 ZygoteState zygoteState, String msgStr) throws ZygoteStartFailedEx {
 try {
 final BufferedWriter zygoteWriter = zygoteState.mZygoteOutputWriter;
 final DataInputStream zygoteInputStream = zygoteState.mZygoteInputStream;

 zygoteWriter.write(msgStr);
 zygoteWriter.flush();

 Process.ProcessStartResult result = new Process.ProcessStartResult();
 result.pid = zygoteInputStream.readInt();
 result.usingWrapper = zygoteInputStream.readBoolean();

 if (result.pid < 0) {
 throw new ZygoteStartFailedEx("fork() failed");
 }

 return result;
 } catch (IOException ex) {
 zygoteState.close();
 Log.e(LOG_TAG, "IO Exception while communicating with Zygote - "
 + ex.toString());
 throw new ZygoteStartFailedEx(ex);
 }
}

从上面的代码我们可以看到,这里其实很简单,就是通过socket向Zytgote发送了我们启动进程需要的参数,然后再通过socket从Zygote读出创建的进程的pid。

Zygote进程创建子进程

这个时候我们需要来看ZygoteInit的main方法,具体zygote进程是如何在系统启动的时候创建的就不去关注了,这里来关注zygote进程如何去创建应用进程的,这里摘抄了一些它的main函数的代码:

1
2
3
4
5
6
7
preload(bootTimingsTraceLog); //zygote启动之后,预加载代码资源等
zygoteServer = new ZygoteServer(isPrimaryZygote); //创建Zygote 的socket server
caller = zygoteServer.runSelectLoop(abiList); // socket server进入监听状态

if (caller != null) {
 caller.run(); //子进程中的时候caller不为空,会执行,此处会执行我们的ActivityThread的main方法,先分析上面的runSelectLoop,其中会有caller的创建
}

runSelectLoop内我们比较关注的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Runnable runSelectLoop(String abiList) {
 while (true) {
 pollReturnValue = Os.poll(pollFDs, pollTimeoutMs);
 if (pollReturnValue == 0) {
 ...
 } else {
 while (--pollIndex >= 0) {
 if (pollIndex == 0) {
 //如果pollIndex为0,则说明没有socket连接,需要创建socket连接
 ZygoteConnection newPeer = acceptCommandPeer(abiList);
 peers.add(newPeer);
 socketFDs.add(newPeer.getFileDescriptor());
 } else if (pollIndex < usapPoolEventFDIndex) { //读取Primary socket
 ZygoteConnection connection = peers.get(pollIndex);
 boolean multipleForksOK = !isUsapPoolEnabled() && ZygoteHooks.isIndefiniteThreadSuspensionSafe();
 final Runnable command = connection.processCommand(this, multipleForksOK);
 if (mIsForkChild) {
 return command; //子进程,返回command
 } else {
 //父进程的一些处理
 }
 ...
 }
 ....
 }
 ...
 }

 }
}

上面的代码省略了一些如果是Usap进程的代码,代码里面有两层的循环,在内层循环中,以pollIndex作为循环的条件,如果pollIndex为0,在acceptCommandPeer中会建立新的Socket Connet,代码里面就是一个ZygoteConnection。如果存在Connect的情况下会,会通过判断当前pollIndex是否小于usapPollEventFDIndex来判断是否是普通的进程创建,之后会调用connection.processCommand来读取socket数据做后续的处理,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
ZygoteArguments parsedArgs;
try (ZygoteCommandBuffer argBuffer = new ZygoteCommandBuffer(mSocket)) {
 while (true) {
 parsedArgs = ZygoteArguments.getInstance(argBuffer);
 ...
 if (parsedArgs.mInvokeWith != null || parsedArgs.mStartChildZygote
 || !multipleOK || peer.getUid() != Process.SYSTEM_UID) {
 pid = Zygote.forkAndSpecialize(parsedArgs.mUid, parsedArgs.mGid,
 parsedArgs.mGids, parsedArgs.mRuntimeFlags, rlimits,
 parsedArgs.mMountExternal, parsedArgs.mSeInfo, parsedArgs.mNiceName,
 fdsToClose, fdsToIgnore, parsedArgs.mStartChildZygote,
 parsedArgs.mInstructionSet, parsedArgs.mAppDataDir,
 parsedArgs.mIsTopApp, parsedArgs.mPkgDataInfoList,
 parsedArgs.mAllowlistedDataInfoList, parsedArgs.mBindMountAppDataDirs,
 parsedArgs.mBindMountAppStorageDirs); //fork子进程的操作

 try {
 if (pid == 0) { //子进程处理分支
 zygoteServer.setForkChild();

 zygoteServer.closeServerSocket();
 IoUtils.closeQuietly(serverPipeFd);
 serverPipeFd = null;

 return handleChildProc(parsedArgs, childPipeFd,
 parsedArgs.mStartChildZygote);
 } else { //父进程处理分支
 IoUtils.closeQuietly(childPipeFd);
 childPipeFd = null;
 handleParentProc(pid, serverPipeFd);
 return null;
 }
 } finally {
 IoUtils.closeQuietly(childPipeFd);
 IoUtils.closeQuietly(serverPipeFd);
 }
 } else {
 ...
 }
 ...
 }
}

上面代码是处理socket数据的代码,我这里省略了除了创建进程之外的处理其他操作的代码。其中我们可以看到系统是使用了ZygoteArguments来解析我们之前从system_server进程传过来的参数,之后调用Zygote.forkAndSpecialize来创建进程,在linux中,fork完进程之后,是通过pid来判断当前是在父进程还是子进程中的,当前为子进程则pid为0。forkAndSpecialize方法中主要是调用了nativeForkAndSpecialize,这个是native方法,代码在com_android_internal_os_Zygote.cpp中,在native中的方法为com_android_internal_os_Zygote_nativeForkAndSpecialize我们去看看它的代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
...
pid_t pid = zygote::ForkCommon(env, /* is_system_server= */ false, fds_to_close, fds_to_ignore, true);
if (pid == 0) {
 SpecializeCommon(env, uid, gid, gids, runtime_flags, rlimits, capabilities, capabilities,
 mount_external, se_info, nice_name, false, is_child_zygote == JNI_TRUE,
 instruction_set, app_data_dir, is_top_app == JNI_TRUE, pkg_data_info_list,
 allowlisted_data_info_list, mount_data_dirs == JNI_TRUE,
 mount_storage_dirs == JNI_TRUE);
}
return pid;

上面的代码可以看到,第一行是去fork子进程,后面会判断是否为子进程,如果为子进程则会为子进程做一些处理。其中我省略了前面一部分fds_to_close 和fds_to_ignore赋值的代码,那些为需要关闭或者忽略的文件描述符,会传到这个ForkCommon方法中,我们具体看看这个方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
SetSignalHandlers(); //设置错误信号监听
BlockSignal(SIGCHLD, fail_fn); //暂时关闭SIGCHLD信号,方便后面关闭fd
__android_log_close(); //关闭log相关的FD
AStatsSocket_close();
...
pid_t pid = fork(); //调用系统调用执行fork进程

if (pid == 0) {
 ...
 PreApplicationInit(); //子进程的初始化,主要是设置当前进程不是zygote进程
 DetachDescriptors(env, fds_to_close, fail_fn); //把传进来的要关闭的fd关掉
 ...
} else {
 ...
}

UnblockSignal(SIGCHLD, fail_fn); //重新打开之前关闭的SIGCHLD信号
return pid;

可以看到上面的代码主要是去调用fork系统调用去从zygote进程fork一个新进程作为应用使用的进程,而SpecializeCommon,我们根据传入的参数和代码可以知道,其中主要是设置子进程的用户组,以及挂载应用目录,一些其他相关的初始化,就不分析其代码了。然后我们就可以继续会到java代码。

创建完子进程后的操作

在前面processCommand方法中,我们知道fork成功之后如果是子进程会执行handleChildProc方法,如果是父进程会执行handleParentProc方法,先来看一下父进程执行的代码:

1
2
3
4
5
6
if (pid > 0) {
 setChildPgid(pid);
}
...
mSocketOutStream.writeInt(pid);
mSocketOutStream.writeBoolean(usingWrapper);

这个方法中我们需要关注的就上面这一部分代码,首先是把这个子进程的pid放到进程的当前进程的孩子进程组中去。后面的就是把子进程的pid和是否使用了wrapper写入到socket中,这样我们之前请求创建进程那个地方就能拿到子进程的id了。

再来看子进程所执行的handleChildProc方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
closeSocket();
Zygote.setAppProcessName(parsedArgs, TAG);

if (parsedArgs.mInvokeWith != null) {
 WrapperInit.execApplication(parsedArgs.mInvokeWith,
 parsedArgs.mNiceName, parsedArgs.mTargetSdkVersion,
 VMRuntime.getCurrentInstructionSet(),
 pipeFd, parsedArgs.mRemainingArgs);

 // Should not get here. 
 throw new IllegalStateException("WrapperInit.execApplication unexpectedly returned");
} else {
 if (!isZygote) {
 return ZygoteInit.zygoteInit(parsedArgs.mTargetSdkVersion,
 parsedArgs.mDisabledCompatChanges,
 parsedArgs.mRemainingArgs, null /* classLoader */);
 } else {
 return ZygoteInit.childZygoteInit(
 parsedArgs.mRemainingArgs /* classLoader */);
 }
}

首先第一行是关闭socket,前面的native代码其实已经关闭过了socket,但是在java层还是有LocalSocket,也需要关闭。 第二行就是给我们这个进程设置名称。 后面的第一个判断是看我们是否使用wrapper,正常流程不会走到这里,else分支中我们这里也不是fork一个新的zygote进程,因此也只需要看ZygoteInit.zygoteInit这个方法即可。

1
2
3
4
5
6
RuntimeInit.redirectLogStreams(); //关闭默认的log,设置使用android的print来输出system.out和system.error的log

RuntimeInit.commonInit();
ZygoteInit.nativeZygoteInit();
return RuntimeInit.applicationInit(targetSdkVersion, disabledCompatChanges, argv,
 classLoader);

App进程的初始化

RuntimeInit.commonInit中是一些初始化,包括错误处理,时区,网络的userAgent等,不看代码了。nativeZygoteInit的代码在AndroidRuntime.cpp

1
2
3
4
static void com_android_internal_os_ZygoteInit_nativeZygoteInit(JNIEnv* env, jobject clazz)
{
 gCurRuntime->onZygoteInit();
}

此处调用了gCurRuntimeonZygoteInit()方法,而这个方法是AndroidRuntime中的一个虚方法,在app_main.cpp中我们看到实际上对于应用我们有一个子类AppRuntime中实现了这个方法,代码如下:

1
2
3
4
5
6
virtual void onZygoteInit()
{
 sp<ProcessState> proc = ProcessState::self();
 ALOGV("App process: starting thread pool.\n");
 proc->startThreadPool();
}

我们之前分析binder的时候,知道ProcessState这个类binder是有使用的,调用self方法会打开binder驱动,这个代码里面是为binder创建应用进程的线程池,具体这里就不分析了。继续看RuntimeInit.applicationInit代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
protected static Runnable applicationInit(int targetSdkVersion, long[] disabledCompatChanges,
 String[] argv, ClassLoader classLoader) {
 nativeSetExitWithoutCleanup(true);

 VMRuntime.getRuntime().setTargetSdkVersion(targetSdkVersion);
 VMRuntime.getRuntime().setDisabledCompatChanges(disabledCompatChanges);

 final Arguments args = new Arguments(argv);

 return findStaticMain(args.startClass, args.startArgs, classLoader);
}

上面的方面,前面的代码主要是设置targetSdkversion和其他的一些设置,我们主要来看后面的findStaticMain方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
protected static Runnable findStaticMain(String className, String[] argv,
 ClassLoader classLoader) {
 Class<?> cl;

 try {
 cl = Class.forName(className, true, classLoader);
 } catch (ClassNotFoundException ex) {
 throw new RuntimeException(
 "Missing class when invoking static main " + className,
 ex);
 }

 Method m;
 try {
 m = cl.getMethod("main", new Class[] { String[].class });
 } catch (NoSuchMethodException ex) {
 throw new RuntimeException(
 "Missing static main on " + className, ex);
 } catch (SecurityException ex) {
 throw new RuntimeException(
 "Problem getting static main on " + className, ex);
 }

 int modifiers = m.getModifiers();
 if (! (Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers))) {
 throw new RuntimeException(
 "Main method is not public and static on " + className);
 }
 return new MethodAndArgsCaller(m, argv);
}

可以看到我们通过反射拿到应用的之前设置的应用入口,也就是ActivityThread类,之后再获取到它的main方法,最后组装成一个MethodAndArgsCaller对象,最后返回。从前面的代码我们知道它会在ZygoteInitmain方法中执行。然后我们就可以来分析ActivityThread代码了。

ActivityThread代码执行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public static void main(String[] args) {
 AndroidOs.install();
 Environment.initForCurrentUser();
 final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());
 TrustedCertificateStore.setDefaultUserDirectory(configDir);
 initializeMainlineModules();

 Looper.prepareMainLooper();
 ...
 ActivityThread thread = new ActivityThread();
 thread.attach(false, startSeq);
 Looper.loop();
 throw new RuntimeException("Main thread loop unexpectedly exited");

}

上面的代码可以看到是为应用进程做一些初始化,首先是为sys call使用android的一些定制,其次是指定CA证书的位置,之后安装Mainline的模块,后面是初始化looper进入Looper循环,这样应用的主线程也就完成了初始化。在启动loop之前有一个attach方法,对于应用进程我们传进来的第一个参数为false, 也就是非系统进程,我们来看代码,只看应用进程的分支。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
RuntimeInit.setApplicationObject(mAppThread.asBinder());
final IActivityManager mgr = ActivityManager.getService();
try {
 mgr.attachApplication(mAppThread, startSeq);
} catch (RemoteException ex) {
 throw ex.rethrowFromSystemServer();
}

BinderInternal.addGcWatcher(new Runnable() {
 public void run() {
 //监听gc,当可用内存比较小的时候尝试回收一些Activity
 }
}

ViewRootImpl.ConfigChangedCallback configChangedCallback = (Configuration globalConfig) -> {
 //config 变化的回调,用来更新app得而configuration
}
ViewRootImpl.addConfigCallback(configChangedCallback);

上面的代码主要做了四件事情,其中两个是注册gc的回调和view configration 变化的回调,我们最关注的是调用ActivityManager的atttachApplication,和RuntimeInit的setApplicationObject,它们都用到了mAppThread,这个对象为ApplicationThread,而它是IApplicationThread.aidl的客户端实现。这里首先是把它的IBinder对象传到RuntimeInit中,这样发生一些事情的时候系统可以通知到应用。

回到AMS

另外我们再来看一下ActivityManager的attchApplication方法,它实际调用的是ActivityManagerServiceattachApplication方法,在它内部又调用了attachApplicationLocked方法,这里只看一下我们比较关心的一部分代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
boolean normalMode = mProcessesReady || isAllowedWhileBooting(app.info);
List<ProviderInfo> providers = normalMode
 ? mCpHelper.generateApplicationProvidersLocked(app)
 : null;
...
final ProviderInfoList providerList = ProviderInfoList.fromList(providers);
...
thread.bindApplication(processName, appInfo,
 app.sdkSandboxClientAppVolumeUuid, app.sdkSandboxClientAppPackage,
 providerList, null, profilerInfo, null, null, null, testMode,
 mBinderTransactionTrackingEnabled, enableTrackAllocation,
 isRestrictedBackupMode || !normalMode, app.isPersistent(),
 new Configuration(app.getWindowProcessController().getConfiguration()),
 app.getCompat(), getCommonServicesLocked(app.isolated),
 mCoreSettingsObserver.getCoreSettingsLocked(),
 buildSerial, autofillOptions, contentCaptureOptions,
 app.getDisabledCompatChanges(), serializedSystemFontMap,
 app.getStartElapsedTime(), app.getStartUptime());
...
if (normalMode) {
 try {
 didSomething = mAtmInternal.attachApplication(app.getWindowProcessController());
 } catch (Exception e) {
 Slog.wtf(TAG, "Exception thrown launching activities in " + app, e);
 badApp = true;
 }
}
if (!badApp) {
 try {
 didSomething |= mServices.attachApplicationLocked(app, processName);
 checkTime(startTime, "attachApplicationLocked: after mServices.attachApplicationLocked"); //检查是否有Service需要在当前进程启动
 } catch (Exception e) {
 Slog.wtf(TAG, "Exception thrown starting services in " + app, e);
 badApp = true;
 }
}

if (!badApp && isPendingBroadcastProcessLocked(pid)) {
 try {
 didSomething |= sendPendingBroadcastsLocked(app); //发送pending的广播
 checkTime(startTime, "attachApplicationLocked: after sendPendingBroadcastsLocked");
 } catch (Exception e) {
 // If the app died trying to launch the receiver we declare it 'bad' 
 Slog.wtf(TAG, "Exception thrown dispatching broadcasts in " + app, e);
 badApp = true;
 }
}

上面省略了一些代码,不过我们application需要做的一些核心代码都还在。除了列出的代码外,这里其实还有一些pending service启动,pending 广播的执行,以及ContentProvider的安装等,这些我们先略过。 首先这个normalMode的判断,我们假设当前已经是使用中而不是刚启动手机,而mProcessesReady是在system_server启动之后就赋值为true了,所以对于app启动的状况来说,这里normalMode为true。这里我们需要重点关注的就两个地方,一个是thread.bindApplication,另一处是mAtmInternal.attachApplication。bindApplication会通过binder调用到应用进程的bindApplication方法。

1
2
3
4
AppBindData data = new AppBindData();
data.processName = processName;
....
sendMessage(H.BIND_APPLICATION, data);

这里主要就去构建了AppBindData,使用ActivityThread内部的H来发送消息,消息回调处会调用ActivityThread的handleBindApplication方法。这个方法的代码非常多,前面的一些是设置包名,进程名称等等信息,以及configration信息以及调试器相关的东西,这些我们都不关注,这里跳过。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21

data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo, isSdkSandbox); //构建apk信息,创建LoadApk对象。

//创建AppContent,并且把confirmration绑定到Context上
final ContextImpl appContext = ContextImpl.createAppContext(this, data.info);
mConfigurationController.updateLocaleListFromAppContext(appContext);

//创建Instrumentation
mInstrumentation = new Instrumentation();
mInstrumentation.basicInit(this);

if (!data.restrictedBackupMode) { //执行安装ContentProvider
 if (!ArrayUtils.isEmpty(data.providers)) {
 installContentProviders(app, data.providers);
 }
}

//创建程序的Application
app = data.info.makeApplicationInner(data.restrictedBackupMode, null);
//调用Application的onCreate方法
mInstrumentation.callApplicationOnCreate(app);

上面的逻辑我们需要关注makeApplicationInner, 后面的callApplicationOnCreate内部就是调用Application的onCreate方法,不再分析了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
final Application cached = sApplications.get(mPackageName);
if (cached != null) {
 if (!allowDuplicateInstances) {
 mApplication = cached;
 return cached;
 }
}
ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
app = mActivityThread.mInstrumentation.newApplication(
 cl, appClass, appContext);
appContext.setOuterContext(app);

上面的代码主要是从缓存里面取Application,如果没有则通过Instrumentaion去创建新的Applicaion,我们继续去看newApplication的代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public Application newApplication(ClassLoader cl, String className, Context context)
 throws InstantiationException, IllegalAccessException,
 ClassNotFoundException {
 String appClass = mApplicationInfo.getCustomApplicationClassNameForProcess(
 myProcessName);
 Application app = getFactory(context.getPackageName())
 .instantiateApplication(cl, className);
 app.attach(context);
 return app;
}

这里的代码比较简单,就是拿到AppComponentFactory然后通过反射创建App的Application对象,之后调用app的attach方法,attach方法内部会调用attachBaseContext方法。就不往里去看代码了。

对于需要启动Activity的情况,我们需要看ActivityManagerServiceattachApplication,我们需要再看一下mAtmInternal.attachApplication。它会调用ActivityTaskManagerService的内部类LocalService的方法,内部会调用mRootWindowContainer.attachApplication(wpc);,它的内部又会调用mAttachApplicationHelper.process(app),内部又会调用ensureActivitiesVisible方法,一路看进去最终会调用EnsureActivitiesVisibleHelperprocess方法,它的内部会调用setActivityVisibilityState

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
if (!r.attachedToProcess()) {
 makeVisibleAndRestartIfNeeded(mStarting, mConfigChanges, isTop,
 resumeTopActivity && isTop, r);
} else if (r.isVisibleRequested()) {
 // If this activity is already visible, then there is nothing to do here. 
 if (DEBUG_VISIBILITY) {
 Slog.v(TAG_VISIBILITY, "Skipping: already visible at " + r);
 }

 if (r.mClientVisibilityDeferred && mNotifyClients) {
 r.makeActiveIfNeeded(r.mClientVisibilityDeferred ? null : starting);
 r.mClientVisibilityDeferred = false;
 }

 r.handleAlreadyVisible();
 if (mNotifyClients) {
 r.makeActiveIfNeeded(mStarting);
 }
} else {
 r.makeVisibleIfNeeded(mStarting, mNotifyClients);
}

这些就是去执行启动Activity相关的逻辑,这里也先略过。

以下是AMS发起创建新进程的时序图:

sequenceDiagram
AMS->>+ProcessList: startProcessLocked
ProcessList->>ProcessList: newProcessRecordLocked
ProcessList->>ProcessList: startProcess
ProcessList->>+ZygoteProcess: startViaZygote
ZygoteProcess->>ZygoteProcess: openZygoteSocketIfNeeded
ZygoteProcess->>ZygoteProcess: zygoteSendArgsAndGetResult
ZygoteProcess->>ZygoteServer: send args and get Result
ZygoteProcess-->>-ProcessList: return result with pid
ProcessList-->>-AMS: return ProcessRecord

以下是Zygote侧处理fork进程请求的时序图:

sequenceDiagram
ZygoteInit->>ZygoteServer: runSelectLoop
loop 无限循环
ZygoteServer->>ZygoteConnection: processCommand
ZygoteConnection->>Zygote: forkAndSpecialize
ZygoteConnection-->>ZygoteConnection: (Parent): notify child pid by socket
ZygoteServer-->>ZygoteInit: return command(child process)
end
rect rgb(191, 223, 255)
note right of ZygoteInit: fork完子进程执行的内容
ZygoteConnection->>ZygoteInit: zygoteInit
ZygoteInit->>ZygoteInit: nativeZygoteInit
ZygoteInit->>AppRuntime: onZygoteInit
ZygoteInit->>RuntimeInit: findStaticMain
RuntimeInit-->>ZygoteInit: return ActivityThread Main function caller
ZygoteInit->>ActivityThread: main
ActivityThread->>AMS: attachApplication
AMS->>ActivityThread: bindApplication
end

以上就是应用进程启动的完整流程,为了使得流程更加简洁,其中不太重要的步骤有作省略。如果你也对于Android系统源码感兴趣,欢迎与我交流。博文因为个人局限,也难免会出现差错,欢迎大家指正。

看完评论一下吧

Android Binder源码分析:AIDL及匿名服务传输

2024年9月7日 17:57

前面介绍的通过ServiceManager添加服务和获取服务,这些服务都是有名称的,我们可以通过ServiceManager来获取它。除此之外Android系统中还有一类Binder服务是匿名它,它们如何让客户端获得代理对象,并且使用呢,本文就一探究竟。

AIDL 介绍

AIDL全称为Android接口定义语言,是Android系统提供的一款可供用户用来抽象IPC的工具,它提供了语法让我们来定义跨进程通讯的服务接口,也就是.aidl文件,它也提供了工具,帮助我们把定义文件专程目标语言的代码。

我们自己使用AIDL创建的服务,或者一部分系统内的服务,比如IWindowSessionIApplicationThread等,这些多是运行在App进程,一般都不是系统服务,因此都是匿名的。我们这里以IApplicationThread来分析AIDL创建的匿名服务是怎么传递Binder给使用端的。

IApplicationThread来分析,则服务端是我们的App进程,而客户端则是system_server进程。作为AIDL创建的Binder,首先会有一个AIDL文件,这里是IApplicationThread.aidl,其中定义了一些跨进程调用的方法,部分内容如下:

package android.app;
...
oneway interface IApplicationThread {
void scheduleReceiver(in Intent intent, in ActivityInfo info,
in CompatibilityInfo compatInfo,
int resultCode, in String data, in Bundle extras, boolean sync,
int sendingUser, int processState);
@UnsupportedAppUsage
void scheduleStopService(IBinder token);
...
}

当前Android AIDL已经支持生成Java、C++、Rust的代码,对于IApplicationThread这里我们只需关注生成Java版本的代码即可。生成的代码在IApplicationThread当中,大概如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
public interface IApplicationThread extends android.os.IInterface {
public static class Default implements android.app.IApplicationThread {
 @Override
 public void scheduleReceiver(android.content.Intent
 ...) throws android.os.RemoteException {}

 @Override
 public android.os.IBinder asBinder() {
 return null;
 }
}

public abstract static class Stub extends android.os.Binder
 implements android.app.IApplicationThread {
 public Stub() {
 this.attachInterface(this, DESCRIPTOR);
 }

 public static android.app.IApplicationThread asInterface(android.os.IBinder obj) {
 if ((obj == null)) {
 return null;
 }
 android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
 if (((iin != null) && (iin instanceof android.app.IApplicationThread))) {
 return ((android.app.IApplicationThread) iin);
 }
 return new android.app.IApplicationThread.Stub.Proxy(obj);
 }

 @Override
 public android.os.IBinder asBinder() {
 return this;
 }

 public static java.lang.String getDefaultTransactionName(int transactionCode) {
 switch (transactionCode) {
 case TRANSACTION_scheduleReceiver:{
 return "scheduleReceiver";
 }
 case TRANSACTION_scheduleCreateService: {
 return "scheduleCreateService";
 }
 default: {
 return null;
 }
 }
 }

 public java.lang.String getTransactionName(int transactionCode) {
 return this.getDefaultTransactionName(transactionCode);
 }

 @Override
 public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags)
 throws android.os.RemoteException {
 java.lang.String descriptor = DESCRIPTOR;
 if (code >= android.os.IBinder.FIRST_CALL_TRANSACTION
 && code <= android.os.IBinder.LAST_CALL_TRANSACTION) {
 data.enforceInterface(descriptor);
 }
 switch (code) {
 case INTERFACE_TRANSACTION:{
 reply.writeString(descriptor);
 return true;
 }
 }

 switch (code) {
 case TRANSACTION_scheduleReceiver:{
 android.content.Intent _arg0;
 _arg0 = data.readTypedObject(android.content.Intent.CREATOR);
 ...
 int _arg8;
 _arg8 = data.readInt();
 data.enforceNoDataAvail();
 this.scheduleReceiver(_arg0, _arg1, _arg2, _arg3, _arg4, _arg5, _arg6, _arg7, _arg8);
 break;
 }
 ...
 default:{
 return super.onTransact(code, data, reply, flags);
 }
 }
 return true;
 }

 private static class Proxy implements android.app.IApplicationThread {
 private android.os.IBinder mRemote;

 Proxy(android.os.IBinder remote) {
 mRemote = remote;
 }

 @Override
 public android.os.IBinder asBinder() {
 return mRemote;
 }

 public java.lang.String getInterfaceDescriptor() {
 return DESCRIPTOR;
 }

 @Override
 public void scheduleReceiver(
 android.content.Intent intent,
 ...
 int processState) throws android.os.RemoteException {
 android.os.Parcel _data = android.os.Parcel.obtain();
 try {
 _data.writeInterfaceToken(DESCRIPTOR);
 ...
 _data.writeTypedObject(intent, 0);
 boolean _status = mRemote.transact(
 Stub.TRANSACTION_scheduleReceiver, _data, null, android.os.IBinder.FLAG_ONEWAY);
 } finally {
 _data.recycle();
 }
 }
 ...
 }

 public static final java.lang.String DESCRIPTOR = "android.app.IApplicationThread";
 static final int TRANSACTION_scheduleReceiver = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
 static final int TRANSACTION_scheduleCreateService = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
...
 public int getMaxTransactionId() {
 return 57;
 }
}
...
public void scheduleReceiver(
android.content.Intent intent,
...
int processState)
throws android.os.RemoteException;

}

对于Java代码,AIDL会生成一个跟AIDL同名的接口,同时继承自IInterface,同时还会创建内部类,分别为Default和Stub。Default为默认实现,大多数情况下是没有的,因为我们自己会实现。而Stub为抽象类,我们自己实现的时候会使用它,它以及继承自Binder,AIDL工具帮助我们把Parcel读写相关的代码已经生成,我们只需要去继承它,实现业务逻辑即可。而Stub中还有一个内部类Proxy,这个类用于Binder服务的客户端使用。对于IApplicationThread的实现,在ActivityThread当中。

匿名服务的传输

那么ActivityServiceManager(之后简称AMS)是怎么拿到ApplicationThread的呢,ActivityThread的attach方法则是这一切的入口:

1
2
3
4
5
6
final IActivityManager mgr = ActivityManager.getService();
try {
 mgr.attachApplication(mAppThread, startSeq);
} catch (RemoteException ex) {
 throw ex.rethrowFromSystemServer();
}

代码中,首先拿到AMS的binder客户端类,这里也就是IActivityManager$Stub$Proxy,因为它也用了AIDL,所以跟我们ApplicationThread的类是类似的,具体如何拿到的,这个之前ServiceManager分析getService的时候已经分析过了,这里不看了。我们可以看一下它的attachApplication方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@Override public void attachApplication(android.app.IApplicationThread app, long startSeq) throws android.os.RemoteException
{
 android.os.Parcel _data = android.os.Parcel.obtain();
 android.os.Parcel _reply = android.os.Parcel.obtain();
 try {
 _data.writeInterfaceToken(DESCRIPTOR);
 _data.writeStrongInterface(app);
 _data.writeLong(startSeq);
 boolean _status = mRemote.transact(Stub.TRANSACTION_attachApplication, _data, _reply, 0);
 _reply.readException();
 }
 finally {
 _reply.recycle();
 _data.recycle();
 }
}

这里也是跟我们之前addService类似,把binder写入到Parcel中去,因为App进程这里相当于是ApplicationThread它的服务端,因此这里写入的type为BINDER_TYPE_BINDER,而调用mRemote.transact则为调用BinderProxy的同名方法,我们知道最终会调用到IPCThreadStatetransact方法,从而调用binder驱动。

类似于getService的方法,AMS所在的system_server进程会收到BR_TRANSACTION命令,在其中解析数据知道调用的是TRANSACTION_attachApplication这个业务命令,进而使用readStrongInterface来获取到binder的代理对象BinderProxy。具体代码在IActivityManager.StubonTransact中,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
case TRANSACTION_attachApplication:
{
 android.app.IApplicationThread _arg0;
 _arg0 = android.app.IApplicationThread.Stub.asInterface(data.readStrongBinder());
 long _arg1;
 _arg1 = data.readLong();
 data.enforceNoDataAvail();
 this.attachApplication(_arg0, _arg1);
 reply.writeNoException();
 break;
}

到这样调用AMS服务端的attachApplication的时候就能使用IApplicationThread所提供的方法了。

Binder驱动中binder节点的处理

但是看到这里,有个问题,就是我们是匿名的binder,驱动怎么是处理的呢。这个需要去看一下binder驱动的源码,不过就不具体看调用流程了,直接看生成创建node和handle部分的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static int binder_translate_binder(struct flat_binder_object *fp,
 struct binder_transaction *t,
 struct binder_thread *thread)
{
 struct binder_node *node;
 struct binder_proc *proc = thread->proc;
 struct binder_proc *target_proc = t->to_proc;
 struct binder_ref_data rdata;
 int ret = 0;

 node = binder_get_node(proc, fp->binder);
 if (!node) {
 node = binder_new_node(proc, fp);
 if (!node)
 return -ENOMEM;
 }
 ...
 ret = binder_inc_ref_for_node(target_proc, node,
 fp->hdr.type == BINDER_TYPE_BINDER,
 &thread->todo, &rdata);
 ...
 if (fp->hdr.type == BINDER_TYPE_BINDER)
 fp->hdr.type = BINDER_TYPE_HANDLE;
 else
 fp->hdr.type = BINDER_TYPE_WEAK_HANDLE;
 fp->binder = 0;
 fp->handle = rdata.desc;
 fp->cookie = 0;

 ...
 done:
 binder_put_node(node);
 return ret;
}

可以看到对于通过Parcel调用经过binder驱动的binder对象,binder驱动都会给他们创建一个binder_node,并且为其设置handle,在传输到客户端的时候还会把type设置为BINDER_TYPE_HANDLE。 这样我们就对整给流程有所了解了,如果读者还想窥探更多的细节,则需要自行去阅读binder驱动的源码了。

开发者如何使用匿名服务

这上面介绍的部分是我们使用系统的服务来获取AIDL创建的服务,对于应用开发者来说有什么办法呢。我们可以通过AIDL+Service来实现,Android的四大组件之一的Service,它提供了通过bindService的方式来启动服务。在它的onBind中就可以返回IBinder,Android Framework会帮助我们调用操作代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void handleBindService(BindServiceData data) {
 CreateServiceData createData = mServicesData.get(data.token);
 Service s = mServices.get(data.token);
 if (s != null) {
 try {
 ....
 try {
 if (!data.rebind) {
 IBinder binder = s.onBind(data.intent);
 ActivityManager.getService().publishService(
 data.token, data.intent, binder);
 } else {
 ...
 }
 } catch (RemoteException ex) {
 throw ex.rethrowFromSystemServer();
 }
 } catch (Exception e) {
 ....
 }
 }
}

可以看到在ActivityThreadhandleBindService方法 中,我们在拿到Service所提供的IBinder之后,AMS会调用publishService,我们可以在ServiceConnection回调中拿到Binder的代理对象,之后就可以进行跨进程通讯了。

另外Android Framework还为我们提供了Messenger,其实现为Service+AIDL+Handler,让我们不用自己写AIDL,我们自己定义Service的时候使用Messenger和Handler就可以实现跨进程通信了。

总结

到此为止,我们已经分析了Binder服务管家ServiceManager的启动、使用ServiceManger添加服务和查找服务以及匿名服务的传递,在此过程中我们了解了进程是如何与Binder驱动交互的,以及binder调用过程中的会执行的方法等,我们对于Binder就有了一个全面的了解。在本文还简单介绍 了应用开发者如何使用Binder,有了这些基础,我们后面分析Android系统其他部分的代码就会更加容易了。当然关于Binder驱动的代码,BInder线程池的管理这两块还没有分析,读者感性确可以自行阅读,也可查看其他博主的文章。

如果你也对于Android系统源码感兴趣,欢迎与我交流。博文因为个人局限,也难免会出现差错,欢迎大家指正。

看完评论一下吧

Android Binder源码分析:添加服务和获取服务解析

2024年9月6日 19:22

通过ServiceManager添加服务和获取服务分别为addServicegetService,两者流程上其实是有一些类似的,其中我们可以看到binder通讯的全过程。为了让内容更有意义,添加服务选择从Java层的代码触发,获取服务则选择从Native层触发。

添加服务

我们以添加ActivityManagerService为例分析添加一个Service。首先先画个简单的流程图,介绍涉及到的类和调用的方法,不过步骤进行了省略,详细看后面的代码解析。

ServiceManagerProxy请求添加Service

其代码在ActivityManagerServicesetSystemProcess方法当中,具体调用如下:

1
2
ServiceManager.addService(Context.ACTIVITY_SERVICE, this, /* allowIsolated= */ true,
 DUMP_FLAG_PRIORITY_CRITICAL | DUMP_FLAG_PRIORITY_NORMAL | DUMP_FLAG_PROTO);

具体实现如下:

1
2
3
4
5
6
7
8
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public static void addService(String name, IBinder service, boolean allowIsolated,
 int dumpPriority) {
 try {
 getIServiceManager().addService(name, service, allowIsolated, dumpPriority);
 } catch (RemoteException e) {
 }
}

其中getIServiceManager的代码我们已经分析过了,我们之前拿到的IServiceManager的实例为ServiceManagerProxy,这里可以直接去看它的这个方法:

1
2
3
4
public void addService(String name, IBinder service, boolean allowIsolated, int dumpPriority)
 throws RemoteException {
 mServiceManager.addService(name, service, allowIsolated, dumpPriority);
}

这里调用了mServiceManager的同名方法,而这个成员变量的初始化如下:

1
mServiceManager = IServiceManager.Stub.asInterface(remote);

这里的remote就是我们之前构造函数传入的BinderProxy,而上面的函数后我们发获取到的对象则为IServiceMaanager.Stub.Proxy,我们可以看一下它的同名方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Override public void addService(java.lang.String name, android.os.IBinder service, boolean allowIsolated, int dumpPriority) throws android.os.RemoteException
{
 android.os.Parcel _data = android.os.Parcel.obtain();
 android.os.Parcel _reply = android.os.Parcel.obtain();
 try {
 _data.writeInterfaceToken(DESCRIPTOR);
 _data.writeString(name);
 _data.writeStrongBinder(service);
 _data.writeBoolean(allowIsolated);
 _data.writeInt(dumpPriority);
 boolean _status = mRemote.transact(Stub.TRANSACTION_addService, _data, _reply, 0);
 _reply.readException();
 }
 finally {
 _reply.recycle();
 _data.recycle();
 }
}

这里我们看到就是把设置的数据和binder写入到Parcel之后 调用transact。这里我们可以看一下Parcel首先是写入了InterfaceToken,也就是IServiceManager的描述符,其次才是其他内容。我们主要关注一下如何写入Binder的。其最终调用的方法在android_os_Parcel.cpp中,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
static void android_os_Parcel_writeStrongBinder(JNIEnv* env, jclass clazz, jlong nativePtr, jobject object)
{
 Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
 if (parcel != NULL) {
 const status_t err = parcel->writeStrongBinder(ibinderForJavaObject(env, object));
 if (err != NO_ERROR) {
 signalExceptionForError(env, clazz, err);
 }
 }
}

我们主要关注第5行,这里有一个ibinderForJavaObject,用于从javaobject中拿到binder的native对象,我们可以看一下其源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
sp<IBinder> ibinderForJavaObject(JNIEnv* env, jobject obj) {
 if (obj == NULL) return NULL;

 // 如果是Binder实例
 if (env->IsInstanceOf(obj, gBinderOffsets.mClass)) {
 JavaBBinderHolder* jbh = (JavaBBinderHolder*)
 env->GetLongField(obj, gBinderOffsets.mObject);
 return jbh->get(env, obj);
 }
 // 如果是BinderProxy实例
 if (env->IsInstanceOf(obj, gBinderProxyOffsets.mClass)) {
 return getBPNativeData(env, obj)->mObject;
 }
 return NULL;
}

BinderProxyNativeData* getBPNativeData(JNIEnv* env, jobject obj) {
 return (BinderProxyNativeData *) env->GetLongField(obj, gBinderProxyOffsets.mNativeData);
}

这个地方会判断我们的传过来的javaobject是Binder的实例还是BinderProxy的实例,前者对应Binder的服务端,后者对应的是客户端,我们刚刚传过来的AMS则是服务端。这里是从javaobject拿到mObject成员变量,对应native的类JavaBBinderHolder,最后调用它的get方法拿到JavaBinder对象。此处算是完成了我们Java层的Binder在Native层的对应对象的获取。现在就可以看看ParcelwriteStrongBinder方法了:

1
2
3
status_t Parcel::writeStrongBinder(const sp<IBinder>& val) {
 return flattenBinder(val);
}

其中又调用了flattenBinder,这个方法比较长,我们先一点点的贴代码:

1
2
3
4
5
6
7
BBinder* local = nullptr;
if (binder) local = binder->localBinder();
if (local) local->setParceled();

if (isForRpc()) {
...
}

这里binder是我们刚刚的拿到的JavaBBinder,它的localBinder()实现如下:

1
2
3
BBinder* BBinder::localBinder() {
 return this;
}

也就是说返回了自己。另外这里我们不是RPC,所以其中的代码我们不需要关注,继续看后面的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
flat_binder_object obj;
if (binder != nullptr) {
 if (!local) {
 ...//如果我们传入的不是BBinder,而是BpBinder执行这里的逻辑,省略
 } else {
 int policy = local->getMinSchedulerPolicy();
 int priority = local->getMinSchedulerPriority();

 if (policy != 0 || priority != 0) {
 // override value, since it is set explicitly
 schedBits = schedPolicyMask(policy, priority);
 }
 obj.flags = FLAT_BINDER_FLAG_ACCEPTS_FDS;
 ....
 obj.hdr.type = BINDER_TYPE_BINDER;
 obj.binder = reinterpret_cast<uintptr_t>(local->getWeakRefs());
 obj.cookie = reinterpret_cast<uintptr_t>(local);
 }
} else {
 ...
}
obj.flags |= schedBits;

上面的代码主要是将binder的一些参数拍平放到flat_binder_object当中。其中binder是放置到cookie字段,binder的弱引用放到了binder字段。

1
2
3
4
status_t status = writeObject(obj, false);
if (status != OK) return status;

return finishFlattenBinder(binder);

这里才开始真正的把数据写入 ,可以先看看这个writeObject方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
status_t Parcel::writeObject(const flat_binder_object& val, bool nullMetaData)
{
 const bool enoughData = (mDataPos+sizeof(val)) <= mDataCapacity;
 const bool enoughObjects = mObjectsSize < mObjectsCapacity;
 if (enoughData && enoughObjects) {
restart_write:
 *reinterpret_cast<flat_binder_object*>(mData+mDataPos) = val; //把数据写如内存
 ...
 // Need to write meta-data?
 if (nullMetaData || val.binder != 0) {
 mObjects[mObjectsSize] = mDataPos;
 acquire_object(ProcessState::self(), val, this);
 mObjectsSize++;
 }
 return finishWrite(sizeof(flat_binder_object)); //调整dataPos和当前DateSize
 }
 ...
 goto restart_write;
}

上面主要就是把binder写入内存当中,其他的则是处理内存不足的情况,有申请内存的代码,这里我们无须关注。可以在看一下前面的finishFlattenBinder方法:

1
2
3
4
5
status_t Parcel::finishFlattenBinder(const sp<IBinder>& binder) {
 internal::Stability::tryMarkCompilationUnit(binder.get());
 int16_t rep = internal::Stability::getRepr(binder.get());
 return writeInt32(rep);
}

这个方法主要为binder设置Repr,并且把值也写入到Parcel当中去,默认值为Level::SYSTEM,我们不再深入看其代码。

到这里大概就看完了Parcel写数据的代码了。可以看看transact方法,这里的mRemoteBinderProxy,它的transact方法中主要调用了一下代码:

1
2
3
4
5
final boolean result = transactNative(code, data, reply, flags);
if (reply != null && !warnOnBlocking) {
 reply.addFlags(Parcel.FLAG_IS_REPLY_FROM_BLOCKING_ALLOWED_OBJECT);
}
return result;

它的native实现在android_util_Binder.cpp中,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
static jboolean android_os_BinderProxy_transact(JNIEnv* env, jobject obj,
 jint code, jobject dataObj, jobject replyObj, jint flags)
{
 Parcel* data = parcelForJavaObject(env, dataObj); //从java层的Parcel对象获取native层的Parcel对象
 ...
 Parcel* reply = parcelForJavaObject(env, replyObj);
 ...
 IBinder* target = getBPNativeData(env, obj)->mObject.get(); //获取native层的BinderProxy对象
 if (target == NULL) {
 ...
 return JNI_FALSE;
 }

 status_t err = target->transact(code, *data, reply, flags);
 ...
 return JNI_FALSE;
}

上面主要就 是获取native层的Parcel对象和Binder对象,并且调用binder的transact方法。这里的Binder对象是什么呢,回顾之前分析的javaObjectForIBinder方法,可知此处拿到的应该是BpBinder对象。我们就可以看它的代码了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
status_t BpBinder::transact(
 uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags)
{
 // 只有binder或者的时候才能执行
 if (mAlive) {
 bool privateVendor = flags & FLAG_PRIVATE_VENDOR;
 // 用户层的flag移除
 flags = flags & ~FLAG_PRIVATE_VENDOR;

 if (code >= FIRST_CALL_TRANSACTION && code <= LAST_CALL_TRANSACTION) {
 ... //Stability 相等判断,此处略过
 }

 status_t status;
 if (CC_UNLIKELY(isRpcBinder())) {
 ...
 } else {
 status = IPCThreadState::self()->transact(binderHandle(), code, data, reply, flags);
 }
 ....
 if (status == DEAD_OBJECT) mAlive = 0;

 return status;
 }

 return DEAD_OBJECT;
}

因为ServiceManager的id为0,此处binderHandle()拿到的值应为0。此处主要也是调用了IPCThreadStatetransact方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
status_t IPCThreadState::transact(int32_t handle,
 uint32_t code, const Parcel& data,
 Parcel* reply, uint32_t flags)
{
 status_t err;

 flags |= TF_ACCEPT_FDS;
 err = writeTransactionData(BC_TRANSACTION, flags, handle, code, data, nullptr);

 if (err != NO_ERROR) {
 if (reply) reply->setError(err);
 return (mLastError = err);
 }

 if ((flags & TF_ONE_WAY) == 0) {
 if (reply) {
 err = waitForResponse(reply);
 } else {
 Parcel fakeReply;
 err = waitForResponse(&fakeReply);
 }
 } else {
 err = waitForResponse(nullptr, nullptr);
 }

 return err;
}

这里主要调用了两个方法,分别是writeTransactionDatawaitForResponse,我们分别看一下。首先是writeTransactionData,它的第一个参数为BC_TRANSACTION,这是用于与Binder驱动交互的命令,除了这个之外还有其他一些,可以在binder.h当中找到。现在可以看writeTransactionData的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
status_t IPCThreadState::writeTransactionData(int32_t cmd, uint32_t binderFlags,
 int32_t handle, uint32_t code, const Parcel& data, status_t* statusBuffer)
{
 binder_transaction_data tr;

 tr.target.ptr = 0; /* Don't pass uninitialized stack data to a remote process */
 tr.target.handle = handle;
 tr.code = code;
 tr.flags = binderFlags;
 tr.cookie = 0;
 tr.sender_pid = 0;
 tr.sender_euid = 0;

 const status_t err = data.errorCheck();
 if (err == NO_ERROR) {
 tr.data_size = data.ipcDataSize();
 tr.data.ptr.buffer = data.ipcData();
 tr.offsets_size = data.ipcObjectsCount()*sizeof(binder_size_t);
 tr.data.ptr.offsets = data.ipcObjects();
 } else if (statusBuffer) {
 ...
 } else {
 return (mLastError = err);
 }

 mOut.writeInt32(cmd);
 mOut.write(&tr, sizeof(tr));

 return NO_ERROR;
}

这个方法中所做的事情为,把我们传入的data和code以及要调用的binder的id等都放到binder_transaction_data中去,同时又把这个tr和调用binder驱动的命令BC_TRANSACTION一起写入到mOut当中去,这个mOut也是一个Parcel对象。到这里,数据都写完了,但是binder驱动在那里读取处理这个数据呢,我们继续看waitForResponse,前面我们因为有传reply过来,因此会调用到第17行的waitForResponse方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
status_t IPCThreadState::waitForResponse(Parcel *reply, status_t *acquireResult)
{
 uint32_t cmd;
 int32_t err;

 while (1) {
 if ((err=talkWithDriver()) < NO_ERROR) break;
 err = mIn.errorCheck();
 if (err < NO_ERROR) break;
 if (mIn.dataAvail() == 0) continue;

 cmd = (uint32_t)mIn.readInt32();

 switch (cmd) {
 case BR_ONEWAY_SPAM_SUSPECT:
 [[fallthrough]];
 ......
 case BR_ACQUIRE_RESULT:
 {
 const int32_t result = mIn.readInt32();
 if (!acquireResult) continue;
 *acquireResult = result ? NO_ERROR : INVALID_OPERATION;
 }
 goto finish;

 case BR_REPLY:
 {
 binder_transaction_data tr;
 err = mIn.read(&tr, sizeof(tr));
 if (err != NO_ERROR) goto finish;

 if (reply) {
 if ((tr.flags & TF_STATUS_CODE) == 0) {
 reply->ipcSetDataReference(
 reinterpret_cast<const uint8_t*>(tr.data.ptr.buffer),
 tr.data_size,
 reinterpret_cast<const binder_size_t*>(tr.data.ptr.offsets),
 tr.offsets_size/sizeof(binder_size_t),
 freeBuffer);
 } else {
 ....
 } else {
 ...
 continue;
 }
 }
 goto finish;

 default:
 err = executeCommand(cmd);
 if (err != NO_ERROR) goto finish;
 break;
 }
 }

finish:
 if (err != NO_ERROR) {
 if (acquireResult) *acquireResult = err;
 if (reply) reply->setError(err);
 mLastError = err;
 }

 return err;
}

talkWithDriver分析

这里开启了一个while的无限循环,首先调用talkWithDriver,看名字就知道是与Binder驱动进行交互,这里首先会看看这个方法有没有报错,没有报错又会检查mIn是否有报错。我们前面看到过mOut,这里又有mIn,它们是用来做什么的呢,我们看一下talkWithDriver,可以发现一些东西:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
status_t IPCThreadState::talkWithDriver(bool doReceive)
{
 if (mProcess->mDriverFD < 0) {
 return -EBADF;
 }

 binder_write_read bwr;

 const bool needRead = mIn.dataPosition() >= mIn.dataSize();
 const size_t outAvail = (!doReceive || needRead) ? mOut.dataSize() : 0;

 bwr.write_size = outAvail;
 bwr.write_buffer = (uintptr_t)mOut.data();

 // This is what we'll read.
 if (doReceive && needRead) {
 bwr.read_size = mIn.dataCapacity();
 bwr.read_buffer = (uintptr_t)mIn.data();
 } else {
 bwr.read_size = 0;
 bwr.read_buffer = 0;
 }

 // 无数据需要读写,直接返回
 if ((bwr.write_size == 0) && (bwr.read_size == 0)) return NO_ERROR;

 bwr.write_consumed = 0;
 bwr.read_consumed = 0;
 status_t err;
 do {

#if defined(__ANDROID__)
 if (ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr) >= 0)
 err = NO_ERROR;
 else
 err = -errno;
#else
 err = INVALID_OPERATION;
#endif
 if (mProcess->mDriverFD < 0) {
 err = -EBADF;
 }
 } while (err == -EINTR);


 if (err >= NO_ERROR) {
 if (bwr.write_consumed > 0) {
 if (bwr.write_consumed < mOut.dataSize())
 ...
 else {
 mOut.setDataSize(0);
 processPostWriteDerefs();
 }
 }
 if (bwr.read_consumed > 0) {
 mIn.setDataSize(bwr.read_consumed);
 mIn.setDataPosition(0);
 }
 ...
 return NO_ERROR;
 }

 return err;
}

这个方法传参的默认值为true,也就是需要接受结果。在这里我们看到有一个新的数据结构binder_write_read,此处会把mOut中的数据指针写入到它的write_buffer当中,同时把mIn的数据指针写入到read_buffer中,此处的写指的是向binder驱动中写。随后我们看到是在一个循环当中调用系统调用ioctl来与binder驱动进行交互,这里使用循环的原因是,当我们调用这个系统调用的时候可能会遇到遇到中断,我们之前的调用未能执行,因此需要一直等待到执行为止。

到这里我们就分析完了添加Service调用端的所有代码,此时我们需要看一下ServiceManager服务端与Binder进行交互的代码。

ServiceManager服务端处理添加Service

我们之前分析ServiceManager启动的时候,知道最后会注册Looper的监听,当Binder驱动有消息的时候,BinderCallbak的handleEvent就会执行去处理,那么当我们在客户端请求添加Binder服务的时候,这里也会执行。这个方法中执行了如下代码:

1
IPCThreadState::self()->handlePolledCommands();

这里我们可以看一下详细的源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
status_t IPCThreadState::handlePolledCommands()
{
 status_t result;
 do {
 //读取缓存数据知道处理完成
 result = getAndExecuteCommand();
 } while (mIn.dataPosition() < mIn.dataSize());
 //减少binder的引用数量,此处也会和驱动交互
 processPendingDerefs();
 //若有为执行的命令,全部执行
 flushCommands();
 return result;
}

此处我们主要关注getAndExecuteCommand方法,后面都已经加了注释,此处不需要详细关注。getAndExecuteCommand方法当中也是首先调用talkWithDriver方法,这个方法前面分析过了,不再分析,这样执行完之后,mIn当中就会拿到客户端请求传输过来的数据了,之后就从数据中拿取命令和数据进行执行,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
size_t IN = mIn.dataAvail();
if (IN < sizeof(int32_t)) return result;
cmd = mIn.readInt32(); //读取命令
pthread_mutex_lock(&mProcess->mThreadCountLock); //为了增加线程计数上锁
mProcess->mExecutingThreadsCount++;
if (mProcess->mExecutingThreadsCount >= mProcess->mMaxThreads &&
 mProcess->mStarvationStartTimeMs == 0) {
 mProcess->mStarvationStartTimeMs = uptimeMillis();
}
pthread_mutex_unlock(&mProcess->mThreadCountLock);

result = executeCommand(cmd); //执行命令

pthread_mutex_lock(&mProcess->mThreadCountLock);
mProcess->mExecutingThreadsCount--;
if (mProcess->mWaitingForThreads > 0) {
 pthread_cond_broadcast(&mProcess->mThreadCountDecrement);
}
pthread_mutex_unlock(&mProcess->mThreadCountLock);

代码很多,但是大多都是为了给binder线程计数增减的。我们主要去看一下executeCommand中的代码,该方法中代码很多,而我们在客户端执行的是BC_TRANSACTION,因此这里应该收到的是BR_TRANSACTION命令,因此只需要看该分支的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
BBinder* obj;
RefBase::weakref_type* refs;
status_t result = NO_ERROR;

switch ((uint32_t)cmd) {
 ...
 case BR_TRANSACTION:
 binder_transaction_data_secctx tr_secctx;
 binder_transaction_data& tr = tr_secctx.transaction_data;
 result = mIn.read(&tr, sizeof(tr)); //读取binder携带过来的数据到tr中

 Parcel buffer;
 //将数据的引用放入Parcel当中
 buffer.ipcSetDataReference(
 reinterpret_cast<const uint8_t*>(tr.data.ptr.buffer),
 tr.data_size,
 reinterpret_cast<const binder_size_t*>(tr.data.ptr.offsets),
 tr.offsets_size/sizeof(binder_size_t), freeBuffer);
 //设置调用这的uid,pid,flag等信息
 mCallingPid = tr.sender_pid;
 mCallingSid = reinterpret_cast<const char*>(tr_secctx.secctx);
 mCallingUid = tr.sender_euid;
 mLastTransactionBinderFlags = tr.flags;
 if (tr.target.ptr) { //ServiceManager的binder无ptr
 //非serviceManager的binder,tr.cookie为本地的BBinder对象指针
 if (reinterpret_cast<RefBase::weakref_type*>(
 tr.target.ptr)->attemptIncStrong(this)) {
 error = reinterpret_cast<BBinder*>(tr.cookie)->transact(tr.code, buffer,
 &reply, tr.flags);
 reinterpret_cast<BBinder*>(tr.cookie)->decStrong(this);
 } else {
 error = UNKNOWN_TRANSACTION;
 }

 } else {
 //ServiceManager使用the_context_object这个BBinder对象。
 error = the_context_object->transact(tr.code, buffer, &reply, tr.flags);
 }
 if ((tr.flags & TF_ONE_WAY) == 0) {
 if (error < NO_ERROR) reply.setError(error);

 constexpr uint32_t kForwardReplyFlags = TF_CLEAR_BUF;
 sendReply(reply, (tr.flags & kForwardReplyFlags)); //写入回复
 } else {
 ...
 }
 ...
}

return result;

上面的代码已经做了省略,逻辑就是首先从mIn这块内存中拿到数据,并且放Parcel中,随后把uid,pid相关的属性设置到当前进程。之后是获取BBinder对象去执行transact方法,对于普通的binder,对于普通的binder,会ptr这个字段,并且tr.cookie就是本地的BBinder对象指针,而对于ServiceManager,这里就会使用在启动ServiceManager时候调用setTheContextObject所设置的BBinder对象,也就是服务端的ServiceManager。这里transact执行完成之后会调用sendReply将执行结果通过binder驱动传递回binder调用端,从而完成整个流程。这里先看transact,分析完再来分析sendReply

transact方法在BBinder类当中,在其中会调用onTransact方法,而到ServiceManager,它的onTransact的实现在BnServiceManager当中,这个类则是通过AIDL工具生成的。因为没有源码,根据经验我们这边可以知道它会调用ServiceManageraddService方法,而其中最重要的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
mNameToService[name] = Service {
 .binder = binder,
 .allowIsolated = allowIsolated,
 .dumpPriority = dumpPriority,
 .debugPid = ctx.debugPid,
};

auto it = mNameToRegistrationCallback.find(name);
if (it != mNameToRegistrationCallback.end()) {
 for (const sp<IServiceCallback>& cb : it->second) {
 mNameToService[name].guaranteeClient = true;
 // permission checked in registerForNotifications
 cb->onRegistration(name, binder);
 }
}

看代码可知道,这里把Binder放到Service结构体当中,随后放入mNameToService当中,mNameToService是一个map。而mNameToRegistrationCallback中为服务注册的回调,当注册完成之后会调用它的onRegistration方法。

前面我们还有一个sendReply方法我们还未分析,这里再看一下:

1
2
3
4
5
6
7
8
9
status_t IPCThreadState::sendReply(const Parcel& reply, uint32_t flags)
{
 status_t err;
 status_t statusBuffer;
 err = writeTransactionData(BC_REPLY, flags, -1, 0, reply, &statusBuffer);
 if (err < NO_ERROR) return err;

 return waitForResponse(nullptr, nullptr);
}

writeTransactionData当中就是把我们的reply打包成为binder_transaction_data写入mOut当中,这里的命令为BC_REPLY,执行完之后调用waitForResponse,其中会调用talkWithDriver来回应,之后便结束了服务端的相应。客户端随后可以读取客户端的mIn数据可以获取reply的数据。到这里就分析完了Service注册的流程。

获取服务(getService)分析

之前分析getIServiceManageraddService我们都是从java层的代码出发去往后走分析代码,而getService其实有一些地方跟他们是类似的,为了减少重复流程的分析,这里从Native层的使用场景出发。这里以获取ICameraService为例。

获取defaultServiceManager

我们的起点在frameworks/av/camera/ndk/impl/ACameraManager.cpp当中,调用代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const char* kCameraServiceName = "media.camera";
....

sp<IServiceManager> sm = defaultServiceManager();
sp<IBinder> binder;
do {
 binder = sm->getService(String16(kCameraServiceName));
 if (binder != nullptr) {
 break;
 }
 usleep(kCameraServicePollDelay);
} while(true);

这里使用defaultServiceManager来拿到ServiceManager,其源码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
//frameworks/native/libs/binder/IServiceManager.cpp
sp<IServiceManager> defaultServiceManager()
{
 std::call_once(gSmOnce, []() {
 sp<AidlServiceManager> sm = nullptr;
 while (sm == nullptr) {
 sm = interface_cast<AidlServiceManager>(ProcessState::self()->getContextObject(nullptr));
 if (sm == nullptr) {
 sleep(1);
 }
 }

 gDefaultServiceManager = sp<ServiceManagerShim>::make(sm);
 });

 return gDefaultServiceManager;
}

这个代码跟我们之前java层的代码比较类似,也是先拿ContentObject,而ServiceManagerShim相当于是native层的ServiceManager的代理。native层的代码因为不需要把对象转成Java的消耗,代码其实更加简单一点。这里我们拿到了ServiceManagerShim,就可以继续去看它的getService方法了。

请求getService

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
sp<IBinder> ServiceManagerShim::getService(const String16& name) const
{
 static bool gSystemBootCompleted = false;

 sp<IBinder> svc = checkService(name);
 if (svc != nullptr) return svc;

 const bool isVendorService =
 strcmp(ProcessState::self()->getDriverName().c_str(), "/dev/vndbinder") == 0;
 constexpr int64_t timeout = 5000;
 int64_t startTime = uptimeMillis();
 // 如果是Vendor的服务,不能够访问系统的属性
 if (!gSystemBootCompleted && !isVendorService) {
#ifdef __ANDROID__
 char bootCompleted[PROPERTY_VALUE_MAX];
 property_get("sys.boot_completed", bootCompleted, "0");
 gSystemBootCompleted = strcmp(bootCompleted, "1") == 0 ? true : false;
#else
 gSystemBootCompleted = true;
#endif
 }
 // 如果拿不到binder service就等待,系统服务和vendor时间有区分,直到超时才停止
 const useconds_t sleepTime = gSystemBootCompleted ? 1000 : 100;
 int n = 0;
 while (uptimeMillis() - startTime < timeout) {
 n++;
 usleep(1000*sleepTime);

 sp<IBinder> svc = checkService(name);
 if (svc != nullptr) {
 return svc;
 }
 }
 return nullptr;
}

这里就是调用checkService去获取Service,源码如下:

1
2
3
4
5
6
7
8
sp<IBinder> ServiceManagerShim::checkService(const String16& name) const
{
 sp<IBinder> ret;
 if (!mTheRealServiceManager->checkService(String8(name).c_str(), &ret).isOk()) {
 return nullptr;
 }
 return ret;
}

这里我们调用了mTheRealServiceManagercheckService方法,这个变量的实例为ServiceManager的BpBinder子类,也是由AIDL生成,其代码如下:

1
2
3
4
Parcel data, reply;
data.writeInterfaceToken(IServiceManager::getInterfaceDescriptor()); data.writeString16(name);
remote()->transact(CHECK_SERVICE_TRANSACTION, data, &reply);
return reply.readStrongBinder();

这里跟之前分析addService的部分类似,只有最后多了一个readStrongBinder,addServicewriteStrongBinder到data,这里是读取binder调用返回的数据。后续流程也跟addService类似,这里就不再分析了。我们更关注这个binder我们是怎么拿到的,因此需要看三个地方的代码,一个是ServiceManger拿到binder并且写入到驱动给我们的过程,第二个地方是IPCThreadState当中接收数据的处理,最后就是通过readStrongBinder拿到binder的处理了。

客户端请求获取Binder服务的流程大概如下图所示:

sequenceDiagram
ServiceManagerShim->>ServiceManagerShim: defaultServiceManager()
ServiceManagerShim->>ServiceManagerShim:getService()
ServiceManagerShim->>ServiceManagerShim:checkService()
ServiceManagerShim->>+BpServiceManager:checkService()
BpServiceManager->>+BpBinder: transact()
BpBinder->>+IPCThreadState: transact()
IPCThreadState->>IPCThreadState: writeTransactionData()
IPCThreadState->>IPCThreadState: waitForResponse()
IPCThreadState->>IPCThreadState: talkWithDriver()
IPCThreadState->>+BinderDriver: ioctl(BC_TRANSACTION)
BinderDriver-->>-IPCThreadState: reply:BR_REPLY
IPCThreadState-->>-BpBinder: return resut
BpBinder-->>-BpServiceManager: return result
BpServiceManager->>BpServiceManager: readStrongBinder()
BpServiceManager-->>-ServiceManagerShim: return binder

ServiceManager服务端getService

前面分析addService我们已经知道服务端调用路径是BBinder.transcat–>BnServiceManager.onTransact–>ServiceManger.addService,这里的服务端也是类似,具体可以看下面的流程图。

sequenceDiagram
BinderDriver-->IPCThreadState:handlePolledCommands
loop mIn.dataPosition < mIn.dataSize (当输入数据未处理完)
IPCThreadState->>+IPCThreadState: getAndExecuteCommand
IPCThreadState->>IPCThreadState: executeCommand: BR_TRANSACTION
IPCThreadState->>BBinder: transact()
BBinder->>+BnServiceManager: onTransact()
BnServiceManager->>+ServiceManager: getService()
ServiceManager-->>-BnServiceManager: return Binder
BnServiceManager->>BnServiceManager: writeStrongBinder()
BnServiceManager-->-BBinder: return reply Parcel
BBinder-->>IPCThreadState: return reply
IPCThreadState->>+IPCThreadState: sendReply
IPCThreadState->>IPCThreadState:writeTransactionData
IPCThreadState->>+IPCThreadState:waitForResponse(null, null)
IPCThreadState->>IPCThreadState: talkWithDriver
IPCThreadState->>BinderDriver: ioctl:BC_REPLY
IPCThreadState-->>-IPCThreadState::
IPCThreadState-->-IPCThreadState: finishSendReply
end

我们就省略与Binder交互的许多代码,可以直接去看getService的代码了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Status ServiceManager::getService(const std::string& name, sp<IBinder>* outBinder) {
 *outBinder = tryGetService(name, true);
 return Status::ok();
}

sp<IBinder> ServiceManager::tryGetService(const std::string& name, bool startIfNotFound) {
 auto ctx = mAccess->getCallingContext();

 sp<IBinder> out;
 Service* service = nullptr;
 if (auto it = mNameToService.find(name); it != mNameToService.end()) {
 service = &(it->second);

 if (!service->allowIsolated) { //是否允许多用户环境运行
 uid_t appid = multiuser_get_app_id(ctx.uid);
 bool isIsolated = appid >= AID_ISOLATED_START && appid <= AID_ISOLATED_END;

 if (isIsolated) {
 return nullptr;
 }
 }
 out = service->binder;
 }

 if (!mAccess->canFind(ctx, name)) { //SELinux 权限检查
 return nullptr;
 }

 if (!out && startIfNotFound) {
 tryStartService(name);
 }

 return out;
}

ServiceManger中获取Binder就是从我们之前添加Service的时候的那个ServiceMap中查找,当查找后做一些权限检查,当找不到的情况下,因为我们传如的startIfNotFound,因此会调用tryStartService去启动对应的Service,其代码如下:

1
2
3
4
5
6
void ServiceManager::tryStartService(const std::string& name) {
 std::thread([=] {
 if (!base::SetProperty("ctl.interface_start", "aidl/" + name)) {
 ...
 }).detach();
}

代码很简单,就是启动了一个线程,其中设置系统的properties,系统便会尝试启动这个服务,具体我们这里就不分析了。

IPCThreadState接收数据处理

在服务端发送数据时候会调用binder执行BC_REPLY,而客户端后收到BR_REPLY命令,也就是会执行waitForResponse中的如下部分:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
status_t IPCThreadState::waitForResponse(Parcel *reply, status_t *acquireResult)
{
....
case BR_REPLY:
 {
 binder_transaction_data tr;
 err = mIn.read(&tr, sizeof(tr));
 ALOG_ASSERT(err == NO_ERROR, "Not enough command data for brREPLY");
 if (err != NO_ERROR) goto finish;

 if (reply) {
 if ((tr.flags & TF_STATUS_CODE) == 0) {
 reply->ipcSetDataReference(
 reinterpret_cast<const uint8_t*>(tr.data.ptr.buffer),
 tr.data_size,
 reinterpret_cast<const binder_size_t*>(tr.data.ptr.offsets),
 tr.offsets_size/sizeof(binder_size_t),
 freeBuffer);
 } else {
 ...
 }
 } else {
 ...
 continue;
 }
 }
 goto finish;
.....
}

也就是执行上面的ipcSetDataReference,可以看一下其源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void Parcel::ipcSetDataReference(const uint8_t* data, size_t dataSize,
 const binder_size_t* objects, size_t objectsCount, release_func relFunc)
{

 freeData(); //初始化Parcel状态

 mData = const_cast<uint8_t*>(data);
 mDataSize = mDataCapacity = dataSize;
 mObjects = const_cast<binder_size_t*>(objects);
 mObjectsSize = mObjectsCapacity = objectsCount;
 mOwner = relFunc;

 binder_size_t minOffset = 0;
 for (size_t i = 0; i < mObjectsSize; i++) {
 binder_size_t offset = mObjects[i];
 if (offset < minOffset) {

 mObjectsSize = 0;
 break;
 }
 const flat_binder_object* flat
 = reinterpret_cast<const flat_binder_object*>(mData + offset);
 uint32_t type = flat->hdr.type;
 if (!(type == BINDER_TYPE_BINDER || type == BINDER_TYPE_HANDLE ||
 type == BINDER_TYPE_FD)) {
 ....
 break;
 }
 minOffset = offset + sizeof(flat_binder_object);
 }
 scanForFds();
}

代码比较简单,主要就是把data传入Parcel中,但是除此之外我们需要关注一下传入的relFunc,传入的方法为freeBuffer,此方法的执行会在下一次调用freeData的时候执行,它的实现如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void IPCThreadState::freeBuffer(Parcel* parcel, const uint8_t* data,
 size_t /*dataSize*/,
 const binder_size_t* /*objects*/,
 size_t /*objectsSize*/)
{
 ALOG_ASSERT(data != NULL, "Called with NULL data");
 if (parcel != nullptr) parcel->closeFileDescriptors();
 IPCThreadState* state = self();
 state->mOut.writeInt32(BC_FREE_BUFFER);
 state->mOut.writePointer((uintptr_t)data);
 state->flushIfNeeded();
}

看到这里,我们知道会调用binder发送这个BC_FREE_BUFFER命令,这样驱动内部会清理内存,这样就完成了Parcel和内存缓冲区的空间清理。

readStrongBinder

readStrongBinder和我们之前看过的writeStrongBinder应该是一个相反的过程,直接看代码:

1
2
3
4
5
status_t Parcel::readStrongBinder(sp<IBinder>* val) const
{
 status_t status = readNullableStrongBinder(val);
 return status;
}

上面的代码会调用readNullableStrongBinder,而其内部又会调用unflattenBinder,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
status_t Parcel::unflattenBinder(sp<IBinder>* out) const
{
 if (isForRpc()) {
 ...
 return finishUnflattenBinder(binder, out);
 }

 const flat_binder_object* flat = readObject(false);

 if (flat) {
 switch (flat->hdr.type) {
 case BINDER_TYPE_BINDER: {
 sp<IBinder> binder =
 sp<IBinder>::fromExisting(reinterpret_cast<IBinder*>(flat->cookie));
 return finishUnflattenBinder(binder, out);
 }
 case BINDER_TYPE_HANDLE: {
 sp<IBinder> binder =
 ProcessState::self()->getStrongProxyForHandle(flat->handle);
 return finishUnflattenBinder(binder, out);
 }
 }
 }
 return BAD_TYPE;
}

其中readObject为从Parcel中读取flat_binder_object对象,当请求的进程和服务在同一个进程时候,这里的type就是BINDER_TYPE_BINDER,当请求的进程和服务不在同一个进程则为BINDER_TYPE_HANDLE,因此我们这里是BINDER_TYPE_HANDLEgetStrongProxyForHandle我们之前在分析获取ServiceManager的时候已经分析过了,只不过那个地方handle为固定的0,而这里则是从驱动中传过来的值,最后我们会拿到一个BpBinder,也就完成了查找的过程。

分析完添加服务,查找服务,一直之前介绍的启动ServiceManager和获取ServiceManager基本上就把Binder除了驱动部分的东西都覆盖了。还剩下应用层应该如何使用Binder以及,我们的匿名binder是怎么查找的,这个留待下次在写。

如果你也对于Android系统源码感兴趣,欢迎与我交流。

看完评论一下吧

自定义 Mac 文件夹图标

2024年9月5日 22:55

不知道是否有网友和我一样,看多了macOS 系统蓝色文件夹图标,难免有些审美疲劳,并且文件夹一多,找起来也会头晕眼花。这篇文章将简单分享一下如何自定义文件夹图标,将其修改为类似于“下载”、“文稿”、“资源库”等系统文件夹那样带图案的文件夹。

自定义 Mac 文件夹图标最先出现在Jack‘s Space

Android源码分析:ServiceManager启动代码解析

2024年9月5日 20:41

之前已经分析过获取ServiceManager了,不过那是在使用端,在分析使用ServiceManager去获取服务或者添加服务的时候发现,我使用的Android Studio for Platform默认没有把ServiceManager的源码导入。并且同时我们不知道ServiceManager的服务端是怎么启动,怎么响应的,因此决定还是需要分析一下这块的代码。

首先简单画了一下启动的流程,如下图所示(Entry表示我们的调用入口,也就是后面所说的main函数):

sequenceDiagram
autonumber
Entry->>+ProcessState: initWithDriver
ProcessState->>ProcessState: init
ProcessState-->>-Entry: return ProcessState
Entry->>ServiceManager: new
Entry->>ServiceManager: addService(this)
Entry ->>IPCThreadState: setTheContentObject
Entry->>ProcessState: becomeContextManager
Entry->>Looper:prepare
Entry ->>Looper:addFd
loop 永不退出
Entry ->>Looper:pollAll
end

ServiceManager的启动是在系统启动的时候进行启动的,它的启动是使用Linux系统的服务启动方式进行配置,配置文件在servicemanager.rc当中,配置如下:

service servicemanager /system/bin/servicemanager
class core animation
user system
group system readproc
critical
onrestart restart apexd
...
task_profiles ServiceCapacityLow
shutdown critical

具体系统如何调用这个Service的我们这里不必关心,我们可以直接来看ServiceManager启动相关的代码,代码在frameworks/native/cmds/servicemanager目录下面。启动的逻辑在main.cppmain方法中,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
int main(int argc, char** argv) {
 ...
 const char* driver = argc == 2 ? argv[1] : "/dev/binder";
 sp<ProcessState> ps = ProcessState::initWithDriver(driver);
 ps->setThreadPoolMaxThreadCount(0); //设置最大线程数,因为serviceManager不使用`startThreadPool`启动线程池,因此设置为0
 ps->setCallRestriction(ProcessState::CallRestriction::FATAL_IF_NOT_ONEWAY);
 sp<ServiceManager> manager = sp<ServiceManager>::make(std::make_unique<Access>());
 if (!manager->addService("manager", manager, false /*allowIsolated*/, IServiceManager::DUMP_FLAG_PRIORITY_DEFAULT).isOk()) {
 LOG(ERROR) << "Could not self register servicemanager";
 }

 IPCThreadState::self()->setTheContextObject(manager);
 ps->becomeContextManager();
 sp<Looper> looper = Looper::prepare(false /*allowNonCallbacks*/);
 BinderCallback::setupTo(looper);
 ClientCallbackCallback::setupTo(looper, manager);
 while(true) {
 looper->pollAll(-1);
 }
 // 应该不会走到,除非发生了错误
 return EXIT_FAILURE;
}

代码如上,数量不多,并且其中看到了不少熟悉的类名,首先是使用ProcessState类去调用它的initWithDriver,这里最终也会走到init方法,做的事情也是创建ProcessState实例,并且打开Binder驱动,获取驱动的FD,可以参考前面的文章。

第7行代码,我们可以看到,创建了一个ServiceManager对象,这个对象与我们之前分析的ServiceManager是不同的,它是ServiceManager的Binder服务端,这个代码也跟我们的初始化代码在同一个目录。拿到这个manager后面做的第一件事情就是调用addService把自己也加进去,addService的代码后面再来分析。

再看12行代码,setTheContextObject处传入了manager,这里IPCThreadState初始化代码之前也已经分析过,此处略过,这个方法也只是把manager作为它的成员放入,暂时略过,直接看后面的becomeContextManager源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
bool ProcessState::becomeContextManager()
{
 AutoMutex _l(mLock);
 flat_binder_object obj {
 .flags = FLAT_BINDER_FLAG_TXN_SECURITY_CTX,
 };
 //与Binder交互,发送命令BINDER_SET_CONTEXT_MGR_EXT,让当前进程成为Binder的上下文管理者
 int result = ioctl(mDriverFD, BINDER_SET_CONTEXT_MGR_EXT, &obj);

 if (result != 0) {
 android_errorWriteLog(0x534e4554, "121035042");

 int unused = 0;
 // 执行失败,则重置该参数
 result = ioctl(mDriverFD, BINDER_SET_CONTEXT_MGR, &unused);
 }
 ...
 return result == 0;
}

上面的代码就是告诉binder驱动,当前的进程要成为binder的上下文管理者,驱动内部作何处理我们便不再深究。

继续看我们前面的代码,就出现了熟悉的Looper,这个Java层的Looper用法和功能都一样,preapre之后又在死循环中调用pollAll,从而我们的代码就会永远在这里执行而不会退出。但是Looper在这里有什么用处,我们需要看看前面的代码。首先是15行的调用,其源码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
static sp<BinderCallback> setupTo(const sp<Looper>& looper) {
 sp<BinderCallback> cb = sp<BinderCallback>::make();

 int binder_fd = -1;
 IPCThreadState::self()->setupPolling(&binder_fd);

 int ret = looper->addFd(binder_fd,
 Looper::POLL_CALLBACK,
 Looper::EVENT_INPUT,
 cb,
 nullptr /*data*/);
 return cb;
}

第5行代码调用的setupPolling代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
status_t IPCThreadState::setupPolling(int* fd)
{
 if (mProcess->mDriverFD < 0) { //当前驱动没有成功打开时候,直接返回报错
 return -EBADF;
 }

 mOut.writeInt32(BC_ENTER_LOOPER); //写入Binder调用命令
 flushCommands();
 *fd = mProcess->mDriverFD; //保存Binder驱动的文件描述符到fd当中
 return 0;
}

void IPCThreadState::flushCommands()
{
 if (mProcess->mDriverFD < 0)
 return;
 talkWithDriver(false); //与驱动进行交互
 if (mOut.dataSize() > 0) { //二次确认,未成功则继续交互
 talkWithDriver(false);
 }
}

可以看到上面的代码主要是与Binder驱动交互,并且执行命令进入Binder的循环,且拿到binder驱动的文件描述符,其中talkWithDriver为与binder交互的具体流程,后续在介绍其代码。

拿到binder驱动的文件描述符后执行Looper的addFd方法,最终执行的方法代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//Looper.cpp
int Looper::addFd(int fd, int ident, int events, const sp<LooperCallback>& callback, void* data) {
 ...

 { // 此代码段上锁
 AutoMutex _l(mLock);

 if (mNextRequestSeq == WAKE_EVENT_FD_SEQ) mNextRequestSeq++;
 const SequenceNumber seq = mNextRequestSeq++;

 Request request;
 request.fd = fd;
 request.ident = ident;
 request.events = events;
 request.callback = callback;
 request.data = data;

 epoll_event eventItem = createEpollEvent(request.getEpollEvents(), seq);
 auto seq_it = mSequenceNumberByFd.find(fd);
 if (seq_it == mSequenceNumberByFd.end()) { //列表中不存在该fd
 int epollResult = epoll_ctl(mEpollFd.get(), EPOLL_CTL_ADD, fd, &eventItem);
 if (epollResult < 0) {
 ...
 return -1;
 }
 mRequests.emplace(seq, request);
 mSequenceNumberByFd.emplace(fd, seq);
 } else {
 int epollResult = epoll_ctl(mEpollFd.get(), EPOLL_CTL_MOD, fd, &eventItem);
 if (epollResult < 0) {
 ...
 return -1;
 }
 const SequenceNumber oldSeq = seq_it->second;
 mRequests.erase(oldSeq);
 mRequests.emplace(seq, request);
 seq_it->second = seq;
 }
 } // release lock
 return 1;
}

上面的代码使用我们的fd创建了epoll_event,并且调用系统调用epoll_ctl来进行注册,只是代码中判断了fd在不再mSequenceNumberByFd当中,在的话使用的是EPOLL_CTL_ADD,不在则使用EPOLL_CTL_MOD。 这里我们需要了解一下epoll,它是linux中的一种高效、可扩展的I/O时间通知机制,而我们这里做的就是监听Binder驱动的FD,当Binder驱动中有变化通知到epoll的文件描述符,也就是我们这里的Looper的回调就可以收到,具体监听的事件为Looper::EVENT_INPUT,从而也就会执行BinderCallbackhandlePolledCommands方法,这个我们留到后面再分析。

看到这,我们可以再看一下main方法的第16行代码,调用的ClientCallbackCallback.setupTo方法,这个类也同样是一个LooperCallback的子类,其代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
static sp<ClientCallbackCallback> setupTo(const sp<Looper>& looper, const sp<ServiceManager>& manager) {
 sp<ClientCallbackCallback> cb = sp<ClientCallbackCallback>::make(manager);

 int fdTimer = timerfd_create(CLOCK_MONOTONIC, 0 /*flags*/);

 itimerspec timespec {
 .it_interval = {
 .tv_sec = 5,
 .tv_nsec = 0,
 },
 .it_value = {
 .tv_sec = 5,
 .tv_nsec = 0,
 },
 };

 int timeRes = timerfd_settime(fdTimer, 0 /*flags*/, &timespec, nullptr);

 int addRes = looper->addFd(fdTimer,
 Looper::POLL_CALLBACK,
 Looper::EVENT_INPUT,
 cb,
 nullptr);

 return cb;
}

这里addFd的代码和之前的一样,但是前面这个timerfd令人感到疑惑,查询一番之后才知道,这里是linux中的定时机制,timerfd为一个基于文件描述符的定时接口,我们这里的代码则是每个5秒钟触发一次,也就是说每隔5秒就会执行一次这个对象的handleEvent方法。

当有客户端请求添加Service或者查询Service等操作的时候,BinderCallbak的handlePolledCommands就会执行去处理,内部会调用如下代码:

1
IPCThreadState::self()->handlePolledCommands();

上面的代码就是会去读取binder传过来的数据,进行处理,具体内容留到后面再分析。

看完评论一下吧

Android源码分析:Binder概述与ServiceManager获取

2024年9月4日 19:09

阅读Android系统源码,Binder是绕不过去的东西,前面看ContentProvider,Activity都有Binder的身影,因此决定还是先把Binder的部分看一看。本文主要简单介绍一下Binder的历史和它的基本架构,介绍Binder的ServiceManager我们在使用的时候如何去拿到它,同时推荐一些Binder的学习资料。

Binder简介

对于普通的Android的应用开发来说,进程的概念是被弱化的。这得益于系统已经帮助我们把Activity,ContentProvider,Broadcast,Service等涉及到跨进程的组件做了很好的封装。我们知道Android也是基于Linux进行开发的,那比如存在跨进程,也就必然存在跨进程通讯。Linux当中跨进程通讯常常使用共享内存、信号量、管道等方式,不过Android中为了安全和使用的便利性,则大部分地方都是使用了Binder。

Binder并不是新提出来的一套跨进程通信机制,它是基于OpenBinder实现的。Binder最早是Be公司开发的, George Hoffman需要一种机制让Be的互联网设备的Javascript UI层与地秤系统服务发生交互,边开发了Binder。后来Be公司的工程师加入了PalmSource开发Palm OS,再后来加入Android,Binder也一直被他们采用,并且也在一直演化,对这段历史感兴趣的话,可以看看《安卓传奇:Android缔造团队回忆录》这本书。开源的OpenBinder是可以工作在Linux内核中的,在2015年已经被合并到Linux 内核3.19版本当中,不仅仅Android,华为的鸿蒙系统当中也在使用Binder。

Binder基本架构

Android中的Binder包括Binder驱动,ServiceManager,Binder服务,他们的关系大概如上图所示。Binder驱动位于Linux内核层,它主要用于实现多个进程之间的通信。ServiceManager位于Android Framework层,用于Binder服务的注册和查找,相当于网络服务中的DNS。而Binder服务的Server端和Client端就是典型的C/S架构,它们通过Binder驱动来进行交互。Android中有两种Binder服务,一种是类似于AMS,PMS这种的系统服务,它们是有名称的服务,注册在ServiceManager当中,Client可以通过名称查找到他们进行使用。还存在另一种匿名Binder服务,比如我们自己通过AIDL创建的,这种我们会直接通过其他的Binder服务把Binder引用传递到客户端,从而双方可以进行通讯。

Binder驱动的源码是在Linux当中的,暂时先不关注了。我这里主要会去看Android Framework层当中Binder相关的代码。ServiceManager本身也是一个Binder,它的ID为0,因此可以很简单的拿到,它的初始化我就不关注了。首先会去关注我们在应用层如何去拿到ServiceManager,因为只有拿到它才能够使用它去注册Binder和获取Binder。其实我们再去看看ServiceManger的addService如何注册一个Binder服务,以及getService 如何获取一个Binder服务。这些看完之后,我们也就知道了Binder的完整运行过程,因为addServicegetService本身也是binder调用,其中我们也会分析Framwork调用kernel相关的代码。最后我们再看看匿名Binder,AIDL,这样差不多就可以对于Framework层的Binder有了全面的了解。

获取ServiceManager

ServiceManager本身是存在一个单独的进程的,并且是在系统启动的时候就启动了它。而我们在其他进程想要通过它来注册服务或者获取服务,就需要首先拿到它的Ibinder对象。通常会用如下的方式获取:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
private static IServiceManager getIServiceManager() {
 if (sServiceManager != null) {
 return sServiceManager;
 }

 // Find the service manager 
 sServiceManager = ServiceManagerNative
 .asInterface(Binder.allowBlocking(BinderInternal.getContextObject()));
 return sServiceManager;
}

上面的代码首先会拿本地的缓存,拿不到才会真正调用获取ServiceManager的步骤,我们先看看这个BinderInternal.getContextObject()方法,它是一个native方法,它的实现在base/core/jni/android_util_Binder.cpp当中,代码如下:

1
2
3
4
5
static jobject android_os_BinderInternal_getContextObject(JNIEnv* env, jobject clazz)
{
 sp<IBinder> b = ProcessState::self()->getContextObject(NULL);
 return javaObjectForIBinder(env, b);
}

首先调用ProcessState::self()来拿到ProcessState实例,它内部会执行如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
sp<ProcessState> ProcessState::init(const char *driver, bool requireDefault)
{

 if (driver == nullptr) {
 std::lock_guard<std::mutex> l(gProcessMutex);
 if (gProcess) {
 verifyNotForked(gProcess->mForked);
 }
 return gProcess;
 }

 [[clang::no_destroy]] static std::once_flag gProcessOnce;
 std::call_once(gProcessOnce, [&](){
 if (access(driver, R_OK) == -1) {
 driver = "/dev/binder";
 }

 int ret = pthread_atfork(ProcessState::onFork, ProcessState::parentPostFork,
 ProcessState::childPostFork); //注册fork进程的监听
 LOG_ALWAYS_FATAL_IF(ret != 0, "pthread_atfork error %s", strerror(ret));

 std::lock_guard<std::mutex> l(gProcessMutex);
 gProcess = sp<ProcessState>::make(driver); //智能指针初始化
 });

 if (requireDefault) {
 ...
 }

 verifyNotForked(gProcess->mForked);
 return gProcess;
}

上面的代码传入的参数driver值为/dev/binder也就是binder驱动的地址,requireDefault为false。上面的代码中的std:call_once方法为android的libc所提供,就是保证下面的代码段只会执行一次,这个实现也就是为了实现单例,和Java代码中的其实是差不多的。代码中的make方法内部实际会调用ProcessState的构造方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
ProcessState::ProcessState(const char* driver)
 : mDriverName(String8(driver)),
 mDriverFD(-1),
 mVMStart(MAP_FAILED),
 mThreadCountLock(PTHREAD_MUTEX_INITIALIZER),
 mThreadCountDecrement(PTHREAD_COND_INITIALIZER),
 mExecutingThreadsCount(0),
 mWaitingForThreads(0),
 mMaxThreads(DEFAULT_MAX_BINDER_THREADS),
 mStarvationStartTimeMs(0),
 mForked(false),
 mThreadPoolStarted(false),
 mThreadPoolSeq(1),
 mCallRestriction(CallRestriction::NONE) {
 base::Result<int> opened = open_driver(driver);

 if (opened.ok()) {
 mVMStart = mmap(nullptr, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, opened.value(), 0);
 if (mVMStart == MAP_FAILED) {
 close(opened.value()); //mmap失败,关闭binder文件描述符
 opened = base::Error()
 << "Using " << driver << " failed: unable to mmap transaction memory.";
 mDriverName.clear();
 }
 }
 verifyNotForked(gProcess->mForked); //检查当前的实例不是fork之后的只进程的实例否则报错
 if (opened.ok()) {
 mDriverFD = opened.value(); //记录binder的文件描述符
 }
}

open_driver内部就是调用linux的系统调用open打开binder驱动,并通过ioctl获取驱动打开状态以及进行驱动的一些设置如最大线程数等,这里就查看相关代码了。

打开驱动后又会调用mmap把进行内存映射并保存内存指针到mVMStart上,其中内存映射的大小为BINDER_VM_SIZE,定义如下:

1
2
#define BINDER_VM_SIZE ((1 * 1024 * 1024) - sysconf(_SC_PAGE_SIZE) * 2)
#define _SC_PAGE_SIZE 0x0028

其中sysconf(_SC_PAGE_SIZE)之前的值为4k,最新的Android 15改成了16K,那我们这里仍然以来版本计算,可以得到值为1016Kb,这也就是我们使用Binder交互时候数据传输的限制。

这里我们拿到了binder的文件描述符,也完成了内存映射,也就完成了ProcessState的初始化。ProcessState这个对象如它的名字,在每个进程当中只会有一个实例。

有了实例我们又可以继续看getContextObject,主要代码如下:

1
2
3
4
5
6
7
8
9
sp<IBinder> ProcessState::getContextObject(const sp<IBinder>& /*caller*/)
{
 sp<IBinder> context = getStrongProxyForHandle(0);

 if (context) {
 internal::Stability::markCompilationUnit(context.get()); //更新Binder的Stability,展示可以跳过
 }
 return context;
}

上面主要关注getStrongProxyForHandle(0),这里传入的id为0,也就是专属于ServiceManager的,此方法代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
ProcessState::handle_entry* ProcessState::lookupHandleLocked(int32_t handle)
{
 const size_t N=mHandleToObject.size();
 if (N <= (size_t)handle) {
 handle_entry e;
 e.binder = nullptr;
 e.refs = nullptr;
 status_t err = mHandleToObject.insertAt(e, N, handle+1-N);
 if (err < NO_ERROR) return nullptr;
 }
 return &mHandleToObject.editItemAt(handle);
}


sp<IBinder> ProcessState::getStrongProxyForHandle(int32_t handle)
{
 sp<IBinder> result;

 AutoMutex _l(mLock);

 handle_entry* e = lookupHandleLocked(handle);

 if (e != nullptr) {
 IBinder* b = e->binder;
 if (b == nullptr || !e->refs->attemptIncWeak(this)) {
 if (handle == 0) {
 IPCThreadState* ipc = IPCThreadState::self();
 CallRestriction originalCallRestriction = ipc->getCallRestriction(); //获取当前的调用限制
 ipc->setCallRestriction(CallRestriction::NONE); //设置限制为空

 Parcel data;
 status_t status = ipc->transact(
 0, IBinder::PING_TRANSACTION, data, nullptr, 0); //调用ping,获取当前Binder的状态

 ipc->setCallRestriction(originalCallRestriction); //恢复原先的限制

 if (status == DEAD_OBJECT)
 return nullptr;
 }

 sp<BpBinder> b = BpBinder::PrivateAccessor::create(handle);
 e->binder = b.get();
 if (b) e->refs = b->getWeakRefs();
 result = b;
 } else {
 result.force_set(b);
 e->refs->decWeak(this);
 }
 }

 return result;
}


struct handle_entry {
 IBinder* binder;
 RefBase::weakref_type* refs;
};

Vector<handle_entry> mHandleToObject;

handle_entry为结构提,其中存放了IBinderrefs,refs为一个弱引用,用于记录Binder的使用数量,这些entry有存放在动态数组mHandleToObject当中。 查找过程很简单,就是数组中有则返回,无则插入一条。对于ServiceManager,此处会调用IPCThreadState的相关方法,首先看看它的self方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
IPCThreadState* IPCThreadState::self()
{
 if (gHaveTLS.load(std::memory_order_acquire)) {
restart:
 const pthread_key_t k = gTLS;
 IPCThreadState* st = (IPCThreadState*)pthread_getspecific(k);
 if (st) return st;
 return new IPCThreadState;
 }

 // Racey, heuristic test for simultaneous shutdown.
 if (gShutdown.load(std::memory_order_relaxed)) {
 ALOGW("Calling IPCThreadState::self() during shutdown is dangerous, expect a crash.\n");
 return nullptr;
 }

 pthread_mutex_lock(&gTLSMutex);
 if (!gHaveTLS.load(std::memory_order_relaxed)) {
 int key_create_value = pthread_key_create(&gTLS, threadDestructor);
 if (key_create_value != 0) {
 pthread_mutex_unlock(&gTLSMutex);
 ALOGW("IPCThreadState::self() unable to create TLS key, expect a crash: %s\n",
 strerror(key_create_value));
 return nullptr;
 }
 gHaveTLS.store(true, std::memory_order_release);
 }
 pthread_mutex_unlock(&gTLSMutex);
 goto restart;
}

这里的gHaveTLS类型为atomic<bool>和java中的AtomicBoolean一样都是原子类型安全的Boolean,这里的TLS不是https中我们说的那个TLS而是表示Thread Local Storage,这里我们就可以明白,此处我们是把IPCThreadState存放在Thread Local中,从而保证每一个线程拥有一个IPCThreadState对象,这个类的构造函数如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
IPCThreadState::IPCThreadState()
 : mProcess(ProcessState::self()),
 mServingStackPointer(nullptr),
 mServingStackPointerGuard(nullptr),
 mWorkSource(kUnsetWorkSource),
 mPropagateWorkSource(false),
 mIsLooper(false),
 mIsFlushing(false),
 mStrictModePolicy(0),
 mLastTransactionBinderFlags(0),
 mCallRestriction(mProcess->mCallRestriction) {
 pthread_setspecific(gTLS, this); //key 为gTLS, value为IPCThreadState,存到ThreadLocal中。
 clearCaller();
 mIn.setDataCapacity(256);
 mOut.setDataCapacity(256);
}

void IPCThreadState::clearCaller()
{
 mCallingPid = getpid();
 mCallingSid = nullptr; // expensive to lookup
 mCallingUid = getuid();
}

构造方法中除了设置mInmOut这两个Parcel外,就是设置Callinguidpid为当前调用进程的值。

回到getStrongProxyForHandle方法,检查binder状态的代码看我的注释就好,可以继续看第41行,它内部调用了如下代码:

1
static sp<BpBinder> create(int32_t handle) { return BpBinder::create(handle); }

而这个create方法内部也主要调用了如下代码:

1
return sp<BpBinder>::make(BinderHandle{handle}, trackedUid);

这里使用了强引用指针,我们解析以下实际上是调用了如下代码:

1
BpBinder(BinderHandle{handle}, trackedUid);

也就是创建了一个BpBinder,在它的构造方法中会调用如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
IPCThreadState::self()->incWeakHandle(this->binderHandle(), this);



void IPCThreadState::incWeakHandle(int32_t handle, BpBinder *proxy)
{
 LOG_REMOTEREFS("IPCThreadState::incWeakHandle(%d)\n", handle);
 mOut.writeInt32(BC_INCREFS);
 mOut.writeInt32(handle);
 if (!flushIfNeeded()) {
 // Create a temp reference until the driver has handled this command.
 proxy->getWeakRefs()->incWeak(mProcess.get());
 mPostWriteWeakDerefs.push(proxy->getWeakRefs());
 }
}

上面写入mOut的数据,在将来调用flushCommands的时候会与Binder驱动交互,这个后面再分析。到这里我们就拿到ServiceManager的BpBinder对象了,但是我们现在还是在native层,因此还需要把对象返回到java层,我们这个时候可以看javaObjectForIBinder方法的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
jobject javaObjectForIBinder(JNIEnv* env, const sp<IBinder>& val)
{
 if (val->checkSubclass(&gBinderOffsets)) {
 // It's a JavaBBinder created by ibinderForJavaObject. Already has Java object.
 jobject object = static_cast<JavaBBinder*>(val.get())->object();
 return object;
 }

 BinderProxyNativeData* nativeData = new BinderProxyNativeData();
 nativeData->mOrgue = new DeathRecipientList;
 nativeData->mObject = val;

 jobject object = env->CallStaticObjectMethod(gBinderProxyOffsets.mClass,
 gBinderProxyOffsets.mGetInstance, (jlong) nativeData, (jlong) val.get());
 if (env->ExceptionCheck()) {
 // In the exception case, getInstance still took ownership of nativeData.
 return NULL;
 }
 BinderProxyNativeData* actualNativeData = getBPNativeData(env, object);
 if (actualNativeData == nativeData) {
 // Created a new Proxy
 uint32_t numProxies = gNumProxies.fetch_add(1, std::memory_order_relaxed);
 uint32_t numLastWarned = gProxiesWarned.load(std::memory_order_relaxed);
 ....
 } else {
 delete nativeData;
 }

 return object;
}

上面的代码首先去看看我们现在的指针中的类是否为Java层Binder类的子类,这种情况在binder由ibinderForJavaObject创建,我们这里不是。因此会使用下面的代码,这里gBinderProxyOffsets的相关值如下:

1
2
3
4
5
6
7
8
9
const char* const kBinderProxyPathName = "android/os/BinderProxy";
jclass clazz = FindClassOrDie(env, kBinderProxyPathName);
gBinderProxyOffsets.mClass = MakeGlobalRefOrDie(env, clazz);
gBinderProxyOffsets.mGetInstance = GetStaticMethodIDOrDie(env, clazz, "getInstance",
 "(JJ)Landroid/os/BinderProxy;");
gBinderProxyOffsets.mSendDeathNotice =
 GetStaticMethodIDOrDie(env, clazz, "sendDeathNotice",
 "(Landroid/os/IBinder$DeathRecipient;Landroid/os/IBinder;)V");
gBinderProxyOffsets.mNativeData = GetFieldIDOrDie(env, clazz, "mNativeData", "J");

也就是说这里会调用BinderProxygetInstance方法来创建BinderProxy实例,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
private static BinderProxy getInstance(long nativeData, long iBinder) {
 BinderProxy result;
 synchronized (sProxyMap) {
 try {
 result = sProxyMap.get(iBinder);
 if (result != null) {
 return result;
 }
 result = new BinderProxy(nativeData);
 } catch (Throwable e) {
NativeAllocationRegistry.applyFreeFunction(NoImagePreloadHolder.sNativeFinalizer, nativeData);
 throw e;
 }
 NoImagePreloadHolder.sRegistry.registerNativeAllocation(result, nativeData);
 sProxyMap.set(iBinder, result);
 }
 return result;
}

主要就是把nativeData放到BinderProxy对象当中,并且用iBinder做为key放到缓存map当中去。到这里native层的代码就全部分析完了。可以继续回到java层的代码。接下来就是调用ServiceManagerNative.asInterface方法,代码如下:

1
2
3
4
5
6
public static IServiceManager asInterface(IBinder obj) {
 if (obj == null) {
 return null;
 }
 return new ServiceManagerProxy(obj);
}

其中就是用ServiceManagerProxy对我们刚刚拿到的BinderProxy进行代理。这样便完成了获取ServiceManager的整个流程。

获取ServiceManager的时序图如下所示:

sequenceDiagram
ServiceManager->>BinderInternal: getContextObject()
BinderInternal->>ProcessState: self()
ProcessState->>ProcessState: getContextObject()
ProcessState->>ProcessState: getStrongProxyForHandle()
ProcessState->>IPCThreadState: self()
IPCThreadState->>IPCThreadState: transact
note over IPCThreadState: PING_TRANSACTION
ProcessState-->>BinderInternal: createBinder
note right of BinderInternal: BpBinder
BinderInternal-->>ServiceManager: javaObjectForIBinder
note right of ServiceManager: BinderProxy
ServiceManager-->>ServiceManager: asInterface
note right of ServiceManager: ServiceManagerProxy

我们现在所分析的流程,本质上还是客户端去获取一个Binder的流程,当然这个binder比较特殊,它直接写死了id为0。可以再回顾一下刚刚涉及到的类。 首先是ProcessState,每个进程都会有一个它的实例,它用于维护打开binder驱动的文件描述符、维护binder线程池以及创建IPCThreadState等。IPCThreadState则用于具体的Binder连接,它会通过ThreadLocal依附于线程,与Binder驱动交互的相关代码都在它的内部。客户端在native端的binder对象为BpBinder,在java端的对象则为BinderProxy。

Binder学习资料

我的文章只会介绍Android Framework层Binder相关的知识,Binder驱动是在Kernel当中的,我不会涉及。另外ServiceManager也是一个系统的守护进程,系统启动的时候也就会启动,我可能也不会分析了。因此推荐以下资料,方便大家在学习Binder,同时对于我没有涉及到的部分也可以参考。

  1. Binder学习指南 这个介绍的还比较通俗易懂
  2. Gityuan Binder系列详解基于Android 6.0,内容详细,从驱动到应用层全部都有讲解
  3. Android深入浅出之Binder机制 邓平凡老师的讲解,可以大概弄清楚binder的机制。

到这里分析完这个流程,我们后面就可以分析addService流程了,待到下次文章继续分享。相互交流才能更好的提高,欢迎读者朋友评论交流。

看完评论一下吧

自定义 Mac 文件夹图标

2024年9月2日 09:55

不知道是否有网友和我一样,看多了macOS 系统蓝色文件夹图标,难免有些审美疲劳,并且文件夹一多,找起来也会头晕眼花。这篇文章将简单分享一下如何自定义文件夹图标,将其修改为类似于“下载”、“文稿”、“资源库”等系统文件夹那样带图案的文件夹。

自定义 Mac 文件夹图标最先出现在Jack‘s Space

如何把钱从新加坡转到香港?Wise使用体验

2024年8月30日 00:00

money

以前我在英国和欧洲时,都是直接到银行操作海外跨境汇款,直到最近几年见JUSTYY博主给我汇款时使用Wise,但我没有这个需求,所以注册了这个Wise账号后也没有亲自试验。

关于Wise

Wise的前身是一家2011年成立的英国金融科技公司,前称TransferWise,是伦敦金融监管局授权的电子货币机构,是伦敦证券交易所上市公司,为全球大多数国家提供国际汇款服务。
Wise提供服务包括国际汇款、借记卡、Wise账户等,中国商户和个人可使用国际汇款和Wise账户汇款、换汇等,外汇可以以极低的汇损提现到国内银联卡和支付宝,或者汇款到国外。中国大陆账户不支持实体卡。

最近因为我想把我新加坡OCBC银行(想开户的朋友参考新加坡OCBC华侨银行线上开户完全指南)上面的一些资金汇到香港的银行账户(想开户的朋友参考香港银行开户经验),然后再通过中银香港和内地之间的免费汇款服务汇到内地,于是体验了一下Wise的国际汇款服务。

作为体验,我尝试汇款3200新加坡元SGD到香港并变成港币HKD,Wise的汇率是1 SGD=5.9604 HKD,扣除Wise的整体汇款费是10.12 SGD(60元左右)后,香港的银行收到了19013.09 HKD。

而如果我直接通过OCBC银行的国际汇款服务,手续费每笔35美元等值,邮费每笔17美元等值,电报费每笔35美元等值,加起至少八九十美元了,因此平均无论是汇率还是手续费,我感觉Wise都不错,Wise的手续费率才千分之三左右。

速度方面,Wise几乎是秒到账,这比银行的跨境国际汇款到账速度快多了——我上次通过国内的工商银行汇款到OCBC也花了几个小时才到账(手续费80元左右),Wise在到账速度方面几乎无可挑剔。

不试不知道,一试吓一跳,原来Wise的服务如此好用。这其实就是专业化,Wise是专门做转账汇兑跨境汇款服务的公司,比起银行自身的国际汇款服务,自然要强得多,就像国内的支付宝做支付,在日常生活中其实已经干翻了各银行的银联支付。

Wise虽是第三方公司,但它也有正规的金融牌照,我通过它申请到了各主流国家银行的银行账号(没有实体卡),Wise甚至还送给了一个VISA的虚拟银行卡号(Debit Card,我使用了非内地信息注册)。

新加坡和香港都没有外汇管制,因此两地之间的资金通过Wise可以自由低成本地流动,当然前提是资金来源合法合规。

各位朋友,趁着国内也还能注册Wise,可以注册一个账号使用,或者留着未来备用。

通过我的Wise注册链接注册,可以获得最高¥4,500等值的免手续费汇款优惠。注册使用其实很简单,通过链接下载APP或者直接在网页上按步骤注册使用即可。

欢迎大家也分享国际跨境汇款这方面的优惠信息和实战经验,让资金自由方便地流动起来。

Android源码分析: ContentProvider查询以及数据变化监听分析

2024年8月29日 18:51

之前已经分析了启动应用安装ContentProvider,使用时获取ContentProvider,我们这里再分析一下使用ContentProvider查询数据已经监听ContentProvider数据变化的情况。

查询数据

上次的文章已经介绍了使用query的方法,并且已经介绍完了通过acquireProvider获取到ContentProvider,如果是是本地应用的话拿到的是Transport对象,如果是查询其他应用(不严谨的说法,其他应用也要排查userId不同,且不共享签名),则拿到的是ContentProviderProxy,这里我们要分析的查询是其他应用的情况,因此我们需要关注ContentProviderProxyquery方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@Override
public Cursor query(@NonNull AttributionSource attributionSource, Uri url,
 @Nullable String[] projection, @Nullable Bundle queryArgs,
 @Nullable ICancellationSignal cancellationSignal)
 throws RemoteException {
 BulkCursorToCursorAdaptor adaptor = new BulkCursorToCursorAdaptor();
 Parcel data = Parcel.obtain();
 Parcel reply = Parcel.obtain();
 try {
 data.writeInterfaceToken(IContentProvider.descriptor);

 attributionSource.writeToParcel(data, 0);
 url.writeToParcel(data, 0);
 int length = 0;
 if (projection != null) {
 length = projection.length;
 }
 data.writeInt(length);
 for (int i = 0; i < length; i++) {
 data.writeString(projection[i]);
 }
 data.writeBundle(queryArgs);
 data.writeStrongBinder(adaptor.getObserver().asBinder());
 data.writeStrongBinder(
 cancellationSignal != null ? cancellationSignal.asBinder() : null);

 mRemote.transact(IContentProvider.QUERY_TRANSACTION, data, reply, 0);

 DatabaseUtils.readExceptionFromParcel(reply);

 if (reply.readInt() != 0) {
 BulkCursorDescriptor d = BulkCursorDescriptor.CREATOR.createFromParcel(reply);
 Binder.copyAllowBlocking(mRemote, (d.cursor != null) ? d.cursor.asBinder() : null);
 adaptor.initialize(d);
 } else {
 adaptor.close();
 adaptor = null;
 }
 return adaptor;
 } catch (RemoteException ex) {
 adaptor.close();
 throw ex;
 } catch (RuntimeException ex) {
 adaptor.close();
 throw ex;
 } finally {
 data.recycle();
 reply.recycle();
 }
}

这个代码比较简单,把需要查询的条件写入到Parcel中,然后通过mRemote进行binder调用,在reply中拿到远端执行的结果。如果执行成功了,则通过BulkCursorDescriptor来读取reply中的数据,主要是拿到了其中IBulkCursor的Binder对象和CursorWindow这个对象。在查询的流程中会涉及到很多的类,我这里画了使用端和服务端会使用到的Cursor所涉及到相关类和接口。

其中BulkCursorToCursorAdapter为客户端使用,用于读取服务端通过binder传过来的数据,其中的封装和使用,我们后面还会继续看到。关于服务端的我们先继续往后看代码,随后会涉及到相关的类。

可以看看Provider服务端是如何把这些东西放到reply中的。我们这个时候可以看一下ContentProviderNativeonTransactQUERY_TRANSACTION的这一分支:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
data.enforceInterface(IContentProvider.descriptor);

AttributionSource attributionSource = AttributionSource.CREATOR
 .createFromParcel(data);
Uri url = Uri.CREATOR.createFromParcel(data);

// String[] projection 
int num = data.readInt();
String[] projection = null;
if (num > 0) {
 projection = new String[num];
 for (int i = 0; i < num; i++) {
 projection[i] = data.readString();
 }
}

Bundle queryArgs = data.readBundle();
IContentObserver observer = IContentObserver.Stub.asInterface(
 data.readStrongBinder());
ICancellationSignal cancellationSignal = ICancellationSignal.Stub.asInterface(
 data.readStrongBinder());

这是其中的第一部分代码,就是把binder传过来的查询需要的数据进行反序列化。

1
2
Cursor cursor = query(attributionSource, url, projection, queryArgs,
 cancellationSignal);

第二部分为调用query进行查询,我们知道在数据提供端,其实是Transport,可以看看它的query方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@Override
public Cursor query(@NonNull AttributionSource attributionSource, Uri uri,
 @Nullable String[] projection, @Nullable Bundle queryArgs,
 @Nullable ICancellationSignal cancellationSignal) {
 uri = validateIncomingUri(uri);
 uri = maybeGetUriWithoutUserId(uri);
 if (enforceReadPermission(attributionSource, uri)
 != PermissionChecker.PERMISSION_GRANTED) {
 if (projection != null) {
 return new MatrixCursor(projection, 0);
 }

 Cursor cursor;
 final AttributionSource original = setCallingAttributionSource(
 attributionSource);
 try {
 cursor = mInterface.query(
 uri, projection, queryArgs,
 CancellationSignal.fromTransport(cancellationSignal));
 } catch (RemoteException e) {
 throw e.rethrowAsRuntimeException();
 } finally {
 setCallingAttributionSource(original);
 }
 if (cursor == null) {
 return null;
 }

 // Return an empty cursor for all columns. 
 return new MatrixCursor(cursor.getColumnNames(), 0);
 }
 traceBegin(TRACE_TAG_DATABASE, "query: ", uri.getAuthority());
 final AttributionSource original = setCallingAttributionSource(
 attributionSource);
 try {
 return mInterface.query(
 uri, projection, queryArgs,
 CancellationSignal.fromTransport(cancellationSignal));
 } catch (RemoteException e) {
 throw e.rethrowAsRuntimeException();
 } finally {
 setCallingAttributionSource(original);
 Trace.traceEnd(TRACE_TAG_DATABASE);
 }
}

其中的代码也是比较简单的,首先调用enforceReadPermission检查是否有使用这个ContentProvider的权限,如果有权限则调用mInterface.query,我们看源码就知道,这个mInterface也就是一个ContentProvider,也就是我们开发过程实现的那个ContentProvider,query方法也就是我们自己的实现。

我们先不着急分析后面的代码,我们前面说过如果是本进程的ContentProvider查询会直接调用Transportquery方法,那么就不存在binder调用,而是直接调用了我们所实现的query。这个实现还是很妙的,值得我们学习。对于我们跨进程的调用,还需要看ContentProviderNativeonTransact后面的代码,也就是我们要说的第三部分:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
if (cursor != null) {
 CursorToBulkCursorAdaptor adaptor = null;

 try {
 adaptor = new CursorToBulkCursorAdaptor(cursor, observer,
 getProviderName());
 cursor = null;

 BulkCursorDescriptor d = adaptor.getBulkCursorDescriptor();
 adaptor = null;

 reply.writeNoException();
 reply.writeInt(1);
 d.writeToParcel(reply, Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
 } finally {
 // Close cursor if an exception was thrown while constructing the adaptor. 
 if (adaptor != null) {
 adaptor.close();
 }
 if (cursor != null) {
 cursor.close();
 }
 }
} else {
 reply.writeNoException();
 reply.writeInt(0);
}

这里在拿到数据的时候,通过CursorToBulkCursorAdapter把刚刚查询到Cursor进行了包装,并且通过BulkCursorDescriptor写入到reply中。这样我们刚刚调用端就可以拿到了。 我们看CursorToBulkCursorAdapter可以看到,它的内部又用CrossProcessCursorWrapper来对Cursor进行了封装。

我们已经完成查询,并且获取到Cursor的封装,接下来我们就可以看一下数据的读取了。通过我们前面的Cursor的各个相关类的关系图,我们知道在客户端我们所拿到的是BulkCursorToCursorAdapter,它的初始化代码如下:

1
2
3
4
5
6
7
8
9
public void initialize(BulkCursorDescriptor d) {
 mBulkCursor = d.cursor;
 mColumns = d.columnNames;
 mWantsAllOnMoveCalls = d.wantsAllOnMoveCalls;
 mCount = d.count;
 if (d.window != null) {
 setWindow(d.window);
 }
}

以下为服务端和客户端交互的流程时序图,通过这个图我们可以具体看到查询过程服务端和客户端交互,以及两边封装的类:

sequenceDiagram
App->>ContentProviderProxy: query
ContentProviderProxy->>+Transport(Server): Binder(QUERY_TRANSACTION)
Transport(Server)->>+ContentProvider: query
ContentProvider -->>- Transport(Server): return query result
note right of Transport(Server): Cursor
Transport(Server) -->>- ContentProviderProxy: Binder(write reply)
note left of Transport(Server): CursorToBulkCursorAdaptor
ContentProviderProxy --> App: return query result
note left of ContentProviderProxy: BulkCursorToCursorAdaptor

服务端传输数据到调用端

可以知道我们主要从服务端拿到两个东西,一个是BulkCursor它是后面我们的数据移动的Binder操作类,CursorWindow则用来存放当前位置的数据。当我们调用CursormoveToNext的时候,就会调用BulkCursorToCursorAdapteronMove方法,进而又会通过binder调用CursorToBulkCursorAdapteronMove方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
public boolean onMove(int oldPosition, int newPosition) {
 throwIfCursorIsClosed();

 try {
 if (mWindow == null
 || newPosition < mWindow.getStartPosition()
 || newPosition >= mWindow.getStartPosition() + mWindow.getNumRows()) {
 setWindow(mBulkCursor.getWindow(newPosition));
 } else if (mWantsAllOnMoveCalls) {
 mBulkCursor.onMove(newPosition);
 }
 } catch (RemoteException ex) {
 return false;
 }

 if (mWindow == null) {
 return false;
 }

 return true;
}

当window还没有初始化的时候,会调用setWindowsetWindow很简单,但是mBulkCursor.getWindow却不简单:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Override
public CursorWindow getWindow(int position) {
 synchronized (mLock) {
 throwIfCursorIsClosed();

 if (!mCursor.moveToPosition(position)) {
 closeFilledWindowLocked();
 return null;
 }

 CursorWindow window = mCursor.getWindow();
 if (window != null) {
 closeFilledWindowLocked();
 } else {
 window = mFilledWindow;
 if (window == null) {
 mFilledWindow = new CursorWindow(mProviderName);
 window = mFilledWindow;
 } else if (position < window.getStartPosition()
 || position >= window.getStartPosition() + window.getNumRows()) {
 window.clear();
 }
 mCursor.fillWindow(position, window);
 }

 if (window != null) {
 window.acquireReference();
 }
 return window;
 }
}

我们可以看到此处会调用fillWindow,而此处的mCursorCrossProcessCursorWrapper的实例,fillWindow则会调用如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Override
public void fillWindow(int position, CursorWindow window) {
 if (mCursor instanceof CrossProcessCursor) {
 final CrossProcessCursor crossProcessCursor = (CrossProcessCursor)mCursor;
 crossProcessCursor.fillWindow(position, window);
 return;
 }

 DatabaseUtils.cursorFillWindow(mCursor, position, window);
}

这样就会把每一个postion的数据填充到CursorWindow当中,但是这样会有个问题,为什么客户端能直接拿到呢。我们可以看看CursorWindow的内部。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public CursorWindow(String name, @BytesLong long windowSizeBytes) {
 if (windowSizeBytes < 0) {
 throw new IllegalArgumentException("Window size cannot be less than 0");
 }
 mStartPos = 0;
 mName = name != null && name.length() != 0 ? name : "<unnamed>";
 mWindowPtr = nativeCreate(mName, (int) windowSizeBytes);
 if (mWindowPtr == 0) {
 throw new AssertionError();
 }
 mCloseGuard.open("CursorWindow.close");
}

我们从这里可以看到,CursorWindow保存数据并没有直接放在java中的,而是在natvie中实现的。我们可以在CursorWindow.cpp中找到nativeCreate的实现,我们在其中会发现如下的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//CursorWindow::create
status_t CursorWindow::create(const String8 &name, size_t inflatedSize, CursorWindow **outWindow) {
 *outWindow = nullptr;

 CursorWindow* window = new CursorWindow();
 if (!window) goto fail;

 window->mName = name;
 window->mSize = std::min(kInlineSize, inflatedSize);
 window->mInflatedSize = inflatedSize;
 window->mData = malloc(window->mSize);
 if (!window->mData) goto fail;
 window->mReadOnly = false;

 window->clear();
 window->updateSlotsData();

 *outWindow = window;
 return OK;

fail:
 LOG(ERROR) << "Failed create";
fail_silent:
 delete window;
 return UNKNOWN_ERROR;
}

但是直接用内存的话,如果我们改变CursorWindow内的数据的时候,在使用端是没办法直接拿到更新的数据的。其实在给插入数据的时候,调用maybeInflate

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
status_t CursorWindow::maybeInflate() {
 int ashmemFd = 0;
 void* newData = nullptr;

 // Bail early when we can't expand any further
 if (mReadOnly || mSize == mInflatedSize) {
 return INVALID_OPERATION;
 }

 String8 ashmemName("CursorWindow: ");
 ashmemName.append(mName);

 ashmemFd = ashmem_create_region(ashmemName.string(), mInflatedSize);
 ...

 newData = ::mmap(nullptr, mInflatedSize, PROT_READ | PROT_WRITE, MAP_SHARED, ashmemFd, 0);
 ...

 {
 // Migrate existing contents into new ashmem region
 uint32_t slotsSize = sizeOfSlots();
 uint32_t newSlotsOffset = mInflatedSize - slotsSize;
 memcpy(static_cast<uint8_t*>(newData),
 static_cast<uint8_t*>(mData), mAllocOffset);
 memcpy(static_cast<uint8_t*>(newData) + newSlotsOffset,
 static_cast<uint8_t*>(mData) + mSlotsOffset, slotsSize);

 free(mData);
 mAshmemFd = ashmemFd;
 mData = newData;
 mSize = mInflatedSize;
 mSlotsOffset = newSlotsOffset;

 updateSlotsData();
 }

 return OK;
...
}

从代码可以看到,当我们不是只读模式,且size不是和inflateSize相同的时候,会去创建匿名内存,把原来的数据复制的新的匿名内存中去。而会把匿名内存的FD保存到mAshmemFd当中。这样在客户端就可以拿到这个fd,从而可以读取到数据。因为这样做,也只是把fd和CursorWindow的一些基本信息从服务端传到了Client,这样服务端往匿名内存中写数据,客户端也就可以拿到其中的数据了。这样做既可以减少Binder调用的数据量,也可以解决掉Binder传输有1MB的限制。为了验证我们的想法,可以看看代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
status_t CursorWindow::writeToParcel(Parcel* parcel) {
 LOG(DEBUG) << "Writing to parcel: " << this->toString();

 if (parcel->writeString8(mName)) goto fail;
 if (parcel->writeUint32(mNumRows)) goto fail;
 if (parcel->writeUint32(mNumColumns)) goto fail;
 if (mAshmemFd != -1) {
 if (parcel->writeUint32(mSize)) goto fail;
 if (parcel->writeBool(true)) goto fail;
 if (parcel->writeDupFileDescriptor(mAshmemFd)) goto fail;
 } else {
 // Since we know we're going to be read-only on the remote side,
 // we can compact ourselves on the wire. size_t slotsSize = sizeOfSlots();
 size_t compactedSize = sizeInUse();
 if (parcel->writeUint32(compactedSize)) goto fail;
 if (parcel->writeBool(false)) goto fail;
 void* dest = parcel->writeInplace(compactedSize);
 if (!dest) goto fail;
 memcpy(static_cast<uint8_t*>(dest),
 static_cast<uint8_t*>(mData), mAllocOffset);
 memcpy(static_cast<uint8_t*>(dest) + compactedSize - slotsSize,
 static_cast<uint8_t*>(mData) + mSlotsOffset, slotsSize);
 }
 return OK;

fail:
 LOG(ERROR) << "Failed writeToParcel";
fail_silent:
 return UNKNOWN_ERROR;
}

可以看到在服务端转成Parcel的时候,是写入了name,numRow,numColumn, size, ashMemFd这些,同样,在客户端也会读取这些东西。代码就不贴了。

下面再理一下执行onMoveToNext时候的流程。在客户端的调用如下:

sequenceDiagram
box LightYellow
participant App
participant CursorWrapperInner
participant BulkCursorToCursorAdaptor
participant BulkCursorProxy
end
App->>+CursorWrapperInner: moveToNext
CursorWrapperInner->>BulkCursorToCursorAdaptor: onMove
BulkCursorToCursorAdaptor->>BulkCursorProxy: onMove
BulkCursorProxy->>Remote(CursorToBulkCursorAdapter): binder(ON_MOVE_TRANSACTION)
CursorWrapperInner-->>-App: finishMoveToNext

服务端的调用如下,前面的onMove调用在使用SQLiteCursor的时候会有一些不同,这里以读取SQLite数据库为例,内容有简化:

sequenceDiagram
(Client)BulkCursorProxy->>CursorToBulkCursorAdapter: binder(ON_MOVE_TRANSACTION)
CursorToBulkCursorAdapter->>CrossProcessCursorWrapper: onMove
CrossProcessCursorWrapper->>SQLiteCursor: onMove
SQLiteCursor->>SQLiteCursor: fillWindow
SQLiteCursor->>SQLiteConnection: executeForCursorWindow
SQLiteConnection->>CursorWindow: putXX
note right of SQLiteConnection: native 写入数据到CursorWindow
box LightGreen
participant CursorToBulkCursorAdapter
participant CrossProcessCursorWrapper
participant SQLiteCursor
participant SQLiteConnection
participant CursorWindow
end

ContentObserver监听的注册

当我们想要监听一个ContentProvider的变化时,可以按照如下的方法创建一个ContentObserver,并调用registerContentObserver来注册监听,通过传入的Uri来设置指定的数据源,通过Uri的path可以设置监听指定数据源中的某一部分数据的变化。

1
2
3
4
5
6
7
val contentObserver = object: ContentObserver(Handler.getMain()) {
 override fun onChange(selfChange: Boolean) {
 super.onChange(selfChange)
 //do something while receive onChange
 }
}
contentResolver.registerContentObserver(Uri.parse("content://sms"), true, contentObserver)

我们继承的这个ContentObserver有一个内部类Transport,它实现了 IContentObserver.Stub, 这个和ContentProvider的内部类实现类似,也是实现了binder的数据交互。registerContentObserver方法也在ContentResolver中,代码如下:

1
2
3
4
5
6
7
8
9
public final void registerContentObserver(Uri uri, boolean notifyForDescendents,
 ContentObserver observer, @UserIdInt int userHandle) {
 try {
 getContentService().registerContentObserver(uri, notifyForDescendents,
 observer.getContentObserver(), userHandle, mTargetSdkVersion);
 } catch (RemoteException e) {
 throw e.rethrowFromSystemServer();
 }
}

可以看到上面的代码调用了ContentServiceregisterContentObserver方法,这里拿到的是一个binder接口的实现IContentService的代理类,在binder另一端真正执行这个方法的是在ContentService中,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@Override
public void registerContentObserver(Uri uri, boolean notifyForDescendants,
 IContentObserver observer, int userHandle, int targetSdkVersion) {
 final int uid = Binder.getCallingUid();
 final int pid = Binder.getCallingPid();
 userHandle = handleIncomingUser(uri, pid, uid,
 Intent.FLAG_GRANT_READ_URI_PERMISSION, true, userHandle);

 final String msg = LocalServices.getService(ActivityManagerInternal.class)
 .checkContentProviderAccess(uri.getAuthority(), userHandle);
 ...
 synchronized (mRootNode) {
 mRootNode.addObserverLocked(uri, observer, notifyForDescendants, mRootNode,
 uid, pid, userHandle);
 }
}

服务端的主要代码如上,其中传过来的observer为IContentObserver,它就是我们刚刚说到的和Transport相同的接口。其中主要就是调用了mRootNode.addObserverLockedmRootNode是内部类ObserverNode的实例。继续看代码之前先介绍一下这个类,这个类内部又有ObserverEntry内部类,我们的Observer会存放到这个Entry内部。ObserverNode有两个成员mChildrenmObservers,分别表示子一级的ObserverNode和当前路径级的ObserverEntry,从而组成了如下的树状结构。

addObserverLocked这个方法的代码我们也能知晓该树状结构的构成,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private void addObserverLocked(Uri uri, int index, IContentObserver observer,
 boolean notifyForDescendants, Object observersLock,
 int uid, int pid, int userHandle) {
 // If this is the leaf node add the observer 
 if (index == countUriSegments(uri)) {
 mObservers.add(new ObserverEntry(observer, notifyForDescendants, observersLock,
 uid, pid, userHandle, uri));
 return;
 }

 // Look to see if the proper child already exists 
 String segment = getUriSegment(uri, index);
 if (segment == null) {
 throw new IllegalArgumentException("Invalid Uri (" + uri + ") used for observer");
 }
 int N = mChildren.size();
 for (int i = 0; i < N; i++) {
 ObserverNode node = mChildren.get(i);
 if (node.mName.equals(segment)) {
 node.addObserverLocked(uri, index + 1, observer, notifyForDescendants,
 observersLock, uid, pid, userHandle);
 return;
 }
 }

 // No child found, create one 
 ObserverNode node = new ObserverNode(segment);
 mChildren.add(node);
 node.addObserverLocked(uri, index + 1, observer, notifyForDescendants,
 observersLock, uid, pid, userHandle);
}

这样操作完,也就完成了添加Observer的操作。

数据更新的发布与分发

那数据更新的部分呢,当我们执行了数据整删改之后,需要调用如下代码通知数据变化:

1
getContext().getContentResolver().notifyChange(uri, null);

这个notifyChange方法有几个实现,最终会调用到如下这个:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public void notifyChange(@NonNull Uri[] uris, ContentObserver observer, @NotifyFlags int flags,
 @UserIdInt int userHandle) {
 try {
 getContentService().notifyChange(
 uris, observer == null ? null : observer.getContentObserver(),
 observer != null && observer.deliverSelfNotifications(), flags,
 userHandle, mTargetSdkVersion, mContext.getPackageName());
 } catch (RemoteException e) {
 throw e.rethrowFromSystemServer();
 }
}

其中getContentService会获取到IContentService在本地的代理,而最终会通过Binder调用到system_server中的ContentService中的notifyChange方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@Override
public void notifyChange(Uri[] uris, IContentObserver observer,
 boolean observerWantsSelfNotifications, int flags, int userId,
 int targetSdkVersion, String callingPackage) {

 final int callingUid = Binder.getCallingUid();
 final int callingPid = Binder.getCallingPid();
 final int callingUserId = UserHandle.getCallingUserId();


 final ObserverCollector collector = new ObserverCollector();



 for (Uri uri : uris) {
 final int resolvedUserId = handleIncomingUser(uri, callingPid, callingUid,
 Intent.FLAG_GRANT_WRITE_URI_PERMISSION, true, userId);
 final Pair<String, Integer> provider = Pair.create(uri.getAuthority(), resolvedUserId);
 if (!validatedProviders.containsKey(provider)) {
 final String msg = LocalServices.getService(ActivityManagerInternal.class)
 .checkContentProviderAccess(uri.getAuthority(), resolvedUserId);
 if (msg != null) {
 if (targetSdkVersion >= Build.VERSION_CODES.O) {
 throw new SecurityException(msg);
 } else {
 if (msg.startsWith("Failed to find provider")) {
 // Sigh, we need to quietly let apps targeting older API 
 } else {
 Log.w(TAG, "Ignoring notify for " + uri + " from "
 + callingUid + ": " + msg);
 continue;
 }
 }
 }

 // Remember that we've validated this access 
 final String packageName = getProviderPackageName(uri, resolvedUserId);
 validatedProviders.put(provider, packageName);
 }

 synchronized (mRootNode) {
 final int segmentCount = ObserverNode.countUriSegments(uri);
 mRootNode.collectObserversLocked(uri, segmentCount, 0, observer,
 observerWantsSelfNotifications, flags, resolvedUserId, collector);
 }
 }

 final long token = clearCallingIdentity();
 try {
 // Actually dispatch all the notifications we collected 
 collector.dispatch();
 .....
 }
 } finally {
 Binder.restoreCallingIdentity(token);
 }
}

以上代码略有简化,只保留了和ContentObserver通知相关的代码,SyncManager相关的代码未放在这里。这段代码最开始是先遍历传入的Uri列表,对检查对应Uri的ContentProvider是否有权限,如果有权限则会调用collectObserversLocked把满足条件的Observer放到ObserverCollector中去,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//ObserverNode.collectObserversLocked
public void collectObserversLocked(Uri uri, int segmentCount, int index,
 IContentObserver observer, boolean observerWantsSelfNotifications, int flags,
 int targetUserHandle, ObserverCollector collector) {
 String segment = null;
 if (index >= segmentCount) {
 // This is the leaf node, notify all observers 
 collectMyObserversLocked(uri, true, observer, observerWantsSelfNotifications,
 flags, targetUserHandle, collector);
 } else if (index < segmentCount){
 segment = getUriSegment(uri, index);
 // Notify any observers at this level who are interested in descendants 
 collectMyObserversLocked(uri, false, observer, observerWantsSelfNotifications,
 flags, targetUserHandle, collector);
 }

 int N = mChildren.size();
 for (int i = 0; i < N; i++) {
 ObserverNode node = mChildren.get(i);
 if (segment == null || node.mName.equals(segment)) {
 // We found the child, 
 node.collectObserversLocked(uri, segmentCount, index + 1, observer,
 observerWantsSelfNotifications, flags, targetUserHandle, collector);
 if (segment != null) {
 break;
 }
 }
 }
}

传入的segmentCount为Uri的path的数量加上authority的数量,比如content://sms/inbox这个uri它的segmentCount就是2,而从外面传如的index为0。它会先调用collectMyObserversLocked方法来遍历当前Node层级的ObserverEntry,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private void collectMyObserversLocked(Uri uri, boolean leaf, IContentObserver observer, boolean observerWantsSelfNotifications, int flags, int targetUserHandle, ObserverCollector collector) {
 int N = mObservers.size();
 IBinder observerBinder = observer == null ? null : observer.asBinder();
 for (int i = 0; i < N; i++) {
 ObserverEntry entry = mObservers.get(i);
 boolean selfChange = (entry.observer.asBinder() == observerBinder);
 if (selfChange && !observerWantsSelfNotifications) {
 continue;
 }

 // Does this observer match the target user? 
 if (targetUserHandle == UserHandle.USER_ALL
 || entry.userHandle == UserHandle.USER_ALL
 || targetUserHandle == entry.userHandle) {
 // Make sure the observer is interested in the notification 
 if (leaf) {
 if ((flags&ContentResolver.NOTIFY_SKIP_NOTIFY_FOR_DESCENDANTS) != 0 && entry.notifyForDescendants) {
 continue;
 }
 } else {
 if (!entry.notifyForDescendants) {
 continue;
 }
 }
 collector.collect(entry.observer, entry.uid, selfChange, uri, flags, targetUserHandle);
 }
 }
}

可以看到以上代码就是遍历我们之前注册observer时候的mObservers列表,分别检查了用户id是否相等,是否满足notifyForDescendants和flag等参数后调用collector.collect方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public void collect(IContentObserver observer, int uid, boolean selfChange, Uri uri,
 int flags, int userId) {
 final Key key = new Key(observer, uid, selfChange, flags, userId);
 List<Uri> value = collected.get(key);
 if (value == null) {
 value = new ArrayList<>();
 collected.put(key, value);
 }
 value.add(uri);
}

Collector中则是以observer,uid,selfChange,flag,userId组合成key,Uri作为value放入collectedmap中。而这些只是完成了一个层级的Observer收集,collectObserversLocked方法中还会遍历mChildren,找到其中的name与segment相同的子Node,再进行收集。收集完成之后,则是调用collector.dispatch(),看名字就知道是去通知对应的Observer,具体实现逻辑如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void dispatch() {
 for (int i = 0; i < collected.size(); i++) {
 final Key key = collected.keyAt(i);
 final List<Uri> value = collected.valueAt(i);

 final Runnable task = () -> {
 try {
 key.observer.onChangeEtc(key.selfChange,
 value.toArray(new Uri[value.size()]), key.flags, key.userId);
 } catch (RemoteException ignored) {
 }
 };
 final boolean noDelay = (key.flags & ContentResolver.NOTIFY_NO_DELAY) != 0;
 final int procState = LocalServices.getService(ActivityManagerInternal.class)
 .getUidProcessState(key.uid);
 if (procState <= ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND || noDelay) {
 task.run();
 } else {
 BackgroundThread.getHandler().postDelayed(task, BACKGROUND_OBSERVER_DELAY);
 }
 }
}

这个方法则是遍历collected这个ArrayMap,从每一个key当中取出observer,并使用Runnable封装,在根据flags和当前的进程状态决定是立即通知变化还是延迟通知变化。而这里所调用的onChangeEtc则会通过Binder调用,从而调用到客户端的Observer。

这便完成了ContentProvider内容变化的通知。

总结

本文介绍了使用ContentProvider进行数据的查询、查处来的数据进行窗口移动、注册数据变化监听以及数据变化接受这几块的代码分析,加上前面两篇关于ContentProvider的文章,基本上可以对于ContentProvider整个体系有详细的了解。整删改查这四种操作中,查是比较复杂的,把它看完,增删改这三种流程,想要看明白就会简单很多,因此这里也便不再分析了。

从Android ContentProvider的设计和代码实现中我们可以学到很多东西。其中之一是前面介绍到IContentProvider在自己调用和其他App调用的区别,以及对于代码的巧妙封装,使得代码的实现比较优雅,同时代码量比较少,对于同UID应用来说性能又比较优。另外就是通过CursoWindow的实现,突破Binder数据传输的限制。ObserverNode中使用树来实现了Observer监听。

当然这只是我的一人之言,因为关注点不同,在看代码的过程中还是会有一些细节被我忽略,但是可能对于其他人来说又比较重要的。如果你对ContentProvider也有自己的见解,又或是我有错误的解读,欢迎留言交流。

看完评论一下吧

ChatGPT超级提示词生成器

2024年3月22日 12:15
ChatGPT超级提示词生成器

网上找到一个好玩儿的东西,通过简单的选择填空,就能生成优质的提示词,让ChatGPT回复更符合我们的预期。

原网页为英文,这里翻译成中文搬运过来了,可以收藏备用。

ChatGPT你好,
请你帮我处理以下工作。


请扮演一位行业顶尖、经验丰富、专业权威的 ,你的专业知识对我来说是无价的。 我需要 用于

过程中你要注意保持
请不要

开始前你要知道,这件事情的紧急性为:
你最终回复我的内容应是

以下是我给你提供的信息:


感谢你的专业支持。
复制提示词

写好一个提示词说难不难。清晰表达需求、明确回复格式、让ChatGPT扮演一个角色,可以让回复更符合自己的要求。但是很多人其实并不知道自己到底要什么,或者不知道怎么表达(做乙方的此刻与ChatGPT产生了共情)。

上面的提示词生成器通过填写模版的方式,简化了思考步骤,提升了利用AI的效率。对于需要快速获取优质回答的使用者或者新手小白,这个提示词模板很实用。

My App Defaults

2023年12月16日 20:27
My App Defaults

大家好,最近工作工作真是忙得不可开交,一天24小时不够用的,也因此好久没机会更新了。
看到很多博主在分享自己的主力软件列表,今天也来分享一下我的,权当水一下更新嘿嘿😜

My App Defaults

📨 邮箱客户端:Canary
📮 邮箱服务器:Gmail(个人)、iCloud(绑定独立域名)
📝 笔记:Memos
✅ To-Do:NotePlan
📷 手机摄影:原生相机
🟦 照片管理:原生相册
📆 日历:原生日历
📁 云盘:Onedrive(主力)+iCloud(照片)
📖 RSS:Miniflux(服务端)+Reeder+Fluent
🙍🏻‍♂️ 联系人:🈚
🌐 浏览器:Chrome
💬 聊天:微信😅
🔖 书签:Linkding(Barely use)
📑 稍后读:Linkding+Miniflux
📜 文字处理:MWeb
📈 表格:MS 365
📊 演示文稿:MS 365
🛒 购物清单:🈚
🍴 饮食规划:🈚
💰 记账:🈚
📰 新闻:同Rss
🎵 音乐:Apple Music
🎤 播客:小宇宙
🔐 密码管理:Chrome密码管理器

🚀软件启动器:Alfred
📃博客:Ghost
🔍翻译:有道翻译Alfred插件+沉浸式翻译
🎬追剧记录:TV Time
🤖人工智能对话:ChatGPT Next Web
🖌️平面设计:稿定设计+PS+Pixelmator

有话要说

  1. 主力笔记App用什么?从印象笔记没落到现在貌似就没有找到过合心意的。要么收费高,要么不支持多平台,要么有那么一两个难以忍受的缺点。老实说,现在我临时记笔记最多的地方,其实是微信文件传输助手。
  2. Rss是我获取新闻的主要方式,用Werss订阅了好几个公众号,还有活菩萨网友提供的华尔街日报源,感恩拜拜🙏
  3. 在密码管理这块,一开始我用了Enpass超过两年时间,一百元以内的买断价格加上全平台支持,在当时很得我心。后来chrome浏览器开始在密码管理的功能上发力,在iOS上开始逐渐接入原生密码管理接口,在各个app上都能填充密码,现在已经完全取代掉Enpass,成为使用起来最没门槛最方便的密码管理软件了。
  4. 不会设计的文案不是一个好活动策划。无论是工作还是平时更新博客,平面设计都占了显著的一部分工作量,稿定设计真的是我离不开的一个设计网站,可以很快套版输出质量很高的图片。

结语

这篇文章或多或少也算是今年生活的一个小小小小总结,要真的写出来一篇年度总结,我想我要请好两周假才能把这完全超出我想象的一年整理成文字吧,这样看来或许农历春节才是一个比较适合我来总结一整年的时机。大家今年过的怎么样呢?

QuitAll:我的人生态度,老Macbook的回春之路

2023年11月28日 21:41
QuitAll:我的人生态度,老Macbook的回春之路

我的2018款MacBook Pro在服役五年后,已经开始显现出各种老态:电池健康度低、使用速度随开机时间降低等。
其中电池健康度没办法,想解决只有换电池,不过目前74%的健康度还能高强度使用四五十分钟,我还能忍。
但是使用速度的降低是实在有点难以忍受。连续开机2-3天,机器的反应速度就开始出现明显卡顿,甚至影响拖动等操作,只有重启才能解决。

重启电脑真的是很烦的一件事情,在重启过程中有各种使用到一半的文档跳出来叫你复查保存,如果使用了微信双开,重启后还得重复一次繁杂的双开过程。那是否有能实现重启的效果,但又不需要真实的退出所有应用的方法呢?
在我查看最新上架的SetApp软件时,就让我发现了这样一个宝藏软件,不仅完美解决了电脑卡顿的问题,还顺带给了我很多其他惊喜。

老MacBook救星:QuitAll

QuitAll:我的人生态度,老Macbook的回春之路

QuitAll是一款简洁小巧的软件,它蛰伏在状态栏,你只需要简单点击一下他的「QuitAll」按钮,就能快速退出当前正在运行的程序。

QuitAll:我的人生态度,老Macbook的回春之路

你还可以单独设置某些进程总是不退出、总是退出,这样就能在为电脑运行腾出更多空间的同时,保留重要的程序运行。

可以说QuitAll是重启电脑的最佳替代品,可以让你毫无负担的完成一次无痛重启。

在小巧的程序界面底部,还会随机跳出各种退堂鼓名言:

Don't give up, quit instead ✔️
There is no failing in quitting 🏅

这些小趣味也消除了一些卡顿带来的烦躁。

重启能解决的玄学问题,它也能解决!

QuitAll能帮Mac恢复运行速度,已经让我很惊喜了。但没想到它还能把很多我们只能归类为「玄学」、「宿命」、「报应」的疑难电脑问题一并解决。

1.解决快捷指令「未能与帮助程序通信」的问题

QuitAll:我的人生态度,老Macbook的回春之路

我的网站CDN和图片都使用了Bunny.net的服务。在写完文章后,我会将Markdown的全文内容复制扔到Bunny图片批量上传捷径里,让捷径帮我把文章里的图片自动挑出来上传,并自动替换为网络图片地址。

但是快捷指令经常会出现「未能与帮助程序通信」的问题,之前只有重启可以解决得了。但是有了QuitAll之后我惊喜的发现,使用它来一键退出大部分程序也能解决问题!

相信是与快捷指令有关的某些程序出现了错误,只有重新启动才能解决。但是我实在找不到是哪个程序需要解决,QuitAll就帮上了大忙。

2.解决OneDrive Mac 客户端频繁自动

QuitAll:我的人生态度,老Macbook的回春之路

囿于MacBook可怜兮兮的256G储存空间,我的大部分工作文件都会同步到OneDrive上,并且会自己动删除本地文件以节省空间。在使用到这些文件时,OneDrive客户端会再从云端把文件下载下来。

这一套逻辑其实是很丝滑的,OneDrive的下载速度在挂梯后并不慢。但是OneDrive的客户端总是会自己默默退出程序,没有任何提示,导致我经常性要手动启动OneDrive,等待它缓慢的登录、同步流程,才能下载文件下来。

但在使用QuitAll后我发现,这个问题也消失了!只要用QuitAll退出所有程序一次,OneDrive就能长久的挂在电脑上。

下载安装

QuitAll是收费软件,不过费用不算贵,一次付费$10就能终身使用;或者如果在用Setapp套餐,也可以直接免费用。

QuitAll:我的人生态度,老Macbook的回春之路

QuiAll for Mac

不必重启,一键回春

前往官网下载

不过我在想,会不会腾讯柠檬清理、CleanMyMac之类的也能达到同样效果呢?

Bunny Storage:便宜稳定免备案的图床,现在更好用了!

2023年8月10日 12:00
Bunny Storage:便宜稳定免备案的图床,现在更好用了!

对一个希望低成本运营的网站来说,由于其本身服务器储存空间十分紧缺,找到一个便宜稳定的图床服务非常重要。

具体到平替生活这个网站,对图床的要求除了便宜,还得满足无需备案、国内访问速度好的要求,这就把一大批国内头部云服务商筛掉了。

Bunny Storage:便宜稳定免备案的图床,现在更好用了!

在之前捣鼓网站CDN加速的时候,我发现了Bunny.Net这个网站,主要提供网站加速服务。Bunny的产品不仅价格便宜,也有香港等亚洲节点,支持国内网络环境访问。

虽然CDN产品由于和Ghost博客系统有各种我搞不定的兼容问题不得不放弃,但是发现他同时提供超高性价比的静态储存服务,很值得探索一下。

计费标准

  • 免费试用: 新账号注册即获得1000GB流量或者14天免费试用,无需信用卡
  • 资费: Bunny的静态储存服务价格取决于你所开通的储存节点,正常可以开通两个节点,保证文件可用性。这个价格还是很美丽的,不过需要注意每月最低费用是1美元。
Bunny Storage:便宜稳定免备案的图床,现在更好用了!
  • 付费方式:支持PayPal、国际信用卡和加密货币支付,每次充值至少10美元,实测绑定了国内双币卡的PayPal账户可以正常支付。

注册和开通Storage流程

使用Bunny的静态储存服务涉及到它的两个产品,一个是Storage Zone,相当于静态网盘,该服务用于储存你的文件;另一个是Pull Zone,相当于CDN加速节点,加速静态资源在全球的访问。
如果你不懂也没关系,直接按照下面的步骤来开通这两个服务即可。

1、点击进入Bunny官网,点击右上角黄色Get Start按钮注册账号。填写邮箱和密码就注册结束了,很方便。

2、验证邮箱后进入后台,首先点击左边Storage,再点击黄色按钮,开始创建静态储存空间。

Bunny Storage:便宜稳定免备案的图床,现在更好用了!

3、Storage Zone Name,只能使用数字、字母和「-」连字符

4、Storage Tier:可以选择你需要标准硬盘还是固态硬盘储存,咱们老百姓默认选择Standard就可以了

5、Main Storage Region:选择储存空间主节点,你的文件主要储存在该节点,这里我们可以选择Asia

⚠️
注意:主节点无法更改,请谨慎选择。
Bunny Storage:便宜稳定免备案的图床,现在更好用了!

6、Geo Replication地理备份:你可以通过启用Geo Replication来开启多节点备份,这里建议开启,选择一个以上的备份节点。我这里选择了US West,基本可以满足需求了。

⚠️
注意:Geo Replication节点开启后无法关闭!涉及计费金额请谨慎选择。
Bunny Storage:便宜稳定免备案的图床,现在更好用了!

7、确认费用,点击「Add Storage Zone」,储存空间就创建完成了

Bunny Storage:便宜稳定免备案的图床,现在更好用了!

8、这时还没有结束,需要链接一个Pull Zone来绑定访问域名。进入你刚刚创建好的Storage Zone后台后,点击右上角+ Connect Pull Zone按钮,再点击Add Pull Zone

Bunny Storage:便宜稳定免备案的图床,现在更好用了!

9、填入Pull Zone Name,之后将会生成一个默认的二级域名「***.b-cdn.net」来访问你的文件
10、其他不用改动,关注一下Pricing Zones,选择你需要的区域即可,避免不必要的资费,我这里选择了三个。点击Add Pull Zone按钮即可。

Bunny Storage:便宜稳定免备案的图床,现在更好用了!

11、Pull Zone创建完成后会自动跳转到该Pull Zone的域名配置页面,你可以直接使用Bunny提供的二级域名,也可以通过CNAME指向该二级域名来绑定自己的域名,Bunny会免费提供SSL证书。之后你就可以使用该页面的域名访问你储存到Storage Zone里的文件了

Bunny Storage:便宜稳定免备案的图床,现在更好用了!

至此系统后台的配置就完成了🎉

整合工作

探索过程

Bunny Storage提供了三种使用方法,你可以通过网页、FTP或者API来上传下载文件,也都支持管理和删除文件的操作。

通过网页使用:

在Storage后台可以直接进行文件批量管理的操作,上传完成网页后即可很方便的复制网址。

通过FTP使用:

这可以说是实用性最低的一个功能了。首先FTP并不是一个适合即用即走的方式,你需要打开专门的FTP工具,登录后才能上传文件,同时你还需要自己根据你绑定的域名和文件路径来自己拼出网址,或者前往网页版复制网址。其次,如果使用PicGo来接入FTP,FTP缓慢的登录、创建目录会大幅降低上传成功率。

通过API使用:

对于博客写作来说,若想要将Bunny Storage整合到自己的写作流程里,最好是可以通过各种接口接入到写作APP(比如MWeb)自带的图床管理功能里了。

但是于Bunny的API使用方式非常奇葩,API的调用地址与你上传的文件名、文件路径相关联,导致着你每一次调用的API地址都不一样,要想整合只能通过各种变量来定义出每次上传图片的API地址,这样的需求基本没有哪个图床App能适配得了。

不过万幸,我还是找到了解决办法:快捷指令。

探索结果

最后我采用的办法是使用苹果系统自带的快捷指令来实现这个功能。我根据我的实际使用需求制作了两个快捷指令,下面是详细介绍。

单文件上传快捷指令

Bunny Storage:便宜稳定免备案的图床,现在更好用了!


如果你只是简单的需要文件上传功能,这个快捷指令就能解决。你可以将这个快捷指令通过Share Sheet、快捷键等方式接入到系统中,简单点击就能上传成功,自动获得文件地址。

原理: 本快捷指令会将每一张你传入的图片重新命名,保证文件名不会重复,同时会在Bunny的储存空间里根据年月日建立文件夹来分类储存。上传成功后会自动将文件网址复制到粘贴板。

Bunny Storage:便宜稳定免备案的图床,现在更好用了!

使用方法:
1、 点击上方的下载地址将快捷指令导入你的电脑或手机
2、根据初始化问题填入配置信息 (填写指南见下文)
3、使用时,右键图片文件调出分享窗口,或者通过拖拽将图片输入到该快捷指令,快捷指令会立即执行上传操作,并在上传完成之后将图片网址拷贝到你的粘贴板。

MWeb全文上传快捷指令

Bunny Storage:便宜稳定免备案的图床,现在更好用了!


适用于Mac上MWeb文章内的图片批量上传。如果您使用其他APP来编辑文章,需要您把文章所有图片都保存在一个固定的文件夹下。

原理: 直接复制MWeb中Markdown格式文章传输给该快捷指令,快捷指令通过正则式提取出所有图片的本地地址,从而也能定义出了Bunny Storage API调用地址。上传完成后自动将图片的网络访问地址替换到原文里,拷贝到粘贴板。

这样的做法其实也是参考了MWeb自己的图床上传办法,好处是你可以保持本地写作的工作流不变,只需要在写作完成后复制粘贴一下原文给到快捷指令来执行上传即可,坏处是如果不是MWeb需要调整自己的写作流程。

Bunny Storage:便宜稳定免备案的图床,现在更好用了!

使用方法:
1、 点击上方的下载地址将快捷指令导入你的电脑或手机
2、根据初始化问题填入配置信息(填写指南见下文)
3、需要上传图片时,直接复制带有本地图片地址的Markdown代码
4、运行该快捷指令,粘贴Markdown代码,等待上传完成,完成在线地址替换的Markdown代码就自动拷贝到你的粘贴板了

快捷指令导入指南

为了方便各位用家免于编辑复杂的内部流程,我在两个快捷指令中都设置了导入问题,在导入快捷指令时直接根据提示填入相关信息即可开始使用。下面是各个导入问题的填写指南。

打开Storage Zone的FTP & API Access页面,根据下图提示复制填写:

Bunny Storage:便宜稳定免备案的图床,现在更好用了!
  1. 请填写你的Bunny Storage Zone名称:填入上图①;
  2. 请输入API节点:填入上图②;
  3. 请填写 Bunny Storage Zone的访问Key:填入上图③;
  4. 请输入你的Storage Zone所绑定的网址:填写你所链接的Pull Zone的域名(见本文注册流程的第11步);
  5. 请填写允许上传的文件类型:默认不变即可;
  6. 请选择Markdown中图片地址的的上级文件夹路径:

👉🏻如果你使用MWeb作为编辑器:打开一篇插入了本地图片的MWeb文章,右键图片选择「在访达中显示图片」

Bunny Storage:便宜稳定免备案的图床,现在更好用了!

 在访达中按三次command+↑,这里的docs文件夹就是我们需要在导入问题中选择的路径

Bunny Storage:便宜稳定免备案的图床,现在更好用了!

👉🏻如果你使用其他编辑器,请自行修改快捷指令。由于快捷指令奇葩的获取文件操作方式,我这里没办法组织出清晰的语言给出指南。主要修改这两个地方:

Bunny Storage:便宜稳定免备案的图床,现在更好用了!

结语

有关Bunny的静态储存服务就介绍到这里,本文的所有图片都通过上面介绍的方式托管到了Bunny,如果你有兴趣尝试可点击下面的按钮注册,获得14天免费使用:

Android源码分析: 使用场景获取ContentProvider分析

2024年8月27日 10:22

之前已经分析过在应用启动的时候安装ContentProvider的流程了,现在我们再从使用者的角度看看是怎样去拿到ContentProvider的。

在使用ContentProvider的时候,我们通常会使用Context拿到ContentResolver,然后在执行CURD的操作,比如我们要查询手机中的联系人,通常会这样做:

1
2
3
4
5
6
7
8
String[] projection = new String[]    {
 Profile._ID,       
 Profile.DISPLAY_NAME_PRIMARY,       
 Profile.LOOKUP_KEY,       
 Profile.PHOTO_THUMBNAIL_URI    };
Cursor profileCursor = getContentResolver().query(
Profile.CONTENT_URI,
projection , null, null,null);

这里先重点分析一下拿到ContentProvider的过程。首先来看看这个ContentResolver是什么东西。通过源码我们可以看到它是一个抽象类,实现了ContentInterface接口,ContentInterface中则定义了CRUD的相关方法。我们可以在ContextImpl中找到getContentResolver(),通过源码我们知道,实际上我们拿到的是ApplicationContentResolver对象。

这里看完,我们可以继续看query方法,实现在ContentResolver类当中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
IContentProvider unstableProvider = acquireUnstableProvider(uri);
IContentProvider stableProvider = null;
Cursor qCursor = null;
try {
 try {
 qCursor = unstableProvider.query(mContext.getAttributionSource(), uri, projection,
 queryArgs, remoteCancellationSignal);
 } catch (DeadObjectException e) {
 unstableProviderDied(unstableProvider);
 stableProvider = acquireProvider(uri);
 if (stableProvider == null) {
 return null;
 }
 qCursor = stableProvider.query(mContext.getAttributionSource(), uri, projection,
 queryArgs, remoteCancellationSignal);
 }
 qCursor.getCount();
 final IContentProvider provider = (stableProvider != null) ? stableProvider
 : acquireProvider(uri);
 final CursorWrapperInner wrapper = new CursorWrapperInner(qCursor, provider);
 stableProvider = null;
 qCursor = null;
 return wrapper;
} catch (RemoteException e) {
 return null;
} finally {
 if (qCursor != null) {
 qCursor.close();
 }
 if (unstableProvider != null) {
 releaseUnstableProvider(unstableProvider);
 }
 if (stableProvider != null) {
 releaseProvider(stableProvider);
 }
}

上面的代码看起来还是比较简单的,首先是是去调用acquireUnstableProvider拿到unstableProvider,通过它去取数据,如果拿不到再去调用acquireProviderstableProvider,最后把stableProvider和数据使用CursorWrappInner包装返回给调用者,在finally中把cursor关掉,把provider给释放掉。

我们先来看看拿stableProvider的逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public final IContentProvider acquireUnstableProvider(Uri uri) {
 if (!SCHEME_CONTENT.equals(uri.getScheme())) {
 return null;
 }
 String auth = uri.getAuthority();
 if (auth != null) {
 return acquireUnstableProvider(mContext, uri.getAuthority());
 }
 return null;
}

简单说一下,上面首先会判断我们的URL是否为content:开头,因为这是ContentProvider的scheme。之后会到url中拿到authority, autority包括这几个部分:[userinfo@]host[:port] 。最后通过authority去调用ApplicationContentResolver中的同名方法。

1
2
3
4
5
protected IContentProvider acquireUnstableProvider(Context c, String auth) {
 return mMainThread.acquireProvider(c,
 ContentProvider.getAuthorityWithoutUserId(auth),
 resolveUserIdFromAuthority(auth), false);
}

上面的方法会从我们的authority分别拿出userId和host,当然userId有可能是不传的,就会默认使用当前用户。我们继续去看ActivityThread.acquireProvider()代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public final IContentProvider acquireProvider(
 Context c, String auth, int userId, boolean stable) {
 final IContentProvider provider = acquireExistingProvider(c, auth, userId, stable);
 if (provider != null) {
 return provider;
 }
 ContentProviderHolder holder = null;
 final ProviderKey key = getGetProviderKey(auth, userId);
 try {
 synchronized (key) {
 holder = ActivityManager.getService().getContentProvider( getApplicationThread(), c.getOpPackageName(), auth, userId, stable);
 if (holder != null && holder.provider == null && !holder.mLocal) {
 synchronized (key.mLock) {
 if(key.mHolder != null) {
 } else {
 key.mLock.wait(ContentResolver.CONTENT_PROVIDER_READY_TIMEOUT_MILLIS)
 }
 holder = key.mHolder;
 }
 }
 }
 } finally {
 synchronized (key.mLock) {
 key.mHolder = null;
 }
 }
 holder = installProvider(c, holder, holder.info, true, holder.noReleaseNeeded, stable);
 return holder.provider;
}

这里我们有一个参数stable,因此我们前面获取stableProviderunstableProvider都会走到这个方法里面来。 第3行代码,我们首先会到已存在的Provider列表中去拿,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public final IContentProvider acquireExistingProvider(
 Context c, String auth, int userId, boolean stable) {
 synchronized (mProviderMap) {
 final ProviderKey key = new ProviderKey(auth, userId);
 final ProviderClientRecord pr = mProviderMap.get(key);
 if (pr == null) {
 return null;
 }

 IContentProvider provider = pr.mProvider;
 IBinder jBinder = provider.asBinder();
 if (!jBinder.isBinderAlive()) {
 //处理Binder不存活的情况
 handleUnstableProviderDiedLocked(jBinder, true);
 return null;
 }

 ProviderRefCount prc = mProviderRefCountMap.get(jBinder);
 if (prc != null) {
 incProviderRefLocked(prc, stable); //增加引用计数
 }
 return provider;
 }
}

可以看到此处为通过auth构建出来的key到mProviderMap中查找ProviderClientRecord,而这个就是我们之前分析安装Provider时候所创建并且放置到这个map中去的。后面会检查Binder是否仍然存活,并返回。 在这里我们需要注意一点,如果安装我们之前分析安装的流程,我们在自己的app里面拿自己的ContentProvider这里是肯定可以拿到的,但是如果是其他的应用提供的ContentProvider这里很显然是拿不到的。因此我们需要继续回到acquireProvider方法去看其他部分的代码。

在第11行中,我们会到AMS中去获取ContentProviderHolder,如果拿到了远端的holder,但是我们本地的ProviderKey中的holder为空,说明我们本地还没有安装这个ContentProvider,需要等待,也就是执行第16行代码进入等待状态。而这个地方的解除等待在ContentProviderHelper类的publishContentProviders方法中,可以去之前分析安装过程的文章最后一部分查看。

而拿到holder之后,最后又去执行了一次installProvider方法,这里的安装跟我们之前的启动App安装是有一些不同的,我们放到后面再来分析。

然而前面的去AMS拿ContentProviderHolder代码我们还没有看,具体代码也仍然在ContentProviderHelper中,现在去看一下它的getContentProviderImpl()方法,内容比较长,先一点一点的贴代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//ContentProviderHelper.java getContentProviderImpl
synchronized (mService) {
 ProcessRecord r = null;
 if (caller != null) {
 r = mService.getRecordForAppLOSP(caller);
 }

 UserManagerService userManagerService = UserManagerService.getInstance();
 if (!isAuthorityRedirectedForCloneProfile(name)
 || !userManagerService.isMediaSharedWithParent(userId)) { //。mediastore需要特殊判断,这里会把那些情况给过滤掉
 cpr = mProviderMap.getProviderByName(name, userId);
 }
 ...

 ProcessRecord dyingProc = null;
 if (cpr != null && cpr.proc != null) {
 providerRunning = !cpr.proc.isKilled(); //检查ContentProvider目标进程是否被杀掉
 if (cpr.proc.isKilled() && cpr.proc.isKilledByAm()) {
 dyingProc = cpr.proc; //如果被杀了或者正在被杀就记录
 }
 }

 if (providerRunning) {
 cpi = cpr.info;
 if (r != null && cpr.canRunHere(r)) {
 checkAssociationAndPermissionLocked(r, cpi, callingUid, userId, checkCrossUser,
 cpr.name.flattenToShortString(), startTime);
 ContentProviderHolder holder = cpr.newHolder(null, true);
 holder.provider = null;
 return holder;
 }
 //PLACEHOLDER1
 }
 //PLACEHOLDER2
}

以上的代码是我们会遇到的第一种情况,首先去拿到进程ProcessRecord,之后根据Provider的authority name和userId到ProviderMap中拿已有的ContentProviderRecord。拿到之后首先检查ContentProvider提供方的进程是否正在运行中,如果在运行中,并且canRunHere检查为true, 就会检查是否有权限来执行,有权限就会创建一个ContentProviderHolder传递出去。 canRunHere所做的判断代码如下:

1
2
3
4
public boolean canRunHere(ProcessRecord app) {
 return (info.multiprocess || info.processName.equals(app.processName))
 && uid == app.info.uid;
}

解释下就是首先判断Provider是否支持多个进程中运行,也就是在Manifest为provider配置了multiprocess=true,另外检查Provider所在进程和当前调用是否为同一个进程,这两者条件满足一个就可以。同时还要满足当前进程的UID和Provider的进程UID相同,这个在两者为同一个应用,或者两者共享签名,或共享UID的情况下满足。这种情况下就可以直接使用ContentProvider。这种情况会创建新的ContentProviderHolder传递到App进程,其中会携带ContentProviderRecord过去。此时我们看到的传到App进程的ContentProviderConnection也是为空,至于这个对象的用处是什么我们后面会分析。同时还会把Holder的成员provider设置为空,这个有什么用呢,可以后面再看installProvider方法。

在这里还有一个检查权限和是否可以联合运行的方法checkAssociationAndPermissionLocked,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
if ((msg = checkContentProviderAssociation(callingApp, callingUid, cpi)) != null) {
 throw new SecurityException("Content provider lookup " + cprName
 + " failed: association not allowed with package " + msg);
}

if ((msg = checkContentProviderPermission(
 cpi, Binder.getCallingPid(), Binder.getCallingUid(), userId, checkUser,
 callingApp != null ? callingApp.toString() : null))
 != null) {
 throw new SecurityException(msg);
}

里面又分别调用了两个方法,第一个用于检查两个进程是否可以联合使用,默认是允许的,除非是系统内置应用或者预装应用会有比较严格的检查,我们这里不必关注。可以去看一下权限检查,这个比较重要:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
private String checkContentProviderPermission(ProviderInfo cpi, int callingPid, int callingUid,
 int userId, boolean checkUser, String appName) {
 boolean checkedGrants = false;
 if (checkUser) { //对于普通应用这个值传过来的为true
 int tmpTargetUserId = mService.mUserController.unsafeConvertIncomingUser(userId);
 if (tmpTargetUserId != UserHandle.getUserId(callingUid)) {
 //检查是否有临时授权,这个一般是在Manifest中添加<grant-uri-permission>或者android:grantUriPermissions
 if (mService.mUgmInternal.checkAuthorityGrants(
 callingUid, cpi, tmpTargetUserId, checkUser)) {
 return null; //检查通过直接返回成功
 }
 checkedGrants = true;
 }
 userId = mService.mUserController.handleIncomingUser(callingPid, callingUid, userId,
 false, ActivityManagerInternal.ALLOW_NON_FULL,
 "checkContentProviderPermissionLocked " + cpi.authority, null);
 if (userId != tmpTargetuserId) {
 checkGrants = false;
 }
 }
 if (ActivityManagerService.checkComponentPermission(cpi.readPermission,
 callingPid, callingUid, cpi.applicationInfo.uid, cpi.exported)
 == PackageManager.PERMISSION_GRANTED) { //检查读权限,授权过返回
 return null;
 }
 if (ActivityManagerService.checkComponentPermission(cpi.writePermission,
 callingPid, callingUid, cpi.applicationInfo.uid, cpi.exported)
 == PackageManager.PERMISSION_GRANTED) { //写权限检查,授权过则返回成功
 return null;
 }
 PathPermission[] pps = cpi.pathPermissions;
if (pps != null) {
 int i = pps.length;
 while (i > 0) {
 i--;
 PathPermission pp = pps[i];
 String pprperm = pp.getReadPermission();
 if (pprperm != null && ActivityManagerService.checkComponentPermission(pprperm,
 callingPid, callingUid, cpi.applicationInfo.uid, cpi.exported)
 == PackageManager.PERMISSION_GRANTED) {
 return null;
 }
 String ppwperm = pp.getWritePermission();
 if (ppwperm != null && ActivityManagerService.checkComponentPermission(ppwperm,
 callingPid, callingUid, cpi.applicationInfo.uid, cpi.exported)
 == PackageManager.PERMISSION_GRANTED) {
 return null;
 }
 }
}

}

关于权限,前面的代码我已经加了相关的注释,我们可以对比官方文档,其中共检查了四种权限,分别是临时授权,路径授权,单独的读写授权和单一读写程序级别的授权。关于权限检查的更多内容,这里我们也先略过。此时我们可以继续回来继续分析getContentProviderImpl方法。我们继续看上面留的PLACEHOLDER 1处的代码:

1
2
3
4
5
6
checkAssociationAndPermissionLocked(r, cpi, callingUid, userId, checkCrossUser,
 cpr.name.flattenToShortString(), startTime);
conn = incProviderCountLocked(r, cpr, token, callingUid, callingPackage,
 callingTag, stable, true, startTime, mService.mProcessList,
 expectedUserId);

其中还有一些关于OOM设置的代码这里先跳过了,上面主要的代码也是检查权限以及这个incProviderCountLocked方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private ContentProviderConnection incProviderCountLocked(ProcessRecord r,
 final ContentProviderRecord cpr, IBinder externalProcessToken, int callingUid,
 String callingPackage, String callingTag, boolean stable, boolean updateLru,
 long startTime, ProcessList processList, @UserIdInt int expectedUserId) {
 final ProcessProviderRecord pr = r.mProviders;
 for (int i = 0, size = pr.numberOfProviderConnections(); i < size; i++) {
 ContentProviderConnection conn = pr.getProviderConnectionAt(i);
 if (conn.provider == cpr) {
 conn.incrementCount(stable);
 return conn;
 }
 }

 ContentProviderConnection conn = new ContentProviderConnection(cpr, r, callingPackage,
 expectedUserId);
 conn.startAssociationIfNeeded();
 conn.initializeCount(stable);
 cpr.connections.add(conn);
 if (cpr.proc != null) {
 cpr.proc.mProfile.addHostingComponentType(HOSTING_COMPONENT_TYPE_PROVIDER);
 }
 pr.addProviderConnection(conn);
 mService.startAssociationLocked(r.uid, r.processName, r.mState.getCurProcState(),
 cpr.uid, cpr.appInfo.longVersionCode, cpr.name, cpr.info.processName);
 if (updateLru && cpr.proc != null
 && r.mState.getSetAdj() <= ProcessList.PERCEPTIBLE_LOW_APP_ADJ) {
 processList.updateLruProcessLocked(cpr.proc, false, null);
 }
 return conn;
}

这里有不少关于Association相关的代码,而我们的应用一般不会走到这里。我们只需要关注其中创建Connection以及为他创建引用计数。关于它的计数,我们放到最好再看一下。

PLACEHOLDER 2处,首先处理的就是provider为运行的情况,这种情况就会回到Provider的进程去安装ContentProvider,这部分代码我们之前已经分析过了,这里略过。而我们是在使用者进程调用的此处的caller也不为空,再往后,则应该是如下的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
mService.grantImplicitAccess(userId, null, callingUid,
 UserHandle.getAppId(cpi.applicationInfo.uid));

if (caller != null) {
 synchronized (cpr) {
 if (cpr.provider == null) {
 if (cpr.launchingApp == null) {
 return null;
 }

 if (conn != null) {
 conn.waiting = true;
 }
 }
 }
 return cpr.newHolder(conn, false);
}

这里可以看到,就是先给调用的uid授权,设置wait 为true,创建一个ContentProviderHolder返回。这里是带着ContentProviderConnectionIContentProvider的。

代码讲解的部分只介绍了我们认为caller不为空的情况,实际上是更加复杂的,这里就把其中的完整流程流程图放在这里,如有需要可参考流程图以及之前的App启动时候的ContentProvider安装一起看。

---
title: getContentProviderImpl流程
---
flowchart TD
A(getContentProviderImpl) --> B(mProviderMap.getProviderByName)
B --> C(providerRunning = !cpr.proc.isKilled)
C --> D{Check providerRunning}
D --> |providerRunning == true|E{cpr.canRunHere}
E --> |No| I{CheckPermission}
E --> |Yes|F{ChecPermission}
F --> |Pass|G((Return local Holder))
F --> |Not Pass|H(Throw Exception)
I --> |Pass|J(incProviderCountLocked)
I --> |Not Pass|H
D --> |No|K(PMS.resolveContentProvider)
K --> L{CheckPermission}
L --> |Not Pass|H
L --> |Pass|M(Generate CPRecord)
M --> A1{cpr.canRunHere}
A1 --> |true|A2((Return local Holder))
A1 --> |False| A3{Process Live}
A3 --> |Process is live|A4(Install Provider)
A3 --> |Not Start Or Die| A5(Start Process)
A4 --> A6(incProviderCountLocked)
A5 --> A6
A6 --> B1(AMS.grantImplictAccess)
J --> B1
B1 --> B2{From customer Call}
B2 --> |Yes|B3((Return Remote Holder))
B2 --> |No|B4{cpr.provider==null}
B4 --> |Yes|B5((cpr.wait))
B5 --> B4
B4 --> |No|B6((Return Remote Holder))

看了这么多,我们就可以继续回去看App进程的代码了。在App进程就是执行我们前面说的installProvider过程。 我们可以继续分析query的过程,看代码我们知道调用的是IContentProvider的query方法,对于同UID的进程,IContentProvider为我们在instalProvider创建的本地的ContentProvider中的mTransport而其他的则是AMS调用带过来的IcontentProvider远端接口,我们这里以非本进程的情况来分析,它的获取是如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
//android.content.ContentProviderNative.java
static public IContentProvider asInterface(IBinder obj)
{
 if (obj == null) {
 return null;
 }
 IContentProvider in =
 (IContentProvider)obj.queryLocalInterface(descriptor);
 if (in != null) {
 return in;
 }

 return new ContentProviderProxy(obj);
}

也就是说,如果是相同的UID的进程拿到的为Transport对象,如果是其他的则拿到的是ContentProviderProxy对象。

前面我们还有关于ContentProviderConnection还有很多东西没有介绍,这里继续看一下。首先是incProviderCountLocked方法中所调用的conn.incrementCount(stable)。在我看代码的过程中stable这个变量唯有这里使用了,我们继续看它的源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public int incrementCount(boolean stable) {
 synchronized (mLock) {
 if (stable) {
 mStableCount++;
 mNumStableIncs++;
 } else {
 mUnstableCount++;
 mNumUnstableIncs++;
 }
 return mStableCount + mUnstableCount;
 }
}

可以看到这个类主要记录了Stable和UnStable的调用次数,实际上AMS这一端stable和unstable似乎除了计数之外没有什么区别。但是在客户端installProvider的时候却是有区别的。我们之前分析的启动时候安装的情况stable都是为true,我们可以看看ActivityThread.installProvider如下的代码:

1
2
3
4
5
6
7
8
if (noReleaseNeeded) {
 prc = new ProviderRefCount(holder, client, 1000, 1000);
} else {
 prc = stable
 ? new ProviderRefCount(holder, client, 1, 0)
 : new ProviderRefCount(holder, client, 0, 1);
}
mProviderRefCountMap.put(jBinder, prc);

ProviderRefCount用于记录Provider的引用计数,其中用stableCount和unstableCount来计数,当我们不需要释放Provider的时候,两个数字都设置为了1000,当我们是stable的时候只设置stable数为1,unstable数量为0,当为unstable的时候也同理。之前我们是有看到对于已经存在的provider是通过incProviderRefLocked来增加起计数的。那我们有了增加计数,那么使用完之后也应该需要减少计数。在query的finally代码块中有如下代码:

1
2
3
4
5
6
if (unstableProvider != null) {
 releaseUnstableProvider(unstableProvider);
}
if (stableProvider != null) {
 releaseProvider(stableProvider);
}

他们最终调用的为ActivityThread.releaseProvider方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public final boolean releaseProvider(IContentProvider provider, boolean stable) {
 if (provider == null) {
 return false;
 }

 IBinder jBinder = provider.asBinder();
 synchronized (mProviderMap) {
 ProviderRefCount prc = mProviderRefCountMap.get(jBinder);
 if (prc == null) {
 // The provider has no ref count, no release is needed. 
 return false;
 }

 boolean lastRef = false;
 if (stable) {
 if (prc.stableCount == 0) {
 return false;
 }
 prc.stableCount -= 1;
 if (prc.stableCount == 0) {
 lastRef = prc.unstableCount == 0;
 try {

 ActivityManager.getService().refContentProvider(
 prc.holder.connection, -1, lastRef ? 1 : 0);
 } catch (RemoteException e) {
 //do nothing content provider object is dead any way 
 }
 }
 } else {
 if (prc.unstableCount == 0) {
 return false;
 }
 prc.unstableCount -= 1;
 if (prc.unstableCount == 0) {
 lastRef = prc.stableCount == 0;
 if (!lastRef) {
 try {

 ActivityManager.getService().refContentProvider(
 prc.holder.connection, 0, -1);
 } catch (RemoteException e) {
 //do nothing content provider object is dead any way 
 }
 }
 }
 }

 return true;
 }
}

代码主要分了两个分支,分别对stable和unstable的情况进行处理,他们都是先把本地对应的ProviderRefCount中的数字减一,但是调用AMS.refContentProvider却不一样,stable count减为0的时候会直接调用,而unstable为0的时候要stableCount不为0才会调用。传递的参数也有区别,代码很简单就不详解了。直接去看ContentProviderHelperrefContentProvider方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
boolean refContentProvider(IBinder connection, int stable, int unstable) {
 ContentProviderConnection conn;
 try {
 conn = (ContentProviderConnection) connection;
 } catch (ClassCastException e) {

 }
 if (conn == null) {
 throw new NullPointerException("connection is null");
 }

 try {
 conn.adjustCounts(stable, unstable);
 return !conn.dead;
 } finally {

 }
}

这里的代码其实比较简单,就是调用ContentProviderConnectionadjustCounts,这个方法的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void adjustCounts(int stableIncrement, int unstableIncrement) {
 synchronized (mLock) {
 if (stableIncrement > 0) {
 mNumStableIncs += stableIncrement;
 }
 final int stable = mStableCount + stableIncrement;
 if (stable < 0) {
 throw new IllegalStateException("stableCount < 0: " + stable);
 }
 if (unstableIncrement > 0) {
 mNumUnstableIncs += unstableIncrement;
 }
 final int unstable = mUnstableCount + unstableIncrement;
 if (unstable < 0) {
 throw new IllegalStateException("unstableCount < 0: " + unstable);
 }
 if ((stable + unstable) <= 0) {
 throw new IllegalStateException("ref counts can't go to zero here: stable="
 + stable + " unstable=" + unstable);
 }
 mStableCount = stable;
 mUnstableCount = unstable;
 }
}

这里就是来根据传过来的参数来调整stableCountunstableCount,也就完成了这几个count的变化。也就是完成了AMS端的减少计数。

到此位置,我们也就拿到了IContentProvider,也就可以使用它提供的CRUD方法,进行数据的增删改查了。至于具体是如何查询数据,如何做到数据的跨进程共享,如何绕过Binder传输限制1MB实现跨进程传输数据,限于篇幅下次再来分析。

看完评论一下吧

❌
❌