- 前言
- Runtime是什么
- Runtime的實現原理
- 消息傳遞機制
- Runtime基礎數據結構
- NSObject & id
- objc_object
- Class
- 元類(Meta Class)
- Category
- Ivar
- Method
- SEL與IMP的區別是什么? IMP如何尋址?
- Category與Extension的區別?
- Runtime的使用
前言
編譯型的語言都需要經過編譯之后再運行,OC編譯器部分由Clang+ LLVM組成,編譯的過程通常包括:預處理(Preprocessor)、編譯(Compiler)、匯編(Assembler)、鏈接(Linker)這幾個過程,然后才是加載運行。
預處理: 簡化代碼,過濾注釋,處理宏定義(
#
開頭關鍵字一般都和預處理有關);
編譯:將預處理之后的文件編譯成匯編語言文件;
匯編:將匯編文件轉成機器可識別指令;
鏈接:將文件鏈接成可以執行的程序。
實際上編譯的過程涉及很多繁瑣復雜的內容,感興趣的話可以看看編譯原理。
正常情況下,靜態語言,比如C語言,會在編譯階段確定數據類型,函數邏輯等等,從main()
開始自上而下執行,這個過程中你將無法再修改執行的函數,事實上,這也正是C語言被稱為面向過程的原因,你需要通過邏輯控制執行過程。
Objective-C是一門動態語言,它將從編譯到鏈接執行的內容推遲到了實際運行之前決定。Objective-C通過消息傳遞機制確定執行的類型和方法,通過其運行時機制,你可以將消息重定向給適當的對象,甚至還可以交換方法的實現。Objective-C具有動態性都要歸功于Runtime
Runtime是什么
Runtime是蘋果公司設計的支持Objective-C動態性的庫,Runtime
是主要由C語言編寫的,通過這個庫為Objective-C言語添加了面向對象的特性和動態機制。所有的OC程序都會鏈接到Runtime
庫,它提供了動態類型、動態加載、動態綁定等一系列基礎。
Runtime的實現
消息傳遞機制
Objective-C面向對象的實現是繼承于Smalltalk的,即將所有的東西都當作對象,通過向對象發送消息來執行程序。例如:
[self doSomthing: var1];
上面調用方法,會被編譯器轉化為C語言的:
objc_msgSend(self, @selector(doSomething:), var1);
即向self(消息的接收者),發送@selector(doSomething:), var1是傳遞的參數。
對象self收到消息之后,具體調用哪個方法則會在運行時決定。換句話說,Objective-C在編譯時并沒有真正的連接要調用的方法,而是通過發送消息給對象的方式,在運行時去連接調用的方法。這也就是實現Objective-C動態性的核心原理。
而對象接收消息后又是如何連接到方法的?回答這個問題之前,我們不得不先看一下Runtime中是如何定義類,對象的。
Runtime基礎數據結構
下面的定義都可以在Runtime源碼Runtime源碼中看到,源碼中可以找到Class
, NSObject
, Protocol
, Category
, SEL
等等概念的結構,下面我們一一分析。
NSObject & id
我們使用的大多數類都是繼承自NSObject,那我們可以先查看下NSObject.h
.
@protocol NSObject
- (BOOL)isEqual:(id)object;
@property (readonly) NSUInteger hash;
@property (readonly) Class superclass;
- (Class)class OBJC_SWIFT_UNAVAILABLE("use 'anObject.dynamicType' instead");
- (instancetype)self;
- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
...
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
+ (void)load;
+ (void)initialize;
- (instancetype)init
...
從以上代碼,可以看到NSObject類是遵循NSObject協議的,并且其中包含一個特殊的Class
類型的成員變量isa
, 這個isa
其實是一個指向objc_class
的指針,也就是指向自己類的指針。
如果繼續查看objc.h
, 我們還會發現特殊類型id
的定義:
/// A pointer to an instance of a class.
typedef struct objc_object *id;
所以id
就是一個指向類實例的指針,所以我們可以用id類型來指代不確定類型的實例。 下面繼續看objc_object
.
objc_object
這部分代碼是在objc-private.h
中:
struct objc_object {
private:
isa_t isa;
public:
// ISA() assumes this is NOT a tagged pointer object
Class ISA();
// getIsa() allows this to be a tagged pointer object
Class getIsa();
...
私有成員isa
是一個union
類型,C語言的union
類型表示幾個變量公用一個內存位置, 在不同的時間保存不同的數據類型和不同長度的變量。在arm64,Objective-C2.0下, isa
的作用不僅僅是指向一個類實例的指針,它包含了引用計數、析構狀態、關聯對象、weak引用等等更多地信息。
isa_t
源碼定義如下:
union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if SUPPORT_NONPOINTER_ISA
// extra_rc must be the MSB-most field (so it matches carry/overflow flags)
// indexed must be the LSB (fixme or get rid of it)
// shiftcls must occupy the same bits that a real class pointer would
// bits + RC_ONE is equivalent to extra_rc + 1
// RC_HALF is the high bit of extra_rc (i.e. half of its range)
// future expansion:
// uintptr_t fast_rr : 1; // no r/r overrides
// uintptr_t lock : 2; // lock for atomic property, @synch
// uintptr_t extraBytes : 1; // allocated with extra bytes
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
struct {
uintptr_t indexed : 1; //0 表示普通的 isa 指針,1 表示使用優化,存儲引用計數
uintptr_t has_assoc : 1; //表示該對象是否包含 associated object,如果沒有,則析構時會更快
uintptr_t has_cxx_dtor : 1; //表示該對象是否有 C++ 或 ARC 的析構函數,如果沒有,則析構時更快
uintptr_t shiftcls : 33; //類的指針
uintptr_t magic : 6; //固定值為 0xd2,用于在調試時分辨對象是否未完成初始化。
uintptr_t weakly_referenced : 1; //表示該對象是否有過 weak 對象,如果沒有,則析構時更快
uintptr_t deallocating : 1; //表示該對象是否正在析構
uintptr_t has_sidetable_rc : 1; //表示該對象的引用計數值是否過大無法存儲在 isa 指針
uintptr_t extra_rc : 19; //存儲引用計數值減一后的結果
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};
...
#if __OBJC2__
typedef struct method_t *Method;
typedef struct ivar_t *Ivar;
typedef struct category_t *Category;
typedef struct property_t *objc_property_t;
};
....
SUPPORT_NONPOINTER_ISA
宏定義表名這個isa
不再只是指向類的指針,而是經過優化,包含更多信息。
其中cls
是objc_class
結構體指針類型,bits
可以操作整個內存區,下面的結構體聲明位域,上面只復制了arm64環境下的代碼。
在Objective-C2.0時,對于Method, Category, Ivar, 屬性進行了新的定義,后面我們一一來看這些結構體。
另外,
Class
看源碼中的定義,Class
是objc_class *
,而objc_class
繼承自objc_object
。(以前objc_class
是單獨定義的, Objective-C2.0做了修改)。
typedef struct objc_class *Class;
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() {
return bits.data();
}
void setData(class_rw_t *newData) {
bits.setData(newData);
}
...
-
superclass
指向父類。 -
cache
處理已調用方法的緩存。 -
bits
存儲class_rw_t
地址,并且定義了一些基本操作。
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 firstSubclass;
Class nextSiblingClass;
...
這里定義了方法、屬性和協議列表,但是注意常量ro
,是一個class_ro_t
結構體指針,其定義如下:
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
const uint8_t * ivarLayout;
const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
method_list_t *baseMethods() const {
return baseMethodList;
}
};
其中也定義了帶著"base"的方法、屬性和協議列表,它們之間是什么關系呢?
我也十分困惑,但在這里找到了一些答案。編譯class時, objc_class
的bits
是指向ro
的,然后創建rw
時將bits
里的ro
賦值給rw
,作為它的一個常量,最后bits
用rw
替換原先存儲的ro
。
當然,類初始化后,這些屬性、方法和協議列表都是空的,運行時會動態添加修改。
元類(Meta Class)
為了引出元類的概念,我們不得不重點提一下objc_class
的isa
,這是從objc_object
繼承下來的。一個對象實例的方法是通過isa
結構體保存的,那么一個類的類方法保存在哪?這就是objc_class
的isa
存在的意義。
一個objc_class
的isa
所指向的類實例,我們稱之為元類(Meta Class)。看下面objc_class
中判斷元類和獲取元類的幾個方法:
bool isMetaClass() {
assert(this);
assert(isRealized());
return data()->ro->flags & RO_META;
}
// NOT identical to this->ISA when this is a metaclass
Class getMeta() {
if (isMetaClass()) return (Class)this;
else return this->ISA();
}
bool isRootClass() {
return superclass == nil;
}
bool isRootMetaclass() {
return ISA() == (Class)this;
}
isMetaClass
的語句data()->ro->flags & RO_META
, 表面這個flag字段是標識一個class
是否是元類的。getMeta()
是獲取一個類的元類的方法,一個類如果是元類,它的元類會指向它本身,如果是一個普通類,則它的元類是指向同類型的類。
我們也引入一個經典的圖片來做解釋:
一個類的元類的父類,是其父類的元類,通常NSObject
類是Root class
(根類), 其superclass
指向nil, 其元類也是NSObject
類,而Root Meta class(根元類)的父類則指向NSObject
(根類),這樣形成一個閉環。
Category
Category就是我們常用的類別,查看源碼定義,我們可以通過類別向已存在的類添加實例方法,實例屬性,類方法,協議。
當Application啟動時,加載類時會調用attachCategories(Class cls, category_list *cats, bool flush_caches)
方法按類別加載順序向類追加所有Category
的相關內容。
/// An opaque type that represents a category.
typedef struct objc_category *Category;
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; // 添加的實例屬性
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta) {
if (isMeta) return nil; // classProperties;
else return instanceProperties;
}
};
關于Category方法調用覆蓋問題,需要知道的是Category的方法是在運行時追加到類的方法列表頂部,IMP查找順序:Category->Class->Super Class, 當在多個Category中定義同名方法時,當查找到第一個IMP時,就會立即返回,而不會繼續查找,也就是會“覆蓋”后面的方法。
Ivar
Ivar 代表類的實例對象。
typedef struct objc_ivar *Ivar;
Method
方法的定義,引出兩個重要的概念,方法名標志類型SEL
和入口指針類型IMP
。
typedef struct method_t *Method;
struct method_t {
SEL name;
const char *types;
IMP imp;
struct SortBySELAddress :
public std::binary_function<const method_t&,
const method_t&, bool>
{
bool operator() (const method_t& lhs,
const method_t& rhs)
{ return lhs.name < rhs.name; }
};
};
SEL
typedef struct objc_selector *SEL;
SEL本質是映射到方法的C字符串,也就是SEL字符串包含了方法名、參數等,它是方法選擇器(selector)在Objc中的表示類型。
IMP
typedef id (*IMP)(id, SEL, ...);
IMP是個函數指針,它指向的函數體就是對象接收消息后最終執行的代碼。
SEL與IMP的區別是什么?IMP如何尋址?
SEL是方法選擇器,它其實保存的是方法編號,而IMP是指向函數地址的指針,方法編號與方法地址一一對應存儲在類的isa
的dispatch table中。
IMP class_getMethodImplementation(Class cls, SEL sel)
{
IMP imp;
if (!cls || !sel) return nil;
imp = lookUpImpOrNil(cls, sel, nil,
YES/*initialize*/, YES/*cache*/, YES/*resolver*/);
// Translate forwarding function to C-callable external version
if (!imp) {
return _objc_msgForward;
}
return imp;
}
上面是獲取方法實現的函數,根據類和SEL來查找IMP,如果cls
或sel
傳入的是nil
, 直接返回nil, 否則通過IMP lookUpImpOrNil(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver)
。繼續查找:
IMP lookUpImpOrNil(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
if (imp == _objc_msgForward_impcache) return nil;
else return imp;
}
這個方法主要還是調用lookUpImpOrForward
方法,只不過用nil替換_objc_msgForward_impcache
。
終于找到核心實現的代碼,我們看看lookUpImpOrForward
方法的實現, 我將部分解釋放在了注釋里。
/***********************************************************************
* lookUpImpOrForward.
* The standard IMP lookup.
* initialize==NO tries to avoid +initialize (but sometimes fails)
* cache==NO skips optimistic unlocked lookup (but uses cache elsewhere)
* Most callers should use initialize==YES and cache==YES.
* inst is an instance of cls or a subclass thereof, or nil if none is known.
* If cls is an un-initialized metaclass then a non-nil inst is faster.
* May return _objc_msgForward_impcache. IMPs destined for external use
* must be converted to _objc_msgForward or _objc_msgForward_stret.
* If you don't want forwarding at all, use lookUpImpOrNil() instead.
**********************************************************************/
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
Class curClass;
IMP imp = nil;
Method meth;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
// Optimistic cache lookup 如果使用了 Optimistic cache,會先從這個cache中查找
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
if (!cls->isRealized()) {
rwlock_writer_t lock(runtimeLock);
realizeClass(cls);
}
if (initialize && !cls->isInitialized()) {
_class_initialize (_class_getNonMetaClass(cls, inst));
// If sel == initialize, _class_initialize will send +initialize and
// then the messenger will send +initialize again after this
// procedure finishes. Of course, if this is not being called
// from the messenger then it won't happen. 2778172
}
// The lock is held to make method-lookup + cache-fill atomic
// with respect to method addition. Otherwise, a category could
// be added but ignored indefinitely because the cache was re-filled
// with the old value after the cache flush on behalf of the category.
retry:
runtimeLock.read();
// Ignore GC selectors如果是忽略方法,goto執行done部分
if (ignoreSelector(sel)) {
imp = _objc_ignored_method;
cache_fill(cls, sel, imp, inst);
goto done;
}
// Try this class's cache. 從本類的緩存中查找(前面提過,一個類的在程序執行過的方法,會被添加到其cache_t中,這樣再次調用時這個方法時會加快速度。)
imp = cache_getImp(cls, sel);
if (imp) goto done;
// Try this class's method lists. 從本類的方法列表查找
meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
// Try superclass caches and method lists.從父類的緩存中查找
curClass = cls;
while ((curClass = curClass->superclass)) {
// Superclass cache.
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
// Found the method in a superclass. Cache it in this class.
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
}
// Superclass method list.從父類的方法列表查找
meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
// No implementation found. Try method resolver once. 從運行時添加的方法中去查找
if (resolver && !triedResolver) {
runtimeLock.unlockRead();
_class_resolveMethod(cls, sel, inst);
// Don't cache the result; we don't hold the lock so it may have
// changed already. Re-do the search from scratch instead.
triedResolver = YES;
goto retry;
}
// No implementation found, and method resolver didn't help.
// Use forwarding.
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlockRead();
// paranoia: look for ignored selectors with non-ignored implementations
assert(!(ignoreSelector(sel) && imp != (IMP)&_objc_ignored_method));
// paranoia: never let uncached leak out
assert(imp != _objc_msgSend_uncached_impcache);
return imp;
}
總結一下查找流程:
- Optimistic cache 查找
- 忽略的方法中查找
- 當前類的緩存中查找
- 當前類的方法列表中查找
- 父類的緩存中查找
- 父類的方法列表中查找
- 動態添加的方法找查找
Category 和 Extension 在Objective-C中的區別
1,Category是運行時加載的,Category中的方法是動態添加到類的方法列表的。Extension是類的擴展,擴展中的方法會在編譯時添加的。
2,類的成員變量在編譯后不可變的,而方法列表是可變的,所以我們可以在Extension中定義成員變量,但是在Category中不可以。為Category添加屬性時,我們也只能通過運行時實現屬性的Setter和getter方法。(通過objc_setAssociatedObject
和objc_getAssociatedObject
這兩個方法)。
3, 我們可以為系統類型添加Category,但是不能為其添加Extension,因為Extension只能局限于類的實現中。
Runtime的使用
Objc 從三種不同的層級上與 Runtime 系統進行交互,分別是通過 Objective-C 源代碼,通過 Foundation 框架的NSObject類定義的方法,通過對 runtime 函數的直接調用。
展開理解一下這三個層級:
Objective-C 源代碼:OC源碼底層都是需要和Runtime交互的,我們已經知道OC的類、方法、協議等等都是在Runtime中通過數據結構定義的,可以說我們所有的代碼都是在Runtime上編寫的。
NSObject類 : 我們在調用isKindOfClass:
,respondsToSelector
,conformsToProtocol
等等類型檢查、方法檢查時,都是在運行時進行的,這是從NSObject類的層面上,諸多方法是在使用Runtime。
Runtime的函數: #import <objc/runtime.h>
之后,我們可以直接調用Runtime開放的接口,為我們的類動態添加方法,交互方法實現等等,比如MJRefresh
,就是使用Runtime的方法為scrollview添加刷新header
和footer
。
runtime.h
中,runtime通過OBJC_EXPORT
宏將部分方法暴露出來給開發者直接使用,我們也可以查看蘋果官方文檔查看如何使用這部分API。
參考文章:
Objective-C Runtime 這篇介紹的很系統、很詳細
iOS開發教程之Objc Runtime筆記 這篇也很不錯,剖析源碼,很深入。
Objective-C Runtime 蘋果文檔
Runtime源碼
元類Mate-Class
Objective-C引用計數原理
https://github.com/DeveloperErenLiu/RuntimePDF