iOS 基础知识之关联对象

iOS 基础知识之关联对象,第1张

associatedObject又称关联对象,把一个对象关联到另外一个对象身上,使两者能够产生联系。关联对象的使用场景最多的是给一个分类增加属性。通过objc_setAssociatedObject 和 objc_getAssociatedObject 方法来设置和获取关联对象。通过 objc_removeAssociatedObjects 移除关联对象。

关联对象并不是存储在被关联对象本身内存中,关联对象是通过AssociationsManager的全局类来做管理。

实现关联对象技术的核心对象有:

存储模型:

1、类拓展是匿名的分类。

2、类拓展可以为类添加属性和方法(类拓展添加的属性和方法是私有的)。

3、类拓展在编译时作为类的一部分编译,读取的时候直接读取ro(前提:如果是以h方法拓展的必须在该拓展的类中导入头文件)

1、类拓展是匿名的分类分析:

类拓展:

分类:

2、类拓展可以为类添加属性和方法分析:

以下也是类拓展。

3、类拓展是编译期加载到内存(ro)里面的,(前提:必须在该拓展的类中导入头文件)

导入头文件:

类拓展和分类的区别:

类拓展可以添加属性(实现ro,方法实现,以及带下划线的成员变量)和方法

注意:可以用runtime给分类添加属性

分类中用@property定义变量,只会生成变量的getter setter方法的声明,不能生成方法实现和带下划线的成员变量。

苹果提供的API和苹果内部的API有区别的。

内部的API加了一个下滑线的隔层目的:防止程序员直接 *** 作私有API。

点击objc_setAssociatedObject进入以下方法objc内部函数

以上是关联对象set方法。

下面是关联对象get方法取值的过程。

点击objc_getAssociatedObject进入objc系统方法。

为什么用关联对象策略:关联对象使用hash表存储,更容易管理,使用全局变量,很难维护。

分类加载进来的时候,对原始的数据做出改变,就会产生脏数据

class_rw_t 读取-编写

dirty memory 运行时会发生改变的内存 类结构被使用 昂贵

class_rw_ext_t

class_ro_t (ro 只读/存成员变量)

clean memory 加载后不会改变的内存 只读 可被移除

属性和成员变量的区别

基本数据类型:string int double float char

对象类型: xyclass xy

编译成对象的就是实例变量

{

UIButton yourButton;

int count;

id data;

}

@property(nonatomic, strong) UIButton myButton;

成员变量:yourButton count data

实例变量:yourButton data

属性:myButton

@property(nonatomic, copy) NSString name;

setName->objc_setProperty

上层都是set方法。所以下层统一objc_setProperty

ivar sel->IMP(重定向到objc_setProperty)

根据什么条件创建objc_setProperty?

IsCopy (不考虑strong)

copy有set方法 无copy修饰没有set方法

strong直接内存平移,直接赋值

实力方法、对象方法 在类里面 避免浪费内存

为什么有元类?

元类存储类方法

为什么元类获取类方法能拿到类的类方法?

元类获取类方法是获取元类的对象方法,而类的类方法就是元类的对象方法。

底层没有类方法,全是对象方法。

元类中为什么能找到类的对象方法?

类为什么能获取类方法的IMP?

寻找IMP过程就是方法的查找流程,即使找不到也会返回_objc_msgForward

为什么类的isa和元类的一样,而对象的isa不一样?

isa里面存储的类的信息,而类的isa只存储元类的地址。

BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]]; // 1 类方法

BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]]; // 0 类方法

BOOL re3 = [(id)[LGPerson class] isKindOfClass:[LGPerson class]]; // 0 类方法

BOOL re4 = [(id)[LGPerson class] isMemberOfClass:[LGPerson class]]; // 0 类方法

NSLog(@" re1 :%hhd\n re2 :%hhd\n re3 :%hhd\n re4 :%hhd\n",re1,re2,re3,re4);

我们可以在/usr/include/objc/objch 和 runtimeh 中找到对 class 与 object 的定义: typedef struct objc_class Class; typedef struct objc_object { Class isa; } id; Class 是一个 objc_class 结构类型的指针;而 id(任意对象) 是一个 objc_object 结构类型的指针,其第一个成员是一个 objc_class 结构类型的指针。注意这里有一关键的引申解读:内存布局以一个 objc_class 指针为开始的所有东东都可以当做一个 object 来对待!那 objc_class 又是怎样一个结构体呢?且看: struct objc_class { struct objc_class isa; struct objc_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; }; objc_class 结构体的各成员介绍如下: isa:是一个 objc_class 类型的指针,看到这里,想起我前面的引申解读了没?内存布局以一个 objc_class 指针为开始的所有东东都可以当做一个 object 来对待!这就是说 objc_class 或者说类其实也可以当做一个 objc_object 对象来对待!对象是对象,类也是对象,是不是有点混淆?别急,ObjC发明(or 重用)了一个术语来区分这两种不同的对象:类对象(class object)与实例对象(instance object)。OK,名称混淆的问题解决,下面我将使用这两个术语来区分不同的对象,而使用对象这一术语来泛指所有的对象。ObjC还对类对象与实例对象中的 isa 所指向的类结构作了不同的命名:类对象中的 isa 指向类结构被称作 metaclass,metaclass 存储类的static类成员变量与static类成员方法(+开头的方法);实例对象中的 isa 指向类结构称作 class(普通的),class 结构存储类的普通成员变量与普通成员方法(-开头的方法)。 super_class:一看就明白,指向该类的父类呗!如果该类已经是最顶层的根类(如 NSObject 或 NSProxy),那么 super_class 就为 NULL。 好,先中断一下其他类结构成员的介绍,让我们厘清一下在继承层次中,子类,父类,根类(这些都是普通 class)以及其对应的 metaclass 的 isa 与 super_class 之间关系: 规则一:类的实例对象的 isa 指向该类;该类的 isa 指向该类的 metaclass; 规则二:类的 super_class 指向其父类,如果该类为根类则值为 NULL; 规则三:metaclass 的 isa 指向根 metaclass,如果该 metaclass 是根 metaclass 则指向自身; 规则四:metaclass 的super_class指向父 metaclass,如果该 metaclass 是根 metaclass 则指向该metaclass 对应的类; 好吧,文字总是那么乏力,有图有真相! instance object,class,metaclass 的 isa 与 super_class 关系图 那么 class 与 metaclass 有什么区别呢? class 是 instance object 的类类型。当我们向实例对象发送消息(实例方法)时,我们在该实例对象的 class 结构的 methodlists 中去查找响应的函数,如果没找到匹配的响应函数则在该 class 的父类中的 methodlists 去查找(查找链为上图的中间那一排)。如下面的代码中,向str 实例对象发送 lowercaseString 消息,会在 NSString 类结构的 methodlists 中去查找 lowercaseString 的响应函数。 NSString str; [str lowercaseString]; metaclass 是 class object 的类类型。当我们向类对象发送消息(类方法)时,我们在该类对象的 metaclass 结构的 methodlists 中去查找响应的函数,如果没有找到匹配的响应函数则在该 metaclass 的父类中的 methodlists 去查找(查找链为上图的最右边那一排)。如下面的代码中,向 NSString 类对象发送 stringWithString 消息,会在 NSString 的 metaclass 类结构的 methodlists 中去查找 stringWithString 的响应函数。 [NSString stringWithString:@str]; 好,至此我们明白了类的结构层次,让我们接着看类结构中的其他成员。 name:一个 C 字符串,指示类的名称。我们可以在运行期,通过这个名称查找到该类(通过:id objc_getClass(const char aClassName))或该类的 metaclass(id objc_getMetaClass(const char aClassName)); version:类的版本信息,默认初始化为 0。我们可以在运行期对其进行修改(class_setVersion)或获取(class_getVersion)。 info:供运行期使用的一些位标识。有如下一些位掩码: CLS_CLASS (0x1L) 表示该类为普通 class ,其中包含实例方法和变量; CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法; CLS_INITIALIZED (0x4L) 表示该类已经被运行期初始化了,这个标识位只被 objc_addClass 所设置; CLS_POSING (0x8L) 表示该类被 pose 成其他的类;(poseclass 在ObjC 20中被废弃了); CLS_MAPPED(0x10L) 为ObjC运行期所使用 CLS_FLUSH_CACHE (0x20L) 为ObjC运行期所使用 CLS_GROW_CACHE (0x40L) 为ObjC运行期所使用 CLS_NEED_BIND (0x80L) 为ObjC运行期所使用 CLS_METHOD_ARRAY (0x100L) 该标志位指示 methodlists 是指向一个 objc_method_list 还是一个包含 objc_method_list 指针的数组; instance_size:该类的实例变量大小(包括从父类继承下来的实例变量); ivars:指向 objc_ivar_list 的指针,存储每个实例变量的内存地址,如果该类没有任何实例变量则为 NULL; methodLists:与 info 的一些标志位有关,CLS_METHOD_ARRAY 标识位决定其指向的东西(是指向单个 objc_method_list还是一个 objc_method_list 指针数组),如果 info 设置了 CLS_CLASS 则 objc_method_list 存储实例方法,如果设置的是 CLS_META 则存储类方法;

1当参数obj为Object实例对象

object_getClass(obj)与[obj class]输出结果一直,均获得isa指针,即指向类对象的指针。

2当参数obj为Class类对象

object_getClass(obj)返回类对象中的isa指针,即指向元类对象的指针;[obj class]返回的则是其本身。

3当参数obj为Metaclass类对象

object_getClass(obj)返回元类对象中的isa指针,因为元类对象的isa指针指向根类,所有返回的是根类对象的地址指针;[obj class]返回的则是其本身。

4obj为Rootclass类对象

object_getClass(obj)返回根类对象中的isa指针,因为跟类对象的isa指针指向Rootclass‘s metaclass(根元类),即返回的是根元类的地址指针;[obj class]返回的则是其本身。

总结:

经上面初步的探索得知,object_getClass(obj)返回的是obj中的isa指针;而[obj class]则分两种情况:一是当obj为实例对象时,[obj class]中class是实例方法:- (Class)class,返回的obj对象中的isa指针;二是当obj为类对象(包括元类和根类以及根元类)时,调用的是类方法:+ (Class)class,返回的结果为其本身。

更多: iOS面试题合集

KVO 的全称是 Key-Value Observing,俗称“键值观察/监听”,是苹果提供的一套事件通知机制,允许一个对象观察/监听另一个对象指定属性值的改变。当被观察对象属性值发生改变时,会触发 KVO 的监听方法来通知观察者。KVO 是在 MVC 应用程序中的各层之间进行通信的一种特别有用的技术。

KVO 和 NSNotification 都是 iOS 中观察者模式的一种实现。

KVO 可以监听单个属性的变化,也可以监听集合对象的变化。监听集合对象变化时,需要通过 KVC 的 mutableArrayValueForKey: 等可变代理方法获得集合代理对象,并使用代理对象进行 *** 作,当代理对象的内部对象发生改变时,会触发 KVO 的监听方法。集合对象包含 NSArray 和 NSSet 。

先创建一个类,作为要监听的对象。

监听实现

KVO 主要用来做键值观察 *** 作,想要一个值发生改变后通知另一个对象,则用 KVO 实现最为合适。斯坦福大学的 iOS 教程中有一个很经典的案例,通过 KVO 在 Model 和 Controller 之间进行通信。如图所示:

KVO 触发分为自动触发和手动触发两种方式。

如果是监听对象特定属性值的改变,通过以下方式改变属性值会触发 KVO:

如果是监听集合对象的改变,需要通过 KVC 的 mutableArrayValueForKey: 等方法获得代理对象,并使用代理对象进行 *** 作,当代理对象的内部对象发生改变时,会触发 KVO。集合对象包含 NSArray 和 NSSet 。

普通对象属性或是成员变量使用:

NSArray 对象使用:

NSSet 对象使用:

observationInfo 属性是 NSKeyValueObservingh 文件中系统通过分类给 NSObject 添加的属性,所以所有继承于 NSObject 的对象都含有该属性;

可以通过 observationInfo 属性查看被观察对象的全部观察信息,包括 observer 、 keyPath 、 options 、 context 等。

注册方法 addObserver:forKeyPath:options:context: 中的 context 可以传入任意数据,并且可以在监听方法中接收到这个数据。

context 作用:标签-区分,可以更精确的确定被观察对象属性,用于继承、 多监听;也可以用来传值。

KVO 只有一个监听回调方法 observeValueForKeyPath:ofObject:change:context: ,我们通常情况下可以在注册方法中指定 context 为 NULL ,并在监听方法中通过 object 和 keyPath 来判断触发 KVO 的来源。

但是如果存在继承的情况,比如现在有 Person 类和它的两个子类 Teacher 类和 Student 类,person、teacher 和 student 实例对象都对象的 name 属性进行观察。问题:

当 name 发生改变时,应该由谁来处理呢?

如果都由 person 来处理,那么在 Person 类的监听方法中又该怎么判断是自己的事务还是子类对象的事务呢?

这时候通过使用 context 就可以很好地解决这个问题,在注册方法中为 context 设置一个独一无二的值,然后在监听方法中对 context 值进行检验即可。

苹果的推荐用法:用 context 来精确的确定被观察对象属性,使用唯一命名的静态变量的地址作为 context 的值。可以为整个类设置一个 context ,然后在监听方法中通过 object 和 keyPath 来确定被观察属性,这样存在继承的情况就可以通过 context 来判断;也可以为每个被观察对象属性设置不同的 context ,这样使用 context 就可以精确的确定被观察对象属性。

context 优点:嵌套少、性能高、更安全、扩展性强。

context 注意点:

KVO 可以监听单个属性的变化,也可以监听集合对象的变化。监听集合对象变化时,需要通过 KVC 的 mutableArrayValueForKey: 等方法获得代理对象,并使用代理对象进行 *** 作,当代理对象的内部对象发生改变时,会触发 KVO 的监听方法。集合对象包含 NSArray 和 NSSet 。(注意:如果直接对集合对象进行 *** 作改变,不会触发 KVO。)

可以在被观察对象的类中重写 + (BOOL)automaticallyNotifiesObserversForKey:(NSString )key 方法来控制 KVO 的自动触发。

如果我们只允许外界观察 person 的 name 属性,可以在 Person 类如下 *** 作。这样外界就只能观察 name 属性,即使外界注册了对 person 对象其它属性的监听,那么在属性发生改变时也不会触发 KVO。

也可以实现遵循命名规则为 + (BOOL)automaticallyNotifiesObserversOf<Key> 的方法来单一控制属性的 KVO 自动触发,<Key> 为属性名(首字母大写)。

使用场景:

使用 KVO 监听成员变量值的改变;

在某些需要控制监听过程的场景下。比如:为了尽量减少不必要的触发通知 *** 作,或者当多个更改同时具备的时候才调用属性改变的监听方法。

由于 KVO 的本质,重写 setter 方法来达到可以通知所有观察者对象的目的,所以只有通过 setter 方法或 KVC 方法去修改属性变量值的时候,才会触发 KVO,直接修改成员变量不会触发 KVO。

当我们要使用 KVO 监听成员变量值改变的时候,可以通过在为成员变量赋值的前后手动调用 willChangeValueForKey: 和 didChangeValueForKey: 两个方法来手动触发 KVO,如:

NSKeyValueObservingOptionPrior (分别在值改变前后触发方法,即一次修改有两次触发)的两次触发分别在 willChangeValueForKey: 和 didChangeValueForKey: 的时候进行的。

如果注册方法中 options 传入 NSKeyValueObservingOptionPrior ,那么可以通过只调用 willChangeValueForKey: 来触发改变前的那次 KVO,可以用于在属性值即将更改前做一些 *** 作。

有时候我们可能会有这样的需求,KVO 监听的属性值修改前后相等的时候,不触发 KVO 的监听方法,可以结合 KVO 的自动触发控制和手动触发来实现。

例如:对 person 对象的 name 属性注册了 KVO 监听,我们希望在对 name 属性赋值时做一个判断,如果新值和旧值相等,则不触发 KVO,可以在 Person 类中如下这样实现,将 name 属性值改变的 KVO 触发方式由自动触发改为手动触发。

有些情况下我们想手动观察集合属性,下面以观察数组为例。

关键方法:

需要注意的是,根据 KVC 的 NSMutableArray 搜索模式:

有些情况下,一个属性的改变依赖于别的一个或多个属性的改变,也就是说当别的属性改了,这个属性也会跟着改变。

比如我们想要对 Download 类中的 downloadProgress 属性进行 KVO 监听,该属性的改变依赖于 writtenData 和 totalData 属性的改变。观察者监听了 downloadProgress ,当 writtenData 和 totalData 属性值改变时,观察者也应该被通知。以下有两种方法可以解决这个问题。

以上两个方法可以同时存在,且都会调用,但是最终结果会以 keyPathsForValuesAffectingValueForKey: 为准。

以上方法在观察集合属性时就不管用了。例如,假如你有一个 Department 类,它有一个装有 Employee 类的实例对象的数组,Employee 类有 salary 属性。你希望 Department 类有一个 totalSalary 属性来计算所有员工的薪水,也就是在这个关系中 Department 的 totalSalary 依赖于所有 Employee 实例对象的 salary 属性。以下有两种方法可以解决这个问题。

有时候我们难以避免多次注册和移除相同的 KVO,或者移除了一个未注册的观察者,从而产生可能会导致 Crash 的风险。

三种解决方案: 黑科技防止多次添加删除KVO出现的问题

我们在对象添加监听之前分别打印对象类型

我们看到,添加监听后,使用 object_getClass 方法获取model类型时获取到的是 NSKVONotifying_DJModel 。

这里就产生了几个问题:

从打印结果可以看出, NSKVONotifying_DJModel 是 DJModel 的子类,说明我们添加了监听之后动态创建了一个 DJModel 的子类 NSKVONotifying_DJModel ,并将对象 DJModel 的类型更改为了 NSKVONotifying_DJModel 。

我们从源码看出,实例对象调用 class 方法会返回 isa 指针,类对象调用 class 方法会返回自己,通过 object_getClass 方法获取对象的类型也会返回 isa 指针。从源码上看model对象添加监听之后使用 class 和使用 object_getClass 方法获取到的类型应该是一样的,但是这里却不同,我们猜测在添加了监听之后在 NSKVONotifying_DJModel 中重写了 class 方法。

我们打印一下添加监听前后 class 方法的 IMP 地址来确认是否重写了 class 方法。

从打印结果可以看出,添加监听之后 class 方法的地址改变了,这验证了我们之前的猜想, NSKVONotifying_DJModel 类中重写了 class 方法。

我们监听对象时调用了 set 方法,我们对监听前后的 set 方法单独分析。

我们再添加监听前后分别打印 setName 方法的 IMP 地址。

通过打印结果可以看出 setName 方法也在 NSKVONotifying_DJModel 中被重写了,我们再使用lldb来看下 setName 具体是什么

第一个地址打印的是添加监听前 setName 方法的 IMP 地址,第二个打印的是添加监听后 setName 方法的 IMP 地址。

这里看出添加监听前 setName 对应的具体方法就是 setName ,但是添加监听后, setName 对应的鸡头方法却变成了 _NSSetObjectValueAndNotify 函数。

下面我们就来研究一下 _NSSetObjectValueAndNotify 函数。

从上面与KVO相关的方法中我们可以看出,每一种数据类型都对应了一个 setXXXValueAndNotify 函数。

不过这些函数的具体实现没有公布,所以内部构造这里还是不清楚。

但是我们知道,在调用 `setXXXValueAndNotify 函数的过程中会调用另外两个方法。

测试后得出了以下几个结论:

我们还可以利用这两个方法手动触发 observeValueForKeyPath 方法:

所以我们判断在 _NSSetObjectValueAndNotify 函数内部,在调用原来的 set 方法之前插入了 willChangeValueForKey 方法,在调用原来的 set 方法之后插入了 didChangeValueForKey 方法,并根据初始化时的枚举值决定调用 observeValueForKeyPath 的时机。

(1)添加监听时,会动态创建一个监听对象类型的子类,并将监听对象的 isa 指针指向新的子类。

(2)子类中重写了 class 和监听属性的 set 方法。

(3)重写 class 方法是为了不将动态创建的类型暴露出来。

(4)重写 set 方法是将 set 方法的具体实现替换成了与属性类型相关的 __NSSetXXXValueAndNotify 函数。

(5)在 __NSSetXXXValueAndNotify 函数内部在 set 方法前后分别插入了 willChangeValueForKey 和 didChangeValueForKey 这两个方法。

(6)根据添加监听时的枚举值决定调用 observeValueForKeyPath 的具体时机。

以上就是关于iOS 基础知识之关联对象全部的内容,包括:iOS 基础知识之关联对象、iOS 类拓展与分类以及关联对象、iOS底层-ro rw rwe等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/9771923.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-05-01
下一篇 2023-05-01

发表评论

登录后才能评论

评论列表(0条)

保存