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);
}
更新中。。。。。