記一些印象深入的 Bug

1、iOS 加載超大尺寸圖片 Crash 的調研及解決方案

1.一、問題描述

前段時間遇到一個工單,客戶反饋,只要進入訂單列表界面 1~2 秒,客戶端就會 Crash,訂單列表界面示意以下:html

訂單示意圖

1.二、問題分析

因爲是客戶投訴的 Bug,沒有 Debug 信息,先猜想各類狀況,數組越界/後臺傳 nil 值/內存泄露/ KVO 賦未定義值等等;然而通過仔細分析模擬逐個排除了上述可能,仍查找不到 Crash 緣由,百思不得其解。ios

排除了代碼的問題,只有多是數據問題了,猜想是異常的圖片/數據解析出現的問題,因而抽取用戶訂單數據分析,發現有 2 張尺寸很是大的 JPEG 圖片,尺寸達到了 15000*8000 的像素,瞬間想明白了怎麼回事,像素總量達到了一億兩千萬,猜想是圖片解壓縮到內存後佔用內存過大,致使系統內存緊張,所以系統殺死了 App 進程。數組

1.三、問題驗證

驗證是否因大尺寸圖片引發的錯誤。驗證過程以下:瀏覽器

寫一個相似上面訂單列表的 Demo,點擊 Cell 逐個加載大圖圖片,測試用的手機爲 iPhone 7P,圖片尺寸爲(15000px*15000px),點擊加載第二張圖片就發生了 Crash,通常狀況下,APP 佔用系統內存 60% 左右就會被殺死進程。iPhone 7P 加載大圖後的內存截圖以下:服務器

Demo初始佔用的內存

加載一張7000×7000大圖內存

加載兩張7000×7000大圖內存

Tips: 不一樣手機因爲內存和屏幕不同,內存超限 App 發生 Crash 的條件不同,其中 iPhone 6P 是最容易 Crash 的,由於它有 5.5 寸的屏幕,卻只有 1G 內存,加載 Assets.xcassets 圖片時會加載 3x 圖片,同一張網絡圖片,UIImageView 佈局通常會按照比例放大,大屏手機圖片會放大,解碼後佔用內存也就更大。網絡

1.四、解決方案

  • 約定大於配置,上傳圖片也要遵照必定的約定。 基於 SDWebImage/YYImage 等第三方庫加載超大圖引發的崩潰,可經過修改源碼解決,但不建議這樣作;修改源碼可能會引發其餘 Bug,並且大圖畢竟是少數,不必對全部圖片都進行判斷,個別大圖單獨處理便可。按照必定約定,經過管理平臺限定上傳圖片尺寸大小,增長 APP 流暢度的同時,還能減小用戶流量損失,此爲最佳方案。
  • 縮放圖片尺寸。 若是是展現整張圖片,不須要展現圖片細節,受限於屏幕分辨率,太大尺寸的圖片是沒有意義的;若是須要作相似於圖片瀏覽器,可對圖片進行放大縮小操做的需求,大圖預覽的時候可加載縮略圖,展現的時候切片處理。

1.五、iOS 圖片解碼

咱們常見的圖片格式例如 PNG/JPG/GIF 等格式都屬於圖像壓縮格式,解壓爲位圖後佔用的內存會很是大。app

假設 iOS 系統從磁盤加載一張圖片,首先將文件數據從磁盤讀到內存中,此時在內存中仍舊是壓縮格式,只有在須要的時機,纔會把圖片解碼爲無壓縮的位圖格式,最後 Core Animation 使用未壓縮的位圖數據渲染 UIImageView 的圖層。框架

將壓縮的圖片數據解碼成未壓縮的位圖形式,這是一個耗時的 CPU 操做,SDWebimage/YYImage 等第三方框架通常都會提早異步強制解碼圖片,保證了 UI 界面的流暢性。異步

1.六、圖片解碼佔用內存計算

圖片解碼後會佔用多少內存呢?其實這個很好計算,蘋果手機採用 24 位真彩色顯示圖像,也就是 24bit(3 字節,RGB 紅綠藍三原色分別佔用 8bit,每一個顏色有 256 種狀態),若是是不包含 Alpha 通道(透明度)的 RGB 圖片,那每一個像素佔用的就是 3 字節,15000px*15000px*3Byte = 644MB,若是是包含透明度的 RGBA 圖片,則爲 15000px*15000px*4Byte = 858MB,如圖2所示,加載一張長和寬 15000px 的圖片,內存暴增 858MB。佈局

1.七、圖片縮放最優選擇

最經常使用的圖片縮放方法是使用 CGContextUIGraphicsGetImageFromCurrentImageContext 方法對圖片進行裁剪縮放,可以知足大部分需求。但若是是處理多張大圖,這時候就須要優化縮放速度了,可經過 Image I/O 框架對圖片進行縮放,在工程中添加 Image I/O Framework,而後在須要使用的地方 #import <ImageIO/ImageIO.h> 便可,示例代碼以下:

//maxPixelSize MUST BE a valid value.
+ (UIImage *)thumbImageFromLargeFile:(NSString *)filePath withConfirmedMaxPixelSize:(CGFloat)maxPixelSize
{
    // Create the image source (from path)
    CGImageSourceRef src = CGImageSourceCreateWithURL((__bridge CFURLRef) [NSURL fileURLWithPath:filePath], NULL);
    
    // Create thumbnail options
    CFDictionaryRef options = (__bridge CFDictionaryRef) @{
                                                           (id) kCGImageSourceCreateThumbnailWithTransform : @YES,
                                                           (id) kCGImageSourceCreateThumbnailFromImageAlways : @YES,
                                                           (id) kCGImageSourceThumbnailMaxPixelSize : @(maxPixelSize)
                                                           };
    // Generate the thumbnail
    CGImageRef thumbnail = CGImageSourceCreateThumbnailAtIndex(src, 0, options);
    CFRelease(src);
    
    UIImage *image = [[UIImage alloc] initWithCGImage:thumbnail];
    CFRelease(thumbnail);
    return image;
}

複製代碼

2、夏令時引發某些時間段轉換爲 NSDate 爲 nil 問題的解決方案

2.一、問題描述

開發中咱們常常會處理用戶的生日,例以下面的代碼,將用戶生日轉換爲NSDate,例以下面的代碼:

NSString *birthStr = @"1986-05-04";
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:@"yyyy-MM-dd"];
NSDate * birDate = [formatter dateFromString:birthStr];
NSLog(@"timeStr to date is %@ %@", birthStr, birDate);
複製代碼

這時候咱們會驚奇的發現,birDate 爲 nil ?嗯,nil。

2.二、問題分析

經過Google搜索及測試,最終定位在了夏令時問題上。

我國解放前幾年在部分地區也曾實行過夏令時。1986年4月,中央有關部門發出「在全國範圍內實行夏時制的通知」,具體做法是:每一年從四月中旬第一個星期日的凌晨2時整(北京時間),將時鐘撥快一小時,即將錶針由2時撥至3時,夏令時開始;到九月中旬第一個星期日的凌晨2時整(北京夏令時),再將時鐘撥回一小時,即將錶針由2時撥至1時,夏令時結束。從1986年到1991年的六個年度,除1986年因是實行夏時制的第一年,從5月4日開始到9月14日結束外,其它年份均按規定的時段施行。1992年起,夏令時暫停實行。

看完這段描述應該就明白緣由了,在中國東八時區時區,某些時間段是不存在的,例如"1988-04-10 00-00-00"至"1988-04-10 01-00-00"中間的時間段。

2.三、解決方案

既然是時區引發的問題,那就把時區轉換爲 UTC 或 GMT 的時區便可。

NSString *birthStr = @"1988-04-10 00-00-00";
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setTimeZone:[NSTimeZone timeZoneWithName:@"GMT"]];// 零時區
[formatter setDateFormat:@"yyyy-MM-dd HH-mm-ss"];
NSDate * birDate = [formatter dateFromString:birthStr];
NSLog(@"BirthStr convert to NSDate is %@", birDate);
複製代碼

Tips: 不要用模擬器測試,模擬器測試結果不正確

3、日期格式 YYYY-MM-dd 和 yyyy-MM-dd 區別

3.一、問題描述

開發中遇到有人使用 YYYY-MM-dd 處理時間格式,以爲不對又說不出爲何,就調研了一下。

大多數狀況下,設置時間格式 YYYY-MM-dd 和 yyyy-MM-dd 轉換的日期是同樣的,只有當到達一些特色的時間節點,跨年時使用 "YYYY-MM-dd" 可能會出現差一年的問題。以下代碼所示:

// 原始的日期字符串
NSString *orginDateStr = @"2015-12-28";
// 轉換爲NSDate
NSDateFormatter *orginFormatter = [[NSDateFormatter alloc] init];
[orginFormatter setDateFormat:@"yyyy-MM-dd"];
NSDate * orginDate = [orginFormatter dateFromString:orginDateStr];
NSLog(@"orginFormatter: orginDate is %@", orginDate);

// 若是用YYYY將orginDate轉換回字符串時就出現了問題
NSDateFormatter *weekFormatter = [[NSDateFormatter alloc] init];
[weekFormatter setDateFormat:@"YYYY-MM-dd"];
NSString *weekDateStr = [weekFormatter stringFromDate:orginDate];
NSLog(@"weekFormatter: weekDateStr is %@", weekDateStr);  
複製代碼

打印結果,相差一年:

orginFormatter: orginDate is Mon Dec 28 00:00:00 2015

weekFormatter: weekDateStr is 2016-12-28

3.二、問題分析

咱們先來理解 YYYY 和 yyyy 的區別:

「YYYY format」 是 「ISO week numbering system」

「yyyy format」 是 「Gregorian Calendar(公曆)」

「YYYY specifies the week of the year (ISO) while yyyy specifies the calendar year (Gregorian)」

yyyy specifies the calendar year whereas YYYY specifies the year (of 「Week of Year」), used in the ISO year-week calendar.

也就是說轉換爲日期時,DateFormatter若是是YYYY格式的話,若是1月1日是星期一,星期二,星期三或星期四,它是在01周。若是一月1日是星期五,星期六或星期日,它在前一年的52周或53周。

蘋果官方文檔說使用YYYY是常見錯誤,正確的應該是使用yyyy格式,官方文檔解釋以下:

It uses yyyy to specify the year component. A common mistake is to use YYYY. yyyy specifies the calendar year whereas YYYY specifies the year (of 「Week of Year」), used in the ISO year-week calendar. In most cases, yyyy and YYYY yield the same number, however they may be different. Typically you should use the calendar year.

The representation of the time may be 13:00. In iOS, however, if the user has switched 24-Hour Time to Off, the time may be 1:00 pm.

3.三、解決方案

使用正確的時間格式 yyyy-MM-dd 來處理日期時間。

4、iOS 局部 BOOL 變量隨機值

4.一、問題描述

測試給一個小夥伴提了一個Bug,點擊一個功能時會不定時出現問題,可以復現,但不是每次都出現。以下代碼所示:

BOOL isSuccess;
if (isSuccess) {
    NSLog(@"success");
}else{
    NSLog(@"failed");
}
複製代碼

測試結果:在 Debug 環境下真機和模擬器都是 failed,但打包成出來安裝後可能爲 success 也多是 failed 了。

4.二、問題分析

很明顯是局部變量 isSuccess 出現了隨機值致使的,雖然我我的平時的習慣是聲明遍歷必定會初始化,但 Debug 模式下正常,打包後就出現隨機值的緣由仍是不清楚,因而調研了一下。

在 ARC 環境下,本地對象建立若是未初始化,指針會指向默認值 nil;可是相似 BOOL 的非對象類型的局部變量,未初始化時會指向最後一次寫入該地址的內容,可能爲任意值,也就是垃圾值,出現隨機值也就不稀奇了。

4.三、解決方案

建立變量時要養成初始化的好習慣,尤爲是基本數據類型,例如:

BOOL isSuccess = NO; 
int a = 0;
複製代碼

5、iPhone 用戶名包含特殊符號引發的後臺 Crash

5.一、問題描述

遇到一個工單,客戶反饋沒法正常進入 App,進入後就報錯,還反饋了機型、系統版本,App 版本等信息。

排查代碼邏輯沒有問題,找到相同系統的機型,相同 App 版本測試沒有問題。

期間也回覆了用戶軟件沒有問題,但這個用戶持之以恆,最終給這個用戶發了一個 Debug 版本,報錯時 Debug 日誌展現在界面上,複製粘貼發過來。最終問題定位在了用戶名上面,這位用戶的用戶名相似於這樣的 &&###???###&&。

5.二、問題分析

這時候可能已經想明白怎麼回事了,特殊符號轉義引發的後臺Bug。例如一些特殊的符號,例如 !#$&'()*+,/:;=?@[] 這些特殊符號,拼接在 URL 或者 Body 裏面,傳送到後臺時均可能引發轉義,不能正常解析,不一樣的後臺表現邏輯不一致。

5.三、解決方案

既然是特殊字符引發的,在網絡傳輸過程當中,對特殊字符進 URLEncode 便可,服務器接收到進行 URLDecode 便可。

// 用戶手機設置的用戶名
NSString *userPhoneName = @"abc&&&???dd**%###!!!";

// 設置須要轉義的特殊字符,例如@"/+=\n"
NSString *characterSetStr = @"?!@#$^&%*+,:;='\"`<>()[]{}/\\| ";
NSCharacterSet *characterSet = [[NSCharacterSet characterSetWithCharactersInString:characterSetStr] invertedSet];
// 返回轉義後的字符串
NSString *urlEncodeStr = [userPhoneName stringByAddingPercentEncodingWithAllowedCharacters:characterSet];
NSLog(@"UserPhoneName Encoding is %@",urlEncodeStr);

// 移除百分號轉義
NSString *removeEncodingStr = urlEncodeStr.stringByRemovingPercentEncoding;
NSLog(@"UserPhoneName removeEncoding is %@",removeEncodingStr);
複製代碼

打印結果:

UserPhoneName Encoding is abc%26%26%26%3F%3F%3Fdd%2A%2A%25%23%23%23%21%21%21

UserPhoneName removeEncoding is abc&&&???dd**%###!!!

備註: 經歷此次事件,在處理特殊字符問題上留下了深入的印象,不管是處理用戶輸入,仍是取值用戶字符串,都會注意特殊字符的轉義問題了。

6、參考連接

developer.apple.com/library/con…

blog.leichunfeng.com/blog/2017/0…

相關文章
相關標籤/搜索