android picasso 显示本地图片,剖析Picasso加载压缩本地图片流程(解决Android 5.0部分机型无法加载本地图片的问题)...

博客介绍了在Android5.0以上版本使用Picasso加载本地图片时遇到的问题,特别是在使用resize方法时,部分手机会加载失败。问题源于BitmapHunter类中的decodeStream方法,当offset超过limit时,reset方法会抛出异常。解决方案是在2.5.2.4b版本的Picasso中,MarkableInputStream的read方法增加了limit处理,避免了offset超出limit的情况。此外,此版本还自动处理了“file”协议,简化了本地图片的加载方式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

之前项目中使用Picasso遇到了一个问题:在Android 5.0以上版本的部分手机上使用Picasso加载本地图片会失败。为了解决这个问题,研究了一下Picasso加载和压缩本地图片的流程,才有了这篇文章。

我们知道,Picasso加载本地图片有两种方法,一种是new File(path),另外一种是url = "file://" + path。尤其后一种在picasso2.5.2及之前版本一定要加"file"协议,否则加载图片出错。

对于上面提到的问题,只出现在使用Picasso加载本地图片并且使用了resize方法的时候。

而且发现只存在Android 5.0以上版本的部分手机上。比如我们发现5.0、5.0.1有问题,5.1.1没有问题;同时有的5.0的手机没问题,而有的会出现这个问题。

由于有时本地照片较大,所以虽然可以不使用resize方法来保证图片正常加载,但是这样内存开销会急剧增加,所以我们不可避免的要解决这个问题。

经研究发现问题出现在picasso的BitmapHunter类中,当得到读取了网络或本地图片后,会调用BitmapHunter的decodeStream来进行处理,该方法代码如下:

static Bitmap decodeStream(InputStream stream, Request request) throws IOException {

MarkableInputStream markStream = new MarkableInputStream(stream);

stream = markStream;

long mark = markStream.savePosition(65536); // TODO fix this crap. (1)

final BitmapFactory.Options options = RequestHandler.createBitmapOptions(request);

final boolean calculateSize = RequestHandler.requiresInSampleSize (options );

boolean isWebPFile = Utils.isWebPFile( stream);

boolean isPurgeable = request .purgeable && android.os.Build.VERSION.SDK_INT < 21;

markStream.reset( mark);

// We decode from a byte array because, a) when decoding a WebP network stream, BitmapFactory

// throws a JNI Exception, so we workaround by decoding a byte array, or b) user requested

// purgeable, which only affects bitmaps decoded from byte arrays.

if (isWebPFile || isPurgeable) { (2)

byte[] bytes = Utils. toByteArray(stream);

if (calculateSize) {

BitmapFactory. decodeByteArray(bytes, 0, bytes.length, options);

RequestHandler. calculateInSampleSize(request.targetWidth, request.targetHeight , options ,

request);

}

return BitmapFactory. decodeByteArray(bytes, 0, bytes.length, options);

} else {

if (calculateSize) { (3)

BitmapFactory. decodeStream(stream, null, options); (4)

RequestHandler. calculateInSampleSize(request.targetWidth, request.targetHeight , options ,

request);

markStream.reset(mark ); (5)

}

Bitmap bitmap = BitmapFactory. decodeStream(stream, null, options);

if (bitmap == null) {

// Treat null as an IO exception, we will eventually retry.

throw new IOException("Failed to decode stream.");

}

return bitmap;

}

}

这里有一个MarkableInputStream是继承InputStream的,复写了其中的几个方法,其中部分代码如下:

public long savePosition(int readLimit) {

long offsetLimit = offset + readLimit ;

if (limit < offsetLimit) {

setLimit( offsetLimit);

}

return offset;

}

private void setLimit( long limit ) {

try {

if (reset < offset && offset <= this.limit ) {

in.reset();

in.mark(( int) (limit - reset ));

skip( reset, offset);

} else {

reset = offset;

in.mark(( int) (limit - offset ));

}

this. limit = limit;

} catch (IOException e) {

throw new IllegalStateException( "Unable to mark: " + e );

}

}

public void reset(long token ) throws IOException {

if (offset > limit || token < reset) {

throw new IOException("Cannot reset" );

}

in.reset();

skip(reset, token);

offset = token;

}

@Override public int read() throws IOException {

int result = in.read();

if (result != -1) {

offset++;

}

return result;

}

@Override public int read(byte [] buffer ) throws IOException {

int count = in.read(buffer);

if (count != -1) {

offset += count;

}

return count;

}

@Override public int read(byte [] buffer , int offset, int length) throws IOException {

int count = in.read(buffer, offset, length);

if (count != -1) {

this. offset += count;

}

return count;

}

从上面的代码可以看出,MarkableInputStream在read的时候会动态改变offset的值。

我们回到decodeStream()函数,代码(1)为markStream设定了一个值,经过MarkableInputStream的savePosition()和setLimit()函数,MarkableInputStream的类变量limit被赋值65536。代码(4)这里有读取操作,所以MarkableInputStream的类变量offset会改变,而代码(5)则调用MarkableInputStream的reset()函数,这个方法中先比较offset和limit,如果offset比limit大会抛出错误,加载过程就停止了,加载出错。

问题就出现在这里:

1、在正常的手机上,代码(4)执行完毕,由于options的inJustDecodeBounds为true,所以只读取图片的信息部分,offset这个变量的值也没有很大,比65536小,所以代码(5)的reset方法会正常执行,会正常加载本地图片。

2、但是在部分手机上,代码(4)执行完毕,offset这个变量的值远远比65536大,所以reset方法会抛出异常,加载出错,显示error图片。

这样我们就得出了结论:

在部分手机上,BitmapFactory. decodeStream(stream, null, options);这个方法的实现可能有差别,导致了问题的出现。

同时我们在代码(2)处可以看到,如果是本地图片而且是5.0以上版本,才走else流程,既有问题的代码。而且在代码(3)处则判断是否压缩,如果压缩才会走代码(4)到(5),否则不走这部分,就不会出错。这就解释了这个问题为什么会有如此出现机制。

最简单的解决方法:

使用it.sephiroth.android.library.picasso:picasso:2.5.2.4b这个版本,这个版本修复了这个bug。(注意Picasso官方版本一直停留在2.5.2这个版本,但是这个版本有几个问题,所以尽量使用2.5.2.4b这个可能是非官方维护的版本)

那么这个问题到底如何解决的?我们来看看2.5.2.4b的源码。

主要的处理方法是MarkableInputStream的每个read方法中添加一个limit的处理,如下:

public int read() throws IOException {

if (!this.allowExpire && this.offset + 1L > this.limit) {

this .setLimit(this.limit + ( long)this .limitIncrement);

}

int result = this.in.read() ;

if(result != - 1) {

++this .offset;

}

return result;

}

public int read(byte [] buffer) throws IOException {

if (!this.allowExpire && this.offset + (long )buffer.length > this.limit) {

this .setLimit(this.offset + ( long)buffer.length + (long)this .limitIncrement);

}

int count = this.in.read(buffer) ;

if(count != - 1) {

this .offset += (long)count ;

}

return count;

}

public int read(byte [] buffer, int offset , int length) throws IOException {

if (!this.allowExpire && this.offset + (long )length > this.limit) {

this .setLimit(this.offset + ( long)length + (long)this .limitIncrement);

}

int count = this.in.read(buffer , offset, length);

if(count != - 1) {

this .offset += (long)count ;

}

return count;

}

可以看见,在每个read方法开始都会坐下判断并重新为limit赋值,这样limit就不是65536这样的固定值了。也保证了正常情况下offset比limit小,不会再reset方法中抛出错误了。

而且这个版本可以自动添加"file"协议,所以本地图片使用url方式的时候,不必使用"file://"+ path这种形式,直接使用path即可。2.5.2.4b处理的方法如下:

RequestCreator(Picasso picasso , Uri uri, int resourceId) {

if (picasso.shutdown) {

throw new IllegalStateException("Picasso instance already shut down. Cannot submit new requests.") ;

} else {

if (null != uri) {

String scheme = uri.getScheme();

if( null == scheme) {

uri = Uri.fromFile(new File(uri.getPath())) ;

}

}

this .picasso = picasso;

this.data = new Builder(uri, resourceId, picasso.defaultBitmapConfig) ;

this.data.setCache(picasso.getCache()) ;

}

}

可以看到,如果uri没有协议,则自动添加"file"协议。

本篇内容就这样了,在picasso 2.5.2版本还存在一个问题:在Android 5.0以下版本加载https图片出错,如果你遇到了这个问题,请阅读解决Picasso在Android 5.0以下版本不兼容https导致图片不显示这篇文章。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值