美文网首页
Runtime从入门到进阶二

Runtime从入门到进阶二

作者: pro648 | 来源:发表于2020-12-14 21:08 被阅读0次

    上一篇文章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

    Runtimeobjc_msgSendSuper2.png

    还可以通过 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

    相关文章

      网友评论

          本文标题:Runtime从入门到进阶二

          本文链接:https://www.haomeiwen.com/subject/jrlvgktx.html