zoukankan      html  css  js  c++  java
  • iOS AOP框架Aspects实现原理

    总结:

    Aspects 是对 类的继承结构isa、mataclass结构的调整和维护;相当于链表的节点插入和删除;

    同时使用method Swizzling 对方法统一重定向;

    同时使用类似代理的机制对消息进行转发;

    在类结构调整和消息交换重定向的过程中插入织入的功能。

     

    前言

    众所周知,Aspects框架运用了AOP(面向切面编程)的思想,这里解释下AOP的思想:AOP是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。也许大家用Aspects提供的两个方法用着很爽,有没有想揭开它的神秘面纱,看一下它的庐山真面目?接下来主要讲下Aspects的主要实现原理。


     
    ruhua.gif

    实现原理

    其实主要涉及到两点,用过runtime的同学都知道swizzling和消息转发,如果还不是很清楚的同学可以看下我之前写的一篇文章OC中swizzling的“移魂大法”,“移魂大法”我这边就不累赘了,提下消息转发。

    消息转发

     
    MessageForward.jpg

    当向一个对象发送消息的时候,在这个对象的类继承体系中没有找到该方法的时候,就会Crash掉,抛出* unrecognized selector sent to …异常信息,但是在抛出这个异常前其实还是可以拯救的,Crash之前会依次执行下面的方法:

    1. resolveInstanceMethod(或resolveClassMethod):实现该方法,可以通过class_addMethod添加方法,返回YES的话系统在运行时就会重新启动一次消息发送的过程,NO的话会继续执行下一个方法。

    2.forwardingTargetForSelector:实现该方法可以将消息转发给其他对象,只要这个方法返回的不是nil或self,也会重启消息发送的过程,把这消息转发给其他对象来处理。

    1. methodSignatureForSelector:会去获取一个方法签名,如果没有获取到的话就回直接挑用doesNotRecognizeSelector,如果能获取的话系统就会创建一个NSlnvocation传给forwardInvocation方法。

    2. forwardInvocation:该方法是上一个步传进来的NSlnvocation,然后调用NSlnvocationinvokeWithTarget方法,转发到对应的Target

    3. doesNotRecognizeSelector:抛出unrecognized selector sent to …异常。

    整体原理

    上面讲了几种消息转发的方法,Aspects主要是利用了forwardInvocation进行转发,Aspects其实利用和kvo类似的原理,通过动态创建子类的方式,把对应的对象isa指针指向创建的子类,然后把子类的forwardInvocationIMP替成__ASPECTS_ARE_BEING_CALLED__,假设要hook的方法名XX,在子类中添加一个Aspects_XX的方法,然后将Aspects_XXIMP指向原来的XX方法的IMP,这样方便后面调用原始的方法,再把要hook的方法XXIMP指向_objc_msgForward,这样就进入了消息转发流程,而forwardInvocationIMP被替换成了__ASPECTS_ARE_BEING_CALLED__,这样就会进入__ASPECTS_ARE_BEING_CALLED__进行拦截处理,这样整个流程大概结束。

    代码解析

    代码解析这块的话主要讲下核心的模块,一些边边角角的东西去看了自然就明白了。

    typedef NS_OPTIONS(NSUInteger, AspectOptions) {
        AspectPositionAfter   = 0,            /// Called after the original implementation (default)
        AspectPositionInstead = 1,            /// Will replace the original implementation.
        AspectPositionBefore  = 2,            /// Called before the original implementation.
    
        AspectOptionAutomaticRemoval = 1 << 3 /// Will remove the hook after the first execution.
    };
    

    AspectOptions提供各种姿势,前插、后插、整个方法替换都行,简直就是爽。

    static void aspect_performLocked(dispatch_block_t block) {
        static OSSpinLock aspect_lock = OS_SPINLOCK_INIT;
        OSSpinLockLock(&aspect_lock);
        block();
        OSSpinLockUnlock(&aspect_lock);
    }
    

    aspect_addaspect_remove方法里面用了aspect_performLocked, 而aspect_performLocked方法用了OSSpinLockLock加锁机制,保证线程安全并且性能高。不过这种锁已经不在安全,主要原因发生在低优先级线程拿到锁时,高优先级线程进入忙等(busy-wait)状态,消耗大量 CPU 时间,从而导致低优先级线程拿不到 CPU 时间,也就无法完成任务并释放锁。这种问题被称为优先级反转,有兴趣的可以点击任意门不再安全的 OSSpinLock

    static Class aspect_hookClass(NSObject *self, NSError **error) {
        NSCParameterAssert(self);
        Class statedClass = self.class;
        Class baseClass = object_getClass(self);
        NSString *className = NSStringFromClass(baseClass);
    
        // Already subclassed
        if ([className hasSuffix:AspectsSubclassSuffix]) {
            return baseClass;
    
            // We swizzle a class object, not a single object.
        }else if (class_isMetaClass(baseClass)) {
        
            return aspect_swizzleClassInPlace((Class)self);
            // Probably a KVO'ed class. Swizzle in place. Also swizzle meta classes in place.
        }else if (statedClass != baseClass) {
            return aspect_swizzleClassInPlace(baseClass);
        }
    
        // Default case. Create dynamic subclass.
        const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String;
        Class subclass = objc_getClass(subclassName);
    
        if (subclass == nil) {
            subclass = objc_allocateClassPair(baseClass, subclassName, 0);
            if (subclass == nil) {
                NSString *errrorDesc = [NSString stringWithFormat:@"objc_allocateClassPair failed to allocate class %s.", subclassName];
                AspectError(AspectErrorFailedToAllocateClassPair, errrorDesc);
                return nil;
            }
    
            aspect_swizzleForwardInvocation(subclass);
            aspect_hookedGetClass(subclass, statedClass);
            aspect_hookedGetClass(object_getClass(subclass), statedClass);
            objc_registerClassPair(subclass);
        }
    
        object_setClass(self, subclass);
        return subclass;
    }
    

    aspect_hookClass这个方法顾名思义就是要hook对应的Class,这在里创建子类,通过aspect_swizzleForwardInvocation方法把forwardInvocation的实现替换成__ASPECTS_ARE_BEING_CALLED__aspect_hookedGetClass修改了 subclass 以及其 subclass metaclass 的 class 方法,使他返回当前对象的 class,最后通过object_setClass把isa指针指向subclass

    static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
        NSCParameterAssert(selector);
        Class klass = aspect_hookClass(self, error);
        Method targetMethod = class_getInstanceMethod(klass, selector);
        IMP targetMethodIMP = method_getImplementation(targetMethod);
        if (!aspect_isMsgForwardIMP(targetMethodIMP)) {
            // Make a method alias for the existing method implementation, it not already copied.
            const char *typeEncoding = method_getTypeEncoding(targetMethod);
            SEL aliasSelector = aspect_aliasForSelector(selector);
            if (![klass instancesRespondToSelector:aliasSelector]) {
                __unused BOOL addedAlias = class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding);
                NSCAssert(addedAlias, @"Original implementation for %@ is already copied to %@ on %@", NSStringFromSelector(selector), NSStringFromSelector(aliasSelector), klass);
            }
    
            // We use forwardInvocation to hook in.
            class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
            AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector));
        }
    }
    

    通过aspect_isMsgForwardIMP方法判断要hook的方法时候被hook了,这里如果有使用JSPatch的话会冲突掉,不过JSPatch已经被苹果禁止使用了,苹果爸爸说的算。如果没有被hook走的话会先创建一个有自己别名的方法,然后把这个带有别名方法的IMP指向原始要hook方法的实现,最后要hook的方法的IMP指向_objc_msgForward或者_objc_msgForward_stret,为什么还会有一个_objc_msgForward_stret,详细原因可以参考JSPatch实现原理详解<二>里面的Special Struct

    // This is the swizzled forwardInvocation: method.
    static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) {
        NSCParameterAssert(self);
        NSCParameterAssert(invocation);
        SEL originalSelector = invocation.selector;
        SEL aliasSelector = aspect_aliasForSelector(invocation.selector);
        invocation.selector = aliasSelector;
        AspectsContainer *objectContainer = objc_getAssociatedObject(self, aliasSelector);
        AspectsContainer *classContainer = aspect_getContainerForClass(object_getClass(self), aliasSelector);
        AspectInfo *info = [[AspectInfo alloc] initWithInstance:self invocation:invocation];
        NSArray *aspectsToRemove = nil;
    
        // Before hooks.
        aspect_invoke(classContainer.beforeAspects, info);
        aspect_invoke(objectContainer.beforeAspects, info);
    
        // Instead hooks.
        BOOL respondsToAlias = YES;
        if (objectContainer.insteadAspects.count || classContainer.insteadAspects.count) {
            aspect_invoke(classContainer.insteadAspects, info);
            aspect_invoke(objectContainer.insteadAspects, info);
        }else {
            Class klass = object_getClass(invocation.target);
            do {
                if ((respondsToAlias = [klass instancesRespondToSelector:aliasSelector])) {
                    [invocation invoke];
                    break;
                }
            }while (!respondsToAlias && (klass = class_getSuperclass(klass)));
        }
    
        // After hooks.
        aspect_invoke(classContainer.afterAspects, info);
        aspect_invoke(objectContainer.afterAspects, info);
    
        // If no hooks are installed, call original implementation (usually to throw an exception)
        if (!respondsToAlias) {
            invocation.selector = originalSelector;
            SEL originalForwardInvocationSEL = NSSelectorFromString(AspectsForwardInvocationSelectorName);
            if ([self respondsToSelector:originalForwardInvocationSEL]) {
                ((void( *)(id, SEL, NSInvocation *))objc_msgSend)(self, originalForwardInvocationSEL, invocation);
            }else {
                [self doesNotRecognizeSelector:invocation.selector];
            }
        }
    
        // Remove any hooks that are queued for deregistration.
        [aspectsToRemove makeObjectsPerformSelector:@selector(remove)];
    }
    

    该函数为swizzle后, 实现新IMP统一处理的核心方法 , 完成一下几件事:

    • 处理调用逻辑, 有before, instead, after, remove四种option
    • 将block转换成一个NSInvocation对象以供调用。
    • AspectsContainer根据aliasSelector取出对象, 并组装一个AspectInfo, 带有原函数的调用参数和各项属性, 传给外部的调用者 (在这是block) .
    • 调用完成后销毁带有removeOptionhook逻辑, 将原selector挂钩到原IMP上, 删除别名selector

    总结

    -forwardInvocation:的消息转发虽然看起来很神奇,但是平时没什么需求的话尽量不要去碰这个东西,一般的话swizzling基本可以满足我们项目的大部分需求了,还有就是JSPatch既然不能用了,但是Aspects这个框架还是正常上AppStore的,利用这个库的话还是可以弄出轻量级Hotfix,感兴趣的同学可以点击轻量级低风险 iOS 热更新方案



    作者:花了个缺
    链接:https://www.jianshu.com/p/0d43db446c5b
    來源:简书
    简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

  • 相关阅读:
    k8s命令
    git绿色、红色图标不显示的问题
    Git下载
    文档(PDF Word Excel PPT)转HTML前端预览方案
    腾讯云生成临时访问链接
    cron表达式的双重人格:星期和数字到底如何对应?
    Windows下nginx报错解决:CreateFile() "xxx/logs/nginx.pid" failed
    Windows命令行在任意位置启动和退出nginx
    解决博客园TinyMCE模式下内置插入代码块功能不支持Go语言的问题(两个并不完美的解决方案)
    linux系统调用system()函数详解
  • 原文地址:https://www.cnblogs.com/feng9exe/p/10386486.html
Copyright © 2011-2022 走看看