一次高效的依赖注入

作者:NewPan

链接:https://www.jianshu.com/p/777ec5edbac9

文章涉及依赖注入方案基于 EXTConcreteProtocol 实现,GitHub链接在这里https://github.com/jspahrsummers/libextobjc。

01、问题场景

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

这里依赖路径有两条:

1、最简单的主项目依赖第三方 pods。

2、组件依赖第三方 pods,主项目再依赖组件。

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

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

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

02、依赖注入

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

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

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

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

第二,要解耦。

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

这句话出自这篇文章:理解依赖注入与控制反转 | Laravel China 社区 - 高品质的 Laravel 开发者社区https://laravel-china.org/topics/2104/understanding-dependency-injection-and-inversion-of-control

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

03、iOS 依赖注入调查

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

1、objectionGitHub - atomicobject/objection: A lightweight dependency injection framework for Objective-C

2、TyphoonGitHub - appsquickly/Typhoon: Powerful dependency injection for iOS & OSX (Objective-C & Swift)

详细对比发现这两个框架都是严格遵循依赖注入的概念来实现的,并没有将 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)来装,而装这些方法的器皿就统称为容器。

2、__attribute__()这是一个 GNU 编译器语法,被 constructor 这个关键字修饰的方法会在所有类的 +load 方法之后,在 main 函数之前被调用。详见:Clang Attributes 黑魔法小记 · sunnyxx的技术博客

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

04.1. EXTConcreteProtocol 的使用

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

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

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

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

04.2. 源码解析

先来看一下头文件:

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

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

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

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

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

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

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

04.3. 问题在哪?

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

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

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

// 获取项目中所有的类 .

// 遍历所有的类 .

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

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

05、解决方案

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

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

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

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

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

1、开始我们仍然在 +load 方法中做准备工作,和原有的实现一样,把所有的协议都存到链表中。

2、在 __attribute__((constructor)) 中仍然做是否能执行注入的检查。

3、现在我们 hook NSObject 的 +resolveInstanceMethod: 和 +resolveClassMethod: 。

4、在 hook 中进行检查,如果该类有遵守了我们实现了注入的协议,那么就给该类注入容器中的方法。

对了,代码和 demo 我放这里了,需要的可以下载看下https://github.com/newyjp/BLMethodInjecting。

●编号327,输入编号直达本文

●输入m获取文章目录

推荐↓↓↓

Web开发

更多推荐《25个技术类微信公众号》

涵盖:程序人生、算法与数据结构、黑客技术与网络安全、大数据技术、前端开发、Java、Python、Web开发、安卓开发、iOS开发、C/C++、.NET、Linux、数据库、运维等。

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20181112B0LENK00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券