[贝聊科技]一次高效的依赖注入

栏目: IOS · 发布时间: 7年前

内容简介:如果基于 Cocopods 和 Git Submodules 来做组件化的时候,我们的依赖关系是这样的:这里依赖路径有两条:这种单向的依赖关系,决定了从组件到项目的通讯是单向的,即主项目可以主动向组件发起通讯,但是组件却没有办法主动和主项目通讯。

如果基于 Cocopods 和 Git Submodules 来做组件化的时候,我们的依赖关系是这样的:

[贝聊科技]一次高效的依赖注入

这里依赖路径有两条:

    1. 最简单的主项目依赖第三方 pods。
    1. 组件依赖第三方 pods,主项目再依赖组件。

这种单向的依赖关系,决定了从组件到项目的通讯是单向的,即主项目可以主动向组件发起通讯,但是组件却没有办法主动和主项目通讯。

你可能说不对,可以发通知啊?是的,是可以发通知,但是这一点都不优雅,也不好维护和拓展。

有没有一种更加优雅、更加方便日常开发的拓展和维护的方式呢?答案是有的,名字叫做“依赖注入”。

02. 依赖注入

依赖注入有另外一个名字,叫做“控制反转”,像上面的组件化的例子,主项目依赖组件,现在有一个需求,组件需要依赖主项目,这种情况就叫做“控制反转”。

能把这部分“控制反转”的代码统一起来解耦维护,方便日后拓展和维护的服务,我们就可以叫做依赖注入。

所以依赖注入有两个比较重要的点:

  • 第一,要实现这种反转控制的功能。
  • 第二,要解耦。

不是我自身的,却是我需要的,都是我所依赖的。一切需要外部提供的,都是需要进行依赖注入的。

这句话出自这篇文章: 理解依赖注入与控制反转 | Laravel China 社区 - 高品质的 Laravel 开发者社区

如果对概念性的东西有更加深入的理解,欢迎谷歌搜索“依赖注入”。

03. iOS 依赖注入调查

iOS 平台实现依赖注入功能的开源项目有两个大头:

详细对比发现这两个框架都是严格遵循依赖注入的概念来实现的,并没有将 Objective-C 的 runtime 特性发挥到极致,所以使用起来很麻烦。

还有一点,这两个框架使用继承的方式实现注入功能,对项目的侵入性不容小视。如果你觉得这个侵入性不算什么,那等到你项目大到一定程度,发现之前选择的技术方案有考虑不周,你想切换到其他方案的时候,你一定会后悔当时没选择那个不侵入项目的方案。

那有没有其他没那么方案呢?

GitHub - jspahrsummers/libextobjc: A Cocoa library to extend the Objective-C programming language. 里有一个 EXTConcreteProtocol 虽然没有直接叫做依赖注入,而是叫做混合协议,但是充分使用了 OC 动态语言的特性,不侵入项目,高度自动化,框架十分轻量,使用非常简单。

轻量到什么地步?就只有一个 .h 一个 .m 文件。 简单到什么地步?就只需要一个 @conreteprotocol 关键字,你就已经注入好了。

从一个评价开源框架的方方面面都甩开上面两个框架好几条街。

但是他也有致命的缺点,鱼和熊掌不可兼得,这个我们等会说。

04. EXTConcreteProtocol 实现原理

有两个比较重要的概念需要提前明白才能继续往下将。

    1. 容器。这里的容器是指,我们注入的方法需要有类(class)来装,而装这些方法的器皿就统称为容器。
    1. __attribute__() 这是一个 GNU 编译器语法,被 constructor 这个关键字修饰的方法会在所有类的 +load 方法之后,在 main 函数之前被调用。详见: Clang Attributes 黑魔法小记 · sunnyxx的技术博客
[贝聊科技]一次高效的依赖注入

如上图,用一句话来描述注入的过程:将待注入的容器中的方法在 load 方法之后 main 函数之前注入指定的类中。

04.1. EXTConcreteProtocol 的使用

比方说有一个协议 ObjectProtocol 。我们只要这样写就已经实现了依赖注入。

@protocol ObjectProtocol<NSObject>

+ (void)sayHello;

- (int)age;

@end

@concreteprotocol(ObjectProtocol)

+ (void)sayHello {
    NSLog(@"Hello");
}

- (int)age {
    return 18;
}

@end
复制代码

之后比方说一个 Person 类想要拥有这个注入方法,就只需要遵守这个协议就可以了。

@interface Person : NSObject<ObjectProtocol>

@end
复制代码

我们接下来就可以对 Person 调用注入的方法。

int main(int argc, char * argv[]) {
     Person *p = [Person new];
	 NSLog(@"%@", [p age]);
	 [p.class sayHello];
}

输出:
>>>18
>>>Hello
复制代码

是不是很神奇?想不想探一下究竟?

04.2. 源码解析

先来看一下头文件:

#define concreteprotocol(NAME) \
	  // 定义一个容器类.
    interface NAME ## _ProtocolMethodContainer : NSObject < NAME > {} \
    @end \
    \
    @implementation NAME ## _ProtocolMethodContainer \
    //  load 方法添加混合协议.
    + (void)load { \
        if (!ext_addConcreteProtocol(objc_getProtocol(metamacro_stringify(NAME)), self)) \
            fprintf(stderr, "ERROR: Could not load concrete protocol %s\n", metamacro_stringify(NAME)); \
    } \
    // load 之后, main 之前执行方法注入.
    __attribute__((constructor)) \
    static void ext_ ## NAME ## _inject (void) { \
ext_loadConcreteProtocol(objc_getProtocol(metamacro_stringify(NAME))); \
    }
//  load 方法添加混合协议.
BOOL ext_addConcreteProtocol (Protocol *protocol, Class methodContainer);
// load 之后, main 之前执行方法注入.
void ext_loadConcreteProtocol (Protocol *protocol);
复制代码

可以在源码中清楚看到 concreteprotocol 这个宏定义为我们的协议添加了一个容器类,我们主要注入的比如 +sayHello-age 方法都被定义在这个容器类之中。

然后在 +load 方法中调用了 ext_addConcreteProtocol 方法。

typedef struct {
    // 用户定义的协议.
    __unsafe_unretained Protocol *protocol;

    // 在 __attribute__((constructor)) 时往指定类里注入方法的 block.
    void *injectionBlock;

    // 对应的协议是否已经准备好注入.
    BOOL ready;
} EXTSpecialProtocol;

BOOL ext_addConcreteProtocol (Protocol *protocol, Class containerClass) { 
    return ext_loadSpecialProtocol(protocol, ^(Class destinationClass){
        ext_injectConcreteProtocol(protocol, containerClass, destinationClass);
    });
}

BOOL ext_loadSpecialProtocol (Protocol *protocol, void (^injectionBehavior)(Class destinationClass)) {
    @autoreleasepool {
        NSCParameterAssert(protocol != nil);
        NSCParameterAssert(injectionBehavior != nil);
        
        // 加锁
        if (pthread_mutex_lock(&specialProtocolsLock) != 0) {
            fprintf(stderr, "ERROR: Could not synchronize on special protocol data\n");
            return NO;
        }
        
        // specialProtocols 是一个链表,每个协议都会被组织成为一个 EXTSpecialProtocol,这个 specialProtocols 里存放了了这些 specialProtocols.
        if (specialProtocolCount >= specialProtocolCapacity) {
           ...
        }

        #ifndef __clang_analyzer__
        ext_specialProtocolInjectionBlock copiedBlock = [injectionBehavior copy];

        // 将协议保存为一个 EXTSpecialProtocol 结构体.
        specialProtocols[specialProtocolCount] = (EXTSpecialProtocol){
            .protocol = protocol,
            .injectionBlock = (__bridge_retained void *)copiedBlock,
            .ready = NO
        };
        #endif

        ++specialProtocolCount;
        pthread_mutex_unlock(&specialProtocolsLock);
    }
    return YES;
}
复制代码

我们的 ext_loadSpecialProtocol 方法里传进去一个 block,这个 block 里调用了 ext_injectConcreteProtocol 这个方法。

ext_injectConcreteProtocol 这个方法接受三个参数,第一个是协议,就是我们要注入的方法的协议;第二个是容器类,就是框架为我们添加的那个容器;第三个参数是目标注入类,就是我们要把这个容器里的方法注入到哪个类。

static void ext_injectConcreteProtocol (Protocol *protocol, Class containerClass, Class class) {
    // 获取容器类里所有的实例方法.
    unsigned imethodCount = 0;
    Method *imethodList = class_copyMethodList(containerClass, &imethodCount);

    // 获取容器类里所有的类方法方法.
    unsigned cmethodCount = 0;
    Method *cmethodList = class_copyMethodList(object_getClass(containerClass), &cmethodCount);
            
    // 拿到要注入方法的类的元类.
    Class metaclass = object_getClass(class);

    // 注入实例方法.
    for (unsigned methodIndex = 0;methodIndex < imethodCount;++methodIndex) {
        Method method = imethodList[methodIndex];
        SEL selector = method_getName(method);

        // 如果该类已经实现了这个方法,就跳过注入,不至于覆盖用户自定义的实现.
        if (class_getInstanceMethod(class, selector)) {
            continue;
        }

        IMP imp = method_getImplementation(method);
        const char *types = method_getTypeEncoding(method);
        if (!class_addMethod(class, selector, imp, types)) {
            fprintf(stderr, "ERROR: Could not implement instance method -%s from concrete protocol %s on class %s\n",
                sel_getName(selector), protocol_getName(protocol), class_getName(class));
        }
    }

    // 注入类方法.
    for (unsigned methodIndex = 0;methodIndex < cmethodCount;++methodIndex) {
        Method method = cmethodList[methodIndex];
        SEL selector = method_getName(method);

        // +initialize 不能被注入.
        if (selector == @selector(initialize)) {
            continue;
        }

        // 如果该类已经实现了这个方法,就跳过注入,不至于覆盖用户自定义的实现.
        if (class_getInstanceMethod(metaclass, selector)) {
            continue;
        }

        IMP imp = method_getImplementation(method);
        const char *types = method_getTypeEncoding(method);
        if (!class_addMethod(metaclass, selector, imp, types)) {
            fprintf(stderr, "ERROR: Could not implement class method +%s from concrete protocol %s on class %s\n",
                sel_getName(selector), protocol_getName(protocol), class_getName(class));
        }
    }

    // 管理内存
    free(imethodList); imethodList = NULL;
    free(cmethodList); cmethodList = NULL;

    // 允许用户在容器类里复写 +initialize 方法,这里调用是保证用户复写的实现能够被执行.
    (void)[containerClass class];
}
复制代码

我们再看一下在 +load 之后 main 之前调用的 ext_loadConcreteProtocol 方法。

void ext_loadConcreteProtocol (Protocol *protocol) {
    ext_specialProtocolReadyForInjection(protocol);
}

void ext_specialProtocolReadyForInjection (Protocol *protocol) {
    @autoreleasepool {
        NSCParameterAssert(protocol != nil);
        
        // 加锁
        if (pthread_mutex_lock(&specialProtocolsLock) != 0) {
            fprintf(stderr, "ERROR: Could not synchronize on special protocol data\n");
            return;
        }

        // 检查要对应的 protocol 是否已经加载进上面的链表中了,如果找到了,就将对应的 EXTSpecialProtocol 结构体的 ready 置为 YES.
        for (size_t i = 0;i < specialProtocolCount;++i) {
            if (specialProtocols[i].protocol == protocol) {
                if (!specialProtocols[i].ready) {
                    specialProtocols[i].ready = YES;
                    assert(specialProtocolsReady < specialProtocolCount);
                    if (++specialProtocolsReady == specialProtocolCount)
						   // 如果所有的 EXTSpecialProtocol 结构体都准备好了,就开始执行注入.
                        ext_injectSpecialProtocols();
                }

                break;
            }
        }
        pthread_mutex_unlock(&specialProtocolsLock);
    }
}
复制代码

上面都是准备工作,接下来开始进入核心方法进行注入。

static void ext_injectSpecialProtocols (void) {
    // 对协议进行排序.
	  // 比方说 A 协议继承自 B 协议,但是不一定是 B 协议对应的容器类的  load 方法先执行,A 的后执行. 所以如果 B 协议的类方法中复写了 A 协议中的方法,那么应该保证 B 协议复写的方法被注入,而不是 A 协议的容器方法的实现.
	  // 为了保证这个循序,所以要对协议进行排序,上面说的 A 继承自 B,那么循序应该是 A 在 B 前面.
    qsort_b(specialProtocols, specialProtocolCount, sizeof(EXTSpecialProtocol), ^(const void *a, const void *b){
        if (a == b)
            return 0;

        const EXTSpecialProtocol *protoA = a;
        const EXTSpecialProtocol *protoB = b;

        int (^protocolInjectionPriority)(const EXTSpecialProtocol *) = ^(const EXTSpecialProtocol *specialProtocol){
            int runningTotal = 0;

            for (size_t i = 0;i < specialProtocolCount;++i) {
                if (specialProtocol == specialProtocols + i)
                    continue;

                if (protocol_conformsToProtocol(specialProtocol->protocol, specialProtocols[i].protocol))
                    runningTotal++;
            }

            return runningTotal;
        };

        return protocolInjectionPriority(protoB) - protocolInjectionPriority(protoA);
    });

	  // 获取项目中所有的类 :sob::sob::sob:.
    unsigned classCount = objc_getClassList(NULL, 0);
    if (!classCount) {
        fprintf(stderr, "ERROR: No classes registered with the runtime\n");
        return;
    }

	Class *allClasses = (Class *)malloc(sizeof(Class) * (classCount + 1));
    if (!allClasses) {
        fprintf(stderr, "ERROR: Could not allocate space for %u classes\n", classCount);
        return;
    }
	classCount = objc_getClassList(allClasses, classCount);

    @autoreleasepool {
        // 遍历所有的要注入的协议结构体.
        for (size_t i = 0;i < specialProtocolCount;++i) {
            Protocol *protocol = specialProtocols[i].protocol;
            
            // 使用 __bridge_transfer 把对象的内存管理交给 ARC.
            ext_specialProtocolInjectionBlock injectionBlock = (__bridge_transfer id)specialProtocols[i].injectionBlock;
            specialProtocols[i].injectionBlock = NULL;

            // 遍历所有的类 :sob::sob::sob:.
            for (unsigned classIndex = 0;classIndex < classCount;++classIndex) {
                Class class = allClasses[classIndex];
                
                // 如果这个类遵守了要注入的协议,那么就执行注入.
				  // 注意: 这里是 continue 不是 break,因为一个类可以注入多个协议的方法.
                if (!class_conformsToProtocol(class, protocol))
                    continue;
                
                injectionBlock(class);
            }
        }
    }

    // 管理内存.
    free(allClasses);
    free(specialProtocols); specialProtocols = NULL;
    specialProtocolCount = 0;
    specialProtocolCapacity = 0;
    specialProtocolsReady = 0;
}
复制代码

这一路看下来,原理看的明明白白,是不是也没什么特别的,都是 runtime 的知识。但是这个思路确实是 666。

04.3. 问题在哪?

这不挺好的吗?别人也分析过这个框架的源码,我再写一遍有什么意义?

这问题挺好,确实是这样,如果一切顺利,我这篇文章没有存在的意义。接下来看一下问题出现在哪?

看到我刚才的注释了吗?这个笑脸很灿烂。如果项目不大,比如项目只有几百个类,这些都没有问题的,但是我们项目有接近 30000 个类,没错,是三万。我们使用注入的地方有几十上百处,两套 for 循环算下来是一个百万级别的。而且 objc_getClassList 这个方法是非常耗时的而且没有缓存。

// 获取项目中所有的类 :sob::sob::sob:.
// 遍历所有的类 :sob::sob::sob:.
复制代码

在贝聊项目上,这个方法在我的 iPhone 6s Plus 上要耗时一秒,在更老的 iPhone 6 上耗时要 3 秒,iPhone 5 可以想象要更久。而且随着项目迭代,项目中的类会越来越多, 这个耗时也会越来越长。

这个耗时是 pre-main 耗时,就是用户看那个白屏启动图的时候在做这个操作,严重影响用户体验。我们的产品就因为这个点导致闪屏广告展示出现问题,直接影响业务。

05. 解决方案

从上面的分析可以知道,导致耗时的原因就是原框架获取所有的类进行遍历。其实这是一个自动化的牛逼思路,这也是这个框架高于前面两个框架的核心原因。但是因为项目规模的原因导致这个点成为了实践中的短板,这也是作者始料未及的。

那我们怎么优化这个点呢?因为要注入方法的类没有做其他的标记,只能扫描所有的类,找到那些遵守了这个协议的再进行注入,这是要注入的类和注入行为的唯一联系点。从设计的角度来说,如果要主动实现注入,确实是这样的,没有更好方案来实现相同的功能。

但是有一个下策,能显著提高这部分性能,就是退回到上面两个框架所做的那样,让用户自己去标识哪些类需要注入。这样我把这些需要注入的类放到一个集合里,遍历注入,这样做性能是最好的。如果我从头设计一个方案,这也是不错的选择。

但是我现在做不了这些,我项目里有好几百个地方用了注入,如果我采用上面的方式,我要改好几百个地方。这样做很低效,而且我也不能保证我眼睛不会花出个错。我只能选择自动化去做这个事。

如果换个思路,我不主动注入,我懒加载,等你调用注入的方法我再执行注入操作呢?如果能实现这个,那问题就解决了。

[贝聊科技]一次高效的依赖注入
    1. 开始我们仍然在 +load 方法中做准备工作,和原有的实现一样,把所有的协议都存到链表中。
    1. __attribute__((constructor)) 中仍然做是否能执行注入的检查。
    1. 现在我们 hook NSObject+forwardTargetForSelector:-forwardTargetForSelector:
    1. 在 hook 中进行检查,如果该类有遵守了我们实现了注入的协议,那么就给该类注入容器中的方法。同时生成一个当前类的子类(实例)去响应当前的方法。
    1. 之后再调用该协议中的方法时,由于已经注入对应的方法,就直接响应消息,不会再走消息转发的路径了。

对了,代码和 demo 我放这里了,需要的可以下载看下。

06. 最后插播一个招聘广告

贝聊科技招聘 iOS 开发工程师 ,坐标广州。如果你想和我一起共事,机会不等人,赶紧动手吧!简历发到13246884282@163.com。


以上所述就是小编给大家介绍的《[贝聊科技]一次高效的依赖注入》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Hit Refresh

Hit Refresh

Satya Nadella、Greg Shaw / HarperBusiness / 2017-9-26 / USD 20.37

Hit Refresh is about individual change, about the transformation happening inside of Microsoft and the technology that will soon impact all of our lives—the arrival of the most exciting and disruptive......一起来看看 《Hit Refresh》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

html转js在线工具
html转js在线工具

html转js在线工具