来公司实习已有一个多月的时间了,也碰到了不少的问题,虽然都比较浅显,但感觉还是有必要记下来的。
图片的加载一直是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就好了呢?答案是不行的,图片依旧显示不出来,并会报下面这条警告:
解决方法很简单,将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