Android Bitmap轉換WebP圖片導致損壞的分析及解決方案


背景

作為移動領域所力推的圖片格式,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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM