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

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

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

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

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

前言

上一篇在實現通訊的基礎上,咱們還實現了對native端功能的插件化,本篇延續上一篇主要從插件配置做爲切入點進行分析。提及插件配置,native端Cordova框架基於一個xml格式的配置文件,在xml裏面配置咱們編寫的插件相關信息。這種方式在咱們實際開發應用中報漏出不少弊端,咱們每每有的時候就忘記了插件怎樣配置,以致於咱們每開發出一個插件還要從新分析對應關係,由於它畢竟不像咱們寫業務代碼那麼頻繁,時間久了一些配置的事情就可能會忘記。那麼它第二個弊端就是咱們配置過的插件,一旦插件再也不使用或者插件類名等信息有所修改,咱們還要進入xml中進行從新配置,或者進行增刪的操做。固然Cordova中配置信息不刪除也不影響框架使用,但那不是咱們想要的,咱們就想插件拔出去了,那工程裏就不要再有和它相關的東西了。前端

在咱們平時開發中,xml文件的方式去配置信息應該是大部分人最後的選擇,在這以前咱們能夠選擇json文件,也能夠選擇plist文件,甚至能夠選擇+load中去配置,固然+load中配置咱們還要考慮是否須要異步加載是否會影響應用啓動性能等等。linux

就在我苦思冥想一想要找個更好的方式註冊插件的時候,恰巧前天美團技術團隊發了篇文章《iOS App冷啓動治理:來自美團外賣的實踐》,恰巧我又看了裏面的第五條,靈感來了。那麼本篇插件配置我選擇了另外一種方式,經過宏來進行,它的好處就是操做簡單,看着直觀,不與其餘產生耦合,也解決了上面所提到的痛點。固然這只是我本身的想法,主要也是依照一篇的思想來進行設計,確定還有不少欠妥或者更優解,也但願大佬們能多指導。git

目錄

  • 插件註冊過程
  • 經常使用插件預加載

1、插件註冊過程

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

在正式進行插件註冊以前,咱們先學習一個知識,說實話這個知識咱們平時工做很難遇到,可是它就恰恰能解決咱們工做中遇到的難題,web

編譯器編譯代碼後生成的文件叫作目標文件,從文件結構上來說,它已是可執行的目標文件了,咱們的程序要跑起來,那麼它的可執行文件的格式要能被操做系統所理解,好比ELF是linux下可執行文件的格式,PE32是windows下的可執行文件格式,那麼對於iOS來講Mach-O就是它可執行文件的格式。關於Mack-O的分析網上文章不少,也很深不是很容易理解,因此這裏就不詳細探討了,實際上Mack-O可執行文件中有不少段,其中一個就是咱們要用到的數據段,也就是說咱們註冊的插件信息要存儲在Mack-O可執行文件的data段中。clang編譯器提供了不少編譯器函數,其中就有section()函數,section()函數就提供了二進制段的讀寫能力,它能夠將一些編譯期就能夠肯定的常量寫入到數據段中。那麼咱們就利用這個能力在編譯期將插件信息寫進數據段。而後找一個合適的節點,咱們再將數據段中存儲的註冊了的插件信息取出來完成註冊過程。json

基於此,框架內也封裝了一個宏供插件使用:windows

#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了,粘下代碼:bash

#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段的數據。這樣咱們就完成了整個過程。

2、經常使用插件預加載

有些經常使用的或者首屏渲染須要用到的可能咱們要特殊處理下,好比標題所說的預加載,也就是插件對象提早初始化,而不是在調用的時候再初始化。基於這個想法,在插件註冊的時候,除了註冊了插件名字之外,還額外增長了一個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.性能調優 (有一些優化,可是這一塊坑很深)

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

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

參考

《Mach-O文件介紹之loadcommand》

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

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

相關文章
相關標籤/搜索