iOS Category 實現解析

一、Category 是什麼

categoryObject-C 2.0 以後添加的語言特性。c++

1-一、category的做用

  • 一、爲已有的類添加方
  • 二、能夠減小單個文件的體積
  • 三、把不一樣的功能添加到不一樣的Category
  • 四、能夠按需加載功能
  • 五、聲明私有有方法
  • 六、公開framework的私有方法

1-二、category的數據結構

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;
    struct property_list_t *_classProperties;
    // Fields below this point are not always present on disk.
    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }
    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
複製代碼
變量 註解
const char *name 類名
classref_t cls 原來的類的指針(一開始爲空,編譯時期最後根據name綁定)
struct method_list_t *instanceMethods 分類聲明的實例方法列表
struct method_list_t *classMethods 分類聲明的類方法列表
struct protocol_list_t *protocols 分類遵照協議列表
struct property_list_t *instanceProperties 分類聲明的實例屬性列表
struct property_list_t *_classProperties 分類聲明的類屬性列表

1-三、method_list_t

method_list_t 是一個範型容器,裏面存放的是 method_t 結構體數組

struct method_list_t : entsize_list_tt<method_t, method_list_t, 0x3> {
    bool isFixedUp() const;
    void setFixedUp();

    uint32_t indexOfMethod(const method_t *meth) const {
        uint32_t i = 
            (uint32_t)(((uintptr_t)meth - (uintptr_t)this) / entsize());
        assert(i < count);
        return i;
    }
};
複製代碼
template <typename Element, typename List, uint32_t FlagMask>
struct entsize_list_tt {
    uint32_t entsizeAndFlags;
    uint32_t count;
    Element first;
};
複製代碼
變量 註解
typename Element 元素類型
typename List 用於指定容器類型
uint32_t FlagMask 標記位

1-四、method_t

struct method_t {
    SEL name;
    const char *types;
    MethodListIMP imp;

    struct SortBySELAddress :
        public std::binary_function<const method_t&,
                                    const method_t&, bool>
    {
        bool operator() (const method_t& lhs,
                         const method_t& rhs)
        { return lhs.name < rhs.name; }
    };
};
複製代碼
變量 註解
SEL name 方法名
const char *types 方法簽名
MethodListIMP 方法指針

二、Category 的加載

  • image這裏指的不是圖片,是Mach-O格式的二進制文件,dyld就是蘋果加載image的動態加載器
  • main函數啓動前,系統內核會啓動dyldApp依賴的各類庫加載到內存,其中包括libobjc (OC和runtime)
  • _objc_initObjcet-C runtime的入口函數,這裏面主要功能就是讀取Mach-O文件OC對應的Segment sction,並根據其中的代碼信息完成OC的內存佈局,以及初始化runtime相關數據結構

2-一、_objc_init 分析

void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    //讀取影響運行的環境變量,若是須要,還能夠打印環境變量幫助
    environ_init();
    //關於線程key的綁定--好比每線程數據的析構函數
    tls_init();
    //運行系統的C++靜態構造函數,在dyld調用咱們的靜態構造函數以前,libc會調用_objc_init(),因此咱們必須本身作
    static_init();
    //無源碼,就是說objc的異常徹底纔有c++那一套
    lock_init();
    //初始化異常處理系統,好比註冊異常的回調函數,來監控異常
    exception_init();
    
    //僅供objc運行時使用,註冊處理程序,以便在映射、取消映射和初始化objc鏡像文件時調用
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
複製代碼

主要關注最後一句代碼安全

_dyld_objc_notify_register(&map_images, load_images, unmap_image);
複製代碼
void _dyld_objc_notify_register(_dyld_objc_notify_mapped    mapped,
                                _dyld_objc_notify_init      init,
                                _dyld_objc_notify_unmapped  unmapped);
複製代碼

這句代碼註冊了dyld關於加載images的回調,有如下三個事件:bash

  • image映射到內存時
  • image被init時
  • image被移時

2-1-一、image映射到內存時

imagedyld加載到內存後會調用回調_dyld_objc_notify_mapped數據結構

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 方法。app

  • 進入map_images_nolock內部,這個方法實現至關長,咱們只須要關注其內部調用的_read_images方法。函數

  • _read_images的做用就是讀取Segment sction來初始化。佈局

    • _read_images中,會讀取類的各類信息。其中: category_t **catlist = _getObjc2CategoryList(hi, &count); 就是讀取分類的信息了。ui

    • 在讀取到分類信息之後,會調用 addUnattachedCategoryForClass(cat, cls, hi); 這把當前分類加載到類當中。this

    • addUnattachedCategoryForClass方法 的最後會調用NXMapInsert(cats, cls, list);這是在作底層數據結構的映射,咱們能夠簡單理解爲創建了cats(分類結構體)cls(類)的關聯。

    • 在創建了關聯之後,就須要把分類中的方法和屬性一一添加到類當中。remethodizeClass(cls->ISA());這句對當前類進行重排列。在 remethodizeClass內部會調用attachCategories(cls, cats, true /*flush caches*/);

    • attachCategories會從新聲明方法列表,協議列表,屬性列表的內存空間並把方法、協議、屬性添加到當前類。

2-1-1-一、分類屬性的添加

//在 attachCategories 方法中
    rw->properties.attachLists(proplists, propcount);
    free(proplists);
複製代碼

2-1-1-二、分類屬性的添加

//在 attachCategories 方法中
    rw->protocols.attachLists(protolists, protocount);
    free(protolists);
複製代碼

2-1-1-三、分類方法的添加

//在 attachCategories 方法中
     auto rw = cls->data();
    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
複製代碼
  • 會先調用prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
  • prepareMethodLists方法裏調用了fixupMethodList(mlist, methodsFromBundle, true/*sort*/);
  • fixupMethodList方法是準備好須要添加的方法列表,主要作了如下工做:
    • 比較方法列表,去重
    • mlist裏的方法都填充當前類的類名
    • mlist裏的方法根據內存地址排序
  • 準備好方法列表之後就會調用attachLists方法把方法添加到原來的類的方法列表裏:
    • 改變原來方法列表的大小,主要經過memmove方法、memcpy方法 實現,這兩個方法都用於內存拷貝。

      • memmove(array()->lists + addedCount, array()->lists,
                oldCount * sizeof(array()->lists[0]));
        memcpy(array()->lists, addedLists,  
               addedCount * sizeof(array()->lists[0]));
        複製代碼
      • void   *memcpy(void *__dst, const void *__src, size_t __n);
        void   *memmove(void *__dst, const void *__src, size_t __len);
        複製代碼
      • memcpy方法是把當前src指向的位置拷貝len的長度放到 dst的位置

      • memmove方法在當前src指向的位置加上n的長度與 dst的位置不重疊的時候和memcpy方法一致,當發生內存重疊的時候memmove可以保證源串在被覆蓋以前將重疊區域的字節拷貝到目標區域中。

      • src位置在dst位置後面,memmovememcpy都能很好的實現。

      • src位置在dst位位置前面,也就內存拷貝重疊的時候memcpy可能致使原數據改變而失敗,使用memmove更加安全。

    • 咱們能夠看到,分類添加方法實際上是發生了內存移動。因此類原有的方法即便在分類中從新實現,也只是被覆蓋而不會消失,還能夠經過訪問方法列表調用。

    • 方法調用實際上是遍歷方法列表找到合適的方法而後調用,在調用過程會先使用二分查找。若是先找到後面有合適的方法,並不會馬上返回該方法IMP指針,而是向前查找是否有同名方法,若是有就返回前面方法的IMP指針不然返回後面的。因此會確保前面的方法先被調用。

2-1-二、image被init時

_dyld_objc_notify_register(&map_images, load_images, unmap_image);
複製代碼
void _dyld_objc_notify_register(_dyld_objc_notify_mapped    mapped,
                                _dyld_objc_notify_init      init,
                                _dyld_objc_notify_unmapped  unmapped);
複製代碼

二進制文件初始化完成後會調用loadImages

2-1-2-一、load方法調用規則

  • 類的load方法必定在其父類的load方法調用後調用
  • 分類的load方法必定在當前類load方法調用後調用
  • 分類的load方法調用順序和編譯順序有關

2-1-2-二、loadImages

  • loadImages裏會調用 prepare_load_methods((const headerType *)mh);
  • loadImages裏會調用 call_load_methods();
2-1-2-2-一、 prepare_load_methods((const headerType *)mh);
  • 依據當前調用規則,把當前類和分類進行重排列
  • Mach-O文件加載類的列表,遍歷調整當前類的順序
  • void prepare_load_methods(const headerType *mhdr)
      {
          size_t count, i;
    
          runtimeLock.assertLocked();
    
          //從 Macho 文件加載類的列表
          classref_t *classlist = 
           _getObjc2NonlazyClassList(mhdr, &count);
          for (i = 0; i < count; i++) {
              //數組:[<cls,method>,<cls,method>,<cls,method>] 有順序
              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方法會基於當前類的指針進行遞歸調用。從當前類開始找父類直到NSObject爲止,而後開始一級一級向下調用load方法。

    • 這個遞歸調用就是爲了保證當前類的load方法必定在其父類的load方法調用後調用。

    • //遞歸調用
        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方法就是分配內存空間並把當前的類添加到一個全局的容器loadable_classes之中。添加順序也是NSObject -> ... -> superclass -> class。添加完成後咱們就能夠獲得一個當前類類的全局容器,裏面存放了當前class以及method
      //分配空間
          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;
      複製代碼
    • 處理完當前類之後經過add_category_to_loadable_list方法對分類作相同的處理,獲得loadable_categories這個裝有全部分類的容器。Mach-O文件中哪一個分 類在前面,哪一個分類就會被先調用

3-1-2-2-一、 call_load_methods();
do {
    // 1. Repeatedly call class +loads until there aren't any more while (loadable_classes_used > 0) { //先調用類的 load 方法 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); 複製代碼
  • 優先調用當前類的load方法 (loadable_classes容器)
  • 後調用分類的load方法 (loadable_categories容器)
  • (*load_method)(cls, SEL_load); 拿到load方法的指針而後發送一個消息

三、總結

  • objc_init
    • _dyld_objc_notify_register:註冊回調
      • map_images:映射
        • map_images_nolock
          • _read_images:讀取image
            • addUnattachedCategoryForClass:添加categoryclass
            • remethodizeClass:從新分配類的內存
              • attachCategories:添加操做
                • prepareMethodLists:去重、綁定類名、排序
                • attachLists:改變列表大小 並添加元素
                  • memmove:內存移動拷貝
                  • memcpy:內存拷貝
      • load_images:加載
        • prepare_load_methods:準備加載
          • schedule_class_load:遞歸調用load
            • add_class_to_loadable_list:獲取全局class容器並調用load
              • loadable_classes:全局容器
          • _getObjc2NonlazyCategoryList:獲取category
          • add_category_to_loadable_list:獲取全局category容器並調用load
            • loadable_categories:全局容器
相關文章
相關標籤/搜索