《Effective Objective-C 2.0》读书笔记---第二章

前言:

当应用程序运行起来以后,为其提供相关支持的代码叫做“Objective-C运行期环境”(Objective-C runtime),它提供了一些使得对象之间能够传递消息的重要函数,并且包含创建类实例所用的全部逻辑。


第六条:理解“属性”这一概念


Java 或 C++ 中,可以使用publish,private等关键字来定义实例变量的作用域,OC也可以,但是很少这么做,这种写法的问题是:对象布局在编译器(compile time)就已经固定了。碰到访问实例变量的代码,编译器就把它替换为“偏移量”(offset)。这个偏移量是硬编码(hardcode),表示该变量距离存放对象的内存区域的起始地址有多远。如果新增了实例变量,就需要重新编译,否则原来的偏移量会指向错误的地址。


OC的应对方法,就是把实例变量当作一种存储偏移量所用的“特殊变量”,交由“类对象”保管。偏移量会在运行期查找,如果类的定义改变了,那么存储的偏移量也就变了。这样的话,无论何时访问实例变量,总能使用正确的偏移量。甚至还可以在运行期间向类中新添实例变量。这就是稳固的“应用程序二进制接口”(Application Binary Interface,ABI)。


编译器会把“点语法”转换为对存取方法的调用,使用“点语法”和直接调用存取方法之间没有丝毫车别,除了生成方法代码之外,编译器还要自动向类中添加相应的实例变量,并且在属性名前加下划线作为实例变量的名字。也可以在类的实现代码里通过@synthesize语法来指定实例变量的名字。


如果不想让编译器自动合成存取方法,可以使用@dynamic关键字,它告诉编译器:不要自动创建实现属性所用的实例变量,也不要为其创建存取方法.


如果属性不具备nonatomic特质,那它就是“原子的”(atomic)。


若是自己来实现这些存储方法,或者在实现自定义的初始化方法时,那么应该保证其具备相关属性所声明的特质。

例如声明特性为copy,则要在赋值的时候使用copy。


尽量使用不可变的对象,类的属性都应该设为“只读”,用初始化方法设置好属性之后,就不能再改变了。


决不应该在init方法(或者dealloc方法)中使用存取方法;


开发iOS程序时应该使用nonatomic属性,因为atomic属性会严重影响性能。


原子性只能保证读和写的原子性,也就是,当一个线程正在改写某属性值时,另外一个线程没法操作属性,但是原子性不能保证正确读写的时机,例如,一个线程在连续多次读取某属性值得过程中有别的线程在同时改写该值,那么即便将属性声明为atomic,也还是会读到不同的属性值。这个就得开发者自己通过锁来实现。


第七条:在对象内部尽量直接访问实例变量


不通过点方法访问实例变量,不会经过OC的“方法派发”步骤,所以直接访问实例变量的速度比较快,编译器所生成的代码会直接访问保存对象实例变量的那块内存。


直接访问实例变量时,不会调用其“设置方法”,这就绕过了为相关属性所定义的“内存管理语义”,比方说,如果在ARC下直接访问一个声明为copy的属性,那么并不会拷贝该属性,只会保留新值并释放旧值。


如果直接访问实例变量,那么不会触发KVO通知。


第八条:理解“对象等同性”这一概念


应该使用NSObject协议中声明的“isEqual:”方法来判断两个对象的等同性。


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

- (BOOL)isEqual:(id)object;
- (NSUInteger)hash;

isEqual:在基类中默认实现是

- (BOOL)isEqual:(id)obj {
    return obj == self;
}

判断指针是否相等,因此如果想实现自己的判断就要重写这个方法来覆盖掉基类中的实现

hash方法默认实现为return (uintptr_t)obj;是返回对象的地址的


如果“isEqual:”方法判定两个对象相等,那么其hash方法也必须返回同一个值,但是,如果两个对象的hash方法返回同一个值,那么“isEqual:”方法未必会认为两者相等。

根据这个约定,下面来实现这两个方法

isEqual:实现:首先判断两个指针是否相等,相等返回YES,否则继续判断。。。


接下来实现hash,如果我们直接返回一个固定的数值,如return 1337;是满足约定的,但是这样会有性能问题,不如在set集合中,set会根据哈希码把对象分装到不同的数组中,在向set中添加新对象时,要根据其哈希码找到与之相关的那个数组,依次检查其中各个元素,看数组中已有的对象是否和将要添加的新对象相等,如果相等,那就说明要添加的对象已经在set里面了。由此可知,如果令每个对象都返回相同的哈希码,那么在set中已有1000000个对象的情况下,若是继续向其中添加对象,则需将这1000000个对象全部扫描一遍。


以下是一个计算哈希码的好的方法,编写hash方法时,应该使用计算速度快而且哈希码碰撞几率低的算法。

- (NSUInteger)hash
{
    NSUInteger firstNameHash = [_firstName hash];
    NSUInteger lastNameHash = [_lastName hash];
    NSUInteger ageHash = _age;
    return firstNameHash^lastNameHash^ageHash;
}

把某个对象放入集合之后,就不应该再改变其哈希码了,因为集合会把各个对象按照其哈希码分装到不同的“箱子数组”中,如果某对象在放入“箱子”之后哈希码又变了,那么其现在所处的这个箱子对它来说就是“错误”的。解决这个问题,就要确保哈希码不是根据对象的“可变部分”计算出来的,或者保证放入集合以后就不再改变对象内容了。


扩展:

hash方法什么时候会调用,下面列举几种情况

往set集合中添加对象的时候

1.如果isEqual返回YES,并且hash也相等,那么不管对象的属性是不是相等的,都会认为对象是等同的,只会添加一个对象。

2.如果isEqual返回YES,并且不实现hash方法(默认返回对象地址),那么不管对象的属性是不是相等的,也会认为对象不是等同的,会添加两个对象。


作为字典的键时

如果isEqual返回YES,并且hash也相等,作为键向字典添加对象时,那么不管对象的属性是不是相等的,只会添加第一个,如下:

 NSDictionary *dictionary = @{obj1 : @"A",obj2 : @"B"};
    NSLog(@"dictionary = %@",dictionary);

打印信息:

dictionary = {

    "<MyObject: 0x600000008600>" = A;

}

之前我以为不去实现hash,返回对象的地址就永远不会发生哈希码碰撞,这样不是很好,但是有个问题,就是上面往set集合中添加对象的时候的第二种情况,用户实际只想添加相同的内容的一个对象,可以两个对象虽然内容相同,但是hash不一样,就会重复添加了。


第九条:以“类族模式”隐藏实现细节


OC的系统框架中普遍使用此模式,例如UIKit框架的UIButton类的类方法:

+ (instancetype)buttonWithType:(UIButtonType)buttonType;

该方法返回的对象,其类型取决于传入的按钮类型参数,然而,不管返回什么类型的对象,它们都继承自同一个基类:UIButton。


这样实现的意义是:隐藏子类和实现细节,保持接口简洁。


这其实就是设计模式中的”工厂方法模式“


在查询其类型信息时就要注意,你可能觉得自己创建了某个类的实例,然而实际上创建的却是其子类的实例。


例如我们调用isMemberOfClass方法时,就会返回NO。


Cocoa中大部分collection类都是类族,例如NSArray和NSMutableArray,我们使用的集合类都是这个类族中的抽象基类。


在使用NSArray的alloc方法来获取实例时,该方法首先会分配一个属于某类的实例,此实例充当”占位数组“,该数组稍后会转为另一个类的实例,而那个类则是NSArray的实体子类。如下:

NSArray *array = [[NSArray alloc]init];
    if ([array class] == [NSArray class]) {
        NSLog(@"array is type of NSArray");
    }
    if ([array isMemberOfClass:[NSArray class]]) {
        NSLog(@"array is instance of NSArray");
    }

可以看到打印信息是空。

不过,我们可以通过isKindOfClass方法判断出某个实例所属的类是否位于类族之中。


没有”工厂方法“的源代码,我们要怎么向NSArray类族中新增子类呢,需要遵守以下规则:

1.子类应该继承自类族中的抽象基类。

2.子类应该定义自己的数据存储方式。

NSArray子类必须用一个实例变量来存放数组中的对象。

3.子类应该覆写超类文档中指明需要覆写的方法。

NSArray子类必须实现count和objectAtIndex方法


我们创建一个MyMutableArray类继承自NSMutableArray,然后调用以下方法

MyMutableArray *mArray = [[MyMutableArray alloc]init];
    [mArray addObject:@"obj"];
    NSLog(@"mArray = %@",mArray);

会发生如下崩溃:

 *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[NSArray count]: method only defined for abstract class.  Define -[MyMutableArray count]!'


第十条:在既有类中使用关联对象存放自定义数据

参考iOS runtime方法详解之对象关联


第十一条:理解objc_msgSend的作用

参考iOS runtime方法详解之消息


第十二条:理解消息转发机制

参考iOS runtime方法详解之消息


第十三条:用“方法调配技术”调试“黑盒方法”


OC对象收到消息之后,究竟会调用何种方法需要在运行期才能解析出来,那么,与给定的选择子名称对应的方法是不是可以在运行期改变呢?没错,就是这样。我们既不需要源代码,也不需要通过继承子类来覆写方法就能改变这个类本身的功能。这样一来,新功能将在本类的所有实例中生效,而不是仅限于覆写了相关方法的那些子类实例。此方案经常称为“方法调配”(method swizzling)。


下面看一下如何互换两个方法实现。想交换方法实现,可用下列函数:

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);

这样交换两个方法实现意义不大,但是可以通过这一手段来为既有的方法实现添加新功能,如下:

#import "UIViewController+AddLog.h"
#include <objc/runtime.h>

@implementation UIViewController(AddLog)

+ (void)load
{
    Method originalMethod = class_getInstanceMethod([UIViewController class],@selector(viewWillAppear:));
    Method swappedMethod = class_getInstanceMethod([UIViewController class],@selector(AddLog_viewWillAppear:));
    method_exchangeImplementations(originalMethod,swappedMethod);
}

- (void)AddLog_viewWillAppear:(BOOL)animated
{
    [self AddLog_viewWillAppear:animated];
    NSLog(@"===%@===",NSStringFromClass([self class]));
}

@end

不需要引如头文件就可以替换所有UIViewController的viewWillAppear方法。

这种用法仅仅在调试程序的时候使用,正式环境切记不要滥用。


扩展:


因为对于加入运行期系统中的每个类(class)及分类(category)来说,必定会调用此方法,而且仅调用一次。当包含类或分类的程序库载入系统时,就会执行此方法。如果分类和其所属的类都定义了load方法,则先调用类里的,再调用分类里的。


load方法是在main调用之前调用的,此时viewWillAppear相关信息已经被加载了,这也就验证了之前说的Objective-C动态运行库会自动注册我们代码中定义的所有的类,类的信息实际上是在main函数调用之前就已经加载到内存中。


这些事务琐碎,跟主要业务逻辑无关,在很多地方都有,又很难抽象出来单独的模块。这种程序设计问题,业界也给了他们一个名字 -Cross Cutting Concerns


而像上面例子用 Method Swizzling 动态给指定的方法添加代码,以解决 Cross Cutting Concerns 的编程方式叫:Aspect Oriented Programming


这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程(AOP)。


第十四条:理解“类对象”的用意


说明:写这个笔记的时候类的底层实现已经发生变化了,但是这里还是依照老版本也就是书中所说的内容来讲解。


我们都知道NSObject的定义如下;

@interface NSObject <NSObject> {
    Class isa ;
}

我们再来看看运行时头文件中的定义如下

struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};
typedef struct objc_object *id;

可见,每个对象结构体的首个成员是Class类型的变量。该变量定义了对象所属的类,通常称为”is a“指针。

下面是Class的定义

typedef struct objc_class *Class;

struct objc_class {

    Class isa  OBJC_ISA_AVAILABILITY;
    Class super_class;
    const char *name;
    long version;
    long info;
    long instance_size;
    struct objc_ivar_list *ivars;
    struct objc_method_list **methodLists;
    struct objc_cache *cache;
    struct objc_protocol_list *protocols;

};

此结构体存放类的”元数据“(metadata),例如类的实例实现了几个方法,具备多少个实例变量等信息,此结构体的首个变量也是isa指针,这说明Class本身也是OC对象。结构体里还有个变量叫做super_class,它定义了本类的超类。类对象所属的类型(也就是isa指针所指的类型)是另外一个类,叫做”元类“(metaclass),原来表述类对象本身所具备的元数据。”类方法“就定义于此处,因为这些方法可以理解成类对象的实例方法。每个类仅有一个”类对象“,而每个”类对象“仅有一个与之相关的”元类“。



Objective-C的设计者让所有的meta-class的isa指向基类的meta-class,而基类的meta-class的isa指针是指向它自己。这样就形成了一个完美的闭环。

super_class指针确立了继承关系,而isa指针描述了实例所属的类。


在类继承体系中查询类型信息


isMemberOfClass能够判断出对象是否为某个特定类的实例 isKindOfClass则能判断出对象是否为某类或其派生类的实例


像这样的类型信息查询方法使用isa指针获取对象所属的类,然后通过super_class指针在继承体系中游走。


“在运行期检视对象类型”这一操作也叫做”类型信息查询“(introspection,”内省”),这个强大而有用的特性内置于Foundation框架的NSObject协议里,凡是由公共根类(common class,即NSObject与NSProxy)继承而来的对象都要遵从此协议。在程序中不要直接比较对象所属的类,明智的做法是调用”类型信息查询方法“,原因如下:

@interface TargetProxy : NSProxy

- (void)uppercaseString;  

@end

@implementation TargetProxy

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    SEL name = [anInvocation selector];
    NSLog(@"%s %@",__FUNCTION__, NSStringFromSelector(name));
    
    NSString * proxy = [[NSString alloc] init];
    if ([proxy respondsToSelector:name]) {
        [anInvocation invokeWithTarget:proxy];
    }
    else {
        [super forwardInvocation:anInvocation];
    }
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"%s",__FUNCTION__);
    return [NSString instanceMethodSignatureForSelector:aSelector];
}

@end

运行

TargetProxy *object = [TargetProxy alloc] ;
NSLog(@"object is instance of %@",[object class]);
    if ([object isKindOfClass:[NSString class]]) {
        NSLog(@"object is instance of NSString");
    }

打印信息:

object is instance of TargetProxy

object is instance of NSString


如果在代理对象上调用class,则返回代理本身的类型,而不是接受的代理的对象的类型。


猜你喜欢

转载自blog.csdn.net/junjun150013652/article/details/53744873