Android中加载图片的常用技巧及问题总结

来公司实习已有一个多月的时间了,也碰到了不少的问题,虽然都比较浅显,但感觉还是有必要记下来的。
图片的加载一直是Android的一个痛处,做的不好总是会出现各种问题,所以这一直是一个热门话题。

  • 图片读取:

google官方文档介绍,2.3版本之前Android为每个APP分配的内存只有16MB,2.3之后也只提高到了64MB,可见内存空间还是很宝贵的,如果加载一张1080*960的图片,根据公式height*width*quality,需要大约1MB的空间,如果大量加载这种图片会很容易抛出OOM异常,而且手机屏幕的分辨率也是有限的,加载这么大的图片也是浪费,所以这里我们的一般策略是读取压缩剪裁后的图片,通过BitmapFactory.options类可以很容易的达到我们想要的效果

public static Bitmap extractThumbNailFromStream(InputStream stream, int width, int height, boolean crop) {

        //……

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

        try {
            options.inJustDecodeBounds = true;
            //……
            // NOTE: out of memory error
            while (options.outHeight * options.outWidth / options.inSampleSize / options.inSampleSize > MAX_DECODE_PICTURE_SIZE) {
                options.inSampleSize++;
            }

            int newHeight = height;
            int newWidth = width;
            if (crop) {
                if (beY > beX) {
                    newHeight = (int) Math.ceil(newWidth * 1.0 * options.outHeight / options.outWidth);
                } else {
                    newWidth = (int) Math.ceil(newHeight * 1.0 * options.outWidth / options.outHeight);
                }
            } else {
                if (beY < beX) {
                    newHeight = (int) Math.ceil(newWidth * 1.0 * options.outHeight / options.outWidth);
                } else {
                    newWidth = (int) Math.ceil(newHeight * 1.0 * options.outWidth / options.outHeight);
                }
            }

            newHeight = newHeight > 0 ? newHeight : 1;
            newWidth = newWidth > 0 ? newWidth : 1;

            options.inJustDecodeBounds = false;

            // if not set true, setPixels method may throw
            // IllegalStateException
            if (android.os.Build.VERSION.SDK_INT >= ANDROID_API_LEVEL_11) {
                options.inMutable = true;
            }

            Log.i(TAG, "bitmap required size=" + newWidth + "x" + newHeight + ", orig=" + options.outWidth + "x" + options.outHeight + ", sample=" + options.inSampleSize);
            bindlowMemeryOption(options);
            Bitmap bm = BitmapFactory.decodeStream(stream, null, options);
            if (bm == null) {
                Log.e(TAG, "bitmap decode failed");
                return null;
            }

            Log.d(TAG, "bitmap decoded size=" + bm.getWidth() + "x" + bm.getHeight());
            final Bitmap scale = Bitmap.createScaledBitmap(bm, newWidth, newHeight, true);
            if (bm != scale && scale != null) {
                Log.i(TAG, "extractThumbNail bitmap recycle adsfad." + bm.toString());
                bm.recycle();
                bm = scale;
            }

            //……

        } catch (final OutOfMemoryError e) {
            Log.e(TAG, "decode bitmap failed: " + e.getMessage());
            options = null;
        } catch (IOException e) {
            Log.printErrStackTrace(TAG, e, "Failed decode bitmap");
        }

        return null;
    }

上面是一段图片压缩处理的读取方法,参数crop是剪裁标志,inMutable表示生成的bitmap是否可以调用setPixels改变像素,inSampleSize表示图片压缩的比例,通过循环判断图片压缩后是否依然大于最大解码值来调整inSampleSize的值,还有就是读取时尽量用BitmapFactory.decodeStream()方法,该方法直接调用JNI>>nativeDecodeAsset()来完成decode,无需再使用java层的createBitmap,从而节省了java层的空间,而decodeResource()方法最后是通过java层的createBitmap来完成的,需要消耗更多内存;不过decodeStream()在2.3之前有bug会抛OOM,如果APP需要兼容2.3,应注意加判断。

  • 缓存策略:

目前的图片框架基本都是采用二级缓存,磁盘缓存+内存缓存,以前的内存缓存通常采用Map

<application android:hardwareAccelerated="false" ...>
or
View.setLayerType(View.LAYER_TYPE_SOFTWARE,null);

2.将图片拆分成多个小图加载。

//BitmapRegionDecoder类
public Bitmap decodeRegion (Rect rect, BitmapFactory.Options options)

通过这个方法可以加载图片的任意矩形区域,再放入多个ImageView中合并展示。

  • 难点问题:

最近遇到了一个有趣的现象,在listview中嵌套imageview显示图片,如果图片过长这时会报上面的渲染尺寸超限错误,但是否将imageview的layerType设置成soft就好了呢?答案是不行的,图片依旧显示不出来,并会报下面这条警告:
softcache
解决方法很简单,将scrollView的layerType设置成soft,图片就正常显示了(imageView的layerType设成什么都无所谓)。
下面来简单分析下这个问题的原因:
先继承scrollView和ImageView,重写他们的draw,dispatchDraw,drawChild和buildDrawingCache等方法,输出log,打印堆栈。
先看一下相关方法的关键代码:

    public void buildDrawingCache(boolean autoScale) {
            //……
            final boolean use32BitCache = attachInfo != null && attachInfo.mUse32BitDrawingCache;

            final long projectedBitmapSize = width * height * (opaque && !use32BitCache ? 2 : 4);
            final long drawingCacheSize =
                    ViewConfiguration.get(mContext).getScaledMaximumDrawingCacheSize();
            if (width <= 0 || height <= 0 || projectedBitmapSize > drawingCacheSize) {
                if (width > 0 && height > 0) {
                    Log.w(VIEW_LOG_TAG, "View too large to fit into drawing cache, needs "
                            + projectedBitmapSize + " bytes, only "
                            + drawingCacheSize + " available");
                }
                destroyDrawingCache();
                mCachingFailed = true;
                return;
            }

            //……
            Canvas canvas;
            if (attachInfo != null) {
                canvas = attachInfo.mCanvas;
                if (canvas == null) {
                    canvas = new Canvas();
                }
                canvas.setBitmap(bitmap);
                // Temporarily clobber the cached Canvas in case one of our children
                // is also using a drawing cache. Without this, the children would
                // steal the canvas by attaching their own bitmap to it and bad, bad
                // thing would happen (invisible views, corrupted drawings, etc.)
                attachInfo.mCanvas = null;
            } else {
                // This case should hopefully never or seldom happen
                canvas = new Canvas(bitmap);
            }

            //……
        }
    }
private DisplayList getDisplayList(DisplayList displayList, boolean isLayer) {
        if (!canHaveDisplayList()) {
            return null;
        }

        //……

            final HardwareCanvas canvas = displayList.start(width, height);

            try {
                if (!isLayer && layerType != LAYER_TYPE_NONE) {
                    if (layerType == LAYER_TYPE_HARDWARE) {
                        final HardwareLayer layer = getHardwareLayer();
                        if (layer != null && layer.isValid()) {
                            canvas.drawHardwareLayer(layer, 0, 0, mLayerPaint);
                        } else {
                            canvas.saveLayer(0, 0, mRight - mLeft, mBottom - mTop, mLayerPaint,
                                    Canvas.HAS_ALPHA_LAYER_SAVE_FLAG |
                                            Canvas.CLIP_TO_LAYER_SAVE_FLAG);
                        }
                        caching = true;
                    } else {
                        buildDrawingCache(true);
                        Bitmap cache = getDrawingCache(true);
                        if (cache != null) {
                            canvas.drawBitmap(cache, 0, 0, mLayerPaint);
                            caching = true;
                        }
                    }
                } else {

                    //……
            } finally {
                displayList.end();
                displayList.setCaching(caching);
                if (isLayer) {
                    displayList.setLeftTopRightBottom(0, 0, width, height);
                } else {
                    setDisplayListProperties(displayList);
                }
            }
        } else if (!isLayer) {
            mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
        }

        return displayList;
    }
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
        boolean useDisplayListProperties = mAttachInfo != null && mAttachInfo.mHardwareAccelerated;
        boolean more = false;
        final boolean childHasIdentityMatrix = hasIdentityMatrix();
        final int flags = parent.mGroupFlags;

        //……
        DisplayList displayList = null;
        Bitmap cache = null;
        boolean hasDisplayList = false;
        if (caching) {
            if (!hardwareAccelerated) {
                if (layerType != LAYER_TYPE_NONE) {
                    layerType = LAYER_TYPE_SOFTWARE;
                    buildDrawingCache(true);
                }
                cache = getDrawingCache(true);
            } else {
                switch (layerType) {
                    case LAYER_TYPE_SOFTWARE:
                        if (useDisplayListProperties) {
                            hasDisplayList = canHaveDisplayList();
                        } else {
                            buildDrawingCache(true);
                            cache = getDrawingCache(true);
                        }
                        break;
                    case LAYER_TYPE_HARDWARE:
                        if (useDisplayListProperties) {
                            hasDisplayList = canHaveDisplayList();
                        }
                        break;
                    case LAYER_TYPE_NONE:
                        // Delay getting the display list until animation-driven alpha values are
                        // set up and possibly passed on to the view
                        hasDisplayList = canHaveDisplayList();
                        break;
                }
            }
        }
       //……

            if (!layerRendered) {
                if (!hasDisplayList) {
                    // Fast path for layouts with no backgrounds
                    if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                        mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                        dispatchDraw(canvas);
                    } else {
                        draw(canvas);
                    }
                } else {
                    mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                    ((HardwareCanvas) canvas).drawDisplayList(displayList, null, flags);
                }
            }
        } 
       //……
        return more;
    }

第一种情况,scrollView默认不设置,imageView设置成soft,图片不能正常显示。(三个参数的draw简称为draw3)
关键log:
这里写图片描述
此时canvas为RecordingCanvas,硬件加速为开启。
方法调用时序图:
这里写图片描述
警告是在buildDrawingCache中报的,由于cache创建失败,displayList中没有存入imageView的渲染信息,HardwareCanvas.drawDisplayList无法绘制出图片。
第二种情况,scrollView设置成soft,imageView不设置,图片正常显示。
关键log:
这里写图片描述

这里写图片描述
注意canvas的变化,我们再看一下调用时序:
这里写图片描述
在ScrollView执行buildDrawingCache的时候,将canvas换成了普通的canvas并向下传递给子节点,这样子节点在进行绘制时都不会走硬件加速的流程。
总结一下,控制一个view是否走硬件加速,最可靠的办法是控制其父节点,如果父节点传的是HardwareCanvas,则子view走的就是硬件加速的流程,此时设置soft与hard的区别则是在于绘制是在哪层进行的。其实以上两个情况都是显示大图时的特殊情况,有兴趣的同学可以再对比一下正常情况下view的绘制过程,就会比较清晰了。关于硬件加速的相关知识可以看下面的参考文章,写的很详细。
最后还要感谢zeus同学的全程指导和帮助,要不然也搞不定这个问题。

参考文章:
http://zhiweiofli.iteye.com/blog/905066
http://blog.csdn.net/ta893115871/article/details/9043559
http://stackoverflow.com/questions/7428996/hw-accelerated-activity-how-to-get-opengl-texture-size-limit#
http://blog.csdn.net/goohong/article/details/7836564
http://km.oa.com/group/22595/articles/show/223947?kmref=search

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值