美文网首页iOS开发文章开发小TipsHTML交互
JavaScript和Objective-C交互的那些事(续)

JavaScript和Objective-C交互的那些事(续)

作者: TIME_for | 来源:发表于2016-05-02 23:05 被阅读8429次

    注:此文只现在只推荐需要适配iOS7的同学读,如果已经扔掉iOS7,强烈建议换用WKWebView。已出WKWebView文章WKWebView使用及注意点(keng)

    交互的细节可以参考我写的上一篇文章JavaScript和Objective-C交互的那些事。已经写过交互了,为什么相隔几个月来还要在出一片续集呢?这是因为过去几个月的使用的过程中出现了几个深坑,在这里特别强调一下。深坑主要包括内存管理和什么时候注入交互对象才是合理的。

    内存管理

    内存泄露问题

    在我的第一篇文章里面注入的交互对象为控制器self,这样JSContext环境引用控制器self,在退出控制器的时候,因为控制器selfJSContext引用而不释放,而JSContext只有等控制器释放了才能随之释放,所以就引起了循环引用,造成内存泄露。

    解决办法

    关于这个问题说三种解决办法。

    • 可以使用我参考文章中提到的,注入一个中间的对象去交互,而不是直接使用控制器selfiOS与JS交互实战篇(ObjC版),这样可能需要在对象中在加一层代理,或者Block来进行和控制器之间的通信。
    • 注入对象改为注入类[self class],这样倒是可以防止内存泄露,但是所写的代理方法就要改为类方法,全部使用类方法在实际开发中会带来一些不便,也不会太好。
    • 使用Block进行交互替掉JSExport协议

    合适时机注入交互对象

    UIWebView什么时机创建JSContext环境

    什么时候UIWebView会创建JSContext环境,分两种方式,第一在渲染网页时遇到<script标签时,就会创建JSContext环境去运行JavaScript代码。第二就是使用方法[webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]去获取JSContext环境时,这时无论是否遇到<script标签,都会去创造出来一个JSContext环境,而且和遇到<script标签再创造环境是同一个。

    我的错误做法

    刚开始的时候,我是在- (void)webViewDidFinishLoad:(UIWebView *)webView中去注入交互对象,但是这时候网页还没加载完,JavaScript那边已经调用交互方法,这样就会调不到原生应用的方法而出现问题。后来我就改成在- (void)viewDidLoad中去注入交互对象,这样倒是解决了上面的问题,但是同时又引起了一个新的问题就是在一个网页内部点击链接跳转到另一个网页的时候,第二个页面需要交互,这时JSContext环境已经变化,但是- (void)viewDidLoad仅仅加载一次,跳转的时候,没有再次注入交互对象,这样就会导致第二个页面没法进行交互。当然你可以在- (void)viewDidLoad- (void)webViewDidFinishLoad:(UIWebView *)webView都注入一次,但是一定会有更优雅的办法去解决此问题。

    解决办法

    那么交互对象到底该什么时候注入呢?其实网上已有很好的解决办法,就是在每次创建JSContext环境的时候,我们都去注入此交互对象这样就解决了上面的问题。具体解决办法参考了此开源库UIWebView-TS_JavaScriptContext。关于这个开源库,我说一点在- (void)webView:(id)unused didCreateJavaScriptContext:(JSContext*)ctx forFrame:(id<TSWebFrame>)frame此方法中使用到代理方法parentFrame可能会被认为是私有API而遭拒,在Issues中有人提到。此开源库的实现思路可以参考readme写的很不错,除了解决这个问题,也可以学习到一些思考问题的思路。有些不太愿意读英文的同学,我这里也有中文版仅供参考。

    UIWebView-TS_JavaScriptContext的readme译文

    我曾经做过很多的混合iOS应用,但是我不屑于承认。这些应用的一个主要痛点是通过web/native边界(运行在UIWebView中的JavaScript与运行在App中的ObjectiveC之间)进行交互。

    我们都知道官方只给出一种方法从ObjectiveC调到网页里,是通过stringByEvaluatingJavaScriptFromString方法。还有一种调用JavaScript的典型办法是人为设置window.location去触发UIWebView的代理方法shouldStartLoadWithRequest:。另一种常常使用到的技术是实现自定义的NSURLProtocol并拦截通过XMLHttpRequest发出的请求。

    在iOS7中苹果给出了一个公开的框架JavaScriptCore(WebKit的一部分),这个框架提供了简单机制在ObjectiveC和JavaScript的环境中互相调用对象和方法。众所周知,UIWebView建立在WebKit之上最终也是使用了JavaScriptCore,不幸的是苹果没有暴露一些途径给我们去访问这套框架。

    可以使用KVC简单粗暴的获取这个深植于UIWebView内部官方文档却未定义的属性JSContext这篇博客介绍了这个技术。当然,这个方法的主要缺点是他依赖UIWebView的内部构造。

    我介绍一个可供选择的方法去获取UIWebViewJSContext。当然我的方法也不是官方的,可能被拒。我应该不会尝试提交一个这样的应用到AppStore。但它看来至少不那么容易被拒,我认为它并没有特别地依赖于UIWebView的内部结构不同于UIWebView自己用WebKit和JavaScriptCore。(这有个小警告,一会解释)

    基本原理是这样的:WebKit用WebFrameLoadDelegate回调与客户端进行通讯就好像UIWebView传达页面加载事件通过他自己的UIWebViewDelegate。WebFrameLoadDelegate其中一个方法是webView:didCreateJavaScriptContext:forFrame:就像所有事件源,WebKit的代码去检测他的代理是否实现了回调方法,如果实现了就调用此方法。下面是WebKit的部分源码(WebFrameLoaderClient.mm)

    if (implementations->didCreateJavaScriptContextForFrameFunc) {
        CallFrameLoadDelegate(implementations->didCreateJavaScriptContextForFrameFunc, webView, @selector(webView:didCreateJavaScriptContext:forFrame:), script.javaScriptContext(), m_webFrame.get());
    }
    

    证实在iOS,UIWebView内,不论任何对象实现WebKit的WebFrameLoadDelegate方法,并不是真的实现webView:didCreateJavaScriptContext:forFrame:所以WebKit从不会调用此方法。如果此方法存在于代理对象中,它将会被自动调用。

    既然如此,在OC中有很多的办法给现有的类和对象动态的增添一个方法。最简单的办法就是通过扩展。我给已有的类NSObject添加一个扩展去实现webView:didCreateJavaScriptContext:forFrame:方法。

    的确,添加这个方法让WebKit开始调用它,因为任何对象(包括UIWebView中的一些sink object)都继承自NSObject,现在都实现了webView:didCreateJavaScriptContext:forFrame:这个方法。如果未来UIWebView内部的sink object实现了这个代理方法,那么这个途径就是失效因为我们自己实现的分类永远不会被调用。

    当我们的方法被WebKit调用的时候会传给我们一个WebKit中的WebView(不是UIWebView),一个JavaScriptCore的JSContext对象和WebKit的WebFrame。因为没有一个公开的WebKit框架的头文件提供给我们,所以WebView和WebFrame对我们来说非常透明。但是JSContext正是我们寻找的,通过JavaScriptCore框架对我们来说完全是适用的。(在实际中,我最终在WebFrame中调用方法,作为一个最佳状态)

    问题现在就变成怎样根据JSContext反找到对应的UIWebView。首先我尝试使用WebView对象我们控制和沿着继承的view去找到他拥有的UIWebView.但是后来证明这个对象是一些UIView的代理,并不是一个真正的UIView。并且因为他对我们来说是透明的,我也没有打算使用它。

    我的解决方案是迭代所有在app中所创建的UIWebViews(参考代码,我是怎么样做的)并且使用stringByEvaluatingJavaScriptFromString:去储存一个token"cookie"在JavaScriptContext中,然后我在JSContext中查找已经存在的这个token,如果他存在这个UIWebView就是我所要找的。

    一旦我们有了JSContext我们就可以做一些很有趣的事情。我的测试App展示了我们怎样映射ObjectiveC的blocks和对象到全局命名空间并且通过JavaScript访问和调用它们。

    总结

    在使用一项技能的时候,一定要挖透,理解其原理,这样在遇到问题的时候才能更从容的应对。这也算是给我的一点点启示和教训吧,更希望大家能引以为戒。关于readme的译文,作者水平有限,欢迎指正。

    更新

    参考

    相关文章

      网友评论

      • LoveY34:博主你好!关于内存泄露问题,注入一个中间对象去交互的话,中间对象貌似释放不了,你有遇到过吗?是怎么解决的?
        cd7a13c82b0b:请教下,中间对象无法释放的问题解决了吗?
        LoveY34:@TIME_for 我试了加载本地html资源的话,注入中间对象,中间对象是会被释放掉的,不存在内存泄露问题,但是如果加载的是html资源链接的话,注入的中间对象一直无法释放,使用博主推荐的文章里面的demo也存在这样的问题,博主可以试试看!
        TIME_for:@LoveY34 这篇文章里面给了解决办法啊。
      • 总想写点东西的陌小默:楼主好,我想问一下,我看git上面的代码- (void)webView:(UIWebView *)webView didCreateJavaScriptContext:(JSContext*) ctx;这个方法写成了uiviewView的扩展方法,在文章的描述上面看是写成NSObject的类目方法,会有什么不同吗?
      • foolmcode:galileio: 谢楼主。我已解决此问题。并且给出了防止被拒的一个方案。请看链接https://galileioo.github.io/posts/UIWebview-JS.html;
        JerseyBro:打不开了可以重新发下吗
        8518f316c550:可以试试
      • 不作不会死:写的很好,你的上一篇里面我也提到了相应的问题,这个是有方法在pc的webkit里面是开放的 但是在uiwebview里面是私有的,可能是苹果不喜欢应用全部是uiwebview的缘故吧 个人猜测。不过被拘的风险还是蛮高的
        TIME_for:之前使用可能是运气好,一直没有被拒,现在已经替换为WKWebView了。不适配iOS7建议替换。
      • da27c260cc85:documentView.webView.mainFrame.javaScriptContext 这样的读取值不会导致被拒么?
        TIME_for:确实有风险。
      • 3040ba0de5d0:请问一个问题 以前uiwebview的时候 前端调用客户端的方法 是不是在wkwebview都不能用了,都要全部替换成 调用window.webkit?
        如果是那样的话 有没有好的方法 可以解决这个问题
        TIME_for:@3040ba0de5d0 是的,要全部替换成新的方式,最近会出一篇WKWebView的使用,里面会有叙述。
      • OwenWong:非常不错
        TIME_for:@OwenWong 谢谢鼓励~~共同进步。
      • 不作不会死:我的被拒了怎么解决的
        TIME_for:@不作不会死 再试一次,或者换其他方法,比如拦截协议,如果不适配iOS7,可直接换用WKWebView。
      • 面试小集:哥们儿, 有没有遇到过单页APP,使用Angular JS, 导致在webview中跳转的时候无法获取到document.title
        TIME_for:@riverli 没有耶~~
      • 2f7360727b82:你好 这句话“刚开始的时候,我是在- (void)webViewDidFinishLoad:(UIWebView *)webView中去注入交互对象,但是这时候网页还没加载完,JavaScript那边已经调用交互方法,这样就会调不到原生应用的方法而出现问题” 没有太明白什么意思, 既然已经回调了webViewDidFinishLoad为什么还说网页没有加载完呢
        择择lee:我也没太理解,webViewDidFinishLoad这个方法就是网页已经加载完了,为什么还说网页没有加载完呢?
        不作不会死:我使用这个私有的api被拒了 怎么解决
        TIME_for:@Super_G 渲染网页的时候,遇到<script>标签,就会创建JSContext去执行<script>标签里面的代码,但是这个时候,整个网页还没有全部渲染完成。
      • 大生活家:[webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"] 会被拒吗?我在viewDidLoad和finished代理方法中各自取得了一次jsContext
        da27c260cc85:@TIME_for 是说开始使用没有被拒, 后来被拒了?
        大生活家:@TIME_for 非常感谢,如果被拒了,我再想想别的办法
        TIME_for:@大生活家 这个不一定啊,我开始使用没有被拒绝过。
      • Dennis_me:使用`- (void)webView:(id)unused didCreateJavaScriptContext:(JSContext*)ctx forFrame:(id<TSWebFrame>)frame`
        `parentFrame`一样有被拒的风险。。。。和[webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]来比,差不了多少。苹果不知道为何不添加获取的api
        TIME_for:@Dennis_me 不太清楚,OC和JS交互,但是不意味着OC中的UIWebView和JS交互,可能苹果不提提倡OC中的UIWebView和JS交互吧,猜测而已。。
      • 一个人的阳光:请问你直接用TS_JavaScriptContext来解决问题吗,AppStore可以通过审核吗?
        e52d6aff7fd0:@不作不会死 被拒之后用什么方法代替的?
        不作不会死:为什么我的被拒了 说了使用了私有api
        TIME_for:@一个人的阳光 可以。
      • 井湾村夫:有点不懂,还是先记着吧
        井湾村夫:@Toyun 好的,谢谢啦
        TIME_for:@申浩栋 结合实际运用,好理解一些
      • 极小光:感谢分享,欢迎关注专集:极光。
        互联网内幕、技术、八卦都在这里,快到碗里来。
        极小光:@Toyun 求关注orz
        TIME_for:@极小光 :smile:
      • 以技术之名:你好,加入了我自己的专题,不介意吧🤓
        TIME_for:@Flying_Einstein 不介意

      本文标题:JavaScript和Objective-C交互的那些事(续)

      本文链接:https://www.haomeiwen.com/subject/enjkrttx.html