Original: Sohu technology products

preface

WWDC2014 apple opened the dynamic library on iOS, which gave us a lot of room to imagine.

Dynamic library is a kind of resource packaging method used in Cocoa/Cocoa Touch programs. It can gather code files, header files, resource files, and description documents together, which is convenient for developers to use. The dynamic library is not copied into the program’s executable (mach-o) at compile time, but is loaded when the program is running.

The feature that dynamic libraries are loaded at runtime also allows us to replace them whenever we want without having to recompile the code. So we can do a lot of things, such as plugins and dynamic updates: Github

  • Application plug-in

    The function to do more and more many application, the software is more and more bloated, if the software function modules can also like a lazy loading on demand load, when the user wants to use a function to make the download from the Internet, and then manually loading dynamic library, realizing the function of the plugin, I don’t have to worry about the function point increase in the number of infinite, it is a good thing!

  • Application modules are dynamically updated

    When there is a serious bug in a function point in the software, or you want to update a function, you just need to download the new version of the dynamic library file from the server at the appropriate time to the local, and then you can realize the new function when the user restarts the application.

The following will specifically introduce how to use dynamic Framework to implement App plug-in and dynamic update:

Implementation approach

The content of a certain module in the App is independent into a dynamic Framework. When the user wants to use a function, the corresponding dynamic library file is downloaded from the server to the sandbox according to the configuration list, and then the dynamic library is loaded and the principalClass enters the independent function module to realize the plug-in dynamic loading of the function. According to the version number of the configuration list, compare and update the downloaded dynamic library, so as to achieve the purpose of dynamic update.

If the user clicks a certain module and then downloads it, there will be an obvious waiting process. In order to have a better user experience, the user can choose the pre-loading strategy or configure the default dynamic library in the project. This part can be selected according to the actual situation of the project, which will not be discussed here.

The following figure is the overall implementation process:

Project structures,

The project implementation is divided into two parts: 1. Create dynamic library; 2. 2. The main App loads and maintains the dynamic library. Here, the project construction is divided into four parts, namely dynamic loading framework SVPCore and SVPRuntime, main project and other functional module plug-ins. The overall architecture design is as follows:

1. SVPCore

The main function of SVPCore is to parse the configuration information, find the corresponding bundle object, and obtain the main entry of the plug-in. A protocol containing SVPURI, the SVPDispatch class, and a SVPBundleDelegate.

SVPURI: Provides a static initialization method. During initialization, scheme(dynamic library protocol name), Parameters (dynamic library initialization parameters), and resourcePath(dynamic library path) are resolved and stored.

SVPDispatch: Provides a SVPBundleProvider protocol to get the bundle object to be loaded, and then obtains the loaded plug-in main entry object through the resourceWithURI: method provided by the SVPBundleDelegate protocol.

SVPBundleDelegate: Provides a protocol for obtaining A UIViewController based on SVPURI. PrincipalClass of the plug-in dynamic library implements the protocol, returning the principal entry object of the plug-in. At the same time, the parameters in the configuration information of the main project can be passed to the main entry object in the form of SVPURI parameters. When the plug-in dynamic library is provided to multiple projects, it can be convenient and flexible to achieve custom initialization.

The main code for SVPURI is as follows:

- (id)initWithURIString:(NSString *)uriString{ self = [super init]; if (self) { _uriString = [uriString copy]; NSURL *url = [NSURL URLWithString:_uriString]; if (! url || ! url.scheme) return nil; // Scheme is used to mark the dynamic library protocol name. _scheme = url.scheme; NSRange pathRange = NSMakeRange(_scheme.length + 3, _uriString.length - _scheme.length - 3); if (url.query) { NSArray *components = [url.query componentsSeparatedByString:@"&"]; NSMutableDictionary *parameters = [NSMutableDictionary dictionaryWithCapacity:0]; for (NSString *item in components) { NSArray *subItems = [item componentsSeparatedByString:@"="]; if (subItems.count >= 2) { parameters[subItems[0]] = subItems[1]; } // initialize the dynamic library. pathRange.length -= (url.query.length + 1); } if (pathrange.length > 0 && pathrange.location < uristring.length) {// resourcePath to mark the dynamic library path _resourcePath = [_uriString substringWithRange:pathRange]; } } return self; }Copy the code

The main code of SVPDispatch is as follows:

- (id)resourceWithURI:(NSString *)uriString{if (! uriString || ! _bundleProvider) return nil; return [self resourceWithObject:[SVPURI URIWithString:uriString]]; }- (id)resourceWithObject:(SVPURI *)uri{ if (! uri) return nil; id resource = nil; // bundleProvider is SVPRuntime, The implementation proxy method returns the principalObject if (_bundleProvider && [_bundleProvider] of the dynamic library corresponding to the URI respondsToSelector:@selector(bundleDelegateWithURI:)]) { id<SVPBundleDelegate> delegate = [_bundleProvider bundleDelegateWithURI:uri]; // Delegate for the dynamic library principalObject, If (delegate && [delegate respondsToSelector: @Selector (resourceWithURI:)]) {resource = [delegate resourceWithURI:uri]; } } return resource; }Copy the code

2. SVPRuntime

The main function of SVPRuntime is to manage function module plug-ins, including downloading/decompressing plug-ins and reading the dynamic library of decompression plug-ins. Contains the SVPBundle, SVPBundleDownloadItem and SVPBundleManager management classes.

SVPBundle: Provides a method to initialize through bundlePath and a load method to read the dynamic library from the sandbox to the bundle object and load it. After the loading is complete, the principalClass object of the bundle is obtained and initialized, and the plug-in module entry is obtained.

SVPBundleDownloadItem: provides a method to initialize the plug-in through the configuration information. Download the plug-in according to the remote address in the configuration information. After the download is successful, decompress the dynamic library to the corresponding directory according to the unique identifier, version number, and dynamic library name in the configuration information.

SVPBundleManager: Implements the SVPBundleProvider protocol provided by SVPCore and provides downloaded, decompression, and loaded plug-ins to SVPCore. After initialization, read the list of local downloaded bundles. If the user clicks on a function module, first check whether the plug-in is installed from the list. If not, initialize a SVPBundleDownloadItem, and then call the download method of Item. Then, in the download callback, unzip the downloaded dynamic library and initialize its corresponding bundle.

Two points to note here:

One is not common Class loadClass = [bundleclassNamed: the className]; Because this implementation method must know the className of the plug-in main entry in advance and can not customize the initialization parameters, it is designed to be implemented in a more flexible and unified dispatching and forwarding way through SVPDispatch: Using SVPDispatch’s resourceWithURI: method, pass the parameters initialization parameter in SVPURI to the plug-in’s main entry object, which initializes the home page and returns.

Second, in order to realize version comparison and dynamic update of dynamic library, the version number of dynamic library should be recorded during storage, and the old version data should be deleted after update.

The main code for SVPBundle is as follows:

- (BOOL)load{ if (self.status == SVPBundleLoaded) return YES; self.status = SVPBundleLoading; / / use path to obtain an NSBundle object self. The bundle = [NSBundle bundleWithPath: self, bundlePath]; NSError *error = nil; if (! [self.bundle preflightAndReturnError:&error]) { NSLog(@"%@", error); If (self.bundle && [self.bundle load]) {self.status = SVPBundleLoaded; Self. principalObject = [[[self. Bundle principalClass] alloc] init]; if (self.principalObject && [self.principalObject respondsToSelector:@selector(bundleDidLoad)]) { [self.principalObject performSelector:@selector(bundleDidLoad)]; } } else { self.status = SVPBundleLoadFailed; } return self.status == SVPBundleLoaded; }Copy the code

The main code of SVPBundleManager is as follows:

- (instancetype)init { self = [super init]; If (self) {// follow the SVPCore protocol [SVPAccessor defaultAccessor]. BundleProvider = self; _installedBundles = [NSMutableDictionary Dictionary]; NSString *mainPath = [self bundleFolder]; NSDirectoryEnumerator *directoryEnumerator = [self.fileManager enumeratorAtPath:mainPath]; for (NSString *path in directoryEnumerator.allObjects) { NSString *subPath = [mainPath stringByAppendingPathComponent:path]; NSArray *dirArray = [self.fileManager contentsOfDirectoryAtPath:subPath error:nil]; if (dirArray.count > 0) { NSString *frameworkName = [dirArray firstObject]; if ([frameworkName hasSuffix:@".framework"]) { NSString *bundlePath = [subPath stringByAppendingPathComponent:frameworkName]; SVPBundle *bundle = [[SVPBundle alloc] initWithBundlePath:bundlePath]; NSString *version = @""; NSArray *strArray = [frameworkName componentsSeparatedByString:@"_"]; if (strArray.count > 0) { version = [strArray firstObject]; NSString *bundleKey = [NSString stringWithFormat:@"%@_%@", version, path]; _installedBundles[bundleKey] = bundle; } } } } return self; } # pragma mark - SVPBundleDownloadItemDelegate / / the download is complete, Decompress the downloaded dynamic library - (void)downloadBundleItem (SVPBundleDownloadItem *)downloadItem finished:(BOOL)success {if (success) {[self  unZipDownloadItem:downloadItem]; } else { if (self.finishBlock) { self.finishBlock(NO); self.finishBlock = nil; }}} # pragma mark - SVPBundleProviderDelegate / / implementation SVPCore agreement, Return the principalObject- (ID)bundleDelegateWithURI:(SVPURI *) URI {if ([uri.scheme isEqual:@"scheme"] && uri.resourcePath.length > 0) { SVPBundle *bundle = _installedBundles[uri.resourcePath]; if (bundle) { return bundle.principalObject; } } return nil; }Copy the code

3. Plug-in module

To create a dynamic library, select Cocoa Touch Framework when creating the project, as shown below:

After importing the SVPCore dynamic library, create a BundleDelegate to implement the SVPBundleDelegate protocol. The code is as follows:

// The dynamic library implements the SVPCore protocol, Return the main entry page of the dynamic library - (UIViewController *)resourceWithURI:(SVPURI *) URI {if ([uri.scheme isEqual:@"scheme"]) {if ([uri.resourcePath isEqualToString:@"wechat"]) { SVPWechatViewController *wechatVC = [[SVPWechatViewController alloc] initWithParameters:uri.parameters]; return wechatVC; } } return nil; }Copy the code

SVPWechatViewController is the main entry object of the plug-in. It is necessary to implement the independent functions of the plug-in on this basis.

Then, the most important step is to configure the Principal class in the info.plist file of the dynamic library. This entry is used to obtain the Principal class from the NSBundle principalClass. After the SVPWechatBundleDelegate is set in the figure below, the loaded Bundle sends the principalClass message and gets this object. Because SVPWechatBundleDelegate implements the resourceWithURI: method of the SVPBundleDelegate protocol, the plug-in’s entry controller can be returned to the caller.

Then compile the dynamic library of the plug-in into a compressed package and put it on the server to provide a download link.

4. The main project

The main project is relatively simple, reading the configuration information from the Plist file and displaying it (the Plist file can be downloaded from the network) :

When the user clicks on the icon, first get the icon information and check whether the plug-in dynamic library is loaded. If not, call SVPBundleManager’s downloadItem method to download it. If it is loaded, call SVPDispatch’s resourceWithURI: method to get the plug-in entry. Proceed to the next operation, the main code is as follows:

- (void)onItemView:(UIButton *)sender {NSInteger itemIndex = sender.tag-1000; If (itemIndex >= 0 && itemIndex < self.pluginarray.count) {// PluginItem * PluginItem = [self.pluginarray  objectAtIndex:itemIndex]; // Dynamic library id: Version number + unique identifier, NSString *bundleKey = [NSString stringWithFormat:@"%@_%@", pluginItem.version, pluginItem.identifier]; if (! [[SVPBundleManager defaultManager] isInstalledBundleWithBundleKey: bundleKey]) {/ / local load, Weak __Typeof (self)weakSelf = self; weak __Typeof (self)weakSelf = self; weak __Typeof (self)weakSelf = self; __weak __typeof(PluginItem *)weakItem = pluginItem; __weak __typeof(UIButton *)weakSender = sender; [[SVPBundleManager defaultManager] downloadItem:[pluginItem toJSONDictionary] finished:^(BOOL success) { __strong __typeof(weakSelf)strongSelf = weakSelf; __strong __typeof(weakItem)strongItem = weakItem; __strong __typeof(weakSender)strongSender = weakSender; if (success) { dispatch_sync(dispatch_get_main_queue(), ^{[strongSelf pushBundleVC:itemIndex];});} else {// Download failure} dispatch_sync(dispatch_get_main_queue(), ^{ [strongSender setTitle:strongItem.name forState:UIControlStateNormal]; }); }]; [sender setTitle: @ "download..." forState: UIControlStateNormal]; } else {// push the main entry page of the dynamic library locally loaded [self pushBundleVC:itemIndex]; } }}- (void)pushBundleVC:(NSInteger)index { if (index >= 0 && index < self.pluginArray.count) { PluginItem *pluginItem =  [self.pluginArray objectAtIndex:index]; NSString *uriString = [NSString stringWithFormat:@"scheme://%@_%@", pluginItem.version, pluginItem.resource]; UIViewController *vc = [[SVPAccessor defaultAccessor] resourceWithURI:uriString]; if (vc) { [self.navigationController pushViewController:vc animated:YES]; }}}Copy the code

When the plug-in module needs to be updated, it only needs to modify the configuration list and the compression package of the plug-in dynamic library on the server. The main project updates the local configuration list at an appropriate time. When the user clicks the plug-in function, the local dynamic library can be searched and updated according to the version number to achieve the purpose of dynamic update.

Matters needing attention

The system checks the signature of the Framework when loading the dynamic library. The signature must contain TeamIdentifier, and the TeamIdentifier of the Framework and the main App must be the same.

If not, the following error is reported:

Error loading /path/to/framework: dlopen(/path/to/framework, 265): no suitable image found. Did find:/path/to/framework:code signature in (/path/to/framework) not valid for use in process  using Library Validation: mapped file has no cdhash, completely unsigned? Code has to be at least ad-hoc signed.Copy the code

conclusion

These are all implementations of plugin loading and dynamic updates using the Framework dynamic library. For now, Apple does not want developers to bypass the App Store to update their apps, so be careful with hot updates. For enterprise applications that do not need to be on the shelf, it is possible to use hot updates. As apple’s open environment continues to evolve, it remains to be seen whether apple will surprise us developers.

Retrieved on: Github