Cocoa开发优化之道

jopen 6年前

对于写过C/C++ for Unix的童鞋,应该都会知道有“高危函数”的说法, 比如gets、strcpy、strcat……都是典型的高危函数。“高危函数”顾名思义,是存在重大隐患的函数,它并不是不可用或者性能不佳, 而是在某些条件会导致严重甚至致命的错误。高危函数往往先是常用函数,对于它们的使用变得需要足够地小心,否则可能带来灾难性的后果。对于“高危函 数”的处理,大多时候选择使用更为安全的函数去替代它们,也可以自己重新定义实现这些函数。

那么,Ojective-C for Cocoa编程中是否也会存在类似的”高危函数”?尽管作为移动应用,相比较后台服务,其后果的影响可能不可以直接拿来比较。然而借鉴Unix编程的这个 概念,了解一些带有隐患的接口,规范使用,不失为提高移动应用编程质量的好方法。考虑Ojective-C习惯用语,我们可以将它们称为“埋坑方法”,以 此提醒自己不要重复踩坑。

 

1、NSArray类

- (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的一个主因。

代码:
Cocoa开发优化之道
Log:
Cocoa开发优化之道
<
</div>

解决办法:一是规范用法,使用前,先做好index越界判断和过滤;二是通过swizzle(《Objective-C与Runtime》)或者继承等方式对objectAtIndex:插入index是否越界判断和过滤。方案二直接适用于应用全局,改动可以说很小,弊端也明显,会带来额外计算量。 

<

2、NSMutableDictionary类

- (void)setObject:(id)anObject forKey:(id<NSCopying>)aKey;

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>

setObject:forKey: 同样是一个普遍用到的字典方法,上面的参数说明也说得比较清楚,当key和value为nil时,方法会抛异常并crash,因此,实践 中,setObject:forKey:成了objectAtIndex:之外的crash另外一个主因。

代码:
Cocoa开发优化之道
Log:
Cocoa开发优化之道
<

解决办法:一是使用方法前,先做好判空兼容逻辑;二是通过合适的机制比如swizzle(《Objective-C与Runtime》)或者继承插入判空兼容逻辑。总的来说,对于老项目,方案二是一个投入小回报高做法;

<

3、UIView类

- (void)setFrame:(CGRect)rect;

setFrame:UIKit 中最基本的类UIView的属性,用于控制视图的布局(layout),一般来说,该方法的使用不会有什么问题。只是在UILabel、UIButton 这些带text显示的类里面,如果rect参数没有取整,设置了带小数点的值,那么系统做文字渲染的时候可能会出现模糊(不锐利)的情况,在过去多个 iOS系统版本都能发现这样的问题。

解决办法:可以针对UILabel、UIButton、甚至UITableViewCell这些带text显示的类做setFrame:的overrride,或者引入safe方法,用floor、floorf、ceil、ceilf等先做取整处理。

<

4、UIViewController类

- (UIView *)view;

这是一个get方法,是UIViewController用来获取自身的底View或者说主View的。如果按照我们平时的使用,想想也不会觉 得该方法有 什么不妥之处,确实是,view的问题并不在自身。不知道大家在类似initWithNibName:bundle:这些 UIViewController的初始化方法中,会不会对view做一个self.view或者viewController.view的引用操作,如 果有,不妨断点跟踪下,你会发现: UIViewController的初始化方法中,第一次 引用view的时候,系统会自动创建一个空白的view,同时会同步先调用viewDidLoad方法,再返回view对象。这时候大家可能看出问题所 在,init初始化方法 ->loadView->viewDidLoad,这是大家熟悉的UIKit的加载流程,大家在做布局,做逻辑时候,也往往是约定这样流程的 前提下做的,那么viewDidLoad在init被提前调用,就可能打乱了一些原来的布局和逻辑,出现莫名其妙的bug。这样的bug并非是假想出来, 而是实践中实打实见过一次又一次的踩坑。。。
代码:
Cocoa开发优化之道
Log:
Cocoa开发优化之道<

解决办法:如果说从代码上去 规避的话,也还没有什么想到好的办法,更多的是依靠攻城师自己的规范和意识吧:在init初始化方法里面,尽量不要提前引用view去做subviews 的layout,尽量把setFrame和addSubview等操作放到loadView或者viewDidLoad当中做。

<

5、NSData类

- (BOOL)writeToFile:(NSString *)path atomically:(BOOL)useAuxiliaryFile;

这个方法大家都不会陌生,是data数据对象转二进制文件的快捷方法。那么,写文件能有什么问题?或许有人猜到了,path,对,路径上,通过简单的实验,就可以确定该方法不会自动生成路径中间的目录,如果路径中某个目录不存在,则方法return NO,写文件失败。

解决办法:一方面,目录路径生成接口需要做有效性检查,发现目录不存在,自动创建;另一方面,通过继承或者代理等方式插入目录有效性检查的相关逻辑;

 <

6、UIColor类

+ (UIColor *)colorWithPatternImage:(UIImage *)image;

这 是一个挺有意思的类方法,我们可以根据一张图片的效果去生成一个Color对象,从而以图片的内容去渲染背景色。该方法经常对backgroundColor做设置来实现背景图片效果。使用该方法的问题在于,图片本身的size和UI控件的size需要保持一致性,因为该方法出来 的图片效果并不支持缩放等特性,仅仅平铺,所以,如果控件发生微调,图片没有跟进调整,往往就是视觉上的bug。
代码:
Cocoa开发优化之道
效果:
Cocoa开发优化之道
代码:
Cocoa开发优化之道
效果:
Cocoa开发优化之道
<

解决办法:建议尽量只用于适合图片平铺的场景,避免其它需要自适应图片的场景使用该方法。

<

7、NSString类

+ (instancetype)stringWithFormat:(NSString *)format;

- (instancetype)initWithFormat:(NSString *)format;

</div>

这 两个方法不用多说,最常用的格式化string方法,存在的问题大家或多或少也会接触到。当格式化参数为对象类型的时候,比如[NSString stringWithFormat:@"%@", object]; 对象可能会是空指针,导致出现@”NULL”这样的输出结果。 

解决办法:当然,由于方法使用数量过于大,我们不可能让使用场景确保不为空,更多地可以通过swizzle、继承、扩展等方法内部加以避免,以下就是用扩展方式做的过滤(由于这种方式效率不佳,并不适合原方法替换,所以只做了扩展,可用于UI显示时候用):

Cocoa开发优化之道

<

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.

</div> </div> </div> </div> </div> </div> </div> </div> </div> </div> </div> </div> </div> </div> </div>

上面一段描述已经很好说明该方法的用途,它 埋下坑就是最后一句提到的——不可以直接invoke该方法,系统会在方法invoke前后做上下文的处理,应用自己直接invoke的话,上下文没有 ready,可能会导致意想不到的情况,所以需要用setNeedsLayout和layoutIfNeeded间接invoke。

<

9、UIView类

- (void)drawRect;

</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>

类似layoutSubviews,不可以直接invoke该方法,系统会在方法 invoke前后处理上下文,应用自己直接invoke的话,上下文没有ready,导致draw rect失败,需要用setNeedsDisplay和setNeedsDisplayInRect间接invoke。

<

</div> </div> </div> </div> </div> </div> </div>
10、CALayer类

- (void)setCornerRadius;

</div>

这是一个能对视图的可视边界快捷生成圆角效果的方法,Apple每年推陈出新的交互指导文档上,iOS7之后圆角将成为新流行的设计语言,圆角相关的设计相信会被大量运用。由于使用便捷,所以该方法很受攻城师欢迎,然而实测该方法会有明显的增加渲染的工作量,最主要原因是触发了离屏渲染的逻辑,运用到tableview的cell上面,可能会影响滚动的流畅度,甚至明显发生卡顿。

解决办法:一是:简单的圆角可以采用双视图叠加的方式,用上面一层视图固定承担圆角遮罩的效果,避免重复渲染;二是:原来,视图的图层(CALayer)设计上,已经支持遮罩的子图层属性,我们同样可以很方便的利用该属性(《巧用Layer做遮罩效果》)(此方案无优势)

>>>>>>>>>>>>>>>>>> 2014-11-28 >>>>>>>>>>>>>>>>>>
经过反复验证,layer的setMask和setCornerRadius的效 率其实接近,方案二无优势,它们都遇到相同的问题——触发了离屏渲染(Offscreen Rendering),严重会导致滑动卡顿。其实,遇到类似流畅度问题,还可以通过“压缩图片”的方向去尝试优化。
<<<<<<<<<<<<<<<<<<< 2014-11-28 <<<<<<<<<<<<<<<<<<<

<

11、NSURL类

+ (instancetype)URLWithString:(NSString *)URLString;

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.

这是iOS上的基础类,提供url对象的操作实例,method是熟悉的,但对于参数的定义就未必。从Parameters的说明可以看 出,method对于输入的string要求必须是符合RFC 2396的规范,即空格等保留字需要按百分号编码(url encode)。同时解析(url decode)是支持RFC 1738的,即遇到+自动转义为空格。对 于大部分的保留字,比如!*’();:@&=+$,/?#[],URLWithString:还是做了兼容,不影响url对象的创建。但是%、空 格以及非ASCII字符,则会直接返回null。%和非ASCII字符一般问题不大,因为涉及这些字符的参数值往往都会被及时发现并做url encode处理,但是,空格比较特殊,它可能一开始不会被发现,然后某种条件下在参数值的随机字符串中间出现(比如例子中的guid),当然这是小概率 事件,然而当出现的时候,后果是非常严重,url无法生成意味着网络访问直接失败。

代码:

Cocoa开发优化之道

Log:

Cocoa开发优化之道

<

</div>

解决办法:最简单有效的方式是借鉴NSString类的NULL问题处理方式,针对输入做safe method加以兼容过滤。

<

从2007年1月份的到现在,iOS的发展历经近8年8大版本(App应用SDK从第2大版本开始),每一个版本都带来大量的新特性和New API。我相信每个iOS攻城师都能从自身长期的实践中总结出自己的“埋坑方法”。我也相信发现问题,解决了问题,就是意味着往前走一步,往更好用户体验 推进。因此,为极致的产品和可靠地项目,找坑,补坑,记之,share之。

来自: http://springox.w18.net/2015/09/04/cocoa开发优化之道/