前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >OC类的原理(二)

OC类的原理(二)

作者头像
拉维
发布2021-10-21 14:41:02
3500
发布2021-10-21 14:41:02
举报
文章被收录于专栏:iOS小生活iOS小生活

WWDC关于Runtime的优化

在WWDC2020大会上,苹果公告了对于Runtime的优化(https://developer.apple.com/videos/play/wwdc2020/10163/),本次优化在API层面未作改动,其优化的是隐藏在API后面的内部数据结构。

本次优化主要由三个方面的改动:

第一个改动是类的运行时数据结构的改变;

第二个改动是OC方法列表的变化;

第三改动是tagged pointer格式的变化。

这些优化可以极大地减少应用程序在运行的时候的内存占用,进而提升了应用程序的运行速度。

我在之前的文章中,是以objc4-756.2源码为研究对象进行研究的,而现在比较新的源码是objc4-818.2,这两份源码中的类的结构是不一样的,接下来也是以objc4-818.2源码为研究对象来研究一下类的成员变量、属性、方法等的存储。

类内存中的ro数据

如上图所示,断点后,我执行了三个lldb指令,作用分别是打印类的地址、获取到bits、获取到rw。

接下来打印rw的结构:

好像看不太懂诶,接下来咱们看一下class_rw_t的结构:

代码语言:javascript
复制
struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint16_t witness;
#if SUPPORT_INDEXED_ISA
    uint16_t index;
#endif

    explicit_atomic<uintptr_t> ro_or_rw_ext;

    Class firstSubclass;
    Class nextSiblingClass;
    
    ......

    const class_ro_t *ro() const {
        auto v = get_ro_or_rwe();
        if (slowpath(v.is<class_rw_ext_t *>())) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->ro;
        }
        return v.get<const class_ro_t *>(&ro_or_rw_ext);
    }

    void set_ro(const class_ro_t *ro) {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            v.get<class_rw_ext_t *>(&ro_or_rw_ext)->ro = ro;
        } else {
            set_ro_or_rwe(ro);
        }
    }

    const method_array_t methods() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
        } else {
            return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods()};
        }
    }

    const property_array_t properties() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->properties;
        } else {
            return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties};
        }
    }

    const protocol_array_t protocols() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->protocols;
        } else {
            return protocol_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProtocols};
        }
    }
}

可以看到,rw中可以通过ro()、methods()、properties()、protocols()等方法获取到ro、methods、properties、protocols等数据。

接下来我们就实际操演一遍,来获取ro中的成员变量。

现在获取到ro了,接下来拿到其中的ivars:

这里是通过get()函数来获取成员变量数组中的各个元素。

方法的编码

现在有这样一个类:

我将其通过clang指令编译成c++源码,如下:

可以看到,属性在CPP底层都会被转换成成员变量。

在C++源码中,我们也可以看到这样的东西:

红框内的这些都叫做类型编码,关于类型编码,我在Runtime——消息转发流程中有过介绍,这里就不赘述了。

下面我总结了各种类型下的编码,可以直接拷贝运行:

代码语言:javascript
复制
#pragma mark - 各种类型编码
void lgTypes(void){
    NSLog(@"char --> %s",@encode(char));
    NSLog(@"int --> %s",@encode(int));
    NSLog(@"short --> %s",@encode(short));
    NSLog(@"long --> %s",@encode(long));
    NSLog(@"long long --> %s",@encode(long long));
    NSLog(@"unsigned char --> %s",@encode(unsigned char));
    NSLog(@"unsigned int --> %s",@encode(unsigned int));
    NSLog(@"unsigned short --> %s",@encode(unsigned short));
    NSLog(@"unsigned long --> %s",@encode(unsigned long long));
    NSLog(@"float --> %s",@encode(float));
    NSLog(@"bool --> %s",@encode(bool));
    NSLog(@"void --> %s",@encode(void));
    NSLog(@"char * --> %s",@encode(char *));
    NSLog(@"id --> %s",@encode(id));
    NSLog(@"Class --> %s",@encode(Class));
    NSLog(@"SEL --> %s",@encode(SEL));
  
    // 数组
    int array[] = {1,2,3};
    NSLog(@"int[] --> %s",@encode(typeof(array)));
  
   // 结构体
    typedef struct person{
        char *name;
        int age;
    }Person;
    NSLog(@"struct --> %s",@encode(Person));
    
    // 联合体
    typedef union union_type{
        char *name;
        int a;
    }Union;
    NSLog(@"union --> %s",@encode(Union));

    int a = 2;
    int *b = {&a};
    NSLog(@"int[] --> %s",@encode(typeof(b)));
}

接下来咱来解释下红框内的每一个编码都代表什么意思:

第一个,nickName,是属性nickName的getter方法,其类型编码是@16@0:8

第1个@指的是返回值是一个对象;

第2个16指的是总共所占用的内存;

第3个@代表的是第一个参数self是一个对象(每一个方法都有两个隐藏参数self和cmd);

第4个0代表的是从第0号位置开始;

第5个:代表的是第二个参数_cmd是一个SEL;

第6个8代表的是从第8位的位置开始。

接下来再来分析一下setNickName:,其类型编码是:v24@0:8@16

第1个v指的是void,没有返回值;

第2个24指的是总共所占用的内存;

第3个@代表的是第一个参数self是一个对象(每一个方法都有两个隐藏参数self和cmd);

第4个0代表的是从第0号位置开始;

第5个:代表的是第二个参数_cmd是一个SEL;

第6个8代表的是从第8位的位置开始;

第7个@代表的是第三个参数,即传入的字符串是一个对象;

第8个16代表的是从第16位的位置开始。

setter方法的底层原理

如上图所示,在c++源码中,还可以看到,有些属性(nickName)的set方法是调用了objc_setProperty函数,而有些(name、aname)是直接通过内存偏移进行的赋值,这是为啥呢?

我们知道,每一个属性都会有一个对应的setter方法,比如属性name的setter方法是setName,属性age的setter方法是setAge。所有的setter方法的本质都是给某一块内存区域赋值,我们现在要讨论的就是如何找到对应的内存区域并且给其赋值。

现在我们知道,有些上层setter方法都会重定向调用objc_setProperty函数,将对应的ivar信息传递进去,该函数内部会有对应属性赋值的底层代码实现。那么objc_setProperty函数是怎么被调用的呢,接下来我们就来研究一下。

首先我们打开LLVM工程源码,搜索objc_setProperty

可以看到,在getSetPropertyFn函数中调用CreateRuntimeDunction函数生成objc_setProperty函数。

然后搜索getSetPropertyFn

发现是在GetPropertySetFunction中调用了getSetPropertyFn

然后搜索GetPropertySetFunction

......

可以看到,在GetSetProperty或者SetPropertyAndExpressionGet这两个case下才会调用GetPropertySetFunction函数,这说明GetPropertySetFunction函数的调用是跟PropertyImplStrategy枚举取值有关的。

接下来我们就来搜索探究下在哪里给PropertyImplStrategy枚举赋值。

搜索PropertyImplStrategy,首先看到PropertyImplStrategy可以有如下取值:

然后我们在看看其初始化赋值的地方:

可以看到,当采取copy内存管理策略的时候,就会给kind赋值为GetSetProprty,此时就会最终调用到objc_setProperty函数。

接下来我们验证一下:

clang指令编译成C++源码 :

可以看到,nickName和acnickName(采取了copy内存管理策略)的setter方法最终是调用了objc_setProperty函数,而nnickName和anickName的setter(采用了默认的strong内存管理策略)方法最终是通过位移的方式进行直接赋值的。

到这里,不知道大家有没有一个疑问,就是为啥必须要用LLVM源码进行研究呢?为什么不能直接在objc源码中进行研究?

通过上面的分析我们已经了解到,采取copy策略的setter方法在C++底层源码中都会被重定向到objc_setProperty函数,而这个重定向的过程就是在编译的时候进行的;在程序运行时,name属性(采取Copy内存管理策略)的设置方法会通过setName这个SEL找到对应的IMP,而这个对应的IMP就会被重定向到objc_setProperty函数中

通过LLDB指令获取类方法

class_getClassMethod

看下面这个例子:

代码语言:javascript
复制
@interface LGPerson : NSObject
- (void)sayHello;
+ (void)sayHappy;
@end

// 执行如下代码:
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);
    
    Method method1 = class_getClassMethod(pClass, @selector(sayHello));
    Method method2 = class_getClassMethod(metaClass, @selector(sayHello));

    Method method3 = class_getClassMethod(pClass, @selector(sayHappy));
    Method method4 = class_getClassMethod(metaClass, @selector(sayHappy)); // ?
    
    NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4);

打印结果如下:

sayHello是实例方法,因此通过class_getClassMethod来查找肯定是找不到的,所以前两个都是0x0没有问题;

sayHappy是类方法,通过class_getClassMethod(pClass, @selector(sayHappy))能找到对应的方法,所以第三个打印0x1000022a0也没啥问题;

问题点就在于第四个,sayHappy是LGPerson的类方法,class_getClassMethod(metaClass, @selector(sayHappy))表示的是在LGPerson的原类中查找类方法,这怎么找到了呢?并且为啥找到的跟第三个打印(在LGPerson的类中查找类方法)是一模一样的呢?想要弄明白这个问题,就得看一下class_getClassMethod的源码了:

代码语言:javascript
复制
Method class_getClassMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;

    return class_getInstanceMethod(cls->getMeta(), sel);
}

再来看cls->getMeta():

代码语言:javascript
复制
Class getMeta() {
    if (isMetaClass()) return (Class)this;
    else return this->ISA();
}

重点来了,当cls是元类的时候,cls->getMeta()返回自身(注意,并不是返回根元类哦!)

因此,上面的method3和method4打印是一样的。

class_getMethodImplementation

再来看这么一段代码:

代码语言:javascript
复制
    /*
    - (void)sayHello;
    + (void)sayHappy;
    */
    
    LVPerson *person = [LVPerson alloc];
    Class pClass     = object_getClass(person);
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);

    IMP imp1 = class_getMethodImplementation(pClass, @selector(sayHello));
    IMP imp2 = class_getMethodImplementation(metaClass, @selector(sayHello));
    IMP imp3 = class_getMethodImplementation(pClass, @selector(sayHappy));
    IMP imp4 = class_getMethodImplementation(metaClass, @selector(sayHappy));

    NSLog(@"%p-%p-%p-%p",imp1,imp2,imp3,imp4);

打印结果如下:

按道理,imp1和imp4是有值的,这毋庸置疑;而imp2和imp3不应该有值啊,但是这里却有值,而且imp2和imp3的值还是一样的,这是为什么呢?

我们接下来看一下class_getMethodImplementation的源码实现:

可以看到,如果没有找到对应的IMP的话,就会直接返回_objc_msgForward函数。也就是说,这里的imp2和imp3打印出来的都是_objc_msgForward函数的地址。

以上。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-10-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 iOS小生活 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档