iOS DevelopmentiOS Developer

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

2018-01-15  本文已影响77人  勇敢的_心_

oc语言特性

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

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

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

方法

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

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

    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 预处理指令

    #define ANIMATION_DURATION 0.3
    static const NSTimeInterval kAnimationDuration = 0.3;
    extern const NSTimeInterval TestAnimationDuration;
    const NSTimeInterval TestAnimationDuration = 0.3;

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

对象、消息、运行期

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

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

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

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

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

@property(nonatomic,readwrite,copy) NSString *firstName
@property(nonatomic,readwrite,copy,getter=isOn) BOOL  on

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

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

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

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

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

给分类添加属性:
//使用前记得#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指针描述里实例所属的类。

接口与API设计

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

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

16. 实现description方法

17. 尽量使用不可变对象

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

19. 为私有方法名加前缀

20. 理解oc错误类型

21. 理解NSCopying协议

协议与分类

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

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

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

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

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

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

25. 勿在分类中声明属性

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

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

@end

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

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

内存管理

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

28. 引用计数

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

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

29. ARC

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

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

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

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

TestObject *object;
@try{// 测试代码
}@catch(){
NSLog(@" there was an error");
}@finally{
[object release];
}

32. 以弱引用避免保留环

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

@autoreleasepool{
    // ...
}

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

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

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

35. 不要使用retainCount

块与大中枢派发

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修饰符,就可以在块内修改其变量值了;

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

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

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

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

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

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

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

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

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

44. 不要使用dispatch_get_current_queue

系统框架

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

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

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

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

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

50.细节

struct CGRect{
  CGPoint origin;
  CGSize size;
};
typedef struct CGRect CGRect;

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

51.开发SDK

上一篇 下一篇

猜你喜欢

热点阅读