《Effective Objective-C 2.0》读书笔记 一

第1章:熟悉Objective-C

通论该语言的核心概念

第1条:了解 Objective-C 语言的起源

  • Objective-C从Smalltalk语言是从Smalltalk语言演化而来,
    Smalltalk是消息语言的鼻祖
  • Objective-C为C语言添加了面向对象特性,是其超集。Objective-C使用动态绑定的消息结构,也就是说,在运行时才会检查对象类型。接收- -条消息之后,究竟应执行何种代码,由运行期环境而非编译器来决定。
  • 消息结构与函数调用的关键区别在于:函数调用的语言,在编译阶段由编译器生成一些虚方法表,在运行时从这个表找到所要执行的方法去执行。而使用了动态绑定的消息结构在运行时接到一条消息,接下来要执行什么代码是运行期决定的,而不是编译器。
NSString *someString = @"Hello World";
NSString *anotherString = @"Hello World";
NSLog(@"someString:%p --- anotherString:%p", someString, anotherString);

印结果:

someString:0x1000102e8 --- anotherString:0x1000102e8

两个变量为指向同一块内存的相同指针。此时将 anotherString 赋值为 “Hello World!!!”

 NSString *anotherString = @"Hello World!!!";
 NSLog(@"someString:%p --- anotherString:%p", someString, anotherString);

打印结果:

someString:0x1000102e8 --- anotherString:0x100010308

此时,两者变为不同的内存地址。所以,对象的本质是指向某一块内存区域的指针,指针的存储位置取决于对象声明的区域和有无成员变量指向。若在方法内部声明的对象,内存会分配到栈中,随着栈帧弹出而被自动清理;若对象为成员变量,内存则分配在堆区,声明周期需要程序员管理。

第2条:在类的头文件中尽量少引入其他头文件

  • 除非确有必要,否则不要引入头文件,一般来说,应该在某个类的头文件中使用向前声明来提及别的类,并在实现文件中引入那些类的头文件。这样做可以尽量降低类之间的耦合。
  • 有时无法使用向前声明,比如要声明某个类遵循一项协议,尽量把“该类遵循某协议” 的这条声明移至“class-continuation 分类中”。如果不行的话,就把协议单独放在某一个头文件中,然后将其引入。
//Student.h
@class Book; //向前引用,避免在 .h 里导入其他文件
@interface Student : NSObject
@property (nonatomic, strong) BOOK *book;
@end

//student.m
#import "Book.h"
@implementation Student
- (void)readBook {
    NSLog(@"read the book name is %@",self.book);
}
@end

这样做有什么优点呢:

  • 不在A的头文件中引入B的头文件,就不会一并引入B的全部内容,这样就减少了编译时间。
  • 可以避免循环引用:因为如果两个类在自己的头文件中都引入了对方的头文件,那么就会导致其中一个类无法被正确编译。

但是个别的时候,必须在头文件中引入其他类的头文件:

  1. 该类继承于某个类,则应该引入父类的头文件。
  2. 该类遵从某个协议,则应该引入该协议的头文件。而且最好将协议单独放在一个头文件中

第3条:多用字面量语法,少用与之等价的方法

  • 应该使用字面量语法来创建字符串、数值、数组、字典。与创建此类对象的常规方法相比,这么做更加简明扼要。
  • 应该通过取下标操作来访问数组下标或字典中的键所对应的元素。
  • 用字面量语法创建数组或字典,若值中有 nil ,则会抛出异常。因此,务必确保值里不含 nil。
  1. 声明时的字面量语法:
    在声明NSNumberNSArrayNSDictionary时,应该尽量使用简洁字面量语法。
NSNumber *intNumber = @1;
NSNumber *floatNumber = @2.5f;

NSArray *animals =[NSArray arrayWithObjects:@"cat", @"dog",@"mouse", @"badger", nil];
Dictionary *dict = @{@"animal":@"tiger",@"phone":@"iPhone 6"};
  1. 集合类取下标的字面量语法:

NSArray,NSDictionary,NSMutableArray,NSMutableDictionary 的取下标操作也应该尽量使用字面量语法。

NSString *cat = animals[0];
NSString *iphone = dict[@"phone"];

字面语法的局限性:

  • 字面量语法所创建的对象必须属于 Foundation 框架,自定义类无法使用字面量语法创建。

  • 使用字面量语法创建的对象只能是不可变的。若希望其变为可变类型,可将其深复制一份

NSMutableArray *arrayM = [@[@1,@"123",@"567"] mutableCopy];

第4条:多用类型常量,少用 #define 预处理指令

  • 不要用预处理指令定义常量。这样定义的常量不含类型信息,编译器只是会在编译前据此执行查找与替换操作。即使有人重新定义了常量值,编译器也不会产生警告信息⚠️,这将导致应用程序中的常量值不一致。
  • 在实现文件中使用 static const 来定义“只在编译单元内可见的常量”。由于此类常量不在全局符号表中,所以无需为其名称加前缀。
  • 在头文件中使用 extern 来声明全局常量,并在相关实现文件中定义其值。这种常量要出现在全局符号表中,所以名称应该加以区隔,通常用与之相关的类名做前缀。

在OC中,定义常量通常使用预处理命令,但是并不建议使用它,而是使用类型常量的方法。
两种方法的区别:

  • 预处理命令:简单的文本替换,不包括类型信息,并且可被任意修改。
  • 类型常量:包括类型信息,并且可以设置其使用范围,而且不可被修改。

我们可以看出来,使用预处理虽然能达到替换文本的目的,但是本身还是有局限性的:不具备类型 + 可以被任意修改,总之给人一种不安全的感觉。
知道了它们的长短处,我们再来简单看一下它们的具体使用方法:

预处理命令:

#define W_LABEL (W_SCREEN - 2*GAP)

这里,(W_SCREEN - 2*GAP)替换了W_LABEL,它不具备W_LABEL的类型信息。而且要注意一下:如果替换式中存在运算符号,最好用括号括起来。

类型常量:

static NSString* const kEnableGestureRecognizer = @"EnableGestureRecognizer";

这里:
const 将其设置为常量,不可更改。
static意味着该变量仅仅在定义此变量的编译单元中可见。如果不声明static,编译器会为它创建一个外部符号(external symbol)。我们来看一下对外公开的常量的声明方法:

注意:const修饰符在常量类型中的位置。常量定义应从右至左解读,所以在本例中,kEnableGestureRecognizer 就是“ 一个常量,而这个常量是指针,指向NSString对象”。这与需求相符:我们不希望有人改变此指针常量,使其指向另-个NSString对象。

对外公开某个常量:

如果我们需要发送通知,那么就需要在不同的地方拿到通知的“频道”字符串,那么显然这个字符串是不能被轻易更改,而且可以在不同的地方获取。这个时候就需要定义一个外界可见的字符串常量。

//header file
extern NSString *const NotificationString;

//implementation file
NSString *const  NotificationString = @"Finish Download";

我们通常在头文件声明常量,在其实现文件里定义该常量。由实现文件生成目标文件时,编译器会在“数据段”为字符串分配存储空间。
最后注意一下公开和非公开的常量的命名规范:

  • 公开的常量:常量的名字最好用与之相关的类名做前缀。
  • 非公开的常量:局限于某个编译单元(tanslation unit,实现文件 implementation file)内,在签名加上字母k。

第5条:用枚举表示状态、选项、状态码

  • 应该用枚举来表示状态机的状态、传递给方法的选项以及状态码等值,给这些值起个易懂的名字。
  • 如果把传递给某个方法的选项表示为枚举类型,而多个选项又可以同时使用,那么就将各选项定义为2的幂,以便通过按位或操作将其组合起来。
  • 用 NS_ENUUM 与 NS_OPTIONS 宏来定义枚举类型,并指明其底层数据类型。这样做可以确保枚举是用开发者所选的底层数据类型实现出来的,而不会采用编译器所选类型。
  • 在处理枚举类型的switch语句中不要实现 default分支。这样的话,加入新枚举之后,编译器就会提示开发者:switch 语句并未处理所以枚举。
/// 位移枚举
typedef NS_OPTIONS(NSUInteger, Direction) {
    DirectionTop          = 0,
    DirectionBottom       = 1 << 0,
    DirectionLeft         = 1 << 1,
    DirectionRight        = 1 << 2,
};

/// 常量枚举
typedef NS_ENUM(NSInteger,ShowType){
    ShowTypeForce,
    ShowTypeNormal
};

第2章:对象、消息、运行时

对象之间能够关联与交互,这是面向对象语言的重要特征。本章讲述这些特征,并深人研究代码在运行期的行为。

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

  • 可以用 @property 语法来定义对象中所封装的数据。
  • 通过“特质”来指定存储数据所需的正确语义。
  • 在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义。
  • 开发iOS程序时应该使用nonatomic属性,因为atomic属性会严重影响性能。
  1. 存取方法
    在设置完属性后,编译器会自动写出一套存取方法,用于访问相应名称的变量:
@interface EOCPerson : NSObject

@property NSString *firstName;
@property NSString *lastName;
@end


@interface EOCPerson : NSObject

- (NSString*)firstName;
- (void)setFirstName:(NSString*)firstName;
- (NSString*)lastName;
- (void)setLastName:(NSString*)lastName;

@end

访问属性,可以使用点语法。编译器会把点语法转换为对存取方法的调用:

aPerson.firstName = @"Bob"; // Same as:
[aPerson setFirstName:@"Bob"];


NSString *lastName = aPerson.lastName; // Same as:
NSString *lastName = [aPerson lastName];

如果我们不希望编译器自动生成存取方法的话,需要设置@dynamic 字段:

@interface EOCPerson : NSManagedObject

@property NSString *firstName;
@property NSString *lastName;

@end


@implementation EOCPerson
@dynamic firstName, lastName;
@end
  1. 属相特质

定义属性的时候,通常会赋予它一些特性,来满足一些对类保存数据所要遵循的需求。

原子性:

  • nonatomic:不使用同步锁
  • atomic:加同步锁,确保其原子性

读写

  • readwrite:同时存在存取方法
  • readonly:只有获取方法

内存管理

  • assign:纯量类型(scalar type)的简单赋值操作
  • strong:拥有关系保留新值,释放旧值,再设置新值
  • weak:非拥有关系(nonowning relationship),属性所指的对象遭到摧毁时,属性也会清空
  • unsafe_unretained :类似assign,适用于对象类型,非拥有关系,属性所指的对象遭到摧毁时,属性不会清空。
  • copy:不保留新值,而是将其拷贝

注意:遵循属性定义

如果属性定义为copy,那么在非设置方法里设定属性的时候,也要遵循copy的语义

- (id)initWithFirstName:(NSString*)firstName lastName:(NSString*)lastName
{
         if ((self = [super init])) {
            _firstName = [firstName copy];
            _lastName = [lastName copy];
        }
       return self;
}

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

关于实例变量的访问,可以直接访问,也可以通过属性的方式(点语法)来访问。书中作者建议在读取实例变量时采用直接访问的形式,而在设置实例变量的时候通过属性来做。

直接访问属性的特点:

绕过set,get语义,速度快;

通过属性访问属性的特点:

不会绕过属性定义的内存管理语义
有助于打断点排查错误
可以触发KVO

因此,有个关于折中的方案:

设置属性:通过属性
读取属性:直接访问

不过有两个特例:

  • 初始化方法和dealloc方法中,需要直接访问实例变量来进行设置属性操作。因为如果在这里没有绕过set方法,就有可能触发其他不必要的操作。
  • 惰性初始化(lazy initialization)的属性,必须通过属性来读取数据。因为惰性初始化是通过重写get方法来初始化实例变量的,如果不通过属性来读取该实例变量,那么这个实例变量就永远不会被初始化。

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

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

1. 同等性判断

==操作符比较的是指针值,也就是内存地址。

然而有的时候我们只是想比较指针所指向的内容,在这个时候,就需要通过isEqual:方法来比较。
而且,如果已知两个对象是字符串,最好通过isEqualToString:方法来比较。
对于数组和字典,也有isEqualToArray:方法和isEqualToDictionary:方法。
另外,如果比较的对象类型和当前对象类型相同,就可以采用自己编写的判定方法,否则调用父类的isEqual方法:

- (BOOL)isEqualToPerson:(EOCPerson*)otherPerson {

     //先比较对象类型,然后比较每个属性
     if (self == object) return YES;
     if (![_firstName isEqualToString:otherPerson.firstName])
         return NO;
     if (![_lastName isEqualToString:otherPerson.lastName])
         return NO;
     if (_age != otherPerson.age)
         return NO;
     return YES;
}


- (BOOL)isEqual:(id)object {
    //如果对象所属类型相同,就调用自己编写的判定方法,如果不同,调用父类的isEqual:方法
     if ([self class] == [object class]) {    
         return [self isEqualToPerson:(EOCPerson*)object];
    } else {    
         return [super isEqual:object];
    }
}

2. 深度等同性判定

比较两个数组是否相等的话可以使用深度同等性判断方法:

1.先比较数组的个数
2.再比较两个数组对应位置上的对象均相等。

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

  • 类族模式可以把实现细节隐藏在一套简单的公共接口后面。
  • 系统框架中经常使用类族。
  • 从类族的公共抽象基类中继承子类时要当心,若有开发文档,则应首先阅读。

在iOS开发中,我们也会使用“类族”(class cluster)这一设计模式,通过“抽象基类”来实例化不同的实体子类。例如

+ (UIButton *)buttonWithType:(UIButtonType)type;

该方法所返回的对象,其类型取决于传人的按钮类型( button type)。然而,不管返回什么类型的对象,它们都继承自同-一个基类: UIButton。 这么做的意义在于: UIButton 类的使用者无须关心创建出来的按钮具体属于哪个子类,也不用考虑按钮的绘制方式等实现细节。使用者只需明白如何创建按钮,如何设置像“标题”( title)这样的属性,如何增加触摸动作的目标对象等问题就好。

利用“工厂模式”(Factory pattern)是创建类族如下。

//EOCEmployee.h

typedef NS_ENUM(NSUInteger, EOCEmployeeType) {
    EOCEmployeeTypeDeveloper,
    EOCEmployeeTypeDesigner,
    EOCEmployeeTypeFinance,
};

@interface EOCEmployee : NSObject

@property (copy) NSString *name;
@property NSUInteger salary;


// Helper for creating Employee objects
+ (EOCEmployee*)employeeWithType:(EOCEmployeeType)type;

// Make Employees do their respective day's work
- (void)doADaysWork;

@end
//EOCEmployee.m

@implementation EOCEmployee

+ (EOCEmployee*)employeeWithType:(EOCEmployeeType)type {
     switch (type) {
         case EOCEmployeeTypeDeveloper:
            return [EOCEmployeeDeveloper new];
         break; 

        case EOCEmployeeTypeDesigner:
             return [EOCEmployeeDesigner new];
         break;

        case EOCEmployeeTypeFinance:
             return [EOCEmployeeFinance new];
         break;
    }
}

- (void)doADaysWork {
 // 需要子类来实现
}
@end

员工基类可以有多个子类,只需要根绝类型即可子类。使用了工厂模式的思想

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

  • 可以通过“关联对象”机制来把两个对象连起来。
  • 定义关联对象时可指定内存管理语义,用以模仿定义属性时所采用的“拥有关系”与“非拥有关系”。
  • 只有在其他做法不可行时才应选用关联对象,因为这种做法通常会引入难于查找的bug。

利用runtime里面的关联对象(Associated Object)为不方便修改的实体类添加属性
用法是

//此方法以给定的键和策略为某对象设置关联对象值。
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,  id _Nullable value, objc_AssociationPolicy policy)
//此方法根据给定的键从某对象中获取相应的关联对象值。
id objc_ getAssociatedObject(id object, void*key)
//此方法移除指定对象的全部关联对象。
void objc_ removeAssociatedObjects(id object)

其中objc_AssociationPolicy是策略模式,模式对应值如下

《Effective Objective-C 2.0》读书笔记 一_第1张图片
image.png

举个例子

#import 

static void *EOCMyAlertViewKey = "EOCMyAlertViewKey";


- (void)askUserAQuestion {

         UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Question"
                                                         message:@"What do you want to do?"
                                                        delegate:self
                                               cancelButtonTitle:@"Cancel"
                                               otherButtonTitles:@"Continue", nil];

         void (^block)(NSInteger) = ^(NSInteger buttonIndex){

                     if (buttonIndex == 0) {
                            [self doCancel];
                     } else {
                            [self doContinue];
                    }
         };

         //将alert和block关联在了一起
         objc_setAssociatedObject(alert,EOCMyAlertViewKey,block, OBJC_ASSOCIATION_COPY);
         [alert show];
}

// UIAlertViewDelegate protocol method
- (void)alertView:(UIAlertView*)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
     //alert取出关联的block
      void (^block)(NSInteger) = objc_getAssociatedObject(alertView, EOCMyAlertViewKey)
     //给block传入index值
      block(buttonIndex);
}

第11条:理解objc_msgSend的作用

  • 消息由接收者、选择子及参数构成。给某对象“发送消息”(invoke a message)日也就相当于在该对象上“调用方法”(call a method)。
  • 发给某对象的全部消息都要由“动态消息派发系统" (dynamic message dispatch system)来处理,该系统会查出对应的方法,并执行其代码。

在OC中,如果向某对象传递信息,那就会使用动态绑定机制来决定需要调用的方法。在底层,所有方法都是普通的C语言函数.

然而对象收到 消息后,究竟该调用哪个方法则完全于运行期决定,甚至可以在程序运行时改变,这些特性使得OC成为一门真正的动态语言。

在OC中,给对象发送消息的语法是:

id returnValue = [someObject messageName:parameter];

这里,someObject叫做“接收者(receiver)”,messageName:叫做"选择子(selector)",选择子和参数合起来称为“消息”。编译器看到此消息后,将其转换为一条标准的C语言函数调用,所调用的函数乃是消息传递机制中的核心函数叫做objc_msgSend,它的原型如下:

void objc_msgSend(id self, SEL cmd, ...)

第一个参数代表接收者,第二个参数代表选择子,后续参数就是消息中的那些参数,数量是可变的,所以这个函数就是参数个数可变的函数。

因此,上述以OC形式展现出来的函数就会转化成如下函数:

id returnValue = objc_msgSend(someObject,@selector(messageName:),parameter);

这个函数会在接收者所属的类中搜寻其“方法列表”,如果能找到与选择子名称相符的方法,就去实现代码,如果找不到就沿着继承体系继续向上查找。如果找到了就执行,如果最终还是找不到,就执行消息转发操作。

如果匹配成功的话,这种匹配的结果会缓存在“快速映射表”里面。每个类都有这样一块缓存。所以如果将来再次向该类发送形同的消息,执行速度就会更快了。

第12条:理解消息转发机制

  • 若对象无法响应某个选择子(seletor),则进入消息转发流程
  • 通过运行期的动态方法解析功能,我们可以在需要用到某个方法时再将其加入类中
  • 对象可以把其无法解读的某些选择子转交给其他对象来处理
  • 经过上述两步之后,如果还是没办法处理选择子,那就启动完整的消息转发机制

消息转发的全流程


《Effective Objective-C 2.0》读书笔记 一_第2张图片
消息转发的全流程

类方法+(BOOL)resolveInstanceMethod:(SEL)selector:查看这个类是否能新增一个实例方法用以处理此选择子;如果是类方法则为+ (BOOL)resolveClassMethod:(SEL)selector

实例方法- (id)forwardTargetForSelector:(SEL)selector;询问是否能找到未知消息的备援接受者,如果能找到备援对象,就将其返回,如果不能,就返回nil。

实例方法- (void)forwardInvocation:(NSInvocation*)invocation:创建NSInvocation对象,将尚未处理的那条消息 有关的全部细节都封于其中,在触发NSInvocation对象时,“消息派发系统(message-dispatch system)”就会将消息派给目标对象。

一个完整例子
下面来看一个关于动态方法解析的例子:

#import 

@interface EOCAutoDictionary : NSObject
@property (nonatomic, strong) NSString *string;
@property (nonatomic, strong) NSNumber *number;
@property (nonatomic, strong) NSDate *date;
@property (nonatomic, strong) id opaqueObject;

@end



#import "EOCAutoDictionary.h"
#import 


@interface EOCAutoDictionary ()
@property (nonatomic, strong) NSMutableDictionary *backingStore;
@end



@implementation EOCAutoDictionary

@dynamic string, number, date, opaqueObject;



- (id)init {
 if ((self = [super init])) {
    _backingStore = [NSMutableDictionary new];
}

   return self;

}



+ (BOOL)resolveInstanceMethod:(SEL)selector {

     NSString *selectorString = NSStringFromSelector(selector);
     if ([selectorString hasPrefix:@"set"]) {
         class_addMethod(self,selector,(IMP)autoDictionarySetter, "v@:@");
     } else {
         class_addMethod(self,selector,(IMP)autoDictionaryGetter, "@@:");
    }
     return YES;
}

id autoDictionaryGetter(id self, SEL _cmd) {

     // Get the backing store from the object
     EOCAutoDictionary *typedSelf = (EOCAutoDictionary*)self;
     NSMutableDictionary *backingStore = typedSelf.backingStore;

     // The key is simply the selector name
     NSString *key = NSStringFromSelector(_cmd);

     // Return the value
     return [backingStore objectForKey:key];
}

void autoDictionarySetter(id self, SEL _cmd, id value) {

     // Get the backing store from the object
     EOCAutoDictionary *typedSelf = (EOCAutoDictionary*)self;
     NSMutableDictionary *backingStore = typedSelf.backingStore;

     /** The selector will be for example, "setOpaqueObject:".
     * We need to remove the "set", ":" and lowercase the first
     * letter of the remainder.
     */
     NSString *selectorString = NSStringFromSelector(_cmd);
     NSMutableString *key = [selectorString mutableCopy];

     // Remove the ':' at the end
    [key deleteCharactersInRange:NSMakeRange(key.length - 1, 1)];

     // Remove the 'set' prefix
    [key deleteCharactersInRange:NSMakeRange(0, 3)];

     // Lowercase the first character
     NSString *lowercaseFirstChar = [[key substringToIndex:1] lowercaseString];
    [key replaceCharactersInRange:NSMakeRange(0, 1) withString:lowercaseFirstChar];

     if (value) {
       [backingStore setObject:value forKey:key];
    } else {
        [backingStore removeObjectForKey:key];        
    }
}

在本例中,EOCAutoDictionary类将属性设置为@dynamic,也就是说编译器无法自动为其属性生成set和get方法,因此我们需要动态给其添加set和get方法。

上面实例用到的编码含义地址为编码地址
更多消息转发请看看
消息转发与NSProxy

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

与选择子名称相对应的方法是可以在运行期被改变的,所以,我们可以不用通过继承类并覆写方法就能改变这个类本身的功能。

那么如何在运行期改变选择子对应的方法呢?
答:通过操纵类的方法列表的IMP指针

什么是类方法表?什么是IMP指针呢?

类的方法列表会把选择子的名称映射到相关的方法实现上,使得“动态消息派发系统”能够据此找到应该调用的方法。这些方法均以函数指针的形式来表示,这些指针叫做IMP。例如NSString类的选择子列表:

OC的运行期系统提供的几个方法就能操纵它。开发者可以向其中增加选择子,也可以改变某选择子对应的方法实现,也可以交换两个选择子所映射到的指针以达到交换方法实现的目的。

举个 :交换lowercaseStringuppercaseString方法的实现:


Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class],@selector(uppercaseString));

method_exchangeImplementations(originalMethod, swappedMethod);

这样一来,类方法表的映射关系就变成了下图:

image

这时,如果我们调用lowercaseString方法就会实际调用uppercaseString的方法,反之亦然。

然而!
在实际应用中,只交换已经存在的两个方法是没有太大意义的。我们应该利用这个特性来给既有的方法添加新功能:

它的实现原理是:先通过分类增加一个新方法,然后将这个新方法和要增加功能的旧方法替换(旧方法名 对应新方法的实现),这样一来,如果我们调用了旧方法,就会实现新方法了。

不知道这么说是否抽象。还是举个 :

需求:我们要在原有的lowercaseString方法中添加一条输出语句。

步骤一:我们先将新方法写在NSString的分类里:

@interface NSString (EOCMyAdditions)
- (NSString*)eoc_myLowercaseString;
@end

@implementation NSString (EOCMyAdditions)

- (NSString*)eoc_myLowercaseString {
     NSString *lowercase = [self eoc_myLowercaseString];//eoc_myLowercaseString方法会在将来方法调换后执行lowercaseString的方法
     NSLog(@"%@ => %@", self, lowercase);//输出语句,便于调试
     return lowercase;
}
@end

步骤二:交换两个方法的实现(操纵调换IMP指针)

Method originalMethod =
 class_getInstanceMethod([NSString class],
 @selector(lowercaseString));
Method swappedMethod =
 class_getInstanceMethod([NSString class],
 @selector(eoc_myLowercaseString));

method_exchangeImplementations(originalMethod, swappedMethod);

这样一来,我们如果交换了lowercaseStringeoc_myLowercaseString的方法实现,那么在调用原来的lowercaseString方法后就可以输出新增的语句了。

“NSString *string = @"ThIs iS tHe StRiNg";
NSString *lowercaseString = [string lowercaseString];
// Output: ThIs iS tHe StRiNg => this is the string”

class_addMethod ,class_replaceMethod和method_exchangeImplementations区别

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

在运行期程序库的头文件里定义了描述OC对象所用的数据结构:

typedef struct objc_class *Class;

    struct objc_class {
         Class isa;
         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;
};

在这里,isa指针指向了对象所属的类:元类(metaclass),它是整个结构体的第一个变量。super_class定义了本类的超类。

我们也可以向对象发送特定的方法来检视类的继承体系:自身属于哪一类;自身继承与哪一类。

  • 我们使用isMemberOfClass:能够判断出对象是否为某个特定类的实例;
  • isKindOfClass:方法能够判断出对象是否为某类或其派生类的实例。

这两种方法都是利用了isa指针获取对象所属的类,然后通过super_class类在继承体系中查询。在OC语言中,必须使用这种查询类型信息的方法才能完全了解对象的真实类型。因为对象类型无法在编译期决定。

尤其注意在集合类里获取对象时,通常要查询类型信息因为这些对象不是强类型的(strongly typed),将它们从集合类中取出来的类型通常是id,也就是能响应任何消息(编译期)。

所以如果我们对这些对象的类型把握不好,那么就会有可能造成对象无法响应消息的情况。因此,在我们从集合里取出对象后,通常要进行类型判断:

- (NSString*)commaSeparatedStringFromObjects:(NSArray*)array {

         NSMutableString *string = [NSMutableString new];

             for (id object in array) {
                    if ([object isKindOfClass:[NSString class]]) {
                            [string appendFormat:@"%@,", object];
                    } else if ([object isKindOfClass:[NSNumber class]]) {
                            [string appendFormat:@"%d,", [object intValue]];
                    } else if ([object isKindOfClass:[NSData class]]) {
                           NSString *base64Encoded = /* base64 encoded data */;
                            [string appendFormat:@"%@,", base64Encoded];
                    } else {
                            // Type not supported
                    }
              }
             return string;
}

你可能感兴趣的:(《Effective Objective-C 2.0》读书笔记 一)