剖析Picasso加载压缩本地图片流程(解决Android 5.0部分机型无法加载本地图片的问题)
之前項目中使用Picasso遇到了一個問題:在Android 5.0以上版本的部分手機上使用Picasso加載本地圖片會失敗。為了解決這個問題,研究了一下Picasso加載和壓縮本地圖片的流程,才有了這篇文章。
我們知道,Picasso加載本地圖片有兩種方法,一種是new File(path),另外一種是url = "file://" + path。尤其后一種在picasso2.5.2及之前版本一定要加"file"協(xié)議,否則加載圖片出錯。
對于上面提到的問題,只出現(xiàn)在使用Picasso加載本地圖片并且使用了resize方法的時候。
而且發(fā)現(xiàn)只存在Android 5.0以上版本的部分手機上。比如我們發(fā)現(xiàn)5.0、5.0.1有問題,5.1.1沒有問題;同時有的5.0的手機沒問題,而有的會出現(xiàn)這個問題。
由于有時本地照片較大,所以雖然可以不使用resize方法來保證圖片正常加載,但是這樣內存開銷會急劇增加,所以我們不可避免的要解決這個問題。
經研究發(fā)現(xiàn)問題出現(xiàn)在picasso的BitmapHunter類中,當?shù)玫阶x取了網(wǎng)絡或本地圖片后,會調用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的時候會動態(tài)改變offset的值。
我們回到decodeStream()函數(shù),代碼(1)為markStream設定了一個值,經過MarkableInputStream的savePosition()和setLimit()函數(shù),MarkableInputStream的類變量limit被賦值65536。代碼(4)這里有讀取操作,所以MarkableInputStream的類變量offset會改變,而代碼(5)則調用MarkableInputStream的reset()函數(shù),這個方法中先比較offset和limit,如果offset比limit大會拋出錯誤,加載過程就停止了,加載出錯。
問題就出現(xiàn)在這里:
1、在正常的手機上,代碼(4)執(zhí)行完畢,由于options的inJustDecodeBounds為true,所以只讀取圖片的信息部分,offset這個變量的值也沒有很大,比65536小,所以代碼(5)的reset方法會正常執(zhí)行,會正常加載本地圖片。
2、但是在部分手機上,代碼(4)執(zhí)行完畢,offset這個變量的值遠遠比65536大,所以reset方法會拋出異常,加載出錯,顯示error圖片。
這樣我們就得出了結論:
在部分手機上,BitmapFactory.?decodeStream(stream,?null,?options);這個方法的實現(xiàn)可能有差別,導致了問題的出現(xiàn)。
同時我們在代碼(2)處可以看到,如果是本地圖片而且是5.0以上版本,才走else流程,既有問題的代碼。而且在代碼(3)處則判斷是否壓縮,如果壓縮才會走代碼(4)到(5),否則不走這部分,就不會出錯。這就解釋了這個問題為什么會有如此出現(xiàn)機制。
最簡單的解決方法:
使用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"協(xié)議,所以本地圖片使用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沒有協(xié)議,則自動添加"file"協(xié)議。
本篇內容就這樣了,在picasso 2.5.2版本還存在一個問題:在Android 5.0以下版本加載https圖片出錯,如果你遇到了這個問題,請閱讀解決Picasso在Android 5.0以下版本不兼容https導致圖片不顯示這篇文章。
總結
以上是生活随笔為你收集整理的剖析Picasso加载压缩本地图片流程(解决Android 5.0部分机型无法加载本地图片的问题)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 剖析Picasso中的内存缓存机制——L
- 下一篇: Android魔法(第二弹)——一步步实