转载请注明原文地址:http://blog.csdn.net/mxm691292118/article/details/51028953

我把Android重难点和读书笔记都整理在github上:https://github.com/miomin/AndroidDifficulty

如果你觉得对你有帮助的话,希望可以star/follow一下哟,我会持续保持更新。


写在前面:笔者的上一篇博文有提到过,如果不恰当的使用Bitmap,很容易造成OOM。这篇博文就来谈谈应该如何正确的管理Bitmap的内存,以及优化策略。

参考: Google官方教程 – 《Android Training Ccourse》


一、加载按显示需要的比例缩小的图片

1、先来说说屏幕密度

  • 在Android中,Imageview控件的长宽单位一般设置为dp/dip,而不用px。这样做的原因,是因为dp/dip与屏幕像素密度无关,px与屏幕密度有关。在Android中,规定以160dpi为基准,1dip=1px,如果密度是320dpi,则1dip=2px。(所以,同一个imageview,在不同的设备上所显示的长宽的像素是不同的,我们需要根据像素不同,来按比例压缩大图)

  • 如果把一个大小为1024x1024像素的图片显示到大小为512x512像素的ImageView上吗,就没有必要加载整张原图到Bitmap中。

  • 为了告诉解码器去加载一个缩小比例是多少的图片到内存中,需要在BitmapFactory.Options 中设置 inSampleSize 的值。例如, 一个分辨率为2048x2048的图片,如果设置 inSampleSize 为4,那么会产出一个大约512x512大小的Bitmap。

  • 下面的代码是动态获取一个ImageView的长宽像素:
    注意:返回的是像素值(px),而不是dp/dip

    int reqHeight = iv.getLayoutParams().height;    int reqWidth = iv.getLayoutParams().width;

2、压缩图片

  • 一般来说,加载本地资源图片才需要压缩,加载网络图片,应该让服务器按需压缩,一方面节省流量,另一方面提高加载流畅度
  • 可以通过下面的代码计算inSampleSize的值,就是需要压缩的倍数:
//reqWidth和reqHeight是需要显示的Imageview的width和heightpublic static int calculateInSampleSize(            BitmapFactory.Options options, int reqWidth, int reqHeight) {    // height和width图片长宽的像素    final int height = options.outHeight;    final int width = options.outWidth;    int inSampleSize = 1;    if (height > reqHeight || width > reqWidth) {        final int halfHeight = height / 2;        final int halfWidth = width / 2;        while ((halfHeight / inSampleSize) > reqHeight                && (halfWidth / inSampleSize) > reqWidth) {            inSampleSize *= 2;        }    }    return inSampleSize;}
  • 通过下面的方法可以获得压缩后的Bitmap:
    注意:可以通过设置 inJustDecodeBounds 属性为true可以在解码的时候避免内存的分配,它会返回一个null的Bitmap,但是可以获取到 outWidth, outHeight 与 outMimeType。确定好压缩比例后,再将inJustDecodeBounds设置为false。
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,        int reqWidth, int reqHeight) {    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);}

二、Bitmap缓存

1、缓存类LruCache介绍

  • 在Android3.0之后,一般使用LruCache来缓存Bitmap,它使用一个强引用的LinkedHashMap保存最近引用的对象,并且在缓存超出设置大小的时候剔除(evict)最少使用到的对象。
  • LinkedHashMap会根据LRU算法来排列对象的顺序,新加入的对象添加到头部,刚被使用过的对象也被移动到头部,所以在链表尾部的对象是最久没有被使用过的,一旦链表满了,有新对象加入,就会删除链表尾部的对象。

2、如何给LruCache设置一个合适的大小?

  • LruCache太大的话,容易造成OOM或者分配给应用的剩余内存不够用,LruCache大小的设置,应该考虑下面的因素:
    • (1)应用允许的最大内存是多少?剩下了多少可用的内存?
    • (2)多少张图片会同时显示在屏幕上?
    • (3)设备的屏幕密度是多少?显示图片的像素是多少?
    • (4)可以根据访问频率给Bitmap分组,为不同的Bitmap组设置不同大小的LruCache对象。

3、一个LruCache使用的完整例子

  • 代码参考了一个大神的代码,并修真了一些写的不对和不好的地方。
public class PhotoAdapter extends BaseAdapter implements AbsListView.OnScrollListener {    // 从network下载图片的线程集合    private List mDownloadTaskList;    private LruCache mLruCache;    // 引用外部的变量    private WeakReference mGridView;    private WeakReference> urls;    private WeakReference mContext;    // 可见项的第一项的index    private int mFirstVisibleIndex;    // 可见项的个数    private int mVisibleItemCount;    // 是不是第一次打开Activity    private boolean isFirstOpen = true;    public PhotoAdapter(Context context, GridView mGridView, List urls) {        this.mContext = new WeakReference(context);        this.urls = new WeakReference>(urls);        this.mGridView = new WeakReference(mGridView);        this.mGridView.get().setOnScrollListener(this);        mDownloadTaskList = new ArrayList<>();        // 初始化图片缓存池        initCache();    }    private void initCache() {        // 获取应用的max heap size        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);        // Android官方教学文档推荐LruCache的size为heap size的1/8        int cacheSize = maxMemory / 8;        mLruCache = new LruCache(cacheSize) {            @Override            protected int sizeOf(String key, Bitmap bitmap) {                if (bitmap != null) {                    return bitmap.getByteCount() / 1024;                }                return 0;            }        };    }    @Override    public int getCount() {        return urls.get().size();    }    @Override    public Object getItem(int position) {        return urls.get().get(position);    }    @Override    public long getItemId(int position) {        return position;    }    @Override    public View getView(int position, View convertView, ViewGroup parent) {        viewHolder holder = null;        if (convertView == null) {            convertView = LayoutInflater.from(mContext.get()).inflate(R.layout.layout_item, parent, false);            holder = new viewHolder();            holder.mImageView = (ImageView) convertView.findViewById(R.id.imageView);            holder.mTextView = (TextView) convertView.findViewById(R.id.textView);            convertView.setTag(holder);        } else {            holder = (viewHolder) convertView.getTag();        }        String url = urls.get().get(position);        //imageview与url绑定,防止错乱显示        holder.mImageView.setTag(MD5Tools.decodeString(url));        holder.mTextView.setText("第" + position + "项");        if (!holder.mImageView.getTag().equals(url)) {            showImageView(holder.mImageView, url);        }        return convertView;    }    /**     * convertView复用     */    private class viewHolder {        ImageView mImageView;        TextView mTextView;    }    /**     * 给ImageView设置Bitmap     */    private void showImageView(ImageView imageView, String url) {        // 对url进行md5编码        String key = MD5Tools.decodeString(url);        // 先从cache中找bitmap缓存        Bitmap bitmap = get(key);        if (bitmap != null) {            // 如果缓存命中            imageView.setImageBitmap(bitmap);        } else {            // 如果cache miss            imageView.setBackgroundResource(R.color.color_five);        }    }    /**     * 将Bitmap put 到 cache中     */    private void put(String key, Bitmap bitmap) {        if (get(key) == null) {            mLruCache.put(key, bitmap);        }    }    /**     * 在Cache中查找bitmap,如果miss则返回null     */    private Bitmap get(String key) {        return mLruCache.get(key);    }    /**     * 从网络下载图片     */    private Bitmap loadBitmap(String urlStr) {        HttpURLConnection connection = null;        Bitmap bitmap = null;        try {            URL url = new URL(urlStr);            connection = (HttpURLConnection) url.openConnection();            connection.setConnectTimeout(5000);            connection.setReadTimeout(5000);            connection.setDoInput(true);            connection.connect();            if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {                InputStream mInputStream = connection.getInputStream();                bitmap = BitmapFactory.decodeStream(mInputStream);            }        } catch (MalformedURLException e) {            e.printStackTrace();        } catch (IOException e) {            e.printStackTrace();        } finally {            if (connection != null) {                connection.disconnect();            }        }        return bitmap;    }    /**     * 取消所有的下载任务     */    public void cancelAllTask() {        if (mDownloadTaskList != null) {            for (int i = 0; i < mDownloadTaskList.size(); i++) {                mDownloadTaskList.get(i).cancel(true);            }        }    }    /**     * 加载可见项的图片     */    private void loadVisibleBitmap(int mFirstVisibleItem, int mVisibleItemCount) {        for (int i = mFirstVisibleItem; i < mFirstVisibleItem + mVisibleItemCount; i++) {            final String url = urls.get().get(i);            String key = MD5Tools.decodeString(url);            Bitmap bitmap = get(key);            ImageView mImageView;            if (bitmap != null) {                //缓存中存在该图片的话就设置给ImageView                mImageView = (ImageView) mGridView.get().findViewWithTag(MD5Tools.decodeString(url));                if (mImageView != null) {                    mImageView.setImageBitmap(bitmap);                }            } else {                //不存在的话就开启一个异步线程去下载                ImageDownloadTask task = new ImageDownloadTask(this);                mDownloadTaskList.add(task);                task.execute(url);            }        }    }    /**     * 从网络下载图片的异步task     */    static class ImageDownloadTask extends AsyncTask {        private String url;        private WeakReference photoAdapter;        public ImageDownloadTask(PhotoAdapter photoAdapter) {            this.photoAdapter = new WeakReference(photoAdapter);        }        @Override        protected Bitmap doInBackground(String... params) {            //在后台开始下载图片            url = params[0];            Bitmap bitmap = photoAdapter.get().loadBitmap(url);            if (bitmap != null) {                //把下载好的图片放入LruCache中                String key = MD5Tools.decodeString(url);                photoAdapter.get().put(key, bitmap);            }            return bitmap;        }        @Override        protected void onPostExecute(Bitmap bitmap) {            super.onPostExecute(bitmap);            //把下载好的图片显示出来            ImageView mImageView = (ImageView) photoAdapter.get().mGridView.get().findViewWithTag(MD5Tools.decodeString(url));            if (mImageView != null && bitmap != null) {                mImageView.setImageBitmap(bitmap);                photoAdapter.get().mDownloadTaskList.remove(this);//把下载好的任务移除            }        }    }    /**     * 监听GridView的滑动状态     */    @Override    public void onScrollStateChanged(AbsListView view, int scrollState) {        //GridView停止滑动时,去加载可见项的图片        if (scrollState == SCROLL_STATE_IDLE) {            loadVisibleBitmap(mFirstVisibleIndex, mVisibleItemCount);        } else {            //GridView开始滑动时,取消所有加载任务            cancelAllTask();        }    }    /**     * 监听并更新GridView滑动过程中的可见项     */    @Override    public void onScroll(AbsListView view, int firstVisibleIndex, int visibleItemCount, int totalItemCount) {        mFirstVisibleIndex = firstVisibleIndex;        mVisibleItemCount = visibleItemCount;        // 第一次打开,加载可见项        if (isFirstOpen && visibleItemCount > 0) {            loadVisibleBitmap(mFirstVisibleIndex, mVisibleItemCount);            isFirstOpen = false;        }    }}

4、Andorid3.0之前的Bitmap管理方法(参考自xiaanming的博客)

  • 在Android3.0之前,Bitmap对象与数据是分开存储的,Bitmap对象存储在Java heap中,而像素数据存储在Native Memory中,Java虚拟机的垃圾回收机制不会主动回收Native Memory中的对象,需要在Bitmap不需要使用的时候,主动调用recycle()方法来释放,而在Android3.0之后,Bitmap的像素数据和Bitmap对象都存放在Java Heap中,所以不需要手动调用recycle()来释放,垃圾收集器会处理。
  • 应该在什么时候去调用recycle()方法呢?可以用引用计数算法,用一个变量来记录Bitmap显示情况,如果Bitmap绘制在View上面displayRefCount加一, 否则就减一, 只有在displayResCount为0且Bitmap不为空且Bitmap没有调用过recycle()的时候,才调用recycle(),下面用BitmapDrawable类来包装下Bitmap对象,代码如下:
public class RecycleBitmapDrawable extends BitmapDrawable {    private int displayResCount = 0;    private boolean mHasBeenDisplayed;    public RecycleBitmapDrawable(Resources res, Bitmap bitmap) {        super(res, bitmap);    }    /**     * @param isDisplay     */    public void setIsDisplayed(boolean isDisplay){        synchronized (this) {            if(isDisplay){                mHasBeenDisplayed = true;                displayResCount ++;            }else{                displayResCount --;            }        }        checkState();    }    /**     * 检查图片的一些状态,判断是否需要调用recycle     */    private synchronized void checkState() {        if (displayResCount <= 0 && mHasBeenDisplayed                && hasValidBitmap()) {            getBitmap().recycle();        }    }    /**     * 判断Bitmap是否为空且是否调用过recycle()     * @return     */    private synchronized boolean hasValidBitmap() {        Bitmap bitmap = getBitmap();        return bitmap != null && !bitmap.isRecycled();    }}
  • 还需要一个自定义的ImageView,重写了setImageDrawable()方法,在这个方法中我们先获取ImageView上面的图片,然后通知之前显示在ImageView的Drawable不在显示了,Drawable会判断是否需要调用recycle(),代码如下:
public class RecycleImageView extends ImageView {    public RecycleImageView(Context context) {          super(context);      }      public RecycleImageView(Context context, AttributeSet attrs) {          super(context, attrs);      }      public RecycleImageView(Context context, AttributeSet attrs, int defStyle) {          super(context, attrs, defStyle);      }      @Override      public void setImageDrawable(Drawable drawable) {          Drawable previousDrawable = getDrawable();          super.setImageDrawable(drawable);          //显示新的drawable          notifyDrawable(drawable, true);          //回收之前的图片          notifyDrawable(previousDrawable, false);      }      @Override      protected void onDetachedFromWindow() {          //当View从窗口脱离的时候,清除drawable          setImageDrawable(null);          super.onDetachedFromWindow();      }      /**      * 通知该drawable显示或者隐藏      *       * @param drawable      * @param isDisplayed      */      public static void notifyDrawable(Drawable drawable, boolean isDisplayed) {          if (drawable instanceof RecycleBitmapDrawable) {              ((RecycleBitmapDrawable) drawable).setIsDisplayed(isDisplayed);          } else if (drawable instanceof LayerDrawable) {              LayerDrawable layerDrawable = (LayerDrawable) drawable;              for (int i = 0, z = layerDrawable.getNumberOfLayers(); i < z; i++) {                  notifyDrawable(layerDrawable.getDrawable(i), isDisplayed);              }          }      }  }
  • 具体的使用方法如下
ImageView imageView = new ImageView(context);          imageView.setImageDrawable(new RecycleBitmapDrawable(context.getResource(), bitmap));

更多相关文章

  1. android NDK 入门之在JNI中修改java中对象的成员变量的值
  2. Android(安卓)- 用WebView开发简单的浏览器
  3. android调用手机摄像头拍照
  4. Android中传递对象的三种方法
  5. Android(安卓)高德地图,显示地图,并定位当前的位置
  6. Android(安卓)webView 中loadData方法加载 带中文时出现乱码
  7. 想在Android上用一下PureMVC,谁知结果……
  8. android上传图片过大处理
  9. Android中如何获取系统中所有的应用程序

随机推荐

  1. Android(安卓)H5混合开发 JS调用原生方法
  2. Android加阴影 -(一键无脑引入,无兼容问题)
  3. ImageView设置图片的几种方式
  4. 深入分析Android监听网络变化的坑
  5. Android(安卓)studio 打包自定义apk名称
  6. 去掉rk3288 android6.0 默认锁屏界面
  7. ZH奶酪:Ubuntu14.04 安装Android(安卓)SDK
  8. Activity全解(一)
  9. 【Android】支持https接口调用的简单证书
  10. Android实现简单的分页效果