图文结合纯c手写内存池

  • 前言
  • 为什么要用内存池
  • 内存池的使用场景
  • 设计一个内存池
    • 总体介绍
    • 小块内存的分配与管理
    • 大块内存的分配与管理
  • 内存池代码实现
    • 向外提供的api
    • 相关结构体的定义
    • 内存对齐
    • 创建与销毁内存池
    • 提供给用户的内存申请api
    • 小块内存block扩容
    • 分配大块内存
    • 释放内存
  • 内存池测试
  • nginx内存池对比分析
    • 相关结构体定义对比
    • 创建内存池对比
    • 内存申请对比

前言

  本文从零到一,手把手实现一个内存池。

  比较出名的内存池有jemalloc和tcmalloc,这两个都是全局内存池,比较推荐使用tcmalloc。

  本专栏知识点是通过零声教育的线上课学习,进行梳理总结写下文章,对c/c++linux课程感兴趣的读者,可以点击链接 C/C++后台高级服务器课程介绍 详细查看课程的服务。

为什么要用内存池

  为什么要用内存池?首先,在7 * 24h的服务器中如果不使用内存池,而使用malloc和free,那么就非常容易产生内存碎片,早晚都会申请内存失败;并且在比较复杂的代码或者继承的屎山中,非常容易出现内存泄漏导致oom的问题。

  为了解决这两个问题,内存池就应运而生了。内存池预先分配一大块内存来做一个内存池,业务中的内存分配和释放都由这个内存池来管理,内存池内的内存不足时其内部会自己申请。所以内存碎片的问题就交由内存池的算法来优化,而内存泄漏的问题只需要遵守内存池提供的api,就非常容易避免内存泄漏了。

  即使出现了内存泄漏,排查的思路也很清晰。1.检查是不是内存池的问题;2.如果不是内存池的问题,就检查是不是第三方库的内存泄漏。

内存池的使用场景

  1. 全局内存池
  2. 一个连接一个内存池(本文实现这个场景的内存池)

设计一个内存池

总体介绍

  由于本文是一个连接一个内存池,所以后续介绍和代码都是以4k为分界线,大于4k的我们认为是大块内存;小于4k的我们认为是小块内存。并且注意这里的4k,并不是严格遵照4096,而是在描述上,用4k比较好描述。

  在真正使用内存之前,内存池提前分配一定数量且大小相等的内存块以作备用,当真正被用户调用api分配内存的时候,直接从内存块中获取内存(指小块内存),当内存块不够用了,再有内存池取申请新的内存块。而如果是需要大块内存,则内存池直接申请大块内存再返回给用户。

  内存池:就是将这些提前申请的内存块组织管理起来的数据结构,内存池实现原理主要分为分配,回收,扩容三部分。

  内存池原理之小块内存:分配=> 内存池预申请一块4k的内存块,这里称为block,即block=4k内存块。当用户向内存池申请内存size小于4k时,内存池从block的空间中划分出去size空间,当再有新申请时,再划分出去。扩容=> 直到block中的剩余空间不足以分配size大小,那么此时内存池会再次申请一块block,再从新的block中划分size空间给用户。回收=> 每一次申请小内存,都会在对应的block中引用计数加1,每一次释放小内存时,都会在block中引用计数减1,只有当引用计数为零的时候,才会回收block使他重新成为空闲空间,以便重复利用空间。这样,内存池避免频繁向内核申请/释放内存,从而提高系统性能。

  内存池原理之大块内存:分配=> 因为大块内存是大于4k的,所以内存池不预先申请内存,也就是用户申请的时候,内存池再申请内存,然后返回给用户。扩容=> 大块内存不存在扩容。回收=> 对于大块内存来说,回收就直接free掉即可。

  上面理论讲完了,下面来介绍如何管理小块内存和大块内存。

小块内存的分配与管理

  在创建内存池的时候,会预先申请一块4k的内存,并且在起始处将pool的结构体和node的结构体放进去,从last开始一直到end都是空闲内存,<last , end >中间的区域就用来存储小块内存。每一次mp_malloc,就将last指针后移,直到 end−last<sizeend - last < sizeend−last<size 时,进行扩容,将新block的last后移即可。

  • 初始状态
  • 分配内存
  • 扩容

大块内存的分配与管理

  对于大块内存,前面已经说了,用户申请的时候,内存池才申请

  • 申请一块大内存

  • 再申请一块大内存

内存池代码实现

向外提供的api

  • mp_create_pool:创建一个线程池,其核心是创建struct mp_pool_s这个结构体,并申请4k内存,将各个指针指向上文初始状态的图一样。
  • mp_destroy_pool:销毁内存池,遍历小块结构体和大块结构体,进行free释放内存
  • mp_malloc:提供给用户申请内存的api
  • mp_calloc:通过mp_malloc申请内存后置零,相当于calloc
  • mp_free:释放由mp_malloc返回的内存
  • mp_reset_pool:将block的last置为初始状态,销毁所有大块内存
  • monitor_mp_poll:监控内存池状态
struct mp_pool_s *mp_create_pool(size_t size);void mp_destroy_pool(struct mp_pool_s *pool);void *mp_malloc(struct mp_pool_s *pool, size_t size);void *mp_calloc(struct mp_pool_s *pool, size_t size);void mp_free(struct mp_pool_s *pool, void *p);void mp_reset_pool(struct mp_pool_s *pool);void monitor_mp_poll(struct mp_pool_s *pool, char *tk);

相关结构体的定义

  mp_pool_s 就是整个内存池的管理结构,我们做的内存池是一个连接一个内存池,所以对于整个程序而言,内存池对象是有很多个的。
  可能读者会有疑问,有了head,为什么还有current,是因为如果一个block剩余空间小于size超过一定次数后,将current指向下一个block,这样就加快内存分配效率,减少遍历次数。

//每4k一block结点
struct mp_node_s {unsigned char *end;//块的结尾unsigned char *last;//使用到哪了struct mp_node_s *next;//链表int quote;//引用计数int failed;//失效次数
};struct mp_large_s {struct mp_large_s *next;//链表int size;//alloc的大小void *alloc;//大块内存的起始地址
};struct mp_pool_s {struct mp_large_s *large;struct mp_node_s *head;struct mp_node_s *current;
};

内存对齐

访问速度是内存对齐的原因之一,另外一个原因是某些平台(arm)不支持未内存对齐的访问

  在4k里面划分内存,那么必然有很多地方是不对齐的,所以这里提供两个内存对齐的函数。那么为什么要内存对齐呢?其一:提高访问速度;其二:某些平台arm不支持未对其的内存访问,会出错。

#define mp_align(n, alignment) (((n)+(alignment-1)) & ~(alignment-1))
#define mp_align_ptr(p, alignment) (void *)((((size_t)p)+(alignment-1)) & ~(alignment-1))

创建与销毁内存池

  创建一个线程池,其核心是创建struct mp_pool_s这个结构体,并申请4k内存,将各个指针指向上文初始状态的图一样。
  销毁内存池,遍历小块结构体和大块结构体,进行free释放内存。

//创建内存池
struct mp_pool_s *mp_create_pool(size_t size) {struct mp_pool_s *pool;if (size < PAGE_SIZE || size % PAGE_SIZE != 0) {size = PAGE_SIZE;}//分配4k以上不用malloc,用posix_memalign/*int posix_memalign (void **memptr, size_t alignment, size_t size);*/int ret = posix_memalign((void **) &pool, MP_ALIGNMENT, size); //4K + mp_pool_sif (ret) {return NULL;}pool->large = NULL;pool->current = pool->head = (unsigned char *) pool + sizeof(struct mp_pool_s);pool->head->last = (unsigned char *) pool + sizeof(struct mp_pool_s) + sizeof(struct mp_node_s);pool->head->end = (unsigned char *) pool + PAGE_SIZE;pool->head->failed = 0;return pool;
}//销毁内存池
void mp_destroy_pool(struct mp_pool_s *pool) {struct mp_large_s *large;for (large = pool->large; large; large = large->next) {if (large->alloc) {free(large->alloc);}}struct mp_node_s *cur, *next;cur = pool->head->next;while (cur) {next = cur->next;free(cur);cur = next;}free(pool);
}

提供给用户的内存申请api

  申请的内存以size做区分,如果大于4k就分配大块内存,小于4k就去block里面划分。

//分配内存
void *mp_malloc(struct mp_pool_s *pool, size_t size) {if (size <= 0) {return NULL;}if (size > PAGE_SIZE - sizeof(struct mp_node_s)) {//largereturn mp_malloc_large(pool, size);}else {//smallunsigned char *mem_addr = NULL;struct mp_node_s *cur = NULL;cur = pool->current;while (cur) {mem_addr = mp_align_ptr(cur->last, MP_ALIGNMENT);if (cur->end - mem_addr >= size) {cur->quote++;//引用+1cur->last = mem_addr + size;return mem_addr;}else {cur = cur->next;}}return mp_malloc_block(pool, size);// open new space}
}
void *mp_calloc(struct mp_pool_s *pool, size_t size) {void *mem_addr = mp_malloc(pool, size);if (mem_addr) {memset(mem_addr, 0, size);}return mem_addr;
}

小块内存block扩容

   所有的block都 end−last<sizeend - last < sizeend−last<size 时,进行扩容,将新block的last后移即可。

//new block 4k
void *mp_malloc_block(struct mp_pool_s *pool, size_t size) {unsigned char *block;int ret = posix_memalign((void **) &block, MP_ALIGNMENT, PAGE_SIZE); //4Kif (ret) {return NULL;}struct mp_node_s *new_node = (struct mp_node_s *) block;new_node->end = block + PAGE_SIZE;new_node->next = NULL;unsigned char *ret_addr = mp_align_ptr(block + sizeof(struct mp_node_s), MP_ALIGNMENT);new_node->last = ret_addr + size;new_node->quote++;struct mp_node_s *current = pool->current;struct mp_node_s *cur = NULL;for (cur = current; cur->next; cur = cur->next) {if (cur->failed++ > 4) {current = cur->next;}}//now cur = last nodecur->next = new_node;pool->current = current;return ret_addr;
}

分配大块内存

//size>4k
void *mp_malloc_large(struct mp_pool_s *pool, size_t size) {unsigned char *big_addr;int ret = posix_memalign((void **) &big_addr, MP_ALIGNMENT, size); //sizeif (ret) {return NULL;}struct mp_large_s *large;//released struct large resumeint n = 0;for (large = pool->large; large; large = large->next) {if (large->alloc == NULL) {large->size = size;large->alloc = big_addr;return big_addr;}if (n++ > 3) {break;// 为了避免过多的遍历,限制次数}}large = mp_malloc(pool, sizeof(struct mp_large_s));if (large == NULL) {free(big_addr);return NULL;}large->size = size;large->alloc = big_addr;large->next = pool->large;pool->large = large;return big_addr;
}

释放内存

  如果是大块内存,找到之后直接释放;如果是小块内存,将引用计数减1,如果引用计数为0则重置last。

//释放内存
void mp_free(struct mp_pool_s *pool, void *p) {struct mp_large_s *large;for (large = pool->large; large; large = large->next) {//大块if (p == large->alloc) {free(large->alloc);large->size = 0;large->alloc = NULL;return;}}//小块 引用-1struct mp_node_s *cur = NULL;for (cur = pool->head; cur; cur = cur->next) {//        printf("cur:%p   p:%p   end:%p\n", (unsigned char *) cur, (unsigned char *) p, (unsigned char *) cur->end);if ((unsigned char *) cur <= (unsigned char *) p && (unsigned char *) p <= (unsigned char *) cur->end) {cur->quote--;if (cur->quote == 0) {if (cur == pool->head) {pool->head->last = (unsigned char *) pool + sizeof(struct mp_pool_s) + sizeof(struct mp_node_s);}else {cur->last = (unsigned char *) cur + sizeof(struct mp_node_s);}cur->failed = 0;pool->current = pool->head;}return;}}
}

内存池测试

//
// Created by 68725 on 2022/7/26.
//
#include <stdlib.h>
#include <stdio.h>
#include <string.h>#define PAGE_SIZE 4096
#define MP_ALIGNMENT 16
#define mp_align(n, alignment) (((n)+(alignment-1)) & ~(alignment-1))
#define mp_align_ptr(p, alignment) (void *)((((size_t)p)+(alignment-1)) & ~(alignment-1))//每4k一block结点
struct mp_node_s {unsigned char *end;//块的结尾unsigned char *last;//使用到哪了struct mp_node_s *next;//链表int quote;//引用计数int failed;//失效次数
};struct mp_large_s {struct mp_large_s *next;//链表int size;//alloc的大小void *alloc;//大块内存的起始地址
};struct mp_pool_s {struct mp_large_s *large;struct mp_node_s *head;struct mp_node_s *current;
};struct mp_pool_s *mp_create_pool(size_t size);void mp_destroy_pool(struct mp_pool_s *pool);void *mp_malloc(struct mp_pool_s *pool, size_t size);void *mp_calloc(struct mp_pool_s *pool, size_t size);void mp_free(struct mp_pool_s *pool, void *p);void mp_reset_pool(struct mp_pool_s *pool);void monitor_mp_poll(struct mp_pool_s *pool, char *tk);void mp_reset_pool(struct mp_pool_s *pool) {struct mp_node_s *cur;struct mp_large_s *large;for (large = pool->large; large; large = large->next) {if (large->alloc) {free(large->alloc);}}pool->large = NULL;pool->current = pool->head;for (cur = pool->head; cur; cur = cur->next) {cur->last = (unsigned char *) cur + sizeof(struct mp_node_s);cur->failed = 0;cur->quote = 0;}
}//创建内存池
struct mp_pool_s *mp_create_pool(size_t size) {struct mp_pool_s *pool;if (size < PAGE_SIZE || size % PAGE_SIZE != 0) {size = PAGE_SIZE;}//分配4k以上不用malloc,用posix_memalign/*int posix_memalign (void **memptr, size_t alignment, size_t size);*/int ret = posix_memalign((void **) &pool, MP_ALIGNMENT, size); //4K + mp_pool_sif (ret) {return NULL;}pool->large = NULL;pool->current = pool->head = (unsigned char *) pool + sizeof(struct mp_pool_s);pool->head->last = (unsigned char *) pool + sizeof(struct mp_pool_s) + sizeof(struct mp_node_s);pool->head->end = (unsigned char *) pool + PAGE_SIZE;pool->head->failed = 0;return pool;
}//销毁内存池
void mp_destroy_pool(struct mp_pool_s *pool) {struct mp_large_s *large;for (large = pool->large; large; large = large->next) {if (large->alloc) {free(large->alloc);}}struct mp_node_s *cur, *next;cur = pool->head->next;while (cur) {next = cur->next;free(cur);cur = next;}free(pool);
}//size>4k
void *mp_malloc_large(struct mp_pool_s *pool, size_t size) {unsigned char *big_addr;int ret = posix_memalign((void **) &big_addr, MP_ALIGNMENT, size); //sizeif (ret) {return NULL;}struct mp_large_s *large;//released struct large resumeint n = 0;for (large = pool->large; large; large = large->next) {if (large->alloc == NULL) {large->size = size;large->alloc = big_addr;return big_addr;}if (n++ > 3) {break;// 为了避免过多的遍历,限制次数}}large = mp_malloc(pool, sizeof(struct mp_large_s));if (large == NULL) {free(big_addr);return NULL;}large->size = size;large->alloc = big_addr;large->next = pool->large;pool->large = large;return big_addr;
}//new block 4k
void *mp_malloc_block(struct mp_pool_s *pool, size_t size) {unsigned char *block;int ret = posix_memalign((void **) &block, MP_ALIGNMENT, PAGE_SIZE); //4Kif (ret) {return NULL;}struct mp_node_s *new_node = (struct mp_node_s *) block;new_node->end = block + PAGE_SIZE;new_node->next = NULL;unsigned char *ret_addr = mp_align_ptr(block + sizeof(struct mp_node_s), MP_ALIGNMENT);new_node->last = ret_addr + size;new_node->quote++;struct mp_node_s *current = pool->current;struct mp_node_s *cur = NULL;for (cur = current; cur->next; cur = cur->next) {if (cur->failed++ > 4) {current = cur->next;}}//now cur = last nodecur->next = new_node;pool->current = current;return ret_addr;
}//分配内存
void *mp_malloc(struct mp_pool_s *pool, size_t size) {if (size <= 0) {return NULL;}if (size > PAGE_SIZE - sizeof(struct mp_node_s)) {//largereturn mp_malloc_large(pool, size);}else {//smallunsigned char *mem_addr = NULL;struct mp_node_s *cur = NULL;cur = pool->current;while (cur) {mem_addr = mp_align_ptr(cur->last, MP_ALIGNMENT);if (cur->end - mem_addr >= size) {cur->quote++;//引用+1cur->last = mem_addr + size;return mem_addr;}else {cur = cur->next;}}return mp_malloc_block(pool, size);// open new space}
}void *mp_calloc(struct mp_pool_s *pool, size_t size) {void *mem_addr = mp_malloc(pool, size);if (mem_addr) {memset(mem_addr, 0, size);}return mem_addr;
}//释放内存
void mp_free(struct mp_pool_s *pool, void *p) {struct mp_large_s *large;for (large = pool->large; large; large = large->next) {//大块if (p == large->alloc) {free(large->alloc);large->size = 0;large->alloc = NULL;return;}}//小块 引用-1struct mp_node_s *cur = NULL;for (cur = pool->head; cur; cur = cur->next) {//        printf("cur:%p   p:%p   end:%p\n", (unsigned char *) cur, (unsigned char *) p, (unsigned char *) cur->end);if ((unsigned char *) cur <= (unsigned char *) p && (unsigned char *) p <= (unsigned char *) cur->end) {cur->quote--;if (cur->quote == 0) {if (cur == pool->head) {pool->head->last = (unsigned char *) pool + sizeof(struct mp_pool_s) + sizeof(struct mp_node_s);}else {cur->last = (unsigned char *) cur + sizeof(struct mp_node_s);}cur->failed = 0;pool->current = pool->head;}return;}}
}void monitor_mp_poll(struct mp_pool_s *pool, char *tk) {printf("\r\n\r\n------start monitor poll------%s\r\n\r\n", tk);struct mp_node_s *head = NULL;int i = 0;for (head = pool->head; head; head = head->next) {i++;if (pool->current == head) {printf("current==>第%d块\n", i);}if (i == 1) {printf("第%02d块small block  已使用:%4ld  剩余空间:%4ld  引用:%4d  failed:%4d\n", i,(unsigned char *) head->last - (unsigned char *) pool,head->end - head->last, head->quote, head->failed);}else {printf("第%02d块small block  已使用:%4ld  剩余空间:%4ld  引用:%4d  failed:%4d\n", i,(unsigned char *) head->last - (unsigned char *) head,head->end - head->last, head->quote, head->failed);}}struct mp_large_s *large;i = 0;for (large = pool->large; large; large = large->next) {i++;if (large->alloc != NULL) {printf("第%d块large block  size=%d\n", i, large->size);}}printf("\r\n\r\n------stop monitor poll------\r\n\r\n");
}int main() {struct mp_pool_s *p = mp_create_pool(PAGE_SIZE);monitor_mp_poll(p, "create memory pool");
#if 0printf("mp_align(5, %d): %d, mp_align(17, %d): %d\n", MP_ALIGNMENT, mp_align(5, MP_ALIGNMENT), MP_ALIGNMENT,mp_align(17, MP_ALIGNMENT));printf("mp_align_ptr(p->current, %d): %p, p->current: %p\n", MP_ALIGNMENT, mp_align_ptr(p->current, MP_ALIGNMENT),p->current);
#endifvoid *mp[30];int i;for (i = 0; i < 30; i++) {mp[i] = mp_malloc(p, 512);}monitor_mp_poll(p, "申请512字节30个");for (i = 0; i < 30; i++) {mp_free(p, mp[i]);}monitor_mp_poll(p, "销毁512字节30个");int j;for (i = 0; i < 50; i++) {char *pp = mp_calloc(p, 32);for (j = 0; j < 32; j++) {if (pp[j]) {printf("calloc wrong\n");exit(-1);}}}monitor_mp_poll(p, "申请32字节50个");for (i = 0; i < 50; i++) {char *pp = mp_malloc(p, 3);}monitor_mp_poll(p, "申请3字节50个");void *pp[10];for (i = 0; i < 10; i++) {pp[i] = mp_malloc(p, 5120);}monitor_mp_poll(p, "申请大内存5120字节10个");for (i = 0; i < 10; i++) {mp_free(p, pp[i]);}monitor_mp_poll(p, "销毁大内存5120字节10个");mp_reset_pool(p);monitor_mp_poll(p, "reset pool");for (i = 0; i < 100; i++) {void *s = mp_malloc(p, 256);}monitor_mp_poll(p, "申请256字节100个");mp_destroy_pool(p);return 0;
}

nginx内存池对比分析

相关结构体定义对比

创建内存池对比

内存申请对比

图文结合纯c手写内存池相关推荐

  1. 手写内存池以及原理代码分析【C语言】

    内存池是对堆进行管理 当进程执行时,操作系统会分出0~4G的虚拟内存空间给进程,程序员可以自行管理(分配.释放)的部分就是mmap映射区.heap堆区,而内存池管理的部分就是用户进程的堆区. 为什么要 ...

  2. 面试官让我现场手写内存溢出案例代码,我反手就是一个王炸!!

    大家好,我是冰河~~ 作为程序员,多多少少都会遇到一些内存溢出的场景,如果你还没遇到,说明你工作的年限可能比较短,或者你根本就是个假程序员!哈哈,开个玩笑. 当初面试官让我现场手写内存溢出案例代码,我 ...

  3. html编写气泡对话框,HTML+CSS入门 纯CSS手写圆角气泡对话框

    本篇教程介绍了HTML+CSS入门 纯CSS手写圆角气泡对话框,希望阅读本篇文章以后大家有所收获,帮助大家HTML+CSS入门. < 嗯--我们设计师强烈要求一定要圆角!圆角的气泡对话框,不要那 ...

  4. 手写内存泄漏检测组件

    手写内存泄漏检测组件 前言 内存泄漏 内存泄漏的现象与危害 内存泄漏检测组件的两个核心需求点 第一版:__libc_malloc, __libc_malloc 与 __builtin_return_a ...

  5. 万字图文 | 学会Java中的线程池,这一篇也许就够了!

    来源:一枝花算不算浪漫 线程池原理思维导图.png 前言 Java中的线程池已经不是什么神秘的技术了,相信在看的读者在项目中也都有使用过.关于线程池的文章也是数不胜数,我们站在巨人的肩膀上来再次梳理一 ...

  6. html编写气泡对话框,纯css手写圆角气泡对话框 微信小程序和web都适用

    嗯--我们设计师强烈要求一定要圆角!圆角的气泡对话框,不要那种尖角的.这其中还遇上了个尴尬的问题,z-index不生效 无非就是两种方法,一种是使用图片再定位拼接起来使用,太简单了具体就不详细的说了. ...

  7. 使用inetaddress测试目标可达性_纯java手写打造方法级白盒测试框架

    我们在做服务端测试时,经常使用的自动化测试框架或平台大多通过restful风格使用http协议接入系统,例如常见的Jmeter.LoadRunner.Postman等,还有不常用或付费的工具如Test ...

  8. g5 幸存者 android,g5幸存者游戏图文详细攻略 | 手游网游页游攻略大全

    发布时间:2016-07-02 史上最坑爹的游戏9第10关失控机器人怎么过关?第十关图文详细攻略,史上最坑爹的游戏9已经在儿童节前夕正式上市了,相信喜欢这个系列游戏的玩家一定会第一时间入手的,作为一款 ...

  9. 今天写的上传类,纯练手之作,供新人学习

    /* *@文件上传 *@要获取上传实例 请使用 Upload::getInstance(); 具体使用方法请参考类接口 *@auther 张文庆 *@email 812035863@qq.com *@ ...

最新文章

  1. 好听!前端竟然自己会变调! | 每日趣闻
  2. 代码Review发现问题
  3. 深入浅出etcd系列 – 心跳和选举
  4. python--从入门到实践--chapter 9 类
  5. 吴恩达深度学习 —— 4.5 搭建深层神经网络块
  6. FPGA仿真类型简介
  7. Eclipse使用Git上传新项目到GitHub
  8. java入门书籍《java语言程序设计 基础篇》原书第10版 强烈推荐
  9. redis安装之yum安装
  10. 证券交易系统术语介绍
  11. UDS tester之Tdrm
  12. H5 +蓝牙打印机 CPCL和ESC
  13. python按日期爬取b站弹幕 2.0版
  14. 什么是uni-app呀?
  15. Centos 7 拨号上网
  16. 扒光Session的那些小秘密
  17. 前端学习 之 JavaScript基础
  18. Java文件复制的三种方法
  19. nodejs+express对微信公众号进行二次开发--接收消息,自动回复文本,图片以及代码优化
  20. ubuntu下如何批量修改文件后缀名

热门文章

  1. 推荐:E都市三维地图, 酷!
  2. 常见的Web前端攻击
  3. 遗传算法神经网络预测彩票
  4. 34个漂亮的应用程序后台管理系统界面(系列二)
  5. HBase的RowKey设计原则含案例(全)
  6. 【备忘】AMD 显卡驱动问题一则及解决方案
  7. E1696 命令行错误: 无法打开 元数据 文件 “platform.winmd” 使用Windows运行时扩展 出现这个问题解决思路
  8. 几款常用的表单设计器解决方案
  9. 超实用!移动端界面中的版式设计原理(上)
  10. Android - 游戏中的声音