Object-C与Swift的RunTime运行机制对比

【注】 文中关于Swift部分转载自欧阳大哥2013的Swift5.0的Runtime机制浅析,为了方便对比学习,我添加了OC RunTime的运行机制。html

Objective-C语言是一门以C语言为基础的面向对象编程语言,其提供的运行时(Runtime)机制使得它也能够被认为是一种动态语言。运行时的特征之一就是对象方法的调用是在程序运行时才被肯定和执行的。系统提供的开放接口使得咱们能够在程序运行的时候执行方法替换以便实现一些诸如系统监控、对象行为改变、Hook等等的操做处理。然而这种开放性也存在着安全的隐患,咱们能够借助Runtime在AOP层面上作一些额外的操做,而这些额外的操做由于没法进行管控, 因此有可能会输出未知的结果。c++

多是苹果意识到了这个问题,因此在推出的Swift语言中Runtime的能力获得了限制,甚至能够说是取消了这个能力,这就使得Swift成为了一门静态语言。Swift语言中对象的方法调用机制和OC语言彻底不一样,Swift语言的对象方法调用基本上是在编译连接时刻就被肯定的,能够看作是一种硬编码形式的调用实现。算法

Swfit中的对象方法调用机制加快了程序的运行速度,同时减小了程序包体积的大小。可是从另一个层面来看当编译连接优化功能开启时反而又会出现包体积增大的状况。Swift在编译连接期间采用的是空间换时间的优化策略,是以提升运行速度为主要优化考虑点。编程

我将从下面4点进行对比分析swift

  • 一、类中定义的常规方法
  • 二、继承父类方法调用
  • 三、extension中定义的方法
  • 四、成员变量的访问

一、类中定义的常规方法

OC普通方法的调用

一个Class类包含如下几种元素数组

  • 一、isa指针,存储着Class、 Meta-Class 对象的内存地址
  • 二、superClass指向父类的指针
  • 三、cache方法缓存
  • 四、bits具体类的信息

bits & FAST_DATA_MASK 指向一个新的结构体Class_rw_t,里面包含着methods方法列表properties属性列表protocols协议列表class_ro_t类的初始化信息等一些类信息缓存

Class_rw_t里面的methods方法列表properties属性列表都是二维数组,是可读可写的,包含类的初始内容分类的内容安全

class_ro_t里面的baseMethodList,baseProtocols,Ivars,baseProperties是一维数组,是只读的,包含类的初始化内容bash

methods方法列表是存储的一个个的method_t结构体。method_t是对方法的封装app

struct method_t{
	SEL name;//函数名
	const char *types;//编码(返回值类型,参数类型)
	IMP imp;//指向函数的指针(函数地址)
}
复制代码

IMP表明函数的具体实现

typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
复制代码

第一个参数是指向self的指针(若是是实例方法,则是类实例的内存地址;若是是类方法,则是指向元类的指针),第二个参数是方法选择器(selector)

SEL

SEL表明方法名,通常叫作选择器,底层结构跟char *相似

  • 能够经过@selector()sel_registerName()得到
  • 能够经过sel_getName()NSStringFromSelector()转成字符串
  • 不一样类中相同名字的方法,所对应的方法的选择器是相同的
  • 具体实现typedef struct objc_selector *SEL

types

types包含了函数返回值,参数编码的字符串

结构为:返回值 参数1 参数2...参数N

iOS中提供了一个叫作@encode的指令,能够将具体的类型表示成字符串编码

例如

// "i24@0:8i16f20"
// 0id 8SEL 16int 20float  == 24
- (int)test:(int)age height:(float)height
复制代码

每个方法都有两个默认参数self_msg 咱们能够查到id类型为@SEL类型为:

  • 一、第一个参数i返回值
  • 二、第二个参数@id 类型的self
  • 三、第三个参数:SEL 类型的_msg
  • 四、第四个参数iInt age
  • 五、第五个参数ffloat height

其中加载的数字实际上是跟所占字节有关

  • 一、24 总共占有多少字节
  • 二、@0id 类型的self的起始位置为0
  • 三、:8 是由于id 类型的self占字节为8,因此SEL 类型的_msg`的起始位置为8

Class内部结构中有一个方法缓存cache_t,用散列表(哈希表)来缓存曾经调用过的方法,能够提升方法的查找速度。

cache_t结构体里面有三个元素

  • buckets 散列表,是一个数组,数组里面的每个元素就是一个bucket_t,bucket_t里面存放两个

    • _key SEL做为key
    • _imp 函数的内存地址
  • _mask 散列表的长度

  • _occupied已经缓存的方法数量

为何会用到方法缓存

这张图片是咱们方法产找路径,若是咱们的一个类有多个父类,须要调用父类方法,他的查找路径为

  • 一、先遍历本身全部的方法
  • 二、若是在本身类中找不到方法,则遍历父类全部方法,没有查找到调用方法以前,一直重复该动做 若是每一次方法调用都是走这样的步骤,对于系统级方法来讲,其实仍是比较消耗资源的,为了应对这个状况。出现了方法缓存,调用过的方法,都放在缓存列表中,下次查找方法的时候,如今缓存中查找,若是缓存中查找不到,而后在执行上面的方法查找流程。

散列表结构

散列表的结构大概就像上面那样,数组的下标是经过@selector(方法名)&_mask来求得,具体每个数组的元素是一个结构体,里面包含两个元素_imp@selector(方法名)做为的key

一个值与&上一个_mask,得出的结果必定小于等于_mask值,而_mask值为数组长度-1,因此任什么时候候,也不会越界。

其实这就是散列表的算法,也有一些其余的算法,取余,一个值取余&的效果是相同的。

Swift普通方法的调用

Swift为每一个类都创建了一个被称之为虚表的数组结构,这个数组会保存着类中全部定义的常规成员方法函数的地址。每一个Swift类对象实例的内存布局中的第一个数据成员和OC对象类似,保存有一个相似isa的数据成员。isa中保存着Swift类的描述信息。对于Swift类的类描述结构苹果并未公开(也许有我并不知道),类的虚函数表保存在类描述结构的第0x50个字节的偏移处,每一个虚表条目中保存着一个常规方法的函数地址指针。每个对象方法调用的源代码在编译时就会转化为从虚表中取对应偏移位置的函数地址来实现间接的函数调用。下面是对于常规方法的调用Swift语言源代码和C语言伪代码实现:

////////Swift源代码

//基类定义
class CA {
 open func foo1(_ a:Int){}
 open func foo1(_ a:Int, _ b:Int){}
 open func foo2(){}
}

//扩展
extension CA{
 open func extfoo(){} 
}

//派生类定义
class CB:CA{
 open func foo3(){}
 override open func foo1(_ a:Int){}
}

func testfunc(_ obj:CA){
 obj.foo1(10)
}

func main() {
 let objA = A()
 objA.foo1(10)
 objA.foo1(10,20)
 objA.foo2()
 objA.extfoo()

 let objB = B()
 objB.foo1(10)
 objB.foo1(10,20)
 objB.foo2()
 objB.foo3()
 objB.extfoo()

 testfunc(objA)
 testfunc(objB)
}

复制代码
////////C伪代码

//...........................................运行时定义部分

//Swift类描述。
struct swift_class {
    ...   //其余的属性,由于这里不关心就不列出了
    //虚函数表恰好在结构体的第0x50的偏移位置。
    IMP vtable[0];
};


//...........................................源代码中类的定义和方法的定义和实现部分


//基类定义
struct CA {
      struct swift_class *isa;
};

//派生类定义
struct CB {
   struct swift_class *isa;
};

//基类CA的方法函数的实现,这里对全部方法名都进行修饰命名
void _$s3XXX2CAC4foo1yySiF(int a){}   //CA类中的foo1
void _$s3XXX2CAC4foo1yySi_SitF(int a, int b){} //CA类中的两个参数的foo1
void _$s3XXX2CAC4foo2yyF(){}   //CA类中的foo2
void _$s3XXX2CAC6extfooyyF(){} //CA类中的extfoo函数  

//派生类CB的方法函数的实现。
void _$s3XXX2CBC4foo1yySiF(int a){}   //CB类中的foo1,重写了基类的方法,可是名字不同了。
void _$s3XXX2CBC4foo3yyF(){}             //CB类中的foo3

 //构造基类的描述信息以及虚函数表
struct swift_class classCA;
classCA.vtable[3] = {&_$s3XXX2CAC4foo1yySiF, &_$s3XXX2CAC4foo1yySi_SitF, &_$s3XXX2CAC4foo2yyF};

//构造派生类的描述信息以及虚函数表,注意这里虚函数表会将基类的函数也添加进来并且排列在前面。
struct swift_class classCB;
classCB.vtable[4] = {&_$s3XXX2CBC4foo1yySiF, &_$s3XXX2CAC4foo1yySi_SitF, &_$s3XXX2CAC4foo2yyF, &_$s3XXX2CBC4foo3yyF};

void testfunc(A *obj){
   obj->isa->vtable[0](10);   //间接调用实现多态的能力。
}


//...........................................源代码中程序运行的部分

void main(){
   CA *objA = CA.__allocating_init(classCA);
   objA->isa = &classCA;
   asm("mov x20, objA")
   objA->isa->vtable[0](10);
   objA->isa->vtable[1](10,20);
   objA->isa->vtable[2]();
   _$s3XXX2CAC6extfooyyF()

  CB *objB = CB.__allocating_init(classCB);
  objB->isa = &classCB;
  asm("mov x20, objB");
  objB->isa->vtable[0](10);
  objB->isa->vtable[1](10,20);
  objB->isa->vtable[2]();
  objB->isa->vtable[3]();
   _$s3XXX2CAC6extfooyyF();

  testfunc(objA);
  testfunc(objB);

}

复制代码

从上面的代码中能够看出一些特色:

  • 一、Swift类的常规方法中不会再有两个隐藏的参数了,而是和字面定义保持一致。那么问题就来了,方法调用时对象如何被引用和传递呢?在其余语言中通常状况下对象老是会做为方法的第一个参数,在编译阶段生成的机器码中,将对象存放在x0这个寄存器中(本文以arm64体系结构为例)。而Swift则不一样,对象再也不做为第一个参数来进行传递了,而是在编译阶段生成的机器码中,将对象存放在x20这个寄存器中(本文以arm64体系结构为例)。这样设计的一个目的使得代码更加安全。

  • 二、每个方法调用都是经过读取方法在虚表中的索引获取到了方法函数的真实地址,而后再执行间接调用。在这个过程虚表索引的值是在编译时就肯定了,所以再也不须要经过方法名来在运行时动态的去查找真实的地址来实现函数调用了。虽然索引的位置在编译时肯定的,可是基类和派生类虚表中相同索引处的函数的地址确能够不一致,当派生类重写了父类的某个方法时,由于会分别生成两个类的虚表,在相同索引位置保存不一样的函数地址来实现多态的能力。

  • 三、每一个方法函数名字都和源代码中不同了,缘由在于在编译连接是系统对全部的方法名称进行了重命名处理,这个处理称为命名修饰。之因此这样作是为了解决方法重载和运算符重载的问题。由于源代码中重载的方法函数名称都同样只是参数和返回类型不同,所以没法简单的经过名字进行区分,而只能对名字进行修饰重命名。另一个缘由是Swift还提供了命名空间的概念,也就是使得能够支持不一样模块之间是能够存在相同名称的方法或者函数。由于整个重命名中是会带上模块名称的。下面就是Swift中对类的对象方法的重命名修饰规则: _$s<模块名长度><模块名><类名长度><类名>C<方法名长度><方法名>yy<参数类型1>_<参数类型2>_<参数类型N>F

就好比上面的CA类中的foo1两个同名函数在编译连接时刻就会被分别重命名为:

//这里面的XXX就是你工程模块的名称。
void _$s3XXX2CAC4foo1yySiF(int a){}   //CA类中的foo1
void _$s3XXX2CAC4foo1yySi_SitF(int a, int b){} //CA类中的两个参数的foo1
复制代码

下面这张图就清晰的描述了Swift类的对象方法调用以及类描述信息。

二、继承父类方法调用

OC继承父类方法调用

OC对象主要能够分为3种

  • 一、instance对象(实例对象):instance实例对象就是经过alloc出来的对象,每次调用alloc都会产生新的instance对象
  • 二、class对象(类对象):每一个类的内存中有且只有一个类对象
  • 三、meta-class对象(元类对象):每一个类的内存中有且只有一个元类对象 实例对象的存储信息
  • isa指针
  • 其余成员变量

类对象的存储信息

  • isa指针
  • superClass指针
  • 类的属性信息(@property),类的对象方法信息(method),类的协议信息(protocol),类的成员变量信息(ivar)

元类的存储信息

  • isa指针
  • superClass指针
  • 类的属性信息(@property),类的对象方法信息(method),类的协议信息(protocol),类的成员变量信息(ivar)

元类和类的存储结构是同样的,可是用途不同

  • instance的isa指向class,当调用对象方法时,经过instance的isa找到class,最后找到对象方法的实现进行调用
  • class的isa指向meta-class,当调用类方法时,经过class的isa找到meta-class,最后找到类方法

总览图

  • 一、instance的isa指向class
  • 二、class的isa指向meta-class
  • 三、meta-class的isa指向基类的meta-class
  • 四、class的superclass指向父类的class,若是没有父类,superclass指向nil
  • 五、meta-class的superclass指向父类的meta-class,基类的meta-class的superclass指向基类的class
  • 六、instance的调用轨迹:isa找class,方法不存在,就经过superclass找父类
  • 七、class调用类方法的轨迹:isa找到meta-class,方法不存在,就经过superclass找父类

Swift继承OC类的派生类而且重写了基类的方法

若是在Swift中的使用了OC类,好比还在使用的UIViewControllerUIView等等。而且还重写了基类的方法,好比必定会重写UIViewControllerviewDidLoad方法。对于这些类的重写的方法定义信息仍是会保存在类的Class结构体中,而在调用上仍是采用OC语言的Runtime机制来实现,即经过objc_msgSend来调用。而若是在OC派生类中定义了一个新的方法的话则实现和调用机制就不会再采用OC的Runtime机制来完成了,好比说在UIView的派生类中定义了一个新方法foo,那么这个新方法的调用和实现将与OC的Runtime机制没有任何关系了!

////////Swift源代码

//类定义
class MyUIView:UIView {
    open func foo(){}   //常规方法
    override func layoutSubviews() {}  //重写OC方法
}

func main(){
  let obj = MyUIView()
  obj.layoutSubviews()   //调用OC类重写的方法
  obj.foo()   //调用常规的方法。
}
复制代码
////////C伪代码

//...........................................运行时定义部分

//OC类的方法结构体
struct method_t {
   SEL name;
   IMP imp;
};

//Swift类描述
struct swift_class {
   ...   //其余的属性,由于这里不关心就不列出了。
   struct method_t  methods[1];
   ...   //其余的属性,由于这里不关心就不列出了。
   //虚函数表恰好在结构体的第0x50的偏移位置。
   IMP vtable[1];
};


//...........................................源代码中类的定义和方法的定义和实现部分

//类定义
struct MyUIView {
     struct swift_class *isa;
}

//类的方法函数的实现
void layoutSubviews(id self, SEL _cmd){}
void foo(){}  //Swift类的常规方法中和源代码的参数保持一致。

//类的描述信息构建,这些都是在编译代码时就明确了而且保存在数据段中。
struct swift_class classMyUIView;
classMyUIView.methods[0] = {"layoutSubviews", &layoutSubviews};
classMyUIView.vtable[0] = {&foo};


//...........................................源代码中程序运行的部分

void main(){
 MyUIView *obj = MyUIView.__allocating_init(classMyUIView);
 obj->isa = &classMyUIView;
 //OC类重写的方法layoutSubviews调用仍是用objc_msgSend来实现
 objc_msgSend(obj, @selector(layoutSubviews);
 //Swift方法调用时对象参数被放到x20寄存器中
 asm("mov x20, obj");
 //Swift的方法foo调用采用间接调用实现
 obj->isa->vtable[0]();
}
复制代码

三、extension中定义的方法

OC分类的原理

对于分类的做用恐怕你们都是知道的吧,咱们研究一下分类的实现原理。

首先建立一个person类,而后在建立person类的两个分类Person+eat&Person+Run。 研究原理咱们的思路就是

  • 一、生成c++文件,查看c++文件中的实现
  • 二、若是c++文件中实现介绍的不太具体就去查看源码实现

咱们使用xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+eat.m来生成c++代码

咱们能够找到分类都包含了哪些东西

struct _category_t {
	const char *name;
	struct _class_t *cls;
	const struct _method_list_t *instance_methods;
	const struct _method_list_t *class_methods;
	const struct _protocol_list_t *protocols;
	const struct _prop_list_t *properties;
};
复制代码

咱们发现里面并无对方法属性协议等等的具体实现过程,那么咱们在去源码中查看一下相关实现过程。

源码解读顺序

  • 一、objc-os.mm(runtime初始化的代码)
    • _objc_init
    • map_images
    • map_images_nolock
  • 二、objc-runtime-new.mm
    • _read_images
    • remethodizeClass
    • attachCategories
    • attachLists
    • realloc、memmove、 memcpy

咱们按照源码查找一路找到attachCategories方法,咱们发现这个方法就是对分类的实现。里面第一句解释Attach method lists and properties and protocols from categories to a class.将方法列表、属性和协议从类别附加到类中。

static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
	if (!cats) return;
	if (PrintReplacedMethods) printReplacements(cls, cats);
	bool isMeta = cls->isMetaClass();
	//方法数组,这是一个二维数组
	/*
	[
	[method_t,method_t],
	[method_t,method_t]
	]
	*/
	method_list_t **mlists = (method_list_t **)
	malloc(cats->count * sizeof(*mlists));
	//属性数组,这是一个二维数组
	property_list_t **proplists = (property_list_t **)
	malloc(cats->count * sizeof(*proplists));
	//协议数组,这是一个二维数组
	protocol_list_t **protolists = (protocol_list_t **)
	malloc(cats->count * sizeof(*protolists));

	int mcount = 0;
	int propcount = 0;
	int protocount = 0;
	int i = cats->count;
	bool fromBundle = NO;
	while (i--) {
    	//取出某个分类
    	auto& entry = cats->list[i];
    	//取出分类里面的方法列表
    	method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
    	if (mlist) {
    		mlists[mcount++] = mlist;
    		fromBundle |= entry.hi->isBundle();
    	}
    
    	property_list_t *proplist = 
    	entry.cat->propertiesForMeta(isMeta, entry.hi);
    	if (proplist) {
    		proplists[propcount++] = proplist;
    	}
    
    	protocol_list_t *protolist = entry.cat->protocols;
    	if (protolist) {
    		protolists[protocount++] = protolist;
    	}
	}
	//获得对象里面的数据
	auto rw = cls->data();

	prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
	//将全部分类的对象方法,附加到类对象的方法列表中
	rw->methods.attachLists(mlists, mcount);
	free(mlists);
	if (flush_caches  &&  mcount > 0) flushCaches(cls);
	//将全部分类的属性,附加到类对象的属性列表中
	rw->properties.attachLists(proplists, propcount);
	free(proplists);
	//将全部分类的协议,附加到类对象的协议列表中
	rw->protocols.attachLists(protolists, protocount);
	free(protolists);
}
复制代码

咱们发现rw->methods.attachLists(mlists, mcount);方法是实现将全部分类的对象方法,附加到类对象的方法列表中,其余两个属性和协议都是调用这个方法,咱们分析一个就能够了。

点击进入attachLists方法,里面有一个段实现代码

if (hasArray()) {
	// many lists -> many lists
	uint32_t oldCount = array()->count;
	uint32_t newCount = oldCount + addedCount;
	setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
	array()->count = newCount;
	//array()->list 原来的方法列表
	memmove(array()->lists + addedCount, array()->lists, 
	oldCount * sizeof(array()->lists[0]));
	//addedList 全部分类的方法列表
	memcpy(array()->lists, addedLists, 
	addedCount * sizeof(array()->lists[0]));
}
复制代码
  • 一、扩容,把类中的方法数组和分类中的方法数组计算出来
  • 二、memmove把类中方法放到数组的最后一位
  • 三、memcpy把分类中的方法放到数组的前面。

extension中定义的方法

若是是在Swift类的extension中定义的方法(重写OC基类的方法除外)。那么针对这个方法的调用老是会在编译时就决定,也就是说在调用这类对象方法时,方法调用指令中的函数地址将会以硬编码的形式存在在extension中定义的方法没法在运行时作任何的替换和改变!并且方法函数的符号信息都不会保存到类的描述信息中去。这也就解释了在Swift中派生类没法重写一个基类中extension定义的方法的缘由了。由于extension中的方法调用是硬编码完成,没法支持多态!下面的Swift源代码以及C伪代码实现说明了这个状况

////////Swift源代码

//类定义
class CA {
   open func foo(){}
}

//类的extension定义
extension CA {
  open func extfoo(){}
}

func main() {
 let obj = CA()
 obj.foo()
 obj.extfoo()
}
复制代码
////////C伪代码

//...........................................运行时定义部分


//Swift类描述。
struct  swift_class {
    ...   //其余的属性,由于这里不关心就不列出了。
   //虚函数表恰好在结构体的第0x50的偏移位置。
    IMP vtable[1];
};


//...........................................源代码中类的定义和方法的定义和实现部分


//类定义
struct CA {
      struct  swift_class *isa;
}

//类的方法函数的实现定义
void foo(){}
//类的extension的方法函数实现定义
void extfoo(){}

//类的描述信息构建,这些都是在编译代码时就明确了而且保存在数据段中。
//extension中定义的函数不会保存到虚函数表中。
struct swift_class classCA;
classCA.vtable[0] = {&foo};


//...........................................源代码中程序运行的部分

void main(){
  CA *obj =  CA.__allocating_init(classCA)
  obj->isa = &classCA;
  asm("mov x20, obj");
  //Swift中常规方法foo调用采用间接调用实现
  obj->isa->vtable[0]();
  //Swift中extension方法extfoo调用直接硬编码调用,而不是间接调用实现
  extfoo();
}

复制代码

【注】extension中是能够重写OC基类的方法,可是不能重写Swift类中的定义的方法。具体缘由根据上面的解释就很是清楚了。

四、成员变量的访问

OC成员变量的访问

@property 的本质就是ivar + getter + setter

咱们建立一个person类,里面建立一个属性

@property (nonatomic,copy) NSString *name;
复制代码

打印属性信息

unsigned int count;
objc_property_t *propertyList = class_copyPropertyList([Person class], &count);
for (unsigned int i = 0; i< count; i++)
	{
	const char *name = property_getName(propertyList[i]);
	NSLog(@"__%@",[NSString stringWithUTF8String:name]);
	objc_property_t property = propertyList[i];
	const char *a = property_getAttributes(property);
	NSLog(@"属性信息__%@",[NSString stringWithUTF8String:a]);
}
复制代码

属性信息中NSString咱们是知道了,可是T,C,N,V_name都是什么意思呢。咱们查看官方介绍

//T@"NSString",C,N,V_name
//T 类型
//C copy
//N nonatomic
//V 实例变量
复制代码

方法列表

u_int methodCount;
NSMutableArray *methodList = [NSMutableArray array];
Method *methods = class_copyMethodList([Person class], &methodCount);
for (int i = 0; i < methodCount; i++)
{
	SEL name = method_getName(methods[i]);
	NSString *strName = [NSString stringWithCString:sel_getName(name) encoding:NSUTF8StringEncoding];
	[methodList addObject:strName];
}
free(methods);

NSLog(@"方法列表:%@",methodList);
复制代码

咱们并无写name&setName:方法,因此这个getset方法应该就是runtime生成的。

property的实现

property的实现主要是利用runtime的两个方法

class_addProperty(Class _Nullable cls, const char * _Nonnull name,
const objc_property_attribute_t * _Nullable attributes,
unsigned int attributeCount)

void
class_addMethods(Class _Nullable, struct objc_method_list * _Nonnull)
复制代码

一、class_addProperty 生成属性

/** 
* Adds a property to a class.
* 
* @param cls 修改的类
* @param name 属性名字
* @param attributes 属性数组
* @param attributeCount 属性数组数量
* @return y 成功,n失败
*/
OBJC_EXPORT BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount)
 
复制代码

生成属性

objc_property_attribute_t type = { "T", "@\"NSString\"" };
objc_property_attribute_t ownership = { "C", "" }; // C = copy
objc_property_attribute_t nonatomic = { "N", "" }; //nonatomic
objc_property_attribute_t backingivar  = { "V", "_name" };//V 实例变量
objc_property_attribute_t attrs[] = { type, ownership,nonatomic, backingivar };
class_addProperty([self class], "name", attrs, 4); 
复制代码

二、class_addMethod 生成方法

NSString *nameGetter(id self, SEL _cmd) {
Ivar ivar = class_getInstanceVariable([self class], "_privateName");
return object_getIvar(self, ivar);
}
void nameSetter(id self, SEL _cmd, NSString *newName) {
Ivar ivar = class_getInstanceVariable([self class], "_privateName");
id oldName = object_getIvar(self, ivar);
if (oldName != newName) object_setIvar(self, ivar, [newName copy]);
}


//其中 “v@:” 表示返回值和参数

if(class_addMethod([self class],  NSSelectorFromString(@"name"), (IMP)nameGetter, "@@:"))
{
	NSLog(@"name get 方法添加成功");
}
else
{
	NSLog(@"name get 方法添加失败");
}

if(class_addMethod([self class], NSSelectorFromString(@"setName:"), (IMP)nameSetter, "v@:@"))
{
	NSLog(@"name set 方法添加成功");
}
else
{
    NSLog(@"name set 方法添加失败");
}

复制代码

Swift类中成员变量的访问

虽说OC类和Swift类的对象内存布局很是类似,每一个对象实例的开始部分都是一个isa数据成员指向类的描述信息,而类中定义的属性或者变量则通常会根据定义的顺序依次排列在isa的后面。OC类还会为全部成员变量,生成一张变量表信息,变量表的每一个条目记录着每一个成员变量在对象内存中的偏移量。这样在访问对象的属性时会经过偏移表中的偏移量来读取偏移信息,而后再根据偏移量来读取或设置对象的成员变量数据。在每一个OC类的get和set两个属性方法的实现中,对于属性在类中的偏移量值的获取都是经过硬编码来完成,也就是说是在编译连接时刻决定的。

对于Swift来讲,对成员变量的访问获得更加的简化。系统会对每一个成员变量生成get/set两个函数来实现成员变量的访问。系统不会再为类的成员变量生成变量偏移信息表,所以对于成员变量的访问就是直接在编译连接时肯定成员变量在对象的偏移位置,这个偏移位置是硬编码来肯定的。下面展现Swift源代码和C伪代码对数据成员访问的实现:

////////Swift源代码

class CA
{
   var a:Int = 10
   var b:Int = 20
}

void main()
{
    let obj = CA()
    obj.b = obj.a
}

复制代码
////////C伪代码

//...........................................运行时定义部分

//Swift类描述。
struct swift_class {
    ...   //其余的属性,由于这里不关心就不列出了
    //虚函数表恰好在结构体的第0x50的偏移位置。
    IMP vtable[4];
};


//...........................................源代码中类的定义和方法的定义和实现部分

//CA类的结构体定义也是CA类对象在内存中的布局。
struct CA
{
   struct swift_class *isa;
   long  reserve;   //这里的值目前老是2
   int a;
   int b;
};

//类CA的方法函数的实现。
int getA(){
    struct CA *obj = x20;   //取x20寄存器的值,也就是对象的值。
    return obj->a;
}
void setA(int a){
 struct CA *obj = x20;   //取x20寄存器的值,也就是对象的值。
 obj->a = a;
}
int getB(){
    struct CA *obj = x20;   //取x20寄存器的值,也就是对象的值。
    return obj->b;
}
void setB(int b){
 struct CA *obj = x20;   //取x20寄存器的值,也就是对象的值。
 obj->b = b;
}

struct swift_class classCA;
classCA.vtable[4] = {&getA,&setA,&getB, &setB};


//...........................................源代码中程序运行的部分

void main(){
   CA *obj =  CA.__allocating_init(classCA);
   obj->isa = &classCA;
   obj->reserve = 2;
   obj->a = 10;
   obj->b = 20;
   asm("mov x20, obj");
   obj->isa->vtable[3](obj->isa->vtable[0]());  // obj.b = obj.a的实现
}

复制代码

从上面的代码能够看出,Swift类会为每一个定义的成员变量都生成一对get/set方法并保存到虚函数表中。全部对对象成员变量的方法的代码都会转化为经过虚函数表来执行get/set相对应的方法。 下面是Swift类中成员变量的实现和内存结构布局图:

补充

Swift 类的方法以及全局函数

Swift类中定义的类方法和全局函数同样,由于不存在对象做为参数,所以在调用此类函数时也不会存在将对象保存到x20寄存器中这么一说。同时源代码中定义的函数的参数在编译时也不会插入附加的参数。Swift语言会对全部符号进行重命名修饰,类方法和全局函数也不例外。这也就使得全局函数和类方法也支持名称相同可是参数不一样的函数定义。简单的说就是类方法和全局函数就像C语言的普通函数同样被实现和定义,全部对类方法和全局函数的调用都是在编译连接时刻硬编码为函数地址调用来处理的

OC调用Swift类中的方法

若是应用程序是经过OC和Swift两种语言混合开发完成的。那就必定会存在着OC语言代码调用Swift语言代码以及相反调用的状况。对于Swift语言调用OC的代码的处理方法是系统会为工程创建一个桥声明头文件:项目工程名-Bridging-Header.h,全部Swift须要调用的OC语言方法都须要在这个头文件中声明。而对于OC语言调用Swift语言来讲,则有必定的限制。由于Swift和OC的函数调用ABI规则不相同,OC语言只能建立Swift中从NSObject类中派生类对象,而方法调用则只能调用原NSObject类以及派生类中的全部方法以及被声明为@objc关键字的Swift对象方法。若是须要在OC语言中调用Swift语言定义的类和方法,则须要在OC语言文件中添加:#import "项目名-Swift.h"。当某个Swift方法被声明为@objc关键字时,在编译时刻会生成两个函数,一个是本体函数供Swift内部调用,另一个是跳板函数(trampoline)是供OC语言进行调用的。这个跳板函数信息会记录在OC类的运行时类结构中,跳板函数的实现会对参数的传递规则进行转换:把x0寄存器的值赋值给x20寄存器,而后把其余参数依次转化为Swift的函数参数传递规则要求,最后再执行本地函数调用。整个过程的实现以下:

////////Swift源代码

//Swift类定义
class MyUIView:UIView {
  @objc    
  open func foo(){}
}

func main() {
  let obj = MyUIView()
  obj.foo()
}

//////// OC源代码
#import "工程-Swift.h"

void main() {
  MyUIView *obj = [MyUIView new];
  [obj foo];
}
复制代码
////////C伪代码

//...........................................运行时定义部分

//OC类的方法结构体
struct method_t {
    SEL name;
    IMP imp;
};

//Swift类描述
struct swift_class {
    ...   //其余的属性,由于这里不关心就不列出了。
    struct method_t  methods[1];
    ...   //其余的属性,由于这里不关心就不列出了。
    //虚函数表恰好在结构体的第0x50的偏移位置。
    IMP vtable[1];
};

//...........................................源代码中类的定义和方法的定义和实现部分

//类定义
struct MyUIView {
      struct swift_class *isa;
}

//类的方法函数的实现

//本体函数foo的实现
void foo(){}
//跳板函数的实现
void trampoline_foo(id self, SEL _cmd){
     asm("mov x20, x0");
     self->isa->vtable[0](); //这里调用本体函数foo
}

//类的描述信息构建,这些都是在编译代码时就明确了而且保存在数据段中。
struct swift_class classMyUIView;
classMyUIView.methods[0] = {"foo", &trampoline_foo};
classMyUIView.vtable[0] = {&foo};


//...........................................源代码中程序运行的部分

//Swift代码部分
void main()
{
  MyUIView *obj = MyUIView.__allocating_init(classMyUIView);
  obj->isa = &classMyUIView;
   asm("mov x20, obj");
   //Swift方法foo的调用采用间接调用实现。
   obj->isa->vtable[0]();
}

//OC代码部分
void main()
{
  MyUIView *obj = objc_msgSend(objc_msgSend(classMyUIView, "alloc"), "init");
  obj->isa = &classMyUIView;
  //OC语言对foo的调用仍是用objc_msgSend来执行调用。
  //由于objc_msgSend最终会找到methods中的方法结构并调用trampoline_foo 
  //而trampoline_foo内部则直接调用foo来实现真实的调用。
  objc_msgSend(obj, @selector(foo));
}

复制代码

下面的图形展现了Swift中带@objc关键字的方法实现,以及OC语言调用Swift对象方法的实现

文中关于Swift部分转载自欧阳大哥2013的Swift5.0的Runtime机制浅析,为了方便对比学习,我添加了OC RunTime的运行机制

相关文章
相关标签/搜索