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

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


  • 首页

  • 归档

  • 搜索

Spring是如何解析占位符的(一)

发表于 2021-04-22

什么是Spring的占位符?

在以前的Spring Xml配置中我们可能会有如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd>
<context:property-placeholder ignore-unresolvable="true" location="classpath:jdbc.properties"/>

<bean id="jdbc" class="com.john.properties.jdbcBean" >
<property name="url" value="${jdbc.url}"/>
</bean>
</beans>

在上面的配置中jdbc这个Bean的url属性值${jdbc.url}就代表占位符,占位符的真实值就存放在上述配置中的context:property-placeholder自定义元素的location属性所代表的配置文件jdbc.properties中,这个配置文件里就一行内容:

1
ini复制代码jdbc.url=127.0.0.1

那问题就来了,Spring又是在什么阶段去解析并且把占位符替换为实际值的呢?

Spring什么时候去解析并占位符

从context:place-holder我们就可以知道它是一个自定义xml标签,那Spring势必要解析它,我就直接黏贴Spring解析这个自定义元素的入口代码给各位看官。该代码就在BeanDefinitionParserDelegate这个类中:

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
java复制代码/**
* Parse the elements at the root level in the document:
* "import", "alias", "bean".
* @param root the DOM root element of the document
*/
//todo doRegisterBeanDefinitions -> parseBeanDefinitions -> parseDefaultElement
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
if (delegate.isDefaultNamespace(root)) {
NodeList nl = root.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (node instanceof Element) {
Element ele = (Element) node;

//如果属于beans命名空间
if (delegate.isDefaultNamespace(ele)) {
//处理默认标签
parseDefaultElement(ele, delegate);
}
else {
//自定义标签
//用到了parser
//todo parser内部 去注册BeanDefinition 2021-3-15
delegate.parseCustomElement(ele);
}
}
}
}
else {
delegate.parseCustomElement(root);
}
}

主要关注点:
delegate.parseCustomElement(ele);

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复制代码	@Nullable
public BeanDefinition parseCustomElement(Element ele) {
return parseCustomElement(ele, null);
}

//todo property 子元素 也有可能 解析自定义元素 parsePropertySubElement
@Nullable
public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
String namespaceUri = getNamespaceURI(ele);
if (namespaceUri == null) {
return null;
}
//resolve里有初始化过程
//根据命名空间uri获取 NamespaceHandler

//todo 获取命名空间下的自定义handler 比如 ContextNamespaceHandler
NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
if (handler == null) {
//todo 如果在spring.handlers配置文件 里没有定义这个命令空间的handler就会 报这个错 2020-09-14
error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
return null;
}
//调用parse方法
//这里ParserContext注入registry
//readerContext里 reader->XmlBeanDefinitionReader 里包含了 registry
//TODO 会初始化一个ParserContext进去
return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}

到这里我们又要关注这个NamespaceHandler是怎么获取到的,从上面代码可知,Spring会从当前readerContext获取到NamespaceHandlerResolver后通过其resolve方法并根据传入的当前namespaceUri就可以获得当前适合的NamespaceHandler。

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
java复制代码/**
* Create the {@link XmlReaderContext} to pass over to the document reader.
*/
public XmlReaderContext createReaderContext(Resource resource) {
//把当前reader放进去 ,reader存放了 beanRegistry
//beanRegistry 里定义了 registerBeanDefinition 这个重要的方法
return new XmlReaderContext(resource, this.problemReporter, this.eventListener,
this.sourceExtractor, this, getNamespaceHandlerResolver());
}

/**
* Lazily create a default NamespaceHandlerResolver, if not set before.
* 解析自定义标签时用到
* @see #createDefaultNamespaceHandlerResolver()
*/
public NamespaceHandlerResolver getNamespaceHandlerResolver() {
if (this.namespaceHandlerResolver == null) {
this.namespaceHandlerResolver = createDefaultNamespaceHandlerResolver();
}
return this.namespaceHandlerResolver;
}

/**
* Create the default implementation of {@link NamespaceHandlerResolver} used if none is specified.
* <p>The default implementation returns an instance of {@link DefaultNamespaceHandlerResolver}.
* @see DefaultNamespaceHandlerResolver#DefaultNamespaceHandlerResolver(ClassLoader)
*/
protected NamespaceHandlerResolver createDefaultNamespaceHandlerResolver() {
ClassLoader cl = (getResourceLoader() != null ? getResourceLoader().getClassLoader() : getBeanClassLoader());
return new DefaultNamespaceHandlerResolver(cl);
}

//返回默认的
public DefaultNamespaceHandlerResolver(@Nullable ClassLoader classLoader) {
this(classLoader, DEFAULT_HANDLER_MAPPINGS_LOCATION);
}

从上面的代码中可知Spring使用的是默认的DefaultNamespaceHandlerResolver,它当然也给开发者留了自定义NamespaceHandlerResolver的机会。那我们现在就可以看看DefaultNamespaceHandlerResolver如何根据namespaceUri解析到对应的NamespaceHandler的。

首先context:place-holder获取到的就是context命名空间,完整路径为http://www.springframework.org/schema/context。我们从DefaultNamespaceHandlerResolver类中可以看到它是如何解析这个命名空间的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
java复制代码/**
* Locate the {@link NamespaceHandler} for the supplied namespace URI
* from the configured mappings.
* @param namespaceUri the relevant namespace URI
* @return the located {@link NamespaceHandler}, or {@code null} if none found
*/
@Override
@Nullable
public NamespaceHandler resolve(String namespaceUri) {
Map<String, Object> handlerMappings = getHandlerMappings();
Object handlerOrClassName = handlerMappings.get(namespaceUri);
if (handlerOrClassName == null) {
return null;
}
else if (handlerOrClassName instanceof NamespaceHandler) {
return (NamespaceHandler) handlerOrClassName;
}
else {
String className = (String) handlerOrClassName;
try {
Class<?> handlerClass = ClassUtils.forName(className, this.classLoader);
if (!NamespaceHandler.class.isAssignableFrom(handlerClass)) {
throw new FatalBeanException("Class [" + className + "] for namespace [" + namespaceUri +
"] does not implement the [" + NamespaceHandler.class.getName() + "] interface");
}
NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);
//todo 命名空间处理器 调用初始化过程 2020-09-04
namespaceHandler.init();
handlerMappings.put(namespaceUri, namespaceHandler);
return namespaceHandler;
}
catch (ClassNotFoundException ex) {
throw new FatalBeanException("Could not find NamespaceHandler class [" + className +
"] for namespace [" + namespaceUri + "]", ex);
}
catch (LinkageError err) {
throw new FatalBeanException("Unresolvable class definition for NamespaceHandler class [" +
className + "] for namespace [" + namespaceUri + "]", err);
}
}
}

/**
* Load the specified NamespaceHandler mappings lazily.
*/
private Map<String, Object> getHandlerMappings() {
Map<String, Object> handlerMappings = this.handlerMappings;
if (handlerMappings == null) {
synchronized (this) {
handlerMappings = this.handlerMappings;
if (handlerMappings == null) {
if (logger.isTraceEnabled()) {
logger.trace("Loading NamespaceHandler mappings from [" + this.handlerMappingsLocation + "]");
}
try {
//todo handlerMappings为空 才去 获取所有属性映射 2020-09-04
//spring-aop spring-beans spring-context
Properties mappings =
PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
if (logger.isTraceEnabled()) {
logger.trace("Loaded NamespaceHandler mappings: " + mappings);
}
handlerMappings = new ConcurrentHashMap<>(mappings.size());
CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
this.handlerMappings = handlerMappings;
}
catch (IOException ex) {
throw new IllegalStateException(
"Unable to load NamespaceHandler mappings from location [" + this.handlerMappingsLocation + "]", ex);
}
}
}
}
return handlerMappings;
}

上面代码中的handlerMappingsLocation一般就是Spring默认的路径:

1
2
3
4
5
java复制代码	//指定了默认的handler路径 ,可以传入指定路径改变
/**
* The location to look for the mapping files. Can be present in multiple JAR files.
*/
public static final String DEFAULT_HANDLER_MAPPINGS_LOCATION = "META-INF/spring.handlers";

我们就回到spring-context项目工程下的resoures/META-INF文件夹下的spring.handlers文件里定义了如下规则:

1
2
3
4
5
ruby复制代码http\://www.springframework.org/schema/context=org.springframework.context.config.ContextNamespaceHandler
http\://www.springframework.org/schema/jee=org.springframework.ejb.config.JeeNamespaceHandler
http\://www.springframework.org/schema/lang=org.springframework.scripting.config.LangNamespaceHandler
http\://www.springframework.org/schema/task=org.springframework.scheduling.config.TaskNamespaceHandler
http\://www.springframework.org/schema/cache=org.springframework.cache.config.CacheNamespaceHandler

可以看到context自定义命名空间就是对应的ContextNamespaceHandler。我们打开这个类瞧一瞧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public class ContextNamespaceHandler extends NamespaceHandlerSupport {

@Override
public void init() {
//调用抽象类NamespaceHandlerSupport的注册解析器方法
registerBeanDefinitionParser("property-placeholder", new PropertyPlaceholderBeanDefinitionParser());
registerBeanDefinitionParser("property-override", new PropertyOverrideBeanDefinitionParser());
registerBeanDefinitionParser("annotation-config", new AnnotationConfigBeanDefinitionParser());
registerBeanDefinitionParser("component-scan", new ComponentScanBeanDefinitionParser());
registerBeanDefinitionParser("load-time-weaver", new LoadTimeWeaverBeanDefinitionParser());
registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser());
registerBeanDefinitionParser("mbean-export", new MBeanExportBeanDefinitionParser());
registerBeanDefinitionParser("mbean-server", new MBeanServerBeanDefinitionParser());
}
}

发现它只定义了一个init方法,顾名思义就是初始化的意思,那想当然的就是它啥时候会执行初始化呢?我们回到DefaultNamespaceHandler的resolve方法,发现它内部有一处**namespaceHandler.init();, **这里就执行了对应命名空间处理器的初始化方法。接下来我们又要看看初始化了做了些啥,我们发现它调用了同一个方法registerBeanDefinitionParser也就是注册Bean定义解析器,到这里我们先按下暂停键,再捋下上面的整体流程:

  1. Spring在解析自定义标签的时候会根据自定义命名空间去查找合适的NamespaceHandler.
  2. 自定义的NamespaceHandler是由NamespaceHandlerResolver去解析得到的。
  3. NamespaceHandlerResolver会根据classLoader.getResources查找所有类路径下的spring.handlers。
  4. 查找到后把文件内容转换成handlerMappings,然后根据传入的自定义命名空间匹配到NamespaceHandler
  5. 执行NamespaceHandler的init方法注册BeanDefinitionParser。

那这个parser做了点什么强大的功能呢?我们下回分解。

本文转载自: 掘金

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

Airtest从入门到放弃?不要急,这份免费的“超长”攻略请

发表于 2021-04-22

此文章来源于项目官方公众号:“AirtestProject”

版权声明:允许转载,但转载必须保留原链接;请勿用作商业或者非法用途

前言

不知道你有没有遇到这种情况?在刚接触我们的Airtest项目的时候,总是兴致满满、斗志昂扬;但使用一段时间后,却总是被“找不到图片”、“连不上设备”、“录制的脚本不能运行”这些问题劝退。

不要着急,今天我们特意跟同学们分享下当年入坑Airtest的经验;希望看完今天这篇攻略,可以让你在入门Airtest的时候少走一些弯路!

1.设备连接篇

1)连接Android设备

Airtest支持2种类型的安卓设备,一种是安卓真机,另一种是安卓模拟器。

先来说下真机,用USB线连接手机和电脑、打开开发者模式的USB调试选项,这些基本操作都不在话下,重要的是,完成这些基本操作之后,发现IDE还是连不上这些安卓设备。

连不上的典型现象如下:

① 在IDE的设备连接窗口,看到待测设备的ADB status是 unauthorized 。这时候你就要看下手机上是否出现了1个 允许USB调试的弹窗 ,看到了点击确认即可。如果没看到的话,那就拔插设备,重新来一遍基础操作。

image

② 连接设备的时候,log查看窗出现1个 AdbShellError 的报错,这个情况就复杂的多。因为造成这个报错的原因有很多种,比如出现ADB冲突,这时候往往会出现 adb server version (40) doesn't match this client (39); killing... 这样的提示。又或者是你的手机没有成功安装Airtest的输入法Yosemite.apk。当然,更多的可能是你这个真机是某些特殊品牌,需要开启一些额外的设置,才能够使用我们的Airtest,这个额外设置因设备而异,具体可以参考我们的 官方文档 给大家 总结的特殊设备厂商需要开启的额外设置 (airtest.doc.io.netease.com/IDEdocs/dev…

③ 连接上设备之后黑屏、闪退或者不能点击屏幕。这时候只能请出3个备选连接参数了,通常情况下,黑屏或者闪退,可以勾选第一个 use Javacap ,不能点击屏幕的特殊设备,可以勾选最后一个 use Adb Touch 。

image

再来说下安卓模拟器,市面上主流的模拟器就那么几款,连接模拟器时,除了基本的打开开发者模式的USB调试之外,我们主要注意以下事项即可:

① 手动给模拟器装上Airtets专用输入法Yosemite.apk,因为很多款模拟器都不能自动装上这个应用,所以我们提前手动装下更加稳妥

② 模拟器基本上都自带adb工具,所以这时候要特别注意下模拟器自带的adb工具版本跟Airtest的adb工具版本是否一致,不一致的话请统一成1个版本,不然非常容易造成ADB冲突,导致模拟器连接不上

③ 搞清楚自己所用品牌的模拟器端口和在IDE连接时是否需要勾选备选连接参数,下述表格可供同学们参考

image

2)连接iOS设备

目前Airtest只支持连接iOS真机;另外我们连接iOS设备,还是需要一台mac完成前期的一些部署工作。

如果底层工具使用的是我们的iOS-tagent(支持到iOS13.5),则连接步骤为:

  • 用xcode部署iOS-tagent到真机
  • 设置代理
  • 在IDE中连接iOS

详细教程可以参考官方文档的 iOS设备连接 章节。(airtest.doc.io.netease.com/IDEdocs/dev…

如果底层工具使用的是appium的WebDriverAgent(Airtest1.1.8版本或者AirtestIDE1.2.8版本开始兼容),则连接步骤为:

  • 用xcode部署appium的WebDriverAgent
  • 设置代理
  • 在IDE中连接iOS

使用appium的WebDriverAgent,xcode和iOS都可以支持到最新版本;另外部署的详细教程可以参考我们的往期推文:Airtest年前重磅更新,对iOS的支持全面升级优化啦! 。

此外,再跟大家分享1个用PC测试iOS的好工具–tidevice,可实现不依赖 xcode 启动 WebDriverAgent 完成设备连接!详细教程可以参考我们的往期推文: 脱离Mac搞iOS自动化,tidevice工具教你轻松实现! 。

另外,iOS测试最头疼的1个问题就是卡顿问题,不像安卓测试那样可以做到实时同步,这个问题受限于底层的WebDriverAgent工具,暂时没有非常好的优化办法,如后续有了较好的优化效果,也会同步给大家的。

3)连接Windows应用

我们需要明确,IDE连接Windows应用,有2种模式,嵌入模式和非嵌入模式。顾名思义,嵌入模式,就是连接Windows窗口时,会把应用窗口嵌入到IDE的设备连接窗口中去:

image

而非嵌入模式,则是连接上了Windows窗口,但该窗口不用嵌入到IDE的设备连接窗口中去,仍可在IDE之外自由活动:

image

还需要注意的是,选择嵌入模式和非嵌入模式,取决于你的Windows应用适合哪种模式;因为并不是所有Windows应用都可以嵌入到IDE去进行测试。详细的连接方式和注意事项,可以参考我们的往期推文: Airtest测试Windows窗口翻车?还是想跟你们聊聊测试win窗口的一些问题 。

4)暂不支持连接mac桌面

没错,如题所示,我们现在还不支持连接mac桌面来进行自动化测试,后续支持了会在官方Q群和官方公众号同步更新内容。

5)脚本连接设备

在我们使用AirtestIDE运行 .air 脚本时,会默认帮我们用设备连接窗口当前连接上的设备来运行。这就相当于,在IDE运行 .air 脚本时,它帮助我们做了设备连接这个步骤。因此,即使在脚本中我们没看到连接设备的语句,依然能够使用设备连接窗口的设备来运行我们的自动化脚本。

但是如果我们使用 .py 脚本,或者在其它编辑器上运行我们的Airtest脚本时,就需要在脚本开头,用脚本完成设备连接这个步骤。

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码# 连接安卓设备
auto_setup(__file__,devices=["Android://127.0.0.1:5037/79d03fa"])
auto_setup(__file__,devices=["Android://127.0.0.1:5037/127.0.0.1:62001?cap_method=JAVACAP&&ori_method=ADBORI"])

# 连接iOS设备
auto_setup(__file__,devices=["iOS:///127.0.0.1:8100"])

# 连接Windows窗口
auto_setup(__file__,devices=["Windows:///123456"])
auto_setup(__file__,devices=["Windows:///"])

# 不支持连接mac桌面进行自动化测试

当然,使用其它设备连接接口(比如 connect_device()、init_device())的方式,以及更多脚本连接设备的注意事项,可以参考我们的往期推文:用1行代码搞定自动化测试的设备连接问题 。

2.脚本录制/编写篇

1)脚本录制

AirtestIDE支持Airtest、Poco和airtest-selenium脚本的录制:

image

但录制脚本功能的使用,都是有一定前提的:

① Airtest录制,保证IDE的设备连接窗口连接上了安卓设备、iOS设备或者Windows窗口(嵌入不嵌入都行)。

② Poco录制,保证IDE的设备连接连接上了安卓设备或者iOS设备(不支持Windows桌面应用),并且在poco辅助窗中选择了待测的poco模式刷出了正常的UI树。能连接上设备,不能刷出UI树的情况,请参考我们的 官方文档 给出的解决办法。(airtest.doc.io.netease.com/IDEdocs/faq…

③ airtest-selenium录制,需要使用selenium window 的浏览器初始化按钮,打开1个chrome或者firefox浏览器之后(只支持这2种浏览器),才能进行录制:

image

另外,同学们要明确,录制脚本只适合简单的点击、滑动等操作,且可以帮助新手同学快速上手框架的部分api。但是仅仅依赖录制功能就完成内容复杂的自动化脚本,是不现实的!

而且录制出来的脚本,并不全是符合同学们期待的脚本,有可能存在录制出来的截图脚本回放时不通过的情况;或者录制出来的poco脚本层次过深,导致回放非常不稳定的情况。这时候我们就要根据实际情况来优化这些录制出来的脚本了。

2)脚本编写

录制不是万能的,最终还是要自己会用框架的api,才能编写出更加丰富,功能更加符合需求的自动化测试脚本。

所以我们来简单聊聊如何编写Airtest脚本:

首先,IDE给我们提供的api快捷键不能放过,这里面几乎包含了Airtest的所有核心API,鼠标移动上去,还可以看到参数详情,非常便捷:

image

其次,Airtest提供了 中文版的API文档 ,核心API还有详细的示例供大家学习(airtest.readthedocs.io/zh_CN/lates…

image

基本上掌握了核心API,就可以实现我们的大部分测试需求了;剩下的就是python脚本能力的体现了,比如如何编写判断语句、循环语句等等。

不喜欢看API文档的同学,还可以查阅我们往期整理的2篇Airtest API的汇总教程,涵盖了Airtest核心API、设置的介绍和脚本示例:

  • 最全Airtest接口功能介绍和示例总结,新手同学千万不能错过呀!(一)
  • 最全Airtest接口功能介绍和示例总结,新手同学千万不能错过呀!(二)

3.脚本运行篇

1)如何运行

如果是在AirtestIDE运行脚本,那直接快捷键F5,或者点击顶部菜单栏的运行按钮即可:

image

那么脱离AirtestIDE如何运行脚本呢?情况大致分为2种:

① 一种是在命令行使用 airtest run test.air 命令运行,该命令后面还可以跟着设备参数、log参数和录屏参数:

  • --device,用来指定连接的被测设备
  • --log,用来指定log内容和截图存放的目录
  • --recording,运行脚本时进行录屏操作

运行脚本时带不带参数,取决于你的脚本是否已经完成了连接设备、保存log、录屏等工作,如果脚本里面已经包含这些步骤,那命令行运行时,这些参数就不用带了;如果你的脚本并没有完成这些步骤,那命令行运行脚本时,就要根据自己的需要,有选择性地带上这些参数。

详细内容可以参考我们的往期推文:如何使用命令行运行脚本、生成报告 。

② 第二种就是在别的python编辑器上运行我们的airtest脚本,比如pycharm。运行的话一般没什么问题,点击编辑器对应的运行脚本按钮即可。重要的是编写脚本的规范性、以及运行环境的建设问题。以pycharm为例,我们在运行脚本之前,需要部署好当前的python环境,把脚本中用到的库,比如airtest、pocoui或者airtest-selenium装好。另外如果脚本是AirtestIDE迁移过来的,还需要特别注意是否有设备连接语句、截图路径是否正确等等,详细的注意事项,也可以参考我们的往期推文:在Pycharm上编写Airtest脚本的常见问题,希望你可以避开 。

2)运行后的常见问题:图像找不到

像 Picture xxx not found in screen ,可能是新手同学在学习Airtest脚本的时候最常遇到的问题了!很多同学会说,明明我用肉眼已经看到截图是存在于设备画面上的,为什么运行总是不通过?或者是截图脚本运行是通过了,但是点到了别的地方去,并不是点到了我的预期位置上。

首先我们要了解截图脚本运行的本质。实际上运行我们的截图脚本,就是1个图像识别的过程,airtest会用多个图像识别算法去设备画面里面匹配我们的截图。算法并不是人眼,它只能依靠截图的一些向量特征或者特征点来匹配结果,这就意味着,它匹配出来的结果会出现不满足我们实际预期的情况。

当然,想要提高我们的截图脚本的命中率,也是有非常多的技巧的,详情可以参考我们的往期推文: 写了10000条Airtest截图脚本总结出来的截图经验,赶紧收藏! 。

3)运行后的其它语法问题

比如 invalid syntax 或者 API的用法报错,这些问题都是没有任何捷径的!基本的python语法,比如符号的完整性、缩进格式的统一性,可以直接网上看一些python基础教程来学习。

API的用法错误,请移步上文,多看推文的示例!多翻API文档!

4.生成报告篇

编写完脚本之后,先运行脚本,再生成报告!右键单独运行某一条脚本,不算正式运行脚本,所以不会保存log内容,也就意味着不会生成有log内容的报告。

1)在IDE生成报告

在AirtestIDE运行 .air 脚本时,运行命令默认是带log参数的,所以运行脚本之后都会保存运行的log内容:

image

运行结束之后,我们直接点击顶部菜单栏的查看报告按钮即可一键生成并打开HTML格式的报告。

在AirtestIDE新建 .py 脚本,自动生成的初始化脚本带有 logdir=True ,所以默认也是保存了log内容的:

image

同理,运行完毕之后,也可以点击顶部菜单栏的查看报告按钮一键生成并打开HTML格式的报告。

2)命令行生成报告

上文我们了解了 airtest run test.air 命令用于运行脚本,使用该命令在命令行运行脚本之后,我们可以使用生成报告的指令:airtest report test.air 来生成该脚本的Airtest报告。

命令行生成报告可带的参数非常多:

  • --log_root,指定log内容和截图文件所在的目录
  • --outfile,指定生成报告的目录
  • --lang,指定报告语言,可以是中文/英文
  • --export,导出一个包含所有资源的报告文件
  • --static_root,指定静态资源文件的路径
3)脚本生成报告

用python脚本生成报告,有2种方式:

① 使用simple_report接口:

1
python复制代码simple_report(__file__,logpath=True,output=r"D:\test\report02\log.html")

API的参数详情可以参考我们的官方API文档:airtest.readthedocs.io/zh_CN/lates… 。

② 使用LogToHtml类:

1
2
python复制代码h1 = LogToHtml(script_root=r'D:\test\report01.air', log_root=r"D:\test\report01.air\log", export_dir=r"D:\test\report02" ,logfile=r'D:\test\report01.air\log\log.txt', lang='en', plugins=None)
h1.report()

此类的使用详情可以参考文档:airtest.readthedocs.io/zh_CN/lates… 。

4)生成的报告为空

报告为空,大概率是因为log.txt文件的内容为空,要么是没有保存log,要么是生成报告的语句放在了脚本开头,导致还没运行到实际的步骤就已经生成了一份空报告。

小结

那么今天这篇教程,从 设备连接–脚本录制/编写–脚本运行–生成报告 ,详细给大家分享了入坑Airtest的经验,内容很长,同学们可以收藏起来,慢慢看~


AirtestIDE下载:airtest.netease.com/

Airtest 教程官网:airtest.doc.io.netease.com/

搭建企业私有云服务:airlab.163.com/b2b

官方答疑 Q 群:654700783

呀,这么认真都看到这里啦,帮忙在文章左侧点一下点赞和收藏,给我一个支持把,灰常感谢~

本文转载自: 掘金

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

Go语言interface万能类型及其类型断言(18) G

发表于 2021-04-22

Go语言接口也叫interface,interface里面主要是定义一些方法名称,前面第二篇讲过,这个高度抽象的类型不理解它很容易忘,而且有一些高级用法需要认真研究才能懂,通常用由结构体(sctuct)来声明方法并实现它们,今天再仔细讲讲它的高级用法

万能类型

Go语言的基础数据类型都实现了interface{},也就是说interface{}这个空接口都能以引用任意的数据类型,例如int,string,float,struct,怎么引用呢?那就是在函数的形参可以使用空接口,实参可以是任意的数据类型,我们来举一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码package main
import "fmt"
func showAny(mytest interface{}){
fmt.Println(mytest) //直接打印输出传入的interface
}

func main() {
type user struct{
name string
}
showAny("string test")
showAny(123)
showAny(123.456)
worker :=user{"土味挖掘机"}
showAny(worker)
}

go run main.go看看运行结果:
image.png
从运行结果可以看的出来,空接口的确可以引用任意的数据类型。这不就是面对对象三大特征特征:抽象、继承、多态中多态的概念吗?

psb.gif

类型断言

断言通俗来说就是判断变量的类型,在Gin框架的文档中也有类似的断言的过程:

image.png
binding.Validator.Engine()是第三方包实现的接口,里面的*validator.Validate就是指针传递的验证内容。

现在就实现一个简单的类型断言吧,改一下上面的代码例子,改一下showAny函数的内容,判断接口引用的内容:

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
go复制代码package main
import "fmt"

type user struct{
name string
}
func showAny(mytest interface{}){
_,ok:=mytest.(string)
if ok{
fmt.Printf("%s是:字符串\n",mytest)
}
_,ok=mytest.(int)
if ok{
fmt.Printf("%d是:整数\n",mytest)
}
_,ok=mytest.(float64)
if ok{
fmt.Printf("%g是:浮点数\n",mytest)
}
_,ok=mytest.(user)
if ok{
fmt.Printf("%s是:user结构体\n",mytest)
}

}

func main() {
showAny("string test")
showAny(123)
showAny(123.456)
worker :=user{"土味挖掘机"}
showAny(worker)
}

执行结果:

image.png
所以使用mytest.(type)来判断mytest接口的内容,如果返回的第二个参数不为False,那么就是断言成功,判断类型正确,为False就继续执行下面的判断。
image.png

总结

之前讲过interface 的普通的用法,可以用任何定义的类型实现任何接口,今天说了一下空接口可以引用任何类型,也可以做类型断言,体现了接口灵活强大,体现了Go语言多态的特征。

image.png

本文转载自: 掘金

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

盘点 SpringIOC 循环依赖

发表于 2021-04-21

总文档 :文章目录

Github : github.com/black-ant

一 . 前言

这是一篇尝试型的文章 , SpringIOC 的笔记基本上已经整理出来了 , 但是始终没有想好该怎么输出为文档 .

在我看来 , 如果我花了时间整理出一篇文档 , 那么不论多久 , 哪怕有所遗忘 , 后面再读的时候也应该能迅速读懂 . 整个 IOC 体系会以这个思路去输出 .

IOC 整个体系过于庞大 , 哪怕把之前的笔记全部梳理完 , 仍然感觉还是不够的 . 不过先挑个好说的 , 看看能不能先把这一部分梳理好 .

二 . 功能现象

循环依赖是指三个对象互相依赖 , 当通过 getBean 去获取依赖的 Bean 时 , 就形成了循环依赖

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

// 这里先使用 Scope 尝试使用 prototype 模式
@Component
@Scope(value = "prototype")
public class BeanCService{}

// 当 BeanA , BeanB , BeanC 相互依赖时 , 结果是这样的 :
The dependencies of some of the beans in the application context form a cycle:
beanStartService (field private com.gang.BeanAService com.gang.BeanStartService.beanAService)
┌─────┐
| beanAService (field private com.gang.BeanBService com.gang.BeanAService.beanBService)
↑ ↓
| beanBService (field private com.gang.BeanCService com.gang.BeanBService.beanCService)
↑ ↓
| beanCService (field private com.gang.BeanAService com.gang.study.BeanCService.beanAService)
└─────┘

去掉 @Scope(value = “prototype”) 后 , 一切正常

循环依赖是在 单例模式中处理完成的

三 . 源码跟踪

我们从源码的角度分析一下 , SpringIOC 的单例是怎么控制循环依赖的

3.1 单例Bean 的加载流程

一切的起点

起点当然是从 AbstractBeanFactory 开始的 , 这一步会在 Bean 创建时详细说说 , 这里先放过

1
2
3
java复制代码C- AbstractBeanFactory 
M- doGetBean :
- Object sharedInstance = getSingleton(beanName) : 从缓存中获取Bean

可以看到 , 当创建第一个 BeanA 时 , 进入 DefaultSingletonBeanRegistry

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
java复制代码// 继续深入 DefaultSingletonBeanRegistry , 其中有以下的参数
C180- DefaultSingletonBeanRegistry
F01- private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
?- 单例对象的缓存:bean到bean实例的名称

F02- private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
?- 单例工厂的缓存:ObjectFactory的bean名

F03- private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
?- 早期单例对象的缓存:bean到bean实例的名称

F04- private final Set<String> registeredSingletons = new LinkedHashSet<>(256);
?- 一组已注册的单例,按注册顺序包含bean名称

F05- private final Set<String> singletonsCurrentlyInCreation =Collections.newSetFromMap(new ConcurrentHashMap<>(16));
?- 当前正在创建的bean的名称

F06- private final Set<String> inCreationCheckExclusions =Collections.newSetFromMap(new ConcurrentHashMap<>(16));
?- 当前在创建检查中被排除的bean的名称

F07- private Set<Exception> suppressedExceptions;
?- 异常列表

F08- private boolean singletonsCurrentlyInDestruction = false;
?- 标志是否正在销毁单例

F09- private final Map<String, Object> disposableBeans = new LinkedHashMap<>();
?- 一次性bean实例:从bean名称到一次性实例

F10- private final Map<String, Set<String>> containedBeanMap = new ConcurrentHashMap<>(16);
?- 包含bean名称之间的映射:bean名称到bean所包含的一组bean名称

F11- private final Map<String, Set<String>> dependentBeanMap = new ConcurrentHashMap<>(64);
?- 依赖bean名称之间的映射:bean名称到一组依赖bean名称

F12- private final Map<String, Set<String>> dependenciesForBeanMap = new ConcurrentHashMap<>(64);
?- 依赖bean名称之间的映射:bean名称到bean依赖项的一组bean名称

怎么创建一个单例Bean

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复制代码// 当我们创建一个单例Bean 时
C180- DefaultSingletonBeanRegistry
M180_01- getSingleton(String beanName, boolean allowEarlyReference)
- 首先从 Map<String, Object> singletonObjects 中获取
- 如果没有 , 且正在创建 (->M180_02) , 则继续从 earlySingletonObjects 中获取
- 如果还是没有 , 且需要创建早期引用 , 则继续从 this.singletonFactories 中获取
-如果 singletonFactories 获取到了 , 将当前 bean 放入 singletonObjects 且从 singletonFactories 移除
M180_02- isSingletonCurrentlyInCreation
?- 判断当前 Bean 是否已经在注册
- singletonsCurrentlyInCreation.contains(beanName)


// M180_01 代码 : 分别从三个 ConcurrentHashMap 中获取对应的 Bean
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}

从整个流程来看 , 最主要的就是从三个 CurrentHashMap 本地缓存中获取 Bean 实例

PS : 这里可以预先说一下 , 当第一个Bean 创建时 , 就已经在缓存总存在了 , 哪怕他还没有完全加载完成

BeanABeanBBeanCsingletonObjectsearlySingletonObjectssingletonFactoriesGet(B) 获取 BeanBGet(C) 获取 BeanCGet(A) 获取 BeanA从singletonObjects 中第一次获取获取 BeanA获取失败 ,且已创建从缓存中Get(A) 获取 BeanA获取失败 ,且需要提前创建从缓存中Get(A) 获取 BeanABeanABeanBBeanCsingletonObjectsearlySingletonObjectssingletonFactories
我们就这几个缓存来好好的看一下 :

继续向下检索 ,看一下 上面2个对象是什么时候放上去的

1
2
3
4
5
6
java复制代码// 着重复习一下上面几个对象

singletonsCurrentlyInCreation : 当前正在创建的 Bean 名称集合
singletonObjects : 单例对象的缓存
earlySingletonObjects : 早期单例对象的缓存
singletonFactories : 单例工厂的缓存

3.2 singletonsCurrentlyInCreation

Step 1 : singletonsCurrentlyInCreation 插入的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码C180- DefaultSingletonBeanRegistry      
M180_03- beforeSingletonCreation
?- this.singletonsCurrentlyInCreation.add(beanName) : 添加当前 BeanName
M180_04- afterSingletonCreation
?- this.singletonsCurrentlyInCreation.remove(beanName) : 移除当前 BeanName

// 这里大概就清楚了 插入的方式
// PS : 以下方法中都会调用 beforeSingletonCreation 方法 , 三个方法处理完成后 , 就会移除
C- AbstractAutowireCapableBeanFactory
M- getSingletonFactoryBeanForTypeCheck
C- DefaultSingletonBeanRegistry
M- getSingleton : 单例Bean 创建的位置
C- FactoryBeanRegistrySupport
M- getObjectFromFactoryBean

3.3 singletonObjects 的存储

singletonObjects 存入的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码M180_05- addSingleton
- this.singletonObjects.put(beanName, singletonObject)
?- 也就是说每添加一个 单例 ,都会往其中添加一个对象
M- getSingletonMutex : 这是唯一一个有可能放入数据的地方了
- return this.singletonObjects;

// 打断点跟踪后发现以下类中进行了调用:
C- FactoryBeanRegistrySupport
M- getObjectFromFactoryBean : 只是作为一个锁对象
C- AbstractApplicationEventMulticaster
M- setBeanFactory
- this.retrievalMutex = this.beanFactory.getSingletonMutex() : 将对象进行了引用
?- 其中也大部分作为锁对象 , 没有进行操作

PS : 使用该对象作为 synchronized 的监视对象可以有效保证唯一性

一开始我在想 ConcurrentHashMap 应该也可以保证并发唯一 , 后来想了一下 , 这里的唯一应该是业务流程上的唯一 ,例如我这里要删除 , 你那里就不能进行添加 , 等我做完了 , 你才能继续

singletonObjects 删除的地方

1
2
3
4
5
6
7
java复制代码
C180- DefaultSingletonBeanRegistry
M180_07- clearSingletonCache
- 这里不止一个集合 , 是把所有的集合都清空了
?- 看了一下 , 主要是再 destroySingletons 流程中调用
M180_08- removeSingleton
- 移除单个单例时 ,此时时 remove

PS : 也就是说 , 销毁单例时 , 该数据会被清空或者移除

1
2
3
4
5
6
7
8
9
10
java复制代码// M180_07 代码
protected void clearSingletonCache() {
synchronized (this.singletonObjects) {
this.singletonObjects.clear();
this.singletonFactories.clear();
this.earlySingletonObjects.clear();
this.registeredSingletons.clear();
this.singletonsCurrentlyInDestruction = false;
}
}

addSingletonsingletonObjectsclearSingletonCacheremoveSingletonsingletonObjects 集合addSingletonFactory 时 put进入参数clearSingletonCache 方法中移除removeSingleton 方法中移除addSingletonsingletonObjectsclearSingletonCacheremoveSingleton

3.4 earlySingletonObjects

earlySingletonObjects 保存的地方

1
2
3
4
java复制代码C180- DefaultSingletonBeanRegistry  
M180_01- getSingleton(String beanName, boolean allowEarlyReference)
?- 还是之前获取的位置 , 可以看到 , 这里有一个参数是 allowEarlyReference
- this.earlySingletonObjects.put(beanName, singletonObject)

PS : 这个的位置为 初始类 BeanA 加载时 , 此时未注入依赖 , 只是第一次构建

earlySingletonObjects 是一个中间缓存 , 在Single Bean 添加完成后 , 这个中间缓存就会被清除

earlySingletonObjects 清除的地方

1
2
3
4
java复制代码C180- DefaultSingletonBeanRegistry     
M180_09- addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) : 清除单个
M180_07- clearSingletonCache : 清空所有
M180_05- addSingleton : 清除单个

getSingletonearlySingletonObjectsaddSingletonFactoryclearSingletonCacheaddSingletonearlySingletonObjects 集合getSingleton 时 put进入参数addSingletonFactory 方法中移除clearSingletonCache 方法中 clearaddSingleton 方法中 移除getSingletonearlySingletonObjectsaddSingletonFactoryclearSingletonCacheaddSingleton

3.5 singletonFactories

singletonFactories 是单例工厂

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复制代码// 还是先看下
C180- DefaultSingletonBeanRegistry
M180_07- addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory)
IF- 如果 singletonObjects (F01)包含 当前 BeanName
TRUE- singletonFactories (F02) 添加当前 Bean 和 singletonFactory
?- this.singletonFactories.put(beanName, singletonFactory);

// PS : 单例工厂是哪个环节被调用的
首先 , getSingle 中 , 实际上接的是一个 lambda 表达式
C180- DefaultSingletonBeanRegistry
M180_01- getSingleton(String beanName, boolean allowEarlyReference)
- ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
- singletonObject = singletonFactory.getObject();

// 该对象是在 AbstractAutowireCapableBeanFactory # doCreateBean 中生成的
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

// 其中传入了一个 getEarlyBeanReference 方法 , 该方法会在 getObject 回调
() -> getEarlyBeanReference(beanName, mbd, bean) --- singletonFactory.getObject()
// 最终会通过 SmartInstantiationAwareBeanPostProcessor 来处理生成
SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);

// 最终会生成一个空的对象 , 放进去

PS : 该对象会在后续中被 getSingleton 获取继续处理

DefaultSingletonBeanRegistrysingletonFactoriesgetSingletonremoveSingletonclearSingletonCachesingletonFactories 集合addSingletonFactory 时 put进入参数singletonFactories 方法中移除removeSingleton 方法中移除clearSingletonCache 方法中 clearDefaultSingletonBeanRegistrysingletonFactoriesgetSingletonremoveSingletonclearSingletonCache
image-20210421095922482.png

image-20210421100927098.png

3.6 为什么 prototype 不可行

这里涉及到 prototype 的加载过程 :

prototype 的创建是在 AbstractBeanFactory 中完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码C171- AbstractBeanFactory 
M171_02- doGetBean( String name, Class<T> requiredType,Object[] args, boolean typeCheckOnly)
- 如果是 Singleton , 则 getSingleton
?- 走了缓存集合
- 如果是 Prototype , 则 createBean
?- 这里是没有缓存类来处理的

// M171_02 伪代码
if (mbd.isSingleton()) {
//.....
} else if (mbd.isPrototype()) {
Object prototypeInstance = null;
try {
beforePrototypeCreation(beanName);
prototypeInstance = createBean(beanName, mbd, args);
}finally {
afterPrototypeCreation(beanName);
}
bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
}

完整流程

BeanCsingletonObjectsearlySingletonObjectssingletonFactories从singletonObjects 中第一次获取获取 BeanA获取失败 ,且已创建从缓存中Get(A) 获取 BeanA获取失败 ,且需要提前创建从缓存中Get(A) 获取 BeanABeanCsingletonObjectsearlySingletonObjectssingletonFactories

补充资料 : SingletonBeanRegistry 体系

SingletonBeanRegistry001.jpg

总结

循环依赖的处理流程其实很简单 , 篇幅也不长 , 主要就是几个缓存集合的使用.

这篇主要是想看看能不能使文档具有快速会议的能力

参考文献

cmsblogs.com/?cat=206

www.cnblogs.com/qinzj/p/114…

附录

本文转载自: 掘金

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

史上最详Android版kotlin协程入门进阶实战(一)

发表于 2021-04-21

banners_twitter.png

前言

笔者在写这篇文章的时候纠结了很久,不知道该以怎样的形式去讲解kotlin协程知识。笔者以前在学习的时候,也白嫖过各种各样的知识,看过很多文章,大概能够总结为三种::

  • 讲的太浅,三两句话就带过去了,看完以后就只剩下索然无味image.png。
  • 讲的太深,从头到尾都是晕乎乎的,最后总结就是三句话:我在哪,我在干嘛,手机真好玩image.png。
  • 内容适中,但是用到实际开发中时开始各种突然的翻车,然后挠头:怎么结果跟我想象的不一样啊image.png

知识的学习过程就像谈恋爱一样,讲究循序渐进。上来就想深入了解的话,那大概率是很容易翻车的。但聊得太浅吧,感情又不到位,后续想深入后很难。没有系统的学习是很难讲学到的知识点融会贯通的,因为笔者的想法是:”让读者在更容易吸收kotlin协程知识的同时,能够无缝衔接到实际应用开发中去“。所以接下来对于每一个知识点讲解,笔者将会根据不同的阶段,讲解到不同的深度,至于这个实际的深度是否能够满足读者们的需求,那就只能各位自己去体验了。

文章概览

本文章面向的对象是具有一定的kotlin基础和Android开发的基础。笔者会以第一视角出发,从零开始创建项目进行讲解。文章主要在于讲解Kotlin协程的基本使用、项目应用以及部分协程原理知识。附带会将讲解一些kotlin知识、Android Jetpack组件、常用第三方框架的基本使用等(不会深究原理,只是基础使用)。文章主要分为5个层次:

  1. kotlin协程的基础用法。
  2. kotlin协程的关键知识点初步讲解。
  3. 使用kotlin协程开发Android的应用。
  4. kotlin协程结合Android Jetpack组件应用。
  5. kotlin协程结合第三方框架的使用,如:Retrofit,OkHttp,coil等。
  6. 深入kotlin协程原理(如:CoroutineContext、Dispatcher、CoroutineExceptionHandler、Continuation、Scheduler等等)

由于文章涉及到的只是点比较多、内容可能过长,可以根据自己的能力水平和熟悉程度分阶段跳着看。如有讲述的不正确的地方劳烦各位动动小手私信给笔者,万分感谢image.png

由于时间原因,笔者白天工作只有晚上空闲时间才能写作,所以更新频率应该在一周一篇,当然我也会尽量的利用时间,争取能够提前发布。为了方便阅读将本文章拆分个多个章节,根据自己需要选择对应的章节,现在也只是目前笔者心里的一个大概目录,最终以更新为准:

Kotlin协程基础及原理系列

  • 史上最详Android版kotlin协程入门进阶实战(一) -> kotlin协程的基础用法
  • 史上最详Android版kotlin协程入门进阶实战(二) -> kotlin协程的关键知识点初步讲解
  • 史上最详Android版kotlin协程入门进阶实战(三) -> kotlin协程的异常处理
  • 史上最详Android版kotlin协程入门进阶实战(四) -> 使用kotlin协程开发Android的应用
  • 史上最详Android版kotlin协程入门进阶实战(五) -> kotlin协程的网络请求封装

Flow系列

  • Kotlin协程之Flow使用(一)
  • Kotlin协程之Flow使用(二)
  • Kotlin协程之Flow使用(三)

扩展系列

  • 封装DataBinding让你少写万行代码
  • ViewModel的日常使用封装

文章中使用的主要版本号

  • Android studio 4.1.3
  • kotlin: 1.4.32
  • kotlinx-coroutines-android:1.4.3
  • Retrofit: 2.9.0
  • okhttp: 4.9.0
  • coil: 1.2.0
  • room: 2.2.5

项目创建以及配置

下面我们就开始进入正题,首先我们使用Android studio(后面简写为AS),创建一个以kotlin为开发语言的工程项目KotlinCoroutineDemo,然后我们先在project的build.gradle中引用下面配置

1
kotlin复制代码  classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.32"

然后在app的build.gradle中引用相关配置

1
2
3
4
5
6
7
kotlin复制代码    // Kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.4.32"
// 协程核心库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3"
// 协程Android支持库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3"
}

现在我们就可以愉快的开始我们下一步演示开发了。记得搬好小板凳带上瓜子花生用心看

Kotlin协程的基础介绍

为了方便我们在此文章把kotlin协程简称为协程(Coroutine)。

协程是什么

很多人听到协程的时候第一想法就是协程是什么,笔者在这里也不会去给它下定义,推荐您移步带kotlin官方网站去找相关解释。但是我们这里引用官方的一段原话:

  • 协程通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、订阅相关事件、在不同线程(甚至不同机器!)上调度执行,而代码则保持如同顺序执行一样简单。
  • 协程是一种并发设计模式,您可以在Android平台上使用它来简化异步执行的代码

简单的概括就是我们可以,以同步的方式去编写异步执行的代码。协程是依赖于线程,但是协程挂起时不需要阻塞线程,几乎是无代价的。所以协程像是一种用户态的线程,非常轻量级,一个线程中可以创建N个协程。协程的创建是过CoroutineScope创建,协程的启动方式有三种:

  1. runBlocking:T 启动一个新的协程并阻塞调用它的线程,直到里面的代码执行完毕,返回值是泛型T,就是你协程体中最后一行是什么类型,最终返回的是什么类型T就是什么类型。
  2. launch:Job 启动一个协程但不会阻塞调用线程,必须要在协程作用域(CoroutineScope)中才能调用,返回值是一个Job。
  3. async:Deferred<T> 启动一个协程但不会阻塞调用线程,必须要在协程作用域(CoroutineScope)中才能调用。以Deferred对象的形式返回协程任务。返回值泛型T同runBlocking类似都是协程体最后一行的类型。

等等,好像哪里不对,奇怪的知识点突然有点增多啊image.png。上面提到协程体中最后一行是什么类型,最终返回的是什么类型T就是什么类型,好像跟我们想的不一样,返回值不应该是用return吗,学过kotlin的会知道,在的kotlin高阶函数中,lambda表达式如果你没有显式返回一个值,那它将隐式返回最后一个表达式的值。

那Job、Deferred和协程作用域又是些啥玩意image.png 。不急,慢慢来,我们一个一个的来解释清楚。

什么是Job 、Deferred 、协程作用域

Job我们可以认为他就是一个协程作业是通过CoroutineScope.launch生成的,同时它运行一个指定的代码块,并在该代码块完成时完成。我们可以通过isActive、isCompleted、isCancelled来获取到Job的当前状态。Job的状态如下图所示,摘自官方文档:

协程的生命周期

State [isActive] [isCompleted] [isCancelled]
New (optional initial state) false false false
Active (default initial state) true false false
Completing (transient state) true false false
Cancelling (transient state) false false true
Cancelled (final state) false true true
Completed (final state) false true false

我们可以通过下图可以大概了解下一个协程作业从创建到完成或者取消,Job在这里不扩展了,后面我们会在实际使用过程中去讲解。

1
2
3
4
5
6
7
8
9
10
11
sql复制代码                                      wait children
+-----+ start +--------+ complete +-------------+ finish +-----------+
| New | -----> | Active | ---------> | Completing | -------> | Completed |
+-----+ +--------+ +-------------+ +-----------+
| cancel / fail |
| +----------------+
| |
V V
+------------+ finish +-----------+
| Cancelling | --------------------------------> | Cancelled |
+------------+ +-----------+

Deferred

Deferred继承自Job,我们可以把它看做一个带有返回值的Job,

1
2
3
4
5
6
7
kotlin复制代码public interface Deferred<out T> : Job {
//返回结果值,或者如果延迟被取消,则抛出相应的异常
public suspend fun await(): T
public val onAwait: SelectClause1<T>
public fun getCompleted(): T
public fun getCompletionExceptionOrNull(): Throwable?
}

我们需要重点关注await()方法,可以看到await()方法返回结果是T,说明我们可以通过await()方法获取执行流的返回值,当然如果出现异常或者被取消执行,则会抛出相对应的异常。

什么是作用域

协程作用域(Coroutine Scope)是协程运行的作用范围。launch、async都是CoroutineScope的扩展函数,CoroutineScope定义了新启动的协程作用范围,同时会继承了他的coroutineContext自动传播其所有的 elements和取消操作。换句话说,如果这个作用域销毁了,那么里面的协程也随之失效。就好比变量的作用域,如下面test方法中的money变量

1
2
3
4
5
js复制代码private fun test(){ // scope start
val money = 100;
println(money)
} // scope end
// println(money)

此时money是不能被调用,因为AS会提示 Unresolved reference: money。协程作用域也是这样一个作用,可以用来确保里面的协程都有一个作用域的限制。我们开发过程中最常见的场景就内存泄露,协程同样存在这样的问题,后面我们再细细讲解协程作用域CoroutineScope的相关知识,这里只是作为基础点讲解,不继续往下延伸。

Kotlin协程的基础用法

现在我们开始使用协程,首先我们在MainActivity的xml布局中新建一个Button按钮然后设置好点击事件,然后创建一个start()方法,通过Button的点击事件执行。现在我们开始在start方法中使用协程。

刚才我们上面提到启动协程有三种方式,接下来我们先看看如何通过runBlocking、launch和async启动协程,我们直接在start方法中使用,但是由于我们的launch和async启动,只能在协程的作用域下启动,那我们又该怎么办呢?

运行第一个协程

在Android中有一个名为GlobalScope全局顶级协程,这个协程是在整个应用程序生命周期内运行的。我们就以此协程来使用launch和async启动,代码如下:

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
kotlin复制代码import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Button
import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.Group
import androidx.viewpager.widget.ViewPager
import kotlinx.coroutines.*
import java.lang.NullPointerException

class MainActivity : AppCompatActivity() {
private lateinit var btn:Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
btn = findViewById(R.id.btn)
btn.setOnClickListener {
start()
}
}

private fun start(){
runBlocking {
Log.d("runBlocking", "启动一个协程")
}
GlobalScope.launch{
Log.d("launch", "启动一个协程")
}
GlobalScope.async{
Log.d("async", "启动一个协程")
}
}
}

然后运行app,点击按钮执行start()方法。我们就可以在控制台上看到如下输出:

1
2
3
kotlin复制代码D/runBlocking: 启动一个协程
D/launch: 启动一个协程
D/async: 启动一个协程

哇image.png,so easy。协程原来这么简单,那我们接着继续往下走。上面提到过三种启动方式分别会的得到各自的返回信息。我们现在增加三个变量然后分别用协程进行赋值,同时进行输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kotlin复制代码private fun start(){
val runBlockingJob = runBlocking {
Log.d("runBlocking", "启动一个协程")
41
}
Log.d("runBlockingJob", "$runBlockingJob")
val launchJob = GlobalScope.launch{
Log.d("launch", "启动一个协程")
}
Log.d("launchJob", "$launchJob")
val asyncJob = GlobalScope.async{
Log.d("async", "启动一个协程")
"我是返回值"
}
Log.d("asyncJob", "$asyncJob")
}

然后运行,我们可以在控制台上看到如下输出:

1
2
3
4
5
6
kotlin复制代码D/runBlocking: 启动一个协程
D/runBlockingJob: 41
D/launchJob: StandaloneCoroutine{Active}@3b8b871
D/launch: 启动一个协程
D/async: 启动一个协程
D/asyncJob: DeferredCoroutine{Active}@63f2656

也有可能是

1
2
3
4
5
6
kotlin复制代码D/runBlocking: 启动一个协程
D/runBlockingJob: 41
D/launchJob: StandaloneCoroutine{Active}@1344515
D/asyncJob: DeferredCoroutine{Active}@38c002a
D/async: 启动一个协程
D/launch: 启动一个协程

还有可能是

1
2
3
4
5
6
kotlin复制代码D/runBlocking: 启动一个协程
D/runBlockingJob: 41
D/launch: 启动一个协程
D/launchJob: StandaloneCoroutine{Active}@b94e973
D/async: 启动一个协程
D/ asyncJob: DeferredCoroutine{Active}@f7aa030

嗯哼,什么情况image.png)怎么后面4条日志顺序还是随机的。没有看懂的童鞋,说明你没有仔细看上面的文字image.png。知识点来了,赶紧拿出你的小本本记下来,我们一个一个的来分析。

我们在上面提到过runBlocking启动的是一个新的协程并阻塞调用它的线程,我们对比输出日志可以看到前两条runBlocking的相关输出日志的位置顺序是不会变化的,这就证明我们之前所说的runBlocking会阻塞调用它的线程,直到runBlocking运行结束才继续往下执行。

接下来我们再继续往下看,我们看到后面四条日志是无序的,但是launchJob始终在asyncJob前面。而launch和async协程体内的日志输出是无序的。每执行一次看到的顺序都有可能跟之前的不一样。我们前面提到过launch和async都是启动一个协程但不会阻塞调用线程,所以launchJob始终在asyncJob前面(2个协程之间不是很明显,你们自己在尝试的时候,可以同时启动5个甚至更多协程去看日志输出)

而launch和async协程体内的日志是无序的,这是因为协程采用的是并发设计模式,所以launch和async的协程体内的log日志输出是无序方式,这就解释了launch和async都是启动一个协程但不会阻塞调用线程,同时也解释了log日志之间输出顺序之间的关系(这里描述是不严谨的,后面会补充)。

难道就这样结束了吗?那你想的可就太简单了。刚才我们提到协程采用的是并发设计模式,多个协程并发执行的。那如果这个时候,我们把启动协程放在同一协程作用域下启动的是顺序又该是怎么样的呢? 大家可以先思考一下这个问题,回头我们再来看这个问题。

runBlocking的返回值

现在我们回到之前的话题,我们看到输出的日志信息中runBlockingJob的输出结果是41,为什么是这么一个数值,其实他默认返回是一个该协程作业的当前状态

image.png
我们通过runBlocking方法可以看到,其返回值是调用了joinBlocking方法,而在joinBlocking方法中

image.png

我们看到joinBlocking方法返回了一个state强转成泛型T类型。我们现在大概知道runBlocking返回的是个什么东西了。如果在runBlocking协程最后一行增加一个返回值:

1
2
3
4
kotlin复制代码 val runBlockingJob = runBlocking {
Log.d("Coroutine", "runBlocking启动一个协程")
"我是runBlockingJob协程的返回值"
}

我们将会看到如下输出:

1
2
kotlin复制代码D/Coroutine: runBlocking启动一个协程
D/runBlockingJob: 我是runBlockingJob协程的返回值

runBlocking它的设计目的是将常规的阻塞代码连接到一起,主要用于main函数和测试中。根据本文章的目标我们后续将不再往下扩展。

继续往下走,我们看到launchJob输出的是一个StandaloneCoroutine对象,为什么会是一个StandaloneCoroutine对象呢,不是说好的返回一个Job吗?

别慌,稳住!继续往下看

launch函数

image.png

我们看到launch函数中有3个参数context、start和block,同时都带有默认值,虽然我们不知道这三个参数是干什么用的,但是我们可以看名知其意,不妨先大胆的猜测一下,我们这里先跳过,后面再针对这三个参数做一些基本讲解。我们看到launch方法最终返回的是一个coroutine对象,由于我们没有传入值其最后返回的是一个StandaloneCoroutine对象,跟我们输出的日志结果一致。那为什么笔者会说launch返回的是一个Job呢。我们再继续看看StandaloneCoroutine又是一个什么鬼,通过查找继承关系我们可以看到,StandaloneCoroutine就是一个Job,现在就一目了然了。

1
2
3
kotlin复制代码private open class StandaloneCoroutine(...) : AbstractCoroutine<Unit>(parentContext, active){
//此处省略.....
}
1
2
3
kotlin复制代码public abstract class AbstractCoroutine<in T>(...) : JobSupport(active), Job, Continuation<T>, CoroutineScope {
//此处省略.....
}

async函数

同理我们也看看async函数,和launch拥有同样的3个参数context、start和block,默认值都是一样的,最终返回的是也是一个coroutine对象。只是async返回的DeferredCoroutine对象。
image.png

1
2
3
kotlin复制代码private open class DeferredCoroutine<T>(...) : AbstractCoroutine<T>(parentContext, active), Deferred<T>, SelectClause1<T> {
//此处省略.....
}

同样的都是继承 AbstractCoroutine<Unit>类,但是DeferredCoroutine同时也继承Deferred<T>接口。这么看来DeferredCoroutine就是一个Deferred<T>,一个携带有返回值Job。那么问题来了,我们要怎么获取到这个Deferred<T>携带的返回值T呢。

我们在一开始的时候提到需要重点关注Deferred的await()方法,我们可以通过返回Deferred对象,调用await()方法来获取返回值,我们看到await()前面有个suspend关键字,这又是个额啥玩意。

1
kotlin复制代码  public suspend fun await(): T

挂起函数

suspend是协程的关键字,表示这个一个挂起函数,每一个被suspend饰的方法只能在suspend方法或者在协程中调用。现在我们修改之前的代码,同时多增加几条输出日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kotlin复制代码private fun start(){
GlobalScope.launch{
val launchJob = launch{
Log.d("launch", "启动一个协程")
}
Log.d("launchJob", "$launchJob")
val asyncJob = async{
Log.d("async", "启动一个协程")
"我是async返回值"
}
Log.d("asyncJob.await", ":${asyncJob.await()}")
Log.d("asyncJob", "$asyncJob")
}
}

现在我们通过GlobalScope.launch启动里一个协程,同时在协程体里面通过launch直接又启动了2个协程。为什么我们没有在协程体使用GlobalScope.launch启动,而是使用launch直接启动。前面我们提到过调用launch必须要在协程作用域(Coroutine Scope)中才能调用,因为通过runBlocking、launch和async启动的协程体等同于协程作用域,所以这里我们就可以直接使用launch启动一个协程。我们运行一下,接着看看日志输出:

1
2
3
4
5
kotlin复制代码D/launchJob: StandaloneCoroutine{Active}@f3d8da3
D/launch: 启动一个协程
D/async: 启动一个协程
D/await: :我是async返回值
D/asyncJob: DeferredCoroutine{Completed}@d6f28a0

也有可能是这样的

1
2
3
4
5
kotlin复制代码D/launchJob: StandaloneCoroutine{Active}@f3d8da3
D/async: 启动一个协程
D/launch: 启动一个协程
D/asyncJob.await: :我是async返回值
D/asyncJob: DeferredCoroutine{Completed}@d6f28a0

现在我们看到asyncJob.await也是输出我们之前定义好的返回值,同时DeferredCoroutine的状态变成了{Completed},这是因为await()是在不阻塞线程的情况下等待该值的完成并继续执行,当deferred计算完成后返回结果值,或者如果deferred被取消,则抛出相应的异常CancellationException。但是又因为await()是挂起函数,他会挂起调用他的协程。所以我们看到的DeferredCoroutine的状态是{Completed},同时输出的await日志也是在最后面。

好了,至此。我们对runBlocking、launch、async的相关介绍就到这里了。

Android中的协程并发与同步

现在我们回过头来看,我们在上面提到过:”因为协程采用的是并发设计模式,所以导致launch和async的协程体内的log日志输出是无序方式(这样说是不严谨)”。

因为协程是采用就是并发的设计模式,这句话的大多数环境下是没有问题。但是,但是,但是,这里需要注意的小细节来了。如果某个协程满足以下几点,那它里面的子协程将会是同步执行的:

  • 父协程的协程调度器是处于Dispatchers.Main情况下启动。
  • 同时子协程在不修改协程调度器下的情况下启动。
1
2
3
4
5
6
7
8
9
10
kotlin复制代码private fun start() {
GlobalScope.launch(Dispatchers.Main) {
for (index in 1 until 10) {
//同步执行
launch {
Log.d("launch$index", "启动一个协程")
}
}
}
}
1
2
3
4
5
6
7
8
9
kotlin复制代码D/launch1: 启动一个协程
D/launch2: 启动一个协程
D/launch3: 启动一个协程
D/launch4: 启动一个协程
D/launch5: 启动一个协程
D/launch6: 启动一个协程
D/launch7: 启动一个协程
D/launch8: 启动一个协程
D/launch9: 启动一个协程
1
2
3
4
5
6
7
8
9
10
kotlin复制代码private fun start() {
GlobalScope.launch {
for (index in 1 until 10) {
//并发执行
launch {
Log.d("launch$index", "启动一个协程")
}
}
}
}
1
2
3
4
5
6
7
8
9
kotlin复制代码D/launch1: 启动一个协程
D/launch2: 启动一个协程
D/launch3: 启动一个协程
D/launch4: 启动一个协程
D/launch5: 启动一个协程
D/launch6: 启动一个协程
D/launch9: 启动一个协程
D/launch7: 启动一个协程
D/launch8: 启动一个协程

那么子协程将是同步执行的,这是在Android平台上如果协程处于Dispatchers.Main调度器,它会将协程调度到UI事件循环中执行,即通常在主线程上执行,这样就能理解为什么是同步执行了吧。如果是不同步的话,那我在操作UI刷新的时候,就会出现各种问题啦image.png。

如果其中的某一个子协程将他的协程调度器修改为非Dispatchers.Main,那么这个子协程将会与其他子协程并发执行,这里我们就不在演示,各位看官可以自己动手试验一下。毕竟知识光看不动手,是很难将知识吸收到位的image.png。

下一本章节我们将在会对以下知识点做初步讲解,包含上文提到的launch和async函数中的3个参数作用。清单如下:

  1. 协程调度器CoroutineDispatcher
  2. 协程下上文CoroutineContext作用
  3. 协程启动模式CoroutineStart
  4. 协程作用域CoroutineScope
  5. 挂起函数以及suspend关键字的作用

需要源码的看这里:demo源码

原创不易。如果您喜欢这篇文章,您可以动动小手点赞收藏image.png。

微信截图_20211227104733.jpg

Android技术交流群,有兴趣的可以私聊加入

关联文章
Kotlin协程基础及原理系列

  • 史上最详Android版kotlin协程入门进阶实战(一) -> kotlin协程的基础用法
  • 史上最详Android版kotlin协程入门进阶实战(二) -> kotlin协程的关键知识点初步讲解
  • 史上最详Android版kotlin协程入门进阶实战(三) -> kotlin协程的异常处理
  • 史上最详Android版kotlin协程入门进阶实战(四) -> 使用kotlin协程开发Android的应用
  • 史上最详Android版kotlin协程入门进阶实战(五) -> kotlin协程的网络请求封装

Flow系列

  • Kotlin协程之Flow使用(一)
  • Kotlin协程之Flow使用(二)
  • Kotlin协程之Flow使用(三)

扩展系列

  • 封装DataBinding让你少写万行代码
  • ViewModel的日常使用封装

本文转载自: 掘金

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

史上最详Android版kotlin协程入门进阶实战(二)

发表于 2021-04-21

banners_twitter.png

由于文章涉及到的只是点比较多、内容可能过长,可以根据自己的能力水平和熟悉程度分阶段跳着看。如有讲述的不正确的地方劳烦各位私信给笔者,万分感谢image.png

由于时间原因,笔者白天工作只有晚上空闲时间才能写作,所以更新频率应该在一周一篇,当然我也会尽量的利用时间,争取能够提前发布。为了方便阅读将本文章拆分个多个章节,根据自己需要选择对应的章节,现在也只是目前笔者心里的一个大概目录,最终以更新为准:

Kotlin协程基础及原理系列

  • 史上最详Android版kotlin协程入门进阶实战(一) -> kotlin协程的基础用法
  • 史上最详Android版kotlin协程入门进阶实战(二) -> kotlin协程的关键知识点初步讲解
  • 史上最详Android版kotlin协程入门进阶实战(三) -> kotlin协程的异常处理
  • 史上最详Android版kotlin协程入门进阶实战(四) -> 使用kotlin协程开发Android的应用
  • 史上最详Android版kotlin协程入门进阶实战(五) -> kotlin协程的网络请求封装

Flow系列

  • Kotlin协程之Flow使用(一)
  • Kotlin协程之Flow使用(二)
  • Kotlin协程之Flow使用(三)

扩展系列

  • 封装DataBinding让你少写万行代码
  • ViewModel的日常使用封装

kotlin协程的关键知识点

上一本章节末尾我们提到,将在本章节中对以下知识点做初步讲解,包含上文提到的launch和async函数中的3个参数作用。清单如下:

  1. 协程调度器CoroutineDispatcher
  2. 协程下上文CoroutineContext作用
  3. 协程启动模式CoroutineStart
  4. 协程作用域CoroutineScope
  5. 挂起函数以及suspend关键字的作用

当然还有一些其他的知识点也是很重要的,比如:CoroutineExceptionHandler、Continuation、Scheduler、ContinuationInterceptor等。但是确实涉及到的东西比较多,如果都展开的话,可能再写几个篇幅都没有办法讲完。上面这些是笔者认为掌握了这些知识点以后,基本可以开始着手项目实战了。我们后面在实战的过程中,边写边讲解。

协程调度器

上文我们提到一个协程调度器CoroutineDispatcher的概念,调度器又是一个什么神奇的东西。在这里我们对调度器不做过多深入的解释,这可是协程的三大件之一,后面我们会有专门的篇幅做深入讲解。为了方便我们把协程调度器简称为调度器,那接下来我们就看看什么是调度器。偷个懒,引用一下官方的原话:

  • 调度器它确定了相关的协程在哪个线程或哪些线程上执行。协程调度器可以将协程限制在一个特定的线程执行,或将它分派到一个线程池,亦或是让它不受限地运行。

对于调度器的实现机制我们已经非常清楚了,官方框架中预置了4个调度器,我们可以通过Dispatchers对象直接访问它们:

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码public actual object Dispatchers {
@JvmStatic
public actual val Default: CoroutineDispatcher = createDefaultDispatcher()
@JvmStatic
public actual val Main: MainCoroutineDispatcher
get() = MainDispatcherLoader.dispatcher
@JvmStatic
public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined
@JvmStatic
public val IO: CoroutineDispatcher = DefaultScheduler.IO
}
  • Default:默认调度器,CPU密集型任务调度器,适合处理后台计算。通常处理一些单纯的计算任务,或者执行时间较短任务。比如:Json的解析,数据计算等
  • IO:IO调度器,,IO密集型任务调度器,适合执行IO相关操作。比如:网络处理,数据库操作,文件操作等
  • Main:UI调度器, 即在主线程上执行,通常用于UI交互,刷新等
  • Unconfined:非受限调度器,又或者称为“无所谓”调度器,不要求协程执行在特定线程上。

比如上面我们通过launch启动的时候,因为我们没有传入参数,所有实际上它使用的是默认调度器Dispatchers.Default

1
2
3
4
5
6
7
kotlin复制代码GlobalScope.launch{
Log.d("launch", "启动一个协程")
}
//等同于
GlobalScope.launch(Dispatchers.Default){
Log.d("launch", "启动一个协程")
}

Dispatchers.IO和Dispatchers.Main就都很好理解了。这是我们以后在Android开发过程中,打交道最多的2个调度器。比如后台数据上传,我们就可以使用Dispatchers.IO调度器。刷新界面我们就使用Dispatchers.Main调度器。为方便使用官方在Android协程框架库中,已经为我们定义好了几个供我们开发使用,如:MainScope、lifecycleScope、viewModelScope。它们都是使用的Dispatchers.Main,这些后续我们都将会使用到。

根据我们上面使用的方法,我们好像只有在启动协程的时候,才能指定具体使用那个Dispatchers调度器。如果我要是想中途切换线程怎么办,比如:

  • 现在我们需要通过网络请求获取到数据的时候填充到我们的布局当中,但是网络处理在IO线程上,而刷新UI是在主线程上,那我们应该怎么办。

莫慌,莫慌,万事万物总有解决的办法。官方为我们提供了一个withContext顶级函数,使用withContext函数来改变协程的上下文,而仍然驻留在相同的协程中,同时withContext还携带有一个泛型T返回值。

1
2
3
4
5
6
kotlin复制代码public suspend fun <T> withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T {
//......
}

呀,这一看withContext这个东西好像很符合我们的需求嘛,我们可以先使用launch(Dispatchers.Main)启动协程,然后再通过withContext(Dispatchers.IO)调度到IO线程上去做网络请求,把得到的结果返回,这样我们就解决了我们上面的问题了。

1
2
3
4
5
6
7
kotlin复制代码GlobalScope.launch(Dispatchers.Main) {
val result = withContext(Dispatchers.IO) {
//网络请求...
"请求结果"
}
btn.text = result
}

是不是很简单!!! 麻麻再也不会说我的handler满飞了,也不用走那万恶的回调地狱了。我想怎么切就怎么切,想去走个线程就去哪个线程。逻辑都按着顺序一步一步走,而且代码都是这么的丝滑。还要什么自行车,额.错了,还要什么handler,管他回调不回调,哥现在就是这么嚣张image.png。

协程上下文

CoroutineContext即协程上下文。它是一个包含了用户定义的一些各种不同元素的Element对象集合。其中主要元素是Job、协程调度器CoroutineDispatcher、还有包含协程异常CoroutineExceptionHandler、拦截器ContinuationInterceptor、协程名CoroutineName等。这些数据都是和协程密切相关的,每一个Element都一个唯一key。

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
kotlin复制代码public interface CoroutineContext {
public operator fun <E : CoroutineContext.Element> get(key: Key<E>): E?

public fun <R> fold(initial: R, operation: (R, CoroutineContext.Element) -> R): R

public operator fun plus(context: CoroutineContext): CoroutineContext =
if (context === EmptyCoroutineContext) this else context.fold(this) { ...}

public fun minusKey(key: Key<*>): CoroutineContext

//注意这里,这个key很关键
public interface Key <E : CoroutineContext.Element>

public interface Element : CoroutineContext {
public val key: Key<*>

public override operator fun <E : Element> get(key: Key<E>): E? =
if (this.key == key) this as E else null

public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
operation(initial, this)

public override fun minusKey(key: Key<*>): CoroutineContext =
if (this.key == key) EmptyCoroutineContext else this
}
}

我们可以看到Element是CoroutineContext的内部接口,同时它又实现了CoroutineContext接口,这么设计的原因是为了保证Element中一定只能存放的Element它自己,而不能存放其他类型的数据CoroutineContext内还有一个内部接口Key,同时它又是Element的一个属性,这个属性很重要,我们先在这里插个眼,待会再讲解这个属性的作用。

那我们上面提到Job、CoroutineDispatcher、CoroutineExceptionHandler、ContinuationInterceptor、CoroutineName等为什么又可以存放到CoroutineContext中呢。我们接着往下看看它们各自的实现:

Job

1
2
3
4
5
kotlin复制代码public interface Job : CoroutineContext.Element {
public companion object Key : CoroutineContext.Key<Job> {
//省略...
}
}

CoroutineDispatcher

1
2
3
4
5
6
kotlin复制代码public abstract class CoroutineDispatcher :
AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
public companion object Key : AbstractCoroutineContextKey<ContinuationInterceptor, CoroutineDispatcher>(
ContinuationInterceptor,
{ it as? CoroutineDispatcher })
}

CoroutineExceptionHandler

1
2
3
kotlin复制代码public interface CoroutineExceptionHandler : CoroutineContext.Element {
public companion object Key : CoroutineContext.Key<CoroutineExceptionHandler>
}

ContinuationInterceptor

1
2
3
kotlin复制代码public interface ContinuationInterceptor : CoroutineContext.Element {
companion object Key : CoroutineContext.Key<ContinuationInterceptor>
}

CoroutineName

1
2
3
4
5
kotlin复制代码public data class CoroutineName(
val name: String
) : AbstractCoroutineContextElement(CoroutineName) {
public companion object Key : CoroutineContext.Key<CoroutineName>
}

现在要开始要集中注意力了。我们可以看到他们都是实现了Element接口,同时都有个CoroutineContext.Key类型的伴生对象key,这个属性的作用是什么呢。那我们就得回过头来看看CoroutineContext接口的几个方法了。

1
2
3
4
5
6
7
8
kotlin复制代码public operator fun <E : CoroutineContext.Element> get(key: Key<E>): E?

public fun <R> fold(initial: R, operation: (R, CoroutineContext.Element) -> R): R

public operator fun plus(context: CoroutineContext): CoroutineContext =
if (context === EmptyCoroutineContext) this else context.fold(this) { ...}

public fun minusKey(key: Key<*>): CoroutineContext

我们先从plus方法说起,plus有个关键字operator表示这是一个运算符重载的方法,类似List.plus的运算符,可以通过+号来返回一个包含原始集合和第二个操作数中的元素的结果。同理CoroutineContext中是通过plus来返回一个由原始的Element集合和通过+号引入的Element产生新的Element集合。

get方法,顾名思义。可以通过 key 来获取一个Element

fold方法它和集合中的fold是一样的,用来遍历当前协程上下文中的Element集合。

minusKey方法plus作用相反,它相当于是做减法,是用来取出除key以外的当前协程上下文其他Element,返回的就是不包含key的协程上下文。

现在我们就知道为什么我们之前说Element中的key这个属性很重要了吧。因为我们就是通过它从协程上下文中获取我们想要的Element,同时也解释为什么Job、CoroutineDispatcher、CoroutineExceptionHandler、ContinuationInterceptor、CoroutineName等等,这些Element都有需要有一个CoroutineContext.Key类型的伴生对象key。我们写个测试方法:
如:

1
2
3
4
5
6
7
8
js复制代码 private fun testCoroutineContext(){
val coroutineContext1 = Job() + CoroutineName("这是第一个上下文")
Log.d("coroutineContext1", "$coroutineContext1")
val coroutineContext2 = coroutineContext1 + Dispatchers.Default + CoroutineName("这是第二个上下文")
Log.d("coroutineContext2", "$coroutineContext2")
val coroutineContext3 = coroutineContext2 + Dispatchers.Main + CoroutineName("这是第三个上下文")
Log.d("coroutineContext3", "$coroutineContext3")
}
1
2
3
kotlin复制代码D/coroutineContext1: [JobImpl{Active}@21a6a21, CoroutineName(这是第一个上下文)]
D/coroutineContext2: [JobImpl{Active}@21a6a21, CoroutineName(这是第二个上下文), Dispatchers.Default]
D/coroutineContext3: [JobImpl{Active}@21a6a21, CoroutineName(这是第三个上下文), Dispatchers.Main]

我们通过对比日志输出信息可以看到,通过+号我们可以把多个Element整合到一个集合中,同时我们也发现:

  • 三个上下文中的Job是同一个对象。
  • 第二个上下文在第一个的基础上增加了一个新的CoroutineName,新增的CoroutineName替换了第一个上下文中的CoroutineName。
  • 第三个上下文在第二个的基础上又增加了一个新的CoroutineName和Dispatchers,同时他们也替换了第二个上下文中的CoroutineName和Dispatchers。

但是因为这个+运算符是不对称的,所以在我们实际的运用过程中,通过+增加Element的时候一定要注意它们结合的顺序。那么现在关于协程上下文的内容就讲到这里,我们点到为止,后面在深入理解阶段在细讲这些东西运行的原理细节。

协程启动模式

CoroutineStart协程启动模式,是启动协程时需要传入的第二个参数。协程启动有4种:

  • DEFAULT 默认启动模式,我们可以称之为饿汉启动模式,因为协程创建后立即开始调度,虽然是立即调度,单不是立即执行,有可能在执行前被取消。
  • LAZY 懒汉启动模式,启动后并不会有任何调度行为,直到我们需要它执行的时候才会产生调度。也就是说只有我们主动的调用Job的start、join或者await等函数时才会开始调度。
  • ATOMIC 一样也是在协程创建后立即开始调度,但是它和DEFAULT模式有一点不一样,通过ATOMIC模式启动的协程执行到第一个挂起点之前是不响应cancel 取消操作的,ATOMIC一定要涉及到协程挂起后cancel 取消操作的时候才有意义。
  • UNDISPATCHED 协程在这种模式下会直接开始在当前线程下执行,直到运行到第一个挂起点。这听起来有点像 ATOMIC,不同之处在于UNDISPATCHED是不经过任何调度器就开始执行的。当然遇到挂起点之后的执行,将取决于挂起点本身的逻辑和协程上下文中的调度器。

我们可以通过一个小例子的来看看这几个启动模式的实际情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
kotlin复制代码private fun testCoroutineStart(){
val defaultJob = GlobalScope.launch{
Log.d("defaultJob", "CoroutineStart.DEFAULT")
}
defaultJob.cancel()
val lazyJob = GlobalScope.launch(start = CoroutineStart.LAZY){
Log.d("lazyJob", "CoroutineStart.LAZY")
}
val atomicJob = GlobalScope.launch(start = CoroutineStart.ATOMIC){
Log.d("atomicJob", "CoroutineStart.ATOMIC挂起前")
delay(100)
Log.d("atomicJob", "CoroutineStart.ATOMIC挂起后")
}
atomicJob.cancel()
val undispatchedJob = GlobalScope.launch(start = CoroutineStart.UNDISPATCHED){
Log.d("undispatchedJob", "CoroutineStart.UNDISPATCHED挂起前")
delay(100)
Log.d("undispatchedJob", "CoroutineStart.UNDISPATCHED挂起后")
}
undispatchedJob.cancel()
}

每个模式我们分别启动一个一次,DEFAULT模式启动时,我们接着调用了cancel取消协程,ATOMIC模式启动时,我们在里面增加了一个挂起点delay挂起函数,来区分ATOMIC启动时的挂起前后执行情况,同样的UNDISPATCHED模式启动时,我们也调用了cancel取消协程,我们看实际的日志输出情况:

1
2
3
kotlin复制代码D/defaultJob: CoroutineStart.DEFAULT
D/atomicJob: CoroutineStart.ATOMIC挂起前
D/undispatchedJob: CoroutineStart.UNDISPATCHED挂起前

或者

1
2
kotlin复制代码D/undispatchedJob: CoroutineStart.UNDISPATCHED挂起前
D/atomicJob: CoroutineStart.ATOMIC挂起前

为什么会出现2种情况。我们上面提到过DEFAULT模式协程创建后立即开始调度,但不是立即执行,所以它有可能会被cancel取消,导致没有输出defaultJob这条日志。

同样的ATOMIC模式启动的时候也接着调用了cancel取消协程,但是因为没有遇到挂起点,所以挂起前的日志输出了,但是挂起后的日志没有输出。

而UNDISPATCHED模式启动的时候也接着调用了cancel取消协程,同样的因为没有遇到挂起点所以输出了UNDISPATCHED挂起前,但是因为UNDISPATCHED是立即执行的,所以他的日志UNDISPATCHED挂起前输出在ATOMIC挂起前的前面(注意这里是概率事件,主要突出UNDISPATCHED是立即执行)。

接着我们在补充一下关于UNDISPATCHED模式。我们上面有提到当以UNDISPATCHED模式启动时,遇到挂起点之后的执行,将取决于挂起点本身的逻辑和协程上下文中的调度器。这句话我们又要怎么理解呢。我们还是以一个例子来认识解释UNDISPATCHED模式,比如:

1
2
3
4
5
6
7
8
9
10
11
12
kotlin复制代码private fun testUnDispatched(){
GlobalScope.launch(Dispatchers.Main){
val job = launch(Dispatchers.IO) {
Log.d("${Thread.currentThread().name}线程", "-> 挂起前")
delay(100)
Log.d("${Thread.currentThread().name}线程", "-> 挂起后")
}
Log.d("${Thread.currentThread().name}线程", "-> join前")
job.join()
Log.d("${Thread.currentThread().name}线程", "-> join后")
}
}

那我们将会看到如下输出,挂起前后都在一个worker-1线程里面执行:

1
2
3
4
kotlin复制代码D/main线程: -> join前
D/DefaultDispatcher-worker-1线程: -> 挂起前
D/DefaultDispatcher-worker-1线程: -> 挂起后
D/main线程: -> join后

现在我们在稍作修改,我们在子协程launch的时候使用UNDISPATCHED模式启动:

1
2
3
4
5
6
7
8
9
10
11
12
kotlin复制代码 private fun testUnDispatched(){
GlobalScope.launch(Dispatchers.Main){
val job = launch(Dispatchers.IO,start = CoroutineStart.UNDISPATCHED) {
Log.d("${Thread.currentThread().name}线程", "-> 挂起前")
delay(100)
Log.d("${Thread.currentThread().name}线程", "-> 挂起后")
}
Log.d("${Thread.currentThread().name}线程", "-> join前")
job.join()
Log.d("${Thread.currentThread().name}线程", "-> join后")
}
}

那我们将会看到如下输出:

1
2
3
4
kotlin复制代码D/main线程: -> 挂起前
D/main线程: -> join前
D/DefaultDispatcher-worker-1线程: -> 挂起后
D/main线程: -> join后

我们看到当以UNDISPATCHED模式即使我们指定了协程调度器Dispatchers.IO,挂起前还是在main线程里执行,但是挂起后是在worker-1线程里面执行,这是因为当以UNDISPATCHED启动时,协程在这种模式下会直接开始在当前线程下执行,直到第一个挂起点。遇到挂起点之后的执行,将取决于挂起点本身的逻辑和协程上下文中的调度器,即join处恢复执行时,因为所在的协程有调度器,所以后面的执行将会在调度器对应的线程上执行。

我们再改一下,把子协程在launch的时候使用UNDISPATCHED模式启动,去掉Dispatchers.IO调度器,那又会出现什么情况呢

1
2
3
4
5
6
7
8
9
10
11
12
kotlin复制代码 private fun testUnDispatched(){
GlobalScope.launch(Dispatchers.Main){
val job = launch(start = CoroutineStart.UNDISPATCHED) {
Log.d("${Thread.currentThread().name}线程", "-> 挂起前")
delay(100)
Log.d("${Thread.currentThread().name}线程", "-> 挂起后")
}
Log.d("${Thread.currentThread().name}线程", "-> join前")
job.join()
Log.d("${Thread.currentThread().name}线程", "-> join后")
}
}
1
2
3
4
kotlin复制代码D/main线程: -> 挂起前
D/main线程: -> join前
D/main线程: -> 挂起后
D/main线程: -> join后

我们发现它们都在一个线程里面执行了。这是因为当通过UNDISPATCHED启动后遇到挂起,join处恢复执行时,如果所在的协程没有指定调度器,那么就会在join处恢复执行的线程里执行,即挂起后是在父协程(Dispatchers.Main线程里面执行,而最后join后这条日志的输出调度取决于这个最外层的协程的调度规则。

现在我们可以总结一下,当以UNDISPATCHED启动时:

  • 无论我们是否指定协程调度器,挂起前的执行都是在当前线程下执行。
  • 如果所在的协程没有指定调度器,那么就会在join处恢复执行的线程里执行,即我们上述案例中的挂起后的执行是在main线程中执行。
  • 当我们指定了协程调度器时,遇到挂起点之后的执行将取决于挂起点本身的逻辑和协程上下文中的调度器。即join处恢复执行时,因为所在的协程有调度器,所以后面的执行将会在调度器对应的线程上执行。

同样的我们点到为止,关于启动模式的的相关内容我们就现讲到这里。

协程作用域

协程作用域CoroutineScope为协程定义作用范围,每个协程生成器launch、async等都是CoroutineScope的扩展,并继承了它的coroutineContext自动传播其所有Element和取消。协程作用域本质是一个接口,不建议手工实现该接口,而应该首选委托实现。下面我们列出了部分CoroutineScope相关定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kotlin复制代码public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}

public operator fun CoroutineScope.plus(context: CoroutineContext): CoroutineScope =
ContextScope(coroutineContext + context)

public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

public object GlobalScope : CoroutineScope {
override val coroutineContext: CoroutineContext
get() = EmptyCoroutineContext
}

public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
ContextScope(if (context[Job] != null) context else context + Job())

我们可以看到CoroutineScope也重载了plus方法,通过+号来新增或者修改我们CoroutineContext协程上下文中的Element。同时官方也为我们定义好了 MainScope和GlobalScope2个顶级作用域。GlobalScope我们已经很熟了,前面的案例都是通过它来实现的。

MainScope我们可以看到它的上下文是通过SupervisorJob和 Dispatchers.Main组合的,说明它是一个在主线程执行的协程作用域,我们在后续的Android实战开发中,会结合Activity、Fragment,dialog等使用它。这里不再继续往下扩展。

至于SupervisorJob分析它之前,我们得先说一下协程作用域的分类。我们之前提到过父协程和子协程的概念,既然有父协程和子协程,那么必然也有父协程作用域和子父协程作用域。不过我们不是这么称呼,因为他们不仅仅是父与子的概念。协程作用域分为三种:

  • 顶级作用域 –> 没有父协程的协程所在的作用域称之为顶级作用域。
  • 协同作用域 –> 在协程中启动一个协程,新协程为所在协程的子协程。子协程所在的作用域默认为协同作用域。此时子协程抛出未捕获的异常时,会将异常传递给父协程处理,如果父协程被取消,则所有子协程同时也会被取消。
  • 主从作用域 官方称之为监督作用域。与协同作用域一致,区别在于该作用域下的协程取消操作的单向传播性,子协程的异常不会导致其它子协程取消。但是如果父协程被取消,则所有子协程同时也会被取消。

同时补充一点:父协程需要等待所有的子协程执行完毕之后才会进入Completed状态,不管父协程自身的协程体是否已经执行完成。我们在最开始提到协程生命周期的时候就提到过下,现在回过头看是不是感觉很流程变得清晰。

1
2
3
4
5
6
7
8
9
10
11
sql复制代码                                      wait children
+-----+ start +--------+ complete +-------------+ finish +-----------+
| New | -----> | Active | ---------> | Completing | -------> | Completed |
+-----+ +--------+ +-------------+ +-----------+
| cancel / fail |
| +----------------+
| |
V V
+------------+ finish +-----------+
| Cancelling | --------------------------------> | Cancelled |
+------------+ +-----------+

子协程会继承父协程的协程上下文中的Element,如果自身有相同key的成员,则覆盖对应的key,覆盖的效果仅限自身范围内有效。这个就可以用上我们前面学到的协程上下文CoroutineContext的知识,小案例奉上:

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码private fun  testCoroutineScope(){
GlobalScope.launch(Dispatchers.Main){
Log.d("父协程上下文", "$coroutineContext")
launch(CoroutineName("第一个子协程")) {
Log.d("第一个子协程上下文", "$coroutineContext")
}
launch(Dispatchers.Unconfined) {
Log.d("第二个子协程协程上下文", "$coroutineContext")
}
}
}

日志顺序的问题我们前面已经分析过原因,如果还不懂的话,麻烦您回到基础用法里面仔细的再看一遍。

1
2
3
kotlin复制代码D/父协程上下文: [StandaloneCoroutine{Active}@81b6e46, Dispatchers.Main]
D/第二个子协程协程上下文: [StandaloneCoroutine{Active}@f6b7807, Dispatchers.Unconfined]
D/第一个子协程上下文: [CoroutineName(第一个子协程), StandaloneCoroutine{Active}@bbe6d34, Dispatchers.Main]

可以看到第一个子协程的覆盖了父协程的coroutineContext,它继承了父协程的调度器 Dispatchers.Main,同时也新增了一个CoroutineName属性。第二个子协程覆盖了父协程的coroutineContext中的Dispatchers,也就是将父协程的调度器Dispatchers.Main覆盖为Dispatchers.Unconfined,但是他没有继承第一个子协程的CoroutineName,这就是我们说的覆盖的效果仅限自身范围内有效。接下来我们看看上面提到的协同作用域和主从(监督)作用域异常传递和协程取消的问题。

我们上面提到协同作用域如果子协程抛出未捕获的异常时,会将异常传递给父协程处理,如果父协程被取消,则所有子协程同时也会被取消。先上代码看看效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
kotlin复制代码private fun  testCoroutineScope2() {
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
Log.d("exceptionHandler", "${coroutineContext[CoroutineName]} $throwable")
}
GlobalScope.launch(Dispatchers.Main + CoroutineName("scope1") + exceptionHandler) {
Log.d("scope", "--------- 1")
launch(CoroutineName("scope2") + exceptionHandler) {
Log.d("scope", "--------- 2")
throw NullPointerException("空指针")
Log.d("scope", "--------- 3")
}
val scope3 = launch(CoroutineName("scope3") + exceptionHandler) {
Log.d("scope", "--------- 4")
delay(2000)
Log.d("scope", "--------- 5")
}
scope3.join()
Log.d("scope", "--------- 6")
}
}
1
2
3
kotlin复制代码D/scope: --------- 1
D/scope: --------- 2
D/exceptionHandler: CoroutineName(scope1) java.lang.NullPointerException: 空指针

可以看到子协程scope2抛出了一个异常,将异常传递给父协程scope1处理,但是因为任何一个子协程异常退出会导致整体都将退出。所以导致父协程scope1未执行完成成就被取消,同时还未执行完子协程scope3也被取消了。

主从(监督)作用域与协同作用域一致,区别在于该作用域下的协程取消操作的单向传播性,子协程的异常不会导致其它子协程取消。分析主从(监督)作用域的时候,我们需要用到supervisorScope或者SupervisorJob,如下代码块:

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复制代码private fun testCoroutineScope3() {
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
Log.d("exceptionHandler", "${coroutineContext[CoroutineName]} $throwable")
}
GlobalScope.launch(Dispatchers.Main + CoroutineName("scope1") + exceptionHandler) {
supervisorScope {
Log.d("scope", "--------- 1")
launch(CoroutineName("scope2")) {
Log.d("scope", "--------- 2")
throw NullPointerException("空指针")
Log.d("scope", "--------- 3")
val scope3 = launch(CoroutineName("scope3")) {
Log.d("scope", "--------- 4")
delay(2000)
Log.d("scope", "--------- 5")
}
scope3.join()
}
val scope4 = launch(CoroutineName("scope4")) {
Log.d("scope", "--------- 6")
delay(2000)
Log.d("scope", "--------- 7")
}
scope4.join()
Log.d("scope", "--------- 8")
}
}
}
1
2
3
4
5
6
kotlin复制代码D/scope: --------- 1
D/scope: --------- 2
D/exceptionHandler: CoroutineName(scope2) java.lang.NullPointerException: 空指针
D/scope: --------- 6
D/scope: --------- 7
D/scope: --------- 8

可以看到子协程scope2抛出了一个异常,并将异常传递给父协程scope1处理,同时也结束了自己本身。因为在于主从(监督)作用域下的协程取消操作是单向传播性,因此协程scope2的异常并没有导致父协程退出,所以6 7 8都照常输出,而3 4 5因为在协程scope2里面所以没有输出。

我们刚刚使用了supervisorScope实现了主从(监督)作用域,那我们通过SupervisorJob又该如何实现呢。我们把supervisorScope称之为主从(监督)作用域,那么SupervisorJob就可以称之为主从(监督)作业,如下:

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
kotlin复制代码private fun testCoroutineScope4() {
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
Log.d("exceptionHandler", "${coroutineContext[CoroutineName]} $throwable")
}
val coroutineScope = CoroutineScope(SupervisorJob() +CoroutineName("coroutineScope"))
GlobalScope.launch(Dispatchers.Main + CoroutineName("scope1") + exceptionHandler) {
with(coroutineScope){
val scope2 = launch(CoroutineName("scope2") + exceptionHandler) {
Log.d("scope", "1--------- ${coroutineContext[CoroutineName]}")
throw NullPointerException("空指针")
}
val scope3 = launch(CoroutineName("scope3") + exceptionHandler) {
scope2.join()
Log.d("scope", "2--------- ${coroutineContext[CoroutineName]}")
delay(2000)
Log.d("scope", "3--------- ${coroutineContext[CoroutineName]}")
}
scope2.join()
Log.d("scope", "4--------- ${coroutineContext[CoroutineName]}")
coroutineScope.cancel()
scope3.join()
Log.d("scope", "5--------- ${coroutineContext[CoroutineName]}")
}
Log.d("scope", "6--------- ${coroutineContext[CoroutineName]}")
}
}
1
2
3
4
5
6
kotlin复制代码D/scope: 1--------- CoroutineName(scope2)
D/exceptionHandler: CoroutineName(scope2) java.lang.NullPointerException: 空指针
D/scope: 2--------- CoroutineName(scope3)
D/scope: 4--------- CoroutineName(coroutineScope)
D/scope: 5--------- CoroutineName(coroutineScope)
D/scope: 6--------- CoroutineName(scope1)

是不是感觉和supervisorScope的用法很像,我们通过创建了一个SupervisorJob的主从(监督)协程作用域,调用了子协程的join是为了保证它一定是会执行。同样的子协程scope2抛出了一个异常,通过协程scope2自己内部消化了,同时也结束了自己本身。

因为协程scope2的异常并没有导致coroutineScope作用域下的协程取消退出,所以协程scope3照常运行输出2,后又因为调用了我们定义的协程作用域coroutineScope的cancel方法取消了协程,所以即使我们后面调用了协程scope3的join,也没有输出3,因为SupervisorJob的取消是向下传播的,所以后面的4 5都是在coroutineScope的作用域中输出的。

现在我们关于协程作用域CoroutineScope的作用我们已经有了一个大概的了解,同样的因为这个篇幅中我们是基础讲解,所以我们点到为止,如果还想深入了解,那就只能看后面的深入协程篇幅。

挂起函数

通过前面的篇幅我们已经知道,使用suspend关键字修饰的函数叫作挂起函数,挂起函数只能在协程体内,或着在其他挂起函数内调用。那挂起又是啥玩意呢image.png

我估计各位看到这里的时候,可能有些人已经被上面的知识点弄的有点晕乎,别急,先放松下大脑,喝杯水,然后做个眼保健操缓解一下image.png。下面开始敲黑板了,打起精神,要开始划重点了。

首先一个挂起函数既然要挂起,那么他必定得有一个挂起点,不然我们怎么知道函数是否挂起,从哪挂起呢。
我们定义一个空实现的suspend方法,然后通过AS的工具栏中Tools->kotlin->show kotlin ByteCode解析成字节码

1
2
kotlin复制代码private suspend fun test(){
}
1
kotlin复制代码final synthetic test(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
1
2
3
4
kotlin复制代码public interface Continuation<in T> {
public val context: CoroutineContext
public fun resumeWith(result: Result<T>)
}

我们看到test方法需要的是一个Continuation接口,官方给的介绍是用于挂起点之后,返回类型为T的值用的。那我们又是怎么拿到的这个Continuation呢。要解开这个问题我们得先回到协程的创建和运行是的过程。

我们启动一个协程无非是通过launch,async等方法。我们之前有说到过他们的启动模式CoroutineStart,但是并没有深入的去分析它的创建和启动过程,我们这里先回过头大概的看一下:

1
2
3
4
5
6
7
8
9
10
11
12
kotlin复制代码public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}

我们看到在通过launch启动一个协程的时候,他通过coroutine的start方法启动协程,然后我们接着往下看

1
2
3
4
5
6
7
8
9
kotlin复制代码public fun start(start: CoroutineStart, block: suspend () -> T) {
initParentJob()
start(block, this)
}

public fun <R> start(start: CoroutineStart, receiver: R, block: suspend R.() -> T) {
initParentJob()
start(block, receiver, this)
}

然后start方法里面调用了CoroutineStart的invoke,这个时候我们发现了Continuation。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
kotlin复制代码public operator fun <T> invoke(block: suspend () -> T, completion: Continuation<T>): Unit =
when (this) {
DEFAULT -> block.startCoroutineCancellable(completion)
ATOMIC -> block.startCoroutine(completion)
UNDISPATCHED -> block.startCoroutineUndispatched(completion)
LAZY -> Unit // will start lazily
}

public operator fun <R, T> invoke(block: suspend R.() -> T, receiver: R, completion: Continuation<T>): Unit =
when (this) {
DEFAULT -> block.startCoroutineCancellable(receiver, completion)
ATOMIC -> block.startCoroutine(receiver, completion)
UNDISPATCHED -> block.startCoroutineUndispatched(receiver, completion)
LAZY -> Unit // will start lazily
}

而Continuation又是通过start方法传进来的coroutine。所以现在可以确定,我们的协程体本身就是一个Continuation,这也就解释了为什么可以在协程体内调用suspend挂起函数了。

现在我们也可以确定,在协程内部挂起函数的调用处就是挂起点,如果挂起点出现异步调用,那么当前协程就被挂起,直到对应的Continuation通过调用resumeWith函数才会恢复协程的执行,同时返回Result<T>类型的成功或者失败的结果。

由于章节主题的限制,这里我们就不再下深入了。需要注意的是挂起函数不一定真的会挂起,如果只是提供了挂起的条件,但是协程没有产生异步调用,那么协程还是不会被挂起。

预告:下一篇我们将会讲解kotlin协程中的异常处理,其实我们在这篇章节中已经,提到了一些异常处理,没有注意的同学可以回到协程作用域看看。

需要源码的看这里:demo源码

原创不易。如果您喜欢这篇文章,您可以动动小手点赞收藏image.png。

Android技术交流群,有兴趣的可以私聊加入

关联文章
Kotlin协程基础及原理系列

  • 史上最详Android版kotlin协程入门进阶实战(一) -> kotlin协程的基础用法
  • 史上最详Android版kotlin协程入门进阶实战(二) -> kotlin协程的关键知识点初步讲解
  • 史上最详Android版kotlin协程入门进阶实战(三) -> kotlin协程的异常处理
  • 史上最详Android版kotlin协程入门进阶实战(四) -> 使用kotlin协程开发Android的应用
  • 史上最详Android版kotlin协程入门进阶实战(五) -> kotlin协程的网络请求封装

Flow系列

  • Kotlin协程之Flow使用(一)
  • Kotlin协程之Flow使用(二)
  • Kotlin协程之Flow使用(三)

扩展系列

  • 封装DataBinding让你少写万行代码
  • ViewModel的日常使用封装

本文转载自: 掘金

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

Spring Cloud Alibaba(三):工程搭建

发表于 2021-04-20

学习Spring Cloud Alibaba,从工程搭建开始!

Spring Boot

1
2
3
4
5
6
xml复制代码<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>xxx</version>
<relativePath/>
</parent>

版本定义

1
2
3
4
5
6
7
8
xml复制代码<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-cloud.version>xxx</spring-cloud.version>
<spring-cloud-alibaba.version>xxx</spring-cloud-alibaba.version>
<seata.version>xxx</seata.version>
</properties>

版本,请参考 《Spring Cloud Alibaba(二):版本对应关系》

依赖管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
xml复制代码<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

必要依赖

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
xml复制代码<!-- actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<!-- web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

<!-- test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>

build

1
2
3
4
5
6
7
8
xml复制代码<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

组件依赖

Nacos服务注册

1
2
3
4
5
xml复制代码<!-- nacos 服务注册 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

Nacos服务配置

1
2
3
4
5
xml复制代码<!-- nacos 服务配置 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

Sentinel

1
2
3
4
5
xml复制代码<!-- sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

Dubbo

1
2
3
4
5
xml复制代码<!-- dubbo -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-dubbo</artifactId>
</dependency>

Seata

1
2
3
4
5
xml复制代码<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>

Demo

  • Gitee:gitee.com/fengwenyi/s…
  • Github:github.com/fengwenyi/s…

本文转载自: 掘金

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

SpringBoot集成Jwt(详细步骤+图解) Sprin

发表于 2021-04-20

SpringBoot集成Jwt(详细步骤+图解)

Jwt简介

JSON Web Token是目前最流行的跨域认证解决方案,,适合前后端分离项目通过Restful API进行数据交互时进行身份认证

Jwt构成(.隔开)
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MTkxNjQ4NjEsInVzZXJuYW1lIjoiYWRtaW4ifQ.fo5a-H_C7XG3fSnNdCEMzM2QmrF5c7yypzoSxGzgJOo

Header(头部):放有签名算法和令牌类型

Payload(负载):你在令牌上附带的信息:比如用户的姓名,这样以后验证了令牌之后就可以直接从这里获取信息而不用再查数据库了

Signature(签名):对前两部分的签名,防止数据篡改

交互流程

image.png

本文将通过几个简单步骤教大家如何集成Jwt,废话不多说,直接上步骤。(注:使用了Lombok,需下载好相关插件及依赖)

  • 在项目的pom文件中加入Jwt依赖
1
2
3
4
5
6
java复制代码        <!---jwt-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
  • 在项目的yml文件中配置账号密码
1
2
3
java复制代码Login:
username: admin
password: admin
  • 创建用户实体类
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复制代码import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

/**
* @author admin
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {

@NotNull(message = "id不能为空")
private Integer id;

@NotBlank(message="姓名不能为空")
@Length(min = 2, max = 4, message = "name 姓名长度必须在 {min} - {max} 之间")
private String username;

@NotBlank(message="密码不能为空")
@Length(min = 5, max = 10, message = "password 密码长度必须在 {min} - {max} 之间")
private String password;

}
  • 创建Jwt(生成/验证)工具类
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
java复制代码import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.apache.commons.lang.StringUtils;

import java.util.Calendar;

/**
* @author admin
*/
public class JWTUtils {

/**
* 获取token
* @param u user
* @return token
*/
public static String getToken(User u) {
Calendar instance = Calendar.getInstance();
//默认令牌过期时间7天
instance.add(Calendar.DATE, 7);

JWTCreator.Builder builder = JWT.create();
builder.withClaim("userId", u.getId())
.withClaim("username", u.getUsername());

return builder.withExpiresAt(instance.getTime())
.sign(Algorithm.HMAC256(u.getPassword()));
}

/**
* 验证token合法性 成功返回token
*/
public static DecodedJWT verify(String token) throws MyException {
if(StringUtils.isEmpty(token)){
throw new MyException("token不能为空");
}

//获取登录用户真正的密码假如数据库查出来的是123456
String password = "admin";
JWTVerifier build = JWT.require(Algorithm.HMAC256(password)).build();
return build.verify(token);
}

/* public static void main(String[] args) {
DecodedJWT verify = verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MTcxMDg1MDAsInVzZXJuYW1lIjoiYWRtaW4ifQ.geBEtpluViRUg66_P7ZisN3I_d4e32Wms8mFoBYM5f0");
System.out.println(verify.getClaim("password").asString());
}*/
}
  • 创建Jwt拦截器(拦截所有请求验证token)
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
java复制代码import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.chentawen.springbootall.util.JWTUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* @author admin
*/
@Slf4j
public class JWTInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String token = request.getHeader("token");
if(StringUtils.isEmpty(token)){
throw new MyException("token不能为空");
}
try {
JWTUtils.verify(token);
} catch (SignatureVerificationException e) {
log.error("无效签名! 错误 ->", e);
return false;
} catch (TokenExpiredException e) {
log.error("token过期! 错误 ->", e);
return false;
} catch (AlgorithmMismatchException e) {
log.error("token算法不一致! 错误 ->", e);
return false;
} catch (Exception e) {
log.error("token无效! 错误 ->", e);
return false;
}
return true;
}
}
  • 将拦截器注入到SpirngMVC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

/**
* @author admin
*/
@Configuration
public class IntercaptorConfig implements WebMvcConfigurer {

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JWTInterceptor())
//拦截的路径
.addPathPatterns("/**")
//排除登录接口
.excludePathPatterns("/user/login");
}
  • 创建Controller进行测试
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
java复制代码import org.springframework.beans.factory.annotation.Value;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;

/**
* @author admin
*/
@RestController
@RequestMapping("user")
public class UserLoginController {

@Value("${Login.username}")
private String realUsername;

@Value("${Login.password}")
private String realPassword;

@GetMapping("login")
public String login(String username, String password) {
if (username.equals(realUsername) && password.equals(realPassword)) {
User u = new User();
u.setPassword(password);
u.setUsername(username);
return JWTUtils.getToken(u);
}
return "登录失败!账号或者密码不对!";
}

@GetMapping("test")
public String test() {
return "访问test - API";
}
}
  • 使用Postman进行测试
  • 登录接口-返回Jwt*
    image.png

测试接口-返回无权限
image.png

测试接口-请求头带token,访问成功
image.png

有什么疑问可评论区提问! 求点赞!求转发!求收藏!

本文转载自: 掘金

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

MySQL数据库缓冲池——Buffer Pool

发表于 2021-04-20

注意:

  • 本文对Buffer Pool的分析建立在InnoDB存储引擎的基础上。
  • 文中的“缓存池”、“缓冲池”表达的是一个意思,即Buffer Pool。

查看/设置参数命令:

1
2
3
4
5
6
mysql复制代码# 查看InnoDB的相关参数
show engine innodb status;
# 查看缓冲池大小
select @@innodb_buffer_pool_size;
# 查看数据库中缓冲池实例的数量
select @@innodb_buffer_pool_instances;

1. Buffer Pool与内存的关系?

Buffer Pool是InnoDB引擎中的一块内存区域,它的默认大小是128M。当MySQL启动的时候就会初始化Buffer Pool,它会根据系统中所设置的innodb_buffer_pool_size大小去内存中申请一块连续的内存空间。缓存池中有多个缓存页。实际申请的内存区域比配置的值稍微大一点,因为缓冲池中的每一个缓存页都附带了一个描述数据块,后面会讲到。

2. 缓存页和数据页的关系

数据页

MySQL在执行增删改操作时首先会定位到这条数据所在的数据页,如果数据页在缓冲池中,那么直接操作并返回结果;如果不在缓存池中,会将这个数据页加载到buffer pool中,然后进行相应的操作。MySQL中一个数据页的大小为16KB。

缓存页

Buffer Pool中有一个缓存页的概念与数据页的概念相对应,大小也是16KB,还会为每个缓存页分配一些额外的空间,用来存放之前提到的描述数据块,用来描述数据页所属的表空间、数据页号等,占缓存页的15%左右,大概是800B。

3. Buffer Pool的结构

MySQL使用chunk机制来设计缓冲池,它将Buffer Pool拆分成多个大小相等的chunk块,每个chunk默认大小为128M。每个chunk中有多个缓存页和对应的描述数据块。除此之外,Buffer Pool还包括三个链表:Free链表、Flush链表、LRU链表,这三个链表是整个Buffer Pool共享的,即多个chunk间共享的。

chunk机制的好处

需要动态调整Buffer Pool大小的时候,可以直接申请新的chunk块,避免了重新申请一块大的连续空间。

4. Buffer Pool的并发性能

Buffer Pool一次只允许一个线程访问,MySQL为了保证数据的一致性,操作的时候必须对缓存池加互斥锁。为了提高并发度,在MySQL中可以设置多个Buffer Pool。

5. Free链表

之前说到每个数据页都会被加载到一个缓存页中,但是加载的时候MySQL怎么判断哪些缓存页是空闲的,哪些已经被占用了呢?

Free链表就是用来解决这个问题的。Free链表是一个双向链表,它的作用是保存空闲缓存页的描述数据块。另外,Free链表还有一个基础节点,它指向Free链表的头节点和尾节点,并保存当前链表节点的个数,即空闲缓存页的个数。

当加载数据页到缓冲池时,MySQL会从Free链表中获取一个描述数据块的信息,根据该信息找到对应的缓存页,然后把数据页加载到该缓存页中,同时将链表中该描述数据块的节点移除。

6. 数据页缓存哈希表

从上一节知识中我们知道了如何判断缓存页是否空闲,由此引发了另一个问题:如何判断即将要加载的数据页是否已经存在于缓冲池中了呢?

MySQL中还有一个哈希表结构,它的作用是用来存储表空间号+数据页号,作为数据页的key,缓存页对应的地址作为value,这样在加载数据页之前就可以通过哈希表中的key来判断数据页是否已经存在于缓存中了。

1
复制代码表空间号 + 数据页号 = 缓存页地址

7. Flush链表

由于MySQL的写操作是在内存中完成的,在写操作完成后,会产生在内存和磁盘中不一致的脏数据,所以会有一个I/O线程在随机的时间将脏数据刷新到磁盘中。

那么这个线程怎么知道哪些是脏数据,这些脏数据又该刷新到磁盘的哪个数据页中呢?

Flush链表就是用来解决这个问题的,它的作用就是记录被修改过的数据页所在缓存页的描述数据。Flush链表也有一个基础节点,作用域Free链表中的基础节点类似。当某个脏缓存页被刷新到磁盘后,Flush链表会移除这个脏数据对应的节点,这个节点会被增加到Free链表中。

8. LRU链表

我们之前谈到了使用Free链表来维护空闲的缓存页信息,那么当Free链表的节点被使用完毕时候,此时需要加载新的数据页到缓冲区,该怎么处理呢?也就是说,Buffer Pool的内存空间不够用了。

这就需要使用到内存淘汰策略了,MySQL使用的是LRU(最近最少使用算法)链表来实现淘汰策略的。

LRU——最近最少使用算法

该算法的思想是:最近使用的数据页会在未来一段时间内仍然被使用,已经很久没有使用的页面很有可能在未来较长的一段时间内仍然不会被使用。

它的主要衡量指标是使用的时间,附加指标是使用的次数。

MySQL中LRU的实现不同于传统的LRU实现。它引入了新生代和老年代、冷数据停留时间窗口的概念。

首先介绍几个概念:预读、预读失效、缓冲池污染

预读

从磁盘中加载数据到内存时,并不是需要哪一行的数据就读取哪一行,而是以页为单位进行读取的。如果之后要读取的数据就在页中,就能省去后续的磁盘IO,提高效率。

预读的理论根据是“局部性原理”:数据访问通常都遵循“集中读写”的原则,使用一些数据时,大概率也会使用附近的数据。它表明提前加载是有效的,确实能减少磁盘IO。

使用传统的LRU算法,很可能会出现预读失效和缓冲池污染的情况。

预读失效

由于预读把数据页加载到了缓冲池,但最终这个数据页中的数据并没有被读取(除了触发把数据页加载到缓冲池的那一次操作之外),这就是预读失效。

缓冲池污染

当某一个SQL语句,要批量扫描大量数据时,可能导致把缓冲池中的所有页都替换出去,导致大量热数据被置换出去,MySQL性能急剧下降,这就是缓冲池污染。

9. MySQL中LRU算法的实现

为了解决以上问题,MySQL对传统的LRU算法进行了改进。

首先,将LRU链表(为了减少数据的移动,通常使用链表实现)分为两部分:

  • 新生代
  • 老年代
    懒了,直接放个链接,下次再来总结:

参考链接

www.cnblogs.com/wyq178/p/99…

www.cnblogs.com/ScarecrowAn…

www.jianshu.com/p/f9ab1cb24…

本文转载自: 掘金

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

程序员架构修炼:架构设计概要,业务、应用、技术、数据架构 架

发表于 2021-04-20

架构设计

在架构设计过程中,我们会根据需要做出不同的架构设计,而在设计时需要涉及一定的架构设计核心要素。

架构设计概要

架构设计是从业务需求到系统实现的一个转换,是对需求进一步深入分析的过程,用于确定系统中实体与实体的关系,以及实体的形式与功能。架构可根据从业务需求到系统实现的不同需要分为:业务架构、应用架构、数据架构、技术架构。下面以电商系统为例进行架构设计。

业务架构

业务架构是对业务需求的提炼和抽象,使用一套方法论对产品(项目)所涉及需求的业务进行业务边界划分,简单地讲就是根据一套逻辑思路进行业务的拆分,开发软件必须满足业务需求,否则就是空中楼阁。软件系统在业务上的复杂度问题,可以从业务架构的角度切分工作交界面来解决。比如在做一个电商网站时,需要将商品类目、商品、订单、支付、退款等功能很清晰地划分出来,不要在业务架构中考虑用什么技术开发、并发量是否太大、选择什么样的硬件,等等。

这里简单规划了电商系统的业务模块,对其根据业务架构的模块不断细化,一直分解到代码流程。对于软件开发而言,以业务架构为依托,架构师和开发者能比较清晰地看到系统的业务全貌,架构师能更方便地分析系统复杂度,分解业务逻辑,做好开发的分工,如图5.1所示。

图片

图5.1

业务架构是决定一个软件项目能否顺利开展的总纲,软件架构是业务架构在技术层面的映射,合理的开发分工也应该基于业务架构去做。如果没有业务架构,进行软件开发就会很盲目。业务架构是需求和开发的汇聚点,需求分析是否做到位,功能开发是否达到预期目标,都以此为依托。我们在工作中会遇到一些问题,例如研发人员说需求分析做得不到位,而做需求的人员会质疑需求做到怎样才算到位,为什么开发出的产品和用户想要的不一致,这些从根上来说,都是因为没有将业务架构梳理清楚,没有达成共识。

站在软件项目的角度来看,在项目前期做好业务架构设计,对整个项目的开发都有重要的意义。例如,对于比较类似的业务系统,可能业务架构在比较粗的颗粒度上是一样的,而在细化过程中不一样。

在做项目时,当手头有一个现成的系统,需要做一个需求类似的项目时,大家可能会首先尝试用现成的系统去覆盖新项目,以求利益最大化。对于该想法能否实现,可以通过业务架构来衡量,如果没有业务架构,则接下来的工作会非常盲目。业务架构的设计原则如下。

(1)将业务平台化。

◎ 业务平台化,相互独立,例如交易平台、物流平台、支付平台、广告平台等。

◎ 基础业务下沉,可复用,例如用户、商品、类目、促销、时效等。(2)将核心业务和非核心业务分离。将电商系统的核心业务和非核心业务如主交易服务和通用交易服务分离,将核心业务精简(利于稳定),并将非核心业务多样化。

(3)隔离不同类型的业务。

◎ 交易平台的作用是让买家和卖家签订交易合同,所以需要优先保证高可用,让用户能快速下单。

◎ 履约业务对可用性没有太高要求,但要优先保证一致性。

◎ 秒杀业务对高并发要求很高,应该和常规业务分离。

(4)区分主流程和辅助流程。要清楚哪些是电商系统的主流程,在运行时优先保证主流程的顺利完成;对辅助流程可以采用后台异步的方式,避免辅助流程的失败影响主流程的失败回流。

应用架构

应用架构介于业务与技术之间,是对整个系统实现的总体架构,需要指出系统的层次、系统开发的原则、系统各个层次的应用服务。

如图5.2所示为将系统分为表现层、业务流程层、服务层、服务构建层、数据层、集成层、数据架构层和服务治理层,并写明每个层次的应用提供的服务。

在进行系统拆分时,要平衡业务和技术的复杂度,保证系统形散神不散。系统采用什么样的应用架构,则受到业务复杂度的影响,包括企业的发展阶段和业务特点;同时受技术复杂度的影响,包括 IT技术的发展阶段和内部技术人员的水平。业务的复杂度(包括业务量大)必然带来技术的复杂度,应用架构的目标是在解决业务复杂度的同时避免技术太复杂,确保业务架构落地。

图片

图5.2

应用架构的设计原则如下。

(1)稳定

◎ 一切以稳定为中心。

◎ 架构尽可能简单、清晰,追求小而美,不要大而全。

◎ 不过度设计。

(2)解耦

◎ 将稳定部分与易变部分分离。

◎ 将核心业务与非核心业务分离。

◎ 将电商主流程和辅助流程分离。

◎ 将应用与数据分离。

◎ 将服务和实现细节分离。(3)抽象

◎ 应用抽象化:应用只依赖服务抽象,不依赖服务实现的细节和位置。

◎ 数据库抽象化:应用只依赖逻辑数据库,不需要关心物理库的位置和分片。

◎ 服务抽象化:应用虚拟化部署,不需要关心实体机的配置,动态调配资源。

(4)松耦合

◎ 跨域调用异步化:在不同的业务域之间尽量异步解耦。

◎ 非核心业务尽量异步化:在核心业务和非核心业务之间尽量异步化。

◎ 在必须同步调用时,需要设置超时时间和任务队列的长度。

(5)容错设计

◎ 服务自治:服务能彼此独立修改、部署、发布和管理,避免引发连锁反应。

◎ 集群容错:应用系统集群部署,避免单点服务。

◎ 多机房容灾:多机房部署、多活。

技术架构

技术架构就是对在业务架构中提出的功能(或服务)进行技术方案的实现,包括软件系统实现、操作系统选择和运行时设计。技术架构的边界比较模糊,对于不同的受众,内容的详细程度也不同,技术栈自上而下比较关注技术架构,但是各层关注的点不同。技术决策层可能关心的是系统或系统群的技术选型,对整体的把握要保证不因为选型引起其他风险,例如,如果在高性能存储方面选择 Redis,就要尽量保证网络的封闭性,避免公网访问;再如,在选择以COBOL语言实现的各类产品时,要考虑市场上开发人员数量少,需要承担更高的迭代成本等。

上述业务架构的一个简单技术架构如图5.3所示。

图片

图5.3

技术架构的设计原则如下。

(1)无状态,即尽量不要把状态数据保存在本机上。

(2)可复用。

◎ 复用粒度是有业务逻辑的抽象服务,不是服务的实现细节。

◎ 服务引用只依赖服务抽象。

(3)松耦合

◎ 跨业务域调用,尽可能异步解耦。◎ 在同步调用时设置超时时间和队列大小。

◎ 将相对稳定的基础服务与易变流程服务分离。

(4)可治理

◎ 服务可降级。

◎ 服务可限流。

◎ 服务可开关。

◎ 服务可监控。

◎ 白名单机制。

◎ 制定服务契约。

(5)基础服务

◎ 基础服务下沉、可复用,例如时效、库存和价格计算。

◎ 基础服务自治、相对独立。

◎ 对基础服务的实现要精简,并可水平扩展。

◎ 对基础服务的实现要进行物理隔离,包括基础服务相关的数据。

数据架构

数据架构是对存储数据(资源)的架构,其设计原则和应用架构

设计大同小异,在设计时需要考虑系统的业务场景,需要根据不同的业务场景对数据进行异构设计、数据库读写分离、分布式数据存储策略等。如图5.4所示是电商系统中数据架构的一个概要。

图片

图5.4

数据架构包括两部分内容:静态部分的内容和动态部分的内容。

静态部分的内容的重点是数据元模型、数据模型,包括主数据、共享动态数据和所有业务相关的业务对象数据的分析和建模;动态部分的内容的重点则是对数据全生命周期的管控和治理。因此,不能单纯地将数据架构理解为纯静态的数据模型。业务架构中数据模型的分析重点是主数据和核心业务对象,应用架构中数据模型的分析重点则进一步转换为逻辑模型和物理模型,直到最终的数据存储和分布。

数据分两个层面的生命周期:单业务对象数据的全生命周期,它往往和流程建模中的单个工作流或审批流相关;跨多个业务域对象数据的全生命周期,它体现的是多个业务对象数据之间的转换和映射,

往往和端到端的业务流程 BPM 相关。这里要注意,数据虽然是静态层面的内容,但是数据的生命周期或端到端的数据映射往往间接反映了流程,这是很重要的内容。

数据建模的方法包括面向结构的传统ER模型分析方法,也包括面向对象的对象类模型分析方法,它们都是可行的数据建模方法,只是传统ER模型分析方法更容易实现向底层物理数据库模型的转换,而面向对象的对象类建模方法更容易体现抽象和复用。特别是在企业架构建模中,面向对象和面向结构往往不是严格区分的,很多时候都会出现两种方法混用的情况,但重点是区分每种方法或工具的重点及要解决的问题。与数据相关的矩阵分析相当多,业务架构阶段的重点矩阵分析是业务对象和业务流程、业务组件、业务功能间的类CRUD矩阵分析;而应用架构阶段的重点矩阵分析是逻辑或物理模型对象和具体的应用模块或应用功能间的矩阵分析。两者的思路基本类似,只是关注的层面不同,前者重点关注主数据的识别和业务组件的分析,而后者重点关注应用功能模块的划分和模块间集成接口的初步分析。

根据前面的思路,我们仍然应该将数据集成分析分解为两个层面的内容:业务层面的分析,以及应用和 IT 实现层面的分析。前者的重点是理清业务流程或业务域之间的业务对象集成和交互,而后者的重点是如何更好地共享数据或如何通过类似的 BI 工具或大数据平台实现数据的集成和交互。

数据架构的设计原则如下。

(1)统一数据视图,即保证数据的及时性、一致性、准确性和完整性。

(2)数据和应用分离。

◎ 应用系统只依赖逻辑数据库。

◎ 应用系统不直接访问其他应用的数据库,只能通过接口访问。

(3)数据异构,即在源数据和目标数据内容相同时做索引异构,在商品库不同维度的内容不同时(如订单数据中的买家库和卖家库)做数据库异构。

(4)数据库读写分离。

◎ 将访问量大的数据库做读写分离,例如订单库。

◎ 将数据量大的数据库做分库分表。

◎ 将不同业务域的数据库做分区隔离。◎ 对重要的数据配置备库。

(5)采用关系数据库。除成本因素外,MySQL 的数据库扩展性和高并发支持能力较强,也比较容易招聘到相关的研发人员和运维人员。

(6)合理利用 NoSQL 数据库。在数据库有能力支撑时,尽量不要引入缓存。另外,要合理利用缓存做容灾。

图片

本文转载自: 掘金

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

1…682683684…956

开发者博客

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