开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

一次线上Xxl-Job定时任务调度失败的排查与解决 问题 调

发表于 2024-02-26

问题

场景:每隔一分钟执行一次的定时任务
现象:第一次调度成功后,第二次调度必会失败。但是等待第三次调度后,第二次的执行结果却是第三次调度的时间。
image.png
奇怪的是,只有每隔一分钟执行一次的任务才会这样,其他间隔一秒或者一小时的又不会有这个问题。
某次调度错误信息:
image.png

调度中心调度原理

线上xxl-job版本:2.2.0-SNAPSHOT

在排查原因之前,我看了一下这个版本的源码,对任务的调度以及执行有了一些了解。
image.png
执行器内部会开启一个http服务器,用于接受调度中心的调度请求。每次的调度请求都会复用Http连接,避免多次连接消耗资源。
调度步骤:

  1. 调度中心从连接池中获取连接
  2. 调度中心发送调度请求(带有RequestId,JobLogId),并阻塞线程等待执行器响应
  3. 执行器返回调度响应,并异步执行定时任务
  4. 调度中心收到调度响应,根据回传的RequestId,回写数据库调取状态。
  5. 执行器执行任务后,回调调度中心更新任务执行状态接口,根据JobLogId回写任务执行情况。

根据异常堆载定位问题

根据异常堆栈XxlRpcFutureResponse.java:117,我们可以在代码中找到对应抛出异常的位置。位于调度请求发送后,从Future获取异步结果的get方法。
image.png
futureResponse的get方法的逻辑是休眠调度线程(默认休眠1秒),等待执行器响应调度请求。线程被唤醒后去检查本次调度是否完成,要是没有完成,就抛出调度失败的错误。
image.png
从上面逻辑可以推断出调度中心是成功发送调用请求出去了,因为执行器没有响应导致的调度失败错误。
那么,为什么执行器没有响应调度请求呢?我们就得去看看执行器那边的情况了。


由于xxl-job中处理调度请求的Netty处理器并没有打印具体的日志,所以我们要使用Arthas来观察Netty调度请求处理的执行情况了。
image.png

使用Arthas观察调用请求执行情况

使用arthas的watch命令观察执行器接收调用请求情况。

1
arduino复制代码watch com.xxl.rpc.remoting.net.impl.netty_http.server.NettyHttpServerHandler channelRead0 '{params}' -x 2

分析日志

image.png
整理一下日志,在问题产生的这段时间内,执行器这边的Netty的调度请求处理器只收到了三个请求

时间点 channel远程地址信息 请求信息
18:02:00 R:/127.0.0.6:41163 POST / HTTP/1.1;content-length: 626
18:02:30 R:/127.0.0.6:41163 POST / HTTP/1.1;content-length: 159
18:04:00 R:/127.0.0.6:52551 POST / HTTP/1.1;content-length: 626

调用日志中,有两个点比较奇怪

  1. 18:02:30收到了一次请求

在这个时间点为什么会收到一次请求的呢?而且这个请求的请求大小与正常的调用请求的大小还一样,仅有159字节。这么小的请求体,让我想到了一个东西:心跳包。
在RPC框架中,为了监测通信通道的正常以及节点的健康,会定时向节点发送一个请求包,用于判断通道是否正常连接。
虽然调度中心与执行器用的是Http请求,但是xxl-job为了减少资源浪费,使用了连接池,于是也引入了心跳监测。当通道在30秒内没有读写操作时,就发送一个心跳请求。
image.png
在执行器也能看到心跳请求的日志。
image.png
但是这个心跳请求有点奇怪,为什么只在18:02:30分收到呢?18:03:30没有收到呢?
这个问题就跟下面的问题有所关联了。

  1. channel远程地址的IP不太对劲,居然是本地地址并非是调度中心的地址

经我了解后,公司的K8S环境中引入了Istio进行服务治理,其中进出Pod的流量都会被Envoy代理,所以127.0.0.6正是Envoy的IP地址。
image.png

Envoy是一个由Lyft开发的高性能、可扩展的代理(Proxy)服务器,用于处理微服务架构中的网络通信。它被设计成可用于多种不同的部署场景,并提供了丰富的功能集,如负载均衡、流量路由、故障恢复、安全认证等。

定时任务执行情况

收集日志的时间段正好处于问题发生的一个区间内,这个时间段内定时任务执行情况:
image.png

  1. 18:02分的任务正常调度并执行成功了。
  2. 18:03分的任务调度失败,但是由于执行时间的回填是由执行器针对JobLogId来进行匹配的,所以能得出一个结论03分发送的调度请求,执行器在04分收到了并触发了定时任务。
  3. 18:04分的任务调度失败,并且没有执行情况。

总结

截至目前能得到的信息总结一下

  1. 调度中心会在通道30秒内没有读写情况下,向各个执行器发送一个心跳请求。
  2. 调度中心发送心跳请求后,后续的调度请求都无法进入执行器中。
  3. 因为执行器服务部署在K8S中,而且K8S中引入了Istio,导致进入Pod的流量都会被Envoy代理。
  4. 该问题的第二次调度虽然失败了,但是会在第三次调度时被执行器成功接收并处理。

好像没有什么突破点,并没有找到真正调度失败的问题。不过我们可以着重去看看Envoy,因为目前进入到Pod的流量都会被它给代理,去观察一下调度失败发生时Envoy的日志。

Envoy日志

1
2
3
4
csharp复制代码[2024-02-21T18:02:00.026Z] "POST / HTTP/1.1" 200 - via_upstream - "-" 626 176 1 0 "-" "-" "d9313e61-6530-4c08-aae2-fd864930ea31" "ebc-portal.ebc-bpm.svc.cluster.local" "10.64.195.45:7666" inbound|7666|| 127.0.0.6:48733 10.64.195.45:7666 10.64.217.207:45584 - default
[2024-02-21T18:02:30.055Z] "POST / HTTP/1.1" 503 UC upstream_reset_before_response_started{connection_termination} - "-" 159 95 90031 - "-" "-" "3de88bd6-41cf-4078-a381-3542b460dd90" "ebc-portal.ebc-bpm.svc.cluster.local" "10.64.195.45:7666" inbound|7666|| 127.0.0.6:48733 10.64.195.45:7666 10.64.217.207:45584 - default
[2024-02-21T18:04:00.087Z] "POST / HTTP/1.1" 200 - via_upstream - "-" 626 176 1 0 "-" "-" "25dac8f9-8364-42bd-8fdd-d08285ed1632" "ebc-portal.ebc-bpm.svc.cluster.local" "10.64.195.45:7666" inbound|7666|| 127.0.0.6:39739 10.64.195.45:7666 10.64.217.207:45584 - default
[2024-02-21T18:04:00.088Z] "POST / HTTP/1.1" 0 DC downstream_remote_disconnect - "-" 159 0 1 - "-" "-" "bfa7dcee-123f-4582-9368-84edcb8476c7" "ebc-portal.ebc-bpm.svc.cluster.local" "10.64.195.45:7666" inbound|7666|| 127.0.0.6:39739 10.64.195.45:7666 10.64.217.207:45584 - default

上面比较异常的日志信息有两段分别是18:02:30以及第二个18:04:00的请求,响应码都不是正常的200,我们来分析一下异常的原因。

异常日志一

[2024-02-21T18:02:30.055Z] “POST / HTTP/1.1” 503 UC upstream_reset_before_response_started{connection_termination} - “-“ 159 95 90031 - “-“ “-“ “3de88bd6-41cf-4078-a381-3542b460dd90” “ebc-portal.ebc-bpm.svc.cluster.local” “10.64.195.45:7666” inbound|7666|| 127.0.0.6:48733 10.64.195.45:7666 10.64.217.207:45584 - default

提取一下比较关键的日志翻译一下。

  • “2024-02-21T18:02:30”:请求到达Envoy时间
  • “503” 本次请求最终的响应码
  • “UC upstream_reset_before_response_started{connection_termination}”:UC代表Upstream Connection,上游服务出现问题,具体的问题信息为上游服务在响应之前重置了,因为连接被终止了。
  • “159 95 90031 -“:159是请求体大小,说明是心跳请求;95是响应体大小;90031是请求从 Envoy 到目标服务的持续时间

这个请求比较异常的点有三个:

  1. 返回了503响应码

为什么会返回503响应码呢?我看了一下执行器的Netty调度请求处理器中,并没有返回503状态码的代码。那么这个503响应码就可能是Envoy代理服务器返回的。
我修改了一下Envoy日志等级,找到本次心跳请求Debug等级的日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
lua复制代码2024-02-21T18:02:30.846361Z debug envoy router [C32432][S12678816626782510759] cluster 'inbound|7666||' match for URL '/'
2024-02-21T18:02:30.846396Z debug envoy router [C32432][S12678816626782510759] router decoding headers:
':authority', 'ebc-portal.ebc-bpm.svc.cluster.local'
':path', '/'
':method', 'POST'
':scheme', 'http'
'content-length', '159'
'x-forwarded-proto', 'http'
'x-request-id', 'ab261a30-679c-403d-be2b-12b161810f28'
2024-02-21T18:02:30.846434Z debug envoy router [C32432][S12678816626782510759] pool ready
2024-02-21T18:02:30.846469Z debug envoy http [C32432][S12678816626782510759] request end stream
2024-02-21T18:04:00.877035Z debug envoy router [C32432][S12678816626782510759] upstream reset: reset reason: connection termination, transport failure reason:
2024-02-21T18:04:00.877095Z debug envoy http [C32432][S12678816626782510759] Sending local reply with details upstream_reset_before_response_started{connection_termination}
2024-02-21T18:04:00.877148Z debug envoy http [C32432][S12678816626782510759] encoding headers via codec (end_stream=false):
':status', '503'
'content-length', '95'
'content-type', 'text/plain'
'date', 'Mon, 21 Feb 2024 18:04:00 GMT'
'server', 'istio-envoy'

从这行日志中可以看到

Sending local reply with details upstream_reset_before_response_started{connection_termination}

本次心跳请求从18:02:30请求进来,18:04:00Envoy发现上游服务器连接终止,于是自己构造了一个503响应码的响应体返回给调度中心。

  1. 请求时间90031毫秒

本次心跳请求时间太长了,找了一下执行器中Netty调度请求处理器对于心跳请求的处理,发现xxl-job在这个版本中,是不会对心跳做任何处理的。
image.png
导致一直不会返回Http响应,而且在Envoy中要是没有一个请求没有收到响应的话,会认为该请求仍在处理中的。直到达到Envoy的请求超时才会取消这次请求。

  1. 上游服务终止了连接

在这个时间点2024-02-21T18:04:00执行器中打印了一行日志,提示执行器关闭了空闲连接。
image.png
找了一下该行日志在源码中的位置,发现这个关闭连接的前提是90秒内通道没有进行读写,然后执行器觉得该连接通道已经异常了,这也是为什么Envoy会提示上游服务终止了连接的原因。
image.png

异常日志二

[2024-02-21T18:04:00.088Z] “POST / HTTP/1.1” 0 DC downstream_remote_disconnect - “-“ 159 0 1 - “-“ “-“ “bfa7dcee-123f-4582-9368-84edcb8476c7” “ebc-portal.ebc-bpm.svc.cluster.local” “10.64.195.45:7666” inbound|7666|| 127.0.0.6:39739 10.64.195.45:7666 10.64.217.207:45584 - default

  • “2024-02-21T18:04:00”:请求到达Envoy时间
  • “0”:本次请求最终的响应码,实际上本次调度中心的请求并没有完成
  • “DC downstream_remote_disconnect”:DC代表Downstream Connection,下游服务出现问题,具体的问题信息为下游远程服务终止了连接。
  • “159 0 1 - “:159是请求体大小,说明是心跳请求;0是响应体大小;1是请求从 Envoy 到目标服务的持续时间。

在这行日志中,下游服务指的正是调度中心,此刻查看一下调度中心打印的日志,发现抛出了一个XxlRpcException的异常,从这个异常信息我们可以看出,是因为收到了一个无效响应码导致的。
image.png
照葫芦画瓢,我也找到这个异常位于源码的位置:
output.png
这个异常的抛出是由于执行器返回了非200状态码的响应,结合异常日志一中Envoy生成的503响应。我们可以推断出,正是由于这个503响应,导致调度中心关闭了与执行器的连接通道,实际上该连接通道是与Pod中Envoy的连接通道。

原因

经过了上面对于该问题的细致分析排查,终于找到了这个问题产生的原因了。
由于该版本xxl-job在Http协议下的RPC调用中,缺少心跳请求的响应导致的,并且两个服务之间的流程被Envoy给代理了,所以产生了该问题。
服务迁移至K8S后,引进了Istio服务治理,给每个Pod都导入了一个新容器Envoy,用于代理进出Pod的流量。
Envoy在转发一个请求到后端服务后,会等待后端服务返回一个响应。如果一个连接上有多个请求,那么这些请求会被阻塞,直到前一个请求的响应返回。并且由于调度中心使用连接池,而不是每次都新建一个请求,就会导致请求在Envoy阻塞了。
当调度中心发送一个心跳请求时,该请求被Envoy转发给执行器后,Envoy会等待执行器返回响应结果。由于执行器收到心跳请求时,不会进行任何操作,导致Envoy中心会一直等待心跳响应,阻塞后续的调度请求。
由于Envoy阻塞了后续正常的调度请求,导致执行器与调度中心的连接在90S内没有进行读写操作,于是执行器就会将该连接通道关闭。
调度失败路径:

  1. 调度中心与执行器正常连接。
  2. 第一次调度,成功调度并执行定时任务。
  3. 经过30S后,调度中心发送一个心跳请求到执行器。
  4. Envoy代理心跳请求并转发到执行器。
  5. 执行器忽略心跳请求,不会返回Http响应。导致Envoy阻塞本条Http连接,以至于后续的调度请求无法进入执行器。
  6. 执行器90秒后发现和调度中心的Http连接没有任何读写操作,于是关闭该条Http连接。
  7. Envoy等待心跳响应的时候,发现转发的请求被异常关闭,于是自己构造了一个503的响应发送给调度中心。构造完503响应后,按顺序处理该通道后续请求,此时第二次调度被转发到执行器中,第二次调度被执行器正常处理。
  8. 调度中心收到503的响应后,内部Netty的处理器会抛出一个XxlRpcException的异常,导致在调度中心也会关闭与执行器的http连接,导致第三次调度失败。

解决办法

该问题的根因是由于在该版本xxl-job在Http协议下的RPC调用中,缺少心跳请求的响应导致的,并且两个服务之间的流程被Envoy给代理了,所以产生了该问题。
这个问题有两个解决方法:

  1. 在执行器端增加心跳请求响应
  2. 调度中心和执行器之间的流量不再经过Envoy代理

由于xxl-job在该版本下的xxl-rpc已经两年没有更新了,所以只能看看怎么将两个服务之间的流量不走Envoy代理。
此时Istio服务治理中另外一个角色Sidecar出现了。

Sidecar

Sidecar的作用:
可以拦截Pod中的所有入口和出口流量,然后根据Istio的路由规则将流量路由到正确的服务。

在k8s中的Istio创建一个Sidecar,设置调用执行器xxl-job的http端口的流量不经过任何Istio功能。
例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
yaml复制代码apiVersion: networking.istio.io/v1beta1
kind: Sidecar
metadata:
name: direct-xxl-job-my-app
namespace: ebc-bpm
spec:
# 入口流量设置
ingress:
# 直接转发到Pod容器的7679接口
- defaultEndpoint: 127.0.0.1:7679
port:
name: direct-access-port
number: 7679
protocol: TCP
# 对应的工作负载
workloadSelector:
labels:
app: my-app

上面的这个Sidecar配置的作用就是将带有标签app=my-app应用的7679端口TCP入口流量重定向到Pod中的7679端口,不再经过Envoy的代理。
所以我们可以在K8S环境中,配置Sidecar来控制流量是否被Enovy代理,就能有效的解决这个问题了。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

古茗是如何将小程序编译速度提升3倍的

发表于 2024-02-26

unclechong

背景

随着业务的发展,小程序的代码量也在飞速膨胀,古茗最大的 B 端小程序页面已经超过 260+,dev 模式下 dist 目录近 35M,性能稍差的设备从 『代码改动 - Taro 热更新 - 小程序IDE build - 页面reload』这个过程超过 13s;而这个过程在日常需求开发时每天可能重复上百次,这会极大的降低开发效率。

构建现状分析

先聊下业务场景,我们的 B 端小程序都跑在钉钉小程序内,小程序的运行时为 Taro + React,debug 工具是支付宝小程序开发者工具(以下简称IDE),先标记一下它方便后面对它开大招。

image.png

用过小程序多端框架的小伙伴应该都清楚,通常都是由框架层进行一次构建,将运行时代码编译成对应平台的 DSL 并且输出 dist 包,在由对应平台对 dist 包在进行一次全量构建,最终输出离线包。

如果是 watch 模式 IDE 还会对 dist 文件进行全量的监听,文件每一次的改动都会重新的走一遍这道流程。

目前我们的技术栈,就是先由 Taro 构建,对应下图左侧的日志;然后呢 IDE 再次构建,下图模拟器下面的 编译中 就是 IDE 构建的标志;IDE 构建完成后模拟器就会自动 reload 加载最新的代码:

视频转Gif_爱给网_aigei_com (2).gif

以上就是一次小程序热更新完整流程,看似挺快没问题对吧,来我们把剂量加大。

Taro 构建没毛病

刚刚只是 hello world,我们上真实的项目(页面做了脱敏),测试设备是一台 M1 Pro,仅改动一行代码,连续跑三次取最快的一次,你们看跑了几秒,整个流程要 10s+。还有些同学在用公司配的 windows 设备,还会更慢。动图中的 IDE版本号 先忽略后面会有大用处。

a.gif

从两张对比图可以直观的看出,热更新的时间主要花费后半段 IDE 的构建上。从数据上看也确实如此,Taro 的构建时间从 0.3s+ 上升到 1.6s+,IDE 的构建速度从 2s 左右居然升到了恐怖的 9s 多,可怕…

Taro 3.5 之前的版本 webpack 还停留在老版本,各种缓存设置也没有优化,热更新还是有一些慢的。碰巧前段时间刚刚完成了 Taro 的升级,从 3.4.0 升级到 3.6.19 ,在升级后首次构建还有热更新速度都得到了很大的提升,默认配置下 200+ 页面的小程序热更新时间控制在 2s 内,这对我们来说足够!所以 Taro 构建这部分可以直接过。

至于 Taro 3.5+ 添加哪些黑科技,可以移步Taro v3.5 正式发布:开发体验提升。听说后续 4.0 版本还支持了 vite 构建(好奇是如何支持原生 esm 的),这里也推荐大家 Taro 的版本至少在3.5+,除了构建速度提升外,阿里系小程序的包体积也会降低非常多,注意是阿里系,因为模板支持了 import 语法每个模板都会减少10k,页面越多效果越明显这里不展开说了。

但是请注意升级有风险,Breaking change 还是有一些的。也巧了,我们有一份升级踩坑记录,仅供参考Taro 3.6+升级踩坑记录。

IDE 构建成为瓶颈

那 IDE 构建部分为什么会慢这么多呢?

代码量增加

最直观的因素就代码量增多了,几乎所有的构建工具的构建速度都和代码量成正相关。前面聊到 IDE 会再次全量构建 Taro 输出的 dist 文件夹:

  • demo 项目 dist 体积为 2.8M
  • 真实项目在 dev 模式下 Taro 输出的 dist 近 35M 的

下图是目前真实项目在 dev 模式下的 dist 目录,在经过几轮构建配置优化后停留在 35M 左右:


前后代码体积膨胀了十几倍,所以 IDE 相比 demo 项目构建速度慢是必然的。即使不经过 Taro 的构建直接在 IDE 中修改代码再触发热更新,最后的结果也是一样的慢。

估计有小伙伴会说了“这不公平”,你这是 Taro 应用,七转八转全是模版代码,如果纯原生小程序肯定不会这样!!!嗯~,我猜可能会好一些吧我也没验证过(没有这么大原生小程序项目),但是我觉得结论应该也是大差不差。如果有小伙伴能验证原生支付宝小程序构建速度不会随着代码增加而变慢,那我评论区直接送 10 杯古茗好吧😄😄😄

IDE 版本彩蛋

继上篇友好的探讨的文章后,我们再来致敬下支付宝小程序开发者工具(IDE)

重点来了,前方高能!!!能看到的都是有缘人,请大家将支付宝小程序开发者工具(IDE) 降级到3.4.3之前,
我保你构建速度提升30%以上,降级后都说好,Mac Windows真的都管用,没有效果我再送5杯古茗,管用的话记得回来赏个一键三连。


无图无证据,先上图。降级后,相同的代码,相同的改动,IDE 相同的配置 只要 4s,惊了!!!这个文章我是不是不用再写的,哈哈,直接完结。这也解释了为什么我要在动图上面加 IDE版本号 。

视频转Gif_爱给网_aigei_com (1).gif

至于为什么我不清楚,我是人肉试出来的,记得那是某天的下午偶然间的一个降级发现了新大陆。有官方大大看到希望解决我的疑惑,你们到底做了啥 “优化” ?

3.4.3 之后官方又发了好多新版本,甚至有比赛专用版本😄(表扬下 changelog 写的真好真详细,比钉钉小程序文档强多了),新版本增加了很多的 feature 修复了大量的缺陷,甭管其他在碉堡的功能,就优化构建速度过快这个一点,反正我是不会升级。

可行方案

那现状如此,来分析下看看有哪些可行的方案。

方案一:小程序一拆多

单一小程序的代码量太大,那就按业务域进行拆分,从源头来降低代码量。比如数据大盘、商品管理这种关联性不高,逻辑又很复杂的业务就完全可以拆成独立的两个小程项目。这样代码量就会被打散,构建速度自然而然就解决了。这个方案目前还有一些阻力,但确实是我们后续b端小程序的迭代的一个大方向。


从开发的角度看这个方案是非常合理的,小程序就是要 “小”, “快”,“轻”;小程序越小,启动体验、内存占用都会更优,而且小程序的架构层也对发布、预览等环节对包体积做了相应的限制。但是从业务的视角看尤其是操作交互已经固化的前提下再去改造,这会大大提升用户的学习成本。

如果进行快速拆分,前提是应用底层的基础设施要非常健壮,基础组件、工具库、uuc 等通用能力不能再每个小程序都冗余一份。除此之外,应用间的通信、业务拆解梳理、版本管理、以及后期的维护成本这都是需要考虑的问题。

方案二:编译成 h5

既然 IDE 热更新慢,那就不能跳出它吗?浏览器不香吗?

对,确实可以,不要忘了古茗所有的小程序都是跑在 Taro 里面的,Taro 多端的特性天然的就可以把代码转换成web应用的哦。这样确实可以跑的通,单纯只是简单的预览是没问题的,但是要真正的应用到开发中还有有些小问题。

API 的差异

浏览器和 IDE 上对于一些 api 的预览模拟差异比较大,并且因为一些兼容问题浏览器能支持的api比较少,如果开发过程中需要调试还是需要切换到 IDE 中。

设计还原

在对设计稿进行还原的时候,浏览器始终和 IDE 是有差距的,不能精准的做到像素级还原。因为本身构建的产物就是不同的,包括一些其他的兼容问题。

流程问题

想象一下,前端在开发的时候以浏览器为准,在提测和最终上线的时候又以小程序容器为准,鬼知道这里面会有多少坑,开发都是好好的,到 IDE 中就各种问题,要知道小程序容器可是各种定制的黑盒。就目前我们只用小程序环境都会出现 IDE 到真机的各种兼容问题,更不要说浏览器到真机小程序了,所以我觉得从流程上和维护的成本上考虑还是要以小程序环境为准。

现在 Taro 转 web 的场景我们也在使用,但还是仅限于文档中的 demo 预览,基础组件的开发预览。

方案三:动态构建

那能不能 ”渐进式“ 构建呢?比如初始化的时候只构建一部分,访问到哪个页面在动态分析这个页面的依赖然后再进行构建,永远只构建需要的。

IDE 肯定是做不到了,它是接收 Taro 输出的 dist ,输出多少就构建多少,要想实现只能在 Taro 这边想办法。

那就减少 Taro 侧构建的代码,这样不仅能降低 IDE 的构建速度,Taro 的构建速度也会随之降低。

实际在这之前我们就是这样做的,只不过是手动挡😄。在这之前先来简单的了解下 Taro 的构建流程:

image.png

app.config.js 就是小程序的配置文件,这里面包括路由信息,Taro 会遍历这份路由配置,并且逐一推到 webpack 的监听队列里面,被监听的文件后每次改动代码 Taro 就会重新构建生成新的 dist。并且这份配置文件(app.config.js)也会被推送到 Taro 的构建监听队列中,也就是说动态修改这份配置文件(app.config.js),Taro 会重新走一遍整个流程。

image.png

那么之前的手动挡操作就是手动去这份配置表(app.config.js)里注释掉一些当前不需要的路由,只保留本次开发需要debug的路由,这样Taro 就只是构建放开的路由,生成的 dist 文件就小很多,IDE 的构建压力也会相对小很多。但是手动除了操作繁琐还会产生很多问题,比如注释错了,或者把注释提交上去了,又或者需要跳转到被注释的路由时就需要重新构建等。

所以这条路肯定是可行的,核心的问题就是如何变成自动挡,动态按需构建。

联想到单页应用(SPA)

我们从小程序的视角跳出来先,如果是一个传统的web单页应用想要提升热更新的速度可以做哪些事情呢?

  1. 少监听,上各种懒加载…
  2. 少编译,上各种缓存还有 esm、umd、cdn…
  3. 其他
    • 升级硬件
    • 多线程
    • 使用 native 语言?swc、esbuild…

在回到小程序 IDE 的场景上,如果不考虑构建相关的优化(构建相关已经被 IDE 收敛了),也只能从 少监听 这一点入手。

最终实现

来看下最终方案自动挡的整个流程:

image.png

核心的关键步骤以下几点:

劫持 app.config.js

在 Taro 构建是生成临时的配置文件 temp.app.config.js,仅保留最基本的路由信息(tabbar、登录和落地页),其他配置保持与 app.config.js 一致。

并且替换 Taro 的 app.config.js 的引用指向换成 temp.app.config.js,这样就能保证项目初始化构建为最小体积。

image.png

启动本地服务管理临时配置

在 Taro 完成后启动本地 node 服务,专门用于对临时配置(temp.app.config.js)进行 CRUD。

并对外暴露一个http的接口 http://localhost:xxxx/update?target=/pages/xxx/a,这个接口会做如下几件事情

  1. 接口接收一个 path 参数
  2. 用 path 去原配置(app.config.js)查找到对应的完整路由信息,因为路由可能为分包路由
  3. 将匹配到的结果更新到临时配置文件(temp.app.config.js)

校验细节

因为可能会存在分包的场景,我们开发的的场景大部分会在一个分包内完成,所以在路由比对命中后会直接构建一个完整的分包。

比如一个分包中 有 /package/a,/package/b,/package/c 三个页面,当访问 /package/a 时,会直接将这个三个页面一次性构建,不然 package/a 跳转 package/b 再跳转 package/c 就会构建三次。当然这也是一个默认配置,可以关闭。

拦截api

  • 拦截跳转api Taro.navigateTo:我们把触发动态构建的时机标定在跳转时,在跳转时会校验当前的路由是否已经被构建过,检验的逻辑就是用当前跳转路由去临时配置(temp.app.config.js)查找是否存在,不存在时发起http://localhost:xxxx/update?target=/pages/xxx/a请求;如果存在的话直接走 Taro.navigateTo 原逻辑就好。
  • 拦截 Taro DidMount:当 http://localhost:xxxx/update?target=/pages/xxx/a 更新完临时配置文件(temp.app.config.js)后,Taro 会监听到临时配置文件(temp.app.config.js)的改动,并触发重新构建。小程序reload后进入到 DidMount,DidMount 在重定向到目标跳转页面

统一封装

因为零零散散的逻辑很多,所以基于 Taro 的插件机制 将上述的所有逻辑包装成了 Taro Plugin。开启同其他的 Taro Plugin 是一样的方式,只要在编译配置中添加此插件就 ok 了

1
2
3
4
5
6
7
8
9
js复制代码/**
* 伪代码,Taro的构建配置文件 https://docs.taro.zone/docs/config-detail
* config/index.js
*/
module.exports = {
...
plugins: ['taro-plugin-xxx'],
...
};

基于此后续也会在这个插件中添加其他的能力,比如收敛其他插件、收敛 patch-package、内置通用页面等。

可视化拓展

方案内测的时候发现了一个问题,在开发时一旦需要 debug 的页面会跨多个分包时,比如一连跳转 3 个分包那么就会重新构建三次,所以有必要配合这套方案开发一个可视化工具来精细化管理路由,直接勾勾勾就能完成下一次的构建。

所以 vscode 插件来了,可以精细化控制单个路由,上述的场景就可以通过搜索和勾选提前将三个分包勾选好,然后提交,这样一次构建就好了。并且会和临时配置文件(temp.app.config.js)数据共享。


禁用的选项为默认最小路由集合。

问题

还是留了一些坑的:

  • 因为是本地 node 服务,在真机预览时访问没有被构建的路由不能触发动态构建,需要提前构建后真机才能正常访问
  • 相对黑盒,劫持 app.config.js 引用以及复写 Taro 相关的api都需要修改源码才能得以实现

总结

这套方案目前团队内页面过百的小程序都已经接入了,在配合 IDE 降级,这一套 “组合拳” 下来热更新速度大约提升 3 倍,我自己日常开发整个热更新的流程基本控制在 3s 左右。

为了实现这套方案前期大量的 Taro 的源码,发现了很多有意思的黑科技,也算是意料之外的收获,也参与了共建,很庆幸能参与到这样的开源框架,Taro 这么多年还在一直坚持迭代维护也是真的很不容易,致敬!

最后

🌟 招聘信息:

  • 25 / 26 届前端开发实习生

📚 小茗文章推荐:

  • 钉钉小程序实现签名板
  • JSPDF + html2canvas A4分页截断
  • Formily JSON Schema 渲染流程及案例浅析

关注公众号「Goodme前端团队」,获取更多干货实践,欢迎交流分享~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【Android 13源码分析】WindowContaine

发表于 2024-02-25

在安卓源码的设计中,将将屏幕分为了37层,不同的窗口将在不同的层级中显示。
对这一块的概念以及相关源码做了详细分析,整理出以下几篇。

【Android 13源码分析】WindowContainer窗口层级-1-初识窗口层级树

【Android 13源码分析】WindowContainer窗口层级-2-构建流程

【Android 13源码分析】WindowContainer窗口层级-3-实例分析

当前为第三篇,以应用窗口和系统窗口2大类型窗口的挂载为例介绍窗口是如何挂载到层级树中的。
这篇看完对AOSP中整个窗口树就有了比较完整的了解。

  1. 应用窗口挂载

应用启动流程中会触发ActivityRecord,Task,WindowState的创建与挂载,其中WindowState的处理上在addWindow流程。

企业微信截图_17089400658325.png

1.1 ActivityRecord的创建

启动流程开始的时候会执行到ActivityStarter::executeRequest,在这个方法里会创建一个ActivityRecord

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
scss复制代码# ActivityStarter
private int executeRequest(Request request) {
......
final ActivityRecord r = new ActivityRecord.Builder(mService)
.setCaller(callerApp)
.setLaunchedFromPid(callingPid)
.setLaunchedFromUid(callingUid)
.setLaunchedFromPackage(callingPackage)
.setLaunchedFromFeature(callingFeatureId)
.setIntent(intent)
.setResolvedType(resolvedType)
.setActivityInfo(aInfo)
.setConfiguration(mService.getGlobalConfiguration())
.setResultTo(resultRecord)
.setResultWho(resultWho)
.setRequestCode(requestCode)
.setComponentSpecified(request.componentSpecified)
.setRootVoiceInteraction(voiceSession != null)
.setActivityOptions(checkedOptions)
.setSourceRecord(sourceRecord)
.build();
......
// 继续执行startActivityUnchecked
mLastStartActivityResult = startActivityUnchecked(r, sourceRecord, voiceSession,
request.voiceInteractor, startFlags, true /* doResume */, checkedOptions,
inTask, inTaskFragment, restrictedBgActivity, intentGrants);
......
}

tips: ActivityRecord的构造方法会创建一个Token,这个token就是阅读源码经常看到看到代表activity的那个token。

1.2 Task的创建与挂载

流程开始会执行到ActivityStarter::startActivityInner方法,在这里会执行ActivityStarter::getOrCreateRootTask方法来创建(获取)一个Task

调用链如下:

企业微信截图_17089401943071.png

主流程代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
less复制代码# ActivityStarter
private Task getOrCreateRootTask(ActivityRecord r, int launchFlags, Task task,
ActivityOptions aOptions) {
final boolean onTop =
(aOptions == null || !aOptions.getAvoidMoveToFront()) && !mLaunchTaskBehind;
final Task sourceTask = mSourceRecord != null ? mSourceRecord.getTask() : null;
return mRootWindowContainer.getOrCreateRootTask(r, aOptions, task, sourceTask, onTop,
mLaunchParams, launchFlags);
}
// onTop 表示是否要移到到当前栈顶,那肯定是要的,新启动的Activity当前要再最上面,这里 aOptions 为null,所以为true
// sourceTask 表示从哪里启动的,当前launch所在的Task 就是sourceTask

# RootWindowContainer
Task getOrCreateRootTask(@Nullable ActivityRecord r, @Nullable ActivityOptions options,
@Nullable Task candidateTask, boolean onTop) {
return getOrCreateRootTask(r, options, candidateTask, null /* sourceTask */, onTop,
null /* launchParams */, 0 /* launchFlags */);
}
Task getOrCreateRootTask(@Nullable ActivityRecord r,
@Nullable ActivityOptions options, @Nullable Task candidateTask,
@Nullable Task sourceTask, boolean onTop,
@Nullable LaunchParamsController.LaunchParams launchParams, int launchFlags) {
......
final int activityType = resolveActivityType(r, options, candidateTask);
if (taskDisplayArea != null) {
if (canLaunchOnDisplay(r, taskDisplayArea.getDisplayId())) {
// 重点*1. 传递到TaskDisplayArea
return taskDisplayArea.getOrCreateRootTask(r, options, candidateTask,
sourceTask, launchParams, launchFlags, activityType, onTop);
} else {
taskDisplayArea = null;
}
}
......
}
// 经过同名调用后,逻辑进入到 TaskDisplayArea

# TaskDisplayArea
Task getOrCreateRootTask(int windowingMode, int activityType, boolean onTop,
@Nullable Task candidateTask, @Nullable Task sourceTask,
@Nullable ActivityOptions options, int launchFlags) {
if(....) {
// 拿到之前创建的Task
return candidateTask.getRootTask();
}
......// 第一次显示所以是新建Task
return new Task.Builder(mAtmService)
.setWindowingMode(windowingMode)
.setActivityType(activityType)
.setOnTop(onTop)
.setParent(this) // 主要这个this被设置为Parent。所以直接挂载到了DefaultTaskDisplayArea下
.setSourceTask(sourceTask)
.setActivityOptions(options)
.setLaunchFlags(launchFlags)
.build();
}
// 看方法名是获取或创建Task, 这边是新启动的Activity所以需要创建Task。如果是以默认启动方式打开应用内的另一个Activity,就走的是上面的 return candidateTask.getRootTask();
接下来就是真正触发Task的创建。
// 另外设置的parent就是层级结构树应用所在的名为“DefaultTaskDisplayArea”的TaskDisplayArea
# Task
# Task.Builder
Task build() {
if (mParent != null && mParent instanceof TaskDisplayArea) {
validateRootTask((TaskDisplayArea) mParent);
}

if (mActivityInfo == null) {
mActivityInfo = new ActivityInfo();
mActivityInfo.applicationInfo = new ApplicationInfo();
}

mUserId = UserHandle.getUserId(mActivityInfo.applicationInfo.uid);
mTaskAffiliation = mTaskId;
mLastTimeMoved = System.currentTimeMillis();
mNeverRelinquishIdentity = true;
mCallingUid = mActivityInfo.applicationInfo.uid;
mCallingPackage = mActivityInfo.packageName;
mResizeMode = mActivityInfo.resizeMode;
mSupportsPictureInPicture = mActivityInfo.supportsPictureInPicture();
if (mActivityOptions != null) {
mRemoveWithTaskOrganizer = mActivityOptions.getRemoveWithTaskOranizer();
}
// 重点* 1. 创建task
final Task task = buildInner();
task.mHasBeenVisible = mHasBeenVisible;

// Set activity type before adding the root task to TaskDisplayArea, so home task can
// be cached, see TaskDisplayArea#addRootTaskReferenceIfNeeded().
if (mActivityType != ACTIVITY_TYPE_UNDEFINED) {
task.setActivityType(mActivityType);
}
// 重点* 2. 入栈 这里的 mOnTop为true
if (mParent != null) {
if (mParent instanceof Task) {
final Task parentTask = (Task) mParent;
parentTask.addChild(task, mOnTop ? POSITION_TOP : POSITION_BOTTOM,
(mActivityInfo.flags & FLAG_SHOW_FOR_ALL_USERS) != 0);
} else {
mParent.addChild(task, mOnTop ? POSITION_TOP : POSITION_BOTTOM);
}
}

// Set windowing mode after attached to display area or it abort silently.
if (mWindowingMode != WINDOWING_MODE_UNDEFINED) {
task.setWindowingMode(mWindowingMode, true /* creating */);
}
// 返回
return task;
}

// 创建
Task buildInner() {
return new Task(mAtmService, mTaskId, mIntent, mAffinityIntent, mAffinity,
mRootAffinity, mRealActivity, mOrigActivity, mRootWasReset, mAutoRemoveRecents,
mAskedCompatMode, mUserId, mEffectiveUid, mLastDescription, mLastTimeMoved,
mNeverRelinquishIdentity, mLastTaskDescription, mLastSnapshotData,
mTaskAffiliation, mPrevAffiliateTaskId, mNextAffiliateTaskId, mCallingUid,
mCallingPackage, mCallingFeatureId, mResizeMode, mSupportsPictureInPicture,
mRealActivitySuspended, mUserSetupComplete, mMinWidth, mMinHeight,
mActivityInfo, mVoiceSession, mVoiceInteractor, mCreatedByOrganizer,
mLaunchCookie, mDeferTaskAppear, mRemoveWithTaskOrganizer);
}

小结:
最后描述一下最后创建的2个重点部分:

  1. 看到通过buildInner 创建了一个task,而buildInner 也很简单粗暴,通过各个变量直接new Task 对象。
  2. mParent 不为null, 是 因为在创建的时候 setParent(this),当前的这个this,就是 getDefaultTaskDisplayArea返回的也就是应用Activity存在的”DefaultTaskDisplayArea”。

在 RootWindowContainer::getOrCreateRootTask 体现。

创建Task-getOrCreateRootTask.png
注意log里的 #17 的这个Task,与前面的层级结构树新增的Task,是对应的上的。而且this= DefaultTaskDisplayArea 说明也确实是往DefaultTaskDisplayArea里添加了。

1.3 ActivityRecord的挂载

调用链
ActivityStarer::setNewTask
ActivityStarer::addOrReparentStartingActivity
主流程代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码# ActivityStarer
private void setNewTask(Task taskToAffiliate) {
// 为true
final boolean toTop = !mLaunchTaskBehind && !mAvoidMoveToFront;
// 就是mTargetRootTask,也就是刚刚创建的Task
final Task task = mTargetRootTask.reuseOrCreateTask(
mNewTaskInfo != null ? mNewTaskInfo : mStartActivity.info,
mNewTaskIntent != null ? mNewTaskIntent : mIntent, mVoiceSession,
mVoiceInteractor, toTop, mStartActivity, mSourceRecord, mOptions);
task.mTransitionController.collectExistenceChange(task);
// ActivityRecord的挂载
addOrReparentStartingActivity(task, "setTaskFromReuseOrCreateNewTask");
// 需要注意这里的日志打印
ProtoLog.v(WM_DEBUG_TASKS, "Starting new activity %s in new task %s",
mStartActivity, mStartActivity.getTask());

// mLaunchTaskBehind 为false,所以taskToAffiliate 为null
if (taskToAffiliate != null) {
mStartActivity.setTaskToAffiliateWith(taskToAffiliate);
}
}

这里的task 和mTargetRootTask是同一个对象, 进源码跟到流程也是一样。
然后进入 addOrReparentStartingActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
less复制代码# ActivityStarer
private void addOrReparentStartingActivity(@NonNull Task task, String reason) {
// newParent = task 都是刚刚创建的Task
TaskFragment newParent = task;
......
if (mStartActivity.getTaskFragment() == null
|| mStartActivity.getTaskFragment() == newParent) {
// 重点, 将 ActivityRecord挂在到新创建的Task中,并且是顶部
newParent.addChild(mStartActivity, POSITION_TOP);
} else {
mStartActivity.reparent(newParent, newParent.getChildCount() /* top */, reason);
}
}

这里的逻辑设计到的Task就是上一步创建的Task,mStartActivity则是“电话”在之前逻辑创建的ActivityRecord.
setNewTask的堆栈信息如下

AcvtivityRecord挂在到Task.png
另外这段逻辑里有个ProtoLog打印,日志如下:

setNewTask的ProtoLog.png

1.4 WindowState的创建与挂载

WindowManagerService::addWindow
WindowState::init – WindowState的创建
WindowToken::addWindow – WindowState的挂载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码#  WindowManagerService

public int addWindow(Session session, IWindow client, LayoutParams attrs, int viewVisibility,
int displayId, int requestUserId, InsetsVisibilities requestedVisibilities,
InputChannel outInputChannel, InsetsState outInsetsState,
InsetsSourceControl[] outActiveControls) {
......// token处理
// 创建WindowState
final WindowState win = new WindowState(this, session, client, token, parentWindow,
appOp[0], attrs, viewVisibility, session.mUid, userId,
session.mCanAddInternalSystemWindow);
......
// 7. 窗口添加进容器
win.attach();
......
win.mToken.addWindow(win);
......
}

“win.mToken”窗口的token是ActyivityRecord

1
2
3
4
5
6
typescript复制代码# ActivityRecord
@Override
void addWindow(WindowState w) {
super.addWindow(w);
......
}

直接调用其父类方法,ActivityRecord是WindowToken

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
scss复制代码# WindowToken
void addWindow(final WindowState win) {
ProtoLog.d(WM_DEBUG_FOCUS,
"addWindow: win=%s Callers=%s", win, Debug.getCallers(5));

if (win.isChildWindow()) {
// Child windows are added to their parent windows.
return;
}
// This token is created from WindowContext and the client requests to addView now, create a
// surface for this token.
// 真正添加进子容器,调用的是WindowContainer的方法
if (!mChildren.contains(win)) {
ProtoLog.v(WM_DEBUG_ADD_REMOVE, "Adding %s to %s", win, this);
// 定义在WindowContainer中,其实就是挂载到父容器下了
addChild(win, mWindowComparator);
mWmService.mWindowsChanged = true;
// TODO: Should we also be setting layout needed here and other places?
}
}
  1. 系统窗口挂载

2.1 WindowToken,WindowState的创建与挂载

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
java复制代码    public int addWindow(Session session, IWindow client, LayoutParams attrs, int viewVisibility,
int displayId, int requestUserId, InsetsVisibilities requestedVisibilities,
InputChannel outInputChannel, InsetsState outInsetsState,
InsetsSourceControl[] outActiveControls) {
......
// 系统应用获取不到token
WindowToken token = displayContent.getWindowToken(
hasParent ? parentWindow.mAttrs.token : attrs.token);
......
if (token == null) {
......
} else {
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();
}
}
// 创建WindowState
final WindowState win = new WindowState(this, session, client, token, parentWindow,
appOp[0], attrs, viewVisibility, session.mUid, userId,
session.mCanAddInternalSystemWindow);
......
// 窗口添加进容器
win.attach();
......
win.mToken.addWindow(win);
......

WindowState的创建和应用窗口一样,区别在与WindowToken,系统窗口执行addWindow方法是没有token的,所以会执行创建逻辑。
在创建的时候会根据窗口类型选择挂载的层级。

2.2 WindowToken的挂载

企业微信截图_1708940254526.png

WindowToken的构造方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码# WindowToken
protected WindowToken(WindowManagerService service, IBinder _token, int type,
boolean persistOnEmpty, DisplayContent dc, boolean ownerCanManageAppTokens,
boolean roundedCornerOverlay, boolean fromClientToken, @Nullable Bundle options) {
super(service);
token = _token;
windowType = type;
......
if (dc != null) {
// 添加token
dc.addWindowToken(token, this);
}
}

创建WindowToken的时候会由DisplayContent执行挂载逻辑,

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
scss复制代码# DisplayContent
DisplayAreaPolicy mDisplayAreaPolicy;

void addWindowToken(IBinder binder, WindowToken token) {
......
// 放入集合
mTokenMap.put(binder, token);

if (token.asActivityRecord() == null) {
......
// 找到对应的位置挂载
final DisplayArea.Tokens da = findAreaForToken(token).asTokens();
da.addChild(token);
}
}

DisplayArea findAreaForToken(WindowToken windowToken) {
// 根据type查找
return findAreaForWindowType(windowToken.getWindowType(), windowToken.mOptions,
windowToken.mOwnerCanManageAppTokens, windowToken.mRoundedCornerOverlay);
}

DisplayArea findAreaForWindowType(int windowType, Bundle options,
boolean ownerCanManageAppToken, boolean roundedCornerOverlay) {
// 应用类型
if (windowType >= FIRST_APPLICATION_WINDOW && windowType <= LAST_APPLICATION_WINDOW) {
return mDisplayAreaPolicy.getTaskDisplayArea(options);
}
// 输入法窗口
if (windowType == TYPE_INPUT_METHOD || windowType == TYPE_INPUT_METHOD_DIALOG) {
return getImeContainer();
}
// 其他类型
return mDisplayAreaPolicy.findAreaForWindowType(windowType, options,
ownerCanManageAppToken, roundedCornerOverlay);
}

状态栏不属于应用窗口,走后面的逻辑,DisplayAreaPolicy是个接口,真正的实现是DisplayAreaPolicyBuilder的内部类Result

1
2
3
4
5
6
7
8
9
10
11
12
13
scala复制代码# DisplayAreaPolicyBuilder

static class Result extends DisplayAreaPolicy {
final BiFunction<Integer, Bundle, RootDisplayArea> mSelectRootForWindowFunc;
......
@Override
public DisplayArea.Tokens findAreaForWindowType(int type, Bundle options,
boolean ownerCanManageAppTokens, boolean roundedCornerOverlay) {
return mSelectRootForWindowFunc.apply(type, options).findAreaForWindowTypeInLayer(type,
ownerCanManageAppTokens, roundedCornerOverlay);
}
......
}

mSelectRootForWindowFunc是一个存放RootDisplayArea的map,所以后续逻辑在RootDisplayArea中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码// 根据type 找到在容器树的位置, 如果是应用或者输入法都走不到这
# RootDisplayArea
// 这个就是层级树的
private DisplayArea.Tokens[] mAreaForLayer;
@Nullable
DisplayArea.Tokens findAreaForWindowTypeInLayer(int windowType, boolean ownerCanManageAppTokens,
boolean roundedCornerOverlay) {
// 获取到type
int windowLayerFromType = mWmService.mPolicy.getWindowLayerFromTypeLw(windowType,
ownerCanManageAppTokens, roundedCornerOverlay);
if (windowLayerFromType == APPLICATION_LAYER) {
throw new IllegalArgumentException(
"There shouldn't be WindowToken on APPLICATION_LAYER");
}
// 根据type查找对应的位置
return mAreaForLayer[windowLayerFromType];
}

getWindowLayerFromTypeLw会根据type找到对应的层级,返回一个int。
然后根据这个值去mAreaForLayer拿到对应的DisplayArea.Tokens,将系统窗口的WindowToken挂载进去
mAreaForLayer其实就是开始构建层级树的那个集合。

2.2.1 mAreaForLayer的赋值

在开机构建窗口层级树的逻辑,最后会执行到RootDisplayArea::onHierarchyBuilt将层级树的集合传递出去。

1
2
3
4
5
6
7
8
9
less复制代码# DisplayAreaPolicyBuilder.HierarchyBuilder

private final RootDisplayArea mRoot;

private void build(@Nullable List<HierarchyBuilder> displayAreaGroupHierarchyBuilders) {
......// 层级树的构建
// 通知根节点已经完成了所有DisplayArea的添加 (将displayAreaForLayer保存在RootDisplayArea成员变量roomAreaForLayer中,供后面逻辑使用)
mRoot.onHierarchyBuilt(mFeatures, displayAreaForLayer, featureAreas);
}

RootDisplayArea下的mAreaForLayer变量赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ini复制代码# RootDisplayArea
private DisplayArea.Tokens[] mAreaForLayer;

void onHierarchyBuilt(ArrayList<Feature> features, DisplayArea.Tokens[] areaForLayer,
Map<Feature, List<DisplayArea<WindowContainer>>> featureToDisplayAreas) {
if (mHasBuiltHierarchy) {
throw new IllegalStateException("Root should only build the hierarchy once");
}
mHasBuiltHierarchy = true;
mFeatures = Collections.unmodifiableList(features);
// 赋值
mAreaForLayer = areaForLayer;
mFeatureToDisplayAreas = featureToDisplayAreas;
}

所以mAreaForLayer保存了层级树各个层级的对象因此根据index可以获取到对应的DisplayArea.Tokens,并执行系统窗口WindowToken的挂载。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【Android 13源码分析】WindowContaine

发表于 2024-02-25

在安卓源码的设计中,将将屏幕分为了37层,不同的窗口将在不同的层级中显示。
对这一块的概念以及相关源码做了详细分析,整理出以下几篇。

【Android 13源码分析】WindowContainer窗口层级-1-初识窗口层级树

【Android 13源码分析】WindowContainer窗口层级-2-构建流程

【Android 13源码分析】WindowContainer窗口层级-3-实例分析

【Android 13源码分析】WindowContainer窗口层级-4-Surface树

当前为第二篇,第一篇对窗口树有一个简单的认识后,本篇介绍窗口树的构建代码流程。
整个过程会相对无聊,但是不讲代码的技术文章就是耍流氓。

  1. dump内容的打印

先看dump的数据在代码中是如何定义的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typescript复制代码# WindowManagerService
RootWindowContainer mRoot;

private void doDump(FileDescriptor fd, PrintWriter pw, String[] args, boolean useProto) {
......
// containers
} else if ("containers".equals(cmd)) {
synchronized (mGlobalLock) {
mRoot.dumpChildrenNames(pw, " ");
pw.println(" ");
mRoot.forAllWindows(w -> {pw.println(w);}, true /* traverseTopToBottom */);
}
return;
} else if ("trace".equals(cmd)) {
......
}
// 打印的name
@Override
String getName() {
return "ROOT";
}

dumpChildrenNames的实现在WindowContainer的父类ConfigurationContainer中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
scss复制代码# ConfigurationContainer
public void dumpChildrenNames(PrintWriter pw, String prefix) {
final String childPrefix = prefix + " ";
pw.println(getName()
+ " type=" + activityTypeToString(getActivityType())
+ " mode=" + windowingModeToString(getWindowingMode())
+ " override-mode=" + windowingModeToString(getRequestedOverrideWindowingMode())
+ " requested-bounds=" + getRequestedOverrideBounds().toShortString()
+ " bounds=" + getBounds().toShortString());
for (int i = getChildCount() - 1; i >= 0; --i) {
final E cc = getChildAt(i);
// 打印 # 加角标
pw.print(childPrefix + "#" + i + " ");
cc.dumpChildrenNames(pw, childPrefix);
}
}

可以看到从RootWindowContainer开始递归打印。 这也就是dump到的窗口容器层级树的内容。比如最开始的RootWindowContainer::getName返回的内容就是 “ROOT”。

  1. 层级树的构建

2.1 调用链

企业微信截图_17089403239745.png

2.2 前期的一些调用链

调用链前面这段可以知道,在系统启动的时候就触发了这段逻辑,这也就是为什么刚进入launcher就可以dump出整个结构树的原因。
setWindowManager 方法传递的参数是WMS, WMS的启动是tartOtherServices中,而RootWindowContainer则是WMS的一个成员变量,RootWindowContainer是层级树中的跟容器,在WMS构建函数中创建。

1
2
3
4
5
6
7
8
9
csharp复制代码# WindowManagerService
// The root of the device window hierarchy.
RootWindowContainer mRoot;
// WMS构造函数
private WindowManagerService(......) {
......
mRoot = new RootWindowContainer(this);
......
}

继续看构建流程

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
ini复制代码# RootWindowContainer
void setWindowManager(WindowManagerService wm) {
mWindowManager = wm;
mDisplayManager = mService.mContext.getSystemService(DisplayManager.class);
mDisplayManager.registerDisplayListener(this, mService.mUiHandler);
mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class);

final Display[] displays = mDisplayManager.getDisplays();
// 遍历每个屏幕
for (int displayNdx = 0; displayNdx < displays.length; ++displayNdx) {
final Display display = displays[displayNdx];
// 重点*1:为每一个 Display 挂载一个 DisplayContent 节点
final DisplayContent displayContent = new DisplayContent(display, this);
addChild(displayContent, POSITION_BOTTOM);
if (displayContent.mDisplayId == DEFAULT_DISPLAY) {
mDefaultDisplay = displayContent;
}
}

// 重点*2 TaskDisplayArea相关
final TaskDisplayArea defaultTaskDisplayArea = getDefaultTaskDisplayArea();
defaultTaskDisplayArea.getOrCreateRootHomeTask(ON_TOP);
positionChildAt(POSITION_TOP, defaultTaskDisplayArea.mDisplayContent,
false /* includingParents */);
}

重点解析:

  1. 这段代码也就能看出,为什么说一个DisplayContent就代表着1个屏幕了。
  2. 处理TaskDisplayArea相关(这里窗口层级树已经构建完成了)
    上篇看层级树知道TaskDisplayArea就是放应用相关容器的,目前先不看这块,先跟踪DisplayContent下的逻辑, 现在需要看DisplayContent的构造方法,因为里面开始构造这个屏幕下层级树。(不考虑多屏幕的情况)
  1. DisplayContent 开始构造当前屏幕的层级树

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
scss复制代码# DisplayContent
// 2个参数为Display和跟容器
DisplayContent(Display display, RootWindowContainer root) {
......
// 创建事务
final Transaction pendingTransaction = getPendingTransaction();
// 重点开始构建层级树
configureSurfaces(pendingTransaction);
// 执行事务
pendingTransaction.apply();
......
}

private void configureSurfaces(Transaction transaction) {
// 构建一个SurfaceControl
final SurfaceControl.Builder b = mWmService.makeSurfaceBuilder(mSession)
.setOpaque(true)
.setContainerLayer()
.setCallsite("DisplayContent");
// 设置名字后构建 (Display 0 name="XXX")
mSurfaceControl = b.setName(getName()).setContainerLayer().build();

// 重点* 设置策略并构建显示区域层次结构
if (mDisplayAreaPolicy == null) {
// WMS的getDisplayAreaPolicyProvider方法按返回 DisplayAreaPolicy.Provider
// 然后其 instantiate的实现 目前只有DisplayAreaPolicy的内部类DefaultProvider

mDisplayAreaPolicy = mWmService.getDisplayAreaPolicyProvider().instantiate(
mWmService, this /* content */, this /* root */,
mImeWindowsContainer);
}
// 事务相关设置
transaction
.setLayer(mSurfaceControl, 0)
.setLayerStack(mSurfaceControl, mDisplayId)
.show(mSurfaceControl)
.setLayer(mOverlayLayer, Integer.MAX_VALUE)
.show(mOverlayLayer);
}

这一块我们目前可以忽略transaction的代码只需要关心中间“getDisplayAreaPolicyProvider”这一块就好了,这段是层级树的主流程。

tips:

  1. 方法最开始的 setName设置name在层级树是能找到对应名字的
  2. 注意instantiate倒数第3第4都个参数传递的都是this,也就是DisplayContent, 因为 DisplayContent这是的父类是RootDisplayArea
1
2
3
4
csharp复制代码# DisplayContent
String getName() {
return "Display " + mDisplayId + " name=\"" + mDisplayInfo.name + "\"";
}

mDisplayId 如果只有一个屏幕就是 0 ,所以dump到层级树中的这句信息

1
shell复制代码  #0 Display 0 name="Built-in Screen"

就是在这里设置的,后面的”Built-in Screen”对应的应该就是mDisplayInfo.name了。
接下来继续看主流程:

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
scss复制代码# DisplayAreaPolicy.Provider
@Override
public DisplayAreaPolicy instantiate(WindowManagerService wmService,
DisplayContent content, RootDisplayArea root,
DisplayArea.Tokens imeContainer) {

// 重点*1. 创建一个名为 "DefaultTaskDisplayArea" 的对象作为应用窗口的默认容器(第三个参数Feature为FEATURE_DEFAULT_TASK_CONTAINER)
final TaskDisplayArea defaultTaskDisplayArea = new TaskDisplayArea(content, wmService,
"DefaultTaskDisplayArea", FEATURE_DEFAULT_TASK_CONTAINER);
final List<TaskDisplayArea> tdaList = new ArrayList<>();
// 实际上只有1个元素
tdaList.add(defaultTaskDisplayArea);

// Define the features that will be supported under the root of the whole logical
// display. The policy will build the DisplayArea hierarchy based on this.
// 传递RootDisplayArea(DisplayContent)构建出一个层级树的数据结构
final HierarchyBuilder rootHierarchy = new HierarchyBuilder(root);
// Set the essential containers (even if the display doesn't support IME).
// 设置输入法容器
rootHierarchy.setImeContainer(imeContainer).setTaskDisplayAreas(tdaList);

// 这个条件满足,肯定是被信任的
if (content.isTrusted()) {
// 重点* 2 配置层级的支持的Feature
configureTrustedHierarchyBuilder(rootHierarchy, wmService, content);
}

// Instantiate the policy with the hierarchy defined above. This will create and attach
// all the necessary DisplayAreas to the root.
// 重点* 3 真正开始构建层级树
return new DisplayAreaPolicyBuilder().setRootHierarchy(rootHierarchy).build(wmService);
}

这个方法是在DisplayContent构造函数掉进来的,注意最后2个参数,root表示跟容器,imeContainer则是输入法容器,在DisplayContent中传过来的,然后被通过setImeContainer设置给了HierarchyBuilder。
重点分析:

  1. “DefaultTaskDisplayArea” 终于出现了, 可以看到确实是TaskDisplayArea对象,然后FEATURE_DEFAULT_TASK_CONTAINER这个ID的值就是1, 那也就是在第二层,和层级树是对应的,然后构建了一个List,但是这个集合就这一个元素。
  2. 配置层级的支持的Feature
  3. 开始真正的构建

3.1 配置Feature

发现层级树中一共就出现了5个Feature就是在当前方法中配置的,分别如下:
WindowedMagnification
HideDisplayCutout
OneHanded
FullscreenMagnification
ImePlaceholder

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
less复制代码# DisplayAreaPolicy.Provider
private void configureTrustedHierarchyBuilder(HierarchyBuilder rootHierarchy,
WindowManagerService wmService, DisplayContent content) {
// WindowedMagnification should be on the top so that there is only one surface
// to be magnified.
rootHierarchy.addFeature(new Feature.Builder(wmService.mPolicy, "WindowedMagnification",
FEATURE_WINDOWED_MAGNIFICATION)
.upTo(TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY)
.except(TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY)
// Make the DA dimmable so that the magnify window also mirrors the dim layer.
.setNewDisplayAreaSupplier(DisplayArea.Dimmable::new)
.build());
if (content.isDefaultDisplay) {
// Only default display can have cutout.
// See LocalDisplayAdapter.LocalDisplayDevice#getDisplayDeviceInfoLocked.
rootHierarchy.addFeature(new Feature.Builder(wmService.mPolicy, "HideDisplayCutout",
FEATURE_HIDE_DISPLAY_CUTOUT)
.all()
.except(TYPE_NAVIGATION_BAR, TYPE_NAVIGATION_BAR_PANEL, TYPE_STATUS_BAR,
TYPE_NOTIFICATION_SHADE)
.build())
.addFeature(new Feature.Builder(wmService.mPolicy, "OneHanded",
FEATURE_ONE_HANDED)
.all()
.except(TYPE_NAVIGATION_BAR, TYPE_NAVIGATION_BAR_PANEL,
TYPE_SECURE_SYSTEM_OVERLAY)
.build());
}
rootHierarchy
.addFeature(new Feature.Builder(wmService.mPolicy, "FullscreenMagnification",
FEATURE_FULLSCREEN_MAGNIFICATION)
.all()
.except(TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, TYPE_INPUT_METHOD,
TYPE_INPUT_METHOD_DIALOG, TYPE_MAGNIFICATION_OVERLAY,
TYPE_NAVIGATION_BAR, TYPE_NAVIGATION_BAR_PANEL)
.build())
.addFeature(new Feature.Builder(wmService.mPolicy, "ImePlaceholder",
FEATURE_IME_PLACEHOLDER)
.and(TYPE_INPUT_METHOD, TYPE_INPUT_METHOD_DIALOG)
.build());
}

这里执行了5次addFeature,每次对应一个Feature刚好是5个。Feature.Builder构造一个Feature对象,代码如下

1
2
3
4
5
6
7
ini复制代码# DisplayAreaPolicy.Feature.Builder
Builder(WindowManagerPolicy policy, String name, int id) {
mPolicy = policy;
mName = name;
mId = id;
mLayers = new boolean[mPolicy.getMaxWindowLayer() + 1];
}

注意后面2个参数,第二个为name,就是名字,后面的是ID,根据使用的地方肯定是定义了对应ID的。
留意下mLayers,mPolicy.getMaxWindowLayer()返回36所以是定义了一个长度为37的boolean类型数组,如果为ture表示这个图层支持这个Feature,为false反之。

5个Feature对应的ID如下,并且有相应的注释:

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
php复制代码# DisplayAreaOrganizer
/**
* Display area that can be magnified in
* ......
*/
public static final int FEATURE_WINDOWED_MAGNIFICATION = FEATURE_SYSTEM_FIRST + 4;

/**
* Display area for hiding display cutout feature
* @hide
*/
public static final int FEATURE_HIDE_DISPLAY_CUTOUT = FEATURE_SYSTEM_FIRST + 6;

/**
* Display area for one handed feature
*/
public static final int FEATURE_ONE_HANDED = FEATURE_SYSTEM_FIRST + 3;

/**
* Display area that can be magnified in
* ......
*/
public static final int FEATURE_FULLSCREEN_MAGNIFICATION = FEATURE_SYSTEM_FIRST + 5;

/**
* Display area that the IME container can be placed in. Should be enabled on every root
* hierarchy if IME container may be reparented to that hierarchy when the IME target changed.
* @hide
*/
public static final int FEATURE_IME_PLACEHOLDER = FEATURE_SYSTEM_FIRST + 7;

根据注释能知道这个Feature代表这个图层具体用于什么特征了。

然后还看到all(),and(),except()等方法。

3.1.1 all,and,except方法

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
typescript复制代码# DisplayAreaPolicy.Feature.Builder

Builder all() {
Arrays.fill(mLayers, true);
return this;
}

Builder and(int... types) {
for (int i = 0; i < types.length; i++) {
int type = types[i];
set(type, true);
}
return this;
}

Builder except(int... types) {
for (int i = 0; i < types.length; i++) {
int type = types[i];
set(type, false);
}
return this;
}

Builder upTo(int typeInclusive) {
// 根据传入的type计算到图层
final int max = layerFromType(typeInclusive, false);
for (int i = 0; i < max; i++) {
mLayers[i] = true;
}
set(typeInclusive, true);
return this;

}
private void set(int type, boolean value) {
mLayers[layerFromType(type, true)] = value;
......
}

private int layerFromType(int type, boolean internalWindows) {
return mPolicy.getWindowLayerFromTypeLw(type, internalWindows);
}

Feature build() {
// 默认为true
if (mExcludeRoundedCorner) {
// Always put the rounded corner layer to the top most layer.
mLayers[mPolicy.getMaxWindowLayer()] = false;
}
return new Feature(mName, mId, mLayers.clone(), mNewDisplayAreaSupplier);
}

mLayers前面说过是一个长度为37的数组,set方法就是将参数的这个图层,对应的boolean设置为true, 换句话说就是指定某个图层是否支持这个Feature。
all():将所有数组所有值都设为true,表示每个图层都支持这个Feature
and(): 将指定某个图层支持这个Feature
except():将指定某个图层不支持这个Feature
upTo(): 将支持Feature的图层设置为从0到typeInclusive
build():将数组的最后最后一个设置为false,剔除最后一层

这里的几个方法都会调用到layerFromType,根据layerFromType方法的调用知道具体逻辑在WindowManagerPolicy::getWindowLayerFromTypeLw方法控制的.
这段代码有点长是因为好多case,但是总体逻辑并不复杂,主要关注传入的WindowType和返回的Layertype,其实就是返回层级树中所在的图层。

3.1.2 重点:getWindowLayerFromTypeLw方法 (决定窗口挂载在那一层)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
kotlin复制代码# WindowManagerPolicy
default int getWindowLayerFromTypeLw(int type, boolean canAddInternalSystemWindow,
boolean roundedCornerOverlay) {
// Always put the rounded corner layer to the top most.
// 第二个参数为false,这里忽略
if (roundedCornerOverlay && canAddInternalSystemWindow) {
return getMaxWindowLayer();
}
// 根据这2个type名字也知道表示 APP图层,对应的值是1-99,如果处于这直接就返回APPLICATION_LAYER =2
if (type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) {
return APPLICATION_LAYER;
}
// 然后开始根据各个WindowType,去返回其在层级树中所在的图层
switch (type) {
case TYPE_WALLPAPER:
// wallpaper is at the bottom, though the window manager may move it.
return 1;
case TYPE_PRESENTATION:
case TYPE_PRIVATE_PRESENTATION:
case TYPE_DOCK_DIVIDER:
case TYPE_QS_DIALOG:
case TYPE_PHONE:
return 3;
case TYPE_SEARCH_BAR:
return 4;
case TYPE_INPUT_CONSUMER:
return 5;
case TYPE_SYSTEM_DIALOG:
return 6;
case TYPE_TOAST:
// toasts and the plugged-in battery thing
return 7;
case TYPE_PRIORITY_PHONE:
// SIM errors and unlock. Not sure if this really should be in a high layer.
return 8;
case TYPE_SYSTEM_ALERT:
// like the ANR / app crashed dialogs
// Type is deprecated for non-system apps. For system apps, this type should be
// in a higher layer than TYPE_APPLICATION_OVERLAY.
return canAddInternalSystemWindow ? 12 : 9;
case TYPE_APPLICATION_OVERLAY:
return 11;
case TYPE_INPUT_METHOD:
// on-screen keyboards and other such input method user interfaces go here.
return 13;
case TYPE_INPUT_METHOD_DIALOG:
// on-screen keyboards and other such input method user interfaces go here.
return 14;
case TYPE_STATUS_BAR:
return 15;
case TYPE_STATUS_BAR_ADDITIONAL:
return 16;
case TYPE_NOTIFICATION_SHADE:
return 17;
case TYPE_STATUS_BAR_SUB_PANEL:
return 18;
case TYPE_KEYGUARD_DIALOG:
return 19;
case TYPE_VOICE_INTERACTION_STARTING:
return 20;
case TYPE_VOICE_INTERACTION:
// voice interaction layer should show above the lock screen.
return 21;
case TYPE_VOLUME_OVERLAY:
// the on-screen volume indicator and controller shown when the user
// changes the device volume
return 22;
case TYPE_SYSTEM_OVERLAY:
// the on-screen volume indicator and controller shown when the user
// changes the device volume
return canAddInternalSystemWindow ? 23 : 10;
case TYPE_NAVIGATION_BAR:
// the navigation bar, if available, shows atop most things
return 24;
case TYPE_NAVIGATION_BAR_PANEL:
// some panels (e.g. search) need to show on top of the navigation bar
return 25;
case TYPE_SCREENSHOT:
// screenshot selection layer shouldn't go above system error, but it should cover
// navigation bars at the very least.
return 26;
case TYPE_SYSTEM_ERROR:
// system-level error dialogs
return canAddInternalSystemWindow ? 27 : 9;
case TYPE_MAGNIFICATION_OVERLAY:
// used to highlight the magnified portion of a display
return 28;
case TYPE_DISPLAY_OVERLAY:
// used to simulate secondary display devices
return 29;
case TYPE_DRAG:
// the drag layer: input for drag-and-drop is associated with this window,
// which sits above all other focusable windows
return 30;
case TYPE_ACCESSIBILITY_OVERLAY:
// overlay put by accessibility services to intercept user interaction
return 31;
case TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY:
return 32;
case TYPE_SECURE_SYSTEM_OVERLAY:
return 33;
case TYPE_BOOT_PROGRESS:
return 34;
case TYPE_POINTER:
// the (mouse) pointer layer
return 35;
default:
Slog.e("WindowManager", "Unknown window type: " + type);
return 3;
}
}

代码很长不用一个个看,直接根据参数找就好,比如当参数是TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY,那对于的返回就是32。
后面看到的对应的TYPE,直接复制WindowManagerPolicy类下搜索即可。

现在再重新看看configureTrustedHierarchyBuilder方法里5个Feature到底是什么。

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
less复制代码# DisplayAreaPolicy.Provider
private void configureTrustedHierarchyBuilder(HierarchyBuilder rootHierarchy,
WindowManagerService wmService, DisplayContent content) {
// WindowedMagnification should be on the top so that there is only one surface
// to be magnified.
rootHierarchy.addFeature(new Feature.Builder(wmService.mPolicy, "WindowedMagnification",
FEATURE_WINDOWED_MAGNIFICATION)
.upTo(TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY) // 0-32
.except(TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY)// 32
// Make the DA dimmable so that the magnify window also mirrors the dim layer.
.setNewDisplayAreaSupplier(DisplayArea.Dimmable::new)
.build()); // 0-31
if (content.isDefaultDisplay) {
// Only default display can have cutout.
// See LocalDisplayAdapter.LocalDisplayDevice#getDisplayDeviceInfoLocked.
rootHierarchy.addFeature(new Feature.Builder(wmService.mPolicy, "HideDisplayCutout",
FEATURE_HIDE_DISPLAY_CUTOUT)
.all() // 0-36
.except(TYPE_NAVIGATION_BAR, TYPE_NAVIGATION_BAR_PANEL, TYPE_STATUS_BAR,
TYPE_NOTIFICATION_SHADE)// 24 25 15 17
.build())// 0-14 16 18-23 26-35
.addFeature(new Feature.Builder(wmService.mPolicy, "OneHanded",
FEATURE_ONE_HANDED)
.all()
.except(TYPE_NAVIGATION_BAR, TYPE_NAVIGATION_BAR_PANEL,
TYPE_SECURE_SYSTEM_OVERLAY)//24 25 33
.build());// 0-23 26-32 34-35
}
rootHierarchy
.addFeature(new Feature.Builder(wmService.mPolicy, "FullscreenMagnification",
FEATURE_FULLSCREEN_MAGNIFICATION)
.all() // 0-36
.except(TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, TYPE_INPUT_METHOD,
TYPE_INPUT_METHOD_DIALOG, TYPE_MAGNIFICATION_OVERLAY,
TYPE_NAVIGATION_BAR, TYPE_NAVIGATION_BAR_PANEL)// 32 13 14 28 24 25
.build())// 0-12 15-23 26-27 29-31 33-35
.addFeature(new Feature.Builder(wmService.mPolicy, "ImePlaceholder",
FEATURE_IME_PLACEHOLDER)
.and(TYPE_INPUT_METHOD, TYPE_INPUT_METHOD_DIALOG)// 13-14
.build());// 13-14
}
}

3.2 Feature总结

WindowedMagnification:
拥有特征的层级: 0-31
特征描述: 支持窗口缩放的一块区域,一般是通过辅助服务进行缩小或放大

HideDisplayCutout:
拥有特征的层级: 0-14 16 18-23 26-35
特征描述:隐藏剪切区域,即在默认显示设备上隐藏不规则形状的屏幕区域,比如在代码中打开这个功能后,有这个功能的图层就不会延伸到刘海屏区域。

OneHanded:
拥有特征的层级:0-23 26-32 34-35
特征描述:表示支持单手操作的图层,这个功能在手机上还是挺常见的

FullscreenMagnification:
拥有特征的层级:0-12 15-23 26-27 29-31 33-35
特征描述:支持全屏幕缩放的图层,和上面的不同,这个是全屏缩放,前面那个可以局部

ImePlaceholder:
拥有特征的层级: 13-14
特征描述:输入法相关

再放上之前画的层级树更加清晰了

层级结构树.png

3.3 构建层级树 DisplayAreaPolicyBuilder::build

上面只是将5个Feature添加到了rootHierarchy的mFeatures这个集合中

1
2
3
4
5
6
7
csharp复制代码# HierarchyBuilder
private final ArrayList<DisplayAreaPolicyBuilder.Feature> mFeatures = new ArrayList<>();

HierarchyBuilder addFeature(DisplayAreaPolicyBuilder.Feature feature) {
mFeatures.add(feature);
return this;
}

DisplayAreaPolicyBuilder::setRootHierarchy方法很简单,就是把添加了ImeContainer和5个Feature的HierarchyBuilder设置给DisplayAreaPolicyBuilder

1
2
3
4
5
ini复制代码# DisplayAreaPolicyBuilder
DisplayAreaPolicyBuilder setRootHierarchy(HierarchyBuilder rootHierarchyBuilder) {
mRootHierarchyBuilder = rootHierarchyBuilder;
return this;
}

然后开始执行DisplayAreaPolicyBuilder::build

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
scss复制代码# DisplayAreaPolicyBuilder
// 这个可以忽略,没有值
private final ArrayList<HierarchyBuilder> mDisplayAreaGroupHierarchyBuilders =
new ArrayList<>();

Result build(WindowManagerService wmService) {
// 对输入参数进行验证,确保它们是有效的
validate();

// Attach DA group roots to screen hierarchy before adding windows to group hierarchies.
// 重点:构建层级树
mRootHierarchyBuilder.build(mDisplayAreaGroupHierarchyBuilders);
// 因为mDisplayAreaGroupHierarchyBuilders没有值,后面的都可以忽略
......
return new Result(wmService, mRootHierarchyBuilder.mRoot, displayAreaGroupRoots,
mSelectRootForWindowFunc);
}
  1. 层级结构树构造

这个mRootHierarchyBuilder就是上一小节操作的RootHierarchyBuilder,然后执行其build方法,这个方法非常重要!!!

层级树构造图.png

在构造层级树一共分为2步:

  1. 构建PendingArea树
    1. 构建Feature相关
    2. 构建Leaf相关
  2. 根据PendingArea树构建最终的DisplayAreas树,也就是层级树

通过2个类的名字也能感觉到一些关系,毕竟叫Pending。既然要先构造PendingPendingArea,那肯定需要先看看PendingArea这个数据结构

4.1 数据结构 PendingArea简介

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ini复制代码# DisplayAreaPolicyBuilder.PendingArea

static class PendingArea {
final int mMinLayer; // 最小层级
final ArrayList<PendingArea> mChildren = new ArrayList<>();// 有Children说明也是一个容器
final Feature mFeature; // 当前支持的Feature
final PendingArea mParent; // 有父亲
int mMaxLayer; // 最大层级
// 从这几个成员变量其实能感觉到和上一篇画的层级树的图有点那味了
@Nullable DisplayArea mExisting; // 当前存在的容器
boolean mSkipTokens = false; // 只有输入法和应用会为true

PendingArea(Feature feature, int minLayer, PendingArea parent) {
mMinLayer = minLayer;
mFeature = feature;
mParent = parent;
}
......
}

PendingArea后面还有一些方法,等后面会再次具体分析,当前只有PendingArea这个数据结构是什么样就好了。

4.2 构建PendingArea树

下面这段代码比较长我再代码里加了很多注释,其实这一块就是java的循环对数据结构的处理。就和刚学java的时候看2个for循环一样。
这个方法其实是构建整个树的方法,先看一眼,后面会再具体分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
ini复制代码# DisplayAreaPolicyBuilder.HierarchyBuilder

private final RootDisplayArea mRoot;

private void build(@Nullable List<HierarchyBuilder> displayAreaGroupHierarchyBuilders) {
final WindowManagerPolicy policy = mRoot.mWmService.mPolicy;
// 定义最大层级数 37 = 36+1
final int maxWindowLayerCount = policy.getMaxWindowLayer() + 1;
// 存储每个窗口层级对应的 DisplayArea.Tokens,一共37个,后续窗口挂载也是在这个数据结构上找
// 在方法底部执行instantiateChildren的时候调用
final DisplayArea.Tokens[] displayAreaForLayer =
new DisplayArea.Tokens[maxWindowLayerCount];

// 存储每个特性对应的 DisplayArea 列表
// mFeatures就是在configureTrustedHierarchyBuilder配置的Feature大小,很明显一共是5个
final Map<Feature, List<DisplayArea<WindowContainer>>> featureAreas =
new ArrayMap<>(mFeatures.size());
for (int i = 0; i < mFeatures.size(); i++) {
// 为每个feature,创建其对应的DisplayArea列表
featureAreas.put(mFeatures.get(i), new ArrayList<>());
}
// 到这里featureAreas里一共是5个值,key就是上节提到的5个Feature,value目前就是个空的List

// -------构建PendingArea树----
// *1 创建 PendingArea 数组,用于临时存储每个窗口层级对应的 PendingArea,也是37个
// 后面需要关注这个areaForLayer成员的变化
PendingArea[] areaForLayer = new PendingArea[maxWindowLayerCount];
// 先创建个PendingArea 再让areaForLayer的37个数据都默认为root的PendingArea
// 注意第一个参数feature为null
final PendingArea root = new PendingArea(null, 0, null);
Arrays.fill(areaForLayer, root);

// 2. 构建Features的树
// mFeatures.size为5,所以有5个大循环
final int size = mFeatures.size();
for (int i = 0; i < size; i++) {
// 拿到当前需要处理的Feature
final Feature feature = mFeatures.get(i);
PendingArea featureArea = null;
// 内部循环,37次
for (int layer = 0; layer < maxWindowLayerCount; layer++) {
// 如果这个层级,支持当前Feature
if (feature.mWindowLayers[layer]) {
//判断是否复用 PendingArea (同一个feature才复用,否则创建新的)
if (featureArea == null || featureArea.mParent != areaForLayer[layer]) {
// 创建新的 PendingArea,作为上一层级的子节点,用于当前层级,并且双向奔赴,设置为各自的孩子或者父亲
// 注意第一个参数feature
featureArea = new PendingArea(feature, layer, areaForLayer[layer]);
areaForLayer[layer].mChildren.add(featureArea);
}
areaForLayer[layer] = featureArea;
} else {
// 如果该特性不应用于当前窗口层级,则featureArea置为空。用于上面if的判断
featureArea = null;
}
}
}
// 到这里,areaForLayer这个37层就按照feature分类,有自己对应的PendingArea了。


// 3. 构建叶子节点相关的PendingArea,注意还是操作areaForLayer数组,但是操作的是内部元素的mChildren的值

// 定义一个叶子节点用的 PendingArea
PendingArea leafArea = null;
int leafType = LEAF_TYPE_TOKENS;// 定义leafType

for (int layer = 0; layer < maxWindowLayerCount; layer++) {
// 获取每层的type,从这看type是和所在层级有关系的
int type = typeOfLayer(policy, layer);
// // 检查是否可以复用前一个层级的 Tokens,和前面的循环类似
if (leafArea == null || leafArea.mParent != areaForLayer[layer]
|| type != leafType) {
// 创建PendingArea,注意参数,featur为null
leafArea = new PendingArea(null /* feature */, layer, areaForLayer[layer]);
// 注意是添加到孩子,而不是跟上一次循环直接修改areaForLayer
areaForLayer[layer].mChildren.add(leafArea);
leafType = type;
// 应用类型处理
if (leafType == LEAF_TYPE_TASK_CONTAINERS) {
// 添加 TaskDisplayArea 到应用程序层级
addTaskDisplayAreasToApplicationLayer(areaForLayer[layer]);
// 添加 DisplayAreaGroup 到应用程序层级
addDisplayAreaGroupsToApplicationLayer(areaForLayer[layer],
displayAreaGroupHierarchyBuilders);
// 跳过创建 Tokens,即不创建 Tokens,即使没有 Task
leafArea.mSkipTokens = true;
} else if (leafType == LEAF_TYPE_IME_CONTAINERS) {
// 输入法处理
leafArea.mExisting = mImeContainer;
// 跳过
leafArea.mSkipTokens = true;
}
}
leafArea.mMaxLayer = layer;
}
// 计算根节点的最大层级
root.computeMaxLayer();
// -------构建DisplayAreas树----
// 4. 根据之前定义的PendingArea生成最后的 DisplayAreas 树
// 注意参数
// We built a tree of PendingAreas above with all the necessary info to represent the
// hierarchy, now create and attach real DisplayAreas to the root.
root.instantiateChildren(mRoot, displayAreaForLayer, 0, featureAreas);

// 通知根节点已经完成了所有DisplayArea的添加 (将displayAreaForLayer保存在RootDisplayArea成员变量roomAreaForLayer中,供后面逻辑使用)
mRoot.onHierarchyBuilt(mFeatures, displayAreaForLayer, featureAreas);
}

4.2.1 构建Feature相关

这边根据具体的执行画了几张图,先看上面Features的循环逻辑,在执行循环前数组areaForLayer和执行第一次大循环后集合如下
第一个Feature是WindowedMagnification拥有特征的层级 0-31,也就是其 前面32个为true。

第一次大循环结束数组.png
在层级树,如果某一块都是支持同一Feature的话,可以写成 “name 起始层:结束层 ”的形式,转换后如下
转换成层级树的方式就是

第一次大循环结束数组-转换.png
然后第二个大循环

第二Feature是HideDisplayCutout拥有特征的层级 0-14 16 18-23 26-35

第二次大循环结束数组.png
太长了所以第24开始换到了下一排, 规律就是结合上一次的循环,0-31以内,HideDisplayCutout的父亲都是上一次循环的WindowedMagnification,然后32之后的父亲就是默认的root了。
再转成层级树的表示形式如下:

第二次大循环结束数组-转换.png
按照这个规则Feature 5次全执行完后,层级树的图就是下面这个,不过做了下顺序的调整,从小到达排序

第一次大循环结束.png

4.2.2 构建Leaf相关

在构建叶子节点的时候,又有一个新的东西,leafType,对应的就是叶子节点的类型,默认是LEAF_TYPE_TOKENS,一共也只定义了3个

1
2
3
4
php复制代码# DisplayAreaPolicyBuilder.HierarchyBuilder
private static final int LEAF_TYPE_TASK_CONTAINERS = 1; // APP
private static final int LEAF_TYPE_IME_CONTAINERS = 2; // 输入法
private static final int LEAF_TYPE_TOKENS = 0; // 默认

然后是通过typeOfLayer方法根据当前层级返回type

1
2
3
4
5
6
7
8
9
10
11
12
csharp复制代码# DisplayAreaPolicyBuilder.HierarchyBuilder

private static int typeOfLayer(WindowManagerPolicy policy, int layer) {
if (layer == APPLICATION_LAYER) {
return LEAF_TYPE_TASK_CONTAINERS;
} else if (layer == policy.getWindowLayerFromTypeLw(TYPE_INPUT_METHOD)
|| layer == policy.getWindowLayerFromTypeLw(TYPE_INPUT_METHOD_DIALOG)) {
return LEAF_TYPE_IME_CONTAINERS;
} else {
return LEAF_TYPE_TOKENS;
}
}

逻辑还是比较简单的除了输入法(13-14)和应用(2)所在的层级,均返回LEAF_TYPE_TOKENS。
经过第二个for循环后,相当于给每个Feature 都加上了一个Leaf , 然后对输入法和应用图做了单独的处理。
先看输入法的, 是把mExisting设置为了最开始从DisplayConten传进来的mImeContainer,然后mSkipTokens设置为false,表示后续的操作可以跳过。
然后看对应用图层的处理,除了也将mSkipTokens设置为false外还执行了2个方法其中第二個方法。addDisplayAreaGroupsToApplicationLayer因为内部依赖displayAreaGroupHierarchyBuilders,而目前也没看到对这个对象操作的地方,所以长度为0,可以忽略,所以只看

addTaskDisplayAreasToApplicationLayer方法即可

addTaskDisplayAreasToApplicationLayer

mTaskDisplayAreas看到应该联想到前面创建的name为“DefaultTaskDisplayArea”的那一个TaskDisplayArea,事实上也就是在那创建的,这个是和应用最相关的图层,
从代码上看也能证明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ini复制代码# DisplayAreaPolicyBuilder.HierarchyBuilder
private final ArrayList<TaskDisplayArea> mTaskDisplayAreas = new ArrayList<>();

HierarchyBuilder setTaskDisplayAreas(List<TaskDisplayArea> taskDisplayAreas) {
mTaskDisplayAreas.clear();
mTaskDisplayAreas.addAll(taskDisplayAreas);
return this;
}
private void addTaskDisplayAreasToApplicationLayer(PendingArea parentPendingArea) {
// 已知长度为1,
final int count = mTaskDisplayAreas.size();

for (int i = 0; i < count; i++) {
PendingArea leafArea =
new PendingArea(null /* feature */, APPLICATION_LAYER, parentPendingArea);
// 所以就是把“DefaultTaskDisplayArea”这个设置为mExisting
leafArea.mExisting = mTaskDisplayAreas.get(i);
leafArea.mMaxLayer = APPLICATION_LAYER;
// parentPendingArea.mChildren本来为大家都一样的Leaf,又添加了一个DefaultTaskDisplayArea
parentPendingArea.mChildren.add(leafArea);
}
}

这段对应用图层的处理非常的重要了,特别最下面对parentPendingArea.mChildren再次添加DefaultTaskDisplayArea的操作

经过这个循环的处理,每个Feature下面都有了对应的叶子节点,如图:

添加叶子节点.png
第二层应用层目前是有2个孩子的,一个是Lead,另一个就是DefaultTaskDisplayArea。
到目前为止,层级树雏形是有了。但是比较还是一个PendingArea数组,另外 Leaf 0:1 这种目前在代码上也还没有得到体现。

4.3 真正DisplayAreas树 PendingArea::instantiateChildren

其实从PendingArea::instantiateChildren上面源码给的2个注释也知道,前面的2个循环,只是构建了一个PendingAreas树,接下来才是真正构建层级树(DisplayAreas)
并把这个树添加到root(DisplayContent)

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
scss复制代码# DisplayAreaPolicyBuilder.PendingArea

void instantiateChildren(DisplayArea<DisplayArea> parent, DisplayArea.Tokens[] areaForLayer,
int level, Map<Feature, List<DisplayArea<WindowContainer>>> areas) {
// 1. 子区域按照它们的最小层级进行升序排列
mChildren.sort(Comparator.comparingInt(pendingArea -> pendingArea.mMinLayer));
// 2. 遍历孩子将PendingArea转换成DisplayArea
for (int i = 0; i < mChildren.size(); i++) {
final PendingArea child = mChildren.get(i);
final DisplayArea area = child.createArea(parent, areaForLayer);
if (area == null) {
// TaskDisplayArea and ImeContainer can be set at different hierarchy, so it can
// be null.
continue;
}
// 将返回的area设置为孩子,第一次执行的时候root就是DisplayContent
parent.addChild(area, WindowContainer.POSITION_TOP);
if (child.mFeature != null) {
// 让Feature对应的容器里添加创建的DisplayArea
areas.get(child.mFeature).add(area);
}
// 开始迭代构建
child.instantiateChildren(area, areaForLayer, level + 1, areas);
}
}

先解析一下3个参数
parent:根据上面代码的代码逻辑,root就是DisplayContent
areaForLayer: 这个是build方法开始创建的displayAreaForLayer
level:从哪级开始
areas: 这个也是build方法创建的map集合,key是Feature。

  1. 上来就执行了个排序,这个mChildren是啥呢?咋一看好像一点印象都没有,但是根据这个方法调用处看,他是root.instantiateChildren,
    而这个root是构建PendingAreas树时最开始创建的root,也就是我们上面图片PendingAreas树里的 root 0:0。所以他的孩子就是2次循环处理后,父亲是他的PendingArea,也就是那些feature或者leaf
  2. 这一步就是将那些PendingArea的数据结构转换为DisplayArea
    之前看过PendingArea的成员变量和构造方法,现在看看
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
typescript复制代码# DisplayAreaPolicyBuilder.PendingArea
@Nullable
private DisplayArea createArea(DisplayArea<DisplayArea> parent,
DisplayArea.Tokens[] areaForLayer) {
// 只有输入法和应用层mExisting有值
if (mExisting != null) {
if (mExisting.asTokens() != null) {
// 只有输入法满足
// Store the WindowToken container for layers
fillAreaForLayers(mExisting.asTokens(), areaForLayer);
}
// 然后将mExisting作为结果返回
return mExisting;
}
// mSkipTokens为true则返回,应用和IME创建的PendingArea
if (mSkipTokens) {
return null;
}
// 2. 定义DisplayArea的type
DisplayArea.Type type;
if (mMinLayer > APPLICATION_LAYER) {
type = DisplayArea.Type.ABOVE_TASKS;
} else if (mMaxLayer < APPLICATION_LAYER) {
type = DisplayArea.Type.BELOW_TASKS;
} else {
type = DisplayArea.Type.ANY;
}

if (mFeature == null) {
// // 3. 构建返回的leaf 注意第三个参数格式
final DisplayArea.Tokens leaf = new DisplayArea.Tokens(parent.mWmService, type,
"Leaf:" + mMinLayer + ":" + mMaxLayer);
fillAreaForLayers(leaf, areaForLayer); // 给对应覆盖的层级都需要赋值
return leaf;
} else {
// 对有Feature的PendingArea返回构建
return mFeature.mNewDisplayAreaSupplier.create(parent.mWmService, type,
mFeature.mName + ":" + mMinLayer + ":" + mMaxLayer, mFeature.mId);
}
}

注意这里的参数areaForLayer这个是一个build方法创建的集合,也是最终层级树的体现。

  1. 方法前面mExisting.asTokens, 这个asTokens,方法定义在DisplayArea中默认返回null,只有DisplayArea.Tokens返回本身。 而ImeContainer是继承DisplayArea.Tokens的,所以有返回值。
    而对于应用层mExisting是TaskDisplayArea,不是DisplayArea.Tokens的子类,所以这个不满足,也就是说只有IME的PendingArea才会执行下面fillAreaForLayers的逻辑
1
2
3
4
5
6
ini复制代码# DisplayAreaPolicyBuilder.PendingArea
private void fillAreaForLayers(DisplayArea.Tokens leaf, DisplayArea.Tokens[] areaForLayer) {
for (int i = mMinLayer; i <= mMaxLayer; i++) {
areaForLayer[i] = leaf;
}
}

fillAreaForLayers方法也比较简单,就是将这个PendingArea的所有图层都设置传进来的leaf。那当前逻辑只处理IME的话,就是把13,14层都设置这个mExisting.
另外应用层不执行到fillAreaForLayers,执行后面的return mExisting, 这里也有个很重要的点,因为前面知道应用层的Feature有2个孩子,但是mExisting却是为DefaultTaskDisplayArea,
这也就是为什么最终层级树的第二层只有DefaultTaskDisplayArea的原因

  1. 定义了个DisplayArea的type, 也不复杂, 如果当前区域最小的图层都大于应用图层(2),那type就是ABOVE_TASKS,如果最大图层还小于应用图层(2)就是BELOW_TASKS(这个只有壁纸了),
    其他的就是ANY。目前还不知道具体用处,我认为了解即可
  2. mFeature == null的条件,在上面build方法里有2个for循环都创建了PendingArea对象,第二个创建叶子节点的时候是没有传递mFeature的。
    直接创建DisplayArea.Tokens,最重要的是第三个参数,是一个字符串,就是构建这个对象的name,看格式也是非常的清楚。其实就是层级树的Leaf节点,比如“Leaf:0:1 ”。(舒服了)
  3. 这里处理的是第一次循环对Feature构建出来的PendingArea,
    这里比较好奇的是这个mNewDisplayAreaSupplier是什么,那么就需要看Feature的定义了
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
arduino复制代码# DisplayAreaPolicyBuilder
static class Feature {
private final String mName;
private final int mId;
private final boolean[] mWindowLayers;
private final NewDisplayAreaSupplier mNewDisplayAreaSupplier;
// 构造函数
private Feature(String name, int id, boolean[] windowLayers,
NewDisplayAreaSupplier newDisplayAreaSupplier) {
mName = name;
mId = id;
mWindowLayers = windowLayers;
mNewDisplayAreaSupplier = newDisplayAreaSupplier;
}
static class Builder {
......
// 默认为DisplayArea对象
private NewDisplayAreaSupplier mNewDisplayAreaSupplier = DisplayArea::new;
private boolean mExcludeRoundedCorner = true;
Feature build() {
......
return new Feature(mName, mId, mLayers.clone(), mNewDisplayAreaSupplier);
}
}
}
/** Supplier interface to provide a new created {@link DisplayArea}. */
interface NewDisplayAreaSupplier {
DisplayArea create(WindowManagerService wms, DisplayArea.Type type, String name,
int featureId);
}

mNewDisplayAreaSupplier这个对象的赋值是在Feature的构造方法,而根据代码分析,添加的5个Feature是通过Builder的方式,所以我们现在分析的
mNewDisplayAreaSupplier的值,就是定义在Feature.Builder下的默认值也就是DisplayArea对象
所以这一步就是返回了一个DisplayArea对象,然后name就是 “mFeature.mName + “:” + mMinLayer + “:” + mMaxLayer” 比如 “HideDisplayCutout:32:35”

到现在为止,层级树每个成员是如何构建,以及里面的字符串名字是怎么来的,就全都清楚了。
后面迭代也只是方法的递归而已,经过一层一层的迭代后,整个层级结构树就构建好了。
现在的层级树如下:

开机后的层级树.png
这个层级树和上一篇看 不太一样那是因为Leaf下没有内容了,应用层“DefaultTask
DisplayArea”和壁纸层也没有内容,那是因为Leaf后面的内容都是具体业务添加上去的。
所以其实对应Window的add流程,其实也就是真没添加到这个层级树的流程。后面具体分析业务的时候肯定是会有具体案例的。

  1. 小结

窗口层级树这一块的代码有点抽象,代码虽然不多但是也挺绕。我写的也水平有限,学习这块最好是自己也能跟着画出一个层级树的图来。当然就算画不了,也问题不大,再怎么不济现在对层级树的概念肯定也是有了解的,也知道怎么命令看,以后实际业务经常会需要比对层级树的变化,看到多了,自如而且就清除了。虽然层级树打印的内容比较多,但是只要关注DefaultTaskDisplayArea下的内容,这一块的内容也就那么点。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

想进国企,跳槽真的不要太频繁 想进国企,跳槽真的不要太频繁

发表于 2024-02-25

想进国企,跳槽真的不要太频繁

image.png

去年内推的一个学弟,面试兜兜转转一个月,最终北京的领导还是把他挂了,因为觉得他跳槽是在太频繁了,担心稳定性不行。

其实这个学弟本身是非常优秀的,本科和研究生都毕业于985大学,学校期间实习的地方也都是腾讯、阿里、字节这样的大公司,毕业后的第一份工作也是大家耳熟能详的某国有大银行的软开岗。

简历和履历都非常不错,但是不知道什么原因他却选择了毕业刚工作半年就裸辞,当我刚认识他的时候他以及在成都到处投简历找工作快一个月了。

虽然这种履历也不至于颗粒无收,但是比起他上一份工作的档次还是差了很多,并且也可能是在银行的科技岗被卷怕了,所以总跟我说想找一个稳定的地方呆上一段时间。

然而,遗憾的是我们这边北京的领导觉得他半年就离职十分不稳定,也对他关上了大门。

想要了解更多成都央国企招聘、内推、简历辅导、面试辅导等信息,欢迎关注知识星球【成都央国企指南】,帮助应届生、社会人士等了解成都央国企信息,可进行一对一简历辅导、职业规面试辅导等。

为什么国企这么在乎稳定性?

我记得当时面试来到这家公司的时候,招聘我的主任心里还很虚,因为我也是呆了一年就跳槽的,所以担心我过来心里落差大,呆不了多久。

好在当时我没有离职,所以心态很平和然后才跟这边把offer谈下来了,所以我想想如果当时我已经离职了的话,作为社招我可能就拿不到这个offer了吧。

那么国企为什么这么在乎稳定性呢,国企其实和私企不同(这里我说的是真国企,像什么科技子公司,有末尾淘汰还真的搞的不在我的讨论范围之内),不需要你作为个人有太大的能力和产出,更不需要把你压榨到极致。

本质上国企的项目是通过领导层的关系运作来的,不需要去市场是厮杀来抢一个项目,有些垄断行业这项目就得你们公司做,哪怕是拿到项目后外包出去,都得你们集团来牵头。

并且国企的领导和你一样,都是给国家打工,而且国企的升迁也不是唯业绩论成败,所以事情即使你干得没那么好对领导的影响也没那么大,不会搞你搞得太难看,除非你这个领导有精神疾病。

所以,一般垄断国企的稳定性还行,很多员工都是呆上好几年甚至数十年的,一方面是工作环境确实不错,没人难为你,另一方面就是在国企呆久了就确实失去了去外面厮杀的能力,也不敢跳槽。

工作的节奏也会比较慢,在国企你呆了半年可能项目方面才上手,在这种环境下,由于待遇也没有外面私企那么高,领导招一个人实际上是非常慎重的。

像这个学弟的半年一跳的情况,在国企领导眼里看起来可能觉得你都还没开始上手,怎么就跳槽了,那我招你进来会不会是瞎折腾,所以就选择敬而远之了。

如何在招聘的时候展示你的稳定性?

上面说了那么多,在国企的招聘当中,如果你能够恰到好处的展示你的稳定性的话,那么国企的领导可能会更加青睐你。主要可以从以下几个方面来展示你的稳定性:

第一、在当前城市已经定居或者有定居打算,如果你买了房子有了生存压力,并且打算扎根在这里的话,肯定就是一个加分项了,因为这样的人一般是不敢轻易跳槽的,所以领导很放心。

第二、能力其实不需要太强,在互联网、私企混了很久的人其实有一个误区,能力越强越好。其实国企的领导心里有数的,第一这里不需要什么高精尖技术,第二,能力太强的呆不了多久就跑了。所以,包括我在内面试的时候也不需要能力太强,或者那种觉得自己能力强就沾沾自喜的人。这些人有更好的去处,我们这小庙就不供这座大佛了,所以如果是真国企的岗位其实像什么阿里P8的还真不愿意要,除非是集团领导直接招进来,普通岗位招进来真怕你卷我。

第三、有女朋友在这边,我同时就是典型的为了女朋友从杭州跳槽到成都来的,所以有这个理由也会很放心,觉得你过来就跑不掉了。

第三、年龄还不大或者年龄稍大从一线城市回来,这样的人一般都会选择在当前的城市定居的,面试的时候如果表现出定居的意图那么也会有一定优势。

想进国企,真的不要随便跳槽

其实现在很多私企都很在乎稳定性,什么五年三跳、三年两跳的都会成为简历上的劣势而被无情拒绝。

虽然我觉得这不是一个好的事情,尤其是私企本身人员变动就频繁你还要求人家有稳定性,这不闹着玩吗?

但是如果你未来想着跳槽进入一个不卷的国企平稳降落,那么一定跳槽就不要太平凡,国企这方面的逻辑和私企完全不同,私企是无理取闹,但是国企是真的希望人员稳定一点儿。

无论什么情况都坚持一些,保证自己的简历不要花掉,不然像这位学弟一样被拒绝就是一件很难受的事情了。

The End

其实这个学弟还是拿到了四川九洲的offer,但是他拿不准到底要不要去,怕又是一个很卷的军工国企。

我是这样劝他的,其实哪里工作都差不多,重点是你要能扛住工作的压力不要因为一些莫名其妙的小问题,玻璃心就离职。

哪怕你等着公司裁你还能拿一波n+1赔偿呢,所以有地方去就先呆着,先找到工作结束失业状态再说,先上岸再考虑其他的。

最后,也衷心希望他可以找到合适的工作。

想要了解更多成都央国企招聘、内推、简历辅导、面试辅导等信息,欢迎关注知识星球【成都央国企指南】,帮助应届生、社会人士等了解成都央国企信息,可进行一对一简历辅导、职业规面试辅导等。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

IDEA 重装我都会默默的下载这些好用的插件 IDEA 重装

发表于 2024-02-24

本文已经收录公众号:IDEA 重装我都会默默的下载这些好用的插件 (qq.com)

IDEA 重装我都会默默的下载这些好用的插件

这几年用得最多的插件,分享给大家。工欲善其事,必先利其器!每一次电脑重装,更换工作,我都会默默地打开我的插件收藏单,挨个下载。

注意:部分插件对 IDEA 的版本有最低要求!过多的插件会吃更多的内存,插件也要适可而止~

所有的插件都需要简单学习才能上手,装上了没用,用上了才有用!

1.1 arthas idea 和 ArthasHotSwap

核心功能:快速复制 arthas 命令

推荐理由:不必记忆和手动拼接命令行;准确又高效。是我日常中用得最多的插件之一

使用评价: 🌟🌟🌟🌟🌟

官网地址:plugins.jetbrains.com/plugin/1358…


其他补充: arthas 是解决 java 日常问题最最热门的工具🔧

配套插件:ArthasHotSwap arthas redefine

推荐理由:远端 refine, 热部署代码

使用评价: 🌟🌟🌟🌟🌟

关于 arthas 的一些高级用法,可以参考我的这篇文章:

工作六年,我学会了用 Arthas 来辅助我的日常工作 - 掘金 (juejin.cn)

1.2 JReble

核心功能:本地代码热部署

推荐理由:本地开发不用重复部署启动应用,有一些本地应用重新编译启动需要十来分钟,通过热部署大大节约开发时间。 日常中用得最多的插件之一

使用评价: 🌟🌟🌟🌟🌟

官网地址:plugins.jetbrains.com/plugin/4441…

其他补充:收费。 用了其他平替的热部署插件,还是这款深得我心

1.3 RestfulToolkit

核心功能:1. 根据 URL 直接跳转到对应的方法; 2. 通过 controller 复制出来对应的 url; 3. java 类转成 json 等

推荐理由:通过 url 找对应 controller 的方法;通过软件快速负责一个 JSON请求参数对象等。日常中用得最多的插件之一

使用评价: 🌟🌟🌟🌟🌟

官网地址:plugins.jetbrains.com/plugin/1029…

其他补充:第一张快速复制 url,第二张是快速根据 url 找到方法

1.4 Free Mybatis Plugin

核心功能:通过 mapper 找到对应的 xml,通过 xml 找到 mapper

推荐理由:mapper 和 xml 对应方法之间可以快速跳转,再也不用通过全文搜索的方式查找。日常中用得最多的插件之一

使用评价: 🌟🌟🌟🌟🌟

官网地址:plugins.jetbrains.com/plugin/8321…

其他补充:下面通过箭头进行跳转

1.5 Lombok

核心功能:简化代码; 比如减少 get、set等

推荐理由:不用写大量的 getter、setter代码,让代码更加整洁。非常喜欢 bulder 功能,日常中用得最多的插件之一

使用评价: 🌟🌟🌟🌟🌟

官网地址:plugins.jetbrains.com/plugin/6317…

1.6 maven helper

核心功能:对依赖的 jar 进行分析,可以定位冲突、查看依赖树

推荐理由:排除 jar 冲突;依赖通过图形化展示,日常中用得最多的插件之一

使用评价: 🌟🌟🌟🌟🌟

官网地址:plugins.jetbrains.com/plugin/7179…

其他补充:下面是使用截图,光下面这几个功能就值得点赞!

1.7 translation

核心功能:翻译软件

推荐理由:对于英文不太好的同学是一个非常不错的工具,不用再将单词复制到单独的翻译工具。当然对于生僻的单词更是克星!

使用评价: 🌟🌟🌟🌟

官网地址:plugins.jetbrains.com/plugin/8579…

其他补充:网络有限制。 平替插件: Chinese-English Translate,

但是翻译有时候不太行

1.8 PlantUML Integration

核心功能:通过 plantuml 语法绘制出对应的 uml 图

推荐理由: 喜欢绘 uml 时序图的同学是一个不错的选择;对于分析系统还是很好的工具!

使用评价: 🌟🌟🌟🌟

官网地址:plugins.jetbrains.com/plugin/8579…

其他补充:插件对时序图支持较好,其他支持 uml 图需要做一些配置!否则不能渲染!没有安装Graphviz,导致无法显示图像

plant uml 语法参考地址:plantuml.com/zh/

1.9 Alibaba Java Coding Guidelines

核心功能:根据阿里巴巴编码规范校验工程中的代码

推荐理由:对不规范的代码进行校验!约束编码,不规范的代码会有下划线提示!

使用评价: 🌟🌟🌟🌟

官网地址:plugins.jetbrains.com/plugin/1004…

其他补充:如果对于下划线部分的提示不想按照其规范,可以在配置中去掉!下次就不会提示了

1.10 Markdown

核心功能:提供 markdown 的编辑和预览

推荐理由: 读写 Readme 是一个不错的选择

使用评价: 🌟🌟🌟

其他补充:渲染能力一般,方便在代码工程中阅读和写 Readme。

1.11 Rainbow Brackets

核心功能:为{} ()等着色。

推荐理由:可以快速找对符号对

使用评价: 🌟🌟🌟🌟

官网地址:plugins.jetbrains.com/plugin/1008…

其他补充:对于大量的括号等情况,可以快速定位。不是一个必需的插件。

1.12 SpotBugs

核心功能:静态代码问题扫描检查

推荐理由:扫描静态代码快,快速发现可能有问题的代码快。可以分析单个文件,也可以分析具体包,模块等

使用评价: 🌟🌟🌟🌟

部分错误提示还是不错的。在对代码进行治理时可以用一用。类似的有 FindBugs 插件 plugins.jetbrains.com/plugin/3847…。 但是有最低版本要求!

1.13 Database Navigator

核心功能:数据库管理工具

推荐理由:方便快捷查询数据的表结构。可以支持 mysql、pg等,整体还是不错的!

使用评价: 🌟🌟🌟🌟

官网地址:plugins.jetbrains.com/plugin/1800…

其它补充:功能没有 navicat 强,但是足够用了。

1.14 AI code 插件

AI 代码助手,有下面几款插件。都需要注册账号

Github Copilot

docs.github.com/zh/copilot/…

核心功能:大模型助理编码

推荐理由:更加智能的编码,提高编码效率!用了几天,还不错

使用评价: 🌟🌟🌟🌟

官网地址:plugins.jetbrains.com/plugin/1771…

其他补充:试用 60 天,收费

TabNine

核心功能:代码补全、代码提示还是非常不错的!

官网地址:plugins.jetbrains.com/plugin/1279…

使用评价: 🌟🌟🌟🌟

其他补充:官网还提供了搜索代码的网站:www.tabnine.com/code?spm=at…

TONGYI Lingma

官网地址:plugins.jetbrains.com/plugin/1279…

使用评价: 🌟🌟🌟🌟

推荐理由:生成单测、注释等都不错

官网地址:plugins.jetbrains.com/plugin/1780…

特别说明:这些提示代码,可能和预期的代码还是有出入的。有时候生成的代码还需删除重写!对于我来说,写工具类是非常不错的;但业务代码还是得自己慢慢写

1.15 主题色相关的插件

与主题相关的插件,根据自己喜欢选择一款

插件 参考 官网地址
# Copilot Dark Theme plugins.jetbrains.com/plugin/1953…

关于主题相关, 插件市场有一个单独的模块可以下载:

plugins.jetbrains.com/search?tags…

1.16 其他插件

插件还有很多,不一一介绍了,下面的插件也用得比较少,感兴趣可以安装使用。

插件 描述 推荐 官网地址
GitToolBox 增加git功能,比如可以显示每一行代码的commit 🌟🌟🌟🌟 plugins.jetbrains.com/plugin/7499…
Mybatis Log(收费) 恢复 mybatis/ibatis sql日志为完整的可执行sql语句; 把sql日志里面的?替换为真正的值.等 🌟🌟🌟🌟 plugins.jetbrains.com/plugin/1390…
Private Notes 对源码添加注释,对喜欢研究源码的同学是一个不错的选择 🌟🌟🌟 plugins.jetbrains.com/plugin/1787…
JUnitGenerator 快速生成单元测试插件。但是生成的代码有时候差一点意思。不过可以节省大量时间,还是不错的。 🌟🌟🌟🌟 plugins.jetbrains.com/plugin/3064…
SequenceDiagram 快速生成时序图;但生成的颗粒度和预期的还是有差距。不过还是有一定的辅助作用 🌟🌟🌟 plugins.jetbrains.com/plugin/8286…
jclasslib Bytecode Viewer 字节码可视化。 可以将 Java类编译成字节码 🌟🌟🌟🌟🌟 plugins.jetbrains.com/plugin/9248…
CodeGlance Pro 右边展示代码整体框架的缩略图;用处不大。对于大类的代码,同步滑动还是可以的。 🌟🌟🌟 plugins.jetbrains.com/plugin/1882…
GenerateSerialVersionUID 一键为实现 Serializable 接口的类生成 SerialVersionUID 🌟🌟🌟 plugins.jetbrains.com/plugin/185-…

其他的插件没有用过或者用的很少,就不再推荐了。后续有再进行补充!

1.17 发现更多

可以通过插件市场搜索更多插件。通过查阅、下载使用等,找到一些适合自己的插件。Themes for IntelliJ-based IDEs | JetBrains Marketplace

最后:工具用得好,下班下得早,插件虽好,可不要贪杯哦, 不然 idea 会吃内存的!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

做副业要建立正反馈,先赚到一块钱

发表于 2024-02-24

必看大字最新消息重磅公众号首图(2).jpg

本文首发于公众号:嘟爷创业日记 。 我已经坚持日更120天+,欢迎过来追剧~

这两天副业社群进来的人挺多,我有向社群的小伙伴开放一些我的产品的分销,并告诉他们一个道理,【万物皆可CPS】,这不陆陆续续就有群友出单了。

图片

图片

我一直和大家提到正反馈这个词,因为它很重要,特别是对那些还没从副业上赚到钱的人尤其重要。我不是有GPT4账号服务吗,这个群友有同事需要,找我买,我看对方怎么转我原价,卖给同事原价,我这里给他指出来了,不能这么干,你应该让自己赚一点,不管多少,要能赚到钱这个正反馈,因为这个很重要。

赚到第一快钱是赚钱路上的质变,有了第一块钱的正反馈,你才会真的相信,原来除了打工上班赚工资,真的有其他赚钱的方式。

所以第一块钱,意义是很大的,麻雀虽小,但是闭环已经形成,流量-产品-变现。

不管你做任何项目,最终还是要围绕这个来展开,无非是赚多赚少的问题。

有了这个正反馈后,会让你爱上赚钱,赚钱其实没那么难,无非就是把合适的产品卖给需要的人。变现的路径其实越短越好,中间步骤如果太多,保不准哪一个步骤操作变形,导致你整体的变现概率大大降低。

很多人可能也尝试过一些项目,但是折腾了许久还是没赚到钱,在没有正反馈的时候,坚持是十分痛苦的事情,除非你有很强烈的赚钱欲望。最好的方法就是让正反馈来得快一点,那怎么来得快?

大家应该把大目标拆解成小目标,形成小而多次的正反馈,这样每一次的完成,那种成功的满足感会让你更有动力,我们打游戏的时候就是这样,为什么打游戏会上瘾,因为游戏开发商设计了一种让你可以及时满足的反馈。升级了你属性变强了,打到好装备了你又变强了,道理是相通。

我最近看到这样一段话,挺有意思:

因为穷开始在网上找各种方法,缺钱带来的强烈赚钱欲望,是一种巨大的赚钱优势,这种欲望会逼着你把自己放在一个很低的姿态和心理上,没什么赚钱欲望的人,稍微碰到点困难,遇到一些不符内心人设的事,就很容易退到原点,反正选择也多。

我回顾了下我为什么开始做副业,并且有这么强烈的赚钱欲望,好像还真是因为太穷了,吃过生活的苦,以后不想再吃了。所以就开干了, 并且这种欲望足够强烈,所以我比较耐揍,遇到困难了也没那么气馁。赚到钱的时候就非常开心,这样反复循环,自己的能力就加强了,现在我还是喜欢赚钱,不管多少。

大家做副业,心态要摆正,老手和新手的区别很大一部分是在心态上,新手遇到挫折容易放弃,老手是去总结问题重新上路。

失败是常态,重要的是收获。要学会从失败的痛苦中觉醒,找出自己的不足与提高空间。要学会在失败的满地狼藉中寻找可采纳的教训与启发,这些都是下一次胜利的基石。

我是嘟嘟MD,6年副业经验,酷爱睡后收入模式研究,累计副业收入破百万,如果大家爱看一些副业认知,点个赞,我以后多分享一些我的想法。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Coze 扣子 AI 养育计划 - "Flutter 大

发表于 2024-02-24

扣子(coze.cn)是一款用来开发新一代 AI Chat Bot 的应用编辑平台。其中可以构建自己的知识库以及作为资源,这样的话,让 AI Bot 拥有我所有文章的 “智慧”,岂不是一位 Flutter 大师 嘛。毕竟连我自己可能都记不清,很久以前文章里的知识细节,让用户和这种 “知识怪物” 交流,肯定能有意料之外的能力。

另外有一点很重要,让读者读完我所有的文章并理解是一件很艰难的事。

但读者可以向一个具有我所有文章知识的 Flutter 大师 提问,并获得回答,会是一件多么棒的事。


一、Flutter 大师的诞生

注册登录到扣子之后,可以通过 创建 Bot ,创建一位 Flutter 大师 的 “小婴儿”。在此为这个 AI Bot 起个小名叫: Toly

image.png

创建完后可以在个人空间,查看这位处于萌新阶段的 Flutter 大师 - Toly 。创建时可以 AI 根据描述生成图标还挺有意思的,虽然不那么精确。

image.png


点进去可以看到有三个主要的区域:

  • 编排区域 : 提示词的编排,设计 Toly 的人设和功能
  • 资源配置区域:
  • 预览调试区域:

image.png


二、喂养未来的 Flutter 大师

虽然扣子的 AI Bot 有一定的知识集,但是毕竟并不是专业的。现在看一下如何喂他一些精确的、高质量的文章。给与他专业的 “记忆” 能力。如下所示,可以构建知识库或者数据库,作为他的 “智慧源泉” 。

image.png


1. 创建知识库和收录数据

首先看一下知识库,在 个人空间 顶部有 知识库 的选项卡,其中可以添加知识库。 知识库中可以包含各个分类的知识集:

image.png

对于 Flutter 而言,最重要的是 Widget 组件的使用,这里拿 Flutter 组件集录 知识库为例。需要准备知识集,刚好本人写过各种 Flutter 组件使用的以及源码解析的文章,可以作为素材。

image.png

添加文章的 URL 即可:

image.png

然后会自动访问文章链接,进行处理:

image.png

最终分段情况如下:

image.png


2. 知识库的使用

在 记忆 区点击加号可以选择知识库:

image.png

下面是有无知识库时提问 详细介绍一下 ColorFiltered 组件的源码实现 的效果:

无知识库时 有知识库时
image.png image.png

可以看出无知识库时,基本上相当于瞎诌了;当添加知识库后,有相关的知识点,会总结知识库中的内容进行输出。从而回答更加精准,所以 知识库就相当于 Toly 的大脑。另外,当用户提问时,匹配到的 “知识片段” 将会命中

image.png


3. 其他形式的知识

文字作为知识的载体,万变不离其宗,在计算机中都是 字符资源 。除了通过 URL 爬取网页资源之外,还有很多其他的形式。

如下的 文本形式 和 表格形式 , 可以是 PDF、Text、DocX 、Excel 等形式的文件,也可以是 json 形式的 api ; 也可以自己编辑文本、表格作为知识库。

文本形式 表格形式
image.png image.png

每个网页、文件、接口内容被称之为 单元,每个单元中会进行分段,通过 URL 抓取的会自动分段。可以理解为每个 分段 就像一个神经元,遇到问题时想到了,它就被命中了一次。这样其实可以通过命中情况,来统计哪些神经元比较 “活跃”,感觉也挺有意思的。

image.png

俗话说,小孩就是四脚吞金兽,生儿难,养儿更难。接下来就是枯燥乏味的养儿过程了,喂食各种 Flutter 领域优秀的知识,来让 Toly 有一个强大的知识库。成为真正的 “Flutter 大师” 。


三、语义化数据库的支持

在记忆中有另一种 “知识” 的存储形式 – 数据库。 用户可通过自然语言插入和查询数据库中的数据,使用户可以便捷地与 Bot 进行交互。

image.png


1. 创建数据库和插入内容

可以创建表格来记录只是,比如这里创建 flutter_points 的记录表,由三个字段:标题、内容和类别:

image.png


然后通过自然语言的描述,就可以插入内容到数据库中:

名称: Flutter 升级的命令,内容: flutter upgrade,类别:命令行

名称: 查看 Flutter 版本,内容: flutter –version,类别:命令行

名称: flutter 三方库的官网,内容:https://pub.dev/ ,类别:资源

名称: flutter 开源地址,内容:https://github.com/flutter/flutter ,类别:资源

这样数据库中就可以添加内容:

image.png

通过自然语言描述,可以查询数据,还是挺有意思的。

image.png


2. 删除内容

同样,可以通过自然语言的描述,来删除或清空数据。可以在详情中看出,扣子是理解语义后通过 sql 进行操作的。

标题
image.png image.png

但是个人感觉目前的支持程度不太完善,只能支持一个数据库,而且有时候语句的识别不太精准。希望可以让开发者自定义一些标识符之类的,方便标识。清空数据库有时候还会出错,不过相信以后会完善的。

另外,不太清楚这个数据库是每个用户一份还是用一个,如果共用一个,不知道有没有权限控制,不然任何用户可以删除不太合理。


四、发布 Bot

养育完成之后,可以发布来让其他的人使用:

image.png

目前支持 豆包、飞书、微信服务号/客服,都需要进行筛选或者配置,详情可以参考文档:

image.png

发布到飞书非常简单,点击 配置 按钮,飞书登录后获取应用后授权即可:

image.png

发布之后,别人就可以在飞书应用中搜索到 Flutter 大师 的机器人:

image.png

然后愉快地玩耍吧 ~

image.png


在扣子的 Bots 页面中,可以在学习助手中搜索到 Flutter 大师

image.png

使用中发现飞书应用好像不支持数据库的能力,在扣子 里可以:

image.png

那扣子的初体验就到这里,总得来说知识库和数据库的记忆能力还是让我很感兴趣的。目前 “Flutter 大师” 还在小白阶段,让我慢慢养育吧 ~


最后小结

扣子给我们带来了什么?

让普通人可以通过自定义知识数据库,来 “养育” 专业领域 AI 智慧体的机会!


最后对扣子的一些建议和小畅想:

  • 指定格式,可以支持批量导入 url,或者掘金可以让作者将文章、专栏导出到 Coze 知识库的功能。
  • 数据库希望未来能够加强,这是个很不错的特色,有了数据库,可以玩很多花样。
  • 掘金小册&Coze 可以打造某本小册专有的 Bot ,喂养小册内容,仅小册购买者可以使用,感觉会挺不错。
  • 网页抓取文章时,图片有时是很重要的。后期可以对图片资源识别,作为资料。或作为问答中可以输出相关图片。
  • 对知识库中的分段命中情况提供一些统计图的支持,方便可视化地查看命中情况。

bot ID: 7338763773840375842

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

尊嘟假嘟~常见 Redis 大key,热key解决方案 一:

发表于 2024-02-23

一:什么是Redis 大Key

在Redis中,”big key” 指的是一个占用较多内存空间的键。当一个键的值占用的内存超过了Redis的配置阈值时,它就被认为是一个 “big key”。大的键可以导致一些性能问题,因为它们需要更多的内存和网络带宽来存储和传输。

image.png

“big key” 可能会对Redis的性能产生负面影响,因为它们可能导致内存碎片,增加数据传输的成本,降低缓存的效率等。一些常见导致大键问题的场景包括:

  1. 字符串过大: 当字符串值的大小远远超过了Redis的配置限制时,它可能成为一个大键。
  2. Hash、Set、List等数据结构的元素过多: 当一个Hash、Set或List中的元素数量非常庞大时,也可能导致它成为一个大键。

为了避免 “big key” 问题,可以采取以下一些措施:

  1. 合理设置内存限制: 在Redis的配置中,可以设置最大使用的内存限制,通过合理设置该值,可以防止大键问题对系统造成过大的影响。
  2. 合理使用数据结构: 选择合适的数据结构来存储数据,避免在单个键中存储过多的元素。
  3. 定期清理不需要的数据: 对于不再需要的大键,可以定期清理或采取适当的缓存策略,以确保内存得到有效利用。
  4. 使用分布式缓存: 在某些情况下,将大量数据分布到多个Redis节点上,以减轻单个节点的负担。

了解和监控大键问题对于维护Redis性能是非常重要的。可以使用Redis的监控工具,如Redis的INFO命令或第三方监控工具,来跟踪内存使用情况和识别潜在的大键。

redis中有常见的几种数据结构,每种结构对大key的定义不同,比如:

  • value是String类型时,size超过10KB
  • value是ZSET、Hash、List、Set等集合类型时,它的成员数量超过1w个

上述的定义并不绝对,主要是根据value的成员数量和字节数来确定,业务可以根据自己的场景也确定标准。

二:针对于redis不同的数据结构怎么解决大key问题

2.1: String

处理大键(big key)问题对于字符串(String)数据结构可以采取以下一些方案:

  1. 数据分割: 如果字符串的值非常大,可以考虑将其分割为多个小的字符串,并使用多个键来存储这些小字符串片段。
1
2
3
4
5
ruby复制代码# 原始大字符串
SET user:1:description "Very long description..."
# 分割后的小字符串片段
SET user:1:description:part1 "Very long"
SET user:1:description:part2 " description..."
  1. 使用压缩算法: 对于文本型的数据,可以考虑使用Redis支持的压缩功能,如zstd压缩算法,以减小存储空间。
1
2
ruby复制代码# 使用压缩算法
SET user:1:description "Compressed description..." ZSTD

注意:需要在客户端应用中进行解压缩,因为Redis本身并不会自动解压缩存储的值。
3. 使用其他数据结构: 如果字符串的值具有结构化信息,可以考虑使用其他数据结构,如Hash或List。这样可以更好地组织数据并避免一个键变得过大。

1
2
ruby复制代码# 使用Hash
HSET user:1:info description "Very long description..."
  1. 数据清理: 定期清理不再需要的字符串数据,以确保只保留必要的信息。
1
2
ruby复制代码# 定期清理字符串数据
DEL user:1:description:old

选择适当的方法取决于应用的具体需求和访问模式。需要根据数据的特性来选择合适的数据结构和拆分策略,以避免大键问题。

2.2: List

处理大键(big key)的问题对于列表(List)结构可以采取以下一些策略:

  1. 分割列表: 如果列表中包含的元素过多,可以考虑将列表分割成多个小的列表。每个小列表只包含部分元素,这样可以降低单个列表的长度。
1
2
3
4
5
6
7
8
ruby复制代码# 原始大列表
LPUSH user:1:activity_log item1
LPUSH user:1:activity_log item2
LPUSH user:1:activity_log item3
# 分割后的小列表
LPUSH user:1:activity_log:part1 item1
LPUSH user:1:activity_log:part1 item2
LPUSH user:1:activity_log:part2 item3
  1. 定期修剪: 定期检查列表,删除掉不再需要的元素,以保持列表的合理大小。可以使用LTRIM命令来截取列表,保留指定范围内的元素。
1
2
ruby复制代码# 定期修剪列表
LTRIM user:1:activity_log 0 99

上述命令保留了列表中的前100个元素,删除了其余的元素。
3. 使用其他数据结构: 如果列表中的元素有一定的关联性或结构化信息,考虑使用其他数据结构,如有序集合(Sorted Set)或哈希(Hash)。这样可以更灵活地管理数据,避免一个大列表的问题。

1
2
3
ruby复制代码# 使用有序集合
ZADD user:1:activity_log timestamp1 item1
ZADD user:1:activity_log timestamp2 item2
  1. 使用Stream数据类型: 如果数据是事件流的形式,可以考虑使用Redis Streams,它是一种支持持久化、多消费者、多生产者的数据结构,能够有效地处理事件流。
1
2
3
bash复制代码# 使用Stream
XADD user:1:activity_log * type item1
XADD user:1:activity_log * type item2

选择适当的方法取决于应用的具体需求。在设计时,需要根据数据的特性和访问模式选择合适的数据结构和拆分策略,以避免大键问题。

2.3: Set

对于集合(Set)数据结构,处理大键(big key)问题可以采取以下一些方案:

  1. 分割集合: 如果集合中元素很多,可以考虑将其分割成多个小的集合。每个小集合只包含部分元素,这样可以降低单个集合的大小。
1
2
3
4
5
6
7
8
ruby复制代码# 原始大集合
SADD user:1:interests interest1
SADD user:1:interests interest2
SADD user:1:interests interest3
# 分割后的小集合
SADD user:1:interests:part1 interest1
SADD user:1:interests:part2 interest2
SADD user:1:interests:part2 interest3
  1. 使用有序集合(Sorted Set): 如果集合中的元素有一定的顺序关系,可以考虑使用有序集合来存储数据。这样可以更方便地处理一部分数据,而不是将所有元素存储在一个集合中。
1
2
3
4
ruby复制代码# 使用有序集合
ZADD user:1:interests 1 interest1
ZADD user:1:interests 2 interest2
ZADD user:1:interests 3 interest3
  1. 使用HyperLogLog: 如果集合的目的是为了计算基数(元素的不重复数量),可以考虑使用HyperLogLog数据结构。它可以在极大规模的数据集合上估计基数,而且占用的内存相对较小。
1
2
ruby复制代码# 使用HyperLogLog
PFADD user:1:interests interest1 interest2 interest3
  1. 定期清理不需要的元素: 定期检查集合,删除掉不再需要的元素,以保持集合的合理大小。可以使用SREM命令来移除指定的元素。
1
2
ruby复制代码# 定期清理集合元素
SREM user:1:interests unwanted_interest

选择合适的方案取决于应用的具体需求。在设计时,需要根据数据的特性和访问模式选择合适的数据结构和拆分策略,以避免大键问题。

2.4: Hash

针对Hash数据结构的大键问题,以下是一些具体的方案:

  1. 字段分组: 如果一个哈希中的字段数量庞大,可以考虑将字段进行适当的分组,然后使用多个哈希键来存储这些分组。例如,将字段按照某种规则划分为多个小组,然后每个小组使用一个独立的哈希键存储。
1
2
3
4
5
6
7
8
sql复制代码# 原始大哈希
HSET user:1 name John
HSET user:1 email john@example.com
HSET user:1 age 30
# 分组后的哈希
HSET user:1:info name John
HSET user:1:info email john@example.com
HSET user:1:info age 30
  1. 分散数据: 将大哈希拆分为多个小的哈希,每个小哈希存储一部分字段。这样可以将数据分散到多个哈希键中,降低单个哈希键的大小。
1
2
3
4
5
6
7
8
sql复制代码# 原始大哈希
HSET user:1 name John
HSET user:1 email john@example.com
HSET user:1 age 30
# 分散后的哈希
HSET user:1:info name John
HSET user:1:contact email john@example.com
HSET user:1:profile age 30
  1. 使用多个哈希键: 将相关的信息存储在多个独立的哈希键中,而不是一个大哈希中。这样可以避免一个键变得过大。
1
2
3
4
ruby复制代码# 使用多个哈希键
HSET user:1:info name John
HSET user:1:contact email john@example.com
HSET user:1:profile age 30
  1. 定期清理不需要的字段: 定期检查哈希中的字段,清理掉不再需要的字段。这可以通过HDEL命令来实现。
1
2
ruby复制代码# 定期清理不需要的字段
HDEL user:1:info unwanted_field

选择合适的方案取决于具体的业务需求和数据访问模式。这些方案可以根据应用程序的情况来调整和组合,以解决大Hash键问题。

2.4: Sorted Set

对于有序集合(Sorted Set)数据结构,处理大键(big key)问题可以采取以下一些方案:

  1. 分割有序集合: 将一个大的有序集合分割成多个小的有序集合,每个小集合只包含部分元素。这样可以降低单个有序集合的大小。
1
2
3
4
5
6
7
8
ruby复制代码# 原始大有序集合
ZADD user:1:scores 100 "Alice"
ZADD user:1:scores 200 "Bob"
ZADD user:1:scores 300 "Charlie"
# 分割后的小有序集合
ZADD user:1:scores:part1 100 "Alice"
ZADD user:1:scores:part2 200 "Bob"
ZADD user:1:scores:part3 300 "Charlie"
  1. 按分数范围存储: 如果有序集合的元素有一定的范围,可以将元素按照分数范围存储在不同的有序集合中,以减小每个有序集合的大小。
1
2
3
4
ruby复制代码# 按分数范围存储
ZADD user:1:scores:0to100 100 "Alice"
ZADD user:1:scores:101to200 200 "Bob"
ZADD user:1:scores:201to300 300 "Charlie"
  1. 定期修剪: 定期检查有序集合,删除掉不再需要的元素,以保持有序集合的合理大小。可以使用ZREMRANGEBYRANK或ZREMRANGEBYSCORE命令来移除一定范围内的元素。
1
2
ruby复制代码# 定期修剪有序集合
ZREMRANGEBYRANK user:1:scores 0 99
  1. 使用其他数据结构: 如果有序集合不再适用,可以考虑使用其他数据结构,如哈希(Hash)或字符串(String),具体取决于数据的特性和访问模式。
1
2
ruby复制代码# 使用哈希或字符串
HSET user:1:info score 100

选择合适的方案取决于应用的具体需求。在设计时,需要根据数据的特性和访问模式选择合适的数据结构和拆分策略,以避免大键问题。

三:热key解决方案

热 Key 问题解决方案

增加 Redis 实例复本数量

对于出现热 Key 的 Redis 实例,我们可以通过水平扩容增加副本数量,将读请求的压力分担到不同副本节点上。

二级缓存(本地缓存)

当出现热 Key 以后,把热 Key 加载到系统的 JVM 中。后续针对这些热 Key 的请求,会直接从 JVM 中获取,而不会走到 Redis 层。这些本地缓存的工具很多,比如 Ehcache,或者 Google Guava 中 Cache 工具,或者直接使用 HashMap 作为本地缓存工具都是可以的。

使用本地缓存需要注意两个问题:

  • 如果对热 Key 进行本地缓存,需要防止本地缓存过大,影响系统性能;
  • 需要处理本地缓存和 Redis 集群数据的一致性问题。

热 Key 备份

通过前面的分析,我们可以了解到,之所以出现热 Key,是因为有大量的对同一个 Key 的请求落到同一个 Redis 实例上,如果我们可以有办法将这些请求打散到不同的实例上,防止出现流量倾斜的情况,那么热 Key 问题也就不存在了。

那么如何将对某个热 Key 的请求打散到不同实例上呢?我们就可以通过热 Key 备份的方式,基本的思路就是,我们可以给热 Key 加上前缀或者后缀,把一个热 Key 的数量变成 Redis 实例个数 N 的倍数 M,从而由访问一个 Redis Key 变成访问 N * M 个 Redis Key。 N * M 个 Redis Key 经过分片分布到不同的实例上,将访问量均摊到所有实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ini复制代码// N 为 Redis 实例个数,M 为 N 的 2倍
const M = N * 2
//生成随机数
random = GenRandom(0, M)
//构造备份新 Key
bakHotKey = hotKey + "_" + random
data = redis.GET(bakHotKey)
if data == NULL {
data = redis.GET(hotKey)
if data == NULL {
data = GetFromDB()
// 可以利用原子锁来写入数据保证数据一致性
redis.SET(hotKey, data, expireTime)
redis.SET(bakHotKey, data, expireTime + GenRandom(0, 5))
} else {
redis.SET(bakHotKey, data, expireTime + GenRandom(0, 5))
}
}

在这段代码中,通过一个大于等于 1 小于 M 的随机数,得到一个 bakHotKey,程序会优先访问 bakHotKey,在得不到数据的情况下,再访问原来的 hotkey,并将 hotkey 的内容写回 bakHotKey。值得注意的是,bakHotKey 的过期时间是 hotkey 的过期时间加上一个较小的随机正整数,这是通过坡度过期的方式,保证在 hotkey 过期时,所有 bakHotKey 不会同时过期而造成缓存雪崩。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

这才开工没几天收到Offer了,简历改的好,找工作没烦恼。

发表于 2024-02-23

喜报喜报

这才开工没几天,就收到了喜报!

就像上面截图中所说的一样:简历改了真的有用。

我也和大家分享一下优化简历的技巧,希望对大家有帮助,把握住金三银四的机会,都能顺利上岸,升职加薪!

思路很重要

我们以Go后端开发工程师的简历举例子,其他岗位也可以参考我的思路。

思路是最重要的:其他岗位的技术栈,知识点根据你的情况替换掉就好了。

我按顺序介绍一下,简历中最重要的几个模块怎么写:

专业技能(个人优势):

  1. 熟练使用Go进行项目开发,对Slice、Map、Goroutine、Channel有深入了解,擅长并发编程;
  2. 熟练使用Gin、GoFrame、Go-zero、Go-micro、kratos等web和微服务框架,熟悉熔断,限流,服务治理等;
  3. 深入理解CSP模型,深入理解GC垃圾回收机制和三色标记法,有过实际调优经验;
  4. 熟悉MySQL的存储引擎、事务隔离级别、锁、索引,有MySQL的性能调优经验;
  5. 熟悉 Redis 持久化机制、过期策略以及集群部署;
  6. 熟悉RabbitMQ消息队列事务消息底层原理,掌握消息丢失、消息重复等问题的解决方案;
  7. 熟练使用Docker和K8S,有CICD经验;擅长敏捷开发;熟练使用常见的设计模式;
  8. 精通基于 LNMP 环境的编程,具备扎实的PHP基础知识,理解面向对象编程思想。
  9. 熟练使用 Yii、ThinkPHP和Laravel等框架进行快速开发,了解基本的核心源代码,熟悉swoole 扩展。

(注意:8和9的思路是这样的,如果有其他语言的开发经验,像上面的PHP经验一样写在最后)

注意:

  • 上面提到的Gin GoFrame GoZero的框架,结合自己情况修改或者删减,保留自己擅长的框架
  • 上面提到的NSQ、Kafka、RabbitMQ,结合自己的情况修改或者删减,保留自己擅长的技术栈

工作经历:

  1. 最终目的是让面试官觉得你之前任职的公司挺牛逼,你负责的工作也挺牛逼
  2. 可以这么写:2行之内写清楚之前所在的公司所属行业(教育、电商),是什么类型的公司(大厂、集团公司、国企),自己在公司负责什么(技术研发、团队管理、和客户对接等等),达到了什么效果,你或者公司取得了什么业绩。
  3. 比如:2018年加入xxxx(中国xxx视频第一门户),做技术负责人,负责公司自有全平台的架构设计和开发,包括:网站+APP+小程序+CMS+公众号+运营系统。是一位懂产品设计的技术负责人。

项目介绍:

  1. 一定尽可能多的写清楚技术栈,比如:go+gozero+etcd+mysql+redis+kafka+elasticsearch+docker+k8s
  2. 用最通俗易懂的话介绍清楚项目,不要超过2行。你就想给自己父母怎么介绍你做的项目,他们能听懂,面试官(HR)就肯定能听懂了。
  3. 工作内容:
* 用xxx技术,解决了xxx问题
  1. 工作业绩:
* 站在公司的角度:你做的哪些事情,为公司降本增效了
* 站在团队的角度:你做的哪些事情,提高团队的效率了
* 站在项目的角度:你做的哪些事情,提高项目的稳定性了,提高接口响应速度了
* 站在技术的角度:你攻克了哪些技术难题,做了哪些有亮点、有难点的事情

思维导图梳理法:

  1. 如果实在想不清楚自己项目的难点和亮点,建议用下面思维导图的方式,尽可能多的补充情况功能点、以及每个功能点下对应的业务场景和技术方案。
  2. 基于思维先做加法,再做减法。先尽可能多的列出,整理,然后再合并总结。这样就不发愁简历中没有可写的了。

当然,你也可以私信我,我给你支支招。

自我评价(加分项):

  1. 自己的博客地址
  2. 自己参加过的开源项目
  3. 以上这两个是最强有力的加分项,如果没有也要通过例子来证明自己“热爱技术”,而不是干巴巴一句“热爱技术”。

好了,以上就是我给大家优化简历的建议。

我们 就业训练营 和 升职加薪星球 的朋友们按照我的思路优化简历之后,都起到了很好的效果。

以上内容如果对你有帮助,欢迎点赞、留言、关注。

更欢迎你转载分享出去:送人玫瑰,手留余香嘛。

又出成绩啦

也欢迎你了解一下我们的就业训练营,辅导到你找到工作为止的那种:

我们又出成绩啦!大厂Offer集锦!遥遥领先!

这些朋友赢麻了!

这是一个专注程序员升职加薪の知识星球

答疑解惑

需要「简历优化」、「就业辅导」、「职业规划」的朋友可以在掘金私信我。

一对一辅导的那种呦!

面试真题共享群

对了,我们准备搞一个金三银四面试真题共享群,互通有无,一起刷题进步,没准能让你能刷到自己意向公司的最新面试题呢。

感兴趣的朋友们可以在掘金私信我。

或者直接加我微信:wangzhongyang1993

关注我的同名公众号:王中阳Go

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…555657…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%