NSNumber *someNumber = @1;
NSArray *animals = @[@"cat",@"dog",@"mouse"];
NSDictionary *personData = @{ @"firstName":@"Luk",
@"lastName":@"Kingyu",
@"age":@25 };
NSString *dog = animals[1];
NSString *lastName = personData[@"lastName"];
static const NSTimeInterval kAnimationDuration = 0.3;
// header file
extern NSString *const EOCStringConstant;
// .m file
NSString *const EOCStringConstant = @"VALUE";
如果使用了属性,编译器会自动编写访问这些属性的存取方法,此过程称为(自动合成),在编译期执行,同时会在类中添加适当类型的实例变量,以属性名前面添加 ‘_’ 作为实例变量的名字。可以用**@dynamic**关键字阻止编译器合成存取方法。
属性拥有的特质分为四类
* 原子性:**nonatomic**,属性默认为 atomic 特质,可以保证属性的**可见性。**
开发iOS程序应该使用 nonatomic 特质,atomic 特质会严重影响性能。
* 读写权限
readwrite特质的属性拥有 getter 和 setter
readonly特质的属性仅拥有 getter
* 内存管理语义
assign只会执行简单的赋值操作,针对“纯量类型”。
strong为这种属性设置新值时,setter 会先保留新值,并释放旧值,再把新值设置上去。
weak设置属性时,既不保留新值,也不释放旧值,当所指的对象被释放后,会指向 nil
copy所属关系与 strong 类似,但设置方法不保留新值,而是将其拷贝。只要实现属性所用对象是可变的,就应该在设置新属性时拷贝一份。
unsafe_unretained语义与 assign 相同,但适用于对象类型,当目标对象释放后,属性值不会清空,造成空悬指针,引起程序崩溃。
* 指定存取方法的方法名
getter=
setter=
但某些情况下必须在初始化方法中调用设置方法:待初始化的实例声明在超类中,而我们又无法在子类中直接访问此实例变量,就需要调用 setter。
自己创建等同性方法因为无须检测参数类型,所以能大大提高检测速度。
hash方法不应该是根据可变部分计算出来的。否则放入 collection 后改变其内容会造成哈希值改变,那么对象所处的位置就是错误的。
即工厂模式。
objc_msgSend(id self, SEL cmd, ...)
会在接收者所属的类中搜寻其方法列表,如果能找到与 选择子 名称相符的方法,就跳转至其实现代码,若找不到则沿着继承体系向上查找。如果最终还是找不到,则进行消息转发。
objc_msgSend 会将匹配结果缓存在“fast map”里,每个类都有这样一块缓存。
利用了“尾调用优化(tail-call optimization)”技术,当某函数的最后一项操作是调用另一个函数且不会将其返回值作他用时,不会向调用堆栈中推入新的栈祯,而是生成跳转至另一函数所需的指令码。
ObjC 可以在运行期继续向类中添加方法,所以编译期时编译期无法确知类中会不会又某方法实现。当对象接收到无法解读的消息后,就会启动消息转发机制,可以经由此过程告诉对象应该如何处理未知消息。
消息转发分为两大阶段
一:先征询接收者所属的类,看其是否能动态添加方法,以处理当前这个“未知的选择子”。这叫做动态方法解析;
二:如果运行期系统已经把第一阶段执行完,那么接收者自己就无法再以动态新增方法的手段来响应包含该选择子的消息了。此时 RunTime 会请求接收者以其他手段处理与消息相关的方法调用。分为两步,首先请接收者看看有没有其他对象能处理这条消息,若有,RumTime 将消息转发给那个对象;若没有则启动完整的消息转发机制,RunTime 会把与消息有关的全部细节封装到 NSInvocation 对象中,再给接收者最后一次机会,令其设法解决当前还未处理的这条消息。
对象收到无法解读的消息后,首先将调用其所属类的类方法
+ (BOOL)resolveInstanceMethod:(SEL)selector
表示这个类是否能新增一个实例方法用于处理此选择子。可以在这里新增一个处理该选择子的方法。
使用此方法的前提是:相关方法的实现代码已写好,只等着运行的时候动态插在类里面就可以了,此方案常用来实现 @dynamic 属性。
当前接收者还有第二次机会处理未知的选择子,在这一步中,RunTime 会问它:能不能把这条消息转给其他接收者来处理。
- (id)forwardingTargetForSelector:(SEL)selector
若当前接收者能找到备援对象,则将其返回,若找不到,就返回nil。
通过此方案可以用 “组合”模拟出“多重继承”的某些特性。在一个对象内部,可能还有一系列其他对象,该对象可经由此方法将能够处理某选择子的相关内部对象返回,这样在外界看来,就像是该对象亲自处理了这些消息。
如果消息转发算法来到这一步,就会启动完整的消息转发机制。首先创建 NSInvocation 对象,把与尚未处理的那条消息有关的全部细节封于其中,包含选择子、目标以及参数。消息派发系统把消息派给目标对象。
- (void)forwardInvocation:(NSInvocation *)invocation
该方案可以改变调用目标,使消息在目标上得以调用即可。但这种做法与第二种类似,很少有人这么做。这一步的区别在于可以在触发消息前,以某种方式改变消息的内容,比如追加另一个参数,或是改换选择子等等。
若发现某调用操作不应由本类处理,则需调用同名的超类方法。继承体系的每个类都有机会处理此调用请求。
通过此技术,可以为那些不知道其具体实现的黑盒方法增加日志记录功能,有助于程序调试。
类型信息查询特性内置于 NSObject 协议里。在程序中不要直接比较对象所属的类,明智的做法是调用“类型信息查询方法”。
声明对象时若指定了具体类型,在该类实例上调用其所没有的方法时,编译器会探知此信息并发出警告。
ObjC 对象所用的数据结构定义在 RunTime 库的头文件里。
对象结构体中的首个成员是 Class 类的变量 isa 指针,定义了对象所属的类。
Class 对象也定义在 RunTime 库的头文件中
此结构体存放类的“元数据”**,**例如类的实例实现了几个方法,具备多少个实例变量等信息。此结构体首个变量也是 isa 指针,即 Class 本身也是 ObjC 对象。
结构体里还有变量 super_class,定义了本类的超类。
类对象所属的类型(isa 指针所指向的类型)是另外一个类,叫做**“元类”(metaclass)**,用来表述类对象本身具备的元数据。“类方法”就定义于此处,因为这些方法可以理解为类对象的实例方法。每个类仅有一个“类对象”,每个“类对象”仅有一个与之相关的“元类”。
super_class 指针确立了继承关系,而 isa 指针描述了实例所属的类。通过这张关系图即可执行“类型信息查询”,可以查出对象是否能响应某个选择子,是否遵从某项协议,并能看出对象位于类继承体系的哪一部分。
**isKindOfClass:**判断对象是否为某类或其派生类的实例。
**isMemberOfClass:**判断对象是否为某个特定类的实例。
由于 ObjC 使用动态类型系统,从 collection 中获取对象时,其类型通常是 id,所以需要查询类型信息。
也可使用 == 操作符比较对象的类对象是否等同,但不能使用比较对象时常用的 isEqual: 方法。因为类对象是单例,每个类的 Class 仅有一个实例,所以判断指针地址就可以精确判断出来。
但应该尽量使用类型信息查询方法,因为可以正确处理那些使用了消息传递机制的对象。
比如,某个对象可能会把其收到的所有选择子都转发给另一个对象,这样的对象叫做“代理”,此种对象均以 NSProxy 为根类。
若在此种代理对象上调用 class 方法,返回的是代理对象本身,而非接受代理的对象所属的类。若是改用 isKindOfClass,那么就会把消息转给“接受代理的对象”。也就是说,这条消息的返回值与直接在接受代理的对象上面查询其类型所得的结果相同。因此,这样查出来的类对象与通过 class 方法返回的类对象不同, class 方法返回的类表示发起代理的对象,而非接受代理的对象。
也可以把错误信息放在 NSError 对象里,经由“输出参数”返回给调用者:
- (BOOL)doSomething: (NSError **)error{
// Dosomething that may cause an error
if(/* there was an error*/){
if(error){
// Pass the error through the out-parameter
*error = [NSerror errorWithDomain:domain
code:code
userInfo:userInfo]
}
return NO;
} else{
return YES;
}
}
传递给方法的参数是个指针,该指针本身又指向另外一个指针,那个指针指向 NSError 对象。这样就能经“输出参数”把 NSError 对象回传给调用者。
用法:
NSError *error = nil;
BOOL ret = [object doSomething:&error];
if(error){
}
实际上,在使用 ARC 时,编译器会把 NSError ** 转换成 NSError * __autoreleasing*,也就是说,指针所指的对象会在方法执行完毕自动释放。即将方法中创建的 NSError 加入 autorelease。
如果想令自己的类支持 copy 操作,就要实现 NSCopying 协议,该协议只有一个方法:
- (id)copyWithZone:(NSZone *)zone;
zone 是历史遗留问题,现在每个程序只有一个“default zone”,可以不用管这个参数。
copy 方法由 NSObject 实现,该方法只是以 default zone 为参数调用 copyWithZone,所以真正该覆写的是 copyWithZone 方法。
如果对象中存在可变的成员,那么拷贝时也应该拷贝这个成员;若是不可变的,就不用担心这个问题,拷贝反而会造成浪费。
通常情况下,可以采用全能初始化方法来初始化待拷贝的对象。有的时候如果全能初始化方法中设置了一个复杂的数据结构,而拷贝后的对象立刻需要用其他数据来覆写,就会造成浪费。
Foundation 框架下的 collection 默认情况下都执行浅拷贝。
将分类方法加入类中这一操作是在运行期系统加载分类时完成的。运行期系统会把分类中所实现的每个方法都加入类的方法列表中。如果类中本来就有此方法,分类中的方法就会覆盖原来的实现代码。若个多个分类中有同名方法,则会发生多次覆盖,覆盖结果以最后一个分类为准。
如果像某个类的分类中加入方法,那么在应用程序中,该类的每个实例均可调用这些方法。
技术上分类可以声明属性,但除了“class-continuation 分类”之外,其他分类无法向类中新增实例变量,因此无法把实现属性所需的实例变量合成出来。
可以用关联对象解决分类中不能合成实例变量的问题。但并不推荐。
因为有“稳固的 ABI”机制,使得我们无需知道对象大小即可使用它。
这样做相当于把实例变量隐藏起来。
与其他语言中“匿名类”的概念不同。
id ,可以不用指明具体使用哪个类,只需要这个对象遵循协议就行。
调用 release 会立刻递减对象的引用计数,调用 autorelease 会稍后递减计数,通常是在下一次“事件循环”时递减,不过也可能会更早一些。
autorelease 可以保证对象在跨越 方法调用边界 后一定存活。
两个对象都持有彼此的强引用,系统将不能销毁这两个对象。
父对象持有子对象的强引用,子对象持有父对象的弱引用,就可以打破循环引用问题。
弱引用,当引用的对象被释放时,弱引用会被自动设置为 nil。
弱变量能够和代理很好地协作。创建一个代理的弱引用,如果代理对象被销毁,变量就会被清零。
以 alloc、new、copy、mutableCopy 词语开头的方法,其返回的对象归调用者所有。即调用上述四种方法的那段代码要负责释放方法返回的对象。
若方法名不以上述四个词语开头,则表示其所返回的对象并不归调用者所有。这种情况下返回的对象会自动释放。
ARC 可以在编译期把能够互相抵消的 reatin、release、autorelease 操作约简。可以减少操作 autoreleasePool 的次数,提高性能。
在应用程序中,可以用下列修饰符来改变局部变量与实例变量的语义:
runtime 对注册的类,会进行布局,对于 weak 对象会放入一个 hash 表中。用 weak 指向的对象内存地址作为 key,当此对象的引用计数为0的时候会 dealloc,假如 weak 指向的对象内存地址是 a,那么就会以 a 为键, 在这个 weak 表中搜索,找到所有以 a 为键的 weak 对象,从而设置为 nil。
属性默认是 unsafe_unretained (相当于 assign)
编译器会保证在 RunLoop 中通过对赋值执行 retain 操作使 strong 属性能够存活下来,assign 和 weak 的属性不会执行这些操作。
一般来说,如果不拥有某对象,那就不要保留它。这条规则对 collection 例外,collection 虽然并不直接拥有其内容,但是它要代表自己所属的那个对象来保留这些元素。有时,对象中的引用会指向另外一个并不归自己所拥有的对象,比如 Delegate 模式。
自动释放池用于存放那些需要在稍后某时刻释放的对象,清空自动释放池时,系统会向其中的对象发送 release 消息。
主线程或是 GCD 机制中的线程默认都有自动释放池,每次执行“事件循环”时,就会将其清空。因此不需要自己来创建“自动释放池块”。
iOS应用运行在 Runloop 中,为了处理新的事件,系统会创建一个新的自动释放池块,调用到应用中的一些方法用于处理事件,再从方法返回,系统会继续等待下一个事件的发生。
在 main 函数里会用自动释放池来包裹应用程序的主入口点,这个池可以理解成最外围捕捉全部自动释放对象所用的池。
指令围住的语句块定义了自动释放池的上下文,位于自动释放池范围内的对象,会在此范围的末尾收到 release 消息。
自动释放池可以嵌套,可以此降低内存峰值。
ARC 已经将此方法废弃了。即使不开启 ARC 也不要用,因为对象可能处在自动释放池中,其保留计数未必精确,且其他程序库也可能自行保留或释放对象,都会扰乱保留计数的具体数值。
在任何给定时间点上的绝对保留计数都无法反映对象生命期的全貌。
Block 与 GCD 是多线程编程的核心。
详见另一篇博客:Block 的内存管理
typedef int (^SomeBlock)(BOOL flag, int value);
SomeBlock block = ^(BOOL flag,int value){};
synchronized(self) 会根据给定的对象自动创建一个锁,这种写法会降低代码效率,粒度太大。
NSLock 与 NSRecuriveLock 效率也不高,遇到死锁会非常麻烦。
GCD能以更简单、高效的形式为代码加锁。
详见另一篇博客:GCD 的使用
- 同步、异步决定是否创建子线程,同步任务不创建子线程,都是在主线程中执行,异步任务创建子线程。
- 串行、并行决定创建子线程的个数,串行创建一个子线程,并行创建多个子线程(具体几个由系统决定)
串行队列 异步任务,会创建子线程,且只创建一个子线程,异步任务执行是有序的。
串行队列 同步任务,不创建新线程,同步任务执行是有序的。
并行队列 异步任务 创建子线程,且多个子线程,异步任务打印结果无序。
并行队列 同步任务,不创建新线程,同步任务执行是有序的。
用串行队列实现 getter
, setter
:
_syncQueue = dispatch_queue_create("com.kingyu.syncQueue", NULL);
- (NSString *)someString {
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
- (void) setSomeString:(NSString *)someString {
dispatch_async(_syncQueue, ^{
_someString = someString;
});
}
执行异步派发的时候,需要拷贝块,若拷贝块的时间明显超过执行块的时间,就会比同步派发慢。
也可用并发队列实现,需要使用 barrier block。性能会比串行队列快。
_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- (NSString *)someString {
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
- (void) setSomeString:(NSString *)someString {
dispatch_barrier_async(_syncQueue, ^{
_someString = someString;
});
}
因为 ARC 无法知道调用的选择子是否具有返回值,或是不知道返回的对象应该由谁来释放,所以 ARC 不会添加 release 操作,这么做就可能会导致内存泄漏,因为方法在返回对象时可能将其保留了。
// Using performSelector
[self performSelector:@selector(doSomething)
withObject:nil
afterDelay:5.0];
// Using dispatch_after
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 5.0 * NSEC_PER_SEC);
dispatch_after(time, dispatch_get_main_queue(), ^(void){
[self doSomething];
})
* 在另一线程中执行:dispatch_sync 及 dispatch_async
// Using performSelector
[self performSelectorOnMainThread:@selector(doSomething)
withObject:nil
waitUntilDone:NO];
// Using dispatch_async
dispatch_async(dispatch_get_main_queue(), ^(void){
[self doSomething];
})
GCD 是纯C的API,而 NSOpearationQueue 是 ObjC 的对象。在 GCD 中任务用 Block 来表示,而块是个轻量级的数据结构。但GCD并不总是最佳方案,有时候采用对象所带来的开销微乎其微,并能带来许多好处。
底层是用 GCD 实现的。使用 NSOperation 及 NSOperationQueue的好处:
NSOperation 对象也有“线程优先级”,只需设置一个属性即可,用GCD也可实现此功能。
dispatch group 能够把任务分组,调用者可以等待这组任务执行完毕,也可以在提供回调函数后继续往下执行,这组任务完成时,调用者会得到通知。
dispatch_apply 可以是实现类似效果,但是dispatch_apply 会持续阻塞,直到所有任务都执行完毕,所以假如把块派给了当前队列,就将导致死锁。若想在后台执行任务,则应使用 dispatch group。
实现单例模式
+ (id) sharedInstance {
static XXObject *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
dispatch_once_t 是 int 类型的指针,对于只需执行一次的块,每次调用函数传入的标记的必须完全相同,因此通常将标记变量声明在 static 或 globla 作用域里。
iOS 系统从 6.0 版本起已经弃用此函数了。
CoreFoundation 虽不是 ObjC 框架,但可以通过“无缝桥接”(tollfree bridging)把 CoreFoundation 中的 C 语言数据结构转换为 Foundation 中的 ObjC 对象,也可以反向转换。
NSArray *anArray = /*...*/;
NSEnumerator *enumerator = [anArray objectEnumerator];
// Dictionary
NSEnumerator *enumerator = [aDictionary keyEnumerator];
// 反向遍历
NSEnumerator *enumerator = [anArray reverseObjectEnumerator];
id object;
while((object = [enumerator nextObject]) != nil){
// Do something
}
* 快速遍历法需要类遵从 NSFastEnumeration 协议。
块枚举法优势:
遍历时可以直接从块里获取更多信息。
使用者可以向其传入“选项掩码”:NSEnumerationOptions,类型是 enum,其各种取值可用“按位或”连接,用以表明遍历方式。如以并发、反向等方式进行遍历。
Foundation 框架中的 NSDictionary 在向其添加对象时,字典会自动**“拷贝” key并“保留” value**,如果用作 key 的对象不支持拷贝操作,就可以用这方法修改字典的内存管理语义。
只有在开销值可以很快得到的情况下,才应该采取这个尺度,比如加入缓存中的是 NSData 对象,就可以采用这个尺度,因为数据大小可以从 NSData 属性中得知。
NSPurgeableData 是 NSMutableData 的子类,实现了 NSDiscardableContent 协议。如果某个对象所占的内存能够根据需要随时丢弃,那么就可以实现该协议所定义的接口。
如果需要访问某个 NSPurgeableData 对象,可以调用其 beginContentAccess 方法,告诉它现在还不应丢弃所占据的内存,用完后调用 endContentAccess 就可以告诉它在必要时可以丢弃自己所占的内存。这些调用可以嵌套,与引用计数方法类似。
类必须先执行某些初始化操作才能正常使用。
对于加入运行期系统中的每个 类 及 分类来说,必定会调用此方法,而且仅调用一次。当包含类或分类的程序库载入系统时,就会执行此方法,即应用程序启动时。如果分类和其所属的类都定义了 load 方法,则先调用类里的,再调用分类里的。
在 load 方法中使用其他类是不安全的,因为无法判断库中各个类的载入顺序,无法确定其他类是不是已经加载好了。
load 方法不会继承,若分类和其所属的类中都实现了 load 方法,那么这些代码都会调用,类的实现要比分类的实现先执行。
load 方法务必实现得精简,因为整个应用程序在执行 load 方法时都会阻塞。不要在里面等待锁会调用可能会加锁的方法。
其主要用途仅在于调试程序,比如判断分类是否正确载入系统中。尽量不要使用这个方法。
由于 load 方法会在类被 import 时调用一次,而这时往往是改变类的行为的最佳时机,可以在分类 load 方法中使用 method swizlling 来修改原有的方法,且load调用的时机总是已知的,是统一的。放在initialize里面的话,子类调用参与替换的方法时,实际上是不会替换的。
对于每个类来说,该方法会在程序首次用该类之前调用,且只调用一次。它是由运行期系统来调用的,绝不应该通过代码直接调用。
与 load 区别:
* 惰性调用,只有程序用到了相关的类时,才会调用。对于load来说,应用程序必须阻塞并等着所有类的 load 都执行完才能继续。
* 运行期系统在执行该方法时,是处于正常状态的,此时可以安全使用并调用任意类中的任意方法,运行期系统也能确保 initialize 方法会在线程安全的环境中执行,即只有执行 initialize 的那个线程可以操作类或类实例,其他线程都要先阻塞,等 initialize 执行完。
* initialize 会被继承,如果某个类未实现它,而超类实现了,就会运行超类的实现代码。所以通常应该在里面判断当前要初始化的是哪个类。
initialize 只应该用来设置内部数据如静态变量的设置,不应该在其中调用其他方法,即便是本类自己的方法。
若某个全局状态无法在编译期初始化,则可以放到 initialize 里做,比如 NSMutableArray 实例,是 ObjC 对象,创建实例前必须先激活运行期系统。
NSTimer 计时器,开发者可以指定绝对的日期和时间,以便到时执行任务,也可以指定执行任务的相对延迟时间。NSTimer 还可以重复运行任务,使用间隔值来指定任务的触发频率。
计时器要和 Runloop 相关联,运行循环到时候会触发任务。可以将其预先安排在当前的运行循环中:
[NSTimer scheduledTimerWithTimeInterval:<#(NSTimeInterval)#>
target:<#(nonnull id)#>
selector:<#(nonnull SEL)#>
userInfo:<#(nullable id)#>
repeats:<#(BOOL)#>]
target 与 selector 参数表示计时器将在哪个对象上调用哪个方法。