金沙澳门官网jin5888:记一次印象深刻的Bug追踪过程,WebView与JS互相调用

问题现象:使用安卓手机以小程序的形式分享产品到微信,使用微信打开,产品详情数据无法显示。而使用iPhone分享到微信,却始终可以正常打开,这个时候所有的矛头都指向了安卓同学。

前言:

这是本人的第一篇博客,有不正确或者不规范之处,敬请见谅!回归正文,由于最近在做Android端与H5界面之间的交互,之前没做过类似的功能,在网上找了很多资料,说法不一,而且实现起来也各有不同。
  我在做功能之前,自己动手做了一个小Demo,当时是用的H5界面中的JavaScript调用Android端的方法进行实现,这个方法是可行的,也就是此文的方法二。
  但是在实际开发中,由于要和IOS统一,
而IOS那边当时想用url的方式,也就是此文要说的方法一,开始他说IOS端做不到用我说的方法二(即调用方法的方式),此时又不能说让web端实现两套代码或者实现判断,这毕竟是不好的。所以我们一开始就选择了通过url的方式。
  后来他通过查阅资料,发现也可以通过调用方法的方式去实现。所以最后的总结就是,方法一和方法二都是可以使用的,但我们开发的时候选择了方法一。下面我们就从方法一开始谈起。

在iOS应用的开发过程中,我们经常会使用到WebView,当我们对WebView进行操作的时候,有时会需要进行源生的操作.那么我记下来就与大家分享一下OC与JS交互.

小程序中打开,显示空白

方法一:

项目地址:https://github.com/fsrmeng/WebView-Master
  其实通过url的方式,就是相当于定义协议规则,也就是条件,我们拿到这个条件,进行判断,实现自己的逻辑代码。这里有一个关键的web前端代码:
window.location.href=“url”,此处的url就是我们定义的协议规则。一旦H5走到上段代码,Android
WebView就会调用shouldOverrideUrlLoading(WebView view, String
url)

  我们定义了url
“app://showgame.toast?”+JSON.stringify(json),而通过获取到app这个字段来判断是否是http协议,这里的json其实就是传递过来的参数,代码如下:

mWebView.setWebViewClient(new WebViewClient() {
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                try {
                    //这一步很关键,web端传过来的有可能是其它编码格式
                    url = URLDecoder.decode(url, "utf-8");

                    //通过判断拦截到的url是否含有pre,来辨别是http请求还是调用android方法的请求
                    String[] parts = url.split("[?]");
                    String code = parts[0];

                    String pre = "app://";
                    if (!url.contains(pre)) {
                        //该url是http请求,用webview加载url
                        view.loadUrl(url);
                        return true;
                    }

                    //该url是调用android方法的请求,通过解析url中的参数来执行相应方法
                    String params = JavaScriptManager.getJSParams(url, code);
                    //在这里实现各种android方法的逻辑代码
                    JavaScriptManager.invokeAndroidMethod(mContext, code, params, mWebView);
                    return true;

                } catch (Exception e) {
                    e.printStackTrace();
                    view.loadUrl(url);
                    return true;
                }
            }
        });

这里我创建了一个管理类JavaScriptManager,并在其中创建了两个方法,一个是getJSParams(url,
code)
,另一个是invokeAndroid(mContext, code, params, mWebView)
  接下来再看JavaScriptManager类两个方法的实现:

/**
     * 获取JS传来的参数
     * @param url
     * @param pre
     * @return
     */
    public static String getJSParams(String url, String pre) {
        String params = "";
        if (url.contains(pre)) {
            int index = url.indexOf(pre);
            int end = index + pre.length();
            params = url.substring(end + 1);
        }
        return params;
    }

/**
     *JS调用Android中的方法,根据code去判断调用具体的方法
     * @param mContext
     * @param code
     * @param params
     * @param mWebView
     */
    public static void invokeAndroidMethod(Context mContext, String code, String params, final WebView mWebView) {
        if (code.equals("app://showgame.toast")) {
            try {
                JSONObject json = new JSONObject(params);
                String toast = json.optString("data");
                Toast.makeText(mContext, toast, Toast.LENGTH_SHORT).show();
            } catch (JSONException e) {
                e.printStackTrace();
            }
            return;
        }else if (code.equals("app://showgame.getHotelData")) {
            try {
                final JSONObject json = new JSONObject(params);
                final String callback = json.optString("callback");
                json.put("hotel_name", "维多利亚大酒店");
                json.put("order_status", "已支付");
                json.put("orderId", "201612291809626");
                json.put("seller", "携程");
                json.put("expire_time", "2017年1月6日 23:00");
                json.put("price", "688.0");
                json.put("back_price", "128.0");
                json.put("pay_tpye", "支付宝支付");
                json.put("room_size", "3间房");
                json.put("room_count", "3");
                json.put("in_date", "2017年1月6日 12:00");
                json.put("out_date", "2017年1月8日 12:00");
                json.put("contact", "赵子龙先生");
                json.put("phone", "18888888888");
                json.put("server_phone", "0755-85699309");
                json.put("address", "深圳市宝安区兴东地铁站旁边");

                invokeJavaScript(mContext, callback, json.toString(), mWebView);
            } catch (JSONException e) {
                e.printStackTrace();
            }
            return;
        }
    }

这里封装的方法很实用,以后诸如类似的JS调用Android端方法的话,就可以定义类似的协议,从而只需要在invokeAndroidMethod(Context
mContext, String code, String params, final WebView
mWebView)方法中
,根据code,通过协议判断,从而实现不同的逻辑代码,也就实现了JS调用Android端不同的方法。
注意:这里还有一点需要特别说明,那就是我们需要让自己的WebView支持JS:

mWebView.getSettings().setJavaScriptEnabled(true);

其实开发中遇到的远没有这么简单,开始是写了Demo能运行,但是一旦和web前端联调的时候,总是会出现各种各样的问题,这里将总结以下几点:
  1.运行起来后,发现并不会走shouldOverrideUrlLoading()方法,这时候不要怀疑自己的程序出现问题了,因为既然Demo能运行起来,那就肯定不是我们这边的问题,而是web端的问题(即使浏览器、IOS或者部分安卓手机能运行),其实遇到这个问题,我们也是研究了很长时间,但是最终找到了问题的所在,就是可能web端需要把所有的代码都写在一个HTML文件中,而不是通过引用JS文件的方式,最好也不要用什么框架,但是问题所存在的原因,我不知道,因为我并不懂web端,其实web端同事也不知道!
  2.解决了第一个问题之后,应该就能调用我们上面说得这个方法了,但是此时可能会发现传过来的参数中文乱码了。当时web端通过各种编码格式,都行不通,后来我在我这边拿到这个url,进行utf-8编码,这样就不会乱码了。也就是我在shouldOverrideUrlLoading()金沙澳门官网jin5888:记一次印象深刻的Bug追踪过程,WebView与JS互相调用。中写得:

//这一步很关键,web端传过来的有可能是其它编码格式
url = URLDecoder.decode(url, "utf-8");

首先先说第一种方法,并没有牵扯OC与JS交互,只是做拦截和跳转.

逻辑设计说明:这里的分享数据来自H5接口,通过addJavascriptInterface自定义接口完成H5和Java端的数据传递,产品ID来自后台接口获取。

写在最后

本文提供了Android
WebView与JS互相调用的方法一,方法二将在我接下来的博客中展示,敬请期待!

拦截跳转的URL,跳转源生界面(用起来感觉怪怪的,万一URL更换了怎么办.)

这个时候,安卓同学首先做出了响应,通过调试拿到了JS端的数据,以下是这位小陈同学的截图消息:

UIWebView
//UIWebViewDelegate
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    NSString *url = request.URL.absoluteString;
    if ([url rangeOfString:@"需要跳转源生界面的URL判断"].location != NSNotFound) {
        //跳转原生界面
        return NO;
    }
    return YES;
}

Android调试结果

WKWebView
//使用WKWebview需要导入WebKit
#import <WebKit/WebKit.h>

//WKNavigationDelegate
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    NSString *url = navigationAction.request.URL.absoluteString;
    if ([url rangeOfString:@"需要跳转源生界面的URL判断"].location != NSNotFound) {
        //跳转原生界面

        //Cancel the navigation
        decisionHandler(WKNavigationActionPolicyCancel);
        return;
    }
    decisionHandler(WKNavigationActionPolicyAllow);
}

金沙澳门官网jin5888:记一次印象深刻的Bug追踪过程,WebView与JS互相调用。小陈同学这个时候把问题抛给了Web前端同学小徐,以为小徐传递了科学计数法的ID字符串。

———-✨↓↓↓↓↓↓✨———-

大家看小陈同学的截图,图中的ID是使用字符串接收的,这个时候我已经完全排除问题出现在安卓端的可能性了。于是,我问小徐,H5有对参数进行处理吗?得到的答案如下:

OC与JS交互(WebView监听事件)

正入主题.

大家看到图中,我已经给出了确定的答案,认为问题来自于后台。因为,后台同学之前的确出现过对ID进行toInt处理最终转换为负数的情况。现在在传递时出现这种低级错误的概率应该也挺高的。这段话抛出去之后,团队炸开了锅,有同学认为大家在互相推诿…

一.OC调用JS

其实,还有很长的截图,这里没有展示出来。群里提到最多的一句话就是:iOS没问题啊。就连我们的运维同学以及UI设计同学都加入了“讨伐”队伍,种种迹象似乎都指向了安卓同学。这个时候,我们的安卓同学真是“哑巴吃黄连,有苦说不出”,心里的潜台词肯定是:我TM的就用string接收了一下,我招谁惹谁了我!

1.UIWebView

①直接运行

NSString *jsStr = @"执行的JS代码";
[webView stringByEvaluatingJavaScriptFromString:jsStr];

②使用JavaScriptCore框架

#import <JavaScriptCore/JavaScriptCore.h>  

- (void)webViewDidFinishLoad:(UIWebView *)webView {
    //获取webview中的JS内容
    JSContext *context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
    NSString *runJS = @"执行的JS代码";
    //准备执行的JS代码
    [context evaluateScript:runJS];
}

但其实出现这种不知所踪的情况,完全可以理解,大家大都集中在单一平台开发,对于其它环节的理解难免有偏差。其实,用常识来理解这个问题的话,的确后台的概率比较大,前端同学对ID进行运算处理的概率几乎为0,这一点即使是刚刚入行的新手也不太可能。而我一直苦等的后台同学却迟迟没有响应,我目前始终无法确定问题到底来自于后台还是Web前端。直到我终于看到了下面的截图。

2.WKWebView
[webView evaluateJavaScript:@"执行的JS代码" completionHandler:^(id _Nullable response, NSError * _Nullable error) {

}];

这个时候,我终于有九成的把握确定问题来自于Web前端了。可是,我知道我不能明说。前端同学已经在聊天记录中给出了证据,在Chrome的控制台打印出了正常的id值,到了安卓端却出现了异常。前端同学这个时候心里也有了一个定性结论,问题来自安卓端。这个时候,我只能亲自上场,而恰好我在外面,正在办理深圳户口,比较不便。于是,我微信给小陈发消息,嘱咐它把详情页的源码“爬”下来,我回来看看源码。

金沙澳门官网jin5888:记一次印象深刻的Bug追踪过程,WebView与JS互相调用。金沙澳门官网jin5888:记一次印象深刻的Bug追踪过程,WebView与JS互相调用。二.JS调用OC ✨✨✨✨✨✨

当网页触发某种操作,可以给App传递消息.比如WebView中购买某样东西,点击购买,需要获取这件商品的订单信息,并且需要App进行源生的支付.
这种方法需要你和后台或者前端协商好一下,让他们在执行JS方法的时候,将你需要的数据放到你能拿到的位置.

下面简单贴一个HTML文件.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>App与WebView交互</title>
</head>
<body>
<button style="width: 100%; height: 100px;" onclick="buttonClick()">点击购买</button>
</body>
<script>
    //按钮点击事件
    function buttonClick() {
        //传递的信息
        var jsonStr = '{"id":"666", "message":"我是传递的数据"}';

        //UIWebView使用
        getMessage(jsonStr);

       //WKWebView使用
       //使用下方方法,会报错,为使界面执行逻辑通畅,因此使用try-catch
        try {
            window.webkit.messageHandlers.getMessage.postMessage(jsonStr)
        } catch(error) {
            console.log(error)
        }
    }
    function getMessage(json){
        //空方法
    }
</script>
</html>

window.webkit.messageHandlers.<方法名>.postMessage(<数据>)
JS端写此方法的盆友可能会报错,导致界面逻辑无法进行,因此使用try-catch就好了.

我在网页上只写了一个按钮.点击按钮,会触发buttonClick()方法.

回到家的时候,我问小陈html源码是否已经“爬”了下来,他给我发来截图,我意识到前端使用了https协议,没法获取html源码。于是,我想了一个办法,在源码中嵌入一段代码,通过代码的形式获取WebView产品详情页的数据。这个方法果然奏效,不一会儿,小陈就发来了页面的html源码。

金沙澳门官网jin5888:记一次印象深刻的Bug追踪过程,WebView与JS互相调用。UIWebView

在网页加载完成的时候检测JS方法执行.

- (void)webViewDidFinishLoad:(UIWebView *)webView {
    //核心方法如下
    JSContext *content = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
    //此处的getMessage和JS方法中的getMessage名称一致.
    content[@"getMessage"] = ^() {
        NSArray *arguments = [JSContext currentArguments];
        for (JSValue *jsValue in arguments) {
            NSLog(@"=======%@",jsValue);
        }
    };
}

由上方方法,当JS方法getMessage()执行的时候,此方法回调的jsValue内容就是我们需要的内容.(HTML中JS传递的数据)

WebView中的getMessage与HTML文件JS方法的getMessage名称需保持一致.

哎哟,我的天哪!混淆后的代码简直不堪入目,不过还好,我可以搜索方法关键字showShareView。可是,很遗憾没有搜索到,事件的绑定被放到了JS代码中。在这段源码中,我注意到一个文件名已经被混淆的JS文件,我猜想代码应该就在这里。可是,怎样抓到具体的方法呢?

WKWebView

实现WKScriptMessageHandler的代理方法.

//设置addScriptMessageHandler与JS对应方法名.并且设置<WKScriptMessageHandler>协议与协议方法
[[_webView configuration].userContentController addScriptMessageHandler:self name:@"getMessage"];

//WKScriptMessageHandler协议方法
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    //code
    NSLog(@"name = %@, body = %@", message.name, message.body);
}

上方-addScriptMessageHandler:name:方法中name填写的方法名必须与window.webkit.messageHandlers.<方法名>.postMessage(<数据>)中的方法名一致.

当JS端执行window.webkit.messageHandlers.<方法名>.postMessage(<数据>).
此协议方法就会被执行.,根据message.name判断一下自己需要执行哪步操作.message.body即是传输的参数信息.(HTML中JS传递的数据)

灵机一动!我之前在代码中让小陈把Debug权限开发给了H5,这次正好可以派上用场。可是,对于混淆后的代码,我心里依然有点打退堂鼓。

WKWebView 内存泄露

但是这样WebView所在的ViewController的- (void)dealloc{}不执行.那么内存又有问提了.
可以另外创建一个代理对象,然后通过代理对象回调指定的self.

//.h
@interface WeakScriptMessageDelegate : NSObject<WKScriptMessageHandler>

@property (nonatomic, assign) id<WKScriptMessageHandler> scriptDelegate;

- (instancetype)initWithDelegate:(id<WKScriptMessageHandler>)scriptDelegate;

@end


//.m
@implementation WeakScriptMessageDelegate

- (instancetype)initWithDelegate:(id<WKScriptMessageHandler>)scriptDelegate {
    self = [super init];
    if (self) {
        _scriptDelegate = scriptDelegate;
    }
    return self;
}

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    [self.scriptDelegate userContentController:userContentController didReceiveScriptMessage:message];
}

@end

设置代理

[[_webView configuration].userContentController addScriptMessageHandler:[[WeakScriptMessageDelegate alloc] initWithDelegate:self] name:@"方法名"];

WebView的ViewController的- (void)dealloc{}方法中进行销毁.

- (void)dealloc {
    ...
    [[_webView configuration].userContentController removeScriptMessageHandlerForName:@"方法名"];
    ...
}

连上手机,在Chrome浏览器中输入chrome://inpsect,点击相应链接,非常顺利地进入了调试界面:

———-END

当然,安卓开发的盆友也是可以通过这种方式从中获取网页的数据的.安卓注入的接口名称在JS中也是会报错的.因此也需要try-catch.

好了.以上就是与大家分享的OC与JS交互(WebView监听事件).有不足之处还请各位大佬指出.万分感激~

在控制台的Source中,我通过关键词搜索找到了混淆后的JS代码片段,在方法名前面增加了一个断点,等调试到底方法位置的时候。这个时候已经获取到了JS的上下文,直接通过this.gid打印出了当前产品ID信息,居然是一个非常正常的整型数字。大家注意,这已经是一个在安卓端出问题的产品了,在JS端居然显示是正常的。这个时候,我的大脑非常转动,我的第一感觉应该是webkit内核看到接收的字符串全是数字做了”自以为是“的转换。于是,我给出了团队如下的答案:

为了进一步确定我的猜想,我让小陈写了一个简单的Demo,通过JS接口传递一个非常大的数字字符串给Java端,看接收是否异常。不一会儿,我就得到了答案:

至此,我终于基本确定问题的原因了!
猜测:JS在传递数据给安卓端的时候,应该是使用了基本数据类型。而webkit内核在处理的时候可能是以JS端数据类型为准,在传递到Java端时候做了转换。

为了验证这个猜想,我使用typeof打印id的数据类型,得到了如下结果:

于是,我告诉小徐,问题来自于你没有传递正确的数据类型给安卓端。其实这是比较危险的,不同CPU可以容纳的最大整型值是不一样的。如果iOS端和安卓处理一致,也是以JS端数据类型为准,只不过iOS的CPU字节宽度较大,恰好在iPhone高端机型上面没有出现而低端机型出现的话。其实问题依然存在,而如果iOS的确是以Native端数据类型为准。这就根本不是一个问题。但答案虽然给了团队,可是小徐仍然一脸狐疑,没有经验的CTO也是跟着一脸狐疑,加上解决问题的时间较长。小徐在发布更新的时候也遇到了问题,导致更新失败,问题持续,整个问题一直在持续。

这个时候,我告诉小徐,你发布更新后先别着急,确定更新成功后再告诉团队小伙伴。

一直到确定更新成功,我们再次尝试分享,问题终于引刃而解!

相关文章