This article deals with dependency injection schemes based on EXTConcreteProtocol. GitHub links here.

01. Problem scenario

If we componentized based on Cocopods and Git Submodules, our dependencies would look like this:

There are two dependency paths:

    1. The simplest main project relies on third-party Pods.
    1. Components depend on third-party Pods, and the main project depends on components.

This one-way dependency determines that the communication from component to project is one-way, that is, the main project can actively initiate communication with the component, but the component cannot actively communicate with the main project.

You may say wrong, can send notice? Yes, notifications can be sent, but it’s not elegant, easy to maintain and expand.

Is there a more elegant, more convenient way to expand and maintain daily development? The answer is yes, and it’s called dependency injection.

02. Dependency injection

Dependency injection has another name, called “inversion of control”. As in the componentization example above, where the main project depends on the component, a situation where there is a requirement that the component depends on the main project is called “inversion of control”.

A service that unifies this “inversion of control” code and decouples it for future extension and maintenance is called dependency injection.

So there are two important points about dependency injection:

  • First, the function of reverse control should be realized.
  • Second, decouple.

Not mine, but what I need, what I depend on. Everything that needs to be externally provided requires dependency injection.

This sentence from the article: to understand the dependency injection and inversion of control | Laravel China community – high quality Laravel developer community

For a deeper understanding of the conceptual, Google “dependency injection”.

IOS dependency Injection survey

There are two major open source projects implementing dependency injection on iOS:

    1. objection GitHub – atomicobject/objection: A lightweight dependency injection framework for Objective-C
    1. Typhoon GitHub – appsquickly/Typhoon: Powerful dependency injection for iOS & OSX (Objective-C & Swift)

A detailed comparison shows that both frameworks are implemented strictly following the concept of dependency injection and do not take the Objective-C Runtime features to their fullest, making them cumbersome to use.

Also, both frameworks use inheritance to implement injection functionality, which is intrusive to the project. If this doesn’t seem intrusive to you, you’ll regret not choosing a non-intrusive solution when you get to the point where your project is big enough to realize that the technical solution you chose was ill-conceived and you want to switch to another solution.

Are there any other options that aren’t?

GitHub – jspahrsummers/libextobjc: A Cocoa library to extend the Objective-C programming language. There is an EXTConcreteProtocol, which is not directly called dependency injection, but is called a hybrid protocol, but fully uses the OC dynamic language features, non-intrusive project, highly automated, lightweight framework, and very simple to use.

How light? There is only one.h and one.m file. How simple? All you need is the @conreteProtocol keyword, and you’re done.

Every aspect of an open source framework is several blocks ahead of the two above.

But he also has a fatal flaw. You can’t have it both ways. We’ll talk about that later.

EXTConcreteProtocol implementation principle

There are two important concepts that need to be understood ahead of time in order to move forward.

    1. The container. By container, we mean that the methods we inject need to have classes to hold them, and the vessels that hold these methods are collectively called containers.
    1. __attribute__()This is a GNU compiler syntax, byconstructorThis keyword modifies methods in all classes+loadMethod after, inmainFunction is called before. See:Clang Attributes is the tech blog of Sunnyxx

As shown above, the injection process is described in one sentence: inject the methods in the container to be injected into the specified class after the load method and before the main function.

04.1. Use of EXTConcreteProtocol

Let’s say you have a protocol ObjectProtocol. We’ve implemented dependency injection just by writing it this way.

@protocol ObjectProtocol<NSObject>

+ (void)sayHello;

- (int)age;

@end

@concreteprotocol(ObjectProtocol)

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

- (int)age {
    return 18;
}

@end
Copy the code

Then, for example, a Person class that wants to own this injection method just needs to follow the protocol.

@interface Person : NSObject<ObjectProtocol>

@end
Copy the code

We can then call the injected method on Person.

int main(int argc, char * argv[]) {
     Person *p = [Person new];
	 NSLog(@ "% @", [p age]); [p.class sayHello]; } output: >>>18
>>>Hello
Copy the code

Isn’t that amazing? Want to take a look?

04.2. Source code analysis

Let’s start with the header file:

#define concreteprotocol(NAME) \
	  // Define a container class.
    interface NAME ## _ProtocolMethodContainer : NSObject < NAME > {} \
    @end \
    \
    @implementation NAME ## _ProtocolMethodContainer \
    // The load method adds a hybrid protocol.
    + (void)load { \
        if(! ext_addConcreteProtocol(objc_getProtocol(metamacro_stringify(NAME)), self)) \fprintf(stderr."ERROR: Could not load concrete protocol %s\n", metamacro_stringify(NAME)); \} \Method injection is performed after load and before main.
    __attribute__((constructor)) \
    static void ext_ ## NAME ## _inject (void) { \ ext_loadConcreteProtocol(objc_getProtocol(metamacro_stringify(NAME))); The \}// The load method adds a hybrid protocol.
BOOL ext_addConcreteProtocol (Protocol *protocol, Class methodContainer);
Method injection is performed after load and before main.
void ext_loadConcreteProtocol (Protocol *protocol);
Copy the code

The ConcreteProtocol macro defines a container class for our protocol, in which our main injected methods, such as +sayHello and -age, are defined.

The ext_addConcreteProtocol method is then called in the +load method.

Typepedef struct {// User-defined Protocol. __unsafe_unretained Protocol * Protocol; // Inject the method block into the specified class when __attribute__((constructor)). // Whether the corresponding protocol is ready for injection. 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 is a linked list, and each protocol is organized into an EXTSpecialProtocol, This specialProtocols store these specialProtocols. If (specialProtocolCount >= specialProtocolCapacity) {... } #ifndef __clang_analyzer__ ext_specialProtocolInjectionBlock copiedBlock = [injectionBehavior copy]; // Save the protocol as an EXTSpecialProtocol struct. specialProtocols[specialProtocolCount] = (EXTSpecialProtocol){. Protocol = protocol,  .injectionBlock = (__bridge_retained void *)copiedBlock, .ready = NO }; #endif ++specialProtocolCount; pthread_mutex_unlock(&specialProtocolsLock); } return YES; }Copy the code

Our ext_loadSpecialProtocol method passes in a block that calls the ext_injectConcreteProtocol method.

Ext_injectConcreteProtocol The ext_injectConcreteProtocol method takes three parameters. The second is the container class, which the framework added for us; The third parameter is the target injection class into which we want to inject the methods in the container.

static void ext_injectConcreteProtocol (Protocol *protocol, Class containerClass, Class class) {
    // Get all the instance methods in the container class.
    unsigned imethodCount = 0;
    Method *imethodList = class_copyMethodList(containerClass, &imethodCount);

    // Get all the class methods in the container class.
    unsigned cmethodCount = 0;
    Method *cmethodList = class_copyMethodList(object_getClass(containerClass), &cmethodCount);
            
    // Get the metaclass of the class to inject the method into.
    Class metaclass = object_getClass(class);

    // Inject instance methods.
    for (unsigned methodIndex = 0; methodIndex < imethodCount; ++methodIndex) { Method method = imethodList[methodIndex]; SEL selector = method_getName(method);// If the class already implements this method, injection is skipped so as not to overwrite the user-defined implementation.
        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)); }}// Inject class methods.
    for (unsigned methodIndex = 0; methodIndex < cmethodCount; ++methodIndex) { Method method = cmethodList[methodIndex]; SEL selector = method_getName(method);// +initialize cannot be injected.
        if (selector == @selector(initialize)) {
            continue;
        }

        // If the class already implements this method, injection is skipped so as not to overwrite the user-defined implementation.
        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)); }}// Manage memory
    free(imethodList); imethodList = NULL;
    free(cmethodList); cmethodList = NULL;

    // Allow the user to replicate the +initialize method in the container class.
    (void)[containerClass class];
}
Copy the code

Let’s look again at the ext_loadConcreteProtocol method called after +load and before main.

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

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

        // Check if the corresponding protocol is already loaded in the list above, and if so, set ready to YES for the corresponding EXTSpecialProtocol structure.
        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)
						   // If all EXTSpecialProtocol constructs are ready, injection starts.
                        ext_injectSpecialProtocols();
                }

                break; } } pthread_mutex_unlock(&specialProtocolsLock); }}Copy the code

That’s all you need to do to get ready for injection into the core method.

static void ext_injectSpecialProtocols (void) {
    // Sort the protocols.
	  // For example, protocol A inherits from protocol B, but the load method of the container class corresponding to protocol B is not necessarily executed first. So if the class method of protocol B is copied from the method of protocol A, then the method of protocol B should be injected instead of the implementation of the container method of protocol A.
	  // In order to ensure this sequence, we need to sort the protocol, A is inherited from B, so the sequence should be A before 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);
    });

	  // Get all the classes in the project 😭😭😭.
    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 {
        // Iterate through all the protocol constructs to be injected.
        for (size_t i = 0; i < specialProtocolCount; ++i) { Protocol *protocol = specialProtocols[i].protocol;// Use __bridge_transfer to hand over memory management of objects to ARC.
            ext_specialProtocolInjectionBlock injectionBlock = (__bridge_transfer id)specialProtocols[i].injectionBlock;
            specialProtocols[i].injectionBlock = NULL;

            // Iterate over all classes 😭😭😭.
            for (unsigned classIndex = 0; classIndex < classCount; ++classIndex) { Classclass = allClasses[classIndex];
                
                // If the class complies with the protocol for injection, then injection is performed.
				  // Note: continue is not break, because a class can inject methods for multiple protocols.
                if(! class_conformsToProtocol(class, protocol))continue; injectionBlock(class); }}}// Manage memory.
    free(allClasses);
    free(specialProtocols); specialProtocols = NULL;
    specialProtocolCount = 0;
    specialProtocolCapacity = 0;
    specialProtocolsReady = 0;
}
Copy the code

All the way down, the principle is clear, is there nothing special, is the knowledge of the Runtime. But the idea is 666.

04.3. What’s the problem?

Isn’t that good? Others have analyzed the source code of this framework, what’s the point of me writing it again?

That’s a good question. It is. If everything goes well, my article has no purpose. Now what’s the problem?

Did you see my comment? The smiling face is brilliant. If the project is small, like the project has a few hundred classes, that’s fine, but our project has close to 30,000 classes, yes, 30,000. We use injection in dozens or hundreds of places, and two for loops add up to a million. The objc_getClassList method is time-consuming and has no cache.

// Get all classes in the project 😭😭😭.Copy the code

On the Baychat project, this method took one second on my iPhone 6S Plus, three seconds on the older iPhone 6, and conceivably longer on the iPhone 5. And as the project iterates, there will be more and more classes in the project, which will take longer and longer.

This is the pre-main time. The operation is performed when the user is looking at the blank screen, which seriously affects the user experience. This point leads to problems in the display of flash screen advertisements of our products, which directly affects our business.

05. Solutions

As can be seen from the above analysis, the time-consuming reason is that the original framework obtains all the classes for traversal. In fact, this is a great idea of automation, which is the core reason why this framework is superior to the previous two frameworks. However, due to the scale of the project, this point became a short board in practice, which was also unexpected by the author.

So how do we optimize this point? Because the class to inject methods is not otherwise marked, you can only scan all classes to find those that comply with the protocol and inject again. This is the only point of contact between the class to be injected and the injection behavior. From a design perspective, if you want to implement injection proactively, which is true, there is no better way to implement the same functionality.

But there is a downside to significantly improving this part of performance, which is to fall back on what the two frameworks did and let the user identify which classes need to be injected. In this way, I put the classes that need to be injected into a collection and iterate over the injection, which gives the best performance. If I design a plan from scratch, this is also a good choice.

But I can’t do that right now. There are hundreds of places in my project that have injection, and if I did that, I would change hundreds of places. It’s inefficient, and I can’t guarantee I won’t make a mistake. I can only choose automation to do this.

What if, instead of injecting, I am lazy about loading, and wait until you call the injection method before I do the injection? If we can do that, then we’re done.

    1. In the beginning we were still there+loadMethod, as in the original implementation, stores all the protocols in the linked list.
    1. in__attribute__((constructor))Is still checked to see if the injection can be performed.
    1. Now we hookNSObject 的 +resolveInstanceMethod: 和 +resolveClassMethod: 。
    1. Check in the hook and inject methods in the container into the class if it complies with the protocol that we implemented the injection.

By the way, I put the code and demo here, you can download the need to see.

My collection of articles

The link below is a catalog of all my articles. These articles are generally concerned with implementation, each article has Github address, Github has source code.

Index of my collection of articles

You can also follow my own short book featureIOS Development Tips. The articles on this topic are solid and solid. If you have a question, in addition to leaving a comment at the end of the article, you can also leave a comment on Weibo@ hope _HKbuyLeave me a message and visit myGithub.

sponsorship

Your sponsorship makes my writing even more exciting!

Wechat sponsored scan code

Alipay sponsored scanning code