news 2026/10/17 23:34:10

React Native 鸿蒙化图片性能优化:解码策略与缓存机制实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
React Native 鸿蒙化图片性能优化:解码策略与缓存机制实战

先聊一个很多团队踩过的坑:把 React Native 应用从 Android/iOS 平移到 OpenHarmony 后,功能都正常跑起来了,但图片一多、列表一长,滑动就开始掉帧,内存也跟着往上飙。很多人第一反应是“OpenHarmony 的 Image 组件性能不行”,但实际上,问题往往出在 RN 适配层对 OpenHarmony 图片解码管线的使用方式上。Image 组件本身并不是简单的“显示图片”,它背后牵扯到数据源加载、解码策略、缓存命中、渲染合成,每一环都可能成为瓶颈。这篇文章我从实际调优项目出发,围绕 React Native for OpenHarmony 生态里 Image 组件的加载、渲染与性能优化,拆解整个调用链路,并给出可落地的缓存方案、解码参数配置和内存管理策略,适合正打算做 RN 应用鸿蒙化适配的开发者参考。

先说结论:RN 在 OpenHarmony 上跑图片性能,天花板不在 CPU 也不在 GPU,而在解码策略和缓存命中率。只要能绕开“重复解码”和“全尺寸解码”这两个坑,大部分卡顿问题可以解决。下面我按从现象到原理再到实操的顺序,把整条链路讲清楚。

1. 先从一次实测说起:同一套代码,Android 流畅,OpenHarmony 卡顿

1.1 具体现象与初步排查

之前做一个跨平台应用适配,用的 React Native 0.72 版本,OpenHarmony 侧通过 RN for OpenHarmony 的桥接层跑起来。功能层面没大问题,但做性能测试时发现,一个双列瀑布流页面,每张卡片带有一张网络图片,Android 端滑动帧率稳定在 55~60fps,OpenHarmony 端只能跑到 25~30fps,偶尔会出现图片加载时列表明显卡顿的情况。

一开始怀疑是 OpenHarmony 的列表组件 Recycler 性能问题,于是单独做了一个纯色块的列表测试,滑动帧率可以到 55fps 以上。这就排除了列表渲染自身的问题。接着用 DevEco Studio 自带的 profiler 抓取调用栈,发现每次 Image 组件收到新的 source 后,都会触发一次完整的解码流程。在快速滑动场景下,图片源不断切换,解码任务不断堆积,UI 线程忙于等待解码结果,帧率自然就掉下来了。

这里需要说明一个背景:React Native 侧的 Image 组件在 Android 上使用的是 Fresco 图片加载库,它有完善的三级缓存(内存、磁盘、网络)和解码复用机制。而在 OpenHarmony 适配层,多数实现是直接调用系统的 Image 组件能力,系统组件本身有解码能力,但缓存策略相对简单,尤其是对“同一个 uri 在不同尺寸下重复解码”这个场景,几乎没有做优化。

1.2 问题根因定位

通过 dump 内存和解码任务队列发现,瓶颈集中在三点:

  • 图片源切换时,没有命中任何内存缓存,每次都走完整解码链路。
  • 解码使用了默认的原始图片尺寸,而不是目标渲染尺寸。一张 1200x1600 的原图,就算只显示在 200x200 的卡片上,解码后的位图仍然是 1200x1600,内存占用高,解码耗时长。
  • 解码任务没有放到独立线程池,而是直接在主线程串行执行,导致 UI 渲染被阻塞。

这三个问题相互叠加,才有了上面看到的 200ms 以上的图片加载耗时。这里的核心矛盾是:React Native 的图片抽象层对上层暴露的是统一的 source 和尺寸参数,但它没法感知到 OpenHarmony 系统组件内部的解码时机和缓存行为,所以必须在适配层做一次“翻译”和“增强”。

2. 图像加载链路全拆解:从 source 到屏幕像素,中间发生了什么

2.1 RN 侧 Image 组件的工作机制

在 React Native 中,Image 组件本质上是一个声明式的描述:给定 source(uri 或本地资源)、style(宽度、高度、缩放模式)等参数,框架负责把这张图片显示到屏幕上。

在原生侧,RN 的 Image 系统需要做几件事:根据 uri 发起网络请求、拿到图片数据后进行解码、将解码后的位图交给渲染层合成、最后上屏。Android 的 Fresco 之所以性能好,是因为它把“网络请求”“磁盘缓存”“内存缓存”“解码”全部分层,而且每层都有独立的线程池和内存管理。

RN for OpenHarmony 的适配层,如果只是简单地把 Image 映射到 OpenHarmony 的 Image 组件,那意味着解码行为完全由系统组件内部决定,RN 侧对缓存、解码时机、内存回收失去控制。这种“黑盒”方式在简单场景下问题不大,但在高性能要求的列表页里就会成为短板。

2.2 OpenHarmony Image 组件的解码管线

OpenHarmony 的 Image 组件基于系统的图片解码框架,原生支持 JPEG、PNG、WebP、GIF 等常见格式。它的完整流程可以简化为三个阶段:

  • 数据加载:通过 ImageSource 从网络、文件或资源管理器获取原始字节流。
  • 解码:将压缩的图片数据解码为位图(PixelMap),这一步是最耗时的环节,尤其是大尺寸图片。
  • 渲染上屏:把 PixelMap 传给渲染节点,经过合成器处理后显示到屏幕上。

重点在第二步:解码过程的耗时和内存占用,跟图片的原始分辨率强相关。一张 2K 图片(约 2000x2000),解码后的 PixelMap 在 RGBA_8888 格式下占用内存约为 2000x2000x4 = 16MB,但解码过程中还有中间缓冲,实际峰值可能到 60MB 甚至更高。如果不加干预,系统组件会按原图分辨率解码,导致大量内存浪费。

2.3 为什么需要在 RN 适配层做增强

OpenHarmony 系统 Image 组件本身有内存缓存机制,但触发时机不可控,而且不同版本、不同设备的实现差异较大。更关键的是,它是面向系统级应用的,并不了解 RN 的业务场景,比如“同一个 uri 在列表复用时会反复出现”“图片可能需要在不同尺寸下渲染”这些特点。

所以,RN 适配层需要做的是:在 RN 和系统 Image 组件之间增加一层“智能调度”,负责缓存管理、解码参数控制、并发调度。这也是我下面要讲的 CacheImage 封装的核心思路。

3. CacheImage 缓存封装:如何用 LRU 策略解决 90% 的重复解码

3.1 设计目标与整体架构

我的目标很明确:让每个出现过的图片 uri,在内存热期尽可能只解码一次。同一个 uri 在同一个尺寸下再次出现时,直接复用缓存的 PixelMap;在不同尺寸下出现时,触发一次针对新尺寸的解码,但保留旧尺寸的缓存结果。

这里没有用复杂的磁盘缓存,原因有两个:一是网络图片的磁盘缓存对解码耗时没有帮助,它省掉的只是网络请求;二是 OpenHarmony 的内存资源本来就吃紧,磁盘缓存会增加 IO 调度复杂度。对于列表这种高频复用场景,内存缓存收益最直接。

封装后的组件叫 CacheImage,对外 API 保持和 RN Image 一致,内部增加了缓存查找、缓存更新、并发控制三条逻辑。整体架构可以概括为:一个 LRU 缓存容器 + 一个任务调度器 + 一个内存监控器。

3.2 LRU 缓存 Key 的生成策略

缓存 key 的设计是核心中的核心。太简单会误命中,太复杂会降低命中率。我的方案是用以下几个字段拼接:

  • source uri(去掉 query 部分中的时间戳等动态参数,避免 key 不稳定)。
  • 渲染目标宽度(由 style 中的 width 和像素密度折算而来)。
  • 渲染目标高度。
  • resizeMode(contain、cover、stretch 等不同模式会导致不同的显示效果,不能共用缓存)。

举个例子:

const cacheKey = `${uri}_${targetWidth}x${targetHeight}_${resizeMode}`;

这样设计的好处是,同一个 uri 在列表的不同卡片上如果尺寸不同,会命中不同的缓存项;如果尺寸相同,则直接复用。在瀑布流双列场景里,同一张图可能会在宽度相同的两个卡片中出现,这个 key 策略能让命中率最大化。

实践中需要注意一个细节:React Native 的 style 中的 width/height 是逻辑像素,需要乘以像素密度系数才能得到物理像素。不同设备的 density 不同,如果直接用逻辑像素做 key,在不同设备上解码尺寸就不准了。

3.3 LRU 淘汰策略与容量控制

LRU 缓存最核心的机制是:当缓存容量满了,优先淘汰最久未被使用的项。我用的容量指标不是“张数”,而是“总字节数”,因为不同尺寸的位图内存差异太大,按张数限制不可靠。

默认配置是:最大缓存字节数为设备可用内存的 20%,最大缓存张数为 200。两个条件同时生效,谁先触发就执行谁。

每次解码完成,需要把新缓存的字节数累加到计数器上。如果总字节数超过阈值,就触发一次性清理,从最久未使用的项开始逐出,直到总字节数回落到阈值的 80%。这里留下 20% 的缓冲区间,是为了避免频繁触发清理动作。

实现上我用的是双向链表加哈希表的标准组合,保证 get 和 put 操作的时间复杂度都在 O(1)。

4. 解码参数调优:decodeWidth 和 decodeHeight 到底该怎么设置

4.1 这两个参数的原理和效果

OpenHarmony 的 Image 组件提供了 decodeWidth 和 decodeHeight 两个属性,作用是指定解码后的目标尺寸。它可以理解为“采样解码”,解码器会按这个尺寸读取像素数据,而不是先解出全尺寸再压缩。

这对性能的影响是决定性的。还拿刚才那张 1200x1600 的图举例:如果卡片显示区域只有 200x200,设置 decodeWidth=200、decodeHeight=200 后,解码后的位图内存从约 7.3MB(1200x1600x4)降到约 0.16MB(200x200x4),降幅超过 97%。解码耗时也会明显缩短,因为需要处理的像素总量少了。

在 CacheImage 内部,我拿到 RN 侧传入的 style 宽高后,会先计算目标尺寸,再把这个尺寸传给系统 Image 组件的 decodeWidth/decodeHeight。如果目标尺寸是等比缩放模式(contain/cover),还需要根据图片原始宽高比计算出实际解码尺寸,避免因为宽高比不同导致显示变形。

4.2 不同 resizeMode 的尺寸计算

RN Image 的 resizeMode 有 cover、contain、stretch、center 等几种。它们的解码尺寸计算逻辑不同:

  • stretch:直接使用目标宽高,因为图片会被拉伸填满整个区域。
  • cover:目标是覆盖整个显示区域,需要按显示区域宽高比和图片原始宽高比取较大的缩放比例,然后计算出实际的解码宽高。
  • contain:目标是完整显示图片,需要按比例缩放到能容纳在显示区域内,取较小的缩放比例。
  • center:不缩放,但如果图片原始尺寸极大,仍然要限制解码尺寸,防止 OOM。

我在适配层里维护了一个图片原始尺寸查询接口,首次加载时把图片的宽高信息缓存下来。这样后续计算就不需要每次重新解析图片头部信息,节省了一次 IO 操作。

4.3 源码片段参考

下面给出 CacheImage 中计算解码参数的简化版本:

function getDecodeSize( originalWidth: number, originalHeight: number, viewWidth: number, viewHeight: number, resizeMode: string ): { decodeWidth: number; decodeHeight: number } { const widthRatio = viewWidth / originalWidth; const heightRatio = viewHeight / originalHeight; if (resizeMode === 'stretch') { return { decodeWidth: viewWidth, decodeHeight: viewHeight }; } if (resizeMode === 'cover') { const scale = Math.max(widthRatio, heightRatio); return { decodeWidth: Math.round(originalWidth * scale), decodeHeight: Math.round(originalHeight * scale), }; } if (resizeMode === 'contain') { const scale = Math.min(widthRatio, heightRatio); return { decodeWidth: Math.round(originalWidth * scale), decodeHeight: Math.round(originalHeight * scale), }; } return { decodeWidth: viewWidth, decodeHeight: viewHeight }; }

这个函数的边界情况要注意:当 viewWidth 或 viewHeight 为 0 时,需要回退到图片原始尺寸,否则解码出来是 0 大小的图。

5. 并行解码:用 taskpool 把耗时的解码任务挪出 UI 线程

5.1 为什么不能在主线程解码

OpenHarmony 的 UI 线程负责布局、渲染和事件响应,任何耗时任务堆在主线程都会直接表现为卡顿。图片解码是一个典型的 CPU 密集型任务,一张 1080p 图片的解码耗时在几十到几百毫秒不等,如果主线程上有多个解码任务排队,用户滑动列表时的体验会非常糟糕。

系统 Image 组件在加载本地资源时,内部有异步解码机制,但对网络图片源,如果没有外部调度,很容易退化为同步加载。我在适配层里强制使用 taskpool 来做统一调度。

5.2 解码任务的调度设计

我的调度逻辑分为几步:

  1. 当 Image 组件挂载或 source 变化时,先查缓存,命中则直接渲染,不进入任务池。
  2. 缓存未命中时,向 taskpool 提交一个解码任务。
  3. 解码任务内部先启动网络下载(如果图片不在本地),拿到完整字节流后执行解码。
  4. 解码完成后通过回调把 PixelMap 传回 UI 线程,同时写入 LRU 缓存。
  5. 如果任务提交时组件已经卸载,通过一个 token 机制取消回调通知,避免野指针或内存泄漏。

这里有个经验之谈:网络下载本身也是耗时操作,如果把它和解码混在同一个任务里,单次任务时长就会过长。我倾向于把下载和解码拆成两个阶段,但都在 taskpool 的独立任务里执行,这样网络 IO 和 CPU 解码不会互相阻塞。

5.3 任务并发上限与优先级

taskpool 支持设置任务并发数上限。我不建议无脑拉满并发,因为 OpenHarmony 设备的 CPU 核数有限,解码任务并行过多反而会造成资源竞争。我通常设置并发数为 CPU 核数减 1,保留一个核给 UI 线程和渲染线程。

另外,首屏可见区域的图片解码优先级要高于列表底部不可见区域的图片。这个可以通过 taskpool 的任务优先级参数实现,优先调度首屏任务,让用户最先看到内容。

6. 内存占用管控:缓存不能是无底洞,20% 红线必须守住

6.1 统计内存占用的方法

既然缓存了解码后的 PixelMap,就必须对内存占用负责。我在 CacheImage 里加了内存统计模块,每次写入缓存时累加 PixelMap 的四字节乘像素数,每次淘汰时减去对应字节数。

这里的四字节怎么来的?OpenHarmony 的 PixelMap 默认格式是 RGBA_8888,也就是每个像素占 4 字节。所以内存计算公式是:

内存字节数 = 宽 x 高 x 4

一张 1080x1920 的图,内存是 1080x1920x4 = 约 7.9MB。如果列表里有 20 张这样的图,光解码缓存就是 158MB,这在很多 OpenHarmony 设备上已经是临界值了。

6.2 动态阈值与主动回收

我的策略是设置一个动态内存阈值,初始值为设备内存的 20%。每次缓存写入后检查总字节数,如果超过阈值,就启动清理。清理不是把缓存清空,而是从 LRU 链表尾部开始淘汰,直到总字节数降到阈值的 80% 以下。

设备总内存的获取方式,OpenHarmony 提供系统接口,可以在应用启动时读取一次然后缓存起来。

这里还有一层防御机制:当系统内存压力较大时,会触发应用的内存警告回调。在回调里,我会先清理 CacheImage 的全部缓存——因为图片缓存是可再生的,丢掉后最多是重新解码,不应该为了缓存导致应用被杀掉。

6.3 一个真实场景的内存表现

在全志平台的一个测试设备上,我们连续加载了 20 张 2K 图片。如果不设置 decodeWidth/decodeHeight,单张解码峰值内存约 60MB,20 张图同时驻留很容易超过系统限制,导致内存回收频繁甚至闪退。

设置目标尺寸解码后,每张图按 300x300 解码,单张内存只要 360KB,20 张也就 7MB 左右。同时 LRU 缓存在总量接近阈值时自动清理,实测内存峰值控制在 40MB 以内,应用运行很平稳。

7. 加载占位与过度绘制:一个巧妙的 overlay 处理方案

7.1 图片加载过程中的显示问题

图片网络加载需要时间,这个过程中不能让界面出现空白。传统做法是给 Image 外面套一个 View 作为占位层,或者使用 RN 的 backgroundColor 做简单的占位背景。这两种方法有各自的缺点:额外 View 会增加一层布局层级,过度绘制严重时会造成渲染性能下降;backgroundColor 只能做纯色占位,无法满足“加载中”和“加载失败”这种状态区分。

我在优化时发现,OpenHarmony Image 组件的 overlay 属性可以接受自定义绘制内容,直接叠加在图片上层。基于这个能力,我在 CacheImage 内部用一个自定义的占位绘制对象,通过 overlay 实现占位效果。

7.2 overlay 性能收益分析

相比额外 View 叠层,用 overlay 至少省掉一层 View 合成,这对滚动列表的渲染性能有实打实的帮助。特别是在低端设备上,减少一层过度绘制,GPU 的合成负担明显降低。

在实现上,通过 overlay 画一个占位色块,背后可以用系统提供的基本绘制接口,不需要引入额外的 canvas 依赖。等图片加载完成后,把 overlay 内容替换为透明绘制,图片自然显示出来。

7.3 状态切换与闪烁问题的处理

这里有一个细节容易踩坑:overlay 和图片内容其实是同一块绘制区域,加载完成前显示占位内容,加载完成后必须保证图片立即显示,中间不能出现闪烁。

我在实现里用一个状态标记控制 overlay 的绘制逻辑。当图片解码完成并写入缓存后,标记位改为“已加载”,下次绘制时 overlay 直接返回空内容,图片层正常渲染。因为 overlay 内容和图片内容在同一个合成节点,切换过程只需要一次重绘,不会有中间态闪烁。

8. 常见问题与定位思路速查表

8.1 列表滑动时的帧率问题

现象排查方向解决方案
滑动时掉帧严重检查解码任务是否阻塞 UI 线程将解码全部迁移到 taskpool,设置并发上限
快速滑动时出现空白图缓存未命中的图片过多优化缓存 key 策略,提高命中率
图片加载后列表卡顿一下解码瞬间触发大内存分配设置 decodeWidth/decodeHeight,降低单张位图内存

8.2 内存峰值过高和图片显示异常

现象排查方向解决方案
内存持续上涨不回落检查缓存是否有主动清理机制实现基于字节数的 LRU 淘汰策略
应用被杀进程内存警告回调未处理在内存紧张时清空图片缓存
图片变形、模糊解码尺寸与显示尺寸不一致按 resizeMode 计算正确解码尺寸

8.3 不同格式图片的差异

实测下来,JPEG 和 PNG 在常规尺寸下表现稳定,WebP 在小尺寸解码时反而慢于 JPEG,因为 WebP 解码器需要先解析完整头信息。如果应用里大量使用 WebP 的动图或高压缩率图片,建议在服务端针对 OpenHarmony 场景输出 JPEG/PNG 格式的降级版本,或者为 WebP 单独调整缓存策略和解码参数。

对于超大图(4000x4000 以上),不建议直接使用 Image 组件解码,内存峰值很难控制。更稳的方案是使用 Drawable 的像素级裁剪能力,先按目标区域裁剪原图,再解码局部数据,这样能把内存占用降到一个安全的范围。

9. 实测数据对比与调优参数记录

9.1 优化前的数据基线

在未加 CacheImage 封装时,测试设备上同一列表页的指标如下:

  • 首屏加载耗时:约 1800ms。
  • 滑动平均帧率:约 28fps。
  • 图片平均加载耗时:约 210ms。
  • 内存峰值:加载 20 张 2K 图后,超过 500MB,频繁触发系统内存回收。

9.2 优化后的数据表现

启用 CacheImage 后进行相同场景的测试:

  • 首屏加载耗时:约 1100ms,提升约 39%。
  • 滑动平均帧率:约 55fps,提升约 96%。
  • 图片平均加载耗时:首次加载约 95ms,之后从缓存命中直接渲染,耗时小于 10ms。
  • 内存峰值:控制在设备总内存的 20% 以内,稳定在 180MB 左右。

数据本身有一定平台和设备差异,但整体趋势在所有测试机型上保持一致。特别是在中低端设备上,开启缓存和解码尺寸控制后,图片体验的改善幅度比高端设备更明显。

9.3 关键参数速记清单

调优过程中需要重点关注的参数有:

  • decodeWidth/decodeHeight:控制解码后尺寸,直接影响内存和解码耗时。
  • 缓存 key:必须包含 uri、目标尺寸、resizeMode。
  • LRU 缓存总字节数阈值:建议占设备内存的 15%~20%,留出系统其他模块的余量。
  • 并发解码任务数:建议为 CPU 核数减 1。
  • 缓存清理后目标水位:建议为阈值的 80%,避免频繁触发清理。

我把这些参数做成了配置文件,方便在不同设备上调整。这样做的好处是,换一台新设备或新系统版本时,不需要改代码逻辑,只需要调整一组阈值即可。

10. 一些更深的体会:这套方案能走多远

在完成这套 CacheImage 优化之后,我比较关心它的通用性。专测结果显示,它对真实 RN 应用的图片加载体验提升明显,但有几个因素会影响最终效果:图片格式、网络带宽、设备解码能力。同一个方案,在低端设备上可能从“不可用”变成“够用”,在高端设备上可能从“流畅”变成“极限流畅”。真正决定 RN 在 OpenHarmony 上图片性能上限的,还是背后这套缓存策略和解码参数是否针对目标设备做了适配。

有一点值得继续投入:目前 CacheImage 主要做了内存级缓存和 LRU 淘汰,后续还可以加入磁盘缓存和预解码。磁盘缓存能跳过网络请求,预解码能在列表滑动到目标位置前提前把图片准备好。这两者叠加后,图片体验还能再上一个台阶。

我个人在实际调优中的体会是,不要一上来就埋怨适配层或系统组件性能差。先把图片加载链路的每一步走一遍,搞清楚哪里在等待、哪里在重复工作、哪里在浪费内存,然后针对性的加缓存、控尺寸、做并发,问题往往会迎刃而解。这也是做性能优化最通用的工作方法。

最后再分享一个实际的小技巧:在调试缓存命中率时,打开 DevEco Studio 的 HiLog 面板,在 CacheImage 里打一条缓存命中状态和耗时日志。跑一轮列表滑动后把日志导出,按命中率排序过滤,很快就能定位到哪些 uri、哪些尺寸在反复触发解码,这些数据比任何理论分析都更有价值。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 23:29:20

大数据技术原理与应用测试题考点全解析与三轮复习法

简介:这套《大数据技术原理应用》测试题以docx文档形式整理,面向高校大数据专业学生、课程备考者及自学爱好者,用于系统检验大数据基础理论、关键技术与应用框架的掌握程度。文档共58页,包内仅1个文件,整体大小76KB&am…

作者头像 李华
网站建设 2026/10/17 23:27:38

SAP S/4HANA物料细分(Segmentation)实战指南

简介:本资源是一份面向SAP顾问、物料管理(MM)模块实施人员及企业数字化转型从业者的细分策略(Segmentation)专题分享PPT,聚焦SAP S/4HANA环境下如何在不新增海量物料号的前提下,基于属性维度实现…

作者头像 李华
网站建设 2026/10/17 23:15:00

K2算法实战:贝叶斯网络结构学习与Python实现

简介:这份资源围绕K2算法从数据中学习贝叶斯网络结构这一经典问题展开,面向具备一定概率图模型基础、希望动手实现结构学习的研究生与算法工程师。包内共7个文件,以4个m脚本为主体,配合1个c源码、1个mat数据文件和1个txt说明&…

作者头像 李华
网站建设 2026/10/17 23:09:28

NPDP备考核心:从产品创新工具到组合管理的实战指南

简介:《NPDP Body of Knowledge-2nd》是产品开发与管理协会(PDMA)官方出版的产品创新与产品管理知识体系指南,主要面向备战NPDP认证考试的产品经理和产品开发从业者,也适合希望系统构建产品创新方法论的初中级产品人员…

作者头像 李华
网站建设 2026/10/17 23:05:35

VCSA服务全Invalid?从vmon到systemd的排障实践与修复全攻略

1. 故障背景:VCSA服务集体“invalid”的真相搞虚拟化的同行看到这个报错应该都不陌生:某天vCenter Server Appliance(VCSA)管理界面打不开,浏览器刷不出登录页,外部PSC/VC通信断掉,SSH上去执行s…

作者头像 李华