Writing a Hybrid Framework that is easy to maintain, convenient to use and reliable in performance (I) — Idea Construction

“Write a Hybrid Framework that is easy to maintain, convenient to use and reliable in performance (II) — Plug-in”

“Write a Hybrid Framework that is easy to maintain, convenient to use and reliable in performance (III) — Configuration Plug-in”

“Write a Hybrid Framework that is easy to maintain, convenient to use and reliable in performance (IV) — Framework Construction”

preface

On the basis of realizing communication, we also realized the plug-in of native end functions. This article continues to analyze from the plug-in configuration as a starting point. The Native Cordova framework uses a configuration file in XML format to configure information about the plugin we wrote. This way in our actual application development center daily news leak out many disadvantages, we tend to forget that sometimes how plug-in configuration, so that each of us to develop a plugin to analysis corresponding relationship again, because after all, it is not as often as we write business code, time is long may forget something configuration. The second disadvantage is the configured plug-in. Once the plug-in is no longer used or the information such as the plug-in class name is changed, we have to go into XML for reconfiguration or add and delete operations. Of course, the configuration information of Cordova will not be deleted or affect the use of the framework, but that is not what we want, we want to remove the plugin, then there is no more related to it in the project.

In our normal development, the XML file to the configuration information should be the choice of most people finally, before that we can choose a json file, also can choose the file, can even choose to + load configuration, + load configuration, of course, we also consider whether you need the asynchronous loading will affect application startup performance and so on.

When I was thinking hard to find a better way to register plug-ins, it happened that the technical team of Meituan issued an article entitled “iOS App Cold Launch Governance: From the practice of Meituan Takeout” the day before yesterday. By chance, I read the fifth article in it, and inspiration came to me. So this plug-in configuration I choose another way, through macro, its advantage is simple to operate, look intuitive, not coupled with other, but also to solve the pain points mentioned above. Of course, this is just my own ideas, mainly in accordance with the idea of a design, there must be a lot of improper or better solutions, but also hope that the big guys can more guidance.

directory

  • Plug-in registration process
  • Common plug-ins are preloaded

I. Plug-in registration process

So let’s stop the analysis stage here, and then enter the main topic. Actually, there is not much code, mainly want to refine good ideas while building the wheel, and step on the shoulders of giants to constantly improve.

Before formally registering the plug-in, we first learn a knowledge, to be honest, this knowledge is difficult for us to meet in our work, but it can solve the problems we encounter in our work.

After the compiler to compile the code generated file called the target file, the file structure, it is the goal of the executable file, our program to run, then its executable file format by the operating system need to be able to understand, such as the ELF is executable file format, under Linux PE32 is under Windows executable file format, So for iOS Mach-O is its executable file format. In fact, there are a lot of sections in the Mack-O executable file, one of which is the data section we need to use, that is to say, the plug-in information we registered to store in the data section of the Mack-O executable file. The clang compiler provides a number of compiler functions, including the section() function, which provides the binary segment read and write capability to write compile-time constants to the data segment. We then use this capability to write the plug-in information into the data segment at compile time. Then we find a suitable node, and then we take out the registered plug-in information stored in the data segment to complete the registration process.

Based on this, the framework also encapsulates a macro for plug-in use:

#define SHRMWebPlugins "SHRMWebPlugins" #define SHRMWebPluginDATA(sectname) __attribute((used, section("__DATA,"#sectname" "))) #define SHRMRegisterWebPlugin(servicename,impl) \ class SHRMWebViewEngine; char * k##servicename##_service SHRMWebPluginDATA(SHRMWebPlugins) = "{ \""#servicename"\" : \""#impl"\"}";Copy the code

If you’ve seen the honeycomb code it’s probably familiar to you, but not everyone has, so just to elaborate on the macro explanation, __attribute((used, section(“__DATA,”# sectName “”))) And set its value to the string “{\””#servicename”\” : \””#impl”\”}”. The __attribute first argument, used, tells the compiler that the symbol I declare is reserved. The used modifier means that the function is not optimized under Release even if it is not referenced. Without this modifier, the Release environment linker removes segments that are not referenced. That’s certainly not what we want. The purpose of this macro is to write the Servicename and impL to the data segment at compile time via the section() function.

If the above is understood, then the rest of our use is easy. After writing the native plug-in, we just need to add @shrmregisterwebPlugin (), and do nothing else, no need to introduce header files, no need to parse configuration files, just need to add a line of code, paste the code:

#import "SHRMMsgCommand.h"
@interface SHRMFetchPlugin : NSObject
- (void)nativeFentch:(SHRMMsgCommand *)command;
@end

@SHRMRegisterWebPlugin(SHRMFetchPlugin, 0)
@implementation SHRMFetchPlugin
- (void)nativeFentch:(SHRMMsgCommand *)command {
    NSString *method = [command argumentAtIndex:0];
    NSString *url = [command argumentAtIndex:1];
    NSString *param = [command argumentAtIndex:2];
    NSLog(@"(% @) : % @, % @, % @",command.callbackId, method, url, param);
    [command.delegate sendPluginResult:@"fetch success" callbackId:command.callbackId];
}
@end
Copy the code

This is based on the fetch plugin we simulated in the previous article. In fact, if we want to register the FETCH plugin with our Hybird framework, we just need to write @shrmregisterwebPlugin (SHRMFetchPlugin, 0) in the plug-in. No profile, no profile, no profile.

How to use the registered plugin in the framework? With that in mind, let’s take a look at what I’m doing in the SHRMWebViewEngine.

- (instancetype)init {
    if (self = [super init]) {
        _webViewhandleFactory = [[SHRMWebViewHandleFactory alloc] initWithWebViewEngine:self];
        _webViewDelegate = [[SHRMWebViewDelegate alloc] initWithWebViewEngine:self];
        _webPluginAnnotation = [[SHRMWebPluginAnnotation alloc] initWithWebViewEngine:self];
        _pluginObject = [NSMutableDictionary dictionary];
        [self loadStartupPlugin];
    }
    return self;
}

- (void)loadStartupPlugin {
    [_webPluginAnnotation getAllRegisterPluginName];
}
Copy the code

I added the loadStartupPlugin method based on the previous article. In fact, the loading process of registered plug-ins is implemented in SHRMWebPluginAnnotation. Here is the code:

- (void)getAllRegisterPluginName {
    _dyld_register_func_for_add_image(dyld_callback);
}

static void dyld_callback(const struct mach_header *mhp, intptr_t vmaddr_slide) {
    NSArray<NSString *> *services = ReadConfiguration(SHRMWebPlugins,mhp);
    for (NSString *map inservices) { NSData *jsonData = [map dataUsingEncoding:NSUTF8StringEncoding]; NSError *error = nil; id json = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error]; // Omit some code... // Execute the registered plug-in processing... } } NSArray<NSString *>* ReadConfiguration(char *sectionName,const struct mach_header *mhp) { NSMutableArray *configs = [NSMutableArray array]; unsigned long size = 0;#ifndef __LP64__
    uintptr_t *memory = (uintptr_t*)getsectiondata(mhp, SEG_DATA, sectionName, &size);
#else
    const struct mach_header_64 *mhp64 = (const struct mach_header_64 *)mhp;
    uintptr_t *memory = (uintptr_t*)getsectiondata(mhp64, SEG_DATA, sectionName, &size);
#endif
    
    unsigned long counter = size/sizeof(void*);
    for(int idx = 0; idx < counter; ++idx){
        char *string = (char*)memory[idx];
        NSString *str = [NSString stringWithUTF8String:string];
        if(! str)continue;
        
        NSLog(@"config = %@", str);
        if(str) [configs addObject:str];
    }
    return configs;
}
Copy the code

The _dyLD_register_func_for_add_image function is the callback that is called when dyLD links symbols. Now I choose to call it manually at run time. Getsectiondata () gets the menory that we stored in the data section using the section() function above. So that completes the process.

Two, common plug-in preloading

There are some common or first-screen rendering requirements that we may need to deal with specifically, such as the title preloading, where the plugin object is initialized ahead of time, rather than when it is called. Based on this idea, when the plug-in is registered, besides the name of the plug-in, an additional onload parameter is added. If onload is 0, it is considered that the plug-in does not need to be initialized in advance, but can be initialized at the time of call. If it is 1, it is assumed that the plug-in needs to be initialized when the webView loads. Look at the code:

- (void)registerStartupPluginName:(NSString *)pluginName onload:(NSNumber *)onload {
    if ([onload boolValue]) {
        [self getCommandInstance:pluginName];
    }
}

- (id)getCommandInstance:(NSString*)pluginName {
    id obj = [_pluginObject objectForKey:[pluginName lowercaseString]];
    if(! obj) { obj = [[NSClassFromString(pluginName) alloc] init];if(obj ! = nil) { [_pluginObjectsetObject:obj forKey:[pluginName lowercaseString]];
        }else {
            NSLog(@"(pluginName: (%@) does not exist.", pluginName); }}return obj;
}
Copy the code

The pluginObject is a cached mutable dictionary of plug-in instances. Once cached, the plug-in is cached directly instead of having to initialize an instance each time it is called.

conclusion

So far, this is the third article of Hybrid framework. It takes a lot of energy to summarize or learn, so now, our Hybrid framework has the functions mentioned at the beginning in the native end:

  • 1. Plug-in (native end implementation, JS side is not yet implemented)
  • 2. Configurability (native side implementation, JS side has not been implemented)
  • 3. Front-end interface unification (not yet realized)
  • 4. Communication based on WKWebView (realized)
  • 5. Performance tuning (there are some optimizations, but this is a deep hole)

This is the end of the third chapter, and the native end is basically completed. At present, there are five classes plus one interface. In addition to optimization, native work will not be carried out in the future, and the main design work of the Hybird wheel will be the front-end direction.

The code has been uploaded to Github “WKJavaScriptBridge”, welcome issue, welcome star.

reference

Mach-O LoadCommand

“IOS App Cold Launch Governance: Practice from Meituan Takeaway”

BeeHive: An Elegant but still improving Decoupling Framework