iOS底层探索--类的加载原理分析(上)

900 阅读8分钟

小谷底层探索合集

  • 兄弟们,按理说应该一波搞定的~ 结果来了个上、下篇。(原谅我的无知。毕竟还没有探索完。也不敢保证😿)

  • 上篇博客写了dyld 和 objc的关联,我们已经了解:APP启动后,会调用map_imagesload_images,今天,我们就根据这两个方法继续探究~

哎。。探究的过程肯定索然无味。也不知道有没有兄弟会看我的博客

1. map_images分析

    1. 找到源码实现
void
map_images(unsigned count, const char * const paths[],
           const struct mach_header * const mhdrs[])
{
    mutex_locker_t lock(runtimeLock);
    return map_images_nolock(count, paths, mhdrs);
}

兄弟们,其实探究源码最好的办法和验证,是可以断点调试,观察他的走向和流程。。如果没有源码的兄弟们,可以看看我原先的博客,有配好的最好也自己配置下,和朋友吃饭可以吹牛皮😆

    1. 找到源码之后发现就2行,那我们直接点进去看看~
void 
map_images_nolock(unsigned mhCount, const char * const mhPaths[],
                  const struct mach_header * const mhdrs[])
{
//内容过长,就先不贴了
}

当我们看到这么长的代码的时候:其实不止是自己。谁都慌、没有半点看下去的欲望!!

    1. 那我们把他的判断都折起来,然后通过注释,了解一下大概。(传说中的:先看总述,在细致分析)
void 
map_images_nolock(unsigned mhCount, const char * const mhPaths[],
                  const struct mach_header * const mhdrs[])
{
    static bool firstTime = YES;
    header_info *hList[mhCount];
    uint32_t hCount;
    size_t selrefCount = 0;

    // Perform first-time initialization if necessary.
    // This function is called before ordinary library initializers. 
    // fixme defer initialization until an objc-using image is found?
    if (firstTime) { … }

    if (PrintImages) { … }


    // Find all images with Objective-C metadata.
    hCount = 0;

    // Count classes. Size various table based on the total.
    int totalClasses = 0;
    int unoptimizedTotalClasses = 0;
    { … }

    // Perform one-time runtime initialization that must be deferred until 
    // the executable itself is found. This needs to be done before 
    // further initialization.
    // (The executable may not be present in this infoList if the 
    // executable does not contain Objective-C code but Objective-C 
    // is dynamically loaded later.
    if (firstTime) { … }

    if (hCount > 0) {
        _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
    }

    firstTime = NO;
    
    // Call image load funcs after everything is set up.
    for (auto func : loadImageFuncs) { … }
}

这样就舒服很多,不会很慌~

    1. 我们分析一波这里面做的啥。为了兄弟们方便,我截个图解释下:

1.1. _read_images分析

    1. 兄弟们~,我们点进去观察一波读取镜像的方法
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
//非常厉害的发现。这群人都有毒,写这么长,不就是不想让我看吗~
//那我必须研究一波啊。。
}
    1. 老规矩咱们先看看总述,没用的折叠起来

我就不贴了,毕竟代码比较长。不过折叠完,就比较清晰了~。

本来想看下注释解释然后截波图说明的,不过苹果也很亲切的写了log。

    1. _read_images流程分析

      1. 条件控制进行一次的加载
      1. 修复预编译阶段的 @selector 的混乱问题
      1. 错误混乱的类处理
      1. 修复重映射一些没有被镜像文件加载进来的类
      1. 修复一些消息
      1. 当我们类里面有协议的时候 : readProtocol
      1. 修复没有被加载的协议
      1. 分类处理
      1. 类的加载处理
      1. 没有被处理的类 优化那些被侵犯的类
    1. 既然大致的框架垒出来了。那我们要分析类的加载。就断点调试一波。找到关于类的方法

    1. 断点调试情况分析

然后

我们发现。走完这一步。cls突然间关联上类名。这里面肯定做了啥不为人知的事情(😆,最喜欢探究不为人知的秘密了~)

1.2. readClass分析

    1. 我们从上面得出的结论是。readClasscls类名关联起来的~。
    1. 老规矩,大致扫上一波。

既然。大致框架看了一下~那我们调试验证下

    1. 打印mangledName,观察分析

我们观察到:这个应该是要关联的名字,既然这样,那我们就看我们自己写的类(毕竟知根知底~)

    1. 增加条件判断---指定判断类

这样的话,相信大家就比较舒服了~那就go on

    1. 我们通过打断点,分析主要代码
    1. 我们通过断点调试知道,就是addNamedClass,给了cls名字。我们近距离观察下~

1.2.1 addNamedClass方法小小分析一波~

    1. 找到源码:
static void addNamedClass(Class cls, const char *name, Class replacing = nil)
{
    runtimeLock.assertLocked();
    Class old;
    if ((old = getClassExceptSomeSwift(name))  &&  old != replacing) {
        inform_duplicate(name, old, cls);

        // getMaybeUnrealizedNonMetaClass uses name lookups.
        // Classes not found by name lookup must be in the
        // secondary meta->nonmeta table.
        addNonMetaClass(cls);
    } else {
        NXMapInsert(gdb_objc_realized_classes, name, cls);
    }
    ASSERT(!(cls->data()->flags & RO_META));

    // wrong: constructed classes are already realized when they get here
    // ASSERT(!cls->isRealized());
}
    1. 这段代码也不长我们断点跟几步
    1. 跟进去发现。连行注释都没有~(看不下去了)

走完NXMapInsert方法之后,这个类名就关联起来了~(做了一些不为人知的操作。好吧~这个代码服了。兄弟们有兴趣的可以研究一波。)

2. 懒加载与非懒加载

上面分析了下给cls类名。我们继续 _read_images向下分析。

    1. 我不是太清楚,大家对于懒加载的理解程度如何(大家肯定是比我强。)
    1. 名词解释~我先解释一波吧(要是缺点什么,大家可以指点下😆):
懒加载:推迟到 第一次消息发送的时候~

非懒加载:当map_images的时候,加载所有类数据的时候就加载

懒加载和非懒加载的区别标志:当前类是否实现load方法(大家都知道load在main函数之前,其实就是你进入main之前我就加载你了)

2.1. 非懒加载分析

    1. 我们也可以根据_read_images中的注释分析

我们调试的时候没有走到这里,说明我们没有实现load,要是我们实现load的话是不是在这里就可以观察类的加载情况了?那我们搞上一波~

    1. 实现load
#import "XGPerson.h"

@implementation XGPerson
+(void)load{
    
}
@end

我就只加了load,什么都没有变。运行下~

    1. 他站出来了~。

我们通过断点调试,发现他走的是 realizeClassWithoutSwift(cls, nil);。那么我们研究下这个方法~

2.1.1. realizeClassWithoutSwift分析

    1. 根据字面意思来看,他要实现这个类了。那我们在XGPerson中增加几个方法,看下他到底是怎么加进去的~
@interface XGPerson : NSObject
- (void)say3;
- (void)say1;
- (void)say4;
- (void)say2;
@end

@implementation XGPerson
+(void)load{
    
}
- (void)say3{
    NSLog(@"-%s-",__func__);
}
- (void)say1{
    NSLog(@"-%s-",__func__);
}
- (void)say4{
    NSLog(@"-%s-",__func__);
}
- (void)say2{
    NSLog(@"-%s-",__func__);
}
@end
    1. 调试准备

在调试之前,偷偷的加一个自己的方法判断~去除冗余

    const char *mangledName  = cls->mangledName();
    const char *XGPersonName = "XGPerson";

    if (strcmp(mangledName, XGPersonName) == 0) {
        auto xg_ro = (const class_ro_t *)cls->data();
        auto xg_isMeta = xg_ro->flags & RO_META;
        if (!xg_isMeta) {
            printf("%s: 这个是我们的 %s \n",__func__,XGPersonName);
        }
    }

大家也可以使用自己的类作为调试~

    1. 总述~

由于代码过长,我感觉贴在这里可能对大家理解造成不好的后果,我就挑重点解释下

    if (supercls) {
        addSubclass(supercls, cls);
    } else {
        addRootClass(cls);
    }

还有这个,代码太长了,截不到,就在这补一下~

这个,分类都出来了,那我要研究下~。(这个方法是必走的,那会不会在里面加的方法之类的?)

    1. 点进去看一波

大致分析一波

大家可以跟进方法里面,发现就是这加的。

这个里面会有sel的排序啥的,这个大家自己跟进来看下。(这篇博客太长了感觉。我精简下~)

2.2. 懒加载分析

    1. 我们知道非懒加载怎么加的方法属性。那么懒加载
    1. 我们搞一波代码(把load去掉,然后调用一波)
    1. 我们可以通过bt,或者 看堆栈

他最终也会走 realizeClassWithoutSwift,不过他是通过方法查找的时候实现类。

    1. 关于懒加载非懒加载的实现,我画了个图。(不好的地方兄弟们担待下~)

3. 分类

兄弟们。刚才咱们看到分类了。那我们研究下分类的结构(我知道~好多兄弟都知道。但是我还是写一下,自己巩固下印象)

3.1. 分类的本质

    1. 写一波分类
@interface XGPerson (XGTest)
- (void)cateA_1;
- (void)cateA_2;
- (void)cateA_3;
@end

@implementation XGPerson (XGTest)
//+(void)load{
//
//}
- (void)cateA_2{
    NSLog(@"%s",__func__);
}
- (void)cateA_1{
    NSLog(@"%s",__func__);
}
- (void)cateA_3{
    NSLog(@"%s",__func__);
}
@end

这个最好写到main.m中,毕竟我们分析的是main.m。

    1. 我们用clang看下结构~ 命令:
clang -rewrite-objc main.m -o main.cpp
    1. 打开main.cpp文件

我们观察到(本来想贴代码,感觉代码的颜色不好看,还是截图清晰点)

    1. 我们找到这个类型

那我们就可以根据这个类型结构,了解里面的内容了。

我们了解了分类的本质,就是一个 _category_t类型的结构体

3.2. 分类的加载

    1. 在上面,我们已经通过 methodizeClass中找到了,分类的加载 attachToClass
    1. 我们研究下attachToClass

这个函数也没有啥代码,根据断点调试他又没有走。就很难受。

    1. 那会不会是不用就不加载?我加load方法试下,会不会走一波关键方法 attachCategories

我们可以观察到他调用了,是在load_images调用的~

    1. 我们寻找关键代码,把一系列判断的都折叠

图截得有点乱(兄弟们见谅·~)

类的加载上,目前先到这~,等我写完下篇,应该会画个流程图啥的。😆