好的代码有一些特性:简明,自我解释,优秀的组织,良好的文档,良好的命名,优秀的设计以及可以被久经考验。
本文参考若干优秀的Object-C编程规范文档。写作目的亦在多人开发时,统一代码风格与命名方式的规范。并以减少错误的产生,提高性能,降低维护成本。
苹果命名约定应坚持尽可能遵守,特别是那些涉及到内存管理规则的地方.尽量使用描述方法和变量名的方式
应该是:
UIButton *settingsButton;
而非:
UIButton *setBut;
驼峰法命令,且为了代码清晰.应以相关类名作为前缀
推荐:
static const NSTimeInterval ZOCSignInViewControllerFadeOutAnimationDuration= 0.4;
而非:
static const NSTimeInterval fadeOutTime = 0.4
推荐使用常量来代替字符串字面值和数字,这样能够方便复用,而且可以快速修改而不需要查找和替换。常量应该用static声明为静态常量,而不要用#define,除非它明确的作为一个宏来使用。
推荐:
static NSString * const ZOCCacheControllerDidClearCacheNotification= @"ZOCCacheControllerDidClearCacheNotification";
static const CGFloat ZOCImageThumbnailHeight =50.0f;
不推荐:
#define CompanyName @"Apple Inc."
#define magicNumber 42
常量应该在头文件中以这样的形式暴露给外部:
extern NSString *const ZOCCacheControllerDidClearCacheNotification;
并在实现文件中为它赋值。
只有公有的常量才需要添加命名空间作为前缀。尽管实现文件中私有常量的命名可以遵循另外一种模式,你仍旧可以遵循这个规则。
方法名与方法类型(-/+符号)之间应该以空格间隔。方法段之间也应该以空格间隔(以符合Apple风格)。参数前应该总是有一个描述性的关键词。
尽可能少用"and"这个词。它不应该用来阐明有多个参数,比如下面的initWithWidth:height:这个例子:
推荐:
- (void)setExampleText:(NSString*)textimage:(UIImage *)image;
- (void)sendAction:(SEL)aSelector to:(id)anObject forAllCells:(BOOL)flag;
- (id)viewWithTag:(NSInteger)tag;
- (instancetype)initWithWidth:(CGFloat)width height:(CGFloat)height;
不推荐:
- (void)setT:(NSString*)texti:(UIImage *)image;
- (void)sendAction:(SEL)aSelector :(id)anObject :(BOOL)flag;
- (id)taggedView:(NSInteger)tag;
- (instancetype)initWithWidth:(CGFloat)width andHeight:(CGFloat)height;
- (instancetype)initWith:(int)width and:(int)height; // Never do this.
(4)字面值
使用字面值来创建不可变的NSString,NSDictionary,NSArray,和NSNumber对象。注意不要将nil传进NSArray和NSDictionary里,因为这样会导致崩溃。
例子:
NSArray*names = @[@"Brian",@"Matt",@"Chris",@"Alex",@"Steve",@"Paul"];
NSDictionary*productManagers = @{@"iPhone":@"Kate",@"iPad":@"Kamal",@"Mobile Web":@"Bill"};
NSNumber*shouldUseLiterals = @YES;
NSNumber*buildingZIPCode = @10018;
不要这样:
NSArray*names = [NSArrayarrayWithObjects:@"Brian",@"Matt",@"Chris",@"Alex",@"Steve",@"Paul",nil];
NSDictionary*productManagers = [NSDictionarydictionaryWithObjectsAndKeys:@"Kate",@"iPhone",@"Kamal",@"iPad",@"Bill",@"Mobile Web",nil];
NSNumber*shouldUseLiterals = [NSNumbernumberWithBool:YES];
NSNumber*buildingZIPCode = [NSNumbernumberWithInteger:10018];
如果要用到这些类的可变副本,我们推荐使用NSMutableArray,NSMutableString这样的类。
应该避免下面这样:
NSMutableArray*aMutableArray = [@[]mutableCopy];
上面这种书写方式的效率和可读性的都存在问题。
效率方面,一个不必要的不可变对象被创建后立马被废弃了;虽然这并不会让你的App变慢(除非这个方法被频繁调用),但是确实没必要为了少打几个字而这样做。
可读性方面,存在两个问题:第一个问题是当你浏览代码并看见@[]的时候,你首先联想到的是NSArray实例,但是在这种情形下你需要停下来深思熟虑的检查;另一个问题是,一些新手以他的水平看到你的代码后可能会对这是一个可变对象还是一个不可变对象产生分歧。他/她可能不熟悉可变拷贝构造的含义(这并不是说这个知识不重要)。当然,不存在绝对的错误,我们只是讨论代码的可用性(包括可读性)。
类名应该以三个大写字母作为前缀(双字母前缀为Apple的类预留)。尽管这个规范看起来有些古怪,但是这样做可以减少Objective-C没有命名空间所带来的问题。
属性应该尽可能描述性地命名,避免缩写,并且是小写字母开头的驼峰命名。我们的工具可以很方便地帮我们自动补全所有东西(嗯。。几乎所有的,Xcode的Derived
Data会索引这些命名)。所以没理由少打几个字符了,并且最好尽可能在你源码里表达更多东西。
例子:
NSString*text;
不要这样:
NSString* text;
(注意:这个习惯和常量不同,这是主要从常用和可读性考虑。C++的开发者偏好从变量名中分离类型,作为类型它应该是NSString*(对于从堆中分配的对象,对于C++是能从栈上分配的)格式。)
你应该总是使用setter和getter方法访问属性,除了init和dealloc方法。通常,使用属性让你增加了在当前作用域之外的代码块的可能所以可能带来更多副作用。
你总应该用getter和setter,因为:
使用setter会遵守定义的内存管理语义(strong,weak,copyetc...),这个在ARC之前就是相关的内容。举个例子,copy属性定义了每个时候你用setter并且传送数据的时候,它会复制数据而不用额外的操作。
KVO通知(willChangeValueForKey,didChangeValueForKey)会被自动执行。
更容易debug:你可以设置一个断点在属性声明上并且断点会在每次getter / setter方法调用的时候执行,或者你可以在自己的自定义setter/getter设置断点。
允许在一个单独的地方为设置值添加额外的逻辑。
你应该倾向于用getter:
它是对未来的变化有扩展能力的(比如,属性是自动生成的)。
它允许子类化。
更简单的debug(比如,允许拿出一个断点在getter方法里面,并且看谁访问了特别的getter
它让意图更加清晰和明确:通过访问ivar_anIvar你可以明确的访问self->_anIvar.这可能导致问题。在block里面访问ivar(你捕捉并且retain了self,即使你没有明确的看到self关键词)。
它自动产生KVO通知。
Designated和Secondary初始化方法
一个类应该有且只有一个designated初始化方法,其他的初始化方法(Secondary)应该调用这个designated的初始化方法
当使用setter getter方法的时候尽量使用点符号。应该总是用点符号来访问以及设置属性。
例子:
view.backgroundColor = [UIColororangeColor];
[UIApplicationsharedApplication].delegate;
不要这样:
[viewsetBackgroundColor:[UIColororangeColor]];
UIApplication.sharedApplication.delegate;
使用点符号会让表达更加清晰并且帮助区分属性访问和方法调用
4.懒加载(Lazy Loading)
当实例化一个对象需要耗费很多资源,或者配置一次就要调用很多配置相关的方法而你又不想弄乱这些方法时,我们需要重写getter方法以延迟实例化,而不是在init方法里给对象分配内存。通常这种操作使用下面这样的模板:
- (NSDateFormatter*)dateFormatter {
if(!_dateFormatter) {
_dateFormatter = [[NSDateFormatteralloc]init];
NSLocale*enUSPOSIXLocale = [[NSLocalealloc]initWithLocaleIdentifier:@"en_US_POSIX"];
[_dateFormattersetLocale:enUSPOSIXLocale];
[_dateFormattersetDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSS"];//毫秒是SSS,而非SSSSS
}
return_dateFormatter;
}
5.NSNotification(广播)
当你定义你自己的NSNotification的时候你应该把你的通知的名字定义为一个字符串常量,就像你暴露给其他类的其他字符串常量一样。你应该在公开的接口文件中将其声明为extern的,并且在对应的实现文件里面定义。
因为你在头文件中暴露了符号,所以你应该按照统一的命名空间前缀法则,用类名前缀作为这个通知名字的前缀。
同时,用一个Did/Will这样的动词以及用"Notifications"后缀来命名这个通知也是一个好的实践。
// Foo.h
externNSString*constZOCFooDidBecomeBarNotification
// Foo.m
NSString*constZOCFooDidBecomeBarNotification =@"ZOCFooDidBecomeBarNotification";
随着项目的不断迭代更新,工程中引入的三方库日益增多。如何管理已有三方库,合理的引入新的第三方库成为了一个新的问题。如下将总结我们引入第三方库的规范,并将持续更新。
1.所引入的第三方库尽量选择有人维护更新的。无人维护的库并被证明存在已知缺陷或明显落后于当前版本环境的,请避免使用。
2.兼容性满足当前APP要求的。引用前请确认该库在所需要兼容的版本范围内良好的运行。不会出现崩溃,或较大的外观差异。
3.易于维护的,应用广泛的。最好是业内特定功能的行业标准库。冷门且复杂的三方库会给项目带来不可预估的风险。如需求变更,或出现崩溃问题时将很难修改,也无法找到有价值的参考资料。
4.一致性。同一功能的第三方库在一个项目内只允许用使用一种。以减小维护成本,降低复杂度,统一标准。
5.关于拓展,当三方库不能满足所需功能时,不要直接修改库的代码(虽然它是开源的)。请建立新的子类去继承它或使用category。修改源码会降低库的复用性。
CPU资源消耗原因和解决方案
对象的创建会分配内存、调整属性、甚至还有读取文件等操作,比较消耗CPU资源。尽量用轻量的对象代替重量的对象,可以对性能有所优化。比如CALayer比UIView要轻量许多,那么不需要响应触摸事件的控件,用CALayer显示会更加合适。如果对象不涉及UI操作,则尽量放到后台线程去创建,但可惜的是包含有CALayer的控件,都只能在主线程创建和操作。通过Storyboard创建视图对象时,其资源消耗会比直接通过代码创建对象要大非常多,在性能敏感的界面里,Storyboard并不是一个好的技术选择。
尽量推迟对象创建的时间,并把对象的创建分散到多个任务中去。尽管这实现起来比较麻烦,并且带来的优势并不多,但如果有能力做,还是要尽量尝试一下(懒加载)。如果对象可以复用,并且复用的代价比释放、创建新对象要小,那么这类对象应当尽量放到一个缓存池里复用。
对象的调整也经常是消耗CPU资源的地方。这里特别说一下CALayer:CALayer内部并没有属性,当调用属性方法时,它内部是通过运行时resolveInstanceMethod为对象临时添加一个方法,并把对应属性值保存到内部的一个Dictionary里,同时还会通知delegate、创建动画等等,非常消耗资源。UIView的关于显示相关的属性(比如frame/bounds/transform)等实际上都是CALayer属性映射来的,所以对UIView的这些属性进行调整时,消耗的资源要远大于一般的属性。对此你在应用中,应该尽量减少不必要的属性修改。
当视图层次调整时,UIView、CALayer之间会出现很多方法调用与通知,所以在优化性能时,应该尽量避免调整视图层次、添加和移除视图。
对象的销毁虽然消耗资源不多,但累积起来也是不容忽视的。通常当容器类持有大量对象时,其销毁时的资源消耗就非常明显。同样的,如果对象可以放到后台线程去释放,那就挪到后台线程去。这里有个小Tip:把对象捕获到block中,然后扔到后台队列去随便发送个消息以避免编译器警告,就可以让对象在后台线程销毁了。
NSArray *tmp=self.array;
self.array=nil;
dispatch_async(queue,^{
[tmpclass];
});
视图布局的计算是App中最为常见的消耗CPU资源的地方。如果能在后台线程提前计算好视图布局、并且对视图布局进行缓存,那么这个地方基本就不会产生性能问题了。
不论通过何种技术对视图进行布局,其最终都会落到对UIView.frame/bounds/center等属性的调整上。上面也说过,对这些属性的调整非常消耗资源,所以尽量提前计算好布局,在需要时一次性调整好对应属性,而不要多次、频繁的计算和调整这些属性。
Autolayout是苹果本身提倡的技术,在大部分情况下也能很好的提升开发效率,但是Autolayout对于复杂视图来说常常会产生严重的性能问题。随着视图数量的增长,Autolayout带来的CPU消耗会呈指数级上升。具体数据可以看这个文章:http://pilky.me/36/。如果你不想手动调整frame等属性,你可以用一些工具方法替代(比如常见的left/right/top/bottom/width/height快捷属性),或者使用ComponentKit、AsyncDisplayKit等框架。
如果一个界面中包含大量文本(比如微博微信朋友圈等),文本的宽高计算会占用很大一部分资源,并且不可避免。如果你对文本显示没有特殊要求,可以参考下UILabel内部的实现方式:用[NSAttributedString boundingRectWithSize:options:context:]来计算文本宽高,用-[NSAttributedString
drawWithRect:options:context:]来绘制文本。尽管这两个方法性能不错,但仍旧需要放到后台线程进行以避免阻塞主线程。
如果你用CoreText绘制文本,那就可以先生成CoreText排版对象,然后自己计算了,并且CoreText对象还能保留以供稍后绘制使用。
屏幕上能看到的所有文本内容控件,包括UIWebView,在底层都是通过CoreText排版、绘制为Bitmap显示的。常见的文本控件(UILabel、UITextView等),其排版和绘制都是在主线程进行的,当显示大量文本时,CPU的压力会非常大。对此解决方案只有一个,那就是自定义文本控件,用TextKit或最底层的CoreText对文本异步绘制。尽管这实现起来非常麻烦,但其带来的优势也非常大,CoreText对象创建好后,能直接获取文本的宽高等信息,避免了多次计算(调整UILabel大小时算一遍、UILabel绘制时内部再算一遍);CoreText对象占用内存较少,可以缓存下来以备稍后多次渲染。
当你用UIImage或CGImageSource的那几个方法创建图片时,图片数据并不会立刻解码。图片设置到UIImageView或者CALayer.contents中去,并且CALayer被提交到GPU前,CGImage中的数据才会得到解码。这一步是发生在主线程的,并且不可避免。如果想要绕开这个机制,常见的做法是在后台线程先把图片绘制到CGBitmapContext中,然后从Bitmap直接创建图片。目前常见的网络图片库都自带这个功能。
(8)图像的绘制
图像的绘制通常是指用那些以CG开头的方法把图像绘制到画布中,然后从画布创建图片并显示这样一个过程。这个最常见的地方就是[UIView drawRect:]里面了。由于CoreGraphic方法通常都是线程安全的,所以图像的绘制可以很容易的放到后台线程进行。一个简单异步绘制的过程大致如下(实际情况会比这个复杂得多,但原理基本一致):
-(void)display{
dispatch_async(backgroundQueue,^{
CGContextRefctx=CGBitmapContextCreate(...);
// draw in context...
CGImageRefimg=CGBitmapContextCreateImage(ctx);
CFRelease(ctx);
dispatch_async(mainQueue,^{
layer.contents=img;
});
});
}
相对于CPU来说,GPU能干的事情比较单一:接收提交的纹理(Texture)和顶点描述(三角形),应用变换(transform)、混合并渲染,然后输出到屏幕上。通常你所能看到的内容,主要也就是纹理(图片)和形状(三角模拟的矢量图形)两类。
所有的Bitmap,包括图片、文本、栅格化的内容,最终都要由内存提交到显存,绑定为GPU Texture。不论是提交到显存的过程,还是GPU调整和渲染Texture的过程,都要消耗不少GPU资源。当在较短时间显示大量图片时(比如TableView存在非常多的图片并且快速滑动时),CPU占用率很低,GPU占用非常高,界面仍然会掉帧。避免这种情况的方法只能是尽量减少在短时间内大量图片的显示,尽可能将多张图片合成为一张进行显示。
当图片过大,超过GPU的最大纹理尺寸时,图片需要先由CPU进行预处理,这对CPU和GPU都会带来额外的资源消耗。目前来说,iPhone 4S以上机型,纹理尺寸上限都是4096x4096,
当多个视图(或者说CALayer)重叠在一起显示时,GPU会首先把他们混合到一起。如果视图结构过于复杂,混合的过程也会消耗很多GPU资源。为了减轻这种情况的GPU消耗,应用应当尽量减少视图数量和层次,并在不透明的视图里标明opaque属性以避免无用的Alpha通道合成。当然,这也可以用上面的方法,把多个视图预先渲染为一张图片来显示。
CALayer的border、圆角、阴影、遮罩(mask),CASharpLayer的矢量图形显示,通常会触发离屏渲染(offscreen rendering),而离屏渲染通常发生在GPU中。当一个列表视图中出现大量圆角的CALayer,并且快速滑动时,可以观察到GPU资源已经占满,而CPU资源消耗很少。这时界面仍然能正常滑动,但平均帧数会降到很低。为了避免这种情况,可以尝试开启CALayer.shouldRasterize属性,但这会把原本离屏渲染的操作转嫁到CPU上去。对于只需要圆角的某些场合,也可以用一张已经绘制好的圆角图片覆盖到原本视图上面来模拟相同的视觉效果。最彻底的解决办法,就是把需要显示的图形在后台线程绘制为图片,避免使用圆角、阴影、遮罩等属性。