美文网首页
编写高质量iOS与OS X代码的52个有效方法(三)

编写高质量iOS与OS X代码的52个有效方法(三)

作者: 辣椒切成丝 | 来源:发表于2023-09-10 20:08 被阅读0次

    二、对象、消息、运行期

    9、以“类族模式”隐藏实现细节

    “类簇”(class cluster)是一种很有用的模式(pattern),可以隐藏“抽象基类”(abstract base class)背后的实现细节。OC的系统框架中普遍使用此模式,大部分的collection类都是类簇,如:UIButton、NSArray、NSMutableArray等:

    // 返回的对象,其类型取决于传入的按钮类型(button type)
    // 不管返回什么类型的对象,都是继承自同一个基类:UIButton
    // 意义:使用者无须关心属于哪个类,和其绘制等实现细节,只须知道如何创建,如何设置,如何使用就好
    + (UIButton *)buttonWithType:(UIButtonType)type; 
    
    // 如果放在同一个类里,你可能会写出这样的代码:
    - (void)drawRect:(CGRect)rect {
      if (_type == TypeA) {
        // Draw TypeA button
      } else if (_type == TypeB) {
        // Draw TypeB button
      }
      // 但是当类型和方法越来越多时,这样做救护很麻烦
    }
    

    优秀的程序猿会重构为多个子类,把各个按钮所用的绘制方法放到相关子类中去。不过需要用户知道各种子类才行,此时应使用“类簇模式”:可以灵活应对多个类,讲他们的实现细节隐藏在抽象基类后面,以保持接口简洁。用户无须自己创建子类实例,只需调用基类方法来创建即可。

    创建类簇:

    // .h:声明枚举、属性、方法。。。
    // .m:实现初始化方法:工程模式,是创建类簇的方式之一
    + (MOEmployee *)employeeWithType:(MOEmployeeType)type {
      swith (type) {
        case MOEmployeeTypeDeveloper: return [MOEmployeeDeveloper new];
        case MOEmployeeTypeDesigner: return [MOEmployeeDesigner new];
        case MOEmployeeTypeFinance: return [MOEmployeeFinance new];
      }
    }
    - (void)doWork {
      // 子类们实现各自的工作内容
    }
    // 子类继承MOEmployee,并实现doWork方法。。。
    // 另外注意:
    [anEmployee isMemberOfClass:[MOEmployee class]] // 永远为NO!!!
    

    Cocoa里的类簇:

    你要是知道NSArray是个类簇,就不会写出下面第一行这样的代码,[anArray class]所返回的类绝对不可能是NSArray类本身!!!

    if ([anArray class] == [NSArray class]) {} // 永远为NO!!!
    // Array的初始化方法返回的是隐藏的某个内部类型
    // 不过可以这样判断是都属于其类簇中
    if ([anArray isKindOfClass:[NSArray class]]) {} // YES
    

    想为类簇新增子类,需要遵循几条规则:

    •子类应该继承自类簇中的抽象基类(如:不可变数组的基类 or 可变数组的基类)

    •子类应该定义自己的数据存储方式(如:Array子类,可以用Array来存储,Array不会自动保存数据)

    •子类应当覆写超类文档中指明需要覆写的方法(如:Array的count、objectAtIndex:)

    要点:

    •类簇模式可以把实现细节隐藏在一套简单的公共接口后面

    •系统框架中经常使用类簇

    •从类簇的公共抽象基类中继承子类时要当心,若有开发文档,则应首先阅读

    10、在既有类中使用关联对象存放自定义数据

    可以给某对象关联许多其他对象,这些对象通过“键”来区分。存储对象值的时候,可以指明“存储策略”(storage policy),用以维护相应的“内存管理语义”。策略有名为objc_AssociationPolicy的枚举所定义:

    <colgroup><col width="auto"><col width="auto"></colgroup>
    | objc_AssociationPolicy关联类型 | 等效的@property属性 |
    | OBJC_ASSOCIATION_ASSIGN | assign |
    | OBJC_ASSOCIATION_RETAIN_NONATOMIC | nonatomic,retain |
    | OBJC_ASSOCIATION_COPY_NONATOMIC | nonatomic,copy |
    | OBJC_ASSOCIATION_RETAIN | retain |
    | OBJC_ASSOCIATION_COPY | copy |

    以下方法可以管理关联对象:

    •void objc_setAssociatedObject(id object, void* key, id value, objc_AssociationPolicy policy)此方法以给定的键和策略为某对象设置关联对象值

    •id objc_getAssociatedObject(id object, void* key)此方法根据给定的键从某对象中获取相应的关联对象值

    •void objc_removeAssociatedObject(id object)此方法移除指定对象的全部关联对象

    可以把某对象想像成NSDictionary,把关联到该对象的值理解为字典中的条目,存储关联对象的值就相当于在字典对象调用setObject:valueforKey:和objectForKey:方法。然而两者之间有个重要差别:设置关联对象时用的键(key)时一个“不透明的指针”(opaque pointer)。如果在两个键上调用isEqual:方法返回YES,那么字典人为二者相等;而在设置关联对象值时,若向令两个键匹配到同一个字,则二则必须是完全相同的指针才行。鉴于此,在设置关联对象值时,通常使用静态全局变量做键。

    要点:

    •可以通过“关联对象”机制来把两个对象连起来

    •定义关联对象时可指定内存管理语义,用以模仿定义属性时所采用的“拥有关系”与“非拥有关系”

    •只有在其他做法不可行时才应选用关联对象,因为这种做法通常会引入难于查找的bug

    11、理解objc_msgSend的作用

    上面讲述的自描述了部分消息的调用过程,其他“边界情况”(edge case)则需要交由Objective-C运行环境中的另一些函数来处理:

    •objc_msgSend_stret:消息返回结构体时调用此方法。(当CPU的寄存器能容纳的心返回值类型时,否则用的是另一个函数)

    •objc_msgSend_fpret:消息返回浮点数时调用此方法。需要对“浮点数寄存器”(floating-point register)做特殊处理。

    •objc_msgSendSuper:消息是给超类发的。如:[super someMethod:parameter]。也有另外两个与objc_msgSend_stret和objc_msgSend_fpret等效的函数,用于处理发给super的相应消息。

    还有一个概念需要理解一下:“尾调用优化”(tail-call optimization)技术:

    如果某函数的最后一项操作是调用另外一个函数且不会返回值另作他用时,那么就可以运用“尾调用优化”技术。编译器会生成调转至另一函数所需的指令码,且不会向调用堆栈中推入新的“栈帧”(frame stack)。(这项优化对objc_msgSend非常关键,若不做优化的话,每次调用OC方法前,都需要为objc_msgSend函数准备“栈帧”(可以在“栈踪迹”stack trace中看到),还会过早地发生“栈溢出”(stack overflow)现象)。

    明白这一点,就能理解为何在在调试的时候,栈“回溯”(backtrace)信息中总是出现objc_msgSend了。

    要点:

    •消息由接收者、选择子、参数构成。给某对象“发送消息”(invoke a message)也就相当于在该对象上“调用方法”(call a method)。

    •发给某对象的全母消息都要用“动态消息派发系统”(dynamic message dispatch system)来处理,该系统会查出对应的方法,并执行其代码。

    12、理解消息转发机制

    要点:

    •若对象无法响应某个选择子,则进入消息转发流程

    •通过运行期的动态方法解析功能,我们可以在需要用到某个方法时再将其加入类中

    •对象可以把其无法解读的某些选择子转交给其他对象来处理

    •经过上述两步之后,如果还是没办法处理选择子,那就启动完整的消息转发机制

    13、用“方法调配技术”调试“黑盒方法”

    类的方法列表会把选择子的名称映射到相关的方法实现上,使得“动态消息派发系统”能够据此找到应该调用的方法。这些方法均以函数指针的形式来表示,这种指针叫IMP,其原型如下:

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

    “方法调配”(method swizzling)

    // 获取方法实现
    Method class_getInstanceMethod(Class aClass, SEL aSelector)
    // 交换两个方法的实现
    void method_exchangeImplementations(Method m1, Method m2)
    

    要点:

    •在运行期,可以向类中新增或替换选择子所对应的方法实现

    •使用另一份实现来替换原有的方法实现,这到工序叫做“方法调配”,开发者常用此技术向原有实现中添加新功能

    •一般来说,只有调试程序时才需要在运行期修改方法实现,这种做法不易滥用

    14、理解“类对象”的用意

    “在运行期检视对象类型”这一操作也叫做“类型信息查询”(introspection,“内省”),这一个强大而有用的特性内置于Foundation框架的NSObject协议里,凡是由公共根类(common root class,即NSObject与NSProxy)继承而来的对象都要遵从此协议。在程序中不要直接比较对象所属的类,明智的做法是调用“类型信息查询方法”。

    OC对象的本质:

    // 每一个OC对象实例都是指向某块内存数据的指针。所以在声明变量时,类型后面都要跟一个`*`字符
    NSString *pointerVariable = @"Hello world";
    // 对于通用对象类型`id`,由于其本身已经是指针了,所以我们能够这样写
    id genericTypeString = @"Hello world"
    
    上面两种定义方式区别在于:
      第一种声明时指定了具体类型,当调用其没有的方法时,编译器会发出警告⚠️。  
    

    OC对象所用的数据结构定义在运行期程序库的头文件里,id类型本身也定义在这里:

    typedef struct objc_object {
      Class isa,
    } *id;
    

    由此可见,每个对象结构体的收个成员是Class类的变量。该变量定义了对象所属的类,通常称为“is a”指针。(如:string对象“是一个”(is a)NSString,所以其“is a”指针就指向NSString)

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

    typedef struct objc_class *Class;
    struct objc_class { // 吃结构体存放类的“元数据”(metadata)
      Class isa; // 说明Class本身也是OC对象
      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_cahce *cache;
      struct objc_protocol_list *protocols;
    }
    

    类对象所属的类型(即isa指针指向的类型)是另外一个类,叫“元类”(metaclass),用来表述类本身所剧本的元数据。“类方法”就定义于此处,这些方法可以理解为类对象的实例方法。每个类仅有一个“类对象”,每个“类对象”仅有一个与之相关的“元类”。

    假如有一个名为SomeClass的子类继承自NSObject,则其继承体系如下:

    [图片上传失败...(image-b95f9e-1694433945327)]

    super_class指针确立了继承关系,而isa指针描述了实例所属的类。通过这张布局关系图即可执行“类信息查询”。可以查出对象是否能响应某个选择子,是否遵从某项协议,看出此对象位于“类继承体系”(class hierarchy)的哪一部分。

    在类继承体系中查询类型信息:

    •isMemberOfClass::判断对象是否为某个特定类的实例

    •isKindOfClass::判断对象是否为某类/其派生类的实例

    使用isa指针获取对象所属的类,然后通过super_class指针在继承体系中游走。由于OC对象是动态的,所以此特性显得极为重要。从collection中取出的对象类型通常是id的,可以使用类型信息查询方法。如:

    NSMutableString *str = [NSMutableString new];
    for (id obj in array) {
      if ([obj isKindOfClass:[NSString class]]) {
        [str appendFormat:@"%@", obj];
      } else if ([obj isKindOfClass:[NSNumber class]]) {
        [str appendFormat:@"%d", [obj intValue]];
      } else {
        // ...
      }
    }
    

    不应该直接比较两个类对象是否等同,如:某个对象可能会把收到的所有选择子都转发给另外一个对象。这样的对象叫做“代理”(proxy),通常继承自NSProxy。通常在此对象上调用class方法,返回的是代理对象本身(NSProxy的子类),而非接受代理的对象所属的类。(用isKindOfClass:这样的类型信息查询方法,代理对象会转发给“接受代理的对象”;而用class返回的是发起代理的对象,而非“接受代理的对象”)。

    要点:

    •每个实例都有一个指向Class对象的指针,用以表明其类型,这些Class对象构成了类的继承体系

    •如果对象类型无法在编译期确定,那么就应该使用类型信息查询方法来探知

    •尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能

    三、接口与API设计

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

    要点:

    •选择与你的公司、应用程序或二者皆有关联之名称作为类名的前缀,并在所有代码中均使用这一前缀

    •若自己所开发的程序库中用到了第三方库,则应为其中的名称加上前缀

    16、提供“全能初始化方法”

    全能初始化方法 or 指定初始化方法 Designated initializer

    要点:

    •在类中提供一个全能初始化方法,并于文档里指明。其他初始化方法均应调用此方法

    •若全能初始化方法与超类不同,则需覆写超类中的对应方法

    •如果超类的初始化方法不适用于子类,那么应该覆写这个超类方法,并在其中抛出异常

    17、实现description方法

    首先description方法是定义在NSObject协议里的,然后NSObject和NSProxy俩“根类”都遵循了该协议,并有默认实现:打印类名和内存地址(如:<Person: 0x7f9a1600600>)。

    用NSLog+%@打印时调用的是description方法
    程序运行打断点时,在调试控制台输入LLDB的po命令,调用的是debugDescription方法
    可以实现如下:
    - (NSString *)description {
      return [NSString stringWithFormat:@"%@ %@", _firstName, _lastName];
    } // Bob Smith
    - (NSString *)debugDescription {
      return [NSString stringWithFormat:@"<%@: %p,\" %@ %@\">", [self class], self, _firstName, _lastName];
    } // (Person *) $1 = 0x07117fb0 <Person: 0x7117fb0, "Bob Smith">
    

    要点:

    •实现description方法返回一个有意义的字符串,用以描述该实例

    •若想在调试时打印出更详尽的对象描述信息,则应实现debugDescription方法

    18、尽量使用不可变对象

    如8条,若把可变对象放到collection之后又修改其内容,那么很容易就会破坏set的内部数据结构,使其失去固有的语义。因此,建议大家尽量减少对象中的可变内容。

    尽量把对外公布出来的属性设为只读,而且只在确有必要时才将属性对外公布。

    如:person有一个friends全部朋友的属性,放在一个“列表”(list)里外界可以增删。通常应该提供一个readonly属性返回不可变set(内部可变set的copy)供外界使用。

    // .h
    @property (nonatomic, strong, readonly) NSSet *friends;
    - (void)addFriend:(Person *)person;
    - (void)removeFriend:(Person *)person;
    
    // .m
    @property (nonatomic, strong, readwrite) NSSet *friends;
    NSMutableSet *_interalFriends;
    - (NSSet)firends {
      return [_interalFriends copy]; // 拷贝不可变版本
    }
    - (void)addFriend:(Person *)person {
      [_interalFriends addObject:person];
    }
    - (void)removeFriend:(Person *)person {
        [_interalFriends removeObject:person];
    }
    

    如果直接提供可变版本NSMutableSet供外部使用,不是借助addFriend:与removeFriend:方法,而是直接操作此属性。这种过分解耦数据的做法很容易出bug。如:在添加或删除朋友时,Person对象可能还要执行其他操作,此时就等于直接从底层修改了其内部用于存放朋友对象的set。在Person对象不知情时,直接从底层修改set可能会令对象内的各个数据之间互不一致。

    要点:

    •尽量创建不可变的对象

    •若某属性仅可于对象内部修改,则在class-continuation分类中将其由readonly属性扩展为readwrite属性

    •不要把可变的collection作为属性公开,而应提供相关方法修改对象中的可变collection

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

    1、方法命名:

    •如果方法的返回值是新创建的,那么方法名的首个词应该是返回值的类型。也可以看情况在前面添加修饰语,如:localizedString。属性的存取方法不遵循这种命名方式~

    •应该把表示参数类型的名词放在参数前面

    •如果方法要在当前对象上执行操作,那么就应该包含动词;若执行操作时还需要参数,则应该在动词后面加上一个或多个名词

    •不要使用str这种简称,应该用string这样的全称

    •Boolean属性应加is前缀。如过某个方法返回Boolean值,应根据其功能添加has或is前缀

    •将set这个前缀留给那些借由输出参数来保存返回值的方法,比如说,把返回值填充到C言语式数组(C-stye array)里的那种方法就可以使用这个词做前缀

    2、类与协议的命名

    应该为类与协议的名称加上前缀,以避免命名空间冲突,而且应该像给方法起名时那样把词句组织好,使其从左至右读起来较为通顺。

    要点:

    •起名时应遵从标准的Objective-C命名规范,这样创建出来的接口更容易为开发者所理解

    •方法名要言简意赅,从左至右读起来要像个日常用语中的句子才好

    •方法名里不要使用缩略后的类型名称

    •给方法起名时第一要务就是确保其风格与你自己的代码或所要继承的框架相符

    20、为私有方法名加前缀

    要点:

    •给私有方法的名称加上前缀,这样可以很容易的将其同公共方法区分开

    •不要单用一个下划线做私有方法的前缀,因为这种做法是预留给苹果公司用的

    21、理解Objective-C错误模型

    Error对象里封装了三条信息:

    •Error domain:错误范围,字符串

    产生错误的根源,通常用一个特有的全局变量来定义。如:NSURLError表示解析URL出错

    •Error code:错误码,整数

    独有的错误码,指明在某个范围内具体发生了何种错误,通常用enum定义。如:HTTP请求出错时,可能回把HTTP的状态码设为错误码

    •User info:用户信息,字典

    有关此错误的额外信息,其中或许包含一段“本地化的描述”(localized description),或许还含有导致该错误发生的另外一个错误,经由此种信息,可将相关错误串成一条“错误链”(chain of errors)

    建议:

    •为自己的程序库中所发生的错误制定一个专用的“错误范围”字符串

    •用枚举定义错误码,不仅解释错误码的含义,还给它们起了个有意义的名字

    要点:

    •只有发生了会使整个应用程序崩溃的严重错误时,才使用异常

    •在错误不那么严重的情况下,可以指派“委托方法”(delegate method)来处理错误,也可以把错误信息放在NSError对象里,经由“输出参数”返回给调用者

    22、理解NSCoping协议

    Foundation框架中的所有collection类在默认情况下都执行浅拷贝,即只拷贝容器对象本身,而不复制其中数据。(主要是因为:容器里的对象未必都能拷贝,而且调用者尾部想在拷贝容器时一并拷贝其中的每个对象)

    另外,不要假定遵从了NSCopying协议的对象都会执行深拷贝。在绝大多数情况下执行的都是浅拷贝。如果需要在某个对象上执行深拷贝,那么除非该类的文档说它是用深拷贝来实现的NSCopying协议,否则:要么寻找能够指向深拷贝的相关方法,要么自己编写方法实现。

    要点:

    •若想令自定义的对象具有拷贝功能,则需实现NSCopying协议

    •若自定义对象分为可变版本与不可变版本,则应同事实现NSCopying与NSMutableCopying协议

    •复制对象时需决定采用浅拷贝还是深拷贝,一般情况下应尽量执行浅拷贝

    •若自定义对象需要深拷贝,那么可考虑新增一个专门执行深拷贝的方法,如deepCopy

    四、协议与分类

    23、通过委托与数据源协议进行对象间通信

    “委托模式”/“代理模式”(Delegate pattern)主旨:

    定义一套接口,其他对象若想接收当前对象的委托,则需遵从此接口成为其“委托对象”(delegate)。而当前对象则可以给其委托对象传递一些信息,也可以在放生相关事件时通知委托对象。(此模式可将数据与业务逻辑解耦)

    通常情况下delegate对象会持有当前对象,所以需要将delegate属性定义成weak,否则会造成循环引用导致内存泄露。

    若要向外界公布此类实现了某协议,那么就在接口(.h文件)中声明;而如果这个协议是个委托协议的话,就可以在类内部(class-continauation分类中)声明。

    委托协议中的方法一般都是“可选的”(用@optional声明,后面的都是可选),因为代理未必关心其中的所有方法。

    在调用delegate中的方法时,总是应该把当前对象也一并传入方法中,这样delegate在实现相关方法时,就能根据传入的实例分别执行不同的代码了。如:

    - (void)networkFetcher:(NetworkFetcher *)fetcher 
                didReceiveDate:(NSData *)data {
      if (fetcher == _myFetcherA) {
      } else if (fetcher == _myFetcherB) {
      }
    }
    

    可以在当前对象中声明一个含有位段的结构体为其实例对象,结构体中的每个位段表示delegate是否实现了协议中的相关方法:

    @interface NetworkFetcher () {
      struct {
        unsigned int didReceiveData : 1;
        unsigned int didFailWithError : 1;
        unsigned int didUpdateProgressTo : 1;
      } _delegateFlages; // 实例对象!!!
    }
    // 设置代理时,就缓存当前delegate是否能响应协议中的相关方法
    - (void)setDelegate:(id< NetworkFetcher>)delegate {
      _delegate = delegate;
      _delegateFlages.didReceiveData = [delegate respondsToSelector:@selector(networkFetcher:didReceiveData:)];
      ....
    }
    

    协议方法要调用很多次时,值得进行这种优化。而是否需要优化,则应依照具体代码来定。这种需要分析代码性能,并找出瓶颈,若发现执行速度需要改进,则可使用此技巧。

    要点:

    •委托模式为对象提供了一套接口,使其可由此将相关事件告知其他对象

    •将委托对象应该支持的接口定义成协议,在协议中把可能需要处理的时间定义成方法

    •当某对象需要从另外一个对象中获取数据时,可使用委托模式。这种情境下,该模式亦称“数据源协议”(data source protocal)

    •若有必要,可实现含有段位的结构体,将委托对象是否能响应相关协议方法这一信息缓存至其中

    24、将类的实现代码分散到便于管理的数个分类之中

    类的基本要素(诸如:属性、初始化方法等)都声明在“主实现”(main implementation)里。执行不同类型的操作所用的另外几套方法则归入各个分类中。

    处在分类中的所有方法,其符号中会包含分类名。如:addFriend:方法的“符号名”(symbol name)如下:

    - [Person(Frendship) addFriend:]
    

    根据回溯信息中的分类名称,很容易就能精确定位到类中的方法所属的功能区,这对于某些应该视为私有的方法来说更是极为有用。

    在编写准备分享给其他开发者使用的程序库时,可以考虑创建Private分类。经常会有些方法:他们不是公共API的一部分,然而确非常适合在程序库之内使用。将其放入Private分类中,哪里用到就引入。而分类的头文件不随公共API一并公开。这样使用者旧不知道库里还有这些私有方法了。

    要点:

    •使用分类机制把类的实现代码划分成易于管理的小块,以便单独检视

    •将应该视为“私有”的方法归入名教Private的分类中,已隐藏实现细节

    25、总是为第三方类的分类名称加前缀

    因为OC中没有命名空间这一概念,所以只能用给类名/方法名添加前缀的方式实现。一般来说这个前缀应该与当前项目/当前模块相同。如:

    @interface NSString (XXX_HTTP) // 为分类名添加前缀!!!!
    - (NSString *)xxx_urlEncodeString; // 为分类方法名名添加前缀!!!
    - (NSString *)xxx_urlDecodeString;
    

    要点:

    •向第三方类中添加分类时,总应给其名称和方法名加上你专用的前缀

    26、勿在分类中声明属性

    虽然说我们可以在分类中运用runtime实现关联对象的方式,实现属性(实现方式之前这篇文章的@dynamic部分有写)。但是这么做不理想。要把相似的代码写很多边,而且在内存管理问题上容易出错(如:当修改了某个属性的特质attrubute时,还要记得修改setter方法中设置关联对象时所用的内存管理语义)。

    分类的目标在于扩展类的功能,而非封装数据。

    但有时候只读属性还是可以在分类中使用的。如:为NSCalendar类创建分类,返回各个月份名称数组。虽说仅是访问数据不需要实例变量来实现。但此时最好不要用属性,用一个方法就好。因为属性表达的意思是:类中有数据在支持着它。

    @interface NSCalendar (XXX_Additions)
    - (NSArray *)xxx_allMothns;
    @end
    @implementation NSCalendar (XXX_Additions)
    - (NSArray *)xxx_allMothns {
      if ([self.calendar isEqualToString:NSGegorianCalendar]) {
        return @[@"January", @"February", ...];
      } else if (/* other calendar identifiers */) {
        /* return months for other calendars */
      }
    }
    @end
    

    要点:

    •把封装数据所用的全部属性都定义在主接口里

    •在"class-continuation分类"之外的其他分类中,可以定义存取方法,但尽量不要定义属性

    27、使用“class-continuation”分类隐藏实现细节

    将使用到的C++文件在实现文件中导入,仅使实现文件扩展名为.mm,使用OC++编译。头文件仍就是.h,使用OC编译。从而实现隐藏C++代码的效果。如系统的WebKit和CoreAnimation就用到了此模式,内部很多都用C++写成,但对外公布的却是一套纯OC接口

    要点:

    •通过“class-continuation分类”向类中新增实例变量

    •如果某属性在主接口中什么为“只读”,而类的内部又要用设置方法修改此属性,那么就在“class-continuation分类”中将其扩展为“可读写”

    •把私有方法的原型声明在“class-continuation分类”里

    •若想是使类遵循的协议不为人所知,则可以于“class-continuation分类”中声明

    28、通过协议提供匿名对象

    若接口背后有多个不同的实现类,而你有不想指明具体使用哪个类,那么可以考虑使用遵从某协议的纯id类型—>因为有时候这些类可能会边,有时候它们又无法容纳于标准的类继承体系中,因而不能以某个公共基类来统一表示。

     如:代理(类型不重要,只要遵循协议就行)
    @property (nonatomic, weak) id <XXXDelegate> delegate;
    1
     如:NSDictionary设置键值对方法
    - (void)setObject:(id)object forKey:(id<NSCopying>)key;
    1
     如:处理数据库连接的程序库,以匿名对象来表示从另一个库中返回的对象
    @protocol XXXDatabaseConnection
    - (void)connect;
    - (void)disconnect;
    - (void)isConnect;
    - (NSArray *)performQuery:(NSString *)query;
    @end
    @interface XXDataBaseManager: NSObject
    + (id)sharedInstance;
    // 返回连接对象
    - (id<XXXDatabaseConnection>)connectionWithIdentifier:(NSString)identifier; 
    @end
    

    如此,处理数据库连接所用的类名就不会泄露,可能来自不同款家的类型现在均可用同一个方法返回了。

    再如:CoreData框架在负责查询接口的NSFetchedResultsContoller中,有个sections属性表示数据分区,是个数组如下:

    NSArray *sections = controller.sections;
    id <NSFecthedResultsSectionInfo> sectionInfo = section[section];
    NSUInteger numberOfObjects = sectionInfo.numberOfObjects;
    

    在幕后,此对象可能使由处理结果的控制器所创建的内部对象,没必要把表示翅中数据的类对外公布,因为使用控制器的人绝对不关心查询结果中的数据分区使如何保存的,他们只要知道可以在这些对象上查询数据就行

    要点:

    •协议可在某种程度上提供匿名类型。具体的对象类型可以淡化成遵从某协议的id类型,协议里规定了对象所应实现的方法(如果具体类型不重要,重要的使对象能够响应(定义在协议里的)特定方法,那么可使用匿名对象来表示)

    •使用匿名对象来隐藏类型名称(或类名)

    五、内存管理

    29、理解引用计数

    要点:

    •引用计数机制通过可以递增递减的计数器来管理内存。对象创建好之后,其保留计数至少为1。若保留计数为正,则对象继续存活。当保留计数降为0时,对象就被销毁了。

    •在对象生命期中,其余对象通过引用来保留或释放此对象。保留与释放操作分别会递增及递减保留计数

    30、以ARC简化引用计数

    要点:

    •有ARC后,程序员无须担心内存管理问题,可省去类中的许多“样板代码”

    •ARC管理对象生命期的办法基本上就是:在合适的地方插入“保留”及“释放”操作。在ARC环境下,变量的内存管理语义可以通过修饰符指明,而原来则需要手工执行“保留”及“释放”操作

    •由方法返回的对象,其内存管理语义是通过方法名来体现。ARC将此确定为开发者必须遵守的规则

    •ARC只负责管理OC对象的内存。尤其要注意:CoreFoundation对象不归ARC管理,开发者必须适时调用CFRetain/CFRelease。

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

    要点:

    •在dealloc方法里,只应释放指向其他对象的引用,并取消原来订阅的“键值观察”(KVO)或NSNotificationCenter等通知,不要做其他事情

    •若对象持有文件描述符等系统资源,那么应专门写方法释放此资源。这样的类要和其使用者约定:用完资源后必须调用close方法

    •执行异步任务的方法不应在dealloc里调用;只能在正常状态下执行的那些方法也不应在dealloc里调用,因为此时对象已处于正在回收的状态了

    dealloc方法:

    •不要随便调用其他方法,在这里无论调用什么方法都不太应该,因为对象此时“已近尾声”(in a winding-down state)。若所调用的方法又要异步执行任务或又要继续调用他们自己的某些方法,等到那些任务执行完毕时还行通知当前对象,而系统已经把当前待回收的对象彻底摧毁了。这会导致很多问题,经常crash。

    •dealloc方法所在的线程会执行“最终的释放操作”(final release),令对象的保留计数降为0,而某些方法必须在特定的线程里(如:主线程)调用才行。(若在dealloc里调用了哪些方法,则无法保证当前这个线程就是那些方法所需的线程)

    •不要调用属性的存取方法,因为有人可能会覆写这些方法,并于其中做一些无法在回收阶段安全执行的操作。另外属性可能正出去“键值观测”(KVO)机制的监控之下,该属性的观察者(observer)可能会在属性值改变时“保留”或使用这个即将回收的对象

    32、编写“异常安全代码”时留意内存管理问题

    虽然OC只有在发生严重错误导致程序无法继续运行时,才应跑出异常;但如果使用OC++编码或使用了第三方库抛出的异常不受控制时,就需要捕获及处理异常了。

    有些系统库也会抛出异常,如:使用KVO时,若注销一个尚未注册的“观察者”,则会抛出异常;

    发生异常时应如何管理内存又是个值得研究的问题。在try块中,如果先保留了某个对象,然后在释放它之前又抛出了异常,此时除非catch块能释放对象,否则就会导致内存泄露。

    ARC模式下,不会在finally块里加代码处理内存泄露问题,因为者需要添加大量的样板代码,会严重影响运行期的性能,即便在不抛异常时也如此。(而且添加的额外代码还会明显增加应用程序的大小。这些副作用都不甚理想)虽说默认不会添加,但可以通过-fobjc-arc-exceptions这个编译器标志来开启此功能。并且处于OC++模式时编译器会自动把-fobjc-are-exceptions标志打开。

    33、以弱引用避免保留还

    要点:

    •将某些引用设置为weak,可避免出现“保留环”

    •weak引用可自动清空,也可以不自动清空。自动清空(automiling)是随着ARC而引入的新特性,由运行期系统来实现。在具备自动清空功能的弱引用上,可随意读取其数据,因为这种引用不会指向已经回收过的对象

    34、以“自动释放池块”降低内存峰值

    要点:

    •自动释放池排布在栈中,对象收到·消息后,系统将其放入最顶端的池里

    •合理运用自动释放池,可降低应用程序的内存峰值

    •@autoreleasepool这种新式写法能创建出更为轻便的自动释放池

    35、用“僵尸对象”调试内存管理问题

    调式内存管理问题很令人头疼。(因为有些问题不是必现的!!!)

    大家都知道,向已回收的对象发送消息是不安全的。这么做有时可以,有时不行。具体可行,完全取决于对象所占内存有没有为其他内容所覆写。而这块内存有没有移做他用,又无法确定。因此,应用程序只是偶尔崩溃。在没用崩溃的情况下,那块内存可能只复用了其中一部分,所以对象中的某些二进制数据依然有效。

    还有一种可能,就是那块内存恰好为另外一个有效且存活的对象所占据。这种情况下,运行期系统会吧消息发到新对象那里,而此对象也许能应答,也许不能。如果能,那么程序就不崩溃,可你会觉得奇怪:为什么收到消息的对象不是预想的那个呢?若新对象无法响应选择子,则程序依然会崩溃。

    所幸Cocoa提供了“僵尸对象”(Zombie Object)模式:运行期系统会把所有已经回收的实例转化成特殊的“僵尸对象”,而不会真正回收它们。这种对象所在的核心内存无法重用,因此不可能遭到覆写。僵尸对象收到消息后,会抛出异常,并给出描述。

    开启方式:Scheme -> Run -> Diagnostics -> Enable Zombie Objects (勾选)

    runtime发现如果开启该模式,则NSObject的dealloc方法会被“调配”(swizzle),从而执行将对象的类改为指向_NSZombie_OriginalClass类。NSZombie类并未实现任何方法,没有超类,跟NSObject一样是个“根类”,该类只有一个实例变量isa,所有OC的根类都必须有此变量。由于这个轻量级的类没有实现任何方法,所以发给它的消息需要寄过“完整的消息转发机制”(full forwarding mechanism)。

    要点:

    •系统在回收对象时,可以不将其真的回收,而是将其转化为僵尸对象。通过环境变量NSZombieEnabled可开启此功能

    •系统会修改对象的isa指针,令其指向特说的僵尸类,从而使该对象变为僵尸对象。僵尸类能够响应所有的选择子,响应方式为:打印一条包含消息内容及其接受者的消息,然后终止应用程序

    36、不用使用retainCount

    OC通过引用计数类管理内存。每个人对象都有一个计数器,其值表明还有多少个其他对象想令此对象继续存活。对象创建好之后,其保留计数大于0。保留与释放操作分别会使计数递增or递减。当技术变为0时,对象就被系统回收了。

    retainCount无用的原因:

    •它所返回的保留计数只是某个给定时间点上的值。该方法并未考虑到系统会稍后把自动释放池清空,因而不会将后续的释放操作从返回值里减去,因此此值未必能真实反映实际的保留计数。

    •retainCount可能永远不返回0,因为有时系统会优化对象的释放行为,在保留计数还是1的时候就把它回收了。只有在系统不打算这么优化时,计数值才会递减至0。

    如果发现某个对象的内存泄露了,应该检查还有谁仍然保留这个数,并查明为何没有释放此对象。

    要点:

    •对象的保留计数看似有用,实则不然,因为任何给定时间点上的“绝对保留计数”(absolute retain count)都无法反映对象声明期的全貌

    •引入ARC之后,retainCount方法就正式废止了,在ARC下调用该方法会导致编译器报错

    六、块与大中枢派发

    37、理解“块”这一概念

    块其实就是个值,而且自有其相关类型。块类型的语法与函数指针近似。

    块的强大之处是:在声明它的范围里,所有变量都可以为其所捕获。有些变量若需在块内修改,需要加上__block修饰符。

    如果块所捕获的变量是对象类型,那么就会自动保留它。系统在释放这个块的时候,也会将其一并释放。

    descriptor:块对象的总体大小;声明了copy与dispose两个辅助函数对于的函数指针(在拷贝or丢弃块对象时执行)。

    块还会把它所捕获的所有变量都拷贝一份。这些拷贝放在descriptor变量后边,捕获了多少个变量,就要占用多少的内存空间。

    块定义时时存储在栈重的。一旦复制到堆上,块就成了带引用计数的对象了。后续的复制操作都不会真的执行复制,只是递增快对象的引用计数。

    要点:

    •块是C、C++、Objective-C中的词法闭包

    •块可接受参数,也可返回值

    •块可以分配在栈or堆上,也可以时全局的。分配在栈上的块可以拷贝到堆里,此时就跟标砖的OC对象一样,具备引用计数了

    38、为常用的块类型创建typedef

    使用块别名,当需要修改时只需修改块类型即可,无须修改所有使用到的地方。

    要点:

    •以typedef重新定义块类型,可令块变量用起来更加简单

    •定义新类型时应遵从现有的命名习惯,勿使用其名称与别的类型相冲突

    •不妨为同一个块签名定义多个类型别名,如果要重构的代码使用了块类型的某个别名,那么只需修改相应的typedef中的块签名即可,无须改动其他typedef

    39、用handler块降低代码分散程度

    笔者建议使用同一个块来处理成功与失败的情况:

    •缺点:全部逻辑写在一起会比较长和复杂

    •优点:处理成功响应的过程中可能会发现错误(更灵活)

    有时需要在相关时间点指向会掉操作,这种情况也可以使用handler块。

    NSNotificationCenter就提供了一个参数,可以让调用这指定块在哪个队列里执行。默认是跟通知同一个线程:

    - (id)addObserverForName:(NSString *)name 
                                    object:(id)object 
                       queue:(NSOperationQueue *)queue 
                  usingBlock:(void(^)(NSNotification *)block
    

    要点:

    •在创建对象时,可以使用内联的handler块将相关业务逻辑一并声明

    •在有多个实例需要监控时,如果采用委托模式,那么经常需要根据传入的对象来切换,而该用handler块来实现,则可直接将块与相关对象放在一起

    •设计API时如果用到了handler块,那么可以增加一个参数,使调用这可以通过此参数来决定应该把块安排在哪个队列上执行

    40、用块应用其所属对象时不要出现保留环

    要点:

    •如果块所捕获的对象直接或间接地保留了块本身,那么就得当心保留环问题

    •一定要找个适当的时机接触保留环,而不能把责任推给API的调用者

    41、多用派发队列、少用同步锁

    1、同步块:@synchronized(self)(多个属性时不宜这么写)

    2、使用锁:NSLockorNSRecursiveLock递归锁(线程能够多次持有该说锁,而不会出现死锁deadlock现象)

    3、GCD:串行同步队列

    _syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    - (NSString *)name {
      __block NSString *localName;
      dispatch_sync(_syncQueue, ^{
        localName = _name;
      });
      return localName;
    }
    - (void)setName:(NSString *)name {
      dispatch_barrier_sync(_syncQueue, ^{ // sync 比 async 效率高一些
        _name = name;
      });
    }
    

    [图片上传失败...(image-b812a9-1694433945326)]

    在队列中栅栏块必须单独执行,不能与其他块并行。这只对并发队列有意义,因为串行队列中的块总是按顺序逐个执行。并发队列如果发现接下来要处理的块使栅栏块(barrier block),那么就一直等当前所有并发块都执行完毕后才会单独执行这个栅栏块。待栅栏块执行过后,再按正常方式继续向下处理。

    最好还是测一测每种做法的性能,然后从中选出最合适当前场景的方案。

    要点:

    •派发队列可以用来表述同步语句,这种做法要比使用@synchronized块或NSLock对象更简单

    •将同步与异步派发结合起来,可以实现与普通加锁机制一样的同步行为,而这么做却不会阻塞执行异步派发的线程

    •使用同步队列及栅栏快,可以令同步行为更加高效

    42、多用GCD,少用performSelector系列方法

    NSObject定义了几个方法,可以推迟执行方法调用,也可以指定运行方法所用的线程。这些功能原来很有用,但是在出现了大中枢派发及块这样的新技术之后,就显得不那么必要了。虽说有些代码还是会经常用到它们,但笔者劝你还是避开为妙。

    如果选择子是在运行时决定的,那么就能体现出此方法的强大之处了:

    SEL selector;
    if ( /* some codition */ ) {
      selector = @selector(newObject); // 需要释放
    } else if ( /* some other codition */ ) {
      selector = @selector(copy); // 需要释放
    } else {
      selector = @selector(someProrperty); // 不需要释放
    }
    id ret = [object performSelector:selector];
    

    这种编程方式看起来比较灵活,但是在ARC模式下会发出警告:

    warning: performSelector may case a leak because its selector
    is unknow [-Warc-performSelector-leaks]
    

    因为编译器并不知道将要调用的选择子是什么,不了解其方法签名及返回值,甚至连私发欧有返回值都不清楚。所以没办法用ARC的内存管理规则来判定返回值是不是应该释放。鉴于此,ARC采用了比较谨慎的做法,就是不添加释放操作。因而可能导致内存泄露。

    这个问题很容易忽视,而且就算用静态分析器,也很难侦测到随后的内存泄露。(所以需谨慎使用performSelector方法)

    大中枢GCD出现之后,performSelector系列方法所提供的功能,都可以用GCD实现:

    // 延迟执行
    dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC));
    dispatch_after(time, dispatch_get_main_queue(), ^(void){
      [self doSomething];
    });
    // 把任务放到主线程执行
    dispatch_async(dipatch_get_main_queue(), ^{
      [self doSomething];
    });
    

    要点:

    •preformSelector系列方法在内存管理方面容易有疏失。它无法确定将要执行的选择子具体是什么,因而ARC编译器也就无法插入适当的内存管理方法

    •performSelector系列方法所能处理的选择子太过局限了,选择子的返回值类型及发送给方法的参数个数都受到限制

    •如果想把任务放在另一个线程上执行,那么最好不要用preformSelector系列方法,而是应该把任务封装到块里,然后调用大中枢派发机制的相关方法来实现

    43、掌握GCD及操作队列的使用时机

    开发者可以把操作以NSOperation子类的形式放在队列中,而这些操作也能后并发执行。虽说“操作队列”(operation queue)在GCD之前就有了,其中某些设计原理因操作队列而流行,GCD就是基于这些原理构建的。实际上,从iOS4与Mac OS10.6开始,操作队列在底层是用GCD来实现的。

    GCD是纯C的API,而操作队列这是OC的对象;在GCD中,任务用块来表示,而块时一个轻量级数据结构,而“操作”(operation)则是个更为重量级的OC对象。

    使用NSOperation及NSOperationQueue的好处:

    •取消某个操作。已经启动的任务无法取消(GCD队列的任务就无法取消)

    •指定操作间的依赖关系。

    •通过键值观察机制监控NSOperation对象的属性。isCancelled、isFinished…

    •指定操作的优先级。此操作与队列中其他操作之间的优先关系(GCD队列确实有优先级,不过那是针对整个队列的不是针对每个块的)

    •重用NSOperation对象。可以继承该类,存放任何信息。对象在执行时可以充分利用继承而来的各种信息,还可以随意调用其方法。

    • 操作队列有很多地方胜过派发队列:操作队列提供了多种执行任务的方式,而且都是写好的,直接就能使用。开发者不用再编写复杂的调度器,也不用自己来实现取消操作或指定操作优先级的功能,这些事情操作队列都已经实现好了。

    要点:

    •在解决多线程与任务管理问题时,派发队列并非唯一方案

    •操作队列提供了一套高层的OC API,能实现纯GCD所具备的绝大部分功能,而且还能完成一些更为复杂的操作,那些操作若改用GCD来实现,则需另外编写代码

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

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

    // 把任务编组
    // 方法1:
    void dispatch_group_async(dispatch_group_t group, 
                              dispatch_queue_t queue, 
                              dispatch_block_t block);
    // 方法2:
    void dispatch_group_enter(dispatch_group_t group);
    void dispatch_group_leave(dispatch_group_t group);
    
    // 等待group执行完毕:
    // 方法1:
    long dispatch_group_wait(dispatch_geoup_t group, 
                             dispatch_time_t timeout); // 会阻塞
    // 方法2:
    void dispatch_group_notify(dispatch_group_t group, 
                               dispatch_queue_t queue, 
                               dispatch_block_t block);
    

    要点:

    •一系列任务可归入一个dispatch group中。开发者可以在这组任务执行完毕时获得通知

    •通过dispatch group,可以在并发式派发队列里同事执行多项任务,此时GCD会根据系统资源状况来调度这些并发执行的任务。开发者若自己来实现此功能,则需编写大量代码

    45、使用dispatch_once来执行只需要运行一次的线程安全代码

    + (id)sharedInstance {
      static EOCClass *sharedInstance = nil;
      static dispatch_once_t onceToken; // 只初始化一次
      dispatch_once(&onceToken, ^{
        sharedInstance = [[self alloc] init];
      });
      return sharedInstance;
    }
    

    dispatch_once更高效,没有使用重量级的同步机制。

    要点:

    •经常需要编写“只需执行一次的线程安全代码”(thread-safe single-code execution)。通过GCD所提供的dispatch_once函数,很容易就能实现此功能

    •标记应该声明在static或global作用域中,这样的话,在把只需执行一次的块传给dispatch_once函数时,传进去的标记也是相同的

    46、不要使用dispatch_get_current_queue

    - (NSString *)name {
      __block NSString *localName;
      // 如果调用方法的队列恰好使_syncQueue,则会死锁 (同步线程里加同步事件)
      dispatch_sync(_syncQueue, ^{ 
        localName = _name;
      });
      return localName;
    }
    
    dispatch_queue_t queueA = dispatch_queue_create("com.mo.queueA", NULL);
    dispatch_queue_t queueB = dispatch_queue_create("com.mo.queueB", NULL);
    dispatch_sync(queueA, ^{
      dispatch_sync(queueB, ^{
        dispatch_block_t block ^{ /* ... */ };
        if (dispatch_get_current_queue() == queueA) { // dispatch_get_current_queue 返回的是queueB
          block();
        } else {
          dispatch_sync(queueA, block); // 所以还是会进入死锁
        }
      });
    });
    

    由于队列间有层级关系,所以“检查当前队列是否为执行同步派发所用的队列”这种办法,并不总是凑效。

    在这种情况下,正确的做法是:不要把存取方法做成可重入的,而是应该确保同步操作所有的队列,绝不会访问属性,也就是绝对不会调用name方法。这种队列只应该用来同步属性。由于派发队列是一种极为轻量的机制,所以不妨为每个每项属性创建专用的同步队列。

    GCD提供了一个功能,设定“队列特有数据”,可以把任意数据以键值对的形式关联到队列里。最重要的是,若在当前层级获取不到关联数据时,系统会沿着层级体系向上查找,直到 找到数据 / 到根队列 为止。

    dispatch_queue_t queueA = dispatch_queue_create("com.mo.queueA", NULL);
    dispatch_queue_t queueB = dispatch_queue_create("com.mo.queueB", NULL);
    dispatch_set_target_queue(queueB, queueA); // B嵌套在A里
    static int kQueueSpecific;
    CFStringRef queueSpecificValue = CFSTR("queueA");
    // 为queueA设置“队列特定值”
    dispatch_queue_set_specific(queueA, // 待设置数据的队列
                                &kQueueSpecific, // key
                                (void *)queueSpecificValue, // value 
                                (dispatch_funcion_t)CFRelease); // 析构方法
    dispatch_sync(queueB, ^{
      dispatch_block_t block = ^{
        NSLog(@"No deadlock!");
      };
      CFStringRef retrievedValue = dispatch_get_specific(&kQueueSpecific); // 获取队列特定值
      if (retrievedValue) { // 在A队列里
        block();
      } else {
        dispatch_sync(queueA, block);
      }
    });
    

    要点:

    •dispatch_get_current_queue函数的行为常常与开发者所预期的不同。此函数已经废弃,只应做调试之用

    •由于派发队列是按层级来组织的,所以无法单用某个队列对象来描述“当前队列”这一概念

    •Dispatch_get_current_queue函数用于解决由不可重入的代码所引发的死锁,然而能用此函数解决的问题,通常该用“队列特定数据”来解决

    七、系统框架

    47、熟悉系统框架

    框架:将一些列代码封装为动态库,并在其中放入描述其接口的头文件。

    主要框架:

    •Foundation:NSObject、NSArray、NSDictionary。。。(用NS前缀),还提供了collection等基础核心功能,还提供了字符串处理这样的复杂功能。如:NSLinguisticTagger可以解析字符串并找到其中的全名名词、动词、代词等。

    •CoreFoundation:Foundation框架中许多功能,都可以在此框架中找到对应的C语言API。“无缝桥接”(toll-free bridging)功能可以把此框架中的C语言数据结构平滑转换为Foundation中的OC对象,也可以方向转换。

    其他框架:

    •CFNetwork:提供了C语言级别的网络通信能力,将“BSD套接字”(BSD socket)抽象成易于使用的网络接口。而Foundation则将该框架里的部分内容封装为OC语言接口,以便进行网络通信,如:NSURLConnection从URL中下载数据

    •CoreAudio:提供C语言API可用来操作设备上的音频硬件。此框架属于比较难用的那种,因为音频处理本身就很复杂。所幸由这套API中可以抽象除另外一套OC式API,用后者来处理音频问题会简单些

    •AVFouncation:提供OC对象可用来回放并录制音频及视频,如:在UI视图类里播放视频

    •CoreData:提供OC接口可将对象放入数据库,便于持久保存。处理数据的获取及存储事宜,且可跨越Mac OS X及iOS平台

    •CoreText:提供C语言接口可以高效执行文字排版及渲染操作

    可以看出OC编程一项重要特点:经常要使用底层C语言API,好处是可以绕过OC的运行期系统,从而提升指向速度。当然ARC只负责OC对象,所以使用这些API时尤其需要注意内存管理问题。

    核心UI框架:Mac OS X的是AppKit、iOS的是UIKIt,都提供了构建在Foundaton与CoreFoundation之上的OC类。

    •CoreAnimation:OC写成,提供了一些工具,UI框架用这些工具来渲染图形并播放动画。CoreAnimation本身不是框架,是QuartzCore框架的一部分。

    •CoreGaphics:C语言写成的框架,提供了2D渲染所必备的数据结构与函数。其中定义了:CGPoint、CGSize、CGRect等数据结构。

    要点:

    •许多系统框架都可以直接使用。其中最重要的是Foudation与CoreFoundation,这两个框架提供了构建应用程序所需的许多核心功能

    •很多常见任务都能用框架来做,例如音频、视频处理、网络通信、数据管理等

    •请记住:用纯C写成的框架于用OC写成的一样重要,若想成为优秀的OC开发者,应该掌握C语言的核心概念

    48、多用块枚举,少用for循环

    1、for循环

    // 遍历NSArray
    for (int i = 0; i < anArray.count; i++) {
      id object = anArray[i];
    }
    // 遍历NSDictionary
    NSArray *keys = [aDic allKeys];
    for (int i = 0; i < keys.count; i++) {
      id key = keys[i];
      id value = aDic[key];
    }
    // 遍历NSSet
    NSArray *objects = [aSet allObjects];
    for (int i = 0; i < objects.count; i++) {
      id object = objects[i];
    }
    

    创建附加数组会有额外开销,而且还会多创建一组对象,它会保留collection中的所有元素对象。释放时这些附加对象也要释放,这样就调用了一些本来不需要执行的方法。其他各种遍历方式都无须创建这种中介数组。

    2、使用OC1.0的NSEnumerator来遍历

    NSEnumerator是一个抽象基类,其中自定义了两个方法,供子类实现:

    - (NSArray *)allObjects;
    - (id)nextObjects; // 返回枚举里的下一个对象
    

    Foundation框架中内建的collection类都实现了这种遍历方式:`

    // 遍历NSArray
    NSEunmerator *enumerator = [anArray objectEnumerator]; // reverseObjectEnumerator 方向遍历
    id object;
    while ((object == [enumerator nextObject]) != nil) {/*...*/}
    // 遍历NSDictionary
    NSEnumerator *enumerator = [aDic keyEnumerator];
    id key;
    while ((key = [enumerator nextObject]) != nil) {
      id value = aDic[key];
    }
    // 遍历NSSet
    NSEunmerator *enumerator = [aSet objectEnumerator];
    id object;
    while ((object == [enumerator nextObject]) != nil) {/*...*/}
    

    3、快速遍历

    OC2.0引入了快速遍历,为for循环开设了in关键字。从而大幅简化了遍历collectin所需的语法:

    // 遍历NSArray
    for (id object in anArray) {/*...*/}
    // 遍历NSDictionary
    for (id key in anDic) {
      id value = aDic[key];
    }
    // 遍历NSSet
    for (id object in aSet) {/*...*/}
    

    支持快速遍历,遵从NSFastEnumeration协议就好,只有一个协议方法:

    - (NSinterger)countBeyEnumeratingWithState:(NSFastEnumerationState *)state 
                                                                     objects:(id *)stackbuffer 
                                         count:(NSUInteger)length;
    

    NSEnumerator对象也实现了NSFastEnumeration协议,所以能用来执行方向遍历:

    for (id object in [anArray reverseObjectEnumerator])  {/*...*/}
    

    4、基于块的遍历方式(遍历时可以获取更堵信息)

    NSArray *anArray = [NSArray array];
    [anArray enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
      if (shouldStop) { 
        *stop = YES; // 优雅的终止遍历(其他的遍历可以用break终止)
      }
    }];
    NSDictionary *aDic = [NSDictionary dictionary];
    [aDic enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
      if (shouldStop) {
        *stop = YES;
      }
    }];
    NSSet *aSet = [NSSet set];
    // 可以修改方法签名,以免进行类型转换操作
    [aSet enumerateObjectsUsingBlock:^(NSString *obj, BOOL * _Nonnull stop) { 
      if (shouldStop) {
        *stop = YES;
      }
    }];
    

    可以利用另一个版本指向方向遍历、并发遍历:

    // NSEnumerationConcurrent 并发的方式遍历
    // NSEnumerationReverse 方向遍历
    [anArray enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
    }];
    

    要点:

    •遍历collection有四种方式。最基本的办法是for循环,其次是NSEnumerator遍历法及快速遍历法。最新、最先进的方式则是“块枚举法”

    •“块枚举法”本身就能通过GCD来并发执行遍历操作,无须另行编写代码。而采用其他遍历方式则无法轻易实现这一点

    •若提前知道代码里的collection含有何种对象,则应该修改块签名,指出对象的具体类型

    49、对自定义其内存管理语义的collection使用无缝桥接

    NSArray *anNSArray = @[@1, @2, @3];
    CFArrayRef aCFArray = (__bridge CFArrayRef)anNSArray;
    CFArrayGetCount(aCFArray); // array.count
    // __bridge: ARC仍拥有该OC对象的所有权
    // __bridge_retained: ARC交出该OC对象的所有权,之后需要 CFRealease(aCFArray) 来释放
    NSArray *anNSArray = (__bridge_transfer)aCFArray; // ARC获得所有权
    

    无缝桥接必要性:因为Foundation框架中OC类所具备的某些功能,是CoreFoundation框架中的C语言数据结构所不具备的,反之亦然。

    要点:

    •通过无缝桥接技术,可以在Foundation框架中昂的OC对象与CoreFoundaton框架中的C语言数据结构之间来回转换

    •在CoreFoundation层面创建collection时,可以指定许多回调函数,这些函数表示此collection应如何处理其元素。然后可运用无缝桥接技术,将其转换成具备特殊内存管理语义的OC collection

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

    NSCache:当系统资源将耗尽时,自动删减缓存(若用普通字典,需要自己写挂钩,在“低内存”警告时通知删减缓存)。 还会先行删减“最久未使用的”对象,不会“拷贝”键,而是会“保留”它(当键不支持拷贝时很合适)。是线程安全的。可以设置缓存 对象总数 和 “总开销”。

    NSCache搭配NSPureableData使用:

    typedef void(^EOCNetworkFetcherCompletionHander)(NSData *data);
    NSCache *_cache;
    - (instancetype)init {
      self = [super init];
      if (self) {
        _cache = [NSCache new];
        _cache.countLimit = 100; // 100 URLs
        _cache.totalCostLimit = 5 * 1024 * 1024; // 5MB
      }
      return self;
    }
    - (void)downloadDataForUrl:(NSURL *)url {
      NSPurgeableData *cachedData = [_cache objectForKey:url];
      if (cachedData) { // Cache hit
    //    cachedData.isContentDiscarded // 相关内存是否已释放
        [cachedData beginContentAccess]; // 告诉它不应丢弃自己所占用的内存
        [self useData:cachedData];
        [cachedData endContentAccess]; // 告诉它在必要时可以丢弃自己所占用的内存
      } else { // Cache miss
        EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
        [fetcher startWithCompletionHander:^(NSData *data){
          NSPurgeableData *purgeableData = [NSPurgeableData dataWithData:data];
          [_cache setObject:purgeableData forKey:url cost:purgeableData.length];
          [self useData:purgeableData];
          [purgeableData endContentAccess];
        }];
      }
    }
    

    要点:

    •实现缓存时应选用NSCache而非NSDictionary对象。因为NSCache可以提供优雅的自动删减功能,而且是“线程安全的”,此外,它与字典不同,并不会拷贝键值

    •可以给NSCache对象设置上限,用以限制缓存中的对象总个数,但是绝不要把这些设置当成可靠的“硬限制”,他们仅对NSCache起指导作用

    •将NSPurgeableData与NSCache搭配使用,可实现自动清楚数据的功能。及当NSPurgeableData对象所占用内存被系统丢弃时,该对象自身也会从缓存中移除

    •如果缓存使用得当,那么应用程序的响应速度就能提高。只有那种“重新计算起来很费事的”数据,才值得放入缓存,如:需要从网络获取或从磁盘读取的数据

    51、精简load与initialize的实现代码

    load:当包含类或分类的程序库载入系统时,就会执行此方法。iOS指应用程序启动时。load方法中使用其他类时不安全的(如:其他类又用到了当前类,则无法正确加载了)。整个应用程序在执行load方法时都会阻塞(不要在里面等待锁/加锁)。总之,能不做的事情就别做。

    initialize:在程序首次用该类之前调用,且只调用一次。它时运行期系统来调用的,绝不应该通过代码直接调用。运行期系统在执行该方法时处于正常状态,可以安全使用并调用任意类中的任意方法。运行期系统会确保其在“线程安全的环境”中执行。遵循继承规则。

    只应用来设置内部数据,不应调用其他方法,即便时本类自己的方法,也最好别调用。

    initialize需要保持精简的原因:

    •对某个类而言,任何线程都可能初次使用到它,若碰巧时UI线程,那么初始化期间会一直阻塞,导致应用无法响应。

    •开发者无法控制类的初始化时机。不能令代码依赖特定的时间点,否则会很危险

    •若某个了实现很复杂,那么其中可能直接或间接用到其他类。若那些类尚未初始化,则系统会迫使其初始化。其他类的初始化又可能依赖本类的某些数据。代码就无法正常运行了。

    要点:

    •在加载阶段,如果类实现了load方法,那么系统就会调用它。分类也可以定义此方法,类的load方法要比分类中的先调用。与其他方法不同,load方法不参与覆写机制。

    •首次使用某个类之前,系统会向其发送initialize消息。由于此方法遵从普通的覆写规则,所以通常应该在里面判断当前要初始化的是哪个类

    •load与initialize方法都应该实现的精简一些,这有助于保持应用程序的响应能力,也能减少引入“依赖环”的几率

    •无法在编译器设定的全局常亮,可以放在initialize方法里初始化

    52、别忘了NSTimer会保留其目标对象

    要点:

    •NSTimer对象会保留其目标,直到计时器本身失效为止,调用invalidate方法可令计时器失效,另外,一次性的计时器在触发完任务之后也会失效

    •反复执行任务的计时器,很容易引入保留环,如果这种计时器的目标又保留了计时器本身,那肯定会导致保留环。这种关系,可能直接发送,也可能通过对象图里的其他对象间接发生

    •可以扩充NSTimer的功能,用“块”来打破保留环。不过除非NSTimer将来在公共接口里提供此功能,否则需创建分类,将相关实现代码加入其中

    相关文章

      网友评论

          本文标题:编写高质量iOS与OS X代码的52个有效方法(三)

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