前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >类的加载(二)

类的加载(二)

作者头像
拉维
发布2021-03-10 14:29:56
5370
发布2021-03-10 14:29:56
举报
文章被收录于专栏:iOS小生活iOS小生活

面试题——如何动态创建一个类

通过前面文章的介绍,我们已经对rw和ro之间的关系有过了解了,本篇文章首先通过一个面试题来加深下诸位对ro和rw的理解,也进一步熟悉下Runtime的API。

题目是:如何动态创建一个类?

第一步,动态创建类对象

代码语言:javascript
复制
Class LVPerson = objc_allocateClassPair([NSObject class], "LVPerson", 0);

我们使用objc_allocateClassPair函数来动态创建一个类对象,该函数需要传三个参数:

第一个参数是父类对象,如果传nil那么新创建的类就是跟NSObject同等级别的根类对象。

第二个参数是本类类名。

第三个参数是初始的内存空间大小。

我们如果使用objc_allocateClassPair函数来创建一个类对象失败了,那么objc_allocateClassPair就会返回Nil。如果所要创建的类已经存在了,那么就会返回Nil。

实际上,我们在Xcode中创建的.h、.m这样的GUN文件,其目的就是为了Xcode能够识别,其最终底层都还是会被转换成我上面所写的那些运行时代码的。

第二步,添加成员变量

代码语言:javascript
复制
class_addIvar(LVPerson, "lvName", sizeof(NSString *), log2(sizeof(NSString *)), "@");

我们是使用class_addIvar函数来给类对象添加成员变量。

该函数定义如下:

代码语言:javascript
复制
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是签名。

第三步,注册到内存

代码语言:javascript
复制
objc_registerClassPair(LVPerson);

objc_registerClassPair函数是往内存中去注册类对象

现在我们考虑一个问题,class_addIvar函数能否在objc_registerClassPair函数之后调用?答案是不能!为什么呢?

objc_registerClassPair函数是将创建的类对象加载到内存,加载完成之后,本类中的ro就已经确定了,我们知道,ro是只读的,它在确定之后就不可以动态增加内容了,如果我们想在运行时增加一些内容,只能是往rw中去增加。接下来我们看一看ro和rw的结构。

rw的数据结构:

代码语言:javascript
复制
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的数据结构:

代码语言:javascript
复制
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三个变量的,所以,在类注册完成之后,可以继续添加方法、属性和协议

首先来看一下添加属性的代码:

代码语言:javascript
复制
// 封装的添加属性的方法
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函数实现的,该函数有四个参数:

第一个参数是给哪个类添加属性

第二个参数是属性名

第三个参数是所添加的属性的一些属性,比如所属类、读写性、原子性、内存管理策略等

对应关系如下:

第四个参数是属性的属性的数量。

第五步,添加方法

上面第四步已经添加了属性了,我们接下来使用一下:

代码语言:javascript
复制
id person = [LVPerson alloc];
[person setValue:@"Lavie" forKey:@"lvName"];
NSLog(@"%@",[person valueForKey:@"lvName"]);

[person setValue:@"master" forKey:@"subject"];
NSLog(@"%@",[person valueForKey:@"subject"]);

此时,运行程序后会报错。这是因为我只添加了属性,并没有给属性添加对应的setter和getter。

接下来我给属性添加设置器和访问器:

代码语言:javascript
复制
// 添加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方法,就说明该类是懒加载类,懒加载类在真正使用的时候才会去实现。

实际上,类默认都是懒加载类,只有实现了+load方法才是非懒加载类。如果所有的类都是非懒加载类的话,势必会增大编译期的压力,因为此时每一个类都需要在编译期进行初始化。所以,除了那些手动覆写了+load方法的类之外,其余的类基本都是懒加载类,也就是说,这些懒加载类会在使用的时候才会真正去实现

懒加载类的加载

我们上面提到,所有的懒加载类都是在使用到的时候去实现的,那么使用的场景有哪些呢?

第一个使用场景就是被其他的子类继承

我在上篇文章类的加载(一)中分析过,realizeClassWithoutSwift函数中会递归调用realizeClassWithoutSwift函数来实现其父类以及元类的实现。

也就是说,当一个类在实现的时候,会连带着它的父类和元类都实现。

第二个使用场景是第一次调用了某个方法

通过之前的文章方法的查找流程——慢速查找我们知道,会通过lookUpImpOrForward函数来进行方法的慢速查找流程:

在lookUpImpOrForward函数里面会有一个该类是否实现的判断(上图红框内),如果没有实现,那么就调用realizeClassMaybeSwiftAndLeaveLocked函数进行实现:

代码语言:javascript
复制
static Class
realizeClassMaybeSwiftAndLeaveLocked(Class cls, mutex_t& lock)
{
    return realizeClassMaybeSwiftMaybeRelock(cls, lock, true);
}

然后我们再点击realizeClassMaybeSwiftMaybeRelock函数:

代码语言:javascript
复制
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所在文件目录下,终端执行如下命令:

代码语言:javascript
复制
clang -rewrite-objc Norman+configure.m

此时该路径下会多出一个cpp文件,如下:

我们大概该cpp文件,查看C++源码:

这说明我们创建的分类在底层是_category_t结构体:

代码语言:javascript
复制
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;
};
  • name是分类的名字
  • cls是谁的分类
  • instance_methods是实例方法列表
  • class_methods是类方法列表
  • protocols是协议列表
  • properties是属性列表

类的加载(一)中我们了解到,动态往rw中添加内容是通过attachLists函数实现的,实际上,这里的分类中的内容也是通过attachLists函数来添加到rw中的。

并且,实例方法和类方法需要分开处理,因为他们需要贴(attach)到不同的类对象中。instance_methods需要贴到类对象中,class_methods需要贴到元类对象中。

懒加载类 + 非懒加载的分类

我们知道,在编译阶段,动态链接器获取到mach-o中的镜像内容完成之后,会调用_read_images函数来读取镜像内容,我们在上篇文章中已经对该内容有过介绍,但是没有介绍类目信息的读取,接下来我们看一下_read_images函数中对类目信息的读取源码:

我们看到,对于那些非懒加载的分类(分类中有实现+load方法),会在_read_images函数中将其内容加载到缓存表。加载到缓存表之后,会判断所在类有没有实现,如果已经实现了,那么就会接着将刚才插入到缓存表里面的分类数据实现到类中;如果类没有实现,那么就跳过,等到后面去实现。

我们再看一下类的实现,realizeClassWithoutSwift函数。其实在编译链接期,realizeClassWithoutSwift函数也有被调用,只不过此时是用于非懒加载类的实现,对于懒加载类,它根本就不会调用。而在编译完成之后使用到该类的时候,会再次调用realizeClassWithoutSwift函数,此时会将刚才插入到缓存表中的类目信息(通过unattachedCategoriesForClass函数获取到)实现到类中。

因此,对于【懒加载类 + 非懒加载分类】的场景,其分类信息的实现如下:

  1. 在_read_images中,通过_getObjc2CategoryList和addUnattachedCategoryForClass将分类信息从Mach-O中读取出来插入到分类缓存表中
  2. 在第一次使用到类的时候会调到realizeClassWithoutSwift函数,realizeClassWithoutSwift会调到methodizeClass,methodizeClass中会通过attachCategories将分类中的内容贴到类里面:

非懒加载类 + 非懒加载的分类

原类和分类里面都实现了+load方法。

在_read_images中,会通过realizeClassWithoutSwift函数来实现类,此时分类信息还没有从Mach-O里面读取出来并加载到缓存表,因此分类信息此时尚未实现到类里面

在_read_images中,通过_getObjc2CategoryList和addUnattachedCategoryForClass将分类信息从Mach-O中读取出来插入到分类缓存表中

此时类已经实现了,因此可以调用remethodizeClass函数将上一步缓存到分类缓存表中的分类信息贴到类里面:

代码语言:javascript
复制
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方法加进去。

以上。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-02-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 iOS小生活 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档