Category – 簡介

發表於2016-09-29

新增實踐部分:偏方 Hook 進某些方法來新增功能

Category – 簡介

Category(類別)是 Objective-C 2.0 新增的新特性(十年前的新特性 ?)。其作用可以擴充套件已有的類, 而不必通過子類化已有類,甚至也不必知道已有類的原始碼,還有就是分散程式碼,使已有類的體積大大減少,也利於分工合作。

蘋果開源專案中,我們可以下載相關的原始碼來檢視 category 的資料。

在 AFNetworking 和 SDWebImage 中也大量用到 category 來擴充套件已有類和分散程式碼。

關於 category 的定義可以在 objc-runtime-new.h 中找到。由其定義可以看出 category 可以正常實現功能有:新增例項方法、類方法、協議、例項屬性。( 在後面的實踐中,發現類屬性也是可以新增的 )

隨便說一句,本文並不主要注重 category 的實現細節和工作原理。關於細節的方面可以看相關文章 深入理解Objective-C:Category結合 category 工作原理分析 OC2.0 中的 runtime


Category – 能做什麼

首先,我們先來建立一個 Person 類以及 Person 類的 category,可以看得出 category 的檔名就是 已有類名+自定義名

建立完程式碼之後,下面我們來看看 category 到底能幹什麼。

順便一提,我是在網上看到很多文章說 category 不能新增屬性,這是說法是不對的,如 Person+OtherSkills.h 中就新增了一個 otherName 的屬性。正確的說法應該是 category 不能新增例項變數,否則編譯器會報錯 instance variables may not be placed in categories。正常情況下,因為 category 不能新增例項變數,也會導致屬性的 setter & getter 方法不能正常工作。( 當然,可以利用 Runtimecategory 動態關聯屬性,最後會介紹兩種使 category 屬性正常工作的方法)

category 可以為已有類新增例項屬性。

Person+OtherSkills.h 中就新增了一個 otherName 的屬性。可以出來能正常工作。

category 可以為已有類新增類屬性。

雖然,category_t 中是沒有定義 clssProperties,但是根據實際操作卻顯示 category 的確可以為已有類新增類屬性並且成功執行。我個人覺得是部分原始碼沒有更新或者隱藏了?,如果有知道原因的同學可以說一下

category 可以為已有類新增例項方法和類方法。

在上面的兩個例子中已經體現了 category 可以為已有類新增例項方法和類方法。這裡將討論加入 category 重寫了已有類的方法會怎麼樣,在建立的程式碼中我們已經重寫了 runtalk 方法,那這時我們來呼叫看看。

可以看得出來,這時候無論是已有類中的類方法和例項方法都可以被 category 替換到其中的重寫方法,即使我現在是沒有匯入 Person+OtherSkills.h 。這就帶來一個很嚴重的問題,如果在 category 中不小心重寫了已有類的方法將導致原方法無法正常執行。所以使用 category 新增方法時候請注意是否和已有類重名了,正如 《 Effective Objective-C 2.0 》 中的第 25 條所建議的:

在給第三方類新增 category 時新增方法時記得加上你的專有字首

然而,因為 category 重寫方法是並不是替換掉原方法,而是往已有類中繼續新增方法,所以還是有機會去呼叫到原方法。這裡利用 class_copyMethodList 獲取 Person 類的全部類方法和例項方法。

其中輸出的類方法和例項方法分別如下,顯示原方法的確可以被呼叫。
不過我這裡有個疑問,使用 imp 時第二個引數 sel 到底有什麼用呢?


category 可以為已有類新增協議。

這裡先新增一個新的 category,負責處理他談笑風生的行為,和寫個協議讓他上電視。

在相應的代理裡面新增 showInTV 的方法

這樣就利用 category 為已有類新增了協議。

關於 category 的基本應用就介紹到這裡了。下面就來分享一下 category 的實踐中的使用。


Category – 實踐

偏方:Hook 進某些方法來新增功能

一般來說,為原方法新增功能都是利用 RuntimeMethod Swizzling。不過這裡也有個奇淫技巧來實現同樣的功能,例如我要在所有 VC- (void)viewDidLoad 裡面列印一個句話,就可以用 category 重寫已有類的方法,因為 category 重寫方法不是通過替換原方法來實現的,而是在原方法列表又增添一個新的同名方法,這就創造了機會給我們重新呼叫原方法了。

檢視輸出結果,可以看得出來我們的 Hook 掉 viewDidLoad 來實現列印成功了。


UIButton 實現點選事件可以“傳參”。

一般建立UIButton的時候都會使用 addTarget ...這個方法來為button新增點選事件,不過這個方法有個不好的地方就是無法傳自己想要的引數。例如下面程式碼中宣告瞭str,我的意圖是點選button就使控制檯或者螢幕顯示str的內容。如果按照這樣來寫的我想到的解決辦法就是將str設定為屬性或者成員變數,不過這樣都是比較麻煩而且不直觀的(程式碼分散)。

我想到較好的解決辦法應該在建立button,就為它設定具體的點選響應事件。實現方法就是為 UIButton 新增 block 屬性或者新增可傳入 block 的方法。具體程式碼如下:

那現在我們來看看呼叫的結果,例如我現在想要的點選事件是 button 顏色隨機變換。

Category – 簡介
實現效果圖

顯然,方法1和方法2在這個例子中實現的效果是相同的。不過,在不同場合這兩個方法適用的範圍也不同。

  1. 直接呼叫例項方法傳入 block 會使程式碼更加簡潔和集中,但不適合 block 需要傳值的情景。
  2. 相反,設定 block 屬性要在 @selector() 中的方法中呼叫 block,比較麻煩,不過在需要的情況下可以傳入合適的引數。

p.s. 以後會繼續補充實踐部分。

最後說一下,兩種使 category 屬性正常工作的方法:

  1. 因為 category 不能建立例項變數,那就直接使用靜態變數,如最開始為 ohterNameclsStr 屬性設定 setter & getter的做法。
  2. 使用objc_setAssociatedObject,其中 key 的選擇有以下幾種,個人比較喜歡第四種。
    • static char *key1; // SDWebImage & AFNetworking 中的做法,比較簡單,而且 &key1 肯定唯一。key 取 &key1
    • static const char * const key2 = "key2"; // 網上看到的做法,指標不可變,指向內容不可變,但是這種情況必須在賦值確保 key2 指向內容的值是唯一。key 取 key2。
    • static const void *key3 = &key3; // 最取巧的方法,指向自己是為了不建立額外空間,而 const 修飾可以確保無法修改 key3 指向的內容。key 取 key3。
    • key 取 @selector(屬性名),最方便,輸入有提示,只要你確保屬性名新增上合適的字首就不會出問題。