[iOS] Effective Objective-C ——熟悉OC、类、对象、运行期

1. OC起源

※ 动态绑定
首先是OC其实是基于“消息机制”的,具体可以参考:https://www.jianshu.com/p/4f69804d0b4c

当我们调用的时候:

Student *student = [Student new];
[student getName:name];

代码会被翻译为以下执行:(还是类似函数调用,但是实际执行的时候查方法列表来执行方法)

objc_msgSend(student,@selector(getName:),name);

如何找到selector嘞?
就是通过NSObject也就是类的isa指针,isa指针结构里面有方法列表。


isa
isa结构

这一切都是在运行时也就是runtime发生的,在编译的时候并不会确定你调用的是什么方法,这也是OC可以使用category来增加方法的基础。而C语言的函数调用是静态绑定的,也就是在编译时就确定了你调用的是哪个函数。

在上面的objc_class结构体中,ivars是objc_ivar_list(成员变量列表)指针;methodLists是指向objc_method_list指针的指针。在Runtime中,一个类objc_class结构体大小是固定的,不可能往这个结构体中添加数据,只能修改。
所以ivars指向的是一个固定区域,只能修改成员变量值,不能增加成员变量个数。methodList是一个二维数组,所以可以修改*methodLists的值来增加成员方法,虽没办法扩展methodLists指向的内存区域,却可以改变这个内存区域的值(存储的是指针)。
因此,可以动态添加方法,不能添加成员变量。


※ 内存分配
对象所占用的内存总是在Heap堆里面的,指向对象的指针是在栈帧Stack frame里面的。

如果我创建两个对象:

NSString *someString = @"Hi";
NSString *anotherString = someString;
对象空间分配

heap上面的内存需要管理,而stack里面的在没有用了以后他会自动被清除掉,这也是为什么其实基础变量int之类(定义时不用*的,例如CGRect)的在stack上面的是不需要weak,只要assign就可以了,因为他们的内存管理是由stack直接搞的所以不会有野指针问题。

2. 在.h文件中减少引入其他的.h文件

当我们import ClassA.h的时候,如果ClassA.h里面import ClassB.h了,那么编译的时候就会一层一层的import,但有可能最后由于if else之类的逻辑分支并没有用到ClassB,那么其实就白白增加了编译的时间,所以其实ClassA.h文件里面只要声明ClassB是一个class就可以了,不用把它的.h文件引入,在.m文件里面再引入,这样当使用的时候才会真正引入。

// Class A.h
#import "ClassB.h"

应改为:
// Class A.h
@class ClassB;

// Class A.m
#import "ClassB.h"

而且如果Class A中import了B,而B又import A,就会造成循环引入,于是两者中有一个无法正常编译。

P.S. import有两个作用:一是和include一样,完完全全的拷贝文件的内容;二是可以自动防止文件内容的重复拷贝(即使文件被多次包含,也只拷贝一份)。

需要注意的是,不是所有状况都可以用@class声明然后不实际引入滴,主要是在继承以及遵从某协议的情况下,需要知道具体接口有啥。delegate一般都会和自己的调用类放在一起,所以不会有这个问题。

如果在.h文件中实现某个协议,这样就必须要在.h文件中import那个协议,而且协议不能用@protocal声明但不实际import(这样编译器会报错无法找到定义),所以最好不要这么做。尽量在.m文件中实现协议哈。

3. 尽量使用字面量

属于Foundation的NSString、NSArray、NSDictionary以及NSNumber是可以用字面量来赋值的,例如:

NSNumber *num = @23;
替换:
NSNumber *num = [NSNumber numberWithInt:23];

NSString *str = @"Hi";
替换:
NSString *str = [[NSString alloc] init];

NSArray *arr1 = @[@(1)];
替换:
NSArray * arr1 = [NSArray arrayWithObjects:@1, nil];

NSNumber *num = arr1[0];
替换:
NSNumber *num = [arr1 objectAtIndex:0];

NSDictionary *dict = @{@"key1": @"key2"};
NSString *obj = dict[@"key1"];
替换:
NSDictionary *dict = @{@"key1": @"key2"};
NSString *obj = [dict objectForKey:@"key1"];

这样的好处是更加简洁,并且如果有nil的话会立刻crash,防止了一些容错导致有问题无法发现。

需要注意的是,如果是NSMutableXXX可以利用mutableCopy,虽然增加了一个对象但是也是有点大于缺点的。

NSMutableArray *muteArr = [@[@1] mutableCopy];

4. 少用define多用const

宏define在C中只是替换,例如:

#define kCONTROL_BAR_HEIGHT 60
#define kSCREEN_WIDTH  ([UIScreen mainScreen].bounds.size.width)

如果我在代码中使用kSCREEN_WIDTH,那么它就会被替换为([UIScreen mainScreen].bounds.size.width),然鹅编译时不会检查这个替换的内容是不是正确的,于是如果define所替代的公式是错的,编译时也不会发现,很容易出错,所以最好不要用define。

尤其是define如果用来定义变量,都不会给出类型,其实是非常不直观而且容易出问题的。


※ 定义const需要注意一下命名规范哈
如果可以在.m文件里面定义的常量就不要放到.h里面定义,因为一旦别的文件引入了这个含有const定义的.h,就会也定义了一个const,定义域相当于类似全局变量了,很容易重复,这样可能几个.h定义了同名const就会有奇怪的bug。

如果在.m文件定义,它的作用域就在.m文件内部,你可以用“k+大驼峰”的明明规范来命名,例如:

static const NSTimeInterval kAnimationDuration = 0.3;

但如果你的const必须放到.h文件,那么命名就不可以用k了,需要加上所属类名,确保不重复性,例如:

static const NSTimeInterval XXXClassAnimationDuration = 0.3;
static const NSTimeInterval MainViewControllerAnimationDuration = 0.3;

※ 关于static
可参考:https://www.jianshu.com/p/4bfd96c57a6d

常量需要用static & const来定义,不能只用const,因为如果不用static声明的全局变量,声明周期是到程序结束的,其他文件可以通过extern引入这个变量,作用域类似全局,当其他文件中定义了同名const会报错duplicate symbol。

而static对于局部变量的作用是将其改成声明周期到App结束,对于全局变量则是生命周期到App结束,但是只能在声明它的文件中调用,也就是作用域局限在了声明它的文件中。所以即使其他文件定义了同名static const也不会报错。


※ 如何写对外的const
如果是对外的常量表,以及一些类似notification的名字之类的,是对外会使用的string,但是其实外面用的人而言,他们并不需要知道string实际的字面量,只要用这个变量就可以了,所以就实现声明分离而言,应该是定义在.m文件里,声明在.h文件,不要直接在.h文件里面用static const这样。

推荐的做法是:

.h文件:
extern NSString * const ClassXXXMMKVKeyVideoPlayStartCount;

.m文件:
// 不能加static哦,否则就不能extern找到啦
NSString * const ClassXXXMMKVKeyVideoPlayStartCount = @"ClassXXXVideoPlayStartCount";

注意如果对外,命名加上类名前缀哦!

const extern static的区别可以参考:https://www.cnblogs.com/qizhuo/p/6038186.html

5. 用枚举表示选项、状态、状态码

typedef NS_ENUM(NSInteger, RateAlertChance) {
    RateAlertChanceSubscription,
    RateAlertChanceMeditationPlay,
    RateAlertChanceMeditationFinish,
    RateAlertChanceMusicPlay,
};

还有一种用法,是不用默认的+1作为枚举值:

typedef NS_ENUM(NSInteger, RateAlertChance) {
    RateAlertChanceSubscription = 0,
    RateAlertChanceMeditationPlay = 1 << 0,
    RateAlertChanceMeditationFinish = 1 << 1,
    RateAlertChanceMusicPlay = 1 << 2,
};

这样就可以用| &之类的位操作了,当有可能两种状态共存的时候最好用这种。

注意如果用枚举,switch就不要有default啦,确保处理所有状态即可,否则多出一种状态很奇怪。

6. 理解“属性”这一概念

※ 实例变量如何寻址

首先OC的实例变量具有运行时寻址,如果增加了变量以后无需重新编译的优点,具体可参考:http://quotation.github.io/objc/2015/05/21/objc-runtime-ivar-access.html

举个栗子:


image

先来看如果是传统C代码,新建一个MyObject集成NSObject,在编译的时候会计算它们实例变量的偏移量,类似于students的偏移量是4,那么编译时代码中所有用到student的地方都会被hard code硬改写为4。

如果苹果发了新版本,给NSObject新加了两个属性:


image

那么在运行时NSObject已经变了,所以它的子类也会自动变,增加secretAry和secretImage,并且偏移量分别为4和8,如果代码不重新编译,所有被替换为4的用到students的地方,取值其实都拿到了secretAry,所以必须要重新编译,让代码中被hard code数值的地方都更新为新的偏移量才能正常运行。

但如果MyObject是第三方库提供的打包后的product,那么还必须等待第三方库打一个新的product才能正常运行我们的程序,这是非常麻烦的。

Objective-C是怎么做的呢?

struct ivar_t {
    int32_t *offset;
    const char *name;
    const char *type;
    //...
};

每个变量都会有一个指向int的指针来存储这个实例变量的偏移量,所以如果运行时的偏移量改变以后,只要顺着class找到他的实例变量list,并且通过指针找到真实存储偏移量的int,改掉这个值就可以啦,虽然占用了空间,给每个实例变量增加了一个offset的存储,但是没用offset替代掉变量增加了很多的灵活性,充分解决了重编译问题。


※ @property等

如果我定义了一个@property,那么会自动生成它的存取两个方法(但readonly就不会了哈):

@property (nonatomic) NSString *entrance;

- (NSString *)entrance;
- (void)setEntrance:(NSString *)entrance;

当我们用xxx.entrance获取属性值的时候其实就是调用的[xxx entrance]方法来得到,如果用xxx.entrance = @"ssss",就是调用了[xxx setEntrance:@"ssss"]。

之前有写过两篇文章探讨属性:https://www.jianshu.com/p/1313aac306b1以及https://www.jianshu.com/p/e13259caf01e

我们创建property会自动创建一个下划线开头的实例变量+set+get方法,这个过程就叫做自动合成(auto-synthesize)

//.h文件
@property (nonatomic) NSString *entrance;

会创建一个实例变量等同于:
//.m文件
@interface ViewController2 () {
    NSString *_entrance;
}


@implementation ViewController2

@synthesize entrance = _entrance;

@end

如果你不希望自动创建的变量名为“下划线+属性名”,可以强制改名:

@implementation ViewController2

@synthesize entrance = _entrance2;

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.entrance = @"ssss";
    _entrance2 = @"ssss";
}

@end

这样的话就不会自动生成 _entrance变量了,只会有_entrance2变量。但是非常不推荐改名字,一个是为了统一规范,大家都很容易懂,下意识就能看懂即使你不用self.xxx来获取;一个是其实没有多大必要性。

但@synthesize不是只是用来改名字滴,它实际语义是如果你没有手动实现setter方法和getter方法,那么编译器会自动为你加上这两个方法。

当然我们可以自己覆写set/get方法,没有被覆写的仍旧会被自动生成。如果你不希望自动生成任何存取方法,就用@吧:

@implementation ViewController2

@dynamic entrance;

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.entrance = @"ssss";
}

@end

这段代码会crash哦,因为没有自动生成set方法,我们也没自己实现set,于是用self.entrance的时候就crash啦。

但编译时是不会报错的,它相信运行时会有相应的set&get方法的,只是运行时如果没有就会crash。

@dynamic的应用场景主要是:CoreData以及category增加属性:https://www.cnblogs.com/Ohero/p/4739089.html


property有很多修饰符,需要注意的是,假设你声明了copy,那么如果你同时覆写了set方法,需要在里面真的去copy传入的参数,确保声明的修饰符和你实际的操作是一致的!因为别人调用的时候看到声明就会认为是copy的。

如果你写了initWithXXX的方法,而且XXX的属性也是copy修饰的,那么一定要在init方法里面真的copy哦,否则也是不一致的。

如果有些readonly的属性是copy的也要声明哦,即使不会自动给他生成set方法,但是init的时候作者就知道需要copy来初始化啦,并且外面调用的时候也会知道是copy过得,不会再次自己去copy一遍,重复copy没啥必要很浪费。

7. 在对象内部尽量访问实例变量

  • 用实例变量访问:
    读取优点不用经过函数,直接读内存更快

  • 用.的方式访问:

    1. 写入优点直接借用了property的修饰符,不用自己再实现一遍
    2. 写入可以触发KVO
    3. 读/写可以在setter以及getter方法里面打断点利于调试

最好是用实例变量直接读取,但赋值的时候通过.的方式。

但有两点需要特别注意一下:
(1)在init方法里面不要用self.的方式给属性赋值
因为有可能子类继承了父类,并且覆写了set属性的方法,偷偷做了检测或者抛个exception之类的,所以在init里面要直接用实例变量赋值
但如果在子类的init方法里面不能直接用父类的实例变量,就要用self.的方式赋值啦
(2)懒加载的时候要用self.的方式读取,不要直接读实例变量
因为懒加载其实就是覆写getter,看实例变量是不是nil,如果是nil就初始化实例变量,不为nil就直接返回实例变量。
如果直接读实例变量不用self.,相当于绕过了懒加载,永远都不能初始化这个实例变量

8. 理解对象同等性

==比较的是指针是否相等,如果想自定义比较方式,可以重写isEqual,需要注意的是,一定要考虑各种情况,例如不是同一种Class、如果父类和子类比应该返回什么等。

//.h文件
@interface Person : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSDate *birthday;

@end

//.m文件
- (BOOL)isEqual:(id)object {
    if (self == object) {
        return YES;
    }
    
    if (![object isKindOfClass:[Person class]]) {
        return NO;
    }
    
    return [self isEqualToPerson:(Person *)object];
}

- (BOOL)isEqualToPerson:(Person *)person {
    if (!person) {
        return NO;
    }
    
    BOOL haveEqualNames = (!self.name && !person.name) || [self.name isEqualToString:person.name];
    BOOL haveEqualBirthdays = (!self.birthday && !person.birthday) || [self.birthday isEqualToDate:person.birthday];
    
    return haveEqualNames && haveEqualBirthdays;
}

isEqual在单独比较的时候没有问题,比如我们姓名和生日一致的两个person对象如果用isEqual比较就是true的。但是如果把这两个equal的person加入到一个set里面,仍旧是可以同时加进去的,这就是有问题的地方了,因为同一个object是不可能作为两个object加入到一个set里面的,set应该保持唯一性,重复加入时应该无效,仍旧只有那一个object。


※ 什么是hash

这个问题要从Hash Table这种数据结构说起
首先我们看下如何在数组中查找某个成员

Step 1: 遍历数组中的成员
Step 2: 将取出的值与目标值比较, 如果相等, 则返回该成员

在数组未排序的情况下, 查找的时间复杂度是O(array_length)
为了提高查找的速度, Hash Table出现了
当成员被加入到Hash Table中时, 会给它分配一个hash值, 以标识该成员在集合中的位置
通过这个位置标识可以将查找的时间复杂度优化到O(1), 当然如果多个成员都是同一个位置标识, 那么查找就不能达到O(1)了

所以尽量不要大家hash值都一样,那样其实根本没有减少时间复杂度。默认的hash值就是对象的内存地址。

分配的这个hash值(即用于查找集合中成员的位置标识), 就是通过hash方法计算得来的, 且hash方法返回的hash值最好唯一

和数组相比, 基于hash值索引的Hash Table查找某个成员的过程就是
Step 1: 通过hash值直接找到查找目标的位置
Step 2: 如果目标位置上有多个相同hash值得成员, 此时再按照数组方式进行查找

所以数组元素比较的时候,如果hash值一样,才会继续比isEqual,如果hash都不一样就直接false了。故而,一样的一定是hash值一致,但是hash值一致不一定equal哈。

所以上面的例子应该覆写hash,防止会往set中加入相同的object:

- (NSUInteger)hash {
    return [self.name hash] ^ [self.birthday hash];
}

或者把name和birthday拼成一个字符串以后取hash值返回,只是不推荐这种,因为产生了新的字符串消耗,还是推荐上面这一种。

关于isEqual和hash可以参考:https://www.jianshu.com/p/915356e280fc
啥时候会调用hash可参考:https://www.cnblogs.com/YouXianMing/p/5397197.html(set以及dict的key,用于去重)

如果重写isEqual方法,一定要重写hash方法。
重写的hash方法一定要简单,因为如果你的对象存在字典或者集中,hash方法会频繁的调用。
相同的对象一定要返回相同的hash值,但是有相同的hash值的对象不一定是同一个对象,这是就是产生了碰撞,但是我们要让产生这种情况的机会尽可能的少。

  • hash方法与判等的关系?

为了优化判等的效率, 基于hash的NSSet和NSDictionary在判断成员是否相等时, 会这样做

  1. 集成成员的hash值是否和目标hash值相等, 如果相同进入Step 2, 如果不等, 直接判断不相等

  2. hash值相同(即Step 1)的情况下, 再进行对象判等, 作为判等的结果

所以如果hash一致但是isEqual返回NO还是不等的哦,可以放进set作为两个不同对象~

简单地说就是:hash值是对象判等的必要非充分条件

补充一个知识点,之前面试的时候小哥哥问过的,hash如果冲突了怎么办,其实就会链表连起来~~ 我查了一下还有3种解决方式比如再算另外的一种hash之类的,可以参考:https://blog.csdn.net/Alexlee1986/article/details/81080449


※ isEqualToXXXClass

NSString的isEqualToString以及Array和Dict的类似方法其实都是没有执行类型检查的,也就是它默认了你传入的就是NSString,不会再去判断class是不是一致啦,这样可以增加执行效率,并且语义更加清晰。

所以如果自定义了class并且实现了isEqual,还是也实现一下isEqualToXXXClass会好一点~

- (BOOL)isEqualToPerson:(Person *)person {
    if(!person) {
        return NO;
    }

    BOOL haveEqualNames = (!self.name && !person.name) || [self.name  isEqualToString:person.name];
    BOOL haveEqualBirthdays = (!self.birthday && !person.birthday) || [self.birthday isEqualToDate:person.birthday];

    return haveEqualNames && haveEqualBirthdays;
}

#pragma mark - NSObject
- (BOOL)isEqual:(id)object {
    if(self == object) {
        return YES;
    }

    if(![object isKindOfClass:[Person class]]) {
        return [super isEqualTo:object];
    }

    return [self isEqualToPerson:(Person *)object];
}

这样isEqual也可以简单的调用一下同类对比,如果不同类就调用super的isEqual方法来对比。


※mutable class放入set之类的container需要注意hash

当将一个mutable object放入set的时候,那个瞬间就会去取他的hash值,之后即使你给这个mutable object做增删改,这个hash是不会再算一次了。

// 正常情况
NSMutableSet* setA = [NSMutableSet new];
NSArray* arrayA = @[@1, @2];
[setA addObject:arrayA];

NSArray* arrayB =@[@1];
[setA addObject:arrayB];
NSLog(@"setA : %@", setA);

输出:
{((1,2), (1))}

也就是可能你放入了一个A,然后放了一个B,然后又把A改的和B一样,那么set里面就有两个看起来一模一样的B。

NSMutableSet* setA = [NSMutableSet new];
NSMutableArray* arrayA = [@[@1,@2] mutableCopy];
[setA addObject:arrayA];

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

输出:
{((1,2), (1,2))}

这个时候如果你copy一下旧set得到一个新set,会发现又只剩下一个B了。

NSSet* setB = [setA copy];
NSLog(@"setB : %@", setB);

输出:
{((1,2))}

这个应该是会新建一个set,然后一个一个往里面add,于是如果有equal的就会加不进去啦。

而如果你加入一个B,然后有放入一个和B一样的B',会发现set里面只有一个元素,不会有相同的两个元素。

NSMutableSet* setA = [NSMutableSet new];
NSArray* arrayA = @[@1, @2];
[setA addObject:arrayA];

NSArray* arrayB =@[@1, @2];
[setA addObject:arrayB];
NSLog(@"setA : %@", setA);

输出:
{((1,2))}

故而,尽量不要将可变对象放到set里面去,因为后续如果更改可变对象,可能会打破set的唯一性。

可参考:https://www.jianshu.com/p/e4ecb4dd14b9


※ NSMutableArray以及NSArray的hash&isEqual判断

NSMutableSet* setA = [NSMutableSet new];
NSArray* arrayA = @[@1, @2];
[setA addObject:arrayA];

NSArray* arrayB =@[@1, @2];
[setA addObject:arrayB];
NSLog(@"setA : %@", setA);

BOOL same = [arrayA isEqual:arrayB];
NSLog(@"arrayA hash:%lu", (unsigned long)[arrayA hash]);
NSLog(@"arrayB hash:%lu", (unsigned long)[arrayB hash]);

NSLog(@"same: %@", same ? @"yes" : @"no");

输出:
setA : {((1,2))}
arrayA hash:2
arrayB hash:2
same: yes

将arrayA和B都替换为[@[@1,@2] mutableCopy],输出结果一样。所以其实array相等大概是通过元素数以及每个元素是否相等来判断的。

注意array木有重复性的check哈,同一个元素也可以加的。

NSMutableArray* arrayA = [@[@1,@2] mutableCopy];
NSNumber *el3 = @3;
[arrayA addObject:el3];
[arrayA addObject:el3];
NSLog(@"arrayA : %@", arrayA);

输出:
arrayA : (1,2,3,3)

9. 以“类族模式”隐藏实现细节

“类族”是一种很有用的模式,可以隐藏“抽象基类”背后的实现细节,OC系统框架中普遍使用此模式。比如UIKit中就有一个名为UIButton的类,创建按钮,则可以调用下面这个类方法:

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

该方法所返回的对象,其类型取决于传入的按钮类型,然而不管返回什么类型的对象,他们都继承同一个基类:UIButton,这么做的意义在于:UIButton类的使用者无需关心创建出来的按钮具体属于哪个子类,也不用考虑按钮的绘制方式等实现细节。

其实这也就是OC对抽象类的一种实现。

举个例子:

typedef NS_ENUM(NSUInteger, CWGEmployeeType) {
  CWGEmployeeTypeDeveloper,
  CWGEmployeeTypeDesigner,
  CWGEmployeeTypeFinance,
}
@interface CWGEmployee : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSUInteger salary;

// 创建对象
+(CWGEmployee *)employeeWithType : (CWGEmployeeType)type;

// 让对象做工作
- (void)doADaysWork;
@end

@implementation CWGEmployee

+(CWGEmployee *)employeeWithType : (CWGEmployeeType)type {
  switch (type) {
    case CWGEmployeeTypeDeveloper:
            return [CWGEmployeeDeveloper new];
            break;
    case CWGEmployeeTypeDesigner:
            return [CWGEmployeeDesigner new];
            break;
    case CWGEmployeeTypeFinance:
            return [CWGEmployeeFinance new];
            break;
  }
}

- (void)doADaysWork {
  // Subclasses implement this.
}
@end

每个“实体子类”都继承基类,例如:

@interface CWGEmployeeDeveloper : CWGEmployee
@end

@implementation CWGEmployeeDeveloper
- (void)doADaysWork {
    [self writeCode];
}
@end

在这个例子中,基类实现了一个“类方法”,该方法根据待创建的雇员类别分配好对应的雇员类实例。这种“工厂模式”是创建类族的办法之一。

还有一点需要注意:如果对象所属的类位于某个类族中,那么在查询类型信息是就要当心了,你可能觉得自己创建了某个类的实例,然而实际上创建的确实其子类的实例。在Employee这个例子中, [employee isMemberOfClass:[CWGEmployee class]]似乎会返回YES,但是发回的确实NO,因为employee并非Employee类的实例,而是其某个子类的实例。


※ NSArray类族

系统框架有许多类族,大部分collection类都是类族,如NSArray和NSMutableArray。有两个抽象基类,一个用于不可变数组,一个用于可变数组。尽管具备公共接口的类有两个,但仍然可以合起来算作一个类族。不可变的类定义对所有数组都通用的方法,可变的类则定义只适用于可变数组的方法。两个类共属同一类族,这意味着两者在实现各自类型的数组时可以同用实现代码。

id maybeArray = /**...*/;
if ([maybeArray class] == [NSArray class]) {
}

NSArray是个类族,其中if语句永远不可能为真。[maybeArray class]返回的类绝不可能是NSArray,因为由NSArray的初始化方法所返回的那个实例其类型是隐藏在类族公共接口后面的某个内部类型。

应该用下面的类型信息查询方法判断:

if ([maybeArray isKindOfClass:[NSArray class]]) {
}

isMemberOfClass:判断是否是这个类的实例
isKindOfClass:判断是否是这个类或者这个类的子类的实例


我们经常需要向类族中新增实体子类,不过这么做的时候得留心。在Employee这个例子中,若是没有“工厂方法”的源代码,那就无法向其中新增雇员类别了。然而对于NSArray这样的类族来说,还是有办法新增子类的,但是需要遵守几条规则。规则如下:

  • 子类应该继承自类族中的抽象基类。
    若要编写NSArray类族的子类,则需令其继承自不可变数组的基类或可变数组的基类。

  • 子类应该定义自己的数据存储方式。
    开发者编写NSArray子类时,经常在这个问题上受阻。子类必须用一个实例变量来存放数组中的对象。这似乎与大家预想的不同,我们以为NSArray本身只不过是包在其他隐藏对象外面的壳,它仅仅定义了所有数组都需具备的一些接口。对于这个自定义的数组子类来说,可以用NSArray来保存其实例。

  • 子类应当覆写超类文档中指明需要覆写的方法。
    在每个抽象基类中,都有一些子类必须覆写的方法。比如说,想要编写NSArray的子类,就需要实现“count” 及 “objectAtIndex:”方法。像lastObject这种方法则无须实现,因为基类可以根据前面两个方法实现出这个方法。

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

如果我们希望给一些类保存一些数据,可能会想到继承这个类,建个子类加一些属性。OC提供了一种更好一点的实现“关联对象”(Associated Object),可以给现有类关联数据,不用为了加属性存数据增加新的子类。

关联对象类似于,每个对象其实都有一个dictionary用于让开发者存储对象相关的数据,最开始就是空的,当你给他增加关联对象的时候,相当于增加了一个键值对。

于是,存取关联对象的值就相当于在NSDictionary对象上调用[object setObject:value forKey:key]与[object objectForKey:key]方法。然而两者之间有个重要差别:设置关联对象时用的键(key)是个“不透明的指针”(opaque pointer)。如果在两个键上调用“isEqual:”方法的返回值是YES,那么NSDictionary就认为二者相等;然而在设置关联对象值时,若想令两个键匹配到同一个值,则二者必须是完全相同的指针才行。

故而,在设置关联对象值时,通常使用静态全局变量做键。

※ 下列方法可以管理关联对象:

  1. void objc_setAssociatedObject(id object, void*key, id value, objc_AssociationPolicy policy)
    此方法以给定的键和策略为某对象设置关联对象值。

  2. id objc_getAssociatedObject(id object, void*key)
    此方法根据给定的键从某对象中获取相应的关联对象值。

  3. void objc_removeAssociatedObjects(id object)
    此方法移除指定对象的全部关联对象。

这里的policy就是对应属性的修饰符哈~

关联policy

※ 关联对象应用

  1. 作为alertView的delegate方法处理

正常的UIAlertView的用法如下:

- (void)askUserAQuestion {  
    UIAlertView *alert = [[UIAlertView alloc]  
                             initWithTitle:@"Question"  
                               message:@"What do you want to do?"  
                                 delegate:self  
                        cancelButtonTitle:@"Cancel"  
                        otherButtonTitles:@"Continue", nil];  
        [alert show];  
}  

// UIAlertViewDelegate protocol method  
- (void)alertView:(UIAlertView *)alertView  
        clickedButtonAtIndex:(NSInteger)buttonIndex  
{  
    if (buttonIndex == 0) {  
        [self doCancel];  
    } else {  
        [self doContinue];  
    }  
} 

这种方式其实delegate和创建alert的地方很有可能是分开的,看起来很不方便,如果用关联将处理的方法关联给alert,那么delegate处理的时候直接取出方法调用就可以啦:

#import <objc/runtime.h> 

static void *EOCMyAlertViewKey = "EOCMyAlertViewKey";  

- (void)askUserAQuestion {  
    UIAlertView *alert = [[UIAlertViewalloc]  
                             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];  
        }  
    };  

      objc_setAssociatedObject(alert,  
                               EOCMyAlertViewKey,  
                               block,  
                              OBJC_ASSOCIATION_COPY);  

      [alert show];  
}  

// UIAlertViewDelegate protocol method  
- (void)alertView:(UIAlertView*)alertView  
        clickedButtonAtIndex:(NSInteger)buttonIndex  
{  
    void (^block)(NSInteger) =  
        objc_getAssociatedObject(alertView, EOCMyAlertViewKey);  
    block(buttonIndex);  
} 

这里需要注意block的里面变量的内存问题,防止循环引用哈,因为这个block相当于被object持有了,如果他又强持有了self的一些属性就会循环引用啦。

  1. category的属性存取

还有一个应用是用于给现有的类增加属性,因为本身是不允许通过category增加属性的,但是借用关联对象可以把属性值存给对象:

@property (nonatomic, strong) UIImageView *commonBackgoundImageView;

- (void)setCommonBackgoundImageView:(UIImageView *)commonBackgoundImageView {
    objc_setAssociatedObject(self, @selector(commonBackgoundImageView), commonBackgoundImageView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (UIImageView *)commonBackgoundImageView {
    return objc_getAssociatedObject(self, @selector(commonBackgoundImageView));
}

这种做法很有用,但是只应该在其他办法行不通时才去考虑用它。若是滥用,则很快就会令代码失控,使其难于调试。而且它容易产生retain cycle,不容易找到问题,policy如果指定的有问题也比较麻烦,所以其实还是一个万不得已才用的option吧,不要因为可以用就用。

11. 理解objc_msgSend的作用

传统C语言的函数调用是硬编码在代码里面滴,类似于:

void A() {}
void B() {}

void doSth (bool isA) {
  if (isA) {
    A();
  } else {
    B();
  }
}

假设A的函数地址是100,B函数地址是200,那么编译完其实就是:
void doSth (bool isA) {
  if (isA) {
    jump to 100;
  } else {
    jump to 200;
  }
}

但是OC是动态绑定的,例如:

void A() {}
void B() {}

void doSth (bool isA) {
  void (*fun)();
  if (isA) {
    fun = A;
  } else {
    fun = B;
  }
  fun();
}

这种情况下,编译时并没有办法知道A是啥,编译器只知道A(),而fun()到底是A函数还是B函数是在运行时才确定的,这也就是动态绑定。

OC中的消息机制就是基于动态绑定,当我们调用object的方法的时候,其实是给他发了一个消息,而底层其实就是对象调用objc_msgSend。

// 消息传递机制的核心函数
void objc_msgSend(id self, SEL cmd, ...)

说明:
是一个“参数可变的函数”,能接受两个或两个以上的参数。
第一个参数:接受者
第二个参数:选择器(SEL是选择器的类型)
后续参数就是消息中的参数,顺序不变。
选择器指的就是方法的名字。

举个例子:

[self reportEvent];
等同于:
objc_msgSend(self, @selector(reportEvent));

完全可以把代码中的函数调用替换成objc_msgSend,因为底层也是这么干的,但是注意需要把.m文件改成.mm即C语言混编,以及把build settings里面的Enable Strict Checking of objc_msgSend Calls改为No
并且#import <objc/message.h>哈

OC中每个对象都有个isa指针,指向的结构体里面有个methodList,存储了方法名以及它所对应的函数地址。所以当我们给一个object发消息的时候,如果能找到与selector名称相符的方法,就调至其实现代码。如果找不到就沿着继承体系继续向上查找,等找到合适的方法之后再跳转。如果最终都找不到,那就执行“消息转发”操作。

当找到相符的方法之后,objc_msgSend会将匹配结果缓存在“快速映射表”里,每个类都会有这么一块缓存,如果稍后还向该类发送此消息,那么执行起来就会很快了。

而且其实OC是有尾调用优化滴:https://www.jianshu.com/p/9e3cd9b1095a?from=timeline&isappinstalled=0

12. 理解消息转发机制

当给object发送它无法解析的消息的时候就会触发消息转发机制:

  • 消息转发分为两大阶段:
    第一阶段:征询接收者,所属的类,看其是否能动态添加方法,以处理当前这个“未知的选择器”。这叫做“动态方法解析”。

    第二阶段:涉及“完整的消息转发机制”。运行时系统会请求接受者以其他手段来处理与消息相关的方法调用。分两小步:
    step 1:请接收者看看有没有其他对象能处理未知消息,若有,则运行时系统会把消息转给那个对象。这叫做“备援接收者”。若没有进行第二步。
    step 2:启动完整的消息转发机制,运行时系统会把与消息有关的全部细节都封装到NSInvocation对象中,再给接收者最后一次机会,令其设法解决当前的未知消息。

转发机制

消息转发,步骤越往后,处理消息的代价就越大,最好在第一步就处理完,这样,运行时系统可以将此方法缓存起来,如果类的实例稍后收到同名选择器,那就无须启动消息转发流程。如果不修改消息内容,则在第二步进行消息转发即可。


※ 应用

// 头文件
#import <Foundation/Foundation.h>

@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 <objc/runtime.h>
#import "EOCAutoHelper.h"

@interface EOCAutoDictionary()

@property (nonatomic, strong) NSMutableDictionary *backingStore;

@property (nonatomic, strong) EOCAutoHelper *heper;

@end

@implementation EOCAutoDictionary

@dynamic string, number, date, opaqueObject;

- (instancetype)init
{
    self = [super init];
    if (self) {
        _backingStore = [NSMutableDictionary new];
        _heper = [EOCAutoHelper new];
    }
    return self;
}

// 动态方法解析
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    NSString *selectorString = NSStringFromSelector(sel);
    
    if ([selectorString isEqualToString:@"autoHeplerMethod"]) {
        // 备援接受者
        return NO;
    } else {
        // 动态方法解析
        if ([selectorString hasPrefix:@"set"]) {
            // 向类中动态添加方法
            // 参数说明:类,选择器,待添加的函数指针,类型编码(返回值类型@:参数)
            class_addMethod(self, sel, (IMP)autoDictionarySetter, "v@:@");
        } else {
            class_addMethod(self, sel, (IMP)autoDictionaryGetter, "@@:");
        }
        return YES;
    }
    return NO;
}

// 备援接受者
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    NSString *selectorString = NSStringFromSelector(aSelector);
    if ([selectorString isEqualToString:@"autoHeplerMethod"]) {
        // 返回一个内部对象来代替实现方法
        return _heper;
    }
    return [super forwardingTargetForSelector:aSelector];
}

id autoDictionaryGetter(id self, SEL _cmd)
{
    EOCAutoDictionary *typeSelf = (EOCAutoDictionary *)self;
    NSMutableDictionary *backingStore = typeSelf.backingStore;
    
    NSString *key = NSStringFromSelector(_cmd);
    return [backingStore objectForKey:key];
}

void autoDictionarySetter(id self, SEL _cmd, id value)
{
    EOCAutoDictionary *typeSelf = (EOCAutoDictionary *)self;
    NSMutableDictionary *backingStore = typeSelf.backingStore;
    
    NSString *selectorString = NSStringFromSelector(_cmd);
    NSMutableString *key = [selectorString mutableCopy];
    
    // 删除':'
    [key deleteCharactersInRange:NSMakeRange(key.length - 1, 1)];
    // 删除'set'
    [key deleteCharactersInRange:NSMakeRange(0, 3)];
    // 将第一个字符串变为小写
    NSString *lowercaseFirstChar = [[key substringToIndex:1] lowercaseString];
    [key replaceCharactersInRange:NSMakeRange(0, 1) withString:lowercaseFirstChar];
    if (value) {
        [backingStore setObject:value forKey:key];
    } else {
        [backingStore removeObjectForKey:key];
    }
}

@end

// 头文件
#import <Foundation/Foundation.h>

@interface EOCAutoHelper : NSObject

- (void)autoHeplerMethod;

@end

// 实现文件
#import "EOCAutoHelper.h"

@implementation EOCAutoHelper

- (void)autoHeplerMethod
{
    NSLog(@"EOCAutoHelper");
}

@end

// 使用
EOCAutoDictionary *dict = [EOCAutoDictionary new];

// 测试动态方法解析
dict.date = [NSDate dateWithTimeIntervalSince1970:475372800];
NSLog(@"dict.date : %@",dict.date);

// 测试备援接收者
[dict performSelector:@selector(autoHeplerMethod)];

现在可以往EOCAutoDictionary加任何属性啦,因为所有dynamic的属性找不到setter/getter的时候就会调用resolveInstanceMethod,resolveInstanceMethod又会自动创建setter和getter,类似CALayer就是可以加任何的属性,然后用key-value的方式读取。

13.用“方法调配技术”调试“黑盒方法”

我们之前一直有提到方法列表,他其实也是类似key-value对,key就是selector,value就是指向实现的指针IMP:

id (*IMP)(id, SEL, ...)
默认正常的方法列表

我们可以修改这个列表的指向,这个过程就叫做方法调配。例如我们可以改成下面这样:


改imp指针指向

通过这个方式我们无须增加子类,就可以改变现有类的方法。

交换方法实现:
void method_exchangeImplementations(Method m1, Method m2)
参数:表示待交换的两个方法实现 

获取方法实现:
Method class_getInstanceMethod(Class cls, SEL name)
参数:类,相关方法

例如我们可以交换String的lower和uppercase方法:

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

method_exchangeImplementations(originalMethod, swappedMethod);

但其实我们很少会交换方法,毕竟方法就应该和它实际做了什么相符合。

方法调配可以用于黑盒调试,在调用现有方法的时候增加日志等,避免了继承子类再调用,毕竟系统类生成子类很麻烦。

举个例子~如果想在lowercaseString被调用的时候打印一些日志,可以:

// 新建NSString类分类,头文件
#import <Foundation/Foundation.h>

@interface NSString (EOCMyAdditions)

- (NSString *)eoc_myLowercaseString;

@end

// 实现文件
#import "NSString+EOCMyAdditions.h"

@implementation NSString (EOCMyAdditions)

- (NSString *)eoc_myLowercaseString
{
    NSString *lowercase = [self eoc_myLowercaseString];
    NSLog(@"%@ => %@",self, lowercase);
    return lowercase;
}

@end

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

method_exchangeImplementations(originalMethod, swappedMethod);

NSString *string = @"This is the Stirng";
NSString *lowercaseString = [string lowercaseString];

输出:
2018-08-18 23:17:24.038765+0800 Demo[12721:496759] This is the Stirng => this is the stirng

注意eoc_myLowercaseString的实现里面又调用了eoc_myLowercaseString不会造成死循环,因为[self eoc_myLowercaseString]相当于[self lowercaseString],这两个方法已经交换啦。

这种方法交换不要滥用哈也是,因为交换是永久的,可能改变了一些默认行为,还是只用在黑盒调试比较好。

14. 理解类对象

id是没有类型的,但是它本身已经是指针啦,所以可以id str = @"ssss"是不会报错滴。

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}

typedef struct objc_class *Class;

// Class类
struct objc_class {
    // metaClass 元类
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    // 父类
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

isa:是一个Class 类型的指针. 每个实例对象有个isa的指针,他指向对象的类,而Class里也有个isa的指针, 指向meteClass(元类)。元类保存了类方法的列表。当类方法被调用时,先会从本身查找类方法的实现,如果没有,元类会向他父类查找该方法。

同时注意的是:元类(meteClass)也是类,它也是对象。元类也有isa指针,它的isa指针最终指向的是一个根元类(root meteClass).根元类的isa指针指向本身,这样形成了一个封闭的内循环。

Objective-C对象主要分为以下3类:
  1> instance对象(实例对象)
  2> class对象(类对象)存储实例方法列表等信息
  3> meta-class对象(元类对象)存储类方法列表等信息

object的Class是ClassXXX,ClassXXX也是对象,它的Class就是metaClass。

注意,一个类只会有一个类对象,一个元类对象,可以有多个实例对象。也就是说Class和metaClass是单例哈。所以可以用[object Class] == [NSString Class]来判断是不是string~

如果我们发送消息给一个object:[object message];
它运行原理便成了,运行时通过object的isa指针,找到对应的class,因为class里维护这方法列表及superClass的isa,如果class本身的方法列表里没有找到message方法,便继续通过superClass的isa往上查找,直到NSObject根,如果依然没用,就会进行消息转发(这里不详述),最后如果转发失败,就会崩溃。

我们前面说过,class也是object,因此,当类方法执行时,就会通过类的isa指针,去MetaClass里找对应方法,具体流程同上面object描述。

获取元类可以用下面的方式哈:

// 必需要传入类对象才能获取元类对象
NSLog(@"meta-class: %p", object_getClass([obj class]));
// 通过类名获取元类对象
NSLog(@"objcMetaClass: %p", objc_getMetaClass(className));

可参考:https://www.cnblogs.com/xgao/archive/2018/09/28/9708163.html
https://blog.csdn.net/zyx196/article/details/50780602


尽量使用类型信息查询方法(isMemberOfClass、isKindOfClass),而不应该直接比较两个类对象是否等同,因为前者可以正确处理那些使用了消息传递机制的对象。

比方说,某个对象可能会把其收到的所有选择子都转发给另外一个对象。这样的对象叫做 代理类,此种对象均以 NSProxy 为根类。

通常情况下,如果在此种代理对象上调用 class 方法,那么返回的是代理类本身,而非代理类转发到的真正接收消息的类。然而,若是改用 “isKindOfClass:” 这样的类型信息查询方法,那么代理类就会把这条消息转给 “接受代理的对象”(proxied object)。也就是说,这条消息的返回值与直接在接受代理的对象上面查询其类型所得的结果相同。因此,这样查出来的类对象与通过 class 方法所返回的那个类对象不同,class 方法所返回的类是代理类,而非真正处理代理类转发的消息的对象类。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容