>>分享Android开发相关的技术 书籍支持  卫琴直播  品书摘要  在线测试  资源下载  联系我们
发表一个新主题 开启一个新投票 回复文章 您是本文章第 20942 个阅读者 刷新本主题
 * 贴子主题:  Android 加载大图/多图,有效避免OOM 回复文章 点赞(0)  收藏  
作者:flybird    发表时间:2020-02-06 20:13:39     消息  查看  搜索  好友  邮件  复制  引用

                                    

    图片压缩技术

     在大多数情况下,实际中用到的图片都会大于程序所需要的大小。比如系统图片库里展示的图片都是用手机摄像头拍摄的,这些图片的分辨率会比我们手机屏幕的分辨率高得多。而我们编写的应用程序都是有一定内存限制的,程序占用了过高的内存就很容易出现 OOM(OutOfMemory)异常。

         我们可以通过下面的代码查看应用程序最高可用内存是多少。                

  int maxMemory = ( int) (Runtime.getRuntime().maxMemory() /  1024);  
Log.d( "TAG",  "Max memory is " + maxMemory +  "KB");  

     在展示高分辨率图片的时候,最好先将图片进行压缩处理。压缩后的图片大小应该和用来展示它的控件大小相近。在一个很小的 ImageView 上显示一张超大的图片不会带来任何视觉上的好处,但会占用我们相当宝贵的内存,而且在性能上还会带来负面影响。

         如何对一张大图片进行适当的压缩,让它能够以最佳大小显示的同时,还能防止 OOM 的出现。            

    BitmapFactory

     BitmapFactory 类提供了多个解析方法( decodeByteArray、 decodeFile、 decodeResource 等)用于 创建 Bitmap 对象,我们根据 图片来源 选择合适的方法。比如:    
  • decodeStream 方法,网络上的图片
  • decodeFile 方法,SD 卡中的图片
  • decodeResource 方法,资源文件中的图片
     上述这些方法会尝试 为已经构建的 bitmap 分配内存,这时会很容易 导致 OOM 出现。为此每一种解析方法都提供了一个可选的  BitmapFactory.Options 参数,将这个参数的  inJustDecodeBounds 属性设置为 true 就可以让解析方法 禁止为 bitmap 分配内存,返回值也不再是一个 Bitmap 对象,而是 null。虽然 Bitmap 是 null 了,但是 BitmapFactory.Options 的  outWidth、  outHeight 和  outMimeType 属性都会被赋值。这个技巧让我们可以在加载图片之前就获取到图片的长宽值和 MIME 类型,从而根据情况对图片进行压缩。代码如下所示:        

BitmapFactory.Options options =  new BitmapFactory.Options();  
options.inJustDecodeBounds =  true;  
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);  
int imageHeight = options.outHeight;  
int imageWidth = options.outWidth;  
String imageType = options.outMimeType;  

     为了避免 OOM 异常,最好 在解析每张图片的时候都先检查一下图片的大小。

         现在图片的大小已经知道了,我们就可以决定是把整张图片加载到内存还是加载一个压缩版的图片到内存中。考虑因素如下:    
  • 预估一下加载整张图片所需要占用的内存;
  • 为了加载这张图片你所愿意提供多少的内存;
  • 用于展示这张图片的控件的实际大小;
  • 当前设备的屏幕尺寸和分辨率。
     我们怎样才能对图片进行压缩呢?

         通过设置 BitmapFactory.Options 中  inSampleSize 的值就可以实现。比如我们有一张 2048*1536 像素的图片,将 inSampleSize 的值设置为 4,就可以把这张图片压缩成 512*384 像素。假设该图片是 ARGB_8888 类型(即每个像素点占用 4 个字节),那么原本这张图片需要占用 13M 的内存,压缩后就只需要占用 0.75M 了。

         下面的方法可以根据传入的宽和高,计算尺合适的 inSampleSize 值:                

  public  static  int  calculateInSampleSize(BitmapFactory.Options options,  
         int reqWidth,  int reqHeight) {  
     // 源图片的高度和宽度  
     final  int height = options.outHeight;  
     final  int width = options.outWidth;  
     int inSampleSize =  1;  
     if (height > reqHeight || width > reqWidth) {  
         // 计算出实际宽高和目标宽高的比率  
         final  int heightRatio = Math.round(( float) height / ( float) reqHeight);  
         final  int widthRatio = Math.round(( float) width / ( float) reqWidth);  
         // 选择宽和高中最小的比率作为inSampleSize的值,这样可以保证最终图片的宽和高  
         // 一定都会大于等于目标的宽和高。  
        inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;  
    }  
     return inSampleSize;  
}  

     根据上面的方法,首先将 BitmapFactory.Options 的  inJustDecodeBounds 属性设置为 true,解析一次图片。然后将 BitmapFactory.Options 连同期望的图片的宽高一起传入  caculateInSampleSize 方法中, 就可以得到合适的 inSampleSize 值了。之后再解析一次图片,使用新获取到的 inSampleSize 值,并把 inJustDecodeBounds 设置为 false,就可以得到压缩后的图片了。                

  public  static Bitmap  decodeSampledBitmapFromResource(Resources res,  int resId,  
         int reqWidth,  int reqHeight) {  
     // 第一次解析将inJustDecodeBounds设置为true,来获取图片大小  
     final BitmapFactory.Options options =  new BitmapFactory.Options();  
    options.inJustDecodeBounds =  true;  
    BitmapFactory.decodeResource(res, resId, options);  
     // 调用上面定义的方法计算inSampleSize值  
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);  
     // 使用获取到的inSampleSize值再次解析图片  
    options.inJustDecodeBounds =  false;  
     return BitmapFactory.decodeResource(res, resId, options);  
}  

     例如将任意一张图片压缩成 100*100 的缩略图,并显示在 imageView 上。代码如下:                

mImageView.setImageBitmap(  
    decodeSampledBitmapFromResource(getResources(), R.id.myimage,  100,  100));  

    图片缓存技术

     在很多情况下,(比如使用 ListView、GridView 或者 ViewPager 这样的组件),屏幕上显示的图片可以通过滑动屏幕等事件不断地增加,最终导致 OOM。

         为了保证内存的使用始终维持在一个合理的范围,通常会把被移除屏幕的图片进行回收处理。此时垃圾回收器也会认为你不再持有这些图片的引用,从而对这些图片进行 GC 操作。用这种思路能解决问题,但是为了能让程序快速运行,在界面上迅速地加载图片,你又必须要考虑到某些图片被回收后,用户又将它重新滑入屏幕这张情况。这时重新去加载一遍刚刚加载过的图片无疑是性能的瓶颈,所以需要想办法来避免这个情况的发生。

         这个时候,使用 内存缓存技术 可以很好的解决这个问题。它可以让组件快速地重新加载和处理图片。

         如何使用内存缓存技术对图片进行缓存?            

    LruCache

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

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

         为了能够选择一个合适的缓存大小给 LruCache,可以考虑以下几个因素:    
  • 设备可以为每个应用程序分配多大的内存?
  • 设备屏幕上一次最多能显示多少张图片?有多少图片需要进行预加载(因为很可能很快会显示在屏幕上)?
  • 设备的屏幕大小和分辨率分别是多少?一个超高分辨率的设备比起一个较低分辨率的设备,在持有相同数量图片的时候,需要更大的缓存空间。
  • 图片的尺寸和大小,还有每张图片会占据多少内存空间。
  • 图片被访问的频率有多高?会不会有一些图片的访问频率比其它图片还要高?如果有的话,应该让一些图片常驻在内存中,或者使用多个 LruCache 对象来区分不同组的图片。
  • 维持好数量与质量之间的平衡,有些时候,存储多个低像素的图片,而在后台去开线程加载高像素的图片会更加有效。
     并没有一个指定的缓存大小可以满足所有的应用程序,这是由你决定的。你应该去分析程序内存的使用情况,然后制定出一个合适的解决方案。一个太小的缓存空间,有可能造成图片频繁地释放和重新加载,这并没有什么好处。而一个太大的缓存空间,则有可能还是会引起 OOM 异常。            

    示例

  private LruCache<String, Bitmap> mMemoryCache;  

@Override  
protected  void  onCreate(Bundle savedInstanceState) {  
     // 获取到可用内存的最大值,使用内存超出这个值会引起OutOfMemory异常。  
     // LruCache通过构造函数传入缓存值,以KB为单位。  
     int maxMemory = ( int) (Runtime.getRuntime().maxMemory() /  1024);  
     // 使用最大可用内存值的1/8作为缓存的大小。  
     int cacheSize = maxMemory /  8;  
    mMemoryCache =  new LruCache<String, Bitmap>(cacheSize) {  
         @Override  
         protected  int  sizeOf(String key, Bitmap bitmap) {  
             // 重写此方法来衡量每张图片的大小,默认返回图片数量。  
             return bitmap.getByteCount() /  1024;  
        }  
    };  
}  

public  void  addBitmapToMemoryCache(String key, Bitmap bitmap) {  
     if (getBitmapFromMemCache(key) ==  null) {  
        mMemoryCache.put(key, bitmap);  
    }  
}  

public Bitmap  getBitmapFromMemCache(String key) {  
     return mMemoryCache.get(key);  
}  

     上述例子中,使用系统分配给应用程序的八分之一内存来缓存大小。在中高配置的手机当中,大概有4M(32/8)的缓存空间。一个全屏幕的 GridView 使用4张 800*480 分辨率的图片来填充,则大概会占用1.5M(800*400*4)的空间。因此,这个缓存大小可以存储2.5页的图片。

         当向 ImageView 中加载一张图片时,首先会在 LruCache 的缓存中进行检查。如果找到了相应的键值,则会立刻更新 ImageView,否则开启一个后台线程来加载这张图片。                

  public  void  loadBitmap( int resId, ImageView imageView) {  
     final String imageKey = String.valueOf(resId);  
     final Bitmap bitmap = getBitmapFromMemCache(imageKey);  
     if (bitmap !=  null) {  
        imageView.setImageBitmap(bitmap);  
    }  else {  
        imageView.setImageResource(R.drawable.image_placeholder);  
        BitmapWorkerTask task =  new BitmapWorkerTask(imageView);  
        task.execute(resId);  
    }  
}  

     BitmapWorkerTask 还要把新加载的图片的键值对放到缓存中。                

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {  
     // 在后台加载图片。  
     @Override  
     protected Bitmap  doInBackground(Integer... params) {  
         final Bitmap bitmap = decodeSampledBitmapFromResource(  
                getResources(), params[ 0],  100,  100);  
        addBitmapToMemoryCache(String.valueOf(params[ 0]), bitmap);  
         return bitmap;  
    }  
}  

     文章只是作为个人记录学习使用,如有不妥之处请指正,谢谢。

                                                                    
----------------------------
原文链接:https://blog.csdn.net/MoDuRooKie/article/details/80279482

程序猿的技术大观园:www.javathinker.net



[这个贴子最后由 flybird 在 2020-02-06 20:57:13 重新编辑]
  Java面向对象编程-->Lambda表达式
  JavaWeb开发-->自定义JSP标签(Ⅰ)
  JSP与Hibernate开发-->域对象在持久化层的四种状态
  Java网络编程-->用Axis发布Web服务
  精通Spring-->通过Axios访问服务器
  Vue3开发-->通过Vuex进行状态管理
  如何提高Android代码的安全性
  Android开发实践:用脚本编译Android工程
  Android静默安装的实现
  Android带有粘性头部的ScrollView-WelliJohn的博客
  Android SDCard UnMounted 流程分析
  Android ExpandableListView 使用范例
  编译Irrlicht On Android
  Android在SDcard建文件夹(在Android中移动文件必用)
  Android语音识别 android.speech 包分析
  android 自定义view 实现定制二维码扫描框
  Android 概述
  MVVM 架构与数据绑定库
  RecyclerView顶部阴影透明度渐变效果
  Android adb你真的会用吗?
  Android 判断当前设备是手机还是平板
  更多...
 IPIP: 已设置保密
树形列表:   
1页 0条记录 当前第1
发表一个新主题 开启一个新投票 回复文章


中文版权所有: JavaThinker技术网站 Copyright 2016-2026 沪ICP备16029593号-2
荟萃Java程序员智慧的结晶,分享交流Java前沿技术。  联系我们
如有技术文章涉及侵权,请与本站管理员联系。