所謂"番外特別篇",就是系列文章更新期間內,隨機插入的一篇文章.目前我正在更新的系列文章是 [實現iOS圖片等資源文件的熱更新化](https://github.com/ios122/ios_assets_hot_update).但是,這兩天,被一個自己App中詭異的相冊讀取的Bug困擾,暫時延緩了... ...
關於"番外特別篇"
所謂"番外特別篇",就是系列文章更新期間內,隨機插入的一篇文章.目前我正在更新的系列文章是 實現iOS圖片等資源文件的熱更新化.但是,這兩天,被一個自己App中詭異的相冊讀取的Bug困擾,暫時延緩了文章的更新進度.這個BUG,詭異而又有趣,既然花了10個小時才理清,不妨再投入1個小時,曬出來供大家鑒賞,品玩.
Bug 的詳細描述
詭異的畫風
此Bug僅在操作多張高像素圖片時才會觸發,所謂高像素就是圖片本身並不算大,但是圖片寬高非常大的圖片.這次觸發這個問題的是一組 5701 * 3171 的圖片.畫風大家可以點擊鏈接查看原圖自行感受下 --https://github.com/ios122/why_not_uiimage/blob/master/bug_img.jpg?raw=true
當BOSS剛好是一個攝影愛好者
在大多數情況下,是很少有用戶觸發這個問題的,但是BOSS是一個攝影愛好者,手機里有許多高像素圖,一天他想往自己公司的App上傳分享幾張圖片時,他竟然沒法把一次性地從相冊選取九張圖,每次選中後,點擊"確定",都會理解Crash.是的,就是那九張圖,其他圖片是沒問題的,8張圖,也是OK的,他還強調了下是用的最新版本的App.
關於 BUG 的預處理
首先,我的第一反應是肯定是他的手機太燙了吧,重啟下,就好了.恩,肯定是這樣.發佈作品的邏輯,好幾個版本都沒動過.模擬器,手機,我自己試了下,都是OK的.也沒有其他用戶反饋過,fabric也看不到任何log.對,手機太燙了.我稍後,再聯繫他,肯定就OK了.
稍後,再直接聯繫BOSS,竟然還是會Crash,他甚至給我錄屏演示了一下,真的每次都會crash.而且我還無法復現.而且BOSS手機iPhone6 plus,自身記憶體不足的原因非常非常小.
形勢,瞬間變得很緊張,這個問題的優先順序瞬間被提到了最高!再次嘗試了各種可能的情況.圖片大小?它是9張1.5M的圖,我就用9張3M的圖,也是OK的呀!選取時,順序有問題?我試著按照錄屏中演示的順序去選取圖片,也是OK的.一股深深地無力感!竟然連復現都無法復現不了!
最後的最後,說是會拿手機給我測試.不過,最後BOSS的手機,還是沒有拿到,只是拿到了開篇那張畫風詭異的圖片.沒錯,就是它,連續選取9張,就Crash了.
至少,我現在能復現問題了.下麵的,需要的就只是時間,耐心還有大開的腦洞了.
Bug 分析思路的簡要描述
我不覺得,分析Bug真的有什麼思路可言.Bug產生的原因,是有許多可能性的,可能行驗證的順序,方式和深度很大程度上取決於coder本身已有的經驗,天賦,甚至還有些許的運氣!我能描述的,可能僅僅是我處理這個問題的一個相對的完整腦洞過程.部分分析過程間,明顯不是有邏輯性的.越是詭異的問題,越是不能循規蹈矩,要時刻嘗試去問自己最可能地問題是什麼,而不是沿著一條路,一條道走到黑.
1.排除通用邏輯問題
Coder有些許高傲,有時候是有利於自己更冷靜地處理問題的.稍微不自信點的童鞋,可能就會懷疑:我代碼是不是有什麼特殊的臨界判斷沒有加?不行,我得去看看.一行一行,看代碼,從天黑到天亮,從期待到絕望...其實,稍微有一些對比實驗常識的人,都很容易猜到: 兩種情況,唯一的變數是 圖片素材本身,那 最可能 的原因肯定是 圖片本身的問題.一種高大上的說法,這某種程度上,也暗合了所謂的"貪心演算法".每次,都只從最可能的原因入手,管他誰是誰,我的代碼就算有問題,那觸發這個問題的可能性,也是遠小於 圖片素材本身的.---多麼朴素的真理呀!
2.確定是相冊選取圖片記憶體過高
這個問題,在真機上,並不好確定,因為連續讀取9張高像素圖時,記憶體是瞬間飆升的,你幾乎沒有機會去觀察記憶體占用,給人一種因為某種邏輯判斷而導致的Crash的錯覺.如果換做模擬器,會很容易看到,這個記憶體占用,是飆升到G單位的.當然,我也沒那麼睿智,我是單個N個斷點,最終確認了Crash的代碼的準確位置.一個for迴圈,每次step 1,這下很明顯地看到記憶體,幾乎是 100M/張的速度在飆升,而圖片本身的大小隻有 1.5M/張.此處我想說的是,打斷點也是有技巧的,最後沒有辦法的辦法也是講究辦法的.可是試著註釋掉可能的引起的代碼,然後逐步放開註釋,這要觀察,會比直接打斷點快些.--意會!
3.確定是PHImageManager 的問題requestImageForAsset:方法引起的高記憶體占用
當你通過註釋法,配合斷點,很容易就可以引起記憶體高占用的代碼.此處,我的App中,是讀取相冊原圖,用的是 PHImageManager 的 requestImageForAsset:targetSize:contentMode:options: resultHandler: 方法.此處接下來的解決思路,有大坑呀!你可能會想,是UIImage載入的問題吧?那就研究下UIImage渲染機制吧.然後1天過去了,等你學成歸來,驀然發現 PHImageManager 是一個系統方法,它載入的圖片機制,你無力干涉!我可能運氣比較好些吧,研究UIImage的渲染機制,想想都頭疼,抱著試一試的態度,我google了下: PHImageManager requestImageForAsset memory high,然後第一條鏈接的第二個回答就是我要到答案: http://stackoverflow.com/questions/33274791/high-memory-usage-looping-through-phassets-and-calling-requestimageforasset
是的,我運氣,似乎總是很好~
4.使用requestImageDataForAsset:替換的問題requestImageForAsset:
答案原文是:
I found that if i switch from
- requestImageForAsset:targetSize:contentMode:options:resultHandler:
to
- requestImageDataForAsset:options:resultHandler:
i will receive the image with the same dimension {5376, 2688} but the size in byte is much smaller. So the memory issue is solved.
hope this help !!
(note : [UIImage imageWithData:imageData] use this to convert NSData to UIImage)
簡單說,就是用 - requestImageDataForAsset:options:resultHandler: 替換 requestImageForAsset:targetSize:contentMode:options:resultHandler: 就可以了,前者是直接返回二進位數據,不渲染.
但是,這裡有一個可能不是問題的問題, 這個方法調用是位於一個名為第三方庫 TZImagePickerController 內,我方便直接改嗎? 我是直接給改了.此處,將來必成大患,以後再用到,肯定還會有相同問題,還不如直接把原來的實現直接替換掉.當然,這也是成本最小的方法.這個庫,本身,已經在App內,深度定製和重寫了,如果一些成熟的第三方庫,這麼做,最好先備份或備註下.
5.使用imageWithData:相容原來的調用
為了和原來的Api介面調用相容,用imageWithData:將NSData轉換為 UIImage 傳出,同時擴展方法,使支持同時傳出 UIImage和原始的 NSData對象.傳出NSData對象的原因是,是因為高像素圖片,會引起一些列的問題,故事到此遠遠沒有結束,詳見衍生問題部分.
6.變更前後的代碼對比
還是來段代碼感受下吧,一碼剩千言:
/*原來的代碼*/
[[PHImageManager defaultManager] requestImageForAsset:asset targetSize:PHImageManagerMaximumSize contentMode:PHImageContentModeAspectFit options:option resultHandler:^(UIImage * _Nullable result, NSDictionary * _Nullable info) {
BOOL downloadFinined = (![[info objectForKey:PHImageCancelledKey] boolValue] && ![info objectForKey:PHImageErrorKey]);
if (downloadFinined && result) {
result = [self fixOrientation:result];
if (completion) completion(result,info);
}
}];
/*優化後代碼*/
[[PHImageManager defaultManager] requestImageDataForAsset:asset options:option resultHandler:^(NSData * _Nullable imageData, NSString * _Nullable dataUTI, UIImageOrientation orientation, NSDictionary * _Nullable info) {
UIImage * result = [UIImage imageWithData:imageData];
BOOL downloadFinined = (![[info objectForKey:PHImageCancelledKey] boolValue] && ![info objectForKey:PHImageErrorKey]);
if (downloadFinined && result) {
result = [self fixOrientation:result];
if (completion) completion(result,info,imageData);
}
}];
此類Bug的可能的通用解決思路
首先,我要說明下,我解決的思路和方式,很大程度上依賴也受限於我已有的經驗,此處的解法,可能不是最優解,最多只能算是個通用解.說不定,將來等我再研究下渲染機制一類的技術,會有一個新的更簡單的方法.歡迎大神補充!
未來遇到UIImage記憶體問題的童鞋,至少能從此處獲取的一個至少驗證可用的解決策略.
回到問題本身,用一句概括就是:永遠不要直接傳遞UIImage對象.在需要傳遞UIImage的場景中,請使用圖片名或者NSData二進位對代替.
衍生問題應用與解決
故事,真的還沒有完結.從相冊順利讀取這張詭異的高像素圖後,我發現我沒有辦法將它上傳,也無法在輪播圖上,連續顯示.簡要概括如下.
無法直接以UIImage格式,連續把九張圖保存到緩存目錄
圖片選取後,並不是立即上傳的,為了能實現"重發"功能,需要在緩存目錄保留副本.原來是將 UIImage 轉換為 NSData寫入.在此過程中,又一次引起了巨額的記憶體開銷.解決方法,就是直接緩存原始獲取的 NSData 的對象,而不要 NSData --> UIImage --> NSData.
無法直接以UIImage格式,連續在輪播圖上顯示九張圖
此處對應的是一個本地大圖預覽功能,實現是在前一個頁面把九張本地圖的UIImage傳遞給輪播預覽組件.此處的坑是: 把一個存放在 數組中的UIImage對象傳遞給 UIImageView的 image屬性,當UIImageView載入到父視圖時,會引起巨額的記憶體占用.原因初步猜測是 UIImage 對象顯示到 UIImageView 會有一個特殊的耗費記憶體的操作,如果原始的 UIImage 對象一直存在,這一塊記憶體那就無法釋放.這一步,困擾了我很久很久,好幾個小時!我真沒想到,一個UIImage對象,竟然會二次引起高記憶體占用.最終的解決方法,就是在前一個頁面傳遞 NSData數組,在賦值處,再使用imageWithData:轉換為 UIImage.這樣,記憶體使用基本沒什麼起伏.
或許,我應該研究下 一個UIImage對象,竟然會二次引起高記憶體占用 的原因.歡迎大神完善!