OC 语法底层基础
文章目录
分类(实现机制,原理等)
分类都做了哪些事情?
- 声明一些私有方法(对外不暴露)
- 分解体积庞大的类文件(根据不同功能组织到不同的分类中,方便多人共同开发一个类)
- 把Framework的私有方法公开
- 模拟多继承(可以模拟多继承的还有protocol、NSProxy)
特点
- 运行时决议 (只有在运行时才会通过runtime添加到宿主类中)
- 可以为系统类添加分类
- 分类可以访问原来类的成员变量
- 分类如果和原来类有同名方法,优先调用分类的(分类(最后参与编译)—>原来类->父类)
分类中都可以添加哪些内容?
- 实例方法
- 类方法
- 协议
- 属性(实际上只声明了getter和setter方法,并未在分类中添加实例变量(可以通过关联对象添加实例变量))
Category的底层结构
typedef struct category_t *Category;
struct category_t {
const char *name; //分类的名称
classref_t cls; //所属的宿主类类名
struct method_list_t *instanceMethods; //实例方法列表
struct method_list_t *classMethods; //类方法列表
struct protocol_list_t *protocols; //分类所实现的协议列表
struct property_list_t *instanceProperties; //实例属性列表
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
分类加载调用栈:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n8gyviLP-1584405995454)(media/15457312839940/15457936552352.jpg)]
images (指的是镜像而不是图片)
源码解读顺序
-
objc-os.mm
- _objc_init
- map_images
- map_images_nolock
-
objc-runtime-new.mm
- _read_images
- remethodizeClass
- attachCategories
- attchLists
- realloc、memmove 、memcpy
-
分类添加的方法可以"覆盖(不是覆盖只是分类的方法比原类的方法靠前)"原类方法
-
同名分类方法谁能生效取决于编译顺序
-
名字相同的分类会引起编译报错
实现原理
- Category 编译之后的底层结构是 struct category_t,里面存储着分类的对象方法、类方法、属性、协议等信息
- 在程序运行时,Runtime 会将 Category 的数据,合并到类信息中
Category 的加载处理过程
- 通过 Runtime 加载某个类的所有的 Category 数据
- 把所有 Category 的方法、属性、协议数据,合并到一个大数组中,后面把参与编译的Category 数据,会在数组的前面(会优先调用)
- 将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面(memmove、 memcpy)
- 分类最后参与编译,分类的方法列表会追加到原来类的方法列表中,并且是在前面,所以调用类的方法时去方法列表中找的时候先找到前面分类的实现,类似被覆盖的效果( memmove memcpy,先把原来的方法往后移动,再把分类的方法列表拷贝到原来的位置)
常见面试题
-
Category 和 Class Extension 的区别是什么?
- Class Extension 在编译的时候(编译时决议),他的数据就已经包含在类信息中
- Category 是在运行时(运行时决议),才会将数据合并到类信息中
- 扩展只以声明的形式存在,多数情况下寄生于宿主类的.m文件中;
- 不能为系统类添加扩展,分类可以为系统类添加扩展
-
Category 中有 load 方法吗? load 方法是什么时候调用的? load 方法能继承吗?
- 有load 方法
- load 方法在 Runtime 加载类、分类的时候调用
- load 方法可以继承,但是一般情况下不会主动去调用 load 方法,都是让系统自动调用
-
laod initialize 方法的区别是什么? 他们在 Category 中的调用顺序? 以及出现继承时他们之间的调用过程?
- 区别:
- 1、调用方式的区别:
- load 是根据函数地址直接调用
- initialize 是通过 objc_msgSend 调用
- 2、调用时刻的区别:
- load 是 runtime 加载类、分类的时候调用(只会调用1次)
- initialize 是类第一次接受到消息的时候调用,每一个类只会 initialize 一次 (父类的initialize 可能会被调用多次)
- 1、调用方式的区别:
- 调用顺序:
- load
- 先调用类的 load
- 先编译的类,优先调用load
- 调用子类的load 之前,会先调用父类的load
- 再调用分类的load
- 先调用的分类,优先调用load
- 先调用类的 load
- initialize
- 先初始化父类
- 再初始化子类 (可能最终调用的是父类的initialize 方法)
- load
- 区别:
-
Category 能否添加成员变量?如果可以,如何给Category 添加成员变量?
- 不能直接给 Category 添加成员变量,但是可以间接实现 Category 有成员变量的效果
load 方法
-
+load 方法会在 runtime 加载类 、分类 时调用
-
每个类、分类的 +load 在程序运行过程中只调用一次
-
调用顺序
- 先调用类的 +load
- 按照编译先后顺序调用(先编译,先调用)
- 调用子类的 +load 之前会先调用父类的 +load 方法
- 再调用分类的 +load
- 按照编译先后顺序调用(先编译,先调用)
- 先调用类的 +load
-
objc4源码解读过程:objc-os.mm
_objc_init load_images prepare_load_methods schedule_class_load add_class_to_loadable_list add_category_to_loadable_list call_load_methods call_class_loads call_category_loads (*load_method)(cls, SEL_load)
+load方法是根据方法地址直接调用,并不是经过objc_msgSend函数调用
initialize 方法
-
+initialize 方法会在 类 第一次接收到消息时调用
-
调用顺序:
- 先调用父类的 +initialize , 再调用子类的 +initialize(没有用到子类(没有继承关系)就只调用类的 +initialize。分类调用 +initialize 与编译顺序有关系)
- 先初始化父类,再初始化子类,每个类只会初始化1次
-
+initialize 和 + load 区别是:
- +initialize 是通过 objc_msgSend 进行调用的
- 如果子类没有实现+initialize ,会调用父类的 +initialize(所以父类的+initialize 可能会被调用多次)
- 如果分类实现了 +initialize 就会覆盖本身类的 +initialize 调用
- +load方法是根据方法地址直接调用,并不是经过objc_msgSend函数调用
- +initialize 是通过 objc_msgSend 进行调用的
-
objc4源码解读过程
objc-msg-arm64.s objc_msgSend objc-runtime-new.mm class_getInstanceMethod lookUpImpOrNil lookUpImpOrForward _class_initialize callInitialize objc_msgSend(cls, SEL_initialize)
关联对象
类和分类添加属性的区别
类 添加属性会自动生成成员变量,getter和setter 方法声明 和 对应的实现
分类 添加属性会自动生成成员变量,getter和setter 方法声明,但是不会生成实现
{
int _age;
}
- (void)setAge:(int)age;
-(int)age;
//@property (nonatomic,assign) int age;
/*
以上代码
1、会自动生成成员变量
2、生成 setter 和 getter 方法的声明
3、生成 setter 和 getter 方法的实现
*/
- (void)setAge:(int)age {
_age = age;
}
-(int)age {
return _age;
}
分类添加“成员变量”
关联对象并不是把成员变量添加到实例对象的属性列表中,而是自己维护的一个 hashMap
//添加关联对象
void objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,id _Nullable value, objc_AssociationPolicy policy)
//获取关联对象
id objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
//移除所有的关联对象
void objc_removeAssociatedObjects(id _Nonnull object)
@implementation Person (Test)
/* key 的方式可以写成多种形式
static void *MyKey = &MyKey;
objc_setAssociatedObject(obj, MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, MyKey)
static char MyKey;
objc_setAssociatedObject(obj, &MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, &MyKey)
使用属性名作为key
objc_setAssociatedObject(obj, @"property", value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_getAssociatedObject(obj, @"property");
使用get方法的@selecor作为key (常用的方式)
objc_setAssociatedObject(obj, @selector(getter), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, @selector(getter))
*/
- (void)setName:(NSString *)name {
objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_COPY);
}
- (NSString *)name {
return objc_getAssociatedObject(self, @"name");
}
//常用以下方式
- (void)setWeight:(int)weight {
objc_setAssociatedObject(self, @selector(weight), @(weight), OBJC_ASSOCIATION_ASSIGN);
}
- (int)weight {
//_cmd = @selector(weight)
return [objc_getAssociatedObject(self, _cmd) intValue];
}
@end
关联对象的实现
- 实现关联对象技术的核心对象有
- AssociationsManager
- AssociationsHashMap
- ObjectAssociationMap
- ObjcAssociation
源码解读:
class AssociationsManager {
static AssociationsHashMap *_map;
......
};
//key : disguised_ptr_t value : ObjectAssociationMap *
class AssociationsHashMap : public unordered_map<disguised_ptr_t, ObjectAssociationMap *, DisguisedPointerHash, DisguisedPointerEqual, AssociationsHashMapAllocator>
// key : ObjcAssociation value : ObjectPointerLess
class ObjectAssociationMap : public std::map<void *, ObjcAssociation, ObjectPointerLess, ObjectAssociationMapAllocator>
class ObjcAssociation {
uintptr_t _policy;
id _value;
......
}
- 关联对象的原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yhnJKWcU-1584405995456)(media/15457312839940/15638762809616.jpg)]
AssociationsManager 是一个全局的管理者,内部是一个字典(AssociationsHashMap) ,key 是传进去的object对象,value 也是一字典(ObjectAssociationMap ),key 是第三个参数 key ,value 是一个 ObjcAssociation ,里面存放的是 policy 和 value
扩展(Extension 和分类的区别)
一般用扩展做什么?
- 声明私有属性
- 声明私有方法(便于阅读,没有太大意义)
- 声明私有成员变量
分类和扩展的区别
- 扩展是编译时决议,而分类是运行时决议;
- 扩展只以声明的形式存在,多数情况下寄生于宿主类的.m文件中;
- 不能为系统类添加扩展,分类可以为系统类添加扩展
代理
介绍代理
- 准确来说是一种设计模式(代理模式)
- iOS中以
@protocol
形式体现 - 传递方式
一对一
工作流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iQpEWr0F-1584405995457)(media/15457312839940/15815242180051.jpg)]
协议中可以定义方法和属性
使用注意
- 一般声明为 weak 以规避循环引用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FnFKaUzo-1584405995460)(media/15457312839940/15458089417658.jpg)]
通知实现机制( NSNotification 原理)
特点
- 使用
观察者模式
来实现的用于跨层传递信息的机制; - 传递方法是
一对多
; - 通知是松耦合的,通知方不需要知道被通知方的任何情况,而 delegate 不行
- 通知的效率比 delegate 略低
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HMsIe6FB-1584405995462)(media/15457312839940/15841812461183.jpg)]
如何实现通知机制?
NSNotificationCenter 内部会维护一个map表(字典) ,key 是 notificationName
value 是 一个数组列表
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uCCIVU3D-1584405995464)(media/15457312839940/15458092549569.jpg)]
KVO (实现机制)
什么是KVO ?KVO 的实现机制是什么?
-
KVO 是 OC 对
观察者设计模式
的一种实现; -
使用了
isa混写(isa-swizzling)
来实现的;(其实就是在调用了addObserver:之后系统动态创建一个派生类(NSKVONotifying_XXX),并把isa 指针指向了派生类)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gss2VyP1-1584405995465)(media/15457312839940/15458095894570.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WE1zJyGN-1584405995466)(media/15457312839940/15458099584582.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oD8tGHiD-1584405995468)(media/15457312839940/15458099689254.jpg)] -
通过KVC 设置的value 能否生效?为什么?
能够生效;
考察的KVC的实现机制,KVC 调用的时候会调用setter方法,而 setter 方法被派生类重写了,就会调用派生类的方法,这样就会触发KVO 。 -
通过成员变量直接赋值value 能否生效?(手动KVO)
不能够生效;
通过添加willChangeValueForKey:
和didChangeValueForKey:
使其生效。
触发KVO 的方式:
- 使用setter 方法改变值 KVO 才会生效。
- 使用setValue:forKey: 改变值KVO 才会生效
- 成员变量直接修改
需要手动添加
KVO 才会生效
KVC
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GyWStZ7I-1584405995475)(media/15457312839940/15458120083176.jpg)]
KVC就是指iOS的开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。而不需要调用明确的存取方法。这样就可以在运行时动态地访问和修改对象的属性。而不是在编译时确定,这也是iOS开发中的黑魔法之一。很多高级的iOS开发技巧都是基于KVC实现的
- 通过
键值编码
是否有违背面向对象的编程思想?
知道一个类或者实例内部私有成员名称的情况下可以通过KVC 进行设置和取值的,是破坏了面向对象编程思想的。
KVC 原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uXEWHm3N-1584405995476)(media/15457312839940/15458122258873.jpg)]
访问器方法是否存在流程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hvcrrAcC-1584405995477)(media/15457312839940/15458122868788.jpg)]
实例变量说明:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WYZcfyk3-1584405995478)(media/15457312839940/15458123529484.jpg)]
setValue:forKey: 调用流程:
setKey _setKey
accessInstanceVariablesDirectly 返回YES
_key _isKey key isKey
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U3e4MR4I-1584405995479)(media/15457312839940/15458124088385.jpg)]
- 程序优先调用set:属性值方法,代码通过setter方法完成设置。注意,这里的是指成员变量名,首字母大小写要符合KVC的命名规则,下同
- 如果没有找到setName:方法,KVC机制会检查+ (BOOL)accessInstanceVariablesDirectly方法有没有返回YES,默认该方法会返回YES,如果你重写了该方法让其返回NO的话,那么在这一步KVC会执行setValue:forUndefinedKey:方法,不过一般开发者不会这么做。所以KVC机制会搜索该类里面有没有名为的成员变量,无论该变量是在类接口处定义,还是在类实现处定义,也无论用了什么样的访问修饰符,只在存在以命名的变量,KVC都可以对该成员变量赋值。
- 如果该类即没有set:方法,也没有_成员变量,KVC机制会搜索_is的成员变量。
- 和上面一样,如果该类即没有set:方法,也没有_和_is成员变量,KVC机制再会继续搜索和is的成员变量。再给它们赋值。
- 如果上面列出的方法或者成员变量都不存在,系统将会执行该对象的setValue:forUndefinedKey:方法,默认是抛出异常。
- 即如果没有找到Set方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员并进行赋值操作。
- 如果开发者想让这个类禁用KVC,那么重写+ (BOOL)accessInstanceVariablesDirectly方法让其返回NO即可,这样的话如果KVC没有找到set:属性名时,会直接用setValue:forUndefinedKey:方法。
valueForKey 调用流程:
getKey key isKey _key
accessInstanceVariablesDirectly 返回yes
_key _isKey key isKey
- 首先按get,,is的顺序方法查找getter方法,找到的话会直接调用。如果是BOOL或者Int等值类型, 会将其包装成一个NSNumber对象。
- 如果上面的getter没有找到,KVC则会查找countOf,objectInAtIndex或AtIndexes格式的方法。如果countOf方法和另外两个方法中的一个被找到,那么就会返回一个可以响应NSArray所有方法的代理集合(它是NSKeyValueArray,是NSArray的子类),调用这个代理集合的方法,或者说给这个代理集合发送属于NSArray的方法,就会以countOf,objectInAtIndex或AtIndexes这几个方法组合的形式调用。还有一个可选的get:range:方法。所以你想重新定义KVC的一些功能,你可以添加这些方法,需要注意的是你的方法名要符合KVC的标准命名方法,包括方法签名。
- 如果上面的方法没有找到,那么会同时查找countOf,enumeratorOf,memberOf格式的方法。如果这三个方法都找到,那么就返回一个可以响应NSSet所的方法的代理集合,和上面一样,给这个代理集合发NSSet的消息,就会以countOf,enumeratorOf,memberOf组合的形式调用。
- 如果还没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默认行为),那么和先前的设值一样,会按_,_is,,is的顺序搜索成员变量名,这里不推荐这么做,因为这样直接访问实例变量破坏了封装性,使代码更脆弱。如果重写了类方法+ (BOOL)accessInstanceVariablesDirectly返回NO的话,那么会直接调用valueForUndefinedKey:方法,默认是抛出异常。
属性关键字 (weak/assign/copy/strong)
- 读写权限
- 原子性
- 引用计数
读写权限
- readonly
- readwrite(默认)
原子性
- atomic (默认)
赋值和获取是线程安全的(成员属性的直接赋值和获取) 不代表操作和访问,比如说对一个数组的赋值和获取是线程安全的,但是对数组进行操作比如添加和移除是不保证线程安全的。 - nonatomic
引用计数
-
retain(MRC)/strong(ARC)
-
assign/unsafe_unretained(MRC ARC几乎不行)
-
weak/copy
-
assgin 和 weak 区别:
assgin:
- 修饰基本数据类型,比如int Bool 等。
- 修饰对象类型时,不改变其引用计数。
- 会产生悬垂指针。(assgin 修饰的对象被释放后,
assgin指针仍然指向原来对象的地址
,如果继续访问的话就会导致异常)
weak:
- 不改变被修饰对象的引用计数
- 所指
对象在被释放之后会自动置为nil
。
相同点:都不改变引用计数。
- 为什么weak 修饰的对象被释放之后会自动置为nil ?<内存管理章节>
-
copy(深拷贝浅拷贝 重点)
- 浅拷贝是对内存地址的复制,让目标对象指针和源对象指向
同一片
内存空间。- 会增加引用计数
- 并未发生新的内存分配
- 让目标对象指针和源对象指针指向
两片
内容相同(不是同一块内存)的内存空间。- 不会增加引用计数
- 产生了新的内存分配
- 区分
- 看是否有新的内存分配
- 看是否增加引用计数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8BYPcfVa-1584405995481)(media/15457312839940/15840938160407.jpg)]
- 浅拷贝是对内存地址的复制,让目标对象指针和源对象指向
可变对象copy 和 mutableCopy 都是深拷贝。
不可变对象的copy 是浅拷贝,mutableCopy 是深拷贝。
copy 方法返回的都是不可变对象
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gwD8QIuz-1584405995481)(media/15457312839940/15458149050100.jpg)]
MRC下如何重写retain 修饰变量的setter 方法?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aIX2m0tn-1584405995483)(media/15457312839940/15458150305498.jpg)]