0%

分类的原理

对于分类的作用恐怕大家都是知道的吧,今天就让我们一起研究一下分类的实现原理。

首先创建一个person类,然后在创建person类的两个分类Person+eat&Person+Run
研究原理我们的思路就是:

  • 1、生成c++文件,查看c++文件中的实现
  • 2、如果c++文件中实现介绍的不太具体就去查看源码实现

我们使用xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+eat.m来生成c++代码

我们可以找到分类都包含了哪些东西

1
2
3
4
5
6
7
8
struct _category_t {
const char *name;
struct _class_t *cls;
const struct _method_list_t *instance_methods;
const struct _method_list_t *class_methods;
const struct _protocol_list_t *protocols;
const struct _prop_list_t *properties;
};

我们发现里面并没有对方法属性协议等等的具体实现过程,那么我们在去源码中查看一下相关实现过程。

源码解读顺序

  • 1、objc-os.mm(runtime初始化的代码)
    • _objc_init
    • map_images
    • map_images_nolock
  • 2、objc-runtime-new.mm
    • _read_images
    • remethodizeClass
    • attachCategories
    • attachLists
    • realloc、memmove、 memcpy

我们按照源码查找一路找到attachCategories方法,我们发现这个方法就是对分类的实现。里面第一句解释Attach method lists and properties and protocols from categories to a class.将方法列表、属性和协议从类别附加到类中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);

bool isMeta = cls->isMetaClass();

//方法数组,这是一个二维数组
/*
[
[method_t,method_t],
[method_t,method_t]
]
*/
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists));
//属性数组,这是一个二维数组
property_list_t **proplists = (property_list_t **)
malloc(cats->count * sizeof(*proplists));
//协议数组,这是一个二维数组
protocol_list_t **protolists = (protocol_list_t **)
malloc(cats->count * sizeof(*protolists));

int mcount = 0;
int propcount = 0;
int protocount = 0;
int i = cats->count;
bool fromBundle = NO;
while (i--) {
//取出某个分类
auto& entry = cats->list[i];
//取出分类里面的方法列表
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
mlists[mcount++] = mlist;
fromBundle |= entry.hi->isBundle();
}

property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
proplists[propcount++] = proplist;
}

protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
protolists[protocount++] = protolist;
}
}
//得到对象里面的数据
auto rw = cls->data();

prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
//将所有分类的对象方法,附加到类对象的方法列表中
rw->methods.attachLists(mlists, mcount);
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls);
//将所有分类的属性,附加到类对象的属性列表中
rw->properties.attachLists(proplists, propcount);
free(proplists);
//将所有分类的协议,附加到类对象的协议列表中
rw->protocols.attachLists(protolists, protocount);
free(protolists);
}

我们发现rw->methods.attachLists(mlists, mcount);方法是实现将所有分类的对象方法,附加到类对象的方法列表中,其他两个属性和协议都是调用这个方法,我们分析一个就可以了。

点击进入attachLists方法,里面有一段实现代码

1
2
3
4
5
6
7
8
9
10
11
if (hasArray()) {
// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
//array()->list 原来的方法列表
memmove(array()->lists + addedCount, array()->lists, oldCount * sizeof(array()->lists[0]));
//addedList 所有分类的方法列表
memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0]));
}
  • 1、扩容,把类中的方法数组和分类中的方法数组计算出来
  • 2、memmove把类中方法放到数组的最后一位
  • 3、memcpy把分类中的方法放到数组的前面。

KVC

KVC(Key-value coding)键值编码,指iOS的开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值而不需要调用明确的存取方法。

1、KVC中常见方法

我们随便点击进入setValue:forKey方法,我们可以发现里面的方法基本上都是基于NSObjectNSKeyValueCoding分类写的,所以对于所有继承了NSObject的类型,也就是几乎所有的Objective-C对象都能使用KVC(一些纯Swift类和结构体是不支持KVC的),下面是KVC最为重要的四个方法:

1
2
3
4
- (nullable id)valueForKey:(NSString *)key;                          //直接通过Key来取值
- (void)setValue:(nullable id)value forKey:(NSString *)key; //通过Key来设值
- (nullable id)valueForKeyPath:(NSString *)keyPath; //通过KeyPath来取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; //通过KeyPath来设值

NSKeyValueCoding类别中还有其他的一些方法,这些方法在碰到特殊情况或者有特殊需求还是会用到的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//默认返回YES,表示如果没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索
+ (BOOL)accessInstanceVariablesDirectly;

//KVC提供属性值正确性验证的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;

//这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;

//如果Key不存在,且没有KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。
- (nullable id)valueForUndefinedKey:(NSString *)key;

//和上一个方法一样,但这个方法是设值。
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;

//如果你在SetValue方法时面给Value传nil,则会调用这个方法
- (void)setNilValueForKey:(NSString *)key;

//输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

2、KVC的内部实现机制

KVC的setValue:forKey原理

我们先来一张图片可以直接明了的看清楚实现原理

  • 1、按照setKey_setKey的顺序查找成员方法,如果找到方法,传递参数,调用方法
  • 2、如果没有找到,查看accessInstanceVariablesDirectly的返回值(accessInstanceVariablesDirectly的返回值默认是YES),
    • 返回值为YES,按照_Key,_isKey,Key,isKey的顺序查找成员变量,如果找到,直接赋值,如果没有找到,调用setValue:forUndefinedKey:,抛出异常
    • 返回NO,直接调用setValue:forUndefinedKey:,抛出异常

KVC的ValueforKey原理

  • 1、按照getKey,key,isKey,_key的顺序查找成员方法,如果找到直接调用取值
  • 2、如果没有找到,查看accessInstanceVariablesDirectly的返回值
  • 返回值为YES,按照_Key,_isKey,Key,isKey的顺序查找成员变量,如果找到,直接取值,如果没有找到,调用setValue:forUndefinedKey:,抛出异常
  • 返回NO,直接调用setValue:forUndefinedKey:,抛出异常

3、KVC的使用

KVC基础使用

假设我们有一个Person类,里面有一个age属性,我们给age赋值和取值

1
2
3
4
5
Person *p = [[Person alloc] init];
//赋值
[p setValue:@10 forKey:@"age"];
//取值
[p valueForKey:@"age"];

这也是最简单的使用方法了,也是我们平时项目中最常使用的方法了。

KVC中使用keyPath

但是当Person类里面有一个Student类,里面有一个height属性,我们怎么赋值height属性呢,

1
2
3
4
5
6
7
@interface Person : NSObject

@property (nonatomic,assign) int age;

@property (nonatomic,strong) Student *stu;

@end

我们能否这样写呢

1
2
3
4
5
Person *p = [[Person alloc]init];
//赋值
[p setValue:@10 forKey:@"stu.height"];
//取值
[p valueForKey:@"stu.height"];

我们运行程序打印结果:

1
2
3
4
5
6
2020-06-27 15:41:13.085990+0800 KVC[2974:107108] *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<Person 0x600000d90160> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key stu.height.'
*** First throw call stack:
(
0 CoreFoundation 0x00007fff23c7127e __exceptionPreprocess + 350
1 libobjc.A.dylib 0x00007fff513fbb20 objc_exception_throw + 48
2 CoreFoundation 0x00007fff23c70e49 -[NSException raise] + 9

打印结果是this class is not key value coding-compliant for the key stu.height.,所以这个方法是不可以的,但是iOS为我们提供了另一个方法KeyPath:

1
2
3
4
5
Person *p = [[Person alloc]init];
p.stu = [[Student alloc]init];
[p setValue:@180 forKeyPath:@"stu.height"];
NSLog(@"valueForKey:%@",[p valueForKeyPath:@"stu.height"]);
NSLog(@"stu.height:%f",p.stu.height);

打印结果:

1
2
2020-06-27 15:43:31.258661+0800 KVC[3012:108720] valueForKey:180
2020-06-27 15:43:31.258805+0800 KVC[3012:108720] stu.height:180.000000

keyPath除了对当前对象的属性进行赋值外,还可以对其更“深层”的对象进行赋值。KVC进行多级访问时,直接类似于属性调用一样用点语法进行访问即可。

KVC之集合属性

如果我们想要修改集合类型,我们该怎么办呢,不要着急,系统还是很友好的给我们提供了一些方法

1
2
3
4
5
6
7
8
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath

- (NSMutableSet *)mutableSetValueForKey:(NSString *)key
- (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath

- (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key
- (NSMutableOrderedSet *)mutableOrderedSetValueForKeyPath:(NSString *)keyPath

简单使用

1
2
3
4
Person *p = [[Person alloc] init];
[[p mutableArrayValueForKey:@"list"] addObject:@"test"];
NSLog(@"mutableArrayValueForKey:%@",[p valueForKeyPath:@"list"]);
NSLog(@"%@",p.list);

关于mutableArrayValueForKey:的适用场景,网上一般说是在KVO中,因为KVO的本质是系统监测到某个属性的内存地址或常量改变时会添加上- (void)willChangeValueForKey:(NSString *)key- (void)didChangeValueForKey:(NSString *)key方法来发送通知,但是如果直接改数组的话,内存地址并没有改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- (void)viewDidLoad {
[super viewDidLoad];
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld;
_p = [[Person alloc]init];
[_p addObserver:self forKeyPath:@"list" options:options context:nil];

}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// [_p.list addObject:@(arc4random()%255)];
// NSLog(@"打印内存地址:%x",self.p.list);
// NSLog(@"打印内容:%@",self.p.list);

[[self.p mutableArrayValueForKey:@"list"] addObject:@(arc4random()%255)];
NSLog(@"打印内存地址:%x",self.p.list);
NSLog(@"打印内容:%@",self.p.list);

}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context{
NSLog(@"%@",change);
}

-(void)dealloc{
[_p removeObserver:self forKeyPath:@"list"]; //一定要在dealloc里面移除观察
}

我们分别用 [_p.list addObject:@(arc4random()%255)];[[self.p mutableArrayValueForKey:@"list"] addObject:@(arc4random()%255)];两个方法修改list内容,我们打印可知 [_p.list addObject:@(arc4random()%255)];方法并没有改变list的内存地址,而使用[[self.p mutableArrayValueForKey:@"list"] addObject:@(arc4random()%255)];list的内存地址改变了。

KVC之字典属性

KVC里面还有两个关于NSDictionary的方法

1
2
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;

dictionaryWithValuesForKeys:是指输入一组key,返回这组key对应的属性,再组成一个字典
setValuesForKeysWithDictionary是用来修改dic中对应key的属性

这个属性最常用到的地方就是字典转模型
例如我们有一个Student类,

1
2
3
4
5
@interface Student : NSObject
@property (nonatomic,assign) float height;
@property (nonatomic,assign) int age;
@property (nonatomic,strong) NSString *name;
@end

我们正常是怎么赋值呢

1
2
3
4
Student *stu = [[Student alloc]init];
stu.age = 10;
stu.name = @"jack";
stu.height = 180;

如果里面有100个属性呢,我们就需要写100遍。如果使用setValuesForKeysWithDictionary方法呢

1
2
3
4
Student *stu = [[Student alloc]init];
//在进行网络请求的时候dic不需要我们手写,是后台返回的
NSDictionary *dic = @{@"name":@"jack",@"height":@180,@"age":@10};
[stu setValuesForKeysWithDictionary:dic];

这样是不是简单了好多。

4、KVC异常处理

当根据KVC搜索规则,没有搜索到对应的key或者keyPath,则会调用对应的异常方法。异常方法的默认实现,在异常发生时会抛出一个NSUndefinedKeyException的异常,并且应用程序Crash。我们可以重写下面两个方法,根据业务需求合理的处理KVC导致的异常。

1
2
3
- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
- (void)setNilValueForKey:(NSString *)key;

其中重写这两个方法,在key值不存在的时候,会走下面方法,而不会异常抛出

1
2
- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;

重写这个方法,当value值为nil的时候,会走下面方法,而不会异常抛出

1
- (void)setNilValueForKey:(NSString *)key;

5、KVC的正确性验证

在调用KVC时可以先进行验证,验证通过下面两个方法进行,支持key和keyPath两种方式。验证方法默认实现返回YES,可以通过重写对应的方法修改验证逻辑。

验证方法需要我们手动调用,并不会在进行KVC的过程中自动调用

1
2
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKeyPath:(NSString *)inKeyPath error:(out NSError **)outError;

在validateValue方法的内部实现中,如果传入的value或key有问题,可以通过返回NO来表示错误,并设置NSError对象。

参考demo

KVO实现原理

KVO(key value observing)键值监听是我们在开发中常使用的用于监听特定对象属性值变化的方法,常用于监听数据模型的变化 。
KVO是为了监听一个对象的某个属性值是否发生变化。在属性值发生变化的时候,肯定会调用其setter方法。所以KVO的本质就是监听对象有没有调用被监听属性对应的setter方法
在学习实现原理之前我们首先先了解一下KVO常用的有哪些方法。

KVO常用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/*
注册监听器
监听器对象为observer,被监听对象为消息的发送者即方法的调用者在回调函数中会被回传
监听的属性路径为keyPath支持点语法的嵌套
监听类型为options支持按位或来监听多个事件类型
监听上下文context主要用于在多个监听器对象监听相同keyPath时进行区分
添加监听器只会保留监听器对象的地址,不会增加引用,也不会在对象释放后置空,因此需要自己持有监听对象的强引用,该参数也会在回调函数中回传
*/
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

/*
删除监听器
监听器对象为observer,被监听对象为消息的发送者即方法的调用者,应与addObserver方法匹配
监听的属性路径为keyPath,应与addObserver方法的keyPath匹配
监听上下文context,应与addObserver方法的context匹配
*/
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));

/*
与上一个方法相同,只是少了context参数
推荐使用上一个方法,该方法由于没有传递context可能会产生异常结果
*/
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

/*
监听器对象的监听回调方法
keyPath即为监听的属性路径
object为被监听的对象
change保存被监听的值产生的变化
context为监听上下文,由add方法回传
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context;

KVO简单实现

我们创建一个person对象,然后在里面添加一个age属性,我们就来观察一下age属性。
person对象

1
2
3
4
5
6
#import <Foundation/Foundation.h>

@interface Person : NSObject
@property (nonatomic,assign) NSInteger age;
@end

简单实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#import "ViewController.h"
#import "Person.h"
@interface ViewController ()

@property (nonatomic,strong) Person *p1;
@property (nonatomic,strong) Person *p2;

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

self.p1 = [[Person alloc]init];
self.p2 = [[Person alloc]init];
self.p1.age = 10;
self.p2.age = 20;

// 给person1对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.p1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.p1.age = arc4random()%100;
self.p2.age = arc4random()%100;
}

- (void)dealloc {
[self.p1 removeObserver:self forKeyPath:@"age" context:@"123"];
}

// 当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}

以上代码就是一个KVO的简单实现,但是我们有没有想过他的内部到底是怎样实现的呢,今天我们就来探究一下KVO的内部实现原理。

KVO的内部实现

探究一个对象底层实现最简单的办法就行打印一些对象信息,看看有什么改变,我们在给person1添加监听之前分别打印p1,p2的类信息。
代码实现:

1
2
3
4
5
6
7
NSLog(@"person1添加KVO监听之前 - %@ %@", object_getClass(self.p1), object_getClass(self.p2));

// 给person1对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.p1 addObserver:self forKeyPath:@"age" options:options context:@"123"];

NSLog(@"person1添加KVO监听之后 - %@ %@", object_getClass(self.p1), object_getClass(self.p2));

打印结果:

1
2
2020-06-23 12:17:41.812979+0800 KVO[28865:5328765] person1添加KVO监听之前 - Person Person
2020-06-23 12:17:41.814729+0800 KVO[28865:5328765] person1添加KVO监听之后 - NSKVONotifying_Person Person

我们根据结果看到,在添加KVO观察者之后p1的类对象由Person变成了NSKVONotifying_Person,虽然p1的类对象变成了NSKVONotifying_Person,但是我们在调用的时候感觉我们的p1的类对象还是Person,所以,我们可以猜测KVO会在运行时动态创建一个新类,将对象的isa指向新创建的类,新类是原类的子类,命名规则是NSKVONotifying_xxx的格式。KVO为了使其更像之前的类,还会将对象的class实例方法重写,使其更像原类。

查看P1内部方法是否改变

我们在发现p1的类对象由Person变成了NSKVONotifying_Person,那我们也随便打印一下PersonNSKVONotifying_Person内部方法都变成了什么。

打印一下方法名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- (void)printMethodNamesOfClass:(Class)cls {
unsigned int count;
// 获得方法数组
Method *methodList = class_copyMethodList(cls, &count);

// 存储方法名
NSMutableString *methodNames = [NSMutableString string];

// 遍历所有的方法
for (int i = 0; i < count; i++) {
// 获得方法
Method method = methodList[i];
// 获得方法名
NSString *methodName = NSStringFromSelector(method_getName(method));
// 拼接方法名
[methodNames appendString:methodName];
[methodNames appendString:@", "];
}

// 释放
free(methodList);

// 打印方法名
NSLog(@"%@ %@", cls, methodNames);
}

然后我们分别在KVO监听前后在分别打印一下p1的类对象:

1
2
3
4
5
6
7
NSLog(@"person1添加KVO监听之前的内部方法===");
[self printMethodNamesOfClass:object_getClass(self.p1)];
// 给person1对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.p1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
NSLog(@"person1添加KVO监听之后的内部方法===");
[self printMethodNamesOfClass:object_getClass(self.p1)];

打印结果:

1
2
3
4
2020-06-23 12:21:35.457729+0800 KVO[28868:5329249] person1添加KVO监听之前的内部方法===
2020-06-23 12:21:35.458094+0800 KVO[28868:5329249] Person setAge:, age,
2020-06-23 12:21:35.459171+0800 KVO[28868:5329249] person1添加KVO监听之后的内部方法===
2020-06-23 12:21:35.459334+0800 KVO[28868:5329249] NSKVONotifying_Person setAge:, class, dealloc, _isKVOA,

我们在来打印一些KVO监听前后setAge方法发生了什么改变,因为值得改变肯定是因为set方法导致的,所以我们打印一下setAge方法。methodForSelector可以打印方法地址,我们分别在KVO监听前后打印。

1
2
3
4
5
6
7
8
9
10
NSLog(@"person1添加KVO监听之前 - %p %p",
[self.p1 methodForSelector:@selector(setAge:)],
[self.p2 methodForSelector:@selector(setAge:)]);

// 给person1对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.p1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
NSLog(@"person1添加KVO监听之后 - %p %p",
[self.p1 methodForSelector:@selector(setAge:)],
[self.p2 methodForSelector:@selector(setAge:)]);

打印结果:

1
2
2020-06-23 12:25:12.925158+0800 KVO[28871:5329672] person1添加KVO监听之前 - 0x100192338 0x100192338
2020-06-23 12:25:12.925468+0800 KVO[28871:5329672] person1添加KVO监听之后 - 0x2269f0a38 0x100192338

我们可以利用lldb分别看一下具体的方法实现:

1
2
3
4
5
6
7
2020-06-23 12:25:12.925158+0800 KVO[28871:5329672] person1添加KVO监听之前 - 0x100192338 0x100192338
2020-06-23 12:25:12.925468+0800 KVO[28871:5329672] person1添加KVO监听之后 - 0x2269f0a38 0x100192338
(lldb) p (IMP)0x100192338
(IMP) $0 = 0x0000000100192338 (KVO`-[Person setAge:] at Person.h:12)
(lldb) p (IMP)0x2269f0a38
(IMP) $1 = 0x00000002269f0a38 (Foundation`_NSSetLongLongValueAndNotify)
(lldb)

根据以上总结,我们大概猜到在使用KVO前后对象的改变了。
未使用KVO监听的对象

使用KVO监听的对象

  • 1、重写class方法是为了我们调用它的时候返回跟重写继承类之前同样的内容。KVO底层交换了 NSKVONotifying_Person 的 class 方法,让其返回 Person。

  • 2、重写setter方法:在新的类中会重写对应的set方法,是为了在set方法中增加另外两个方法的调用。

    1
    2
    - (void)willChangeValueForKey:(NSString *)key
    - (void)didChangeValueForKey:(NSString *)key

    在didChangeValueForKey:方法再调用:

    1
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
  • 3、重写dealloc方法,销毁新生成的NSKVONotifying_类。

  • 4、重写_isKVOA方法,这个私有方法估计可能是用来标示该类是一个 KVO 机制声称的类。

_NSSetLongLongValueAndNotify

在添加KVO监听方法以后setAge方法变成了_NSSetLongLongValueAndNotify,所以我们可以大概猜测动态监听方法主要就是在这里面实现的。我们可以在终端使用命令来查看NSSet * ValueAndNotify的类型。

1
nm -a /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation | grep ValueAndNotify

我们可以在Person类中重写willChangeValueForKeydidChangeValueForKey,来猜测一下_NSSetLongLongValueAndNotify的内部实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)setAge:(NSInteger)age{
_age = age;
NSLog(@"调用set方法");
}

- (void)willChangeValueForKey:(NSString *)key{
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey");
}

- (void)didChangeValueForKey:(NSString *)key{
NSLog(@"didChangeValueForKey - begin");

[super didChangeValueForKey:key];

NSLog(@"didChangeValueForKey - end");
}

控制台打印的截图:

1
2
3
4
5
6
7
8
9
2020-06-24 22:48:59.344573+0800 KVO[1762:38744] willChangeValueForKey
2020-06-24 22:48:59.344766+0800 KVO[1762:38744] 调用set方法
2020-06-24 22:48:59.344915+0800 KVO[1762:38744] didChangeValueForKey - begin
2020-06-24 22:48:59.345209+0800 KVO[1762:38744] 监听到<Person: 0x6000018a4250>的age属性值改变了 - {
kind = 1;
new = 61;
old = 10;
} - 123
2020-06-24 22:48:59.345340+0800 KVO[1762:38744] didChangeValueForKey - end

根据打印结果我们可以推断_NSSetLongLongValueAndNotify内部实现为:
1.调用willChangeValueForKey方法。
2.调用setAge方法。
3.调用didChangeValueForKey方法。
4.在didChangeValueForKey方法内部调用oberser
observeValueForKeyPath: ofObject: change: context: 方法。

1
2
3
4
5
6
7
8
9
10
11
// 伪代码
void _NSSetIntValueAndNotify() {
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
}

- (void)didChangeValueForKey:(NSString *)key {
// 通知监听器,某某属性值发生了改变
[oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
}

面试题

讲了这些,我们来讨论面试题吧

1、iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)

  • 1、利用RuntimeAPI动态生成一个子类NSKVONotifying_XXX,并且让instance对象的isa指向这个全新的子类NSKVONotifying_XXX
  • 2、当修改对象的属性时,会在子类NSKVONotifying_XXX调用Foundation的_NSSetXXXValueAndNotify函数。
  • 3、在_NSSetXXXValueAndNotify函数中依次调用:
      - 1、willChangeValueForKey
      - 2、父类原来的setter
      - 3、didChangeValueForKey,didChangeValueForKey:内部会触发监听器(Oberser)的监听方法( observeValueForKeyPath:ofObject:change:context:)

2、如何手动触发KVO方法
手动调用willChangeValueForKeydidChangeValueForKey方法。

键值观察通知依赖于 NSObject 的两个方法: willChangeValueForKey:didChangeValueForKey。在一个被观察属性发生改变之前, willChangeValueForKey: 一定会被调用,这就会记录旧的值。而当改变发生后, didChangeValueForKey 会被调用,继而observeValueForKey:ofObject:change:context: 也会被调用。如果可以手动实现这些调用,就可以实现“手动触发”了有人可能会问只调用didChangeValueForKey方法可以触发KVO方法,其实是不能的,因为willChangeValueForKey: 记录旧的值,如果不记录旧的值,那就没有改变一说了。

3、直接修改成员变量会触发KVO吗
不会触发KVO,因为KVO的本质就是监听对象有没有调用被监听属性对应的setter方法,直接修改成员变量,是在内存中修改的,不走set方法。

4、不移除KVO监听,会发生什么

  • 不移除会造成内存泄漏。
  • 但是多次重复移除会崩溃。系统为了实现KVO,为NSObject添加了一个名为NSKeyValueObserverRegistration的Category,KVO的add和remove的实现都在里面。在移除的时候,系统会判断当前KVO的key是否已经被移除,如果已经被移除,则主动抛出一个NSException的异常。

参考demo

OC对象的分类

OC对象主要可以分为3种

  • 1、instance对象(实例对象):instance实例对象就是通过alloc出来的对象,每次调用alloc都会产生新的instance对象
  • 2、class对象(类对象):每个类在内存中有且只有一个类对象
  • 3、meta-class对象(元类对象):每个类在内存中有且只有一个元类对象

实例对象的存储信息

  • isa指针
  • 其他成员变量

类对象的存储信息

  • isa指针
  • superclass指针
  • 类的属性信息(@property),类的对象方法信息(instance method),类的协议信息(protocol),类的成员变量信息(ivar)

元类的存储信息

  • isa指针
  • superclass指针
  • 类的类方法信息(class method)

元类和类的存储结构是一样的,但是用途不一样

  • instance的isa指向class,当调用对象方法时,通过instance的isa找到class,最后找到对象方法的实现进行调用
  • class的isa指向meta-class,当调用类方法时,通过class的isa找到meta-class,最后找到类方法

总览图

  • 1、instance的isa指向class
  • 2、class的isa指向meta-class
  • 3、meta-class的isa指向基类的meta-class
  • 4、class的superclass指向父类的class,如果没有父类,superclass指向nil
  • 5、meta-class的superclass指向父类的meta-class,基类的meta-class的superclass指向基类的class
  • 6、instance的调用轨迹:isa找class,方法不存在,就通过superclass找父类
  • 7、class调用类方法的轨迹:isa找到meta-class,方法不存在,就通过superclass找父类

一个NSObject对象占用多少内存

我们平时所编写的Objective-C代码,底层实现都是C/C++代码,

所以OC的面向对象都是基于C/C++的数据结构实现的

思考:OC对象主要是基于C/C++的什么数据结构实现的呢???

想要了解OC对象主要是基于C/C++的什么数据结构实现的,我们首先要做的就是将Objective-C代码转化为C/C++代码,这样我们才能清楚的看清是怎么实现的

然后我们打开终端,在命令行找到cd到文件目录,然后中输入:

1
2
clang -rewrite-objc main.m 

命令可以将main.m编译成C++的代码,改成不同的文件名,就会生成不同的c++代码
这是就生成了main.cpp这个c++文件,打开文件代码
查看该main.cpp最底下的main函数,

但是不同平台支持的代码肯定是不一样的,像平台有WindowsmaciOS,架构有模拟器(i386)、32bit(armv7)、64bit(arm64),我们使用iOS,他的架构现在基本上都是64bit(arm64)

1
2
xcrun  -sdk  iphoneos  clang  -arch  arm64  -rewrite-objc OC源文件  -o  输出的CPP文件
如果需要链接其他框架,使用-framework参数。比如-framework UIKit

在终端输入命令以后,我们会生成一个main.cpp文件,打开main.cpp文件文件,我们把main.cpp文件拉到最下面,我们会看到这样一段代码

1
2
3
4
5
6
7
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;


}
return 0;
}

这一段代码就是我们OC代码中的main函数的实现

1
2
3
4
5
6
7
int main(int argc, const char * argv[]) {
@autoreleasepool {

}
return 0;
}

这时我们在main函数写入这一段代码,然后我们点击进入,查看代码实现

1
NSObject *obj = [[NSObject alloc] init];

点击NSObject进入内部,可以看到NSObject底层实现

1
2
3
struct NSObject {
Class isa;
};

我们用NSObject_IMPL查找在c++文件中具体的实现

1
2
3
struct NSObject_IMPL {
Class isa;
};

我们再一次执行命令

1
xcrun  -sdk  iphoneos  clang  -arch  arm64  -rewrite-objc main.m

生成的C++代码为

1
2
3
4
5
6
7
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
NSObject *obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));

}
return 0;
}

有两个方法可以打印内存大小

1
2
3
4
5
// 获得NSObject实例对象的成员变量所占用的大小  
NSLog(@"%zd", class_getInstanceSize([NSObject class]));

// 获得obj指针所指向内存的大小
NSLog(@"%zd", malloc_size((__bridge const void *)obj));

打印结果

一个OC对象在内存中是怎么样布局的呢

我们在C++文件中找到NSObject的实现
OC代码

1
2
3
struct NSObject {
Class isa;
};

c++代码

1
2
3
struct NSObject_IMPL {
Class isa;
};

我们知道一个指针是8个字节,但是NSObject对象打印16个字节,他们是怎么样布局的呢

我们可以根据内存地址实时查看内存分配情况Debug -> Debug Workfllow -> View Memory (Shift + Command + M)

菜单选项如何查看内存:

输入内存地址:

通过Xcode查看内存数据:

通过LLDB命令查看内存数据:

我们也可以直接使用 LLDB命令来查看内存地址
常用LLDB命令

  • print、p:打印

  • po:打印对象

  • 读取内存

    • memory read/数量格式字节数 内存地址
    • x/数量格式字节数 内存地址(格式:x是16进制,f是浮点,d是10进制;字节大小
      :b:byte 1字节,h:half word 2字节,w:word 4字节,g:giant word 8字节)
  • 修改内存中的值(memory write 内存地址 数值 memory write 0x0000010 10)

问题1:假设我创建一个Animal类,里面有age,weight两个属性,那么他的内存是多大呢?

1
2
3
Animal *animal = [[Animal alloc] init];
animal.age = 10;
animal.weight = 20;

我们先执行命令,查看一下c++源码

1
2
3
4
5
struct Animal_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
int _weight;
};

我们在知道结果之前大概猜猜内存是多大呢?16,24,32…

猜16字节的猜对了,我们先看看结果

我们用LLDB命令打印一下

1
2
3
4
5
6
7
(lldb) po animal
<Animal: 0x1005c23c0>

(lldb) memory read 0x1005c23c0
0x1005c23c0: 81 13 00 00 01 80 1d 00 0a 00 00 00 14 00 00 00 ................
0x1005c23d0: a0 24 5c 00 01 00 00 00 e0 26 5c 00 01 00 00 00 .$\......&\.....
(lldb)

为什么会是0a 00 00 0014 00 00 00呢,而不是00 00 00 0a00 00 00 14,这个就要考虑大端小端,具体概念自己可以去查。

但是为什么会是16个字节呢,因为int类型占用4个字节,两个int类型8个字节,一个isa8个字节,因为刚刚占满16个字节,对象就没有在开辟新的空间了

如果在多一个feetCount会占用几个字节呢

占用32个字节,大家是不是很惊讶,没有猜到

其实这又要提到一个新的知识点了内存对齐,我们知道OC对象就是C++结构体,而结构体的大小必须是最大成员大小的倍数,当在多了一个feetCount以后,内存不够用了,然后就需要扩展了。

如果是这样呢,占用内存是多少

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@interface Animal : NSObject

@property (nonatomic, assign) int age;
@property (nonatomic, assign) int weight;

@end

@implementation Animal

@end

@interface Cat : Animal
@property (nonatomic, assign) int feetCount;
@end

@implementation Cat

@end

Cat继承自Animal

我们生成C++代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Cat_IMPL {
struct Animal_IMPL Animal_IVARS;
int _feetCount;
};

struct Animal_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _weight;
int _age;
};

struct NSObject_IMPL {
Class isa;
};

整理一下就是这样

1
2
3
4
5
6
struct Cat_IMPL {
Class isa;
int _weight;
int _age;
int _feetCount;
};

参考demo

科学上网

http://gufenso.coderschool.cn/ (谷粉搜导航,可以临时用一下谷歌搜索)

https://coderschool.cn/2366.html (谷歌镜像网站链接收集,有广告)

链接: https://pan.baidu.com/s/1gePw35p 密码: kw1k (Chrom的饭前插件,注册一个账号,可以免费使用)

https://github.com/XX-net/XX-Net (这个配置有些麻烦,不过用起来还不错,免费)

https://google.jiongjun.cc/user/miror.html (廉价VPS,还有搬瓦工优惠码)

https://blog.kuoruan.com/48.html (搬瓦工搭建Shadowsocks并优化速度)

https://github.com/iMeiji/shadowsocks_install/blob/master/shadowsocksR-wiki/config.json.md (SSR配置文件更改)

https://github.com/racaljk/hosts/blob/master/README.md (Hosts文件)

谷歌镜像网站

https://www.uedbox.com/post/54776/ (体验盒子)
http://gufenso.coderschool.cn/ (谷粉搜导航,可以临时用一下谷歌搜索)

Vultr搭建VPS相关教程:

https://www.jiongjun.cc/vultr/315.html (购买和搭建VPS详细教程)

http://yucc.me/p/b4d20b2d/ (简略版教程)

Shadowsocks 客户端下载:

https://lvii.gitbooks.io/outman/content/ss.mac.html

https://github.com/shadowsocks/ShadowsocksX-NG/releases/ (MAC版客户端下载地址)

资源搜索:

https://www.panc.cc/ 胖次(最新的电影资源,会抽风)

https://www.quzhuanpan.com/ 去转盘 (最新的电影,综艺)

https://search.chongbuluo.com/ 虫部落 (整理非常好的搜索站)

http://www.torrent.org.cn (种子和磁链互相转换的网站)

BT之家

BT之家

BT之家

不求人导航

人人影视分享站

资源下载:

Aria2c

bt-tracker数据:https://github.com/ngosang/trackerslist/blob/master/trackers_all_udp.txt

书籍下载

https://elib.cc/ (每天限制下载一本书)
https://b-ok.cc/?signAll=1&ts=0723 (电子书下载)
https://ebook.huzerui.com (熊猫搜书)
https://ebook.ipfs-lab.com(Ebook)
https://obook.cc(偶书)
https://yabook.org(雅书)
https://ebook2.lorefree.com(Lore Free)
https://www.jiumodiary.com(鸠摩搜索)
https://bks.thefuture.top(thefuture 书籍搜索)
https://new.shuge.org(书格)

计算机免费书籍GitHub合集:

justjavac/free-programming-books-zh_CN(免费的编程中文书籍索引)

iOS 开发:

https://github.com/Tim9Liu9/TimLiu-iOS (iOS Demo,教程等等)

https://github.com/Aufree/trip-to-iOS (偏向于教程)

https://github.com/jobbole/awesome-ios-cn (iOS 资源大全中文版,Demo比较全)

https://github.com/Lax/iOS-Swift-Demos (Swift 学习资源)

https://github.com/leecade/ios-dev-flow (iOS 开发证书相关)

https://github.com/Draveness/analyze (iOS开源框架源代码解析)

https://github.com/zhouhuanqiang/LearningIOS (iOS视频教程)

http://bbs.itheima.com/thread-338489-1-1.html

https://zsisme.gitbooks.io/ios-/content/index.html (iOS核心动画)

https://nshipster.cn/

https://www.agoniblog.com/article/75 (有很多视频教程)

Vue 开发:

vuejs/awesome-vue(vue 学习资料)

硅谷外卖视频课程以及很多免费课程

iPhone机型介绍:

https://support.apple.com/zh-cn/HT201296 (iPhone各种机型的详细介绍)

https://www.theiphonewiki.com/wiki/Models (iOS各种设备的Model的介绍)

Android开发:

http://bbs.itheima.com/thread-338469-1-1.html

VR开发:

https://github.com/GeekLiB/Lee-VR-Source

工具网站:

http://www.iconfont.cn/ (阿里巴巴矢量图标库)

https://github.com/shengxinjing/programmer-job-blacklist (程序员找工作黑名单)

设计类网站:

http://www.xueui.cn

https://magdeleine.co (免费高清摄影图片下载)

http://blog.cocoachina.com/article/73408 (8个免费UI图标网站)

无版权图片网站:

VisualHunt
Unsplash
免费素材图片Pexels
Free Images
FreeImages
Superfamous Images
picjumbo: Free Stock Photos
IM Creator
Gratisography
Morguefile
Little Visuals
New Old Stock
Getrefe
jay mantri
Public Domain Images
Magdeleine
LibreShot
Kaboompics
Free Travel Photos
Sharing beautiful travel moments
Free Food Photos
Cupcake
Moveast
stokpic
SplitShire
Start Overr
Free Stock Photos and Images
The Stocks
Pixelmob
Free Stock Photos
Foter Magazine
FindA.Photo
NegativeSpace
Free Stock Photos
Free Stock Photos
free stock photos
Free Stock Photos
Picography
FOCA Stock
Realistic Shots
Life of Pix

逆向工程与加密解密:

https://pan.baidu.com/pcloud/album/info?uk=1745354485&album_id=3855093648109743244 (百度网盘分享链接)

https://www.hackfun.org/kali-tools/kali-tools-zh.html (Kali工具说明书)

http://www.he11oworld.com/tag/shentoujiaocheng/page/2/ (渗透教程下载)

http://daily.zhihu.com/story/3877456 (安全网站列表)

社区:

伯乐在线 (https://github.com/jobbole)

https://www.v2ex.com/go/idev

临时手机号接码网站汇总

https://www.receiveasms.com/
https://smsreceivefree.com/
https://getfreesmsnumber.com/
https://sms-online.co/receive-free-sms
http://sms-receive.net/
https://www.receivesmsonline.net/
http://7sim.net/
http://receivefreesms.com/
https://receivefreesms.net/
https://receivesmsonline.in/
https://receive-sms-online.com/
https://www.smsver.com/
http://sms.sellaite.com/
https://www.freeonlinephone.org/
https://smsget.net/free/
http://freereceivesmsonline.com/
https://es.mytrashmobile.com/numeros
https://sms24.me

临时邮箱

用于网站以及app注册,薅羊毛等,免费功能足够日常使用,其中有一些邮箱提供更高级功能,在这里分两类:
第一类为可自定义邮箱前后缀,有七个:
https://temp-mail.io/zh (提供邮件转发服务、查看邮件历史记录)
https://mail.td (提供邮件转发服务)
http://24mail.chacuo.net (提供邮件转发服务、伪造邮件地址、邮箱真实性检测、邮件ip查询等)
https://temp-mail.org (高级会员可修改后缀、自定义后缀)
https://linshiyouxiang.net
https://www.guerrillamail.com/zh
https://www.moakt.com/zh

第二类为只能自定义邮箱前缀,有三个:
https://yopmail.com/zh (可以使用系统自动分配的电子邮件别名)
https://mail.mjj.edu.ge (可以获得部分教育优惠)
https://www.linshi-email.com

廉价域名

https://www.freenom.com/ (免费)
https://www.namesilo.com/ (支持支付宝付款)
https://www.namecheap.com/ (信用卡付款)

在线格式转换

https://www.alltoall.net/ (PDF转WORD等多种文件转换)

在线抠图

https://picwish.cn/ (在线抠图、去水印)
https://tinypng.com/ (在线压缩图片)
https://convertio.co/(任意文件格式转换)

匿名网盘

1.Sql.gg
此网盘内置图床以及类似pastebin的文字传输服务,同样支持文件,文件数量不限,单文件大小最大10g,上传与下载流量速度不限制。该网盘甚至提供镜像网站以及tor网站方便各国用户使用

2.catbox
单文件最大上传限制200M,不注册账户不保存记录,注册登录保存上传记录并可以创建分享专辑,可以分享多文件,上传速度快,下载速度一般,配合IDM下载速度可提升。
网站不允许上传可执行文件以及色情等内容。

3.Uploadfiles
该网盘一次最多上传10 个文件,每个文件 最大5GB,商业用户最大文件大小为 100GB。作为访客,上传的文件最多可托管 30 天。永久托管需注册用户。文件类型没有限制。

4.AnonFiles
老牌的匿名网盘,不限制上传下载速度,文件最大上传量20G

5.file.io
该网盘特点在于访客阅后即焚,上传的文件下载成功,文件就会从服务器删除,下载链接无法访问,注册用户可自定义文件的到期时间以及最大下载量,达到设定值文件即销毁,单个文件大小最大100M

25个资源网站,让你永久告别资源付费

视频类素材:mixkit、videvo、Pexels videvo、33台词网和找台词、预告片世界
图片类素材:Pixabay、ColorHub、Hippopx、LOGO神器
音频素材:FreePD、淘声网、耳聆网
PPT素材:51PPT、优品PPT、OfficePlus
配色素材:中国色、trianglify、colorhunt
效率工具:remove bg、Bigjpg、docsmall、pdfpai、magicm ockups
全能导航:奇迹秀、万能导航

Apple ID 相关

小火箭免费账号

注册 App Store 美区ID的方法↓

独立开发

产品变现周刊
远程工作者

Python3中的字符串是Unicode字符串而不是字节数组。这是与Python2相比最大的差别。在Python2中,我们需要区分普通的字节为单位的字符串以及Unicode字符串。

使用UTF-8编码和解码

对字符串进行处理时,并不需要在意Python中Unicode字符的存储细节。当需要与外界进行数据交互时则需要完成两件事情:

  • 将字符串__编码__为字节
  • 将字节__解码__为字符串

如果Unicode包含的字符种类不超过64000种,我们就可以将字符ID统一存储在2字节中。遗憾的是,Unicode所包含的字符种类远不只此。诚然,我们将字符ID统一编码在3或4字节中,但是这样会使空间开销(内存和硬盘)增加3到4倍。两位Unix开发大神设计出了UTF-8动态编码方案。这种方案会动态的为每个Unicode字符分配1到4字节不等:

  • 为ASCII字符分配1字节
  • 为拉丁语系(除西里尔语)的语言分配2字节
  • 为其他的位于基本多语言平面的字符分配3字节
  • 为剩下的字符集分配4字节,这包括一些亚洲语言及符号

UTF-8是Python、Linux以及HTML的标准文本编码格式。

编码

__编码是将字符串转化为一系列字节的过程__。字符串的__encode()__函数所接收的第一个参数是编码方式。可选的编码方式见下表。

下面使用代码进行说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 定义一个Unicode字符串,它含有一个中文字符
name = "中"
# 打印字符串的类型
print(type(name))
# 打印字符串
print(name)
# 打印字符串的长度,最后结果为1,说明len()获取的是字符串中字符的个数,不是字节长度
print(len(name))

# 将字符串编码为UTF-8的字节序列
ds = name.encode('utf-8')
print("--------")

# 打印字节序列的类型
print(type(ds))
# 打印字符串编码过后的字节序列
print(ds)
# 打印字节序列的长度,发现一个中文字符在UTF-8编码中占3个字节
print(len(ds))

程序的运行结果如下:

1
2
3
4
5
6
7
<class 'str'>

1
--------
<class 'bytes'>
b'\xe4\xb8\xad'
3

由上面的运行结果可以知道,单个的Unicode中文字符在UTF-8编码的格式下占用了3字节的空间。当然,你也可以使用UTF-8以外的编码方式,但该Unicode字符串可能无法被指定的编码方式处理,此时Python会抛出异常。例如将上面的Unicode中文字符编码为ascii字节,就会报错。

1
2
3
name = '中'
ds = name.encode('ascii')
print(ds)

运行结果:

1
2
3
4
Traceback (most recent call last):
File "/Users/kris/PycharmProjects/pythons_demo/Python语言及其应用/第七章/test.py", line 26, in <module>
ds = name.encode('ascii')
UnicodeEncodeError: 'ascii' codec can't encode character '\u4e2d' in position 0: ordinal not in range(128)

__encode()__函数可以接受额外的第二个参数来帮助你避免编码异常。它的默认值是’strict’,如上例所示,当函数检测到需要处理的字符串包含非ASCII字符时,会抛出UnicodeEncodeError异常。当然还有别的可选值,使用’ignore’会抛弃任何无法进行编码的字符;使用’replace’会将所有无法进行编码的字符替换为?;’backslashreplace’则会创建一个和Unicode-escape类似的Unicode字符串。

1
2
3
4
5
name = 'ab中cd'
print(name)
print(name.encode('ascii', 'ignore'))
print(name.encode('ascii', 'replace'))
print(name.encode('ascii', 'backslashreplace'))

程序的运行结果:

1
2
3
4
ab中cd
b'abcd'
b'ab?cd'
b'ab\\u4e2dcd'

解码

__解码__是将字节序列转化为Unicode字符串的过程。我们从外界文本源(文件、数据库、网站、网络API等)获得的所有文本都是经过编码的字节序列。重要的是需要知道它是以何种方式编码的,这样才能逆转编码过程以获得Unicode字符串。

问题是字节序列本身不带有任何指明编码方式的信息。之前我也提到过网站随意复制粘贴文本的风险,你也可能遇到过网页乱码的情况,本应是ASCII字符的位置却被奇怪的字符占据了,这些都是编码和解码的方式不一致导致的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
place = 'caf\u00e9'
print(place)
print(type(place))

# 编码
place_tytes = place.encode('utf-8')
print(place_tytes)
print(type(place_tytes))

# 解码
place2 = place_tytes.decode('utf-8')
print(place2)

# 使用ASCII解码,会报错
# place3 = place_tytes.decode('ascii')
# print(place3)

# 使用其他编码格式解码
place4 = place_tytes.decode('latin_1')
print(place4)

place5 = place_tytes.decode('windows-1252')
print(place5)

程序的运行结果:

1
2
3
4
5
6
7
café
<class 'str'>
b'caf\xc3\xa9'
<class 'bytes'>
café
café
café

上面的运行结果说明,

使用class定义类

Python中使用class关键字来定义类,下面定义一个Person类

1
2
3
4
5
6
7
8
class Person():
def __init__(self, name):
self.name = name


if __name__ == '__main__':
hunter = Person('Elmer Fudd')
print(hunter.name)

其中 hunter = Person(‘Elmer Fudd’) 这一行代码执行的操作有:

    1. 查看Person类的定义
    1. 在内存中创建一个新的对象
    1. 调用对象的__init__方法,将这个新创建的对象作为self传入,并将另一个参数(‘Elmer Fudd’)作为name传入
    1. 将name的值存入对象
    1. 返回这个新的对象
    1. 将名字hunter与这个对象关联
      在类的定义中,__init__方法并不是必须的。只有当需要区分该类创建的不同对象时,才需要指定__init__方法。

继承

创建两个类,Car类和Yugo类,Yugo类继承自Car类,同时,Yugo类覆盖了Car类的exclaim方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Car():
def exclaim(self):
print("I'm a Car!")


class Yugo(Car):
def exclaim(self):
print("I'm a Yugo! Much like a Car, but more Yugo-ish.")


if __name__ == '__main__':
give_me_a_car = Car()
give_me_a_yugo = Yugo()
give_me_a_car.exclaim()
give_me_a_yugo.exclaim()

程序运行结果:

1
2
I'm a Car!
I'm a Yugo! Much like a Car, but more Yugo-ish.

在子类中,可以覆盖任何父类的方法,包括__init__()。下面使用代码来说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Person():
def __init__(self, name):
self.name = name


class MDPerson(Person):
def __init__(self, name):
self.name = "Doctor " + name


class JDPerson(Person):
def __init__(self, name):
self.name = name + ", Esquire"


if __name__ == '__main__':
person = Person('Fudd')
doctor = MDPerson('Fudd')
lawyer = JDPerson('Fudd')
print(person.name)
print(doctor.name)
print(lawyer.name)

程序的运行结果:

1
2
3
Fudd
Doctor Fudd
Fudd, Esquire

子类可以添加父类中没有的方法。新增的方法子类对象可以调用,父类对象无法调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Car():
def exclaim(self):
print("I'm a Car!")


class Yugo(Car):
def exclaim(self):
print("I'm a Yugo! Much like a Car, but more Yugo-ish.")

def need_a_push(self):
print("A little help here?")


if __name__ == '__main__':
give_me_a_car = Car()
give_me_a_yugo = Yugo()
give_me_a_yugo.need_a_push()
give_me_a_car.need_a_push()

程序的运行结果:

1
2
3
4
5
A little help here?
Traceback (most recent call last):
File "/Users/kris/PycharmProjects/pythons_demo/Python语言及其应用/对象和类/创建类.py", line 41, in <module>
give_me_a_car.need_a_push()
AttributeError: 'Car' object has no attribute 'need_a_push'

使用super()从父类获得帮助。子类中想要调用父类的方法需要使用super()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person():
def __init__(self, name):
self.name = name


class EmailPerson(Person):
def __init__(self, name, email):
super().__init__(name)
self.email = email


if __name__ == '__main__':
bob = EmailPerson('Bob Frapples', 'bob@frapples.com')
print(bob.name)
print(bob.email)

在子类中定义__init__()方法时,父类的__init__()方法会被覆盖。因此在子类中,父类的初始化方法并不会被自动调用,我们必须显式调用。这样有什么好处呢?这样处理的好处在于子类在实例化的时候会经过父类,如果父类中对name属性进行了业务处理,子类也会体现出来,否则父类的name将来需要修改还需要修改一遍子类的实现。
程序运行结果:

1
2
Bob Frapples
bob@frapples.com

Python使用self参数来找到正确对象所包含的特性和方法。通过下面的例子,说明Python在调用对象方法背后实际做的工作。

1
2
3
4
5
6
7
8
9
class Car():
def exclaim(self):
print("I'm a Car!")


if __name__ == '__main__':
car = Car()
car.exclaim()
Car.exclaim(car)

Python在背后所做的事情:

    1. 查找car对象所属的类(Car)
    1. 把car对象作为self参数传给Car类所包含的exclaim()方法
      上面程序运行的结果:
      1
      2
      I'm a Car!
      I'm a Car!

      使用属性对特性进行访问和设置

      有一些面向对象的语言支持私有特性。这些特性无法从对象外部直接访问,我们需要编写getter和setter方法对这些私有特性进行读写操作。Python不需要getter和setter方法,因为Python里所有特性都是公开的。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      class Duck():
      def __init__(self, input_name):
      self.hidden_name = input_name

      def get_name(self):
      print('inside the getter')
      return self.hidden_name

      def set_name(self, input_name):
      print('inside the setter')
      self.hidden_name = input_name

      name = property(get_name, set_name)


      if __name__ == '__main__':
      fowl = Duck('Howard')
      print(fowl.name)
      fowl.get_name()
      fowl.name = 'Daffy'
      print(fowl.name)
      fowl.set_name('ZhangSan')
      print(fowl.name)
      这两个新方法在最后一行之前都与普通的getter和setter方法没有区别,而最后一行则把这两个方法定义为了name属性。property()的第一个参数是getter方法,第二个参数是setter方法。现在当你尝试访问Duck类对象的name属性时,get_name()会被自动调用,当然也可以显式调用get_name()方法,它就像普通的getter方法一样;当对name属性进行赋值的时候,set_name()方法会被调用,也可以显式调用set_name()方法。
      运行的结果如下:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      inside the getter
      Howard
      inside the getter
      inside the setter
      inside the getter
      Daffy
      inside the setter
      inside the getter
      ZhangSan

使用属性修饰符定义属性,@property 修饰符用于指示getter方法,@name.setter 用于指示setter方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Duck():
def __init__(self, input_name):
self.hidden_name = input_name

@property
def name(self):
print('inside the getter')
return self.hidden_name

@name.setter
def name(self, input_name):
print('inside the setter')
self.hidden_name = input_name

if __name__ == '__main__':
fowl = Duck('Howard')
print(fowl.name)
# fowl.get_name()
fowl.name = 'Daffy'
print(fowl.name)
# fowl.set_name('ZhangSan')
print(fowl.name)
print(fowl.hidden_name)
fowl.hidden_name = "lisi"
print(fowl.hidden_name)

仍然可以像之前一样访问属性一样访问name,但是这里没有了显式的get_name()和set_name()方法,所以我注掉了显式调用的代码,不然会报错。如果有人猜到在类的内部用的变量名是hidden_name,他仍然可以直接通过fowl.hidden_name进行读写操作。运行结果如下:

1
2
3
4
5
6
7
8
9
inside the getter
Howard
inside the setter
inside the getter
Daffy
inside the getter
Daffy
Daffy
lisi

使用名称重整保护私有变量

前面的Duck例子中,为了隐藏内部的变量,我们曾将其命名为hidden_name。其实,Python对那些需要刻意隐藏在类内部的特性有自己的命名规范:由连续的两个下划线开头(__)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Duck():
def __init__(self, input_name):
self.__name = input_name

@property
def name(self):
print('inside the getter')
return self.__name

@name.setter
def name(self, input_name):
print('inside the setter')
self.__name = input_name


if __name__ == '__main__':

fowl = Duck('Howard')
print(fowl.name)
fowl.name = 'Donald'
print(fowl.name)
# print(fowl.__name)
fowl.__name = "zhangsan"
print(fowl.name)
print(fowl._Duck__name)

这种命名规范本质上并没有把变量变成私有,但是Python确实将它的名字重整了,让外部的代码无法使用。最后一行打印可以成功绕过getter方法,但是这种命名重整能在一定程度上避免我们无意或有意的对变量进行直接访问。
运行结果:

1
2
3
4
5
6
7
8
inside the getter
Howard
inside the setter
inside the getter
Donald
inside the getter
Donald
Donald

方法的类型

  • 1.实例方法
  • 2.类方法(@classmethod)
  • 3.静态方法(@staticmethod)

有些数据(变量) 和函数(方法)是类本身的一部分,还有一些是由类创建的实例的一部分。在类的定义中,以self作为第一个参数的方法都是实例方法。他们在创建自定义类时最常见。实例方法的首个参数是self,当它被调用时,Python会把调用该方法的对象作为self参数传入。与之相对,类方法会作用于整个类,对类作出的任何改变会对它的所有实例对象产生影响。在类定义内部,用前缀修饰符@classmethod指定的方法都是类方法。与实例方法类似,类方法的第一个参数是类本身。在Python中,这个参数常被写作cls,因为全程class是保留字,在这里我们无法使用。下面的例子中,我们为A定义一个类方法来记录一共多少个类A的对象被创建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A():
count = 0
def __init__(self):
A.count += 1

def exclaim(self):
print("I'm an A!")
@classmethod
def kids(cls):
print("A has", cls.count, "little objects.")


if __name__ == '__main__':

easy_a = A()
breezy_a = A()
wheezy_a = A()
A.kids()
print(A.count)

上面的代码中,在kids()方法中,我们使用的是cls.count,它与A.count的作用一样。
运行结果:

1
2
A has 3 little objects.
3

类定义中的方法还存在第三种类型,它既不会影响类,也不会影响对象。他们出现在类的定义中仅仅是为了方便,这种类型的方法被称为静态方法,使用@staticmethod修饰,它既不需要self参数也不需要class参数。下面的例子中的静态方法是一则CoyoteWeapon的广告:

1
2
3
4
5
6
7
8
9
class CoyoteWeapon():
@staticmethod
def commercial():
print('This CoyoteWeapon has been brought to you by Acme')


if __name__ == '__main__':

CoyoteWeapon.commercial()

在这个例子中,我们甚至都不用创建任何CoyoteWeapon类的对象就可以调用这个方法,句法优雅不失风格!

特殊方法

Python中有些特殊的方法,这些特殊方法的名称以双下划线(__)开头和结束,有时也被称作__魔术方法__。
下面列出常见的魔术方法:

  • 1.和比较相关的魔术方法
  • 2.和数学相关的魔术方法
  • 3.其他种类的魔术方法

下面定义魔术方法__eq__()__str__()__repr__(),看看运行的结果如何。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Word():

def __init__(self, text):
self.text = text

def __eq__(self, other):
return self.text.lower() == other.text.lower()

def __str__(self):
return self.text

def __repr__(self):
return 'Word("'+self.text+'")'



if __name__ == '__main__':

first = Word('ha')
print(first)
print(first == Word('Ha'))

定义__eq__()魔术方法方便同一类型的对象进行比较。定义 __str__()魔术方法方便print方法打印对象的相关信息,定义 __repr__() 魔术方法方便控制台输出对象的相关信息。
程序的运行结果如下:

1
2
ha
True

问题

  • iOS中如何实现对一个对象的KVO?(KVO的本质是什么?)
    答: 当给一个对象添加KVO监听之后,iOS系统会修改这个对象的isa指针,将这个指针指向Runtime动态创建的这个对象的子类。子类拥有自己的set方法的实现,set方法内部会顺序调用willChangeValueForKey方法、父类的set方法、didChangeValueForKey方法,这样会触发observeValueForKeyPath:ofObject:change:context:监听方法。
  • 如何手动触发KVO?
    答: 被监听的属性值被修改时,就会触发KVO。如果想要手动触发KVO,则需要我们手动同时调用willChangeValueForKey方法和didChangeValueForKey方法,这两个方法缺一不可。
  • KVO执行的条件?
    答: 对象添加观察者(addObserver:forKeyPath:)、修改对象被监听的属性、对象在dealloc方法中移除观察者。

官方文档:

Key-Value Observing Implementation Details

Automatic key-value observing is implemented using a technique called isa-swizzling.

The isa pointer, as the name suggests, points to the object’s class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.

When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.

You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.
大致意思是:KVO使用isa-swizzling技术实现,对象添加观察者之后,isa指针会指向一个临时的类,不能依赖isa的值来判断类的关系,应该使用class方法来判断实例对象真实的类。
接下来使用代码来验证KVO的执行过程:
Person.h (Person.m中什么也没有)

1
2
3
4
5
6
7
8
9
10
11
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject

@property (nonatomic, assign) int age;

@end

NS_ASSUME_NONNULL_END

ViewController.m (ViewController.h中什么也没有)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#import "ViewController.h"
#import <objc/runtime.h>
#import "Person.h"

@interface ViewController ()
@property (nonatomic, strong) Person *p1;
@end

@implementation ViewController

- (void)dealloc {
// 移除观察者
[self.p1 removeObserver:self forKeyPath:@"age"];
}


- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

Person *p1 = [[Person alloc] init];
[self printMethods:object_getClass(p1)];
self.p1 = p1;
// 打印p1对象所属类的内部方法列表
NSLog(@"p1 class = %@", [p1 class]);

// 添加观察者
[p1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];

// 打印p1对象所属类的内部方法列表
[self printMethods:object_getClass(p1)];
NSLog(@"p1 class = %@", [p1 class]);
}

/** 打印类中所有的方法 */
- (void) printMethods:(Class)cls
{
unsigned int count ;
Method *methods = class_copyMethodList(cls, &count);
NSMutableString *methodNames = [NSMutableString string];
[methodNames appendFormat:@"%@ - ", cls];

for (int i = 0 ; i < count; i++) {
Method method = methods[i];
NSString *methodName = NSStringFromSelector(method_getName(method));

[methodNames appendString: methodName];
[methodNames appendString:@" "];

}

NSLog(@"%@",methodNames);
free(methods);
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"执行了监听方法:%@", change);
}

@end

运行结果:

1
2
3
4
2019-03-31 19:06:48.896565+0800 KVO测试Demo[29498:3594134] Person - setAge: age
2019-03-31 19:06:48.897033+0800 KVO测试Demo[29498:3594134] p1 class = Person
2019-03-31 19:06:48.897503+0800 KVO测试Demo[29498:3594134] NSKVONotifying_Person - setAge: class dealloc _isKVOA
2019-03-31 19:06:48.897638+0800 KVO测试Demo[29498:3594134] p1 class = Person

由此可见,对象p1添加观察者之后,p1的所属类由原来的Person类变成了NSKVONotifying_Person类,原来的Person类内部只实现了setAge:方法和age方法,新类NSKVONotifying_Person中有setAge:、class、dealloc、_isKVOA方法。经过分析判断,NSKVONotifying_Person是Person的子类,它重写了父类的setAge:和class方法,使NSKVONotifying_Person的class方法的返回值也是Person,隐藏了内部的实现。

如何手动触发KVO?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

Person *p1 = [[Person alloc] init];
[self printMethods:object_getClass(p1)];
self.p1 = p1;
// 打印p1对象所属类的内部方法列表
NSLog(@"p1 class = %@", [p1 class]);

// 添加观察者
[p1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];

// 打印p1对象所属类的内部方法列表
[self printMethods:object_getClass(p1)];
NSLog(@"p1 class = %@", [p1 class]);

// 手动触发KVO
[p1 willChangeValueForKey:@"age"];
[p1 didChangeValueForKey:@"age"];
}

执行结果:

1
2
3
4
5
6
7
8
2019-03-31 19:37:41.217175+0800 KVO测试Demo[29990:3611620] Person - setAge: age
2019-03-31 19:37:41.217409+0800 KVO测试Demo[29990:3611620] p1 class = Person
2019-03-31 19:37:41.217999+0800 KVO测试Demo[29990:3611620] NSKVONotifying_Person - setAge: class dealloc _isKVOA
2019-03-31 19:37:41.218145+0800 KVO测试Demo[29990:3611620] p1 class = Person
2019-03-31 19:37:41.218488+0800 KVO测试Demo[29990:3611620] 执行了监听方法:{
kind = 1;
new = 0;
}

如何关闭自动KVO?
Person.m

1
2
3
4
5
6
7
8
9
10
11
12
#import "Person.h"

@implementation Person

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"age"]) {
return NO;
}
return YES;
}

@end

当返回值为NO的时候,即使改变p1的值也无法触发KVO,此时要想触发KVO只有手动模式,手动调用willChangeValueForKey和didChangeValueForKey。

None作为布尔值和False是一样的,如何区分None和False?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
thing = None

if thing:
print("It's some thing.")
else:
print("It's no thing.")


if thing is None:
print("It's nothing")
else:
print("It's something")

# 你需要把None和不含任何值的空数据结构区分开来。0值的整型/浮点型、
# 空字符串('')、空列表([])、空元组((,))、空字典({})、
# 空集合(set())都等价于False,但是不等于None。


def is_none(thing):
if thing is None:
print("It's None")
elif thing:
print("It's True")
else:
print("It's False")

is_none(None)
is_none(True)
is_none(False)
is_none(0)
is_none(0.0)
is_none(())
is_none([])
is_none({})
is_none(set())

运行结果:

1
2
3
4
5
6
7
8
9
10
11
It's no thing.
It's nothing
It's None
It's True
It's False
It's False
It's False
It's False
It's False
It's False
It's False