OC编码规范与性能提升

好的代码有一些特性:简明,自我解释,优秀的组织,良好的文档,良好的命名,优秀的设计以及可以被久经考验。

本文参考若干优秀的Object-C编程规范文档。写作目的亦在多人开发时,统一代码风格与命名方式的规范。并以减少错误的产生,提高性能,降低维护成本。

1.命名规范制度

苹果命名约定应坚持尽可能遵守,特别是那些涉及到内存管理规则的地方.尽量使用描述方法和变量名的方式

(1)控件命名

应该是:

UIButton *settingsButton;

而非:

UIButton *setBut;

(2)常量

驼峰法命令,且为了代码清晰.应以相关类名作为前缀

推荐:

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;

并在实现文件中为它赋值。

只有公有的常量才需要添加命名空间作为前缀。尽管实现文件中私有常量的命名可以遵循另外一种模式,你仍旧可以遵循这个规则。

(3)方法

方法名与方法类型(-/+符号)之间应该以空格间隔。方法段之间也应该以空格间隔(以符合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实例,但是在这种情形下你需要停下来深思熟虑的检查;另一个问题是,一些新手以他的水平看到你的代码后可能会对这是一个可变对象还是一个不可变对象产生分歧。他/她可能不熟悉可变拷贝构造的含义(这并不是说这个知识不重要)。当然,不存在绝对的错误,我们只是讨论代码的可用性(包括可读性)。

(5)类名

类名应该以个大写字母作为前缀(双字母前缀为Apple的类预留)。尽管这个规范看起来有些古怪,但是这样做可以减少Objective-C没有命名空间所带来的问题。

(6)属性命名

属性应该尽可能描述性地命名,避免缩写,并且是小写字母开头的驼峰命名。我们的工具可以很方便地帮我们自动补全所有东西(嗯。。几乎所有的,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通知。

2.初始化方法

Designated和Secondary初始化方法

一个类应该有且只有一个designated初始化方法,其他的初始化方法(Secondary)应该调用这个designated的初始化方法

3.点符号

当使用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";

2.三方库使用规范

随着项目的不断迭代更新,工程中引入的三方库日益增多。如何管理已有三方库,合理的引入新的第三方库成为了一个新的问题。如下将总结我们引入第三方库的规范,并将持续更新。

1.所引入的第三方库尽量选择有人维护更新的。无人维护的库并被证明存在已知缺陷或明显落后于当前版本环境的,请避免使用。

2.兼容性满足当前APP要求的。引用前请确认该库在所需要兼容的版本范围内良好的运行。不会出现崩溃,或较大的外观差异。

3.易于维护的,应用广泛的。最好是业内特定功能的行业标准库。冷门且复杂的三方库会给项目带来不可预估的风险。如需求变更,或出现崩溃问题时将很难修改,也无法找到有价值的参考资料。

4.一致性。同一功能的第三方库在一个项目内只允许用使用一种。以减小维护成本,降低复杂度,统一标准。

5.关于拓展,当三方库不能满足所需功能时,不要直接修改库的代码(虽然它是开源的)。请建立新的子类去继承它或使用category。修改源码会降低库的复用性。

3.性能优化

CPU资源消耗原因和解决方案

(1)对象创建

对象的创建会分配内存、调整属性、甚至还有读取文件等操作,比较消耗CPU资源。尽量用轻量的对象代替重量的对象,可以对性能有所优化。比如CALayer比UIView要轻量许多,那么不需要响应触摸事件的控件,用CALayer显示会更加合适。如果对象不涉及UI操作,则尽量放到后台线程去创建,但可惜的是包含有CALayer的控件,都只能在主线程创建和操作。通过Storyboard创建视图对象时,其资源消耗会比直接通过代码创建对象要大非常多,在性能敏感的界面里,Storyboard并不是一个好的技术选择。

尽量推迟对象创建的时间,并把对象的创建分散到多个任务中去。尽管这实现起来比较麻烦,并且带来的优势并不多,但如果有能力做,还是要尽量尝试一下(懒加载)。如果对象可以复用,并且复用的代价比释放、创建新对象要小,那么这类对象应当尽量放到一个缓存池里复用。

(2)对象调整

对象的调整也经常是消耗CPU资源的地方。这里特别说一下CALayer:CALayer内部并没有属性,当调用属性方法时,它内部是通过运行时resolveInstanceMethod为对象临时添加一个方法,并把对应属性值保存到内部的一个Dictionary里,同时还会通知delegate、创建动画等等,非常消耗资源。UIView的关于显示相关的属性(比如frame/bounds/transform)等实际上都是CALayer属性映射来的,所以对UIView的这些属性进行调整时,消耗的资源要远大于一般的属性。对此你在应用中,应该尽量减少不必要的属性修改。

当视图层次调整时,UIView、CALayer之间会出现很多方法调用与通知,所以在优化性能时,应该尽量避免调整视图层次、添加和移除视图。

(3)对象销毁

对象的销毁虽然消耗资源不多,但累积起来也是不容忽视的。通常当容器类持有大量对象时,其销毁时的资源消耗就非常明显。同样的,如果对象可以放到后台线程去释放,那就挪到后台线程去。这里有个小Tip:把对象捕获到block中,然后扔到后台队列去随便发送个消息以避免编译器警告,就可以让对象在后台线程销毁了。

NSArray *tmp=self.array;

self.array=nil;

dispatch_async(queue,^{

[tmpclass];

});

(4)布局计算

视图布局的计算是App中最为常见的消耗CPU资源的地方。如果能在后台线程提前计算好视图布局、并且对视图布局进行缓存,那么这个地方基本就不会产生性能问题了。

不论通过何种技术对视图进行布局,其最终都会落到对UIView.frame/bounds/center等属性的调整上。上面也说过,对这些属性的调整非常消耗资源,所以尽量提前计算好布局,在需要时一次性调整好对应属性,而不要多次、频繁的计算和调整这些属性。

(5)Autolayout

Autolayout是苹果本身提倡的技术,在大部分情况下也能很好的提升开发效率,但是Autolayout对于复杂视图来说常常会产生严重的性能问题。随着视图数量的增长,Autolayout带来的CPU消耗会呈指数级上升。具体数据可以看这个文章:http://pilky.me/36/。如果你不想手动调整frame等属性,你可以用一些工具方法替代(比如常见的left/right/top/bottom/width/height快捷属性),或者使用ComponentKit、AsyncDisplayKit等框架。

(6)文本计算

如果一个界面中包含大量文本(比如微博微信朋友圈等),文本的宽高计算会占用很大一部分资源,并且不可避免。如果你对文本显示没有特殊要求,可以参考下UILabel内部的实现方式:用[NSAttributedString boundingRectWithSize:options:context:]来计算文本宽高,用-[NSAttributedString

drawWithRect:options:context:]来绘制文本。尽管这两个方法性能不错,但仍旧需要放到后台线程进行以避免阻塞主线程。

如果你用CoreText绘制文本,那就可以先生成CoreText排版对象,然后自己计算了,并且CoreText对象还能保留以供稍后绘制使用。

(7)文本渲染

屏幕上能看到的所有文本内容控件,包括UIWebView,在底层都是通过CoreText排版、绘制为Bitmap显示的。常见的文本控件(UILabel、UITextView等),其排版和绘制都是在主线程进行的,当显示大量文本时,CPU的压力会非常大。对此解决方案只有一个,那就是自定义文本控件,用TextKit或最底层的CoreText对文本异步绘制。尽管这实现起来非常麻烦,但其带来的优势也非常大,CoreText对象创建好后,能直接获取文本的宽高等信息,避免了多次计算(调整UILabel大小时算一遍、UILabel绘制时内部再算一遍);CoreText对象占用内存较少,可以缓存下来以备稍后多次渲染。

(8)图片的解码

当你用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;

});

});

}

GPU资源消耗原因和解决方案

相对于CPU来说,GPU能干的事情比较单一:接收提交的纹理(Texture)和顶点描述(三角形),应用变换(transform)、混合并渲染,然后输出到屏幕上。通常你所能看到的内容,主要也就是纹理(图片)和形状(三角模拟的矢量图形)两类。

(1)纹理的渲染

所有的Bitmap,包括图片、文本、栅格化的内容,最终都要由内存提交到显存,绑定为GPU Texture。不论是提交到显存的过程,还是GPU调整和渲染Texture的过程,都要消耗不少GPU资源。当在较短时间显示大量图片时(比如TableView存在非常多的图片并且快速滑动时),CPU占用率很低,GPU占用非常高,界面仍然会掉帧。避免这种情况的方法只能是尽量减少在短时间内大量图片的显示,尽可能将多张图片合成为一张进行显示。

当图片过大,超过GPU的最大纹理尺寸时,图片需要先由CPU进行预处理,这对CPU和GPU都会带来额外的资源消耗。目前来说,iPhone 4S以上机型,纹理尺寸上限都是4096x4096,

(2)视图的混合(Composing)

当多个视图(或者说CALayer)重叠在一起显示时,GPU会首先把他们混合到一起。如果视图结构过于复杂,混合的过程也会消耗很多GPU资源。为了减轻这种情况的GPU消耗,应用应当尽量减少视图数量和层次,并在不透明的视图里标明opaque属性以避免无用的Alpha通道合成。当然,这也可以用上面的方法,把多个视图预先渲染为一张图片来显示。

(3)图形的生成。

CALayer的border、圆角、阴影、遮罩(mask),CASharpLayer的矢量图形显示,通常会触发离屏渲染(offscreen rendering),而离屏渲染通常发生在GPU中。当一个列表视图中出现大量圆角的CALayer,并且快速滑动时,可以观察到GPU资源已经占满,而CPU资源消耗很少。这时界面仍然能正常滑动,但平均帧数会降到很低。为了避免这种情况,可以尝试开启CALayer.shouldRasterize属性,但这会把原本离屏渲染的操作转嫁到CPU上去。对于只需要圆角的某些场合,也可以用一张已经绘制好的圆角图片覆盖到原本视图上面来模拟相同的视觉效果。最彻底的解决办法,就是把需要显示的图形在后台线程绘制为图片,避免使用圆角、阴影、遮罩等属性。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容