DSBridge原始碼分析

d_d發表於2019-03-12

dsBridge介紹

dsBridge是一個三端易用的現代跨平臺 Javascript bridge, 通過它,你可以在Javascript和原生之間同步或非同步的呼叫彼此的函式。

dsBridge之js原始碼分析

我對dsBridge中js程式碼做了詳細註釋方便大家閱讀。

var bridge = {
        default:this,// for typescript
        call: function (method, args, cb) {
            var ret = '';
            if (typeof args == 'function') {//無引數有回撥的情況
                cb = args;
                args = {};
            }
            var arg={data:args===undefined?null:args}
            if (typeof cb == 'function') {
                var cbName = 'dscb' + window.dscb++;//將方法定義為一個全域性變數,用於後面呼叫
                window[cbName] = cb;
                arg['_dscbstub'] = cbName;//將方法名儲存到arg中,用於Native端呼叫
            }
            arg = JSON.stringify(arg)
            //if in webview that dsBridge provided, call!
            if(window._dsbridge){
                ret=  _dsbridge.call(method, arg)
            }else if(window._dswk||navigator.userAgent.indexOf("_dsbridge")!=-1){//使用時Native會註冊_dswk引數
                ret = prompt("_dsbridge=" + method, arg);//調起原生程式碼(ios端會呼叫WKUIDelegate的方法)
            }

            return  JSON.parse(ret||'{}').data
        },
        //Native呼叫的方法使用此方法註冊
        register: function (name, fun, asyn) {
        //註冊的方法會儲存到_dsaf或_dsf中
            var q = asyn ? window._dsaf : window._dsf
            if (!window._dsInit) {
                window._dsInit = true;
                //notify native that js apis register successfully on next event loop
                setTimeout(function () {
                    bridge.call("_dsb.dsinit");
                }, 0)
            }
            //object型別儲存到_obs下,方法直接儲存到_dsf(_dsaf)下
            if (typeof fun == "object") {
                q._obs[name] = fun;
            } else {
                q[name] = fun
            }
        },
        registerAsyn: function (name, fun) {
            this.register(name, fun, true);
        },
        hasNativeMethod: function (name, type) {
            return this.call("_dsb.hasNativeMethod", {name: name, type:type||"all"});
        },
        disableJavascriptDialogBlock: function (disable) {
            this.call("_dsb.disableJavascriptDialogBlock", {
                disable: disable !== false
            })
        }
    };

    //立即執行函式
    !function () {
    //判斷是否需要給window進行引數新增,如果沒有新增會把ob內引數進行一次新增
        if (window._dsf) return;
        var ob = {
            _dsf: {//儲存同步方法
                _obs: {}//儲存同步方法相關object
            },
            _dsaf: {//儲存非同步方法
                _obs: {}//儲存非同步方法相關object
            },
            dscb: 0,//避免方法同名每次加1
            dsBridge: bridge,
            close: function () {
                bridge.call("_dsb.closePage")
            },
            //處理Native呼叫js方法
            _handleMessageFromNative: function (info) {
                var arg = JSON.parse(info.data);
                var ret = {
                    id: info.callbackId,
                    complete: true
                }
                var f = this._dsf[info.method];
                var af = this._dsaf[info.method]
                var callSyn = function (f, ob) {
                    ret.data = f.apply(ob, arg)
                    bridge.call("_dsb.returnValue", ret)//js方法處理完後回撥原生方法,並返回處理後的結果
                }
                var callAsyn = function (f, ob) {
                    arg.push(function (data, complete) {
                        ret.data = data;
                        ret.complete = complete!==false;
                        bridge.call("_dsb.returnValue", ret)
                    })
                    f.apply(ob, arg)
                }
                if (f) {
                    callSyn(f, this._dsf);
                } else if (af) {
                    callAsyn(af, this._dsaf);
                } else {
                    //with namespace
                    var name = info.method.split('.');
                    if (name.length<2) return;
                    var method=name.pop();
                    var namespace=name.join('.')
                    var obs = this._dsf._obs;
                    var ob = obs[namespace] || {};
                    var m = ob[method];
                    if (m && typeof m == "function") {
                        callSyn(m, ob);
                        return;
                    }
                    obs = this._dsaf._obs;
                    ob = obs[namespace] || {};
                    m = ob[method];
                    if (m && typeof m == "function") {
                        callAsyn(m, ob);
                        return;
                    }
                }
            }
        }
        //將ob所有引數賦值給window
        for (var attr in ob) {
            window[attr] = ob[attr]
        }
        bridge.register("_hasJavascriptMethod", function (method, tag) {
            var name = method.split('.')
            if(name.length<2) {
                return !!(_dsf[name]||_dsaf[name])//js用!!進行bool轉換
            }else{
                // with namespace
                var method=name.pop()
                var namespace=name.join('.')
                var ob=_dsf._obs[namespace]||_dsaf._obs[namespace]
                return ob&&!!ob[method]
            }
        })
    }();
複製程式碼

dsBridge之ios端原始碼分析

初始化

首先通過js程式碼注入為window新增_dswk用於標註是Native使用。

WKUserScript *script = [[WKUserScript alloc] initWithSource:@"window._dswk=true;"
                                              injectionTime:WKUserScriptInjectionTimeAtDocumentStart
                                           forMainFrameOnly:YES];
[configuration.userContentController addUserScript:script];
複製程式碼

註冊本地Native類並新增相應的namespacejavaScriptNamespaceInterfacesFDInternalApis類註冊了部分需要的方法,namespace_dsb

FDInternalApis *  interalApis= [[FDInternalApis alloc] init];
interalApis.webview=self;
[self addJavascriptObject:interalApis namespace:@"_dsb"];

if(object!=NULL){
    [javaScriptNamespaceInterfaces setObject:object forKey:namespace];
    NSLog(javaScriptNamespaceInterfaces.description);
}
複製程式碼

Native 呼叫 js

Native呼叫js使用方法:

[dwebview callHandler:@"addValue" arguments:@[@3,@4] completionHandler:^(NSNumber * value){
    NSLog(@"%@",value);
}];
複製程式碼

js中需要註冊相應方法(同步):

dsBridge.register('addValue', function (r, l) {
    return r + l;
})
複製程式碼

對於非同步js中的註冊方式:

dsBridge.registerAsyn('append',function(arg1,arg2,arg3,responseCallback){
     responseCallback(arg1+" "+arg2+" "+arg3);
})
複製程式碼

native端使用時會走以下方法:

NSString * json=[FDWebUtil objToJsonString:@{@"method":info.method,@"callbackId":info.id,
                                                 @"data":[FDWebUtil objToJsonString: info.args]}];
[self evaluateJavaScript:[NSString stringWithFormat:@"window._handleMessageFromNative(%@)",json]
           completionHandler:nil];
複製程式碼

js會去呼叫_handleMessageFromNative方法並對資料進行解析。 當js處理完畢會呼叫:

bridge.call("_dsb.returnValue", ret)
複製程式碼

然後native會取出儲存在handerMapblock執行:

- (id) returnValue:(NSDictionary *) args{
    void (^ completionHandler)(NSString *  _Nullable)= handerMap[args[@"id"]];
    if(completionHandler){
        if(isDebug){
            completionHandler(args[@"data"]);
        }else{
            @try{
                completionHandler(args[@"data"]);
            }@catch (NSException *e){
                NSLog(@"%@",e);
            }
        }
        if([args[@"complete"] boolValue]){
            [handerMap removeObjectForKey:args[@"id"]];
        }
    }
    return nil;
}
複製程式碼

js 呼叫 Native

var str=dsBridge.call("testSyn","testSyn");
複製程式碼

然後會執行到

ret = prompt("_dsbridge=" + method, arg);
複製程式碼

通過promptiOS端會呼叫代理方法

- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable result))completionHandler;
複製程式碼

可以看到其處理方式:

NSString * prefix=@"_dsbridge=";
if ([prompt hasPrefix:prefix])
{
    NSString *method= [prompt substringFromIndex:[prefix length]];
    NSString *result=nil;
    if(isDebug){
        result =[self call:method :defaultText ];
    }else{
        @try {
            result =[self call:method :defaultText ];
        }@catch(NSException *exception){
            NSLog(@"%@", exception);
        }
    }
    completionHandler(result);
}
複製程式碼

這裡會呼叫一個關鍵方法

-(NSString *)call:(NSString*) method :(NSString*) argStr
{
    NSArray *nameStr=[FDWebUtil parseNamespace:[method stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]];
    id JavascriptInterfaceObject= [javaScriptNamespaceInterfaces  valueForKey:nameStr[0]];
    NSString *error=[NSString stringWithFormat:@"Error! \n Method %@ is not invoked, since there is not a implementation for it",method];
    NSMutableDictionary*result =[NSMutableDictionary dictionaryWithDictionary:@{@"code":@-1,@"data":@""}];
    if(!JavascriptInterfaceObject){
        NSLog(@"Js bridge  called, but can't find a corresponded JavascriptObject , please check your code!");
    }else{
        method=nameStr[1];
        NSString *methodOne = [FDWebUtil methodByNameArg:1 selName:method class:[JavascriptInterfaceObject class]];
        NSString *methodTwo = [FDWebUtil methodByNameArg:2 selName:method class:[JavascriptInterfaceObject class]];
        SEL sel=NSSelectorFromString(methodOne);
        SEL selasyn=NSSelectorFromString(methodTwo);
        NSDictionary * args=[FDWebUtil jsonStringToObject:argStr];
        NSString *arg = [args safeObjectForKey:@"data"];
        NSString * cb;
        do{
            if(args && (cb= args[@"_dscbstub"])){
                if([JavascriptInterfaceObject respondsToSelector:selasyn]){
                    void (^completionHandler)(id,BOOL) = ^(id value,BOOL complete){
                        NSString *del=@"";
                        result[@"code"]=@0;
                        if(value!=nil){
                            result[@"data"]=value;
                        }
                        value=[FDWebUtil objToJsonString:result];
                        value=[value stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
                        
                        if(complete){
                            del=[@"delete window." stringByAppendingString:cb];
                        }
                        NSString*js=[NSString stringWithFormat:@"try {%@(JSON.parse(decodeURIComponent(\"%@\")).data);%@; } catch(e){};",cb,(value == nil) ? @"" : value,del];
                        
                        @synchronized(self)
                        {
                            UInt64  t=[[NSDate date] timeIntervalSince1970]*1000;
                            self->jsCache=[self->jsCache stringByAppendingString:js];
                            if(t-self->lastCallTime<50){
                                if(!self->isPending){
                                    [self evalJavascript:50];
                                    self->isPending=true;
                                }
                            }else{
                                [self evalJavascript:0];
                            }
                        }
                        
                    };
                    SuppressPerformSelectorLeakWarning(
                                                       [JavascriptInterfaceObject performSelector:selasyn withObject:arg withObject:completionHandler];
                                                       
                                                       );
                    
                    break;
                }
            }else if([JavascriptInterfaceObject respondsToSelector:sel]){
                id ret;
                SuppressPerformSelectorLeakWarning(
                                                   ret=[JavascriptInterfaceObject performSelector:sel withObject:arg];
                                                   );
                
                [result setValue:@0 forKey:@"code"];
                if(ret!=nil){
                    [result setValue:ret forKey:@"data"];
                }
                break;
            }
            NSString*js=[error stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
            if(isDebug){
                js=[NSString stringWithFormat:@"window.alert(decodeURIComponent(\"%@\"));",js];
                [self evaluateJavaScript :js completionHandler:nil];
            }
            NSLog(@"%@",error);
        }while (0);
    }
    return [FDWebUtil objToJsonString:result];
}
複製程式碼

裡面會去取兩個方法methodOnemethodTwo,當方法帶block則會得到methodTwo,否則得到methodOne

//return method name for xxx: or xxx:handle:
+(NSString *)methodByNameArg:(NSInteger)argNum selName:(NSString *)selName class:(Class)class
{
    NSString *result = nil;
    if(class){
        NSArray *arr = [FDWebUtil allMethodFromClass:class];
        for (int i=0; i<arr.count; i++) {
            NSString *method = arr[i];
            NSArray *tmpArr = [method componentsSeparatedByString:@":"];
            if ([method hasPrefix:selName]&&tmpArr.count==(argNum+1)) {
                result = method;
                return result;
            }
        }
        if (result == nil) {
            NSArray *arr = [FDWebUtil allMethodFromSuperClass:class];
            for (int i=0; i<arr.count; i++) {
                NSString *method = arr[i];
                NSArray *tmpArr = [method componentsSeparatedByString:@":"];
                if ([method hasPrefix:selName]&&tmpArr.count==(argNum+1)) {
                    result = method;
                    return result;
                }
            }
        }
    }
    return result;
}
複製程式碼

帶非同步方法的會執行因為args[@"_dscbstub"]不為空會帶定義的completionHandler執行:

SuppressPerformSelectorLeakWarning(
                                                       [JavascriptInterfaceObject performSelector:selasyn withObject:arg withObject:completionHandler];
複製程式碼

completionHandler中會每50毫秒呼叫一次evalJavascript

NSString*js=[NSString stringWithFormat:@"try {%@(JSON.parse(decodeURIComponent(\"%@\")).data);%@; } catch(e){};",cb,(value == nil) ? @"" : value,del];
複製程式碼
[self evalJavascript:50];
複製程式碼

會執行到非同步方法,從而完成回撥。

相關文章