Android中Service在新进程中的启动流程3

       

目录

1、AMS调用客户端onCreate前的准备工作

2、AMS调用客户端onCreate方法

3、AMS调用客户端的onBind方法

4、AMS调用客户端onStart前的准备

5、AMS调用客户端onStart方法


        还是先放上Service启动流程概览图,如下:

        上一篇文章, 我们分析到了第五步scheduleCreateService,该方法位于AMS的realStartServiceLocked方法调用中,我们说过当新进程启动之后,会通过AMS调用attachApplication,该方法通过最后调用了realStartServiceLocked,所以我们继续从该方法开始分析,代码如下:

    private final void realStartServiceLocked(ServiceRecord r,ProcessRecord app) throws RemoteException {//...//1、保存相关变量r.app = app;r.restartTime = r.lastActivity = SystemClock.uptimeMillis();app.services.add(r);//2、仔细看看?bumpServiceExecutingLocked(r, "create");updateLruProcessLocked(app, true, true);boolean created = false;try {//3、调用客户端的scheduleCreateService方法ensurePackageDexOpt(r.serviceInfo.packageName);app.thread.scheduleCreateService(r, r.serviceInfo);r.postNotification();created = true;} finally {//...}//4调用绑定方法requestServiceBindingsLocked(r);//5、设置相关变量if (r.startRequested && r.callStart && r.pendingStarts.size() == 0) {r.lastStartId++;if (r.lastStartId < 1) {r.lastStartId = 1;}r.pendingStarts.add(new ServiceRecord.StartItem(r, r.lastStartId, null, -1));}//6、调用onStartsendServiceArgsLocked(r, true);}

        这里再深入分析下该方法,主要涉及以下几点:

  1. r.app = app:把ProcessRecord保存到ServiceRecord中,表示服务ServiceRecord服务r运行于ProcessRecord对象app中。app.services.add(r):把服务ServiceRecord服务对象r保存到ProcessRecord对象的services集合中,表示ProcessRecord进程中运行了哪些服务。
  2. bumpServiceExecutingLocked(r, "create"):这里待会分析,主要是调用服务onCreate前,AMS需要做些工作。
  3. scheduleCreateService:该方法最后会调用客户端服务的onCreate方法。
  4. requestServiceBindingsLocked(r):该方法最后会调用客户端服务的onBind方法。
  5. 调用客户端onStart之前的一些准备工作。
  6. 调用客户端的onStart

        下面我们一步步分析以上几点(第一点以及比较明确,不再说明)

1、AMS调用客户端onCreate前的准备工作

        我们知道在服务的onCreate方法中执行太久会导致系统弹窗ANR,这是如何实现的呢?玄机就在这里,我们分析下调用bumpServiceExecutingLocked(r, "create"),代码如下:

    private final void bumpServiceExecutingLocked(ServiceRecord r, String why) {//...long now = SystemClock.uptimeMillis();if (r.executeNesting == 0 && r.app != null) {if (r.app.executingServices.size() == 0) {Message msg = mHandler.obtainMessage(SERVICE_TIMEOUT_MSG);msg.obj = r.app;mHandler.sendMessageAtTime(msg, now+SERVICE_TIMEOUT);}r.app.executingServices.add(r);}r.executeNesting++;//主要用于计时,看服务是否允许超时,超时则会ANRr.executingStart = now;}

        发送了消息SERVICE_TIMEOUT_MSG,延时SERVICE_TIMEOUT,时间是20s;所以onCreate执行的时间最多及时20s,我们看看超时的处理,也是位于AMS中,跟踪到逻辑如下:

    void serviceTimeout(ProcessRecord proc) {String anrMessage = null;synchronized(this) {if (proc.executingServices.size() == 0 || proc.thread == null) {return;}long maxTime = SystemClock.uptimeMillis() - SERVICE_TIMEOUT;Iterator<ServiceRecord> it = proc.executingServices.iterator();ServiceRecord timeout = null;long nextTime = 0;while (it.hasNext()) {ServiceRecord sr = it.next();if (sr.executingStart < maxTime) {timeout = sr;break;}if (sr.executingStart > nextTime) {nextTime = sr.executingStart;}}if (timeout != null && mLruProcesses.contains(proc)) {Slog.w(TAG, "Timeout executing service: " + timeout);anrMessage = "Executing service " + timeout.shortName;} else {Message msg = mHandler.obtainMessage(SERVICE_TIMEOUT_MSG);msg.obj = proc;mHandler.sendMessageAtTime(msg, nextTime+SERVICE_TIMEOUT);}}if (anrMessage != null) {appNotResponding(proc, null, null, anrMessage);}}

         遍历ProcessRecord对象中的集合executingServices,看是否有超时的服务,如果有最后会调用appNotResponding,也就弹出了ANR弹窗。

2、AMS调用客户端onCreate方法

        现在来看看app.thread.scheduleCreateService(r, r.serviceInfo)如何调用到客户端的onCreate方法呢。

        我们知道该方法会进入到ActivityThread的scheduleCreateService方法(严格来说是ApplicationThread)中,代码如下:

        public final void scheduleCreateService(IBinder token,ServiceInfo info) {CreateServiceData s = new CreateServiceData();s.token = token;s.info = info;queueOrSendMessage(H.CREATE_SERVICE, s);}

        发送了个CREATE_SERVICE消息,该消息的处理如下:

    private final void handleCreateService(CreateServiceData data) {//...LoadedApk packageInfo = getPackageInfoNoCheck(data.info.applicationInfo);Service service = null;try {//1 加载服务类java.lang.ClassLoader cl = packageInfo.getClassLoader();service = (Service) cl.loadClass(data.info.name).newInstance();} catch (Exception e) {//...}try {//2 创建ContextImpl对象ContextImpl context = new ContextImpl();context.init(packageInfo, null, this);Application app = packageInfo.makeApplication(false, mInstrumentation);context.setOuterContext(service);service.attach(context, this, data.info.name, data.token, app,ActivityManagerNative.getDefault());//3 调用服务的onCreate方法service.onCreate();mServices.put(data.token, service);try {//4 服务执行完通知AMSActivityManagerNative.getDefault().serviceDoneExecuting(data.token, 0, 0, 0);} catch (RemoteException e) {// nothing to do.}} catch (Exception e) {//....}}

         主要有以下几个关键点:

  1. 通过loadClass加载我们实现的服务类,比如MyService。
  2. 创建ContextImpl对象,所以我们在服务Service里面调用的相关Context方法最终都是由这里创建的ContextImpl对象实现的。
  3. 调用服务的onCreate方法,这是我们需要实现的服务生命周期方法。
  4. 服务执行完onCreate方法后调用serviceDoneExecuting(
                            data.token, 0, 0, 0)通知AMS。

        主要看看第四点做了啥,serviceDoneExecuting的实现位于AMS中,代码如下:

    public void serviceDoneExecuting(IBinder token, int type, int startId, int res) {synchronized(this) {//...ServiceRecord r = (ServiceRecord)token;boolean inStopping = mStoppingServices.contains(token);if (r != null) {if (r != token) {//...return;}//1、不会走这里if (type == 1) {//...}final long origId = Binder.clearCallingIdentity();//2serviceDoneExecutingLocked(r, inStopping);Binder.restoreCallingIdentity(origId);} else {//...}}}

        参数type为0,所第一处不会进入;继续调用serviceDoneExecutingLocked,代码如下:

 

    public void serviceDoneExecutingLocked(ServiceRecord r, boolean inStopping) {//...r.executeNesting--;if (r.executeNesting <= 0 && r.app != null) {//移除正在执行的服务r.app.executingServices.remove(r);if (r.app.executingServices.size() == 0) {//1mHandler.removeMessages(SERVICE_TIMEOUT_MSG, r.app);}if (inStopping) {//移除已经停止的服务mStoppingServices.remove(r);r.bindings.clear();}updateOomAdjLocked(r.app);}}

        标记1处移除了延时消息SERVICE_TIMEOUT_MSG ,而该消息正是执行方法bumpServiceExecutingLocked时post的,这里移除掉就不会有ANR产生了。

        所以我们是不是已经清楚了为啥Service的onCreate方法耗时太久会产生ANR呢?其实正是因为在调用客户端Service的onCreate之前,AMS悄悄的post了一个延时消息SERVICE_TIMEOUT_MSG,时间是20s,要是客户端的onCreate在20s内执行完了,就会把延时消息移除掉,这样就不会产生ANR了,反之则AMS会弹窗ANR。

3、AMS调用客户端的onBind方法

        我们继续分析,接下来是requestServiceBindingsLocked方法调用,代码如下:

    private final void requestServiceBindingsLocked(ServiceRecord r) {Iterator<IntentBindRecord> bindings = r.bindings.values().iterator();while (bindings.hasNext()) {IntentBindRecord i = bindings.next();if (!requestServiceBindingLocked(r, i, false)) {break;}}}

         IntentBindRecord代表了绑定到该服务的客户端,这里遍历绑定了的对象,然后调用方法requestServiceBindingLocked,代码如下:

    private final boolean requestServiceBindingLocked(ServiceRecord r,IntentBindRecord i, boolean rebind) {//假如服务还没起来,不能调用onBind,直接返回了if (r.app == null || r.app.thread == null) {// If service is not currently running, can't yet bind.return false;}if ((!i.requested || rebind) && i.apps.size() > 0) {try {bumpServiceExecutingLocked(r, "bind");r.app.thread.scheduleBindService(r, i.intent.getIntent(), rebind);if (!rebind) {i.requested = true;}i.hasBound = true;i.doRebind = false;} catch (RemoteException e) {return false;}}return true;}

        套路与调用客户端的onCreate方法是一致的,先调用bumpServiceExecutingLocked方法post一个延时消息,然后调用scheduleBindService,该方法最终会走到ActivityThread的 handleBindService中,代码如下:

    private final void handleBindService(BindServiceData data) {Service s = mServices.get(data.token);if (s != null) {try {data.intent.setExtrasClassLoader(s.getClassLoader());try {if (!data.rebind) {//绑定服务IBinder binder = s.onBind(data.intent);ActivityManagerNative.getDefault().publishService(data.token, data.intent, binder);} else {//重新绑定s.onRebind(data.intent);ActivityManagerNative.getDefault().serviceDoneExecuting(data.token, 0, 0, 0);}ensureJitEnabled();} catch (RemoteException ex) {}} catch (Exception e) {//...}}}

         根据标识data.rebind判断是重新绑定还是首次绑定,首次绑定会调用onBind(然后调用AMS的publishServie发布服务,读者可以自己分析发布服务做了些啥);重新绑定调用onRebind,然后调用AMS的serviceDoneExecuting方法,从AMS调用onCreate的分析中我们知道方法serviceDoneExecuting的作用就是移除延时消息,避免ANR产生(publishService最后也会调用方法serviceDoneExecuting,所以也不会出现ANR)。

4、AMS调用客户端onStart前的准备

        这比较简单,代码注释清楚了,不再重复说明,代码如下:

    private final void realStartServiceLocked(ServiceRecord r,ProcessRecord app) throws RemoteException {//....//在调用onStart前创建了个StartItem并放入集合pendingStarts中if (r.startRequested && r.callStart && r.pendingStarts.size() == 0) {r.lastStartId++;if (r.lastStartId < 1) {r.lastStartId = 1;}r.pendingStarts.add(new ServiceRecord.StartItem(r, r.lastStartId, null, -1));}sendServiceArgsLocked(r, true);}

5、AMS调用客户端onStart方法

        最后是AMS对onStart方法的调用,sendServiceArgsLocked代码如下:

    private final void sendServiceArgsLocked(ServiceRecord r,boolean oomAdjusted) {final int N = r.pendingStarts.size();if (N == 0) {return;}while (r.pendingStarts.size() > 0) {try {//1 ServiceRecord.StartItem si = r.pendingStarts.remove(0);if (si.intent == null) {continue;}si.deliveredTime = SystemClock.uptimeMillis();r.deliveredStarts.add(si);si.deliveryCount++;//...//2bumpServiceExecutingLocked(r, "start");if (!oomAdjusted) {oomAdjusted = true;updateOomAdjLocked(r.app);}int flags = 0;if (si.deliveryCount > 0) {flags |= Service.START_FLAG_RETRY;}if (si.doneExecutingCount > 0) {flags |= Service.START_FLAG_REDELIVERY;}//3r.app.thread.scheduleServiceArgs(r, si.id, flags, si.intent);} catch (RemoteException e) {//....break;} catch (Exception e) {Slog.w(TAG, "Unexpected exception", e);break;}}}

        还是对几个关键点说明如下:

  1. 取出上一步放入pendingStarts中的StartItem对象,并记录了deliveredTime和deliveredCount。
  2. 调用bumpServiceExecutingLocked(r, "start"),套路与上面分析的一致。
  3. 调用客户端的scheduleServiceArgs。

        我们重点看看scheduleServiceArgs在客户端侧的实现,一路跟踪下去,最后调用了方法handleServiceArgs方法,代码如下:

    private final void handleServiceArgs(ServiceArgsData data) {Service s = mServices.get(data.token);if (s != null) {try {if (data.args != null) {data.args.setExtrasClassLoader(s.getClassLoader());}//1 调用onStartCommandint res = s.onStartCommand(data.args, data.flags, data.startId);QueuedWork.waitToFinish();try {//2 调用serviceDoneExecutingActivityManagerNative.getDefault().serviceDoneExecuting(data.token, 1, data.startId, res);} catch (RemoteException e) {// nothing to do.}ensureJitEnabled();} catch (Exception e) {//...}}}

        首先是调用onStartCommand方法,然后其返回值作为参数调用AMS侧的方法serviceDoneExecuting(data.token, 1, data.startId, res),注意第二个参数为1,返回值为最后一个参数,我们再次看看serviceDoneExecuting的相关实现,代码如下:

    public void serviceDoneExecuting(IBinder token, int type, int startId, int res) {synchronized(this) {//...ServiceRecord r = (ServiceRecord)token;boolean inStopping = mStoppingServices.contains(token);if (r != null) {if (r != token) {//...return;}//1 这次会进入这个分支了if (type == 1) {// This is a call from a service start...  take care of// book-keeping.r.callStart = true;switch (res) {case Service.START_STICKY_COMPATIBILITY:case Service.START_STICKY: {// We are done with the associated start arguments.r.findDeliveredStart(startId, true);// Don't stop if killed.r.stopIfKilled = false;break;}case Service.START_NOT_STICKY: {// We are done with the associated start arguments.r.findDeliveredStart(startId, true);if (r.lastStartId == startId) {// There is no more work, and this service// doesn't want to hang around if killed.r.stopIfKilled = true;}break;}case Service.START_REDELIVER_INTENT: {// We'll keep this item until they explicitly// call stop for it, but keep track of the fact// that it was delivered.ServiceRecord.StartItem si = r.findDeliveredStart(startId, false);if (si != null) {si.deliveryCount = 0;si.doneExecutingCount++;// Don't stop if killed.r.stopIfKilled = true;}break;}default:throw new IllegalArgumentException("Unknown service start result: " + res);}if (res == Service.START_STICKY_COMPATIBILITY) {r.callStart = false;}}final long origId = Binder.clearCallingIdentity();serviceDoneExecutingLocked(r, inStopping);Binder.restoreCallingIdentity(origId);} else {//...}}}

        这次type参数为1,所以会用到onStartCommond的返回值res了,我们知道onStartCommond的返回值有START_STICKY_COMPATIBILITY、START_STICKY、START_NOT_STICKY、START_REDELIVER_INTENT。对这些参数的处理就在AMS侧的这个方法中。

  1. START_STICKY:粘性的”。服务被异常kill掉,保留service的状态为开始状态,但不保留递送的intent对象。随后系统会尝试重新创建service,由于服务状态为开始状态,所以创建服务后一定会调用onStartCommand(Intent,int,int)方法。如果在此期间没有任何启动命令被传递到service,那么参数Intent将为null。
  2. START_NOT_STICKY:“非粘性的”。使用这个返回值时,如果在执行完onStartCommand后,服务被异常kill掉,系统不会自动重启该服务。
  3. START_REDELIVER_INTENT:重传Intent。使用这个返回值时,如果在执行完onStartCommand后,服务被异常kill掉,系统会自动重启该服务,并将Intent的值传入。
  4. START_STICKY_COMPATIBILITY:START_STICKY的兼容版本,但不保证服务被kill后一定能重启。

         所以我们可以根据业务需求,在重写onStartCommond方法的时候返回不同的值。

        startService的启动流程终于说完了;当然还有很多地方没有提到,但是我们可以这个流程为线索,一步步探索AMS中相关的实现,不至于密室在代码的海洋中,让自己对AMS有更深入的理解。

        最后恭喜你已学习完了startService的启动流程,还有疑问或者需要从新再次学习该启动流程,可以从这里进入:

  1. Android中Service在新进程中的启动流程-CSDN博客
  2. Android中Service在新进程中的启动流程2-CSDN博客

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

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

相关文章

MVCC底层原理实现

MVCC的实现原理 了解实现原理之前&#xff0c;先理解下面几个组件的内容 1、 当前读和快照读 先普及一下什么是当前读和快照读。 当前读&#xff1a;读取数据的最新版本&#xff0c;并对数据进行加锁。 例如&#xff1a;insert、update、delete、select for update、 sele…

基于Springboot + vue实现的在线装修管理系统

“前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站&#xff1a;人工智能学习网站” &#x1f496;学习知识需费心&#xff0c; &#x1f4d5;整理归纳更费神。 &#x1f389;源码免费人人喜…

【多视图学习】显式视图-标签问题:多视图聚类的多方面互补性研究

Explicit View-labels Matter:A Multifacet Complementarity Study of Multi-view Clustering TPAMI 2024 论文链接 代码链接 0.论文摘要 摘要-一致性和互补性是促进多视图聚类&#xff08;MVC&#xff09;的两个关键因素。最近&#xff0c;随着流行的对比学习的引入&#…

Docker Hub 全面解析及应对策略

在现代 DevOps 和容器化应用开发中&#xff0c;Docker Hub 是一个不可或缺的工具。然而&#xff0c;一些地区或企业对 Docker Hub 的访问受到限制&#xff0c;甚至全面禁止。这种现象引发了开发者和运维人员的广泛关注。那么&#xff0c;为什么 Docker Hub 会被禁用&#xff1f…

掌握Spring事务隔离级别,提升并发处理能力

Spring框架支持的事务隔离级别与标准的JDBC隔离级别保持一致&#xff0c;共包括五大隔离级别&#xff0c;它们分别是&#xff1a;DEFAULT&#xff08;默认隔离级别&#xff09;、READ_UNCOMMITTED&#xff08;读未提交&#xff09;、READ_COMMITTED&#xff08;读已提交&#x…

Rabbitmq高级特性之消费方确认

背景&#xff1a; 发送方发送消息之后&#xff0c;到达消费端之后&#xff0c;可能会有以下情况&#xff1a;消息处理成功&#xff0c;消息处理异常。RabbitMQ在向消费者发送消息之后&#xff0c;就会把这条消息给删除掉&#xff0c;那么第二种情况&#xff0c;就会造成消息丢…

[C]基础8.详解操作符

博客主页&#xff1a;算法歌者本篇专栏&#xff1a;[C]您的支持&#xff0c;是我的创作动力。 文章目录 0、总结1、操作符的分类2、二进制和进制转换2.1、2进制转10进制2.2、10进制转2进制2.3、2进制转8进制和16进制 3、原码、反码、补码4、移位操作符4.1 左移操作符4.2 右移操…

【豆包MarsCode蛇年编程大作战】花样贪吃蛇

目录 引言 展示效果 prompt提示信息 第一次提示&#xff08;实现基本功能&#xff09; 初次实现效果 第二次提示&#xff08;美化UI&#xff09; 第一次美化后的效果 第二次美化后的效果 代码展示 实现在线体验链接 码上掘金使用教程 体验地址&#xff1a; 花样贪吃蛇…

【Maui】注销用户,采用“手势”点击label弹窗选择

文章目录 前言一、问题描述二、解决方案三、软件开发&#xff08;源码&#xff09;3.1 方法一&#xff1a;前端绑定3.2 方法二&#xff1a;后端绑定3.3 注销用户的方法 四、项目展示 前言 .NET 多平台应用 UI (.NET MAUI) 是一个跨平台框架&#xff0c;用于使用 C# 和 XAML 创…

RoCE网络及其协议栈详解(没有中间商赚差价的网络)

引言 随着数据中心对高性能、低延迟通信需求的不断增长&#xff0c;传统的TCP/IP以太网连接已经难以满足现代应用的要求。为了解决这些问题&#xff0c;RDMA&#xff08;Remote Direct Memory Access&#xff09;技术应运而生。RDMA是一种允许网络中的不同计算机直接访问对方内…

数据结构:二叉树—面试题(一)

目录 1、相同的树 2、另一棵树的子树 3、翻转二叉树 4、平衡二叉树 5、对称二叉树 6、二叉树遍历 7、二叉树的分层遍历 1、相同的树 习题链接https://leetcode.cn/problems/same-tree/description/https://leetcode.cn/problems/same-tree/description/ 描述&#xff1a…

2025年新开局!谁在引领汽车AI风潮?

汽车AI革命已来。 在2025年伊始开幕的CES展上&#xff0c;AI汽车、AI座舱无疑成为了今年汽车行业的最大热点。其中不少车企在2025年CES上展示了其新一代AI座舱&#xff0c;为下一代智能汽车的人机交互、场景创新率先打样。 其中&#xff0c;东软集团也携带AI驱动、大数据支撑…

HarmonyOS Next 应用UI生成工具介绍

背景 HarmonyOS Next适配开发过程中难买难要参考之前逻辑&#xff0c;但是可能时间较长文档不全&#xff0c;只能参考Android或iOS代码&#xff0c;有些逻辑较重的场景还可以通过AI工具将Android 的Java代码逻辑转成TS完成部分复用。对于一些UI场景只能手动去写&#xff0c;虽…

python学opencv|读取图像(四十一 )使用cv2.add()函数实现各个像素点BGR叠加

【1】引言 前序已经学习了直接在画布上使用掩模&#xff0c;会获得彩色图像的多种叠加效果&#xff0c;相关文章链接为&#xff1a; python学opencv|读取图像&#xff08;四十&#xff09;掩模&#xff1a;三通道图像的局部覆盖-CSDN博客 这时候如果更进一步&#xff0c;直接…

宝塔Linux+docker部署nginx出现403 Forbidden

本文主要讲述了宝塔docker部署nginx出现403 Forbidden的原因&#xff0c;以及成功部署前端的方法步骤。 目录 1、问题描述2、问题检测2.1 检测监听端口是否异常2.2 检测Docker容器是否异常2.2.1 打开宝塔Linux的软件商店&#xff0c;找到Docker管理器&#xff0c;查看前端容器是…

PVE 虚拟机安装 Debian 无图形化界面服务器

Debian 安装 Debian 镜像下载 找一个Debian镜像服务器&#xff0c;根据需要的版本和自己硬件选择。 iso-cd/&#xff1a;较小&#xff0c;仅包含安装所需的基础组件&#xff0c;可能需要网络访问来完成安装。有镜像 debian-12.9.0-amd64-netinst.isoiso-dvd/&#xff1a;较…

docker环境搭建,docker拉取mysql,docker制作自定义C++镜像

目录 centos 安装和使用 dockerdocker拉取mysql使用可执行文件制作docker镜像Dockerfile文件优化Dockerfile简介Dockerfile优化 centos 安装和使用 docker yum install docker systemctl start docker systemctl status docker# 查询docker版本 docker version # 查询docker基…

2025牛客寒假算法营2

A题 知识点&#xff1a;模拟 打卡。检查给定的七个整数是否仅包含 1,2,3,5,6 即可。为了便于书写&#xff0c;我们可以反过来&#xff0c;检查这七个整数是否不为 4 和 7。 时间 O(1)&#xff1b;空间 O(1)。 #include <bits/stdc.h> using namespace std;signed main()…

STM32 FreeRTOS中断管理

目录 FreeRTOS的中断管理 1、STM32中断优先级管理 2、FreeRTOS任务优先级管理 3、寄存器和内存映射寄存器 4、BASEPRI寄存器 5、FreeRTOS与STM32中断管理结合使用 vPortRaiseBASEPRI vPortSetBASEPRI 6、FromISR后缀 7、在中断服务函数中调用FreeRTOS的API函数需注意 F…

【ComfyUI】python调用生图API,实现批量出图

官方给的示例&#xff1a; https://github.com/comfyanonymous/ComfyUI/blob/master/script_examples/websockets_api_example.pyhttps://github.com/comfyanonymous/ComfyUI/blob/master/script_examples/websockets_api_example.pyhttps://github.com/comfyanonymous/ComfyU…