Objective-C之Class底層結構探索

来源:https://www.cnblogs.com/mysweetAngleBaby/p/18092347
-Advertisement-
Play Games

isa 走點陣圖 在講 OC->Class 底層類結構之前,先看下下麵這張圖: 通過isa走點陣圖 得出的結論是: 1,類,父類,元類都包含了 isa, superclass 2,對象isa指向類對象,類對象的isa指向了元類,元類的 isa 指向了根元類,根元類 isa 指向自己 3,類的 super ...


isa 走點陣圖

在講 OC->Class 底層類結構之前,先看下下麵這張圖:

isa走位

通過isa走點陣圖 得出的結論是:
1,類,父類,元類都包含了 isa, superclass

2,對象isa指向類對象,類對象的isa指向了元類,元類的 isa 指向了根元類,根元類 isa 指向自己

3,類的 superclass 指向父類,父類的 superclass 指向的根類,根類的superclass 指向的nil

4,元類的 superclass 指向父元類,父元類 superclass 指向的根元類,根元類 superclass 指向根類,根類 superclass 指向nil

這下又複習了 isasuperclass 走位;那麼問題這些類,類對象,元類對象當中的在底層展現的數據結構是怎樣呢,這是我需要探索的,於是把源碼貼出來展開分析下:

struct 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 *data() const {
        return bits.data();
    }
    const class_ro_t *safe_ro() const {
        return bits.safe_ro();
    }
}

從源碼沒見 isa 屬性,其實它繼承了objc_object ,而 objc_object 中有個isa ,在運行時類圖生成中會產生一個isa 指向objc_object 這個類圖,而 superclass 指向它的父類;根據上面 isa , superclass 走點陣圖就知道它的指向關係。

cache_t & class_data_bits_t

cache 方法緩存,這個作用將常調用的方法緩存下來;便於下次直接查找調用,提高查找效率。
它的結構:

struct cache_t {
	struct bucket_t *buckets() const;//存儲方法的散列表
	mask_t mask() const;//散列表緩存長度
	mask_t occupied() const;//已緩存方法個數
}
struct class_data_bits_t {
    class_rw_t* data() const;//類信息
}

bits 存儲具體類信息,它需要&FAST_DATA_MASK來計算得到類心所有信息,源碼如下:

FAST_DATA_MASK 掩碼值

imageng

bool has_rw_pointer() const {
	#if FAST_IS_RW_POINTER
	        return (bool)(bits & FAST_IS_RW_POINTER);
	#else
	        class_rw_t *maybe_rw = (class_rw_t *)(bits & FAST_DATA_MASK);
	        return maybe_rw && (bool)(maybe_rw->flags & RW_REALIZED);
	#endif
}

通過源碼確實需要這種方式計算能得到類的存儲信息;那為什麼要用這種方式去處理呢。
比如說我要得到存儲在 class_rw_t 類信息信息我只要通過 FAST_DATA_MASK 掩碼值就能得到它的地址信息,通過地址信息就能從記憶體中拿到所有類的存儲信息。

那這樣我的FAST_DATA_MASK掩碼值不一樣,我通過&計算,得到的數據信息也就不一樣,不得不說蘋果工程師想的周到,而且這種方式不僅isa也是這樣,很多地方都用這種方式取值,大大提高訪問速度,數據提取效率。

class_rw_t ,class_ro_t,class_rw_ext_t

struct class_rw_t {
     const class_ro_t *ro() const ;
     const method_array_t methods() const ;//如果是類對象:放對象方法,元類:元類對象方法
     
     const property_array_t properties() const;
     const protocol_array_t protocols() const;
     class_rw_ext_t *ext() const;
}
struct class_rw_ext_t {
    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;
    uint32_t version;
}

可以看出類的信息具體就存儲在class_rw_tclass_ro_tclass_rw_ext_t 中,

剖析下class_rw_t
先看看method_array_tproperty_array_tprotocol_array_t源碼結構

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

 public:
    property_array_t() : Super() { }
    property_array_t(property_list_t *l) : Super(l) { }
};


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

 public:
    protocol_array_t() : Super() { }
    protocol_array_t(protocol_list_t *l) : Super(l) { }
};

看完之後,他們都繼承list_array_tt,那麼 list_array_tt 是什麼鬼,它數據結構是怎樣的,這下在取找下它。源碼如下:

template <typename Element, typename List, template<typename> class Ptr>
class list_array_tt {
 protected:
    template <bool authenticated>
    class iteratorImpl {
        const Ptr<List> *lists;
        const Ptr<List> *listsEnd;
    }
        
    using iterator = iteratorImpl<false>;
    using signedIterator = iteratorImpl<true>;

 public:
    list_array_tt() : list(nullptr) { }
    list_array_tt(List *l) : list(l) { }
    list_array_tt(const list_array_tt &other) {
        *this = other;
    }

    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;
            array_t *newArray =(array_t*)malloc(array_t::byteSize(newCount));
            newArray->count = newCount;
            array()->count = newCount;

            for (int i = oldCount - 1; i >= 0; i--)
                newArray->lists[i + addedCount] = array()->lists[i];
            for (unsigned i = 0; i < addedCount; i++)
                newArray->lists[i] = addedLists[i];
            free(array());
            setArray(newArray);
            validate();
        }
        else if (!list  &&  addedCount == 1) {
            // 0 lists -> 1 list
            list = addedLists[0];
            validate();
        } 
        else {
            // 1 list -> many lists
            Ptr<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;
            for (unsigned i = 0; i < addedCount; i++)
                array()->lists[i] = addedLists[i];
            validate();
        }
    }
    
}

我把主要地方拿去出來,可以看到 attachLists 它的目的是將一個或多個列表(List 類型)附加到某個 list_array_tt對象中。這個對象可以包含零個、一個或多個列表,這些列表可以是單個指針,也可以是指針數組。函數的輸入參數是一個指向 List 指針數組的指針 addedLists 和一個無符號整數 addedCount,表示要添加的列表數量。

由此我推斷它是一個數組,而且是一個二維數組存儲的,所有由此得出 class_rw_tmethodspropertiesprotocols這幾個屬性利用二維數組取存儲類的方法,協議等信息,而且是可讀可寫的屬性。

那它設計這種二維數組有什麼好處呢?當然有好處,它可以動態的給數組裡面增加刪除方法,很方便我們分類方法的編寫完進行存儲。

那搞清楚了 class_rw_t 幾個重要數據存儲信息,那 class_rw_t 它的作用是乾什麼的呢;

class_rw_t 結構體定義來看;它是在應用運行時,將OC類,分類的信息直接寫入到class_rw_t結構的數據結構中,在類的方法,協議進行調用時,從裡面去讀取,然後常調用的方法,又存儲在cache_t這個結構體中,可想而知,蘋果對OC類的處理,煞費苦心。

struct class_ro_t

class_rw_t結構體中有個 class_ro_t 結構體,在探索下這個東西做什麼的,它的源碼如下:

struct class_ro_t {
    WrappedPtr<method_list_t, method_list_t::Ptrauth> baseMethods;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;
    property_list_t *baseProperties;
}

先說說 ivars 這個屬性修飾的結構體源碼如下:

struct ivar_list_t : entsize_list_tt<ivar_t, ivar_list_t, 0> {
    bool containsIvar(Ivar ivar) const {
        return (ivar >= (Ivar)&*begin()  &&  ivar < (Ivar)&*end());
    }
};

這個貌似只有一個繼承 entsize_list_tt,那在探索下源碼:

struct entsize_list_tt {
    uint32_t entsizeAndFlags;
    uint32_t count;
     struct iteratorImpl {
     uint32_t entsize;
        uint32_t index;  // keeping track of this saves a divide in operator-

        using ElementPtr = std::conditional_t<authenticated, Element * __ptrauth(ptrauth_key_process_dependent_data, 1, 0xdead), Element *>;

        ElementPtr element;

        typedef std::random_access_iterator_tag iterator_category;
        typedef Element value_type;
        typedef ptrdiff_t difference_type;
        typedef Element* pointer;
        typedef Element& reference;

        iteratorImpl() { }

        iteratorImpl(const List& list, uint32_t start = 0)
            : entsize(list.entsize())
            , index(start)
            , element(&list.getOrEnd(start))
        { }
     }
}

可以看出這段代碼定義了一個結構體 entsize_list_tt,它內部包含一個嵌套的結構體 iteratorImpl,用於實現一個迭代器。遍歷容器(如列表、數組等)的對象。

到此可以得出ivars 是一個 ivar_list_t 數組,它存儲了類的屬性變數信息,那protocol_list_t結構體內部也是數組形式構建的。

baseProtocolsbaseProperties 這兩個屬性對類的存儲信息只能讀取,不能寫入。

所以總結的是:從 class_ro_t 結構體定義來看,它存儲類的變數,方法,協議信息,而且這個結構體屬於類的只讀信息,它包含了類的初始信息。

class_rw_ext_t

這個結構體不在過多敘述,簡單來說它是基於 class_rw_t 之後為了更好管理oc類的高級特性,比如關聯屬性等,衍生出來的一個結構體,包括:method_array_t ,property_arrat_t ,protocol_array_t 等定義屬性類型

到這裡類結構及存儲所關聯的信息都在這裡了;來一張他們關聯的結構思維圖:

imageng

總結:一開始編譯時,程式將類的初始信息放在 class_ro_t中,當程式運行時,將類的信息合併在一起的時候,它會將 class_ro_t 類的信息合併到 class_rw_t 結構體中去。

struct method_t

為什麼要說method_t,因為它不僅在 class_ro_t 有使用,在OC底層其他地方也有使用;比如如下源碼:

void method_exchangeImplementations(Method m1Signed, Method m2Signed)
{
    if (!m1Signed  ||  !m2Signed) return;

    method_t *m1 = _method_auth(m1Signed);
    method_t *m2 = _method_auth(m2Signed);

    mutex_locker_t lock(runtimeLock);

    IMP imp1 = m1->imp(false);
    IMP imp2 = m2->imp(false);
    SEL sel1 = m1->name();
    SEL sel2 = m2->name();

    m1->setImp(imp2);
    m2->setImp(imp1);


    // RR/AWZ updates are slow because class is unknown
    // Cache updates are slow because class is unknown
    // fixme build list of classes whose Methods are known externally?

    flushCaches(nil, __func__, [sel1, sel2, imp1, imp2](Class c){
        return c->cache.shouldFlush(sel1, imp1) || c->cache.shouldFlush(sel2, imp2);
    });

    adjustCustomFlagsForMethodChange(nil, m1);
    adjustCustomFlagsForMethodChange(nil, m2);
}

static IMP
_method_setImplementation(Class cls, method_t *m, IMP imp)
{
    lockdebug::assert_locked(&runtimeLock);

    if (!m) return nil;
    if (!imp) return nil;

    IMP old = m->imp(false);
    SEL sel = m->name();

    m->setImp(imp);

    // Cache updates are slow if cls is nil (i.e. unknown)
    // RR/AWZ updates are slow if cls is nil (i.e. unknown)
    // fixme build list of classes whose Methods are known externally?

    flushCaches(cls, __func__, [sel, old](Class c){
        return c->cache.shouldFlush(sel, old);
    });

    adjustCustomFlagsForMethodChange(cls, m);

    return old;
}


方法交換,實現中底層都有用到,我們探索下,先看看 method_t 源碼:

struct method_t {

    // The representation of a "big" method. This is the traditional
    // representation of three pointers storing the selector, types
    // and implementation.
    struct big {
        SEL name;
        const char *types;
        MethodListIMP imp;
    };

    // A "big" method, but name is signed. Used for method lists created at runtime.
    struct bigSigned {
        SEL __ptrauth_objc_sel name;
        const char * ptrauth_method_list_types types;
        MethodListIMP imp;
    };

    // ***HACK: This is a TEMPORARY HACK FOR EXCLAVEKIT. It MUST go away.
    // rdar://96885136 (Disallow insecure un-signed big method lists for ExclaveKit)
#if TARGET_OS_EXCLAVEKIT
    struct bigStripped {
        SEL name;
        const char *types;
        MethodListIMP imp;
    };
#endif

}

可以看到這結構體中掐套了多個結構體;在把它簡化下:

struct method_t {
    SEL name;//方法名
    const char *types;//包含函數具有參數編碼的字元串類型的返回值
    MethodListIMP imp;//函數指針(指向函數地址的指針)
}

SEL :函數名,沒特別的意義;

特點:
1,使用@selector()sel_registerName()獲得
2,使用sel_getName()NSStringFromSelector()轉成字元串
3,不同類中相同名字方法,對應的方法選擇器是相同或相等的

底層代碼結構:

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

types:包含了函數返回值、參數編碼的字元串

imageng
imageng

可以看到types在值:v16@0:8 ,可以看出name,types,IMP其實都在class_ro_t結構體中,這樣確實證明瞭之前說的;class_ro_t結構體在運行時存儲著類的初始狀態數據。

v16@0:8說明下:

v:方法返回類型,這裡說void,

16:第一個參數,

@:id類型第二個參數,

0:第三個參數

: :selector類型

8:第四個參數

那這種types參數又是什麼鬼東西,查下了資料這叫:Type Encoding(類型編碼)
怎麼證明瞭,使用如下代碼:
imagepng

蘋果官網types encoding表格:
imageng

IMP 其實就是指向函數的指針,感覺這個就沒有必要講了。

struct cache_t

cache_t 用於 class的方法緩存,對class常調用的方法緩存下來,提高查詢效率,這個上之前都已經說過;接下來看看 bucket_t

struct bucket_t

struct bucket_t {
	cache_key_t _key;//函數名
	IMP _imp;//函數記憶體地址
}

這種散列表的模型,其實在底層用一個數組展現:

imagng

其實它的內部就是一個一維數組,那可能問了,數組難道它是迴圈查找嗎,其實不然;在它元素超找時,它是拿到你的 函數名 & mask,而這個 mask 就是 cache_t 結構體中的 mask值;計算得到函數在 散列表 存儲的索引值,在通過索引拿到函數地址,進行執行。

接下來看個事例:

int main(int argc, const char * argv[]) {

    @autoreleasepool {

        Student *stu=[Student new];

        [stu test];

        [stu test];

        [stu test];

        [stu test];

    }

    return 0;

}

如上方法:當首次調用它會去類對象中查找,在方法執行時,他會放入cache_t 緩存中,當第二次,第三次,第四次時,它就去緩存中查找。

imagpng

當方法執行後;我們看到 _mask 是:3,這個3代表了我類中定義了三個函數;而——_occupied 是一個隨意的值;它其實代表了換存方法的個數。

那如何知道方法有緩存了,再繼續往下執行:

imageng

這時候執行完 test02, _mask的值從 3 變成了 7 ,說明散列表 bucket_t 做了擴容操作。在這裡bucket_t 元素需要 _mask 個元素,所以最終 bucket_t 從原有的3個元素進行了 2倍 擴容。

在看下方法是否進行緩存:

imageng

可以看見當執行完 [stu test02] 時,數據做了擴容,並且擴容的數據使用(null) 進行填充。

在看個事例:

imageng

在執行 [stu test] 之前;其實bucket_t 就3個元素,並且存入了 init 方法;

imageng

當執行完 [stu test] 之後;就存入 test 方法。

但是註意的地方:它在擴容時對之前的緩存進行清除。

image.png

通過查看源碼,我們知道了它如何進行清除操作,

imageng

當執行完 [stu test02];[stu test03]; 之後,它先將緩存清空;這時候 init , test 方法被清空,bucket_t擴容完在存儲:test02test03 方法。

那問題又來了,它是如何快速定位到方法的,然後執行的?接下來看看代碼:

imagepng

可以清楚看見,當我使用 @selector(test03)&stu_cache._mask 就可以得到下標,然後再從 bucket_t 拿到方法。

到這裡 class結構,類的方法緩存到此結束了,從上面也可以思考下:如果自己去實現散列表數組,是不是思路就跟清晰了。

謝謝大家!青山不改,綠水長流。後會有期!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本文記錄如何使用 adb 命令修改 Android/data 目錄下的文件,然後給國服的碧藍檔案打上布丁~ 前言 今天下午刷著刷著微博就看到國服 BA 又又又發了和諧公告 ... 心情複雜。jpg 於是乎終於想起來得吃布丁了,至於此次更新後布丁有沒有用還未知,但還是先搞上 食用方法之前就出了 ...
  • 原創研發flutter3+getX+mediaKit仿抖音app短視頻直播實戰Flutter-DouYin。 flutter3_dylive使用最新跨平臺技術flutter3.x+dart3+getx+get_storage+media_kit開發手機端仿抖音app小視頻直播實戰項目。實現了抖音全屏 ...
  • 一、Stack 1.概述 HarmonyOS中的層疊佈局Stack是一種可以將多個組件按照一定順序疊放的佈局。Stack佈局中的組件可以是任意類型的組件,且每個組件都可以設置在哪個位置疊放。在疊放時,後添加的組件會自動覆蓋前面添加的組件。 Stack佈局佈局中的每個子組件都可以設置偏移量、旋 ...
  • 一、是什麼 TCP/IP,傳輸控制協議/網際協議,是指能夠在多個不同網路間實現信息傳輸的協議簇 TCP(傳輸控制協議) 一種面向連接的、可靠的、基於位元組流的傳輸層通信協議 IP(網際協議) 用於封包交換數據網路的協議 TCP/IP協議不僅僅指的是TCP和IP兩個協議,而是指一個由FTP、SMTP、T ...
  • 前言 習慣了在 css 文件裡面編寫樣式,其實JavaScript 的 CSS對象模型也提供了強大的樣式操作能力, 那就隨文章一起看看,有多少能力是你不知道的吧。 樣式來源 客從八方來, 樣式呢, 樣式五方來。 chrome舊版本用戶自定義樣式目錄: %LocalAppData%/Google/Ch ...
  • VUE 腳手架 腳手架文件結構 ├── node_modules ├── public │ ├── favicon.ico: 頁簽圖標 │ └── index.html: 主頁面 ├── src │ ├── assets: 存放靜態資源 │ │ └── logo.png │ │── componen ...
  • 最近看到了許多關於 :has() 選擇器的知識點,在此總結下來。 MDN 對 :has() 選擇器 的解釋是這樣的: CSS 函數式偽類 :has() 表示一個元素,如果作為參數傳遞的任何相對選擇器在錨定到該元素時,至少匹配一個元素。這個偽類通過把可容錯相對選擇器列表作為參數,提供了一種針對引用元素 ...
  • 系統功能文檔是一種描述軟體系統功能和操作方式的文檔。它讓開發團隊、測試人員、項目管理者、客戶和最終用戶對系統行為有清晰、全面的瞭解。 通過ChatGPT,我們能讓編寫系統功能文檔的效率提升10倍以上。 用ChatGPT生成系統功能文檔 我們以線上商城系統為例,介紹如何使用ChatGPT幫我們完成系統 ...
一周排行
    -Advertisement-
    Play Games
  • 1、預覽地址:http://139.155.137.144:9012 2、qq群:801913255 一、前言 隨著網路的發展,企業對於信息系統數據的保密工作愈發重視,不同身份、角色對於數據的訪問許可權都應該大相徑庭。 列如 1、不同登錄人員對一個數據列表的可見度是不一樣的,如數據列、數據行、數據按鈕 ...
  • 前言 上一篇文章寫瞭如何使用RabbitMQ做個簡單的發送郵件項目,然後評論也是比較多,也是準備去學習一下如何確保RabbitMQ的消息可靠性,但是由於時間原因,先來說說設計模式中的簡單工廠模式吧! 在瞭解簡單工廠模式之前,我們要知道C#是一款面向對象的高級程式語言。它有3大特性,封裝、繼承、多態。 ...
  • Nodify學習 一:介紹與使用 - 可樂_加冰 - 博客園 (cnblogs.com) Nodify學習 二:添加節點 - 可樂_加冰 - 博客園 (cnblogs.com) 介紹 Nodify是一個WPF基於節點的編輯器控制項,其中包含一系列節點、連接和連接器組件,旨在簡化構建基於節點的工具的過程 ...
  • 創建一個webapi項目做測試使用。 創建新控制器,搭建一個基礎框架,包括獲取當天日期、wiki的請求地址等 創建一個Http請求幫助類以及方法,用於獲取指定URL的信息 使用http請求訪問指定url,先運行一下,看看返回的內容。內容如圖右邊所示,實際上是一個Json數據。我們主要解析 大事記 部 ...
  • 最近在不少自媒體上看到有關.NET與C#的資訊與評價,感覺大家對.NET與C#還是不太瞭解,尤其是對2016年6月發佈的跨平臺.NET Core 1.0,更是知之甚少。在考慮一番之後,還是決定寫點東西總結一下,也回顧一下.NET的發展歷史。 首先,你沒看錯,.NET是跨平臺的,可以在Windows、 ...
  • Nodify學習 一:介紹與使用 - 可樂_加冰 - 博客園 (cnblogs.com) Nodify學習 二:添加節點 - 可樂_加冰 - 博客園 (cnblogs.com) 添加節點(nodes) 通過上一篇我們已經創建好了編輯器實例現在我們為編輯器添加一個節點 添加model和viewmode ...
  • 前言 資料庫併發,數據審計和軟刪除一直是數據持久化方面的經典問題。早些時候,這些工作需要手寫複雜的SQL或者通過存儲過程和觸發器實現。手寫複雜SQL對軟體可維護性構成了相當大的挑戰,隨著SQL字數的變多,用到的嵌套和複雜語法增加,可讀性和可維護性的難度是幾何級暴漲。因此如何在實現功能的同時控制這些S ...
  • 類型檢查和轉換:當你需要檢查對象是否為特定類型,並且希望在同一時間內將其轉換為那個類型時,模式匹配提供了一種更簡潔的方式來完成這一任務,避免了使用傳統的as和is操作符後還需要進行額外的null檢查。 複雜條件邏輯:在處理複雜的條件邏輯時,特別是涉及到多個條件和類型的情況下,使用模式匹配可以使代碼更 ...
  • 在日常開發中,我們經常需要和文件打交道,特別是桌面開發,有時候就會需要載入大批量的文件,而且可能還會存在部分文件缺失的情況,那麼如何才能快速的判斷文件是否存在呢?如果處理不當的,且文件數量比較多的時候,可能會造成卡頓等情況,進而影響程式的使用體驗。今天就以一個簡單的小例子,簡述兩種不同的判斷文件是否... ...
  • 前言 資料庫併發,數據審計和軟刪除一直是數據持久化方面的經典問題。早些時候,這些工作需要手寫複雜的SQL或者通過存儲過程和觸發器實現。手寫複雜SQL對軟體可維護性構成了相當大的挑戰,隨著SQL字數的變多,用到的嵌套和複雜語法增加,可讀性和可維護性的難度是幾何級暴漲。因此如何在實現功能的同時控制這些S ...