歷時四年,Google終于修復了一個我發現的Android Framework Bug
2014年在做一個Android終端設備開發過程中,發現了一個Android Framework層的Bug,給Google提交了issue和解決方案,和外界傳言一致Google一般不太在意個人開發者提交的issue,直到2017年12月,再次提交了issue,在幾輪溝通無果下,忍不住噴了Google幾句后,終于該issue被轉交給了Android development team處理,又經過快三個月的時間收到了下面的答復:
![]()
google issues:https://issuetracker.google.com/issues/70016687
issue背景:
在對我們自己研發的一款Android終端設備進行Camera拍照壓力測試時,發現當拍照張數達到數萬張時,出現OOM,導致系統崩潰。
issue分析解決過程:
該項目為開發一款Android工業終端設備,采用TI芯片方案,由于芯片方案商支持不夠完善(主要TI被高通打的放棄了移動端芯片市場),Camera的HAL層需要我們自己移植適配。
分析思路:
首先看下Android系統架構圖中中Camera功能模塊的分布情況,從App層->Framework->HAL->Kernel一路下來:
(圖片取自https://blog.csdn.net/asd1031/article/details/53699867)
- 首先懷疑測試app自身存在內存問題。
- Android application Framework,libraries層和jni相關模塊是經過Google大量驗證的模塊,出現問題的概率比較小,暫時排除。
- 懷疑HAL層移植問題。
懷疑1:測試app問題
壓力測試app由測試同學開發提供的,該app每隔3s觸發一次拍照操作,并對拍攝的照片進行清理,以達到拍攝10萬張照片的測試目的。基于以上分析,為了排除測試app問題,采用Android原生Camera app進行壓力測試,編寫monkey測試腳本,觸發原生Camera app拍照,進行壓力測試。(此處遇到的問題是:如何實現對照片的清理工作,直接觸發shell環境下rm操作,并不會清除Android內部文件緩存索引,拍攝幾千張照片后仍然會導致存儲空間用盡,解決此問題也耗費了點時間,不過不是本文的重點,此處不做展開)
結果:通過原生Camera app進行測試后,仍然出現內存泄漏,此處基本排除測試app的問題。
懷疑2:HAl層
基于之前的分析,我們把懷疑對象聚集在我們自己集成的Android HAL層,在分析之前,簡單描述下Android Camera拍照的流程:Linux Kernel提供標準的v4l2接口,供上層(此處即為HAL)獲取圖像原始數據,HAL層拿到圖像數據進行編碼(一般為jpeg),回調給Camera service。其中Linux Kernel下Camera驅動和HAL適配層一般由芯片廠商提供,其余部分由Linux Kernel和Android系統官方維護,開發。
這樣對HAL的測試分為三步:
- 驗證芯片廠商的Camera驅動是否存在問題。
- 驗證HAL層圖像數據捕獲流程是否存在問題。
- 驗證圖像編碼流程是否存在問題。
Camera驅動
Android系統是基于Linux Kernel開發,支持標準的v4l2接口,只需要編寫一個簡單的基于v4l2的視頻捕獲程序就可以驗證camera驅動的問題,此處測試驗證沒有內存泄漏,排查驅動問題。
HAL層視頻捕獲流程
測試思路非常明確,難點在于要把芯片提供商的HAL層源碼中進行視頻捕獲功能的模塊剝離出來,單獨進行壓力測試。(由于原廠提供的HAL層代碼,耦合比較嚴重,在不影響內部流程,結構的情況下,要找到適合的切面mock一些數據接口,才好進行有效的測試。)
經過以上的工作,進行了壓力測試,系統未出現內存泄漏,基本排除HAL層捕獲流程。
HAL層圖像編碼流程
繼續對圖像編碼部分剝離,進行壓力測試,發現內存泄漏,基本定位大概的泄漏位置,不過由于Android整個編碼過程也進行層層的封裝,泄漏位置還需要繼續細致的定位,這樣經過層層的細化,像剝洋蔥一樣一層層mock輸入數據,最終定位在Android系統層的jpeg編碼處理中:(frameworks/base/core/jni/android/graphics/YuvToJpegEncoder.cpp)
關于Android的jpeg編碼:Android系統jpeg編碼支持硬編碼和軟編碼,如果芯片集成了jpeg硬件編碼模塊,會優先選擇硬編碼,而如果沒有該模塊,會采用軟件的jpeg編碼進行處理。
Android采用的軟件編碼庫是業內知名的libjpeg庫,而正是對這個庫的使用出了問題:
bool YuvToJpegEncoder::encode(SkWStream* stream, void* inYuv, int width,int height, int* offsets, int jpegQuality) {jpeg_compress_struct cinfo;skjpeg_error_mgr sk_err;skjpeg_destination_mgr sk_wstream(stream);cinfo.err = jpeg_std_error(&sk_err);sk_err.error_exit = skjpeg_error_exit;if (setjmp(sk_err.fJmpBuf)) {return false;}jpeg_create_compress(&cinfo);cinfo.dest = &sk_wstream;setJpegCompressStruct(&cinfo, width, height, jpegQuality);jpeg_start_compress(&cinfo, TRUE);compress(&cinfo, (uint8_t*) inYuv, offsets);jpeg_finish_compress(&cinfo);return true;
}
坑就在上面這個接口函數中:
熟悉libjpeg的同學可能會注意到,上面的接口在調用完jpeg_finish_compress()后,沒有調用jpeg_destroy_compress(),這個接口是釋放壓縮工作過程中所申請的資源,就是代碼中的cinfo結構,該結構只占十幾個字節的內存, 這樣就導致了每拍攝一張照片,就泄漏一個cinfo的內存,當拍照數量達到萬級時,才會有所察覺。
對這種數據流的控制,pipeline方式是比較好的方案,因為可以明確輸入輸出,這樣非常方便通過偽造輸入數據對各個模塊進行單獨的壓力測試,最難控制的就是“洋蔥”式的包裹調用,要像“剝洋蔥”一樣一層層的剝離,找準切面十分麻煩。
這個bug是否影響到你的Android手機
七成的概率下你的手機應該不會有這個問題,即時有這個問題你也很難發現這個問題,因為上面講到android系統有兩種編碼方式選擇,優先使用硬件編碼模塊,如果沒有硬件編碼模塊,才會使用軟編碼的方式,而目前大部分中高端的芯片方案都集成了硬件模塊,只有在少數低端芯片上才會使用軟編碼的方式,并且即使你的手機沒有硬編碼模塊,用的軟編碼,也很難遇見這個問題,因為對于普通用戶,持續拍攝上萬張照片是不太可能的,第一受限于手機的存儲空間(一萬張照片,至少要30G的空間),第二即使能拍攝上萬張照片,但要保持手機一直工作不重啟也還是比較苦難的(總會死個機啥的)。
哈哈,這么一說發現這個bug其實是一個不會發生的bug了!!!不過我們之前的產品,定位于工業級別,對圖像采集有比較高的要求,所以制定了10萬張照片的測試標準,也就讓我發現了這個不會影響到大部分人的bug。
最后再吐槽下Google
改bug我在2014年就已經提交了issue,不過沒持續關注,過了幾個月被莫名其妙的關閉了,當時沒有在意,不過當Android 6.0,7.0版本出來時,我都看了下這個bug,一直存在,所以在去年(2017年)12月份又提了一個issue,Google方面的處理人仍然各種推諉扯皮,最后我沒忍住噴了幾句,這次Google方面回復會轉給開發團隊處理,終于在今年(2018年)給出了fixed的結論。