第一章
1、起源
OC由Smalltalk演化而来,后者是消息型语言的鼻祖。
消息与函数调用的关键区别在于:消息结构的语言,运行时所应执行的代码由运行环境来决定;而使用函数调用的语言,则由编译器决定。
OC的重要工作由“运行期组件”而非编译器来完成,使用OC的面形对象特性所需的全部数据结构及函数都在运行期组建里面。运行期组件本质上就是一种与开发者所编代码相连接的动态库,其代码能把开发者编写的所有程序粘合起来。(ps:我的个人理解是,OC代码在编写之后依然是一个骨架,真正成为一个能跑能跳的人,还是在运行期间,通过runtime将这个人需要的“血肉”粘合起来,这个“血肉”已经客观存在。)
OC是c的超集,OC语言中的指针是用来指示对象的。
NSString *someString = @"The string";
someString为指向NSString的指针,指向分配在堆里的某块指针,其中含有一个NSString对象。
NSString *someString = @"The string";
NSString *anotherString = someString;
这里是在栈上分配两块内存,每块内存的大小都能容下一枚指针。两块内存里的值都一样,都是指向NSString实例的内存地址。
分配在堆中的内存必须直接管理,而分配在栈上用于保存变量的内存则会在其栈帧弹出时自动清理。
OC将堆内存管理抽象出来,OC运行期环境把这部分工作抽象为一套内存管理架构,名叫“引用计数”。
第二章
对象是“基本构造单元”,开发者通过对象来存储并传递数据。对象之间传递数据并执行任务的过程就叫做“消息传递”。
7、属性
“属性”是OC的一项特性,用于封装对象中的数据。OC对象通常会把所需要的数据保存为各种实例变量,实例变量一般通过“存取方法”来访问。
我们要讨论的是访问_firstName变量的代码,编译器就把其替换为offset,offset是硬编码,表示该变量具体存放对象的内存区域的起始地址有多远
,当再开头添加一个实例变量就会出现问题。
不讨论其他语言的做法,OC的做法是,把实例变量当作一种存储偏移量所用的“特殊变量”,交由“类对象”保管。偏移量会在运行期查找,如果累的定义变了,存储的偏移量也就变了,保证访问地址正确。(PS:代码测试确实是这样的,偏移量会改变,但不是上图那种改变)。
由此引出了属性的存取方法,也就是setter和getter,OC提供了点语法来进行存取,这里就不多做叙述了。说一点就是,使用property的自动合成是在编译期执行。synthesize可以指定实例变量的名字,dynamic代表不自动生成实例变量和存取方法。
关于关键字的描述之前的文章中有提到,这里就不再重复了。
如果想在其他方法里设置属性指,那么同样要遵守属性定义中所宣称的语意。也就是对外暴露一个方法时,内部属性的设置也要对应到关键字。
.h
- (id)initWithFirstName:(NSString)firstName lastName:(NSString *)lastName;
.m
- (id)initWithFirstName:(NSString)firstName lastName:(NSString *)lastName
{
_firstName = [firstName copy];
_lastName = [lastName copy];
}
7、在对象内部尽量直接访问实例变量
在对象之外访问实例变量时,总是应该通过属性来做,在对象内部访问实例变量时,作者建议读取实例变量时采用直接访问的形式,设置实例变量时通过属性来做。
.h
- (NSString *)fullName;
- (void)setFullName:(NSString *)fullName;
.m
- (NSString *)fullName{
return [NSString stringWithFormat:@"%@ %@",self.firstName,self.lastName];
}
- (void)setFullName:(NSString *)fullName
{
NSArray *components = [fullName componentsSeparatedByString:@" "];
self.firstName = [components objectAtIndex:0];
self.lastName = [components objectAtIndex:1];
}
重写实现方法
- (NSString *)fullName{
return [NSString stringWithFormat:@"%@ %@",_firstName,_lastName];
}
- (void)setFullName:(NSString *)fullName
{
NSArray *components = [fullName componentsSeparatedByString:@" "];
_firstName = [components objectAtIndex:0];
_lastName = [components objectAtIndex:1];
}
区别
- 不经过OC的方法派发,直接访问实例变量的速度快,在这种情况下,编译器生成的代码会直接访问保存对象实例变量的那块内存
- 直接访问实例变量时,不会调用其“设置方法”,这就如熬过了相关属性定义的“内存管理语义”,例如copy等。
- 如果直接访问实例变量,不会出发键值观测“KVO”。
- 通过属性访问,有助于排查与之相关的错误,可以通过其存取方法中增加断点进行调试。
这种方案,写通过设置方法来做,读取直接访问。
注意一、在初始化方法中应该如何设置属性值,这种情况下总是应该直接访问实例变量,因为子类可能会“覆写”设置方法。
注意二、惰性初始化,必须通过“获取方法”来访问属性,如果直接访问实例变量,则会看到尚未设置好的brain。
8、理解“对象等同性”这一概念
NSString *foo = @"Badger 123";
NSString *bar = [NSString stringWithFormat:@"Badger %i",123];
NSString *foo1 = @"Badger 123";
这个比较结果还是有意思的,这里不贴运行结果了。
NSObject 协议中有两个用于判断等同性的关键方法:
- (BOOL)isEqual:(id)object;
- (NSUInteger)hash;
isEqual判断步骤,指针,所属类,每个属性。
这里调用的是系统默认的方法,与作者写的方法有不同。即使是内存地址不同,也可以isEqual成立。
- (BOOL)isEqual:(id)other
{
if (other == self) {
return YES;
} else if (![super isEqual:other]) {
return NO;
} else {
EOCPerson *ohterPerson = (EOCPerson *)other;
if(![_firstName isEqualToString:ohterPerson.firstName]){
return NO;
}
if(![_lastName isEqualToString:ohterPerson.lastName]){
return NO;
}
return YES;
}
}
hash方法,若两对象相等,则其哈希码也想等,但是哈希码相同的对象却并未想等。如上例中bar和foo的哈希码相同,但是内存地址是不同的。
重写hash方法可以返回一个固定的值,这样在collection中使用这种对象会产生性能问题,因为collection在检索哈希表时,会用对象的哈希码做索引。如果collection用set实现,set可能会根据哈希码把对象分装在不同的数组中。在向set中添加新对象时,要根据其哈希码找到与之相关的那个数组,依次检查其中的各个元素,看数组中已有的对象是否和将要添加的新对象相等。如果相等,那就说明要添加的对象已经在set里面了,由此可知,如果每个对象返回相同的哈希码,那么在set中已有100000个对象的情况下,若是继续向其中添加对象,则需要将这100000个对象全部扫描一遍。
特定类所具有的等同性判定方法
(PS;个人理解重写isEqual方法)
等同性判定的执行深度
NSArray检测方式,对象个数,逐个调用“isEqual:”方法。
通过类的某个属性,“唯一标识符”来判断
容器中可变类的等同性
在容器中放入可变类对象的时候,把某个对象放入collection之后,就不应再改变其哈希码了。
NSMutableArray *arrayA = [@[@1,@2]mutableCopy];
[set addObject:arrayA];
NSMutableArray *arrayB = [@[@1,@2]mutableCopy];
[set addObject:arrayB];
NSLog(@"%@",set);
NSMutableArray *arrayC = [@[@1]mutableCopy];
[set addObject:arrayC];
NSLog(@"%@",set);
[arrayC addObject:@2];
NSLog(@"%@",set);
NSSet *setB = [set copy];
NSLog(@"%@",setB);
打印结果(PS:手写)
- {[1,2]}
- {[1],[1,2]}
- {[1,2],[1,2]}
- {[1,2]}
11、理解objc_msgSend的作用
void printHello() {
printf("hello");
}
void printGoodbye () {
printf("goodbye");
}
void doTheThing(int type) {
if (type == 0) {
printHello();
}else{
printGoodbye();
}
}
void printHello() {
printf("hello");
}
void printGoodbye () {
printf("goodbye");
}
void doTheThing(int type) {
void (*fun)();
if (type == 0) {
fun = printHello;
}else{
fun = printGoodbye;
}
fun();
}
第二种使用“动态绑定”,因为所要调用的函数知道运行期才能确定。编译器在这种情况下生成的指令与刚才那个例子不同,在第一个例子中,if和else语句里都有函数调用指令。而在第二个例子中,只有一个函数调用指令,不过待调用的函数地址无法硬编码在指令中,而是要在运行期读取出来。
id returnValue = [someObject messageName:parameter];
someObject叫做接受不了者,messageName叫做选择子,选择子与参数结合起来成为“消息”。转化为C语言函数调用
objc_msgSend(id self,SEL cmd,...)
这是个“参数个数可变的函数”,能接受两个或两个以上的参数。第一个代表参数接收者,第二个参数代表选择子,后续参数就是消息中的那些参数。
id returnValue = objc_msgSend(someObject,@selector(messageName:),parameter);
objc_msgSend函数会依据接受着雨选择子的类型来调用适当的方法。为了完成此菜做,该方法需要在接受者所属的类中搜寻其“方法列表”,找不到就沿着继承体系继续向上查找,找到合适方法之后再跳转,找不到执行“消息转发”。
objc_msgSend会将匹配结果缓存在“快速映射表”里,每个类都有这样一块缓存。其他特殊情况由OC运行环境的另一些函数来处理:
- objc_msgSend_stret
- objc_msgSend_fpret
- objc_msgSendSuper
每个类中都有一张表,其中的指针都会指向这种函数,选择子的名称则是查表时所用的“键”。objc_msgSend等函数正是通过这张表来寻找应该执行的方法并跳至其实现。请注意,原型的样子和objc_msgSend函数很想,这不是巧合,而是利用尾调用优化技术,令“跳转至方法实现”这一操作变得更简单。
如果某函数的最后一项操作是调用另外一个函数,那么久可以运用“尾调用优化”技术。编译器会生成调转至另一函数所需的指令码,而且不会向调用对战中推入新的栈帧。只有当某函数的最后一个操作仅仅调用其他函数而不会将其返回值另作他用时,才执行尾调用优化。如果不这么做的话,每次调用OC方法钱,都需要为调用objc_msgSend函数准备栈帧,大家在栈踪迹种可以看到这种栈帧,不优化会过早发生栈溢出。
(PS:我的个人理解,函数调用,会把函数的指针压入新的栈空间,尾调用就是返回时调用新的函数,也就是再次压入,如果还有调用,栈空间很容易溢出,这里OC使用了指令码,不将尾调用的函数压入栈,也就是理想情况下只在最开始的调用时压入一次,节省了栈空间)
12、理解消息转发机制
消息转发分为两大阶段。第一阶段先征询接受者,所属的类,能否动态添加方法,这叫做“动态方法解析”。第二阶段涉及“完整的消息转发机制”。
如果第一阶段执行失败,运行时系统会请接受者看看有没有其他对象能处理这条消息。如果没有则启动完整的消息转发机制,运行时系统会把与消息有关的细节封装在NSInvocation对象中,给接受者最后一次机会,令其设法解决当前还未处理的消息。
@dynamic string,number,date,opaqueObject;
- (instancetype)init
{
if(self = [super init]){
_backingStore = [NSMutableDictionary new];
}
return self;
}
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
NSString *selectorString = NSStringFromSelector(sel);
if([selectorString hasPrefix:@"set"]){
/**
1:消息接受者
2:方法选择子
3:待添加方法函数指针
4:待添加方法的“类型编码”
*/
class_addMethod(self, sel, (IMP)autoDictionarySetter, "v@:@");
}else{
class_addMethod(self, sel, (IMP)autoDictionaryGetter, "@@:");
}
return YES;
}
id autoDictionaryGetter(id self,SEL _cmd){
//从类中获取backingStore对象
EOCAutoDictionary *typedSelf = (EOCAutoDictionary *)self;
NSMutableDictionary *backingStore = typedSelf.backingStore;
//key是selector的名字
NSString *key = NSStringFromSelector(_cmd);
//返回这个值
return [backingStore objectForKey:key];
}
void autoDictionarySetter(id self,SEL _cmd, id value){
//从类中获取backingStore对象
EOCAutoDictionary *typedSelf = (EOCAutoDictionary *)self;
NSMutableDictionary *backingStore = typedSelf.backingStore;
//方法的应该是类似于setOpaqueObject:,我们需要截掉set和:,并把首字母转化为小写
NSString *selectorString = NSStringFromSelector(_cmd);
NSMutableString *key = [selectorString mutableCopy];
//删除:
[key deleteCharactersInRange:NSMakeRange(key.length-1, 1)];
//删除set
[key deleteCharactersInRange:NSMakeRange(0, 3)];
//最小化首字母
NSString *lowercaseFirstChar = [[key substringToIndex:1]lowercaseString];
[key replaceCharactersInRange:NSMakeRange(0, 1) withString:lowercaseFirstChar];
if(value){
[backingStore setObject:value forKey:key];
}else{
[backingStore removeObjectForKey:key];
}
}
以上代码的基本思路是,创建一个字典,存放属性的数据。
使用resolveInstanceMethod:方法截获到set和get请求。
按照一般逻辑,会先用set方法赋值,就动态添加这个方法class_addMethod(self, sel, (IMP)autoDictionarySetter, "v@:@");
在autoDictionarySetter方法中,会把set方法的方法名截取成get方法,步骤就是去除set和“:”然后首字母小写。把方法名作为key,传入的数据作为value,存在字典中。在调用get方法时,会根据get方法名找到字典中对应的value。
13、用“方法调配技术”调试“黑盒方法”
为什么要出现这个方法呢,因为我们即不需要源代码,也不需要通过继承子类来覆写方法就能改变这个类的本身功能。简而言之就是方法替换。
类的方法列表会吧选择子的名字映射到相关的方法实现上。
OC运行时系统提供方法可以操作这个表,如新增选择子,改变选择子对应的方法实现,交换选择子所映射到的指针。
本条讨论互换两个方法实现。
交换方法实现:
void method_exchangeImplementations(Method m1, Method m2)
上述参数的方法实现:
Method class_getInstanceMethod(class aClass, SEL aSelector)
执行下列代码,即可交换前面提到的lowercaseString 与 uppercaseString方法实现:
Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class], @selector(uppercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);
实际应用:
Method originalMethod = class_getInstanceMethod([self class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([self class], @selector(eocMyLowercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);
- (NSString *)eocMyLowercaseString
{
NSString *lowercase = [self eocMyLowercaseString];
NSLog(@"%@ => %@",self,lowercase);
return lowercase;
}
NSString *string = @"Asssssss";
NSString *lowString = [string lowercaseString];
2018-05-11 11:22:09.843709+0800 EffecitveOC[45073:2901031] Asssssss => asssssss
14、理解@“类对象”的用意
每个OC对象实例都是指向某块内存数据的指针。所以在声明变量时,类型后面要跟一个@“*”字符:
NSString *pointerVariable = @"Some thing";
对于通用的对象类型id,由于其本身已经是指针了,所以我们能够这样写:
id string1 = @"Some thing";
上面这种定义方式与用NSString*来定义性笔,语法意义相同,区别在于,如果声明时制定了具体类型,那么在该类的实例上调用其所没有的方法,会有警告。
super_class指针确立了继承关系,而isa指针描述了实例所属的类。
以下代码与原书运行有出入,应该是apple修改了规则
NSMutableDictionary *mutableDict = [NSMutableDictionary new];
BOOL boo1 = [mutableDict isMemberOfClass:[NSDictionary class]];
BOOL boo2 = [mutableDict isMemberOfClass:[NSMutableDictionary class]];
BOOL boo3 = [mutableDict isKindOfClass:[NSDictionary class]];
BOOL boo4 = [mutableDict isKindOfClass:[NSDictionary class]];
NSLog(@"%d%d%d%d",boo1,boo2,boo3,boo4);
2018-05-11 13:45:22.112139+0800 EffecitveOC[46246:2977156] 0011
以上代码调用isMemberOfClass时,由于apple使用了类簇模式,所以mutableDict并不是NSMutableDictionary类型,而是子类型__NSDictionaryM,故都为NO。
- 每个实例都有一个指向class对象的指针,用以表示其类型,而写着class对象则构成了类的继承体系
- 如果对象类型无法在编译期确定,那么就应该使用消息类型查询方法来探知isMemberOfClass isKindOfClass
-尽量使用类型消息查询方法来确定对象类型,不要直接比较类对象,因为某些对象可能实现消息转发功能。
第四章
23、通过委托与数据源协议进行对象间通信
委托模式,用于对象间的通信,可将数据与业务逻辑解耦。
比如用户界面有个显示数据的视图,此视图应包含数据所需的逻辑代码,不应界定要显示何种数据。视图对象的属性中,可以包含负责数据与事件处理的对象。这两种对象分别称为“数据源(data source)”和“委托(delegate)”。
第五章
29、内存管理
这本书有的地方和现在的方式有出入,我尽量总结和现在类似的概念或代码。
OC语言使用引用计数来管理内存。
- retain 增加引用计数
- release 减少引用计数
- autorelease 待稍后清理“自动释放池”时,再递减保留计数。
属性存取方法中的内存管理
- (void)setFoo:(id)foo {
[foo retain];
[_foo release];
_foo = foo;
}
先retain后release是因为 如果两个值指向同一个对象,先执行release可能会释放掉对象,retain就失效,实例变量成了悬挂指针。
autorelease可以保证对象在跨越“方法调用边界”后一定存活。
30、以ARC简化引用计数
内存泄漏的意思是,没有正确释放已经不再使用的内存。
ARC中引用计数还在执行,只不过保留与释放操作现在是由ARC自动为你添加。因为ARC会自动执行retain、release、autorelease等操作,所以调用这些方法是非法的。手动调用会干扰ARC工作。ARC调用这些方法时,不采用OC的消息机制,而是直接调用底层c语言版本。
使用ARC时必须遵循的方法命名规则
第六章块与大中枢派发
37、理解“块”这一概念
块的强大之处时:在声明它的范围内,所有变量都可以为其所捕获。也就是说,那个范围里的全部变量,在块里依然可用。
void (^someBlock)() = ^{
//block implementation here
};
int (^addBlock) (int a,int b) = ^(int a, int b){
return a+b;
};
int add = addBlock(2,5);
NSLog(@"%d",add);
默认情况下,为块所捕获的变量,是不可以在块里修改的,声明变量的时候可以加上__block修饰符,就可以在块内修改了。
NSArray *array = @[@1,@2,@3,@4];
__block NSInteger count = 0;
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if([obj compare:@2] == NSOrderedAscending){
count ++;
}
}];
NSLog(@"%lu",count);
如果块所捕获的变量是对象类型,那么就会自动保留它,系统在释放这个块的时候,也会将其一并释放。块本身也和其他对象一样,有引用计数,当最后一个指向块的引用移走之后,块就会瘦了,回收时也会释放块所捕获的变量,以便平衡捕获时所执行的保留操作。
如果读取或写入操作捕获了实例变量,那么也会自动把self变量一并捕获。self也是个对象,因而块在捕获它时也会将其保留,如果self所指代的那个对象同时也保留了块,那么这种种情况下就会导致“保留环”。
块本身也是对象,在存放块对象的内存区域中,首个变量指向class对象的指针,该指针叫做isa。其余内存里还有块对象正常运转所需的各种信息。
下面这个图就这样吧。
全局块、栈块、堆块
这一段其实解释了为什么现在block要用copy修饰
if和else语句中的两个块都分配在栈内存中。编译器会给每个块分配好栈内存,等离开相应范围后,编译器有可能吧分配给给块的内存覆写掉。这样就可能导致程序崩溃。为了解决此问题,可以给块发送copy,将块从栈复制到堆上,块就是带有引用计数的对象了,就需要arc管理了。
全局块,这种块不回捕捉任何状态,运行时也无需有状态来参与,块所使用的整个内存区域,在编译器已经完全确定,因此,在全局块可以声明在全局内存中,而不需要每次用的时候与栈中创建,另外全局块的拷贝操作是个空操作,因为全局块绝不可能为系统所回收,这种块实际上相当于单例。
41、派发队列
文章中提到了synchronized和NSLock,篇幅不多,我这边也就直接用GCD。
就属性来说,可以用原子性来修饰,即可实现,如果使用GCD就可以这么写
- (NSString *)name{
@synchronized(self){
return _name;
}
}
- (void)setName:(NSString *)name
{
@synchronized(self){
_name = name;
}
}
使用 @synchronized(self)会很危险,因为所有同步块都会彼此抢夺同一个锁,要是有很多属性都这么写的话,那么每个属性的同步块都要等其他所有同步块执行完毕才能执行,我们想要的是每个属性各自独立的执行。同样,atomic也不是肯定线程安全。
gcd第一步
使用“穿行同步队列”,将读写操作安排在同一个队列里,即可保证数据同步。
_syncQueue = dispatch_queue_create("com.zhjy.larkdata.FaceEaxm", NULL);
- (NSString *)name{
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _name;
});
return localSomeString;
}
- (void)setName:(NSString *)name
{
dispatch_sync(_syncQueue, ^{
_name = name;
});
}
此模式的思路是,把设置操作与获取操作都安排在穿行队列中,这样的话,所有针对属性的访问操作就都同步了。
然而还可以进一步优化。设置方法并不一定非得是同步的。设置实例变量所用的块,并不需要想设置方法返回什么值。
- (void)setName:(NSString *)name
{
dispatch_async(_syncQueue, ^{
_name = name;
});
}
把同步派发改成了异步派发,坏处:执行异步派发,需要拷贝块,效率低。
多个获取方法可以并发执行,而获取方法与设置方法之间不能并发执行
_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- (NSString *)name{
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _name;
});
return localSomeString;
}
- (void)setName:(NSString *)name
{
dispatch_async(_syncQueue, ^{
_name = name;
});
}
现在无法正确实现同步,所有的读取和写入会在同一个队列上执行,不过由于是并发队列,所以读取与写入操作可以随时执行,而我们恰恰不想让这些操作随意执行,可以用一个栅栏来解决。
- (NSString *)name{
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _name;
});
return localSomeString;
}
- (void)setName:(NSString *)name
{
dispatch_barrier_sync(_syncQueue, ^{
_name = name;
});
}
对于读取操作依然可以并发执行,但是写入操作就要单独执行了。