剖析DeFi交易产品之UniswapV3:交易路由合约
本文首发于公众号:Keegan小钢
SwapRouter 合约封装了面向用户的交易接口,但不再像 UniswapV2Router 一样根据不同交易场景拆分为了那么多函数,UniswapV3 的 SwapRouter 核心就只有 4 个交易函数:
exactInputSingle
:指定输入数量的单池内交易exactOutputSingle
:指定输出数量的单池内交易exactInput
:指定输入数量和交易路径的交易exactOutput
:指定输出数量和交易路径的交易
带 Single
的只支持单池内的交易,而不带 Single
的则支持跨不同池子的互换交易。
exactInputSingle
先来看简单的单池交易,以 exactInputSingle
为始,其代码实现如下:
struct ExactInputSingleParams {address tokenIn; //输入tokenaddress tokenOut; //输出tokenuint24 fee; //手续费率address recipient; //收款地址uint256 deadline; //过期时间uint256 amountIn; //指定的输入token数量uint256 amountOutMinimum; //输出token的最小数量uint160 sqrtPriceLimitX96; //限定的价格
}function exactInputSingle(ExactInputSingleParams calldata params)externalpayableoverridecheckDeadline(params.deadline)returns (uint256 amountOut)
{amountOut = exactInputInternal(params.amountIn,params.recipient,params.sqrtPriceLimitX96,SwapCallbackData({path: abi.encodePacked(params.tokenIn, params.fee, params.tokenOut), payer: msg.sender}));require(amountOut >= params.amountOutMinimum, 'Too little received');
}
其入参有 9 个参数,返回值就一个 amountOut
,即输出的 token 数量。
从代码上可看出,实际的逻辑实现是在内部函数 exactInputInternal
。查看该内部函数之前,我们先来了解下 SwapCallbackData
。我们从上面代码可以看到,调用 exactInputInternal
时,最后一个传入的参数就是 SwapCallbackData
,这其实是一个结构体,定义了两个属性:
struct SwapCallbackData {bytes path;address payer;
}
path
表示交易路径,在以上代码中,就是由 tokenIn
、fee
、tokenOut
这三个变量拼接而成。payer
表示支付输入 token 的地址,上面的就是 msg.sender
。
接着,来看看内部函数 exactInputInternal
的代码实现:
function exactInputInternal(uint256 amountIn,address recipient,uint160 sqrtPriceLimitX96,SwapCallbackData memory data
) private returns (uint256 amountOut) {// allow swapping to the router address with address 0if (recipient == address(0)) recipient = address(this);//从路径中解码出第一个池子(address tokenIn, address tokenOut, uint24 fee) = data.path.decodeFirstPool();//当tokenIn<tokenOUt时,则说明tokenIn为token0,所以是要将token0兑换成token1bool zeroForOne = tokenIn < tokenOut;//调用底层池子的swap函数执行交易(int256 amount0, int256 amount1) =getPool(tokenIn, tokenOut, fee).swap(recipient,zeroForOne,amountIn.toInt256(),sqrtPriceLimitX96 == 0? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1): sqrtPriceLimitX96,abi.encode(data));//返回amountOutreturn uint256(-(zeroForOne ? amount1 : amount0));
}
首先,如果 recipient 地址为零地址的话,那会把 recipient 重置为当前合约地址。
接着,通过 data.path.decodeFirstPool()
从路径中解码得出 tokenIn
、tokenOut
和 fee
。decodeFirstPool
函数是在库合约 Path 里实现的。
布尔类型的 zeroForOne
表示底层 token0
和 token1
的兑换方向,为 true
表示用 token0
兑换 token1
,false
则反之。因为底层的 token0
是小于 token1
的,所以,当 tokenIn
也小于 tokenOut
的时候,说明 tokenIn == token0
,所以 zeroForOne
为 true
。
然后,通过 getPool
函数可得到池子地址,再调用底层池子的 swap
函数来执行实际的交易逻辑。
最后,我们要得到的是 amountOut
,这是 amount0 和 amount1 中的其中一个。我们已经知道,zeroForOne
为 true
的时候,tokenIn
等于 token0
,所以 tokenOut
就是 token1
,因此 amountOut
就是 amount1
。另外,对底层池子来说,属于输出的时候,返回的数值是负数,即 amount1
其实是一个负数,因此需要再加个负号转为正数的 uint256
类型。
在这个函数里,我们可以看出并没有支付 token 的功能,但前面讲解 UniswapV3Pool 时已经了解到,支付是在回调函数 uniswapV3SwapCallback
里完成的。因为这个回调函数会涉及到所有 4 种交易类型,所以我们留到最后再来讲解。
exactOutputSingle
接着,来看 exactOutputSingle
函数的实现,其代码如下:
struct ExactOutputSingleParams {address tokenIn; //输入tokenaddress tokenOut; //输出tokenuint24 fee; //手续费率address recipient; //收款地址uint256 deadline; //过期时间uint256 amountOut; //指定的输出token数量uint256 amountInMaximum; //输入token的最大数量uint160 sqrtPriceLimitX96; //限定的价格
}function exactOutputSingle(ExactOutputSingleParams calldata params)externalpayableoverridecheckDeadline(params.deadline)returns (uint256 amountIn)
{// avoid an SLOAD by using the swap return dataamountIn = exactOutputInternal(params.amountOut,params.recipient,params.sqrtPriceLimitX96,SwapCallbackData({path: abi.encodePacked(params.tokenOut, params.fee, params.tokenIn), payer: msg.sender}));require(amountIn <= params.amountInMaximum, 'Too much requested');// has to be reset even though we don't use it in the single hop caseamountInCached = DEFAULT_AMOUNT_IN_CACHED;
}
可看出,exactOutputSingle
函数的实现与 exactInputSingle
函数大同小异。首先,参数上,只有两个不同,exactInputSingle
函数指定的是 amountIn
和 amountOutMinimum
;而 exactOutputSingle
函数改为了 amountOut
和 amountInMaximum
,即输出是指定的,而输入则限制了最大值。其次,实际逻辑封装在了 exactOutputInternal
内部函数,而且传给该内部函数的最后一个参数的 path
组装顺序也不一样了,排在第一位的是 tokenOut
。
核心实现还是在 exactOutputInternal
内部函数,其代码实现如下:
function exactOutputInternal(uint256 amountOut,address recipient,uint160 sqrtPriceLimitX96,SwapCallbackData memory data
) private returns (uint256 amountIn) {// allow swapping to the router address with address 0if (recipient == address(0)) recipient = address(this);//从路径中解码出第一个池子(address tokenOut, address tokenIn, uint24 fee) = data.path.decodeFirstPool();//是否token0兑换token1bool zeroForOne = tokenIn < tokenOut;//调用底层池子的swap函数执行交易(int256 amount0Delta, int256 amount1Delta) =getPool(tokenIn, tokenOut, fee).swap(recipient,zeroForOne,-amountOut.toInt256(), //指定输出需转为负数sqrtPriceLimitX96 == 0? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1): sqrtPriceLimitX96,abi.encode(data));//确定amountIn和amountOutuint256 amountOutReceived;(amountIn, amountOutReceived) = zeroForOne? (uint256(amount0Delta), uint256(-amount1Delta)): (uint256(amount1Delta), uint256(-amount0Delta));// it's technically possible to not receive the full output amount,// so if no price limit has been specified, require this possibility awayif (sqrtPriceLimitX96 == 0) require(amountOutReceived == amountOut);
}
可见和 exactInputInternal
的实现也是大同小异。不过,有一个细节需要补充一下。因为是指定的输出数额,所以调用底层的 swap 函数时,第三个传参转为了负数,这也是前面讲解 UniswapV3Pool 的 swap 函数时讲过的,当指定的交易数额是输出的数额时,则需传负数。
和 exactInputInternal
一样,在当前函数里没有支付 token 的逻辑,也是统一在 uniswapV3SwapCallback
回调函数里去完成支付。
exactInput
exactInput
函数则用于处理跨多个池子的指定输入数量的交易,相比单池交易会复杂一些,而且这里面的逻辑还有点绕,我们来进行一一剖析。其实现代码如下:
struct ExactInputParams {bytes path; //交易路径address recipient; //收款地址uint256 deadline; //过期时间uint256 amountIn; //指定输入token数量uint256 amountOutMinimum; //输出token的最小数量
}function exactInput(ExactInputParams memory params)externalpayableoverridecheckDeadline(params.deadline)returns (uint256 amountOut)
{//调用者需支付路径中的第一个代币address payer = msg.sender;//遍历路径while (true) {//路径中是否还存在多个池子bool hasMultiplePools = params.path.hasMultiplePools();//先前交换的输出成为后续交换的输入params.amountIn = exactInputInternal(params.amountIn,hasMultiplePools ? address(this) : params.recipient,0,SwapCallbackData({path: params.path.getFirstPool(), // 只需要路径里的第一个池子payer: payer}));//当路径依然由多个池子组成时,则继续循环,否则退出循环if (hasMultiplePools) {payer = address(this);//跳过第一个token,作为下一轮的路径params.path = params.path.skipToken();} else {//最后一次兑换,把前面设为了amountIn的重新赋值给amountOutamountOut = params.amountIn;break;}}require(amountOut >= params.amountOutMinimum, 'Too little received');
}
其中,需要跨多个池子的路径编码方式如下图:
和 UniswapV2 一样,这个路径是由前端计算出来再传给合约的。寻找最优路径的算法也是和 UniswapV2 一样的思路。
exactInput
函数的核心实现逻辑是,循环处理路径中的每一个配对池,每处理完一个池子的交易,就从路径中移除第一个 token 和 fee,直到路径只剩下最后一个池子就结束循环。期间,每一次执行 exactInputInternal
后,将返回的 amounOut
作为下一轮的 amountIn
。第一轮兑换时,payer
是合约的调用者,即 msg.sender
,而输出代币的 recipient
则是当前合约地址。中间的每一次兑换,payer
和 recipient
都是当前合约地址。到最后一次兑换时,recipient
才转为用户传入的地址。
exactOutput
剩下最后一个函数 exactOutput
了,也是用于处理跨多个池子的的交易,而指定的是输出的数量。以下是其代码实现:
struct ExactOutputParams {bytes path; //交易路径address recipient; //收款地址uint256 deadline; //过期时间uint256 amountOut; //指定输出token数量uint256 amountInMaximum; //输入token的最大数量
}function exactOutput(ExactOutputParams calldata params)externalpayableoverridecheckDeadline(params.deadline)returns (uint256 amountIn)
{// it's okay that the payer is fixed to msg.sender here, as they're only paying for the "final" exact output// swap, which happens first, and subsequent swaps are paid for within nested callback framesexactOutputInternal(params.amountOut,params.recipient,0,SwapCallbackData({path: params.path, payer: msg.sender}));amountIn = amountInCached;require(amountIn <= params.amountInMaximum, 'Too much requested');amountInCached = DEFAULT_AMOUNT_IN_CACHED;
}
可看到其逻辑就直接调用内部函数 exactOutputInternal
完成交易,并没有像 exactInput
一样的循环处理。但在整个流程中,其实还是进行了遍历路径的多次交易的,只是这个流程完成得比较隐晦。其关键其实是在 uniswapV3SwapCallback
回调函数里,后面我们会说到。
uniswapV3SwapCallback
以下就是回调函数的实现:
function uniswapV3SwapCallback(int256 amount0Delta,int256 amount1Delta,bytes calldata _data
) external override {require(amount0Delta > 0 || amount1Delta > 0);//解码出_data数据SwapCallbackData memory data = abi.decode(_data, (SwapCallbackData));//解码出路径的第一个池子(address tokenIn, address tokenOut, uint24 fee) = data.path.decodeFirstPool();//校验callback的调用者CallbackValidation.verifyCallback(factory, tokenIn, tokenOut, fee);//用于判断当前需要支付的代币(bool isExactInput, uint256 amountToPay) =amount0Delta > 0? (tokenIn < tokenOut, uint256(amount0Delta)): (tokenOut < tokenIn, uint256(amount1Delta));if (isExactInput) { //指定金额的是输入,直接执行支付pay(tokenIn, data.payer, msg.sender, amountToPay);} else { //指定金额的是输出// either initiate the next swap or payif (data.path.hasMultiplePools()) {// 路径里有多个池子时,则跳过路径的第一个token,使用下一个配对的池子进行交易data.path = data.path.skipToken();exactOutputInternal(amountToPay, msg.sender, 0, data);} else { //只剩下一个池子,执行支付amountInCached = amountToPay;tokenIn = tokenOut; // swap in/out because exact output swaps are reversedpay(tokenIn, data.payer, msg.sender, amountToPay);}}
}
另外,这个是 swap 时的回调函数。而之前的文章我们还讲了另一个回调函数 uniswapV3MintCallback
是添加流动性时的回调函数,两者是不同的,不要搞混了。
其逻辑实现并不复杂。首先,先把 _data
解码成 SwapCallbackData
结构体类型数据。接着,解码出路径的第一个池子。然后,通过 verifyCallback
校验调用当前回调函数的是否为底层 pool 合约,非底层 pool 合约是不允许调起回调函数的。
isExactInput
和 amountToPay
的赋值需要拆解一下才好理解。首先需知道,amount0Delta
和 amount1Delta
其实是一正一负的,正数是输入的,负数是输出的。因此,amount0Delta
大于 0 的话则 amountToPay
就是 amount0Delta
,否则就是 amount1Delta
了。 amount0Delta
大于 0 也说明了输入的是 token0
,因此,当 tokenIn < tokenOut
的时候,说明 tokenIn
就是 token0
,也即是说用户指定的是输入数量,所以这时候的 isExactInput
即为 true
。
当指定金额为输出的时候,也就是处理 exactOutput
和 exactOutputSingle
函数的时候。我们前面看到 exactOutput
的代码逻辑里并没有对路径进行遍历处理,这个遍历其实就是在这个回调函数里完成的。仔细看这段代码:
if (data.path.hasMultiplePools()) {// 路径里有多个池子时,则跳过路径的第一个token,使用下一个配对的池子进行交易data.path = data.path.skipToken();exactOutputInternal(amountToPay, msg.sender, 0, data);
}
这不就是遍历路径多次执行 exactOutputInternal
了吗。
至此,SwapRouter 合约也讲解完了。
相关文章:

剖析DeFi交易产品之UniswapV3:交易路由合约
本文首发于公众号:Keegan小钢 SwapRouter 合约封装了面向用户的交易接口,但不再像 UniswapV2Router 一样根据不同交易场景拆分为了那么多函数,UniswapV3 的 SwapRouter 核心就只有 4 个交易函数: exactInputSingle:指…...

Agent下载安装步骤
目录 一. 环境准备 二. 部署安装 三. Server端Web页面添加agent客户端 一. 环境准备 准备一台虚拟机,关闭防火墙和selinux,进行时间同步。 版本主机名IP系统zabbix6.4-agentweb1192.168.226.29Rocky_linux9.4 修改主机名 [rootlocalhost ~]# hostna…...

2024年AI技术深入研究
2024年AI技术持续快速发展,应用领域广泛,产业发展迅速,市场趋势积极,学术研究深入。 AI技术进展大模型发展 2024年,智谱AI正在研发对标OpenAI Sora的高质量文生视频模型,预计最快年内发布。智谱AI的进展显示了国内AI大模型领域的快速发展,以及与国际领先技术的竞争态势…...

Apache Seata分布式事务启用Nacos做配置中心
本文来自 Apache Seata官方文档,欢迎访问官网,查看更多深度文章。 本文来自 Apache Seata官方文档,欢迎访问官网,查看更多深度文章。 Seata分布式事务启用Nacos做配置中心 Seata分布式事务启用Nacos做配置中心 项目地址 本文作…...

Emacs之解决:java-mode占用C-c C-c问题(一百四十六)
简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏:Audio工程师进阶系列【原创干货持续更新中……】🚀 优质专栏:多媒…...

go语言day10 接口interface 类型断言 type关键字
接口: 空接口类型: 要实现一个接口,就要实现该接口中的所有方法。因为空接口中没有方法,所以自然所有类型都实现了空接口。那么就可以使用空接口类型变量去接受所有类型对象。 类比java,有点像Object类型的概念&#x…...

Java实现登录验证 -- JWT令牌实现
目录 1.实现登录验证的引出原因 2.JWT令牌2.1 使用JWT令牌时2.2 令牌的组成 3. JWT令牌(token)生成和校验3.1 引入JWT令牌的依赖3.2 使用Jar包中提供的API来实现JWT令牌的生成和校验3.3 使用JWT令牌验证登录3.4 令牌的优缺点 1.实现登录验证的引出 传统…...

liunx文件系统,日志分析
文章目录 1.inode与block1.1 inode与block概述1.2 inode的内容1.3 文件存储1.4 inode的大小1.5 inode的特殊作用 2.硬链接与软链接2.1链接文件分类 3.恢复误删除的文件3.1 案例:恢复EXT类型的文件3.2 案例:恢复XFS类型的文件3.2.1 xfsdump使用限制 4.分析日志文件4.1日志文件4.…...

Apipost接口测试工具的原理及应用详解(二)
本系列文章简介: 随着软件行业的快速发展,API(应用程序编程接口)作为不同软件组件之间通信的桥梁,其重要性日益凸显。API的质量直接关系到软件系统的稳定性、性能和用户体验。因此,对API进行严格的测试成为软件开发过程中不可或缺的一环。在众多API测试工具中,Apipost凭…...

「AIGC」大数据开发语言Scala入门
Scala 是一种多范式编程语言,设计初衷是集成面向对象编程和函数式编程的特点。它运行在 Java 虚拟机(JVM)上,因此可以与 Java 库无缝集成。Scala 也因其在大数据处理领域的应用而受到欢迎,特别是与 Apache Spark 这类框架结合使用。 1. 环境搭建 安装 Scala:可以从 Scala…...

2.1 tmux和vim
文章目录 前言概述tmuxvim总结 前言 开始学习的时间是 2024.7.6 ,13:47 概述 最好多使用,练成条件反射式的 直接使用终端的工具,可以连接到服务器,不需要使用本地的软件 tmux 这个主要有两个功能,第一个功能是分…...

运行vue : 无法加载文件 C:\Program Files\nodejs\node_global\vue.ps1,因为在此系统上禁止运行脚本。
报错背景: 重装了win10系统,然后准备安装Vue,这个时候我已经安装好了node.js和npm,输入node -v和npm -v都有正确输出,但是每次输入npm install -g vue/cli 安装的时候,就会报错. 大家安装node.js的时候最好就是默认路径(C:\Program Files\nodejs),别去修改不然很多报错.(个人…...

Lambda架构
1.Lambda架构对大数据处理系统的理解 Lambda架构由Storm的作者Nathan Marz提出,其设计目的在于提供一个能满足大数据系统关键特性的架构,包括高容错、低延迟、可扩展等。其整合离线计算与实时计算,融合不可变性、读写分离和复杂性隔离等原则&…...

数据库作业day3
创建一个student表用于存储学生信息 CREATE TABLE student( id INT PRIMARY KEY, name VARCHAR(20) NOT NULL, grade FLOAT ); 向student表中添加一条新记录 记录中id字段的值为1,name字段的值为"monkey",grade字段的值为98.5 insert into …...

计算机网络——数据链路层(以太网扩展、虚拟局域网、高速以太网)
在许多情况下,我们希望把以太网的覆盖范围扩展。本节先讨论在物理层把以太网扩展,然后讨论在数据链路层把以太网扩展。这种扩展的以太网在网络层看来仍然是一个网络。 在物理层扩展以太网 现在,扩展主机和集线器之间的距离的一种简单方法就是…...

Nuxt 项目集成第三方UI组件库(九)
Nuxt.js 本身并不提供内置的 UI 组件库,它是一个用于构建服务端渲染(SSR)和静态生成(SSG)Vue.js应用的框架。Nuxt.js 的设计目的是提供基础的架构和配置,以便开发者可以根据自己的需求选择和集成第三方 UI …...

vulnhub靶场之Jarbas
1 信息收集 1.1 主机发现 arp-scan -l 发现主机IP地址为:192.168.1.16 1.2 端口发现 nmap -sS -sV -A -T5 -p- 192.168.1.16 存在端口22,80,3306,8080 1.3 目录扫描 dirsearch -u 192.168.1.16 2 端口访问 2.1 80端口 2.2…...

解决onlyoffice警告的一些思路
解决思路: 1、监听出现警告的事件:已经实现 <script setup> import {message} from "ant-design-vue";const onError (event) > {console.log("ONLYOFFICE Document Editor reports an error: code " event.data.error…...

快速上手指南:使用 Minikube 在本地运行 Kubernetes 集群
前言 Minikube 是一个开源工具,用于在本地运行 Kubernetes 集群。它提供了一种简单的方法来在本地开发和测试 Kubernetes 应用程序,而无需设置完整的 Kubernetes 集群。以下是 Minikube 的基本使用步骤: 安装 Minikube 安装依赖项 虚拟化…...

【C语言】指针(1):入门理解篇
目录 一、内存和地址 1.1内存 1.2 深入理解计算机编址 二、指针变量和地址 2.1 取地址操作符(&) 2.2 指针变量和解应用操作符 2.2.1 指针变量 2.2.2 解引用操作符 2.3指针变量的大小 三、指针变量类型的意义 3.1 指针的解引用 3.1指针-整数…...

经典双运算放大器LM358
前言 LM358双运放有几十年的历史了吧?通用运放,很常用,搞电路的避免不了接触运放,怎么选择运放,是工程师关心的问题吧? 从本文开始,将陆续发一些常用的运放,大家选型可以参考&#…...

adb不插usb线通过wifi调试
说起做手机开发也有好多年了,说来惭愧,我最近才知道安卓手机是可以不插数据线进行开发调试的。起因是公司近期采购了一批安卓一卡通设备,需要对其进行定制开发APP,但是由于我插USB调试发现没有反应。通过询问厂家才知道可以通过WIFI进行调试。…...

vulnhub--IMF
环境 攻击机:192.168.96.4 靶机:ip未知 主机探测 确定靶机ip为32的主机 端口扫描 访问80端口 外围打点 在contact.php页面源码中找到了flag1 之后没啥突破 但查看网络后发现contact.php页面请求的三个js文件的文件名很有特点,猜测是base64编码…...

ePTFE膜(膨体聚四氟乙烯膜)应用前景广阔 本土企业技术水平不断提升
ePTFE膜(膨体聚四氟乙烯膜)应用前景广阔 本土企业技术水平不断提升 ePTFE膜全称为膨体聚四氟乙烯膜,指以膨体聚四氟乙烯(ePTFE)为原材料制成的薄膜。ePTFE膜具有耐化学腐蚀、防水透气性好、耐候性佳、耐磨、抗撕裂等优…...

C++与C#创建位图,是否需要区分RGB和BGR模式
在处理位图时,确实需要区分RGB和BGR模式,因为不同的库和API对颜色通道的排序有不同的约定。具体到C与C#,这一点也是需要注意的。 C 创建位图 使用GDI或WIC (Windows Imaging Component): 当你在C中使用这些Windows API创建或操作位图时&…...

web学习笔记(八十)
目录 1.小程序实现微信一键登录 2. 小程序的授权流程 3.小程序配置vant库 4.小程序配置分包 5.小程序配置独立分包 6.小程序分包预下载 1.小程序实现微信一键登录 要先实现小程序一键登录首先我们需要给按钮设置一个绑定事件,然后在绑定事件内部通过wx.login…...

深度解析 Raft 分布式一致性协议
本文参考转载至:浅谈 Raft 分布式一致性协议|图解 Raft - 白泽来了 - 博客园 (cnblogs.com) 深度解析 Raft 分布式一致性协议 - 掘金 (juejin.cn) raft-zh_cn/raft-zh_cn.md at master maemual/raft-zh_cn (github.com) 本篇文章将模拟一个KV数据读写服…...

Android10以上实现获取设备序列号功能
Android10以上实现获取设备唯一标识,目前只支持华为和荣耀设备。实现原理:通过无障碍服务读取序列号界面。 public class DeviceHelper implements Application.ActivityLifecycleCallbacks {static final String TAG "WADQ_DeviceHelper";s…...

从0到1:培训老师预约小程序开发笔记二
背景调研 培训老师预约小程序: 教师和学生可以更便捷地安排课程,并提升教学质量和学习效果,使之成为管理和提升教学效果的强大工具。培训老师可以在小程序上设置自己的可预约时间,学员可以根据老师的日程安排选择合适的时间进行预…...

【FFmpeg】av_read_frame函数
目录 1.av_read_frame1.2 从pkt buffer中读取帧(avpriv_packet_list_get)1.3 从流当中读取帧(read_frame_internal)1.3.1 读取帧(ff_read_packet)1.3.2 解析packet(parse_packet)1.3…...