前言
WWDC2014给了我们一个很大的想象空间--iOS允许使用动态库、App Extension等。动态库是程序中使用的一种资源打包方式,可以将代码文件、头文件、资源文件、说明文档等集中在一起,并且可以在运行时手动加载,这样就可以做很多事情,比如应用插件化。
目前很多应用功能越做越多,软件显得越发臃肿,如果软件的功能模块也能像懒加载那样按需加载岂不妙哉?比如像支付宝这种平台级别的软件:
首页上这密密麻麻的功能,并且还在不断增多,照这个趋势发展下去,软件包的大小势必会越来越大。如果这些图标只是一个入口,代码和资源文件并未打包进去,而是在用户想使用这个功能时再从服务器下载该模块的动态库,这是否能在一定程度上减小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 delegate = [_bundleProvider bundleDelegateWithURI:uri];
if (delegate && [delegate respondsToSelector:@selector(resourceWithURI:)])
{
resource = [delegate resourceWithURI:uri];
}
}
return resource;
}
之后把以上代码打包成动态库供外部使用:
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;
}
之后把以上代码打包成动态库供外部使用:
主工程
主工程的功能相对简单,先从Plist文件中读取列表信息展示(该Plist文件可从网络下载):
紧接着将读取到的列表信息按照一行三列展示:
- (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;
}
所得到的效果如下图:
将之前打包好的PACore和PARuntime导入:
当用户点击图标时先获取图标信息并查看该插件动态库是否已加载,若未加载则调用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,如下图:
点击下一步,输入bundle名称,这个bundle名称最好和前面所说的配置信息的identifier对应,接着将PACore的动态库导入后创建一个BundleDelegate实现PACore的PABundleDelegate协议,如下图:
最重要的一步,需在该动态库的Info.plist文件配置Principal class,这个条目的作用是通过NSBundle的principalClass获取到该对象,如下图将PAWechatBundleDelegate设置进去之后,加载完成后的Bundle发送principalClass消息,拿到的就是这个对象,拿到这个对象后执行PABundleDelegate协议的resourceWithURI:方法,由于PAWechatBundleDelegate实现了协议,所以通过解析PAURI将入口控制器返回给调用方。
之后将该插件的动态库编译后压缩放到服务器上提供下载链接即可。
总结
以上便是demo的所有实现,值得一提的是就目前而言拿动态库做动态部署虽然苹果能审核通过,但是下载下来的动态库是无法加载的。主要原因是因为签名无法通过,因为Distribution的APP只能加载相同证书打包的framework。所以就目前而言,基于动态库的插件化动态部署方案还是无法做到的,但是随着技术日新月异的发展,苹果会不会给我们开发者惊喜呢,这就不得而知了。