Objective-C(二)对象、消息、运行期

这是Objective-C系列的第2篇。objective-c

什么是属性

属性是Objective-C的一项特性,用于封装对象中的数据;多线程

1 实例变量

@interface Person:NSObject {
@public
    NSString *_firstName;
    NSString *_lastName;	
}
@end
复制代码

在Java或C++中,咱们能够定义实例变量的做用域。然而编写Objective-C却不多这么作。这种写法的问题是:对象布局在编译期就已经固定了。只要碰到访问_firstName变量的代码,编译器就把其替换成偏移量(offset),这个偏移量是硬编码,表示该变量距离存放对象的内存区域的起始地址有多远。框架

这样作,是不能应对增长一个实例变量这种状况带来的麻烦的,好比:ide

@interface Person:NSObject {
@public
    NSString *_dateOfBirth;
    NSString *_firstName;
    NSString *_lastName;	
}
@end
复制代码

能够看到,实例变量中在内存中的地址偏移量的改变(假设指针为4个字节)。

因此,若是代码使用编译期计算出来的偏移量,那么在修改类定义以后必须从新编译,不然就会出错。

在Objective-C中,为了应对这种问题,把实例变量当作一种存储偏移量所用的“特殊变量”,交由“类对象”保管。偏移量会在运行期查找,若是类的定义变了,那么存储的偏移量也就变了。这样,不管什么时候访问实例变量,总能使用正确的偏移量。甚至能够在运行期向类中新增实例变量。

这个问题还有一种解决方法就是——尽可能不要直接访问实例变量,而应该经过存取方法来作。虽然说属性最终仍是得经过实例变量来实现,但它却提供了一种简洁的抽象机制。

@interface Person:NSObject 
@property	NSString *firstName;
@peoperty	NSString *lastName;		
@end
复制代码

上面代码,编译器替咱们作了两件事:

  • 生成存取方法;
  • 关联实例变量;

其中,关联的实例变量,是属性名前加“_”,即firstName属性对应添加的实例变量是“_firstName”。

固然,你也能够经过@synthesize指定关联的实例变量,但通常不推荐这么作!

@implementation Person
@synthesize firstName = _myFirstName;
@end
复制代码

最后,存取方法,也能够本身实现。还有一种阻止编译器自动合成存取方法,就是使用@dynamic关键字,它告诉编译器:不要自动建立实现属性所用的实例变量的存取方法。并且,在编译访问属性的代码是,即便编译器发现没有定义存取方法,也不会报错,由于它相信能在运行期找到。

好比,从CoreData框架中的NSManagedObject类里继承给一个子类,那么就须要在运行期动态建立存取方法,由于子类的某些属性不是实例变量,其数据来自于后端的数据库。

2 属性特质

原子性

atomicnonatomic

具有atomic特质的获取方法会经过锁定机制来确保操做的原子性。若是两个线程读写同一属性,那么不论什么时候,总能看到有效的属性值。如果不加锁的话,那么当其中一个线程正在改写某属性值时,另一个线程也许会忽然闯入,把还没有修改好的属性值读取出来。

在iOS程序中,全部的属性都会声明为nonatomic。这样作的历史缘由是:在iOS中使用同步锁的开销很大(Mac OS程序中,不会遇到性能瓶颈),将会带来性能问题。

通常状况下并不要求属性必须是“原子的”,由于这并不能保证“线程安全”,若要实现线程安全,还需采用更为深层的锁定机制才行。例如,一个线程在连续屡次读取某属性值的过程当中有别的线程在同时改写该值,那么即使将属性声明为atomic,也仍是会读取到不一样的属性值。

读写权限

readwirtereadonly

内存管理语义

属性用于封装数据,而数据则要有“具体的全部权语义”。

  • assign:只会执行针对“纯量类型”的简单赋值操做;
  • strong:此特质代表一种“拥有关系”,为这种属性设置新值时,设置方法会先保留新值,并释放旧值,而后再将新值设置上去;
  • weak:此特质代表该属性定义了一种“非拥有关系”。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。与assign相似,而后在属性所指的对象遭到摧毁时,属性值会被清空(nil out)。
  • unsafe_unretained:此特质的语义和assign相似,可是它适用于“对象类型”,表达一种“非拥有关系”。当目标对象遭到摧毁是,属性值不会被清空。
  • copy:此特质所表达的所属关系和strong相似。然而设置方法并不保留新值,而是将其拷贝。

在对象内部尽可能直接访问实例变量

经过点语法与直接访问内部实例变量的区别在于:

  • 因为不通过Objective-C的“方法派发”(method dispatch)步骤,因此直接访问实例变量的速度会比较快在这种状况下,编译器所生成的代码会直接访问对象实例变量的那块内存;
  • 直接访问实例变量,不会调用其“设置方法”,这就绕过了为相关属性所定义的内存管理语义。好比,在ARC下,直接访问一个声明为copy的属性,那么并不会拷贝该属性,只会保留新值并释放旧值;
  • 若是直接访问实例变量,那么不会触发“键值观察(key-value observing)”通知,这样作是否会产生问题,取决于具体的对象行为;
  • 经过属性来访问有助于排查与之相关的错误,由于能够在“获取方法”与“设置方法”中打断点,监控该属性的调用者及其访问时机;

因此,合理的方案是:写入实例变量时,经过其“设置方法”来作,而在读取实例变量时,则直接访问之

读取直接访问,是为了提升访问速度,而写入则为了保留内存管理语义。

这样作须要注意下面两点:

  1. 在初始化方法中应该如何设置属性值?
  2. 惰性初始化

理解“对象等同性”

根据等同性(equality)来比较对象是一个很是有用的功能。不过按照==操做符比较出来的结果未必是咱们想要的,由于该操做符比较的是两个指针自己,而不是其所指的对象。应该使用NSObject协议中声明的isEqual:方法来判断两个对象的等同性。某些对象还提供了特别的等同性判断方法,好比NSStirng类提供的isEqualToString:

NSString *foo = @"Badger 123";
NSString *bar = [NSString stringWithFormat:@"Badger %i",123];

BOOL equalA = (foo == bar);					//NO
BOOL equalB = [foo isEqual:bar];			//YES
BOOL equalC = [foo isEqualToString:bar];	//YES
复制代码

针对NSString能够看出:

  • == :比较两个对象的指针是否一致;
  • isEqaual: :比较两个对象的字符串是否一致;

isEqualToString方法在这里和isEqual是等效的,可是isEqualToString更快,由于后者因为不知道受测对象的类型,还须要执行额外的步骤。

NSObject协议中有两个用于判断等同性的关键方法:

- (BOOL)isEqual:(id)object;
@property (readonly) NSUInteger hash;	
复制代码

NSObject对这两个方法的默认实现是:当前仅当其“指针值”彻底相等时,这两个对象才相等。若想在自定义的对象中重写这些方法,就必须理解其约定;

若是isEqual:方法判断两个对象相等,那么其hash值一定相等。

反过来,若是其hash值相等,那么isEqual:方法未必会认为二者相等。

下面有个类,咱们认为其全部字段相等,那么这两个对象就相等。重写其isEqual:以下:

@interface JSDog : NSObject
@property (nonatomic ,assign)NSInteger          age;
@property (nonatomic ,copy)NSString             *name;
@end
复制代码

@implementation JSDog中:

- (BOOL)isEqual:(id)object
{
    if (self == object) {
        return YES;
    }
    
    if ([self class] != [object class]) {
        return NO;
    }
    
    JSDog *otherDog = (JSDog*)object;
    if (_age != otherDog.age) {
        return NO;
    }
    
    if (![_name isEqualToString:otherDog.name]) {
        return NO;
    }
    
    return YES;
}
复制代码

这是一种比较典型的写法,分析一下:

  • 首先判断两个指针是否一致,若是一致,毫无疑问,两个对象是相等的;
  • 其次,判断两个类是否一致,假如类不一致,那么便不相等,可是此处须要注意的是,在业务中须要继承体系上认为相等的,好比JSDog实例能够JSAnimal(父类)相等的状况要考虑,即继承体系上的父子关系是否相等须要业务来作决定。
  • 最后,判断各个字段是否相等,只有所有相等两个对象才算相等,不然,就不相等。

接下来,看一下hash方法,首先,假如咱们认为isEqual:相等了,那么hash必相等,咱们简单的能够以下:

-(NSUInteger)hash
{
    return 12345;
}
复制代码

这么写,一看就知道有问题,由于无论什么状况,都返回相等的hash值。那么是什么问题呢?

在collection中检索对象是依靠哈希表(hash table)时,会用对象的哈希码作索引。假如某个collection使用set来实现的,那么set可能会根据哈希码将对象分装到不一样的数组(也成为箱子)中。在向set中添加新对象时,要根据其哈希码找到与之相关的那个数组,依次检查其中各个元素,判断其是否相等。若是相等,就说明要添加的对象已经在set里面了。

问题来了,假如哈希码都同样,咱们不是要每依次判断对象是否相等,假如如今数组中已经有10000个对象,那么我再加入一个对象时,因为哈希码一致,咱们要作10000次的对象是否相等的判断,效率低下,性能堪忧。因此,咱们要改进咱们的hash方法,至少要根据不一样的属性返回不一样的hash值,下面是改进的版本:

-(NSUInteger)hash
{
    NSString *stringToHash = [NSString stringWithFormat:@"%@:%ld",_name,(long)_age];
    return [stringToHash hash];
}
复制代码

上面的hash方法能够根据不一样的属性返回不一样的hash值,可是该hash方法,仍然要负担建立字符串的开销,因此比返回单一值要慢,因为计算哈希码的开销过大,也许在collection中仍然会出现性能问题。

-(NSUInteger)hash
{
    NSUInteger ageHash = _age;
    NSUInteger nameHash = [_name hash];
    
    return ageHash ^ nameHash;
}
复制代码

这里,避免了建立字符串的开销,又能使生成的哈希码至少位于必定的范围内,而不会过于频繁的重复。固然,这种算法生成的哈希码避免不了彻底不碰撞。因此在设计哈希算法是要在碰撞频度与下降运算复杂程度之间取舍。

1. 特定类所具备的等同性断定方法

除了NSString中,具备isEqualToString:这种特定类的断定方法,NSArray具备isEqualToArray:,以及NSDictionary具备isEqualToDictionary:

本身来写特定类断定方法时:

- (BOOL)isEqual:(id)object
{
    if ([self class] == [object class]) {
       return [self isEqualToDog:(JSDog *)object];
    }else{
        return [super isEqual:object];
    }
}

- (BOOL)isEqualToDog:(JSDog*)otherDog
{
    if (self == otherDog) {
        return YES;
    }
    
    if ([self class] != [otherDog class]) {
        return NO;
    }
    
    if (_age != otherDog.age) {
        return NO;
    }
    
    if (![_name isEqualToString:otherDog.name]) {
        return NO;
    }
    
    return YES;
}
复制代码

2. 等同性断定的执行深度

建立等同性断定方法时,须要决定是根据整个对象来判断等同性,仍是仅根据其中几个字段来判断。

NSArray的检测方式是先看两个数组所含对象个数是否一致,若相同,则在每一个对应位置的两个对象身上调用“isEqual:”方法。若是对应位置上的对象都相等,那么则两个数组相等,这叫作“深度等同性断定”。

不过,有时候咱们仍然能够根据业务来将深度维度降下来,只根据其中某一个属性来断定。好比JSDog存储在数据库表中有个identifier的惟一标识符,假如此标识符相同,咱们就认为这是同一条🐶,无需多作其余判断,这种降维的工做通常由业务驱动,而不是凭空构想的。

3. 容器中可变类的等同性

看一个实例:

NSMutableSet *set = [NSMutableSet set];
//1.
NSMutableArray *arrayA = [@[@1,@2] mutableCopy];
[set addObject:arrayA];
NSLog(@"set = %@",set);

//2.
NSMutableArray *arrayB = [@[@1,@2] mutableCopy];
[set addObject:arrayB];
NSLog(@"set = %@",set);

//3.
NSMutableArray *arrayC = [@[@1] mutableCopy];
[set addObject:arrayC];
NSLog(@"set = %@",set);

//4.
[arrayC addObject:@2];
NSLog(@"set = %@",set);

//5.
NSSet *setB = [set copy];
NSLog(@"setB = %@",setB);
复制代码

打印出来的log:

set = {(         (1,2) )}
set = {(         (1,2) )}
set = {( (1),    (1,2) )}
set = {( (1,2),  (1,2) )}
setB = {(         (1,2) )}
复制代码

第一步,添加arrayA是正常的,第二步添加arrayB,因为set中arrayA与arrayB相等,因此set仍然只有一个对象也是正常的。第三步,添加了一个set中没有的arrayC,正常的。

第四步,将arrayC中数组,添加一个元素2,使得arrayC此时与arrayA相等了,可奇怪的是,竟然同时存在于set中;

第五步,将此时包含了两个相等的数组arrayA,arrayC的set进行copy,获得setB,又发生了奇怪的事,两个相同的数组只剩下一个。

把某个对象放入了collection中,就不该该改变其哈希码,像上面的状况就是在讲arrayC加入到set后,又更改了arrayC的哈希码。collection会根据哈希码将不一样的对象放入到不一样的“箱子数组”中,若是某对象在放入“箱子”以后,哈希码改变,那么如今所处的这个箱子对它来讲就是错误的。

因此,要么确保哈希码不是根据对象的“可变部分”计算出来的,要么保证放入collection中的对象是再也不可变的。

  • 若想检测对象的等同性,请提供isEqual:hash方法;
  • 相同的对象必须有相同的hash码,可是hash码相同的对象却未必相同;
  • 不要盲目地检测每一个属性,而是应该按照具体需求来制定检测方案;
  • 编写hash方法时,应该使用计算速度快并且哈希码碰撞率低的算法。

new与alloc/init

  • new 实际调用alloc/init方法,等效;
  • new 不支持自定义init方法;
  • alloc-init 更清晰;

@synchronized

指令@synchronized()经过对一段代码的使用进行加锁。其余试图执行该段代码的线程都会被阻塞,直到加锁线程退出执行该段被保护的代码段,也就是说@synchronized()代码块中的最后一条语句已经被执行完毕的时候。 通常在公用变量的时候使用,如单例模式或者操做类的static变量中使用。

指令@synchronized()须要一个参数。该参数可使任何的Objective-C对象,包括self。这个对象就是互斥信号量。他可以让一个线程对一段代码进行保护,避免别的线程执行该段代码。针对程序中的不一样的关键代码段,咱们应该分别使用不一样的信号量。只有在应用程序编程执行多线程以前就建立好全部须要的互斥信号量对象来避免线程间的竞争才是最安全的。

相关文章
相关标签/搜索