Cocoa開發優化之道
對于寫過C/C++ for Unix的童鞋,應該都會知道有“高危函數”的說法, 比如gets、strcpy、strcat……都是典型的高危函數。“高危函數”顧名思義,是存在重大隱患的函數,它并不是不可用或者性能不佳, 而是在某些條件會導致嚴重甚至致命的錯誤。高危函數往往先是常用函數,對于它們的使用變得需要足夠地小心,否則可能帶來災難性的后果。對于“高危函 數”的處理,大多時候選擇使用更為安全的函數去替代它們,也可以自己重新定義實現這些函數。
那么,Ojective-C for Cocoa編程中是否也會存在類似的”高危函數”?盡管作為移動應用,相比較后臺服務,其后果的影響可能不可以直接拿來比較。然而借鑒Unix編程的這個 概念,了解一些帶有隱患的接口,規范使用,不失為提高移動應用編程質量的好方法。考慮Ojective-C習慣用語,我們可以將它們稱為“埋坑方法”,以 此提醒自己不要重復踩坑。
1、NSArray類
</div>- (id)objectAtIndex:(NSUInteger)index;
</div>Parameters
index
An index within the bounds of the array.
Return Value
The object located at index.
objectAtIndex:是一個非常常用的NSArray容器方法,當index大于等于count的時候,方法會crash,對,會崩潰,原因是越界。因為NSArray使用實在非常普遍,objectAtIndex:成了應用crash的一個主因。
解決辦法:一是規范用法,使用前,先做好index越界判斷和過濾;二是通過swizzle(《Objective-C與Runtime》)或者繼承等方式對objectAtIndex:插入index是否越界判斷和過濾。方案二直接適用于應用全局,改動可以說很小,弊端也明顯,會帶來額外計算量。
<
- (void)setObject:(id)anObject forKey:(id<NSCopying>)aKey;
</div>Parameters
anObject
The value for aKey. A strong reference to the object is maintained by the dictionary.
IMPORTANT
Raises an NSInvalidArgumentException if anObject is nil. If you need to represent a nil value in the dictionary, use NSNull.
aKey
The key for value. The key is copied (using copyWithZone:; keys must conform to the NSCopying protocol). If aKey already exists in the dictionary, anObject takes its place.
IMPORTANT
</div> </div>Raises an NSInvalidArgumentException if aKey is nil.
</div> </div>setObject:forKey: 同樣是一個普遍用到的字典方法,上面的參數說明也說得比較清楚,當key和value為nil時,方法會拋異常并crash,因此,實踐 中,setObject:forKey:成了objectAtIndex:之外的crash另外一個主因。
解決辦法:一是使用方法前,先做好判空兼容邏輯;二是通過合適的機制比如swizzle(《Objective-C與Runtime》)或者繼承插入判空兼容邏輯。總的來說,對于老項目,方案二是一個投入小回報高做法;
<
3、UIView類
- (void)setFrame:(CGRect)rect;
</div>setFrame:UIKit 中最基本的類UIView的屬性,用于控制視圖的布局(layout),一般來說,該方法的使用不會有什么問題。只是在UILabel、UIButton 這些帶text顯示的類里面,如果rect參數沒有取整,設置了帶小數點的值,那么系統做文字渲染的時候可能會出現模糊(不銳利)的情況,在過去多個 iOS系統版本都能發現這樣的問題。
解決辦法:可以針對UILabel、UIButton、甚至UITableViewCell這些帶text顯示的類做setFrame:的overrride,或者引入safe方法,用floor、floorf、ceil、ceilf等先做取整處理。
<
4、UIViewController類
- (UIView *)view;
</div>解決辦法:如果說從代碼上去 規避的話,也還沒有什么想到好的辦法,更多的是依靠攻城師自己的規范和意識吧:在init初始化方法里面,盡量不要提前引用view去做subviews 的layout,盡量把setFrame和addSubview等操作放到loadView或者viewDidLoad當中做。
<
5、NSData類
- (BOOL)writeToFile:(NSString *)path atomically:(BOOL)useAuxiliaryFile;
</div>這個方法大家都不會陌生,是data數據對象轉二進制文件的快捷方法。那么,寫文件能有什么問題?或許有人猜到了,path,對,路徑上,通過簡單的實驗,就可以確定該方法不會自動生成路徑中間的目錄,如果路徑中某個目錄不存在,則方法return NO,寫文件失敗。
解決辦法:一方面,目錄路徑生成接口需要做有效性檢查,發現目錄不存在,自動創建;另一方面,通過繼承或者代理等方式插入目錄有效性檢查的相關邏輯;
<
6、UIColor類
+ (UIColor *)colorWithPatternImage:(UIImage *)image;
</div>解決辦法:建議盡量只用于適合圖片平鋪的場景,避免其它需要自適應圖片的場景使用該方法。
<
7、NSString類
+ (instancetype)stringWithFormat:(NSString *)format;
- (instancetype)initWithFormat:(NSString *)format;
</div> </div>這 兩個方法不用多說,最常用的格式化string方法,存在的問題大家或多或少也會接觸到。當格式化參數為對象類型的時候,比如[NSString stringWithFormat:@"%@", object]; 對象可能會是空指針,導致出現@”NULL”這樣的輸出結果。
解決辦法:當然,由于方法使用數量過于大,我們不可能讓使用場景確保不為空,更多地可以通過swizzle、繼承、擴展等方法內部加以避免,以下就是用擴展方式做的過濾(由于這種方式效率不佳,并不適合原方法替換,所以只做了擴展,可用于UI顯示時候用):
<
8、UIView類
- (void)layoutSubviews;
Discussion
The default implementation of this method does nothing on iOS 5.1 and earlier. Otherwise, the default implementation uses any constraints you have set to determine the size and position of any subviews.
……
You should not call this method directly. If you want to force a layout update, call the setNeedsLayout method instead to do so prior to the next drawing update. If you want to update the layout of your views immediately, call the layoutIfNeeded method.
上面一段描述已經很好說明該方法的用途,它 埋下坑就是最后一句提到的——不可以直接invoke該方法,系統會在方法invoke前后做上下文的處理,應用自己直接invoke的話,上下文沒有 ready,可能會導致意想不到的情況,所以需要用setNeedsLayout和layoutIfNeeded間接invoke。
</div><
9、UIView類
- (void)drawRect;
</div> </div>Discussion
The default implementation of this method does nothing. Subclasses that use technologies such as Core Graphics and UIKit to draw their view’s content should override this method and implement their drawing code there. You do not need to override this method if your view sets its content in other ways. For example, you do not need to override this method if your view just displays a background color or if your view sets its content directly using the underlying layer object.
……
This method is called when a view is first displayed or when an event occurs that invalidates a visible part of the view. You should never call this method directly yourself. To invalidate part of your view, and thus cause that portion to be redrawn, call the setNeedsDisplay or setNeedsDisplayInRect: method instead.
</div> </div>類似layoutSubviews,不可以直接invoke該方法,系統會在方法 invoke前后處理上下文,應用自己直接invoke的話,上下文沒有ready,導致draw rect失敗,需要用setNeedsDisplay和setNeedsDisplayInRect間接invoke。
<
</div> </div> </div> </div> </div> </div> </div> </div>- (void)setCornerRadius;
</div> </div>這是一個能對視圖的可視邊界快捷生成圓角效果的方法,Apple每年推陳出新的交互指導文檔上,iOS7之后圓角將成為新流行的設計語言,圓角相關的設計相信會被大量運用。由于使用便捷,所以該方法很受攻城師歡迎,然而實測該方法會有明顯的增加渲染的工作量,最主要原因是觸發了離屏渲染的邏輯,運用到tableview的cell上面,可能會影響滾動的流暢度,甚至明顯發生卡頓。
解決辦法:一是:簡單的圓角可以采用雙視圖疊加的方式,用上面一層視圖固定承擔圓角遮罩的效果,避免重復渲染;二是:原來,視圖的圖層(CALayer)設計上,已經支持遮罩的子圖層屬性,我們同樣可以很方便的利用該屬性(《巧用Layer做遮罩效果》)(此方案無優勢)
<
11、NSURL類
+ (instancetype)URLWithString:(NSString *)URLString;
</div>Parameters
URLString | The URL string with which to initialize the NSURL object. Must be a URL that conforms to RFC 2396. This method parses URLString according to RFCs 1738 and 1808. | </tr> </tbody> </table>