如何不改一行代码,让Hippy启动速度提升50%?

news/2024/5/10 13:25:32/文章来源:https://blog.csdn.net/QcloudCommunity/article/details/128424742

1097375f456a4f1d7ea9495af909bc6f.gif

导读|Hippy使用JS引擎进行异步渲染,在用户从点击到打开首屏可交互过程中会有一定的耗时,影响用户体验。如何优化这段耗时?腾讯客户端开发工程师李鹏,将介绍QQ浏览器通过切换JS引擎来优化耗时的探索过程和效果收益。在分析Hippy耗时瓶颈、对比业界可选引擎方案后,最终QQ浏览器通过选择使用Hermes引擎、将JS离线生成Bytecode并使用引擎直接加载Bytecode,让首帧耗时优化50%起。希望本文对面临同样困扰的你有帮助。

3f6c15ca86f04d46a66df1bc61bfd7ce.jpeg背景

目前QQ浏览器(下简称QB)使用Hippy的业务超过100个,基本上95%的核心业务都是使用Hippy作为首要技术栈来开发。但是跟Native相比较而言,Hippy是使用JS引擎进行异步渲染,在用户从点击到打开首屏可交互过程中会有一定的耗时,影响用户体验。如何优化耗时,尽量对齐Native体验,想必是许多开发者都在思考优化的事情。

本文主要介绍QQ浏览器通过切换JS引擎来优化耗时的探索过程和效果收益。本文我将分析Hippy执行流程及耗时瓶颈、对比业界JS引擎方案,最终选择使用Hermes引擎。之后分析将JS离线生成Bytecode,使用引擎直接加载Bytecode的能力。值得一提的是,在业务无需修改一行代码的前提下,Hippy的包加载速度提高80%,首帧耗时优化50%起。下面我将展开讲述。

5fbd31b156aced594bf6184b135fddae.jpegHippy业务耗时瓶颈分析

Hippy整个启动流程依赖JS线程的执行。我们其实可以将整个过程抽象看成一个串行的操作,以QB冷启动首页Feed流,结合线上数据性能监控可以看到如下阶段耗时

6da8deb565e6d10f1fc9fdfac7403577.png

注:TTI = Time To Interact,意思是从业务创建到业务可交互所花费的时间,因为衡量业务可交互比较复杂,各个业务对可交互的定义不一样,所以这里以首帧上屏为准来衡量;

通过打点分析得到,用户从打开业务创建RootView开始,到最终首帧上屏总共耗时1488毫秒,其中主要在Module初始化、创建HippyCore(bootstrap.js以及common包执行耗时)、业务包执行耗时上。其中加载执行业务包耗时1303毫秒,占整体TTI的87%。

如果我们能够优化加载执行业务包的耗时,那么我们就可以极大的降低TTI。在iOS上Hippy使用的是系统提供的JavascriptCore引擎来运行JS代码,所以我们要分析一下JSC的执行过程。

8030da2be672fef85b4c784df412c3f4.jpegJavascriptCore执行流程分析

具体流程:词法分析,输出tokens;语法分析,生产AST(抽象语法树);从AST生成字节码; 通过Low Level解释器执行字节码;使用JIT加速解释执行机器码(带JIT的版本)。

98f924a5f4d166a2febcf362f8ef3e14.png

注:本文JSC是指苹果官方提供的JavascriptCore.framework,JSC分带JIT与不带JIT的版本,带JIT的版本目前只有苹果自家的Safari能够使用,公开的JavascriptCore因为安全原因(JIT可以动态执行机器码),实际是不带JIT的版本。下面讨论的也是指不带JIT的JSC版本。

整个流程,在JS代码被解释执行前,绝大部分时间消耗是在字节码生成上。如果能将Bytecode生成前置缓存起来,每次执行JS的时候直接取缓存的Bytecode,那将会极大降低耗时。但是很可惜的是,JavascriptCore属于系统库,并没有提供这个能力。我们可以考虑选择其他支持Bytecode的引擎替换掉JSC。

6ce34d826a1ef07633235537971a9289.jpeg

可选引擎对比

除了JSC,常见的开源引擎包括V8、QuickJS、Hermes。

JS引擎

是否支持Bytecode

SDK大小

是否开源

作者

JavascriptCore

😭

0

😊

Apple

V8

😭(仅仅是支持CodeCache,不支持直出Bytecode)

8-10M

😊

Google

QuickJS

😊

1M

😊

Bellard

Hermes

😊

3M

😊

Facebook

注:直出是指支持编译输出Bytecode文件,并且直接运行Bytecode。

Hermes和QuickJS支持直出Bytecode,并且在包大小上对比V8和JSC占优。

1)性能指标对比

以下各项对比取至Linux上各引擎测试数据

  • 包加载耗时速度对比(越低越好)

使用引擎执行业务JS代码,其中JSC和V8均是直接执行JS代码,QuickJS和Hermes是执行Bytecode。

468f19be10ec9e9429e875b0b6479493.png

QuickJS一骑绝尘,Hermes紧跟其后,JSC次之,V8最差;

  • 执行效率对比(越高越好)

使用引擎跑一些开源的算法或者知名JS功能库。

207862f4abde7b99370b79bd240de288.png

fcd50678c6f7435fef137bc4b4b0a3e0.png

V8和JSC性能最好,Hermes次之,QuickJS最差;

  • 内存增量(越低越好)

03fd9adb5dfdba70ec4a5a89372ad203.png

4cddd8b2ac2de3bb75cd8cbacef9e8be.png

表现最好的是JSC,其次是Hermes和V8;带JIT的JSC和V8,内存消耗最高;

  • 编译文件大小

衡量编译文件压缩比是为了衡量包下发更新效率,以QB首页Feed流(3.8M左右)举例,JSC和V8均输入原始js文件,QuickJS和Hermes输入JS编译后的Bytecode文件。

9e02f493fce1f02a9aec9d0c93e86534.png

JSC和V8压缩比较高,Hermes和QuickJS压缩比不高,在下发效率上,差于JSC和V8;


2)结论

从执行耗时、执行性能、内存增量、编译文件大小以及整体framework大小5个纬度来分析看: 带JIT的JSC和V8性能最好,但是加载时间是最长的,内存消耗也是最多的,包也较大;支持提前预编译的Hermes和QuickJS,加载速度以及内存表现是最好的。‍

对于提高TTI,加载速度指标最为重要。虽然性能低于JSC和V8,但是对于JS耗时高的操作,可以充分利用modules放在Native去操作;所以基于以上,会优先考虑Hermes和QuickJS;

Hermes在性能、内存以及编译包大小上是优于QuickJS的,另外Hermes有Facebook的React Native社区生态支持,相较于QuickJs更新演进更快,所以更倾向使用Hermes来替换JSC。

a940aeb9ac0ee2cf41dbfb13bb0cb176.jpegHermes引擎调研


1)编译

Hermes虽然是深度集成在React Native里的,但是facebook也将单独的引擎独立出来了,官网地址 仓库地址 编译指南。

按照编译指南编译之后,实际编译的产物只是用于在PC/Mac/Linux运行的Hermes二进制文件。通过这些二进制文件,我们可以在Terminal里执行JS,以及将JS编译成Bytecode。

# 执行原始JS 
hermes test.js
# 编译并输出以及执行Bytecode 
hermes -emit-binary -out test.hbc test.js hermes test.hbc

在移动端上,Hermes也是使用CMake进行编译,并且提供了脚本可以方便输出Android和iOS动态库。具体可以在官网上查看编译指南。


2)运行

Hermes包含几个非常重要的结构对象,下面主要讲其中的几个。

  • Runtime

Hermes使用非常简单,提供了一个Runtime的抽象类,所有的js对象都执行在Runtime对象上,类似JSC的JSContext;派生了HermesRuntime子类来实现所有JS操作。通过静态方法创建一个HermesRuntime对象;

HERMES_EXPORT std::unique_ptr<HermesRuntime> makeHermesRuntime(const ::hermes::vm::RuntimeConfig &runtimeConfig =::hermes::vm::RuntimeConfig());

同时也提供了一些执行JS的方法

  
// 执行JS(JS or Bytecode)virtual Value evaluateJavaScript(const std::shared_ptr<const Buffer>& buffer,const std::string& sourceURL) = 0;// 预编译JSvirtual std::shared_ptr<const PreparedJavaScript> prepareJavaScript(const std::shared_ptr<const Buffer>& buffer,std::string sourceURL) = 0;// 执行预编译的JSvirtual Value evaluatePreparedJavaScript(const std::shared_ptr<const PreparedJavaScript>& js) = 0;
  • Value

JSC在处理基础数据的时候,所有的类型都是JSValue类型;处理Object是JSObjectRef对象,在Hermes上也有对应的实现;

8f6ce5c77dfe8c955509127baa90f019.png

提供方法判断是什么类型,以及快捷获取类型值,比如:

Bool isStr = value.isString()
facebook::jsi::String str = value.asString()
  • Object

Object对应就是JS的对象,基于Object派生Function以及Array和JSArrayBuffer,同样Object也提供很多方法获取和设置属性。‍

Runtime提供一个默认的全局对象global, 所有的JS逻辑均运行在默认的global之上。Object也提供很对方法获取属性,比如:

// 判断是否有该属性
bool hasProperty(Runtime& runtime, const char* name) const;// 获取属性值
Value getProperty(Runtime& runtime, const char* name) const;// 获取属性值并转化成object
Object getPropertyAsObject(Runtime& runtime, const char* name) const;
  • Function

对应JS的Function,提供静态方法创建Function:

static Function createFromHostFunction(Runtime& runtime,const jsi::PropNameID& name,unsigned int paramCount,jsi::HostFunctionType func);

提供实例方法调用:

auto func = Function::createFromHostFunction(...)
func.call(...)

同样还有Array,ArrayBuffer,HostObject等等。

通过Runtime,我们可以获取JS Object、Function,同时我们也可以创建JS Object、Function,注入给JS,这样就可以实现双向通信。

0695d94bd35f4ff20e9627faeceab06d.jpegHippy2.0架构分析

1)架构

e7ad1306ef9c1bc0f44eee8ae7c96a7d.png

包含三层:

和平台相关的能力扩展比如Module能力和UI组件,以及调用底层HippyCore的接口封装的Bridge和JS Executor层,该层在iOS和Android上分别使用OC和JAVA实现。‍

HippyCore层,通过napi对不同JS引擎的接口进行接口封装,抹平不同引擎的接口差异,让上层调用通过调用简单的接口实现复杂的能力,该层使用C++实现,跨平台。

前端JS SDK层,主要是定义了双向通信的方法函数跟上层进行通信以及功能处理。

另外还包括一些能力,基本是在hippycore层实现。比如C++ Modules, TurboModules等。

我们需要切换引擎,上下两层其实都不需要特别(大量)修改,核心就是在hippycore层,需要使用hermes将napi定义的接口全部实现一遍,以及同时实现现在已经有的Abilites。

  • napi

主要有几种概念:Engine:负责创建VM以及Scope;VM:负责创建管理Ctx,一个VM可以创建一个或者多个Ctx;Ctx:负责创建引擎实例,并封装操作引擎的接口供外部调用;CtxValue:负责封装不同引擎的JS Value;Scope:使用Ctx,执行Hippy基础初始化流程。‍

  • Scope

主要负责Hippy基础初始化流程,核心步骤如下:

513eaaeb1797d37c74b45be7d82665f9.png


注入Natives方法

通过给JS注入Native Function方法的方式,让JS可以直接调用终端方法;主要是常见的JS侧CallNative方法均通过此进行分发。

执行JS Native Source Code

Hippy将一部分基础JS SDK代码,通过脚本将JS代码转换成二进制集成在hippycore的C++代码里,在通过Ctx执行这些JS代码。好处是:解决C++ Module跟JS侧代码一致性问题(均使用C++形式加载调用);对于常用的基础JS的SDK代码,不用打包到基础包里,可以减少Common包大小,另外职责也分离。

其中包括C++ Module跟JS对象绑定,以及TurboModule和DynamicImport均在此步骤进行定义实现;

  • Abilities

C++ Module:不同于Native Module字符串消息映射和TurboModule HostObject的实现,C++ Module是将HippyCore里标记为导出的C++Module和其函数对应在前端生成一个名字一样的JS对象和方法。Hippy里常见的TimeModule,ContextifyModule均是如此实现。


TurboModule:前有NativeModule,后有C++Module,为什么还有TurboModle?

NativeModule好处是对于一些能力要分端去实现的,两端实现起来比较方便,但是其是通过字符串映射到终端方法的方式进行调用以及存在JS线程到NativeModule线程切换效率问题。‍

C++Module的好处就是在JS线程直接调用绑定JS对象和方法执行,效率高,但是暴露的Module是用C++实现,如果分发调用到Native侧,一个是要区分平台,第二个是分发到上层Java或者OC需要对应的类型转换。

为了解决上述问题,TuroboModule应运而生,兼具JS线程直接调用,并且不同平台可以分别实现自己的Turbo能力,关键是直接使用的引擎提供的HostObject方式实现,相较于C++Module 效率都更高。

Dynamic Import:动态导入能力,容许在JS侧动态加载远程或者本地JS代码,主要使用场景是对于分包加载,减少主包大小,提高业务加载包速度;最终实现也是通过C++Module ContextifyModule的LoadUntrustedContent方法来执行远端或者本地JS代码并返回给JS侧。

HippyCore异常处理JS引擎接口异常,不同引擎异常不同(JSI Exception);Native异常,主要是Native侧的代码调用以及JS方法注入实现异常。

JSC引擎和V8处理逻辑不太一样,JSC的JSI接口会将Exception通过参数传递出来,V8是通过在调用上下文初始化TryCatch对象,对异常进行捕获。

所以对于JSC的JS异常,只需要处理接口的Exception就行;V8处理TryCatch对象捕获的异常就可以。‍

JSValueRef js_error = nullptr;JSValueRef value_ref =JSObjectGetProperty(context_, global_obj, name_ref, &js_error);bool is_str = JSValueIsString(context_, value_ref);JSStringRelease(name_ref);
Native异常一般就是平台相关的异常,比如OC就是NSException,在双向通信以及各种JS接口注入实现处加Try-Catch进行捕获。

2)总结

通过以上架构分析,Hippy整个实现流程都已经变得非常清晰,我们可以使用Hermes的能力将上述能力均实现一下。

d13bd548e76526c434b5012515f356a0.jpegHermes接入对比


1)性能

基于已经上线的业务性能统计数据(数据取至12月12日),对比如下:

ee2ea2e9d0e0c3a17d237b8a410a6659.png

可以看到包加载执行耗时已经被彻底打下来了(70-80%幅度),进而极大降低了首帧耗时。

另外通过线上业务大盘整体耗时曲线图可以更直观看到效果(大部分业务没有全量,所以还会有持续下降的趋势):

8b9d026f285067efe07aa9af7d600476.png

7f3e2c3a36df5b4df9d3abd608ee4676.png


2)内存

在滑动相同的的List Item的情况下,Hippy Hermes和JSC的内存增量差别不大。根据官方文档介绍Hermes应该是略优于JSC的,所以这里不排除Hippy或者前端SDK还有优化空间。

3)Crash

Hippy的JSC相关的Crash率较高,比较难修改。Hermes也有一定的crash,但是从目前的对比来看,数量级较JSC少很多。以12月12日,iOS 13.4.0.5401版本的数据对比来看,Hermes的Crash率为JSC的50%,也就是说如果切换到Hermes上的话,相关引擎的Crash会下降一半。

9f820a059152055b1af17347b59ef9a8.png

JSC Crash关键词:jscctx/HippyJSCExecutor   Hermes Crash关键词:hermes/HippyHermesExecutor

93990084ba545f0f3784a98f439c9281.jpeg展望

目前Hermes已经在QB iOS版本上上线。业务接入成本非常低,无需修改一行代码,只需要打包的时候使用插件,输出Bytecode文件即可。接入上线的业务已经遍布信息流、阅读、商业、搜索等各个业务场景。

当然,还有很多事情可以持续做以持续提升性能: Android接入,对比V8性能,已经接近完成(对比V8,在低中端手机上有近50%的性能提升)。Hermes调试能力,可以使用Hermes在Chrome上调试JS代码。 基于Hermes的内存调试诊断工具。本文不展开赘述,欢迎各位开发者交流探索~

通过接入Hermes,可以让业务更多的关注在JS业务逻辑里,让前置SDK流程的耗时不再是性能瓶颈。希望本文能给你灵感。

回复“性能优化“,查看作者推荐的更多文章‍‍‍

腾讯工程师技术干货直达:

1、H5开屏从龟速到闪电,企微是如何做到的

2、内存泄露?腾讯工程师2个压箱底的方法和工具

3、全网首次揭秘:微秒级“复活”网络的HARP协议及其关键技术

4、万字避坑指南!C++的缺陷与思考(下)

c09a711bf4d7ac4d96515a9aece30d16.png

264c65a5f8a8fe72f32152503e0a2b22.png

点个 在看展示你的技术态度

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_239621.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

微导纳米科创板上市:市值125亿 无锡首富王燕清再敲钟

雷递网 雷建平 12月23日江苏微导纳米科技股份有限公司&#xff08;简称&#xff1a;“微导纳米”&#xff0c;股票代码为&#xff1a;“688147”&#xff09;今日在科创板上市。微导纳米此次发行4544.55万股&#xff0c;发行价为24.21元&#xff0c;募资总额为11亿元。微导纳米…

对Python的学习【如何查看路径和安装包】

1&#xff1a;怎么查看本地电脑的Python版本号及安装路径&#xff1a; 对于Windows平台&#xff0c;打开cmd 使用命令py -0p 【其中0是零】 显示已安装的 python 版本且带路径的列表&#xff0c;参见下图&#xff1a; 其中带星号*的为默认版本。 2:怎么查看python pip…

认识 Fuchsia OS

认识 Fuchsia OS 1 说明背景 1.1 基本信息 开发者: Google编程语言: C、C、Rust、Go、Python、Dart内核: Zircon运作状态: 当前源码模式: 开放源代码初始版本: 2016年8月15日支持的语言: 英语支持平台: ARM64、X86-64内核类别: 微内核 基于能力 实时操作系统许可证: BSD 3 c…

腾讯焦虑了,一向温文尔雅的马化腾也发脾气了

大家好&#xff0c;我是校长。昨天小马哥内部讲话在互联网上疯传&#xff0c;这应该是&#xff0c;腾讯这家公司创办以来&#xff0c;马化腾最焦虑也最外露的一次讲话了&#xff0c;重点大概涉及 3 大方面&#xff0c;8 大项内容&#xff1a;1、所有业务线 ROI 化&#xff0c;再…

该怎么选择副业,三条建议形成自己的副业思维

受经济环境的影响&#xff0c;许多年轻人觉得原来稳定的工作不那么稳定&#xff0c;看着周围的朋友因为企业破产和失业&#xff0c;生活变得没有信心&#xff0c;也想找到自己的副业&#xff0c;在紧急情况下赚更多的钱。所以&#xff0c;年轻人在选择副业时也面临着很多困惑&a…

LeetCode HOT 100 —— 581. 最短无序连续子数组

题目 给你一个整数数组 nums &#xff0c;你需要找出一个 连续子数组 &#xff0c;如果对这个子数组进行升序排序&#xff0c;那么整个数组都会变为升序排序。 请你找出符合题意的 最短 子数组&#xff0c;并输出它的长度。 思路 方法一&#xff1a;双指针 排序 最终目的是让…

2023春季招聘面试集锦:MYSQL数据库高频面试题

mysql索引的数据结构&#xff0c;各自优劣 索引的数据结构和具体存储引擎的实现有关&#xff0c;在MySQL中使用较多的索引有Hash索引&#xff0c;B树索引等&#xff0c; InnoDB存储引擎的默认索引实现为&#xff1a;B树索引。对于哈希索引来说&#xff0c;底层的数据结构就是…

SpringBoot:模块探究之spring-boot-starters

Spring Boot Starters 是一组方便的依赖描述符&#xff0c;您可以将它们包含在您的应用程序中。您可以获得所需的所有 Spring 和相关技术的一站式服务&#xff0c;而无需搜索示例代码和复制粘贴大量依赖项描述符。 例如&#xff0c;如果想使用 Spring 和 JPA 进行数据库访问&am…

前端小知识:文本分句、词、字(Intl.Segmenter)

5. 文本分字、词、句 参考文章&#xff1a; https://mp.weixin.qq.com/s/MLmi-Yoi9sez8-5DPtcBVw   官方文档&#xff08;构造参数&#xff09;&#xff1a; https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter/Segmenter   …

win环境mysql版本升级到5.7过程

win环境mysql版本升级到5.7过程&#xff0c;我win电脑里mysql版本是5.0&#xff0c;版本太老了&#xff0c;也不支持和nacos集成&#xff08;nacos至少需要5.6版本的mysql&#xff09;&#xff0c;思来想去还是要升级一下自己电脑的mysql版本&#xff0c;保守点升级到5.7吧&…

项目实战之旅游网(三)后台用户管理(下)

目录 一.查询用户角色 二.修改用户角色 三.修改用户状态 一.查询用户角色 一个用户可以有多个角色&#xff0c;我们也可以给某个用户分配某些角色&#xff0c;所以我们还需要新建一个实体类&#xff08;这个实体类需要放到bean下&#xff0c;因为这个实体类和数据据库不是对…

SpringCloud 网关组件 Zuul-1.0 原理深度解析

为什么要使用网关&#xff1f; 在当下流行的微服务架构中&#xff0c;面对多端应用时我们往往会做前后端分离&#xff1a;如前端分成 APP 端、网页端、小程序端等&#xff0c;使用 Vue 等流行的前端框架交给前端团队负责实现&#xff1b;后端拆分成若干微服务&#xff0c;分别…

独立开发变现周刊(第85期):一个会员服务的SaaS,月收入2万美金

分享独立开发、产品变现相关内容&#xff0c;每周五发布。目录1、Obsidian Canvas&#xff1a;一个无限的空间来构建你的想法2、message-pusher: 搭建专属于你的消息推送服务3、Careerflow LinkedIn: 40倍提升你的工作机会4、vue-pure-admin: 一款开源后台管理系统5、一个提供会…

【HarmonyOS】调测助手安装失败10内部错误

关于鸿蒙开发通过应用调测助手向watch gt 3 手表安装hap时报错。 问题背景&#xff1a; 鸿蒙开发&#xff0c;使用新建工程的helloworld 没有其他修改&#xff0c;生成hap包。然后通过应用调测助手向watch gt 3 手表安装hap时提示 安装失败:10.内部错误。 Sdk&#xff1a; a…

基于VUE学生选课管理系统

开发工具(eclipse/idea/vscode等)&#xff1a;idea 数据库(sqlite/mysql/sqlserver等)&#xff1a;mysql 功能模块(请用文字描述&#xff0c;至少200字)&#xff1a; 一、登录注册模块: 1.学生&#xff0c;教师&#xff0c;管理员三个角色&#xff08;同一时刻&#xff0c;账户…

WSL2的安装、应用

WSL2的安装、应用WSL安装、升级常用命令WSL导入导出其他 - 图形界面、虚拟化WSL安装、升级 win10系统上开启WSL参考如下&#xff0c;我先是安装了WSL1&#xff0c;之后又升级到WSL2的。关键是一些Win10上电配置&#xff0c;之后在windows应用商店下载ubuntu即可。 win10上lin…

Python基础(十八):学员管理系统应用

文章目录 学员管理系统应用 一、系统简介 二、步骤分析 三、需求实现 1、显示功能界面 2、用户输入序号&#xff0c;选择功能 3、根据用户选择&#xff0c;执行不同的功能 4、定义不同功能的函数 学员管理系统应用 一、系统简介 需求&#xff1a;进入系统显示系统功能…

跨域问题以及解决跨域问题的vue-cli解决方案

跨域问题 写项目前要问后端,接口支持跨域吗? 支持就不会出现问题,不支持就需要解决跨域问题 1.如何判断一个浏览器的请求是否跨域&#xff1f; 在A地址&#xff08;发起请求的页面地址&#xff09;向B地址&#xff08;要请求的目标页面地址&#xff09;发起请求时&#xff…

Java环境配置——Linux 安装JDK

注意这是用普通用户登录后&#xff0c;单独设置用户的java环境变量&#xff0c;非root用户 root用户的编辑命令是 vi /etc/profile 下载安装包 创建java目录 mkdir java 进入目录 cd java 上传安装包 将jdk-8u161-linux-x64.tar.gz上传到java目录 配置环境变量 解压安…

leetcode——155. 最小栈

leetcode——155. 最小栈&#x1f50d;题目详情&#x1f914;解题思路&#x1f4bb;代码实现&#x1f4ac;总结&#x1f440;先看这里&#x1f448; &#x1f600;作者&#xff1a;江不平 &#x1f4d6;博客&#xff1a;江不平的博客 &#x1f4d5;学如逆水行舟&#xff0c;不进…