flink 任务提交流程源码解析

news2024/9/22 22:15:23

flinkjob 提交流程


在这里插入图片描述

任务启动流程图

请添加图片描述
请添加图片描述
可以先简单看下流程图,对比下面详细说明,再回来看会更加清晰

1客户端的工作内容

1.1解析命令

  • 第一步是命令行的解析,根据用户提交的 flink run 命令,在客户端类cliFronted中进行解析
  • 通过空格与-c -m 等参数指令,提取出,用户提交的参数详情
  • 获取flink的conf目录的路径
  • 根据conf目录的路径,加载配置
  • 加载命令行的输入 command line 封装命令行接口:按顺序 generic yarn default
  • 获取run动作 默认的配置项
  • 根据用户指定的配置项 进行解析

这个帖子是根据flink1.12来做的基础,所以对比以后的版本可能会有一点出入,但总的流程都是一致的,例如,flink1.12中 任务的启动模式 会根据提交命令中指定的是否是yarn,standalone等来封装一个 command line 提交给后面使用,如果用户没有指定,flink就会根据默认顺序generic yarn default 来封装这个flink任务的启动模式

1.2 执行用户代码

用户代码会从env.execute开始执行

  • 从StreamExecutionEnvironment开始 execute
  • 根据用户代码 ,调用的算子,生成streamgraph图
  • streamgraph 转化 jobgraph
  • yarnjobClusterExcutor 创建启动 yarnclient 包含了一些yarn flink的配置和环境信息,并构造了一个yarnClusterDescriptor
  • yarnClusterDescriptor 获取集群特有配置 : jobmanager 内存 每个taskmanager 内存 每个 slot槽数
  • 部署前检查: jar包路径 conf路径 yarn最大核数 检查置顶的yarn队列是否存在, 检查yarn有足够的资源
  • yarn启动appmaster AM 通过startAppMaster
YarnClusterDescriptor.java

private ClusterClientProvider<ApplicationId> deployInternal(
		ClusterSpecification clusterSpecification,
		String applicationName,
		String yarnClusterEntrypoint,
		@Nullable JobGraph jobGraph,
		boolean detached) throws Exception {
... ...
// 创建应用
	final YarnClientApplication yarnApplication = yarnClient.createApplication();
... ...
	ApplicationReport report = startAppMaster(
			flinkConfiguration,
			applicationName,
			yarnClusterEntrypoint,
			jobGraph,
			yarnClient,
			yarnApplication,
			validClusterSpecification);
... ...
}


private ApplicationReport startAppMaster(
		Configuration configuration,
		String applicationName,
		String yarnClusterEntrypoint,
		JobGraph jobGraph,
		YarnClient yarnClient,
		YarnClientApplication yarnApplication,
		ClusterSpecification clusterSpecification) throws Exception {
... ...
	// 初始化文件系统(HDFS)
	final FileSystem fs = FileSystem.get(yarnConfiguration);
... ...
ApplicationSubmissionContext appContext = yarnApplication.getApplicationSubmissionContext();

final List<Path> providedLibDirs = getRemoteSharedPaths(configuration);
// 上传文件的工具类
final YarnApplicationFileUploader fileUploader = YarnApplicationFileUploader.from(
	fs,
	fs.getHomeDirectory(),
	providedLibDirs,
	appContext.getApplicationId(),
	getFileReplication());
... ...
	final ApplicationId appId = appContext.getApplicationId();
... ...
	if (HighAvailabilityMode.isHighAvailabilityModeActivated(configuration)) {
		// yarn重试次数,默认2
		appContext.setMaxAppAttempts(
						configuration.getInteger(
						YarnConfigOptions.APPLICATION_ATTEMPTS.key(),
						YarnConfiguration.DEFAULT_RM_AM_MAX_ATTEMPTS));

		activateHighAvailabilitySupport(appContext);
	} else {
		//不是高可用重试次数为1
		appContext.setMaxAppAttempts(
				configuration.getInteger(
						YarnConfigOptions.APPLICATION_ATTEMPTS.key(),
						1));
	}
... ...
	
	// 多次调用上传HDFS的方法,分别是:
	//  => systemShipFiles:日志的配置文件、lib/目录下除了dist的jar包
	//  => shipOnlyFiles:plugins/目录下的文件
	//  => userJarFiles:用户代码的jar包
fileUploader.registerMultipleLocalResources (... ...);
... ...
	// 上传和配置ApplicationMaster的jar包:flink-dist*.jar
	final YarnLocalResourceDescriptor localResourceDescFlinkJar = fileUploader.uploadFlinkDist(flinkJarPath);
... ...
//
fileUploader.registerSingleLocalResource(
					jobGraphFilename,
					new Path(tmpJobGraphFile.toURI()),
					"",
					true,
					false);
... ...
	// 上传flink配置文件
	String flinkConfigKey = "flink-conf.yaml";
	Path remotePathConf = setupSingleLocalResource(
		flinkConfigKey,
		fs,
		appId,
		new Path(tmpConfigurationFile.getAbsolutePath()),
		localResources,
		homeDir,
		"");
... ...
	// 将JobGraph写入tmp文件并添加到本地资源,并上传到HDFS
	fileUploader.registerSingleLocalResource(
		jobGraphFilename,
		new Path(tmpJobGraphFile.toURI()),
		"",
		true,
		false);
... ...
// 上传flink配置文件
String flinkConfigKey = "flink-conf.yaml";
fileUploader.registerSingleLocalResource(
	flinkConfigKey,
	new Path(tmpConfigurationFile.getAbsolutePath()),
	"",
	true,
	true);
... ...
final JobManagerProcessSpec processSpec = JobManagerProcessUtils.processSpecFromConfigWithNewOptionToInterpretLegacyHeap(
flinkConfiguration,
			JobManagerOptions.TOTAL_PROCESS_MEMORY);
	//封装启动AM container的Java命令
	final ContainerLaunchContext amContainer = setupApplicationMasterContainer(
			yarnClusterEntrypoint,
			hasKrb5,
			processSpec);
... ...	
appContext.setApplicationName(customApplicationName);
appContext.setApplicationType(applicationType != null ? applicationType : "Apache Flink");
appContext.setAMContainerSpec(amContainer);
appContext.setResource(capability);
... ...
	yarnClient.submitApplication(appContext);
... ...	
}

2集群工作内容

2.1 启动AM

  • 初始化文件系统
  • 封装appcontext 一些环境信息 appid
  • yarn应用的文件上传器初始化: fs hdfs路径
  • 配置appid zk的namespace,高可用重试次数 id

2.2启动JobManager和 ResourceManager

  • ApplicationMaster启动dispatch和ResourceManager
  • ResourceManager 中的 slotmanager 组件负责真正像yarn申请资源
  • dispatch 启动 jobManager
  • jobManage中的slotpool 负责真正发送请求

这么看来,在JobManager启动的过程中 主要发生了4个动作:

  • 初始化各种服务initializeservices(…) 7个服务
  • webMonitorEndpoint启动
  • ResourceManager启动
  • Dispatcher启动
    以上四个动作全部完成,才算JobManager完成启动:下面是关键点的代码

其中 webMonitorEndpoint又启动了netty的服务端,一个leadership服务,这个服务里注册了几十个handler, 来接收客户端提交的命令,例如savepoint或停止指令等,他本身

其中ResourceManager 启动了两个心跳管理器,一个jobManagerHeartbeatManager,一个taskManagerHeartbeatManager
jobManagerHeartbeatManager -> jobmaster-ResourceManager
taskManagerHeartbeatManager -> taskManager-taskexecutor

每个job 都有一个jobmaster,jobmaster会被注册到ResourceManager,这些jobmaster会被维持与ResourceManager的心跳

ResourceManager还启动了两个定时服务,与taskmanager和slot相关,如果 taskmanager掉线或者slot分配超时,会参与一些处理。

//yarn启动入口  YarnApplicationClusterEntryPoint.java

    public static void main(final String[] args) {
    ...
            ClusterEntrypoint.runClusterEntrypoint(yarnApplicationClusterEntrypoint);
	...
}

//ClusterEntrypoint.java
import org.apache.flink.runtime.webmonitor.retriever.impl.RpcMetricQueryServiceRetriever;
    public static void runClusterEntrypoint(ClusterEntrypoint clusterEntrypoint) {
    ...
                clusterEntrypoint.startCluster();
	...
	}
    public void startCluster() throws ClusterEntrypointException {
    ...
    runCluster(configuration, pluginManager);
    ...
	}

private void runCluster(Configuration configuration, PluginManager pluginManager)
            throws Exception {
        synchronized (lock) {
            initializeServices(configuration, pluginManager);

            // write host information into configuration
            configuration.setString(JobManagerOptions.ADDRESS, commonRpcService.getAddress());
            configuration.setInteger(JobManagerOptions.PORT, commonRpcService.getPort());
			关注!!!!!
            final DispatcherResourceManagerComponentFactory
                    dispatcherResourceManagerComponentFactory =
                            createDispatcherResourceManagerComponentFactory(configuration);

            clusterComponent =
                    dispatcherResourceManagerComponentFactory.create(
                            configuration,
                            resourceId.unwrap(),
                            ioExecutor,
                            commonRpcService,
                            haServices,
                            blobServer,
                            heartbeatServices,
                            metricRegistry,
                            executionGraphInfoStore,
				关注!!!!!      
                            new RpcMetricQueryServiceRetriever(
                                    metricRegistry.getMetricQueryServiceRpcService()),
                            this);

            clusterComponent
                    .getShutDownFuture()
                    .whenComplete(
                            (ApplicationStatus applicationStatus, Throwable throwable) -> {
                                if (throwable != null) {
                                    shutDownAsync(
                                            ApplicationStatus.UNKNOWN,
                                            ShutdownBehaviour.GRACEFUL_SHUTDOWN,
                                            ExceptionUtils.stringifyException(throwable),
                                            false);
                                } else {
                                    // This is the general shutdown path. If a separate more
                                    // specific shutdown was
                                    // already triggered, this will do nothing
                                    shutDownAsync(
                                            applicationStatus,
                                            ShutdownBehaviour.GRACEFUL_SHUTDOWN,
                                            null,
                                            true);
                                }
                            });
        }
    }
    
各种初始化

    protected void initializeServices(Configuration configuration, PluginManager pluginManager)
            throws Exception {

        LOG.info("Initializing cluster services.");

        synchronized (lock) {
            resourceId =
                    configuration
                            .getOptional(JobManagerOptions.JOB_MANAGER_RESOURCE_ID)
                            .map(
                                    value ->
                                            DeterminismEnvelope.deterministicValue(
                                                    new ResourceID(value)))
                            .orElseGet(
                                    () ->
                                            DeterminismEnvelope.nondeterministicValue(
                                                    ResourceID.generate()));

            LOG.debug(
                    "Initialize cluster entrypoint {} with resource id {}.",
                    getClass().getSimpleName(),
                    resourceId);

            workingDirectory =
                    ClusterEntrypointUtils.createJobManagerWorkingDirectory(
                            configuration, resourceId);

            LOG.info("Using working directory: {}.", workingDirectory);

            rpcSystem = RpcSystem.load(configuration);

            commonRpcService =
                    RpcUtils.createRemoteRpcService(
                            rpcSystem,
                            configuration,
                            configuration.getString(JobManagerOptions.ADDRESS),
                            getRPCPortRange(configuration),
                            configuration.getString(JobManagerOptions.BIND_HOST),
                            configuration.getOptional(JobManagerOptions.RPC_BIND_PORT));

            JMXService.startInstance(configuration.getString(JMXServerOptions.JMX_SERVER_PORT));

            // update the configuration used to create the high availability services
            configuration.setString(JobManagerOptions.ADDRESS, commonRpcService.getAddress());
            configuration.setInteger(JobManagerOptions.PORT, commonRpcService.getPort());

            ioExecutor =
                    Executors.newFixedThreadPool(
                            ClusterEntrypointUtils.getPoolSize(configuration),
                            new ExecutorThreadFactory("cluster-io"));
            haServices = createHaServices(configuration, ioExecutor, rpcSystem);
            blobServer =
                    BlobUtils.createBlobServer(
                            configuration,
                            Reference.borrowed(workingDirectory.unwrap().getBlobStorageDirectory()),
                            haServices.createBlobStore());
            blobServer.start();
            configuration.setString(BlobServerOptions.PORT, String.valueOf(blobServer.getPort()));
            heartbeatServices = createHeartbeatServices(configuration);
            metricRegistry = createMetricRegistry(configuration, pluginManager, rpcSystem);

            final RpcService metricQueryServiceRpcService =
                    MetricUtils.startRemoteMetricsRpcService(
                            configuration,
                            commonRpcService.getAddress(),
                            configuration.getString(JobManagerOptions.BIND_HOST),
                            rpcSystem);
            metricRegistry.startQueryService(metricQueryServiceRpcService, null);

            final String hostname = RpcUtils.getHostname(commonRpcService);

            processMetricGroup =
                    MetricUtils.instantiateProcessMetricGroup(
                            metricRegistry,
                            hostname,
                            ConfigurationUtils.getSystemResourceMetricsProbingInterval(
                                    configuration));

            executionGraphInfoStore =
                    createSerializableExecutionGraphStore(
                            configuration, commonRpcService.getScheduledExecutor());
        }
    }

2.3 申请资源 启动 taskmanager

  • JobManager 将JobGraph 转换 ExcuetionGraph
  • jobManager 中的slotpool 想 ResourceManager发出申请资源的请求
  • jobManager 收到 ResourceManager中来自 yarn 的许可的请求回复后,会启动TaskManager

在整个TaskManager启动过程中,最重要的事情,就是在TaskManager初始化了一些基础服务和一些对外提供服务的核心服务之后就启动TaskExecutor,向ResourceManager 进行TaskManager的注册,并在注册成功之后,维持TaskManager和JobManager的心跳。

3分配任务

3.1 资源计算

其实这是2.3中的工作,在申请资源时,flink在生成StreamGraph时,会根据用户代码,来计算任务的并行度,并计算出所需多少个slot,根据系统配置的slot大小,来计算所需任务的内容大小

3.2 分发任务

  • TaskManager启动后,TaskManager中 TaskExecutor 会像 resourceManager 注册slot
  • TaskExecutor 收到 resourceManager回复的肯定分配指令后,会把resourceManager给过来的offset,给到JobMaster
  • JobMaster 会提交具体的task到TaskExecutor
  • 任务就启动了

4 Task 任务调度执行图

请添加图片描述

5 任务提交过程

在那么client的东西是如何提交到JobManager的呢?

  • JobManager:
    • WebMonitorEndpoint:维护了一个netty服务端,client通过RestClient 提交job(JobSubmitHandler)
    • ResourceManager:资源集群的主节点
    • Dispatcher:job的调度执行
  • TaskManager:
    • TaskExecutor:提供计算资源,注册给ResourceManager,维持心跳,执行JobMaster发送给他要执行的task

在JobSubmitHandler.java 是真正处理提交过来的东西的类。

JobSubmitHandler.java  集群入口
    protected CompletableFuture<JobSubmitResponseBody> handleRequest(
            @Nonnull HandlerRequest<JobSubmitRequestBody> request,
            @Nonnull DispatcherGateway gateway){
    		//获取jobGraph  requestBody 就是客户端通过RestClient 发来的。
            CompletableFuture<JobGraph> jobGraphFuture = loadJobGraph(requestBody, nameToFile);

			//通过dispatcher提交jobgraph
			CompletableFuture<Acknowledge> jobSubmissionFuture =
                finalizedJobGraphFuture.thenCompose(
                        jobGraph -> gateway.submitJob(jobGraph, timeout));

	}
Dispatcher.java
//提交jobGraph 去执行

//重点1 创建Jobmaster jobGraph-> ExecutionGraph  
   private JobManagerRunner createJobMasterRunner(JobGraph jobGraph) throws Exception {
       Preconditions.checkState(!jobManagerRunnerRegistry.isRegistered(jobGraph.getJobID()));
       return jobManagerRunnerFactory.createJobManagerRunner(
               jobGraph,
               configuration,
               getRpcService(),
               highAvailabilityServices,
               heartbeatServices,
               jobManagerSharedServices,
               new DefaultJobManagerJobMetricGroupFactory(jobManagerMetricGroup),
               fatalErrorHandler,
               System.currentTimeMillis());
   }
   
//重点2 启动Jobmaster
private void runJob(JobManagerRunner jobManagerRunner, ExecutionType executionType)
           throws Exception {
       jobManagerRunner.start();
}
跑起来jobmaster相关服务,主要是注册和星跳
开始申请slot,并部署task

总结

  1. flink cliFronted类, 解析参数,封装commandLine, 执行用户代码生成streamGraph 并将streamGraph转化成JobGraph
  2. yarnjobClusterExcutor 初始化一个yarnclient对象,构造yarnClusterDescriptor
  3. yarnClusterDescriptor 将 依赖,jar,及其集群配置上传到Yarn Resource manager上
  4. yarn检查集群配置
  5. yarn的ResourceManager分配Container资源并通知对应的NodeManager启动ApplicationMaster。
  6. ApplicationMaster启动后加载Flink的Jar包和配置构建环境
  7. ApplicationMaster 启动 dispatch 和 ResourceManager(里面有slotmanager 真正管理资源向yarn申请资源的)
  8. dispatch 启动 JobMaster (里面有slotpool 真正发送请求的)
  9. JobMaster 将 JobGraph 转换 ExcuetionGraph
  10. JobMaster 向 Resourcemanager申请资源
  11. 启动JobManager之后ApplicationMaster向ResourceManager申请资源启动TaskManager。
  12. ResourceManager分配Container资源后,由ApplicationMaster通知资源所在节点
  13. NodeManager加载Flink的Jar包和配置构建环境并启动TaskManager
  14. TaskManager启动后 TaskExecutor 向 resourceManager 注册slot
  15. TaskExecutor 接收到分配的指令,提供offset给JobMaster
  16. JobMaster 提交具体的task 到 TaskExecutor
  17. JobManager 的 职 责 主 要 是 接 收 Flink 作 业 , 调 度 Task , 收 集 作 业 状 态 和 管 理TaskManager。
  18. TaskManager 相当于整个集群的 Slave 节点,负责具体的任务执行和对应任务在每个节点上的资源申请和管理。

具体Graph中如何转换的 可以参考 https://blog.csdn.net/Direction_Wind/article/details/121773408

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

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

相关文章

【Vue3】toRefs和toRef在reactive中的一些应用

&#x1f497;&#x1f497;&#x1f497;欢迎来到我的博客&#xff0c;你将找到有关如何使用技术解决问题的文章&#xff0c;也会找到某个技术的学习路线。无论你是何种职业&#xff0c;我都希望我的博客对你有所帮助。最后不要忘记订阅我的博客以获取最新文章&#xff0c;也欢…

如何在debian上实现一键恢复操作系统?

在Debian或任何其他Linux发行版上实现一键恢复操作系统&#xff0c;需要创建一个系统镜像或快照&#xff0c;并设置一个简单的方法来从该镜像恢复。以下是创建和恢复系统的基本步骤&#xff1a; 1. 创建系统镜像&#xff1a; 使用像dd&#xff0c;rsync或专门的备份工具&#…

详细分析UML的10种图(全)

目录 前言1. 基本知识2. 结构图2.1 类图2.2 对象图2.3 组件图2.4 部署图2.5 包图 3. 行为图3.1 用例图3.2 活动图3.3 状态图 4. 行为图4.1 顺序图4.2 协作图 前言 在软考高级中常见的一种题型&#xff0c;对此补充这方面的知识&#xff0c;并将其归入软考的专栏 1. 基本知识 …

全网最最最详细DataEase源码Docker方式部署教程

1.源码获取 有条件的小伙伴可以使用GitHub方式获取&#xff0c;要是没有条件的小伙伴可以去码云上面获取也是一样的&#xff0c;或者可以联系博主&#xff0c;博主手把手教学~ GitHub地址 Gitee地址 2.配置源码信息 1.配置单机版的配置文件中的数据库信息 2.下载前端的依赖包…

顺序表经典算法及其相关思考

27. 移除元素 - 力扣&#xff08;LeetCode&#xff09; 思路一 利用顺序表中的SLDestroy函数的思想&#xff0c;遇到等于val值的就挪动 思路二 双指针法&#xff1a;不停的将和val不相等的数字往前放。此时的des更像一个空数组&#xff0c;里面存放的都是和val不相等、能够存…

java面试JVM虚拟机篇

1 JVM组成 1.1 JVM由那些部分组成&#xff0c;运行流程是什么&#xff1f; 难易程度&#xff1a;☆☆☆ 出现频率&#xff1a;☆☆☆☆ JVM是什么 Java Virtual Machine Java程序的运行环境&#xff08;java二进制字节码的运行环境&#xff09; 好处&#xff1a; 一次编写&a…

常见消息中间件

ActiveMQ 我们先看ActiveMQ。其实一般早些的项目需要引入消息中间件&#xff0c;都是使用的这个MQ&#xff0c;但是现在用的确实不多了&#xff0c;说白了就是有些过时了。我们去它的官网看一看&#xff0c;你会发现官网已经不活跃了&#xff0c;好久才会更新一次。 它的单机吞…

Unity Meta XR SDK 快捷配置开发工具【Building Block/Quick Action/OVRCameraRigInteraction】

文章目录 &#x1f4d5;教程说明&#x1f4d5;Building Block&#x1f4d5;Quick Action&#x1f4d5;OVRCameraRigInteraction 此教程相关的详细教案&#xff0c;文档&#xff0c;思维导图和工程文件会放入 Spatial XR 社区。这是一个高质量 XR 社区&#xff0c;博主目前在内…

redis在go语言中的使用

redis在go语言中的使用 以下说明以读者有redis基础的前提下进行 未学习redis的可以到b站1小时浅学redis了解大概&#xff0c;学会如何使用 【GeekHour】一小时Redis教程_哔哩哔哩_bilibili 以下开发环境以windows为测试环境&#xff0c;旨在练习redis在go语言中的使用 red…

Java 面向对象进阶 14 抽象类和抽象方法(黑马)

抽象类不能实例化&#xff08;创建对象&#xff09;&#xff1a; 抽象类中不一定有抽象方法&#xff1a; 有抽象方法的类一定是抽象类&#xff1a; 可以有构造方法&#xff1a;&#xff08;作用&#xff1a;在创建子类对象时&#xff0c;给属性进行赋值的&#xff09; Perso…

Springboot展示本地图片

1. 创建本地图片目录 在resources下创建目录static/image 2. 修改配置文件 在application.yml中新增 spring:mvc:static-path-pattern: /** 3. 编写拦截器类&#xff0c;继承自HandlerInterceptor 重写preHandle方法 public boolean preHandle(HttpServletRequest request…

Uniapp真机调试没有检测到设备,请插入设备或启动模拟器后刷新再试

最近用HbuilderX开发遇到了一个问题&#xff0c;之前插上手机就能调试&#xff0c;但最近再写app的时候&#xff0c;插上手机&#xff0c;也打开了开发者模式&#xff0c;但就是检测不到设备。 后来发现是要打开MIDI模式。vivo手机路径为&#xff1a;系统管理与升级->开发者…

RK3568平台开发系列讲解(Linux系统篇)编写I2C客户端驱动程序

🚀返回专栏总目录 文章目录 一、定义和注册I2C驱动程序二、在设备树中实例化I2C设备——新方法三、总结沉淀、分享、成长,让自己和他人都能有所收获!😄 配置I2C设备基本上分为两个步骤。 定义并注册I2C驱动程序定义并注册I2C设备在DT中,I2C设备属于非存储器映射设备系列…

HQYJ 2024-2-21 作业

复习课上内容&#xff08;已完成&#xff09;结构体字节对齐&#xff0c;64位没做完的做完&#xff0c;32位重新都做一遍&#xff0c;课上指定2字节对齐的做一遍&#xff0c;自己验证&#xff08;已完成&#xff09;两种验证大小端对齐的代码写一遍复习指针内容&#xff08;已完…

人脸美型SDK解决方案,包括瘦脸、大眼、瘦鼻等功能

为了满足市场不断升级的美颜需求&#xff0c;美摄科技凭借其在人脸识别与图像处理领域的深厚积累&#xff0c;推出了一款高效且易集成的人脸美型SDK解决方案。该方案旨在通过先进的算法和丰富的调节功能&#xff0c;帮助企业客户快速实现用户脸部形状的精准美化&#xff0c;进而…

MFC 多文档程序的基本编程

下载了一个openGL mfc的多文档程序,以此来学习mfc多文档模式的编程; 它每次新建一个文档,会在窗口绘制一个三角形、一个矩形;如果没有了图形刷新一下; 先看一下为什么每次打开新文档会绘制图形; 生成工程之后主要有5个类,比单文档程序多了一个子框架类; 可以打开多个…

微信小程序开发学习笔记——3.2page内的onload及data差值表达式

>>跟着b站up主“咸虾米_”学习微信小程序开发中&#xff0c;把学习记录存到这方便后续查找。 课程连接&#xff1a;https://www.bilibili.com/video/BV19G4y1K74d?p16&vd_source9b149469177ab5fdc47515e14cf3cf74 一、注册页面 https://developers.weixin.qq.com…

C#之WPF学习之路(2)

目录 控件的父类 DispatcherObject类 DependencyObject类 DependencyObject 类的关键成员和方法 Visual类 Visual 类的主要成员和方法 UIElement类 UIElement 类的主要成员和功能 FrameworkElement类 FrameworkElement 类的主要成员和功能 控件的父类 在 WPF (Windo…

idea 打不开项目 白屏

使用IDEA打开项目&#xff0c; 不知名原因崩溃了&#xff0c; 直接出现缩略图白屏。 解决过程&#xff1a; 尝试过重启IDEA&#xff0c;重启过电脑&#xff0c;重新引入相同项目&#xff08;使用不同路径&#xff0c;存在缓存记录&#xff0c;依然打不开&#xff09;&#xff…

亿道丨三防平板pad丨三防平板是指哪三防丨三防工业级平板电脑

三防工业级平板电脑成为许多行业中的重要工具。本文将介绍三防工业级平板电脑的特点以及其在各个领域中的广泛应用。 三防工业级平板电脑的特点 三防工业级平板电脑是指具备防水、防尘和防震功能的平板电脑。这些特点使得它们能够在恶劣环境中工作&#xff0c;如沙尘飞扬的工地…