做者介绍:NewPan,贝聊科技高级 iOS 工程师。html
文章涉及依赖注入方案基于
EXTConcreteProtocol
实现,GitHub连接在这里。laravel
若是基于 Cocopods 和 Git Submodules 来作组件化的时候,咱们的依赖关系是这样的:git
这里依赖路径有两条:github
这种单向的依赖关系,决定了从组件到项目的通信是单向的,即主项目能够主动向组件发起通信,可是组件却没有办法主动和主项目通信。缓存
你可能说不对,能够发通知啊?是的,是能够发通知,可是这一点都不优雅,也很差维护和拓展。bash
有没有一种更加优雅、更加方便平常开发的拓展和维护的方式呢?答案是有的,名字叫作“依赖注入”。app
依赖注入有另一个名字,叫作“控制反转”,像上面的组件化的例子,主项目依赖组件,如今有一个需求,组件须要依赖主项目,这种状况就叫作“控制反转”。框架
能把这部分“控制反转”的代码统一块儿来解耦维护,方便往后拓展和维护的服务,咱们就能够叫作依赖注入。jsp
因此依赖注入有两个比较重要的点:函数
不是我自身的,倒是我须要的,都是我所依赖的。一切须要外部提供的,都是须要进行依赖注入的。
这句话出自这篇文章:理解依赖注入与控制反转 | Laravel China 社区 - 高品质的 Laravel 开发者社区
若是对概念性的东西有更加深刻的理解,欢迎谷歌搜索“依赖注入”。
iOS 平台实现依赖注入功能的开源项目有两个大头:
详细对比发现这两个框架都是严格遵循依赖注入的概念来实现的,并无将 Objective-C 的 runtime 特性发挥到极致,因此使用起来很麻烦。
还有一点,这两个框架使用继承的方式实现注入功能,对项目的侵入性不容小视。若是你以为这个侵入性不算什么,那等到你项目大到必定程度,发现以前选择的技术方案有考虑不周,你想切换到其余方案的时候,你必定会后悔当时没选择那个不侵入项目的方案。
那有没有其余没那么方案呢?
GitHub - jspahrsummers/libextobjc: A Cocoa library to extend the Objective-C programming language. 里有一个 EXTConcreteProtocol
虽然没有直接叫作依赖注入,而是叫作混合协议,可是充分使用了 OC 动态语言的特性,不侵入项目,高度自动化,框架十分轻量,使用很是简单。
轻量到什么地步?就只有一个 .h
一个 .m
文件。 简单到什么地步?就只须要一个 @conreteprotocol
关键字,你就已经注入好了。
从一个评价开源框架的方方面面都甩开上面两个框架好几条街。
可是他也有致命的缺点,鱼和熊掌不可兼得,这个咱们等会说。
有两个比较重要的概念须要提早明白才能继续往下将。
__attribute__()
这是一个 GNU 编译器语法,被 constructor
这个关键字修饰的方法会在全部类的 +load
方法以后,在 main
函数以前被调用。详见:Clang Attributes 黑魔法小记 · sunnyxx的技术博客如上图,用一句话来描述注入的过程:将待注入的容器中的方法在 load
方法以后 main
函数以前注入指定的类中。
比方说有一个协议 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
复制代码
是否是很神奇?想不想探一下究竟?
先来看一下头文件:
#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);
});
// 获取项目中全部的类 😭😭😭.
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;
// 遍历全部的类 😭😭😭.
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。
这不挺好的吗?别人也分析过这个框架的源码,我再写一遍有什么意义?
这问题挺好,确实是这样,若是一切顺利,我这篇文章没有存在的意义。接下来看一下问题出如今哪?
看到我刚才的注释了吗?这个笑脸很灿烂。若是项目不大,好比项目只有几百个类,这些都没有问题的,可是咱们项目有接近 30000 个类,没错,是三万。咱们使用注入的地方有几十上百处,两套 for 循环算下来是一个百万级别的。并且 objc_getClassList
这个方法是很是耗时的并且没有缓存。
// 获取项目中全部的类 😭😭😭.
// 遍历全部的类 😭😭😭.
复制代码
在贝聊项目上,这个方法在个人 iPhone 6s Plus 上要耗时一秒,在更老的 iPhone 6 上耗时要 3 秒,iPhone 5 能够想象要更久。并且随着项目迭代,项目中的类会愈来愈多, 这个耗时也会愈来愈长。
这个耗时是 pre-main 耗时,就是用户看那个白屏启动图的时候在作这个操做,严重影响用户体验。咱们的产品就由于这个点致使闪屏广告展现出现问题,直接影响业务。
从上面的分析能够知道,致使耗时的缘由就是原框架获取全部的类进行遍历。其实这是一个自动化的牛逼思路,这也是这个框架高于前面两个框架的核心缘由。可是由于项目规模的缘由致使这个点成为了实践中的短板,这也是做者始料未及的。
那咱们怎么优化这个点呢?由于要注入方法的类没有作其余的标记,只能扫描全部的类,找到那些遵照了这个协议的再进行注入,这是要注入的类和注入行为的惟一联系点。从设计的角度来讲,若是要主动实现注入,确实是这样的,没有更好方案来实现相同的功能。
可是有一个下策,能显著提升这部分性能,就是退回到上面两个框架所作的那样,让用户本身去标识哪些类须要注入。这样我把这些须要注入的类放到一个集合里,遍历注入,这样作性能是最好的。若是我从头设计一个方案,这也是不错的选择。
可是我如今作不了这些,我项目里有好几百个地方用了注入,若是我采用上面的方式,我要改好几百个地方。这样作很低效,并且我也不能保证我眼睛不会花出个错。我只能选择自动化去作这个事。
若是换个思路,我不主动注入,我懒加载,等你调用注入的方法我再执行注入操做呢?若是能实现这个,那问题就解决了。
+load
方法中作准备工做,和原有的实现同样,把全部的协议都存到链表中。__attribute__((constructor))
中仍然作是否能执行注入的检查。NSObject
的 +resolveInstanceMethod:
和 +resolveClassMethod:
。贝聊科技招聘 iOS 开发工程师,坐标广州。若是你想和我一块儿共事,机会不等人,赶忙动手吧!简历发到 13246884282@163.com。