面试题——如何动态创建一个类
通过前面文章的介绍,我们已经对rw和ro之间的关系有过了解了,本篇文章首先通过一个面试题来加深下诸位对ro和rw的理解,也进一步熟悉下Runtime的API。
题目是:如何动态创建一个类?
第一步,动态创建类对象
Class LVPerson = objc_allocateClassPair([NSObject class], "LVPerson", 0);
我们使用objc_allocateClassPair函数来动态创建一个类对象,该函数需要传三个参数:
第一个参数是父类对象,如果传nil那么新创建的类就是跟NSObject同等级别的根类对象。
第二个参数是本类类名。
第三个参数是初始的内存空间大小。
我们如果使用objc_allocateClassPair函数来创建一个类对象失败了,那么objc_allocateClassPair就会返回Nil。如果所要创建的类已经存在了,那么就会返回Nil。
实际上,我们在Xcode中创建的.h、.m这样的GUN文件,其目的就是为了Xcode能够识别,其最终底层都还是会被转换成我上面所写的那些运行时代码的。
第二步,添加成员变量
class_addIvar(LVPerson, "lvName", sizeof(NSString *), log2(sizeof(NSString *)), "@");
我们是使用class_addIvar函数来给类对象添加成员变量。
该函数定义如下:
class_addIvar(<#Class _Nullable __unsafe_unretained cls#>, <#const char * _Nonnull name#>, <#size_t size#>, <#uint8_t alignment#>, <#const char * _Nullable types#>)
我们可以看到,该函数有五个参数。
第一个参数cls是类对象,它表示是往哪个类添加成员变量。需要注意的是,这个cls不能是元类对象,因为我们不支持在元类中添加实例变量。
第二个参数name是成员变量的名字。
第三个参数size是成员变量的类型的大小。
第四个参数alignment是对齐处理方式,即二进制对齐位数,对于所有指针类型的变量,都是取成员变量类型大小以2为底的对数。比如8=2^3,因此这里就应该赋值3。
第五个参数types是签名。
第三步,注册到内存
objc_registerClassPair(LVPerson);
objc_registerClassPair函数是往内存中去注册类对象
现在我们考虑一个问题,class_addIvar函数能否在objc_registerClassPair函数之后调用?答案是不能!为什么呢?
objc_registerClassPair函数是将创建的类对象加载到内存,加载完成之后,本类中的ro就已经确定了,我们知道,ro是只读的,它在确定之后就不可以动态增加内容了,如果我们想在运行时增加一些内容,只能是往rw中去增加。接下来我们看一看ro和rw的结构。
rw的数据结构:
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;
#if SUPPORT_INDEXED_ISA
uint32_t index;
#endif
......
};
ro的数据结构:
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
const uint8_t * ivarLayout;
const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
method_list_t *baseMethods() const {
return baseMethodList;
}
};
通过比较上面?ro和rw的数据结构我们发现,存储成员变量的数组ivars只在ro中有,rw中是没有ivars的。因此,成员变量在ro初始化了之后就不能再继续动态新增了。所以,必须在ro初始化之前(即在调用objc_registerClassPair函数之前)完成成员变量的定义。
第四步,添加属性
好,现在我们知道了,在类注册完成之后,不可以继续添加成员变量了。那么我们现在再思考一个点,在类注册完成之后,是否可以添加属性呢?答案是可以的。
我们可以翻到上面再看一下rw的结构,可以看到是有methods、properties和protocols三个变量的,所以,在类注册完成之后,可以继续添加方法、属性和协议。
首先来看一下添加属性的代码:
// 封装的添加属性的方法
void lv_class_addProperty(Class targetClass , const char *propertyName) {
objc_property_attribute_t type = { "T", [[NSString stringWithFormat:@"@\"%@\"",NSStringFromClass([NSString class])] UTF8String] }; //type
objc_property_attribute_t ownership0 = { "C", "" }; // C = copy
objc_property_attribute_t ownership = { "N", "" }; //N = nonatomic
objc_property_attribute_t backingivar = { "V", [NSString stringWithFormat:@"_%@",[NSString stringWithCString:propertyName encoding:NSUTF8StringEncoding]].UTF8String }; //variable name
objc_property_attribute_t attrs[] = {type, ownership0, ownership,backingivar};
class_addProperty(targetClass, propertyName, attrs, 4);
}
// 添加属性
lv_class_addProperty(LVPerson, "subject");
我们看到,添加属性在对底层是通过class_addProperty函数实现的,该函数有四个参数:
第一个参数是给哪个类添加属性
第二个参数是属性名
第三个参数是所添加的属性的一些属性,比如所属类、读写性、原子性、内存管理策略等
对应关系如下:
第四个参数是属性的属性的数量。
第五步,添加方法
上面第四步已经添加了属性了,我们接下来使用一下:
id person = [LVPerson alloc];
[person setValue:@"Lavie" forKey:@"lvName"];
NSLog(@"%@",[person valueForKey:@"lvName"]);
[person setValue:@"master" forKey:@"subject"];
NSLog(@"%@",[person valueForKey:@"subject"]);
此时,运行程序后会报错。这是因为我只添加了属性,并没有给属性添加对应的setter和getter。
接下来我给属性添加设置器和访问器:
// 添加setter + getter 方法
class_addMethod(LVPerson, @selector(setSubject:), (IMP)lgSetter, "v@:@");
class_addMethod(LVPerson, @selector(subject), (IMP)lvName, "@@:");
// setter
void lgSetter(NSString *value){
printf("%s/n",__func__);
}
// getter
NSString *lvName(){
printf("%s/n",__func__);
return @"master NB";
}
我们看到,是通过class_addMethod函数来添加方法的,它有四个参数:
第一个参数是在哪个类中添加方法
第二个参数是所添加方法的编号SEL
第三个参数是所添加方法的函数实现的指针IMP
第四个参数是所添加方法的签名。
懒加载类的加载
在上篇文章类的加载(一)中,我们聊到了非懒加载类的加载。当时我就有个疑问,什么是非懒加载类?是不是还有个懒加载类?二者的区别是什么?接下来我们就来区分一下懒加载类和非懒加载类。
懒加载类 VS 非懒加载类
其实区分是否为懒加载类的标准很简单,就是看是否实现了+load方法:
实际上,类默认都是懒加载类,只有实现了+load方法才是非懒加载类。如果所有的类都是非懒加载类的话,势必会增大编译期的压力,因为此时每一个类都需要在编译期进行初始化。所以,除了那些手动覆写了+load方法的类之外,其余的类基本都是懒加载类,也就是说,这些懒加载类会在使用的时候才会真正去实现。
懒加载类的加载
我们上面提到,所有的懒加载类都是在使用到的时候去实现的,那么使用的场景有哪些呢?
第一个使用场景就是被其他的子类继承。
我在上篇文章类的加载(一)中分析过,realizeClassWithoutSwift函数中会递归调用realizeClassWithoutSwift函数来实现其父类以及元类的实现。
也就是说,当一个类在实现的时候,会连带着它的父类和元类都实现。
第二个使用场景是第一次调用了某个方法。
通过之前的文章方法的查找流程——慢速查找我们知道,会通过lookUpImpOrForward函数来进行方法的慢速查找流程:
在lookUpImpOrForward函数里面会有一个该类是否实现的判断(上图红框内),如果没有实现,那么就调用realizeClassMaybeSwiftAndLeaveLocked函数进行实现:
static Class
realizeClassMaybeSwiftAndLeaveLocked(Class cls, mutex_t& lock)
{
return realizeClassMaybeSwiftMaybeRelock(cls, lock, true);
}
然后我们再点击realizeClassMaybeSwiftMaybeRelock函数:
static Class
realizeClassMaybeSwiftMaybeRelock(Class cls, mutex_t& lock, bool leaveLocked)
{
lock.assertLocked();
if (!cls->isSwiftStable_ButAllowLegacyForNow()) {
// Non-Swift class. Realize it now with the lock still held.
// fixme wrong in the future for objc subclasses of swift classes
realizeClassWithoutSwift(cls);
if (!leaveLocked) lock.unlock();
} else {
// Swift class. We need to drop locks and call the Swift
// runtime to initialize it.
lock.unlock();
cls = realizeSwiftClass(cls);
assert(cls->isRealized()); // callback must have provoked realization
if (leaveLocked) lock.lock();
}
return cls;
}
我们发现,对于OC类和Swift类,有不同的实现方式。对于OC类而言,会调用realizeClassWithoutSwift函数进行实现。对于realizeClassWithoutSwift函数我们已经很熟悉了,在类的加载(一)中我们介绍过,realizeClassWithoutSwift函数就是进行非Swift类的实现的。
分类的加载
分类的数据结构
给Norman类新建一个名为configure的分类:
然后进入Norman+configure.m所在文件目录下,终端执行如下命令:
clang -rewrite-objc Norman+configure.m
此时该路径下会多出一个cpp文件,如下:
我们大概该cpp文件,查看C++源码:
这说明我们创建的分类在底层是_category_t结构体:
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;
};
在类的加载(一)中我们了解到,动态往rw中添加内容是通过attachLists函数实现的,实际上,这里的分类中的内容也是通过attachLists函数来添加到rw中的。
并且,实例方法和类方法需要分开处理,因为他们需要贴(attach)到不同的类对象中。instance_methods需要贴到类对象中,class_methods需要贴到元类对象中。
懒加载类 + 非懒加载的分类
我们知道,在编译阶段,动态链接器获取到mach-o中的镜像内容完成之后,会调用_read_images函数来读取镜像内容,我们在上篇文章中已经对该内容有过介绍,但是没有介绍类目信息的读取,接下来我们看一下_read_images函数中对类目信息的读取源码:
我们看到,对于那些非懒加载的分类(分类中有实现+load方法),会在_read_images函数中将其内容加载到缓存表。加载到缓存表之后,会判断所在类有没有实现,如果已经实现了,那么就会接着将刚才插入到缓存表里面的分类数据实现到类中;如果类没有实现,那么就跳过,等到后面去实现。
我们再看一下类的实现,realizeClassWithoutSwift函数。其实在编译链接期,realizeClassWithoutSwift函数也有被调用,只不过此时是用于非懒加载类的实现,对于懒加载类,它根本就不会调用。而在编译完成之后使用到该类的时候,会再次调用realizeClassWithoutSwift函数,此时会将刚才插入到缓存表中的类目信息(通过unattachedCategoriesForClass函数获取到)实现到类中。
因此,对于【懒加载类 + 非懒加载分类】的场景,其分类信息的实现如下:
非懒加载类 + 非懒加载的分类
原类和分类里面都实现了+load方法。
在_read_images中,会通过realizeClassWithoutSwift函数来实现类,此时分类信息还没有从Mach-O里面读取出来并加载到缓存表,因此分类信息此时尚未实现到类里面
在_read_images中,通过_getObjc2CategoryList和addUnattachedCategoryForClass将分类信息从Mach-O中读取出来插入到分类缓存表中
此时类已经实现了,因此可以调用remethodizeClass函数将上一步缓存到分类缓存表中的分类信息贴到类里面:
static void remethodizeClass(Class cls)
{
category_list *cats;
bool isMeta;
runtimeLock.assertLocked();
isMeta = cls->isMetaClass();
// Re-methodizing: check for more categories
if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
if (PrintConnecting) {
_objc_inform("CLASS: attaching categories to class '%s' %s",
cls->nameForLogging(), isMeta ? "(meta)" : "");
}
attachCategories(cls, cats, true /*flush caches*/);
free(cats);
}
}
懒加载类 + 懒加载的分类
原类和分类中都没有实现+load方法。
如果分类中没有实现load方法,那么这个分类就是懒加载分类,该分类中的信息都会在编译期直接编译进ro,我们直接在ro中获取就可以了。
在第一次使用到类的时候会调到lookUpImpOrForward函数中的realizeClassMaybeSwiftAndLeaveLocked,进而调用realizeClassWithoutSwift函数,realizeClassWithoutSwift会调到methodizeClass。
初始化完成之后,ro中就已经存在懒加载的分类方法了。
非懒加载类 + 懒加载的分类
原类中实现了+load方法,分类中没有实现。
在_read_images中,会通过realizeClassWithoutSwift函数来实现类,realizeClassWithoutSwift函数中又会调用methodizeClass函数。
初始化完成之后,ro中就已经存在懒加载的分类方法了。这里编译器会自动将category方法加进去。
以上。