上一篇文章Runtime從入門到進階一介紹了消息發送,以及與之相關的Object、Class、metaClass、Method等。這一篇文章將介紹動態方法解析、消息轉發和 runtime 的具體應用。
在消息解析階段找不到 selector,會進入動態方法解析、消息轉發,這樣可以捕獲、響應未處理消息。即當有未處理消息時,提供了額外處理的機會,避免應用崩潰。
執行以下代碼會發生什麼?
[engineer run];
如果Engineer
實現了run
方法,runtime 會查找到實現run
方法的類並調用;當查找不到run
方法時,會進入以下階段:
- 動態方法解析:runtime 查看該類的
resolveInstanceMethod:
方法,在該方法內使用class_addMethod()
函數動態添加方法並返回YES
。這時會再次進入消息發送階段,並標記爲已進行動態方法解析,即使找不到方法也不會再次進行動態方法解析。如果是類方法,在resolveClassMethod:
方法內實現所需功能。 - 快速轉發:runtime 查看該類的
forwardingTargetForSelector:
方法。如果該方法返回值不爲nil
、self
,返回的 target 進入處理消息階段。 - 消息轉發:runtime 先查看
methodSignatureForSelector:
方法返回值,判斷返回值、參數類型。如果 method signature 不爲nil
,runtime 創建描述消息的NSInvocation
,並向對象發送forwardInvocation:
;如果 method signature 爲nil
,表示徹底放棄處理消息,runtime 發送doesNotRecognizeSelector:
消息。
1. 動態方法解析
Runtime 通過查找方法或IMP
來發送消息,然後跳轉到該方法。有時,將IMP
動態的插入到類中,而非預先設置可能很有用。這樣可以實現快速”轉發“,因爲解析之後,其將作爲常規消息發送的一部分進行調用。缺點是其不夠靈活,需要準備好要插入的IMP
和其 type encoding。
void otherRun(id self, SEL _cmd) {
NSLog(@"%s", __func__);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(run)) {
class_addMethod([self class], sel, (IMP)otherRun, "v@:");
return YES;
} else {
return [super resolveInstanceMethod:sel];
}
}
調用run
方法,輸出如下:
// 調用 run
[engineer run];
// 輸出如下
otherRun
可以看到其並沒有崩潰。如果要動態解析類方法,重寫resolveClassMethod:
即可。
事實上,動態解析之後返回YES
和NO
沒有區別。源碼如下:
/***********************************************************************
* _class_resolveMethod
* Call +resolveClassMethod or +resolveInstanceMethod.
* Returns nothing; any result would be potentially out-of-date already.
* Does not check if the method already exists.
**********************************************************************/
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
// 動態解析實例方法
_class_resolveInstanceMethod(cls, sel, inst);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
// 動態解析類方法
_class_resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
_class_resolveInstanceMethod(cls, sel, inst);
}
}
}
/***********************************************************************
* _class_resolveInstanceMethod
* Call +resolveInstanceMethod, looking for a method to be added to class cls.
* cls may be a metaclass or a non-meta class.
* Does not check if the method already exists.
**********************************************************************/
static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// Resolver not implemented.
return;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
// 拿到是否動態解析的返回值
bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
IMP imp = lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
// 使用返回值進行打印,並沒有其他操作。
if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}
可以看到,只使用返回值進行了打印,沒有進一步的操作。但推薦遵守文檔規範,解析後返回YES
;反之,返回NO
。
2. 消息轉發
如果在動態方法解析階段未處理消息,則會進入消息轉發階段,其首先進入快速轉發。
2.1 快速轉發
進入轉發階段後,runtime 首先會判斷是否希望將消息完整轉發給其他對象。這是常用的轉發,開銷相對小一些。
Runtime 會給forwardingTargetForSelector:
發送消息,如果該方法返回non-nil
、non-self
對象,則返回的對象作爲 receiver 接收消息;如果返回的是self
,則會進入無限循環。如果在子類實現了該方法,但對某個 selector 沒有對象可以返回,則需調用super
實現。
在下面的代碼中,Engineer
只聲明瞭numberOfDaysInMonth:
方法,並未實現。Student
類聲明並實現了numberOfDaysInMonth:
方法:
@interface Engineer : Person
- (int)numberOfDaysInMonth:(int)month;
@end
@implementation Engineer
@end
@interface Student : NSObject
- (int)numberOfDaysInMonth:(int)month;
@end
@implementation Student
- (int)numberOfDaysInMonth:(int)month {
NSLog(@"%s", __func__);
return 99;
}
@end
執行[engineer numberOfDaysInMonth:3]
會崩潰,可以通過將numberOfDaysInMonth:
消息轉發給Student
解決這一問題。
在Engineer.m
實現部分添加以下代碼:
// 快速轉發
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(numberOfDaysInMonth:)) {
return [[Student alloc] init];
} else {
return [super forwardingTargetForSelector:aSelector];
}
}
再次執行[engineer numberOfDaysInMonth:3]
,輸出如下:
-[Student numberOfDaysInMonth:]
通過
forwardingTargetForSelector:
可以實現多重繼承。
forwardingTargetForSelector:
可以將未知消息轉發給其他對象處理,其比forwardInvocation:
更爲輕量。如果只將消息轉發給其他對象處理,這是一個很好的解決方案;如果想要捕獲NSInvocation
,獲取轉發消息的參數、返回值,則應使用forwardInvocation:
方法。
2.2 常規轉發
動態方法解析、快速轉發是轉發的優化,可以進行快速轉發。如果沒有在上述階段採取措施,就會進入常規消息轉發。在常規消息轉發時,會創建封裝了消息信息的NSInvocation
,NSInvocation
包含 target、selector、參數,還可以控制返回值。
爲了將參數封裝到NSInvocation
,runtime 需要知道參數數量、類型,返回值類型,這些信息封裝到NSMethodSignature
提供。通過methodSignatureForSelector:
方法爲其提供NSMethodSignature
。
invocation 創建完成後,runtime 調用forwardInvocation:
方法。在該方法內,可以進行任意操作。
假設沒有在快速處理階段處理numberOfDaysInMonth:
方法,可以通過下面代碼進行處理:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(numberOfDaysInMonth:)) {
return [NSMethodSignature signatureWithObjCTypes:"i@:I"];
} else {
return [super methodSignatureForSelector:aSelector];
}
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"%@ %@", anInvocation.target, NSStringFromSelector(anInvocation.selector));
[anInvocation invokeWithTarget:[[Student alloc] init]];
}
invokeWithTarget:
方法可以更改消息的 target。
另外,getArgument:atIndex:
方法可以獲取 invocation 參數:
- (void)forwardInvocation:(NSInvocation *)anInvocation {
...
int month;
[anInvocation getArgument:&month atIndex:2];
NSLog(@"%d", month + 10);
}
NSInvocation
第一個參數是 id 類型的self
,第二個參數是 SEL 類型的_cmd
。所以,上述方法的 month 參數 index 爲2。
利用forwardInvocation:
可以解決方法找不到的異常問題。
Runtime 不僅對實例方法進行轉發,也會對類方法進行轉發,但 Xcode 不能自動補全
forwardingTargetForSelector:
、methodSignatureForSelector:
、forwardInvocation:
類方法。// 快速轉發 + (id)forwardingTargetForSelector:(SEL)aSelector { if (aSelector == @selector(numberOfDaysInMonth:)) { return [Student class]; } else { return [super forwardingTargetForSelector:aSelector]; } } // 常規轉發 + (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { if (aSelector == @selector(numberOfDaysInMonth:)) { return [NSMethodSignature signatureWithObjCTypes:"i@:I"]; } else { return [super methodSignatureForSelector:aSelector]; } } + (void)forwardInvocation:(NSInvocation *)anInvocation { NSLog(@"%@ %@", anInvocation.target, NSStringFromSelector(anInvocation.selector)); [anInvocation invokeWithTarget:[Student class]]; int month; [anInvocation getArgument:&month atIndex:2]; NSLog(@"%d", month + 10); }
下面是消息查找的源碼:
/***********************************************************************
* 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)
{
IMP imp = nil;
// 首次進入,標記爲未進行動態方法解析。
bool triedResolver = NO;
runtimeLock.assertUnlocked();
// 緩存中如果存在,直接從緩存中取。
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
// runtimeLock is held during isRealized and isInitialized checking
// to prevent races against concurrent realization.
// runtimeLock is held during method search 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.
runtimeLock.read();
if (!cls->isRealized()) {
// Drop the read-lock and acquire the write-lock.
// realizeClass() checks isRealized() again to prevent
// a race while the lock is down.
runtimeLock.unlockRead();
runtimeLock.write();
realizeClass(cls);
runtimeLock.unlockWrite();
runtimeLock.read();
}
if (initialize && !cls->isInitialized()) {
runtimeLock.unlockRead();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.read();
// 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
}
retry:
runtimeLock.assertReading();
// Try this class's cache.
imp = cache_getImp(cls, sel);
if (imp) goto done;
// Try this class's method lists.
{
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) { // 從當前類的 method lists查找,找到後添加到緩存。
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}
// 進入父類緩存、method lists查找
// Try superclass caches and method lists.
{
unsigned attempts = unreasonableClassCount();
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
// Halt if there is a cycle in the superclass chain.
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}
// 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.
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) { // 在父類 method list 找到,添加到消息接收者緩存。
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);
runtimeLock.read();
// 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;
}
// 沒有找到imp,動態解析也沒有找到,進行消息轉發。
// 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();
return imp;
}
3. 具體應用
Runtime 應用場景非常多,下面介紹一些常用的場景。
3.1 交換方法 Method Swizzling
Method swizzling 是改變現有 selector 的實現。
假設需要攔截UIButton
點擊事件。可以在每個UIButton
的響應事件中攔截,但需要添加很多代碼。繼承自自定義的UIButton
也是一種解決方案,但需要修改所有按鈕,且後續需使用指定的 button。另一種解決方案是爲UIButton
創建分類,在分類中實現 method swizzling。
@implementation UIControl (Extension)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class cls = [self class];
SEL originalSelector = @selector(sendAction:to:forEvent:);
SEL swizzledSelector = @selector(pr_sendAction:to:forEvent:);
Method originalMethod = class_getInstanceMethod(cls, originalSelector);
Method swizzledMethod = class_getInstanceMethod(cls, swizzledSelector);
BOOL didAddMethod = class_addMethod(cls, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(cls, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
- (void)pr_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
// 進行所需的處理
NSLog(@"--- %@ --- %@ --- %@ ---", self, NSStringFromSelector(action), target);
[self pr_sendAction:action to:target forEvent:event];
}
@end
UIButton
繼承自UIControl
,點擊事件會調用sendAction:to:forEvent:
,因此這裏交換endAction:to:forEvent:
。
3.1.1 load vs initialize
交換方法應寫在load
方法中。
Objective-C 的 runtime 會自動調用load
和initialize
方法。類初次加載時調用load
方法,調用類的第一個實例方法、類方法之前調用initialize
方法。兩個方法都是可選實現,並且僅在實現該方法時才執行。
由於,Method Swizzling 影響全局狀態,應儘可能避免 race condition。類初始化期間確保會調用load
方法,這樣可以使全局行爲一致。相反,initialize
方法不能明確調用時間。如果沒有消息發送給該類,initialize
永遠不會被調用。
3.1.2 Method Swizzling 應在 dispatch_once 中進行
因爲交換方法會改變全局狀態,要儘可能確保其只執行一次。原子性就是這樣一種預防措施,即使從不同線程調用它也可以確保只執行一次。Grand Central Dispatch 的dispatch_once
可以滿足這種需求,在dispatch_once
中執行交換方法應是標準操作。
類初次加載時會調用
load
方法,一般load
只會執行一次。如果沒有使用dispatch_once
,手動調用load
方法後會出現問題。
3.1.3 調用自身
下面的代碼看似會進入無限循環:
- (void)pr_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
// 進行所需的處理
NSLog(@"--- %@ --- %@ --- %@ ---", self, NSStringFromSelector(action), target);
[self pr_sendAction:action to:target forEvent:event];
}
事實證明其不會進入無限循環。pr_sendAction:to:forEvent:
的IMP
指針已經指向了sendAction:to:forEvent:
實現。如果在上述方法內調用sendAction:to:forEvent:
,則會進入無限循環。
分類方法一般需要添加前綴,以避免和系統方法重複。
3.1.4 爲何要先添加 method
既然method_exchangeImplementations()
可以交換方法的IMP
,爲何不直接交換?爲何還需要使用class_addMethod()
、class_replaceMethod()
函數。
User
繼承自NSObject
,PremiumUser
繼承自User
。User
聲明並實現了happyBirthday
方法,PremiumUser
沒有重寫happyBirthday
方法。如果此時在PremiumUser
類直接交換happyBirthday
方法,會發生什麼?
交換之後,子類方法實現指向父類,父類方法實現指向子類。但由於子類並未重寫happyBirthday
方法,如果父類此時調用happyBirthday
方法,app 會拋出unrecognized selector sent to instance:
@implementation PremiumUser
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class cls = [self class];
SEL originalSelector = @selector(happyBirthday);
SEL swizzledSelector = @selector(pr_happyBirthday);
Method originalMethod = class_getInstanceMethod(cls, originalSelector);
Method swizzledMethod = class_getInstanceMethod(cls, swizzledSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);
});
}
- (void)pr_happyBirthday {
NSLog(@"+++ %s +++", __func__);
[self pr_happyBirthday];
}
@end
- (void)testMethodSwizzling {
// 如果直接使用 method_exchangeImplementations(),調用父類的happyBirthday會閃退。
User *user = [[User alloc] init];
[user happyBirthday];
}
使用class_addMethod()
添加到子類,即重寫父類方法,可以解決方法找不到的問題:
BOOL success = class_addMethod(cls, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (success) {
Method original = class_getInstanceMethod(cls, originalSelector);
Method swizzle = class_getInstanceMethod(cls, swizzledSelector);
IMP oriIMP = method_getImplementation(original);
IMP swiIMP = method_getImplementation(swizzle);
class_replaceMethod(cls, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
}
po
查看oriIMP
、swiIMP
指針會發現其現在指向同一IMP
:
(lldb) po oriIMP
(Runtime`-[PremiumUser pr_happyBirthday] at PremiumUser.m:47)
(lldb) po swiIMP
(Runtime`-[PremiumUser pr_happyBirthday] at PremiumUser.m:47)
這時使用class_replaceMethod()
替換IMP
即可。
判斷
class_addMethod()
是比較安全的寫法。如果確定當前類存在要交換的方法,也可以直接使用method_exchangeImplementations()
函數。
method_exchangeImplementations()
函數可以看作是交換IMP
,交換後會清空緩存。
3.1.5 注意事項
Method swizzling 容易發生不可預測的行爲和無法預料的後果,使用時需注意以下事項:
- 在交換方法內調用原始方法的
IMP
,除非有明確原因不進行這種操作。不調用原始實現,可能導致系統私有狀態改變,app 其他功能失效等。 - 分類方法添加前綴,並確保沒有其他方法(包括依賴庫)使用相同名稱。
- 即使你對
Foundation
、UIKit
或其他系統API再熟悉,也應知道系統下一次的更新可能改變原來實現,導致交換方法失效。
還可以通過交換
NSMutableArray
的insertObject:atIndex:
方法,解決向數組添加nil
元素導致崩潰的問題。因爲NSString
、NSDictionary
、NSMutableArray
、NSNumber
是類簇,真實類型是其他類型,需要注意交換方法的類名稱。
3.1.6 method_exchangeImplementations() 源碼
method_exchangeImplementations()
源碼如下:
void method_exchangeImplementations(Method m1, Method m2)
{
if (!m1 || !m2) return;
rwlock_writer_t lock(runtimeLock);
IMP m1_imp = m1->imp;
m1->imp = m2->imp;
m2->imp = m1_imp;
// RR/AWZ updates are slow because class is unknown
// Cache updates are slow because class is unknown
// fixme build list of classes whose Methods are known externally?
flushCaches(nil);
updateCustomRR_AWZ(nil, m1);
updateCustomRR_AWZ(nil, m2);
}
可以看到其只是交換imp
,交換之後清空緩存。
4. 關聯對象 Associated Objects
關聯對象(associated objects,也稱爲 associative references)允許對象在運行時關聯任何值。其有以下三個函數:
- objc_setAssociatedObject:爲指定對象、使用指定key關聯值。值爲
nil
時,用於清楚關聯對象。 - objc_getAssociatedObject:取出指定對象使用指定key關聯的值。
- objc_removeAssociatedObjects:清除指定對象的所有關聯對象。主要用於將對象還原爲「初始狀態」,不應用於從對象中刪除關聯對象,因爲它會刪除所有對象添加到該對象的關聯。通常,爲
objc_setAssociatedObject
傳值nil
以清除關聯。
使用 associated objects 可以爲分類添加屬性:
@interface UIView (DefaultColor)
@property (nonatomic, strong) UIColor *defaultColor;
@end
static void *kDefaultColorKey = &kDefaultColorKey;
@implementation UIView (DefaultColor)
- (UIColor *)defaultColor {
return objc_getAssociatedObject(self, kDefaultColorKey);
}
- (void)setDefaultColor:(UIColor *)defaultColor {
objc_setAssociatedObject(self, kDefaultColorKey, defaultColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
kDefaultColorKey 表示一個靜態變量指向其自身,即一個唯一的常量。由於SEL
也是唯一、常量,也可以使用SEL
替換上述key。
4.1 內存管理
objc_AssociationPolicy
枚舉類型決定關聯值存儲類型。
objc_AssociationPolicy | 對應property | 描述 |
---|---|---|
OBJC_ASSOCIATION_ASSIGN | @property(assign) | 對關聯對象弱引用。 |
OBJC_ASSOCIATION_COPY | @property(nonatomic, strong) | 對關聯對象強引用,非原子性。 |
OBJC_ASSOCIATION_COPY_NONATOMIC | @property(nonatomic, copy) | 複製關聯對象,非原子性。 |
OBJC_ASSOCIATION_RETAIN | @property(atomic, strong) | 對關聯對象強引用,原子性。 |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | @property(atomic, copy) | 複製關聯對象,原子性。 |
4.2 移除關聯對象的值
不要使用objc_removeAssociatedObjects()
函數移除關聯對象,objc_removeAssociatedObjects()
主要用於將對象還原爲「初始狀態」,開發者極少需要調用該函數。需要移除關聯對象時,爲爲objc_setAssociatedObject()
傳值nil
即可。
關聯對象應作爲萬不得已的方法,而非遇到問題的首選解決方案。
5. 創建類
使用 runtime 創建類與使用代碼創建類效果一致,只是 runtime 會直接在內存中創建。可以向類添加方法、實例變量、協議等。
5.1 創建類
使用objc_allocateClassPair()
函數可以在運行時創建類,傳入要創建類的父類、類名稱、大小,返回創建的類:
Class newCls = objc_allocateClassPair([NSObject class], "Dog", 0);
5.2 添加方法
使用class_addMethod()
函數向類添加方法。該函數有以下四個參數:
- cls:要添加方法的類。
- name:方法名稱,即 selector。
- imp:要添加的方法的
IMP
。 - types:要添加方法的 type encoding。
如下所示:
class_addMethod(newCls, @selector(run), (IMP)run, "v@:");
5.3 添加成員變量
使用class_addIvar()
函數添加成員變量。該函數有以下幾個參數:
- cls:要添加成員變量的類。不能是元類,不支持向元類添加成員變量。
- name:成員變量名稱。
- size:成員變量大小。如果成員變量是普通 C 類型,可以使用
sizeof()
獲取其大小。 - alignment:成員變量對齊方式。表示成員變量的存儲在內存中如何對齊,包括與上一個成員變量的padding。該參數一般是 alignment 的 log2,而非 alignment 本身。傳入1表示邊界是2,傳入4表示 alignment 是16字節。由於大部分類型希望按照自身大小對齊,可以直接傳入
log2(sizeof(type))
。 - types:添加的成員變量類型。可以通過
@encode()
指令獲取。
如下所示:
class_addIvar(newCls, "_age", sizeof(int), log2(sizeof(int)), @encode(int));
class_addIvar(newCls, "_weight", sizeof(int), log2(sizeof(int)), @encode(int));
成員變量是隻讀的,位於 class_ro_t *ro,註冊完成後不可變。
只能在
objc_allocateClassPair()
函數後、objc_registerClassPair()
函數前,調用objc_addIvar()
函數。不能使用objc_addIvar()
向已經存在的類添加成員變量。
此外,使用class_addProtocol()
函數聲明類遵守某項協議,使用class_addProperty()
函數爲類添加屬性。
5.4 註冊類
配置完畢後,必須註冊類纔可以使用。使用objc_registerClassPair()
函數註冊類:
objc_registerClassPair(newCls);
5.5 訪問成員變量
訪問成員變量時不能直接調用訪問器方法,因爲編譯器甚至不知道該屬性是否存在。
使用鍵值編碼設值、取值。其會將基本類型轉換爲NSValue
、NSNumber
。
[dog setValue:@10 forKey:@"_age"];
[dog setValue:@20 forKey:@"_weight"];
NSLog(@"%@ %@", [dog valueForKey:@"_age"], [dog valueForKey:@"_weight"]);
5.6 使用類
註冊之後,可以像其他類一樣使用。
id dog = [[newCls alloc] init];
[dog run];
5.7 銷燬類
使用objc_allocateClassPair()
創建的類,不再使用時需使用objc_disposeClassPair()
函數銷燬。但該類及其子類存在時,不能調用objc_disposeClassPair()
,否則會拋出Attempt to use unknown class錯誤。
// 當newClass類及子類存在時,不能調用objc_disposeClassPair()函數,否則會拋出Attempt to use unknown class錯誤。
dog = nil;
// 不需要的時候釋放newCls。
objc_disposeClassPair(newCls);
6. 查看私有成員變量
系統提供的功能有時不能滿足需求,需要修改系統控件的某些屬性。如果沒有可用API直接修改,可以通過class_copyIvarList()
獲取、遍歷其私有成員變量。
修改UITextField
佔位符顏色需要使用NSAttributedString
。如果使用私有成員變量應該如何修改呢?
self.textField.placeholder = @"github.com/pro648";
unsigned int count;
Ivar *ivars = class_copyIvarList(self.textField.class, &count);
for (int i=0; i<count; ++i) {
Ivar ivar = ivars[I];
NSLog(@"%s %s", ivar_getName(ivar), ivar_getTypeEncoding(ivar));
}
free(ivars);
上述代碼會輸出所有成員變量:
...
_clearButton @"UIButton"
_clearButtonOffset {CGSize="width"d"height"d}
_leftViewOffset {CGSize="width"d"height"d}
_rightViewOffset {CGSize="width"d"height"d}
_backgroundView @"UITextFieldBorderView"
_disabledBackgroundView @"UITextFieldBorderView"
_systemBackgroundView @"UITextFieldBackgroundView"
_textContentView @"_UITextFieldCanvasView"
_floatingContentView @"_UIFloatingContentView"
_contentBackdropView @"UIVisualEffectView"
_fieldEditorBackgroundView @"_UIDetachedFieldEditorBackgroundView"
_fieldEditorEffectView @"UIVisualEffectView"
_placeholderLabel @"UITextFieldLabel"
...
可以看到清空是UIButton
,placeholderLabel 是UITextFieldLabel
類型,通過isKindOfClass
可以判斷其是UILabel
子類。因此,可以使用以下代碼修改修改佔位符顏色:
id placeholderLabel = [self.textField valueForKeyPath:@"placeholderLabel"];
if ([placeholderLabel isKindOfClass:UILabel.class]) {
UILabel *label = (UILabel *)placeholderLabel;
label.textColor = [UIColor redColor];
}
由於佔位符使用了懶加載,需先設值佔位符,才能查看到佔位符變量名稱。
4. 問題
4.1 super的本質
調用以下init
方法,輸出結果是什麼?
@interface Browser : NSObject
@end
@implementation Browser
@end
@interface Safari : Browser
- (instancetype)init;
@end
@implementation Safari
- (instancetype)init
{
self = [super init];
if (self) {
NSLog(@"[self class] = %@", [self class]);
NSLog(@"[super class] = %@", [super class]);
NSLog(@"[self superclass] = %@", [self superclass]);
NSLog(@"[super superclass] = %@", [super superclass]);
}
return self;
}
@end
輸出如下:
[self class] = Safari
[super class] = Safari
[self superclass] = Browser
[super superclass] = Browser
class
、superclass
的實現在NSObject
中,源碼如下:
+ (Class)class {
return self;
}
- (Class)class {
return object_getClass(self);
}
+ (Class)superclass {
return self->superclass;
}
- (Class)superclass {
return [self class]->superclass;
}
可以看到,class
方法返回的object_getClass(self)
,即消息接收者的類。superclass
返回的是消息接收者的父類。
objc_super
結構體如下,其定義了兩個成員:消息接收者、父類。
/// Specifies the superclass of an instance.
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained _Nonnull id receiver;
/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained _Nonnull Class class;
#else
__unsafe_unretained _Nonnull Class super_class;
#endif
/* super_class is the first class to search */
};
objc_msgSendSuper()
源碼如下:
/**
* Sends a message with a simple return value to the superclass of an instance of a class.
*
* @param super A pointer to an \c objc_super data structure. Pass values identifying the
* context the message was sent to, including the instance of the class that is to receive the
* message and the superclass at which to start searching for the method implementation.
* @param op A pointer of type SEL. Pass the selector of the method that will handle the message.
* @param ...
* A variable argument list containing the arguments to the method.
*
* @return The return value of the method identified by \e op.
*
* @see objc_msgSend
*/
OBJC_EXPORT id _Nullable
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
可以看到superclass指定開始查找實現的起點。因此,[super class]
只是指定從父類開始查找class
的實現,消息接收者仍然是當前類。由於class
在NSObject
中實現,因此輸出和[self class]
一致。
在self = [super init];
添加斷點,運行至斷點後選擇Debug >> Debug Workflow >> Always Show Disassembly 查看彙編代碼,可以看到其實質上調用的是objc_msgSendSuper2
。
還可以通過 Product >> Perform Action >> Assemble "Safari.m" 將文件轉變爲彙編代碼,如下所示:
...
.loc 3 15 12 prologue_end ; Runtime/Q&A/Safari.m:15:12
ldur x0, [x29, #-8]
mov x1, #0
stur x1, [x29, #-8]
stur x0, [x29, #-32]
ldr x9, [x9]
stur x9, [x29, #-24]
ldr x1, [x8]
sub x0, x29, #32 ; =32
bl _objc_msgSendSuper2
mov x8, x0
stur x8, [x29, #-8]
.loc 3 15 10 is_stmt 0 ; Runtime/Q&A/Safari.m:15:10
...
如果當前選擇的是模擬器,生成的彙編代碼是 x86 架構的。調用 super 方式爲
callq _objc_msgSendSuper2
。
4.2 isKindOf: isMemberOf:
下面代碼打印結果是什麼?
BOOL res1 = [[NSObject class] isKindOfClass:[NSObject class]];
BOOL res2 = [[NSObject class] isMemberOfClass:[NSObject class]];
BOOL res3 = [[Safari class] isKindOfClass:[Safari class]];
BOOL res4 = [[Safari class] isMemberOfClass:[Safari class]];
NSLog(@"%i %i %i %i", res1, res2, res3, res4);
以下是isMemberOfClass:
和isKindOfClass:
區別:
-
isMemberOfClass:
判斷當前實例對象、類對象的isa
是否指向參數中的類、元類。 -
isKindOfClass:
判斷當前實例對象、類對象的isa
是否指向參數中的類、元類及其子類。
在上述方法中,如果調用者是實例對象,參數應爲類對象;如果調用者是類對象,參數應爲元類。
其源碼如下:
+ (BOOL)isMemberOfClass:(Class)cls {
return object_getClass((id)self) == cls;
}
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
可以看到,isMemberOf:
只是比較消息接收者的isa
是否指向參數的類。isKindOfClass:
判斷消息接收者的isa
是否指向參數的類及其子類。
查看以下代碼:
Engineer *engineer = [[Engineer alloc] init];
BOOL res5 = [engineer isKindOfClass:[NSObject class]];
BOOL res6 = [engineer isMemberOfClass:[NSObject class]];
BOOL res7 = [engineer isKindOfClass:[Engineer class]];
BOOL res8 = [engineer isMemberOfClass:[Engineer class]];
NSLog(@"%i %i %i %i", res5, res6, res7, res8);
打印結果是:
1 0 1 1
[NSObject class]
是類對象,右側參數應爲元類,[Safari class]
也一樣。
由於類的isa
指向元類,元類的基類是NSObject
的元類,NSObject
元類的父類指向NSObject
自身。所以,NSObject
是所有類、元類的父類。因此,以下代碼輸出:1 0 0 0。
BOOL res1 = [[NSObject class] isKindOfClass:[NSObject class]];
BOOL res2 = [[NSObject class] isMemberOfClass:[NSObject class]];
BOOL res3 = [[Safari class] isKindOfClass:[Safari class]];
BOOL res4 = [[Safari class] isMemberOfClass:[Safari class]];
NSLog(@"%i %i %i %i", res1, res2, res3, res4);
Demo名稱:Runtime
源碼地址:https://github.com/pro648/BasicDemos-iOS/tree/master/Runtime
參考資料:
- Runtime Method Swizzling 實戰
- Method Swizzling
- Associated Objects
- Friday Q&A 2010-11-6: Creating Classes at Runtime in Objective-C
- Digging Into the Objective-C Runtime