category 详解
我們講解的category分為三個(gè)部分:
1:怎么用:運(yùn)用場(chǎng)景
2:不能用:爭(zhēng)議性的特點(diǎn):能做什么不能做什么
3:原因:內(nèi)部原理
前言:
無(wú)論一個(gè)類設(shè)計(jì)的多么完美,在未來(lái)的需求演進(jìn)中,都有可能會(huì)碰到一些無(wú)法預(yù)測(cè)的情況。那怎么擴(kuò)展已有的類呢?一般而言,繼承和組合是不錯(cuò)的選擇。但是在Objective-C 2.0中,又提供了category這個(gè)語(yǔ)言特性,可以動(dòng)態(tài)地為已有類添加新行為。Objective-C的這個(gè)語(yǔ)言特性對(duì)于純動(dòng)態(tài)語(yǔ)言來(lái)說(shuō)可能不算什么,比如javascript,你可以隨時(shí)為一個(gè)“類”或者對(duì)象添加任意方法和實(shí)例變量。但是對(duì)于不是那么“動(dòng)態(tài)”的語(yǔ)言而言,這確實(shí)是一個(gè)了不起的特性。
1:怎么用:運(yùn)用場(chǎng)景
1.1:主要作用是為已經(jīng)存在的類添加方法。
想要收集每個(gè)頁(yè)面的啟動(dòng)時(shí)間:項(xiàng)目中已經(jīng)有上百個(gè)頁(yè)面了,如果一個(gè)一個(gè)的加,浪費(fèi)時(shí)間不說(shuō),以后增加了新頁(yè)面,還需要添加方法。
解決方法:我們可以發(fā)現(xiàn)頁(yè)面都繼承了UIViewController,想要在每個(gè)頁(yè)面都執(zhí)行的代碼,可以寫(xiě)在這些頁(yè)面的父類中。我們可以把代碼寫(xiě)在UIViewController中,使用分類(category)。
1.2 :分類
一個(gè)類中的代碼非常多,如果超過(guò)1000多行,那么在翻閱的時(shí)候也會(huì)覺(jué)得很困難,但是繼承下來(lái),再使用的時(shí)候,只能集成子類,而且方法比較多,想要直接分散開(kāi)來(lái),不是特別容易,所以可以采取分類的方式,把想通類型的代碼加載在一起。
好處:可以把類的實(shí)現(xiàn)分開(kāi)在幾個(gè)不同的文件里面。這樣做有幾個(gè)顯而易見(jiàn)的好處,
?a)可以減少單個(gè)文件的體積
?b)可以把不同的功能組織到不同的category里
?c)可以由多個(gè)開(kāi)發(fā)者共同完成一個(gè)類
?d)可以按需加載想要的category 等等。
并且 還可以按照需要加載想要的category。
1.3:引用父類未公開(kāi)方法
比如父類 XSDLabel:
XSDLabel1繼承自XSDLabel:
現(xiàn)在需要在設(shè)置text時(shí),同時(shí)設(shè)置文字顏色,調(diào)用父類的giveTextRandomColor:
直接編譯會(huì)報(bào)錯(cuò):
編譯器提示找不到父類的方法
在子類中聲明父類類別后,即可通過(guò)編譯:
類別名private是任意的,但不可以缺省。
請(qǐng)不要亂來(lái):蘋果官方會(huì)拒絕使用系統(tǒng)私有API的應(yīng)用上架,因此即使學(xué)會(huì)了如何調(diào)用私有方法,在遇到調(diào)用其它類的私有方法時(shí),要謹(jǐn)慎處理,盡量用其它方法替代。
1.4:聲明私有方法
1.5:模擬多繼承
1.6:把framework的私有方法公開(kāi)
2:不能用:爭(zhēng)議性的特點(diǎn):能做什么不能做什么
(如果看下面簡(jiǎn)單的原因解釋不明白的 可以繼續(xù)向下面看category的內(nèi)部原理,講的比較細(xì)致)
2.1:只能添加方法,不能為一個(gè)類動(dòng)態(tài)的添加成員變量,可以給類動(dòng)態(tài)增加方法和屬性。
在類別中聲明的屬性,將無(wú)法存取,
原因:因?yàn)榉诸愂莿?dòng)態(tài)時(shí),此時(shí),類的框架已經(jīng)實(shí)現(xiàn),屬性列表里并沒(méi)有給動(dòng)態(tài)添加的屬性賦值。其次并不是不能添加屬性,而是添加一個(gè)Property,系統(tǒng)不會(huì)自動(dòng)給他創(chuàng)建set和get方法,運(yùn)用runtime:objc_setAssociatedObject和objc_getAssociatedObject給分類創(chuàng)建set和get方法。。
因?yàn)榉椒ê蛯傩圆⒉弧皩儆凇鳖悓?shí)例,而成員變量“屬于”類實(shí)例。我們所說(shuō)的“類實(shí)例”概念,指的是一塊內(nèi)存區(qū)域,包含了isa指針和所有的成員變量。所以假如允許動(dòng)態(tài)修改類成員變量布局,已經(jīng)創(chuàng)建出的類實(shí)例就不符合類定義了,變成了無(wú)效對(duì)象。但方法定義是在objc_class中管理的,不管如何增刪類方法,都不影響類實(shí)例的內(nèi)存布局,已經(jīng)創(chuàng)建出的類實(shí)例仍然可正常使用。具體請(qǐng)看這里
2.2:類別中的方法,會(huì)“覆蓋”父類中的同名方法,無(wú)法再調(diào)用父類中的方法(因?yàn)轭悇e中無(wú)法使用super)
為防止意外覆蓋,總是應(yīng)該給類別加上前綴。
原因:這個(gè)不是覆蓋,因?yàn)樗械念惖姆椒ê头诸惖姆椒?#xff0c;都會(huì)在類的方法列表中存在,類自己的是會(huì)先放進(jìn)去,類別是動(dòng)態(tài)添加的所以是后放入的,根據(jù)取方法的順序,后進(jìn)先出,而系統(tǒng)的特性是,只要找到了相同的方法便不再繼續(xù)尋找,所以如果分類實(shí)現(xiàn)了本類的方法,會(huì)先調(diào)用分類的,如果有多個(gè)分類同時(shí)實(shí)現(xiàn),那要看編譯的時(shí)候哪個(gè)方法最后放入。
Category 可以實(shí)現(xiàn)原始類的方法:
具體原因:
1).重寫(xiě)原生方法之后 會(huì)覆蓋掉原有的方法,因?yàn)樵贠C中是runtime執(zhí)行 且方法執(zhí)行時(shí)僅僅有一個(gè)方法會(huì)被執(zhí)行,除loadView會(huì)先執(zhí)行原生方法然后執(zhí)行category外 其他都是執(zhí)行category重寫(xiě)的方法體。
2).當(dāng)多個(gè)地方對(duì)系統(tǒng)本身的方法進(jìn)行了重寫(xiě) 則執(zhí)行時(shí)系統(tǒng)無(wú)法知道執(zhí)行哪一個(gè)方法。
3).category的出現(xiàn)只是為了擴(kuò)展原有的類。如果需要重寫(xiě) 使用繼承吧。
2.3:不同文件中的同名類別,同名方法,不會(huì)報(bào)錯(cuò),實(shí)際執(zhí)行的方法以最后一個(gè)加載的文件為準(zhǔn),因此使用前綴防止類別人互相覆蓋。
2.4:分類中可以訪問(wèn)原來(lái)類中的成員變量,但是只能訪問(wèn)@protect和@public形式的變量。如果想要訪問(wèn)本類中的私有變量,分類和子類一樣,只能通過(guò)方法來(lái)訪問(wèn)。
原因:
extension看起來(lái)很像一個(gè)匿名的category,但是extension和有名字的category幾乎完全是兩個(gè)東西。 extension在編譯期決議,它就是類的一部分,在編譯期和頭文件里的@interface以及實(shí)現(xiàn)文件里的@implement一起形成一個(gè)完整的類,它伴隨類的產(chǎn)生而產(chǎn)生,亦隨之一起消亡。extension一般用來(lái)隱藏類的私有信息,你必須有一個(gè)類的源碼才能為一個(gè)類添加extension,所以你無(wú)法為系統(tǒng)的類比如NSString添加extension。但是category則完全不一樣,它是在運(yùn)行期決議的。就category和extension的區(qū)別來(lái)看,我們可以推導(dǎo)出一個(gè)明顯的事實(shí),extension可以添加實(shí)例變量,而category是無(wú)法添加實(shí)例變量的(因?yàn)樵谶\(yùn)行期,對(duì)象的內(nèi)存布局已經(jīng)確定,如果添加實(shí)例變量就會(huì)破壞類的內(nèi)部布局,這對(duì)編譯型語(yǔ)言來(lái)說(shuō)是災(zāi)難性的)。
想知道詳細(xì)區(qū)別的,請(qǐng)看這里
小記:
我自己寫(xiě)了一個(gè)分類,通過(guò)runtime來(lái)獲取類的方法和屬性,發(fā)現(xiàn)分類方法和本類方法在一個(gè)方法列表,引用不引用都在,只是引用之后,在類里就可以用這個(gè)分類的方法。不引用的話,編譯器找不到方法,就會(huì)報(bào)錯(cuò)。
3:原因:內(nèi)部原理
現(xiàn)在講解最重要的內(nèi)部原理,上面的有爭(zhēng)議的特性還沒(méi)有徹底明白為啥的 可以細(xì)細(xì)看下面。
3.1:category真面目
我們知道,所有的OC類和對(duì)象,在runtime層都是用struct表示的,category也不例外,在runtime層,category用結(jié)構(gòu)體category_t(在objc-runtime-new.h中可以找到此定義),它包含了
1)、類的名字(name)
2)、類(cls)
3)、category中所有給類添加的實(shí)例方法的列表(instanceMethods)
4)、category中所有添加的類方法的列表(classMethods)
5)、category實(shí)現(xiàn)的所有協(xié)議的列表(protocols)
6)、category中添加的所有屬性(instanceProperties)
從category的定義也可以看出category的可為(可以添加實(shí)例方法,類方法,甚至可以實(shí)現(xiàn)協(xié)議,添加屬性)和不可為(無(wú)法添加實(shí)例變量)。
ok,我們先去寫(xiě)一個(gè)category看一下category到底為何物:
MyClass.h:
MyClass.m:
我們使用clang的命令去看看category到底會(huì)變成什么:
clang -rewrite-objc MyClass.m好吧,我們得到了一個(gè)3M大小,10w多行的.cpp文件(這絕對(duì)是Apple值得吐槽的一點(diǎn)),我們忽略掉所有和我們無(wú)關(guān)的東西,在文件的最后,我們找到了如下代碼片段:
我們可以看到,
1)、首先編譯器生成了實(shí)例方法列表OBJC$_CATEGORY_INSTANCE_METHODSMyClass$_MyAddition和屬性列表OBJC$_PROP_LISTMyClass$_MyAddition,兩者的命名都遵循了公共前綴+類名+category名字的命名方式,而且實(shí)例方法列表里面填充的正是我們?cè)贛yAddition這個(gè)category里面寫(xiě)的方法printName,而屬性列表里面填充的也正是我們?cè)贛yAddition里添加的name屬性。還有一個(gè)需要注意到的事實(shí)就是category的名字用來(lái)給各種列表以及后面的category結(jié)構(gòu)體本身命名,而且有static來(lái)修飾,所以在同一個(gè)編譯單元里我們的category名不能重復(fù),否則會(huì)出現(xiàn)編譯錯(cuò)誤。
2)、其次,編譯器生成了category本身OBJC$_CATEGORYMyClass$_MyAddition,并用前面生成的列表來(lái)初始化category本身。
3)、最后,編譯器在DATA段下的objc_catlist section里保存了一個(gè)大小為1的category_t的數(shù)組L_OBJC_LABELCATEGORY$(當(dāng)然,如果有多個(gè)category,會(huì)生成對(duì)應(yīng)長(zhǎng)度的數(shù)組^_^),用于運(yùn)行期category的加載。
到這里,編譯器的工作就接近尾聲了,對(duì)于category在運(yùn)行期怎么加載,我們下節(jié)揭曉。
3.2、追本溯源-category如何加載
我們知道,Objective-C的運(yùn)行是依賴OC的runtime的,而OC的runtime和其他系統(tǒng)庫(kù)一樣,是OS X和iOS通過(guò)dyld動(dòng)態(tài)加載的。對(duì)于OC運(yùn)行時(shí),入口方法如下(在objc-os.mm文件中):
category被附加到類上面是在map_images的時(shí)候發(fā)生的,在new-ABI的標(biāo)準(zhǔn)下,_objc_init里面的調(diào)用的map_images最終會(huì)調(diào)用objc-runtime-new.mm里面的_read_images方法,而在_read_images方法的結(jié)尾,有以下的代碼片段:
首先,我們拿到的catlist就是上節(jié)中講到的編譯器為我們準(zhǔn)備的category_t數(shù)組,關(guān)于是如何加載catlist本身的,我們暫且不表,這和category本身的關(guān)系也不大,有興趣的同學(xué)可以去研究以下Apple的二進(jìn)制格式和load機(jī)制。
略去PrintConnecting這個(gè)用于log的東西,這段代碼很容易理解:
1)、把category的實(shí)例方法、協(xié)議以及屬性添加到類上
2)、把category的類方法和協(xié)議添加到類的metaclass上
值得注意的是,在代碼中有一小段注釋 /?|| cat->classProperties?/,看來(lái)蘋果有過(guò)給類添加屬性的計(jì)劃啊。
ok,我們接著往里看,category的各種列表是怎么最終添加到類上的,就拿實(shí)例方法列表來(lái)說(shuō)吧:
在上述的代碼片段里,addUnattachedCategoryForClass只是把類和category做一個(gè)關(guān)聯(lián)映射,而remethodizeClass才是真正去處理添加事宜的功臣。
而對(duì)于添加類的實(shí)例方法而言,又會(huì)去調(diào)用attachCategoryMethods這個(gè)方法,我們?nèi)タ聪耡ttachCategoryMethods:
attachCategoryMethods做的工作相對(duì)比較簡(jiǎn)單,它只是把所有category的實(shí)例方法列表拼成了一個(gè)大的實(shí)例方法列表,然后轉(zhuǎn)交給了attachMethodLists方法(我發(fā)誓,這是本節(jié)我們看的最后一段代碼了^_^),這個(gè)方法有點(diǎn)長(zhǎng),我們只看一小段:
需要注意的有兩點(diǎn):
1)、category的方法沒(méi)有“完全替換掉”原來(lái)類已經(jīng)有的方法,也就是說(shuō)如果category和原來(lái)類都有methodA,那么category附加完成之后,類的方法列表里會(huì)有兩個(gè)methodA
2)、category的方法被放到了新方法列表的前面,而原來(lái)類的方法被放到了新方法列表的后面,這也就是我們平常所說(shuō)的category的方法會(huì)“覆蓋”掉原來(lái)類的同名方法,這是因?yàn)檫\(yùn)行時(shí)在查找方法的時(shí)候是順著方法列表的順序查找的,它只要一找到對(duì)應(yīng)名字的方法,就會(huì)罷休^_^,殊不知后面可能還有一樣名字的方法。
3.3:旁枝末葉-category和+load方法
我們知道,在類和category中都可以有+load方法,那么有兩個(gè)問(wèn)題:
1)、在類的+load方法調(diào)用的時(shí)候,我們可以調(diào)用category中聲明的方法么?
2)、這么些個(gè)+load方法,調(diào)用順序是咋樣的呢?
鑒于上述幾節(jié)我們看的代碼太多了,對(duì)于這兩個(gè)問(wèn)題我們先來(lái)看一點(diǎn)直觀的:
我們的代碼里有MyClass和MyClass的兩個(gè)category (Category1和Category2),MyClass和兩個(gè)category都添加了+load方法,并且Category1和Category2都寫(xiě)了MyClass的printName方法。
在Xcode中點(diǎn)擊Edit Scheme,添加如下兩個(gè)環(huán)境變量(可以在執(zhí)行l(wèi)oad方法以及加載category的時(shí)候打印log信息,更多的環(huán)境變量選項(xiàng)可參見(jiàn)objc-private.h):
運(yùn)行項(xiàng)目,我們會(huì)看到控制臺(tái)打印很多東西出來(lái),我們只找到我們想要的信息,順序如下:
objc[1187]: REPLACED: -[MyClass printName] by category Category1
objc[1187]: REPLACED: -[MyClass printName] by category Category2
.
.
.
objc[1187]: LOAD: class 'MyClass' scheduled for +load
objc[1187]: LOAD: category 'MyClass(Category1)' scheduled for +load
objc[1187]: LOAD: category 'MyClass(Category2)' scheduled for +load
objc[1187]: LOAD: +[MyClass load]
.
.
.
objc[1187]: LOAD: +[MyClass(Category1) load]
.
.
.
objc[1187]: LOAD: +[MyClass(Category2) load]
所以,對(duì)于上面兩個(gè)問(wèn)題,答案是很明顯的:
1)、可以調(diào)用,因?yàn)楦郊觕ategory到類的工作會(huì)先于+load方法的執(zhí)行
2)、+load的執(zhí)行順序是先類,后category,而category的+load執(zhí)行順序是根據(jù)編譯順序決定的。
目前的編譯順序是這樣的:
我們調(diào)整一個(gè)Category1和Category2的編譯順序,run。ok,我們可以看到控制臺(tái)的輸出順序變了:
objc[1187]: REPLACED: -[MyClass printName] by category Category2
objc[1187]: REPLACED: -[MyClass printName] by category Category1
.
.
.
objc[1187]: LOAD: class 'MyClass' scheduled for +load
objc[1187]: LOAD: category 'MyClass(Category2)' scheduled for +load
objc[1187]: LOAD: category 'MyClass(Category1)' scheduled for +load
objc[1187]: LOAD: +[MyClass load]
.
.
.
objc[1187]: LOAD: +[MyClass(Category2) load]
.
.
.
objc[1187]: LOAD: +[MyClass(Category1) load]
雖然對(duì)于+load的執(zhí)行順序是這樣,但是對(duì)于“覆蓋”掉的方法,則會(huì)先找到最后一個(gè)編譯的category里的對(duì)應(yīng)方法。
這一節(jié)我們只是用很直觀的方式得到了問(wèn)題的答案,有興趣的同學(xué)可以繼續(xù)去研究一下OC的運(yùn)行時(shí)代碼。?
3.4、觸類旁通-category和方法覆蓋
鑒于上面幾節(jié)我們已經(jīng)把原理都講了,這一節(jié)只有一個(gè)問(wèn)題:
怎么調(diào)用到原來(lái)類中被category覆蓋掉的方法?
對(duì)于這個(gè)問(wèn)題,我們已經(jīng)知道category其實(shí)并不是完全替換掉原來(lái)類的同名方法,只是category在方法列表的前面而已,所以我們只要順著方法列表找到最后一個(gè)對(duì)應(yīng)名字的方法,就可以調(diào)用原來(lái)類的方法:
3.5、更上一層-category和關(guān)聯(lián)對(duì)象
如上所見(jiàn),我們知道在category里面是無(wú)法為category添加實(shí)例變量的。但是我們很多時(shí)候需要在category中添加和對(duì)象關(guān)聯(lián)的值,這個(gè)時(shí)候可以求助關(guān)聯(lián)對(duì)象來(lái)實(shí)現(xiàn)。
MyClass+Category1.h:
MyClass+Category1.m:
但是關(guān)聯(lián)對(duì)象又是存在什么地方呢? 如何存儲(chǔ)? 對(duì)象銷毀時(shí)候如何處理關(guān)聯(lián)對(duì)象呢?
我們?nèi)シ幌聄untime的源碼,在objc-references.mm文件中有個(gè)方法_object_set_associative_reference:
我們可以看到所有的關(guān)聯(lián)對(duì)象都由AssociationsManager管理,而AssociationsManager定義如下:
AssociationsManager里面是由一個(gè)靜態(tài)AssociationsHashMap來(lái)存儲(chǔ)所有的關(guān)聯(lián)對(duì)象的。這相當(dāng)于把所有對(duì)象的關(guān)聯(lián)對(duì)象都存在一個(gè)全局map里面。而map的的key是這個(gè)對(duì)象的指針地址(任意兩個(gè)不同對(duì)象的指針地址一定是不同的),而這個(gè)map的value又是另外一個(gè)AssociationsHashMap,里面保存了關(guān)聯(lián)對(duì)象的kv對(duì)。
而在對(duì)象的銷毀邏輯里面,見(jiàn)objc-runtime-new.mm:
嗯,runtime的銷毀對(duì)象函數(shù)objc_destructInstance里面會(huì)判斷這個(gè)對(duì)象有沒(méi)有關(guān)聯(lián)對(duì)象,如果有,會(huì)調(diào)用_object_remove_assocations做關(guān)聯(lián)對(duì)象的清理工作。
這個(gè)是從大牛的網(wǎng)址里看的資料,順便整理下來(lái),連接地址:
http://tech.meituan.com/DiveIntoCategory.html(這里是內(nèi)部原理講解,很透徹)
感謝大神:都是大神的精華,我整理成自己需要學(xué)習(xí)的效果以便以后自己快速查閱。
總結(jié)
以上是生活随笔為你收集整理的category 详解的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: linux 小企鹅输入法,Linux基础
- 下一篇: JAVA 图片格式转换 jpg、jpeg