该文章属于 < 简书 — 刘小壮 > 原创,转载请注明:
< 简书 — 刘小壮 > https://www.jianshu.com/p/3019605a4fc9
本文基于 objc-723
版本,在 Apple Github 和 Apple OpenSource 上有源码,但是需要自己编译。
重点来了~,可以到我的 Github
上下载编译好的源码,源码中已经写了大量的注释,方便读者研究。(如果觉得还不错,各位大佬麻烦点个Star
????)
Runtime Analyze
对象的初始化流程
在对象初始化的时候,一般都会调用 alloc+init
方法实例化,或者通过 new
方法进行实例化。下面将会分析通过 alloc+init
的方式实例化的过程,以下代码都是关键代码。
前面两步很简单,都是直接进行函数调用。
+ (id)alloc {return _objc_rootAlloc(self);
}
id _objc_rootAlloc(Class cls)
{return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
在创建对象的地方有两种方式,一种是通过 calloc
开辟内存,然后通过 initInstanceIsa
函数初始化这块内存。第二种是直接调用 class_createInstance
函数,由内部实现初始化逻辑。
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{if (fastpath(cls->canAllocFast())) {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 {id obj = class_createInstance(cls, 0);
if (slowpath(!obj)) return callBadAllocHandler(cls);
return obj;
}
}
但是在最新版的 objc-723
中,调用 canAllocFast
函数直接返回 false
,所以只会执行上面第二个else
代码块。
bool canAllocFast() {return false;}
初始化代码最终会调用到 _class_createInstanceFromZone
函数,这个函数是初始化的关键代码。下面代码中会进入 if
语句内,根据 instanceSize
函数返回的 size
,通过calloc
函数分配内存,并初始化 isa_t
指针。
id class_createInstance(Class cls, size_t extraBytes)
{return _class_createInstanceFromZone(cls, extraBytes, nil);
}
static __attribute__((always_inline))
id _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{bool hasCxxCtor = cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
size_t size = cls->instanceSize(extraBytes);
id obj;
if (!zone && fast) {obj = (id)calloc(1, size);
if (!obj) return nil;
obj->initInstanceIsa(cls, hasCxxDtor);
}
else {if (zone) {obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
} else {obj = (id)calloc(1, size);
}
if (!obj) return nil;
obj->initIsa(cls);
}
return obj;
}
在 instanceSize()
函数中,会通过 alignedInstanceSize
函数获取对象原始大小,在 class_ro_t
结构体中的 instanceSize
变量中定义。这个变量中存储了对象实例化时,所有变量所占的内存大小,这个大小是在编译器就已经决定的,不能在运行时进行动态改变。
获取到 instanceSize
后,对获取到的 size
进行地址对其。需要注意的是,CF
框架要求所有对象大小最少是 16 字节,如果不够则直接定义为 16 字节。
size_t instanceSize(size_t extraBytes) {size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
这也是很关键的一步,由于调用 initIsa
函数时,nonpointer
字段传入 true
,所以直接执行if
语句,设置 isa
的cls
为传入的 Class
。isa
是objc_object
的结构体成员变量,也就是 isa_t
的类型。
inline void objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{initIsa(cls, true, hasCxxDtor);
}
inline void objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{if (!nonpointer) {isa.cls = cls;} else {isa_t newisa(0);
newisa.bits = ISA_MAGIC_VALUE;
newisa.has_cxx_dtor = hasCxxDtor;
newisa.shiftcls = (uintptr_t)cls >> 3;
isa = newisa;
}
}
通过 new
函数创建对象其实是一样的,内部通过 callAlloc
函数执行创建操作,如果调用 alloc
方法的话也是调用的 callAlloc
函数。所以调用 new
函数初始化对象时,可以等同于 alloc+init
的调用。
+ (id)new {return [callAlloc(self, false/*checkNil*/) init];
}
在 runtime 源码中,执行 init 操作本质上就是直接把 self 返回。
- (id)init {return _objc_rootInit(self);
}
id _objc_rootInit(id obj)
{return obj;}
dealloc
在对象销毁时,运行时环境会调用 NSObject
的dealloc
方法执行销毁代码,并不需要我们手动去调用。接着会调用到 Runtime
内部的 objc_object::rootDealloc
(C++ 命名空间) 函数。
在 rootDealloc
函数中会执行一些释放前的操作,例如将对象所有的引用指向 nil
,并且调用free
函数释放内存空间等。
下面的 if-else
语句中有判断条件,如果是 ARC
环境,并且当前对象定义了实例变量,才会进入 else
中执行 object_dispose
函数,否则进入上面的 if
语句。上面的 if
语句表示当前对象没有实例变量,则直接将当前对象free
。
inline void
objc_object::rootDealloc()
{if (isTaggedPointer()) return; // fixme necessary?
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc))
{assert(!sidetable_present());
free(this);
}
else {object_dispose((id)this);
}
}
在 object_dispose
函数中,主要是通过 objc_destructInstance
函数实现的。在函数内部主要做了三件事:
- 对当前对象进行析构,会调用析构函数
.cxx_destruct
函数,在函数内部还会进行对应的release
操作。 - 移除当前对象的所有关联关系。
- 进行最后的
clear
操作。
// dealloc 方法的核心实现,内部会做判断和析构操作
void *objc_destructInstance(id obj)
{if (obj) {
// 判断是否有 OC 或 C ++ 的析构函数
bool cxx = obj->hasCxxDtor();
// 对象是否有相关联的引用
bool assoc = obj->hasAssociatedObjects();
// 对当前对象进行析构
if (cxx) object_cxxDestruct(obj);
// 移除所有对象的关联,例如把 weak 指针置 nil
if (assoc) _object_remove_assocations(obj);
obj->clearDeallocating();}
return obj;
}
上面的函数中会调用 object_cxxDestruct
函数进行析构,而函数内部是通过 object_cxxDestructFromClass
函数实现的。
函数内部会从当前对象所属的类开始遍历,一直遍历到根类位置。在遍历的过程中,会不断执行 .cxx_destruct
函数,对传入的对象进行析构。
因为在继承者链中,每个类都会有自己的析构代码,所以需要将当前对象传入,并逐个执行析构操作,将对象的所有析构操作都执行完成才可以。
// 调用 C ++ 的析构函数
static void object_cxxDestructFromClass(id obj, Class cls)
{void (*dtor)(id);
// 从当前类开始遍历,直到遍历到根类
for (; cls; cls = cls->superclass) {if (!cls->hasCxxDtor()) return;
// SEL_cxx_destruct 就是.cxx_destruct 的 selector
dtor = (void(*)(id))
lookupMethodInClassAndLoadCache(cls, SEL_cxx_destruct);
if (dtor != (void(*)(id))_objc_msgForward_impcache) {
// 获取到.cxx_destruct 的函数指针并调用
(*dtor)(obj);
}
}
}
在对象被执行 .cxx_destruct
析构函数后,析构函数内部还会调用一次 release
函数,完成最后的释放操作。
addMethod 实现
在项目中经常会动态对方法列表进行操作,例如动态添加或替换一个方法,这时候会用到下面两个 Runtime
函数。在下面两个函数中,本质上都是通过 addMethod
函数实现的,在 class_addMethod
中对返回值进行了一个取反,所以如果此函数返回 NO
则表示方法已存在,不要重复添加。
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
{if (!cls) return NO;
rwlock_writer_t lock(runtimeLock);
return ! addMethod(cls, name, imp, types ?: "", NO);
}
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
{if (!cls) return nil;
rwlock_writer_t lock(runtimeLock);
return addMethod(cls, name, imp, types ?: "", YES);
}
下面我们就分析一下 addMethod
函数的实现,依然只保留核心源码。
在 addMethod
函数中会先判断需要添加的方法是否存在,如果已经存在则直接返回对应的 IMP
,否则就动态添加一个方法。在class_addMethod
函数中有一个 replace
字段,表示区别是否 class_replaceMethod
函数调用过来的。如果 replace
是NO
则直接返回 IMP
,如果是YES
则替换方法原有实现。
如果添加的方法不存在,则创建一个 method_list_t
结构体指针,并设置三个基本参数 name
、types
、imp
,然后通过attachLists
函数将新创建的 method_list_t
结构体添加到方法列表中。
static IMP
addMethod(Class cls, SEL name, IMP imp, const char *types, bool replace)
{
IMP result = nil;
method_t *m;
if ((m = getMethodNoSuper_nolock(cls, name))) {
// already exists
if (!replace) {result = m->imp;} else {result = _method_setImplementation(cls, m, imp);
}
} else {
// fixme optimize
method_list_t *newlist;
newlist = (method_list_t *)calloc(sizeof(*newlist), 1);
newlist->entsizeAndFlags =
(uint32_t)sizeof(method_t) | fixed_up_method_list;
newlist->count = 1;
newlist->first.name = name;
newlist->first.types = strdupIfMutable(types);
newlist->first.imp = imp;
prepareMethodLists(cls, &newlist, 1, NO, NO);
cls->data()->methods.attachLists(&newlist, 1);
flushCaches(cls);
result = nil;
}
return result;
}
在 attachLists
函数中实现比较简单,通过对原有地址做位移,并将新创建的 method_list_t
结构体 copy
到方法列表中。
void attachLists(List* const * addedLists, uint32_t addedCount) {
// ...
memmove(array()->lists + addedCount, array()->lists, oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0]));
// ...
}
添加 Ivar
在 Runtime
中可以通过 class_addIvar
函数,向一个类添加实例对象。但是需要注意的是,这个函数不能向一个已经存在的类添加实例变量,只能想通过 Runtime API
创建的类动态添加实例变量。
函数应该在调用 objc_allocateClassPair
函数创建类之后,以及调用 objc_registerClassPair
函数注册的类之间添加实例变量,否则就会失败。也不能向一个元类添加实例变量,只能想类添加实例变量。
下面是动态创建一个类,并向新创建的类添加实例变量的代码。
Class testClass = objc_allocateClassPair([NSObject class], "TestObject", 0);
BOOL isAdded = class_addIvar(testClass, "password", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));
objc_registerClassPair(testClass);
if (isAdded) {id object = [[testClass alloc] init];
[object setValue:@"lxz" forKey:@"password"];
}
那么,为什么需要把动态添加实例变量的代码放在这两个函数中间呢?让我们一起来探究一下吧。
首先通过 objc_allocateClassPair
函数来创建类,创建时通过 getClass
函数判断类名是否已用,然后通过 verifySuperclass
函数判断 superclass
是否合适,如果任意条件不符合则创建类失败。
下面通过 alloc_class_for_subclass
函数创建类和元类,在 alloc
函数内部本质上是通过 calloc
函数分配内存空间,没有做其他操作。然后就执行 objc_initializeClassPair_internal
函数,initialize
函数内部都是初始化操作,用来初始化刚刚创建的 Class
和metaClass
。
Class objc_allocateClassPair(Class superclass, const char *name,
size_t extraBytes)
{
Class cls, meta;
if (getClass(name) || !verifySuperclass(superclass, true/*rootOK*/)) {return nil;}
cls = alloc_class_for_subclass(superclass, extraBytes);
meta = alloc_class_for_subclass(superclass, extraBytes);
objc_initializeClassPair_internal(superclass, name, cls, meta);
return cls;
}
这就是 initialize
函数内部的实现,都是各种初始化代码,没有做其他逻辑操作。至此,类的初始化完成,可以在外面通过 class_addIvar
函数添加实例变量了。
static void objc_initializeClassPair_internal(Class superclass, const char *name, Class cls, Class meta)
{
class_ro_t *cls_ro_w, *meta_ro_w;
cls->setData((class_rw_t *)calloc(sizeof(class_rw_t), 1));
meta->setData((class_rw_t *)calloc(sizeof(class_rw_t), 1));
cls_ro_w = (class_ro_t *)calloc(sizeof(class_ro_t), 1);
meta_ro_w = (class_ro_t *)calloc(sizeof(class_ro_t), 1);
cls->data()->ro = cls_ro_w;
meta->data()->ro = meta_ro_w;
// Set basic info
cls->data()->flags = RW_CONSTRUCTING | RW_COPIED_RO | RW_REALIZED | RW_REALIZING;
meta->data()->flags = RW_CONSTRUCTING | RW_COPIED_RO | RW_REALIZED | RW_REALIZING;
cls->data()->version = 0;
meta->data()->version = 7;
// ....
}
在创建类之后,会通过 objc_registerClassPair
函数注册新类。和创建新类一样,注册新类也分为注册类和注册元类。通过下面的 addNonMetaClass
函数注册元类,通过直接调用 NXMapInsert
函数注册类。
void objc_registerClassPair(Class cls)
{cls->ISA()->changeInfo(RW_CONSTRUCTED, RW_CONSTRUCTING | RW_REALIZING);
cls->changeInfo(RW_CONSTRUCTED, RW_CONSTRUCTING | RW_REALIZING);
addNamedClass(cls, cls->data()->ro->name);
}
static void addNamedClass(Class cls, const char *name, Class replacing = nil)
{
Class old;
if ((old = getClass(name)) && old != replacing) {inform_duplicate(name, old, cls);
addNonMetaClass(cls);
} else {NXMapInsert(gdb_objc_realized_classes, name, cls);
}
}
无论是注册类还是注册元类,内部都是通过 NXMapInsert
函数实现的。在 Runtime
中,所有类都是存在一个哈希表中的,在 table
的buckets
中存储。每次新创建类之后,都需要把该类加入到哈希表中,下面是向哈希表插入的逻辑。
void *NXMapInsert(NXMapTable *table, const void *key, const void *value) {MapPair *pairs = (MapPair *)table->buckets;
// 计算 key 在当前 hash 表中的下标,hash 下标不一定是最后
unsigned index = bucketOf(table, key);
// 找到 buckets 的首地址,并通过 index 下标计算对应位置,获取到 index 对应的 MapPair
MapPair *pair = pairs + index;
// 如果 key 为空,则返回
if (key == NX_MAPNOTAKEY) {_objc_inform("*** NXMapInsert: invalid key: -1\n");
return NULL;
}
unsigned numBuckets = table->nbBucketsMinusOne + 1;
// 如果当前地址未冲突,则直接对 pair 赋值
if (pair->key == NX_MAPNOTAKEY) {
pair->key = key; pair->value = value;
table->count++;
if (table->count * 4 > numBuckets * 3) _NXMapRehash(table);
return NULL;
}
/* 到这一步,则表示 hash 表冲突了 */
// 如果同名,则将旧类换为新类
if (isEqual(table, pair->key, key)) {
const void *old = pair->value;
if (old != value) pair->value = value;
return (void *)old;
// hash 表满了,对 hash 表做重哈希,然后再次执行这个函数
} else if (table->count == numBuckets) {
/* no room: rehash and retry */
_NXMapRehash(table);
return NXMapInsert(table, key, value);
// hash 表冲突了
} else {
unsigned index2 = index;
// 解决 hash 表冲突,这里采用的是线性探测法,解决哈希表冲突
while ((index2 = nextIndex(table, index2)) != index) {
pair = pairs + index2;
if (pair->key == NX_MAPNOTAKEY) {
pair->key = key; pair->value = value;
table->count++;
// 在查找过程中,发现哈希表不够用了,则进行重哈希
if (table->count * 4 > numBuckets * 3) _NXMapRehash(table);
return NULL;
}
// 找到同名类,则用新类替换旧类,并返回
if (isEqual(table, pair->key, key)) {
const void *old = pair->value;
if (old != value) pair->value = value;
return (void *)old;
}
}
return NULL;
}
}
思考
那为什么只能向运行时动态创建的类添加
ivars
,不能向已经存在的类添加ivars
呢?
这是因为在编译时只读结构体 class_ro_t
就会被确定,在运行时是不可更改的。ro
结构体中有一个字段是 instanceSize
,表示当前类在创建对象时需要多少空间,后面的创建都根据这个size
分配类的内存。
如果对一个已经存在的类增加一个参数,改变了 ivars
的结构,这样在访问改变之前创建的对象时,就会出现问题。
以上图为例,在项目中创建 TestObject
类,并且添加三个成员变量,其 ivars
的内存结构占用 20 字节。如果在运行时动态添加一个 bool
型参数,之后创建的对象 ivars
都占用 21 字节。
在通过 ivars
结构体访问之前创建的对象时,因为之前创建的对象没有 sex
,所以还是按照 20 字节分配的内存空间,这时候访问sex
就会导致地址越界。
数据访问
定义对象时都会给其设置类型,类型本质上并不是一个对象,而是用来标示当前对象所占空间的。以 C 语言为例,访问对象都是通过地址做访问的,而类型就是从首地址开始读取多少位是当前对象。
int number = 18;
char text = 'i';
以上面代码为例,定义了一个 int
类型的 number
,占用四字节,定义一个char
类型的 text
变量,占用一字节。在内存中访问对象时,就是根据指针地址找到对应的内存区,然后按照指针类型取多少范围的内存,就完成对象的读取操作。
而在面向对象语言中,函数或方法的命名规则还需要保留在运行期。以 C++
为例,C++
中有一个概念叫做“函数重载”,函数重载指的是允许有一组相同函数名,但参数列表类型不同的函数。
原函数:void print(char c)
重载结果:_ZN4test5printEc
C++
函数重载是有一定规则的,例如上面就是对 print
函数重载后的结果,重载结果才是运行时真正执行的函数。函数重载发生在编译期,会包含namespace
、class name
、function name
、返回值、参数等部分,根据这些部分重新生成函数名。
在 OC 中其实也存在函数重载的概念,只不过 OC 并不是直接对原有方法名做修改,而是增加对返回值和参数按照一定规则进行编码,然后放在 method_t
结构体中。
method_t
结构体存储着方法的信息,其中 types
字段就是返回值和参数的编码。编码后的字符串类似于"iv@:d"
,完整的编码规则可以查看官方文档。
下面就是 Method
的定义,主要包含了三个关键信息。
struct method_t {
SEL name;
const char *types;
IMP imp;
};
Protocol
我们在项目中经常使用协议,那协议又是怎么实现的呢?
根据 Runtime
源码可以看出,协议都是 protocol_t
结构体的对象,而 protocol_t
结构体是继承自 objc_object
的,所以具备对象的特征。
除了 objc_object
中定义的一些结构体参数外,protocol_t
中还定义了一些独有的参数,例如常用的 name
、method list
、property list
、size
等。所以可以看出,一个协议中可以声明对象方法、类方法,以及对象属性和类属性。
struct protocol_t : objc_object {
const char *mangledName;
struct protocol_list_t *protocols;
method_list_t *instanceMethods;
method_list_t *classMethods;
method_list_t *optionalInstanceMethods;
method_list_t *optionalClassMethods;
property_list_t *instanceProperties;
uint32_t size; // sizeof(protocol_t)
uint32_t flags;
// Fields below this point are not always present on disk.
const char **_extendedMethodTypes;
const char *_demangledName;
property_list_t *_classProperties;
};
既然具备了对象的特征,那也是有 isa 指针的。在 Protocol 中所有的 isa 都指向同一个类 Protocol。 在 Protocol
类中没有做太复杂的处理,只是实现了一些基础的方法。
@implementation Protocol
+ (void) load {
}
- (BOOL) conformsTo: (Protocol *)aProtocolObj {return protocol_conformsToProtocol(self, aProtocolObj);
}
- (struct objc_method_description *) descriptionForInstanceMethod:(SEL)aSel {return method_getDescription(protocol_getMethod((struct protocol_t *)self,
aSel, YES, YES, YES));
}
- (struct objc_method_description *) descriptionForClassMethod:(SEL)aSel {return method_getDescription(protocol_getMethod((struct protocol_t *)self,
aSel, YES, NO, YES));
}
- (const char *)name {return protocol_getName(self);
}
// Protocol 重写了 isEqual 方法,内部不断查找其父类,判断是否 Protocol 的子类。- (BOOL)isEqual:other {
Class cls;
Class protoClass = objc_getClass("Protocol");
for (cls = object_getClass(other); cls; cls = cls->superclass) {if (cls == protoClass) break;
}
if (!cls) return NO;
// check equality
return protocol_isEqual(self, other);
}
- (NSUInteger)hash {return 23;}
@end
协议的初始化也是在 _read_images
函数中完成的,初始化过程主要是一个遍历。逻辑就是获取 Protocol list
,然后遍历这个数组,并调用readProtocol
函数进行初始化操作。
// 遍历所有协议列表,并且将协议列表加载到 Protocol 的哈希表中
for (EACH_HEADER) {
extern objc_class OBJC_CLASS_$_Protocol;
// cls = Protocol 类,所有协议和对象的结构体都类似,isa 都对应 Protocol 类
Class cls = (Class)&OBJC_CLASS_$_Protocol;
assert(cls);
// 获取 protocol 哈希表
NXMapTable *protocol_map = protocols();
bool isPreoptimized = hi->isPreoptimized();
bool isBundle = hi->isBundle();
// 从编译器中读取并初始化 Protocol
protocol_t **protolist = _getObjc2ProtocolList(hi, &count);
for (i = 0; i < count; i++) {readProtocol(protolist[i], cls, protocol_map,
isPreoptimized, isBundle);
}
}
在 readProtocol
函数中,会根据传入的协议进行初始化操作。在传入参数中,protocol_class
就是 Protocol
类,所有的协议类的 isa
都指向这个类。
根据 Protocol
的源码可以看出,其对象模型是比较简单的,和 Class
的对象模型还不太一样。Protocol
的对象模型只有从 Protocol list
中加载的对象和 isa
指向的 Protocol
类构成,没有其他的实例化过程,Protocol
类并没有元类。
// 初始化传入的所有 Protocol,如果哈希表中已经存在初始化的 Protocol,则不做任何处理
static void
readProtocol(protocol_t *newproto, Class protocol_class,
NXMapTable *protocol_map,
bool headerIsPreoptimized, bool headerIsBundle)
{
auto insertFn = headerIsBundle ? NXMapKeyCopyingInsert : NXMapInsert;
// 根据名字获得对应的 Protocol 对象
protocol_t *oldproto = (protocol_t *)getProtocol(newproto->mangledName);
// 如果 Protocol 不为 NULL,表示已经存在相同的 Protocol,则不做任何处理,进入下面 if 语句。if (oldproto) {// nothing}
// 如果 Protocol 为 NULL,则对其进行简单的初始化,并将 Protocol 的 isa 设置为 Protocol 类
else if (headerIsPreoptimized) {protocol_t *cacheproto = (protocol_t *)
getPreoptimizedProtocol(newproto->mangledName);
protocol_t *installedproto;
if (cacheproto && cacheproto != newproto) {installedproto = cacheproto;}
else {installedproto = newproto;}
// 哈希表插入函数的指针
insertFn(protocol_map, installedproto->mangledName,
installedproto);
}
// 下面两个 else 都是初始化 protocol_t 的过程
else if (newproto->size >= sizeof(protocol_t)) {newproto->initIsa(protocol_class);
insertFn(protocol_map, newproto->mangledName, newproto);
}
else {size_t size = max(sizeof(protocol_t), (size_t)newproto->size);
protocol_t *installedproto = (protocol_t *)calloc(size, 1);
memcpy(installedproto, newproto, newproto->size);
installedproto->size = (__typeof__(installedproto->size))size;
installedproto->initIsa(protocol_class);
insertFn(protocol_map, installedproto->mangledName, installedproto);
}
}
Protocol 是可以在运行时动态创建添加的,和创建 Class 的过程类似,分为创建和注册两部分。 创建 Protocol
之后,Protocol
处于一个未完成的状态,只有注册后才是可以使用的Protocol
。
// 创建新的 Protocol,创建后还需要调用下面的 register 方法
Protocol *
objc_allocateProtocol(const char *name)
{if (getProtocol(name)) {return nil;}
protocol_t *result = (protocol_t *)calloc(sizeof(protocol_t), 1);
// 下面的 cls 是__IncompleteProtocol 类,表示是未完成的 Protocol
extern objc_class OBJC_CLASS_$___IncompleteProtocol;
Class cls = (Class)&OBJC_CLASS_$___IncompleteProtocol;
result->initProtocolIsa(cls);
result->size = sizeof(protocol_t);
result->mangledName = strdupIfMutable(name);
return (Protocol *)result;
}
注册Protocol
。
// 向 protocol 的哈希表中,注册新创建的 Protocol 对象
void objc_registerProtocol(Protocol *proto_gen)
{protocol_t *proto = newprotocol(proto_gen);
extern objc_class OBJC_CLASS_$___IncompleteProtocol;
Class oldcls = (Class)&OBJC_CLASS_$___IncompleteProtocol;
extern objc_class OBJC_CLASS_$_Protocol;
Class cls = (Class)&OBJC_CLASS_$_Protocol;
// 如果已经被注册到哈希表中,则直接返回
if (proto->ISA() == cls) {return;}
// 如果当前 protocol 的 isa 不是__IncompleteProtocol,表示这个 protocol 是有问题的,则返回
if (proto->ISA() != oldcls) {return;}
proto->changeIsa(cls);
NXMapKeyCopyingInsert(protocols(), proto->mangledName, proto);
}
SEL
之前 SEL
是由 objc_selector
结构体实现的,但是从现在的源码来看,SEL
是一个 const char*
的常量字符串,只是代表一个名字而已。
typedef struct objc_selector *SEL;
为什么说
SEL
只是一个常量字符串呢?我们在Runtime
源码中探究一下。
这是在 _read_images
函数中 SEL list
的实现,主要逻辑是加载 SEL list
到内存中,然后通过 sel_registerNameNoLock
函数,将所有 SEL
都注册到属于 SEL
的哈希表中。
但是我们从这段代码中可以看出,大部分的 SEL
和const char*
的转换,都是直接进行强制类型转换的,所以二者是同一块内存。
// 将所有 SEL 都注册到哈希表中,是另外一张哈希表
static size_t UnfixedSelectors;
sel_lock();
for (EACH_HEADER) {if (hi->isPreoptimized()) continue;
bool isBundle = hi->isBundle();
// 取出的是字符串数组,例如首地址是 "class"
SEL *sels = _getObjc2SelectorRefs(hi, &count);
UnfixedSelectors += count;
for (i = 0; i < count; i++) {
// sel_cname 函数内部就是将 SEL 强转为常量字符串
const char *name = sel_cname(sels[i]);
// 注册 SEL 的操作
sels[i] = sel_registerNameNoLock(name, isBundle);
}
}
再进入 sel_registerNameNoLock
函数中可以看出,SEL
的哈希表也是将字符串注册到哈希表中,并不是之前的 objc_selector
结构体,所以可以看出现在 SEL
就是单纯的 const char*
常量字符串。
static SEL sel_alloc(const char *name, bool copy)
{return (SEL)(copy ? strdupIfMutable(name) : name);
}
对等交换协议
研究 Apple
的源码时,还可以通过 GNUStep
研究,GNUStep
是苹果的一套对等交换源码,将 OC 代码以重新实现了一遍,内部实现大致和苹果的类似。
GNUStep
简书由于排版的问题,阅读体验并不好,布局、图片显示、代码等很多问题。所以建议到我 Github
上,下载 Runtime PDF
合集。把所有 Runtime
文章总计九篇,都写在这个 PDF
中,而且左侧有目录,方便阅读。
下载地址:Runtime PDF
麻烦各位大佬点个赞,谢谢!????