[iOS]使用WKWebView遇到的问题总结

1. 获取UserAgent为nil

在使用WKWebView获取userAgent的时候, 如果要全局配置, 使所有的WKWebView都能生效, 我们可能的做法是在AppDelegate中来配置, 但是需要一个WKWebView实例对象, 所以可能是这么写的:

let webView = WKWebView()
webView.evaluateJavaScript("navigator.userAgent") { (info, error) in
            // 获取默认值
            if var userAgent = info as? String {
                // 添加自定义的内容
                if userAgent.hasSuffix("/ios-app") == false {
                    userAgent += "/ios-app"
                }
                // 设置global User-Agent
                let dic = ["UserAgent": userAgent]
                UserDefaults.standard.register(defaults: dic)
                UserDefaults.standard.synchronize()
            }
        }

这样, 你会发现设置一直是无效的, 在回调里打印一下:

print(info)
print(error)

这时发现获取到的 info 字段为nil, 并且error信息如下:

Error Domain=WKErrorDomain Code=3 "WKWebView 已失效" UserInfo={NSLocalizedDescription=WKWebView 已失效}

个人猜测: 这是因为, WKWebView的evaluateJavaScript方法是异步执行的, 当WKWebView回调这个方法的时候, 其实例对象已经从内存中释放了, 所以导致回调出错.
我做了如下验证, 在回调方法里输出webView实例对象:

let webView = WKWebView()
webView.evaluateJavaScript("navigator.userAgent") { (info, error) in

          print(info)
          print(error)
          print(webView)
            // 获取默认值
            if var userAgent = info as? String {
                // 添加自定义的内容
                if userAgent.hasSuffix("/ios-app") == false {
                    userAgent += "/ios-app"
                }
                // 设置global User-Agent
                let dic = ["UserAgent": userAgent]
                UserDefaults.standard.register(defaults: dic)
                UserDefaults.standard.synchronize()
            }
        }

会发现输出是info有值, 而error为nil, webView有值, 又正常了, 有人说了,看样子不是这个问题! 真的么? 仔细想一下会发现, 在webView的方法回调闭包里使用了webView实例, 会发生什么? 对, 循环引用! webView实例此时不为nil, 这也验证了, 如果webView实例正常的话, 获取结果是不会有误的! 继续上面的验证, 我们弱引用一下:

let webView = WKWebView()
webView.evaluateJavaScript("navigator.userAgent") {[weak webView] (info, error) in

          print(info)
          print(error)
          print(webView)
            // 获取默认值
            if var userAgent = info as? String {
                // 添加自定义的内容
                if userAgent.hasSuffix("/ios-app") == false {
                    userAgent += "/ios-app"
                }
                // 设置global User-Agent
                let dic = ["UserAgent": userAgent]
                UserDefaults.standard.register(defaults: dic)
                UserDefaults.standard.synchronize()
            }
        }

这时, 会发现: info和webView都为nil, error值为上面那个错误!!!

这样, 基本验证出现这个问题的原因是: webView 提前释放了!

但是为了添加这个设置, 而将webView 设为全局变量, 仿佛有点得不偿失, 这时可以在使用webView的页面进行设置, 或者使用UIWebView替换:

// 获取默认值
        if let oldAgent = UIWebView().stringByEvaluatingJavaScript(from: "navigator.userAgent") {
            var newAgent = oldAgent

            // 添加自定义的内容
            if oldAgent.hasSuffix("/artron-cgyc") == false {
                newAgent += "/artron-cgyc"
            }

            // 设置global User-Agent
            let dic = ["UserAgent": newAgent]
            UserDefaults.standard.register(defaults: dic)
            UserDefaults.standard.synchronize()
        }
2. 无需传参时注入的交互协议无效

在做JS与原生交互的时候, 使用下面方法注入的协议无效:

 let user = WKUserContentController()
 // 向js中注入协议, 作为ios和js交互的依据
 user.add(self, name: "appProtocol")

然后在js端使用的时候: 这里不需要传参数, 直接这么写的

window.webkit.messageHandlers.appProtocol.postMessage();

这样, 没有响应js端的事件!!!
在代理方法中:

func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        
        if message.name == "appProtocol" {
            print(message.body)
        }
    }

一直没有收到回调!!!
其实, 并不是注入协议失败, 这么使用也没问题, 问题就出在postMessage的参数上, 如果是带参数的:

window.webkit.messageHandlers.appProtocol.postMessage({info: 'info', num: '123456788', price: '100'});

这么写, 是完全没有问题的, 所以如果不需传参数的话, 可以这么写:

// 无参数使用
window.webkit.messageHandlers.appProtocol.postMessage({});

给一个空的字典, 就能正常交互了!!!

3. 屏蔽页面长按手势

在WKWebView加载的HTML页面上, 如果长按会弹出一些选择框, 在文字上长按, 会弹出UIMenuController选择框:

长按文字

而在图片上长按, 会弹出一个alertSheet:

屏幕快照 2017-11-30 下午3.20.31.png

这里可以保存图片到系统相册(如果有权限), 或者复制到剪切板. 但是这些需求并不是我们需要, 如何禁止这些行为呢?需要从JS入手, 只需要执行下面两句js即可:

// 禁止图片长按事件
document.documentElement.style.webkitTouchCallout='none';
//禁止文本长按事件
document.documentElement.style.webkitUserSelect='none';

可以在创建WKWebView的时候注入:

let config = WKWebViewConfiguration()
config.userContentController = WKUserContentController()
let jsStr = """
    document.documentElement.style.webkitTouchCallout='none';
    document.documentElement.style.webkitUserSelect='none';
          """
let noneSelectJS = WKUserScript(source: jsStr, injectionTime: WKUserScriptInjectionTime.atDocumentEnd, forMainFrameOnly: true)
config.userContentController.addUserScript(noneSelectJS)
let web = WKWebView(frame: .zero, configuration: config)

也可以在页面加载完成后的代理方法中执行:

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {

        // 结束加载
        let jsStr = """
                    document.documentElement.style.webkitTouchCallout='none';
                    document.documentElement.style.webkitUserSelect='none';
                """
        webView.evaluateJavaScript(jsStr) { (info, error) in
            
        }
    }
4. 页面中出现第三方的广告悬浮框(Ta)

在加载的HTML页面中, 无端出现一个广告的悬浮框:

第三方广告悬浮框

打开之后是这样的:

第三方广告页面

而且只会在移动4G网络下才会出现, 其实这是移动的流量劫持, 强加的广告推广,目前网上有一些解决方式,常用的有:

  1. 使用IP地址访问:将域名改为IP地址访问数据;
  2. 使用HTTPS;
  3. 前端的小伙伴想想办法

其他的可参考这篇文章 iOS 客户端对于运营商劫持的一点点对抗方式

5. 显示HTML页面不是最新的内容

在联调的时候, 前端的同学改了一些东西, 例如页面的布局, 显示元素, 或者js方法, 而APP端没反应!!!

这是因为, WKWebView有缓存, 为了保证每次加载的都是最新的页面, 可以在加载的链接后面加上一个时间戳, 例如你的HTML地址为:

http://your host name/test/20171127.html

一般使用是这样的:

let urlStr = "http://your host name/test/20171127.html"

      if let url = URL(string: urlStr) {
            let request = URLRequest(url: url)
            webView.load(request)
}

这样的话是有缓存, 加载一次之后, 再去加载也不是最新的页面, 可以这样使用:

let time = Date().timeIntervalSince1970   
let urlStr = "http://your host name/test/20171127.html?_t=\(time)"
                
        if let url = URL(string: urlStr) {
            let request = URLRequest(url: url)
            webView.load(request)
        }

这样每次加载的时候都会是最新的, 当然弊端就是, 每次都会耗费一些额外的流量.

6. 当前页面无导航时不能填充状态栏(iOS11+ 会下移状态栏的高度)

在页面无导航的情况下,系统会自动调节滚动视图的contentInset,使其视图永远处于状态栏之下,但是如果我们想让滚动视图的Y坐标从屏幕顶端(状态栏)开始,我们都知道怎么修改,但是 WKWebView不是继承自UIScrollView 的,所以不能直接设置,可以这么写:

if (@available(iOS 11.0, *)) {
        self.webView.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
    } else {
        // Fallback on earlier versions
        self.automaticallyAdjustsScrollViewInsets = NO;
    }
7. Attempt to add script message handler with name 'action_toiOS' when one already exists.

这个闪退发生在与 JS 进行交互,使用下面的方法注册协议时:

 let user = WKUserContentController()
 // 向js中注入协议, 作为ios和js交互的依据
 user.add(self, name: "action_toiOS")

如果重复注册了相同名称的协议,就会发生闪退,所以在使用完webView的时候,一定要记得移除已注册的协议:

self.wkWeb.configuration.userContentController.removeScriptMessageHandler(forName: "action_toiOS")
8. Invalid parameter not satisfying: targetNod

app首页使用 WKWebView 来承载的内容,在启动时,如果添加了引导页/广告页,这时如果有手势操作,例如在出现广告页时点击屏幕,就会闪退,并在控制台输出:


Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid parameter not satisfying: sourceNode'
*** First throw call stack:
(0x1d7131ea4 0x1d6301a50 0x1d7047a2c 0x1d7b361d0 0x203ed4984 0x203ed34e4 0x203edb728 0x203ee144c 0x203edd398 0x203edd2cc 0x203edd09c 0x204308cb4 0x2042e7fcc 0x2043b6e38 0x2043b9830 0x2043b2320 0x1d70c20e0 0x1d70c2060 0x1d70c1944 0x1d70bc810 0x1d70bc0e0 0x1d9335584 0x2042ccc00 0x1042c47a4 0x1d6b7abb4)

添加一个全局断点,调试发现崩溃信息为

 Assertion failure in -[UIGestureGraphEdge initWithLabel:sourceNode:targetNode:directed:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKitCore/UIKit-3698.94.14/Source/GestureGraph/UIGestureGraphEdge.m:24

查了些资料,没找到具体原因,但是了解到和 +load方法有关,我是在 +load 方法内初始化的广告页的信息,把这些放在 didFinishLaunchingWithOptions 进行初始化,就不会有这个问题;

解决:
将广告/引导页视图的初始化放在 didFinishLaunchingWithOptions 方法内。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容