【Linux】进程等待

news2024/11/26 2:52:25

文章目录

  • 进程等待
    • 进程等待必要性
    • 实验(见见猪跑)
    • 进程等待的方法
      • wait方法
      • waitpid**方法**
      • 宏的使用方法
      • 获取子进程status
    • 阻塞VS非阻塞
      • 概念对比
      • 非阻塞有什么好处
  • 具体代码实现
    • 进程的阻塞等待方式:
    • 进程的非阻塞等待方式:
    • 让父进程做其他任务

进程等待

进程等待必要性

  • 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
  • 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
  • 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
  • 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
    因此,进程等待是为了:
  1. 避免内存泄漏
  2. 获取子进程执行的结果
    1. 代码跑完,结果对->退出码
    2. 代码跑完,结果不对->退出码
    3. 代码运行异常->信号

等待就是通过系统调用,获取子进程退出码或者退出信号的方式,顺便释放内存

实验(见见猪跑)

#include<sys/types.h>  
#include<sys/wait.h>  
int main()
{
	pid_t id = fork();
	if(id == 0)
	{
		int cnt = 10;
		while(cnt)
		{
			printf("我是子进程:%d,父进程:%d,cnt:%d\n",getpid(),getppid(),cnt--);
		}
		exit(10);
		//此时都是S状态
	}
	//此时子进程变成Z状态
	sleep(15);
	pid_t ret = wait(NULL);//回收子进程
	if(id > 0)
	{
		printf("wait success:%d\n",ret);
	}
	sleep(5);//父进程退出
}

waitpid用法;

#include<sys/types.h>  
#include<sys/wait.h>  
int main()
{
	pid_t id = fork();
	if(id == 0)//子进程返回0,父进程返回子进程id
	{
		int cnt = 10;
		while(cnt)
		{
			printf("我是子进程:%d,父进程:%d,cnt:%d\n",getpid(),getppid(),cnt--);
		}
		exit(10);
		//退出的三种结果:
		//1.代码跑完,结果是对的
		//2.代码跑完,结果是错的
		
		//3.代码没有跑完,出异常了
		
		//此时都是S状态
	}
	//此时子进程变成Z状态
	sleep(15);
	int status = 0;
	pid_t ret = waitpid(id,&status,0);//回收子进程
	//错误观念:此时status的值变成了10
	//因为status不是被整体使用的,有自己的位图结构
	if(id > 0)
	{
		printf("wait success:%d,sig number:%d,child exit code:%d\n",ret,(status & 0x7F,(status>>8)&0xFF);
	}
	//sig number:0 child exit code:10
	sleep(5);//父进程退出
}

监控脚本:

ps ajx | head -1 && ps axj | grep mytest | grep -v grep
第一句话是把标题拿出来
第二句话是把matest的进程信息拿出来
第三句话是把grep本身的进程信息去掉
之后写一个循环语句
while :; do ps ajx | head -1 && ps axj | grep mytest | grep -v grep; sleep 1; done

![[Pasted image 20221212134644.png]]
(ctrl + z 退出)

进程等待的方法

wait方法

:!man 2 wait //可以查看到相关wait的用法信息
需要包含两个头文件:
#include<sys/types.h>  
#include<sys/wait.h>  
pid_t wait(int* status);  
返回值:  
成功返回被等待进程pid,失败返回-1。  
参数:  
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

waitpid方法

pid_ t waitpid(pid_t pid, int *status, int options);  
返回值:  
当正常返回的时候waitpid返回收集到的子进程的进程ID;  
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;  
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;  
参数:  
	pid:  
		Pid=-1,等待任一个子进程。与wait等效。  
		Pid>0.等待其进程ID与pid相等的子进程。  
	status:  
		WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)  
		WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)  
	options:  
		WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。

这里有一个问题:
status按理来说是会返回信号+退出码的,但是一个整数怎么能返回两个整数呢?
所以我们不应该吧status当作一个完整的整数,而是应该看作位图结构(后面会详解)

宏的使用方法

int ret = waitpid(id,&status,0)
if(ret > 0)
{
	//是否正常退出
	if(WIFEXITED(status))
	{
		//判断子进程运行结果是否OK
		printf("exit code :%d\n",WEXITSTATUS(status));
	}
	else//出异常了(比如kill -9)
	{
		//TODO
		printf("child exit not normal\n");
	}
}
  • 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
  • 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
  • 如果不存在该子进程,则立即出错返回。
    ![[Pasted image 20221112090227.png]]

获取子进程status

wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
如果传递NULL,表示不关心子进程的退出状态信息。
否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(本身是32位,但是这里我们只研究status低16比特位):

1.代码跑完,结果是对的
2.代码跑完,结果是错的
3.代码没有跑完,出异常了

终止信号(即退出码)用来表示是否正常结束(为0则表示正常退出)
用退出状态表示结果是否正确(通过kill -l进行对应数字的查看(比如发生段错误,除0操作等)),此时退出码是否为0或者是其他的,都无意义,我们不讨论它.
第七位是信号编号
![[Pasted image 20221112090250.png]]

获取退出状态:
(status>>8)&0xFF )
获取终止信号:
``(status & 0x7F))

#include<sys/types.h>
#include<stdio.h>
#include<sys/wait.h>  
int main()
{
	pid_t id = fork();
	if (id == 0)
	{
		int cnt = 10;
		while (cnt)
		{
			printf("我是子进程:%d,父进程:%d,cnt:%d\n", getpid(), getppid(), cnt--);
			sleep(1);
		}
	}
	int status = 0;
	//1.让OS释放子进程的僵尸状态
	//2.获取子进程的退出结果
	//3.在等待期间,子进程没有退出的时候,父进程只能阻塞等待.
	pid_t ret = waitpid(id, &status, 0);
	if (id > 0)
	{
		printf("wait success: %d,sig number : %d ,child exit code: %d\n",ret,(status & 0x7F),(status>>8)&0xFF );
	}
	
}
测试代码:  
#include <sys/wait.h>  
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <errno.h>  
int main( void )
{  
	pid_t pid;  
	if ( (pid=fork()) == -1 )  
		perror("fork"),exit(1);  
	if ( pid == 0 ){  
		sleep(20);  
		exit(10);  
	} else {  
		int st;  
		int ret = wait(&st);  
		if ( ret > 0 && ( st & 0X7F ) == 0 ){ // 正常退出  
			printf("child exit code:%d\n", (st>>8)&0XFF);  
		} else if( ret > 0 ) { // 异常退出  
			printf("sig code : %d\n", st&0X7F );  
		}  
	}  
}  
测试结果:  
	[root@localhost linux]# ./a.out #等20秒退出  
	child exit code:10  
	[root@localhost linux]# ./a.out #在其他终端kill掉  
	sig code : 9

等待的本质:
监测子进程退出的信息
并且将子进程退出信息通过status拿回来
这个信息(exit code ,signal)在子进程的PCB中

![[进程控制 2022-12-12 14.39.31.excalidraw]]

再谈进程退出:
1.进程退出会变成僵尸进程->会把自己的退出结果写到自己的task_struct
2.wait/waitpid是一个系统调用->OS->OS有资格也有能力去读取子进程的task_struct

阻塞VS非阻塞

概念对比

例子:

1.不挂电话,监测李四的状态->阻塞
2.张三->李四,本质是状态检测,如果没有就绪,直接返回->每一次都是一次非阻塞等待->多次非阻塞等待我们称为:轮询

打电话->系统调用wait/waitpid
张三->父进程
李四->子进程
我们上面写的代码都是阻塞等待
轮询等待:

#include<sys/types.h>
#include<stdio.h>
#include<sys/wait.h>  
int main()
{
	pid_t id = fork();
	if (id == 0)
	{
		int cnt = 10;
		while (cnt)
		{
			printf("我是子进程:%d,父进程:%d,cnt:%d\n", getpid(), getppid(), cnt--);
			sleep(1);
		}
	}
	int status = 0;
	while(1)//这是一个死循环,作为轮询等待的一个条件!
	{
		pid_t ret = waitpid(id, &status, WNOHANG);
		//wnohang:非阻塞:子进程没有退出时,父进程监测之后立即返回
		//如果没有设置WNOHANG,则会卡在这一步,除非子进程退出,或者你的id值设置得有问题.否则不会执行下面的代码
		if(ret == 0)//设置了WNOHANG才会可能出现返回值为0的情况
		{
		//waitpid调用成功&&子进程没有退出,我的waitpid没有等待失败
		
		//仅仅是监测到了子进程没有退出
		printf("wait done,but child is running\n",);
		}
		else if(ret > 0)
		{
	        //1.waitpid调用成功(等待成功)&&子进程退出了		
			printf("wait success: %d,sig number : %d ,child exit code: %d\n",ret,(status & 0x7F),(status>>8)&0xFF );
			break;
		}
		else
		{
			//waitpid调用失败,返回值为-1
			//eg.传入的id参数传错了
			printf("waitpid call failed\n");
			break;
		}
		sleep(1);
	}
}

非阻塞有什么好处

不会占用父进程的所有精力,可以在轮询期间干干其他的
[[指针进阶]]
[[typedef函数进阶]]

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>
#include <stdlib.h>

#define TASK_NUM 10

typedef void (*func_t)();
func_t other_task[TASK_NUM] = {NULL};

void sync_disk()
{
    printf("这是一个刷新磁盘的任务\n");
}
void sync_log()
{
    printf("这是一个同步日志的任务\n");
}
void net_send()
{
    printf("这是一个网络发送的任务\n");
}

void LoadTask(func_t func)
{
    for (int i = 0; i < TASK_NUM; i++)
    {
        if (other_task[i] == NULL)
        {
            other_task[i] = func;
            break;
        }
    }
}

void InitTask()
{
    for (int i = 0; i < TASK_NUM; i++)
        other_task[i] = NULL;
    LoadTask(sync_disk);
    LoadTask(sync_log);
    LoadTask(net_send);
}

void RunTask()
{
    for (int i = 0; i < TASK_NUM; i++)
    {
        if (other_task[i] == NULL)
            continue;
        other_task[i]();
    }
}

int main()
{
    pid_t id = fork();
    assert(id >= 0);
    if (id == 0)
    {
        int cnt = 5;
        while (cnt)
        {
            printf("我是子进程,我的pid是:%d,我的ppid是%d,我还有%dS存活\n", getpid(), getppid(), cnt--);
            sleep(1);
        }
        printf("我已经退出\n");
        exit(11);
    }
    InitTask();
    while (1)
    {
        sleep(1);
        int status;
        pid_t ret_id = waitpid(id, &status, WNOHANG);
        if (ret_id > 0)
        {
            printf("我是父进程,我已经回收子进程,ret_id是%d,退出码为%d,退出信号为%d\n", ret_id, (status >> 8) & 0xFF, status & 0x7F);
            exit(0);
        }
        else if (ret_id == 0)
        {
            RunTask();
            continue;
        }
        else
        {
            printf("调用出错\n");
        }
    }
}

具体代码实现

进程的阻塞等待方式:

#define _CRT_SECURE_NO_WARNINGS 1
int main()
{
	pid_t pid;
	pid = fork();
	if (pid < 0) {
		printf("%s fork error\n", __FUNCTION__);
		return 1;
	}
	else if (pid == 0) { //child
		printf("child is run, pid is : %d\n", getpid());
		sleep(5);
		exit(257);
	}
	else {
		int status = 0;
		pid_t ret = waitpid(-1, &status, 0);//阻塞式等待,等待5S
		printf("this is test for wait\n");
		if (WIFEXITED(status) && ret == pid) {
			printf("wait child 5s success, child return code is :%d.\n", WEXITSTATUS(status));
		}
		else {
			printf("wait child failed, return.\n");
			return 1;
		}
	}
	return 0;
}
运行结果:
[root@localhost linux] # . / a.out
child is run, pid is : 45110
this is test for wait
wait child 5s success, child return code is : 1.

进程的非阻塞等待方式:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
	pid_t pid;
	pid = fork();
	if (pid < 0) {
		printf("%s fork error\n", __FUNCTION__);
		return 1;
	}
	else if (pid == 0) { //child
		printf("child is run, pid is : %d\n", getpid());
		sleep(5);
		exit(1);
	}
	else {
		int status = 0;
		pid_t ret = 0;
		do
		{
			ret = waitpid(-1, &status, WNOHANG);//非阻塞式等待->子进程没有退出,父进程检测时候,立即返回
			if (ret == 0) 
			{
				//子进程没有退出,我的waitpid没有等待失败
				//仅仅是监测到了子进程没有退出
				printf("child is running\n");
			}
			sleep(1);
		} while (ret == 0);
		if (WIFEXITED(status) && ret == pid) {
			printf("wait child 5s success, child return code is :%d.\n", WEXITSTATUS(status));
		}
		else {
			printf("wait child failed, return.\n");
			return 1;
		}
	}
	return 0;
}

让父进程做其他任务

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>

#define TASK_NUM 10

// 要保存的任务相关的
typedef void (*func_t)(); //?
func_t other_task[TASK_NUM] = {NULL};  //函数指针数组

// 预设一批任务
void sync_disk()
{
    printf("这是一个刷新数据的任务!\n");
}
void sync_log()
{
    printf("这是一个同步日志的任务!\n");
}
void net_send()
{
    printf("这是一个进行网络发送的任务!\n");
}

//将任务加载进任务列表
int LoadTask(func_t func)
{
    int i = 0;
    for(; i < TASK_NUM; i++){
        if(other_task[i] == NULL) break;
    }
    if(i == TASK_NUM) return -1;
    else other_task[i] = func;

    return 0;
}

void InitTask()
{
    for(int i = 0; i < TASK_NUM; i++) other_task[i] = NULL;
    LoadTask(sync_disk);
    LoadTask(sync_log);
    LoadTask(net_send);
}

void RunTask()
{
    for(int i = 0; i < TASK_NUM; i++)
    {
        if(other_task[i] == NULL) continue;
        other_task[i]();
    }
}

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //子进程
        int cnt = 50;
        while(cnt)
        {
            printf("我是子进程,我还活着呢,我还有%dS, pid: %d, ppid%d\n", cnt--, getpid(), getppid());
            sleep(1);
            //int *p = NULL;
            //*p = 100;
        }
        exit(111);
    }

    InitTask();
    // 父进程
    //pid_t ret_id = wait(NULL);
    while(1)
    {
        int status = 0;
        pid_t ret_id = waitpid(id, &status, WNOHANG); // 夯住了
        if(ret_id < 0)
        {
            printf("waitpid error!\n");
            exit(1);
        }
        else if(ret_id == 0)
        {
            RunTask();
            sleep(1);
            continue;
        }
        else{
            if(WIFEXITED(status)) // 是否收到信号
            {
                printf("wait success, child exit code: %d\n", WEXITSTATUS(status));
            }
            else
            {
                printf("wait success, child exit signal: %d\n", status & 0x7F);
            }
           // printf("我是父进程,等待子进程成功, pid: %d, ppid: %d, ret_id: %d, child exit code: %d, child exit signal: %d\n",\
           //     getpid(), getppid(), ret_id, (status>>8)&0xFF, status & 0x7F);
            break;
        }
    }

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

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

相关文章

2023腾讯云双11优惠3年轻量2核2G4M服务器366.6元,三年价哦!

腾讯云3年轻量应用服务器配置为2核2G4M带宽、50GB SSD系统盘双11优惠价格366.6元三年、108元一年&#xff0c;只是限制月流量&#xff0c;套餐自带300GB月流量。腾讯云百科txybk.com分享2023腾讯云双11优惠活动3年轻量2核2G4M带宽优惠价格、购买条件&#xff1a; 3年轻量2核2G…

大模型的“成本瘦身”运动

数据大、参数量大、算力大&#xff0c;大模型的某些能力才会“涌现”&#xff0c;这一点在科技圈广为流传。 做大模型的主流思想是&#xff1a;不要轻易说模型“不行”&#xff0c;如果“它还没行”&#xff0c;那就做得更大一点。 所以&#xff0c;不到一年的时间&#xff0c;…

MySQL InnoDB数据存储结构

1. 数据库的存储结构&#xff1a;页 索引结构给我们提供了高效的索引方式&#xff0c;不过索引信息以及数据记录都是保存在文件上的&#xff0c;确切说是存储在页结构中。另一方面&#xff0c;索引是在存储引擎中实现的&#xff0c;MySQL服务器上的存储引擎负责对表中数据的读…

Visual Studio使用Git忽略不想上传到远程仓库的文件

前言 作为一个.NET开发者而言&#xff0c;有着宇宙最强IDE&#xff1a;Visual Studio加持&#xff0c;让我们的开发效率得到了更好的提升。我们不需要担心环境变量的配置和其他代码管理工具&#xff0c;因为Visual Studio有着众多的拓展工具。废话不多说&#xff0c;直接进入正…

佳易王商超便利店等会员快速积分、积分兑换管理系统软件下载

佳易王商超便利店等会员快速积分、积分兑换管理系统软件下载 一、佳易王会员管理软件大众版 部分功能简介&#xff1a; 1、会员信息登记 &#xff1a;可以直接使用手机号登记&#xff0c;也可以使用实体卡片&#xff0c;推荐用手机号即可。 2、会员卡类型 &#xff1a;可以自…

6.Spark共享变量

概述 共享变量 共享变量的工作原理Broadcast VariableAccumulator 共享变量 共享变量的工作原理 通常&#xff0c;当给 Spark 操作的函数(如 mpa 或 reduce) 在 Spark 集群上执行时&#xff0c;函数中的变量单独的拷贝到各个节点上&#xff0c;函数执行时&#xff0c;使用…

074基于web+springboot的智能物流管理系统

欢迎大家关注&#xff0c;一起好好学习&#xff0c;天天向上 文章目录 一项目简介技术介绍 二、功能组成三、效果图四、 文章目录 一项目简介 本智能物流管理系统有管理员&#xff0c;顾客&#xff0c;员工&#xff0c;店主。功能有个人中心&#xff0c;顾客管理&#xff0c;员…

零基础Linux_25(多线程)信号量+基于环形队列的生产消费模型+自选锁+读写锁

目录 1. 信号量 1.1 信号量和信号量操作的概念 1.2 信号量的基本使用接口 2. 基于环形队列的生产者消费者模型 2.1 环形队列再分析 2.2 代码分步实现 sem.hpp ringQueue.hpp testMain.cc 2.3 代码解析和再理解 3. 自旋锁和读写锁 3.1 自旋锁的概念和接口 3.2 读写…

腾讯云3年轻量应用服务器涨价了?阿里云降价腾讯云涨?

2023双11云服务器优惠活动上线&#xff0c;腾讯云3年轻量应用服务器价格非常优惠&#xff0c;阿里云双11活动上是后上的&#xff0c;阿里云推出一款新老用户均可以购买的云服务器ECS经济型e实例&#xff0c;2核2G3M固定带宽&#xff0c;一年只要99元&#xff0c;第二年续费依旧…

0基础学编程从哪里入手?零基础学些代码怎么入手

0基础学编程从哪里入手&#xff1f;零基础学些代码怎么入手&#xff1f; 给大家分享一款中文编程工具&#xff0c;零基础轻松学编程&#xff0c;不需英语基础&#xff0c;编程工具可下载。 这款工具不但可以连接部分硬件&#xff0c;而且可以开发大型的软件&#xff0c;向如图…

LangChain安装和入门案例

一、介绍 LangChain是一个用于开发由语言模型驱动的应用程序的框架 官网 https://www.langchain.com/ 中文官网 https://www.langchain.com.cn/ python langchain https://python.langchain.com.cn/docs/get_started/introduction https://python.langchain.com/docs/ge…

小程序使用echarts(超详细教程)

小程序使用echarts第一步就是先引用到小程序里面&#xff0c;可以直接从这里下载 文件很多&#xff0c;我们值下载 ec-canvas 就好&#xff0c;下载完成后&#xff0c;直接放在pages同级目录下 index.js 在我们需要的页面的 js 文件顶部引入 // pages/index/index.js impor…

项目管理之如何估算项目工作时间

在项目管理中&#xff0c;项目工作时间的估算是一个关键环节&#xff0c;它直接影响到项目的进度、预算和资源分配。本文将介绍几种常用的时间估算技术和时间估算的十步法&#xff0c;帮助你更好地估算项目工作时间。 常用时间估算技术 类比估算 参照以往同类同规模项目时间数…

01_stable_diffusion_introduction_CN

stable_diffusion 配置 !pip install -Uq diffusers ftfy accelerate# Installing transformers from source for now since we need the latest version for Depth2Img: !pip install -Uq githttps://github.com/huggingface/transformers import torch import requests fro…

项目实战:删除特定水果库存记录

1、在index.js中添加删除点击事件 1.1、common.js function $(key){if(key){if(key.startsWith("#")){key key.substring(1)return document.getElementById(key)}else{let nodeList document.getElementsByName(key)return Array.from(nodeList)}} } window.onloa…

c语言从入门到实战——VS2022实用调试技巧

VS实用调试技巧 前言1. 什么是bug2. 什么是调试&#xff08;debug&#xff09;3. Debug和Release4. VS调试快捷键4.1 环境准备4.2 调试快捷键 5. 监视和内存观察5.1 监视5.2 内存 6. 调试举例17. 调试举例28. 编程常见错误归类8.1 编译型错误8.2 链接型错误8.3 运行时错误 前言…

UI自动化测试:会消失的弹窗(Toast)如何定位?

前言 看到标题可能有的小伙伴们懵了&#xff0c;什么是Toast&#xff0c;其实Toast大家都见过&#xff0c;就是一般在我们页面中停留大概2~3秒的时间后自动消失的弹框&#xff0c;那么既然要做自动化&#xff0c;可能Toast也需要大家进行测试&#xff0c;那么小编今天就来介绍…

如何使用CSS命名规范提高您的编码效率

CSS命名约定可以提高团队成员在项目中的协作能力&#xff0c;通过允许开发人员简化工作流程&#xff0c;增强项目的可维护性和可扩展性。在本文中&#xff0c;我们将深入探讨CSS命名约定的世界&#xff0c;展示实际示例以及它们为您的开发过程带来的好处。 在前端开发中&#x…

[极客大挑战 2019]Http 1

题目环境&#xff1a; 看起来挺花里胡哨的 F12查看源代码寻找隐藏文件 这是啥子呀&#xff0c;果然防不胜防 点击隐藏文件Secret.php 它不是来自这个地址的请求 报头&#xff1a;https://Sycsecret.buuoj.cn 需要抓包&#xff0c;在抓包前了解部分数据包参数 GET:到 Host:来自 …

Flutter vs 前端 杂谈:SliverAppBar、手动实现Appbar、前端Html+JS怎么实现滚动变化型Appbar - 比较

Flutter vs 前端 杂谈 SliverAppBar的弹性背景的显隐效果使用HtmlJS怎么实现 作者&#xff1a;李俊才 &#xff08;jcLee95&#xff09;&#xff1a;https://blog.csdn.net/qq_28550263 邮箱 &#xff1a;291148484163.com 本文地址&#xff1a;https://blog.csdn.net/qq_28550…