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

0x00 背景

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

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

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

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

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

图片来自 glennrobinsononline.com

0x01 问题重现

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

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格式存储到本地。

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

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

损坏的图片

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

0x02 分析

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

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

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

通过Picture Compare模式对比图片

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

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

为了进一步考究导致差异的根本原因,可以通过Hex 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写入并没有问题。随后与手机厂商沟通,问题也近乎尘埃落定:该手机ROM在处理BitmapFactory的底层出现问题。

0x03 解决方案

现在的问题很明确,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。

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

0x04 后记

对于这种结论我是跪了一地的。。。

毕竟不是第一次遇到这种问题。每当厂商ROM出现bug,这种锅就得开发者来背。

你总不能等厂商去修复吧?你的App新版还要不要上线?再说了,厂商修复了,用户也未必会去升级。除了少数几个厂商把ROM品牌玩的飞起,其他厂商即使更新ROM版本,能够主动升级的用户也并非多数。

所以,我很讨厌那种所谓“深度定制”的系统。

你说优化系统好不好,我当然支持。但是拜托,要有把握才去改。埋的坑,以后填都填不上,何必呢。老大哥Google写的代码你说不好要去改,改完搞不好就没人维护了。

机友们说得好:Nexus大法好

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容