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

之前项目中使用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;
    }
  }
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导致图片不显示这篇文章。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BennuCTech

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值