Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >JS获取GIF总帧数

JS获取GIF总帧数

作者头像
神奇的程序员
发布于 2022-10-30 06:04:08
发布于 2022-10-30 06:04:08
8K00
代码可运行
举报
运行总次数:0
代码可运行

前言

有一个Gif图片,我们想要获取它的总帧数,超过一定帧数的图片告知用户不可上传,在服务端有很多现成的库可以使用,这种做法不是很友好,前端需要先将gif上传至服务端,服务端解析完毕后将结果返回,大大降低了用户体验。

那么如何通过js在上传前就拿到它的总帧数来判断呢?本文就跟大家分享一种解决方案,并将其封装成插件发布至npm仓库,欢迎各位感兴趣的开发者阅读本文。

在小程序中阅读

为了更好的阅读体验,你可以点击下方小程序来阅读本文。

写在前面

此插件已经发布至npm,采用原生JS编写支持任意一个前端框架,如果你对其实现原理不感兴趣,只是想拿来解决你的实际问题,可以直接通过npm/yarn来安装,命令如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
# yarn安装
yarn add gif-parser-web

# npm安装
npm install gif-parser-web --save

文档地址请移步👉:README.md

思路分析

我们都知道无论什么文件在计算机中都是以流的形式进行存储的,因此我们可以通过读取文件流来拿到它的所有信息。Gif类型的文件也是如此,我们只要能知道它的文件流结构就可以根据它的规则进行解析读取了。

什么是Gif

Gif的全称是Graphics Interchange Format,是一种位图,以8位色重现真彩色的图像。采用LZW压缩算法进行编码,可以有效的减少图像文件在网上的传输时间,我们在网站上看到的会动的表情包,基本上都是Gif格式的。

组成结构

正如上面所说,我们想解析gif就得先知道它的文件流结构,在What's In A GIF网站中我们知道了它是由多种不同类型的块组成,如下所示:

  • 未标记块:Header(文件头)、Logical Screen Descriptor(逻辑屏幕描述符)、Global Color Table(全局颜色表)、局部颜色表(Local Color Table)
  • 控制块:图形控制扩展(Graphics Control Extension)
  • 图形渲染块:纯文本扩展(Plain Text Extension)、图像描述符(Image Descriptor)
  • 特殊用途块:应用扩展( Application Extension)、注解扩展(Comment Extension)、数据流结束标记(Trailer)
  • 图像数据块:图像数据(Image Data)

解析原理

了解完gif的组成结构后,接下来我们来看下如何获取它的数据流,如下所示:

  • 读取Gif图片文件(从url读取或者从本地上传的File类型的数据)
  • 将读取到的数据转成arrayBuffer
  • arrayBuffer放到DataView
  • 使用DataView底层的相关API来读取十六进制编码
  • 对十六进制编码进行解码,获取图像的信息

它的解码过程如下图所示:

  • 从Header开始顺着箭头一直读到PlainTextExtension完成第一帧的读取,其中GlobalColorTable、ApplicationExtension、CommentExtension、LocalColorTable、PlainTextExtension不一定存在
  • 接下来重复GraphicControlExtension、ImageDescriptor、ImageData 读取剩下的帧图片数据
  • 直至读取到Trailer标识,就完成了整个Gif的读取

GIF file stream diagram

注意:在读取过程中,每个块都有自己特殊的编码标记。

数据块分析

我们了解完gif的构成后,接下来我们来看下每一个具体的数据块的编码信息。

Header Block

该数据块用于标记数据流的开始,位于文件头数据流的上下文内,里面包含了gif的签名与版本信息,它是必须存在的且只有一个。

该块在数据流中占6个字节,其中签名与版本信息各占3个字节,即:

  • 数据流的0-2位置的元素一定表示gif的签名信息
  • 数据流的3-5位置的元素一定表示gif的版本信息

我们以89a格式的gif为例,它的Header信息就如下所示:

  • Signature的16进制值为47、49、46,将其转换为Unicode编码字符后就为:"G"、"I"、"F"
  • Version的16进制值为38、39、61,将其转换为Unicode编码字符后就为:"8"、"9"、"a"

GIF header block layout

我们来看下如何用代码来读取。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 假设我们已经得到了dataView
const signature = dataView.getUint16(0); // 使用getUint16方法从0号位置开始连续获取2个字节的值,转换成转换为Unicode编码为:G I
const version = dataView.getUint16(2); // 使用getUint16方法从2号位置开始连续获取2个字节的值,转换成转换为Unicode编码为:F 8

Logical Screen Descriptor

该数据块中定义了图像在设备中显示所需的参数,位于Header数据块的后面,它是必须存在的且只有一个,其值的坐标是相对于虚拟屏幕左上角计算出来的。

该块在数据流中占7个字节,包含的信息如下所示:

  • Canvas Width 图片的宽度(以像素为单位),占2个字节空间。
  • Canvas Height 图片的高度(以像素为单位),占2个字节空间。
  • Packed Fields 压缩字段,占1字节空间,里面包含4个值
    • Global Color Table Flag 全局颜色标记,用于标识全局颜色表。如果值为0则表示不存在全局颜色块;如果值为1则表示全局颜色块紧跟于此块之后。
    • Color Resolution 颜色分辨率,即颜色的位数,有1位、8位、16位、32位等。在gif格式的图像定义中,它的颜色不能超过256种,深度不能超过8位。
    • Sort Flag 排序标记,0为未设置,1为按重要性递减排序,最重要的颜色在前。
    • Size of Global Color Table 全局颜色表的大小,如果值为1,则该字段中的值用于计算全局颜色表中包含的字节数。
  • Background Color Index 背景颜色索引,它描述了全局颜色表的索引,背景颜色是用于屏幕上未被图像覆盖的像素的颜色。如果全局颜色标记设置为0,该字段将会被忽略。
  • Pixel Aspect Ratio 像素纵横比,用于计算原始图像中像素纵横比的近似值的因子。如果该值不为0,则近似值的计算公式为:(N + 15) / 64 ,N为像素纵横比,它的值为像素宽度与其高度的商。

GIF logical screen descriptor block layout

我们用代码来获取下它的宽度与高度。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 假设我们已经得到了dataView
const width = this.dataView.getUint16(6, true);
const height = this.dataView.getUint16(8, true);

Global Color Table

该数据块包含了一个颜色表,由红-绿-蓝三元组的字节序列构成。正如前面所说,它并非必须存在,如果存在的话它将位于Logical Screen Descriptor块的后面。

所占的字节数为3*2^(N+1),N为全局颜色表的大小 + 1,该数据块在数据流中只存在一个,如下图所示。

GIF global color table block layout

我们来看下代码的实现。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
let pos = 0;
const PaletteColorsRGB = [];
const gifInfo = {}

// 解析全局调色板
const unpackedField = getBitArray(dataView.getUint8(10));
if (unpackedField[0]) {
  const globalPaletteSize = getPaletteSize(unpackedField);
  gifInfo.globalPalette = true;
  // 计算全局调色板的大小
  gifInfo.globalPaletteSize = globalPaletteSize / 3;
  // 调整指针位置
  pos += globalPaletteSize;
  // 遍历获取此块区域的所有颜色并存起来
  for (let i = 0; i < gifInfo.globalPaletteSize; i++) {
    const palettePos = 13 + i * 3;
    const r = dataView.getUint8(palettePos);
    const g = dataView.getUint8(palettePos + 1);
    const b = dataView.getUint8(palettePos + 2);
    PaletteColorsRGB.push({ r, g, b });
  }
}
pos += 13;

// 获取调色板大小函数
function getPaletteSize(palette: Array<number>): number {
  return 3 * Math.pow(2, 1 + bitToInt(palette.slice(5, 8)));
}

Graphics Control Extension

该数据块包含了处理图形渲染块时需要使用的参数,它只包含了一个数据子块。该块中记录了7种数据的描述,如下所示:

  • Extension Introducer 扩展导入符,标识扩展块的开始,包含固定值0x21
  • Graphic Control Label 图形控制标签,用于将当前块标识为图形控制扩展,包含固定值0xF9
  • Byte Size 块中的字节数,在此字段之后,直到但不包括终止符。该字段包含固定值4,里面包含了4种数据的描述。
    • Reserved for Future Use 保留模块
    • Disposal Method 处理方法,表示图形在显示后的处理方式。
    • User Input Flag 用户输入标识,在继续之前是否需要用户输入,如果是0则不需要用户输入,1代表需要用户输入。输入的性质由程序决定(如回车、鼠标点击等)
    • Transparency Color Flag 透明标识,用于描述是否在透明索引字段中给出了透明索引。0:未给出透明索引;1:给出了透明索引
  • Delay Time 当前帧图像的延迟时间,如果不为0,则表示该字段在继续处理数据流之前等待的百分之一秒(即gif每一帧的时长)
  • Transparency Index 透明度指数
  • Block Terminator 块终止符,用于标识图形控制扩展块的结束

GIF graphic control extension block layout

此处我们最关心的就是如何取出gif每一帧的时长,我们来看下代码的实现。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 假设我们已经得到了dataView且pos可能指向图形控制快
const type = dataView.getUint8(pos);
// 图形控制块
if (type === 0xf9) {
  const length = dataView.getUint8(this.pos + 2);
  if (length === 4) {
    // 获取每一帧的时长
    const delay = getFrameDuration(dataView.getUint16(pos + 4, true));
    pos += 8;
  }
}

Image Descriptor

一个gif文件可能会包含多个图像,每个图像都以一个图像描述符块开始。这个块在数据流中占10个字节。该块中记录了6种数据的描述,如下所示:

  • Image Separator 图像分割符,用于标识此数据块的开头,它的固定值为0x2C
  • Image Left Position 图像左位置,图像左边缘距离逻辑屏幕左边缘的行数(以像素为单位)
  • Image Top Position 图像顶部位置,图像顶部边缘相对于逻辑屏幕顶部边缘的行数(以像素为单位)
  • Image Width 图像宽度
  • Image Height 图像高度
  • Packed Field 压缩块
    • Local Color Table Flag 局部颜色表标志,紧跟在该图像描述符之后的局部颜色表的存在,0:不存在,则使用全局颜色表,1:存在,则使用紧跟其后的Local Color Table数据块
    • Interlace Flag 隔行标志,标识图像是否是隔行的(图像以四遍交错模式交错)
    • Sort Flag 排序标志 - 指示本地颜色表是否已排序。0:未设置排序,1:按重要性递减排序,最重要的颜色在前
    • Size of Local Color Table 局部颜色表的大小

GIF image descriptor block layout

Image Data

该块由一系列子块组成,每个子块的大小最多为255字节,包含对图像中每个像素的活动颜色表的索引, 像素索引按从左到右和从上到下的顺序排列。每个索引必须在活动颜色表的大小范围内,从 0 开始。索引序列使用具有可变长度代码的 LZW 算法进行编码,如下所示。

GIF image data block layout

GIF image data block layout

每解析完一轮Image Descriptor都需要读取下Data Sub-blocks,直至所有子块被读取完毕。

实现代码

通过前面的了解,我们知道了Gif图像中每个数据块的组成原理,接下来我们就可以编写代码来解决我们所遇到的问题了🤗

我们将数据块分析章节的思路整理下,核心代码如下所示:

  • 插件初始化的时候,接受一个url作为可选参数,如果存在则使用fetch解析这个url,将最终的数据放入dataView中
  • 暴露一个getInfo方法用于获取Gif的信息,接受一个File类型的可选参数,如果url与此参数同时传入,则优先使用此参数
  • 完整代码👉:main.ts
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
export default class GifParser {
  private urlLoadStatus: boolean | undefined = undefined;
  private dataView: DataView | undefined;
  // 当前指向DataView的指针位置
  private pos = 0;
  // 当前解析的帧索引
  private index = 0;
  private gifInfo: gifInfoType = {
    valid: false,
    globalPalette: false,
    globalPaletteSize: 0,
    globalPaletteColorsRGB: [],
    loopCount: 0,
    height: 0,
    width: 0,
    animated: false,
    images: [],
    duration: 0,
    identifier: "0"
  };
  constructor(url?: string) {
    if (url) {
      this.urlLoadStatus = false;
      // 解析url,将其转化为DataView格式的数据
      fetch(url)
        .then((response) => response.arrayBuffer())
        .then((arrayBuffer) => {
          return new DataView(arrayBuffer);
        })
        .then((dataView) => {
          // GIF加载成功
          this.urlLoadStatus = true;
          this.dataView = dataView;
        });
    }
  }
  /**
   * 获取图像信息
   * @param gifStream
   */
  public async getInfo(gifStream?: File): Promise<gifInfoType> {
    // 参数有效性校验
    await this.validityCheck(gifStream);
    // url与gifStream都未传入则抛出异常
    if (this.dataView == null) {
      throw new Error("未找到GIF解析源, 请检查参数是否正确传入");
    }
    
    // 只解析GIF8格式的图像:使用getUint16获取2个字节十六进制值,判断它是否满足Gif格式的Header块的签名与版本号
    // 47 49 为签名信息,转换为Unicode编码为:G I
    // 46 38 为版本信息,转换为Unicode编码为:F 8
    if (
      this.dataView.getUint16(0) != 0x4749 ||
      this.dataView.getUint16(2) != 0x4638
    ) {
      return this.gifInfo;
    }
    
    // 经过上述判断后,此时的GIF已经有效了
    this.gifInfo.valid = true;
    // 获取GIF图像的宽,高
    this.gifInfo.width = this.dataView.getUint16(6, true);
    this.gifInfo.height = this.dataView.getUint16(8, true);
    
    // 获取全局调色板、读取每一帧的图像信息等代码省略,请移步GitHub查看完整代码
  }
}

测试用例

最后,我们将插件打包,写一个简单的demo来测试下。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<meta charset="utf-8">
<title>gifParserPlugin demo</title>
<script src="./gifParserPlugin.umd.js"></script>


<script>
async function getGifInfo(e) {
  const gifParser = new gifParserPlugin()
  const gifInfo = gifParser.getInfo(e.target.files[0])
  gifInfo.then((res) => {
    console.log("解析完成", res);
  })
}
window.onload = function() {
  const input = document.getElementById('input');
  input.addEventListener('change', getGifInfo);
}
</script>

<input type="file" id="input">

运行结果如下所示。

  • gif的宽度是748px,高度是358px
  • gif的总时长为11400ms,总共有114帧

image-20220526204406993

插件地址

该插件已发布至npm,地址为请移步👇:

  • npm地址:gif-parser-web
  • GitHub地址:gif-parser-web-github

此处不讲插件的发布流程,如果你对此感兴趣请移步👇:

  • 使用CLI开发一个Vue3的npm库
  • 实现Web端自定义截屏(原生JS版)

写在最后

至此,文章就分享完毕了。

我是神奇的程序员,一位前端开发工程师。

如果你对我感兴趣,请移步我的个人网站,进一步了解。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-05-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 神奇的程序员 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
gif 格式
请看图片,gif 图分为图片文件头(File Header),gif信息(GIF Data Stream)和文件结尾(Trailer)三个部分,最主要的是 gif 信息。gif信息是由控制块(Control Block)和数据块(Data Sub-blocks)组成的。
林德熙
2018/09/19
2.3K0
gif 格式
利用Android系统源码中giflib实现播放gif文件
目前市场上流行的图片框架都是可以很好的处理gif图片,像glide是通过Java层来处理gif的展示,但是Java层来处gif的展示,始终会存在OOM的风险。今天学习了一下Android系统源码中拓展源码的giflib加载gif。
包子388321
2020/07/01
2.1K0
GIF简述及其在QQ音乐的应用
QQ音乐技术团队
2017/09/30
2.2K0
GIF简述及其在QQ音乐的应用
你真的了解 gif 吗?分析 gif 文件和一些奇怪的 gif 特性
是的,我指的是主流的,遍布全网的普通 gif,谷歌旗下的 Tenor 或 Facebook 旗下的 giphy 这样的网站到处都是这种 gif。Gif 是所有人都喜欢的,用来分享简短动画片断的文件格式。
winty
2022/04/08
1.5K0
你真的了解 gif 吗?分析 gif 文件和一些奇怪的 gif 特性
GIF格式解析
前言 本文参考gif 格式图片详细解析。加入了一些自己的理解和解析方面的示例。 ---- GIF格式解析 图像互换格式(GIF,Graphics Interchange Format)是一种位图图形文件格式,以8位色(即256种颜色)重现真彩色的图像。它实际上是一种压缩文档,采用LZW压缩算法进行编码,有效地减少了图像文件在网络上传输的时间。它是目前广泛应用于网络传输的图像格式之一。 图像互换格式主要分为两个版本,即图像互换格式87a和图像互换格式89a。 图像互换格式87a:是在1987年制定的版本。
Oceanlong
2018/07/03
6.4K0
庖丁解牛:GIF
该文介绍了GIF动画的基本原理、GIF文件的格式、GIF的编码方式以及GIF的帧格式。GIF是一种无损压缩的8位图像文件格式,常用于网络上的图片存储和传输。GIF格式支持灰度图像和彩色图像,但不支持Alpha通道。GIF格式采用Lempel-Zev-Welch(LZW)压缩算法进行压缩,该算法是一种无损压缩算法,能够在保证图像质量的同时有效地减小文件大小。GIF格式还支持调色板、透明区域、渐进式显示、动画等特性。
郭艺帆
2017/09/01
1.7K0
庖丁解牛:GIF
常见图片文件格式简析下载_图片的文件格式有哪些
bmp文件头(bmp file header):14Byte。提供文件的格式、大小等信息 。
全栈程序员站长
2022/09/20
1.3K0
基于STM32设计的数码相册
项目是基于STM32设计的数码相册,能够通过LCD显示屏解码显示主流的图片,支持bmp、jpg、gif等格式。用户可以通过按键或者触摸屏来切换图片,同时还可以旋转显示,并能够自适应居中显示,小尺寸图片居中显示,大尺寸图片自动缩小显示(超出屏幕范围)。图片从SD卡中获取。
DS小龙哥
2023/08/02
3730
基于STM32设计的数码相册
js玩转APNG -- 逆转火狐
APNG是一种常见的网页动画,兼容性较好,交互性差,要想对其进行深入了解,则要了解其文件格式。本文以一个具体的问题为例,带你深入了解APNG的格式。
IMWeb前端团队
2019/12/13
2.4K0
js玩转APNG -- 逆转火狐
舞动的表情包——浅析GIF格式图片的存储和压缩
导语 GIF(Graphics Interchange Format)原义是“图像互换格式”,是CompuServe公司在1987年开发出的图像文件格式,可以说是互联网界的老古董了。 GIF格式可以存储多幅彩色图像,如果将这些图像连续播放出来,就能够组成最简单的动画。所以常被用来存储“动态图片”,通常时间短,体积小,内容简单,成像相对清晰,适于在早起的慢速互联网上传播。 本来,随着网络带宽的拓展和视频技术的进步,这种图像已经渐渐失去了市场。可是,近年来流行的表情包文化,让老古董GIF图有了新的用武之地。
腾讯Bugly
2018/03/23
2.1K0
硬核APNG实践 -- 逆转火狐
APNG是一种常见的网页动画,兼容性良好(可惜微信不兼容,本文动图以gif代替),交互性差,要想对其进行深入了解,则要了解其文件格式。本文以一个具体的问题为例,带你深入了解APNG的格式。 带着问题学习 -- 逆转火狐 先上问题:有一张火狐logo的图片,原图是顺时针旋转的,我们怎么来把它改为逆时针旋转呢?(微信公众号自动压缩图片。这里以gif作为演示,可点击文章底部“阅读原文”查看apng效果) 动画的基本原理 帧动画的基本原理是这样的,事先准备若干张静态图片(关键帧),每张图片之间有细微的差异,在
用户1097444
2022/06/29
1K0
硬核APNG实践 -- 逆转火狐
浓缩的才是精华:浅析 GIF 格式图片的存储和压缩
该文章介绍了如何通过Guetzli算法对图片进行压缩,节省存储空间,同时又不损失太多图片质量。文章首先介绍了图片压缩的背景和意义,然后详细讲解了Guetzli算法的原理和实现,最后列举了一些应用场景和案例。
WendyGrandOrder
2017/03/30
12.2K3
浓缩的才是精华:浅析 GIF 格式图片的存储和压缩
OpenHarmony轻松玩转GIF数据渲染
OpenAtom OpenHarmony(以下简称“OpenHarmony”)提供了Image组件支持GIF动图的播放,但是缺乏扩展能力,不支持播放控制等。今天介绍一款三方库——ohos-gif-drawable三方组件,带大家一起玩转GIF的数据渲染,搞定GIF动图的各种需求。
小帅聊鸿蒙
2025/04/22
720
OpenHarmony轻松玩转GIF数据渲染
万字长文带你学习【前端开发中的二进制数据】| 技术创作特训营第五期
在现代前端开发中,处理二进制数据变得越来越重要。从图像、音频到文件上传,这些数据类型常常以二进制形式存在。这个分享将带你深入探索 ArrayBuffer、Blob、File 以及流(Stream)等概念,探讨它们如何在前端开发中发挥作用,解锁了解和利用二进制数据的强大能力。
程序员库里
2024/01/24
7720
Android终端上视频转GIF的实现及GIF质量讨论
在生成 GIF 的过程中,最关键的步骤就是生成调色板以及像素到调色板的映射关系。
天天P图攻城狮
2018/02/02
3.8K0
Android终端上视频转GIF的实现及GIF质量讨论
文件上传杂谈
文件上传是前端很常见的一类场景。图片、视频和文档等等都属于文件范畴,每个文件则是通过 File.Type 进行更细的划分。本文将针对文件上传的一些通用维度场景做简单的剖析和尝试,抛砖引玉,希望共同学习,共同成长。
有赞coder
2021/01/18
1.6K0
文件上传杂谈
NDK--实现gif图片播放
部分Gif图片不能自适应大小, 播放速度比实际播放速度快, 如果要显示的gif过大,还会出现OOM的问题。
aruba
2020/07/02
1.5K0
js操作二进制数据
使用ArrayBuffer对象保存二进制数据,使用TypedArray和DataView 视图来读写数据。
风花一世月
2024/03/19
3160
js操作二进制数据
ArrayBuffer
ArrayBuffer对象、TypedArray视图和DataView视图是 JavaScript 操作二进制数据的一个接口。这些对象早就存在,属于独立的规格(2011 年 2 月发布),ES6 将它们纳入了 ECMAScript 规格,并且增加了新的方法。它们都是以数组的语法处理二进制数据,所以统称为二进制数组。 这个接口的原始设计目的,与 WebGL 项目有关。所谓 WebGL,就是指浏览器与显卡之间的通信接口,为了满足 JavaScript 与显卡之间大量的、实时的数据交换,它们之间的数据通信必须是二进制的,而不能是传统的文本格式。文本格式传递一个 32 位整数,两端的 JavaScript 脚本与显卡都要进行格式转化,将非常耗时。这时要是存在一种机制,可以像 C 语言那样,直接操作字节,将 4 个字节的 32 位整数,以二进制形式原封不动地送入显卡,脚本的性能就会大幅提升。
小小杰啊
2022/12/21
2.6K0
WebP原理和Android支持现状介绍
1.背景 目前网络中图片仍然是占用流量较大的一部分,对于移动端更是如此,因此,如何在保证图片视觉不失真前提下缩小体积,对于节省带宽和电池电量十分重要。 然而目前对于JPEG、PNG、GIF等常用图片格式的优化已几乎达到极致,因此Google于2010年提出了一种新的图片压缩格式 — WebP,给图片的优化提供了新的可能。 WebP为网络图片提供了无损和有损压缩能力,同时在有损条件下支持透明通道。据官方实验显示:无损WebP相比PNG减少26%大小;有损WebP在相同的SSIM(Structural Simi
腾讯Bugly
2018/03/23
4.6K0
相关推荐
gif 格式
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验