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

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


  • 首页

  • 归档

  • 搜索

Go语言学习1-基础入门 引言 1 Go语言配置环境变量

发表于 2024-03-13

《Go语言学习系列》

18a325e136e34114b15a6d2caa5b02de.jpeg

引言

本篇介绍 Go 语言的基础入门知识,如下贴出了 Go 语言相关的网站,方便大家学习

Go语言官方网站

代码包文档网站

Go语言中文网

Go语言开发包下载路径

《Go并发编程实战》所用到的源码实例下载路径

  1. Go语言配置环境变量

windows 下:

GOROOT={你的Go语言的根目录}
在环境变量PATH后追加;%GOROOT%\bin

linux 下:
Go 语言官方建议把 go 文件夹复制到 /usr/local目录中,但也可以复制到其他目录;编辑 /etc/profile 文件,如下:

1
2
bash复制代码export GOROOT=/usr/local/go
export PATH=\$PATH:\$GOROOT/bin

保存 /etc/profile 文件,使用 source 命令使配置生效。

1
bash复制代码source /etc/profile

注意: 路径连接符windows下是”",linux下是 “/“

Go 语言还有两个隐含的环境变量—- GOOS 和 GORACH

  • GOOS 代表程序构建环境的目标操作系统,其值可以是 liunx,windows,freebsd,darwin;
  • GORACH 代表程序构建环境的目标计算架构,其值可以是386,amd64 或 arm;

之后提到的 平台相关目录 是通过 ${GOOS}_${GORACH} 的方式来命名的。(如 Go 归档文件的存放路径就是根据 “平台相关目录” 来指定的)

设置好环境变量后,在命令行中输入 go 出现如下信息,表示成功。

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
cmd复制代码Go is a tool for managing Go source code.

Usage:

go command [arguments]

The commands are:

build compile packages and dependencies
clean remove object files
doc show documentation for package or symbol
env print Go environment information
fix run go tool fix on packages
fmt run gofmt on package sources
generate generate Go files by processing source
get download and install packages and dependencies
install compile and install packages and dependencies
list list packages
run compile and run Go program
test test packages
tool run specified go tool
version print Go version
vet run go tool vet on packages

Use "go help [command]" for more information about a command.

Additional help topics:

c calling between Go and C
buildmode description of build modes
filetype file types
gopath GOPATH environment variable
environment environment variables
importpath import path syntax
packages description of package lists
testflag description of testing flags
testfunc description of testing functions

Use "go help [topic]" for more information about that topic.
  1. Go语言安装目录介绍

不管是安装版还是免安装版,Go 语言安装目录的文件夹名都是相同,下面我们一一来介绍下【如下图是 go1.6.3.windows-amd64 的】:

image.png

文件夹名 描述
api 存放 Go API 检查器的辅助文件。其中,go1.1.txt、go1.2.txt、go1.3.txt和 go1.txt 等文件分别罗列了不同版本的 Go 语言的全部 API 特征; except.txt 文件中罗列了一些(在不破坏兼容性的前提下)可能会消失的 API 特性; next.txt 文件则列出了可能在下一个版本中添加的新 API 特性。image.png
bin 存放所有由官方提供的 Go 语言相关工具的可执行文件。默认情况下,该目录会包含 go、godoc 和 gofmt 这3个工具。image.png
blog 用于存放官方博客中的所有文章,这些文章都是 Markdown 格式的。
doc 存放 Go 语言几乎全部的 HTML 格式的官方文档和说明,方便开发者在离线时查看。
lib 用于存放一些特殊的库文件【如时区相关】。
misc 存放各类编辑器或 IDE(集成开发环境)软件的插件,辅助它们查看和编写Go代码。有经验的软件开发者定会在该文件夹中看到很多熟悉的工具。
pkg 用于在构建安装后,保存 Go 语言标准库的所有归档文件。pkg 文件夹包含一个与 Go 安装平台相关的子目录,我们称之为“平台相关目录”。 例如,在针对 Linux 32bit 操作系统的二进制安装包中,平台相关目录的名字就是 linux_386;而在针对Windows 64bit 操作系统的安装包中,平台相关目录的名字则为 windows amd64 Go 源码文件对应于以 “.a” 为结尾的归档文件,它们就存储在 pkg 文件夹下的平台相关目录中。 pkg 文件夹下有一个名叫 tool 的子文件夹,该子文件夹下也有一个平台相关目录,其中存放了很多可执行文件【可参见 1.6 标准命令概述】。
src 存放所有标准库、Go 语言工具,以及相关底层库( C 语言实现)的源码。通过查看这个文件夹,可以了解到 Go 语言的方方面面。本书的后续章节会适时地对其中的部分文件进行说明。
test 存放测试 Go 语言自身代码的文件。通过阅读这些测试文件,可大致了解 Go 语言的一些特性和使用方法。
  1. 工作区

Go 代码必须放在工作区中,工作区其实就是一个对应于特定工程的目录,它包含 3 个子目录 src 目录,pkg 目录和 bin 目录。

  • src目录
    用于以代码包的形式组织并保存 Go 源码文件。这里的代码包,与 src 下的子目录一一对应。例如,若一个源码文件被声明为属于代码包logging,那么它就应当被保存在 src目录下名为 logging 的子目录中。当然,我们也可以把 Go 源码文件直接放于 src 目录下,但这样的 Go 源码文件就只能被声明为属于 main 代码包了。除非用于临时测试或演示,一般还是建议把Go 源码文件放入特定的代码包中。
  • pkg目录
    用于存放经由 go install 命令构建安装后的代码包(包含 Go 库源码文件)的 *.a 归档文件。该目录与 GOROOT 目录下的 pkg 功能类似,区别在于,工作区中的 pkg 目录专门用来存放用户代码的归档文件。构建和安装用户源码的过程一般会以代码包为单位进行,比如 logging 包被编译安装后,将生成一个名为 logging.a 的归档文件,并存放在当前工作区的 pkg 目录下的平台相关目录中。
  • bin目录
    与 pkg 目录类似,在通过 go install 命令完成安装后,保存由 Go 命令源码文件生成的可执行文件。在 Linux 操作系统下,这个可执行文件一般是一个与源码文件同名的文件。在 Windows 操作系统下,这个可执行文件的名称是源码文件名称加 .exe 后缀。
  1. GOPATH

工作区的目录路径需要添加到环境变量 GOPATH 中。否则,即使处于同一个工作区(事实上,未被加入到环境变量 GOPATH 中的目录不应该称作工作区),代码之间也无法通过绝对代码包路径完成调用。在实际开发中,工作区往往有多个,这些工作区的目录路径都需要添加至 GOPATH 中。
如 Linux 下有两个工作区:

1
2
bash复制代码~/Go/lib
~/Go/goc2p

修改/etc/profile文件,添加环境变量GOPATH的内容:

1
bash复制代码export GOPATH=\$HOME/Go/lib:\$HOME/Go/goc2p

保存 /etc/profile 文件,并用 source 命令使配置生效。

注意:

  • GOPATH 中不要包含环境变量 GOROOT 的值(即 Go 的安装目录路径),将 Go 语言本身的工作区和用户的工作区严格地分开;
  • 通过 Go 工具中的代码获取命令 go get,可以将指定项目的源码下载到我们在环境变量 GOPATH 中设定的第一个工作区中,并在其中完成构建和安装的过程。

Windows 下直接在系统变量中添加 GOPATH 环境变量即可,其中值为你的工作区的根目录。

  1. 源码文件

Go 语言的源码文件分为 3 类:

  • Go 库源码文件
  • Go 命令源码文件
  • Go 测试源码文件

5.1 命令源码文件

声明为属于 main 代码包,并且包含 无参数声明 和 结果声明 的 main 函数的源码文件。这类文件可以独立运行(使用 go run 命令),也可以被 go build 或 go install 命令转换为可执行文件。

同一个代码包中的所有源码文件,其所属代码包的名称必须一致。如果命令源码文件和库源码文件处于同一代码包中,该包就无法正确执行 go build 和 go install 命令。换句话说,这些源码文件也就无法被编译和安装。因此,命令源码文件通常会单独放在一个代码包中。一般情况下,一个程序模块或软件的启动入口只有一个。

同一个代码包中可以有多个命令源码文件,可通过 go run 命令分别运行它们。但通过 go build 和 go install 命令无法编译和安装该代码包。所以一般情况下,不建议把多个命令源码文件放在同一个代码包中。

当代码包中有且仅有一个命令源码文件时,在文件所在目录中执行 go build 命令,即可在该目录下生成一个与目录同名的可执行文件;若使用 go install 命令,则可在当前工作区的 bin 目录下生成相应的可执行文件。

5.2 库源码文件

存在于某个代码包中的普通源码文件。通常,库源码文件声明的包名会与它实际所属的代码包(目录)名一致,且库源码文件中不包含 无参数声明 和 无结果声明 的 main 函数。如在 basic/set 目录下执行 go install 命令,成功地安装了 basic/set 包,并生成一个名为 set.a 的归档文件。归档文件的存放目录由以下规则产生:

  1. 安装库源码文件时所生成的归档文件会被存放到当前工作区的 pkg 目录中。
  2. 根据被编译的目标计算机架构,归档文件会被放置在 pkg 目录下的平台相关目录中。如上的 set.a 在我的 64 位 window 系统上就是pkg\windows_amd64 目录中。
  3. 存放归档文件的目录的相对路径与被安装的代码包的上一级代码包的相对路径是一致的。第一个相对路径就是相对于工作区的 pkg 目录下的平台相关目录而言的,而第二个相对路径是相对于工作区的 src 目录而言的。如果被安装的代码包没有上一级代码包(也就是说它的父目录就是工作的 src 目录),那么它的归档文件就会被直接存放到当前工作区的 pkg 目录的平台相关目录下。如 basic 包的归档文件 basic.a 总会被直接存放到 pkg\windows_amd64 目录下,而 basic/set 包的归档文件 set.a 则会被存放到 pkg\ windows_amd64\basic 目录下。

5.3 测试源码文件

这是一种特殊的库文件,可以通过执行 go test 命令运行当前代码包下的所有测试源码文件。成为测试源码文件的充分条件有两个:

  1. 文件名需要以 _test.go 结尾
  2. 文件中需要至少包含该一个名称为 Test 开头或 Benchmark 开头,拥有一个类型为 testing.T 或 testing.B 的参数的函数。类型 testing.T 或 testing.B 分别对应功能测试和基础测试所需的结构体。

当在某个代码包中执行 go test 命令,该代码包中的所有测试源码文件就会被找到并运行。

注意:存储 Go 代码的文本文件需要以 UTF-8 编码存储。如果源码文件中出现了非 UTF-8 编码的字符,则在运行、编译或安装时,Go 会抛出 illegal UTF-8 sequence 的错误。

  1. 代码包

Go 语言中的代码包是对代码进行构建和打包的基本单元。

6.1 包声明

在 Go 语言中,代码包中的源码文件名可以是任意的;这些任意名称的源码文件都必须以包声明语句作为文件中代码的第一行。比如 src 目录下的代码包 basic/set 包中的所有源码文件都要先声明自己属于 basic/set 包:

1
go复制代码	package set

package 是 Go 语言中用于包声明语句的关键字。Go 语言规定包声明中的包名为代码包路径的最后一个元素。如上,basic/set 包的包路径为basic/set,而包声明中的包名则为 set。除了命令源码文件不论存放在哪个包中,都必须声明为属于 main 包。

6.2 包导入

代码包的导入使用代码包导入路径。代码包导入路径就是代码包在工作区的 src 目录下的相对路径,比如 basic 的绝对路径为 E:\Go\goc2p\src\basic\set,而 E:\Go\goc2p 是被包含在环境变量 GOPATH 中的工作区目录路径,则其代码包导入路径就是 basic/set。

1
go复制代码	import basic/set

当导入多个代码包时,需要用圆括号括起它们,且每个代码包名独占一行。在调用被导入代码包中的函数或使用其中的结构体、变量或常量时,需要使用包路径的最后一个元素加 . 的方式指定代码所在的包。

如果我们有两个包 logging 和 go_lib/logging,并且有相同的方法NewSimpleLogger(),且有一个源码文件需要导入这两个包:

1
2
3
4
go复制代码	import (
"logging"
"go_lib/logging"
)

则这句代码 logging.NewSimpleLogger() 就会引起冲突,Go 语言无法知道logging. 代表的是哪一个包。所以,在 Go 语言中,如果在同一个源码文件中导入多个代码包,那么代码包路径的最后一个元素不可以重复。

如果用这段代码包导入代码,在编译代码时,Go 语言会抛出 ”logging redeclared as imported package name” 的错误。如果确实需要导入,当有这类重复时,我们可以给它们起个别名来区别:

1
2
3
4
go复制代码	import (
la "logging"
lb "go_lib/logging"
)

调用包中的代码:

1
go复制代码	var logger la.Logger = la.NewSimpleLogger()

这里不必给每个引起冲突的代码包都起一个别名,只要能够区分它们就可以了。

如果我们想直接调用某个依赖包的程序,就可以用 . 来代替别名。

1
2
3
4
go复制代码	import (
. "logging"
lb "go_lib/logging"
)

在当前源码文件中,可以直接进行代码调用了:

1
go复制代码	var logger Logger = NewSimpleLogger()

Go 语言把变量、常量、函数、结构体和接口统称为程序实体,而把它们的名字统称为标识符。标识符可以是任何 Unicode 编码可以表示的字母字符、数字以及下划线 _,并且,首字母不能是数字或下划线。

标识符的首字母的大小写控制着对应程序实体的访问权限。如果标识符的首字母是大写的,那么它对应的程序实体就可以被本代码包之外的代码访问到,也可以称其为可导出的。否则对应的程序实体就只能被本包内的代码访问。当然,还需要有以下两个额外条件:

  1. 程序实体必须是非局部的。局部程序实体是被定义在函数或结构体的内部。
  2. 代码包所在的目录必须被包含在环境变量 GOPATH 中的工作区目录中。

如果代码包 logging 中有一个叫做 getSimpleLogger 的函数,那么光从这个函数的名字上我们就可以看出,这个函数是不能被包外代码调用的。

如果我们只想初始化某个代码包而不需要在当前源码文件中使用那个代码包中的任何代码,既可以用 _ 来代替别名

1
2
3
go复制代码	import (
_ "logging"
)

6.3 包初始化

在 Go 语言中,可以有专门的函数负责代码包初始化。这个函数需要无参数声明和结果声明,且名称必须为 init,如下:

1
2
3
go复制代码	func init() {
println("Initialize")
}

Go语言会在程序真正执行前对整个程序的依赖进行分析,并初始化相关的代码包。也就是说,所有的代码包初始化函数都会在 main 函数(命令源码文件中的入口函数)之前执行完成,而且只会执行一次。并且,当前代码包中的所有全局变量的初始化都会在代码包初始化函数执行前完成。这就避免了在代码包初始化函数对某个变量进行赋值之后又被该变量声明中赋予的值覆盖掉的问题。

这里举出 《Go并发编程实战》 中的例子,帮助理解上面的包初始化,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
go复制代码	package main // 命令源码文件必须在这里声明自己属于main包

import ( // 引入了代码包fmt和runtime
"fmt"
"runtime"
)

func init() { // 包初始化函数
fmt.Printf("Map: %v\n", m) // 先格式化再打印
// 通过调用runtime包的代码获取当前机器所运行的操作系统以及计算架构
// 而后通过fmt包的Sprintf方法进行字符串格式化并赋值给变量info
info = fmt.Sprintf("OS: %s, Arch: %s", runtime.GOOS, runtime.GOARCH)
}

// 非局部变量,map类型,且已初始化
var m map[int]string = map[int]string{1: "A", 2: "B", 3: "C"}
var info string // 非局部变量,string类型,未被初始化

func main() { // 命令源码文件必须有的入口函数
fmt.Println(info) // 打印变量info
}

命名源码文件名为 initpkg_demo.go,并保存到工作区的 basic/pkginit 包中。如下图为我本机运行的结果:

image.png

在同一个代码包中,可以存在多个代码包初始化函数,甚至代码包内的每一个源码文件都可以定义多个代码包初始化函数。Go 语言编译器不能保证同一个代码包中的多个代码包初始化函数的执行顺序。如果要求按特定顺序执行的话,可以考虑使用 Channel(Go语言并发编程模型的一员)进行控制。

Go 语言认可两个特殊的代码包名称—-all 和 std。all 代表了环境变量 GOPATH 中包含的所有工作区中的所有代码包,而 std 则代表了 Go 语言标准库中的所有代码包。

  1. 标准命令概述

  • build 编译给定的代码包或Go语言源码文件及其依赖包
  • clean 清除执行其他go命令后遗留的目录和文件
  • doc 执行godoc命令以打印指定代码包。
  • env 打印Go语言环境信息
  • fix 执行go tool fix命令以修正给定代码包的源码文件中包含的过时语法和代码调用
  • fmt 执行gofmt命令以格式化戈丁代码包中的源码文件。
  • generate generate Go files by processing source
  • get 下载和安装给定的代码包及其依赖包
  • install 编译和安装给定的代码包及其依赖包
  • list 显示给定代码包的信息
  • run 编译并运行给定的命令源码文件
  • test 测试给定的代码包
  • tool 运行Go语言的特殊工具
  • version 显示当前安装的Go语言的版本信息
  • vet run go tool vet on packages

在执行上述命令的时候可以通过附加一些额外的标记来定制命令的执行过程,这些标记可以看做是命令的特殊参数,这些特殊参数可以添加到命令名称和命令的真正参数中间,如下:

  • -a 强行重新构建所有涉及的Go语言代码包(包括Go语言标准库中的代码包),即使它们已经是最新的了。
  • -n 使命令仅打印在执行期间使用到的所有命令,而不真正执行它们。
  • -v 打印出命令执行过程中涉及的Go语言代码包的名字。这些代码包一般包括我们自己给定的目标代码包,有时候还会包括该代码包直接或间接依赖的代码包。
  • -work 打印出命令执行时生成和使用的临时工作目录的名字,且命令执行完成后不对它进行删除。
  • -x 打印出命令执行期间使用到的所有命令。

常用的Go语言的特殊工具,如下:

  • fix 可以把给定代码包的所有Go语言源码文件中的旧版本代码修改为新版本。它是我们升级Go语言版本后会使用到的工具。
  • vet 用于检查Go语言源码中静态错误的简单工具。可以使用它检测一些常见的Go语言代码编写错误。
  • pprof 用于以交互的方式访问一些性能概要文件。命令将会分析给定的概要文件,并根据要求提供高可读性的输出信息。这个工具可以分析的概要文件包括CPU概要文件、内存概要文件和程序阻塞概要文件。这些内含Go语言运行时信息的概要文件可以通过标准库代码包runtime和runtime/pprof中的程序来生成。
  • cgo 用于帮助Go语言代码使用C语言代码库,以及使用Go语言代码可以被C语言代码引用。

结语

最后附上 《Go并发编程实战》 作者郝林托管到 GitHub 的 Go 命令教程,里面涉及了 Go 命令和工具的详细用法。

Go语言学习的第一天,以后持续更新。。。

本文转载自: 掘金

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

Jetpack Compose -> 无状态、状态提升,单向

发表于 2024-03-13

前言


上一章节我们讲解了重组作用域和 remember,本章我们来讲解下 Compose 的 『无状态』,状态提升,单向数据流;

无状态


所谓的无状态(Stateless)的这个状态指得是什么呢?就是控件的各种属性,比如 TextView 它就有一个状态属性,就是它的内容,我们可以通过 getText 和 setText 来分别获取设置它的内容,这个内容就是它的一个状态信息,而 Compose 是无状态的,是没有这种属性的,对应的Compose中的 Text()

1
2
ini复制代码var name = "Mars"
Text(text = name)

我们在设置这个name之后,我们是无法从其他地方拿到这个 name 的文字信息的,这个文字参数只是被 Text 拿来设置用来显示之后就扔了,后续想拿到是拿不到的,因为它根本就没有保存,这就是所谓的无状态;

这个无状态只能说是 Compose 这种声明式框架的一个特点,Compose 可以的无状态并不是绝对的,例如我们来看下面这个函数

1
2
3
4
5
kotlin复制代码@Composable
fun say() {
var text = "Hello"
Text(text)
}

这个函数中的 Text() 是无状态的,但是这个 say() 函数却是有状态的,它的里面有一个仅仅它自己能看到的字符串 “Hello”;

所以 这里所说的无状态都是指的内部的无状态,例如 say() 这个 Compose 组件,它的内部的 Hello 是有状态的,但是,当我们调用这个 say() 的时候

1
2
3
scss复制代码setContent {
say()
}

我们在 setContent 中是获取不到 say 的任何状态的;如果我们想获取这个内部状态的状态值应该怎么实现呢?

状态提升


例如我们想获取这个 text 的状态,我们需要将这个 text 放到say() 和 Text() 的外部,

1
2
3
4
5
6
7
8
9
kotlin复制代码setContent {
var text = "Hello"
say(text)
}

@Composable
fun say(value: String) {
Text(value)
}

这样,我们就可以在 say() 的外部获取到这个状态了;这种在 Compose 中就叫作状态提升(State Hoisting)这个 Hoisting 就是提升,意思就是将状态提升到外部组件中;

同理,我们也只能在 setContent 中拿到这个状态,但是如果我们想在 setContent 的外部获取到这个状态,那么就需要将这个 text 提升到 setContent 的外部;

但是,这种状态提升,要尽量少的提升,最需要的地方提升即可;

这个时候,可能就会有人有疑问了,这种状态提升,不就导致调用麻烦了吗?我只想调用 say() 函数,结果现在需要定义一个变量传递进去,这个其实也好修改,我们可以给 value 定义一个默认值

1
2
3
4
kotlin复制代码@Composable
fun say(value: String = "Kobe") {
Text(value)
}

这样就可以直接调用 say() 方法了;

无状态、状态提升的另一种特殊用法

我们接下来看另一个比较特殊的无状态、状态提升的用法,我们来看另一个组件 TextFiled 文字输入框

1
ini复制代码TextField(value = , onValueChange = ) //文字输入框

相当于原生的 EditText,它是 material 层的,不是 foundation 层,也不是 ui 层的,跟 Button 类似,是一个符合 material 风格的输入框,如果不想使用 material 风格,可以使用 BasicTextField 自己设计输入框风格;

它有两个参数,一个 value 一个 onValueChange,一个文字参数,一个文字改变监听数据变化,其中这个 value 就是提出来的参数,文字原本是内部的状态,现在提出来了,就成了输入的了,外部输入,就成了无状态的了;

1
2
java复制代码var name
TextField(name , onValueChange = ) //文字输入框

name 作为一个外部变量来充当这个无状态文本输入框的外部状态;

onValueChange 它是一个函数类型的表达式,我们可以写成

1
2
java复制代码var name
TextField(name , {}) //文字输入框

这种形式的,这个函数中要做的就是处理文字变化的事,也就是回调,我们来运行看下效果,这就是 material 风格的输入框

image.png

我们接下来输入几个数据看下:

SVID_20240312_202210_1.gif

可以看到,输入内容之后不显示,这是为什么呢?这是因为用户在输入新的内容之后并没有更新到 name 字段导致的,我们需要做如下更改:

1
2
3
4
ini复制代码var name = ""
TextField(name , onValueChange ={
name = it
})

就是说 我们并没有把用户的输入行为和显示来源做关联,这就需要我们进行一个关联才行,但是这还不是我们最终的写法,因为我们是在 Compose 中,所以这些会变的变量我们不能直接写,需要用 mutableStateOf 和 remember 将它包裹起来才行

1
2
3
4
5
6
ini复制代码var name by remember {
mutableStateOf("")
}
TextField(name , onValueChange = { newValue->
name = newValue
})

这样,我们的修改才能生效,我们运行看下效果:

SVID_20240312_204027_1.gif

可以看到,我们的输入显示到了输入框中;对于这个 TextFiled Compose 并不会主动帮我们更新,而是需要我们手动更新,那么 Compose 为什么要这么做呢?

单向数据流


带着这个问题,我们先来聊聊数据,当我们既有缓存数据又有网络数据的时候,我们是如何将缓存数据和网络数据进行一个结合呢?第一次打开的时候,本地数据为空,从网络加载到数据之后显示并存到本地数据库,当加载下一页数据的时候,取到下一页数据合并到内存后显示,同时存到数据库,如果用户杀死app,重新打开,优先取数据库的数据同时从网络取数据,然后合并到内存,同时更新数据库;

那么问题来了,在这种双通道取数据的情况下,怎么保证数据的有效性呢?如何保证数据的同步性呢?

解决的本质就是:Single source of Truth(单一数据源),这样就不会出现数据冲突的问题了;让网络数据作为本地数据的上游,这样就不会出现数据不同步的问题了,这种方案也是被 Compose 官方建议的方案;这种单一数据源在 Compose 之前就已经被 Android 使用了,例如 Jetpack ViewModel 中的 Repository,它内部就是『数据库 + 网络』的形式;

Compose 所有会用到的界面数据都是单一数据源的方式,例如我们上面说的 TextFiled

1
2
3
4
5
6
ini复制代码var name by remember {
mutableStateOf("")
}
TextField(name , onValueChange = { newValue->
name = newValue
})

我们接着来看这段代码,如果我们想给输入框做一些限制,例如,限制输入框不能输入一些标点符号,那么我们就需要在 onValueChange 的回调中判断 newValue 的值是否符合规范,在不符合规范的情况下将用户输入的内容清除掉;

那么在这种情况下, Compose 就是通过单向数据流的方式来实现用户从 输入- 修改 - 显示 的过程的;整体数据从上往下传输,事件从下往上传输,一层一层的单向数据流传递;

好了,今天的讲解就到这里吧

下一章预告


状态机制的原理

欢迎三连


来都来了,点个关注,点个赞吧,你的支持是我最大的动力~~

本文转载自: 掘金

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

面试官:线程调用2次start会怎样?我支支吾吾没答上来

发表于 2024-03-11

写在开头

在写完上一篇文章Java面试必考题之线程的生命周期,结合源码,透彻讲解! - 掘金 (juejin.cn)后,本以为这个小知识点就总结完了。

但刚刚吃晚饭时,突然想到了多年前自己面试时的亲身经历,决定再回来补充一个小知识点!

记得是一个周末去面试Java后端开发工程师岗位,面试官针对Java多线程进行了狂轰乱炸般的考问,什么线程创建的方式、线程的状态、各状态间的切换、如果保证线程安全、各种锁的区别,如何使用等等,因为有好好背八股文,所以七七八八的也答上来了,但最后面试官问了一个现在看来很简单,但当时根本不知道的问题,他先是问了我,看过Thread的源码没,我毫不犹豫的回答看过,紧接着他问:

线程在调用了一次start启动后,再调用一次可以不?如果线程执行完,同样再调用一次start又会怎么样?

这个问题抛给你们,请问该如何作答呢?

线程的启动

我们知道虽然很多八股文面试题中说Java创建线程的方式有3种、4种,或者更多种,但实际上真正可以创建一个线程的只有new Thread().start();

【代码示例1】

1
2
3
4
5
6
7
8
java复制代码public class Test {
public static void main(String[] args) {
Thread thread = new Thread(() -> {});
System.out.println(thread.getName()+":"+thread.getState());
thread.start();
System.out.println(thread.getName()+":"+thread.getState());
}
}

输出:

1
2
java复制代码Thread-0:NEW
Thread-0:RUNNABLE

创建一个Thread,这时线程处于NEW状态,这时调用start()方法,会让线程进入到RUNNABLE状态。

RUNNABLE的线程调用start

在上面测试代码的基础上,我们再次调用start()方法。

【代码示例2】

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class Test {
public static void main(String[] args) {
Thread thread = new Thread(() -> {});
System.out.println(thread.getName()+":"+thread.getState());
//第一次调用start
thread.start();
System.out.println(thread.getName()+":"+thread.getState());
//第二次调用start
thread.start();
System.out.println(thread.getName()+":"+thread.getState());
}
}

输出:

1
2
3
4
5
java复制代码Thread-0:NEW
Thread-0:RUNNABLE
Exception in thread "main" java.lang.IllegalThreadStateException
at java.lang.Thread.start(Thread.java:708)
at com.javabuild.server.pojo.Test.main(Test.java:17)

第二次调用时,代码抛出IllegalThreadStateException异常。

这是为什么呢?我们跟进start源码中一探究竟!

【源码解析1】

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复制代码// 使用synchronized关键字保证这个方法是线程安全的
public synchronized void start() {
// threadStatus != 0 表示这个线程已经被启动过或已经结束了
// 如果试图再次启动这个线程,就会抛出IllegalThreadStateException异常
if (threadStatus != 0)
throw new IllegalThreadStateException();

// 将这个线程添加到当前线程的线程组中
group.add(this);

// 声明一个变量,用于记录线程是否启动成功
boolean started = false;
try {
// 使用native方法启动这个线程
start0();
// 如果没有抛出异常,那么started被设为true,表示线程启动成功
started = true;
} finally {
// 在finally语句块中,无论try语句块中的代码是否抛出异常,都会执行
try {
// 如果线程没有启动成功,就从线程组中移除这个线程
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
// 如果在移除线程的过程中发生了异常,我们选择忽略这个异常
}
}
}

这里有个threadStatus,若它不等于0表示线程已经启动或结束,直接抛IllegalThreadStateException异常,我们在start源码中打上断点,从第一次start中跟入进去,发现此时没有报异常。

new线程.png
此时的threadStatus=0,线程状态为NEW,断点继续向下走时,走到native方法start0()时,threadStatus=5,线程状态为RUNNABLE。此时,我们从第二个start中进入断点。

runnable线程.png
这时threadStatus=5,满足不等于0条件,抛出IllegalThreadStateException异常!

TERMINATED的线程调用start

终止状态下的线程,情况和RUNNABLE类似!

【代码示例3】

1
2
3
4
5
6
7
8
9
10
java复制代码public class Test {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {});
thread.start();
Thread.sleep(1000);
System.out.println(thread.getName()+":"+thread.getState());
thread.start();
System.out.println(thread.getName()+":"+thread.getState());
}
}

输出:

1
2
3
4
java复制代码Thread-0:TERMINATED
Exception in thread "main" java.lang.IllegalThreadStateException
at java.lang.Thread.start(Thread.java:708)
at com.javabuild.server.pojo.Test.main(Test.java:17)

这时同样也满足不等于0条件,抛出IllegalThreadStateException异常!

我们其实可以跟入到state的源码中,看一看线程几种状态设定的逻辑。

【源码解析2】

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复制代码// Thread.getState方法源码:
public State getState() {
// get current thread state
return sun.misc.VM.toThreadState(threadStatus);
}

// sun.misc.VM 源码:
// 如果线程的状态值和4做位与操作结果不为0,线程处于RUNNABLE状态。
// 如果线程的状态值和1024做位与操作结果不为0,线程处于BLOCKED状态。
// 如果线程的状态值和16做位与操作结果不为0,线程处于WAITING状态。
// 如果线程的状态值和32做位与操作结果不为0,线程处于TIMED_WAITING状态。
// 如果线程的状态值和2做位与操作结果不为0,线程处于TERMINATED状态。
// 最后,如果线程的状态值和1做位与操作结果为0,线程处于NEW状态,否则线程处于RUNNABLE状态。
public static State toThreadState(int var0) {
if ((var0 & 4) != 0) {
return State.RUNNABLE;
} else if ((var0 & 1024) != 0) {
return State.BLOCKED;
} else if ((var0 & 16) != 0) {
return State.WAITING;
} else if ((var0 & 32) != 0) {
return State.TIMED_WAITING;
} else if ((var0 & 2) != 0) {
return State.TERMINATED;
} else {
return (var0 & 1) == 0 ? State.NEW : State.RUNNABLE;
}
}

总结

OK,今天就讲这么多啦,其实现在回头看看,这仅是一个简单且微小的细节而已,但对于刚准备步入职场的我来说,却是一个难题,今天写出来,除了和大家分享一下Java线程中的小细节外,更多的是希望正在准备面试的小伙伴们,能够心细,多看源码,多问自己为什么?并去追寻答案,Java开发不可浅尝辄止。

结尾彩蛋

如果本篇博客对您有一定的帮助,大家记得留言+点赞+收藏呀。原创不易,转载请联系Build哥!
一键三连.png

如果您想与Build哥的关系更近一步,还可以关注“JavaBuild888”,在这里除了看到《Java成长计划》系列博文,还有提升工作效率的小笔记、读书心得、大厂面经、人生感悟等等,欢迎您的加入!

在这里插入图片描述

本文转载自: 掘金

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

在Jetpack Compose中使用状态提升?我提升个P

发表于 2024-03-11

原文地址:# 在Compose中使用状态提升?我提升个P…Provider

写在前面

本文中提及的use开头的函数,都出自与我的 ComposeHooks 项目,它提供了一系列 React Hooks 风格的状态封装函数,可以帮你更好的使用 Compose,无需关系复杂的状态管理,专心于业务与UI组件。

这是系列文章的第已篇,全部文章:

  • 在Compose中使用useRequest轻松管理网络请求
  • 在Compose中使用状态提升?我提升个P…Provider
  • 在Compose中父组件如何调用子组件的函数?
  • 在Compose中方便的使用MVI思想?试试useReducer!

关于状态提升

”总所周知“,在 Compose 中有个思想叫做状态提升,在之前的文章Compose学习笔记2 - LaunchedEffect、状态与 状态管理中我们曾提及过。

状态提升的目的是为了让我们的组件尽可能的”无状态“,无状态的优点:

  • 可复用,组件只负责组件的职责,不持有或者少持有状态
  • 可测试,组件不持有状态,更接近于纯函数,相同输入必然有相同输出

状态提升的想法很好,但是实践的时候可能并不美妙。

可能有点丑陋的状态提升

我们快速的写一个 TodoList,来演示一下状态提升可能存在的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
kotlin复制代码@Composable
fun TestStateHoisting() {
// 在顶层组件声明状态与改变状态的函数(称之为事件)
val list = useList<Todo>()
fun addTodo(todo: Todo) {
list.add(todo)
}
fun delTodo(id: String) {
list.removeIf { it.id == id }
}
Surface {
Column {
//事件传递
Header(::addTodo)
TodoList(todos = list, ::delTodo)
}
}
}

data class Todo(val name: String, val id: String)

@Composable
fun Header(addTodo: (Todo) -> Unit) {
val (input, setInput) = useState("")
Row {
OutlinedTextField(
value = input,
onValueChange = setInput,
)
TButton(text = "add") {
addTodo(Todo(input, NanoId.generate()))
setInput("")
}
}
}

@Composable
fun TodoList(todos: List<Todo>, delTodo: (String) -> Unit) {
Column {
todos.map {
TodoItem(item = it, delTodo)
}
}
}

@Composable
fun TodoItem(item: Todo, delTodo: (String) -> Unit) {
Row(modifier = Modifier.fillMaxWidth()) {
Text(text = item.name)
TButton(text = "del") {
delTodo(item.id)
}
}
}

@Composable
fun TButton(
text: String,
enabled: Boolean = true,
modifier: Modifier = Modifier,
onClick: () -> Unit
) {
Button(onClick = onClick, enabled = enabled, modifier = modifier.padding(PaddingValues(4.dp))) {
Text(text = text)
}
}

这是一个非常完整的 ”状态提升“ 示例,但是它有一点点丑陋。例子中这种组织、管理状态的思想称之为:单向数据流,即状态(数据)从父组件向下流向子组件,数据只有一个唯一可信源,就是来自父组件的状态。子组件从过向上传递事件(通过调用由父组件传递的改变状态的函数实现传递),来改变状态。

使用状态提升,在面对一些复杂场景,例如多个不同层级的组件,需要将所有状态提升到共有的顶层组件,然后通过 props 在组件之间传递。一来代码量上提升很多,二来如果涉及修改,就会比较麻烦。

有的中间组件可能并不需要使用这些状态,或者函数。例如 TodoList 组件,在它的实现中它其实并不关心 delTodo 函数到底是什么,它也不会使用这个函数。但是为了传递到目标组件还是需要在 props 中进行声明,显得非常的笨重。

使用 useContext 来解耦组件之间的状态、事件传递

上面的例子我们只传递了两层,Root -> TodoList -> TodoItem,实际开发可能会存在更多的状态传递层级,还用这种方式显然有些笨拙了。

我们还有其他方法么?当然,我们还可以使用 ViewModel,通过它持有状态、改变状态的函数,这都很好,很符合开发 Android 的既往路线。

但是我们还可以试一试更好玩的方法,使用junerver/ComposeHooks 中的 useContext 函数,在无需创建 vm 文件的情况下,更函数式的处理状态。

改造第一步:创建上下文

首先使用 createContext 创建一个上下文对象,同时传入默认值:

1
2
3
4
5
kotlin复制代码val TodoContext = createContext(tuple(
emptyList<Todo>(), // 对应list状态
{ _: Todo -> }, // 对应 addTodo函数
{ _: String -> } // 对应 delTodo函数
))

这里我们传入的都是空值、空函数,tuple函数是我自定义的快速创建 Triple 的函数。

改造第二步:使用上下文对象提供的 Provider 组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
kotlin复制代码@Composable
fun TestStateHoisting() {
val list = useList<Todo>()
fun addTodo(todo: Todo) {
list.add(todo)
}

fun delTodo(id: String) {
list.removeIf { it.id == id }
}
// 在这个组件之下的所有组件都能使用我们暴露出的这三个内容
TodoContext.Provider(
value = tuple(
list,
::addTodo,
::delTodo
)
) {
Surface {
Column {
// Header、TodoList 都改造成无参组件
Header()
TodoList()
}
}
}
}

改造第三步:改造子组件,使用 useContext 函数获取需要的状态、函数

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
kotlin复制代码@Composable
fun Header() {
// 传入上下文,使用解构声明拿到对应顺序的函数
val (_, addTodo) = useContext(context = TodoContext)
val (input, setInput) = useState("")
Row {
OutlinedTextField(
value = input,
onValueChange = setInput,
)
TButton(text = "add") {
addTodo(Todo(input, NanoId.generate()))
setInput("")
}
}
}

@Composable
fun TodoList() {
// 拿到的todos本身就是状态,可以直接使用
val (todos) = useContext(context = TodoContext)
Column {
todos.map {
TodoItem(item = it)
}
}
}

@Composable
fun TodoItem(item: Todo) {
// 不使用的解构声明对象,可以使用`_` 作为占位符
val (_, _, delTodo) = useContext(context = TodoContext)
Row(modifier = Modifier.fillMaxWidth()) {
Text(text = item.name)
TButton(text = "del") {
delTodo(item.id)
}
}
}

完成:现在我们的组件互相之间不再耦合,无需传递状态、函数

对比改造前后,我们再也不用关心状态的传递,后续代码更新也不用担心牵一发而动全身。

总结:

  1. 使用 createContex 创建上下文对象
  2. 使用 上下文对象.Provider 作为根组件
  3. 在需要使用状态、函数的组件中使用 useContext(上下文对象)获取

探索更多

项目开源地址:junerver/ComposeHooks

MavenCentral:hooks

1
kotlin复制代码implementation("xyz.junerver.compose:hooks:1.0.3")

欢迎使用、勘误、pr、star。

本文转载自: 掘金

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

哈希思想在算法中的实践 一、引言 二、什么是哈希思想 三、删

发表于 2024-03-11

【Abstract】In order to improve the efficiency of the algorithm, the hash idea is a very useful method in the practice of the algorithm. The reasonable use of hash idea to solve the algorithm problem can effectively help us solve the algorithm problem, especially some problems are very suitable for using hash idea to solve. This article will use some examples on LeetCode to explain and illustrate some practical cases of hashing ideas in algorithm cases.

【摘要】为了提高算法的效率,哈希思想是在算法实践中很有用的的一个方法,合理运用哈希思想来解决算法问题,可以有效的帮助我们解决算法问题,尤其是有些问题非常适合使用哈希思想来解决。本文将借助LeetCode上的一些例子,来讲解和说明哈希思想在算法案例中的一些实践案例。

【关键词】 算法思想 LeetCode 算法效率 算法

一、引言

在日常进行代码编程和实际的工作中,经常会遇到需要设计一些算法的场景,哈希表作为数据结构中常见的一种形式,有很大的用处,合理使用哈希表可以有效的帮助到我们来设计更有效率的算法。一般地,在很多算法设计的时候,都会遇到动态集合的需求,需要支持插入、查找、删除的操作,使用哈希表来构建,会是一种非常有效的思路。哈希思想就是使用哈希表的手段,来解决一些实际的问题,即本文主要通过一些实践例子来试图讲解哈希思想的运用。

二、什么是哈希思想

哈希表也叫散列表,由三部分组成,一部分是关键字,也叫直接寻址表,一部分是数组,也叫哈希表或者散列表,一部分是哈希函数,也叫散列函数,可以通过关键字通过散列函数在数组中查找数据,这样理想的情况就是查找时间O(1),最差的情况会出现整个列表全都查找一遍,时间是O(n)。但是这种最差的情况一般不会出现,因为哈希表的关键字在构建的时候一般尽量选择具有唯一性的值,比如使用哈希散列算法计算出来的哈希值。哈希思想简单总结来说,就是构建一个关键字函数,然后通过哈希函数来与数组部分相关联就可以查找数据了。

当关键字数量比较小的时候,可以使用直接寻址法,把全部的关键字都构建出来,然后建立查找关系就可以了。当关键字比较多的时候,可以使用开放寻址法,按照需要,动态构建相关的关键字集合,然后再建立增删改查的关系就可以了。构建完全哈希表,静态存储全部关键字,可以在完全哈希表中最坏情况下完成关键字查找,这种在有些情况下也是合适的做法,具体采用何种做法,视具体情况而定。

当关键字无法保证覆盖全部的数据时,就会出现关键字冲突,导致一个关键字对应多个数据,采用的方法很多,比如对于冲突的数据采用链表的方式,或者红黑树来建立查找关系,通过一一查找来寻找数据,这种方法好处是基本可以覆盖所有的数据,坏处是,在冲突的关键字上会存在数据堆积现象,可能需要把冲突的数据全部查找一遍才可以,而且还需要另外开辟一个空间来存储数据,增加空间消耗。当然还有别的办法,比如开放寻址法,当关键字冲突的时候,使用在散列法,使用该关键字再生成一个新的关键字,如果还冲突就重复再用新生成的这个关键字生成一个关键字,直到不重复为止,这种方法的优点是,保证可以放下全部的数据,节省空间,这种方法的缺点是不能真正地删除数据,因为后面新生成的关键字是可能和之前删除的关键字相关的,只能标记一下已经删除了。在开放寻址法中重新生成冲突的关键字的方法,还有链表法,还有比如线性探查法,直接依次探查下一个关键字就可以了,还有比如二次探查、双重散列、建立公共溢出区等等方法。

随着哈希表数据的增加,可以使用装填因子也叫装载因子来表示当前数据的填满程度,装填因子的计算方法是装入哈希表中的关键字个数与当前哈希表总的长度的比值,当装填因子越大的时候,填充的数据越多,空间利用率越高,但是关键字冲突的可能性就会增加,查找数据的成本可能就会增加。当装填因子越小的时候,填充的数据越少,空间利用率就越低,关键字冲突的概率就会减少,查找数据的成本越低。往往在设置装填因子的时候,就需要根据实际情况做一些取舍和平衡来冲突的概率和空间利用率,然后根据装填因子来决定是否增加哈希表的长度还是什么别的处理。

文本的代码实现都是使用Java实现。

三、删除字符使频率相同

题目描述:

给你一个下标从0开始的字符串word,字符串只包含小写英文字母。你需要选择一个下标并删除下标处的字符,使得word中剩余每个字母出现频率相同。如果删除一个字母后,并且恰好只删除一个字母,word中剩余所有字母的出现频率都相同,那么返回true,否则返回false。比如输入的字符串是aaccc,’c’的数量是3,数量减1,就可以让’a’和’c’的数量一样,所以返回tue。1

首先分析下该问题,首先我们需要遍历一遍字符串,首先统计出所有字母的出现频率。英文字母有26,且不考虑大小写,只有小写字母,那么我们就可以构建一个哈希表来记录字母的出现频率,表的长度最大值也就是26了,因为关键字数量很小,所以可以首先初始化出一个26长度的数组,也就是哈希表,然后再去遍历整个字符串,按照26个字母的顺序,来记录频率数据,所以其实这就相当于建立一个字母和频率一一对应关系的字典,这样就可以节约内存空间来避免保存所有的字母,因为数组可以按照字母顺序直接去读取,所以还可以节省插入时的时间消耗。

然后就可以来处理上一步得到的数组了。首先如果得到的所有字母的最大数量就是1,也就是说字符串里所有的字母都不相同,那么可以直接返回为true,因为任何一个数量减1之后都可以符合条件。然后遍历一遍这个数组,可以得到数量的最大值(下方代码中的max,代表出现数量最多的次数是多少)和最小值(下方代码中的min,代表出现数量最少的次数是多少),还可以得到最大数量出现的次数(下方代码中的countMax,代表有多少种字母是这个最大数量max的),和最小数量出现的次数(下方代码中的countMin,代表有多少种字母是这个最小数量min的)。如果判断最大数量出现的次数等于全部的字符长度,那就代表该字符串全部的字母都相同,除非该字符串word的长度小于等于2可以返回为true,否则是返回false。如果最大数量只出现了一次,也就是只有一种字母数量最多,那么最大的数量减去最小的数量,差值为1或者0,比如abcc、abb、cdd、ab、ef这样的字符串,这些都是可以返回为true的,如果是abccc、abbccc、abbb这样的字符串就该返回false。如果最大数量只出现了一次,也就是只有一种字母数量最多,并且其他的字母都是1个,比如accccc、bffff这样的字符串,这些都是返回的true,除此之外的其他的情况都是false。如果最大数量出现了多个,但是最小数量的只出现了一个,并且这两种加起来的数量刚好等于字符串的长度,就代表,比如abbbccc、effgg这样的字符串,这些都应该返回true,除此之外的其他情况都是false,比如abbcccfff这样的字符串就该是false。

实现的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java复制代码class Solution {
public boolean equalFrequency(String word) {
int[] hash = new int[26];
int max = 0;
for (char c : word.toCharArray()) {
hash[c - 'a']++;
max = Math.max(hash[c - 'a'], max);
}
if (max == 1) return true;
int countMax = 0, countMin = 0, min = max;
for (Integer value : hash) {
if (value == 0) continue;
if (value < min) {
countMin = 1;
} else if (value == min) {
countMin++;
}
min = Math.min(value, min);
if (value == max) {
countMax++;
}
}
if (countMax == word.length()) return word.length() <= 2;
if (countMax == 1) {
return max - min == 1 || max - min == 0 || (countMin == 1 && min == 1 && max + min == word.length());
} else {
return countMin == 1 && min == 1 && ((countMax * max + countMin) == word.length());
}
}
}

以上代码在本地执行的时候,当输入是s:wordgoodgoodgoodd时,得到的结果是false计算耗时为23ms。当输入是s:abbbcccdddeeefff时,得到的结果是true计算耗时为24ms。在leetcode上执行用时:0ms,计算的很快就得到了结果。因为环境的差异,大家在比较算法的效率高低的时候,以leetcode上的为准。

四、串联所有单词的子串

题目描述:

给定一个字符串s和一个字符串数组words。words中所有字符串长度相同。s中的串联子串是指一个包含words中所有字符串以任意顺序排列连接起来的子串,比如words = [“ab”,”cd”,”ef”],那么”abcdef”,”abefcd”,”cdabef”,”cdefab”,”efabcd”,和”efcdab” 都是串联子串。”acdbef”不是串联子串,因为他不是任何words排列的连接。返回所有串联字串在s中的开始索引。你可以以任意顺序返回答案。2

方法一

看到这样的问题时,最简单的做法是,首先生成word数组中的全排列,生成全排列的方法有很多,比如邻位对换法、回溯算法等等,这里不做赘述,但是无论是何种全排列方法,随着word数组中的数量增加,最后的时间复杂度都是指数级增长的,最后有可能会花费相当长的时间来生成全排列,如果电脑资源不够的话,甚至会生成失败,对资源是一种极大的浪费。所以看到这样的问题,如何缩短时间复杂度呢?问题的关键就是如何处理关键字。

滑动窗口也叫双指针,是我们在设计算法的时候的一个很有用的方法。在面对需要我们处理字符串的情况下,可以设计一个窗口,窗口的大小可以是固定的,也可以动态变化的,之后按照一定的方向依次去扫描字符串或者数组或者其他的数据形式,然后在每次扫描之后处理窗口中出现的字符串或者数组或者其他的数据。在本问题中,首先统计下words中字符串出现的次数然后保存在wordMap中,当做我们当前要比较的哈希表,这样在之后进行比较的时候就不用每次都再来计算一遍wordMap了。因为words中所有的字符串长度相同,所以可以把单个字符串的长度记为wordLength,把words的字符数量记为n,那words中所有的字符串拼到一起,可以组成的字符串的长度是maxWordLength。然后从左往右,每次开始的时候复制一份wordMap到diffMap中,作为当前比较的哈希表,依次读取n个长度为wordLength的字符串,然后判断是否在diffMap中,如果出现了不存在于diffMap中的字符串,就代表该次读取出来的字符串不符合要求,直接开始下一轮读取。那如何判断该次读取出的字符串是否存在于diffMap中呢?可以把diffMap中的对应的值减1,一直减到如果出现小于0的情况出现,就表示当前的读取出来的字符串不符合要求,因为每次读取出来的都是n个字符串,在进行减1操作的时候,除非全部符合diffMap的字符数,否者就会出现某一个关键字减1之后小于0的情况出现,这样就可以就可以简化运算,也就是说除非出现小于0的情况,除此之外就表示都是符合要求的。

实现的代码如下,在leetcode上执行用时:91ms。

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复制代码class Solution {
public List<Integer> findSubstring(String s, String[] words) {
List<Integer> result = new ArrayList<>();
if (words.length == 0) return result;
int wordLength = words[0].length(), maxWordLength = wordLength * words.length;
Map<String, Integer> wordMap = new HashMap<>();
for (String word : words) {
wordMap.put(word, wordMap.getOrDefault(word, 0) + 1);
}
Map<String, Integer> diffMap = new HashMap<>();
for (int i = 0; i < s.length() - maxWordLength + 1; i++) {
diffMap.clear();
diffMap.putAll(wordMap);
boolean haveIndex = true;
for (int n = 0; n < words.length; n++) {
String word = s.substring(i + n * wordLength, i + (n + 1) * wordLength);
int value = diffMap.getOrDefault(word, 0) - 1;
if (value >= 0) {
diffMap.put(word, value);
} else {
haveIndex = false;
break;
}
}
if (haveIndex) {
result.add(i);
}
}
return result;
}
}

方法二

如果仔细观察,就会发现以上的算法存在一种浪费的情况,那就是每次读取了n个字符串出来做比较的时候,会发现是存在重复读取字符串来比较的。基于此,可以优化以上的算法。在初始化wordMap的时候,都标记为负数,作为哈希表。从左往右读取,记字符串s的长度为ls,记开始读取字符串的位置i,记words中的单个字符串长度为wordLength,words的字符串数量记为n,从i开始,读取n个长度为wordLength的字符串,那就是把i~ls的字符串划分为数个长度为wordLength的字符串,这样,最后几个不满足wordLength的字符串就可以舍弃掉了,因为无法构成一个符合要求的字符串了。

设计好了滑动的窗口,然后就是开始移动,每次开始移动的时候,把wordMap中的数据写入到diffMap中初始化diffMap作为当前的哈希表,每次移动一个长度为wordLength的字符串,然后给diffMap中对应的值加1,如果出现了等于0的情况就删除该键值,如果diffMap中刚好全都被移除了,表示刚好符合要求,记录下当前的值,除此之外表示全都不满足要求。需要注意的是,外层循环的部分,只需要0~wordLength-1次,因为wordLength长度的字符串才能符合要求,所以实际上的开始位置在wordLength位置上的时候,就已经在第一轮的滑动窗口中比较过了。

最终的实现代码如下,在leetcode上执行用时:11ms。

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
java复制代码class Solution {
public List<Integer> findSubstring(String s, String[] words) {
List<Integer> result = new ArrayList<>();
if (words.length == 0) return result;
int wordLength = words[0].length(), maxWordLength = wordLength * words.length;
Map<String, Integer> wordMap = new HashMap<>();
for (String word : words) {
wordMap.put(word, wordMap.getOrDefault(word, 0) - 1);
}
Map<String, Integer> diffMap = new HashMap<>();
for (int i = 0; i < wordLength && i + maxWordLength <= s.length(); i++) {
diffMap.clear();
diffMap.putAll(wordMap);
for (int j = 0; i + (j + 1) * wordLength <= s.length(); j++) {
int startIndex = i + j * wordLength;
int endIndex = i + (j + 1) * wordLength;
if (j >= words.length) {
String firstWord = s.substring(startIndex - maxWordLength, endIndex - maxWordLength);
int value = diffMap.getOrDefault(firstWord, 0) - 1;
if (value == 0) {
diffMap.remove(firstWord);
} else {
diffMap.put(firstWord, value);
}
}
String nextWord = s.substring(startIndex, endIndex);
int value = diffMap.getOrDefault(nextWord, 0) + 1;
if (value != 0) {
diffMap.put(nextWord, value);
} else {
diffMap.remove(nextWord);
if (diffMap.isEmpty()) {
result.add(endIndex - maxWordLength);
}
}
}
}
return result;
}
}

可以看到相比于方法一,速度提高了8倍。滑动窗口往往可以和其他算法相互结合,比如堆栈、队列,根据实际的需要,可以帮助提高算的效率。在利用哈希表来设计算的时候,如果可以使用到滑动窗口的话会非常的有用。

五、直线上最多的点数

题目描述:

给你一个数组points,其中 points[i] = [xi, yi] 表示 X-Y 平面上的一个点。求最多有多少个点在同一条直线上。3

看到这样的描述,可以显而易见的发现,这是一个二维平面坐标,也就是可以使用二元一次函数来表示是,也就是y=ax+b形式的函数,其中的特例是x=c(c表示一个常数),二元一次方程都可以用上面的函数来表示,用这样的函数形式就可以表示X-Y平面上所有的直线了。这样的话就可以用两个点来表示一条直线了,然后只需要判断剩下的点是否在当前这条直线上就可以了。那我们借此来构建哈希函数,然后按照分桶算法来给点分组,然后计数,然后输出其中最大的值即可。

分桶算法就是很直观的一种哈希思想实现,按照关键词,把数据分成多组,然后对数组进行处理,每个桶分别维护自己内部的数据,然后管理每个桶就可以了。在本问题中因为两点成一条直线,所以可以用[xi, yi]和[xi+1, yi+1]两两相减,然后就可以按照y=ax+b来计算出a和b,或者当x坐标都一样的时候就可以按照x=c来计算出c,对于y=ax+b的形式,首先按照a分成一组,然后在这一组数据中再按照b再进行一次划分,细划分出新的桶来,另外对于x=c的形式,把c作为关键字划分出数据来当做一个桶。以a、b、c作为哈希表的关键字分别计算,这样的话就可以把数据分到一个个的桶中,然后再去计数有多少点是一条直接上的就可以了。

以下是代码实现,在leetcode上执行用时: 29ms。

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复制代码class Solution {
public int maxPoints(int[][] points) {
if (points.length <= 2) return points.length;
int result = 0;
Map<Float, Map<Float, Set<int[]>>> map = new HashMap<>();
Map<Integer, Set<int[]>> xMap = new HashMap<>();
for (int i = 0; i < points.length - 1; i++) {
for (int f = i + 1; f < points.length; f++) {
float dx = (float) points[i][0] - points[f][0];
float dy = (float) points[i][1] - points[f][1];
float pi = dy / dx;
float b = points[i][1] - pi * points[i][0];
Set<int[]> value = null;
if (dx == 0) {
value = xMap.computeIfAbsent(points[i][0], k -> new HashSet<>());
}
if (value == null) {
Map<Float, Set<int[]>> bMap = map.computeIfAbsent(pi, k -> new HashMap<>());
value = bMap.computeIfAbsent(b, k -> new HashSet<>());
}
value.add(points[i]);
value.add(points[f]);
result = Math.max(value.size(), result);
}
}
return result;
}
}

总结

通过以上的了例子来讲解说明,在实际中如何使用哈希表来设计高效的算法。在实际的操作做,关键字、哈希表、哈希函数,有很多种的设计方式,按照自己的需要选择合适的就可以了。其中关于关键字函数的设计有很多种,比如直接寻址法、平方取中法、折叠法、随机数法等。在实际的查找过程中,还可以和其他的算法相互结合来优化算法的效率,比如差分算法、分桶算法、二叉树、平衡树、链表、队列、前缀和等等。在数据量较大的情况下,尤其在现实的实际使用场景中,关键字冲突有可能频繁地出现,在冲突出现的时候,可以使用二叉树、红黑树、队列等等方式来保存数据,但是如果出现最差的情况,就可能出现关键字全部都一样,导致所有的数据都堆积在一起,降低查询的效率。这也是在工业上设计哈希函数的时候要注意的问题,要对极差情况,攻击者可以使用特定的方式,来让哈希表出现最差的情况,从而拖慢速度,从而发起攻击。

如何设计合理的哈希函数,往往都是根据具体的情况来分析。但总的原则来说就是希望关键字尽量的唯一不会出现重复冲突,还有就是尽量地让数据保持平衡,把数据尽量的散列开来,不要让某个或者某些关键字出现大量的数据堆积,从而拖慢速度。在实际应用中,哈希表往往是动态扩容的,当装填因子渐渐变大的时候,如何进行动态扩容也是会在实际运用中可能需要面对的问题。一般地可以在装填因子达到某个阈值之后,就去申请一个新的更大的哈希表来保存数据,然后把数据迁移过去。当然也可以在装填因子变得太小的时候,缩小哈希表,从而节省空间,但是否需要这么做,要根据具体的信息来决定,比如这种装填因子是否只是短期内的波动,如果频繁地更新哈希表,也会导致效率下降,比如波峰可能只有一段时间内,超过一段时间之后,就恢复成可接受的装填因子了。这些信息都可能会决定是否需要动态缩表或者动态扩表。如果一次性的完成动态缩表或者扩表,就可能对当前正在进行的操作造成效率降低的问题。可以先申请新的哈希表,把新的数据写入到新的哈希表中,然后分批把旧的数据写入到新的哈希表来完成缩表或者扩表,对于这个时候的查询和删除来说,就需要先去在新的哈希表操作,然后再去旧的哈希表操作,这样既可以降低动态改变哈希表的影响,也可以完成动态改变哈希表。所以操作的复杂度、时间复杂度、空间复杂度,就需要做出权衡,如果可以全部顾及那自然是最好的,否则就需要选择一个对自己最好的选择。

本文中只是讲了比较简单一些的例子,在实际运用中,会比上面的例子管理的数据庞大得多,但是基本的哈希思想是不变的,就是创建一个哈希表,然后来建立快速的查找关系,尤其在大量的数据面前,是可以收获到很好的效果的。比如安全加密、数据校验、LRU缓存淘汰算法、word中的拼写检查等等,这些都会用到哈希思想。还有在大型系统中的负载均衡,如何快速地管理系统中的服务器并找到对应的IP地址,就可以建立一个哈希表来增删改查从而达到负载均衡的目的。还有比如数据分片,管理大型日志库,如何查找日志库中查询频率最高的关键字,如何判断图片是否在图库之中等等。还有分布式的存储、布谷鸟哈希表等等,这里不多做赘述,这些的应用都会很常见。合理使用哈希思想确实可以帮助我们有效地提高算法效率。

参考文献

[1] Thomas H.Cormen, Charles E.Leiserson, Ronald L.Rivest, Clifford Stein:算法导论[M].殷建平,徐云,王刚,刘晓光,苏明,邹恒明,王宏志.
机械工业出版社,2012-12

Footnotes

  1. leetcode.cn/problems/re… ↩
  2. leetcode.cn/problems/su… ↩
  3. leetcode.cn/problems/ma… ↩

本文转载自: 掘金

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

在大公司工作之后才真正领悟到它真的是宇宙级编辑器 开胃菜 与

发表于 2024-03-11

开胃菜

我们在用vscode写代码时候经常需要选中文本,比如下图
20240310160850_rec_.gif

但绝大多数情况下我们想选中的是整个类名,如下图
20240310161455_rec_.gif
其实这个就牵扯到了vscode的分词机制,它认为-应该截断文本

其实不止-,还有其他字符都被vscode认为是分隔符

所以贴心的它提供了如下配置

1
2
3
4
5
6
7
js复制代码{
// 如下是被vscode认为是分隔符的字符
// 我们在设置中搜索editor.wordSeparators
// 然后根据自己的需要删除不想要的分隔符即可
// 比如删除@,这样我们就可以直接选中less变量和装饰器如@xxx
"editor.wordSeparators": "`~#!@$%^&*()-=+[{]}\\|;:'\",<>/?."
}

我把webstorm的设置全看了一遍,没有找到类似配置,如果有大佬知道怎么在webstorm配置的还请评论区留言指教

如果你觉得上方这种小配置可以提升你的代码编写效率以及幸福感,那就继续往下看吧,一定不会让你失望>_<

与vscode并不美丽的邂逅

本人是通过Java入门编程的,接触的第一个编辑器是ecilpse,后来idea越来越火,让我第一次接触到了jetbrains家的编辑器,刚开始使用idea时我就受到了深深的震撼,原来编辑器之间亦有差距。后来机缘巧合成为了前端,第一次接触到同为jb家出品的真正的前端编辑器webstorm,由于idea使用的很熟,所以基本无缝切换。

因为本人一直对数学很感兴趣,小时候就参加过奥数比赛,接触过一些算法题,再加上有java开发经验,所以typescript上手也很快且很感兴趣。因为擅长这两项,所以即使学历平庸,甚至都不是计算机相关专业,但仍然可以得到jz及la的面试官认可并与他们成为同学

犹记得刚进jz的时候,因为公司有提供正版jetbrains开发工具,但我发现身边的同学都在使用免费的vscode,所以让我非常诧异,为什么放着webstorm不用,都去用vscode。肯定是收费的更好用啊!所以小组内只有我一个人在使用webstorm,后来因为安全原因我们组必须要安装一个插件,但是一开始只有vscode版本的,也就意味着必须要使用vscode,我有尝试使用,但是发现她就和一张白纸一样,令我很不习惯,所以我拒绝使用,甚至和mentor吵了一架。

现在想来,估计是我当初付费购买的时候就已经被它给CPU了,为了说服自己不是冤大头,自己也在催眠自己吧。在此不得不感谢那位mentor,我当时应该是魔怔了,但她仍然不离不弃,甚至找了心理部门的同学给我做心理辅导及插件开发组的同学为我讲解vscode的相关配置,让开发更搞笑。在此,对生命中遇到的那些给与过我帮助的人,说一声谢谢——这个世界并不温柔,但有些人真的很温柔^_^

话不多说,下面进入正题。因为前端开发领域各不相同,所以我会进行分类讲解

通用

字符串里的文件路径快速跳转到对应文件中

有些字符串里的文件路径支持cmd+点击跳转。如大多数的导入语句,见下图
20240408211654_rec_.gif

但其他位置的字符串里的文件路径大多不支持跳转,如下图
image.png

安装插件

marketplace.visualstudio.com/items?itemN…

将鼠标光标放到包含文件路径的字符串上,使用cmd + e(Windows系统ctrl + e)

即可弹出最匹配的文件查询,就我使用的情况来看,第一个总是最匹配的。

如果默认行为不符合,我们可以手动选择字符串,它就会按我们选择的字符串精确搜索

如果这个快捷键不符合你的要求,可以自行修改

20240408213723_rec_.gif

更快捷清晰的打印调试

有时候我们为了排查问题,需要打印一些东西,其实大部分情况下这属于一种模板操作

如下图,复制要打印的变量,然后输入自定义代码片段

1
2
3
4
5
6
js复制代码"log打印": {
"prefix": "clog",
"body": ["console.log('[ $CLIPBOARD ] >', $CLIPBOARD)$0"],
"description": "log打印",
"scope": "typescript,typescriptreact,javascript,javascriptreact"
},

20240312123929_rec_.gif
其实可以通过设置宏一步完成

20240312124632_rec_.gif

也可以安装插件实现,类似的插件有很多

我举其中一个,感兴趣的可以自行搜索console关键词就行

marketplace.visualstudio.com/items?itemN…

在此顺便推荐一个非常好用的vscode代码片段生成工具

snippet-generator.app/?descriptio…

举个例子,把自己常用的代码片段或webstorm里好用的代码片段转移过来

20240313105006_rec_.gif

清晰的代码高亮

查看代码时不需要进入对应文件,甚至都不需要鼠标hovre就能知道一段代码到底是什么类型

看到紫色的加粗属性我就知道它是只读的

看到绿色斜体我就知道它是枚举,紫色斜体是枚举值

看到黄色下划线我就知道它是被async修饰的方法,我下意识就会考虑到要不要加await调用,虽然eslint之类的也能检测到,但它只会一刀切的全部警告。实际上并不一定要加await修饰,所有都警告一定是好的吗?无用的警告,甚至错误的警告只会影响写代码的心情。它应该表达一种语义,用我喜欢的不同于变量,属性之类的其他颜色就很好

20240313125122_rec_.gif

更快捷的功能入口

资源管理器以及源代码管理面板里的功能tab是可以拖动或者隐藏的

可以隐藏不想要的tab让资源管理器更加清爽

也可以把一些常用的gitlens的tab拖出来,让git视图空间更大,更清晰
20240313130254_rec_.gif

让文件夹层级更清晰

默认情况下,如果一个文件夹下面只有一个文件夹,那么这两个文件夹会合并展示

如下图,style的下层文件夹是public吗?

不,其实是css,这种不统一的展示至少会让我有些困惑
image.png

更改如下配置即可展示为下图样子

1
js复制代码"explorer.compactFolders": false

image.png

快速打开并利用vscode自身强大的功能修改非项目文件

有时候我们安装使用一些工具时需要修改配置文件或是host文件啥的,那些文章一般都会说使用vim之类的命令,说实话真的很难用,不仅看着不清晰,修改更是不方便。如下图
20240315125939_rec_.gif

其实vscode提供了一个code命令,可以很方便的打开文件或文件夹,同样是修改上述文件

20240315130237_rec_.gif

自动复制终端中选择的文本

有时候我们在启动项目时会出现一些报错需要通过百度去解决

这时候就需要去搜索一些终端打印的关键字,配置自动复制的话就可以省去手动复制的操作

1
js复制代码"terminal.integrated.copyOnSelection": true

禁止通过拖放来移动选中内容

有时候选中了一些文本,但因为误触不小心把代码移到了别处,如下
20240310184145_rec_.gif
这个因人而异,我是不需要这个功能,反而有时候因为误触让我很困惑,配置如下

1
2
js复制代码// 改为false即可禁止拖动
"editor.dragAndDrop": false

代码提示与跳转

package.json

有些包的版本使用了^修饰,导致版本并未完全锁定,有时候可能某个小版本有问题需要我们排查

这个时候就需要知道我们具体安装了哪个版本

通过下图我们可以知道当前安装的版本,最新版本,也可以直接进入查看细节
20240313133556_rec_.gif
我们知道有些包是间接依赖的,并没有列在package.json里,我想知道node_moduels里到底有没有,可以直接搜索,以lodash为例,如下图
20240313134004_rec_.gif

Vue

属性与方法等提示和跳转

20240313134447_rec_.gif

css类名提示和跳转

20240313134641_rec_.gif

vuex跳转

20240313135326_rec_.gif

文件路径提示与跳转

20240313135454_rec_.gif

React

本人主用react,个人认为vscode对react的支持比vue更好,上面vue能做到的,react也都能做到,就不赘述了

这里简单再举个css的例子
20240313140245_rec_.gif
顺便就此说一个相关的,属性的值,如className到底是字符串还是jsx表达式,vscode默认是自动推断的,也就是说className默认是字符串。

由此就出现了一个问题,比如本人项目中使用的都是cssmodule,其实需要输入jsx表达式。其实倒不如这样说,绝大多数属性值,包括className,其实我想输入的都是jsx表达式,可以进行如下配置

1
2
js复制代码"javascript.preferences.jsxAttributeCompletionStyle": "braces",
"typescript.preferences.jsxAttributeCompletionStyle": "braces"

20240313140832_rec_.gif

话说webstorm,你连大小写都不认识了吗,这样会生效吗,你还****跳转???真是**!

这里就不得不说webstorm的一大缺点,总是喜欢强行提示,给人一种仿佛很智能的感觉。所以很吃性能,容易卡顿
20240313141021_rec_.gif

vscode不仅可以正确提示,对于错误也有很清晰的解释,并且提供了解决方案
20240313141437_rec_.gif

结语

作为一个使用webstorm远超两年半的开发者,vscode仅用半个月就通过它强大的代码提示与搜索及代码跳转让我有更加高效的开发效率,难怪大公司的员工可以免费使用的情况下都不愿意看webstorm一眼,基本都在使用vscode。

本人最近在准备晋升材料,所以有点忙。且由于配置稍微有点多,为了不耽误大家阅读,我只截取了一部分功能。

如果读者老爷有遇到什么痛点都可以在评论区留言,我会一一解决,请相信我的没能力!等我晋升之后基本就不用写代码了,到时候会把文章补全并且就vscode的提效技巧开个专题

俗话说,工欲善其事,必先利其器。有一个趁手的编辑器可以极大提升自己的开发效率及幸福感。毕竟跟编辑器相处的时间甚至比跟对象相处的时间还要长╮(╯▽╰)╭

webstorm在某些人眼里或许很好用,但我愿称vscode为宇宙级编辑器(#^.^#)

也许,在程序员的世界,收费的并不一定就是最好的吧!!!

本文转载自: 掘金

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

在 2023 年屌爆了一整年的 shadcn/ui 用的 H

发表于 2024-03-11

作者:易师傅 、github

声明:本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

前言

近日,百无聊赖,上网游逛,在《2023 JavaScript Rising Stars》 上看到了一个令人匪夷所思的仓库 shadcn/ui,就一年那个 star 数啊,蹭蹭的上涨了 39500 颗星星,这星星数™甩我的王者不知道多少条街,虽然我是个星耀常驻玩家,但是也受不了这刺激啊。

image.png

说来就干,研究一下什么来头,居然以迅雷不及掩耳之势,偷偷发育成难以仰望之帝~

好家伙,看了源代码,原来是一个基于 Headless UI 组件库 radix-ui 的上层 React UI 组件库,心想 Headless UI 组件不是几年前才火起来的玩意嘛,怎么在前端怎么就有一席之地了呢?

看样子真是一代新人换旧人,一代技术一代新人,技术的日新月异也未免是个坏事 ~

一、Headless 的历史和背景

我相信很多同学对计算机界的 Headless 比较熟悉,因为 Headless 一词最初就是来源于 Headless Computer(无头计算机)也就是 Headless System(无头系统),维基百科的介绍为:

无头系统(Headless System)是指已配置为无须显示器(即“头”)、键盘和鼠标操作的计算机系统或设备。无头系统通常通过网络连接控制,但也有部分无头系统的设备需要通过RS-232串行连接进行设备的管理。服务器通常采用无头模式以降低运作成本。

后来慢慢的就有了 Headless Browser(无头浏览器)、Headless CMS, Headless BI 等等常见的 Headless 系列;

Headless UI

在 2017 年,一个在基于 React 的 高阶函数(HOC) 以及 复合组件(Compound Components) 研发的 downshift 诞生了,它的诞生无疑间接的促使了 Headless UI 的来临;

image.png


在 2018 年 6 月 22 日一篇受到 downshift 启发介绍 Headless User Interface Components 的文章应运而生,只是那时候的评判大多褒贬不一,有赞同博主的,也有质疑博主的;由此 Headless UI 一词正式被大家所认知

image.png


后来在 2018 年 10 月 26 日改变现有 React 和 Vue 格局的 《React Conf 2018》大会引入了现有屌爆了的 React Hooks 概念之后;
Headless UI 概念才慢慢的被大家所接受。

image.png


因为在大家后知后觉中,其实发现 Headless UI 概念其实与 React Hooks 概念大同小异,只是差别在了一个更多的实现所有数据或交互的状态逻辑层,一个实现现有常见的 UI 库的数据或状态逻辑层,理解不了没关系,下面再详细介绍下;

但是那时国内其实对 Headless UI 这一概念认知还较少,因为在国内这个内卷的时代,大家更多关心的是 React Hooks。


就这样到了2020年,在国内社区有较高知名度和影响的 Tailwind Labs 团队 介绍的 Introducing Headless UI 中慢慢开始在国内社区崭露头角;

image.png

二、什么是 Headless UI

Headless UI 全称是 Headless User Interface (无头用户界面),是一种前端开发的方法论(亦或者是一种设计模式),其核心思想是将 用户界面(UI)的逻辑和交互行为 与 视觉表现(CSS 样式) 分离开来;

换句话说,Headless UI 提供了一种方式来构建只包含逻辑和功能的用户界面组件,而不依赖于特定的 CSS 样式框架或 UI 库。

具体而言,Headless UI 的组件通常是纯粹的 JavaScript(或其他编程语言)组件,它们包含了一些交互逻辑和状态管理,但没有任何与视觉样式相关的代码。

三、为什么需要 Headless UI?

众所周知,在传统的 UI 组件中,通常被拆分为两大部分:

  • 外观样式(传统组件 UI 展示层)
  • 逻辑部分(Headless UI部分)

外观样式负责展示元素标签与样式,而逻辑组件部分则负责处理数据逻辑、状态管理和用户交互等功能。

我们看图说话

传统 UI 组件框架

传统 UI 组件包含部分:

Headless UI-1.png

优势:

我相信在座的各位同学,都有用过现成的一些 UI 组件库,例如:Bootstrap 、Material UI、Ant Design、element-ui 等等,在其中,我们能看的出来,传统 UI 组件的优势主要有:

  • 开箱即用: 字面意思,直接 install 就可以用了;
  • 易学易用: 文档嘛,一看就懂,一用就废;
  • 功能性全: 根据这么些年的发展,这些个开源组件库,几乎能遇到的 bug 都给整治了;
  • 部分响应式: 这不 Bootstrap 就是一个很好的例子吗

限制:

那么用过的同学肯定也知道现成 UI 组件库的一些痛点,比如:

  • 样式难以定制: 正所谓一个萝卜一个坑,一个公司一套样式,一个项目一套样式,那么这时候的劣势就无限被放大了;
  • 耦合性高: 传统 UI 组件库通常将界面样式、数据逻辑和用户交互等功能耦合在一起,导致代码难以维护和扩展;
  • 创意受限: 公司设计师根据现有传统 UI 组件库提供的一套固定组件和样式,他们想要拓展创意,都收到了很大的影响;
  • 依赖过多: 一些传统 UI 组件库可能依赖于大量的第三方库和插件,增加了项目的复杂性和维护成本;

研发中实际场景的重现

1、需求第一期:

参与人员:前端 A、后端 A、设计师 A、产品 A

其中有一个功能要实现一个日期选择组件,下图是第一期的实现

image.png

你噼里啪啦一顿操作的实现了

那么现在需求迭代了

2、需求第二期:

参与人员:前端 A、后端 A、设计师 B、产品 B

这时候问题就来了,设计师 B 想把这个日期选择的样式改一下,改成如下:

image.png

那么此时阁下又该如何去面对呢?

  • 魔改 ??
  • :deep 强制改 ??
  • !important ??
  • 重写自定义 ??
  • 和设计师沟通 ??
  • 沟通不过来打一顿 ??
  • 不解气,这破工谁爱打谁打

那么我们此时不妨换一种思路,试试用 Headless UI 解决看看??

Headless UI 的解决方案

根据上述咱们已经知道了,普通的 Headless UI 组件包含:数据逻辑、状态逻辑、用户交互等等

Headless UI_副本1.png

其实 Headless UI 的一些拓展还可以包括 浏览器兼容性 和 无障碍访问等功能

用传统 UI 组件库实现一个 date 组件代码:

1
2
3
4
5
6
7
8
9
10
11
12
ts复制代码<template>
<date> 切换 </date>
// do something 此处省略 500 行
</template>

<script>
// do something 此处省略 500 行
</script>

<style>
// do something 此处省略 500 行
</style>

能看到组件的逻辑与标签和样式都是高度耦合在一起的,很不利于拓展

我们看下 Headless UI 实现的组件,截取部分:

image.png

能看到 Headless UI 实现的组件简便了很多,完全没有所谓的样式集成在里面了,你想怎么改就怎么改;

说白了,就是 Headless UI 的出现就是为了解决传统 UI 组件框架的缺点,你不是要定制样式吗,现在我 ™ 连样式都不给你,全部由你自己来实现,爱咋滴咋滴,想怎么折腾就怎么折腾,无论多离谱的定制化都能实现。

四、Headless UI 的优势和特点

Headless UI3-1.png

  • 灵活性高: 因为 Headless UI 将组件的逻辑和样式分离,咱们可以根据项目的需要,想怎么玩就怎么玩,你说呢?
  • 可定制: 样式都没了,我这不是想咋定制就定制了吗,你别说两个设计师,1 V 10 都不成问题,还担心啥呢?
  • 轻量级: 只有逻辑了,那可不少了一大堆代码了;
  • 测试友好: 写过单元测试的同学应该都知道,有样式组件和没样式组件的单元测试那就是一个天一个地;
  • 无需学习新的样式框架: 我只看你的 API 不就行了,那样式有啥好关注的。

五、Headless UI 的应用场景

根据个人实际经验,我总结出来的一些实际应用场景:

  1. 跨平台应用: 如果你司同个项目在多个平台上共存,那么此时的样式肯定都是不大一致的,那么现在 Headless UI 就可以发挥它的作用了;
  2. 定制化界面: 如果你们项目需要高度定制样式和功能,我觉得这是一个很不错的选择;
  3. 公司项目较多: 因为一个项目会存在很多个不同样式同逻辑的组件,所以在一个公司中,业务场景较大,以及项目较多,不失为一个很好的选择;

当然 Headless UI 也有缺点,并不适用于所有的项目,

  • 如果你项目使用简单,对设计没有较大的要求,还是用现成的 UI 库比较合适,毕竟开箱即用;
  • 如果公司大家的技术水平参差不齐,还是别瞎整了,还是用现成得吧;
  • 切莫为了跟风而且瞎折腾技术,从实际出发

总结

上述只是给大家介绍了 Headless UI 理论部分,也就是所谓八股文吧;

但是Headless UI的设计模式还是很值得大家去参考学习的;

下面将会有 Headless UI 实战等其它部分:

Headless UI (1).png

感谢大家的支持,码字实在不易,其中如若有错误,望指出,如果您觉得文章不错,记得 点赞关注加收藏 哦 ~

关注我,带您一起搞前端 ~

本文转载自: 掘金

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

老项目的外链太多?我是这么处理的

发表于 2024-03-11

作者:苏梓铭

背景介绍

目前,古茗前端团队内部统一采用了 React 技术栈,所有新项目和基础建设均基于 React 框架开发。然而,许多老旧的 Vue 应用仍在使用和运行,因此我们需要进行项目迁移,统一技术栈,减少开发认知负担,接轨现有基建,增强可维护性,以便更好地满足业务新需求。

特别地,以笔者负责的学院业务域为例,其业务具有这么一个特点:存在众多外部链接跳转场景(例如,发布给加盟商的考试待办链接、张贴在店内设备上的资料二维码等)。在迁移老项目的过程中,我们也要格外关注外链治理。

本文主要讲述笔者治理老项目存量外链投放问题的解决思路。

现状及目标分析

在迁移老项目的过程中,我们发现了许多问题:

外链更新的跳转问题

我们对外投放的链接通常具有很强的时效性,例如给加盟商发送的考试链接等,而且这些外链是静态、无法直接修改的,因此我们通常会采用重定向逻辑,将旧路由映射到新路由,让老外链也能跳转到新页面。

在老项目的导航守卫里,已经积累了大量的重定向逻辑,并且每个模块都有一套自己单独的重定向策略,如下图所示:

现有逻辑

因此,当老项目迁移完成下线后,已经投放出去的老链接将会无法访问(因为所有的重定向逻辑都在老项目内完成)

其次,外链投放还存在一定的不确定性,有的甚至是产研团队自己都不知道的外链投放,这就导致我们有时对页面路由进行改造后,会得到其他业务域的业务反馈。

路由传参的心智负担

在分析页面迁移时,我们注意到新旧页面的路由经常通过 URL 传递大量参数。缺乏统一规范来维护这些参数,使得页面的维护变得复杂。

目标分析

针对上述问题现状,我们可以总结出如下目标:

  1. 老项目路由重定向:将重定向逻辑做保留,实现对历史外链的兼容
  2. 操作可感知:用户的访问应当有完善的链路可以被感知到,用户访问了老项目路由时,我们需要能够记录日志,当用户访问 404 时我们需要知道是哪个页面报了 404,并且自动及时地告警同步给业务 Owner 排查。
  3. 统一技术栈/统一规范:我们应当全链路接入前端基建,使用最新的统一技术栈,针对路由跳转等场景需要有一个统一的技术规范

明确了几个基本目标后,我们就可以着手进行代码结构设计了

核心代码设计与实现

路由重定向

如何转发——Nginx, NodeJS OR JavaScript?

在设计重定向转发方案之前,我们首先考虑的是直接使用 Nginx 或 Node 做转发,但由于目前两个 H5 项目都使用 Hash 路由,而我们知道 Hash 路由 # 后面的参数是不会发送给服务器的,因此 Nginx 和 Node 甚至都无法读取到路由本身,更不用提做重定向了,因此我们确定了最终解决方案还是交由客户端处理,Nginx 层只做映射路径的转发(将老项目 / 的路径直接转发到 /college 下即可)

API 调用方式设计

设计一个简洁明了的 API 调用方式同样是技术方案的重要一环。基于配置化的思想,我们需要把原有的导航守卫中 if else 的糅杂一团的重定向逻辑,改造成维护一个路由跳转表。即通过配置固定格式的原 URL 和目标 URL 来声明重定向,调用方式大致如下:

1
2
3
typescript复制代码const redirector = new Redirector()
.register('/knowledge/learnList', '/pages/material-list/index')
.register('/knowledge/detail/:id', '/pages/material-detail/index?id=:id');

这样一来我们注册重定向就变得非常简单。注册完成之后,我们就可以通过调用redirector.run方法来执行一次重定向,接下来是具体的代码实现。

重定向路由跳转表设计

名词解析

在设计跳转表之前,我们需要明确,路由传参有两种类型的方式,一种是 params,一种是 query。其中 params 是作为路由路径的一部分存在的,比如 /pending/list/1234567 中的 1234567 就代表着 id 这个参数;而 query 就是在 ? 之后的键值对参数了,例如 /pages/list/index?id=1234567 这种形式。

上文的 API 设计中,仅做了 params 类型的传参映射,而对于页面的其他参数是没有处理的,这里我们要求,新老页面的出入参要保持相同,这样就可以直接共用一套 query 参数,减轻我们的开发成本

如何转换路由正则匹配式

原先的 Vue2 老项目实现重定向是利用 vue-router 自带的实现。经过调研,我们选择使用 path-to-regex 库做路由解析,它是 Vue-Router 等众多知名路由库的底层依赖库,正如其名,它能够将路由转化成正则表达式去匹配链接,并且支持动态路由参数等许多功能,官方示例如下:

官方示例

实现 Redirector 类

path-to-regex 具有非常简单易用的 API,对于我们检测和提取路由参数的需求来说已经完全足够,我们可以通过调用 match 方法获取到解析路由的正则表达式,并据此实现一个 Redirector 类:

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
typescript复制代码import { history } from '@tarojs/router';
import Taro from '@tarojs/taro';
import { match, MatchFunction } from 'path-to-regexp';

type RegisterPath = `/${string}`;

interface RouteObject {
matchFn: MatchFunction;
target: string;
}

class Redirector {
// 本地注册的路由跳转配置
private routes: RouteObject[] = [];

register(oldPath: RegisterPath, newPath: RegisterPath): Redirector {
this.routes.push({
matchFn: match(oldPath),
target: newPath,
});
// 链式调用
return this;
}

redirect(path: string, query = ''): void {
this.routes.some(({ matchFn, target }) => {
const result = matchFn(path);

if (result) {
// 替换路由参数
Object.entries(result.params).forEach(([key, value]) => {
target = target.replace(`:${key}`, value);
});
// 处理 query,Taro 路由的需要直接拼在参数上
target = `${target}${target.includes('?') ? '&' : '?'}${query}`;
// 执行重定向
Taro.redirectTo({ url: target });
return true;
}
return false;
});
}

run(): void {
const [path, query] = location.hash.slice(1).split('?');
this.redirect(path, query);
}
}

// 直接注册重定向后导出实例即可
const redirector = new Redirector()
.register('/knowledge/learnList', '/pages/material-list/index')
.register('/knowledge/detail/:id', '/pages/material-detail/index?id=:id');

export default redirector;

其基本思路就是通过 match 方法生成的正则表达式,匹配摘出原 URL 中的路由参数,并拼接和替换到新的 URL 上

何时执行——完整的一次重定向链路

本次迁移的 C 端 H5 微应用同时由两个项目组成,分别是使用 Vue2 技术栈的老项目和使用 Taro React 的新项目。两个项目通过 Nginx 转发到同一个域名的不同路由下:老项目映射在 / 根目录,而新项目则是映射在 /college 目录下,所处同一个域,从而可以共享一些本地数据如 localStorage、sessionStorage 等。

(1)用户访问,Nginx 层转发

以用户外链跳转到老项目路由为例:

1
bash复制代码https://host/#/knowledge/learnList?rankId=aaa&ListName=bbb

由于老页面会下线,访问服务时 Nginx 正常匹配其他路由,兜底匹配根路由 /直接转发到 /college 上:

1
ruby复制代码https://host/college/#/pages/material-list/index?rankId=aaa&ListName=bbb

因此用户在 Nginx 层被第一次重定向到:

1
bash复制代码https://host/college/#/knowledge/learnList?rankId=aaa&ListName=bbb
(2)执行重定向

重定向后,我们需要在项目内解析路由参数后拼接跳转。这里 Taro 无法匹配到页面,但是 app.tsx 中仍然会执行代码逻辑,因此我们在新项目的 app.tsx 中执行重定向

1
2
3
4
5
6
7
8
tsx复制代码// app.tsx
import redirector from "@/utils"

function App({ children }){
useMount(() => {
redirector.run()
})
}

需要监听路由变化吗?

我们可以通过 history.listen 方法监听路由的变化,并且在每次路由改变时都去匹配一次重定向:

1
2
3
4
typescript复制代码history.listen(({ location }) => {
const { pathname, search } = location;
redirector.redirect(pathname, search);
});

但实际上我们并不需要做到这么全面,我们完全可以保证在学院新项目里不会出现老项目的路由,因此如果用户已经进入了应用里,就不会通过路由跳转进入到老项目了。如果监听了路由变更反而会带来额外的性能开销。

操作感知

如果没有触发重定向或是重定向失败,都会导致发生 404 错误,我们会上报一次错误日志。而只要触发了重定向,我们就会上报一次重定向记录

404 感知

1
2
3
4
5
6
7
8
9
10
typescript复制代码function App({ children }) {
usePageNotFound((e) => {
const tag = 'PAGE_NOT_FOUND';
const log = `path=${e.path}`;
Slardar.logger()?.error(log, tag);
Taro.redirectTo({
url: `/pages/404/index?from=${e.path}`,
});
})
}

具体操作为:在 App.tsx 下新增 usePageNotFound hook,调用 Taro 原生路由回调,通过数据平台上报 error 日志,并在 404 页面展示提示和链接,引导用户回到学院应用和反馈

404 页面

我们可以在数据中心查看上报的日志信息等

相关文章:古茗是如何做前端数据中心的 - 掘金

日志查询

由于 404 错误一般不会偶现,只要出现就代表页面路由或是外链投放存在问题。我们通过配置错误告警策略,来及时通知开发查看日志,发现并解决路由问题。

告警配置

重定向感知

在执行重定向的时候上报执行日志,提交原路由和目标路由的完整信息

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
diff复制代码class Redirector {
private routes: RouteObject[] = [];

register(oldPath: RegisterPath, newPath: RegisterPath): Redirector {}

redirect(path: string, query = ''): void {
this.routes.some(({ matchFn, target }) => {
const result = matchFn(path);

if (result) {
Object.entries(result.params).forEach(([key, value]) => {
target = target.replace(`:${key}`, value);
});
target = `${target}${target.includes('?') ? '&' : '?'}${query}`;
+ const tag = 'PAGE_NOT_FOUND';
+ const log = `from=${path}&to=${target}`;
+ Slardar.logger()?.info(log, tag);
Taro.redirectTo({ url: target });
return true;
}
return false;
});
}

run(): void {
const [path, query] = location.hash.slice(1).split('?');
this.redirect(path, query);
}
}

本次我们把新迁移的页面路由和已有的重定向路由全部加入到配置表中,并且在重定向发生时上报访问的链接和跳转的链接,收集 30 日的数据后再进行一次整理,观察线上流量分布,逐渐下掉没有使用的重定向,减少维护成本

统一规范——路由跳转方法

由于老项目的迁移过程中涉及到了大量的路由传参,经常会出现链接里挂了一大堆参数,实际页面里却根本没有用到的情况,我们很难分辨哪些路由参数是有效的,哪些路由参数又是可以舍弃的。因此,我们在本业务域内的移动端项目内做了如下统一规范:

  • 针对页面维护 TS 入参类型定义;
  • 路由跳转和获取入参使用公共方法,传入页面入参的类型定义作为泛型参数,规范页面跳转

具体调用方式如下:

  1. 跳转页面使用 formatUrlParams 格式化 query 参数
1
2
3
4
5
6
7
8
9
10
11
12
13
typescript复制代码import { BaseAssignmentParams } from "@/pages/assignment/index/index"

const handleClick = (assignment: CourseAssignmentItem) => {
const baseQuery = formatUrlParams<AssignmentParams>({
trainingId: trainingDetail.id,
semesterId: trainingDetail.semesterId,
assignmentId: assignment.id,
});

Taro.navigateTo({
url: `/pages/assignment/index/index?${baseQuery}`,
});
};
  1. 承接页面使用 getUrlParams 反序列化路由参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
typescript复制代码export interface AssignmentParams {
/** 培训 id */
trainingId: string;
/** 学期 id */
semesterId: string;
/** 任务 id */
assignmentId: string;
}

const Assignment: React.FC = () => {
const { semesterId, trainingId, assignmentId } = getUrlParams<AssignmentParams>();
}

export default Assignment;

如此一来,就在两个页面成功通过类型系统架起了一道桥梁,如果页面新增或是删改了入参,都可以通过静态类型分析在开发阶段就得到提示,修改路由出入参时的心智负担明显减小,项目可维护性得到了显著提高。

完整流程图

完整流程图

总结

本文介绍了笔者在做业务项目迁移的时候处理外链逻辑以及对项目内的页面出入参和跳转场景进行统一化治理的思路,希望能给同样被历史项目和技术债折磨的同学们提供一些可行性建议,也欢迎大家在评论区一起交流,共同进步

最后

📚 小茗文章推荐:

  • formily原来是这样解决这些表单难题
  • 古茗是如何将小程序编译速度提升3倍的
  • 钉钉小程序实现签名板

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

本文转载自: 掘金

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

Java面试必考题之线程的生命周期,结合源码,透彻讲解!

发表于 2024-03-10

写在开头

在前面的几篇博客里,我们学习了Java的多线程,包括线程的作用、创建方式、重要性等,那么今天我们就要正式踏入线程,去学习更加深层次的知识点了。

第一个需要学的就是线程的生命周期,也可以将之理解为线程的几种状态,以及互相之间的切换,这几乎是Java多线程的面试必考题,每一年都有大量的同学,因为这部分内容回答不够完美而错过高薪,今天我们结合源码,好好来聊一聊。

线程的生命周期

所谓线程的生命周期,就是从它诞生到消亡的整个过程,而不同的编程语言,对线程生命周期的封装是不同的,在Java中线程整个生命周期被分为了六种状态,我们下面就来一起学习一下。

线程的6种状态

对于Java中线程的状态划分,我们其实要从两个方面去看,一是JVM层面,这是我们程序运行的核心,另一层面是操作系统层面,这是我们JVM能够运行的核心。为了更直观的分析,build哥列了一个对比图:

JavaSE思维导图(带部分知识点链接).png
在操作系统层面,对于RUNNABLE状态拆分为(READY、RUNNING),那为什么在JVM层面没有分这么细致呢?

这是因为啊,在当下时分多任务操作系统架构下,线程的驱动是通过获取CPU时间片,而每个时间片的间隔非常之短(10-20ms),这就意味着一个线程在cpu上运行一次的时间在0.01秒,随后CPU执行权就会发生切换,在如此高频的切换下,JVM就没必要去区分READY和RUNNING了。

在Java的源码中也可以看到,确实只分了6种状态:

【源码解析1】

1
2
3
4
5
6
7
8
9
10
java复制代码// Thread.State 源码
public enum State {
//省略了每个枚举值上的注释
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}

NEW(初始化状态)

我们通过new一个Thread对象,进行了初始化工作,这时的线程还没有被启动。

【代码示例1】

1
2
3
4
5
6
7
8
java复制代码public class Test {
public static void main(String[] args) {
//lambda 表达式
Thread thread = new Thread(() -> {});
System.out.println(thread.getState());
}
}
//执行结果:NEW

我们通过thread.getState()方法去获得当前线程所处在的状态,此时输出为NEW。

RUNNABLE(可运行状态)

对于这种状态的描述,我们来看一下Thread源码中如何说的:

1
2
3
4
5
6
bash复制代码/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/

大致意思是线程处于RUNNABLE状态下,代表它可能正处于运行状态,或者正在等待CPU资源的分配。

那么我们怎样从NEW状态变为RUNNABLE呢?答案很简单,我们只需要调用start()方法即可!

【代码示例2】

1
2
3
4
5
6
7
8
9
java复制代码public class Test {
public static void main(String[] args) {
//lambda 表达式
Thread thread = new Thread(() -> {});
thread.start();
System.out.println(thread.getState());
}
}
//执行结果:RUNNABLE

BLOCKED(阻塞状态)

当线程线程进入 synchronized 方法/块或者调用 wait 后(被 notify)重新进入 synchronized 方法/块,但是锁被其它线程占有,这个时候线程就会进入 BLOCKED(阻塞) 状态。这时候只有等到锁被另外一个线程释放,重新获取锁后,阻塞状态解除!

WAITING(无限时等待)

当通过代码将线程转为WAITING状态后,这种状态不会自动切换为其他状态,是一种无限时状态,直到整个线程接收到了外界通知,去唤醒它,才会从WAITING转为uRUNNABLE。
调用下面这 3 个方法会使线程进入等待状态:

  1. Object.wait():使当前线程处于等待状态直到另一个线程唤醒它;
  2. Thread.join():等待线程执行完毕,底层调用的是 Object 的 wait 方法;
  3. LockSupport.park():除非获得调用许可,否则禁用当前线程进行线程调度。

TIMED_WAITING(有限时等待)

与WAITING相比,TIMED_WAITING是一种有限时的状态,可以通过设置等待时间,没有外界干扰的情况下,达到指定等待时间后,自动终止等待状态,转为RUNNABLE状态。
调用如下方法会使线程进入超时等待状态:

  1. Thread.sleep(long millis):使当前线程睡眠指定时间;
  2. Object.wait(long timeout):线程休眠指定时间,等待期间可以通过notify()/notifyAll()唤醒;
  3. Thread.join(long millis):等待当前线程最多执行 millis 毫秒,如果 millis 为 0,则会一直执行;
  4. LockSupport.parkNanos(long nanos): 除非获得调用许可,否则禁用当前线程进行线程调度指定时间;
  5. LockSupport.parkUntil(long deadline):同上,也是禁止线程进行调度指定时间;

TERMINATED(终止状态)

线程正常执行结束,或者异常终止,会转变到 TERMINATED 状态。

线程状态的切换

上面的6种状态随着程序的运行,代码(方法)的执行,上下文的切换,也伴随着状态的转变。

线程运行.png

NEW 到 RUNNABLE 状态

这一种转变比较好理解,我们通过new,初始化一个Thread对象后,这时就是处于线程的NEW状态,此时线程是不会获取CPU时间片调度执行的,只有在调用了start()方法后,线程彻底创建完成,进入RUNNABLE状态,等待操作系统调度执行!这种状态是NEW -> RUNNABLE的单向转变。

RUNNABLE 与 BLOCKED 的状态转变

synchronized 修饰的方法、代码块同一时刻只允许一个线程执行,其他线程只能等待,等待的线程会从 RUNNABLE 转变到 BLOCKED 状态,当等待的线程获得 synchronized 隐式锁时,就又会从 BLOCKED 转变到 RUNNABLE 状态。我们通过一段代码示例看一下:

【代码示例3】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码public class Test {
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
testMethod();
});
Thread thread2 = new Thread(() -> {
testMethod();
});

thread1.start();
thread2.start();

System.out.println(thread1.getName()+":"+thread1.getState());
System.out.println(thread2.getName()+":"+thread2.getState());
}
// 同步方法争夺锁
private static synchronized void testMethod() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

输出:

1
2
java复制代码Thread-0:RUNNABLE
Thread-1:BLOCKED

代码中在主线程中创建了2个线程,线程中都调用了同步方法,随后去启动线程,因为CPU的执行效率较高,还没阻塞已经完成的打印,所以大部分时间里会输出两线程均为RUNNABLE状态;

当CPU效率稍低时,就会呈现上述结果,thread1启动后进入RUNNABLE状态,并且获得了同步方法,这是thread2启动后,调用的同步方法锁已经被占用,它作为等待的线程会从 RUNNABLE 转变到 BLOCKED 状态,待到thread1同步方法执行完毕,释放synchronized锁后,thread2获得锁,从BLOCKED转为RUNNABLE状态。

RUNNABLE 与 WAITING 的状态转变

1、获得 synchronized 隐式锁的线程,调用无参数的 Object.wait() 方法,状态会从 RUNNABLE 转变到 WAITING;调用 Object.notify()、Object.notifyAll() 方法,线程可能从 WAITING 转变到 RUNNABLE 状态。

2、调用无参数的 Thread.join() 方法。join() 是一种线程同步方法,如有一线程对象 Thread t,当调用 t.join() 的时候,执行代码的线程的状态会从 RUNNABLE 转变到 WAITING,等待 thread t 执行完。当线程 t 执行完,等待它的线程会从 WAITING 状态转变到 RUNNABLE 状态。

3、调用 LockSupport.park() 方法,线程的状态会从 RUNNABLE 转变到 WAITING;调用 LockSupport.unpark(Thread thread) 可唤醒目标线程,目标线程的状态又会从 WAITING 转变为 RUNNABLE 状态。

RUNNABLE 与 TIMED_WAITING 的状态转变

这种与上面的很相似,只是在方法调用和参数上有细微差别,因为,TIMED_WAITING 和 WAITING 状态的区别,仅仅是调用的是超时参数的方法。
转变方法在上文中已经提到了,这里以sleep(time)为例,写一个测试案例:

【代码示例4】

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
java复制代码public class Test {
public static void main(String[] args) throws InterruptedException {

Thread thread1 = new Thread(() -> {
testMethod();
});
Thread thread2 = new Thread(() -> {
// testMethod();
});
thread1.start();
Thread.sleep(1000L);
thread2.start();

System.out.println(thread1.getName()+":"+thread1.getState());
System.out.println(thread2.getName()+":"+thread2.getState());
}
// 同步方法争夺锁
private static synchronized void testMethod() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

输出:

1
2
java复制代码Thread-0:TIMED_WAITING
Thread-1:TERMINATED

这里面我们启动threa1后,让主线程休眠了1秒,这时thread1获得同步方法后,方法内部执行了休眠2秒的操作,因此它处于TIMED_WAITING状态,而thread2正常运行结束,状态处于TERMINATED(这个案例同样可以印证下面RUNNABLE到TERMINATED的转变)。

RUNNABLE 到 TERMINATED 状态

转变为TERMINATED状态,表明这个线程已经执行完毕,通常用如下几种情况:

  1. 线程执行完 run() 方法后,会自动转变到 TERMINATED 状态;
  2. 执行 run() 方法时异常抛出,也会导致线程终止;
  3. Thread类的 stop() 方法已经不建议使用。

总结

今天关于线程的6种状态就讲到这里啦,这是个重点知识点,希望大家能够铭记于心呀!

结尾彩蛋

如果本篇博客对您有一定的帮助,大家记得留言+点赞+收藏呀。原创不易,转载请联系Build哥!

点赞.png

如果您想与Build哥的关系更近一步,还可以关注“JavaBuild888”,在这里除了看到《Java成长计划》系列博文,还有提升工作效率的小笔记、读书心得、大厂面经、人生感悟等等,欢迎您的加入!

公众号.png

本文转载自: 掘金

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

Jetpack-ViewModel(面试深度起来)

发表于 2024-03-10

面试素质三联?

  • ViewModel优点是啥? 答:保存数据 自动管理。
  • Fragment能拿Activity的ViewModel么? 答:能吧?。 = = !
  • ViewModel怎么管理的? 答:母鸡。
  • ViewModel怎么创建的? 答:母鸡啊

结果

回去等消息吧,面试官顺手评价 一个深度不够,基础不牢。

逐步拆解

1
2
3
kotlin复制代码 private  val  model by lazy {
       ViewModelProvider(this).get(BaseViewModel::class.java);
  }

ViewModelProvider干了啥?

1
2
3
4
5
6
7
8
9
10
kotlin复制代码  public constructor(
       owner: ViewModelStoreOwner
  ) : this(owner.viewModelStore, defaultFactory(owner), defaultCreationExtras(owner))



//构建Factory
internal fun defaultFactory(owner: ViewModelStoreOwner): Factory =
               if (owner is HasDefaultViewModelProviderFactory)
                   owner.defaultViewModelProviderFactory else instance

整体看了一下,大体就三件事, 用this的 viewModelStore,搞Factory, 搞参数。

this 里面搞大事。

原来ViewModelProvider需要传入一个ViewModelStoreOwner 接口

1
2
3
4
scss复制代码 
public constructor(
       owner: ViewModelStoreOwner
  ) : this(owner.viewModelStore, defaultFactory(owner), defaultCreationExtras(owner))

看下ViewModelStoreOwner 就一个方法,返回一个ViewModelStore

1
2
3
kotlin复制代码interface ViewModelStoreOwner {
   val viewModelStore: ViewModelStore
}

我们常用的ComponentActivity/Fragment都实现了ViewModelStoreOwner和HasDefaultViewModelProviderFactory ,我们看下他们的实现getViewModelStore()的区别。

  • ComponentActivity的实现方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
scss复制代码   @NonNull
   @Override
   public ViewModelStore getViewModelStore() {
       if (getApplication() == null) {
           throw new IllegalStateException("Your activity is not yet attached to the "+ "Application instance. You can't request ViewModel before onCreate call.");
      }
       ensureViewModelStore();
       return mViewModelStore;
  }

  void ensureViewModelStore() {
       if (mViewModelStore == null) {
           NonConfigurationInstances nc =
                  (NonConfigurationInstances) getLastNonConfigurationInstance();
           if (nc != null) {

               mViewModelStore = nc.viewModelStore;
          }
           if (mViewModelStore == null) {
               mViewModelStore = new ViewModelStore();
          }
      }
  }

简略看下核心代码,getLastNonConfigurationInstance没有,就直接new一个返回。

ComponentActivity 还是比较简单的,相当于自己管自己。

  • Fragment的实现(版本 1.6.2)

1
2
3
4
5
6
less复制代码 @NonNull
   @Override
   public ViewModelStore getViewModelStore() {
      // 省略部分代码
       return mFragmentManager.getViewModelStore(this);
  }

Fragment 调用FragmentManager的getViewModelStore(),(下面简称Fm)并把自己传入进去了。

追一下看看 进入 Fm。

1
2
3
4
5
less复制代码 private FragmentManagerViewModel mNonConfig; 
@NonNull
   ViewModelStore getViewModelStore(@NonNull Fragment f) {
       return mNonConfig.getViewModelStore(f);
  }

FragmentManagerViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码FragmentManagerViewModel extends ViewModel{
//省略部分代码
private final HashMap<String, ViewModelStore> mViewModelStores = new HashMap<>();
 @NonNull
   ViewModelStore getViewModelStore(@NonNull Fragment f) {
       ViewModelStore viewModelStore = mViewModelStores.get(f.mWho);
       if (viewModelStore == null) {
           viewModelStore = new ViewModelStore();
           mViewModelStores.put(f.mWho, viewModelStore);
      }
       return viewModelStore;
  }
 
  //省略部分代码
  }

FragmentManager 调用自己的mNonConfig,传入了Fragment,mNonConfig 自己就是一个FragmentManagerViewModel。也是一个ViewModel。

到这里我们理解Frament 其实委托给了FragmentManager。

先看下Fm里的mNonConfig 咋来的。看初始化

FragmentManager

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

void attachController(@NonNull FragmentHostCallback<?> host,
@NonNull FragmentContainer container, @Nullable final Fragment parent) {
//省略部分代码

if (parent != null) {
mNonConfig = parent.mFragmentManager.getChildNonConfig(parent);
} else if (host instanceof ViewModelStoreOwner) {
ViewModelStore viewModelStore = ((ViewModelStoreOwner) host).getViewModelStore();
mNonConfig = FragmentManagerViewModel.getInstance(viewModelStore);
} else {
mNonConfig = new FragmentManagerViewModel(false);
}
}

mNonConfig.setIsStateSaved(isStateSaved());
// 设置setNonConfig()
mFragmentStore.setNonConfig(mNonConfig);

FragmentManagerViewModel

1
2
3
4
5
6
csharp复制代码@NonNull
static FragmentManagerViewModel getInstance(ViewModelStore viewModelStore) {
ViewModelProvider viewModelProvider = new ViewModelProvider(viewModelStore,
FACTORY);
return viewModelProvider.get(FragmentManagerViewModel.class);
}

我们解析三个创建

+ `if` 可以理解为`fragment`嵌套 ,去找`getChildNonConfig()`。
+ `else if` 取到`host`的`viewModelStore`,自己创建了一个 `ViewModelProvider` 缓存`FragmentManagerViewModel`类进去。
+ `else` 自己 host 取不到,自己创建一个`FragmentManagerViewModel`,传入`false`,表示不自动缓存。我们看下看下host的真面目

Fragment

1
2
3
4
csharp复制代码 void performAttach() {
//省略部分代码
mChildFragmentManager.attachController(mHost, createFragmentContainer(), this);
}

这host 看下哪里初始化的

FragmentController

1
2
3
4
5
less复制代码
public void attachHost(@Nullable Fragment parent) {
mHost.mFragmentManager.attachController(
mHost, mHost /*container*/, parent);
}

再看下 这个mHost怎么初始化的

1
2
3
typescript复制代码public static FragmentController createController(FragmentHostCallback<?> callbacks) {
return new FragmentController(callbacks);
}

追一下 进入到了FragmentActivity

1
2
3
4
5
6
7
8
9
10
11
scala复制代码 final FragmentController mFragments = FragmentController.createController(new HostCallbacks());

//内部类
class HostCallbacks extends FragmentHostCallback<FragmentActivity>{
// 省略部分代码
@NonNull
@Override
public ViewModelStore getViewModelStore() {
return FragmentActivity.this.getViewModelStore();
}
}

捋一下,Fragment里面的 mNonConfig.getViewModelStore(f),
最后会调用到 host .getViewModelStore()。而 host 是 FragmentActivity 里的一个内部类。 最后 调用到 FragmentActivity.this.getViewModelStore()。 而FragmentActivity继承了ComponentActivity。

正常情况下来说,FragmentManager 调用的是 ComponentActivity的getViewModelStore(),拿到了 然后 构建了一个ViewModelProvider() 返回。

实在不行了,自己 创建一个 FragmentManagerViewModel(false)用于处理。

ViewModelStore是个啥?

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
kotlin复制代码open class ViewModelStore {

private val map = mutableMapOf<String, ViewModel>()


@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
fun put(key: String, viewModel: ViewModel) {
val oldViewModel = map.put(key, viewModel)
oldViewModel?.onCleared()
}
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
operator fun get(key: String): ViewModel? {
return map[key]
}


@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
fun keys(): Set<String> {
return HashSet(map.keys)
}

fun clear() {
for (vm in map.values) {
vm.clear()
}
map.clear()
}
}

一眼明白,一个map,能存ViewModel。 能清除,无了。

ViewModelProvider. get() 又搞了啥?

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
kotlin复制代码 @MainThread
public open operator fun <T : ViewModel> get(modelClass: Class<T>): T {
val canonicalName = modelClass.canonicalName
?: throw IllegalArgumentException("Local and anonymous classes can not be ViewModels")
return get("$DEFAULT_KEY:$canonicalName", modelClass)
}

@Suppress("UNCHECKED_CAST")
@MainThread
public open operator fun <T : ViewModel> get(key: String, modelClass: Class<T>): T {
val viewModel = store[key]
if (modelClass.isInstance(viewModel)) {
(factory as? OnRequeryFactory)?.onRequery(viewModel!!)
return viewModel as T
} else {
@Suppress("ControlFlowWithEmptyBody")
if (viewModel != null) {
// TODO: log a warning.
}
}
val extras = MutableCreationExtras(defaultCreationExtras)
extras[VIEW_MODEL_KEY] = key

return try {
factory.create(modelClass, extras)
} catch (e: AbstractMethodError) {
factory.create(modelClass)
}.also { store.put(key, it) }
}

扫一眼,拿传入的类名 canonicalName 前面拼接 “DEFAULT_KEY”,如果当前store有,且是 是当前类的实例,就强转返回。 没有就factory 创建。最后调用 also缓存进去。看下 factory。

前面我们知道ComponentActivity和Fragment实现了HasDefaultViewModelProviderFactory。

  • ComponentActivity
1
2
3
4
5
6
7
8
9
10
11
less复制代码 @NonNull
@Override
public ViewModelProvider.Factory getDefaultViewModelProviderFactory() {
if (mDefaultFactory == null) {
mDefaultFactory = new SavedStateViewModelFactory(
getApplication(),
this,
getIntent() != null ? getIntent().getExtras() : null);
}
return mDefaultFactory;
}
  • Fragment
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
ini复制代码   @NonNull
@Override
public ViewModelProvider.Factory getDefaultViewModelProviderFactory() {
if (mFragmentManager == null) {
throw new IllegalStateException("Can't access ViewModels from detached fragment");
}
if (mDefaultFactory == null) {
Application application = null;
Context appContext = requireContext().getApplicationContext();
while (appContext instanceof ContextWrapper) {
if (appContext instanceof Application) {
application = (Application) appContext;
break;
}
appContext = ((ContextWrapper) appContext).getBaseContext();
}
if (application == null && FragmentManager.isLoggingEnabled(Log.DEBUG)) {
Log.d(FragmentManager.TAG, "Could not find Application instance from "
+ "Context " + requireContext().getApplicationContext() + ", you will "
+ "need CreationExtras to use AndroidViewModel with the default "
+ "ViewModelProvider.Factory");
}
mDefaultFactory = new SavedStateViewModelFactory(
application,
this,
getArguments());
}
return mDefaultFactory;
}

很好他们都返回了SavedStateViewModelFactory()。看下SavedStateViewModelFactory 的creat();

  • SavedStateViewModelFactory.create()
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
less复制代码  @NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
// ViewModelProvider calls correct create that support same modelClass with different keys
// If a developer manually calls this method, there is no "key" in picture, so factory
// simply uses classname internally as as key.
String canonicalName = modelClass.getCanonicalName();
if (canonicalName == null) {
throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels");
}
return create(canonicalName, modelClass);
}


public <T extends ViewModel> T create(@NonNull String key, @NonNull Class<T> modelClass) {
//判断是否是isAndroidViewModel
boolean isAndroidViewModel = AndroidViewModel.class.isAssignableFrom(modelClass);
Constructor<T> constructor;
if (isAndroidViewModel) {
constructor = findMatchingConstructor(modelClass, ANDROID_VIEWMODEL_SIGNATURE);
} else {
constructor = findMatchingConstructor(modelClass, VIEWMODEL_SIGNATURE);
}
// doesn't need SavedStateHandle
if (constructor == null) {
return mFactory.create(modelClass);
}

SavedStateHandleController controller = SavedStateHandleController.create(
mSavedStateRegistry, mLifecycle, key, mDefaultArgs);
try {
T viewmodel;
if (isAndroidViewModel) {
viewmodel = constructor.newInstance(mApplication, controller.getHandle());
} else {
viewmodel = constructor.newInstance(controller.getHandle());
}
viewmodel.setTagIfAbsent(TAG_SAVED_STATE_HANDLE_CONTROLLER, controller);
return viewmodel;
} catch (IllegalAccessException e) {
throw new RuntimeException("Failed to access " + modelClass, e);
} catch (InstantiationException e) {
throw new RuntimeException("A " + modelClass + " cannot be instantiated.", e);
} catch (InvocationTargetException e) {
throw new RuntimeException("An exception happened in constructor of "
+ modelClass, e.getCause());
}
}

一个参数调用俩参数,判断了是否是判断是否是AndroidViewModel,再往下一看newInstance(),反射, 绝对的反射。

到此为止,我们知道 get() 其实就是缓存则取,没缓存就反射搞个对象 ,also 缓存进去。

至此,还没成艺术。

ViewModel 的自我修养(管理)?

认真阅读的朋友们都知道,Fragment正常情况下用FragmentActivity的ViewModelStore。

  • 看下FragmentActivity 的 父类ComponentActivity 构造参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
less复制代码public ComponentActivity() {

//省略部分代码
getLifecycle().addObserver(new LifecycleEventObserver() {
@Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
if (event == Lifecycle.Event.ON_DESTROY) {

mContextAwareHelper.clearAvailableContext();

if (!isChangingConfigurations()) {
getViewModelStore().clear();
}
}
}
});




}

我们看到 做了个监听,在 *ON_DESTROY*的时候, 判断了下!isChangingConfigurations()也就是 不是发生了配置变化,是真正销毁的时候,调用了 getViewModelStore().clear();

其实就是相当于把当前缓存的ViewModel 的对象嘎嘎的清空了。

经常说ViewModel 为什么不建议持有Context。 因为是在onDestroy后才执行。

也为什么说旋转屏幕ViewModel 不会丢数据。因为虽然走了onDestroy 但是内部判断了是否旋转屏幕。

  • 那Fragment 怎么管理的呢

我们记得Fragment 在 this 那一步的时候,有2种 情况,一种 拿FragmentActivity的ViewModelStore 。一种自己构建了一个FragmentManagerViewModel();

这个mNonConfig 其实在创建后 赛进了mFragmentStore。

1
2
3
4
5
6
7
8
9
10
11
ini复制代码 if (parent != null) {
mNonConfig = parent.mFragmentManager.getChildNonConfig(parent);
} else if (host instanceof ViewModelStoreOwner) {
ViewModelStore viewModelStore = ((ViewModelStoreOwner) host).getViewModelStore();
mNonConfig = FragmentManagerViewModel.getInstance(viewModelStore);
} else {
mNonConfig = new FragmentManagerViewModel(false);
}
// Ensure that the state is in sync with FragmentManager
mNonConfig.setIsStateSaved(isStateSaved());
mFragmentStore.setNonConfig(mNonConfig);

mFragmentStore.setNonConfig(mNonConfig);把mNonConfig传出去了,追着看一下。

进入 FragmentStore。

发现 FragmentStore 是在FragmentManager初始化就创建了

1
java复制代码 private final FragmentStore mFragmentStore = new FragmentStore();

FragmentStore里调用mNonConfig 查看下被调用的地方。

1
2
3
csharp复制代码 FragmentManagerViewModel getNonConfig() {
return mNonConfig;
}

进入FragmentManager.java

->clearBackStackStateViewModels()

这里去判断了哪些Fragment 需要清理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ini复制代码private void clearBackStackStateViewModels() {
boolean shouldClear;
if (mHost instanceof ViewModelStoreOwner) {
shouldClear = mFragmentStore.getNonConfig().isCleared();
} else if (mHost.getContext() instanceof Activity) {
Activity activity = (Activity) mHost.getContext();
shouldClear = !activity.isChangingConfigurations();
} else {
shouldClear = true;
}
if (shouldClear) {
for (BackStackState backStackState : mBackStackStates.values()) {
for (String who : backStackState.mFragments) {
mFragmentStore.getNonConfig().clearNonConfigState(who, false);
}
}
}
}

->dispatchDestroy()

1
2
3
4
5
6
7
scss复制代码 void dispatchDestroy() {
mDestroyed = true;
execPendingActions(true);
endAnimatingAwayFragments();
clearBackStackStateViewModels();
//省略代码
}

-> FragmentController.java->dispatchDestroy()

1
2
3
csharp复制代码public void dispatchDestroyView() {
mHost.mFragmentManager.dispatchDestroyView();
}

-> FragmentActivity.java->onDestroy(){}

1
2
3
4
5
6
scss复制代码 @Override
protected void onDestroy() {
super.onDestroy();
mFragments.dispatchDestroy();
mFragmentLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
}

把整个调用反过来看一下,FragmentActivity 在onDestroy 调用

mFragments.dispatchDestroy(); (FragmentController.java)

-> FragmentManager.dispatchDestroy() (FragmentManger.java)

-> clearBackStackStateViewModels() 。

可以理解为FragmentActivity销毁才销毁。

另外 FragmentStateManager 里 重新构建的时候,也会销毁清理对应的ViewModel。感兴趣可以看一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
scss复制代码void moveToExpectedState{
//省列部分代码
case Fragment.ATTACHED:
if (mFragment.mBeingSaved
&& mFragmentStore.getSavedState(mFragment.mWho) == null) { mFragmentStore.setSavedState(mFragment.mWho, saveState());
}
destroy();
}
void destroy() {
//省列部分代码
if (shouldDestroy) {
mFragment.performDestroy();
if ((beingRemoved && !mFragment.mBeingSaved) || shouldClear) { mFragmentStore.getNonConfig().clearNonConfigState(mFragment, false);
}
mFragment.performDestroy();
}
}

面试的侃侃而谈

  • 优势
+ 保存数据,页面变化能缓存
+ 自动管理,页面销毁自动清理
+ `Fragment`和 `Activity`可共用。
  • Fragment能拿 Activity 的ViewModel么?
+ 能 ,毕竟`FragmentManger` 那向上管理,其实取的就是上层`FragmentActivity`的`ViewModelStore`。
  • 怎么自动管理的?
+ `ComponentActivity` 监听`onDestroy()` ,清理
+ `Fragment`在`FragmentActivity` 的 `onDestroy()` 会清理。
  • ViewModelStore 知道么?
+ 知道 ,一个`map` 就是干。
  • 知道怎么创建的么?
+ 内部 `factory` 反射就是干。
  • 为啥旋转还能保存数据?

判断了是配置变化,如旋转屏幕等,等到真正销毁才清空。

  • 剩下的自行发挥

本文转载自: 掘金

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

1…495051…956

开发者博客

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