iOS插件化开发

前言

WWDC2014给了我们一个很大的想象空间--iOS允许使用动态库、App Extension等。动态库是程序中使用的一种资源打包方式,可以将代码文件、头文件、资源文件、说明文档等集中在一起,并且可以在运行时手动加载,这样就可以做很多事情,比如应用插件化。

目前很多应用功能越做越多,软件显得越发臃肿,如果软件的功能模块也能像懒加载那样按需加载岂不妙哉?比如像支付宝这种平台级别的软件:

alipay.jpg

首页上这密密麻麻的功能,并且还在不断增多,照这个趋势发展下去,软件包的大小势必会越来越大。如果这些图标只是一个入口,代码和资源文件并未打包进去,而是在用户想使用这个功能时再从服务器下载该模块的动态库,这是否能在一定程度上减小APP包大小并实现动态部署方案,绕过长时间审核周期呢?

答案是肯定的。那么如何将功能模块打包成动态库并上传到服务器、如何下载动态库、如何找到动态库插件入口这一系列问题随之而来,接下来将以Demo的形式一一解答上面疑问。

插件项目搭建

这里把插件项目搭建分为4个部分,分别是PACore、PARuntime、主工程以及其他功能模块插件。

PACore

PACore提供了PAURI、PABusAccessor类及一个PABundleDelegate的协议。

PAURI: 提供了一个静态初始化方法,在初始化时对传入的地址进行解析,分别将scheme、parameters及resourcePath解析出来并存储;

PABusAccessor: 提供了一个PABundleProvider的协议用于获取将要加载的bundle对象,然后通过PABundleDelegate协议提供的resourceWithURI:方法获取加载好的插件主入口对象。

PAURI解析代码如下:

+ (instancetype)URIWithString:(NSString *)uriString
{
    if (!uriString) return nil;

    return [[PAURI alloc] initWithURIString:uriString];
}


- (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 dictionaryWithCapacity:0];

            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;
}

PABusAccessor主要功能代码如下:

- (id)resourceWithURI:(NSString *)uriString
{
    if (!uriString || !_bundleProvider) return nil;

    return [self resourceWithObject:[PAURI URIWithString:uriString]];
}

- (id)resourceWithObject:(PAURI *)uri
{
    if (!uri) return nil;

    id resource = nil;

    if ([_bundleProvider respondsToSelector:@selector(bundleDelegateWithURI:)])
    {
        id<PABundleDelegate> delegate = [_bundleProvider bundleDelegateWithURI:uri];

        if (delegate && [delegate respondsToSelector:@selector(resourceWithURI:)])
        {
            resource = [delegate resourceWithURI:uri];
        }
    }

    return resource;
}

之后把以上代码打包成动态库供外部使用:

core_framework.jpg

PARuntime

PARuntime的主要作用是对功能模块插件进行管理,包括插件的配置文件、下载/解压插件以及读取解压后插件的动态库等。

PABundle: 提供了一个通过NSDictionary来初始化的静态方法,分别将配置信息里的唯一标识、版本号、动态库名称及资源文件读取到内存中存储,并提供一个load方法从沙盒中将动态库读取到bundle对象并加载,加载完成后获取bundle的principalClass对象并初始化,拿到插件模块入口;

PABundleDownloadItem: PABundle的子类,专门用于下载插件,同样提供一个通过NSDictionary来初始化的静态方法,分别将配置信息里的唯一标识、版本号、远程地址等信息读取到内存中存储,并提供一个下载方法通过这个远程地址对插件进行下载,下载成功后执行代理让代理处理接下来的操作;

PABundleManager: 实现PACore提供的PABundleProvider协议,将下载、解压并加载好的插件入口提供给PACore,除此之外还从本地配置文件读取已加载好的bundles、已安装好的bundles、已下载好的bundles等配置信息,若用户点击了某个功能模块则先从配置文件中查看该插件是否已安装,若未安装则初始化一个PABundleDownloadItem,然后调用Item的下载方法,之后在回调里将下载好的动态库解压并更新本地配置文件。

PABundle加载动态库代码如下:

- (BOOL)load
{
    if (self.status >= PABundleLoading) return NO;

    self.status = PABundleLoading;

    self.bundle = [NSBundle bundleWithPath:[self fullFilePath]];

    NSError *error = nil;

    if (![self.bundle preflightAndReturnError:&error])
    {
        NSLog(@"%@", error);
    }

    if (self.bundle && [self.bundle load])
    {
        self.status = PABundleLoaded;

        self.principalObject = [[[self.bundle principalClass] alloc] init];

        if (self.principalObject && [self.principalObject respondsToSelector:@selector(bundleDidLoad)])
        {
            [self.principalObject performSelector:@selector(bundleDidLoad)];
        }
    }
    else
    {
        self.status = PABundleLoadFailed;
    }

    return self.status == PABundleLoaded;
}

PABundleDownloadItem主要功能代码如下,由于demo不涉及服务端,下载代码略:

//初始化

- (instancetype)initWithDownloadItem:(NSDictionary *)item
{
    self = [super init];

    if (self)
    {
        self.identifier = item[@"identifier"];
        self.version = item[@"version"];
        self.templatePath = item[@"zipName"];
        self.name = item[@"frameworkName"];

        self.filePath = self.name;
        self.isEmbedded = NO;
        self.status = PABundleNone;

        self.remoteURL = item[@"remoteURL"];;
        self.resources = item[@"resources"];
    }

    return self;
}

//下载

- (BOOL)start
{
    if (PABundleDownloading <= self.status) return NO;

    // TODO: Download Item

    self.status = PABundleDownloaded;

    if (self.delegate && [self.delegate respondsToSelector:@selector(didDownloadBundleItem:)])
    {
        [self.delegate didDownloadBundleItem:self];
    }

    return YES;
}

PABundleManager主要功能代码如下:

//检测用户点击Bundle是否已安装

- (BOOL)isInstalledBundleWithIdentifier:(NSString *)identifier
{
    return nil != _installedBundles[identifier];
}

//初始化DownloadItem

- (PABundleDownloadItem *)downloadItem:(NSDictionary *)item
{
    PABundleDownloadItem *downloadItem = [PABundleDownloadItem itemWithDownloadItem:item];
    downloadItem.delegate = self;

    _downloadingBundles[downloadItem.identifier] = downloadItem;

    [downloadItem start];

    return downloadItem;
}

//解压下载下来的动态库

- (BOOL)unZipDownloadItem:(PABundleDownloadItem *)downloadItem
{
    if (!downloadItem || !downloadItem.templatePath) return NO;

    BOOL bResult = NO;

    downloadItem.status = PABundleInstalling;

    NSString *src  = [downloadItem fullTemplatePath];
    NSString *dest = [downloadItem installFolder];

    if (src && dest)
    {
        if ([[NSFileManager defaultManager] fileExistsAtPath:dest])
        {
            [[NSFileManager defaultManager] removeItemAtPath:dest error:nil];
        }

        bResult = [SSZipArchive unzipFileAtPath:src toDestination:dest];

        downloadItem.status = bResult == YES ? PABundleInstalled : PABundleNone;
    }
    else
    {
        downloadItem.status = PABundleDownloaded;
    }

    return bResult;
}

//更新本地配置文件

- (BOOL)updateDataBase:(PABundleDownloadItem *)downloadItem
{
    if (!downloadItem || PABundleInstalled != downloadItem.status) return NO;

    @synchronized(_installedBundles)
    {
        _installedBundles[downloadItem.identifier] = downloadItem;
    }

    @synchronized(_routes)
    {
        for (NSString *name in downloadItem.resources)
        {
            _routes[name] = downloadItem;
        }
    }

    NSMutableArray *array = [NSMutableArray arrayWithCapacity:0];

    for (PABundle *item in _installedBundles.allValues)
    {
        [array addObject:[item keyInformation]];
    }

    [PARuntimeUtils updateInstalledBundles:array];

    return YES;
}

之后把以上代码打包成动态库供外部使用:

runtime_framework.jpg

主工程

主工程的功能相对简单,先从Plist文件中读取列表信息展示(该Plist文件可从网络下载):

embedded.jpg

紧接着将读取到的列表信息按照一行三列展示:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *identifier = @"Cell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];

    if (!cell)
    {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
                                      reuseIdentifier:identifier];

        cell.contentView.backgroundColor = tableView.backgroundColor;
        cell.selectionStyle = UITableViewCellSelectionStyleNone;

        CGFloat width = 100.0;
        CGFloat itemWidth = [UIScreen mainScreen].bounds.size.width / 3;
        CGFloat offsetX = (itemWidth - width) / 2;

        for (NSInteger index = 0; index < 3; index ++)
        {
            PAAppStoreItem *itemView = [[PAAppStoreItem alloc] initWithFrame:CGRectMake(itemWidth * index + offsetX, 0, width, 120.0)];
            itemView.tag = index + 1000;

            [itemView addTarget:self
                         action:@selector(onItemView:)
               forControlEvents:UIControlEventTouchUpInside];

            [cell.contentView addSubview:itemView];
        }
    }

    for (NSInteger index = 0; index < 3; index ++)
    {
        NSDictionary *storeItem = [self storeItemAtIndex:indexPath.row * 3 + index];

        PAAppStoreItem *itemView = (PAAppStoreItem *)[cell.contentView viewWithTag:index + 1000];
        [itemView reloadSubViewsWithStoreItem:storeItem];
    }

    return cell;
}

所得到的效果如下图:

Main.jpg

将之前打包好的PACore和PARuntime导入:

import_framework.jpg

当用户点击图标时先获取图标信息并查看该插件动态库是否已加载,若未加载则调用PABundleManager的downloadItem方法进行下载,若已加载则调用PABusAccessor的resourceWithURI:方法获取插件入口,进行接下来的操作。

- (void)onItemView:(id)sender
{
    PAAppStoreItem *itemView = (PAAppStoreItem *)sender;
    NSDictionary *storeItem = itemView.storeItem;

    if (![[PABundleManager defaultBundleManager] isInstalledBundleWithIdentifier:storeItem[@"identifier"]])
    {
        [[PABundleManager defaultBundleManager] downloadItem:storeItem];

        [itemView download];
    }
    else
    {
        NSString *uriString = [NSString stringWithFormat:@"ui://%@", [storeItem[@"resources"] firstObject]];
        UIViewController *vc = [[PABusAccessor defaultBusAccessor] resourceWithURI:uriString];

        if (vc)
        {
            [self.navigationController pushViewController:vc animated:YES];
        }
    }
}

第三方插件

首先得先创建一个动态库,在创建工程时选Cocoa Touch Framework,如下图:

create_framework.jpg

点击下一步,输入bundle名称,这个bundle名称最好和前面所说的配置信息的identifier对应,接着将PACore的动态库导入后创建一个BundleDelegate实现PACore的PABundleDelegate协议,如下图:

plugin_code.jpg

最重要的一步,需在该动态库的Info.plist文件配置Principal class,这个条目的作用是通过NSBundle的principalClass获取到该对象,如下图将PAWechatBundleDelegate设置进去之后,加载完成后的Bundle发送principalClass消息,拿到的就是这个对象,拿到这个对象后执行PABundleDelegate协议的resourceWithURI:方法,由于PAWechatBundleDelegate实现了协议,所以通过解析PAURI将入口控制器返回给调用方。

configuration.jpg

之后将该插件的动态库编译后压缩放到服务器上提供下载链接即可。

总结

以上便是demo的所有实现,值得一提的是就目前而言拿动态库做动态部署虽然苹果能审核通过,但是下载下来的动态库是无法加载的。主要原因是因为签名无法通过,因为Distribution的APP只能加载相同证书打包的framework。所以就目前而言,基于动态库的插件化动态部署方案还是无法做到的,但是随着技术日新月异的发展,苹果会不会给我们开发者惊喜呢,这就不得而知了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,340评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,762评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,329评论 0 329
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,678评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,583评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,995评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,493评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,145评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,293评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,250评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,267评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,973评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,556评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,648评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,873评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,257评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,809评论 2 339

推荐阅读更多精彩内容

  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    X先生_未知数的X阅读 15,967评论 3 119
  • 汇总: 生活:9小时27分 纯时间:14小时33分 CPA:5小时19分钟 长半衰期:0小时0分 纯时间利用...
    Water氺氺阅读 159评论 0 0
  • 精神层面的东西,为什么会用心来代表?心也可以是心脏,又可以代指精神,只能说明两者之间是有联系的。 当我们面...
    水沉檀香阅读 565评论 0 0
  • 靖江市华夏科技有限公司专业从事染整机械制造34年,本着产品优于同行,质量高于同行,服务良于同行的经营理念,为社会设...
    华夏科技张林阅读 185评论 0 0