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

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

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

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

  2. Android 7.0 Launcher3的启动和加载流程分析----转载

     Android 7.0 Launcher3的启动和加载流程分析,Launcher的本质就是一个普通应用,它比普通应用多配置了Category的Android:name="android ...

  3. android app启动流程分析,Android应用开发之Android 7.0 Launcher3的启动和加载流程分析...

    本文将带你了解Android应用开发Android 7.0 Launcher3的启动和加载流程分析,希望本文对大家学Android有所帮助. Android 7.0 Launcher3的启动和加载流程 ...

  4. android6.0升级名单,一加公布Android 6.0升级名单 一加X还要再等等

    原标题:一加公布Android 6.0升级名单 一加X还要再等等 科客点评:真心觉得圆形比方形好看. 自Android 6.0 Marshmallow正式发布以来,厂商陆续公布了自家手机型号升级名单. ...

  5. Android 7.0 Launcher3的启动和加载流程分析

    本文的分析基于MTK提供的Android 7.0源码,并非Google官方提供的源码,其中可能有一些小的差异,还望谅解. Launcher的本质就是一个普通应用,它比普通应用多配置了Category的 ...

  6. Android SoftAp SoftAp打开/关闭代码流程(基于android 7.0)

    在Android手机中,SoftAp也是一个较为常用的功能.对于framework开发者来说,要开发.维护SoftAp,了解framework中热点开关的具体流程是非常有必要的.下面就对这部分内容做一 ...

  7. Android OpenGLES2.0(九)——利用OpenGL进行图片处理

    在之前的博客中我们就有提过OpenGLES的常见应用范围,其中有一个就是图片的处理.为了保证效率,Android手机中许多美颜相机.图片处理应用,都用到了OpenGLES来处理图片.上一篇博客中,我们 ...

  8. android 调用相机并获取图片地址,Android 7.0使用FileProvider获取相机拍照的图片路径...

    这里主要是基于Android 7.0,Nougat 实现一个获取相机拍照的图片后,使用FileProvider把图片转换为实际的路径. 首先需要在AndroidManifest.xml声明调用相机的权 ...

  9. 小米7.0 android 图片裁剪失败,Android 7.0适配 -- FileProvider 拍照、选择相册、裁切图片, 小米机型适配...

    需求: 最近把APP的TargetSdk从21提高至25后,测试时, 在Android7.0以上的系统上,爆出了一些异常. 在个别小米等机型也存在一些异常. 问题分析: FileUriExposedE ...

最新文章

  1. 【Java从0到架构师(3),【好文推荐
  2. 打造极致体验:字节跳动亿级 DAU 背后的音视频技术最佳实践
  3. mac idea配置配置自动清除类中无用的import包
  4. IT运维面试问题总结-运维工具、开源应用(Ansible、Ceph、Docker、Apache、Nginx等)
  5. js识别用户设备是移动端手机时跳转到手机网站
  6. 【问答系统】QA问答系统(Question Answering)
  7. MySQL - 查询今天的数据(以及昨天、本月、上个月、今年...)
  8. python约瑟夫环单向循环链表_约瑟夫环_约瑟夫环单向循环链表_约瑟夫环c语言循环链式...
  9. 求100以内的所有素数
  10. 零信任之微分段(微隔离)
  11. ADC噪声全面分析 -01- ADC噪声的类型以及ADC特性
  12. 外卖红包返利小程序源码系统
  13. 鼠标滑轮将物体放大缩小
  14. win10 删除删除账户,新的账户中文件资源管理器图标变白 找回方法
  15. [转载]屏蔽双显卡笔记本的独显
  16. ulipad 无法安装使用、无法调试、64位版本问题 ImportError: No module named comtypes 整体解决方案!
  17. 【Matlab人脸识别】人脸实时检测与跟踪【含GUI源码 673期】
  18. 华为到底算不算是一份好工作?看完你们还会羡慕华为的高薪吗?
  19. 详解淘宝直播背后的技术
  20. 浅谈Android Service

热门文章

  1. 访百度奥运logo设计师李兴钢:虚实之间最美的呈现
  2. 数据库存带微信表情的昵称
  3. mkdir: cannot create directory ‘/soft/hadoop-2.7.3/logs’: Permission denied问题
  4. CTR预估中GBDT与LR融合方案
  5. 【winfrom】读写文本
  6. css对齐 挖坑~
  7. 在VC中 改变List列表中一行的颜色
  8. django2中关于时间处理策略
  9. Java逆向基础之AspectJ的Eclipse插件AJDT
  10. android之Activity的生命周期