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

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


  • 首页

  • 归档

  • 搜索

JDK21|借鉴了近十种语言,String终于变好用了 前言

发表于 2024-03-27

作者:鱼仔
博客首页: <codeease.top>
公众号:Java鱼仔

前言

要想看官方对于JDK21的更新说明,可以直接跳转到下面这个官方网站中

官网地址为:openjdk.org/projects/jd…

JDK21是最新的LTS版本,里面添加了不少新的特性,本文将介绍JEP430–字符串模板

新特性产生的动机

这是一个预览功能,使用字符串模板可以更加方便地进行字符串的拼接。
由于是预览功能,在编译和执行时需要增加对应的参数:

1
2
shell复制代码javac --enable-preview --release 21 Test.java
java --enable-preview Test

在之前的JDK中,如果想要实现字符串的拼接,主要有下面几种方式。

第一种使用+号进行字符串的拼接,问题在于这种方式难以阅读,就像下面这样

1
java复制代码String s = x + " plus " + y + " equals " + (x + y);

第二种是使用工具类StringBuffer或者是StringBuilder,但是会显得代码很冗长。

1
2
3
4
5
6
7
java复制代码String s = new StringBuilder()
.append(x)
.append(" plus ")
.append(y)
.append(" equals ")
.append(x + y)
.toString();

或者使用String的format方法,但这种方式会将字符串和参数分开,对于阅读和写代码都不够友好

1
2
java复制代码String s = String.format("%2$d plus %1$d equals %3$d", x, y, x + y);
String t = "%2$d plus %1$d equals %3$d".formatted(x, y, x + y);

在很多语言中,都支持了字符串插值来代替加号连接的方式,比如下面这些语言:

终于JDK在21这个版本推出了字符串模板的功能,有多种字符串模板,下面会一个个介绍。

STR模板

STR是Java平台中定义的模板处理器,可以像下面这样去使用STR模板:

1
2
3
4
5
6
7
8
9
java复制代码public class TestString {
public static void main(String[] args) {
String firstName = "Bill";
String lastName = "Duck";
String fullName = STR."\{firstName} \{lastName}";
System.out.println(fullName);
// 结果为:Bill Duck
}
}

在上面的代码中,STR.是字符串模板的前缀修饰符,{}为具体的表达式,比如上面的代码中就从上下文中获取到了对应的变量。
除了简单的变量替换之外,表达式中可以编写对应的逻辑执行代码,比如下面这样进行相加

1
2
3
4
5
6
7
8
java复制代码public class TestString {
public static void main(String[] args) {
int x = 10, y = 20;
String s = STR."\{x} + \{y} = \{x + y}";
System.out.println(s);
// 结果为:10 + 20 = 30
}
}

甚至可以在表达式中编写一些代码

1
2
3
4
5
6
7
8
9
10
11
java复制代码public class TestString {
public static void main(String[] args) {
String time = STR."The time is \{
DateTimeFormatter
.ofPattern("HH:mm:ss")
.format(LocalTime.now())
} right now";
System.out.println(time);
// 结果为 The time is 23:18:16 right now
}
}

多行模板表达式

上面这些代码主要介绍的还是单行的STR模板,如果是对像Json或者Html等字符串进行拼接时,就可以采用多行模板表达式,比如下面这行代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public class TestString {
public static void main(String[] args) {
String name = "Joan Smith";
String phone = "555-123-4567";
String address = "1 Maple Drive, Anytown";
String json = STR."""
{
"name": "\{name}",
"phone": "\{phone}",
"address": "\{address}"
}
""";
System.out.println(json);
}
}

得到的结果为:

1
2
3
4
5
json复制代码{
"name": "Joan Smith",
"phone": "555-123-4567",
"address": "1 Maple Drive, Anytown"
}

FMT处理器

FMT处理器是JDK21中定义的另外一个模板处理器,在语法上和STR类似,只是多了一个格式说明符,这个格式的使用和String的format方法类似。
在下面的例子中,通过结果就能看出格式化和未格式化的区别,%-12s表示占用12个字符且左对齐。

1
2
3
4
5
6
7
8
9
10
11
java复制代码public class TestString {
public static void main(String[] args) {
String name = "神秘的鱼仔";
String message1 = STR."STR \{name}STR";
String message2 = FMT."FMT %-12s\{name}FMT";
System.out.println(message1);
// STR 神秘的鱼仔STR
System.out.println(message2);
// FMT 神秘的鱼仔 FMT
}
}

RAW模式

RAW模式会生成一个StringTemplate对象,就好比StringBuilder和StringBuffer一样,想要获得最终的String对象还需要手动转一层,就像下面这样:

1
2
3
4
5
6
7
8
9
java复制代码public class TestString {
public static void main(String[] args) {
String name = "Joan";
StringTemplate st = RAW."My name is \{name}";
String info = STR.process(st);
System.out.println(info);
// 输出:My name is Joan
}
}

自定义模板处理器

前面介绍的这几种模板处理器都是JDK21中自带的,同时也提供了一个接口使得我们可以自己去实现一个字符串处理器,只需要继承StringTemplate.Processor,然后实现process方法即可。

比如我现在想要自定义一个字符串的模板处理器,效果是将传入的变量中的空格都去除,就可以按照下面这种写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码public class Test implements StringTemplate.Processor<String,RuntimeException>{
public static final Test TEST = new Test();
@Override
public String process(StringTemplate stringTemplate) throws RuntimeException {
StringBuilder builder = new StringBuilder();
List<String> fragments = stringTemplate.fragments();
List<Object> values = stringTemplate.values();
for (int i = 0; i < fragments.size()-1; i++) {
String fragment = fragments.get(i);
String value = (String) values.get(i);
String newValue = value.replaceAll("\\s", "");
builder.append(fragment).append(newValue);
}
builder.append(fragments.get(fragments.size()-1));
return builder.toString();
}

public static void main(String[] args) {
String test = "te st";
System.out.println(TEST."测试一下\{test},测试一下\{test}");

}
}

上面的这段代码最终输出如下,去除了字符串中的空格。

1
java复制代码测试一下test,测试一下test

自定义字符串模板很简单,需要关注的是fragments和values两个对象,在上面的例子中,这两个对象里的内容如下:

1
java复制代码StringTemplate{ fragments = [ "测试一下", ",测试一下", "" ], values = [test, test] }

总结

字符串模板功能主要还是为了弥补Java在字符串拼接上的弱点,在设计上也是借鉴了大量的语言,总体来说还是变得更好用了。

本文转载自: 掘金

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

你知道git有多少命令吗?盘点那些你可能没见过但很有用的gi

发表于 2024-03-26

Scott Chacon 在今年有一个演讲题目名为:# So You Think You Know Git。

这个一个非常棒的演讲,非常建议你去看一看原视频。本文基本上是对演讲中提到的部分内容的重新阐述。不要被这个标题吓到,这不是什么晦涩的 git 原理讲解文章。放松心情,Let’s go!

演讲者是 Scott Chacon。github 联合创始人,《Pro Git》的作者。

git 有多少命令?

145 个!

包括我们常用的 push / pull / merge /rebase,也有很多你可能根本没听说过的 apply / blame / fetch-back 等…

git 诞生于 2005 年,到今天它依然非常活跃。git 的代码仓库平均每天会有 9 次代码提交。让我们看看这个星球上最成功的版本管理工具有什么有用或者有趣的命令吧。

git config

git 别名

我们先从 git config 讲起, 使用 alias 可以为我们经常使用的 git 命令配置别名,比如:git config --global alias.staash 'stash --all'。
先回顾一下 git stash 和 git stash –all 的区别:

  • git stash:这个命令会储藏工作区和暂存区的修改。也就是说,它会储藏你已经修改但还没有提交的文件。但是,它不会储藏未跟踪的文件(你新创建的但还没有添加到 Git 的文件)。
  • git stash –all:这个命令会储藏所有的修改,包括工作区和暂存区的修改,以及未跟踪的文件。
    就我的日常工作而言,git stash –all 是更为常用的命令,使用 alias 可以更方便的使用命令。

执行自定义脚本

git config --global alias.bb !better-branch.sh。通过别名,我们也可以将我们自定义的 shell 脚本包装为 git 命令。在这个例子中,使用 git bb 即可执行 better-branch.sh。

image.png

includeIf

includeIf 是 Git 配置中的一个指令,它允许你根据特定的条件来包含 git 配置文件。

这个功能在很多情况下都很有用。例如,你可能在不同的项目中使用不同的 git 配置。你可以为每个项目创建一个单独的配置文件,然后使用 includeIf 指令来根据项目的路径来选择性地包含这些配置文件。

下面是一个例子:

1
2
3
4
5
ini复制代码[includeIf "gitdir:~/work/"]
path = ~/work/.gitconfig // user.email = 'xxx@company.com'

[includeIf "gitdir:~/oss/"]
path = ~/oss/.gitconfig // user.email = 'xxx@github.com'

我在 work 路径下放置的是公司工作的代码,配置的邮箱是公司的邮箱。而在 oss 路径下是参与开源项目的代码,配置的邮箱是 github 的邮箱。

一些旧功能

git 一直存在的功能!看看有哪些你是不知道的!

git blame -L

git blame 的主要作用是显示指定文件是由哪些开发者在何时进行的修改。对于文件中的每一行,git blame 命令都会显示最后修改这一行的 commit,作者和时间。

正如它的名字:blame,一般出现问题需要找人背锅时,可以使用 blame 来看看这行代码最后是谁提交的(笑)。

那 git blame -L 呢?可以指定行数,缩小 git blame 的范围,例如 git blame -L 28,43 path/to/file:

image.png

git blame -w

git blame 很好用。但在有一种场景下,比如你用了 lint 工具格式化了一个文件,将代码缩进从两格变成了四格。这个时候使用 git blame 就全是你的提交记录!

使用 该 git blame -w 选项将忽略这些类型的空格更改。

还有更智能的变体:git blame -w -C,除了查看文件的修改历史,还查看文件的复制历史。也就是说,如果一部分代码是从文件的其他部分复制过来的,git blame 将会把这部分代码的修改归因于原来的代码。

在这个基础上,你甚至可以进一步套娃🤯:

  • git blame -w -C:查找文件内部的复制代码;
  • git blame -w -C -C:除了查找文件内部的复制代码,还查找其他文件的复制代码;
  • git blame -w -C -C -C:除了查找文件内部和其他文件的复制代码,还查找跨越多个文件的复制代码。

妈妈再也不用担心我背锅啦!

diff 每一个单词

默认情况下,git diff 会对比行的差异。其实 git diff 也可以对比细粒度到单词,这在某些需要查看文档改动时比较有用,比如下面的例子:

image.png
git diff

image.png
git diff –word-diff

git rerere

“Reuse Recorded Resolution”(复用已记录的解决方案)的主要作用是在解决合并冲突时复用之前的解决方案。

当你在 Git 中进行合并(merge)或者拉取(pull)操作时,有可能会遇到冲突。这时,你需要手动解决这些冲突,然后将解决方案记录到 Git 中。

如果你在后续的操作中遇到了相同的冲突,你可以使用 git rerere 功能来自动应用之前的解决方案!

使用 git config 可以自动开启它:

1
2
arduino复制代码$ git config --global rerere.enabled true
$ git config --global rerere.autoUpdate true

笔者注:这看起来像是个魔法🪄 需要注意 Reuse Recorded Resolution 只能在冲突的上下文没有改变的情况下正常工作。我在实际工作中并没有遇到过需要广泛使用它的场景。暂不做深入介绍。

一些新玩意

分支排序

Git Branch 不是什么新东西,它默认是以最愚蠢的字母顺序排列的。现在你可以选择设置它的排序规则:

1
2
bash复制代码# 使用提交日期降序排序
$ git config --global branch.sort -committerdate

如果 git branch 太多!你可以让他更为紧凑的显示在屏幕上:

1
bash复制代码git config --global column.ui auto

效果如下:
image.png
cool!

git push –force-with-lease

考虑这种场景,小明在分支 feat 上提交了 commit:

1
sql复制代码add some magic🪄

push 到远端后,小明想改点东西,于是用 git commit --amend 修改了这个 commit。这个时候本地的 commit 和远端的不一致了,小明掏出了 git push --force 强制覆盖了远端代码,满意的下班了。

一般来说,这样没什么问题!

但是如果小明和同事小强在同一个分支工作,问题就来了,小明在分支 feat 上提交了 commit,小强也提交了自己的 commit。小强提交完满意地下班了。这个时候小明修改了自己的代码,掏出了 git push --force 强制覆盖了远端代码。

第二天小强一来上班发现自己的代码被覆盖了!提着刀就去了小明的工位。

git push --force-with-lease 就是用来应对这种场景。他可以看作是一个更安全的 git push --force。--force-with-lease 选项在强制更新新分支之前,会检查您上次推送的内容是否仍然是服务器上的内容。

如果有人更新了远程引用(同时推送),则会推送失败。

如果你经常用 git push --force,可以添加一个别名,比如:

1
csharp复制代码git config --global alias.fpush push --force-with-lease

May the force be with you.

SSH 提交验证

image.png

你逛 github 的时候肯定看到过有的 commit 上会打上一个 verified 的标记。
Verified 标签的主要意义在于提供了一种验证机制,确保提交的真实性和完整性。这对于开源项目尤其重要,它可以帮助维护者确认提交是由特定的贡献者完成的,并且在提交过程中没有被修改。

Git 支持使用 GPG 签署提交已经有一段时间了,但如果你以前从未使用过 GPG,别担心,现在 github 可以使用 SSH 验证。具体内容可以看看这篇文章。

Git Maintenance

git maintenance start,用于自动启动 Git 仓库的后台维护任务。这个命令会在你的系统上安排定期的 Git 维护任务,这些任务包括:

  • 清理:Git的垃圾收集器会清理那些不再需要的文件和对象,以减少仓库的大小;
  • 压缩:Git会重新打包你的对象。这个过程会将多个小文件合并成一个大文件,从而提高Git的性能;
  • 修复:如果Git仓库出现了问题,Git会检查和修复它。

如果你想要停止后台的维护任务,你可以运行 git maintenance stop 命令。这对于代码仓库很大的场景非常有用!试一试吧。

wrap up

以上是对演讲中提到的有意思的 git 相关的内容的一个总结。演讲中还有针对大型仓库的相关内容。感兴趣的话可以看一看原视频。

感谢阅读~

本文转载自: 掘金

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

来,实现一下这个报表功能,速度要快,要嘎嘎快

发表于 2024-03-26

我们有一段业务,类似一个报表,就是获取用户的订单汇总,邮费汇总,各种手续费汇总,然后拿时间噶一卡,显示在页面。

但是呢,这几个业务没啥实际关系,数据也是分开的,一个一个获取会有点慢,我开始就是这样写的,老板嫌页面太慢,让我改,可是页面反应慢,关我后端程序什么事,哥哥别打了,错了错了,我改,我改。那么最好的方案就是多线程分别获取然后汇总到一起返回。

在Java中获取异步线程的结果通常可以使用Future和Callable、CompletableFuture、FutureTask等类来实现。这些类可以用来提交任务到线程池,并在任务完成后获取结果。这就是我们想要的结果,那么这里来深入研究分析一下这三个方案。

使用Future和Callable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码package com.luke.designpatterns.demo;

import java.util.concurrent.*;

public class demo {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(new Callable<Integer>() {
public Integer call() throws Exception {
// 获取各种汇总的代码,返回结果
return 42;
}
});
// 获取异步任务的结果
Integer result = future.get();
System.out.println("异步任务的结果是" + result);
executor.shutdown();
}
}

image.png

它们的原理是通过将任务提交到线程池执行,同时返回一个Future对象,该对象可以在未来的某个时刻获取任务的执行结果。

  1. Callable 接口:Callable 是一个带泛型的接口,它允许你定义一个返回结果的任务,并且可以抛出异常。这个接口只有一个方法 call(),在该方法中编写具体的任务逻辑。
  2. Future 接口:Future 接口代表一个异步计算的结果。它提供了方法来检查计算是否完成、等待计算的完成以及检索计算的结果。Future 提供了一个 get() 方法,它会阻塞当前线程直到计算完成,并返回计算的结果。

Callable 接口本身并不直接启动线程,它只是定义了一个可以返回结果的任务。要启动一个 Callable 实例的任务,通常需要将其提交给 ExecutorService 线程池来执行。

在 ExecutorService 中,可以使用 submit(Callable<T> task) 方法提交 Callable 任务。这个方法会返回一个 Future 对象,它可以用来获取任务的执行结果。

启动 Callable 任务的原理可以概括为以下几个步骤:

  1. 创建 Callable 实例:首先需要创建一个实现了 Callable 接口的类,并在 call() 方法中定义具体的任务逻辑,包括要执行的代码和返回的结果。
  2. 创建 ExecutorService 线程池:使用 Executors 类的工厂方法之一来创建一个 ExecutorService 线程池,例如 newFixedThreadPool(int nThreads)、newCachedThreadPool() 等。
  3. 提交任务:将 Callable 实例通过 ExecutorService 的 submit(Callable<T> task) 方法提交到线程池中执行。线程池会为任务分配一个线程来执行。
  4. 异步执行:ExecutorService 线程池会在后台异步执行任务,不会阻塞当前线程,使得主线程可以继续执行其他操作。
  5. 获取结果:通过 Future 对象的 get() 方法获取任务的执行结果。如果任务尚未完成,get() 方法会阻塞当前线程直到任务完成并返回结果。

总的来说,Callable 启动线程的原理是将任务提交给 ExecutorService 线程池,线程池会负责管理线程的执行,执行任务的过程是在独立的线程中进行的,从而实现了异步执行的效果。

使用CompletableFuture

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码import java.util.concurrent.CompletableFuture;

public class Main {
public static void main(String[] args) throws InterruptedException, ExecutionException {
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
// 获取各种汇总的代码,返回结果
return 43;
});

// 获取异步任务的结果
Integer result = future.get();

System.out.println("异步任务的结果:" + result);
}
}

image.png

CompletableFuture 是 Java 8 引入的一个类,用于实现异步编程和异步任务的组合。它的原理是基于”Completable”(可以完成的)和”Future”(未来的结果)的概念,提供了一种方便的方式来处理异步任务的执行和结果处理。

CompletableFuture 的原理可以简单概括为以下几点:

  1. 异步执行:CompletableFuture 允许你以异步的方式执行任务。你可以使用 supplyAsync()、runAsync() 等方法提交一个任务给 CompletableFuture 执行,任务会在一个独立的线程中执行,不会阻塞当前线程。
  2. 回调机制:CompletableFuture 提供了一系列的方法来注册回调函数,这些回调函数会在任务执行完成时被调用。例如,thenApply(), thenAccept(), thenRun() 等方法可以分别处理任务的结果、完成时的操作以及任务执行异常时的处理。
  3. 组合多个任务:CompletableFuture 支持多个任务的组合,可以使用 thenCombine()、thenCompose()、thenAcceptBoth() 等方法来组合多个任务,实现任务之间的依赖关系。
  4. 异常处理:CompletableFuture 允许你对任务执行过程中抛出的异常进行处理,可以使用 exceptionally()、handle() 等方法来处理异常情况。
  5. 等待任务完成:与 Future 类似,CompletableFuture 也提供了 get() 方法来等待任务的完成并获取结果。但与传统的 Future 不同,CompletableFuture 的 get() 方法不会阻塞当前线程,因为任务的执行是异步的。

总的来说,CompletableFuture 的原理是基于回调和异步执行的机制,提供了一种方便的方式来处理异步任务的执行和结果处理,同时支持任务的组合和异常处理。

使用FutureTask

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码import java.util.concurrent.*;

public class Main {
public static void main(String[] args) throws InterruptedException, ExecutionException {
FutureTask<Integer> futureTask = new FutureTask<>(() -> {
// 获取各种汇总的代码,返回结果
return 44;
});

Thread thread = new Thread(futureTask);
thread.start();

// 获取异步任务的结果
Integer result = futureTask.get();
System.out.println("异步任务的结果:" + result);
}
}

image.png

FutureTask 是 Java 中实现 Future 接口的一个基本实现类,同时也实现了 Runnable 接口,因此可以被用作一个可运行的任务。FutureTask 的原理是将一个可调用的任务(Callable 或 Runnable)封装成一个异步的、可取消的任务,它提供了一个机制来获取任务的执行结果。

FutureTask 的原理可以简要概括如下:

  1. 封装任务:FutureTask 接受一个 Callable 或 Runnable 对象作为构造函数的参数,并将其封装成一个异步的任务。
  2. 执行任务:FutureTask 实现了 Runnable 接口,因此可以作为一个可运行的任务提交给 Executor(通常是 ExecutorService)来执行。当 FutureTask 被提交到线程池后,线程池会在一个独立的线程中执行该任务。
  3. 获取结果:通过 Future 接口的方法,可以等待任务执行完成并获取其结果。FutureTask 实现了 Future 接口,因此可以调用 get() 方法来获取任务的执行结果。如果任务尚未完成,get() 方法会阻塞当前线程直到任务完成并返回结果。
  4. 取消任务:FutureTask 提供了 cancel(boolean mayInterruptIfRunning) 方法来取消任务的执行。可以选择是否中断正在执行的任务。一旦任务被取消,get() 方法会立即抛出 CancellationException 异常。

总的来说,FutureTask 的原理是将一个可调用的任务封装成一个异步的、可取消的任务,并通过 Future 接口来提供获取任务执行结果和取消任务的机制。

这些方法中,get()方法会阻塞当前线程,直到异步任务完成并返回结果。如果任务抛出异常,get()方法会将异常重新抛出。

我们平时常用的方法就是这四种,都能实现我的需求,随便找一个哐哐干上去就好啦。

本文转载自: 掘金

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

聊聊http发展史 前言 HTTP/09 HTTP/10

发表于 2024-03-26

前言

HTTP(HyperText Transfer Protocal) 是超文本传输协议,http最初被打造出来就是为了传输文本内容,用作学术交流,将html文本布局传来传去,那个时候是没有后端的概念的,数据全是写死的,类似电子书。本期就带大家回顾下http的发展历程,认清咩个版本的优缺点~

HTTP/0.9

  1. 客户端发送get请求,比如请求一个/index.html
  2. 服务端接收请求,读取对应的html文件,以ASCII的字符流返回给客户端

特点

  1. 只有请求行,没有请求头和请求体,因此0.9版本的http也被称之为单行协议
  2. 因此也没有响应头
  3. 传输的内容是以ASCII的字符流

推动0.9到1.0的发展是网景公司,世界上第一款浏览器就是网景公司开发的,自从他推出浏览器后,万维网(World Wide Web)就不再是仅仅做学术交流了,互联网高速发展,万维网联盟(W3C)成立,http工作组同时成立,这个工作组专门致力于http协议的更新,以前查html,css,js就是在w3c上查找,类似现在的mdn

随着互联网需求的增加,0.9不再满足,1.0诞生

0.9只能传文本格式,用户接触浏览器不仅仅要看文本,还需要看图片,音频等格式

HTTP/1.0

  1. 相比较0.9,支持多种类型文件的传输,且不限于ASCII编码方式
  2. 既然多种文件,那么就需要信息告诉浏览器如何加载这些文件,因此引入请求头,响应头来让客户端和服务端更加深入的交流,并且是key-value的形式

为什么有了请求头和响应头就能支持多种文件的数据传输

请求头

accept: text/html 告诉服务端我期望接收到一个html的文件

accept-encoding: gzip, deflate, br 告诉服务端以这种方式压缩

accept-language: zh-CN 告诉服务端以中文的格式返回

响应头

content-encoding: br 告诉浏览器压缩方式是br

content-type: text/html; charset=utf-8 告诉浏览器以这种方式,编码加载

1.0时,比如展示一个页面,用到几个js文件,css文件,用一次就会单独用http请求一次,文件不多还好,但是随着需求增加,一个页面可能会用到很多js文件,以及第三方库,或者说图片资源,展示一个页面会发很多次请求,非常影响性能

HTTP/1.1

  1. 持久连接,一个tcp连接建立,可以传输多个http请求,减少了大量的tcp连接和断开连接带来的开销

持久连接带来了队头阻塞的问题,这个问题没办法解决
2. Chunk transfer机制处理动态数据包的长度

1.0时一个页面展示每用到一个js脚本,图片等都需要重新建立一次连接,做无畏的重复,因此1.1推出了持久连接或者长连接来解决这个问题

1.1可以建立一次tcp连接后,发送多个http请求,最后tcp关闭连接,持久连接在1.1是默认开启的,当然你也可以通过改请求头中的Connection字段,Connection: keep-alive || close ,close就是关闭的

实际开发中,页面所有的资源不可能全部一次性请求回来,比如有个按钮,点击后才会发请求,假设我页面初次加载的tcp请求还没有关闭,就点击了按钮,于是就会再次建立一次持久连接,这种建立是有数量限制的,通常为6-8个keep-alive,也就是tcp持久连接

凡事有利有弊,这种方式可以1次tcp连接 n 次http请求,我们设想一个情景,如果 n 个http请求有个http请求的数据很多,造成很慢,后面的http还会过来吗?不能,当年的http请求需要顺序,因此这就导致了http的队头阻塞

队头阻塞

  • 管线化:批量化发请求(放弃)

针对这个http队头阻塞问题,http工作组想过一个解决方案,管线化,一次性把http请求全部发出去,这些http请求都会打上顺序标记,保证接收的顺序,谷歌和火狐以前采用过这个方案,但是不清楚出于什么原因放弃了这个方案

后面请求头中增加了一个host字段,这就导致了虚拟机技术的成熟,浏览器的同源策略就是通过host字段判断域名是否一致,虚拟机也会被分配ip,虚拟机和主机的ip是不同的,host当初就是用来区分虚拟机的域名地址的

另外,1.0时,需要在响应头中设置数据的大小,字段content-Length: 1024 比如这个就是1024个字节的大小,但是有时候后端也不清楚自己发送的数据多大,因为数据可能是动态的,这就导致了客户端不清楚自己是否接收完毕,1.1于是推出片段化数据,将数据分割成若干个任意大小的数据块,每个数据块发送时带有自身的长度,最后会发送一个长度为0的数据块来标志发送完毕,这就有点像是柯里化函数,最后不传参数才会执行函数

浏览器的cookie也是这个时候加入的,cookie虽然是浏览器的存储但是大部分是归于后端负责的,前端可以读取cookie的内容,另外cookie的内容也可以被设置为可读取和不可读取,一般为了安全性会设置不可读取,这就是为了防止脚本攻击

HTTP/2.0

1.1的问题

  1. 带宽用不满,多条tcp连接竞争带宽导致每条tcp连接中能被分配的带宽大大降低
  2. tcp的慢启动:拥塞控制导致一定会慢启动,但是会推迟页面关键资源加载时间
  3. http队头阻塞问题,阻塞时会浪费带宽

1.1存在队头阻塞问题,以及6个持久连接,也就是说一个浏览器只能同时存在6个tcp的建立,一个tcp建立有多个http请求,其实这6个持久连接会共享网络速度,假设1000M宽带,网速最终就是1000/6M的效果,也就是说对网速的利用率很低,其实造成网速低的原因还有个就是tcp的慢启动,到达理想速度有个过程,这个慢启动就是为了解决网络拥塞问题

tcp慢启动有个问题就是,对于很小的css,js文件,就没必要耗时这么久,文件大还好,文件小但是文件又很重要就很难受

多路复用

  1. 一个域名只使用一个tcp长连接
  2. 将每个请求分成一帧一帧的数据进行传输并打上标记,同时发送给服务端,且可以在重要资源请求中标记为加急,服务端接收到带有各种编号的数据帧后,可以区分哪个数据帧加急,优先处理和响应该请求的数据帧(通过引入了二进制分帧层实现多路复用)

首先,tcp的慢启动我们肯定无法进行优化的,只能进行规避,因此我就尽可能的少点tcp的建立连接,2.0的多路复用就是一个域名只使用一个tcp的长连接,6个变1个,这样就不会产生多个keep-alive占用带宽问题,因此网速大大提高

另外,多路复用还将每个请求都划分成一帧一帧的样子,每帧都会标记一些数据,比如标记加急,这样就可以解决http队头阻塞问题,里面就是因为引入了二进制分帧层

2.0同时引入了https

HTTP/HTTPS

首先一个请求行大概如下这样

GET /image/logo.png HTTP/1.1

这里提到get,顺带带大家认清get和post的区别

GET vs POST

get和post本质上没有很多区别,真要说区别需要从用法上来说

get没有加密效果!

要从用法上说就得扯上 副作用 和 幂等

  • 副作用:对服务器上的资源有变更

比如,搜索是不会对数据发生变更的,注册会有

  • 幂等:注册10次和20次不是幂等,新增了数据,但是更改文章10次和20次是幂等,改来改去还是那么多数据

get多作用于无副作用,幂等的场景,比如搜索

post多作用于有副作用,无幂等的场景,比如注册

从技术角度来说,get请求能缓存,post不能缓存

浏览器浏览页面会有历史页面的,历史页面就有url,get请求的数据放在url中,因此,get请求会被浏览器缓存住

要说安全,post请求确实是会安全一点,post请求放在请求体中,get请求拼接在url中

另外url的长度是有限的,所以get请求数据有限制,而post不会

post请求支持更多的编码类型,并且不对数据类型做限制

用过koa的小朋友都清楚,koa默认是不支持post的,需要另外安装依赖,就是因为post的数据类型太多了,不支持

总结

  1. get用于无副作用幂等的场景,post用于有副作用不幂等的场景
  2. get请求能缓存,post不能
  3. post相对安全一点,post的参数是在请求体中,get的参数是拼接在url中
  4. url的长度有限制,所以get请求会受影响
  5. post支持更多的编码类型,且不对数据类型做限制

这里也顺带放下面试官喜欢问你的状态码

http状态码

200 请求成功,请求在服务端被正确处理

204 响应成功,没有数据

205 服务器处理成功,浏览器应重置文档视图

206 服务器成功处理了部分get请求

301 资源永久重定向 资源 后端换地方不作任何操作就是404

302 资源临时重定向

303 让你查看其他地址

304 请求的资源没有修改,服务端不会返回任何资源

400 请求语法错误,服务器看不懂

401 请求没有携带信息,比如token认证失败

403 请求被拒绝 敏感词

404 找不到资源

500 服务器内部错误,无法完成请求

501 服务器不支持当前请求所需的功能

503 服务器系统维护或者超载,暂时无法处理客户端的请求

回到http/https的比较

https是加密后的http,这个s就是TLS

因此https = http + tls

TLS位于传输层之上,应用层之下,TLS中包含了对称加密和非对称加密

对称加密

对称加密需要客户端,服务端双方都知道加密的方式,以及解密的方式,或者说双方都有相同的密钥,都知道如何加密和解密

对称加密有个很大问题,双方如何清楚共同的密钥?通过传输的方式,密钥也是通过网络传输,这个过程要是被截取就完蛋了

非对称加密

非对称加密不同,非对称加密会有个公钥和私钥,公钥用于加密,私钥用于解密,比如服务端发送一个公钥给客户端,客户端会用这个公钥进行加密,创建一个密钥,用公钥来加密这个密钥,给到服务端,服务端用独有的私钥进行解密得到密钥,两个过程都不怕被截取

1.png

tls是先用非对称加密让双方都有密钥,再对称加密进行数据传输

http2.0本身是没有问题的,问题主要存在tcp身上,因为这些版本的http都是基于tcp,tcp自身就有慢启动问题,不够高效

HTTP/3.0

http2.0的问题在于tcp,因为tcp有个慢启动问题,其实tcp还有个问题就是tcp的队头阻塞,因为tcp需要保证数据包传输的顺序,数据在传输的过程会出现丢包的问题,tcp有个超时重传的机制,为保证顺序就需要等待,这个过程就导致了队头阻塞

因此2.0的问题就是tcp的慢启动和``队头阻塞`

这个时候你肯定想,既然tcp有问题就去解决tcp的问题,实则不行,因为tcp作用于物理层,比如交换机就是物理层,里面的tcp要是变更了,全球的交换机就需要淘汰掉,就像是js的null被当做obj一样,改不了了

像是这种tcp的问题无法解决,被称之为tcp僵化

其实导致tcp僵化的原因还有一点是操作系统,tcp协议是通过操作系统的内核实现的,应用程序只能使用不能修改

因此http工作组非常无奈,想要解决这些问题就只能不用tcp协议了

3.0基于的协议是QUIC协议

quic协议同样面临着tcp同样的挑战,因为quic协议需要硬件的支持,比较麻烦,但是又比更新tcp好,因为新增个quic协议至少不会让设备崩掉,更新tcp会很麻烦

这也就是为何3.0目前还没有普及的原因

QUIC协议

quic协议不是从0打造的,它是基于udp协议,udp的特点是高效,虽然不可靠,quic协议在udp上实现了类似于tcp的多路复用,可靠性传输等功能

2.png

  1. 实现了类似于tcp的流量控制和可靠性传输
  2. 集成了TLS加密
  3. 实现了HTTP2中的多路复用

缺点:需要浏览器,服务器,操作系统支持quic协议,普及开还需要一段时间

最后

一段话总结下http发展史:

0.9是最初版本,此时的http被称之为单行协议,因为只有请求行,没有请求头请求体,以ASCII码的形式传输,只能支持html文件类型,后面随着文件类型的增加,1.0诞生,可以支持多种文件类型的传输,并且不仅仅只支持ASCII这一种编码,引入了请求头响应头,因为里面的字段信息才可以区分文件类型等操作,但是1.0有个缺陷就是一个文件对应一个tcp的建立,后面1.1诞生,推出了keep-alive长连接,一个tcp的建立与断开之间可以有多个http请求,但是keep-alive的数量有限,6-8个,并且keep-alive带来了http队头阻塞的问题,就是一个http请求万一很慢,会阻塞后面的http请求,以前推出过管线化方法去解决,但是因为某些原因不采用了,至今无果,此时1.1还推出了host字段,导致后面虚拟机技术的成熟,并且1.0时的数据包会包含content-Length字段,但是有时候后端的数据是动态的,因此1.1推出Chunk transfer机制来处理动态数据,它是通过切片的形式进行传输,最终发送一个长度为0的数据块来标志发送完毕,因为1.1的队头阻塞问题,以及6个keep-alive共同占用网速的原因,对带宽的利用率很低,2.0诞生,2.0的多路复用,将6个keep-alive变成了一个,有效提高宽带利用率,并且通过二进制分帧层实现了对数据帧的传输顺序管理,解决了http队头阻塞问题,但是由于tcp僵化问题,比如无法解决它的慢启动和队头阻塞问题,3.0摒弃了tcp协议,采用了quic协议,这个协议基于udp,udp高效但是不可靠,于是quic又实现了类似于tcp的流量控制和可靠性传输,并且还有加密,实现了多路复用,总之很强大,只是目前还没有普及开

如果你对春招感兴趣,可以加我的个人微信:Dolphin_Fung,我和我的小伙伴们有个面试群,可以进群讨论你面试过程中遇到的问题,我们一起解决

另外有不懂之处欢迎在评论区留言,如果觉得文章对你学习有所帮助,还请”点赞+评论+收藏“一键三连,感谢支持!

本文转载自: 掘金

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

飞书很好,但赢不了,只能裁员

发表于 2024-03-26

心碎飞书

3 月 26 日,字节跳动旗下产品飞书的 CEO 谢欣发布全员信,正式宣布进行新一轮的组织调整,即裁员。

内部全员信如下:

我有不少朋友是在字节跳动,甚至就在 Lark 的。

同时我也因为会经常和一些平台的运营小伙伴有沟通需求,经常会使用飞书。

说一下我对飞书的看法。

飞书是一个很棒的产品,它能给你一些在其他 App(企业微信、钉钉)体会不到的舒适感。

企业微信和钉钉这两个 App 我也看过,在 tabbar 随便点点就会有满屏 icon 按宫格布局的界面。明明什么都还没做,那种无形的压力就上来了,在飞书上我基本上没有这种感觉。

那些宫格布局的功能,飞书也有,只不过紧迫感被良好的 UI 设计给摊开掉了。

当然,可能不少人会认为,作为一个办公应用,企业微信和钉钉给人带来的紧迫感觉,才是对的 …

但办公软件敢于简洁化,恰恰说明产品的自信。

作为一个 5G 冲浪选手,我也没少听说飞书在 C 端收获的好评。

你没听错,很多个人团队/小型工作室,甚至只是有即时通讯需求的个人,也会选择飞书,这对于一个办公类的 IM 软件来说,是不可想象的。

那,如此好的一款产品,为什么会走到这一步?

总归到底,飞书应该是一款 B 端产品,而非其他。

基本面垮了,别的地方再好也没用。

飞书好比是名田径运动员,额外点亮了体操这一技能,气质方面满分,但田径成绩长期与第一二名断层。

要知道,大众(无论个人还是企业,企业一定程度也是个人)心智空间,往往只够容纳第一名,少数情况能容纳第二名,例如「可乐可乐和百事可乐」以及「企业微信和钉钉」,而飞书是三名。

导致局面的原因是显然的。

企业微信背靠微信,具有天然的链接优势,而钉钉依靠于入场得早,以及前期与政府的和谐关系,也抢占了属于自己的份额。

那飞书呢?

飞书只能去 to B 一些人家不要的企业,同时字节又和政府关系不好,丢失了最大头的客户。

后发的飞书,哪怕要成为前三,也注定要砸巨量资源,所以目前飞书的人数是钉钉的五倍。

从产品和组织架构来看,精简,确实是飞书现在应该要走的路。

只不过,时代的尘埃这次又要几百上千的人来一同承担了。

…

回归主线。

来做一道和「飞书」相关的算法原题。

题目描述

平台:LeetCode

题号:621

给你一个用字符数组 tasks 表示的 CPU 需要执行的任务列表,其中每个字母表示一种不同种类的任务。

任务可以以任意顺序执行,并且每个任务都可以在 1 个单位时间内执行完。

在任何一个单位时间,CPU 可以完成一个任务,或者处于待命状态。

然而,两个相同种类的任务之间必须有长度为整数 n 的冷却时间,因此至少有连续 n 个单位时间内 CPU 在执行不同的任务,或者在待命状态。

你需要计算完成所有任务所需要的最短时间。

示例 1:

1
2
3
4
5
6
css复制代码输入:tasks = ["A","A","A","B","B","B"], n = 2

输出:8

解释:A -> B -> (待命) -> A -> B -> (待命) -> A -> B
在本示例中,两个相同类型任务之间必须间隔长度为 n = 2 的冷却时间,而执行一个任务只需要一个单位时间,所以中间出现了(待命)状态。

示例 2:

1
2
3
4
5
6
7
8
9
10
css复制代码输入:tasks = ["A","A","A","B","B","B"], n = 0

输出:6

解释:在这种情况下,任何大小为 6 的排列都可以满足要求,因为 n = 0
["A","A","A","B","B","B"]
["A","B","A","B","A","B"]
["B","B","B","A","A","A"]
...
诸如此类

示例 3:

1
2
3
4
5
6
rust复制代码输入:tasks = ["A","A","A","A","A","A","B","C","D","E","F","G"], n = 2

输出:16

解释:一种可能的解决方案是:
A -> B -> C -> A -> D -> E -> A -> F -> G -> A -> (待命) -> (待命) -> A -> (待命) -> (待命) -> A

提示:

  • 1<=task.length<=1041 <= task.length <= 10^41<=task.length<=104
  • tasks[i] 是大写英文字母
  • n 的取值范围为 [0,100][0, 100][0,100]

构造

先考虑最为简单的情况:假设只有一类任务,除了最后一个任务以外,其余任务在安排后均需要增加 nnn 个单位的冻结时间。

将任务数记为 mmm 个,其中前 m−1m - 1m−1 个任务均要消耗 n+1n + 1n+1 的单位时间,最后一个任务仅消耗 111 个单位时间,即所需要的时间为 (n+1)×(m−1)+1(n + 1) \times (m - 1) + 1(n+1)×(m−1)+1。

当存在多个任务时,由于每一类任务都需要被完成,因此本质上我们最需要考虑的是将数量最大的任务安排掉,其他任务则是间插其中。

假设数量最大的任务数为 max,共有 tot 个任务数为 max 的任务种类。

实际上,当任务总数不超过 (n+1)×(max⁡−1)+tot(n + 1) \times (\max - 1) + tot(n+1)×(max−1)+tot 时,我们总能将其他任务插到空闲时间中去,不会引入额外的冻结时间(下左图);而当任务数超过该值时,我们可以在将其横向添加每个 n+1n + 1n+1 块的后面,同时不会引入额外的冻结时间(下右图):

综上,我们所需要的最小时间为上述两种情况中的较大值即可:

max⁡(task.length,(n+1)×(max−1)+tot)\max(task.length, (n + 1) \times (max - 1) + tot)max(task.length,(n+1)×(max−1)+tot)
Java 代码:

1
2
3
4
5
6
7
8
9
10
Java复制代码class Solution {
public int leastInterval(char[] tasks, int n) {
int[] cnts = new int[26];
for (char c : tasks) cnts[c - 'A']++;
int max = 0, tot = 0;
for (int i = 0; i < 26; i++) max = Math.max(max, cnts[i]);
for (int i = 0; i < 26; i++) tot += max == cnts[i] ? 1 : 0;
return Math.max(tasks.length, (n + 1) * (max - 1) + tot);
}
}

C++ 代码:

1
2
3
4
5
6
7
8
9
10
C++复制代码class Solution {
public:
int leastInterval(vector<char>& tasks, int n) {
vector<int> cnts(26, 0);
for (char c : tasks) cnts[c - 'A']++;
int maxv = *max_element(cnts.begin(), cnts.end());
int tot = count(cnts.begin(), cnts.end(), maxv);
return max(static_cast<int>(tasks.size()), (n + 1) * (maxv - 1) + tot);
}
};

Python 代码:

1
2
3
4
5
6
7
8
9
10
11
Python复制代码class Solution:
def leastInterval(self, tasks: List[str], n: int) -> int:
cnts = [0] * 26
for c in tasks:
cnts[ord(c) - ord('A')] += 1
maxv, tot = 0, 0
for i in range(26):
maxv = max(maxv, cnts[i])
for i in range(26):
tot += 1 if maxv == cnts[i] else 0
return max(len(tasks), (n + 1) * (maxv - 1) + tot)

TypeScript 代码:

1
2
3
4
5
6
7
8
TypeScript复制代码function leastInterval(tasks: string[], n: number): number {
const cnts = new Array<number>(26).fill(0)
for (const c of tasks) cnts[c.charCodeAt(0) - 'A'.charCodeAt(0)]++
let max = 0, tot = 0
for (let i = 0; i < 26; i++) max = Math.max(max, cnts[i])
for (let i = 0; i < 26; i++) tot += max == cnts[i] ? 1 : 0
return Math.max(tasks.length, (n + 1) * (max - 1) + tot)
}
  • 时间复杂度:O(n+C)O(n + C)O(n+C)
  • 空间复杂度:O(C)O(C)O(C),其中 C=26C = 26C=26 为任务字符集大小

最后

给大伙通知一下 📢 :

全网最低价 LeetCode 会员目前仍可用,快来薅羊毛!!!

📅 年度会员:有效期加赠两个月!!; 季度会员:有效期加赠两周!!

🧧 年度会员:获 66.66 现金红包!!; 季度会员:获 22.22 现金红包!!

🎁 年度会员:参与当月丰厚专属实物抽奖(中奖率 > 30%)!!

专属链接:leetcode.cn/premium/?pr…

更多详情请戳 这里 。

我是宫水三叶,每天都会分享算法知识,并和大家聊聊近期的所见所闻。

欢迎关注,明天见。

更多更全更热门的「笔试/面试」相关资料可访问排版精美的 合集新基地 🎉🎉

本文转载自: 掘金

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

LCEL在编程乐高世界中的无限可能

发表于 2024-03-25

要理解LCEL,需要有个类比的东西,下面,我就通过类比搭积木的过程,深入浅出的解释一下LCEL到底是怎么回事。

图片

在我们写程序的时候,我们经常会遇到需要将多个基本组件串联起来构建复杂功能的情况。这就像是在玩乐高积木,每个小块都有其独特的功能,但当我们将它们组合在一起时,就能创造出令人惊叹的城堡或飞船。这就是LCEL(Langchain Expression Language)的魅力所在,它让构建复杂链条变得简单而直观。

那么,让我们来了解一下LCEL提供的两大核心特性。首先是统一接口,每个LCEL对象都实现了Runnable接口,这意味着它们都支持一组通用的调用方法,如invoke、batch、stream等。这就好比是乐高积木的通用连接点,无论你如何组合,最终的结构都能保持稳固和一致。

图片

统一接口与组合原语

想象一下,如果你手中的每个乐高积木块都能以相同的方式连接,并且可以轻易地构建出任何你想要的结构,那将是多么令人兴奋的事情。这就是LCEL带给我们的魔法——它通过提供一个统一的接口,让每个组件都能以相同的方式进行调用,无论是单个调用还是批量处理,都变得轻而易举。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
python复制代码# 无LCEL的情况
def invoke_chain(topic: str) -> str:
prompt_value = prompt_template.format(topic=topic)
messages = [{"role": "user", "content": prompt_value}]
return call_chat_model(messages)

# 有LCEL的情况
chain = (
{"topic": RunnablePassthrough()}
| prompt
| model
| output_parser
)
chain.invoke("ice cream")

在这个例子中,我们可以看到LCEL如何简化了代码,并使其更加模块化。无论是串行调用、流式处理、批量执行还是异步操作,LCEL都能提供简洁的解决方案。

图片

动态配置与模型切换

在构建复杂的系统时,我们经常需要根据实际情况动态调整策略。LCEL就像是拥有变形能力的机器人,能够根据我们的需求变换形态。它允许我们在运行时选择不同的模型提供商,就像是在不同的乐高套装之间无缝切换,让我们的创作不受限制。

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
python复制代码# 无LCEL的情况
def invoke_configurable_chain(topic: str, model: str = "chat_openai") -> str:
if model == "chat_openai":
return invoke_chain(topic)
elif model == "openai":
return invoke_llm_chain(topic)
elif model == "anthropic":
return invoke_anthropic_chain(topic)
else:
raise ValueError(f"Received invalid model '{model}'. Expected one of chat_openai, openai, anthropic")

# 有LCEL的情况
configurable_model = model.configurable_alternatives(
ConfigurableField(id="model"),
default_key="chat_openai",
openai=llm,
anthropic=anthropic,
)
configurable_chain = (
{"topic": RunnablePassthrough()}
| prompt
| configurable_model
| output_parser
)
configurable_chain.invoke("ice cream", config={"model": "openai"})

在这个例子中,我们可以看到LCEL如何让我们的代码更加灵活和可配置。通过使用LCEL,我们可以轻松地在不同的模型和提供商之间切换,而无需重写整个逻辑。这种动态配置的能力极大地提高了我们的工作效率和系统的适应性。

日志记录与故障转移

在构建复杂的系统时,我们不仅需要关注功能的强大和灵活,还需要确保系统的稳定性和可追踪性。LCEL就像是一位细心的守护者,它通过内置的日志记录和故障转移机制,确保我们的系统即使在遇到问题时也能保持稳定运行。

日志记录就像是在探险过程中记录下每一步的足迹,它帮助我们回顾和分析整个探险过程,从而更好地理解系统的运行状态。而故障转移则像是在遇到险阻时,能够迅速切换到备用路径,保证探险队伍能够继续前进。

1
2
3
4
5
6
7
8
9
10
python复制代码# 无LCEL的情况
def invoke_chain_with_fallback(topic: str) -> str:
try:
return invoke_chain(topic)
except Exception:
return invoke_anthropic_chain(topic)

# 有LCEL的情况
fallback_chain = chain.with_fallbacks([anthropic_chain])
fallback_chain.invoke("ice cream")

在这个例子中,我们看到了LCEL如何简化故障转移的实现。通过使用with_fallbacks方法,我们可以轻松地为链条添加备用模型,确保当主要模型不可用时,系统能够自动切换到备选方案,从而提高系统的鲁棒性。

同时,LCEL的日志记录功能允许我们通过设置环境变量,将所有链条的执行过程记录下来,这些记录可以在LangSmith平台上查看,为我们提供了一个强大的调试和分析工具。

至此,我们已经探索了LCEL的统一接口、组合原语、动态配置、模型切换、日志记录和故障转移等核心特性。这些特性共同构成了LCEL的强大功能,使得构建和管理复杂的表达式语言链条变得简单而高效。如果你对LCEL的其他方面还有疑问或者想要深入了解,欢迎与我联系,深入沟通。

本文转载自: 掘金

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

Redis不再“开源”~

发表于 2024-03-25

大家好,我是石头~

3月20日,Redis背后的商业实体Redis Labs的首席执行官Rowan Trollope在其官方博客上公布了一项具有里程碑意义的决定:自Redis 7.4版本起,该项目将启用一套全新的双许可证体系——RSALv2和SSPLv1。

2c8375b258f943448738c760b270c70d.png

自面世以来,Redis始终坚守开源理念,依托宽松的BSD许可证授权,赋予开发者充分的自由度去使用、修改和传播代码。随着Redis Labs的迅速崛起,公司在对诸如Redis Graph、ReJSON等附加模块的管理上已先行试水Source Available License,这昭示了其开源策略逐步演进的趋势。

现在,从7.4版本起,Redis核心项目正式切换至RSALv2和SSPLv1双许可证制度,此举既是为了在保证开源共享的基础上,更好地维护商业利益,尤其是强化在云计算环境中的核心知识产权保护力度。

尽管Redis核心项目的开源本质仍在延续,但新许可证所带来的改变却至关重要。相较传统的BSD许可证,RSALv2和SSPLv1对代码的二次分发和商业用途设定了更为严谨的规定。

具体来说,按照新许可证的条款,提供Redis托管服务的云服务提供商将不能再无偿利用Redis的源代码。例如,云服务供应商只有在与Redis官方(即Redis代码的实际维护者)签署许可协议后,才有权将其客户交付使用Redis 7.4版本的产品。

然而,对于直接使用Redis开源版本或新版Redis(遵循双许可证)的终端用户,用于内部或个人用途的情况则不会有任何改变。同样地,对于那些利用Redis构建客户端库或其他集成组件的合作方,现有的使用权限也将保持不变。

MORE | 更多精彩文章

  • 别再这么写POST请求了~
  • H5推送,为什么都用WebSocket?
  • 了解Redis数据结构,一篇就够!!!

本文转载自: 掘金

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

大模型应用开发之业务架构和技术架构(从AI Embedded

发表于 2024-03-25

前言

截止到目前,我们已经从大模型的定义概念,应用层涉及的一些概念做了些了解。在大模型的应用开发中,RAG、Agent等概念我们之前也做了些简述,没有看过的可以粗略回顾一下:大模型06-大模型应用开发之准备篇(OpenAI的plugins、GPTs与RAG、Agent)。

本文我们重点讲的就是伴随着大模型的广泛应用,这些概念是在什么体系和场景下衍生的;换句话说,基于LLM,目前大家在做的应用,他主流的业务架构和技术架构都是什么样子的,我们在了解之后,可以根据依据我们现实的业务需求,来选择自己的技术路线。

技术往往一半是基础设施,一半是应用设施

就像我们的软件开发,一半是做中间件,框架等基础层的,另一半是在基础层之上,来开发应用的。

大模型目前也是,目前技术分为两个方面:

  1. 建设和训练基础大模型
  2. 建造大模型应用,或者基于基础大模型的应用开发

同时,基础大模型的建设和训练,又需要更为复杂、丰富和专业的知识,这部分长期看来,不会需要太多的人;我们绝大多数人,都会在大模型的应用层这一层,而像我在01篇写到的:

我们在不断被迫接受着过量的信息和超出认知的技术革新,否则就会处于被革新的尴尬境地。

我们大部分人或者所有人都需要接触和掌握的。

典型的业务架构

image.png

目前在实际落地场景中,广泛在用的或者是不断迭代演进的,基本都是围绕这三种类型来的:

AI Embedded模式

这个场景,很好理解,就是在我们的传统应用中,其中某个环节加入了LLM的能力来帮我们提效做一些事情。

AI Copilot模式

这种模式,是在我们的系统应用中,广泛的应用LLM的能力,再通过我们的应用进行串联,这也是目前使用最多的模式。
我们目前能看到各种各样的Copilot,Microsoft Copilot,GitHub Copilot等等。

在这些场景中,大家并不会依赖算法的结果进行最终决策,大都是作为一种信息的收集来源和参考。对比传统的搜索引擎,更多的是效率上的提升,形态其实没有发生本质变化。

AI Agent模式

这个我们可以看到,明显与前两种模式不同,前两种模式的任务主要还是以人来实现为主,LLM作为辅助。

而Agent模式,人只需要提出要求和指令,AI可以自动帮助拆解任务,完成任务的执行。

单Agent和Multi-Agent

我们之前说,在大模型领域,大模型替代了传统agent 中的规则引擎以及知识库,Agent提供了并寻求推理、观察、批评和验证的对话通道。

而Multi-Agent(多智能体系统) 是指由多个自主个体组成的群体系统,其目标是通过个体间的相互信息通信和交互作用。

在基于大模型的应用领域中,当复杂任务被分解成更简单的子任务时,LLM已经被证明了拥有解决复杂任务的能力。Multi-Agent 的通信与协作可以通过“对话”这一直观的方式实现这种子任务的分拆和集成。

为了使基于大模型的Agent适合于Multi-Agent的对话,每个Agent都可以进行对话,它们可以接收、响应和响应消息。当配置正确时 ,Agent可以自动与其他代理进行多次对话,或者在某些对话轮次中请求人工输入,从而通过人工反馈形成RLHF。可对话的Agent设计利用了LLM通过聊天获取反馈并取得进展的强大能力,还允许以模块化的方式组合LLM的功能。

基于大模型的常见单Agent 系统包括:

AutoGPT:AutoGPT是一个AI代理的开源实现,它试图自动实现一个给定的目标。它遵循单Agent范式,使用了许多有用的工具来增强AI模型,并且不支持Multi-Agent协作。

ChatGPT+ (code interpreter or plugin) :ChatGPT是一种会话AI Agent,现在可以与code interpreter或插件一起使用。code interpreter使ChatGPT能够执行代码,而插件通过管理工具增强了ChatGPT。

LangChain Agent:LangChain是开发基于LLM应用的通用框架。LangChain有各种类型的代理,ReAct Agent是其中一个著名的示例。LangChain所有代理都遵循单Agent范式,并不是天生为交流和协作模式而设计的。

Transformers Agent:Transformers Agent 是一个建立在Transformer存储库上的实验性自然语言API。它包括一组经过策划的工具和一个用来解释自然语言和使用这些工具的Agent。与 AutoGPT类似,它遵循单Agent范式,不支持Agent间的协作。

基于大模型的常见Multi-Agent 系统包括:

BabyAGI:BabyAGI 是一个用Python脚本实现的人工智能任务管理系统的示例。在这个已实现的系统中,使用了多个基于LLM的代理。例如,有一个Agent用于基于上一个任务的目标和结果创建新任务,有一个Agent用于确定任务列表的优先级,还有一个用于完成任务/子任务的Agent。BabyAGI作为一个Multi-Agent系统,采用静态Agent对话模式,一个预定义的Agent通信顺序。

CAMEL:CAMEL 是一个agent 通信框架。它演示了如何使用角色扮演来让聊天Agent相互通信以完成任务。它还记录了Agent的对话, 以进行行为分析和能力理解,并采用初始提 示技术来实现代理之间的自主合作。但是,CAMEL本身不支持工具的使用,比如代码执行。虽然它被提议作为多代理会话的基础设施,但它只支持静态会话模式。

Multi-Agent Debate:Multi-Agent Debate试图构建具有多代理对话的LLM应用程序,是鼓励LLM中发散思维的有效方式,并改善了LLM的事实性和推理。在这两种工作中 ,多个LLM推理实例被构建为多个Agent来解决与Agent争论的问题。每个Agent都是一个LLM推理实例,而不涉及任何工具或人员,并且Agent间的对话需要遵循预定义的顺序。

MetaGPT:MetaGPT 是一种基于Multi-Agent对话框架的LLM自动软件开发应用程序。他们为各种gpt分配不同的角色来协作开发软件,针对特定场景制定专门的解决方案。

基于Multi-Agent的LLM 应用开发框架:Autogen

在单Agent和Multi-Agent的应用开发中,大家看到了我们之前提到的,LangChain与Autogen,就是为了Agent开发而出现的应用开发框架。

技术架构

纯prompt

基本的对话式,你问一句,我答一句。。。

image.png

Agent + Function Calling

  • Agent:AI 主动提要求
  • Function Calling:AI 要求执行某个函数
  • 场景举例:你问过年去哪玩,ta 先反问你有多少预算

image.png

RAG(Retrieval-Augmented Generation)

  • Embeddings:把文字转换为更易于相似度计算的编码。这种编码叫向量
  • 向量数据库:把向量存起来,方便查找
  • 向量搜索:根据输入向量,找到最相似的向量
  • 场景举例:考试时,看到一道题,到书上找相关内容,再结合题目组成答案。然后,就都忘了

image.png

Fine-tuning

大模型的微调

image.png

如何选择技术路线

面对一个需求,如何选择技术方案?下面是个不严谨但常用思路。

image.png

题外话:值得尝试 Fine-tuning 的情况

刚接触LLM的小伙伴在听到Fine-tuning的时候都觉得蛮高级的,在我实际工作中应用了一段时间大模型之后,我自己的感受时,在很多基础应用场景中,我们用好提示工程,就足够了。

值得尝试 Fine-tuning 的情况

  1. 提高大模型的稳定性
  2. 用户量大,降低推理成本的意义很大
  3. 提高大模型的生成速度

总结

本文章,我们从大模型目前应用的典型业务架构和技术架构进行分析,让大家初步能够了解我们都是在如何使用LLM的,从而大家在自己的实际落地场景中,也可以对照分析,如何建设自己的业务架构和技术架构,以及选择什么样的技术路线。

本文转载自: 掘金

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

输入url到页面渲染前半段 前言 输入url到页面渲染 最后

发表于 2024-03-25

前言

当我们输入url到页面渲染的时候,电脑其实做了很多操作,其中就包括了后半段,生成dom树,cssom树,render树,回流,重绘,这个我们已经聊完了,附上链接:输入url到页面渲染后半段:回流,重绘,优化【一次性带你搞明白】 - 掘金 (juejin.cn)

本期就来把前半段给细细聊明白,这也是老八股了,面试官很喜欢问这个东西,年年如此,经久不衰……

输入url到页面渲染

url解析

首先就是判断用户输入的url是否合法,一个合法的url的样子如下

https://juejin.cn/user

  • 协议号

https://是协议号,多了个s指的是TLS,相比较http,https进行了加密

  • ip

ip因为很难记住,因此人们用域名来进行代替

  • 端口

默认端口号是80。协议号,ip,端口是同源策略三要素

  • 路径

比如user页,home页

dns解析

DNS(Domain names Systems)就是将域名对应的ip地址找到来

dns解析也有好几个步骤,这些步骤目的都是去找到对应的ip来,比如我访问baidu.com

  1. 去操作系统的本地缓存中查找ip

这就相当于第一次访问一个新的地址,浏览器会帮你把地址域名对应的ip映射关系存入本地中,这样日后再次访问就不需要查找了
2. 去系统配置的dns服务器(或缓存)中查询
3. 去根服务器中查询
4. 去com顶级域名服务器中查找
5. 去到baidu.com这个域名服务器中查找

1.png

tcp三次握手

此时已经拿到了对方的ip,接下来就是通过tcp的三次握手来建立网络连接

其实如果没有tcp照样可以进行网络通讯,这里先插入udp

udp

udp(User Datagram Protocal)用户数据报协议

以前面试官很喜欢问tcp和udp二者的区别,现在好像问这个比较少了

udp特征如下:

  1. 无连接:不需要再正式传数据之前建立双方的连接

如果网络通讯依靠tcp得话,tcp相比udp就更礼貌了,还需要先寒暄几下
2. 只是数据报文的搬运工,不会对数据报文进行拆分和拼接

从这里就可以看出,udp传输的时候可以很高效,但是不安全

总结下特点

  • 不可靠性

因为面向无连接,不会去备份数据,没有拥塞控制

这个拥塞控制我来举个情景:udp传输的时候不会去管网络好坏,都是一个劲的去传输,就像是一个莽夫在路上开车,路况你无从控制的,万一碰到了窄路就会发生拥堵

拥塞控制就是可以控制网不好的时候传少点,网好的时候传多点

  • 高效
  • 传输方式:一对一(单播),一对多(多播),多对一,多对多(广播)

虽然行为很莽夫,但是因为传输高效以及方式导致这个udp还是有很多应用场景的

比如直播,在线游戏,视频通话,只要是实时通讯的,就要用上udp协议

tcp

tcp(Transmission Control Protocal)传输控制协议

tcp特征如下

  1. 需要握手来建立连接和断开连接
  2. 通过某种算法保障了数据的可靠性

既然是网络协议,那么传输一定是会受网络影响的,tcp在传输之前会先把数据先进行备份,并且可以通过网络的好坏来进行拥塞控制,网不好就慢慢发

tcp有些字段也比较重要:

  • Sequence Number

序列号:给每个数据报文都打上序列号,保证了接收顺序

  • Acknowledgement Number

确认号:接收端期望接收到的下一个字节的编码是多少。比如你接收了第一包,确认号就是第二个包

  • 标识符

URG = 1表示数据包的优先级,1表示最高,加急的效果

ACK = 1表示该数据包有效,比如超时重传后,先前很慢过来的数据包就成了无效的

SYN = 1; ACK = 0 表示连接请求报文

SYN = 1; ACK = 1 表示应答连接报文

就是因为有这么多字段,才可以使得tcp可靠传输

第一次握手

客户端向服务端发送建立连接请求,客户端进入SYN-SEND状态,syn就是报文,send就是发送

第二次握手

服务端接收到建立连接请求后,向客户端发送一个应答,服务端进入SYN-RECEIVED状态,received就是接收到了

第三次握手

客户端接收到应答后,发送确认接收到应答,客户端进入ESTABLISHED状态,established就是建立了

面试官:能否仅两次握手?

不行。两次连接网络请求通讯完毕后,双方都会进入closed状态,假设之前有个包因为某些原因丢失了,等下次回来的时候双方都是closed状态(tcp有超时重传机制,若丢失会再次发送),服务端接收到时,进入SYN-RECEIVED状态,但是客户端因为是closed状态没有去理服务端的应答,于是服务端一直都是SYN-RECEIVED造成资源浪费,服务端在这个状态得不到应答不会自动关闭,但是三次握手,之前的包回来的时候,服务端接收到进入SYN-RECEIVED状态,得不到应答会自动关闭,就不会造成资源浪费

三次握手的目的就是用来建立连接,接下来数据通讯就是归http负责

其实tcp也可以做数据传输

http发请求

服务端响应请求返回数据

浏览器渲染页面

这里面的详细过程已经在那期文章讲得很详细了

  • 解析html代码,生成一个dom树
  • 解析css代码,生成CSSOM树
  • 将dom树和CSSOM树结合,去除不可见的元素,生成Render Tree
  • 计算布局,(回流 | 重排)根据Render Tree进行布局计算,得到每一个节点的几何信息
  • 绘制页面,(重绘)GPU根据布局信息绘制

tcp四次挥手断开连接

第一次挥手

客户端向服务端发送断开请求连接

第二次挥手

服务端接收到断开连接请求后,告诉应用层去释放tcp连接,此时服务端仍然可以给客户端发送数据包

应用层指的是OSI七层协议

2.png

路由器一般是网络层,交换机是物理层,应用层就是为终端或者应用提供网络服务

第三次挥手

服务端向客户端发送最后一个数据包FINBIT后,服务端进入Last-ACK状态

第四次挥手

客户端接收到服务端的释放连接请求后,向服务端确认应答,最终双方均进入closed状态

面试官:能否仅三次挥手

第四次就是客户端确认收到了释放连接的请求,也就是确认可以关闭,如果没有这一步,客户端就一直都是开放的状态,浪费性能

最后

输入url后第一步就是url解析,判断url是否合法,然后就是dns解析,这个过程就是解析出域名对应的ip,一层一层的找,然后就是tcp三次握手建立连接,建立好连接就是http数据通讯,服务端响应请求返回数据,浏览器通过生成html树,cssom树,合并成render树,回流重绘渲染好页面,最终tcp四次挥手断开连接

如果你对春招感兴趣,可以加我的个人微信:Dolphin_Fung,我和我的小伙伴们有个面试群,可以进群讨论你面试过程中遇到的问题,我们一起解决

另外有不懂之处欢迎在评论区留言,如果觉得文章对你学习有所帮助,还请”点赞+评论+收藏“一键三连,感谢支持!

本文转载自: 掘金

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

【Android 13源码分析】窗口显示第二步 relayo

发表于 2024-03-25
  1. 前景回顾

activity启动--一级框图.png

以冷启动的场景看启动 TargetActivity 需要2个条件:

    1. TargetActivity 所在的进程需要启动
    1. 需要SourceActivity 的 completePause 流程

启动进程和completePause 流程是异步执行的,这2个流程都会执行到 ActivityTaskSupervisor::realStartActivityLocked 方法试图启动 TargetActivity ,但是因为需要同时满足上面的2个条件,所以肯定是后执行到这个方法的流程来触发 TargetActivity 的创建和启动。

不过 TargetActivity 的启动了并不能代表屏幕上就显示了UI数据,中间还有很多流程。

1.2 应用端显示窗口一级框图

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

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

    1. 应用端的Activity启动后,会通过Session与SystemService端通信处理显示UI的逻辑
    1. 真正显示是SurfaceFlinger端是控制的,所以SystemService端还需要与SurfaceFlinger端通信

SurfaceFlinger控制屏幕显示,非常重要但不是现在分析的重点,当前阶段以黑盒的形式了解SurfaceFlinger端的概念即可,当前小节要知道SystemService会在relayoutoutWindow流程的时候创建一个Surface返回给客户端绘制。

在【Activity启动流程】的几篇记录里已经把Activity启动的主流程介绍了,也知道WMS最后做的事是触发了2个事务:LaunchActivityItem 和 ResumeActivityItem ,剩下的逻辑就又交给了应用端处理。
这两个事务后续的调用链如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
arduino复制代码LaunchActivityItem::execute
ActivityThread::handleLaunchActivity
ActivityThread::performLaunchActivity
Instrumentation::newActivity --- 创建Activity
Activity::attach --- 创建Window
Window::init
Window::setWindowManager
Instrumentation::callActivityOnCreate
Activity::performCreate
Activity::onCreate --- onCreate

ResumeActivityItem::execute
ActivityThread::handleResumeActivity
ActivityThread::performResumeActivity
Activity::performResume
Instrumentation::callActivityOnResume
Activity::onResume --- onResume
WindowManagerImpl::addView --- 创建ViewRootImpl
WindowManagerGlobal::addView
ViewRootImpl::setView --- 与WMS通信触发窗口的显示逻辑

其中 LaunchActivityItem 事务会触发 Activity 的创建,并且执行到 Activity 的 onCreate 生命周期。

ResumeActivityItem 事务则做了另外2件事:

    1. 执行到 Activity 的 onResume 生命周期
    1. 与WMS通信触发窗口的显示逻辑

可以看到Activity的创建和生命周期的执行与是否屏幕上有对应的窗口信息没有绝对的联系,因为屏幕上窗口UI的显示是在 ViewRootImpl::setView 方法中触发的。

1.3 应用端显示窗口二级框图

窗口显示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进行合成

因为是窗口是以 SystemService 端的角度来分析的,所以对比上面的框图,这3个步骤均是在WMS中完成的。而应用端会触发这WMS这3个步骤的执行,从框图中可以看到应用端这边多了一个“View绘制三部曲”的步骤。

这是因为经过第二步relayoutWindow后,应用端就有了一个可以用来绘制的Surface,所以就需要进行View的绘制了,屏幕上窗口显示的UI信息本质性就是View绘制的, View绘制后才可以进行第三步:finishDrawingWindow。

窗口显示的三部曲的触发点都是在 ResumeActivityItem 事务执行到 ViewRootImpl::setView 方法触发的,调用链如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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绘制三部曲
ViewRootImpl::performLayout
ViewRootImpl::performDraw
ViewRootImpl::createSyncIfNeeded --- 第三步:绘制完成 finishDrawingWindow
Session.addToDisplayAsUser --- 第一步:addWindow

doTraversal 方法是异步执行,所以 Session.addToDisplayAsUser 触发的 addWindow 流程是比 relayoutWindow 先执行的。

上一篇介绍【addWindow流程】知道在 SystemService 端已经为应用窗口创建对应的WindowState并且挂载到窗口树中了,现在开始分析第二步:relayoutWindow。

relayoutWindow 流程主要做了2件事:

    1. 创建装载View绘制数据的Surface
    1. 触发窗口摆放,计算窗口位置

relayoutWindow一级框图.png

应用端通过 Session 与 SystemService 端通信,应用端的ViewRootImpl下有2个成员变量分别代码应用端的Surface和应用端的窗口尺寸信息。

这2个变量在跨进程通信时以“出参”的形式传递到 SystemService 端,经过 WindowManagerService::relayoutWindow 方法处理后应用端这2个变量就有值了。

本篇分析 relayoutWindow 的第一件事:创建装载View绘制数据的Surface

本篇的核心内容和窗口层级-4-Surface树中的 “Buff类型Surface的创建与挂载” 小结内容是一样的,但是本篇会先把前面的调用流程介绍完全。

  1. 正文

winscope看window的SC.png

在【addWindow流程】中分析了一个WindowState是如何创建并被挂载到层级树中的,但是WindowState本身也是一个“容器”,其对应的Surface也是一个“容器”类型的Surface,没有UI数据。

在 【WindowContainer窗口层级-4-Surface树】中也知道了SurfaceFlinger层也映射了一个Surface树,还知道了 “容器”类型和“Buff”类型Surface的区别。

下面完整介绍 relayoutWindow 流程是如何创建“Buff”类型Surface的。

2.1 应用端逻辑处理

开始撸代码,先看一下 ViewRootImpl::setView 方法

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

public final Surface mSurface = new Surface();
private final SurfaceControl mSurfaceControl = new SurfaceControl();
final IWindowSession mWindowSession;

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
int userId) {
synchronized (this) {
// 当前第一次执行肯定为null
if (mView == null) {
mView = view;
......
int res; // 定义稍后跨进程add返回的结果
// 重点* 第二步:会触发relayoutWindow
requestLayout();
InputChannel inputChannel = null; // input事件相关
if ((mWindowAttributes.inputFeatures
& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
inputChannel = new InputChannel();
}
......
try {
......
// 重点* 第一步:addWindow流程
res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), userId,
mInsetsController.getRequestedVisibilities(), inputChannel, mTempInsets,
mTempControls);
......
}
// 后续流程与addWindow主流程无关,但是也非常重要
......
// 计算window的尺寸
......
if (res < WindowManagerGlobal.ADD_OKAY) {
......// 对WMS调用后的结果判断是什么错误
}
......
view.assignParent(this); //这就是为什么decorView调用getParent返回的是ViewRootImpl的原因
......
}
}
}
    1. 首先看到 ViewRootImpl 下面有2个和Surface相关的变量 mSurface,mSurfaceControl。 但是点击去会发现都没什么东西,这是因为真正的Suface创建是在 SystemSerive 端触发
    1. 调用 addToDisplayAsUser 方法触发了addWindow 流程
    1. 本篇重点,触发 relayoutWindow

requestLayout 这个方法写App的同学可能比较熟悉,布局刷新的使用调用 View::requestLayout 虽然不是当前 ViewRootImpl 下的这个方法,但是最终也会触发 ViewRootImpl::requestLayout 的执行。

所以看看ViewRootImpl::requestLayout的代码

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

boolean mLayoutRequested;

@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
......
// 只有主线程才能更新UI
checkThread();
// 正确请求layout
mLayoutRequested = true;
scheduleTraversals();
}
}

void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}

这个方法主要是做了2件事

    1. 线程检查,可以看到checkThread() 方法的报错很多写App的同学就很熟悉: 不能在子线程更新UI。
    1. 执行 scheduleTraversals()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
scss复制代码# ViewRootImpl

final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

// 是否在执行scheduleTraversals
public boolean mTraversalScheduled;

void scheduleTraversals() {
// 如果遍历操作尚未被调度
if (!mTraversalScheduled) {
// 将调度标志设置为true,表示遍历操作已被调度
mTraversalScheduled = true;
// 设置一个同步屏障
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// 重点 * 执行mTraversalRunnable
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
// 通知渲染器有一个新的帧即将开始处理
notifyRendererOfFramePending();
// 根据需要戳一下绘制锁
pokeDrawLockIfNeeded();
}
}

这个方法虽然代码不多,但是还是有不少知识点的,比如: 同步屏障,Vsync,感兴趣的自行了解,当前不做拓展。

当前只要知道当下一个软件Vsync到来的时候,会执行 TraversalRunnable 这个 Runnable 就好,所以重点看看这个 TraversalRunnable 做了什么。

前面看 ViewRootImpl::setView 方法的时候看到在代码顺序上是先执行 requestLayout 再执行 addToDisplayAsUser,就是因为 requestLayout 方法内部需要等待 Vsync 的到来,并且还是异步执行Runable,所以 addToDisplayAsUser 触发的 addWindow 流程是先于 relayoutWindow 流程执行的。

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

final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}

void doTraversal() {
if (mTraversalScheduled) {
// 正在执行或已经执行完毕
mTraversalScheduled = false;
// 移除同步屏障
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
.....
performTraversals();
......
}
}

这里移除了同步屏障,那么 mHandler 就可以正常处理后面的消息了, 主要流程还是在 performTraversals() 中,这个方法非常重要。
我手上android 13的源码中这个方法有1890行。 所以我省略了很多代码,保留了个人认为和当前学习相关的一些逻辑,本篇重点看注释的第2步relayoutWindow。

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
ini复制代码# ViewRootImpl

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. View绘制三部曲----
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
......
performLayout(lp, mWidth, mHeight);
......
----4. finishDrawing流程----
createSyncIfNeeded();
......
if (!performDraw() && mSyncBufferCallback != null) {
mSyncBufferCallback.onBufferReady(null);
}
......
// 触发执行回调
mSurfaceSyncer.markSyncReady(mSyncId);
......
}
    1. 后续需要介绍软绘硬绘的流程,所以可以看到硬绘的初始化逻辑也在这个方法
    1. relayoutWindow 相关,也是当前分析重点
    1. 经过第第二步relayoutWindow后就View就可以绘制了,也是需要分析的重点流程,后面会陆续写博客
    1. 绘制完成后就要通知SurfaceFlinger进行合作了,finishDrawing流程也很重要。

上面的分析有个印象就好,当前不关注其他,只看 relayoutWindow 流程,关心的太多没有重点分析对象就很容易跑偏。

2.2 ViewRootImpl::relayoutWindow

ViewRootImpl::relayoutWindow方法如下:

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

public final Surface mSurface = new Surface();
private final SurfaceControl mSurfaceControl = new SurfaceControl();

private int relayoutWindow(WindowManager.LayoutParams params, int viewVisibility,
boolean insetsPending) throws RemoteException {
......
// 重点* 1. 调用WMS的 relayoutWindow流程
int relayoutResult = mWindowSession.relayout(mWindow, ...,mTmpFrames, ..., mSurfaceControl,...);
......
if (mSurfaceControl.isValid()) {
if (!useBLAST()) {
mSurface.copyFrom(mSurfaceControl);
} else {
// 重点* 2. 给当前的成员变量mSurface赋值
updateBlastSurfaceIfNeeded(); // 目前版本都走这
}
if (mAttachInfo.mThreadedRenderer != null) {
// 重点* 3. 置硬件加速渲染器的 SurfaceControl 和 BlastBufferQueue
mAttachInfo.mThreadedRenderer.setSurfaceControl(mSurfaceControl);
mAttachInfo.mThreadedRenderer.setBlastBufferQueue(mBlastBufferQueue);
}
} else ......
// 将WMS计算的窗口大小设置到当前
setFrame(mTmpFrames.frame);
return relayoutResult;
}

这个方法主要就是跨进程通信触发 WMS的relayoutWindow流程,注意这里将 mTmpFrames 和 mSurfaceControl 作为参数传递了过去。执行这个方法前 mSurfaceControl 只是一个没有实际内容的对象,但是经过 WMS::relayoutWindow 流程处理后,mSurfaceControl 就会真正持有一个 native层的Surface句柄,有个这个native的Surface句柄,View就可以把图像数据保存到Surface中了。

而mTmpFrames 表示临时窗口大小,和 mSurfaceControl 一样传递给WMS处理,然后由WMS计算出一个值再返回到应用端。

上面这个方法我标记了3个重点,除了第一个重点是主流程外,后面2个重点都是在应用端真正拿到Surface后的处理,比如第三点会把Surface设置给硬绘渲染器。重点二是给ViewRootImpl下的mSurface赋值,执行完后这个 mSurface也就持有一个 native层的Surface句柄。

在看主流程之前先看一下ViewRootImpl::updateBlastSurfaceIfNeeded 方法:

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

private BLASTBufferQueue mBlastBufferQueue;

void updateBlastSurfaceIfNeeded() {
// 经过system_service处理后的mSurfaceControl有值
if (!mSurfaceControl.isValid()) {
return;
}
......
// 创建对象
mBlastBufferQueue = new BLASTBufferQueue(mTag, mSurfaceControl,
mSurfaceSize.x, mSurfaceSize.y, mWindowAttributes.format);
mBlastBufferQueue.setTransactionHangCallback(sTransactionHangCallback);
Surface blastSurface = mBlastBufferQueue.createSurface();
// Only call transferFrom if the surface has changed to prevent inc the generation ID and
// causing EGL resources to be recreated.
// 给当前mSurface赋值
mSurface.transferFrom(blastSurface);
}

现在知道 WMS::relayoutWindow 流程执行后拿应用端拿到到Surface的一些处理,需要回头正式看一下 relayoutWindow到底做了什么。

2.3 WindowManagerService::relayoutWindow

应该端通过Session 与 SystemService 端通信

1
2
3
4
5
6
7
8
9
10
11
typescript复制代码# Session
@Override
public int relayout(IWindow window, ...ClientWindowFrames outFrames,...SurfaceControl outSurfaceControl,...) {
......
int res = mService.relayoutWindow(this, window, attrs,
requestedWidth, requestedHeight, viewFlags, flags,
outFrames, mergedConfiguration, outSurfaceControl, outInsetsState,
outActiveControls, outSyncSeqIdBundle);
......
return res;
}

主要就是调用到通过Session调用到WMS的relayoutWindow流程,上面看到在ViewRootImpl的mSurface和mSurfaceControl对象都是直接创建的,然后将mSurfaceControl专递到了WMS,这里注意在 Session::relayout 方法的参数中应用端传过来的 mSurfaceControl 变成了:outSurfaceControl,说明这是个出参,会在 WMS::relayoutWindow 对其进行真正的赋值。

outFrames 参数也同理。

WindowManagerService::relayoutWindow 代码如下:

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

public int relayoutWindow(Session session, IWindow client, LayoutParams attrs,
int requestedWidth, int requestedHeight, int viewVisibility, int flags,
ClientWindowFrames outFrames, MergedConfiguration mergedConfiguration,
SurfaceControl outSurfaceControl, InsetsState outInsetsState,
InsetsSourceControl[] outActiveControls, Bundle outSyncIdBundle) {
......
synchronized (mGlobalLock) {
// 重点* 从mWindowMap中获取WindowState
final WindowState win = windowForClientLocked(session, client, false);
if (win == null) {
return 0;
}
......
if (attrs != null) {
// 调整窗口属性和类型
displayPolicy.adjustWindowParamsLw(win, attrs);
attrs.flags = sanitizeFlagSlippery(attrs.flags, win.getName(), uid, pid);
inAnimator.mAlpha = attrs.alpha;
......
}
.......
// 设置窗口可见 viewVisibility = VISIBLE
win.setViewVisibility(viewVisibility);
// 打印Proto日志
ProtoLog.i(WM_DEBUG_SCREEN_ON,
"Relayout %s: oldVis=%d newVis=%d. %s", win, oldVisibility,
viewVisibility, new RuntimeException().fillInStackTrace());
......
if (shouldRelayout) {
try {
// 重点* 1. 创建SurfaceControl
result = createSurfaceControl(outSurfaceControl, result, win, winAnimator);
} catch (Exception e) {
......
return 0;
}
}

// 重点* 2. 计算窗口大小 (极其重要的方法)
mWindowPlacerLocked.performSurfacePlacement(true /* force */);
......
if (focusMayChange) {
// 更新焦点
if (updateFocusedWindowLocked(UPDATE_FOCUS_NORMAL, true /*updateInputWindows*/)) {
imMayMove = false;
}
}
......
// 重点* 3. 填充WMS计算好后的数据,返回应用端
win.fillClientWindowFramesAndConfiguration(outFrames, mergedConfiguration,
false /* useLatestConfig */, shouldRelayout);
......
}

Binder.restoreCallingIdentity(origId);
return result;
}

方法开始 就执行了 windowForClientLocked 方法是从mWindowMap去获取WindowState,mWindowMap在【addWindow流程】讲过了。
然后 setViewVisibility 设置可见性了,这里的参数是传过来的,根据打印的ProtoLog:

1
ini复制代码09-25 14:10:36.963 10280 16547 I WindowManager: Relayout Window{2fa12a u0 com.example.myapplication/com.example.myapplication.MainActivity2}: oldVis=4 newVis=0. java.lang.RuntimeException

值为0,也就是VISIBLE了。

这个方法在WMS中也是个核心方法了,注释都在代码中了,当前分析的 relayoutWindow 流程,所以主要跟踪下面2个执行逻辑:

    1. createSurfaceControl : 创建“Buff”类型的Surface
    1. performSurfacePlacement :计算窗口大小 (View一般有变化也要执行 layout,WMS在管理窗口这边肯定也要执行layout)
    1. fillClientWindowFramesAndConfiguration :将计算好的窗口尺寸返回给应用端

由于篇幅原因,本篇先介绍 createSurfaceControl 这个分支是如何创建 Surface的。

2.3.1 了解“容器”和“Buff”类型的Surface

看调用栈一般除了debug外,还可以在关键点加上堆栈,比如在SurfaceControl的构造方法加堆栈,只要有触发创建SurfaceControl的地方必然会打印,然后发现有以下2个输出(模拟的场景是在MainActivity点击按钮启动MainActivity2)

addWindow触发的堆栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
yaml复制代码09-25 19:42:46.028 13422 14723 E biubiubiu: SurfaceControl mName: 4e72d78 com.example.myapplication/com.example.myapplication.MainActivity2  mCallsiteWindowContainer.setInitialSurfaceControlProperties
09-25 19:42:46.028 13422 14723 E biubiubiu: java.lang.Exception
09-25 19:42:46.028 13422 14723 E biubiubiu: at android.view.SurfaceControl.<init>(SurfaceControl.java:1580)
09-25 19:42:46.028 13422 14723 E biubiubiu: at android.view.SurfaceControl.<init>(Unknown Source:0)
09-25 19:42:46.028 13422 14723 E biubiubiu: at android.view.SurfaceControl$Builder.build(SurfaceControl.java:1240)
09-25 19:42:46.028 13422 14723 E biubiubiu: at com.android.server.wm.WindowContainer.setInitialSurfaceControlProperties(WindowContainer.java:630)
09-25 19:42:46.028 13422 14723 E biubiubiu: at com.android.server.wm.WindowContainer.createSurfaceControl(WindowContainer.java:626)
09-25 19:42:46.028 13422 14723 E biubiubiu: at com.android.server.wm.WindowContainer.onParentChanged(WindowContainer.java:607)
09-25 19:42:46.028 13422 14723 E biubiubiu: at com.android.server.wm.WindowContainer.onParentChanged(WindowContainer.java:594)
09-25 19:42:46.028 13422 14723 E biubiubiu: at com.android.server.wm.WindowState.onParentChanged(WindowState.java:1341)
09-25 19:42:46.028 13422 14723 E biubiubiu: at com.android.server.wm.WindowContainer.setParent(WindowContainer.java:584)
09-25 19:42:46.028 13422 14723 E biubiubiu: at com.android.server.wm.WindowContainer.addChild(WindowContainer.java:730)
09-25 19:42:46.028 13422 14723 E biubiubiu: at com.android.server.wm.WindowToken.addWindow(WindowToken.java:302)
09-25 19:42:46.028 13422 14723 E biubiubiu: at com.android.server.wm.ActivityRecord.addWindow(ActivityRecord.java:4248)
09-25 19:42:46.028 13422 14723 E biubiubiu: at com.android.server.wm.WindowManagerService.addWindow(WindowManagerService.java:1814)
09-25 19:42:46.028 13422 14723 E biubiubiu: at com.android.server.wm.Session.addToDisplayAsUser(Session.java:215)

relayoutWindow触发的堆栈

1
2
3
4
5
6
7
8
9
10
yaml复制代码09-25 19:42:46.036 13422 14723 E biubiubiu: SurfaceControl mName: com.example.myapplication/com.example.myapplication.MainActivity2  mCallsiteWindowSurfaceController
09-25 19:42:46.036 13422 14723 E biubiubiu: java.lang.Exception
09-25 19:42:46.036 13422 14723 E biubiubiu: at android.view.SurfaceControl.<init>(SurfaceControl.java:1580)
09-25 19:42:46.036 13422 14723 E biubiubiu: at android.view.SurfaceControl.<init>(Unknown Source:0)
09-25 19:42:46.036 13422 14723 E biubiubiu: at android.view.SurfaceControl$Builder.build(SurfaceControl.java:1240)
09-25 19:42:46.036 13422 14723 E biubiubiu: at com.android.server.wm.WindowSurfaceController.<init>(WindowSurfaceController.java:109)
09-25 19:42:46.036 13422 14723 E biubiubiu: at com.android.server.wm.WindowStateAnimator.createSurfaceLocked(WindowStateAnimator.java:335)
09-25 19:42:46.036 13422 14723 E biubiubiu: at com.android.server.wm.WindowManagerService.createSurfaceControl(WindowManagerService.java:2686)
09-25 19:42:46.036 13422 14723 E biubiubiu: at com.android.server.wm.WindowManagerService.relayoutWindow(WindowManagerService.java:2449)
09-25 19:42:46.036 13422 14723 E biubiubiu: at com.android.server.wm.Session.relayout(Session.java:267)

发现2个地方创建了SurfaceControl,而且看名字都是为MainActivity2创建的,区别就是调用栈不同,和一个是带 “4e72d78 “这种对象名的,这让我很好奇,然后我立马想到这种类型之前在窗口层级树中见过。于是dump了层级树的信息

addWindow创建的SC.png

果然就是以WindowState的名字取的,看调用栈在addWindow的时候将这个WindowState添加到层级树的时候就创建了。后面的“mCallsiteWindowContainer.setInitialSurfaceControlProperties”2个调用栈输出的也不同,代表的是调用的地方。

这就很奇怪了,在addWindow的时候就创建好了SurfaceControl为什么执行relayoutWindow的时候又创建一个?那到底是用的哪个呢?

我用winscope看了trace后发现原来是下面这个结构:

winscope看window的SC.png

原来下面创建的才是真正可见的,而带 “4e72d78 “的则是作为parent,dump一下SurfaceFlinger看一下

dump SF看window的SC.png

发现带”4e72d78 “ 的是ContainerLayer类型,而下面的是BufferStateLayer类型,也是作为起孩子的存在,我们知道BufferStateLayer类型的才是真正绘制显示数据的Surface。

容器类型的图层不能显示只能作为容器,只有BufferStateLayer才可以作为显示图层

原来在addWindow流程中,将WindowState挂在到层级树中就创建了一个容器类型的SurfaceControl,而后在执行WindowManagerService::relayoutWindow又创建了一个BufferStateLayer类型的SurfaceControl用来做真正的显示数据。

这块的内容和后面的内容其实在【WindowContainer窗口层级-4-Surface树】也介绍过了,关于“容器”类型比如WindowState的Surface是如何创建就不再重复了,但是“Buff”类型Surface的创建还是需要再说一遍,比较是本篇分析的重点。看过并且熟悉这一流程的可以忽略 2.4 小节

2.4 Buff类型Surface的创建与挂载

2.4.1 流程概览

relayoutWindow的调用链如下:

1
2
3
4
5
6
7
8
arduino复制代码WindowManagerService::relayoutWindow
WindowManagerService::createSurfaceControl
WindowStateAnimator::createSurfaceLocked -- 创建“Buff” 类型Surface
WindowStateAnimator::resetDrawState -- 设置窗口状态为DRAW_PENDING
WindowSurfaceController::init
SurfaceControl.Builder::build
SurfaceControl::init
WindowSurfaceController::getSurfaceControl -- 给应用端Surface赋值

开始撸代码,WindowManagerService::relayoutWindow 下调用 createSurfaceControl 方法有4个参数

1
2
3
4
5
6
7
8
9
10
11
csharp复制代码# WindowManagerService 

public int relayoutWindow(Session session, IWindow client, LayoutParams attrs,
int requestedWidth, int requestedHeight, int viewVisibility, int flags,
ClientWindowFrames outFrames, MergedConfiguration mergedConfiguration,
SurfaceControl outSurfaceControl, InsetsState outInsetsState,
InsetsSourceControl[] outActiveControls, Bundle outSyncIdBundle) {
......
result = createSurfaceControl(outSurfaceControl, result, win, winAnimator);
......
}

createSurfaceControl 方法有4个参数:

  • outSurfaceControl: WMS创建好一个Surface后,还需要返回给应用端用于View的绘制,就是通过这个参数,由参数命名也可以知道这是一个“出参”。
  • result: 方法执行结果
  • win: 当前窗口对应的WindowState,稍后创建Surface会挂载到这个WindowState节点之下
  • winAnimator:WindowStateAnimator对象,管理窗口状态和动画,稍后通过其内部方法创建Surface
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
csharp复制代码# WindowManagerService
private int createSurfaceControl(SurfaceControl outSurfaceControl, int result,
WindowState win, WindowStateAnimator winAnimator) {
// 1. 创建WindowSurfaceController对象
WindowSurfaceController surfaceController;
try {
// 2. 创建“Buff”类型Surface
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "createSurfaceControl");
surfaceController = winAnimator.createSurfaceLocked();
} finally {
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
}
if (surfaceController != null) {
// 3. 出参给应用端
surfaceController.getSurfaceControl(outSurfaceControl);
// 打印日志,outSurfaceControl复制到了framework的值
ProtoLog.i(WM_SHOW_TRANSACTIONS, "OUT SURFACE %s: copied", outSurfaceControl);

}......
return result;
}

这个方法主要有三步,都是围绕着 WindowSurfaceController 来的:

    1. 先创建出一个WindowSurfaceController 对象 surfaceController
    1. 通过WindowStateAnimator::createSurfaceLocked 对 surfaceController 赋值,根据方法名猜测是创建了一个Surface
    1. 通过 WindowSurfaceController::getSurfaceControl,给应用端 Surface 赋值

这么看来重点是在第二步 WindowStateAnimator::createSurfaceLocked 是如何创建Surface的。

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

WindowSurfaceController mSurfaceController;
// WindowState的状态
int mDrawState;

WindowSurfaceController createSurfaceLocked() {
final WindowState w = mWin;
if (mSurfaceController != null) {
return mSurfaceController;
}

w.setHasSurface(false);
// 打印窗口状态
ProtoLog.i(WM_DEBUG_ANIM, "createSurface %s: mDrawState=DRAW_PENDING", this);
// 重点* 1. 重置窗口状态
resetDrawState();
......
// 重点* 2. 创建WindowSurfaceController
mSurfaceController = new WindowSurfaceController(attrs.getTitle().toString(), format,
flags, this, attrs.type);
......
return mSurfaceController;
}

这里有2个重点:

    1. 设置窗口状态为 DRAW_PENDING
    1. 创建Surface

2.4.2 设置窗口状态–DRAW_PENDING

1
2
3
4
5
6
7
8
9
10
11
12
13
ini复制代码# WindowStateAnimator
void resetDrawState() {
// 设置windowState状态为DRAW_PENDING
mDrawState = DRAW_PENDING;

if (mWin.mActivityRecord == null) {
return;
}

if (!mWin.mActivityRecord.isAnimating(TRANSITION)) {
mWin.mActivityRecord.clearAllDrawn();
}
}

WindowState有很多状态,以后会单独说,这里需要注意

  1. WindowState状态是保存在WindowStateAnimator中
  2. WindowStateAnimator::createSurfaceLocked方法会将WindowState状态设置为DRAW_PENDING,表示等待绘制。

2.4.3 创建与挂载“Buff”类型Surface

继续回到主流程,看看 WindowSurfaceController 的构造方法

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
ini复制代码# WindowSurfaceController

SurfaceControl mSurfaceControl;

WindowSurfaceController(String name, int format, int flags, WindowStateAnimator animator,
int windowType) {
mAnimator = animator;
// 1. 也会作为Surface的name
title = name;

mService = animator.mService;
// 2. 拿到WindowState
final WindowState win = animator.mWin;
mWindowType = windowType;
mWindowSession = win.mSession;

Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "new SurfaceControl");
// 3. 重点* 构建Surface(也是通过makeSurface 方法)
final SurfaceControl.Builder b = win.makeSurface()
.setParent(win.getSurfaceControl()) // 设置为父节点
.setName(name)
.setFormat(format)
.setFlags(flags)
.setMetadata(METADATA_WINDOW_TYPE, windowType)
.setMetadata(METADATA_OWNER_UID, mWindowSession.mUid)
.setMetadata(METADATA_OWNER_PID, mWindowSession.mPid)
.setCallsite("WindowSurfaceController");

final boolean useBLAST = mService.mUseBLAST && ((win.getAttrs().privateFlags
& WindowManager.LayoutParams.PRIVATE_FLAG_USE_BLAST) != 0);
// 高版本都为BLAST
if (useBLAST) {
// 4. 重点* 设置为“Buff”图层
b.setBLASTLayer();
}
// 触发build
mSurfaceControl = b.build();
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
}

这个方法有4个点

  1. 第一个参数传递的字符串最终也会作为Surface的name
  2. 获取到WindowState对象,后面会设置为创建Surface的父节点
  3. 构建出一个Surface对象, 注意name和 父节点的设置。 另外可以知道也是通过makeSurface()方法构建的, 这个方法会构建出一个“容器”类型的Surface。
  4. 将Surface设置为“Buff”类型,这个非常重要,因为上一步默认还是“容器”类型,所以需要设置成“Buff”类型,再后面就是build出一个Surface了

makeSurface() 方法是如何构建Surface的需要移步【WindowContainer窗口层级-4-Surface树】看第二小节:2 容器类型的创建,就不重复介绍了。

那么到这里Surface的创建就完成了,这里可能有的人如果对Surface知识不太清楚的话会比较迷糊,WindowSurfaceController,SurfaceController,Surface到底是什么关系,这个不在当前流程的重点,暂且理解为同级吧,有WindowSurfaceController就可以拿到内部的SurfaceController,而SurfaceController又可以获取到Surface。

2.4.4 返回Surface到应用端

最后再来看一下 WMS这边创建好后的Surface是如何设置给应用端的。

应用端View的绘制信息都是保存到Surface上的,因为必定要有一个”Buff”类型的Surface,也就是上面流程中创建的这个Surface。

应用端的ViewRootImpl触发WMS的relayoutWindow会传递一个出参 :outSurfaceControl过来, 现在WMS会通过以下方法将刚刚创建好是Surface传递到应用端。

这样一来应用端就有了可以保持绘制数据的Surface,然后就可以执行 View::draw。

1
2
3
4
5
csharp复制代码# WindowSurfaceController
void getSurfaceControl(SurfaceControl outSurfaceControl) {
// 将framework层的SurfaceControl copy给应用层传递过来的outSurfaceControl
outSurfaceControl.copyFrom(mSurfaceControl, "WindowSurfaceController.getSurfaceControl");
}

2.4.5 创建Surface小结

对于Surface的知识是一个复杂的模块,是需要单独详细讲解的,目前可以知道的是原以为给WindowState创建图层就是一个,但是实际上发现创建了2个。

    1. WindowState本身对应的是“容器”类型的Surface,在“addWindow流程”就创建了,而relayoutWindow创建的是一个“BufferStateLayer”类型的Surface,这个也是被copy到应用层的Surface,说明应用层的数据是被绘制在这个Surface上的。
    1. “BufferStateLayer”类型Surface的创建其实创建一个WindowSurfaceController对象,然后内部会创建SurfaceController。从WindowSurfaceController这个类名也能看出来是针对Window显示的。
    1. 不仅仅Framework层的层级树有容器概念,SurfaceFlinger里也有容器概念
    1. 我们在执行adb shell dumpsys activity containers 看到层级结构树,最底层的WindowState其实也是个容器,不是真正显示的地方。这个点从 “containers”也能理解,毕竟是容器树。

2.4.5.1 WindowState “容器”概念拓展

WindowState是容器这个是肯定的,也是WindowContainer子类,然后他的孩子也是WindowState定义如下:

1
2
3
4
5
scala复制代码# WindowState
public class WindowState extends WindowContainer<WindowState> implements
WindowManagerPolicy.WindowState, InsetsControlTarget, InputTarget {

}

那么什么场景下WindowState下还有孩子呢?答案是子窗口,子窗口的定义在Window类型里,具体的不在当前讨论,之前我一直有个误区,我一直以为我们弹出的Dialog是子窗口,但是实际上并不是,我目前找到了一个比较常见的子窗口是PopupWindow。
以在google电话应用打开一个菜单为例

Screenshot_PopupWindow.png

对应的dump 为

dump子窗口容器.png

看的到子窗口PopupWindow的WindowState是被挂载到Activity的WindowState下
对应的winscope trace为:

winscope看sub_window的SC.png

这里能看到PopupWindow也有一个容器图层和显示图层,容器图层挂载在Activity窗口容器图层下,和Activity窗口显示图层同级

  1. 小结

本篇介绍了应用端发起relayoutWindow的逻辑,然后介绍了一些“容器”和“Buff”类型Surface的概念,知道了 relayoutWindow 流程主要是做2件事,本篇介绍了第一件事:创建Surface。下一篇开始分析窗口尺寸计算和摆放流程。

relayoutWindow -2

本文转载自: 掘金

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

1…454647…956

开发者博客

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