RunLoop
- runloop是事件的接受的分發機制的實現
- runloop提供一種非同步執行程式碼的機制,不能並行執行任務
- 在主佇列中,main Runloop直接配合任務的執行,負責處理UI事件, 定時器以及其他核心相關的事件
Runloop 的主要目的
- 保證程式執行時不會被系統終止
- 什麼時候使用Runloop
- 當需要和該執行緒進行互動的時候才會使用Runloop
- 滅一個執行緒都有對應的的Runloop,但是預設的非主執行緒的Runloop是不執行,需要為Runloop新增至少一個事件源,然後去run。
- 一般情況下我們不去做這些,除非你要長久去檢測單獨執行緒中的某個事件
- Runloop,是執行緒進入好被執行緒用來響應事件以及呼叫事件處理函式的地方,需要在程式碼中使用控制語句實現Runloop的迴圈,需要程式碼提供while或者for來驅動Runloop在這個迴圈中,使用一個Runloop物件[NSRunLoop CurrentRunLoop]執行接收訊息,呼叫對應的處理函式
Runloop接受兩種資料來源事件:Input 和timer Source
Input Source傳遞事件,通常是來自其他執行緒和不同執行緒和不同程式中的訊息
Timer Source (定時器)傳遞同步事件(重複執行或者在特定時間上觸發)
處理 input Source Runloop 也會產生一些關於本身的notification,註冊Runloop的observe,可以接收這些notification,做一些額外的處理。(使用CoreFundation來成為Runloop的observe)
Runloop工作特點
當有時間發生時,Runloop會根據具體的事件型別通知相應程式作出相應
當沒有事件發生時,Runloop會進入休眠狀態,從而達到省電的目的
當事件再次發生時,Runloop會被喚醒,處理事件
Runloop組成
-
與執行緒和自動釋放遲有關
-
CFRunLoopRef構造:資料結構;建立與退出;mode切換和item依賴;Runloop啟動
- CFRunLoopModeRef:資料結構(與CFRunLoopRef放一起了);建立;型別;
- modeItems:
- CFRunLoopSourceRef:資料結構(source0/source1);
- source0 :
- source1 :
- CFRunLoopSourceRef:資料結構(source0/source1);
- CFRunLoopTimerRef:資料結構;建立與生效;相關型別(GCD的timer與CADisplayLink)
- CFRunLoopObserverRef:資料結構;建立與新增;監聽的狀態;
-
Runloop內部邏輯:關鍵在兩個判斷點(是否睡覺,是否退出)
- 程式碼實現:
- 函式作用棧顯示:
-
Runloop本質:mach port和mach_msg()。
-
如何處理事件:
- 介面重新整理:
- 手勢識別:
- GCD任務:
- timer:(與CADisplayLink)
- 網路請求:
-
應用:
- 滑動與圖片重新整理;
- 常駐子執行緒,保持子執行緒一直處理事件
執行緒(建立)-->runloop將進入-->最高優先順序OB建立釋放池-->runloop將睡-->最低優先順序OB銷燬舊池建立新池-->runloop將退出-->最低優先順序OB銷燬新池-->執行緒(銷燬)
RunLoop 的構造
CFRunLoopRef構造:
// runloop資料結構
struct __CFRunLoopMode {
CFStringRef _name; // Mode名字,
CFMutableSetRef _sources0; // Set<CFRunLoopSourceRef>
CFMutableSetRef _sources1; // Set<CFRunLoopSourceRef>
CFMutableArrayRef _observers; // Array<CFRunLoopObserverRef>
CFMutableArrayRef _timers; // Array<CFRunLoopTimerRef>
...
};
// mode資料結構
struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set<CFStringRef>
CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set<CFRunLoopModeRef>
...
};
複製程式碼
建立與退出: model切換和item的依賴
-
主執行緒的runloop自動建立,子執行緒的runloop預設不建立(在子執行緒中呼叫NSRunLoop *runloop = [NSRunLoop currentRunLoop]; 獲取RunLoop物件的時候,就會建立RunLoop);
-
runloop退出的條件:app退出;執行緒關閉;設定最大時間到期;modeItem為空;
-
同一時間一個runloop只能在一個mode,切換mode只能退出runloop,再重進指定mode(隔離modeItems使之互不干擾);
-
一個item可以加到不同mode;一個mode被標記到commonModes裡(這樣runloop不用切換mode)。
RunLoop 啟動
-
用DefaultMode啟動
void CFRunLoopRun(void) { CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false); } 複製程式碼
-
用指定的Mode啟動,允許設定RunLoop最大時間(假無限迴圈),執行完畢是否退出
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) { return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled); } 複製程式碼
-
RunLoop 自動建立對應的model,model 只能新增不能刪除
新增model CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName); 複製程式碼
型別
- kCFRunLoopDefaultMode: 預設 mode,通常主執行緒在這個 Mode 下執行。
- UITrackingRunLoopMode: 追蹤mode,保證Scrollview滑動順暢不受其他 mode 影響。
- UIInitializationRunLoopMode: 啟動程式後的過渡mode,啟動完成後就不再使用。
- GSEventReceiveRunLoopMode: Graphic相關事件的mode,通常用不到。
- kCFRunLoopCommonModes: 佔位mode,作為標記DefaultMode和CommonMode用。
modelItems
// 新增移除item的函式(引數:新增/移除哪個item到哪個runloop的哪個mode下) CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName); CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName); CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode); CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName); CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName); CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode); 複製程式碼
CFRunLoopSourceRef:事件來源
按照官方文件CFRunLoopSourceRef為3類,但資料結構只有兩類(???) Port-Based Sources:與核心埠相關 Custom Input Sources:與自定義source相關 Cocoa Perform Selector Sources:與PerformSEL方法相關) 複製程式碼
CFRunLoopObserverRef:監聽runloop狀態,接收回撥資訊(常見於自動釋放池建立銷燬)
// 第一個引數用於分配該observer物件的記憶體空間
// 第二個引數用以設定該observer監聽什麼狀態
// 第三個引數用於標識該observer是在第一次進入run loop時執行還是每次進入run loop處理時均執行
// 第四個引數用於設定該observer的優先順序,一般為0
// 第五個引數用於設定該observer的回撥函式
// 第六個引數observer的執行狀態
CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
// 執行程式碼
}
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即將進入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7), // 即將退出Loop
};
複製程式碼
Runloop內部邏輯:關鍵在兩個判斷點(是否睡覺,是否退出)
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
// 0.1 根據modeName找到對應mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
// 0.2 如果mode裡沒有source/timer/observer, 直接返回。
if (__CFRunLoopModeIsEmpty(currentMode)) return;
// 1.1 通知 Observers: RunLoop 即將進入 loop。---(OB會建立釋放池)
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
// 1.2 內部函式,進入loop
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
Boolean sourceHandledThisLoop = NO;
int retVal = 0;
do {
// 2.1 通知 Observers: RunLoop 即將觸發 Timer 回撥。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
// 2.2 通知 Observers: RunLoop 即將觸發 Source0 (非port) 回撥。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
// 執行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
// 2.3 RunLoop 觸發 Source0 (非port) 回撥。
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
// 執行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
// 2.4 如果有 Source1 (基於port) 處於 ready 狀態,直接處理這個 Source1 然後跳轉去處理訊息。
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
if (hasMsg) goto handle_msg;
}
// 3.1 如果沒有待處理訊息,通知 Observers: RunLoop 的執行緒即將進入休眠(sleep)。--- (OB會銷燬釋放池並建立新釋放池)
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}
// 3.2. 呼叫 mach_msg 等待接受 mach_port 的訊息。執行緒將進入休眠, 直到被下面某一個事件喚醒。
// - 一個基於 port 的Source1 的事件。
// - 一個 Timer 到時間了
// - RunLoop 啟動時設定的最大超時時間到了
// - 被手動喚醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
}
// 3.3. 被喚醒,通知 Observers: RunLoop 的執行緒剛剛被喚醒了。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
// 4.0 處理訊息。
handle_msg:
// 4.1 如果訊息是Timer型別,觸發這個Timer的回撥。
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
}
// 4.2 如果訊息是dispatch到main_queue的block,執行block。
else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}
// 4.3 如果訊息是Source1型別,處理這個事件
else {
CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
if (sourceHandledThisLoop) {
mach_msg(reply, MACH_SEND_MSG, reply);
}
}
// 執行加入到Loop的block
__CFRunLoopDoBlocks(runloop, currentMode);
// 5.1 如果處理事件完畢,啟動Runloop時設定引數為一次性執行,設定while引數退出Runloop
if (sourceHandledThisLoop && stopAfterHandle) {
retVal = kCFRunLoopRunHandledSource;
// 5.2 如果啟動Runloop時設定的最大運轉時間到期,設定while引數退出Runloop
} else if (timeout) {
retVal = kCFRunLoopRunTimedOut;
// 5.3 如果啟動Runloop被外部呼叫強制停止,設定while引數退出Runloop
} else if (__CFRunLoopIsStopped(runloop)) {
retVal = kCFRunLoopRunStopped;
// 5.4 如果啟動Runloop的modeItems為空,設定while引數退出Runloop
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
retVal = kCFRunLoopRunFinished;
}
// 5.5 如果沒超時,mode裡沒空,loop也沒被停止,那繼續loop,回到第2步迴圈。
} while (retVal == 0);
}
// 6. 如果第6步判斷後loop退出,通知 Observers: RunLoop 退出。--- (OB會銷燬新釋放池)
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}
複製程式碼
一步一步寫具體的實現邏輯過於繁瑣不便理解,按Runloop狀態大致分為:
- Entry:通知OB(建立pool);
- 執行階段:按順序通知OB並執行timer,source0;若有source1執行source1;
- 休眠階段:利用mach_msg判斷進入休眠,通知OB(pool的銷燬重建);被訊息喚醒通知OB;
- 執行階段:按訊息型別處理事件;
- 判斷退出條件:如果符合退出條件(一次性執行,超時,強制停止,modeItem為空)則退出,否則回到第2階段;
- Exit:通知OB(銷燬pool)
Runloop本質:mach port和mach_msg()。
Mach是XNU的核心,程式、執行緒和虛擬記憶體等物件通過埠發訊息進行通訊,Runloop通過mach_msg()函式傳送訊息,如果沒有port 訊息,核心會將執行緒置於等待狀態 mach_msg_trap() 。如果有訊息,判斷訊息型別處理事件,並通過modeItem的callback回撥。
Runloop有兩個關鍵判斷點,一個是通過msg決定Runloop是否等待,一個是通過判斷退出條件來決定Runloop是否迴圈。
複製程式碼
如何處理事件
1.介面處理
- 當UI改變( Frame變化、 UIView/CALayer 的繼承結構變化等)時,或手動呼叫了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法後,這個 UIView/CALayer 就被標記為待處理。
- 蘋果註冊了一個用來監聽BeforeWaiting和Exit的Observer,在它的回撥函式裡會遍歷所有待處理的 UIView/CAlayer 以執行實際的繪製和調整,並更新 UI 介面。
- 事件響應
- 當一個硬體事件(觸控/鎖屏/搖晃/加速等)發生後,首先由 IOKit.framework 生成一個 IOHIDEvent 事件並由 SpringBoard 接收, 隨後由mach port 轉發給需要的App程式。
- 蘋果註冊了一個 Source1 (基於 mach port 的) 來接收系統事件,通過回撥函式觸發Sourece0(所以UIEvent實際上是基於Source0的),呼叫 _UIApplicationHandleEventQueue() 進行應用內部的分發。
- _UIApplicationHandleEventQueue() 會把 IOHIDEvent 處理幷包裝成 UIEvent 進行處理或分發,其中包括識別 UIGesture/處理螢幕旋轉/傳送給 UIWindow 等。
- 手勢識別
- 如果上一步的 _UIApplicationHandleEventQueue() 識別到是一個guesture手勢,會呼叫Cancel方法將當前的touchesBegin/Move/End 系列回撥打斷。隨後系統將對應的 UIGestureRecognizer 標記為待處理。
- 蘋果註冊了一個 Observer 監測 BeforeWaiting (Loop即將進入休眠) 事件,其回撥函式為_UIGestureRecognizerUpdateObserver(),其內部會獲取所有剛被標記為待處理的 GestureRecognizer,並執行GestureRecognizer的回撥。
- 當有 UIGestureRecognizer 的變化(建立/銷燬/狀態改變)時,這個回撥都會進行相應處理。
4.GCD任務
- 當呼叫 dispatch_async(dispatch_get_main_queue(), block) 時,libDispatch 會向主執行緒的 RunLoop 傳送訊息,RunLoop會被喚醒,並從訊息中取得這個 block,並在回撥裡執行這個 block。Runloop只處理主執行緒的block,dispatch 到其他執行緒仍然是由 libDispatch 處理的。
RunTime
Runtime就是系統在執行的時候的一種機制,其中最重要的就是訊息機制,對於C語言,函式的呼叫在編譯的時候就會決定呼叫那個函式C語言的函式呼叫請看這裡
)。編譯完成之後直接順序執行,無任何二義性。OC的函式呼叫成為訊息傳送。屬於動態呼叫過程。在編譯的時候並不能決定真正呼叫哪個函式(事實證明,在編
譯階段,OC可以呼叫任何函式,即使這個函式並未實現,只要申明過就不會報錯。而C語言在編譯階段就會報錯)。只有在真正執行的時候才會根據函式的名稱找
到對應的函式來呼叫。
複製程式碼
- 封裝:在這個庫中,物件可以用C語言中的結構體表示,而方法可以用C函式來實現,另外再加上了一些額外的特性。這些結構體和函式被runtime函式封裝後,我們就可以在程式執行時建立,檢查,修改類、物件和它們的方法了。
Runtime主要實現思路
例項物件instance->類class->方法method(->SEL->IMP)->實現函式
例項物件只存放ISA指標和例項變數,由ISA指標找到所屬類,類維護一個執行時可接收的方法列表;方法列表中的每個入口是一個方法(Method),其中key是一個特定名稱,即選擇器(SEL),其對應一個指向底層C實現函式的指標,即實現(IMP),。執行時機制最關鍵核心是objc_msgSend函式,通過給target(類)傳送selecter(SEL)來傳遞訊息,找到匹配的IMP,指向實現的C函式。
由於OC的執行時動態特性,在編譯之後可以在執行時通過C操作函式,動態地建立修改類資訊,動態繫結方法和重寫實現,靈活地實現一些自定義功能。
在oc中訊息機制如何呼叫
複製程式碼
RunTime 介紹
- 類的本質
- 類相關:
+ 資料型別:class,object;
- isa 元類
- superClass 根類
+ 操作函式:
- class_:
+ get: 類名,父類; 例項變數,成員變數;屬性;例項方法,類方法,方法實現;
+ copy: 成員變數列表;屬性列表;方法列表;協議列表;
+ add: 成員變數;屬性;方法;協議;
+ replace:屬性;方法;
+ respond:響應方法判斷(內省)
+ isMetaclass:元類判斷(內省)
+ conform:遵循協議判斷(內省)
- objc_:
+ get: 例項變數;成員變數;類名;類;元類;關聯物件;
+ copy: 物件;類;類列表;協議列表;
+ set: 例項變數;成員變數;類;類列表;協議;關聯物件;
+ dispose: 物件;
- 動態建立/銷燬類、物件
- 成員變數、屬性相關:
+ 資料型別:Ivar;objc_property_t;objc_property_attribute_t;
+ 操作函式:
- ivar_:
- property_:
- 方法訊息相關:
+ 資料型別:SEL;IMP; Method;方法快取
+ 操作函式:
- method_:
+ invoke: 方法實現的返回值;
+ get: 方法名;方法實現;引數與返回值相關;
+ set:方法實現;
+ exchange:交換方法實現
+ 方法呼叫:msgSend函式(找到方法實現)
+ 訊息轉發:
- Method Resolution
- Fast Forwarding
- Normal Forwarding
- 協議相關:
+ 資料型別:Protocol;
+ 操作函式:
- protocol_:
+ get: 協議;屬性;
+ copy:協議列表;屬性列表;
+ add:屬性;方法;協議;
+ isEqual:判斷兩協議等同;
+ comform:判斷是否遵循協議;
- 其他:類名;版本號;類資訊;(忽略)
複製程式碼
動態實現方法交換
Method Swizzling
具體方法:方法交還,也可做方法攔截
//靜態就交換靜態,例項方法就交換例項方法
void Swizzle(Class c, SEL origSEL, SEL newSEL) {
Method origMethod = class_getInstanceMethod(c, origSEL);
Method newMethod = nil;
if (!origMethod) {
origMethod = class_getClassMethod(c, origSEL);
if (!origMethod) {
return;
}
newMethod = class_getClassMethod(c, newSEL);
if (!newMethod) {
return;
}
} else {
newMethod = class_getInstanceMethod(c, newSEL);
if (!newMethod) {
return;
}
}
//自身已經有了就新增不成功,直接交換即可
if(class_addMethod(c, origSEL, method_getImplementation(newMethod), method_getTypeEncoding(newMethod))) {
class_replaceMethod(c, newSEL, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
} else {
method_exchangeImplementations(origMethod, newMethod);
}
}
複製程式碼
runtime對類分析
@interface NSObject<nsobject>{
Class isa OBJC_ISA_AVAILABILITY;
}</nsobject>
在NSObject中存在一個Class 指標, 然後我們看Class
typedef struct objc_Class *Class;
struct objc_class{
Class isa;//指向metaclass
Class super_class;//指向父類
Const char *name;// 類名
Long version;//版本資訊,初始化預設是0,可以根據runtime函式class_setVersion和class_getVersion進行修改、讀取
Long info一些標識資訊,如CLS_CLASS (0x1L)
表示該類為普通 class ,其中包含物件方法和成員變數;CLS_META (0x2L)
表示該類為 metaclass,其中包含類方法;
long instance_size ; // 該類的例項變數大小(包括從父類繼承下來的例項變數);
struct objc_ivar_list *ivars; // 用於儲存每個成員變數的地址
struct objc_method_list **methodLists ; // 與 info 的一些標誌位有關,如CLS_CLASS (0x1L),則儲存物件方法,如CLS_META (0x2L),則儲存類方法;
struct objc_cache *cache; // 指向最近使用的方法的指標,用於提升效率;
struct objc_protocol_list *protocols; // 儲存該類遵守的協議
}
Class
isa:指向metaclass,也就是靜態的Class。一般一個Obj物件中的isa會指向普通的Class,這個Class中儲存普通成員變數和對
象方法(“-”開頭的方法),普通Class中的isa指標指向靜態Class,靜態Class中儲存static型別成員變數和類方法(“+”開頭的方
法)。
複製程式碼
- 資料型別:
|isa和super_class :不同的類中可以有相同的方法(同一個類的方法不能同名,哪怕引數型別不同,後面解釋...),所以要先確定是那個類。isa和super_class是找到實現函式的關鍵對映,決定找到存放在哪個類的方法實現。(isa用於自省確定所屬類,super_class確定繼承關係)。
例項物件的isa指標指向類,類的isa指標指向其元類(metaClass)。物件就是一個含isa指標的結構體。類儲存例項物件的方法列表,元類儲存類的方法列表,元類也是類物件。
複製程式碼
- 操作函式:類物件以class_為字首,例項物件以object_為字首
- class
get: 類名,父類,元類;例項變數,成員變數;屬性;例項方法,類方法,方法實現;
// 獲取類的類名
const char * class_getName ( Class cls );
// 獲取類的父類
Class class_getSuperclass ( Class cls );
// 獲取例項大小
size_t class_getInstanceSize ( Class cls );
// 獲取類中指定名稱例項成員變數的資訊
Ivar class_getInstanceVariable ( Class cls, const char *name );
// 獲取類成員變數的資訊
Ivar class_getClassVariable ( Class cls, const char *name );
// 獲取指定的屬性
objc_property_t class_getProperty ( Class cls, const char *name );
// 獲取例項方法
Method class_getInstanceMethod ( Class cls, SEL name );
// 獲取類方法
Method class_getClassMethod ( Class cls, SEL name );
// 獲取方法的具體實現
IMP class_getMethodImplementation ( Class cls, SEL name );
IMP class_getMethodImplementation_stret ( Class cls, SEL name );
複製程式碼
- copy 成員變數列表;屬性列表;方法列表;協議列表;
Ivar * class_copyIvarList ( Class cls, unsigned int *outCount );
// 獲取屬性列表
objc_property_t * class_copyPropertyList ( Class cls, unsigned int *outCount );
// 獲取所有方法的列表
Method * class_copyMethodList ( Class cls, unsigned int *outCount );
// 獲取類實現的協議列表
Protocol * class_copyProtocolList ( Class cls, unsigned int *outCount )
複製程式碼
- add: 成員變數;屬性;方法;協議;(新增成員變數只能在執行時建立的類,且不能為元類)
// 新增成員變數
BOOL class_addIvar ( Class cls, const char *name, size_t size, uint8_t alignment, const char *types );
// 新增屬性
BOOL class_addProperty ( Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount );
// 新增方法
BOOL class_addMethod ( Class cls, SEL name, IMP imp, const char *types );
// 新增協議
BOOL class_addProtocol ( Class cls, Protocol *protocol );
複製程式碼
- replace: 屬性和方法
// 替換類的屬性
void class_replaceProperty ( Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount );
// 替代方法的實現
IMP class_replaceMethod ( Class cls, SEL name, IMP imp, const char *types );
複製程式碼
- reponse:響應方法判斷
// 類例項是否響應指定的selector
BOOL class_respondsToSelector ( Class cls, SEL sel );
複製程式碼
- isMetaCLass: 元類判斷
// 判斷給定的Class是否是一個元類
BOOL class_isMetaClass ( Class cls );
複製程式碼
- conform: 遵循協議判斷
// 返回類是否實現指定的協議
BOOL class_conformsToProtocol ( Class cls, Protocol *protocol );
複製程式碼
- objc_ //物件
get: 例項變數;成員變數;類名;類;元類;關聯物件
// 獲取物件例項變數
Ivar object_getInstanceVariable ( id obj, const char *name, void **outValue );
// 獲取物件中例項變數的值
id object_getIvar ( id obj, Ivar ivar );
// 獲取物件的類名
const char * object_getClassName ( id obj );
// 獲取物件的類
Class object_getClass ( id obj );
Class objc_getClass ( const char *name );
// 返回指定類的元類
Class objc_getMetaClass ( const char *name );
//獲取關聯物件
id objc_getAssociatedObject(self, &myKey);
複製程式碼
- copy : 物件、類、類列表、協議列表
// 獲取指定物件的一份拷貝
id object_copy ( id obj, size_t size );
// 建立並返回一個指向所有已註冊類的指標列表
Class * objc_copyClassList ( unsigned int *outCount );
複製程式碼
- set: 例項變數、類、類列表、協議、關聯物件
// 設定類例項的例項變數的值
Ivar object_setInstanceVariable ( id obj, const char *name, void *value );
// 設定物件中例項變數的值
void object_setIvar ( id obj, Ivar ivar, id value );
//設定關聯物件.其中myKey自己在用的時候需要在設定一個,在執行時的時候它是通過這個未標記的,裡面有一套完善的機制,就是你在實現kvo的時候也是可以的
void objc_setAssociatedObject(self, &myKey, anObject, OBJC_ASSOCIATION_RETAIN);
複製程式碼
- dispose: 物件
// 釋放指定物件佔用的記憶體
id object_dispose ( id obj );
複製程式碼
動態建立/銷燬類、物件
- 動態建立/銷燬類:
// 建立一個新類和元類
Class objc_allocateClassPair ( Class superclass, const char *name, size_t extraBytes );
// 銷燬一個類及其相關聯的類
void objc_disposeClassPair ( Class cls );
// 在應用中註冊由objc_allocateClassPair建立的類
void objc_registerClassPair ( Class cls );
複製程式碼
- 動態建立/銷燬物件:
// 建立類例項
id class_createInstance ( Class cls, size_t extraBytes );
// 在指定位置建立類例項
id objc_constructInstance ( Class cls, void *bytes );
// 銷燬類例項
void * objc_destructInstance ( id obj );
Class super_class:指向父類,如果這個類是根類,則為NULL。
下面一張圖片很好的描述了類和物件的繼承關係:
複製程式碼
注意:所有metaclass中isa指標都指向跟metaclass。而跟metaclass則指向自身。
Root metaclass是通過繼承Root class產生的。與root class結構體成員一致,也就是前面提到的結構。不同的是Root
metaclass的isa指標指向自身。
上圖的解釋
1、isa:例項物件->類->元類->(不經過父元類)直接到根元類(NSObject的元類),根元類的isa指向自己;
2、 superclass:類->父類->...->根類NSObject,元類->父元類->...->根元類->根類,NSObject的superclass指向nil。
@selector (makeText):
這是一個SEL方法選擇器。SEL其主要作用是快速的通過方法名字(makeText)查詢到對應方法的函式指標,然後呼叫其函式。SEL其本身是一個
Int型別的一個地址,地址中存放著方法的名字。對於一個類中。每一個方法對應著一個SEL。所以iOS類中不能存在2個名稱相同的方法,即使引數型別不
同,因為SEL是根據方法名字生成的,相同的方法名稱只能對應一個SEL。
這就是為什麼 oc中不支援方法過載,因為函式指標只是根據函式名來生成的,在呼叫的時候,在去通過對映去找,
objc_msgSend(self, @selector(makeText));
void makeText (id self,SEL sel)
{
NSLog(@"%@ %@",self,NSStringFromSelector(sel));
}
// 第一個引數:給哪個類新增方法
// 第二個引數:新增方法的方法編號
// 第三個引數:新增方法的函式實現(函式地址)
// 第四個引數:函式的型別,(返回值+引數型別) v:void @:物件->self :表示SEL->_cmd
class_addMethod(self, @selector(eat), eat, "v@:");
下面我們就來看看具體訊息傳送之後是怎麼來動態查詢對應的方法的。
首先,編譯器將程式碼[obj makeText];轉化為objc_msgSend(obj, @selector
(makeText));,在objc_msgSend函式中。首先通過obj的isa指標找到obj對應的class。在Class中先去cache中
通過SEL查詢對應函式method(猜測cache中method列表是以SEL為key通過hash表來儲存的,這樣能提高函式查詢速度),若
cache中未找到。再去methodList中查詢,若methodlist中未找到,則取superClass中查詢。若能找到,則將method加
入到cache中,以方便下次查詢,並通過method中的函式指標跳轉到對應的函式中去執行。
複製程式碼
例項變數、屬性相關
- 例項變數和屬性也是類物件的關鍵配置。
- 資料型別:
Ivar;
typedef struct objc_ivar *Ivar;
struct objc_ivar {
char *ivar_name OBJC2_UNAVAILABLE; // 變數名
char *ivar_type OBJC2_UNAVAILABLE; // 變數型別
int ivar_offset OBJC2_UNAVAILABLE; // 基地址偏移位元組
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
}
objc_property_attribute_t(屬性的特性有:返回值、是否為atomic、getter/setter名字、是否為dynamic、背後使用的ivar名字、是否為弱引用等)
typedef struct {
const char *name; // 特性名
const char *value; // 特性值
} objc_property_attribute_t;
複製程式碼
- 操作函式
ivar_:
get:
// 獲取成員變數名
const char * ivar_getName ( Ivar v );
// 獲取成員變數型別編碼
const char * ivar_getTypeEncoding ( Ivar v );
// 獲取成員變數的偏移量
ptrdiff_t ivar_getOffset ( Ivar v );
property
// 獲取屬性名
const char * property_getName ( objc_property_t property );
// 獲取屬性特性描述字串
const char * property_getAttributes ( objc_property_t property );
// 獲取屬性中指定的特性
char * property_copyAttributeValue ( objc_property_t property, const char *attributeName );
// 獲取屬性的特性列表
objc_property_attribute_t * property_copyAttributeList ( objc_property_t property, unsigned int *outCount );
複製程式碼
方法訊息機制相關
-
SEL
SEL又叫選擇器,是表示一個方法的selector的指標,對映方法的名字。Objective-C在編譯時,會依據每一個方法的名字、引數序列,生成一個唯一的整型標識(Int型別的地址),這個標識就是SEL。 SEL的作用是作為IMP的KEY,儲存在NSSet中,便於hash快速查詢方法。SEL不能相同,對應方法可以不同。所以在Objective-C同一個類(及類的繼承體系)中,不能存在2個同名的方法,就算引數型別不同。多個方法可以有同一個SEL。 不同的類可以有相同的方法名。不同類的例項物件執行相同的selector時,會在各自的方法列表中去根據selector去尋找自己對應的IMP 相關概念:型別編碼(Type Encoding) 編譯器將每個方法的返回值和引數型別編碼為一個字串,並將其與方法的selector關聯在一起。可以使用@encode編譯器指令來獲取它。
typedef struct objc_selector *SEL; <objc/runtime.h>中沒有公開具體的objc_selector結構體成員。
但通過log可知SEL本質是一個字串。
-
IMP
IMP是指向實現函式的指標,通過SEL取得IMP後,我們就獲得了最終要找的實現函式的入口
typedefine id (*IMP)(id, SEL, ...)
-
Method
這個結構體相當於在SEL和IMP之間作了一個繫結。這樣有了SEL,我們便可以找到對應的IMP,從而呼叫方法的實現程式碼。(在執行時才將SEL和IMP繫結, 動態配置方法) 複製程式碼
typedef struct objc_method *Method;
struct objc_method { SEL method_name OBJC2_UNAVAILABLE; // 方法名 char *method_types OBJC2_UNAVAILABLE; // 引數型別 IMP method_imp OBJC2_UNAVAILABLE; // 方法實現 } objc_method_list 就是用來儲存當前類的方法連結串列,objc_method儲存了類的某個方法的資訊。 struct objc_method_list { struct objc_method_list obsolete OBJC2_UNAVAILABLE; int method_count OBJC2_UNAVAILABLE; #ifdef LP64 int space OBJC2_UNAVAILABLE; #endif / variable length structure */ struct objc_method method_list[1] OBJC2_UNAVAILABLE; }
```
複製程式碼
-
方法快取
方法呼叫最先是在方法快取裡找的,方法呼叫是懶呼叫,第一次呼叫時載入後加到快取池裡。一個objc程式啟動後,需要進行類的初始化、呼叫方法時的cache初始化,再傳送訊息的時候就直接走快取(引申:+load方法和+initialize方法。load方法是首次載入類時呼叫,絕對只呼叫一次;initialize方法是首次給類發訊息時呼叫,通常只呼叫一次,但如果它的子類初始化時未定義initialize方法,則會再呼叫一次它的initialize方法)。 struct objc_cache { // 快取bucket的總數 unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE; // 實際快取bucket的總數 unsigned int occupied OBJC2_UNAVAILABLE; // 指向Method資料結構指標的陣列 Method buckets[1] OBJC2_UNAVAILABLE; }; 複製程式碼
-
操作函式
method_:
invoke: 方法實現的返回值;
// 呼叫指定方法的實現
id method_invoke ( id receiver, Method m, ... );
// 呼叫返回一個資料結構的方法的實現
void method_invoke_stret ( id receiver, Method m, ... );
get: 方法名;方法實現;引數與返回值相關;
// 獲取方法名
SEL method_getName ( Method m );
// 返回方法的實現
IMP method_getImplementation ( Method m );
// 獲取描述方法引數和返回值型別的字串
const char * method_getTypeEncoding ( Method m );
// 返回方法的引數的個數
unsigned int method_getNumberOfArguments ( Method m );
// 通過引用返回方法指定位置引數的型別字串
void method_getArgumentType ( Method m, unsigned int index, char *dst, size_t dst_len );
copy: 返回值型別,引數型別
// 獲取方法的返回值型別的字串
char * method_copyReturnType ( Method m );
// 獲取方法的指定位置引數的型別字串
char * method_copyArgumentType ( Method m, unsigned int index );
// 通過引用返回方法的返回值型別字串
void method_getReturnType ( Method m, char *dst, size_t dst_len );
set:方法實現;
// 設定方法的實現
IMP method_setImplementation ( Method m, IMP imp );
exchange:交換方法實現
// 交換兩個方法的實現
void method_exchangeImplementations ( Method m1, Method m2 );
description : 方法描述
// 返回指定方法的方法描述結構體
struct objc_method_description * method_getDescription ( Method m );
sel_
// 返回給定選擇器指定的方法的名稱
const char * sel_getName ( SEL sel );
// 在Objective-C Runtime系統中註冊一個方法,將方法名對映到一個選擇器,並返回這個選擇器
SEL sel_registerName ( const char *str );
// 在Objective-C Runtime系統中註冊一個方法
SEL sel_getUid ( const char *str );
// 比較兩個選擇器
BOOL sel_isEqual ( SEL lhs, SEL rhs );
c、方法呼叫流程:向物件傳送訊息,實際上是呼叫objc_msgSend函式,obj_msgSend的實際動作就是:找到這個函式指標,然後呼叫它。
id objc_msgSend(receiver self, selector _cmd, arg1, arg2, ...)
self和_cmd是隱藏引數,在編譯期被插入實現程式碼。
self:指向訊息的接受者target的物件型別,作為一個佔位引數,訊息傳遞成功後self將指向訊息的receiver。
_cmd: 指向方法實現的SEL型別。
當向一般物件傳送訊息時,呼叫objc_msgSend;當向super傳送訊息時,呼叫的是objc_msgSendSuper; 如果返回值是一個結構體,則會呼叫objc_msgSend_stret或objc_msgSendSuper_stret。
0.1-檢查target是否為nil。如果為nil,直接cleanup,然後return。(這就是我們可以向nil傳送訊息的原因。) 如果方法返回值是一個物件,那麼傳送給nil的訊息將返回nil;如果方法返回值為指標型別,其指標大小為小於或者等於sizeof(void*),float,double,long double 或者long long的整型標量,傳送給nil的訊息將返回0;如果方法返回值為結構體,傳送給nil的訊息將返回0。結構體中各個欄位的值將都是0;如果方法的返回值不是上述提到的幾種情況,那麼傳送給nil的訊息的返回值將是未定義的。 0.2-如果target非nil,在target的Class中根據Selector去找IMP。(因為同一個方法可能在不同的類中有不同的實現,所以我們需要依賴於接收者的類來找到的確切的實現)。
1-首先它找到selector對應的方法實現:
*1.1-在target類的方法快取列表裡檢查有沒有對應的方法實現,有的話,直接呼叫。 *1.2-比較請求的selector和類方法列表中的selector,對應的話,直接呼叫。
*1.3-比較請求的selector和父類方法列表,父類的父類,直至根類,如果有對應,則直接呼叫。(方法重寫攔截父類方法的原理)
2-呼叫方法實現,並將接收者物件及方法的所有引數傳給它。
3-最後,將實現函式的返回值作為自己的返回值。
複製程式碼
- 動態方法解析與訊息轉發:如果以上的類中沒有找到對應的selector(一般保險起見先用respondsToSelector:內省判斷):,還可以利用訊息轉發機制依次執行以下流程:
Method Resolution(動態方法解析):
用所屬類的類方法+(BOOL)resolveInstanceMethod:(例項方法)或者+(BOOL)resolveClassMethod:(類方法),在此方法裡新增class_addMethod函式。一般用於@dynamic動態屬性。(當一個屬性宣告為@dynamic,就是向編譯器保證編譯時不用管/get實現,一定會在執行時實現)。
Fast Forwarding (快速訊息轉發):
如果上一步無法響應訊息,呼叫- (id)forwardingTargetForSelector:(SEL)aSelector方法,將訊息接受者轉發到另一個物件target(不能為self,否則死迴圈)。
Normal Forwarding(普通訊息轉發):
如果上一步無法響應訊息:
呼叫方法簽名- (NSMethodSignature )methodSignatureForSelector:(SEL)aSelector,方法簽名目的將函式的引數型別和返回值封裝;
如果返回非nil,則建立一個NSInvocation物件利用方法簽名和selector封裝未被處理的訊息,作為引數傳遞給- (void)forwardInvocation:(NSInvocation )anInvocation。
這一步比較耗時。
如果以上步驟(訊息傳遞和訊息轉發)還是不能響應訊息,則調動doesNotRecognizeSelector:方法,丟擲異常。
複製程式碼
- 協議相關:@protocol宣告瞭可以被其他任何類實現的方法,協議僅僅是定義一個介面,而由其他的類去負責實現。 protocol是一個物件結構體。
objc_:
// 返回指定的協議
Protocol * objc_getProtocol ( const char *name );
// 獲取執行時所知道的所有協議的陣列
Protocol ** objc_copyProtocolList ( unsigned int *outCount );
// 建立新的協議例項
Protocol * objc_allocateProtocol ( const char *name );
// 在執行時中註冊新建立的協議
void objc_registerProtocol ( Protocol *proto );
protocol_:
get: 協議;屬性;
// 返回協議名
const char * protocol_getName ( Protocol *p );
// 獲取協議的指定屬性
objc_property_t protocol_getProperty ( Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty );
copy:協議列表;屬性列表;
// 獲取協議中的屬性列表
objc_property_t * protocol_copyPropertyList ( Protocol *proto, unsigned int *outCount );
// 獲取協議採用的協議
Protocol ** protocol_copyProtocolList ( Protocol *proto, unsigned int *outCount );
add:屬性;方法;協議;
// 為協議新增方法
void protocol_addMethodDescription ( Protocol *proto, SEL name, const char *types, BOOL isRequiredMethod, BOOL isInstanceMethod );
// 新增一個已註冊的協議到協議中
void protocol_addProtocol ( Protocol *proto, Protocol *addition );
// 為協議新增屬性
void protocol_addProperty ( Protocol *proto, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount, BOOL isRequiredProperty, BOOL isInstanceProperty );
isEqual:判斷兩協議等同;
// 測試兩個協議是否相等
BOOL protocol_isEqual ( Protocol *proto, Protocol *other );
comform:判斷是否遵循協議;
// 檢視協議是否採用了另一個協議
BOOL protocol_conformsToProtocol ( Protocol *proto, Protocol *other );
複製程式碼
- 動態實現
Method Swizzling;
Method Swizzling可以在執行時通過修改類的方法列表中selector對應的函式或者設定交換方法實現,來動態修改方法。可以重寫某個方法而不用繼承,同時還可以呼叫原先的實現。通常應用於在category中新增一個方法。
為保證改變方法引起衝突,確保方法混用只能一次性:
比如,在+load方法或者dispatch_once中執行。
ISA Swizzling;
ISA Swizzling可以動態修改物件的isa指標,改變物件的類,類似於建立子類實現相同的功能。KVO即是同過ISA Swizzling實現的。
複製程式碼
- 其他概念:category、super
category:
typedef struct objc_category *Category;
struct objc_category {
char *category_name OBJC2_UNAVAILABLE; // 分類名
char *class_name OBJC2_UNAVAILABLE; // 分類所屬的類名
struct objc_method_list *instance_methods OBJC2_UNAVAILABLE; // 例項方法列表
struct objc_method_list *class_methods OBJC2_UNAVAILABLE; // 類方法列表
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 分類所實現的協議列表
}
// objc-runtime-new.h中定義:
struct category_t {
const char *name; // name 是指 class_name 而不是 category_name
classref_t cls; // cls是要擴充套件的類物件,編譯期間是不會定義的,而是在Runtime階段通過name對應到對應的類物件
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties; // instanceProperties表示Category裡所有的properties,(這就是我們可以通過objc_setAssociatedObject和objc_getAssociatedObject增加例項變數的原因,)不過這個和一般的例項變數是不一樣的
};
category是定義方法的結構體,instance_methods列表是objc_class中方法列表的一個子集,class_methods列表是元類方法列表的一個子集。由其結構成員可知,category為什麼不能新增成員變數(可新增屬性,只有set/get方法)。
給category新增方法後,category_list會生成method list。這個方法列表是倒序新增的,也就是說,新生成的category的方法會先於舊的category的方法插入。(category的方法會優先於類方法執行)。
super:
super並不是隱藏引數,它實際上只是一個”編譯器標示符”,它負責告訴編譯器,當呼叫方法時,跳過當前類去呼叫父類的方法,而不是本類中的方法。self是類的一個隱藏引數,每個方法的實現的第一個引數即為self。實際上給super發訊息時,super還是與self指向的是相同的訊息接收者。
struct objc_super {
__unsafe_unretained id receiver;
__unsafe_unretained Class super_class;
};
原理:使用super來接收訊息時,編譯器會生成一個objc_super結構體。傳送訊息時,不是呼叫objc_msgSend函式,而是呼叫objc_msgSendSuper函式:(這就是為什麼我們們在super init 返回的是本類)
id objc_msgSendSuper ( struct objc_super *super, SEL op, ... );
該函式實際的操作是:從objc_super結構體指向的superClass的方法列表開始查詢selector,找到後以objc->receiver去呼叫這個selector。
複製程式碼
- RunTime對一些方法的實現
- (Class)class ;
- (Class)class {
return object_getClass(self);
}
+ (Class)class;
+ (Class)class {
return self;
}
- (BOOL)isKindOf:aClass;// (for迴圈遍歷父類,每次判斷返回的結果可能不同)
- (BOOL)isKindOf:aClass
{
Class cls;
for (cls = isa; cls; cls = cls->superclass)
if (cls == (Class)aClass)
return YES;
return NO;
}
- (BOOL)isMemberOf:aClass;
- (BOOL)isMemberOf:aClass
{
return isa == (Class)aClass;
}
複製程式碼