OC 语法底层基础

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 可能会被调用多次)
    • 调用顺序:
      • load
        • 先调用类的 load
          • 先编译的类,优先调用load
          • 调用子类的load 之前,会先调用父类的load
        • 再调用分类的load
          • 先调用的分类,优先调用load
      • initialize
        • 先初始化父类
        • 再初始化子类 (可能最终调用的是父类的initialize 方法)
  • Category 能否添加成员变量?如果可以,如何给Category 添加成员变量?

    • 不能直接给 Category 添加成员变量,但是可以间接实现 Category 有成员变量的效果

load 方法

  • +load 方法会在 runtime 加载类 、分类 时调用

  • 每个类、分类的 +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函数调用
  • 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)]

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值