iOS插件化開(kāi)發(fā)

前言

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í)別的軟件:

alipay.jpg

首頁(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ù)供外部使用:

core_framework.jpg

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ù)供外部使用:

runtime_framework.jpg

主工程

主工程的功能相對(duì)簡(jiǎn)單啥容,先從Plist文件中讀取列表信息展示(該P(yáng)list文件可從網(wǎng)絡(luò)下載):

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導(dǎo)入:

import_framework.jpg

當(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遥昧,如下圖:

create_framework.jpg

點(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é)議永脓,如下圖:

plugin_code.jpg

最重要的一步,需在該動(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)用方溃斋。

configuration.jpg

之后將該插件的動(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ā)者驚喜呢瓷翻,這就不得而知了聚凹。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市齐帚,隨后出現(xiàn)的幾起案子妒牙,更是在濱河造成了極大的恐慌,老刑警劉巖对妄,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件湘今,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡剪菱,警方通過(guò)查閱死者的電腦和手機(jī)摩瞎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門拴签,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人旗们,你說(shuō)我怎么就攤上這事蚓哩。” “怎么了上渴?”我有些...
    開(kāi)封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵岸梨,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我稠氮,道長(zhǎng)盛嘿,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任括袒,我火速辦了婚禮次兆,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘锹锰。我一直安慰自己芥炭,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布恃慧。 她就那樣靜靜地躺著园蝠,像睡著了一般。 火紅的嫁衣襯著肌膚如雪痢士。 梳的紋絲不亂的頭發(fā)上彪薛,一...
    開(kāi)封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音怠蹂,去河邊找鬼善延。 笑死,一個(gè)胖子當(dāng)著我的面吹牛城侧,可吹牛的內(nèi)容都是我干的易遣。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼嫌佑,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼豆茫!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起屋摇,我...
    開(kāi)封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤揩魂,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后炮温,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體火脉,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了忘分。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片棋枕。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖妒峦,靈堂內(nèi)的尸體忽然破棺而出重斑,到底是詐尸還是另有隱情,我是刑警寧澤肯骇,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布窥浪,位于F島的核電站,受9級(jí)特大地震影響笛丙,放射性物質(zhì)發(fā)生泄漏漾脂。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一胚鸯、第九天 我趴在偏房一處隱蔽的房頂上張望骨稿。 院中可真熱鬧,春花似錦姜钳、人聲如沸坦冠。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)辙浑。三九已至,卻和暖如春拟糕,著一層夾襖步出監(jiān)牢的瞬間判呕,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工送滞, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留侠草,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓累澡,卻偏偏與公主長(zhǎng)得像梦抢,于是被迫代替她去往敵國(guó)和親般贼。 傳聞我的和親對(duì)象是個(gè)殘疾皇子愧哟,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容

  • 1、通過(guò)CocoaPods安裝項(xiàng)目名稱項(xiàng)目信息 AFNetworking網(wǎng)絡(luò)請(qǐng)求組件 FMDB本地?cái)?shù)據(jù)庫(kù)組件 SD...
    陽(yáng)明先生_x閱讀 15,968評(píng)論 3 119
  • 匯總: 生活:9小時(shí)27分 純時(shí)間:14小時(shí)33分 CPA:5小時(shí)19分鐘 長(zhǎng)半衰期:0小時(shí)0分 純時(shí)間利用...
    Water氺氺閱讀 161評(píng)論 0 0
  • 精神層面的東西哼蛆,為什么會(huì)用心來(lái)代表蕊梧?心也可以是心臟,又可以代指精神腮介,只能說(shuō)明兩者之間是有聯(lián)系的肥矢。 當(dāng)我們面...
    水沉檀香閱讀 569評(píng)論 0 0
  • 靖江市華夏科技有限公司專業(yè)從事染整機(jī)械制造34年,本著產(chǎn)品優(yōu)于同行,質(zhì)量高于同行甘改,服務(wù)良于同行的經(jīng)營(yíng)理念旅东,為社會(huì)設(shè)...
    華夏科技張林閱讀 186評(píng)論 0 0