普通视图

Docker 部署一个精美的画廊 —— ChronoFrame

2025年12月8日 22:30

如果你热爱摄影,又有一定折腾能力,那么或许也希望拥有一座属于自己的精致在线画廊,用来与他人分享作品。近期在一位网友的推荐下,我了解到 ChronoFrame —— 一款开源、自托管的个人云相册解决方案。借助 Docker,即便是新手也能够相对轻松地部署属于自己的独立画廊网站。

效果展示

我的网站(不打算长期保留):https://gallery.veryjack.com

官方示例:https://lens.bh8.ga

优势

  • 自部署,数据属于你自己
  • 优雅的浏览体验
  • 多格式兼容,甚至支持 Live photo

需要什么

  • 准备一台 VPS(NAS 也可以)
  • 一个域名(可选,但建议有)
  • 若干照片

项目地址

https://github.com/HoshinoSuzumi/chronoframe/blob/main/README_zh.md

开始部署

我们采用 docker compose 的方式进行部署,这样最简单,也方便后期管理。一共需要配置两个 docker-compose.yml.env 文件。

你可以通过 ssh 连接上服务器,然后 cd /path/to/chronofram_folder 进入chronoframe 项目文件夹下,直接创建,具体步骤不再赘述,会命令行的用户自然知道怎么做。

  1. 对于小白,大概率会部署 1panel 或者 宝塔面板,我建议可以直接通过面板,进入项目目录下,直接创建并配置这两个文件即可。比如我用的 1panel,文件 下直接进入 /root/data/docker/chronoframe 文件夹下,然后点击 创建 按钮,新建两个文件,命名分别为 docker-compose.yml.env
  1. docker-compose.yml 文件中填入以下信息
services:
  chronoframe:
    image: ghcr.io/hoshinosuzumi/chronoframe:latest
    container_name: chronoframe
    restart: unless-stopped
    ports:
      - '3000:3000'
    volumes:
      - ./data:/app/data
    env_file:
      - .env

其中 '3000:3000'第一个 3000 可以改为任意你喜欢的且没被占用的端口号,配置好后,确保防火墙有放行该端口号。

  1. .env 文件中填入以下信息
# 管理员邮箱(必须,默认 admin@chronoframe.com)
CFRAME_ADMIN_EMAIL=example@mail.com
# 管理员用户名(可选,默认 ChronoFrame)
CFRAME_ADMIN_NAME=Your_name
# 管理员密码(可选,默认 CF1234@!)
CFRAME_ADMIN_PASSWORD=input_your_password

# 站点信息(均可选)
NUXT_PUBLIC_APP_TITLE=
NUXT_PUBLIC_APP_SLOGAN=
NUXT_PUBLIC_APP_AUTHOR=
NUXT_PUBLIC_APP_AVATAR_URL=

# 地图提供器 (maplibre/mapbox)
NUXT_PUBLIC_MAP_PROVIDER=maplibre
# 使用 MapLibre 需要 MapTiler 访问令牌
NUXT_PUBLIC_MAP_MAPLIBRE_TOKEN=
# 使用 Mapbox 需要 Mapbox 访问令牌
NUXT_PUBLIC_MAPBOX_ACCESS_TOKEN=

# Mapbox 无域名限制令牌(反向地理编码,可选)
NUXT_MAPBOX_ACCESS_TOKEN=

# 存储提供者(local、s3 或 openlist)
NUXT_STORAGE_PROVIDER=local
NUXT_PROVIDER_LOCAL_PATH=/app/data/storage

# 会话密码(必须,32 位随机字符串)
NUXT_SESSION_PASSWORD=Q7RZjHCLGiZhUj24hJYUCW1AlCJ6NngQ

其中,务必修改 NUXT_SESSION_PASSWORD 中的随机字符串。CFRAME_ADMIN_EMAILCFRAME_ADMIN_NAMECFRAME_ADMIN_PASSWORD 如果不填就是默认值,CFRAME_ADMIN_EMAIL 如果填写,需要确保是邮箱格式

如果你想将图片上传到 VPS,则可以保持 NUXT_STORAGE_PROVIDER 的配置,这样最简单,如果你想使用 s3,则可以访问官网文档自行研究(我没用过 s3)。

站点信息 等都是选填,按需填写即可。

  1. 配置好文件后,通过 ssh 连接服务器,并且 cd /path/to/chronofram_folder 进入到 docker-compose.yml 所在的文件夹下,然后运行 docker-compose up -d 即可运行该项目。 如果想要停止该项目,同样是进入到相同目录下,运行 docker-compose down 即可。 另外,项目所有的数据都会保存到 /path/to/chronofram_folder 目录下的 data 文件夹中。
  1. 部署好服务后,你就可以通过 http://ip:3000进行访问了,最好是用一个二级域名反向代理一下,便于访问,也更加安全。

上传 Live Photo

部署好服务后,只需输入账户密码即可进入后台上传照片。但是 Live photo 上传需要特别注意一下。

  1. 需要将 iPhone 通过 AirDrop 传到 Mac 上(其他方式也行)
  2. 选中 Live Photo 照片,点击左下角 分享 按钮,在顶部 选项 中,勾选最底部的 所有照片数据,这样 Mac 上接收到的就是一个文件夹,文件夹中包含一张图片和一个同名的 .mov文件。
  3. 进入 chronoframe 后台,上传照片时,需要同时上传这两个文件,这样就能在画廊看到 Live Photo 了

最后

教程非常简单,其余的功能就留给大家自行探索啦~

Docker 部署一个精美的画廊 —— ChronoFrame最先出现在Jack's Space

用一台被淘汰的 Mac mini 来做软路由

2024年1月24日 17:35

其实在之前的文章里就提到过,我一直是一个垃圾佬节俭主义者,所有的东西我会本着能用就用的原则让它发挥余热。最近手上拿到了一台已经退役的 Mac mini,它有多老呢?

截屏2024-01-16 18.25.34.png

这是一台 2014 Late 版本的 mini,双核四线程的 i5 处理器,8G 的内存,硬盘已经被升级成了 256G 的 SATA SSD。这样一台主机,现在到小黄鱼上几百块就能拿下,可能很多人的软路由都比它的纸面性能要好。而我在它发布十年后的今天,依然选择其作为软路由却是看中了它的几个独特优势。

Mac mini 的优势

对我来说,其最大最核心的优势就是 macOS,是的你没有看错,很多人拿到这类过时的苹果电脑,第一时间就是格式化掉硬盘,然后装一个 ubuntu 之类的 Linux。但如果你要装 Linux 真的没必要用苹果电脑,还要解决一堆兼容性问题。反之如果我继续沿用 macOS 则可以享受其完善的软件生态还有原生的硬件兼容性。当然,最重要的一点,我是 Surge 的用户,这样软路由的软件就不用再去重复投资了。

颜值,即使已经到 2024 年了,你要找一台能摆上台面 PC 主机依然还是很难,这代 Mac mini 的设计已经沿用了至少十年,看上去依然优雅毫不过时。我打算把它放在客厅的电视柜,这么一个每天都能看到的地方可不能放一个看着糟心的玩意。我个人比较欣赏的是它把电源集成进了机箱,不像现在很多迷你主机虽然看着很小,但还得配一个巨大的外置电源。

macOS 配置

首先我们要为这台机器装上最新的 macOS 操作系统,通过这个网址查询得知,这台 2014 年末的 Mac mini 最高只能装到 macOS 12 Monterey。选择尽量新的操作系统可以让系统更好地获得安全补丁,因此我把之前的系统抹掉,重新安装了 macOS 12。

截屏2024-01-24 15.58.48.png

为了让这台 Mac mini 胜任软路由的角色,我们还需要对其系统进行一些配置。路由器是全年 24 小时常开的服务,所以稳定性是第一要务,同时我们还要考虑一些特殊情况,比如停电后的恢复。

截屏2024-01-24 16.02.34.png

所以我们进入“设置/节能”,第一个钩一定要打上,要不然一会儿系统会自动进入睡眠模式,软路由就噶了。如果有“如果可能,使硬盘进入睡眠”这个选项(比如你外接了硬盘),把这个钩可以点掉,不需要节约这一点电量,反而有时候硬盘休眠再唤醒会有 Bug。“断电后自动启动”这个选项也要打开,这可以让你的机器在停电后再来电时自动开机,从而实现网络的自动恢复。要不然家里没人,就只能干瞪眼了。

截屏2024-01-24 16.12.46.png

再进入“用户与群组/登录选项”,把自动登录打开,选择当前的用户登录即可。Surge 运行在用户态,如果没有登录是无法启动的,所以我们要保证这台机器只要一开机就能自动进入系统,然后启动相应的服务。

截屏2024-01-24 16.17.24.png

在“软件更新”的设置里,把所有自动检查和自动更新的钩都去掉,你不希望网上着好好的,软路由自己重启去更新系统了吧。

其它设置:

  1. 建议把 WIFI 网络禁用掉,我们只需要用到有线网卡。把蓝牙也禁用掉。
  2. 建议把声音静音,Mac mini 是有自带扬声器的,你的某些操作可能会触发音效,你应该不希望自己的路由器发出莫名其妙的声音吧。
  3. 系统偏好设置 / 辅助功能 / 显示:勾选 ✅减弱动态效果
  4. 系统偏好设置 / 通用:取消 ☑️允许基于墙纸调整窗口色调

软路由/Surge 配置

在配置软路由之前,我们面临一个选择,是让这台路由成为主路由还是旁路由。我的考虑是前者虽然节省了一台设备,但系统的容错性比较低,软路由虽然我们做到尽量稳定,但它的宕机概率还是高过硬路由。如果你把它作为主路由,届时很可能会面对整体无法上网的情况。而作为旁路由就要灵活得多,这台 Mac mini 你可以随时拿掉去升级/修理,我只需要把子网络的出口网关地址改一下就行了。

Untitled-2022-06-08-1159.png

上图就是一个简单的家庭网络拓扑示意图,可以看到我们分了两个网络(黄色和绿色),一般的家庭联网设备都运行在绿色的网络(192.168.10.0/24)中。它们只与一个主路由连接(一般是一个 WIFI 路由),与上层设备之间是隔离的,不需要关心自己的流量经过了哪些设备,连上就可以用。

而对于黄色网络(192.168.1.0/24),WIFI 路由的网关设置到软路由(192.168.1.10)上就可以通过软路由上网,如果软路由出故障了,就可以把网关设置到主网关(192.168.1.1)继续使用,上层设备可以做到无感知切换。这种组网的方式比较简单可靠,也兼顾了灵活性,是一种不折腾的选择。

截屏2024-01-24 16.36.04.png

而如果你跟我一样选择 Surge,那么软路由的设置就相当简单了,只需要打开它的增强模式即可。注意两点:

  1. 不要打开 DHCP 模式,因为你只是个旁路由,DHCP 服务已经由主路由承担了,你只需要承担网络流量即可。
  2. 记得给 Mac mini 设置一个固定的 IP 地址,而且要避开主路由的 DHCP 自动分配的址段。

截屏2024-01-24 17.13.44.png

接下来就是给绿色网络的 WIFI 路由器设置出口网关了:

  1. 把它的出口(WAN)配置为固定 IP(Static IP)模式,网关地址(Gateway)设置为这台软路由的地址。
  2. 还有一个比较特殊的,你要把它的 DNS 设置为如上图所示 Effective DNS 的地址。

至此,所有的软路由配置已经完成了,如果不出意外的话所有的上层网络流量都会经由这台 Mac mini 再联向互联网。而 Surge 本身还有不少玩法,比如你可以把 Surge Ponte 打开,这样如果你在多台设备上运行了 Surge,它们之间就可以组成一个加密的虚拟网络,我一般来连 VNC 查看这台 Mac mini 的运行状态。

后话

WechatIMG1278.jpg

一图胜千言,垃圾佬的快乐谁能懂😂

一次惊心动魄的数据拯救之旅

2023年12月24日 00:20

作为一个苦逼的创业公司,我们对开发服务器硬件的态度是能省就省。于是我们利用各自垃圾闲置硬件攒出了一个堪用的集群,来供公司平时开发使用。

IMG_8779.jpg

可以看出这分属不同厂商,不同年代,不同配置的服务器及网络设备体现了深深的历史厚重感。里面最耀眼当属 4 台银光闪闪的 Mac mini,把它们加进来还是我的主意,毕竟跑跑持续集成又不是不能用。但此次事件的主角不是它们,而是下方那台贴了黄色符咒的 ThinkStation。符咒是@明城贴的,这台机器也是他攒的,作为一个资深垃圾佬当然是用不起不会用 ThinkStation 这么高端的货色,所以如你所见它只是个机箱,里面早就面目全非了。不过为了方便起见我后面都称其为 ThinkStation 了。

流年不利

周四那天首先有同事报告公司 Gitlab 打不开了,作为我们内部开发使用的主要服务,它一直作为 KVM 虚拟机跑在这台 ThinkStation 上,于是我立马 ssh 登陆上母机去。一顿猛操作发现虚拟机控制台已经进不去了,用虚拟机重启命令也毫无反应,看起来是彻底死机了。这个时候我们只好祭出大杀器,重启服务器,于是轻车熟路敲下 poweroff 然后准备等它关闭,可奇怪的是我都喝完一杯茶了这服务器还丝毫没有关闭的迹象,看起来母机也死掉了。这个时候只有最后一招,关闭电源。

长按电源键关机之前,我看了一眼符咒,心想这次只能靠你了。按住电源键几秒后,听见继电器咔哒一声脆响,然后所有闪烁的灯光都熄灭了。深吸一口气,再次按下电源键,耳边响起硬盘电机开始加速的声音,回到位置上准备如往常一样再次 ssh 连接上去。但这次等待的时间有点长,十分钟以后我依然无法连接。不仅如此,连 ping 都没法到达这台服务器。

情况有些不对,我再次重复了几次物理开关机的过程,服务器依然如一潭死水无法连接。事情大条了!于是我赶紧联系明城让他看看这台一手打造的服务器到底出了什么问题。于是再经过他的一顿猛烈操作,告诉我一个噩耗,硬盘 RAID 挂了,数据损坏导致母机内核都崩溃了😱。

WechatIMG1004.jpg

这是两块 2T 的磁盘做了 RAID 1,母机用的是 ZFS 的文件系统,按道理说安全性已经拉满了,怎么会一声不响都挂掉了?不过现在首要任务是把里面的数据弄出来,毕竟现在没地方提交代码了。虽然我们做了数据备份,但备份是按天来计的,如果能拿到实时的数据是最好不过了。

拯救数据

IMG_9706.jpg

于是拆下其中的一块硬盘,插到我自己的黑苹果上。也幸亏我在公司用的是黑苹果,还有地方给我插这块硬盘,但马上我遇到了第一个问题,怎么在 MacOS 下挂载一个 ZFS 的磁盘。幸好我发现了 OpenZFS, 而且已经有开发者做了 MacOS 的实现 OpenZFS on OSX,用 brew 安装起来也非常简单丝滑:

brew install openzfs

因为涉及到扩展安装,所以中途会跳出安全警告,记得要到 “安全性与隐私” 处忽略这个警告继续安装。安装好以后你还需要手动挂载这块硬盘,ZFS 跟一般的文件系统还有点不同,它有个 pool 池的概念,你要先使用命令

sudo zpool import [poolname]

来导入 ZFS 池,如果你不知道 pool 的名字,可以运行 sudo zpool import 来查看所有待导入的 pool。导入以后按说还有个 sudo zfs mount 的动作来挂载,不过在 MacOS 上已经自动挂载好了。

WechatIMG1024.jpg

这三个最重要的 KVM 磁盘映像文件就映入眼帘了,分别对应了三台运行在这台服务器上的虚拟机,而 Gitlab 就在这台 wuhan(武汉)上,大小有 128G 左右,最理想的情况是我直接把它拷贝出来就行了。然而

WechatIMG1031.jpg

我试了三四次,每次都在拷贝到 116G 左右的时候就遇到 IO 错误,看来问题就出在这里了。根据我的猜测这个文件应该不是完全损坏,坏掉的部分可能只是一小块,如果它对应的不是系统核心文件(这个概率很大,毕竟 128G 的文件核心只占一小部分),那这个映像完全还能使用。因此我需要一个更底层的拷贝工具来跳过错误部分,它就是 dd

dd if=source_file of=target_file bs=1M conv=noerror,sync

这里 bs (block size) 我设置了 1M,也就是每个区块大小为 1M,遇到错误就跳过它。执行这个命令后,果然在读取到 116G 左右的时候报了两个错误,跳过之后顺利拷贝完成了。现在我在自己的 MacOS 上得到一个未知的 KVM 磁盘映像,其实工作按理说已经告一段落了。但我看了自己性能过剩的黑苹果主机,冒出一个想法,能不能让它暂时运行在我的电脑上,起码先让大家恢复代码提交。

在 Mac 上运行 QEMU

qcow2 是常用的虚拟机镜像,在 MacOS 上我们可以用 QEMU 来运行它,通常情况下我们可以直接用 brew install qemu 来安装它。但现在有个更好的选择:UTM,这是一个 MacOS 上开源的 QEMU 的图形界面实现。我选择它的原因是图形界面能帮助我省掉不少时间,毕竟 QEMU 的参数非常复杂,如果有人帮你做好了适配可以省掉不少时间。

但是 UTM 现在有个问题,它不能直接用一个 qcow2 文件来作为磁盘启动,创建虚拟机的时候必须选择一个启动镜像,并且新建一块虚拟磁盘。为了解决这个问题,你可以在创建虚拟机的时候随便选一个文件作为启动光盘,创建完以后编辑虚拟机,把这块镜像删掉即可。

现在还需要解决如何让它使用我这块已经拷贝好的 qcow2 文件作为硬盘,千万不要使用它的导入磁盘映像功能,因为这只会让它把镜像拷贝过去,然后转换成自己的格式。经过我的摸索可以这样做,首先把当前磁盘映像的 Interface 修改成 virtio,因为这也是 KVM 创建映像时的类型,既然我们要直接读取它就必须保持一致。改完后记得点保存,因为后面要直接操作文件。

截屏2023-12-23 18.46.35.png

然后我们要把这块硬盘替换成拷贝好的 qcow2 文件,首先找到 UTM 为这个虚拟机自动创建好的磁盘映像,它位于目录

~/Library/Containers/com.utmapp.UTM/Data/Documents/{你的虚拟机名称}.utm/Images/

这里有个已经存在的 qcow2 文件,给它重命名一下,然后用 ln -s 命令把原来系统的 qcow2 文件软链接过来替换已经存在的文件,这一步的目的就是“骗” UTM,让它以为硬盘是自己那块,其实已经被我们给换掉了。

然后还得对网络做一番修改,我专门接了一块 USB 网卡来做桥接,所以把这里的网络模式改为“桥接”,选择对应的网卡,然后 "Emulated Network Card" 这一项选择 "virtio-net-pci",这也是为了和 KVM 的模式兼容。

截屏2023-12-23 18.46.58.png

选择好以后保存,咱们就可以大喊原神启动。

截屏2023-12-22 21.02.19.png

牛X!启动成功,可以看到操作系统已经检测到了文件错误,尝试修复成功之后终于进入到了熟悉的登录界面。正常进入系统之后就是一些常规的操作了,总之经过一番折腾我把运行在服务器上的 Gitlab 成功迁移至本地运行,性能居然还不错。

1393545561.png

结论

  1. 任何系统都有可能出故障,重要的数据最好做异地多活。这次是硬件故障,万一哪天硬盘直接被拿走就废了。
  2. 磁盘故障不一定代表文件不可用,越大的文件其中不重要的部分占比就越大,因此还是有很大几率抢救成功的。
  3. 越开放的技术,工具和资料也越多,其实健壮性也越好。比如 KVM 用的 qcow2,我拷贝到 MacOS 上照样可以通过 QEMU 启动它。
  4. 符咒没起作用,封建迷信害死人🤦

现在做个人博客的最低成本是多少

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可能会被墙,先这样吧。

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/

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之类的也能达到同样效果呢?

Google Play 应用上架二三事

2025年7月13日 21:05

首次上架应用到google play还是2015年的时候,那个时候上架应用限制比较少,注册个账号信用卡付个款就行了。自己之前有个账号,但是闲置许久,加上自己的一些骚操作账号被Google给永久禁用了,并且还给我发了个邮件告知不要再尝试注册新的账号了。最近自己的新应用想要上架,于是又重新注册了个个人账号。同时公司的产品也要上架Google play,前前后后经过了小半年的折腾才终于搞定,将应用上架,这里就来说道说道。

账号注册

早期的时候,无论是个人账号还是公司账号注册都不需要实名验证的,因此只需要填一下联系信息使用信用卡付款25美元就可以直接注册成功了。但是从2023年开始,无论是已有账号还是新注册的账号都需要进行验证。对于个人账号,只需要填写一份付款资料,并且验证身份,身份证,护照信息都可以,这个信息需要与付款资料中所填写的需要一样,并不要求付款信用卡的账单地址和姓名与这个相同。

对于公司账号的注册,则是和苹果一样要求提供邓白氏码(D-U-N-S Number),输入邓白氏码之后会自动获取到公司的名称地址信息生成付款资料。和个人账号一样,付款的信用卡也没啥要求。在验证的时候,则是需要提供公司的政府签发的文件,比如国内的营业执照,具体可以查看Google play 官方文档。公司的验证也还需要验证一位开发发者的信息,这里的要求跟个人差不多,身份证,护照等都可以,而且不需要提供在这家公司任职的任何证明文件,公司注册地和个人不在同一个国家也没关系(这一点,苹果的开发者注册是要求提供个人在公司任职的证明文件或者名片之类的东西的)。

如果所有的这些证明文件都能够顺利的提供,并且邮箱验证和电话验证都没问题,那么账号注册是很容易的。最后有一点需要补充,付款的信用卡是不能使用银联卡的,需要visa或者mastercard。总体来说,比苹果开发者账号要容易,就上面说的我的个人账号被封之后,使用家人的信息又重新顺利注册了一个新账号。而公司的账号,在获取法务同时完成公司DUNS Number的申请之后,也都顺利的完成注册了。但这些完成之后,也还是只是完成了万里长征的第一步。

个人应用上架

以前个人应用在google play上架是很容易的,而在2023年11月,google 出了一个新政策,对于在23年11月13日之后注册的新账号,发布应用时必须满足特定的测试要求才能正式发布。具体要求是,正式发布之前,需要在google play上进行封闭测试,需要至少12名测试人员测试至少14天持续参与测试。这个对于个人开发者来说,还是不太容易达成的,这至少12名测试人员,是需要开发者将他们的Google 邮箱输入到google play开发后台,他们接受并且根据开发者提供的链接进入下载安装的。至于连续14天的持续参与测试,这个不太清楚Google 是如何统计的,封闭时间肯定是要保证14天以上,但是如何保证每天都至少有12人参与这个不确定是否强制要求。

我的应用在开发完成之后,通过在小众软件和Linux.do社区征集到了一定的热心网友参与了测试,从而顺利完成了封闭测试,对于工具类的软件这是个不错的方式。对于这一点,虽然加大了个人上架应用的难度,但是我想也是可以提高上架应用质量,毕竟对于个人开发开发者来说,测试相比于公司开发的应用来说会更加薄弱。

公司应用上架

公司开发者应用的上架,没有上面对于个人开发者的限制,但是我们在上架的时候遇到了更多的问题。因为我们所在的是金融行业,在上架的时候会更加的敏感,因此也更需要小心一点。

我们在第一次上架的时候,填完了所有的信息,上传了应用,提交审核之后,谷歌以我们需要登录为由审核不通过。提交了登录需要的信息之后,等了超过十天,结果直接账号被禁用了,原因是高风险,发邮件和申述都没用,并且不告知具体的原因。搞得我们很受伤,不知道该怎么办。商量之后决定让公司注册新的实体再重新注册开发者账号。

另一方面,想到公司以前的应用可能还有老的开发者账号,或许可以用,找回了老账号的邮箱,使用新的公司主体信息进行了验证,之后顺利的提交了应用。这一次为了稳妥起见,我们先提交了封闭测试和开发测试,都通过了之后才提交的正式发布,所有信息都填写准确,google 也很快的通过了审核。

一些经验

虽然说上架Google play越来越严格,但是相比与国内来说还是容易很多的,国内上架对于个人开发者极度的不友好,并且备案,软著,哪个都不是好搞的。

上架Google play我认为第一条原则就是诚实,填写资料要真实,不仅仅是开发者信息如此,同时应用使用到的权限,收集的用户资料,等等都要如实填写,不可挂羊头卖狗肉。也要避免给审核人员看到的只有某一个功能,实际应用内有很多的功能会在审核后对用户开放。如果应用需要登录,最后要提供账号密码给google 的审核人员,对于非账号登录的,如加密货币钱包应用可以提供助记词或者操作指导的视频等。

另外,上架的应用应用做到尽量少的用户信息收集和权限获取。比如获取用户位置,如果不是主要功能,尽量不要获取。对于一些权限,如读写相册,相机权限等,在新版本系统中有提供不使用这些权限,直接通过系统的API实现的方式,也最好不要请求这些权限。对于DeviceId现在已经不允许收集了,对于Phone_State, 广告Id等,也应该尽量做到不要收集。

谷歌现在每年都要求应用升级Target 版本,这一点开发者也是需要去乖乖的做的,否者新应用无法上架,老应用无法更新。除此之外,对于Android的新特性开发者也应用去积极适配,对于一些特性,google play console也会提醒开发者去适配,比如下图所示。

对于上架的应用,即使没有发布新版本,也有可能被抽查去审核,这时候如果遇到了问题,谷歌也会发信息来让你整改,因此需要关注后台和邮箱,遇到问题要在最终截止日之前修掉对应问题,否者真的会被下架。我就遇到了这个问题,我所提供的登录凭据,审核人员自己输入错误,把我填写的o输入成了0,导致他无法登录,就给我发了整改通知。不过我在修复之后,错误提醒过了仍然没有消失,对于这一点,如果你已经确保改过了就不用再担心了。以下是错误提醒,过了这么多天仍然还在显示。

老账号的价值很高,审核也会比新账号更容易过,因此如果你有一个老账号,请保管好。

对于权限和隐私方面的检查,可以使用Google做的一款工具Checks,它可以帮助检查app中使用到的权限,请求的网络,同时还能够审查隐私政策文件,在发布前借助这个工具检查可以很大程度减少应用审核被拒的风险。

最后

从事Android开发10余年,大多数时间也都是做的海外应用,也是经历过了很多次google play被拒,审核的政策一直在变,之前能通过不代表现在也能通过审核,因此需要不断的学习google play的政策文件。

最后的最后,宣传一下我开发的Memos客户端应用fymemos,欢迎到google play下载。也欢迎留言交流应用上架的故事。

值得反复学习的google 文档:

  1. Play Academy
  2. Play 开发者政策中心
  3. Play Console Help Center

看完评论一下吧

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

看完评论一下吧

使用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地理小工具系列

看完评论一下吧

强大的壳-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脚本)

看完评论一下吧

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文件。

看完评论一下吧

威联通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进程的时候是有差别的。另外关于权限相关的逻辑,以后在权限代码的分析中可以再进行关注。

看完评论一下吧

更优雅的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源码分析:系统进程中事件的读取与分发

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,为了使得文章不至于太长,就留到下次再进行分析。

看完评论一下吧

❌