《Objective-C学习总结》

学习要点 runtime(√√√√√√√√√√√√√√√√√√) runloop(√√√√√√√√√√√√√√√√√√) category(√√√√√√√√√√√√√√√√√√) protocol(√√√√√√√√√√√√√√√√√√√) extension(√√√√√√√√√√√√√√√√√√√) property(√√√√√√√√√√√√√√√√√√√√) 深复制与浅复制(√√√√√√√√√
摘要由CSDN通过智能技术生成
学习要点
    • runtime(√√√√√√√√√√√√√√√√√√)
    • runloop(√√√√√√√√√√√√√√√√√√)
    • category(√√√√√√√√√√√√√√√√√√)
    • protocol(√√√√√√√√√√√√√√√√√√√)
    • extension(√√√√√√√√√√√√√√√√√√√)
    • property(√√√√√√√√√√√√√√√√√√√√)
    • 深复制与浅复制(√√√√√√√√√√√√√√√√)
    • NSNotification(√√√√√√√√√√√√√√√√√√)
    • Block(√√√√√√√√√√√√√√√√√√√√√√√√√)
    • KVO&KVC(√√√√√√√√√√√√√√√√√√√√√)
    • 多态性(√√√√√√√√√√√√√√√√√)
    • 继承(√√√√√√√√√√√√√√√√√√√√)
    • 垃圾回收机制(√√√√√√√√√√√√√√√√)
    • 异常处理(√√√√√√√√√√√√√√√√√√√)
    • 多线程()



Runtime:

  • 作用
  • 原理
  • 实战应用
  • 参考文章

(1)作用
  • OC最开始是参考SmartTalk,而且smartTalk是一门动态语言,所以OC可以理解为在C的基础上扩展了runtime库,从而实现具有面向对象性。
  • runtime是如何实现让OC具备面向对象性?
    • 首先面向对象的一大特点就是把方法与属性捆绑在一起形成对象,所以OC就通过runtime,把C语言的结构体与函数捆绑到一起,到OC代码层我们是直观地编码一门面向对象语言,但实际上就是通过runtime在动态地调用C中结构体中的变量与相应的函数指针实现效果。
  • runtime的实际主要做了什么事情?
    • 封装:在runtime的封装下,对象的属性可以用结构体表示,而方法可以用C函数实现,并让程序运行时能实现创建、检查、修改类、对象方法。
    • 消息传递:通过objc_sendmsg(),找到相应的函数指针与数据进行运算并返回结果。


(2)原理

  • 类与对象
  • 成员变量与属性
  • 方法与消息
  • 协议与分类
  • …...



  • 类与对象:
          由于在Runtime中,类与对象的表示实际上就是结构体,因此我们得从结构体的组成的角度去探索类与对象在OC的实现机制。
 
类在runtime中的表示:
struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;                                            // isa指针用于指向自身属于的类,在OC中对象-》类对象-》元类,所以isa指针在这里指向原类 

#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;  //指向父类的指针
    const char *name                                         OBJC2_UNAVAILABLE;  //类名
    long version                                             OBJC2_UNAVAILABLE;  //类版本
    long info                                                OBJC2_UNAVAILABLE;  //类信息
    long instance_size                                       OBJC2_UNAVAILABLE;  //实例的控件大小
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;  //变量列表指针
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;  //方法列表指针
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;  //缓存指针
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;  //协议指针
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

对象在runtime的表示:
/// Represents an instance of a class.
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object * id ;
关于isa、super_class指针与类对象、元类的概念:

isa指针:存储着对象或者类所属的类型的指针,对象-》类对象,类对象-》元类。
super_class指针:指向父类的指针,若为顶层的跟类则为NULL
类对象与元类:对象-》类对象-》元类,这是在OC中的一种独特的组织方法,但类对象与元类在runtime中,本质上都是 objc_class结构体,区别是类对象管理动态方法,元类管理静态方法。
因此若我们用一个类调用类方法,如[NSDictionary dictionary];,在runtime的表现实际上就是objc_sendmsg(NSDictionary, @selector(dictionary));就是通过NSDictionary类对象的isa指针找到其对应的元类,并找元类的方法列表中找到相应的静态方法指针,并执行相应的代码。
那么一个类的元类的isa指针又会指向哪里?元类的isa指针会直接指向跟类(这里是NSObject的meta_class),而跟类的isa指针会自指形成闭环。
那么一个跟类的super_class的指向又会指向哪里?从下图我们可以发现root class的super指向了nil,而root meta的super指向了root class,那么最终还是指向了nil。


根据上面图,我们就可以很清楚每当发送一个消息,这个消息在对象、类对象、元类中的传递与走向。

关于super指针的调用实现:

关于oc中super指针容易造成一个误解,就是super指针是直接指向父类的,但并不是,如下
struct objc_super { id receiver; Class superClass; };
根据runtime中super指针的结构体,我们可以发现消息的接收者还是对象本身,它是通过自己的superClass指针来访问父类的。

关于instance_size的对象控件创建的实现:

instance_size用于记录创建该类对象的实例需要多大的内存空间,但根据OC的继承规则,这里会有个疑问,到底instance_size是指这个类所需的全部空间,还是需要把对象的继承体系中的所有instance_size相累加才是所需的控件大小?
答案是,每个类对象的instance_size已经是创建对象所需的全部存储空间,那时因为在创建一个子类时,即使我们不用调用父类的构造方法,也能访问从父类中继承回来的属性。

关于cache指针的实现:

OC是门动态语言,那么每调用一次方法则需要一次发送消息的行为,这样相比静态语言的C、C++直接调用函数对应的指针回慢不少,毕竟中间还穿插着一个runtime寻找对象的实现。那么如何能优化这个过程。
OC采用了缓存的策略,chahe指针的定义如下:
typedef struct objc_cache *Cache                             OBJC2_UNAVAILABLE;

#define CACHE_BUCKET_NAME(B)  ((B)->method_name)
#define CACHE_BUCKET_IMP(B)   ((B)->method_imp)
#define CACHE_BUCKET_VALID(B) (B)
#ifndef __LP64__
#define CACHE_HASH(sel, mask) (((uintptr_t)(sel)>>
2 ) & (mask))
#else
#define CACHE_HASH(sel, mask) (((unsigned int)((uintptr_t)(sel)>>
3 )) & (mask))
#endif
struct objc_cache {
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;      //
    unsigned int occupied                                    OBJC2_UNAVAILABLE;     //
    Method buckets[
1 ]                                        OBJC2_UNAVAILABLE;    //
};

关于Method的定义:
typedef struct objc_method *Method;
struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;  //SEL方法子,方法名经过hash后得到的一个字符串,与IMP是一一对应关系
    char *method_types                                       OBJC2_UNAVAILABLE;  //方法的类型,动态方法/静态方法
    IMP method_imp                                           OBJC2_UNAVAILABLE;  //IMP指针,指向具体的C实现函数
}      
每当对一个对象或者类对象发送消息时,就会先访问其cache的方法列表里是否有相同的Method,有则直接调用其IMP实现调用。
PS:iOS中的方法名字是以字符串为准的,在同一个类中,即使字符串同名参数不同也会编译不过,那时因为sel的原理就会把方法名进行hash生成唯一的字符串,所以在比较的时候,实际上只比对指针地址。

关于ivar_list指针的实现:

ivar_list实际上就是指向实例变量的指针。下面是runtime中的定义代码:
struct objc_ivar_list {
    int ivar_count                                           OBJC2_UNAVAILABLE;   //变量的数量
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_ivar ivar_list[1]                            OBJC2_UNAVAILABLE;   //数组,数组单元为 objc_ivar结构体
}          
                                                  OBJC2_UNAVAILABLE;
/// An opaque type that represents an instance variable.
typedef struct objc_ivar *Ivar;
struct objc_ivar {
   
char *ivar_name                                          OBJC2_UNAVAILABLE;    //变量名
    char *ivar_type                                          OBJC2_UNAVAILABLE;    //变量类型
    int ivar_offset                                          OBJC2_UNAVAILABLE;    //变量的内存位置偏移量
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
}      
首先访问一个对象本质上就是对其内存地址进行读写操作,而对象runtime的表示就是一个isa指针,而在内存的表示就是isa指针指向的地址+各种实例变量的存储。
所以当我们访问一个对象,实际上就是通过 objc_ivar_list获取到相应的 objc_ivar,然后通过其 ivar_offset计算出实际的内存地址,计算方法为isa指针地址+ivar_offset。

关于method_list指针的实现:

struct objc_method_list {
    struct objc_method_list *obsolete                        OBJC2_UNAVAILABLE;   //废弃方法列表

   
int method_count                                         OBJC2_UNAVAILABLE;   //方法总数
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;   //方法列表数组
}                                                            OBJC2_UNAVAILABLE;

/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;

struct objc_method {
   
SEL method_name                                          OBJC2_UNAVAILABLE;
   
char *method_types                                       OBJC2_UNAVAILABLE;
   
IMP method_imp                                           OBJC2_UNAVAILABLE;
}       
当在类对象或者元类中查询方法,实际上就是对 objc_method_list进行操作,通过遍历 objc_method_list方法列表通过SEL找到对应IMP执行调用,以IMP的返回作为返回结果,同事把对应的 objc_method缓存在cache中。
其中 struct objc_method_list *obsolete负责管理与维护准备废弃的方法。

关于protocol指针的实现:

struct objc_protocol_list {
   
struct objc_protocol_list *next;
   
long count;
    Protocol *list[
1 ];
};

#ifdef __OBJC__
@class Protocol;
#else
typedef struct objc_object Protocol;
#endif
protocol的本质也是一个方法列表,实现与动态方法与静态方法是类似的,不同的是在类中它专门由 objc_protocol_list指针管理,这样就可以加快方法查询的效率。

关于property属性的实现:

typedef struct objc_property *objc_property_t; 

typedef struct { 
    const char *name;           // 特性名 
    const char *value;          // 特性值 
} objc_property_attribute_t; 
property的本质就是实例变量+getter&setter,通过设置修饰词 objc_property_attribute_t,赋予getter&setter不同的实现效果。

类型编码(Type Encoding):
每一个函数的参数与返回的类型,都可以通过 @encode返回其编码,这个编码为类型编码,系统就是通过这些类型编码来识别变量的类型的。
如OC不支持long double,但我们依然可以使用long double进行变量的定义,但在底层实现中其 @encode返回结构与double是一致的,所以分配的存储空间大小也与double是一致的。

关联对象:
实现思路:通过
objc_getAssociatedObject
objc_setAssociatedObject
方法,把静态字符串(keyName)与相应的实例变量联系到一起。
经典应用:为category动态添加属性。

关于category的实现:

struct objc_category {
    char *category_name                                      OBJC2_UNAVAILABLE;   //category名
    char *class_name                                         OBJC2_UNAVAILABLE;   //类名
    struct objc_method_list *instance_methods                OBJC2_UNAVAILABLE;   //实例方法列表
    struct objc_method_list *class_methods                   OBJC2_UNAVAILABLE;   //类方法列表
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;   //协议方法列表
}                                                            OBJC2_UNAVAILABLE;
从category的runtime表示中可以大概知道,在编译期间会把category中的实例方法、类方法、协议方法写入相应类对象或元类的实例方法、类方法、协议方法,根据OC的继承体系与消息发送规则,可知,这样若有与父类同名的方法,就会被子类拦截掉,从而实现重写。

消息转发的机制:

当一条消息在其对象的继承体系中找不到相应的方法调用,这会启动消息转发机制,若消息转发机制都无法处理,则会触发sigforbid的错误,程序奔溃。

消息转发机制的三个步骤:
  1. 动态方法的解析:(允许动态添加实例方法/类方法,处理未知消息)
  2. 备用接收者:(把消息转发到具备处理该消息的别的对象里)
  3. 完整转发:()

动态方法的解析:

类通过实现 +resolveInstanceMethod:(实例方法)或 者+resolveClassMethod:(类方法)。方法可以为未知消息添加相应的处理方法。

备用接收者:

若动态方法解析失败,则会调用以下方法:
- (id)forwardingTargetForSelector:(SEL)aSelector
如果一个对象实现了这个方法,并返回一个非nil的结果,则这个对象会作为消息的新接收者,且消息会被分发到这个对象。当然这个对象不能是self自身,否则就是出现无限循环。当然,如果我们没有指定相应的对象来处理aSelector,则应该调用父类的实现来返回结果。
同时通过该方法可以实现类似多继承的效果。

完整转发:

如果上两个步骤都无法处理未知消息,则启动完成的消息转发机制。有以下两个关键方法需要重写:

- ( void )forwardInvocation:( NSInvocation *)anInvocation;     
- ( NSMethodSignature *)methodSignatureForSelector:( SEL )aSelector;
对象会创建 NSInvocation对象,里面包含消息处理的所有参数,并且我们能修改相应的参数,通过其实现相应的消息转发,并且消息转发的处理结果会作为原始发送者的处理结果。
其中 methodSignatureForSelector一定要重写,因为 aninvocaaion中有个关键参数signature,需要重写该方法来定义,才能正常地生成 aninvocaaion。

(3)实践应用

因为根据runtime给OC带来的动态性,以及其的调用规则,我们能灵活地添加、删除、改变方法的实现,能实现很多特殊的功能与效果。
首先可以看看runtime提供的函数类别:

即可得知它大概能实现什么功能,灵活的runtime function能让我们实现功能:

Method swizzling:
通过改变SEL所对应的IMP,从而实现方法的统一修改。
JSPatch:
通过消息转发机制,实现通过JS往OC中动态添加、修改方法的实现。
多继承:
通过消息转发机制,实现类似多继承的效果
为Category添加属性:
在编译期,category不允许自己创建属性,但通过runtime可以实现。
插件:
不少的xcode插件都是通过利用runtime在运行时注入代码实现的。
万能控制器跳转:
自动归档与解档:
本来写归档与接档程序,常规写法就是一个个属性手动写入,但这样会写入大量无聊的重复代码,通过runtime的函数可以大大简化这个过程。
通过class_copyIvarList函数取出相应对象的属性,从而获得ivar_name&ivar_value从而实现归档,通过这种方式能实现像遍历NSDictionary与NSArray一样地去遍历一个对象。
对象转model
实现方法同上。
未完待续。。。

(4)参考文章
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值