番外特別篇之 爲什麼我不建議你直接使用UIImage傳值?--從一個詭異的相冊九圖連讀崩潰bug談起

關於”番外特別篇”

所謂”番外特別篇”,就是系列文章更新期間內,隨機插入的一篇文章.目前我正在更新的系列文章是 實現iOS圖片等資源文件的熱更新化.但是,這兩天,被一個自己App中詭異的相冊讀取的Bug困擾,暫時延緩了文章的更新進度.這個BUG,詭異而又有趣,既然花了10個小時才理清,不妨再投入1個小時,曬出來供大家鑑賞,品玩.

Bug 的詳細描述

詭異的畫風

bug_img

此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中,是讀取相冊原圖,用的是 PHImageManagerrequestImageForAsset: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對象,竟然會二次引起高內存佔用 的原因.歡迎大神完善!

參考鏈接


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章