Runtime從入門到進階二

上一篇文章Runtime從入門到進階一介紹了消息發送,以及與之相關的Object、Class、metaClass、Method等。這一篇文章將介紹動態方法解析、消息轉發和 runtime 的具體應用。

在消息解析階段找不到 selector,會進入動態方法解析、消息轉發,這樣可以捕獲、響應未處理消息。即當有未處理消息時,提供了額外處理的機會,避免應用崩潰。

執行以下代碼會發生什麼?

    [engineer run];

如果Engineer實現了run方法,runtime 會查找到實現run方法的類並調用;當查找不到run方法時,會進入以下階段:

  1. 動態方法解析:runtime 查看該類的resolveInstanceMethod:方法,在該方法內使用class_addMethod()函數動態添加方法並返回YES。這時會再次進入消息發送階段,並標記爲已進行動態方法解析,即使找不到方法也不會再次進行動態方法解析。如果是類方法,在resolveClassMethod:方法內實現所需功能。
  2. 快速轉發:runtime 查看該類的forwardingTargetForSelector:方法。如果該方法返回值不爲nilself,返回的 target 進入處理消息階段。
  3. 消息轉發: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:即可。

事實上,動態解析之後返回YESNO沒有區別。源碼如下:

/***********************************************************************
* _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-nilnon-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 常規轉發

動態方法解析、快速轉發是轉發的優化,可以進行快速轉發。如果沒有在上述階段採取措施,就會進入常規消息轉發。在常規消息轉發時,會創建封裝了消息信息的NSInvocationNSInvocation包含 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 會自動調用loadinitialize方法。類初次加載時調用load方法,調用類的第一個實例方法、類方法之前調用initialize方法。兩個方法都是可選實現,並且僅在實現該方法時才執行。

由於,Method Swizzling 影響全局狀態,應儘可能避免 race condition。類初始化期間確保會調用load方法,這樣可以使全局行爲一致。相反,initialize方法不能明確調用時間。如果沒有消息發送給該類,initialize永遠不會被調用。

3.1.2 Method Swizzling 應在 dispatch_once 中進行

因爲交換方法會改變全局狀態,要儘可能確保其只執行一次。原子性就是這樣一種預防措施,即使從不同線程調用它也可以確保只執行一次。Grand Central Dispatchdispatch_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繼承自NSObjectPremiumUser繼承自UserUser聲明並實現了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查看oriIMPswiIMP指針會發現其現在指向同一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 其他功能失效等。
  • 分類方法添加前綴,並確保沒有其他方法(包括依賴庫)使用相同名稱。
  • 即使你對FoundationUIKit或其他系統API再熟悉,也應知道系統下一次的更新可能改變原來實現,導致交換方法失效。

還可以通過交換NSMutableArrayinsertObject:atIndex:方法,解決向數組添加nil元素導致崩潰的問題。因爲NSStringNSDictionaryNSMutableArrayNSNumber是類簇,真實類型是其他類型,需要注意交換方法的類名稱。

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 訪問成員變量

訪問成員變量時不能直接調用訪問器方法,因爲編譯器甚至不知道該屬性是否存在。

使用鍵值編碼設值、取值。其會將基本類型轉換爲NSValueNSNumber

    [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

classsuperclass的實現在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的實現,消息接收者仍然是當前類。由於classNSObject中實現,因此輸出和[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

參考資料:

  1. Runtime Method Swizzling 實戰
  2. Method Swizzling
  3. Associated Objects
  4. Friday Q&A 2010-11-6: Creating Classes at Runtime in Objective-C
  5. Digging Into the Objective-C Runtime

歡迎更多指正:https://github.com/pro648/tips

本文地址:https://github.com/pro648/tips/blob/master/sources/Runtime%E4%BB%8E%E5%85%A5%E9%97%A8%E5%88%B0%E8%BF%9B%E9%98%B6%E4%BA%8C.md

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章