<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          iOS插件化架構(gòu)探索

          共 11702字,需瀏覽 24分鐘

           ·

          2021-08-27 13:09

          ????關(guān)注后回復(fù) “進(jìn)群” ,拉你進(jìn)程序員交流群????

          作者丨視頻團(tuán)隊(duì) 董佩佩

          來源丨搜狐技術(shù)產(chǎn)品(ID:sohu-tech)


          本文字?jǐn)?shù):3228

          預(yù)計閱讀時間:15分鐘


          前言

          WWDC2014蘋果在iOS上開放了動態(tài)庫,這給了我們一個很大的想象空間。

          動態(tài)庫即動態(tài)鏈接庫,是Cocoa/Cocoa Touch程序中使用的一種資源打包方式,可以將代碼文件、頭文件、資源文件、說明文檔等集中在一起,方便開發(fā)者使用。動態(tài)庫在編譯時并不會被拷貝到程序的可執(zhí)行文件(也就是mach-o)中,等到程序運(yùn)行時,動態(tài)庫才會被真正加載。

          動態(tài)庫運(yùn)行時才載入的特性,也可以讓我們隨時對庫進(jìn)行替換,而不需要重新編譯代碼。這樣我們就可以做很多事情,比如應(yīng)用插件化及動態(tài)更新:

          • 應(yīng)用插件化

            目前很多應(yīng)用功能越做越多,軟件顯得越來越臃腫,如果軟件的功能模塊也能像懶加載那樣按需加載,在用戶想使用某個功能的時候讓其從網(wǎng)絡(luò)下載,然后手動加載動態(tài)庫,實(shí)現(xiàn)功能的插件化,就再也不用擔(dān)心功能點(diǎn)的無限增多了,這該是件多么美好的事!

          • 應(yīng)用模塊動態(tài)更新

            當(dāng)軟件中的某個功能點(diǎn)出現(xiàn)了嚴(yán)重的 bug,或者想更新某個功能,這時候只需要在適當(dāng)?shù)臅r候從服務(wù)器上將新版本的動態(tài)庫文件下載到本地,然后在用戶重啟應(yīng)用的時候即可實(shí)現(xiàn)新功能的展現(xiàn)。

          下面將具體介紹如何使用動態(tài) Framework的方式實(shí)現(xiàn)App的插件化及動態(tài)更新:


          實(shí)現(xiàn)思路

          將 App中的某個模塊的內(nèi)容獨(dú)立成一個動態(tài)Framework的形式,在用戶想使用某個功能的時候,根據(jù)配置列表從服務(wù)器上將對應(yīng)的動態(tài)庫文件下載到沙盒,然后加載動態(tài)庫并由principalClass進(jìn)入獨(dú)立功能模塊,實(shí)現(xiàn)功能的插件化動態(tài)加載。并根據(jù)配置列表的版本號,對已下載的動態(tài)庫進(jìn)行比對更新,即可達(dá)到動態(tài)更新的目的。

          用戶點(diǎn)擊某個模塊再下載的話,會有明顯的等待過程,為了有更好的用戶體驗(yàn),可以選擇預(yù)加載策略,或在項(xiàng)目中配置默認(rèn)動態(tài)庫,這部分可以根據(jù)項(xiàng)目的實(shí)際情況來選擇,這里暫不展開討論。

          下圖是整體的實(shí)現(xiàn)流程:


          項(xiàng)目搭建

          項(xiàng)目實(shí)現(xiàn)主要分為兩部分:1、創(chuàng)建動態(tài)庫;2、主App加載維護(hù)動態(tài)庫。這里把項(xiàng)目搭建拆分細(xì)化為四個部分,分別是動態(tài)加載框架SVPCore和SVPRuntime、主工程以及其他功能模塊插件,整體的架構(gòu)設(shè)計如下圖:

          插件化及動態(tài)加載框架設(shè)計圖

          1. SVPCore

          SVPCore的主要作用是對配置信息進(jìn)行解析,查找到對應(yīng)的bundle對象,并獲取插件的主入口。包含SVPURI、SVPDispatch類及一個SVPBundleDelegate的協(xié)議。

          SVPURI: 提供了一個靜態(tài)初始化方法,在初始化時對傳入的地址進(jìn)行解析,分別將scheme(動態(tài)庫協(xié)議名)、parameters(動態(tài)庫初始化參數(shù))及resourcePath(動態(tài)庫路徑)解析出來并存儲;

          SVPDispatch: 提供了一個SVPBundleProvider的協(xié)議用于獲取將要加載的bundle對象,然后通過SVPBundleDelegate協(xié)議提供的resourceWithURI:方法獲取加載好的插件主入口對象。

          SVPBundleDelegate: 提供了一個根據(jù)SVPURI獲取UIViewController的協(xié)議,由插件動態(tài)庫的principalClass實(shí)現(xiàn)該協(xié)議,返回插件的主入口對象。同時,可以將主工程配置信息里的參數(shù),通過SVPURI的parameters的形式傳遞給主入口對象,當(dāng)插件動態(tài)庫提供給多個工程使用時,可以方便靈活的實(shí)現(xiàn)自定義初始化。

          SVPURI的主要代碼如下:

          - (id)initWithURIString:(NSString *)uriString
          {
          self = [super init];

          if (self)
          {
          _uriString = [uriString copy];

          NSURL *url = [NSURL URLWithString:_uriString];

          if (!url || !url.scheme) return nil;

          // scheme用來標(biāo)記動態(tài)庫協(xié)議名
          _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用來標(biāo)記動態(tài)庫初始化參數(shù)
          _parameters = parameters;

          pathRange.length -= (url.query.length + 1);
          }

          if (pathRange.length > 0 && pathRange.location < uriString.length)
          {
          // resourcePath用來標(biāo)記動態(tài)庫路徑
          _resourcePath = [_uriString substringWithRange:pathRange];
          }
          }

          return self;
          }

          SVPDispatch主要代碼如下:

          // 根據(jù)URI獲取動態(tài)庫主入口頁面
          - (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;

          // bundleProvider為SVPRuntime,其實(shí)現(xiàn)代理方法返回URI對應(yīng)的動態(tài)庫的principalObject
          if (_bundleProvider && [_bundleProvider respondsToSelector:@selector(bundleDelegateWithURI:)])
          {
          id<SVPBundleDelegate> delegate = [_bundleProvider bundleDelegateWithURI:uri];

          // delegate為動態(tài)庫的principalObject,其實(shí)現(xiàn)代理方法返回動態(tài)庫的主入口頁面
          if (delegate && [delegate respondsToSelector:@selector(resourceWithURI:)])
          {
          resource = [delegate resourceWithURI:uri];
          }
          }

          return resource;
          }

          2. SVPRuntime

          SVPRuntime的主要作用是對功能模塊插件進(jìn)行管理,包括下載/解壓插件以及讀取解壓后插件的動態(tài)庫等。包含SVPBundle、SVPBundleDownloadItem類及SVPBundleManager管理類。

          SVPBundle: 提供了一個通過bundlePath來初始化的方法,并提供了一個load方法,從沙盒中將動態(tài)庫讀取到bundle對象并加載,加載完成后獲取bundle的principalClass對象并初始化,拿到插件模塊入口;

          SVPBundleDownloadItem: 提供了一個通過配置信息來初始化的方法,根據(jù)配置信息里的遠(yuǎn)程地址對插件進(jìn)行下載,下載成功后根據(jù)配置信息里的唯一標(biāo)識、版本號、動態(tài)庫名稱等將動態(tài)庫解壓到對應(yīng)的目錄;

          SVPBundleManager: 實(shí)現(xiàn)SVPCore提供的SVPBundleProvider協(xié)議,將下載、解壓并加載好的插件入口提供給SVPCore。初始化后讀取本地已下載好的bundles列表,若用戶點(diǎn)擊了某個功能模塊則先從列表中查看該插件是否已安裝,若未安裝則初始化一個SVPBundleDownloadItem,然后調(diào)用Item的下載方法,之后在下載回調(diào)里將下載好的動態(tài)庫解壓并初始化其對應(yīng)的bundle。

          在這里需要注意兩點(diǎn):

          一是沒有采用普遍的Class loadClass = [bundleclassNamed:className];的形式獲取插件主入口對象,因?yàn)檫@種實(shí)現(xiàn)方式必須提前知道插件主入口的className,而且不能自定義初始化參數(shù),因此設(shè)計為更為靈活的通過SVPDispatch統(tǒng)一調(diào)度中轉(zhuǎn)的方式來實(shí)現(xiàn):通過SVPDispatch的resourceWithURI:方法,將SVPURI里的parameters初始化參數(shù)傳遞給插件主入口對象,由主入口對象進(jìn)行主頁面的初始化并返回。

          二是為了實(shí)現(xiàn)動態(tài)庫的版本比對和動態(tài)更新,在存儲時需記錄動態(tài)庫的版本號,并且在更新后刪除之前的舊版本數(shù)據(jù)。

          SVPBundle的主要代碼如下:

          - (BOOL)load
          {
          if (self.status == SVPBundleLoaded) return YES;

          self.status = SVPBundleLoading;

          // 使用路徑獲取一個NSBundle對象
          self.bundle = [NSBundle bundleWithPath:self.bundlePath];

          NSError *error = nil;

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

          // 加載NSBundle
          if (self.bundle && [self.bundle load])
          {
          self.status = SVPBundleLoaded;

          // 獲取NSBundle的principalObject
          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;
          }

          SVPBundleManager主要代碼如下:

          - (instancetype)init {
          self = [super init];
          if (self) {
          // 遵循SVPCore的協(xié)議
          [SVPAccessor defaultAccessor].bundleProvider = self;

          // 遍歷本地文件夾,加載動態(tài)庫
          _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];
          }
          // 動態(tài)庫標(biāo)識:版本號+唯一標(biāo)識
          NSString *bundleKey = [NSString stringWithFormat:@"%@_%@", version, path];
          _installedBundles[bundleKey] = bundle;
          }
          }
          }
          }
          return self;
          }

          #pragma mark - SVPBundleDownloadItemDelegate

          // 下載完成,解壓下載下來的動態(tài)庫
          - (void)downloadBundleItem:(SVPBundleDownloadItem *)downloadItem finished:(BOOL)success {
          if (success) {
          [self unZipDownloadItem:downloadItem];
          } else {
          if (self.finishBlock) {
          self.finishBlock(NO);
          self.finishBlock = nil;
          }
          }
          }

          #pragma mark - SVPBundleProviderDelegate

          // 實(shí)現(xiàn)SVPCore的協(xié)議,返回URI對應(yīng)的動態(tài)庫的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. 插件模塊

          首先創(chuàng)建一個動態(tài)庫,在創(chuàng)建工程時選Cocoa Touch Framework,如下圖:

          創(chuàng)建動態(tài)庫

          接下來將SVPCore動態(tài)庫導(dǎo)入后,創(chuàng)建一個BundleDelegate實(shí)現(xiàn)SVPCore的SVPBundleDelegate協(xié)議,代碼如下:

          // 動態(tài)庫實(shí)現(xiàn)SVPCore的協(xié)議,返回動態(tài)庫的主入口頁面
          - (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;
          }

          SVPWechatViewController,就是該插件的主入口對象,在此基礎(chǔ)上實(shí)現(xiàn)插件的獨(dú)立功能就可以了。

          然后,最重要的一步,需要在該動態(tài)庫的Info.plist文件配置Principal class,這個條目的作用是通過NSBundle的principalClass獲取到該對象,如下圖將SVPWechatBundleDelegate設(shè)置進(jìn)去之后,加載完成后的Bundle發(fā)送principalClass消息,拿到的就是這個對象。由于SVPWechatBundleDelegate實(shí)現(xiàn)了SVPBundleDelegate協(xié)議的resourceWithURI:方法,就可以將插件的入口控制器返回給調(diào)用方。

          動態(tài)庫

          之后將該插件的動態(tài)庫編譯后打成壓縮包,放到服務(wù)器上提供下載鏈接即可。

          4. 主工程

          主工程的功能相對簡單,先從Plist文件中讀取配置信息并展示(該P(yáng)list文件可從網(wǎng)絡(luò)下載):

          配置信息

          當(dāng)用戶點(diǎn)擊圖標(biāo)時先獲取圖標(biāo)信息并查看該插件動態(tài)庫是否已加載,若未加載則調(diào)用SVPBundleManager的downloadItem方法進(jìn)行下載,若已加載則調(diào)用SVPDispatch的resourceWithURI:方法獲取插件入口,進(jìn)行接下來的操作,主要代碼如下:

          // 用戶點(diǎn)擊插件
          - (void)onItemView:(UIButton *)sender {
          NSInteger itemIndex = sender.tag - 1000;
          if (itemIndex >= 0 && itemIndex < self.pluginArray.count) {
          // 點(diǎn)擊的插件對應(yīng)的配置列表信息
          PluginItem *pluginItem = [self.pluginArray objectAtIndex:itemIndex];

          // 動態(tài)庫標(biāo)識:版本號+唯一標(biāo)識,以實(shí)現(xiàn)動態(tài)更新的目的
          NSString *bundleKey = [NSString stringWithFormat:@"%@_%@", pluginItem.version, pluginItem.identifier];
          if (![[SVPBundleManager defaultManager] isInstalledBundleWithBundleKey:bundleKey])
          {
          // 本地未加載,先從服務(wù)器下載動態(tài)庫
          __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];
          });
          } else {
          // 提示下載失敗
          }
          dispatch_sync(dispatch_get_main_queue(), ^{
          [strongSender setTitle:strongItem.name forState:UIControlStateNormal];
          });
          }];
          [sender setTitle:@"下載中..." forState:UIControlStateNormal];
          }
          else
          {
          // 本地已加載,push動態(tài)庫的主入口頁面
          [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];
          }
          }
          }

          當(dāng)插件模塊需要更新時,只需要修改服務(wù)器上的配置列表和插件動態(tài)庫壓縮包,主工程在適當(dāng)?shù)臅r機(jī)更新本地配置列表,當(dāng)用戶點(diǎn)擊該插件功能時,即可根據(jù)版本號查找并更新本地動態(tài)庫,達(dá)到動態(tài)更新的目的。


          注意事項(xiàng)

          系統(tǒng)在加載動態(tài)庫時,會檢查Framework的簽名,簽名中必須包含TeamIdentifier,并且Framework和主App的TeamIdentifier必須一致。

          如果不一致,會報下面的錯誤:

          Error loading /path/to/framework: dlopen(/path/to/framework, 265): no suitable image found. Did find:/path/to/framework:code signature in (/path/to/framework) not valid for use in process using Library Validation: mapped file has no cdhash, completely unsigned? Code has to be at least ad-hoc signed.


          總結(jié)

          以上便是利用Framework動態(tài)庫進(jìn)行插件化加載以及動態(tài)更新的所有實(shí)現(xiàn),就目前而言,Apple并不希望開發(fā)者繞過App Store來更新App,因此需謹(jǐn)慎對待熱更新的使用,對于不需要上架的企業(yè)級應(yīng)用,是可以使用的。隨著蘋果開放環(huán)境的不斷發(fā)展,蘋果會不會給我們開發(fā)者驚喜呢,這就不得而知了。

          -End-

          最近有一些小伙伴,讓我?guī)兔φ乙恍?nbsp;面試題 資料,于是我翻遍了收藏的 5T 資料后,匯總整理出來,可以說是程序員面試必備!所有資料都整理到網(wǎng)盤了,歡迎下載!

          點(diǎn)擊??卡片,關(guān)注后回復(fù)【面試題】即可獲取

          在看點(diǎn)這里好文分享給更多人↓↓

          瀏覽 18
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  久久深爱网 | 国产精品剧情亚洲二区 | 乱伦精品视频 | 欧美黄色成人在线 | 亚洲色播爱爱爱 |