6、理解“属性”这一概念
Objective-C面向对象语言编程。
对象就是“基本构造单元”,开发者用对象存储并传递数据。
对象之间传递数据并且执行任务的过程就叫做“消息传递”
程序运行起来。相关支持的代码就叫做“Objective-C runtime”。“属性”是Objective-C的一项特性,用于封装对象中的数据。
Objective-C对象会把数据保存为各种实例变量。
实例变量通过“存取方法”(accessmethod)访问。
getter 用于读取变量值
setter用于写入变量值@property
对象接口的定义中,可以使用属性。 -> 标准的写法
编译器会自动写出一套存取方法,用以访问给定类型中具有给定名称的变量。
@property (nonatomic ,copy) NSString *nameString;
等同于下面的这种写法
- (void)setNameString:(NSString *)nameString;
- (NSString *)nameString;
-
属性优势:
- 编译器自动编写与访问这些属性所需的方法 =“自动合成”(autosynthesis)
这个过程是由编译器在编译时期执行的,所以编辑器看不到这些“合成方法”的源代码。 - 除了生成方法代码之外,编译器还会自动向类中添加适当类型的实例变量,并且添加下划线作为实例变量的名字。
比如上面的名称就为_nameString。
- 编译器自动编写与访问这些属性所需的方法 =“自动合成”(autosynthesis)
-
自己实现存取方法
- 使用@dynamic关键字 。告诉编译器不让自动创建实例变量、存取方法!
-
属性特质
- 原子性
默认情况下。编译器合成的方法通过通过锁定机制确实其原子性(atomicity)。
如果属性具备nonatomic特质,则不使用同步锁。 - 读/写权限
readwrite(读写):具有setter和getter方法!
readonly(只读):仅有getter方法。 - 内存管理
assign:针对CGFloat或者NSInteger等“纯量类型”(基础数据类型 和C数据类型)简单赋值,不更改索引计算。
strong:为属性设置新值时,保留新值,释放旧值,再将新值设置上去。
weak:既不保留新值,也不释放旧值,同assign。
copy:与strong类似,但是并不保留新值。而是将其“拷贝”。
经典案列:NSString 确保对象中的字符串值不会无意间改动。
- 原子性
7、在对象内部尽量直接访问实例变量
- 使_XXX直接访问实例变量。
- 速度快。不经过Objective-C的“方法派发”(method dispatch),编译器直接访问保存对象实例变量的那块内存
- 直接访问实例变量,不调取“设置方法”(setter)这就绕过了内存管理语义。
- 直接访问实例变量,也不会触发“键值观测”(Key-Value Observing, KVO)
这样做是否有问题还是取决于具体的对象行为。
- 使用self.XXX来访问
- 有助于排查与之相关的错误,因为可以给“setter”和“getter”设置断点
- 懒加载也是需要通过“获取方法”来访问属性。否则。实例变量永远不会初始化。
- 建议:
除特殊情况。
在读取实例变量的时候采用直接访问的形式,
在设置实例变量的时候通过属性来做。
8、理解“对象等同性”这一概念
等同性(equality)
一般情况下相比较我们都是用的 “==” 但是比较出来的结果未必是我们想要的。
因为“==”比较是指针本身,而不是其对象。-
NSObject中的“isEqual”
- 判断两个对象的等同性。
- NSObject协议中2个判断等同性的关键方法
- (BOOL)isEqual:(id)object; @property (readonly) NSUInteger hash;
定义:如果“isEqual:”方法判断两个对象相等,那么其hash方法也必须返回同一个值。
如果两个对象的hash方法返回同一个值,“isEqual”未必会认为两者相等。
NSString实现了一个独有的等同性判断方法:isEqualToString
该方法比“isEqual”快,因为该方法快递对象规定为NSString。而“isEqual”还要执行额外步骤,因为“isEqual”不知道受测对象类型。NSArray与NSDictionary也有类似的特殊的等同性方法。
“isEqualToArray”与“isEqualToDictionary”
如果检测到受测对象不是数组或者字典就会抛出异常。-
自己实现等同性方法原理:
- 首先,判断两个指针是否相等。相等则说明指向同一对象!
- 其次,比较两个对象所属的类(考虑到父类与子类的判断)
- 然后,检测每个属性是否相等(不要盲目逐步检查每条属性,而是根据需求来定制)
- 最后,实现hash方法:
等同性约定:若两个对象相等,则哈希码相等,但是两个哈希码相同的对象却未必相等。(应使用计算速度快而且哈希码碰撞几率低的算法,否则会影响性能)
-
小技巧:
- 如果要重写“isEqual”方法。
如果受测参数与接收消息对象属于同一个类,就调用自己写的判定方法。否则就交给超类来判断。 - 等同性的判断深度。
如果判断两个对象的所有属性是否相等,这样的叫“深度等同性判定”。
不过更多时候是根据其中部分数据即可判断二者是否等同。
- 如果要重写“isEqual”方法。
9、以“类族模式”隐藏实现细节
- 类族:把实现细节隐藏在一套简单的公共接口后面。
- 例子:
- 系统框架中有很多类族。
比如UIButton创建的时候的类方法
UIButton *button = [UIButton buttonWithType:<#(UIButtonType)#>]
这样该方法返回的对象,决定传入的按钮类型。 - NSArray与可变类型NSMutableArray。
有两个抽象基类,一个不可变数组,一个可变数组。 - 动手创建一个类族:
- 需求:一个公司分为2种人:1、管理者。2、工人。分别干不同的事!
- 创建一个基于NSObject的类
#import <Foundation/Foundation.h>
typedef NS_ENUM(NSUInteger, SCUserType) {
SCUserTypeManager,
SCUserTypeWorker,
};
@interface SCUser : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
//创建
+ (SCUser *)scUserWithType:(SCUserType)type;
//做事情
- (void)doWork;
@end
- .m的实现:
#import "SCUser.h"
#import "SCUserWorker.h"
#import "SCUserManager.h"
@implementation SCUser
+ (SCUser *)scUserWithType:(SCUserType)type {
switch (type) {
case SCUserTypeManager:
return [SCUserManager new];
break;
case SCUserTypeWorker:
return [SCUserWorker new];
break;
}
}
- (void)doWork {
}
@end
每个“实体子类” 都是从基类继承来的。比如:
#import "SCUser.h"
@interface SCUserManager : SCUser
@end
//.m的实现
#import "SCUserManager.h"
@implementation SCUserManager
- (void)doWork {
NSLog(@"管理者巡逻");
}
@end
10、在既有类中使用关联对象存放自定义数据
-
关联对象(Associated Object)
为了解决某些情况(无法从对象所属的类中继承一个子类,然后用子类对象存放相关信息)
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
此方法可以给定的键和策略为某对象设置关联对象值objc_getAssociatedObject(id object, const void *key)
此方法根据给定的键从某个对象中获取相关对象值
objc_removeAssociatedObjects(id object)此方法移除指定对象的全部关联对象。
项目中遇到的问题(runtime)
当时是为了解决NSURL中有个URLWithString的方法会默认把带中文的链接给转码。因为程序中很多地方都用到了这个方法,很明显,一个一个的修改是很费力气的。用到了runtime中的一个交换方法:
自己实现一个函数,然后与系统的函数交换一下。完美解决问题。
主要代码:
#import <objc/runtime.h>
@implementation NSURL (Unicode)
+ (void)load {
/*
self:UIImage
谁的事情,谁开头 1.发送消息(对象:objc) 2.注册方法(方法编号:sel) 3.交互方法(方法:method) 4.获取方法(类:class)
Method:方法名
获取方法,方法保存到类
Class:获取哪个类方法
SEL:获取哪个方法
imageName
*/
// 获取imageName:方法的地址
Method URLWithStringMethod = class_getClassMethod(self, @selector(URLWithString:));
// 获取wg_imageWithName:方法的地址
Method sc_URLWithStringMethod = class_getClassMethod(self, @selector(sc_URLWithString:));
// 交换方法地址,相当于交换实现方式2
method_exchangeImplementations(URLWithStringMethod, sc_URLWithStringMethod);
}
+ (NSURL *)sc_URLWithString:(NSString *)URLString {
NSString *newURLString = [self IsChinese:URLString];
return [NSURL sc_URLWithString:newURLString];
}
11、理解 objc_msgSend 的作用
-
Objective-C中给对象发消息
[object message:parameter];
原理:
objc_msgSend(id self, SEL cmd,...)
核心函数,这个是“参数个数可变的函数”,能接收两个或者两个以上的参数。
第一个参数代表接收者。第二个参数代表方法的名字。
上面的OC代码换成函数就为:
objc_msgSend(object, @selector(message:),parameter);
objc_msgSend函数根据接收者和方法名来调用适当的方法。
过程:
1.先到所属的类寻找“方法列表”,找到就跳转
2.找不到,就会沿着继承体系继续向上查找,找到再跳转。
3.实在找不到就执行“消息转发”
看起来调用一个方法需要很多步骤。但是objc_msgSend会将匹配结果缓存在“快速映射表”里。这样子执行就快了。 其他的方法:
// 待发送消息返回结构体
objc_msgSend_stret
//消息返回的是浮点数
objc_msgSend_fpret
//给超类发消息 例如[super XXX];
objc_msgSendSuper
大家码代码时期能更多的了解一些底层的工作原理。在调试的时候会帮助你很多。
12、理解消息转发机制
- 消息转发:
因为Objective-C中,在编译期向类发送无法解读的消息并不会报错,因为在运行期还可以继续向类添加方法。因此,编译器在编译时无法确定类中到底会不会有某个方法实现。
当对象接受到无法解读的消息后,就会启动“消息转发”机制,程序员可以由此告诉对象如何处理未知的消息。
大家在开发期间肯定见过这样的错误:
unrecognized selector sent to instance 0x610000026560
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[SCUserWorker sss]: unrecognized selector sent to instance 0x610000026560'
错误原因:是因为接收者无法理解sss的这个方法名,因此致使程序崩溃。
- 对象在接收到无法解读的消息后,会依次调用下列方法。
+ (BOOL)resolveInstanceMethod:(SEL)sel
在这个方法中,参数就是未知的方法名称。在这里你可以解决问题。
- (id)forwardingTargetForSelector:(SEL)aSelector
这个是备援接收者,也就是给接收者第二次处理的机会,如果可以找到备援对象则将其返回,若找不到就返回nil。 - 完整的消息转发
- (void)forwardInvocation:(NSInvocation *)anInvocation
启动完整的消息转发机制,首先要创建 NSInvocation 对象,把尚未处理的消息相关的细节全部封与其中(方法名,目标以及参数)。
使用:只需改变调用目标,使消息在新目标上得以调用就好了,和第二种方法“备援接收者”等效。
-
案列:使用class_addMethod动态添加方法
假设我故意一个类只在.h声明了方法 没有在.m中实现该方法
结果就会报上面的错,接收者无法解读消息。让你用runtime动态添加方法你会怎么办呢?- 考虑
原因是因为没有实现该方法,所以无法解读,那么我们要为其添加方法。
那么这个方法添加到哪呢?该如何添加? - 动手
首先找到没有实现方法的那个类,在其.m添加
+ (BOOL)resolveInstanceMethod:(SEL)sel
这个方法。上述讲到过,无法解读消息时会第一时间调用这个方法,我们可以在这来解决问题。
接下来要用到runtime中的 class_addMethod 方法
class_addMethod(__unsafe_unretained Class cls, SEL name, IMP imp, const char *types)
1.Class cls : 参数表示添加新方法的类
2.SEL name : 表示方法名称
3.IMP imp : 表示由编译器生成的、指向实现方法的指针。也就是说,这个指针指向的方法就是我们要添加的方法。
4.const char **types :最后一个参数 *types 表示我们要添加的方法的返回值和参数。
主要代码在下面:
eat:只声明没有实现的方法。
SCUser:创建的类。
- 考虑
C语言函数的实现
记得导入
#import <objc/runtime.h>
void sayHello (id self,SEL _cmd) {
NSLog(@"Hello");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(eat)) {
class_addMethod([SCUser class], sel, (IMP)sayHello, "v@:@");
return YES;
}
return [super resolveInstanceMethod:sel];
};
- OC形式的实现
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(eat)) {
class_addMethod([SCUser class], sel, class_getMethodImplementation(self, @selector(sayHello)), "v@:@");
return YES;
}
return [super resolveInstanceMethod:sel];
};
- (void)sayHello {
NSLog(@"hahaha");
}
13、用“方法调配技术”调试“黑盒方法”
- 在第10条中有介绍过交换方法的案列。
- 交换方法:
- 在运行期,向类中新增或者替换方法。
- 使用另一个方法替换一个方法可以向其添加新功能。
- 只有调试程序中才会用的运行期修改方法,不宜滥用。
14、理解“类对象”的用意
- 看下面的代码
NSString *nameStirng = @"小伙子";
可以理解:nameString 为存放内存地址的变量。而NSString自身的数据就存在地址中。所有的Objective-C对象都是如此。 - 还可以这样写:
id nicknameStirng = @"牛";
对于通用的对象类型id,因为其自身已经是指针了。所以可以这样写。 - 比较
两者语法意义相同,
唯一区别:如果声明的时候指定了具体类型,那么在该类实例上调用没有的方法时,编译器会发出警告信息。 - id的定义
typedef struct object {
Class isa;
} *id;
说明每个对象结构体的首个成员是Class类的变量。通常称为“isa”指针。
- metaclass
metaclass就是isa指向的一个结构体。 - 举例
用一个例子说明:
大学期间,小明的辅导员要调查小明家里有没有党员。
首先,辅导员通过身份证号找到小明的档案,发现小明不是党员,从小明的档案中发现小明父母的身份证号,通过小明父母的身份证号找到小明父母的档案,发现小明父母都是党员。
身份证号 = isa ,档案 = metaclass。
(这是作者自己的粗浅理解,如果不对,欢迎指出) - 检测继承体系
- isKindOfClass
isKindOfClass来确定一个对象是否是一个类的成员,或者是派生自该类的成员。 - isMemberOfClass
isMemberOfClass只能确定一个对象是否是当前类的成员。
- isKindOfClass
接下来也将会继续整理。如果觉得有用请点个喜欢!
您的支持将是我继续写作的动力!谢谢。
观“编写高质量iOS与OC X代码的52个有效方法”有感(一)· 熟悉Objective-C
观“编写高质量iOS与OC X代码的52个有效方法”有感(二)· 对象、消息、运行时
观“编写高质量iOS与OC X代码的52个有效方法”有感(三)· 接口与API设计
观“编写高质量iOS与OC X代码的52个有效方法”有感(四)· 协议与分类