心辰·Dev

Objective-C 代码规范与最佳实践

声明:本文整理自《Effective Objective-C》

Objective-C 基础

  • Objective-C 中使用消息传递机制而不是函数调用,消息传递机制是借鉴 smalltalk 语言发展而来。

  • 对象的类型在运行时被发现,编译器不关心接收消息的对象的类型,而是在运行时进行动态绑定。

  • 对象的内存始终分配在堆(heap)而不是栈(stack)中。而指向对象的指针,分配在栈(stack)中。

  • 使用@class, 进行向前声明,将引入头文件的时机尽量延后。

  • 将遵从某个 protocol 的声明移动到 class-continuation,这样只需要将包含协议的头文件引入到实现(.m)文件,而不需将其放入公共头文件。

  • 多用 NSNumber / NSArray / NSDictionary 的字面量语法(literal syntax),缩减代码长度。

  • 使用 literal syntax 在 NSArray 存入 nil 时会抛出异常,可提前发现要加入数组的无效对象。

  • 使用 literal syntax 创建的字符串、数组、字典对象都是不可变的,如果需要可变版本的对象,复制一份:

    1
    NSMutableArray *mutablearray  = [@[@1,@2,@3,@4] mutableCopy];
  • 多用类型常量,少用宏定义:

    1
    static const NSTimeInterval kAnimationDuration = 0.3;

    相比#define宏定义,类型常量有类型信息,可提前发现代码编写错误。

  • 要对外公开某个常量,把常量放入全局符号表(global symbol table),以便在定义常量的编译单元之外使用。

    1
    extern NSString *const BXStringConstant;
  • 除常用情况外,可在定义能够互相组合的选项时使用 NS_OPTIONS。如系统框架中定义的 UIViewAutoresizing 枚举:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
    UIViewAutoresizingNone = 0,
    UIViewAutoresizingFlexibleLeftMargin = 1 << 0,
    UIViewAutoresizingFlexibleWidth = 1 << 1,
    UIViewAutoresizingFlexibleRightMargin = 1 << 2,
    UIViewAutoresizingFlexibleTopMargin = 1 << 3,
    UIViewAutoresizingFlexibleHeight = 1 << 4,
    UIViewAutoresizingFlexibleBottomMargin = 1 << 5
    };
  • 在处理枚举类型的 switch 语句中,不用添加 default 情况,以开启漏写枚举值情况的提示

对象、消息、运行时

  • @synthesize 语法指定实例变量的名字,@dynamic 取消自动合成的存取方法和实例变量。

  • 属性各自用法:
    nonatomic / atomic: 指定原子性。
    readwrite / readonly: 指定读写权限。
    assign: 用于标量类型(scalar type),不持有。
    strong: 表明持有关系。
    unsafe_unretained: 用于对象类型(object type),不持有对象。
    weak: 不持有对象,且对象销毁后自动置nil
    copy: 如果一个不可变属性后续有可能会被可变实例赋值,使用copy关键字保证不可变。如 NSString, 可保护其封装性;用于 Block 可将 Block 栈变量复制到堆。
    getter = / setter=: 指定存取方法名。

  • 私有属性在头文件中声明为readonly, 在 class-continuation category 中声明为 readwrite

  • 使用 nonatomic 因为iOS性能瓶颈,并且 atomic 不能保证线程安全。

  • 为了更有效率,直接的方式读实例变量;为了使用属性的内存管理性质,用属性的方式赋值。

  • 在对象间用==符号比较的是指针是否相同,isEqual:方法比较的是对象是否相同。用isEqualToString:比较字符串更快。

  • 自定义类的等同性判定方法isEqualToObj如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    - (BOOL)isEqualToPerson:(OCPerson*)otherPerson {
    if (self == otherPerson) return YES;
    if (![_firstName isEqualToString:otherPerson.firstName]) return NO;
    if (![_lastName isEqualToString:otherPerson.lastName]) return NO;
    return YES;
    }

    - (BOOL)isEqual:(id)object {
    // override 父类方法
    if([self class] == [object class]) {
    return [self isEqualToPerson:(OCPerson*)object];
    } else {
    return [super isEqual:object];
    }
    }
  • Cocoa 中对类簇(class cluster)的应用如下:

    1
    + (UIButton*)buttonWithType:(UIButtonType)type;

    类簇是抽象工厂方法设计模式的一种实现。

  • 给对象发送消息id returnValue = [someObject messageName:parameter];语句由编译器转换为标准的 C 语言函数调用:

    1
    id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);

    objc_msgSend会在接收者所属类中搜寻方法列表(list of methods),并跳转至与 selector 相匹配的方法,并且将匹配结果缓存在映射表里(fast map)。

  • 某些情况需要由运行时另一些函数处理,如objc_msgSend_stret处理返回结构体的消息,objc_msgSend_fpret处理返回浮点数的消息,objc_msgSendSuper处理发给超类的消息。

  • 一张图理解消息转发(message forwarding)机制:
    message forwarding

  • Method Swizzling 在运行时改变 selector 所对应的实现。

  • 类继承体系如图所示:
    class hierarchy

接口与 API 设计

  • 由于 Objective-C 没有命名空间机制,要使用前缀避免命名空间冲突。

  • 类中所有的初始化方法都应该调用同一个 Designated initializer.

  • 调试程序时,实现debugDescription方法可以打印更详细的信息。

    1
    2
    3
    -(NSString*)debugDescription {
    return [NSStringstringWithFormat: @"<%@: %p, \"%@ %@\">", [self class], self, _firstVariable, _lastVariable];
    }
  • 私有函数加前缀进行区分如p_privateMethod:

  • 异常(throw exception)处理严重错误,会 crash 应用。更好的是用 NSError 来处理错误,NSError 通过委托协议传递此错误如:

    1
    2
    -(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { 
    }

    还可以经方法的输出参数返回给调用者,如:

    1
    2
    3
    4
    5
    6
    7
    -(BOOL)doSomething:(NSError**)error;

    NSError *error = nil;
    BOOL ret = [object doSomething:&error];
    if(error) {

    }
  • 遵循 NSCopying 协议并实现协议规定的方法,即可使自定义的对象使用 copy 方法。

    1
    2
    3
    4
    - (id)copyWithZone:(NSZone*)zone {
    BXPerson *copy = [[[self class] allocWithZone:zone] initWithFirst:_first andLast:_last];
    return copy;
    }
  • 深拷贝指对象中的数据将被拷贝一份,所指对象是原始相关对象的一份拷贝;而浅拷贝则仅拷贝对象本身,与原始内容均指向相同对象。

协议与分类

  • 使用分类(category)机制把类的实现代码划分成易于管理的块。

  • 使用 delegateflags 位段(bitfield)数据类型对 delegate 相关代码进行优化。

  • 运用关联对象(associated objects)在分类中声明属性。

  • 对象的类型不重要,重要的是对象有没有实现某些方法,这种情况下使用匿名对象,如
    @property (nonatomic, weak) id delegate;

内存管理

  • 悬空指针(dangling pointer)指向无效对象,指向对象 release 后应该置空,即 weak 属性所做的。

  • MRC 下的属性设置方法如下:

    1
    2
    3
    4
    5
    -(void)setFoo:(id)foo {
    [foo retain];
    [_foo release];
    _foo = foo;
    }
  • ARC 下可用下列修饰符改变局部变量或实例变量的内存管理语义:
    __strong: 默认,持有此值。
    __unsafe_unretained: 不持有此值,可能不安全,因为再次使用变量时,其指向对象可能已经回收。
    __weak: 不持有此值,不会造成安全问题,如果系统把对象回收了,那么变量会自动清空(autonilling)。
    __autoreleasing: 在方法返回时自动释放。

  • clang 的静态分析器完成内存操作代码的插入,实现 ARC。ARC 的底层优化很多,例如objc_autoreleaseReturnValue(id object) 和 objc_retainAutoreleasedReturnValue(id object).

  • dealloc() 中进行解除观察者监听等操作。

  • 使用 ARC 时必须捕获异常,需打开编译器的 -fobjc-arc-exceptions 以安全处理异常。

  • 非 ARC,使用 unsafe_unretained(对象类型) / assign(C类型) 避免 retain cycle。

  • 在 for 循坏中大量创建对象,可在循环中用 autoreleasepool 进行自动释放。

Block 与 GCD

  • Block 定义格式是return_type (^block) (parameters),下面是一个标准的 Block 变量赋值操作:

    1
    2
    3
    int (^addBlock)(int a, int b) = ^(int a, int b) {
    return a+b;
    };
  • 原本 Block 内存分配在栈里,对其 copy 可以拷贝到堆中。

  • 最好使用typedef return_type (^block) (parameters)为常用的块类型创建typedef

  • 在有多个实例需要监控时,使用委托模式会经常需要根据传入的对象来切换操作,而若改用 handler blocks 来实现,则可直接将块与对象联系起来。

  • 使用 NSArray / NSDictionary / NSSet 中的块枚举方法实现遍历功能,如 NSArray 的方法:

    1
    - (void)enumerateObjectsUsingBlock:(void (^)(ObjectType obj, NSUInteger idx, BOOL *stop))block;
  • Block 能够持有变量,注意消除 Block 中的 retain cycle.

  • 用 GCD 实现属性原子性:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    _syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    -(NSString *)someString {
    __block NSString *localSomeString;

    dispatch_sync(_syncQueue, ^{
    localSomeString = _someString;
    });

    return localSomeString;
    }

    -(void)setSomeString:(NSString*)someString {
    dispatch_barrier_async(_syncQueue, ^{
    _someString = someString;
    });

    }

    写入时用dispatch_barrier,串行执行。

  • dispatch_after替代performSelector:afterDelay:,在主线程运行并行队列来替代performSelectorOnMainThread

    1
    2
    3
    dispatch_async(dispatch_get_main_queue(), ^{
    // code here
    });
  • 使用dispatch_once来执行只需要运行一次的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    + (instancetype)sharedManager {
    static BXManager *sharedManager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    sharedManager = [[BXManager alloc] init];
    });
    return sharedManager;
    }