前言
WWDC2014給了我們一個(gè)很大的想象空間--iOS允許使用動(dòng)態(tài)庫(kù)剖张、App Extension等爆存。動(dòng)態(tài)庫(kù)是程序中使用的一種資源打包方式,可以將代碼文件尤筐、頭文件汇荐、資源文件洞就、說(shuō)明文檔等集中在一起,并且可以在運(yùn)行時(shí)手動(dòng)加載掀淘,這樣就可以做很多事情旬蟋,比如應(yīng)用插件化。
目前很多應(yīng)用功能越做越多革娄,軟件顯得越發(fā)臃腫倾贰,如果軟件的功能模塊也能像懶加載那樣按需加載豈不妙哉?比如像支付寶這種平臺(tái)級(jí)別的軟件:
首頁(yè)上這密密麻麻的功能拦惋,并且還在不斷增多匆浙,照這個(gè)趨勢(shì)發(fā)展下去,軟件包的大小勢(shì)必會(huì)越來(lái)越大厕妖。如果這些圖標(biāo)只是一個(gè)入口首尼,代碼和資源文件并未打包進(jìn)去,而是在用戶想使用這個(gè)功能時(shí)再?gòu)姆?wù)器下載該模塊的動(dòng)態(tài)庫(kù)言秸,這是否能在一定程度上減小APP包大小并實(shí)現(xiàn)動(dòng)態(tài)部署方案软能,繞過(guò)長(zhǎng)時(shí)間審核周期呢?
答案是肯定的举畸。那么如何將功能模塊打包成動(dòng)態(tài)庫(kù)并上傳到服務(wù)器查排、如何下載動(dòng)態(tài)庫(kù)、如何找到動(dòng)態(tài)庫(kù)插件入口這一系列問(wèn)題隨之而來(lái)抄沮,接下來(lái)將以Demo的形式一一解答上面疑問(wèn)跋核。
插件項(xiàng)目搭建
這里把插件項(xiàng)目搭建分為4個(gè)部分岖瑰,分別是PACore、PARuntime了罪、主工程以及其他功能模塊插件锭环。
PACore
PACore提供了PAURI、PABusAccessor類及一個(gè)PABundleDelegate的協(xié)議泊藕。
PAURI: 提供了一個(gè)靜態(tài)初始化方法辅辩,在初始化時(shí)對(duì)傳入的地址進(jìn)行解析,分別將scheme娃圆、parameters及resourcePath解析出來(lái)并存儲(chǔ);
PABusAccessor: 提供了一個(gè)PABundleProvider的協(xié)議用于獲取將要加載的bundle對(duì)象玫锋,然后通過(guò)PABundleDelegate協(xié)議提供的resourceWithURI:方法獲取加載好的插件主入口對(duì)象。
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;
}
之后把以上代碼打包成動(dòng)態(tài)庫(kù)供外部使用:
PARuntime
PARuntime的主要作用是對(duì)功能模塊插件進(jìn)行管理讼呢,包括插件的配置文件撩鹿、下載/解壓插件以及讀取解壓后插件的動(dòng)態(tài)庫(kù)等。
PABundle: 提供了一個(gè)通過(guò)NSDictionary來(lái)初始化的靜態(tài)方法悦屏,分別將配置信息里的唯一標(biāo)識(shí)节沦、版本號(hào)、動(dòng)態(tài)庫(kù)名稱及資源文件讀取到內(nèi)存中存儲(chǔ)础爬,并提供一個(gè)load方法從沙盒中將動(dòng)態(tài)庫(kù)讀取到bundle對(duì)象并加載甫贯,加載完成后獲取bundle的principalClass對(duì)象并初始化,拿到插件模塊入口;
PABundleDownloadItem: PABundle的子類看蚜,專門用于下載插件叫搁,同樣提供一個(gè)通過(guò)NSDictionary來(lái)初始化的靜態(tài)方法,分別將配置信息里的唯一標(biāo)識(shí)供炎、版本號(hào)渴逻、遠(yuǎn)程地址等信息讀取到內(nèi)存中存儲(chǔ),并提供一個(gè)下載方法通過(guò)這個(gè)遠(yuǎn)程地址對(duì)插件進(jìn)行下載音诫,下載成功后執(zhí)行代理讓代理處理接下來(lái)的操作;
PABundleManager: 實(shí)現(xiàn)PACore提供的PABundleProvider協(xié)議惨奕,將下載、解壓并加載好的插件入口提供給PACore竭钝,除此之外還從本地配置文件讀取已加載好的bundles梨撞、已安裝好的bundles、已下載好的bundles等配置信息蜓氨,若用戶點(diǎn)擊了某個(gè)功能模塊則先從配置文件中查看該插件是否已安裝聋袋,若未安裝則初始化一個(gè)PABundleDownloadItem,然后調(diào)用Item的下載方法穴吹,之后在回調(diào)里將下載好的動(dòng)態(tài)庫(kù)解壓并更新本地配置文件幽勒。
PABundle加載動(dòng)態(tài)庫(kù)代碼如下:
- (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不涉及服務(wù)端港令,下載代碼略:
//初始化
- (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主要功能代碼如下:
//檢測(cè)用戶點(diǎn)擊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;
}
//解壓下載下來(lái)的動(dòng)態(tài)庫(kù)
- (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;
}
之后把以上代碼打包成動(dòng)態(tài)庫(kù)供外部使用:
主工程
主工程的功能相對(duì)簡(jiǎn)單啥容,先從Plist文件中讀取列表信息展示(該P(yáng)list文件可從網(wǎng)絡(luò)下載):
緊接著將讀取到的列表信息按照一行三列展示:
- (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導(dǎo)入:
當(dāng)用戶點(diǎn)擊圖標(biāo)時(shí)先獲取圖標(biāo)信息并查看該插件動(dòng)態(tài)庫(kù)是否已加載锈颗,若未加載則調(diào)用PABundleManager的downloadItem方法進(jìn)行下載,若已加載則調(diào)用PABusAccessor的resourceWithURI:方法獲取插件入口咪惠,進(jìn)行接下來(lái)的操作击吱。
- (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];
}
}
}
第三方插件
首先得先創(chuàng)建一個(gè)動(dòng)態(tài)庫(kù),在創(chuàng)建工程時(shí)選Cocoa Touch Framework遥昧,如下圖:
點(diǎn)擊下一步覆醇,輸入bundle名稱,這個(gè)bundle名稱最好和前面所說(shuō)的配置信息的identifier對(duì)應(yīng)炭臭,接著將PACore的動(dòng)態(tài)庫(kù)導(dǎo)入后創(chuàng)建一個(gè)BundleDelegate實(shí)現(xiàn)PACore的PABundleDelegate協(xié)議永脓,如下圖:
最重要的一步,需在該動(dòng)態(tài)庫(kù)的Info.plist文件配置Principal class鞋仍,這個(gè)條目的作用是通過(guò)NSBundle的principalClass獲取到該對(duì)象常摧,如下圖將PAWechatBundleDelegate設(shè)置進(jìn)去之后,加載完成后的Bundle發(fā)送principalClass消息威创,拿到的就是這個(gè)對(duì)象落午,拿到這個(gè)對(duì)象后執(zhí)行PABundleDelegate協(xié)議的resourceWithURI:方法,由于PAWechatBundleDelegate實(shí)現(xiàn)了協(xié)議肚豺,所以通過(guò)解析PAURI將入口控制器返回給調(diào)用方溃斋。
之后將該插件的動(dòng)態(tài)庫(kù)編譯后壓縮放到服務(wù)器上提供下載鏈接即可。
總結(jié)
以上便是demo的所有實(shí)現(xiàn)详炬,值得一提的是就目前而言拿動(dòng)態(tài)庫(kù)做動(dòng)態(tài)部署雖然蘋果能審核通過(guò)盐类,但是下載下來(lái)的動(dòng)態(tài)庫(kù)是無(wú)法加載的寞奸。主要原因是因?yàn)楹灻麩o(wú)法通過(guò)呛谜,因?yàn)镈istribution的APP只能加載相同證書打包的framework。所以就目前而言枪萄,基于動(dòng)態(tài)庫(kù)的插件化動(dòng)態(tài)部署方案還是無(wú)法做到的隐岛,但是隨著技術(shù)日新月異的發(fā)展,蘋果會(huì)不會(huì)給我們開(kāi)發(fā)者驚喜呢瓷翻,這就不得而知了聚凹。