OC类的底层原理结构

这里有一篇apple官方关于runtime底层优化的思路,为什么要定义class_rw_t、class_ro_t等等这么多结构体:
https://developer.apple.com/videos/play/wwdc2020/10163/

一、类的isa分析

新建一个空工程,创建LGPerson类,直接通过clang -rewrite-objc main.m生成编译后的文件;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 0x00007ffffffffff8
        LGPerson *p = [LGPerson alloc];
        NSLog(@"%@",p);  
    }
    return 0;
}

因为我要分析类,所以要看Class,再main.cpp中发现以下代码,这里可以看出Class的本质是objc_class结构体指针;

typedef struct objc_class *Class;

在objc源码中(https://opensource.apple.com/releases/ 可以找到不同版本的源码)可以找到objc_class结构体具体定义,如下;

objc_class结构体源码

之前在分析对象原理的时候,有提过影响对象内存大小的因素主要是成员变量,所以这里也可以类比对象,主要分析类的成员变量,即

// Class ISA;
Class superclass;
cache_t cache;           
class_data_bits_t bits;   

这里还加了个注释Class ISA,提示我们存在隐藏变量isa,这个主要是父结构体中有isa这个变量;

通过打印class的内存地址会发现,第一个地址isa指向的是LGPerson,第二个地址指向NSObject,即superclass,到这里都还比较明朗,但是打印第三个开始,就是一串莫名其妙的数字了,接下来就只能进一步探索了;

二、内存偏移分析

这里先做一个简单的分析,通过创建a、b两个int类型数据,分别打印他们的值跟地址;通过打印我们会发现a、b两个指针内存地址只相差4字节,而我们打印int的size会发现它刚好需要4字节,这与我们的两个指针偏移大小吻合;



这里就可以得出一个结论,首先是a、b两个指针内存都是在栈上,栈内存连续的两个地址,指向同一片内存空间10;


接下来我们创建两个对象看一下,打印对象的地址会发现,他们其实是不连续的,这也可以说明我们的对象其实是在堆中开辟的内存;
打印对象指针会发现,他们相差符合对象内存对齐的原则,所以对象的指针还是在栈上的;


对象地址

简单的画一下对象内存指向:


对象指针指向

数组指针

下面换种方式,通过数组指针来类比当前对象,我们会发现数组指针地址跟对象的的第一个元素C[0]是同一个地址0x16fdff210,即他们都指向数组的首地址;


数组指针

然后换种方式,通过一个int指针d,执行c,打印结果如上图d=&c,d+1 = &c[1],这个时候我们大胆的猜测,可以通过指针地址平移的方式,即增加步长,遍历得到数组指针相同的内容,如下图所示;


指针平移得到值

根据以上分析,OC类也可以类比上诉方式,OC 类 结构 首地址->isa, 平移一些大小也可以得到我们想要的内容;
接下来我们就可以通过,LGPerson.class地址 -> 平移得到所有我们想要值;

三、类的结构属性分析

上一节我们有提到,可以通过指针平移的方式,得到我们想要的内容,
首先尝试一下通过指针首地址,平移8个字节(因为isa大小是8),打印出我们的父类NSObject,得到它的指针地址0x100008320,当我们打印NSObject.class类对象地址时,会发现跟我们x/4gx打印出来的第二个值一样,就证明当前的类第一个指向isa,偏移8得到NSObject的指针地址,指向后面的值地址0x0000000100721140,这跟我们上面猜想的是一致的;



接下来就是要探索我们之前剩下的几个成员变量cache_t、class_data_bits_t了,这里比较关键的就是要知道cache_t的大小,才能知道bits的位置;



通过查看objc源码我们会发现cache_t本质是个结构体,而影响结构体大小的因素是成员变量,所以这里我们得找一下它里面的成员变量;
cache_t结构体

通过分析会发现,成员变量就俩,一个是uintptr_t类型_bucketsAndMaybeMask,大小是8,另一个是联合体,联合体里面包含一个结构体跟_originalPreoptCache指针类型,因为联合体内部是互斥的,所以这里可以分析union大小是8,所以分析到这里,可以得到cache_t大小是16;

所以这个时候,我们就可以通过指针偏移32位(isa是8,superclass也是8,cache_t是16)得到我们想要的class_data_bits_t;这个时候又会发现一个问题,通过打印这片内存无法得到里面具体的内容,这又是个难题了;


通过观察class_data_bits_t结构体内部会发现,它有提供api,这个可以获取class_rw_t,这个是类里面非常关键的结构体,也是我们要着重研究的对象;

class_rw_t* data() const {
#if __BUILDING_OBJCDT__
        return (class_rw_t *)((uintptr_t)ptrauth_strip((class_rw_t *)bits,
                                                           CLASS_DATA_BITS_RW_SIGNING_KEY) & FAST_DATA_MASK);
#else
        return (class_rw_t *)((uintptr_t)ptrauth_auth_data((class_rw_t *)bits,
                                                           CLASS_DATA_BITS_RW_SIGNING_KEY,
                                                           ptrauth_blend_discriminator(&bits,
                                                                                       CLASS_DATA_BITS_RW_DISCRIMINATOR)) & FAST_DATA_MASK);
#endif

    }
class_rw_t结构-objc4-866.9

class_rw_t内部也有api可以获取到class_ro_t结构体;

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);
    }

通过底层api调用,我们可以打印出想要探索的结构体信息;


lldb获取class_rw_t、class_ro_t

这个时候可以尝试打印一下成员变量值,看看是不是我们定义的属性;



通过打印会发现,ivars跟baseProperties分别是ivar_list_t、property_list_t结构体,他们都是继承自entsize_list_tt,这是结构体设计的一种复用方式,所以这时真正要探索的是entsize_list_tt;
struct ivar_list_t : entsize_list_tt<ivar_t, ivar_list_t, 0> {
    bool containsIvar(Ivar ivar) const {
        return (ivar >= (Ivar)&*begin()  &&  ivar < (Ivar)&*end());
    }
};

struct property_list_t : entsize_list_tt<property_t, property_list_t, 0> {
};

entsize_list_tt中的两个成员变量entsizeAndFlags、count,上图都有打印出来了,我们现在比较关心的就是是否有获取当前属性的方法;

entsize_list_tt结构体

通过api可以找到get、getOrEnd方法,这俩方法意思比较明显,传入参数也是个序号,尝试调用该方法,很惊喜也在情理之中,成功获取到我们定义的两个属性;


lldb打印成员变量

越界了,跟源码中的提示一样

四、类方法归属分析

分析完属性成员变量,最让我好奇的也是最值得探究的当属OC的方法,lldb打印的字面分析,baseMethods应该就是存储方法的结构体;

(lldb) p (class_data_bits_t *)0x00000001000082e0
(class_data_bits_t *) $4 = 0x00000001000082e0
(lldb) p $4->data()->ro()
(const class_ro_t *) $5 = 0x0000000100008228
(lldb) p *$5
(const class_ro_t) $6 = {
  flags = 388
  instanceStart = 8
  instanceSize = 40
  reserved = 0
   = {
    ivarLayout = 0x0000000100003e9f "\U00000012"
    nonMetaclass = 0x0000000100003e9f
  }
  name = {
    std::__1::atomic<const char *> = "LGPerson" {
      Value = 0x0000000100003e96 "LGPerson"
    }
  }
  baseMethods = {
    ptr = 0x0000000100008078
  }
  baseProtocols = nil
  ivars = 0x0000000100008158
  weakIvarLayout = 0x0000000000000000
  baseProperties = 0x00000001000081e0
  _swiftMetadataInitializer_NEVER_USE = {}
}

用同样的方法,打印baseMethods,发现它是一个方法列表指针method_list_t,但是它外层包装了一层WrappedPtr,通过.ptr取出当前列表指针,再通过*读取这片内存地址内容,出现了一个熟悉的东西entsize_list_tt,这跟我们之前分析属性成员列表的时候是一样的东西,所以就尝试着用同样的的方式,get看能不能获取我们想要的方法名,结果很遗憾,打印出来都是空的;


唯一的可能就是property_t跟method_t的结构不一样,才会导致lldb的调试结果不一样,这个时候只能从源码分析,查看其二者的区别了;

struct property_t {
    const char *name;
    const char *attributes;
};
method_t结构体截图

通过查看发现,property_t结构体内部就是简单的两个成员变量,而method_t内部确实好多的结构体跟方法;从官方注释可以知道,这么做是为了更好的保护我们的方法,因为方法的存储比较复杂,存储在方法区,不像成员变量,跟着我们的对象;
method_t根据架构的不同会有Small跟big类型;


image.png

通过big()方法读取,会惊奇的发现我们想要的结果,所以我们尝试打印我们所有的方法(因为此处测试写的不多,所以就这么干);


打印所有的方法

比较我们在类当中定义的方法,会发现类方法没有被打印出来,即不在方法列表里面(cxx_destruct这个是在OC下层编译的时候加的析构方法,编译过程中就会添加的);
类中定义的方法

sayHello跟sayNB之所以能出现在method_list中,是因为他们是对象方法,对象方法存在类当中的,通过isa指向类获取方法,我们此时分析的是类,所以此时能打印出来对象方法也是情理之中;而类方法其实是存在元类中的,这里也进一步解释了为什么要有元类;

对象方法 (对象 --> isa --> 类) - 类方法 (类 --> isa --> 元类)

为了验证这一点,lldb通过类的isa查找元类,看是否与我猜想的一致,isa指向元类首地址,通过指针平移的方式,找到元类的bits,再找到原来的ro;

(lldb) x/4gx LGPerson.class
0x1000081f8: 0x00000001000081d0 0x0000000108d68140
0x100008208: 0x0000000108d60cb0 0x801c000000000000
(lldb) po 0x00000001000081d0
LGPerson
(lldb) x/4gx 0x00000001000081d0
0x1000081d0: 0x0000000108d680f0 0x0000000108d680f0
0x1000081e0: 0x0000600001704100 0xe035000100000003
(lldb) p/x 0x1000081d0+32
(long) $5 = 0x00000001000081f0
(lldb) p (class_data_bits_t *)0x00000001000081f0
(class_data_bits_t *) $6 = 0x00000001000081f0
(lldb) p $6->data()->ro()
(const class_ro_t *) $7 = 0x0000000100008070
(lldb) p *$7
(const class_ro_t) $8 = {
  flags = 389
  instanceStart = 40
  instanceSize = 40
  reserved = 0
   = {
    ivarLayout = 0x0000000000000000
    nonMetaclass = nil
  }
  name = {
    std::__1::atomic<const char *> = "LGPerson" {
      Value = 0x0000000100003f08 "LGPerson"
    }
  }
  baseMethods = {
    ptr = 0x0000000100008038
  }
  baseProtocols = nil
  ivars = nil
  weakIvarLayout = 0x0000000000000000
  baseProperties = nil
  _swiftMetadataInitializer_NEVER_USE = {}
}
(lldb) p $8.baseMethods
(const WrappedPtr<method_list_t, method_list_t::Ptrauth>) $9 = {
  ptr = 0x0000000100008038
}
(lldb) p $9.ptr
(method_list_t *const) $10 = 0x0000000100008038
(lldb) p $10
(method_list_t *const) $10 = 0x0000000100008038
(lldb) p $10.get(0)
(method_t) $11 = {}
  Fix-it applied, fixed expression was:
    $10->get(0)
(lldb) p $10.get(0).big()
(method_t::big) $12 = {
  name = "class_sayHello"
  types = 0x0000000100003f7d "v16@0:8"
  imp = 0x0000000100003ca0 (KCObjcBuild`+[LGPerson class_sayHello])
}
  Fix-it applied, fixed expression was:
    $10->get(0).big()
(lldb) p $10.get(1).big()
(method_t::big) $13 = {
  name = "class_sayNB:"
  types = 0x0000000100003f85 "@20@0:8i16"
  imp = 0x0000000100003cd0 (KCObjcBuild`+[LGPerson class_sayNB:])
}
  Fix-it applied, fixed expression was:
    $10->get(1).big()
  Fix-it applied, fixed expression was:
    $10->get(2).big()
error: execution stopped with unexpected state.
error: Execution was interrupted, reason: breakpoint 2.1.
The process has been returned to the state before expression evaluation.
Program ended with exit code: 6

当然,如果对lldb调试不是很熟悉的话,也可以通过API调用进行分析,得到的结果也是一样的;

void lgObjc_copyMethodList(Class pClass){
    unsigned int count = 0;
    Method *methods = class_copyMethodList(pClass, &count);
    for (unsigned int i=0; i < count; i++) {
        Method const method = methods[i];
        //获取方法名
        NSString *key = NSStringFromSelector(method_getName(method));
        LGLog(@"Method, name: %@", key);
    }
    free(methods);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        // LGTeacher *teacher = [LGTeacher alloc];
        LGPerson *person = [LGPerson alloc];
        Class pClass     = object_getClass(person);
        lgObjc_copyMethodList(pClass);
        const char *className = class_getName(pClass);
        Class metaClass = objc_getMetaClass(className);
        NSLog(@"*************");
        lgObjc_copyMethodList(metaClass); 
        NSLog(@"Hello, World!");
    }
    return 0;
}

API打印结果
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容