美文网首页学海无涯
Effective Objective-C---阅读笔记

Effective Objective-C---阅读笔记

作者: xxxxxxxx_123 | 来源:发表于2020-01-05 14:42 被阅读0次

    熟悉 Objective-C

    一、OC的起源

      OC的方法(本质上讲是消息发送)在运行时决定。使用函数调用的语言,由编译器决定。如果涉及多态,则用到虚函数表。

    二、降低类之间的耦合

      在类的头文件里尽量少的的引入其他头文件,或者将引入其他头文件的时机延后(使用@class),@class 也被称为向前声明。这样可以减少编译时间。
      如果在各自头文件引入对方的头文件,还对导致循环引用(chicken-and-egg situation)。使用#import而非#include指令虽然不会导致死循环,但是也会导致其中一个类无法正确编译。
      有时无法使用向前声明的时候,比如要声明某各类遵循一项协议,尽量把该类遵循某协议的这条声明移动至分类中,或者是把协议单独放在一个头文件中,然后将其引入。

    三、多用字面量语法,少用与之等价的方法。

    如:使用 NSString *str = @"HHHH" 代替 NSString *str = [[NSString alloc] initWithString: @"str"] 等等。
    注意:向数组或者字典中插入 nil 时,如果使用字面量会直接报错,如果是使用初始化方法,则 nil 以后的对象被忽略。

    四、多用类型常量,少用#define预处理指令

    #define ANIMATION_DURATION 0.3
    static const NSTimerInterval kAnimationDuration = 0.3
    
    • 不在头文件里面声明预处理命令,防止被别的文件引用,static const 也不能用
    • .m 文件里面声明的变量,需要加上 static,不然会产生外部符号,重复的外部符号会导致编译错误
    • const保证了所声明的变量不能被修改。static则可以用来保证只在编译单元内可见。
    • static const 定义只在类内部可见的常量
    • extern 定义全局常量,并使用类名作为前缀,但是尽量少使用extern
    • 使用 FOUNDATION_EXPORTextern 更好

    五、用枚举值表示状态、选项、状态码

    • 指定枚举变量的类型
      1 << 0: 按位左移,即1的二进制向左移动0位,结果还是1
      M << N: 将M的二进制左移N位,即M乘以2的N次方
      M >> N: 是M除以2的N次方
      &: 按位与
      |: 按位或
    typedef NS_ENUM(NSInteger, Direction) {
         left = 1
         right = 2
         up = 3
         down = 4
    };
    

    对象、消息、运行期

    六、属性相关

    属性:是OC的一项特性,用于封装对象中的数据;OC对象通常会把其所需要的数据保存为各种实例变量。
    @property 的作用是合成存取方法
    @synthesize 可以给属性生成的实例变量改名
    @dynamic 不会自动创建实现属性的实例变量,也不会为其创建存取方法
    nonatomic 原子性
    readwrite/readonly 读写权限
    assign/strong/weak/copy/ 内存管理语义
    getter=<name> / setter=<name> 方法名

    七、实例变量和属性

    • 在对象内部尽量直接访问实例变量,在写入数据时则应该通过属性写入;
    • 在初始化方法和dealloc方法中,总是应该通过实例变量的方式来读写数据;
    • 懒加载的时候,需要通过属性来读写数据(getter

    Tips:直接访问实例变量会比较快,绕过了OC的method dispatch,也不会触发KVO

    八、理解"对象等同性"这一概念 --- isEqual

    一般认为:当且仅当NSObject类其"指针值"完全相等时,这两个对象才相等。

    - (BOOL)isEqual:(id)object;
    - (NSUInteger)hash;
    

    对象相等时,这两个等式都成立

    如果把一个对象放入到set之后又修该对象的内容,那么可能破坏set的一些特性。如:

    NSMutableSet *set = [NSMutableSet new];
    NSMutableArray *arr01 = [@[@1, @2] mutableCopy];
    [set addObject: arr01]; // set {((1, 2))}
    NSMutableArray *arr02 = [@[@1] mutableCopy];
    [set addObject: arr02]; // set {((1), (1, 2))}
    [arr02 addObject: @2]; // set {((1, 2), (1, 2))} set元素的不重复性则被破坏了
    NSSet *setB = [set set copy]; // setB {((1, 2))} copy之后setB正常了
    

    所以,要么确保对象的哈希值不依赖内部可变的状态,要么确保依赖的状态不会改变。

    九、以"类族模式"隐藏实现细节 类族(class cluster)
    也就是工厂模式(Factory pattern)实现方式如下:

    typedef NS_ENUM(NSUInteger, EmployeeType) {
      EmployeeTypeDeveloper,
      EmployeeTypeDesigner,
      EmployeeTypeFinance,
    };
    
    @interface Employee: NSObject
    @property (copy) NSStirng *name;
    @property (assign) NSUInteger salary;
    
    + (Employee *)employeeWithType:(EmployeeType)type;
    - (void)doWork;
    
    @end
    
    @implementation: Employee
    + (Employee *)employeeWithType:(EmployeeType)type {
      switch (type) {
      case EmployeeTypeDeveloper:
      return [EmployeeTypeDeveloper new];
      break;
      case EmployeeTypeDesigner:
      return [EmployeeTypeDesigner new];
      break;
      case EmployeeTypeFinance:
      return [EmployeeTypeFinance new];
      break
      }
    }
    
    - (void)doWork {
      // subclass implement
    }
    @end
    
    @interface EmployeeTypeDeveloper:: Employee
    @end
    
    @implementation: EmployeeTypeDeveloper
    - (void)doWork {
      [self writeCode];
    }
    @end
    
    • 子类应该继承自类族中的抽象基类
    • 子类应该定义自己的数据存储方法
    • 子类应该重写(overwrite)父类文档中指明要重写的方法
    • 类族模式可以把实现细节隐藏在一套简单的公共接口后面
    • 系统框架中经常使用类族

    十、在既有类中使用关联对象存放自定义数据Associated Object管理关联对象

    1、以给定的键和策略为某对象设置关联对象值

    void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy)
    

    2、根据给定的键从某对象中获取相应的关联对象值

    id objc_getAssociatedObject(id object, void *key)
    

    3、移除指定对象的全部关联对象

    void objc_removeAssociatedObjects(id object)
    

    eg: 如果在一个类中处理多个警告信息视图:

    UIAlertView *alert = [[UIAlertView alloc] initWithTitle: @"title" message: @"message" delegate: self cancelButtonTitle: @"cancel" otherButtonTitles: @"continue", nil];
    void (^block)(NSInteger) = ^(NSInteger buttonIndex) {
      if (buttonIndex == 0) {
    
      } else {
    
      }
    };
    objc_setAssociatedObject(alert, AlertViewKey, block, OBJC_ASSOCIATION_COPY);
    [alert show];
    
    - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
      void (^block)(NSInteger) = objc_getAssociatedObject(alertView, AlertViewKey);
      block(buttonIndex);
    }
    
    • 可以通过关联对象将两个对象连起来
    • 定义关联对象时可以指定内存管理语义,用以模仿定义属性时所采用的"拥有关系"与"非拥有关系"
    • 尽量少使用关联对象

    十一、理解objc_msgSend的作用

    消息包括 "name"、"selector"可以接受参数,而且还可以有返回值。

    给对象发消息可以这样写:

    id returnValue = [someObject messageName:parameter]
    

    someObject叫做"接收者(receiver)"; messageNameselectorselector和参数合起来称为message。编译器看到此消息后,将其转换为一个标准的C语言函数调用,所调用的函数是消息传递机制中的核心函数,叫做

    objc_msgSend:void objc_msgSend(id self, SEL cmd, ...) 
    

    第一个参数代表接收者;第二个指的是selector;然后编译器会把刚才那个例子找那个的消息转换为如下函数:

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

    objc_msgSend函数会依据接收者与selector的类型来调整适当的方法。为了完成此操作,该方法需要在接收者所属的类中搜寻其方法列表(list of methods),如果能找到与selector名称相符的方法,就跳转至其实现代码。如果找不到就会沿着继承继续向上查找。如果最终还是找不到相符的方法,那就执行消息转发(message forwarding)操作。

    • 消息由接收者、selector及参数构成
    • 给对象发消息就是在该对象上调用方法
    • 发给某对象的全部消息都要由"动态消息派发系统"(dynamic message dispatch system)来处理,该系统会查出对应的方法,并执行其代码

    十二、理解消息转发机制

      在编译期向类发送了无法解读的消息并不会报错,因为在运行时我们可以继续向类中添加方法,当对象接收到无法解读的消息之后,就会启动消息转发机制(message forwarding),我们可以在此时告诉对象应该如何处理未知消息。
    消息转发分为三个阶段:

    • 第一个阶段,先征询接收者所属的类,看其能否动态添加方法,以处理当前这个"未知的selector",这叫做动态方法解析(dynamic method resolution)
    • 第二阶段,当接收者无法增加方法来响应该消息,那么先让接收者看看有没有其他对象能处理这条消息;若有,系统会将消息转给那个对象,消息转发过程结束
    • 若没有,则启动完整的消息转发机制,运行系统会把与消息有关的全部细节都封装到NSInvocation对象中,在给接收者最后一次机会,另其设法解决当前还未处理的消息。

    动态方法解析:

      对象在收到无法解读的消息之后,先会调用下面的方法(使用该方法的前提是:相关的方法的实现代码已经写好,只等着运行的时候动态的插在类里面就可以了,此方法常用来@dynamic属性)

    + (BOOL)resolveInstanceMethod:(SEL)selector
    

    当前接收者还有第二次机会来处理该selector,系统会让接收者看看有没有其他对象能处理这条消息,下面方法返回的是接收者找到的能处理该消息的对象,有就返回对象,没有就返回nil

    - (id)forwardingTargetForSelector:(SEL)selector
    

    若没有能够处理的对象,则创建一个NSInvocation对象把该消息有关的全部细节都封存。

    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
    - (void)forwardInvocation:(NSInvocation *)invocation;
    
    • 若对象无法响应某个selector,则进入消息转发流程
    • 通过运行时额动态方法解析功能,我们可以在需要用到某个方法时再将其加入类中
    • 对象可以把无法响应的selector转交给其他对象来处理
    • 如果上述之后,还没办法处理selector,那就启动完整的消息转发机制

    十三、用"方法调配技术"调试"黑盒方法"

      方法调配技术:method swizzling类的方法列表会把selector的名称映射到相关的方法实现之上,使得"动态消息派发系统"能够据此找到应该调用的方法。这些方法均以函数指针的形式来表示,这种指针叫做

    IMP:id(* IMP)(id, SEL, ...)
    

    交换两个方法的实现:

    void method_exchangeImplementations(Method m1, Method m2)
    

    上述函数中的方法可以通过以下函数获得:

    Method class_getInstanceMethod(Class aClass, SEL aSelector)
    
    • 在运行时,可以向类中新增或者替换selector所对应的方法实现
    • 使用另一份实现来替换原有的方法实现,叫做方法调配,我们可以使用此技术向原有实现中添加新功能。
    • 一般来说,只有调试程序的时候才需要在运行时修改方法实现

    十四、理解类对象的用意

    Class对象定义在运行期程序库的头文件:

    typedef struct objc_class *Class;
    struct objc_class {
      Class isa;
      Class super_class;
      const char *name;
      long version;
      long info;
      long instance_size;
      struct objc_ivar_list *ivars;
      struct objc_method_list **methodLists;
      struct objc_cache *cache;
      struct objc_protocol_list *protocols;
    };
    

      此结构体存放类的元数据"metadata"、例如类的实例实现了几个方法,具备多少个实例变量等信息。结构体的第一个变量也是isa指针,说明了Class本身就是Object-C对象。类对象所属的类型是另外一个类,叫做元类metaclass。用来表述类对象本身所具备的元数据。每一个类就只有一个类对象,每个类对象只有一个元类。

    • -isMemberOfClass 能够判断出对象是否为某个特定类的实例
    • -isKindOfClass 能够判断出对象是否为某类或其派生类的实例
      isMemberOfClass/isKindOfClass 具体的分析可见另一篇文章 isKindOf vs isMemberOf
    NSMutableDictionary *dict = [NSMutableDictionary new];
    [dict isMemberOfClass:[NSDictionary class]];        // NO
    [dict isMemberOfClass:[NSMutableDictionary class]]; // YES
    [dict isKindOfClass:[NSDictionary class]];          // YES
    [dict isKindOfClass:[NSArray class]];               // NO
    
    • 类型信息查询方法:isMemberOfClassisKindOfClass
    • 每个实例都有一个指向Class对象的指针,用以表明其类型,而这些Class对象则构成了类的继承体系
    • 如果对象类型无法在编译期确定,那么就应该使用类型信息查询方法来探知
    • 不要直接比较类对象

    接口与 API 设计

    十五、用前缀避免命名空间冲突

    • 全局函数和变量需要注意避免冲突
    • 在自己开发的库中,为用到的第三方库添加前缀

    十六、提供"全能初始化方法"

      一般创建一个类的时候,我们尽可能的会提供比较多的初始化方法,但是最好提供一个全能的初始化方法,让其他初始化方法都调用此方法实现
    eg:NSDate

    - (id)init;
    - (id)initWithString:(NSString *)string;
    - (id)initWithTimeIntervalSinceNow:(NSTimeInterval)seconds;
    - (id)initWithTimeIntervalSinceReferenceDate:(NSTimeInterval)seconds;
    

    initWithTimeIntervalSinceReferenceDate就是一个全能的初始化方法,这些初始化方法都会调用这个方法。

    十七、实现description方法

    实现模型的description方法,在控制台打印log的时候才会显示出打印的属性

    • 正常NSLog需要实现description
    • 在控制台里po,需要实现debugDescription

    十八、尽量使用不可变对象

    十九、使用清晰而协调的命名方式

    • 如果返回值是新建的,首个词是返回值类型
    • 不要使用 str 这种简称,使用 string
    • get 前缀仅在由“输出参数”保存返回值的方法中使用

    二十、为私有方法名添加前缀

    为私有方法名添加前缀,但是不要以一个下划线作为前缀,这是苹果自己预留的方法,将私有方法和共有方法区别用过命名方式开来。

    二十一、理解OC的错误模型

    • 只有在发生了可以使整个应用程序崩溃的严重错误时,才应该使用异常
    • 一般情况下使用NSError,比如封装在delegate 方法里,返回给调用者
    • NSError对象封装了三条信息:
      1、Error domain(错误范围,其类型为字符串);
      2、Error code(错误码,其类型为整数);
      3、userInfo(用户信息,其类型为字典)

    二十二、理解NSCopying协议

    想要使自己的类支持copy操作,就要实现NSCopying协议:----返回一个不可变的copy版本

    - (id)copyWithZone:(NSZone *)zone
    

    OC中使用->语法,是因为修饰的是实例变量而不是属性
    NSMutableCopying协议----返回一个可变的copy版本

    - (id)mutableCopyWithZone:(NSZone *)zone
    

    可变copy和不可变copy

    [NSMutableArray copy] => NSArray
    [NSArray copy] => NSMutableArray
    
    • deep copy: 在拷贝对象自身时,将其底层数据也一并复制过去;深copy之后内容所指向的对象是原始内容中相关对象的一份copy
    • shallow copy: 浅copy之后的内容与原始内容均指向相同的对象

    一般情况下,大部分执行了NSCopying协议的对象的copy都是执行的浅copy,如果需要执行深copy需要自己来编写相关的方法

    对象的copy:

    • copy: 直接copy整个对象到另一块内存中(内容拷贝)
    • copy: 不复制对象本身,仅仅是copy指向对象的指针(指针拷贝)

    集合的浅复制有非常多种方法。当你进行浅复制时,会向原始的集合发送retain消息,引用计数加1,同时指针被拷贝到新的集合。

    NSArray *shallowCopyArray = [someArray copyWithZone:nil];
    NSSet *shallowCopySet = [NSSet mutableCopyWithZone:nil];
    NSDictionary *shallowCopyDict = [[NSDictionary alloc] initWithDictionary:someDictionary copyItems:NO];
    

    集合的深复制

    NSDictionary shallowCopyDict = [[NSDictionary alloc] initWithDictionary:someDictionary copyItems:YES];
    
    • 如果你用这种方法深复制,集合里的每个对象都会收到 copyWithZone: 消息。
    • 如果集合里的对象遵循 NSCopying协议,那么对象就会被深复制到新的集合。
    • 如果对象没有遵循 NSCopying协议,而尝试用这种方法进行深复制,会在运行时出错。

    copyWithZone: 这种拷贝方式只能够提供一层内存拷贝(one-level-deep copy),而非真正的深复制。
    第二个方法是将集合进行归档(archive),然后解档(unarchive),如:

      NSArray *trueDeepCopyArray = [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject:oldArray]];
    

    系统对象的copymutableCopy:

    • copy返回imutable对象;所以,如果对copy返回值使用mutable对象接口就会crash;
    • mutableCopy返回mutable对象;

    在非集合类对象中

    [immutableObject copy]          // 浅复制
    [immutableObject mutableCopy]   // 深复制
    [mutableObject copy]            // 深复制
    [mutableObject mutableCopy]     // 深复制
    

    集合对象中

    [immutableObject copy]          // 浅复制
    [immutableObject mutableCopy]   // 单层深复制
    [mutableObject copy]            // 单层深复制
    [mutableObject mutableCopy]     // 单层深复制
    

    注意点:

    NSString *str = @"string";
    str = @"newString";
    

    上面这段代码,在执行第二行代码后,内存地址发生了变化。乍一看,有点意外。按照 C 语言的经验,初始化一个字符串之后,字符串的首地址就被确定下来,不管之后如何修改字符串内容,这个地址都不会改变。但此处第二行并不是对 str 指向的内存地址重新赋值,因为赋值操作符左边的 str 是一个指针,也就是说此处修改的是内存地址
    赋值之后地址就会发生变化。

    NSString *name = @"name";
    NSLog(@"===name====%p==", name);// ===name====0x10f803188==
    name = @"HHHH";
    NSLog(@"===name====%p==", name);// ===name====0x10f8031c8==
    

    @property声明NSString、NSArray、NSDictionary 经常使用copy关键字,是因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary,他们之间可能进行赋值操作,为确保对象中的字符串值不会无意间变动,应该在设置新属性值时拷贝一份。

    • strong修饰的对象发生变化时也发生了变化
    • copy修饰的对象则不受外界的影响

    协议与分类

    二十三、通过委托与数据源协议进行对象间的通信

    Objective-c分类(Category)和扩展(Extension)

    Category:
    
    typedef struct objc_category *Category;
    struct objc_category {
      char *category_name   // 分类名
      char *class_name      // 分类所属的类名
      struct objc_method_list *instance_methods // 实例方法列表
      struct objc_method_list *class_methods    // 类方法列表
      struct objc_protocol_list *protocols     //分类所实现的协议列表
    }
    

    这个结构体里面是没有实例变量列表的。分类中可以写@property,但是不会生成setter/getter方法,也不会实现合成成员变量。
    分类中也可以通过其他方式添加属性,但是一般不推荐这么做:

    (objc_getAssociatedObject/objc_setAssociatedObject)
    
    Extension类扩展:
    

    Tips: class-continuation中文也翻译扩展
    实现方式如下:

    @interface Class_Name ()
    @end
    

    一般的类扩展是写到.m文件中,一般的私有属性都是写到.m文件的类扩展中。
    作用:为一个类添加额外的变量、方法和属性。

    分类和扩展的区别:

    1. 分类原则上只能增加方法(添加属性是通过runtime操作)
    2. 类扩展不仅可以增加方法,还可以增加实例变量(或者属性),只是该实例变量默认是@private类型的(范围只能是当前类)
    3. 类扩展中声明的方法没被实现,编译器会报警,但是分类中的方法没被实现,编译器不会报警。因为类扩展是在编译阶段添加到类的,分类是在运行时添加到类中的。
    4. 如果分类和其所属类有相同的方法时,优先调用分类的方法,然后super
    5. 扩展没有自己单独的实现部分(implementation),依托所属类的实现来实现

    二十四、将类的实现代码分散到便于管理的数个分类中

    按照不同的分类和属性进行代码分块,私有方法放入私有的类中。

    二十五、总是为第三方类的分类名添加前缀

    向第三方类中添加分类时,为分类名称和方法名称添加前缀,防止出现了重写系统方法或者是被其他分类重写了当前分类的方法。

    二十六、不要在分类中声明属性

    1. 分类中无法创建实例变量
    2. 可以定义存取方法,但是不要定义属性

     尽管在分类里也可以声明属性,但是这种做法还是要尽量避免。原因是除了class-continuation分类之外,其他分类都无法向类中新增实例变量,因此它们无法把实现属性所需的实例变量合成出来。此时需要使用@dynamic,在分类中为该属性实现存取方法。

    二十七、使用class-continuation隐藏实现实现细节

    • 比如我们将不愿意暴露的属性或者方法实现在扩展中
    • 比如当一个类需要遵从某一个协议的时候,我们可以在扩展中遵从,在类的实现中实现协议方法

    二十八、通过协议提供匿名对象

    • 具体的对象类型可以淡化成遵从某协议的id类型,协议里规定了对象所应该实现的方法
    • 使用匿名对象来隐藏类型名称(或者类名)

    内存管理

    二十九、理解引用计数

    引用计数工作原理:对象有个计数器,用以表示当前有多少个对象想令此对象继续存活下去在Objective-C中叫做保留计数(retain count)也叫引用计数(reference count)
    NSObject协议声明了下面三个方法用于操作计数器:

    • retain: 递增保留计数
    • release: 递减保留计数
    • autorelease: 在清理自动释放池时,再递减保留计数
    1. 先保留新值然后释放旧值,然后更新实例变量
    2. 引用计数机制通过可以递增递减的计数器来管理内存。对象创建好之后,其保留计数至少为1。若保留计数为正,则对象继续存活,当保留计数降为0的时候,对象就被销毁了。
    3. 在对象生命周期中,其余对象通过引用来保留或释放此对象。保留与释放操作分别会递增及递减保留计数。

    三十、以ARC简化引用计数

    • __strong: 默认语义,保留此值
    • __weak: 不保留此值,但是变量可以安全使用,因为如果系统把这个对象回收了,那么变量也会自动清空
    • __autoreleasing: 把对象"按引用传递"给方法时,使用这个特殊的修饰符,此值在方法返回时自动释放。
    • __weak 常常用来防止block的循环引用

    ARC管理对象生命周期的方法基本上就是:在合适的地方插入"retain"和"release"操作。在ARC环境下,变量的内存管理语义可以通过属性的修饰符来致命。

    ARC只负责管理Objective-C对象的内存。CoreFoundation对象不归ARC管理,开发者必须手动调用CFRealse、CFRetain

    三十一、在dealloc方法中只释放引用并解除监听

    dealloc中只用来移除KVONSNotificationCenter的通知,不要做其他事情

    三十二、编写"异常安全代码"时留意内存管理问题

    try {
    
    } catch {
        
    }
    

    若使用ARC且必须捕获异常时,则需要打开编译器的-fobjc-arc-exceptions标志。如果是手动管理引用计数且需要捕获异常时,则需要设法把对象正确清理干净。

    三十三、以弱引用来避免循环引用

    • weak
    • unsafe_unretained

    三十四、以"自动释放池"降低内存峰值

    使用

    @autoreleasepool {
    
    }
    

    将会占用大量内存的代码包裹起来,系统就会在代码块的结尾把某些对象回收掉,从而降低内存峰值。

    三十五、用僵尸对象(Zombie Object)调试内存管理问题

    退出程序调用abort()方法
    TODO 此处需要扩充

    三十六、不要使用retainCount方法

    NSObject协议定义了下列方法,用来查询当前对象的保留计数:

    - (NSUInteger)retainCount;
    

    ARC此方法已经废弃了。

    Block和GCD

    三十七、理解block

    block基础知识:

    ^{};
    void (^someBlock)() = ^{
    
    }
    

    定义block的时候,其所占的内存区域是分配在栈中的。当对一个block执行copy操作时,就会把block从栈复制到堆上。

    三十八、为block创建typedef

    typedef int(^someBlock)(BOOL flag, int value);
    

    block的名称为someBlock;返回值为int;传入的参数有两个,一个是BOOL,一个是int

    三十九、用handler降低代码分散程度

    将方法里的闭包用typedef抽取出来

    typedef void(^NetworkErrorHandler)(NSError *error);
    - (void)finishWithCompletionHandler:(NetworkErrorHandler)failure;
    
    • 在创建对象时,可以使用内联的handler块将相关的业务逻辑一并声明
    • 在设计接口的时候,根据情形判断是需要时候用handlerblock还是delegate

    四十、用块引用所属对象时注意循环引用

    block捕捉的对象直接或间接的保留了块本身,那就需要注意循环引用。
    如:在block里持有了selfself又持有了block;或者block里持有了selfself持有了一个对象AA又持有block

    四十一、多用派发队列,少用同步锁

    GCD出来之前,如果有多个线程要执行同一份代码,通常使用锁来实现某种同步机制。

    1. 是使用内置的同步块(synchronization block):
    @synchronized(self) {
    
    }
    

    此方法会根据给定的对象,自动创建一个锁,并等待块中的代码执行完毕。这种写法会降低代码效率。

    1. NSLock、NSRecursiveLock对象:
    NSLock *lock = [[NSLock alloc] init];
    [lock lock];
    // 将代码写在此处
    [lock unlock];
    

    或者使用NSRecursiveLock这种递归锁(recursive lock)。线程能够多次持有该锁,而且不会出现死锁deadlock现象。
    缺陷:效率也不是很高。

    使用串行同步队列(serial synchronization queue)可以代替同步块或锁对象。将读取、写入操作都安排在同一个队列里,即可保证数据同步。

    _syncQueue = dispatch_queue_create("queueName", NULL);
    dispatch_sync(_syncQueue, ^{
    
    });
    

    并发队列(concurrent queue):

    _syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    

    栅栏(barrier):

    dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);
    
    • 派发队列可用来表述同步(synchronization semantic),这种做法要比使用@synchronized或者NSLock对象更简单,更有效率
    • 将同步与异步派发结合起来,可以实现与普通枷锁机制一样的同步行为,而这么做却不会阻塞执行异步派发的线程
    • 使用同步队列及栅栏,可以令同步行为更加高效

    四十二、多用GCD,少用performSelector系列方法

    如果想要把任务放在另一个线程上执行,最好不要用performSelectorOnMainThread,而应该用GCD的方法实现。因为performSelector系列方法在内存管理方面容易有疏失。

    四十三、掌握GCD及操作队列的使用时机

    • 同步机制、只需执行一次的代码(比如单例)使用GCD是最好的选择。
    • 但是执行后台任务时,GCD并不一定是最好的方法。NSOperationQueue。开发者可以把操作以NSOperation子类的形式放在队列中,这些操作也可以并发执行。
    • GCD是纯CAPI,操作队列是OC的对象。
    NSOperationQueue类的addOperationWithBlock
    

    使用NSOperationNSOperationQueue的好处如下:

    • 取消某个操作。如果使用操作队列,那么想要取消操作是很容易的。运行任务之前,可以在NSOperation对象上调用cancel方法。不过已经启动的任务是无法取消的。
    • 指定操作间的依赖关系。一个操作可以依赖其他多个操作。使一个操作必须在另一个操作顺利执行完毕之后方可执行。
    • 通过键值观察机制监控NSOperation对象的属性。如isCancelled属性来判断任务是否已经取消,isFinished属性来判断任务是否已经完成。
    • 指定操作的优先级。GCD的优先级是针对队列而言,而不是针对每个块。NSOperation对象也有线程优先级(thread priority)这决定了运行此操作的线程处在何种优先级上。这个功能要比GCD方便。
    • 重用NSOperation对象。系统内置了很多NSOperation的子类,我们也可以自己创建。
    • NSOperationQueue: 操作队列
    • GCD:派发队列

    四十四、通过Dispatch Group机制,根据系统资源状况来执行任务

    dispatch groupGCD的一项特性,能够把任务分组。调用者可以等待这组任务执行完毕,也可以在提供回调函数之后继续往下执行,这组任务完成时,调用者会得到通知。

    四十五、用dispatch_once来执行只需运行一次的线程安全代码

    单例模式singleton

    + (id)sharedInstance {
        static SomeClass *sharedInstance = nil;
        @synchronized(self) {
            if (!sharedInstance) {
                sharedInstance = [[self alloc] init];
            }
        }
        return sharedInstance;
    }
    

    Tips: 上述方法可能会出现线程安全问题

    只执行一次的GCDAPI:

    dispatch_once(dispatch_once_t *token, dispatch_block_t block) 
    

    该函数里的block必定会执行,且只执行一次。

    + (id)sharedInstance {
        static SomeClass *sharedInstance = nil;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            sharedInstance = [[self alloc] init];
        });
        return sharedInstance;
    }
    
    • dispatch_one 可以彻底的保证线程安全。而且更高效。
    • static 表示把该变量定义在static作用域中,可以保证编译器每次在每次执行sharedInstance方法时都会复用这个变量,而不会创建新的变量。

    四十六、不要使用diapatch_get_current_queue

    系统框架

    四十七、熟悉系统框架

    四十八、多用块枚举,少用for循环

    NSEnumerator:
    - (NSArray *)allObjects
    - (id)nextObject // 返回nil就表示到达枚举末端了
    

    遍历数组:

    NSArray *anArray = /*....*/;
    NSEnumerator *enumerator = [anArray objectEnumerator];
    id object;
    while ((object = [enumerator nextObject]) != nil) {
    
    }
    

    这种写法和For循环相似,但是代码多了一些,不过是通用的,可以用在所有的collection

    NSDictionary *aDictionary = /*...*/;
    NSEnumerator *enumerator = [aDictionary keyEnumerator];
    id key;
    while ((key = [enumerator nextObject]) != nil) {
        id value = aDictionary[key];
    }
    

    for in快速遍历;
    enumerateObjectsUsingBlock 块枚举法具备比较多的优势,提供的信息比较丰富

    四十九、对自定义其内存管理语义的collection使用__bridge

    当使用CoreFoundation框架的时候,(带CF前缀的),注意使用桥式转换(bridgedcast)

    1. __bridge: ARC仍然具备这个OC对象的所有权。
    2. __bridge_retained: ARC将交出对象的所有权,需要调用CFRelease
    3. __bridge_transfer: 让ARC获得所有权,把CFArray转换为NSArray

    五十、构建缓存时选用NSCache而非NSDictionary

    NSCache优点:

    1. 当系统资源将要耗尽时,可以自动删减缓存
    2. NSCache并不会拷贝键,而是会保留它。
    3. NSCache是线程安全的

    五十一、精简initializeload的实现代码

    1. 尽量不要使用load方法
    2. initialize是懒加载的,只有在调用的时候才会执行
    3. 在加载阶段,如果类实现了load方法,那么系统就会调用它。load方法不支持overwrite

    五十二、NSTimer会保留其目标对象 防止出现循环引用

    相关文章

      网友评论

        本文标题:Effective Objective-C---阅读笔记

        本文链接:https://www.haomeiwen.com/subject/iraqactx.html