iOS探索 分類、類拓展的加載過程

歡迎閱讀iOS探索系列(按序閱讀食用效果更加)c++

寫在前面

上篇文章iOS探索 類的加載過程分析了類的加載過程,本文就來好好聊聊分類加載的那些事面試

(請先對類的加載過程有了必定了解以後再開啓本文)express

1、分類初探

1.clang輸出cpp

FXPerson新建一個分類FXPerson-FX 數組

終端利用clang輸出cpp安全

clang -rewrite-objc FXPerson+FX.m -o cate.cpp
複製代碼

2.底層分析

從cpp文件最下面看起,首先看到分類是存儲在MachO文件的__DATA段的__objc_catlistbash

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

其次能看到FXPerson分類的結構app

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搜索無果)ide

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:分類所定義的類屬性

爲何分類的方法要將實例方法和類方法分開存呢?post

由於類和元類以前在不斷編譯,實例方法存在類中,類方法存在元類中,已經肯定好其方法歸屬的地方;而分類是後面才加進來的

2、分類的加載

經過上一篇文章咱們知道了類分爲懶加載類非懶加載類,他們的加載時機不同,那麼分類又是如何呢?下面咱們就依次來進行探究

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最後一個分類

3、類拓展的加載

類拓展extension又稱做匿名的分類,爲了給當前類增長屬性方法

具體由兩種形式:

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

1.類拓展的加載

數據很早的時候都會來到_read_image,那正好在處理類時使用咱們的慣用伎倆

可是仔細一想不對呀,已經在類中有了方法實現了,此時的do_hExtension不足以說明問題

那麼能夠經過查看屬性的settergetter方法來驗證

經過上圖就能夠得出:

  • 類拓展在編譯時便做爲類的一部分進行編譯
  • 類拓展在讀取時直接讀取ro

2.類拓展的細節點

若是類拓展沒有被引用(#import)就不會編譯到到內存中

4、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`出棧一個自動釋放池

5、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類與分類的加載過程,筆者已經將加載->使用的流程進行一小波探究以後,接下來將開始乾貨分享——底層面試題

相關文章
相關標籤/搜索