iOS开发-代码细节优化(长期更新)

96
Eiwodetianna
0.2 2016.04.12 15:11* 字数 4834

代码细节优化是所有开发者一直关注并锻炼的能力,这也是为什么一些技术型团队坚持在做Code Review,我相信每一个专注于技术的开发者都希望自己的代码越来越有质量。写这篇文章的目的是为了记录我在开发中学习总结的一点点关于细节处理的经验,也希望看到此文的人能够积极补充,共同来维护这篇文章。

命名法则

在代码书写方面首先要先来谈谈各种命名的规则。在企业中往往要先统一代码规范,这个做法是值得且有必要的,代码规范可以使项目的代码看起来更加美观整洁,可以降低沟通的成本,可以避免低级错误等多种好处。

1.变量命名法则

比较常用的变量命名法则有三种:驼峰命名法;下划线命名法;帕斯卡命名法。

  • 驼峰命名法:用的最广的命名法,变量名常用此命名,命名由一个单词或多个单词组合而成,首字母小写其余单词首字母大写,如:“myName”;

  • 下划线命名法:每个单词间使用下划线“_”分割,所有字母均小写,如:“my_name”;

  • 帕斯卡命名法:每个单词的首字母均大写的一串字符,与“骆驼命名法”的区别在于前者的首字母大写,后者的首字母小写。如:“MyName”。

ps:这里面需要注意的是变量名尽量不要使用缩写,如我们经常可以看到很多开发者习惯于把根视图控制器写成rootVC或者mainVC等等,而系统给我们提供的却是完整的命名:self.window.rootViewController,假如系统给我们提供的是self.window.rootVC这种形式,以及其他命名方式也这样以非专业词汇的缩写命名,相信很多开发者会看的一头雾水。

2.宏的命名

如果我们在编码中使用了宏来表示一个常量时,通常会把单词的所有字母大写,目的是为了告诉开发者这是一个宏,而不是一个普通的变量,当然如果宏的名称如果由多个单词组成,通常是每个单词之间使用下划线分割开,如:“JXL_ABC_DEF”这种形式。你可以点开系统提供的类的头文件,可以看到iOS SDK中的绝大多数宏也是按照此规则命名。

3.项目名称使用帕斯卡命名法:

Xcode项目的命名个人推荐使用帕斯卡命名法则,可能有人会问,我以其它词汇命名,如中文词汇,空格等都是可以的呀,没错,是可以创建成功并可以正常运行,但是在有些时候我们需要引用工程下某个文件的路径时(如一些SDK的引用)就容易出错了,如功能名中使用了空格,在Xcode设置中引用路径时可能被拆分成两个路径,那么此时通常的解决方案是在引用时填写的文件路径所包含的空格前使用“\”, 但是不如从最根源的地方去规避掉这个问题,避免带来的不必要的麻烦。

4.给类名添加前缀

我们在构建应用程序时,很有可能会有部分代码用于后续的项目,或者发布出去供他人使用,那么在别人使用你的类库或者你引用其他人的类库时很有可能出现相同命名的情况,从而引发出“重复符号错误”(duplicate symbol error),为了避免这种令人恼怒的情况发生,我们应该习惯于给自己的类添加一个前缀,可以是公司名称的缩写,也可以是你个人姓名的缩写,还有可能是框架名称的缩写,Xcode设置类前缀的地方在这里:

填写该设置后你每次创建新的文件都会自动帮你填写上类前缀:

这里需要注意的是,推荐大家使用三个字母以上作为类的前缀,很多开发者都习惯于使用两个字母作为前缀(作者本人以前也是),但是苹果公司保留了两个字母作为类前缀的权利,大家可以发现苹果公司的类都是以框架名缩写并且都是两个字母作为前缀的,为了避免苹果公司将来新发布的框架缩写和你的前缀发生冲突,所以采用三个字母以上的前缀命名方案。不光类名如此,类目及延展或者自定义的结构体等同样推荐使用前缀。

5.类目方法及私有方法添加前缀

  • 方法命名:Objective-C这门语言的语法在其他语言的开发者眼中一直是很怪异的语言,它的方法名除了使用了方括号,方法名长的有够过分,但是正是因为这个特点,我们阅读Objective-C代码的时候就像阅读英文语句一样通俗易懂。尤其是很多人喜欢使用词汇的缩写来命名方法,命名缩写只用于通用专业术语,如 URL,不可自创命名缩写,如 Str、Msg。命名宁可长一些,也不要难于理解。在方法开头通常使用表明执行此方法的目的词汇,如“init”,“sort”,“find”等词汇,案例:
- (NSArray<ObjectType> *)sortedArrayUsingSelector:(SEL)comparator;  // 表明目的是排序

也可能以返回值的类型进行开头(ps:其实也是在表明目的,只不过目的是获取某种数据),如:“string”,“data”,“image”等,案例:

- (NSRange)rangeOfString:(NSString *)searchString;  // 表明获取一个range

如果该方法需要参数,每个参数前最好添加参数提示。如下面两种代码对比。

- (instancetype)init:(CGRect)frame;  // 糟糕的方法命名
- (instancetype)initWithFrame:(CGRect)frame;  // 好的方法命名

一些代表过程监听的方法可能以“谁执行什么过程”这种形式命名,且动作发生之前通常使用“Will”,发生之后使用“Did”,询问是否发生使用“Should”。案例:

- (void)viewDidLoad;
  • 类目方法:如果我们在编码中使用类目给一些系统的类拓展方法,那么推荐给这些方法添加前缀,目的很简单,就是避免和这个类的私有方法或者将来系统可能拓展的方法出现方法名相同的冲突。如我当初在写夜间模式的demo的时候给UIView写了一个类目,拓展的方法名我使用了姓名的缩写“jxl”作为方法的前缀:
- (void)jxl_setDayMode:(DAY_AND_NIGHT_MODE_BLOCK)dayMode nightMode:(DAY_AND_NIGHT_MODE_BLOCK)nightMode;   
  • 私有方法:在项目编码过程中,很有可能会改变我们现有的方法名,那么如果我们更改的是在.h文件中的方法,那么会影响到外部类的使用,很有可能这个外部类的代码是其他开发者负责编写的,然而他不知道你的修改操作,你需要与他及时沟通(ps:直接修改他人类的代码是极其不推荐的做法),如果更改的是私有方法的话,那么就无所谓了,你可以随意更改而不会影响到其他人,所以推荐大家把私有方法添加一个特殊的前缀,这样我们在修改这个方法时通过有无这个前缀来确定修改后所带来的影响。苹果自家的私有方法是使用下划线作为前缀的,所以我们需要避免这种前缀命名防止和系统方法命名冲突所带来的一些莫名其妙的错误,你可以使用下面这种形式:
- (void)p_privateMethodName;

6.其他的命名规则

一些公开的常量通常使用类名作为前缀,同样是避免命名冲突而引发问题。如果是私有的,则使用“k + 类名”作为前缀。同样枚举类型也通常使用类名作为前缀命名,并且枚举值以枚举类型作为前缀,案例:

UIKIT_EXTERN NSString *const UIApplicationDidEnterBackgroundNotification;  // 常量命名

// 枚举类型命名
typedef NS_ENUM(NSInteger, UIViewAnimationTransition) { 
    UIViewAnimationTransitionNone,   // 枚举值命名
    UIViewAnimationTransitionFlipFromLeft, 
    UIViewAnimationTransitionFlipFromRight, 
    UIViewAnimationTransitionCurlUp, 
    UIViewAnimationTransitionCurlDown, 
}; 

代码编写设计

在编写代码的时候,注意一些细节上的处理,如代码编写方式或者API设计会使我们的代码业务逻辑更清晰,可读性更高,或者更方便调试和维护。

1.成员变量 VS Setter&Getter

一些刚刚入门的iOS开发者总是纠结在给属性赋值或者取值的时候,究竟是直接操作成员变量还是通过使用setter或者getter进行赋值取值操作,关于这个问题很多人的观点也不相同,从性能上来说,由于Objective-C的消息机制使用setter和getter效率要比直接操作成员变量的效率低,从内存方面来说,setter和getter都有内存的处理,所以使用起来更安全,一个折中的办法是除Lazy Loading对象外,赋值时使用setter,取值时直接操作成员变量,这样既保障了内存的安全,又提升了效率,而且通常我们的取值操作又多于赋值操作。这里可能有好多开发者习惯所有的属性都使用Lazy Loading的方式去创建,觉得这部分的效率提升可以牺牲掉换来更强的代码逻辑层次,这个问题我会在紧接着的关于纯代码对象创建的编写方式优化里去谈。

2.纯代码对象创建的编写方式优化

使用纯代码创建一个视图对象,下面的方法我们已经再熟悉不过了:

UIView *subView = [[UIView alloc] initWithFrame:[UIScreen mainScreen].bounds];
subView.backgroundColor = [UIColor whiteColor];
[self.view addSubview:subView];

然后这段代码可以转换成另外一种等价写法:

UIView *subView = ({
    UIView *view = [[UIView alloc] initWithFrame:[UIScreen mainScreen].bounds];
    view.backgroundColor = [UIColor whiteColor];
    [self.view addSubview:view];
    view;
});

第二种写法比较少见,知道的人并不多,这种写法为GCC对C的拓展,如今被clang继承,有点像block和内联函数的结合体,它最大的好处是使代码逻辑结构层次划分得更加明显,同时,对于一个无需复用的小段逻辑也免去了重量级的调用函数,而且还有一个好处是,当你想要复制粘贴这段代码时,你仅需要更改一处变量名即可,就上例而言原来的写法需要更改三处变量名。在成员变量 VS Setter&Getter话题我们说到许多开发者习惯使用Lazy Loading的方式创建对象,因为这样逻辑层次很清晰,但是我个人觉得对比这种GCC的写法,同样具有逻辑层次,这种“({})”包裹起来的方式可以让我们的肉眼很容易捕捉这段代码的主体,使用Lazy Loading创建的对象形式前提是你使用getter,而一些临时变量是没有getter的,而且使用Lazy Loading你还需要找到对应getter方法实现的位置去看具体的设置,没有这个一目了然。还有一点就是,Lazy Loading对象主要优点是延迟加载内存,在需要被使用的时候才会被创建,增加类的灵活性,更好的控制内存。而针对一些必然会创建的对象有些大材小用,失去原本的意义了。

4.使用字面量语法替换等价方法

字面量语法实际上是一种“语法糖”(syntactic sugar),以一种非常简单快捷的方式能创建对象,使我们开发者编程更高效,更愉悦。目前Objective-C支持的字面量语法的类有NSString,NSNumber, NSArray, NSDictionary。使用字面量语法的好处:

  • 使用字面量语法可以缩减源代码长度,没有多余语法成分,提高可读性;

  • 在使用字面量语法创建数组时,如果数组元素对象中有nil,则会抛出异常,其效果等于先创建一个数组,然后把方括号内的所有对象都加到这个数组中。抛出的异常会是这样:

***Terminating app due to uncaught exception
‘NSInvalidArgumentException', reason:’***
-[__NSPlaceholderArray initWithObjects:count:] : attempt to
insert nil object from objects[0]'

案例:

NSArray *arrayA = [NSArray arrayWithObjects:object1, object2, object3, nil];
NSArray *arrayB = @[object1, object2, object3];

针对上面代码进行分析,如果object2=nil;,arrayA数组可以创建,但只有object1一个对象,因为“+ (instancetype)arrayWithObjects:”方法会一次处理各个参数,直到发现nil为止,而arrayB会抛出异常,这个特性使我们更容易发现程序中存在的问题,提高了安全性。ps:字典跟数组一样,一旦有值为nil,也会抛出异常,而且创建时的“键”“值”顺序和我们正常说的“键值”顺序一样(正常初始化为“值”“键”),便于阅读。

使用字面量语法的缺点:使用字面量创建都是不可变对象,如果想创建可变对象需要复制一份:

NSMutableArray *mutableArray = [@[@1, @2, @3] mutableCopy];

5.使用类型常量替换#define预处理指令

在编写代码时,我们常用#define去定义一个宏。我们定义一个播放动画的时间为常量,可能这样定义:

 #define ANIMATION_DURATION 1.0

上述预处理指令会把源代码中的 ANIMATION_DURATION 替换为1.0,可以达到效果,不过这样定义是存在问题的,定义出来的常量没有类型信息,无法一眼看出代表的是一个时间,可读性差,而且如果把这个宏定义放在头文件中的话,那么引入了这个头文件的代码,其 ANIMATION_DURATION 都会被替换,如果有人定义了常量值,这将导致应用程序中的常量值不一致。一种更好的处理方案是使用类型常量替换掉相应的#define预处理指令。

  • 外部不可见:

    .m文件中:

static const NSTimeInterval kAnimationDuration = 1.0;// 命名规则:不被外部访问时 k+变量名

static修饰:意味着仅在此编译单元(.m文件)中可见;const修饰:如果试图修改值,编译器就会报错;static const:二者都使用,编译器的处理效果和#define一样,把遇到的变量替换为常值。

  • 外部可见:
    有些时候是需要向外部公开某个常量的,For example,在使用通知中心的时候,你需要向其他对象派发通知,监听者需要知道监听的事件,这个事件的名称我们通常写成一个外界可见的常值变量,这样的话,监听者无需知道实际字符串的值,只需要以常值变量来作为自己监听的事件名称,系统的 UIApplicationDidEnterBackgroundNotification, UIApplicationWillEnterForegroundNotification等都是这样做的:
    在.h文件中:
extern NSString *const MyClassNameActionNameNotification; // 命名规则:类名+事件名+Notification   

ps:从右至左解读,“一个常量,而这个常量是一个指针,指向NSString对象”。
在.m文件中:

NSString *const MyClassNameActionNameNotification = @"MyClassNameActionNameNotification";
  • 本例中的写法为:
    在.h文件中:
extern const NSTimeInterval MyClassNameAnimationDuration;// 命名规则:类名+变量名

在.m文件中:

const NSTimeInterval MyClassNameAnimationDuration = 0.3;

6.用枚举表示设置或状态

当我们想要表示某一种设置的多个选项或者多种状态时,推荐使用枚举。枚举的意义本来就是将一些表示某一种设置或者状态的数字转化成方便开发者阅读的形式,极大的提高了可读性。以上面其他的命名规则话题中提到过的枚举为例:

typedef NS_ENUM(NSInteger, UIViewAnimationTransition) { 
        UIViewAnimationTransitionNone,   // 枚举值命名
        UIViewAnimationTransitionFlipFromLeft, 
        UIViewAnimationTransitionFlipFromRight, 
        UIViewAnimationTransitionCurlUp, 
        UIViewAnimationTransitionCurlDown, 
    }; 

这个枚举含有5个值,分别表示了5种动画状态,如果系统以“0”,“1”,“2”等这样的数字来表示状态的话,作为开发者想要知道每个数字代表什么样的动画效果只能一个一个的去测试,并且需要记住每一个数字代表什么状态,方便以后使用,那么我相信绝大多数开发者都会疯掉的。

7.协议的签订格式

协议的签订推荐下面这种写法,这种写法的好处是你签订了什么协议一目了然,而且后面填写注释看起来也会更加舒服。

@interface FooViewController ()
<
    UITableViewDataSource, // 你的注释
    UITableViewDelegate // 你的注释
>
@end

8.将你的代码做好整理

为了让你的代码更整洁,你需要将你的代码做好归类整理,例如一个ViewController实现文件里的代码可能是这样:

#pragma mark - life cycle
// Methods...
#pragma mark - UITableViewDataSource
// Methods...
#pragma mark - CustomDelegate
// Methods...
#pragma mark - event response
// Methods...
#pragma mark - private methods
// Methods...
#pragma mark - getters and setters
// Methods...

将整个ViewController生命周期,系统或者自定义的协议方法(协议名称写完全,这样做的好处是可以直接点击mark后面的协议名跳转至此协议声明位置),一些事件的响应,私有方法,setter和getter方法,使你的代码更方便查找。如果是.h文件中声明的方法,最好使用类目进行方法归类,你可以查看系统的方法声明,做法亦是如此,使API的查看更加方便且清晰。

9.使用ARC

ARC的出现使开发者减轻对内存把控的压力,减少了编码量并且ARC目前已趋近于稳定,出错的概率很小,在代码执行效率上要高于MRC机制下的编写,因为ARC机制是Xcode编译器的特性,编译器会根据上下文语境插入相关的内存管理代码,并且这些代码是较为底层的,最重要的是会根据语境抵消retain和release操作,也就是说编译器检测到当一个对象需要五次retain操作,四次release操作的时候,它只做一次的retain操作,减少了方法调用,大大提升了效率。

参考:

《GCC的官方说明》
《Objective-C 之优雅的命名》
《iOS应用架构谈 view层的组织和调用方案》
《Effective Objective-C 2.0-编写高质量iOS与OS X代码的52个有效方法》

iOS开发
Web note ad 1