OC底层原理(12)类的加载原理(上)

189 阅读8分钟

dyld 加载流程 中,我们知道_objc_init 是一个很关键的方法。因为_objc_init方法向dyld中注册了回调函数,下面探究下_objc_init方法

一. _objc_init

上源码:

void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    // fixme defer initialization until an objc-using image is found?
    environ_init(); // 读取配置的环境变量
    tls_init(); // 设置线程key
    static_init(); // 执行C++静态函数
    runtime_init(); // 运行环境初始化
    exception_init(); // 异常系统初始化

#if __OBJC2__
    cache_t::init(); // 缓存初始化
#endif
    _imp_implementationWithBlock_init();
    _dyld_objc_notify_register(&map_images, load_images,  unmap_image); // 注册回调函数
 
#if __OBJC2__
    didCallDyldNotifyRegister = true ;
#endif
}

1.1 environ_init()

environ_init 是读取环境变量,源码:

Xnip2022-07-15_10-34-55.png

PrintHelp或者PrintOptions条件下,可以打印所有的环境变量,修改源码,删除条件判断代码,只留下打印代码:

void  environ_init(void) 
{
    for (size_t i = 0; i < sizeof(Settings)/sizeof(Settings[0]); i++) {
        const option_t *opt = &Settings[i];
        _objc_inform("%s: %s", opt->env, opt->help);
        _objc_inform("%s is set", opt->env);
    }
}

运行项目(objc源码),输出:

Xnip2022-07-15_10-40-13.png

也可以通过终端命令 export OBJC_HELP=1 输出:

Xnip2022-07-15_13-28-20.png

Xcode中环境变量配置的位置:选中运行的target--> Edit scheme... --> Run --> Arguments --> Environment Variables

这些环境变量是可以通过Xcode配置的,下面就举几个例子演示下:

1.1.1 环境变量 OBJC_PRINT_LOAD_METHODS

OBJC_PRINT_LOAD_METHODS 记录对类和类别 +(void)load 方法的调用,在自定义类/类别中添加 load 方法,配置环境变量OBJC_PRINT_LOAD_METHODS = YES

Xnip2022-07-15_13-31-34.png

运行项目:

Xnip2022-07-15_13-35-02.png

+[YJPerson load] 这个是自定义YJPerson类中的load方法,其它的都是系统级别的load方法。load方法太多会导致你应用程序启动变慢,或者有的人在load方法里面做一些骚操作,你可以通过这个环境变量检查出谁实现的load方法最多。。。

1.1.2 环境变量 OBJC_PRINT_REPLACED_METHODS

OBJC_PRINT_REPLACED_METHODS : 记录被分类重写的方法

Xnip2022-07-15_13-59-21.png

写个 NSString+(YJ) 分类:

@implementation NSString (YJ)
- (NSString *)substringToIndex:(NSUInteger)to
{
    return @"方法被类别重写了";
}
@end

运行项目,输出结果:

Xnip2022-07-15_13-57-00.png

1.2 tls_init

tls_init关于线程key的绑定,比如每个线程数据的析构函数

 void  tls_init(void)
{ 
#if SUPPORT_DIRECT_THREAD_KEYS
    //创建线程的詹存缓存池
    pthread_key_init_np(TLS_DIRECT_KEY, &_objc_pthread_destroyspecific);
#else
    //析构函数
    _objc_pthread_key = tls_create(&_objc_pthread_destroyspecific);
#endif
}

1.3 static_init

运行C++静态构造函数。Libcdyld调用静态构造函数之前调用_objc_init()

/***********************************************************************
* static_init
* Run C++ static constructor functions.
* libc calls _objc_init() before dyld would call our static constructors, 
* so we have to do it ourselves.
**********************************************************************/
static void static_init()
{
    size_t count;
    auto inits = getLibobjcInitializers(&_mh_dylib_header, &count);
    for (size_t i = 0; i < count; i++) {
        inits[i]();
    }
    auto offsets = getLibobjcInitializerOffsets(&_mh_dylib_header, &count);
    for (size_t i = 0; i < count; i++) {
        UnsignedInitializer init(offsets[i]);
        init();
    }
}

1.4 runtime_init

runtime运行时环境初始化,里面主要是unattachedCategoriesallocatedClasses两张表

void runtime_init(void)
{  
   objc::unattachedCategories.init(32);// 分类表的初始化
   objc::allocatedClasses.init();// 类表的初始化
}

1.5 exception_init

完成 objc 异常处理系统的初始化,进行回调函数的设置,实现异常捕获处理。

// Initialize libobjc's exception handling system.
// 初始化异常处理系统
void exception_init(void)
{
  old_terminate = std::set_terminate(&_objc_terminate);
}
/***********************************************************************
* _objc_terminate
*自定义std::terminate处理程序。
*
未捕获的异常回调被实现为std::terminate处理程序。
* 1。检查是否存在活动异常
* 2。如果是,检查它是否是Objective-C异常
* 3。如果是,用对象调用已注册的回调函数。
* 4。最后,调用前面的终止处理程序。
**********************************************************************/
static void (*old_terminate)(void) = nil;
static void _objc_terminate(void)
{
    if (PrintExceptions) {
        _objc_inform("EXCEPTIONS: terminating");
    }

    if (! __cxa_current_exception_type()) {
        // No current exception.
        (*old_terminate)();
    }
    else {
        // There is a current exception. Check if it's an objc exception.
        // 检查它是否是一个objc异常。
        @try {
            __cxa_rethrow();
        } @catch (id e) {
            // It's an objc object. Call Foundation's handler, if any.
            // 是 objc 对象,调用处理
            (*uncaught_handler)((id)e);
            (*old_terminate)();
        } @catch (...) {
            // It's not an objc object. Continue to C++ terminate.
            // 非 objc 对象
            (*old_terminate)();
        }
    }
}

当应用出现crash,什么是crash当你上层的代码出现一些不符合系统底层规则时,系统会发出异常的信号。通过 _objc_terminate --> uncaught_handler方法将异常信息传递到上层代码

/***********************************************************************
* objc_setUncaughtExceptionHandler
* 为未捕获的Objective-C异常设置一个处理程序。
* 返回前一个处理器。
**********************************************************************/
objc_uncaught_exception_handler 
objc_setUncaughtExceptionHandler(objc_uncaught_exception_handler fn)
{
    objc_uncaught_exception_handler result = uncaught_handler;
    uncaught_handler = fn;
    return result;
}

OC层,我们可以通过调用方法 NSSetUncaughtExceptionHandler 设置回调函数,回调函数会被赋值给uncaught_handler。代码测试:

Xnip2022-07-15_16-41-45.png

1.6 cache_init()

缓存条件初始化

void cache_t::init()
{
#if HAVE_TASK_RESTARTABLE_RANGES
    mach_msg_type_number_t count = 0;
    kern_return_t kr;
    while (objc_restartableRanges[count].location) {
        count++;
    }
    //开启缓存
    kr = task_restartable_ranges_register(mach_task_self(),
                                         objc_restartableRanges, count)
    if (kr == KERN_SUCCESS) return;
    _objc_fatal("task_restartable_ranges_register failed (result 0x%x: %s)",
                kr, mach_error_string(kr));
#endif // HAVE_TASK_RESTARTABLE_RANGES
}

1.7 _imp_implementationWithBlock_init

启动回调机制。通常不会做什么,因为所有的初始化都是惰性的,但是对于某些进程,会迫不及待的加载 trampolines dylib

void
_imp_implementationWithBlock_init(void)
{
#if TARGET_OS_OSX
    // Eagerly load libobjc-trampolines.dylib in certain processes. Some
    // programs (most notably QtWebEngineProcess used by older versions of
    // embedded Chromium) enable a highly restrictive sandbox profile which
    // blocks access to that dylib. If anything calls
    // imp_implementationWithBlock (as AppKit has started doing) then we'll
    // crash trying to load it. Loading it here sets it up before the sandbox
    // profile is enabled and blocks it.
    //
    // This fixes EA Origin (rdar://problem/50813789)
    // and Steam (rdar://problem/55286131)
    if (__progname &&
        (strcmp(__progname, "QtWebEngineProcess") == 0 ||
         strcmp(__progname, "Steam Helper") == 0)) {
        Trampolines.Initialize();
    }
#endif
}

1.8 _dyld_objc_notify_register

dyld的注册回调函数,_dyld_objc_notify_register仅供objc运行时调用且方法的实现在dyld源码中

//
// Note: only for use by objc runtime
// Register handlers to be called when objc images are mapped, unmapped, and initialized.
// Dyld will call back the "mapped" function with an array of images that contain an objc-image-info section.
// Those images that are dylibs will have the ref-counts automatically bumped, so objc will no longer need to
// call dlopen() on them to keep them from being unloaded.  During the call to _dyld_objc_notify_register(),
// dyld will call the "mapped" function with already loaded objc images.  During any later dlopen() call,
// dyld will also call the "mapped" function.  Dyld will call the "init" function when dyld would be called
// initializers in that image.  This is when objc calls any +load methods in that image.
//
void _dyld_objc_notify_register(_dyld_objc_notify_mapped    mapped,
                                _dyld_objc_notify_init      init,
                                _dyld_objc_notify_unmapped  unmapped);

这三个参数是:

  • map_images:这里传入的是方法引用,也就是方法的实现地址。管理文件中和动态库中所有文件,如类class协议protocol方法selector分类category的实现。
  • load_images:该方法传入的是值,即方法实现。加载执行+load方法
  • unmap_image:dyld将image移除时,会触发该函数

二. _read_images 分析

进入map_images,源码如下

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);
}

点击进入 map_images_nolock,由于里面代码比较多,直接看重点:

Xnip2022-07-15_17-18-11.png

点击进入 _read_images

void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{   
    // 1.条件控制,进行一次的加载。将所有类放入一个表中
    if (!doneOnce) { ... }
    
    // Fix up @selector references
    // 2.修复预编译阶段的 `@selector` 的混乱问题
    // 带地址的字符串匹配,不同库的坐标不一样,需要加载一块进行统一调度
    static size_t UnfixedSelectors;
    { ... }
    
    ts.log("IMAGE TIMES: fix up selector references");
    
    // Discover classes. Fix up unresolved future classes. Mark bundle classes.
    bool hasDyldRoots = dyld_shared_cache_some_image_overridden();
    
    // 3.初始化名称 - 错误混乱的类处理
    for (EACH_HEADER) { ... }
    
    ts.log("IMAGE TIMES: discover classes");
    
    // 4.修复重映射⼀些没有被镜像⽂件加载进来的 类
    if (!noClassesRemapped()) { ... }
    
    ts.log("IMAGE TIMES: remap classes");
    
#if SUPPORT_FIXUP
    // 5.修复一些消息
    // Fix up old objc_msgSend_fixup call sites
    for (EACH_HEADER) { ... }
    
    ts.log("IMAGE TIMES: fix up objc_msgSend_fixup");
#endif
    
    bool cacheSupportsProtocolRoots = sharedCacheSupportsProtocolRoots();
    
    // Discover protocols. Fix up protocol refs.
    // 6.读取协议
    for (EACH_HEADER) { ... }
    
    ts.log("IMAGE TIMES: discover protocols");
    
    // 7.修复没有被加载的协议
    for (EACH_HEADER) { ... }
    
    ts.log("IMAGE TIMES: fix up @protocol references");
    
    // 8.分类处理
    if (didInitialAttachCategories) { ... }
    
    ts.log("IMAGE TIMES: discover categories");
    
    // 9.类的加载处理 类实现
    // Realize non-lazy classes (for +load methods and static instances)
    for (EACH_HEADER) { ... }
    
    ts.log("IMAGE TIMES: realize non-lazy classes");
    
    // 10.没有被处理的类 优化那些被侵犯的类
    // Realize newly-resolved future classes, in case CF manipulates them
    if (resolvedFutureClasses) { ... }
    
    ts.log("IMAGE TIMES: realize future classes");
    
    if (DebugNonFragileIvars) {
        realizeAllClasses();
    }
}

根据log信息,可以将_read_images分为以下步骤:

  1. 条件控制,进行第一次加载
  2. 修复预编译时@selector混乱问题
  3. 错误混乱的类处理
  4. 修复重映射⼀些没有被镜像⽂件加载进来的类
  5. 修复一些消息
  6. 当我们类⾥⾯有协议的时候 读取协议
  7. 修复没有被加载的协议
  8. 分类处理
  9. 类的加载处理
  10. 没有被处理的类 优化那些被侵犯的类

2.1 只加载一次

 if(!doneOnce) {
        doneOnce = YES; // 加载一次后,就不会在进判断 doneOnce = YES
        launchTime = YES;
         ...
        // Preoptimized classes don't go in this table.
        // 4/3 is NXMapTable's load factor
        int namedClassesSize = 
        (isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;  
        // 创建哈希表 存放所有的类
        gdb_objc_realized_classes =
            NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
        ts.log("IMAGE TIMES: first time tasks");
  }

加载一次后doneOnce=YES,下次就不会在进入判断。第一次进来主要创建表gdb_objc_realized_classes,表里存放所有的类不管是实现的还是没有实现

2.2 修复@selector的混乱

static size_t UnfixedSelectors;
{
    mutex_locker_t lock(selLock);
    for (EACH_HEADER) {
        if (hi->hasPreoptimizedSelectors()) continue;
        bool isBundle = hi->isBundle();
        // 从macho文件中获取方法名列表
        SEL *sels = _getObjc2SelectorRefs(hi, &count);
        UnfixedSelectors += count;
        for (i = 0; i < count; i++) {
            const char *name = sel_cname(sels[i]);
            SEL sel = sel_registerNameNoLock(name, isBundle);
            if (sels[i] != sel) {
                sels[i] = sel;
            }
        }
    }
}

因为不同类中可能有相同的方法,虽然方法相同但是地址不同,对那些混乱的方法进行修复。因为方法是存放在类中,每个类中的位置是不一样的,所以方法的地址也就不一样

2.3 错误混乱的类处理

for (EACH_HEADER) {
    if (! mustReadClasses(hi, hasDyldRoots)) {
        // Image is sufficiently optimized that we need not call readClass()
        continue;
    }
    // 从macho中读取类列表信息
    classref_t const *classlist = _getObjc2ClassList(hi, &count);
    bool headerIsBundle = hi->isBundle();
    bool headerIsPreoptimized = hi->hasPreoptimizedClasses();

    for (i = 0; i < count; i++) {
        Class cls = (Class)classlist[i];
        Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);
        
        // 类信息发生混乱,类运行时可能发生移动,但是没有被删除,相当于常说的野指针
        if (newCls != cls  &&  newCls) {
            // Class was moved but not deleted. Currently this occurs 
            // only when the new class resolved a future class.
            // Non-lazily realize the class below.
            resolvedFutureClasses = (Class *)
                realloc(resolvedFutureClasses, 
                        (resolvedFutureClassCount+1) * sizeof(Class));
            resolvedFutureClasses[resolvedFutureClassCount++] = newCls;
        }
    }
}

_getObjc2ClassList 可执行文件machO中获取类列表,对类进行处理。进入readClass方法 查看源码实现:

Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized)
{
    // 获取类名
    const char *mangledName = cls->nonlazyMangledName();
    if (missingWeakSuperclass(cls)) { ... }
    cls->fixupBackwardDeployingStableSwift();
    Class replacing = nil;

    if (mangledName != nullptr) { ... }

    if (headerIsPreoptimized  &&  !replacing) {...
    } else {
        if (mangledName) { 
        //some Swift generic classes can lazily generate their names
            // 将类名和地址关联起来
            addNamedClass(cls, mangledName, replacing);
        } else { ...}
        // 将关联的类插入到另一张哈希表中
        addClassTableEntry(cls);
    }
    // for future reference: shared cache never contains MH_BUNDLEs
    if (headerIsBundle) { ... }
    return cls;
}

cls->nonlazyMangledName(),获取类名

  • nonlazyMangledName获取类名
  • rw的赋值和ro的获取并不在readClass里面,等下运行源码探究下
  • addNamedClass将类名和地址关联绑定起来
  • addClassTableEntry将关联的类插入到哈希表中,这张表中都是初始化过的类

2.3.1 nonlazyMangledName 类名获取

探究下 nonlazyMangledName 怎么获取类名的

const char *nonlazyMangledName() const {
    return bits.safe_ro()->getName();
}

进入safe_ro,源码如下

const class_ro_t *safe_ro() const {
    class_rw_t *maybe_rw = data();
    if (maybe_rw->flags & RW_REALIZED) {
        // maybe_rw is rw
        // rw有值 直接从rw中的ro获取
        return maybe_rw->ro();
    } else 
        // maybe_rw is actually ro
        // 直接从ro中获取,ro是macho中的数据
        return (class_ro_t *)maybe_rw;、
    }
}

2.3.2 addNamedClass 类名和地址绑定

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 {
        // 更新gdb_objc_realized_classes表,将key设置为 name value 设置为cls
        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());
}

更新gdb_objc_realized_classes哈希表,keynamevaluecls

2.3.3. addClassTableEntry 插入另一张表

rw的赋值和ro的获取并不在readClass里面

static void
addClassTableEntry(Class cls, bool addMeta = true)
{
    runtimeLock.assertLocked();
    // This class is allowed to be a known class via the shared cache or via
    // data segments, but it is not allowed to be in the dynamic 
    //table already.
    // allocatedClasses
    auto &set = objc::allocatedClasses.get();
    ASSERT(set.find(cls) == set.end());
    if (!isKnownClass(cls))
        set.insert(cls);
    if (addMeta)
        // 将元类插入哈希表中
        addClassTableEntry(cls->ISA(), false);
}
  • allocatedClasses_objc_initruntime_init运行时环境初始化,里面主要是unattachedCategoriesallocatedClasses两张表,此时插入allocatedClasses表中
  • addMeta = true 将元类添加allocatedClasses表中

Xnip2022-07-15_19-07-55.png

2.4 类的加载处理

类的加载处理是比较复杂和重要的,这先引出一点,查看源码:

Xnip2022-07-15_19-25-49.png

注释很明显的提示初始化非懒加载类,那么什么是非懒加载类呢?其实就是实现了load方法或者静态实例方法的类

现在给YJPerson 实现 load 和不实现 load 分别运行

  • 未实现 load 方法:

Xnip2022-07-15_19-40-35.png

  • 实现 load 方法:

Xnip2022-07-15_19-41-08.png

nlclslist 里面调用的 _getObjc2NonlazyClassList方法就是从macho 中获取非懒加载列表,从 machoSection__objc_nlclslist 里获取,简单验证下:

Xnip2022-07-15_19-58-51.png

使用 MachOView 打开 项目 MachO 文件,找到 __objc_nlclslist Xnip2022-07-15_20-00-55.png

__objc_nlclslist 列表中一条数据(16字节,分高底位),第一个8字节(低位)是的数据是0x00000001000081c8YJTeacher 类;第二个8字节(高位)是 0x0000000100008281YJPerson类(因为iOS是小端模式从右往左读)

realizeClassWithoutSwift这个方法,后面会进行详细的探究。。。

总结

dyld_objc_init 再到 read_images 这整个流程的探究逐渐的串联起来了。许多知识也有点到面,脉络越来越清晰。后面就是非常重要详细的类的加载