美文网首页iOS DevelopmentiOS Developer
编写高质量iOS与OS X代码的有效方法

编写高质量iOS与OS X代码的有效方法

作者: 勇敢的_心_ | 来源:发表于2018-01-15 17:32 被阅读77次

    oc语言特性

    oc使用动态绑定的消息结构,在运行时才会检查对象类型。接收消息后,执行代码,由运行环境而非编译器来决定。

    面向对象语言,"对象"就是"基本构造单元",开发者通过对象来存储并传递数据。在对象之间传递数据并执行任务的过程就叫做"消息传递"。

    使用消息结构的语言,其运行时所执行的代码由运行环境来决定;而使用函数调用的语言,由编译器决定。如果调用的函数是多态的,那么在运行时就按照“虚方法表”来查出到底应该执行哪个函数实现。而采用消息结构的语言,不论是否多态,总是在运行时才会去查找所要执行的方法。实际上,编译器甚至不关心接收消息的对象是何种类型。接收消息的对象问题也要在运行时处理,甚至过程叫做“动态绑定”。

    方法

    1. 在类的头文件中尽量少引入其他头文件

    • 在编译使用PersonTest类文件时,不需要知道PersonTest类的全部细节,只需知道类名就好,使用向前声明(@class PersonTest;)就行。
    • 除非确有必要,否则不要引入头文件。一般应在某个类的头文件中使用向前声明(@class 类名)来提及别的类,并在实现文件中引用那些类的头文件。这样做尽量降低类之间的耦合。
    • 有时无法使用向前声明,比如要声明某个类遵循一项协议。这种情况,尽量吧“该类遵循某协议”的这条声明移至“calss-continuation分类”中。若不行,就把协议单独放在一个头文件中,然后将其引入。

    2. 多用字面量语法,少用与之等价的方法

    • 使用字面量语法创建字符串、数值、数组、字典,与常规方法比更简明扼要
    • 应该通过取小标操作来访问数组下标或字典中键所对应的元素
    • 用字面量语法创建数组或字典时,若值中有nil,则会抛出异常,终止程序,其语法更安全
    • 局限性:除字符串外,所创建的对象必须属于Foundation框架才行
        NSNumber *number = [NSNumber numberWithInt:1];
        NSNumber *num2 = @1;
        NSNumber *booln = @YES;
        NSNumber *cn = @'a';
        NSArray *ary = [NSArray arrayWithObjects:@"cat",@"tom",@"mouse", nil];
        NSArray *ary1 = @[@"cat",@"tom"];
        NSString *dog = [ary objectAtIndex:1];
        NSString *dog1 = ary[1];
        NSDictionary * pd = [NSDictionary dictionaryWithObjectsAndKeys:@"Tom",@"name",[NSNumber numberWithInt:26],@"age", nil];
        NSDictionary *pd1 = @{@"name":@"Tom",
                              @"age":@26};
    

    3. 多用类型常量,少用 #define 预处理指令

    • 不要用预处理指令定义常量。这样定义出来的常量不含类性信息,编译器只会在编译前据此执行查找与替换操作。即使有人重新定义了常量值,编译器也不会产生警告
    • 在实现文件中使用static const 来定义"只在编译单元内可见的常量"。由于此类常量不会在全局符号表中,所以无需为其名称加前缀
    • 在头文件中使用extern 来声明全局常量,并在相关实现文件中定义其值。这种常量要出现在全局符号表中,所以其名称应加以区隔,通常用与之相关的类名做前缀
        #define ANIMATION_DURATION 0.3
        static const NSTimeInterval kAnimationDuration = 0.3;
        extern const NSTimeInterval TestAnimationDuration;
        const NSTimeInterval TestAnimationDuration = 0.3;
    

    4. 用枚举表示状态、选项、状态码

    • 应该使用枚举来表示状态机的状态、传递给方法的选项以及状态码等值
    • 如果把传递给某个方法的选项表示为枚举类型,而多个选项又可同时使用,那么就将个选项值定义为2的幂,以便通过按位或操作将其组合起来
    • 用NS_ENUM与NS_OPTIONS宏来定义枚举,并指明其底层数据类型。这样做可以确保枚举是用开发者所选的底层数据类型实现的,而不会采用编译器所选的类型
    • 在处理枚举类型的switch语句中不要实现default分支。这样的话,加入新枚举之后,编译器就会提示开发者:switch语句并未处理所有枚举

    对象、消息、运行期

    5. 理解"属性"这一概念

    "属性"用于封装对象中的数据。对象通常会把其所需要的数据保存为各种实例变量。实例变量一般通过"存取方法"来访问。其中,"获取方法"(getter)用于读取变量值,而"设置方法"(setter)用于写入变量值.开发者可令编译器自动编写与属性相关的存取方法。此特性引入了一种新的“点语法”,使开发者可以更容易的依照类对象来访问其中的数据。

    存取方法有着严格的命名规范,所以OC语言才能根据名称自动创建存取方法,@property语法等同与写一套存取方法,@property NSString*name就是编译器自动写出一套存取方法。

    若不想令编译器自动合成存取方法,则可以自己实现,如果你只实现了其中一个存取方法,那么另一个还是由编译器来合成。使用@dynamic关键字,可以阻止编译器自动合成存取方法。

    属性特质: 原子性、读/写权限、内存管理语义、方法名

    @property(nonatomic,readwrite,copy) NSString *firstName
    @property(nonatomic,readwrite,copy,getter=isOn) BOOL  on
    
    • 原子性
      默认情况下,由编译器所合成的方法会通过锁定机制确保其原子性。
      atomic与nonatomic区别?

    • 具备atomic特质的获取方法会通过锁定机制来确保其操作的原子性,就是说,如果两个线程读写同一属性,那么不论何时,总能看到有效的属性值。若是不加锁的话(或者使用nonatomic语义),那么当其中一个线程在改写某属性值时,另外一个线程也许会突然闯入,把尚未修改好的属性值读取出来。发生这种情况时,线程读到的属性值可能不对。

    • 使用nonatomic历史原因:在iOS中使用同步锁的开销较大,会带来性能问题。一般情况下并不要求属性必须是“原子的”,因为这样并不能保证“线程安全”,若要实现“线程安全”的操作,还需采用更深层次的锁定机制。如一个线程在连续多次读取属性值的过程中有别的线程在同时修改该值,即使用atomic,也还是会读到不同的属性值。

    • 读写权限

    • readwrite(读写)特质属性拥有"获取方法"与"设置方法";

    • readonly(只读)特质属性只拥有"获取方法";

    • 内存管理语义

    • assign "设置方法"只会执行针对"纯量类型"(如CGFloat、NSInteger等)的简单赋值操作

    • strong 此特质表明该属性定义了一种"拥有关系"。为这种属性设置新值时,设置方法会先保留新值,并释放旧值,然后将新值设置上去

    • weak 此特质表明该属性定义了一种"非拥有关系"。 为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同assign类似,然而在属性所指的对象遭到摧毁时,属性也会清空

    • unsafe_undertained 此特质的语义和assign相同,但是它适合用于"对象类型",该特质表达一种"非拥有关系",当目标对象遭到摧毁时,属性值不会自动清空,这一点与weak区别

    • copy 此特质所表达的所属关系与strong类似。然而设置方法并不保留新值,而是将其"拷贝"。

    • 方法名

    • getter =<name>

    • setter=<name>

    • 多使用nonatomic属性,因为atomic属性会影响性能

    • 通过"特质"来指定存储数据所需的正确语义

    6. 在对象内部尽量直接访问实例变量

    不经过OC的"方法派送"步骤,所以直接访问实例变量的速度比较快,此情况下,编译器所生成的代码会直接访问保存对象实例变量的那块内存。
    直接访问实例变量不会触发“键值观测”通知;
    直接访问实例变量有助于排查与之相关的错误,加“断点”,监控该属性的调用者及访问时机;

    直接访问实例变量不会调用其"设置方法",绕过了相关属性所定义的"内存管理语义"。

    • 在对象内部读取数据时,应该直接通过实例变量来读,而写入数据时,则通过那个属性来写
    • 在初始化方法及dealloc方法中,总是应该直接通过实例变量来读写数据
    • 有时会使用惰性初始化技术配置某份数据,此情况,需要通过属性来读取数据

    7. 理解"对象等同性"概念

    • 若检测对象的等同性,提供"isEqual:"与"hash"方法
    • 相同对象具有相同的哈希码,但是两个哈希码相同的对象却未必相同
    • 不要盲目的逐个检测每条属性,而是应该依照具体需求来制定检测方案
    • 编写hash方法时,应该使用计算速度快而且哈希码碰撞几率低的算法

    8. 以"类族模式"隐藏实现细节

    • 类族模式可以把实现细节隐藏在一套简单的公共接口后面
    • 系统框架常使用类族
    • 从类族的公共抽象类中继承子类要当心
    * 子类应该继承自类族中的抽象基类
    * 子类应该定义自己的数据存储方式
    * 子类应当覆写超类文档中指明需要覆写的方法
    

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

    有时候类的实例可能是有某种机制所创建的,而开发者无法令这种机制创建出自己所写的字类实例,这时候就需要关联对象解决问题。

    * void objc_setAssociatedObject(id object,void *key,id value,objc_AssociationPolicy policy)
    此方法以给定的键和策略为某对象设置关联对象值
    * id objc_getAssociatedObject(id object,void *key)
    此方法根据给定的键从某对象中获取相应的关联对象值
    * void objc_removeAssociatedObject(id object)
    此方法移除指定对象的全部关联对象
    
    例子将创建警告视图与处理操作结果的代码放在一起:
    #import <objc/runtime.h>
    static void *EOCMyAlertViewKey = "ECOMyAlertViewKey";
    - (void)askUserAQuestion{
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Question" message:@"What are you doing?" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Continue", nil];
        void(^block)(NSUInteger) = ^(NSUInteger buttonIndex){
            if(buttonIndex == 0 ){
                NSLog(@"doCancel");
            }else{
                NSLog(@"doContinue");
            }
        };
        
        objc_setAssociatedObject(alert, EOCMyAlertViewKey, block, OBJC_ASSOCIATION_COPY);
        [alert show];
        
    }
    - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{
        void(^block)(NSUInteger) = objc_getAssociatedObject(alertView, EOCMyAlertViewKey);
        block(buttonIndex);
    }
    
    • 可以通过"关联对象"机制来把两个对象连起来
    • 定义关联对象时可指定内存管理语义,用以模仿定义属性所采用的"拥有关系"与"非拥有关系"
    • 只有在其他做法不可行时才因选用关联对象,因为这种做法通常会引入难于查找的bug

    给分类添加属性:
    //使用前记得#import <objc/runtime.h>

    - (void)setName:(NSString *)name{
        // 保存name
        // 动态添加属性 = 本质:让对象的某个属性与值产生关联
        /*
         object:保存到那个对象中
         key:用什么属性保存 属性名
         value:保存值
         policy:策略,strong,weak
         */
        objc_setAssociatedObject(self, "name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        // _name = name;
    }
    - (NSString *)name{
        return objc_getAssociatedObject(self, "name");
        // return _name;
    }
    

    10. 理解objc_msgSend的作用

    对象调用方法,oc术语叫"消息传递",消息有"名称"(name)或"选者子"(selector),可以接受参数,可有返回值。

    void objc_msgSend(id self,SEL cmd,...)
    这个是"参数个数可变的函数",能接受两个或两个以上的参数.第一个参数代表接收者,第二个参数代表选择子(SEL是选择子的类型),后续参数是消息中的参数,其顺序不变。选择子指的就是方法的名字。

    objc_msgSend函数会根据接收者与选择子来调用适当的方法。为了完成此操作,该方法需要在接收者所属的类中搜寻其"方法列表",如果能找到与选择子名称相符的方法,就跳至其实现代码。若找不到,那就沿着继承体系继续向上查找,等找到合适的方法之后再跳转。如果最终还是找不到相符的方法,那就执行"消息转发"操作。

    • 消息由接收者、选择子及参数构成。给某对象"发送消息"也就是相当于在该对象上"调用方法"
    • 发给某对象全部消息都要由"动态消息派发系统"来处理,该系统会查出对应的方法,并执行起代码

    11. 消息转发机制

    消息转发分为两大阶段:
    第一阶段先征询接收者,所属的类,看其是否能动态添加方法,以处理这个"未知的选择子"(unknow selector),这叫做"动态方法分析"(先判断这个类是否能新增一个实例方法用以处理此选择子)。
    第二个阶段涉及"完整的消息转发机制"。如果运行期系统已经把第一阶段执行完了,那么接收者自己无法在已动态新增方法的手段来响应包含该选择子的消息。此时,运行期系统会请求接收者已其他手段来处理与消息相关的方法调用。细分两小步1.请接收者看看有没有其他对象能处理这条消息。若有,则运行系统会把消息传给那个对象,于是消息转发结束。若没有“备援接收者”则启动完整的消息转发机制,运行系统会把与消息有关的全部细节都封装到NSInvocation对象中,再给接收者最后一个机会,令其设法解决当前还未处理的这条消息.

    动态方法解析:
    对象在收到无法解读的消息后,首先将调用其所属类的类方法:
    +(BOOL)resolveInstanceMethod:(SEL)selector
    该方法的参数就是那个未知的选择子,返回类型为bool类型,表示这个类是否能新增一个实例方法用以处理此选择子。
    使用这方法前提是:相关方法的实现代码已经写好,只等着运行的时候动态插在类里面就行了。此方案常用来实现@dynamic属性,比如要访问CoreData框架中NSManagedObjects对象的属性时就可以用,因为实现这些属性所需的存取方法在编译器就能确定。

    备援接收者:
    当前接收者还有第二次机会能处理未知的选择子,这一步中,运行期系统会问他:能不能把这条消息转发给其它接收者来处理。该步骤对应处理方法:
    -(id)forwardingTargetForSelector:(SEL)selector
    方法参数代表未知的选择子,若当前选择子能找到备援对象,则将其返回,若找不到,返回nil。

    完整的消息转发:
    若转发算法来到这一步,那么只能启用完整的消息转发机制了。
    首先创建NSInvocation对象,把与尚未处理的那条消息有关的全部细节都封装与其中。此对象包含选择子、目标及参数。在触发NSInvocation对象时,"消息派发系统"将亲自出马,把消息指派给目标对象。此步骤会调用下列方法来转发消息:- (void)forwardInvocation:(NSInvocation*)invocation
    此方法很简单:只需改变调用目标,使其消息在新目标上得以调用即可。实现此方法,若发现某调用操作不应由本类处理,则需调用超类的同名方法。这样,继承体系中的每个类都有机会处理此调用方法,直至NSObject。若最后调用NSObject类的方法,那么该方法还会继续调用"doesNotRecognizeSelector:"以抛出异常,此异常表明选择子最终未能得到处理。

    • 若对象无法响应某个选择子,则进入消息转发流程
    • 通过运行期的动态方法解析功能,我们可以在需要用到某个方法再将其加入类中
    • 对象可以把其无法解读的某些选择子转交给其他对象来处理
    • 经过上述两步之后,如果还是没办法处理选择子,那就启动完整的消息转发机制

    12. 用"方法调配技术"调试"黑盒方法"

    类的方法列表会把选择子的名称映射到相关的方法实现上,使得"动态信息派发系统"能够据此找到应该调用的方法。这些方法均已函数指针的形式来表示,此指针叫做IMP。
    oc运行期系统提供了几种方法可以让我们操作IMP(指针),开发者可以向其新增选择子,也可以改变某选择子所对应的方法实现,还可以交换两个选择子所映射到的指针。

    交换实现方法
    void method_exchangeImplementations(Method m1,Method m2)
    获取实现方法
    Method class_getInstanceMethod(Class aClass,SEL aSelector)
    
    • 在运行期,可以向类中新增或替换选择子所对应的方法实现
    • 使用另一份实现来替换原来的方法实现,这道工序叫做"方法调配",开发者常用此技术向原有的实现中添加新功能
    • 一般只有调试程序的时候才需要在运行期修改方法实现,这种做法不宜滥用

    13. 理解"类对象"的用意

    "在运行期检视对象类型"这一操作也叫做"类型信息查询",这个强大而有用的特性内置于Foundation框架的NSObject协议里,凡是由公共根类继承而来的对象都要遵从此协议。

    每个OC对象对象实例都是指向某块内存数据的指针。所以在声明变量时,类型后面要加上"*"字符:NSString * s = @"someString";

    对象数据结构
    typedef struct objc_object{
        Class isa;
    } *id;
    

    每个对象数据结构的首个成员时Class类的变量。该变量定义了对象所属的类,通常称为"is a"指针。

    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;
    };
    

    此结构体存放类的"元数据",例如类的实例实现了几个方法,具备多少个实例变量等信息。此结构体的首个变量也是isa指针,这说明Class本身亦为OC对象。结构体里还有个变量叫做super_class,它定义了本类的超类。类对象所属的类型(也就是isa指针所指向的类型)是另外一个类,叫做"元类",用来表述对象本身所具备的元数据。"类方法"就定义在此处,因为这些方法可以理解成类对象的实例方法。每个类仅有一个"类对象",而每个"类对象"仅有一个与之相关的"元类"。super_class指针确立了继承关系,而isa指针描述里实例所属的类。

    • 每个实例都有一个指向Class对象的指针,用以表面其类型,而这些Class对象则构成了类的继承体系
    • 如果对象类型无法在编译期确定,那么就应该使用类信息查询方法来探知
    • 尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象实现了消息转发功能

    接口与API设计

    14. 用前缀避免命名空间冲突

    • 选择公司、应用程序或二者关联之名称作为类名前缀
    • 若自己所开发的程序库中用到了第三方库,则应该为其中的名称加上前缀

    15. 提供"全能初始化方法"

    • 若全能初始化方法与超类不同,则需要覆写超类中的对应方法
    • 若超类的初始化方法不适应于子类,那么应该覆写这个超类方法,并在其中抛出异常

    16. 实现description方法

    • 实现description方法返回一个有意义的字符串,用以描述该实例
    • 若想在调试时打印出更详尽的对象描述信息,则应实现debugDescription方法

    17. 尽量使用不可变对象

    • 若某属性仅可与对象内部修改,则在"class-continuation分类"中将其由readonly属性扩展为readwrite属性
    • 不要把可变的collection作为属性公开,而应该提供相关方法,以此修改对象中的可变collection

    18. 使用清晰而协调的命名方式

    • 遵从oc命名规范

    19. 为私有方法名加前缀

    • 给私有方法的名称加前缀,这样可以很容易的将其同公共方法区分开
    • 不要单用一个下划线做私有方法的前缀,因为这种做法是预留给苹果公司用的

    20. 理解oc错误类型

    • 只有发生可使整个应用程序崩溃的严重错误时,才应使用异常
    • 在错误不严重的情况下,可以指派"委托方法"来处理错误,
      也可以把错误信息放在NSError对象里,经由"输出参数"返回给调用者

    21. 理解NSCopying协议

    • 要想令自己所写的对象具有拷贝功能,则需实现NSCopying协议,该协议只有一个方法:-(id)copyWithZone:(NSZone*)zone;
    • 如果自定义的对象分为可变版本和不可变版本,那么就要同时实现NSCopying与NSMUtableCopying协议
    • 如果你写的对象需要深拷贝,那么可以考虑新增一个专门执行深拷贝的方法
    • 深拷贝:在拷贝对象时,将其底层数据也一并复制过去。
      浅拷贝:只拷贝容器对象本身

    协议与分类

    OC语言特性:“协议”,与Java的"接口"类似。OC不支持多重继承,因而把某个类应该实现的一系列方法定义在协议里面。

    OC语言特性:"分类",利用分类机制,无需继承子类即可直接为当前类添加方法。

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

    委托模式:定义一套接口,某对象若想接受另一个对象的委托,则需遵从此接口,以便成为其"委托对象",而这"另一个对象"则可以给其委托对象回传一些信息,也可以在发生相关事件时通知委托对象。
    此模式可将数据与业务逻辑解耦;

    • 委托模式为对象提供了一套接口,使其可由此将相关事件告知其他对象
    • 将委托对象应该支持的接口定义成协议,在协议中把可能需要处理的时间定义成方法
    • 当某对象需要从另外一个对象中获取数据时,可以使用委托模式,此情况,该模式亦称"数据源协议"
    • 若有必要,可实现含有位段的结构体,将委托对象是否能响应相关协议方法这一信息缓存至其中

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

    • 通过分类机制,可把类代码分成多个容易管理的小块,以便单独检视,便于调试;
    • 将应该视为"私有"的方法归入Private的分类中,以隐藏实现细节;

    24. 为第三方类的分类名称加前缀

    • 向第三方类中添加分类时,给其名称加前缀,给其中的方法加前缀

    25. 勿在分类中声明属性

    • 分类机制,应该理解为一种手段,目标在于扩展类的功能,而非封装数据;
    • 把封装数据所用的全部属性都定义在主接口里,这样更加清晰
      *在"class-continuation分类" 之外的其它分类中尽量不要定义属性

    26. 使用"class-continuation分类"隐藏实现细节

    "class-continuation分类"和普通的分类不同,它必须定义在其所接续的那个类的实现文件里。中重要之处在于,这是唯一能声明实例变量的分类,而且此分类没有特定的实现文件,其中的方法都应该定义在类的主实现文件里。此分类没有名字,比如有个类叫做Person,其"class-continuation分类"写法如下:
    @interface Person(){
    NSString * _anInstanceVariable; // 实例变量
    }
    // Methods here

    @end

    • 通过"class-continuation分类'向类中新增实例变量
    • 把私有方法的原型声明在"class-continuation分类"中
    • 如果某属性在主接口中声明为"只读",而类的内部又要用设置方法修改此属性,那么就在"class-continuation分类"中将其扩展为"可读写"
    • 若想使类所遵循的协议不为人知,则可于"class-continuation分类"中声明

    27. 通过协议提供匿名对象

    可以用协议把自己所写的API之中的实现细节隐藏起来,将返回的对象设计为遵从此协议的纯id类型。这样的话,想要隐藏的类名就不会出现在API之中了。此概念称为"匿名对象".

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

    内存管理

    OC语言使用引用计数来管理内存,即每个对象都有个可以递增或递减的计数器。

    28. 引用计数

    引用计数机制通过可以递增递减的计数器来管理内存。

    对象创建出来时,其保留计数至少为1.若想令其继续存活,则调用retain方法。要是某部分代码不再使用此对象,不想令其继续存活,那就调用release或autorelease方法。最终当保留计数归零时,对象就回收了,也就是说,系统会将其占用的内存标记为"可重用"(reuse)。此时,所有指向该对象的引用也就变得无效了。

    29. ARC

    ARC实际上也是一种引用计数机制,ARC几乎把所有的内存管理事宜都交给编译器来决定。
    ARC自动执行retain,release,autorelease等操作,ARC在调用这些方法时,并不通过普通的OC消息派发机制,而是直接调用底层C语言版本。这样做性能更好,因为保留及释放操作需要频繁执行,所以直接调用底层函数能节省很多CPU周期。

    • ARC管理oc对象内存,注意:CoreFoundation对象不归ARC管理,开发者需适度调用CFRetain/CFRelease

    30. 在dealloc方法中只释放引用并解除监听

    对象在经历生命周期后,最终会被系统回收,当保留计数降为o的时候,执行dealloc方法。

    • dealloc方法主要是释放对象所拥有的引用,也就是把所有OC对象都释放掉。另一件事,就是把原来配置过的观测行为都清理掉。
    • 如果对象持有文件描述符等系统资源,那么应该专门编写一个方法来释放此种资源。这样的类要和其使用者约定:用完资源后必须调用close方法;
      开销大或系统内稀缺的资源不应该在dealloc中释放,文件描述,套接字,大块内存等都属于这种资源。不能指望dealloc方法必定会在某个特定的时机调用,因为有一些无法预料的东西可能也持有此对象。此情况,如果非要等到系统调用dealloc方法时才释放,那么保留这些稀缺资源的时间就有些过长了,通常做法是:实现另一个方法,当应用程序用完资源对象后,就调用此方法。这样,资源对象的生命周期就变得更明确了。
    • 执行异步任务等方法不应该在dealloc里调用,只能在正常状态下执行的那些方法也不应在dealloc里调用,因为此时对象已处于正在回收的状态了。

    31. 编写"异常安全代码"时注意内存管理问题

    TestObject *object;
    @try{// 测试代码
    }@catch(){
    NSLog(@" there was an error");
    }@finally{
    [object release];
    }
    
    • 捕获异常时,要注意将try块内所创立的对象清理干净
    • 默认情况,ARC不生成安全处理异常所需要的清理代码。开启编译器标志后,可生成这种代码,不过会导致应用程序变大,而且会降低运行效率

    32. 以弱引用避免保留环

    • 将某些引用设为weak可避免出现"保留环"
    • weak引用可以自动清空,也可以不自动清空。

    33. 以"自动释放池"降低内存峰值

    @autoreleasepool{
        // ...
    }
    
    • 自动释放池排布在栈中,对象受到autorelease消息后,系统将其放入最顶端的池里
    • 合理利用自动释放池,可降低应用程序的内存峰值

    34. 用"僵尸对象"调试内存管理问题

    Cocoa提供了"僵尸对象",启用这项调试功能后,运行期系统会把所有的已经回收的实例转化成特殊的"僵尸对象",而不会真正回收它们。这种对象所在的核心内存无法重用,因此不可能遭到覆写。僵尸对象收到消息后,会抛出异常,其中准确的说明了发送过来的消息,并描述了回收之前的那个对象。僵尸对象是调试内存管理问题的最佳方式。
    将NSZombieEnabled环境变量设置为YES,即开启此功能。
    Xcode开启:Scheme-Diagnostics-enable Zombie Objects

    僵尸对象工作原理:系统在即将回收对象时,如果发现通过环境变量启用了僵尸对象功能,那么还将执行一个附加步骤。这一步就是把对象转化为僵尸对象,而不彻底回收。

    • 系统在回收对象时,可以不将其真的回收,而是把它转化为僵尸对象。通过环境变量NSZombieEnabled可以开启此功能
    • 系统会修改对象的isa指针,令其指向特殊的僵尸类,从而使该对象变为僵尸对象。将是类能够响应所有的选择子,响应方式为:打印一条包含消息内容及其接收者的消息,然后终止应用程序

    35. 不要使用retainCount

    • 对象的保留计数看似有用,实则不然,因为任何给定时间点上的"绝对保留计数"都无法反映对象生命期的全貌
    • 引用ARC后,retainCount方法就正式废止了,在ARC下调用该方法会导致编译器报错

    块与大中枢派发

    36. 块

    块与函数类似,只不过是直接定义在另一个函数里的,和定义它的那个函数共享同一个范围内的东西。块用"^"符号表示,后面跟一对花括号,括号里面是块的实现代码。
    ^{
    // 实现代码
    }
    块其实就是个值,而且自有其相关类型。与int,float或OC对象一样,也可以把块赋值给变量,然后像是用其他变量那样使用它。
    块类型语法结构:return_type (^block_name)(parameters)

    块的强大之处:在声明它的范围里,所有变量都可以为其所捕获。也就是说,那个范围里的全部变量,在块里依然可用。

    int (^addBlock)(int a,int b) = ^(int a,int b){
    return a+b;
    };
    int add = addBlock(2,5);

    声明变量的时候加上_block修饰符,就可以在块内修改其变量值了;

    块可视为对象,可有引用计数,当最后一个指向块的引用被移走之后,块就回收了。
    定义块的时候,其所占的内存区域是分配在栈中的,也就是说,块只在定义它的那个范围内有效。

    • 块是C、C++、OC中的语法闭包
    • 块可以接受参数,也可返回值
    • 块可分配在栈或堆上,也可以是全局的。分配在栈上的块可拷贝到堆里,这样就和标准的OC对象一样,具备引用计数了。

    37. 为常用的块类型创建typedef

    每个块都具备其"固有类型",因而可将其赋给适当类型的变量。这个类型由块所接受的参数及其返回值组成。

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

    38. 用handler块降低代码的分散程度

    • 在创建对象时,可使用内联的handler块将其相关业务逻辑一并声明
    • 由多个实例需要监控时,如果采用委托模式,那么经常需要根据传入的对象来切换,而若该用handler块来实现,则可直接将块与相关对象放在一起
    • 设计API时若果用到了handler块,那么可以增加一个参数,使其调用者可以通过此参数来觉得应该把块安排在哪个队列上执行

    39. 用块引用其所属对象时不要出现保留环

    40. 多用派发队列,少用同步锁

    • 派发队列可用来表述同步语义,这种做法要比使用@synchronized块或NSLock对象更简单
    • 将同步与异步派发结合起来,可以实现与普通加锁机制一样的同步行为,而这么做却不会阻塞执行派发的线程
    • 使用同步队列及栅栏块,可以令同步行为更加高效

    41. 多用GCD,少用performSelector系列方法

    • performSelector系列方法在内存管理方面容易有疏失。它无法确定将要执行的选择子具体是什么,因为ARC编译器也就无法插入适当的内存管理方法
    • performSelector系列方法所能处理的选择子太过局限了,选择子的返回值类型及发送给方法的参数个数都受限制

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

    43. 使用dispatch_once来执行只需运行一次的线程安全代码

    44. 不要使用dispatch_get_current_queue

    • dispatch_get_current_queue此函数已经废弃,只应做调试用
    • 由于派发队列是按层级来组织的,所以无法单用某个队列对象来描述"当前队列"这一概念

    系统框架

    45. 多用块枚举,少用for循环

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

    47. 构建缓存时选用NSCache而非NSDictionary

    • NSCache可以提供优雅的自动删减功能,而且是"线程安全的",此外,它与字典不同,并不会拷贝键
    • 将NSPurgeableData与NSCache搭配使用,可实现自动清除数据的功能
    • 可给NSCache对象设置上限,用以限制缓存中的对象总个数及"总成本",而这些尺度则定义了缓存删减其中对象的时机

    48. 精简initialize与load的实现代码

    • 在加载阶段,如果类实现了load方法,那么系统就会调用它。分类里也可以定义此方法,类的load方法要比分类中的先调用。与其他方法不同,load方法不参与覆写机制
    • 首次使用某类之前,系统会向其发送initialize消息。由于此方法遵从普通的覆写规则,所以通常应在里面判断当前要初始化的是哪个类
    • load与initailize方法都应该实现的精简一些,这些有助于保持应用程序的响应能力,也能减少引入"依赖环"的几率
    • 无法在编译期设定的全局变量,可以放在initialize方法里初始化

    49. 别忘了NSTimer会保留其目标对象

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

    50.细节

    • CGRect是C结构体,定义:
    struct CGRect{
      CGPoint origin;
      CGSize size;
    };
    typedef struct CGRect CGRect;
    

    整个系统框架都是使用这种结构体,因为如果改用Objective-C对象来做的话,性能会受影响。与重建结构体相比,创建对象还需要额外开销,例如分配及释放堆内存等。如果只需保存int,float,double,char等"非对象类型",那么通常使用CGRect这种结构体就可以了。

    51.开发SDK

    • 注意事项一:所有类名都应该加前缀

    • 注意事项二:所有 category 方法加前缀

    • 注意事项三:不要将第三方库打包进 SDK
      说明:尽量不要将第三方库打包进 SDK,如果要打包,最好也要将该第三方库重命名,以避免冲突。

    • 注意事项四:做基本的检查和测试
      说明:SDK 对外公布前应该进行基本的编译检查,不应该有编译器警告存在。

    • 注意事项五:文档完整并且正确

    • 注意事项六:支持最新的 CPU 版本

    相关文章

      网友评论

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

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