Runtime详解

Runtime 简介

Objective-C是一门动态性比较强的编程语言,跟C、C++等语言有着很大的不同
Objective-C的动态性是由Runtime API来支撑的
Runtime API提供的接口基本都是C语言的,源码由C\C++\汇编语言编写

基础知识

这是在读取Apple底层的源码的基础知识,如果能够理解透彻对于研读源码会带来很大帮助。

如果想取出来二进制中某位的值,则将那一位的值置为1,其余位置为0,然后按位与运算即可。如某值1010,想取出来第二位的值(从右往左),则采用0010进行与运算,得到0010,即第二位是1

0b 0000 1010              0b 0000 1000     
&  0000 0010              | 0000 0010
---------------------------------------------
0b 0000 0010              0b 0000 1010

一般情况下,在给某个类设定属性的时候,都会是直接利用property进行设定,例如设定三个BOOL属性,那么占据了3个字节的空间地址,可是实际上是使用到的只有3位,那么会造成3 * 8 - 3 = 21位的浪费,此时可以利用一个char型字节进行节省空间,并采用上述介绍到的位操作,进行char字节某几个位置的使用。

位运算在Apple官方源码中很常用,下面模仿Apple官方源码为如下的大致情况

typedef enum {
    ZJOptionsOne = 1 << 0,
    ZJOptionsTwo = 1 << 1,
    ZJOptionsThree = 1 << 2,
    ZJOptionsFour = 1 << 3
} ZJOptions;

-(void)setOptions:(ZJOptions)options{
    if (options & ZJOptionsOne) {
        NSLog(@"ZJOptionsOne");
    }
    if (options & ZJOptionsTwo) {
        NSLog(@"ZJOptionsTwo");
    }
    if (options & ZJOptionsThree) {
        NSLog(@"ZJOptionsThree");
    }
    if (options & ZJOptionsFour) {
        NSLog(@"ZJOptionsFour");
    }
}

Talk is easy, show me your code
即如下代码

内存使用空间优化V1.0
@interface Person : NSObject

-(void)setTall:(BOOL)tall;
-(void)setRich:(BOOL)rich;
-(void)setFat:(BOOL)fat;

-(BOOL)getTall;
-(BOOL)getRich;
-(BOOL)getFat;

@end

#define ZJTallMASK 1 << 0  //0b 0000 0001
#define ZJRichMASK 1 << 1  //0b 0000 0010
#define ZJFatMASK 1 << 2  //0b 0000 0100


@interface Person()
{
    char _tallRichFat;
}

@end

@implementation Person
- (instancetype)init
{
    self = [super init];
    if (self) {
        _tallRichFat = 0b00000101;
    }
    return self;
}

-(void)setTall:(BOOL)tall{
    if (tall) {
        _tallRichFat |= ZJTallMASK;
    }else{
        _tallRichFat &= ~(ZJTallMASK);
    }
}
-(BOOL)getTall{
    return !!(_tallRichFat & ZJTallMASK);
}
-(void)setRich:(BOOL)rich{
    if (rich) {
        _tallRichFat |= ZJRichMASK;
    }else{
        _tallRichFat &= ~(ZJRichMASK);
    }
}
-(BOOL)getRich{
    return !!(_tallRichFat & ZJRichMASK);
}
-(void)setFat:(BOOL)fat{
    if (fat) {
        _tallRichFat |= ZJFatMASK;
    }else{
        _tallRichFat &= ~(ZJFatMASK);
    }
}
-(BOOL)getFat{
    return !!(_tallRichFat & ZJFatMASK);
}

在本代码中设置了一个char型的成员变量,并通过位操作来使用1个字节中的某几位来达到节省内存的目的。
似乎这样子的代码已经能够做到日常的优化使用,倒是对于维护而言似乎有些繁琐。这时候可以将_tallRichFat设置成结构体,并在结构体中利用位域声明改变量使用到的位数。即如下:

V2.0
@interface Person()
{
//    char _tallRichFat;
    struct {
        char tall : 1;
        char rich : 1;
        char fat : 1;
    } _tallRichFat;
}
@end

@implementation Person
-(void)setTall:(BOOL)tall{
    _tallRichFat.tall = tall;
}
-(BOOL)getTall{
    return _tallRichFat.tall;
}
-(void)setRich:(BOOL)rich{
    _tallRichFat.rich = rich;
}
-(BOOL)getRich{
    return _tallRichFat.rich;
}
-(void)setFat:(BOOL)fat{
    _tallRichFat.fat = fat;
}
-(BOOL)getFat{
    return _tallRichFat.fat;
}
@end

不过这样子会产生一个问题,也就是我们在get方法里,采用结构体取出来的是某一位,可是我们返回的是一个BOOL值,例如getTall方法里,我们返回了0b1,这时候将一位二进制强制转换成一个八位二进制,会发生0b11111111,也就是原本是要返回的1,此时会返回-1(无符号是255 有符号是-1),有两种解决方案,此时可以调整结构体中每个变量所使用的空间大小,调整成两位,即如下:

@interface Person()
{
//    char _tallRichFat;
    struct {
        char tall : 2;
        char rich : 2;
        char fat : 2;
    } _tallRichFat;
}
@end

此时不会出现什么问题,那么在后续的调整优化中似乎不是那么友好,因此采用如下的方式进行,即进行两次取反操作即可。

-(BOOL)getTall{
    return !!_tallRichFat.tall;
}
-(BOOL)getRich{
    return !!_tallRichFat.rich;
}
-(BOOL)getFat{
    return !!_tallRichFat.fat;
}

这时候似乎好像是进行了优化,后期调整也挺好的,不过这种并不是Apple官方所采取的方式,Apple官方采取了共用体 union的方式,进行优化。什么是union呢,如下解释,union是指在union中制定某一占用字节数最长变量的内存地址为空间,union中所有的变量都共享这一片空间,即如下:

union data{
    int n;
    char ch;
    double f;
};

此时data共用体中f的字节数最大,占用8个字节的空间,那么就制定这片空间的地址为共用内存(经过验证,是首先出现的最大变量的内存空间为共用内存地址)。那么在使用中,指定 n = 2020 后,在指定 ch = ‘a’,此时并不能保证n的值为2020了,会发生改变,那么这就是共用体 union。

那么回到上述的情况中,按照Apple官方的方式进行优化,可以对上述声明的结构体采用union进行优化,即如下:

V3.0
@interface Person()
{
    union {
        char bits;
        struct {
            char tall : 1;
            char rich : 1;
            char fat : 1;
        };
    } _tallRichFat;
}
@end

这里的struct并没有任何的使用,只是起到了说明解释的作用,把他删除也是无所谓的,本质上还是通过MASK掩码来控制位运算,达到对于某个位置的读取作用。

isa

此时再去查看isa,可以发现如下源码:

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};

struct objc_object {
private:
    isa_t isa;

public:
	......
}

查看 ISA_BITFIELD 宏定义可以发现

# if __arm64__                            //ios
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
#   define ISA_BITFIELD                                                      \
      uintptr_t nonpointer        : 1;                                       \
      uintptr_t has_assoc         : 1;                                       \
      uintptr_t has_cxx_dtor      : 1;                                       \
      uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
      uintptr_t magic             : 6;                                       \
      uintptr_t weakly_referenced : 1;                                       \
      uintptr_t deallocating      : 1;                                       \
      uintptr_t has_sidetable_rc  : 1;                                       \
      uintptr_t extra_rc          : 19
#   define RC_ONE   (1ULL<<45)
#   define RC_HALF  (1ULL<<18)

# elif __x86_64__                     // macos
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
#   define ISA_BITFIELD                                                        \
      uintptr_t nonpointer        : 1;                                         \
      uintptr_t has_assoc         : 1;                                         \
      uintptr_t has_cxx_dtor      : 1;                                         \
      uintptr_t shiftcls          : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
      uintptr_t magic             : 6;                                         \
      uintptr_t weakly_referenced : 1;                                         \
      uintptr_t deallocating      : 1;                                         \
      uintptr_t has_sidetable_rc  : 1;                                         \
      uintptr_t extra_rc          : 8
#   define RC_ONE   (1ULL<<56)
#   define RC_HALF  (1ULL<<7)

# else
#   error unknown architecture for packed isa
# endif

nonpointer
0,代表普通的指针,存储着Class、Meta-Class对象的内存地址
1,代表优化过,使用位域存储更多的信息

has_assoc
是否有设置过关联对象,如果没有,释放时会更快

has_cxx_dtor
是否有C++的析构函数(.cxx_destruct),如果没有,释放时会更快

shiftcls
存储着Class、Meta-Class对象的内存地址信息

magic
用于在调试时分辨对象是否未完成初始化

weakly_referenced
是否有被弱引用指向过,如果没有,释放时会更快

deallocating
对象是否正在释放

extra_rc
里面存储的值是引用计数器减1

has_sidetable_rc
引用计数器是否过大无法存储在isa中
如果为1,那么引用计数会存储在一个叫SideTable的类的属性中

这时候可以看到 uintptr_t shiftcls 在iOS上是33位,在macos上是44位。
其实在arm64架构之前,isa就是一个普通的指针,存储着Class、Meta-Class对象的内存地址。

从arm64架构开始,对isa进行了优化,变成了一个共用体(union)结构,还使用位域来存储更多的信息,arm64之后的的isa指针需要先 & ISA_MASK ,然后才是能取出来地址。
通过计算器可以进行尽职转换 ISA_MASK 如下:


可以观察到后面的三位都为0,那么也就是意味着isa指针在 &ISA_MASK 后取出来的Class metaClass地址值最后三位一定是0,也就是用十六进制打印地址的话最后一位必定是都是0或者8。

Class结构
struct objc_class : objc_object {  // objc_object是包含了isa指针的结构体
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable 方法缓存
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags  用于获取具体的类信息

    class_rw_t *data() { 
        return bits.data();
    }
    void setData(class_rw_t *newData) {
        bits.setData(newData);
    }
    ......
}

我们查看这里的class_data_bits_t,可以发现有这样子的注释

// class_data_bits_t is the class_t->data field (class_rw_t pointer plus flags)
// The extra bits are optimized for the retain/release and alloc/dealloc paths.

// class_data_bits_t是class_t->数据字段(class_rw_t指针加标记)
//额外的位是针对retain/release和alloc/dealloc路径进行优化的。

在进行查看 class_data_bits_t 结构体中,发现了这样子的一个函数

struct class_data_bits_t{
	......
	public:
		    class_rw_t* data() {
        		return (class_rw_t *)(bits & FAST_DATA_MASK);
    		}
   ......
}

也就是说objc_class 中的bits 在& FAST_DATA_MASK 后会取出来 class_rw_t ,在进行查看了 class_rw_t 发现其结构是这样子的

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    method_array_t methods;          //方法列表  存放类和分类中的方法
    property_array_t properties;     //属性列表   这三个都是一个二维数组,可以查看下面的类型
    protocol_array_t protocols;      //协议列表

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;
	......
};

class method_array_t : //二维数组,首先是一个 method_list_t ,method_list_t包含了method_t,下面两个类似
    public list_array_tt<method_t, method_list_t> 
	......

class property_array_t : 
    public list_array_tt<property_t, property_list_t> 
	......

class protocol_array_t : 
    public list_array_tt<protocol_ref_t, protocol_list_t> 
	......

const class_ro_t是如下的结构体

struct class_ro_t {             	//存放不包含分类的方法,成员变量等信息
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;              //instance对象所占据的空间大小
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;
    
    const char * name;                 //类名
    method_list_t * baseMethodList;     //里面存放method,正好对应了上面的 class_rw_t 中的 method_array_t 中的元素
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;        //成员变量列表

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;

    method_list_t *baseMethods() const {
        return baseMethodList;
    }
};

其实上 objc_class 结构体中的 class_data_bits_t bits 首先是指向了 class_ro_t,后面把分类加载后,又重新设置了bits指向为 class_rw_t ,在realizeClass中可以查看到这样子的情况

static Class realizeClass(Class cls)
{
    runtimeLock.assertWriting();

    const class_ro_t *ro;
    class_rw_t *rw;
    Class supercls;
    Class metacls;
    bool isMeta;

    if (!cls) return nil;
    if (cls->isRealized()) return cls;
    assert(cls == remapClass(cls));

    // fixme verify class is not in an un-dlopened part of the shared cache?

    ro = (const class_ro_t *)cls->data();
    if (ro->flags & RO_FUTURE) {                            //已经存在了class_rw_t
        // This was a future class. rw data is already allocated.
        rw = cls->data();
        ro = cls->data()->ro;
        cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
    } else {
        // Normal class. Allocate writeable class data.
        rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);     //为 class_rw_t 进行内存空间的分配
        rw->ro = ro;                                         //使得新创建的rw的ro指向最开始的ro
        rw->flags = RW_REALIZED|RW_REALIZING;                //设置flags
        cls->setData(rw);                                    //设置数据
    }

    isMeta = ro->flags & RO_META;

    rw->version = isMeta ? 7 : 0;  // old runtime went up to 6


    // Choose an index for this class.
    // Sets cls->instancesRequireRawIsa if indexes no more indexes are available
    cls->chooseClassArrayIndex();
    
    ......
};

何为method_t,查看源码可以看到

struct method_t {
    SEL name;    //SEL代表方法\函数名,一般叫做选择器,底层结构跟char *类似
    		 //可以通过@selector()和sel_registerName()获得
    		 //可以通过sel_getName()NSStringFromSelector()转成字符串
    		 //不同类中相同名字的方法,所对应的方法选择器是相同的,意思就是所有相同的的SEL是唯一的地址
    const char *types;  //types包含了函数返回值、参数1、参数2、参数3...的字符串 采用@encode(...)
    IMP imp;    //IMP是一个指针,代表函数的具体实现,存储函数地址值
	......
};
方法缓存

在objc_class中存在cache_t cache,观察其结构体如下:

struct cache_t {
    struct bucket_t *_buckets;  //散列表 数组
    mask_t _mask;               //散列表长度 - 1
    mask_t _occupied;          //已经缓存的方法数量
	......
};
struct bucket_t {
private:
    cache_key_t _key;          //SEL作为Key
    IMP _imp;                  //函数的内部地址
	......
};

//散列表的计算方式,采用_mask与SEL进行&运算,然后转换成mask_t,直接从散列表中取出来相应的方法地址
static inline mask_t cache_hash(cache_key_t key, mask_t mask)。 //进行hash运算,得到索引
{
    return (mask_t)(key & mask);
};
bucket_t * cache_t::find(cache_key_t k, id receiver)           //取出方法
{
    assert(k != 0);

    bucket_t *b = buckets();
    mask_t m = mask();
    mask_t begin = cache_hash(k, m);
    mask_t i = begin;
    do {
        if (b[i].key() == 0  ||  b[i].key() == k) {           //没有找到或者找到了
            return &b[i];
        }
    } while ((i = cache_next(i, m)) != begin);                //如果存在散列冲突,那么调用cache_next,后退一位进行寻找

    // hack
    Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
    cache_t::bad_cache(receiver, (SEL)k, cls);
}

#__arm64__
// objc_msgSend has lots of registers available.
// Cache scan decrements. No end marker needed.
#define CACHE_END_MARKER 0
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask;                                   //后退一位进行寻找,如果到达头部,从尾部开始寻找
}

对于散列表而言,其容量可能会填满,当填满时,Apple采用了清空缓存,并且扩大散列表的空间容量,_mask的值变为新容量-1 。

void cache_t::expand()                   //扩容散列表
{
    cacheUpdateLock.assertLocked();
    
    uint32_t oldCapacity = capacity();
    uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;

    if ((uint32_t)(mask_t)newCapacity != newCapacity) {
         // mask overflow  can nott grow further
         // fixme this wastes one bit of mask
         newCapacity = oldCapacity;
    }

    reallocate(oldCapacity, newCapacity);
}

更新中。。。。。

代码地址GitHub

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值