0
评论

分享:大型网站图片服务器架构的演进 高并发

oscar 发表了文章 • 2201 次浏览 • 2015-07-03 17:53 • 来自相关话题

在主流的Web站点中,图片往往是不可或缺的页面元素,尤其在大型网站中,几乎都将面临“海量图片资源”的存储、访问等相关技术问题。在针对图片服务器的架构扩展中,也会历经很多曲折甚至是血泪教训(尤其是早期规划不足,造成后期架构上很难兼容和扩展)。

本文将以一个真实垂直门户网站的发展历程,向大家娓娓道来。

构建在Windows平台之上的网站,往往会被业内众多技术认为很“保守”,甚至会有点。很大部分原因,是由于微软技术体系的封闭和部分技术人员的短视造成的(当然,主要还是人的问题)。由于长期缺乏开源支持,所以很多人只能“闭门造车”,这样很容易形成思维局限性和短板。以图片服务器为例子,如果前期没有容量规划和可扩展的设计,那么随着图片文件的不断增多和访问量的上升,由于在性能、容错/容灾、扩展性等方面的设计不足,后续将会给开发、运维工作带来很多问题,严重时甚至会影响到网站业务正常运作和互联网公司的发展(这绝不是在危言耸听)。

很多公司之所以选择Windows(.NET)平台来构建网站和图片服务器,很大部分由创始团队的技术背景决定的,早期的技术人员可能更熟悉.NET,或者团队的负责人认为Windows/.NET的易用性、“短平快”的开发模式、人才成本等方面都比较符合创业初期的团队,自然就选择了Windows。后期业务发展到一定规模,也很难轻易将整体架构迁移到其它开源平台上了。当然,对于构建大规模互联网,更建议首选开源架构,因为有很多成熟的案例和开源生态的支持(也会有很多坑,就看是你自己最先去踩坑,还是在别人踩了修复之后你再用),避免重复造轮子和支出高额授权费用。对于迁移难度较大的应用,个人比较推荐Linux、Mono、Jexus、Mysql、Memcahed、Redis……混搭的架构,同样能支撑具有高并发访问和大数据量等特点的互联网应用。
 
单机时代的图片服务器架构(集中式)
 
初创时期由于时间紧迫,开发人员水平也很有限等原因。所以通常就直接在website文件所在的目录下,建立1个upload子目录,用于保存用户上传的图片文件。如果按业务再细分,可以在upload目录下再建立不同的子目录来区分。例如:upload\QA,upload\Face等。

在数据库表中保存的也是”upload/qa/test.jpg”这类相对路径。

用户的访问方式如下:

http://www.yourdomain.com/upload/qa/test.jpg

程序上传和写入方式:

程序员A通过在web.config中配置物理目录D:\Web\yourdomain\upload  然后通过stream的方式写入文件;

程序员B通过Server.MapPath等方式,根据相对路径获取物理目录  然后也通过stream的方式写入文件。

优点:实现起来最简单,无需任何复杂技术,就能成功将用户上传的文件写入指定目录。保存数据库记录和访问起来倒是也很方便。

缺点:上传方式混乱,严重不利于网站的扩展。

针对上述最原始的架构,主要面临着如下问题:
随着upload目录中文件越来越多,所在分区(例如D盘)如果出现容量不足,则很难扩容。只能停机后更换更大容量的存储设备,再将旧数据导入。在部署新版本(部署新版本前通过需要备份)和日常备份website文件的时候,需要同时操作upload目录中的文件,如果考虑到访问量上升,后边部署由多台Web服务器组成的负载均衡集群,集群节点之间如果做好文件实时同步将是个难题。
 集群时代的图片服务器架构(实时同步)
 
在website站点下面,新建一个名为upload的虚拟目录,由于虚拟目录的灵活性,能在一定程度上取代物理目录,并兼容原有的图片上传和访问方式。用户的访问方式依然是:

http://www.yourdomain.com/upload/qa/test.jpg
优点:配置更加灵活,也能兼容老版本的上传和访问方式。

因为虚拟目录,可以指向本地任意盘符下的任意目录。这样一来,还可以通过接入外置存储,来进行单机的容量扩展。
缺点:部署成由多台Web服务器组成的集群,各个Web服务器(集群节点)之间(虚拟目录下的)需要实时的去同步文件,由于同步效率和实时性的限制,很难保证某一时刻各节点上文件是完全一致的。

基本架构如下图所示:





在早期的很多基于Linux开源架构的网站中,如果不想同步图片,可能会利用NFS来实现。事实证明,NFS在高并发读写和海量存储方面,效率上存在一定问题,并非最佳的选择,所以大部分互联网公司都不会使用NFS来实现此类应用。当然,也可以通过Windows自带的DFS来实现,缺点是“配置复杂,效率未知,而且缺乏资料大量的实际案例”。另外,也有一些公司采用FTP或Samba来实现。

上面提到的几种架构,在上传/下载操作时,都经过了Web服务器(虽然共享存储的这种架构,也可以配置独立域名和站点来提供图片访问,但上传写入仍然得经过Web服务器上的应用程序来处理),这对Web服务器来讲无疑是造成巨大的压力。所以,更建议使用独立的图片服务器和独立的域名,来提供用户图片的上传和访问。
 
独立图片服务器/独立域名的好处
 
图片访问是很消耗服务器资源的(因为会涉及到操作系统的上下文切换和磁盘I/O操作)。分离出来后,Web/App服务器可以更专注发挥动态处理的能力。独立存储,更方便做扩容、容灾和数据迁移。浏览器(相同域名下的)并发策略限制,性能损失。访问图片时,请求信息中总带cookie信息,也会造成性能损失。方便做图片访问请求的负载均衡,方便应用各种缓存策略(HTTP Header、Proxy Cache等),也更加方便迁移到CDN。
......
 
我们可以使用Lighttpd或者Nginx等轻量级的web服务器来架构独立图片服务器。
 
当前的图片服务器架构(分布式文件系统+CDN)
 
在构建当前的图片服务器架构之前,可以先彻底撇开web服务器,直接配置单独的图片服务器/域名。但面临如下的问题:
旧图片数据怎么办?能否继续兼容旧图片路径访问规则?独立的图片服务器上需要提供单独的上传写入的接口(服务API对外发布),安全问题如何保证?同理,假如有多台独立图片服务器,是使用可扩展的共享存储方案,还是采用实时同步机制?
 
直到应用级别的(非系统级) DFS(例如FastDFS HDFS MogileFs MooseFS、TFS)的流行,简化了这个问题:执行冗余备份、支持自动同步、支持线性扩展、支持主流语言的客户端api上传/下载/删除等操作,部分支持文件索引,部分支持提供Web的方式来访问。

考虑到各DFS的特点,客户端API语言支持情况(需要支持C#),文档和案例,以及社区的支持度,我们最终选择了FastDFS来部署。

唯一的问题是:可能会不兼容旧版本的访问规则。如果将旧图片一次性导入FastDFS,但由于旧图片访问路径分布存储在不同业务数据库的各个表中,整体更新起来也十分困难,所以必须得兼容旧版本的访问规则。架构升级往往比做全新架构更有难度,就是因为还要兼容之前版本的问题。(给飞机在空中换引擎可比造架飞机难得多)
 
解决方案如下:
 
首先,关闭旧版本上传入口(避免继续使用导致数据不一致)。将旧图片数据通过rsync工具一次性迁移到独立的图片服务器上(即下图中描述的Old Image Server)。在最前端(七层代理,如Haproxy、Nginx)用ACL(访问规则控制),将旧图片对应URL规则的请求(正则)匹配到,然后将请求直接转发指定的web 服务器列表,在该列表中的服务器上配置好提供图片(以Web方式)访问的站点,并加入缓存策略。这样实现旧图片服务器的分离和缓存,兼容了旧图片的访问规则并提升旧图片访问效率,也避免了实时同步所带来的问题。
 
整体架构如图:





基于FastDFS的独立图片服务器集群架构,虽然已经非常的成熟,但是由于国内“南北互联”和IDC带宽成本等问题(图片是非常消耗流量的),我们最终还是选择了商用的CDN技术,实现起来也非常容易,原理其实也很简单,我这里只做个简单的介绍:

将img域名cname到CDN厂商指定的域名上,用户请求访问图片时,则由CDN厂商提供智能DNS解析,将最近的(当然也可能有其它更复杂的策略,例如负载情况、健康状态等)服务节点地址返回给用户,用户请求到达指定的服务器节点上,该节点上提供了类似Squid/Vanish的代理缓存服务,如果是第一次请求该路径,则会从源站获取图片资源返回客户端浏览器,如果缓存中存在,则直接从缓存中获取并返回给客户端浏览器,完成请求/响应过程。

由于采用了商用CDN服务,所以我们并没有考虑用Squid/Vanish来自行构建前置代理缓存。

上面的整个集群架构,可以很方便的做横向扩展,能满足一般垂直领域中大型网站的图片服务需求(当然,像taobao这样超大规模的可能另当别论)。经测试,提供图片访问的单台Nginx服务器(至强E5四核CPU、16G内存、SSD),对小静态页面(压缩后大概只有10kb左右的)可以扛住几千个并发且毫无压力。当然,由于图片本身体积比纯文本的静态页面大很多,提供图片访问的服务器的抗并发能力,往往会受限于磁盘的I/O处理能力和IDC提供的带宽。Nginx的抗并发能力还是非常强的,而且对资源占用很低,尤其是处理静态资源,似乎都不需要有过多担心了。可以根据实际访问量的需求,通过调整Nginx的参数,对Linux内核做调优,加入分级缓存策略等手段能够做更大程度的优化,也可以通过增加服务器或者升级服务器配置来做扩展,最直接的是通过购买更高级的存储设备和更大的带宽,以满足更大访问量的需求。

值得一提的是,在“云计算”流行的当下,也推荐高速发展期间的网站,使用“云存储”这样的方案,既能帮你解决各类存储、扩展、备灾的问题,又能做好CDN加速。最重要的是,价格也不贵。

总结,有关图片服务器架构扩展,大致围绕这些问题展开:
 
容量规划和扩展问题。数据的同步、冗余和容灾。硬件设备的成本和可靠性(是普通机械硬盘,还是SSD,或者更高端的存储设备和方案)。文件系统的选择。根据文件特性(例如文件大小、读写比例等)选择是用ext3/4或者NFS/GFS/TFS这些开源的(分布式)文件系统。图片的加速访问。采用商用CDN或者自建的代理缓存、web静态缓存架构。旧图片路径和访问规则的兼容性,应用程序层面的可扩展,上传和访问的性能和安全性等。 查看全部
在主流的Web站点中,图片往往是不可或缺的页面元素,尤其在大型网站中,几乎都将面临“海量图片资源”的存储、访问等相关技术问题。在针对图片服务器的架构扩展中,也会历经很多曲折甚至是血泪教训(尤其是早期规划不足,造成后期架构上很难兼容和扩展)。

本文将以一个真实垂直门户网站的发展历程,向大家娓娓道来。

构建在Windows平台之上的网站,往往会被业内众多技术认为很“保守”,甚至会有点。很大部分原因,是由于微软技术体系的封闭和部分技术人员的短视造成的(当然,主要还是人的问题)。由于长期缺乏开源支持,所以很多人只能“闭门造车”,这样很容易形成思维局限性和短板。以图片服务器为例子,如果前期没有容量规划和可扩展的设计,那么随着图片文件的不断增多和访问量的上升,由于在性能、容错/容灾、扩展性等方面的设计不足,后续将会给开发、运维工作带来很多问题,严重时甚至会影响到网站业务正常运作和互联网公司的发展(这绝不是在危言耸听)。

很多公司之所以选择Windows(.NET)平台来构建网站和图片服务器,很大部分由创始团队的技术背景决定的,早期的技术人员可能更熟悉.NET,或者团队的负责人认为Windows/.NET的易用性、“短平快”的开发模式、人才成本等方面都比较符合创业初期的团队,自然就选择了Windows。后期业务发展到一定规模,也很难轻易将整体架构迁移到其它开源平台上了。当然,对于构建大规模互联网,更建议首选开源架构,因为有很多成熟的案例和开源生态的支持(也会有很多坑,就看是你自己最先去踩坑,还是在别人踩了修复之后你再用),避免重复造轮子和支出高额授权费用。对于迁移难度较大的应用,个人比较推荐Linux、Mono、Jexus、Mysql、Memcahed、Redis……混搭的架构,同样能支撑具有高并发访问和大数据量等特点的互联网应用。
 
单机时代的图片服务器架构(集中式)
 
初创时期由于时间紧迫,开发人员水平也很有限等原因。所以通常就直接在website文件所在的目录下,建立1个upload子目录,用于保存用户上传的图片文件。如果按业务再细分,可以在upload目录下再建立不同的子目录来区分。例如:upload\QA,upload\Face等。

在数据库表中保存的也是”upload/qa/test.jpg”这类相对路径。

用户的访问方式如下:

http://www.yourdomain.com/upload/qa/test.jpg

程序上传和写入方式:

程序员A通过在web.config中配置物理目录D:\Web\yourdomain\upload  然后通过stream的方式写入文件;

程序员B通过Server.MapPath等方式,根据相对路径获取物理目录  然后也通过stream的方式写入文件。

优点:实现起来最简单,无需任何复杂技术,就能成功将用户上传的文件写入指定目录。保存数据库记录和访问起来倒是也很方便。

缺点:上传方式混乱,严重不利于网站的扩展。

针对上述最原始的架构,主要面临着如下问题:
  • 随着upload目录中文件越来越多,所在分区(例如D盘)如果出现容量不足,则很难扩容。只能停机后更换更大容量的存储设备,再将旧数据导入。
  • 在部署新版本(部署新版本前通过需要备份)和日常备份website文件的时候,需要同时操作upload目录中的文件,如果考虑到访问量上升,后边部署由多台Web服务器组成的负载均衡集群,集群节点之间如果做好文件实时同步将是个难题。

 集群时代的图片服务器架构(实时同步)
 
在website站点下面,新建一个名为upload的虚拟目录,由于虚拟目录的灵活性,能在一定程度上取代物理目录,并兼容原有的图片上传和访问方式。用户的访问方式依然是:

http://www.yourdomain.com/upload/qa/test.jpg
  • 优点:配置更加灵活,也能兼容老版本的上传和访问方式。


因为虚拟目录,可以指向本地任意盘符下的任意目录。这样一来,还可以通过接入外置存储,来进行单机的容量扩展。
  • 缺点:部署成由多台Web服务器组成的集群,各个Web服务器(集群节点)之间(虚拟目录下的)需要实时的去同步文件,由于同步效率和实时性的限制,很难保证某一时刻各节点上文件是完全一致的。


基本架构如下图所示:

1.jpg

在早期的很多基于Linux开源架构的网站中,如果不想同步图片,可能会利用NFS来实现。事实证明,NFS在高并发读写和海量存储方面,效率上存在一定问题,并非最佳的选择,所以大部分互联网公司都不会使用NFS来实现此类应用。当然,也可以通过Windows自带的DFS来实现,缺点是“配置复杂,效率未知,而且缺乏资料大量的实际案例”。另外,也有一些公司采用FTP或Samba来实现。

上面提到的几种架构,在上传/下载操作时,都经过了Web服务器(虽然共享存储的这种架构,也可以配置独立域名和站点来提供图片访问,但上传写入仍然得经过Web服务器上的应用程序来处理),这对Web服务器来讲无疑是造成巨大的压力。所以,更建议使用独立的图片服务器和独立的域名,来提供用户图片的上传和访问。
 
独立图片服务器/独立域名的好处
 
  • 图片访问是很消耗服务器资源的(因为会涉及到操作系统的上下文切换和磁盘I/O操作)。分离出来后,Web/App服务器可以更专注发挥动态处理的能力。
  • 独立存储,更方便做扩容、容灾和数据迁移。
  • 浏览器(相同域名下的)并发策略限制,性能损失。
  • 访问图片时,请求信息中总带cookie信息,也会造成性能损失。
  • 方便做图片访问请求的负载均衡,方便应用各种缓存策略(HTTP Header、Proxy Cache等),也更加方便迁移到CDN。

......
 
我们可以使用Lighttpd或者Nginx等轻量级的web服务器来架构独立图片服务器。
 
当前的图片服务器架构(分布式文件系统+CDN)
 
在构建当前的图片服务器架构之前,可以先彻底撇开web服务器,直接配置单独的图片服务器/域名。但面临如下的问题:
  • 旧图片数据怎么办?能否继续兼容旧图片路径访问规则?
  • 独立的图片服务器上需要提供单独的上传写入的接口(服务API对外发布),安全问题如何保证?
  • 同理,假如有多台独立图片服务器,是使用可扩展的共享存储方案,还是采用实时同步机制?

 
直到应用级别的(非系统级) DFS(例如FastDFS HDFS MogileFs MooseFS、TFS)的流行,简化了这个问题:执行冗余备份、支持自动同步、支持线性扩展、支持主流语言的客户端api上传/下载/删除等操作,部分支持文件索引,部分支持提供Web的方式来访问。

考虑到各DFS的特点,客户端API语言支持情况(需要支持C#),文档和案例,以及社区的支持度,我们最终选择了FastDFS来部署。

唯一的问题是:可能会不兼容旧版本的访问规则。如果将旧图片一次性导入FastDFS,但由于旧图片访问路径分布存储在不同业务数据库的各个表中,整体更新起来也十分困难,所以必须得兼容旧版本的访问规则。架构升级往往比做全新架构更有难度,就是因为还要兼容之前版本的问题。(给飞机在空中换引擎可比造架飞机难得多)
 
解决方案如下:
 
首先,关闭旧版本上传入口(避免继续使用导致数据不一致)。将旧图片数据通过rsync工具一次性迁移到独立的图片服务器上(即下图中描述的Old Image Server)。在最前端(七层代理,如Haproxy、Nginx)用ACL(访问规则控制),将旧图片对应URL规则的请求(正则)匹配到,然后将请求直接转发指定的web 服务器列表,在该列表中的服务器上配置好提供图片(以Web方式)访问的站点,并加入缓存策略。这样实现旧图片服务器的分离和缓存,兼容了旧图片的访问规则并提升旧图片访问效率,也避免了实时同步所带来的问题。
 
整体架构如图:

2.jpg

基于FastDFS的独立图片服务器集群架构,虽然已经非常的成熟,但是由于国内“南北互联”和IDC带宽成本等问题(图片是非常消耗流量的),我们最终还是选择了商用的CDN技术,实现起来也非常容易,原理其实也很简单,我这里只做个简单的介绍:

将img域名cname到CDN厂商指定的域名上,用户请求访问图片时,则由CDN厂商提供智能DNS解析,将最近的(当然也可能有其它更复杂的策略,例如负载情况、健康状态等)服务节点地址返回给用户,用户请求到达指定的服务器节点上,该节点上提供了类似Squid/Vanish的代理缓存服务,如果是第一次请求该路径,则会从源站获取图片资源返回客户端浏览器,如果缓存中存在,则直接从缓存中获取并返回给客户端浏览器,完成请求/响应过程。

由于采用了商用CDN服务,所以我们并没有考虑用Squid/Vanish来自行构建前置代理缓存。

上面的整个集群架构,可以很方便的做横向扩展,能满足一般垂直领域中大型网站的图片服务需求(当然,像taobao这样超大规模的可能另当别论)。经测试,提供图片访问的单台Nginx服务器(至强E5四核CPU、16G内存、SSD),对小静态页面(压缩后大概只有10kb左右的)可以扛住几千个并发且毫无压力。当然,由于图片本身体积比纯文本的静态页面大很多,提供图片访问的服务器的抗并发能力,往往会受限于磁盘的I/O处理能力和IDC提供的带宽。Nginx的抗并发能力还是非常强的,而且对资源占用很低,尤其是处理静态资源,似乎都不需要有过多担心了。可以根据实际访问量的需求,通过调整Nginx的参数,对Linux内核做调优,加入分级缓存策略等手段能够做更大程度的优化,也可以通过增加服务器或者升级服务器配置来做扩展,最直接的是通过购买更高级的存储设备和更大的带宽,以满足更大访问量的需求。

值得一提的是,在“云计算”流行的当下,也推荐高速发展期间的网站,使用“云存储”这样的方案,既能帮你解决各类存储、扩展、备灾的问题,又能做好CDN加速。最重要的是,价格也不贵。

总结,有关图片服务器架构扩展,大致围绕这些问题展开:
 
  • 容量规划和扩展问题。
  • 数据的同步、冗余和容灾。
  • 硬件设备的成本和可靠性(是普通机械硬盘,还是SSD,或者更高端的存储设备和方案)。
  • 文件系统的选择。根据文件特性(例如文件大小、读写比例等)选择是用ext3/4或者NFS/GFS/TFS这些开源的(分布式)文件系统。
  • 图片的加速访问。采用商用CDN或者自建的代理缓存、web静态缓存架构。
  • 旧图片路径和访问规则的兼容性,应用程序层面的可扩展,上传和访问的性能和安全性等。

3
回复

在一台手机锁屏的状态下,环信demo实时语音无法接通 环信_Android

lizg 回复了问题 • 3 人关注 • 2705 次浏览 • 2015-07-03 16:39 • 来自相关话题

0
评论

解析:带你从源码的角度彻底理解,Android事件分发机制(下) 移动开发

oscar 发表了文章 • 1362 次浏览 • 2015-07-03 14:28 • 来自相关话题

记得在前面的文章中,我带大家一起从源码的角度分析了Android中View的事件分发机制,相信阅读过的朋友对View的事件分发已经有比较深刻的理解了。

还未阅读过的朋友,请先参考上文:http://www.imgeek.org/article/51

那么今天我们将继续上次未完成的话题,从源码的角度分析ViewGruop的事件分发。


首先我们来探讨一下,什么是ViewGroup?它和普通的View有什么区别?

顾名思义,ViewGroup就是一组View的集合,它包含很多的子View和子VewGroup,是Android中所有布局的父类或间接父类,像LinearLayout、RelativeLayout等都是继承自ViewGroup的。但ViewGroup实际上也是一个View,只不过比起View,它多了可以包含子View和定义布局参数的功能。ViewGroup继承结构示意图如下所示:






可以看到,我们平时项目里经常用到的各种布局,全都属于ViewGroup的子类。

简单介绍完了ViewGroup,我们现在通过一个Demo来演示一下Android中VewGroup的事件分发流程吧。


先我们来自定义一个布局,命名为MyLayout,继承自LinearLayout,如下所示:
public class MyLayout extends LinearLayout {
02.
03. public MyLayout(Context context, AttributeSet attrs) {
04. super(context, attrs);
05. }
06.
07.}
然后,打开主布局文件activity_main.xml,在其中加入我们自定义的布局:

[html] view plaincopy
01.<com.example.viewgrouptouchevent.MyLayout xmlns:android="http://schemas.android.com/apk/res/android"
02. xmlns:tools="http://schemas.android.com/tools"
03. android:id="@+id/my_layout"
04. android:layout_width="match_parent"
05. android:layout_height="match_parent"
06. android:orientation="vertical" >
07.
08. <Button
09. android:id="@+id/button1"
10. android:layout_width="match_parent"
11. android:layout_height="wrap_content"
12. android:text="Button1" />
13.
14. <Button
15. android:id="@+id/button2"
16. android:layout_width="match_parent"
17. android:layout_height="wrap_content"
18. android:text="Button2" />
19.
20.</com.example.viewgrouptouchevent.MyLayout>
可以看到,我们在MyLayout中添加了两个按钮,接着在MainActivity中为这两个按钮和MyLayout都注册了监听事件:
myLayout.setOnTouchListener(new OnTouchListener() {
02. @Override
03. public boolean onTouch(View v, MotionEvent event) {
04. Log.d("TAG", "myLayout on touch");
05. return false;
06. }
07.});
08.button1.setOnClickListener(new OnClickListener() {
09. @Override
10. public void onClick(View v) {
11. Log.d("TAG", "You clicked button1");
12. }
13.});
14.button2.setOnClickListener(new OnClickListener() {
15. @Override
16. public void onClick(View v) {
17. Log.d("TAG", "You clicked button2");
18. }
19.});
 我们在MyLayout的onTouch方法,和Button1、Button2的onClick方法中都打印了一句话。现在运行一下项目,效果图如下所示:






分别点击一下Button1、Button2和空白区域,打印结果如下所示:






你会发现,当点击按钮的时候,MyLayout注册的onTouch方法并不会执行,只有点击空白区域的时候才会执行该方法。你可以先理解成Button的onClick方法将事件消费掉了,因此事件不会再继续向下传递。

那就说明Android中的touch事件是先传递到View,再传递到ViewGroup的?现在下结论还未免过早了,让我们再来做一个实验。

查阅文档可以看到,ViewGroup中有一个onInterceptTouchEvent方法,我们来看一下这个方法的源码:
/**
02. * Implement this method to intercept all touch screen motion events. This
03. * allows you to watch events as they are dispatched to your children, and
04. * take ownership of the current gesture at any point.
05. *
06. * <p>Using this function takes some care, as it has a fairly complicated
07. * interaction with {@link View#onTouchEvent(MotionEvent)
08. * View.onTouchEvent(MotionEvent)}, and using it requires implementing
09. * that method as well as this one in the correct way. Events will be
10. * received in the following order:
11. *
12. * <ol>
13. * <li> You will receive the down event here.
14. * <li> The down event will be handled either by a child of this view
15. * group, or given to your own onTouchEvent() method to handle; this means
16. * you should implement onTouchEvent() to return true, so you will
17. * continue to see the rest of the gesture (instead of looking for
18. * a parent view to handle it). Also, by returning true from
19. * onTouchEvent(), you will not receive any following
20. * events in onInterceptTouchEvent() and all touch processing must
21. * happen in onTouchEvent() like normal.
22. * <li> For as long as you return false from this function, each following
23. * event (up to and including the final up) will be delivered first here
24. * and then to the target's onTouchEvent().
25. * <li> If you return true from here, you will not receive any
26. * following events: the target view will receive the same event but
27. * with the action {@link MotionEvent#ACTION_CANCEL}, and all further
28. * events will be delivered to your onTouchEvent() method and no longer
29. * appear here.
30. * </ol>
31. *
32. * @param ev The motion event being dispatched down the hierarchy.
33. * @return Return true to steal motion events from the children and have
34. * them dispatched to this ViewGroup through onTouchEvent().
35. * The current target will receive an ACTION_CANCEL event, and no further
36. * messages will be delivered here.
37. */
38.public boolean onInterceptTouchEvent(MotionEvent ev) {
39. return false;
40.}
如果不看源码你还真可能被这注释吓到了,这么长的英文注释看得头都大了。可是源码竟然如此简单!只有一行代码,返回了一个false!好吧,既然是布尔型的返回,那么只有两种可能,我们在MyLayout中重写这个方法,然后返回一个true试试,代码如下所示:
public class MyLayout extends LinearLayout {
02.
03. public MyLayout(Context context, AttributeSet attrs) {
04. super(context, attrs);
05. }
06.
07. @Override
08. public boolean onInterceptTouchEvent(MotionEvent ev) {
09. return true;
10. }
11.
12.}
现在再次运行项目,然后分别Button1、Button2和空白区域,打印结果如下所示:





你会发现,不管你点击哪里,永远都只会触发MyLayout的touch事件了,按钮的点击事件完全被屏蔽掉了!这是为什么呢?如果Android中的touch事件是先传递到View,再传递到ViewGroup的,那么MyLayout又怎么可能屏蔽掉Button的点击事件呢?

看来只有通过阅读源码,搞清楚Android中ViewGroup的事件分发机制,才能解决我们心中的疑惑了,不过这里我想先跟你透露一句,Android中touch事件的传递,绝对是先传递到ViewGroup,再传递到View的。记得在Android事件分发机制完全解析,带你从源码的角度彻底理解(上) 中我有说明过,只要你触摸了任何控件,就一定会调用该控件的dispatchTouchEvent方法。这个说法没错,只不过还不完整而已。实际情况是,当你点击了某个控件,首先会去调用该控件所在布局的dispatchTouchEvent方法,然后在布局的dispatchTouchEvent方法中找到被点击的相应控件,再去调用该控件的dispatchTouchEvent方法。如果我们点击了MyLayout中的按钮,会先去调用MyLayout的dispatchTouchEvent方法,可是你会发现MyLayout中并没有这个方法。那就再到它的父类LinearLayout中找一找,发现也没有这个方法。那只好继续再找LinearLayout的父类ViewGroup,你终于在ViewGroup中看到了这个方法,按钮的dispatchTouchEvent方法就是在这里调用的。修改后的示意图如下所示:






那还等什么?快去看一看ViewGroup中的dispatchTouchEvent方法的源码吧!代码如下所示:
public boolean dispatchTouchEvent(MotionEvent ev) {
02. final int action = ev.getAction();
03. final float xf = ev.getX();
04. final float yf = ev.getY();
05. final float scrolledXFloat = xf + mScrollX;
06. final float scrolledYFloat = yf + mScrollY;
07. final Rect frame = mTempRect;
08. boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
09. if (action == MotionEvent.ACTION_DOWN) {
10. if (mMotionTarget != null) {
11. mMotionTarget = null;
12. }
13. if (disallowIntercept || !onInterceptTouchEvent(ev)) {
14. ev.setAction(MotionEvent.ACTION_DOWN);
15. final int scrolledXInt = (int) scrolledXFloat;
16. final int scrolledYInt = (int) scrolledYFloat;
17. final View[] children = mChildren;
18. final int count = mChildrenCount;
19. for (int i = count - 1; i >= 0; i--) {
20. final View child = children[i];
21. if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
22. || child.getAnimation() != null) {
23. child.getHitRect(frame);
24. if (frame.contains(scrolledXInt, scrolledYInt)) {
25. final float xc = scrolledXFloat - child.mLeft;
26. final float yc = scrolledYFloat - child.mTop;
27. ev.setLocation(xc, yc);
28. child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
29. if (child.dispatchTouchEvent(ev)) {
30. mMotionTarget = child;
31. return true;
32. }
33. }
34. }
35. }
36. }
37. }
38. boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
39. (action == MotionEvent.ACTION_CANCEL);
40. if (isUpOrCancel) {
41. mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
42. }
43. final View target = mMotionTarget;
44. if (target == null) {
45. ev.setLocation(xf, yf);
46. if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
47. ev.setAction(MotionEvent.ACTION_CANCEL);
48. mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
49. }
50. return super.dispatchTouchEvent(ev);
51. }
52. if (!disallowIntercept && onInterceptTouchEvent(ev)) {
53. final float xc = scrolledXFloat - (float) target.mLeft;
54. final float yc = scrolledYFloat - (float) target.mTop;
55. mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
56. ev.setAction(MotionEvent.ACTION_CANCEL);
57. ev.setLocation(xc, yc);
58. if (!target.dispatchTouchEvent(ev)) {
59. }
60. mMotionTarget = null;
61. return true;
62. }
63. if (isUpOrCancel) {
64. mMotionTarget = null;
65. }
66. final float xc = scrolledXFloat - (float) target.mLeft;
67. final float yc = scrolledYFloat - (float) target.mTop;
68. ev.setLocation(xc, yc);
69. if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
70. ev.setAction(MotionEvent.ACTION_CANCEL);
71. target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
72. mMotionTarget = null;
73. }
74. return target.dispatchTouchEvent(ev);
75.}
这个方法代码比较长,我们只挑重点看。首先在第13行可以看到一个条件判断,如果disallowIntercept和!onInterceptTouchEvent(ev)两者有一个为true,就会进入到这个条件判断中。disallowIntercept是指是否禁用掉事件拦截的功能,默认是false,也可以通过调用requestDisallowInterceptTouchEvent方法对这个值进行修改。那么当第一个值为false的时候就会完全依赖第二个值来决定是否可以进入到条件判断的内部,第二个值是什么呢?竟然就是对onInterceptTouchEvent方法的返回值取反!也就是说如果我们在onInterceptTouchEvent方法中返回false,就会让第二个值为true,从而进入到条件判断的内部,如果我们在onInterceptTouchEvent方法中返回true,就会让第二个值为false,从而跳出了这个条件判断。这个时候你就可以思考一下了,由于我们刚刚在MyLayout中重写了onInterceptTouchEvent方法,让这个方法返回true,导致所有按钮的点击事件都被屏蔽了,那我们就完全有理由相信,按钮点击事件的处理就是在第13行条件判断的内部进行的!

那我们重点来看下条件判断的内部是怎么实现的。在第19行通过一个for循环,遍历了当前ViewGroup下的所有子View,然后在第24行判断当前遍历的View是不是正在点击的View,如果是的话就会进入到该条件判断的内部,然后在第29行调用了该View的dispatchTouchEvent,之后的流程就和Android事件分发机制(上)中讲解的是一样的了。我们也因此证实了,按钮点击事件的处理确实就是在这里进行的。

然后需要注意一下,调用子View的dispatchTouchEvent后是有返回值的。我们已经知道,如果一个控件是可点击的,那么点击该控件时,dispatchTouchEvent的返回值必定是true。因此会导致第29行的条件判断成立,于是在第31行给ViewGroup的dispatchTouchEvent方法直接返回了true。这样就导致后面的代码无法执行到了,也是印证了我们前面的Demo打印的结果,如果按钮的点击事件得到执行,就会把MyLayout的touch事件拦截掉。

那如果我们点击的不是按钮,而是空白区域呢?这种情况就一定不会在第31行返回true了,而是会继续执行后面的代码。那我们继续往后看,在第44行,如果target等于null,就会进入到该条件判断内部,这里一般情况下target都会是null,因此会在第50行调用super.dispatchTouchEvent(ev)。这句代码会调用到哪里呢?当然是View中的dispatchTouchEvent方法了,因为ViewGroup的父类就是View。之后的处理逻辑又和前面所说的是一样的了,也因此MyLayout中注册的onTouch方法会得到执行。之后的代码在一般情况下是走不到的了,我们也就不再继续往下分析。

再看一下整个ViewGroup事件分发过程的流程图吧,相信可以帮助大家更好地去理解:






现在整个ViewGroup的事件分发流程的分析也就到此结束了,我们最后再来简单梳理一下吧。
 
 Android事件分发是先传递到ViewGroup,再由ViewGroup传递到View的。 在ViewGroup中可以通过onInterceptTouchEvent方法对事件传递进行拦截,onInterceptTouchEvent方法返回true代表不允许事件继续向子View传递,返回false代表不对事件进行拦截,默认返回false。子View中如果将传递的事件消费掉,ViewGroup中将无法接收到任何事件。好了,Android事件分发机制完全解析到此全部结束,结合上下两篇,相信大家对事件分发的理解已经非常深刻了。
  查看全部
记得在前面的文章中,我带大家一起从源码的角度分析了Android中View的事件分发机制,相信阅读过的朋友对View的事件分发已经有比较深刻的理解了。

还未阅读过的朋友,请先参考上文:http://www.imgeek.org/article/51

那么今天我们将继续上次未完成的话题,从源码的角度分析ViewGruop的事件分发。


首先我们来探讨一下,什么是ViewGroup?它和普通的View有什么区别?

顾名思义,ViewGroup就是一组View的集合,它包含很多的子View和子VewGroup,是Android中所有布局的父类或间接父类,像LinearLayout、RelativeLayout等都是继承自ViewGroup的。但ViewGroup实际上也是一个View,只不过比起View,它多了可以包含子View和定义布局参数的功能。ViewGroup继承结构示意图如下所示:

7.png


可以看到,我们平时项目里经常用到的各种布局,全都属于ViewGroup的子类。

简单介绍完了ViewGroup,我们现在通过一个Demo来演示一下Android中VewGroup的事件分发流程吧。


先我们来自定义一个布局,命名为MyLayout,继承自LinearLayout,如下所示:
public class MyLayout extends LinearLayout {  
02.
03. public MyLayout(Context context, AttributeSet attrs) {
04. super(context, attrs);
05. }
06.
07.}
然后,打开主布局文件activity_main.xml,在其中加入我们自定义的布局:

[html] view plaincopy
01.<com.example.viewgrouptouchevent.MyLayout xmlns:android="http://schemas.android.com/apk/res/android"
02. xmlns:tools="http://schemas.android.com/tools"
03. android:id="@+id/my_layout"
04. android:layout_width="match_parent"
05. android:layout_height="match_parent"
06. android:orientation="vertical" >
07.
08. <Button
09. android:id="@+id/button1"
10. android:layout_width="match_parent"
11. android:layout_height="wrap_content"
12. android:text="Button1" />
13.
14. <Button
15. android:id="@+id/button2"
16. android:layout_width="match_parent"
17. android:layout_height="wrap_content"
18. android:text="Button2" />
19.
20.</com.example.viewgrouptouchevent.MyLayout>
可以看到,我们在MyLayout中添加了两个按钮,接着在MainActivity中为这两个按钮和MyLayout都注册了监听事件:
myLayout.setOnTouchListener(new OnTouchListener() {  
02. @Override
03. public boolean onTouch(View v, MotionEvent event) {
04. Log.d("TAG", "myLayout on touch");
05. return false;
06. }
07.});
08.button1.setOnClickListener(new OnClickListener() {
09. @Override
10. public void onClick(View v) {
11. Log.d("TAG", "You clicked button1");
12. }
13.});
14.button2.setOnClickListener(new OnClickListener() {
15. @Override
16. public void onClick(View v) {
17. Log.d("TAG", "You clicked button2");
18. }
19.});
 我们在MyLayout的onTouch方法,和Button1、Button2的onClick方法中都打印了一句话。现在运行一下项目,效果图如下所示:

8.png


分别点击一下Button1、Button2和空白区域,打印结果如下所示:

9.png


你会发现,当点击按钮的时候,MyLayout注册的onTouch方法并不会执行,只有点击空白区域的时候才会执行该方法。你可以先理解成Button的onClick方法将事件消费掉了,因此事件不会再继续向下传递。

那就说明Android中的touch事件是先传递到View,再传递到ViewGroup的?现在下结论还未免过早了,让我们再来做一个实验。

查阅文档可以看到,ViewGroup中有一个onInterceptTouchEvent方法,我们来看一下这个方法的源码:
/** 
02. * Implement this method to intercept all touch screen motion events. This
03. * allows you to watch events as they are dispatched to your children, and
04. * take ownership of the current gesture at any point.
05. *
06. * <p>Using this function takes some care, as it has a fairly complicated
07. * interaction with {@link View#onTouchEvent(MotionEvent)
08. * View.onTouchEvent(MotionEvent)}, and using it requires implementing
09. * that method as well as this one in the correct way. Events will be
10. * received in the following order:
11. *
12. * <ol>
13. * <li> You will receive the down event here.
14. * <li> The down event will be handled either by a child of this view
15. * group, or given to your own onTouchEvent() method to handle; this means
16. * you should implement onTouchEvent() to return true, so you will
17. * continue to see the rest of the gesture (instead of looking for
18. * a parent view to handle it). Also, by returning true from
19. * onTouchEvent(), you will not receive any following
20. * events in onInterceptTouchEvent() and all touch processing must
21. * happen in onTouchEvent() like normal.
22. * <li> For as long as you return false from this function, each following
23. * event (up to and including the final up) will be delivered first here
24. * and then to the target's onTouchEvent().
25. * <li> If you return true from here, you will not receive any
26. * following events: the target view will receive the same event but
27. * with the action {@link MotionEvent#ACTION_CANCEL}, and all further
28. * events will be delivered to your onTouchEvent() method and no longer
29. * appear here.
30. * </ol>
31. *
32. * @param ev The motion event being dispatched down the hierarchy.
33. * @return Return true to steal motion events from the children and have
34. * them dispatched to this ViewGroup through onTouchEvent().
35. * The current target will receive an ACTION_CANCEL event, and no further
36. * messages will be delivered here.
37. */
38.public boolean onInterceptTouchEvent(MotionEvent ev) {
39. return false;
40.}
如果不看源码你还真可能被这注释吓到了,这么长的英文注释看得头都大了。可是源码竟然如此简单!只有一行代码,返回了一个false!好吧,既然是布尔型的返回,那么只有两种可能,我们在MyLayout中重写这个方法,然后返回一个true试试,代码如下所示:
public class MyLayout extends LinearLayout {  
02.
03. public MyLayout(Context context, AttributeSet attrs) {
04. super(context, attrs);
05. }
06.
07. @Override
08. public boolean onInterceptTouchEvent(MotionEvent ev) {
09. return true;
10. }
11.
12.}
现在再次运行项目,然后分别Button1、Button2和空白区域,打印结果如下所示:

10.png

你会发现,不管你点击哪里,永远都只会触发MyLayout的touch事件了,按钮的点击事件完全被屏蔽掉了!这是为什么呢?如果Android中的touch事件是先传递到View,再传递到ViewGroup的,那么MyLayout又怎么可能屏蔽掉Button的点击事件呢?

看来只有通过阅读源码,搞清楚Android中ViewGroup的事件分发机制,才能解决我们心中的疑惑了,不过这里我想先跟你透露一句,Android中touch事件的传递,绝对是先传递到ViewGroup,再传递到View的。记得在Android事件分发机制完全解析,带你从源码的角度彻底理解(上) 中我有说明过,只要你触摸了任何控件,就一定会调用该控件的dispatchTouchEvent方法。这个说法没错,只不过还不完整而已。实际情况是,当你点击了某个控件,首先会去调用该控件所在布局的dispatchTouchEvent方法,然后在布局的dispatchTouchEvent方法中找到被点击的相应控件,再去调用该控件的dispatchTouchEvent方法。如果我们点击了MyLayout中的按钮,会先去调用MyLayout的dispatchTouchEvent方法,可是你会发现MyLayout中并没有这个方法。那就再到它的父类LinearLayout中找一找,发现也没有这个方法。那只好继续再找LinearLayout的父类ViewGroup,你终于在ViewGroup中看到了这个方法,按钮的dispatchTouchEvent方法就是在这里调用的。修改后的示意图如下所示:

11.png


那还等什么?快去看一看ViewGroup中的dispatchTouchEvent方法的源码吧!代码如下所示:
public boolean dispatchTouchEvent(MotionEvent ev) {  
02. final int action = ev.getAction();
03. final float xf = ev.getX();
04. final float yf = ev.getY();
05. final float scrolledXFloat = xf + mScrollX;
06. final float scrolledYFloat = yf + mScrollY;
07. final Rect frame = mTempRect;
08. boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
09. if (action == MotionEvent.ACTION_DOWN) {
10. if (mMotionTarget != null) {
11. mMotionTarget = null;
12. }
13. if (disallowIntercept || !onInterceptTouchEvent(ev)) {
14. ev.setAction(MotionEvent.ACTION_DOWN);
15. final int scrolledXInt = (int) scrolledXFloat;
16. final int scrolledYInt = (int) scrolledYFloat;
17. final View[] children = mChildren;
18. final int count = mChildrenCount;
19. for (int i = count - 1; i >= 0; i--) {
20. final View child = children[i];
21. if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
22. || child.getAnimation() != null) {
23. child.getHitRect(frame);
24. if (frame.contains(scrolledXInt, scrolledYInt)) {
25. final float xc = scrolledXFloat - child.mLeft;
26. final float yc = scrolledYFloat - child.mTop;
27. ev.setLocation(xc, yc);
28. child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
29. if (child.dispatchTouchEvent(ev)) {
30. mMotionTarget = child;
31. return true;
32. }
33. }
34. }
35. }
36. }
37. }
38. boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
39. (action == MotionEvent.ACTION_CANCEL);
40. if (isUpOrCancel) {
41. mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
42. }
43. final View target = mMotionTarget;
44. if (target == null) {
45. ev.setLocation(xf, yf);
46. if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
47. ev.setAction(MotionEvent.ACTION_CANCEL);
48. mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
49. }
50. return super.dispatchTouchEvent(ev);
51. }
52. if (!disallowIntercept && onInterceptTouchEvent(ev)) {
53. final float xc = scrolledXFloat - (float) target.mLeft;
54. final float yc = scrolledYFloat - (float) target.mTop;
55. mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
56. ev.setAction(MotionEvent.ACTION_CANCEL);
57. ev.setLocation(xc, yc);
58. if (!target.dispatchTouchEvent(ev)) {
59. }
60. mMotionTarget = null;
61. return true;
62. }
63. if (isUpOrCancel) {
64. mMotionTarget = null;
65. }
66. final float xc = scrolledXFloat - (float) target.mLeft;
67. final float yc = scrolledYFloat - (float) target.mTop;
68. ev.setLocation(xc, yc);
69. if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
70. ev.setAction(MotionEvent.ACTION_CANCEL);
71. target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
72. mMotionTarget = null;
73. }
74. return target.dispatchTouchEvent(ev);
75.}
这个方法代码比较长,我们只挑重点看。首先在第13行可以看到一个条件判断,如果disallowIntercept和!onInterceptTouchEvent(ev)两者有一个为true,就会进入到这个条件判断中。disallowIntercept是指是否禁用掉事件拦截的功能,默认是false,也可以通过调用requestDisallowInterceptTouchEvent方法对这个值进行修改。那么当第一个值为false的时候就会完全依赖第二个值来决定是否可以进入到条件判断的内部,第二个值是什么呢?竟然就是对onInterceptTouchEvent方法的返回值取反!也就是说如果我们在onInterceptTouchEvent方法中返回false,就会让第二个值为true,从而进入到条件判断的内部,如果我们在onInterceptTouchEvent方法中返回true,就会让第二个值为false,从而跳出了这个条件判断。这个时候你就可以思考一下了,由于我们刚刚在MyLayout中重写了onInterceptTouchEvent方法,让这个方法返回true,导致所有按钮的点击事件都被屏蔽了,那我们就完全有理由相信,按钮点击事件的处理就是在第13行条件判断的内部进行的!

那我们重点来看下条件判断的内部是怎么实现的。在第19行通过一个for循环,遍历了当前ViewGroup下的所有子View,然后在第24行判断当前遍历的View是不是正在点击的View,如果是的话就会进入到该条件判断的内部,然后在第29行调用了该View的dispatchTouchEvent,之后的流程就和Android事件分发机制(上)中讲解的是一样的了。我们也因此证实了,按钮点击事件的处理确实就是在这里进行的。

然后需要注意一下,调用子View的dispatchTouchEvent后是有返回值的。我们已经知道,如果一个控件是可点击的,那么点击该控件时,dispatchTouchEvent的返回值必定是true。因此会导致第29行的条件判断成立,于是在第31行给ViewGroup的dispatchTouchEvent方法直接返回了true。这样就导致后面的代码无法执行到了,也是印证了我们前面的Demo打印的结果,如果按钮的点击事件得到执行,就会把MyLayout的touch事件拦截掉。

那如果我们点击的不是按钮,而是空白区域呢?这种情况就一定不会在第31行返回true了,而是会继续执行后面的代码。那我们继续往后看,在第44行,如果target等于null,就会进入到该条件判断内部,这里一般情况下target都会是null,因此会在第50行调用super.dispatchTouchEvent(ev)。这句代码会调用到哪里呢?当然是View中的dispatchTouchEvent方法了,因为ViewGroup的父类就是View。之后的处理逻辑又和前面所说的是一样的了,也因此MyLayout中注册的onTouch方法会得到执行。之后的代码在一般情况下是走不到的了,我们也就不再继续往下分析。

再看一下整个ViewGroup事件分发过程的流程图吧,相信可以帮助大家更好地去理解:

12.png


现在整个ViewGroup的事件分发流程的分析也就到此结束了,我们最后再来简单梳理一下吧。
 
  •  Android事件分发是先传递到ViewGroup,再由ViewGroup传递到View的。
  •  在ViewGroup中可以通过onInterceptTouchEvent方法对事件传递进行拦截,onInterceptTouchEvent方法返回true代表不允许事件继续向子View传递,返回false代表不对事件进行拦截,默认返回false。
  • 子View中如果将传递的事件消费掉,ViewGroup中将无法接收到任何事件。
  • 好了,Android事件分发机制完全解析到此全部结束,结合上下两篇,相信大家对事件分发的理解已经非常深刻了。

 
0
评论

解析:带你从源码的角度彻底理解,Android事件分发机制(上) 移动开发

oscar 发表了文章 • 1254 次浏览 • 2015-07-03 14:05 • 来自相关话题

其实我一直准备写一篇关于Android事件分发机制的文章,从我的第一篇博客开始,就零零散散在好多地方使用到了Android事件分发的知识。也有好多朋友问过我各种问题,比如:onTouch和onTouchEvent有什么区别,又该如何使用?为什么给ListView引入了一个滑动菜单的功能,ListView就不能滚动了?为什么图片轮播器里的图片使用Button而不用ImageView?等等……对于这些问题,我并没有给出非常详细的回答,因为我知道如果想要彻底搞明白这些问题,掌握Android事件分发机制是必不可少的,而Android事件分发机制绝对不是三言两语就能说得清的。

在我经过较长时间的筹备之后,终于决定开始写这样一篇文章了。目前虽然网上相关的文章也不少,但我觉得没有哪篇写得特别详细的(也许我还没有找到),多数文章只是讲了讲理论,然后配合demo运行了一下结果。而我准备带着大家从源码的角度进行分析,相信大家可以更加深刻地理解Android事件分发机制。


阅读源码讲究由浅入深,循序渐进,因此我们也从简单的开始,本篇先带大家探究View的事件分发,下篇再去探究难度更高的ViewGroup的事件分发。


那我们现在就开始吧!比如说你当前有一个非常简单的项目,只有一个Activity,并且Activity中只有一个按钮。你可能已经知道,如果想要给这个按钮注册一个点击事件,只需要调用:
 
button.setOnClickListener(new OnClickListener() {
02. @Override
03. public void onClick(View v) {
04. Log.d("TAG", "onClick execute");
05. }
06.});
这样在onClick方法里面写实现,就可以在按钮被点击的时候执行。你可能也已经知道,如果想给这个按钮再添加一个touch事件,只需要调用:
button.setOnTouchListener(new OnTouchListener() {
02. @Override
03. public boolean onTouch(View v, MotionEvent event) {
04. Log.d("TAG", "onTouch execute, action " + event.getAction());
05. return false;
06. }
07.});
onTouch方法里能做的事情比onClick要多一些,比如判断手指按下、抬起、移动等事件。那么如果我两个事件都注册了,哪一个会先执行呢?我们来试一下就知道了,运行程序点击按钮,打印结果如下:





可以看到,onTouch是优先于onClick执行的,并且onTouch执行了两次,一次是ACTION_DOWN,一次是ACTION_UP(你还可能会有多次ACTION_MOVE的执行,如果你手抖了一下)。因此事件传递的顺序是先经过onTouch,再传递到onClick。

细心的朋友应该可以注意到,onTouch方法是有返回值的,这里我们返回的是false,如果我们尝试把onTouch方法里的返回值改成true,再运行一次,结果如下:





我们发现,onClick方法不再执行了!为什么会这样呢?你可以先理解成onTouch方法返回true就认为这个事件被onTouch消费掉了,因而不会再继续向下传递。

如果到现在为止,以上的所有知识点你都是清楚的,那么说明你对Android事件传递的基本用法应该是掌握了。不过别满足于现状,让我们从源码的角度分析一下,出现上述现象的原理是什么。

首先你需要知道一点,只要你触摸到了任何一个控件,就一定会调用该控件的dispatchTouchEvent方法。那当我们去点击按钮的时候,就会去调用Button类里的dispatchTouchEvent方法,可是你会发现Button类里并没有这个方法,那么就到它的父类TextView里去找一找,你会发现TextView里也没有这个方法,那没办法了,只好继续在TextView的父类View里找一找,这个时候你终于在View里找到了这个方法,示意图如下:





然后我们来看一下View中dispatchTouchEvent方法的源码:
public boolean dispatchTouchEvent(MotionEvent event) {
02. if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
03. mOnTouchListener.onTouch(this, event)) {
04. return true;
05. }
06. return onTouchEvent(event);
07.}
这个方法非常的简洁,只有短短几行代码!我们可以看到,在这个方法内,首先是进行了一个判断,如果mOnTouchListener != null,(mViewFlags & ENABLED_MASK) == ENABLED和mOnTouchListener.onTouch(this, event)这三个条件都为真,就返回true,否则就去执行onTouchEvent(event)方法并返回。

先看一下第一个条件,mOnTouchListener这个变量是在哪里赋值的呢?我们寻找之后在View里发现了如下方法:
public void setOnTouchListener(OnTouchListener l) {
02. mOnTouchListener = l;
03.}
Bingo!找到了,mOnTouchListener正是在setOnTouchListener方法里赋值的,也就是说只要我们给控件注册了touch事件,mOnTouchListener就一定被赋值了。第二个条件(mViewFlags & ENABLED_MASK) == ENABLED是判断当前点击的控件是否是enable的,按钮默认都是enable的,因此这个条件恒定为true。

第三个条件就比较关键了,mOnTouchListener.onTouch(this, event),其实也就是去回调控件注册touch事件时的onTouch方法。也就是说如果我们在onTouch方法里返回true,就会让这三个条件全部成立,从而整个方法直接返回true。如果我们在onTouch方法里返回false,就会再去执行onTouchEvent(event)方法。

现在我们可以结合前面的例子来分析一下了,首先在dispatchTouchEvent中最先执行的就是onTouch方法,因此onTouch肯定是要优先于onClick执行的,也是印证了刚刚的打印结果。而如果在onTouch方法里返回了true,就会让dispatchTouchEvent方法直接返回true,不会再继续往下执行。而打印结果也证实了如果onTouch返回true,onClick就不会再执行了。

根据以上源码的分析,从原理上解释了我们前面例子的运行结果。而上面的分析还透漏出了一个重要的信息,那就是onClick的调用肯定是在onTouchEvent(event)方法中的!那我们马上来看下onTouchEvent的源码,如下所示:
public boolean onTouchEvent(MotionEvent event) {
02. final int viewFlags = mViewFlags;
03. if ((viewFlags & ENABLED_MASK) == DISABLED) {
04. // A disabled view that is clickable still consumes the touch
05. // events, it just doesn't respond to them.
06. return (((viewFlags & CLICKABLE) == CLICKABLE ||
07. (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
08. }
09. if (mTouchDelegate != null) {
10. if (mTouchDelegate.onTouchEvent(event)) {
11. return true;
12. }
13. }
14. if (((viewFlags & CLICKABLE) == CLICKABLE ||
15. (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
16. switch (event.getAction()) {
17. case MotionEvent.ACTION_UP:
18. boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
19. if ((mPrivateFlags & PRESSED) != 0 || prepressed) {
20. // take focus if we don't have it already and we should in
21. // touch mode.
22. boolean focusTaken = false;
23. if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
24. focusTaken = requestFocus();
25. }
26. if (!mHasPerformedLongPress) {
27. // This is a tap, so remove the longpress check
28. removeLongPressCallback();
29. // Only perform take click actions if we were in the pressed state
30. if (!focusTaken) {
31. // Use a Runnable and post this rather than calling
32. // performClick directly. This lets other visual state
33. // of the view update before click actions start.
34. if (mPerformClick == null) {
35. mPerformClick = new PerformClick();
36. }
37. if (!post(mPerformClick)) {
38. performClick();
39. }
40. }
41. }
42. if (mUnsetPressedState == null) {
43. mUnsetPressedState = new UnsetPressedState();
44. }
45. if (prepressed) {
46. mPrivateFlags |= PRESSED;
47. refreshDrawableState();
48. postDelayed(mUnsetPressedState,
49. ViewConfiguration.getPressedStateDuration());
50. } else if (!post(mUnsetPressedState)) {
51. // If the post failed, unpress right now
52. mUnsetPressedState.run();
53. }
54. removeTapCallback();
55. }
56. break;
57. case MotionEvent.ACTION_DOWN:
58. if (mPendingCheckForTap == null) {
59. mPendingCheckForTap = new CheckForTap();
60. }
61. mPrivateFlags |= PREPRESSED;
62. mHasPerformedLongPress = false;
63. postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
64. break;
65. case MotionEvent.ACTION_CANCEL:
66. mPrivateFlags &= ~PRESSED;
67. refreshDrawableState();
68. removeTapCallback();
69. break;
70. case MotionEvent.ACTION_MOVE:
71. final int x = (int) event.getX();
72. final int y = (int) event.getY();
73. // Be lenient about moving outside of buttons
74. int slop = mTouchSlop;
75. if ((x < 0 - slop) || (x >= getWidth() + slop) ||
76. (y < 0 - slop) || (y >= getHeight() + slop)) {
77. // Outside button
78. removeTapCallback();
79. if ((mPrivateFlags & PRESSED) != 0) {
80. // Remove any future long press/tap checks
81. removeLongPressCallback();
82. // Need to switch from pressed to not pressed
83. mPrivateFlags &= ~PRESSED;
84. refreshDrawableState();
85. }
86. }
87. break;
88. }
89. return true;
90. }
91. return false;
92.}
相较于刚才的dispatchTouchEvent方法,onTouchEvent方法复杂了很多,不过没关系,我们只挑重点看就可以了。首先在第14行我们可以看出,如果该控件是可以点击的就会进入到第16行的switch判断中去,而如果当前的事件是抬起手指,则会进入到MotionEvent.ACTION_UP这个case当中。在经过种种判断之后,会执行到第38行的performClick()方法,那我们进入到这个方法里瞧一瞧:
public boolean performClick() {
02. sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
03. if (mOnClickListener != null) {
04. playSoundEffect(SoundEffectConstants.CLICK);
05. mOnClickListener.onClick(this);
06. return true;
07. }
08. return false;
09.}
可以看到,只要mOnClickListener不是null,就会去调用它的onClick方法,那mOnClickListener又是在哪里赋值的呢?经过寻找后找到如下方法:
public void setOnClickListener(OnClickListener l) {
02. if (!isClickable()) {
03. setClickable(true);
04. }
05. mOnClickListener = l;
06.}
一切都是那么清楚了!当我们通过调用setOnClickListener方法来给控件注册一个点击事件时,就会给mOnClickListener赋值。然后每当控件被点击时,都会在performClick()方法里回调被点击控件的onClick方法。这样View的整个事件分发的流程就让我们搞清楚了!不过别高兴的太早,现在还没结束,还有一个很重要的知识点需要说明,就是touch事件的层级传递。我们都知道如果给一个控件注册了touch事件,每次点击它的时候都会触发一系列的ACTION_DOWN,ACTION_MOVE,ACTION_UP等事件。这里需要注意,如果你在执行ACTION_DOWN的时候返回了false,后面一系列其它的action就不会再得到执行了。简单的说,就是当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发后一个action。

说到这里,很多的朋友肯定要有巨大的疑问了。这不是在自相矛盾吗?前面的例子中,明明在onTouch事件里面返回了false,ACTION_DOWN和ACTION_UP不是都得到执行了吗?其实你只是被假象所迷惑了,让我们仔细分析一下,在前面的例子当中,我们到底返回的是什么。

参考着我们前面分析的源码,首先在onTouch事件里返回了false,就一定会进入到onTouchEvent方法中,然后我们来看一下onTouchEvent方法的细节。由于我们点击了按钮,就会进入到第14行这个if判断的内部,然后你会发现,不管当前的action是什么,最终都一定会走到第89行,返回一个true。

是不是有一种被欺骗的感觉?明明在onTouch事件里返回了false,系统还是在onTouchEvent方法中帮你返回了true。就因为这个原因,才使得前面的例子中ACTION_UP可以得到执行。

那我们可以换一个控件,将按钮替换成ImageView,然后给它也注册一个touch事件,并返回false。如下所示:
imageView.setOnTouchListener(new OnTouchListener() {
02. @Override
03. public boolean onTouch(View v, MotionEvent event) {
04. Log.d("TAG", "onTouch execute, action " + event.getAction());
05. return false;
06. }
07.});
运行一下程序,点击ImageView,你会发现结果如下:





ACTION_DOWN执行完后,后面的一系列action都不会得到执行了。这又是为什么呢?因为ImageView和按钮不同,它是默认不可点击的,因此在onTouchEvent的第14行判断时无法进入到if的内部,直接跳到第91行返回了false,也就导致后面其它的action都无法执行了。

好了,关于View的事件分发,我想讲的东西全都在这里了。现在我们再来回顾一下开篇时提到的那三个问题,相信每个人都会有更深一层的理解。

1. onTouch和onTouchEvent有什么区别,又该如何使用?

从源码中可以看出,这两个方法都是在View的dispatchTouchEvent中调用的,onTouch优先于onTouchEvent执行。如果在onTouch方法中通过返回true将事件消费掉,onTouchEvent将不会再执行。

另外需要注意的是,onTouch能够得到执行需要两个前提条件,第一mOnTouchListener的值不能为空,第二当前点击的控件必须是enable的。因此如果你有一个控件是非enable的,那么给它注册onTouch事件将永远得不到执行。对于这一类控件,如果我们想要监听它的touch事件,就必须通过在该控件中重写onTouchEvent方法来实现。

2. 为什么给ListView引入了一个滑动菜单的功能,ListView就不能滚动了?

如果你阅读了Android滑动框架完全解析,教你如何一分钟实现滑动菜单特效 这篇文章,你应该会知道滑动菜单的功能是通过给ListView注册了一个touch事件来实现的。如果你在onTouch方法里处理完了滑动逻辑后返回true,那么ListView本身的滚动事件就被屏蔽了,自然也就无法滑动(原理同前面例子中按钮不能点击),因此解决办法就是在onTouch方法里返回false。

3. 为什么图片轮播器里的图片使用Button而不用ImageView?

提这个问题的朋友是看过了Android实现图片滚动控件,含页签功能,让你的应用像淘宝一样炫起来 这篇文章。当时我在图片轮播器里使用Button,主要就是因为Button是可点击的,而ImageView是不可点击的。如果想要使用ImageView,可以有两种改法。第一,在ImageView的onTouch方法里返回true,这样可以保证ACTION_DOWN之后的其它action都能得到执行,才能实现图片滚动的效果。第二,在布局文件里面给ImageView增加一个android:clickable="true"的属性,这样ImageView变成可点击的之后,即使在onTouch里返回了false,ACTION_DOWN之后的其它action也是可以得到执行的。

今天的讲解就到这里了,相信大家现在对Android事件分发机制又有了进一步的认识,在后面的文章中我会再带大家一起探究Android中ViewGroup的事件分发机制,感兴趣的朋友可以继续关注~
 
作者:郭霖 查看全部
其实我一直准备写一篇关于Android事件分发机制的文章,从我的第一篇博客开始,就零零散散在好多地方使用到了Android事件分发的知识。也有好多朋友问过我各种问题,比如:onTouch和onTouchEvent有什么区别,又该如何使用?为什么给ListView引入了一个滑动菜单的功能,ListView就不能滚动了?为什么图片轮播器里的图片使用Button而不用ImageView?等等……对于这些问题,我并没有给出非常详细的回答,因为我知道如果想要彻底搞明白这些问题,掌握Android事件分发机制是必不可少的,而Android事件分发机制绝对不是三言两语就能说得清的。

在我经过较长时间的筹备之后,终于决定开始写这样一篇文章了。目前虽然网上相关的文章也不少,但我觉得没有哪篇写得特别详细的(也许我还没有找到),多数文章只是讲了讲理论,然后配合demo运行了一下结果。而我准备带着大家从源码的角度进行分析,相信大家可以更加深刻地理解Android事件分发机制。


阅读源码讲究由浅入深,循序渐进,因此我们也从简单的开始,本篇先带大家探究View的事件分发,下篇再去探究难度更高的ViewGroup的事件分发。


那我们现在就开始吧!比如说你当前有一个非常简单的项目,只有一个Activity,并且Activity中只有一个按钮。你可能已经知道,如果想要给这个按钮注册一个点击事件,只需要调用:
 
button.setOnClickListener(new OnClickListener() {  
02. @Override
03. public void onClick(View v) {
04. Log.d("TAG", "onClick execute");
05. }
06.});
这样在onClick方法里面写实现,就可以在按钮被点击的时候执行。你可能也已经知道,如果想给这个按钮再添加一个touch事件,只需要调用:
button.setOnTouchListener(new OnTouchListener() {  
02. @Override
03. public boolean onTouch(View v, MotionEvent event) {
04. Log.d("TAG", "onTouch execute, action " + event.getAction());
05. return false;
06. }
07.});
onTouch方法里能做的事情比onClick要多一些,比如判断手指按下、抬起、移动等事件。那么如果我两个事件都注册了,哪一个会先执行呢?我们来试一下就知道了,运行程序点击按钮,打印结果如下:

1.jpg

可以看到,onTouch是优先于onClick执行的,并且onTouch执行了两次,一次是ACTION_DOWN,一次是ACTION_UP(你还可能会有多次ACTION_MOVE的执行,如果你手抖了一下)。因此事件传递的顺序是先经过onTouch,再传递到onClick。

细心的朋友应该可以注意到,onTouch方法是有返回值的,这里我们返回的是false,如果我们尝试把onTouch方法里的返回值改成true,再运行一次,结果如下:

2.png

我们发现,onClick方法不再执行了!为什么会这样呢?你可以先理解成onTouch方法返回true就认为这个事件被onTouch消费掉了,因而不会再继续向下传递。

如果到现在为止,以上的所有知识点你都是清楚的,那么说明你对Android事件传递的基本用法应该是掌握了。不过别满足于现状,让我们从源码的角度分析一下,出现上述现象的原理是什么。

首先你需要知道一点,只要你触摸到了任何一个控件,就一定会调用该控件的dispatchTouchEvent方法。那当我们去点击按钮的时候,就会去调用Button类里的dispatchTouchEvent方法,可是你会发现Button类里并没有这个方法,那么就到它的父类TextView里去找一找,你会发现TextView里也没有这个方法,那没办法了,只好继续在TextView的父类View里找一找,这个时候你终于在View里找到了这个方法,示意图如下:

3.png

然后我们来看一下View中dispatchTouchEvent方法的源码:
public boolean dispatchTouchEvent(MotionEvent event) {  
02. if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
03. mOnTouchListener.onTouch(this, event)) {
04. return true;
05. }
06. return onTouchEvent(event);
07.}
这个方法非常的简洁,只有短短几行代码!我们可以看到,在这个方法内,首先是进行了一个判断,如果mOnTouchListener != null,(mViewFlags & ENABLED_MASK) == ENABLED和mOnTouchListener.onTouch(this, event)这三个条件都为真,就返回true,否则就去执行onTouchEvent(event)方法并返回。

先看一下第一个条件,mOnTouchListener这个变量是在哪里赋值的呢?我们寻找之后在View里发现了如下方法:
public void setOnTouchListener(OnTouchListener l) {  
02. mOnTouchListener = l;
03.}
Bingo!找到了,mOnTouchListener正是在setOnTouchListener方法里赋值的,也就是说只要我们给控件注册了touch事件,mOnTouchListener就一定被赋值了。第二个条件(mViewFlags & ENABLED_MASK) == ENABLED是判断当前点击的控件是否是enable的,按钮默认都是enable的,因此这个条件恒定为true。

第三个条件就比较关键了,mOnTouchListener.onTouch(this, event),其实也就是去回调控件注册touch事件时的onTouch方法。也就是说如果我们在onTouch方法里返回true,就会让这三个条件全部成立,从而整个方法直接返回true。如果我们在onTouch方法里返回false,就会再去执行onTouchEvent(event)方法。

现在我们可以结合前面的例子来分析一下了,首先在dispatchTouchEvent中最先执行的就是onTouch方法,因此onTouch肯定是要优先于onClick执行的,也是印证了刚刚的打印结果。而如果在onTouch方法里返回了true,就会让dispatchTouchEvent方法直接返回true,不会再继续往下执行。而打印结果也证实了如果onTouch返回true,onClick就不会再执行了。

根据以上源码的分析,从原理上解释了我们前面例子的运行结果。而上面的分析还透漏出了一个重要的信息,那就是onClick的调用肯定是在onTouchEvent(event)方法中的!那我们马上来看下onTouchEvent的源码,如下所示:
public boolean onTouchEvent(MotionEvent event) {  
02. final int viewFlags = mViewFlags;
03. if ((viewFlags & ENABLED_MASK) == DISABLED) {
04. // A disabled view that is clickable still consumes the touch
05. // events, it just doesn't respond to them.
06. return (((viewFlags & CLICKABLE) == CLICKABLE ||
07. (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
08. }
09. if (mTouchDelegate != null) {
10. if (mTouchDelegate.onTouchEvent(event)) {
11. return true;
12. }
13. }
14. if (((viewFlags & CLICKABLE) == CLICKABLE ||
15. (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
16. switch (event.getAction()) {
17. case MotionEvent.ACTION_UP:
18. boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
19. if ((mPrivateFlags & PRESSED) != 0 || prepressed) {
20. // take focus if we don't have it already and we should in
21. // touch mode.
22. boolean focusTaken = false;
23. if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
24. focusTaken = requestFocus();
25. }
26. if (!mHasPerformedLongPress) {
27. // This is a tap, so remove the longpress check
28. removeLongPressCallback();
29. // Only perform take click actions if we were in the pressed state
30. if (!focusTaken) {
31. // Use a Runnable and post this rather than calling
32. // performClick directly. This lets other visual state
33. // of the view update before click actions start.
34. if (mPerformClick == null) {
35. mPerformClick = new PerformClick();
36. }
37. if (!post(mPerformClick)) {
38. performClick();
39. }
40. }
41. }
42. if (mUnsetPressedState == null) {
43. mUnsetPressedState = new UnsetPressedState();
44. }
45. if (prepressed) {
46. mPrivateFlags |= PRESSED;
47. refreshDrawableState();
48. postDelayed(mUnsetPressedState,
49. ViewConfiguration.getPressedStateDuration());
50. } else if (!post(mUnsetPressedState)) {
51. // If the post failed, unpress right now
52. mUnsetPressedState.run();
53. }
54. removeTapCallback();
55. }
56. break;
57. case MotionEvent.ACTION_DOWN:
58. if (mPendingCheckForTap == null) {
59. mPendingCheckForTap = new CheckForTap();
60. }
61. mPrivateFlags |= PREPRESSED;
62. mHasPerformedLongPress = false;
63. postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
64. break;
65. case MotionEvent.ACTION_CANCEL:
66. mPrivateFlags &= ~PRESSED;
67. refreshDrawableState();
68. removeTapCallback();
69. break;
70. case MotionEvent.ACTION_MOVE:
71. final int x = (int) event.getX();
72. final int y = (int) event.getY();
73. // Be lenient about moving outside of buttons
74. int slop = mTouchSlop;
75. if ((x < 0 - slop) || (x >= getWidth() + slop) ||
76. (y < 0 - slop) || (y >= getHeight() + slop)) {
77. // Outside button
78. removeTapCallback();
79. if ((mPrivateFlags & PRESSED) != 0) {
80. // Remove any future long press/tap checks
81. removeLongPressCallback();
82. // Need to switch from pressed to not pressed
83. mPrivateFlags &= ~PRESSED;
84. refreshDrawableState();
85. }
86. }
87. break;
88. }
89. return true;
90. }
91. return false;
92.}
相较于刚才的dispatchTouchEvent方法,onTouchEvent方法复杂了很多,不过没关系,我们只挑重点看就可以了。首先在第14行我们可以看出,如果该控件是可以点击的就会进入到第16行的switch判断中去,而如果当前的事件是抬起手指,则会进入到MotionEvent.ACTION_UP这个case当中。在经过种种判断之后,会执行到第38行的performClick()方法,那我们进入到这个方法里瞧一瞧:
public boolean performClick() {  
02. sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
03. if (mOnClickListener != null) {
04. playSoundEffect(SoundEffectConstants.CLICK);
05. mOnClickListener.onClick(this);
06. return true;
07. }
08. return false;
09.}
可以看到,只要mOnClickListener不是null,就会去调用它的onClick方法,那mOnClickListener又是在哪里赋值的呢?经过寻找后找到如下方法:
public void setOnClickListener(OnClickListener l) {  
02. if (!isClickable()) {
03. setClickable(true);
04. }
05. mOnClickListener = l;
06.}
一切都是那么清楚了!当我们通过调用setOnClickListener方法来给控件注册一个点击事件时,就会给mOnClickListener赋值。然后每当控件被点击时,都会在performClick()方法里回调被点击控件的onClick方法。这样View的整个事件分发的流程就让我们搞清楚了!不过别高兴的太早,现在还没结束,还有一个很重要的知识点需要说明,就是touch事件的层级传递。我们都知道如果给一个控件注册了touch事件,每次点击它的时候都会触发一系列的ACTION_DOWN,ACTION_MOVE,ACTION_UP等事件。这里需要注意,如果你在执行ACTION_DOWN的时候返回了false,后面一系列其它的action就不会再得到执行了。简单的说,就是当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发后一个action。

说到这里,很多的朋友肯定要有巨大的疑问了。这不是在自相矛盾吗?前面的例子中,明明在onTouch事件里面返回了false,ACTION_DOWN和ACTION_UP不是都得到执行了吗?其实你只是被假象所迷惑了,让我们仔细分析一下,在前面的例子当中,我们到底返回的是什么。

参考着我们前面分析的源码,首先在onTouch事件里返回了false,就一定会进入到onTouchEvent方法中,然后我们来看一下onTouchEvent方法的细节。由于我们点击了按钮,就会进入到第14行这个if判断的内部,然后你会发现,不管当前的action是什么,最终都一定会走到第89行,返回一个true。

是不是有一种被欺骗的感觉?明明在onTouch事件里返回了false,系统还是在onTouchEvent方法中帮你返回了true。就因为这个原因,才使得前面的例子中ACTION_UP可以得到执行。

那我们可以换一个控件,将按钮替换成ImageView,然后给它也注册一个touch事件,并返回false。如下所示:
imageView.setOnTouchListener(new OnTouchListener() {  
02. @Override
03. public boolean onTouch(View v, MotionEvent event) {
04. Log.d("TAG", "onTouch execute, action " + event.getAction());
05. return false;
06. }
07.});
运行一下程序,点击ImageView,你会发现结果如下:

6.jpg

ACTION_DOWN执行完后,后面的一系列action都不会得到执行了。这又是为什么呢?因为ImageView和按钮不同,它是默认不可点击的,因此在onTouchEvent的第14行判断时无法进入到if的内部,直接跳到第91行返回了false,也就导致后面其它的action都无法执行了。

好了,关于View的事件分发,我想讲的东西全都在这里了。现在我们再来回顾一下开篇时提到的那三个问题,相信每个人都会有更深一层的理解。

1. onTouch和onTouchEvent有什么区别,又该如何使用?

从源码中可以看出,这两个方法都是在View的dispatchTouchEvent中调用的,onTouch优先于onTouchEvent执行。如果在onTouch方法中通过返回true将事件消费掉,onTouchEvent将不会再执行。

另外需要注意的是,onTouch能够得到执行需要两个前提条件,第一mOnTouchListener的值不能为空,第二当前点击的控件必须是enable的。因此如果你有一个控件是非enable的,那么给它注册onTouch事件将永远得不到执行。对于这一类控件,如果我们想要监听它的touch事件,就必须通过在该控件中重写onTouchEvent方法来实现。

2. 为什么给ListView引入了一个滑动菜单的功能,ListView就不能滚动了?

如果你阅读了Android滑动框架完全解析,教你如何一分钟实现滑动菜单特效 这篇文章,你应该会知道滑动菜单的功能是通过给ListView注册了一个touch事件来实现的。如果你在onTouch方法里处理完了滑动逻辑后返回true,那么ListView本身的滚动事件就被屏蔽了,自然也就无法滑动(原理同前面例子中按钮不能点击),因此解决办法就是在onTouch方法里返回false。

3. 为什么图片轮播器里的图片使用Button而不用ImageView?

提这个问题的朋友是看过了Android实现图片滚动控件,含页签功能,让你的应用像淘宝一样炫起来 这篇文章。当时我在图片轮播器里使用Button,主要就是因为Button是可点击的,而ImageView是不可点击的。如果想要使用ImageView,可以有两种改法。第一,在ImageView的onTouch方法里返回true,这样可以保证ACTION_DOWN之后的其它action都能得到执行,才能实现图片滚动的效果。第二,在布局文件里面给ImageView增加一个android:clickable="true"的属性,这样ImageView变成可点击的之后,即使在onTouch里返回了false,ACTION_DOWN之后的其它action也是可以得到执行的。

今天的讲解就到这里了,相信大家现在对Android事件分发机制又有了进一步的认识,在后面的文章中我会再带大家一起探究Android中ViewGroup的事件分发机制,感兴趣的朋友可以继续关注~
 
作者:郭霖
2
回复

环信demo 环信 环信技术支持

环信技术支持中心 回复了问题 • 2 人关注 • 2054 次浏览 • 2015-07-03 11:15 • 来自相关话题

2
回复

使用REST API测试服务器端上线,发现一直是offline不知道为啥? 环信 环信技术支持

jindez 回复了问题 • 2 人关注 • 1807 次浏览 • 2015-07-03 09:20 • 来自相关话题

2
回复

环信后台明明有群组,调用getAllPublicGroupsFromServer却没有信息回来 环信 环信技术支持

zhangnan 回复了问题 • 3 人关注 • 1248 次浏览 • 2015-07-02 18:51 • 来自相关话题

1
回复

后台群组中得“对群主禁用”是什么意思? 环信

beyond 回复了问题 • 2 人关注 • 2191 次浏览 • 2015-07-02 17:37 • 来自相关话题

0
评论

手把手教你如何从从零开始构建JavaScript模块化加载器 移动开发

oscar1212 发表了文章 • 1055 次浏览 • 2015-07-02 17:20 • 来自相关话题

对任何程序,都存在一个规模的问题,起初我们使用函数来组织不同的模块,但是随着应用规模的不断变大,简单的重构函数并不能顺利的解决问题。尤其对JavaScript程序而言,模块化有助于解决我们在前端开发中面临的越来越复杂的需求。
 
为什么需要模块化
 
对开发者而言,有很多理由去将程序拆分为小的代码块。这种模块拆分的过程有助于开发者更清晰的阅读和编写代码,并且能够让编程的过程更多的集中在模块的功能实现上,和算法一样,分而治之的思想有助于提高编程生产率。
在下文中,我们将集中讨论JavaScript的模块化开发,并实现一个简单的module loader。
 实现模块化

使用函数作为命名空间

在JavaScript中,函数是唯一的可以用来创建新的作用域的途径。考虑到一个最简单的需求,我们通过数字来获得星期值,例如通过数字0得到星期日,通过数字1得到星期一。我们可以编写如下的程序:
var names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];

function dayName(number) {
return names[number];
}

console.log(dayName(1));上面的程序,我们创建了一个函数dayName()来获取星期值。但问题是,names变量被暴露在全局作用域中。更多的时候,我们希望能够构造私有变量,而暴露公共函数作为接口。

对JavaScript中的函数而言,我们可以通过创建立即调用的函数表达式来达到这个效果,我们可以通过如下的方式重构上面的代码,使得私有作用域成为可能:
var dayName = function() {
var names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];

return {
name: function(number) {
return names[number];
},

number: function(name) {
return names.indexOf(name);
}
};
}();

console.log(dayName.name(3));
console.log(dayName.number("Sunday"));上面的程序中,我们通过将变量包括在一个函数中,这个函数会立即执行,并返回一个包含两个属性的对象,返回的对象会被赋值给dayName变量。在后面,我们可以通过dayName变量来访问暴露的两个函数接口name和number。

对代码进一步改进,我们可以利用一个exports对象来达到暴露公共接口的目的,这种方法可以通过如下方法实现,代码如下:
var weekDay = {};

(function(exports) {
var names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];

exports.name = function(number) {
return names[number];
};

exports.number = function(name) {
return names.indexOf(name);
};

})(weekDay); // outside of a function, this refers to the global scope object

console.log(weekDay.name(weekDay.number("Saturday")));上面的这种模块构造方式在以浏览器为核心的前端编码中非常常见,通过暴露一个全局变量的方式来将代码包裹在私有的函数作用域中。但这种方法依然会存在问题,在复杂应用中,你依然无法避免同名变量。

从全局作用域中分离,实现require方法

更进一步的,为了实现模块化,我们可以通过构造一个系统,使得一个函数可以require另一个函数的方式来实现模块化编程。所以我们的目标是,实现一个require方法,通过传入模块名来取得该模块的调用。这种实现方式要比前面的方法更为优雅的体现模块化的理念。对require方法而言,我们需要完成两件事。

我们需要实现一个readFile方法,它能通过给定字符串返回文件的内容。
我们需要能够将返回的字符串作为代码进行执行。

我们假设已经存在了readFile这个方法,我们更加关注的是如何能够将字符串作为可执行的程序代码。通常我们有好几种方法来实现这个需求,最常见的方法是eval操作符,但我们常常在刚学习JavaScript的时候被告知,使用eval是一个非常不明智的决策,因为使用它会导致潜在的安全问题,因此我们放弃这个方法。

一个更好的方法是使用Function构造器,它需要两个参数:使用逗号分隔的参数列表字符串,和函数体字符串。例如:
var plusOne = new Function("n", "return n+1");
console.log(plusOne(5)); // 6下面我们可以来实现require方法了:
// module.js
function require(name) {

// 调用一个模块,首先检查这个模块是否已被调用
if(name in require.cache) {
return require.cache[name];
}

var code = new Function("exports, module", readFile(name));
var exports = {},
module = {exports: exports};
code(exports, module);

require.cache[name] = module.exports;
return module.exports;
}

// 缓存对象,为了应对重复调用的问题
require.cache = Object.create(null);

// todo:
function readFile(fileName) { ... }在页面中使用require函数:
 <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>demo</title>
<script src="module.js"></script>
</head>
<body>
<script>
var weekDay = require("weekDay");
var today = require("today");

console.log(weekDay.name(today.dayNumber()));
</script>
</body>
</html>通过这种方式实现的模块化系统通常被称为是CommonJS模块风格的,Node.js正式使用了这种风格的模块化系统。这里只是提供了一个最简单的实现方法,在真实应用中会有更加精致的实现方法。

慢载入模块和AMD

对浏览器编程而言,通常不会使用CommonJS风格的模块化系统,因为对于Web而言,加载一个资源远没有在服务端来的快,这收到网络性能的影响,尤其一个模块如果过大的话,可能会中断方法的执行。Browserify是解决这个问题的一个比较出名的模块化方案。

这个过程大概是这样的:首先检查模块中是否存在require其他模块的语句,如果有,就解析所有相关的模块,然后组装为一个大模块。网站本身为简单的加载这个组装后的大模块。

模块化的另一个解决方案是使用AMD,即异步模块定义,AMD允许通过异步的方式加载模块内容,这种就不会阻塞代码的执行。

我们想要实现的功能大概是这个样子的:
// index.html 中的部分代码
define(["weekDay.js", "today.js"], function (weekDay, today) {
console.log(weekDay.name(today.dayNumber()));
document.write(weekDay.name(today.dayNumber()));
});问题的核心是实现define方法,它的第一个参数是定义该模块说需要的依赖列表,参数而是该模块的具体工作函数。一旦所依赖的模块都被加载后,define便会执行参数2所定义的工作函数。weekDay模块的内容大概是下面的内容:
// weekDay.js
define([], function() {
var names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];

return {
name: function(number) { return names[number]},
number: function(name) { return names.indexOf(name)}
}
});下面我们来关注如何实现define()方法。为了实现这个方法,我们需要定义一个backgroundReadFile()方法来异步的获取文件内容。此外我们需要能够监视模块的加载状态,当模块加载完后能够告诉函数去执行具体的工作函数(回调)。
// 通过Ajax来异步加载模块
function backgroundReadFile(url, callback) {
var req = new XMLHttpRequest();
req.open("GET", url, true);
req.addEventListener("load", function () {
if (req.status < 400)
callback(req.responseText);
});
req.send(null);
}通过实现一个getModule函数,通过给定的模块名进行模块的调度运行工作。同样,我们需要通过缓存的方式避免同一个模块被重复的载入。实现代码如下:
// module.js 的部分内容
var defineCache = Object.create(null);
var currentMod = null;

function getModule(name) {
if (name in defineCache) {
return defineCache[name];
}

var module = {
exports: null,
loaded: false,
onLoad: []
};

defineCache[name] = module;
backgroundReadFile(name, function(code) {
currentMod = module;
new Function("", code)();
});
return module;
}有了getModule()了函数之后,define方法可以借助该方法来为当前模块的依赖列表获取或创建模块对象。define方法的简单实现如下:
// module.js 的部分内容
function define(depNames, moduleFunction) {
var myMod = currentMod;
var deps = depNames.map(getModule);

deps.forEach(function(mod) {
if(!mod.loaded) {
mod.onLoad.push(whenDepsLoaded);
}
});

// 用于检查是否所有的依赖模块都被成功加载了
function whenDepsLoaded() {
if(!deps.every(function(m) { return m.loaded; })) {
return;
}

var args = deps.map(function(m) { return m.exports; });

var exports = moduleFunction.apply(null, args);
if (myMod) {
myMod.exports = exports;
myMod.loaded = true;
myMod.onLoad.forEach(function(f) { f(); });
}
}

whenDepsLoaded();
}关于AMD的更加常见的实现是RequireJS,它提供了AMD风格的更加流行的实现方式。

小结

模块通过将代码分离为不同的文件和命名空间,为大型程序提供了清晰的结构。通过构建良好的接口可以使得开发者更加建议的阅读、使用、扩展代码。尤其对JavaScript语言而言,由于天生的缺陷,使得模块化更加有助于程序的组织。在JavaScript的世界,有两种流行的模块化实现方式,一种称为CommonJS,通常是服务端的模块化实现方案,另一种称为AMD,通常针对浏览器环境。其他关于模块化的知识,你可以参考这篇文章。

References

Eloquent JavaScript, chapter 10, Modules
Browserify运行原理分析
Why AMD?
JavaScript模块化知识点小结 查看全部
对任何程序,都存在一个规模的问题,起初我们使用函数来组织不同的模块,但是随着应用规模的不断变大,简单的重构函数并不能顺利的解决问题。尤其对JavaScript程序而言,模块化有助于解决我们在前端开发中面临的越来越复杂的需求。
 
为什么需要模块化
 
对开发者而言,有很多理由去将程序拆分为小的代码块。这种模块拆分的过程有助于开发者更清晰的阅读和编写代码,并且能够让编程的过程更多的集中在模块的功能实现上,和算法一样,分而治之的思想有助于提高编程生产率。
在下文中,我们将集中讨论JavaScript的模块化开发,并实现一个简单的module loader。
 实现模块化

使用函数作为命名空间

在JavaScript中,函数是唯一的可以用来创建新的作用域的途径。考虑到一个最简单的需求,我们通过数字来获得星期值,例如通过数字0得到星期日,通过数字1得到星期一。我们可以编写如下的程序:
var names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];

function dayName(number) {
return names[number];
}

console.log(dayName(1));
上面的程序,我们创建了一个函数dayName()来获取星期值。但问题是,names变量被暴露在全局作用域中。更多的时候,我们希望能够构造私有变量,而暴露公共函数作为接口。

对JavaScript中的函数而言,我们可以通过创建立即调用的函数表达式来达到这个效果,我们可以通过如下的方式重构上面的代码,使得私有作用域成为可能:
var dayName = function() {
var names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];

return {
name: function(number) {
return names[number];
},

number: function(name) {
return names.indexOf(name);
}
};
}();

console.log(dayName.name(3));
console.log(dayName.number("Sunday"));
上面的程序中,我们通过将变量包括在一个函数中,这个函数会立即执行,并返回一个包含两个属性的对象,返回的对象会被赋值给dayName变量。在后面,我们可以通过dayName变量来访问暴露的两个函数接口name和number。

对代码进一步改进,我们可以利用一个exports对象来达到暴露公共接口的目的,这种方法可以通过如下方法实现,代码如下:
var weekDay = {};

(function(exports) {
var names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];

exports.name = function(number) {
return names[number];
};

exports.number = function(name) {
return names.indexOf(name);
};

})(weekDay); // outside of a function, this refers to the global scope object

console.log(weekDay.name(weekDay.number("Saturday")));
上面的这种模块构造方式在以浏览器为核心的前端编码中非常常见,通过暴露一个全局变量的方式来将代码包裹在私有的函数作用域中。但这种方法依然会存在问题,在复杂应用中,你依然无法避免同名变量。

从全局作用域中分离,实现require方法

更进一步的,为了实现模块化,我们可以通过构造一个系统,使得一个函数可以require另一个函数的方式来实现模块化编程。所以我们的目标是,实现一个require方法,通过传入模块名来取得该模块的调用。这种实现方式要比前面的方法更为优雅的体现模块化的理念。对require方法而言,我们需要完成两件事。

我们需要实现一个readFile方法,它能通过给定字符串返回文件的内容。
我们需要能够将返回的字符串作为代码进行执行。

我们假设已经存在了readFile这个方法,我们更加关注的是如何能够将字符串作为可执行的程序代码。通常我们有好几种方法来实现这个需求,最常见的方法是eval操作符,但我们常常在刚学习JavaScript的时候被告知,使用eval是一个非常不明智的决策,因为使用它会导致潜在的安全问题,因此我们放弃这个方法。

一个更好的方法是使用Function构造器,它需要两个参数:使用逗号分隔的参数列表字符串,和函数体字符串。例如:
var plusOne = new Function("n", "return n+1");
console.log(plusOne(5)); // 6
下面我们可以来实现require方法了:
// module.js
function require(name) {

// 调用一个模块,首先检查这个模块是否已被调用
if(name in require.cache) {
return require.cache[name];
}

var code = new Function("exports, module", readFile(name));
var exports = {},
module = {exports: exports};
code(exports, module);

require.cache[name] = module.exports;
return module.exports;
}

// 缓存对象,为了应对重复调用的问题
require.cache = Object.create(null);

// todo:
function readFile(fileName) { ... }
在页面中使用require函数:
 
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>demo</title>
<script src="module.js"></script>
</head>
<body>
<script>
var weekDay = require("weekDay");
var today = require("today");

console.log(weekDay.name(today.dayNumber()));
</script>
</body>
</html>
通过这种方式实现的模块化系统通常被称为是CommonJS模块风格的,Node.js正式使用了这种风格的模块化系统。这里只是提供了一个最简单的实现方法,在真实应用中会有更加精致的实现方法。

慢载入模块和AMD

对浏览器编程而言,通常不会使用CommonJS风格的模块化系统,因为对于Web而言,加载一个资源远没有在服务端来的快,这收到网络性能的影响,尤其一个模块如果过大的话,可能会中断方法的执行。Browserify是解决这个问题的一个比较出名的模块化方案。

这个过程大概是这样的:首先检查模块中是否存在require其他模块的语句,如果有,就解析所有相关的模块,然后组装为一个大模块。网站本身为简单的加载这个组装后的大模块。

模块化的另一个解决方案是使用AMD,即异步模块定义,AMD允许通过异步的方式加载模块内容,这种就不会阻塞代码的执行。

我们想要实现的功能大概是这个样子的:
// index.html 中的部分代码
define(["weekDay.js", "today.js"], function (weekDay, today) {
console.log(weekDay.name(today.dayNumber()));
document.write(weekDay.name(today.dayNumber()));
});
问题的核心是实现define方法,它的第一个参数是定义该模块说需要的依赖列表,参数而是该模块的具体工作函数。一旦所依赖的模块都被加载后,define便会执行参数2所定义的工作函数。weekDay模块的内容大概是下面的内容:
// weekDay.js
define([], function() {
var names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];

return {
name: function(number) { return names[number]},
number: function(name) { return names.indexOf(name)}
}
});
下面我们来关注如何实现define()方法。为了实现这个方法,我们需要定义一个backgroundReadFile()方法来异步的获取文件内容。此外我们需要能够监视模块的加载状态,当模块加载完后能够告诉函数去执行具体的工作函数(回调)。
// 通过Ajax来异步加载模块
function backgroundReadFile(url, callback) {
var req = new XMLHttpRequest();
req.open("GET", url, true);
req.addEventListener("load", function () {
if (req.status < 400)
callback(req.responseText);
});
req.send(null);
}
通过实现一个getModule函数,通过给定的模块名进行模块的调度运行工作。同样,我们需要通过缓存的方式避免同一个模块被重复的载入。实现代码如下:
// module.js 的部分内容
var defineCache = Object.create(null);
var currentMod = null;

function getModule(name) {
if (name in defineCache) {
return defineCache[name];
}

var module = {
exports: null,
loaded: false,
onLoad: []
};

defineCache[name] = module;
backgroundReadFile(name, function(code) {
currentMod = module;
new Function("", code)();
});
return module;
}
有了getModule()了函数之后,define方法可以借助该方法来为当前模块的依赖列表获取或创建模块对象。define方法的简单实现如下:
// module.js 的部分内容
function define(depNames, moduleFunction) {
var myMod = currentMod;
var deps = depNames.map(getModule);

deps.forEach(function(mod) {
if(!mod.loaded) {
mod.onLoad.push(whenDepsLoaded);
}
});

// 用于检查是否所有的依赖模块都被成功加载了
function whenDepsLoaded() {
if(!deps.every(function(m) { return m.loaded; })) {
return;
}

var args = deps.map(function(m) { return m.exports; });

var exports = moduleFunction.apply(null, args);
if (myMod) {
myMod.exports = exports;
myMod.loaded = true;
myMod.onLoad.forEach(function(f) { f(); });
}
}

whenDepsLoaded();
}
关于AMD的更加常见的实现是RequireJS,它提供了AMD风格的更加流行的实现方式。

小结

模块通过将代码分离为不同的文件和命名空间,为大型程序提供了清晰的结构。通过构建良好的接口可以使得开发者更加建议的阅读、使用、扩展代码。尤其对JavaScript语言而言,由于天生的缺陷,使得模块化更加有助于程序的组织。在JavaScript的世界,有两种流行的模块化实现方式,一种称为CommonJS,通常是服务端的模块化实现方案,另一种称为AMD,通常针对浏览器环境。其他关于模块化的知识,你可以参考这篇文章。

References

Eloquent JavaScript, chapter 10, Modules
Browserify运行原理分析
Why AMD?
JavaScript模块化知识点小结
5
评论

微信开放平台之公众号第三方平台开发及全网发布验证 微信 移动开发

admin 发表了文章 • 14006 次浏览 • 2015-07-02 16:55 • 来自相关话题

微信公众号第三方平台的开放,让公众号运营者在面向垂直行业需求时,可以通过一键登录授权给第三方开发者,来完成相关的处理能力,方便快捷,那如何才能开发出一个公众号第三方平台供一键授权呢?本文以JAVA作为后台服务的实现语言,实现了微信第三方开放平台开发所需要的主要业务流程,并针对全网发布的检测做了相应的代码处理,以通过微信全网检测,可以接入任意的微信公众号。
根据微信第三方平台的审核需求,你需要在微信开放平台上注册第三方平台信息时,提供如下几个主要的服务:











1、授权事件接收服务,对应填写的审核资料中授权事件接收URL,微信会将相关的授权事件信息推送到该REST服务上,推送的主要消息包括验证票据ComponentVerifyTicket和取消授权的公众号AuthorizerAppid,该服务需要对微信推送过来的该类消息立即做出回应并返回success内容,该服务事件的JAVA实现方式如下: /**
* 授权事件接收
*
* @param request
* @param response
* @throws IOException
* @throws AesException
* @throws DocumentException
*/
@RequestMapping(value = "/open/event/authorize", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.NO_CONTENT)
public void acceptAuthorizeEvent(HttpServletRequest request, HttpServletResponse response) throws IOException, AesException, DocumentException {
WeixinOpenService.getInstance().processAuthorizeEvent(request);
WeixinOpenService.getInstance().output(response, "success"); // 输出响应的内容。
}


更具体的实现代码如下: /**
* 处理授权事件的推送
*
* @param request
* @throws IOException
* @throws AesException
* @throws DocumentException
*/
public void processAuthorizeEvent(HttpServletRequest request) throws IOException, DocumentException, AesException {
String token = WeixinOpenService.TOKEN;
String nonce = request.getParameter("nonce");
String timestamp = request.getParameter("timestamp");
String signature = request.getParameter("signature");
String msgSignature = request.getParameter("msg_signature");

if (!StringUtils.isNotBlank(msgSignature))
return;// 微信推送给第三方开放平台的消息一定是加过密的,无消息加密无法解密消息
boolean isValid = WechatCallbackServiceController.checkSignature(token, signature, timestamp, nonce);
if (isValid) {
StringBuilder sb = new StringBuilder();
BufferedReader in = request.getReader();
String line;
while ((line = in.readLine()) != null) {
sb.append(line);
}
String xml = sb.toString();
String encodingAesKey = WeixinOpenService.ENCODINGAESKEY;// 第三方平台组件加密密钥
String appId = getAuthorizerAppidFromXml(xml, "authorizationEvent");// 此时加密的xml数据中ToUserName是非加密的,解析xml获取即可
WXBizMsgCrypt pc = new WXBizMsgCrypt(token, encodingAesKey, appId);
xml = pc.decryptMsg(msgSignature, timestamp, nonce, xml, "AppId");
processAuthorizationEvent(xml);
}
}



2、公众号消息与事件接收服务,对应填写的审核资料中公众号消息与事件接收URL,微信会将粉丝发送给公众号的消息和事件推送到该REST服务上,微信公众平台要求该消息和事件接收服务在5秒内做出回应,如果5秒内微信公众平台得不到响应消息,粉丝将将收到提示公众号暂时服务提供服务的错误信息。对于需要对粉丝发送的消息走人工渠道做出响应的公众号来说,此时就需要首先接收下消息,将消息交给后来逻辑转人工处理,然后立即以空格消息响应微信公众平台,微信收到空格消息后就会知道该粉丝发送的消息已经被妥善处理,并对该响应不做任何处理,同时不会发起消息重新推送的重试。该服务的JAVA实现实现方式如下: /**
* 处理微信推送过来的授权公众号的消息及事件
*
*/
public void processMessageAndEvent(HttpServletRequest request,String xml) throws IOException, AesException, DocumentException {
String nonce = request.getParameter("nonce");
String timestamp = request.getParameter("timestamp");
String msgSignature = request.getParameter("msg_signature");

String encodingAesKey = WeixinOpenService.ENCODINGAESKEY;
String token = WeixinOpenService.TOKEN;
WXBizMsgCrypt pc = new WXBizMsgCrypt(token, encodingAesKey, WeixinOpenService.COMPONENT_APPID);
xml = pc.decryptMsg(msgSignature, timestamp, nonce, xml, "ToUserName");
WechatCallbackServiceController.processMessage(xml);
}


以上是开发微信第三方开发平台的主要服务代码,想要通过微信全网接入检测并成功发布,还有如下的工作的需要做:
开发一个体验页,可以直接让审核人员体验,因为需要的是直接体验,所以访问该页面就不要有认证和权限控制之类的逻辑了,这个页面要求符合微信第三方平台基本的设计要求,本人简单实现了如下的页面格式是可以成功通过审核的,如下:





 
针对微信全网检测的固定账号做出特定的响应,主要包括一个文本消息响应,一个事件消息响应和一个客服接口调用验证,微信全网检测要求测试的固定账号接收到以上消息后,分别做出如下的响应:接收到TESTCOMPONENT_MSG_TYPE_TEXT这样的文本消息立即回复给粉丝文本内容TESTCOMPONENT_MSG_TYPE_TEXT_callback;接收到事件消息,立即以文本内容的消息格式回复粉丝内容event + “from_callback”,其中event需要根据实际内容替换为具体事件类型;接收到QUERY_AUTH_CODE:query_auth_code  这样的文本消息,需要立即响应空字符串给微信,之后调用客服接口回复粉丝文本消息,内容为:$query_auth_code\$_from_api,其中query_auth_code需要替换为微信实际推送过来的数据。主要的JAVA后台实现代码如下:
  /**
* 公众号消息与事件接收
*
* @param request
* @param response
* @throws DocumentException
* @throws AesException
* @throws IOException
*/
@RequestMapping(value = "/open/{appid}/callback", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.NO_CONTENT)
public void acceptMessageAndEvent(HttpServletRequest request, HttpServletResponse response) throws IOException, AesException, DocumentException {
String msgSignature = request.getParameter("msg_signature");
if (!StringUtils.isNotBlank(msgSignature))
return;// 微信推送给第三方开放平台的消息一定是加过密的,无消息加密无法解密消息

StringBuilder sb = new StringBuilder();
BufferedReader in = request.getReader();
String line;
while ((line = in.readLine()) != null) {
sb.append(line);
}
in.close();

String xml = sb.toString();
Document doc = DocumentHelper.parseText(xml);
Element rootElt = doc.getRootElement();
String toUserName = rootElt.elementText("ToUserName");

if (StringUtils.equalsIgnoreCase(toUserName, "gh_3c884a361561")) {// 微信全网测试账号
WeixinWholeNetworkTestService.getInstance().checkWeixinAllNetworkCheck(request,response,xml);
}else{
WeixinOpenService.getInstance().processMessageAndEvent(request,xml);
WeixinOpenService.getInstance().output(response, "");
}
}

 
其中gh_3c884a361561这个账号是微信全网接入检测的固定账号,针对全网检测需要对该账号做特出响应,一旦全网接入检测通过,这部分的代码是可以去掉的,只有全网检测的时候才需要这部分代码。
public void checkWeixinAllNetworkCheck(HttpServletRequest request, HttpServletResponse response,String xml) throws DocumentException, IOException, AesException{
String nonce = request.getParameter("nonce");
String timestamp = request.getParameter("timestamp");
String msgSignature = request.getParameter("msg_signature");

String encodingAesKey = WeixinOpenService.ENCODINGAESKEY;
String token = WeixinOpenService.TOKEN;
WXBizMsgCrypt pc = new WXBizMsgCrypt(token, encodingAesKey, WeixinOpenService.COMPONENT_APPID);
xml = pc.decryptMsg(msgSignature, timestamp, nonce, xml, "ToUserName");

Document doc = DocumentHelper.parseText(xml);
Element rootElt = doc.getRootElement();
String msgType = rootElt.elementText("MsgType");
String toUserName = rootElt.elementText("ToUserName");
String fromUserName = rootElt.elementText("FromUserName");

switch (msgType) {
case "event":
String event = rootElt.elementText("Event");
replyEventMessage(request,response,event,toUserName,fromUserName);
break;
case "text":
String content = rootElt.elementText("Content");
processTextMessage(request,response,content,toUserName,fromUserName);
break;
default:
break;
}
}


根据消息或事件类型区分后,剩余的逻辑只需要处理成对应的回复内容即可,如下:public void replyEventMessage(HttpServletRequest request, HttpServletResponse response, String event, String toUserName, String fromUserName) throws DocumentException, IOException {
String content = event + "from_callback";
replyTextMessage(request,response,content,toUserName,fromUserName);
}

public void processTextMessage(HttpServletRequest request, HttpServletResponse response,String content,String toUserName, String fromUserName) throws IOException, DocumentException{
if("TESTCOMPONENT_MSG_TYPE_TEXT".equals(content)){
String returnContent = content+"_callback";
replyTextMessage(request,response,returnContent,toUserName,fromUserName);
}else if(StringUtils.startsWithIgnoreCase(content, "QUERY_AUTH_CODE")){
WeixinOpenService.getInstance().output(response, "");
//接下来客服API再回复一次消息
replyApiTextMessage(request,response,content.split(":")[1],fromUserName);
}
}

public void replyApiTextMessage(HttpServletRequest request, HttpServletResponse response, String auth_code, String fromUserName) throws DocumentException, IOException {
String authorization_code = auth_code;
// 得到微信授权成功的消息后,应该立刻进行处理!!相关信息只会在首次授权的时候推送过来
WeixinOpenData weixinOpenData = WeixinOpenService.getInstance().getWeixinOpenData(WeixinOpenService.COMPONENT_APPID);
long accessTokenExpires = weixinOpenData.getAccessTokenExpires();
String componentAccessToken = weixinOpenData.getComponentAccessToken();
String componentVerifyTicket = weixinOpenData.getComponentVerifyTicket();
JSONObject authorizationInfoJson;
if (!this.isExpired(accessTokenExpires)) {
authorizationInfoJson = WeixinOpenService.getInstance().apiQueryAuth(componentAccessToken, WeixinOpenService.COMPONENT_APPID, authorization_code);
} else {
JSONObject accessTokenJson = WeixinOpenService.getInstance().getComponentAccessToken(WeixinOpenService.COMPONENT_APPID, WeixinOpenService.COMPONENT_APPSECRET, componentVerifyTicket);
componentAccessToken = accessTokenJson.getString("component_access_token");
authorizationInfoJson = WeixinOpenService.getInstance().apiQueryAuth(componentAccessToken, WeixinOpenService.COMPONENT_APPID, authorization_code);
}
if (log.isDebugEnabled()) {
log.debug("weixinopen callback authorizationInfo is " + authorizationInfoJson);
}

JSONObject infoJson = authorizationInfoJson.getJSONObject("authorization_info");
String authorizer_access_token = infoJson.getString("authorizer_access_token");

String url = "https://api.weixin.qq.com/cgi- ... ot%3B + authorizer_access_token;
DefaultHttpClient client = new DefaultHttpClient();
enableSSLDefaultHttpClient(client);
HttpPost httpPost = new HttpPost(url);

JSONObject message = processWechatTextMessage(client, httpPost, fromUserName, auth_code + "_from_api");
if(log.isDebugEnabled()){
log.debug("api reply messto to weixin whole network test respose = "+message);
}
}
public void replyTextMessage(HttpServletRequest request, HttpServletResponse response, String content, String toUserName, String fromUserName) throws DocumentException, IOException {
Long createTime = Calendar.getInstance().getTimeInMillis() / 1000;
StringBuffer sb = new StringBuffer();
sb.append("");
sb.append("");
sb.append("");
sb.append("" + createTime + "");
sb.append("");
sb.append("");
sb.append("");
String replyMsg = sb.toString();

String returnvaleue = "";
try {
WXBizMsgCrypt pc = new WXBizMsgCrypt(WeixinOpenService.TOKEN, WeixinOpenService.ENCODINGAESKEY, WeixinOpenService.COMPONENT_APPID);
returnvaleue = pc.encryptMsg(replyMsg, createTime.toString(), "easemob");
} catch (AesException e) {
log.error("auto reply to weixin whole network test occur exception = "+ e);
e.printStackTrace();
}
if(log.isDebugEnabled()){
log.debug("return weixin whole network test Text message is = "+returnvaleue);
}
WeixinOpenService.getInstance().output(response, returnvaleue);
}



以上是微信第三方开放平台开发主要的业务流程,在实际开发中,还有两点需要特别注意:
1、微信要求第三方开放平台必须以密文方式接收消息;
2、在实际部署时,需要更换JAVA安全包相关的内容,否则将出现秘钥长度不够的异常,需要替换的文件包括JAVA_HOME/jre/lib/security/local_policy.jar和  JAVA_HOME/jre/lib/security/US_export_policy.jar这两个文件。
作者:徐正礼 查看全部
微信公众号第三方平台的开放,让公众号运营者在面向垂直行业需求时,可以通过一键登录授权给第三方开发者,来完成相关的处理能力,方便快捷,那如何才能开发出一个公众号第三方平台供一键授权呢?本文以JAVA作为后台服务的实现语言,实现了微信第三方开放平台开发所需要的主要业务流程,并针对全网发布的检测做了相应的代码处理,以通过微信全网检测,可以接入任意的微信公众号。
根据微信第三方平台的审核需求,你需要在微信开放平台上注册第三方平台信息时,提供如下几个主要的服务:

41.png


42.png


1、授权事件接收服务,对应填写的审核资料中授权事件接收URL,微信会将相关的授权事件信息推送到该REST服务上,推送的主要消息包括验证票据ComponentVerifyTicket和取消授权的公众号AuthorizerAppid,该服务需要对微信推送过来的该类消息立即做出回应并返回success内容,该服务事件的JAVA实现方式如下:
  /**
* 授权事件接收
*
* @param request
* @param response
* @throws IOException
* @throws AesException
* @throws DocumentException
*/
@RequestMapping(value = "/open/event/authorize", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.NO_CONTENT)
public void acceptAuthorizeEvent(HttpServletRequest request, HttpServletResponse response) throws IOException, AesException, DocumentException {
WeixinOpenService.getInstance().processAuthorizeEvent(request);
WeixinOpenService.getInstance().output(response, "success"); // 输出响应的内容。
}


更具体的实现代码如下:
  /**
* 处理授权事件的推送
*
* @param request
* @throws IOException
* @throws AesException
* @throws DocumentException
*/
public void processAuthorizeEvent(HttpServletRequest request) throws IOException, DocumentException, AesException {
String token = WeixinOpenService.TOKEN;
String nonce = request.getParameter("nonce");
String timestamp = request.getParameter("timestamp");
String signature = request.getParameter("signature");
String msgSignature = request.getParameter("msg_signature");

if (!StringUtils.isNotBlank(msgSignature))
return;// 微信推送给第三方开放平台的消息一定是加过密的,无消息加密无法解密消息
boolean isValid = WechatCallbackServiceController.checkSignature(token, signature, timestamp, nonce);
if (isValid) {
StringBuilder sb = new StringBuilder();
BufferedReader in = request.getReader();
String line;
while ((line = in.readLine()) != null) {
sb.append(line);
}
String xml = sb.toString();
String encodingAesKey = WeixinOpenService.ENCODINGAESKEY;// 第三方平台组件加密密钥
String appId = getAuthorizerAppidFromXml(xml, "authorizationEvent");// 此时加密的xml数据中ToUserName是非加密的,解析xml获取即可
WXBizMsgCrypt pc = new WXBizMsgCrypt(token, encodingAesKey, appId);
xml = pc.decryptMsg(msgSignature, timestamp, nonce, xml, "AppId");
processAuthorizationEvent(xml);
}
}



2、公众号消息与事件接收服务,对应填写的审核资料中公众号消息与事件接收URL,微信会将粉丝发送给公众号的消息和事件推送到该REST服务上,微信公众平台要求该消息和事件接收服务在5秒内做出回应,如果5秒内微信公众平台得不到响应消息,粉丝将将收到提示公众号暂时服务提供服务的错误信息。对于需要对粉丝发送的消息走人工渠道做出响应的公众号来说,此时就需要首先接收下消息,将消息交给后来逻辑转人工处理,然后立即以空格消息响应微信公众平台,微信收到空格消息后就会知道该粉丝发送的消息已经被妥善处理,并对该响应不做任何处理,同时不会发起消息重新推送的重试。该服务的JAVA实现实现方式如下:
 /**
* 处理微信推送过来的授权公众号的消息及事件
*
*/
public void processMessageAndEvent(HttpServletRequest request,String xml) throws IOException, AesException, DocumentException {
String nonce = request.getParameter("nonce");
String timestamp = request.getParameter("timestamp");
String msgSignature = request.getParameter("msg_signature");

String encodingAesKey = WeixinOpenService.ENCODINGAESKEY;
String token = WeixinOpenService.TOKEN;
WXBizMsgCrypt pc = new WXBizMsgCrypt(token, encodingAesKey, WeixinOpenService.COMPONENT_APPID);
xml = pc.decryptMsg(msgSignature, timestamp, nonce, xml, "ToUserName");
WechatCallbackServiceController.processMessage(xml);
}


以上是开发微信第三方开发平台的主要服务代码,想要通过微信全网接入检测并成功发布,还有如下的工作的需要做:
  • 开发一个体验页,可以直接让审核人员体验,因为需要的是直接体验,所以访问该页面就不要有认证和权限控制之类的逻辑了,这个页面要求符合微信第三方平台基本的设计要求,本人简单实现了如下的页面格式是可以成功通过审核的,如下:


43.png

 
  • 针对微信全网检测的固定账号做出特定的响应,主要包括一个文本消息响应,一个事件消息响应和一个客服接口调用验证,微信全网检测要求测试的固定账号接收到以上消息后,分别做出如下的响应:接收到TESTCOMPONENT_MSG_TYPE_TEXT这样的文本消息立即回复给粉丝文本内容TESTCOMPONENT_MSG_TYPE_TEXT_callback;接收到事件消息,立即以文本内容的消息格式回复粉丝内容event + “from_callback”,其中event需要根据实际内容替换为具体事件类型;接收到QUERY_AUTH_CODE:query_auth_code  这样的文本消息,需要立即响应空字符串给微信,之后调用客服接口回复粉丝文本消息,内容为:$query_auth_code\$_from_api,其中query_auth_code需要替换为微信实际推送过来的数据。主要的JAVA后台实现代码如下:

 
 /**
* 公众号消息与事件接收
*
* @param request
* @param response
* @throws DocumentException
* @throws AesException
* @throws IOException
*/
@RequestMapping(value = "/open/{appid}/callback", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.NO_CONTENT)
public void acceptMessageAndEvent(HttpServletRequest request, HttpServletResponse response) throws IOException, AesException, DocumentException {
String msgSignature = request.getParameter("msg_signature");
if (!StringUtils.isNotBlank(msgSignature))
return;// 微信推送给第三方开放平台的消息一定是加过密的,无消息加密无法解密消息

StringBuilder sb = new StringBuilder();
BufferedReader in = request.getReader();
String line;
while ((line = in.readLine()) != null) {
sb.append(line);
}
in.close();

String xml = sb.toString();
Document doc = DocumentHelper.parseText(xml);
Element rootElt = doc.getRootElement();
String toUserName = rootElt.elementText("ToUserName");

if (StringUtils.equalsIgnoreCase(toUserName, "gh_3c884a361561")) {// 微信全网测试账号
WeixinWholeNetworkTestService.getInstance().checkWeixinAllNetworkCheck(request,response,xml);
}else{
WeixinOpenService.getInstance().processMessageAndEvent(request,xml);
WeixinOpenService.getInstance().output(response, "");
}
}

 
  • 其中gh_3c884a361561这个账号是微信全网接入检测的固定账号,针对全网检测需要对该账号做特出响应,一旦全网接入检测通过,这部分的代码是可以去掉的,只有全网检测的时候才需要这部分代码。

 public void checkWeixinAllNetworkCheck(HttpServletRequest request, HttpServletResponse response,String xml) throws DocumentException, IOException, AesException{
String nonce = request.getParameter("nonce");
String timestamp = request.getParameter("timestamp");
String msgSignature = request.getParameter("msg_signature");

String encodingAesKey = WeixinOpenService.ENCODINGAESKEY;
String token = WeixinOpenService.TOKEN;
WXBizMsgCrypt pc = new WXBizMsgCrypt(token, encodingAesKey, WeixinOpenService.COMPONENT_APPID);
xml = pc.decryptMsg(msgSignature, timestamp, nonce, xml, "ToUserName");

Document doc = DocumentHelper.parseText(xml);
Element rootElt = doc.getRootElement();
String msgType = rootElt.elementText("MsgType");
String toUserName = rootElt.elementText("ToUserName");
String fromUserName = rootElt.elementText("FromUserName");

switch (msgType) {
case "event":
String event = rootElt.elementText("Event");
replyEventMessage(request,response,event,toUserName,fromUserName);
break;
case "text":
String content = rootElt.elementText("Content");
processTextMessage(request,response,content,toUserName,fromUserName);
break;
default:
break;
}
}


根据消息或事件类型区分后,剩余的逻辑只需要处理成对应的回复内容即可,如下:
public void replyEventMessage(HttpServletRequest request, HttpServletResponse response, String event, String toUserName, String fromUserName) throws DocumentException, IOException {
String content = event + "from_callback";
replyTextMessage(request,response,content,toUserName,fromUserName);
}

public void processTextMessage(HttpServletRequest request, HttpServletResponse response,String content,String toUserName, String fromUserName) throws IOException, DocumentException{
if("TESTCOMPONENT_MSG_TYPE_TEXT".equals(content)){
String returnContent = content+"_callback";
replyTextMessage(request,response,returnContent,toUserName,fromUserName);
}else if(StringUtils.startsWithIgnoreCase(content, "QUERY_AUTH_CODE")){
WeixinOpenService.getInstance().output(response, "");
//接下来客服API再回复一次消息
replyApiTextMessage(request,response,content.split(":")[1],fromUserName);
}
}

public void replyApiTextMessage(HttpServletRequest request, HttpServletResponse response, String auth_code, String fromUserName) throws DocumentException, IOException {
String authorization_code = auth_code;
// 得到微信授权成功的消息后,应该立刻进行处理!!相关信息只会在首次授权的时候推送过来
WeixinOpenData weixinOpenData = WeixinOpenService.getInstance().getWeixinOpenData(WeixinOpenService.COMPONENT_APPID);
long accessTokenExpires = weixinOpenData.getAccessTokenExpires();
String componentAccessToken = weixinOpenData.getComponentAccessToken();
String componentVerifyTicket = weixinOpenData.getComponentVerifyTicket();
JSONObject authorizationInfoJson;
if (!this.isExpired(accessTokenExpires)) {
authorizationInfoJson = WeixinOpenService.getInstance().apiQueryAuth(componentAccessToken, WeixinOpenService.COMPONENT_APPID, authorization_code);
} else {
JSONObject accessTokenJson = WeixinOpenService.getInstance().getComponentAccessToken(WeixinOpenService.COMPONENT_APPID, WeixinOpenService.COMPONENT_APPSECRET, componentVerifyTicket);
componentAccessToken = accessTokenJson.getString("component_access_token");
authorizationInfoJson = WeixinOpenService.getInstance().apiQueryAuth(componentAccessToken, WeixinOpenService.COMPONENT_APPID, authorization_code);
}
if (log.isDebugEnabled()) {
log.debug("weixinopen callback authorizationInfo is " + authorizationInfoJson);
}

JSONObject infoJson = authorizationInfoJson.getJSONObject("authorization_info");
String authorizer_access_token = infoJson.getString("authorizer_access_token");

String url = "https://api.weixin.qq.com/cgi- ... ot%3B + authorizer_access_token;
DefaultHttpClient client = new DefaultHttpClient();
enableSSLDefaultHttpClient(client);
HttpPost httpPost = new HttpPost(url);

JSONObject message = processWechatTextMessage(client, httpPost, fromUserName, auth_code + "_from_api");
if(log.isDebugEnabled()){
log.debug("api reply messto to weixin whole network test respose = "+message);
}
}
public void replyTextMessage(HttpServletRequest request, HttpServletResponse response, String content, String toUserName, String fromUserName) throws DocumentException, IOException {
Long createTime = Calendar.getInstance().getTimeInMillis() / 1000;
StringBuffer sb = new StringBuffer();
sb.append("");
sb.append("");
sb.append("");
sb.append("" + createTime + "");
sb.append("");
sb.append("");
sb.append("");
String replyMsg = sb.toString();

String returnvaleue = "";
try {
WXBizMsgCrypt pc = new WXBizMsgCrypt(WeixinOpenService.TOKEN, WeixinOpenService.ENCODINGAESKEY, WeixinOpenService.COMPONENT_APPID);
returnvaleue = pc.encryptMsg(replyMsg, createTime.toString(), "easemob");
} catch (AesException e) {
log.error("auto reply to weixin whole network test occur exception = "+ e);
e.printStackTrace();
}
if(log.isDebugEnabled()){
log.debug("return weixin whole network test Text message is = "+returnvaleue);
}
WeixinOpenService.getInstance().output(response, returnvaleue);
}



以上是微信第三方开放平台开发主要的业务流程,在实际开发中,还有两点需要特别注意:
1、微信要求第三方开放平台必须以密文方式接收消息;
2、在实际部署时,需要更换JAVA安全包相关的内容,否则将出现秘钥长度不够的异常,需要替换的文件包括JAVA_HOME/jre/lib/security/local_policy.jar和  JAVA_HOME/jre/lib/security/US_export_policy.jar这两个文件。
作者:徐正礼
1
回复
2
回复

Android登录一直是-1005 错误 环信 环信_Android

lizg 回复了问题 • 2 人关注 • 2205 次浏览 • 2015-07-02 15:31 • 来自相关话题

1
回复

为什么对方删除我后,不会回调我的删除方法 环信

环信技术支持中心 回复了问题 • 2 人关注 • 1528 次浏览 • 2015-07-02 11:28 • 来自相关话题

2
回复

Android 突然登录不上 环信_Android

环信技术支持中心 回复了问题 • 4 人关注 • 739 次浏览 • 2015-07-02 11:13 • 来自相关话题

3
回复

错误统计报Invoked on incorrect queue,怎么修复? 环信技术支持

Half12345 回复了问题 • 2 人关注 • 1767 次浏览 • 2015-07-02 11:00 • 来自相关话题

1
回复

加入群聊失败,错误码405,信息:not-allowed 环信技术支持 环信

beyond 回复了问题 • 3 人关注 • 1397 次浏览 • 2015-07-02 10:10 • 来自相关话题

1
回复

MainViewController添加到tabbar上后,点击聊天无法跳转 环信_iOS

Half12345 回复了问题 • 2 人关注 • 667 次浏览 • 2015-07-02 10:03 • 来自相关话题

2
回复

申请好友后,为什么会自动申请通过 环信 环信_Android

环信技术支持中心 回复了问题 • 3 人关注 • 2420 次浏览 • 2015-07-01 20:03 • 来自相关话题

0
评论

详解:Android开发中常用的 DPI / DP / SP 移动开发 Android

oscar1212 发表了文章 • 1450 次浏览 • 2015-07-01 17:20 • 来自相关话题

Android的碎片化已经被喷了好多年,随着国内手机厂商的崛起,碎片化也越来越严重,根据OpenSignal的最新调查,2014年市面上有18796种不同的Android设备,作为开发者,一个无法回避的难题就是需要适配各种各样奇奇怪怪的机型。

设备机型不同必然也会导致屏幕大小和分辨率(Resolution)的不同,但是无论分辨率有多大,屏幕有多大,我们手指触控范围的大小不会发生变化,所以最优的适配方式应该是指定大小的控件在所有的设备上的显示都一样。

Android的官方文档对此也有明确的说明
 
When adding support for multiple screens, applications do not work directly with resolution; applications should be concerned only with screen size and density, as specified by the generalized size and density groups.
 
所以,适配应该与分辨率无关,只与屏幕大小和屏幕密度相关,首先来看一下什么是屏幕密度 - DPI。
 
DPI
 
DPI的全称是 Dots Per Inch,Inch是一个物理单位(无论在任何设备上,其大小都是固定的),所以DPI就指在一个Inch的物理长度内有多少个Dot,160DPI的屏幕就表示一个Inch包含160个Dot,320DPI的屏幕表示一个Inch有320个Dot,所以说Dot的大小是不固定的。

Android设备用DPI来表示屏幕密度(Density),屏幕密度大就表示一个Inch包含的Dot比较多。那PPI是什么呢?
我们会经常看到iPad、iPhone是用PPI来表示屏幕密度,小米Pad也是用PPI来表示。





PPI in mi pad
 
其实对Android而言,DPI等价于PPI(Pixels-Per-Inch),DPI最早是用于印刷行业,跟PPI还是有本质不同的,Android应该是误用了DPI这个概念。具体可以参考PPI vs. DPI: what’s the difference?。

其实我们只要知道在Android设备中,DPI 等价于 PPI 就可以了。





PPI 定义
通常我们说一个设备是多少寸时,指的是屏幕对角线(Diagonal)是多少inch,所以用对角线的像素值(px)除以对角线长度(inch),就可以计算出PPI。





PPI 计算公式
为了简化适配工作,Android根据屏幕大小(Inch)和屏幕密度(DPI)对设备做了如下划分:





PPI 对应屏幕尺寸

DP

既然有那么多不同分辨率、不同大小的屏幕,使用PX必然会导致适配困难,为了进一步简化适配工作,Android为我们提供了一个虚拟的像素单位 - DP 或者 DIP (Density-Independent pixel),当然也可以理解为 Device-Independent Pixel。为什么说是虚拟呢,因为它的大小不是一个物理(Phisical)值,而是由操作系统根据屏幕大小和密度动态渲染出来的。

PX跟DP之间的换算关系很简单 
px = dp * (dpi / 160)举例来说,小米Pad的屏幕密度为326dpi,如果需要显示的图片大小为20dp,那么就需要提供一个 20 (326 / 160) = 40px的图片才能达到最佳显示效果,如果还要适配一个163dpi的屏幕,那么还需要再提供一个20 (163 / 160) = 20px的图片。

那么一个20dp的图片,在不同设备上的显示效果如何呢?我们以iPad为例来说明。





iPad 屏幕参数
 
iPad2 和 iPad Retina的物理尺寸都是 9.7 inch,不同的是分辨率和PPI,一个是1024x768 / 132ppi,另一个是2048x1536 / 264ppi,
分别计算一下20dp对应多少inchipad2 = 20 * (132 / 160) * (7.9 / (math.sqrt(1024 * 1024 + 768 * 768)))
ipad_retina = 20 * (264 / 160) * (7.9 / (math.sqrt(2048 * 2048 + 1536 * 1536)))计算结果都是0.1018359375,这就是dp的功能,它能保证在所有的设备上显示的大小都一样。

如果只提供了一个大小为20px的图片,为了保证图片在所有设备上的物理大小都一样,高DPI的设备上系统会拉伸图片,低DPI的设备上图片会被缩小,这样既会影响UE也会影响APP的执行效率。所以我们需要为不同屏幕密度的设备提供不同的图片,他们之间的对应关系如下。







Android 设备屏幕分级
我们在用Sketch作图的时候,如果1x图片对应的是屏幕是MDPI (160dpi),那么1.5x,2x就分别对应HDPI,XHDPI。






screens_densitiesSP
 
SP 

SP 全称是 Scale-independent Pixels,用于字体大小,其概念与DP是一致的,也是为了保持设备无关。因为Android用户可以根据喜好来调整字体大小,所以要使用sp来表示字体大小。





changing_android_font_size
 
参考文献

http://developer.android.com/g ... youts
http://developer.android.com/t ... .html
http://stackoverflow.com/quest ... droid
http://99designs.com/designer- ... ence/
作者:liangfeizc 查看全部
Android的碎片化已经被喷了好多年,随着国内手机厂商的崛起,碎片化也越来越严重,根据OpenSignal的最新调查,2014年市面上有18796种不同的Android设备,作为开发者,一个无法回避的难题就是需要适配各种各样奇奇怪怪的机型。

设备机型不同必然也会导致屏幕大小和分辨率(Resolution)的不同,但是无论分辨率有多大,屏幕有多大,我们手指触控范围的大小不会发生变化,所以最优的适配方式应该是指定大小的控件在所有的设备上的显示都一样。

Android的官方文档对此也有明确的说明
 
When adding support for multiple screens, applications do not work directly with resolution; applications should be concerned only with screen size and density, as specified by the generalized size and density groups.
 
所以,适配应该与分辨率无关,只与屏幕大小和屏幕密度相关,首先来看一下什么是屏幕密度 - DPI。
 
DPI
 
DPI的全称是 Dots Per Inch,Inch是一个物理单位(无论在任何设备上,其大小都是固定的),所以DPI就指在一个Inch的物理长度内有多少个Dot,160DPI的屏幕就表示一个Inch包含160个Dot,320DPI的屏幕表示一个Inch有320个Dot,所以说Dot的大小是不固定的。

Android设备用DPI来表示屏幕密度(Density),屏幕密度大就表示一个Inch包含的Dot比较多。那PPI是什么呢?
我们会经常看到iPad、iPhone是用PPI来表示屏幕密度,小米Pad也是用PPI来表示。

51.png

PPI in mi pad
 
其实对Android而言,DPI等价于PPI(Pixels-Per-Inch),DPI最早是用于印刷行业,跟PPI还是有本质不同的,Android应该是误用了DPI这个概念。具体可以参考PPI vs. DPI: what’s the difference?。

其实我们只要知道在Android设备中,DPI 等价于 PPI 就可以了。

52.png

PPI 定义
通常我们说一个设备是多少寸时,指的是屏幕对角线(Diagonal)是多少inch,所以用对角线的像素值(px)除以对角线长度(inch),就可以计算出PPI。

53.png

PPI 计算公式
为了简化适配工作,Android根据屏幕大小(Inch)和屏幕密度(DPI)对设备做了如下划分:

54.png

PPI 对应屏幕尺寸

DP

既然有那么多不同分辨率、不同大小的屏幕,使用PX必然会导致适配困难,为了进一步简化适配工作,Android为我们提供了一个虚拟的像素单位 - DP 或者 DIP (Density-Independent pixel),当然也可以理解为 Device-Independent Pixel。为什么说是虚拟呢,因为它的大小不是一个物理(Phisical)值,而是由操作系统根据屏幕大小和密度动态渲染出来的。

PX跟DP之间的换算关系很简单 
px = dp * (dpi / 160)
举例来说,小米Pad的屏幕密度为326dpi,如果需要显示的图片大小为20dp,那么就需要提供一个 20 (326 / 160) = 40px的图片才能达到最佳显示效果,如果还要适配一个163dpi的屏幕,那么还需要再提供一个20 (163 / 160) = 20px的图片。

那么一个20dp的图片,在不同设备上的显示效果如何呢?我们以iPad为例来说明。

55.jpg

iPad 屏幕参数
 
iPad2 和 iPad Retina的物理尺寸都是 9.7 inch,不同的是分辨率和PPI,一个是1024x768 / 132ppi,另一个是2048x1536 / 264ppi,
分别计算一下20dp对应多少inch
ipad2 = 20 * (132 / 160) * (7.9 / (math.sqrt(1024 * 1024 + 768 * 768))) 
ipad_retina = 20 * (264 / 160) * (7.9 / (math.sqrt(2048 * 2048 + 1536 * 1536)))
计算结果都是0.1018359375,这就是dp的功能,它能保证在所有的设备上显示的大小都一样。

如果只提供了一个大小为20px的图片,为了保证图片在所有设备上的物理大小都一样,高DPI的设备上系统会拉伸图片,低DPI的设备上图片会被缩小,这样既会影响UE也会影响APP的执行效率。所以我们需要为不同屏幕密度的设备提供不同的图片,他们之间的对应关系如下。


56.png


Android 设备屏幕分级
我们在用Sketch作图的时候,如果1x图片对应的是屏幕是MDPI (160dpi),那么1.5x,2x就分别对应HDPI,XHDPI。

57.png


screens_densitiesSP
 
SP 

SP 全称是 Scale-independent Pixels,用于字体大小,其概念与DP是一致的,也是为了保持设备无关。因为Android用户可以根据喜好来调整字体大小,所以要使用sp来表示字体大小。
58.png


changing_android_font_size
 
参考文献

http://developer.android.com/g ... youts
http://developer.android.com/t ... .html
http://stackoverflow.com/quest ... droid
http://99designs.com/designer- ... ence/
作者:liangfeizc
0
评论

推荐:八个最优秀的 Android Studio 插件 移动开发 Android

oscar1212 发表了文章 • 1812 次浏览 • 2015-07-01 16:30 • 来自相关话题

Android Studio是目前Google官方设计的用于原生Android应用程序开发的IDE。基于JetBrains的IntelliJ IDEA,这是Google I/O 2013第一个宣布的作为Eclipse的继承者,深受广大Android社区的欢迎。在经过漫长的测试阶段后,最终版本于去年12月发布。

Android Studio是一个功能全面的开发环境,装备了为各种设备——从智能手表到汽车——开发Android应用程序所需要的所有功能。不但总是有改进的余地,Android Studio还提供了对第三方插件的支持,下面本文将列出一些最有用的插件。

1. H.A.X.M(硬件加速执行管理器)

如果你想使用Android模拟器更快地执行应用程序,那么H.A.X.M是你的最佳选择。H.A.X.M提供Android SDK模拟器在英特尔系统中的硬件加速。我认为H.A.X.M是最有用的插件,因为它能让Android开发人员尽快地在模拟器上运行最新的Android版本。

安装H.A.X.M

打开Android SDK管理器,选择“Intel x86 Emulator Accelerator (HAXM installer)”,接受许可并安装软件包。








这个进程只是下载软件包,还没有安装。为了完成安装到图片所示的SDK路径C:\Users\Administrator\AppData\Local\Android\sdk\ (安装在Windows机器上)并找到下载的文件夹。我的是:C:\Users\Administrator\AppData\Local\Android\sdk\extras\intel. 打开安装文件Hardware_Accelerated_Execution_Manager,单击可执行的intelhaxm-android,继续安装。完成此安装后,你就可以使用该模拟器了。







2. genymotion

Genymotion是测试Android应用程序,使你能够运行Android定制版本的旗舰工具。它是为了VirtualBox内部的执行而创建的,并配备了一整套与虚拟Android环境交互所需的传感器和功能。使用Genymotion能让你在多种虚拟开发设备上测试Android应用程序,并且它的模拟器比默认模拟器要快很多。








如果你想要确保你开发的应用程序能够在所有支持的设备上流畅地运行,但在特定设备上排除错误有困难时,那就应该好好利用这款伟大的插件。

想要安装Genymotion,可以参见以前发布过的教程。

3. Android  Drawable Importer

为了适应所有Android屏幕的大小和密度,每个Android项目都会包含drawable文件夹。任何具备Android开发经验的开发人员都知道,为了支持所有的屏幕尺寸,你必须给每个屏幕类型导入不同的画板。Android  Drawable Importer插件能让这项工作变得更容易。它可以减少导入缩放图像到Android项目所需的工作量。Android  Drawable Importer添加了一个在不同分辨率导入画板或缩放指定图像到定义分辨率的选项。这个插件加速了开发人员的画板工作。















安装Android  Drawable Importer






4. Android ButterKnife Zelezny

Android ButterKnife是一个“Android视图注入库”。它提供了一个更好的代码视图,使之更具可读性。 ButterKnife能让你专注于逻辑,而不是胶合代码用于查找视图或增加侦听器。用ButterKnife编程,你必须对任意对象进行注入,注入形式是这样的:@InjectView(R.id.title) TextView title;Android ButterKnife Zelezny是一款Android Studio插件,用于在活动、片段和适配器中,从所选的XML布局文件生成ButterKnife注入。该插件提供了生成XML对象注入的最快方式。如果只是一两个注入,那么这样写是没有问题的,但如果你有很多要写,那就需要参考所有的注入,将它们编写到源文件中。

下面是一个代码在使用Android ButterKnife之前的样子的例子:








以及使用之后:








安装ButterKnife Zelezny:






5. Android  Holo Colors Generator

开发Android应用程序需要伟大的设计和布局。Android  Holo Colors Generator则是定制符合喜好的Android应用程序的最简单方法。Android  Holo Colors Generator是一个允许你为你的应用程序随心所欲地创建Android布局组件的插件。此插件会生成所有必要的可在项目中使用的相关的XML画板和样式资源。

安装 Holo Colors Generator:






6. Robotium Recorder

Robotium Recorder是一个自动化测试框架,用于测试在模拟器和Android设备上原生的和混合的移动应用程序。Robotium Recorder可以让你记录测试案例和用户操作。你也可以查看不同Android活动时的系统功能和用户测试场景。

Robotium Recorder能让你看到当你的应用程序运行在设备上时,它是否能按预期工作,或者是否能对用户动作做出正确的回应。如果你想要开发稳定的Android应用程序,那么此插件对于进行彻底的测试很有帮助。

下面是一个例子,是我的应用程序使用Robotium Recorder时的样子:








想要安装Robotium Recorder,请登录它的官方页面,并根据你的操作系统的版本在安装区域选择Robotium Recorder。

7.jimu Mirror

Android Studio配备了一个可视化的布局编辑器。但是一个静态的布局预览有时候对于开发人员而言可能还不够,因为静态预览不能预览动画、颜色和触摸区域,所以jimu Mirror来了,这是一个可以让你在真实的设备上迅速测试布局的插件。jimu Mirror允许在设备上预览随同编码更新的Android布局。

安装jimu Mirror:






8.Strings-xml-tools

Strings-xml-tools是一个虽小但很有用的插件,可以用来管理Android项目中的字符串资源。它提供了排序Android本地文件和添加缺少的字符串的基本操作。虽然这个插件是有限制的,但如果应用程序有大量的字符串资源,那这个插件就非常有用了。

安装Android Strings.xml tools:






您有更优秀的Android Studio插件吗,欢迎在留言中告诉我们~ 查看全部
Android Studio是目前Google官方设计的用于原生Android应用程序开发的IDE。基于JetBrains的IntelliJ IDEA,这是Google I/O 2013第一个宣布的作为Eclipse的继承者,深受广大Android社区的欢迎。在经过漫长的测试阶段后,最终版本于去年12月发布。

Android Studio是一个功能全面的开发环境,装备了为各种设备——从智能手表到汽车——开发Android应用程序所需要的所有功能。不但总是有改进的余地,Android Studio还提供了对第三方插件的支持,下面本文将列出一些最有用的插件。

1. H.A.X.M(硬件加速执行管理器)

如果你想使用Android模拟器更快地执行应用程序,那么H.A.X.M是你的最佳选择。H.A.X.M提供Android SDK模拟器在英特尔系统中的硬件加速。我认为H.A.X.M是最有用的插件,因为它能让Android开发人员尽快地在模拟器上运行最新的Android版本。

安装H.A.X.M

打开Android SDK管理器,选择“Intel x86 Emulator Accelerator (HAXM installer)”,接受许可并安装软件包。


31.png



这个进程只是下载软件包,还没有安装。为了完成安装到图片所示的SDK路径C:\Users\Administrator\AppData\Local\Android\sdk\ (安装在Windows机器上)并找到下载的文件夹。我的是:C:\Users\Administrator\AppData\Local\Android\sdk\extras\intel. 打开安装文件Hardware_Accelerated_Execution_Manager,单击可执行的intelhaxm-android,继续安装。完成此安装后,你就可以使用该模拟器了。

312.png



2. genymotion

Genymotion是测试Android应用程序,使你能够运行Android定制版本的旗舰工具。它是为了VirtualBox内部的执行而创建的,并配备了一整套与虚拟Android环境交互所需的传感器和功能。使用Genymotion能让你在多种虚拟开发设备上测试Android应用程序,并且它的模拟器比默认模拟器要快很多。


313.png



如果你想要确保你开发的应用程序能够在所有支持的设备上流畅地运行,但在特定设备上排除错误有困难时,那就应该好好利用这款伟大的插件。

想要安装Genymotion,可以参见以前发布过的教程。

3. Android  Drawable Importer

为了适应所有Android屏幕的大小和密度,每个Android项目都会包含drawable文件夹。任何具备Android开发经验的开发人员都知道,为了支持所有的屏幕尺寸,你必须给每个屏幕类型导入不同的画板。Android  Drawable Importer插件能让这项工作变得更容易。它可以减少导入缩放图像到Android项目所需的工作量。Android  Drawable Importer添加了一个在不同分辨率导入画板或缩放指定图像到定义分辨率的选项。这个插件加速了开发人员的画板工作。


314.png




315.png



安装Android  Drawable Importer

Drawable-Importer.gif


4. Android ButterKnife Zelezny

Android ButterKnife是一个“Android视图注入库”。它提供了一个更好的代码视图,使之更具可读性。 ButterKnife能让你专注于逻辑,而不是胶合代码用于查找视图或增加侦听器。用ButterKnife编程,你必须对任意对象进行注入,注入形式是这样的:@InjectView(R.id.title) TextView title;Android ButterKnife Zelezny是一款Android Studio插件,用于在活动、片段和适配器中,从所选的XML布局文件生成ButterKnife注入。该插件提供了生成XML对象注入的最快方式。如果只是一两个注入,那么这样写是没有问题的,但如果你有很多要写,那就需要参考所有的注入,将它们编写到源文件中。

下面是一个代码在使用Android ButterKnife之前的样子的例子:



316.png


以及使用之后:


317.png



安装ButterKnife Zelezny:

ButterKnife-Zelezny.gif


5. Android  Holo Colors Generator

开发Android应用程序需要伟大的设计和布局。Android  Holo Colors Generator则是定制符合喜好的Android应用程序的最简单方法。Android  Holo Colors Generator是一个允许你为你的应用程序随心所欲地创建Android布局组件的插件。此插件会生成所有必要的可在项目中使用的相关的XML画板和样式资源。

安装 Holo Colors Generator:

41.gif


6. Robotium Recorder

Robotium Recorder是一个自动化测试框架,用于测试在模拟器和Android设备上原生的和混合的移动应用程序。Robotium Recorder可以让你记录测试案例和用户操作。你也可以查看不同Android活动时的系统功能和用户测试场景。

Robotium Recorder能让你看到当你的应用程序运行在设备上时,它是否能按预期工作,或者是否能对用户动作做出正确的回应。如果你想要开发稳定的Android应用程序,那么此插件对于进行彻底的测试很有帮助。

下面是一个例子,是我的应用程序使用Robotium Recorder时的样子:


412.jpg



想要安装Robotium Recorder,请登录它的官方页面,并根据你的操作系统的版本在安装区域选择Robotium Recorder。

7.jimu Mirror

Android Studio配备了一个可视化的布局编辑器。但是一个静态的布局预览有时候对于开发人员而言可能还不够,因为静态预览不能预览动画、颜色和触摸区域,所以jimu Mirror来了,这是一个可以让你在真实的设备上迅速测试布局的插件。jimu Mirror允许在设备上预览随同编码更新的Android布局。

安装jimu Mirror:

413.gif


8.Strings-xml-tools

Strings-xml-tools是一个虽小但很有用的插件,可以用来管理Android项目中的字符串资源。它提供了排序Android本地文件和添加缺少的字符串的基本操作。虽然这个插件是有限制的,但如果应用程序有大量的字符串资源,那这个插件就非常有用了。

安装Android Strings.xml tools:

414.gif


您有更优秀的Android Studio插件吗,欢迎在留言中告诉我们~
0
评论

技术分享:开源代码应用之Eclipse篇 eclipse

oscar 发表了文章 • 1679 次浏览 • 2015-07-01 12:07 • 来自相关话题

开写这篇的时候,恰逢Eclipse Mars(4.5)正式发布,终于由日蚀变登火星了,也离我开始基于Eclipse开发产品已经过去10年,这10年间,经历了Eclipse由私有核心框架到拥抱OSGi, 由单一Java IDE成长为巨无霸式的技术平台,由纯桌面到Web,嵌入式全面开花,个人也经历了从普通开发者成长为committer,又离开社区的过程,唯一不变的是:Eclipse依然是我开发Java唯一的选择。

对于这样一个由全世界最smart的一群人贡献和维护的开源项目(群),我相信任何热爱这个行业的工程师都能从中获得收益,这次就谈谈我基于Eclipse写的一个小工具。

不知道大家有没有类似的体会,每到产品发布期截止的时候,team就会开始忙乱的整理Java源代码中的license声明问题,严格统一的开发风格对所有的team来讲,基本都是一种奢望,从头开始不可能,那怎么办,不修复吧,不能发布,修复吧,这样的烂活没人愿意干,大概说来,修复Java源代码里面的license声明分为以下两个主流方式:
1. 既然是Java源代码,那就Java上啊,不就读出文件来,插入或替换吗?,定位吗,嗯,文件头的easy,成员变量型的,得想想...
2. 杀鸡焉用牛刀?,组合下Unix里面的小命令,分分钟搞定。

两种方式下的结果我都见过,实话说,的确不怎么样。

这件事情简单吗?说实话不难,但 Oracle依然把Java源代码里的license声明整成下面这个模样,就为了把以前Sun的license声明改成自己的。





这对很多有代码格式强迫症的工程师来讲,比杀了他们还难受啊。

其实我并没有接到这样的烂活,我只是思考了下,如果要处理好,该怎么办?嗯,这事要搞好,要是能操纵Java源代码的每一个部分不就行了?

哇靠,有人马上会跳起来说,这得懂编译器哪,对,就是编译器,不过也没有那么复杂,也就用了一丁丁点AST知识,不知道AST?哦,哪也没问题,有Eclipse替你做。

于是我开始动手实现这么一个能快速修复Java源代码中license声明的小工具,基本思路是基于Eclipse JDT里的AST实现,在Java语法这个粒度来修改,并做成一个Eclipse Plug-in,这下大家安装后,简单到点个button,就能完成工作。

具体实现步骤如下:

1. 生成一个Eclipse Plug-in项目,选个模版,最简单的那种,能点toolbar上面的button,弹出个"hello, world"对话框就可以。不知道怎么开发一个Eclipse Plug-in啊,没关系,看完这篇blog,你就会了。(别忘了好评!)

2. 在Action的回调方法里面,代码如下。
public void run(IAction action) {
license = getLicenseContent(LICENSE_FILE_NAME);
license_inline = getLicenseContent(LICENSE_INLINE_FILE_NAME);
if (license_inline.endsWith("\n")) {
license_inline = license_inline.substring(0, license_inline.length() - 1);
}
sum = 0;

IWorkspace workspace = ResourcesPlugin.getWorkspace();
IWorkspaceRoot root = workspace.getRoot();
IProject[] projects = root.getProjects();
for (IProject project : projects) {
try {
if (project.isOpen()) {
processProject(project);
}
} catch (Exception e) {
MessageDialog.openInformation(window.getShell(), "Fix License", "Exception happened, please check the console log.");
e.printStackTrace();
return;
}
}
MessageDialog.openInformation(window.getShell(), "Fix License", "All java source files have been processed. Total = " + sum);
}
首先获得license的内容,分为主license和行内license,具体内容这里就不显示了,然后获取Eclipse里面所有的项目,遍历每个项目并处理,这里只处理打开的项目,如果你有不想处理的项目,关闭就行。

3. 处理项目。
private void processProject(IProject project) throws Exception {
if (project.isNatureEnabled("org.eclipse.jdt.core.javanature")) {
IJavaProject javaProject = JavaCore.create(project);
IPackageFragment[] packages = javaProject.getPackageFragments();
for (IPackageFragment mypackage : packages) {
if (mypackage.getKind() == IPackageFragmentRoot.K_SOURCE) {
for (ICompilationUnit unit : mypackage.getCompilationUnits()) {
sum = sum + 1;
processJavaSource(unit);
}
}
}
}
}
当然只修复Java项目,没有Java nature的,一律抛弃。
获得Java项目后,获取所有的package,这里的package和通常意义上Java的package不同,具体意义看API,就当课后作业。
再进一步,就可以获取Java源文件,并取得编译单元,有了这个,以后的路就有方向了。

4. 处理Java源文件。
private void processJavaSource(ICompilationUnit unit) throws Exception {
ITextFileBufferManager bufferManager = FileBuffers.getTextFileBufferManager();
IPath path = unit.getPath();
try {
bufferManager.connect(path, null);
ITextFileBuffer textFileBuffer = bufferManager.getTextFileBuffer(path);
IDocument doc = textFileBuffer.getDocument();
if ((license !=null) && (license.length() > 0)) {
processHeadLicense(doc);
}
if ((license_inline != null) && (license_inline.length() > 0)) {
processInlineLicense(doc);
}
textFileBuffer.commit(null, false);
} finally {
bufferManager.disconnect(path, null);
}
}
这里用到了一些Eclipse Jface text包里面的东西,和Java里面常见的文件读写API有些不一样,但基本思想是一致的。等取到了IDocument对象,就可以开始正式的license处理。

5. 处理Java文件头license声明。
private void processHeadLicense(IDocument doc) throws Exception {
CompilationUnit cu = getAST(doc);
Comment comment = null;
if (cu.getCommentList().size() == 0) {
doc.replace(0, 0, license);
} else {
comment = (Comment)cu.getCommentList().get(0);
String firstComment = doc.get().substring(comment.getStartPosition(), comment.getStartPosition() + comment.getLength());
if (validateHeadLicense(firstComment)) {
doc.replace(comment.getStartPosition(), comment.getLength(), license);
} else {
doc.replace(0, 0, license);
}
}
}

private CompilationUnit getAST(IDocument doc) {
ASTParser parser = ASTParser.newParser(AST.JLS4);
parser.setKind(ASTParser.K_COMPILATION_UNIT);
parser.setSource(doc.get().toCharArray());
parser.setResolveBindings(true);
CompilationUnit cu = (CompilationUnit) parser.createAST(null);

return cu;
}
基于AST就可以得到Java源代码里面的所有comments,接下来就可以根据各种情况插入或替换文件头的license声明。

6. 成员变量型license声明。
这种license声明类似下面这个例子。
public class Demo {
public static final String COPYRIGHT = "(C) Copyright IBM Corporation 2013, 2014, 2015.";

public Demo() {

}

public void hello() {

}

}
它的处理方式如下。
private void processInlineLicense(IDocument doc) throws Exception {
CompilationUnit cu = getAST(doc);
cu.recordModifications();
AST ast = cu.getAST();

if (cu.types().get(0) instanceof TypeDeclaration) {
TypeDeclaration td = (TypeDeclaration)cu.types().get(0);
FieldDeclaration[] fd = td.getFields();
if (fd.length == 0) {
td.bodyDeclarations().add(0, createLiceseInLineField(ast));
} else {
FieldDeclaration firstFd = fd[0];
VariableDeclarationFragment vdf = (VariableDeclarationFragment)firstFd.fragments().get(0);
if (vdf.getName().getIdentifier().equals("COPYRIGHT")) {
td.bodyDeclarations().remove(0);
td.bodyDeclarations().add(0, createLiceseInLineField(ast));
} else {
td.bodyDeclarations().add(0, createLiceseInLineField(ast));
}
}
}

//record changes
TextEdit edits = cu.rewrite(doc, null);
edits.apply(doc);
}

private FieldDeclaration createLiceseInLineField(AST ast) {
VariableDeclarationFragment vdf = ast.newVariableDeclarationFragment();
vdf.setName(ast.newSimpleName("COPYRIGHT"));
StringLiteral sl = ast.newStringLiteral();
sl.setLiteralValue(license_inline);
vdf.setInitializer(sl);
FieldDeclaration fd = ast.newFieldDeclaration(vdf);
fd.modifiers().addAll(ast.newModifiers(Modifier.PUBLIC | Modifier.STATIC | Modifier.FINAL));
fd.setType(ast.newSimpleType(ast.newSimpleName("String")));

return fd;
}
成员变量类型的license声明处理起来稍显麻烦,主要原因是牵扯到Java成员变量的创建和解析,但其实也不是很难理解,而且从中可以学到AST是如何精细处理Java类的各个组成部分的。

7. 测试。
启动一个新的调试Eclipse Plug-in的Eclipse Runtime,导入任意几个Java项目,从菜单或工具栏上面选择“Fix License” action,完成之后检查任意的Java源文件,看看license是否已经修复。

来看看一个简单的测试结果吧。
这个是修复前的
package com.demo;

public class Demo {
public Demo() {

}

public void hello() {

}

}
这个是修复后的
/* IBM Confidential
* OCO Source Materials
*
* (C)Copyright IBM Corporation 2013, 2014, 2015.
*
* The source code for this program is not published or otherwise
* divested of its trade secrets, irrespective of what has been
* deposited with the U.S. Copyright Office.
*/
package com.demo;

public class Demo {
public static final String COPYRIGHT = "(C) Copyright IBM Corporation 2013, 2014, 2015.";

public Demo() {

}

public void hello() {

}

}
8. 打包分发。
这个工具Plug-in可以按Eclipse的标准插件打包并安装,或者生成一个Update Site以供用户在线安装。

好了,啰嗦了这么多,到了该结束的时刻,最后一句,这个小工具所有的源代码已经在GitHub上开源,喜欢可以去下载并测试,源代码里面附有一份详细安装的文档。

工具地址:https://github.com/alexgreenbar/open_tools.git
作者:Alex 查看全部
开写这篇的时候,恰逢Eclipse Mars(4.5)正式发布,终于由日蚀变登火星了,也离我开始基于Eclipse开发产品已经过去10年,这10年间,经历了Eclipse由私有核心框架到拥抱OSGi, 由单一Java IDE成长为巨无霸式的技术平台,由纯桌面到Web,嵌入式全面开花,个人也经历了从普通开发者成长为committer,又离开社区的过程,唯一不变的是:Eclipse依然是我开发Java唯一的选择。

对于这样一个由全世界最smart的一群人贡献和维护的开源项目(群),我相信任何热爱这个行业的工程师都能从中获得收益,这次就谈谈我基于Eclipse写的一个小工具。

不知道大家有没有类似的体会,每到产品发布期截止的时候,team就会开始忙乱的整理Java源代码中的license声明问题,严格统一的开发风格对所有的team来讲,基本都是一种奢望,从头开始不可能,那怎么办,不修复吧,不能发布,修复吧,这样的烂活没人愿意干,大概说来,修复Java源代码里面的license声明分为以下两个主流方式:
1. 既然是Java源代码,那就Java上啊,不就读出文件来,插入或替换吗?,定位吗,嗯,文件头的easy,成员变量型的,得想想...
2. 杀鸡焉用牛刀?,组合下Unix里面的小命令,分分钟搞定。

两种方式下的结果我都见过,实话说,的确不怎么样。

这件事情简单吗?说实话不难,但 Oracle依然把Java源代码里的license声明整成下面这个模样,就为了把以前Sun的license声明改成自己的。

1.png

这对很多有代码格式强迫症的工程师来讲,比杀了他们还难受啊。

其实我并没有接到这样的烂活,我只是思考了下,如果要处理好,该怎么办?嗯,这事要搞好,要是能操纵Java源代码的每一个部分不就行了?

哇靠,有人马上会跳起来说,这得懂编译器哪,对,就是编译器,不过也没有那么复杂,也就用了一丁丁点AST知识,不知道AST?哦,哪也没问题,有Eclipse替你做。

于是我开始动手实现这么一个能快速修复Java源代码中license声明的小工具,基本思路是基于Eclipse JDT里的AST实现,在Java语法这个粒度来修改,并做成一个Eclipse Plug-in,这下大家安装后,简单到点个button,就能完成工作。

具体实现步骤如下:

1. 生成一个Eclipse Plug-in项目,选个模版,最简单的那种,能点toolbar上面的button,弹出个"hello, world"对话框就可以。不知道怎么开发一个Eclipse Plug-in啊,没关系,看完这篇blog,你就会了。(别忘了好评!)

2. 在Action的回调方法里面,代码如下。
public void run(IAction action) {  
license = getLicenseContent(LICENSE_FILE_NAME);
license_inline = getLicenseContent(LICENSE_INLINE_FILE_NAME);
if (license_inline.endsWith("\n")) {
license_inline = license_inline.substring(0, license_inline.length() - 1);
}
sum = 0;

IWorkspace workspace = ResourcesPlugin.getWorkspace();
IWorkspaceRoot root = workspace.getRoot();
IProject[] projects = root.getProjects();
for (IProject project : projects) {
try {
if (project.isOpen()) {
processProject(project);
}
} catch (Exception e) {
MessageDialog.openInformation(window.getShell(), "Fix License", "Exception happened, please check the console log.");
e.printStackTrace();
return;
}
}
MessageDialog.openInformation(window.getShell(), "Fix License", "All java source files have been processed. Total = " + sum);
}
首先获得license的内容,分为主license和行内license,具体内容这里就不显示了,然后获取Eclipse里面所有的项目,遍历每个项目并处理,这里只处理打开的项目,如果你有不想处理的项目,关闭就行。

3. 处理项目
private void processProject(IProject project) throws Exception {  
if (project.isNatureEnabled("org.eclipse.jdt.core.javanature")) {
IJavaProject javaProject = JavaCore.create(project);
IPackageFragment[] packages = javaProject.getPackageFragments();
for (IPackageFragment mypackage : packages) {
if (mypackage.getKind() == IPackageFragmentRoot.K_SOURCE) {
for (ICompilationUnit unit : mypackage.getCompilationUnits()) {
sum = sum + 1;
processJavaSource(unit);
}
}
}
}
}
当然只修复Java项目,没有Java nature的,一律抛弃。
获得Java项目后,获取所有的package,这里的package和通常意义上Java的package不同,具体意义看API,就当课后作业。
再进一步,就可以获取Java源文件,并取得编译单元,有了这个,以后的路就有方向了。

4. 处理Java源文件
private void processJavaSource(ICompilationUnit unit) throws Exception {  
ITextFileBufferManager bufferManager = FileBuffers.getTextFileBufferManager();
IPath path = unit.getPath();
try {
bufferManager.connect(path, null);
ITextFileBuffer textFileBuffer = bufferManager.getTextFileBuffer(path);
IDocument doc = textFileBuffer.getDocument();
if ((license !=null) && (license.length() > 0)) {
processHeadLicense(doc);
}
if ((license_inline != null) && (license_inline.length() > 0)) {
processInlineLicense(doc);
}
textFileBuffer.commit(null, false);
} finally {
bufferManager.disconnect(path, null);
}
}
这里用到了一些Eclipse Jface text包里面的东西,和Java里面常见的文件读写API有些不一样,但基本思想是一致的。等取到了IDocument对象,就可以开始正式的license处理。

5. 处理Java文件头license声明
private void processHeadLicense(IDocument doc) throws Exception {  
CompilationUnit cu = getAST(doc);
Comment comment = null;
if (cu.getCommentList().size() == 0) {
doc.replace(0, 0, license);
} else {
comment = (Comment)cu.getCommentList().get(0);
String firstComment = doc.get().substring(comment.getStartPosition(), comment.getStartPosition() + comment.getLength());
if (validateHeadLicense(firstComment)) {
doc.replace(comment.getStartPosition(), comment.getLength(), license);
} else {
doc.replace(0, 0, license);
}
}
}

private CompilationUnit getAST(IDocument doc) {
ASTParser parser = ASTParser.newParser(AST.JLS4);
parser.setKind(ASTParser.K_COMPILATION_UNIT);
parser.setSource(doc.get().toCharArray());
parser.setResolveBindings(true);
CompilationUnit cu = (CompilationUnit) parser.createAST(null);

return cu;
}
基于AST就可以得到Java源代码里面的所有comments,接下来就可以根据各种情况插入或替换文件头的license声明。

6. 成员变量型license声明
这种license声明类似下面这个例子。
public class Demo {  
public static final String COPYRIGHT = "(C) Copyright IBM Corporation 2013, 2014, 2015.";

public Demo() {

}

public void hello() {

}

}
它的处理方式如下。
private void processInlineLicense(IDocument doc) throws Exception {  
CompilationUnit cu = getAST(doc);
cu.recordModifications();
AST ast = cu.getAST();

if (cu.types().get(0) instanceof TypeDeclaration) {
TypeDeclaration td = (TypeDeclaration)cu.types().get(0);
FieldDeclaration[] fd = td.getFields();
if (fd.length == 0) {
td.bodyDeclarations().add(0, createLiceseInLineField(ast));
} else {
FieldDeclaration firstFd = fd[0];
VariableDeclarationFragment vdf = (VariableDeclarationFragment)firstFd.fragments().get(0);
if (vdf.getName().getIdentifier().equals("COPYRIGHT")) {
td.bodyDeclarations().remove(0);
td.bodyDeclarations().add(0, createLiceseInLineField(ast));
} else {
td.bodyDeclarations().add(0, createLiceseInLineField(ast));
}
}
}

//record changes
TextEdit edits = cu.rewrite(doc, null);
edits.apply(doc);
}

private FieldDeclaration createLiceseInLineField(AST ast) {
VariableDeclarationFragment vdf = ast.newVariableDeclarationFragment();
vdf.setName(ast.newSimpleName("COPYRIGHT"));
StringLiteral sl = ast.newStringLiteral();
sl.setLiteralValue(license_inline);
vdf.setInitializer(sl);
FieldDeclaration fd = ast.newFieldDeclaration(vdf);
fd.modifiers().addAll(ast.newModifiers(Modifier.PUBLIC | Modifier.STATIC | Modifier.FINAL));
fd.setType(ast.newSimpleType(ast.newSimpleName("String")));

return fd;
}
成员变量类型的license声明处理起来稍显麻烦,主要原因是牵扯到Java成员变量的创建和解析,但其实也不是很难理解,而且从中可以学到AST是如何精细处理Java类的各个组成部分的。

7. 测试
启动一个新的调试Eclipse Plug-in的Eclipse Runtime,导入任意几个Java项目,从菜单或工具栏上面选择“Fix License” action,完成之后检查任意的Java源文件,看看license是否已经修复。

来看看一个简单的测试结果吧。
这个是修复前的
package com.demo;  

public class Demo {
public Demo() {

}

public void hello() {

}

}
这个是修复后的
/* IBM Confidential 
* OCO Source Materials
*
* (C)Copyright IBM Corporation 2013, 2014, 2015.
*
* The source code for this program is not published or otherwise
* divested of its trade secrets, irrespective of what has been
* deposited with the U.S. Copyright Office.
*/
package com.demo;

public class Demo {
public static final String COPYRIGHT = "(C) Copyright IBM Corporation 2013, 2014, 2015.";

public Demo() {

}

public void hello() {

}

}
8. 打包分发
这个工具Plug-in可以按Eclipse的标准插件打包并安装,或者生成一个Update Site以供用户在线安装。

好了,啰嗦了这么多,到了该结束的时刻,最后一句,这个小工具所有的源代码已经在GitHub上开源,喜欢可以去下载并测试,源代码里面附有一份详细安装的文档。

工具地址:https://github.com/alexgreenbar/open_tools.git
作者:Alex
1
回复

历史消息中的图片消息该怎么显示? 环信_WebIM 历史消息

beyond 回复了问题 • 2 人关注 • 1952 次浏览 • 2015-07-01 11:44 • 来自相关话题

1
回复

今天添加好友一直报500错误 环信

lizg 回复了问题 • 3 人关注 • 989 次浏览 • 2015-07-01 11:37 • 来自相关话题