Objective-C
了解 Objective-C 语言的起源
Objective-C 语言使用”消息结构”而非”函数调用”.Objective-C 语言由 SmallTalk演化而来,后者是消息类型语言的鼻祖.编译器甚至不关心接收消息对象的何种类型.接收消息的对象问题也要在运行时处理,其过程叫做”动态绑定”.
Objective-C为 C 语言添加了面向对象特性,是其超类. Objective-C 使用动态绑定的消息结构,也就是说,在运行时才会检查对象类型.接收一条消息后,究竟应执行何种代码,有运行期环境而非编译器决定.理解 C 语言的核心有助于写好 Objective-C 程序.尤其是掌握内存模型与指针.
在类的头文件中尽量少引用其他头文件
Objective-C 语言编写类的标准行为:以类名做文件名,分别闯将两个文件,有文件后缀用. h,实现文件后缀用. m.
在开发中有时候我们会在. h 文件中引入很多用不到的内容,这当然会增加编译时间.除非有必要,否则不要引入头文件,一般来说,某个类的头文件中使用向前声明来体积别的类,并在实现文件中引入哪些类的头文件,这样做可以尽量降低类之间的耦合.有时无法使用向前声明,比如要声明某个类遵循意向协议,这种情况下,尽量把 “该类遵循某协议”的这条声明移至 class=continuation 分类中,如果不行的话,就把协议单独存放在一个头文件中,然后将其引入.
多用字面量语法,少用与之等价的方法
1 | NSArray *arr = [NSArray arrayWithObjects:@"num1",@"num2",@"num3", nil]; |
字面量语法创建字符串,数组,数值,字典.与创建此类对象的常规方法相比,这么做更加简明扼要,并且更加安全。
注意事项:
除了字符串以外,所创建的类必须属于 Foundation 框架才行,如果自定义了这些类的子类,则无法用字面量语法创建其对象.
创建的数组或字典时,若值有 nil, 则会抛出异常.因此,务必确保值中不含 nil。
多用类型常量,少用# deine 预处理指令
不要用预处理指令定义常量,这样定义出来的常量不含类型信息,编译器只会在编译前根据执行查找与替换操作,即使有人重新定义了常量值,编译器也不会产生井道信息,这将导致应用程序常量值不一致.
1 | static NSString *const PersonConstant = @"PersonConstantStr” ; |
但是我个人认为其实,还是#define用的多, 开发避免不了使用 pch文件, 同时#define还可以定义方法,这个是类型常量无法做到的。 如果有强迫症的同学,定义常量就想使用 staitc,extren,const 这些关键字.那我建议新建一个专门存放这些常量的类,然后在 pch 中导入这个类.
- static 修饰符意味着该变量仅在定义此变量的单元中可见
- extern 全局变量
用枚举表示状态,选项,状态码
应该用枚举来表示状态机的状态,传递给方法的选项以及状态码等值,给这些值起个易懂的名字。
如果把传递的给某个方法的选项表示为枚举类型,而多个类型又可同时使用,那么就将各选项值定义为2的幂,通过按位或操作将其结合起来。
1 | enum PersonEnum{ |
对象、消息、运行时
理解属性这一概念
属性是 Objective-C 的一项特性,用于封存对象中的数据.
属性特质:原子性 读写权限
内存管理语义:
- assign 这是方法只会执行针对纯量类型(CGFloat,NSInteger)的简单赋值操作
- strong 此特质表明该属性定义一种拥有关系,为这种属性设置新值时,这只方法会先保存新值,并释放旧值
- weak 此特质表明属性定义了一种”非拥有关系”,为这种属性设置新值是,设置方法既不保留新值,也不释放旧值.此特质同 assign 类似,然而在属性所指对象遭到摧毁时,属性值会清空
- unsafe_unretainde 此特质与 assign 相同,它适用于对象类型,该特质表达一种”非拥有关系”,当目标对象遭到摧毁时,属性不会自动清空,因为它是不安全的,这一点与 weak 的区别
- copy 此特质所表达的所属关系与 strong 类似,然而设置方法并不保留新值,而是将其拷贝,多用于 NSString.
在对象内部尽量直接访问实例变量
直接访问实例变量的速度比较快,因为不经过 Objective-C 方法派发,编译器所生成的代码会直接访问保存催下实例量的那块内存。
直接访问实例变量时,不会调用设置方法,这就绕过了相关属性所定义的内存管理语义。
读取实例变量的时候采用直接访问的的形式,设置实例变量的时候通过属性来做。
注意:
- 直接访问访问实例变量,不会触发KVO。
- 懒加载时,必须通过属性来读取数据。
理解”对象等同性”这一概念
根据等同性来比较对象是一个非常有用的功能,不过,按照 == 操作符比较出来的结果未必是我们想要的,因为该操作比较的事两个指针本身,而不是其所指的对象,应该使用 NSObject 协议中的声明的”isEqual”方法来判断两个对象的等同性,一般来说两个类型不同的对象总是不相等的.直接比较字符串的时候 isEqual 比 isEqualToString慢,因为前者还要执行额外步骤.
NSObjec中有两个判断等同性的关键方法:1
2- (BOOL) isEqual:(id)object;
- (NSUInteger)hash;
以”类族模式”隐藏实现细节
“类族”是一种很有用的模式,可以隐藏抽象基类背后实现的细节. 这是一种”工厂模式”.比如iOS 用户界面框架 UIKit 中就有一个名为 UIButton 的类.想创建按钮,需要调用下面这个类方法。1
+ (UIButton*)buttonWithType:(UIButtonType)type;
在既有类中使用关联对象存放自定义数据
有时需要在对象中存放相关信息,这是我们通常会从对象所属的类中继承一个子类,然后改用这个子类对象.然而并非所有情况下都这么做,有时候类的实例可能是由某种机制所创建的,而开发者无法令这种机制创建出自己所写的实例. Objective-C 中有一项强大的特性可以解决问题,这就是关联对象。
1 | //关联对象 |
理解objc_msgSend的作用
用Objetive-C的术语来说,这叫做“消息传递”。这里说的是运行时。
理解消息转发机制
当对象接收到无法解读的消息后,就会启动消息转发机制,程序员可经此过程告诉对象应该图和处理未知消息。这里说的是运行时。
动态方法解析
1
2
3
4
5
6
7
8
9
10
11+ (BOOL)resolveClassMethod:(SEL)sel
{
/**
动态消息转发
if (sel == @selector(foo:)) {
class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
}
return [super resolveInstanceMethod:sel];
*/
return YES; // 进入下一步转发
}备用接收者
1
2
3
4
5
6
7
8
9
10
11
12- (id)forwardingTargetForSelector:(SEL)aSelector
{
/**
备用接收者
if (aSelector == @selector(foo)) {
return [Person new]; // 返回一个Person实例作为备用接收者
}
return [super forwardingTargetForSelector:aSelector];
*/
return nil; // 进入下一步转发
}完整的消息转发
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if (aSelector == @selector(foo)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];//签名,进入forwardInvocation
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
SEL sel = anInvocation.selector;
Person *person = [Person new];
if ([person respondsToSelector:sel]) {
[anInvocation invokeWithTarget:person]; // 直接调用 person 的 foo 方法
}else{
[self doesNotRecognizeSelector:sel];
}
}
用方法调配技术调试黑盒方法
运行期间,可以向类中新增或替换选择子所对应的方法实现。
使用另一份实现来替换原有的方法实现,这道工序叫做方法调配,开发者常用此技术想原有实现中添加新功能。
一般来说,只有调试程序的时候才需要运行期修改方法实现,这种做法不易滥用。这里说的是方法交换。
理解类对象的用意
每个Objective-C对象实例都是指向某块内存数据的指针,如果把对象所需的内存分配到栈上编译器就会报错.
每个对象结构体的首个成员是Class类的变量,该变量定义了对象所属的类,通常称为isa指针。
1 | typedef struct objc_class *class |
此结构体存放类的元数据,例如类的实例实现了几个方法,具备多少个实例变量等信息。此结构体的首个变量也是isa指针,这说明Class本身亦为Objctive-C对象。结构体里还有个变量叫做super_class,它定义本类的超类,类对象所属的类型(isa指针所指向的类型)是另外一个类,叫做元类,用来标书类本身所具备的元数据。类方法就定义于此处,因为这些方法可以理解成类对象的实例方法,每个类仅有一个类对象,每个类对象仅有一个与之相关的元类。(元数据,就是这个类的数据)。
- isKindOfClass:能够判断对象是否为某类或其派生类的实例
- isMemberOfClass: 能够判断出对象是否为某个特定类的实例
接口设计
用前缀避免命名空间冲突
Objective-C没有其他语言那种内置的命名空间,所以需要避免命名冲突,否则会直接报错。
提供全能初始化方法
即可以为对象提供必要信息以便其完成能完成工作的初始化方法。
注意:如果子类的全能初始化方法与父类的不一致,就应该覆写父类的全能初始化方法。有时我们不想覆写,这时我们可以覆写父类的全能初始化方法并在里面抛出异常。
实现description方法
调试程序时经常需要打印并查看对象信息。description 很实用。
debugDescription 方法是开发者在调试器中以命令打印对象时候才调用。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19Person.h
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property(nonatomic,assign)int age;
@property(nonatomic ,copy)NSString* name;
@end
Person.m
#import "Person.h"
@implementation Person
- (NSString *)description
{
return [NSString stringWithFormat:@"name %@ , age %d", self.name, self.age];
}
- (NSString *)debugDescription
{
return [NSString stringWithFormat:@"name %@ , age %d", self.name, self.age];
}
@end
尽量使用不可变对象
通过readonly将属性修饰为不可变,如果想修改封装在对象内部的数据,可以在对象的内部将readonly属性重新声明为readwrite。
注意:readonly修饰的属性,仍然可以使用KVC来修改。
使用清晰而协调的全名方式
没啥好说的
为私有方法名加前缀
不要单用一个下划线给私有方法做前缀,这个是苹果爸爸用的。
理解Objective-C错误模型
NSError的用法更加灵活,因此经由此对象,我们可以把导致错误的原因汇报给调用者。
- NSError domain(错误范围,其类型为字符串)
错误发生的范围,也就是产生错误的根源,通常用一个特有的全局变量来定义,比方说 “处理URL子系统”从URL的解析获取数据时如果出错了,那么就会使用NSURLErrorDomain来表示错误范围 - Error code(错误码,其类型为整数)
独有的错误代码,用以指明在某个范围内具体发生了何种错误。某个特性范围内可能会发生一系列相关错误,这些错误情况通常采用enum来定义。例如,当HTTP请求出错时,可能会把HTTP状态码设为错误码 - User info(用户信息,其类型为字典)
有关错误的额外信息,其中或许包含一段“本地化的描述”或许还含有导致错误发生的另外一个错误,经由此种信息,可将相关错误串成一条“错误链”
1 | @try { |
理解NSCopying协议
- copy方法实际上是调用 -(id)copyWithZone:(NSZone*)zone; 实现copy操作, 如果想对自己的类支持拷贝并且做额外操作,那就要实现NSCopying协议此的方法。
为何出现NSZone呢,以前开发程序时,会据此把内存分成不用的区,而对象会创建在某个区。 现在不用了,每个程序只有一个区:“默认区”,所以不用担心zone参数。
copy方法由NSObject实现,该方法只是以默认区为参数调用。 - mutableCopy方法实际上是调用 -(id)mutableCopyWithZone:(NSZone*)zone; 实现mutableCopy操作
协议与分类
通过委托与数据协议进行对象间通信
这一条说的就是delegate(代理设计模式)。但是并没有说delegate的循环引用的问题,在使用代理声明一个 @property的时候,记得用weak。
将类的实现代码分散到便于管理的数个分类之中
- 使用分类机制把类的实现代码划分成易于管理的小块
- 将应该视为“私有”的方法归入名为Private的分类中,以隐藏细节。
勿在分类中声明属性
正常的分类是不可以声明属性的,但是从技术上说,分类里可以用runtime声明属性。
1 | #import <objc.runtime.h> |
这样做可行,但是不太理想,要把相似的代码写很多遍。而且容易出现Bug,可以使用class-continuation实现分类添加属性。
使用class-continuation分类隐藏实现细节
class-continuation分类和普通的分类不同,它必须在其所接续的那个类的实现文件里。其重要之处在于,这是唯一能声明实例变量的分类,而且此分类没有特定的实现文件,其中的方法应该定义在类的主实现文件里。与其他分类不同,“class-continuation分类”没有名字,比如,有个类叫做EOCPerson,其“class-continuation分类”写法如下:
1 | @interface EOCPerson() |
没错它就是 class-continuation分类,在此代码之间可以添加属性,修改属性。
1 | @interface ViewController () |
使用class-continuation分类的好处
- 可以向类中新增实例变量。
- 如果类中的主接口声明为只读,可以再类内部修改此属性。
- 把私有方法的原型文件生命在”class-continuation分类”里面。
- 想使类遵循的协议不为人知,可以用“class-continuation分类”中声明。
通过协议提供匿名对象
就说下面这句话1
@property (nonatomic,weak)id<PersonDelegate> pd;
内存管理
理解引用计数
理解引用计数,方便于了解iOS的内存管理。不过现在都是ARC的时代了。
引用计数机制通过可以递增递减的计数器来管理内存。对象创建好之后,其保留计数至少为1。若保留计数为正,则对象继续存活。当保存计数降为0,对象就被销毁了。
在对象生命期中,其余对象通过引用来保留或释放此对象。保留与释放操作分别会递增及递减保留计数。
以ARC简化引用计数
使用ARC要计数,引用计数实际上还是要执行的,只不过保留与释放操作现在由ARC自动为你添加。由于ARC会自动执行retain、release、autorelease等操作,所以直接在ARC下调用这些内存管理方法是非法的。
ARC管理对象生命周期的的办法基本就是:在合适的位置插入“保留”和“释放”操作。
ARC在调用这些方法时,并不用过普通的Objective-C消息派发机制,而是直接调用其底层C语言版本,这样做性能更好,直接调用底层函数节省很多CPU周期。
虽然有了ARC之后无需担心内存管理问题,但是CoreFoundation对象不归ARC管理,开发者必须适时调用CFRetain/CFRelease。
在dealloc方法中只释放引用并解除监听
当一个对象销毁的时候会调用dealloc方法,但是当开销较大或系统内稀缺资源则不再此列,像是文件描述、套接字、大块内存等都属于这种资源,通常对于开销较大的资源实现一个方法,当程序用完资源对象后,就调用此方法。这样一来,资源对象的生命期就变得明确了。
执行异步任务的方法不应该在dealloc里面调用;只能在正常状态下调用的那些方法也不应该调用,因为此时对象已经处于正在回收的状态了。
编写“异常安全代码”时留意内存管理问题
- 在使用@try 的时也要注意,在捕获到异常的时候@try{}中的语句执行到异常代码的那一行后不在执行,然后把异常抛给@catch。当然@finally是一定要执行的。
- 在默认情况下,ARC不生成安全处理异常所需的清理代码。开启编译器标志后,可以生成这种代码,不过会导致应用程序变大,而且会降低运行效率。
以弱引用避免保留环
- unsafe_unretained 语义同assign等价。然而assign通常用于int、float、结构体等。unsafe_unretained多用于对象类型。
- weak 与 unsafe_unretained 作用相同,然而只要系统把属性回收,属性值为nil。
推荐使用weak,毕竟是ARC时代的产物,而且用的人也很多。
以“自动释放池块”降低内存峰值
自动释放池排布在栈中,对象收到autorelease消息后,系统将其放入最顶端的池里。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16@autoreleasepool {
<#statements#>
}
for (int i = 0; i < 1000000; i++) {
@autoreleasepool {
NSNumber *num = [NSNumber numberWithInt:i];
NSString *str = [NSString stringWithFormat:@"%d ", i];
[NSString stringWithFormat:@"%@%@", num, str];
if(lagerNum-1 == i)
{
NSLog(@"end");
}
}
}
用“僵尸对象”调试内存管理问题
在左上角标题栏找到项目单击后选择 Edit scheme 勾选图中检测僵尸对象。
- 开启后,系统在回收对象的时候,可以不真的将其回收,而是转成僵尸对象。
- 系统会修改对象的isa指针,令其指向特殊的僵尸类,从而使该对象变成僵尸对象。僵尸类能响应所有的方法,响应方式为:打印一条包含消息内容及其接受者的消息,然后终止应用程序。
不要使用retainCount
- 任何给定时间点上的“绝对保留计数”都无法反应对象生命周期的全貌。
- ARC的时代,调用该方法会直接报错。
块与大中枢派发
理解”块“这一概念
这里其实就是在说block,复习一下block的语法:
返回值类型(block名称)(参数)
需要注意的是定义block时候,其所占内存区域是分配在栈中的,块只在定义它的那个范围内有效。
block所使用的整个内存区域,在编译期已经完全确定,因此,全局block可以生命在全局内存里,而不需要在每次用到的时候于栈中创建,另外,全局block的拷贝是个空操作,因为全局block绝不可能为系统所回收,这种block实际上相当于单例。
可以调用 copy 方法将块从栈拷贝到堆,拷贝之后的块就可以在定义它的范围之外使用了。而且,拷贝到堆以后,块就变成带引用计数的对象了,后续的copy操作不会真的执行,只是递增引用计数。
为常用的块类型创建typedef
就是给block起个别名1
2typedef <#returnType#>(^<#name#>)(<#arguments#>);
@property (nonatomic,copy)name nm_blk;
用handler块降低代码分散程度
说的就是block的回调。只不过是把block放在方法中去使用,使代码更加紧致。
1 | // Person.h |
用块引用其所属对象时不要出现保留环
注意用weak,不要出现循环引用。
多用派发队列,少用同步锁
派发队列可用来表述同步语义,这种做法比使用@synchronize块或NSLock对象更简单
将同步与异步派发结合起来,可以实现与普通枷锁机制一样的同步行为,而这么做却不会阻塞执行异步派发的线程。
使用同步队列及栅栏块,可以令同步行为更加高效(不常用)。
多用GCD,少用performSelector系列方法
这个没啥可说的
掌握GCD及操作队列的适用时机
这个没啥可说的
解决多线程与任务管理问题时,派发队列并非唯一方案
操作队列提供了一套高层的Objective-C API
能实现纯GCD所具备的绝大部分功能,而且还完成一些更为复杂的操作,那些操作弱改用GCD来实现,则需另外编写代码。
使用NSOperation对线程管理
通过Dispatch Group机制,根据系统资源状况来执行任务
这个没啥可说的
一系列任务可归入一个dispatch group之中。开发者可以在这组任务执行完毕时获得通知
通过dispatch group,可以在并发式派发队列里同时执行多项任务。此时GCD会根据系统西苑状况来调度这些并发执行的任务。开发者若自己来实现此功能。则需要便携大量代码。
使用dispatch_once来执行秩序运行一次的线程安全代码
1 | static dispatch_once_t onceToken; |
不用使用dispatch_get_current_queue
这个没啥可说的。
iOS系统6.0版本起,已经正式弃用此函数了。
系统框架
熟悉系统框架
打开Xcode command + shift + 0 选择性的了解一些 Foundation、UIKit
也可以看看这篇博客 http://www.jianshu.com/p/58bc11c800e4
多用块枚举,少用for循环
因为枚举遍历的时候用的多线程(GCD并发执行),所以效率更快些。我觉得其实用什么都行。
1 | NSArray *arr = @[@"b",@"c",@"s"]; |
对自定义其内存管理语义的collection使用无缝桥接
1 | NSArray *anNSArray = @[@1,@3,@5,@8]; |
通过无缝桥接技术,可以在Foundation框架中Objective-C对象与CoreFoundation框架中的C语言数据结构之间来回转换。
在CoreFoundation层面穿件collection时,可以指定许多回调函数,这些函数表示此collection应如何处理其元素,然后可运用无缝桥接技术,将其转换成具备特殊内存管理语义的Objective-C collection。
- __bridge:ARC仍然具备这个OC对象的所有权
- bridgeretained:ARC将交出对象的所有权
构建缓存时选用NSCache而非NSDictionary
- NSCache胜过NSDictionary之处在于,当系统资源耗尽时,它能自动删减缓存。
- NSCache线程安全
精简Initialize与load的实现代码
类初始化的时候一定会调用两个方法
1 | +(void)load{} |
- load方法只会调用一次,不管该类的头文件有没有被使用,该类都会被系统自动调用,而且只调用一次。 当然了,如果不重写这个方法的话,我们是不知道这个方法有没有被调用的。如果分类也重写了load方法,先调用类里的,在调用分类。
- load方法执行时,运行期系统处于“脆弱状态”,在执行子类的load方法之前,必定会先执行所以超类的load方法,而如果代码还依赖了其他的库,那么其他库的相关类的load方法一定会先执行,但是执行的顺序不好判断,所以在load方法中使用其他类是不安全的。
- 整个程序在执行load方法时都会阻塞。
- initialize 和load类似,不过在类被初始化的时候才会被调用(init之前)。需要注意的是,<#ClassName#>如果有子类继承的时候要判断类名。
- 两个方法的实现都应该精简些,这有助于保持应用程序的响应能力。
别忘了NSTimer会保留其目标对象
1 | // Person.h |
好,那么问题来了,是不是没有调用dealloc方法,没有调用dealloc方法就说明Person对象并没有被销毁,为什么没有被销毁
因为在控制器强引用了self.person,[self.person start]强引用了 self.timer; self.timer 的target指向了self(self.person)所以循环引用了。
怎么解决。 NSTimer销毁的时候,把Person对象为nil即可。
1 | -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ |