Android图像与动画<1>

作者: 凉凉菇凉 | 来源:发表于2017-03-02 18:04 被阅读47次

    android项目中为了界面的展示和效果,不可避免的用到图片和动画,所以我会分三个模块来讲解自己所知道的图像处理和各种的动画视觉显示,学习中,虚心接受大神们的建议。

    本文中主要讲解如何对大图片进行压缩避免OOM(OutOfMemory)异常,图片加载到内存中占多大内存的问题,如何避免UI线程阻塞。如果我们不注意这些很容易导致图片占用大量的可用内存导致程序崩溃。出现下面的异常: java.lang.OutofMemoryError: bitmap size exceeds VM budget.

    为什么要处理Bitmap

    简单的来说就是三点:
    1.手机设备内存有限,Android设备对于单个程序至少需要16MB的内存,所以要尽量优化程序的内存,提高效率。
    2.Bitmap特别消耗内存,特别是图片丰富的程序。
    3.有时候需要一次性加载多张图片,例如在ListView, GridViewViewPager 等控件中.需要预加载一些图片达到用户滑动时体验顺畅的效果。

    获取大图像的尺寸

    为了减少内存的利用,我们在屏幕中展示图片的时候不需要那么高的分辨率,只需要根据控件的大小压缩展示。BitmapFactory提供了一些解码(decode)的方法(decodeByteArray, decodeFile, decodeResource等),用来创建一个Bitmap对象。们应该根据图片的来源选择合适的方法。比如SD卡中的图片可以使用decodeFile方法,网络上的图片可以使用decodeStream方法,资源文件中的图片可使用decodeResource方法。让我们可以在加载图片之前就获取到图片的长宽值和MIME类型,从而根据情况对图片进行压缩。

    <small>注意:
    一定要设置BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true;因为decode方法构建bitmap分配内存,很容易导致OOM异常。只需要知道图片大小的情形下,可以不完整加载图片到内存.这时候就需要引用.为此每一种解析方法都提供了一个可选BitmapFactory.Options参数,将这个参数的inJustDecodeBounds属性设置为true就可以让解析方法禁止为bitmap分配内存,返回值也不再是一个Bitmap对象,而是null。虽然Bitmap是null了,但是BitmapFactory.Options的outWidth、outHeight和outMimeType属性都会被赋值。</small>

    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
    int imageHeight = options.outHeight;
    int imageWidth = options.outWidth;
    String imageType = options.outMimeType;```
    
    **按照比例压缩图片**
    
    已经知道图片的尺寸,可以通过设置BitmapFactory.Options中inSampleSize的值来按照比例进行压缩,减少占用的内存。下面代码计算出适当的压缩比例。
    

    /*凉菇凉
    reqWidth reqHeight目标宽高
    */
    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连同期望的宽度和高度一起传递到到calculateInSampleSize方法中,就可以得到合适的inSampleSize值了。之后再解析一次图片,使用新获取到的inSampleSize值,并把inJustDecodeBounds设置为false,就可以得到压缩后的图片了。

    public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
            int reqWidth, int reqHeight) {
    
        // 将BitmapFactory.Options的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值,并把inJustDecodeBounds设置为false
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resId, options);
    }```
    
    压缩图片像素是100*100的缩略图
    

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

    本篇主要讲解一些压缩图片的方法,以后会更新从本地图库中读取所有的图片裁剪压缩并且显示在imagview控件中的实例。

    Bitmap缓存处理

    android中处理图片的基础类是Bitmap,顾名思义,就是位图。占用内存的算法如:图片的widthheightConfig。 如果Config设置为ARGB_8888,那么上面的Config就是4。一张480320的图片占用的内存就是480320*4 byte。如果单个bitmap加载到imagview控件中挺简单的,但是如果需要一次性的加载大量的图片,为了避免OOM需要考虑两个问题,内存回收的问题和图片缓存的问题。

    1.内存问题:为了保证内存的使用始终维持在一个合理的范围,通常会把被移除屏幕的图片进行回收处理。此时垃圾回收器也会认为你不再持有这些图片的引用,从而对这些图片进行GC操作。

    2.图片缓存问题:大量的图片加载到listview 、gridview、viewpager这样的控件中,需要在滑动的时候界面上快速展示图片,保证流畅的用户体验,为了避免重复处理相同的图片,需要用到内存缓存机制。可以使用LruCache或者磁盘缓存来处理这个问题,快速的加载已经处理过得图片。

    Note: 在过去,一种比较流行的内存缓存实现方法是使用软引用(SoftReference)或弱引用(WeakReference)对Bitmap进行缓存,然而我们并不推荐这样的做法。从Android 2.3 (API Level 9)开始,垃圾回收机制变得更加频繁,这使得释放软(弱)引用的频率也随之增高,导致使用引用的效率降低很多。而且在Android 3.0 (API Level 11)之前,备份的Bitmap会存放在Native Memory中,它不是以可预知的方式被释放的,这样可能导致程序超出它的内存限制而崩溃。

    LruCache缓存

    LruCache类(在API Level 4的Support Library中也可以找到)特别适合用来缓存Bitmaps,它使用一个强引用(strong referenced)的LinkedHashMap保存最近引用的对象,并且在缓存超出设置大小的时候剔除最近最少使用到的对象。
    当加载Bitmap显示到ImageView 之前,会先从LruCache 中检查是否存在这个Bitmap。如果确实存在,它会立即被用来显示到ImageView上,如果没有找到,会触发一个后台线程去处理显示该Bitmap任务。

    
    
    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);  
    }```
    
    注意:有1/8的内存空间被用作缓存。 这意味着在常见的设备上(hdpi),最少大概有4MB的缓存空间(32/8)。如果一个填满图片的GridView控件放置在800x480像素的手机屏幕上,大概会花费1.5MB的缓存空间(800x480x4 bytes),因此缓存的容量大概可以缓存2.5页的图片内容。
    
    **磁盘缓存**
    如果listview或者gridview中的图片量太多,使用内存缓存还是会报内存溢出的问题,或者状态发生改变的时候比如打电话的时候导致暂停并退出后台,那么内存缓存也会被清除,恢复应用的时候就会重新处理这些图片。所以考虑磁盘缓存,还可以减少那些不再内存缓存中的Bitmap的加载次数,必须在后台进行缓存,因为磁盘缓存读取图片的信息比内存缓存要慢。
    > **Note:**如果图片会被更频繁的访问,使用[ContentProvider](http://developer.android.com/reference/android/content/ContentProvider.html)或许会更加合适,比如在图库应用中。
    
    这是我直接复制的别人写从[Android源码](https://android.googlesource.com/platform/libcore/+/jb-mr2-release/luni/src/main/java/libcore/io/DiskLruCache.java)中剥离出来的DiskLruCache。非Google官方编写,但获得官方认证。Android Doc中并没有对DiskLruCache的用法给出详细的说明,但是平时咱们熟悉的网易新闻里面的图片缓存就用到了DiskLruCache缓存。有兴趣的可以详细去了解一下,[下载源码地址](http://download.csdn.net/detail/qq_31927865/9768201)ps:磁盘缓存都是在I/O进程中
    
    

    private DiskLruCache mDiskLruCache;
    private final Object mDiskCacheLock = new Object();
    private boolean mDiskCacheStarting = true;
    private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
    private static final String DISK_CACHE_SUBDIR = "thumbnails";

    @Override
    protected void onCreate(Bundle savedInstanceState) {

    //初始化磁盘缓存DiskCacheDir
    File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR);
    new InitDiskCacheTask().execute(cacheDir);
    ...
    

    }

    class InitDiskCacheTask extends AsyncTask<File, Void, Void> {
    @Override
    protected Void doInBackground(File... params) {
    synchronized (mDiskCacheLock) {
    File cacheDir = params[0];
    mDiskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);
    mDiskCacheStarting = false; // 完成初始化
    mDiskCacheLock.notifyAll(); // 等待线程
    }
    return null;
    }
    }

    class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    ...
    // 在后台缓存处理图片
    @Override
    protected Bitmap doInBackground(Integer... params) {
    final String imageKey = String.valueOf(params[0]);

        // Check disk cache in background thread
        Bitmap bitmap = getBitmapFromDiskCache(imageKey);
    
        if (bitmap == null) { //图片在磁盘中没有找到
            // Process as normal
            final Bitmap bitmap = decodeSampledBitmapFromResource(
                    getResources(), params[0], 100, 100));
        }
    
        // 添加到磁盘中
        addBitmapToCache(imageKey, bitmap);
    
        return bitmap;
    }
    ...
    

    }

    public void addBitmapToCache(String key, Bitmap bitmap) {
    // 先添加到内存缓存中
    if (getBitmapFromMemCache(key) == null) {
    mMemoryCache.put(key, bitmap);
    }

    // 再添加到磁盘中
    synchronized (mDiskCacheLock) {
        if (mDiskLruCache != null && mDiskLruCache.get(key) == null) {
            mDiskLruCache.put(key, bitmap);
        }
    }
    

    }

    //
    public Bitmap getBitmapFromDiskCache(String key) {
    synchronized (mDiskCacheLock) {
    // Wait while disk cache is started from background thread
    while (mDiskCacheStarting) {
    try {
    mDiskCacheLock.wait();
    } catch (InterruptedException e) {}
    }
    if (mDiskLruCache != null) {
    return mDiskLruCache.get(key);
    }
    }
    return null;
    }

    // 创建一个独特的指定应用程序缓存目录的子目录。尝试使用外部
    //如果没有安装,内部存储器。
    public static File getDiskCacheDir(Context context, String uniqueName) {
    // Check if media is mounted or storage is built-in, if so, try and use external cache dir
    // otherwise use internal cache dir
    final String cachePath =
    Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
    !isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
    context.getCacheDir().getPath();

    return new File(cachePath + File.separator + uniqueName);
    

    }```

    注意:DiskLruCache缓存地址前面通常都会存放在 /sdcard/Android/data/<application package>/cache 这个路径下面,但同时我们又需要考虑如果这个手机没有SD卡,或者SD正好被移除了的情况,因此比较优秀的程序都会专门写一个方法来获取缓存地址,获取到的是 /data/data/<application package>/cache 这个路径。

    Bitmap回收机制
    通常Activity或者Fragment在onStop/onDestroy时候就可以释放图片资源:在释放资源时,需要注意释放的Bitmap或者相关的Drawable是否有被其它类引用。如果正常的调用,可以通过Bitmap.isRecycled()方法来判断是否有被标记回收;而如果是被UI线程的界面相关代码使用,就需要特别小心避免回收有可能被使用的资源,不然有可能抛出系统异常: E/AndroidRuntime: java.lang.IllegalArgumentException: Cannot draw recycled bitmaps 并且该异常无法有效捕捉并处理。

     if(imageView !=  null &&  imageView.getDrawable() != null){     
    
          Bitmap oldBitmap =  ((BitmapDrawable) imageView.getDrawable()).getBitmap();    
    
           imageView.setImageDrawable(null);    
    
          if(oldBitmap !=  null){    
    
                oldBitmap.recycle();     
    
                oldBitmap =  null;   
    
          }    
    
     }   
    
     //  Other code.
    
     System.gc();```
    
    **优化Dalvik虚拟机的堆内存分配 **
    对于[Android](http://lib.csdn.net/base/android)平台来说,其托管层使用的Dalvik [Java ](http://lib.csdn.net/base/java)VM从目前的表现来看还有很多地方可以优化处理,比如我们在开发一些大型游戏或耗资源的应用中可能考虑手动干涉GC处理,使用 dalvik.system.VMRuntime类提供的setTargetHeapUtilization方法可以增强程序堆内存的处理效率。
    当然具体原理我们可以参考开源工程,这里我们仅说下使用方法:  
    

    private final static float TARGET_HEAP_UTILIZATION = 0.75f;

    // 在程序onCreate时就可以调用

    VMRuntime.getRuntime().setTargetHeapUtilization(TARGET_HEAP_UTILIZATION);```

    不过要注意的是,VMRuntime这个类在2.2以上的版本已经去掉了,可能是因为要规范统一管理内存的原因吧,需要更多地从优化程序本身出发

    Manifest中处理

    在Manifest.xml文件里面的<application 里面添加Android:largeHeap="true"
    简单粗暴。这种方法允许应用需要耗费手机很多的内存空间,但却是最快捷的解决办法

    写了这么多需要对大家有所帮助!!!
    这篇文章参考于http://hukai.me/android-training-course-in-chinese/graphics/displaying-bitmaps/index.html

    相关文章

      网友评论

        本文标题:Android图像与动画<1>

        本文链接:https://www.haomeiwen.com/subject/puaqgttx.html