单片机实现动态内存管理
1.简介
多数传统的单片机并没有动态内存管理功能。单片机通常具有有限的存储资源,包括固定大小的静态RAM(SRAM)用于数据存储和寄存器用于特定功能。这些资源在编译时被分配并且在程序的整个生命周期中保持不变。
2.动态内存管理好处
-
灵活性和效率:动态内存管理可以根据程序的需要,在运行时动态分配和释放内存空间。这种灵活性使得程序能够更高效地利用可用的内存资源,避免了静态分配固定大小内存的限制。
-
节省内存空间:动态内存管理允许程序只在需要时分配内存,释放不再使用的内存。这样可以避免静态内存分配导致的内存浪费,提高内存利用率。
-
支持动态数据结构:许多数据结构,如链表、树等,大小在运行时无法预先确定,需要动态分配内存以存储变量数量可变的元素。动态内存管理使得这些动态数据结构的实现变得简单和高效。
-
扩展性:使用动态内存管理,可以根据程序的需求动态地调整内存大小。这使得程序能够适应不同的输入规模和需求变化,提供更好的扩展性和灵活性。
3.如何自行实现动态内存管理
3.1 步骤
-
分配内存空间:首先,需要实现一个分配内存空间的函数。该函数需要检查内存池中是否有足够的空闲内存。如果有空闲内存,则将其标记为已使用,并返回指向所分配内存块的指针。如果没有足够的空闲内存,则需要采取相应的策略,例如返回空指针或扩展内存池。
-
释放内存空间:当不再需要分配的内存块时,需要实现一个释放内存空间的函数。该函数需要接收指向待释放内存块的指针,并将其标记为空闲状态,以便后续的内存分配可以再次利用它。
-
管理内存池:需要设计和管理一个内存池,也称为内存堆。内存池是一个预先分配的内存区域,用于存储动态分配和释放的内存块。需要跟踪每个已分配内存块的状态(已使用或空闲),以及其大小和地址信息。
-
错误处理和边界检查:在实现动态内存管理时,必须考虑各种错误情况和边界条件。例如,分配失败、重复释放内存、越界访问等。需要在代码中加入相应的检查和错误处理机制,以确保内存管理的正确性和安全性。
3.2方案
1.增加外部SRAM或者DRAM,使用外部空间来作为动态内存管理,这种方法需要,编写外部SRAM的驱动,并将外部的SRAM地址映射到芯片地址中。
例如以下驱动控制器:
STM32系列微控制器的FSMC(Flexible Static Memory Controller)是一种专门设计用于连接外部存储器设备(如SRAM、NOR Flash等)的控制器。
NXP LPC系列微控制器的EMC(External Memory Controller)来支持与外部存储器设备的数据交互可以连接多种类型的存储器,如SRAM、NOR Flash、SDRAM等。
TM4C系列微控制器的嵌入式外部存储器接口(EMIFA),通过该接口可以连接外部存储器设备,并支持SRAM、NOR Flash、NAND Flash等多种存储器类型。
2.使用内部SRAM的静态变量存储区来当作动态内存管理,这部分空间不允许再当静态空间来操作,这种方法无需依赖外部控制器,但是同时也无法管理更大的动态空间。
4.实现
动态内存管理的本质就是对一段地址的管理。它涉及到在运行时动态分配和释放内存空间,并管理各个内存块的状态可用性。
4.1直接使用静态变量
#define MEM_MAX_SIZE 100*1024 //100K
static unsigned char mem1base[MEM_MAX_SIZE]; //内部SRAM内存池
4.2使用外部SRAM地址需要映射
static unsigned char mem2base[MEM_MAX_SIZE] __attribute__((at(0XC0000000))); //外部SDRAM内存池
这里的地址0XC0000000只是示例 需要按照上文提到驱动控制器进行修改。
4.3 数据结构设计
typedef struct _MemBlockList
{union {struct {unsigned int valid : 1; // 0 表示未使用 1表示使用了unsigned int length : 31; // 长度};int info; // 整体的信息(部分)};struct _MemBlockList *next; // 下一块
} MemBlockList;//共占用8个字节typedef struct _MemoryManager
{void *start;void *end;unsigned int size;MemBlockList *head;
} MemoryManager;static MemoryManager g_memoryManager;
MemBlockList
用于管理一块连续的内存区域,并维护了一个内存块链表,用于跟踪空闲和已分配的内存块,MemoryManager
用于表示内存管理器。通过使用这两个结构体,可以实现对一块连续内存的管理,包括内存块的分配和释放。g_memoryManager
是一个全局的内存管理器变量,用于在程序中跟踪和管理内存块的分配和释放情况。
4.4 动态内存的初始化
bool memoryManagerInit(void)
{int size = MEM_MAX_SIZE;g_memoryManager.end = memXbase + size;g_memoryManager.start = memXbase;g_memoryManager.size = size;g_memoryManager.head = (MemBlockList *)g_memoryManager.start;g_memoryManager.head->length = g_memoryManager.size - sizeof(MemBlockList);g_memoryManager.head->valid = 0;g_memoryManager.head->next = NULL; printf("memoryManagerInit success : %d KB\n", size / 1024);return true;
}
4.6 动态内存的申请
void *memoryManager_malloc(int size)
{int free_size = getMaxFreeBlockSize();if (free_size >= size){MemBlockList *ptr = g_memoryManager.head;MemBlockList *free_block = NULL;/* 指针对齐,保证了 currentSize 是指针 sizeof(void*) 的整数倍大小 */int n = size / sizeof(void*);int currentSize = n * sizeof(void*);if (size % sizeof(void*) != 0) {currentSize += sizeof(void*);}bool isNeedCut = false; //是否需要分割MemBlockList *node = getFreeBlock(currentSize, &isNeedCut); if (node == NULL){printf("malloc size = %d faile !!!!!\n", size);print_mem_info();return (void *)(NULL);}/* 标记内存块使用了 */node->valid = 1;unsigned char *p = (unsigned char *)node;if (isNeedCut){p += sizeof(MemBlockList) + currentSize;free_block = (MemBlockList *)(p);free_block->length = node->length - currentSize - sizeof(MemBlockList);free_block->valid = 0;free_block->next = node->next;node->next = free_block;node->length = currentSize;}p = (unsigned char *)node;p += sizeof(MemBlockList);//偏移8个字节为真正使用的malloc地址checkMem();print_mem_info();return (void *)(p);}return NULL;
}
-
首先,通过调用
getMaxFreeBlockSize
函数获取当前可用的最大内存块的大小,以判断是否能够满足请求的内存大小。 -
如果最大可用内存块的大小大于等于所需的内存大小
size
,则尝试分配内存。 -
通过
getFreeBlock
函数查找一个合适大小的空闲内存块node
,并返回该内存块的指针。如果没有足够大的空闲内存块,则打印错误消息并返回NULL
。 -
将找到的内存块
node
标记为已使用(valid = 1
)。 -
计算实际分配的内存块大小
currentSize
,并判断是否需要对内存块进行分割。 -
如果需要进行分割,将原内存块的长度更新为
currentSize
,并在其后创建一个新的空闲内存块free_block
,其长度为原内存块长度减去currentSize
和sizeof(MemBlockList)
的大小。 -
返回分配的内存块的起始地址,即
node
指针偏移sizeof(MemBlockList)
字节后的地址。
4.7 动态内存的释放
void memoryManager_free(void *ptr)
{if (ptr != NULL){if ((ptr > g_memoryManager.start) && (ptr < g_memoryManager.end)){/* 计算地址对应的原始节点 */MemBlockList *source_node = (MemBlockList *)((unsigned char *)ptr - sizeof(MemBlockList));/* 找到node的前一个节点和下一个节点 */MemBlockList *previous_node = g_memoryManager.head;while (previous_node && previous_node->next != source_node){previous_node = previous_node->next;}MemBlockList *next_node = source_node->next;checkMem();source_node->valid = 0;MemBlockList *connect_node;if (previous_node && (previous_node->valid == 0))// 前一个节点是否空闲{connect_node = source_node->next;int size = source_node->length + sizeof(MemBlockList);if (next_node && (next_node->valid == 0))// 下一个节点是否空闲{connect_node = next_node->next;size += next_node->length + sizeof(MemBlockList);}previous_node->next = connect_node;previous_node->length += size;}else{connect_node = source_node->next;int size = source_node->length;if (next_node && (next_node->valid == 0)){ connect_node = next_node->next;size += next_node->length + sizeof(MemBlockList);}source_node->next = connect_node;source_node->length = size;}checkMem();}else{printf("memoryManager_free not allowd this address = %p(%p --- %p)\n", ptr, g_memoryManager.start, g_memoryManager.end);}}
}
-
首先判断参数
ptr
是否为NULL
,如果是NULL
,则直接返回。 -
接下来,检查
ptr
指向的内存地址是否在内存管理器所管理的内存范围内(g_memoryManager.start
到g_memoryManager.end
之间)。如果不在范围内,则打印错误消息并返回。 -
计算
ptr
对应的原始内存节点source_node
的地址,即ptr
指针向前偏移sizeof(MemBlockList)
字节。 -
遍历内存块链表,找到
source_node
的前一个节点previous_node
和下一个节点next_node
。 -
将
source_node
标记为未使用(valid = 0
)。 -
根据前一个节点
previous_node
和下一个节点next_node
的状态,进行内存块合并操作。-
如果前一个节点
previous_node
存在且为空闲(valid = 0
),则将source_node
与前一个节点连接,并将原来的内存长度包括前一个节点的长度进行合并。 -
否则,将
source_node
单独作为一个内存块,且不与前一个节点连接。
-
-
最后,调用
checkMem
函数检查内存状态。
5. 实现代码总览
#ifndef NULL
#define NULL ((void *)0)
#endiftypedef struct _MemBlockList
{union {struct {unsigned int valid : 1; // 0 表示未使用 1表示使用了unsigned int length : 31; // 长度};int info; // 整体的信息(部分)};struct _MemBlockList *next; // 下一块
} MemBlockList;//共占用8个字节typedef struct _MemoryManager
{void *start;void *end;unsigned int size;MemBlockList *head;
} MemoryManager;static MemoryManager g_memoryManager;/* 获取当前内存中空闲的最大块大小 */
static unsigned int getMaxFreeBlockSize(void)
{MemBlockList *node = g_memoryManager.head;unsigned int size = 0;while (node){if(node->valid == 0){if (node->length > size){size = node->length;}}node = node->next;}return size;
}/* 获取当前内存中 满足size条件下 最小的内存块 */
static MemBlockList *getFreeBlock(unsigned int size, bool *isNeedCutMem)
{MemBlockList *current_node = g_memoryManager.head;MemBlockList *ret_node = NULL;unsigned int min_size = 0xffffffff;*isNeedCutMem = false;while (current_node){if (current_node->valid == 0 && current_node->length >= size) //块是否空闲{bool current_cut = false;if (current_node->length >= (size + sizeof(MemBlockList) + (sizeof(void *))))current_cut = true;if (current_node->length < min_size){min_size = current_node->length;*isNeedCutMem = current_cut;ret_node = current_node;if (min_size == size)break;}}current_node = current_node->next;}return ret_node;
}/* 打印当前的内存使用情况 */
void print_mem_info(void)
{MemBlockList *node = g_memoryManager.head;while (node){printf("address = %p, valid = %d, next = %p, size = %d\n", node, node->valid, node->next, node->length);node = node->next;}
}/* 内存检查 */
static void checkMem(void)
{MemBlockList *node = g_memoryManager.head;int size = 0;int cnt = 0;while (node){cnt ++;size += node->length;node = node->next;}if (size < (g_memoryManager.size - cnt * sizeof(MemBlockList))){printf("checkMem err now only have %d block, size = %d!!!\n", cnt, size);print_mem_info();while (1);}return;
}/* 内存操作检查 */
static bool checkMemUse(void *ptr, int size)
{if((ptr < g_memoryManager.start) || (ptr > g_memoryManager.end)){//不在管控范围内的地址不进行校验return true;}MemBlockList *node = g_memoryManager.head;unsigned char *current_p = NULL;while (node){int current_size = node->length;current_p = (unsigned char *)node + sizeof(MemBlockList);if ((ptr >= current_p) && (ptr < (current_p + current_size))){if (node->valid == 0){printf("checkMemUse this address = %p is not active !!\n", ptr);return false;}else{if(size > (node->length - ((unsigned char *)ptr - current_p))){printf("checkMemUse this address = %p size = %d, is size over, source address = %p, len = %d!!\n",ptr, size, current_p, node->length);return false;}}}node = node->next;}return true;
} bool memoryManagerInit(void)
{int size = MEM_MAX_SIZE;g_memoryManager.end = memXbase + size;g_memoryManager.start = memXbase;g_memoryManager.size = size;g_memoryManager.head = (MemBlockList *)g_memoryManager.start;g_memoryManager.head->length = g_memoryManager.size - sizeof(MemBlockList);g_memoryManager.head->valid = 0;g_memoryManager.head->next = NULL; printf("memoryManagerInit success : %d KB\n", size / 1024);return true;
}void *memoryManager_malloc(int size)
{int free_size = getMaxFreeBlockSize();if (free_size >= size){MemBlockList *ptr = g_memoryManager.head;MemBlockList *free_block = NULL;/* 指针对齐,保证了 currentSize 是指针 sizeof(void*) 的整数倍大小 */int n = size / sizeof(void*);int currentSize = n * sizeof(void*);if (size % sizeof(void*) != 0) {currentSize += sizeof(void*);}bool isNeedCut = false; //是否需要分割MemBlockList *node = getFreeBlock(currentSize, &isNeedCut); if (node == NULL){printf("malloc size = %d faile !!!!!\n", size);print_mem_info();return (void *)(NULL);}/* 标记内存块使用了 */node->valid = 1;unsigned char *p = (unsigned char *)node;if (isNeedCut){p += sizeof(MemBlockList) + currentSize;free_block = (MemBlockList *)(p);free_block->length = node->length - currentSize - sizeof(MemBlockList);free_block->valid = 0;free_block->next = node->next;node->next = free_block;node->length = currentSize;}p = (unsigned char *)node;p += sizeof(MemBlockList);//偏移8个字节为真正使用的malloc地址checkMem();print_mem_info();return (void *)(p);}return NULL;
}void memoryManager_free(void *ptr)
{if (ptr != NULL){if ((ptr > g_memoryManager.start) && (ptr < g_memoryManager.end)){/* 计算地址对应的原始节点 */MemBlockList *source_node = (MemBlockList *)((unsigned char *)ptr - sizeof(MemBlockList));/* 找到node的前一个节点和下一个节点 */MemBlockList *previous_node = g_memoryManager.head;while (previous_node && previous_node->next != source_node){previous_node = previous_node->next;}MemBlockList *next_node = source_node->next;checkMem();source_node->valid = 0;MemBlockList *connect_node;if (previous_node && (previous_node->valid == 0))// 前一个节点是否空闲{connect_node = source_node->next;int size = source_node->length + sizeof(MemBlockList);if (next_node && (next_node->valid == 0))// 下一个节点是否空闲{connect_node = next_node->next;size += next_node->length + sizeof(MemBlockList);}previous_node->next = connect_node;previous_node->length += size;}else{connect_node = source_node->next;int size = source_node->length;if (next_node && (next_node->valid == 0)){ connect_node = next_node->next;size += next_node->length + sizeof(MemBlockList);}source_node->next = connect_node;source_node->length = size;}checkMem();}else{printf("memoryManager_free not allowd this address = %p(%p --- %p)\n", ptr, g_memoryManager.start, g_memoryManager.end);}}
}void memoryManager_cpy(void *dest, const void *src, unsigned int n)
{if(checkMemUse(src, n) == false){printf("memoryManager_cpy error check src\n");}if(checkMemUse(dest, n) == false){printf("memoryManager_cpy error check dest\n");}for (unsigned int i = 0; i < n; i++) {dest[i] = src[i];}
}void memoryManager_set(void *ptr, unsigned char value, unsigned int num)
{if(checkMemUse(ptr, num) == false){printf("memoryManager_set error check ptr\n");}for (unsigned int i = 0; i < num; i++) {ptr[i] = value;}
}
相关文章:

单片机实现动态内存管理
1.简介 多数传统的单片机并没有动态内存管理功能。单片机通常具有有限的存储资源,包括固定大小的静态RAM(SRAM)用于数据存储和寄存器用于特定功能。这些资源在编译时被分配并且在程序的整个生命周期中保持不变。 2.动态内存管理好处 灵活性和…...

(JS逆向专栏十一)某融平台网站登入RSA
声明: 本文章中所有内容仅供学习交流,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关,若有侵权,请联系我立即删除! 名称:点融 目标:登入参数 加密类型:RSA 目标网址:https://www.dianrong.com/accoun…...

c++ boost circular_buffer
boost库中的 circular_buffer顾名思义是一个循环缓冲器,其 capcity是固定的当容量满了以后,插入一个元素时,会在容器的开头或结尾处删除一个元素。 circular_buffer为了效率考虑,使用了连续内存块保存元素 使用固定内存&#x…...

网络编程——端口
端口 一、端口概述 TCP/IP 协议采用端口标识通信的进程 用于区分一个系统里的多个进程 二、端口特点 1、对于同一个端口,在本同系统中对应着不同的进程 2、对于同一个系统,一个端口只能被一个进程拥有 3、一个进程拥有一个端口后,传输层送…...

【网络】自定义协议 | 序列化和反序列化 | Jsoncpp
本文首发于 慕雪的寒舍 以tcpServer的计算器服务为例,实现用jsoncpp来进行序列化和反序列化 阅读本文之前,请先阅读 自定义协议 | 序列化和反序列化 | 以tcpServer为例 1.安装jsoncpp 我所用的系统是centos7.6,先用下面的命令查找相关的包 …...

PHP实践:用openssl打造安全可靠的API签名验证系统
🏆作者简介,黑夜开发者,全栈领域新星创作者✌,阿里云社区专家博主,2023年6月csdn上海赛道top4。 🏆数年电商行业从业经验,历任核心研发工程师,项目技术负责人。 🏆本文已…...

每天一道leetcode:剑指 Offer 50. 第一个只出现一次的字符(适合初学者)
今日份题目: 在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。 示例1 输入:s "abaccdeff" 输出:b 示例2 输入:s "" 输出: 提示 0 …...

【第五章 flutter学习之flutter进阶组件-下篇】
文章目录 一、Scaffold属性二、TabBar三、路由四、AlertDialog、SimpleDialog、showM...五、PageView六、Key七、AnimatedList八、动画 一、Scaffold属性 Flutter Scaffold 是一个用于构建基本用户界面的布局组件。它提供了许多属性,使得开发者能够轻松地创建一个完…...

单元测试和集成测试有什么区别
单元测试和集成测试有什么区别 单元测试和集成测试是软件开发中的两个重要测试阶段,它们的主要区别如下: 目的: 单元测试:主要针对代码的最小可测试单元,通常是一个函数或方法,确保它按照预期工作。集成…...

如何实现基于场景的接口自动化测试用例?
自动化本身是为了提高工作效率,不论选择何种框架,何种开发语言,我们最终想实现的效果,就是让大家用最少的代码,最小的投入,完成自动化测试的工作。 基于这个想法,我们的接口自动化测试思路如下…...

SAP 开发编辑界面-关闭助手
打开关闭助手时的开发界面如下: 关闭关闭助手后的界面如下: 菜单栏: 编辑--》修改操作--》关闭助手...

【el-image图片查看时 样式穿透表格问题】
element-ui el-image图片查看 样式混乱 解决方式 ::v-deep(.el-table__cell) {position: static !important; // 解决el-image 和 el-table冲突层级冲突问题 }加个样式即可...

GPT带我学-设计模式-模板模式
1 请你给我介绍一下设计模式中的模板模式 模板模式是一种行为设计模式,它定义了一个算法的骨架,将一些步骤的具体实现延迟到子类中。模板模式允许子类重新定义算法的某些特定步骤,而不需要改变算法的结构。 模板模式由以下几个角色组成&…...

Windows下调试UEFI程序:Visual Studio调试
以edk2\MdeModulePkg\Application\HelloWorld这个项目作为调试目标。 1. 使用VS2017建立Makefile工程 VS2017, 新建 project,取名X64dbg_vs。 Visual C > Other > Makefile Project, 注意项目路径为HelloWord程序路径。 随便填写config中的字符串ÿ…...

Vue中监听路由参数变化的几种方式
目录 一. 路由监听方式: 通过 watch 进行监听 1. 监听路由从哪儿来到哪儿去 2. 监听路由变化获取新老路由信息 3. 监听路由变化触发方法 4. 监听路由的 path 变化 5. 监听路由的 path 变化, 使用handler函数 6. 监听路由的 path 变化,触发method…...

angular——子组件如何接收父组件的动态传值
开发过程中,父组件给子组件传值的情况很常见,今天我们就来聊聊父组件给子组件传值可能会发生哪些意外,什么情况下子组件无法接收到父组件最新的传值; 传值情况: 基本数据类型:父组件给子组件传递 基本数据…...

php 桥接模式
一,桥接模式,是结构设计模式的一种,其将抽象部分和实现部分分离开来,使两部分可以独立的进行修改,提高系统的灵活性。在桥接模式中,需要定义一个抽象类和一个实现类,通过将实现类注入到抽象类中…...

Android 13 Hotseat定制化修改——004 hotseat布局位置
目录 一.背景 二.原生hotseat布局位置 三.修改Hotseat布局位置 一.背景 由于需求是需要自定义修改Hotseat,所以此篇文章是记录如何自定义修改hotseat的,应该可以覆盖大部分场景,修改点有修改hotseat布局方向,hotseat图标数量,hotseat图标大小,hotseat布局位置,hotseat…...

海外版金融理财系统源码 国际投资理财系统源码 项目投资理财源码
海外版金融理财系统源码 国际投资理财系统源码 项目投资理财源码...

洛谷P1162 - 填涂颜色
题目描述 由数字 0 0 0 组成的方阵中,有一任意形状闭合圈,闭合圈由数字 1 1 1 构成,围圈时只走上下左右 4 4 4 个方向。现要求把闭合圈内的所有空间都填写成 2 2 2。例如: 6 6 6\times 6 66 的方阵( n 6 n6 n6&…...

设计模式十一:外观模式(Facade Pattern)
外观模式(Facade Pattern)是一种结构型设计模式,它提供了一个统一的接口,用于访问系统中的一组复杂子系统。外观模式通过将复杂子系统的接口封装在一个高层接口中,简化了客户端与子系统之间的交互,使得客户…...
GIS和倾斜摄影的关系?
GIS(地理信息系统)和倾斜摄影是两种在地理空间数据处理和分析中扮演重要角色的技术。但是我们总是会分不清二者,本文就带大家从不同角度了解二者之间的关系。 概念 GIS是一种用来捕获、存储、分析和展示地理空间数据的技术,它可以…...

【CI/CD】图解六种分支管理模型
图解六种分支管理模型 任何一家公司乃至于一个小组织,只要有写代码的地方,就有代码版本管理的主场,初入职场,总会遇到第一个拦路虎 git 管理流程,但是每一个企业似乎都有自己的 git 管理流程,倘若我们能掌握…...

LeetCode105. 从前序与中序遍历序列构造二叉树
105. 从前序与中序遍历序列构造二叉树 文章目录 [105. 从前序与中序遍历序列构造二叉树](https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-inorder-traversal/)一、题目二、题解 一、题目 给定两个整数数组 preorder 和 inorder ,其中 preo…...

编码技巧——Sentinel的blockHandler与fallback
本文介绍Sentinel的blockHandler与fallback的区别,背景是:发生限流时,配置的sentinel的blockhandler没有生效而fallback生效了;排查原因,从而给出Sentinel配置异常降级和限流降级的代码写法; 在查看源码前…...

最新成果展示:GaN基Micro-LED热学模型数据库的开发及应用
由于GaN基Micro-LED表面积-体积比增加,其在热学方面的性质有别于大尺寸的LED,如缺陷复合导致的热效应将在发光区域中产生诸多“热”点,导致发光波长不均匀,这将影响后期显示系统的成像稳定性。针对上述问题,天津赛米卡…...

【Vue3】动态组件
动态组件的基本使用 动态组件(Dynamic Components)是一种在 Vue 中根据条件或用户输入来动态渲染不同组件的技术。 在 Vue 中使用动态组件,可以使用 元素,并通过 is 特性绑定一个组件的名称或组件对象。通过在父组件中改变 is 特…...

Java超级玛丽小游戏制作过程讲解 第五天 创建并完成常量类04
//加载障碍物 try {obstacle.add(ImageIO.read(new File(path"brick.png")));obstacle.add(ImageIO.read(new File(path"soil_up.png")));obstacle.add(ImageIO.read(new File(path"soil_base.png"))); } catch (IOException e) {e.printStackTr…...

设置浏览器兼容
浏览器兼容 css兼容 cursor定义手型 Firefox不支持hand,IE支持pointer 解决方法:统一使用pointercss透明 IE:filter:progid:DXImageTransform.Microsoft.Alpha(style0,opacity60) Firefox:opacity:0.6 解决…...

Java # List
ArrayList<>() import java.util.ArrayList; // 引入 ArrayList 类ArrayList<E> objectName new ArrayList<>(); // 初始化 常用方法 方法描述add()将元素插入到指定位置的 arraylist 中addAll()添加集合中的所有元素到 arraylist 中clear()删除 arrayl…...