Objc开发规范

前言

这篇文章是基于笔者个人经验以及一些开发规范总结而成,目的是提高公司内部项目的稳定性与效率。
文章的前半部分是一些通用开发规范,适用于所有编程语言甚至是生活中遇到的问题;
后半部分是关于OC的开发规范以及开发中容易犯的错误,如果不想看可以跳过。

原则

我们无法写出完全没有BUG的代码,但是我们可以尽量写出容易阅读和逻辑简单清楚的代码,容易阅读和逻辑简单清楚的代码从某种程度上会减少BUG以及BUG的修复难度。

通用规范

Tips:
虽然BUG无法完全避免,但是我们可以遵循一些规范和规则让BUG尽早暴露,或者让它便于排查。
  • [必须] 开发完一个模块(功能、页面等等)时,自测一遍,确认没有问题后再开发下一个模块,如果一个模块很大,可以完成一个小功能时自测一遍,不要把所有问题都放到最后测试,更不要依赖测试人员,关于自测流程可以参考自测流程规范

  • [必须] 禁止实现未来可能需要的功能,如果这个功能现在不需要就不要实现它,等未来需要的时候再去实现它,因为那个时候你可以更清晰的看到那个功能完整的模样(如果必须要实现它,必须要考虑所有可能发生的情况以及细节,并且加上注释说明)。

    Tips:
    实现一个未来可能需要的功能在大部分情况下都是得不偿失的,有以下缺点:
    1. 未来可能不需要这个功能。
    2. 增加垃圾代码,当别人看见这段代码时不理解为什么要这么写。
    3. 增加维护成本和开发时间(即使某段代码不使用也是需要维护的)。
    4. 后期真正要用的时候可能会发现当初实现的时候考虑不全面,最后还是要重新写一遍。
    5. ……
  • [必须] 不要尝试去推测并解决未来可能发生的问题,等问题出现的时候再去解决它,因为那个时候你可以更清晰的看见问题的真实样子。

    Tips:
    这个规则不是说要你发现问题了不去解决等到被别人发现提出后再解决;
    现实生活中有一些完美主义者想让自己写的代码非常完美没有任何问题,所以他们会尽
    量假设各种各样的情况,甚至很多极端情况,有时候会为了一个发生概率很小并且
    影响不是很重要的问题而大改特改,直到满意为止,这么做很可能会影响别人甚至导致项目延迟上线。
    在修改过程中如果改动的范围特别大还可能会引进新的问题,而且由于这个问题是未来
    可能会发生的问题,所以可能还会有你没考虑到的地方,最后既引进了新的问题还可能没解决问题。
  • [必须] 对于服务端返回的值,永远不要假设它是正确的类型和值(即使服务端的人保证),使用之前必须检查它的真实类型和值是否符合预期。

  • [必须] 不要让代码脱离开发者的掌控。当我们开发一个功能时,总是希望让使用者调用更少的API或者自动调用,但是有些步骤让使用者主动调用会更好一些,这样使用者知道他干了什么,而不是出了问题时一头雾水,当你把某个步骤设置成自动调用时想一下这是否有必要?如果不是请让使用者主动调用它并在文档中加以说明。

  • [必须] 所见即所得,例如页面上2个控件的间距是10,那么代码中的间距设置也应该是10,而不应该是一个控件很高,有很多的空白区域,然后用-xx(负间距)去填充。

    正例:
    UILabel *label1 = [[UILabel alloc] init];
    label1.frame = CGRectMake(0, 0, 100, 30);

    UILabel *label2 = [[UILabel alloc] init];
    label2.frame = CGRectMake(0, 30 + 10, 100, 30);
    上述代码明显的告诉你 label2 的顶部和 label1 的底部间距为10。

    反例:
    UILabel *label1 = [[UILabel alloc] init];
    label1.frame = CGRectMake(0, 0, 100, 30);

    UILabel *label2 = [[UILabel alloc] init];
    label2.frame = CGRectMake(0, -20, 100, 90);
    假设文字实际展示需要 30 高度,上述代码和正例中的代码给用户的感觉是一样的,
    但是阅读性却非常差。
  • [必须] 一般情况下禁止使用过时的方法或类,应该及时去了解和使用新方法或类。

    Tips:
    对于过时的方法或类,大都是因为其自身有缺陷或BUG,
    使用新方法前建议了解一下旧方法/类废弃的原因。
  • [必须] 尽量不要使用runtime去交换方法,可以使用中间者模式或者其他方法代替,如果一定要这么做,那么请留下注释说明交换方法后做了什么,以及为什么要这么做?

  • [必须] 尽量不要直接使用成员变量,而是使用属性替代它。

  • [必须] 在dealloc方法内禁止将self传递出去,如果self被retain,到下个runloop周期再释放则会多次释放导致crash。

    反例:
    - (void)dealloc {
    [self unsafeMethod:self];
    }
  • [必须] 对剪切板的读取操作必须放在子线程中进行,因为用户可能在Mac上复制大量数据然后通过接力同步到iPhone上。

  • [必须] 当方法可能会提前return时,需要注意对象的释放问题,避免内存泄漏。

    反例:
    CFArrayRef arrayRef = (__bridge CFArrayRef)array;

    if (x == YES) return;

    CFRelease(arrayRef);

    如果if条件成立那么arrayRef对象就会内存泄漏。
  • [必须] 使用@try处理异常时,需要注意对象的释放问题,避免内存泄漏。

    反例:
    @try {
    CFArrayRef arrayRef = (__bridge CFArrayRef)array;

    do some thing……

    CFRelease(arrayRef);

    } @catch (NSException *exception) {

    }

    如果do some thing……出现异常的话那么arrayRef就会出现内存泄漏。
  • [必须] 如果使用到的值和另一个值有所关联,在代码中体现出这种关联性,这能增加代码可读性,也能增加代码稳定性。

    Tips:
    例如有1个头像控件需要显示为圆形,我们经常会这样设置:
    1. 给头像控件设置圆角:layer.cornerRadius = 15.0;
    2. 给头像控件设置宽高:.frame = CGRectMake(0, 0, 30.0, 30.0);
    // 上述代码中的15.0 和 30.0 就没有任何关联性,如果它们之间相隔了很多代码,
    修改宽高的时候可能会忘记修改cornerRadius。

    正例:
    1. 给头像控件设置圆角:layer.cornerRadius = 15.0;
    2. 给头像控件设置宽高:.frame = CGRectMake(0, 0, layer.cornerRadius * 2.0, layer.cornerRadius * 2.0);
    // 这样后面阅读代码的人一眼就能明白宽高和 layer.cornerRadius 的关系,既增加了可读性,又增加了稳定性。
  • [必须] 如果一个值需要特别多个变量计算出来,请把它们提取成一个变量并且加上注释说明这个变量是怎么组成的。

    正例:
    // 屏幕宽度 - label左边距 - label右边距 - 开关按钮宽度 - 按钮右边距
        CGFloat titleMaxWidth = kScreenWidth - labelLeftSpacing - labelRightSpacing - buttonWidth - buttonRightSpacing;

    // 这样写的好处在于以后如果需要修改某个控件的布局信息,只需要修改一处即可,降低了后续维护难度。
  • [建议] 声明常量尽量使用const类型,不要使用#define。

    Tips: 
    宏定义声明常量的缺点:
    1. 宏定义只是简单的替换,缺少编译检查,运行期可能会出现溢出或数据错误等问题。
    2. 宏定义缺少类型,不方便编写文档用例。
    3. 宏定义可能会被替换。
    4. 宏定义无法编写符合规范的注释信息。

    反例:
    #define kTime @"10"

    if (NO) {
    #define kTime @"20"
    }

    NSLog(@"time = %@", kTime);

    即使if永远不会执行,但是编译器也会将kTime替换为@"20"
  • [建议] 工具类尽量在头文件的注释中写清楚该功能如何使用,以及需要注意的事项。

    Tips:
    1. 如果是一个UI工具,使用时是否需要提前设置frame,是否可以使用约束布局?
    2. 对于一些比较复杂的工具,最好在注释中给出一段示例代码。
    包括但不限于上述的注意事项,
    例如一个封装好的弹窗工具,应该说明如何弹出,是否需要提前设置frame或者约束

    以下是一个简单的示例伪代码:
    /**
    * @brief UIActivityIndicatorView的增强版,和UIActivityIndicatorView使用方式一致,但扩展了一些额外功能。
    * @discussion 1. 除了可以自定义颜色之外,还可以自定义指示器的详细大小(例如指示器宽度、高度、离心距离等)。
    * @discussion 2. 内部会自动计算控件所需要的最小宽高,可以不设置宽高约束或宽高Frame。
    * @discussion 3. 可以暂停/恢复指示器动画。
    * @code
    * LLActivityIndicatorView *activityIndicatorView = [LLActivityIndicatorView activityIndicatorWithStyle:LLActivityIndicatorViewStyleGrayMedium];
    * activityIndicatorView.backgroundColor = UIColor.redColor;
    * activityIndicatorView.frame = CGRectMake(0, 0, 50, 50);
    * activityIndicatorView.center = self.view.center;
    * [self.view addSubview:activityIndicatorView];
    * [activityIndicatorView startAnimating];
    * @encode
    */
    @interface LLActivityIndicatorView : UIView
    @end
  • [必须] 不要滥用懒加载,只在必要时刻使用它。

    只在以下三种情况下才使用懒加载:
    1. 对象的创建需要依赖其他对象
    2. 对象可能被使用,也可能不被使用
    3. 对象创建比较消耗性能
  • [必须] 使用NSUserDefaults存储数据时禁止调用synchronize方法,因为系统会在合适的时机将数据保存到本地(即使程序闪退等极端情况)。

  • [建议] 对于一些体积小并且不是特别重要的数据,不要频繁的进行写入操作,可以使用NSUserDefaults。它会在合适的时机将数据存储到本地,这避免了频繁的写入操作,而且在某些极端情况下它也能保证数据存储到本地(例如程序闪退等情况)。

  • [必须] 添加到集合中的对象应该是不可变的,或者在加入之后其哈希值是不可变的。

    反例:
    NSMutableSet *sets = [NSMutableSet set];
    NSMutableString *string1 = [NSMutableString stringWithString:@"1"];
    [sets addObject:string1];
    [sets addObject:@"12"];

    [string1 appendString:@"2"];

    当 [string1 appendString:@"2"] 执行完以后sets对象内会包含2个@"12"。
  • [必须] 不可变对象请使用copy修饰,如果重写set方法,请注意调用copy方法。

  • [必须] 请使用CGRectGet函数获取Frame的各种值,而不是通过frame.的方式获取。

    Tips:
    CGRect t_frame = CGRectMake(-10, -10, -10, -10);
    当一个view的frame设置成t_frame后,其坐标会隐式的转换为CGRectMake(-20, -20, 10, 10),
    因为宽高不可能出现负值;
    这时通过t_frame.的方式获取的值都是错误的,而CGRectGet会自动帮您处理这些隐式转换。

    正例:
    CGRectGetWidth(frame)、CGRectGetMinX(frame)、CGRectGetMaxX(frame)

    反例:
    frame.size.width、frame.origin.x、frame.size.width + frame.origin.x
  • [必须] 代码中单行字符数不要超过150个,超过请换行(空格除外),可以在 Xcode -> Preferences… -> Text Editing -> Page guide at column 中设置为150方便排查。

    正例:
    - (void)setImageWithURL:(nullable NSURL *)imageURL
    placeholder:(nullable UIImage *)placeholder
    options:(YYWebImageOptions)options
    progress:(nullable YYWebImageProgressBlock)progress
    ransform:(nullable YYWebImageTransformBlock)transform
    completion:(nullable YYWebImageCompletionBlock)completion;
  • [建议] 单个方法的行数建议不超过80行,注释、左右大括号、空行、回车等除外。

  • [必须] 在多线程环境下谨慎使用可变集合,必要时候可以采用加锁或GCD的同步线程进行保护,或者在访问可变集合时先将其copy为不可变对象然后再对其访问。

  • [必须] 属性和方法必须有 nullablenonnull 限定符,由于OC是动态语言,所以即使使用 nonnull 声明对象不为空,使用前也必须判断是否为空。

    正例:
    - (void)setName:(NSString * _Nonnull)name {
    if (name == nil) {
    ………
    }
    }
  • [必须] 如果有使用到数组、字典等类型,尽量使用泛型声明其包含的类型,这样可以提高代码可读性。

    正例:
    NSArray<NSString *> *array;

    反例:
    NSArray *array;
  • [必须] 如果类中包含多个初始化方法,请使用 NS_DESIGNATED_INITIALIZERNS_UNAVAILABLE 宏标记提高代码可读性。

  • [必须] 避免使用无符号整数(除非匹配系统接口使用的类型),在工程中全部使用一种类型可以提高代码安全与一致性,无符号整数在进行数学运算和倒数到零的时候会出现细微的错误。

    正例:
    NSUInteger numberOfObjects = array.count;
    for (NSInteger counter = numberOfObjects - 1; counter > 0; counter--)

    反例:
    for (NSUInteger counter = numberOfObjects - 1, counter > 0; counter--)
  • [必须] 在自动引用计数下,OC对象会自动初始化为nil,但是有些对象不会自动初始化为nil,例如CoreFoundation中的对象,所以在声明局部对象时需要手动初始化。

    正例:
    NSObject *obj = nil;

    反例:
    NSObject *obj;
  • [必须] 不要使用一个类去维护多个类的内容,例如使用一个常量类维护所有的常量,要按功能进行归类,分开维护。

    Tips: 
    大而全的类,杂乱无章,使用查找功能才能定位到具体位置,不利于理解也不利于维护。

    正例:
    缓存相关常量类放在CacheConsts下,系统配置相关常量类放在SystemConfigConsts下。
  • [必须] 如果超类的某个初始化方法不适用于子类,那么子类一定要重写超类的这个方法解决问题或抛出异常。

  • [必须] 把一些稳定的、公共的变量或者方法抽取到父类中。子类尽量只维持父类所不具备的特性和功能。

  • [必须] 禁止将布尔对象直接和 YES 或者 NO进行判断,例如 == YES, != YES,== NO,!= NO。

    Tips:
    在32位机器上YES被定义为1,NO定义为0;
    而64位机器上YES被定义为非0,NO定义为0;

    BOOL result = 4;

    if (result == YES) {
    NSLog(@"YES");
    } else {
    NSLog(@"NO");
    }

    以上代码在64位机器会输出YES,而上32位机器上则会输出NO。因为在32位机器上
    (result == YES) 会被解释为 (4 == (signed int) 1),
    所以会输出NO,而在64位机器上会认为result不等于0所以输出YES。
  • [必须] 删除代码时将内部用到的无用文件、无用类、无用函数等统一删除干净。

  • [建议] 如果可以,尽量使用 #undef 限制宏的作用范围。

  • [建议] 局部变量尽量定义在最靠近使用它的地方。

  • [建议] 在写一些简单的类方法和宏方法时,尽量使用内联函数或全局函数代替它。

    Tips:
    函数不通过对象调用,所以不会走OC的消息转发流程,效率高于方法调用;
    而且函数会有返回值和参数类型以及参数检查,这些都是宏定义没有的。

    正例:
    UIKIT_STATIC_INLINE NSString * kNSStringFromInteger(NSInteger a) {
    return [NSString stringWithFormat:@"%zd", a];
    }

    反例:
    #define kNSStringFromInteger(a) [NSString stringWithFormat:@"%zd", a]
  • [建议] 如果用到了很多协议,必要时可以把协议封装到一个单独的头文件中,这样做不仅可以减少编译时间,还能避免循环引用。

  • [建议] 使用switch枚举时尽量将所有枚举类型都case出来而不要使用default,这样的话以后增加或删除枚举类型时如果switch没有处理的话编译器会有警告提醒。

  • [建议] 尽量使用字面量语法创建对象,少用与之等价的方法。

    Tips:
    OC中的NSArray、NSString、NSDictionary、NSNumber都有与之对应的字面量语法: @[]、@""、@{}、@();
    使用它们有以下优点:
    1. 简单易读,提高代码的可读性和维护性。
    2. 使用字面量创建数组、字典时如果元素里在nil则会抛出异常,
    而使用arrayWithObjects:方法创建则会丢失nil后的数据,
    抛出异常能让你知道这里有问题及时修改防止问题在线上发生。

    缺点:
    1. 使用字面量创建的对象默认是不可变的,如果要创建可变对象需要进行mutableCopy操作。
    2. 不支持子类,如果你创建了一个NSString的子类,@""并不会返回你想要的子类对象。
  • [建议] UI控件尽量使用weak修饰而不是strong修饰,这样对梳理对象引用会更清晰明了。

  • [建议] 尽量不要使用+load方法,如果必须要使用那么不要在方法内实现复杂逻辑或堵塞线程。

  • [建议] 尽量减少继承层级,类的继承建议不要超过3层,必要时刻可以考虑用分类、协议来代替继承。

头文件规范

  • [必须] 头文件中尽量不要直接引用其他头文件,而是使用@class向前声明,每次引入其他头文件时先问问自己是否必须要这样做。

  • [建议] 头文件中暴露的方法和属性尽可能少,例如外部只需要覆值操作,那就不要提供getter方法或者属性。

    正例:
    @interface BookView : NSObject

    - (void)setBookName:(NSString *)bookName;

    @end

    反例:
    @interface BookView : NSObject

    @property (nonatomic, strong) NSString *bookName;

    @end
  • [建议] 头文件中的属性尽量声明为只读,可以在实现文件中再将属性声明为可读可写。

    正例:
    @interface WXYZModel : NSObject

    @property (nonatomic, readonly) NSString *name;

    @end

    @interface WXYZModel ()

    @property (nonatomic, strong) NSString *name;

    @end

    @implementation WXYZModel

    @end

Block开发规范

  • [必须] 在Block内部使用上下文的对象时要注意相互引用的问题(不一定要在 block 内使用 self 才会相互引用)。

    Tips: 
    1. 不一定在Block内使用self才会相互引用,如下情况也会造成循环引用:
    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    WXYZ_TitleTableViewCell *cell = ………

    cell.refreshTableViewBlock = ^{
    [tableView reloadData];
    };

    return cell;
    }

    2. Block内部是否要使用weak需要看Block本身和weak的这个对象是否存在直接或间接的相互引用,
    若无相互引用则不需要使用weak。

    3. 如果Block内部使用了strong修饰了外部的weak变量,那么当使用strong指向成员变量时需要进行判空,否则会崩溃,参考以下代码:
    __weak typeof(self) weakSelf = self;
    cell.refreshTableViewBlock = ^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    if (strongSelf != nil) {
    strongSelf->_name = @"name";
    }
    };

    如果把(strongSelf != nil)的判断去掉那么可能会崩溃。

通知开发规范

  • [必须] 在发送通知时,请使用userInfo对象进行传值,而不是object

  • [必须] 避免重复注册通知,这会导致重复执行通知方法。

  • [必须] 在使用通知的object参数时,需要确保接收方和发送方的object对象类型是一致的。

    反例:
    [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(testFunction) name:@"testNotificationName" object:model.bookID];
    [NSNotificationCenter.defaultCenter postNotificationName:@"testNotificationName" object:@"123"];

    假设 model.bookID 的值就是字符串123,也可能无法收到通知,
    因为NSString有__NSCFConstantString, __NSCFString, NSTaggedPointerString等多个子类对象,
    如果 model.bookID 的真实对象类型是 NSTaggedPointerString 的话就会收不到通知。
  • [建议] 在工程里能不用通知尽量不用通知,通知虽然灵活强大,但是如果滥用会导致工程质量下降并且增加维护难度。

结语

  1. 可以转载,但是请注明来源。

参考

  1. Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法
  2. Google的Objective-C风格指南
  3. Google的代码审查指南
文章作者: 布多
文章链接: https://internetwei.github.io/2022/01/20/Objc开发规范/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 布多的博客