寫一個易於維護使用方便效能可靠的Hybrid框架(三)—— 配置外掛

Sevin發表於2018-12-09

《寫一個易於維護使用方便效能可靠的Hybrid框架(一)—— 思路構建》

《寫一個易於維護使用方便效能可靠的Hybrid框架(二)—— 外掛化》

《寫一個易於維護使用方便效能可靠的Hybrid框架(三)—— 配置外掛》

《寫一個易於維護使用方便效能可靠的Hybrid框架(四)—— 框架構建》

前言

上一篇在實現通訊的基礎上,我們還實現了對native端功能的外掛化,本篇延續上一篇主要從外掛配置作為切入點進行分析。說起外掛配置,native端Cordova框架基於一個xml格式的配置檔案,在xml裡面配置我們編寫的外掛相關資訊。這種方式在我們實際開發應用中報漏出很多弊端,我們往往有的時候就忘記了外掛怎樣配置,以至於我們每開發出一個外掛還要重新分析對應關係,因為它畢竟不像我們寫業務程式碼那麼頻繁,時間久了一些配置的事情就可能會忘記。那麼它第二個弊端就是我們配置過的外掛,一旦外掛不再使用或者外掛類名等資訊有所修改,我們還要進入xml中進行重新配置,或者進行增刪的操作。當然Cordova中配置資訊不刪除也不影響框架使用,但那不是我們想要的,我們就想外掛拔出去了,那工程裡就不要再有和它相關的東西了。

在我們平時開發中,xml檔案的方式去配置資訊應該是大部分人最後的選擇,在這之前我們可以選擇json檔案,也可以選擇plist檔案,甚至可以選擇+load中去配置,當然+load中配置我們還要考慮是否需要非同步載入是否會影響應用啟動效能等等。

就在我苦思冥想想要找個更好的方式註冊外掛的時候,恰巧前天美團技術團隊發了篇文章《iOS App冷啟動治理:來自美團外賣的實踐》,恰巧我又看了裡面的第五條,靈感來了。那麼本篇外掛配置我選擇了另一種方式,通過巨集來進行,它的好處就是操作簡單,看著直觀,不與其他產生耦合,也解決了上面所提到的痛點。當然這只是我自己的想法,主要也是依照一篇的思想來進行設計,肯定還有很多欠妥或者更優解,也希望大佬們能多指導。

目錄

  • 外掛註冊過程
  • 常用外掛預載入

一、外掛註冊過程

那我們分析階段就到這裡,接下來進入正題,其實程式碼沒有多少,主要是想在造輪子的同時提煉好的思想,踩在巨人肩膀上不斷完善。

在正式進行外掛註冊之前,我們先學習一個知識,說實話這個知識我們平時工作很難遇到,但是它就偏偏能解決我們工作中遇到的難題,

編譯器編譯程式碼後生成的檔案叫做目標檔案,從檔案結構上來講,它已經是可執行的目標檔案了,我們的程式要跑起來,那麼它的可執行檔案的格式要能被作業系統所理解,比如ELF是linux下可執行檔案的格式,PE32是windows下的可執行檔案格式,那麼對於iOS來說Mach-O就是它可執行檔案的格式。關於Mack-O的分析網上文章很多,也很深不是很容易理解,所以這裡就不詳細探討了,實際上Mack-O可執行檔案中有很多段,其中一個就是我們要用到的資料段,也就是說我們註冊的外掛資訊要儲存在Mack-O可執行檔案的data段中。clang編譯器提供了很多編譯器函式,其中就有section()函式,section()函式就提供了二進位制段的讀寫能力,它可以將一些編譯期就可以確定的常量寫入到資料段中。那麼我們就利用這個能力在編譯期將外掛資訊寫進資料段。然後找一個合適的節點,我們再將資料段中儲存的註冊了的外掛資訊取出來完成註冊過程。

寫一個易於維護使用方便效能可靠的Hybrid框架(三)—— 配置外掛

基於此,框架內也封裝了一個巨集供外掛使用:

#define SHRMWebPlugins "SHRMWebPlugins"
#define SHRMWebPluginDATA(sectname) __attribute((used, section("__DATA,"#sectname" ")))
#define SHRMRegisterWebPlugin(servicename,impl) \
class SHRMWebViewEngine;char * k##servicename##_service SHRMWebPluginDATA(SHRMWebPlugins) = "{ \""#servicename"\" : \""#impl"\"}";
複製程式碼

如果有人看過蜂巢的程式碼或許會很熟悉,但畢竟不是所有人都看過,還是詳細說一下這個巨集的解釋,__attribute((used, section("__DATA,"#sectname" ")))表示在專案的Mach-o檔案的名字為__DATAsegment中新增一個名字為sectnamesection,並將其值設定為字串"{ \""#servicename"\" : \""#impl"\"}"__attribute第一個引數used,它的作用是告訴編譯器,我宣告的這個符號是需要保留的。被used修飾以後,意味著即使函式沒有被引用,在Release下也不會被優化。如果不加這個修飾,那麼Release環境連結器會去掉沒有被引用的段。當然這肯定不是我們想要的。這段巨集目的只有一個,那就是在編譯期將servicenameimpl通過section()函式寫入到資料段中儲存。

如果上面的理解了,那麼我們接下來的使用就很容易了。我們在把native端外掛編寫好後,只需要加一個@SHRMRegisterWebPlugin()就可以了,別的什麼都不需要做,不需要引入標頭檔案,不需要解析配置檔案,只需要加一行程式碼就OK了,粘下程式碼:

#import "SHRMMsgCommand.h"
@interface SHRMFetchPlugin : NSObject
- (void)nativeFentch:(SHRMMsgCommand *)command;
@end

@SHRMRegisterWebPlugin(SHRMFetchPlugin, 0)
@implementation SHRMFetchPlugin
- (void)nativeFentch:(SHRMMsgCommand *)command {
    NSString *method = [command argumentAtIndex:0];
    NSString *url = [command argumentAtIndex:1];
    NSString *param = [command argumentAtIndex:2];
    NSLog(@"(%@):%@,%@,%@",command.callbackId, method, url, param);
    [command.delegate sendPluginResult:@"fetch success" callbackId:command.callbackId];
}
@end
複製程式碼

這還是基於上一篇我們模擬的fetch外掛,實際上如果我們要把fetch外掛註冊到我們的Hybird框架中,只需要在外掛裡面寫入@SHRMRegisterWebPlugin(SHRMFetchPlugin, 0)這行程式碼就可以了,沒有配置檔案,沒有配置檔案,沒有配置檔案

那註冊完了,框架內是怎麼使用註冊好的外掛的呢?帶著這個問題,再看下我在SHRMWebViewEngine中做的事情。

- (instancetype)init {
    if (self = [super init]) {
        _webViewhandleFactory = [[SHRMWebViewHandleFactory alloc] initWithWebViewEngine:self];
        _webViewDelegate = [[SHRMWebViewDelegate alloc] initWithWebViewEngine:self];
        _webPluginAnnotation = [[SHRMWebPluginAnnotation alloc] initWithWebViewEngine:self];
        _pluginObject = [NSMutableDictionary dictionary];
        [self loadStartupPlugin];
    }
    return self;
}

- (void)loadStartupPlugin {
    [_webPluginAnnotation getAllRegisterPluginName];
}
複製程式碼

在使用框架的人呼叫了init的時候,我基於上一篇新增了loadStartupPlugin方法,實際上註冊過的外掛載入的過程都在SHRMWebPluginAnnotation裡面實現的,看下程式碼:

- (void)getAllRegisterPluginName {
    _dyld_register_func_for_add_image(dyld_callback);
}

static void dyld_callback(const struct mach_header *mhp, intptr_t vmaddr_slide) {
    NSArray<NSString *> *services = ReadConfiguration(SHRMWebPlugins,mhp);
    for (NSString *map in services) {
        NSData *jsonData =  [map dataUsingEncoding:NSUTF8StringEncoding];
        NSError *error = nil;
        id json = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
        //省略了部分程式碼...
        //執行註冊的外掛處理...
    }
}

NSArray<NSString *>* ReadConfiguration(char *sectionName,const struct mach_header *mhp) {
    NSMutableArray *configs = [NSMutableArray array];
    unsigned long size = 0;
#ifndef __LP64__
    uintptr_t *memory = (uintptr_t*)getsectiondata(mhp, SEG_DATA, sectionName, &size);
#else
    const struct mach_header_64 *mhp64 = (const struct mach_header_64 *)mhp;
    uintptr_t *memory = (uintptr_t*)getsectiondata(mhp64, SEG_DATA, sectionName, &size);
#endif
    
    unsigned long counter = size/sizeof(void*);
    for(int idx = 0; idx < counter; ++idx){
        char *string = (char*)memory[idx];
        NSString *str = [NSString stringWithUTF8String:string];
        if(!str)continue;
        
        NSLog(@"config = %@", str);
        if(str) [configs addObject:str];
    }
    return configs;
}
複製程式碼

對於這段程式碼,看過蜂巢原始碼的會比較熟悉,這裡我們需要了解兩件事情,_dyld_register_func_for_add_image函式當dyld連結符號時,就會呼叫此回撥函式,現在我選擇在執行時手動呼叫。getsectiondata()拿到的menory就是我們上面利用section()函式儲存在data段的資料。這樣我們就完成了整個過程。

二、常用外掛預載入

有些常用的或者首屏渲染需要用到的可能我們要特殊處理下,比如標題所說的預載入,也就是外掛物件提前初始化,而不是在呼叫的時候再初始化。基於這個想法,在外掛註冊的時候,除了註冊了外掛名字以外,還額外增加了一個onload引數,如果onload為0,那麼認為這個外掛不需要提前初始化,在呼叫的時候在初始化就可以,如果為1那麼就認為需要在webView載入的時候就把外掛也跟著初始化。看程式碼:

- (void)registerStartupPluginName:(NSString *)pluginName onload:(NSNumber *)onload {
    if ([onload boolValue]) {
        [self getCommandInstance:pluginName];
    }
}

- (id)getCommandInstance:(NSString*)pluginName {
    id obj = [_pluginObject objectForKey:[pluginName lowercaseString]];
    if (!obj) {
        obj = [[NSClassFromString(pluginName) alloc] init];
        if (obj != nil) {
            [_pluginObject setObject:obj forKey:[pluginName lowercaseString]];
        }else {
            NSLog(@"(pluginName: (%@) does not exist.", pluginName);
        }
    }
    return obj;
}
複製程式碼

通過程式碼就很直觀,如果[onload boolValue]那麼才會往下執行,pluginObject為外掛例項的快取可變字典,一旦有快取,那麼外掛直接在快取取,而不必每次呼叫都要初始化一個例項出來。

總結

目前為止hybrid框架文章已經是第三篇了,不管是總結也好還是學習也好,挺費精力的,那麼到現在,我們的hybrid框架在native端已經具備了開篇所講的功能:

  • 1.外掛化(native端實現,js端還未實)
  • 2.可配置性 (native端實現,js端還未實現)
  • 3.前端介面統一 (還未實現)
  • 4.通訊基於WKWebView (已實現)
  • 5.效能調優 (有一些優化,但是這一塊坑很深)

寫一個易於維護使用方便效能可靠的Hybrid框架(三)—— 配置外掛

第三篇就到這裡了,native端也基本完成了,目前是五個類外加一個介面。後續除了優化以外不會再進行native的工作了,後續這個hybird輪子主要設計工作是前端方向了。

程式碼已經上傳到github 《SHRMJavaScriptBridge》,歡迎issue,歡迎star。

參考

《Mach-O檔案介紹之loadcommand》

《iOS App冷啟動治理:來自美團外賣的實踐》

《BeeHive —— 一個優雅但還在完善中的解耦框架》

相關文章