一.软件测试工具有哪些
测试管理工具:可以帮助完成测试计划、跟踪测试运行结果等的工具。这类工具还包括有助于需求、设计、编码测试及缺陷跟踪的工具;
静态分析工具:分析代码而不执行代码。这种工具检测某些缺陷比用其它方法更有效,开销也更小。这种工具一般可以度量代码的各种指标,如McCabe测定复杂度,Logiscope度量代码和规范的复合度等等;
覆盖率工具:这种工具评估通过一系列测试后,软件被执行的程度。这种工具大量的被应用于单元测试中,如PureCoverage、TrueCoverage、Logiscope等;
动态分析工具:这种工具评估正在运行的系统。例如,检查系统运行过程中的内存使用情况,是否有内存越界、内存泄露等等,这类工具有Purify、BoundChecker等;
测试执行工具:这类工具可使测试能够自动化进行,并且各个层次(单元测试、集成测试、系统测试)的执行工具都有。例如系统测试阶段有功能测试自动化工具,如Robot、Winrunner、SilkTest等;还有性能测试工具,如Loadrunner、SilKPerformer等。
白盒测试工具主要有:
(1)内存资源泄漏检查:Numega中的bouncechecker,Rational的Purify
(2)代码覆盖率检查:Numega中的truecoverage,Rational的Purecoverage,Telelogic公司的logiscope,Macabe公司的Macabe
(3)代码性能检查:Numega中的truetime,Rational的Quantify
(4)代码静态度量分析质量检查工具:logiscope和Macabe
黑盒测试工具主要有:
(1)客户端功能测试:MI公司的winrunner,compuware的qarun,Rational的robot
(2)服务器端压力性能测试:MI公司的winload,compuware的qaload,Rational的SQAload等等
(3)Web测试工具:MI公司的Astra系列,rsw公司的e-testsuite
(4)测试管理工具:rational的testmanager,compuware的qadirector等
(5)缺陷跟踪工具:trackrecord,Testtrack
单元测试工具:
(1)测试框架:delphidunit
(2)java junit
(3)c++ cppunit
(4)VisualBasicVBUnit
(5)(.NETplatform)NUnit
二.几种测试工具使用介绍
(1)sourcemonitor安装及使用
1.介绍
SourceMonitor是一款免费的软件,运行在Windows平台下。它可对多种语言写就的代码进行度量,包括C、C++、C#、Java、VB、Delphi和HTML,并且针对不同的语言,输出不同的代码度量值。
像其他代码度量工具一样,SourceMonitor只关注代码,并为编码人员提供及时的反馈,它不是1款项目管理工具,不关注项目实行中从功能分析到设计编码,再到测试这全部进程。
SourceMonitor只是一个“度量”工具,但是通过基本的度量,可以从代码表面层次发现1些根本的,基础的问题,所以将其作为介绍的第一个工具,也应当做为最基础的一个工具来为软件质量把关。
2.C语言度量值(C Metrics)
说到SourceMonitor的度量,对不同的语言提供不同方面的度量,这里仅针对C++进行介绍,其提供了以下几方面的度量。(关于其他语言,请参考SourceMonitor的帮助文档Explanation of Language Metrics章节)
总行数(Lines)
包括空行在内的代码行数。
语句数(Statements)
在C++中,语句是以分号结尾的。分支语句if,循环语句for、while,跳转语句goto都被计算在内,预处理语句#include、#define和#undef也被计算在内,对其他的预处理语句则不作计算,在#else和#endif、#elif和#endif之间的语句将被疏忽。
分支语句比例(Percent Branch Statements)
该值表示分支语句占语句数目的比例,这里的“分支语句”指的是使程序不顺序履行的语句,包括if、else、for、while、break、continue、goto、switch、case、default和return。需要注意的是,do不被计算在内,由于其对应的while已计算了。另外,异常处理的catch也被作为1个分支计算。
注释比例(Percent Lines with Comments)
该值唆使注释行(包括/*……*/和//……情势的注释)占总行数的比例。1般公司会对每一个文档的header或footer部份进行特殊的声明注释,可以再工程属性中设置过滤,不计算在内。
类个数(Classes)
包括class,struct和template在内的个数。
平均每一个类方法数(Methods per Class)
平均每一个类的方法数,即包括内联和非内联的,template函数在内的类方法数除以所有类的个数。
函数个数(Functions)
所有函数的个数。
平均每一个函数包括的语句数目(Average Statements per Method)
总的函数语句数目除以函数数目得到该值。
函数圈复杂度(Function Complexity)
圈复杂度唆使1个函数可履行路径的数目,以下语句为圈复杂度的值贡献1:if/else/for/while语句,3元运算符语句,if/for/while判断条件中的"&&"或“||”,switch语句,后接break/goto/ return/throw/continue语句的case语句,catch/except语句等。对应有最大圈复杂度(Max Complexity)和平均圈复杂度(Avg Complexity)。
函数深度(Block Depth)
函数深度唆使函数中分支嵌套的层数。对应有最大深度(Max Depth)和平均深度(Avg Depth)。
3.安装
关于SourceMonitor的安装,我们可以在其官方网站:http://www.campwoodsw.com/ 上下载这个软件最新版,安装过程很通俗易懂,按照步骤一步一步安装就行。
4.使用
打开软件后点击File->New Project来创建一个新的项目,之后需要经过如下几个步骤:
1.程序语言选择,这里我们选择Java语言;
2.命名当前项目并选择保存路径;
3.选择该项目要度量解析的文件,可以通过XML配置文件导入,也可通过选择项目目录通过扩展名自动筛选文件;
4.选择项目配置,这里可以根据自己的需要去选择这三项,我们选择不修改直接下一步;
5.选择项目保存的格式,这里我们选择New SourceMonitor project format;
6.创建项目的第一个检查点并命名,如果涉及到UTF-8格式编码的,可以选择下面的选项窗口;
7.最后再确认以上所选择的所有信息,如有错误点击上一步退回重新选择,无误则单机完成;
8.完成后确认要度量的文件列表;
9.这样项目就成功建立了,并且完成了第一个度量点的建立;
10.双击我们刚才创立的度量点,我们可以看到项目中包含的各个Java文件的度量值;
11.双击文件我们可以看到对这个文件进行分析的详细内容;
这样,SourceMonitor的安装和使用方法就简单的介绍完了。
下面再简要提一下在Eclipse中集成SourceMonitor的方法:
点击Run->External Tools->External Tools Configurations...
点击左上角的添加图标或者在Program处右击选择New
填写Name、Location、Arguments如下如所示,之后点击Apply
此时再单击该窗口中的Run(以后可以点击Run->External Tools->SourceMoniter)即可运行处当前Eclipse中项目的SourceMonitor结果了
(2)Junit
1.junit是什么
JJUnit是用于编写和运行可重复的自动化测试的开源测试框架,这样可以保证我们的代码按预期工作。JUnit可广泛用于工业和作为支架(从命令行)或IDE(如Eclipse)内单独的Java程序。
JUnit提供:
- 断言测试预期结果。
- 测试功能共享通用的测试数据。
- 测试套件轻松地组织和运行测试。
- 图形和文本测试运行。
JUnit用于测试:
- 整个对象
- 对象的一部分 - 交互的方法或一些方法
- 几个对象之间的互动(交互)
注意:Junit 测试也是程序员测试,即所谓的白盒测试,它需要程序员知道被测试的代码如何完成功能,以及完成什么样的功能
2.特点
· JUnit是用于编写和运行测试的开源框架。
· 提供了注释,以确定测试方法。
· 提供断言测试预期结果。
· 提供了测试运行的运行测试。
· JUnit测试让您可以更快地编写代码,提高质量
· JUnit是优雅简洁。它是不那么复杂以及不需要花费太多的时间。
· JUnit测试可以自动运行,检查自己的结果,并提供即时反馈。没有必要通过测试结果报告来手动梳理。
· JUnit测试可以组织成测试套件包含测试案例,甚至其他测试套件。
· Junit显示测试进度的,如果测试是没有问题条形是绿色的,测试失败则会变成红色。
3.Eclipse JUnit简单示例
先创建一个工程,名称为:CalculateTest,并在这个工程上点击右键,选择:Build Path -> Add Library -> JUnit ...,如下图所示:
选择 JUnit 的库版本为:JUnit 4,如下图所示:
整个工程的结构如下:
首先,我们将介绍一个测试类:
Calculate.java
package com.yiibai.junit;
public class Calculate {
public int sum(int var1, int var2) {
System.out.println("相加的值是: " + var1 + " + " + var2);
return var1 + var2;
}
}
在上面的代码中,我们可以看到,类有一个公共的方法sum(), 它得到输入两个整数,将它们相加并返回结果。在这里,我们将测试这个方法。为了这个目的,我们将创建另一个类包括方法,将测试之前的类(在此情况下,我们只有一个方法进行测试)中每一个的方法。这是使用的最常见的方式。当然,如果一个方法非常复杂且要扩展,我们可以在一个以上的试验方法来测试这一复杂方法。创建测试用例的详细信息将显示在下面的部分。下面,有一个类是:CalculateTest.java,它具有我们的测试类的角色的代码:
CalculateTest.java
package com.yiibai.junit;
import static org.junit.Assert.*;
import org.junit.Test;
public class CalculateTest {
Calculate calculation = new Calculate();
int sum = calculation.sum(2, 5);
int testSum = 7;
@Test
public void testSum() {
System.out.println("@Test sum(): " + sum + " = " + testSum);
assertEquals(sum, testSum);
}
}
让我们来解释一下上面的代码。首先,我们可以看到,有一个@Test的注解在 testSum()方法的上方。 这个注释指示该公共无效(public void)方法它所附着可以作为一个测试用例。因此,testSum()方法将用于测试公开方法 sum() 。 我们还可以观察一个方法 assertEquals(sum, testsum)。 assertEquals ([String message], object expected, object actual) 方法持有两个对象作为输入,并断言这两个对象相等。
如果要运行测试类,右键点击测试类,并选择 Run As -> Junit Test, 该程序的输出将类似于如下:
相加的值是: 2 + 5
@Test sum(): 7 = 7
若要查看JUnit测试的实际结果,Eclipse IDE提供了JUnit的窗口,它显示了测试的结果。 在这种情况下测试成功,JUnit 窗口不显示任何错误或失败,因为我们可以在下面的图片中看到(Errors 0):
现在,如果我们改变这一行的代码:
int testSum = 10;
使整数待测试不相等,则输出将是:
相加的值是: 2 + 5
@Test sum(): 7 = 10
在JUnit窗口,有一个错误将出现,并且会显示这样的信息:
java.lang.AssertionError: expected: but was:
at com.yiibai.junit.CalculateTest.testSum(CalculateTest.java:16)
4.JUnit注解
注解 | 描述 |
@Test | 测试注释指示该公共无效方法它所附着可以作为一个测试用例。 |
@Before | Before注释表示,该方法必须在类中的每个测试之前执行,以便执行测试某些必要的先决条件。 |
@BeforeClass | BeforeClass注释指出这是附着在静态方法必须执行一次并在类的所有测试之前。发生这种情况时一般是测试计算共享配置方法(如连接到数据库)。 |
@After | After 注释指示,该方法在执行每项测试后执行(如执行每一个测试后重置某些变量,删除临时变量等) |
@AfterClass | 当需要执行所有的测试在JUnit测试用例类后执行,AfterClass注解可以使用以清理建立方法,(从数据库如断开连接)。注意:附有此批注(类似于BeforeClass)的方法必须定义为静态。 |
@Ignore | 当想暂时禁用特定的测试执行可以使用忽略注释。每个被注解为@Ignore的方法将不被执行。 |
让我们看看一个测试类,在上面提到的一些注解的一个例子。
AnnotationsTest.java
package com.yiibai.junit;
import static org.junit.Assert.*;
import java.util.*;
import org.junit.*;
public class AnnotationsTest {
private ArrayList testList;
@BeforeClass
public static void onceExecutedBeforeAll() {
System.out.println("@BeforeClass: onceExecutedBeforeAll");
}
@Before
public void executedBeforeEach() {
testList = new ArrayList();
System.out.println("@Before: executedBeforeEach");
}
@AfterClass
public static void onceExecutedAfterAll() {
System.out.println("@AfterClass: onceExecutedAfterAll");
}
@After
public void executedAfterEach() {
testList.clear();
System.out.println("@After: executedAfterEach");
}
@Test
public void EmptyCollection() {
assertTrue(testList.isEmpty());
System.out.println("@Test: EmptyArrayList");
}
@Test
public void OneItemCollection() {
testList.add("oneItem");
assertEquals(1, testList.size());
System.out.println("@Test: OneItemArrayList");
}
@Ignore
public void executionIgnored() {
System.out.println("@Ignore: This execution is ignored");
}
}
如果我们运行上面的测试,控制台输出将是以下几点:
@BeforeClass: onceExecutedBeforeAll
@Before: executedBeforeEach
@Test: EmptyArrayList
@After: executedAfterEach
@Before: executedBeforeEach
@Test: OneItemArrayList
@After: executedAfterEach
@AfterClass: onceExecutedAfterAll
5.JUnit断言
断言 | 描述 |
void assertEquals([String message], expected value, actual value) | 断言两个值相等。值可能是类型有 int, short, long, byte, char or java.lang.Object. 第一个参数是一个可选的字符串消息 |
void assertTrue([String message], boolean condition) | 断言一个条件为真 |
void assertFalse([String message],boolean condition) | 断言一个条件为假 |
void assertNotNull([String message], java.lang.Object object) | 断言一个对象不为空(null) |
void assertNull([String message], java.lang.Object object) | 断言一个对象为空(null) |
void assertSame([String message], java.lang.Object expected, java.lang.Object actual) | 断言,两个对象引用相同的对象 |
void assertNotSame([String message], java.lang.Object unexpected, java.lang.Object actual) | 断言,两个对象不是引用同一个对象 |
void assertArrayEquals([String message], expectedArray, resultArray) | 断言预期数组和结果数组相等。数组的类型可能是 int, long, short, char, byte or java.lang.Object. |
让我们看的一些前述断言的一个例子。
AssertionsTest.java
package com.yiibai.junit;
import static org.junit.Assert.*;
import org.junit.Test;
public class AssertionsTest {
@Test
public void test() {
String obj1 = "junit";
String obj2 = "junit";
String obj3 = "test";
String obj4 = "test";
String obj5 = null;
int var1 = 1;
int var2 = 2;
int[] arithmetic1 = { 1, 2, 3 };
int[] arithmetic2 = { 1, 2, 3 };
assertEquals(obj1, obj2);
assertSame(obj3, obj4);
assertNotSame(obj2, obj4);
assertNotNull(obj1);
assertNull(obj5);
assertTrue(var1 var2);
assertArrayEquals(arithmetic1, arithmetic2);
}
}
在以上类中我们可以看到,这些断言方法是可以工作的。
- assertEquals() 如果比较的两个对象是相等的,此方法将正常返回;否则失败显示在JUnit的窗口测试将中止。
- assertSame() 和 assertNotSame() 方法测试两个对象引用指向完全相同的对象。
- assertNull() 和 assertNotNull() 方法测试一个变量是否为空或不为空(null)。
- assertTrue() 和 assertFalse() 方法测试if条件或变量是true还是false。
- assertArrayEquals() 将比较两个数组,如果它们相等,则该方法将继续进行不会发出错误。否则失败将显示在JUnit窗口和中止测试
6.使用Eclipse的JUnit实例
1. 初始步骤
让我们创建一个名为 JUnitGuide 的Java项目. 在 src 文件夹, 我们用鼠标右键单击并选择 New -> Package, 创造一个新的包名为: com.yiibai.junit 这里我们将定位类用于测试。 对于测试类,一个很好的做法就是创建专用于测试的一个新的源文件夹,这样的类用于测试以及测试类将在不同的源的文件夹。 为此,右键单击您的项目,选择 New -> Source Folder, 命名新的源文件夹test 并点击 Finish.
提示:
或者,可以通过右键点击项目创建一个新的源文件夹,选择Properties -> Java Build Path, 选择标签 Source, 选择 Add Folder -> Create New Folder, 写上名称为test,然后按 Finish。
可以很容易地看到,在项目中有两个源文件夹:
还可以创建一个新的包在新创建的测试文件夹,名称为 com.javacodegeeks.junit, 测试类不会在默认包中,我们已经准备好了,现在就开始吧!
2. 创建Java类用于测试
右键单击src文件夹并创建一个新的Java类称为 FirstDayAtSchool.java. 这个类的公共方法将被测试。
FirstDayAtSchool.java
package com.yiibai.junit;
import java.util.Arrays;
public class FirstDayAtSchool {
public String[] prepareMyBag() {
String[] schoolbag = { "Books", "Notebooks", "Pens" };
System.out.println("My school bag contains: "
+ Arrays.toString(schoolbag));
return schoolbag;
}
public String[] addPencils() {
String[] schoolbag = { "Books", "Notebooks", "Pens", "Pencils" };
System.out.println("Now my school bag contains: "
+ Arrays.toString(schoolbag));
return schoolbag;
}
}
3. 创建和运行JUnit测试案例
要为现有类 FirstDayAtSchool.java 创建一个JUnit测试案例, 在Package Explorer视图上右键单击并选择 New → JUnit Test Case. 更改源文件夹,这样的类将位于 test 源文件夹并确保该标志 New JUnit4 测试选择。
然后点击 Finish. 如果您的项目不包含JUnit库在classpath中,下面的消息会显示成为将JUnit库添加到类路径:
下面,有一个名为:FirstDayAtSchoolTest.java 类,这个测试类的代码如下所示:
FirstDayAtSchool.java
package com.yiibai.junit;
import static org.junit.Assert.*;
import org.junit.Test;
public class FirstDayAtSchoolTest {
FirstDayAtSchool school = new FirstDayAtSchool();
String[] bag1 = { "Books", "Notebooks", "Pens" };
String[] bag2 = { "Books", "Notebooks", "Pens", "Pencils" };
@Test
public void testPrepareMyBag() {
System.out.println("Inside testPrepareMyBag()");
assertArrayEquals(bag1, school.prepareMyBag());
}
@Test
public void testAddPencils() {
System.out.println("Inside testAddPencils()");
assertArrayEquals(bag2, school.addPencils());
}
}
现在,我们可以通过右击运行测试测试类用例,选择 Run As -> JUnit Test.
程序将输出类似如下所示:
Inside testPrepareMyBag()
My school bag contains: [Books, Notebooks, Pens]
Inside testAddPencils()
Now my school bag contains: [Books, Notebooks, Pens, Pencils]
在JUnit视图将没有失败或错误。如果改变其中一个数组,以便它包含超过预期的元素:
String[] bag2 = { "Books", "Notebooks", "Pens", "Pencils", "Rulers"};
我们再次运行测试类,JUnit视图将包含一个错误:
否则,如果我们再次更改其中一个数组,让它包含一个不同的元素:
String[] bag1 = { "Books", "Notebooks", "Rulers" };
我们再次运行测试类,JUnit视图将再一次失败:
7.编写测试类的原则
①测试方法上必须使用@Test进行修饰
②测试方法必须使用public void 进行修饰,不能带任何的参数
③新建一个源代码目录来存放我们的测试代码,即将测试代码和项目业务代码分开
④测试类所在的包名应该和被测试类所在的包名保持一致
⑤测试单元中的每个方法必须可以独立测试,测试方法间不能有任何的依赖
⑥测试类使用Test作为类名的后缀(不是必须)
⑦测试方法使用test作为方法名的前缀(不是必须)
(2)安卓性能测试工具--emmagee
1.简介
Emmagee是监控指定被测应用在使用过程中占用机器的CPU、内存、流量资源的性能测试小工具。
该工具的优势在于如同windows系统性能监视器类似,它提供的是数据采集的功能,而行为则基于用户真实的应用操作
支持SDK:Android2.2以及以上版本
2.为什么使用Emmagee?
1、开源
2、使用方便,无需root权限
3、可以监控单个应用性能
4、浮窗显示实时展示数据
5、CSV格式保存性能数据,方便转换为其它格式
6、用户自定义采集性能数据频率
7、支持2.2以及以上版本(7.0及以上不支持)
3.Emmagee详细功能介绍
1、检测当前时间被测应用占用的CPU使用率以及总体CPU使用量
2、检测当前时间被测应用占用的内存量,以及占用的总体内存百分比,剩余内存量
3、检测应用从启动开始到当前时间消耗的流量数
4、测试数据写入到CSV文件中,同时存储在手机中
5、可以选择开启浮窗功能,浮窗中实时显示被测应用占用性能数据信息
6、在浮窗中可以快速启动或者关闭手机的wifi网络
4.Emmagee如何使用?
1、安装Emmagee应用
apk下载地址:https://github.com/NetEase/Emmagee/releases
2、启动Emmagee,右上角设置采集频率,列表中会默认加载手机安装的所有应用
3、选择需要测试的应用,点击“开始测试”,被测应用会被启动
4、开始功能测试,测试过程中会自动记录相关性能参数
5、测试完成后回到Emmagee界面,点击“结束测试”,测试结果会保存在手机指定目录的CSV文件中(因为我的手机安卓版本在7.0以上,借了室友的手机测试)
生成的CSV文件内容见图:
可以选择发送到邮箱
6.将csv数据拷贝到excel中(或另存为excel文件)生成图表,使用自带的统计图标功能生成统计图,即可清晰看到整个操作过程中cpu、内存等关键数据的变化。
(4)安卓性能测试工具--GT
1.什么是GT?
GT(随身调)安卓/IOS手机端调测组件,用于APP的性能测试、竞品测试及仅凭一台手机即可进行App测试。 利用GT,仅凭一部手机,无需连接电脑,您即可对APP进行快速的性能测试(CPU、内存、流量、电量、帧率/流畅度等)、 开发日志的查看、Crash日志查看、网络数据包的抓取、APP内部参数的调试、真机代码耗时统计等等;更重要的是,您可以在任意真实场所、 任何时候做如上的系列事情,这就是“APP的场测”。如果您觉得GT提供的功能还不够满足您的需要, 您还可以利用GT提供的基础API自行开发有特殊功能的GT插件(目前,仅iOS版支持), 帮助您解决更加复杂的APP调试、测试问题。
2.使用
1.打开GT,允许访问权限
进入工具AUT页面,勾选指标,点击“启动”按钮
2.设置参数,点击右上角的“编辑”按钮,然后选中想测试的参数将其拖拽到已关注区域
a.点击“完成”按钮,勾选已关注的参数,点击右上角的红点即可开始监控
b.点击删除按钮会删除所选参数记录的数据
c.点击保存按钮会保存记录数据到手机本地GT/GW/<AUT名>/GW_DATA目录下,后期使用USB连接电脑,借助pc端的应用宝便可将数据一键导出到电脑上,用来分析数据
d.点击某个参数可查看详情
3.耗时:需借助GT的sdk使用,暂未深入研究
4.日志:抓取产品在运行过程中日志,方便监控crash log
日志的展示,一条日志三段组成,第一段是时间,第二段是日志级别(V,D,I,W,E)、 tag、线程号,第三段是日志消息。
保存:保存到本地方便随时完整查看
搜索:可快速定位日志内容
3、插件的使用
注:GT自带多款插件,这扩展了性能测试指标范围
A.耗电数据采集插件:
1.设置采样间隔,单位为毫秒,一般范围为100-1000ms
2.勾选耗电量相关指标,电流、电压、电量、温度
3.点击指标可查看详情
B.抓包插件:
注:因为该插件需要获取手机Root权限,没有深入研究
C.内存填充插件:
注:可手动输入分配内存数,单位为兆(一般要求小于1100),点击填充后,会为GT进程在Native量分配的内存,GT所占用的内存数在pss数据指标里可以看到, 这里强调一下,填充的内存是分配到GT工具下的,不是被测应用;我通过使用GT检测GT本身的内存变化给大家看一下,截图如下。
(Dalvik内存使用情况,即Java堆消耗的内存量,Native内存,即JVM外部进程使用的内存量)
1.填充前,去参数列表查看GT的PSS0数值大约16018KB,如下;
2.现在我们通过插件去手动为GT进程添加内存500M,且值会显示在PSS指标里的Native属性里,再次去查看,我们发现Native值和Total值都发生了突变528087KB,这从曲线变化里也可以看出来,二者的差值的确为500M左右
3.接下来我们释放内存,看变化如何:可以发现,内存释放后,Native值和Total值再次变为16971KB(因为是动态变化的,可能会与上次有所不同),且曲线图瞬间下降到原有值附近。
其他几个插件比价冷门,目前介绍它们的相关文档并不多,大家有兴趣可以私下自己去了解,再次就不再描述。
4.全局功能设置区
点击右上角的功能按钮,关于页显示当前版本号;点击“退出”按钮会退出GT
参考文档:http://m.elecfans.com/article/605981.html
https://www.cnblogs.com/szqmvp/p/7723326.html
https://www.cnblogs.com/ysocean/p/6889906.html
https://blog.csdn.net/haoxuhong/article/details/80599176
https://blog.csdn.net/qq_21209179/article/details/80006637