iOS底层iOS 底层面试

Runtime的相关知识

2021-03-17  本文已影响0人  __weak

Runtime是近年来面试遇到的一个高频方向,也是我们平时开发中或多或少接触的一个领域,那么什么是runtime呢?它又可以用来做什么呢?

什么是Runtime?平时项目中有用过么?
    OC是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行
    OC的动态性就是由Runtime来支撑和实现的,Runtime是一套C语言的API,封装了很多动 态性相关的函数
    平时编写的OC代码,底层都是转换成了Runtime API进行调用

具体应用
    利用关联对象(AssociatedObject)给分类添加属性
    遍历类的所有成员变量(修改textfield的占位文字颜色、字典转模型、自动归档解档)
    交换方法实现(交换系统的方法)
    利用消息转发机制解决方法找不到的异常问题

1、详解isa

我们在研究对象的本质的时候提到过isa,当时说的是isa是个指针,存储的是个类对象或者元类对象的地址,实例对象的isa指向类对象,类对象的isa指向元类对象。确实,在arm64架构(真机环境)前,isa单纯的就是一个指针,里面存储着类对象或者元类对象地址,但是arm64架构后,系统对isa指针进行了优化,我们在源码中可以探其结构:


image.png

  可以看到,isa是个isa_t类型的数据,我们在点进去看一下isa_t是什么数据:

image.png

也就是这个机构体里面包含很多东西,但是究竟是什么东西要根据系统来确定。

那么在arm64架构下,isa指针的真实结构是:


image.png

  在我们具体分析isa内部各个参数分别代表什么之前,我们需要弄清楚这个union是什么呢?我们看着这个union和结构体的结构很像,这两者的区别如下↓↓

union:共用体,顾名思义,就是多个成员共用一块内存。在编译时会选取成员中长度最长的来声明。     共用体内存=MAX(各变量)
struct:结构体,每个成员都是独立的一块内存。                                          结构的内存=sizeof(各变量之和)+内存对齐

也就是说,union共用体内所有的变量,都用同一块内存,而struct结构体内的变量是各个变量有各个变量自己的内存,举例说明:


image.png

我们分别定义了一个共用体test1和一个结构体test2,里面都各自有八个char变量,打印出来各自占用内存我们发现共用体只占用了1个内存,而结构体占用了8个内存,

其实结构体占用8个内存很好理解,8个char变量,每个char占用一个,所以是8;而union共用体为什么只占用一个呢?这是因为他们共享同一个内存存储东西,他们的内存结构是这样的:

image.png
我们看到te就一个内存空间,也就是所有的公用体成员公用一个空间,并且同一时间只能存储其中一个成员变量的值,这一点我们可以打断点或打印进行确认 image.png

我们发现,第一次打印的时候,bdf这些值都是1的打印出来都是0,这是因为当te.g = '0',执行完后,这个内存存储的是g的值0,所以访问的时候打印结果都是0。第二次打印同理,te.h执行完内存中存储的是1,再访问这块内存那么得到的结果都会是1。所以我们从这也可以看出,union共用体就是系统分配一个内存供里面的成员共同使用,某一时间只能存储其中某一个变量的值,这样做相比结构体而言可以很大程度的节省内存空间。

既然我们已经知道isa_t使用共用体的原因是为了最大限度节省内存空间,那么各个成员后面的数字代表什么呢?这就涉及到了位域.

我们看到union共用体为了节省空间是不断的进行值覆盖操作,也就是新值覆盖旧值,结合位域的话可以更大限度的节约内存空间还不用覆盖旧值。我们都知道一个字节是8个bit位,所以位域的作用就是将字节这个内存单位缩小为bit位来存储东西。我们把上面这个union共用体加上位域:

image.png
 上面这段代码的意思就是,abcdefgh这八个char变量不再是不停地覆盖旧值操作了,而是将一个字节分成8个bit位,每个变量一个bit位,按照顺序从右到左一次排列。

我们都知道char变量占用一个字节,一个字节有8个bit位,也就是char变量有8位,那么te和te2的内存结构如下所示:


image.png

 这个结构我们也可以通过打印来验证:te占用一个字节位置,内存地址对应的值是0xaa,转换成二进制正好是10101010,也就是a~h存储的值。


image.png
我们可以看到,现在是将一个字节中的8个bit位分别让给8个char变量存储数据,所以这些char变量存储的数据不是0就是1,可以看出来这种方式非常省内存空间,将一个字节分成8个bit位存储东西,物尽其用。所以我们根据isa_t结构体中的所占用bit位加起来=64可以得知isa指针占用8个字节空间。

虽然位域极大限度的节省了内存空间,但是现在面临着一个问题,那就是如何给这些变量赋值或者取值呢?普通结构体中因为每个变量都有自己的内存地址,所以直接根据地址读取值即可, 但是union共用体中是大家共用同一个内存地址,只是分布在不同的bit位上,所以是没有办法通过内存地址读取值的,那么这就用到了位运算符,我们需要知道以下几个概念:

&:按位与,同真为真,其余为假

|:按位或,有真则真,全假则假

<<:左移,表示左移动一位 (默认是00000001 那么1<<1 则变成了00000010 1<<2就是00000100)

~:按位取反

掩码 : 一般把用来进行按位与(&)运算来取出相应的值的值称之为掩码(Mask)。如 #define TallMask 0b00000100 :TallMask就是用来取出右边第三个bit位数据的掩码

好,那么我们来看下这些运算符是怎么可以做到取值赋值的呢?比如说我们上面的te共用体内有8个char,要是我们想出去char b的值怎么取呢?这就用到了&:


image.png

按位与上1<<1 就可以取出b位的值了,b是1那么结果就是1,b是0那么结果就是0;

同理,当我们为f设置值的时候,也是类似的操作,就是在改变f的值的同时不影响其他值,这里我们要看赋的值是0还是1,不同值操作不同:


image.png

所以,这就是共同体中取值赋值的操作流程,那么我们接下来回到isa指针这个结构体中,看一下它里面的各个成员以及怎么取赋值的↓↓

/*nonpointer
         0,代表普通的指针,存储着Class、Meta-Class对象的内存地址
         1,代表优化过,使用位域存储更多的信息
         */
        uintptr_t nonpointer        : 1;                                       \
        
        /*has_assoc:是否有设置过关联对象,如果没有,释放时会更快*/
        uintptr_t has_assoc         : 1;                                       \
        
        /*是否有C++的析构函数(.cxx_destruct),如果没有,释放时会更快*/
        uintptr_t has_cxx_dtor      : 1;                                       \
        
        /*存储着Class、Meta-Class对象的内存地址信息*/
        uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
        
        /*用于在调试时分辨对象是否未完成初始化*/
        uintptr_t magic             : 6;                                       \
        
        /*是否有被弱引用指向过,如果没有,释放时会更快*/
        uintptr_t weakly_referenced : 1;                                       \
        
        /*对象是否正在释放*/
        uintptr_t deallocating      : 1;                                       \
        
        /*里面存储的值是引用计数器减1*/
        uintptr_t has_sidetable_rc  : 1;                                       \
        
        /*
         引用计数器是否过大无法存储在isa中
         如果为1,那么引用计数会存储在一个叫SideTable的类的属性中
         */
        uintptr_t extra_rc          : 19;

我们看到,isa指针确实做了很大的优化,同样是占用8个字节,优化后的共用体不仅存放这类对象或元类对象地址,还存放了很多额外属性,接下来我们对这个结构进行验证:需要注意的是因为是arm64架构 所以这个验证需要是ios项目且需要运行在真机上 这样才会得出准确的结果

首先,我们来验证这个shiftcls是否就是类对象内存地址。


image.png

我们定义了一个dog对象,我们打印它的isa是0x000001a102a48de1

从上面的分析我们得知,要取出shiftcls的值需要isa的值&ISA_MASK(这个isa_mask在源码中有定义),得出$1 = 0x000001a102a48de0

而$1的地址值正是我们上面打印出来Dog类对象的地址值,所以这也验证了isa_t的结构。


image.png

 我们还可以来看一下其他一些成员,比如说是否被弱指针指向过?我们先将上面没有被__weak指向过的数据保存一下,其中红色框中的就是这个属性,0表示没有被指向过


image.png
 然后我们修改代码,添加弱指针指向dog:__weak Dog *weaKDog = dog;
注意:只要设置过关联对象或者弱引用引用过对象,has_assoc或weakly_referenced的值就会变成1,不论之后是否将关联对象置为nil或断开弱引用。
image.png

  发现确实由0变成了1,所以可以验证isa_t的结构,这个实验要确保程序运行在真机才能出现这个结果。所以arm64后确实对isa指针做了优化处理,不在单纯的存放类对象或者元类对象的内存地址,而是除此之外存储了更多内容。

2、class的具体结构

我们之前在讲分类的时候讲到了类的大体结构,如下图所示:

image.png
 就如我们之前讲到的,当我们调用方法的时候是从bits中的methods中查找方法,分类的方法是排在主类方法前面的,所以调用同名方法是先调用分类的,而且究竟调用哪个分类的方法要取决于编译的先后顺序等等:
image.png
那么这个rw_t中的methods和ro_t中的methods有什么不一样呢?

首先,ro_t中methods,是只包含原始类的方法,不包括分类的,而rw_t中的methods即包含原始类的也包含分类的;

其次,ro_t中的methods只能读取不能修改,而rw_t中的methods既可以读取也可以修改,所以我们今后在动态添加方法修改方法的时候是在rw_t中的methods去操作的;

然后,ro_t中的methods是个一维数组,里面存放着method_t(对方法/函数的封装,即一个method_t代表一个方法或函数),而rw_t中的methods是个二维数组,里面存放着各个分类和原始类的数组,分类和原始类的数组中存放着method_t。即:


image.png

我们也可以在源码中找到rw_t和ro_t的关系,

static Class realizeClass(Class cls)
{
    runtimeLock.assertLocked();

    const class_ro_t *ro;
    class_rw_t *rw;
    Class supercls;
    Class metacls;
    bool isMeta;

    if (!cls) return nil;
    if (cls->isRealized()) return cls;
    assert(cls == remapClass(cls));
    
    // 最开始cls->data是指向ro的
    ro = (const class_ro_t *)cls->data();
    if (ro->flags & RO_FUTURE) {
        // rw已经初始化并且分配内存空间
        rw = cls->data(); // cls->data指向rw
        ro = cls->data()->ro; // cls->data()->ro指向ro  即rw中的ro指向ro
        cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
    } else {
        // 如果rw并不存在,则为rw分配空间
        rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);// 分配空间
        rw->ro = ro;// rw->ro重新指向ro
        rw->flags = RW_REALIZED|RW_REALIZING;
        // 将rw传入setData函数,等于cls->data()重新指向rw
        cls->setData(rw);
    }
}

首先,cls->data(即bits)是指向存储类初始化信息的ro_t的,然后在运行过程中创建了class_rw_t,等rw_t分配好内存空间后,开始将cls->data指向了rw_t并将rw_t中的ro指向了存储初始化信息的ro_t。

那么ro_t和rw_t中存储的这个method_t是个什么结构呢?我们阅读源码发现结构如下,我们发现有三个成员:name、types、imp,我们一一来看:

image

name,表示方法的名称,一般叫做选择器,可以通过@selector()sel_registerName()获得。

/*
比如test方法,它的SEL就是@selector(test);或者sel_registerName("test");需要注意的一点就是不同类中的同名方法,它们的方法选择器是相同的,比如A、B两个类中都有test方法,那么这两个test方法的名称都是@selector(test);或者sel_registerName("test"); 
*/

types,表示方法的编码,即返回值、参数的类型,通过字符串拼接的方式将返回值和参数拼接成一个字符串,来代表函数返回值及参数。

/*
   比如ViewDidload方法,我们都知道它的返回值是void,参数转为底层语言后是self和_cmd,即一个id类型和一个方法选择器,那么encode后就是v16@0:8(它所表示的意思是:返回值是void类型,参数一共占用16个字节,第一个参数是@类型,内存空间从0开始,第二个参数是:类型,内存空间从8开始),当然这里的数字可以不写,简写成V@:   
*/

关于更多encode规则,可以查看下面这个表:

image

当然除了自己手写外,iOS提供了@encode的指令,可以将具体的类型转化成字符串编码。

NSLog(@"%s",@encode(int));
NSLog(@"%s",@encode(float));
NSLog(@"%s",@encode(id));
NSLog(@"%s",@encode(SEL));

// 打印内容
Runtime-test[25275:9144176] i
Runtime-test[25275:9144176] f
Runtime-test[25275:9144176] @
Runtime-test[25275:9144176] :

imp,表示指向函数的指针(函数地址),即方法的具体实现,我们调用的方法实际上最后都是通过这个imp去进行最终操作的。

3、方法缓存

我们在分析清楚方法列表和方法的结构后,我们再来看一下方法的调用是怎么一个流程呢?是直接去方法列表里面遍历查找对应的方法吗?

其实不然,我们在分析类的结构的时候,除了bits(指向类的具体信息,包括rw_t、ro_t等等一些内容)外,还有一个方法缓存:cache,用来缓存曾经调用过的方法

image

所以系统查找对应方法不是通过遍历rw_t这个二维数组来寻找方法的,这样做太慢,效率太低,系统是先从方法缓存中找有没有对应的方法,有的话就直接调用缓存里的方法,根据imp去调用方法,没有的话,就再去方法数组中遍历查找,找到后调用并保存到方法缓存里,流程如下:

image

那么方法是怎么缓存到cache中的呢?系统又是怎么查找缓存中的方法的呢?我们通过源码来看一下cache的结构:

散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

我们可以看到,cache_t里面就三个成员,后两个代表长度和数量,是int类型,肯定不是存储方法的地方,所以方法应该是存储在_buckets这个散列表中。散列存储的是一个个的bucket_t的结构体,那么这个bucket_t又是个什么结构呢?

image

所以cache_t底部结构是这样的:

image

  我们看到,bucket_t就两个值,一个key一个imp,key的话就是方法名,也就是SEL,而imp就是Value,也就是当我们调用一个方法是来到方法缓存中查找,通过比对方法名是不是一致,一致的话就返回对应的imp,也就是方法地址,从而可以调用方法,那么这个散列表是怎么查找的呢?难道也是通过遍历吗?

我们通过阅读源码来一探究竟:

image
 通过上面代码的阅读,我们可以知道系统在cache_t中查找方法并不是通过遍历,而是通过方法名SEL&mask得到一个索引,直接去读数组索引中的方法,如果该方法的SEL与我们调用的方法名SEL一直,那么就返回这个方法,否则就一直向下寻找直到找完为止。

好,既然取值的时候不是遍历,而是直接读的索引,那么讲方法存储到缓存中也肯定是通过这种方式了,直接方法名&mask拿到索引,然后将_key和_imp存储到对应的索引上,这一点我们通过源码也可以确认:

image

我们看到无论是存还是读,都是调用了find函数,查看SEL&mask对应的索引的方法,不合适的话再向下寻找直到找到合适的位置。

那么这里有两个疑问,为什么SEL&mask会出现不是该方法名(读)或者不为空(写)的情况呢?散列表扩容后方法还在吗?

首先,SEL&mask这个问题,是因为不同的方法名&mask可能出现同一个结果,比如test方法的SEL是011,run方法的SEL是010,mask是010,那么无论是test的SEL&mask还是run的SEL&mask 记过都是010,如果大家都存在这个索引里面是会出问题的,所以为了解决这个索引重复的问题需要先做判断,即拿到索引后先判断这个索引对应的值是不是你想要的,是的话你拿走用,不是的话向下继续找,方法缓存也是同样的道理。我们先调用test方法,缓存到010索引,再调用run方法,发现010位置不为空了,那就判断010下面的索引是否为空,为空的话就将run方法缓存到这个位置。

关于散列表扩容后,缓存方法在不在的问题,通过源码就可以知道,旧散列表已经释放掉了,所以是不存在的,再次调用的时候就得重新去rw_t中遍历找方法然后重新缓存到散列表中,比如下面这个例子:

image

  更正更正更正

我们前面讲到当SEL&mask出来一个索引发现被占用或者不是我想要的时候,系统是向索引下一位再次寻找,这个地方失误了,不是向下是向上寻找,这个地方看源码的时候忽略了条件,在x86或者i386架构中是向下寻找,在arm64架构中是向上寻找:(因为上面图片资源都已经删掉了就没有再更改,这里需要注意一下)

image

到现在我们清楚了,那就是散列表中并不是按照索引依次排序或者遍历索引依次读取,那么就会出现个问题,因为SEL&mask是个小于mask的随机值且散列表存储空间超过3/4的时候就要扩容,那就会导致散列表中有一部分空间始终被限制。确实,散列表当分配内存后,每个地方最初都是null的,当某个位置的索引被用到时,对应的位置才会存储方法,其余位置仍处于空闲状态,但是这样做可以极大提高查找速度(比遍历快很多),所以这是一种空间换时间的方式。

image

4.方法的传递过程

我们现在已经清楚方法的调用顺序了,实现从缓存中找没有的话再去rw_t中找,那么在没有的话就去其父类中找,父类中查找也是如此,先去父类中的cache中查找,没有的话再去父类的rw_t中找,以此类推。如果查找到基类还没有呢?难道就直接报unrecognized selector sent to instance 这个经典错误吗?

其实不是,方法的传递主要涉及到三个部分,这也是我们平时用得最多以及面试中经常出现的问题:

我们都知道,当我们调用一个方法是,其实底层是将这个方法转换成了objc_msgSend函数来进行调用,objc_msgSend的执行流程可以分为3大阶段:

消息发送->动态方法解析->消息转发

这个流程我们是可以从源码中得到确认,以下是源码:

1 /***********************************************************************
  2 * _class_lookupMethodAndLoadCache.
  3 * Method lookup for dispatchers ONLY. OTHER CODE SHOULD USE lookUpImp().
  4 * This lookup avoids optimistic cache scan because the dispatcher 
  5 * already tried that.
  6 **********************************************************************/
  7 IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
  8 {
  9     return lookUpImpOrForward(cls, sel, obj, 
 10                               YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
 11 }
 12 
 13 
 14 /***********************************************************************
 15 * lookUpImpOrForward.
 16 * The standard IMP lookup. 
 17 * initialize==NO tries to avoid +initialize (but sometimes fails)
 18 * cache==NO skips optimistic unlocked lookup (but uses cache elsewhere)
 19 * Most callers should use initialize==YES and cache==YES.
 20 * inst is an instance of cls or a subclass thereof, or nil if none is known. 
 21 *   If cls is an un-initialized metaclass then a non-nil inst is faster.
 22 * May return _objc_msgForward_impcache. IMPs destined for external use 
 23 *   must be converted to _objc_msgForward or _objc_msgForward_stret.
 24 *   If you don't want forwarding at all, use lookUpImpOrNil() instead.
 25 **********************************************************************/
 26 //这个函数是方法调用流程的函数 即消息发送->动态方法解析->消息转发
 27 IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
 28                        bool initialize, bool cache, bool resolver)
 29 {
 30     IMP imp = nil;
 31     bool triedResolver = NO;
 32 
 33     runtimeLock.assertUnlocked();
 34 
 35     // Optimistic cache lookup
 36     if (cache) {
 37         imp = cache_getImp(cls, sel);
 38         if (imp) return imp;
 39     }
 40 
 41     // runtimeLock is held during isRealized and isInitialized checking
 42     // to prevent races against concurrent realization.
 43 
 44     // runtimeLock is held during method search to make
 45     // method-lookup + cache-fill atomic with respect to method addition.
 46     // Otherwise, a category could be added but ignored indefinitely because
 47     // the cache was re-filled with the old value after the cache flush on
 48     // behalf of the category.
 49 
 50     runtimeLock.lock();
 51     checkIsKnownClass(cls);
 52 
 53     if (!cls->isRealized()) {
 54         realizeClass(cls);
 55     }
 56 
 57     if (initialize  &&  !cls->isInitialized()) {
 58         runtimeLock.unlock();
 59         _class_initialize (_class_getNonMetaClass(cls, inst));
 60         runtimeLock.lock();
 61         // If sel == initialize, _class_initialize will send +initialize and 
 62         // then the messenger will send +initialize again after this 
 63         // procedure finishes. Of course, if this is not being called 
 64         // from the messenger then it won't happen. 2778172
 65     }
 66 
 67     
 68  retry:    
 69     runtimeLock.assertLocked();
 70 
 71     // Try this class's cache.
 72     //先从当前类对象的方法缓存中查看有没有对应方法
 73     imp = cache_getImp(cls, sel);
 74     if (imp) goto done;
 75 
 76     // Try this class's method lists.
 77     //没有的话再从类对象的方法列表中寻找
 78     {
 79         Method meth = getMethodNoSuper_nolock(cls, sel);
 80         if (meth) {
 81             log_and_fill_cache(cls, meth->imp, sel, inst, cls);
 82             imp = meth->imp;
 83             goto done;
 84         }
 85     }
 86 
 87     // Try superclass caches and method lists.
 88     {
 89         unsigned attempts = unreasonableClassCount();
 90         //遍历所有父类 知道其父类为空
 91         for (Class curClass = cls->superclass;
 92              curClass != nil;
 93              curClass = curClass->superclass)
 94         {
 95             // Halt if there is a cycle in the superclass chain.
 96             if (--attempts == 0) {
 97                 _objc_fatal("Memory corruption in class list.");
 98             }
 99             
100             // Superclass cache.
101             //先查找父类的方法缓存
102             imp = cache_getImp(curClass, sel);
103             if (imp) {
104                 if (imp != (IMP)_objc_msgForward_impcache) {
105                     // Found the method in a superclass. Cache it in this class.
106                     log_and_fill_cache(cls, imp, sel, inst, curClass);
107                     goto done;
108                 }
109                 else {
110                     // Found a forward:: entry in a superclass.
111                     // Stop searching, but don't cache yet; call method 
112                     // resolver for this class first.
113                     break;
114                 }
115             }
116             
117             // Superclass method list.
118             //再查找父类的方法列表
119             Method meth = getMethodNoSuper_nolock(curClass, sel);
120             if (meth) {
121                 log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
122                 imp = meth->imp;
123                 goto done;
124             }
125         }
126     }
127 
128     // No implementation found. Try method resolver once.
129     //消息发送阶段没找到imp 尝试进行一次动态方法解析
130     if (resolver  &&  !triedResolver) {
131         runtimeLock.unlock();
132         _class_resolveMethod(cls, sel, inst);
133         runtimeLock.lock();
134         // Don't cache the result; we don't hold the lock so it may have 
135         // changed already. Re-do the search from scratch instead.
136         triedResolver = YES;
137         //跳转到retry入口  retry入口就在上面,也就是x消息发送过程即找缓存找rw_t
138         goto retry;
139     }
140 
141     // No implementation found, and method resolver didn't help. 
142     // Use forwarding.
143     //消息发送阶段没找到imp而且执行动态方法解析也没有帮助 那么就执行方法转发
144     imp = (IMP)_objc_msgForward_impcache;
145     cache_fill(cls, sel, imp, inst);
146 
147  done:
148     runtimeLock.unlock();
149 
150     return imp;
151 }

消息传递过程源码实现

首先,消息发送,就是我们刚才提到的系统会先去cache_t中查找,有的话调用,没有的话去类对象的rw_t中查找,有的话调用并缓存到cache_t中,没有的话根据supperclass指针去父类中查找。父类查找也是如此,先去父类的cache_t中查找,有的话进行调用并添加到自己的cache_t中而不是父类的cache_t中,没有的话再去父类的rw_t中查找,有的话调用并缓存到自己的cache_t中,没有的话以此类推。流程如下:

image
当消息发送找到最后一个父类还没有找到对应的方法时,就会来到动态方法解析。动态解析,就是意味着开发者可以在这里动态的往rw_t中添加方法实现,这样的话系统再次遍历rw_t就会找到对应的方法进行调用了。

动态方法解析的流程示意图如下:

image

主要涉及到了两个方法:

+resolveInstanceMethod://添加对象方法  也就是-开头的方法
+resolveClassMethod://添加类方法  也就是+开头的方法

我们在实际项目中进行验证:

image.png

动态添加类方法也是如此,只不过是添加到元类对象中(此时run方法已经改成了个类方法)

image

而且我们也发现,动态添加方法的话其实无非就是找到方法实现,添加到类对象或元类对象中,至于这个方法实现是什么形式都没有关系,比如说我们再给对象方法添加方法实现时,这个实现方法可以是个类方法,同样给类方法动态添加方法实现时也可以是对象方法。也就是说系统根本没有区分类方法和对象方法,只要把imp添加到元类对象的rw_t中就是类方法,添加到类对象中就是对象方法。

image
  当我们在消息发送和动态消息解析阶段都没有找到对应的imp的时候,系统回来到最后一个消息转发阶段。所谓消息转发,就是你这个消息处理不了后可以找其他人或者其他方法来代替,消息转发的流程示意图如下: image

即分为两步,第一步是看能不能找其他人代你处理这方法,可以的话直接调用这个人的这个方法,这一步不行的话就来到第二部,这个方法没有的话有没有可以替代的方法,有的话就执行替代方法。我们通过代码来验证:

我们调用dog的run方法是,因为dog本身没有实现这个方法,所以不能处理。正好cat实现了这个方法,所以我们就将这个方法转发给cat处理:

image

我们发现,确实调用了小猫run方法,但是只转发方法执行者太局限了,要求接收方法对象必须实现了同样的方法才行,否则还是无法处理,所以实用性不强。这时候,我们可以通过methodSignatureForSelector来进行更大限度的转发。

需要注意的是要想来到methodSignatureForSelector这一步需要将*****forwardingTargetForSelector返回nil(即默认状态)否则系统找到目标执行者后就不会再往下转发了。*

开发者可以在forwardInvocation:方法中自定义任何逻辑。

////为方法重新转发一个目标执行
//- (id)forwardingTargetForSelector:(SEL)aSelector{
//    if (aSelector == @selector(run)) {
//        //dog的run方法没有实现 所以我们将此方法转发到cat对象上去实现 也就是相当于将[dog run]转换成[cat run]
//        return [[Cat alloc] init];
//    }
//    return [super forwardingTargetForSelector:aSelector];
//}

//方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if (aSelector == @selector(run)) {
        //注意:这里返回的是我们要转发的方法的签名 比如我们现在是转发run方法 那就是返回的就是run方法的签名

        //1.可以使用methodSignatureForSelector:方法从实例中请求实例方法签名,或者从类中请求类方法签名。
        //2.也可以使用instanceMethodSignatureForSelector:方法从一个类中获取实例方法签名
        //这里使用self的话会进入死循环 所以不可以使用 如果其他方法中有同名方法可以将self换成其他类
//        return [self methodSignatureForSelector:aSelector];
//        return [NSMethodSignature instanceMethodSignatureForSelector:aSelector];
        
        //3.直接输入字符串
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}
//当返回方法签名后 就会转发到这个方法  所以我们可以在这里做想要实现的功能  可操作空间很大
//这个anInvocation里面有转发方法的信息,比如方法调用者/SEL/types/参数等等信息
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    //这样写不安全  可以导致cat被过早释放掉引发怀内存访问
//    anInvocation.target = [[Cat alloc] init];
    
    Cat *ca = [[Cat alloc] init];
    //指定target
    anInvocation.target = ca;
    //对anInvocation做出修改后要执行invoke方法保存修改
    [anInvocation invoke];
    
    //或者干脆一行代码搞定
    [anInvocation invokeWithTarget:[[Cat alloc] init]];
    
    //上面这段代码相当于- (id)forwardingTargetForSelector:(SEL)aSelector{}中的操作
    //当然 转发到这里的话可操作性更大  也可以什么都不写 相当于转发到的这个方法是个空方法  也不会报方法找不到的错误
    //也可以在这里将报错信息提交给后台统计 比如说某个方法找不到提交给后台 方便线上错误收集
    //...很多用处
}

当然我们也可以访问修改anInvocation的参数,比如现在run有个age参数,

  // 参数顺序:receiver、selector、other arguments
    int age;    
    //索引为2的参数已经放到了&age的内存中,我们可以通过age来访问
    [anInvocation getArgument:&age atIndex:2];
    NSLog(@"%d", age + 10);

我们发现,消息转发有两种情况,一种是forwardingTargetForSelector,一种是methodSignatureForSelector+forwardInvocation:

其实,第一种也称快速转发,特点就是简单方便,缺点就是能做的事情有限,只能转发消息调用者;第二种也称标准转发,缺点就是写起来麻烦点,需要写方法签名等信息,但是好处就是可以很大成都的自定义方法的转发,可以在找不到方法imp的时候做任何逻辑。

当然,我们上面的例子都是通过对象方法来演示消息转发的,类方法同样存在消息转发,只不过对应的方法都是类方法,也就是-变+

   image

所以,以上关于消息传递过程可以用下面这个流程图进一步总结:

image

关于源码阅读指南:

image

5、super的相关内容

首先我们来看一下这段代码:

image

 我们发现最终的打印结果和我们预期的不一样,按我们的思路Super就是指的的Dog的父类Animal,Animal调用class方法应该返回Animal 但是结果却不是这样,这是为什么?首先我们先将这段代码转换成c++底层代码来一探究竟:

static instancetype _I_Dog_init(Dog * self, SEL _cmd) {
    self = ((Dog *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Dog"))}, sel_registerName("init"));
    if (self) {
        // NSLog(@"%@",[self class]);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_f1_q0392lf551qfbg1b5sy48qb80000gn_T_Dog_db6ed5_mi_0,((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class")));
        
       //NSLog(@"%@",[self superclass]);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_f1_q0392lf551qfbg1b5sy48qb80000gn_T_Dog_db6ed5_mi_1,((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("superclass")));
        
        //NSLog(@"%@",[super class]);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_f1_q0392lf551qfbg1b5sy48qb80000gn_T_Dog_db6ed5_mi_2,((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Dog"))}, sel_registerName("class")));
        
        //NSLog(@"%@",[super superclass]);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_f1_q0392lf551qfbg1b5sy48qb80000gn_T_Dog_db6ed5_mi_3,((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Dog"))}, sel_registerName("superclass")));
    }
    return self;
}

将上述代码简化后得到下面的结果:

image

我们发现,当self调用class方法时,是执行的objc_msdSend(self,@selector(class))函数,消息的接收者是当前所在类的实例对象(Dog) , 这个时候就会去self所在类 Dog去查找class方法 , 如果当前类Dog没有class方法会向其父类Animal类找 class 方法, 如果Animal类也没有找到class方法,最终会找到最顶级父类NSObject的class方法, 最终找到NSObject的class方法 ,并调用了object_getClass(self) ,由于消息接收者是 self 当前类实例对象, 所以最终 [self class]输出Dog(class方法是返回方法调用者的类型,superclass方法是返回方法调用者的父类)

[self superclass] 也是同理,找到superclass方法,然后返回调用者的父类,即Animal;

但是当我们调用super的class方法时,底层不是转换成objc_msdSend而是变成了objc_msgSendSuper函数。这个函数有两个参数,第一个参数是个结构体,结构体中有两个成员:方法调用者和调用者的父类,第二个参数就是方法名,也就是class方法的SEL。

[super class] ->
objc_msgSendSuper(
                  //第一个参数:结构体
                  {self,//方法调用者
                   class_getSuperclass(objc_getClass("Dog"))//当前类的父类
                  },
                  //第二个参数:方法名
                  sel_registerName("class")));

所以,我们看到[self class]和[super class],他们转换成的底层实现都不一致。objc_msgSendSuper函数的作用是告诉方法调用者去其父类中查找该方法,也就是相比objc_msdSend函数而言少了去自己类中查找方法这一步,而是直接去父类中找class方法,但是方法调用者还是没变,都是Dog。class方法和superclass它们都是返回方法调用者的类型或父类,所以[super class]和[super superclass]还是返回的Dog的类型和父类,所以打印结果是Dog和Animal,与[self class]和[self superclass]结果一致。

所以,总结起来就是,super方法底层会转换为objc_msgSendSuper函数的调用,这个函数的作用是告诉方法调用者去父类中查找方法。

6、runtime的常见API与应用案例

动态创建一个类(参数:父类,类名,额外的内存空间)
Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes)

注册一个类(要在类注册之前添加成员变量)
void objc_registerClassPair(Class cls)

销毁一个类
void objc_disposeClassPair(Class cls)

获取isa指向的Class
Class object_getClass(id obj)

设置isa指向的Class
Class object_setClass(id obj, Class cls)

判断一个OC对象是否为Class
BOOL object_isClass(id obj)

判断一个Class是否为元类
BOOL class_isMetaClass(Class cls)

获取父类
Class class_getSuperclass(Class cls)

获取一个实例变量信息
Ivar class_getInstanceVariable(Class cls, const char *name)

拷贝实例变量列表(最后需要调用free释放)
Ivar *class_copyIvarList(Class cls, unsigned int *outCount)

设置和获取成员变量的值
void object_setIvar(id obj, Ivar ivar, id value)
id object_getIvar(id obj, Ivar ivar)

动态添加成员变量(已经注册的类是不能动态添加成员变量的)
BOOL class_addIvar(Class cls, const char * name, size_t size, uint8_t alignment, const char * types)

获取成员变量的相关信息
const char *ivar_getName(Ivar v)
const char *ivar_getTypeEncoding(Ivar v)

获取一个属性
objc_property_t class_getProperty(Class cls, const char *name)

拷贝属性列表(最后需要调用free释放)
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)

动态添加属性
BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes,
                       unsigned int attributeCount)

动态替换属性
void class_replaceProperty(Class cls, const char *name, const objc_property_attribute_t *attributes,
                           unsigned int attributeCount)

获取属性的一些信息
const char *property_getName(objc_property_t property)
const char *property_getAttributes(objc_property_t property)

获得一个实例方法、类方法
Method class_getInstanceMethod(Class cls, SEL name)
Method class_getClassMethod(Class cls, SEL name)

方法实现相关操作
IMP class_getMethodImplementation(Class cls, SEL name)
IMP method_setImplementation(Method m, IMP imp)
void method_exchangeImplementations(Method m1, Method m2)

拷贝方法列表(最后需要调用free释放)
Method *class_copyMethodList(Class cls, unsigned int *outCount)

动态添加方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)

动态替换方法
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)

获取方法的相关信息(带有copy的需要调用free去释放)
SEL method_getName(Method m)
IMP method_getImplementation(Method m)
const char *method_getTypeEncoding(Method m)
unsigned int method_getNumberOfArguments(Method m)
char *method_copyReturnType(Method m)
char *method_copyArgumentType(Method m, unsigned int index)

选择器相关
const char *sel_getName(SEL sel)
SEL sel_registerName(const char *str)

用block作为方法实现
IMP imp_implementationWithBlock(id block)
id imp_getBlock(IMP anImp)
BOOL imp_removeBlock(IMP anImp)

runtime相关API

这些api中有些我们用的比较少,有的比较常用,比如我们在修改UITextField的占位文字颜色的话,可以通过获取UITextField的成员列表,发现其中占位文字的显示其实是个placeholderLabel,所以我们直接可以通过kvo修改这个label的颜色:

image

还有比如经常用的字典转模型框架MJExtension中,也是通过runtime函数遍历所有的属性或者成员变量,然后利用KVO去设值等等。

另外需要注意的一点是,我们一般将动态添加方法、方法交换等等这些运行时操作放在load方法里面实现,我们在之前讲解分类的时候提到了:

当类被引用进项目的时候就会执行load函数(在main函数开始执行之前),与这个类是否被用到无关,每个类的load函数只会自动调用一次.也就是load函数是系统自动加载的,load方法会在runtime加载类、分类时调用。

方法交换一般用在将系统的某个方法交换成我们自己写的方法从而实现相应功能,这里也有两点需要注意:

  ①避免死循环

方法交换交换的只是两个方法的实现,也就是imp的交换,所以原理如下:

image

用代码来演示,比如我们现在要拦截所有按钮的点击事件,在做出点击相应之前打印出相关信息,UIButton继承自UIControl,button的addTarget: action: forControlEvents:方法底层也是调用了UIControl的sendAction:to:forEvent:方法,所以我们需要将sendAction:to:forEvent:来进行方法交换,交换成我们自己的方法:

#import "UIControl+Extension.h"
#import <objc/runtime.h>

@implementation UIControl (Extension)
+ (void)load
{
    //这里最好加上一个dispatch_once 虽然load方法原则上只会调用一次,但是万一开发者手动再调用一次的话,那么两个方法交换了两次就相当于没交换
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method method1 = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
        Method method2 = class_getInstanceMethod(self, @selector(my_sendAction:to:forEvent:));
        method_exchangeImplementations(method1, method2);
    });
}
//我们自己实现的方法
- (void)my_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
{
    NSLog(@"%@-%@-%@", self, target, NSStringFromSelector(action));
    // 调用系统原来的实现
    //调用sendAction:会出现死循环 因为sendAction:方法的实现是my_sendAction:
    //[self sendAction:action to:target forEvent:event];
    //所以需要调用my_sendAction:方法来实现系统原来的实现 因为my_sendAction:方法实现就是系统的sendAction:方法实现
    [self my_sendAction:action to:target forEvent:event];
}

②需要注意类簇,确保交换的是正确的类

比如我们在使用NSMutableArray添加数据的时候,如果添加nil会出错,所以我们要将系统的这个方法交换成我们自己的方法从而可以进行判断,我们也知道

addObject:方法底层是调用的insertObject:atIndex:方法,所以:

#import "NSMutableArray+Extension.h"
#import <objc/runtime.h>

@implementation NSMutableArray (Extension)
+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method method1 = class_getInstanceMethod(self, @selector(insertObject:atIndex:));
        Method method2 = class_getInstanceMethod(self, @selector(my_insertObject:atIndex:));
        method_exchangeImplementations(method1, method2);
    });
}

- (void)my_insertObject:(id)anObject atIndex:(NSUInteger)index
{
    if (anObject == nil) return;
    [self my_insertObject:anObject atIndex:index];
}

但是我们发现,这样做还是不行,根本进不去我们自己的my_insertObject:方法就会出错:

//reason: '*** -[__NSArrayM insertObject:atIndex:]: object cannot be nil'

这是因为我们交换的类不对,在出错信息我们可以看到这个insertObject:atIndex:是存在__NSArrayM中的,所以我们应该交换__NSArrayM的方法而不是NSMutableArray的方法:

#import "NSMutableArray+Extension.h"
#import <objc/runtime.h>

@implementation NSMutableArray (Extension)
+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        //这里要是交换方法的真实类
        Class cls = NSClassFromString(@"__NSArrayM");
        Method method1 = class_getInstanceMethod(cls, @selector(insertObject:atIndex:));
        Method method2 = class_getInstanceMethod(cls, @selector(mj_insertObject:atIndex:));
        method_exchangeImplementations(method1, method2);
    });
}

- (void)mj_insertObject:(id)anObject atIndex:(NSUInteger)index
{
    if (anObject == nil) return;
    
    [self mj_insertObject:anObject atIndex:index];
}

几种常见类的真实类型:

image

method swizzling(俗称黑魔法),简单说就是进行方法交换,可以通过以下几种方式实现:

利用 method_exchangeImplementations 交换两个方法的实现
利用 class_replaceMethod 替换方法的实现
利用 method_setImplementation 来直接设置某个方法的IMP

8、weak的底层实现

weak使我们开发中常见的修饰符,它是一种“非拥有关系”的指针(即弱引用)。通过weak修饰的指针变量,都不会改变被引用对象的引用计数,最主要的作用是为了防止引用循环(retained cycle),经常用于block和delegate。

weak、assign以及unsafe_unretained都是弱引用,这三者有什么区别吗?

在一个对象被释放后,weak会自动将指针指向nil,而assign和unsafe_unretained则不会。在iOS中,向nil发送消息时不会导致崩溃的,所以assign和unsafe_unretained会导致野指针的错误unrecognized selector sent to instance。

weak 只可以修饰对象。如果修饰基本数据类型,编译器会报错-“Property with ‘weak’ attribute must be of object type”。
   assign 可修饰对象,和基本数据类型。unsafe_unretained也是只可修饰对象,所以用assign修饰对象和unsafe_unretained修饰对象其实是一样的。

weak不论是用作property修饰符还是用来修饰一个变量的声明其作用是一样的,就是不增加新对象的引用计数,被释放时也不会减少新对象的引用计数,同时在新对象被销毁时,weak修饰的属性或变量均会被设置为nil,这样可以防止野指针错误,那么runtime如何将weak修饰的变量的对象在销毁时自动置为nil?weak底层实现原理是什么?

//用作property修饰符
    @property(weak,nonatomic) NSObject *weakObj;
//修饰变量的声明
    NSObject *obj = [[NSObject alloc]init];
    __weak typeof(obj)weakObj = obj;    
runtime对注册的类会进行布局,对于weak修饰的对象会放入一个hash表中。用weak指向的对象内存地址作为key,当此对象的引用计数为0的时候会dealloc,假如weak指向的对象内存地址是a,那么就会以a为键在这个weak表中搜索,找到所有以a为键的weak对象,从而设置为nil。

比如我们上面__weak typeof(obj)weakObj = obj;这个例子:

当为weakObj这一weak类型的对象赋值时,编译器会根据obj的地址为key去查找weak哈希表,这个表可以理解成一个数组,将weakObj对象的地址(&weakObj)加入到数组中。当obj引用计数为0时,会执行dealloc函数,在执行该函数时,编译器会以obj变量的地址去查找weak哈希表的值,并将数组里所有 weak对象全部赋值为nil。

也就是,系统会创建一个全局的weak表(其实是一个hash(哈希)表),Key是所指对象的地址,Value是weak指针的地址数组

NSObject *obj = [[NSObject alloc]init];
    __weak typeof(obj)weakObj = obj; 
    __weak typeof(obj)weakTest = obj; 


    PeopleClass *perple = [[PeopleClass alloc]init];
    __weak typeof(perple)weakTeacher = perple; 


    AnimalClass *animal = [[AnimalClass alloc]init];
    __weak typeof(animal)weakCat = animal; 
    __weak typeof(animal)weakDog= animal; 
    __weak typeof(animal)weakPig = animal;
image.png

接下来我们在源码中去查看weak的实现:

property中使用weak修饰

@property (nonatomic,weak) NSObject *referent;

// 底层实现函数入口
id objc_storeWeak(id *location, id newObj)
{
    return storeWeak<DoHaveOld, DoHaveNew, DoCrashIfDeallocating>
        (location, (objc_object *)newObj);
}

使用__weak修饰对象:

__weak NSObject *referent

// 底层实现函数入口
id objc_initWeak(id *location, id newObj)
{
    if (!newObj) {
        *location = nil;
        return nil;
    }

    return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
        (location, (objc_object*)newObj);
}

不论是使用weak还是__weak底层都是调用storeWeak这个函数,区别在于模板的第一个参数HaveOld,这个参数用来表示这个弱指针是否有值。

Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。weak表其实是一个hash(哈希)表,Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象的地址)数组。

weak 的实现原理可以概括一下三步:

1、初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。【property中使用weak修饰不存在这一步】

2、添加引用时:objc_initWeak函数会调用 objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。

3、释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。

接下来用伪代码说明一下具体流程

//weak对象创建
static id storeWeak(id *location, objc_object *newObj){
    //首先,我们可以拿到两个值:一个是弱指针指向的对象obj,一个是弱指针对象weakObj
    if(全局weak表中存在一个key为&obj的键值对){
        1、取出&obj这个key对应的value,这个value是个数组array;
        2、将&weakObj插入到这个数组中 [weak_entry_insert(weak_table, &new_entry);]
        
    }else{//--如果weak表中没有一个key为&obj的键值对 那么说明这个对象从来没有被弱指针对象指向过  所以就需要在weak表中创建一个新的键值对存储了
        1、创建这样一个键值对
        NSArry *value = @[&weakObj];
        NSDictory *dic = @{&obj:value};
        
        2、查看weak剩余存储空间还多不多
        if(weak表使用空间不足3/4)
            将键值对dic插入到weak表中
        }else{//如果剩余空间不如1/4了  那么就进行扩容
            /* Grow if at least 3/4 full.
             if (weak_table->num_entries >= old_size * 3 / 4) {
             weak_resize(weak_table, old_size ? old_size*2 : 64);
             }
             */
            将weak表的存储空间扩展其两倍大;
            将旧表中的数据通过for循环 全部copy到新表中
            将旧表内存空间释放
            将将键值对dic插入到weak表中
        }
}
//这个weak表很像我们之前讲方法缓存中的那个cache_t,其实都一样,weak表中插入数据也不是按照索引去插入的,而是由&obj&mask得到一个索引,如果这个索引有数据的话那就向下再找空间存储
size_t begin = hash_pointer(new_entry->referent) & (weak_table->mask);
size_t index = begin;
size_t hash_displacement = 0;
while (weak_entries[index].referent != nil) {
    index = (index+1) & weak_table->mask;
    if (index == begin) bad_weak_table(weak_entries);
        hash_displacement++;
}
//weak的释放
weak_clear_no_lock(weak_table_t *weak_table, id referent_id){
    1.从weak表中取出这个以&obj为key的键值对
    2.取出键值对的value 存放弱指针的数组,weakAry
    for(int i = 0,i<weakAry.count,i++){
        将weakAry中的指针指向的内容全部置为nil
    }
    3.从weak表中删除这个键值对
    weak_entry_remove(weak_table, entry);
}

文章转自:https://www.cnblogs.com/gaoxiaoniu/p/10801356.html
如有侵权,请联系本人删除

上一篇下一篇

猜你喜欢

热点阅读