「性能优化系列」不使用第三方库,Bitmap的优化策略

简介: 在Android3.0到Android7.0,Bitmap对象和像素都是放置到Java堆中,这个时候即使不调用recycle,Bitmap内存也会随着对象一起被回收。虽然Bitmap内存可以很容易被回收,但是Java堆的内存有很大的限制,也很容易造成GC。在Android8.0的时候,Bitmap内存又重新放置到了Native中。Bitmap造成OOM很多时候也是因为对Bitmap的资源没有得到很好的利用,同时没有做到及时的释放。

如今市场上有很多封装好的第三方库,对Bitmap内存也是做到了很好的优化,比如Glide、Fresco,每次加载只要直接调用就好,但是除掉第三方库外,我们还是需要去了解一下Bitmap的基本优化手段。

一、Bitmap内存进程

首先我们有必要去了解一下Bitmap的基本知识点,在Android3.0之前,Bitmap的对象是放在Java堆中,而Bitmap的像素是放置在Native内存中,这个时候需要手动的去调用recycle,才能去回收Native内存;


在Android3.0到Android7.0,Bitmap对象和像素都是放置到Java堆中,这个时候即使不调用recycle,Bitmap内存也会随着对象一起被回收。虽然Bitmap内存可以很容易被回收,但是Java堆的内存有很大的限制,也很容易造成GC。

在Android8.0的时候,Bitmap内存又重新放置到了Native中。

Bitmap造成OOM很多时候也是因为对Bitmap的资源没有得到很好的利用,同时没有做到及时的释放。

二、优化策略

对于Bitmap的优化主要分为针对不同密度的设备合理的分配资源,压缩以及缓存处理三种。

2.1.drawable的合理分配


总所周知,drawable时放置本地图片资源的地方,从上图可以发现,AS将drawable分为了mdpi,hdpi,xhdpi...不同的等级,简单概括为不同等级的dpi代表着不同的设备密度,它们之间的区别暂时先不论,有必要先去了解一下AS对于drawable的匹配规则.


举个例子,当当前的设备密度为xhdpi,此时代码中ImageView需要去引用drawable中的图片,那么根据匹配规则,系统首先会在drawable-xhdpi文件夹中去搜索,如果需要的图片存在,那么直接显示;如果不存在,那么系统将会开始从更高dpi中搜索,例如drawable-xxhdpi,drawable-xxxhdpi,如果在高dpi中搜索不到需要的图片,那么就会去drawable-nodpi中搜索,有则显示,无则继续向低dpi,如drawable-hdpi,drawable-mdpi,drawable-ldpi等文件夹一级一级搜索.


当在比当前设备密度低的文件夹中搜到图片,那么在ImageView(宽高在wrap_content状态下)中显示的图片将会被放大.图片放大也就意味着所占内存也开始增多.这也就是为什么分辨率不高的图片随意放置在drawable中也会出现OOM.而在高密度文件夹中搜到图片,图片在该设备上将会被缩小,内存也就相应减少.


在理想的状态下,不同dpi的文件下应该放置相应dpi的图片资源,以对不同的设备进行适配.但在图片资源没有做dpi区分的时候,根据以上所说的匹配规则,将图片资源放置在高dpi 如drawable-xdpi,drawable-xxdpi文件夹中.是比较好的选择,在最大程度上减少OOM的几率。

2.2.尺寸优化

当装载图片的容器例如ImageView只有100100,而图片的分辨率为800 800,这个时候将图片直接放置在容器上,很容易OOM,同时也是对图片和内存资源的一种浪费。当容器的宽高都很小于图片的宽高,其实就需要对图片进行尺寸上的压缩,将图片的分辨率调整为ImageView宽高的大小,一方面不会对图片的质量有影响,同时也可以很大程度上减少内存的占用。

对于尺寸压缩首先需要去了解一个知识点inSampleSize,



从上图发现Android官方对它的解释是,如果inSampleSize 设置的值大于1,则请求解码器对原始的bitmap进行子采样图像,然后返回较小的图片来减少内存的占用,例如inSampleSize == 4,则采样后的图像宽高为原图像的1/4,而像素值为原图的1/16,也就是说采样后的图像所占内存也为原图所占内存的1/16;当inSampleSize <=1时,就当作1来处理也就是和原图一样大小。另外最后一句还注明,inSampleSize的值一直为2的幂,如1,2,4,8。任何其他的值也都是四舍五入到最接近2的幂。


采样率inSampleSize其实是一个规定图片压缩倍数的一个参数,通过图片宽高的比较得到一个新的数值,inSampleSize设置到BitmapFactory中重新去解码图片。下面就是利用inSampleSize对图片进行尺寸上的优化代码。

/**

    * 对图片进行解码压缩。

    *

    * @param resourceId 所需压缩的图片资源

    * @param reqHeight  所需压缩到的高度

    * @param reqWidth   所需压缩到的宽度

    * @return Bitmap

    */

   private Bitmap decodeBitmap(int resourceId, int reqHeight, int reqWidth) {

       BitmapFactory.Options options = new BitmapFactory.Options();

       //inJustDecodeBounds设置为true,解码器将返回一个null的Bitmap,系统将不会为此Bitmap上像素分配内存。

       //只做查询图片宽高用。

       options.inJustDecodeBounds = true;

       BitmapFactory.decodeResource(getResources(), resourceId, options);

       //查询该图片的宽高。

       int height = options.outHeight;

       int width = options.outWidth;

       int inSampleSize = 1;


       //如果当前图片的高或者宽大于所需的高或宽,

       // 就进行inSampleSize的2倍增加处理,直到图片宽高符合所需要求。

       if (height > reqHeight || width > reqWidth) {

           int halfHeight = height / 2;

           int halfWidth = width / 2;

           while ((halfHeight / inSampleSize >= reqHeight)

                   && (halfWidth / inSampleSize) >= reqWidth) {

               inSampleSize *= 2;

           }

       }


       //inSampleSize获取结束后,需要将inJustDecodeBounds置为false。

       options.inJustDecodeBounds = false;

       //返回压缩后的Bitmap。

       return BitmapFactory.decodeResource(getResources(), resourceId, options);

   }


2.3.质量优化

一般情况下质量压缩是不推荐的一种优化手法,此手法压缩后图片将会失真。但不排除有项目对图片的清晰度没有过高的要求。


在开始谈如何压缩之前我们需要了解一下Bitmap的质量等级,在API29中,将Bitmap分为ALPHA_8, RGB_565, ARGB_4444, ARGB_8888, RGBA_F16, HARDWARE六个等级。


  • ALPHA_8:不存储颜色信息,每个像素占1个字节;
  • RGB_565:仅存储RGB通道,每个像素占2个字节,对Bitmap色彩没有高要求,可以使用该模式;
  • ARGB_4444:已弃用,用ARGB_8888代替;
  • ARGB_8888:每个像素占用4个字节,保持高质量的色彩保真度,默认使用该模式;
  • RGBA_F16:每个像素占用8个字节,适合宽色域和HDR;
  • HARDWARE:一种特殊的配置,减少了内存占用同时也加快了Bitmap的绘制。


每个等级每个像素所占用的字节也都不一样,所存储的色彩信息也不同。同一张100像素的图片,ARGB_8888就占了400字节,RGB_565才占200字节,RGB_565在内存上取得了优势,但是Bitmap的色彩值以及清晰度却不如ARGB_8888模式下的Bitmap。质量压缩说到底就是用清晰度来换内存。


质量压缩的具体操作也和上面2.2一样,只是将options.inPreferredConfig 设置为所需的图片质量,如下:


options.inPreferredConfig = Bitmap.Config.ARGB_8888;


2.4.缓存

不管是从网络上下载图片,还是直接从USB中读取图片,缓存对于图片加载的优化起到了至关重要的作用。当我们首次从网络上或者USB读取图片,会对图片进行相应的压缩处理。在处理过后不加入缓存,下一次请求图片还是直接从网络上或者USB中直接读取,不仅消耗了用户的流量还重复对图片进行压缩处理,占用多余内存的同时加载图片也很缓慢。


对于缓存,目前的策略是内存缓存和存储设备缓存。当加载一张图片时,首先会从内存中去读取,如果没有就接着在存储设备中读,最后才直接从网络或者USB中读取。接下来就聊一聊这两种缓存的具体内容。


2.4.1.内存缓存


LRU是用于实现内存缓存的一种常见算法,LRU也叫做最近最少使用算法,通俗来讲就是当缓存满了的时候,就会优先的去淘汰最近最少使用的缓存对象。接下来就以代码的方式直观的分析。


...

private LruCache<Integer,Bitmap> mCache;


   @Override

   protected void onCreate(Bundle savedInstanceState) {

       super.onCreate(savedInstanceState);

       setContentView(R.layout.activity_main);



       //1.初始化LruCache.

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

       int cacheSize = maxMemory / 8;

       mCache = new LruCache<Integer,Bitmap>(cacheSize){

           @Override

           protected int sizeOf(Integer key, Bitmap value) {

               return value.getRowBytes() * value.getHeight() / 1024;

           }

       };


   }


   //2.从Cache中获取数据

   public Bitmap getDataFromCache(int key) {

       if (mCache.size() != 0) {

           return mCache.get(key);

       }

       return null;

   }


   //3.将数据存储到Cache中

   public void putDataToCache(int key, Bitmap bitmap) {

       if (getDataFromCache(key) == null) {

           mCache.put(key,bitmap);

       }

   }

...


从代码中看首先对LruCache进行初始化,获取当前进程可用的内存,然后将内存缓存的容量制定为可用内存的1/8,同时对Bitmap对象进行大小的计算。接着构造出两个对外的方法,一个是根据Key从Cache中获取数据,一个是将数据存储到cache中。简单的3步也就完成了LruCache的使用。


2.4.2.磁盘缓存


磁盘缓存所使用的算法为DiskLruCache,它的使用比内存缓存要复杂一点,但是还是离不开上面的3步,初始化,查找和添加。同样的,直接从代码中开始分析。


2.4.2.1.DiskLruCache的初始化


private final static int DISK_MAX_SIZE = 20 * 1024 * 1024;

   private DiskLruCache mDiskLruCache;


   @Override

   protected void onCreate(Bundle savedInstanceState) {

       super.onCreate(savedInstanceState);

       setContentView(R.layout.activity_main);


       //初始化DiskLruCache。

       File directory = getFile(this,"DiskCache");

       if (!directory.exists()) {

           directory.mkdirs();

       }

       try {

           mDiskLruCache = DiskLruCache.open(directory, 1, 1, DISK_MAX_SIZE);

       } catch (IOException e) {

           e.printStackTrace();

       }

   }


private File getFile(Context context,String dirName){

       String filePath = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)

               ? Objects.requireNonNull(context.getExternalCacheDir()).getPath() :         context.getCacheDir().getPath();

       return new File(filePath + File.pathSeparator + dirName);

   }


DiskLruCache的创建是DiskLruCache.open()来创建,其中会传入4个参数,第一个参数表示磁盘缓存所要存储的路径,一般来说,如果外部设备存在,那么存储路径放置在  /storage/emulated/0/Android/data/package_name/cache 中;反之就放置在 /data/data/package_name/cache 这个目录下。存储路径可以根据自己的实际要求进行制定,值得注意的是,如果缓存路径选择SD卡上的缓存目录,即 /storage/emulated/0/Android/data/package_name/cache,那么当应用被卸载时,该目录也会被删除。


第二个参数表示应用的版本号,直接设置为1即可;第三个参数表示单个节点所对应的数据的个数,设置为1即可;第四个参数表示磁盘缓存的容量大小。


2.4.2.2.DiskLruCache的添加


private void addDataToDisk(String url) {

       //采用url的md5值作为key。

       String key = hashKeyFromUrl(url);

       try {

           DiskLruCache.Editor editor = mDiskLruCache.edit(key);

           if (editor != null) {

               OutputStream outputStream = editor.newOutputStream(0);

               if (downloadDataFromNet(url, outputStream)) {

                   //提交至缓存

                   editor.commit();

               } else {

                   //回退整个操作

                   editor.abort();

               }

               mDiskLruCache.flush();

           }

       } catch (IOException e) {

           e.printStackTrace();

       }

   }


private String hashKeyFromUrl(String url) {

       String cacheKey;

       try {

           final MessageDigest digest = MessageDigest.getInstance("MD5");

           digest.update(url.getBytes());


           cacheKey = bytesToHexString(digest.digest());


       } catch (NoSuchAlgorithmException e) {

           cacheKey = String.valueOf(url.hashCode());

       }


       return cacheKey;

   }


private String bytesToHexString(byte[] bytes) {

       StringBuilder sb = new StringBuilder();

       for (int i = 0; i < bytes.length; i++) {

           String hex = Integer.toHexString(0xFF & bytes[i]);

           if (hex.length() == 1) {

               sb.append('0');

           }

           sb.append(hex);

       }


       return sb.toString();

   }


DiskLruCache的添加主要是由DiskLruCache.Editor来完成,首先我们会采用url的md5值来作为key,通过.Editor和key获取一个文件输出流,下载好图片通过这个文件输出流写入到文件系统中,最后通过editor.commit()的方法将文件提交才算真正将图片写入文件系统。

2.4.2.3.DiskLruCache的查找


private Bitmap getDataFromDisk(String url) {

       Bitmap bitmap = null;

       String key = hashKeyFromUrl(url);

       try {

           DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);

           if (snapshot != null) {

               FileInputStream inputStream = (FileInputStream) snapshot.getInputStream(0);

               return BitmapFactory.decodeStream(inputStream);

           }

       } catch (IOException e) {

           e.printStackTrace();

       }

       return null;

   }


DiskLruCache的添加是通过Editor来完成,而查找是由DiskLruCache.Snapshot来完成的。首先通过url获取到当前文件的key值,初始化Snapshot后获取一个文件输入流,最后通过该文件输入流来解析出当前缓存的文件。

三、总结

上面已经分别描述了几种优化手段,最后再来总结一下。


  1. 根据不同的密度的设备将图片资源放置再不同的drawable文件夹中;
  2. 利用inSampleSize对图片进行尺寸上的压缩;
  3. 利用inPreferredConfig对图片进行质量上的压缩;
  4. 利用三级缓存,依次从内存缓存(LruCache)磁盘缓存(DiskLruCache)网络上获取图片;
目录
相关文章
|
4月前
|
缓存 监控 Android开发
安卓应用性能优化的实用策略
【4月更文挑战第2天】 在竞争激烈的应用市场中,一款应用的性能直接影响用户体验和市场表现。本文针对安卓平台,深入探讨了性能优化的关键要素,包括内存管理、代码效率、UI渲染和电池使用效率。通过分析常见的性能瓶颈,并提供针对性的解决策略,旨在帮助开发者构建更加流畅、高效的安卓应用。
|
23天前
|
缓存 前端开发 网络协议
性能优化|几个方法让图片加载更快一些
对电商网页的性能而言,图片优化是至关重要的事情,本文就此探讨了一些简单、可靠的图片优化手段。
|
2月前
|
SQL 缓存 Java
系统性能优化总结
系统性能优化总结
53 10
|
2月前
|
缓存 监控 NoSQL
深入解析数据库性能优化:策略与实践
【7月更文挑战第23天】数据库性能优化是一个复杂而持续的过程,涉及硬件、软件、架构、管理等多个方面。通过本文的介绍,希望能够为读者提供一个全面的性能优化框架,帮助大家在实际工作中更有效地提升数据库性能。记住,优化不是一蹴而就的,需要持续的观察、分析和调整。
|
4月前
|
缓存 NoSQL Redis
接口性能优化的小技巧
接口性能优化的小技巧
|
4月前
|
存储 缓存 编解码
Android 性能优化: 解释Bitmap的优化策略。
Android 性能优化: 解释Bitmap的优化策略。
75 1
|
存储 编译器 C语言
性能优化特性之:LTO
本文介绍了倚天实例上的编译优化特性:LTO,并从优化原理、使用方法进行了详细阐述。
|
缓存 前端开发 JavaScript
前端如何进行性能优化的方法(详细版本)
每当有人访问您网站上的页面时,浏览器都必须请求大量文件。这些HTTP请求直接影响网页的加载速度。通常,更少的HTTP请求意味着网站加载速度更快。 现在,网站的加载速度是搜索引擎排名的重要因素。平均而言,媒体页面加载速度为谷歌的10个结果只是1.65秒。
152 0
|
编译器 C++ Anolis
性能优化特性之:PGO
本文介绍了倚天实例上的编译优化特性:PGO,并从优化原理、使用方法进行了详细阐述。
|
存储 缓存 NoSQL
性能优化方案及思考
周末闲暇在家,朋友让我帮忙优化一个接口,这个接口之前每次加载都需要40s左右,经过优化将性能提了10倍左右;又加了缓存直接接口响应目前为300ms左右,于是将自己的优化思路整理总结一下