安全防护

cache分析

2021-07-22  本文已影响0人  浅墨入画
面试题iskindOfClass & isMemberOfClass的理解

下面是关于iskindOfClass & isMemberOfClass的代码,分析最终结果

//-----使用 iskindOfClass & isMemberOfClass 类方法
BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];       //
BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];     //
BOOL re3 = [(id)[LGPerson class] isKindOfClass:[LGPerson class]];       //
BOOL re4 = [(id)[LGPerson class] isMemberOfClass:[LGPerson class]];     //
NSLog(@" re1 :%hhd\n re2 :%hhd\n re3 :%hhd\n re4 :%hhd\n",re1,re2,re3,re4);

//------iskindOfClass & isMemberOfClass 实例方法
BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];       //
BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];     //
BOOL re7 = [(id)[LGPerson alloc] isKindOfClass:[LGPerson class]];       //
BOOL re8 = [(id)[LGPerson alloc] isMemberOfClass:[LGPerson class]];     //
NSLog(@" re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);

// 打印结果
re1 : 1
re2 : 0
re3 : 0
re4 : 0
re5 : 1
re6 : 1
re7 : 1
re8 : 1
源码解析
// + isKindOfClass:第一次比较是 获取类的元类 与 传入类对比,再次之后的对比是获取上次结果的父类 与 传入 类进行对比
+ (BOOL)isKindOfClass:(Class)cls {
    // 获取类的元类 vs 传入类
    // 根元类 vs 传入类
    // 根类 vs 传入类
    for (Class tcls = self->ISA(); tcls; tcls = tcls->getSuperclass()) {
        if (tcls == cls) return YES;
    }
    return NO;
}
// - isKindOfClass:第一次是获取对象类 与 传入类对比,如果不相等,后续对比是继续获取上次 类的父类 与传入类进行对比
- (BOOL)isKindOfClass:(Class)cls {
    // 获取对象的类 vs 传入的类 
    // 父类 vs 传入的类
    // 根类 vs 传入的类
    // nil vs 传入的类
    for (Class tcls = [self class]; tcls; tcls = tcls->getSuperclass()) {
        if (tcls == cls) return YES;
    }
    return NO;
}
//-----类方法
//+ isMemberOfClass : 获取类的元类,与 传入类对比
+ (BOOL)isMemberOfClass:(Class)cls {
    return self->ISA() == cls;
}
//-----实例方法
//- isMemberOfClass : 获取对象的类,与 传入类对比
- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}
源码分析总结
  • 类方法:元类(isa) --> 根元类(父类) --> 根类(父类) --> nil(父类) 与 传入类的对比
  • 实例方法:对象的类 --> 父类 --> 根类 --> nil与 传入类的对比

类方法: 类的元类 与 传入类 对比
实例方法:对象的类 与 传入类 对比

然后通过断点调试查看汇编,isMemberOfClass的类方法和实例方法的流程是正常的,会走到上面分析的源码,而isKindOfClass根本不会走到上面分析的源码中(!!!注意这里,这是一个坑点),其类方法和实例方法都是走到objc_opt_isKindOfClass方法源码中

objc_opt_isKindOfClass方法源码如下
// Calls [obj isKindOfClass]
BOOL
objc_opt_isKindOfClass(id obj, Class otherClass)
{
#if __OBJC2__
    if (slowpath(!obj)) return NO;
    //获取isa,
    //如果obj 是对象,则isa是类,
    //如果obj是类,则isa是元类
    Class cls = obj->getIsa(); 
    if (fastpath(!cls->hasCustomCore())) {
        // 如果obj 是对象,则在类的继承链进行对比,
        // 如果obj是类,则在元类的isa中进行对比
        for (Class tcls = cls; tcls; tcls = tcls->superclass) { 
            if (tcls == otherClass) return YES;
        }
        return NO;
    }
#endif
    return ((BOOL(*)(id, SEL, Class))objc_msgSend)(obj, @selector(isKindOfClass:), otherClass);
}

主要是因为在llvm中编译时对其进行了优化处理
调用objc_opt_isKindOfClass实际走的逻辑如下图

image.png

cache数据结构

cache_t是什么?cache中存储的又是什么?

打开objc4-818.2源码,创建LGPerson类,添加如下代码。NSLog(@"%@",pClass);添加断点,运行工程进行lldb调试

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

// lldb调试内容
(lldb) p/x pClass
(Class) $0 = 0x0000000100008400 LGPerson
(lldb) p (cache_t *)0x0000000100008410
(cache_t *) $1 = 0x0000000100008410
(lldb) p *$1
(cache_t) $2 = {
  _bucketsAndMaybeMask = {
    std::__1::atomic<unsigned long> = {
      Value = 4298515296
    }
  }
   = {
     = {
      _maybeMask = {
        std::__1::atomic<unsigned int> = {
          Value = 0
        }
      }
      _flags = 32808
      _occupied = 0
    }
    _originalPreoptCache = {
      std::__1::atomic<preopt_cache_t *> = {
        Value = 0x0000802800000000
      }
    }
  }
}
struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask; // 8
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask; // 4
#if __LP64__
            uint16_t                   _flags;  // 2
#endif
            uint16_t                   _occupied; // 2
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache; // 8
    };

#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
    // _bucketsAndMaybeMask is a buckets_t pointer
    // _maybeMask is the buckets mask

    static constexpr uintptr_t bucketsMask = ~0ul;
    static_assert(!CONFIG_USE_PREOPT_CACHES, "preoptimized caches not supported");
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
    static constexpr uintptr_t maskShift = 48;
    static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
    static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << maskShift) - 1;
    
    static_assert(bucketsMask >= MACH_VM_MAX_ADDRESS, "Bucket field doesn't have enough bits for arbitrary pointers.");
#if CONFIG_USE_PREOPT_CACHES
    static constexpr uintptr_t preoptBucketsMarker = 1ul;
    static constexpr uintptr_t preoptBucketsMask = bucketsMask & ~preoptBucketsMarker;
#endif
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    // _bucketsAndMaybeMask is a buckets_t pointer in the low 48 bits
    // _maybeMask is unused, the mask is stored in the top 16 bits.
......
struct bucket_t {
private:
    // IMP-first is better for arm64e ptrauth and no worse for arm64.
    // SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
    explicit_atomic<uintptr_t> _imp;
    explicit_atomic<SEL> _sel;
#else
    explicit_atomic<SEL> _sel;
    explicit_atomic<uintptr_t> _imp;
#endif
......
得出结论

cache_t中缓存的是sel-imp,在类的方法调用过程中,已知过程是通过SEL(方法编号)在内存中查找IMP(方法指针),为了使方法响应更加快速效率更高,不需要每一次都去内存中把方法遍历一遍,cache_t结构体出现了。cache_t将调用过的方法的SEL和IMP以及receiver以bucket_t结构体方式存储在当前类结构中,以便后续方法的查找。

cache_t结构流程
cache_t结构流程

cache底层lldb分析

通过lldb验证cache_t方法的存储内容

继续上面的lldb调试

(lldb) p $2._bucketsAndMaybeMask
(explicit_atomic<unsigned long>) $3 = {
  std::__1::atomic<unsigned long> = {
    Value = 4298515296
  }
}
(lldb) p $2._maybeMask
(explicit_atomic<unsigned int>) $4 = {
  std::__1::atomic<unsigned int> = {
    Value = 0
  }
}
(lldb) p $2._originalPreoptCache
(explicit_atomic<preopt_cache_t *>) $5 = {
  std::__1::atomic<preopt_cache_t *> = {
    Value = 0x0000802800000000
  }
}
// 其中$3 $4 $5分别打印Value,都会报错,取不到值
(lldb) p $3.Value
(lldb) p $3->Value

$3 $4 $5分别打印Value,都会报错取不到值,这个时候我们就该考虑从源码中找获取值的方法,我们找到了buckets(),那么就继续进行lldb调试

(lldb) p $2.buckets()
(bucket_t *) $6 = 0x0000000100362360
(lldb) p *$6 
(bucket_t) $7 = {
  _sel = {
    std::__1::atomic<objc_selector *> = (null) {
      Value = (null)
    }
  }
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 0
    }
  }
}

上面调试$7_sel对应的Value值为null,这是为什么呢?因为这个时候还没有调用方法,没有内容可缓存。下面调用方法后继续调试

(lldb) p [p saySomething]
2021-07-21 23:05:12.808682+0800 KCObjcBuild[46873:5018317] -[LGPerson saySomething]
(lldb) p/x pClass
(Class) $8 = 0x0000000100008400 LGPerson
(lldb) p (cache_t *)0x0000000100008410
(cache_t *) $9 = 0x0000000100008410
(lldb) p *$9
(cache_t) $10 = {
  _bucketsAndMaybeMask = {
    std::__1::atomic<unsigned long> = {
      Value = 4315035808
    }
  }
   = {
     = {
      _maybeMask = {
        std::__1::atomic<unsigned int> = {
          Value = 7
        }
      }
      _flags = 32808
      _occupied = 1
    }
    _originalPreoptCache = {
      std::__1::atomic<preopt_cache_t *> = {
        Value = 0x0001802800000007
      }
    }
  }
}
(lldb) p $10.buckets()
(bucket_t *) $11 = 0x00000001013238a0
(lldb) p *$11 
(bucket_t) $12 = {
  _sel = {
    std::__1::atomic<objc_selector *> = (null) {
      Value = (null)
    }
  }
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 0
    }
  }
}

上面调用了saySomething方法,再打印$12_sel对应的值还是null,这该如何解决呢?

(lldb) p $10.buckets()[1] 
(bucket_t) $13 = {
  _sel = {
    std::__1::atomic<objc_selector *> = "" {
      Value = ""
    }
  }
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 47232
    }
  }
}

上面打印buckets()第一个元素,发现为null,我们尝试打印buckets()的第二个元素发现_imp对应的Value = 47232,为什么呢?接下来我们就去bucket_t中找看有没有相应的方法,结果找到了sel()

<!-- objc4-818.2源码 -->
struct bucket_t {
private:
    // IMP-first is better for arm64e ptrauth and no worse for arm64.
    // SEL-first is better for armv7* and i386 and x86_64.
......
inline SEL sel() const { return _sel.load(memory_order_relaxed); }

<!-- lldb调试信息 -->
(lldb) p $13.sel()
(SEL) $14 = "saySomething"
(lldb) p $13.imp(nil,pClass)
(IMP) $15 = 0x0000000100003c80 (KCObjcBuild`-[LGPerson saySomething])

总结 上面调试成功验证cache_t中缓存的是sel-imp

脱离源码分析

脱离源码环境,就是将所需的源码部分拷贝至项目中,其完整代码如下

typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits

struct kc_bucket_t {
    SEL _sel;
    IMP _imp;
};
struct kc_cache_t {
    struct kc_bucket_t *_bukets; // 8
    mask_t    _maybeMask; // 4
    uint16_t  _flags;  // 2
    uint16_t  _occupied; // 2
};

struct kc_class_data_bits_t {
    uintptr_t bits;
};

// cache class
struct kc_objc_class {
    Class isa;
    Class superclass;
    struct kc_cache_t cache;             // formerly cache pointer and vtable
    struct kc_class_data_bits_t bits;
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *p  = [LGPerson alloc];
        Class pClass = p.class;  // objc_clas
        [p say1];

        struct kc_objc_class *kc_class = (__bridge struct kc_objc_class *)(pClass);
        NSLog(@"%hu - %u",kc_class->cache._occupied,kc_class->cache._maybeMask);
    }
    return 0;
}

源码中objc_classisa属性是继承自objc_object的,但在我们将其拷贝过来时去掉了objc_class的继承关系,打印的结果会有问题如下图所示

image.png

加上isa属性后,其正确的打印结果应该是这样的

image.png

再次添加代码如下,打印buckets中的impsel

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *p  = [LGPerson alloc];
        Class pClass = p.class;  // objc_clas
        [p say1];

        struct kc_objc_class *kc_class = (__bridge struct kc_objc_class *)(pClass);
        NSLog(@"%hu - %u",kc_class->cache._occupied,kc_class->cache._maybeMask);

        for (mask_t i = 0; i<kc_class->cache._maybeMask; i++) {
            struct kc_bucket_t bucket = kc_class->cache._bukets[i];
            NSLog(@"%@ - %pf",NSStringFromSelector(bucket._sel),bucket._imp);
        }
    }
    return 0;
}

// 打印日志如下
2021-07-22 19:50:51.582854+0800 003-cache_t脱离源码环境分析[50079:5415756] LGPerson say : -[LGPerson say1]
2021-07-22 19:50:51.583832+0800 003-cache_t脱离源码环境分析[50079:5415756] 1 - 3
2021-07-22 19:50:51.584157+0800 003-cache_t脱离源码环境分析[50079:5415756] say1 - 0xb820f
2021-07-22 19:50:51.584229+0800 003-cache_t脱离源码环境分析[50079:5415756] (null) - 0x0f
2021-07-22 19:50:51.584281+0800 003-cache_t脱离源码环境分析[50079:5415756] (null) - 0x0f
Program ended with exit code: 0
针对上面的打印结果,有以下几点疑问

cache底层原理分析

image.png

incrementOccupied()函数的具体实现如下

void cache_t::incrementOccupied() 
{
    _occupied++;
}
image.png
疑问解答
上一篇下一篇

猜你喜欢

热点阅读