OC类的原理探究(二)——方法的缓存

2021-03-10 14:16:33 浏览数 (1)

objc_alloc的分析

运行时,alloc方法流程分析

代码语言:javascript复制
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        LGPerson *p  = [LGPerson alloc];
        Class pClass = [LGPerson class];
        NSLog(@"Hello, World! %@ -- %p",p,pClass);
    }
    return 0;
}

接下来我们摁住conmand,单击alloc:

现在跳到了NSObject的类方法alloc中:【①】

代码语言:javascript复制
  (id)alloc {
    return _objc_rootAlloc(self);
}

然后跳到_objc_rootAlloc中:【②】

代码语言:javascript复制
// Base class implementation of  alloc. cls is not nil.
// Calls [cls allocWithZone:nil].
id
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

再然后跳到callAlloc中:【③】

代码语言:javascript复制
// Call [cls alloc] or [cls allocWithZone:nil], with appropriate 
// shortcutting optimizations.
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    if (slowpath(checkNil && !cls)) return nil;

#if __OBJC2__
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        // No alloc/allocWithZone implementation. Go straight to the allocator.
        // fixme store hasCustomAWZ in the non-meta class and 
        // add it to canAllocFast's summary
        if (fastpath(cls->canAllocFast())) {
            // No ctors, raw isa, etc. Go straight to the metal.
            bool dtor = cls->hasCxxDtor();
            id obj = (id)calloc(1, cls->bits.fastInstanceSize());
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            obj->initInstanceIsa(cls, dtor);
            return obj;
        }
        else {
            // Has ctor or raw isa or something. Use the slower path.
            id obj = class_createInstance(cls, 0);
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            return obj;
        }
    }
#endif

    // No shortcuts available.
    if (allocWithZone) return [cls allocWithZone:nil];
    return [cls alloc];
}

上面的代码中,如果我们覆写了该类的allocWithZone方法,那么就会走到第31行的逻辑;不过一般而言我们是不会自己去覆写allocWithZone方法的,所以一般都会走第8~28行的逻辑。

接下来我们看上面的第13行,跳到canAllocFast中:【④】

代码语言:javascript复制
    bool canAllocFast() {
        assert(!isFuture());
        return bits.canAllocFast();
    }

然后再跳到bits的canAllocFast中:【⑤】

代码语言:javascript复制
#if FAST_ALLOC
    size_t fastInstanceSize() 
    {
        assert(bits & FAST_ALLOC);
        return (bits >> FAST_SHIFTED_SIZE_SHIFT) * 16;
    }
    void setFastInstanceSize(size_t newSize) 
{
        // Set during realization or construction only. No locking needed.
        assert(data()->flags & RW_REALIZING);

        // Round up to 16-byte boundary, then divide to get 16-byte units
        newSize = ((newSize   15) & ~15) / 16;
        
        uintptr_t newBits = newSize << FAST_SHIFTED_SIZE_SHIFT;
        if ((newBits >> FAST_SHIFTED_SIZE_SHIFT) == newSize) {
            int shift = WORD_BITS - FAST_SHIFTED_SIZE_SHIFT;
            uintptr_t oldBits = (bits << shift) >> shift;
            if ((oldBits & FAST_ALLOC_MASK) == FAST_ALLOC_VALUE) {
                newBits |= FAST_ALLOC;
            }
            bits = oldBits | newBits;
        }
    }

    bool canAllocFast() {
        return bits & FAST_ALLOC;
    }
#else
    size_t fastInstanceSize() {
        abort();
    }
    void setFastInstanceSize(size_t) {
        // nothing
    }
    bool canAllocFast() {
        return false;
    }
#endif

我们发现,上面有一个FAST_ALLOC条件,那么具体是走上面的canAllocFast呢还是下面的canAllocFast呢?这完全取决于FAST_ALLOC条件的取值,所以我们需要看一下FAST_ALLOC的取值:【⑥】

代码语言:javascript复制
#if !__LP64__

// class or superclass has .cxx_construct implementation
#define RW_HAS_CXX_CTOR       (1<<18)
// class or superclass has .cxx_destruct implementation
#define RW_HAS_CXX_DTOR       (1<<17)
// class or superclass has default alloc/allocWithZone: implementation
// Note this is is stored in the metaclass.
#define RW_HAS_DEFAULT_AWZ    (1<<16)
// class's instances requires raw isa
#if SUPPORT_NONPOINTER_ISA
#define RW_REQUIRES_RAW_ISA   (1<<15)
#endif
// class or superclass has default retain/release/autorelease/retainCount/
//   _tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference
#define RW_HAS_DEFAULT_RR     (1<<14)

// class is a Swift class from the pre-stable Swift ABI
#define FAST_IS_SWIFT_LEGACY  (1UL<<0)
// class is a Swift class from the stable Swift ABI
#define FAST_IS_SWIFT_STABLE  (1UL<<1)
// data pointer
#define FAST_DATA_MASK        0xfffffffcUL

#elif 1
// Leaks-compatible version that steals low bits only.

// class or superclass has .cxx_construct implementation
#define RW_HAS_CXX_CTOR       (1<<18)
// class or superclass has .cxx_destruct implementation
#define RW_HAS_CXX_DTOR       (1<<17)
// class or superclass has default alloc/allocWithZone: implementation
// Note this is is stored in the metaclass.
#define RW_HAS_DEFAULT_AWZ    (1<<16)
// class's instances requires raw isa
#define RW_REQUIRES_RAW_ISA   (1<<15)

// class is a Swift class from the pre-stable Swift ABI
#define FAST_IS_SWIFT_LEGACY    (1UL<<0)
// class is a Swift class from the stable Swift ABI
#define FAST_IS_SWIFT_STABLE    (1UL<<1)
// class or superclass has default retain/release/autorelease/retainCount/
//   _tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference
#define FAST_HAS_DEFAULT_RR     (1UL<<2)
// data pointer
#define FAST_DATA_MASK          0x00007ffffffffff8UL

#else
// Leaks-incompatible version that steals lots of bits.

// class is a Swift class from the pre-stable Swift ABI
#define FAST_IS_SWIFT_LEGACY    (1UL<<0)
// class is a Swift class from the stable Swift ABI
#define FAST_IS_SWIFT_STABLE    (1UL<<1)
// summary bit for fast alloc path: !hasCxxCtor and 
//   !instancesRequireRawIsa and instanceSize fits into shiftedSize
#define FAST_ALLOC              (1UL<<2)
// data pointer
#define FAST_DATA_MASK          0x00007ffffffffff8UL
// class or superclass has .cxx_construct implementation
#define FAST_HAS_CXX_CTOR       (1UL<<47)
// class or superclass has default alloc/allocWithZone: implementation
// Note this is is stored in the metaclass.
#define FAST_HAS_DEFAULT_AWZ    (1UL<<48)
// class or superclass has default retain/release/autorelease/retainCount/
//   _tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference
#define FAST_HAS_DEFAULT_RR     (1UL<<49)
// class's instances requires raw isa
//   This bit is aligned with isa_t->hasCxxDtor to save an instruction.
#define FAST_REQUIRES_RAW_ISA   (1UL<<50)
// class or superclass has .cxx_destruct implementation
#define FAST_HAS_CXX_DTOR       (1UL<<51)
// instance size in units of 16 bytes
//   or 0 if the instance size is too big in this field
//   This field must be LAST
#define FAST_SHIFTED_SIZE_SHIFT 52

// FAST_ALLOC means
//   FAST_HAS_CXX_CTOR is set
//   FAST_REQUIRES_RAW_ISA is not set
//   FAST_SHIFTED_SIZE is not zero
// FAST_ALLOC does NOT check FAST_HAS_DEFAULT_AWZ because that 
// bit is stored on the metaclass.
#define FAST_ALLOC_MASK  (FAST_HAS_CXX_CTOR | FAST_REQUIRES_RAW_ISA)
#define FAST_ALLOC_VALUE (0)

#endif

这里的第25行和第48行都是else语句,而FAST_ALLOC的定义是在第57行,也就是最后一个else中,但我们仔细看第25行,是#elif 1,这个条件是恒真的,因此永远不会走到最后一个else,也就是说,FAST_ALLOC永远不会被赋值。

因此,上面【⑤】中的#if FAST_ALLOC永远不会走,只会走下面的#else,也就是说,永远canAllocFast返回false。

因此,【③】中第13行if (fastpath(cls->canAllocFast()))条件永远不会走,只会走else中的第23~25行:

编译期,alloc方法流程分析

我们现在换个角度来研究alloc。

我们在下面位置打个断点:

然后查看汇编源码:

如下:

我们发现,当我们调用alloc方法的时候,会调起一个名为objc_alloc的符号,而其他的一些方法基本都是走得正常的objc_msgSend消息转发流程。此时我不禁就有疑问了,为什么这里的objc_alloc是一种符号形式(symbol stub for: objc_alloc)呢,为什么没有走消息转发(objc_msgSend)呢?

实际上,objc_alloc是系统在编译期调用的一种符号

那么编译期我怎么探索呢?答案是使用LLVM。

在llvm测试工程中,我们搜索【test_alloc_class】,就可以看到下面这段注释:

代码语言:javascript复制
// Make sure we get a bitcast on the return type as the
// call will return i8* which we have to cast to A*
// CHECK-LABEL: define {{.*}}void @test_alloc_class_ptr
A* test_alloc_class_ptr() {
  // CALLS: {{call.*@objc_alloc}}
  // CALLS-NEXT: bitcast i8*
  // CALLS-NEXT: ret
  return [B alloc];
}

通过这段注释我们知道了:

系统在真正调用alloc方法之前会首先调用objc_alloc。也就是说,在编译期会绑定objc_alloc符号,然后在运行时会走本文一开始讲的那一套【运行时alloc流程】

接下来我们就来探索objc_alloc。

经过全局搜索,我们发现在下面的emitObjCValueOperation方法中会调用objc_alloc:

代码语言:javascript复制
/// Allocate the given objc object.
///   call i8* @objc_alloc(i8* %value)
llvm::Value *CodeGenFunction::EmitObjCAlloc(llvm::Value *value,
                                            llvm::Type *resultType) {
  return emitObjCValueOperation(*this, value, resultType,
                                CGM.getObjCEntrypoints().objc_alloc,
                                "objc_alloc");
}

接下来我们看emitObjCValueOperation的实现:

现在我们知道了,在emitObjCValueOperation方法中会调用objc_alloc,那么哪里会调用emitObjCValueOperation方法呢?

代码语言:javascript复制
/// Allocate the given objc object.
///   call i8* @objc_alloc(i8* %value)
llvm::Value *CodeGenFunction::EmitObjCAlloc(llvm::Value *value,
                                            llvm::Type *resultType) {
  return emitObjCValueOperation(*this, value, resultType,
                                CGM.getObjCEntrypoints().objc_alloc,
                                "objc_alloc");
}

我们发现,在EmitObjCAlloc中会调用emitObjCValueOperation

那么,在哪里会调用EmitObjCAlloc呢?

我们发现,在tryGenerateSpecializedMessageSend中,只要方法名是alloc,那么就会调EmitObjCAlloc,进而调emitObjCValueOperation,进而通过EmitCallOrInvoke来调用objc_alloc

现在我们来想一下,为什么系统会调用tryGenerateSpecializedMessageSend方法呢?苹果在文档中有这样一段解释:

简而言之就是说,就是:

(1)tryGenerateSpecializedMessageSend方法能够比消息转发更快地生成实例对象

(2)如果运行时确实支持所需的入口点(alloc、autoRelease、retain、release),那么此方法将生成一个调用并返回结果值;否则它将返回空值None,此时调用者在外层会直接走一般的msgSend流程。

然后我们再接着看,看一下tryGenerateSpecializedMessageSend方法是在哪里被调用的呢?如下:

现在我们知道了,所有的消息都有两种处理方式,一种是特殊的消息发送,一种是一般消息发送。alloc、release、retain、autorelease走的就是特殊的消息发送

将代码转成汇编之后,带有objc_messageSend的就是一般的消息发送,带有symble stub for的就是特殊的消息发送。

需要注意的是,objc_alloc只会被调用一次,原因我暂时没有理清楚,待后期理清楚之后会补上⚠️⚠️⚠️

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打印是一样的。

cache_t探究

我们对类的结构已经很清楚了,如下:

代码语言:javascript复制

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;
    class_data_bits_t bits;

    ......

}

接下来就来探究一下cache_t到底是什么,首先看看其结构:

代码语言:javascript复制
struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
    ......
};

我们看到,cache_t中第一个变量是_buckets,它是bucket_t类型,为了弄明白_buckets到底承载的是什么,我们就需要看一下bucket_t的结构:

代码语言:javascript复制
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__
    MethodCacheIMP _imp;
    cache_key_t _key;
#else
    cache_key_t _key;
    MethodCacheIMP _imp;
#endif

public:
    inline cache_key_t key() const { return _key; }
    inline IMP imp() const { return (IMP)_imp; }
    inline void setKey(cache_key_t newKey) { _key = newKey; }
    inline void setImp(IMP newImp) { _imp = newImp; }

    void set(cache_key_t newKey, IMP newImp);
};

我们看到,bucket_t中有两个私有变量:_imp和_key。现在我们可以猜到,bucket_t中缓存的应该是方法的实现

我们可以看到,当方法被调用一次之后,就被被缓存到objc_class的cache_t的_buckets中。这样的话,我后面再调用相同的方法的时候,就不需要走漫长的消息发送机制,而是在缓存中直接获取到其实现

这时你可能还有一个疑问,我alloc方法、class方法都调用了呀,为啥没缓存起来?为啥只缓存了sayHello方法?原因就是:

alloc、class都是类方法,其缓存到了元类的cache_t中

缓存的流程

现在我们已经知道了catch_t的结构:

代码语言:javascript复制
struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
    ......
};

通过bucket_t的结构,我们也已经知道了cache_t中缓存的是方法

但是,目前我们也仅仅是了解到这些表象,接下来我们就来细究一下,方法整个的缓存流程到底是怎样的。

首先,我们看一下cache_t的完整定义:

然后分别查看mask()以及capacity()函数的实现:

我们发现,mask()中就是返回了变量_mask的值;而capacity()函数的实现就有点意思了:当mask()有值的时候就让它 1,没有值的时候就赋值为0。

接着,我们就来看一下是谁调用了capacity()这个函数:

原来是在expand()中调用了capacity()这个函数。简而言之,expand()的作用就是对老的空间进行扩容,然后重新分配新的空间。

接下来我们就会想,系统在什么时候执行expand()进行扩容呢?

代码语言:javascript复制
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
    cacheUpdateLock.assertLocked();

    // Never cache before  initialize is done
    if (!cls->isInitialized()) return;

    // 首先查看缓存中是否已经缓存过该方法.
    // 如果已经缓存过则直接返回;否则进行缓存
    if (cache_getImp(cls, sel)) return;

    // 获取到原来的缓存
    cache_t *cache = getCache(cls);
    // 将当前的sel转换成cache_key_t类型,作为方法的唯一标识
    cache_key_t key = getKey(sel);

    // Use the cache as-is if it is less than 3/4 full
    mask_t newOccupied = cache->occupied()   1; // 预计新的占用
    mask_t capacity = cache->capacity(); // 当前容量
    if (cache->isConstantEmptyCache()) {
        // 如果之前没有创建过缓存空间,那么就新建缓存空间
        cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
    }
    else if (newOccupied <= capacity / 4 * 3) {
        // 如果预计新的占用没超过当前容量的3/4,那么就直接使用当前的缓存空间
    }
    else {
        // 如果预计新的占用超过当前容量的3/4,那么就扩容
        cache->expand();
    }

    // Scan for the first unused slot and insert there.
    // There is guaranteed to be an empty slot because the 
    // minimum size is 4 and we resized at 3/4 full.
    bucket_t *bucket = cache->find(key, receiver); // 找到当前方法对应的bucket
    if (bucket->key() == 0) cache->incrementOccupied(); // 占用 1
    bucket->set(key, imp); // 将当前方法存储到缓存bucket中
}

我们发现,是在cache_fill_nolock中调用了expand进行扩容。我们来看下一下cache_fill_nolock中是怎么做的:

  1. 首先,通过cache_getImp查找是否已经缓存过该方法,如果已经缓存过了,则不采取任何操作,直接结束该方法的调用;如果没有缓存过,那么就进行下面的步骤进行缓存。
  2. 获取到之前的缓存,进而获取到缓存空间的大小capacity 和 已经占用的容量newOccupied。(1)如果之前的缓存是空的,也就是说没有创建过缓存空间,那么就使用reallocate创建新的缓存空间;(2)如果之前已经有缓存空间了,但是预测新增缓存后所占用容量没有超过原空间大小的3/4,那么就使用该缓存空间(3)如果预测新增缓存后占用容量即将超过原空间大小的3/4,那么就使用expand()将cache扩容到原来的两倍。需要注意的是,扩容之前会将原来的缓存给清空掉,也就是说,扩容之后,原来缓存的方法都没有了,需要重新缓存。但是我清空的是扩容之前原来缓存的方法,我当前的方法还是会在扩容后作为最新的缓存空间中的第一个缓存方法被缓存下来的
  3. 将当前的sel转成cache_key_t类型的key,作为方法的唯一标识,然后通过find方法获取当前方法对应的bucket_t类型的bucket,然后将occupied占用加1,最后将key和imp绑定,存储到bucket中

以上。

今天大年初一,祝各位在新的一年健健康康、心想事成。

0 人点赞