换一个角度学习objc-NSObject.mm

662 阅读6分钟

本文是objc源码阅读的第一篇,我们来尝试换一个角度阅读NSObject的源码来学习一些知识。

typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;

首先我们看到这么一个RefcountMap的定义,这里边有两个东西是比较有趣的。

  • DisguisedPtr
    对传入的指针进行伪装。
    static uintptr_t disguise(T* ptr) {
        return -(uintptr_t)ptr;
    }
    可以看出,伪装的方式就是讲指针的内存地址(16进制)强制转化为十进制的无符号整形。最后对它取负。为什么取负是因为无符号和有符号数在计算机里使用补码进行存储的,对无符号数进行取负操作,会变成一个和原值相差较大的数,从而达到伪装的目的。
    
  • DenseMap
    DenseMap是llvm里常用的数据结构,是一种基于Quadratic probing的hash表。
    查看llvm-DenseMap.h可以了解到DenseMap的负载因子和hash算法。
    
    BucketT *InsertIntoBucketImpl(const KeyT &Key, BucketT *TheBucket) {
    // If the load of the hash table is more than 3/4, grow the table. 
    // If fewer than 1/8 of the buckets are empty (meaning that many are 
    // filled with tombstones), rehash the table without growing.
    //
    // The later case is tricky.  For example, if we had one empty bucket with
    // tons of tombstones, failing lookups (e.g. for insertion) would have to
    // probe almost the entire table until it found the empty bucket.  If the
    // table completely filled with tombstones, no lookup would ever succeed,
    // causing infinite loops in lookup.
    unsigned NewNumEntries = getNumEntries() + 1;
    unsigned NumBuckets = getNumBuckets();
    if (NewNumEntries*4 >= NumBuckets*3) {
      this->grow(NumBuckets * 2);
      LookupBucketFor(Key, TheBucket);
      NumBuckets = getNumBuckets();
    }
    if (NumBuckets-(NewNumEntries+getNumTombstones()) <= NumBuckets/8) {
      this->grow(NumBuckets);
      LookupBucketFor(Key, TheBucket);
    }
    assert(TheBucket);
    
    // Only update the state after we have grown our bucket space appropriately
    // so that when growing buckets we have self-consistent entry count.
    // If we are writing over a tombstone or zero value, remember this.
    if (KeyInfoT::isEqual(TheBucket->first, getEmptyKey())) {
      // Replacing an empty bucket.
      incrementNumEntries();      
    }
    else if (KeyInfoT::isEqual(TheBucket->first, getTombstoneKey())) {
      // Replacing a tombstone.
      incrementNumEntries();
      decrementNumTombstones();
    }
    else if (ZeroValuesArePurgeable  &&  TheBucket->second == 0) {
      // Purging a zero. No accounting changes.
      TheBucket->second.~ValueT();
    } else {
      // Updating an existing entry. No accounting changes.
    }
    
        return TheBucket;
    }
    可以看到,如果负载因子大于3/4,则增加buckets,数量为原桶数量*2并保持桶的数量为2次幂。
    
    具体的hash算法可以在DenseMapInfo里边查看。
    // Pointer hash function.
    // This is not a terrific hash, but it is fast 
    // and not outrageously flawed for our purposes.
    
    // Based on principles from http://locklessinc.com/articles/fast_hash/
    // and evaluation ideas from http://floodyberry.com/noncryptohashzoo/
    #if __LP64__
    static inline uint32_t ptr_hash(uint64_t key)
    {
        key ^= key >> 4;
        key *= 0x8a970be7488fda55;
        key ^= __builtin_bswap64(key);
        return (uint32_t)key;
    }
    #else
    static inline uint32_t ptr_hash(uint32_t key)
    {
        key ^= key >> 4;
        key *= 0x5052acdb;
        key ^= __builtin_bswap32(key);
        return key;
    }
    #endif
    #The hash state is repeatedly multiplied by a large odd number. It then is "folded" with a half-shift + xor operation. These operations are chosen because they are invertible. This means that the available phase-space is conserved, and thus all possible hash values can be obtained.
    正如注释上所说的,对于指针的hash算法,是一种fast hash,想更多了解,可以查看[fast_hash](
    http://locklessinc.com/articles/fast_hash/)
    
    #字符串的hash算法
    static __inline uint32_t _objc_strhash(const char *s) {
        uint32_t hash = 0;
        for (;;) {
            int a = *s++;
            if (0 == a) break;
        hash += (hash << 8) + a;
        }
        return hash;
    }
    这里的字符串hash算法选择也是基于上述的原则。因为c语言中char占1个字节,也就是8位,所以上面的hash << 8选择左移8位就是获取所有可能的hash值。尽可能的减少冲突。
    #数值型hash算法
    static unsigned getHashValue(const long& Val) {
        return (unsigned)(Val * 37UL);
    }
    这里选择37作为乘子确实困扰了我很久,查找了很多资料发现,有两个原因可以归纳一。
    1、选择质数作为乘子是因为如果选择偶数,则最后一位永远是0,这将减少hash结果的多样性。
    2、[科普:为什么 String hashCode 方法选择数字31作为乘子](https://segmentfault.com/a/1190000010799123)从这篇文章中我们可以了解到如何选择一个合理的乘子。这里选择31作为乘子的原因也很明确,是可以被vm优化,虽然37、41的表现和它差不多。
    那么为什么这边使用的是37,根据Effective Java's recipe这本书里提到的,好像大家都是使用37作为hash乘子,然后在第二版里边才修改为31
    He explains that choice: "A nice property of 31 is that the multiplication can be replaced by a shift and a subtraction for better performance: 31 * i == (i << 5) - i. Modern VMs do this sort of optimization automatically."
    

之后就是SideTable结构体。关于SideTable的结构体解析,已经有很多详细的资料进行解释,这里就不再赘述了。 存放SideTable的是一个叫做SideTables的变量。

static StripedMap<SideTable>& SideTables() {
    return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}

我们来查看一下StripedMap这个类的代码。

#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
    enum { StripeCount = 8 };
#else
    enum { StripeCount = 64 };
#endif
可以发现SideTable的数量是在这边定义的。
static unsigned int indexForPointer(const void *p) {
    uintptr_t addr = reinterpret_cast<uintptr_t>(p);
    return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
}
#reinterpret_cast这个函数是C++里的强制类型转换符。
reinterpret_cast<type-id> (expression)
type-id 必须是一个指针、引用、算术类型、函数指针或者成员指针。它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,再把该整数转换成原类型的指针,还可以得到原先的指针值)。
所以上述的indexForPointer方法一种hash算法。,具体的((addr >> 4) ^ (addr >> 9)),取数字4和9的原因暂无任何发现。

接下来就是AutoreleasePoolPage的源码部分了。 比较有意思的是下面这段代码

static __inline__ void*
_os_tsd_get_direct(unsigned long slot)
{
	void *ret;
#if defined(__i386__) || defined(__x86_64__)
	__asm__("mov %%gs:%1, %0" : "=r" (ret) : "m" (*(void **)(slot * sizeof(void *))));
#endif


	return ret;
}

这段汇编代码是用来获取hotPage的。我们来细细分析一下这段汇编代码。 首先我们将分解一下部分内嵌汇编指令

mov %1,%0是指令模板,%0,%1是指令的占位符,将它们与C语言的表达式与后面的ret和(*(void **)(slot * sizeof(void *))))关联起来
"=r"其中"="表示ret是输出操作数。"r"表示ret与某个通用寄存器相关联起来。这里使用的寄存器就是gs寄存器。"m"表示直接指示内存地址,意思就是直接修改内存变量。
%rdi保存参数1,%rsi,%rdx,%rcx分别保存参数2、3、4,%rax保存返回结果,%rsp是栈指针。

有了上面的解释之后,我们可以尝试将汇编代码修改为

movq 0(,%rdi,8), %rax
movq %rax,-8(%rsp)
mov %gs:-8(%rsp), %rax
ret

可以看到对应的setHotpage的方法

static __inline__ int
_os_tsd_set_direct(unsigned long slot, void* val)
{
#if defined(__i386__) && defined(__PIC__)
	__asm__("movl %1, %%gs:%0" : "=m" (*(void **)(slot * sizeof(void *))) : "rn" (val));
#elif defined(__i386__) && !defined(__PIC__)
	__asm__("movl %1, %%gs:%0" : "=m" (*(void **)(slot * sizeof(void *))) : "ri" (val));
#elif defined(__x86_64__)
	__asm__("movq %1, %%gs:%0" : "=m" (*(void **)(slot * sizeof(void *))) : "rn" (val));
#endif

	return 0;
}

转换后汇编为

movq %rsi, %gs:0(,%rdi,8)
xorl %eax, %eax
ret

如果对GCC-Inline-Assembly感兴趣的话,可以看一下GCC-Inline-Assembly-HOWTO

接下来的部分就是NSObject的方法。将在之后阅读runtime和class之后再展开。

参考资料

fast_hash

科普:为什么 String hashCode 方法选择数字31作为乘子

GCC-Inline-Assembly-HOWTO