iOS進階專項分析(八)、深入App啟動之dyld、map_images、load_images

今天我們先來看兩個經典的面試題:

1、應用程序啟動 在main函數之前都具體做了哪些內容?

2、load在什么時候調用?子類、父類以及分類load的調用順序?

帶著這幾個問題我們開始本節的內容:

  • 1、App編譯/啟動流程及動態鏈接器dyld
  • 2、map_images流程分析
  • 3、load_images流程分析
  • 4、面試題答案(僅供參考~)

一、App編譯流程及啟動流程dyld


下面圖示編譯流程

編譯流程.png

注意:只有靜態庫會在編譯階段會打包進入可執行文件,動態庫是在程序運行時才會被加入可執行文件。靜態庫與動態庫知識點深入傳送門

應用程序啟動前,會先對代碼進行編譯,在編譯階段會把靜態庫打包到可執行性文件中,編譯完成后,進入啟動階段,談及App啟動流程,肯定少不了我們的動態鏈接器dyld,整個啟動過程都是它在進行協調,dyld操作流程如下圖:

啟動流程dyld.png

從App啟動角度深入了解dyld

其實在App啟動過程中主要分為main函數之前和main函數之后,main函數之后就是從main函數到我們第一個視圖出現的這段時間。先來看一下main()之前主要做了哪些操作:

main()之前通過調用dyld對主程序運行環境初始化,生成imageLoader把動態庫生成對應的image鏡像文件,載入到內存中,然后進行鏈接綁定,接著初始化所有動態庫,在執行所有插入的動態庫初始化的時候,同時也對load_images進行了綁定。執行初始化這個過程中,會優先初始化系統庫libSystem,運行起來Runtime,這個過程會進入Runtime的入口函數_objc_init,接下來把之前鏈接的動態庫及符號都交給Runtime進行map_imagesload_images操作,然后Runtime執行完load_images之后會回調到dyld內部,dyld收到信息回調后,最后查找main()函數的入口LC_MAIN,找到后就會調起我們的main()函數,進入我們開發者的代碼。

接下來結合文章開始的面試題,我們就來仔細分析一下Runtimemap_imagesload_images流程中間做了哪些操作?

二、map_images流程分析


Objc源碼下載地址

還是從系統庫libSystem的Runtime入口函數_objc_init開始分析:

/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/

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();
    
    //運行C ++靜態構造函數。libc在dyld調用我們的靜態構造函數之前調用_objc_init()
    static_init();
    
    lock_init();
    //初始化libobjc的異常處理系統。由map_images()調用。
    exception_init();

    注冊回調函數
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

接下來我們開始進入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直接返回了map_images_nolock

先來看一下map_images函數的注釋部分,我們得知:map_images主要作用就是處理由dyld映射的image(此處的image泛指二進制可執行程序)。

繼續點進入map_images_nolock的實現部分, 我們來分析這里面主要做了什么:

void 
map_images_nolock(unsigned mhCount, const char * const mhPaths[],
                  const struct mach_header * const mhdrs[])
{
    定義一系列變量
    ......
    
    必要時執行首次初始化
    
    //如果是第一次,就準備初始化環境
    if (firstTime) {
        preopt_init();
    }

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

    計算class數量,根據總數調整各種表的大小。
    // Count classes. Size various table based on the total.
    int totalClasses = 0;
    int unoptimizedTotalClasses = 0;
    {
        uint32_t i = mhCount;
        while (i--) {
            const headerType *mhdr = (const headerType *)mhdrs[i];

            auto hi = addHeader(mhdr, mhPaths[i], totalClasses, unoptimizedTotalClasses);
            if (!hi) {
                // no objc data in this entry
                continue;
            }
            
            if (mhdr->filetype == MH_EXECUTE) {
                // Size some data structures based on main executable's size
#if __OBJC2__
                size_t count;
                _getObjc2SelectorRefs(hi, &count);
                selrefCount += count;
                _getObjc2MessageRefs(hi, &count);
                selrefCount += count;
#else
                _getObjcSelectorRefs(hi, &selrefCount);
#endif
                
#if SUPPORT_GC_COMPAT
                // Halt if this is a GC app.
                if (shouldRejectGCApp(hi)) {
                    _objc_fatal_with_reason
                        (OBJC_EXIT_REASON_GC_NOT_SUPPORTED, 
                         OS_REASON_FLAG_CONSISTENT_FAILURE, 
                         "Objective-C garbage collection " 
                         "is no longer supported.");
                }
#endif
            }
            
            hList[hCount++] = hi;
            
            if (PrintImages) {
                _objc_inform("IMAGES: loading image for %s%s%s%s%s\n", 
                             hi->fname(),
                             mhdr->filetype == MH_BUNDLE ? " (bundle)" : "",
                             hi->info()->isReplacement() ? " (replacement)" : "",
                             hi->info()->hasCategoryClassProperties() ? " (has class properties)" : "",
                             hi->info()->optimizedByDyld()?" (preoptimized)":"");
            }
        }
    }

    ......
    
    執行一次運行時初始化,必須將其推遲到找到可執行文件本身為止。 這需要在進一步初始化之前完成。(如果可執行文件不包含Objective-C代碼,但稍后會動態加載Objective-C,則該可執行文件可能不會出現在此infoList中。
    
    if (firstTime) {
    
        //初始化sel方法表 并注冊系統內部專門的方法。
        sel_init(selrefCount);
        arr_init();

#if SUPPORT_GC_COMPAT
        // Reject any GC images linked to the main executable.
        // We already rejected the app itself above.
        // Images loaded after launch will be rejected by dyld.

        for (uint32_t i = 0; i < hCount; i++) {
            auto hi = hList[i];
            auto mh = hi->mhdr();
            if (mh->filetype != MH_EXECUTE  &&  shouldRejectGCImage(mh)) {
                _objc_fatal_with_reason
                    (OBJC_EXIT_REASON_GC_NOT_SUPPORTED, 
                     OS_REASON_FLAG_CONSISTENT_FAILURE, 
                     "%s requires Objective-C garbage collection "
                     "which is no longer supported.", hi->fname());
            }
        }
#endif

#if TARGET_OS_OSX
        // Disable +initialize fork safety if the app is too old (< 10.13).
        // Disable +initialize fork safety if the app has a
        //   __DATA,__objc_fork_ok section.

        if (dyld_get_program_sdk_version() < DYLD_MACOSX_VERSION_10_13) {
            DisableInitializeForkSafety = true;
            if (PrintInitializing) {
                _objc_inform("INITIALIZE: disabling +initialize fork "
                             "safety enforcement because the app is "
                             "too old (SDK version " SDK_FORMAT ")",
                             FORMAT_SDK(dyld_get_program_sdk_version()));
            }
        }

        for (uint32_t i = 0; i < hCount; i++) {
            auto hi = hList[i];
            auto mh = hi->mhdr();
            if (mh->filetype != MH_EXECUTE) continue;
            unsigned long size;
            if (getsectiondata(hi->mhdr(), "__DATA", "__objc_fork_ok", &size)) {
                DisableInitializeForkSafety = true;
                if (PrintInitializing) {
                    _objc_inform("INITIALIZE: disabling +initialize fork "
                                 "safety enforcement because the app has "
                                 "a __DATA,__objc_fork_ok section");
                }
            }
            break;  // assume only one MH_EXECUTE image
        }
#endif

    }

    直接開始image讀取
    if (hCount > 0) {
        _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
    }

    firstTime = NO;
}


對源碼進行分析, map_images_nolock 方法的流程如下:

1、 判斷是不是第一次,如果是第一次,那么就開始準備初始化環境

    if (firstTime) {
        preopt_init();
    }

2、計算class數量,根據總數調整各種表的大小(這個步驟里面會判斷GC(Garbage Collection),因為Objective-C之前是做了垃圾回收機制兼容的,現在則不支持了。盡管目前不支持GC了,但是蘋果并沒有刪除這些兼容性代碼)

    // Count classes. Size various table based on the total.
    int totalClasses = 0;
    int unoptimizedTotalClasses = 0;
    {
        uint32_t i = mhCount;
        while (i--) {
        
            調整表的大小部分操作
            ......
            
            
            GC相關邏輯判斷            
#if SUPPORT_GC_COMPAT

                // Halt if this is a GC app.
                if (shouldRejectGCApp(hi)) {
                    _objc_fatal_with_reason
                        (OBJC_EXIT_REASON_GC_NOT_SUPPORTED, 
                         OS_REASON_FLAG_CONSISTENT_FAILURE, 
                         "Objective-C garbage collection " 
                         "is no longer supported.");
                }
#endif
            }
            
            hList[hCount++] = hi;
            
        }
    }


3、判斷是不是首次執行,如果是,會初始化各種表

if (firstTime) {
        sel_init(selrefCount);
        arr_init();
        
        ......
        
        繼續邏輯判斷GC相關
        
        #if SUPPORT_GC_COMPAT
        ......
        #endif
       
}



4、接著開始讀取images,并將firstTime置為 NO

//判斷,然后進行images讀取

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

//將firstTime置為NO,下次就不重新創建表了
firstTime = NO;

總結map_images_nolock的流程就是:

  1. 判斷firstTimefirstTime為YES,則執行環境初始化的準備,為NO就不執行
  2. 計算class數量,根據總數調整各種表的大小并做了GC相關邏輯處理(不支持GC則打印提示信息)
  3. 判斷firstTimefirstTime為YES,執行各種表初始化操作,為NO則不執行
  4. 執行_read_images進行讀取,然后將firstTime置為NO,就不再進入上面的邏輯了,下次進入map_images_nolock就開始直接_read_images

接下來我們重點分析_read_images底層實現,看看到底做了哪些主要操作,進入源碼實現如下:

/***********************************************************************
* _read_images
* Perform initial processing of the headers in the linked 
* list beginning with headerList. 
*
* Called by: map_images_nolock
*
* Locking: runtimeLock acquired by map_images
**********************************************************************/
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{

    定義一系列局部變量
    ......

    1. 重新初始化TaggedPointer環境****************
    
    if (!doneOnce) {
        doneOnce = YES;

         ......

        if (DisableTaggedPointers) {
            disableTaggedPointers();
        }
        
        initializeTaggedPointerObfuscator();

        ......
        
        注意!!!!!創建表gdb_objc_realized_classes和allocatedClasses
        
        ......
        
        int namedClassesSize = 
            (isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;
        gdb_objc_realized_classes =
            NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
        
        allocatedClasses = NXCreateHashTable(NXPtrPrototype, 0, nil);
        
        ......
    }


    // Discover classes. Fix up unresolved future classes. Mark bundle classes.
    
    2. 開始遍歷頭文件,進行類與元類的讀取操作并標記(舊類改動后會生成新的類,并重映射到新的類上)************************

    for (EACH_HEADER) {
        //從頭文件中拿到類的信息
        classref_t *classlist = _getObjc2ClassList(hi, &count);
        
        if (! mustReadClasses(hi)) {
            // Image is sufficiently optimized that we need not call readClass()
            continue;
        }

        bool headerIsBundle = hi->isBundle();
        bool headerIsPreoptimized = hi->isPreoptimized();

        for (i = 0; i < count; i++) {
            Class cls = (Class)classlist[i];
            
            //!!!!!!核心操作,readClass讀取類的信息及類的更新
            Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);
            
            ......
            
        }
    }
        
    ......

    3. 讀取@selector*************************************
    
    // Fix up @selector references
    static size_t UnfixedSelectors;
    {
        mutex_locker_t lock(selLock);
        for (EACH_HEADER) {
            if (hi->isPreoptimized()) continue;
            
            bool isBundle = hi->isBundle();
            SEL *sels = _getObjc2SelectorRefs(hi, &count);
            UnfixedSelectors += count;
            for (i = 0; i < count; i++) {
                const char *name = sel_cname(sels[i]);
                sels[i] = sel_registerNameNoLock(name, isBundle);
            }
        }
    }


    ......
    
    
    4. 讀取協議protocol*************************************
    // Discover protocols. Fix up protocol refs.
    for (EACH_HEADER) {
        extern objc_class OBJC_CLASS_$_Protocol;
        Class cls = (Class)&OBJC_CLASS_$_Protocol;
        assert(cls);
        NXMapTable *protocol_map = protocols();
        bool isPreoptimized = hi->isPreoptimized();
        bool isBundle = hi->isBundle();

        protocol_t **protolist = _getObjc2ProtocolList(hi, &count);
        for (i = 0; i < count; i++) {
            readProtocol(protolist[i], cls, protocol_map, 
                         isPreoptimized, isBundle);
        }
    }

   ......


    5. 處理分類category,并rebuild重建這個類的方法列表method list*******************************
    
    // Discover categories. 
    for (EACH_HEADER) {
        category_t **catlist = 
            _getObjc2CategoryList(hi, &count);
        bool hasClassProperties = hi->info()->hasCategoryClassProperties();

        for (i = 0; i < count; i++) {
            category_t *cat = catlist[i];
            Class cls = remapClass(cat->cls);
            
            ......
            
            bool classExists = NO;
            if (cat->instanceMethods ||  cat->protocols  
                ||  cat->instanceProperties) 
            {
                addUnattachedCategoryForClass(cat, cls, hi);
                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" : "");
                }
            }

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

    ......

    if (DebugNonFragileIvars) {
        realizeAllClasses();
    }


    最后是一堆打印***********
    ......
   
}


_read_images的實現主要分為以下步驟:

  1. 重新初始化TaggedPointer環境
  2. 開始遍歷頭文件,進行類與元類的讀取操作并標記(舊類改動后會生成新的類,并重映射到新的類上)
  3. 讀取@selector方法
  4. 讀取協議protocol
  5. 處理分類category,并rebuild重建這個類的方法列表method list

既然是讀取類,類的結構中包含類本身以及類的所有信息(例如分類,方法,協議)。

下面我們就針對這些我們想知道的內容進行分析:類與元類的讀取、方法@selector的讀取、協議protocol的讀取以及分類category的讀取

第1步、重新初始化TaggedPointer環境

判斷doneOnce,如果doneOnce為NO,則首先重置及初始化taggedPointer,然后創建兩個表,一個叫gdb_objc_realized_classes用來存放已命名的類的列表,另一個叫allocatedClasses用來存放已分配的所有類(和元類)


if (!doneOnce) {
        doneOnce = YES;//這個邏輯只執行一次
        
        //重置及初始化TaggedPointer環境
        
        if (DisableTaggedPointers) {
            disableTaggedPointers();
            }
        initializeTaggedPointerObfuscator();
        
        
        //創建表gdb_objc_realized_classes和allocatedClasses
        int namedClassesSize = 
            (isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;
        gdb_objc_realized_classes =
            NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
        
        allocatedClasses = NXCreateHashTable(NXPtrPrototype, 0, nil);
        
    }

分別點到這兩個表的定義部分,根據注釋能查看出這兩個表大概的作用

// This is a misnomer: gdb_objc_realized_classes is actually a list of 
// named classes not in the dyld shared cache, whether realized or not.
gdb_objc_realized_classes實際上是不在dyld共享緩存中的已命名類的列表,無論是否實現

NXMapTable *gdb_objc_realized_classes;  // exported for debuggers in objc-gdb.h

/***********************************************************************
* allocatedClasses
* A table of all classes (and metaclasses) which have been allocated
* with objc_allocateClassPair.
**********************************************************************/

static NXHashTable *allocatedClasses = nil;

這里拓展一下這兩張表的類型:gdb_objc_realized_classes的類型是NXMapTableallocatedClasses表的類型是NXHashTable

可以簡單理解NSHashTable、NSMapTable分別對應的是我們常用的NSSet和NSDictionary,并且額外提供了weak指針來使用垃圾回收機制。

NSDictionary底層實現也是使用了NSMapTable(散列表),(備注:蘋果官網并沒有這些類的實現,想要查看NSDictionary和NSArray的實現源碼可以去GNUstep官網下載或者百度網盤下載)

使用NSMapTable是因為它更強大NSMapTable相對于NSDictionary的優勢

第2步、類與元類的讀取

遍歷頭文件,進行類與元類的讀取操作,讀取完后標記


// Discover classes. Fix up unresolved future classes. Mark bundle classes.

    for (EACH_HEADER) {
        classref_t *classlist = _getObjc2ClassList(hi, &count);
        
        if (! mustReadClasses(hi)) {
            // Image is sufficiently optimized that we need not call readClass()
            continue;
        }

        bool headerIsBundle = hi->isBundle();
        bool headerIsPreoptimized = hi->isPreoptimized();

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

其中主要做了readClass來讀取編譯器編寫的類和元類,我們重點來仔細分析一下類的讀取過程,進入readClass源碼

Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized)
{
    const char *mangledName = cls->mangledName();
    
    if (missingWeakSuperclass(cls)) {
        // No superclass (probably weak-linked). 
        // Disavow any knowledge of this subclass.
        if (PrintConnecting) {
            _objc_inform("CLASS: IGNORING class '%s' with "
                         "missing weak-linked superclass", 
                         cls->nameForLogging());
        }
        addRemappedClass(cls, nil);
        cls->superclass = nil;
        return nil;
    }
    
    ......

    Class replacing = nil;
    if (Class newCls = popFutureNamedClass(mangledName)) {
    
        ......
                
        class_rw_t *rw = newCls->data();
        const class_ro_t *old_ro = rw->ro;
        memcpy(newCls, cls, sizeof(objc_class));
        rw->ro = (class_ro_t *)newCls->data();
        newCls->setData(rw);
        freeIfMutable((char *)old_ro->name);
        free((void *)old_ro);
        
        addRemappedClass(cls, newCls);
        
        replacing = cls;
        cls = newCls;
    }
    
    if (headerIsPreoptimized  &&  !replacing) {
    
        ......
        
        assert(getClass(mangledName));
    } else {
        addNamedClass(cls, mangledName, replacing);
        addClassTableEntry(cls);
    }

    ......
    
    return cls;
}


從源碼中可以看出,readClass方法有返回值,并且包含三種邏輯處理:

  1. 找不到該類的父類,可能是弱綁定,直接返回nil;
  2. 找到類了,判斷這個類是否是一個future的類(可以理解為需要實現的一個類,也可以理解為這個類是否有變化),如果有變化則創建新類,并把舊類的數據拷貝一份然后賦值給新類newCls,然后調用addRemappedClass進行重映射,用新的類替換掉舊的類,并返回新類newCls的地址
  3. 找到類了,如果類沒有任何變化,則不進行任何操作,直接返回class

readClass的底層實現部分做個延伸思考:日常開發中,對于已經啟動完成的工程項目,如果我們未修改任何類的數據,那么再次點擊運行會很快完成,但是一旦我們在對這些類進行修改后,在讀取這些類的信息(包括類本身的信息以及下面我們要繼續分析的協議protocol、分類category、方法selector),就需要對該類的數據進行更新,這個更新實際上是新建一個類,然后拷貝舊類的數據賦值給新類,然后重映射并用新類替換掉新類,這里面的拷貝以及讀寫過程其實是相當耗時的!這是類信息改動之后項目再次Run運行起來會比較慢的原因之一。

繼續分析,既然做了類信息的讀取,那么讀取到的數據到底存在哪里呢?在readClass源碼最后部分找到這兩句代碼

addNamedClass(cls, mangledName, replacing);
addClassTableEntry(cls);
        

先看這兩句的第一句代碼做了什么:進入addNameClass找到

NXMapInsert(gdb_objc_realized_classes, name, cls);

發現已經讀取完成的類,會被存放到了這個表gdb_objc_realized_classes里面!

然后繼續看第二句里面做了啥:

static void addClassTableEntry(Class cls, bool addMeta = true) {
    runtimeLock.assertLocked();
    
    ......

    if (!isKnownClass(cls))
        NXHashInsert(allocatedClasses, cls);
    if (addMeta)
        addClassTableEntry(cls->ISA(), false);
}

分析源碼注釋及源碼得出,addClassTableEntry里面會把這個讀取完成的類直接先添加到allocatedClasses表里面,然后再判斷addMeta是否為YES,然后會把當前這個類的元類metaClass也添加到allocatedClasses這個表里面。

這里是一個遞歸的邏輯,我們需要來分析一下:由于我們上一步是這樣直接調用的:

addClassTableEntry(cls);

所以進入這個方法的時候,只傳入了一個cls并沒有傳入addMeta,所以這里addMeta默認就是YES,然后繼續遞歸調用當前addClassTableEntry,注意:第二次遞歸調用的時候,addMeta傳入的是false,所以第二次就不會再添加元類了,這里的邏輯主要是保證元類只添加一次。所以addClassTableEntry里面其實是做了把類和元類都加到allocatedClasses表里面。

到此為止,類和元類的讀取我們已經明白了,下面用同樣的分析,分析類的方法、協議以及分類

第3步、方法@selector的讀取

接下來進到第四部的代碼部分

// Fix up @selector references
    static size_t UnfixedSelectors;
    {
        mutex_locker_t lock(selLock);
        for (EACH_HEADER) {
            if (hi->isPreoptimized()) continue;
            
            bool isBundle = hi->isBundle();
            SEL *sels = _getObjc2SelectorRefs(hi, &count);
            UnfixedSelectors += count;
            for (i = 0; i < count; i++) {
                const char *name = sel_cname(sels[i]);
                sels[i] = sel_registerNameNoLock(name, isBundle);
            }
        }
    }

點擊sel_registerNameNoLock,找到__sel_registerName,在它里面找到關鍵代碼

if (!namedSelectors) {
        namedSelectors = NXCreateMapTable(NXStrValueMapPrototype, 
                                          (unsigned)SelrefCount);
    }
    if (!result) {
        result = sel_alloc(name, copy);
        // fixme choose a better container (hash not map for starters)
        NXMapInsert(namedSelectors, sel_getName(result), result);
    }

邏輯其實就是:把方法名插入并存儲到namedSelectors這個表里面.

第4步,協議protocol的讀取
// Discover protocols. Fix up protocol refs.
    for (EACH_HEADER) {
        extern objc_class OBJC_CLASS_$_Protocol;
        Class cls = (Class)&OBJC_CLASS_$_Protocol;
        assert(cls);
        創建表protocol_map
        NXMapTable *protocol_map = protocols();
        bool isPreoptimized = hi->isPreoptimized();
        bool isBundle = hi->isBundle();

        拿到頭文件中協議列表
        protocol_t **protolist = _getObjc2ProtocolList(hi, &count);
        for (i = 0; i < count; i++) {
        讀取protocol
            readProtocol(protolist[i], cls, protocol_map, 
                         isPreoptimized, isBundle);
        }
    }

找到關鍵函數readProtocol,進入發現其實讀取protocol的操作是把protocol存進協議表protocol_map

insertFn(protocol_map, installedproto->mangledName, installedproto);

第5步,分類category的讀取

來看看分類部分的邏輯

// Discover categories. 
    for (EACH_HEADER) {
    從頭文件中找到所有分類
        category_t **catlist = 
            _getObjc2CategoryList(hi, &count);
        bool hasClassProperties = hi->info()->hasCategoryClassProperties();

        
        for (i = 0; i < count; i++) {
            category_t *cat = catlist[i];
            Class cls = remapClass(cat->cls);
            根據分類,獲取分類對應的類
            if (!cls) {
            如果分類所屬的類找不到,那么就會把這個這個分類category_t置為nil
                ......
                
                catlist[i] = nil;
                
                ......
                
                continue;
            }

            ......
            
            如果分類所屬的類找到了,那么判斷這個分類里面的實例方法instanceMethods,協議protocols,以及屬性instanceProperties是否存在,如果存在,就把這些方法分別同步更新到對應的類和元類中
            
            bool classExists = NO;
            
            //把分類新增的方法、協議、屬性都添加到類中
            if (cat->instanceMethods ||  cat->protocols  
                ||  cat->instanceProperties) 
            {
                addUnattachedCategoryForClass(cat, cls, hi);
                if (cls->isRealized()) {
                    remethodizeClass(cls);
                    classExists = YES;
                }
                
                ......
                
             }

            //把分類新增的方法、協議、屬性都添加到元類中
            if (cat->classMethods  ||  cat->protocols  
                ||  (hasClassProperties && cat->_classProperties)) 
            {
                addUnattachedCategoryForClass(cat, cls->ISA(), hi);
                if (cls->ISA()->isRealized()) {
                    remethodizeClass(cls->ISA());
                }
                
                ......
                
            }
        }
    }

總結一下分類category的讀取,里面主要做了下面這些步驟:

  1. 從頭文件中獲取所有的分類列表catlist,然后循環遍歷這個列表
  2. 在循環中,判斷當前分類cat所屬的類是否存在,如果不存在則把這個分類置為空catlist[i] = nil; 如果這個分類所屬的類存在,那么開始下面兩個步驟:
  3. 第一個步驟:判斷這個分類cat中是否有實例方法instanceMethods,協議protocols以及屬性實例instanceProperties,如果有,那么進入remethodizeClass,重新rebuild當前類cls的方法列表
  4. 第二個步驟:繼續判斷這個分類cat中是否有類方法classMethods,協議protocols以及類屬性_classProperties,然后重新rebuild當前類所對應元類cls->ISA()的方法列表。

注意第一步和第二步這兩個方法分別對應處理的是分類的類和分類的類對應的元類。處理類調用的是remethodizeClass(cls);,而處理元類調用的是remethodizeClass(cls->ISA())

接下來我們進入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*/))) {
    
        ......
        
        attachCategories(cls, cats, true /*flush caches*/);  
              
        free(cats);
    }
}

找到關鍵實現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);
}


注意,這里面的類型和Class類中的類型是完全一致并且對應的,下面貼上Class的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;
    
    ......
    
}

注意類結構中方法表、屬性表、協議表的類型分別是method_array_tproperty_array_tprotocol_array_t,而這個三個表底層都是由list_array_tt進行實現的,只不過里面存儲的數據類型不相同,這個三個表里面分別對應存儲的是method_list_tproperty_list_t以及protocol_list_t類型的數據

class method_array_t : 
    public list_array_tt<method_t, method_list_t> 
{
    typedef list_array_tt<method_t, method_list_t> Super;

 public:
    method_list_t **beginCategoryMethodLists() {
        return beginLists();
    }
    
    method_list_t **endCategoryMethodLists(Class cls);

    method_array_t duplicate() {
        return Super::duplicate<method_array_t>();
    }
};

class property_array_t : 
    public list_array_tt<property_t, property_list_t> 
{
    typedef list_array_tt<property_t, property_list_t> Super;

 public:
    property_array_t duplicate() {
        return Super::duplicate<property_array_t>();
    }
};


class protocol_array_t : 
    public list_array_tt<protocol_ref_t, protocol_list_t> 
{
    typedef list_array_tt<protocol_ref_t, protocol_list_t> Super;

 public:
    protocol_array_t duplicate() {
        return Super::duplicate<protocol_array_t>();
    }
};


并且list_array_tt在設計的時候,提供了attachList方法,可以調用這個方法往表里繼續添加數據

void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;

        if (hasArray()) {
            // many lists -> many lists
            uint32_t oldCount = array()->count;
            uint32_t newCount = oldCount + addedCount;
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
            array()->count = newCount;
            memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
        else if (!list  &&  addedCount == 1) {
            // 0 lists -> 1 list
            list = addedLists[0];
        } 
        else {
            // 1 list -> many lists
            List* oldList = list;
            uint32_t oldCount = oldList ? 1 : 0;
            uint32_t newCount = oldCount + addedCount;
            setArray((array_t *)malloc(array_t::byteSize(newCount)));
            array()->count = newCount;
            if (oldList) array()->lists[addedCount] = oldList;
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
    }

所以,通過結合objc_class源碼,來對比分析attachCategories源碼,我們能夠明白attachCategories函數里面主要做了下面的操作:

  1. 對應Class的結構,新建方法表method_list_t **mlistsproperty_list_t **proplistsprotocol_list_t **protolists
  2. 根據當前類cls分類的數量,進行while循環,把分類里面包含的方法,協議,屬性都加到上面的三個表中
  3. 獲取當前類的rw,通過rw拿到對應的methodspropertiesprotocols,然后由于這三個表都是由list_array_tt實現,直接調用list_array_ttattachLists方法,把category分類里面的方法,協議,屬性都添加到當前類的表里面去。

到此,map_images的主要操作都已經分析完成,下面總結一下map_images的主要流程及流程圖。

流程如下:

  1. 初始化環境TaggedPointer環境,同時新建兩個表:gdb_objc_realized_classes用來存儲讀取完成的類的類名,allocatedClasses存儲已經創建的類及元類,接下來作為類是否創建的邏輯判斷
  2. 讀取類read_class,如果是需要實現的新類,那么進行實現并重映射,并用新類的地址替換舊類的地址,然后把實現的類的類名存儲到表gdb_objc_realized_classes中,同時順帶把這個類以及元類都保存到表allocatedClasses中做了關聯綁定,方便后續邏輯處理
  3. 讀取類的方法@selector,調用sel_registerNameNoLock,把方法名存儲到表namedSelectors
  4. 讀取類的protocol協議,調用readProtocol,把協議對象protocol_tmangledName存儲到表protocol_map中。
  5. 最后讀取類的分類category,category對應兩個邏輯分別調用remethodizeClass,這兩個邏輯分別是:實例方法/屬性/協議添加到當前類,而類方法/屬性/協議添加給當前類對應的元類,因為類方法本身就是是存儲在元類中的。具體操作就是先獲取到所有分類及中的數據,添加到新的數組中,然后直接調用rw->methods.attachLists/rw->properties.attachLists/rw->protocols.attachLists,利用list_array_tt中的attachLists方法,把這些分類,協議,屬性都添加到類和元類的rw數據中

流程圖如下:

map_images.png

三、load_images流程分析


接下來我們分析load_images底層的邏輯流程,點擊load_images進入

void
load_images(const char *path __unused, const struct mach_header *mh)
{
    // Return without taking locks if there are no +load methods here.
    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    {
        mutex_locker_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    call_load_methods();
}

找到關鍵代碼

  1. prepare_load_methods
  2. call_load_methods

開始分析

1、prepare_load_methods底層實現

貼上prepare_load_methods源碼

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
        realizeClass(cls);
        assert(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }
}

進入schedule_class_load,這個函數底層如下

static void schedule_class_load(Class cls)
{
    ......
    
    schedule_class_load(cls->superclass);

    add_class_to_loadable_list(cls);
    
    ......
}

這里面添加的方法add_class_to_loadable_list的底層實現如下

void add_class_to_loadable_list(Class cls)
{
    IMP method;

    loadMethodLock.assertLocked();
    取到load方法
    method = cls->getLoadMethod();
    
    ......
    
    loadable_classes[loadable_classes_used].cls = cls;
    loadable_classes[loadable_classes_used].method = method;
    loadable_classes_used++;
}


我們發現這個添加過程實際上就是把loadable_class類型的結構體,存儲到表待調度load的這張表loadable_classes中,而存儲的結構體loadable_class類型包含類名cls以及該類的load方法IMP

struct loadable_class {
    Class cls;  // may be nil
    IMP method;
};

cls->getLoadMethod()方法得到的就是該類的Load方法

IMP 
objc_class::getLoadMethod()
{
    ......

    mlist = ISA()->data()->ro->baseMethods();
    if (mlist) {
        for (const auto& meth : *mlist) {
            const char *name = sel_cname(meth.name);
            if (0 == strcmp(name, "load")) {
                return meth.imp;
            }
        }
    }

    return nil;
}

schedule_class_load底層實現原理是:獲取父類,然后繼續遞歸調用schedule_class_load,然后把這些類按父類的父類->父類->子類這個順序把類和類的load方法添加到loadable_classes表中。這也是為什么類的+(load)方法執行順序是從父類到子類的。

在調用schedule_class_load添加完成類之后,又繼續處理分類,分類內部調用_category_getLoadMethod拿到分類中重寫的load方法,然后也調用add_category_to_loadable_list把分類cat和分類的load方法添加到表loadable_categories中。

所以總結prepare_load_methods準備load方法的邏輯就是:

  1. 先處理類:遞歸按照先父類再子類的順序,把類和類的load方法整合成一個結構體對象loadable_class,然后把這個結構體對象存到表loadable_classes中。
  2. 處理完成類之后,再開始處理分類:獲取分類的load方法,把分類和分類的load方法整合成一個結構體對象loadable_category然后存儲到表loadable_categories中。

到這里,load方法準備工作完畢。

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

先觀察這個函數實現部分,發現這個do—while循環被包含在objc_autoreleasePoolPush()objc_autoreleasePoolPop中,蘋果使用了autoreleasePool是為了節省內存開銷。

然后我們繼續來看循環體部分:

do {
        //1、while循環調用call_class_loads()方法
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        //2、調用call_category_loads()并返回一個bool布爾值并賦值給more_categories
        more_categories = call_category_loads();

    } while (loadable_classes_used > 0  ||  more_categories);

接下來我們繼續分析call_class_loadscall_category_loads底層實現。

先來看看調用類的load函數call_class_loads

static void call_class_loads(void)
{
    int i;
    
    // Detach current loadable list.
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;
    
    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
        load_method_t load_method = (load_method_t)classes[i].method;
        if (!cls) continue; 

        if (PrintLoading) {
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
        (*load_method)(cls, SEL_load);
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}

簡化源碼如下

static void call_class_loads(void)
{
    ......
    
    for (i = 0; i < loadable_classes_used; i++) {
        Class cls = loadable_classes[i].cls;
        load_method_t load_method = (load_method_t) loadable_classes[i].method;

        ......
        
        (*load_method)(cls, SEL_load);
    }
    ......
}

這個過程其實就是從之前存儲好的表loadable_classes中取出Class和對應load方法的load_method_t對象,直接調用。

然后看看調用分類的load函數call_category_loads

static bool call_category_loads(void)
{
    int i, shift;
    bool new_categories_added = NO;
    
    // Detach current loadable list.
    struct loadable_category *cats = loadable_categories;
    int used = loadable_categories_used;
    int allocated = loadable_categories_allocated;
    loadable_categories = nil;
    loadable_categories_allocated = 0;
    loadable_categories_used = 0;

    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Category cat = cats[i].cat;
        load_method_t load_method = (load_method_t)cats[i].method;
        Class cls;
        if (!cat) continue;

        cls = _category_getClass(cat);
        if (cls  &&  cls->isLoadable()) {
        
            ......
            
            (*load_method)(cls, SEL_load);
            cats[i].cat = nil;
        }
    }

    ......
    
    return new_categories_added;
}

這個過程和類的邏輯基本一致,也是就是從之前存儲好的表loadable_categories中取出分類Category和對應load方法的load_method_t對象,然后通過_category_getClass獲取到分類對應的類,然后用類直接調用load方法。

到此為止,load_images主要流程也已經分析完畢。load_images主要做了下面這些步驟:

第一步.準備load方法:prepare_load_methods

以先處理類,后處理分類 以及 先處理父類,后處理子類的順序存儲到待調度的表中。

類的處理邏輯:把類對象Class和類對應的load方法的IMP整合成一個loadable_class類型的結構體對象存儲在表loadable_classes中。

分類的處理邏輯:把分類對象Category和對應的load方法IMP整合成一個loadable_category類型的結構體對象存儲在表loadable_categories中。

第二步.調用load方法:call_load_methods

以先調用類Class的load,后處理分類Category,通過分類找到對應的類,然后由類調用load方法的順序進行處理

這個調用處理的順序是根據準備方法prepare_load_methods中準備好的兩張表loadable_classesloadable_categories的順序而來的。調用完就從表中移除,全部調用完結束循環。

下面是我對load_images方法實現邏輯的的流程圖:

load_images.png

四、面試題答案(僅供參考~)


1、應用程序啟動 在main函數之前都具體做了哪些內容?

程序啟動時,系統XNU執行程序的可執行二進制文件,從內核態切換到用戶態,根據路徑找到并運行動態鏈接器dyld,并把控制權交給dyld,然后啟動dyld進行程序環境初始化,然后讀取可執行文件Mach-O,開始根據頭文件內容讀取動態庫并初始化主程序,初始化主程序后,就開始鏈接讀取完成的動態庫到主程序可執行文件中,然后初始化動態庫。在初始化其他動態庫之前,會最先初始化系統庫libsystem,運行Runtime。系統庫libsystem初始化完成后,就會初始化其他動態庫,然后由Runtime調用map_images來讀取類、方法、協議以及分類并存儲到對應的表中(注意:分類并不是直接存,而是通過attachLists方法把分類的數據添加到類里面),然后Runtime會繼續調用load_images調用所有類的load方法以及分類的load方法,這些都做完之后,通過dyld提供的回調_dyld_objc_notify_register,告訴dyld加載完畢,然后dyld就開始找主程序的入口main函數,最后進入程序的main函數。

2、load在什么時候調用?子類、父類以及分類load的調用順序?

load方法調用是在應用程序main函數之前,應用啟動時dyld處理完image鏡像文件,通過回調傳給runtime,交由runtimeload_images方法中調用的。

load方法調用順序為:先處理類,后處理分類;處理類的順序是先父類,后子類

在調用類的load方法時,做了遞歸處理,會先調用父類的load,然后再調用子類的load,所有類的load方法調用完成后,才會開始處理所有類的分類,分類的處理順序取決于Mach-O頭文件,和類的順序沒有直接關系。先后順序即:父類->子類->所有類的分類

驗證方式:實現子類和父類,重寫load方法,在其中進行NSLog打印便可以看出,這里我就不驗證了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。