当前位置: 首页 > news >正文

uboot启动linux kernel的流程

目录

  • 前言
  • 流程图
  • autoboot_command
  • run_command_list
  • do_bootm
  • do_bootm_states
  • do_bootm_linux
  • boot_prep_linux
  • boot_jump_linux

前言

本文在u-boot启动流程分析这篇文章的基础上,简要梳理uboot启动linux kernel的流程。

流程图

请添加图片描述
其中,

  • autoboot_command位于uboot/common/autoboot.c
  • run_command_list位于uboot/common/cli.c
  • do_bootm位于uboot/cmd/bootm.c
  • do_bootm_states位于uboot/common/bootm.c
  • do_bootm_linux位于uboot/arch/arm/lib/bootm.c
  • boot_prep_linux位于uboot/arch/arm/lib/bootm.c
  • boot_jump_linux位于uboot/arch/arm/lib/bootm.c

autoboot_command

void autoboot_command(const char *s)
{debug("### main_loop: bootcmd=\"%s\"\n", s ? s : "<UNDEFINED>");if (s && (stored_bootdelay == -2 ||(stored_bootdelay != -1 && !abortboot(stored_bootdelay)))) {bool lock;int prev;lock = IS_ENABLED(CONFIG_AUTOBOOT_KEYED) &&!IS_ENABLED(CONFIG_AUTOBOOT_KEYED_CTRLC);if (lock)prev = disable_ctrlc(1); /* disable Ctrl-C checking */run_command_list(s, -1, 0);if (lock)disable_ctrlc(prev);	/* restore Ctrl-C checking */}if (IS_ENABLED(CONFIG_USE_AUTOBOOT_MENUKEY) &&menukey == AUTOBOOT_MENUKEY) {s = env_get("menucmd");if (s)run_command_list(s, -1, 0);}
}

其中,abortboot会等待一段时间timeout(由环境变量bootdelay设定),
如果有按键被按下,则会返回1,此时将停止启动linux kernel,返回命令行。如果在timeout后依然无按键被按下,则继续执行启动linux kernel的命令。

run_command_list

run_command_list调用do_bootm的流程实现的关键点:
在这里插入图片描述
在上面的代码中,将"bootm"和do_bootm这个函数绑定。

U_BOOT_CMD的定义在uboot/include/command.h中,可以自行查看源代码。

do_bootm

int do_bootm(struct cmd_tbl *cmdtp, int flag, int argc, char *const argv[])
{
#ifdef CONFIG_NEEDS_MANUAL_RELOCstatic int relocated = 0;if (!relocated) {int i;/* relocate names of sub-command table */for (i = 0; i < ARRAY_SIZE(cmd_bootm_sub); i++)cmd_bootm_sub[i].name += gd->reloc_off;relocated = 1;}
#endif/* determine if we have a sub command */argc--; argv++;if (argc > 0) {char *endp;simple_strtoul(argv[0], &endp, 16);/* endp pointing to NULL means that argv[0] was just a* valid number, pass it along to the normal bootm processing** If endp is ':' or '#' assume a FIT identifier so pass* along for normal processing.** Right now we assume the first arg should never be '-'*/if ((*endp != 0) && (*endp != ':') && (*endp != '#'))return do_bootm_subcommand(cmdtp, flag, argc, argv);}return do_bootm_states(cmdtp, flag, argc, argv, BOOTM_STATE_START |BOOTM_STATE_FINDOS | BOOTM_STATE_FINDOTHER |BOOTM_STATE_LOADOS |
#ifdef CONFIG_SYS_BOOT_RAMDISK_HIGHBOOTM_STATE_RAMDISK |
#endif
#if defined(CONFIG_PPC) || defined(CONFIG_MIPS)BOOTM_STATE_OS_CMDLINE |
#endifBOOTM_STATE_OS_PREP | BOOTM_STATE_OS_FAKE_GO |BOOTM_STATE_OS_GO, &images, 1);
}

这个函数最核心的部分就是调用do_bootm_states。

do_bootm_states

int do_bootm_states(struct cmd_tbl *cmdtp, int flag, int argc,char *const argv[], int states, bootm_headers_t *images,int boot_progress)
{boot_os_fn *boot_fn;ulong iflag = 0;int ret = 0, need_boot_fn;images->state |= states;/** Work through the states and see how far we get. We stop on* any error.*/if (states & BOOTM_STATE_START)ret = bootm_start(cmdtp, flag, argc, argv);if (!ret && (states & BOOTM_STATE_FINDOS))ret = bootm_find_os(cmdtp, flag, argc, argv);if (!ret && (states & BOOTM_STATE_FINDOTHER))ret = bootm_find_other(cmdtp, flag, argc, argv);/* Load the OS */if (!ret && (states & BOOTM_STATE_LOADOS)) {iflag = bootm_disable_interrupts();ret = bootm_load_os(images, 0);if (ret && ret != BOOTM_ERR_OVERLAP)goto err;else if (ret == BOOTM_ERR_OVERLAP)ret = 0;}/* Relocate the ramdisk */
#ifdef CONFIG_SYS_BOOT_RAMDISK_HIGHif (!ret && (states & BOOTM_STATE_RAMDISK)) {ulong rd_len = images->rd_end - images->rd_start;ret = boot_ramdisk_high(&images->lmb, images->rd_start,rd_len, &images->initrd_start, &images->initrd_end);if (!ret) {env_set_hex("initrd_start", images->initrd_start);env_set_hex("initrd_end", images->initrd_end);}}
#endif
#if IMAGE_ENABLE_OF_LIBFDT && defined(CONFIG_LMB)if (!ret && (states & BOOTM_STATE_FDT)) {boot_fdt_add_mem_rsv_regions(&images->lmb, images->ft_addr);ret = boot_relocate_fdt(&images->lmb, &images->ft_addr,&images->ft_len);}
#endif/* From now on, we need the OS boot function */if (ret)return ret;boot_fn = bootm_os_get_boot_func(images->os.os);need_boot_fn = states & (BOOTM_STATE_OS_CMDLINE |BOOTM_STATE_OS_BD_T | BOOTM_STATE_OS_PREP |BOOTM_STATE_OS_FAKE_GO | BOOTM_STATE_OS_GO);if (boot_fn == NULL && need_boot_fn) {if (iflag)enable_interrupts();printf("ERROR: booting os '%s' (%d) is not supported\n",genimg_get_os_name(images->os.os), images->os.os);bootstage_error(BOOTSTAGE_ID_CHECK_BOOT_OS);return 1;}/* Call various other states that are not generally used */if (!ret && (states & BOOTM_STATE_OS_CMDLINE))ret = boot_fn(BOOTM_STATE_OS_CMDLINE, argc, argv, images);if (!ret && (states & BOOTM_STATE_OS_BD_T))ret = boot_fn(BOOTM_STATE_OS_BD_T, argc, argv, images);if (!ret && (states & BOOTM_STATE_OS_PREP)) {ret = bootm_process_cmdline_env(images->os.os == IH_OS_LINUX);if (ret) {printf("Cmdline setup failed (err=%d)\n", ret);ret = CMD_RET_FAILURE;goto err;}ret = boot_fn(BOOTM_STATE_OS_PREP, argc, argv, images);}#ifdef CONFIG_TRACE/* Pretend to run the OS, then run a user command */if (!ret && (states & BOOTM_STATE_OS_FAKE_GO)) {char *cmd_list = env_get("fakegocmd");ret = boot_selected_os(argc, argv, BOOTM_STATE_OS_FAKE_GO,images, boot_fn);if (!ret && cmd_list)ret = run_command_list(cmd_list, -1, flag);}
#endif/* Check for unsupported subcommand. */if (ret) {puts("subcommand not supported\n");return ret;}/* Now run the OS! We hope this doesn't return */if (!ret && (states & BOOTM_STATE_OS_GO))ret = boot_selected_os(argc, argv, BOOTM_STATE_OS_GO,images, boot_fn);/* Deal with any fallout */
err:if (iflag)enable_interrupts();if (ret == BOOTM_ERR_UNIMPLEMENTED)bootstage_error(BOOTSTAGE_ID_DECOMP_UNIMPL);else if (ret == BOOTM_ERR_RESET)do_reset(cmdtp, flag, argc, argv);return ret;
}

该函数的核心部分:

  • bootm_load_os将kernel镜像加载到内存中。
  • 调用do_bootm_linux启动kernel。

do_bootm_linux

int do_bootm_linux(int flag, int argc, char *const argv[],bootm_headers_t *images)
{/* No need for those on ARM */if (flag & BOOTM_STATE_OS_BD_T || flag & BOOTM_STATE_OS_CMDLINE)return -1;if (flag & BOOTM_STATE_OS_PREP) {boot_prep_linux(images);return 0;}if (flag & (BOOTM_STATE_OS_GO | BOOTM_STATE_OS_FAKE_GO)) {boot_jump_linux(images, flag);return 0;}boot_prep_linux(images);boot_jump_linux(images, flag);return 0;
}
  • boot_prep_linux负责准备传递给kernel的参数。
  • boot_jump_linux负责跳转执行kernel代码。

boot_prep_linux

static void boot_prep_linux(bootm_headers_t *images)
{char *commandline = env_get("bootargs");if (IMAGE_ENABLE_OF_LIBFDT && images->ft_len) {
#ifdef CONFIG_OF_LIBFDTdebug("using: FDT\n");if (image_setup_linux(images)) {printf("FDT creation failed! hanging...");hang();}
#endif} else if (BOOTM_ENABLE_TAGS) {debug("using: ATAGS\n");setup_start_tag(gd->bd);if (BOOTM_ENABLE_SERIAL_TAG)setup_serial_tag(&params);if (BOOTM_ENABLE_CMDLINE_TAG)setup_commandline_tag(gd->bd, commandline);if (BOOTM_ENABLE_REVISION_TAG)setup_revision_tag(&params);if (BOOTM_ENABLE_MEMORY_TAGS)setup_memory_tags(gd->bd);if (BOOTM_ENABLE_INITRD_TAG) {/** In boot_ramdisk_high(), it may relocate ramdisk to* a specified location. And set images->initrd_start &* images->initrd_end to relocated ramdisk's start/end* addresses. So use them instead of images->rd_start &* images->rd_end when possible.*/if (images->initrd_start && images->initrd_end) {setup_initrd_tag(gd->bd, images->initrd_start,images->initrd_end);} else if (images->rd_start && images->rd_end) {setup_initrd_tag(gd->bd, images->rd_start,images->rd_end);}}setup_board_tags(&params);setup_end_tag(gd->bd);} else {printf("FDT and ATAGS support not compiled in - hanging\n");hang();}board_prep_linux(images);
}

可以看到,u-boot使用的是tag的方式传参。共计分为以下几类tag:

  • setup_serial_tag设定与板子序列号(64位)相关的参数。
  • setup_commandline_tag设置命令行启动参数,参数来自环境变量"bootargs"。
  • setup_revision_tag 设置修订版本。
  • setup_memory_tags设置内存区块相关参数。
  • setup_initrd_tag设置ramdisk相关的参数。

注:以上参数并非都是必要的。

boot_jump_linux

static void boot_jump_linux(bootm_headers_t *images, int flag)
{
#ifdef CONFIG_ARM64void (*kernel_entry)(void *fdt_addr, void *res0, void *res1,void *res2);int fake = (flag & BOOTM_STATE_OS_FAKE_GO);kernel_entry = (void (*)(void *fdt_addr, void *res0, void *res1,void *res2))images->ep;debug("## Transferring control to Linux (at address %lx)...\n",(ulong) kernel_entry);bootstage_mark(BOOTSTAGE_ID_RUN_OS);announce_and_cleanup(fake);if (!fake) {
#ifdef CONFIG_ARMV8_PSCIarmv8_setup_psci();
#endifdo_nonsec_virt_switch();update_os_arch_secondary_cores(images->os.arch);#ifdef CONFIG_ARMV8_SWITCH_TO_EL1armv8_switch_to_el2((u64)images->ft_addr, 0, 0, 0,(u64)switch_to_el1, ES_TO_AARCH64);
#elseif ((IH_ARCH_DEFAULT == IH_ARCH_ARM64) &&(images->os.arch == IH_ARCH_ARM))armv8_switch_to_el2(0, (u64)gd->bd->bi_arch_number,(u64)images->ft_addr, 0,(u64)images->ep,ES_TO_AARCH32);elsearmv8_switch_to_el2((u64)images->ft_addr, 0, 0, 0,images->ep,ES_TO_AARCH64);
#endif}
#elseunsigned long machid = gd->bd->bi_arch_number;char *s;void (*kernel_entry)(int zero, int arch, uint params);unsigned long r2;int fake = (flag & BOOTM_STATE_OS_FAKE_GO);kernel_entry = (void (*)(int, int, uint))images->ep;
#ifdef CONFIG_CPU_V7Mulong addr = (ulong)kernel_entry | 1;kernel_entry = (void *)addr;
#endifs = env_get("machid");if (s) {if (strict_strtoul(s, 16, &machid) < 0) {debug("strict_strtoul failed!\n");return;}printf("Using machid 0x%lx from environment\n", machid);}debug("## Transferring control to Linux (at address %08lx)" \"...\n", (ulong) kernel_entry);bootstage_mark(BOOTSTAGE_ID_RUN_OS);announce_and_cleanup(fake);if (IMAGE_ENABLE_OF_LIBFDT && images->ft_len)r2 = (unsigned long)images->ft_addr;elser2 = gd->bd->bi_boot_params;if (!fake) {
#ifdef CONFIG_ARMV7_NONSECif (armv7_boot_nonsec()) {armv7_init_nonsec();secure_ram_addr(_do_nonsec_entry)(kernel_entry,0, machid, r2);} else
#endifkernel_entry(0, machid, r2);}
#endif
}

在这里插入图片描述

  • r2 = gd->bd->bi_boot_params; 是将TAG列表所在的内存地址赋值给通用寄存器R2。

  • kernel_entry(0, machid, r2); 跳转执行kernel的代码(自此uboot的使命完成,生命周期结束)。

综上,Uboot传递给linux kernel的参数是通过R2传递的。当linux kernel启动后,会从R2中拿到TAG列表的地址,然后将TAG参数解析出来使用。

相关文章:

uboot启动linux kernel的流程

目录 前言流程图autoboot_commandrun_command_listdo_bootmdo_bootm_statesdo_bootm_linuxboot_prep_linuxboot_jump_linux 前言 本文在u-boot启动流程分析这篇文章的基础上&#xff0c;简要梳理uboot启动linux kernel的流程。 流程图 其中&#xff0c; autoboot_command位于…...

垃圾回收系统小程序定制开发搭建攻略

在这个数字化快速发展的时代&#xff0c;垃圾回收系统的推广对于环境保护和可持续发展具有重要意义。为了更好地服务于垃圾回收行业&#xff0c;本文将分享如何使用第三方制作平台乔拓云网&#xff0c;定制开发搭建垃圾回收系统小程序。 首先&#xff0c;使用乔拓云网账号登录平…...

可变参数模板

1. sizeof...计算参数个数 template<typename... Ts> void magic(Ts... args) {std::cout << sizeof...(args) << std::endl; } 2.递归模板函数 template<typename T> void printf1(T value) {std::cout << value << std::endl; }templ…...

坐公交:内外向乘客依序选座(python字典、字符串、元组)

n排宽度不一的座位&#xff0c;每排2座&#xff0c;2n名内外向乘客依序上车按各自喜好选座。 (笔记模板由python脚本于2023年11月05日 21:49:31创建&#xff0c;本篇笔记适合熟悉python列表list、字符串str、元组tuple的coder翻阅) 【学习的细节是欢悦的历程】 Python 官网&…...

十年老程序员分享13个最常用的Python深度学习库和介绍,赶紧收藏码住!

文章目录 前言CaffeTheanoTensorFlowLasagneKerasmxnetsklearn-theanonolearnDIGITSBlocksdeepypylearn2Deeplearning4j关于Python技术储备一、Python所有方向的学习路线二、Python基础学习视频三、精品Python学习书籍四、Python工具包项目源码合集①Python工具包②Python实战案…...

【pytorch源码分析--torch执行流程与编译原理】

背景 解读torch源码方便算子开发方便后续做torch 模型性能开发 基本介绍 代码库 https://github.com/pytorch/pytorch 模块介绍 aten: A Tensor Library的缩写。与Tensor相关的内容都放在这个目录下。如Tensor的定义、存储、Tensor间的操作&#xff08;即算子/OP&#xff…...

编辑器报警处理

1、warning CS8600: 将 null 文本或可能的 null 值转换为不可为 null 类型。 原代码 string returnedString Marshal.PtrToStringAuto(pReturnedString, (int)bytesReturned); 处理后的代码 string returnedString Marshal.PtrToStringAuto(pReturnedString, (int)bytesR…...

Python库学习(十二):数据分析Pandas[下篇]

接着上篇《Python库学习(十一):数据分析Pandas[上篇]》,继续学习Pandas 1.数据过滤 在数据处理中&#xff0c;我们经常会对数据进行过滤&#xff0c;为此Pandas中提供mask()和where()两个函数&#xff1b; mask(): 在 满足条件的情况下替换数据&#xff0c;而不满足条件的部分…...

工具: MarkDown学习

具体内容看官方教程&#xff1a; Markdown官方教程...

JS逆向爬虫---请求参数加密②【某麦数据analysis参数加密】

主页链接: https://www.qimai.cn/rank analysis逆向 完整参数生成代码如下&#xff1a; const {JSDOM} require(jsdom) const dom new JSDOM(<!DOCTYPE html><p>hello</p>) window dom.windowfunction customDecrypt(n, t) {t t || generateKey(); //…...

基于APM(PIX)飞控和missionplanner制作遥控无人车-从零搭建自主pix无人车无人坦克

前面的步骤和无人机调试一样&#xff0c;可以参考无人机相关专栏。这里不再赘述。 1.安装完rover的固件后&#xff0c;链接gps并进行校准。旋转小车不同方向&#xff0c;完成校准&#xff0c;弹出成功窗口。 2.校准遥控器。 一定要确保遥控器模式准确&#xff0c;尤其是使用没…...

Vue3的手脚架使用和组件父子间通信-插槽(Options API)学习笔记

Vue CLI安装和使用 全局安装最新vue3 npm install vue/cli -g升级Vue CLI&#xff1a; 如果是比较旧的版本&#xff0c;可以通过下面命令来升级 npm update vue/cli -g通过脚手架创建项目 vue create 01_product_demoVue3父子组件的通信 父传子 父组件 <template>…...

第九章软件管理

云计算第九章软件管理 概述 1RPM包 RPM Package Manager 由Red Hat公司提出被众多Linux发现版所采用 也称二进制无需编译可以直接使用 无法设定个人设置开关功能 软件包示例 认识ROM包 2源码包 source code 需要经过GCC,C编辑环境编译才能运行 可以设定个人设置&…...

Web渗透编程语言基础

Web渗透初学者JavaScript专栏汇总-CSDN博客 Web渗透Java初学者文章汇总-CSDN博客 一 Web渗透PHP语言基础 PHP 教程 | 菜鸟教程 (runoob.com) 一 PHP 语言的介绍 PHP是一种开源的服务器端脚本语言,它被广泛用于Web开发领域。PHP可以与HTML结合使用,创建动态网页。 PHP的特…...

Vue-router 路由的基本使用

Vue-router是一个Vue的插件库&#xff0c;专门用于实现SPA应用&#xff0c;也就是整个应用是一个完整的页面&#xff0c;点击页面上的导航不会跳转和刷新页面。 一、安装Vue-router npm i vue-router // Vue3安装4版本 npm i vue-router3 // Vue2安装3版本 二、引入…...

如何在CPU上进行高效大语言模型推理

大语言模型&#xff08;LLMs&#xff09;已经在广泛的任务中展示出了令人瞩目的表现和巨大的发展潜力。然而&#xff0c;由于这些模型的参数量异常庞大&#xff0c;使得它们的部署变得相当具有挑战性&#xff0c;这不仅需要有足够大的内存空间&#xff0c;还需要有高速的内存传…...

简简单单入门Makefile

笔记来源&#xff1a;于仕琪教授&#xff1a;Makefile 20分钟入门&#xff0c;简简单单&#xff0c;展示如何使用Makefile管理和编译C代码 操作环境 MacosVscode 前提准备 新建文件夹 mkdir learn_makefile新建三个cpp文件和一个头文件 // mian.cpp #include <iostrea…...

New Maven Project

下面两个目录丢失了&#xff1a; src/main/java(missing) src/test/java(missing) 换个JRE就可以跑出来了 变更目录...

IDEA中如何移除未使用的import

&#x1f468;&#x1f3fb;‍&#x1f4bb; 热爱摄影的程序员 &#x1f468;&#x1f3fb;‍&#x1f3a8; 喜欢编码的设计师 &#x1f9d5;&#x1f3fb; 擅长设计的剪辑师 &#x1f9d1;&#x1f3fb;‍&#x1f3eb; 一位高冷无情的编码爱好者 大家好&#xff0c;我是全栈工…...

第18章_MySQL8新特性之CTE(公用表表达式)

文章目录 新特性&#xff1a;公用表表达式(cte)普通公用表表达式递归公用表表达式小 结 新特性&#xff1a;公用表表达式(cte) 公用表表达式&#xff08;或通用表表达式&#xff09;简称为CTE&#xff08;Common Table Expressions&#xff09;。CTE是一个命名的临时结果集&am…...

论文解读:交大港大上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化学习框架(二)

HoST框架核心实现方法详解 - 论文深度解读(第二部分) 《Learning Humanoid Standing-up Control across Diverse Postures》 系列文章: 论文深度解读 + 算法与代码分析(二) 作者机构: 上海AI Lab, 上海交通大学, 香港大学, 浙江大学, 香港中文大学 论文主题: 人形机器人…...

JavaScript 中的 ES|QL:利用 Apache Arrow 工具

作者&#xff1a;来自 Elastic Jeffrey Rengifo 学习如何将 ES|QL 与 JavaScript 的 Apache Arrow 客户端工具一起使用。 想获得 Elastic 认证吗&#xff1f;了解下一期 Elasticsearch Engineer 培训的时间吧&#xff01; Elasticsearch 拥有众多新功能&#xff0c;助你为自己…...

CMake基础:构建流程详解

目录 1.CMake构建过程的基本流程 2.CMake构建的具体步骤 2.1.创建构建目录 2.2.使用 CMake 生成构建文件 2.3.编译和构建 2.4.清理构建文件 2.5.重新配置和构建 3.跨平台构建示例 4.工具链与交叉编译 5.CMake构建后的项目结构解析 5.1.CMake构建后的目录结构 5.2.构…...

1688商品列表API与其他数据源的对接思路

将1688商品列表API与其他数据源对接时&#xff0c;需结合业务场景设计数据流转链路&#xff0c;重点关注数据格式兼容性、接口调用频率控制及数据一致性维护。以下是具体对接思路及关键技术点&#xff1a; 一、核心对接场景与目标 商品数据同步 场景&#xff1a;将1688商品信息…...

c++ 面试题(1)-----深度优先搜索(DFS)实现

操作系统&#xff1a;ubuntu22.04 IDE:Visual Studio Code 编程语言&#xff1a;C11 题目描述 地上有一个 m 行 n 列的方格&#xff0c;从坐标 [0,0] 起始。一个机器人可以从某一格移动到上下左右四个格子&#xff0c;但不能进入行坐标和列坐标的数位之和大于 k 的格子。 例…...

WordPress插件:AI多语言写作与智能配图、免费AI模型、SEO文章生成

厌倦手动写WordPress文章&#xff1f;AI自动生成&#xff0c;效率提升10倍&#xff01; 支持多语言、自动配图、定时发布&#xff0c;让内容创作更轻松&#xff01; AI内容生成 → 不想每天写文章&#xff1f;AI一键生成高质量内容&#xff01;多语言支持 → 跨境电商必备&am…...

20个超级好用的 CSS 动画库

分享 20 个最佳 CSS 动画库。 它们中的大多数将生成纯 CSS 代码&#xff0c;而不需要任何外部库。 1.Animate.css 一个开箱即用型的跨浏览器动画库&#xff0c;可供你在项目中使用。 2.Magic Animations CSS3 一组简单的动画&#xff0c;可以包含在你的网页或应用项目中。 3.An…...

Java求职者面试指南:计算机基础与源码原理深度解析

Java求职者面试指南&#xff1a;计算机基础与源码原理深度解析 第一轮提问&#xff1a;基础概念问题 1. 请解释什么是进程和线程的区别&#xff1f; 面试官&#xff1a;进程是程序的一次执行过程&#xff0c;是系统进行资源分配和调度的基本单位&#xff1b;而线程是进程中的…...

DingDing机器人群消息推送

文章目录 1 新建机器人2 API文档说明3 代码编写 1 新建机器人 点击群设置 下滑到群管理的机器人&#xff0c;点击进入 添加机器人 选择自定义Webhook服务 点击添加 设置安全设置&#xff0c;详见说明文档 成功后&#xff0c;记录Webhook 2 API文档说明 点击设置说明 查看自…...

安卓基础(Java 和 Gradle 版本)

1. 设置项目的 JDK 版本 方法1&#xff1a;通过 Project Structure File → Project Structure... (或按 CtrlAltShiftS) 左侧选择 SDK Location 在 Gradle Settings 部分&#xff0c;设置 Gradle JDK 方法2&#xff1a;通过 Settings File → Settings... (或 CtrlAltS)…...