依赖注入

一、简介

依赖注入可以通过初始化方法(或构造函数)传递所需要的参数,或者通过属性(setter)传递。这里将对这两种方法进行讲解。

初始化方法注入:

- (instancetype)initWithDependency1:(Dependency1 *)d1 dependency2:(Dependency2 *)d2;

属性注入:

@property (nonatomic, retain) Dependency1 * dependency1;
@property (nonatomic, retain) Dependency2 * dependency2;

一般更趋向于初始化注入,如果在初始化(构造函数)的时候没办法进行注入,才通过属性进行注入。在通过初始化注入的情况下,这些依赖可能仍然需要作为属性存在,但是这些属性应该被设置为只读(readonly)的。

二、为什么使用依赖注入

依赖注入的几个重要之处:

①、明确的定义。使用依赖注入很明确的告诉了使用者要操作这个类对象需要做什么事情,初始化哪些变量,并且可以隐藏一些危险的依赖,如全局变量;

②、使用构成。 即一直坚持的多构成少继承原则。它能提高代码的可重用性;

③、更简单的自定义。在创建一个对象的时候通过传入指定的参数,更容易自定义。

④、明确的所有者。

⑤、可测试性。因为只需根据初始化方法,传入需要的参数即可进行操作,不需要去管理被隐藏的依赖。

三、使用依赖注入

3.1 注入类的类型

首先,将类(Class)的分为两种类型(type):简单的类和复杂的类。

简单的类是一个没有任何依赖或者只是依赖于其他简单的类,这个简单类是不可能被子类化的,因为它们的功能是很明确的并且不可变的,也没有引用其他额外的资源。在 Cocoa 框架中就有很多简单类,如:NSString、NSArray、NSDictionary、NSNumber 等。

复杂类则相反。它们有其他复杂的依赖,包括应用程序等级逻辑(根据应用程序的逻辑不同可能改变)。又或者它们需要访问其他外部的资源,如硬盘、网络或全局变量。这些类在你的应用程序中将变得很复杂,它们可能包含所有的控制器对象或所有的 model对象。Cocoa 框架中的复杂类有:NSURLConnection、UIViewController 等。

分类后,我们就可以很容易的在应用程序中选出哪些是复杂类了,然后开始对它们进行优化。

3.2 在初始化时依赖分配

原始代码:

@interface RCRaceCar ()
@property (nonatomic, readonly) RCEngine * engine;
@end

@implementation RCRaceCar

- (instancetype)init
{
     if (self = [super init]) { 
         ...
         _engine = [[RCEngine alloc] init];
     }
     return self;
}

@end

使用依赖注入改版后:

@interface RCRaceCar ()
@property (nonatomic, readonly) RCEngine * engine;
@end

@implementation RCRaceCar

- (instancetype)initWithEngine:(RCEngine *)engine
{
     if (self = [super init]) {

         ...
         _engine = engine;
     }
     return self;
}

@end

3.3 延迟初始化依赖

通常,有一些对象是初始化之后才需要用到的,甚至有时可能几乎用不到,如在用户的一个收藏列表中,当一个收藏都没有的时候显示一个和谐的页面,但这种情况有时很少遇到,因为只要用户收藏了一个资源,这个页面就不需要了。如下面以灭火器为例子:

@implementation RCRaceCar

- (void)recoverFromCrash
{
     if (self.fire != nil) {
          RCFireExtinguisher * fireExtinguisher = [[RCFireExtinguisher alloc] init];
          [fireExtinguisher extinguishFire:self.fire];
     }
}

@end

在这种情况,汽车当然是希望永远都没事,所以我们可能永远不需要灭火器。因为用到这个灭火器对象的几率很低,我们不想使得每一辆车创建得缓慢直接通过初始化方法创建它。或者,如果我们的汽车需要为多次车祸去恢复,这将需要创建多个灭火器。这种情况,我们可以使用一个工厂方法。

工厂方法是一个标准的 Objective-C 的 block,它要求没有参数并且返回一个具体的实例对象。当一个对象依赖使用这个 block 创建时它不需要知道它具体是怎样被创建的。

下面,通过一个工厂,使用依赖注入创建一个灭火器。

typedef RCFireExtinguisher *(^RCFireExtinguisherFactory)();

@interface RCRaceCar ()
@property (nonatomic, readonly) RCEngine * engine;
@property (nonatomic, copy, readonly) RCFireExtinguisherFactory fireExtinguisherFactory;
@end

@implementation RCRaceCar

- (instancetype)initWithEngine:(RCEngine *)engine fireExtinguisherFactory:(RCFireExtinguisherFactory)extFactory
{
     if (self = [super init]) { 

          ...
          _engine = engine;  
          _fireExtinguisherFactory = [extFactory copy];   
     }
     return self;
}

- (void)recoverFromCrash
{ 
     if (self.fire != nil) {     
          RCFireExtinguisher * fireExtinguisher = self.fireExtinguisherFactory();     
          [fireExtinguisher extinguishFire:self.fire];   
     }
}

@end

工厂在下面的情况下也很有用。当我们需要去创建一个不知道数量的依赖,甚至它是知道初始化之后才被创建的。如下:

@implementation RCRaceCar

- (instancetype)initWithEngine:(RCEngine *)engine transmission:(RCTransmission *)transmission wheelFactory:(RCWheel *(^)())wheelFactory;
{
     if (self = [super init]) {      
 
          _engine = engine;   
          _transmission    = transmission;   
          _leftFrontWheel  = wheelFactory();   
          _leftRearWheel   = wheelFactory();   
          _rightFrontWheel = wheelFactory();  
          _rightRearWheel  = wheelFactory();   // 保留轮子工厂,之后还需要一个备胎。   
          _wheelFactory    = [wheelFactory copy];   
     }
     return self;
}

@end

四、避免多余的配置

如果一个对象不应该在其他对象内部进行配置,那就用便利构造器(如 +[NSDictionary dictionary])。我们将把配置从我们的对象图中移出到我们普通的对象,分开它们使得代码更整洁,可测试,业务逻辑更清晰。

在添加一个便利构造器时,应该先确保是否是必须的。如果一个对象在 init 方法中只有几个参数,并且这些参数没有确切的默认值,那添加一个便利构造器是没必要的并且调用者应该直接使用标准的 init 方法。

为了配置我们的对象,将从 4 个点来收集我们的依赖:

①、没有一个确切的默认值。 包括 boolean 值或 number 值,他们可能根据在不同实例变量中的值各不相同。所以这些值应该作为参数传递到便利构造器中;

②、存在共享对象。 这个也需要作为参数传递到便利构造器中(比如一个无线电频率)。这些对象之前可能已经作为单例或通过父类指针被赋值;

③、被新创建的对象。 如果一个对象没有把这个依赖分享给其他对象,那其他对象(同一个类)应该在遍历构造器内创建一个新的依赖对象。

④、系统单例。 Cocoa 内提供的单例是可以直接被访问的,比如文件管理者单例 [NSFileManager defaultManager], 这里很明确在你的应用程序中只有一个实例将会被使用。

下面是关于赛车的简单初便利构造器

+ (instancetype)raceCarWithPitRadioFrequency:(RCRadioFrequency *)frequency
{
     RCEngine * engine = [[RCEngine alloc] init];   
     RCTransmission * transmission = [[RCTransmission alloc] init];   

     RCWheel *(^wheelFactory)() = ^{      
          return [[RCWheel alloc] init];   
     };   
     return [[self alloc] initWithEngine:engine
                            transmission:transmission 
                       pitRadioFrequency:frequency
                            wheelFactory:wheelFactory];
}

便利构造器应该放置在一个更加适合的地方与类分离。通常情况下都是放置在相同的 *.m 文件中,但是当指定通过如 Foo 对象配置的时候就应该将它放置在 @interface RaceCar(FooConfiguration) 这个 category 中,并且命名为类似 fooRaceCar 之类的。

五、系统单例

在 Cocoa 中有许多对象只有一个实例存在,如 [UIApplication sharedApplication]、[NSFileManager defaultManager]、[NSUserDefaults standardUserDefaults] 和 [UIDevice currentDevice] 等。如果一个对象依赖于这些对象中的一个,那就应该被作为参数包含进来。即使在你的应用程序中只有这样一个实例。在你的测试中可能想要模拟实例或在测试前创建一个实例来避免测试依赖。

这里建议避免在你的代码中创建全局的单例,而是在一个对象中创建一个单一的实例,当它第一次被使用时,将它注入到其他依赖它的对象中去。

六、不可修改的构造函数

有些时候,一个类的初始化方法/构造方法不能被修改或不能被直接调用。在这种情况下,你需要使用 setter 注入。如下代码:

// 一个我们不能直接调用初始化方法的例子。
RCRaceTrack * raceTrack = [objectYouCantModify createRaceTrack];
// 我们仍然可以使用属性来配置我们的赛车路径
raceTrack.width = 10;
raceTrack.numberOfHairpinTurns = 2;

setter 注入允许你配置这个对象,但是它引入了额外的可变性使得在这个类的设计中必须做额外的测试和处理。幸运的是,这里有两种主要的场景导致无法访问和修改初始化方法,并且这两种情况都是可以避免的。

七、类注册

“类注册” 工厂模式的使用意味着对象不能修改它们的初始化方法。见代码:

NSArray * raceCarClasses = @[ [RCFastRaceCar class], [RCSlowRaceCar class] ];
NSMutableArray *raceCars = [[NSMutableArray alloc] init];

for (Class raceCarClass in raceCarClasses) {   
     // 所有赛车必须有相同的初始化方法 (在这个例子中是 "init" 方法).
     // 这里意味着我们不能自定义不同的子类
     [raceCars addObject:[[raceCarClass alloc] init]];
}

// 一个简单的替换方法是:使用工厂 block 来代替

typedef RCRaceCar *(^RCRaceCarFactory)();

NSArray * raceCarFactories = @[ ^{ return [[RCFastRaceCar alloc] initWithTopSpeed:200]; }, 
                                ^{ return [[RCSlowRaceCar alloc] initWithLeatherPlushiness:11]; } ];
NSMutableArray *  raceCars = [[NSMutableArray alloc] init];

for (RCRaceCarFactory raceCarFactory in raceCarFactories) {
    // 现在这样,我们就不用关心到底是那个初始化方法被调用了
    [raceCars addObject:raceCarFactory()];
}

八、Storyboards

Storyboards 提供了很方便的方法来构建界面,但是在依赖注入中它也带来了问题。 特别是当在 Storyboard 中实例化一个初始化的视图控制器,它不允许你选择调用哪一个初始化方法。 类似的,当在 storyboard 中定义一个 segue 时,目标控制器在实例化时也不能让你指定调用那个初始化方法。

解决方法是避免使用 storyboard。这看起来是一种极端的解决方案,但是我们发现在大型团队开发中, storyboard 带来了其他问题。另外,不适用 storyboard 并没有丢掉它的所有好处,除了 storyboard 提供的 segues 外,xib 也提供了和 storyboard 相同的好处,而且 xib 可以让你自定义初始化方法。

九、公有和私有

依赖注入鼓励在你的共有接口上暴露出更多的是对象。正如上面提到的,这有很多好处。但是当在构建框架时,它使你的共有 API变得臃肿。使用依赖注入之前,公有对象 A 已经使用私有对象 B(对象 B 反过来使用私有对象 C),但是对象 B 和对象 C 从来没有从框架中暴露。通过依赖注入,对象 A 在它的公有初始化方法中有对象 B ,而对象 B 反过来使得对象 C 在它的初始化方法中公开。

// In public ObjectA.h.
@interface ObjectA

// 因为初始化方法使用了对象 B 的引用,所以我们需要在使用对象 B 之前引入它的头文件
- (instancetype)initWithObjectB:(ObjectB *)objectB;
@end

@interface ObjectB

// 这里也一样:需要暴露 ObjectC.h
- (instancetype)initWithObjectC:(ObjectC *)objectC;
@end

@interface ObjectC
- (instancetype)init;
@end

对象 B 和对象 C 都是具体的实现,而你不想让框架的使用者去关心它们。这时可以通过协议(protocol)来解决。

@interface ObjectA
- (instancetype)initWithObjectB:(id <ObjectB>)objectB;
@end

// 这个协议只暴露 ObjectA 需要原始的 ObjectB。
// 我们并不是在具体的 ObjectB(或 ObjectC)实现创建一个硬依赖 

@protocol ObjectB
- (void)methodNeededByObjectA;
@end

十、一次高效的依赖注入

10.1 问题场景

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

这里依赖路径有两条:

①、主项目依赖第三方 pods。

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

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

可以用通知来与主项目通讯,但是这一点都不优雅,也不好维护和拓展。更加优雅、更加方便日常开发的拓展和维护的方式,叫做“依赖注入”。

10.2 依赖注入

依赖注入有另外一个名字,叫做“控制反转”。

像上面的组件化的例子,主项目依赖组件,现在有一个需求,组件需要依赖主项目,这种情况就叫做“控制反转”。能把这部分“控制反转”的代码统一起来解耦维护,方便日后拓展和维护的服务,我们就可以叫做依赖注入。

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

①、要实现这种反转控制的功能。

②、要解耦。

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

出自这篇文章:理解依赖注入与控制反转

10.3 iOS 依赖注入调查

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

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

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

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

libextobjc 里有一个 EXTConcreteProtocol 虽然没有直接叫做依赖注入,而是叫做混合协议。它的优点:

①、充分使用了 OC 动态语言的特性,不侵入项目

②、高度自动化

③、框架十分轻量(只有一个 .h 和 .m 文件)

④、使用非常简单(只需要一个 @conreteprotocol 关键字就已经注入好了)。

10.4 EXTConcreteProtocol 实现原理

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

①、容器。这里的容器是指,我们注入的方法需要有类(class)来装,而装这些方法的器皿就统称为容器。

②、_attribute__() 这是一个 GNU 编译器语法,被 constructor 这个关键字修饰的方法会在所有类的 +load 方法之后,在 main 函数之前被调用。

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

10.4.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
10.4.2 源码解析

先来看一下头文件:

#define concreteprotocol(NAME) \
    /*
     * create a class used to contain all the methods used in this protocol
     */ \
     // 定义一个容器类
    interface NAME ## _ProtocolMethodContainer : NSObject < NAME > {} \
    @end \
    \
    @implementation NAME ## _ProtocolMethodContainer \
    /*
     * when this class is loaded into the runtime, add the concrete protocol
     * into the list we have of them
     */ \
    // load 方法添加混合协议
    + (void)load { \
        /*
         * passes the actual protocol as the first parameter, then this class as
         * the second
         */ \
        if (!ext_addConcreteProtocol(objc_getProtocol(metamacro_stringify(NAME)), self)) \
            fprintf(stderr, "ERROR: Could not load concrete protocol %s\n", metamacro_stringify(NAME)); \
    } \
    \
    /*
     * using the "constructor" function attribute, we can ensure that this
     * function is executed only AFTER all the Objective-C runtime setup (i.e.,
     * after all +load methods have been executed)
     */ \
    // 在 load 之后,main 之前执行方法注入
    __attribute__((constructor)) \
    static void ext_ ## NAME ## _inject (void) { \
        /*
         * use this injection point to mark this concrete protocol as ready for
         * loading
         */ \
        ext_loadConcreteProtocol(objc_getProtocol(metamacro_stringify(NAME))); \
    }

/*** implementation details follow ***/
// load 方法添加混合协议
BOOL ext_addConcreteProtocol (Protocol *protocol, Class methodContainer);
// load 之后,main 之前执行方法注入
void ext_loadConcreteProtocol (Protocol *protocol);

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

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

// contains the information needed to reference a full special protocol
typedef struct {
    // the actual protocol declaration (@protocol block)  用户定义的协议.
    __unsafe_unretained Protocol *protocol;

    // the injection block associated with this protocol
    //
    // this block is RETAINED and must eventually be released by transferring it
    // back to ARC
    // 在 __attribute__((constructor)) 时往指定类里注入方法的 block.
    void *injectionBlock;

    // whether this protocol is ready to be injected to its conforming classes
    //
    // this does NOT refer to a special protocol having been injected already
    // 对应的协议是否已经准备好注入.
    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);
        
        // lock the mutex to prevent accesses from other threads while we perform
        // this work    加锁
        if (pthread_mutex_lock(&specialProtocolsLock) != 0) {
            fprintf(stderr, "ERROR: Could not synchronize on special protocol data\n");
            return NO;
        }
        
        // if we've hit the hard maximum for number of special protocols, we can't
        // continue
        if (specialProtocolCount == SIZE_MAX) {
            pthread_mutex_unlock(&specialProtocolsLock);
            return NO;
        }

        // if the array has no more space, we will need to allocate additional
        // entries
        // specialProtocols 是一个链表,每个协议都会被组织成为一个 EXTSpecialProtocol,这个 specialProtocols 里存放了了这些 specialProtocols.
        if (specialProtocolCount >= specialProtocolCapacity) {
            size_t newCapacity;
            if (specialProtocolCapacity == 0)
                // if there are no entries, make space for just one
                newCapacity = 1;
            else {
                // otherwise, double the current capacity
                newCapacity = specialProtocolCapacity << 1;

                // if the new capacity is less than the current capacity, that's
                // unsigned integer overflow
                if (newCapacity < specialProtocolCapacity) {
                    // set it to the maximum possible instead
                    newCapacity = SIZE_MAX;

                    // if the new capacity is still not greater than the current
                    // (for instance, if it was already SIZE_MAX), we can't continue
                    if (newCapacity <= specialProtocolCapacity) {
                        pthread_mutex_unlock(&specialProtocolsLock);
                        return NO;
                    }
                }
            }

            // we have a new capacity, so resize the list of all special protocols
            // to add the new entries
            void * restrict ptr = realloc(specialProtocols, sizeof(*specialProtocols) * newCapacity);
            if (!ptr) {
                // the allocation failed, abort
                pthread_mutex_unlock(&specialProtocolsLock);
                return NO;
            }

            specialProtocols = ptr;
            specialProtocolCapacity = newCapacity;
        }

        // at this point, there absolutely must be at least one empty entry in the
        // array
        assert(specialProtocolCount < specialProtocolCapacity);

        // disable warning about "leaking" this block, which is released in
        // ext_injectSpecialProtocols()
        #ifndef __clang_analyzer__
        ext_specialProtocolInjectionBlock copiedBlock = [injectionBehavior copy];

        // construct a new EXTSpecialProtocol structure and add it to the first
        // empty space in the array
        // 将协议保存为一个 EXTSpecialProtocol 结构体。
        specialProtocols[specialProtocolCount] = (EXTSpecialProtocol){
            .protocol = protocol,
            .injectionBlock = (__bridge_retained void *)copiedBlock,
            .ready = NO
        };
        #endif

        ++specialProtocolCount;
        pthread_mutex_unlock(&specialProtocolsLock);
    }

    // success!
    return YES;
}

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

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

static void ext_injectConcreteProtocol (Protocol *protocol, Class containerClass, Class class) {
    // get the full list of instance methods implemented by the concrete
    // protocol   获取容器类里所有的实例方法.
    unsigned imethodCount = 0;
    Method *imethodList = class_copyMethodList(containerClass, &imethodCount);

    // get the full list of class methods implemented by the concrete
    // protocol   获取容器类里所有的类方法方法.
    unsigned cmethodCount = 0;
    Method *cmethodList = class_copyMethodList(object_getClass(containerClass), &cmethodCount);
            
    // get the metaclass of this class (the object on which class
    // methods are implemented)  拿到要注入方法的类的元类.
    Class metaclass = object_getClass(class);

    // inject all instance methods in the concrete protocol   注入实例方法.
    for (unsigned methodIndex = 0;methodIndex < imethodCount;++methodIndex) {
        Method method = imethodList[methodIndex];
        SEL selector = method_getName(method);

        // first, check to see if such an instance method already exists
        // (on this class or on a superclass)
        // 如果该类已经实现了这个方法,就跳过注入,不至于覆盖用户自定义的实现.
        if (class_getInstanceMethod(class, selector)) {
            // it does exist, so don't overwrite it
            continue;
        }

        // add this instance method to the class in question
        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 %sn",
                sel_getName(selector), protocol_getName(protocol), class_getName(class));
        }
    }

    // inject all class methods in the concrete protocol   注入类方法.
    for (unsigned methodIndex = 0;methodIndex < cmethodCount;++methodIndex) {
        Method method = cmethodList[methodIndex];
        SEL selector = method_getName(method);

        // +initialize is a special case that should never be copied
        // into a class, as it performs initialization for the concrete
        // protocol
        // +initialize 不能被注入.
        if (selector == @selector(initialize)) {
            // so just continue looking through the rest of the methods
            continue;
        }

        // first, check to see if a class method already exists (on this
        // class or on a superclass)
        //
        // since 'class' is considered to be an instance of 'metaclass',
        // this is actually checking for class methods (despite the
        // function name)
        // 如果该类已经实现了这个方法,就跳过注入,不至于覆盖用户自定义的实现.
        if (class_getInstanceMethod(metaclass, selector)) {
            // it does exist, so don't overwrite it
            continue;
        }

        // add this class method to the metaclass in question
        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 %sn",
                sel_getName(selector), protocol_getName(protocol), class_getName(class));
        }
    }

    // free the instance method list  管理内存
    free(imethodList); imethodList = NULL;

    // free the class method list
    free(cmethodList); cmethodList = NULL;

    // use [containerClass class] and discard the result to call +initialize
    // on containerClass if it hasn't been called yet
    //
    // this is to allow the concrete protocol to perform custom initialization
    // 允许用户在容器类里复写 +initialize 方法,这里调用是保证用户复写的实现能够被执行.
    (void)[containerClass class];
}

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

void ext_specialProtocolReadyForInjection (Protocol *protocol) {
    @autoreleasepool {
        NSCParameterAssert(protocol != nil);
        
        // lock the mutex to prevent accesses from other threads while we perform
        // this work   加锁
        if (pthread_mutex_lock(&specialProtocolsLock) != 0) {
            fprintf(stderr, "ERROR: Could not synchronize on special protocol data\n");
            return;
        }

        // loop through all the special protocols in our list, trying to find the
        // one associated with 'protocol'
        // 检查要对应的 protocol 是否已经加载进上面的链表中了,如果找到了,就将对应的 EXTSpecialProtocol 结构体的 ready 置为 YES.
        for (size_t i = 0;i < specialProtocolCount;++i) {
            if (specialProtocols[i].protocol == protocol) {
                // found the matching special protocol, check to see if it's
                // already ready
                if (!specialProtocols[i].ready) {
                    // if it's not, mark it as being ready now
                    specialProtocols[i].ready = YES;

                    // since this special protocol was in our array, and it was not
                    // loaded, the total number of protocols loaded must be less
                    // than the total count at this point in time
                    assert(specialProtocolsReady < specialProtocolCount);

                    // ... and then increment the total number of special protocols
                    // loaded – if it now matches the total count of special
                    // protocols, begin the injection process
                    if (++specialProtocolsReady == specialProtocolCount)
                        // 如果所有的 EXTSpecialProtocol 结构体都准备好了,就开始执行注入.
                        ext_injectSpecialProtocols();
                }

                break;
            }
        }

        pthread_mutex_unlock(&specialProtocolsLock);
    }
}

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

/**
 * This function actually performs the hard work of special protocol injection.
 * It obtains a full list of all classes registered with the Objective-C
 * runtime, finds those conforming to special protocols, and then runs the
 * injection blocks as appropriate.
 */
static void ext_injectSpecialProtocols (void) {
    /*
     * don't lock specialProtocolsLock in this function, as it is called only
     * from public functions which already perform the synchronization
     */
    
    /*
     * This will sort special protocols in the order they should be loaded. If
     * a special protocol conforms to another special protocol, the former
     * will be prioritized above the latter.
     */
    // 对协议进行排序.
    // 比方说 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 the pointers are equal, it must be the same protocol
        if (a == b)
            return 0;

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

        // A higher return value here means a higher priority
        int (^protocolInjectionPriority)(const EXTSpecialProtocol *) = ^(const EXTSpecialProtocol *specialProtocol){
            int runningTotal = 0;

            for (size_t i = 0;i < specialProtocolCount;++i) {
                // the pointer passed into this block is guaranteed to point
                // into the 'specialProtocols' array, so we can compare the
                // pointers directly for identity
                if (specialProtocol == specialProtocols + i)
                    continue;

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

            return runningTotal;
        };

        /*
         * This will return:
         * 0 if the protocols are equal in priority (such that load order does not matter)
         * < 0 if A is more important than B
         * > 0 if B is more important than A
         */
        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;
    }

	// use this instead of ext_copyClassList() to avoid sending +initialize to
	// classes that we don't plan to inject into (this avoids some SenTestingKit
	// timing issues)
	classCount = objc_getClassList(allClasses, classCount);

    /*
     * set up an autorelease pool in case any Cocoa classes get used during
     * the injection process or +initialize
     */
    @autoreleasepool {
        // loop through the special protocols, and apply each one to all the
        // classes in turn
        //
        // ORDER IS IMPORTANT HERE: protocols have to be injected to all classes in
        // the order in which they appear in specialProtocols. Consider classes
        // X and Y that implement protocols A and B, respectively. B needs to get
        // its implementation into Y before A gets into X.
        // 遍历所有的要注入的协议结构体.
        for (size_t i = 0;i < specialProtocolCount;++i) {
            Protocol *protocol = specialProtocols[i].protocol;
            
            // transfer ownership of the injection block to ARC and remove it
            // from the structure
            // 使用 __bridge_transfer 把对象的内存管理交给 ARC.
            ext_specialProtocolInjectionBlock injectionBlock = (__bridge_transfer id)specialProtocols[i].injectionBlock;
            specialProtocols[i].injectionBlock = NULL;

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

    // 管理内存.
    // free the allocated class list
    free(allClasses);

    // now that everything's injected, the special protocol list can also be
    // destroyed
    free(specialProtocols); specialProtocols = NULL;
    specialProtocolCount = 0;
    specialProtocolCapacity = 0;
    specialProtocolsReady = 0;
}

这一路看下来,原理看的明明白白,是不是也没什么特别的,都是 runtime 的知识。主要看思路。

10.4.3 问题在哪?

接下来讨论 EXTConcreteProtocol 的缺点。

如果项目不大,只有几百个类,这些都没有问题的,但是当项目有接近 30000 个类或者更多时。我们使用注入的地方有几十上百处,两套 for 循环算下来是一个百万级别的。而且 objc_getClassList 这个方法是非常耗时的而且没有缓存。

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

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

10.5 解决方案

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

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

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

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

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

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

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

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

https://github.com/newyjp/BLMethodInjecting

十一、结语

依赖注入在 Objective-C、Swift 中是很自然的存在。合理的使用它能让代码可读性更强,可测试性更好,可维护性更高。

十二、文章

依赖注入——让iOS代码更简洁

[iOS]一次高效的依赖注入

posted @ 2020-02-25 23:54  和风细羽  阅读(616)  评论(0编辑  收藏  举报