結合 category 工作原理分析 OC2.0 中的 runtime

絕大多數 iOS 開發者在學習 runtime 時都閱讀過 runtime.h 文件中的這段代碼:

struct objc_class {Class isa  OBJC_ISA_AVAILABILITY;#if !__OBJC2__Class super_class                                        OBJC2_UNAVAILABLE;const char *name                                         OBJC2_UNAVAILABLE;long version                                             OBJC2_UNAVAILABLE;long info                                                OBJC2_UNAVAILABLE;long instance_size                                       OBJC2_UNAVAILABLE;struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;struct objc_cache *cache                                 OBJC2_UNAVAILABLE;struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
#endif} OBJC2_UNAVAILABLE;復制代碼

可以看到其中保存了類的實例變量,方法列表等信息。

不知道有多少讀者思考過 OBJC2_UNAVAILABLE 意味著什么。其實早在 2006 年,蘋果在 WWDC 大會上就發布了 Objective-C 2.0,其中的改動包括 Max OS X 平臺上的垃圾回收機制(現已廢棄),runtime 性能優化等。

這意味著上述代碼,以及任何帶有 OBJC2_UNAVAILABLE 標記的內容,都已經在 2006 年就永遠的告別了我們,只停留在歷史的文檔中。

Category 的原理

雖然上述代碼已經過時,但仍具備一定的參考意義,比如 methodLists 作為一個二級指針,其中每個元素都是一個數組,數組中的每個元素則是一個方法。

接下來就介紹一下 category 的工作原理,在美團的技術博客 深入理解Objective-C:Category 中已經有了非常詳細的解釋,然而可能由于時間問題,其中的不少內容已經過時,我根據目前最新的版本(objc-680) 做一些簡單的分析,為了便于閱讀,在不影響代碼邏輯的前提下有可能刪除部分無關緊要的內容。

概述

首先 runtime 依賴于 dyld 動態加載,在 objc-os.mm 文件中可以找到入口,它的調用棧簡單整理如下:

void _objc_init(void)
└──const char *map_2_images(...)└──const char *map_images_nolock(...)└──void _read_images(header_info **hList, uint32_t hCount)復制代碼

以上四個方法可以理解為 runtime 的初始化過程,我們暫且不深究。在 _read_images 方法中有如下代碼:

if (cat->classMethods  ||  cat->protocols  /* ||  cat->classProperties */) {addUnattachedCategoryForClass(cat, cls->ISA(), hi);if (cls->ISA()->isRealized()) {remethodizeClass(cls->ISA());}
}復制代碼

根據注釋可見蘋果曾經計劃利用 category 來添加屬性。在 addUnattachedCategoryForClass 方法中會找到當前類的所有 category,然后在 remethodizeClass 真正的去做處理。不過到目前為止還沒有接觸到相關的 category 處理,我們繼續沿著調用棧向下走:

void _read_images(header_info **hList, uint32_t hCount)
└──static void remethodizeClass(Class cls)└──static void attachCategories(Class cls, category_list *cats, bool flush_caches)復制代碼

這里的 attachCategories 就是處理 category 的核心所在,不過在閱讀這段代碼之前,我們有必要先熟悉一下相關的數據結構。

Category 相關的數據結構

首先來了解一下一個 Category 是如何存儲的,在 objc-runtime-new.h 中可以看到如下定義,我只列出了其中成員變量:

struct category_t {const char *name;classref_t cls;struct method_list_t *instanceMethods;struct method_list_t *classMethods;struct protocol_list_t *protocols;struct property_list_t *instanceProperties;
};復制代碼

可見一個 category 持有了一個 method_list_t 類型的數組,method_list_t 又繼承自 entsize_list_tt,這是一種泛型容器:

struct method_list_t : entsize_list_tt<method_t, method_list_t, 0x3> {// 成員變量和方法
};template <typename Element, typename List, uint32_t FlagMask>
struct entsize_list_tt {uint32_t entsizeAndFlags;uint32_t count;Element first;
};復制代碼

這里的 entsize_list_tt 可以理解為一個容器,擁有自己的迭代器用于遍歷所有元素。 Element 表示元素類型,List 用于指定容器類型,最后一個參數為標記位。

雖然這段代碼實現比較復雜,但仍可了解到 method_list_t 是一個存儲 method_t 類型元素的容器。method_t 結構體的定義如下:

struct method_t {SEL name;const char *types;IMP imp;
};復制代碼

最后,我們還有一個結構體 category_list 用來存儲所有的 category,它的定義如下:

struct locstamped_category_list_t {uint32_t count;locstamped_category_t list[0];
};
struct locstamped_category_t {category_t *cat;struct header_info *hi;
};
typedef locstamped_category_list_t category_list;復制代碼

除了標記存儲的 category 的數量外,locstamped_category_list_t 結構體還聲明了一個長度為零的數組,這其實是 C99 中的一種寫法,允許我們在運行期動態的申請內存。

以上就是相關的數據結構,只要了解到這個程度就可以繼續讀源碼了。

處理 Category

對 Category 中方法的解析并不復雜,首先來看一下 attachCategories 的簡化版代碼:

static void attachCategories(Class cls, category_list *cats, bool flush_caches) {if (!cats) return;bool isMeta = cls->isMetaClass();method_list_t **mlists = (method_list_t **)malloc(cats->count * sizeof(*mlists));// Count backwards through cats to get newest categories firstint mcount = 0;int i = cats->count;while (i--) {auto& entry = cats->list[i];method_list_t *mlist = entry.cat->methodsForMeta(isMeta);if (mlist) {mlists[mcount++] = mlist;}}auto rw = cls->data();prepareMethodLists(cls, mlists, mcount, NO, fromBundle);rw->methods.attachLists(mlists, mcount);free(mlists);if (flush_caches  &&  mcount > 0) flushCaches(cls);
}復制代碼

首先,通過 while 循環,我們遍歷所有的 category,也就是參數 cats 中的 list 屬性。對于每一個 category,得到它的方法列表 mlist 并存入 mlists 中。

換句話說,我們將所有 category 中的方法拼接到了一個大的二維數組中,數組的每一個元素都是裝有一個 category 所有方法的容器。這句話比較繞,但你可以把 mlists 理解為文章開頭所說,舊版本的 objc_method_list **methodLists

在 while 循環外,我們得到了拼接成的方法,此時需要與類原來的方法合并:

auto rw = cls->data();
rw->methods.attachLists(mlists, mcount);復制代碼

這兩行代碼讀不懂是必然的,因為在 Objective-C 2.0 時代,對象的內存布局已經發生了一些變化。我們需要先了解對象的布局模型才能理解這段代碼。

Objective-C 2.0 對象布局模型

objc_class

相信讀到這里的大部分讀者都學習過文章開頭所說的對象布局模型,因此在這一部分,我們采用類比的方法,來看看 Objective-C 2.0 下發生了哪些改變。

首先,Classid 指針的定義并沒有發生改變,他們一個指向類對應的結構體,一個指向對象對應的結構體:

// objc.h
typedef struct objc_class *Class;
typedef struct objc_object *id;復制代碼

比較有意思的一點是,objc_class 結構體是繼承自 objc_object 的:

struct objc_object {Class isa  OBJC_ISA_AVAILABILITY;
};struct objc_class : objc_object {Class superclass;cache_t cache;             // formerly cache pointer and vtableclass_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flagsclass_rw_t *data() { return bits.data();}
};復制代碼

這一點也很容易理解,早在 Objective-C 1.0 時代,我們就知道一個對象的結構體只有 isa 指針,指向它所屬的類。而類的結構體也有 isa 指針指向它的元類。因此讓類結構體繼承自對象結構體就很容易理解了。

可見 Objective-C 1.0 的布局模型中,cachesuper_class 被原封不動的移過來了,而剩下的屬性則似乎消失不見。取而代之的是一個 bits 屬性,以及 data() 方法,這個方法調用的其實是 bits 屬性的 data() 方法,并返回了一個 class_rw_t 類型的結構體指針。

class_data_bits_t

以下是簡化版 class_data_bits_t 結構體的定義:

struct class_data_bits_t {uintptr_t bits;
public:class_rw_t* data() {return (class_rw_t *)(bits & FAST_DATA_MASK);}
}復制代碼

可見這個結構體只有一個 64 位的 bits 成員,存儲了一個指向 class_rw_t 結構體的指針和三個標志位。它實際上由三部分組成。首先由于 Mac OS X 只使用 47 位內存地址,所以前 17 位空余出來,提供給 retain/release 和 alloc/dealloc 方法使用,做一些優化。其次,由于內存對齊,指針地址的后三位都是 0,因此可以用來做標志位:

// class is a Swift class
#define FAST_IS_SWIFT           (1UL<<0)
// class or superclass has default retain/release/autorelease/retainCount/
//   _tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference
#define FAST_HAS_DEFAULT_RR     (1UL<<1)
// class's instances requires raw isa
#define FAST_REQUIRES_RAW_ISA   (1UL<<2)
// data pointer
#define FAST_DATA_MASK          0x00007ffffffffff8UL復制代碼

如果計算一下就會發現,FAST_DATA_MASK 這個 16 進制常量的二進制表示恰好后三位為0,且長度為47位: 11111111111111111111111111111111111111111111000,我們通過這個掩碼做按位與運算即可取出正確的指針地址。

引用 Draveness 在 深入解析 ObjC 中方法的結構 中的圖片做一個總結:

class_rw_t

bits 中包含了一個指向 class_rw_t 結構體的指針,它的定義如下:

struct class_rw_t {uint32_t flags;uint32_t version;const class_ro_t *ro;method_array_t methods;property_array_t properties;protocol_array_t protocols;
}復制代碼

注意到有一個名字很類似的結構體 class_ro_t,這里的 'rw' 和 ro' 分別表示 'readwrite' 和 'readonly'。因為 class_ro_t 存儲了一些由編譯器生成的常量。

These are emitted by the compiler and are part of the ABI.

正是由于 class_ro_t 中的兩個屬性 instanceStartinstanceSize 的存在,保證了 Objective-C2.0 的 ABI 穩定性。因為即使父類增加方法,子類也可以在運行時重新計算 ivar 的偏移量,從而避免重新編譯。

關于 ABI 穩定性的問題,本文不做贅述,讀者可以參考 Non Fragile ivars。

如果閱讀 class_ro_t 結構體的定義就會發現,舊版本實現中類結構體中的大部分成員變量現在都定義在 class_ro_tclass_rw_t 這兩個結構體中了。感興趣的讀者可以自行對比,本文不再贅述。

class_rw_t 結構體中還有一個 methods 成員變量,它的類型是 method_array_t,繼承自 list_array_tt

list_array_tt 是一個泛型結構體,用于存儲一些元數據,而它實際上是元數據的二維數組:

template <typename Element, typename List>{struct array_t {uint32_t count;List* lists[0];};
}
class method_array_t : public list_array_tt<method_t, method_list_t>復制代碼

其中 Element 表示元數據的類型,比如 method_t,而 List 則表示用于存儲元數據的一維數組,比如 method_list_t

list_array_tt 有三種狀態:

  1. 自身為空,可以類比為 [[]]
  2. 它只有一個指針,指向一個元數據的集合,可以類比為 [[1, 2]]
  3. 它有多個指針,指向多個元數據的集合,可以類比為 [[1, 2], [3, 4]]

當一個類剛創建時,它可能處于狀態 1 或 2,但如果使用 class_addMethod 或者 category 來添加方法,就會進入狀態 3,而且一旦進入狀態 3 就再也不可能回到其他狀態,即使新增的方法后來又被移除掉。

方法合并

掌握了這些 runtime 的基礎只是以后就可以繼續鉆研剩下的 category 的代碼了:

auto rw = cls->data();
rw->methods.attachLists(mlists, mcount);復制代碼

這是剛剛卡住的地方,現在來看,rw 是一個 class_rw_t 類型的結構體指針。根據 runtime 中的數據結構,它有一個 methods 結構體成員,并從父類繼承了 attachLists 方法,用來合并 category 中的方法:

void attachLists(List* const * addedLists, uint32_t addedCount) {if (addedCount == 0) return;uint32_t oldCount = array()->count;uint32_t newCount = oldCount + addedCount;setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));array()->count = newCount;memmove(array()->lists + addedCount, array()->lists, oldCount * sizeof(array()->lists[0]));memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0]));
}復制代碼

這段代碼很簡單,其實就是先調用 realloc() 函數將原來的空間拓展,然后把原來的數組復制到后面,最后再把新數組復制到前面。

在實際代碼中,比上面略復雜一些。因為為了提高性能,蘋果做了一些優化,比如當 List 處于第二種狀態(只有一個指針,指向一個元數據的集合)時,其實并不需要在原地擴容空間,而是只要重新申請一塊內存,并將最后一個位置留給原來的集合即可。

這樣只多花費了很少的內存空間,也就是原來二維數組占用的內存空間,但是 malloc() 的性能優勢會更加明顯,這其實是一個空間換時間的權衡問題。

需要注意的是,無論執行哪種邏輯,參數列表中的方法都會被添加到二維數組的前面。而我們簡單的看一下 runtime 在查找方法時的邏輯:

static method_t *getMethodNoSuper_nolock(Class cls, SEL sel){for (auto mlists = cls->data()->methods.beginLists(), end = cls->data()->methods.endLists(); mlists != end;++mlists) {method_t *m = search_method_list(*mlists, sel);if (m) return m;}return nil;
}static method_t *search_method_list(const method_list_t *mlist, SEL sel) {for (auto& meth : *mlist) {if (meth.name == sel) return &meth;}
}復制代碼

可見搜索的過程是按照從前向后的順序進行的,一旦找到了就會停止循環。因此 category 中定義的同名方法不會替換類中原有的方法,但是對原方法的調用實際上會調用 category 中的方法。

總結

讀完本文后,你應該對以下內容有比較深刻的理解,排名不分先后:

  1. 定義在 runtime.h 中的數據結構,如果有 OBJC2_UNAVAILABLE 標記則表示已經廢棄。
  2. Objective-C 2.0 中,類結構體的結構層次: objc_class -> class_data_bits_t -> class_rw_t -> method_array_t
  3. class_ro_t 結構體的作用,與 class_rw_t 的區別,以及和 ABI 穩定性的關系。
  4. category 解析過程的調用棧以及基本的流程。
  5. method_array_t 為什么要設計成一種類似于二維數組的數據結構,以及它的三種狀態之間的關系。

參考資料

  1. 深入理解Objective-C:Category
  2. 從源代碼看 ObjC 中消息的發送
  3. 深入解析 ObjC 中方法的結構
  4. Whats is methodLists attribute of the structure objc_class for?
  5. Objc與C(C++)之親緣關系(一) Class
  6. Objective-C Runtime

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/458180.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/458180.shtml
英文地址,請注明出處:http://en.pswp.cn/news/458180.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

獲取子元素

1、純css 獲取子元素 #test1>div {background-color:red;}#test1 div {font-size:14px;}#test1>div:first-child {color:#ccc;} <div id"test1"><div>性別</div><div>男</div></div> 因1示例中為#test1下的子元素 #test1…

JPG PNG GIF BMP圖片格式的區別

類型優點缺點應用場景相同圖片大小比較BMP無損壓縮&#xff0c;圖質最好文件太大&#xff0c;不利于網絡傳輸 152KGIF動畫存儲格式最多256色&#xff0c;畫質差 53KPNG可保存透明背景的圖片畫質中等 202KJPG文件小&#xff0c;利于網絡傳輸畫質損失車牌識別84K BMP BMP&…

EasyUI左右布居

<!DOCTYPE html><html xmlns"http://www.w3.org/1999/xhtml"><head runat"server"> <meta http-equiv"Content-Type" content"text/html; charsetutf-8" /> <title>首頁</title> <li…

44.Android之Shape設置虛線、圓角和漸變學習

Shape在Android中設定各種形狀&#xff0c;今天記錄下&#xff0c;由于比較簡單直接貼代碼。 Shape子屬性簡單說明一下:   gradient -- 對應顏色漸變。 startcolor、endcolor就不多說了。 android:angle是指從哪個角度開始變.solid -- 填充。stroke -- 描邊。corners -- 圓角…

幾種邊緣檢測算子的比較Roberts,Sobel,Prewitt,LOG,Canny

from&#xff1a;https://blog.csdn.net/gdut2015go/article/details/46779251 邊緣檢測是圖像處理和計算機視覺中的基本問題&#xff0c;邊緣檢測的目的是標識數字圖像中亮度變化明顯的點。圖像屬性中的顯著變化通常反映了屬性的重要事件和變化。這些包括&#xff1a;深度上的…

django 初試

/*************************************************************************************** django 初試* 說明&#xff1a;* 昨天打搭了dgango的服務器&#xff0c;今天學一下怎么來輸出一個hello world出來。* * …

淺析“高斯白噪聲”,“泊松噪聲”,“椒鹽噪聲”的區別

from&#xff1a;https://www.jianshu.com/p/67f909f3d0ce 在圖像處理的過程中&#xff0c;一般情況下都進行圖像增強&#xff0c;圖像增強主要包括“空域增強”和“頻域增強”&#xff0c; 空域增強包括平滑濾波和銳化濾波。 平滑濾波&#xff0c;就是將圖像模糊處理&#x…

HttpClient通過Post上傳文件(轉)

在之前一段的項目中&#xff0c;使用Java模仿Http Post方式發送參數以及文件&#xff0c;單純的傳遞參數或者文件可以使用URLConnection進行相應的處理。 但是項目中涉及到既要傳遞普通參數&#xff0c;也要傳遞多個文件&#xff08;不是單純的傳遞XML文件&#xff09;。在網上…

數字圖像處理:各種變換濾波和噪聲的類型和用途總結

摘自http://imgtec.eetrend.com/blog/4564 一、基本的灰度變換函數 1.1圖像反轉 適用場景&#xff1a;增強嵌入在一幅圖像的暗區域中的白色或灰色細節&#xff0c;特別是當黑色的面積在尺寸上占主導地位的時候。 1.2對數變換&#xff08;反對數變換與其相反&#xff09; …

Java 開發環境部署

1.下載Java開發環境工具包JDK&#xff0c;下載地址&#xff1a;http://www.oracle.com/technetwork/java/javase/downloads/index.html 下載后&#xff0c;雙擊jdk應用程序&#xff0c;根據提示完成安裝&#xff0c;安裝過程中可以自定義安裝目錄等信息&#xff0c;這里我選擇…

枚舉enum、NS_ENUM 、NS_OPTIONS

2019獨角獸企業重金招聘Python工程師標準>>> enum 了解位移枚舉之前&#xff0c;我們先回顧一下C語言位運算符。 1 << : 左移,比如1<<n,表示1往左移n位&#xff0c;即數值大小2的n次方; 例如 : 0b0001 << 1 變為了 0b0010 2 >> : 右…

mysql 關鍵詞相關度排序方法詳細示例分析

http://www.jb51.net/article/40480.htm轉載于:https://www.cnblogs.com/lixiuran/p/5299305.html

數字圖像處理-頻率域濾波原理

from&#xff1a;https://blog.csdn.net/forrest02/article/details/55510711?locationNum15&fps1 寫在前面的話 作者是一名在讀的碩士研究僧&#xff0c;方向是圖像處理。由于圖像處理是一門相對復雜的學科&#xff0c;作者在課堂上學到的東西只是非常淺顯的內容&#…

tomcat優化-有改protocol 和 緩存 集群方案

tomcat優化 在線上環境中我們是采用了tomcat作為Web服務器&#xff0c;它的處理性能直接關系到用戶體驗&#xff0c;在平時的工作和學習中&#xff0c;歸納出以下七種調優經驗。 1. 服務器資源 服務器所能提供CPU、內存、硬盤的性能對處理能力有決定性影響。 (1) 對于高并發…

深入淺出的講解傅里葉變換(真正的通俗易懂)

原文出處&#xff1a; 韓昊 1 2 3 4 5 6 7 8 9 10 作 者&#xff1a;韓 昊 知 乎&#xff1a;Heinrich 微 博&#xff1a;花生油工人 知乎專欄&#xff1a;與時間無關的故事 謹以此文獻給大連海事大學的吳楠老師&#xff0c;柳曉鳴老師&#xff0c;王新年老師以及張晶泊老…

分布式鎖 基于Redis

分布式鎖的實現(基于Redis)參考:http://www.jb51.net/article/75439.htm http://www.linuxidc.com/Linux/2015-01/111827.htm http://www.tuicool.com/articles/6juqmm7 方式一: 基于第三方類庫 redssion 1.安裝redis安裝redssion的鎖服務隊redis的版本有要求&#xff0c;要求必…

學好Linux決心書

我叫李楊&#xff0c;經過在老男孩教育linux運維班5個月學習后&#xff0c;我一定要達到的的薪水目標是7k,為了達到此目標我將采取如下5大行動或方案&#xff1a;1.每天堅持學習7個小時2.堅持每天跑步1小時3.調整思路 嚴于利己4.讓自己生活更加充實&#xff0c;不在渾渾噩噩5.為…

如何通俗易懂地解釋歐拉公式(e^πi+1=0)?

from&#xff1a;https://www.zhihu.com/question/41134540 http://www.matongxue.com/madocs/8.html

IIS(1)

轉載&#xff1a;http://blog.csdn.net/ce123 IIS音頻總線學習&#xff08;一&#xff09;數字音頻技術 一、聲音的基本概念 聲音是通過一定介質傳播的連續的波。 圖1 聲波重要指標&#xff1a; 振幅&#xff1a;音量的大小周期&#xff1a;重復出現的時間間隔頻率&#xff1a;…

手機屏幕適配原理及實現

為什么80%的碼農都做不了架構師&#xff1f;>>> 手機屏幕是用戶與 App 最直接的交互點 不同的分辨率下用戶對我們的 App 具有明顯的感觀差異&#xff0c;主流分辨率的更新迭代卻又完全獨立于 App 進行。這讓我們想要使 App 在絕大多數主流手機上都保持感觀、體驗的…