文章目录
- 前言
- 6 理解”属性“这一概念
-
-
-
- 定义变量
- 不兼容现象的出现
- 解决不兼容现象-@Property
- 使用属性更便捷
- 属性特质
-
-
- 原子性
- 读写权限
- 内存管理语义
- 方法名
- 原子性和非原子性
- 要点总结
- 7 在对象内部尽量直接访问实例变量
-
- 8 理解“对象等同性”
-
- == 和 isEqual
-
-
- “==” 判断的依据
- “isEuqalToString"
- NSObject协议里的方法
-
- 特定类所具有的等同性判断方法
- 等同性判定的执行深度
- 容器中可变类的等同性
- 要点总结
- 9 以“类族模式”隐藏实现细
-
-
-
- OC框架里的类族
- 理解类族模式的实现
- 手动增加实体子类的规则
- 要点总结
- 10.在既有类中使用关联对象存放自定义数据
-
- 11.理解objc_msgSend的作用
-
-
-
- 静态绑定和动态绑定
- 理解OC的消息机制-动态绑定
- OC如何发送消息
- objc_msgSend
- `objc_msgSend`其他边界情况
- 尾调用优化
- 要点总结
- 总结
前言
- 继续学习OBJC第二章
- 蓝皮书的第二章讲的是对象,消息,运行期。这里面我只对对象比较熟悉,因为经常用。
- 在本章的前言里面介绍了它们三个的关系,对象是OC基本构造单元,我们在使用对象的时候可以通过对象来存储并传递数据,而在对象之间进行传输数据的并执行任务的过程就叫做消息传递。而在程序运行起来之后的过程,为其提供了相关支持的代码叫做OC运行期的环境 runTime
- runtime提供了一些能够给对象和对象之间能够传递消息的重要参数,并且包含了各种逻辑。这就是简单总结了一下三者的关系
6 理解”属性“这一概念
- “属性”是OC的一项特性,用于封装对象中的数据。OC对象通常会把其需要的数据保存为各种实例变量。实例变量通过“存取方法”来访问。其中,“获取方法”用于读取变量值,而“设置方法”用于写入变量值。
定义变量
@interface EOCPerson : NSObject {
@public
NSString *_name;
NSString *_name2;
}
@end
- 这是Java和C++工程师经常干的事情,首先我们可以通过@publicd等关键字来限定变量的作用域。而在OC语言里面这样的写法存在问题,就是在编译时期对象的布局已经固定了,我们现在在name前面加一个变量
- 由于在编译时期已经确定了name的位置,系统俗偏移量,如果贸然的添加一个实例变量,那么会出现以前name偏移量的位置变成了 dateBirth ,书上是这样解释的
不兼容现象的出现
- 在平常的书写里面并不会报错,因为我们在添加了新的实例变量之后就自然会重新编译,这也是修改类定义之后必须要做的事情。真正的问题在于,某个代码库的一个代码使用了一份旧的类的定义吗,但是和它相链接的代码使用了新的类定义,那么在运行的时候就会出现不兼容现象,至于这个不兼容,我还没有碰到过
解决不兼容现象-@Property
- 对于OC语言的解决方法是利用存取方法访问实例变量,也就是常说的属性。
- 属性的实现还是实例变量,但是出现了新的名词 @property,在之前有学习过并且一直在用属性这个关键字,对于之前变量的定义我们需=需要自己实现存取方法setter getter方法,但是对于@Property语法
- 这样子相当于如下代码
- 上述代码是对应的存取方法实现,我刚开始以为- (Nsstring *)是变量,属实是学着忘着,后来翻了翻博客这样写就测试出来了
使用属性更便捷
- 对一个变量添加了@property 要访问属性我们就可以直接点语法或者箭头➕下划线变量
- 使用property会对对属性生成2个实例变量,访问方法如下
- 使用了属性,编译器会自动合成访问属性所需要的方法,此过程叫自动合成。这个过程实在编译期进行的,我们是看不到这些源代码的。
- 需要注意的是,当我们不需要系统自动合成存取方法的时候我我们可以这样写
@dynamic firstName;
- 那么系统会自动取消合成存取方法,当然自动合成的下划线变量也无法访问
属性特质
- 这里的属性特质特别指代了四个特性,在之前也学习过其实就是属性关键字OC属性关键字
原子性
- 在默认情况下,由编译器所合成的方法通过锁定机制确保其原子性。在并发编程中,如果某操作具备整体性,也就是说,系统其他部分无法观察到其中间步骤所生成的临时结果,而只能看到操作前与操作后的结果,那么该操作就是“原子的”,或者说该操作具备“原子性”。如果属性具备nonatomic特质,则不使用同步锁,如果一个属性不具备nonatomic特质,那它具有原子特性
读写权限
内存管理语义
- 学了很久OC了,之前知道在属性里面添加一些关键字,比如数组需要copy,控件需要strong,协议需要weak,NSIntger需要assgin等等,这些都是OC语言需要封装属性的方法特质
- assign :设置方法只执行纯量类型的简单赋值(CGFloat, NSIntger等)
- strong: 强拥有关系,表示了指向并且拥有该对象,其修饰的对象引用计数会加1.该对象只要引用计数不为0则不会被销毁。当然强制将其置为nil也可以销毁它。
- weak:表示了指向但不拥有该对象,其修饰的对象引用计数不变并且在程序结束的时候自行销毁,属性也会清空
- unsafe_unretained :unsafe_unretained和weak的区别在于unsafe_unretained针对的是对象类型,在其杯销毁的时候该对象nil但是属性还存在- 注意区别于weak
- copy: copy 修饰的是不可变对象,而strong修饰的是可变对象
-
- copy修饰不可变对象、原对象为不可变对象时,将原对象赋值给属性,会将原对象进行copy,此时是浅复制,两个指针指向的是同一个地址。
方法名
原子性和非原子性
- atomic- 默认线程安全
-
- atomic(默认属性)原子属性,为setter方法加锁,线程安全的,效率相对低。
-
- atomic描述的是属性赋值,属性赋值中还包含着很多其他操作,如访问对象,赋值等等,natomic是保证这个赋值的整个过程的完整性,并且不受其他线程的干扰,要么成功要么失败- 原子操作。
-
- atomic仅仅保证了属性赋值的过程是安全并不保证对属性的读写操作是安全的
-
- atomic对一个数组,进行赋值或获取,是可以保证线程安全的。但是如果进行数组进行操作,比如给数据加对象或移除对象,是不在atomic的保证范围。
- nonatomic- 高效
-
- nonatomic是非原子属性,线程不安全的,不会为属性的Setter方法加锁,效率高 -推荐使用的原因在看了书之后有了新的理解如下
-
- 其实在写一些小项目的时候会发现所有属性都会使用 nonatomic关键字,在iOS开发中为了保证线程安全的操作,还需要使用更为深层次的锁定机制,也就是一个属性在某个线程里面被使用时候为了避免其他线程同时操作该属性,为了维护线程安全,
要点总结
- 可以通过**@property**语法来定义对象中所封装的数据。
- 通过“特质”来指定存储数据所需的正确语义。尤其指代属性关键字
- 在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义。
- 开发iOS程序时应该使用nonatomic属性,因为atomic属性会严重影响性能,nonatomic的加锁维护了线程安全
7 在对象内部尽量直接访问实例变量
- 在对象之外访问实例变量的时候,总是应该通过属性来做,但是对于对象内部访问实例变量,OC更希望直接访问实例变量
- 这里的属性访问是self.name 而直接访问这是_name;
- 二者区别
- 我的理解
-
- 取值赋值要用self.去访问,通过setter.getter方法访问,下划线是成员变量,一般不允许直接对成员变量进行修改,安全问题。所以要通过属性点语法来取值赋值。
要点总结
- 在对象内部读取数据时,应该直接通过实例变量来读,而写入数据时,则应通过属性来写。
- 在初始化方法及其dealloc方法中,总是应该直接通过实例变量来读写数据。
- 有时会使用惰性初始化技术配置某份数据,这种情况下,需要通过属性来读取数据。
8 理解“对象等同性”
== 和 isEqual
- 在C语言中判断相等通常“==”来判断,当然OC里也可以用这个方法,不过有时候比较出来的结果不一定是我们想要的,==的比较仅仅是比较指针本身,并不会对其所指代的对象进行比较.
- 而对于OC里的任何对象都是继承NSObject的 ,它们都可以使用协议里的 “isEqual”方法来判断两个对象的等同性,这个方法针对的是两个不同的类型的对象
- 对于相同的对象,OC提供了更快的方法,比如对于NSString类型 OC提供了"isEqualToString"方法来判断其是否相等
NSString *strOne = @"BadGer 123";
NSString *strBar = [NSString stringWithFormat:@"BadGer %d", 123];
BOOL equalA = (strOne == strBar);
BOOL equalB = [strOne isEqual:strBar];
BOOL equalC = [strOne isEqualToString:strBar];
“==” 判断的依据
- 前面说过“==”判断的是两个对象的指针是否相等,那么对于字符strOne和字符串strBar,我们探究它们在内存的地址是如何
- 我单独抽调出了它们的内存位置,明显是不同的指针,== 就直接认为它们不是相等的
- 当我们直接copy一个新的字符串的时候,结果是这样的
NSString *strOneOne = [strOne copy];
BOOL equalD = (strOne == strOneOne);
- 首先先知道这是浅拷贝,再单独抽出来它们发现它们的指针地址指向了同一块内存区域,这就更加印证了 == 判断指针是否相同。
“isEuqalToString"
- 这个是OC提供的对于相同对象判断的方法,这个对象的参数必须是NSString * 对象 ,否则抛出异常,这里不多赘述
NSObject协议里的方法
- NSObject里的isEqual 方法相比于上面的isEuqalToString 是慢一些,因为系统还要去判断是不是同一个类型的对象
- 而NSObject协议提供的判断等同性方法有两个。
- NSObject类对这两个方法的默认实现是:当且仅当其“指针值”(内存地址)完全相等时,这两个对象才相等。当hash函数和isEqual函数返回的值都完全相同的时候才能说这两个对象相等。仅hash函数返回相同但是isEqual函数返回的值也可能不同。
isEqual
- 由于看不到里面是如何实现的,我们可以对isEqual方法进行仿写,
- 从这里面可以看出isEuqal的逻辑:首先判断两个指针是否相等,若相等则指向同一个对象,接下来比较对象所属的类,若不属于同一类则对象不相等,最后还需要对对象的每个属性进行一次判断。
hash
- 对于等同性来说,两个对象相等那么他们的hash编码一定相等,前面测试了strOne 和 strBar 是相等的,那么测试一下他们的hash编码
NSUInteger hashA = [strOne hash];
NSUInteger hashB = [strBar hash];
- 不出意外它们是相等的,符合对象等同性的约束。
- 其实对于hash方法存在一定的弊端,就是hash编码存在偶然性,可能两个对象hash编码相等但是却不是同一个对象,所以为了保证高效率方法,书上展示hash内部的时候建议返回的是位运算的对象
- (NSUInteger)hash {
NSUInteger hashA = [_str1 hash];
NSUInteger hashB = [_str2 hash];
return hashA ^ hashB;
}
- 测试结果虽然为0,但是书上的位运算是异或运算,所以相同为0.
- 这就是hash的判断原理,我更喜欢isEqual,可以自定义如何判断两个对象是否相等的条件
特定类所具有的等同性判断方法
- 除了自定义类之外,每个类几乎都有其特定的等同性判定方法,NSArray类为“isEqualToArray:”,NSDictionary类为“isEqualToDictionary:”,若不是同一个类型的话,它就会直接抛出异常,是同一个类型才会开始一一处理。
- 书上是如此简化的特定类的等同性判断
- 自己写的isEuqal方法里面先判断了是否是同一类型的对象,如果是同一类那就调自己写的判断方法,如果不是同一类那就交由超类来判断
等同性判定的执行深度
- NSArray类型如何进行等同性判断呢?如果存在两个数组长度都不相等那么系统还会继续进行等同性的判断吗?
- 就用NSArray来说,NSArray的检测方式为:先看两个数组所含的对象个数是否相同,若相同,则在每个对应位置的两个对象身上调用其“isEqual:”方法。如果对应位置上的对象均相等,那么这两个数组就相等,这叫做“深度等同性判定”。
- 是否需要在等同性判定方法中检测全部字段取决于受测对象。只有类的编写者才可以确定两个对象实例在何种情况下应判定为相等。
容器中可变类的等同性
- 这里书上想表的意思是当你在容器里创建了一个可变对象的时候,尤其指Set类型,这个时候不应该去改变它的哈希码了,也就是你一旦把可变对象放入容器并且使容器的哈希码改变,那么本质上就是改变了该可变对象本身的位置,这样的做法不会抛出异常,但是是需要谨慎操作的。
- 当你真的这么做了,那么出现的问题也需要相应的代码来处理
要点总结
- 若想检测对象的等同性,请提供“isEqual:”与hash方法。
- 相同的对象必须具有相同的哈希码,但是两个哈希码相同的对象却未必相同,哈希码存在偶然性。
- 不要盲目地逐个检测每条属性,而是应该依照具体需求来制定检测方案,能有特殊的一针见血的标志判断是最好的
- 编写hash方法时,应该使用计算速度快而且哈希码碰撞概率低的算法。
9 以“类族模式”隐藏实现细
- 类族是一种很有用的模式,可以隐藏基本类型背后实现的细节。在OC语言的系统框架里面普遍使用此模式。
OC框架里的类族
- 对于OC语言的框架,大部分集合都是的类都适用的类族模式,比如可变数组和不可变数组,虽然在表面上定义了两个接口,但是仔细看来二者是存在共同的方法的,那么二者合起来看也是类族。
NSArray *mayBeArray = [NSArray arrayWithObject:@"isSameArray"];
NSMutableArray *mutableArray = [[NSMutableArray alloc] initWithObjects:@"1", @"2", nil];
- 定义2个数组,观察发现是存在公用的方法的
- 不可变数组实现的事所有数组的同用方法,可变数组仅仅实现的是可变数组可用的代码,二者在实现各自类型的同时能够有公用代码,也可以互相转换,这就是类族模式的体现
理解类族模式的实现
- 在使用NSArray的alloc方法来获取实例时,该方法首先会分配一个属于某个类的实例,此实例充当一个“占位数组”,也就是说,你把这个位置是先分配给其类族的,后来其类族才将这个位置分配给你创建的具体数据类型的
- 测试一下NSArray内部是否有实现类族模式
NSArray *mayBeArray = [NSArray arrayWithObject:@"isSameArray"];
if ([mayBeArray class] == [NSArray class]) {
NSLog(@"wow Same Class");
}
- 打印结果为空,发现我们alloc的数组实例其实不是NSArray类,而是
__NSSingleObjectArrayI *
一个类,这也印证了NSArray里面存在我们看不到的实体子类
- 添加多个元素试试发现出现了同样的新的实体子类
- 解释:首先验证了NSArray这个类是一个类族,其次mayBeArray class是返回的NSArray隐藏在公共接口的后面的某个内部类
- 不过OC仍旧提供了判断实例所属的类 是否位于类族之中
isKindOfClass
if ([mayBeArray isKindOfClass:[NSArray class]]) {
NSLog(@"mayBeArray isKindOfClass NSArray");
} else {
NSLog(@"ON NO Bad");
}
手动增加实体子类的规则
- 子类应该继承自类族中的抽象基类。
- 子类应该定义自己的数据存储方式。
- 子类应当覆写超类文档中指明需要覆写的方法。
要点总结
- 类族模式可以把实现细节隐藏在一套简单的公共接口后面。
- 系统框架中经常使用类族。(可变和不可变是最直接的例子)
- 从类族的公共抽象基类中继承子类时要当心,若有开发文档,则应首先阅读。
10.在既有类中使用关联对象存放自定义数据
关联对象的出现
- 在iOS开发里,分类是不能添加成员变量的,只允许给分类添加属性,所以出现了关联对象
- 一个类添加属性@property,实际上是做了3个事情
-
-
-
- 给一个分类添加属性
-
- 所以在OC里需要使用关联对象给分类添加成员变量
关联对象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
id objc_getAssociatedObject(id object, const void *key);
void objc_removeAssociatedObjects(id object);
对象/属性/值/关联策略
objc_setAssociatedObject(self,“str”,urlString,OBJC_ASSOCIATION_COPY);
objc_getAssociatedObject(self,“str”);
OBJC_ASSOCIATION_ASSIGN
OBJC_ASSOCIATION_RETAIN_NONATOMIC
OBJC_ASSOCIATION_COPY_NONATOMIC
OBJC_ASSOCIATION_RETAIN
OBJC_ASSOCIATION_COPY
- 值得注意的是关联对象虽然好用,但是我们也是在没有办法继承类的情况下为了方便才使用的,本质上面关联对象的简便已经经过了层层筛选不得才使用关联对象
要点总结
- 可以通过“关联对象”机制来把两个对象连起来。
- 定义关联对象时可指定内存管理语义,用以模仿定义属性时所采用的“拥有关系”与“非拥有关系”。
- 只有在其他做法不可行时才应选用关联对象,因为这种做法通常会引入难于查找的bug。
11.理解objc_msgSend的作用
- 这条到第12条讲的是iOS的消息机制,也就是在对象上调用方法机制,在OC的专业术语里面叫做
pess a message 消息传递
。对于每个消息,消息都有名称和选择子,可以用来接收参数和具有返回值
- 我们可以近似的理解OC的消息机制和C的函数机制类似但不一样。以下2段代码解释了区别
静态绑定和动态绑定
void printHello() {
printf("Hello iOS\n");
}
void printGoodBye() {
printf("GoodBye iOS\n");
}
int main () {
int type;
scanf("%d", &type);
if (type == 0) {
printHello();
} else {
printGoodBye();
}
}
- 上述代码总结到了C语言函数调用的方法,C语言使用静态绑定,也就是说在编译期就能决定运行时所调用的函数
- 在上述代码里。在编译的时候就已经知道了系统存在hello 和 Goodbye函数,于是会直接生成函数的指令,所以函数的地址即被硬编码在了指令里面
- 下面理解动态绑定
#include
void printHello() {
printf("Hello iOS\n");
}
void printGoodBye() {
printf("GoodBye iOS\n");
}
int main () {
int type;
void (* function)();
scanf("%d", &type);
if (type == 0) {
function = printHello;
} else {
function = printGoodBye;
}
function();
return 0;
}
- **若是我们使用一个函数指针来实现函数调用的话,这时他就成为一个“动态绑定”了,因为所调用的函数直到运行期才能确定。**第二个例子里面只有一个函数调用指令,而且等待调用的函数地址无法被硬编码在指令里面,只有在运行期的时候才会读出来;
理解OC的消息机制-动态绑定
- 对于OC中,如果向某对象传递消息,那就会使用动态绑定机制来决定需要调用的方法。在底层,所有的方法都是普通的C语言函数,然而对象收到消息之后究竟该调用那个方法则完全取决于运行期决定,甚至可以在程序运行时改变,这些特性使得OC成为一门真正的动态语言
OC如何发送消息
id returnValue = [someObject messageName: parameter];
这里的someObject叫做“接收者
”,messageName叫做“选择子”
。选择子与参数合起来称为“消息”。编译器看到此消息后,会将其转换为一条标准的C语言函数调用,所调用的函数是消息传递机制中的核心函数,叫做objc_msgSend
objc_msgSend
void objc_msgSend(id self, SEL cmd, ...);
第一个参数代表接收者,第二个参数代表选择子(SEL是选择子的类型),后边还可以再加参数。所以普遍的写法是这样的
id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);
objc_msgSend
函数会依据接收者与选择子的类型来调用适当的方法,为了完成此操作,**该方法需要在接收者所属类中搜寻其“方法列表”,如果能找到与选择子名称相符的方法,就跳至其实现代码。若是找不到,那就沿着继承体系继续向上查找,等找到合适的方法之后再跳转。**如果最终还是找不到相符的方法,那就执行“消息转发”操作。(第12条)
- 这里需要说的是,iOS的方法调用并没有上述描述的那么麻烦,在
objc_msgSend
函数会将匹配到的结果缓存到快速映射表里面,对于下一次同样的调用速度会更快
objc_msgSend
其他边界情况
- 消息调用存在很多,书上只是纤细介绍了一种而已,其他对应的函数如下
尾调用优化
- 刚才说到
objc_msgSend
函数会去检索方法,那么某个方法结果是调用另一个方法的时候,编译器不会一直向堆栈推入新的栈帧,而是会生成跳转到另一个函数的指令码
-
- 再次说明尾调用优化的出现是某个函数的最后操作仅仅是调用其他函数而不会将其返回值另作他用的时候才会触发该方法,这也是
objc_msgSend
强大之处。它完美的防治了栈溢出现象
要点总结
- 消息由接收者、选择子及参数构成。给某对象“发送消息”也就相当于在该对象上“调用方法”。
- 发给某对象的全部消息都要由“动态消息派发系统”来处理,该系统会查出对应的方法,并执行其代码
总结
- 这里学到了消息传递机制,接下来会详细学习消息转发机制