一、了解三级缓存

三级缓存一般分为:内存,磁盘和网络

重要步骤:
① UI:请求数据,使用唯一的Key值索引Memory Cache中的Bitmap。
② 内存缓存:缓存搜索,如果能找到Key值对应的Bitmap,则返回数据。否则执行第三步。
③ 硬盘存储:使用唯一Key值对应的文件名,检索SDCard上的文件。
④ 如果有对应文件,使用BitmapFactory.decode*方法,解码Bitmap并返回数据,同时将数据写入缓存。如果没有对应文件,执行第五步。
⑤ 下载图片:启动异步线程,从数据源下载数据(Web)。
⑥ 若下载成功,将数据同时写入硬盘和缓存,并将Bitmap显示在UI中。

二、有效的显示位图

如果使用bitmap的消耗过大,就会出现
java.lang.OutofMemoryError: bitmap size exceeds VM budget.的问题–OOM

导致bitmap的OOM的原因有如下几个:
1、Android的系统为每个应用提供至少16MB的内存,但是现在好点的设备会有所增加
2、Bitmap会消耗更多的内存,特别是像这样丰富的图片显示,当配置好点的显示的时候,设备会消耗掉每个应用的限制
3、Android的UI经常会立即加载几张位图,例如ListView,GridView和ViewPager等等

①计算图片的缩放比例(使用inSampleSize

/*** 获取缩放的比例** @param _options* @param _reWidth* @param _reHeight* @return*/public int calculateInSampleSize(BitmapFactory.Options _options, int _reWidth, int _reHeight) {//获取图片的高和宽int _width = _options.outWidth;int _height = _options.outHeight;int _inSampleSize = 1;//计算缩放比例,inSampleSize>1说明图片是被压缩的if (_width > _reWidth || _height > _reHeight) {int _halfWidth = _width / 2;int _halfHeight = _height / 2;while ((_halfWidth / _inSampleSize) > _reWidth && (_halfHeight / _inSampleSize) > _reHeight) {_inSampleSize *= 2;}}return _inSampleSize;}

②根据缩放比例进行缩放图片,返回bitmap
将inJustDecodeBounds设置为true,可以解码的时候避免分配内存

/*** 获取缩放之后的图片Bitmap** @param _resources* @param _res* @param _reWidth* @param _reHeight* @return*/public Bitmap getResourceBitmap(Resources _resources, int _res, int _reWidth, int _reHeight) {BitmapFactory.Options _options = new BitmapFactory.Options();//设置为true则解码器不计算_options.inJustDecodeBounds = true;//解码图片资源BitmapFactory.decodeResource(_resources, _res, _options);//根据图片的信息进行计算缩放比例_options.inSampleSize = this.calculateInSampleSize(_options, _reWidth, _reHeight);//设置为false则解码器开始计算_options.inJustDecodeBounds = false;//返回解码缩放之后的bitmapreturn BitmapFactory.decodeResource(_resources, _res, _options);}

③使用

Bitmap _bitmap = getResourceBitmap(getResources(), params[0], 200, 100);
mImageBitmapCache.setImageBitmap(bitmap);

三、内存缓存(LruCache的使用

  • 内存缓存技术对那些大量占用应用程序宝贵内存的图片提供了快速访问的方法。其中最核心的类是LruCache(此类在android-support-v4的包中提供) 。这个类非常适合用来缓存图片,它的主要算法原理是把最近使用的对象用强引用存储在 LinkedHashMap 中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。

  • 在过去,我们经常会使用一种非常流行的内存缓存技术的实现,即软引用或弱引用 (SoftReference or
    WeakReference)。但是现在已经不再推荐使用这种方式了,因为从 Android 2.3 (API Level 9)开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠。另外,Android 3.0 (API Level 11)中,图片的数据会存储在本地的内存当中,因而无法用一种可预见的方式将其释放,这就有潜在的风险造成应用程序的内存溢出并崩溃。

  • 并没有一个指定的缓存大小可以满足所有的应用程序,这是由你决定的。你应该去分析程序内存的使用情况,然后制定出一个合适的解决方案。一个太小的缓存空间,有可能造成图片频繁地被释放和重新加载,这并没有好处。而一个太大的缓存空间,则有可能还是会引起 java.lang.OutOfMemory 的异常。

①计算能使用的最大内存

int _maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

②计算分配的缓存的大小

int _cacheSize = _maxMemory / 8;

③创建一个LruCache对象

private LruCache<String, Bitmap> mMemoryCache;
mMemoryCache = new LruCache<String, Bitmap>(_cacheSize) {@Overrideprotected int sizeOf(String key, Bitmap value) {return value.getByteCount() / 1024;}};

④两个方法用于分别存取缓存数据

/*** 将bitmap添加到内存中** @param _key* @param _bitmap*/public void addBitmapToMemoryCache(String _key, Bitmap _bitmap) {if (null == getBitmapFromMemoryCache(_key))mMemoryCache.put(_key, _bitmap);}/*** 获取内存中的bitmap** @param _key* @return*/public Bitmap getBitmapFromMemoryCache(String _key) {return mMemoryCache.get(_key);}

⑤使用内存缓存,这里使用了AsyncTask在子线程中进行解码图片

/*** 点击按钮加载图片** @param _res*/public void loadImage(int _res) {String _resKey = String.valueOf(_res);Bitmap _bitmap = getBitmapFromMemoryCache(_resKey);if (null != _bitmap) {mImageBitmapCache.setImageBitmap(_bitmap);} else {BitmapWorkerTask _bitmapWorkerTask = new BitmapWorkerTask();_bitmapWorkerTask.execute(_res);}}/*** 使用AsyncTask来异步对图片的处理*/public class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {@Overrideprotected Bitmap doInBackground(Integer... params) {Bitmap _bitmap = getResourceBitmap(getResources(), params[0], 200, 100);addBitmapToMemoryCache(String.valueOf(params[0]), _bitmap);return _bitmap;}@Overrideprotected void onPostExecute(Bitmap bitmap) {if (null != bitmap)mImageBitmapCache.setImageBitmap(bitmap);}}

使用LruCache内存缓存代码:

public class BitmapCacheActivity extends BaseActivity {//UIprivate ImageView mImageBitmapCache;private Button mButtonBitmapCache;//LruCacheprivate LruCache<String, Bitmap> mMemoryCache;@Overridepublic void initData(Bundle savedInstanceState) {//计算最大的内存int _maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);//使用最大内存的1/8来进行内存缓存int _cacheSize = _maxMemory / 8;mMemoryCache = new LruCache<String, Bitmap>(_cacheSize) {@Overrideprotected int sizeOf(String key, Bitmap value) {return value.getByteCount() / 1024;}};}@Overridepublic void initView(Bundle savedInstanceState) {setContentView(R.layout.activity_bitmap_cache);this.mImageBitmapCache = (ImageView) findViewById(R.id.activity_bitmap_cache_image);this.mButtonBitmapCache = (Button) findViewById(R.id.activity_bitmap_cache_button);}@Overridepublic void loadData(Bundle savedInstanceState) {this.mButtonBitmapCache.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {loadImage(R.mipmap.bitmap_cache);}});}/*** 将bitmap添加到内存中** @param _key* @param _bitmap*/public void addBitmapToMemoryCache(String _key, Bitmap _bitmap) {if (null == getBitmapFromMemoryCache(_key))mMemoryCache.put(_key, _bitmap);}/*** 获取内存中的bitmap** @param _key* @return*/public Bitmap getBitmapFromMemoryCache(String _key) {return mMemoryCache.get(_key);}/*** 点击按钮加载图片** @param _res*/public void loadImage(int _res) {String _resKey = String.valueOf(_res);Bitmap _bitmap = getBitmapFromMemoryCache(_resKey);if (null != _bitmap) {mImageBitmapCache.setImageBitmap(_bitmap);} else {BitmapWorkerTask _bitmapWorkerTask = new BitmapWorkerTask();_bitmapWorkerTask.execute(_res);}}/*** 使用AsyncTask来异步对图片的处理*/public class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {@Overrideprotected Bitmap doInBackground(Integer... params) {Bitmap _bitmap = getResourceBitmap(getResources(), params[0], 200, 100);addBitmapToMemoryCache(String.valueOf(params[0]), _bitmap);return _bitmap;}@Overrideprotected void onPostExecute(Bitmap bitmap) {if (null != bitmap)mImageBitmapCache.setImageBitmap(bitmap);}}/*** 获取缩放之后的图片Bitmap** @param _resources* @param _res* @param _reWidth* @param _reHeight* @return*/public Bitmap getResourceBitmap(Resources _resources, int _res, int _reWidth, int _reHeight) {BitmapFactory.Options _options = new BitmapFactory.Options();//设置为true则解码器不计算_options.inJustDecodeBounds = true;//解码图片资源BitmapFactory.decodeResource(_resources, _res, _options);//根据图片的信息进行计算缩放比例_options.inSampleSize = this.calculateInSampleSize(_options, _reWidth, _reHeight);//设置为false则解码器开始计算_options.inJustDecodeBounds = false;//返回解码缩放之后的bitmapreturn BitmapFactory.decodeResource(_resources, _res, _options);}/*** 获取缩放的比例** @param _options* @param _reWidth* @param _reHeight* @return*/public int calculateInSampleSize(BitmapFactory.Options _options, int _reWidth, int _reHeight) {//获取图片的高和宽int _width = _options.outWidth;int _height = _options.outHeight;int _inSampleSize = 1;//计算缩放比例,inSampleSize>1说明图片是被压缩的if (_width > _reWidth || _height > _reHeight) {int _halfWidth = _width / 2;int _halfHeight = _height / 2;while ((_halfWidth / _inSampleSize) > _reWidth && (_halfHeight / _inSampleSize) > _reHeight) {_inSampleSize *= 2;}}return _inSampleSize;}
}

四、使用内存缓存和磁盘缓存(LruCache和DiskLruCache)

①先查看SD卡是否挂载
/*** 获取磁盘缓存的地址,如果有SD卡,则创建SD卡上的缓存目录,如果没有SD卡,则创建内部缓存目录** @param _context* @param _name* @return*/public File getDiskCacheDir(Context _context, String _name) {String _cachePath;//判断SD卡是否挂载和SD卡非移除状态if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || !Environment.isExternalStorageRemovable()) {_cachePath = _context.getExternalCacheDir().getPath();} else {_cachePath = _context.getCacheDir().getPath();}return new File(_cachePath + File.separator + _name);}

②添加到Disk缓存中(key对应的OutPutStream)

synchronized (mDiskCacheLock) {try {if (null != mDiskLruCache && null == mDiskLruCache.get(_key)) {DiskLruCache.Editor _editor = mDiskLruCache.edit(_key);//将bitmap写入到outputstream中去_bitmap.compress(Bitmap.CompressFormat.JPEG, 30, _editor.newOutputStream(0));//提交_editor.commit();}} catch (IOException e) {e.printStackTrace();}}

③获取Disk缓存中数据(根据key获取inputStream)

/*** 获取disk中的缓存** @param _key* @return*/public Bitmap getBitmapFromDiskCache(String _key) {Bitmap _bitmap = null;while (mDiskCacheStaring) {try {//mDiskLruCache没有初始化的时候,不能进行获取流mDiskCacheLock.wait();} catch (InterruptedException e) {e.printStackTrace();}}if (null != mDiskLruCache) {try {DiskLruCache.Snapshot _snapShot = mDiskLruCache.get(_key);if (null != _snapShot) {//将获取到的inputstream转为bitmapInputStream _is = _snapShot.getInputStream(0);_bitmap = BitmapFactory.decodeStream(_is);return _bitmap;}} catch (IOException e) {e.printStackTrace();}}return _bitmap;}

④在子线程中初始化DiskLruCache对象

 /*** 在子线程中初始化DiskLruCache对象*/public class InitDiskCache implements Runnable {@Overridepublic void run() {synchronized (mDiskCacheLock) {try {mDiskLruCache = DiskLruCache.open(mCacheDir, 1, 1, DISK_CACHE_SIZE);mDiskCacheStaring = false;mDiskCacheLock.notifyAll();} catch (IOException e) {e.printStackTrace();}}}}

⑤执行操作
1、初始化DiskLruCache

//初始化DiskLruCachemCacheDir = getDiskCacheDir(this, DISK_CACHE_NAME);new Thread(new InitDiskCache()).start();

2、执行

Bitmap _diskBitmap = getBitmapFromDiskCache(_resKey);
if (null != _diskBitmap) {mImageBitmapCache.setImageBitmap(_diskBitmap);

⑥附加

//DiskLruCacheprivate DiskLruCache mDiskLruCache;private Object mDiskCacheLock = new Object();//创建对象锁private boolean mDiskCacheStaring = true;private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10;//磁盘缓存的文件大小private static final String DISK_CACHE_NAME = "Millet";//磁盘缓存的名字private File mCacheDir;

完整代码:

public class BitmapCacheActivity extends BaseActivity {//UIprivate ImageView mImageBitmapCache;private Button mButtonBitmapCache;//LruCacheprivate LruCache<String, Bitmap> mMemoryCache;//DiskLruCacheprivate DiskLruCache mDiskLruCache;private Object mDiskCacheLock = new Object();//创建对象锁private boolean mDiskCacheStaring = true;private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10;//磁盘缓存的文件大小private static final String DISK_CACHE_NAME = "Millet";//磁盘缓存的名字private File mCacheDir;@Overridepublic void initData(Bundle savedInstanceState) {//计算最大的内存int _maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);//使用最大内存的1/8来进行内存缓存int _cacheSize = _maxMemory / 8;mMemoryCache = new LruCache<String, Bitmap>(_cacheSize) {@Overrideprotected int sizeOf(String key, Bitmap value) {return value.getByteCount() / 1024;}};//初始化DiskLruCachemCacheDir = getDiskCacheDir(this, DISK_CACHE_NAME);new Thread(new InitDiskCache()).start();}@Overridepublic void initView(Bundle savedInstanceState) {setContentView(R.layout.activity_bitmap_cache);this.mImageBitmapCache = (ImageView) findViewById(R.id.activity_bitmap_cache_image);this.mButtonBitmapCache = (Button) findViewById(R.id.activity_bitmap_cache_button);}@Overridepublic void loadData(Bundle savedInstanceState) {this.mButtonBitmapCache.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {loadImage(R.mipmap.bitmap_cache);}});}/*** 将bitmap添加到内存中和Disk缓存中** @param _key* @param _bitmap*/public void addBitmapToCache(String _key, Bitmap _bitmap) {if (null == getBitmapFromMemoryCache(_key))mMemoryCache.put(_key, _bitmap);synchronized (mDiskCacheLock) {try {if (null != mDiskLruCache && null == mDiskLruCache.get(_key)) {DiskLruCache.Editor _editor = mDiskLruCache.edit(_key);//将bitmap写入到outputstream中去_bitmap.compress(Bitmap.CompressFormat.JPEG, 30, _editor.newOutputStream(0));//提交_editor.commit();}} catch (IOException e) {e.printStackTrace();}}}/*** 获取内存中的bitmap** @param _key* @return*/public Bitmap getBitmapFromMemoryCache(String _key) {return mMemoryCache.get(_key);}/*** 获取disk中的缓存** @param _key* @return*/public Bitmap getBitmapFromDiskCache(String _key) {Bitmap _bitmap = null;while (mDiskCacheStaring) {try {//mDiskLruCache没有初始化的时候,不能进行获取流mDiskCacheLock.wait();} catch (InterruptedException e) {e.printStackTrace();}}if (null != mDiskLruCache) {try {DiskLruCache.Snapshot _snapShot = mDiskLruCache.get(_key);if (null != _snapShot) {//将获取到的inputstream转为bitmapInputStream _is = _snapShot.getInputStream(0);_bitmap = BitmapFactory.decodeStream(_is);return _bitmap;}} catch (IOException e) {e.printStackTrace();}}return _bitmap;}/*** 点击按钮加载图片** @param _res*/public void loadImage(int _res) {String _resKey = String.valueOf(_res);Bitmap _memoryBitmap = getBitmapFromMemoryCache(_resKey);Bitmap _diskBitmap = getBitmapFromDiskCache(_resKey);if (null != _memoryBitmap) {mImageBitmapCache.setImageBitmap(_memoryBitmap);} else if (null != _diskBitmap) {mImageBitmapCache.setImageBitmap(_diskBitmap);} else {BitmapWorkerTask _bitmapWorkerTask = new BitmapWorkerTask();_bitmapWorkerTask.execute(_res);}}/*** 使用AsyncTask来异步对图片的处理*/public class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {@Overrideprotected Bitmap doInBackground(Integer... params) {Bitmap _bitmap = getResourceBitmap(getResources(), params[0], 200, 100);addBitmapToCache(String.valueOf(params[0]), _bitmap);return _bitmap;}@Overrideprotected void onPostExecute(Bitmap bitmap) {if (null != bitmap)mImageBitmapCache.setImageBitmap(bitmap);}}/*** 获取缩放之后的图片Bitmap** @param _resources* @param _res* @param _reWidth* @param _reHeight* @return*/public Bitmap getResourceBitmap(Resources _resources, int _res, int _reWidth, int _reHeight) {BitmapFactory.Options _options = new BitmapFactory.Options();//设置为true则解码器不计算_options.inJustDecodeBounds = true;//解码图片资源BitmapFactory.decodeResource(_resources, _res, _options);//根据图片的信息进行计算缩放比例_options.inSampleSize = this.calculateInSampleSize(_options, _reWidth, _reHeight);//设置为false则解码器开始计算_options.inJustDecodeBounds = false;//返回解码缩放之后的bitmapreturn BitmapFactory.decodeResource(_resources, _res, _options);}/*** 获取缩放的比例** @param _options* @param _reWidth* @param _reHeight* @return*/public int calculateInSampleSize(BitmapFactory.Options _options, int _reWidth, int _reHeight) {//获取图片的高和宽int _width = _options.outWidth;int _height = _options.outHeight;int _inSampleSize = 1;//计算缩放比例,inSampleSize>1说明图片是被压缩的if (_width > _reWidth || _height > _reHeight) {int _halfWidth = _width / 2;int _halfHeight = _height / 2;while ((_halfWidth / _inSampleSize) > _reWidth && (_halfHeight / _inSampleSize) > _reHeight) {_inSampleSize *= 2;}}return _inSampleSize;}/*** 获取磁盘缓存的地址,如果有SD卡,则创建SD卡上的缓存目录,如果没有SD卡,则创建内部缓存目录** @param _context* @param _name* @return*/public File getDiskCacheDir(Context _context, String _name) {String _cachePath;//判断SD卡是否挂载和SD卡非移除状态if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || !Environment.isExternalStorageRemovable()) {_cachePath = _context.getExternalCacheDir().getPath();} else {_cachePath = _context.getCacheDir().getPath();}return new File(_cachePath + File.separator + _name);}/*** 在子线程中初始化DiskLruCache对象*/public class InitDiskCache implements Runnable {@Overridepublic void run() {synchronized (mDiskCacheLock) {try {mDiskLruCache = DiskLruCache.open(mCacheDir, 1, 1, DISK_CACHE_SIZE);mDiskCacheStaring = false;mDiskCacheLock.notifyAll();} catch (IOException e) {e.printStackTrace();}}}}}

Android图片的三级缓存整理相关推荐

  1. Android图片的三级缓存

    图片的三级缓存目的 主要目的是为了节省流量.加快加载速度: 每个 app 都会有大量的网络图片存在,当我们不做处理,每次打开 app 都去加载大量网络图片时,会耗费大量的流量,当网速不好时加载速度很慢 ...

  2. Android 系统(173)---Android中图片的三级缓存

    Android中图片的三级缓存 为什么要使用三级缓存 如今的 Android App 经常会需要网络交互,通过网络获取图片是再正常不过的事了 假如每次启动的时候都从网络拉取图片的话,势必会消耗很多流量 ...

  3. android picasso 三级缓存,Android中图片的三级缓存浅析

    图片的三级缓存机制一般是指应用加载图片的时候,分别去访问内存,文件和网络而获取图片数据的一种行为.以下内容只是简单的介绍了三级缓存的思想和大致流程,还有很多细节未进行处理,如果想深入研究可以在Gith ...

  4. android三级缓存封装,Android 中图片的三级缓存详解

    图片的三级缓存机制一般是指应用加载图片的时候,分别去访问内存,文件和网络而获取图片数据的一种行为. 一.三级缓存流程图 三级缓存流程图 二.代码框架搭建 这里我仿造Picasso[3]的加载图片代码, ...

  5. Android中图片的三级缓存策略

    在开发过程中,经常会碰到进行请求大量的网络图片的样例.假设处理的不好.非常easy造成oom.对于避免oom的方法,无非就是进行图片的压缩.及时的回收不用的图片.这些看似简单可是处理起来事实上涉及的知 ...

  6. 【android】实现图片的三级缓存。工具类

    本文主要分析以下几点 为什么使用三级缓存 什么是三级缓存 三级缓存原理 代码的具体实现:有注释 工具类使用方法 github分享地址 1.为什么使用三级缓存 如今的 Android App 经常会需要 ...

  7. android bitmap图片下载三级缓存

    Google为Android开发提供了一个培训教程,在加载图片一节中提供了示例程序BitmapFun,实现了图片下载.缓存.解析加载的功能,具体分析如下: 1.程序介绍 程序整体结构如上图所示,应用启 ...

  8. Android中的三级缓存解析与实战

    凡永恒伟大的爱,都要绝望一次,消失一次,一度死,才会重获爱,重新知道生命的价值.--<文学回忆录> 1.概述 由于Bitmap的特殊性以及Android对单个进程应用只分配16M的内存,这 ...

  9. Android 浅析Glide三级缓存

    Android 简要分析Glide三级缓存 概括 Glide version is 4.8.0 Glide拥有三级缓存,分别为 当前正在使用得资源缓存(ActiveResources) 内存缓存(Lr ...

  10. Android图片加载缓存库3

    Universal-Image-Loader 基本介绍及使用 大家平时做项目的时候,或多或少都会接触到异步加载图片,或者大量加载图片的问题,而加载图片时候经常会遇到各种问题,如oom,图片加载混乱等. ...

最新文章

  1. TensorFlow惊现大bug?网友:这是逼着我们用PyTorch啊
  2. 阿里云上创建个人网站
  3. 【算法竞赛学习】资金流入流出预测-挑战Baseline_数据探索与分析1
  4. 20100519 学习记录:asp CreateFolder/上传附件
  5. 四年级上册数学计算机笔记,四年级数学下册笔记整理
  6. Datax将Oracle数据导入ElasticSearch7完成教程
  7. 【转】解决XMLHTTP获取网页中文乱码问题
  8. Gartner:克服SIEM部署失败的通病
  9. 使用python制作聊天框解谜游戏_使用Python写一个小游戏alien invasion!
  10. 前嗅ForeSpider教程:采集图片/视频/资源文件的链接地址 1
  11. 筛选出英语与计算机成绩之和,职称计算机和英语考试的成绩什么地方能够查到 – 手机爱问...
  12. 打印快递面单pdf_如何开通使用拼多多电子面单?
  13. 英文怎么读_数学公式的英文读法
  14. php 模板 {{}},PHP模板技术
  15. html5都能干嘛种花,把这19种花种起来,整个家都能变成大花园!
  16. APP在推广之渠道为王(一 )
  17. 神兵利器——敏感文件发现工具
  18. python(3.10,Win10 64位)的wordcloud安装
  19. 北京元阔装饰安徽分公司 装修过后两年不到的墙面 及 相关
  20. Linux:生成core的几种方式

热门文章

  1. Linux Regmap分析
  2. 多线程编程(Linux C)
  3. 深入解读Linux内存管理系列(6)——地址空间划分
  4. Linux内核学习笔记(一)CFS完全公平调度类
  5. st7789s显示芯片驱动代码
  6. Java的GUI学习六(Action事件)
  7. 面向对象15:单例设计模式、main方法的使用
  8. MongoDB最佳实践
  9. 思科网院--路由和交换基础---Packet Tracer路由器使用基础
  10. mysql找出最大的天数_mysql 计算连续登录最大天数