Mobile Development 15 min read

Implementing iOS App Plugin Architecture and Dynamic Updates with Dynamic Frameworks

This article explains how to use iOS dynamic frameworks to modularize app features, enable on‑demand plugin loading and seamless runtime updates, detailing the design of SVPCore, SVPRuntime, bundle management, and integration steps with code examples.

Sohu Tech Products
Sohu Tech Products
Sohu Tech Products
Implementing iOS App Plugin Architecture and Dynamic Updates with Dynamic Frameworks

WWDC 2014 introduced dynamic libraries for iOS, allowing developers to package code, headers, resources, and documentation into a single bundle that is loaded at runtime rather than being statically linked into the executable. This runtime loading enables easy replacement of libraries without recompiling the host app.

Two main use cases are highlighted: application pluginization, where features are lazily downloaded and loaded as needed, and dynamic module updates, where bug fixes or new functionality can be delivered by downloading a new framework and loading it on the next app launch.

The implementation follows a four‑step process: creating a dynamic framework, importing it into the host project, defining a bundle delegate, and managing download, extraction, and loading of the framework.

1. SVPCore

SVPCore parses configuration URIs, locates the corresponding bundle, and retrieves the plugin’s entry point. It defines SVPURI , SVPDispatch , and the SVPBundleDelegate protocol.

- (id)initWithURIString:(NSString *)uriString
{
    self = [super init];
    if (self)
    {
        _uriString = [uriString copy];
        NSURL *url = [NSURL URLWithString:_uriString];
        if (!url || !url.scheme) return nil;
        _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 dictionary];
            for (NSString *item in components)
            {
                NSArray *subItems = [item componentsSeparatedByString:@"="];
                if (subItems.count >= 2)
                {
                    parameters[subItems[0]] = subItems[1];
                }
            }
            _parameters = parameters;
            pathRange.length -= (url.query.length + 1);
        }
        if (pathRange.length > 0 && pathRange.location < uriString.length)
        {
            _resourcePath = [_uriString substringWithRange:pathRange];
        }
    }
    return self;
}

SVPDispatch forwards resource requests to the bundle delegate:

// 根据URI获取动态库主入口页面
- (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;
    if (_bundleProvider && [_bundleProvider respondsToSelector:@selector(bundleDelegateWithURI:)])
    {
        id<SVPBundleDelegate> delegate = [_bundleProvider bundleDelegateWithURI:uri];
        if (delegate && [delegate respondsToSelector:@selector(resourceWithURI:)])
        {
            resource = [delegate resourceWithURI:uri];
        }
    }
    return resource;
}

2. SVPRuntime

SVPRuntime manages plugin lifecycle: downloading, unzipping, loading, and version comparison. It defines SVPBundle , SVPBundleDownloadItem , and SVPBundleManager .

- (BOOL)load
{
    if (self.status == SVPBundleLoaded) return YES;
    self.status = SVPBundleLoading;
    self.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;
}
- (instancetype)init {
    self = [super init];
    if (self) {
        [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;
}

// 下载完成,解压下载下来的动态库
- (void)downloadBundleItem:(SVPBundleDownloadItem *)downloadItem finished:(BOOL)success {
    if (success) {
        [self unZipDownloadItem:downloadItem];
    } else {
        if (self.finishBlock) {
            self.finishBlock(NO);
            self.finishBlock = nil;
        }
    }
}

// 实现SVPCore的协议,返回URI对应的动态库的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;
}

3. Plugin Module

Developers create a Cocoa Touch Framework, import SVPCore, and implement a class conforming to SVPBundleDelegate that returns the plugin’s entry view controller based on the URI.

// 动态库实现SVPCore的协议,返回动态库的主入口页面
- (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;
}

The framework’s Info.plist must specify the principal class so that NSBundle can locate it at load time.

4. Host Application

The host app reads a remote configuration plist, displays available plugins, and on user interaction either downloads the missing framework or loads an already installed one, then pushes the plugin’s view controller.

// 用户点击插件
- (void)onItemView:(UIButton *)sender {
    NSInteger itemIndex = sender.tag - 1000;
    if (itemIndex >= 0 && itemIndex < self.pluginArray.count) {
        PluginItem *pluginItem = [self.pluginArray objectAtIndex:itemIndex];
        NSString *bundleKey = [NSString stringWithFormat:@"%@_%@", pluginItem.version, pluginItem.identifier];
        if (![[SVPBundleManager defaultManager] isInstalledBundleWithBundleKey:bundleKey]) {
            __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]; });
                }
                dispatch_sync(dispatch_get_main_queue(), ^{ [strongSender setTitle:strongItem.name forState:UIControlStateNormal]; });
            }];
            [sender setTitle:@"下载中..." forState:UIControlStateNormal];
        } else {
            [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]; }
    }
}

When updating a plugin, the server provides a new bundle and version; the host app checks the version, downloads, unzips, and replaces the old bundle, achieving seamless dynamic updates.

Notes

Dynamic frameworks must be signed with a Team Identifier that matches the host app; otherwise loading fails with a code‑signature validation error.

Error loading /path/to/framework: dlopen(/path/to/framework, 265): no suitable image found. … Code has to be at least ad‑hoc signed.

Conclusion

The article demonstrates a complete solution for iOS pluginization and runtime updates using dynamic frameworks, acknowledging Apple’s restrictions on App Store distribution but noting that the approach is suitable for enterprise or internal apps where hot‑updates are permissible.

mobile developmentiOSplugin architectureObjective-Capp updateDynamic Framework
Sohu Tech Products
Written by

Sohu Tech Products

A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.