iOS底层分析之类的加载(中)

749 阅读9分钟

和谐学习!不急不躁!!我是你们的老朋友小青龙~

前一篇文章: iOS底层分析之类的加载(上) 我们分析到_objc_init里, map_images->...->_read_images->readClass,readClass内部:

  • 类名写入非元类映射表(addNamedClass)
  • 将类添加到所有类的那张表里(addClassTableEntry) 到目前为止,我们还没发现类的rorw信息在哪里初始化。
    纵观_read_images函数内部几个操作,我们发现只有以下几个部分代码是跟类有关系:
  • ************ 修复类的混乱问题  ************(这个我们已经分析过)
  • ************ 修复重映射没有被镜像文件加载进来的 类  ************
  • ************ 类的加载处理  ************
  • ************ 实现未来类解析  ************ 通过辅助代码,定位自定义Direction类:
/******* 辅助代码   START *********/
const char *mangledName = cls->nonlazyMangledName();
const char *comName = "Direction";
if (strcmp(comName, mangledName) == 0) {
    printf("mangledName-->%s\n",mangledName);
}
/******* 辅助代码   END *********/

image.png

image.png

最后定位到类的加载处理这块代码:

void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses) {
    ...
    // 8、************ 类的加载处理  ************
    ...
    classref_t const *classlist = hi->nlclslist(&count);
    for (i = 0; i < count; i++) {
        Class cls = remapClass(classlist[i]);
        if (!cls) continue;
        /******* 辅助代码   START *********/
        const char *mangledName = cls->nonlazyMangledName();
        const char *comName = "Direction";
        if (strcmp(comName, mangledName) == 0) {
            printf("mangledName-->%s\n",mangledName);
        }
        /******* 辅助代码   END *********/
        ...
        realizeClassWithoutSwift(cls, nil);
        ...
    }
    ...
}
realizeClassWithoutSwift

进入realizeClassWithoutSwift

/***********************************************************************

* realizeClassWithoutSwift

* Performs first-time initialization on class cls, 
* including allocating its read-write data.
* Does not perform any Swift-side initialization.
* Returns the real class structure for the class. 
* Locking: runtimeLock must be write-locked by the caller
**********************************************************************/
/**
*   对类cls执行首次初始化,
*   包括分配读写数据。
*   不执行任何Swift端初始化。
*   返回类的实际类结构。
*   锁定:runtimeLock必须由调用方进行写锁定
*/
static Class realizeClassWithoutSwift(Class cls, Class previously)
{
    ...
    /// ************ ro、rw处理  ************
    /// 这里的ro赋值请看后面的“通过地址指针强转类型并自动赋值”
   auto ro = (const class_ro_t *)cls->data();
   auto isMeta = ro->flags & RO_META;
   if (ro->flags & RO_FUTURE) {
       ...
   }else{
       rw = objc::zalloc<class_rw_t>();
       rw->set_ro(ro);// ro复制一份给rw
       rw->flags = RW_REALIZED|RW_REALIZING|isMeta;
       cls->setData(rw);
   }
    ...
    /// ************ 类的处理  ************

    // 递归加载父类
    supercls = realizeClassWithoutSwift(remapClass(cls->getSuperclass()), nil);
    // 递归加载元类
    metacls = realizeClassWithoutSwift(remapClass(cls->ISA()), nil);
    ...
    //关联父类与元类。也就是继承链与isa走位。
    cls->setSuperclass(supercls);
    cls->initClassIsa(metacls);
    ...
    // 关联父类子类关系
    if (supercls) {
        addSubclass(supercls, cls);
    } else {
        addRootClass(cls);
    }
    ...
    // 方法名写入、方法排序
    methodizeClass(cls, previously);
    return cls;
}

001.png 执行完ro赋值,继续打印:

image.png

image.png

发现,已经成功打印出了run和sleep这两个实例方法。
控制台输入x/4gx cls,并且进入下一次递归,定位到断点为止,控制台继续输入x/4gx cls

image.png

已经确定当前进入断点的是元类,那就重复上述类的操作,查找ro里是否存放着类方法

image.png

至此,类方法也在元类的ro里打印出来了。

methodizeClass
static void methodizeClass(Class cls, Class previously)
{
    ...
    /******* 辅助代码   START *********/
    const char *mangledName = cls->nonlazyMangledName();
    const char *comName = "Direction";
    if (strcmp(comName, mangledName) == 0) {
        printf("类的加载处理|| mangledName-->%s\n",mangledName);
    }
    /******* 辅助代码   END *********/
    bool isMeta = cls->isMetaClass();
    auto rw = cls->data();
    auto ro = rw->ro();
    auto rwe = rw->ext();
    ...
    method_list_t *list = ro->baseMethods();
    if (list) {
        prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls), nullptr);
        if (rwe) rwe->methods.attachLists(&list, 1);
    }
    ...
}

为了保证研究的class是普通类Direction,加上辅助代码,在printf哪一行打上断点进行调试:

image.png

image.png

进入prepareMethodLists

static void 
prepareMethodLists(Class cls, method_list_t **addedLists, int addedCount,
                   bool baseMethods, bool methodsFromBundle, const char *why)
{
    ...
    /// 核心部分
    for (int i = 0; i < addedCount; i++) {
        method_list_t *mlist = addedLists[i];
        ASSERT(mlist);
        if (!mlist->isFixedUp()) {
            fixupMethodList(mlist, methodsFromBundle, true/*sort*/);
        }
    }
    ...
}

for语句里打上断点:

image.png

说明for语句内部的mlist就是前面ro的list
进入fixupMethodList:

static void 
fixupMethodList(method_list_t *mlist, bool bundleCopy, bool sort)
{
    ...
    /// 设置SEL
    for (auto& meth : *mlist) {
        const char *name = sel_cname(meth.name());
        meth.setName(sel_registerNameNoLock(name, bundleCopy));
    }
    ...
    /// 方法排序
    if (sort && !mlist->isSmallList() && mlist->entsize() == method_t::bigSize) {
        method_t::SortBySELAddress sorter;
        std::stable_sort(&mlist->begin()->big(), &mlist->end()->big(), sorter);
    }
    ...
}

查看方法排序,对fixupMethodList增加一些打印代码

static void 
fixupMethodList(method_list_t *mlist, bool bundleCopy, bool sort)
{
    ...
    ...
    // 为了确保当前打印的是Direction类,
    // 可以在methodizeClass方法,先断点卡住,然后清空控制台数据,再放开断点。
    for (auto& meth : *mlist) {
        const char *name = sel_cname(meth.name());
        printf("%s ->%p\n",name,meth.name());
    }
    
    /// 方法排序
    if (sort && !mlist->isSmallList() && mlist->entsize() == method_t::bigSize) {
        method_t::SortBySELAddress sorter;
        std::stable_sort(&mlist->begin()->big(), &mlist->end()->big(), sorter);
    }
    
    printf("排序后\n\n");
    for (auto& meth : *mlist) {
        const char *name = sel_cname(meth.name());
        printf("%s ->%p\n",name,meth.name());
    }
    ...
}

image.png

懒加载和非懒加载

一个工程随着时间的累积,类会越来越多,如果每一个类都在main启动之前就去读写ro、rw、methodlist等等,那无疑会大大增加应用程序的启动速度。所以苹果设计了懒加载类非懒加载类模式。针对一些非懒加载类,只需要在类内部实现+(void)load方法皆可。 回顾前面read_image方法:

void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses) {
    ...
    // 8、************ 类的加载处理  ************
    // Category discovery MUST BE Late to avoid potential races
    // when other threads call the new category code before
    // this thread finishes its fixups.
    // +load handled by prepare_load_methods()
    // 实现了load方法才会进入for循环
    ...
    classref_t const *classlist = hi->nlclslist(&count);
    for (i = 0; i < count; i++) {
        ...
        realizeClassWithoutSwift(cls, nil);
        ...
    }
    ...
}

注释里解释了,只有实现了load方法的非懒加载类,才会执行到for循环里, 继而执行realizeClassWithoutSwift函数,对类进行一系列的初始化。
那么问题来了,那些没有实现load方法的懒加载类的初始化是由哪里发起的呢?
为了研究这个问题,我们新建了一个类DirectionChild类,没有实现load方法。

image.png

接下来,我们在main里实现DirectionChild:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"这里是main函数~");
        DirectionChild *dir = [DirectionChild new];
        NSLog(@"这里是main函数结束~");
    }
    return 0;
}

realizeClassWithoutSwift增加一段辅助代码,用于下断点定位DirectionChild类:

/******* 辅助代码   START *********/
const char *mangledName = cls->nonlazyMangledName();
const char *comName = "DirectionChild";
if (strcmp(comName, mangledName) == 0) {
    printf("类的加载处理 -> 我是DirectionChild");
}
/******* 辅助代码   END *********/

image.png 控制台打印的最后一句可以说明,执行到当前断点,已经走完了main之前的那些步骤,再看左边,可以发现懒加载类DirectionChild的初始化realizeClassWithoutSwift函数是由lookUpImpOrForward发起的。

回到objc源码,搜索lookUpImpOrForward

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    ...
    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
    ...
}
static Class
realizeAndInitializeIfNeeded_locked(id inst, Class cls, bool initialize)
{
    ...
    cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
    ...
}
static Class

realizeClassMaybeSwiftAndLeaveLocked(Class cls, mutex_t& lock)
{
    return realizeClassMaybeSwiftMaybeRelock(cls, lock, true);
}
static Class

realizeClassMaybeSwiftMaybeRelock(Class cls, mutex_t& lock, bool leaveLocked)
{
    ...
    realizeClassWithoutSwift(cls, nil);
    ...
}

所以懒加载类的大致流程走向:

  • _objc_msgSend_uncached
  • lookUpImpOrForward
  • realizeAndInitializeIfNeeded_locked
  • realizeClassMaybeSwiftAndLeaveLocked
  • realizeClassMaybeSwiftMaybeRelock
  • realizeClassWithoutSwift

具体是由哪个方法发起的呢?目前还不太确定,可能是alloc,也可能是类方法等。这边一一测试:
测试alloc

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"这里是main函数~ 测试alloc");
        DirectionChild *dir = [DirectionChild new];
        NSLog(@"这里是main函数结束~");
    }
    return 0;
}

image.png

测试类方法

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"这里是main函数~ 测试类方法");
        [DirectionChild happy];
        NSLog(@"这里是main函数结束~");
    }
    return 0;
}

image.png

测试结果:调用alloc或类方法都会对DirectionChild类进行初始化。

结论:对普通懒加载类发送一个消息,就会进入类初始化流程。

懒加载和非懒加载类 -> 加载流程

非懒加载类(map_images的时候):

  • _read_images
  • _getObjc2ClassList
  • readClass
  • realizeClassWithoutSwift
  • methodizeClass

懒加载类(对类第一次发送消息的时候):

  • _objc_msgSend_uncached
  • lookUpImpOrForward
  • realizeAndInitializeIfNeeded_locked
  • realizeClassMaybeSwiftAndLeaveLocked
  • realizeClassMaybeSwiftMaybeRelock
  • realizeClassWithoutSwift
  • methodizeClass

分类探究

分了结构 给添加分类

#import "Direction.h"
@interface Direction (category)
@property (nonatomic , strong) NSString *name_category;
@property (nonatomic , assign) NSInteger count;
- (void)eatSome;
+ (void)cleanSome;
@end


#import "Direction+category.h"
@implementation Direction (category)<NSObject>
- (void)eatSome{
    NSLog(@"Direction+category ----eat something.");
}
+ (void)cleanSome{
    NSLog(@"Direction+category ----cleanSome.");
}
@end

cd到分类所在目录,并执行命令$xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Direction+category.m
打开Direction+category.cpp,全局搜索category定位到分类结构:

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:属性列表

注意:分类中的属性不会自动生成set、get方法。

代码拉到底,可以看到这样一段代码:

image.png 跟category结构体一一对应。

回到objc源码,搜索category_timage.png 再次验证了分类本质上也是一种结构体

分类的加载

category分类的本质我们已经分析了,它和普通类一样,也有属性、协议、实例方法、类方法,那么它又是如何加载到普通类里,以便我们直接通过类调用的呢?

回到objc源码,定位到methodizeClass函数,看到一段跟category有关的代码:

static void methodizeClass(Class cls, Class previously){
    ...
    auto rwe = rw->ext();
    ...
    if (rwe) rwe->methods.attachLists(&list, 1);
    
    property_list_t *proplist = ro->baseProperties;
    if (rwe && proplist) {
        rwe->properties.attachLists(&proplist, 1);
    }
    
    protocol_list_t *protolist = ro->baseProtocols;
    if (rwe && protolist) {
        rwe->protocols.attachLists(&protolist, 1);
    }
    // Attach categories.
    // 附加到类别
    if (previously) {
        if (isMeta) {
            objc::unattachedCategories.attachToClass(cls, previously,
                                                     ATTACH_METACLASS);
        } else {
             // When a class relocates, categories with class methods
            // may be registered on the class itself rather than on
            // the metaclass. Tell attachToClass to look for those.
            objc::unattachedCategories.attachToClass(cls, previously,
                                                     ATTACH_CLASS_AND_METACLASS);
        }
    }
    ...

}

我们发现,attachLists函数需要rwe有值才能执行,定位到rwe初始化代码,进入ext

class_rw_ext_t *ext() const {
    return get_ro_or_rwe().dyn_cast<class_rw_ext_t *>(&ro_or_rw_ext);
}

class_rw_ext_t *extAllocIfNeeded() {
        auto v = get_ro_or_rwe();
        if (fastpath(v.is<class_rw_ext_t *>())) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext);
        } else {
            return extAlloc(v.get<const class_ro_t *>(&ro_or_rw_ext));
        }
    }
// 到此,我们知道ext返回get_ro_or_rwe,而get_ro_or_rwe的操作是在extAllocIfNeeded函数里

搜索extAllocIfNeeded,发现有以下几个地方调用了:

  • attachCategories函数(将类别的属性、协议、方法添加到类)
  • demangledName函数
  • class_setVersion函数(类的版本设置)
  • addMethods_finish函数(添加方法)
  • class_addProtocol函数(添加协议)
  • _class_addProperty函数(添加属性)
  • objc_duplicateClass函数

这几个初始化rwe的函数,核心是在attachCategories函数,全局搜一下它,看看哪里调用了它:

void attachToClass(Class cls, Class previously, int flags){
    ...
    if (flags & ATTACH_CLASS_AND_METACLASS) {
        int otherFlags = flags & ~ATTACH_CLASS_AND_METACLASS;
        attachCategories(cls, list.array(), list.count(), otherFlags | ATTACH_CLASS);
        attachCategories(cls->ISA(), list.array(), list.count(), otherFlags | ATTACH_METACLASS);
    } else {
        attachCategories(cls, list.array(), list.count(), flags);
    }
    ...
}


static void load_categories_nolock(header_info *hi){
    ...
    attachCategories(cls, &lc, 1, ATTACH_EXISTING);
    ...
    attachCategories(cls->ISA(), &lc, 1, ATTACH_EXISTING | ATTACH_METACLASS);
    ...
}

根据搜索结果来看,只有attachToClassload_categories_nolock会调用attachCategories函数,继而对rwe进行赋值

分类的探索接下去的部分,会放在下篇文章继续《iOS底层分析之类的加载(下)》

通过地址指针强转类型并自动赋值
static Class realizeClassWithoutSwift(Class cls, Class previously)
{
    ...
    /// ************ ro、rw处理  ************
   auto ro = (const class_ro_t *)cls->data();
   ...
}

进入data

class_rw_t *data() const {
    return bits.data();
}

进入data

class_rw_t* data() const {
    // 这个data函数通过bit地址与上FAST_DATA_MASK,返回一个地址指针
    return (class_rw_t *)(bits & FAST_DATA_MASK);
}

maim.m里写一段代码:

/*  
// Method本质上就是objc_method结构体指针
typedef struct objc_method *Method;

struct objc_method {
    SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
    char * _Nullable method_types                            OBJC2_UNAVAILABLE;
    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
}

*/

// 按照objc_method格式自定义了一个结构体
struct ssj_objc_method {
    SEL _Nonnull ssj_method_name                                 OBJC2_UNAVAILABLE;
    char * _Nullable ssj_method_types                            OBJC2_UNAVAILABLE;
    IMP _Nonnull ssj_method_imp                                  OBJC2_UNAVAILABLE;
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        Method m = class_getInstanceMethod(Direction.class, @selector(eatSome));

        struct ssj_objc_method * objc_m =(struct ssj_objc_method *) class_getInstanceMethod(Direction.class, @selector(eatSome));

        NSLog(@"这里是main函数结束~");
    }
    return 0;
}
/** 
* 分析这段代码,class_getInstanceMethod函数最终返回的是一个结构体指针,
* 即地址指针;理论上,只要类型匹配的上,结构体内部组织也一样,就可以强转
*/

为了验证分析的猜想,运行代码:

image.png

结果证明:地址指针除了可以访问到值以外,还可以用来做强转。

再回头来看这句代码auto ro = (const class_ro_t *)cls->data();
意思就是一旦从bits里读取到data数据,就会按照class_ro_t的格式,对里面的数据一一赋值,然后返回一个class_ro_t指针给ro。

代码

链接: pan.baidu.com/s/17bzAR9wp…
密码: jo8a
--来自百度网盘的分享

本文总结

  1. 研究的方向:ro、rw在哪里初始化?

  2. 研究的方式:

  • 通过辅助代码,定位cls=Direction类,阅读_read_images函数内部操作

  • 定位到realizeClassWithoutSwift

  • 定位到methodizeClass(方法名写入、方法排序)

  • rwe初始化由 extAllocIfNeeded函数完成

  • 懒加载类和非懒加载类(实现了+load方法) -> 加载流程

  • 分类的本质

下篇预告:

iOS底层分析之类的加载(下)----待更新
(分类的加载)