iOS探索 分类、类拓展的加载过程

5,565

欢迎阅读iOS探索系列(按序阅读食用效果更加)

写在前面

上篇文章iOS探索 类的加载过程分析了类的加载过程,本文就来好好聊聊分类加载的那些事

(请先对类的加载过程有了一定了解之后再开启本文)

一、分类初探

1.clang输出cpp

FXPerson新建一个分类FXPerson-FX

终端利用clang输出cpp

clang -rewrite-objc FXPerson+FX.m -o cate.cpp

2.底层分析

从cpp文件最下面看起,首先看到分类是存储在MachO文件的__DATA段的__objc_catlist

static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
	&_OBJC_$_CATEGORY_FXPerson_$_FX,
};

其次能看到FXPerson分类的结构

static struct _category_t _OBJC_$_CATEGORY_FXPerson_$_FX __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
	"FXPerson",
	0, // &OBJC_CLASS_$_FXPerson,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_FXPerson_$_FX,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_FXPerson_$_FX,
	0,
	(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_FXPerson_$_FX,
};

来到objc源码中搜索category_t查看底层中分类的结构(_category_t搜索无果)

struct category_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods;
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

根据FXPerson分类结构和底层分类结构对比:

  • name:类的名字,不是分类的名字
  • cls:类对象
  • instanceMethods:分类上存储的实例方法
  • classMethods:分类上存储的类方法
  • protocols:分类上所实现的协议
  • instanceProperties:分类所定义的实例属性,不过我们一般在分类中添加属性都是通过关联对象来实现的
  • _classProperties:分类所定义的类属性

为什么分类的方法要将实例方法和类方法分开存呢?

因为类和元类之前在不断编译,实例方法存在类中,类方法存在元类中,已经确定好其方法归属的地方;而分类是后面才加进来的

二、分类的加载

通过上一篇文章我们知道了类分为懒加载类非懒加载类,他们的加载时机不一样,那么分类又是如何呢?下面我们就依次来进行探究

1.懒加载类和懒加载分类

类、分类均不实现+load方法

已知懒加载类很累,只有调用它发送消息时才会加载

而添加分类在两处出现了:_read_imagesmethodizeClass,我们不妨来尝试一下

// Discover categories.
// 发现和处理所有Category
for (EACH_HEADER) {
    // 外部循环遍历找到当前类,查找类对应的Category数组
    category_t **catlist = 
        _getObjc2CategoryList(hi, &count);
    bool hasClassProperties = hi->info()->hasCategoryClassProperties();

    for (i = 0; i < count; i++) {
        // 内部循环遍历当前类的所有Category
        category_t *cat = catlist[i];
        Class cls = remapClass(cat->cls);
        
        // 首先,通过其所属的类注册Category。如果这个类已经被实现,则重新构造类的方法列表。
        bool classExists = NO;
        if (cat->instanceMethods ||  cat->protocols  
            ||  cat->instanceProperties) 
        {
            // 将Category添加到对应Class的value中,value是Class对应的所有category数组
            addUnattachedCategoryForClass(cat, cls, hi);
            // 将Category的method、protocol、property添加到Class
            if (cls->isRealized()) {
                remethodizeClass(cls);
                classExists = YES;
            }
            if (PrintConnecting) {
                _objc_inform("CLASS: found category -%s(%s) %s", 
                             cls->nameForLogging(), cat->name, 
                             classExists ? "on existing class" : "");
            }
        }

        // 这块和上面逻辑一样,区别在于这块是对Meta Class做操作,而上面则是对Class做操作
        // 根据下面的逻辑,从代码的角度来说,是可以对原类添加Category的
        if (cat->classMethods  ||  cat->protocols  
            ||  (hasClassProperties && cat->_classProperties)) 
        {
            addUnattachedCategoryForClass(cat, cls->ISA(), hi);
            if (cls->ISA()->isRealized()) {
                remethodizeClass(cls->ISA());
            }
            if (PrintConnecting) {
                _objc_inform("CLASS: found category +%s(%s)", 
                             cls->nameForLogging(), cat->name);
            }
        }
    }
}
static void methodizeClass(Class cls)
{
    ...
    // Attach categories.
    category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
    attachCategories(cls, cats, false /*don't flush caches*/);
    ...
}

为了避免其他类调用_read_imagesmethodizeClass(红色框)难以调试,分别往这两处添点代码(绿色框)从而更好的研究FXPerson的类和分类

运行代码,接下来就等鱼儿上钩了

从左边的函数调用栈可以得出:

  1. 懒加载类发送消息,lookupOrForward->realizeClassMaybeSwiftAndLeaveLocked
  2. realizeClassMaybeSwiftMaybeRelock->realizeClassWithoutSwift开始加载内存
  3. methodizeClass处理父类、元类关系,调用了两次打印
  4. unattachedCategoriesForClass返回NULL
  5. 另一处_read_images加载分类没有调用

一不小心就翻车了,先换非懒加载类和懒加载分类情况研究吧

2.非懒加载类和懒加载分类

只有类实现+load方法

①同样的研究方法,运行项目

查看函数调用栈得出:(不明白的可以阅读浅尝辄止dyld加载流程类的加载过程

  1. 程序启动dyld->libSystem_initializer->libdispatch_init->_os_object_init
  2. _objc_init->map_images->map_images_nolock->_read_images
  3. realizeClassWithoutSwift->methodizeClass加载类到内存中
  4. methodizeClass处理父类、元类关系,调用了两次打印
  5. unattachedCategoriesForClass返回NULL
  6. 另一处_read_images加载分类没有调用

②又是和懒加载类和懒加载分类一样的情况...继续探索一下rw(看不懂就阅读类的结构分析

(lldb) p/x cls
(Class) $0 = 0x0000000100001188
(lldb) p (class_data_bits_t *)0x00000001000011a8
(class_data_bits_t *) $1 = 0x00000001000011a8
(lldb) p $1->data()
(class_rw_t *) $2 = 0x0000000103200060
(lldb) p *$2
(class_rw_t) $3 = {
  flags = 2148007936
  version = 7
  ro = 0x00000001000010e8
  methods = {
    list_array_tt<method_t, method_list_t> = {
       = {
        list = 0x00000001000010b0
        arrayAndFlag = 4294971568
      }
    }
  }
  properties = {
    list_array_tt<property_t, property_list_t> = {
       = {
        list = 0x0000000000000000
        arrayAndFlag = 0
      }
    }
  }
  protocols = {
    list_array_tt<unsigned long, protocol_list_t> = {
       = {
        list = 0x0000000000000000
        arrayAndFlag = 0
      }
    }
  }
  firstSubclass = nil
  nextSiblingClass = 0x00007fff9383faa0
  demangledName = 0x0000000000000000
}
(lldb) p $3.methods
(method_array_t) $4 = {
  list_array_tt<method_t, method_list_t> = {
     = {
      list = 0x00000001000010b0
      arrayAndFlag = 4294971568
    }
  }
}
(lldb) p $4.list
(method_list_t *) $5 = 0x00000001000010b0
(lldb) p *$5
(method_list_t) $6 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 2
    first = {
      name = "cate_doClass"
      types = 0x0000000100000faa "v16@0:8"
      imp = 0x0000000100000e00 (objc-debug`+[FXPerson(FX) cate_doClass] at FXPerson+FX.m:23)
    }
  }
}
(lldb) p $5->get(0)
(method_t) $7 = {
  name = "cate_doClass"
  types = 0x0000000100000faa "v16@0:8"
  imp = 0x0000000100000e00 (objc-debug`+[FXPerson(FX) cate_doClass] at FXPerson+FX.m:23)
}
(lldb) p $5->get(1)
(method_t) $8 = {
  name = "load"
  types = 0x0000000100000faa "v16@0:8"
  imp = 0x0000000100000e90 (objc-debug`+[FXPerson load] at FXPerson.m:12)
}
(lldb) 

第一次调用先处理元类关系metacls = realizeClassWithoutSwift(remapClass(cls->ISA()));

元类中存下了FXPerson类+load方法和FXPerson分类+cate_doClass方法

第二次本类中存下了-cate_doInstance方法

说明在methodizeClassunattachedCategoriesForClass前已经把分类的方法加载到类中

③修改一下源码,将调试代码放到操作rwmethods之前,发现此时的methods还没赋值

(lldb) p *$5
error: Couldn't apply expression side effects : Couldn't dematerialize a result variable: couldn't read its memory
methodizeClass:类名 :FXPerson - 0x100001188 

断点来到methods赋值之后,分类的方法已经躺在里面了

method_list_t *list = ro->baseMethods() 这一步只是对ro->baseMethods放到rw中

④重新跑项目,在第一个断点处打印ro,分类方法已经存在了...

结论: 不管是懒加载类或是非懒加载类懒加载分类在编译时就确定了

3.非懒加载类和非懒加载分类

类、分类均实现+load方法

恢复成最初的调试代码,运行项目

①首先断点来到methodizeClass

两次unattachedCategoriesForClass返回的都是NULL

②其次断点来到_read_imagesDiscover categories,按照图中的顺序依次调用

  • addUnattachedCategoryForClass把类/元类和分类做一个关联映射
  • 调用remethodizeClass调用attachCategories处理分类
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);
    }
}

remethodizeClass调用attachCategories处理分类

static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    if (!cats) return;
    if (PrintReplacedMethods) printReplacements(cls, cats);

    bool isMeta = cls->isMetaClass();

    // fixme rearrange to remove these intermediate allocations
    method_list_t **mlists = (method_list_t **)
        malloc(cats->count * sizeof(*mlists));
    property_list_t **proplists = (property_list_t **)
        malloc(cats->count * sizeof(*proplists));
    protocol_list_t **protolists = (protocol_list_t **)
        malloc(cats->count * sizeof(*protolists));

    // Count backwards through cats to get newest categories first
    int mcount = 0;
    int propcount = 0;
    int protocount = 0;
    int i = cats->count;
    bool fromBundle = NO;
    while (i--) {
        auto& entry = cats->list[i];

        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle();
        }

        property_list_t *proplist = 
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            proplists[propcount++] = proplist;
        }

        protocol_list_t *protolist = entry.cat->protocols;
        if (protolist) {
            protolists[protocount++] = protolist;
        }
    }

    auto rw = cls->data();

    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
    if (flush_caches  &&  mcount > 0) flushCaches(cls);

    rw->properties.attachLists(proplists, propcount);
    free(proplists);

    rw->protocols.attachLists(protolists, protocount);
    free(protolists);
}

attachCategories分析:

  • (假如两个分类都实现了+loadwhile (i--)中并不是网上所说的按照Compile Sources倒序加载的,先加载的是FXPerson+FX,至于这里这么写可能是为了方便吧
  • 调用attachLists添加分类的方法、属性、协议(类的加载过程有详细介绍)
    • memmove将原数据移到末尾
    • memcpy把新数据拷贝到起始位置
  • 分类的方法没有替换掉类已经有的方法,也就是说如果分类和类都有doInstance,那么分类附加完成之后,类的方法列表里会有两个doInstance
  • 分类的方法被放到了新方法列表的前面,而类的方法被放到了新方法列表的后面,这也就是我们平常所说的分类的方法会“覆盖”掉类的同名方法,这是因为运行时查找方法时是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会返回imp,殊不知后面可能还有一样名字的方法

4.懒加载类和非懒加载分类

只有分类实现+load方法

①首先断点来到_read_imagesDiscover categories,不走remethodizeClass

②断点来到methodizeClass,这次终于通过unattachedCategoriesForClass取到值了,然后通过attachCategories添加

注意一下函数调用栈:load_images->prepare_load_methods->realizeClassWithoutSwift->...->methodizeClass(这个知识点后面会提到)

5.类和分类加载总结

  • 懒加载类 + 懒加载分类

    • 类的加载在第一次消息发送的时候,而分类的加载则在编译时
  • 非懒加载类 + 懒加载分类

    • 类的加载在_read_images处,分类的加载则在编译时
  • 非懒加载类 + 非懒加载分类

    • 类的加载在_read_images处,分类的加载在类加载之后的reMethodizeClass
  • 懒加载类 + 非懒加载分类

    • 类的加载在load_images处,分类的加载在类加载之后的methodizeClass

6.类和分类的同名方法之争

  • 类和分类方法同名时,必定响应分类方法(不管类和分类是否实现+load
  • 类和多个分类方法同名时
    • 如果分类没实现+load方法,就响应Compile Sources最后一个分类
    • 如果其中一个实现了+load,响应非懒加载分类——因为懒加载分类在编译时就已经加载到内存,而非懒加载分类运行时才加载
    • 如果都实现+load,响应Compile Sources最后一个分类

三、类拓展的加载

类拓展extension又称作匿名的分类,为了给当前类增加属性方法

具体由两种形式:

  • 直接在.m文件中新增类拓展
  • 新建类拓展的.h文件

1.类拓展的加载

数据很早的时候都会来到_read_image,那正好在处理类时使用我们的惯用伎俩

但是仔细一想不对呀,已经在类中有了方法实现了,此时的do_hExtension不足以说明问题

那么可以通过查看属性的settergetter方法来验证

通过上图就可以得出:

  • 类拓展在编译时便作为类的一部分进行编译
  • 类拓展在读取时直接读取ro

2.类拓展的细节点

如果类拓展没有被引用(#import)就不会编译到到内存中

四、load_image

上篇文章讲到dyld初始化image会触发load_image,本文又提到了懒加载类非懒加载分类情况下,分类加载到内存时的调用栈中有load_image,那么我们在该种情况下进行探索

load_image实现处打下断点,发现类和分类都没有打印+load方法——load_image先于+load方法

接着把目光移向两条注释:

  • Discover load methods——prepare_load_methods
  • Call +load methods——call_load_methods

1.prepare_load_methods

发现并准备+load方法

void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;

    runtimeLock.assertLocked();

    classref_t *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        schedule_class_load(remapClass(classlist[i]));
    }

    category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
    for (i = 0; i < count; i++) {
        category_t *cat = categorylist[i];
        Class cls = remapClass(cat->cls);
        if (!cls) continue;  // category for ignored weak-linked class
        if (cls->isSwiftStable()) {
            _objc_fatal("Swift class extensions and categories on Swift "
                        "classes are not allowed to have +load methods");
        }
        realizeClassWithoutSwift(cls);
        assert(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }
}

/***********************************************************************
* prepare_load_methods
* Schedule +load for classes in this image, any un-+load-ed 
* superclasses in other images, and any categories in this image.
**********************************************************************/
// Recursively schedule +load for cls and any un-+load-ed superclasses.
// cls must already be connected.
static void schedule_class_load(Class cls)
{
    if (!cls) return;
    assert(cls->isRealized());  // _read_images should realize

    if (cls->data()->flags & RW_LOADED) return;

    // Ensure superclass-first ordering
    schedule_class_load(cls->superclass);

    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}

/***********************************************************************
* add_class_to_loadable_list
* Class cls has just become connected. Schedule it for +load if
* it implements a +load method.
**********************************************************************/
void add_class_to_loadable_list(Class cls)
{
    IMP method;

    loadMethodLock.assertLocked();

    method = cls->getLoadMethod();
    if (!method) return;  // Don't bother if cls has no +load method
    
    if (PrintLoading) {
        _objc_inform("LOAD: class '%s' scheduled for +load", 
                     cls->nameForLogging());
    }
    
    if (loadable_classes_used == loadable_classes_allocated) {
        loadable_classes_allocated = loadable_classes_allocated*2 + 16;
        loadable_classes = (struct loadable_class *)
            realloc(loadable_classes,
                              loadable_classes_allocated *
                              sizeof(struct loadable_class));
    }
    
    loadable_classes[loadable_classes_used].cls = cls;
    loadable_classes[loadable_classes_used].method = method;
    loadable_classes_used++;
}

prepare_load_methods分析:

  • 通过_getObjc2NonlazyClassList获取非懒加载类列表
  • 通过schedule_class_load遍历这些类
    • 递归调用遍历父类的+load方法,确保父类的+load方法顺序排在子类的前面
    • 调用add_class_to_loadable_list把类的+load方法存在loadable_classes里面
  • 调用_getObjc2NonlazyCategoryList取出非懒加载分类列表
  • 遍历分类列表
    • 通过realizeClassWithoutSwift来防止类没有初始化(若已经初始化了则不影响)
    • 调用add_category_to_loadable_list加载分类中的+load方法到loadable_categories

此时就能看懂之前懒加载类非懒加载分类的函数调用栈了

2.call_load_methods

唤醒+load方法

void call_load_methods(void)
{
    static bool loading = NO;
    bool more_categories;

    loadMethodLock.assertLocked();

    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush();

    do {
        // 1. Repeatedly call class +loads until there aren't any more
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        // 2. Call category +loads ONCE
        more_categories = call_category_loads();

        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}
  • 通过objc_autoreleasePoolPush压栈一个自动释放池
  • do-while循环开始
    • 循环调用类的+load方法直到找不到为止
    • 调用一次分类中的+load方法
  • 通过 objc_autoreleasePoolPop`出栈一个自动释放池

五、initalize分析

关于initalize苹果文档是这么描述的

Initializes the class before it receives its first message.

在这个类接收第一条消息之前调用。

Discussion

The runtime sends initialize to each class in a program exactly one time just before the class, or any class that inherits from it, is sent its first message from within the program. (Thus the method may never be invoked if the class is not used.) The runtime sends the initialize message to classes in a thread-safe manner. Superclasses receive this message before their subclasses.

Runtime在一个程序中每一个类的一个程序中发送一个初始化一次,或是从它继承的任何类中,都是在程序中发送第一条消息。(因此,当该类不使用时,该方法可能永远不会被调用。)运行时发送一个线程安全的方式初始化消息。父类的调用一定在子类之前。

然后我们在objc源码lookUpImpOrForward找到了它的踪迹

lookUpImpOrForward->initializeAndLeaveLocked->initializeAndMaybeRelock->initializeNonMetaClass

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    ...
    if (initialize && !cls->isInitialized()) {
        cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
        ...
    }
    ...
}

static Class initializeAndLeaveLocked(Class cls, id obj, mutex_t& lock)
{
    return initializeAndMaybeRelock(cls, obj, lock, true);
}

static Class initializeAndMaybeRelock(Class cls, id inst,
                                      mutex_t& lock, bool leaveLocked)
{
    ···
    initializeNonMetaClass(nonmeta);
    ···
}

initializeNonMetaClass递归调用父类initialize,然后调用callInitialize

/***********************************************************************
* class_initialize.  Send the '+initialize' message on demand to any
* uninitialized class. Force initialization of superclasses first.
**********************************************************************/
void initializeNonMetaClass(Class cls)
{
    ...
    supercls = cls->superclass;
    if (supercls  &&  !supercls->isInitialized()) {
        initializeNonMetaClass(supercls);
    }
    ...
    {
            callInitialize(cls);

            if (PrintInitializing) {
                _objc_inform("INITIALIZE: thread %p: finished +[%s initialize]",
                             pthread_self(), cls->nameForLogging());
            }
        }
    ...
}

callInitialize是一个普通的消息发送

void callInitialize(Class cls)
{
    ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
    asm("");
}

关于initalize的结论:

  • initialize在类或者其子类的第一个方法被调用前(发送消息前)调用
  • 只在类中添加initialize但不使用的情况下,是不会调用initialize
  • 父类的initialize方法会比子类先执行
  • 当子类未实现initialize方法时,会调用父类initialize方法;子类实现initialize方法时,会覆盖父类initialize方法
  • 当有多个分类都实现了initialize方法,会覆盖类中的方法,只执行一个(会执行最后被加载到内存中的分类的方法)

写在后面

类结构消息发送dyld类与分类的加载过程,笔者已经将加载->使用的流程进行一小波探究之后,接下来将开始干货分享——底层面试题