您当前的位置:首页 > 电脑百科 > 程序开发 > 移动端 > 百科

300行代码实现一个多任务OS

时间:2022-09-25 10:44:09  来源:今日头条  作者:嵌入式Linux

本期主角:cola_os,它是一个开源OS,包含了300多行代码实现的多任务管理功能。对于很多MCU开发者来说,任务变多了之后,管理任务变得越来越困难,并且实时性的要求也变得更高。

在这种情况下,使用RTOS显然是有些浪费的,这时可以考虑使用基于软件定时器实现的时间片轮询框架,例如cola_os。

cola_os的仓库链接为https://gitee.com/schuck/cola_os,使用的license为MulanPSL-1.0(木兰宽松许可证,第1版)。

cola_os代码十分简洁明了,并且包含了很多有用的编程思想,非常值得通读。接下来,我们一起来分析并学习cola_os的使用。

回到cola_os的内容上,它主要包含以下几个部分:

image-20231105210345230
image-20231105210345230
image-20231105210359165
image-20231105210359165

1、cola_os

cola_os就是cola_os的任务管理模块。任务使用链表进行管理,其数据结构如:

typedef void (*cbFunc)(uint32_t event);

typedef struct task_s
{
    uint8_t     timerNum;    //定时编号
    uint32_t    period;      //定时周期
    bool        oneShot;     //true只执行一次
    bool        start;       //开始启动
    uint32_t    timerTick;   //定时计数
    bool        run;         //任务运行标志 
    bool        taskFlag;    //任务标志是主任务还是定时任务
    uint32_t    event;       //驱动事件  
    cbFunc      func;        //回调函数
    struct task_s *next;
}task_t;

每创建一个任务吗,就是往任务链表中插入一个任务节点。

其创建任务的方法有两种:

  • 创建主循环任务
  • 创建定时任务

两种方式创建,都是会在while(1)循环中调度执行任务函数。

我们可以看看cola_task_loop任务遍历函数,这个函数最终是要放在主函数while(1)中调用的。其内容如:

void cola_task_loop(void)
{
    uint32_t events;
    task_t *cur  = task_list;
    OS_CPU_SR cpu_sr;
    
    while( cur != NULL )
    {
        if(cur->run)
        {
            if(NULL !=cur->func)
            {
                events = cur->event;
                if(events)
                {
                    enter_critical();
                    cur->event = 0;
                    exit_critical();
                }
                cur->func(events);                
            }
            if(TASK_TIMER == cur->taskFlag)
            {
                enter_critical();
                cur->run = false;
                exit_critical();
            }
            if((cur->oneShot)&&(TASK_TIMER == cur->taskFlag))
            {
               cur->start = false; 
            }
        }
        cur = cur->next;
    }
}

两种方式创建的任务都会在cur->func(events);被调用。不同的就是:遍历执行到定时任务时,需要清掉定时相关标志。

其中,events作为任务函数的参数传入。从cola_task_loop可以看到,事件并未使用到,events无论真还是假,在执行任务函数前,都被清零了。events的功能应该是作者预留的。

创建任务很简单,比如创建一个定时任务:

static task_t timer_500ms;

//每500ms执行一次
static void timer_500ms_cb(uint32_t event)
{
    printf("task0 running...n");
}

cola_timer_create(&timer_500ms, timer_500ms_cb);
cola_timer_start(&timer_500ms, TIMER_ALWAYS, 500);

cola_os是基于软件定时器来进行任务调度管理的,需要一个硬件定时器提供时基。比如使用系统滴答定时器,配置为1ms中断一次。

在1ms中断中不断轮询判断定时计数是否到达定时时间:

void SysTick_Handler(void)
{
  cola_timer_ticker();
}

void cola_timer_ticker(void)
{
    task_t *cur  = task_list;
    OS_CPU_SR cpu_sr;
    while( cur != NULL )
    {
        if((TASK_TIMER == cur->taskFlag)&& cur->start)
        {
            if(++cur->timerTick >= cur->period)
            {
                cur->timerTick = 0;
                if(cur->func != NULL)
                {
                    enter_critical();                   
                    cur->run = true;
                    exit_critical();
                }
            }
        }
        cur = cur->next;
    }
}

如果到了则将标志cur->run置位,在while大循环中的cola_task_loop函数中如果检测到该标志就执行该任务函数。

2、cola_device

cola_device是硬件抽象层,使用链表来管理各个设备。其借鉴了RT-Thread及linux相关驱动框架思想。大致内容如:

数据结构如:

typedef struct cola_device  cola_device_t;

struct cola_device_ops
{
    int  (*init)   (cola_device_t *dev);
    int  (*open)   (cola_device_t *dev, int oflag);
    int  (*close)  (cola_device_t *dev);
    int  (*read)   (cola_device_t *dev, int pos, void *buffer, int size);
    int  (*write)  (cola_device_t *dev, int pos, const void *buffer, int size);
    int  (*control)(cola_device_t *dev, int cmd, void *args);

};

struct cola_device
{
    const char * name;
    struct cola_device_ops *dops;
    struct cola_device *next;
};

硬件抽象层的接口如:

/*
    驱动注册
*/
int cola_device_register(cola_device_t *dev);
/*
    驱动查找
*/
cola_device_t *cola_device_find(const char *name);
/*
    驱动读
*/
int cola_device_read(cola_device_t *dev,  int pos, void *buffer, int size);
/*
    驱动写
*/
int cola_device_write(cola_device_t *dev, int pos, const void *buffer, int size);
/*
    驱动控制
*/
int cola_device_ctrl(cola_device_t *dev,  int cmd, void *arg);

首先,在驱动层注册好设备,把操作设备的函数指针及设备名称插入到设备链表中:

static cola_device_t led_dev;

static void led_gpio_init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOC, ENABLE);
    GPIO_InitStructure.GPIO_Pin = PIN_GREENLED;                            
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;                     
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;                  
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;                     
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;                  
    GPIO_Init(PORT_GREEN_LED, &GPIO_InitStructure);
    LED_GREEN_OFF;
}

static int led_ctrl(cola_device_t *dev, int cmd, void *args)
{
    if(LED_TOGGLE == cmd)
    {
        LED_GREEN_TOGGLE;
    }
    else 
    {
        
    }
    return 1;
}


static struct cola_device_ops ops =
{
    .control = led_ctrl,
};

static void led_register(void)
{
    led_gpio_init();
    led_dev.dops = &ops;
    led_dev.name = "led";
    cola_device_register(&led_dev);
}

cola_device_register函数如:

int cola_device_register(cola_device_t *dev)
{
    if((NULL == dev) || (cola_device_is_exists(dev)))
    {
        return 0;
    }

    if((NULL == dev->name) ||  (NULL == dev->dops))
    {
        return 0;
    }
    return device_list_inster(dev);
}

驱动注册好设备之后,应用层就可以根据设备名称来查找设备是否被注册,如果已经注册则可以调用设备操作接口操控设备。比如创建一个定时任务定时反转led:

void App_init(void)
{
    app_led_dev = cola_device_find("led");
    assert(app_led_dev);
    cola_timer_create(&timer_500ms,timer_500ms_cb);
    cola_timer_start(&timer_500ms,TIMER_ALWAYS,500);
}

static void timer_500ms_cb(uint32_t event)
{
    cola_device_ctrl(app_led_dev,LED_TOGGLE,0);
}

3、cola_init

cola_init是一个自动初始化模块,模仿Linux的initcall机制。RT-Thread也有实现这个功能:

image-20231105210408193
image-20231105210408193

一般的,我们的初始化在主函数中调用,如:

image-20231105210411356
image-20231105210411356

有了自动初始化模块,可以不在主函数中调用,例如:

void SystemClock_Config(void)
{
}
pure_initcall(SystemClock_Config);

这样也可以调用SystemClock_Config。pure_initcall如:

#define  __used  __attribute__((__used__))

typedef void (*initcall_t)(void);

#define __define_initcall(fn, id) 
    static const initcall_t __initcall_##fn##id __used 
    __attribute__((__section__("initcall" #id "init"))) = fn; #define pure_initcall(fn)       __define_initcall(fn, 0) //可用作系统时钟初始化  #define fs_initcall(fn)         __define_initcall(fn, 1) //tick和调试接口初始化#define device_initcall(fn)     __define_initcall(fn, 2) //驱动初始化#define late_initcall(fn)       __define_initcall(fn, 3) //其他初始化

在cola_init中,首先是调用不同顺序级别的__define_initcall宏来把函数指针fn放入到自定义的指定的段中。各个需要自动初始化的函数放到指定的段中,形成一张初始化函数表。

__ attribute __ (( __ section __)) 关键字就是用来指定数据存放段。

do_init_call函数在我们程序起始时调用,比如在bsp_init中调用:

void bsp_init(void)
{
    do_init_call();
}

do_init_call里做的事情就是遍历初始化函数表里的函数:

void do_init_call(void)
{
    extern initcall_t initcall0init$$Base[];
    extern initcall_t initcall0init$$Limit[];
    extern initcall_t initcall1init$$Base[];
    extern initcall_t initcall1init$$Limit[];
    extern initcall_t initcall2init$$Base[];
    extern initcall_t initcall2init$$Limit[];
    extern initcall_t initcall3init$$Base[];
    extern initcall_t initcall3init$$Limit[];
    
    initcall_t *fn;
    
    for (fn = initcall0init$$Base;
            fn $Limit;
            fn++)
    {
        if(fn)
            (*fn)();
    }
    
    for (fn = initcall1init$$Base;
            fn $Limit;
            fn++)
    {
        if(fn)
            (*fn)();
    }
    
    for (fn = initcall2init$$Base;
            fn $Limit;
            fn++)
    {
        if(fn)
            (*fn)();
    }
    
    for (fn = initcall3init$$Base;
            fn $Limit;
            fn++)
    {
        if(fn)
            (*fn)();
    }
}

这里有 initcall0init $$ Base 及 initcall0init Limit这几个initcall_t类型的函数指针数组的声明。它们事先是调用__define_initcall把函数指针fn放入到自定义的指定的段.initcall0init、.initcall1init、.initcall2init、.initcall3init。

initcall0init$$Baseinitcall0init$$Limit按照我的理解就是各个初始化函数表的开始及结束地址。从而实现遍历:

    for (fn = initcall0init$$Base;
            fn $Limit;
            fn++)
    {
        if(fn)
            (*fn)();
    }

例如RT-Thread里的实现也是类似的:

    volatile const init_fn_t *fn_ptr;

    for (fn_ptr = &__rt_init_rti_board_start; fn_ptr 

关于init自动初始化机制大致就分析这些。

cola_os包含有cola_os任务管理、cola_device硬件抽象层及cola_init自动初始化三大块,这三块内容其实可以单独抽出来学习、使用。

4、cola_os的使用

下面我们基于小熊派IOT开发板来简单实践实践。

image-20231105210416988
image-20231105210416988

我们创建两个定时任务:

  • task0任务:定时500ms打印一次。
  • task1任务:定时1000ms打印一次。

mAIn.c:

/* Private variables ---------------------------------------------------------*/
static task_t timer_500ms;
static task_t timer_1000ms;
/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);

/* USER CODE BEGIN PFP */
/* Private function prototypes -----------------------------------------------*/

/* USER CODE END PFP */

/* USER CODE BEGIN 0 */

//每500ms执行一次
static void timer_500ms_cb(uint32_t event)
{
    printf("task0 running...n");
}

//每1000ms执行一次
static void timer_1000ms_cb(uint32_t event)
{
    printf("task1 running...n");
}

int main(void)
{
  /* USER CODE BEGIN 1 */
 
  /* USER CODE END 1 */

  /* MCU Configuration----------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
//  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */
  printf("微信公众号:嵌入式大杂烩rn");
  printf("cola_os test!rn");
  
  cola_timer_create(&timer_500ms,timer_500ms_cb);
  cola_timer_start(&timer_500ms,TIMER_ALWAYS,500);
  cola_timer_create(&timer_1000ms,timer_1000ms_cb);
  cola_timer_start(&timer_1000ms,TIMER_ALWAYS,1000);

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {

  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */
    cola_task_loop();
  }
  /* USER CODE END 3 */

}

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{

  RCC_OscInitTypeDef RCC_OscInitStruct;
  RCC_ClkInitTypeDef RCC_ClkInitStruct;
  RCC_PeriphCLKInitTypeDef PeriphClkInit;

    /**Initializes the CPU, AHB and APB busses clocks 
    */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_MSI;
  RCC_OscInitStruct.MSIState = RCC_MSI_ON;
  RCC_OscInitStruct.MSICalibrationValue = 0;
  RCC_OscInitStruct.MSIClockRange = RCC_MSIRANGE_6;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_MSI;
  RCC_OscInitStruct.PLL.PLLM = 1;
  RCC_OscInitStruct.PLL.PLLN = 40;
  RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7;
  RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2;
  RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    _Error_Handler(__FILE__, __LINE__);
  }

    /**Initializes the CPU, AHB and APB busses clocks 
    */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK)
  {
    _Error_Handler(__FILE__, __LINE__);
  }

  PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART1;
  PeriphClkInit.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2;
  if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
  {
    _Error_Handler(__FILE__, __LINE__);
  }

    /**Configure the main internal regulator output voltage 
    */
  if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK)
  {
    _Error_Handler(__FILE__, __LINE__);
  }

    /**Configure the Systick interrupt time 
    */
  HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);

    /**Configure the Systick 
    */
  HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);

  /* SysTick_IRQn interrupt configuration */
  HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);
}
pure_initcall(SystemClock_Config);

SysTick_Handler:

void SysTick_Handler(void)
{
  /* USER CODE BEGIN SysTick_IRQn 0 */

  /* USER CODE END SysTick_IRQn 0 */
  cola_timer_ticker();
  HAL_IncTick();
  HAL_SYSTICK_IRQHandler();
  /* USER CODE BEGIN SysTick_IRQn 1 */

  /* USER CODE END SysTick_IRQn 1 */
}

编译、下载、运行:

image-20231105210422325
image-20231105210422325

从运行结果可以看到,task1的定时周期是task0的两倍,符合预期。



Tags:OS   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
从Windows到MacOS:操作系统的选择与个人偏好
在数字世界的广阔天地中,操作系统作为计算机的核心软件,为用户提供了与计算机硬件交互的平台。在众多操作系统中,Windows和MacOS因其各自的特点和优势,占据了市场的主导地位。本...【详细内容】
2024-04-11  Search: OS  点击:(4)  评论:(0)  加入收藏
Win10/Win11和 macOS用户反馈:谷歌云服务“捆绑”系统 DNS 设置
IT之家 4 月 6 日消息,谷歌公司承认旗下的 Google One 订阅服务中存在问题,在 Windows 10、Windows 11 以及 macOS 系统上会更改系统 DNS 设置,变更为 8.8.8.8 地址。Google On...【详细内容】
2024-04-08  Search: OS  点击:(7)  评论:(0)  加入收藏
iOS 17.5进一步开放 游戏可官网下载直接安装
苹果iOS 17.5版本系统将在近期开放测试,在该版本中,苹果会进一步开放侧载功能,让用户能够如同安卓手机那样,随意安装应用。据外媒消息称,苹果iOS 17.5版本系统将在近期开放测试,在...【详细内容】
2024-04-02  Search: OS  点击:(7)  评论:(0)  加入收藏
如何正确选择NoSQL数据库
译者 | 陈峻审校 | 重楼Allied Market Research最近发布的一份报告指出,业界对于NoSQL数据库的需求正在持续上升。2022年,全球NoSQL市场的销售额已达73亿美元,预计到2032年将达...【详细内容】
2024-03-28  Search: OS  点击:(14)  评论:(0)  加入收藏
揭秘 iOS 17.4:欧盟独享功能,其他地区无法体验
【环球网科技综合报道】据外媒消息,随着iOS 17.4的发布,欧洲用户能够独享一些全球其他地区无法获得的新功能。这得益于欧洲联盟的《数字市场法案》,苹果不得不向侧载等功能开放...【详细内容】
2024-03-18  Search: OS  点击:(16)  评论:(0)  加入收藏
GitHub顶流"Web OS"——运行于浏览器的桌面操作系统、用户超100万、原生jQuery和JS编写
Puter 是近日在 GitHub 上最受欢迎的一款开源项目,正式开源还没到一周 ——star 数就已接近 7k。作者表示这个项目已开发 3 年,并获得了超过 100 万用户。根据介绍,P...【详细内容】
2024-03-10  Search: OS  点击:(24)  评论:(0)  加入收藏
苹果iOS17.4值得升级吗?续航信号评测来了
苹果iOS17.4值得升级吗?很多iPhone用户都在纠结这个版本续航和信号都有没提升,纠结是否需要升级,那么这个版本信号信号到底怎样呢?下面就给大家分享iPhone13升级iOS17.4的续航信...【详细内容】
2024-03-07  Search: OS  点击:(10)  评论:(0)  加入收藏
微信发布“朋友圈违规避坑指南”:涉及虚拟货币、售卖 POS 机等
IT之家 3 月 7 日消息,微信安全中心日前发布“朋友圈违规避坑指南”,提醒用户避免上当受骗。微信称,朋友圈是个分享交流的社交场景,好友之间,可以时刻分享生活和乐趣。可是有些人...【详细内容】
2024-03-07  Search: OS  点击:(17)  评论:(0)  加入收藏
章若楠cos王者荣耀大乔,不穿鞋露美背,和张予曦角色错位?
大乔是王者荣耀里的人气女英雄,官方为她出过许多漂亮的皮肤,比如白鹤梁神女。在王者荣耀和时尚芭莎的联动里,邀请了年轻小花章若楠cos大乔的白鹤梁神女造型,一起来看看吧。大乔...【详细内容】
2024-03-04  Search: OS  点击:(50)  评论:(0)  加入收藏
轻松实现Centos系统的软件包安装管理:yum指令实战详解
yum 是一种用于在 CentOS、Red Hat Enterprise Linux (RHEL) 等基于 RPM 的 Linux 发行版上安装、更新和管理软件包的命令行工具。它可以自动解决软件包依赖关系,自动下载并...【详细内容】
2024-02-27  Search: OS  点击:(54)  评论:(0)  加入收藏
▌简易百科推荐
一文搞明白Hive与数据库区别
Hive 采用了类似SQL 的查询语言 HQL (Hive Query Language),因此很容易将 Hive 理解为数据库。其实Hive 是为数据仓库设计的,是数据仓库的客户端,所以要搞清楚Hive与数据库的区...【详细内容】
2023-11-02    尚硅谷教育  Tags:Hive   点击:(186)  评论:(0)  加入收藏
Google Play上线问题及解决方案
将应用上线到Google Play商店也可能会面临一些问题,在上线应用到Google Play商店之前,确保你充分测试应用,遵循Google Play的开发者政策和要求,以及关注用户的反馈,这些都能帮助...【详细内容】
2023-08-24  北京APP外包    Tags:Google Play   点击:(209)  评论:(0)  加入收藏
Google Play应用成功上架秘籍:如何避免封号、拒审、下架?
Google Play是全球最大的移动应用商店之一,它是运行Android操作系统的设备的官方应用商店。它提供各种数字内容,包括应用程序(应用)、游戏、音乐、书籍等,包括免费和付费选项。这...【详细内容】
2023-08-17  Balen跨境说    Tags:Google play   点击:(289)  评论:(0)  加入收藏
开发移动应用如何高效利用Taro工具
最近公司的一些项目需要跨端框架,技术老大选了Taro,实践了一段时间下来,愈发觉得Taro是个好东西,所以在本篇文章中稍微介绍下。什么是Taro?Taro(或称为Taro框架)是一种用于构建跨平...【详细内容】
2023-06-07  Onegun    Tags:Taro工具   点击:(399)  评论:(0)  加入收藏
如何获得GitHub Copilot并结合VS Code使用?
​译者 | 布加迪审校 | 重楼GitHub Copilot是一种基于AI的代码生成工具。它使用OpenAI的GPT(生成式预训练Transformer)技术来提供建议。它可以根据您正在编写的代码上下文建议...【详细内容】
2023-04-17  布加迪  51CTO  Tags:GitHub   点击:(269)  评论:(0)  加入收藏
全面讲解在Rust中处理错误的有效方法
错误不可避免,可能由于各种原因而发生:从无效的用户输入到网络故障、硬件故障或编程错误,不一而足。错误处理是检测和报告错误并从中恢复的机制,以防程序崩溃或数据损坏。有效的...【详细内容】
2023-04-17  布加迪  51CTO  Tags:Rust   点击:(223)  评论:(0)  加入收藏
DevOps 在移动应用程序开发中的作用是什么?
本文探讨了 DevOps 在移动应用程序开发中扮演的角色以及它如何帮助开发人员和企业。您想将您的移动应用程序开发过程提升到一个新的水平吗?如果您在开发应用程序时实施 DevOp...【详细内容】
2023-04-09  科技狠活与软件技术  今日头条  Tags:DevOps   点击:(220)  评论:(0)  加入收藏
一名C++程序员的 Rust入门初体验
作 者 | 吴强强(去鸿)作者最近尝试写了一些Rust代码,本文主要讲述了对Rust的看法和Rust与C++的一些区别。背景S2在推进团队代码规范时,先后学习了盘古编程规范,CPP core guide...【详细内容】
2023-03-31  阿里云云栖号     Tags:Rust   点击:(246)  评论:(0)  加入收藏
利用微信公众号的图片上传接口,创造属于自己的图床功能!
作为一个技术博主,了不起不是在创作就是在创作的路上(当然偶尔也会有点恰饭文~还指望大家多多支持),特别是在写一些技术文章的时候,经常会用到图片,然而为了在多个平台同步发文,就...【详细内容】
2023-03-27  Java极客技术  微信公众号  Tags:接口   点击:(233)  评论:(0)  加入收藏
移动开发中Docker等容器技术如何落地?
移动应用程序的开发面临着很多挑战,包括开发环境的设置、测试的困难、部署的复杂性等。由于移动应用程序通常需要在多个平台上运行,因此开发人员需要花费大量的时间来构建和测...【详细内容】
2023-03-22  Onegun    Tags:移动开发   点击:(148)  评论:(0)  加入收藏
站内最新
站内热门
站内头条