背景

作为移动领域所力推的图片格式,WebP图片在商业领域证明了其应有的价值。基于其他格式的横向对比,其在压缩性能表现,及还原度极为优秀,节省大量的带宽开销。基于可观的效益比,团队早前已开始磋商将当前图片资源迁移至.webp资源。

然而对于Android而言,加载.webp图片所消耗的时间比.jpg.png要慢数倍。对于这点而言是无法忍受的。因此解决方案是:

从网络拿到.webp数据流 -> Bitmap通过.png格式保存到本地

注意,整个过程必须在子线程执行。这样,在使用了WebP节省了带宽的同时,下一次加载图片的速度也不会受到影响。

但在客户端实现的最后阶段,出现了一些问题。

问题重现

对于上述的解决方案,隐去业务复杂性,我用以下示例来展示:

private void saveImage(String uri, String savePath) throws IOException {

    // 创建连接
HttpURLConnection conn = createConnection(uri); // 拿到输入流,此流即是图片资源本身
InputStream imputStream = conn.getInputStream(); // 指使Bitmap通过流获取数据
Bitmap bitmap = BitmapFactory.decodeStream(imputStream); File file = new File(savePath); OutputStream out = new BufferedOutputStream(new FileOutputStream(file.getCanonicalPath()), BUFFER_SIZE); // 指使Bitmap以相应的格式,将当前Bitmap中的图片数据保存到文件
if (bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)) {
out.flush();
out.close();
}
}

上述代码意图明显:拿到流,将该流通过decodeStream(InputStream)方法传送到Bitmap,随后以.png格式存储到本地。

在很长一段时间内,该代码运作良好。直到有一天,在某国产机型上做测试的时候,发现图片保存到本地后出现了损坏。

那些保存到本地出现损坏的图片,长这样:

损坏的图片

在这张样图中,图片的下半部分出现了缺失。在随后的循环测试中,每张图片的缺失程度大小不一,从完整到全黑都有。

分析

对于这种情况,第一猜想可能是网络返回的数据流有问题。但在随后的排查中,发现InputStream数据流是完整的。随后开始对图片本身进行分析。

对文件差异进行分析是一种好办法。在这里,使用Beyond Compare以不同的方式进行分析。于是准备了两张图片,一张成功从.webp转为.png,另一张也从.webp转为.png,但是出现缺失黑块。

现在,通过Picture Compare模式直观地对比两张图片:

通过Picture Compare模式对比图片

在这里,左侧为完整图片,右侧为存在数据缺失的图片,下方为差异标记:红色区域为两张图片的差异之处。

可以观察到,相对于完整图片而言,存在数据缺失的图片并非零散地缺失数据,而是从某一刻开始,数据便不复存在了。

为了进一步考究导致差异的根本原因,可以通过Hex Compare模式进行对比。也就是说,以十六进制的方式对比文件。现在,通过Hex Compare模式进行文件对比:

 
 

左侧的红条表示两个文件中二进制数据不一致的地方。

其中,左侧为完整的.png文件,右侧为存在缺失黑块的.png文件。观察缺失文件的十六进制数据,存在着大量的空值块(0x00000000),并且数据长度是短于完整文件的。同时,此现象与早前出现黑块的规律相似:大块的数据丢失,并非零散的缺失。

但是,文件的分析尚未结束。有一个非常重要的问题不要忽略了:

我们是打开了一张数据损坏的图像吗?

我们知道,如果一个图像文件的关键数据块出现损坏,该图像是无法被打开的。也就是说,如果一个图像文件能够被打开,说明该图像文件结构完整。

那么,如何分析一张图像的数据块是否完整?在这里,我们关心的是:那张缺失的图像,文件末尾写入成功了吗?

在这里有必要解释一下PNG文件末尾的数据块是个什么东西。引用PNG格式标准的官方说法(PNG格式块简述:w3.org):

Chunks can appear in any order, subject to the restrictions placed on each chunk type. (One notable restriction is that IHDR must appear first and IEND must appear last; thus the IEND chunk serves as an end-of-file marker.) Multiple chunks of the same type can appear, but only if specifically permitted for that type.

解释:在整个PNG文件中,用以标记文件开始的IHDR标记必须在文件的最开始,标记文件结束的IEND标记必须在文件的最末端。对于其他数据块则没有顺序要求。

也就是说,如果一张PNG图片能够被打开,那么它在文件的最后,必定存在IEND标记。

回到刚才的Hex Compare,拉到最底部,于是发现:

完整的文件末尾写入

没错。两张图片的末端都有IEND标记。

也就是说,那张存在黑块的.png文件,IO写入并没有问题。于是可以得出一个让人惊惶的结论:那台国产机的BitmapFactory的底层处理有问题。

没错,就是这么坑。

解决方案

现在的问题很明确,BitmapFactory中某些native方法存在bug。那是不是所有的native方法都有问题呢?

BitmapFactory.decodeStream(InputStream)方法最终调用的是native方法nativeDecodeStream(InputStream, byte[], Rect, Options)。尝试绕开它试试看。

可否尝试将网络数据流保存到内存,随后再将其指向BitmapFactory?答案是肯定的。我们尝试替换一部分代码。将此部分代码:

// 拿到输入流,此流即是图片资源本身
InputStream imputStream = conn.getInputStream(); // 指使Bitmap通过流获取数据
Bitmap bitmap = BitmapFactory.decodeStream(imputStream);

替换成:

// 拿到输入流,此流即是图片资源本身
InputStream imputStream = conn.getInputStream(); // 将所有InputStream写到byte数组当中
byte[] targetData = null;
byte[] bytePart = new byte[4096];
while (true) {
int readLength = imputStream.read(bytePart);
if (readLength == -1) {
break;
} else {
byte[] temp = new byte[readLength + (targetData == null ? 0 : targetData.length)];
if (targetData != null) {
System.arraycopy(targetData, 0, temp, 0, targetData.length);
System.arraycopy(bytePart, 0, temp, targetData.length, readLength);
} else {
System.arraycopy(bytePart, 0, temp, 0, readLength);
}
targetData = temp;
}
} // 指使Bitmap通过byte数组获取数据
Bitmap bitmap = BitmapFactory.decodeByteArray(targetData, 0, targetData.length);

BitmapFactory.decodeByteArray(byte[], int, int)方法最终调用了native方法nativeDecodeByteArray(byte[], int, int, Options),与通过InputStream处理所指向的native方法不同。

经过测试,使用这种方法所保存的.png文件不存在黑块问题。我们无法得知厂商ROM中对于这两种方法有什么差异对待,但至少可以明确:上文中提到的那台国产机子,通过InputStream传递WebP数据并存储为.png图像这一过程存在可预知的bug。

至此,问题分析及解决方案阐述完毕。

原文地址http://www.jianshu.com/p/e5837a85e6cb

Android Bitmap转换WebP图片导致损坏的分析及解决方案的更多相关文章

  1. Android Bitmap转换WebPng图片导致损坏的分析及解决方案

    出现问题的code!!! private void saveImage(String uri, String savePath) throws IOException { // 创建连接 HttpUR ...

  2. Android View转换成图片保存

    package zhangphil.viewtoimage; import java.io.File;import java.io.FileOutputStream; import android.o ...

  3. android bitmap compress(图片压缩)

    android bitmap compress android的照相功能随着手机硬件的发展,变得越来越强大,能够找出很高分辨率的图片. 有些场景中,需要照相并且上传到服务,但是由于图片的大小太大,那么 ...

  4. webp图片技术调研最终结论(完全真实数据可自行分析)

    关于webp图片格式调研及测试 资料收集 什么是 WebP? WebP(发音 weppy),是一种支持有损压缩和无损压缩的图片文件格式,派生自图像编码格式 VP8.根据 Google 的测试,无损压缩 ...

  5. Android Bitmap 和 ByteArray的互相转换

    Android Bitmap 和 ByteArray的互相转换 移动平台图像处理,需要将图像传给native处理,如何传递?将bitmap转换成一个 byte[] 方便传递也方便cpp代码直接处理图像 ...

  6. 【Android】Bitmap加载图片错误 java.lang.OutOfMemoryError: bitmap size exceeds VM budget

    今天测试程序的时候出现下面的错误日志信息,程序当场挂掉 07-09 14:11:25.434: W/System.err(4890): java.lang.OutOfMemoryError: bitm ...

  7. Android bitmap图片处理

    一.View转换为Bitmap         在Android中所有的控件都是View的直接子类或者间接子类,通过它们可以组成丰富的UI界面.在窗口显示的时候Android会把这些控件都加载到内存中 ...

  8. Android Bitmap与DrawAble与byte[]与InputStream之间的转换工具类【转】

    package com.soai.imdemo; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; ...

  9. Android,View转换bitmap,bitmap转换drawable

    Android View转换Bitmap,Bitmap转换Drawable //测试设置bitmap View view1 = ViewGroup.inflate(context, R.layout. ...

随机推荐

  1. 使用go语言实现简单的反向代理工具激活IntelliJ和PyCharm,持续更新

    最近Jetbrians系列IDE更新至2017.3版本,激活检测机制也变成了动态封禁域名,导致大部分域名激活被屏蔽了,所以找了下资料,根据ilanyu的代码,改了下地址,实现了本地反向代理激活服务器. ...

  2. Setting up Storm and Running Your First Topology

    http://www.haroldnguyen.com/blog/2015/01/setting-up-storm-and-running-your-first-topology/ --------- ...

  3. DMA32映射问题

    近期在调试PCIe的行情加速卡的驱动.当中使用DMA在CPU和FPGA间数据传输. 最開始使用的是低16M的DMA ZONE的内存,用slab分配器的kmalloc分配获取.但因为最新的需求,须要使用 ...

  4. Python全栈

    Python基础 Python基础01 Hello World! Python基础02 基本数据类型 Python基础03 序列 Python基础04 运算 Python基础05 缩进和选择 Pyth ...

  5. c#生成AVI自动设置压缩格式,不调用AVISaveOptions

    工作中遇到生成AVI视频的项目,代码中会调用AVISaveOptions来设置压缩格式,针对单个文件还好说,但是批量生成视频的时候,每一个都要设置格式, 体验不是很好,经过查询资料问题得到解决 最开始 ...

  6. 2016/1/22 3,将id为005的对象从集合中移除

    package shuzu; public class Emp { private String id; private String name; public Emp(String id, Stri ...

  7. JavaScript基础 -- 焦点图轮播(转载)

    首先将HTML结构搭建好: <div id="container"> <div id="list" style="left: -60 ...

  8. iOS开发——基础篇——iOS开发 Xcode8中遇到的问题及改动

      iOS开发 Xcode8中遇到的问题及改动 新版本发布总会有很多坑,也会有很多改动. 一个一个填吧... 一.遇到的问题 1.权限以及相关设置 iOS10系统下调用系统相册.相机功能,或者苹果健康 ...

  9. GuiLite 1.2 发布(希望通过这100+行代码来揭示:GuiLite的初始化,界面元素Layout,及消息映射的过程)

    经过开发群的长期验证,我们发现:即使代码只有5千多行,也不意味着能够轻松弄懂代码意图.痛定思痛,我们发现:虽然每个函数都很简单(平均长度约为30行),可以逐个击破:但各个函数之间如何协作,却很难说明清 ...

  10. [Codeforces 425A] Sereja and Swaps

    [题目链接] https://codeforces.com/contest/425/problem/A [算法] 枚举最终序列的左端点和右端点 , 尝试用这段区间中小的数与区间外大的数交换 时间复杂度 ...