Category在iOS開發中使用很是的頻繁,特別是在爲系統類進行拓展的時候,咱們能夠不用繼承系統類,直接給系統類添加方法,最大程度的體現了Objective-C的動態語言特性。安全
本文篇幅較長,但內容完整,建議能跟隨文章內容探索一遍,畢竟實踐出真知。bash
將類的實現分散到多個不一樣的文件或多個不一樣的框架中。以下:不一樣的功能模塊用不一樣的Category處理 數據結構
能夠在不修改原來類的基礎上,爲一個類添加擴展方法。如咱們須要給系統自帶的類添加方法。框架
會覆蓋原類中方法名相同的方法,多個Category的同名方法,會按照編譯順序,執行最後編譯
的Category裏的方法。 ide
和Extension(擴展)
的區別:函數
@property
添加屬性,可是Category添加的屬性不能生成成員變量和getter
,setter
方法的實現,即不能經過_var
調用,不過能夠手動經過objc_get/setAssociatedObject
手動實現。上面說了一大堆,如今正式開始探索歷程,探索上面解釋的正確性,先來看看編譯期幹了什麼吧。工具
新建了一個測試工程,建立了一個實體類,和一個Test分類 , 分類裏聲明一個Test方法,並cmd+B
編譯一下 佈局
打開命令行工具,cd到當前目錄下,執行clang -rewrite-objc Person+Test.m
命令,而後找到當前工程文件夾,找到編譯後的 Person+Test.cpp
文件並打開測試
文件內容很是多,因爲知道category
的結構體是_categoy_t
, 全局搜索找到了它的結構體ui
struct _category_t {
const char *name;
struct _class_t *cls; //類
const struct _method_list_t *instance_methods;//實例方法列表
const struct _method_list_t *class_methods;//類方法列表
const struct _protocol_list_t *protocols;//協議列表
const struct _prop_list_t *properties;//屬性列表
};
複製代碼
_category_t
,找到了咱們的測試的分類static struct _category_t _OBJC_$_CATEGORY_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
"Person",
0, // &OBJC_CLASS_$_Person,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test,
0,
0,
0,
};
複製代碼
section ("__DATA,__objc_const")
,是一個段標識,它會存放到咱們的dyld
加載Macho
可執行文件裏的這個section
段裏(關於dyld及Macho文件內容較多,這裏暫時不作詳細解釋,這裏瞭解到它會在編譯的時候會存放到Macho
文件裏便可)_category_t
的結構體,發現這裏的類名是Person
,實例方法列表裏存放了一個Test
方法_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"Test", "v16@0:8", (void *)_I_Person_Test_Test}}
};
複製代碼
_objc_method
結構體對照着能夠看到imp
裏包含了Sel
方法編號,方法簽名及真實的函數地址struct _objc_method {
struct objc_selector * _cmd;
const char *method_type;
void *_imp;
};
複製代碼
category
編譯成結構體,而後把對應的值填充到結構體裏,保存在Macho
可執行文件裏_category_t
,發現還有以下代碼static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
&_OBJC_$_CATEGORY_Person_$_Test,
};
複製代碼
category
方法都保存到__objc_catlist
裏,也就是在加載到Macho
文件裏的對應的section
段裏在編譯期,把category
編譯成對應的結構體保存到Macho
文件裏的section
段,把全部的分類方法都保存到Macho
文件__objc_catlist
這個section
段裏
瞭解到編譯器主要作了保存的操做,那麼運行期毫無疑問是作的加載操做,須要把剛剛編譯期保存的內容都進行加載。
先來看看Category
是如何被加載的
dyld是蘋果的動態加載器,用來加載image(image指的是Mach-O格式的二進制文件,不是圖片)
當程序啓動時,系統內核首先會加載dyld,而dyld會將咱們APP所依賴的各類庫加載到內存中,其中就包括libobjc
庫(OC和runtime),這些工做,是在APP的main函數執行以前完成的
_objc_init
是Object-C runtime 的入口函數,在這裏主要是讀取Mach-O
文件OC對應的Segment section
,並根據其中的數據代碼信息,完成爲OC的內存佈局,以及初始化runtime
相關的數據結構。
先驗證一下_objc_init
是不是入口函數,打開剛纔的測試工程,添加一個符號斷點_objc_init
,而後運行工程
_objc_init
,接下來纔會執行
dyld_start
加載函數
源碼工程objc4網盤連接 密碼:tuw8
objc4
,直接搜索_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();
tls_init();
static_init();
lock_init();
exception_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
複製代碼
init
函數,這裏重點是_dyld_objc_notify_register
,註冊了三個回調函數&map_images
將image加載進內存load_images
dyld初始化加載image方法unmap_images
移除內存咱們要探索Category
是如何被加載進內存的,因此要看&map_images
到底作了什麼,點進這個方法
void 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);
}
複製代碼
_read_images
void map_images_nolock(unsigned mhCount, const char * const mhPaths[], const struct mach_header * const mhdrs[]) {
//其他無關代碼已省略
if (hCount > 0) {
_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
}
firstTime = NO;
}
複製代碼
_read_images
函數裏的內容較多,找到與category
相關的代碼,這段代碼較長void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
//其他代碼已省略
for (EACH_HEADER) {
category_t **catlist =
_getObjc2CategoryList(hi, &count);
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
if (!cls) {
// Category's target class is missing (probably weak-linked).
// Disavow any knowledge of this category.
catlist[i] = nil;
if (PrintConnecting) {
_objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
"missing weak-linked target class",
cat->name, cat);
}
continue;
}
// Process this category.
// First, register the category with its target class.
// Then, rebuild the class's method lists (etc) if
// the class is realized.
bool classExists = NO;
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
addUnattachedCategoryForClass(cat, cls, hi);
if (cls->isRealized()) {
remethodizeClass(cls);
classExists = YES;
}
if (PrintConnecting) {
_objc_inform("CLASS: found category -%s(%s) %s",
cls->nameForLogging(), cat->name,
classExists ? "on existing class" : "");
}
}
if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA());
}
if (PrintConnecting) {
_objc_inform("CLASS: found category +%s(%s)",
cls->nameForLogging(), cat->name);
}
}
}
}
複製代碼
_getObjc2CategoryList
函數是讀取全部的category
方法,點進該方法能夠看到它其實就是讀取咱們編譯期時的_objc_catlist
的section
段內容GETSECT(_getObjc2CategoryList, category_t *, "__objc_catlist");
複製代碼
if (cat->instanceMethods)
能夠看到這裏判斷了當前category
的方法是類方法仍是實例方法,並分別作不一樣的處理addUnattachedCategoryForClass(cat, cls, hi);
這裏是把category
與該class
原類關聯映射起來,能夠點進去該方法看內容
remethodizeClass(cls);
,看名字像是從新設置類裏面的函數,點進去看看具體函數內容static void remethodizeClass(Class cls) {
//多餘代碼省略
category_list *cats;
// Re-methodizing: check for more categories
if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
attachCategories(cls, cats, true /*flush caches*/);
free(cats);
}
}
複製代碼
attachCategories
關聯分類函數的具體實現static void attachCategories(Class cls, category_list *cats, bool flush_caches) {
//省略無關代碼
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists));
property_list_t **proplists = (property_list_t **)
malloc(cats->count * sizeof(*proplists));
protocol_list_t **protolists = (protocol_list_t **)
malloc(cats->count * sizeof(*protolists));
//省略無關代碼
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
rw->methods.attachLists(mlists, mcount);
}
複製代碼
mlists
prepareMethodLists
和attachLists
prepareMethodLists
//省略其餘無關代碼
for (int i = 0; i < addedCount; i++) {
//把要添加原類的方法列表取出來
method_list_t *mlist = addedLists[i];
assert(mlist);
// Fixup selectors if necessary
if (!mlist->isFixedUp()) {
fixupMethodList(mlist, methodsFromBundle, true/*sort*/);
}
}
複製代碼
取出來方法列表後,調用了fixupMethodList
,點進去看看
這裏作的是把方法列表裏的方法都註冊到原類裏
總之,prepareMethodLists
作的是添加方法列表前的準備工做
回到外面,點擊進入attachLists
看看是如何關聯原類方法的
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;
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
/** 拷貝的操做 void *memmove(void *__dst, const void *__src, size_t __len); */
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
複製代碼
memmove
和memcpy
函數完成了拷貝操做,那麼這兩個函數具體究竟是作了什麼呢?
memmove
是把原類裏的方法列表,向後移動了要添加的方法列表的大小的距離
memcopy
是把要添加的方法列表拷貝到原類剛剛的方法列表裏空出來的位置上
衆所周知,在分類裏能夠經過objc_get/setAssociatedObject
來模擬添加屬性,那麼它究竟是如何實現的呢?
objc_setAssociatedObject
,找到其方法內容void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
_object_set_associative_reference(object, (void *)key, value, policy);
}
複製代碼
繼續找_object_set_associative_reference
,這裏的代碼較多,逐行分析
acquireValue
是進行內存管理,能夠點進去看一下
這裏有一個AssociationsManager
,看到它裏面有個AssociationsHashMap
,而且訪問會加鎖,是線程安全的
disguised_ptr_t disguised_object = DISGUISE(object);
這裏使用對象取反後的值做爲key,迭代器裏的value 是ObjectAssociationMap
再看ObjectAssociationMap
,它的key是用戶傳進來的自定義key
,它的value是ObjcAssociation
還有最後一個重要的方法setHasAssociatedObjects
,這裏把屬性和類關聯起來,而且設置isa指針的標識isa.has_assoc
,以便釋放的時候使用
同理,objc_getAssociatedObject
也是從這裏取出來值的
在上面咱們知道屬性是經過類的isa關聯起來的,那麼理應在這個對象銷燬的時候一塊兒移除該屬性。 一樣的在當前objc源碼裏搜索dealloc
,找到了它的實現
- (void)dealloc {
_objc_rootDealloc(self);
}
複製代碼
void
_objc_rootDealloc(id obj)
{
assert(obj);
obj->rootDealloc();
}
複製代碼
rootDealloc
inline void
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
free(this);
}
else {
object_dispose((id)this);
}
}
複製代碼
object_dispose
id object_dispose(id obj) {
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}
複製代碼
objc_destructInstance
裏找到了屬性的銷燬void *objc_destructInstance(id obj) {
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);
obj->clearDeallocating();
}
return obj;
}
複製代碼
這裏有經過isa.has_assoc
標識符判斷當前對象是否有關聯屬性,若是有就調用上面代碼裏的_object_remove_assocations
移除關聯屬性
繼續看_object_remove_assocations
,和咱們設置關聯屬性的代碼相似,不過這裏是取出來而後delete refs
刪除
以上就是探索Category
底層原理的整個過程,也使得文章開頭的Category
的做用獲得驗證。整個過程是枯燥和冗長的,可是探索完仍是有很大的收穫。本文篇幅很長,但願你們也能親自試着探索一遍,能不只僅知足於知道這是什麼,還要去探究爲何會是這樣。