iOS 分類的加載

前言

  通過上一篇對類加載的分析探索,我們瞭解了dyld進入程序,加載完鏡像文件,在objc_init方法中註冊回調函數,然後通過map_images的一系列操作,將其加載到內存中。
map_images中,通過_read_images方法,先創建表,遍歷所有的類將其映射到表中,然後將SEL協議添加到對應的表中,對類和非懶加載類進行初始化,對rorw賦值等等一系列流程。那麼今天我們先來看幾道關於rorw的面試題,然後再瞭解一下類和非懶加載類以及分類Category的加載。

1.  Runtime 面試題

問 :可否給類動態添加成員變量?爲什麼?

答 :動態創建的類,可以添加成員變量,已經註冊好的類,不能動態添加成員變量。

分析如下:

首先,我們使用Runtime API編寫下面代碼:

// 1: 動態創建類
Class LGPerson = objc_allocateClassPair([NSObject class], "LGPerson", 0);
// 2: 添加成員變量 
// ivar - ro - ivarlist
class_addIvar(LGPerson, "lgName", sizeof(NSString *), log2(sizeof(NSString *)), "@");
// 3: 註冊到內存
objc_registerClassPair(LGPerson);

通過上面代碼進行 動態創建類添加成員變量、然後註冊到內存,運行代碼,程序可以正常運行。

當我們對 步驟二步驟三 順序互換,先註冊到內存,再添加成員變量,此時,程序就會崩潰,接下來通過源碼來分析一下。

通過之前的學習,知道,成員變量是存儲在Classclass_rw_t *data()中的ro中的ivar_list_t * ivars裏面。如下源碼:

objc_class源碼

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

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

class_rw_t源碼:

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_ro_t源碼:

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

通過上一篇的學習,我們知道ro中在程序編譯時就進行賦值的,只能讀取,不能進行改變,rw是在對類初始化時,進行賦值的。而rw—>ro的賦值也是在這個時候完成。

我們查看Runtime將註冊類的API objc_registerClassPair,源碼如下:

/***********************************************************************
* objc_registerClassPair
* fixme
* Locking: acquires runtimeLock
**********************************************************************/
void objc_registerClassPair(Class cls)
{
    mutex_locker_t lock(runtimeLock);

    checkIsKnownClass(cls);

    if ((cls->data()->flags & RW_CONSTRUCTED)  ||
        (cls->ISA()->data()->flags & RW_CONSTRUCTED)) 
    {
        _objc_inform("objc_registerClassPair: class '%s' was already "
                     "registered!", cls->data()->ro->name);
        return;
    }

    if (!(cls->data()->flags & RW_CONSTRUCTING)  ||  
        !(cls->ISA()->data()->flags & RW_CONSTRUCTING))
    {
        _objc_inform("objc_registerClassPair: class '%s' was not "
                     "allocated with objc_allocateClassPair!", 
                     cls->data()->ro->name);
        return;
    }

    // Clear "under construction" bit, set "done constructing" bit
    // 替換
    cls->ISA()->changeInfo(RW_CONSTRUCTED, RW_CONSTRUCTING | RW_REALIZING);
    cls->changeInfo(RW_CONSTRUCTED, RW_CONSTRUCTING | RW_REALIZING);

    // Add to named class table.
    addNamedClass(cls, cls->data()->ro->name);
}

其中關鍵步驟

cls->ISA()->changeInfo(RW_CONSTRUCTED, RW_CONSTRUCTING | RW_REALIZING);
cls->changeInfo(RW_CONSTRUCTED, RW_CONSTRUCTING | RW_REALIZING);

在註冊時,將RW_CONSTRUCTING | RW_REALIZING替換爲RW_CONSTRUCTED

通過查看動態添加成員變量class_addIvar API

BOOL 
class_addIvar(Class cls, const char *name, size_t size, 
              uint8_t alignment, const char *type)
{
    if (!cls) return NO;

    if (!type) type = "";
    if (name  &&  0 == strcmp(name, "")) name = nil;

    mutex_locker_t lock(runtimeLock);

    checkIsKnownClass(cls);
    assert(cls->isRealized());

    // No class variables
    if (cls->isMetaClass()) {
        return NO;
    }

    // Can only add ivars to in-construction classes.
    if (!(cls->data()->flags & RW_CONSTRUCTING)) {
        return NO;
    }

    // Check for existing ivar with this name, unless it's anonymous.
    // Check for too-big ivar.
    // fixme check for superclass ivar too?
    if ((name  &&  getIvar(cls, name))  ||  size > UINT32_MAX) {
        return NO;
    }

    class_ro_t *ro_w = make_ro_writeable(cls->data());

    // fixme allocate less memory here
    
    ivar_list_t *oldlist, *newlist;
    if ((oldlist = (ivar_list_t *)cls->data()->ro->ivars)) {
        size_t oldsize = oldlist->byteSize();
        newlist = (ivar_list_t *)calloc(oldsize + oldlist->entsize(), 1);
        memcpy(newlist, oldlist, oldsize);
        free(oldlist);
    } else {
        newlist = (ivar_list_t *)calloc(sizeof(ivar_list_t), 1);
        newlist->entsizeAndFlags = (uint32_t)sizeof(ivar_t);
    }

    uint32_t offset = cls->unalignedInstanceSize();
    uint32_t alignMask = (1<<alignment)-1;
    offset = (offset + alignMask) & ~alignMask;

    ivar_t& ivar = newlist->get(newlist->count++);
#if __x86_64__
    // Deliberately over-allocate the ivar offset variable. 
    // Use calloc() to clear all 64 bits. See the note in struct ivar_t.
    ivar.offset = (int32_t *)(int64_t *)calloc(sizeof(int64_t), 1);
#else
    ivar.offset = (int32_t *)malloc(sizeof(int32_t));
#endif
    *ivar.offset = offset;
    ivar.name = name ? strdupIfMutable(name) : nil;
    ivar.type = strdupIfMutable(type);
    ivar.alignment_raw = alignment;
    ivar.size = (uint32_t)size;

    ro_w->ivars = newlist;
    cls->setInstanceSize((uint32_t)(offset + size));

    // Ivar layout updated in registerClass.

    return YES;
}

看上面源碼中的關鍵代碼,

    // Can only add ivars to in-construction classes.
    if (!(cls->data()->flags & RW_CONSTRUCTING)) {
        return NO;
    }

當我們在註冊類時,對cls->ISA()->changeInfocls->changeInfo進行修改,所以在上面判斷中直接返回NO,而不會在後續對ro中的ivar賦值。所以不能對註冊好的類,進行動態添加成員變量。

那麼接下來對LGPerson類,添加屬性,並打印,如下代碼,問能否打印?爲什麼?

定義如下方法:

void lg_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);

}

添加屬性到rw

// 添加property - rw
lg_class_addProperty(LGPerson, "subject");

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

答案:不能打印,我們只添加了subject屬性到rw,由於是我們動態添加的,系統並未生成settergetter,而賦值打印相當於調用了這兩個方法,所以不能打印。

我們需要添加如下代碼,將settergetter添加到方法列表中。

添加setter  +  getter 方法
class_addMethod(LGPerson, @selector(setSubject:), (IMP)lgSetter, "v@:@");
class_addMethod(LGPerson, @selector(subject), (IMP)lgName, "@@:");

小結:

1. 動態創建的類,可以動態添加成員變量和屬性,而已經創建好註冊好的類,不能動態添加成員變量。
2. 動態添加的屬性,默認不會生成 setter 和 getter,需要將setter  +  getter 
   方法添加到方法列表中。

2.  類和非懶加載類的加載

2.1  類和非懶加載類分析

通過上一篇的學習得知,在_read_images()方法中,在將類添加到表中之後,會進行一系列的操作,比如:將SEL註冊到哈希表中、將協議添加到表中、初始化非懶加載類、初始化懶加載類、處理分類category等。

那麼什麼是懶加載類,什麼是非懶加載類呢?

接下來,先創建LGTeacherLGStudentLGPerson三個類,在其中前兩個類中,實現如下load方法:

+(void)load
{
    NSLog(@"%s",__func__);
}

然後在_read_images()方法中,初始化非懶加載類的時候,打印類信息,如下

然後運行代碼,查看控制檯,

只打印了有load方法的兩個類,並未找到沒有實現load方法的LGPerson,而LGPerson是在main函數裏進行了調用,實例出一個對象。

由此:
load方法會將類的加載提前,將類的編譯期提前到加載數據的地方。而實現了load方法的類,就是非懶加載類。而懶加載類是當你用到此類的時候再進行加載實現。

接下來,我們分析一下 非懶加載類的加載懶加載類的加載

2.2  非懶加載類的加載

在上一篇中分析中得知,當進入_read_images()方法中的非懶加載類的加載步驟後,流程如下:

  • 獲取所有非懶加載類classref_t *classlist = _getObjc2NonlazyClassList(hi, &count)
  • 循環讀取非懶加載類,將其加到內存,addClassTableEntry(cls)
  • 實現所有非懶加載類(實例化類對象的一些信息,例如rw),realizeClassWithoutSwift(cls)
  • realizeClassWithoutSwift(cls)中,對clssupClassisa以及rw->ro等進行賦值,然後進入methodizeClass(cls),用ro中的數據對rw進行賦值。

rw->ro的賦值

2.3  懶加載類的加載

懶加載類是在調用的時候纔會加載,那麼我們在main函數中,創建LGPerson,然後打下斷點,進行分析。

當我們調用alloc方法時,會進入方法查找流程,必然會進入lookUpImpOrForward方法。

然後判斷進入realizeClassMaybeSwiftAndLeaveLocked方法,

查看realizeClassMaybeSwiftAndLeaveLocked方法源碼:

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

realizeClassMaybeSwiftAndLeaveLocked()調用realizeClassMaybeSwiftMaybeRelock()方法,源碼如下:

/***********************************************************************
* realizeClassMaybeSwift (MaybeRelock / AndUnlock / AndLeaveLocked)
* Realize a class that might be a Swift class.
* Returns the real class structure for the class. 
* Locking: 
*   runtimeLock must be held on entry
*   runtimeLock may be dropped during execution
*   ...AndUnlock function leaves runtimeLock unlocked on exit
*   ...AndLeaveLocked re-acquires runtimeLock if it was dropped
* This complication avoids repeated lock transitions in some cases.
**********************************************************************/
static Class
realizeClassMaybeSwiftMaybeRelock(Class cls, mutex_t& lock, bool leaveLocked)
{
    lock.assertLocked();
    // 判斷是否是Swift
    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;
}

通過上面realizeClassMaybeSwiftMaybeRelock源碼分析,先判斷是否是Swift,不是,則調用realizeClassWithoutSwift(cls)對類初始化,對superClassisarw賦值,和上面的非懶加載類一樣。

當類初始化完成後,會進入lookUpImpOrForward的方法查找流程,然後進行初始化,分配空間等等的操作。

注意:
當一個類繼承另一個類的時候,子類實現了load方法,子類變成了非懶加載類,而父類也會變成非懶加載類。通過realizeClassWithoutSwift方法中的下面的代碼,遞歸對supercls初始化。

3.  分類 Category 的加載

首先,我們創建一個LGTeacher類和LGTeacher (test)分類如下:

@interface LGTeacher : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *subject;
@property (nonatomic, assign) int age;

- (void)sayHello;

+ (void)sayMaster;
@end

#import "LGTeacher.h"


@implementation LGTeacher

//+ (void)load{
//    NSLog(@"%s",__func__);
//}
@end

@interface LGTeacher (test)
@property (nonatomic, copy) NSString *cate_p1;
@property (nonatomic, copy) NSString *cate_p2;

- (void)cate_instanceMethod1;
- (void)cate_instanceMethod2;

+ (void)cate_classMethod1;
+ (void)cate_classMethod2;

@end

@implementation LGTeacher (test)

//+ (void)load{
//    NSLog(@"分類 load");
//}

- (void)setCate_p1:(NSString *)cate_p1{
}

- (NSString *)cate_p1{
    return @"cate_p1";
}

- (void)cate_instanceMethod2{
    NSLog(@"%s",__func__);
}

+ (void)cate_classMethod2{
    NSLog(@"%s",__func__);
}
@end

3.1 clang 初探 分類 Category 的結構

通過clang,查看c++文件,

_category_t的結構:

// attachlist 方法 對象 C++
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;
};
// OC
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);
};

爲什麼會有兩個方法列表呢?
對象方法存在類中,類方法存在元類中。當通過attachLists方法添加方法時,需要添加到不同的類中。

3.2 類 與 分類 Category 的搭配加載

1. 懶加載的分類(未實現load方法)

我們知道在read_iamge方法中,在類初始化時,是通過在methodizeClass方法中,用rorw進行賦值的,通過attachCategories方法,將分類中的方法添加到方法列表中的。

通過斷點調試,判斷爲LGTeacher時,


category_listNULL,當爲NULL時,attachCategories直接返回,

那麼分類的方法是否添加呢?我們通過lldb分析一下:

methods裏面有值,

打印methods裏面的方法,

我們發現,方法列表中已經有分類的方法,那麼說明,懶加載的分類的加載時在編譯時處理的,不需要添加到表中,直接添加到相應data()ro裏面,然後在初始化類的時候,直接用ro->baseMethods()rw->methods賦值。即下面的代碼

    // Install methods and properties that the class implements itself.
    method_list_t *list = ro->baseMethods();
    if (list) {
        prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
        rw->methods.attachLists(&list, 1);
    }

那麼 懶加載的分類(未實現load方法) 搭配 懶加載類非懶加載類 時,又會出現兩中情況。

上面說的是搭配 懶加載類 的情況,而兩者的分類的加載都是一樣的,是在編譯期進行處理,直接添加到相應data()ro中,主要的區別就是在對類的加載,上面的類和非懶加載類的加載已經說過,
懶加載類是在發送消息時,通過lookuporforward->realizeClassWithoutSwift->methodlizeClass的流程加載的。而非懶加載類是通過read_images->realizeClassWithoutSwift->methodlizeClass的流程加載的.

2. 非懶加載的分類(實現load方法)

分類實現load方法時,其加載也會被提前,即read_iamges方法中對分類的處理,如下關鍵代碼:

for (i = 0; i < count; i++) {
            category_t *cat = catlist[i];
            Class cls = remapClass(cat->cls);
   
            if (!cls) {
                // Category's target class is missing (probably weak-linked).
                // Disavow any knowledge of this category.
                catlist[i] = nil;
                if (PrintConnecting) {  }
                continue;
            }

            // Process this category. 
            // First, register the category with its target class. 
            // Then, rebuild the class's method lists (etc) if 
            // the class is realized. 
            bool classExists = NO;
            // ✅判斷是否是對象方法
            if (cat->instanceMethods ||  cat->protocols  
                ||  cat->instanceProperties) 
            {
                // ✅爲類添加未附加的類別
                addUnattachedCategoryForClass(cat, cls, hi);
                const char *cname = cls->demangledName();
                const char *oname = "LGTeacher";
                if (cname && (strcmp(cname, oname) == 0)) {
                    printf("_getObjc2CategoryList :%s \n",cname);
                }
                // ✅爲判斷是否是懶加載類
                if (cls->isRealized()) {
                    remethodizeClass(cls);
                    classExists = YES;
                }
                if (PrintConnecting) { }
            }
            // // ✅判斷是否是類方法
            if (cat->classMethods  ||  cat->protocols  
                ||  (hasClassProperties && cat->_classProperties)) 
            {
                addUnattachedCategoryForClass(cat, cls->ISA(), hi);
                // ✅爲判斷是否是懶加載類
                if (cls->ISA()->isRealized()) {
                    remethodizeClass(cls->ISA());
                }
                if (PrintConnecting) {   }
            }
        }
    }

先讀取分類,判斷分類中的方法是否是對象方法,然後addUnattachedCategoryForClass爲類添加未附加的類別。然後判斷是否是懶加載類,不是懶加載類則調用remethodizeClass方法,將分類方法添加的主類的方法列表中

remethodizeClass方法源碼:

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

懶加載類的加載是在發送消息時,通過lookuporforward->realizeClassWithoutSwift->methodlizeClass的流程加載的。

所以,當搭配非懶加載類的時候,會進入remethodizeClass方法,進而調用attachCategories()方法,將分類的方法貼到主類裏面。

那麼當非懶加載分類搭配懶加載類的時候,此時就會出現,分類已經加載,而主類還未加載。分類的方法不知要添加到哪裏主類裏面去,那這樣的分類提前加載是不是沒有意義呢?

當然不是,那麼接下來,系統會調用下面方法,


最終進入到prepare_load_methods方法中,然後調用realizeClassWithoutSwift方法,源碼如下:

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]));
    }
    // map_images 完畢了
    category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
    for (i = 0; i < count; i++) {
        category_t *cat = categorylist[i];
        Class cls = remapClass(cat->cls);
        
        const class_ro_t *ro = (const class_ro_t *)cls->data();
        const char *cname = ro->name;
        const char *oname = "LGTeacher";
        if (strcmp(cname, oname) == 0) {
           printf("prepare_load_methods :%s \n",cname);
        }
        
        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);
    }
}

而當從此進入realizeClassWithoutSwift方法,進而調用methodizeClass方法時,此時methodizeClass方法中cats不爲空,然後調用attachCategories()方法,將分類的方法貼到主類裏面。

category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
attachCategories(cls, cats, false /*don't flush caches*/);

總結

  本篇介紹了 懶加載類非懶加載類 的加載,以及 懶加載分類非懶加載分類 搭配 懶加載類非懶加載類 的4種組合情況的加載原理。

  • 實現了load方法的類爲非懶加載類,在啓動時初始化

  • 未實現load方法的方法的類爲懶加載類,在調用的時候初始化

  • 非懶加載類的加載:

    • read_iamges
    • 循環讀取非懶加載類,將其加到內存,addClassTableEntry(cls)
    • realizeClassWithoutSwift(cls)中,對clssupClassisa以及rw->ro等進行賦值,然後進入methodizeClass(cls),用ro中的數據對rw進行賦值。
  • 懶加載類的加載:在調用的時候初始化,比然進入lookUpImpOrForward方法

    • 進入方法查找流程,進入lookUpImpOrForward
    • !cls->isRealized()判斷是否是懶加載類,不是,開始方法查找
    • 懶加載類,進入realizeClassMaybeSwiftAndLeaveLocked
    • 調用realizeClassWithoutSwift
  • 懶加載分類的加載是在編譯期處理的,直接添加到相應的data()ro中,在類初始化的時候,直接用ro->baseMethods()rw->methods賦值。

  • 非懶加載分類的加載 + 懶加載類

    • read_image 中 對分類的處理
    • 判斷是否是對象方法還是類方法
    • 爲類添加未附加的類別,addUnattachedCategoryForClass
    • 判斷是否是懶加載類cls->isRealized(),此次爲懶加載類,不進入判斷
    • 進入prepare_load_methods
    • 進入realizeClassWithoutSwift
    • 進入methodizeClass,此時category_list *cats不爲空
    • 調用attachCategories()方法,將分類的方法貼到主類裏面。
  • 非懶加載分類的加載 + 非懶加載類

    • read_image 中 對分類的處理
    • 判斷是否是對象方法還是類方法
    • 爲類添加未附加的類別,addUnattachedCategoryForClass
    • 判斷是否是懶加載類cls->isRealized(),此次爲非懶加載類,進入判斷
    • 進入remethodizeClass
    • 調用attachCategories()方法,將分類的方法貼到主類裏面。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章