首页> 搜索结果页
"IOS 开发之应用唤起实现原理详解" 检索
共 5 条结果
了解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)
文章
Android开发  ·  iOS开发
2018-07-16
Netty干货分享:京东京麦的生产级TCP网关技术实践总结
1、引言 京东的京麦商家后台2014年构建网关,从HTTP网关发展到TCP网关。在2016年重构完成基于Netty4.x+Protobuf3.x实现对接PC和App上下行通信的高可用、高性能、高稳定的TCP长连接网关。 早期京麦搭建HTTP和TCP长连接功能主要用于消息通知的推送,并未应用于API网关。随着逐步对NIO的深入学习和对Netty框架的了解,以及对系统通信稳定能力的愈加高要求,采用NIO技术应用网关实现API请求调用的想法,最终在2016年实现,并完全支撑业务化运行。由于诸多的改进,包括TCP长连接容器、Protobuf的序列化、服务泛化调用框架等等,性能比HTTP网关提升10倍以上,稳定性也远远高于HTTP网关。 本文重点介绍京麦TCP网关的技术架构及Netty的应用实践。 简单介绍一下京麦是什么: 京麦工作台是京东商城为京东的商家准备的一款后台管理工具,它可以使您不登陆商家后台就能进行订单生产,快速实现订单下载发货流程。类似于淘宝的旺旺商家版(现在叫淘宝千牛)这样的东西。 学习交流: - 即时通讯开发交流群:320837163  [推荐] - 移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM》 (本文同步发布于:http://www.52im.net/thread-1243-1-1.html ) 2、本文作者 张松然 - 京东商家研发部架构师; - 丰富的构建高性能高可用大规模分布式系统的研发、架构经验; - 2013年加入京东,目前负责京麦服务网关和京麦服务市场的系统研发工作。 3、TCP网关的网络结构 基于Netty构建京麦TCP网关的长连接容器,作为网关接入层提供服务API请求调用。 客户端通过域名+端口访问TCP网关,域名不同的运营商对应不同的VIP,VIP发布在LVS上,LVS将请求转发给后端的HAProxy,再由HAProxy把请求转发给后端的Netty的IP+Port。 LVS转发给后端的HAProxy,请求经过LVS,但是响应是HAProxy直接反馈给客户端的,这也就是LVS的DR模式。 4、TCP网关长连接容器架构 TCP网关的核心组件是Netty,而Netty的NIO模型是Reactor反应堆模型(Reactor相当于有分发功能的多路复用器Selector)。每一个连接对应一个Channel(多路指多个Channel,复用指多个连接复用了一个线程或少量线程,在Netty指EventLoop),一个Channel对应唯一的ChannelPipeline,多个Handler串行的加入到Pipeline中,每个Handler关联唯一的ChannelHandlerContext。 TCP网关长连接容器的Handler就是放在Pipeline的中。我们知道TCP属于OSI的传输层,所以建立Session管理机制构建会话层来提供应用层服务,可以极大的降低系统复杂度。所以,每一个Channel对应一个Connection,一个Connection又对应一个Session,Session由Session Manager管理,Session与Connection是一一对应,Connection保存着ChannelHandlerContext(ChannelHanderContext可以找到Channel),Session通过心跳机制来保持Channel的Active状态。 每一次Session的会话请求(ChannelRead)都是通过Proxy代理机制调用Service层,数据请求完毕后通过写入ChannelHandlerConext再传送到Channel中。数据下行主动推送也是如此,通过Session Manager找到Active的Session,轮询写入Session中的ChannelHandlerContext,就可以实现广播或点对点的数据推送逻辑。如下图所示。 京麦TCP网关使用Netty Channel进行数据通信,使用Protobuf进行序列化和反序列化,每个请求都将被封装成Byte二进制字节流,在整个生命周期中,Channel保持长连接,而不是每次调用都重新创建Channel,达到链接的复用。 我们接下来来看看基于Netty的具体技术实践。 5、TCP网关Netty Server的IO模型 具体的实现过程如下: 1)创建ServerBootstrap,设定BossGroup与WorkerGroup线程池; 2)bind指定的port,开始侦听和接受客户端链接(如果系统只有一个服务端port需要监听,则BossGroup线程组线程数设置为1); 3)在ChannelPipeline注册childHandler,用来处理客户端链接中的请求帧。 6、TCP网关的线程模型 TCP网关使用Netty的线程池,共三组线程池,分别为BossGroup、WorkerGroup和ExecutorGroup。其中,BossGroup用于接收客户端的TCP连接,WorkerGroup用于处理I/O、执行系统Task和定时任务,ExecutorGroup用于处理网关业务加解密、限流、路由,及将请求转发给后端的抓取服务等业务操作。 NioEventLoop是Netty的Reactor线程,其角色: 1)Boss Group:作为服务端Acceptor线程,用于accept客户端链接,并转发给WorkerGroup中的线程; 2)Worker Group:作为IO线程,负责IO的读写,从SocketChannel中读取报文或向SocketChannel写入报文; 3)Task Queue/Delay Task Queu:作为定时任务线程,执行定时任务,例如链路空闲检测和发送心跳消息等。 7、TCP网关执行时序图 如上图所示,其中步骤一至步骤九是Netty服务端的创建时序,步骤十至步骤十三是TCP网关容器创建的时序。 步骤一:创建ServerBootstrap实例,ServerBootstrap是Netty服务端的启动辅助类。 步骤二:设置并绑定Reactor线程池,EventLoopGroup是Netty的Reactor线程池,EventLoop负责所有注册到本线程的Channel。 步骤三:设置并绑定服务器Channel,Netty Server需要创建NioServerSocketChannel对象。 步骤四:TCP链接建立时创建ChannelPipeline,ChannelPipeline本质上是一个负责和执行ChannelHandler的职责链。 步骤五:添加并设置ChannelHandler,ChannelHandler串行的加入ChannelPipeline中。 步骤六:绑定监听端口并启动服务端,将NioServerSocketChannel注册到Selector上。 步骤七:Selector轮训,由EventLoop负责调度和执行Selector轮询操作。 步骤八:执行网络请求事件通知,轮询准备就绪的Channel,由EventLoop执行ChannelPipeline。 步骤九:执行Netty系统和业务ChannelHandler,依次调度并执行ChannelPipeline的ChannelHandler。 步骤十:通过Proxy代理调用后端服务,ChannelRead事件后,通过发射调度后端Service。 步骤十一:创建Session,Session与Connection是相互依赖关系。 步骤十二:创建Connection,Connection保存ChannelHandlerContext。 步骤十三:添加SessionListener,SessionListener监听SessionCreate和SessionDestory等事件。 8、TCP网关源码分析 8.1 Session管理 Session是客户端与服务端建立的一次会话链接,会话信息中保存着SessionId、连接创建时间、上次访问事件,以及Connection和SessionListener,在Connection中保存了Netty的ChannelHandlerContext上下文信息。Session会话信息会保存在SessionManager内存管理器中。 创建Session的源码: 通过源码分析,如果Session已经存在销毁Session,但是这个需要特别注意,创建Session一定不要创建那些断线重连的Channel,否则会出现Channel被误销毁的问题。因为如果在已经建立Connection(1)的Channel上,再建立Connection(2),进入session.close方法会将cxt关闭,Connection(1)和Connection(2)的Channel都将会被关闭。在断线之后再建立连接Connection(3),由于Session是有一定延迟,Connection(3)和Connection(1/2)不是同一个,但Channel可能是同一个。 所以,如何处理是否是断线重练的Channel,具体的方法是在Channel中存入SessionId,每次事件请求判断Channel中是否存在SessionId,如果Channel中存在SessionId则判断为断线重连的Channel,代码如下图所示。 8.2 心跳 心跳是用来检测保持连接的客户端是否还存活着,客户端每间隔一段时间就会发送一次心跳包上传到服务端,服务端收到心跳之后更新Session的最后访问时间。在服务端长连接会话检测通过轮询Session集合判断最后访问时间是否过期,如果过期则关闭Session和Connection,包括将其从内存中删除,同时注销Channel等。如下图代码所示。 通过源码分析,在每个Session创建成功之后,都会在Session中添加TcpHeartbeatListener这个心跳检测的监听,TcpHeartbeatListener是一个实现了SessionListener接口的守护线程,通过定时休眠轮询Sessions检查是否存在过期的Session,如果轮训出过期的Session,则关闭Session。如下图代码所示。 同时,注意到session.connect方法,在connect方法中会对Session添加的Listeners进行添加时间,它会循环调用所有Listner的sessionCreated事件,其中TcpHeartbeatListener也是在这个过程中被唤起。如下图代码所示。 8.3 数据上行 数据上行特指从客户端发送数据到服务端,数据从ChannelHander的channelRead方法获取数据。数据包括创建会话、发送心跳、数据请求等。这里注意的是,channelRead的数据包括客户端主动请求服务端的数据,以及服务端下行通知客户端的返回数据,所以在处理object数据时,通过数据标识区分是请求-应答,还是通知-回复。如下图代码所示。 8.4 数据下行 数据下行通过MQ广播机制到所有服务器,所有服务器收到消息后,获取当前服务器所持有的所有Session会话,进行数据广播下行通知。如果是点对点的数据推送下行,数据也是先广播到所有服务器,每天服务器判断推送的端是否是当前服务器持有的会话,如果判断消息数据中的信息是在当前服务,则进行推送,否则抛弃。如下图代码所示。 通过源码分析,数据下行则通过NotifyProxy的方式发送数据,需要注意的是Netty是NIO,如果下行通知需要获取返回值,则要将异步转同步,所以NotifyFuture是实现java.util.concurrent.Future的方法,通过设置超时时间,在channelRead获取到上行数据之后,通过seq来关联NotifyFuture的方法。如下图代码所示。 下行的数据通过TcpConnector的send方法发送,send方式则是通过ChannelHandlerContext的writeAndFlush方法写入Channel,并实现数据下行,这里需要注意的是,之前有另一种写法就是cf.await,通过阻塞的方式来判断写入是否成功,这种写法偶发出现BlockingOperationException的异常。如下图代码所示。 使用阻塞获取返回值的写法: 关于BlockingOperationException的问题我在StackOverflow进行提问,非常幸运的得到了Norman Maurer(Netty的核心贡献者之一)的解答: 最终结论大致分析出,在执行write方法时,Netty会判断current thread是否就是分给该Channe的EventLoop,如果是则行线程执行IO操作,否则提交executor等待分配。当执行await方法时,会从executor里fetch出执行线程,这里就需要checkDeadLock,判断执行线程和current threads是否时同一个线程,如果是就检测为死锁抛出异常BlockingOperationException。 9、本文小结 本篇文章粗浅的向大家介绍了京麦TCP网关中使用的Netty实现长连接容器的架构,涉及TCP长连接容器搭建的关键点一一进行了阐述,以及对源码进行简单的分析。在京麦发展过程里Netty还有很多的实践应用,例如Netty4.11+HTTP2实现APNs的消息推送等等。 (本文同步发布于:http://www.52im.net/thread-1243-1-1.html) 附录:更多精编资料汇总 [1] 网络编程基础资料: 《TCP/IP详解-第11章·UDP:用户数据报协议》 《TCP/IP详解-第17章·TCP:传输控制协议》 《TCP/IP详解-第18章·TCP连接的建立与终止》 《TCP/IP详解-第21章·TCP的超时与重传》 《技术往事:改变世界的TCP/IP协议(珍贵多图、手机慎点)》 《通俗易懂-深入理解TCP协议(上):理论基础》 《通俗易懂-深入理解TCP协议(下):RTT、滑动窗口、拥塞处理》 《理论经典:TCP协议的3次握手与4次挥手过程详解》 《理论联系实际:Wireshark抓包分析TCP 3次握手、4次挥手过程》 《计算机网络通讯协议关系图(中文珍藏版)》 《UDP中一个包的大小最大能多大?》 《P2P技术详解(一):NAT详解——详细原理、P2P简介》 《P2P技术详解(二):P2P中的NAT穿越(打洞)方案详解》 《P2P技术详解(三):P2P技术之STUN、TURN、ICE详解》 《通俗易懂:快速理解P2P技术中的NAT穿透原理》 《高性能网络编程(一):单台服务器并发TCP连接数到底可以有多少》 《高性能网络编程(二):上一个10年,著名的C10K并发连接问题》 《高性能网络编程(三):下一个10年,是时候考虑C10M并发问题了》 《高性能网络编程(四):从C10K到C10M高性能网络应用的理论探索》 《不为人知的网络编程(一):浅析TCP协议中的疑难杂症(上篇)》 《不为人知的网络编程(二):浅析TCP协议中的疑难杂症(下篇)》 《不为人知的网络编程(三):关闭TCP连接时为什么会TIME_WAIT、CLOSE_WAIT》 《不为人知的网络编程(四):深入研究分析TCP的异常关闭》 《不为人知的网络编程(五):UDP的连接性和负载均衡》 《不为人知的网络编程(六):深入地理解UDP协议并用好它》 《网络编程懒人入门(一):快速理解网络通信协议(上篇)》 《网络编程懒人入门(二):快速理解网络通信协议(下篇)》 《网络编程懒人入门(三):快速理解TCP协议一篇就够》 《网络编程懒人入门(四):快速理解TCP和UDP的差异》 《Netty干货分享:京东京麦的生产级TCP网关技术实践总结》 >>更多同类文章 …… [2] NIO异步网络编程资料: 《Java新一代网络编程模型AIO原理及Linux系统AIO介绍》 《有关“为何选择Netty”的11个疑问及解答》 《开源NIO框架八卦——到底是先有MINA还是先有Netty?》 《选Netty还是Mina:深入研究与对比(一)》 《选Netty还是Mina:深入研究与对比(二)》 《NIO框架入门(一):服务端基于Netty4的UDP双向通信Demo演示》 《NIO框架入门(二):服务端基于MINA2的UDP双向通信Demo演示》 《NIO框架入门(三):iOS与MINA2、Netty4的跨平台UDP双向通信实战》 《NIO框架入门(四):Android与MINA2、Netty4的跨平台UDP双向通信实战》 《Netty 4.x学习(一):ByteBuf详解》 《Netty 4.x学习(二):Channel和Pipeline详解》 《Netty 4.x学习(三):线程模型详解》 《Apache Mina框架高级篇(一):IoFilter详解》 《Apache Mina框架高级篇(二):IoHandler详解》 《MINA2 线程原理总结(含简单测试实例)》 《Apache MINA2.0 开发指南(中文版)[附件下载]》 《MINA、Netty的源代码(在线阅读版)已整理发布》 《解决MINA数据传输中TCP的粘包、缺包问题(有源码)》 《解决Mina中多个同类型Filter实例共存的问题》 《实践总结:Netty3.x升级Netty4.x遇到的那些坑(线程篇)》 《实践总结:Netty3.x VS Netty4.x的线程模型》 《详解Netty的安全性:原理介绍、代码演示(上篇)》 《详解Netty的安全性:原理介绍、代码演示(下篇)》 《详解Netty的优雅退出机制和原理》 《NIO框架详解:Netty的高性能之道》 《Twitter:如何使用Netty 4来减少JVM的GC开销(译文)》 《绝对干货:基于Netty实现海量接入的推送服务技术要点》 《Netty干货分享:京东京麦的生产级TCP网关技术实践总结》 >>更多同类文章 …… [3] 有关IM/推送的通信格式、协议的选择: 《简述传输层协议TCP和UDP的区别》 《为什么QQ用的是UDP协议而不是TCP协议?》 《移动端即时通讯协议选择:UDP还是TCP?》 《如何选择即时通讯应用的数据传输格式》 《强列建议将Protobuf作为你的即时通讯应用数据传输格式》 《全方位评测:Protobuf性能到底有没有比JSON快5倍?》 《移动端IM开发需要面对的技术问题(含通信协议选择)》 《简述移动端IM开发的那些坑:架构设计、通信协议和客户端》 《理论联系实际:一套典型的IM通信协议设计详解》 《58到家实时消息系统的协议设计等技术实践分享》 《详解如何在NodeJS中使用Google的Protobuf》 >>更多同类文章 …… [4] 有关IM/推送的心跳保活处理: 《应用保活终极总结(一):Android6.0以下的双进程守护保活实践》 《应用保活终极总结(二):Android6.0及以上的保活实践(进程防杀篇)》 《应用保活终极总结(三):Android6.0及以上的保活实践(被杀复活篇)》 《Android进程保活详解:一篇文章解决你的所有疑问》 《Android端消息推送总结:实现原理、心跳保活、遇到的问题等》 《深入的聊聊Android消息推送这件小事》 《为何基于TCP协议的移动端IM仍然需要心跳保活机制?》 《微信团队原创分享:Android版微信后台保活实战分享(进程保活篇)》 《微信团队原创分享:Android版微信后台保活实战分享(网络保活篇)》 《移动端IM实践:实现Android版微信的智能心跳机制》 《移动端IM实践:WhatsApp、Line、微信的心跳策略分析》 >>更多同类文章 …… [5] 有关WEB端即时通讯开发: 《新手入门贴:史上最全Web端即时通讯技术原理详解》 《Web端即时通讯技术盘点:短轮询、Comet、Websocket、SSE》 《SSE技术详解:一种全新的HTML5服务器推送事件技术》 《Comet技术详解:基于HTTP长连接的Web端实时通信技术》 《新手快速入门:WebSocket简明教程》 《WebSocket详解(一):初步认识WebSocket技术》 《WebSocket详解(二):技术原理、代码演示和应用案例》 《WebSocket详解(三):深入WebSocket通信协议细节》 《socket.io实现消息推送的一点实践及思路》 《LinkedIn的Web端即时通讯实践:实现单机几十万条长连接》 《Web端即时通讯技术的发展与WebSocket、Socket.io的技术实践》 《Web端即时通讯安全:跨站点WebSocket劫持漏洞详解(含示例代码)》 《开源框架Pomelo实践:搭建Web端高性能分布式IM聊天服务器》 《使用WebSocket和SSE技术实现Web端消息推送》 《详解Web端通信方式的演进:从Ajax、JSONP 到 SSE、Websocket》 >>更多同类文章 …… [6] 有关IM架构设计: 《浅谈IM系统的架构设计》 《简述移动端IM开发的那些坑:架构设计、通信协议和客户端》 《一套海量在线用户的移动端IM架构设计实践分享(含详细图文)》 《一套原创分布式即时通讯(IM)系统理论架构方案》 《从零到卓越:京东客服即时通讯系统的技术架构演进历程》 《蘑菇街即时通讯/IM服务器开发之架构选择》 《腾讯QQ1.4亿在线用户的技术挑战和架构演进之路PPT》 《微信后台基于时间序的海量数据冷热分级架构设计实践》 《微信技术总监谈架构:微信之道——大道至简(演讲全文)》 《如何解读《微信技术总监谈架构:微信之道——大道至简》》 《快速裂变:见证微信强大后台架构从0到1的演进历程(一)》 《17年的实践:腾讯海量产品的技术方法论》 《移动端IM中大规模群消息的推送如何保证效率、实时性?》 《现代IM系统中聊天消息的同步和存储方案探讨》 >>更多同类文章 …… [7] 有关IM安全的文章: 《即时通讯安全篇(一):正确地理解和使用Android端加密算法》 《即时通讯安全篇(二):探讨组合加密算法在IM中的应用》 《即时通讯安全篇(三):常用加解密算法与通讯安全讲解》 《即时通讯安全篇(四):实例分析Android中密钥硬编码的风险》 《即时通讯安全篇(五):对称加密技术在Android平台上的应用实践》 《即时通讯安全篇(六):非对称加密技术的原理与应用实践》 《传输层安全协议SSL/TLS的Java平台实现简介和Demo演示》 《理论联系实际:一套典型的IM通信协议设计详解(含安全层设计)》 《微信新一代通信安全解决方案:基于TLS1.3的MMTLS详解》 《来自阿里OpenIM:打造安全可靠即时通讯服务的技术实践分享》 《简述实时音视频聊天中端到端加密(E2EE)的工作原理》 《移动端安全通信的利器——端到端加密(E2EE)技术详解》 《Web端即时通讯安全:跨站点WebSocket劫持漏洞详解(含示例代码)》 《通俗易懂:一篇掌握即时通讯的消息传输安全原理》 >>更多同类文章 …… [8] 有关实时音视频开发: 《专访微信视频技术负责人:微信实时视频聊天技术的演进》 《即时通讯音视频开发(一):视频编解码之理论概述》 《即时通讯音视频开发(二):视频编解码之数字视频介绍》 《即时通讯音视频开发(三):视频编解码之编码基础》 《即时通讯音视频开发(四):视频编解码之预测技术介绍》 《即时通讯音视频开发(五):认识主流视频编码技术H.264》 《即时通讯音视频开发(六):如何开始音频编解码技术的学习》 《即时通讯音视频开发(七):音频基础及编码原理入门》 《即时通讯音视频开发(八):常见的实时语音通讯编码标准》 《即时通讯音视频开发(九):实时语音通讯的回音及回音消除�概述》 《即时通讯音视频开发(十):实时语音通讯的回音消除�技术详解》 《即时通讯音视频开发(十一):实时语音通讯丢包补偿技术详解》 《即时通讯音视频开发(十二):多人实时音视频聊天架构探讨》 《即时通讯音视频开发(十三):实时视频编码H.264的特点与优势》 《即时通讯音视频开发(十四):实时音视频数据传输协议介绍》 《即时通讯音视频开发(十五):聊聊P2P与实时音视频的应用情况》 《即时通讯音视频开发(十六):移动端实时音视频开发的几个建议》 《即时通讯音视频开发(十七):视频编码H.264、VP8的前世今生》 《实时语音聊天中的音频处理与编码压缩技术简述》 《网易视频云技术分享:音频处理与压缩技术快速入门》 《学习RFC3550:RTP/RTCP实时传输协议基础知识》 《简述开源实时音视频技术WebRTC的优缺点》 《良心分享:WebRTC 零基础开发者教程(中文)》 《开源实时音视频技术WebRTC中RTP/RTCP数据传输协议的应用》 《基于RTMP数据传输协议的实时流媒体技术研究(论文全文)》 《声网架构师谈实时音视频云的实现难点(视频采访)》 《浅谈开发实时视频直播平台的技术要点》 《还在靠“喂喂喂”测试实时语音通话质量?本文教你科学的评测方法!》 《实现延迟低于500毫秒的1080P实时音视频直播的实践分享》 《移动端实时视频直播技术实践:如何做到实时秒开、流畅不卡》 《如何用最简单的方法测试你的实时音视频方案》 《技术揭秘:支持百万级粉丝互动的Facebook实时视频直播》 《简述实时音视频聊天中端到端加密(E2EE)的工作原理》 《移动端实时音视频直播技术详解(一):开篇》 《移动端实时音视频直播技术详解(二):采集》 《移动端实时音视频直播技术详解(三):处理》 《移动端实时音视频直播技术详解(四):编码和封装》 《移动端实时音视频直播技术详解(五):推流和传输》 《移动端实时音视频直播技术详解(六):延迟优化》 《理论联系实际:实现一个简单地基于HTML5的实时视频直播》 《IM实时音视频聊天时的回声消除技术详解》 《浅谈实时音视频直播中直接影响用户体验的几项关键技术指标》 《如何优化传输机制来实现实时音视频的超低延迟?》 《首次披露:快手是如何做到百万观众同场看直播仍能秒开且不卡顿的?》 《实时通信RTC技术栈之:视频编解码》 《开源实时音视频技术WebRTC在Windows下的简明编译教程》 《Android直播入门实践:动手搭建一套简单的直播系统》 >>更多同类文章 …… [9] IM开发综合文章: 《移动端IM中大规模群消息的推送如何保证效率、实时性?》 《移动端IM开发需要面对的技术问题》 《开发IM是自己设计协议用字节流好还是字符流好?》 《请问有人知道语音留言聊天的主流实现方式吗?》 《IM消息送达保证机制实现(一):保证在线实时消息的可靠投递》 《IM消息送达保证机制实现(二):保证离线消息的可靠投递》 《如何保证IM实时消息的“时序性”与“一致性”?》 《一个低成本确保IM消息时序的方法探讨》 《IM单聊和群聊中的在线状态同步应该用“推”还是“拉”?》 《IM群聊消息如此复杂,如何保证不丢不重?》 《谈谈移动端 IM 开发中登录请求的优化》 《移动端IM登录时拉取数据如何作到省流量?》 《浅谈移动端IM的多点登陆和消息漫游原理》 《完全自已开发的IM该如何设计“失败重试”机制?》 《通俗易懂:基于集群的移动端IM接入层负载均衡方案分享》 《微信对网络影响的技术试验及分析(论文全文)》 《即时通讯系统的原理、技术和应用(技术论文)》 《开源IM工程“蘑菇街TeamTalk”的现状:一场有始无终的开源秀》 《QQ音乐团队分享:Android中的图片压缩技术详解(上篇)》 《QQ音乐团队分享:Android中的图片压缩技术详解(下篇)》 《腾讯原创分享(一):如何大幅提升移动网络下手机QQ的图片传输速度和成功率》 《腾讯原创分享(二):如何大幅压缩移动网络下APP的流量消耗(上篇)》 《腾讯原创分享(二):如何大幅压缩移动网络下APP的流量消耗(下篇)》 《如约而至:微信自用的移动端IM网络层跨平台组件库Mars已正式开源》 《基于社交网络的Yelp是如何实现海量用户图片的无损压缩的?》 >>更多同类文章 …… [10] 开源移动端IM技术框架资料: 《开源移动端IM技术框架MobileIMSDK:快速入门》 《开源移动端IM技术框架MobileIMSDK:常见问题解答》 《开源移动端IM技术框架MobileIMSDK:压力测试报告》 >>更多同类文章 …… [11] 有关推送技术的文章: 《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公司)》 《腾讯信鸽技术分享:百亿级实时消息推送的实战经验》 《百万在线的美拍直播弹幕系统的实时推送技术实践之路》 >>更多同类文章 …… [12] 更多即时通讯技术好文分类: http://www.52im.net/forum.php?mod=collection&op=all (本文同步发布于:http://www.52im.net/thread-1243-1-1.html)
文章
编解码  ·  网络协议  ·  安全  ·  Android开发  ·  容器
2017-12-01
DingTalk「开发者说」— 钉钉数据授权开发实战
分享人:骏隆,钉钉开放平台能力中心前端负责人一键回看视频地址:一键回看目录:一、数据流转为什么需要授权,哪些数据需要授权二、钉钉如何做数据授权三、无线端统一授权套件详解四、各类型应用获取手机号流程详解五、三方应用获取用户手机号实践 – 完成一个小作业正文:一、数据流转为什么需要授权,哪些数据需要授权?为什么获取个人信息需要授权?2020年10月1日,我国实施的《信息安全技术个人信息安全规范》中,对个人信息保护做出了更明确的规定;2021年6月10日,《中华人民共和国数据安全法》正式公布,在法律层面对个人信息保护提出规范,任意采集和获取个人信息都属于违法行为。哪些数据需要授权?需要授权的数据主要包括:个人信息和个人敏感信息,在法律条文中都给出了明确规定。个人信息个人信息是指:以电子或者其它方式记录的、能够单独或者与其它信息结合识别特定自然人身份或者反映特定自然人活动情况的各种信息;个人信息包括:姓名、出生日期、身份证件号码、个人生物识别信息、住址、通信通讯联系方式、通信记录和内容、账号密码、财产信息、征信信息、行踪轨迹、住宿信息、健康生理信息、交易信息等。个人敏感信息个人敏感信息是指:一旦泄露、非法提供或滥用,可能危害人身和财产安全,极易导致个人名誉、身心健康受到损害或歧视性待遇等的个人信息;个人敏感信息包括:身份证件号码、个人生物识别信息、银行账号、通信记录和内容、财产信息、征信信息、行踪轨迹、住宿信息、健康生理信息、交易信息、以下(含)儿童的个人信息等。在钉钉场景下,需要授权的数据包括两类:自己的数据给别人范围:个人隐私类数据:个人手机号、身份证号,个人邮箱、钉钉头像、URL等;平台上产生的属于自己的数据:个人支付信息、好友关系等;场景:获取用户手机号用于同步登录;获取蚂蚁森林好友关系(蚂蚁森林);读取工作台小程序用户信息(工作台);读取个人支付宝信息(企业金融);别人的数据给自己:属于平台赋予个人的权限场景:个人发票夹添加发票(企业金融);个人名片夹添加名片(名片);允许发送个人服务窗消息(服务窗);二、钉钉如何做数据授权钉钉数据授权主体钉钉数据授权包括三个主体:信息主体(用户)、信息控制者(钉钉)和信息使用者(钉钉应用)。信息使用者向信息控制者发出数据请求,信息控制者向信息主体发出获取个人信息请求,得到允许后将个人信息数据传输给信息使用者。钉钉数据授权流程钉钉数据授权流程主要有四个步骤(见下图):① 被授权方请求授权方给同意凭证;② 被授权方拿凭证查询数据接口;③ 接口做调用鉴权;④ 返回授权方数据给被授权方;钉钉数据授权方式(旧版)在2021年以前,钉钉采用OAuth2登录授权方式获取个人信息。一个三方应用对接OAuth2登录授权方式获取用户信息的流程如下图:a. 钉钉用户要登录三方应用;b. 三方应用请求钉钉开放平台授权登录;c. 钉钉开放平台确认用户登录;d. 用户登录确认授权给钉钉开放平台;e. 钉钉开放平台拉起三方应用或重定向导三方带上auth_code;f. 三方应用根据auth_code换取访问Token;g. 三方应用server调用接口得到访问Token;h. 钉钉开放平台返回Token给三方应用server;i. 三方应用server调用业务接口;j. 钉钉开放平台返回业务数据给三方应用server。OAuth2方案支持企业内部应用和三方企业应用,但不支持三方个人应用。痛点:a. OAuth2方案无法支持小程序,因为带auth_code回传小程序的链路无法实现;b. 无法做权限细分;c. 移动端的体验非常差;d. 授权内容(接口、字段)文案无法定制;三、移动端统一授权套件基于OAuth2方案在移动端存在的问题,钉钉开发了移动端统一授权套件。方案设计难点Native组件更新依赖发版,升级困难;需要支持H5&小程序应用对接;解法:授权套件(授权小程序 + SDK)多端一致,抹平iOS、Android功能差异和版本差异;多应用类型支持(小程序、H5);半屏呈现,类组件体验;接入简单,文档完备;授权套件方案流程,如下:通信原理 为什么用小程序?查询授权状态,需要一个安全环境;小程序支持半屏打开,且支持半屏高度定制,体感上像原生组件;通信原理:小程序既支持与小程序之间通信,又支持与H5应用之间的通信(见下图);小程序对接授权套件在App.js中运用app.onShow方法添加onAuthAppBack调用,用于接收授权小程序返回的数据;在page.js中操作:通过sdk openAuthMiniApp方法唤起授权套件;在page.onShow里调用disposeAuthData处理App.js中app.onShow接收到的返回数据,具体方法是App.js的onShow里通过eventemitter2注册事件并emit接收到授权数据,然后在page.js的onShow里监听事件,处理授权结果;H5微应用对接授权套件H5与授权小程序通信原理:通过resume事件回调监听来实现应用之间的通信;H5是通过钉钉jsapi方法打开的小程序,所以需要在jsapi 的ready回调里执行。四、钉钉个人信息授权实现方式汇总获取授权移动端(小程序、H5)方案:统一授权套件PC端(浏览器、电脑钉钉)方案:OAuth2.0授权Demo源码:https://github.com/opendingtalk/h5app-auth-user-demo取消授权个人解除授权方法:钉钉app我的–设置–安全中心–隐私开关–应用授权管理应用对接取消授权功能五、三方应用获取个人信息授权实战实践一:三方企业应用(H5)获取用户手机号详细步骤:创建第三方应用H5 a. 登录开发者后台,在应用开发页面,选择第三方企业应用,然后单击创建应用。 b. 选择应用类型,然后填写应用的基本信息,再单击确定创建。 c. 应用创建完成。代码准备配置应用权限 在权限管理中勾选“个人手机号信息”和“通讯录个人信息读权限”,然后点击批量申请。代码端配置 a. 将应用信息中的key和secret代码配置到代码端。 b. Java的逻辑解析。 提供获取用户信息的接口,以及authCode; 在getUserInfo中,用authCode换取AccessToken,使用AccessToken请求开放接口,从而获取用户信息;应用授权对接 在app.js中引入钉钉统一授权套件SDK;import { onAuthAppBack } from 'dingtalk-design-libs/biz/openAuthMiniApp';直接在触发事件里调用openAuthMiniApp即可;运行构建npm run build启动移动端实践二、企业自建应用(小程序)获取用户信息详细步骤:创建小程序a. 登录开发者后台,在应用开发页面,选择企业内部开发,然后单击创建应用。b. 选择应用类型,然后填写应用的基本信息,再单击确定创建。c. 小程序创建完成;配置应用权限在权限管理中勾选“个人手机号信息”和“通讯录个人信息读权限”,然后点击批量申请。在代码端初始化小程序,运用钉钉小程序开发工具打开小程序应用授权对接a. 在小程序app.js文件中引入钉钉统一授权套件SDK;import { onAuthAppBack } from ‘dingtalk-design-libs/biz/openAuthMiniApp’;b. 在app.onShow方法添加onAuthAppBack调用;c. 在小程序需要授权的页面,使用授权SDK。例如page/index/index.js,通过openAuthMiniApp唤起授权套件;import { openAuthMiniApp, disposeAuthData} from 'dingtalk-design-libs/biz/openAuthMiniApp';注:clientId需要根据小程序应用信息中的AppKey重新配置d. 使用page.onShow方法调用disposeAuthData处理授权后的结果;在模拟器中操作六、Q&AQ: 目前,移动端统一授权套件一定要引入才可以使用,在未来是否会集成到钉钉小程序开发环境中?A:不会。移动端统一授权套件之所以选择授权小程序+SDK的方案,其中一个原因就是可以做到灵活定制,达到更好的效果。如果集成到钉钉小程序中则不容易实现。Q:Demo是否有Python或C语言版本?A:目前Demo中只提供Java版本,后续会逐步完善。Q:在实际操作中,在同一个小程序的两个页面都调用了授权套件,在第一个页面的授权弹窗选择拒绝后,在另一个页面的授权弹窗选择同意,结果却是第一个页面同意,是什么原因?A:在小程序对接中,App.js的onShow里通过eventemitter2注册事件并emit接收到授权数据,然后在page.js的onShow里监听事件,处理授权结果。事件的key是同一个,在两个页面中同时监听事件时,当第一个页面弹窗取消而第二个页面又唤起弹窗时,返回数据被第一个配置监听事件捕获,就导致执行第一个页面的后续数据处理流程。为解决这一问题,钉钉进行了SDK的优化,后续会将方法以文档方式提供给大家。Q:PC端的个人授权每次都要请求服务端,后期是否有针对这个的改善方案?A:钉钉正在规划将小程序的能力布置到PC钉钉桌面端,在钉钉桌面端的应用授权则可以得到改善。Q:如何导出通讯录数据?A:可以在开发者文档中搜索“通讯录”找到相关文档介绍。
文章
移动开发  ·  小程序  ·  安全  ·  Java  ·  开发工具  ·  数据安全/隐私保护  ·  C语言  ·  Android开发  ·  开发者  ·  Python
2022-04-18
iOS10 SiriKit QQ 适配详解
1. 概述 苹果在iOS10开放了siriKit接口给第三方应用。目前,QQ已经率先适配了Siri的发消息和打电话功能。这意味着在iOS10中你可以直接告诉Siri让它帮你发QQ消息和打QQ电话了,听起来是不是很酷炫? 那么第三方应用使用Siri的体验究竟如何?哪些应用可以接入SiriKit?接入SiriKit又需要做哪些工作呢?这篇文章会为你一一解答这些疑惑。  图1 用Siri发QQ消息效果展示 2. SiriKit简介 我们都知道Siri是iphone手机中的智能语音助手,那么什么是SiriKit呢?SiriKit是苹果为第三方应用支持Siri提供的开发框架。在官方文档中,SiriKit将对不同场景的语音支持划分为不同的domain,目前,SiriKit支持的domain包括:VoIP电话、发消息、转账、图片搜索、网约车订车、CarPlay和餐厅预定,也就是说如果你的应用中包含有这些功能之一,就可以考虑将这些功能接入到SiriKit中啦。 实现SiriKit相关功能时,我们并不需要真正对语音进行识别,语音的识别工作会由Siri完成。Siri识别完语音后,会将语音要完成的功能抽象成Intent对象传递给我们,而我们的接入工作主要是与这些Intent对象打交道,并不会涉及到自然语言处理(NLP)的技术。 关于SiriKit的开发网上已有一些文章,也可参考苹果的官方文档SiriKit Programming Guide,本文着重介绍QQ的适配经验。  图2 SiriKit原理 3. SiriKit接入 要实现SiriKit的功能需要在Xcode工程中添加Intents Extension的target,和其他extension一样, Intents Extension是一个独立于Containing App进程运行的插件,主要用于处理和确认来自siri的intent请求。如果想让Siri在处理App相关intent时提供一些自定义的界面,那么你就需要再添加Intents UI Extension的target,Intents UI Extension也是一个独立运行的插件(所以要完整的支持SiriKit其实是需要添加两个target,有点蛋疼)。关于App Extension的开发可以参考苹果的App Extension Programming Guide。 我们以QQ中的发消息功能为例说明一下SiriKit的接入方法: 首先,我们需要在Intents Extentsion的info.plist文件中配置我们需要支持的siri Intents,在IntentsSupported中加入INSendMessageIntent,如果需要在锁屏时禁用某个功能,则再在IntentsRestrictedWhileLocked中加入相应项的Intent,如图3所示。  图3 Intent Extentsion info.plist配置 SiriKit的接入主要分为Intents Extension和Intents UI Extension两部分,下面分别进行介绍。 Intents Extension 当我们对siri说“用QQ发消息给王一然说你好”时,语音的识别将会由Siri自动完成,Siri会将识别好的内容展示在Siri的界面。如图4所示,我们可以看到一个完整的发消息语句主要由四部分组成: 应用名:告诉Siri要使用哪个App,siri会根据app的bundle displayname自动识别app的名称,无需额外注册。 发消息Intent:告诉Siri要使用发消息的功能,我们实测发现说发信息也是能识别,具体还有哪些词汇会识别为发消息的intent苹果没有在文档中说明。 消息接收者:告诉siri消息的接收者是谁,“王一然”是我QQ好友的昵称。 消息内容:告诉Siri你要发的消息内容是什么,这里的消息内容为“我很生气”。  图4 确认发送消息界面 其中应用名和Intent是必须的,不然Siri无法抽象出你的“Intent”。后两项如果缺省的话,我们可以在实现中要求用户进一步提供数据或者忽略。在识别完成后Siri会将消息内容和接收者抽象成一个INSendMessageIntent传递给 QQ的Intent Extension。 我们从图4还可以看到Siri准确从我的语音中识别出我QQ好友中昵称为“王一然”的好友,然而“王一然”并不是一个通用的短语,那么这是怎么做到的呢?奥秘就在于在QQ运行时我们把所有QQ好友的昵称同步到了Siri云端,这样Siri就可以识别出特定用户要使用的特定短语,详细同步方法可参考INVocabulary的setVocabularyStrings:ofType:方法。 每个domain的功能在Siri中都有对应的Intents,而每个intents都对应一个特定的handler协议。对于发消息来讲,对应的Intent和handler协议分别为INSendMessageIntent和INSendMessageIntentHandling。只要实现INSendMessageIntentHandling协议中的相关方法,并在Siri解析出INSendMessageIntent请求时用我们的INSendMessageIntentHandling对象去处理相关的发消息请求。具体的流程如图5:  图5 Siri发QQ消息流程 1)ResolveRecipientsForSendMessage 对siri从Intent中传递过来的接收者名称进行处理和确认,比如可以确认该名称当前是否在QQ好友列表中,并将resolution result反馈给Siri。Resolution result代表了应用对intent处理后的结果,对于发消息来说,表1列举了几种可能的resolution results。 表1 send resolution result 2)ResolveContent 与接收者的处理类似,在这个方法中可以对Siri识别出的消息内容进行“修饰”,并且将resolution result反馈给Siri,比如QQ对一些消息里面的特殊词汇如“生气”做了emoji适配。 3)ConfirmSendMessage 这个方法的作用是确认是否要发送该消息,可以在这一步进行一些鉴权工作,鉴权通过后再确认发送,否则取消。确认可以发送后会调起确认发送界面,如图4所示。如果需要从Containing App共享数据,具体的实现方案参考App Group的Shared Container。 4)HandleSendMessage 如图4,当用户点击了“发送”按钮或者用语音给出了发送指令时会最终进入到这个方法,在这个方法里我们需要实现发消息的逻辑,发送成功后可以调起消息发送成功的界面,如图6。  图6 消息发送成功界面 Intents UI Extension 对于支持自定义界面的Intent类型,可以在Intents UI Extension中提供更美观的自定义界面。 Custom UI的实现相对较简单,和ios app的开发一样,都是通过UIViewController的子类实现。我们需要在Intents UI Extension的info.plist文件中设置initial viewcontroller或者设置main storyboard,对于不同类型的Intent的界面展示通过Child Viewcontrollers的方式实现差异化界面展示。 如图7所示,当接收到来自Intents Extension的response时,系统会唤起Intents UI Extension并加载initial viewcontroller,通过INUIHostedViewSiriProviding协议的configureWithInteraction:context:completion:方法可以获取intent,比如在发消息功能中,在消息确认发送和发送成功后都会回调一次这个方法。根据Intent对象的类型和状态,在收到相关Intent的回调时present对应的Child Viewcontroller即可实现定制化的界面展示。 这里需要注意的是,Intents UI Extension的进程并不会在界面销毁后就退出,很可能只是在后台处于休眠状态,下次response到来时再被唤醒。  图7 Life cycle of an Intents UI extension 4. 总结 总的来说虽然苹果这一次对SiriKit开放的场景有限,但是从我们的适配经历来看苹果对Siri还是非常重视的。另外,这是SiriKit首次对第三方应用开放接口,所以不可避免存在一些问题。我们在开发过程中也确实遇到了一些SiriKit本身的Bug,大部分bug在向苹果反馈后都得到了解决,但是在语言识别方面Siri依然存在一些缺陷,比如对中英文混合的场景识别依旧不太好。期待以后Siri对中文的支持越来越好,也希望Siri能够开放更多的场景给第三方应用适配。 作者:腾讯Bugly 来源:51CTO
文章
iOS开发
2017-08-08
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
文章
缓存  ·  JavaScript  ·  前端开发  ·  安全  ·  Android开发  ·  数据安全/隐私保护  ·  Java  ·  网络协议  ·  网络安全  ·  API
2019-11-04
钉钉开发者社区
49772 人关注 | 409 讨论 | 92 内容
+ 订阅
  • 一个容易被开发者忽视的强力护盾——软件著作权申请
  • 「开发者说」实验室上钉钉--南大研究生的数字化校园故事
  • 「开发者说」悦积分上钉钉,关于“游戏化管理”的应用开发故事
查看更多 >
开发与运维
5257 人关注 | 126180 讨论 | 204817 内容
+ 订阅
  • ESC使用心得——来自一名大一学生的暑期使用体验
  • ECS初次体验
  • 初识阿里云
查看更多 >
安全
1064 人关注 | 23300 讨论 | 56901 内容
+ 订阅
  • ESC使用心得——来自一名大一学生的暑期使用体验
  • ECS初次体验
  • 高校学生在家实践感想
查看更多 >
云原生
230407 人关注 | 9836 讨论 | 30185 内容
+ 订阅
  • 初识阿里云
  • 易秋博客
  • ECS使用体验
查看更多 >
人工智能
2625 人关注 | 9304 讨论 | 69178 内容
+ 订阅
  • 热饭的测开成果盘点第十七期:web自动化智能平台
  • 玩法平台-文字识别OCR-任务组的测评
  • 阿里云易立:云原生如何破解企业降本提效难题?
查看更多 >