AMS的起源,SystemServer的前世今生

news2025/1/21 2:48:43

作者:OpenGL

前世

SystemServer的创建还得追溯到之前的Zygote进程中的ZygoteInit代码中。这里的Zygote.forkSystemServer()方法就是实现分裂的关键代码。它内部调用了native方法实现了进程分裂。

执行完这行代码之后。我们的系统中就会出现两个一模一样的进程,只不过进程id不一样。这两个进程都会继续执行之后的代码。一个是Zygote进程,一个是分裂出来的子进程(此时还没给该进程命名SystemServer)。

1. 梦回Zygote进程

这里精简了一下ZygoteInit的代码,只留了几个重要的方法。并且重要代码都加了注释,不必深究代码内部做了什么。这样可以更加方便理解。让我们先看下代码,再接着往后看。

  class ZygoteInit {
      public static void main(String argv[]) {
           // ...代码忽略...
           
           // 此处的startSystemServer为true
          if (startSystemServer) {
              // 调用forkSystemServer方法
              Runnable r = forkSystemServer();
              if (r != null) {
                  r.run();
                  return;
              }
          }
          
          // runselectloop内部是一个while(true)的循环,
          // 只有在退出while循环时,会return一个runnable
          // 一旦执行完成,意味着zygote进程所有代码执行完成。
          Runnable caller = zygoteServer.runSelectLoop(abiList);
          if (caller != null) {
              caller.run();
          }
      }

      private static Runnable forkSystemServer() {
          // 内部通过native方法分裂进程,
          // 分裂之后,一个zygote进程,一个子进程。
          int pid = Zygote.forkSystemServer();
          // 这里之后的代码,Zygote进程和分裂出的子进程都会继续执行
          
          if (pid == 0) {
          
              // 子进程关闭socket
              zygoteServer.closeServerSocket(); 
              
              // 这里给进程命名,并启动Systemserver.java
              return handleSystemServerProcess(parsedArgs);
          }
           // Zygote进程,执行完forkSystemServer方法会返回null
           return null;
      }

      private static Runnable handleSystemServerProcess(ZygoteArguments parsedArgs) 
         // niceName是进程名字,即SystemServer。
         // 在这里把这个分裂出来的进程,重新命名为SystemServer
        if (parsedArgs.mNiceName != null) {
            Process.setArgV0(parsedArgs.mNiceName);
        }
        
       // ...代码忽略...
       
       // 此处调用native方法开启Binder线程池
       // 这里通过SystemServer的全类名,用反射的方式,启动SystemServer的main方法
       return ZygoteInit.zygoteInit(parsedArgs.mTargetSdkVersion,
                  parsedArgs.mDisabledCompatChanges,
                  parsedArgs.mRemainingArgs, cl);
      }
  }

我们在main方法里调用了forkSystemServer方法。forkSystemServer方法中通过 int pid = Zygote.forkSystemServer(); 分裂了一个进程出来,并返回分裂出的进程的pid值。这里分裂进程是调用的native方法,即底层的c++代码。所以这里一笔带过。有兴趣的同学可以进源码中查看。

此时已经有了两个进程。一个Zygote进程,一个分裂出来的子进程。并且两个进程都会继续执行未执行完的代码

2. 两条不同的路

我们再将目光转回到 int pid = Zygote.forkSystemServer();,这句代码可以理解为,分裂进程并返回子进程的pid。

进程在分裂后,都会返回一个pid的值,且继续执行。只不过Zygote进程返回的是子进程的pid,而 子进程由于它是被分裂出来的进程,所以Zygote.forkSystemServer();方法返回的是0。从这里开始两个进程的命运就开始走向了两条不同的道路。

3. 前提假设

这里为了方便理解,我们做个假设,假设Zygote的进程id为1000,分裂出的子进程id为2000。

4. Zygote进程

假设在Zygote进程中,返回的pid的值是分裂出的子进程的2000。

我们在看回上面ZygoteInit代码中,由于Zygote进程返回的pid的值是非0的数,所以forkSystemServer() 这个方法对于Zygote进程来说返回的是null,再看ZygoteInit的代码可知,Zygote进程将会执行zygoteServer.runSelectLoop(abiList) 这行代码。

即在分裂进程后,将会进入循环等待。

5. 分裂出的子进程

而在子进程中,由于 int pid = Zygote.forkSystemServer(); 返回的pid是0,所以会执行if (pid == 0) 这个代码块里的handleSystemServerProcess(parsedArgs) 方法。

这个方法里会给分裂出来的进程重新命名SystemServer,并且会通过反射执行SystemServer.java中的main方法。

至此,SystemServer就正式和Zygote分离开了。


今生

上面说到,分裂出的子进程会执行SystemServer的main方法。所以为了方便理解,先上一个SystemServer大致的功能图。同时后面加上SystemServer精简过的代码。以方便大家加深记忆。从图中可知SystemServer大概做了7件事。不过,准备Looper和Looper.loop()也可以看做一件事。

public final class SystemServer {
    /**
     * The main entry point from zygote.
     */
    public static void main(String[] args) {
        new SystemServer().run();
    }

    private void run() {
            // 1. Prepare the main looper thread (this thread).
            android.os.Process.setThreadPriority(
                    android.os.Process.THREAD_PRIORITY_FOREGROUND);
            Looper.prepareMainLooper();

            // Initialize native services.
            System.loadLibrary("android_servers");

            // 2. Initialize the system context.
            createSystemContext();

            // 3. Create the system service manager.
            mSystemServiceManager = new SystemServiceManager(mSystemContext);

            // Start services.
            // 4. 启动引导类服务
            startBootstrapServices(t); 
            
            // 5. 启动核心类服务
            startCoreServices(t); 
            
            // 6. 启动其他类服务
            startOtherServices(t); 

            // 7. Loop forever.
            Looper.loop();
        throw new RuntimeException("Main thread loop unexpectedly exited");
    }
}

1. 准备Looper

这里做的第一件事,就是给当前的线程设置了优先级。紧接着就是大家特别熟悉的一句代码Looper.prepareMainLooper();

熟悉Handler的童鞋看到这个肯定特别亲切。这就是为什么在主线程中可以直接使用Handler的原因,这个的话涉及Handler和Looper的关系,这里不做过多的赘述,感兴趣的同学可以上网搜一下,或者让我出一篇这个的文章。不过网上已经有太多的人写这个了。 后面的ActivityThread类的main方法中会出现这个方法。大家可以找一下这个方法。

这里的Looper.prepareMainLooper(); 和后面的 Looper.loop(); 是相对应的。

2. 创建上下文啦

这里会初始化系统类上下文,并且会出现我们很熟悉的一个类ActivityThread。调用它的systemMain() 方法。

public final class SystemServer {
    // 省略其他代码
    private void createSystemContext() {
        ActivityThread activityThread = ActivityThread.systemMain();
        mSystemContext = activityThread.getSystemContext();
        final Context systemUiContext = activityThread.getSystemUiContext();
    }
}

下面有ActivityThread的相关代码。systemMain方法中创建了ActivityThread对象,并且执行了attach方法。由于这里的参数system是true,所以执行的是else里的代码块。

attach方法里,创建了context上下文,同时通过Instrumentation创建了application。为了控制文章长度,就不在这里贴相关代码了。

public final class ActivityThread{

    public static void main(String[] args) {
        // 准备looper
        Looper.prepareMainLooper();
        // 主线程进入循环等待
        Looper.loop();
        throw new RuntimeException("Main thread loop unexpectedly exited");
    }

    @UnsupportedAppUsage
    public static ActivityThread systemMain() {
        ActivityThread thread = new ActivityThread();
        thread.attach(true, 0);
        return thread;
    }
    
    Application mInitialApplication;

    private void attach(boolean system, long startSeq) {
        if (!system) {
            // 内容很重要,涉及App启动,但是此处用不到所以省略.......
        } else {
            mInstrumentation = new Instrumentation();
            mInstrumentation.basicInit(this);
            
            // 创建上下文,context持有该ActivityThread对象
            ContextImpl context = ContextImpl.createAppContext(this, getSystemContext().mPackageInfo);
            
            // mPackageInfo是LoadedApk,LoadedApk内部含有Instrumentation,
            // makeApplication内部通过Instrumentation的newApplication方法创建了Application。
            mInitialApplication = context.mPackageInfo.makeApplication(true, null);
            mInitialApplication.onCreate();
        }
    }
}

ContextImpl是Context的具体实现类。同时创建了LoadedApk对象。LoadedApk记录了当前要加载的apk的相关信息,例如PackageName,LibDir,ResDir等。


/**
 * Common implementation of Context API, which provides the base
 * context object for Activity and other application components.
 */
class ContextImpl extends Context {

    @UnsupportedAppUsage
    static ContextImpl createSystemContext(ActivityThread mainThread) {
        LoadedApk packageInfo = new LoadedApk(mainThread);
        ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, null,
                0, null, null);
        return context;
    }
}

3. SystemServiceManager

SystemServiceManager官方的注释解释是,管理SystemService的创建、开始以及其他生命周期事件。通过反射的方式,调用SystemService以及其子类的start方法。后面的startCoreServices方法里有相关代码。

/**
 * Manages creating, starting, and other lifecycle events of system services
 * {@link com.android.server.SystemService system services}.
 *
 * {@hide}
 */
public class SystemServiceManager {

    /**
     * Starts a service by class name.
     */
    public SystemService startService(String className) {
        final Class<SystemService> serviceClass = loadClassFromLoader(className,
                this.getClass().getClassLoader());
        return startService(serviceClass);
    }


    /**
     * Creates and starts a system service. The class must be a subclass of
     * {@link com.android.server.SystemService}.
     */
    public <T extends SystemService> T startService(Class<T> serviceClass) {
            // Create the service.
            Constructor<T> constructor = serviceClass.getConstructor(Context.class);
            final T service = constructor.newInstance(mContext);

            startService(service);
            return service;
    }

    public void startService(@NonNull final SystemService service) {
        // Register it.
        mServices.add(service);

        // Start it.
        service.onStart();
    }
}

4. startBootstrapServices

开启系统服务,这里有很多很多的Service被启动。其中最关键的几个Service就是ActivityManagerService,简称AMS。另外一个是ActivityTaskManagerService,简称ATMS 。还有PackageManagerService ,即PMS

其实AMSATMS 他们两个其实是有些渊源的。最开始四大组件的启动服务都在ActivityManagerService中,不过等到了Android10.0之后,可能是研发人员觉得AMS的工作量太大了,所以给他找了个小弟ATMS,将activity的相关工作放到了ActivityTaskManagerService中。不过AMS还会持有ATMS引用,这波属实是让AMS从一个底层员工变成了一个带新人的老员工。

private void startBootstrapServices(@NonNull TimingsTraceAndSlog t) {
     mPackageManagerService = PackageManagerService.main(mSystemContext, installer,
                    mFactoryTestMode != FactoryTest.FACTORY_TEST_OFF, mOnlyCore);
                    
    // Activity manager runs the show.
    ActivityTaskManagerService atm = mSystemServiceManager.startService(
            ActivityTaskManagerService.Lifecycle.class).getService();
            
    mActivityManagerService = ActivityManagerService.Lifecycle.startService(
            mSystemServiceManager, atm);
    mActivityManagerService.setSystemServiceManager(mSystemServiceManager);
    
    //省略很多代码...
}

startBootstrapServices这里创建了很多service,其中最引人注意的是PMS,AMS以及ATMS。


5. startCoreServices

核心服务这里的话,没有太多可说的。如果各位有感兴趣的可以直接看代码中的英文注释自行理解。我就不多做加工了,可能原汁原味比我加工过的更好理解。

不过从这里通过SystemServeiceManagerstart方法,启动了很多SystemService的子类。

/**
 * Starts some essential services that are not tangled up in the bootstrap process.
 */
private void startCoreServices(@NonNull TimingsTraceAndSlog t) {
    // Service for system config
    mSystemServiceManager.startService(SystemConfigService.class);

    // Tracks the battery level.  Requires LightService.
    mSystemServiceManager.startService(BatteryService.class);

    // Tracks application usage stats.
    mSystemServiceManager.startService(UsageStatsService.class);
    mActivityManagerService.setUsageStatsManager(
            LocalServices.getService(UsageStatsManagerInternal.class));

    // Tracks whether the updatable WebView is in a ready state and watches for update installs.
    if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_WEBVIEW)) {
        mWebViewUpdateService = mSystemServiceManager.startService(WebViewUpdateService.class);
    }

    // Tracks and caches the device state.
    mSystemServiceManager.startService(CachedDeviceStateService.class);

    // Tracks cpu time spent in binder calls
    mSystemServiceManager.startService(BinderCallsStatsService.LifeCycle.class);

    // Tracks time spent in handling messages in handlers.
    mSystemServiceManager.startService(LooperStatsService.Lifecycle.class);

    // Manages apk rollbacks.
    mSystemServiceManager.startService(ROLLBACK_MANAGER_SERVICE_CLASS);

    // Service to capture bugreports.
    mSystemServiceManager.startService(BugreportManagerService.class);

    // Serivce for GPU and GPU driver.
    mSystemServiceManager.startService(GpuService.class);
}

6. startOtherServices

他来了他来了,他带着Launcher走来了。这里面创建了windowsManagerService,而且调用了AMS的systemReady方法启动了Launcher这个app。

这里稍微挖个坑,启动Launcher的流程,留到AMS里面在进行讲解,这里只说结果。那就是startOtherServices里创建了Launcher。

private void startOtherServices(@NonNull TimingsTraceAndSlog t) {

    WindowManagerService wm = null;
    wm = WindowManagerService.main(context, inputManager, !mFirstBoot, mOnlyCore,
            new PhoneWindowManager(), mActivityManagerService.mActivityTaskManager);
            
    ServiceManager.addService(Context.WINDOW_SERVICE, wm, /* allowIsolated= */ false,
            DUMP_FLAG_PRIORITY_CRITICAL | DUMP_FLAG_PROTO);
}

7. Looper.loop

最后的最后,和我们的开头相呼应。我们的SystemServer进程的主线程通过Looper.loop进入循环等待。一旦有handler传递消息,就会在systemServer的这个线程中进行处理。

最后

SystemServer到此就结束了,其实还有一些东西没有说,比如SystemManager如何注册服务,比如AMS如何启动Launcher。这些如果各位感兴趣的话,可以给我留言,我会再写相应的文章进行讲解。

另外,只看文章真的只能是暂时记住。真的想加深记忆,需要自己走一遍源码流程。好记性不如烂笔头,在走源码的时候,将重要的点进行记录,也是加深记忆的一种方式。

如果各位觉得本文写的还算凑合,请大家帮忙点个赞加个关注,你们的点赞关注,就是我更新文章的最大动力。让我们一起学习,一起进步,最后给各位一个图解,让我们一起加油~

图解


如果你还没有掌握Framework,现在想要在最短的时间里吃透它,可以参考一下《Android Framework核心知识点》,里面内容包含了:Init、Zygote、SystemServer、Binder、Handler、AMS、PMS、Launcher……等知识点记录。

《Framework 核心知识点汇总手册》:https://qr18.cn/AQpN4J

Handler 机制实现原理部分:
1.宏观理论分析与Message源码分析
2.MessageQueue的源码分析
3.Looper的源码分析
4.handler的源码分析
5.总结

Binder 原理:
1.学习Binder前必须要了解的知识点
2.ServiceManager中的Binder机制
3.系统服务的注册过程
4.ServiceManager的启动过程
5.系统服务的获取过程
6.Java Binder的初始化
7.Java Binder中系统服务的注册过程

Zygote :

  1. Android系统的启动过程及Zygote的启动过程
  2. 应用进程的启动过程

AMS源码分析 :

  1. Activity生命周期管理
  2. onActivityResult执行过程
  3. AMS中Activity栈管理详解

深入PMS源码:

1.PMS的启动过程和执行流程
2.APK的安装和卸载源码分析
3.PMS中intent-filter的匹配架构

WMS:
1.WMS的诞生
2.WMS的重要成员和Window的添加过程
3.Window的删除过程

《Android Framework学习手册》:https://qr18.cn/AQpN4J

  1. 开机Init 进程
  2. 开机启动 Zygote 进程
  3. 开机启动 SystemServer 进程
  4. Binder 驱动
  5. AMS 的启动过程
  6. PMS 的启动过程
  7. Launcher 的启动过程
  8. Android 四大组件
  9. Android 系统服务 - Input 事件的分发过程
  10. Android 底层渲染 - 屏幕刷新机制源码分析
  11. Android 源码分析实战

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

复现基于PYNQ-Z2的手写数字识别卷积加速器设计

来源雪天鱼 基于PYNQ-Z2的手写数字识别卷积加速器设计【持续更新】_雪天鱼的博客-CSDN博客 一、设计思路 1、输入28 x 28 的图片&#xff0c;非png格式&#xff0c;而是txt格式&#xff0c;将图片数据进行量化&#xff0c;存入到txt文件当中。 2、在PL端实现卷积神经网络LeN…

关于Linux Docker springboot jar 日志时间不正确 问题解决

使用Springboot项目的jar&#xff0c;制作了一个Docker镜像&#xff0c;启动该镜像后发现容器和容器中的Springboot 项目的日志时间不正确。 解决 查看容器时间命令为&#xff1a; docker exec 容器id date 1. 容器与宿主机同步时间 在启动镜像时候把操作系统的时间通过&q…

可白嫖的4家免费CDN,并测试其网络加速情况(2023版)

网站加载速度优化过程中&#xff0c;不可避免的会用上CDN来加速资源的请求速度。但是市面上的CDN资源几乎都是要收费的&#xff0c;而且价格还不便宜&#xff0c;对于小公司站长来讲&#xff0c;这将是一笔不小的开销。不过还是有一些良心公司给我们提供了免费的资源&#xff0…

SASS 学习笔记

SASS 学习笔记 总共会写两个练手项目&#xff0c;成品在 https://goldenaarcher.com/scss-study 可以看到&#xff0c;代码在 https://github.com/GoldenaArcher/scss-study。 什么是 SASS SASS 是 CSS 预处理&#xff0c;它提供了变量&#xff08;虽然现在 CSS 也提供了&am…

【C# 基础精讲】异常的类型和处理方法

异常&#xff08;Exception&#xff09;是在程序执行过程中发生的意外或异常情况&#xff0c;例如除零错误、空引用访问、文件不存在等。在C#及其他编程语言中&#xff0c;异常处理是一种重要的机制&#xff0c;用于捕获和处理程序运行时可能出现的错误&#xff0c;以保证程序的…

939. 最小面积矩形;2166. 设计位集;2400. 恰好移动 k 步到达某一位置的方法数目

939. 最小面积矩形 核心思想&#xff1a;枚举矩形的右边那条边的两个点&#xff0c;并用一个哈希表存储相同纵坐标的最近出现的列的列数,不断更新最近出现的左边那条边。 2166. 设计位集 核心思想&#xff1a;这题主要是时间复杂度的优化&#xff0c;用一个flag来标记当前翻转…

word在页眉页脚添加第几页

如果直接在页脚添加数字&#xff0c;整个文档的页脚会是统一的。 这里我们需要的是每一页按照页码排布的文档&#xff0c;所以首先打开页脚设置&#xff1a; 在插入内选择页脚 在弹出的下拉窗口中选择空白即可 在菜单栏会多出“页眉和页脚”的选项卡&#xff0c;选择其中的页…

使用拦截器+Redis实现接口幂等

文章目录 使用拦截器Redis实现接口幂等1.思路分析2.具体实现2.1 创建redis工具类2.2 自定义幂等注解2.2 自定义幂等拦截器2.3 注入拦截器到容器 3.测试 使用拦截器Redis实现接口幂等 1.思路分析 接口幂等有很多种实现方式&#xff0c;拦截器/AOPRedis&#xff0c;拦截器/AOP本…

以对话为场景本质,AIGC 将如何改变游戏规则

8 月 17 日&#xff08;本周四&#xff09;&#xff0c;融云直播课从排查问题到预警风险&#xff0c;社交产品如何更好保障体验、留住用户&#xff1f;欢迎点击报名~ 生成式 AI 公司 MosaicML 以约 13 亿美元的价格被大数据巨头 Databricks 收购&#xff0c;这个发生于 6 月底的…

python获取音乐文件

浏览器打开音乐地址 http://www.htqyy.com/top/hot 点击第一首歌曲&#xff0c;会打开新的网页并且可以获取 改歌曲的id&#xff0c;就是url中的33 在播放页面点击F12&#xff0c;打开开发者调试功能 如下图所示&#xff0c;在script脚本中可以获取歌曲的下载数据 host&#…

【DICOM医学影像1】数据格式存储于显示,基本知识科普指南

DICOM&#xff08;Digital Imaging and Communications in Medicine&#xff09;数据格式&#xff0c;是医学影像存储中的标准格式。无论是X光、CT&#xff0c;还是MRI等等影像&#xff0c;采集的原理不同&#xff0c;但是存储的格式一般都是统一的。本文就对DICOM文件的图像显…

Hands on RL 之 Deep Deterministic Policy Gradient(DDPG)

Hands on RL 之 Deep Deterministic Policy Gradient&#xff08;DDPG&#xff09; 文章目录 Hands on RL 之 Deep Deterministic Policy Gradient&#xff08;DDPG&#xff09;1. 理论部分1.1 回顾 Deterministic Policy Gradient(DPG)1.2 Neural Network Difference1.3 Why i…

大模型PEFT技术原理(三):Adapter Tuning及其变体

随着预训练模型的参数越来越大&#xff0c;尤其是175B参数大小的GPT3发布以来&#xff0c;让很多中小公司和个人研究员对于大模型的全量微调望而却步&#xff0c;近年来研究者们提出了各种各样的参数高效迁移学习方法&#xff08;Parameter-efficient Transfer Learning&#x…

java17新特性+ZGC

ZGC垃圾收集 11引入的追求低延迟的垃圾回收器 1.ZGC的内存布局 1.1 region 和G1一样&#xff0c;也是基于Region的堆内存布局。但是ZGC的Region具有动态性&#xff1a;动态创建、动态销毁、动态数据容量。 1.2 垃圾回收机制 相较于CMS&#xff0c;ZGC只有6个阶段&#xff1…

同步、异步、协程

目录 同步异步https 异步请求&#xff1a; 协程1.为什么会要协程?2.异步的运行流程是什么3.协程的原语操作4.协程的定义?5.调度器的定义?6.调度的策略?7. api封装, hook8.多核的模式?9.协程的性能?10.要有哪些案例?nty_servernty_ mysql_client.cnty_ mysql oper.cnty_ …

Python项目实战:基于napari的3D可视化(点云+slice)

文章目录 一、napari 简介二、napari 安装与更新三、napari【巨巨巨大的一个BUG】四、napari 使用指南4.1、菜单栏&#xff08;File View Plugins Window Help&#xff09;4.2、Window&#xff1a;layer list&#xff08;参数详解&#xff09;4.3、Window&#xff1a;layer…

city walk结合VR全景,打造新时代下的智慧城市

近期爆火的city walk是什么梗&#xff1f;它其实是近年来备受追捧的城市漫步方式&#xff0c;一种全新的城市探索方式&#xff0c;与传统的旅游观光不同&#xff0c;城市漫步更注重与城市的亲密接触&#xff0c;一步步地感受城市的脉动。其实也是一种自由、休闲的方式&#xff…

vscode搭建java开发环境

一、配置extensions环境变量VSCODE_EXTENSIONS&#xff0c; 该环境变量路径下的存放安装组件&#xff1a; 二、setting配置文件 {"java.jdt.ls.java.home": "e:\\software\\jdk\\jdk17",// java运行环境"java.configuration.runtimes": [{"…

分类预测 | MATLAB实现DRN深度残差网络多输入分类预测

分类预测 | MATLAB实现DRN深度残差网络多输入分类预测 目录 分类预测 | MATLAB实现DRN深度残差网络多输入分类预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.分类预测 | MATLAB实现DRN深度残差网络多输入分类预测 2.代码说明&#xff1a;MATLAB实现DRN深度残差网络…

信捷 XDH Ethercat A_GearIn指令与轴配置

在前面的文章中描述了A_FOLLOW指令&#xff0c;有时不能满足要求&#xff0c;需要更高级的指令A_GearIn指令。 下面的例子A_GearIn指令和CNT_AB指令 实现手轮动马达动&#xff0c;手轮停马达停&#xff0c;手轮转的快马达也转得快。&#xff08;手轮输出接到PLC的X0和X1点&am…