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

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


  • 首页

  • 归档

  • 搜索

你知道吗?axios 请求是 JSON 响应优先的

发表于 2024-04-21

本文是“axios源码系列”第二篇。上文我们介绍了 axios 中是如何实现取消请求 的,本文我将介绍另一个话题:axios 是 JSON 响应优先的。

那为什么这么说呢?我们能从 2 个方面的表现进行阐述。

表现一:默认请求头

你可能没有注意到,每个 axios 请求默认会有 2 个请求头配置,位于 lib/defaults/index.js。

1
2
3
4
5
6
7
8
9
10
js复制代码// /v1.6.8/lib/defaults/index.js#L144
const defaults = {
// ...
headers: {
common: {
'Accept': 'application/json, text/plain, */*',
'Content-Type': undefined
}
}
}
  1. 首先,明确告知服务器,我能接受 JSON 数据响应。
  2. 其次,不发送 Content-Type 头信息。

这块配置会在请求发出前,与用户传入的 config.headers 进行合并。

1
2
js复制代码// /v1.6.8/lib/core/Axios.js#L72
config = mergeConfig(this.defaults, config);

接着扁平化,将 config.headers 处理成 AxiosHeaders 实例对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
js复制代码// /v1.6.8/lib/core/Axios.js#L100-L113
// Flatten headers
let contextHeaders = headers && utils.merge(
headers.common,
headers[config.method]
);

headers && utils.forEach(
['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
(method) => {
delete headers[method];
}
);

config.headers = AxiosHeaders.concat(contextHeaders, headers);

AxiosHeaders 类似于标准 Headers API,提供了各种操作头信息的方法,比如 get()、set()、concat() 等。

看下最终请求头效果。

这就是 axios 是 JSON 响应优先的第一个表现。

表现二:响应数据处理

不过重要的还是第二个表现,即在处理响应数据的时候。

我们都知道,我们可以为 axios 请求指定 responseType 配置项。以下面代码为例:

1
2
3
4
5
6
7
js复制代码axios.get('https://httpstat.us/200', {
responseType: 'json'
})
.then(res => {
console.log(res)
// { data: {code: 200, description: 'OK'} }
})

这里我们告知 axios,响应数据是 JSON 格式的,需要处理成 JSON 对象给我们。

但这个配置项是可选的,因为 axios 默认就会把响应数据看做 JSON 格式处理。

这块逻辑跟另一个配置项 transformResponse 有关。

如果你没有自定义 transformResponse 这个配置项,那么它的默认逻辑如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
js复制代码// /blob/v1.6.8/lib/defaults/index.js#L99
const defaults = {
// ...
transformResponse: [function transformResponse(data) {
const JSONRequested = this.responseType === 'json';

if (
typeof data === 'string' &&
(!this.responseType || JSONRequested)
) {
try {
return JSON.parse(data);
} catch (e) {}
}

return data;
}],
// ...
}

transformResponse 配置项接收请求返回的响应数据 data,决定最终返回的数据类型。而 data 的来源如下(以浏览器端实现为例)。

1
2
3
4
js复制代码// /v1.6.8/lib/adapters/xhr.js#L100
const responseData = !responseType || responseType === 'text' || responseType === 'json'
? request.responseText
: request.response;

这里的 responseType,是从传入用户的 config 里获取的。

1
2
js复制代码// /v1.6.8/lib/adapters/xhr.js#L52
let {responseType, withXSRFToken} = config;

观察就能知道:当传入的 config 没有包含 responseType,或 responseType 值为 ‘text’ 或 ‘json’ 时,data 即 request.responseText,也就是一个字符串。

回到刚才的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
js复制代码transformResponse: [function transformResponse(data) {
const JSONRequested = this.responseType === 'json';

if (
typeof data === 'string' &&
(!this.responseType || JSONRequested)
) {
try {
return JSON.parse(data);
} catch (e) {}
}

return data;
}],

如果 responseType 没有设置,或者 responseType 被设置成 ‘json’。那么就以 JSON.parse(data) 方式处理并返回。

看,这里的 data 属性就是一个 JS 对象了。

如果把 responseType 被设置成 ‘text’,返回的就是没经过任何处理的字符串了。

1
2
3
js复制代码axios.get('https://httpstat.us/200', {
responseType: 'text'
})

效果如下。

这就是 axios 是 JSON 响应优先的第二个表现,也是直接原因。

总结

本文带大家了解了 axios 的另一个特性,它是 JSON 响应优先的。也就是说,JSON 响应在 axios 中是“一等公民”。

这并不难理解,因为目前几乎所有前端项目都在使用 JSON 格式返回响应数据。因此,这种默认设置也减少了一些样板代码的编写,提升了开发体验。

本文转载自: 掘金

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

2024-4-19 群讨论:GraalVM 与 JVM 使用

发表于 2024-04-20

以下来自本人拉的一个关于 Java 技术的讨论群。关注公众号:hashcon,私信进群拉你

GraalVM Native Image 的进程能否被 jps 看到?

感谢 dreamlike_ocean ( space.bilibili.com/8227104 )指正

如果编译参数里面开启了 jstat,jmx 等特性(--enable-monitoring=jmxserver,jmxclient,jvmstat),就能看到,没有开启就看不到。

当前对于 GraalVM 与 JVM 的使用

  1. 针对 Lambda 类型任务(例如不频繁的但是处理数据比较多的定时任务,如每周报表,以及临时的数据导出任务),使用 GraalVM Native Image:
    1. 这种任务不适合放在常驻的微服务,浪费资源:
      1. 微服务要按照这个定时任务的标准设计内存和 CPU,很长时间是不用这么多的。
      2. 要节约,并且放在常驻的微服务,需要定时任务启动前,以更高的内存和 CPU 重启微服务,定时任务结束后回复原来的参数重启微服务。
    2. 适合放在 k8s cronjob 或者 AWS Lambda 这种服务,但是这些服务都对启动时间有很高有求,需要在一定时间内启动好并告诉容器就绪了。
    3. 这种的适合 GraalVM Native Image,一是对启动时间有要求,二是一般依赖的库比常驻微服务简单很多,改造起来简单很多。
  2. 针对常驻的微服务,还是使用 JVM:
    1. 针对涉及很多存储 io 连接的微服务,先不要使用 CRaC 机制,先只使用 CDS 加速类加载,然后可以考虑用 Graal JIT 替代 C2 编译器。
    2. 针对没有很多存储 io 连接的微服务,使用 CRaC,例如 web 引擎微服务,网关微服务,广告微服务(大部分访问在本地缓存),这些正好是对于流量更敏感的微服务,需要在流量到来时,快速扩容并启动。

个人简介:个人业余研究了 AI LLM 微调与 RAG,目前成果是微调了三个模型:

  1. 一个模型是基于 whisper 模型的微调,使用我原来做的精翻的视频按照语句段落切分的片段,并尝试按照方言类别,以及技术类别分别尝试微调的成果。用于视频字幕识别。
  2. 一个模型是基于 Mistral Large 的模型的微调,识别提取视频课件的片段,辅以实际的课件文字进行识别微调。用于识别课件的片段。
  3. 最后一个模型是基于 Claude 3 的模型微调,使用我之前制作的翻译字幕,与 AWS、Go 社区、CNCF 生态里面的官方英文文档以及中文文档作为语料,按照内容段交叉拆分,进行微调,用于字幕翻译。

目前,准确率已经非常高了。大家如果有想要我制作的视频,欢迎关注留言。

本人也是开源代码爱好者,贡献过很多项目的源码(Mycat 和 Java JFRUnit 的核心贡献者,贡献过 OpenJDK,Spring,Spring Cloud,Apache Bookkeeper,Apache RocketMQ,Ribbon,Lettuce、 SocketIO、Langchain4j 等项目 ),同时也是深度技术迷,编写过很多硬核的原理分析系列(JVM)。本人也有一个 Java 技术交流群,感兴趣的欢迎关注。

另外,一如即往的是,全网的所有收益,都会捐赠给希望工程,坚持靠爱与兴趣发电。

本文转载自: 掘金

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

axios 是如何实现取消请求的?

发表于 2024-04-20

axios 是现在前端项目中最常使用的一个请求库,目前 Github star 已经达到了 104k star。

最近一段时间在看 axios 仓库源码,于是起了一个写“axios 源码系列”的念头,本文是这个系列的第一篇。

源码系列的安排

我对这个源码系列的安排,不是循规蹈矩的从头说起,而是在阅读源码的过程当中,突然想写哪块内容我就写哪块。

不过,在座或在站的读者不用担心阅读的连贯性。这个系列的每篇文章内容相对独立,这样方便大家按需阅读。另外,每篇文章也会写得比较简短,保证大家在 5 分钟内看完,同时又能有所收获。

好了,书归正传。

本文我们讨论的问题是——axios 是如何实现取消请求(Cancel requests)的?

在 axios 中取消请求

axios 是这样取消请求的。

1
2
3
4
5
6
7
8
9
10
js复制代码const controller = new AbortController();

axios.get('https://httpstat.us/200', {
signal: controller.signal
}).catch(error => {
// 2) 在 catch 中捕获错误
console.log(error) // { message: 'canceled', name: 'CanceledError', code: 'ERR_CANCELED' }
});
// 1) 取消请求
controller.abort()

AbortController 是在 DOM 标准中定义的 API,统一了取消类操作的实现,在浏览器端已经受到广泛支持了。

取消请求的实现

取消请求的实现位于 lib/adapters/xhr.js(暂不考虑 Node.js 实现)。

1
2
3
4
5
6
js复制代码// /v1.6.8/lib/adapters/xhr.js#L244
if (config.signal) {
config.signal.aborted
? onCanceled() :
config.signal.addEventListener('abort', onCanceled)
}

内部会判断是否传入了 signal 配置项。如果传入了,就监听 controller.signal 上的 abort 事件,这个事件会在调用 controller.abort() 方法时触发。

不过,还有一种可能是传入 controller.signal 时,先前就已经调用了 controller.abort(),这样 abort 事件永远都不会触发了。

不过没有关系,调用了 controller.abort() 后,controller.signal.aborted 属性会置为 true,因此可以通过 aborted 属性,确保 onCanceled 回调函数始终被调用。

onCanceled 处理函数内容如下。

1
2
3
4
5
6
7
8
js复制代码// /v1.6.8/lib/adapters/xhr.js#L234
onCanceled = cancel => {
reject(!cancel || cancel.type
? new CanceledError(null, config, request)
: cancel
);
request.abort();
};

调用 controller.abort() 后,处理函数 onCanceled 会接受一个 cancel Event 参数,结构类似 { type: ‘abort’ } 这样,然后就会返回 axios 自定义的 CanceledError 错误。

这里有 2 个知识点:

  1. axios 对接口请求过程使用 new Promise((resolve,reject)){…} 进行了封装,确保始终返回 Promise 对象,这里的 reject(…) 表示“请求以失败告终”
  2. 至于 reject(cancel) 的分支逻辑,是为了兼容 CancelToken 的写法,这是旧的取消请求的实现,不过现在已经弃用了,新项目中不要再用了

最后,调用 request.abort(),这是实际终止当前请求的地方。

request 就是 XMLHttpRequest 实例,abort() 是 XMLHttpRequest 天然支持的用于终止请求的方法。

1
2
js复制代码// /v1.6.8/lib/adapters/xhr.js#L76
let request = new XMLHttpRequest();

到此,我们就完成了 axios 取消请求的实现介绍。你是否看懂了呢?

总结

取消请求是 axios 提供的核心功能之一,本文我们介绍了它在浏览器端的实现。

axios 在浏览器端是使用 XMLHttpRequest API 提供请求能力的,其实例上提供了 abort() 方法用于终止请求,而 axios 就是利用这一点跟 AbortController API 配合实现请求的取消的。

本文转载自: 掘金

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

面试不慌张:一文读懂FactoryBean的实现原理 1、F

发表于 2024-04-20
大家好,我是石头~


在深入探讨Spring框架内部机制时,FactoryBean无疑是一个关键角色,也是面试中经常出现的熟悉面孔。


不同于普通Java Bean,FactoryBean是一种特殊的Bean类型,它的存在并非为了提供业务逻辑,而在于它具备动态创建和返回其他Bean实例的能力。


这一特性使Spring容器在管理Bean时拥有了更高的灵活性和智能化水平。

u=459711531,4262691532&fm=253&fmt=auto&app=138&f=PNG.png

1、FactoryBean接口的核心方法解析

FactoryBean接口的核心方法主要包括以下几个:

99714.png

  • T getObject() throws Exception: 这个方法是FactoryBean的核心实现,负责创建并返回所需的对象实例。具体实现时,开发者可以根据业务需求编写逻辑来构造Bean对象。例如,如果需要创建一个复杂对象,或者需要基于某些条件动态地生成不同的Bean,那么在这个方法中就可以实现这些复杂的创建逻辑。
  • Class<T> getObjectType(): 返回由FactoryBean创建的Bean的类型。Spring容器利用此方法来确定Bean的类型,以便正确处理依赖关系和类型转换。
  • boolean isSingleton(): 指定由FactoryBean创建的Bean是否为单例。若返回true,则Spring容器会在整个应用程序上下文中仅创建一次Bean实例;若返回false,则每次请求都会创建一个新的Bean实例。

2、FactoryBean的工作流程

当Spring容器解析配置并遇到FactoryBean定义时,它并不直接实例化FactoryBean作为最终的Bean去使用。取而代之的是,容器会调用FactoryBean的getObject()方法,以此来获取真正需要的Bean实例。


以下是FactoryBean发挥作用的具体步骤:
  1. 初始化FactoryBean实例:Spring容器负责创建并初始化FactoryBean对象。
  2. 执行Bean创建逻辑:容器随后调用该FactoryBean的getObject()方法,按照预定义的规则或用户自定义逻辑生产目标Bean。
  3. 遵守Bean生命周期:由FactoryBean生成的Bean同样遵循Spring Bean的标准生命周期,包括但不限于依赖注入、初始化回调等过程。
  4. 交付目标Bean:最终,客户端从Spring容器获取的实际上是FactoryBean通过getObject()方法生产的Bean,而非FactoryBean自身。

image.png

3、FactoryBean示例

下面是一个简单的Java代码示例,展示了如何实现一个自定义的FactoryBean来创建一个`Book`对象:
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
Java复制代码    import org.springframework.beans.factory.FactoryBean;
import org.springframework.stereotype.Component;

// 定义一个简单的书本实体类
public class Book {
private String name;
private double price;

// 构造函数、getter和setter省略...

// 输出书本信息
@Override
public String toString() {
return "Book{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}
}

// 实现FactoryBean接口以自定义Book对象的创建过程
@Component
public class BookFactoryBean implements FactoryBean<Book> {

@Override
public Book getObject() throws Exception {
// 在此处实现具体的创建逻辑
Book book = new Book();
book.setName("Spring in Action");
book.setPrice(49.99);
return book;
}

@Override
public Class<?> getObjectType() {
return Book.class;
}

@Override
public boolean isSingleton() {
// 假设此Book Bean为单例
return true;
}
}

// 在Spring配置或注解驱动的环境下,我们可以像这样通过FactoryBean获取Book对象
// Spring容器会调用BookFactoryBean.getObject()方法来创建Book实例
// 而不是直接创建BookFactoryBean自身
@Autowired
private Book bookFromFactoryBean;
在上述例子中,`BookFactoryBean`实现了`FactoryBean<Book>`接口,并在`getObject()`方法中创建并返回了一个`Book`实例。当我们在Spring容器中通过Bean名称获取`Book`时,实际上得到的是由`BookFactoryBean`创建的`Book`对象。如果需要获取`BookFactoryBean`自身,需要在Bean名称前加上"&"符号。

4、结语

面试中被问到关于FactoryBean的问题,实质上面试官是在考察候选人对Spring框架核心原理的理解深度。深入学习和理解FactoryBean有助于我们在日常开发中更好地运用Spring框架,也有利于我们进一步领会控制反转(IoC)和依赖注入(DI)的设计理念。

**MORE | 更多精彩文章**

  • 面试官:说说单点登录都是怎么实现的?
  • JWT重放漏洞如何攻防?你的系统安全吗?
  • JWT vs Session:到底哪个才是你的菜?
  • JWT:你真的了解它吗?
  • 别再这么写POST请求了~

本文转载自: 掘金

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

文档引擎+AI可视化打造下一代文档编辑器

发表于 2024-04-20

hi,大家好, 我是徐小夕。之前和大家分享了很多我设计的零代码和可视化解决方案,最近也是一直在研究文档引擎和零代码搭建引擎,今天就来分享一下最新进展。

往期精彩:

  • Nocode/Doc,可视化+ 零代码打造下一代文件编辑器
  • 爆肝1000小时, Dooring零代码搭建平台3.5正式上线
  • 可视化表单&试卷搭建平台技术详解

上一期和大家分享了文档引擎的基础功能, 技术选型, 产品设计思路,接下来先来总结一下最新实现的功能进展:

  • 文档支持配置弹幕
  • 支持文章全局评论
  • 支持划线评论
  • 支持关键词注释,批注
  • 支持文章数据统计(访问量,PUV,字数和阅读时间分析)
  • 支持文章自定义赞赏配置
  • 支持json导入导出
  • 支持一键复制内容块
  • 支持无限层级目录

设计 Nocode/Doc 的主要背景是基于我对零代码积累的大量产品技术实践和1万+的用户反馈,希望通过一种更高效简洁的方式来快速构建企业级应用。文档搭建引擎是一个基础内核,后续2个月内我会通过零代码 + 文档引擎 + AI模型来研发一款AI + 可视化办公解决方案.

下面我就来和大家介绍一下最新的功能和技术方案设计。

效果演示

静态演示:

c2896db6b1c2dfb138638094ec36560.png

动态效果:

chrome-capture-2024-4-20.gif

1. 支持无限层级目录

对于市面上的知识库产品,支持多级空间目录是非常有必要的,因为这样的设计更利于分类管理。所以我最近也设计了这个功能:

image.png

我们可以无限层级的创建目录,并在指定目录下创建文档,同时在右侧会自动渲染知识库的目录思维导图:

8.gif

目录构建和层级编历大家可以采用了二叉树的遍历算法,比如前序遍历、中序遍历和后序遍历,

代码实现参考:

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
js复制代码// 定义二叉树节点类
function Node(value) {
this.value = value;
this.left = null;
this.right = null;
}

// 创建一个示例二叉树
var nodeA = new Node("a");
var nodeB = new Node("b");
var nodeC = new Node("c");
var nodeD = new Node("d");
var nodeE = new Node("e");
var nodeF = new Node("f");
var nodeG = new Node("g");
nodeA.left = nodeB;
nodeA.right = nodeC;
nodeB.left = nodeD;
nodeB.right = nodeE;
nodeC.left = nodeF;
nodeC.right = nodeG;

// 前序遍历:先打印当前节点,再打印左边子树的节点,再打印右边子树的节点
function DLR(root) {
if (root == null) return;
console.log(root.value);
DLR(root.left);
DLR(root.right);
DLR(nodeA);
}

// 中序遍历:先打印左边子树的节点,再打印当前节点,再打印右边子树的节点
function LDR(root) {
if (root == null) return;
LDR(root.left);
console.log(root.value);
LDR(root.right);
LDR(nodeA);
}

// 后序遍历:先打印左边子树的节点,再打印右边子树的节点,再打印当前节点
function LRD(root) {
if (root == null) return;
LRD(root.left);
LRD(root.right);
console.log(root.value);
LRD(nodeA);
}

2. 支持一键复制内容块

这个功能主要是我在使用开发文档编辑器写内容的时候,有很多重复的内容需要使用,如果每次都复制粘贴,效率还是不能达到我的要求,所以就实现了一键复制内容块的功能:

image.png

它可以对任何内容形式进行一键复制,比如在传统文档中不可复制的视频,音频,轮播图,各种组件等。

文档支持配置弹幕

弹幕必须得有!作为二次元1年工龄的小朋友,看到B站逼格拉满的弹幕,想一想,文档也是内容消费型,为啥不能有弹幕,更好的和用户互动呢?所以我也实现了文档弹幕的功能, 大家可以选择配置文档弹幕:

image.png

丝滑的弹幕就这样实现了。由于涉及的知识点比较多,后续我会专门分享一下弹幕的前端技术实现。感兴趣的朋友可以体验一下,

文档弹幕案例

支持文章全局评论

内容的评论功能是少不了的, 为了让知识博主有个更好的交流环境,我在 Nocode/Doc 产品里实现了评论功能(当然国家规定使用评论必须手机号实名注册~)

image.png

支持划线评论

除了对文档进行全局评论,还有很多企业级的场景,需要不同人来协作管理,需要对文档进行更精细的反馈,目前飞书和钉钉文档都支持划线评论,所以 Nocode/Doc 也必须安排!

本来想划线评论功能画个小半天就实现了, 没想到坑还挺多,对于追求代码质量,代码性能,产品体验的我来说,真的是快要把 javascript 祖师爷扒出来了。

image.png

这里分享一下需要考虑的点:

  • 划线评论不能污染文档本身
  • 划线评论内容的加载性能
  • 划线位置计算和评论列表的位置计算
  • 划线评论完整业务数据流设计
  • 文档滚动对划线的位置计算的影响

本来想用 canvas 实现的, 所以做出来了,但是考虑到后期的扩展性和灵活性,我还是重新选择了离屏dom + js 来实现。 最终效果如下:

88.gif

大家也可以用电脑体验一下划线评论的快感~

划线评论案例

支持关键词注释,批注

这块对于知识文档非常有价值,比如对于一些行业术语,专业词汇,如果作者能给一定的解释,对于阅读用户来说是非常有帮助的,所以为了更好的用户体验,这波功能必须加上!

image.png

添加批注的效果:

image.png

支持文章数据统计

当然这个功能是为了更好的服务作者和用户,方便大家对文章进行更好的数据把控, 后续会实现更多数据分析能力:

image.png

支持文章自定义赞赏配置

当然这块主要是为作者服务的,比如你写了一篇好的文章,可以配置自己的赞赏二维码,如果用户觉得有帮助,有可能给你打赏一杯奶茶?

image.png

支持json导入导出

这块主要是为了更好的分享和保存你的内容, 并对内容做程序式修改:

image.png

最后

好啦。这就是本周的更新,预计4月29号会做一波更大规模的更新和功能上线,欢迎随时和我反馈,建议,技术交流~

往期精彩:

  • Nocode/Doc,可视化+ 零代码打造下一代文件编辑器
  • 爆肝1000小时, Dooring零代码搭建平台3.5正式上线
  • 可视化表单&试卷搭建平台技术详解

本文转载自: 掘金

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

关于Llama 3 AI大模型的几点总结

发表于 2024-04-20

Meta最近终于发布了Llama3大模型,OpenAI和CloseAI又要“开战了”,以下是关于Llama3的几点总结:

1、发布时间

Llama3的发布时间是北京时间4月19号0点37分,这主要是依据 Meta的首席AI科学家 Yann LeCun 发布Twitter帖文的时间。

loading

2、发布型号

目前Llama3发布了两款模型,分别是 Meta-Llama-3-8B 和 Meta-Llama-3-70B

3、Llama3 VS Llama2

  • 4倍的词汇量(Llama3是128k,Llama2是32k)
  • 使用 15T token训练,是 Llama2 的7倍
  • 2倍的上下文,Llama3是8k,Llama2是4k(个人认为还是太小了,不过据说后续会发布更大的)
  • 两个模型都具备分组查询注意力(GQA) ,Llama2只有70B这个模型具备
  • 整体性能优于Llama2

4、性能测试

Llama3 在MMLU(学科知识理解)、GPQA(一般问题)、HumanEval(代码能力)、GSM-8K(数学能力)、MATH(比较难得数学)五个基准测试上,不管是Llama3-8B,还是Llama-70B,都优于目前比较优秀的大模型。

loading

但是这个我觉得看看就行,毕竟比别人差的东西还发出来肯定会被用户吐槽,但性能肯定是不错的。

另外,为了寻求Llama3在现实场景的性能,开发人员还开发了一套新的高质量人类评估集。该评估集包含 1,800 个提示,涵盖 12 个关键用例:寻求建议、头脑风暴、分类、封闭式问答、编码、创意写作、提取、塑造角色/角色、开放式问答、推理、重写和总结。而且为了避免模型在此评估集上过度拟合,即使是Llama3的模型开发人员也无法访问它。

这是Llama3在这些评估集的测试结果,整体是优于其他模型的

loading

5、训练数据

  • 数量上, Llama 3 经过 15T token以上的预训练,并且官方声明这些数据全部从公开来源收集(比Llama 2使用的数据集大7倍)
  • 超过 5% 的 Llama 3 预训练数据集由涵盖 30 多种语言的高质量非英语数据组成(但是这些语言的性能水平不会与英语相同,还是得靠其他开源大佬微调)
  • 8B的知识库时间只到2023年3月,70B的知识库时间只到了2023年12月(感觉是不是8B的训练时间早于70B,所以时间早了这么多)

6、如何体验

6.1 Replicate

墙裂推荐,直接访问: https://llama3.replicate.dev/ 就行,无需登录

loading

6.2 使用我们之前提到过的 ollama 工具

loading

6.3 使用 MetaAI

不太建议,锁地区,而且要Facebook账户,对国内用户不友好

7、未来规划

Meta在后续的几个月中,将发布多个具有新功能的模型,包括多模态、以多种语言交谈的能力、更长的上下文窗口。

比如目前有一个400B的模型就仍在训练中,这是它的一个简单基准测试

loading

注: 原文链接,欢迎关注公众号AI技术巫

本文转载自: 掘金

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

Intellij IDEA 配置 Go 语言开发环境 1

发表于 2024-04-19

《开发工具系列》

18a325e136e34114b15a6d2caa5b02de.jpeg

  1. Go语言环境搭建

本篇博文是在读者 Go 自身环境已经搭好,Intellij IDEA 环境也已装好的基础上所总结而来。

Go 语言环境搭建可参考笔者的另一篇博文 Go语言学习1-基础入门。

  1. IDEA 配置 Go 语言开发环境

2.1 添加Go插件

(1)首先,启动 Intellij IDEA,点击 File –> Settings –> Plugins,如下:

image.png

(2)然后,点击 Browse repositories,打开 Browse Repositories,并搜索 go,这时候出现很多相关的结果,选择如下即可。

image.png

(3)点击 Install,等安装好了,提示重启IDEA即可。

image.png

2.2 配置GOROOT

点击 File –> Settings –> Languages & Frameworks –> Go –> GOROOT,如下所示:

image.png

2.3 配置GOPATH

点击 File –> Settings –> Languages & Frameworks –> Go –> GOPATH,如果按1中 Go 语言环境搭建的步骤,相信到这边的 Global GOPATH 就有了如下截图所示的内容,在下面的 Project GOPATH 可以添加我们自己的工程路径。

image.png

  1. 新建Go项目

点击 File –> New –> Project,打开 New Project页面,如下截图:

image.png

选择 Go,点击 Next 按钮,进入如下页面,填写 项目名称 和 项目路径

image.png

点击 Finish,选择 以 新窗口 打开新建工程,如下所示:

image.png

按照2中配置新建项目的 GOPATH,如下截图:

image.png

  1. 编写Go代码

现在可以编写 Go 代码了,可以看到如下截图,拥有的代码提示功能,很大程度上方便了开发。

image.png

  1. 运行Go代码

简单编写打印输出代码,然后右键 运行,如下截图所示:

image.png

运行结果如下:

image.png

总结

Intellij IDEA 配置 Go 语言开发环境到此完成【适用于 GOPATH】,欢迎大家尝试 !!!

拓展

从 Go 1.11 及其更高版本,Go 语言支持 go mod,它是 Go 语言提供的一个官方包管理工具,用于管理 Go 项目中的依赖关系和版本号。通过 go mod,开发者可以很方便地管理自己的项目,并且不需要再向 GOPATH 中添加第三方的依赖包。在使用 go mod 时,开发者可以将项目代码保存在本地文件系统中,不再需要克隆到 GOPATH 的指定目录下。同时,go mod 还可以从网络上下载并管理所需的依赖包,非常方便快捷。

针对 包管理方式的,后续有机会将讲解,敬请期待!

本文转载自: 掘金

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

你的Android“老”项目适配了吗?在Android14上

发表于 2024-04-19

此处的老项目指的是targetSdkVersion低于23 的应用。

由于公司老项目好久没动过,用户那边安卓14系统报以下问题:

image.png

拿到老项目运行在自己安卓11手机上正常,看了下项目SDK比较老,都低于23,以为SDK太老,升到23以上部分代码报错,就升级了几个版本,低于23,用户那边安装后还是不行。于是想到6.0系统动态权限,所以把目标SDK升级到了23,添加了动态权限,修改了部分代码,于是用户那边14系统便能正常安装了。

之前Android14没发布之前就好像在哪见过说不再支持Android6.0 系统,一直没放在心上,直到过了将近一年没想到遇到了这个问题,所以又找了官网看了一下变更,官网中安全性中是这样介绍的:

安全性

最低可安装的目标 API 级别

从 Android14开始,targetSdkVersion无法安装低于 23 的应用。要求应用程序满足这些最低目标 API 级别要求可以提高用户的安全性和隐私性。

恶意软件通常针对较旧的 API 级别,以绕过较新 Android 版本中引入的安全和隐私保护。例如,某些恶意软件应用程序使用targetSdkVersion22来避免受到 Android6.0 Marshmallow(API 级别 23)于 2015 年引入的运行时权限模型的影响。 Android 14 的这一变化使恶意软件更难避开安全和隐私方面的改进。尝试安装针对较低 API 级别的应用程序将导致安装失败,并在 Logcat 中显示以下消息:

1
go复制代码INSTALL_FAILED_DEPRECATED_SDK_VERSION: App package must target at least SDK version 23, but found 7

在升级到 Android 14 的设备上,任何targetSdkVersion低于 23 的应用程序都将保留安装。

如果您需要测试针对较旧 API 级别的应用程序,请使用以下 ADB 命令:

1
css复制代码adb install --bypass-low-target-sdk-block FILENAME.apk

所以如果你的项目targetSdkVersion低于23时,想要在Android14系统上更新或重新安装时,请一定要把targetSdkVersion改为23或以上版本。老项目可能没有用到动态权限,所以适配时一定要加上动态权限,不然软件会闪退报错的哦。

本文转载自: 掘金

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

【Android 13源码分析】窗口显示第三步 finish

发表于 2024-04-19

忽然有一天,我想要做一件事:去代码中去验证那些曾经被“灌输”的理论。

                        – 服装学院的IT男

应用端显示窗口一级框图.png

一个应用想要将它的UI内容显示到屏幕窗口上,涉及到3个模块: 应用端,SystemService端和SurfaceFlinger端。

窗口显示2级框图.png

在App开发中一个View想要显示需要经过3个步骤,也就是View三部曲:Measure,Layout,Draw
对应的一个Window想要在屏幕上显示也需要经过3个步骤:

    1. addWindow : SystemService端为应用窗口创建对应的WindowState并且挂载到窗口树中
    1. relayoutWindow : 这一步会创建一个Surface返回给应用端进行绘制,并且触发WMS的各个窗口的位置摆放和窗口尺寸计算(relayout)
    1. finishDrawingWindow:这一步是应用端的View绘制完成后,Surface已经有UI信息了,需要通过SurfaceFlinger进行合成

经过前面2步,现在 WMS 中已经有了应用的 WindowState 并且也计算好了窗口的大小,同时还创建了对应的 Surface 供应用 View 进行绘制。

接下来应用端会开始绘制,应用端的 View 绘制完成后就需要显示到屏幕上了,这就需要告知 SurfaceFlinger 进行合成处理了。

下面徐要分析的窗口显三部曲的最后一步:finishDrawingWindow 。

本篇的目的就是梳理 FrameWork 层是如何通知 SurfaceFlinger 应用窗口已经绘制完成。

有2个线索:

    1. FrameWork 中对窗口(WindowState)定义了不同的状态,找到对应状态切换的地方。
    1. FrameWork 通知 SF 做操作,是需要通过 SurfaceControl.Transaction 完成的

finishDrawing一级框图.png

    1. 首先看到是应用端完成了绘制,通知到 system_service 来做后续处理
    1. 然后WMS执行 finishDrawingWindow 方法开始处理,处理的事情可以分为2块
      • 2.1 窗口状态的处理
      • 2.2 构建出一个 SurfaceControl.Transaction 用来通知 SurfaceFlinger 显示这个窗口的 Surface
    1. SurfaceFlinger 做后续的处理

这里提到的窗口状态定义在 WindowStateAnimator.java 下面,结合源码的注释和实际场景简单解释一下各个状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
php复制代码# WindowStateAnimator

/** This is set when there is no Surface */
// 没有 Surface的时候,说明没有创建或者窗口销毁
static final int NO_SURFACE = 0;
/** This is set after the Surface has been created but before the window has been drawn. During
* this time the surface is hidden. */
// Surface 刚刚创建但是还没绘制的状态。 也就是 relayoutWindow 流程时设置的
static final int DRAW_PENDING = 1;
/** This is set after the window has finished drawing for the first time but before its surface
* is shown. The surface will be displayed when the next layout is run. */
// 窗口第一次完成绘制之后的状态,将在下一次 layout 的时候执行。
// 是等待提交到SF的状态
static final int COMMIT_DRAW_PENDING = 2;
/** This is set during the time after the window's drawing has been committed, and before its
* surface is actually shown. It is used to delay showing the surface until all windows in a
* token are ready to be shown. */
// 已经提交到SF, 准备显示到屏幕上
static final int READY_TO_SHOW = 3;
/** Set when the window has been shown in the screen the first time. */
// 窗口已经显示
static final int HAS_DRAWN = 4;

大概流程如下:

finishDrawing二级框图.png

    1. 在上一步 relayoutWindow 的时候状态 WindowState 状态已经被设置为 DRAW_PENDING
    1. 应用绘制完成会后触发 finishDrawingWindow 方法,这个方法分为2步
      • 2.1 执行 finishDrawing 方法将窗口状态设置为 COMMIT_DRAW_PENDING
      • 2.2 执行 requestTraversal 触发 layout 流程,这里可能对多次执行。相关的事情都在内部的 applySurfaceChangesTransaction 方法中处理
        • 2.2.1 在 commitFinishDrawingLocked 方法把窗口状态设置为 READY_TO_SHOW
        • 2.2.2 在 performShowLocked 方法把窗口状态设置为 HAS_DRAWN
        • 2.2.3 执行 prepareSurfaces 方法,最终构建窗口 Surface 显示的事务,通知 SurfaceFlinger 做后续的处理

开始撸代码。

  1. 应用端处理

首先既然是绘制完成后的处理,触发的地方还是应用端本身,只有应用端绘制完成了才会触发逻辑。

再看一下 ViewRootImpl::setView 的调用链:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
arduino复制代码ViewRootImpl::setView
ViewRootImpl::requestLayout
ViewRootImpl::scheduleTraversals
ViewRootImpl.TraversalRunnable::run --- Vsync相关--scheduleTraversals
ViewRootImpl::doTraversal
ViewRootImpl::performTraversals
ViewRootImpl::relayoutWindow --- 第二步:relayoutWindow
Session::relayout --- 跨进程调用
ViewRootImpl::updateBlastSurfaceIfNeeded
Surface::transferFrom --- 应用端Surface赋值
ViewRootImpl::performMeasure --- View绘制三部曲 --Measure
ViewRootImpl::performLayout --- View绘制三部曲 --Layout
ViewRootImpl::createSyncIfNeeded --- 第三步:绘制完成 finishDrawingWindow
SurfaceSyncer::setupSync
SyncSet::init --- 将回调封装在SyncSet下
ViewRootImpl::reportDrawFinished -- 等待回调,触发finishDrawingWindow
Session::finishDrawing -- 触发system_service进程执行finishDrawing流程 -- 开始跨进程(finishDrawing流程)
ViewRootImpl::performDraw --- View绘制三部曲 --Draw
SurfaceSyncer::markSyncReady --- 触发:finishDrawingWindow
Session.addToDisplayAsUser --- 第一步:addWindow

前面分析relayoutWindow流程 的时候已经分析过ViewRootImpl::performTraversals 方法了,不过当前重点不一样,所以还需要再看一遍这个方法(增加了一些当前流程相关的代码)

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
ini复制代码# ViewRootImpl
// 创建对象
private final SurfaceSyncer mSurfaceSyncer = new SurfaceSyncer();
// 是否有同步的内容需要上报
boolean mReportNextDraw;

private void performTraversals() {
......
// mWinFrame保存的是当前窗口的尺寸
Rect frame = mWinFrame;
----1.1 硬绘相关----
// 硬件加速是否初始化
boolean hwInitialized = false;
......
----2. relayoutWindow流程----
// 内部会将经过WMS计算后的窗口尺寸给mWinFrame
relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
......
// 1.2 初始化硬件加速,将Surface与硬件加速绑定
hwInitialized = mAttachInfo.mThreadedRenderer.initialize(mSurface);
......
---- 3.1 View绘制三部曲--Measure
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
......3.2 View绘制三部曲--Layout
performLayout(lp, mWidth, mHeight);
......
// 设置 mReportNextDraw = true,表示当前需要上报SurfaceFlinger
if ((relayoutResult & WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME) != 0) {
reportNextDraw(); // 打开应用的时候触发上报
}
----4.1 finishDrawing流程----
createSyncIfNeeded();
...... 3.3 View绘制三部曲--Draw
if (!performDraw() && mSyncBufferCallback != null) {
mSyncBufferCallback.onBufferReady(null);
}
......
// 4.2 触发执行回调
mReportNextDraw = false;
if (isInLocalSync()) {
mSurfaceSyncer.markSyncReady(mSyncId);
mSyncId = UNSET_SYNC_ID;
}
......
}
    1. 后续需要介绍软绘硬绘的流程,所以可以看到硬绘的初始化逻辑也在这个方法
    1. relayoutWindow 相关
    1. 经过第二步 relayoutWindow 后 View 就可以绘制了
    1. 绘制完成后就要通知 SurfaceFlinger 进行合成了,也就是本篇分析的 finishDrawing 流程

当前分析 finishDrawing 流程,首先可以看到 relayoutWindow 方法执行后,会触发3个View绘制的方法,也就是常说的 View 绘制三部曲,但是这里有个奇怪的地方:
“4.1 createSyncIfNeeded” 方法是触发 finishDrawingWindow 的,但是这个方法在 “3.3 performDraw”的上面。

这是因为代码的顺序不代表真正的执行顺序,这里的“4.1 createSyncIfNeeded”只是设置了“回调”,等时机到了就会触发执行,而这个时机就是 View 绘制完成后,在 “4.2 markSyncReady触发”

这一部分的逻辑有点绕,不过目前分析的是主流程,所以这块逻辑以上的描述当黑盒理解这段的调用: View绘制结束后就会在 4.2 出触发 4.1 内部的执行,进入触发 finishDrawingWindow 流程即可。

后面再单独写一篇详细解释直接的调用逻辑。

1.2 finishDrawingWindow 的触发

在 ViewRootImpl::performTraversals 方法最后会执行 SurfaceSyncer::markSyncReady 方法,最终会触发 ViewRootImpl::createSyncIfNeeded 方法下的 ViewRootImpl::reportDrawFinished 来真正 finishDrawingWindow 流程。

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
scss复制代码# ViewRootImpl
// 创建对象
private final SurfaceSyncer mSurfaceSyncer = new SurfaceSyncer();
// 是否有同步的内容需要上报
boolean mReportNextDraw;

private void createSyncIfNeeded() {
// 如果已经在本地进行同步或者没有需要同步的内容
if (isInLocalSync() || !mReportNextDraw) {
return;
}

// 获取当前同步序列号
final int seqId = mSyncSeqId;

// 调用mSurfaceSyncer::setupSync,并传入一个匿名类
mSyncId = mSurfaceSyncer.setupSync(transaction -> {
mHandler.postAtFrontOfQueue(() -> {
// 合并传入的transaction到mSurfaceChangedTransaction中
mSurfaceChangedTransaction.merge(transaction);
// 重点* 报告绘制完成,传入之前获取的序列号
reportDrawFinished(seqId);
});
});

if (DEBUG_BLAST){
// 打印日志
Log.d(mTag, "Setup new sync id=" + mSyncId);
}

// 将mSyncTarget添加到mSyncId对应的同步中
mSurfaceSyncer.addToSync(mSyncId, mSyncTarget);
notifySurfaceSyncStarted();
}

ViewRootImpl::reportDrawFinished 方法如下:

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
typescript复制代码# ViewRootImpl
private void reportDrawFinished(int seqId) {
// 日志和Trace相关
if (DEBUG_BLAST) {
Log.d(mTag, "reportDrawFinished " + Debug.getCallers(5));
}
if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "finish draw");
}

try {
// 重点* finishDrawing流程
mWindowSession.finishDrawing(mWindow, mSurfaceChangedTransaction, seqId);
......
} ......
if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}

# Session
@Override
public void finishDrawing(IWindow window,
@Nullable SurfaceControl.Transaction postDrawTransaction, int seqId) {
if (DEBUG) Slog.v(TAG_WM, "IWindow finishDrawing called for " + window);
mService.finishDrawingWindow(this, window, postDrawTransaction, seqId);
}

唯一做的一件事就是跨进程触发 WindowManagerService::finishDrawingWindow 。
到这里应用端的事情就处理完了,后面的流程在 system_service 进程。

  1. system_service处理

结合文章上面看到的流程图,这里也先把代码主要的调用链整理出来:

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
php复制代码WindowManagerService::finishDrawingWindow
WindowState::finishDrawing
WindowStateAnimator::finishDrawingLocked -- COMMIT_DRAW_PENDING

WindowPlacerLocked::requestTraversal -- 触发layout (可能会执行多次)
Traverser::run -- Runnable异步执行
WindowSurfacePlacer::performSurfacePlacement
WindowSurfacePlacer::performSurfacePlacementLoop
WindowSurfacePlacer::performSurfacePlacementLoop
RootWindowContainer::performSurfacePlacement -- 开始逻辑
RootWindowContainer::performSurfacePlacementNoTrace
WindowManagerService::openSurfaceTransaction -- 打开Surface事务
RootWindowContainer::applySurfaceChangesTransaction -- 处理Surface事务
DisplayContent::applySurfaceChangesTransaction
DisplayContent::performLayout -- relayoutWinodw 流程
DisplayContent::mApplySurfaceChangesTransaction
WindowStateAnimator::commitFinishDrawingLocked -- READY_TO_SHOW
WindowState::performShowLocked -- HAS_DRAWN(一般第二次菜进入)
ActivityRecord::updateDrawnWindowStates
mTmpUpdateAllDrawn::add -- 存入
mTmpUpdateAllDrawn::removeLast -- 取出
ActivityRecord::updateAllDrawn -- allDrawn = true
DisplayContent.setLayoutNeeded -- 再来一次layout
DisplayContent::prepareSurfaces -- Surface 处理
WindowContainer::prepareSurfaces -- 遍历每个孩子
WindowState::prepareSurfaces -- 忽略其他,只看窗口的实现
WindowStateAnimator::prepareSurfaceLocked
WindowStateAnimator::showSurfaceRobustlyLocked
WindowSurfaceController::showRobustly
WindowSurfaceController::setShown
SurfaceControl.Transaction::show -- Surface显示
WindowManagerService::closeSurfaceTransaction -- 处理关闭Surface事务
SurfaceControl::closeTransaction
GlobalTransactionWrapper::applyGlobalTransaction
SurfaceControl::nativeApplyTransaction -- 触发native

现在开始撸代码:

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
java复制代码# WindowManagerService
final WindowSurfacePlacer mWindowPlacerLocked;

void finishDrawingWindow(Session session, IWindow client,
@Nullable SurfaceControl.Transaction postDrawTransaction, int seqId) {
if (postDrawTransaction != null) {
postDrawTransaction.sanitize();
}

final long origId = Binder.clearCallingIdentity();
try {
synchronized (mGlobalLock) {
// 获取到对应的WindowState
WindowState win = windowForClientLocked(session, client, false);
// log
ProtoLog.d(WM_DEBUG_ADD_REMOVE, "finishDrawingWindow: %s mDrawState=%s",
win, (win != null ? win.mWinAnimator.drawStateToString() : "null"));
// 重点* 1. 执行WindowState::finishDrawing
if (win != null && win.finishDrawing(postDrawTransaction, seqId)) {
if (win.hasWallpaper()) {
win.getDisplayContent().pendingLayoutChanges |=
WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
}
// 将当前WindowState.mLayoutNeeded置为true
win.setDisplayLayoutNeeded();
// 重点* 2. 请求进行布局刷新
mWindowPlacerLocked.requestTraversal();
}
}
} finally {
Binder.restoreCallingIdentity(origId);
}
}

system_service 进程第一个处理的方法就是 WindowManagerService::finishDrawingWindow 这个方法也就做了2件事:

    1. WindowState::finishDrawing 当前分析的主要流程,将状态设置为 COMMIT_DRAW_PENDING
    1. WindowSurfacePlacer::requestTraversal 常见核心函数,触发layout,将状态设置为 READY_TO_SHOW ,HAS_DRAWN ,然后通知到 SurfaceFlinger

这里有上述的2个流程需要分析,首先会执行 WindowState::finishDrawing ,将WindowState状态设置为 COMMIT_DRAW_PENDING ,表示应用端已经绘制完成了,可以提交给SF了。

第一步操作完之后,就会执行 WindowSurfacePlacer::requestTraversal ,这个方法是执行一次 layout 逻辑。

在前面看窗口状态 COMMIT_DRAW_PENDING 的时候,google 注释提过: “会下一次 layout 的时候显示到屏幕上”,指的就是在这里触发的 layout。

在第二步 layout 的时候会遍历每个窗口,目前只关心我们当前分析的场景的这个窗口,在这次 layout 会做3件事:

    1. 将窗口状态设置为 READY_TO_SHOW
    1. 将窗口状态设置为 HAS_DRAWN
    1. 执行 SurfaceControl.Transaction 通知 SurfaceFlinger 做显示合成

下面开始在代码中梳理流程。

2.1 WindowState状态 – COMMIT_DRAW_PENDING

1
2
3
4
5
6
7
8
9
10
11
arduino复制代码# WindowState
final WindowStateAnimator mWinAnimator;
boolean finishDrawing(SurfaceControl.Transaction postDrawTransaction, int syncSeqId) {
......
// 主流程
final boolean layoutNeeded =
mWinAnimator.finishDrawingLocked(postDrawTransaction, mClientWasDrawingForSync);
mClientWasDrawingForSync = false;
// We always want to force a traversal after a finish draw for blast sync.
return !skipLayout && (hasSyncHandlers || layoutNeeded);
}

主要是执行了 WindowStateAnimator::finishDrawingLocked ,内部会将 WindowState 的状态设置为 COMMIT_DRAW_PENDING ,这个是非常重要的一步。

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

boolean finishDrawingLocked(SurfaceControl.Transaction postDrawTransaction,
boolean forceApplyNow) {
......
// 只有当前状态是DRAW_PENDING的时候才可以走进逻辑
if (mDrawState == DRAW_PENDING) {
ProtoLog.v(WM_DEBUG_DRAW,
"finishDrawingLocked: mDrawState=COMMIT_DRAW_PENDING %s in %s", mWin,
mSurfaceController);
if (startingWindow) {
// 如果是StartingWindow还有专门的log
ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "Draw state now committed in %s", mWin);
}
mDrawState = COMMIT_DRAW_PENDING;
// 表示需要 layout
layoutNeeded = true;
}
......
}

这样第一步就执行完了,流程很简单,只是设置窗口状态为 COMMIT_DRAW_PENDING 。

2.2 layout 流程简述

上一小节只是改了状态,下一个状态是 READY_TO_SHOW ,前面看到google对它有一个注释:The surface will be displayed when the next layout is run.

也就是说在下一次 layout 会触发 Surface 的显示,所以关键流程还是在 “next layout”,

那什么是 “next layout” ?

我们知道屏幕上有任何风吹操作都会触发一次 layout 流程,主要就是执行 WindowSurfacePlacer::performSurfacePlacement 这就是 一次 layout 。

WindowPlacerLocked::requestTraversal 触发的 layout 流程就是之前 relayoutWindow 流程看到的 WindowSurfacePlacer::performSurfacePlacement 。这个流程触发的地方非常多,只是当前 finishDrawingWindow 会主动触发一次罢了。对于这种高频率触发的方法,需要留意一下,初学者知道每个主流程会走什么逻辑就好,慢慢的随着知识体系的构建,再看这个流程其实就没那么复杂了。

WindowSurfacePlacer::performSurfacePlacement 的逻辑还是很复杂的,它会遍历屏幕上每一个窗口去,然后让其根据最新情况做对应的处理,比如 relayoutWinodw 流程的时候就会遍历到窗口做
执行 computeFrames 计算窗口大小。

当前分析的场景自然也会遍历窗口,正常逻辑下就是让目标窗口执行完 finishDrawingWindow 流程。

这个流程之前看过了,现在再完整看一遍 layout 流程,会忽略更多无关的的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码# WindowSurfacePlacer
private final Traverser mPerformSurfacePlacement = new Traverser();

private class Traverser implements Runnable {
@Override
public void run() {
synchronized (mService.mGlobalLock) {
performSurfacePlacement();
}
}
}

void requestTraversal() {
......
mService.mAnimationHandler.post(mPerformSurfacePlacement);
}

可以看到 WindowPlacerLocked::requestTraversal 其实就是触发了 WindowSurfacePlacer::performSurfacePlacement 方法的调用。

需要注意这边的一个异步的 Runnable 所以打堆栈或打 Trace 的时候需要留意一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
arduino复制代码# WindowSurfacePlacer
final void performSurfacePlacement() {
performSurfacePlacement(false /* force */);
}
// 控制是否需要继续执行 performSurfacePlacementLoop方法
private boolean mTraversalScheduled;

final void performSurfacePlacement(boolean force) {
if (mDeferDepth > 0 && !force) {
mDeferredRequests++;
return;
}
// 最大次数循环为6次
int loopCount = 6;
do {
// 设置为false
mTraversalScheduled = false;
// 重点方法
performSurfacePlacementLoop();
mService.mAnimationHandler.removeCallbacks(mPerformSurfacePlacement);
loopCount--;
} while (mTraversalScheduled && loopCount > 0);
mService.mRoot.mWallpaperActionPending = false;
}

这个方法在 relayoutWindow 也详细讲过了, 主要看 performSurfacePlacementLoop

1
2
3
4
5
6
7
csharp复制代码# WindowSurfacePlacer
private void performSurfacePlacementLoop() {
......
// 重点* 对所有窗口执行布局操作
mService.mRoot.performSurfacePlacement();
......
}

上面都是一些执行的条件处理,真正的处理还是从 RootWindowContainer::performSurfacePlacement 方法开始

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
scss复制代码# RootWindowContainer

// 这个方法加上了trace
void performSurfacePlacement() {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "performSurfacePlacement");
try {
performSurfacePlacementNoTrace();
} finally {
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
}
}
// 主要干活的还是这个
void performSurfacePlacementNoTrace() {
......
// Trace
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "applySurfaceChanges");
// 开启Surface事务
mWmService.openSurfaceTransaction();
try {
// 重点* 1. 处理Surface事务
applySurfaceChangesTransaction();
} catch (RuntimeException e) {
Slog.wtf(TAG, "Unhandled exception in Window Manager", e);
} finally {
// 关闭Surface事务
mWmService.closeSurfaceTransaction("performLayoutAndPlaceSurfaces");
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
}
......
// 重点* 2. 处理App事务
checkAppTransitionReady(surfacePlacer);
......
}

这里就有2个主要流程,也是2个类型的事务处理:

    1. applySurfaceChangesTransaction 方法,执行 Surface 事务

可以看到这行代码前后有 SurfaceTransaction 的打开和关闭,那说明这里的逻辑是会触发 Surface 操作的。

applySurfaceChangesTransaction 方法内部做很多事,比如上一篇的 layoutWindow 流程,当前分析的场景在这个方法里会执行将窗口状态设置为 READY_TO_SHOW ,并且构建一个 Surface 事务来显示当前窗口的 Surface 。

然后会在 WindowManagerService::closeSurfaceTransaction 方法中触发 SurfaceTransaction 的apply 把 Surface 操作的事务提交到 SurfaceFlinger 。

    1. checkAppTransitionReady 方法,执行 App 事务
      这个方法也是很常见并且核心的,Framework 层专门定义了 AppTransition 来表示一些 APP 的事务,根据用户具体的操作执行对应的 App 事务。如果事务已经满足执行条件,则会触发对应的 AppTransition 执行,然后也有一些窗口动画的触发。

这个方法知道一下即可,当前流程可以不分析。

2.3 applySurfaceChangesTransaction 方法概览

1
2
3
4
5
6
7
8
9
10
11
ini复制代码# RootWindowContainer
private void applySurfaceChangesTransaction() {
......
// 遍历每个屏幕
final int count = mChildren.size();
for (int j = 0; j < count; ++j) {
final DisplayContent dc = mChildren.get(j);
dc.applySurfaceChangesTransaction();
}
......
}

不考虑多个屏幕的场景

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
java复制代码# DisplayContent
// 需要更少是否已经绘制的集合
private final LinkedList<ActivityRecord> mTmpUpdateAllDrawn = new LinkedList();

void applySurfaceChangesTransaction() {
......
// 新的执行,清除数据
mTmpUpdateAllDrawn.clear();
......
// 重点* 1. layoutWindow 流程
performLayout(true /* initial */, false /* updateInputWindows */);
......
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "applyWindowSurfaceChanges");
try {
// 重点* 2. 遍历所有窗口执行 lambda表达式
forAllWindows(mApplySurfaceChangesTransaction, true /* traverseTopToBottom */);
} finally {
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
}
// 重点* 3. Surface 操作
prepareSurfaces();
......
// 如果有需要更新的ActivityRecord则处理
while (!mTmpUpdateAllDrawn.isEmpty()) {
final ActivityRecord activity = mTmpUpdateAllDrawn.removeLast();
// See if any windows have been drawn, so they (and others associated with them)
// can now be shown.
// 内部会设置这个ActivityRecord 下的 allDrawn 为true
activity.updateAllDrawn();
}
}

这里有有个3重要的流程:

    1. relayoutWinodw 流程计算窗口大小(已经分析过,当前不管)
    1. 遍历每个窗口,执行 mApplySurfaceChangesTransaction 这个 lambda表达式 ,当前分析的场景是会把目标窗口状态设置为 READY_TO_SHOW
    1. Framework 层对Surface 的操作,当前场景就是会提交一个 Surface 显示的事务

然后还有一个重要的数据结构:mTmpUpdateAllDrawn

这个集合存储的是这次 layout 执行到当前 applySurfaceChangesTransaction 方法时,哪些 ActivityRecord 需要更新 allDrawn 属性了。

ActivityRecord 下面的这个 allDrawn 变量表示当前 ActivityRecord 下面的窗口是否全部绘制。

执行方法前会先把 mTmpUpdateAllDrawn 清空,然后在方法末尾遍历是否有元素。 那这个集合的元素是在哪里添加的呢?

在每个 WindowState 执行 mApplySurfaceChangesTransaction 时,如果符合条件就会加入集合。

2.4 WindowState状态 – READY_TO_SHOW

1
arduino复制代码forAllWindows(mApplySurfaceChangesTransaction, true /* traverseTopToBottom */);

这句代码会从上到下遍历每个窗口,然后执行 lambda 表达式, mApplySurfaceChangesTransaction 的定义如下

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
java复制代码# DisplayContent

private final Consumer<WindowState> mApplySurfaceChangesTransaction = w -> {
......
final WindowStateAnimator winAnimator = w.mWinAnimator;
......
// 判断当前是否有Surface
if (w.mHasSurface) {
// Take care of the window being ready to display.
// 重点 * 1. 主流程设置为 READY_TO_SHOW
final boolean committed = winAnimator.commitFinishDrawingLocked();
......
}
......
// 重点 * 2. allDrawn 相关逻辑
// 拿到这个WindowState所属的ActivityRecord
final ActivityRecord activity = w.mActivityRecord;
// 已经请求可见
if (activity != null && activity.isVisibleRequested()) {
activity.updateLetterboxSurface(w);
// 更新绘制状态
final boolean updateAllDrawn = activity.updateDrawnWindowStates(w);
if (updateAllDrawn && !mTmpUpdateAllDrawn.contains(activity)) {
// 符合条件加入集合
mTmpUpdateAllDrawn.add(activity);
}
}
w.updateResizingWindowIfNeeded();
};

这里有2个逻辑需要注意:

    1. 主流程,将状态设置成 READY_TO_SHOW
    1. allDrawn 属性逻辑

当前还是先看 READY_TO_SHOW 流程,allDrawn相关的后面单独梳理。

mHasSurface 在relayoutWindow 流程创建 Surface 时设置为true,表示当前windowState的是否有 Surface 。
然后就调用其 WindowStateAnimator::commitFinishDrawingLocked 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
csharp复制代码# WindowStateAnimator

boolean commitFinishDrawingLocked() {
......
// 1. 当前状态的判断,不满足则return
if (mDrawState != COMMIT_DRAW_PENDING && mDrawState != READY_TO_SHOW) {
return false;
}
// 2. 日志
ProtoLog.i(WM_DEBUG_ANIM, "commitFinishDrawingLocked: mDrawState=READY_TO_SHOW %s",
mSurfaceController);
// 3. 设置状态
mDrawState = READY_TO_SHOW;
......
// 4. 系统Window或者StartWindow则会走后续流程设置为 HAS_DRAW
if (activity == null || activity.canShowWindows()
|| mWin.mAttrs.type == TYPE_APPLICATION_STARTING) {
result = mWin.performShowLocked();
}
return result;
......
}

这里有4个点,其中后面2个很重要:

    1. 这个方法每次 layout 的时候都可能会执行过来,所以要判断当前窗口的状态是否符合条件执行后面的逻辑
    1. 这个日志是上层分析黑屏问题看状态状态的关键
    1. 窗口状态设置成 READY_TO_SHOW ,这也是当前分析的主要里程碑
    1. 满足3个条件之一就会执行下一步,将窗口状态设置为 HAS_DRAW
      • 4.1 不依赖 Activity 的窗口,一般是状态栏导航栏这种系统窗口,或者应用启动的悬浮窗
      • 4.2 ActivityRecord::canShowWindows 是否可以显示窗口。这个方法的返回值又受3个因素影响,下一小节解释。
      • 4.3 窗口类型为 StartWindow

我们当前分析场景是不是系统窗口和StartWindow,不过是符合了第二个条件的,详细看下一节。

2.5 WindowState状态 – HAS_DRAW

现在执行到 WindowStateAnimator::commitFinishDrawingLocked 方法,窗口状态已经为 READY_TO_SHOW 了,离最终状态还差一步。
根据上面代码可知,只要条件符合就会进入执行 WindowState::performShowLocked 完整 HAS_DRAW 的设置,根据这3个条件,当前分析的是应用窗口,如果要进入肯定是第二个 ActivityRecord::canShowWindows 返回true 。

先看一下这个方法。

1
2
3
4
5
6
7
8
9
10
scss复制代码# ActivityRecord
/**
* @return Whether we are allowed to show non-starting windows at the moment. We disallow
* showing windows during transitions in case we have windows that have wide-color-gamut
* color mode set to avoid jank in the middle of the transition.
*/
boolean canShowWindows() {
return allDrawn && !(isAnimating(PARENTS, ANIMATION_TYPE_APP_TRANSITION)
&& hasNonDefaultColorWindow());
}

根据注释:方法返回当前是否允许显示非启动窗口。不允许在转换过程中显示窗口,以防我们有宽色域的窗口颜色模式设置为避免在过渡中间出现jank。

感觉似懂非懂还是直接看具体代码的3个条件吧:

    1. allDrawn :当前ActivityRecord 下的所有窗口都已经绘制
    1. 第二个条件是窗口没在做动画(除StartWindow外)
    1. 没有设置非默认颜色模式的窗口 (不知道啥意思,不过正常都为true)

如果因为这个方法返回 false 导致没有走到设置 HAS_DRAW 状态,则可以 debug 一下是哪个值异常了,具体情况具体分析。

其中比较常见会导致返回 false 的一般是 allDrawn 这边变量导致的。 这边变量定义在 ActivityRecord 下,表示当前 ActivityRecord 下的所有窗口 是否都绘制完成并准备显示。

2.5.1 allDrawn 属性逻辑

首先 layout 会触发 RootWindowContainer::performSurfacePlacement 方法的调用,而且我们知道一次 layout 内部可能会执行多次 RootWindowContainer::performSurfacePlacement

以我当前的源码版本,这个逻辑大部分情况都是执行2次 (刷过android 14的版本,执行了一次)

来完整看一下这边变量的设置逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
scss复制代码# DisplayContent
// 需要更少是否已经绘制的集合
private final LinkedList<ActivityRecord> mTmpUpdateAllDrawn = new LinkedList();

void applySurfaceChangesTransaction() {
......
// 1. 新的执行,清除数据
mTmpUpdateAllDrawn.clear();
......
// 2. 遍历所有窗口执行 lambda表达式
forAllWindows(mApplySurfaceChangesTransaction, true /* traverseTopToBottom */);
......
// Surface 操作
prepareSurfaces();
......
// 如果有需要更新的ActivityRecord则处理
while (!mTmpUpdateAllDrawn.isEmpty()) {
final ActivityRecord activity = mTmpUpdateAllDrawn.removeLast();
// See if any windows have been drawn, so they (and others associated with them)
// can now be shown.
// 3. 内部会设置这个ActivityRecord 下的 allDrawn 为true
activity.updateAllDrawn();
}
}

这里和 allDrawn 相关的操作分三步:

    1. 把 mTmpUpdateAllDrawn 的数据清除
    1. 在执行 mApplySurfaceChangesTransaction 这个 Lambda 的时候把符合条件的 ActivityRecord 添加到集合中
    1. 对集合进行遍历,内部将 allDrawn 设置为 true

先看第二步怎么把数据添加到 mTmpUpdateAllDrawn 的

2.5.1.1 mTmpUpdateAllDrawn 下元素的添加

这部分的代码之前也看过,当前关注下 allDrawn 相关的代码

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
java复制代码# DisplayContent

private final Consumer<WindowState> mApplySurfaceChangesTransaction = w -> {
......
final WindowStateAnimator winAnimator = w.mWinAnimator;
......
// 判断当前是否有Surface
if (w.mHasSurface) {
// Take care of the window being ready to display.
// 重点 * 1. 主流程设置为 READY_TO_SHOW
final boolean committed = winAnimator.commitFinishDrawingLocked();
......
}
......
// 重点 * 2. allDrawn 相关逻辑
// 拿到这个WindowState所属的ActivityRecord
final ActivityRecord activity = w.mActivityRecord;
// 已经请求可见
if (activity != null && activity.isVisibleRequested()) {
activity.updateLetterboxSurface(w);
// 更新绘制状态
final boolean updateAllDrawn = activity.updateDrawnWindowStates(w);
if (updateAllDrawn && !mTmpUpdateAllDrawn.contains(activity)) {
// 符合条件加入集合
mTmpUpdateAllDrawn.add(activity);
}
}
w.updateResizingWindowIfNeeded();
};

commitFinishDrawingLocked 方法的时候将窗口状态设置成了 READY_TO_SHOW ,下面如果 ActivityRecord::updateDrawnWindowStates 返回 true 就可以添加到 mTmpUpdateAllDrawn 中了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ini复制代码# ActivityRecord
boolean updateDrawnWindowStates(WindowState w) {
......
// 定义返回值
boolean isInterestingAndDrawn = false;
......
if (w != mStartingWindow) {
if (w.isInteresting()) {
......
if (w.isDrawn()) {
......
// 设置为true
isInterestingAndDrawn = true;
}
}
} ......
......
return isInterestingAndDrawn;
}

应用窗口这个方法走的是这个逻辑,返回值受 WindowState::isDrawn 影响,如果返回 true,则这整个方法也返回 true 了。

1
2
3
4
5
6
7
8
9
10
csharp复制代码# WindowState
/**
* Returns true if the window has a surface that it has drawn a complete UI in to. Note that
* this is different from {@link #hasDrawn()} in that it also returns true if the window is
* READY_TO_SHOW, but was not yet promoted to HAS_DRAWN.
*/
public boolean isDrawn() {
return mHasSurface && !mDestroying &&
(mWinAnimator.mDrawState == READY_TO_SHOW || mWinAnimator.mDrawState == HAS_DRAWN);
}

根据注释和代码,前面的2个条件一般都满足,所以只要窗口状态为 READY_TO_SHOW 或者 HAS_DRAWN 就返回 true 。
然后还提到了,有一个 hasDrawn ,区别就是 hasDrawn 只要状态为 HAS_DRAWN 才返回true 。

1
2
3
4
csharp复制代码# WindowState
public boolean hasDrawn() {
return mWinAnimator.mDrawState == WindowStateAnimator.HAS_DRAWN;
}

当前执行到这的时候窗口状态刚被设置为 READY_TO_SHOW ,所以满足条件返回 true 。

这块逻辑就清楚了。在 layout 的时候,会让每个窗口执行 mApplySurfaceChangesTransaction ,条件满足的话会把窗口状态设置成 READY_TO_SHOW ,并将这个窗口对应的 ActivityRecord 添加到 mTmpUpdateAllDrawn 集合。 然后再遍历集合执行 ActivityRecord::updateAllDrawn 。

2.5.1.2 设置 allDrawn = true

在 DisplayContent::applySurfaceChangesTransaction 方法最后会遍历 mTmpUpdateAllDrawn 的数据来执行 ActivityRecord::updateAllDrawn 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
scss复制代码# ActivityRecord

void updateAllDrawn() {
if (!allDrawn) {
......
if (numInteresting > 0 && allDrawnStatesConsidered()
&& mNumDrawnWindows >= numInteresting && !isRelaunching()) {
if (DEBUG_VISIBILITY) Slog.v(TAG, "allDrawn: " + this
+ " interesting=" + numInteresting + " drawn=" + mNumDrawnWindows);
// 重点* 1. 设置为true
allDrawn = true;
// Force an additional layout pass where
// WindowStateAnimator#commitFinishDrawingLocked() will call performShowLocked().
if (mDisplayContent != null) {
// 重点* 2. 还需要layout
mDisplayContent.setLayoutNeeded();
}
// 发生信息通知 Task 设置了 allDrawn
mWmService.mH.obtainMessage(H.NOTIFY_ACTIVITY_DRAWN, this).sendToTarget();
}
}
}
    1. 设置 allDrawn = true
    1. 设置标志位,那说明还需要再执行 layout (再一次执行 requestTraversal)

当前分析的是第一次执行 layout 时在执行 WindowStateAnimator::commitFinishDrawingLocked 方法的时候会把 WindowState 状态设置 READY_TO_SHOW ,但是由于此时 allDrawn = false,所以 ActivityRecord::canShowWindows 返回 false ,流程就不会往下走了。
不过这一次 layout 执行到 ActivityRecord::updateAllDrawn 把 allDrawn 设置为 true 了,并又触发了一次 layout
第二次 layout 还是会执行到 这个时候再执行到 WindowStateAnimator::commitFinishDrawingLocked 方法,这一次因为条件已经满足了,所以会执行 WindowState::performShowLocked 来把窗口状态设置成 HAS_DRAW

2.5.2 真正设置 – HAS_DRAW

经过上面的分析,现在是第二遍执行 layout 走到 WindowStateAnimator::commitFinishDrawingLocked 方法时,条件满足执行到 WindowState::performShowLocked

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
kotlin复制代码# WindowState
boolean performShowLocked() {
......
// 获取到当前状态
final int drawState = mWinAnimator.mDrawState;
// 当前分析过来的条件肯定都是满足的
if ((drawState == HAS_DRAWN || drawState == READY_TO_SHOW) && mActivityRecord != null) {
//窗口类型不为启动窗口
if (mAttrs.type != TYPE_APPLICATION_STARTING) {
// remonve startWindow 流程
mActivityRecord.onFirstWindowDrawn(this);
} else {
mActivityRecord.onStartingWindowDrawn();
}
}
// 不满足条件则直接返回
if (mWinAnimator.mDrawState != READY_TO_SHOW || !isReadyForDisplay()) {
return false;
}
......
// Force the show in the next prepareSurfaceLocked() call.
mWinAnimator.mLastAlpha = -1;
// 日志
ProtoLog.v(WM_DEBUG_ANIM, "performShowLocked: mDrawState=HAS_DRAWN in %s", this);
// 重点* 状态为HAS_DRAWN
mWinAnimator.mDrawState = HAS_DRAWN;

mWmService.scheduleAnimationLocked();
......
return true;
}

这里会对当前床头状态做检查,只有满足条件才会将状态设为 HAS_DRAWN。 然后因为这会窗口已经要显示了,可以移除 StartWindow 了。(以后会单独分析)

到这里窗口的状态已经设置成 HAS_DRAW 表示窗口已经显示了,但是这里只是状态,一个类的变量而已,还没看到实际显示的代码, 因为具体的 Surface 显示逻辑在 prepareSurfaces 中。

2.6 显示Surface

到这里状态状态已经是 READY_TO_SHOW 了,现在需要真正的将窗口显示事务提交到 SurfaceFlinger

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

@Override
void prepareSurfaces() {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "prepareSurfaces");
try {
// 1. 拿到事务
final Transaction transaction = getPendingTransaction();
// 2. 调用父类方法
super.prepareSurfaces();

// 3. 把事务merge到全局事务,供后续统一处理
SurfaceControl.mergeToGlobalTransaction(transaction);
} finally {
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
}
}

这里的主要流程是“super.prepareSurfaces();”,不过可以看到前后先是获取到了一个事务,然后再把这个事务merge到全局事务,这个全局事务就是 SurfaceControl 下面的一个类,也是一个 Surface 事务。

那说明这直接的 super.prepareSurfaces() 会有Surface 事务的处理,才需要把它 merge 到全局事务中。

当前逻辑中 super.prepareSurfaces() 内部对Surface 的处理就是将目标创建的 Surface 显示的事务

统一处理 GlobalTransaction 的时机就是在 2.2 小节看 RootWindowContainer::performSurfacePlacementNoTrace 方法的时候在执行 applySurfaceChangesTransaction 完成后处理。

DisplayContent 的父类是 DisplayArea ,不过 DisplayArea::prepareSurfaces 方法也是调用了父类 WindowContainer 的方法,所以直接看 WindowContainer::prepareSurfaces

1
2
3
4
5
6
7
8
csharp复制代码# WindowContainer
void prepareSurfaces() {
......
for (int i = 0; i < mChildren.size(); i++) {
// 遍历孩子
mChildren.get(i).prepareSurfaces();
}
}

WindowContainer::prepareSurfaces 这个方法被很多子类重写,比如前面提到的 DisplayContent 和 DisplayArea ,另外还有像场景的 Task , ActivityRecord , WindowState 等,但是他们内部重写的逻辑也会再调用 “super.prepareSurfaces();”来遍历他的孩子,最终会调用到 DisplayContent 每个容器类,这里的调用略微有点绕,不过也不是很复杂,放慢思路理一下就好了。

其他类的重新不管,当前分析的窗口,所以直接看 WindowState 的实现

1
2
3
4
5
6
7
8
9
10
scss复制代码# WindowState

@Override
void prepareSurfaces() {
......
// 主流程
mWinAnimator.prepareSurfaceLocked(getSyncTransaction());
// 调用父类,继续遍历它的孩子
super.prepareSurfaces();
}

主要是调用了 WindowStateAnimator::prepareSurfaceLocked 方法,注意参数是是获取了一个 Transaction ,说明要开始对 Surface 做操作了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typescript复制代码# WindowStateAnimator
void prepareSurfaceLocked(SurfaceControl.Transaction t) {
......
// 状态是 HAS_DRAWN 才执行
if (prepared && mDrawState == HAS_DRAWN) {
if (mLastHidden) {
if (showSurfaceRobustlyLocked(t)) {
......
}
}
}
......
}
private boolean showSurfaceRobustlyLocked(SurfaceControl.Transaction t) {
// 主流程
boolean shown = mSurfaceController.showRobustly(t);
if (!shown)
return false;

t.merge(mPostDrawTransaction);
return true;
}

这里又将 Transaction 交到了 WindowSurfaceController 处理,这个类我们在 relayoutWindow 的时候创建窗口 Surface 的时候提过,Window 的 Surface 创建是在这里类控制的,那么提交的逻辑交给他也很合理。

1
2
3
4
5
6
7
8
9
10
11
typescript复制代码# WindowSurfaceController
boolean showRobustly(SurfaceControl.Transaction t) {
// 关键日志
ProtoLog.i(WM_SHOW_TRANSACTIONS, "SURFACE SHOW (performLayout): %s", title);
......
// 2. 内部将mSurfaceShown设置为true
setShown(true);
// 3. 重点* 真正的提交
t.show(mSurfaceControl);
......
}
    1. 这个日志很关键,表示 Framework 已经将 Surface 提交到 SurfaceFlinger 了。(严格来说需要等后面事务的apply)
    1. 将 mSurfaceShown 变量设置为true, 这个也是分析黑屏问题dump要看第一个关键变量,如果为 false 说明窗口并没有显示,可能是被遮挡了
    1. 这里看到操作 Surface Transaction 的地方了, 这个show ,就说明需要把 Suface 显示。 也是 finishDrawingWindow 最终的结果。

然后只要找到 Surface Transaction 做 apply 的操作的地方,就说明 Framework 层已经通知 SurfaceFlinger 了。

2.6.1 真正的窗口显示–提交显示Surface事务到SurfaceFlinger

这一步在 2.2 小节提过了,所以直接看 WindowManagerService::closeSurfaceTransaction 方法

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
typescript复制代码# WindowManagerService
/**
* Closes a surface transaction.
* @param where debug string indicating where the transaction originated
*/
void closeSurfaceTransaction(String where) {
try {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "closeSurfaceTransaction");
SurfaceControl.closeTransaction();
mWindowTracing.logState(where);
} finally {
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
}
}
# SurfaceControl
// SurfaceControl下定义的全局事务
static GlobalTransactionWrapper sGlobalTransaction;

public static void closeTransaction() {
synchronized(SurfaceControl.class) {
......
sGlobalTransaction.applyGlobalTransaction(false);
}
}

private static class GlobalTransactionWrapper extends SurfaceControl.Transaction {
void applyGlobalTransaction(boolean sync) {
......
// 重点* apply
nativeApplyTransaction(mNativeObject, sync);
}

}

本文转载自: 掘金

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

SwiftUI 支持即时模式绘制视图 - Canvas

发表于 2024-04-19

前言

在 SwiftUI 中,你可以使用 Shape 的 API 去绘制你所需要的 2D 图形。但最终,SwiftUI 框架会将你绘制的所有图形转换为 SwiftUI 视图并去渲染它们。这种方法有利有弊,当我们需要绘制复杂的图形时,我们需要组合多个简单图形去实现。

但现在,我们可以在不组合多个形状的情况下绘制丰富的 2D 图形。这就需要使用到我们接下来要介绍的 Canvas 视图。

简单使用

Canvas 视图支持即时模式绘制,无需使用 Shape API。我们可以用它来画任何我们想要的东西,以一种程序化的方式,逐行绘制。让我们看一个下面的这个小例子。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
less复制代码struct ContentView: View {
var body: some View {
Canvas(
opaque: true,
colorMode: .linear,
rendersAsynchronously: false
) { context, size in
let rect = CGRect(origin: .zero, size: size)


var path = Circle().path(in: rect)
context.fill(path, with: .color(.blue))
}
}
}

效果图如下:

截屏2024-04-19 13.41.05.png

正如你在上面的例子中看到的,我们创建了一个 Canvas 视图作为 ContentView 的根视图。它接受一些参数,允许我们用不透明、颜色模式和异步渲染选项配置画布。

我们应该把所有的绘图逻辑放在传递给 Canvas 视图的闭包中。这个闭包称为渲染器。渲染器闭包为我们提供了一个 GraphicalContext 的实例,我们用它来绘制内容和画布的大小。

GraphicsContext 类型的实例是渲染器闭包的 inout 参数。这意味着我们可以在绘制内容时对其进行适当的修改。比如我们在当前圆形的左上角再绘制一个紫色小圆形,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
less复制代码struct ContentView: View {
var body: some View {
Canvas(
opaque: true,
colorMode: .linear,
rendersAsynchronously: false
) { context, size in
context.opacity = 0.3

let rect = CGRect(origin: .zero, size: size)

var path = Circle().path(in: rect)
context.fill(path, with: .color(.red))

let newRect = rect.applying(.init(scaleX: 0.5, y: 0.5))
path = Circle().path(in: newRect)
context.fill(path, with: .color(.red))
}
}
}

效果图如下:

截屏2024-04-19 13.42.10.png

如上图所示,我们调整了上下文的不透明度,它影响了该线之后出现的所有绘图逻辑。GraphicsContext 类型允许我们调整许多绘图过程参数,如不透明度、缩放和混合模式。它还允许我们使用 addFilter 函数添加不同的过滤器。

GraphicsContext 类型提供描边、填充和剪辑功能,允许我们绘制任何需要的路径。但它也提供了绘制功能,允许我们绘制文本和图像。

绘制文本和图像

需要注意的是,我们不能直接通过 Canvas 绘制文本或图像类型的实例。我们应该使用 GraphicsContext 类型上的 resolve 函数将它们转换为 draw 函数接受的格式。resolve 函数返回一个 ResolvedText 或 ResolvedImage 类型的实例,它允许我们调整已转换类型对象的阴影。代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
css复制代码struct ContentView: View {
var body: some View {
Canvas(
opaque: true,
colorMode: .linear,
rendersAsynchronously: false
) { context, size in
let rect = CGRect(origin: .zero, size: size)
let text = Text(verbatim: "Canvas Text").font(.title)
var resolvedText = context.resolve(text)
resolvedText.shading = .color(.white)
context.draw(resolvedText, in: rect)
}
}
}

效果图如下:
截屏2024-04-19 13.49.28.png

你不仅可以使用 Canvas 类型来绘制文本和图像,还可以绘制任何 SwiftUI 视图。但在此之前,我们应该在创建画布时使用符号闭包来注册它们。符号闭包中的每个 SwiftUI 视图都应该有其唯一的标签,以便我们稍后在渲染器闭包中通过 id 解析视图。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
css复制代码struct ContentView: View {
private let symbolID = 1
var body: some View {
Canvas(
opaque: true,
colorMode: .linear,
rendersAsynchronously: false
) { context, size in

let rect = CGRect(origin: .zero, size: size)

if let symbol = context.resolveSymbol(id: symbolID) {
context.draw(symbol, in: rect)
}
} symbols: {
Text(verbatim: "Canvas Text")
.foregroundColor(.blue)
.tag(symbolID)
}
}
}

效果图如下:
截屏2024-04-19 13.57.07.png

动画

Canvas 视图不支持动画,但是你可以用动画调度器把它嵌入到 TimelineView中。示例代码如下:

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
less复制代码struct ContentView: View {
var body: some View {
TimelineView(.animation) { timelineContext in
let value = secondsValue(for: timelineContext.date)

Canvas(
opaque: true,
colorMode: .linear,
rendersAsynchronously: false
) { context, size in
let newSize = size.applying(.init(scaleX: value, y: 1))
let rect = CGRect(origin: .zero, size: newSize)

context.fill(
Rectangle().path(in: rect),
with: .color(.purple)
)
}
}
}

private func secondsValue(for date: Date) -> Double {
let seconds = Calendar.current.component(.second, from: date)
return Double(seconds) / 60
}
}

该代码的实际效果大家可以自行在 SwiftUI 中的预览中自行查看。

本文转载自: 掘金

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

1…373839…956

开发者博客

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