ObjC中Category的原理簡析

SoC發表於2019-01-24

Objective-C類別也叫分類,是一種不需要繼承即可給類新增方法的語法技術。

Category的使用

通常我們是用Category為一個類新增一些方法。我們可以直接用類似物件對方法呼叫的樣子直接對Category中的方法進行呼叫。比如下面的例子,為Person(Person類定義在.h和.m檔案中了,圖片沒有給出)類定義了一個名為Test的Category。

main.m

呼叫Category中的test方法與例項直接呼叫例項方法一樣,也是通過訊息傳送機制(objc_sendMethod)進行呼叫。

我們都知道類似這種方法呼叫是通過例項的isa指標查詢類物件,在類物件中查詢到相應的例項方法進行呼叫的,當然還包括superclass查詢父類,這裡就不做贅述了。因為通過isa指標查詢方法已經在往深處看-ObjC物件中有過介紹。

那麼Category定義的方法存放在哪裡呢?其實它們跟類的例項物件一樣,也是存放在類物件中。同樣,Category中定義的類方法,也是存放在元類物件中的。只不過將Category中的資訊合併到類或元類物件中的操作是在執行時完成的而非編譯的時。

Category的底層結構

使用xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc mian.m -o main.cpp 將上述main.m轉換成main.cpp檔案,可以看到Person的Category (Test) 被轉換成了下面這樣一個_category_t型別的結構體。

Person+Test

檢視_category_t結構體的結構:其中存放有:名稱、類資訊、例項方法和類方法列表、協議列表和屬性列表。

_category_t

實際上在runtime原始碼的最新版本objc4-723中對category_t的描述中新增了獲取方法列表的方法和元類屬性的方法,並且將properties分成了instanceProperties和_classProperties。

runtime-723中的category_t-w1298

Category初始化傳值

我們可以看到在_OBJC_$_CATEGORY_Person_$_test中,傳入的值:

instance_methods

class_methods

protocols

properties

在runtime原始碼中窺探Category

上面說過Category中的方法、屬性、協議是在執行時合併到類中的,所以我們在執行時的入口objc_os.mm檔案中找到_objc_init方法。我們可以看到在方法上面有註釋表示這是入口函式,會通過dyid去載入一些模組。

順著這個思路我們檢視map_image是通過map_images_nolock(count, paths, mhdrs)獲得的,在這個方法中存在一個載入模組的方法。

_read_images

載入模組的函式中有一個重排方法的函式,在這個函式中有一個attachCategories(cls, cats, true /*flush caches*/);函式。這個函式是實現附加分類的作用。引數分別傳遞了cls物件和Category列表。

attachCategories()中將Category中的屬性、方法和協議取出放到陣列中,然後用cls取出類中的class_rw_t結構體,取出的methods、properties、protocols分別呼叫attachLists方法,並且將方法陣列、屬性陣列、協議陣列和陣列的中元素的個數傳遞到attachLists中。

在attachLists函式中將之前存放方法、屬性、協議的陣列擴容至oldCount+addedCount大小。將原方法、屬性、協議的陣列從大陣列的第一個位置移動到最後的位置,然後將attachCategories傳入的列表拷貝到大陣列的前面,完成Category中的方法、屬性、協議向類物件或元類物件的合併。

attachList函式片段

將Category中的內容合併到類或元類在runtime中的操作順序:

首先類或元類方法列表是一個二維陣列:

方法列表

runtime合併Category資訊到類或元類物件中在runtime原始碼中的函式用軌跡:

合併Category到類或元類中

attachLists圖示:

attachLists

attachLists最終結果

使用Category中方法的一個問題

我們可以看到在沒有將Category的方法列表加到類或元類物件中之前,陣列中只有這一個列表,而合併之後,它們就被放到了類或元類物件的方法列表的最後面,所以我們呼叫在Category中重寫的類中的例項方法或者類方法都會優先執行Category中的方法,因為isa在類物件或者元類物件中尋找方法的時候會首先在Category中找到,既然找到了就不會繼續往下找了。

但是假如有多個擴充套件都重寫了類的某個方法,首先必然是執行某個Category的方法,這個Category在方法列表的最前面,那麼多個Category的方法列表在類或元類的方法列表中的順序是怎麼決定的呢?

我們看到attachLists中擴充套件中的方法、屬性和協議列表是在attachCategories中生成的,生成的過程:

生成方法、屬性和協議列表

通過i--來進行一個while迴圈,其中i是int i = cats->count;即cats的個數。而mlist、proplists和protolists是進行++操作的,操作的結果造成了先取出cats列表最後一個放到mlist,propcounts的最前面。cats是按照編譯順序排列的,所以後編譯的cat中取出的方法、屬性和協議列表,分別放在mlist、proplists和protolists的最前面。

所以isa指標也會按照上述順序查詢方法,即後編譯的cats中的方法會先呼叫。

**注:**編譯順序可以在Xcode中檢視和改變

檔案編譯順序

相關文章