了解iOS消息推送一文就够:史上最全iOS Push技术详解
本文作者:陈裕发, 腾讯系统测试工程师,由腾讯WeTest整理发表。
1、引言
开发iOS系统中的Push推送,通常有以下3种情况:
1)在线Push:比如QQ、微信等IM界面处于前台时,聊天消息和指令都会通过IM自建的网络长连接通道推送过来,这种Push在本文中暂且称为“在线Push”;
2)本地Push:这种就是最常见的iOS系统通知(作用相当于传统PC端的提示窗口,在iOS10以后全部整合到UserNotifications.framework框架了),不涉及任何网络数据,仅仅是让APP拥有一个统一系统通知方式而已,比如:闹钟的定时提醒等;
3)离线/远程Push:这就是iOS程序员最熟悉的APNs这一套东西了,它使得APP处于后台或者被kill的情况下仍能收到网络通知,最常见的应场景就是IM聊天工具了。
本文将对iOS Push的在线push、本地push及离线(远程)push进行了详细梳理,介绍相关逻辑、测试时要注意的要点以及相关工具的使用。小小的Push背后蕴藏着大大的逻辑,我们一起来学习吧!
消息推送/im开发学习交流:
- 即时通讯开发交流3群:185926912[推荐]
- 移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM》
(本文同步发布于:http://www.52im.net/thread-1762-1-1.html)
2、相关文章
《移动端实时消息推送技术浅析》
《iOS的推送服务APNs详解:设计思路、技术原理及缺陷等》
《信鸽团队原创:一起走过 iOS10 上消息推送(APNS)的坑》
《扫盲贴:浅谈iOS和Android后台实时消息推送的原理和区别》
3、iOS的Push种类
3.1 在线push
在线push:当用户在线(APP在前台)时,收到的状态栏的消息提醒,称为在线push。这个功能与苹果系统无关,是我们自己的APP开发的一种功能,该push与设置中是否打开“通知”无关。
这里以iOS Qzone为例,当APP在前台时,自己发的说说被点赞了,收到的在线push如下:
3.2 离线/远程push
离线push:当APP在离线(kill掉进程、切到后台、锁屏)时,收到的消息提醒,称为离线push。离线push是需要经过苹果的APNs服务器才可以推送到某台设备的某个APP上的,这是和本地push的本质区别。push与设置中是否打开“通知”有关。
这里最简单的以大家常用的手机QQ为例,当APP在后台、锁屏或者被kiil了进程时,收到了消息:
一种特殊的远程push:静默push
严格来说,静默push属于远程push的一种特殊情况,静默push用的场景不较少,这里只做简要介绍。
首先我们看看离线(远程)push与静默push的区别:
【普通离线(远程)push】:收到推送后(有文字有声音),点开通知,进入APP后,才执行-- (void)application:(UIApplication didReceiveRemoteNotification:(NSDictionary fetchCompletionHandler:(void result))handler *)application *)userInfo (^)(UIBackgroundFetchResult
【静默push】:收到推送(没有文字没有声音),不用点开通知,不用打开APP,就能执行(void)application:(UIApplication )application)userInfo didReceiveRemoteNotification:(NSDictionary fetchCompletionHandler:(void (^)(UIBackgroundFetchResultresult))handler,用户完全感觉不到。
所以静默push又被我们称做 Background Remote Notification(后台远程推送)。静默推送是在iOS7之后推出的一种推送方式。它与其他推送的区别在于允许应用收到通知后在后台(background)状态下运行一段代码,可用于从服务器获取内容更新。
3.3 本地push
本地push:本地推送和远程推送的功能是一样的,都是要提醒用户去做某些事情。但是和远程推送不同的就是本地推送是不需要设备联网的,而远程推送是必需要设备联网的,因为只有联网状态下,才能和苹果的APNs服务器建立长连接,从而推送消息。本地推送是由App自己设定的,并且发送给安装此App的这台设备,属于一对一的对应关系。比较典型的应用是闹钟类似的场景。该push与设置中是否打开“通知”有关。
最容易看到本地push的场景,可以直接在手机设置一个计时器,计时器时间到了就会弹出本地push:
由于本地push原理和作用相对于在线push和离线push都更为简单明了,下文主要介绍在线push和离线push。
4、本地push实现
4.1 iOS10以前本地push弹出方式
试验过iOS10以前的本地push方法在iOS10+的系统也能使用,不过可能有些参数不生效。
1)立即展示( iOS10以前)
本地push稍微简单,有两种方式可以调用,一种是presentLocalNotificationNow方法,立即展示本地push:
2)延迟展示( iOS10以前)
另一种是用scheduleLocalNotification方法按计划来弹本地推送:
如果使用这种方法,需要对推送的时间进行设置,举个例子,设为5秒后:
4.2 设置本地push内容( iOS10以前)
其中alertBody是消息内容锁屏与不锁屏时效果如下:
applicationIconBadgeNumber是消息数量,我们可以看到这里设置为66:
4.3 处理本地push ( iOS10以前)
1)App没有启动情况下处理本地push
这种情况下,当点击通知时,会启动App,而在App中,开发人员可以通过实现AppDelegate中的方法:- (BOOL)application:UIApplication)application didFinishLaunchingWithOptions:NSDictionary *)launchOptions,然后从lauchOptions中获取App启动的原因,若是因为本地通知,则可以App启动时对App做对应的操作,比方说跳转到某个画面等等。
2)App运行在后台及前台
上面的2种情况的处理基本一致, 不同点只有当运行再后台的时候,会有弹窗提示用户另外一个App有通知,对于本地通知单的处理都是通过AppDelegate的方法:- (void)application
UIApplication )application didReceiveLocalNotification:UILocalNotification *)notification来处理的。
4.4 iOS10以后本地push弹出方式
iOS10以后,本地通知可以由使用 UNUserNotificationCenter来管理。
创建方法:
接下来需要需创建一个包含待通知内容的 UNMutableNotificationContent 对象:
在iOS上可以通过以下几种触发器来触发本地push:
1)UNCalendarNotificationTrigger 传送本地通知的日期和时间;
2)UNTimeIntervalNotificationTrigger 传递本地通知之前必须过期的时间;
3)UNLocationNotificationTrigger 用户必须达到的地理位置才能提供本地通知;
4)UNPushNotificationTrigger 表示通知是从Apple推送通知服务发送的对象。
假如以时间间隔(TimeInterval)来触发,则设置触发器代码为:
推送本地push的代码为:
5、在线、离线(远程)push流程
5.1 在线push流程
在线push相对简单,因为是内部实现,具体流程如上面所示。
1)判断app是否在线:
此处可以根据APP自身的后台策略如上一次与后台交互的时间等方法来判断APP是否在线或者离线。认为在线,会发送在线push,否则,发送离线push。
2)在线push有以下几个特点:
不需要经过苹果APNs;
需要自己实现长链接;
代码在app内部实现。
5.2 离线(远程)push流程
主要流程为:
1)服务器端将消息先发送到苹果的APNs;
2)由苹果的APNs将消息推送到客户的设备端;
3)由iOS系统将接收到的消息传递给相应的App。
简而言之离线push是苹果系统的行为,与app状态无关,能够直接推送到指定手机的指定app。
在进一步了解离线push前,我们有必要先了解几个名词。
【离线push名词解释】:
(1)名词解释之APNs
APNs:Apple Push Notification service(苹果推送通知服务)。
APNs主要用于以下场景:当用户主动杀掉 APP,或者 APP 进入后台超过约定时长时,APP会被kill,这样保障了前台 APP 的流畅性,也延长了手机的使用时长,获得了较好的用户体验,但是这也意味着,服务器无法主动和用户交互(如推送实时消息等),所以苹果推出了 APNs,允许设备和服务器分别与苹果的推送通知服务器保持长连接状态。
关于APNs的更新有以下几点:
iOS 8以后,APNs推送的字节是2k,iOS8以前是256字节;
iOS 9以后APNs支持HTTP/2协议栈,优化长连接,具有标准的HTTP返回和管道复用技术;
iOS 10以后,推送的字节是4k,APNs可根据推送消息的唯一标示符查询某条消息是否被用户阅读,可更新某一推送消息,而不用发重读的多条消息。
关于APNs更全面的介绍可以看官方文档:点此进入。
(2)名词解释之payload
什么是payload?对于每一条发送给APNs的推送消息,都包含一个payload,通常是组成了一个JSON的Dictionary,这其中必不可少的是aps属性,它对应的value也是一个Dictionary,包含一些但不限于以下内容:标题、副标题、内容、附件、category等,如
(3)名词解释之device token
什么是device token?我们看一下官方的简介:
device token: APNs uses device tokens to identify each unique app and device combination. It also uses them to authenticate the routing of remote notifications sent to a device.(device token是APNs用于区分识别每个iOS设备和设备上不同app的一个标识符,还可以用于APNs通过它将推送消息路由到指定设备上)
即:device token里包含了device id和bundle id的信息,但是device id和bundle id不会确定唯一的device token。
但是,这里有个坑,查资料得知,iOS8及之前的iOS系统,对于同一部手机,如果卸载后重装APP的话,device token是不会变的,在token变了以后,老的token,就被认为是无效了,苹果不会对这部分无效的token推送。但是,对iOS9及以后的iOS系统,对于同一部手机,卸载后重装APP的device token是会发生变化的,而且老的token不会无效,还可以正常推送,这应该是苹果的一个bug,但是苹果也没有修复这个问题,所以这个需要开发者自己来解决,否则容易出现一个app收到多个push的问题。
官方的说法是:
To protect user privacy, do not use device tokens to identify user devices. Device tokens change when the user updates the operating system and when a device’s data and settings are erased. As a result, apps should always request the current device token at launch time.(即此举为了保护用户隐私,device token会在更新系统、擦除设置重置后变化,在一定时间后会过期)
【离线push详细流程】
知道了以上概念后我们重新来看一下离线(远程)push的详细流程:
1) 首先是应用程序注册消息推送;
2) iOS跟APNS Server要deviceToken。应用程序接受deviceToken;
3) 应用程序将deviceToken发送给PUSH服务端程序;
4) 服务端程序向APNS服务发送消息;
5) APNS服务将消息发送给iPhone应用程序。
值得注意的是,当由于用户反复卸载重装程序(虽然概率很小)等原因导致多个device Token指向同一台设备的同一个app,又把多个device Token发给APNs时,用户就会收到多条push。苹果APNs是不会对多个device Token是否指向同一台设备的同一个app做校验的,所以需要后台来做去重等处理保证用户不会收到多条push。
5.3 对离线(远程)push的响应
1)iOS 7以上对离线(远程)push时的响应
iOS 7以上关于接受离线push有两个函数:
那么这两个函数有什么区别呢?其实这两个方法都是用来处理离线push的。
差别就是,如果app在前台是收到离线(远程)push,那么就会调用:
相对的,如果在后台或者杀进程情况下,点击收到的离线push,那么就会调用,如果没有实现:
则会调用:
若实现了前者,就只调用前者。
2)iOS 10以上对离线(远程)push的响应
iOS10对push的处理主要增加了两个方法:
其中前者是对APP在前台时收到push时的处理,后者是点击push进入APP执行的函数。
用得比较多的是后者,我们可以举个例子,点击push进入APP后如何获取push的消息、角标、标题等内容:
6、iOS 10关于push的一些新特性
iOS10新增的UserNotifications框架,主要有了这样几方面的更新:
1)用UserNotifications框架替换了原先与通知相关的接口,通知文字可分为title、subtitle和body三部分,通知可携带附件;
2)系统在展示通知之前,可以唤起app附带的service extension,并且允许它改动通知的内容;
3)用户在对通知右滑查看、下拉或者3d touch的时候,通知会展开,展开后页面的布局可以由app附带的content extension来决定。
6.1 push的多样性
iOS10以前的push只有文字,甚至没有标题。iOS10以后的push更加多样化,可以有主标题,副标题,甚至还有附件。
这里以我司的腾讯新闻为例(有标题,内容,和附件):
3D touch点入详情以后:
这里我们惊奇的发现,除了可以携带图片这样的附件、push还能展开详情以外,进入详情以后,下面还多了“打开”、“收藏”、“不感兴趣”这些选项,这里就涉及到以下iOS10的新特性。
6.2 push携带附件
因为payload有大小限制,所以如果remote notification想要携带附件,那么payload上只能带上如附件下载地址之类的信息,等通知到达客户端后由service extension下载附件到本地,然后在初始化UNNotificationAttachment对象时传入附件在本地的URL。
初始化UNNotificationAttachment对象时,可以传入option参数。这里的option参数可以强制指定附件的类型,可以选择是否展示缩略图,以及缩略图截取自附件的哪一帧、哪一部分。
目前iOS10通知只将几种格式的图片、音频和视频作为附件,附件的大小也有一定限制,具体可以看官方文档中的限制说明。
关于附件的更加详细的说明,可以参考官方文档:点此进入。
6.3 携带action的通知
上面提到的“打开”、“收藏”、“不感兴趣”这些选项其实就是push携带的action,其实从iOS8开始,通知已经可以携带action了。而在iOS10中,通知的action被放在了更明显的位置,与action相关的接口也有了很大变化。
决定一个通知应该有哪些action呢?在payload中,这是由category字段决定的。如果我们希望一个通知能携带若干个action,我们就需要将若干个action和一个category绑定起来。通知到达前端后,系统会根据category的名字来决定要给这个通知展示哪些action:
怎么得知用户选了哪个action并做出相应操作呢?这需要给UNUserNotificationCenter指定一个delegate:
然后在delegate的类中实现:
方法:通过response.notification.request.content.categoryIdentifier和response.actionIdentifier就可以得知用户选择的action了。
6.4 改变push内容
这里主要讲应用的比较多的离线(远程)push的改变push方法。
1)改变本地push内容:
本地push,只要request的id一样,那么就可以更新推送。
更新的例子:
此外,还有删除所有推送等,都在UNUserNotificationCenter.h中实现。
2)改变离线(远程)push内容:
目前远程push只支持更新push内容,更新需要通过新的字段apps-collapse-id来作为唯一标示。方法是在HTTP/2 请求头中使用相同的apns-collapse-id,这样收到同样的apns-collapse-id的push时,push内容便会更新。
使用场景:比较容易理解的一个场景就是球赛比分,比如现在是1:0,如果变成1:1的话,只需要刷新原来的新闻,这样用户就不会因为同一场比赛收到多条push。
6.5 两个extension
有两个与push相关的extension,可能我们会好奇这两个extension有什么不同,为什么需要两个?它们分别实现什么功能呢?
【1)notification service extension】
给app添加notification service extension后,系统会在收到通知后唤醒它,并允许它修改通知的内容,之后再展示这个通知。
service extension只对remote notification起作用,local notification是无法唤起它的。
如果想要让系统唤起service extension的话,payload必须符合这样几个条件:
1)必须增加mutable-content字段并为1,这表示允许客户端修改这个通知:
payload(举例)如下:
2)这个通知必须展示一个alert,如果只是一个修改badge的通知的话,是不会唤起service extension的;
3)静默推送是不能唤起service extension的,所以payload中不能有”content-available” : 1字段。
所以,通过这个notification service extension,你可以在接收到推送之后、展示推送之前处理一些事情,比如说更新一下推送内容,或者在后台做一些其他事情。
【2)notification content extension】
另一项notification content extension用于完全自定义推送展开后的视图。上面腾讯新闻的展开后的视图就是通过这个notification content extension实现的。
依然以腾讯新闻为例子:
这里Notification Content Extension大展拳脚的地方,在这里可以自定义绘制不同的内容,将希望展现给用户的额外信息可以加载这里。
下半部分的notification action的实现就是在上面提到的“携带action的通知”。
7、iOS Push的测试要点罗列
另外注意一点:测试Push的时候,区分好Appstore证书和开发证书。两者不能相互发Push。
8、有关iOS Push的常见疑问汇总
Q:离线push,支持角标(badge)在本地角标数值上+1这样的操作吗?
A:不支持。如果是自己实现push服务的话,需要自己的后台将角标值badge发送个APNs服务器,有些APP使用第三方push SDK除外。
Q:如果重复收到离线push,可能是什么情况?
A:
1)iOS9之后卸载重装后生成新的deviceToken,后台对多个deviceToken都发送了push
2)后台对注销了的账号也发送了push。
总而言之一般是后台的逻辑出现了问题,而不是APNs服务器出现问题。
Q:直接卸载APP,还能收到离线push吗?
A:不会收到。直接卸载APP,虽然后台不知道APP被卸载了,仍然会对之前的账号发送push,但是由于手机上没有对应APP,所以并不会收到push。
Q:为什么有时候全新安装APP就立马有红点角标?
A:这是因为卸载该APP时有红点角标。每个 APP 的角标都是存在 iOS 手机系统里的,开发无法修改,所以此时卸载前有角标,重新安装也会有角标。但是,APP 卸载之后超过一天的时间再重装,那么角标就会被系统清空,届时也不会有新安装的 APP 就有角标的情况存在。
Q:自己Server通过APNs发的每一条Push,客户端都会收到么?
答案是否定的,Push是不可靠的,push通知是fire-and-forget,比如手机关机,那么自然就收不到,虽然Apple会尝试几次。
Q:Push消息的大小是多少?
iOS8发的时间点起,无论那个iOS系统,push消息的body大小调整为2k,注意这里是iOS8的时间点,也就是2014年秋,就目前来说push的限制应该是2k不再是256了。
9、相关工具推荐
Knuff离线push工具下载链接:https://github.com/KnuffApp/Knuff/releases
使用方法也比较简单:
比如我的payload输入如下:
得到的应该是有“Knuff测试”文字,和角标数变为999,我们可以看下结果,与预料是一致的:
有了这个工具也更加方便了我们的iOS push的调试。
附录:更多消息推送技术文章
《iOS的推送服务APNs详解:设计思路、技术原理及缺陷等》
《信鸽团队原创:一起走过 iOS10 上消息推送(APNS)的坑》
《Android端消息推送总结:实现原理、心跳保活、遇到的问题等》
《扫盲贴:认识MQTT通信协议》
《一个基于MQTT通信协议的完整Android推送Demo》
《IBM技术经理访谈:MQTT协议的制定历程、发展现状等》
《求教android消息推送:GCM、XMPP、MQTT三种方案的优劣》
《移动端实时消息推送技术浅析》
《扫盲贴:浅谈iOS和Android后台实时消息推送的原理和区别》
《绝对干货:基于Netty实现海量接入的推送服务技术要点》
《移动端IM实践:谷歌消息推送服务(GCM)研究(来自微信)》
《为何微信、QQ这样的IM工具不使用GCM服务推送消息?》
《极光推送系统大规模高并发架构的技术实践分享》
《从HTTP到MQTT:一个基于位置服务的APP数据通信实践概述》
《魅族2500万长连接的实时消息推送架构的技术实践分享》
《专访魅族架构师:海量长连接的实时消息推送系统的心得体会》
《深入的聊聊Android消息推送这件小事》
《基于WebSocket实现Hybrid移动应用的消息推送实践(含代码示例)》
《一个基于长连接的安全可扩展的订阅/推送服务实现思路》
《实践分享:如何构建一套高可用的移动端消息推送系统?》
《Go语言构建千万级在线的高并发消息推送系统实践(来自360公司)》
《腾讯信鸽技术分享:百亿级实时消息推送的实战经验》
《百万在线的美拍直播弹幕系统的实时推送技术实践之路》
《京东京麦商家开放平台的消息推送架构演进之路》
《了解iOS消息推送一文就够:史上最全iOS Push技术详解》
>> 更多同类文章 ……
(本文同步发布于:http://www.52im.net/thread-1762-1-1.html)
x5开源库后续知识点
目录介绍
01.基础使用目录介绍
1.0.1 常用的基础介绍
1.0.2 Android调用Js
1.0.3 Js调用Android
1.0.4 WebView.loadUrl(url)流程
1.0.5 js的调用时机分析
1.0.6 清除缓存数据方式有哪些
1.0.7 如何使用DeepLink
1.0.8 应用被作为第三方浏览器打开
02.优化汇总目录介绍
2.0.1 视频全屏播放按返回页面被放大
2.0.2 加快加载webView中的图片资源
2.0.3 自定义加载异常error的状态页面
2.0.4 WebView硬件加速导致页面渲染闪烁
2.0.5 WebView加载证书错误
2.0.6 web音频播放销毁后还有声音
2.0.7 DNS采用和客户端API相同的域名
2.0.8 如何设置白名单操作
2.0.9 后台无法释放js导致发热耗电
2.1.0 可以提前显示加载进度条
2.1.1 WebView密码明文存储漏洞优化
03.问题汇总目录介绍
3.0.0 WebView进化史介绍
3.0.1 提前初始化WebView必要性
3.0.2 x5加载office资源
3.0.3 WebView播放视频问题
3.0.4 无法获取webView的正确高度
3.0.5 使用scheme协议打开链接风险
3.0.6 如何处理加载错误
3.0.7 webView防止内存泄漏
3.0.8 关于js注入时机修改
3.0.9 视频/图片宽度超过屏幕
3.1.0 如何保证js安全性
3.1.1 如何代码开启硬件加速
3.1.2 WebView设置Cookie
3.1.4 webView加载网页不显示图片
3.1.5 绕过证书校验漏洞
3.1.6 allowFileAccess漏洞
3.1.7 WebView嵌套ScrollView问题
3.1.8 WebView中图片点击放大
3.1.9 页面滑动期间不渲染/执行
3.2.0 被运营商劫持和注入问题
3.2.1 解决资源加载缓慢问题
3.2.2 判断是否已经滚动到页面底端
3.2.3 使用loadData加载html乱码
3.2.4 WebView下载进度无法监听
3.2.5 webView出现302/303重定向
x5封装库YCWebView开源项目地址
https://github.com/yangchong211/YCWebView
该后续知识点,几乎包含了实际开发中绝大多数的问题,再次学习和巩固webView,希望这篇文章对你有用……更多内容,可以看我的开源项目,如果觉得给你带来一些收获,麻烦star一下,这也可以增加开发者开源项目的动力!
01.基础使用目录介绍
1.0.1 常用的基础介绍
在activity中最简单的使用
webview.loadUrl("http://www.baidu.com/"); //加载web资源
//webView.loadUrl("file:///android_asset/example.html"); //加载本地资源
//这个时候发现一个问题,启动应用后,自动的打开了系统内置的浏览器,解决这个问题需要为webview设置 WebViewClient,并重写方法:
webview.setWebViewClient(new WebViewClient(){
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
//返回值是true的时候控制去WebView打开,为false调用系统浏览器或第三方浏览器
return true;
}
//还可以重写其他的方法
});
那些因素影响页面加载速度
影响页面加载速度的因素有非常多,在对 WebView 加载一个网页的过程进行调试发现
每次加载的过程中都会有较多的网络请求,除了 web 页面自身的 URL 请求
有 web 页面外部引用的JS、CSS、字体、图片等等都是个独立的http请求。这些请求都是串行的,这些请求加上浏览器的解析、渲染时间就会导致 WebView 整体加载时间变长,消耗的流量也对应的真多。
1.0.2 Android调用Js
第一种方式:native 调用 js 的方法,方法为:
注意的是名字一定要对应上,要不然是调用不成功的,而且还有一点是 JS 的调用一定要在 onPageFinished 函数回调之后才能调用,要不然也是会失败的。
//java
//调用无参方法
mWebView.loadUrl("javascript:callByAndroid()");
//调用有参方法
mWebView.loadUrl("javascript:showData(" + result + ")");
//javascript,下面是对应的js代码
<script type="text/javascript">
function showData(result){
alert("result"=result);
return "success";
}
function callByAndroid(){
console.log("callByAndroid")
showElement("Js:无参方法callByAndroid被调用");
}
</script>
第二种方式:
如果现在有需求,我们要得到一个 Native 调用 Web 的回调怎么办,Google 在 Android4.4 为我们新增加了一个新方法,这个方法比 loadUrl 方法更加方便简洁,而且比 loadUrl 效率更高,因为 loadUrl 的执行会造成页面刷新一次,这个方法不会,因为这个方法是在 4.4 版本才引入的,所以使用的时候需要添加版本的判断:
if (Build.VERSION.SDK_INT < 18) {
mWebView.loadUrl(jsStr);
} else {
mWebView.evaluateJavascript(jsStr, new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
//此处为 js 返回的结果
}
});
}
两种方式的对比
一般最常使用的就是第一种方法,但是第一种方法获取返回的值比较麻烦,而第二种方法由于是在 4.4 版本引入的,所以局限性比较大。
注意问题
记得添加ws.setJavaScriptEnabled(true)代码
1.0.3 Js调用Android
第一种方式:通过 addJavascriptInterface 方法进行添加对象映射
这种是使用最多的方式了,首先第一步我们需要设置一个属性:
mWebView.getSettings().setJavaScriptEnabled(true);
这个函数会有一个警告,因为在特定的版本之下会有非常危险的漏洞,设置完这个属性之后,Native需要定义一个类:
在 API17 版本之后,需要在被调用的地方加上 @addJavascriptInterface 约束注解,因为不加上注解的方法是没有办法被调用的
public class JSObject {
private Context mContext;
public JSObject(Context context) {
mContext = context;
}
@JavascriptInterface
public String showToast(String text) {
Toast.show(mContext, text, Toast.LENGTH_SHORT).show();
return "success";
}
/**
* 前端代码嵌入js:
* imageClick 名应和js函数方法名一致
*
* @param src 图片的链接
*/
@JavascriptInterface
public void imageClick(String src) {
Log.e("imageClick", "----点击了图片");
}
/**
* 网页使用的js,方法无参数
*/
@JavascriptInterface
public void startFunction() {
Log.e("startFunction", "----无参");
}
}
//特定版本下会存在漏洞
mWebView.addJavascriptInterface(new JSObject(this), "yc逗比");
JS 代码调用
这种方式的好处在于使用简单明了,本地和 JS 的约定也很简单,就是对象名称和方法名称约定好即可,缺点就是要提到的漏洞问题。
function showToast(){
var result = myObj.showToast("我是来自web的Toast");
}
function showToast(){
myObj.imageClick("图片");
}
function showToast(){
myObj.startFunction();
}
第二种方式:利用 WebViewClient 接口回调方法拦截 url
这种方式其实实现也很简单,使用的频次也很高,上面介绍到了 WebViewClient ,其中有个回调接口 shouldOverrideUrlLoading (WebView view, String url)) ,就是利用这个拦截 url,然后解析这个 url 的协议,如果发现是我们预先约定好的协议就开始解析参数,执行相应的逻辑。注意这个方法在 API24 版本已经废弃了,需要使用 shouldOverrideUrlLoading (WebView view, WebResourceRequest request)) 替代,使用方法很类似,我们这里就使用 shouldOverrideUrlLoading (WebView view, String url)) 方法来介绍一下:
代码很简单,这个方法可以拦截 WebView 中加载 url 的过程,得到对应的 url,我们就可以通过这个方法,与网页约定好一个协议,如果匹配,执行相应操作。
public boolean shouldOverrideUrlLoading(WebView view, String url) {
//假定传入进来的 url = "js://openActivity?arg1=111&arg2=222",代表需要打开本地页面,并且带入相应的参数
Uri uri = Uri.parse(url);
String scheme = uri.getScheme();
//如果 scheme 为 js,代表为预先约定的 js 协议
if (scheme.equals("js")) {
//如果 authority 为 openActivity,代表 web 需要打开一个本地的页面
if (uri.getAuthority().equals("openActivity")) {
//解析 web 页面带过来的相关参数
HashMap<String, String> params = new HashMap<>();
Set<String> collection = uri.getQueryParameterNames();
for (String name : collection) {
params.put(name, uri.getQueryParameter(name));
}
Intent intent = new Intent(getContext(), MainActivity.class);
intent.putExtra("params", params);
getContext().startActivity(intent);
}
//代表应用内部处理完成
return true;
}
return super.shouldOverrideUrlLoading(view, url);
}
JS 代码调用
function openActivity(){
document.location = "js://openActivity?arg1=111&arg2=222";
}
存在问题:这个代码执行之后,就会触发本地的 shouldOverrideUrlLoading 方法,然后进行参数解析,调用指定方法。这个方式不会存在第一种提到的漏洞问题,但是它也有一个很繁琐的地方是,如果 web 端想要得到方法的返回值,只能通过 WebView 的 loadUrl 方法去执行 JS 方法把返回值传递回去,相关的代码如下:
//java
mWebView.loadUrl("javascript:returnResult(" + result + ")");
//javascript
function returnResult(result){
alert("result is" + result);
}
第三种方式:利用 WebChromeClient 回调接口的三个方法拦截消息
这个方法的原理和第二种方式原理一样,都是拦截相关接口,只是拦截的接口不一样:
@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
return super.onJsAlert(view, url, message, result);
}
@Override
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
return super.onJsConfirm(view, url, message, result);
}
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
//假定传入进来的 message = "js://openActivity?arg1=111&arg2=222",代表需要打开本地页面,并且带入相应的参数
Uri uri = Uri.parse(message);
String scheme = uri.getScheme();
if (scheme.equals("js")) {
if (uri.getAuthority().equals("openActivity")) {
HashMap<String, String> params = new HashMap<>();
Set<String> collection = uri.getQueryParameterNames();
for (String name : collection) {
params.put(name, uri.getQueryParameter(name));
}
Intent intent = new Intent(getContext(), MainActivity.class);
intent.putExtra("params", params);
getContext().startActivity(intent);
//代表应用内部处理完成
result.confirm("success");
}
return true;
}
return super.onJsPrompt(view, url, message, defaultValue, result);
}
和 WebViewClient 一样,这次添加的是WebChromeClient接口,可以拦截JS中的几个提示方法,也就是几种样式的对话框,在 JS 中有三个常用的对话框方法:
onJsAlert 方法是弹出警告框,一般情况下在 Android 中为 Toast,在文本里面加入n就可以换行;
onJsConfirm 弹出确认框,会返回布尔值,通过这个值可以判断点击时确认还是取消,true表示点击了确认,false表示点击了取消;
onJsPrompt 弹出输入框,点击确认返回输入框中的值,点击取消返回 null。
但是这三种对话框都是可以本地拦截到的,所以可以从这里去做一些更改,拦截这些方法,得到他们的内容,进行解析,比如如果是 JS 的协议,则说明为内部协议,进行下一步解析然后进行相关的操作即可,prompt 方法调用如下所示:
function clickprompt(){
var result=prompt("js://openActivity?arg1=111&arg2=222");
alert("open activity " + result);
}
需要注意的是 prompt 里面的内容是通过 message 传递过来的,并不是第二个参数的 url,返回值是通过 JsPromptResult 对象传递。为什么要拦截 onJsPrompt 方法,而不是拦截其他的两个方法,这个从某种意义上来说都是可行的,但是如果需要返回值给 web 端的话就不行了,因为 onJsAlert 是不能返回值的,而 onJsConfirm 只能够返回确定或者取消两个值,只有 onJsPrompt 方法是可以返回字符串类型的值,操作最全面方便。
以上三种方案的总结和对比
以上三种方案都是可行的,在这里总结一下
第一种方式:是现在目前最普遍的用法,方便简洁,但是唯一的不足是在 4.2 系统以下存在漏洞问题;
第二种方式:通过拦截 url 并解析,如果是已经约定好的协议则进行相应规定好的操作,缺点就是协议的约束需要记录一个规范的文档,而且从 Native 层往 Web 层传递值比较繁琐,优点就是不会存在漏洞,iOS7 之下的版本就是使用的这种方式。
第三种方式:和第二种方式的思想其实是类似的,只是拦截的方法变了,这里拦截了 JS 中的三种对话框方法,而这三种对话框方法的区别就在于返回值问题,alert 对话框没有返回值,confirm 的对话框方法只有两种状态的返回值,prompt 对话框方法可以返回任意类型的返回值,缺点就是协议的制定比较麻烦,需要记录详细的文档,但是不会存在第二种方法的漏洞问题。
1.0.4 WebView.loadUrl(url)流程
WebView.loadUrl(url)加载网页做了什么?
加载网页是一个复杂的过程,在这个过程中,我们可能需要执行一些操作,包括:
加载网页前,重置WebView状态以及与业务绑定的变量状态。WebView状态包括重定向状态(mTouchByUser)、前端控制的回退栈(mBackStep)等,业务状态包括进度条、当前页的分享内容、分享按钮的显示隐藏等。
加载网页前,根据不同的域拼接本地客户端的参数,包括基本的机型信息、版本信息、登录信息以及埋点使用的Refer信息等,有时候涉及交易、财产等还需要做额外的配置。
开始执行页面加载操作时,会回调WebViewClient.onPageStarted(webview,url,favicon)。在此方法中,可以重置重定向保护的变量(mRedirectProtected),当然也可以在页面加载前重置,由于历史遗留代码问题,此处尚未省去优化。
加载页面的过程中,WebView会回调几个方法。
页面加载结束后,WebView会回调几个方法。
加载页面的过程中回调哪些方法?
WebChromeClient.onReceivedTitle(webview, title),用来设置标题。需要注意的是,在部分Android系统版本中可能会回调多次这个方法,而且有时候回调的title是一个url,客户端可以针对这种情况进行特殊处理,避免在标题栏显示不必要的链接。
WebChromeClient.onProgressChanged(webview, progress),根据这个回调,可以控制进度条的进度(包括显示与隐藏)。一般情况下,想要达到100%的进度需要的时间较长(特别是首次加载),用户长时间等待进度条不消失必定会感到焦虑,影响体验。其实当progress达到80的时候,加载出来的页面已经基本可用了。事实上,国内厂商大部分都会提前隐藏进度条,让用户以为网页加载很快。
WebViewClient.shouldInterceptRequest(webview, request),无论是普通的页面请求(使用GET/POST),还是页面中的异步请求,或者页面中的资源请求,都会回调这个方法,给开发一次拦截请求的机会。在这个方法中,我们可以进行静态资源的拦截并使用缓存数据代替,也可以拦截页面,使用自己的网络框架来请求数据。包括后面介绍的WebView免流方案,也和此方法有关。
WebViewClient.shouldOverrideUrlLoading(webview, request),如果遇到了重定向,或者点击了页面中的a标签实现页面跳转,那么会回调这个方法。可以说这个是WebView里面最重要的回调之一,后面WebView与Native页面交互一节将会详细介绍这个方法。
WebViewClient.onReceivedError(webview,handler,error),加载页面的过程中发生了错误,会回调这个方法。主要是http错误以及ssl错误。在这两个回调中,我们可以进行异常上报,监控异常页面、过期页面,及时反馈给运营或前端修改。在处理ssl错误时,遇到不信任的证书可以进行特殊处理,例如对域名进行判断,针对自己公司的域名“放行”,防止进入丑陋的错误证书页面。也可以与Chrome一样,弹出ssl证书疑问弹窗,给用户选择的余地。
加载页面结束回调哪些方法
会回调WebViewClient.onPageFinished(webview,url)。
这时候可以根据回退栈的情况判断是否显示关闭WebView按钮。通过mActivityWeb.canGoBackOrForward(-1)判断是否可以回退。
1.0.5 js的调用时机分析
onPageFinished()或者onPageStarted()方法中注入js代码
做过WebView开发,并且需要和js交互,大部分都会认为js在WebViewClient.onPageFinished()方法中注入最合适,此时dom树已经构建完成,页面已经完全展现出来。但如果做过页面加载速度的测试,会发现WebViewClient.onPageFinished()方法通常需要等待很久才会回调(首次加载通常超过3s),这是因为WebView需要加载完一个网页里主文档和所有的资源才会回调这个方法。
能不能在WebViewClient.onPageStarted()中注入呢?答案是不确定。经过测试,有些机型可以,有些机型不行。在WebViewClient.onPageStarted()中注入还有一个致命的问题——这个方法可能会回调多次,会造成js代码的多次注入。
从7.0开始,WebView加载js方式发生了一些小改变,官方建议把js注入的时机放在页面开始加载之后。
WebViewClient.onProgressChanged()方法中注入js代码
WebViewClient.onProgressChanged()这个方法在dom树渲染的过程中会回调多次,每次都会告诉我们当前加载的进度。
在这个方法中,可以给WebView自定义进度条,类似微信加载网页时的那种进度条
如果在此方法中注入js代码,则需要避免重复注入,需要增强逻辑。可以定义一个boolean值变量控制注入时机
那么有人会问,加载到多少才需要处理js注入逻辑呢?
正是因为这个原因,页面的进度加载到80%的时候,实际上dom树已经渲染得差不多了,表明WebView已经解析了标签,这时候注入一定是成功的。在WebViewClient.onProgressChanged()实现js注入有几个需要注意的地方:
1 上文提到的多次注入控制,使用了boolean值变量控制
2 重新加载一个URL之前,需要重置boolean值变量,让重新加载后的页面再次注入js
3 如果做过本地js,css等缓存,则先判断本地是否存在,若存在则加载本地,否则加载网络js
4 注入的进度阈值可以自由定制,理论上10%-100%都是合理的,不过建议使用了75%到90%之间可以。
1.0.6 清除缓存数据方式有哪些
清除缓存数据的方法有哪些?
//清除网页访问留下的缓存
//由于内核缓存是全局的因此这个方法不仅仅针对webview而是针对整个应用程序.
Webview.clearCache(true);
//清除当前webview访问的历史记录//只会webview访问历史记录里的所有记录除了当前访问记录
Webview.clearHistory();
//这个api仅仅清除自动完成填充的表单数据,并不会清除WebView存储到本地的数据
Webview.clearFormData();
1.0.7 如何使用DeepLink
具体可以看这篇文章:https://www.jianshu.com/p/127c80f62655
1.0.8 应用被作为第三方浏览器打开
微信里的文章页面,可以选择“在浏览器打开”。现在很多应用都内嵌了WebView,那是否可以使自己的应用作为第三方浏览器打开此文章呢?
在Manifest文件中,给想要接收跳转的Activity添加配置:
<activity
android:name=".X5WebViewActivity"
android:configChanges="orientation|screenSize"
android:hardwareAccelerated="true"
android:launchMode="singleTask"
android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<!--需要添加下面的intent-filter配置-->
<intent-filter tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!--使用http,则只能打开http开头的网页-->
<data android:scheme="https" />
</intent-filter>
</activity>
然后在 X5WebViewActivity 中获取相关传递数据。具体可以看lib中的X5WebViewActivity类代码。
public class X5WebViewActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_web_view);
getIntentData();
initTitle();
initWebView();
webView.loadUrl(mUrl);
// 处理 作为三方浏览器打开传过来的值
getDataFromBrowser(getIntent());
}
/**
* 使用singleTask启动模式的Activity在系统中只会存在一个实例。
* 如果这个实例已经存在,intent就会通过onNewIntent传递到这个Activity。
* 否则新的Activity实例被创建。
*/
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
getDataFromBrowser(intent);
}
/**
* 作为三方浏览器打开传过来的值
* Scheme: https
* host: www.jianshu.com
* path: /p/yc
* url = scheme + "://" + host + path;
*/
private void getDataFromBrowser(Intent intent) {
Uri data = intent.getData();
if (data != null) {
try {
String scheme = data.getScheme();
String host = data.getHost();
String path = data.getPath();
String text = "Scheme: " + scheme + "\n" + "host: " + host + "\n" + "path: " + path;
Log.e("data", text);
String url = scheme + "://" + host + path;
webView.loadUrl(url);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
一些重点说明
在微信中“通过浏览器”打开自己的应用,然后将自己的应用切到后台。重复上面的操作,会一直创建应用的实例,这样肯定是不好的,为了避免这种情况我们设置启动模式为:launchMode="singleTask"。
02.优化汇总目录介绍
2.0.1 视频全屏播放按返回页面被放大(部分手机出现)
至于原因暂时没有找到,解决方案如下所示
/**
* 当缩放改变的时候会调用该方法
* @param view view
* @param oldScale 之前的缩放比例
* @param newScale 现在缩放比例
*/
@Override
public void onScaleChanged(WebView view, float oldScale, float newScale) {
super.onScaleChanged(view, oldScale, newScale);
//视频全屏播放按返回页面被放大的问题
if (newScale - oldScale > 7) {
//异常放大,缩回去。
view.setInitialScale((int) (oldScale / newScale * 100));
}
}
2.0.2 加载webView中的资源时,加快加载的速度优化,主要是针对图片
html代码下载到WebView后,webkit开始解析网页各个节点,发现有外部样式文件或者外部脚本文件时,会异步发起网络请求下载文件,但如果在这之前也有解析到image节点,那势必也会发起网络请求下载相应的图片。在网络情况较差的情况下,过多的网络请求就会造成带宽紧张,影响到css或js文件加载完成的时间,造成页面空白loading过久。解决的方法就是告诉WebView先不要自动加载图片,等页面finish后再发起图片加载。
//初始化的时候设置,具体代码在X5WebView类中
if(Build.VERSION.SDK_INT >= KITKAT) {
//设置网页在加载的时候暂时不加载图片
ws.setLoadsImagesAutomatically(true);
} else {
ws.setLoadsImagesAutomatically(false);
}
/**
* 当页面加载完成会调用该方法
* @param view view
* @param url url链接
*/
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
//页面finish后再发起图片加载
if(!webView.getSettings().getLoadsImagesAutomatically()) {
webView.getSettings().setLoadsImagesAutomatically(true);
}
}
2.0.3 自定义加载异常error的状态页面,比如下面这些方法中可能会出现error
当WebView加载页面出错时(一般为404 NOT FOUND),安卓WebView会默认显示一个出错界面。当WebView加载出错时,会在WebViewClient实例中的onReceivedError(),还有onReceivedTitle方法接收到错误
/**
* 请求网络出现error
* @param view view
* @param errorCode 错误
* @param description description
* @param failingUrl 失败链接
*/
@Override
public void onReceivedError(WebView view, int errorCode, String description, String
failingUrl) {
super.onReceivedError(view, errorCode, description, failingUrl);
if (errorCode == 404) {
//用javascript隐藏系统定义的404页面信息
String data = "Page NO FOUND!";
view.loadUrl("javascript:document.body.innerHTML=\"" + data + "\"");
} else {
if (webListener!=null){
webListener.showErrorView();
}
}
}
// 向主机应用程序报告Web资源加载错误。这些错误通常表明无法连接到服务器。
// 值得注意的是,不同的是过时的版本的回调,新的版本将被称为任何资源(iframe,图像等)
// 不仅为主页。因此,建议在回调过程中执行最低要求的工作。
// 6.0 之后
@Override
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
super.onReceivedError(view, request, error);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
X5WebUtils.log("服务器异常"+error.getDescription().toString());
}
//ToastUtils.showToast("服务器异常6.0之后");
//当加载错误时,就让它加载本地错误网页文件
//mWebView.loadUrl("file:///android_asset/errorpage/error.html");
if (webListener!=null){
webListener.showErrorView();
}
}
/**
* 这个方法主要是监听标题变化操作的
* @param view view
* @param title 标题
*/
@Override
public void onReceivedTitle(WebView view, String title) {
super.onReceivedTitle(view, title);
if (title.contains("404") || title.contains("网页无法打开")){
if (webListener!=null){
webListener.showErrorView();
}
} else {
// 设置title
}
}
2.0.4 WebView硬件加速导致页面渲染闪烁
4.0以上的系统我们开启硬件加速后,WebView渲染页面更加快速,拖动也更加顺滑。但有个副作用就是,当WebView视图被整体遮住一块,然后突然恢复时(比如使用SlideMenu将WebView从侧边滑出来时),这个过渡期会出现白块同时界面闪烁。解决这个问题的方法是在过渡期前将WebView的硬件加速临时关闭,过渡期后再开启
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
webview.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}
2.0.5 WebView加载证书错误
webView加载一些别人的url时候,有时候会发生证书认证错误的情况,这时候我们希望能够正常的呈现页面给用户,我们需要忽略证书错误,需要调用WebViewClient类的onReceivedSslError方法,调用handler.proceed()来忽略该证书错误。
/**
* 在加载资源时通知主机应用程序发生SSL错误
* 作用:处理https请求
* @param view view
* @param handler handler
* @param error error
*/
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
super.onReceivedSslError(view, handler, error);
if (error!=null){
String url = error.getUrl();
X5WebUtils.log("onReceivedSslError----异常url----"+url);
}
//https忽略证书问题
if (handler!=null){
//表示等待证书响应
handler.proceed();
// handler.cancel(); //表示挂起连接,为默认方式
// handler.handleMessage(null); //可做其他处理
}
}
2.0.6 web音频播放销毁后还有声音
WebView页面中播放了音频,退出Activity后音频仍然在播放,需要在Activity的onDestory()中调用
@Override
protected void onDestroy() {
try {
//有音频播放的web页面的销毁逻辑
//在关闭了Activity时,如果Webview的音乐或视频,还在播放。就必须销毁Webview
//但是注意:webview调用destory时,webview仍绑定在Activity上
//这是由于自定义webview构建时传入了该Activity的context对象
//因此需要先从父容器中移除webview,然后再销毁webview:
if (webView != null) {
ViewGroup parent = (ViewGroup) webView.getParent();
if (parent != null) {
parent.removeView(webView);
}
webView.removeAllViews();
webView.destroy();
webView = null;
}
} catch (Exception e) {
Log.e("X5WebViewActivity", e.getMessage());
}
super.onDestroy();
}
2.0.7 DNS采用和客户端API相同的域名
建立连接/服务器处理;在页面请求的数据返回之前,主要有以下过程耗费时间。
DNS
connection
服务器处理
DNS采用和客户端API相同的域名
DNS会在系统级别进行缓存,对于WebView的地址,如果使用的域名与native的API相同,则可以直接使用缓存的DNS而不用再发起请求图片。
举个简单例子,客户端请求域名主要位于api.yc.com,然而内嵌的WebView主要位于 i.yc.com。
当我们初次打开App时:客户端首次打开都会请求api.yc.com,其DNS将会被系统缓存。然而当打开WebView的时候,由于请求了不同的域名,需要重新获取i.yc.com的IP。静态资源同理,最好与客户端的资源域名保持一致。
2.0.8 如何设置白名单操作
客户端内的WebView都是可以通过客户端的某个schema打开的,而要打开页面的URL很多都并不写在客户端内,而是可以由URL中的参数传递过去的。上面4.0.5 使用scheme协议打开链接风险已经说明了scheme使用的危险性,那么如何避免这个问题了,设置运行访问的白名单。或者当用户打开外部链接前给用户强烈而明显的提示。具体操作如下所示:
在onPageStarted开始加载资源的方法中,获取加载url的host值,然后和本地保存的合法host做比较,这里domainList是一个数组
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
String host = Uri.parse(url).getHost();
LoggerUtils.i("host:" + host);
if (!BuildConfig.IS_DEBUG) {
if (Arrays.binarySearch(domainList, host) < 0) {
//不在白名单内,非法网址,这个时候给用户强烈而明显的提示
} else {
//合法网址
}
}
}
设置白名单操作其实和过滤广告是一个意思,这里你可以放一些合法的网址允许访问。
2.0.9 后台无法释放js导致发热耗电
在有些手机你如果webView加载的html里,有一些js一直在执行比如动画之类的东西,如果此刻webView 挂在了后台这些资源是不会被释放用户也无法感知。
导致一直占有cpu 耗电特别快,所以如果遇到这种情况,处理方式如下所示。大概意思就是在后台的时候,会调用onStop方法,即此时关闭js交互,回到前台调用onResume再开启js交互。
//在onStop里面设置setJavaScriptEnabled(false);
//在onResume里面设置setJavaScriptEnabled(true)。
@Override
protected void onResume() {
super.onResume();
if (mWebView != null) {
mWebView.getSettings().setJavaScriptEnabled(true);
}
}
@Override
protected void onStop() {
super.onStop();
if (mWebView != null) {
mWebView.getSettings().setJavaScriptEnabled(false);
}
}
2.1.0 可以提前显示加载进度条
提前显示进度条不是提升性能 , 但是对用户体验来说也是很重要的一点 , WebView.loadUrl("url") 不会立马就回调 onPageStarted 或者 onProgressChanged 因为在这一时间段,WebView 有可能在初始化内核,也有可能在与服务器建立连接,这个时间段容易出现白屏,白屏用户体验是很糟糕的 ,所以建议
//正确
pb.setVisibility(View.VISIBLE);
mWebView.loadUrl("https://github.com/yangchong211/LifeHelper");
//不太好
@Override
public void onPageStarted(WebView webView, String s, Bitmap bitmap) {
super.onPageStarted(webView, s, bitmap);
//设定加载开始的操作
pb.setVisibility(View.VISIBLE);
}
//下面这个是监听进度条进度变化的逻辑
mWebView.getX5WebChromeClient().setWebListener(interWebListener);
mWebView.getX5WebViewClient().setWebListener(interWebListener);
private InterWebListener interWebListener = new InterWebListener() {
@Override
public void hindProgressBar() {
pb.setVisibility(View.GONE);
}
@Override
public void showErrorView() {
}
@Override
public void startProgress(int newProgress) {
pb.setProgress(newProgress);
}
@Override
public void showTitle(String title) {
}
};
2.1.1 WebView密码明文存储漏洞优化
WebView 默认开启密码保存功能 mWebView.setSavePassword(true),如果该功能未关闭,在用户输入密码时,会弹出提示框,询问用户是否保存密码,如果选择”是”,密码会被明文保到 /data/data/com.package.name/databases/webview.db 中,这样就有被盗取密码的危险,所以需要通过 WebSettings.setSavePassword(false) 关闭密码保存提醒功能。
具体代码操作如下所示
/设置是否开启密码保存功能,不建议开启,默认已经做了处理,存在盗取密码的危险
mX5WebView.setSavePassword(false);
03.问题汇总目录介绍
3.0.0 WebView进化史介绍
进化史如下所示
从Android4.4系统开始,Chromium内核取代了Webkit内核。
从Android5.0系统开始,WebView移植成了一个独立的apk,可以不依赖系统而独立存在和更新。
从Android7.0 系统开始,如果用户手机里安装了 Chrome , 系统优先选择 Chrome 为应用提供 WebView 渲染。
从Android8.0系统开始,默认开启WebView多进程模式,即WebView运行在独立的沙盒进程中。
3.0.1 提前初始化WebView必要性
第一次打开Web面 ,使用WebView加载页面的时候特别慢,第二次打开就能明显的感觉到速度有提升,为什么?
是因为在你第一次加载页面的时候 WebView 内核并没有初始化 ,所以在第一次加载页面的时候需要耗时去初始化WebView内核 。
提前初始化WebView内核 ,例如如下把它放到了Application里面去初始化 , 在页面里可以直接使用该WebView,这种方法可以比较有效的减少WebView在App中的首次打开时间。当用户访问页面时,不需要初始化WebView的时间。
但是这样也有不好的地方,额外的内存消耗。页面间跳转需要清空上一个页面的痕迹,更容易内存泄露。
3.0.2 x5加载office资源
关于加载word,pdf,xls等文档文件注意事项:Tbs不支持加载网络的文件,需要先把文件下载到本地,然后再加载出来
还有一点要注意,在onDestroy方法中调用此方法mTbsReaderView.onStop(),否则第二次打开无法浏览。更多可以看FileReaderView类代码!
3.0.3 WebView播放视频问题
1、此次的方案用到WebView,而且其中会有视频嵌套,在默认的WebView中直接播放视频会有问题, 而且不同的SDK版本情况还不一样,网上搜索了下解决方案,在此记录下. webView.getSettings.setPluginState(PluginState.ON);webView.setWebChromeClient(new WebChromeClient());
2、然后在webView的Activity配置里面加上: android:hardwareAccelerated="true"
3、以上可以正常播放视频了,但是webview的页面都finish了居然还能听 到视频播放的声音, 于是又查了下发现webview的onResume方法可以继续播放,onPause可以暂停播放, 但是这两个方法都是在Added in API level 11添加的,所以需要用反射来完成。
4、停止播放:在页面的onPause方法中使用:webView.getClass().getMethod("onPause").invoke(webView, (Object[])null);
5、继续播放:在页面的onResume方法中使用:webView.getClass().getMethod("onResume").invoke(webView,(Object[])null);这样就可以控制视频的暂停和继续播放了。
3.0.4 无法获取webView的正确高度
偶发情况,获取不到webView的内容高度
其中htmlString是一个HTML格式的字符串。
webView.loadData(htmlString, "text/html", "utf-8");
webView.setWebViewClient(new WebViewClient() {
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
Log.d("yc", view.getContentheight() + "");
}
});
这是因为onPageFinished回调指的WebView已经完成从网络读取的字节数,这一点。在点onPageFinished被激发的页面可能还没有被解析。
第一种解决办法:提供onPageFinished()一些延迟
webView.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
webView.postDelayed(new Runnable() {
@Override
public void run() {
int contentHeight = webView.getContentHeight();
int viewHeight = webView.getHeight();
}
}, 500);
}
});
第二种解决办法:使用js获取内容高度,具体可以看这篇文章:https://www.jianshu.com/p/ad22b2649fba
3.0.5 使用scheme协议打开链接风险
常见的用法是在APP获取到来自网页的数据后,重新生成一个intent,然后发送给别的组件使用这些数据。比如使用Webview相关的Activity来加载一个来自网页的url,如果此url来自url scheme中的参数,如:yc://ycbjie:8888/from?load_url=http://www.taobao.com。
如果在APP中,没有检查获取到的load_url的值,攻击者可以构造钓鱼网站,诱导用户点击加载,就可以盗取用户信息。
这个时候,别人非法篡改参数,于是将scheme协议改成yc://ycbjie:8888/from?load_url=http://www.doubi.com。这个时候点击进去即可进入钓鱼链接地址。
使用建议
APP中任何接收外部输入数据的地方都是潜在的攻击点,过滤检查来自网页的参数。
不要通过网页传输敏感信息,有的网站为了引导已经登录的用户到APP上使用,会使用脚本动态的生成URL Scheme的参数,其中包括了用户名、密码或者登录态token等敏感信息,让用户打开APP直接就登录了。恶意应用也可以注册相同的URL Sechme来截取这些敏感信息。Android系统会让用户选择使用哪个应用打开链接,但是如果用户不注意,就会使用恶意应用打开,导致敏感信息泄露或者其他风险。
解决办法
在内嵌的WebView中应该限制允许打开的WebView的域名,并设置运行访问的白名单。或者当用户打开外部链接前给用户强烈而明显的提示。具体操作可以看5.0.8 如何设置白名单操作方式。
3.0.6 如何处理加载错误(Http、SSL、Resource)
对于WebView加载一个网页过程中所产生的错误回调,大致有三种
/**
* 只有在主页面加载出现错误时,才会回调这个方法。这正是展示加载错误页面最合适的方法。
* 然而,如果不管三七二十一直接展示错误页面的话,那很有可能会误判,给用户造成经常加载页面失败的错觉。
* 由于不同的WebView实现可能不一样,所以我们首先需要排除几种误判的例子:
* 1.加载失败的url跟WebView里的url不是同一个url,排除;
* 2.errorCode=-1,表明是ERROR_UNKNOWN的错误,为了保证不误判,排除
* 3failingUrl=null&errorCode=-12,由于错误的url是空而不是ERROR_BAD_URL,排除
* @param webView webView
* @param errorCode errorCode
* @param description description
* @param failingUrl failingUrl
*/
@Override
public void onReceivedError(WebView webView, int errorCode,
String description, String failingUrl) {
super.onReceivedError(webView, errorCode, description, failingUrl);
// -12 == EventHandle.ERROR_BAD_URL, a hide return code inside android.net.http package
if ((failingUrl != null && !failingUrl.equals(webView.getUrl())
&& !failingUrl.equals(webView.getOriginalUrl())) /* not subresource error*/
|| (failingUrl == null && errorCode != -12) /*not bad url*/
|| errorCode == -1) { //当 errorCode = -1 且错误信息为 net::ERR_CACHE_MISS
return;
}
if (!TextUtils.isEmpty(failingUrl)) {
if (failingUrl.equals(webView.getUrl())) {
//做自己的错误操作,比如自定义错误页面
}
}
}
/**
* 只有在主页面加载出现错误时,才会回调这个方法。这正是展示加载错误页面最合适的方法。
* 然而,如果不管三七二十一直接展示错误页面的话,那很有可能会误判,给用户造成经常加载页面失败的错觉。
* 由于不同的WebView实现可能不一样,所以我们首先需要排除几种误判的例子:
* 1.加载失败的url跟WebView里的url不是同一个url,排除;
* 2.errorCode=-1,表明是ERROR_UNKNOWN的错误,为了保证不误判,排除
* 3failingUrl=null&errorCode=-12,由于错误的url是空而不是ERROR_BAD_URL,排除
* @param webView webView
* @param webResourceRequest webResourceRequest
* @param webResourceError webResourceError
*/
@Override
public void onReceivedError(WebView webView, WebResourceRequest webResourceRequest,
WebResourceError webResourceError) {
super.onReceivedError(webView, webResourceRequest, webResourceError);
}
/**
* 任何HTTP请求产生的错误都会回调这个方法,包括主页面的html文档请求,iframe、图片等资源请求。
* 在这个回调中,由于混杂了很多请求,不适合用来展示加载错误的页面,而适合做监控报警。
* 当某个URL,或者某个资源收到大量报警时,说明页面或资源可能存在问题,这时候可以让相关运营及时响应修改。
* @param webView webView
* @param webResourceRequest webResourceRequest
* @param webResourceResponse webResourceResponse
*/
@Override
public void onReceivedHttpError(WebView webView, WebResourceRequest webResourceRequest,
WebResourceResponse webResourceResponse) {
super.onReceivedHttpError(webView, webResourceRequest, webResourceResponse);
}
/**
* 任何HTTPS请求,遇到SSL错误时都会回调这个方法。
* 比较正确的做法是让用户选择是否信任这个网站,这时候可以弹出信任选择框供用户选择(大部分正规浏览器是这么做的)。
* 有时候,针对自己的网站,可以让一些特定的网站,不管其证书是否存在问题,都让用户信任它。
* 坑:有时候部分手机打开页面报错,绝招:让自己网站的所有二级域都是可信任的。
* @param webView webView
* @param sslErrorHandler sslErrorHandler
* @param sslError sslError
*/
@Override
public void onReceivedSslError(WebView webView, SslErrorHandler sslErrorHandler, SslError sslError) {
super.onReceivedSslError(webView, sslErrorHandler, sslError);
//判断网站是否是可信任的,与自己网站host作比较
if (WebViewUtils.isYCHost(webView.getUrl())) {
//如果是自己的网站,则继续使用SSL证书
sslErrorHandler.proceed();
} else {
super.onReceivedSslError(webView, sslErrorHandler, sslError);
}
}
3.0.7 webView防止内存泄漏
https://my.oschina.net/zhibuji/blog/100580
3.0.9 视频/图片宽度超过屏幕
视频播放宽度或者图片宽度比webView设置的宽度大,超过屏幕:这个时候可以设置ws.setLoadWithOverviewMode(false);
另外一种让图片不超出屏幕范围的方法,可以用的是css
<script type="text/javascript">
var tables = document.getElementsByTagName("img"); //找到table标签
for(var i = 0; i<tables.length; i++){ // 逐个改变
tables[i].style.width = "100%"; // 宽度改为100%
tables[i].style.height = "auto";
}
</script>
通过webView的setting属性设置
// 网页内容的宽度是否可大于WebView控件的宽度
ws.setLoadWithOverviewMode(false);
3.1.0 如何保证js安全性
Android和js如何通信
为了与Web页面实现动态交互,Android应用程序允许WebView通过WebView.addJavascriptInterface接口向Web页面注入Java对象,页面Javascript脚本可直接引用该对象并调用该对象的方法。
这类应用程序一般都会有类似如下的代码:
webView.addJavascriptInterface(javaObj, "jsObj");
此段代码将javaObj对象暴露给js脚本,可以通过jsObj对象对其进行引用,调用javaObj的方法。结合Java的反射机制可以通过js脚本执行任意Java代码,相关代码如下:
当受影响的应用程序执行到上述脚本的时候,就会执行someCmd指定的命令。
<script>
function execute(cmdArgs) {
return jsobj.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec(cmdArgs);
}
execute(someCmd);
</script>
addJavascriptInterface任何命令执行漏洞
在webView中使用js与html进行交互是一个不错的方式,但是,在Android4.2(16,包含4.2)及以下版本中,如果使用addJavascriptInterface,则会存在被注入js接口的漏洞;在4.2之后,由于Google增加了@JavascriptInterface,该漏洞得以解决。
@JavascriptInterface注解做了什么操作
之前,任何Public的函数都可以在JS代码中访问,而Java对象继承关系会导致很多Public的函数都可以在JS中访问,其中一个重要的函数就是getClass()。然后JS可以通过反射来访问其他一些内容。通过引入 @JavascriptInterface注解,则在JS中只能访问 @JavascriptInterface注解的函数。这样就可以增强安全性。
3.1.1 如何代码开启硬件加速
开启软硬件加速这个性能提升还是很明显的,但是会耗费更大的内存 。直接调用代码api即可完成,webView.setOpenLayerType(true);
3.1.2 WebView设置Cookie
h5页面为何要设置cookie,主要是避免网页重复登录,作用是记录用户登录信息,下次进去不需要重复登录。
代码里怎么设置Cookie,如下所示
/**
* 同步cookie
*
* @param url 地址
* @param cookieList 需要添加的Cookie值,以键值对的方式:key=value
*/
private void syncCookie (Context context , String url, ArrayList<String> cookieList) {
//初始化
CookieSyncManager.createInstance(context);
//获取对象
CookieManager cookieManager = CookieManager.getInstance();
cookieManager.setAcceptCookie(true);
//移除
cookieManager.removeSessionCookie();
//添加
if (cookieList != null && cookieList.size() > 0) {
for (String cookie : cookieList) {
cookieManager.setCookie(url, cookie);
}
}
String cookies = cookieManager.getCookie(url);
X5LogUtils.d("cookies-------"+cookies);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
cookieManager.flush();
} else {
CookieSyncManager.getInstance().sync();
}
}
在android里面在调用webView.loadUrl(url)之前一句调用此方法就可以给WebView设置Cookie
注:这里一定要注意一点,在调用设置Cookie之后不能再设置,否则设置Cookie无效。该处需要校验,为何???
webView.getSettings().setBuiltInZoomControls(true);
webView.getSettings().setJavaScriptEnabled(true);
还有跨域问题: 域A: test1.yc.com 域B: test2.yc.com
那么在域A生产一个可以使域A和域B都能访问的Cookie就需要将Cookie的domain设置为.yc.com;
如果要在域A生产一个令域A不能访问而域能访问的Cookie就要将Cookie设置为test2.yc.com。
Cookie的过期机制
可以设置Cookie的生效时间字段名为: expires 或 max-age。
expires:过期的时间点
max-age:生效的持续时间,单位为秒。
若将Cookie的 max-age 设置为负数,或者 expires 字段设置为过期时间点,数据库更新后这条Cookie将从数据库中被删除。如果将Cookie的 max-age 和 expires 字段设置为正常的过期日期,则到期后再数据库更新时会删除该条数据。
下面列出几个有用的接口:
获取某个url下的所有Cookie:CookieManager.getInstance().getCookie(url)
判断WebView是否接受Cookie:CookieManager.getInstance().acceptCookie()
清除Session Cookie:CookieManager.getInstance().removeSessionCookies(ValueCallback callback)
清除所有Cookie:CookieManager.getInstance().removeAllCookies(ValueCallback callback)
Cookie持久化:CookieManager.getInstance().flush()
针对某个主机设置Cookie:CookieManager.getInstance().setCookie(String url, String value)
3.1.4 webView加载网页不显示图片
webView从Lollipop(5.0)开始webView默认不允许混合模式, https当中不能加载http资源, 而开发的时候可能使用的是https的链接, 但是链接中的图片可能是http的, 所以需要设置开启。
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) {
mWebView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}
mWebView.getSettings().setBlockNetworkImage(false);
3.1.5 绕过证书校验漏洞
webviewClient中有onReceivedError方法,当出现证书校验错误时,我们可以在该方法中使用handler.proceed()来忽略证书校验继续加载网页,或者使用默认的handler.cancel()来终端加载。
因为我们使用了handler.proceed(),由此产生了该“绕过证书校验漏洞”。如果确定所有页面都能满足证书校验,则不必要使用handler.proceed()
@SuppressLint("NewApi")
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
//handler.proceed();// 接受证书
super.onReceivedSslError(view, handler, error);
}
3.1.6 allowFileAccess漏洞
如果webView.getSettings().setAllowFileAccess(boolean)设置为true,则会面临该问题;该漏洞是通过WebView对Javascript的延时执行和html文件替换产生的。
解决方案是禁止WebView页面打开本地文件,即:webView.getSettings().setAllowFileAccess(false);
或者更直接的禁止使用JavaScript:webView.getSettings().setJavaScriptEnabled(false);
3.1.7 WebView嵌套ScrollView问题
问题描述
当 WebView 嵌套在 ScrollView 里面的时候,如果 WebView 先加载了一个高度很高的网页,然后加载了一个高度很低的网页,就会造成 WebView 的高度无法自适应,底部出现大量空白的情况出现。
解决办法
可以参考这篇博客:https://blog.csdn.net/self_study/article/details/54378978
3.1.8 WebView中图片点击放大
首先载入js
//将js对象与java对象进行映射
webView.addJavascriptInterface(new ImageJavascriptInterface(context), "imagelistener");
html加载完成之后,添加监听图片的点击js函数,这个可以在onPageFinished方法中操作
@Override
public void onPageFinished(WebView view, String url) {
X5LogUtils.i("-------onPageFinished-------"+url);
//html加载完成之后,添加监听图片的点击js函数
//addImageClickListener();
addImageArrayClickListener(webView);
}
具体看addImageArrayClickListener的实现方法。
/**
* android与js交互:
* 首先我们拿到html中加载图片的标签img.
* 然后取出其对应的src属性
* 循环遍历设置图片的点击事件
* 将src作为参数传给java代码
* 这个循环将所图片放入数组,当js调用本地方法时传入。
* 当然如果采用方式一获取图片的话,本地方法可以不需要传入这个数组
* 通过js代码找到标签为img的代码块,设置点击的监听方法与本地的openImage方法进行连接
* @param webView webview
*/
private void addImageArrayClickListener(WebView webView) {
webView.loadUrl("javascript:(function(){" +
"var objs = document.getElementsByTagName(\"img\"); " +
"var array=new Array(); " +
"for(var j=0;j<objs.length;j++){" +
" array[j]=objs[j].src; " +
"}"+
"for(var i=0;i<objs.length;i++) " +
"{"
+ " objs[i].onclick=function() " +
" { "
+ " window.imagelistener.openImage(this.src,array); " +
" } " +
"}" +
"})()");
}
最后看看js的通信接口做了什么
public class ImageJavascriptInterface {
private Context context;
private String[] imageUrls;
public ImageJavascriptInterface(Context context,String[] imageUrls) {
this.context = context;
this.imageUrls = imageUrls;
}
public ImageJavascriptInterface(Context context) {
this.context = context;
}
/**
* 接口返回的方式
*/
@android.webkit.JavascriptInterface
public void openImage(String img , String[] imageUrls) {
Intent intent = new Intent();
intent.putExtra("imageUrls", imageUrls);
intent.putExtra("curImageUrl", img);
// intent.setClass(context, PhotoBrowserActivity.class);
context.startActivity(intent);
for (int i = 0; i < imageUrls.length; i++) {
Log.e("图片地址"+i,imageUrls[i].toString());
}
}
}
3.1.9 页面滑动期间不渲染/执行
在有些需求中会有一些吸顶的元素,例如导航条,购买按钮等;当页面滚动超出元素高度后,元素吸附在屏幕顶部。在WebView中成了难题:在页面滚动期间,Scroll Event不触发。不仅如此,WebView在滚动期间还有各种限定:
setTimeout和setInterval不触发。
GIF动画不播放。
很多回调会延迟到页面停止滚动之后。
background-position: fixed不支持。
这些限制让WebView在滚动期间很难有较好的体验。这些限制大部分是不可突破的,但至少对于吸顶功能还是可以做一些支持,解决方法:
在Android上,监听touchMove事件可以在滑动期间做元素的position切换(惯性运动期间就无效了)。
参考美团技术文章
3.2.0 被运营商劫持和注入问题
由于WebView加载的页面代码是从服务器动态获取的,这些代码将会很容易被中间环节所窃取或者修改,其中最主要的问题出自地方运营商和一些WiFi。监测到的问题包括:
无视通信规则强制缓存页面。
header被篡改。
页面被注入广告。
页面被重定向。
页面被重定向并重新iframe到新页面,框架嵌入广告。
HTTPS请求被拦截。
DNS劫持。
针对页面注入的行为,有一些解决方案:
1.使用CSP(Content Security Policy)
2.HTTPS。
HTTPS可以防止页面被劫持或者注入,然而其副作用也是明显的,网络传输的性能和成功率都会下降,而且HTTPS的页面会要求页面内所有引用的资源也是HTTPS的,对于大型网站其迁移成本并不算低。HTTPS的一个问题在于:一旦底层想要篡改或者劫持,会导致整个链接失效,页面无法展示。这会带来一个问题:本来页面只是会被注入广告,而且广告会被CSP拦截,而采用了HTTPS后,整个网页由于受到劫持完全无法展示。
对于安全要求不高的静态页面,就需要权衡HTTPS带来的利与弊了。
3.App使用Socket代理请求
如果HTTP请求容易被拦截,那么让App将其转换为一个Socket请求,并代理WebView的访问也是一个办法。
通常不法运营商或者WiFi都只能拦截HTTP(S)请求,对于自定义的包内容则无法拦截,因此可以基本解决注入和劫持的问题。
Socket代理请求也存在问题:
首先,使用客户端代理的页面HTML请求将丧失边下载边解析的能力;根据前面所述,浏览器在HTML收到部分内容后就立刻开始解析,并加载解析出来的外链、图片等,执行内联的脚本……而目前WebView对外并没有暴露这种流式的HTML接口,只能由客户端完全下载好HTML后,注入到WebView中。因此其性能将会受到影响。
其次,其技术问题也是较多的,例如对跳转的处理,对缓存的处理,对CDN的处理等等……稍不留神就会埋下若干大坑。
此外还有一些其他的办法,例如页面的MD5检测,页面静态页打包下载等等方式,具体如何选择还要根据具体的场景抉择。
3.2.1 解决资源加载缓慢问题
在资源预加载方面,其实也有很多种方式,下面主要列举了一些:
第一种方式是使用 WebView 自身的缓存机制:如果我们在 APP 里面访问一个页面,短时间内再次访问这个页面的时候,就会感觉到第二次打开的时候顺畅很多,加载速度比第一次的时间要短,这个就是因为 WebView 自身内部会做一些缓存,只要打开过的资源,他都会试着缓存到本地,第二次需要访问的时候他直接从本地读取,但是这个读取其实是不太稳定的东西,关掉之后,或者说这种缓存失效之后,系统会自动把它清除,我们没办法进行控制。基于这个 WebView 自身的缓存,有一种资源预加载的方案就是,我们在应用启动的时候可以开一个像素的 WebView ,事先去访问一下我们常用的资源,后续打开页面的时候如果再用到这些资源他就可以从本地获取到,页面加载的时间会短一些。
第二种方案是,自己去构建和管理缓存:把这些需要预加载的资源放在 APP 里面,可能是预先放进去的,也可能是后续下载的,问题在于前端这些页面怎么去缓存,两个方案,第一种是前端可以在 H5 打包的时候把里面的资源 URL 进行替换,这样可以直接访问本地的地址;第二种是客户端可以拦截这些网页发出的所有请求做替换。
具体可以看美团的技术文章:美团大众点评 Hybrid 化建设
3.2.2 判断是否已经滚动到页面底端
getScrollY()方法返回的是当前可见区域的顶端距整个页面顶端的距离,也就是当前内容滚动的距离.
getHeight()或者getBottom()方法都返回当前WebView 这个容器的高度
getContentHeight 返回的是整个html的高度,但并不等同于当前整个页面的高度,因为WebView有缩放功能,所以当前整个页面的高度实际上应该是原始html 的高度再乘上缩放比例. 因此,更正后的结果,准确的判断方法应该是:
if(WebView.getContentHeight*WebView.getScale() == (webview.getHeight()+WebView.getScrollY())){
//已经处于底端
}
3.2.3 使用loadData加载html乱码
可以通过使用 WebView.loadData(String data, String mimeType, String encoding)) 方法来加载一整个 HTML 页面的一小段内容,第一个就是我们需要 WebView 展示的内容,第二个是我们告诉 WebView 我们展示内容的类型,一般,第三个是字节码,但是使用的时候,这里会有一些坑
明明已经指定了编码格式为 UTF-8,加载却还会出现乱码……
String html = new String("<h3>我是loadData() 的标题</h3><p>&nbsp&nbsp我是他的内容</p>");
webView.loadData(html, "text/html", "UTF-8");
使用loadData()或 loadDataWithBaseURL()加载一段HTML代码片段
data:是要加载的数据类型,但在数据里面不能出现英文字符:'#', '%', '' , '?' 这四个字符,如果有的话可以用 %23, %25, %27, %3f,这些字符来替换,在平时测试时,你的数据时,你的数据里含有这些字符,但不会出问题,当出问题时,你可以替换下。
%,会报找不到页面错误,页面全是乱码。乱码样式见符件。
,会让你的goBack失效,但canGoBAck是可以使用的。于是就会产生返回按钮生效,但不能返回的情况。
和? 我在转换时,会报错,因为它会把当作转义符来使用,如果用两级转义,也不生效,我是对它无语了。
我们在使用loadData时,就意味着需要把所有的非法字符全部转换掉,这样就会给运行速度带来很大的影响,因为在使用时,在页面stytle中会使用很多%号。页面的数据越多,运行的速度就会越慢。
data中,有人会遇到中文乱码问题,解决办法:参数传"utf-8",页面的编码格式也必须是utf-8,这样编码统一就不会乱了。别的编码我也没有试过。
解决办法
String html = new String("<h3>我是loadData() 的标题</h3><p>&nbsp&nbsp我是他的内容</p>");
webView.loadData(html, "text/html;charset=UTF-8", "null");
3.2.4 WebView下载进度无法监听
https://www.jianshu.com/p/6e38e1ef203a
3.2.5 webView出现302/303重定向
专业叙述
302重定向又称之为302代表暂时性转移
网络解释
重定向是网页制作中的一个知识,几个例子跟你说明,假设你现在所处的位置是一个论坛的登录页面,你填写了帐号,密码,点击登陆,如果你的帐号密码正确,就自动跳转到论坛的首页,不正确就返回登录页;这里的自动跳转,就是重定向的意思。或者可以说,重定向就是,在网页上设置一个约束条件,条件满足,就自动转入到其它网页、网址 。比如,你输入一个网站链接,一般可以直接进入网站,如果出现错误,则又跳转到另外一个网页。
举个例子
叙述下这种问题的情况,就是WebView首先加载A链接,然后在WebView上点击一个B链接进行加载,B链接会自动跳转到C链接,这个时候调用WebView的goback方法,会返回到加载B链接,但是B链接又会跳转到C链接,从而导致没法返回到A链接界面(当然也有朋友说快速的按两次返回键-也就是连续触发了两次goback可以返回到A链接,但并不是所有用户都懂这个,而且操作上也很恶心。),这就是重定向问题。
实现WebView的滑动监听和优雅处理回退栈问题
WebView能否知道某个url是不是301/302呢?当然知道,WebView能够拿到url的请求信息和响应信息,根据header里的code很轻松就可以实现,事实正是如此,交给WebView来处理重定向(return false),这时候按返回键,是可以正常地回到重定向之前的那个页面的。(PS:从上面的章节可知,WebView在5.0以后是一个独立的apk,可以单独升级,新版本的WebView实现肯定处理了重定向问题)
但是,业务对url拦截有需求,肯定不能把所有的情况都交给系统WebView处理。为了解决url拦截问题,本文引入了另一种思想——通过用户的touch事件来判断重定向。具体可以看项目lib中的ScrollWebView!
04.关于参考
感谢开源库
x5官方开发文档
JsBridge开源库
WebViewStudy开源库
参考博客
WebView性能、体验分析与优化
WebView详解,常见漏洞详解和安全源码
05.关于x5开源库YCWebView
5.0.1 前沿说明
基于腾讯x5封源库,提高webView开发效率,大概要节约你百分之六十的时间成本。该案例支持处理js的交互逻辑且无耦合、同时暴露进度条加载进度、可以监听异常error状态、支持视频播放并且可以全频、支持加载word,xls,ppt,pdf,txt等文件文档、发短信、打电话、发邮件、打开文件操作上传图片、唤起原生App、x5库为最新版本,功能强大。
5.0.2 该库功能和优势
提高webView开发效率,大概要节约你百分之六十的时间成本,一键初始化操作;
支持处理js的交互逻辑,方便快捷,并且无耦合,操作十分简单;
暴露进度条加载进度,结束,以及异常状态(分多种状态:无网络,404,onReceivedError,sslError异常等)listener给开发者;
支持视频播放,可以切换成全频播放视频,可旋转屏幕,暴露视频操作监听listener给开发者;
集成了腾讯x5的WebView,最新版本,功能强大;
支持打开文件的操作,比如打开相册,然后选中图片上传,兼容版本(5.0);
支持加载word,xls,ppt,pdf,txt等文件文档,使用方法十分简单;
支持设置仿微信加载H5页面进度条,完全无耦合,操作简单,极大提高用户体验;
5.0.3 项目地址
https://github.com/yangchong211/YCWebView