iOS

iOS底层探索--类底层分析

2021-06-22  本文已影响0人  spyn_n

前期回顾

上个篇章 我们已经讲了对象的本质,isa的走位图,以及类的结构,下面进行简单的总结:

  1. 对象的本质
    • 对象的本质:不管是实例对象和类对象,对象的本质是结构体。objc对象的底层结构是objc_object结构体。类对象的底层结构是objc_class结构体。
  2. isa走位图
    • superclass走位
      • 实例对象没有继承关系,类对象才有继承关系
      • 类的superclass,指向父类,依次指到NSObject
      • NSObjectsuperclass指向nil
      • 元类的superclass指向上一级父的元类,依次指向根元类
      • 根元类的superclass指向NSObject
    • isa走位
      • 实例的isa指向类
      • 类的isa指向元类
      • 类的isa指向根元类
      • 根元类的isa指向根元类自己
  3. 类的结构
    • Class isa
    • Class superclass
    • cache_t cache
    • class_data_bits_t bits


      image.png

类的属性、成员变量和方法(探究源码是objc4-756.2

建议有条件的使用最新的源码探究,此处因为自己的Mac版本比较旧无法装最新系统,还因为自己的Mac配置了很多的环境变量

成员变量 & 属性

一、通过LLDB动态打印类的内部结构
找到class_rw_t地址
class_rw_t结构
  因为在class_rw_t中,methodspropertiesprotocols是运行时加载分类的时候或者开发者调用runtimeAPI动态添加方法,关联属性等的时候才需要用到,所以我们这里暂时先关注 ro。根据源码定义可以直接点出来p $3.ro 最新的需要 p $3.ro(),通过C++结构体内部的ro()函数获得 $6
class_ro_t结构

  通过p $6.ivars可获得ivar_list_t结构体的指针,然后再打印输出所有成员列表p *$10: 发现count有4个,第一个是_isWorking,大小size占1个字节,也可以通过p $11.get(0)打印第一个成员变量;第二个p $11.get(1),第三个p $11.get(2),第四个p $11.get(3).

ivars数组 所有ivars

class_ro_t中的baseProperties属性,也可通过$13.get(index)输出每个对应的属性

baseProperties数组
baseProperties

而我的代码的属性确是如LLDB打印的结果如下:

@interface PSYPerson : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, assign) BOOL isWorking;

+ (void)run;
- (void)run;

@end
二、通过clang查看类的内部结构

如下图,通过编译属性最后优化成了如下的成员变量,并且生成了getset方法


  仔细看四个属性的set方法,我们发现,为什么有些事通过调用objc_setProperty方法实现,有些通过内存平移实现呢?
set和get
  如果是我们自己设计底层,我们会怎么设计呢?开发者的属性名字随机性大,Xcode编译的时候自动转成setPropertygetProperty,最终是如何关联到objc_setProperty方法的调用呢。通过源码全局检索objc_setProperty,发现其只有两个文件包含,一个是objc-accessors.mm文件,也就是objc_setProperty的实现,一个是objc_abi.h头文件暴露给外面使用,整个源码没有调用者,我们可以大胆猜测是编译器LLVM干了这个工作
image.png
  继续深究会发现,是因为在LLVM编译器编译期就根据IsCopy条件判断是否走objc_setProperty还是走属性内存平移方式。objc_setProperty是作为中间层,在编译器将setXxxx的IMP重定向到objc_setProperty然后再指向底层代码。

我们再来看objc_setPropertyobjc_setProperty_atomicobjc_setProperty_nonatomicobjc_setProperty_atomic_copyobjc_setProperty_nonatomic_copy这几个方法的实现源码,最后都调用了reallySetProperty

// reallySetProperty的源码解析如下注释:

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    if (offset == 0) { // 如果偏移为0直接赋值
        object_setClass(self, newValue);
        return;
    }

  // 获取属性内存偏移地址
    id oldValue;
    id *slot = (id*) ((char*)self + offset);

    if (copy) {
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        if (*slot == newValue) return; // 如果新旧值一致则直接返回
        newValue = objc_retain(newValue); // 对新值retain 
    }

    if (!atomic) { // 非原子
        oldValue = *slot; // 保存旧值用于后面release,因为每一个旧值都曾是新值,曾被objc_retain
        *slot = newValue; // 对内存赋值
    } else { // 原子需要加锁操作,保证安全性
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }

    objc_release(oldValue); // 对旧值进行release
}

对象方法 & 类方法

一、通过LLDB动态打印

  同样的我也将所有的methods打印出来,发现所有的方法都是对象方法- setProperty-propertysetget 方法),加上一个C++的析构函数.cxx_destruct\color{#0xfff0000}{共9个},为毛没有我的自定义方法+ (void) run- (void) run,经查因为我没有写他们的实现(*_*)[尴尬]。

  添加自定义方法的实现,重新再来一次,可得到10methods,比原来多了一个对象方法- (void) run。没有+ (void) run类方法,既然对象的方法存在类中,类方法存在哪里呢?元类?

(lldb) p $5.baseMethodList
(method_list_t *const) $6 = 0x0000000100002338
(lldb) p *$6
(method_list_t) $7 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 10
    first = {
      name = "nickName"
      types = 0x0000000100001f6a "@16@0:8"
      imp = 0x0000000100001a50 (objc-debug`-[PSYPerson nickName] at PSYPerson.h:16)
    }
  }
}
(lldb) p $7.get(0)
(method_t) $8 = {
  name = "nickName"
  types = 0x0000000100001f6a "@16@0:8"
  imp = 0x0000000100001a50 (objc-debug`-[PSYPerson nickName] at PSYPerson.h:16)
}
(lldb) p $7.get(1)
(method_t) $9 = {
  name = "setNickName:"
  types = 0x0000000100001f72 "v24@0:8@16"
  imp = 0x0000000100001a80 (objc-debug`-[PSYPerson setNickName:] at PSYPerson.h:16)
}
(lldb) p $7.get(2)
(method_t) $10 = {
  name = "setIsWorking:"
  types = 0x0000000100001f98 "v20@0:8c16"
  imp = 0x0000000100001ae0 (objc-debug`-[PSYPerson setIsWorking:] at PSYPerson.h:17)
}
(lldb) p $7.get(3)
(method_t) $11 = {
  name = ".cxx_destruct"
  types = 0x0000000100001f62 "v16@0:8"
  imp = 0x0000000100001b00 (objc-debug`-[PSYPerson .cxx_destruct] at PSYPerson.m:10)
}
(lldb) p $7.get(4)
(method_t) $12 = {
  name = "name"
  types = 0x0000000100001f6a "@16@0:8"
  imp = 0x00000001000019a0 (objc-debug`-[PSYPerson name] at PSYPerson.h:14)
}
(lldb) p $7.get(5)
(method_t) $13 = {
  name = "setName:"
  types = 0x0000000100001f72 "v24@0:8@16"
  imp = 0x00000001000019d0 (objc-debug`-[PSYPerson setName:] at PSYPerson.h:14)
}
(lldb) p $7.get(6)
(method_t) $14 = {
  name = "run"
  types = 0x0000000100001f62 "v16@0:8"
  imp = 0x0000000100001990 (objc-debug`-[PSYPerson run] at PSYPerson.m:16)
}
(lldb) p $7.get(7)
(method_t) $15 = {
  name = "isWorking"
  types = 0x0000000100001f90 "c16@0:8"
  imp = 0x0000000100001ac0 (objc-debug`-[PSYPerson isWorking] at PSYPerson.h:17)
}
(lldb) p $7.get(8)
(method_t) $16 = {
  name = "age"
  types = 0x0000000100001f7d "q16@0:8"
  imp = 0x0000000100001a10 (objc-debug`-[PSYPerson age] at PSYPerson.h:15)
}
(lldb) p $7.get(9)
(method_t) $17 = {
  name = "setAge:"
  types = 0x0000000100001f85 "v24@0:8q16"
  imp = 0x0000000100001a30 (objc-debug`-[PSYPerson setAge:] at PSYPerson.h:15)
}

带着猜想紧接着通过x/4gx PSYPerson.class打印类的内存拿到 isa,之后可以通过两种方式拿到元类地址。
方式一:
  由于__x86_64__shiftcls类(元类)信息在中间占44位(第4位~第47位),所以将其他无关的低三位高17位清零即可得到。

方式二:

拿到元类的地址0x00000001000025e0之后,顺着打印实例方法的方式找到baseMethodList,并打印出方法。(注意0x00000001000025e0偏移0x20结果是0x0000000100002600,不是,不是,不是0x0000000100002610,可通过p/x (0x00000001000025e0+0x20)打印,不要自己加)如下图:

image.png

针对上面得到的方法的types types = 0x0000000100001f62 "v16@0:8"可参考下面官方给出的对应的类型编码表,需要的时候查:

TypeEncoding链接

二、通过RunTime的API直接打印

通过使用RunTime的APIclass_copyMethodList拿到方法列表,再通过NSStringFromSelector将方法转成字符串输出。为了区分两个实例方法和类方法,将实例方法改成-(void)runFast

image.png

+ (void) run
- (void) runFast

class_getInstanceMethod(Class, @selector(run)) --- 意思是在类/元类中查找实例方法run
class_getClassMethod(Class, @selector(run))--- 意思是在类/元类中查找类方法run
所以验证结果:

image.png

  为什么在类中和元类中找的ClassMethod结果和在元类中查找实例方法结果都一样,都是0x100002400?

  这时候看一下class_getClassMethod源码,发现在class_getClassMethod方法中调用了class_getInstanceMethod在元类中获取实例方法,因为类本身也是对象

下面再从类和元类中查找runrunFastIMP验证一下 ---》其结果就是在元类中找到run方法,在类中找到runFast方法。但是为什么在类中找不到run方法和在元类中找不到runFast方法,却也返回了libobjc.A.dylib中的_objc_msgForward方法?这就涉及到IMP方法的查找流程,将会在后面篇章讲解....

IMP
小结
  1. class_getInstanceMethod是获取的都是对象方法,这要看参数Class传入的是类还是元类。如果Class是类,则该方法是获取实例方法;如果Class是元类,则该方法是获取类方法。
  2. 底层是没有 +方法 和- 方法之分的,但是其存储的区域不一样,-方法存储在类信息的内存中,+方法存储在元类信息内存中,体现到上层就是类方法和对象方法。
Method class_getClassMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;
    // 也就是也调用了class_getInstanceMethod方法
    return class_getInstanceMethod(cls->getMeta(), sel);
}

Class getMeta() {
        if (isMetaClass()) return (Class)this; // 如果是元类,直接返回
        else return this->ISA(); // 如果不是元类返回类的ISA
    }

拓展

下面通过几个例子看一下下面几个函数原理:
- (BOOL)isKindOfClass:(Class)cls
+ (BOOL)isKindOfClass:(Class)cls
+ (BOOL)isMemberOfClass:(Class)cls
- (BOOL)isMemberOfClass:(Class)cls

例子:
BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]]; 
BOOL re3 = [(id)[PSYPerson class] isKindOfClass:[PSYPerson class]];

源码分析:
+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}
image.png image.png
例子:
BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
BOOL re4 = [(id)[PSYPerson class] isMemberOfClass:[PSYPerson class]];

源码分析:
+ (BOOL)isMemberOfClass:(Class)cls {
    return object_getClass((id)self) == cls;
}
Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}
例子:
    BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];   
    BOOL re8 = [(id)[PSYPerson alloc] isMemberOfClass:[PSYPerson class]]; 

源码分析:
- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}
例子:
    BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];       
    BOOL re7 = [(id)[PSYPerson alloc] isKindOfClass:[PSYPerson class]];     

源码分析:
- (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

isa走位图

结合源码,断点调试很容易就可以得到结果,但是其是遵循isa的走位图的,正好验证了isa走位图。再结合汇编查看确实也是走了isKindOfClass方法。但是在objc4-779.1以后,在汇编层就走的是objc_opt_isKindOfClass了。

其实源码是一样的是一样的,分析结果也是一样的,只是走的流程有些问题,所以还是要再运行时查看汇编调用函数,在去分析,省得走弯路。

源码:
BOOL objc_opt_isKindOfClass(id obj, Class otherClass)
{
#if __OBJC2__
    if (slowpath(!obj)) return NO;
    Class cls = obj->getIsa();
    if (fastpath(!cls->hasCustomCore())) {
        for (Class tcls = cls; tcls; tcls = tcls->getSuperclass()) {
            if (tcls == otherClass) return YES;
        }
        return NO;
    }
#endif
    return ((BOOL(*)(id, SEL, Class))objc_msgSend)(obj, @selector(isKindOfClass:), otherClass);
}
上一篇 下一篇

猜你喜欢

热点阅读