什么叫有专职工程师值守的话题
什么叫有专职工程师值守的话题
如今的社区,热心的人越来越少,当你有问题需要发贴求助的时候,大多会遇到的情况会是这样的:
. 永远不知道问题何时能得到回复
终于有了回复的,还是:
. 无聊捣乱的
. 自动顶贴机的自动回复,后面还带有一串含有Url的签名
. 各种奇葩的、不搭调的回复
. ......
最郁闷的是,你的贴子就像一颗石子扔到湖里,到最后,却没有任何涟漪:没.有.回.帖......
imgeek努力想改变这种现状,循着极客们开放、分享、协作、创新的精神,我们努力构建一个具有服务质量保障(Service Level Assurance , SLA)的社区
我们努力征集一些热心的技术专家,得到他们的承诺,可以值守一些话题,当有该话题提交的时候,他可以及时的收到消息提醒,这样可以保障他可以及时的看到你提的问题,以便及时回复。
当然,如果你在提交一个问题之前,可以先搜索一下,说不定你要提的问题已经有人提过并且得到解答。这样可以省却不少你的时间。
我们努力的寻找一些热心的专家,可以为社区兄弟们解决一些问题,imgeek社区也可以提供一些必要的补贴给你们,你可以填写表格申请成为我们的”社区专家“,我们将会及时联系你 收起阅读 »
如今的社区,热心的人越来越少,当你有问题需要发贴求助的时候,大多会遇到的情况会是这样的:
. 永远不知道问题何时能得到回复
终于有了回复的,还是:
. 无聊捣乱的
. 自动顶贴机的自动回复,后面还带有一串含有Url的签名
. 各种奇葩的、不搭调的回复
. ......
最郁闷的是,你的贴子就像一颗石子扔到湖里,到最后,却没有任何涟漪:没.有.回.帖......
imgeek努力想改变这种现状,循着极客们开放、分享、协作、创新的精神,我们努力构建一个具有服务质量保障(Service Level Assurance , SLA)的社区
我们努力征集一些热心的技术专家,得到他们的承诺,可以值守一些话题,当有该话题提交的时候,他可以及时的收到消息提醒,这样可以保障他可以及时的看到你提的问题,以便及时回复。
当然,如果你在提交一个问题之前,可以先搜索一下,说不定你要提的问题已经有人提过并且得到解答。这样可以省却不少你的时间。
我们努力的寻找一些热心的专家,可以为社区兄弟们解决一些问题,imgeek社区也可以提供一些必要的补贴给你们,你可以填写表格申请成为我们的”社区专家“,我们将会及时联系你 收起阅读 »
技术分享:Python 并行任务技巧
Python的并发处理能力臭名昭著。先撇开线程以及GIL方面的问题不说,我觉得多线程问题的根源不在技术上而在于理念。大部分关于Pyhon线程和多进程的资料虽然都很不错,但却过于细节。这些资料讲的都是虎头蛇尾,到了真正实际使用的部分却草草结束了。
传统例子
在DDG https://duckduckgo.com/ 搜索“Python threading tutorial”关键字,结果基本上却都是相同的类+队列的示例。
标准线程多进程,生产者/消费者示例:
这里是代码截图,如果用其他模式贴出大段代码会很不美观。文本模式点这里 here
Mmm.. 感觉像是java代码
在此我不想印证采用生产者/消费者模式来处理线程/多进程是错误的— 确实没问题。实际上这也是解决很多问题的最佳选择。但是,我却不认为这是日常工作中常用的方式。
问题所在
一开始,你需要一个执行下面操作的铺垫类。接着,你需要创建一个传递对象的队列,并在队列两端实时监听以完成任务。(很有可能需要两个队列互相通信或者存储数据)
Worker越多,问题越大.
下一步,你可能会考虑把这些worker放入一个线程池一边提高Python的处理速度。下面是
IBM tutorial 上关于线程较好的示例代码。这是大家常用到的利用多线程处理web页面的场景
Seriously, Medium. Fix your code support. Code is Here.
感觉效果应该很好,但是看看这些代码!初始化方法、线程跟踪,最糟的是,如果你也和我一样是个容易犯死锁问题的人,这里的join语句就要出错了。这样就开始变得更加复杂了!
到现在为止都做了些什么?基本上没什么。上面的代码都是些基础功能,而且很容易出错。(天啊,我忘了写上在队列对象上调用task_done()方法(我懒得修复这个问题在重新截图)),这真是性价比太低。所幸的是,我们有更好的办法.
引入:Map
Map 是个很酷的小功能,也是简化Python并发代码的关键。对那些不太熟悉Map的来说,它有点类似Lisp.它就是序列化的功能映射功能. e.g.
为什么要提到它?因为在引入需要的包文件后,Map能大大简化并发的复杂度!
支持Map并发的包文件有两个:
Multiprocessing,还有少为人知的但却功能强大的子文件 multiprocessing.dummy. .
Digression这是啥东西?没听说过线程引用叫dummy的多进程包文件。我也是直到最近才知道。它在多进程的说明文档中也只被提到了一句。它的效果也只是让大家直到有这么个东西而已。这可真是营销的失误!
Dummy是一个多进程包的完整拷贝。唯一不同的是,多进程包使用进程,而dummy使用线程(自然也有Python本身的一些限制)。所以一个有的另一个也有。这样在两种模式间切换就十分简单,并且在判断框架调用时使用的是IO还是CPU模式非常有帮助。
准备开始
准备使用带有并发的map功能首先要导入相关包文件:
pool对象需要一些参数,但现在最紧要的就是:进程。它可以限定线程池中worker的数量。如果不填,它将采用系统的内核数作为初值。
一般情况下,如果你进行的是计算密集型多进程任务,内核越多意味着速度越快(当然这是有前提的)。但如果是涉及到网络计算方面,影响的因素就千差万别。所以最好还是能给出合适的线程池大小数。
pool = ThreadPool(4) # Sets the pool size to 4
如果运行的线程很多,频繁的切换线程会十分影响工作效率。所以最好还是能通过调试找出任务调度的时间平衡点。
好的,既然已经建好了线程池对象还有那些简单的并发内容。咱们就来重写一些example2.py中的url opener吧!
看吧!只用4行代码就搞定了!其中三行还是固定写法。使用map方法简单的搞定了之前需要40行代码做的事!为了增加趣味性,我分别统计了不同线程池大小的运行时间。
结果:
效果惊人!看来调试一下确实很有用。当线程池大小超过9以后,在我本机上的运行效果已相差无几。
示例 2:
生成上千张图像的缩略图:
现在咱们看一年计算密集型的任务!我最常遇到的这类问题之一就是大量图像文件夹的处理。
其中一项任务就是创建缩略图。这也是并发中比较成熟的一项功能了。
基础单线程创建过程
作为示例来说稍微有点复杂。但其实就是传一个文件夹目录进来,获取到里面所有的图片,分别创建好缩略图然后保存到各自的目录当中。
在我的电脑上,处理大约6000张图片大约耗时27.9秒.
如果使用并发map处理替代其中的for循环:
只用了5.6 秒!
就改了几行代码速度却能得到如此巨大的提升。最终版本的处理速度还要更快。因为我们将计算密集型与IO密集型任务分派到各自独立的线程和进程当中,这也许会容易造成死锁,但相对于map强劲的功能,通过简单的调试我们最终总能设计出优美、高可靠性的程序。就现在而言,也别无它法。
好了。来感受一下一行代码的并发程序吧。 收起阅读 »
传统例子
在DDG https://duckduckgo.com/ 搜索“Python threading tutorial”关键字,结果基本上却都是相同的类+队列的示例。
标准线程多进程,生产者/消费者示例:
这里是代码截图,如果用其他模式贴出大段代码会很不美观。文本模式点这里 here
Mmm.. 感觉像是java代码
在此我不想印证采用生产者/消费者模式来处理线程/多进程是错误的— 确实没问题。实际上这也是解决很多问题的最佳选择。但是,我却不认为这是日常工作中常用的方式。
问题所在
一开始,你需要一个执行下面操作的铺垫类。接着,你需要创建一个传递对象的队列,并在队列两端实时监听以完成任务。(很有可能需要两个队列互相通信或者存储数据)
Worker越多,问题越大.
下一步,你可能会考虑把这些worker放入一个线程池一边提高Python的处理速度。下面是
IBM tutorial 上关于线程较好的示例代码。这是大家常用到的利用多线程处理web页面的场景
Seriously, Medium. Fix your code support. Code is Here.
感觉效果应该很好,但是看看这些代码!初始化方法、线程跟踪,最糟的是,如果你也和我一样是个容易犯死锁问题的人,这里的join语句就要出错了。这样就开始变得更加复杂了!
到现在为止都做了些什么?基本上没什么。上面的代码都是些基础功能,而且很容易出错。(天啊,我忘了写上在队列对象上调用task_done()方法(我懒得修复这个问题在重新截图)),这真是性价比太低。所幸的是,我们有更好的办法.
引入:Map
Map 是个很酷的小功能,也是简化Python并发代码的关键。对那些不太熟悉Map的来说,它有点类似Lisp.它就是序列化的功能映射功能. e.g.
urls = [', ']这里调用urlopen方法,并把之前的调用结果全都返回并按顺序存储到一个集合中。这有点类似
results = map(urllib2.urlopen, urls)
results = []Map能够处理集合按顺序遍历,最终将调用产生的结果保存在一个简单的集合当中。
for url in urls:
results.append(urllib2.urlopen(url))
为什么要提到它?因为在引入需要的包文件后,Map能大大简化并发的复杂度!
支持Map并发的包文件有两个:
Multiprocessing,还有少为人知的但却功能强大的子文件 multiprocessing.dummy. .
Digression这是啥东西?没听说过线程引用叫dummy的多进程包文件。我也是直到最近才知道。它在多进程的说明文档中也只被提到了一句。它的效果也只是让大家直到有这么个东西而已。这可真是营销的失误!
Dummy是一个多进程包的完整拷贝。唯一不同的是,多进程包使用进程,而dummy使用线程(自然也有Python本身的一些限制)。所以一个有的另一个也有。这样在两种模式间切换就十分简单,并且在判断框架调用时使用的是IO还是CPU模式非常有帮助。
准备开始
准备使用带有并发的map功能首先要导入相关包文件:
from multiprocessing import Pool然后初始化:
from multiprocessing.dummy import Pool as ThreadPool
pool = ThreadPool()就这么简单一句解决了example2.py中build_worker_pool的功能. 具体来讲,它首先创建一些有效的worker启动它并将其保存在一些变量中以便随时访问。
pool对象需要一些参数,但现在最紧要的就是:进程。它可以限定线程池中worker的数量。如果不填,它将采用系统的内核数作为初值。
一般情况下,如果你进行的是计算密集型多进程任务,内核越多意味着速度越快(当然这是有前提的)。但如果是涉及到网络计算方面,影响的因素就千差万别。所以最好还是能给出合适的线程池大小数。
pool = ThreadPool(4) # Sets the pool size to 4
如果运行的线程很多,频繁的切换线程会十分影响工作效率。所以最好还是能通过调试找出任务调度的时间平衡点。
好的,既然已经建好了线程池对象还有那些简单的并发内容。咱们就来重写一些example2.py中的url opener吧!
看吧!只用4行代码就搞定了!其中三行还是固定写法。使用map方法简单的搞定了之前需要40行代码做的事!为了增加趣味性,我分别统计了不同线程池大小的运行时间。
结果:
效果惊人!看来调试一下确实很有用。当线程池大小超过9以后,在我本机上的运行效果已相差无几。
示例 2:
生成上千张图像的缩略图:
现在咱们看一年计算密集型的任务!我最常遇到的这类问题之一就是大量图像文件夹的处理。
其中一项任务就是创建缩略图。这也是并发中比较成熟的一项功能了。
基础单线程创建过程
作为示例来说稍微有点复杂。但其实就是传一个文件夹目录进来,获取到里面所有的图片,分别创建好缩略图然后保存到各自的目录当中。
在我的电脑上,处理大约6000张图片大约耗时27.9秒.
如果使用并发map处理替代其中的for循环:
只用了5.6 秒!
就改了几行代码速度却能得到如此巨大的提升。最终版本的处理速度还要更快。因为我们将计算密集型与IO密集型任务分派到各自独立的线程和进程当中,这也许会容易造成死锁,但相对于map强劲的功能,通过简单的调试我们最终总能设计出优美、高可靠性的程序。就现在而言,也别无它法。
好了。来感受一下一行代码的并发程序吧。 收起阅读 »
Go语言对Android原生应用开发的支持情况
Google工程师和独立开发人员提出了几份不同的提案,旨在让Go语言支持开发原生的Android应用。这项工作无法让Go语言编写的应用使用Android NDK的全部接口,但有可能使用其中的一个子集。
David Crawshaw是Google的工程师,他写了一份提案,旨在让Go语言部分支持编写Android应用。根据他的说法,“用Go语言来实现整个Android平台非常困难。Android平台是用Java写的,并拥有庞大的API层。”
但是,Crawshaw说,一部分Android应用——比如游戏——使用了精简得多的C语言API编写代码,这些API由Android NDK提供。这样,使用Go语言来开发和NDK一样的功能,提供对Android的支持是有可能实现的。
Crawshaw建议在Go 1.4的开发周期中,Go语言的代码库引入一个叫GOOS=android的选项,这个选项可以提供以下功能:
不止Crawshaw一个人提出了把Go语言和Android结合起来的想法。Elias Naur建议扩展Go语言的工具链来支持创建动态库。这样我们就可以在Android应用中使用Go语言编写的库,它们被Android应用加载和运行,并打包在apk中发行。要把这个想法变为现实,有一个重要的前提条件:加入对交叉编译的支持,而Go 1.3已经实现了它。交叉编译是必需的,因为NDK本身并不能在Android上运行,只有使用NDK编译和(或)链接的可执行程序和动态库才能在Android设备上运行。
上面这个提案基于已有的开源项目goandroid,作者就是Elias Naur。Goandroid修改了Go语言的工具链和运行时库,使之能编写动态库,在原生的Android应用中运行,而Google官方并不支持这个功能。
最后,还有一个叫Mandala的项目,它是一个更全面的框架,它的目标是使Go语言能编写Android原生应用。Mandala利用了Goandroid的工具链,它的作者Andrea Fazzi说,感谢Goandroid,“你可以在桌面环境中开发、测试和运行你的应用,然后再把它部署到Android设备上。它鼓励大家以Go语言独特的方式来编写Android应用:使用通道(channels)来实现通讯,而不是回调函数(callbacks)”
在功能方面,Mandala项目跟Crawshaw的提案很接近,它的目标也主要是为游戏提供解决方案:“我们不应该把Mandala框架看作是一个上层的游戏引擎,而是应该在它基础之上构建游戏引擎,或者把已有的游戏引擎移植到它上面。” Fazzi 提醒道,Google并不支持用Go语言来开发原生的Android应用,但他也表达了他的期望“当前这些工作可以起到某种激励作用,促使Go语言开发团队从官方层面支持Android。”
来源:infoq
参考原文链接:The State of Go Language for Android Native Development 收起阅读 »
David Crawshaw是Google的工程师,他写了一份提案,旨在让Go语言部分支持编写Android应用。根据他的说法,“用Go语言来实现整个Android平台非常困难。Android平台是用Java写的,并拥有庞大的API层。”
但是,Crawshaw说,一部分Android应用——比如游戏——使用了精简得多的C语言API编写代码,这些API由Android NDK提供。这样,使用Go语言来开发和NDK一样的功能,提供对Android的支持是有可能实现的。
Crawshaw建议在Go 1.4的开发周期中,Go语言的代码库引入一个叫GOOS=android的选项,这个选项可以提供以下功能:
- 为Android NDK中导出的OpenGL、OpenSL和OpenMAX接口,提供Go语言的绑定(binding)。
- 从Java语言到Go语言的绑定生成器(binding generator)。如果我们有一个Go语言编写的软件包,那么这个生成器可以帮助Java代码调用它,所以游戏菜单界面就可以直接使用标准的SDK来编写了。
- 集成到Android Studio的编译系统中。
不止Crawshaw一个人提出了把Go语言和Android结合起来的想法。Elias Naur建议扩展Go语言的工具链来支持创建动态库。这样我们就可以在Android应用中使用Go语言编写的库,它们被Android应用加载和运行,并打包在apk中发行。要把这个想法变为现实,有一个重要的前提条件:加入对交叉编译的支持,而Go 1.3已经实现了它。交叉编译是必需的,因为NDK本身并不能在Android上运行,只有使用NDK编译和(或)链接的可执行程序和动态库才能在Android设备上运行。
上面这个提案基于已有的开源项目goandroid,作者就是Elias Naur。Goandroid修改了Go语言的工具链和运行时库,使之能编写动态库,在原生的Android应用中运行,而Google官方并不支持这个功能。
最后,还有一个叫Mandala的项目,它是一个更全面的框架,它的目标是使Go语言能编写Android原生应用。Mandala利用了Goandroid的工具链,它的作者Andrea Fazzi说,感谢Goandroid,“你可以在桌面环境中开发、测试和运行你的应用,然后再把它部署到Android设备上。它鼓励大家以Go语言独特的方式来编写Android应用:使用通道(channels)来实现通讯,而不是回调函数(callbacks)”
在功能方面,Mandala项目跟Crawshaw的提案很接近,它的目标也主要是为游戏提供解决方案:“我们不应该把Mandala框架看作是一个上层的游戏引擎,而是应该在它基础之上构建游戏引擎,或者把已有的游戏引擎移植到它上面。” Fazzi 提醒道,Google并不支持用Go语言来开发原生的Android应用,但他也表达了他的期望“当前这些工作可以起到某种激励作用,促使Go语言开发团队从官方层面支持Android。”
来源:infoq
参考原文链接:The State of Go Language for Android Native Development 收起阅读 »
请问:webim使用时,本地数据库和环信数据库的关系?
我公司想使用环信这个聊天工具,有几个问题请环信工程师帮助解答一下,非常感谢
1. 我自己的本地程序数据库如何与环信数据库对接,比如好友,聊天信息,群组信息
2. 我发信息时,WEB-IM 和 IOS 聊天,用DEMO已经实现及时通信,怎么及时同步到我的数据库中?
3. 添加删除好友增量变化,如何及时同步到我的数据库中?
我是不是可以理解成
比如我添加一个好友,我第一步调用自己程序的方法,把这个操作添加到自己的数据库中,同时再调用环信的方法,就保存在了环信的数据库中?
有没有咱们环信的成功案例,给与我一个最好的解决方案?
如果我使用自己的数据
整体的解决方案是不是这样:
1. 我加载好友及群组数据,数据来源是自己的数据库
2. 我发送消息给好友,需要调用环信的发信息接口
3. 我添加好友删除好友等操作,也是保存到自己的数据库,不需要保存到环信的数据库中
也就是说,我只有在发送和接收消息的时候,才用到环信?
一些问题求解答,确实想使用环信,谢谢
收起阅读 »
1. 我自己的本地程序数据库如何与环信数据库对接,比如好友,聊天信息,群组信息
2. 我发信息时,WEB-IM 和 IOS 聊天,用DEMO已经实现及时通信,怎么及时同步到我的数据库中?
3. 添加删除好友增量变化,如何及时同步到我的数据库中?
我是不是可以理解成
比如我添加一个好友,我第一步调用自己程序的方法,把这个操作添加到自己的数据库中,同时再调用环信的方法,就保存在了环信的数据库中?
有没有咱们环信的成功案例,给与我一个最好的解决方案?
如果我使用自己的数据
整体的解决方案是不是这样:
1. 我加载好友及群组数据,数据来源是自己的数据库
2. 我发送消息给好友,需要调用环信的发信息接口
3. 我添加好友删除好友等操作,也是保存到自己的数据库,不需要保存到环信的数据库中
也就是说,我只有在发送和接收消息的时候,才用到环信?
一些问题求解答,确实想使用环信,谢谢
收起阅读 »
干货:Nginx/LVS/HAProxy 负载均衡软件的优缺点详解
Nginx/LVS/HAProxy是目前使用最广泛的三种负载均衡软件,一般对负载均衡的使用是随着网站规模的提升根据不同的阶段来使用不同的技术,具体的应用需求还得具体分析。
如果是中小型的Web应用,比如日PV小于1000万,用Nginx就完全可以了;如果机器不少,可以用DNS轮询,LVS所耗费的机器还是比较多的;大型网站或重要的服务,且服务器比较多时,可以考虑用LVS。
目前关于网站架构一般比较合理流行的架构方案:Web前端采用Nginx/HAProxy+Keepalived作负载均衡器;后端采用MySQL数据库一主多从和读写分离,采用LVS+Keepalived的架构。当然要根据项目具体需求制定方案,下面说说各自的特点和适用场合。
Nginx
Nginx的优点:
来源:linux中国 收起阅读 »
如果是中小型的Web应用,比如日PV小于1000万,用Nginx就完全可以了;如果机器不少,可以用DNS轮询,LVS所耗费的机器还是比较多的;大型网站或重要的服务,且服务器比较多时,可以考虑用LVS。
目前关于网站架构一般比较合理流行的架构方案:Web前端采用Nginx/HAProxy+Keepalived作负载均衡器;后端采用MySQL数据库一主多从和读写分离,采用LVS+Keepalived的架构。当然要根据项目具体需求制定方案,下面说说各自的特点和适用场合。
Nginx
Nginx的优点:
- 工作在网络的7层之上,可以针对http应用做一些分流的策略,比如针对域名、目录结构,它的正则规则比HAProxy更为强大和灵活,这也是它目前广泛流行的主要原因之一,Nginx单凭这点可利用的场合就远多于LVS了。
- Nginx对网络稳定性的依赖非常小,理论上能ping通就就能进行负载功能,这个也是它的优势之一;相反LVS对网络稳定性依赖比较大,这点用过Nginx的人都深有体会;
- Nginx安装和配置比较简单,测试起来比较方便,它基本能把错误用日志打印出来。LVS的配置、测试就要花比较长的时间了,LVS对网络依赖比较大。
- 可以承担高负载压力且稳定,在硬件不差的情况下一般能支撑几万次的并发量,负载度比LVS相对小些。
- Nginx可以通过端口检测到服务器内部的故障,比如根据服务器处理网页返回的状态码、超时等等,并且会把返回错误的请求重新提交到另一个节点,不过其中缺点就是不支持url来检测。比如用户正在上传一个文件,而处理该上传的节点刚好在上传过程中出现故障,Nginx会把上传切到另一台服务器重新处理,而LVS就直接断掉了,如果是上传一个很大的文件或者很重要的文件的话,用户可能会因此而不满。
- Nginx不仅仅是一款优秀的负载均衡器/反向代理软件,它同时也是功能强大的Web应用服务器。LNMP也是近几年非常流行的web架构,在高流量的环境中稳定性也很好。
- Nginx现在作为Web反向加速缓存越来越成熟了,速度比传统的Squid服务器更快,可以考虑用其作为反向代理加速器。
- Nginx可作为中层反向代理使用,这一层面Nginx基本上无对手,唯一可以对比Nginx的就只有lighttpd了,不过lighttpd目前还没有做到Nginx完全的功能,配置也不那么清晰易读,社区资料也远远没Nginx活跃。
- Nginx也可作为静态网页和图片服务器,这方面的性能也无对手。还有Nginx社区非常活跃,第三方模块也很多。
- Nginx仅能支持http、https和Email协议,这样就在适用范围上面小些,这个是它的缺点。
- 对后端服务器的健康检查,只支持通过端口来检测,不支持通过url来检测。不支持Session的直接保持,但能通过ip_hash来解决。
- 抗负载能力强,工作在网络4层之上,仅作分发之用,没有流量的产生,这个特点也决定了它在负载均衡软件里的性能最强的,对内存和cpu资源消耗比较低。
- 配置性比较低,这是一个缺点也是一个优点,因为没有可太多配置的东西,所以并不需要太多接触,大大减少了人为出错的几率。
- 工作稳定,因为其本身抗负载能力很强,自身有完整的双机热备方案,如LVS+Keepalived,不过我们在项目实施中用得最多的还是LVS/DR+Keepalived。
- 无流量,LVS只分发请求,而流量并不从它本身出去,这点保证了均衡器IO的性能不会收到大流量的影响。
- 应用范围比较广,因为LVS工作在4层,所以它几乎可以对所有应用做负载均衡,包括http、数据库、在线聊天室等等。
- 软件本身不支持正则表达式处理,不能做动静分离;而现在许多网站在这方面都有较强的需求,这个是Nginx/HAProxy+Keepalived的优势所在。
- 如果是网站应用比较庞大的话,LVS/DR+Keepalived实施起来就比较复杂了,特别后面有Windows Server的机器的话,如果实施及配置还有维护过程就比较复杂了,相对而言,Nginx/HAProxy+Keepalived就简单多了。
- HAProxy也是支持虚拟主机的。
- HAProxy的优点能够补充Nginx的一些缺点,比如支持Session的保持,Cookie的引导;同时支持通过获取指定的url来检测后端服务器的状态。
- HAProxy跟LVS类似,本身就只是一款负载均衡软件;单纯从效率上来讲HAProxy会比Nginx有更出色的负载均衡速度,在并发处理上也是优于Nginx的。
- HAProxy支持TCP协议的负载均衡转发,可以对MySQL读进行负载均衡,对后端的MySQL节点进行检测和负载均衡,大家可以用LVS+Keepalived对MySQL主从做负载均衡。
- 5、HAProxy负载均衡策略非常多,HAProxy的负载均衡算法现在具体有如下8种:
- 第一阶段:利用Nginx或HAProxy进行单点的负载均衡,这一阶段服务器规模刚脱离开单服务器、单数据库的模式,需要一定的负载均衡,但是仍然规模较小没有专业的维护团队来进行维护,也没有需要进行大规模的网站部署。这样利用Nginx或HAproxy就是第一选择,此时这些东西上手快, 配置容易,在七层之上利用HTTP协议就可以。这时是第一选择。
- 第二阶段:随着网络服务进一步扩大,这时单点的Nginx已经不能满足,这时使用LVS或者商用Array就是首要选择,Nginx此时就作为LVS或者Array的节点来使用,具体LVS或Array的是选择是根据公司规模和预算来选择,Array的应用交付功能非常强大,本人在某项目中使用过,性价比也远高于F5,商用首选!但是一般来说这阶段相关人才跟不上业务的提升,所以购买商业负载均衡已经成为了必经之路。关人才的能力以及数量也随之提升,这时无论从开发适合自身产品的定制,以及降低成本来讲开源的LVS,已经成为首选,这时LVS会成为主流。
- 第三阶段:这时网络服务已经成为主流产品,此时随着公司知名度也进一步扩展,相关人才的能力以及数量也随之提升,这时无论从开发适合自身产品的定制,以及降低成本来讲开源的LVS,已经成为首选,这时LVS会成为主流。最终形成比较理想的基本架构为:Array/LVS — Nginx/Haproxy — Squid/Varnish — AppServer。
来源:linux中国 收起阅读 »
移动5年 Android生态系统的演进
由Google、HTC、Qualcomm联手打造的第一部Android手机G1,开启了移动时代的Android纪元(如图1所示),直到现在Android也是唯一能在移动市场上与iOS相抗衡的平台。简单地说,Android与iOS占尽了移动时代的先机(这个故事要从2007年的iPhone和2008年的Android G1开始说起,甚至是更早之前的开发史及并购史),App开发者已在Android及iOS上扎下了深厚的根基,因此再也无力也没有必要去为第三个平台开发或移植自己的App,原因很简单,因为受众太少,支出与收入不成正比。
一开始Android生态系统很简单,就是拉拢App开发者,并且寻求与更多芯片制造商及手机厂商的合作,共同推广Android这个开放平台。但随着Android市场占有率逐步攀升,Google开始一步步地收紧及控制生态系的发展,以期自己能在Android平台上获得更多的利益(简单地说,就是收入,Google希望Android能够为自己带来更多的收益),而不再满足于只是打造及提供Android平台的角色。
随着Android一路开疆辟土,出现在越来越多的移动设备上,再加上Google对Android策略的转变,现今的Android生态系统已变得较以往复杂了许多,系统中的角色较以往多了电信运营商、汽车制造商、串流内容及媒体提供商等。Google在全球移动市场的策略也已从“移动优先”转变至“攻占所有屏幕(装置)”,而Android也企图往这个目标上不断迈进(如图2所示)。
时至今日,欧洲、美国、日本的手机制造商节节败退,摩托罗拉、诺基亚、索尼等老牌制造商相继裁员(甚至出售手机及移动业务),而中国手机制造商却不断崛起,中兴、华为、联想、酷派、OPPO、小米等相继杀入手机市场后,如今中国已成为全球最大的智能机生产及销售国,智能机用户数早已超越美国。而新兴的国外市场,如印度、南美、东南亚正刮起一阵低价智能机风暴,Android正是这波低价智能机的推手(如采用MTK芯片的Android One)。低价智能机席卷着新兴国家的市场,而新兴国家的市场也俨然成为智能机的一个重要主战场,Android的野心当然不止于此,Android企图发力于高、中、低阶的智能机市场,而这些细微的变化正一点点地牵动着Android生态系统的转变。
Android的开放与制约
Android一开始由Andy Rubin领军,从一开始的全面开放,到像选妃似的,每次释放出新版Android软件的同时,选择与特定的手机制造商合作开发Android原生机,例如HTC、三星等。这一举动令各家手机制造商对Android仰望备至,都想抱上Android的大腿。因为手机制造商一旦获得与Android合作开发新版原生机的机会,就等于能提早得到新版Android软件代码,进而取得先机来开发其他自家的产品(如三星的Galaxy系列),早对手一步将自家产品推向市场销售,从而取得市场先机。这种方式无疑令部分手机制造商对Android产生反感。
2013年3月,Android从Andy Rubin转由原领导Chrome的Sundar Pichai接手负责,而Android也由工程导向转为营利导向,例如减少手机制造商与Google分成Google Play和Google Search的收入。Android更强烈地主导其UI显示的一致性,并与各家手机制造商签订约束性的协议,强加Google的相关应用服务于Android系统上,甚至将应用摆放在Android桌面上的位置都强加限制,大幅度收紧了各家Android设备制造商分散且碎片化的UI风格,以期用户在汽车、电视、可穿戴设备、手机等所有Android设备上都能得到相同的用户体验,Google此举也造成了部分手机制造商及开发者的反感。
归根结底,Google还是希望自己的角色能像苹果一样介于用户和运营商之间,由目前的“用户/手机品牌商→运营商”,转变为“用户/手机品牌商→Google→运营商”,以期收紧分散且碎片化的Android生态圈,并获取更大的潜在利益。这一点,我们可以在最新发布的Android L版本的软件中看到,Google加入了更多特定运营商所需的功能。
移动互联网时代,Android已然成为Google的一个标准平台,而非过去传闻的与Chrome整合成一个新的平台,或是将Android整合进Chrome中。这种情况下,Android设备制造商如何应对Google的策略调整及Android版本的快速演进,并调整自己迅速定位市场,从而避免自己成为只是帮Google打工的打工仔是至关重要的。Android设备制造商必须打造出自己独特的生态系统(例如跨手机、平板、电视等),而非只是一个设备制造商。这样一来,我们可以清晰地看到,目前纯手机制造商已越发难以生存。
Android系统与架构演进
从Android 1.0至今(更甚至是1.0之前的m*至今),Android系统每一版都有不少的改动(如图3所示),例如HAL有过两次的版本改进、Camera HAL已迭代至第三版、多媒体核心由一开始的OpenCore直到现在的Stagefright。而一开始的WebView至今则完全被Chrome取代,Android也从ARM一路拓展到x86及MIPS平台。版本演进的过程及内容实在太多,如果真的要巨细靡遗地写,可能三天三夜也写不完,所以我想仅对每一版本的改动给予一句代表性的描述,并在最后针对Android的最新版(代号L),做一些基本介绍。
Android 1.0:第一部Android手机以及Google Apps的诞生;
Android 1.5 Cupcake:支持软件键盘;
Android 1.6 Donut:支持CDMA;
Android 2.0 Éclair:GPS大放异彩,GoogleNexus One手机诞生;
Android 2.2 Froyo:加入语音识别功能;
Android 2.3 Gingerbread:Nexus S;
Android 3.0 Honeycomb:第一次专给平板设备设计UI;
Android 4.0 Ice Cream Sandwich:一个新的UI界面Holo与内嵌字体Roboto,并支持人脸解锁;
Android 4.1 Jelly Bean:Nexus 7:Google官方第一台Android平板设备;
Android 4.3 Jelly Bean:引入对可穿戴设备的支持;
Android 4.4 KitKat:支持更少内存的移动设备。
接下来是Android L,它的预设虚拟机为ART,支持64位,所需要的系统空间较以往的Android版本更大,因为ART有一个转换档案格式的动作,其缺点就是会占用更多的系统空间,但之后软件的执行速度可能有一定的提升。另外,Android L可能会加入部分地区及运营商的特殊需求,例如multi-SIM、NFC、Wi-Fi等;此外,Android L也更强调系统安全,它可以预设为强制性安全模式,即一般应用启动时需要多道的身分及权限确认才能执行。
时至今日,Android身影几乎无处不在,回顾以往,Android系统的演进总是先求有、再求好,智能移动时代发展至今,全球正迈向下一个里程碑,希望Android此时能不忘初心,做一个平衡生态系统的维护者及领导者,而非仅是利益上的掮客,不要为了利益而让这个生态圈走向封闭。
作为开发者,此时更应该思考自己拥有如此巨大的市场优势、健全的物流体系及现金流系统,以及众多的手机及移动设备制造商,如果能积极培养系统级的软件人才,有条件打造出一个来自中国的自主生态系统,以期将来与Android分庭抗礼。
作者:钟文昌 收起阅读 »
一开始Android生态系统很简单,就是拉拢App开发者,并且寻求与更多芯片制造商及手机厂商的合作,共同推广Android这个开放平台。但随着Android市场占有率逐步攀升,Google开始一步步地收紧及控制生态系的发展,以期自己能在Android平台上获得更多的利益(简单地说,就是收入,Google希望Android能够为自己带来更多的收益),而不再满足于只是打造及提供Android平台的角色。
随着Android一路开疆辟土,出现在越来越多的移动设备上,再加上Google对Android策略的转变,现今的Android生态系统已变得较以往复杂了许多,系统中的角色较以往多了电信运营商、汽车制造商、串流内容及媒体提供商等。Google在全球移动市场的策略也已从“移动优先”转变至“攻占所有屏幕(装置)”,而Android也企图往这个目标上不断迈进(如图2所示)。
时至今日,欧洲、美国、日本的手机制造商节节败退,摩托罗拉、诺基亚、索尼等老牌制造商相继裁员(甚至出售手机及移动业务),而中国手机制造商却不断崛起,中兴、华为、联想、酷派、OPPO、小米等相继杀入手机市场后,如今中国已成为全球最大的智能机生产及销售国,智能机用户数早已超越美国。而新兴的国外市场,如印度、南美、东南亚正刮起一阵低价智能机风暴,Android正是这波低价智能机的推手(如采用MTK芯片的Android One)。低价智能机席卷着新兴国家的市场,而新兴国家的市场也俨然成为智能机的一个重要主战场,Android的野心当然不止于此,Android企图发力于高、中、低阶的智能机市场,而这些细微的变化正一点点地牵动着Android生态系统的转变。
Android的开放与制约
Android一开始由Andy Rubin领军,从一开始的全面开放,到像选妃似的,每次释放出新版Android软件的同时,选择与特定的手机制造商合作开发Android原生机,例如HTC、三星等。这一举动令各家手机制造商对Android仰望备至,都想抱上Android的大腿。因为手机制造商一旦获得与Android合作开发新版原生机的机会,就等于能提早得到新版Android软件代码,进而取得先机来开发其他自家的产品(如三星的Galaxy系列),早对手一步将自家产品推向市场销售,从而取得市场先机。这种方式无疑令部分手机制造商对Android产生反感。
2013年3月,Android从Andy Rubin转由原领导Chrome的Sundar Pichai接手负责,而Android也由工程导向转为营利导向,例如减少手机制造商与Google分成Google Play和Google Search的收入。Android更强烈地主导其UI显示的一致性,并与各家手机制造商签订约束性的协议,强加Google的相关应用服务于Android系统上,甚至将应用摆放在Android桌面上的位置都强加限制,大幅度收紧了各家Android设备制造商分散且碎片化的UI风格,以期用户在汽车、电视、可穿戴设备、手机等所有Android设备上都能得到相同的用户体验,Google此举也造成了部分手机制造商及开发者的反感。
归根结底,Google还是希望自己的角色能像苹果一样介于用户和运营商之间,由目前的“用户/手机品牌商→运营商”,转变为“用户/手机品牌商→Google→运营商”,以期收紧分散且碎片化的Android生态圈,并获取更大的潜在利益。这一点,我们可以在最新发布的Android L版本的软件中看到,Google加入了更多特定运营商所需的功能。
移动互联网时代,Android已然成为Google的一个标准平台,而非过去传闻的与Chrome整合成一个新的平台,或是将Android整合进Chrome中。这种情况下,Android设备制造商如何应对Google的策略调整及Android版本的快速演进,并调整自己迅速定位市场,从而避免自己成为只是帮Google打工的打工仔是至关重要的。Android设备制造商必须打造出自己独特的生态系统(例如跨手机、平板、电视等),而非只是一个设备制造商。这样一来,我们可以清晰地看到,目前纯手机制造商已越发难以生存。
Android系统与架构演进
从Android 1.0至今(更甚至是1.0之前的m*至今),Android系统每一版都有不少的改动(如图3所示),例如HAL有过两次的版本改进、Camera HAL已迭代至第三版、多媒体核心由一开始的OpenCore直到现在的Stagefright。而一开始的WebView至今则完全被Chrome取代,Android也从ARM一路拓展到x86及MIPS平台。版本演进的过程及内容实在太多,如果真的要巨细靡遗地写,可能三天三夜也写不完,所以我想仅对每一版本的改动给予一句代表性的描述,并在最后针对Android的最新版(代号L),做一些基本介绍。
Android 1.0:第一部Android手机以及Google Apps的诞生;
Android 1.5 Cupcake:支持软件键盘;
Android 1.6 Donut:支持CDMA;
Android 2.0 Éclair:GPS大放异彩,GoogleNexus One手机诞生;
Android 2.2 Froyo:加入语音识别功能;
Android 2.3 Gingerbread:Nexus S;
Android 3.0 Honeycomb:第一次专给平板设备设计UI;
Android 4.0 Ice Cream Sandwich:一个新的UI界面Holo与内嵌字体Roboto,并支持人脸解锁;
Android 4.1 Jelly Bean:Nexus 7:Google官方第一台Android平板设备;
Android 4.3 Jelly Bean:引入对可穿戴设备的支持;
Android 4.4 KitKat:支持更少内存的移动设备。
接下来是Android L,它的预设虚拟机为ART,支持64位,所需要的系统空间较以往的Android版本更大,因为ART有一个转换档案格式的动作,其缺点就是会占用更多的系统空间,但之后软件的执行速度可能有一定的提升。另外,Android L可能会加入部分地区及运营商的特殊需求,例如multi-SIM、NFC、Wi-Fi等;此外,Android L也更强调系统安全,它可以预设为强制性安全模式,即一般应用启动时需要多道的身分及权限确认才能执行。
时至今日,Android身影几乎无处不在,回顾以往,Android系统的演进总是先求有、再求好,智能移动时代发展至今,全球正迈向下一个里程碑,希望Android此时能不忘初心,做一个平衡生态系统的维护者及领导者,而非仅是利益上的掮客,不要为了利益而让这个生态圈走向封闭。
作为开发者,此时更应该思考自己拥有如此巨大的市场优势、健全的物流体系及现金流系统,以及众多的手机及移动设备制造商,如果能积极培养系统级的软件人才,有条件打造出一个来自中国的自主生态系统,以期将来与Android分庭抗礼。
作者:钟文昌 收起阅读 »
2015 年 6 月 RedMonk 编程语言排行榜
RedMonk 发布了 2015 年 6 月的编程语言排行榜,JavaScript 居榜首,Go,Swift 继续上升,函数式编程语言 Scala,Haskell 和 Clojure 上升明显。
现在已经是 2015 的第三个季度了,RedMonk 发布了一年两次的编程语言排行榜,一如既往,跟 Drew Conway 和 John Myles White 在 2010 分析的过程差不多,是根据编程语言在 GitHub 和 Stack Overflow 上讨论的多少和使用量来统计的,可以预测未来编程语言的发展趋势。
排名的根据是编程语言在 Stack Overflow 和 GitHub 都观察过,同时结合很多其他社区的分析统计,GitHub 主要是根据代码行数统计。
下图是 2015 年 6 月的排行榜图表
根据上面的图表,很难分析大体情况,所以提供了下面的数值排名。注意下面列出的是前 21 个编程语言:
1 JavaScript
2 Java
3 PHP
4 Python
5 C#
5 C++
5 Ruby
8 CSS
9 C
10 Objective-C
11 Perl
11 Shell
13 R
14 Scala
15 Go
15 Haskell
17 Matlab
18 Swift
19 Clojure
19 Groovy
19 Visual Basic
跟上一季度一样,JavaScript 比第二的 Java 只是稍稍领先了一点,这些数值差距是非常微小的。这些能体现编程语言的持久热度,但是也一定程度反映了语言的多样性和在企业和初创企业中的作用。
除了这两个语言,前十的语言位置非常稳固的。除了一些小的改变,事实上这些年都差不多是这样的排名。同时发现这是一定周期支持一种特定的语言或者是一种风格的语言,简单的来说,最受欢迎的语言几乎没什么改变,对未来也没什么倾向性的变化,这是不是意味着语言的采用和语言的分化已经达到了顶峰?
除了前十,值得关注的变化有:
在 我们官方排名中,当然会要求完整的 Stack Overflow 数据,所以 WWDC 之后又收集了一次数据,最新的结果 Swift 从第 21 名升至第 18 名。这就是 WWDC 效应,Swift 排名历史从 68,22 到 18,成为第一个在一年内挺近前 20 的语言。
未来
Go 和 Swift 是前十的种子选手,这也许只是时间问题,我们将会继续关注!Go 也许会取代 Objective C, Perl, Shell, R 和 Scala 的位置。Perl 和 Shell 无处不在,但是频率却不够高;R 和 Scala 非常流行,但是使用范围不够广泛。
Go 成为一个非常受欢迎的现代化后端语言,Swift 也在 iOS 占有一席之地,下一次排名应该会更有趣~ 收起阅读 »
现在已经是 2015 的第三个季度了,RedMonk 发布了一年两次的编程语言排行榜,一如既往,跟 Drew Conway 和 John Myles White 在 2010 分析的过程差不多,是根据编程语言在 GitHub 和 Stack Overflow 上讨论的多少和使用量来统计的,可以预测未来编程语言的发展趋势。
排名的根据是编程语言在 Stack Overflow 和 GitHub 都观察过,同时结合很多其他社区的分析统计,GitHub 主要是根据代码行数统计。
下图是 2015 年 6 月的排行榜图表
根据上面的图表,很难分析大体情况,所以提供了下面的数值排名。注意下面列出的是前 21 个编程语言:
1 JavaScript
2 Java
3 PHP
4 Python
5 C#
5 C++
5 Ruby
8 CSS
9 C
10 Objective-C
11 Perl
11 Shell
13 R
14 Scala
15 Go
15 Haskell
17 Matlab
18 Swift
19 Clojure
19 Groovy
19 Visual Basic
跟上一季度一样,JavaScript 比第二的 Java 只是稍稍领先了一点,这些数值差距是非常微小的。这些能体现编程语言的持久热度,但是也一定程度反映了语言的多样性和在企业和初创企业中的作用。
除了这两个语言,前十的语言位置非常稳固的。除了一些小的改变,事实上这些年都差不多是这样的排名。同时发现这是一定周期支持一种特定的语言或者是一种风格的语言,简单的来说,最受欢迎的语言几乎没什么改变,对未来也没什么倾向性的变化,这是不是意味着语言的采用和语言的分化已经达到了顶峰?
除了前十,值得关注的变化有:
- Go:一年前,我们预言 Go 在 6 -12 个月的时间内会成为前 20 的语言。在 1 月份的排行中 Go 成为了第 17 位,预言成真。现在 Go 是第 15 位,超越了 Haskell 和 Matlab。
- Erlang:这是并发方面开发者长期选择的一种语言。Erlang 之前从第 26 位升到第 25 位,这主要是两周前 Erlang 抛弃了之前的 MPL 协议,选择了 Apache 协议。
- Julia/Rust::历史性原因,这两个语言的发展轨迹很相似。上一季度,Rust 向上跳了 8 位,Julia 向上跳了 3 位。此次排名 Julia 比之前上升了 2 位到第 52 位,Rust 向上跳了 2 位到第 48 位。继续保持观望!
- CoffeeScript::2013 年 Q3 排名 17,之后排名 18,18,21,现在是 22。重回前 20 这不是不可能的,至少找到了立足点和稳定的地位,但是前景并不乐观,因其缺乏动力和竞争。
- Dart / Visual Basic: 这是经常被问到的两个语言。Visual Basic 现在跟 Clojure,Groovy 排名第 19,未来是否还会在前 20 还不是很明朗。Dart,有着 Google 血统,还有 JavaScript 方面的野心,还在稳定增长中,比 Google 的另一个语言 Go 稍稍落后那么一点点,现在是从第 34 位升到第 33 位。
- Swift:这个月的 排名因为某些原因有些小小的问题。在几方的要求下,苹果 WWDC 前,我们去看 Swift 是否从第 68 升至第 22 位。不幸的是,因为 Stack Overflow 页面结构的改变,数据抓取失败,所以只能手动查看,缩小了范围,Swift 直接在前 20 位以后,排名 21。
在 我们官方排名中,当然会要求完整的 Stack Overflow 数据,所以 WWDC 之后又收集了一次数据,最新的结果 Swift 从第 21 名升至第 18 名。这就是 WWDC 效应,Swift 排名历史从 68,22 到 18,成为第一个在一年内挺近前 20 的语言。
未来
Go 和 Swift 是前十的种子选手,这也许只是时间问题,我们将会继续关注!Go 也许会取代 Objective C, Perl, Shell, R 和 Scala 的位置。Perl 和 Shell 无处不在,但是频率却不够高;R 和 Scala 非常流行,但是使用范围不够广泛。
Go 成为一个非常受欢迎的现代化后端语言,Swift 也在 iOS 占有一席之地,下一次排名应该会更有趣~ 收起阅读 »
技术干货:网络性能测试
实时音视频这种实时业务一般用udp传输数据,其对网络性能是非常敏感的,在实战中,经常需要测试当前端到端或端到云的网络性能。在这里我们讨论一下网络性能测试中所涉及到指标,技术和相关工具,以及如何编写自己的网络性能测试工具。
性能指标
先给出几个比较重要的指标的定义以及它们的意义。
带宽(吞吐量)
测试工具
网上有很多测试网络性能的工具,如果它们能满足需求的话,就不用自己再造轮子了。
ping
ping是最常见的,几乎在所有的OS上都有它的存在。 其工作原理如图
Local发送的数据包,Remote收到数据包后原样发回来;
数据包里包含有序号和时间戳信息;
序号用于判断是否丢包;
时间戳用于计算来回时延(图中蓝色部分),它等于接收时间减去数据包时间戳;
不同OS的ping命令选项可能会略有差别,以Mac OSX的ping为例
其中 -s 1024 指示包的大小为1024字节;从ping结果可以看出,发送了30个包,收到29个包,3.3%的丢包率,最小时延77.843毫秒,最大时延141.314毫秒,平均时延95.375毫秒,时延的标准差19.167。另外,ping用的是ICMP协议,网络对ICMP协议处理性能,可能跟UDP或TCP是不一样的,所以测试结果只能做为参考。
小结:ping的优点是简单便捷,可以测试时延和丢包,缺点是无法测试带宽。
iperf
iperf功能功能强一些,可以测带宽,丢包,抖动, 但是测不了时延。它的工作原理如图:
服务端:
客户端:
其中 -b 16K 指定了带宽参数。测试结果为丢包0个,平均抖动为0.046毫秒。
自己开发
从上面可以看出,ping和iperf各有优缺点,通常需要两者组合才能满足我们的需求。有时候现有工具不能满足实际应用的需求,比如说完全模拟实际业务环境或者在产品里集成测试功能,这时候就需要发挥“自己动手,丰衣足食”的精神,造出一个适合自己用的轮子来。我们这里只讨论关键点之一:如何匀速发送数据。
我们以设定发送包长为1024字节,带宽为64kbps为例子,讨论发送数据的实现方案。
发送数据最简单的方法就是,起一个线程,每秒直接发送完当前秒的数据,然后sleep一秒,再继续发送,如下:
这种方法比较简单,但是因为发送数据是需要花费时间的,假如发送64Kbit花费了5毫秒,实际发送码率(带宽)为64/1005≈63.68Kbps,比设定值低一些。把发送时间考虑在内,第2个改进后的代码版本如下:
从大尺度上看,这个版本确实会按设定带宽发送数据,但从小的的时间片上看,其瞬时发送速率是非常高的。假如发送64Kbit花费了5毫秒,则瞬时速率为 64*1000/5=12800Kbps,是设定值的20倍。这种瞬时高发送速率可能会导致网络中某些路由器或交换机来不及处理而大量丢包。所以我们继续改进,在每发送一个包时check是否发送太快,如果发送太快的话就sleep一下缓一缓。改进后的第三个版本如下:
这个版本基本能够按照设定值匀速发送数据了。当然,它还不是最完美的,当设定带宽很高而包长很小时,会导致太多的check,占用太多CPU。这里就不继续改进了,有兴趣的看官可以自己实现之。
作者:符宁 收起阅读 »
性能指标
先给出几个比较重要的指标的定义以及它们的意义。
带宽(吞吐量)
- 单位时间内传输的数据量,单位通常是每秒比特数,记作bps;
- 带宽反映了网络的传输能力,越大越好;
- 数据包丢失个数,等于“发送数据包数” - “接受数据包数”;
- 丢包反映了网络可靠性,越小越好;
- 数据包从发送开始到接收到该数据所耗费的时间,单位通常是毫秒;
- 时延反映了网络的速度,越小越好;
- 指时延的变化,即两个数据包时延的差值;
- 抖动反映了网络的稳定性,越小越好;
- 指接收到的数据包顺序和发送顺序不一致的次数;
- 乱序反映了网络的稳定性,越小越好;
- 当乱序比较严重时,丢包也会比较严重,所以一般都以丢包指标为主,忽略乱序指标;
测试工具
网上有很多测试网络性能的工具,如果它们能满足需求的话,就不用自己再造轮子了。
ping
ping是最常见的,几乎在所有的OS上都有它的存在。 其工作原理如图
Local发送的数据包,Remote收到数据包后原样发回来;
数据包里包含有序号和时间戳信息;
序号用于判断是否丢包;
时间戳用于计算来回时延(图中蓝色部分),它等于接收时间减去数据包时间戳;
不同OS的ping命令选项可能会略有差别,以Mac OSX的ping为例
$ping -s 1024 192.168.1.100
PING www.microsoft.com (23.42.217.205): 1024 data bytes
1032 bytes from 23.42.217.205: icmp_seq=0 ttl=49 time=83.883 ms
1032 bytes from 23.42.217.205: icmp_seq=1 ttl=49 time=77.958 ms
1032 bytes from 23.42.217.205: icmp_seq=2 ttl=49 time=80.053 ms
1032 bytes from 23.42.217.205: icmp_seq=3 ttl=49 time=78.244 ms
1032 bytes from 23.42.217.205: icmp_seq=4 ttl=49 time=77.937 ms
...
^C
--- 192.168.1.100 ping statistics ---
30 packets transmitted, 29 packets received, 3.3% packet loss
round-trip min/avg/max/stddev = 77.843/95.375/141.314/19.167 ms
其中 -s 1024 指示包的大小为1024字节;从ping结果可以看出,发送了30个包,收到29个包,3.3%的丢包率,最小时延77.843毫秒,最大时延141.314毫秒,平均时延95.375毫秒,时延的标准差19.167。另外,ping用的是ICMP协议,网络对ICMP协议处理性能,可能跟UDP或TCP是不一样的,所以测试结果只能做为参考。
小结:ping的优点是简单便捷,可以测试时延和丢包,缺点是无法测试带宽。
iperf
iperf功能功能强一些,可以测带宽,丢包,抖动, 但是测不了时延。它的工作原理如图:
服务端:
$iperf -u -s -p 12345 -i 1 -w 1000000
------------------------------------------------------------
Server listening on UDP port 12345
Receiving 1470 byte datagrams
UDP buffer size: 977 KByte
------------------------------------------------------------
客户端:
$ iperf -u -c 127.0.0.1 -p 12345 -i 1 -t 5 -b 16K -l 62
------------------------------------------------------------
Client connecting to 127.0.0.1, UDP port 12345
Sending 62 byte datagrams
UDP buffer size: 9.00 KByte (default)
------------------------------------------------------------
[ 4] local 127.0.0.1 port 59805 connected with 127.0.0.1 port 12345
[ ID] Interval Transfer Bandwidth
[ 4] 0.0- 1.0 sec 2.00 KBytes 16.4 Kbits/sec
[ 4] 1.0- 2.0 sec 1.94 KBytes 15.9 Kbits/sec
[ 4] 2.0- 3.0 sec 1.94 KBytes 15.9 Kbits/sec
[ 4] 3.0- 4.0 sec 1.94 KBytes 15.9 Kbits/sec
[ 4] 4.0- 5.0 sec 2.00 KBytes 16.4 Kbits/sec
[ 4] 0.0- 5.1 sec 9.87 KBytes 16.0 Kbits/sec
[ 4] Sent 163 datagrams
[ 4] Server Report:
[ 4] 0.0- 5.1 sec 9.87 KBytes 16.0 Kbits/sec 0.046 ms 0/ 163 (0%)
其中 -b 16K 指定了带宽参数。测试结果为丢包0个,平均抖动为0.046毫秒。
自己开发
or(unsigned second = 0; second < test_seconds; second++)
{
for(unsigned ui = 0; ui < 8; ui++)
{
sendto 1024 bytes;
}
msleep(1000);
}
从上面可以看出,ping和iperf各有优缺点,通常需要两者组合才能满足我们的需求。有时候现有工具不能满足实际应用的需求,比如说完全模拟实际业务环境或者在产品里集成测试功能,这时候就需要发挥“自己动手,丰衣足食”的精神,造出一个适合自己用的轮子来。我们这里只讨论关键点之一:如何匀速发送数据。
我们以设定发送包长为1024字节,带宽为64kbps为例子,讨论发送数据的实现方案。
发送数据最简单的方法就是,起一个线程,每秒直接发送完当前秒的数据,然后sleep一秒,再继续发送,如下:
这种方法比较简单,但是因为发送数据是需要花费时间的,假如发送64Kbit花费了5毫秒,实际发送码率(带宽)为64/1005≈63.68Kbps,比设定值低一些。把发送时间考虑在内,第2个改进后的代码版本如下:
for(unsigned second = 0; second < test_seconds; second++)
{
unsigned ts_start = gettimestamp();
for(unsigned ui = 0; ui < 8; ui++)
{
sendto 1024 bytes;
}
unsigned elapsed = gettimestamp() - ts_start;
msleep(1000-elapsed);
}
从大尺度上看,这个版本确实会按设定带宽发送数据,但从小的的时间片上看,其瞬时发送速率是非常高的。假如发送64Kbit花费了5毫秒,则瞬时速率为 64*1000/5=12800Kbps,是设定值的20倍。这种瞬时高发送速率可能会导致网络中某些路由器或交换机来不及处理而大量丢包。所以我们继续改进,在每发送一个包时check是否发送太快,如果发送太快的话就sleep一下缓一缓。改进后的第三个版本如下:
uint64_t sent_bytes = 0;
unsigned kick_time = gettimestamp();
for(unsigned second = 0; second < test_seconds; second++) { sendto 1024 bytes; sent_bytes += 1024; unsigned elapsed = gettimestamp() - kick_time; unsigned normal = sent_bytes * 1000 * 8 / (64*1000); if(normal > elapsed)
{
msleep(normal-elapsed);
}
}
这个版本基本能够按照设定值匀速发送数据了。当然,它还不是最完美的,当设定带宽很高而包长很小时,会导致太多的check,占用太多CPU。这里就不继续改进了,有兴趣的看官可以自己实现之。
作者:符宁 收起阅读 »
Airbnb开源的三个大数据神器
7月5日 Airbnb召开了第一次开放技术大会OpenAir,重点是数据驱动在airbnb开发过程中的实践,在大会上,Airbnb开源了3个大数据神器
Airpal
第一个神器叫Airpal,是airbnb内部最炙手可热的数据分析工具,目前在github上面有900多个star。
Airpal是建立在Facebook的Prestodb上的一个可视化分布式SQL查询引擎。Airbnb现在大概有1.5PB的数据。传统上是可以用hive查询,但Hive有以下几个缺点。
第一是对于一些小规模的query,map reduce的overhead太大,比如我就想看一张表的前10行,select * from * limit 10 Hive会触发一个map reduce job,然后半分钟过去了还在map阶段。。。而Airpal背后采用的prestodb则没有这个问题,并且Airpal提供对一个表的数据预览。
Hive的第二个缺点是对于非技术人士不大友好,而airpal是图形界面,只要会sql就可以使用,结果直接生成一个csv文件。很多非技术部门,比如finance的分析员需要做大数据分析的时候,Airpal会非常方便。据我观察,在airbnb,数据科学家还是喜欢用命令行的hive,而非技术人士,或者需要做一些简单查询的工程师和产品经理,则多用airpal
Airpal还有个好处是可以直接和公司的LDAP相连,员工用LDAP登陆,可以直接设置相应的访问权限,使得全公司可以放心用一套数据分析系统
Aerosolve
Aerosolve是支撑Airbnb定价建议系统的机器学习引擎。
传统的机器学习引擎更像一个黑箱,很难知道是哪一个feature对最后的结果产生 了最大的影响。比如Airbnb上的房东设定价格后,我们不仅是希望提示这个价格是过高或过低(模型判断结果),而是希望给房东具体的原因,比如位置太偏,或者评价数不够多(feature的权重)。
比如下图就说明了评价数量以及三星评价数量对价格的影响。我们(惊奇)的发现,一个评价和15个评价的效果差不多,房东并不会因为有更多的评价而得到更多的订单,而3星评价甚至会起到副作用
Airflow
大数据的基础还是data pipeline。Airflow则是Airbnb内部发起、排序、监控data pipeline的工具。
来源:用友开发者 收起阅读 »
Airpal
第一个神器叫Airpal,是airbnb内部最炙手可热的数据分析工具,目前在github上面有900多个star。
Airpal是建立在Facebook的Prestodb上的一个可视化分布式SQL查询引擎。Airbnb现在大概有1.5PB的数据。传统上是可以用hive查询,但Hive有以下几个缺点。
第一是对于一些小规模的query,map reduce的overhead太大,比如我就想看一张表的前10行,select * from * limit 10 Hive会触发一个map reduce job,然后半分钟过去了还在map阶段。。。而Airpal背后采用的prestodb则没有这个问题,并且Airpal提供对一个表的数据预览。
Hive的第二个缺点是对于非技术人士不大友好,而airpal是图形界面,只要会sql就可以使用,结果直接生成一个csv文件。很多非技术部门,比如finance的分析员需要做大数据分析的时候,Airpal会非常方便。据我观察,在airbnb,数据科学家还是喜欢用命令行的hive,而非技术人士,或者需要做一些简单查询的工程师和产品经理,则多用airpal
Airpal还有个好处是可以直接和公司的LDAP相连,员工用LDAP登陆,可以直接设置相应的访问权限,使得全公司可以放心用一套数据分析系统
Aerosolve
Aerosolve是支撑Airbnb定价建议系统的机器学习引擎。
传统的机器学习引擎更像一个黑箱,很难知道是哪一个feature对最后的结果产生 了最大的影响。比如Airbnb上的房东设定价格后,我们不仅是希望提示这个价格是过高或过低(模型判断结果),而是希望给房东具体的原因,比如位置太偏,或者评价数不够多(feature的权重)。
比如下图就说明了评价数量以及三星评价数量对价格的影响。我们(惊奇)的发现,一个评价和15个评价的效果差不多,房东并不会因为有更多的评价而得到更多的订单,而3星评价甚至会起到副作用
Airflow
大数据的基础还是data pipeline。Airflow则是Airbnb内部发起、排序、监控data pipeline的工具。
来源:用友开发者 收起阅读 »
2015 年度开源项目新秀榜
黑鸭(Black Duck)软件公布了一份名叫“年度开源项目新秀”的报告,介绍了由全球开源协会发起的10个最有趣、最活跃的新项目。
年度开源项目新秀
每年都有上千新的开源项目问世,但只有少数能够真正的吸引我们的关注。一些项目因为利用了当前比较流行的技术而发展壮大,有一些则真正地开启了一个新的领域。很多开源项目建立的初衷是为了解决一些生产上的问题,还有一些项目则是世界各地志同道合的开发者们共同发起的一个宏伟项目。
从2009年起,开源软件管理公司黑鸭便发起了 年度开源项目新秀 这一活动,它的评选根据 Open Hub 网站(即以前的Ohloh)上的活跃度。今年,我们很荣幸能够报道2015年10大开源项目新秀的得主和2名荣誉奖得主,它们是从上千个开源项目中脱颖而出的。评选采用了加权评分系统,得分标准基于项目的活跃度,交付速度和几个其它因数。
开源俨然成为了产业创新的引擎,就拿今年来说,和Docker容器相关的开源项目在全球各地兴起,这也不恰巧反映了企业最感兴趣的技术领域吗?最后,我们接下来介绍的项目,将会让你了解到全球开源项目的开发者们的在思考什么,这很快将会成为一个指引我们发展的领头羊。
2015年度开源项目新秀: DebOps
DebOps 收集打包了一套 Ansible 方案和规则(Ansible是一种自动化运维工具),可以从1个容器扩展到一个完整的数据中心。它的创始人Maciej Delmanowski将DebOps开源来保证项目长久进行,从而更好的通过外部贡献者的帮助发展下去。
DebOps始创于波兰的一个不起眼大学校园里,他们运营自己的数据中心,一切工作都采用手工配置。有时系统崩溃而导致几天的宕机,这时Delmanowski意识到一个配置管理系统是很有必要的。以Debian作为基础开始,DebOps是一组配置一整个数据基础设施的Ansible方案。此项目已经在许多不同的工作环境下实现,而创始者们则打算继续支持和改进这个项目。
2015年度开源项目新秀: Code Combat
传统的纸笔学习方法已近不能满足技术学科了。然而游戏却有很多人都爱玩,这也就是为什么 CodeCombat 的创始人会去开发一款多人协同编程游戏来教人们如何编码。
刚开始CodeCombat是一个创业想法,但其创始人决定取而代之创建一个开源项目。此想法在社区传播开来,很快不少贡献者加入到项目中来。项目发起仅仅两个月后,这款游戏就被接纳到Google’s Summer of Code活动中。这款游戏吸引了大量玩家,并被翻译成45种语言。CodeCombat希望成为那些想要一边学习代码同时获得乐趣的同学的风向标。
2015年度开源项目新秀: Storj
Storj 是一个点对点的云存储网络,可实现端到端加密,保证用户不用依赖第三方即可传输和共享数据。基于比特币block chain技术和点对点协议,Storj提供安全、私密、加密的云存储。
云数据存储的反对者担心成本开销和漏洞攻击。针对这两个担忧,Storj提供了一个私有云存储市场,用户可以通过Storjcoin X(SJCX) 购买交易存储空间。上传到Storj的文件会被粉碎、加密和存储到整个社区。只有文件所有者拥有密钥加密的信息。
在2014年举办的Texas Bitcoin Conference Hackathon会议上,去中心化的云存储市场概念首次被提出并证明可行。在第一次赢得黑客马拉松活动后,项目创始人们和领导团队利用开放论坛、Reddit、比特币论坛和社交媒体增长成了一个活跃的社区,如今,它们已成为影响Storj发展方向的一个重要组成部分。
2015年度开源项目新秀: Neovim
自1991年出现以来,Vim已经成为数以百万计软件开发人员所钟爱的文本编辑器。 而 Neovim 就是它的下一个版本。
在过去的23年里,软件开发生态系统经历了无数增长和创新。Neovim创始人Thiago de Arruda认为Vim缺乏当代元素,跟不上时代的发展。在保留Vim的招牌功能的前提下,Neovim团队同样在寻求改进和发展这个最受欢迎的文本编辑器的技术。早期众筹让Thiago de Arruda可以连续6个月时间投入到此项目。他相信Neovim社区会支持这个项目,激励他继续开发Neovim。
2015年度开源项目新秀: CockroachDB
前谷歌员工开发了一个开源的大型企业数据存储项目 CockroachDB ,它是一个可扩展的、跨地域复制且支持事务的数据存储的解决方案。
为了保证在线的百万兆字节流量业务的质量,Google开发了Spanner系统,这是一个可扩展的,稳定的,支持事务的系统。许多参与开发CockroachDB的团队现在都服务于开源社区。就像真正的蟑螂(cockroach)一样,CockroachDB可以在没有数据头、任意节点失效的情况下正常运行。这个开源项目有很多富有经验的贡献者,创始人们通过社交媒体、Github、网络、会议和聚会结识他们并鼓励他们参与其中。
2015年度开源项目新秀: Kubernetes
在将容器化软件到引入开源社区发展时, Docker 是一匹黑马,它创新了一套技术和工具。去年6月谷歌推出了 Kubernetes ,这是一款开源的容器管理工具,用来加快开发和简化操作。
谷歌在它的内部运营上使用容器技术多年了。在2014年夏天的DockerCon上大会上,谷歌这个互联网巨头开源了Kubernetes,Kubernetes的开发是为了满足迅速增长的Docker生态系统的需要。通过和其它的组织、项目合作,比如Red Hat和CoreOS,Kubernetes项目的管理者们推动它登上了Docker Hub的工具下载榜榜首。Kubernetes的开发团队希望扩大这个项目,发展它的社区,这样的话软件开发者就能花更少的时间在管理基础设施上,而更多的去开发他们自己的APP。
2015年度开源项目新秀: Open Bazaar
OpenBazaar是一个使用比特币与其他人交易的去中心化的市场。OpenBazaar这一概念最早在编程马拉松(hackathon)活动中被提出,它的创始人结合了BitTorent、比特币和传统的金融服务方式,创造了一个不受审查的交易平台。OpenBazaar的开发团队在寻求新的成员,而且不久以后他们将极度扩大Open Bazaar社区。Open Bazaar的核心是透明度,其创始人和贡献者的共同目标是在商务交易中掀起一场革命,让他们向着一个真实的、一个无控制的,去中心化的市场奋进。
2015年度开源项目新秀: IPFS
IPFS 是一个面向全球的、点对点的分布式版本文件系统。它综合了Git,BitTorrent,HTTP的思想,开启了一个新的数据和数据结构传输协议。
人们所知的开源,它的本意用简单的方法解决复杂的问题,这样产生许多新颖的想法,但是那些强大的项目仅仅是开源社区的冰山一角。IFPS有一个非常激进的团队,这个概念的提出是大胆的,令人惊讶的,有点甚至高不可攀。看起来,一个点对点的分布式文件系统是在寻求将所有的计算设备连在一起。这个可能的 HTTP 替换品通过多种渠道维护着一个社区,包括Git社区和超过100名贡献者的IRC。这个疯狂的想法将在2015年进行软件内部测试。
2015年度开源项目新秀: cAdvisor
cAdvisor (Container Advisor) 是一个针对在运行中的容器进行收集,统计,处理和输出信息的工具,它可以给容器的使用者提供资源的使用情况和工作特性。对于每一个容器,cAdvisor记录着资源的隔离参数,资源使用历史,资源使用历史对比框图,网络状态。这些从容器输出的数据跨越主机传递。
cAdvisor可以在绝大多数的Linux发行版上运行,并且支持包括Docker在内的多种容器类型。事实上它成为了一种容器的代理,并被集成在了很多系统中。cAdvisor在DockerHub下载量也是位居前茅。cAdvisor的开发团队希望把cAdvisor改进到能够更深入地理解应用性能,并且集成到集群系统。
2015年度开源项目新秀: Terraform
Terraform 提供了一些常见设置来创建一个基础设施,从物理机到虚拟机,以及email服务器、DNS服务器等。这个想法包括从家庭个人机解决方案到公共云平台提供的服务。一旦建立好了以后,Terraform可以让运维人员安全又高效地改变你的基础设施,就如同配置一样。
Terraform.io的创始者工作在一个Devops模式的公司,他找到了一个窍门把建立一个完整的数据中心所需的知识结合在一起,可以从添加服务器到支持网络服务的功能齐备的数据中心。基础设施的描述采用高级的配置语法,允许你把数据中心的蓝图按版本管理,并且转换成多种代码。著名开源公司HashiCorp赞助开发这个项目。
荣誉奖: Docker Fig
Drone 是一个基于Docker的持续集成平台,而且它是用Go语言写的。Drone项目不满于现存的设置开发环境的技术和流程。
Drone提供了一个简单的自动测试和持续交付的方法:简单选择一个Docker镜像来满足你的需求,连接并提交至GitHub即可。Drone使用Docker容器来提供隔离的测试环境,让每个项目完全自主控制它的环境,没有传统的服务器管理的负担。Drone背后的100位社区贡献者强烈希望把这个项目带到企业和移动应用程序开发中。
开源新秀
作者: Black Duck Software
Linux中国|译者: sevenot 收起阅读 »
年度开源项目新秀
每年都有上千新的开源项目问世,但只有少数能够真正的吸引我们的关注。一些项目因为利用了当前比较流行的技术而发展壮大,有一些则真正地开启了一个新的领域。很多开源项目建立的初衷是为了解决一些生产上的问题,还有一些项目则是世界各地志同道合的开发者们共同发起的一个宏伟项目。
从2009年起,开源软件管理公司黑鸭便发起了 年度开源项目新秀 这一活动,它的评选根据 Open Hub 网站(即以前的Ohloh)上的活跃度。今年,我们很荣幸能够报道2015年10大开源项目新秀的得主和2名荣誉奖得主,它们是从上千个开源项目中脱颖而出的。评选采用了加权评分系统,得分标准基于项目的活跃度,交付速度和几个其它因数。
开源俨然成为了产业创新的引擎,就拿今年来说,和Docker容器相关的开源项目在全球各地兴起,这也不恰巧反映了企业最感兴趣的技术领域吗?最后,我们接下来介绍的项目,将会让你了解到全球开源项目的开发者们的在思考什么,这很快将会成为一个指引我们发展的领头羊。
2015年度开源项目新秀: DebOps
DebOps 收集打包了一套 Ansible 方案和规则(Ansible是一种自动化运维工具),可以从1个容器扩展到一个完整的数据中心。它的创始人Maciej Delmanowski将DebOps开源来保证项目长久进行,从而更好的通过外部贡献者的帮助发展下去。
DebOps始创于波兰的一个不起眼大学校园里,他们运营自己的数据中心,一切工作都采用手工配置。有时系统崩溃而导致几天的宕机,这时Delmanowski意识到一个配置管理系统是很有必要的。以Debian作为基础开始,DebOps是一组配置一整个数据基础设施的Ansible方案。此项目已经在许多不同的工作环境下实现,而创始者们则打算继续支持和改进这个项目。
2015年度开源项目新秀: Code Combat
传统的纸笔学习方法已近不能满足技术学科了。然而游戏却有很多人都爱玩,这也就是为什么 CodeCombat 的创始人会去开发一款多人协同编程游戏来教人们如何编码。
刚开始CodeCombat是一个创业想法,但其创始人决定取而代之创建一个开源项目。此想法在社区传播开来,很快不少贡献者加入到项目中来。项目发起仅仅两个月后,这款游戏就被接纳到Google’s Summer of Code活动中。这款游戏吸引了大量玩家,并被翻译成45种语言。CodeCombat希望成为那些想要一边学习代码同时获得乐趣的同学的风向标。
2015年度开源项目新秀: Storj
Storj 是一个点对点的云存储网络,可实现端到端加密,保证用户不用依赖第三方即可传输和共享数据。基于比特币block chain技术和点对点协议,Storj提供安全、私密、加密的云存储。
云数据存储的反对者担心成本开销和漏洞攻击。针对这两个担忧,Storj提供了一个私有云存储市场,用户可以通过Storjcoin X(SJCX) 购买交易存储空间。上传到Storj的文件会被粉碎、加密和存储到整个社区。只有文件所有者拥有密钥加密的信息。
在2014年举办的Texas Bitcoin Conference Hackathon会议上,去中心化的云存储市场概念首次被提出并证明可行。在第一次赢得黑客马拉松活动后,项目创始人们和领导团队利用开放论坛、Reddit、比特币论坛和社交媒体增长成了一个活跃的社区,如今,它们已成为影响Storj发展方向的一个重要组成部分。
2015年度开源项目新秀: Neovim
自1991年出现以来,Vim已经成为数以百万计软件开发人员所钟爱的文本编辑器。 而 Neovim 就是它的下一个版本。
在过去的23年里,软件开发生态系统经历了无数增长和创新。Neovim创始人Thiago de Arruda认为Vim缺乏当代元素,跟不上时代的发展。在保留Vim的招牌功能的前提下,Neovim团队同样在寻求改进和发展这个最受欢迎的文本编辑器的技术。早期众筹让Thiago de Arruda可以连续6个月时间投入到此项目。他相信Neovim社区会支持这个项目,激励他继续开发Neovim。
2015年度开源项目新秀: CockroachDB
前谷歌员工开发了一个开源的大型企业数据存储项目 CockroachDB ,它是一个可扩展的、跨地域复制且支持事务的数据存储的解决方案。
为了保证在线的百万兆字节流量业务的质量,Google开发了Spanner系统,这是一个可扩展的,稳定的,支持事务的系统。许多参与开发CockroachDB的团队现在都服务于开源社区。就像真正的蟑螂(cockroach)一样,CockroachDB可以在没有数据头、任意节点失效的情况下正常运行。这个开源项目有很多富有经验的贡献者,创始人们通过社交媒体、Github、网络、会议和聚会结识他们并鼓励他们参与其中。
2015年度开源项目新秀: Kubernetes
在将容器化软件到引入开源社区发展时, Docker 是一匹黑马,它创新了一套技术和工具。去年6月谷歌推出了 Kubernetes ,这是一款开源的容器管理工具,用来加快开发和简化操作。
谷歌在它的内部运营上使用容器技术多年了。在2014年夏天的DockerCon上大会上,谷歌这个互联网巨头开源了Kubernetes,Kubernetes的开发是为了满足迅速增长的Docker生态系统的需要。通过和其它的组织、项目合作,比如Red Hat和CoreOS,Kubernetes项目的管理者们推动它登上了Docker Hub的工具下载榜榜首。Kubernetes的开发团队希望扩大这个项目,发展它的社区,这样的话软件开发者就能花更少的时间在管理基础设施上,而更多的去开发他们自己的APP。
2015年度开源项目新秀: Open Bazaar
OpenBazaar是一个使用比特币与其他人交易的去中心化的市场。OpenBazaar这一概念最早在编程马拉松(hackathon)活动中被提出,它的创始人结合了BitTorent、比特币和传统的金融服务方式,创造了一个不受审查的交易平台。OpenBazaar的开发团队在寻求新的成员,而且不久以后他们将极度扩大Open Bazaar社区。Open Bazaar的核心是透明度,其创始人和贡献者的共同目标是在商务交易中掀起一场革命,让他们向着一个真实的、一个无控制的,去中心化的市场奋进。
2015年度开源项目新秀: IPFS
IPFS 是一个面向全球的、点对点的分布式版本文件系统。它综合了Git,BitTorrent,HTTP的思想,开启了一个新的数据和数据结构传输协议。
人们所知的开源,它的本意用简单的方法解决复杂的问题,这样产生许多新颖的想法,但是那些强大的项目仅仅是开源社区的冰山一角。IFPS有一个非常激进的团队,这个概念的提出是大胆的,令人惊讶的,有点甚至高不可攀。看起来,一个点对点的分布式文件系统是在寻求将所有的计算设备连在一起。这个可能的 HTTP 替换品通过多种渠道维护着一个社区,包括Git社区和超过100名贡献者的IRC。这个疯狂的想法将在2015年进行软件内部测试。
2015年度开源项目新秀: cAdvisor
cAdvisor (Container Advisor) 是一个针对在运行中的容器进行收集,统计,处理和输出信息的工具,它可以给容器的使用者提供资源的使用情况和工作特性。对于每一个容器,cAdvisor记录着资源的隔离参数,资源使用历史,资源使用历史对比框图,网络状态。这些从容器输出的数据跨越主机传递。
cAdvisor可以在绝大多数的Linux发行版上运行,并且支持包括Docker在内的多种容器类型。事实上它成为了一种容器的代理,并被集成在了很多系统中。cAdvisor在DockerHub下载量也是位居前茅。cAdvisor的开发团队希望把cAdvisor改进到能够更深入地理解应用性能,并且集成到集群系统。
2015年度开源项目新秀: Terraform
Terraform 提供了一些常见设置来创建一个基础设施,从物理机到虚拟机,以及email服务器、DNS服务器等。这个想法包括从家庭个人机解决方案到公共云平台提供的服务。一旦建立好了以后,Terraform可以让运维人员安全又高效地改变你的基础设施,就如同配置一样。
Terraform.io的创始者工作在一个Devops模式的公司,他找到了一个窍门把建立一个完整的数据中心所需的知识结合在一起,可以从添加服务器到支持网络服务的功能齐备的数据中心。基础设施的描述采用高级的配置语法,允许你把数据中心的蓝图按版本管理,并且转换成多种代码。著名开源公司HashiCorp赞助开发这个项目。
荣誉奖: Docker Fig
Drone 是一个基于Docker的持续集成平台,而且它是用Go语言写的。Drone项目不满于现存的设置开发环境的技术和流程。
Drone提供了一个简单的自动测试和持续交付的方法:简单选择一个Docker镜像来满足你的需求,连接并提交至GitHub即可。Drone使用Docker容器来提供隔离的测试环境,让每个项目完全自主控制它的环境,没有传统的服务器管理的负担。Drone背后的100位社区贡献者强烈希望把这个项目带到企业和移动应用程序开发中。
开源新秀
作者: Black Duck Software
Linux中国|译者: sevenot 收起阅读 »
环信CEO:“即时通讯云+移动客服”为App打造用户体验闭环
随着移动互联网的发展,即时通讯、移动客服已经成为了很多移动应用的必备功能,环信作为新晋移动即时通讯PaaS平台服务商,凭借着近期刚刚上线的跨平台移动端客服产品吸引了大量应用开发者的关注。为了进一步了解移动客服产品的发展现状,InfoQ专门对环信联合创始人及CEO刘俊彦进行了专访。
InfoQ:环信成立已经有两年的时间,能聊一下这两年环信的整体发展吗?
刘俊彦:环信在2013年4月成立,2014年6月第一个产品”环信即时通讯云”正式上线。然后在今年4月份上线了移动客服产品。在过去的一年里面,环信做了三轮融资,分别是去年5月份的天使轮,去年8月份的A轮,和去年10月份的A+轮。截止到今年5月底,一共有23000款APP在使用环信即时通讯云SDK。环信的SDK覆盖了2.5亿的终端用户,环信即时通讯平台日活是千万级别,每天处理将近两亿条消息。这就是环信过去两年大概的情况。InfoQ:移动客服可以算是现在移动应用通讯领域的一个刚性需求了,您能否谈一谈移动客服的形式都有哪些?
刘俊彦:虽然移动客服是一种移动互联网时代的新产品,但其实也只是形式上的一个创新。基本上每一个APP的设置页面都会有一个“意见反馈”的功能。其实这个功能就是一个客服功能,只是受限于技术、资源等因素,需要以表单的形式来呈现,有的时候还需要用户选择类别、提交联系方式,很难做到实时更新。InfoQ:针对这种移动客服的形式,存在哪些技术难点?环信是怎样客服这些难点的?
最近这一年,随着移动电商、O2O,在线教育,在线旅游,互联网金融产品的发展,出现了很多形式的客服产品。比如:作为电商,做O2O一定要做售前、售中、售后。这个过程中就涉及到用户与商家的沟通。
常见的沟通形式有4种,第一种是在App里面提供一个按钮,用户点击该按钮直接跳转到QQ,然后通过QQ去完成与商家的交流。
第二种是通过链接跳转微信。第三种形式就是我们最开始提到的表单形式,这种形式是非实时的。
最新的一种形式是用即时通讯的方法来跟商家沟通。用户打开一个窗口,在该窗口可以发语音、图片、文字,可以跟商家进行实时互动,这种形式就类似与微信、旺旺。第四种形式是最受大家欢迎,受O2O的商家、电商、医疗、互联网金融认可度最高的的一种客服形式,这种形式非常有利于用户通过手机与商家进行沟通。
刘俊彦:用IM来做客服虽然很方便,但它的技术门槛比较高。第一需要你的服务器能够做到千万级、亿级的并发处理。移动客服是基于IM的长连接技术实现的。举个例子,如果某个App有100万日活用户,那么用户的手机和客服服务器之间就存在一百万条长连接。一些大型的App可能会有几百万、几千万的日活用户,那么提供服务的厂家就需要支持几百万、几千万的用户长连接。如果你的平台要支持上几百家厂商,那么平台就需要有支持几千万到几亿用户同时连接的能力。InfoQ:因为领域的不同导致用户流量分布特点也不同,所以说即时通讯服务里面会经常产生波峰波谷。能不能谈一谈环信在这个弹性方面的具体措施呢?
第二需要做到不丢消息,并且每一条消息能够做到最实时的到达。即时通讯服务是帮助商家来进行销售的,用户可能是在三线城市,也可能是在四线城市,网络环境可能是2G、3G或4G。要做到任何情况下,只要有网络,消息一定能够即时到达是非常困难的。但如果你的平台做不到,就会给商家带来损失。拿电商来举例:一个消费者想买一样东西,发了一条咨询消息,但这个消息丢失了,那么就意味着这个单子丢了。这个用户可能是商家花了很多钱,从其它平台导流过来的,但是因为一条咨询没有即时收到,结果流失了,这样就给商家带来了损失。
环信对于移动客服的技术已经非常成熟,因为环信从去年6月1日正式上线,做的就是即时通讯云。我们在即时通讯云这一块已经做到了全国有2.3万家APP使用,有2.5亿用户,平台的日活用户是几千万。两年多的技术储备让我们敢保证绝对不丢消息,并且消息能够非常实时的到达。
刘俊彦:解决该问题要从技术与非技术两方面着手。非技术其实就只能靠烧钱来解决。我们系统上大概有50%的余量,超过50%的压力之后,就开始加服务器。这样能保证系统在不到50%冗余的情况下运行。当然这也意味着有50%的容量浪费,其实这个“浪费”是应付一些不可预料的波峰和波谷。单个APP的用户行为基本是固定的。社交类APP,大概在晚上十点半会产生波峰。而有一些企业办公类、教育类App是在白天产生波峰。我们为两万多家APP提供服务,综合起来整个波峰和波谷就会比较平均一些。InfoQ:环信开放了UI源码,现在用户可以深度定制应用的UI。环信还建立了自己的开源社区,能不能谈一谈环信在开源方面有什么样的规划?是否考虑给开发者开放更灵活的API,或者是直接开源一些核心技术?
我们也采取了一些技术手段来解决该问题。现在有一些云服务平台提供秒级计算API。当到半夜两点钟,所有APP都进入波谷后,我们就会调用API释放掉一批服务器。但这样带来的经济效益也不高,因为秒级API走的是另外一套收费体系。
刘俊彦:环信的四位创始人有三位都是长期在开源社区工作的。而我从03年以后,基本上没有做过商业软件。所以开源精神已经深深植入到每一位创始人。InfoQ:最近IT行业内数据中心机房出的问题也很多,环信在异地多活这方面有什么样的规划吗?
除了UI开源之外,我们还建了一个自己的开源社区。在过去的两年里,我们看到了一个很有意思的现象。很多人用环信来做社交,有单聊、群聊、匿名群聊等等各种玩法。但是归纳之后,大概可分为几十种。所以我们希望通过社区的力量,把这几十种常见的社交模块做出来,然后用开源的形式提供给大家。当用户想要做一款新的社交APP时,基于环信这样的底层云服务模块,有可能会节约几个月的时间。
有了上面的基础,我们就想走的更远一点。当你想做一款APP的时候,你可能用到一个朋友圈的功能,用到一个匿名的功能,我们希望这些功能也变成一个现成的模块,甚至整个APP都能够以开源的形式完整呈现。这样大家在开发一款APP的时候,就像是搭积木,选不同的积木模块,然后把他们拼起来。特别是对于创业者,可以把更多的时间用于提升用户体验。
最近这一个月我们开放了三个应用级别的模块:第一个是凡信,它是高仿微信的一款APP。凡信实现了单聊、群聊、朋友群等功能。这是我们社区里一个网友开发的,他完全无私的把服务器端和客服端代码开源出来。第二个是一个类似于陌陌的陌生人交友APP,功能主要是看附近的人,看到附近的人之后可以跟他聊天。第三个是我们面向企业的开源产品。上面提到的开源项目大家都可以到我们的社区下载,当然你也可以成为这些项目的贡献者,一起来推动这些产品往前走。
刘俊彦:到现在为止,环信的云服务都托管在国内最一流的云服务平台上。按照他们的星级来说,都是最顶尖的机房,可靠性、安全性都是有保证的。但为了给大家提供更可靠、更放心的云服务,“异地多活”已经列入了我们的开发计划,未来的几个月环信的“异地多活”就会正式上线。InfoQ:最近有消息说环信要开始新一轮的融资,您能不能谈一谈环信在短期内的发展计划呢?
刘俊彦:刚刚我提到过环信在过去一年里进行了三轮融资。我们目前其实正在做B轮融资,因为还在进行中,如果有更多的细节我会尽快告诉大家。收起阅读 »
环信发展到现在已成规模,下一步我们会继续巩固环信在“即时通讯云”领域国内第一的位置,我们希望以最低的价格、最好的服务为做社交以及各种应用服务者提供即时通讯功能。其次我们在今年4月份上线了环信移动客服产品。移动客服是我们在即时通讯领域一个很大的扩展。在即时通讯领域有两种场景,一种是在APP里面用户和用户之间进行社交活动,另一种是用户与商家之间的聊天,也就是客服。我们一直认为任何一款APP,都需要即时通讯,那么要达到100%的覆盖,光做社交是无法实现的,所以我们要加上客服这一块。我们最终的愿景是用环信的力量和技术为每一款APP提供即时通讯功能。
盘点:被互联网女皇报告点名,美国14家潜在“独角兽”SaaS公司
这个slide是从女皇报告中节选出来的,互联网对消费者群体带来了颠覆式的影响,这种影响也诞生了伟大的新时代的互联网公司,Airbnb,uber,instantcart就是其中在住行吃方面的典范。而这种颠覆的影响将迅速扩展到企业端,于是企业SaaS服务拥有巨大的机会。
而在美国,这些企业服务公司已获得极大的关注:
1、Slack:成立1年多估值28亿美元当人们谈论Slack时,经常会说它特别“有趣”。使用Slack不像是在工作,而是在“放松”,但是在这个过程中却能通过Slack把该做工作切实有效地完成。有3个方面的因素是Slack脱颖而出的关键:用户界面与众不同;使用感受与众不同;交流方式与众不同。
2、Square:移动支付鼻祖,估值60亿美元Twitter之父Jack Dorsey的创办的Square,移动支付公司日交易额已达1亿美元,深入到中小企业、个体零售商和移动商贩里去,并不断拓展到各种O2O的订单支付环节。
3、Stripe:在线支付挑战者,估值50亿美元由20多岁的两兄弟Patrick Collison和John Collison创办的Stripe为公司提供网上支付的解决方案。在这两位分别从麻省理工和哈佛辍学创业的兄弟看来,PayPal的电子支付流程是在是太繁琐太复杂了,他们的目标是要把在线接收付款服务变成像添加 YouTube 视频一般简单。
4、Domo:低调的商业智能软件公司,估值20亿美DOMO是一个真正的业务管理平台,可以将许多不同来源的数据以真正实时、直观的方式呈现出来。而Domo的创始人乔希•詹姆斯(Josh James)早在1996年就创办了Omniture公司,SaaS这个商业模式的鼻祖,也是Analytics这个领域的开创者和领导者。2009年,Adobe以18亿美元的价格收购了Omniture,后者改名叫做Adobe Analytics。2010年乔希•詹姆斯(Josh James)离开Adobe,从Benchmark Capital拿到了3000万美元的第一轮融资并创建了DOMO。
5、DocuSign:小签名大市场,数字签名公司估值30亿美元DocuSign是一家提供电子签名服务的初创企业,成立于2003年,总部位于旧金山,在全球有10个分支机构,共有1300名员工。通过DocunSign的服务,用户只需通过智能手机或平板电脑即可完成手写签名,免除了用户要通过传真或邮件签名的麻烦;同时DocuSign还通过数字签名等方式验证用户的真伪,从而帮助企业用户安全地在网上获取具有法律效力的电子签名。目前全球188个国家有超过10万家公司以及5000万个人都在使用DocuSign的电子签名服务。
6、Intercom:应用内的客户沟通工具,估值4亿美元Intercom是一款面向企业用户的客户沟通上SaaS工具,可以让企业与用户的沟通轻易在应用和网站内实现,强大的后台管理系统能够实时监控客户反馈,指派任务并统计客户的订单转换率,而相对于传统的电子邮件营销产品,应用内消息能带来更好的用户参与度(基于回复率)。
7、Gainsight:帮你留住客户,估值6亿美元Gainsight公司整合了第三方应用,他们评估销售数据,使用日志,支持投票,调研报告,以及其他客户智能资源。通过收集和分析这些数据,让企业减少客户流失率。当客户出现离开风险的时候,Gainsight的系统会发出早期预警,企业因此可以采取行动,挽留客户,另外还可以提供一些工具,识别出那些对企业产品评价较高的客户,这样可以为企业带来提升销售量的机会。
8、Directly:众包客户服务中心,估值1亿美元Directly以帮助创业公司将客户服务外包给其他人。Directly 希望帮助客户通过提问与回答的方式向用户提供更好的体验,而注册这家网站的一干“专家”则希望能够因此获得报酬。Directly 用户在网站上发布问题后,系统将自动分析问题,并匹配可能解答这些问题的专家,然后通过短信提醒他们。然后专家们便可以通过 Directly 网页端或者 Android 应用进行解答。大多数问题会在数小时内得到妥善解决,一旦对答案感到满意,提问者便可以关注回答者。
9、Zenefits:疯狂的HR管理工具,2年估值45亿美元Zenefits 为中小企业提供免费的一站式云 HR 管理工具,简单地说,就是让 HR 杂活更简单便捷,包括员工的入职和离职手续办理,工资和福利发放,保险和退休基金办理,缴税缴费,专利追踪等等。和 Uber 一样, Zenefits 在推广过程中也受到了部分地方政府的管制。在美国犹他州, Zenefits 一度被判定为非法,罪名是不正当返利和诱导用户。罪名的由头是, Zenefits 免费提供保险办理的入口,如果用户通过 Zenefits 办理保险,保险公司需要支付给 Zenefits 5%的佣金。这样一来,绕过了保险经销商。
10、Anaplan:帮助企业业务建模,估值5亿美元Anaplan是一家基于云的企业销售、运营及财务建模与规划公司。其解决方案可以收集企业客户的销售、运营及财务数据,然后运用复杂的模型对企业的绩效、开支等情况进行深入分析,为决策提供支持。
11、Greenhouse:企业一体化招聘流程,估值3亿美元Greenhouse 是一家招聘领域的SaaS软件服务商,为企业提供招聘管理、求职信管理、面试、人才招聘广告评估等服务,提供求职者比较、推荐等服务。Uber、Pinterest、airbnb Snapchat、和Buzzfeed都是Greenhouse的客户。
12、Checkr:高效的自动化背景调查平台,估值0.5亿美元Checkr 使用相同的数据源,提供包括社会安全号码、历史地址、性犯罪检索、恐怖分子监控名单、国家犯罪记录、驾驶记录等方面的背景调查。但与传统调查报告不同的是,Checkr 提供了一个客户只需输入名字就能得到数据反馈的 API,简化了调查报告的手工操作过程,使得背景调查更加自动化,从而更容易链接到公司现行的入职流程之中。
13、Guidespark:数字化员工培训资料,估值2亿美元创立于 2008 年,为人力资源管理人员提供沟通和培训解决方案,将纸质文件变成电子文件,并将这些资料移动化。这样能够减少人力资源支持的时间,提升人力资源的效率。
14、Envoy:智能访客管理系统,估值2亿美元智能访客管理系统 Envoy 专门为科技范儿的互联网公司量身打造,简它将整个访客管理流程电子化,提供从登记信息到通知负责人再到访客管理的一条龙服务,大大提升了前台效率。
来源:创业投资笔记
作者:苏东 收起阅读 »
而在美国,这些企业服务公司已获得极大的关注:
1、Slack:成立1年多估值28亿美元当人们谈论Slack时,经常会说它特别“有趣”。使用Slack不像是在工作,而是在“放松”,但是在这个过程中却能通过Slack把该做工作切实有效地完成。有3个方面的因素是Slack脱颖而出的关键:用户界面与众不同;使用感受与众不同;交流方式与众不同。
2、Square:移动支付鼻祖,估值60亿美元Twitter之父Jack Dorsey的创办的Square,移动支付公司日交易额已达1亿美元,深入到中小企业、个体零售商和移动商贩里去,并不断拓展到各种O2O的订单支付环节。
3、Stripe:在线支付挑战者,估值50亿美元由20多岁的两兄弟Patrick Collison和John Collison创办的Stripe为公司提供网上支付的解决方案。在这两位分别从麻省理工和哈佛辍学创业的兄弟看来,PayPal的电子支付流程是在是太繁琐太复杂了,他们的目标是要把在线接收付款服务变成像添加 YouTube 视频一般简单。
4、Domo:低调的商业智能软件公司,估值20亿美DOMO是一个真正的业务管理平台,可以将许多不同来源的数据以真正实时、直观的方式呈现出来。而Domo的创始人乔希•詹姆斯(Josh James)早在1996年就创办了Omniture公司,SaaS这个商业模式的鼻祖,也是Analytics这个领域的开创者和领导者。2009年,Adobe以18亿美元的价格收购了Omniture,后者改名叫做Adobe Analytics。2010年乔希•詹姆斯(Josh James)离开Adobe,从Benchmark Capital拿到了3000万美元的第一轮融资并创建了DOMO。
5、DocuSign:小签名大市场,数字签名公司估值30亿美元DocuSign是一家提供电子签名服务的初创企业,成立于2003年,总部位于旧金山,在全球有10个分支机构,共有1300名员工。通过DocunSign的服务,用户只需通过智能手机或平板电脑即可完成手写签名,免除了用户要通过传真或邮件签名的麻烦;同时DocuSign还通过数字签名等方式验证用户的真伪,从而帮助企业用户安全地在网上获取具有法律效力的电子签名。目前全球188个国家有超过10万家公司以及5000万个人都在使用DocuSign的电子签名服务。
6、Intercom:应用内的客户沟通工具,估值4亿美元Intercom是一款面向企业用户的客户沟通上SaaS工具,可以让企业与用户的沟通轻易在应用和网站内实现,强大的后台管理系统能够实时监控客户反馈,指派任务并统计客户的订单转换率,而相对于传统的电子邮件营销产品,应用内消息能带来更好的用户参与度(基于回复率)。
7、Gainsight:帮你留住客户,估值6亿美元Gainsight公司整合了第三方应用,他们评估销售数据,使用日志,支持投票,调研报告,以及其他客户智能资源。通过收集和分析这些数据,让企业减少客户流失率。当客户出现离开风险的时候,Gainsight的系统会发出早期预警,企业因此可以采取行动,挽留客户,另外还可以提供一些工具,识别出那些对企业产品评价较高的客户,这样可以为企业带来提升销售量的机会。
8、Directly:众包客户服务中心,估值1亿美元Directly以帮助创业公司将客户服务外包给其他人。Directly 希望帮助客户通过提问与回答的方式向用户提供更好的体验,而注册这家网站的一干“专家”则希望能够因此获得报酬。Directly 用户在网站上发布问题后,系统将自动分析问题,并匹配可能解答这些问题的专家,然后通过短信提醒他们。然后专家们便可以通过 Directly 网页端或者 Android 应用进行解答。大多数问题会在数小时内得到妥善解决,一旦对答案感到满意,提问者便可以关注回答者。
9、Zenefits:疯狂的HR管理工具,2年估值45亿美元Zenefits 为中小企业提供免费的一站式云 HR 管理工具,简单地说,就是让 HR 杂活更简单便捷,包括员工的入职和离职手续办理,工资和福利发放,保险和退休基金办理,缴税缴费,专利追踪等等。和 Uber 一样, Zenefits 在推广过程中也受到了部分地方政府的管制。在美国犹他州, Zenefits 一度被判定为非法,罪名是不正当返利和诱导用户。罪名的由头是, Zenefits 免费提供保险办理的入口,如果用户通过 Zenefits 办理保险,保险公司需要支付给 Zenefits 5%的佣金。这样一来,绕过了保险经销商。
10、Anaplan:帮助企业业务建模,估值5亿美元Anaplan是一家基于云的企业销售、运营及财务建模与规划公司。其解决方案可以收集企业客户的销售、运营及财务数据,然后运用复杂的模型对企业的绩效、开支等情况进行深入分析,为决策提供支持。
11、Greenhouse:企业一体化招聘流程,估值3亿美元Greenhouse 是一家招聘领域的SaaS软件服务商,为企业提供招聘管理、求职信管理、面试、人才招聘广告评估等服务,提供求职者比较、推荐等服务。Uber、Pinterest、airbnb Snapchat、和Buzzfeed都是Greenhouse的客户。
12、Checkr:高效的自动化背景调查平台,估值0.5亿美元Checkr 使用相同的数据源,提供包括社会安全号码、历史地址、性犯罪检索、恐怖分子监控名单、国家犯罪记录、驾驶记录等方面的背景调查。但与传统调查报告不同的是,Checkr 提供了一个客户只需输入名字就能得到数据反馈的 API,简化了调查报告的手工操作过程,使得背景调查更加自动化,从而更容易链接到公司现行的入职流程之中。
13、Guidespark:数字化员工培训资料,估值2亿美元创立于 2008 年,为人力资源管理人员提供沟通和培训解决方案,将纸质文件变成电子文件,并将这些资料移动化。这样能够减少人力资源支持的时间,提升人力资源的效率。
14、Envoy:智能访客管理系统,估值2亿美元智能访客管理系统 Envoy 专门为科技范儿的互联网公司量身打造,简它将整个访客管理流程电子化,提供从登记信息到通知负责人再到访客管理的一条龙服务,大大提升了前台效率。
来源:创业投资笔记
作者:苏东 收起阅读 »
群详情获取失败
在确保登录成功的情况下,并且初始化完成,可以进行群创建和聊天的情况下
调用群详情获取
//根据群聊ID从服务器获取群聊信息
EMGroup group =EMGroupManager.getInstance().getGroupFromServer(grounId);
这个代码走之后直接进去了异常,
EaseMobException的描述是
com.easemob.exceptions.EaseMobException: android.os.NetworkOnMainThreadException
打印的堆栈消息:
07-04 07:42:34.087: W/System.err(7386): com.easemob.exceptions.EaseMobException: android.os.NetworkOnMainThreadException
07-04 07:42:34.097: W/System.err(7386): at com.easemob.cloud.HttpClientManager.sendRequestWithCountDown(Unknown Source)
07-04 07:42:34.107: W/System.err(7386): at com.easemob.cloud.HttpClientManager.sendRequest(Unknown Source)
07-04 07:42:34.107: W/System.err(7386): at com.easemob.cloud.HttpClientManager.sendHttpRequestWithRetryToken(Unknown Source)
07-04 07:42:34.117: W/System.err(7386): at com.easemob.cloud.HttpClientManager.sendRequestWithToken(Unknown Source)
07-04 07:42:34.127: W/System.err(7386): at com.easemob.cloud.EMHttpClient.sendRequestWithToken(Unknown Source)
07-04 07:42:34.137: W/System.err(7386): at com.easemob.chat.EMGroupManager.getGroupFromRestServer(Unknown Source)
07-04 07:42:34.137: W/System.err(7386): at com.easemob.chat.EMGroupManager.getGroupFromServer(Unknown Source)
07-04 07:42:34.137: W/System.err(7386): at com.tomatotown.util.InitIM.getPublicGroupInfo(InitIM.java:324)
07-04 07:42:34.137: W/System.err(7386): at com.tomatotown.parent.activity.friends.GroupInfoActivity.getGroupInfo(GroupInfoActivity.java:246)
07-04 07:42:34.137: W/System.err(7386): at com.tomatotown.parent.activity.friends.GroupInfoActivity.onCreate(GroupInfoActivity.java:67)
07-04 07:42:34.137: W/System.err(7386): at android.app.Activity.performCreate(Activity.java:5231)
07-04 07:42:34.137: W/System.err(7386): at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1087)
07-04 07:42:34.137: W/System.err(7386): at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2227)
07-04 07:42:34.147: W/System.err(7386): at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2313)
07-04 07:42:34.147: W/System.err(7386): at android.app.ActivityThread.access$800(ActivityThread.java:147)
07-04 07:42:34.147: W/System.err(7386): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1226)
07-04 07:42:34.147: W/System.err(7386): at android.os.Handler.dispatchMessage(Handler.java:102)
07-04 07:42:34.147: W/System.err(7386): at android.os.Looper.loop(Looper.java:136)
07-04 07:42:34.147: W/System.err(7386): at android.app.ActivityThread.main(ActivityThread.java:5137)
07-04 07:42:34.147: W/System.err(7386): at java.lang.reflect.Method.invokeNative(Native Method)
07-04 07:42:34.147: W/System.err(7386): at java.lang.reflect.Method.invoke(Method.java:515)
07-04 07:42:34.147: W/System.err(7386): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:801)
07-04 07:42:34.147: W/System.err(7386): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:617)
07-04 07:42:34.147: W/System.err(7386): at dalvik.system.NativeStart.main(Native Method)
求助!! 收起阅读 »
调用群详情获取
//根据群聊ID从服务器获取群聊信息
EMGroup group =EMGroupManager.getInstance().getGroupFromServer(grounId);
这个代码走之后直接进去了异常,
EaseMobException的描述是
com.easemob.exceptions.EaseMobException: android.os.NetworkOnMainThreadException
打印的堆栈消息:
07-04 07:42:34.087: W/System.err(7386): com.easemob.exceptions.EaseMobException: android.os.NetworkOnMainThreadException
07-04 07:42:34.097: W/System.err(7386): at com.easemob.cloud.HttpClientManager.sendRequestWithCountDown(Unknown Source)
07-04 07:42:34.107: W/System.err(7386): at com.easemob.cloud.HttpClientManager.sendRequest(Unknown Source)
07-04 07:42:34.107: W/System.err(7386): at com.easemob.cloud.HttpClientManager.sendHttpRequestWithRetryToken(Unknown Source)
07-04 07:42:34.117: W/System.err(7386): at com.easemob.cloud.HttpClientManager.sendRequestWithToken(Unknown Source)
07-04 07:42:34.127: W/System.err(7386): at com.easemob.cloud.EMHttpClient.sendRequestWithToken(Unknown Source)
07-04 07:42:34.137: W/System.err(7386): at com.easemob.chat.EMGroupManager.getGroupFromRestServer(Unknown Source)
07-04 07:42:34.137: W/System.err(7386): at com.easemob.chat.EMGroupManager.getGroupFromServer(Unknown Source)
07-04 07:42:34.137: W/System.err(7386): at com.tomatotown.util.InitIM.getPublicGroupInfo(InitIM.java:324)
07-04 07:42:34.137: W/System.err(7386): at com.tomatotown.parent.activity.friends.GroupInfoActivity.getGroupInfo(GroupInfoActivity.java:246)
07-04 07:42:34.137: W/System.err(7386): at com.tomatotown.parent.activity.friends.GroupInfoActivity.onCreate(GroupInfoActivity.java:67)
07-04 07:42:34.137: W/System.err(7386): at android.app.Activity.performCreate(Activity.java:5231)
07-04 07:42:34.137: W/System.err(7386): at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1087)
07-04 07:42:34.137: W/System.err(7386): at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2227)
07-04 07:42:34.147: W/System.err(7386): at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2313)
07-04 07:42:34.147: W/System.err(7386): at android.app.ActivityThread.access$800(ActivityThread.java:147)
07-04 07:42:34.147: W/System.err(7386): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1226)
07-04 07:42:34.147: W/System.err(7386): at android.os.Handler.dispatchMessage(Handler.java:102)
07-04 07:42:34.147: W/System.err(7386): at android.os.Looper.loop(Looper.java:136)
07-04 07:42:34.147: W/System.err(7386): at android.app.ActivityThread.main(ActivityThread.java:5137)
07-04 07:42:34.147: W/System.err(7386): at java.lang.reflect.Method.invokeNative(Native Method)
07-04 07:42:34.147: W/System.err(7386): at java.lang.reflect.Method.invoke(Method.java:515)
07-04 07:42:34.147: W/System.err(7386): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:801)
07-04 07:42:34.147: W/System.err(7386): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:617)
07-04 07:42:34.147: W/System.err(7386): at dalvik.system.NativeStart.main(Native Method)
求助!! 收起阅读 »
分享:大型网站图片服务器架构的演进
在主流的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的方式写入文件。
优点:实现起来最简单,无需任何复杂技术,就能成功将用户上传的文件写入指定目录。保存数据库记录和访问起来倒是也很方便。
缺点:上传方式混乱,严重不利于网站的扩展。
针对上述最原始的架构,主要面临着如下问题:
集群时代的图片服务器架构(实时同步)
在website站点下面,新建一个名为upload的虚拟目录,由于虚拟目录的灵活性,能在一定程度上取代物理目录,并兼容原有的图片上传和访问方式。用户的访问方式依然是:
http://www.yourdomain.com/upload/qa/test.jpg
因为虚拟目录,可以指向本地任意盘符下的任意目录。这样一来,还可以通过接入外置存储,来进行单机的容量扩展。
基本架构如下图所示:
在早期的很多基于Linux开源架构的网站中,如果不想同步图片,可能会利用NFS来实现。事实证明,NFS在高并发读写和海量存储方面,效率上存在一定问题,并非最佳的选择,所以大部分互联网公司都不会使用NFS来实现此类应用。当然,也可以通过Windows自带的DFS来实现,缺点是“配置复杂,效率未知,而且缺乏资料大量的实际案例”。另外,也有一些公司采用FTP或Samba来实现。
上面提到的几种架构,在上传/下载操作时,都经过了Web服务器(虽然共享存储的这种架构,也可以配置独立域名和站点来提供图片访问,但上传写入仍然得经过Web服务器上的应用程序来处理),这对Web服务器来讲无疑是造成巨大的压力。所以,更建议使用独立的图片服务器和独立的域名,来提供用户图片的上传和访问。
独立图片服务器/独立域名的好处
......
我们可以使用Lighttpd或者Nginx等轻量级的web服务器来架构独立图片服务器。
当前的图片服务器架构(分布式文件系统+CDN)
在构建当前的图片服务器架构之前,可以先彻底撇开web服务器,直接配置单独的图片服务器/域名。但面临如下的问题:
直到应用级别的(非系统级) 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加速。最重要的是,价格也不贵。
总结,有关图片服务器架构扩展,大致围绕这些问题展开:
收起阅读 »
本文将以一个真实垂直门户网站的发展历程,向大家娓娓道来。
构建在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静态缓存架构。
- 旧图片路径和访问规则的兼容性,应用程序层面的可扩展,上传和访问的性能和安全性等。
收起阅读 »
解析:带你从源码的角度彻底理解,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继承结构示意图如下所示:
可以看到,我们平时项目里经常用到的各种布局,全都属于ViewGroup的子类。
简单介绍完了ViewGroup,我们现在通过一个Demo来演示一下Android中VewGroup的事件分发流程吧。
首
先我们来自定义一个布局,命名为MyLayout,继承自LinearLayout,如下所示:
分别点击一下Button1、Button2和空白区域,打印结果如下所示:
你会发现,当点击按钮的时候,MyLayout注册的onTouch方法并不会执行,只有点击空白区域的时候才会执行该方法。你可以先理解成Button的onClick方法将事件消费掉了,因此事件不会再继续向下传递。
那就说明Android中的touch事件是先传递到View,再传递到ViewGroup的?现在下结论还未免过早了,让我们再来做一个实验。
查阅文档可以看到,ViewGroup中有一个onInterceptTouchEvent方法,我们来看一下这个方法的源码:
你会发现,不管你点击哪里,永远都只会触发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方法的源码吧!代码如下所示:
那我们重点来看下条件判断的内部是怎么实现的。在第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的事件分发流程的分析也就到此结束了,我们最后再来简单梳理一下吧。
收起阅读 »
还未阅读过的朋友,请先参考上文: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 {然后,打开主布局文件activity_main.xml,在其中加入我们自定义的布局:
02.
03. public MyLayout(Context context, AttributeSet attrs) {
04. super(context, attrs);
05. }
06.
07.}
可以看到,我们在MyLayout中添加了两个按钮,接着在MainActivity中为这两个按钮和MyLayout都注册了监听事件:
[html] view plaincopy
01.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. 09. android:id="@+id/button1"
10. android:layout_width="match_parent"
11. android:layout_height="wrap_content"
12. android:text="Button1" />
13.
14. 15. android:id="@+id/button2"
16. android:layout_width="match_parent"
17. android:layout_height="wrap_content"
18. android:text="Button2" />
19.
20.
myLayout.setOnTouchListener(new OnTouchListener() {我们在MyLayout的onTouch方法,和Button1、Button2的onClick方法中都打印了一句话。现在运行一下项目,效果图如下所示:
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.});
分别点击一下Button1、Button2和空白区域,打印结果如下所示:
你会发现,当点击按钮的时候,MyLayout注册的onTouch方法并不会执行,只有点击空白区域的时候才会执行该方法。你可以先理解成Button的onClick方法将事件消费掉了,因此事件不会再继续向下传递。
那就说明Android中的touch事件是先传递到View,再传递到ViewGroup的?现在下结论还未免过早了,让我们再来做一个实验。
查阅文档可以看到,ViewGroup中有一个onInterceptTouchEvent方法,我们来看一下这个方法的源码:
/**如果不看源码你还真可能被这注释吓到了,这么长的英文注释看得头都大了。可是源码竟然如此简单!只有一行代码,返回了一个false!好吧,既然是布尔型的返回,那么只有两种可能,我们在MyLayout中重写这个方法,然后返回一个true试试,代码如下所示:
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. *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. *
13. *- You will receive the down event here.
14. *- 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. *- 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. *- 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. *
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.}
public class MyLayout extends LinearLayout {现在再次运行项目,然后分别Button1、Button2和空白区域,打印结果如下所示:
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.}
你会发现,不管你点击哪里,永远都只会触发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) {这个方法代码比较长,我们只挑重点看。首先在第13行可以看到一个条件判断,如果disallowIntercept和!onInterceptTouchEvent(ev)两者有一个为true,就会进入到这个条件判断中。disallowIntercept是指是否禁用掉事件拦截的功能,默认是false,也可以通过调用requestDisallowInterceptTouchEvent方法对这个值进行修改。那么当第一个值为false的时候就会完全依赖第二个值来决定是否可以进入到条件判断的内部,第二个值是什么呢?竟然就是对onInterceptTouchEvent方法的返回值取反!也就是说如果我们在onInterceptTouchEvent方法中返回false,就会让第二个值为true,从而进入到条件判断的内部,如果我们在onInterceptTouchEvent方法中返回true,就会让第二个值为false,从而跳出了这个条件判断。这个时候你就可以思考一下了,由于我们刚刚在MyLayout中重写了onInterceptTouchEvent方法,让这个方法返回true,导致所有按钮的点击事件都被屏蔽了,那我们就完全有理由相信,按钮点击事件的处理就是在第13行条件判断的内部进行的!
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.}
那我们重点来看下条件判断的内部是怎么实现的。在第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事件分发机制(上)
其实我一直准备写一篇关于Android事件分发机制的文章,从我的第一篇博客开始,就零零散散在好多地方使用到了Android事件分发的知识。也有好多朋友问过我各种问题,比如:onTouch和onTouchEvent有什么区别,又该如何使用?为什么给ListView引入了一个滑动菜单的功能,ListView就不能滚动了?为什么图片轮播器里的图片使用Button而不用ImageView?等等……对于这些问题,我并没有给出非常详细的回答,因为我知道如果想要彻底搞明白这些问题,掌握Android事件分发机制是必不可少的,而Android事件分发机制绝对不是三言两语就能说得清的。
在我经过较长时间的筹备之后,终于决定开始写这样一篇文章了。目前虽然网上相关的文章也不少,但我觉得没有哪篇写得特别详细的(也许我还没有找到),多数文章只是讲了讲理论,然后配合demo运行了一下结果。而我准备带着大家从源码的角度进行分析,相信大家可以更加深刻地理解Android事件分发机制。
阅读源码讲究由浅入深,循序渐进,因此我们也从简单的开始,本篇先带大家探究View的事件分发,下篇再去探究难度更高的ViewGroup的事件分发。
那我们现在就开始吧!比如说你当前有一个非常简单的项目,只有一个Activity,并且Activity中只有一个按钮。你可能已经知道,如果想要给这个按钮注册一个点击事件,只需要调用:
可以看到,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方法的源码:
先看一下第一个条件,mOnTouchListener这个变量是在哪里赋值的呢?我们寻找之后在View里发现了如下方法:
第三个条件就比较关键了,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的源码,如下所示:
说到这里,很多的朋友肯定要有巨大的疑问了。这不是在自相矛盾吗?前面的例子中,明明在onTouch事件里面返回了false,ACTION_DOWN和ACTION_UP不是都得到执行了吗?其实你只是被假象所迷惑了,让我们仔细分析一下,在前面的例子当中,我们到底返回的是什么。
参考着我们前面分析的源码,首先在onTouch事件里返回了false,就一定会进入到onTouchEvent方法中,然后我们来看一下onTouchEvent方法的细节。由于我们点击了按钮,就会进入到第14行这个if判断的内部,然后你会发现,不管当前的action是什么,最终都一定会走到第89行,返回一个true。
是不是有一种被欺骗的感觉?明明在onTouch事件里返回了false,系统还是在onTouchEvent方法中帮你返回了true。就因为这个原因,才使得前面的例子中ACTION_UP可以得到执行。
那我们可以换一个控件,将按钮替换成ImageView,然后给它也注册一个touch事件,并返回false。如下所示:
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的事件分发机制,感兴趣的朋友可以继续关注~
作者:郭霖 收起阅读 »
在我经过较长时间的筹备之后,终于决定开始写这样一篇文章了。目前虽然网上相关的文章也不少,但我觉得没有哪篇写得特别详细的(也许我还没有找到),多数文章只是讲了讲理论,然后配合demo运行了一下结果。而我准备带着大家从源码的角度进行分析,相信大家可以更加深刻地理解Android事件分发机制。
阅读源码讲究由浅入深,循序渐进,因此我们也从简单的开始,本篇先带大家探究View的事件分发,下篇再去探究难度更高的ViewGroup的事件分发。
那我们现在就开始吧!比如说你当前有一个非常简单的项目,只有一个Activity,并且Activity中只有一个按钮。你可能已经知道,如果想要给这个按钮注册一个点击事件,只需要调用:
button.setOnClickListener(new OnClickListener() {这样在onClick方法里面写实现,就可以在按钮被点击的时候执行。你可能也已经知道,如果想给这个按钮再添加一个touch事件,只需要调用:
02. @Override
03. public void onClick(View v) {
04. Log.d("TAG", "onClick execute");
05. }
06.});
button.setOnTouchListener(new OnTouchListener() {onTouch方法里能做的事情比onClick要多一些,比如判断手指按下、抬起、移动等事件。那么如果我两个事件都注册了,哪一个会先执行呢?我们来试一下就知道了,运行程序点击按钮,打印结果如下:
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执行了两次,一次是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) {这个方法非常的简洁,只有短短几行代码!我们可以看到,在这个方法内,首先是进行了一个判断,如果mOnTouchListener != null,(mViewFlags & ENABLED_MASK) == ENABLED和mOnTouchListener.onTouch(this, event)这三个条件都为真,就返回true,否则就去执行onTouchEvent(event)方法并返回。
02. if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
03. mOnTouchListener.onTouch(this, event)) {
04. return true;
05. }
06. return onTouchEvent(event);
07.}
先看一下第一个条件,mOnTouchListener这个变量是在哪里赋值的呢?我们寻找之后在View里发现了如下方法:
public void setOnTouchListener(OnTouchListener l) {Bingo!找到了,mOnTouchListener正是在setOnTouchListener方法里赋值的,也就是说只要我们给控件注册了touch事件,mOnTouchListener就一定被赋值了。第二个条件(mViewFlags & ENABLED_MASK) == ENABLED是判断当前点击的控件是否是enable的,按钮默认都是enable的,因此这个条件恒定为true。
02. mOnTouchListener = l;
03.}
第三个条件就比较关键了,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) {相较于刚才的dispatchTouchEvent方法,onTouchEvent方法复杂了很多,不过没关系,我们只挑重点看就可以了。首先在第14行我们可以看出,如果该控件是可以点击的就会进入到第16行的switch判断中去,而如果当前的事件是抬起手指,则会进入到MotionEvent.ACTION_UP这个case当中。在经过种种判断之后,会执行到第38行的performClick()方法,那我们进入到这个方法里瞧一瞧:
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.}
public boolean performClick() {可以看到,只要mOnClickListener不是null,就会去调用它的onClick方法,那mOnClickListener又是在哪里赋值的呢?经过寻找后找到如下方法:
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.}
public void setOnClickListener(OnClickListener l) {一切都是那么清楚了!当我们通过调用setOnClickListener方法来给控件注册一个点击事件时,就会给mOnClickListener赋值。然后每当控件被点击时,都会在performClick()方法里回调被点击控件的onClick方法。这样View的整个事件分发的流程就让我们搞清楚了!不过别高兴的太早,现在还没结束,还有一个很重要的知识点需要说明,就是touch事件的层级传递。我们都知道如果给一个控件注册了touch事件,每次点击它的时候都会触发一系列的ACTION_DOWN,ACTION_MOVE,ACTION_UP等事件。这里需要注意,如果你在执行ACTION_DOWN的时候返回了false,后面一系列其它的action就不会再得到执行了。简单的说,就是当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发后一个action。
02. if (!isClickable()) {
03. setClickable(true);
04. }
05. mOnClickListener = l;
06.}
说到这里,很多的朋友肯定要有巨大的疑问了。这不是在自相矛盾吗?前面的例子中,明明在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() {运行一下程序,点击ImageView,你会发现结果如下:
02. @Override
03. public boolean onTouch(View v, MotionEvent event) {
04. Log.d("TAG", "onTouch execute, action " + event.getAction());
05. return false;
06. }
07.});
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的事件分发机制,感兴趣的朋友可以继续关注~
作者:郭霖 收起阅读 »
手把手教你如何从从零开始构建JavaScript模块化加载器
对任何程序,都存在一个规模的问题,起初我们使用函数来组织不同的模块,但是随着应用规模的不断变大,简单的重构函数并不能顺利的解决问题。尤其对JavaScript程序而言,模块化有助于解决我们在前端开发中面临的越来越复杂的需求。
为什么需要模块化
对开发者而言,有很多理由去将程序拆分为小的代码块。这种模块拆分的过程有助于开发者更清晰的阅读和编写代码,并且能够让编程的过程更多的集中在模块的功能实现上,和算法一样,分而治之的思想有助于提高编程生产率。
在下文中,我们将集中讨论JavaScript的模块化开发,并实现一个简单的module loader。
实现模块化
使用函数作为命名空间
在JavaScript中,函数是唯一的可以用来创建新的作用域的途径。考虑到一个最简单的需求,我们通过数字来获得星期值,例如通过数字0得到星期日,通过数字1得到星期一。我们可以编写如下的程序:
对JavaScript中的函数而言,我们可以通过创建立即调用的函数表达式来达到这个效果,我们可以通过如下的方式重构上面的代码,使得私有作用域成为可能:
对代码进一步改进,我们可以利用一个exports对象来达到暴露公共接口的目的,这种方法可以通过如下方法实现,代码如下:
从全局作用域中分离,实现require方法
更进一步的,为了实现模块化,我们可以通过构造一个系统,使得一个函数可以require另一个函数的方式来实现模块化编程。所以我们的目标是,实现一个require方法,通过传入模块名来取得该模块的调用。这种实现方式要比前面的方法更为优雅的体现模块化的理念。对require方法而言,我们需要完成两件事。
我们需要实现一个readFile方法,它能通过给定字符串返回文件的内容。
我们需要能够将返回的字符串作为代码进行执行。
我们假设已经存在了readFile这个方法,我们更加关注的是如何能够将字符串作为可执行的程序代码。通常我们有好几种方法来实现这个需求,最常见的方法是eval操作符,但我们常常在刚学习JavaScript的时候被告知,使用eval是一个非常不明智的决策,因为使用它会导致潜在的安全问题,因此我们放弃这个方法。
一个更好的方法是使用Function构造器,它需要两个参数:使用逗号分隔的参数列表字符串,和函数体字符串。例如:
慢载入模块和AMD
对浏览器编程而言,通常不会使用CommonJS风格的模块化系统,因为对于Web而言,加载一个资源远没有在服务端来的快,这收到网络性能的影响,尤其一个模块如果过大的话,可能会中断方法的执行。Browserify是解决这个问题的一个比较出名的模块化方案。
这个过程大概是这样的:首先检查模块中是否存在require其他模块的语句,如果有,就解析所有相关的模块,然后组装为一个大模块。网站本身为简单的加载这个组装后的大模块。
模块化的另一个解决方案是使用AMD,即异步模块定义,AMD允许通过异步的方式加载模块内容,这种就不会阻塞代码的执行。
我们想要实现的功能大概是这个样子的:
小结
模块通过将代码分离为不同的文件和命名空间,为大型程序提供了清晰的结构。通过构建良好的接口可以使得开发者更加建议的阅读、使用、扩展代码。尤其对JavaScript语言而言,由于天生的缺陷,使得模块化更加有助于程序的组织。在JavaScript的世界,有两种流行的模块化实现方式,一种称为CommonJS,通常是服务端的模块化实现方案,另一种称为AMD,通常针对浏览器环境。其他关于模块化的知识,你可以参考这篇文章。
References
Eloquent JavaScript, chapter 10, Modules
Browserify运行原理分析
Why AMD?
JavaScript模块化知识点小结 收起阅读 »
为什么需要模块化
对开发者而言,有很多理由去将程序拆分为小的代码块。这种模块拆分的过程有助于开发者更清晰的阅读和编写代码,并且能够让编程的过程更多的集中在模块的功能实现上,和算法一样,分而治之的思想有助于提高编程生产率。
在下文中,我们将集中讨论JavaScript的模块化开发,并实现一个简单的module loader。
实现模块化
使用函数作为命名空间
在JavaScript中,函数是唯一的可以用来创建新的作用域的途径。考虑到一个最简单的需求,我们通过数字来获得星期值,例如通过数字0得到星期日,通过数字1得到星期一。我们可以编写如下的程序:
var names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];上面的程序,我们创建了一个函数dayName()来获取星期值。但问题是,names变量被暴露在全局作用域中。更多的时候,我们希望能够构造私有变量,而暴露公共函数作为接口。
function dayName(number) {
return names[number];
}
console.log(dayName(1));
对JavaScript中的函数而言,我们可以通过创建立即调用的函数表达式来达到这个效果,我们可以通过如下的方式重构上面的代码,使得私有作用域成为可能:
var dayName = function() {上面的程序中,我们通过将变量包括在一个函数中,这个函数会立即执行,并返回一个包含两个属性的对象,返回的对象会被赋值给dayName变量。在后面,我们可以通过dayName变量来访问暴露的两个函数接口name和number。
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"));
对代码进一步改进,我们可以利用一个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");下面我们可以来实现require方法了:
console.log(plusOne(5)); // 6
// module.js在页面中使用require函数:
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) { ... }
通过这种方式实现的模块化系统通常被称为是CommonJS模块风格的,Node.js正式使用了这种风格的模块化系统。这里只是提供了一个最简单的实现方法,在真实应用中会有更加精致的实现方法。
demo
慢载入模块和AMD
对浏览器编程而言,通常不会使用CommonJS风格的模块化系统,因为对于Web而言,加载一个资源远没有在服务端来的快,这收到网络性能的影响,尤其一个模块如果过大的话,可能会中断方法的执行。Browserify是解决这个问题的一个比较出名的模块化方案。
这个过程大概是这样的:首先检查模块中是否存在require其他模块的语句,如果有,就解析所有相关的模块,然后组装为一个大模块。网站本身为简单的加载这个组装后的大模块。
模块化的另一个解决方案是使用AMD,即异步模块定义,AMD允许通过异步的方式加载模块内容,这种就不会阻塞代码的执行。
我们想要实现的功能大概是这个样子的:
// index.html 中的部分代码问题的核心是实现define方法,它的第一个参数是定义该模块说需要的依赖列表,参数而是该模块的具体工作函数。一旦所依赖的模块都被加载后,define便会执行参数2所定义的工作函数。weekDay模块的内容大概是下面的内容:
define(["weekDay.js", "today.js"], function (weekDay, today) {
console.log(weekDay.name(today.dayNumber()));
document.write(weekDay.name(today.dayNumber()));
});
// weekDay.js下面我们来关注如何实现define()方法。为了实现这个方法,我们需要定义一个backgroundReadFile()方法来异步的获取文件内容。此外我们需要能够监视模块的加载状态,当模块加载完后能够告诉函数去执行具体的工作函数(回调)。
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)}
}
});
// 通过Ajax来异步加载模块通过实现一个getModule函数,通过给定的模块名进行模块的调度运行工作。同样,我们需要通过缓存的方式避免同一个模块被重复的载入。实现代码如下:
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);
}
// module.js 的部分内容有了getModule()了函数之后,define方法可以借助该方法来为当前模块的依赖列表获取或创建模块对象。define方法的简单实现如下:
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;
}
// module.js 的部分内容关于AMD的更加常见的实现是RequireJS,它提供了AMD风格的更加流行的实现方式。
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();
}
小结
模块通过将代码分离为不同的文件和命名空间,为大型程序提供了清晰的结构。通过构建良好的接口可以使得开发者更加建议的阅读、使用、扩展代码。尤其对JavaScript语言而言,由于天生的缺陷,使得模块化更加有助于程序的组织。在JavaScript的世界,有两种流行的模块化实现方式,一种称为CommonJS,通常是服务端的模块化实现方案,另一种称为AMD,通常针对浏览器环境。其他关于模块化的知识,你可以参考这篇文章。
References
Eloquent JavaScript, chapter 10, Modules
Browserify运行原理分析
Why AMD?
JavaScript模块化知识点小结 收起阅读 »
微信开放平台之公众号第三方平台开发及全网发布验证
微信公众号第三方平台的开放,让公众号运营者在面向垂直行业需求时,可以通过一键登录授权给第三方开发者,来完成相关的处理能力,方便快捷,那如何才能开发出一个公众号第三方平台供一键授权呢?本文以JAVA作为后台服务的实现语言,实现了微信第三方开放平台开发所需要的主要业务流程,并针对全网发布的检测做了相应的代码处理,以通过微信全网检测,可以接入任意的微信公众号。
根据微信第三方平台的审核需求,你需要在微信开放平台上注册第三方平台信息时,提供如下几个主要的服务:
1、授权事件接收服务,对应填写的审核资料中授权事件接收URL,微信会将相关的授权事件信息推送到该REST服务上,推送的主要消息包括验证票据ComponentVerifyTicket和取消授权的公众号AuthorizerAppid,该服务需要对微信推送过来的该类消息立即做出回应并返回success内容,该服务事件的JAVA实现方式如下:
更具体的实现代码如下:
2、公众号消息与事件接收服务,对应填写的审核资料中公众号消息与事件接收URL,微信会将粉丝发送给公众号的消息和事件推送到该REST服务上,微信公众平台要求该消息和事件接收服务在5秒内做出回应,如果5秒内微信公众平台得不到响应消息,粉丝将将收到提示公众号暂时服务提供服务的错误信息。对于需要对粉丝发送的消息走人工渠道做出响应的公众号来说,此时就需要首先接收下消息,将消息交给后来逻辑转人工处理,然后立即以空格消息响应微信公众平台,微信收到空格消息后就会知道该粉丝发送的消息已经被妥善处理,并对该响应不做任何处理,同时不会发起消息重新推送的重试。该服务的JAVA实现实现方式如下:
以上是开发微信第三方开发平台的主要服务代码,想要通过微信全网接入检测并成功发布,还有如下的工作的需要做:
根据消息或事件类型区分后,剩余的逻辑只需要处理成对应的回复内容即可,如下:
以上是微信第三方开放平台开发主要的业务流程,在实际开发中,还有两点需要特别注意:
1、微信要求第三方开放平台必须以密文方式接收消息;
2、在实际部署时,需要更换JAVA安全包相关的内容,否则将出现秘钥长度不够的异常,需要替换的文件包括JAVA_HOME/jre/lib/security/local_policy.jar和 JAVA_HOME/jre/lib/security/US_export_policy.jar这两个文件。
作者:徐正礼 收起阅读 »
根据微信第三方平台的审核需求,你需要在微信开放平台上注册第三方平台信息时,提供如下几个主要的服务:
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-bin/message/custom/send?access_token=" + 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这两个文件。
作者:徐正礼 收起阅读 »
详解:Android开发中常用的 DPI / DP / SP
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之间的换算关系很简单
那么一个20dp的图片,在不同设备上的显示效果如何呢?我们以iPad为例来说明。
iPad 屏幕参数
iPad2 和 iPad Retina的物理尺寸都是 9.7 inch,不同的是分辨率和PPI,一个是1024x768 / 132ppi,另一个是2048x1536 / 264ppi,
分别计算一下20dp对应多少inch
如果只提供了一个大小为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/guide/practices/screens_support.html#DeclaringTabletLayouts
http://developer.android.com/training/multiscreen/screendensities.html
http://stackoverflow.com/questions/2025282/difference-between-px-dp-dip-and-sp-in-android
http://99designs.com/designer-blog/2013/02/26/ppi-vs-dpi-whats-the-difference/
作者:liangfeizc 收起阅读 »
设备机型不同必然也会导致屏幕大小和分辨率(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对应多少inch
ipad2 = 20 * (132 / 160) * (7.9 / (math.sqrt(1024 * 1024 + 768 * 768)))计算结果都是0.1018359375,这就是dp的功能,它能保证在所有的设备上显示的大小都一样。
ipad_retina = 20 * (264 / 160) * (7.9 / (math.sqrt(2048 * 2048 + 1536 * 1536)))
如果只提供了一个大小为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/guide/practices/screens_support.html#DeclaringTabletLayouts
http://developer.android.com/training/multiscreen/screendensities.html
http://stackoverflow.com/questions/2025282/difference-between-px-dp-dip-and-sp-in-android
http://99designs.com/designer-blog/2013/02/26/ppi-vs-dpi-whats-the-difference/
作者:liangfeizc 收起阅读 »
推荐:八个最优秀的 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)”,接受许可并安装软件包。
这个进程只是下载软件包,还没有安装。为了完成安装到图片所示的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是一个功能全面的开发环境,装备了为各种设备——从智能手表到汽车——开发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插件吗,欢迎在留言中告诉我们~ 收起阅读 »
技术分享:开源代码应用之Eclipse篇
开写这篇的时候,恰逢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的回调方法里面,代码如下。
3. 处理项目。
获得Java项目后,获取所有的package,这里的package和通常意义上Java的package不同,具体意义看API,就当课后作业。
再进一步,就可以获取Java源文件,并取得编译单元,有了这个,以后的路就有方向了。
4. 处理Java源文件。
5. 处理Java文件头license声明。
6. 成员变量型license声明。
这种license声明类似下面这个例子。
7. 测试。
启动一个新的调试Eclipse Plug-in的Eclipse Runtime,导入任意几个Java项目,从菜单或工具栏上面选择“Fix License” action,完成之后检查任意的Java源文件,看看license是否已经修复。
来看看一个简单的测试结果吧。
这个是修复前的
这个工具Plug-in可以按Eclipse的标准插件打包并安装,或者生成一个Update Site以供用户在线安装。
好了,啰嗦了这么多,到了该结束的时刻,最后一句,这个小工具所有的源代码已经在GitHub上开源,喜欢可以去下载并测试,源代码里面附有一份详细安装的文档。
工具地址:https://github.com/alexgreenbar/open_tools.git
作者:Alex 收起阅读 »
对于这样一个由全世界最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的内容,分为主license和行内license,具体内容这里就不显示了,然后获取Eclipse里面所有的项目,遍历每个项目并处理,这里只处理打开的项目,如果你有不想处理的项目,关闭就行。
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);
}
3. 处理项目。
private void processProject(IProject project) throws Exception {当然只修复Java项目,没有Java nature的,一律抛弃。
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项目后,获取所有的package,这里的package和通常意义上Java的package不同,具体意义看API,就当课后作业。
再进一步,就可以获取Java源文件,并取得编译单元,有了这个,以后的路就有方向了。
4. 处理Java源文件。
private void processJavaSource(ICompilationUnit unit) throws Exception {这里用到了一些Eclipse Jface text包里面的东西,和Java里面常见的文件读写API有些不一样,但基本思想是一致的。等取到了IDocument对象,就可以开始正式的license处理。
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);
}
}
5. 处理Java文件头license声明。
private void processHeadLicense(IDocument doc) throws Exception {基于AST就可以得到Java源代码里面的所有comments,接下来就可以根据各种情况插入或替换文件头的license声明。
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;
}
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 {成员变量类型的license声明处理起来稍显麻烦,主要原因是牵扯到Java成员变量的创建和解析,但其实也不是很难理解,而且从中可以学到AST是如何精细处理Java类的各个组成部分的。
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;
}
7. 测试。
启动一个新的调试Eclipse Plug-in的Eclipse Runtime,导入任意几个Java项目,从菜单或工具栏上面选择“Fix License” action,完成之后检查任意的Java源文件,看看license是否已经修复。
来看看一个简单的测试结果吧。
这个是修复前的
package com.demo;这个是修复后的
public class Demo {
public Demo() {
}
public void hello() {
}
}
/* IBM Confidential8. 打包分发。
* 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() {
}
}
这个工具Plug-in可以按Eclipse的标准插件打包并安装,或者生成一个Update Site以供用户在线安装。
好了,啰嗦了这么多,到了该结束的时刻,最后一句,这个小工具所有的源代码已经在GitHub上开源,喜欢可以去下载并测试,源代码里面附有一份详细安装的文档。
工具地址:https://github.com/alexgreenbar/open_tools.git
作者:Alex 收起阅读 »
环信CTO马晓宇:开源重燃移动IM大连接梦想
2014年,微信凭借着QQ得天独厚的优势牢牢把控着国内的市场,陌陌通过资本力量洗牌一骑绝尘奔赴美国纳斯达克上市,Facebook 拆资190亿美元收购WhatsApp,买下了一个未来。当人们都认为移动IM局面已经尘埃落定时,众多通过移动IM走向社交类的APP也相继拿到风投,加入了这场无法预知的战争。在2015年的今天,移动互联网将进入活跃度时代,随着人们对手机依赖感的提升,用户需求不断在变化,4G的迅速发展,移动IM又掀起一场入口争夺战。WhatsApp股东的知名风投机构红杉资本高管合伙人Aaref Hilaly曾表示,移动聊天正在重新定义社交网络,诸如Facebook等充斥陌生人关系的社交模式,已经显得没有价值。移动IM将迎来一个巨大的创新机会,无论是社交、电商、教育、甚至嵌入式领域,都可以创造一个大链接时代。
"我们的方向是即时通信云,主要面向多端,除了现有的iOS、Android和Web端,我们实际还在做Linux嵌入式,会支持更多嵌入设备,加入IOT(物体组成的因特网)实现设备之间互联。"
与记者谈起移动IM的发展以及环信未来的方向,作为环信CTO的马晓宇从移动通信领域的实践经验和感悟来表达了自己的看法。同时作为一个开源理想主义者,他和整个团队致力把环信逐步开源,也把开源运用在整个工作环境里面,让企业跟随着开源世界的脚步,坚定在技术路线上前进,让一切都变得美好起来。
马晓宇,环信CTO,从事十七八年程序开发,从最早的IC设计,到后来电信短信网关程序以及网关软件的开发,先后入职Symbian和Nokia公司,从事中间件以及内核软件开发,在移动技术领域有深厚积累,可以说马晓宇伴随着中国移动互联网的成长一路走来。
在环信项目的早期,马晓宇和其他项目的成员看到移动互联网这个大趋势,以及市场的需求,想基于BaaS平台上提供IM以及推送这样的服务,在Key-Value存储、用户体系、权限管理等方面做一套完整的BaaS平台,不过这个项目持续了半年就被终止。面对着这样的打击马晓宇和整个团队也并没有因此而停止整个项目的开发,从非理性的热烈追捧到泡沫破裂,这让他们更加认识到了整个市场用户的需求和开放性及整个生态圈的重要性。2013年底,马晓宇和他们的团队再次出发,他们把之前的项目全部回收,重新定位并聚焦一件事情,那就是即时通讯业务。
在移动互联网依旧澎湃的今天,移动IM在整个市场的冲击下愈演愈烈。不过马晓宇认为移动IM领域仍然蕴藏着巨大的潜力,差异化的市场仍有很大的一部分用户需求。所以他们开始以平台化、入口化为重点,在满足用户的即时通讯需求外,还继续不断的纵向扩展。
可以说2013年是马晓宇和整个团队最艰难的一年。微信、易信、飞信、来往、陌陌等等似乎已经占据了整个移动IM市场,但是移动端即时通讯应用的形式还远没有定型,用户也在不断适应和迭代,一切的确定都是充满不确定性。马晓宇和整个团队也是看到了这个市场,凭借着整个团队有着开源和移动技术背景,一步一步的把环信这个项目带入移动互联网的台面上,在成功的背后也隐藏着很多艰难的决定和方向的抉择。下面就同记者一起来了解马晓宇和环信。
记者:您是基于什么考虑和整个团队一起研发环信这类移动IM产品?
马晓宇:我们环信这个项目从2013年初开始,当时我们看到移动互联网这个大趋势和需求,最早想做一个BaaS平台(后端即服务:Backend as a Service),提供一部分的IM功能,当时也搭建了一个推送功能,加入Key-Value存储,用户体系,权限管理等功能,最终想做一套比较全的BaaS平台,大概有半年多的时间,发现这个项目有一个很大的难度。
主要问题是BaaS对中小开发者很好,不需要有自己的后台,很容易跑起来。但是如果你的业务发展,很难满足个性化的需求定制。实际上在2013年底我们就讨论这个事情,BaaS这个平台基本解决不了用户的个性化需求,所以后来我们就回收了这个项目,然后聚焦做一件事情---也就是现在IM即时通讯。从2013年底我们就重新定位,开始专注即时通讯业务,产品真正上线是在2014年的年中,6月份正式上线。
环信后台架构图
记者:在2012年大家开始提及到BaaS平台,2013年在移动互联网上就有越来越多的人在谈整合BaaS服务,您认为在BaaS服务上比较适合做些什么?
马晓宇:它对中小开发团队特别有效。如果你做移动APP开发,首先要有你的用户系统,还有Key-Value数据存储。然后了解推送社交,甚至朋友圈模块,发现很容易地搭建一套这种APP。
但是用户有什么需求就不好解决,Key-Value即使能存储不同数据,从用户权限管需求这一块就不好办。因为有些应用,比如说只能是领导的秘书可以有一个特定的权限。所以真正到企业级的移动应用,我觉得就比较难做进一步的扩展。我们做了一年这个BaaS平台。
记者:后面有没有继续?
马晓宇:没有,不过我们做BaaS平台的这个团队都有科研背景,我们选了Usergrid Apache这顶级的开源项目,同时我们的主要创始人也是Usergrid Committer,通过技术的演变,基于Usergrid开发环信的用户系统。
记者:据了解你们团队很多高管都有着一些重要的开源项目背景,像Jboss和Hibernate这类顶级的开源项目中也有着丰富的经验,而环信这样的平台大部分的用户都是开发者,而您以及整个团队从某个角度来讲也算是自己产品的用户,也是直接最懂自己用户的。那么您认为最能够让开发者接受的产品最重要的是什么?
马晓宇:对,首先我们用环信,同时我们也用其他的开发者工具,从统计到一些分析、监控、推送,我们自身觉得比较重要的还是服务。包括我们以前做开源社区,有时候也在一些兄弟公司QQ群里看看,基本上环信是唯一一个每天晚上两三点开发者能找到主要技术人员给你回复,我们在服务商还是获得了很好的评价,这也是我们现在能赢得开发者的认可。
记者:从开源的角度看,环信把即时通信云的SDK代码开源了,这对于开发者来说是非常有利部署自己的应用,从这方面我们能够看出环信对开源的态度。请谈谈你们是如何看待开源?以及接下来的开源计划是怎样的?
马晓宇:首先我们是逐步开源的,包括我们的后台。因为我们最终不是靠这个CODE本身,毕竟即时通讯现在都快做成免费市场了,所以我们最终还是要靠服务来赢得市场。
还有一个,我们其实做开源的时间也比较长,利用这个机会做环信也是想做一些尝试,也一直在推动。我们是希望做环信能做到三个成功。
第一个是商业成功,首先是产品有价值,我们做的是toD面向开发者和面向企业SAAS服务,最后我们是能有收入,能有一个合理的利润,能够商业成功。
第二个是技术成功。我们本身的技术栈用到的不光是我们自己研发的技术,同时我们也用了大量的优秀开源软件,从中间件到数据库大多数都是来自国外的。我们在解决移动终端和服务器之间的这种消费程度,或者是跨平台部署。所以我们是希望我们的一个目标,技术成功的目标是说,我们找一个important的问题,然后做一个方案,作为我们商业产品的一部分,同时我们把它进行开源,吸引用户来使用,这也是整个团队需要长期要做的事情。
第三个是团队成功。我们团队有一部分都是从事过开源项目背景,比较向往开源企业的文化和情怀。这个团队成功最好理解,就是说比如我们有同事家在海南,他就可以在海南工作,拿北京的工资,然后隔几个月来一起开会。
我们也开始逐渐尝试,在过完年后有一个同事在泰国工作了一星期,然后回来跟我们分享。这样的结果我们认为还是比较靠谱的,在自己比较向往或者熟悉的地方工作效率完全不一样,思路特别清晰。所以我们是希望在商业公司也实现一些开源的企业文化。
记者:目前市场上即时通信类的产品很多,我个人认为还都比较成熟,但是当技术发展到一定程度的时候,用户往往不会再关注到底技术哪家强,更多的是取决于产品体验以及服务态度。在这方面,您认为环信从哪些方面更能够体现出来?
马晓宇:现在是三方面,一方面还是服务进一步深化,我觉得接入环信平台,提供IM只是第一步,我们现在专门有一个CSM(Cluster System Management 专门用于集群系统管理的中间件),帮助我们的客户成功。通过第一步的发消息到头像、位置、分级、互动、推送等等提高它的活跃度,让用户真正玩起来。所以我们专门有这么一个团队开始组建,帮助这些比较大的APP真正去分析用户指标,通过我们的数据怎么去帮助他们更多的互动,这是我们叫客户成功。
第二步其实大家也在做了,就是做数据挖掘,挖掘用户的行为实际上对APP是非常有利的。比如,我们有一个用户是猎聘网,可以通过挖掘用户和露猎头在上面的一些行为关键词进行分析,根据这些数据去指导他的业务。
第三步就是我们现在逐渐感到有一定用户量后,即时通讯需要保持一个长连接,所以这是有一定费用的。我认为成本优势也是一个优势,企业真正做到百万用户后,每月运维成本可能是几万。但是如果环信能经过优化,提供一半或者四分之一的价格,对企业来讲是挺大一笔费用。
所以总结一下,第一个是我们的客户成功,第二个是数据挖掘,第三个是成本优势。目前客户成功和数据挖掘我们都启动,成本优势我们还在继续优化过程中,下一步主要是在技术上,我们要把成本给优化下来。
记者:环信是基于PaaS平台去做的,而像这样的产品也很多,但是真正的提高核心竞争力我认为还是要加强自己的生态圈建设,在不断的收集用户的需求以及痛点并及时解决,完善和壮大自己的产品,在这方面你们是如何建设的呢?
马晓宇:现在我们是分三层,第一层是我们所有的用户,就是环信的直接用户。
第二层是我们有一个QQ群的形式,一个QQ群有2000人。我们有五个群,现在建了第六个群。然后在这个基础上,我们有一些基于环信的核心开发者,他们会给我们做demo,或者真正地提交了一个完全开源的APP,就是基于环信的。
第三层是我们和一些合作伙伴做的一些探索,专门做个IM Geek开源社区,作为我们整个生态圈的核心部分。
记者:从微信、飞信、易信这些即时通信软件到即时通信云平台,从只需要服务自己一家产品到要服务于成千上万家不同的APP产品,她们的需求也是不一样的,从系统设计和运维的角度,你们如何保证他们的稳定性以及安全性?
马晓宇:不太一样,因为我们是做多租户APP平台,每个APP有不同的需求,所以我们现在是有一个基于APP一些参数可以动态的,这是一部分。
还有一部分是因为多租户,所以我们对数据安全比较看重。我们参与运维项目主要是用户体系比较好,提供了最基本的用户隔离和安全,完全做好用户访问其他APP信息的隔离。
另外从运维层面上,平台上运维用户数据比较关键,系统内部规定只有两人有权限,真正登陆以后是能够看到用户。
还有一部分是大家做云端要解决的一个共享问题,也就是公有云。怎么能保证所有用户体验在操作上不影响其他的用户。最基本的接口有限流,服务器端提供调用接口,可以给你的客户发消息,可以创建群组,可以做用户管理,但这个是有限流的,不能无限制地发,超过限流就会反馈错误。
最后是队列设计。避免有些APP大量给用户发消息,影响其他用户,所以在系统的这些队列设计上,我们也考虑到让APP尽量隔离。现在我们有高速、低速两条通道,如果发现有大量的信息时就会走到低速队列,我们尽量保持用户体验。
记者:从近两年来看,我们都能够感觉到移动互联网都是呈现爆发式增长,还有从今年春节我们也看到了大家都比较倾向通过IM通信这种方式拜年,交流,IM通信功能也会被广泛应用在其他的APP当中,面对这种趋势,我们在这方面如何去保证高稳定性及高并发这种情况?还有应急措施有哪些?
马晓宇:我们会有相应的措施,像您说春节期间的突发状况,我们主要技术人员和运维在春节期间都没怎么休息,就是为了保证整个系统正常运转。首先我们的架构比较好,在后面的数据库、服务器,如果有系统瓶颈的话,我们能够及时加机器。现在我们加机器用的是云,不需要到机房去搭建,基本我们现在做到在分钟级就能够配好整个架构集群。
真正线上我们有标准的监控,通过购买第三方服务监控全国各地到我们服务器的访问情况。对比系统的指标,比如登陆整个集群时间,登陆每台服务器的时间,监控发消息的整个过程时间。然后监控数据库系统的主要的队列,当发现有队列堆积,我们会及时进一步处理。
同时我们也监控DB,主要是DB负载,整个系统是自动化的,有问题会报警,防患于未然。然后如果是真出了紧急情况,在系统里有一些降级开关,如果有紧急情况,保证最小可用。所以现在我们的系统设计,在关键模块是有降级开关的,那么降级开关实际上是要运维,我们每个月有一次演习,比如说我们的缓存宕机,整个储存都宕机,如何做到进一步重新加载数据。还有一些没有出现过的情况,假如数据库宕机,如何切换成直接写到log文件,再把log重新导到数据库恢复。
记者:我们都知道2月27日工信部正式向中国电信和中国联通发放了FDD制式4G牌照,这也意味着移动音频和视频的时代到来了,我认为提高这方面的技术支持给开发者更好的体验将是突破竞争门槛的重要突破点,你们将如何面对这个浪潮?
马晓宇:我们今年上半年主要的开发方向就是将音视频产品化,现在iOS和安卓都支持实时语音,而且可以互通,但是实时视频只有Android支持,iOS系统在今年4月初我们也会出支持实时视频的release版本, 有开发者基于我们视频SDK,做一些视频交流,这也是4G带宽的优势,把之前的不可能变成为可能。
另外除了我们基础功能之外,我们还深入研究,在一对一语音接通率进行优化。大家在Wifi下,中间要跨防火墙,所以有接通率,我们现在在改进这一块。还有在4G网络下,当网络不稳定或弱网络的情况下,我们也做了优化。通过优化对音视频掉包补偿算法,在标准算法的基础上再做进一步优化。
现在看到了很多机会,很多APP开始集成视频功能。包括我们一个合作伙伴,基于我们多人语音开始做智能的导航设备。通过定位,基于4G加入频道通话。现在实验是在3G网,但在使用上设备绑定是4G卡,基于云端。
记者:还是要看未来终端的发展,兼容性好的话可以往上发力。从即时通信产品的多样化来看,环信还是显得比较单一,是做“专”还是做“全”,环信在未来将有哪样的布局?
马晓宇:首先我们的方向是即时通信云,主要面向多端,除了现有的iOS、Android和Web端,我们实际还在做Linux嵌入式,会支持更多嵌入设备,加入IOT(物体组成的因特网)实现设备之间互联。现在很多巨头都在定标准,我们目前只提供SDK,让设备之间互联。
同时,我们从去年年底基于即时通讯提供SaaS服务,它能提供移动应用实时客服,这和以前的呼叫中心不太一样,我们围绕的客户是一些移动应用,进入APP后点击帮助就可以通过IM技术和后台的客服沟通,给你提供帮助。 收起阅读 »
"我们的方向是即时通信云,主要面向多端,除了现有的iOS、Android和Web端,我们实际还在做Linux嵌入式,会支持更多嵌入设备,加入IOT(物体组成的因特网)实现设备之间互联。"
与记者谈起移动IM的发展以及环信未来的方向,作为环信CTO的马晓宇从移动通信领域的实践经验和感悟来表达了自己的看法。同时作为一个开源理想主义者,他和整个团队致力把环信逐步开源,也把开源运用在整个工作环境里面,让企业跟随着开源世界的脚步,坚定在技术路线上前进,让一切都变得美好起来。
马晓宇,环信CTO,从事十七八年程序开发,从最早的IC设计,到后来电信短信网关程序以及网关软件的开发,先后入职Symbian和Nokia公司,从事中间件以及内核软件开发,在移动技术领域有深厚积累,可以说马晓宇伴随着中国移动互联网的成长一路走来。
在环信项目的早期,马晓宇和其他项目的成员看到移动互联网这个大趋势,以及市场的需求,想基于BaaS平台上提供IM以及推送这样的服务,在Key-Value存储、用户体系、权限管理等方面做一套完整的BaaS平台,不过这个项目持续了半年就被终止。面对着这样的打击马晓宇和整个团队也并没有因此而停止整个项目的开发,从非理性的热烈追捧到泡沫破裂,这让他们更加认识到了整个市场用户的需求和开放性及整个生态圈的重要性。2013年底,马晓宇和他们的团队再次出发,他们把之前的项目全部回收,重新定位并聚焦一件事情,那就是即时通讯业务。
在移动互联网依旧澎湃的今天,移动IM在整个市场的冲击下愈演愈烈。不过马晓宇认为移动IM领域仍然蕴藏着巨大的潜力,差异化的市场仍有很大的一部分用户需求。所以他们开始以平台化、入口化为重点,在满足用户的即时通讯需求外,还继续不断的纵向扩展。
可以说2013年是马晓宇和整个团队最艰难的一年。微信、易信、飞信、来往、陌陌等等似乎已经占据了整个移动IM市场,但是移动端即时通讯应用的形式还远没有定型,用户也在不断适应和迭代,一切的确定都是充满不确定性。马晓宇和整个团队也是看到了这个市场,凭借着整个团队有着开源和移动技术背景,一步一步的把环信这个项目带入移动互联网的台面上,在成功的背后也隐藏着很多艰难的决定和方向的抉择。下面就同记者一起来了解马晓宇和环信。
记者:您是基于什么考虑和整个团队一起研发环信这类移动IM产品?
马晓宇:我们环信这个项目从2013年初开始,当时我们看到移动互联网这个大趋势和需求,最早想做一个BaaS平台(后端即服务:Backend as a Service),提供一部分的IM功能,当时也搭建了一个推送功能,加入Key-Value存储,用户体系,权限管理等功能,最终想做一套比较全的BaaS平台,大概有半年多的时间,发现这个项目有一个很大的难度。
主要问题是BaaS对中小开发者很好,不需要有自己的后台,很容易跑起来。但是如果你的业务发展,很难满足个性化的需求定制。实际上在2013年底我们就讨论这个事情,BaaS这个平台基本解决不了用户的个性化需求,所以后来我们就回收了这个项目,然后聚焦做一件事情---也就是现在IM即时通讯。从2013年底我们就重新定位,开始专注即时通讯业务,产品真正上线是在2014年的年中,6月份正式上线。
环信后台架构图
记者:在2012年大家开始提及到BaaS平台,2013年在移动互联网上就有越来越多的人在谈整合BaaS服务,您认为在BaaS服务上比较适合做些什么?
马晓宇:它对中小开发团队特别有效。如果你做移动APP开发,首先要有你的用户系统,还有Key-Value数据存储。然后了解推送社交,甚至朋友圈模块,发现很容易地搭建一套这种APP。
但是用户有什么需求就不好解决,Key-Value即使能存储不同数据,从用户权限管需求这一块就不好办。因为有些应用,比如说只能是领导的秘书可以有一个特定的权限。所以真正到企业级的移动应用,我觉得就比较难做进一步的扩展。我们做了一年这个BaaS平台。
记者:后面有没有继续?
马晓宇:没有,不过我们做BaaS平台的这个团队都有科研背景,我们选了Usergrid Apache这顶级的开源项目,同时我们的主要创始人也是Usergrid Committer,通过技术的演变,基于Usergrid开发环信的用户系统。
记者:据了解你们团队很多高管都有着一些重要的开源项目背景,像Jboss和Hibernate这类顶级的开源项目中也有着丰富的经验,而环信这样的平台大部分的用户都是开发者,而您以及整个团队从某个角度来讲也算是自己产品的用户,也是直接最懂自己用户的。那么您认为最能够让开发者接受的产品最重要的是什么?
马晓宇:对,首先我们用环信,同时我们也用其他的开发者工具,从统计到一些分析、监控、推送,我们自身觉得比较重要的还是服务。包括我们以前做开源社区,有时候也在一些兄弟公司QQ群里看看,基本上环信是唯一一个每天晚上两三点开发者能找到主要技术人员给你回复,我们在服务商还是获得了很好的评价,这也是我们现在能赢得开发者的认可。
记者:从开源的角度看,环信把即时通信云的SDK代码开源了,这对于开发者来说是非常有利部署自己的应用,从这方面我们能够看出环信对开源的态度。请谈谈你们是如何看待开源?以及接下来的开源计划是怎样的?
马晓宇:首先我们是逐步开源的,包括我们的后台。因为我们最终不是靠这个CODE本身,毕竟即时通讯现在都快做成免费市场了,所以我们最终还是要靠服务来赢得市场。
还有一个,我们其实做开源的时间也比较长,利用这个机会做环信也是想做一些尝试,也一直在推动。我们是希望做环信能做到三个成功。
第一个是商业成功,首先是产品有价值,我们做的是toD面向开发者和面向企业SAAS服务,最后我们是能有收入,能有一个合理的利润,能够商业成功。
第二个是技术成功。我们本身的技术栈用到的不光是我们自己研发的技术,同时我们也用了大量的优秀开源软件,从中间件到数据库大多数都是来自国外的。我们在解决移动终端和服务器之间的这种消费程度,或者是跨平台部署。所以我们是希望我们的一个目标,技术成功的目标是说,我们找一个important的问题,然后做一个方案,作为我们商业产品的一部分,同时我们把它进行开源,吸引用户来使用,这也是整个团队需要长期要做的事情。
第三个是团队成功。我们团队有一部分都是从事过开源项目背景,比较向往开源企业的文化和情怀。这个团队成功最好理解,就是说比如我们有同事家在海南,他就可以在海南工作,拿北京的工资,然后隔几个月来一起开会。
我们也开始逐渐尝试,在过完年后有一个同事在泰国工作了一星期,然后回来跟我们分享。这样的结果我们认为还是比较靠谱的,在自己比较向往或者熟悉的地方工作效率完全不一样,思路特别清晰。所以我们是希望在商业公司也实现一些开源的企业文化。
记者:目前市场上即时通信类的产品很多,我个人认为还都比较成熟,但是当技术发展到一定程度的时候,用户往往不会再关注到底技术哪家强,更多的是取决于产品体验以及服务态度。在这方面,您认为环信从哪些方面更能够体现出来?
马晓宇:现在是三方面,一方面还是服务进一步深化,我觉得接入环信平台,提供IM只是第一步,我们现在专门有一个CSM(Cluster System Management 专门用于集群系统管理的中间件),帮助我们的客户成功。通过第一步的发消息到头像、位置、分级、互动、推送等等提高它的活跃度,让用户真正玩起来。所以我们专门有这么一个团队开始组建,帮助这些比较大的APP真正去分析用户指标,通过我们的数据怎么去帮助他们更多的互动,这是我们叫客户成功。
第二步其实大家也在做了,就是做数据挖掘,挖掘用户的行为实际上对APP是非常有利的。比如,我们有一个用户是猎聘网,可以通过挖掘用户和露猎头在上面的一些行为关键词进行分析,根据这些数据去指导他的业务。
第三步就是我们现在逐渐感到有一定用户量后,即时通讯需要保持一个长连接,所以这是有一定费用的。我认为成本优势也是一个优势,企业真正做到百万用户后,每月运维成本可能是几万。但是如果环信能经过优化,提供一半或者四分之一的价格,对企业来讲是挺大一笔费用。
所以总结一下,第一个是我们的客户成功,第二个是数据挖掘,第三个是成本优势。目前客户成功和数据挖掘我们都启动,成本优势我们还在继续优化过程中,下一步主要是在技术上,我们要把成本给优化下来。
记者:环信是基于PaaS平台去做的,而像这样的产品也很多,但是真正的提高核心竞争力我认为还是要加强自己的生态圈建设,在不断的收集用户的需求以及痛点并及时解决,完善和壮大自己的产品,在这方面你们是如何建设的呢?
马晓宇:现在我们是分三层,第一层是我们所有的用户,就是环信的直接用户。
第二层是我们有一个QQ群的形式,一个QQ群有2000人。我们有五个群,现在建了第六个群。然后在这个基础上,我们有一些基于环信的核心开发者,他们会给我们做demo,或者真正地提交了一个完全开源的APP,就是基于环信的。
第三层是我们和一些合作伙伴做的一些探索,专门做个IM Geek开源社区,作为我们整个生态圈的核心部分。
记者:从微信、飞信、易信这些即时通信软件到即时通信云平台,从只需要服务自己一家产品到要服务于成千上万家不同的APP产品,她们的需求也是不一样的,从系统设计和运维的角度,你们如何保证他们的稳定性以及安全性?
马晓宇:不太一样,因为我们是做多租户APP平台,每个APP有不同的需求,所以我们现在是有一个基于APP一些参数可以动态的,这是一部分。
还有一部分是因为多租户,所以我们对数据安全比较看重。我们参与运维项目主要是用户体系比较好,提供了最基本的用户隔离和安全,完全做好用户访问其他APP信息的隔离。
另外从运维层面上,平台上运维用户数据比较关键,系统内部规定只有两人有权限,真正登陆以后是能够看到用户。
还有一部分是大家做云端要解决的一个共享问题,也就是公有云。怎么能保证所有用户体验在操作上不影响其他的用户。最基本的接口有限流,服务器端提供调用接口,可以给你的客户发消息,可以创建群组,可以做用户管理,但这个是有限流的,不能无限制地发,超过限流就会反馈错误。
最后是队列设计。避免有些APP大量给用户发消息,影响其他用户,所以在系统的这些队列设计上,我们也考虑到让APP尽量隔离。现在我们有高速、低速两条通道,如果发现有大量的信息时就会走到低速队列,我们尽量保持用户体验。
记者:从近两年来看,我们都能够感觉到移动互联网都是呈现爆发式增长,还有从今年春节我们也看到了大家都比较倾向通过IM通信这种方式拜年,交流,IM通信功能也会被广泛应用在其他的APP当中,面对这种趋势,我们在这方面如何去保证高稳定性及高并发这种情况?还有应急措施有哪些?
马晓宇:我们会有相应的措施,像您说春节期间的突发状况,我们主要技术人员和运维在春节期间都没怎么休息,就是为了保证整个系统正常运转。首先我们的架构比较好,在后面的数据库、服务器,如果有系统瓶颈的话,我们能够及时加机器。现在我们加机器用的是云,不需要到机房去搭建,基本我们现在做到在分钟级就能够配好整个架构集群。
真正线上我们有标准的监控,通过购买第三方服务监控全国各地到我们服务器的访问情况。对比系统的指标,比如登陆整个集群时间,登陆每台服务器的时间,监控发消息的整个过程时间。然后监控数据库系统的主要的队列,当发现有队列堆积,我们会及时进一步处理。
同时我们也监控DB,主要是DB负载,整个系统是自动化的,有问题会报警,防患于未然。然后如果是真出了紧急情况,在系统里有一些降级开关,如果有紧急情况,保证最小可用。所以现在我们的系统设计,在关键模块是有降级开关的,那么降级开关实际上是要运维,我们每个月有一次演习,比如说我们的缓存宕机,整个储存都宕机,如何做到进一步重新加载数据。还有一些没有出现过的情况,假如数据库宕机,如何切换成直接写到log文件,再把log重新导到数据库恢复。
记者:我们都知道2月27日工信部正式向中国电信和中国联通发放了FDD制式4G牌照,这也意味着移动音频和视频的时代到来了,我认为提高这方面的技术支持给开发者更好的体验将是突破竞争门槛的重要突破点,你们将如何面对这个浪潮?
马晓宇:我们今年上半年主要的开发方向就是将音视频产品化,现在iOS和安卓都支持实时语音,而且可以互通,但是实时视频只有Android支持,iOS系统在今年4月初我们也会出支持实时视频的release版本, 有开发者基于我们视频SDK,做一些视频交流,这也是4G带宽的优势,把之前的不可能变成为可能。
另外除了我们基础功能之外,我们还深入研究,在一对一语音接通率进行优化。大家在Wifi下,中间要跨防火墙,所以有接通率,我们现在在改进这一块。还有在4G网络下,当网络不稳定或弱网络的情况下,我们也做了优化。通过优化对音视频掉包补偿算法,在标准算法的基础上再做进一步优化。
现在看到了很多机会,很多APP开始集成视频功能。包括我们一个合作伙伴,基于我们多人语音开始做智能的导航设备。通过定位,基于4G加入频道通话。现在实验是在3G网,但在使用上设备绑定是4G卡,基于云端。
记者:还是要看未来终端的发展,兼容性好的话可以往上发力。从即时通信产品的多样化来看,环信还是显得比较单一,是做“专”还是做“全”,环信在未来将有哪样的布局?
马晓宇:首先我们的方向是即时通信云,主要面向多端,除了现有的iOS、Android和Web端,我们实际还在做Linux嵌入式,会支持更多嵌入设备,加入IOT(物体组成的因特网)实现设备之间互联。现在很多巨头都在定标准,我们目前只提供SDK,让设备之间互联。
同时,我们从去年年底基于即时通讯提供SaaS服务,它能提供移动应用实时客服,这和以前的呼叫中心不太一样,我们围绕的客户是一些移动应用,进入APP后点击帮助就可以通过IM技术和后台的客服沟通,给你提供帮助。 收起阅读 »
关于发帖礼仪
1. 问题尽量描述清楚,一般需要包括平台、编程语言、版本、错误信息
2. 问题是否得到了解答,需要回帖说明,如果有时间可以总结解决过程并贴出来
3. 如果你认同别人的回复,记得点赞;
4. 如果别人的回帖对你有帮助,记得点“感谢”
2. 问题是否得到了解答,需要回帖说明,如果有时间可以总结解决过程并贴出来
3. 如果你认同别人的回复,记得点赞;
4. 如果别人的回帖对你有帮助,记得点“感谢”
APICloud牵手环信 为你的App量身打造精巧丰富社交功能
移动互联网时代,用户对于移动应用的各种功能的要求正变得越来越高,而即时通讯就是其中之一。说到通信,可能首先想到的是微信、陌陌、易信这样的工具,但实际上,IM通信功能被更广泛地用于其他的移动应用中,例如会议系统、协作工具,或是其他的社交平台中的附属功能。App开发过程中如果让开发者自己去做,不仅耗时耗力,要保证数百万甚至上亿人即时通讯的顺畅、安全,可不是一件易事。
环信是国内即时通讯云服务领域第一品牌,提供基于移动互联网的即时通讯能力,如单聊、群聊、发语音、发图片、发位置、实时音频、实时视频等,通过云端开放的Rest API 和客户端 SDK包的方式提供给开发者和企业。让App内置聊天功能和以前网页中嵌入分享功能一样简单。同时环信还基于领先的即时通讯云技术,提供专业的移动客服平台。
环信一直致力于为开发者提供更稳定更好用的即时通讯云服务,让APP内置聊天功能和以前网页中嵌入分享功能一样简单。环信能够帮助开发者在一天时间内为APP极简、极速集成单聊、群聊等社交功能。截至2015年上半年,环信共服务了23062家APP客户,SDK覆盖用户数高达2.51亿。已覆盖包括移动电商、P2P金融、移动医疗、O2O、移动教育、移动广告、移动游戏、移动新闻、移动旅游、移动健康管理、智能硬件等20大领域的Top10客户。
近日,环信即时通讯云服务提供商与国内领先的“云端一体”移动应用云服务提供商APICloud达成合作。APICloud平台帮助开发者快速实现移动应用的开发、测试、发布、管理和运营的全生命周期管理。APICloud为开发者从“云”和“端”两个方向提供API,简化移动应用开发技术,让移动应用的开发周期从一个月缩短到7天。通过APICloud平台,只需添加移动环信SDK即可快速实现iOS和安卓双平台的即时通讯功能,快速实现App开发。
双方此次强强联合,大大降低了互联网创业的门槛,尤其是让传统行业的中小型商户也有了触网的机会。同时,创业者和开发者可以把精力放到自己的核心业务上,提升用户体验和粘性,来开拓更广阔的商业价值。 收起阅读 »
环信是国内即时通讯云服务领域第一品牌,提供基于移动互联网的即时通讯能力,如单聊、群聊、发语音、发图片、发位置、实时音频、实时视频等,通过云端开放的Rest API 和客户端 SDK包的方式提供给开发者和企业。让App内置聊天功能和以前网页中嵌入分享功能一样简单。同时环信还基于领先的即时通讯云技术,提供专业的移动客服平台。
环信一直致力于为开发者提供更稳定更好用的即时通讯云服务,让APP内置聊天功能和以前网页中嵌入分享功能一样简单。环信能够帮助开发者在一天时间内为APP极简、极速集成单聊、群聊等社交功能。截至2015年上半年,环信共服务了23062家APP客户,SDK覆盖用户数高达2.51亿。已覆盖包括移动电商、P2P金融、移动医疗、O2O、移动教育、移动广告、移动游戏、移动新闻、移动旅游、移动健康管理、智能硬件等20大领域的Top10客户。
近日,环信即时通讯云服务提供商与国内领先的“云端一体”移动应用云服务提供商APICloud达成合作。APICloud平台帮助开发者快速实现移动应用的开发、测试、发布、管理和运营的全生命周期管理。APICloud为开发者从“云”和“端”两个方向提供API,简化移动应用开发技术,让移动应用的开发周期从一个月缩短到7天。通过APICloud平台,只需添加移动环信SDK即可快速实现iOS和安卓双平台的即时通讯功能,快速实现App开发。
双方此次强强联合,大大降低了互联网创业的门槛,尤其是让传统行业的中小型商户也有了触网的机会。同时,创业者和开发者可以把精力放到自己的核心业务上,提升用户体验和粘性,来开拓更广阔的商业价值。 收起阅读 »
imgeek可以用QQ, google登陆了
大家帮测试一下,看有没有问题
关于UIView 你知道多少?
曾经有人这么说过,在iphone里你看到的,摸到的,都是UIView,所以UIView在iphone开发里具有非常重要的作用。那么UIView我们到底知道多少呢。请看看下面的问题, 如果这些你都知道,那么本文章的内容就请绕道,如果你还不太清楚,我想看了下面的内容,你就明白了。
视图和窗口展示了应用的用户界面,同时负责界面的交互。UIKit和其他系统框架提供了很多视图,你可以就地使用而几乎不需要修改。当你需要展示的内容与标准视图允许的有很大的差别时,你也可以定义自己的视图。
不管你是使用系统的视图还是创建自己的视图,你需要理解UIView和UIWindow类所提供的基本结构。这些类提供了复杂的方法来管理视图的布局和展示。理解这些方法的工作非常重要,使你在应用发生改变时可以确认视图有合适的行为。
视图架构 fundamentals
大部分你想要可视化操作都是由视图对象-即UIView类的实例-来进行的。一个视图对象定义了一个屏幕上的一个矩形区域,同时处理该区域的绘制和触屏事件。一个视图也可以作为其他视图的父视图,同时决定着这些子视图的位置和大小。UIView类做了大量的工作去管理这些内部视图的关系,但是需要的时候你也可以定制默认的行为。
视图与Core Animation层联合起来处理着视图内容的解释和动画过渡。每个UIKit框架里的视图都被一个层对象支持(通常是一个CALayer类的实例),它管理管理着后台的视图存储和处理视图相关的动画。然而,当你需要对视图的解释和动画行为有更多的控制权时,你可以使用层。
为了理解视图和层之间的关系,我们可以借助于一些例子。图1-1显示了ViewTransitions样例程序的视图层次及其对底层Core Animation层的关系。应用中的视图包括了一个window(同时也是一个视图),一个通用的表现得像一个容器视图的UIView对象,一个图像视图,一个控制显示用的工具条,和一个工具条按钮(它本身不是一个视图但是在内部管理着一个视图)。(注意这个应用包含了一个额外的图像视图,它是用来实现动画的)。为了简化,同时因为这个视图通常是被隐藏的,所以没把它包含在下面的图中。每个视图都有一个相应的层对象,它可以通过视图礶r属性被访问。(因为工具条按钮不是一个视图,你不能直接访问它的层对象。)在它们的层对象之后是Core Animation的解释对象,最后是用来管理屏幕上的位的硬件缓存。
Figure 1-1 View architecture
使用Core Animation的层对象有很重要的性能意义。一个视图对象的绘制代码需要尽量的少被调用,当它被调用时,其绘制结果会被Core Animation缓存起来并在往后可以被尽可能的重用。重用已经解释过的内容消除了通常需要更新视图的开销昂贵的绘制周期。内容的重用在动画中特别重要,我们可以使用已有的内容,这样比创建新的内容开销更小。
视图层次和子视图管理
除了提供自己的内容之外,一个视图也可以表现得像一个容器。当一个视图包含其他视图时,就在两个视图之间创建了一个父子关系。在这个关系中孩子视图被当作子视图,父视图被当作超视图。创建这样一个关系对应用的可视化和行为都有重要的意义。
在视觉上,子视图隐藏了父视图的内容。如果子视图是完全不透明的,那么子视图所占据的区域就完全的隐藏了父视图的相应区域。如果子视图是部分透明的,那么两个视图在显示在屏幕上之前就混合在一起了。每个父视图都用一个有序的数组存储着它的子视图,存储的顺序会影响到每个子视图的显示效果。如果两个兄弟子视图重叠在一起,后来被加入的那个(或者说是排在子视图数组后面的那个)出现在另一个上面。
父子视图关系也影响着一些视图行为。改变父视图的尺寸会连带着改变子视图的尺寸和位置。在这种情况下,你可以通过合适的配置视图来重定义子视图的尺寸。其他会影响到子视图的改变包括隐藏父视图,改变父视图的alpha值,或者转换父视图。
视图层次的安排也会决定着应用如何去响应事件。在一个具体的视图内部发生的触摸事件通常会被直接发送到该视图去处理。然而,如果该视图没有处理,它会将该事件传递给它的父视图,在响应者链中以此类推。具体视图可能也会传递事件给一个干预响应者对象,像视图控制器。如果没有对象处理这个事件,它最终会到达应用对象,此时通常就被丢弃了。
获取更多关于如何创建视图层次,查看 creating and managing a view hierarchy
视图绘制周期
UIView类使用一个点播绘制模型来展示内容。当一个视图第一次出现在屏幕前,系统会要求它绘制自己的内容。在该流程中,系统会创建一个快照,这个快照是出现在屏幕中的视图内容的可见部分。如果你从来没有改变视图的内容,这个视图的绘制代码可能永远不会再被调用。这个快照图像在大部分涉及到视图的操作中被重用。
如果你确实改变了视图内容,也不会直接的重新绘制视图内容。相反,使用setNeedsDisplay或者 setNeedsDisplayInRect:方法废止该视图,同时让系统在稍候重画内容。系统等待当前运行循环结束,然后开始绘制操作。这个延迟给了你一个机会来废止多个视图,从你的层次中增加或者删除视图,隐藏,重设大小和重定位视图。所有你做的改变会稍候在同一时间反应。
注意:改变一个视图的几何结构不会自动引起系统重画内容。视图的contentMode属性决定了改变几何结构应该如果解释。大部分内容模式在视图的边界内拉伸或者重定位了已有快照,它不会重新创建一个新的快照。获取更多关于内容模式如果影响视图的绘制周期,查看 content modes
当绘制视图内容的时候到了时,真正的绘制流程会根据视图及其配置改变。系统视图通常会实现私有的绘制方法来解释它们的视图,(那些相同的系统视图经常开发接口,好让你可以用来配置视图的真正表现。)对于定制的UIView子类,你通常可以覆盖drawRect:方法并使用该方法来绘制你的视图内容。也有其他方法来提供视图内容,像直接在底部的层设置内容,但是覆盖drawRect:时最通用的技术
内容模式
视图的内容模式控制着视图如何回收内容来响应视图几何结构的变化,也控制着是否需要回收内容。当一个视图第一次显示时,它通常会解释内容,其结果会被底层的层级树捕获为一张位图。在那之后,改变视图的几何结构不会导致重新创建位图。相反,视图中contentMode属性的值决定着这张位图是否该被拉伸,以适应新的边界或者只是简单的被放到角落或者视图的边界。
视图的内容模式在你进行如下操作时被应用:
Figure 1-2
拉伸视图
你可以指定视图的某部分为可拉伸的,以便当视图的尺寸改变时只有可拉伸的部分被影响到。可拉伸的部分通常给按钮或者其他的部分为重复模式的视图。由你指定的可拉伸区域允许沿着两条或者其中一条轴拉伸。当然,当一个视图沿着两条轴拉伸的时候,视图的边界必须也定义了一个重复的模式来避免任何的扭曲。Figure1-3展示了这种扭曲在视图里是怎么表现自己的。每个视图里的原始像素的颜色都自我复制,以便可以填充更大视图的相应区域。
Figure 1-3 拉伸一个按钮的背景
你可以用contentStretch属性来定义一个视图的可拉伸区域。这个属性的值一个边的值被标准化为0.0到1.0之间的矩形。当拉伸这个视图时,系统将视图的当前边界值和放缩因子乘以标准值,以便决定哪些像素需要被拉伸。使用标准值可以减轻每次改变视图的边界值都更新contentStretch属性的需要。
视图的内容模式也在决定如何视图的可拉伸区域的使用中扮演着重要的角色。只有当内容模式可能绘引起视图内容放缩的时候可拉伸区域才会被使用。这意味这你的可拉伸视图只被UIViewContentModeScaleToFill, UIViewContentModeScaleAspectFit和UIViewContentModeScaleAspectFill内容模式。如果你指定了一个将内容弹到边界或者角落的内容模式(这样就没有真正的放缩内容),这个视图会忽视可拉伸区域。
注意:当需要创建一个可拉伸UIImage对象作为视图的背景时,使用contentStretch属性是推荐的。可拉伸视图完全被Core Animation层处理,这样性能通常更好。
嵌入式动画支持
使用层对象来支持视图的其中一个利益是你可以轻松的用动画处理视图相关的改变。动画是与用户进行信息交流的一个有用的方法,而且应该总是在进行应用设计的过程中考虑使用动画。UIView类的很多属性是动画化的-也就是,可以半自动的从一个值动画的变化到另一个值。为了实现这样一个动画,你需要做的只是:
改变这个属性的值
frame - 你可以使用这个来动画的改变视图的尺寸和位置
bounds - 使用这个可以动画的改变视图的尺寸
center - 使用这个可以动画的改变视图的位置
transform - 使用这个可以翻转或者放缩视图
alpha - 使用这个可以改变视图的透明度
backgroundColor - 使用这个可以改变视图的背景颜色
contentStretch - 使用这个可以改变视图内容如何拉伸
动画的一个很重要的地方是用于从一组视图到另一组视图的过渡。通常来说,会用一个视图控制器来管理关系到用户界面的主要变更的动画。例如,涉及到从高层到底层信息的导航的界面,通常会使用一个导航控制器来管理视图的过渡,这些视图显示了数据的每一个连续层面。然而,你也可以使用动画来创建两组视图的过渡,而不是视图控制器。当你想用一个系统提供的视图控制器无法支持的导航方案时你可能会这样做。
除了用UIKit类可以创建动画外,你也可以用Core Animation层来创建动画。在更低层你有更多的在时间或者动画属性上的控制权。
获取更多关于如何创建一个基于视图的动画,查看 Animations
获取更多关于使用Core Animation创建动画的信息,查看Core Animation Programming Guide和Core Animation Cookbook.
视图几何结构和坐标系统
UIKit的默认坐标系统把原点设置在左上角,两条轴往下和右扩展。做标志被表示为浮点数,这样允许内容的精确布局和定位而不管底层的屏幕。Figure1-4展示了相对于屏幕的坐标系统。除了屏幕坐标系统窗口和视图也定义了它们自己的本地坐标系统,这样允许你指定相对于视图或者窗口原点的坐标而不是屏幕。
Figure 1-4 UIKit中的坐标系统
因为每个视图和窗口都定义了它自己的本地坐标系统,你需要留意在任何时间内是哪个坐标系统在起作用。每次绘制或者改变一个视图都是基于一个坐标系统的。在某些绘制中会基于视图本身的坐标系统。在某些几何结构变更中是基于父视图的坐标系统的。UIWindow和UIView类都包含了帮助你从一个坐标系统转换到另一个的方法。
重要:一些iOS技术定义了默认的坐标系统,它们的原点和方向与UIKit的不同。;例如,Core Graphics和OpenGL ES的坐标系统是原点在可视区域的左下角,而y轴往上递增。当绘制或者创建内容时,你的代码应该考虑到一些不同并且适应坐标值。
frame, bounds和center属性之间的关系
主要使用center和frame属性来控制当前视图的几何结构。例如,当在运行时构建你的视图层次或者改变视图的尺寸或者位置时你可以使用这些属性。如果你只是要改变视图的位置,那么推荐使用center属性。center属性的值永远是可用的,即使添加了放缩或者转换因子到视图的转换矩阵当中。但是对于frame属性却不是,当视图的转换矩形不等于原始矩阵时它被当作时无效的。
在绘制的过程中主要使用bounds属性。这个边界矩阵在视图的本地坐标系统被解释。这个矩形的默认原点是(0, 0),它的尺寸也适应frame矩形的尺寸。任何绘制在这个矩形当中的东西都是该视图的可视内容的一部分。如果你改变了bounds矩形的原点,任何你绘制在新矩形的东西都会变成该视图可视内容的一部分。
Figure1-5展示了一个图像视图的frame和bounds矩形之间的关系。图中,图像视图的右上角被定位在父视图坐标系统的(40, 40),它的矩形尺寸为240x380。对于bounds矩形,原点是(0, 0),矩形尺寸也是240x380。
Figure 1-5 视图frame和bounds之间的关系
即使你可以独立的改变frame,bounds和center属性,其中一个改变还是会影响到另外两个属性:
当你设置了frame属性,bounds属性的尺寸值也改变来适应frame矩形的新尺寸。center属性也会改变为新frame矩形的中心值。
视图的框架默认不会被它的父视图框架裁剪。这样的化,任何放置在父视图外的子视图都会被完整的解释。你可以改变这种行为,改变父视图的clipsToBounds属性就可以。不管子视图是否在视觉上被裁剪,触屏事件总是发生在目标视图父视图的bounds矩形。换句话说,如果触摸位于父视图外的那部分视图,那么该事件不会被发送到该视图。
坐标系统转换矩阵
坐标系统转换矩阵给改变视图(或者是它的视图)提供了一个轻松和简易的方法。一个仿射转换是一个数学矩阵,它指定了在坐标系统中的点是怎么被映射到另一个坐标系统中的点。你可以对整个视图应用仿射转换,以基于其父视图来改变视图的尺寸,位置或者朝向。你也可以在你的绘制代码中应用仿射转换,以对已解释内容的独立部分实现相同的操控。如何应用仿射转换是基于这样的上下文的:
当你想实现动画时,通常可以修改视图的transform属性值。例如,你可以使用这个属性来制作一个视图围绕中心点翻转的动画。你不应该在其父视图的坐标空间中用这个属性来永久的改变你的视图,像修改它的位置和尺寸。对于这种类型的改变,你可以修改视图的frame矩形。
注意:当修改视图的transform属性值时,所有的转换都是基于视图的中心点来实现的。
在视图的drawRect:方法中,你可以使用仿射转换来定位或者翻转你想要绘制的项目。相对于在视图某些部位中修正对象的位置,我们更倾向于相对于一个固定点去创建对象,通常是(0, 0),同时在绘制之前使用转换来定位对象。这样的话,如果在视图中对象的位置改变了,你要做的只是修改转换矩阵,这样比为对象重新创建新的位置性能更好开销更低。你可以通过使用CGContextGetCTM方法来获取关于图形上下文的仿射转换,同时可以用Core Graphics的相关方法在绘制中来设置或者修改这个转换矩阵。
当前转换矩阵(CTM)是一个在任何时候都被使用的仿射矩阵。当操控整个视图的几何结构时,CTM就是视图transform属性的值。在drawRect:方法中,CTM是关于图形上下文的仿射矩阵。
每个子视图的坐标系统都是构建在其祖先的坐标系统之上的。所以当你修改一个视图的transform属性,这个改变会影响到视图及其所有的子视图。然而,这些改变只会影响到屏幕上视图的最终解释。因为每个视图都负责绘制自己的内容和对自己的子视图进行布局,所以在绘制和布局的过程中它可以忽略父视图的转换。
Figure1- 6描述了在解释的时候,两个不同的转换因子是如何在视觉上组合起来的。在视图的drawRect:方法中,对一个形状应用一个45度的转换因子会使该形状翻转指定的角度。另外加上一个45度的转换因子会导致整个形状翻转90度。这个形状对于绘制它的视图来讲仍然只是翻转了45度,但是视图自己的转换让它看起来像使翻转了90度。
Figure 1-6 翻转一个视图和它的内容
重要:如果一个视图的transform属性不是其定义时转换矩阵,那么视图的frame属性是未定义的而且必须被忽略。当对视图应用转换时,你必须使用视图的bounds和center属性来获取视图的位置和尺寸。子视图的frame矩形仍然是有效的,因为它们与视图的bounds相关。
获取更多关于在运行时修改视图的transform属性,查看 “Translating, Scaling, and Rotating Views.”获取更多如何在绘制过程中使用转换来定位内容,查看 Drawing and Printing Guide for iOS.
点与像素在iOS中,所有的坐标值和距离都被指定为使用浮点数,其单元值称为点。点的数量随着设备的不同而不同,而且彼此不相关。要明白关于点的最主要一点是它们提供了一个绘制用的固定框架。
Table 1-1 列出了不同iOS设备的分辨率(点度量)。前为宽后为长。只要你依照这些屏幕的尺寸来设计用户界面,你的视图就回被相应的设备正确显示。
Table 1-1
Device Screen dimensions (in points)
iPhone and iPod touch 320 x 480
iPad 768 x 1024
每一种使用基于点度量系统的设备都定义了一个用户坐标空间。这是几乎在你所有的代码都会用到的标准坐标空间。例如,当你要操控视图的几何结构或者调用Core Graphics方法来绘制内容时会用到点和用户坐标空间。即使有时用户坐标空间里的坐标时直接映射到设备屏幕的像素,你还是永远不应该假设这是永远不变的。相反,你应该记住:一个点并不一定对应着屏幕上的一个像素在设备层面,所有由你指定的视图上的坐标在某些点上必须被转化成像素。然而,从用户坐标空间上的点到设备坐标空间上的像素通常由系统来处理。UIKit和Core Graphics都主要使用基于向量的绘制模型,所有的坐标值都被指定为使用点。这样,如果你用Core Graphics画了一条曲线,你会用一些值来指定这条曲线,而不管底层屏幕使用怎样的解决方法。
当你需要处理图像或者其他基于像素的技术,像OpenGL ES时,iOS会帮你管理这些像素。对于存储为应用程序的束中的资源的静态图像文件,iOS定义了一些约定,可以指定不同像素密度的图像,也可以在加载图像时最大限度的适应当前屏幕的解决方案。视图也提供了关于当前放缩因子的信息,以便你可以适当的调整任何基于像素的绘制代码来适应有更高级解决方案的屏幕。在不同屏幕的解决方案中处理基于像素内容的技术可以在"Supporting High-Resolution Screens"和"Drawing and Printing Guide for iOS"找到描述。
视图的运行时交互模型
当用户和界面进行交互时,或者由代码程序性的改变一些东西时,一系列复杂的事件就会发生在UIKit的内部来处理这些交互。在这个系列中的某些点,UIKit唤出你的视图类,同时给它们一个机会去响应程序的行为。理解这些唤出点对于理解视图在哪里融入系统很重要。Figure 1-7 展示了这些事件的基本序列,从用户触屏开始到图形系统更新屏幕内容来响应结束。同样的事件序列也会发生在任何程序性启动的动作。
Figure 1-7 UIKit 与视图对象进行交互
以下的步骤分解了图1-7中的事件序列,既解释了在每一步发生了什么,也解释了应用如何响应
改变视图或者其子视图的属性(frame, bounds, alpha, 等等)
调用setNeedsLayout方法以标记该视图(或者它的子视图)为需要进行布局更新
调用setNeedsDisplay或者setNeedsDisplayInRect:方法以标记该视图(或者它的子视图)需要进行重画
通知一个控制器关于一些数据的更新
当然,哪些事情要做,哪些方法要被调用是由视图来决定的。
a 如果自动重设尺寸的规则在发生作用,UIKit会根据这些规则来调整视图。获取更多关于自动重设尺寸规则如何工作,查看"Handling Layout Changes Automatically Using Autoresizing Rules."
b 如果视图实现了layoutSubviews方法,UIKit会调用它。你可以在你的定制视图中覆盖这个方法同时用它来调整任何子视图的位置和大小。例如,一个提供了巨大滚动区域的视图会需要使用几个子视图作为“瓦块”而不是创建一个不太可能放进内存的巨大视图。在这个方法的实现中,视图会隐藏任何屏幕外的子视图,或者重定位它们然后用来绘制新的可视内容。作为这个流程的一部分,视图的布局代码也可以废止任何需要被重画的视图。
对于显式的定义了drawRect:方法的定制视图,UIKit会调用这个方法。这方法的实现应该尽快重画视图的指定区域,并且不应该再做其他事。不要在这个点上做额外的布局,也不要改变应用的数据模型。提供这个方法仅仅是为了更新视图的可视内容。
标准的系统视图通常不会实现drawRect:方法,但是也会在这个时候管理它们的绘制。
注意:上面的更新模型主要应用于使用标准系统视图和绘制技术的应用。使用OpenGL ES来绘制的应用通常会配置一个单一的全屏视图和直接绘制相关的OpenGL图像上下文。你的视图还是应该处理触屏事件,但是它是全屏的,毋需给子视图布局或者实现drawRect:方法。获取更多关于使用OpenGL ES的信息,查看 OpenGL ES Programming Guide for iOS.
给定之前的一系列步骤,将自己的定制视图整合进去的方法包括:
事件处理方法:
touchesBegan:withEvent:
touchesMoved:withEvent:
touchesEnded:withEvent:
touchesCancelled:withEvent:
layoutSubviews方法
drawRect:方法
这些是视图的最常用的覆盖方法,但是你可能不需要覆盖全部。如果你使用手势识别来处理事件,你不需要覆盖事件处理方法。相似的,如果你的视图没有包含子视图或者它的尺寸不会改变,那就没有理由去覆盖layoutSubviews方法。最后,只有当视图内容会在运行时改变,同时你要用UIKit或者Core Graphics等本地技术来绘制时才需要用到drawRect。
要记住这些是主要的整合点,但是不仅仅只有这些。UIView类中有些方法是专门设计来给子类覆盖的。你应该到UIView Class Reference中查看这些方法的描述,以便在定制时清楚哪些方法适合给你覆盖。
有效使用视图的提示
当你需要绘制一些标准系统视图不能提供的内容时,定制视图是很有用的。但是你要负责保证视图的性能要足够的高。UIKit会尽可能的优化视图相关的行为,也会帮助你提高性能。然而,考虑一些提示可以帮助到UIKit。
重要:在调整绘制代码之前,你应该一直收集与你视图当前性能有关的数据。估量当前性能让你可以确定是否真的有问题,同时如果真的有问题,它也提供一个基线,让你在未来的优化中可以比较。
视图不会总是有一个相应的视图控制器
在应用中,视图和视图控制器之间的一对一关系是很少见的。视图控制器的工作是管理一个视图层次,而视图层次经常是包含了多个视图,它们都有自包含特性。对于iPhone应用,每个视图层次通常都填满了整个屏幕,尽管对于iPad应用来说不是。
当你设计用户界面的时候,考虑到视图控制器的所扮演的角色是很重要的。视图控制器提供了很多重要的行为,像协调视图的展示,协调视图的剔除,释放内存以响应低内存警告,还有翻转视图以响应界面的方向变更。逃避这些行为会导致应用发生错误。
获取更多关于视图控制器的信息,查看 View Controller Programming Guide for iOS
最小化定制的绘画
尽管定制的绘画有时是需要的,但是你也应该尽量避免它。真正需要定制绘画的时候是已有的视图类无法提供足够的表现和能力时。任何时候你的内容都应该可以被组装到其他视图,最好结果时组合那些视图对象到定制的视图层次
利用内容模式
内容模式可以最小化重画视图要花费的时间。默认的,视图使用UIViewContentModeScaleToFill 内容模式,这个模式会放缩视图的已有内容来填充视图的frame矩形。需要时你可以改变这个模式来调整你的内容,但是应该避免使用UIViewContentModeRedraw内容模式。不管哪个内容模式发生作用,你都可以调用setNeedsDisplay或者setNeedsDisplayInRect:方法来强制视图重画它的内容。
可能的话将视图声明为不透明
UIKit使用opaque属性来决定它是否可以优化组合操作。将一个定制视图的这个属性设置为YES会告诉UIKit不需要解释任何在该视图后的内容。这样可以为你的绘制代码提高性能并且是推荐的。当然,如果你将这个属性设置为YES,你的视图一定要用不透明的内容完全填充它的bounds矩形。
滚动时调整视图的绘制行为
滚动会导致数个视图在短时间内更新。如果视图的绘制代码没有被适当的调整,滚动的性能会非常的缓慢。相对于总是保证视图内容的平庸,我们更倾向于考虑滚动操作开始时改变视图行为。例如,你可以暂时减少已解释的内容,或者在滚动的时候改变内容模式。当滚动停止时,你可以将视图返回到前一状态,同时需要时更新内容。
不要嵌入子视图来定制控制
尽管在技术上增加子视图到标准系统控制对象-继承自UIControl的类-是可行的,你还是永远不应该用这种方法来定制它们。控制对象支持定制,它们有显式并且良好归档的接口。例如,UIButton类包含了设置标题和背景图片的方法。使用已定义好的定制点意味着你的代码总是会正确的工作。不用这些方法,而嵌入一个定制的图像视图或者标签到按钮中去会导致应用出现未预期的结果。 收起阅读 »
- bounds和frame分别表示什么?
- ContentMode里UIViewContentModeScaleToFill代表什么?
- contentStretch 里的指定UIView里缩放区域是如何计算的?
- UIVIew里的哪些属性变化可以用动画来呈现?
- UIKit的坐标系和Core Graphics的坐标系的差别是什么?
视图和窗口展示了应用的用户界面,同时负责界面的交互。UIKit和其他系统框架提供了很多视图,你可以就地使用而几乎不需要修改。当你需要展示的内容与标准视图允许的有很大的差别时,你也可以定义自己的视图。
不管你是使用系统的视图还是创建自己的视图,你需要理解UIView和UIWindow类所提供的基本结构。这些类提供了复杂的方法来管理视图的布局和展示。理解这些方法的工作非常重要,使你在应用发生改变时可以确认视图有合适的行为。
视图架构 fundamentals
大部分你想要可视化操作都是由视图对象-即UIView类的实例-来进行的。一个视图对象定义了一个屏幕上的一个矩形区域,同时处理该区域的绘制和触屏事件。一个视图也可以作为其他视图的父视图,同时决定着这些子视图的位置和大小。UIView类做了大量的工作去管理这些内部视图的关系,但是需要的时候你也可以定制默认的行为。
视图与Core Animation层联合起来处理着视图内容的解释和动画过渡。每个UIKit框架里的视图都被一个层对象支持(通常是一个CALayer类的实例),它管理管理着后台的视图存储和处理视图相关的动画。然而,当你需要对视图的解释和动画行为有更多的控制权时,你可以使用层。
为了理解视图和层之间的关系,我们可以借助于一些例子。图1-1显示了ViewTransitions样例程序的视图层次及其对底层Core Animation层的关系。应用中的视图包括了一个window(同时也是一个视图),一个通用的表现得像一个容器视图的UIView对象,一个图像视图,一个控制显示用的工具条,和一个工具条按钮(它本身不是一个视图但是在内部管理着一个视图)。(注意这个应用包含了一个额外的图像视图,它是用来实现动画的)。为了简化,同时因为这个视图通常是被隐藏的,所以没把它包含在下面的图中。每个视图都有一个相应的层对象,它可以通过视图礶r属性被访问。(因为工具条按钮不是一个视图,你不能直接访问它的层对象。)在它们的层对象之后是Core Animation的解释对象,最后是用来管理屏幕上的位的硬件缓存。
Figure 1-1 View architecture
使用Core Animation的层对象有很重要的性能意义。一个视图对象的绘制代码需要尽量的少被调用,当它被调用时,其绘制结果会被Core Animation缓存起来并在往后可以被尽可能的重用。重用已经解释过的内容消除了通常需要更新视图的开销昂贵的绘制周期。内容的重用在动画中特别重要,我们可以使用已有的内容,这样比创建新的内容开销更小。
视图层次和子视图管理
除了提供自己的内容之外,一个视图也可以表现得像一个容器。当一个视图包含其他视图时,就在两个视图之间创建了一个父子关系。在这个关系中孩子视图被当作子视图,父视图被当作超视图。创建这样一个关系对应用的可视化和行为都有重要的意义。
在视觉上,子视图隐藏了父视图的内容。如果子视图是完全不透明的,那么子视图所占据的区域就完全的隐藏了父视图的相应区域。如果子视图是部分透明的,那么两个视图在显示在屏幕上之前就混合在一起了。每个父视图都用一个有序的数组存储着它的子视图,存储的顺序会影响到每个子视图的显示效果。如果两个兄弟子视图重叠在一起,后来被加入的那个(或者说是排在子视图数组后面的那个)出现在另一个上面。
父子视图关系也影响着一些视图行为。改变父视图的尺寸会连带着改变子视图的尺寸和位置。在这种情况下,你可以通过合适的配置视图来重定义子视图的尺寸。其他会影响到子视图的改变包括隐藏父视图,改变父视图的alpha值,或者转换父视图。
视图层次的安排也会决定着应用如何去响应事件。在一个具体的视图内部发生的触摸事件通常会被直接发送到该视图去处理。然而,如果该视图没有处理,它会将该事件传递给它的父视图,在响应者链中以此类推。具体视图可能也会传递事件给一个干预响应者对象,像视图控制器。如果没有对象处理这个事件,它最终会到达应用对象,此时通常就被丢弃了。
获取更多关于如何创建视图层次,查看 creating and managing a view hierarchy
视图绘制周期
UIView类使用一个点播绘制模型来展示内容。当一个视图第一次出现在屏幕前,系统会要求它绘制自己的内容。在该流程中,系统会创建一个快照,这个快照是出现在屏幕中的视图内容的可见部分。如果你从来没有改变视图的内容,这个视图的绘制代码可能永远不会再被调用。这个快照图像在大部分涉及到视图的操作中被重用。
如果你确实改变了视图内容,也不会直接的重新绘制视图内容。相反,使用setNeedsDisplay或者 setNeedsDisplayInRect:方法废止该视图,同时让系统在稍候重画内容。系统等待当前运行循环结束,然后开始绘制操作。这个延迟给了你一个机会来废止多个视图,从你的层次中增加或者删除视图,隐藏,重设大小和重定位视图。所有你做的改变会稍候在同一时间反应。
注意:改变一个视图的几何结构不会自动引起系统重画内容。视图的contentMode属性决定了改变几何结构应该如果解释。大部分内容模式在视图的边界内拉伸或者重定位了已有快照,它不会重新创建一个新的快照。获取更多关于内容模式如果影响视图的绘制周期,查看 content modes
当绘制视图内容的时候到了时,真正的绘制流程会根据视图及其配置改变。系统视图通常会实现私有的绘制方法来解释它们的视图,(那些相同的系统视图经常开发接口,好让你可以用来配置视图的真正表现。)对于定制的UIView子类,你通常可以覆盖drawRect:方法并使用该方法来绘制你的视图内容。也有其他方法来提供视图内容,像直接在底部的层设置内容,但是覆盖drawRect:时最通用的技术
内容模式
视图的内容模式控制着视图如何回收内容来响应视图几何结构的变化,也控制着是否需要回收内容。当一个视图第一次显示时,它通常会解释内容,其结果会被底层的层级树捕获为一张位图。在那之后,改变视图的几何结构不会导致重新创建位图。相反,视图中contentMode属性的值决定着这张位图是否该被拉伸,以适应新的边界或者只是简单的被放到角落或者视图的边界。
视图的内容模式在你进行如下操作时被应用:
- 改变视图frame或者bounds矩形的宽度或者高度时。
- 赋值给视图的transform属性,新的转换包括一个放缩因子。
- 大部分视图的contentMode值是UIViewContentModeScaleToFiill,它使视图的内容被放缩到适合新框架的值。Figure 1-2展示了使用其他可用的内容模式的结果。正如你在图中所看到的那样,不是所有的内容模式都可以填充视图的范围,可以的模式可能会扭曲内容。
- 内容模式很好的支持了视图的内容回收,但是当你想视图在放缩和重设尺寸的操作中重绘你也可以用UIViewContentModeRedraw内容模式。设置这个值绘强制系统调用视图的drawRect:方法来响应几何结构的变化。通常来讲,你应该尽可能的避免使用这个模式,同时你不应该在标准的系统视图中使用这个模式。
- 获取更多骨干与可用的内容模式,查看UIView Class Reference
Figure 1-2
拉伸视图
你可以指定视图的某部分为可拉伸的,以便当视图的尺寸改变时只有可拉伸的部分被影响到。可拉伸的部分通常给按钮或者其他的部分为重复模式的视图。由你指定的可拉伸区域允许沿着两条或者其中一条轴拉伸。当然,当一个视图沿着两条轴拉伸的时候,视图的边界必须也定义了一个重复的模式来避免任何的扭曲。Figure1-3展示了这种扭曲在视图里是怎么表现自己的。每个视图里的原始像素的颜色都自我复制,以便可以填充更大视图的相应区域。
Figure 1-3 拉伸一个按钮的背景
你可以用contentStretch属性来定义一个视图的可拉伸区域。这个属性的值一个边的值被标准化为0.0到1.0之间的矩形。当拉伸这个视图时,系统将视图的当前边界值和放缩因子乘以标准值,以便决定哪些像素需要被拉伸。使用标准值可以减轻每次改变视图的边界值都更新contentStretch属性的需要。
视图的内容模式也在决定如何视图的可拉伸区域的使用中扮演着重要的角色。只有当内容模式可能绘引起视图内容放缩的时候可拉伸区域才会被使用。这意味这你的可拉伸视图只被UIViewContentModeScaleToFill, UIViewContentModeScaleAspectFit和UIViewContentModeScaleAspectFill内容模式。如果你指定了一个将内容弹到边界或者角落的内容模式(这样就没有真正的放缩内容),这个视图会忽视可拉伸区域。
注意:当需要创建一个可拉伸UIImage对象作为视图的背景时,使用contentStretch属性是推荐的。可拉伸视图完全被Core Animation层处理,这样性能通常更好。
嵌入式动画支持
使用层对象来支持视图的其中一个利益是你可以轻松的用动画处理视图相关的改变。动画是与用户进行信息交流的一个有用的方法,而且应该总是在进行应用设计的过程中考虑使用动画。UIView类的很多属性是动画化的-也就是,可以半自动的从一个值动画的变化到另一个值。为了实现这样一个动画,你需要做的只是:
- 告诉UIKit你想要实现一个动画
改变这个属性的值
- 在一个UIView对象中有以下的动画化属性:
frame - 你可以使用这个来动画的改变视图的尺寸和位置
bounds - 使用这个可以动画的改变视图的尺寸
center - 使用这个可以动画的改变视图的位置
transform - 使用这个可以翻转或者放缩视图
alpha - 使用这个可以改变视图的透明度
backgroundColor - 使用这个可以改变视图的背景颜色
contentStretch - 使用这个可以改变视图内容如何拉伸
动画的一个很重要的地方是用于从一组视图到另一组视图的过渡。通常来说,会用一个视图控制器来管理关系到用户界面的主要变更的动画。例如,涉及到从高层到底层信息的导航的界面,通常会使用一个导航控制器来管理视图的过渡,这些视图显示了数据的每一个连续层面。然而,你也可以使用动画来创建两组视图的过渡,而不是视图控制器。当你想用一个系统提供的视图控制器无法支持的导航方案时你可能会这样做。
除了用UIKit类可以创建动画外,你也可以用Core Animation层来创建动画。在更低层你有更多的在时间或者动画属性上的控制权。
获取更多关于如何创建一个基于视图的动画,查看 Animations
获取更多关于使用Core Animation创建动画的信息,查看Core Animation Programming Guide和Core Animation Cookbook.
视图几何结构和坐标系统
UIKit的默认坐标系统把原点设置在左上角,两条轴往下和右扩展。做标志被表示为浮点数,这样允许内容的精确布局和定位而不管底层的屏幕。Figure1-4展示了相对于屏幕的坐标系统。除了屏幕坐标系统窗口和视图也定义了它们自己的本地坐标系统,这样允许你指定相对于视图或者窗口原点的坐标而不是屏幕。
Figure 1-4 UIKit中的坐标系统
因为每个视图和窗口都定义了它自己的本地坐标系统,你需要留意在任何时间内是哪个坐标系统在起作用。每次绘制或者改变一个视图都是基于一个坐标系统的。在某些绘制中会基于视图本身的坐标系统。在某些几何结构变更中是基于父视图的坐标系统的。UIWindow和UIView类都包含了帮助你从一个坐标系统转换到另一个的方法。
重要:一些iOS技术定义了默认的坐标系统,它们的原点和方向与UIKit的不同。;例如,Core Graphics和OpenGL ES的坐标系统是原点在可视区域的左下角,而y轴往上递增。当绘制或者创建内容时,你的代码应该考虑到一些不同并且适应坐标值。
frame, bounds和center属性之间的关系
- 视图对象使用frame, bounds和center属性来跟踪它的尺寸和位置:
- frame属性包含了frame矩形,指定了在父视图坐标系统中该视图的尺寸和位置。
- center属性包含了在父视图坐标系统中的已知中心点。
- bounds属性包含了边界矩形,指定了在视图本地坐标系统中视图的尺寸。
主要使用center和frame属性来控制当前视图的几何结构。例如,当在运行时构建你的视图层次或者改变视图的尺寸或者位置时你可以使用这些属性。如果你只是要改变视图的位置,那么推荐使用center属性。center属性的值永远是可用的,即使添加了放缩或者转换因子到视图的转换矩阵当中。但是对于frame属性却不是,当视图的转换矩形不等于原始矩阵时它被当作时无效的。
在绘制的过程中主要使用bounds属性。这个边界矩阵在视图的本地坐标系统被解释。这个矩形的默认原点是(0, 0),它的尺寸也适应frame矩形的尺寸。任何绘制在这个矩形当中的东西都是该视图的可视内容的一部分。如果你改变了bounds矩形的原点,任何你绘制在新矩形的东西都会变成该视图可视内容的一部分。
Figure1-5展示了一个图像视图的frame和bounds矩形之间的关系。图中,图像视图的右上角被定位在父视图坐标系统的(40, 40),它的矩形尺寸为240x380。对于bounds矩形,原点是(0, 0),矩形尺寸也是240x380。
Figure 1-5 视图frame和bounds之间的关系
即使你可以独立的改变frame,bounds和center属性,其中一个改变还是会影响到另外两个属性:
当你设置了frame属性,bounds属性的尺寸值也改变来适应frame矩形的新尺寸。center属性也会改变为新frame矩形的中心值。
- 当你设置了center属性,frame的原点也会相应的改变。
- 当你设置了bounds属性,frame属性会改变以适应bounds矩形的新尺寸。
视图的框架默认不会被它的父视图框架裁剪。这样的化,任何放置在父视图外的子视图都会被完整的解释。你可以改变这种行为,改变父视图的clipsToBounds属性就可以。不管子视图是否在视觉上被裁剪,触屏事件总是发生在目标视图父视图的bounds矩形。换句话说,如果触摸位于父视图外的那部分视图,那么该事件不会被发送到该视图。
坐标系统转换矩阵
坐标系统转换矩阵给改变视图(或者是它的视图)提供了一个轻松和简易的方法。一个仿射转换是一个数学矩阵,它指定了在坐标系统中的点是怎么被映射到另一个坐标系统中的点。你可以对整个视图应用仿射转换,以基于其父视图来改变视图的尺寸,位置或者朝向。你也可以在你的绘制代码中应用仿射转换,以对已解释内容的独立部分实现相同的操控。如何应用仿射转换是基于这样的上下文的:
- 为了修改整个视图,可以修改视图transform属性的仿射转换值。
- 为了在视图中的drawRect:方法中修改内容的指定部分,可以修改与当前图形上下文相关的仿射转换。
当你想实现动画时,通常可以修改视图的transform属性值。例如,你可以使用这个属性来制作一个视图围绕中心点翻转的动画。你不应该在其父视图的坐标空间中用这个属性来永久的改变你的视图,像修改它的位置和尺寸。对于这种类型的改变,你可以修改视图的frame矩形。
注意:当修改视图的transform属性值时,所有的转换都是基于视图的中心点来实现的。
在视图的drawRect:方法中,你可以使用仿射转换来定位或者翻转你想要绘制的项目。相对于在视图某些部位中修正对象的位置,我们更倾向于相对于一个固定点去创建对象,通常是(0, 0),同时在绘制之前使用转换来定位对象。这样的话,如果在视图中对象的位置改变了,你要做的只是修改转换矩阵,这样比为对象重新创建新的位置性能更好开销更低。你可以通过使用CGContextGetCTM方法来获取关于图形上下文的仿射转换,同时可以用Core Graphics的相关方法在绘制中来设置或者修改这个转换矩阵。
当前转换矩阵(CTM)是一个在任何时候都被使用的仿射矩阵。当操控整个视图的几何结构时,CTM就是视图transform属性的值。在drawRect:方法中,CTM是关于图形上下文的仿射矩阵。
每个子视图的坐标系统都是构建在其祖先的坐标系统之上的。所以当你修改一个视图的transform属性,这个改变会影响到视图及其所有的子视图。然而,这些改变只会影响到屏幕上视图的最终解释。因为每个视图都负责绘制自己的内容和对自己的子视图进行布局,所以在绘制和布局的过程中它可以忽略父视图的转换。
Figure1- 6描述了在解释的时候,两个不同的转换因子是如何在视觉上组合起来的。在视图的drawRect:方法中,对一个形状应用一个45度的转换因子会使该形状翻转指定的角度。另外加上一个45度的转换因子会导致整个形状翻转90度。这个形状对于绘制它的视图来讲仍然只是翻转了45度,但是视图自己的转换让它看起来像使翻转了90度。
Figure 1-6 翻转一个视图和它的内容
重要:如果一个视图的transform属性不是其定义时转换矩阵,那么视图的frame属性是未定义的而且必须被忽略。当对视图应用转换时,你必须使用视图的bounds和center属性来获取视图的位置和尺寸。子视图的frame矩形仍然是有效的,因为它们与视图的bounds相关。
获取更多关于在运行时修改视图的transform属性,查看 “Translating, Scaling, and Rotating Views.”获取更多如何在绘制过程中使用转换来定位内容,查看 Drawing and Printing Guide for iOS.
点与像素在iOS中,所有的坐标值和距离都被指定为使用浮点数,其单元值称为点。点的数量随着设备的不同而不同,而且彼此不相关。要明白关于点的最主要一点是它们提供了一个绘制用的固定框架。
Table 1-1 列出了不同iOS设备的分辨率(点度量)。前为宽后为长。只要你依照这些屏幕的尺寸来设计用户界面,你的视图就回被相应的设备正确显示。
Table 1-1
Device Screen dimensions (in points)
iPhone and iPod touch 320 x 480
iPad 768 x 1024
每一种使用基于点度量系统的设备都定义了一个用户坐标空间。这是几乎在你所有的代码都会用到的标准坐标空间。例如,当你要操控视图的几何结构或者调用Core Graphics方法来绘制内容时会用到点和用户坐标空间。即使有时用户坐标空间里的坐标时直接映射到设备屏幕的像素,你还是永远不应该假设这是永远不变的。相反,你应该记住:一个点并不一定对应着屏幕上的一个像素在设备层面,所有由你指定的视图上的坐标在某些点上必须被转化成像素。然而,从用户坐标空间上的点到设备坐标空间上的像素通常由系统来处理。UIKit和Core Graphics都主要使用基于向量的绘制模型,所有的坐标值都被指定为使用点。这样,如果你用Core Graphics画了一条曲线,你会用一些值来指定这条曲线,而不管底层屏幕使用怎样的解决方法。
当你需要处理图像或者其他基于像素的技术,像OpenGL ES时,iOS会帮你管理这些像素。对于存储为应用程序的束中的资源的静态图像文件,iOS定义了一些约定,可以指定不同像素密度的图像,也可以在加载图像时最大限度的适应当前屏幕的解决方案。视图也提供了关于当前放缩因子的信息,以便你可以适当的调整任何基于像素的绘制代码来适应有更高级解决方案的屏幕。在不同屏幕的解决方案中处理基于像素内容的技术可以在"Supporting High-Resolution Screens"和"Drawing and Printing Guide for iOS"找到描述。
视图的运行时交互模型
当用户和界面进行交互时,或者由代码程序性的改变一些东西时,一系列复杂的事件就会发生在UIKit的内部来处理这些交互。在这个系列中的某些点,UIKit唤出你的视图类,同时给它们一个机会去响应程序的行为。理解这些唤出点对于理解视图在哪里融入系统很重要。Figure 1-7 展示了这些事件的基本序列,从用户触屏开始到图形系统更新屏幕内容来响应结束。同样的事件序列也会发生在任何程序性启动的动作。
Figure 1-7 UIKit 与视图对象进行交互
以下的步骤分解了图1-7中的事件序列,既解释了在每一步发生了什么,也解释了应用如何响应
- 用户触屏
- 硬件报告触摸事件给UIKit框架
- UIKit框架将触摸事件打包成UIEvent对象,同时分发给适合的视图。(对于UIKit框架如何提交事件给视图的详细解释,查看 Event Handing Guide for iOS)
- 视图中的事件处理代码可能进行以下的动作来响应:
改变视图或者其子视图的属性(frame, bounds, alpha, 等等)
调用setNeedsLayout方法以标记该视图(或者它的子视图)为需要进行布局更新
调用setNeedsDisplay或者setNeedsDisplayInRect:方法以标记该视图(或者它的子视图)需要进行重画
通知一个控制器关于一些数据的更新
当然,哪些事情要做,哪些方法要被调用是由视图来决定的。
- 5 如果一个视图的几何结构改变了,UIKit会根据以下几条规则来更新它的子视图:
a 如果自动重设尺寸的规则在发生作用,UIKit会根据这些规则来调整视图。获取更多关于自动重设尺寸规则如何工作,查看"Handling Layout Changes Automatically Using Autoresizing Rules."
b 如果视图实现了layoutSubviews方法,UIKit会调用它。你可以在你的定制视图中覆盖这个方法同时用它来调整任何子视图的位置和大小。例如,一个提供了巨大滚动区域的视图会需要使用几个子视图作为“瓦块”而不是创建一个不太可能放进内存的巨大视图。在这个方法的实现中,视图会隐藏任何屏幕外的子视图,或者重定位它们然后用来绘制新的可视内容。作为这个流程的一部分,视图的布局代码也可以废止任何需要被重画的视图。
- 如果任何视图的任何部分被标记为需要重画,UIKit会要求视图重画自身。
对于显式的定义了drawRect:方法的定制视图,UIKit会调用这个方法。这方法的实现应该尽快重画视图的指定区域,并且不应该再做其他事。不要在这个点上做额外的布局,也不要改变应用的数据模型。提供这个方法仅仅是为了更新视图的可视内容。
标准的系统视图通常不会实现drawRect:方法,但是也会在这个时候管理它们的绘制。
- 任何已经更新的视图会与应用余下的可视内容组合在一起,同时被发送到图形硬件去显示。
- 图形硬件将已解释内容转化到屏幕上。
注意:上面的更新模型主要应用于使用标准系统视图和绘制技术的应用。使用OpenGL ES来绘制的应用通常会配置一个单一的全屏视图和直接绘制相关的OpenGL图像上下文。你的视图还是应该处理触屏事件,但是它是全屏的,毋需给子视图布局或者实现drawRect:方法。获取更多关于使用OpenGL ES的信息,查看 OpenGL ES Programming Guide for iOS.
给定之前的一系列步骤,将自己的定制视图整合进去的方法包括:
事件处理方法:
touchesBegan:withEvent:
touchesMoved:withEvent:
touchesEnded:withEvent:
touchesCancelled:withEvent:
layoutSubviews方法
drawRect:方法
这些是视图的最常用的覆盖方法,但是你可能不需要覆盖全部。如果你使用手势识别来处理事件,你不需要覆盖事件处理方法。相似的,如果你的视图没有包含子视图或者它的尺寸不会改变,那就没有理由去覆盖layoutSubviews方法。最后,只有当视图内容会在运行时改变,同时你要用UIKit或者Core Graphics等本地技术来绘制时才需要用到drawRect。
要记住这些是主要的整合点,但是不仅仅只有这些。UIView类中有些方法是专门设计来给子类覆盖的。你应该到UIView Class Reference中查看这些方法的描述,以便在定制时清楚哪些方法适合给你覆盖。
有效使用视图的提示
当你需要绘制一些标准系统视图不能提供的内容时,定制视图是很有用的。但是你要负责保证视图的性能要足够的高。UIKit会尽可能的优化视图相关的行为,也会帮助你提高性能。然而,考虑一些提示可以帮助到UIKit。
重要:在调整绘制代码之前,你应该一直收集与你视图当前性能有关的数据。估量当前性能让你可以确定是否真的有问题,同时如果真的有问题,它也提供一个基线,让你在未来的优化中可以比较。
视图不会总是有一个相应的视图控制器
在应用中,视图和视图控制器之间的一对一关系是很少见的。视图控制器的工作是管理一个视图层次,而视图层次经常是包含了多个视图,它们都有自包含特性。对于iPhone应用,每个视图层次通常都填满了整个屏幕,尽管对于iPad应用来说不是。
当你设计用户界面的时候,考虑到视图控制器的所扮演的角色是很重要的。视图控制器提供了很多重要的行为,像协调视图的展示,协调视图的剔除,释放内存以响应低内存警告,还有翻转视图以响应界面的方向变更。逃避这些行为会导致应用发生错误。
获取更多关于视图控制器的信息,查看 View Controller Programming Guide for iOS
最小化定制的绘画
尽管定制的绘画有时是需要的,但是你也应该尽量避免它。真正需要定制绘画的时候是已有的视图类无法提供足够的表现和能力时。任何时候你的内容都应该可以被组装到其他视图,最好结果时组合那些视图对象到定制的视图层次
利用内容模式
内容模式可以最小化重画视图要花费的时间。默认的,视图使用UIViewContentModeScaleToFill 内容模式,这个模式会放缩视图的已有内容来填充视图的frame矩形。需要时你可以改变这个模式来调整你的内容,但是应该避免使用UIViewContentModeRedraw内容模式。不管哪个内容模式发生作用,你都可以调用setNeedsDisplay或者setNeedsDisplayInRect:方法来强制视图重画它的内容。
可能的话将视图声明为不透明
UIKit使用opaque属性来决定它是否可以优化组合操作。将一个定制视图的这个属性设置为YES会告诉UIKit不需要解释任何在该视图后的内容。这样可以为你的绘制代码提高性能并且是推荐的。当然,如果你将这个属性设置为YES,你的视图一定要用不透明的内容完全填充它的bounds矩形。
滚动时调整视图的绘制行为
滚动会导致数个视图在短时间内更新。如果视图的绘制代码没有被适当的调整,滚动的性能会非常的缓慢。相对于总是保证视图内容的平庸,我们更倾向于考虑滚动操作开始时改变视图行为。例如,你可以暂时减少已解释的内容,或者在滚动的时候改变内容模式。当滚动停止时,你可以将视图返回到前一状态,同时需要时更新内容。
不要嵌入子视图来定制控制
尽管在技术上增加子视图到标准系统控制对象-继承自UIControl的类-是可行的,你还是永远不应该用这种方法来定制它们。控制对象支持定制,它们有显式并且良好归档的接口。例如,UIButton类包含了设置标题和背景图片的方法。使用已定义好的定制点意味着你的代码总是会正确的工作。不用这些方法,而嵌入一个定制的图像视图或者标签到按钮中去会导致应用出现未预期的结果。 收起阅读 »
从showalert方法中看iOS中的MVC
我们知道MVC是个模式,一个开发模式,一个构架模式,一个放之四海而皆为准的模式。
那什么是MVC?
MVC模式(Model-View-Controller)是软件工程中的一种软件架构模式,把软件系统分为三个基本部分:
模型(Model)、视图(View)和控制器(Controller)。
那么iOS中的showAlert函数又是什么样子的呢?
我们来看一个最简单的例子:
@IBAction func showAlert(){
let alert = UIAlertController(title: "Hello,World", message: "This is my first app!", preferredStyle: .Alert)
let action = UIAlertAction(title: "Awesome", style: .Default, handler: nil)
alert.addAction(action)
presentViewController(alert, animated: true, completion: nil)
}
那我们根据MVC来一一确认这些都代表什么意思?
let alert 是一个UIAlertController,很明显,首先它是一个Controller,但是我更感觉它像MV,
如 title,message很明显是View的内容,preferredStyle 表示是一个什么方式,应该是属于Model,功能的设计。
let Action 是一个 UIAlertAction,其中包含了 View 和 Control 的内容。title,style应属于
View 内容。handler是属于Controller。
alert.addAction(action) 将Action添加到Controller中,这是一个混合了View和Model和Controll
的组合。
presentViewController表示在当前视图中将对应的controller表示出来,这不折不扣的就是controller
里面的参数就是前面对应已经添加了action的alert。 收起阅读 »
那什么是MVC?
MVC模式(Model-View-Controller)是软件工程中的一种软件架构模式,把软件系统分为三个基本部分:
模型(Model)、视图(View)和控制器(Controller)。
- (控制器 Controller)- 负责转发请求,对请求进行处理。
- (视图 View) - 界面设计人员进行图形界面设计。
- (模型 Model) -程序员编写程序应有的功能(实现算法等等)、数据库专家进行数据管理和数据库设计(可以实现具体的功能)。
那么iOS中的showAlert函数又是什么样子的呢?
我们来看一个最简单的例子:
@IBAction func showAlert(){
let alert = UIAlertController(title: "Hello,World", message: "This is my first app!", preferredStyle: .Alert)
let action = UIAlertAction(title: "Awesome", style: .Default, handler: nil)
alert.addAction(action)
presentViewController(alert, animated: true, completion: nil)
}
那我们根据MVC来一一确认这些都代表什么意思?
let alert 是一个UIAlertController,很明显,首先它是一个Controller,但是我更感觉它像MV,
如 title,message很明显是View的内容,preferredStyle 表示是一个什么方式,应该是属于Model,功能的设计。
let Action 是一个 UIAlertAction,其中包含了 View 和 Control 的内容。title,style应属于
View 内容。handler是属于Controller。
alert.addAction(action) 将Action添加到Controller中,这是一个混合了View和Model和Controll
的组合。
presentViewController表示在当前视图中将对应的controller表示出来,这不折不扣的就是controller
里面的参数就是前面对应已经添加了action的alert。 收起阅读 »
小窍门:关于环信的问题,如果想收到更及时的回复......
小窍门:关于环信的问题,提交问题后,
相关人员将会收到邮件提醒,以便及时回复
- 如果是关于android的问题,邀请se_android
- 如果是关于ios的问题,邀请se_ios
- 如果是其它问题,记得邀请SE
相关人员将会收到邮件提醒,以便及时回复
用swift集成iOS聊天页面
今天有人问swift集成问题,临时写了一个。基于ios sdk2.1.8,没有集成实时语音电话页面,但是应该也能用,抛砖引玉。
http://pan.baidu.com/s/1dDlOYzv
http://pan.baidu.com/s/1dDlOYzv
开发技巧:iOS 9适配系列教程--后台定位
Demo:GitHub地址:https://github.com/ChenYilong/iOS9AdaptationTips
【iOS9在定位的问题上,有一个坏消息一个好消息】坏消息:如果不适配iOS9,就不能偷偷在后台定位(不带蓝条,见图)!好消息:将允许出现这种场景:同一App中的多个location manager:一些只能在前台定位,另一些可在后台定位,并可随时开启或者关闭特定location manager的后台定位。
如果没有请求后台定位的权限,也是可以在后台定位的,不过会带蓝条:
【iOS9在定位的问题上,有一个坏消息一个好消息】坏消息:如果不适配iOS9,就不能偷偷在后台定位(不带蓝条,见图)!好消息:将允许出现这种场景:同一App中的多个location manager:一些只能在前台定位,另一些可在后台定位,并可随时开启或者关闭特定location manager的后台定位。
如果没有请求后台定位的权限,也是可以在后台定位的,不过会带蓝条:
// 1. 实例化定位管理器
_locationManager = [[CLLocationManager alloc] init];
// 2. 设置代理
_locationManager.delegate = self;
// 3. 定位精度
[_locationManager setDesiredAccuracy:kCLLocationAccuracyBest];
// 4.请求用户权限:分为:?只在前台开启定位?在后台也可定位,
//注意:建议只请求?和?中的一个,如果两个权限都需要,只请求?即可,
//??这样的顺序,将导致bug:第一次启动程序后,系统将只请求?的权限,?的权限系统不会请求,只会在下一次启动应用时请求?
if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 8) {
//[_locationManager requestWhenInUseAuthorization];//?只在前台开启定位
[_locationManager requestAlwaysAuthorization];//?在后台也可定位
}
// 5.iOS9新特性:将允许出现这种场景:同一app中多个location manager:一些只能在前台定位,另一些可在后台定位(并可随时禁止其后台定位)。
if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 9) {
_locationManager.allowsBackgroundLocationUpdates = YES;
}
// 6. 更新用户位置
[_locationManager startUpdatingLocation];
但是如果照着这种方式尝试,而没有配置Info.plist,100%你的程序会崩溃掉,并报错:
*** Assertion failure in -[CLLocationManager setAllowsBackgroundLocationUpdates:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/CoreLocationFramework_Sim/CoreLocation-1808.1.5/Framework/CoreLocation/CLLocationManager.m:593
要将 Info.plist 配置如下:
对应的 Info.plist 的XML源码是:
内容来源:ChenYilong 收起阅读 »
开发技巧:iOS界面布局之一——使用autoresizing进行动态布局
autoresizing是iOS开发中传统的布局模式。通过它可以设计控件与其父视图的变换关系。autoresizing是iOS中传统的界面自动布局方式,通过它,当父视图frame变换时,子视图会自动的做出相应的调整。
一、通过代码进行布局
任何一个view都有autoresizingMask这个属性,通过这个属性可以设置当前view与其父视图的相对关系。我们先来看UIViewAutoresizing这个枚举:
typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
UIViewAutoresizingNone = 0,//默认
UIViewAutoresizingFlexibleLeftMargin = 1 << 0,//与父视图右边间距固定,左边可变
UIViewAutoresizingFlexibleWidth = 1 << 1,//视图宽度可变
UIViewAutoresizingFlexibleRightMargin = 1 << 2,//与父视图左边间距固定,右边可变
UIViewAutoresizingFlexibleTopMargin = 1 << 3,//与父视图下边间距固定,上边可变
UIViewAutoresizingFlexibleHeight = 1 << 4,//视图高度可变
UIViewAutoresizingFlexibleBottomMargin = 1 << 5//与父视图上边间距固定,下边可变
};
、
下面我们通过效果来看这些属性的作用:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
UIView * view1 = [[UIView alloc]initWithFrame:CGRectMake(20, 40, 200, 200)];
view1.backgroundColor=[UIColor redColor];
UIView * view2 = [[UIView alloc]initWithFrame:CGRectMake(10, 10, 100, 100)];
view2.backgroundColor=[UIColor greenColor];
[view1 addSubview:view2];
[self.view addSubview:view1];
}
这时的效果如下:
改变view1的frame如下:
UIView * view1 = [[UIView alloc]initWithFrame:CGRectMake(20, 40, 300, 300)];
效果如下:
这时view2的下边距离相对父视图是可变的。
设置如下:
view2.autoresizingMask=UIViewAutoresizingFlexibleHeight;
效果如下:
可以看出,这时子视图的高度是随父视图变化而自动改变的。
如下设置:
view2.autoresizingMask=UIViewAutoresizingFlexibleLeftMargin;
效果如下:
这时子视图的左边是随父视图变化而可变的。
同理,UIViewAutoresizingFlexibleRightMargin将使子视图右边与父视图的距离可变。
UIViewAutoresizingFlexibleTopMargin将使子视图上边与父视图距离可变。UIViewAutoresizingFlexibleWidth将使子视图的宽度可变。
注意:这些自动布局的属性是可以叠加的,比如保持视图与父视图边距不变,如下设置:
view2.autoresizingMask=UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;
效果如下:
二、nib文件中可视化设置自动布局
在storyboard中我们可以更加轻松的进行autoresizing自动布局。在view设置栏中有autoresizing这个设置,点中相应的箭头,就是刚才我们探讨的设置选项。并且我们把鼠标放在这个上面的时候,右侧会自动为我们预览效果。
如果你觉得autoresizing很强大,那么你就太容易满足了,autoresizing可以满足大部分简单的自动布局需求,可是它有一个致命的缺陷,它只能设置子视图相对于父视图的变化,却不能精确这个变化的度是多少,因此对于复杂的精准的布局需求,它就力不从心了。但是有一个好消息告诉你,iOS6之后的autolayout自动布局方案,正是解决复杂布局的好帮手。
收起阅读 »
一、通过代码进行布局
任何一个view都有autoresizingMask这个属性,通过这个属性可以设置当前view与其父视图的相对关系。我们先来看UIViewAutoresizing这个枚举:
typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
UIViewAutoresizingNone = 0,//默认
UIViewAutoresizingFlexibleLeftMargin = 1 << 0,//与父视图右边间距固定,左边可变
UIViewAutoresizingFlexibleWidth = 1 << 1,//视图宽度可变
UIViewAutoresizingFlexibleRightMargin = 1 << 2,//与父视图左边间距固定,右边可变
UIViewAutoresizingFlexibleTopMargin = 1 << 3,//与父视图下边间距固定,上边可变
UIViewAutoresizingFlexibleHeight = 1 << 4,//视图高度可变
UIViewAutoresizingFlexibleBottomMargin = 1 << 5//与父视图上边间距固定,下边可变
};
、
下面我们通过效果来看这些属性的作用:
- 先创建两个view,为了区分,设置不同的背景色:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
UIView * view1 = [[UIView alloc]initWithFrame:CGRectMake(20, 40, 200, 200)];
view1.backgroundColor=[UIColor redColor];
UIView * view2 = [[UIView alloc]initWithFrame:CGRectMake(10, 10, 100, 100)];
view2.backgroundColor=[UIColor greenColor];
[view1 addSubview:view2];
[self.view addSubview:view1];
}
- 设置view2的自动布局属性如下:
这时的效果如下:
改变view1的frame如下:
UIView * view1 = [[UIView alloc]initWithFrame:CGRectMake(20, 40, 300, 300)];
效果如下:
这时view2的下边距离相对父视图是可变的。
设置如下:
view2.autoresizingMask=UIViewAutoresizingFlexibleHeight;
效果如下:
可以看出,这时子视图的高度是随父视图变化而自动改变的。
如下设置:
view2.autoresizingMask=UIViewAutoresizingFlexibleLeftMargin;
效果如下:
这时子视图的左边是随父视图变化而可变的。
同理,UIViewAutoresizingFlexibleRightMargin将使子视图右边与父视图的距离可变。
UIViewAutoresizingFlexibleTopMargin将使子视图上边与父视图距离可变。UIViewAutoresizingFlexibleWidth将使子视图的宽度可变。
注意:这些自动布局的属性是可以叠加的,比如保持视图与父视图边距不变,如下设置:
view2.autoresizingMask=UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;
效果如下:
二、nib文件中可视化设置自动布局
在storyboard中我们可以更加轻松的进行autoresizing自动布局。在view设置栏中有autoresizing这个设置,点中相应的箭头,就是刚才我们探讨的设置选项。并且我们把鼠标放在这个上面的时候,右侧会自动为我们预览效果。
如果你觉得autoresizing很强大,那么你就太容易满足了,autoresizing可以满足大部分简单的自动布局需求,可是它有一个致命的缺陷,它只能设置子视图相对于父视图的变化,却不能精确这个变化的度是多少,因此对于复杂的精准的布局需求,它就力不从心了。但是有一个好消息告诉你,iOS6之后的autolayout自动布局方案,正是解决复杂布局的好帮手。
收起阅读 »
移动互联网实时视频通讯之视频采集
一 、前言
一套完整的实时网络视频通讯系统包括视频采集、视频编码、视频传输、视频解码和播放。对于视频采集,大多数视频编码器对输入原始视频的格式要求是YUV420。YUV420格式是YUV格式的一种,YUV分为三个分量,Y代表亮度,也就是灰度值,U和V表示的是色度,用于描述图像的色彩和饱和度。YUV420格式的数据的各分量的布局是YYYYYYYYUUVV,视频采集时,如果输入的YUV数据各分量不是这种布局,需要先转化为这种布局,然后再送到编码器进行编码。对于android手机,从摄像头捕获的YUV数据格式是NV21,其YUV布局是YYYYYYYYVUVU;对于iphone手机,摄像头采集的YUV数据格式是NV12,其YUV布局是YYYYYYYYUVUV。因此对于android手机和iphone手机,视频采集获得的数据都需 要进行转换为YUV420的数据布局才能进一步进行编码和传输等工作。
二、android视频采集
对于android系统,通过Camera.PreviewCallback的onPreviewFrame回调函数,实时截取每一帧视频流数据。在Camera对象上,有3种不同的方式使用这个回调:
示例代码
public class VideoActivity extends Activity implements SurfaceHolder.Callback,Camera.PreviewCallback {
static int screenWidth = 0;
static int screenHeight = 0;
private SurfaceView mSurfaceview = null;
private SurfaceHolder mSurfaceHolder = null;
private Camera mCamera = null;
private byte yuv_frame[];
private Parameters mParameters;
static int mwidth = 320;
static int mheight = 240;
// Setup
protected void onCreate(Bundle savedInstanceState) {
requestWindowFeature(Window.FEATURE_NO_TITLE);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_video_capture);
mSurfaceview = (SurfaceView) findViewById(R.id.surfaceview);
mSurfaceHolder = mSurfaceview.getHolder();
mSurfaceHolder.addCallback(this);
screenWidth = getWindowManager().getDefaultDisplay().getWidth();
screenHeight = getWindowManager().getDefaultDisplay().getHeight();
LayoutParams layoutparam = new LayoutParams(screenWidth, screenHeight);
SurfaceHolder holder = mSurface.getHolder();
holder.setFixedSize(screenWidth,screenHeight);
}
void startcapture()
{
try {
if (mCamera == null) {
mCamera = Camera.open();
}
mCamera.stopPreview();
mParameters = mCamera.getParameters();
mParameters.setPreviewSize(mwidth, mheight);//set video resolution ratio
mParameters.setPreviewFrameRate(15); //set frame rate
mCamera.setParameters(mParameters);
int mformat = mParameters.getPreviewFormat();
int bitsperpixel = ImageFormat.getBitsPerPixel(mformat);
yuv_frame = new byte[mwidth * mheight * bitsperpixel / 8];//buffer to store NV21preview data
mCamera.addCallbackBuffer(yuv_frame);
mCamera.setPreviewDisplay(mSurfaceHolder);
mCamera.setPreviewCallbackWithBuffer(this);//set callback for camera
mCamera.startPreview();//trigger callback onPreviewFrame
}catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
//add code to process the data captured,data layout is YYYYYYYYVUVU,should be converted to // YYYYYYYYUUVV before encoded
camera.addCallbackBuffer(yuv_frame);
}
protected void onPause() {
super.onPause();
}
public void onResume() {
super.onResume();
}
protected void onDestroy() {
if (mCamera != null) {
mCamera.setPreviewCallback(null);
mCamera.stopPreview(); //stop capture video data
mCamera.release();
mCamera = null;
}
super.onDestroy();
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width_size,int height_size) {
startcapture();//start capture NV21 video data
}
}
二、IOS视频采集
对于IOS系统,为了完成实时视频采集,首先初始化一个AVCaputureSession对象,AVCaptureSession对象用于将AV输入设备的数据流转换到输出。然后,初始化一个AVCaptureDeviceInput对象,调用addInput方法将AVCaptureDeviceInput对象添加到AVCaptureSession对象。接着初始化一个AVCaptureVideoDataOuput对象,调用addOutput方法将该对象添加到AVCaputureSession对象。AVCaptureVideoDataOutput初始化后可以通过captureOutput:didOutputSampleBuffer:fromConnection:这个委托方法获取视频帧,这个委托方法必须采用AVCaptureVideoDataOutputSampleBufferDelegate协议。
示例代码
int frame_rate = 15;
int mWidth = 640;
int mHeight 480;
AVCaptureDeviceInput *videoInput = nil;
AVCaptureVideoDataOutput *avCaptureVideoDataOutput =nil;
AVCaptureSession* mCaptureSession = nil;
AVCaptureDevice *mCaptureDevice = nil;
- (void)startCapture
{
if(mCaptureDevice || mCaptureSession)
{
NSLog(@"Already capturing");
return;
}
NSArray *cameras = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
for (AVCaptureDevice *device in cameras){
if (device.position == AVCaptureDevicePositionFront){
AVCaptureDevice = device;
}
}
if(AVCaptureDevice == nil)
{
AVCaptureDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
}
if(mCaptureDevice == nil)
{
NSLog(@"Failed to get valide capture device");
return;
}
NSError *error = nil;
videoInput = [AVCaptureDeviceInput deviceInputWithDevice:mCaptureDevice error:&error];
if (!videoInput)
{
NSLog(@"Failed to get video input");
mCaptureDevice = nil;
return;
}
mCaptureSession = [[AVCaptureSession alloc] init];
mCaptureSession.sessionPreset = AVCaptureSessionPreset640x480;//set video resolution ratio
[mCaptureSession addInput:videoInput];
avCaptureVideoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
NSDictionary *settings = [[NSDictionary alloc] initWithObjectsAndKeys:
[NSNumber numberWithUnsignedInt:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange], kCVPixelBufferPixelFormatTypeKey,
[NSNumber numberWithInt: mWidth], (id)kCVPixelBufferWidthKey,
[NSNumber numberWithInt: mHeight], (id)kCVPixelBufferHeightKey,
nil];
avCaptureVideoDataOutput.videoSettings = settings;
[settings release];
avCaptureVideoDataOutput.minFrameDuration = CMTimeMake(1, frame_rate);//set video frame rate
avCaptureVideoDataOutput.alwaysDiscardsLateVideoFrames = YES;
dispatch_queue_t queue_ = dispatch_queue_create("www.easemob.com", NULL);
[avCaptureVideoDataOutput setSampleBufferDelegate:self queue:queue_];
[mCaptureSession addOutput:avCaptureVideoDataOutput];
[avCaptureVideoDataOutput release];
dispatch_release(queue_);
}
- (void)stopCapture
{
if(mCaptureSession){
[mCaptureSession stopRunning];
[mCaptureSession removeInput:videoInput];
[mCaptureSession removeOutput:avCaptureVideoDataOutput];
[avCaptureVideoDataOutput release];
[videoInput release];
[mCaptureSession release], mCaptureSession = nil;
[mCaptureDevice release], mCaptureDevice = nil;
}
}
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
{
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
/* unlock the buffer*/
if(CVPixelBufferLockBaseAddress(imageBuffer, 0) == kCVReturnSuccess)
{
UInt8 *bufferbasePtr = (UInt8 *)CVPixelBufferGetBaseAddress(imageBuffer);
UInt8 *bufferPtr = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,0);
UInt8 *bufferPtr1 = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,1);
size_t buffeSize = CVPixelBufferGetDataSize(imageBuffer);
size_t width = CVPixelBufferGetWidth(imageBuffer);
size_t height = CVPixelBufferGetHeight(imageBuffer);
size_t bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer);
size_t bytesrow0 = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer,0);
size_t bytesrow1 = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer,1);
size_t bytesrow2 = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer,2);
UInt8 *yuv420_data = (UInt8 *)malloc(width * height *3/ 2);//buffer to store YUV with layout YYYYYYYYUUVV
/* convert NV21 data to YUV420*/
UInt8 *pY = bufferPtr ;
UInt8 *pUV = bufferPtr1;
UInt8 *pU = yuv420_data + width*height;
UInt8 *pV = pU + width*height/4;
for(int i =0;i
{
memcpy(yuv420_data+i*width,pY+i*bytesrow0,width);
}
for(int j = 0;j
{
for(int i =0;i
{
*(pU++) = pUV[i<<1];
*(pV++) = pUV[(i<<1) + 1];
}
pUV+=bytesrow1;
}
//add code to push yuv420_data to video encoder here
free(yuv420_data);
/* unlock the buffer*/
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
}
作者: 彭祖元 收起阅读 »
一套完整的实时网络视频通讯系统包括视频采集、视频编码、视频传输、视频解码和播放。对于视频采集,大多数视频编码器对输入原始视频的格式要求是YUV420。YUV420格式是YUV格式的一种,YUV分为三个分量,Y代表亮度,也就是灰度值,U和V表示的是色度,用于描述图像的色彩和饱和度。YUV420格式的数据的各分量的布局是YYYYYYYYUUVV,视频采集时,如果输入的YUV数据各分量不是这种布局,需要先转化为这种布局,然后再送到编码器进行编码。对于android手机,从摄像头捕获的YUV数据格式是NV21,其YUV布局是YYYYYYYYVUVU;对于iphone手机,摄像头采集的YUV数据格式是NV12,其YUV布局是YYYYYYYYUVUV。因此对于android手机和iphone手机,视频采集获得的数据都需 要进行转换为YUV420的数据布局才能进一步进行编码和传输等工作。
二、android视频采集
对于android系统,通过Camera.PreviewCallback的onPreviewFrame回调函数,实时截取每一帧视频流数据。在Camera对象上,有3种不同的方式使用这个回调:
- setPreviewCallback(Camera.PreviewCallback):在屏幕上显示一个新的预览帧时调用onPreviewFrame方法。
- setOneShotPreviewCallback(Camera.PreviewCallback):当下一幅预览图像可用时调用onPreviewFrame。
- setPreviewCallbackWithBuffer(Camera.PreviewCallback):在Android 2.2中引入了该方法,其与setPreviewCallback的工作方式相同,但需要提供一个字节数组作为缓冲区,用于保存预览图像数据。这是为了能够更好地管理处理预览图像时使用的内存,避免内存的频繁分配和销毁。
示例代码
public class VideoActivity extends Activity implements SurfaceHolder.Callback,Camera.PreviewCallback {
static int screenWidth = 0;
static int screenHeight = 0;
private SurfaceView mSurfaceview = null;
private SurfaceHolder mSurfaceHolder = null;
private Camera mCamera = null;
private byte yuv_frame[];
private Parameters mParameters;
static int mwidth = 320;
static int mheight = 240;
// Setup
protected void onCreate(Bundle savedInstanceState) {
requestWindowFeature(Window.FEATURE_NO_TITLE);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_video_capture);
mSurfaceview = (SurfaceView) findViewById(R.id.surfaceview);
mSurfaceHolder = mSurfaceview.getHolder();
mSurfaceHolder.addCallback(this);
screenWidth = getWindowManager().getDefaultDisplay().getWidth();
screenHeight = getWindowManager().getDefaultDisplay().getHeight();
LayoutParams layoutparam = new LayoutParams(screenWidth, screenHeight);
SurfaceHolder holder = mSurface.getHolder();
holder.setFixedSize(screenWidth,screenHeight);
}
void startcapture()
{
try {
if (mCamera == null) {
mCamera = Camera.open();
}
mCamera.stopPreview();
mParameters = mCamera.getParameters();
mParameters.setPreviewSize(mwidth, mheight);//set video resolution ratio
mParameters.setPreviewFrameRate(15); //set frame rate
mCamera.setParameters(mParameters);
int mformat = mParameters.getPreviewFormat();
int bitsperpixel = ImageFormat.getBitsPerPixel(mformat);
yuv_frame = new byte[mwidth * mheight * bitsperpixel / 8];//buffer to store NV21preview data
mCamera.addCallbackBuffer(yuv_frame);
mCamera.setPreviewDisplay(mSurfaceHolder);
mCamera.setPreviewCallbackWithBuffer(this);//set callback for camera
mCamera.startPreview();//trigger callback onPreviewFrame
}catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
//add code to process the data captured,data layout is YYYYYYYYVUVU,should be converted to // YYYYYYYYUUVV before encoded
camera.addCallbackBuffer(yuv_frame);
}
protected void onPause() {
super.onPause();
}
public void onResume() {
super.onResume();
}
protected void onDestroy() {
if (mCamera != null) {
mCamera.setPreviewCallback(null);
mCamera.stopPreview(); //stop capture video data
mCamera.release();
mCamera = null;
}
super.onDestroy();
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width_size,int height_size) {
startcapture();//start capture NV21 video data
}
}
二、IOS视频采集
对于IOS系统,为了完成实时视频采集,首先初始化一个AVCaputureSession对象,AVCaptureSession对象用于将AV输入设备的数据流转换到输出。然后,初始化一个AVCaptureDeviceInput对象,调用addInput方法将AVCaptureDeviceInput对象添加到AVCaptureSession对象。接着初始化一个AVCaptureVideoDataOuput对象,调用addOutput方法将该对象添加到AVCaputureSession对象。AVCaptureVideoDataOutput初始化后可以通过captureOutput:didOutputSampleBuffer:fromConnection:这个委托方法获取视频帧,这个委托方法必须采用AVCaptureVideoDataOutputSampleBufferDelegate协议。
示例代码
int frame_rate = 15;
int mWidth = 640;
int mHeight 480;
AVCaptureDeviceInput *videoInput = nil;
AVCaptureVideoDataOutput *avCaptureVideoDataOutput =nil;
AVCaptureSession* mCaptureSession = nil;
AVCaptureDevice *mCaptureDevice = nil;
- (void)startCapture
{
if(mCaptureDevice || mCaptureSession)
{
NSLog(@"Already capturing");
return;
}
NSArray *cameras = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
for (AVCaptureDevice *device in cameras){
if (device.position == AVCaptureDevicePositionFront){
AVCaptureDevice = device;
}
}
if(AVCaptureDevice == nil)
{
AVCaptureDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
}
if(mCaptureDevice == nil)
{
NSLog(@"Failed to get valide capture device");
return;
}
NSError *error = nil;
videoInput = [AVCaptureDeviceInput deviceInputWithDevice:mCaptureDevice error:&error];
if (!videoInput)
{
NSLog(@"Failed to get video input");
mCaptureDevice = nil;
return;
}
mCaptureSession = [[AVCaptureSession alloc] init];
mCaptureSession.sessionPreset = AVCaptureSessionPreset640x480;//set video resolution ratio
[mCaptureSession addInput:videoInput];
avCaptureVideoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
NSDictionary *settings = [[NSDictionary alloc] initWithObjectsAndKeys:
[NSNumber numberWithUnsignedInt:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange], kCVPixelBufferPixelFormatTypeKey,
[NSNumber numberWithInt: mWidth], (id)kCVPixelBufferWidthKey,
[NSNumber numberWithInt: mHeight], (id)kCVPixelBufferHeightKey,
nil];
avCaptureVideoDataOutput.videoSettings = settings;
[settings release];
avCaptureVideoDataOutput.minFrameDuration = CMTimeMake(1, frame_rate);//set video frame rate
avCaptureVideoDataOutput.alwaysDiscardsLateVideoFrames = YES;
dispatch_queue_t queue_ = dispatch_queue_create("www.easemob.com", NULL);
[avCaptureVideoDataOutput setSampleBufferDelegate:self queue:queue_];
[mCaptureSession addOutput:avCaptureVideoDataOutput];
[avCaptureVideoDataOutput release];
dispatch_release(queue_);
}
- (void)stopCapture
{
if(mCaptureSession){
[mCaptureSession stopRunning];
[mCaptureSession removeInput:videoInput];
[mCaptureSession removeOutput:avCaptureVideoDataOutput];
[avCaptureVideoDataOutput release];
[videoInput release];
[mCaptureSession release], mCaptureSession = nil;
[mCaptureDevice release], mCaptureDevice = nil;
}
}
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
{
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
/* unlock the buffer*/
if(CVPixelBufferLockBaseAddress(imageBuffer, 0) == kCVReturnSuccess)
{
UInt8 *bufferbasePtr = (UInt8 *)CVPixelBufferGetBaseAddress(imageBuffer);
UInt8 *bufferPtr = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,0);
UInt8 *bufferPtr1 = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,1);
size_t buffeSize = CVPixelBufferGetDataSize(imageBuffer);
size_t width = CVPixelBufferGetWidth(imageBuffer);
size_t height = CVPixelBufferGetHeight(imageBuffer);
size_t bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer);
size_t bytesrow0 = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer,0);
size_t bytesrow1 = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer,1);
size_t bytesrow2 = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer,2);
UInt8 *yuv420_data = (UInt8 *)malloc(width * height *3/ 2);//buffer to store YUV with layout YYYYYYYYUUVV
/* convert NV21 data to YUV420*/
UInt8 *pY = bufferPtr ;
UInt8 *pUV = bufferPtr1;
UInt8 *pU = yuv420_data + width*height;
UInt8 *pV = pU + width*height/4;
for(int i =0;i
memcpy(yuv420_data+i*width,pY+i*bytesrow0,width);
}
for(int j = 0;j
for(int i =0;i
*(pU++) = pUV[i<<1];
*(pV++) = pUV[(i<<1) + 1];
}
pUV+=bytesrow1;
}
//add code to push yuv420_data to video encoder here
free(yuv420_data);
/* unlock the buffer*/
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
}
作者: 彭祖元 收起阅读 »
环信RTBDA系统架构
RTBDA(Real Time Big Data Analysis)架构是目前大数据分析领域的流行架构理念,最早是由David Smith在他非常流行的博客([Reference 2])里头提出来的。他的提议里包括了四层架构 - 数据,分析,集成,决策。尽管他当初的提议主要是为了进行预测性分析计算,但后来这个架构逐渐演变为大数据分析领域的主流架构。如下图:
图1 :David Smith 的 RTBDA架构图
今天我们以这个主流架构模式来看看和理解环信分析系统的体系架构。
环信大数据分析系统架构由底向上基本上也可以分四层 :数据,分析,集成,决策。
环信大数据系统的最底层使用的是cassandra数据库,所有需要分析的数据存储在这个数据库里。环信也使用mysql数据库保存与分析系统管理相关的数据。这两个数据库里的所有这些个数据是整个分析系统的基础。数据层之上的第三层是分析层。环信大数据系统分析层使用spark做分析平台。各种社交指标分析算法的实现采用spark环境支持的scala,python 或者java 语言编写。Spark本身包含一个机器学习的库。环信的预测建模以这个库为基础。SPARK平台构成环信指标分析模型计算的基础。
图2 :环信系统RTBDA架构图
分析层之上是集成层,集成层就像一层粘糊剂,把系统的各个组成部分有机的粘接起来。在这个层次上,环信主要需要开发两个子系统 – 社交建模规则执行引擎和和任务调度引擎。社交建模规则执行引擎根据建模规则调用分析层的分析计算进程,计算各类社交指标。任务调度引擎按照执行逻辑的优先次序和依赖关系调用规则执行逻辑完成计算任务,计算结果通过web服务反馈到web前端,所以集成层以分析层为基础,把分析计算和决策层无缝连接起来,形成一个大数据分析计算的处理系统。
环信大数据系统分析层之上的决策层包括有手机和web前端,主要提供各个社交指标的计算结果及其所产生各类分析图表。基本的图形组件有饼图,线图,柱状图等等,全部采用javascripts开发。环信大数据系统决策层直接面对用户及其相关的决策领导,从环信的角度出发,这一层是吸引用户购买和使用环信社交大数据分析系统工具的重要一环。
环信有2万多家app客户,为了适应这么多不同客户的需求, 环信构造的这四层分析系统基础架构尽量采取灵活的可配置功能。每层都有其相关的配置对应,使用起来非常灵活,应当能够高效率的充分满足不同领域的客户需求。
[Reference]
1. 环信 Social Activity Indicator Analysis Engine - TD.docx
2. http://blog.revolutionanalytics.com/
3. http://www.oreilly.com/data/free/big-data-analytics-emerging-architecture.csp
作者:黄智,负责环信大数据部门运作和大数据分析系统的搭建 收起阅读 »
图1 :David Smith 的 RTBDA架构图
今天我们以这个主流架构模式来看看和理解环信分析系统的体系架构。
环信大数据分析系统架构由底向上基本上也可以分四层 :数据,分析,集成,决策。
环信大数据系统的最底层使用的是cassandra数据库,所有需要分析的数据存储在这个数据库里。环信也使用mysql数据库保存与分析系统管理相关的数据。这两个数据库里的所有这些个数据是整个分析系统的基础。数据层之上的第三层是分析层。环信大数据系统分析层使用spark做分析平台。各种社交指标分析算法的实现采用spark环境支持的scala,python 或者java 语言编写。Spark本身包含一个机器学习的库。环信的预测建模以这个库为基础。SPARK平台构成环信指标分析模型计算的基础。
图2 :环信系统RTBDA架构图
分析层之上是集成层,集成层就像一层粘糊剂,把系统的各个组成部分有机的粘接起来。在这个层次上,环信主要需要开发两个子系统 – 社交建模规则执行引擎和和任务调度引擎。社交建模规则执行引擎根据建模规则调用分析层的分析计算进程,计算各类社交指标。任务调度引擎按照执行逻辑的优先次序和依赖关系调用规则执行逻辑完成计算任务,计算结果通过web服务反馈到web前端,所以集成层以分析层为基础,把分析计算和决策层无缝连接起来,形成一个大数据分析计算的处理系统。
环信大数据系统分析层之上的决策层包括有手机和web前端,主要提供各个社交指标的计算结果及其所产生各类分析图表。基本的图形组件有饼图,线图,柱状图等等,全部采用javascripts开发。环信大数据系统决策层直接面对用户及其相关的决策领导,从环信的角度出发,这一层是吸引用户购买和使用环信社交大数据分析系统工具的重要一环。
环信有2万多家app客户,为了适应这么多不同客户的需求, 环信构造的这四层分析系统基础架构尽量采取灵活的可配置功能。每层都有其相关的配置对应,使用起来非常灵活,应当能够高效率的充分满足不同领域的客户需求。
[Reference]
1. 环信 Social Activity Indicator Analysis Engine - TD.docx
2. http://blog.revolutionanalytics.com/
3. http://www.oreilly.com/data/free/big-data-analytics-emerging-architecture.csp
作者:黄智,负责环信大数据部门运作和大数据分析系统的搭建 收起阅读 »
imgeek更新
2015.6.23
1.将”活动“菜单置于顶层
2.修改”活动“报名页
3.”发现“右下页加上赞助商LOGO栏
1.将”活动“菜单置于顶层
2.修改”活动“报名页
3.”发现“右下页加上赞助商LOGO栏
Docker五大优势:持续集成、版本控制、可移植性、隔离性和安全性
对于Docker,应该不需要进行详细的介绍了。它是最火热的开源项目之一,通过在容器中增加一个抽象层(a layer of abstraction),就可以将应用程序部署到容器中。在看似稳定而成熟的场景下,使用Docker的好处越来越多。在这篇文章中,我不谈论Docker是什么或者Docker是怎么工作的,取而代之,我会提出使用这个不断成长的平台的五大好处。
持续部署与测试
Docker在开发与运维的世界中具有极大的吸引力,因为它能保持跨环境的一致性。在开发与发布的生命周期中,不同的环境具有细微的不同,这些差异可能是由于不同安装包的版本和依赖关系引起的。然而,Docker可以通过确保从开发到产品发布整个过程环境的一致性来解决这个问题*Docker容器通过相关配置,保持容器内部所有的配置和依赖关系始终不变。最终,你可以在开发到产品发布的整个过程中使用相同的容器来确保没有任何差异或者人工干预。
使用Docker,你还可以确保开发者不需要配置完全相同的产品环境,他们可以在他们自己的系统上通过VirtualBox建立虚拟机来运行Docker容器。Docker的魅力在于它同样可以让你在亚马逊EC2实例上运行相同的容器。如果你需要在一个产品发布周期中完成一次升级,你可以很容易地将需要变更的东西放到Docker容器中,测试它们,并且使你已经存在的容器执行相同的变更。这种灵活性就是使用Docker的一个主要好处。和标准部署与集成过程一样,Docker可以让你构建、测试和发布镜像,这个镜像可以跨多个服务器进行部署。哪怕安装一个新的安全补丁,整个过程也是一样的。你可以安装补丁,然后测试它,并且将这个补丁发布到产品中。
多云平台
Docker最大的好处之一就是可移植性。在过去的几年里,所有主流的云计算提供商,包括亚马逊AWS和谷歌的GCP,都将Docker融入到他们的平台并增加了各自的支持。Docker容器能运行在亚马逊的EC2实例、谷歌的GCP实例、Rackspace服务器或者VirtualBox这些提供主机操作系统的平台上。举例来说,如果运行在亚马逊EC2实例上的Docker容器能够很容易地移植到其他几个平台上,比如说VirtualBox,并且达到类似的一致性和功能性,那这将允许你从基础设施层中抽象出来。除了AWS和GCP,Docker在其他不同的IaaS提供商也运行的非常好,例如微软的Azure、OpenStack和可以被具有不同配置的管理者所使用的Chef、Puppet、Ansible等。
环境标准化和版本控制
通过上面的讨论,Docker容器可以在不同的开发与产品发布生命周期中确保一致性,进而标准化你的环境。除此之外,Docker容器还可以像git仓库一样,可以让你提交变更到Docker镜像中并通过不同的版本来管理它们。设想如果你因为完成了一个组件的升级而导致你整个环境都损坏了,Docker可以让你轻松地回滚到这个镜像的前一个版本。这整个过程可以在几分钟内完成,如果和虚拟机的备份或者镜像创建流程对比,那Docker算相当快的,它可以让你快速地进行复制和实现冗余。此外,启动Docker就和运行一个进程一样快。
隔离性
Docker可以确保你的应用程序与资源是分隔开的。几个月前,Gartner发表了一篇报告,这份报告说明了运行Docker 容器进行资源隔离的效果和虚拟机(VM)管理程序一样的好,但是管理与控制方面还需要进行完善。
我们考虑这样一个场景,你在你的虚拟机中运行了很多应用程序,这些应用程序包括团队协作软件(例如Confluence)、问题追踪软件(例如JIRA)、集中身份管理系统(例如Crowd)等等。由于这些软件运行在不同的端口上,所以你必须使用Apache或者Nginx来做反向代理。到目前为止,一切都很正常,但是随着你的环境向前推进,你需要在你现有的环境中配置一个内容管理系统(例如Alfresco)。这时候有个问题发生了,这个软件需要一个不同版本的Apache Tomcat,为了满足这个需求,你只能将你现有的软件迁移到另一个版本的Tomcat上,或者找到适合你现有Tomcat的内容管理系统(Alfresco)版本。
对于上述场景,使用Docker就不用做这些事情了。Docker能够确保每个容器都拥有自己的资源,并且和其他容器是隔离的。你可以用不同的容器来运行使用不同堆栈的应用程序。除此之外,如果你想在服务器上直接删除一些应用程序是比较困难的,因为这样可能引发依赖关系冲突。而Docker可以帮你确保应用程序被完全清除,因为不同的应用程序运行在不同的容器上,如果你不在需要一款应用程序,那你可以简单地通过删除容器来删除这个应用程序,并且在你的宿主机操作系统上不会留下任何的临时文件或者配置文件。
除了上述好处,Docker还能确保每个应用程序只使用分配给它的资源(包括CPU、内存和磁盘空间)。一个特殊的软件将不会使用你全部的可用资源,要不然这将导致性能降低,甚至让其他应用程序完全停止工作。
安全性
如上所述,Gartner也承认Docker正在快速地发展。从安全角度来看,Docker确保运行在容器中的应用程序和其他容器中的应用程序是完全分隔与隔离的,在通信流量和管理上赋予你完全的控制权。Docker容器不能窥视运行在其他容器中的进程。从体系结构角度来看,每个容器只使用着自己的资源(从进程到网络堆栈)。
作为紧固安全的一种手段,Docker将宿主机操作系统上的敏感挂载点(例如/proc和/sys)作为只读挂载点,并且使用一种写时复制系统来确保容器不能读取其他容器的数据。Docker也限制了宿主机操作系统上的一些系统调用,并且和SELinux与AppArmor一起运行的很好。此外,在Docker Hub上可以使用的Docker镜像都通过数字签名来确保其可靠性。由于Docker容器是隔离的,并且资源是受限制的,所以即使你其中一个应用程序被黑,也不会影响运行在其它Docker容器上的应用程序。
结语
将云计算一起考虑,上面提到的这些好处能够清楚地证明Docker是一个有效的开源平台。使用Docker的好处越来越多,今天我只想强调这前五大好处。如果你使用了Docker,欢迎分享你的使用案例或者任何你觉得使用Docker带来的好处。
收起阅读 »
持续部署与测试
Docker在开发与运维的世界中具有极大的吸引力,因为它能保持跨环境的一致性。在开发与发布的生命周期中,不同的环境具有细微的不同,这些差异可能是由于不同安装包的版本和依赖关系引起的。然而,Docker可以通过确保从开发到产品发布整个过程环境的一致性来解决这个问题*Docker容器通过相关配置,保持容器内部所有的配置和依赖关系始终不变。最终,你可以在开发到产品发布的整个过程中使用相同的容器来确保没有任何差异或者人工干预。
使用Docker,你还可以确保开发者不需要配置完全相同的产品环境,他们可以在他们自己的系统上通过VirtualBox建立虚拟机来运行Docker容器。Docker的魅力在于它同样可以让你在亚马逊EC2实例上运行相同的容器。如果你需要在一个产品发布周期中完成一次升级,你可以很容易地将需要变更的东西放到Docker容器中,测试它们,并且使你已经存在的容器执行相同的变更。这种灵活性就是使用Docker的一个主要好处。和标准部署与集成过程一样,Docker可以让你构建、测试和发布镜像,这个镜像可以跨多个服务器进行部署。哪怕安装一个新的安全补丁,整个过程也是一样的。你可以安装补丁,然后测试它,并且将这个补丁发布到产品中。
多云平台
Docker最大的好处之一就是可移植性。在过去的几年里,所有主流的云计算提供商,包括亚马逊AWS和谷歌的GCP,都将Docker融入到他们的平台并增加了各自的支持。Docker容器能运行在亚马逊的EC2实例、谷歌的GCP实例、Rackspace服务器或者VirtualBox这些提供主机操作系统的平台上。举例来说,如果运行在亚马逊EC2实例上的Docker容器能够很容易地移植到其他几个平台上,比如说VirtualBox,并且达到类似的一致性和功能性,那这将允许你从基础设施层中抽象出来。除了AWS和GCP,Docker在其他不同的IaaS提供商也运行的非常好,例如微软的Azure、OpenStack和可以被具有不同配置的管理者所使用的Chef、Puppet、Ansible等。
环境标准化和版本控制
通过上面的讨论,Docker容器可以在不同的开发与产品发布生命周期中确保一致性,进而标准化你的环境。除此之外,Docker容器还可以像git仓库一样,可以让你提交变更到Docker镜像中并通过不同的版本来管理它们。设想如果你因为完成了一个组件的升级而导致你整个环境都损坏了,Docker可以让你轻松地回滚到这个镜像的前一个版本。这整个过程可以在几分钟内完成,如果和虚拟机的备份或者镜像创建流程对比,那Docker算相当快的,它可以让你快速地进行复制和实现冗余。此外,启动Docker就和运行一个进程一样快。
隔离性
Docker可以确保你的应用程序与资源是分隔开的。几个月前,Gartner发表了一篇报告,这份报告说明了运行Docker 容器进行资源隔离的效果和虚拟机(VM)管理程序一样的好,但是管理与控制方面还需要进行完善。
我们考虑这样一个场景,你在你的虚拟机中运行了很多应用程序,这些应用程序包括团队协作软件(例如Confluence)、问题追踪软件(例如JIRA)、集中身份管理系统(例如Crowd)等等。由于这些软件运行在不同的端口上,所以你必须使用Apache或者Nginx来做反向代理。到目前为止,一切都很正常,但是随着你的环境向前推进,你需要在你现有的环境中配置一个内容管理系统(例如Alfresco)。这时候有个问题发生了,这个软件需要一个不同版本的Apache Tomcat,为了满足这个需求,你只能将你现有的软件迁移到另一个版本的Tomcat上,或者找到适合你现有Tomcat的内容管理系统(Alfresco)版本。
对于上述场景,使用Docker就不用做这些事情了。Docker能够确保每个容器都拥有自己的资源,并且和其他容器是隔离的。你可以用不同的容器来运行使用不同堆栈的应用程序。除此之外,如果你想在服务器上直接删除一些应用程序是比较困难的,因为这样可能引发依赖关系冲突。而Docker可以帮你确保应用程序被完全清除,因为不同的应用程序运行在不同的容器上,如果你不在需要一款应用程序,那你可以简单地通过删除容器来删除这个应用程序,并且在你的宿主机操作系统上不会留下任何的临时文件或者配置文件。
除了上述好处,Docker还能确保每个应用程序只使用分配给它的资源(包括CPU、内存和磁盘空间)。一个特殊的软件将不会使用你全部的可用资源,要不然这将导致性能降低,甚至让其他应用程序完全停止工作。
安全性
如上所述,Gartner也承认Docker正在快速地发展。从安全角度来看,Docker确保运行在容器中的应用程序和其他容器中的应用程序是完全分隔与隔离的,在通信流量和管理上赋予你完全的控制权。Docker容器不能窥视运行在其他容器中的进程。从体系结构角度来看,每个容器只使用着自己的资源(从进程到网络堆栈)。
作为紧固安全的一种手段,Docker将宿主机操作系统上的敏感挂载点(例如/proc和/sys)作为只读挂载点,并且使用一种写时复制系统来确保容器不能读取其他容器的数据。Docker也限制了宿主机操作系统上的一些系统调用,并且和SELinux与AppArmor一起运行的很好。此外,在Docker Hub上可以使用的Docker镜像都通过数字签名来确保其可靠性。由于Docker容器是隔离的,并且资源是受限制的,所以即使你其中一个应用程序被黑,也不会影响运行在其它Docker容器上的应用程序。
结语
将云计算一起考虑,上面提到的这些好处能够清楚地证明Docker是一个有效的开源平台。使用Docker的好处越来越多,今天我只想强调这前五大好处。如果你使用了Docker,欢迎分享你的使用案例或者任何你觉得使用Docker带来的好处。
收起阅读 »
深度思考:互联网产业的困境与进化
在互联网、风险投资和资本市场互相结合、互相支持的运作机制下,依托“互联网产业资本市场估值特权”而制造的造富效应是十分惊人的。在这种造富效应的烘托下,互联网产业必然产生一系列的政治、社会、文化影响。这些影响是非常深刻的,它会反馈给科技、产业,影响基本的生活态度,进而影响人类未来的命运。
互联网公司在获得人类社会高科技公司的代表资格之后,开始塑造文化尤其是高智商、高技术人群的文化。如果将1920-1970年代的科技文化与如今互联网为首的科技文化作对比,就能发现两者气质很不一样。在1920-1970年代,从早期的航空到后来的航天,代表了一种强有力的生产型文化,那时赞美这种社会进步的艺术如装饰类风格(ART DECO)等,富有男性阳刚美,大有“改天换地舍我其谁”的气概,这在美、苏、德、日、韩等不同社会制度的国家都是类似的。
但到1980年代末之后,这种“边疆开拓”的风格就悄然消退了。这种蜕化鲜明地反映在某些行业的研究重点上,比如航天,1990年代以前主要是对地球以外的探测,对地球主要是搞卫星通讯;1990年代以后,由于“发明”了气候变化理论,西方航天主流一个劲地研究地球本身,地球以外研究领域配置资源占比下降。这是人类前沿――科技行业的重大气质转折。
当然,这时候中国出现了,因为没有太多受到二十世纪七八十年代西方社会思潮的影响,中国延续了西方二十世纪五六十年代黄金时代的思维,一举成为世界工业中心,西方大工程领域的大批精英纷纷投靠中国,直到如今大量发展中国家被中国模式所吸引,群起响应“一带一路,互联互通”。而西方进入互联网时代尤其是近几年社交-移动互联网时代后,很多公司一方面说是高科技公司,另一方面又特像时尚传媒公司,“小清新”味道很浓,与此前“边疆开拓”的科技气质已经非常不同。
那么,人类科技系统是怎么走到今天这一步的?它未来会怎么发展?有没有可能再把过去的优秀成分拾起来形成新的文化?这关系到全球科技产业下一步提高的可能性,进而影响资本市场价值创造的性质――是有着坚实的实体基础还是浮夸的泡沫。需要说明的是,互联网走到今天这步有着复杂的缘由,和1920-1970年代西方科技繁盛期的理念并不是截然两条路的,它的起源其实包含了后者最优秀的文化元素,但因为其他因素的渗透出现了“变异”,变成了今天这个模样。
工业时代后期科技机制的难题与“极客资本主义”的诞生
工业时代开启后,科技类人才获得普遍尊敬始于19世纪后半叶的美国和德国,这两个国家形成了系统性培养工程师的传统。一大批技术人才转变为企业家,取得社会的尊敬。此时的科技企业家,集科技、探索、企业、资本于一身,主要体现的是开拓边疆的气质,颇具“征服自然”的男子汉气概。但是,随着技术进步的要求,工业生产日趋复杂,对应管理系统也快速进化成复杂的科层制,组织设计及管理本身成为一门学问。
这样的组织机构有其优势也有其劣势。优势在于机构庞大,经营稳定,有充分的剩余来养活其内部的研究所。在这些研究部门里,一些耐得住寂寞,对创办企业没有欲望的纯技术天才能够创造出让人惊叹的基础性产品,为后世再次技术起飞创造基础。比如,一度垄断美国电信行业的美国电话电报公司(AT&T)旗下的贝尔实验室,先后发明了射电天文学、半导体、激光、信息论、UNIX操作系统、C语言和C++语言,实现了商用微波通信、商用通信卫星……几乎整个现代信息产业的基础就在那里诞生。其弊端是企业有动力维持垄断高利润,经营改进的动力放缓(AT&T经营的呆板是惊人的),后台慢慢地也就按部就班――大部分科研人员也就是平庸的,要由少数书呆子天才来产生闪光点,而这些闪光点有时会因为前台经营的慵懒而被应用缓慢。总而言之,“经理人资本主义”锐气减弱,但蕴藏了大量潜在的人类科技财富。
在这样的社会环境里,人才怎么办?体系内偶尔能出全才,其中有的人很可能得到快速提升,但事实证明这样的全才往往痛恨这个体系,最后成为改革者(比如通用电气的韦尔奇)。另一种技术性人才则选择了反叛:他们要么从这样的体系里离职创办新公司,要么干脆就不加入这样的巨人而另起炉灶。这样的文化最早诞生于1960年代――西方已经相当富足同时又比较平均的年代,年轻人学习了知识,又没有经历过战争,倾向于认为自己无所不能,这就是极客文化(Geek)的来源。
这个群体中有很多天才,他们不甘心于当螺丝钉,而有兴趣了解各种事情,并付诸于个人实践――包括制造新奇玩意,进而创造公司。极客文化的大本营之一就在加州――一个美丽、富足、温暖的地方。到1970年代,以半导体、电子产业为核心的信息产业已经经历了资本市场追捧的热潮,加州硅谷及其风险投资机制正在启动。而技术进步使得计算机从大型机开始小型、微型化发展,计算机的文化形象从二十世纪五六十年代“军工联合体科层体系”的“监视工具”(好比《1984》老大哥的探头)变成个人自由的万能工具。于是一些投资人开始促成大量小团体的科技发明转化为企业(比如马库拉促成乔布斯及其伙伴成立苹果)。这些新诞生的企业,相对于老一辈信息企业如IBM,早期就是小不点,在西方文化中有“大卫对抗巨人哥利亚”的道德感。
很自然的,这样的新创科技企业又吸收了1960年代学生运动(加州伯克利正是美国左派学生运动的心脏)、嬉皮士“重归田园”运动的叛逆色彩,崇尚个人自由,并把这种意识形态注入企业经营的口号里。虽然这些企业成熟后,其内部经营仍然充满政治斗争与领导专断(比如苹果),但在产品设计、宣传口号上则高喊消费者自由,并以此得到已经中产化的社会的支持。极客得到风险投资进而资本市场的追捧,从而产生“极客资本主义”。这种文化底子为1990年代互联网发展时的基调做了铺垫。
美国资本市场大慢牛对“轻产业”的追捧及变迁
1980年代“大慢牛”盘活了美国资本市场,自然需要有新的内容充实其牛市根基。资本的欲求天然地追逐具有爆炸性增长潜力或想象的领域。1980年代的产业寻找选择了电信(通信)产业,并期望于整合广电、传媒产业。原因是这个领域比较“轻”,而且人的通信需求看起来是无穷无尽的,增长空间大。
以资本青睐“爆炸性增长”的标准来选择产业,选择信息产业并最后选择纯信息的互联网产业是必然的。与传统工业相比,信息内容被认为更可能可实现爆炸性增长。这个领域不涉及物质(至少到2013年智能硬件兴起前是如此),各种开发都在相当表层的应用层面(网站做到巨型之后才要考虑架构优化问题),而且始终不需要什么生产设备(巨型网站最多也就需要堆海量服务器),所以至少在创业阶段可以“很轻”。当然,和传统通信产业相比,强调信息获取便捷性和免费性,但内容又只是纯信息本身的互联网产业更难从实体经营中获取收入,因此,如笔者分析互联网公司资本市场的奥妙所显示的,美国资本市场参与者修订了资本市场规则,建立事实上的“互联网公司豁免权”,对互联网公司不再计算信息增长带来的收入增长,而追求信息量或某个业务指标量(如注册用户数)本身的增长,做出信息爆炸的样子。
同时,又对长期成本支出(如技术研发尤其是基础技术研发)有着本能的厌恶,只对能带来指标量爆炸性增长的支出感兴趣。这使得互联网公司天生带上了传媒行业的基因。自1995年网景上市以来,互联网一直是风投的重点,近五六年甚至成为美国风投财富创造的绝大部分源泉,而高科技行业也正是从那时开始带上了传媒产业色彩,与以往的科技产业乃至1990年代以前信息产业形成强烈对比。
互联网产业追求眼球的“传媒产业”天性及其后果
互联网产业因为是纯信息产业,只要找对门路,所以可以脱离上下游的物质羁绊而爆炸式发展。这种形象与“极客”文化气质正好相符合,于是成为新世纪最好的“企业英雄”。1990年代以来,整个科技产业传媒越来越倾向于渲染明星企业、独行侠或明星团队,而不再提及大产业系统的复杂性。同理,投资它们的天使、风险投资人以及先创业再转化为风投的个人,也成为类似的英雄。这就是美国近二十年“明星企业”道路的特点。这样一条道路媒体性极强。这是由信息产业技术和本身业务内容两方面决定的。
从信息产业技术方面来看,随着基础技术的阶段性成熟,就会出现“模块化”的现象,也就是新从业者不需要从底部干起,底层技术可以以“模块”的形态直接使用,这使得应用层面的生产者(或服务提供者)“傻瓜化”。举一个相近领域的产业――手机制造业来说明,2005年以前,手机是高端产品,只有诺基亚、摩托罗拉公司能生产,但是亚洲一些电子、芯片企业将手机核心部件模块化,它们的动力就是让更多的老板能够生产手机,不再为少数手机的发明企业垄断,扩大其下游市场。
于是手机生产的门槛大大降低,这就有了深圳的山寨手机进而山寨智能手机的繁盛。但是当门槛普遍降低的时候,手机这个领域的竞争焦点就前移到市场营销方面,当门槛进一步降低到有一定资源的个人也可以做自己的品牌手机的时候,竞争就更下移到口碑传播方面,于是会编段子的脱口秀演员而不是工程师型企业家就更容易赢得媒体聚光灯的青睐。这个产业的媒体性就大大增强。
互联网也是这样,由于西方开源社区的贡献,开发流程模块化,网站、手机应用开发难度大大降低,行业竞争焦点转移到抢占用户、UI界面的简洁靓丽上,美术人才而非技术人才成了关键。中国2013年以后“互联网思维”满天飞,一些年轻人语不惊人死不休,也是这个机理所致。
从互联网本身业务内容上看,它天生倾向于媒体化。由于生产领域的系统性要求比较强,一个企业单点突破基本不可能,所以美国互联网近十年的主要突破点都在消费、社交等非生产领域。互联网诞生之初,因为本身内容免费,最后选择的商业模式只能是广告,这就是媒体行业的商业模式,所以谷歌本身就类似于媒体的广告部。2005年以后,形形色色的社交互联网干脆自身就是个媒体。实际上这已经是个媒体行当而不是一个科技行当了。
而互联网的中心在加州,加州又恰恰是西方左派思潮的大本营(反对大工业大公司、“环保”、气候变化、女权运动、同性恋权利、动物权利、“不作恶”……),各种互联网企业或者为了迎合第一批用户的需求,或者自身创立者就是左派媒体、社区工作者(如Twitter创始人),无不把自己的外表风格、经营风格整得非常的“文艺范”、“小清新”。而这些思潮由于普遍不能带来经济利益甚至是“负经济利益”,或者经不起科学的推敲,所以特别重视媒体传播,需要用“压倒声势”的宣传来站住脚。于是两者一拍即合,新兴社交-移动互联网公司用加州文艺风格,推送西方左派内容,西方左派运动借它们推销自己,发展社会运动,并成了“高科技”这个词语的定义者。这正是西方左派政治代表人物希拉里那么喜欢Facebook、Twitter的原因。
更进一步的是,由于互联网创业技术门槛大大降低,大量本来没什么经济地位的左翼社会人士开始互联网创业,并通过资本市场对互联网企业的热捧,也能实现以前不可想象的数十亿美元身家――比如估值百亿美元以上的Airbnb,创始人就是个加州风格整天闲游的背包客。至此,信息产业的文化风格走向了其诞生时的反面――IBM的创立者老沃森和半导体发明者兼企业家肖克利都是非常典型的保守主义者,他们的初衷是为生产、军事服务。
但是,具有强烈传媒性的移动互联网的大发展其实对科技进步有着强需求――因为它催生了对大量高质量的图像、声音、视频的需求,带来了海量数据的传输和处理需求,这需要强大通信技术的支撑。可是经过前几年通信产业的优胜劣汰,西方通信设备企业已经没有几家能支撑研发这样的技术。华为领导人任正非这几年常说“要抢占大数据的战略制高点,占住这个制高点,别人将来想攻下来就难了……小孩拿着手机啪啪啪拍照不删减就往数据中心传,流量超几何级数的增长……华为要做太平洋那么粗(信息)管道的铁皮,全世界能做的没几家”,就是针对这个趋势说的。这个大数据才是真正的大数据技术,硬骨头。
事实上,正是这几年在中国悄然完成的光通信宽带革命支撑了新出现在公众眼前的互联网电视、智能手机产业。如果在西方设备企业已经无法完成这一社会需求的时候,中国的集成电路、光通信、无线通信技术能继续像任正非所说的那样取得主导地位,那么中国就能在未来人类生活――不管它走哪种内容路线――把握住其科技基础。从这个意义上说,中国必须扶植互联网之外的信息技术“中坚”企业。
以上述这种“媒体化”的趋向,互联网领域的成功企业表现出几个重要特点:
所以从内心上来说,互联网人和19世纪后期以来的那些创业资本家的心态有很大不同,后者相信边疆无限,资源可以不断地被发现并转换成可利用的形态,是乐观派,而前者则认为资源有限,必须先下手为强,而且要主动攻击置对手于死地,在对手处于萌芽状态时就消灭它,这是一种深沉的悲观派,中国互联网圈内崇拜《三体》这部科幻小说就是这种心态的极好反映。而这种基于资源有限的深沉悲观正和资本市场上互联网公司被撑得极大的市值(反映了资本的高期望)形成了巨大的张力。
真正的互联网精神
今天,互联网思维在中国得到了广泛认同。但上文所述的种种偏媒体泡沫化的现象,显然指向其中的问题。那么,有没有互联网精神呢?笔者相信当然是有的,世界上有真正的互联网精神,应该把它同媒体化、泡沫化的伪互联网精神区分开来。那么,真正的互联网精神在哪里?恐怕要从它的源头说起。
正如在互联网UGC的实践中,精英聚在一起还是精英,垃圾聚在一起仍然是垃圾的结果所暗示的,真正的互联网精神是通过共享的网络平台实现的“精英共和制”和“无限边疆观”。当前的互联网模式已经把这种精神在很大程度上掩盖了,变成了比表层工夫――在中国干脆成了比噱头。
需要指出的是,互联网尤其是近几年来的移动互联网带来的对既有资源的更有效利用,是创造了很大价值的。但是,这是在生产力老本上进行资源优化配置,生产力基础还没有被驱动进步。网络本身有可能成为一种先进的生产力(例如,在先进的传输和终端技术的支持下,人们实现知识的快速自动吸收和分享),但这种未来场景靠目前的互联网文化很难实现――因为它意味着生产层面的技术大变革,而当前移动互联网的热门集中在对原有生产力资源的消费上,比如2013年以来最时髦的O2O模式(“线上线下联动”),相当于把人类现代社会中的生活方方面面再用互联网走一遍,按照2012年以来美国资本市场的玩法,每个领域都可以创造一个至少大中型市值水平的上市公司。 收起阅读 »
互联网公司在获得人类社会高科技公司的代表资格之后,开始塑造文化尤其是高智商、高技术人群的文化。如果将1920-1970年代的科技文化与如今互联网为首的科技文化作对比,就能发现两者气质很不一样。在1920-1970年代,从早期的航空到后来的航天,代表了一种强有力的生产型文化,那时赞美这种社会进步的艺术如装饰类风格(ART DECO)等,富有男性阳刚美,大有“改天换地舍我其谁”的气概,这在美、苏、德、日、韩等不同社会制度的国家都是类似的。
但到1980年代末之后,这种“边疆开拓”的风格就悄然消退了。这种蜕化鲜明地反映在某些行业的研究重点上,比如航天,1990年代以前主要是对地球以外的探测,对地球主要是搞卫星通讯;1990年代以后,由于“发明”了气候变化理论,西方航天主流一个劲地研究地球本身,地球以外研究领域配置资源占比下降。这是人类前沿――科技行业的重大气质转折。
当然,这时候中国出现了,因为没有太多受到二十世纪七八十年代西方社会思潮的影响,中国延续了西方二十世纪五六十年代黄金时代的思维,一举成为世界工业中心,西方大工程领域的大批精英纷纷投靠中国,直到如今大量发展中国家被中国模式所吸引,群起响应“一带一路,互联互通”。而西方进入互联网时代尤其是近几年社交-移动互联网时代后,很多公司一方面说是高科技公司,另一方面又特像时尚传媒公司,“小清新”味道很浓,与此前“边疆开拓”的科技气质已经非常不同。
那么,人类科技系统是怎么走到今天这一步的?它未来会怎么发展?有没有可能再把过去的优秀成分拾起来形成新的文化?这关系到全球科技产业下一步提高的可能性,进而影响资本市场价值创造的性质――是有着坚实的实体基础还是浮夸的泡沫。需要说明的是,互联网走到今天这步有着复杂的缘由,和1920-1970年代西方科技繁盛期的理念并不是截然两条路的,它的起源其实包含了后者最优秀的文化元素,但因为其他因素的渗透出现了“变异”,变成了今天这个模样。
工业时代后期科技机制的难题与“极客资本主义”的诞生
工业时代开启后,科技类人才获得普遍尊敬始于19世纪后半叶的美国和德国,这两个国家形成了系统性培养工程师的传统。一大批技术人才转变为企业家,取得社会的尊敬。此时的科技企业家,集科技、探索、企业、资本于一身,主要体现的是开拓边疆的气质,颇具“征服自然”的男子汉气概。但是,随着技术进步的要求,工业生产日趋复杂,对应管理系统也快速进化成复杂的科层制,组织设计及管理本身成为一门学问。
这样的组织机构有其优势也有其劣势。优势在于机构庞大,经营稳定,有充分的剩余来养活其内部的研究所。在这些研究部门里,一些耐得住寂寞,对创办企业没有欲望的纯技术天才能够创造出让人惊叹的基础性产品,为后世再次技术起飞创造基础。比如,一度垄断美国电信行业的美国电话电报公司(AT&T)旗下的贝尔实验室,先后发明了射电天文学、半导体、激光、信息论、UNIX操作系统、C语言和C++语言,实现了商用微波通信、商用通信卫星……几乎整个现代信息产业的基础就在那里诞生。其弊端是企业有动力维持垄断高利润,经营改进的动力放缓(AT&T经营的呆板是惊人的),后台慢慢地也就按部就班――大部分科研人员也就是平庸的,要由少数书呆子天才来产生闪光点,而这些闪光点有时会因为前台经营的慵懒而被应用缓慢。总而言之,“经理人资本主义”锐气减弱,但蕴藏了大量潜在的人类科技财富。
在这样的社会环境里,人才怎么办?体系内偶尔能出全才,其中有的人很可能得到快速提升,但事实证明这样的全才往往痛恨这个体系,最后成为改革者(比如通用电气的韦尔奇)。另一种技术性人才则选择了反叛:他们要么从这样的体系里离职创办新公司,要么干脆就不加入这样的巨人而另起炉灶。这样的文化最早诞生于1960年代――西方已经相当富足同时又比较平均的年代,年轻人学习了知识,又没有经历过战争,倾向于认为自己无所不能,这就是极客文化(Geek)的来源。
这个群体中有很多天才,他们不甘心于当螺丝钉,而有兴趣了解各种事情,并付诸于个人实践――包括制造新奇玩意,进而创造公司。极客文化的大本营之一就在加州――一个美丽、富足、温暖的地方。到1970年代,以半导体、电子产业为核心的信息产业已经经历了资本市场追捧的热潮,加州硅谷及其风险投资机制正在启动。而技术进步使得计算机从大型机开始小型、微型化发展,计算机的文化形象从二十世纪五六十年代“军工联合体科层体系”的“监视工具”(好比《1984》老大哥的探头)变成个人自由的万能工具。于是一些投资人开始促成大量小团体的科技发明转化为企业(比如马库拉促成乔布斯及其伙伴成立苹果)。这些新诞生的企业,相对于老一辈信息企业如IBM,早期就是小不点,在西方文化中有“大卫对抗巨人哥利亚”的道德感。
很自然的,这样的新创科技企业又吸收了1960年代学生运动(加州伯克利正是美国左派学生运动的心脏)、嬉皮士“重归田园”运动的叛逆色彩,崇尚个人自由,并把这种意识形态注入企业经营的口号里。虽然这些企业成熟后,其内部经营仍然充满政治斗争与领导专断(比如苹果),但在产品设计、宣传口号上则高喊消费者自由,并以此得到已经中产化的社会的支持。极客得到风险投资进而资本市场的追捧,从而产生“极客资本主义”。这种文化底子为1990年代互联网发展时的基调做了铺垫。
美国资本市场大慢牛对“轻产业”的追捧及变迁
1980年代“大慢牛”盘活了美国资本市场,自然需要有新的内容充实其牛市根基。资本的欲求天然地追逐具有爆炸性增长潜力或想象的领域。1980年代的产业寻找选择了电信(通信)产业,并期望于整合广电、传媒产业。原因是这个领域比较“轻”,而且人的通信需求看起来是无穷无尽的,增长空间大。
以资本青睐“爆炸性增长”的标准来选择产业,选择信息产业并最后选择纯信息的互联网产业是必然的。与传统工业相比,信息内容被认为更可能可实现爆炸性增长。这个领域不涉及物质(至少到2013年智能硬件兴起前是如此),各种开发都在相当表层的应用层面(网站做到巨型之后才要考虑架构优化问题),而且始终不需要什么生产设备(巨型网站最多也就需要堆海量服务器),所以至少在创业阶段可以“很轻”。当然,和传统通信产业相比,强调信息获取便捷性和免费性,但内容又只是纯信息本身的互联网产业更难从实体经营中获取收入,因此,如笔者分析互联网公司资本市场的奥妙所显示的,美国资本市场参与者修订了资本市场规则,建立事实上的“互联网公司豁免权”,对互联网公司不再计算信息增长带来的收入增长,而追求信息量或某个业务指标量(如注册用户数)本身的增长,做出信息爆炸的样子。
同时,又对长期成本支出(如技术研发尤其是基础技术研发)有着本能的厌恶,只对能带来指标量爆炸性增长的支出感兴趣。这使得互联网公司天生带上了传媒行业的基因。自1995年网景上市以来,互联网一直是风投的重点,近五六年甚至成为美国风投财富创造的绝大部分源泉,而高科技行业也正是从那时开始带上了传媒产业色彩,与以往的科技产业乃至1990年代以前信息产业形成强烈对比。
互联网产业追求眼球的“传媒产业”天性及其后果
互联网产业因为是纯信息产业,只要找对门路,所以可以脱离上下游的物质羁绊而爆炸式发展。这种形象与“极客”文化气质正好相符合,于是成为新世纪最好的“企业英雄”。1990年代以来,整个科技产业传媒越来越倾向于渲染明星企业、独行侠或明星团队,而不再提及大产业系统的复杂性。同理,投资它们的天使、风险投资人以及先创业再转化为风投的个人,也成为类似的英雄。这就是美国近二十年“明星企业”道路的特点。这样一条道路媒体性极强。这是由信息产业技术和本身业务内容两方面决定的。
从信息产业技术方面来看,随着基础技术的阶段性成熟,就会出现“模块化”的现象,也就是新从业者不需要从底部干起,底层技术可以以“模块”的形态直接使用,这使得应用层面的生产者(或服务提供者)“傻瓜化”。举一个相近领域的产业――手机制造业来说明,2005年以前,手机是高端产品,只有诺基亚、摩托罗拉公司能生产,但是亚洲一些电子、芯片企业将手机核心部件模块化,它们的动力就是让更多的老板能够生产手机,不再为少数手机的发明企业垄断,扩大其下游市场。
于是手机生产的门槛大大降低,这就有了深圳的山寨手机进而山寨智能手机的繁盛。但是当门槛普遍降低的时候,手机这个领域的竞争焦点就前移到市场营销方面,当门槛进一步降低到有一定资源的个人也可以做自己的品牌手机的时候,竞争就更下移到口碑传播方面,于是会编段子的脱口秀演员而不是工程师型企业家就更容易赢得媒体聚光灯的青睐。这个产业的媒体性就大大增强。
互联网也是这样,由于西方开源社区的贡献,开发流程模块化,网站、手机应用开发难度大大降低,行业竞争焦点转移到抢占用户、UI界面的简洁靓丽上,美术人才而非技术人才成了关键。中国2013年以后“互联网思维”满天飞,一些年轻人语不惊人死不休,也是这个机理所致。
从互联网本身业务内容上看,它天生倾向于媒体化。由于生产领域的系统性要求比较强,一个企业单点突破基本不可能,所以美国互联网近十年的主要突破点都在消费、社交等非生产领域。互联网诞生之初,因为本身内容免费,最后选择的商业模式只能是广告,这就是媒体行业的商业模式,所以谷歌本身就类似于媒体的广告部。2005年以后,形形色色的社交互联网干脆自身就是个媒体。实际上这已经是个媒体行当而不是一个科技行当了。
而互联网的中心在加州,加州又恰恰是西方左派思潮的大本营(反对大工业大公司、“环保”、气候变化、女权运动、同性恋权利、动物权利、“不作恶”……),各种互联网企业或者为了迎合第一批用户的需求,或者自身创立者就是左派媒体、社区工作者(如Twitter创始人),无不把自己的外表风格、经营风格整得非常的“文艺范”、“小清新”。而这些思潮由于普遍不能带来经济利益甚至是“负经济利益”,或者经不起科学的推敲,所以特别重视媒体传播,需要用“压倒声势”的宣传来站住脚。于是两者一拍即合,新兴社交-移动互联网公司用加州文艺风格,推送西方左派内容,西方左派运动借它们推销自己,发展社会运动,并成了“高科技”这个词语的定义者。这正是西方左派政治代表人物希拉里那么喜欢Facebook、Twitter的原因。
更进一步的是,由于互联网创业技术门槛大大降低,大量本来没什么经济地位的左翼社会人士开始互联网创业,并通过资本市场对互联网企业的热捧,也能实现以前不可想象的数十亿美元身家――比如估值百亿美元以上的Airbnb,创始人就是个加州风格整天闲游的背包客。至此,信息产业的文化风格走向了其诞生时的反面――IBM的创立者老沃森和半导体发明者兼企业家肖克利都是非常典型的保守主义者,他们的初衷是为生产、军事服务。
但是,具有强烈传媒性的移动互联网的大发展其实对科技进步有着强需求――因为它催生了对大量高质量的图像、声音、视频的需求,带来了海量数据的传输和处理需求,这需要强大通信技术的支撑。可是经过前几年通信产业的优胜劣汰,西方通信设备企业已经没有几家能支撑研发这样的技术。华为领导人任正非这几年常说“要抢占大数据的战略制高点,占住这个制高点,别人将来想攻下来就难了……小孩拿着手机啪啪啪拍照不删减就往数据中心传,流量超几何级数的增长……华为要做太平洋那么粗(信息)管道的铁皮,全世界能做的没几家”,就是针对这个趋势说的。这个大数据才是真正的大数据技术,硬骨头。
事实上,正是这几年在中国悄然完成的光通信宽带革命支撑了新出现在公众眼前的互联网电视、智能手机产业。如果在西方设备企业已经无法完成这一社会需求的时候,中国的集成电路、光通信、无线通信技术能继续像任正非所说的那样取得主导地位,那么中国就能在未来人类生活――不管它走哪种内容路线――把握住其科技基础。从这个意义上说,中国必须扶植互联网之外的信息技术“中坚”企业。
以上述这种“媒体化”的趋向,互联网领域的成功企业表现出几个重要特点:
- 其一,著名互联网公司只在人口大国中产生。
- 其二,成功的互联网公司通常是2C(to consumer,面向个人消费者)而不是2B(to business,面向公司型客户)。
- 其三,互联网明星公司获得高估值后,其获得的资金将强化上游那些在技术密集型行业中已经获得成功的公司(他们在资本市场上已经不再被青睐)的地位。
- 第一,应用大放异彩的同时基础科技储备开始吃紧。
- 第二,放大财富分化。互联网“明星企业”模式会进一步放大财富的分化。
- 第三,UGC平台泡沫。UGC(User Generated Content,用户生产内容的平台模式)是2004年前后web2.0(博客、维基是第一批2.0产物)的思想精髓,到社交互联网时代放大,是“互联网思维”的核心。
- 第四,争夺用户导致末日心态。互联网产业重心在于抓用户。
- 一方面在资本市场上“打肿脸充胖子”,阅读近几年互联网公司的上市招股说明书,可发现他们特别强调自己的“用户数”,动辄数千万,多则数亿。如果看完若干互联网公司的招股说明书,可能会很奇怪:为什么它们的用户数那么多但我自己却从来没有使用过,也不曾有印象周围的人使用过。答案是利用“用户定义”的伸缩空间,把那种每月登录一次(社交网站)的注册人也算做用户,通过强调用户数来获取估值――实质上大部分用户形成收入的可能性几乎为零。
- 另一方面,这种对于“人的时间”资源有限的忧虑,又影响了互联网企业家的心态――虽然互联网在不断产生出新的想法,创造出新的边疆,但在“人的时间”这个大陆里面不断加塞新想法将使得互联网世界越来越拥挤,因此对于单个互联网公司来说“资源”是有限的甚至是在萎缩的。
所以从内心上来说,互联网人和19世纪后期以来的那些创业资本家的心态有很大不同,后者相信边疆无限,资源可以不断地被发现并转换成可利用的形态,是乐观派,而前者则认为资源有限,必须先下手为强,而且要主动攻击置对手于死地,在对手处于萌芽状态时就消灭它,这是一种深沉的悲观派,中国互联网圈内崇拜《三体》这部科幻小说就是这种心态的极好反映。而这种基于资源有限的深沉悲观正和资本市场上互联网公司被撑得极大的市值(反映了资本的高期望)形成了巨大的张力。
真正的互联网精神
今天,互联网思维在中国得到了广泛认同。但上文所述的种种偏媒体泡沫化的现象,显然指向其中的问题。那么,有没有互联网精神呢?笔者相信当然是有的,世界上有真正的互联网精神,应该把它同媒体化、泡沫化的伪互联网精神区分开来。那么,真正的互联网精神在哪里?恐怕要从它的源头说起。
正如在互联网UGC的实践中,精英聚在一起还是精英,垃圾聚在一起仍然是垃圾的结果所暗示的,真正的互联网精神是通过共享的网络平台实现的“精英共和制”和“无限边疆观”。当前的互联网模式已经把这种精神在很大程度上掩盖了,变成了比表层工夫――在中国干脆成了比噱头。
需要指出的是,互联网尤其是近几年来的移动互联网带来的对既有资源的更有效利用,是创造了很大价值的。但是,这是在生产力老本上进行资源优化配置,生产力基础还没有被驱动进步。网络本身有可能成为一种先进的生产力(例如,在先进的传输和终端技术的支持下,人们实现知识的快速自动吸收和分享),但这种未来场景靠目前的互联网文化很难实现――因为它意味着生产层面的技术大变革,而当前移动互联网的热门集中在对原有生产力资源的消费上,比如2013年以来最时髦的O2O模式(“线上线下联动”),相当于把人类现代社会中的生活方方面面再用互联网走一遍,按照2012年以来美国资本市场的玩法,每个领域都可以创造一个至少大中型市值水平的上市公司。 收起阅读 »
闲谈集群管理模式
Docker很火很红,简直到了没有道理的地步了。Docker为什么这么红?因为它是一种可以用来掀桌子的技术。在部署自动化这条产业上的工人和机床制造商们,看家护院的 cmdb,分布式脚本执行等所谓核心技术即便不会变成明日黄花,也会沦为二流技术。仅仅把 Docker 当成一个轻量级 vmware 来使用,是没法看穿其实质的。要理解 Docker 的意义,不能从 Docker 是什么,能够干什么说起。让我们先来回忆一下集群管理模式的发展历程,以及这些落后的模式的种种弊端。
手工管理时代
IP地址是放在 excel 表里的。管理是靠登陆跳板机,用 SSH 连接服务器。手工执行命令做新的服务器部署,已有服务器的程序版本升级,以及各种配置刷新修改的工作。
弊端不言而喻,主要有这么几点:
业务数量的增长,很快使得机器的数量超过手工操作维护的极限。无论再烂的团队,只要业务长到这个份上了,必然会出现大量的自动化工具用脚本自动化执行的方式快速地支撑业务。这个时代是一个黄金时代,运维真正长脸的时代。因为没有自动化的运维技术,业务就会遇到瓶颈。自动化技术的引入,切实地体现成了业务的收益。
这个时代的特征是两个关键的系统
效率低下了不再是主要问题,主要的弊端变为了:
这些弊端短期对业务来说并没有立竿见影的伤害,属于内伤型的。而且很多隐患即便暴露了也会流于强调纪律,强调运维意识云云。很少会有人去追究背后的运维理念的问题。结果就是大部分公司都停留在这个阶段了。毕竟运维是一个足够用即可的支撑领域。运维搞得再高科技,特高可用,未必和创业公司的成功有多少直接联系。
开发闹革命时代
伴随 DevOps 同时出现的是 infrastructure as code 的提法。简单来说就是一帮开发杀到运维领域之后,看见这些运维居然是这样去管理现网状态的。于是他们把写代码的经验带过来,将现网状态建立成模型(所谓 code),把预期的状态提交到版本控制中。就像写代码一样,去管理服务器配置。
很多后台开发主导的小创业公司直接跳过了上个时代,运维自动化体系从一开始就是基于 puppet 和 chef 来搞的。平心而论,用 puppet 的更多是缺少历史包袱,而不是因为运维问题有多复杂。很多管理的机器数量不超过十台,却在如何使用 puppet/chef 上浪费大把时间的团队也是有的。相反很多大公司因为有沉重的历史包袱,和庞大的传统运维团队,这种开发闹革命的路反而走不通。
这种做法主要是解决了脚本的管理问题,而且因为直接定义了现网状态,服务器之间的一致性也会好很多。但是光鲜亮丽的模型背后本质上还是一堆脚本来驱动的。上个时代的弊端只是经过了包装和改良,并没有办法根除。
应用预期状态到现网依靠的还是跑脚本。而且与之前不同,现在更多的是跑别人写的cookbook了,质量也是良莠不齐的。
虽然定义了预期的现网状态,但是起点不同(比如从a=>c, b=>c)需要做的升级操作可能完全是不同的。要编写一个面面俱到的升级脚本其实非常困难。
还有哪些问题?
一致性和稳定性是最大的问题。服务器开机之后,常年是不重装系统的。无数人在上面跑过脚本,执行过命令,定位过问题。服务器实际的状态是没有办法精确管控的。infrastructure as code 是一种改良,但是仍未根除这个问题。每一次在服务器上跑脚本其实就是一种赌博,因为没有两台服务器是完全一样的。在本地测试可行的脚本,未必在另外一台上不会引起问题。这不是强调一下代码里不能 rm * ,而要 rm path/* 就可以解决的问题。
版本管理其实一直是没有的。做过开发的人,可能还会用 git/svn 来作为部署的基线,基本的版本都会提交到仓库里。更多的一线运维用的还是 rsync 的模式。rsync 的意思就是要安装一个新服务器,需要找一台“与之最像”的服务器。然后把文件拷贝到新服务器上,把配置修改一下,启动完事。携程出事了,我个人猜测应该与版本管理混乱有关系。
故障替换是非常困难的。先不说故障替换,就是故障机剔除就是一个头疼的事情。比如ZooKeeper。各个客户端都硬编码三个 ip 地址。一旦其中一个 ip 挂掉了。zookeepr按照高可用协议可以保持正常,但是长期来说这个挂掉的ip还是要从各个使用方里剔除的。这个就且改了。一旦业务的高可用做得不好,需要运维来搞一些接告警之后替换故障机的事情,那就是各种脚本折腾各种配置文件的节奏了。
Docker 是如何掀桌子的
两点神论,进入到 Docker 时代之后
Docker的实质是一个真正的版本管理工具。在 Docker 之前版本管理是各种拼凑的解决方案。什么是版本,服务器是由三部分组成:版本、配置、数据。所谓版本就是操作系统,以及操作系统的配置。各种第三方包,开发给的可执行文件,和一部分配置文件。这些的集合是一个版本,其实就是一个完整的可执行环境。除此之外一般就是一个数据库,里面放了两部分内容,一部分是管理员可以从页面上修改的配置,一部分是业务数据。在 puppet 时代的版本,是一个申明文件。这个申明文件执行的时候,需要先从某个 ISO 安装出一个操作系统,然后用 apt-get/yum 从某个镜像源安装一堆系统的包,然后用 pip/bundle 安装一堆 python/ruby 语言层面的包,最后才是开发给你的 git/svn/某个不知名的tar.gz。你以为这些东西每次拼装出来的东西都是同样的版本么?其实未必。想当年某墙干掉 github 的时候,不知道多少人无法做发布了。Docker 打包出的连系统在一起的镜像,其实是对版本的最好阐述。
使用 Docker 之后不再需要修改现网的 container 了。一个 container 如果需要升级,那么就把它干掉,再把预先做好的新的镜像发布成一个新的 container 替换上去。分布式脚本执行,变成了分布式容器替换了。当然这种标准化的操作,用 mesos marathon 已经完美解决了。
使用 Docker 之后,无法再基于 IP 做管理了。倒不是给每个 container 分配一个 IP 分配不过来,而是 IP 代表的静态模型无法跟上时代了。基于 IP 管理,就意味你会基于 SSH 登陆这个 IP 来管理。这种思想从骨子里就是落后的了。进程,进程组,模块,set 这些才是管理的粒度。至于进程是跑在哪个 IP 上的哪个容器里,不再重要了。一图可以说明这个问题:
上面这个扩容的按钮点完之后有让你填 IP 吗?没有!你只需要告诉marathon,我要32个进程实例。它就会去找这些资源运行这 32 个实例。业务最终需要的是 32 个进程,而不是 32 个 IP。IP只是运行进程需要的资源而已。实际运行的时候进程可能是在一个IP上启动了32个端口,也可能是随机分配了5个IP,每个各跑了一些端口。当然这些分配都是可以通过“约束”的方式表达的。而不是让你去搞32个IP来,再跑个脚本去这些IP上部署这些进程。
The Missing Piece
拼图游戏就差最后这一块了。Docker 做为一个版本工具是绝对合格的。Marathon 以 Docker 的方式托管所有进程也是靠谱的。但是还不完整:
Docker镜像作为版本发布到现网之后是无法运行的,因为任何一个应用起码都有好几个服务要互相访问。这些硬编码在镜像里的 IP 地址换了一个环境是无法执行的。一个版本里任何配置都可以硬编码,就是 IP 地址和端口是没硬编码的。
扩容缩容可以很容易创建和销毁容器,但是引用了这个容器的服务器的其他容器怎么办呢?
发布,故障替换都是同样的问题
解决方案可以看这两张图:
方案其实非常简单。把 app1 => app2 的网络访问关系,改成 app1 =local=> haproxy =network=> haproxy =local=> app2。通过在容器本地部署 haproxy “托管所有的端口”,也就是用 haproxy 在进程之间做联线,而不是每个进程自己去负责连接网络上的其他进程。
试想一下之前是在配置文件里硬编码 10.0.0.1:3306 是某台数据库。硬编码是不对的,是要打屁股的。所以我们把硬编码的 ip 地址改成 127.0.0.1:10010。这一次我们不再硬编码任何 IP 了,我们只硬编码一个特殊的端口号。每个进程都有一堆特殊的本地端口号用于访问自己需要的上下游服务。这个端口号背后的进程到底在哪个 IP,哪个 端口,哪个 container 里执行。做为使用方不需要修改任何代码(比如兼容什么 ZooKeeper/etcd 神马的),也不用关心。甚至这个端口后面是多个远程的IP构成一个基于客户端的高可用。代理甚至还可以做一些出错换一个后端再重试的事情。
有了这种神器之后,扩容所容,发布变更,故障替换都很轻松了。容器随便新增,随便删除。网络结构变化了之后,刷新各个地方的 haproxy 配置就是了。各种灰度,各种零停机替换方案都可以搞起。
名字服务与网络
类似的方案有很多。最底层的方案是 SDN/IP 漂移,以及网络的bonding。这种方案的特点是保持 IP 地址作为最传统的名字服务,妄图延续其生命。
上层一点的方案是 DNS。再上层一些的方案是 ZooKeeper。
各种方案争的就是服务如何注册自己,如何彼此发现这个点。各种方案的优缺点可以自己去读:
最有意思的是把这种 haproxy 的方案与基于 SDN 的 IP 漂移方案做对比。haproxy 的就是替网络做应用层进程之间联线的事情,通过引入 haproxy 让这种联线更具有灵活性。 而 SDN 的方案是说,你现在的业务进程之间是通过 IP 之间静态链接的,这种连接不够灵活没关系,路由器帮你整。一个 IP 挂掉了,可以把IP漂移到另外一台机器上去继续使用。其实就是在一个场景下实现两个进程的重新联线,突破两 IP 之间静态互访的限制,给基于 IP 的部署方案续命。
两者底层的技术是相通的。所谓 IP 漂移最后靠的是现代牛逼的CPU,和软件路由技术。最后玩的都是用户态转发,dpdk神马的。所以 haproxy 慢,转发效率有问题神马的,长期来看都不会是问题。用软件来联线,是趋势。连路由器都开始这么玩了,连硬件厂商都开始卖软件了。
The Final Battle
集群管理纯粹变成进程管理,IP不再重要,状态不再重要。CMDB会变得越来越边缘化。
发布变更不再是去修改服务器,而是新建销毁容器,以及更新进程间网络联线关系。分布式作业系统会越来越少用,跳板机就更加不允许使用了。
记住“immutable servers”这个提法吧,它终将会得到历史的认可。
来源:Dockerone.io 收起阅读 »
手工管理时代
IP地址是放在 excel 表里的。管理是靠登陆跳板机,用 SSH 连接服务器。手工执行命令做新的服务器部署,已有服务器的程序版本升级,以及各种配置刷新修改的工作。
弊端不言而喻,主要有这么几点:
- 缺乏一致性,因为是手工操作所以服务器之间总是有一些差异
- 效率低下,一个人可以管理的服务器数量非常有限
- 自动化大跃进时代
业务数量的增长,很快使得机器的数量超过手工操作维护的极限。无论再烂的团队,只要业务长到这个份上了,必然会出现大量的自动化工具用脚本自动化执行的方式快速地支撑业务。这个时代是一个黄金时代,运维真正长脸的时代。因为没有自动化的运维技术,业务就会遇到瓶颈。自动化技术的引入,切实地体现成了业务的收益。
这个时代的特征是两个关键的系统
- 把本地 excel 表格里的 IP 地址用数据库的方式管理起来,称之为 CMDB
- 基于 SSH 或者 agent 的分布式脚本执行平台
效率低下了不再是主要问题,主要的弊端变为了:
- 大量的脚本,杂乱无章,内容重复,质量难以保证,最终给故障留下隐患
- 没有对现网预期状态的定义和管理,所有的现网状态都是脚本日积月累的产物,导致服务器状态漂移,产生雪花服务器(每个机器都不一样),进而给业务稳定性留下隐患
这些弊端短期对业务来说并没有立竿见影的伤害,属于内伤型的。而且很多隐患即便暴露了也会流于强调纪律,强调运维意识云云。很少会有人去追究背后的运维理念的问题。结果就是大部分公司都停留在这个阶段了。毕竟运维是一个足够用即可的支撑领域。运维搞得再高科技,特高可用,未必和创业公司的成功有多少直接联系。
开发闹革命时代
伴随 DevOps 同时出现的是 infrastructure as code 的提法。简单来说就是一帮开发杀到运维领域之后,看见这些运维居然是这样去管理现网状态的。于是他们把写代码的经验带过来,将现网状态建立成模型(所谓 code),把预期的状态提交到版本控制中。就像写代码一样,去管理服务器配置。
很多后台开发主导的小创业公司直接跳过了上个时代,运维自动化体系从一开始就是基于 puppet 和 chef 来搞的。平心而论,用 puppet 的更多是缺少历史包袱,而不是因为运维问题有多复杂。很多管理的机器数量不超过十台,却在如何使用 puppet/chef 上浪费大把时间的团队也是有的。相反很多大公司因为有沉重的历史包袱,和庞大的传统运维团队,这种开发闹革命的路反而走不通。
这种做法主要是解决了脚本的管理问题,而且因为直接定义了现网状态,服务器之间的一致性也会好很多。但是光鲜亮丽的模型背后本质上还是一堆脚本来驱动的。上个时代的弊端只是经过了包装和改良,并没有办法根除。
应用预期状态到现网依靠的还是跑脚本。而且与之前不同,现在更多的是跑别人写的cookbook了,质量也是良莠不齐的。
虽然定义了预期的现网状态,但是起点不同(比如从a=>c, b=>c)需要做的升级操作可能完全是不同的。要编写一个面面俱到的升级脚本其实非常困难。
还有哪些问题?
一致性和稳定性是最大的问题。服务器开机之后,常年是不重装系统的。无数人在上面跑过脚本,执行过命令,定位过问题。服务器实际的状态是没有办法精确管控的。infrastructure as code 是一种改良,但是仍未根除这个问题。每一次在服务器上跑脚本其实就是一种赌博,因为没有两台服务器是完全一样的。在本地测试可行的脚本,未必在另外一台上不会引起问题。这不是强调一下代码里不能 rm * ,而要 rm path/* 就可以解决的问题。
版本管理其实一直是没有的。做过开发的人,可能还会用 git/svn 来作为部署的基线,基本的版本都会提交到仓库里。更多的一线运维用的还是 rsync 的模式。rsync 的意思就是要安装一个新服务器,需要找一台“与之最像”的服务器。然后把文件拷贝到新服务器上,把配置修改一下,启动完事。携程出事了,我个人猜测应该与版本管理混乱有关系。
故障替换是非常困难的。先不说故障替换,就是故障机剔除就是一个头疼的事情。比如ZooKeeper。各个客户端都硬编码三个 ip 地址。一旦其中一个 ip 挂掉了。zookeepr按照高可用协议可以保持正常,但是长期来说这个挂掉的ip还是要从各个使用方里剔除的。这个就且改了。一旦业务的高可用做得不好,需要运维来搞一些接告警之后替换故障机的事情,那就是各种脚本折腾各种配置文件的节奏了。
Docker 是如何掀桌子的
两点神论,进入到 Docker 时代之后
- CMDB 不再至关重要了。CMDB 连同IP,以及服务器资源变成底层蓝领工人关心的问题了。上层的后台开发和业务运维不再需要也无法再以 IP 为中心的 CMDB 来管理配置。
- 分布式脚本执行平台从核心作业系统退居二线。很简单,服务器不再需要变更了,常规的上新服务器,发布新版本都不再依赖脚本在一个已有的服务器上执行去修改状态。而是创建一个新的容器。
Docker的实质是一个真正的版本管理工具。在 Docker 之前版本管理是各种拼凑的解决方案。什么是版本,服务器是由三部分组成:版本、配置、数据。所谓版本就是操作系统,以及操作系统的配置。各种第三方包,开发给的可执行文件,和一部分配置文件。这些的集合是一个版本,其实就是一个完整的可执行环境。除此之外一般就是一个数据库,里面放了两部分内容,一部分是管理员可以从页面上修改的配置,一部分是业务数据。在 puppet 时代的版本,是一个申明文件。这个申明文件执行的时候,需要先从某个 ISO 安装出一个操作系统,然后用 apt-get/yum 从某个镜像源安装一堆系统的包,然后用 pip/bundle 安装一堆 python/ruby 语言层面的包,最后才是开发给你的 git/svn/某个不知名的tar.gz。你以为这些东西每次拼装出来的东西都是同样的版本么?其实未必。想当年某墙干掉 github 的时候,不知道多少人无法做发布了。Docker 打包出的连系统在一起的镜像,其实是对版本的最好阐述。
使用 Docker 之后不再需要修改现网的 container 了。一个 container 如果需要升级,那么就把它干掉,再把预先做好的新的镜像发布成一个新的 container 替换上去。分布式脚本执行,变成了分布式容器替换了。当然这种标准化的操作,用 mesos marathon 已经完美解决了。
使用 Docker 之后,无法再基于 IP 做管理了。倒不是给每个 container 分配一个 IP 分配不过来,而是 IP 代表的静态模型无法跟上时代了。基于 IP 管理,就意味你会基于 SSH 登陆这个 IP 来管理。这种思想从骨子里就是落后的了。进程,进程组,模块,set 这些才是管理的粒度。至于进程是跑在哪个 IP 上的哪个容器里,不再重要了。一图可以说明这个问题:
上面这个扩容的按钮点完之后有让你填 IP 吗?没有!你只需要告诉marathon,我要32个进程实例。它就会去找这些资源运行这 32 个实例。业务最终需要的是 32 个进程,而不是 32 个 IP。IP只是运行进程需要的资源而已。实际运行的时候进程可能是在一个IP上启动了32个端口,也可能是随机分配了5个IP,每个各跑了一些端口。当然这些分配都是可以通过“约束”的方式表达的。而不是让你去搞32个IP来,再跑个脚本去这些IP上部署这些进程。
The Missing Piece
拼图游戏就差最后这一块了。Docker 做为一个版本工具是绝对合格的。Marathon 以 Docker 的方式托管所有进程也是靠谱的。但是还不完整:
Docker镜像作为版本发布到现网之后是无法运行的,因为任何一个应用起码都有好几个服务要互相访问。这些硬编码在镜像里的 IP 地址换了一个环境是无法执行的。一个版本里任何配置都可以硬编码,就是 IP 地址和端口是没硬编码的。
扩容缩容可以很容易创建和销毁容器,但是引用了这个容器的服务器的其他容器怎么办呢?
发布,故障替换都是同样的问题
解决方案可以看这两张图:
方案其实非常简单。把 app1 => app2 的网络访问关系,改成 app1 =local=> haproxy =network=> haproxy =local=> app2。通过在容器本地部署 haproxy “托管所有的端口”,也就是用 haproxy 在进程之间做联线,而不是每个进程自己去负责连接网络上的其他进程。
试想一下之前是在配置文件里硬编码 10.0.0.1:3306 是某台数据库。硬编码是不对的,是要打屁股的。所以我们把硬编码的 ip 地址改成 127.0.0.1:10010。这一次我们不再硬编码任何 IP 了,我们只硬编码一个特殊的端口号。每个进程都有一堆特殊的本地端口号用于访问自己需要的上下游服务。这个端口号背后的进程到底在哪个 IP,哪个 端口,哪个 container 里执行。做为使用方不需要修改任何代码(比如兼容什么 ZooKeeper/etcd 神马的),也不用关心。甚至这个端口后面是多个远程的IP构成一个基于客户端的高可用。代理甚至还可以做一些出错换一个后端再重试的事情。
有了这种神器之后,扩容所容,发布变更,故障替换都很轻松了。容器随便新增,随便删除。网络结构变化了之后,刷新各个地方的 haproxy 配置就是了。各种灰度,各种零停机替换方案都可以搞起。
名字服务与网络
类似的方案有很多。最底层的方案是 SDN/IP 漂移,以及网络的bonding。这种方案的特点是保持 IP 地址作为最传统的名字服务,妄图延续其生命。
上层一点的方案是 DNS。再上层一些的方案是 ZooKeeper。
各种方案争的就是服务如何注册自己,如何彼此发现这个点。各种方案的优缺点可以自己去读:
- SmartStack: Service Discovery in the Cloud(http://nerds.airbnb.com/smartstack-service-discovery-cloud/)
- DOCKERCON VIDEO: BUILDING A SMARTER APPLICATION STACK(https://blog.docker.com/tag/smartstack/)btw,airbnb 在 13 年就把这套方案投入生产了。
最有意思的是把这种 haproxy 的方案与基于 SDN 的 IP 漂移方案做对比。haproxy 的就是替网络做应用层进程之间联线的事情,通过引入 haproxy 让这种联线更具有灵活性。 而 SDN 的方案是说,你现在的业务进程之间是通过 IP 之间静态链接的,这种连接不够灵活没关系,路由器帮你整。一个 IP 挂掉了,可以把IP漂移到另外一台机器上去继续使用。其实就是在一个场景下实现两个进程的重新联线,突破两 IP 之间静态互访的限制,给基于 IP 的部署方案续命。
两者底层的技术是相通的。所谓 IP 漂移最后靠的是现代牛逼的CPU,和软件路由技术。最后玩的都是用户态转发,dpdk神马的。所以 haproxy 慢,转发效率有问题神马的,长期来看都不会是问题。用软件来联线,是趋势。连路由器都开始这么玩了,连硬件厂商都开始卖软件了。
The Final Battle
集群管理纯粹变成进程管理,IP不再重要,状态不再重要。CMDB会变得越来越边缘化。
发布变更不再是去修改服务器,而是新建销毁容器,以及更新进程间网络联线关系。分布式作业系统会越来越少用,跳板机就更加不允许使用了。
记住“immutable servers”这个提法吧,它终将会得到历史的认可。
来源:Dockerone.io 收起阅读 »
环信即时通讯云急招 高级Java后台工程师(薪资:20K-40K/月)
高级Java后台工程师(技术开发部)(薪资:20K-40K/月)
1. 5年以上java开发工作经验,具有服务器开发工作经验者优先;
2、深入了解java开发工具及主流开发框架,具有扎实的技术功底,熟悉主流技术架构;
3、熟悉REST架构和HTTP协议,以及Nginx等;
4、熟悉Cassandra, Kafka,Zookeeper等流行的分布式系统及其架构;
5、熟悉TCP/IP协议,熟悉socket和多线程开发,具备高访问量web开发工作经验(10W同时在线或日PV达千万);
6、逻辑思维能力强,具有团队意识;
7、熟悉linux相关开发优先考虑;
8、熟悉ruby, python, bash 等脚本语言优先考虑;
9、有开源社区经验者优先考虑;
10、全栈工程师,DevOps直接录取;
简历请发邮件:steven@easemob.com
关于环信:
环信是行业内领先的云服务提供商,国内最大的即时通讯云平台。环信上线半年内,已经完成了3轮融资。截止2015年上半年,环信已经帮助了23763个App加入社交和沟通能力,典型用户包括海豚浏览器、猎聘网、百合相亲、蜻蜓FM、汽车之家车友会、优听、节操精选、华图教育等。同时环信SDK覆盖用户高达2.51亿,日均消息量过亿,是经过真实亿级用户考验的即时通讯云平台。 收起阅读 »
1. 5年以上java开发工作经验,具有服务器开发工作经验者优先;
2、深入了解java开发工具及主流开发框架,具有扎实的技术功底,熟悉主流技术架构;
3、熟悉REST架构和HTTP协议,以及Nginx等;
4、熟悉Cassandra, Kafka,Zookeeper等流行的分布式系统及其架构;
5、熟悉TCP/IP协议,熟悉socket和多线程开发,具备高访问量web开发工作经验(10W同时在线或日PV达千万);
6、逻辑思维能力强,具有团队意识;
7、熟悉linux相关开发优先考虑;
8、熟悉ruby, python, bash 等脚本语言优先考虑;
9、有开源社区经验者优先考虑;
10、全栈工程师,DevOps直接录取;
简历请发邮件:steven@easemob.com
关于环信:
环信是行业内领先的云服务提供商,国内最大的即时通讯云平台。环信上线半年内,已经完成了3轮融资。截止2015年上半年,环信已经帮助了23763个App加入社交和沟通能力,典型用户包括海豚浏览器、猎聘网、百合相亲、蜻蜓FM、汽车之家车友会、优听、节操精选、华图教育等。同时环信SDK覆盖用户高达2.51亿,日均消息量过亿,是经过真实亿级用户考验的即时通讯云平台。 收起阅读 »
环信即时通讯云急招高级运维开发工程师(薪资:20K-40K/月)
高级运维开发工程师(技术开发部) (薪资:20K-40K/月)
1、精通Linux以及主要Unix系统及原理,了解网络基本技术,熟悉TCP/IP协议工作原理;
2、熟悉nginx, tomcat, redis, cassandra, zookeeper等技术的原理,优化,排错;
3、熟悉shell, perl, python, java, php, erlang, C/C++等开发语言一种以上;
4、熟悉大型网站架构及优化,熟悉分布式系统,大型数据库,缓存,队列等技术原理;
5、责任心强,积极沟通,热爱分享;
6、有开源社区/项目/大型互联网公司经验者优先考虑;
7、全栈工程师,DevOps直接录取;
8、最少2年相关工作经验。
简历请发邮件:steven@easemob.com
关于环信:
环信是行业内领先的云服务提供商,国内最大的即时通讯云平台。环信上线半年内,已经完成了3轮融资。截止2015年上半年,环信已经帮助了23763个App加入社交和沟通能力,典型用户包括海豚浏览器、猎聘网、百合相亲、蜻蜓FM、汽车之家车友会、优听、节操精选、华图教育等。同时环信SDK覆盖用户高达2.51亿,日均消息量过亿,是经过真实亿级用户考验的即时通讯云平台。 收起阅读 »
1、精通Linux以及主要Unix系统及原理,了解网络基本技术,熟悉TCP/IP协议工作原理;
2、熟悉nginx, tomcat, redis, cassandra, zookeeper等技术的原理,优化,排错;
3、熟悉shell, perl, python, java, php, erlang, C/C++等开发语言一种以上;
4、熟悉大型网站架构及优化,熟悉分布式系统,大型数据库,缓存,队列等技术原理;
5、责任心强,积极沟通,热爱分享;
6、有开源社区/项目/大型互联网公司经验者优先考虑;
7、全栈工程师,DevOps直接录取;
8、最少2年相关工作经验。
简历请发邮件:steven@easemob.com
关于环信:
环信是行业内领先的云服务提供商,国内最大的即时通讯云平台。环信上线半年内,已经完成了3轮融资。截止2015年上半年,环信已经帮助了23763个App加入社交和沟通能力,典型用户包括海豚浏览器、猎聘网、百合相亲、蜻蜓FM、汽车之家车友会、优听、节操精选、华图教育等。同时环信SDK覆盖用户高达2.51亿,日均消息量过亿,是经过真实亿级用户考验的即时通讯云平台。 收起阅读 »
环信即时通讯云急招前端工程师(薪资:20K-40K/月)
【前端工程师】
您的责任:
环信web IM SDK 等的开发设计,环信业务后台的开发设计
我们的要求:
1.3年web前端经验,熟悉流行的前端技术, 包括但不限于bootstrap, html5, css3, saas, less, jquery, bower, grunt
2.熟悉AJAX, REST等原理和使用方式
3.熟悉HTTP, WebSocket, Spdy等协议
4.熟悉Haml, Jade, Slim等模板语言优先考虑
5.有设计美感者优先考虑
6.深刻理解Web标准,对可用性. 可访问性等相关知识有实际的了解和实践经验;
7.熟悉ruby, python, bash, nodejs 等脚本语言优先考虑;
8.有开源社区经验者优先考虑
9.全栈工程师,DevOps直接录取
简历请发邮件:steven@easemob.com
关于环信:
环信是行业内领先的云服务提供商,国内最大的即时通讯云平台。环信上线半年内,已经完成了3轮融资。截止2015年上半年,环信已经帮助了23763个App加入社交和沟通能力,典型用户包括海豚浏览器、猎聘网、百合相亲、蜻蜓FM、汽车之家车友会、优听、节操精选、华图教育等。同时环信SDK覆盖用户高达2.51亿,日均消息量过亿,是经过真实亿级用户考验的即时通讯云平台。
收起阅读 »
您的责任:
环信web IM SDK 等的开发设计,环信业务后台的开发设计
我们的要求:
1.3年web前端经验,熟悉流行的前端技术, 包括但不限于bootstrap, html5, css3, saas, less, jquery, bower, grunt
2.熟悉AJAX, REST等原理和使用方式
3.熟悉HTTP, WebSocket, Spdy等协议
4.熟悉Haml, Jade, Slim等模板语言优先考虑
5.有设计美感者优先考虑
6.深刻理解Web标准,对可用性. 可访问性等相关知识有实际的了解和实践经验;
7.熟悉ruby, python, bash, nodejs 等脚本语言优先考虑;
8.有开源社区经验者优先考虑
9.全栈工程师,DevOps直接录取
简历请发邮件:steven@easemob.com
关于环信:
环信是行业内领先的云服务提供商,国内最大的即时通讯云平台。环信上线半年内,已经完成了3轮融资。截止2015年上半年,环信已经帮助了23763个App加入社交和沟通能力,典型用户包括海豚浏览器、猎聘网、百合相亲、蜻蜓FM、汽车之家车友会、优听、节操精选、华图教育等。同时环信SDK覆盖用户高达2.51亿,日均消息量过亿,是经过真实亿级用户考验的即时通讯云平台。
收起阅读 »
小窍门: @用户,被@方可以收到提示
直接@就可以了, 新编辑器里面, 回复暂时不能有下拉提示, 评论是有用户提示的.
不信你可以试试
P2P实时音视频之NAT穿越
在P2P实时音视频领域,NAT穿越是一个非常重要的技术。NAT穿越技术使得客户端和客户端直接进行通讯,从而减少了端到端的延迟,并大大减轻了服务器的压力,降低成本。
NAT是什么
NAT的全称Network Address Translation,通常指的是把内网地址转换成外网地址。一般家用的无线路由器就用到了NAT技术。NAT技术的出现是为了解决IPv4地址不够的问题,而且还能够避免来自网络外部的攻击,隐藏和保护网络内部的计算机。凡事有利必有弊,NAT同样带来了新的问题。
NAT工作原理
我们先看一下NAT的工作过程
NAT维护一个地址映射表,记录内容为内网主机地址iAddr、映射地址eAddr和外网主机地址hAddr,表初始为空
内网主机主机A发送数据包给服务器A,10.0.1.10:1111 -> 203.22.22.22:6000;
NAT在映射表里没找到源地址等于10.0.1.10:1111的记录,于是新建一条记录1,分配外网端口2000
NAT修改数据包的源地址再发到外网,202.11.11.11:2000 -> 203.22.22.22:6000;
后续所有源地址为10.0.1.10:1111,目标地址为203.22.22.22:6000都做同样的修改
服务器A发送数据包回给内网主机A,203.22.22.22:6000 -> 202.11.11.11:2000
NAT发现外网地址202.11.11.11:2000映射的内网地址为10.0.1.10:1111
NAT修改数据包的目的地址再发到内网,203.22.22.22:6000 -> 10.0.1.10:1111
内网主机B和服务器B通讯的过程也类似A,只是分配的外网端口是3000
从上面NAT的工作过程可以看出,NAT通过修改数据包的源地址或目的地址来实现地址映射的。NAT修改数据包对内网主机是透明的,不需要内网主机做任何配置,方便简单。
NAT工作原理可以总结为:
只有内网主机主动向外网发送数据,外网才有可能发送数据给内网主机
内网发送到外网的数据包会被修改源地址,外网发送给内网的数据包会被修改目的地址
很显然,第1条原理保护了内网主机免受外网的攻击,但却违背了网络端到端的设计原则。如果两台主机在不同的NAT后面,是没有办法穿越NAT直接端到端(P2P))通讯的。幸运的是,在大部分情况下,我们可以在服务器的协助下实现NAT穿越。
NAT类型
在讲NAT穿越之前,我们先来分析NAT的类型。由于没有强制性的NAT标准,在实际应用中NAT有多种类型。根据内网地址到外网地址的映射是1对1,还是1对多,NAT可以分成两大类:Cone NAT(锥型)和 Symmetric NAT(对称型)
从图中的淡紫色形状应该可以看出来它们名字的来历(呕心沥血独家原创图)。锥型NAT把一个内网地址固定的转换成一个外网地址,即1对1映射;对称型NAT的一个内网地址可以转换成多个外网地址,即1对多映射。从锥型NAT和对称型NAT的定义我们可以推测出他们的映射表内容。
锥型映射表应该是这样的:
对称型映射表应该是这样的:
Cone NAT子类型
锥型NAT还可以再继续细分类型。外网主机发送给内网主机的数据包在通过NAT时,NAT会根据映射表的外网主机地址限制条件来允许或限制数据包通过。根据这个限制条件,锥型NAT还可以分成三种子类型:
Full-cone NAT,全锥型
一旦某内网地址向外网发送过数据包,NAT允许任意外网地址发送数据给此内网地址。
(Address)-restricted-cone NAT,(地址)限制锥型
一旦某内网地址向某外网主机发送过数据包,NAT允许此外网主机发送数据给此内网地址。换句话说,只限制ip,不限制端口。
Port-restricted cone NAT,端口限制锥型
只有从内网地址发送过的外网地址,NAT才允许此外网地址发送数据给此内网地址。换句话说,同时限制ip和端口。
穿越NAT
通过上面对NAT的分析可以看出,在不同NAT后面的两个客户端A和B,如果知道对方的NAT映射后的外网地址,就有可能直接发送UDP包给对方外网地址进行通讯。但是这里有一个问题,客户端不能直接获取自身的NAT外网地址,解决的办法就是引入一个服务器S来协助客户端获取自身的外网地址。NAT的类型有多种,类型两两组合有很多种,不是每种组合都可以被穿越的,我们来分析两个典型的组合。
锥型 vs 锥型
端口限制锥型 vs 对称型
A发送数据包给eB1,因为eB1只接受来自S的数据,所以A的数据被NATB丢弃
B通过发送数据包给eA,因为eA是新的目标地址,NATB 创建新的映射地址eB2,而eA只接受来自S和eB1的数据,所以B的数据被NATA丢弃,无法建立P2P通道
这里就不一一分析其他组合,各位看官可以自行分析,这里直接给出结论:
现实中的NAT
在穿越NAT的结论里,只有两种组合不能穿越,即对称型vs对称型、端口限制锥型vs对称型,占比并不高,看起来结论还不错。但是,理论是美好的,现实是残酷的,生活中对称型NAT的数量并不少。只要是大型组织的网络,一般都采用对称型NAT,因为这类NAT安全性最好。我们团队曾经对常用的网络做过调查研究,以下是调研结果:
有公网IP的宽带:比如联通的ADSL,这类宽带会给每个用户分配一个公网IP,所以其NAT类型取决于用户所选用的路由器,大部分家用路由器都是端口限制锥型NAT;
无公网IP的宽带:比如宽带通,这类宽带给用户分配的是局域网IP,连接公网的NAT是运营商的,一般都是对称型NAT;
移动互联网:跟“无公网IP的宽带”类似,分配给手机的是局域网IP,出口基本都是对称型NAT;
大公司路由器:大部分都把路由器配置成对称型NAT;
比较可惜的是移动互联网也是对称型NAT,也就是说,如果通讯双方都走3G或4G的话,是很难直接P2P通讯的。我们的产品可以穿越部分对称型NAT,当碰到无法穿越的NAT时,为用户提供relay服务,保证接通率。
奇葩的NAT
我们现在知道NAT分为1种对称型和3种锥型,那还有没有其他类型的NAT呢?答案是YES。这个NAT各位看官应该并不陌生,它就是大名鼎鼎的netfilter/iptables。大家接触最多的iptables,是运行在ring3层用户态的配置程序,而运行在ring0内核态的netfilter才是真正实现NAT功能的程序。在大部分情况下,netfilter表现出来的是人见人爱的锥型NAT,但是在某种条件刺激下,它就华丽丽地变身成高贵冷漠的对称型NAT!
先上图:
在穿越时,假如右边B发给A的包比左边A发给B的包先到达netfilter,netfilter会用之前的映射地址eB把B的包发出去,这时候netfilter表现出来的是锥型NAT,穿越成功。反过来,假如A发给B的包先到达netfilter,那么B发给A的包就会被netfilter映射成新的地址eB’,这时候netfilter表现出来的是对称型NAT,导致穿越失败。见下图。
netfilter不分内网和外网,它会跟踪内网和外网所有协议的连接(conntrack),包括tcp和udp。当外网的数据先到达netfilter时,netfilter创建一条conntrack,内网的数据后到达netfilter,netfilter发现conntrack1已经占用了端口,就会选择另外一个外网端口作为映射端口。看官如果想了解详细情况,请阅读博大精深的netfilter源码,这里提示一下,看get_unique_tuple函数就可以了。虽然netfilter很奇葩,但我们的产品依然能够轻松的穿越它。作者介绍:
符宁,环信音视频team leader,在音视频客户端/服务器领域拥有多年设计、开发和管理经验。
联系方式:simon.fu@easemob.com 收起阅读 »
NAT是什么
NAT的全称Network Address Translation,通常指的是把内网地址转换成外网地址。一般家用的无线路由器就用到了NAT技术。NAT技术的出现是为了解决IPv4地址不够的问题,而且还能够避免来自网络外部的攻击,隐藏和保护网络内部的计算机。凡事有利必有弊,NAT同样带来了新的问题。
NAT工作原理
我们先看一下NAT的工作过程
NAT维护一个地址映射表,记录内容为内网主机地址iAddr、映射地址eAddr和外网主机地址hAddr,表初始为空
内网主机主机A发送数据包给服务器A,10.0.1.10:1111 -> 203.22.22.22:6000;
NAT在映射表里没找到源地址等于10.0.1.10:1111的记录,于是新建一条记录1,分配外网端口2000
NAT修改数据包的源地址再发到外网,202.11.11.11:2000 -> 203.22.22.22:6000;
后续所有源地址为10.0.1.10:1111,目标地址为203.22.22.22:6000都做同样的修改
服务器A发送数据包回给内网主机A,203.22.22.22:6000 -> 202.11.11.11:2000
NAT发现外网地址202.11.11.11:2000映射的内网地址为10.0.1.10:1111
NAT修改数据包的目的地址再发到内网,203.22.22.22:6000 -> 10.0.1.10:1111
内网主机B和服务器B通讯的过程也类似A,只是分配的外网端口是3000
从上面NAT的工作过程可以看出,NAT通过修改数据包的源地址或目的地址来实现地址映射的。NAT修改数据包对内网主机是透明的,不需要内网主机做任何配置,方便简单。
NAT工作原理可以总结为:
只有内网主机主动向外网发送数据,外网才有可能发送数据给内网主机
内网发送到外网的数据包会被修改源地址,外网发送给内网的数据包会被修改目的地址
很显然,第1条原理保护了内网主机免受外网的攻击,但却违背了网络端到端的设计原则。如果两台主机在不同的NAT后面,是没有办法穿越NAT直接端到端(P2P))通讯的。幸运的是,在大部分情况下,我们可以在服务器的协助下实现NAT穿越。
NAT类型
在讲NAT穿越之前,我们先来分析NAT的类型。由于没有强制性的NAT标准,在实际应用中NAT有多种类型。根据内网地址到外网地址的映射是1对1,还是1对多,NAT可以分成两大类:Cone NAT(锥型)和 Symmetric NAT(对称型)
从图中的淡紫色形状应该可以看出来它们名字的来历(呕心沥血独家原创图)。锥型NAT把一个内网地址固定的转换成一个外网地址,即1对1映射;对称型NAT的一个内网地址可以转换成多个外网地址,即1对多映射。从锥型NAT和对称型NAT的定义我们可以推测出他们的映射表内容。
锥型映射表应该是这样的:
对称型映射表应该是这样的:
Cone NAT子类型
锥型NAT还可以再继续细分类型。外网主机发送给内网主机的数据包在通过NAT时,NAT会根据映射表的外网主机地址限制条件来允许或限制数据包通过。根据这个限制条件,锥型NAT还可以分成三种子类型:
Full-cone NAT,全锥型
一旦某内网地址向外网发送过数据包,NAT允许任意外网地址发送数据给此内网地址。
(Address)-restricted-cone NAT,(地址)限制锥型
一旦某内网地址向某外网主机发送过数据包,NAT允许此外网主机发送数据给此内网地址。换句话说,只限制ip,不限制端口。
Port-restricted cone NAT,端口限制锥型
只有从内网地址发送过的外网地址,NAT才允许此外网地址发送数据给此内网地址。换句话说,同时限制ip和端口。
穿越NAT
通过上面对NAT的分析可以看出,在不同NAT后面的两个客户端A和B,如果知道对方的NAT映射后的外网地址,就有可能直接发送UDP包给对方外网地址进行通讯。但是这里有一个问题,客户端不能直接获取自身的NAT外网地址,解决的办法就是引入一个服务器S来协助客户端获取自身的外网地址。NAT的类型有多种,类型两两组合有很多种,不是每种组合都可以被穿越的,我们来分析两个典型的组合。
锥型 vs 锥型
- A发送数据包给S询问自身地址,S把A的外网地址eA返回给A
- B发送数据包给S询问自身地址,S把B的外网地址eB返回给B
- S把B的外网地址eB发送给A
- S把A的外网地址eA发送给B
- A发送数据包给eB,B发送数据包给eA,建立P2P通道
端口限制锥型 vs 对称型
- A发送数据包给S询问自身地址,S把A的外网地址eA返回给A
- B发送数据包给S询问自身地址,S把B的外网地址eB1返回给B
- S把B的外网地址eB1发送给A
- S把A的外网地址eA发送给B
A发送数据包给eB1,因为eB1只接受来自S的数据,所以A的数据被NATB丢弃
B通过发送数据包给eA,因为eA是新的目标地址,NATB 创建新的映射地址eB2,而eA只接受来自S和eB1的数据,所以B的数据被NATA丢弃,无法建立P2P通道
这里就不一一分析其他组合,各位看官可以自行分析,这里直接给出结论:
现实中的NAT
在穿越NAT的结论里,只有两种组合不能穿越,即对称型vs对称型、端口限制锥型vs对称型,占比并不高,看起来结论还不错。但是,理论是美好的,现实是残酷的,生活中对称型NAT的数量并不少。只要是大型组织的网络,一般都采用对称型NAT,因为这类NAT安全性最好。我们团队曾经对常用的网络做过调查研究,以下是调研结果:
有公网IP的宽带:比如联通的ADSL,这类宽带会给每个用户分配一个公网IP,所以其NAT类型取决于用户所选用的路由器,大部分家用路由器都是端口限制锥型NAT;
无公网IP的宽带:比如宽带通,这类宽带给用户分配的是局域网IP,连接公网的NAT是运营商的,一般都是对称型NAT;
移动互联网:跟“无公网IP的宽带”类似,分配给手机的是局域网IP,出口基本都是对称型NAT;
大公司路由器:大部分都把路由器配置成对称型NAT;
比较可惜的是移动互联网也是对称型NAT,也就是说,如果通讯双方都走3G或4G的话,是很难直接P2P通讯的。我们的产品可以穿越部分对称型NAT,当碰到无法穿越的NAT时,为用户提供relay服务,保证接通率。
奇葩的NAT
我们现在知道NAT分为1种对称型和3种锥型,那还有没有其他类型的NAT呢?答案是YES。这个NAT各位看官应该并不陌生,它就是大名鼎鼎的netfilter/iptables。大家接触最多的iptables,是运行在ring3层用户态的配置程序,而运行在ring0内核态的netfilter才是真正实现NAT功能的程序。在大部分情况下,netfilter表现出来的是人见人爱的锥型NAT,但是在某种条件刺激下,它就华丽丽地变身成高贵冷漠的对称型NAT!
先上图:
在穿越时,假如右边B发给A的包比左边A发给B的包先到达netfilter,netfilter会用之前的映射地址eB把B的包发出去,这时候netfilter表现出来的是锥型NAT,穿越成功。反过来,假如A发给B的包先到达netfilter,那么B发给A的包就会被netfilter映射成新的地址eB’,这时候netfilter表现出来的是对称型NAT,导致穿越失败。见下图。
netfilter不分内网和外网,它会跟踪内网和外网所有协议的连接(conntrack),包括tcp和udp。当外网的数据先到达netfilter时,netfilter创建一条conntrack,内网的数据后到达netfilter,netfilter发现conntrack1已经占用了端口,就会选择另外一个外网端口作为映射端口。看官如果想了解详细情况,请阅读博大精深的netfilter源码,这里提示一下,看get_unique_tuple函数就可以了。虽然netfilter很奇葩,但我们的产品依然能够轻松的穿越它。作者介绍:
符宁,环信音视频team leader,在音视频客户端/服务器领域拥有多年设计、开发和管理经验。
联系方式:simon.fu@easemob.com 收起阅读 »
实时网络音视频通讯qos的一种解决方案
一、前言
随着移动互联网的快速发展以及智能终端性能的逐步提高,智能终端间进行实时音视频通讯成为未来移动互联网
发展的一个重要方向。那么如何保证智能终端之间实时音视频通讯的服务质量成为一个必须加以重视的问题。实时音视频通讯包括采集、编码、网络传输、解码、播放等环节,其中采集、编解码和播放是不受网络条件影响的,只受限于编解码算法,播放策略等因素,网络传输的丢包、抖动和乱序对qos的影响最为重大,因此本文介绍的qos解决方案
要解决的是网络传输丢包、抖动和乱序因素对服务质量的不好影响。
二、发送端
对于实时音视频通讯,常采用UDP协议来传输多媒体数据,本文是采用基于udp的rtp协议来传输音视频数据。对
于不同格式的编码数据,会有不同的rtp打包协议,比如对于H.264视频数据,文档rfc3984对NAL U的rtp打包封装进行了规范,详情请参考该文档。对于视频数据的打包封装,因为一帧视频数据的数据长度可能大于MTU,所以相关的打包协议都会规定将长度大于MTU的帧进行切割,分块封装到多个rtp包进行传输。为了避免丢包、抖动和乱序对服务质量的影响,本方案在发送端和接收端各建立了节点数相等的一段循环buffer,用于缓存发送端数据和接收端数据。
发送端在发送数据的时候,某个rtp包的seq为send_seq,发送端把这个包通过udp socket发送出去的同时,把这
个rtp包的数据拷贝到send_seq对应节点的buffer中去,以便这个rtp包接收方没收到时,发送方还能重发这个rtp包。
这里要注意的一点是,发送端和接收端的循环buffer节点数要能被65536整除,这样rtp seq增加到最大值65535时对应最后一个节点,下一个rtp包的seq为0正好对应上第一个节点,避免rtp seq掉头时出现漏洞。
三、接收端
和发送端类似,接收端也开辟了一段节点数能被65536整除的循环buffer,用于缓存接收到的rtp包。接收端收到rtp包时,需要去解析rtp包头,取出接收到的rtp包的seq,对应下图中的received_seq。
当收到第一个包时,start_seq和end_seq都被设置为received_seq,并把收到的rtp包送到解码单元。后面收到
rtp包时,有两个工作要做,一个工作是接收的模块将接收到的rtp包拷贝到received_seq指向的节点的buffer,并将这个节点的数据flag(用于标记该节点是否填充了数据)设置为true,同时要根据start_seq、end_seq和received_seq的关系来决定要不要将end_seq更新为received_seq的值,如果received_seq对应的包本来应该end_seq对应的包之前到达,则不更新end_seq的值,否则就更新。另一个工作是要每过一段时间都要去扫描start_seq到end_seq对应的每个节点,首先,若当前时间和start_seq对应的数据到达时间的差值超过一定阈值(比如500ms),则将start_seq和end_seq之间的每个节点的数据全部丢弃,将每个节点的数据flag设置为false,更新start_seq为end_seq。其次,若start_seq对应的节点的下一个节点的数据falg为true,则将该节点的数据送到解码单元,同时将start_seq更新为该节点的seq,并将该节点的数据flag设置为false;若flag为false,且当前时间和start_seq对应的数据到达时间的差值超过一定阈值(比如50ms),则将该节点的seq(lost_seq)发送给发送端,请求发送端将seq对应的rtp数据再发一遍。这样,当有些包很久(大于500ms)都没收到,就认为它来不了,直接将它们丢弃;有些包短时间(小于50ms)没来,则向发送端发送重传请求,请求发送端再发一次该包,试图能补上这些包。
四、结果分析
没加qos模块时,两个手机视频通信在有丢包情况下回出现视频帧不完整,播放出现马赛克的现象,加上qos模块后,视频播放流畅,效果大为改善。同时我们为了测试该方案的作用,在发送端人为地分别丢弃10%和20%的视频rtp包,接收端解码播放效果良好,没有出现马赛克现象。
作者介绍:
彭祖元,环信资深音视频技术专家。拥有多年音视频编解码开发经验,在Android,iOS等平台音视频采集,编码,传输,解码,播放等方面有着丰富的经验,熟悉流媒体服务器开发。 收起阅读 »
随着移动互联网的快速发展以及智能终端性能的逐步提高,智能终端间进行实时音视频通讯成为未来移动互联网
发展的一个重要方向。那么如何保证智能终端之间实时音视频通讯的服务质量成为一个必须加以重视的问题。实时音视频通讯包括采集、编码、网络传输、解码、播放等环节,其中采集、编解码和播放是不受网络条件影响的,只受限于编解码算法,播放策略等因素,网络传输的丢包、抖动和乱序对qos的影响最为重大,因此本文介绍的qos解决方案
要解决的是网络传输丢包、抖动和乱序因素对服务质量的不好影响。
二、发送端
对于实时音视频通讯,常采用UDP协议来传输多媒体数据,本文是采用基于udp的rtp协议来传输音视频数据。对
于不同格式的编码数据,会有不同的rtp打包协议,比如对于H.264视频数据,文档rfc3984对NAL U的rtp打包封装进行了规范,详情请参考该文档。对于视频数据的打包封装,因为一帧视频数据的数据长度可能大于MTU,所以相关的打包协议都会规定将长度大于MTU的帧进行切割,分块封装到多个rtp包进行传输。为了避免丢包、抖动和乱序对服务质量的影响,本方案在发送端和接收端各建立了节点数相等的一段循环buffer,用于缓存发送端数据和接收端数据。
发送端在发送数据的时候,某个rtp包的seq为send_seq,发送端把这个包通过udp socket发送出去的同时,把这
个rtp包的数据拷贝到send_seq对应节点的buffer中去,以便这个rtp包接收方没收到时,发送方还能重发这个rtp包。
这里要注意的一点是,发送端和接收端的循环buffer节点数要能被65536整除,这样rtp seq增加到最大值65535时对应最后一个节点,下一个rtp包的seq为0正好对应上第一个节点,避免rtp seq掉头时出现漏洞。
三、接收端
和发送端类似,接收端也开辟了一段节点数能被65536整除的循环buffer,用于缓存接收到的rtp包。接收端收到rtp包时,需要去解析rtp包头,取出接收到的rtp包的seq,对应下图中的received_seq。
当收到第一个包时,start_seq和end_seq都被设置为received_seq,并把收到的rtp包送到解码单元。后面收到
rtp包时,有两个工作要做,一个工作是接收的模块将接收到的rtp包拷贝到received_seq指向的节点的buffer,并将这个节点的数据flag(用于标记该节点是否填充了数据)设置为true,同时要根据start_seq、end_seq和received_seq的关系来决定要不要将end_seq更新为received_seq的值,如果received_seq对应的包本来应该end_seq对应的包之前到达,则不更新end_seq的值,否则就更新。另一个工作是要每过一段时间都要去扫描start_seq到end_seq对应的每个节点,首先,若当前时间和start_seq对应的数据到达时间的差值超过一定阈值(比如500ms),则将start_seq和end_seq之间的每个节点的数据全部丢弃,将每个节点的数据flag设置为false,更新start_seq为end_seq。其次,若start_seq对应的节点的下一个节点的数据falg为true,则将该节点的数据送到解码单元,同时将start_seq更新为该节点的seq,并将该节点的数据flag设置为false;若flag为false,且当前时间和start_seq对应的数据到达时间的差值超过一定阈值(比如50ms),则将该节点的seq(lost_seq)发送给发送端,请求发送端将seq对应的rtp数据再发一遍。这样,当有些包很久(大于500ms)都没收到,就认为它来不了,直接将它们丢弃;有些包短时间(小于50ms)没来,则向发送端发送重传请求,请求发送端再发一次该包,试图能补上这些包。
四、结果分析
没加qos模块时,两个手机视频通信在有丢包情况下回出现视频帧不完整,播放出现马赛克的现象,加上qos模块后,视频播放流畅,效果大为改善。同时我们为了测试该方案的作用,在发送端人为地分别丢弃10%和20%的视频rtp包,接收端解码播放效果良好,没有出现马赛克现象。
作者介绍:
彭祖元,环信资深音视频技术专家。拥有多年音视频编解码开发经验,在Android,iOS等平台音视频采集,编码,传输,解码,播放等方面有着丰富的经验,熟悉流媒体服务器开发。 收起阅读 »
Big Data Processing at Easemob – Big Data Processing Platform
Easemob plug-in and its SDK on mobile text and voice messaging communication service become extremely popular in China recently and are used by over 13,000 mobile Apps with millions of registered users in total. One of the driving design principles of Easemob is that better quality of social activities means a better App with a more engaging user experience. With regards to this, Easemob decided to invest into Big Data technology for app developers in the form of an interactive web portal that presents business analytics related to their plug-ins.
Figure 1 Easemob Social Activity Big Data Analysis Application
Most data on mobile Apps with Easemob’s plug-in is consumed in real-time. Easemob recognized that providing results to app developers in closer to real time could help generate more engaging social activities by allowing more dynamic interaction with users. The finer grained details of a real-time system also provide much more information than a delayed multiple hours aggregate. Hence, the primary goal of Easemob's big data analysis system was to provide analytic results to app developers with minutes or even seconds of latency.
To achieve such target, we actually don’t have many options. The original idea is to use apache hadoop to complete the task. Hadoop MapReduce/Hive based solution certainly can handle such scale required by Easemob with high availability and accuracy. However, its batch processing logic based upon MapReduce framework means probably it won’t deliver results in real time. Hence Easemob will not be able to provide a specific SLA for its app developers if such idea is implemented.
With some careful research, we came up with a solution that relies on Spark integrated with Cassandra to deliver results. Easemob has been already using Cassandra for its messaging system and the nosql big table database has been proved to be able to scale and meet the requirement of Easemob app clients. In summary, we chose Spark and Cassandra for the new big data processing system based upon high performance Spark can provide and high availability and scalability from Cassandra. Some details about Spark and Cassandra are discussed in a later section of this article.
About Spark
Spark is an open source cluster computing system developed in the UC Berkeley AMP Lab. Some benchmark results done by Berkeley shows the results of comparison of performance between spark/shark and hadoop/hive combination.
Figure 2 Benchmark of Spark/Shark and Hadoop/Hive
It is not surprising to see Spark beats Hadoop easily. This is because spark provides primitives for in-memory cluster computing while MapReduce processing can only occur on data stored either in a file system (unstructured) or in a physical database (structured). It can not avoid I/O bottleneck between the individual jobs of an iterative MapReduce workflow. Spark does not have this issue.
Apart from its lighting speed, spark provides a few interesting features which will be needed by Easemob’s big data processing system sooner or later. Shark for Spark is like Hive for Hadoop. It actually builds directly on the Apache Hive codebase. However, it has a “Spark” heart. Underneath, it is using Spark execution engine for its query processing so it appears much faster than Hive. Spark also directly supports real time processing through its streaming framework on which Easemob’s social activity big data engine will be built. The apache machine learning library is tightly integrated into Spark. Interactive data mining is a supported feature by Spark. It is absolutely vital feature for Easemob ‘s future success of its social activity big data analysis system.
As for Cassandra, as mentioned previously in this article, it has been selected by Easemob for its messaging system. Today in Easemob, more than 100 millions messages a day are being handled by Cassandra database across multiple data centers. The big table database has been proven to be able to scale. The figure 3 as below shows what the big data processing platform for Easemob’s future social activity analysis engine looks like.
Figure 3 - Platform for Easemob Social Activity Big Data Analysis
There is one thing I particularly like about Spark. It makes use of the Scala language, which allows distributed datasets to be manipulated like local collections. It combines Object-oriented and functional programming perfectly. It has a very strong static type system. It overcomes many shortcomings of Java language but however, it is still compiled to java bytecode hence it runs on java virtual machine. The Interoperability of the two languages is maintained well. It is perfectly integrated into Spark.
In the big data world, Spark is relatively new technology, however, it is already used by many companies in production such as Yahoo, Sohu and Ebay etc. The long list can be viewed in Apache website.
About the author
Zhi Huang is the Head of Big Data Analytics Department in easemob.com. He can be reached at zhi.huang@easemob.com
Today, in China, our mobile text and voice messaging communication service serves more than 13,000 mobile apps and hundreds of millions of users. We are currently expanding our business globally. If you are app developer, you are more than welcome to use our free service at any time.
作者: 黄智 收起阅读 »
Figure 1 Easemob Social Activity Big Data Analysis Application
Most data on mobile Apps with Easemob’s plug-in is consumed in real-time. Easemob recognized that providing results to app developers in closer to real time could help generate more engaging social activities by allowing more dynamic interaction with users. The finer grained details of a real-time system also provide much more information than a delayed multiple hours aggregate. Hence, the primary goal of Easemob's big data analysis system was to provide analytic results to app developers with minutes or even seconds of latency.
To achieve such target, we actually don’t have many options. The original idea is to use apache hadoop to complete the task. Hadoop MapReduce/Hive based solution certainly can handle such scale required by Easemob with high availability and accuracy. However, its batch processing logic based upon MapReduce framework means probably it won’t deliver results in real time. Hence Easemob will not be able to provide a specific SLA for its app developers if such idea is implemented.
With some careful research, we came up with a solution that relies on Spark integrated with Cassandra to deliver results. Easemob has been already using Cassandra for its messaging system and the nosql big table database has been proved to be able to scale and meet the requirement of Easemob app clients. In summary, we chose Spark and Cassandra for the new big data processing system based upon high performance Spark can provide and high availability and scalability from Cassandra. Some details about Spark and Cassandra are discussed in a later section of this article.
About Spark
Spark is an open source cluster computing system developed in the UC Berkeley AMP Lab. Some benchmark results done by Berkeley shows the results of comparison of performance between spark/shark and hadoop/hive combination.
Figure 2 Benchmark of Spark/Shark and Hadoop/Hive
It is not surprising to see Spark beats Hadoop easily. This is because spark provides primitives for in-memory cluster computing while MapReduce processing can only occur on data stored either in a file system (unstructured) or in a physical database (structured). It can not avoid I/O bottleneck between the individual jobs of an iterative MapReduce workflow. Spark does not have this issue.
Apart from its lighting speed, spark provides a few interesting features which will be needed by Easemob’s big data processing system sooner or later. Shark for Spark is like Hive for Hadoop. It actually builds directly on the Apache Hive codebase. However, it has a “Spark” heart. Underneath, it is using Spark execution engine for its query processing so it appears much faster than Hive. Spark also directly supports real time processing through its streaming framework on which Easemob’s social activity big data engine will be built. The apache machine learning library is tightly integrated into Spark. Interactive data mining is a supported feature by Spark. It is absolutely vital feature for Easemob ‘s future success of its social activity big data analysis system.
As for Cassandra, as mentioned previously in this article, it has been selected by Easemob for its messaging system. Today in Easemob, more than 100 millions messages a day are being handled by Cassandra database across multiple data centers. The big table database has been proven to be able to scale. The figure 3 as below shows what the big data processing platform for Easemob’s future social activity analysis engine looks like.
Figure 3 - Platform for Easemob Social Activity Big Data Analysis
There is one thing I particularly like about Spark. It makes use of the Scala language, which allows distributed datasets to be manipulated like local collections. It combines Object-oriented and functional programming perfectly. It has a very strong static type system. It overcomes many shortcomings of Java language but however, it is still compiled to java bytecode hence it runs on java virtual machine. The Interoperability of the two languages is maintained well. It is perfectly integrated into Spark.
In the big data world, Spark is relatively new technology, however, it is already used by many companies in production such as Yahoo, Sohu and Ebay etc. The long list can be viewed in Apache website.
About the author
Zhi Huang is the Head of Big Data Analytics Department in easemob.com. He can be reached at zhi.huang@easemob.com
Today, in China, our mobile text and voice messaging communication service serves more than 13,000 mobile apps and hundreds of millions of users. We are currently expanding our business globally. If you are app developer, you are more than welcome to use our free service at any time.
作者: 黄智 收起阅读 »
IM客户端数据库加载过程优化
IM通讯里面有两个重要的数据概念,一个是会话,一个是会话中的消息。
在系统初始化时,这两部分都要从数据库中加载到内存中。
数据组织结构是ConversatonManager包含多个会话,每个会话含有消息列表。
每次系统启动的时候,首先查询会话列表,然后对每一个会话加载其中的消息。对应的伪码
conversationList = db.loadConverstaions()
FOR (conversation : conversationList) {
db.loadMessages(conversation);
}
因为每次查询都要涉及数据库操作,导致加载时间很长,而且大量的IO操作也会导致耗电量增加,
所以这部分加载过程,是我们优化的重点。
思路很简单:一条SQL语句做完所有事情,避免for循环,避免多次遍历数据库。
修改后的结构是:
conversationList = db.loadConverstaionsAndMessages();
这样大量的细节隐藏在SQL语句实现中。
这里面的实现有两种情况:
1. 一种是每个会话只加载一条消息记录。
2. 另一种是每个会话加载多条消息记录。
针对“1”中每个会话只加载一条消息记录(假设是最后一条消息),这种情况可以使用关键字group by 处理:
select *, max(msgTime) from xxx_table group by conversation
这种情况比较好理解,而且网上类似的问题很多,很容易找到答案。
对于“2”中每个会话要求加载多条消息的情况(消息按照时间排序),我的思路是在group by, order by, limit这些关键字中寻找答案。
先在网络上寻找答案,寻找一些类似的实现,可惜都不理想。
有的实现就是把for循环转移到sql语句中,利用游标的概念,但是计算的数量级并没有下降,使用我本地的较大的数据量进行试验,执行时间过长。
或者是看到oracle数据库中有解决方案,但是需要使用关键字partition,这个应该是oracle数据看到经常会有类似的问题而提出的专用关键字。
对于mysql, sqlite等常用数据库,没法移植该实现。
最终我使用的方法是,
select * from xxx_table order by conversation, msgTime desc.
这样整个表单进行排序,首先按照会话名称进行排序,然后按照消息时间排序。
还剩下一个条件没有满足,就是每个会话消息的限定个数。
把个数的遍历放在外面实现,通过一个while循环将会话中超出limit部分的消息剔除。
伪码:
cursor = db.EXEC('select * from xxx_table order by conversation, msgTime desc');
while (cursor.NEXT()) {
msg = msgFrom(cursor)
IF (! msg belong TO conversation) {
// 消息不属于当前的会话,所以
conversation = NEW Conversation();
conversation.ADD(msg);
continue;
}
IF (conversation.msgSize() < LIMIT && msg belong TO conversation) {
conversation.ADD(msg);
} ELSE {
// 消息个数已经超过会话消息限制
continue;
}
}
这种方法的缺点是cursor会把整个表单都返回到用户空间,然后把所有的数据在用户空间都遍历一遍,有多余的操作。
不属于最优实现。
优点是两次排序使用order by,可以由数据库实现,这部分执行效率比较高,然后一次遍历cursor就执行完剩余操作,执行效率在接受范围之内,和改动之前相比效率提升至少一个数量级。
测试结果:一万条消息记录,一千个会话,执行时间大概4秒
补充一下,对于非数据库专业人员来说,有一点需要注意:
group by, order by, limit这些关键字在sql语句中有强制的顺序要求,limit , order by,都不能写到group by前面。
下面是我在寻找这个问题过程中看到的一些帖子,第一行是文章标题,后面是我看后的感受。如有冒犯,敬请原谅。
[SQL中Group分组获取Top N方法实现]
游标方法可取,网上讨论说运行比较慢。
一条SQL语句搞定分组并且每组限定记录集的数量]
仅适用于oracle
mysql实现每组取前N条记录的sql,以及后续的组数据量限制]
好像是可以,没看明白
SQL--分组显示数据,显示每组的前几行数据]
http://blog.163.com/peng_peng1028/blog/static/107463820111019240379/
像是答案,效率好像很低
[取每组前几条记录的SQL写法]
http://blog.sina.com.cn/s/blog_412897e10100r2rq.html
该页面提供两种方法,都尝试过,效率太低,杀掉程序时还没执行完
作者: 李楠 收起阅读 »
在系统初始化时,这两部分都要从数据库中加载到内存中。
数据组织结构是ConversatonManager包含多个会话,每个会话含有消息列表。
每次系统启动的时候,首先查询会话列表,然后对每一个会话加载其中的消息。对应的伪码
conversationList = db.loadConverstaions()
FOR (conversation : conversationList) {
db.loadMessages(conversation);
}
因为每次查询都要涉及数据库操作,导致加载时间很长,而且大量的IO操作也会导致耗电量增加,
所以这部分加载过程,是我们优化的重点。
思路很简单:一条SQL语句做完所有事情,避免for循环,避免多次遍历数据库。
修改后的结构是:
conversationList = db.loadConverstaionsAndMessages();
这样大量的细节隐藏在SQL语句实现中。
这里面的实现有两种情况:
1. 一种是每个会话只加载一条消息记录。
2. 另一种是每个会话加载多条消息记录。
针对“1”中每个会话只加载一条消息记录(假设是最后一条消息),这种情况可以使用关键字group by 处理:
select *, max(msgTime) from xxx_table group by conversation
这种情况比较好理解,而且网上类似的问题很多,很容易找到答案。
对于“2”中每个会话要求加载多条消息的情况(消息按照时间排序),我的思路是在group by, order by, limit这些关键字中寻找答案。
先在网络上寻找答案,寻找一些类似的实现,可惜都不理想。
有的实现就是把for循环转移到sql语句中,利用游标的概念,但是计算的数量级并没有下降,使用我本地的较大的数据量进行试验,执行时间过长。
或者是看到oracle数据库中有解决方案,但是需要使用关键字partition,这个应该是oracle数据看到经常会有类似的问题而提出的专用关键字。
对于mysql, sqlite等常用数据库,没法移植该实现。
最终我使用的方法是,
select * from xxx_table order by conversation, msgTime desc.
这样整个表单进行排序,首先按照会话名称进行排序,然后按照消息时间排序。
还剩下一个条件没有满足,就是每个会话消息的限定个数。
把个数的遍历放在外面实现,通过一个while循环将会话中超出limit部分的消息剔除。
伪码:
cursor = db.EXEC('select * from xxx_table order by conversation, msgTime desc');
while (cursor.NEXT()) {
msg = msgFrom(cursor)
IF (! msg belong TO conversation) {
// 消息不属于当前的会话,所以
conversation = NEW Conversation();
conversation.ADD(msg);
continue;
}
IF (conversation.msgSize() < LIMIT && msg belong TO conversation) {
conversation.ADD(msg);
} ELSE {
// 消息个数已经超过会话消息限制
continue;
}
}
这种方法的缺点是cursor会把整个表单都返回到用户空间,然后把所有的数据在用户空间都遍历一遍,有多余的操作。
不属于最优实现。
优点是两次排序使用order by,可以由数据库实现,这部分执行效率比较高,然后一次遍历cursor就执行完剩余操作,执行效率在接受范围之内,和改动之前相比效率提升至少一个数量级。
测试结果:一万条消息记录,一千个会话,执行时间大概4秒
补充一下,对于非数据库专业人员来说,有一点需要注意:
group by, order by, limit这些关键字在sql语句中有强制的顺序要求,limit , order by,都不能写到group by前面。
下面是我在寻找这个问题过程中看到的一些帖子,第一行是文章标题,后面是我看后的感受。如有冒犯,敬请原谅。
[SQL中Group分组获取Top N方法实现]
游标方法可取,网上讨论说运行比较慢。
一条SQL语句搞定分组并且每组限定记录集的数量]
仅适用于oracle
mysql实现每组取前N条记录的sql,以及后续的组数据量限制]
好像是可以,没看明白
SQL--分组显示数据,显示每组的前几行数据]
http://blog.163.com/peng_peng1028/blog/static/107463820111019240379/
像是答案,效率好像很低
[取每组前几条记录的SQL写法]
http://blog.sina.com.cn/s/blog_412897e10100r2rq.html
该页面提供两种方法,都尝试过,效率太低,杀掉程序时还没执行完
作者: 李楠 收起阅读 »
移动开发之语言之美 – 类
现在最受开发者欢迎的两大平台 IOS, android现在主要还使用Objective-C 和 Java 来进行开发,不过苹果公司推出的编程语言 Swift,她吐故纳新,抛弃了Objective C繁琐的语法规则,引入了极为简洁,功能强大的语法。实际上除了平台本身的限制,包括如何调用平台提供的API,如何使用线程,如何启动定时器,如何设计UI界面等等之外,语言本身其实都是大同小异,所以为了方便开发者掌握不同平台的语言,对比学习Swift,Objective-C,Java等移动开发主流语言也是一种不错的选择。
在这个篇章中,我们首先从类定义开始,逐步探讨继承,接口,访问控制
类
如果你学过面向对象编程的语言,你就知道类是个什么概念,简而言之,我们把某种具有共同属性及功能单元的设计蓝图称之为类,它代表的设计定义,而对象就是根据类定义所产生的内存实体。
我现在用一个简单的需求实例来阐明不同的语言是如何实现这个需求的,以便读者可以了解不同的语言是对类如何定义和实现的。
需求:
实现一个类包含4个变量
包含5个函数
构造函数
Objective-C 定义
@interface YN_Example_Class : NSObject{
@private
int mExampleData;
@protected
NSString* mExampleStr;
@public
BOOL mExampleBool;
}
-(YN_Example_Class*) init;
@property(nonatomic) int exampleProperty;
-(void) exampleFunc;
-(void) exampleFuncWithParameters:(NSString*) str withBool:(BOOL) boolP;
-(NSString*) exampleFuncWithReturnValue;
+(void) classExampleFunc;
Objective-C 类实现
@implementation YN_Example_Class
-(YN_Example_Class*) init{
mExampleStr = @"YN_Example_Class";
mExampleData = 8;
mExampleBool = YES;
return self;
}
-(void) exampleFunc{
NSLog(@"exampleFunc");
}
-(void) exampleFuncWithParameters:(NSString *)str withBool:(BOOL) boolP{
mExampleStr = str;
mExampleBool = boolP;
}
-(NSString*) exampleFuncWithReturnValue{
return mExampleStr;
}
+(void) classExampleFunc{
NSLog(@"classExampleFunc");
}
-(void) setExampleProperty: (int) data{
if(_exampleProperty < 100){
_exampleProperty = data;
}
}
-(int) getExampleProperty{
_exampleProperty++;
return _exampleProperty;
}
@end
Swift
class YN_Example_class{
var mExampleStr:String
var mExampleData:UInt
var mExampleBool:Bool
var exampleProperty:UInt{
get{
mExampleData++
return mExampleData;
}
set(data){
if(mExampleData < 100){ mExampleData = data } } } init(){ mExampleStr = "YN_Example_class" mExampleData = 8 mExampleBool = true } func exampleFunc(){ println("exampleFunc") } func exampleFuncWithParameters(str:String, boolP:Bool){ mExampleStr = str; mExampleBool = boolP } func exampleFuncWithReturnValue() -> String{
return mExampleStr
}
class func exampleClassFunc(){
println("exampleClassFunc")
}
}
Java
class YN_Example_Class{
private int mExampleData;
protected String mExampleStr;
public boolean mExampleBool;
private int exampleProperty;
static private String gStr;
YN_Example_Class(){
mExampleStr = "YN_Example_Class";
mExampleData = 8;
mExampleBool = true;
setExampleProperty(0);
}
void exampleFunc(){
System.out.println("exampleFunc");
}
void exampleFuncWithParameters(String str, boolean boolP){
mExampleStr = str;
mExampleBool = boolP;
}
String exampleFuncWithReturnValue(){
return mExampleStr;
}
static void classExampleFunc(){
System.out.println("classExampleFunc");
}
public int getExampleProperty() {
exampleProperty++;
return exampleProperty;
}
public void setExampleProperty(int exampleProperty) {
if(this.exampleProperty < 100){
this.exampleProperty = exampleProperty;
}
}
}
属性property/成员变量
Objective-C既提供变量也提供属性的概念,实际上属性就是一种语法规则,属性本身并不存在内存中,但编译器为之合成一个内存实体叫_PropertyName,我们可以简单的理解为是用来替代麻烦的setter和getter操作,编译器会自动合成相应的setter和getter函数,不过属性定义的不同会导致setter和getter实现的不同。
为了简洁,去除混淆,Swift已经不再区分成员变量和property,她应经统一了二者,直接用关键字var来定义。
Swift 定义了两种propertyStored Property -- 可以理解为property直接存储数据
Computed Property -- 可以理解为property不能直接存储数据,而是通过getter,setter来间接存取exampleProperty就是个Computed Property
var example:YN_Example_class = YN_Example_class()
example.exampleProperty = 9;
println(example.exampleProperty)
example.exampleProperty 值为10
Java 和 Objective-c 还有 Swift不同之处就是,语言层面没有property的支持,不过无非就是多写两行代码,用来setter和getter成员变量
类函数/静态函数
直接了当的说,就是直接可以通过类名访问
YN_Example_Class.classExampleFunc
类变量/静态变量
用来做所有实例共享的变量,单例模式
swift,和Objective-C不能声明类变量在类里,Java一定要声明在类里。
Objective-C
#import
#import "objc_language_class.h"
static NSString* gStr;
@implementation YN_Example_Delegate
Swift
import Foundation
private var gStr:String?
类属性,所有的实例都共享同一个类属性,类属性一定是个Computed Property(看上述property属性),类属性的实现需要全局属性的支持,上述就是如何定义一个全局属性来存储数据
类属性例子如下:
class var gProperty:String {
set(g){
gStr = g
}
get{
return gStr!
}
}
Java
class YN_Example_Class{
static private String gStr;
接口,继承,重载
Swift
import Foundation
protocol YNExample_Protocol{
func getMe() -> String
}
class YN_Example_Delegte : YNExample_Protocol{
var myproto:UInt
var myStr:String
init(){
myproto = 3;
myStr = String()
}
func getMe() -> String{
return "YN_Example_Delegte"
}
}
class YN_Example_Delegte_derived : YN_Example_Delegte{
// without override, complier will give the error
override func getMe() -> String{
return "YN_Example_Delegte_derived"
}
}
Objective-C
#ifndef objc_language_class_objc_language_class_h
#define objc_language_class_objc_language_class_h
@protocol YN_Example_Protocol
-(NSString*) getMe;
@end
@interface YN_Example_Delegate : NSObject{
NSString* myStr;
}
@property int myprop;
-(YN_Example_Delegate*) init;
@end
@interface YN_Example_DelegateDerived : YN_Example_Delegate;
@end
#endif
@implementation YN_Example_Delegate
- (NSString*) getMe{
return @"YN_Example_Delegate";
}
-(YN_Example_Delegate*) init{
myStr = [[NSString alloc] initWithUTF8String:""];
return self;
}
@end
@implementation YN_Example_DelegateDerived
- (NSString*) getMe{
myStr = @"";
return @"YN_Example_DelegateDerived";
}
@end
Java
interface YNExample_Protocol{
String getMe();
}
class YN_Example_Delegte implements YNExample_Protocol{
@Override
public String getMe() {
// TODO Auto-generated method stub
return "YN_Example_Delegte";
}
}
class YN_Example_Delegte_derived extends YN_Example_Delegte{
@Override
public String getMe() {
// TODO Auto-generated method stub
return "YN_Example_Delegte_derived";
}
}
三种语言都不支持多继承,但是都支持多接口实现。
关于重载只有Swift硬性要求在重载的函数前加override,否者complier会报错
构造函数
Swift和Objective-C的默认构造函数都是init(),并且可以定制的构造函,只不过名称都必须命名为init只是参数不同,Java和C++一样都是类名开始的函数,但init有返回值,Java构造函数没有。可以参考上面代码。
Java 和 Objective-C默认都会调用父类的构造函数,但swift则不同,默认不调用除非以下情况:子类没有实现任何init构造函数
子类重载或新实现某些构造函数,但子类所有的property都必须要有默认的初始值,也就是在声明时赋的值
Swift 可以重载构造函数,不过必须要加override前缀
类探测API
Java:
用 instanceof 来探测类type
if(example instanceof YN_Example_Class){
System.out.println("is YN_Example_Class");
}
Swift:
用as来探测类type
delegate as YN_Example_Delegte
Objective-C
用isKindOfClass来探测类type
BOOL isClass = [delegate isKindOfClass:[YN_Example_DelegateDerived class]];
访问控制
Objective-C
其访问控制基本和C++一致
@private: 只有这个类可以访问
@protected: 此类和子类可访问
@public: 任何类都可以访问
默认权限是protected
Swift:
private: 只有在本源文件里的代码可以访问
internal: 只有在本module(例如,app,或framework)里可以访问
public: 没有限制,可以作为对外暴漏的接口,例如创建个library
默认权限是internal
Java:
private: 只有这个类可以访问,不可跨包访问
default: 只有本包得类可以访问,默认的类都是default
protected:本类和此类的子类可以访问,跨包可访问
public: 没有限制,所有类可以访问,跨包可以访问
默认权限是default
此文主要讲解不同的语言对类不同的定义和实现,哪些地方不同并且应该注意什么,不过从结构来看,大体语言都很类似。此文并没有讲解的面面俱到,例如教大家如何熟悉语言的基本语法,如何声明变量,如何写控制流,这些都需要读者自己写程序加以熟悉。
由于时间有限,如果有纰漏之处,还望提醒,我会及时改正,谢谢。
PS:如果需要源码,请发信给mailto : syyorient@outlook.com
作者: 隋云怡
[Reference]
https://itunes.apple.com/cn/book/swift-programming-language/id881256329?mt=11
https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Introduction/I
收起阅读 »
在这个篇章中,我们首先从类定义开始,逐步探讨继承,接口,访问控制
类
如果你学过面向对象编程的语言,你就知道类是个什么概念,简而言之,我们把某种具有共同属性及功能单元的设计蓝图称之为类,它代表的设计定义,而对象就是根据类定义所产生的内存实体。
我现在用一个简单的需求实例来阐明不同的语言是如何实现这个需求的,以便读者可以了解不同的语言是对类如何定义和实现的。
需求:
实现一个类包含4个变量
- mExampleData 是integer类型
- mExampleStr是字符串类型
- mExampleBool 布尔类型
- exampleProperty 是int类型
- 但是exampleProperty是可以通过api进行改变的
包含5个函数
构造函数
- exampleFunc函数没有任何返回值和参数
- exampleFuncWithParameters 没有返回值但是有一个字符串参数,和 布尔型参数
- exampleFuncWithReturnValue 返回值为字符,但没有参数的函数
- classExampleFunc 静态或者类函数
Objective-C 定义
@interface YN_Example_Class : NSObject{
@private
int mExampleData;
@protected
NSString* mExampleStr;
@public
BOOL mExampleBool;
}
-(YN_Example_Class*) init;
@property(nonatomic) int exampleProperty;
-(void) exampleFunc;
-(void) exampleFuncWithParameters:(NSString*) str withBool:(BOOL) boolP;
-(NSString*) exampleFuncWithReturnValue;
+(void) classExampleFunc;
Objective-C 类实现
@implementation YN_Example_Class
-(YN_Example_Class*) init{
mExampleStr = @"YN_Example_Class";
mExampleData = 8;
mExampleBool = YES;
return self;
}
-(void) exampleFunc{
NSLog(@"exampleFunc");
}
-(void) exampleFuncWithParameters:(NSString *)str withBool:(BOOL) boolP{
mExampleStr = str;
mExampleBool = boolP;
}
-(NSString*) exampleFuncWithReturnValue{
return mExampleStr;
}
+(void) classExampleFunc{
NSLog(@"classExampleFunc");
}
-(void) setExampleProperty: (int) data{
if(_exampleProperty < 100){
_exampleProperty = data;
}
}
-(int) getExampleProperty{
_exampleProperty++;
return _exampleProperty;
}
@end
Swift
class YN_Example_class{
var mExampleStr:String
var mExampleData:UInt
var mExampleBool:Bool
var exampleProperty:UInt{
get{
mExampleData++
return mExampleData;
}
set(data){
if(mExampleData < 100){ mExampleData = data } } } init(){ mExampleStr = "YN_Example_class" mExampleData = 8 mExampleBool = true } func exampleFunc(){ println("exampleFunc") } func exampleFuncWithParameters(str:String, boolP:Bool){ mExampleStr = str; mExampleBool = boolP } func exampleFuncWithReturnValue() -> String{
return mExampleStr
}
class func exampleClassFunc(){
println("exampleClassFunc")
}
}
Java
class YN_Example_Class{
private int mExampleData;
protected String mExampleStr;
public boolean mExampleBool;
private int exampleProperty;
static private String gStr;
YN_Example_Class(){
mExampleStr = "YN_Example_Class";
mExampleData = 8;
mExampleBool = true;
setExampleProperty(0);
}
void exampleFunc(){
System.out.println("exampleFunc");
}
void exampleFuncWithParameters(String str, boolean boolP){
mExampleStr = str;
mExampleBool = boolP;
}
String exampleFuncWithReturnValue(){
return mExampleStr;
}
static void classExampleFunc(){
System.out.println("classExampleFunc");
}
public int getExampleProperty() {
exampleProperty++;
return exampleProperty;
}
public void setExampleProperty(int exampleProperty) {
if(this.exampleProperty < 100){
this.exampleProperty = exampleProperty;
}
}
}
属性property/成员变量
Objective-C既提供变量也提供属性的概念,实际上属性就是一种语法规则,属性本身并不存在内存中,但编译器为之合成一个内存实体叫_PropertyName,我们可以简单的理解为是用来替代麻烦的setter和getter操作,编译器会自动合成相应的setter和getter函数,不过属性定义的不同会导致setter和getter实现的不同。
为了简洁,去除混淆,Swift已经不再区分成员变量和property,她应经统一了二者,直接用关键字var来定义。
Swift 定义了两种propertyStored Property -- 可以理解为property直接存储数据
Computed Property -- 可以理解为property不能直接存储数据,而是通过getter,setter来间接存取exampleProperty就是个Computed Property
var example:YN_Example_class = YN_Example_class()
example.exampleProperty = 9;
println(example.exampleProperty)
example.exampleProperty 值为10
Java 和 Objective-c 还有 Swift不同之处就是,语言层面没有property的支持,不过无非就是多写两行代码,用来setter和getter成员变量
类函数/静态函数
直接了当的说,就是直接可以通过类名访问
YN_Example_Class.classExampleFunc
类变量/静态变量
用来做所有实例共享的变量,单例模式
swift,和Objective-C不能声明类变量在类里,Java一定要声明在类里。
Objective-C
#import
#import "objc_language_class.h"
static NSString* gStr;
@implementation YN_Example_Delegate
Swift
import Foundation
private var gStr:String?
类属性,所有的实例都共享同一个类属性,类属性一定是个Computed Property(看上述property属性),类属性的实现需要全局属性的支持,上述就是如何定义一个全局属性来存储数据
类属性例子如下:
class var gProperty:String {
set(g){
gStr = g
}
get{
return gStr!
}
}
Java
class YN_Example_Class{
static private String gStr;
接口,继承,重载
Swift
import Foundation
protocol YNExample_Protocol{
func getMe() -> String
}
class YN_Example_Delegte : YNExample_Protocol{
var myproto:UInt
var myStr:String
init(){
myproto = 3;
myStr = String()
}
func getMe() -> String{
return "YN_Example_Delegte"
}
}
class YN_Example_Delegte_derived : YN_Example_Delegte{
// without override, complier will give the error
override func getMe() -> String{
return "YN_Example_Delegte_derived"
}
}
Objective-C
#ifndef objc_language_class_objc_language_class_h
#define objc_language_class_objc_language_class_h
@protocol YN_Example_Protocol
-(NSString*) getMe;
@end
@interface YN_Example_Delegate : NSObject
NSString* myStr;
}
@property int myprop;
-(YN_Example_Delegate*) init;
@end
@interface YN_Example_DelegateDerived : YN_Example_Delegate;
@end
#endif
static NSString* gStr;
@implementation YN_Example_Delegate
- (NSString*) getMe{
return @"YN_Example_Delegate";
}
-(YN_Example_Delegate*) init{
myStr = [[NSString alloc] initWithUTF8String:""];
return self;
}
@end
@implementation YN_Example_DelegateDerived
- (NSString*) getMe{
myStr = @"";
return @"YN_Example_DelegateDerived";
}
@end
Java
interface YNExample_Protocol{
String getMe();
}
class YN_Example_Delegte implements YNExample_Protocol{
@Override
public String getMe() {
// TODO Auto-generated method stub
return "YN_Example_Delegte";
}
}
class YN_Example_Delegte_derived extends YN_Example_Delegte{
@Override
public String getMe() {
// TODO Auto-generated method stub
return "YN_Example_Delegte_derived";
}
}
三种语言都不支持多继承,但是都支持多接口实现。
关于重载只有Swift硬性要求在重载的函数前加override,否者complier会报错
构造函数
Swift和Objective-C的默认构造函数都是init(),并且可以定制的构造函,只不过名称都必须命名为init只是参数不同,Java和C++一样都是类名开始的函数,但init有返回值,Java构造函数没有。可以参考上面代码。
Java 和 Objective-C默认都会调用父类的构造函数,但swift则不同,默认不调用除非以下情况:子类没有实现任何init构造函数
子类重载或新实现某些构造函数,但子类所有的property都必须要有默认的初始值,也就是在声明时赋的值
Swift 可以重载构造函数,不过必须要加override前缀
类探测API
Java:
用 instanceof 来探测类type
if(example instanceof YN_Example_Class){
System.out.println("is YN_Example_Class");
}
Swift:
用as来探测类type
delegate as YN_Example_Delegte
Objective-C
用isKindOfClass来探测类type
BOOL isClass = [delegate isKindOfClass:[YN_Example_DelegateDerived class]];
访问控制
Objective-C
其访问控制基本和C++一致
@private: 只有这个类可以访问
@protected: 此类和子类可访问
@public: 任何类都可以访问
默认权限是protected
Swift:
private: 只有在本源文件里的代码可以访问
internal: 只有在本module(例如,app,或framework)里可以访问
public: 没有限制,可以作为对外暴漏的接口,例如创建个library
默认权限是internal
Java:
private: 只有这个类可以访问,不可跨包访问
default: 只有本包得类可以访问,默认的类都是default
protected:本类和此类的子类可以访问,跨包可访问
public: 没有限制,所有类可以访问,跨包可以访问
默认权限是default
此文主要讲解不同的语言对类不同的定义和实现,哪些地方不同并且应该注意什么,不过从结构来看,大体语言都很类似。此文并没有讲解的面面俱到,例如教大家如何熟悉语言的基本语法,如何声明变量,如何写控制流,这些都需要读者自己写程序加以熟悉。
由于时间有限,如果有纰漏之处,还望提醒,我会及时改正,谢谢。
PS:如果需要源码,请发信给mailto : syyorient@outlook.com
作者: 隋云怡
[Reference]
https://itunes.apple.com/cn/book/swift-programming-language/id881256329?mt=11
https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Introduction/I
收起阅读 »
自组织是不是团队管理的乌托邦?
对于很多管理者,最幸福的事,莫过于做到名义上管理一个团队,但实际上什么都不需要做。他所带的逆天团队还可以成果迭出。对于团队中的成员来讲,如果他可以做什么都不被管,做什么都有人帮,那真是可以做梦也会笑醒的。这样的团队存在么?自组织的团队据说就可以,不管你信不信,所以我看了这本书。
我想要谈论它,还有个人变动的原因。我曾经待过养老的公司,也从热衷微管理的公司走过。几个月前又刚从一个大公司出来,到了一个小小的初创企业。环境的变化会让人变得更加敏感,上面那些变化让我得以对比思考管理的各个方面。其实那个大公司也不大,不过大公司很多问题它都有而已。(大公司病嘛,等有时间再讨论)
很多时候,加入一个有问题的团队,就像旅行路上经过的沼泽地。为了生存你只能滚着前进,与沼泽保持尽可能大的接触面,然后弄一身脏泥巴。等到滚多了,再加上勤奋努力,很容易的,你会成为一个滚泥巴专家。
你很可能忘记,本来只是为了经过。你想要去的,是美好的远方。
这只是个提醒。也许你只想研究技术,但你值得知道,哪些是该受的,哪些是该得的。如果你还想管理,不想掉进泥沼里,你更不应该停止对好团队的追求。这本书也就更值得一看。
这是一篇读书笔记,书是《管理3.0》。这本书都很多理论和观点有意思,我会谈谈我喜欢的一些,包括自组织相关的思维方式、创新问题、开发模式和团队管理。也会补充我认为缺失的一环。最后是一个问题,也是对个人最重要的问题,你适合什么样的团队?
思维方式
值得一提的是系统思维,来源于彼得·桑吉的《第五项修炼》,“将问题视为整个系统的一个组成部分,侧重于组织内的循环关系和非线性因果关系”(P47)。其实就是不要将一个运动的整体割裂对待。当一个人作为个体存在于团队内部时,其他人都是环境变量。你做的所有动作,都会对其他人产生影响。要认识到自己能力可以扩展的边界。当然重要的是,不要跟猪做队友。
这本书我也是几年前看过,关于系统内部正向反向反馈的说法印象深刻。只要反馈环建立起来,就会有习惯的力量帮助你或者阻碍你。所以要关心当前团队的运转情况,强化正向反馈。
其次是关于简化论的说明,“找到了心脏病的诱因(简化论),并不意味着可以制造一个不生病的心脏(构建论)”(P9)。这也是很多人知道自己团队的问题,却不知道如何构建一个好团队的原因。简化论是很多人思考停止的地方,因为对于吐槽来说足够了,殊不知后面才是更有价值的。
关于创新
1. 创新需要土壤
在这个人人视抄袭为平常的国家里,创新已经稀有到要比鬼还少了。为什么中世纪后有文艺复兴,为什么有春秋战国可以百家争鸣?我与作者一样,非常赞同土壤的说法,“复杂性系统方法认为创新是可遇而不可求的,它是水到渠成而自然涌现的结果。得先有涌现所需的土壤,才能期待它的发生”(P52)。就在去年提这说法的时候(内部在创新方面一直有讨论),我是希望说明创新是需要多数人参与,发生在少数人身上的,要给工程师们空间,不要太多管理操作。
前文有个关键词是“涌现”,它在做属性时,按照定义是“当一个系统的属性不能追溯到系统的任何独立部分时,我们就称之为涌现属性”,就像“流动性是水的涌现属性,文化是群体的涌现属性”;做动词时,强调的是自然发生,整体性质的体现,比如在说涌现式设计则强调 “最好的架构不是预先定义好的(可能有一个基本的形式),它可以在产品开发过程中逐步浮现出来”(P21)。这里隐含的意思,我理解,创新是一种整体表现,也是需要用结果后验的,我们要做和能做的只是增大其可能性而已。
2. 包容式多样性
作者更进一步,强调了土壤的肥沃问题,“团队的多样性能够显著增加其创造力。但必须有足够的共同点加一些制衡,即包容式多样性”(P60)。做事要找志趣相投的人只是其一,只有能力互补,才容易有火花碰撞出来。这个在我组织过的黑客马拉松、参加过的内部创新大赛表现得十分明显,如果只是技术人组队,很容易陷在 Geek 圈里,玩玩原型做技术储备可以,离产品化还是甚远。
说起来,此次微博的升级视频,我自认为是个小的正向例子。那是有一天,设计师跑过来问说如果想利用每个人的信息来生成个性化的视频,能不能搞得定。如果没有服务的话,他只能每个人手动生成了,可能几千个。我说没有经验,但可以试试看。于是有了最后的 AppleScript + Shell + JavascriptX 的奇怪组合。我想说的是,由于有设计师的参与,由于不同领域的人共同对技术应用边界的摸索,才有了产品质量的成果出来。事实上效果还是比较令人满意的,没看过的可以看看这里:http://www.weibo.com/1819367693/Bp6II3a1V
我说这个例子是偶然,因为从想法到实现还有相当长的路要走。大多数想法都像书中所说,"一个很好地想法可能在公司内被踢来踢去好几年而得不到利用,这不是因为大家没有看到它的好处,而是因为不知道应该由谁来负责将它踏实地落到行动上”(P63)。我是负责后端团队,通常不会与设计师打交道,与这位设计师认识纯粹是由于组织黑客马拉松的时候他帮忙设计制作了宣传视频。
3. 勒曼第六法则
对于一个业务基本稳定的公司,创新难还有一个重要原因,就是勒曼第六法则,"许多软件产品并没有演化使自身变得更好,他们演化之目的只是为了尽量不被用户抛弃(不可避免)”(P310),大多时候,它们 “为了将客户满意度维持在相同的水平,要不断增加产品的功能性”。制定 KPI 当然要 SMART 不是么?你见过 KPI 说要将创新水平提高几个百分点么?。。。
说回创造力,还有个有趣的三阶段理论。先是前习俗创造力,属于儿童的,是自发性的;然后是习俗创造力,一般在7-11岁左右,开始是真正的思考,了解世界的边界;最后是后习俗创造力,也是创新开始的地方(P67)。最后一阶段的时候,便是讲初心的地方,“即使知道有实实在在的约束,也可以像小孩子一样充满想象力”(P70)。这个理论很类似的一个学习理论,会在后面人员培养处再讲。这里主要说明边界学习对创新的影响。边界客观存在,不要让它制约你的想法。
4. 罗杰斯创新曲线
罗杰斯有个创新曲线,说的是团队人群分布。“创新者2.5%、早期采用者13.5%、早期采用人群34%、后期采用者34%、迟缓者16%。改变要从渴望尝试新事物的创新者入手,一次努力推动后面人群。忽略行动迟缓者,他们会一直抵制改变,直到其他所有人都采用”(P335)。这个比例在不同团队肯定会有所差异,你只要记住,不要想当然所有人都愿意改变就行。然后在尝试新事物的时候,不要怕反对声音。
管理者要做的就是发现这种萌芽,帮助其成长而不被恶劣环境影响。对那些不愿意改变的力量进行驱赶,甚至可以让他们适当痛苦。“让停滞痛苦不堪”(P336)说的就是这种迂回方式,有些人吃一堑才能长一智。
手中的鞭子和驴头前面的胡萝卜同样有用。
开发模式
书中提了两家的宣言:
敏捷软件开发(P19)
“个人和交互 胜于 流程和工具;可以工作的软件 胜于 复杂的文档”;
“虽然右项也具有价值,我们认为左项具有更大的价值”
精益软件开发(P24)
“可以工作的软件犹不足,尚需精益求精;响应变化犹不足,尚需稳步增加价值”
“在追求左项的过程中,我们发现右项亦不可或缺”
说实话我对这些模式都不感冒,这方面我更功利,我觉得不同发展阶段需要不同的开发方式。特别在互联网行业,一个团队的开发活动需要在高速度和高质量方面反复切换。
在团队人员紧缺的时候,你要有速度意识,这是质量下降几乎是必然的;如果人员稍微充沛,就要重新思考质量问题,承认低质量软件的存在与不合理之处,持续改进。因为当用户请求量上来,几乎所有的问题都会显现出来(墨菲定律),你要做的是在Bug 酿成灾难之前消灭掉。
我在心里是渴望精益的。
关于不同开发方式其实网上有很多争吵,极端言论也比比皆是,但看两者的宣言,你很难产生针锋相对的感觉。为什么?因为很多人在交流时急于表达自己的观点,(或有意或无意地)不会强调与对方观点的相同之处,这在沟通中造成了很多无谓的争执。相互熟悉的人之间也无法避免这种情况,更别说背景差异的团队成员之间了。
因此我也更加赞同敏捷宣言里对个人和交互的强调。有效沟通才可能让改进发生。
团队管理
1. 动机管理,以人为本
“人是任何软件项目中最复杂的元素”(P62),确实所有对时间和项目的管理,都是对人的管理。而其中最难的部分,是人员的动机管理,“所有管理者首先关注的应该是激励员工以确保他们愿意全心全意做事,而这一切,都需要动力”(P56)。如果一个人已经与团队贰心,那你实质上已经失去他了。所有的CEO(大忽悠)都会谈价值观,谈企业文化,也是这个目的。
我们把只画饼的宣传叫做忽悠,是在强调奖励的必要性。但奖励最重要的目的是要变成激励,书里提醒 “不要把为团队聚餐买单当成庆祝里程碑式胜利表达谢意的方式。为团队买单是为了解决人们社交和相互联系的需求”(P85)。而且“不要应某些员工的要求而取悦他们,从而引入规则、实践和政策。真正的目的必须是引入秩序和稳定性”。
后面这句话我不知道是否理解得准确,我只能说部分同意。我同意规则和秩序,只要他们是公平的,而不在意是否为了取悦员工。事实上我认为,除了真正的团队目标,其他所有事情都应该为了成员愉悦而做,这样才能最大发挥成员的价值。
在某种程度上,管理应该是服务性的。
2. 适当授权,解除电网
如果说动机管理搞定了人员能动性的问题,那么授权管理就是为解决工作效率的问题。“智慧的控制要做到无形中的影响。决策的制定也应该来自人员之间的互动,而非来自我的权威”(P109),这方面英文Empowerment更容易理解一些。把权力下放给员工,但不要忘记目的是“不是增强激励,而是改善管理”。当然,授权也是分级别的:告知、贩卖、咨询、商定、建议、征询、委托(P125)。当然,“这并不意味着需要分级的架构”。
授权不当就会造成隐形电网的存在。“当管理者对下属赋能时,他们并不会清楚地点明其职权界限,这意味着人们往往只能摸着石头过河,磕磕绊绊,中间还伴随着情感伤害”,“它是时间和资源的双重浪费。屡屡触及隐形电网的经历会扼杀人们的积极性”(P173)。这方面我倒是体会颇深,因为不同等级的信息不对称和相互之间的沟通问题,会造成很多理解上的偏差,碰上电网是非常可能发生的。这个事故的预防(降低频次)以及处理好坏(减轻伤害),不同的管理者之间差距非常明显。坏的结果多了,就很容易造成多一事不如少一事的氛围,离自组织也必然是越来越远。
3. 运用同侪压力
适当授权之后,就可以放手团队去解决问题了,也是时候做好准备接受成员犯错的考验了。“若要有所作为,必先磨练耐性”(P130),而且“当已授权的团队走进办公室请求你决定某件事情时,要想办法让他们自己解决问题”(P135)。不要做保姆,不止是减轻当前的负担,重要的是要防止成员思维惰性,养成依赖别人思考的习惯。
团队可以自行运转,来源于成员想一起共事的意愿,但意愿能促进协作么?书里提到的“同侪压力”,很好的说明了这一点。当你做事的时候,大家都在看着你。你的能力展现无遗,好与不好都在每个人心里。这种社会压力让成员之间可以互相驱动,也是规则发挥作用的地方。
要让同侪压力转化成动力,要注意“与那些试图破坏这些团队的人做斗争”,因为“只有在人们想要加入某个团体时,该团体的社会压力才会起作用”(P224)。最好的方式是,“建立团队、设定目标、再退后一步”。如果有人要破坏团队的规则,那么“找他谈一谈”,不奏效就“再谈一谈”,还不行就“请他离开”。团队是作为团队而存在,而不是为某个人。我的经验是,在这方面越果断越好。
如果怕请人离开,那就把好入门的关。“在允许某人加入(有挑战性的)项目之前,必须对他进行适当的测试”(P185),这是书中根据荷兰的低交通事故率总结的经验。我在微博也一直坚持,没有经过内部新兵训练以及高性能服务设计演练的人,再忙也不会让其进入正式的开发序列。这是对当事人负责,如果他因为经验缺乏酿成事故,以后做事也会畏手畏脚;更是对团队其他人负责,团队不应该因为某个人的问题而受拖累。互联网应用,完成功能只是基本的,还要保证性能、可用性等很多方面。问题在上线前解决要远比上线后简单。而一旦上线,就是7x24小时的服务,出了问题基本是不搞定不眠不休的(也是苦逼之处)。
一句话,宁肯一堆人累,也不要被一个人坑。
4. 保护团队成员
“管理者既要促进自组织,还要保护大家不受伤害”(P175)。我本来觉得这是对管理者基本的要求,但是在一次内部讨论中,出现了截然不同的相反观点。有人反问说,如果一个人不能很好地保护自己,总需要人呵护,如何期望他在对外合作中,甚至在公司外部的行业洗礼中生存下来?
如果说从个人角度来看待这个问题,尚可以理解的话,从团队角度看来我是完全不赞同。组成团队的主要目的是为了完成共同的目标,而不是为了让个人接受锻炼。而在完成目标过程中成员之间互相配合互相帮助,也是团队存在的价值所在。让个人成长,在团队中发挥更大价值,跟让个人接受磨练以成长到更高层次,是两件事。
特别是当管理者不是领导整个公司的时候(个人还没体验过),如果不喜欢随波逐流,期望自己的团队在公司内可以变得更好,举例来讲先变成自组织模式,更要注意这点。如果让团队中的普通成员去接受不必要的压力,要么他觉得太困难无法做事,要么他适应下来,结果把外界的习惯带回当前小团队,所有这些都会对团队向自组织转变起到负面作用。这就是环境的力量。用系统思维方式来看,如之前所述,就是不要让环境对团队改进产生负向反馈。
5. 开始,不要停止改进
开始改变并不意味着就从此一路向前了。一切都在变化之中,环境、人员甚至是目标都有可能随时改变。而整个团队也很可能进入局部优化的状态,事情做得马马虎虎,也总是问题不断。这时候就要考虑模拟退火了。“先加热再冷却会改变金属的性质,比如强度和硬度,这种技术叫退火。复杂性系统中,错误和噪声-通常由环境引发,会扰乱系统并使之拜托局部优化,在这之后,系统更容易找到更好地位置安定下来。”(P339)
用错误和噪声来改善团队,说起来有些奇怪,再看一下模因论可能就好理解了:“不要求模因组内所有思想、概念和实践都必须是有益的。其中的一些有害处,但由于同属于一个模因组,负面思想也会帮助正面思想互相复制,这样便可以中和负面影响。危险范例:没有确凿证据能够证明代码集体所有制的价值,但是此实践似乎能增强其他敏捷实践,所以复制它也不会有什么坏处。”(P204)
如果发生了局部优化,我理解很可能是某些方法措施有了什么副作用。这时就要重新审视团队,是否有规则没有实施或者被错误的实施了;如果发现新规则不适应当前的团队,要继续尝试其他可能。
不恰当的错误补救例子很多,比如弹性制度下一个人钻空子不好好工作变为全部人强制坐班,或者一个人在项目过程中出现沟通问题,全部人天天一起开会。这种情况还有很多,说个亲身经历的创新相关的例子。
曾经有一次大家想搞创新,但是在如何创新上分化成了两派。一派就是我刚才提的土壤论,希望解放大多数同学的创造力;另一派则是规划论,认为创新是可以规划出来,用项目来管理的。讨论结果是决定先用后者。不出意料的是,执行规划论的结果就是,大家只是将其看做是另一项工作任务而已。问题是本来都忙不过来了,还要再进行一项自己毫不在意的事情,即使它的名字叫创新,会有什么动力呢。因此这个行动基本上还未开始就结束了。然后,整个事情最奇怪的地方出现了,再没有人谈论创新问题,好像不存在这件事情一样。
举这个例子,不是说理念分歧的问题,而是整个团队在失败尝试之后的总结和使用方式,也就是如何获取经验的问题。当一件事情失败之后,不要怕把事情放在台面上讨论,特别是尝试,大部分都是要失败的。如果失败了而不总结,才是对自己最大的不负责任。总结可以得到经验,但要时刻对经验保持警惕。好的经验固然让人成熟进步,错误经验却让人裹足不前。
攻打威虎山的时候,正面土匪多有坦克,小分队又从背面上山了不是?
这就够了么
思想是好思想,行动也有方法,就能实现么?在我看来,至少还有两个问题需要解决才行,也是书上没有讲的。一是目标管理,二是人员培养。
1. 目标管理
这个目标管理不同于前面说的动机管理。动机管理是解决愿不愿意做的问题,目标是解决要做什么的问题。一个团队是自组织的,意味着每个人都是自主的而不是被管理的,好的时候会像大雁一样跟随头雁,不好的时候就各自为政,一盘散沙。
因此目标管理是不可忽略的。咦,那还怎么自组织?我个人理解,这不是像KPI 一样的设定与分解,而是一起来讨论团队目标,然后自我选择与设定的过程。事情一样,但规则不同。
2. 人员培养
并不是每个人都适合自组织团队。有的人能力不足,无法自主完成任务;有的人驱动不足,需要监督和指导;有的人悟性不够,无法自己发现问题。你也许会说,那就不是我们需要的人啊。
那只是理想。什么都是变化的,市场是变的,目标是变的,人也是如此。好的方面是,他的能力会提高,你有可能培养出一个合格的人;不好的方面是,他可能离开,你会流失已有的成员。
所以现实是,在你集合一群合格的人之前,你有漫长的路要走,而你的船不能停或掉头。因此,人员培养是必须要重视的。但其实团队能给的帮助有限,只有足够的压力和适当的指导。适当的指导让其找到方向,适当的压力让其不要懈怠,如此而已。
人必先自救,而后人救之。这句话放在这里依然是适用的。
自组织是不是团队管理的乌托邦
你可能会想,有个先进社会还说达到之后人人富足,衣食无忧,我不也天天在初级阶段生活么?自组织会不会成为团队管理的乌托邦?坦率来讲,我也不知道。我曾经看过(自以为)自组织的影子,但是没有完全实现。
每个人有特定适应的团队,如果你怀疑自组织,你可以不去实践,但你肯定需要寻找自己心仪的团队,不管是加入还是组建。我只是知道我喜欢这样的团队,我也愿意尝试。
你要考虑的问题其实是,你适合什么样的团队。想一想,
你是否能够把握自己的方向?
你是否希望不再被指手画脚?
你是否能够自学愿意帮助他人?
你希望找到一群跟你一样的人?
...
如果是的话,那就值得一试。
我们正在试。
原文: http://ericliang.info/is-self-organization-utopia-of-group-management/
作者: 梁宇鹏|
收起阅读 »
环信SDK与Apple Watch的结合系列讲解(3)
第3章主要介绍怎样在Watch App的页面上显示iPhone程序里的数据。主要操作的是“EMWatchOCDemo WatchKit Extension”这个文件夹,附源码EMWatchOCDemo。
如果你已经看过我在第1章推荐的blog,应该明白这个target主要是负责逻辑的,从iPhone App中获取数据,调动Watch App显示数据。
默认是这个样子的
一、WathKit定义了一些专门用于Watch App的类,与UIKit的对比如下图
二、整合Watch App和iPhone App
1、新建Controller
根据Interface.storyboard,我需要新建5个Controller。右键---New File---Cocoa Touch Class
新建的类默认有三个方法,[-awakeWithContext:]相当于[-viewDidLoad],[-willActivate]相当于[-viewWillAppear],[-didDeactivate]相当于[-viewDidDisappear],“相当于”一下是不是就很容易理解每个方法中能进行什么操作了?
建好这5个Controller之后,再次打开Interface.storyboard,在每个storyboard Controller的Class属性中填写对应的类名,这种操作对于熟悉storyboard的开发者来说,应该都不陌生。
附图一张
2、将自定义的类与storyboard关联起来之后,继续关联其他的控件。
声明插件变量Table,并在storyboard中进行关联。
@property (weak, nonatomic) IBOutlet WKInterfaceTable *table;
创建自定义的Table Row Controller,右键---New File---Cocoa Touch Class---Subclass of “NSObject”,声明插件变量Label,在storyboard中将Table Row Controller和Label进行关联。要记得填写Table Row Controller的Identifier,在加载数据时会用到这个属性。
3、接下来要进行每个页面的数据获取了,我是在[-awakeWithContext:]中进行的数据获取。
WKInterfaceController有个类方法[+ openParentApplication: reply:],用于向对应的iPhone App发起申请。
而对应的iPhone App要想检测到这个请求,需要在AppDelegate中监听 [- application: handleWatchKitExtensionRequest: reply:].
以菜单页面MenuController为例,当页面加载时要先向iPhone App发起获取是否登录的申请,iPhone App收到申请,将是否登录的值返给WatchKit Extension;如果没有登录,页面上显示“登录”选项,如果登录了,显示“会话”“好友”“群组”三个选项。
MenuController:
[WKInterfaceController openParentApplication:@{@"action":@"isLogined"} reply:^(NSDictionary *replyInfo, NSError *error) {
BOOL isLogined = NO;
if ([replyInfo count] > 0) {
isLogined = [[replyInfo objectForKey:@"isLogined"] boolValue];
}
if (isLogined) {
NSDictionary *conversationInfo = [NSDictionary dictionaryWithObjectsAndKeys:@"会话", @"title", nil];
NSDictionary *friendInfo = [NSDictionary dictionaryWithObjectsAndKeys:@"好友", @"title", nil];
NSDictionary *groupInfo = [NSDictionary dictionaryWithObjectsAndKeys:@"群组", @"title", nil];
[self.dataSoure addObject:conversationInfo];
[self.dataSoure addObject:friendInfo];
[self.dataSoure addObject:groupInfo];
NSInteger count = [self.dataSoure count];
//@"RowType2Controller"就是上边提到的Table Row Controller 的Identifier属性
[self.table setNumberOfRows:[self.dataSoure count] withRowType:@"RowType2Controller"];
for (int i = 0; i < count; i++) {
RowType2Controller *rowController = [self.table rowControllerAtIndex:i];
NSDictionary *dic = self.dataSoure[i];
NSString *title = dic[@"title"];
[rowController.titleLabel setText:title];
}
}
else{
//@"RowType2Controller"就是上边提到的Table Row Controller 的Identifier属性
[self.table setNumberOfRows:1 withRowType:@"RowType2Controller"];
RowType2Controller *rowController = [self.table rowControllerAtIndex:0];
[rowController.titleLabel setText:@"登录"];
}
}];
AppDelegate
- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void (^)(NSDictionary *))reply
{
if ([userInfo count] > 0) {
NSString *actionString = [userInfo objectForKey:@"action"];
EaseMob *easemob = [EaseMob sharedInstance];
if ([actionString isEqualToString:@"isLogined"]) {
reply(@{@"isLogined":[NSNumber numberWithBool:[easemob.chatManager isLoggedIn]]});
}
}
4、获取到了数据,接下来要调用Watch App显示数据了。
显示数据主要用到了WKInterfaceTable。WKInterfaceTable相对于UITableView而言,能调用的接口少的可怜
WKInterfaceTable.h
WK_CLASS_AVAILABLE_IOS(8_2)
@interface WKInterfaceTable : WKInterfaceObject
- (void)setRowTypes:(NSArray *)rowTypes; // row names. size of array is number of rows
- (void)setNumberOfRows:(NSInteger)numberOfRows withRowType:(NSString *)rowType; // repeating row name
@property(nonatomic,readonly) NSInteger numberOfRows;
- (id)rowControllerAtIndex:(NSInteger)index;
- (void)insertRowsAtIndexes:(NSIndexSet *)rows withRowType:(NSString *)rowType;
- (void)removeRowsAtIndexes:(NSIndexSet *)rows;
- (void)scrollToRowAtIndex:(NSInteger)index;
@end
WKInterfaceController中
上一步中的代码示例已经给出了WKInterfaceTable使用方式,具体代码请看demo。
5、每个单独的页面都写好了,现在要让他们动起来。
WatchKit提供了三类页面导航方式。
第一种UINavigationController 控制的类似栈的导航方式,相应接口
- (void)pushControllerWithName:(NSString *)name context:(id)context; // context passed to child controller via initWithContext:
- (void)popController;
- (void)popToRootController;
第二种 modal 形式,相应接口
- (void)presentControllerWithName:(NSString *)name context:(id)context; // modal presentation - (void)dismissController;
第三种 类似 UIPageController 的分页式导航,相应接口
- (void)presentControllerWithNames:(NSArray *)names contexts:(NSArray *)contexts; // modal presentation of paged controllers. contexts matched to controllers - (void)becomeCurrentPage;
其中的“WithName(s):”参数就是每个控件在storyboard中设置的Identifier属性。
好了,就先写这么多吧,后期有时间会继续补充。
作者: 谢雅杰
环信SDK与Apple Watch的结合系列讲解(1)
环信SDK与Apple Watch的结合系列讲解(2)
收起阅读 »
如果你已经看过我在第1章推荐的blog,应该明白这个target主要是负责逻辑的,从iPhone App中获取数据,调动Watch App显示数据。
默认是这个样子的
一、WathKit定义了一些专门用于Watch App的类,与UIKit的对比如下图
二、整合Watch App和iPhone App
1、新建Controller
根据Interface.storyboard,我需要新建5个Controller。右键---New File---Cocoa Touch Class
新建的类默认有三个方法,[-awakeWithContext:]相当于[-viewDidLoad],[-willActivate]相当于[-viewWillAppear],[-didDeactivate]相当于[-viewDidDisappear],“相当于”一下是不是就很容易理解每个方法中能进行什么操作了?
建好这5个Controller之后,再次打开Interface.storyboard,在每个storyboard Controller的Class属性中填写对应的类名,这种操作对于熟悉storyboard的开发者来说,应该都不陌生。
附图一张
2、将自定义的类与storyboard关联起来之后,继续关联其他的控件。
声明插件变量Table,并在storyboard中进行关联。
@property (weak, nonatomic) IBOutlet WKInterfaceTable *table;
创建自定义的Table Row Controller,右键---New File---Cocoa Touch Class---Subclass of “NSObject”,声明插件变量Label,在storyboard中将Table Row Controller和Label进行关联。要记得填写Table Row Controller的Identifier,在加载数据时会用到这个属性。
3、接下来要进行每个页面的数据获取了,我是在[-awakeWithContext:]中进行的数据获取。
WKInterfaceController有个类方法[+ openParentApplication: reply:],用于向对应的iPhone App发起申请。
而对应的iPhone App要想检测到这个请求,需要在AppDelegate中监听 [- application: handleWatchKitExtensionRequest: reply:].
以菜单页面MenuController为例,当页面加载时要先向iPhone App发起获取是否登录的申请,iPhone App收到申请,将是否登录的值返给WatchKit Extension;如果没有登录,页面上显示“登录”选项,如果登录了,显示“会话”“好友”“群组”三个选项。
MenuController:
[WKInterfaceController openParentApplication:@{@"action":@"isLogined"} reply:^(NSDictionary *replyInfo, NSError *error) {
BOOL isLogined = NO;
if ([replyInfo count] > 0) {
isLogined = [[replyInfo objectForKey:@"isLogined"] boolValue];
}
if (isLogined) {
NSDictionary *conversationInfo = [NSDictionary dictionaryWithObjectsAndKeys:@"会话", @"title", nil];
NSDictionary *friendInfo = [NSDictionary dictionaryWithObjectsAndKeys:@"好友", @"title", nil];
NSDictionary *groupInfo = [NSDictionary dictionaryWithObjectsAndKeys:@"群组", @"title", nil];
[self.dataSoure addObject:conversationInfo];
[self.dataSoure addObject:friendInfo];
[self.dataSoure addObject:groupInfo];
NSInteger count = [self.dataSoure count];
//@"RowType2Controller"就是上边提到的Table Row Controller 的Identifier属性
[self.table setNumberOfRows:[self.dataSoure count] withRowType:@"RowType2Controller"];
for (int i = 0; i < count; i++) {
RowType2Controller *rowController = [self.table rowControllerAtIndex:i];
NSDictionary *dic = self.dataSoure[i];
NSString *title = dic[@"title"];
[rowController.titleLabel setText:title];
}
}
else{
//@"RowType2Controller"就是上边提到的Table Row Controller 的Identifier属性
[self.table setNumberOfRows:1 withRowType:@"RowType2Controller"];
RowType2Controller *rowController = [self.table rowControllerAtIndex:0];
[rowController.titleLabel setText:@"登录"];
}
}];
AppDelegate
- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void (^)(NSDictionary *))reply
{
if ([userInfo count] > 0) {
NSString *actionString = [userInfo objectForKey:@"action"];
EaseMob *easemob = [EaseMob sharedInstance];
if ([actionString isEqualToString:@"isLogined"]) {
reply(@{@"isLogined":[NSNumber numberWithBool:[easemob.chatManager isLoggedIn]]});
}
}
4、获取到了数据,接下来要调用Watch App显示数据了。
显示数据主要用到了WKInterfaceTable。WKInterfaceTable相对于UITableView而言,能调用的接口少的可怜
WKInterfaceTable.h
WK_CLASS_AVAILABLE_IOS(8_2)
@interface WKInterfaceTable : WKInterfaceObject
- (void)setRowTypes:(NSArray *)rowTypes; // row names. size of array is number of rows
- (void)setNumberOfRows:(NSInteger)numberOfRows withRowType:(NSString *)rowType; // repeating row name
@property(nonatomic,readonly) NSInteger numberOfRows;
- (id)rowControllerAtIndex:(NSInteger)index;
- (void)insertRowsAtIndexes:(NSIndexSet *)rows withRowType:(NSString *)rowType;
- (void)removeRowsAtIndexes:(NSIndexSet *)rows;
- (void)scrollToRowAtIndex:(NSInteger)index;
@end
WKInterfaceController中
上一步中的代码示例已经给出了WKInterfaceTable使用方式,具体代码请看demo。
5、每个单独的页面都写好了,现在要让他们动起来。
WatchKit提供了三类页面导航方式。
第一种UINavigationController 控制的类似栈的导航方式,相应接口
- (void)pushControllerWithName:(NSString *)name context:(id)context; // context passed to child controller via initWithContext:
- (void)popController;
- (void)popToRootController;
第二种 modal 形式,相应接口
- (void)presentControllerWithName:(NSString *)name context:(id)context; // modal presentation - (void)dismissController;
第三种 类似 UIPageController 的分页式导航,相应接口
- (void)presentControllerWithNames:(NSArray *)names contexts:(NSArray *)contexts; // modal presentation of paged controllers. contexts matched to controllers - (void)becomeCurrentPage;
其中的“WithName(s):”参数就是每个控件在storyboard中设置的Identifier属性。
好了,就先写这么多吧,后期有时间会继续补充。
作者: 谢雅杰
环信SDK与Apple Watch的结合系列讲解(1)
环信SDK与Apple Watch的结合系列讲解(2)
收起阅读 »
环信SDK与Apple Watch的结合系列讲解(2)
这一篇主要是介绍怎么拖apple watch上的相关页面,附源码EMWatchOCDemo。
需要在工程中的“EMWatchOCDemo WatchKit App”中进行操作,该文件夹的结构如图
WatchKit几乎不允许直接coding页面,只能在storyboard上拖来拖去,对于我这种习惯直接coding页面的人来说,真真是极痛苦的。
一、确定apple watch上的操作流程
首先,我想要一个菜单页面,跟iPhone程序对应,在环信SDK未登录情况下,显示登录选项;在环信SDK登录情况下,有三项:会话,好友,群组。
然后,登录选项点击之后,能启动iPhone进行登录操作,因为watch的页面实在是太小了,没有键盘,环信SDK也不支持指纹或者声音登录。
再然后,登录操作成功之后菜单页面显示三项选项,每项点击都可以进入对应页面,显示相应的数据。
最后,进入聊天页面,可以显示已有的聊天记录,可以发送表情。本来想加入发送心跳的功能,但是发现WatchKit不支持watch app获取硬件传感器参数,而且找到了一篇不错的自问自答文章http://www.cocoachina.com/ios/20150323/11396.html
二、在storyboard上拖来拖去
1、点击文件夹下的interface.storyboard文件,右边会出现相应的视图显示。根据第一步的思路,直接拖4个Interface Controller, duang~,duang~,duang~, duang~,就成这个样子了
每个Interface Controller的Identifier属性强烈建议写上,为什么请见第3章。为了便于区分每个controller,个人习惯给每个Interface Controller都填写Title属性。
2、想了一下菜单,会话,好友,群组的功能,显示用table是再好不过的了,直接再拖table到每个controller,然后你就会发现一些奇怪的东西,比如这样的
Table Row Controller是类似于自定义UITableViewCell的东西,默认是继承于NSObject。
Group应该算是个新概念,它的作用是将页面上的控件分块。WatchKit不支持设置frame,目前我知道的,只是支持在横向上“左中右”,在纵向上“上中下”,支持设置大小。如果你想实现一个九宫格样式的页面,比较讨巧的方式是直接用三行Table Row Controller,每行三个button。不讨巧的方法,抱歉,我发现不管怎样排列组合这些属性,都出不来九宫格。
3、在会话页面显示和谁聊天的username,在好友页面显示好友的username,在群组页面显示群组的名字,用Label通通能搞定。
我在group里拖一个label,默认就是这个样子
自己选下不同的选项,看下效果,应该就能理解了。
4、聊天页面比较特殊,需要自己发送的在左边显示,对方发的在右边显示。不过这个很容易实现,只需要发送的和接收的放在两个Table Row Controller里,然后设置空间的Position即可,示意图:
拖完上边这些,页面就差不多了 。在Scheme中选择EMWatchOCDemo WatchKit App,编译 OK,可以进行第3章了。
环信SDK与Apple Watch的结合系列讲解(1)
环信SDK与Apple Watch的结合系列讲解(3) 收起阅读 »
需要在工程中的“EMWatchOCDemo WatchKit App”中进行操作,该文件夹的结构如图
WatchKit几乎不允许直接coding页面,只能在storyboard上拖来拖去,对于我这种习惯直接coding页面的人来说,真真是极痛苦的。
一、确定apple watch上的操作流程
首先,我想要一个菜单页面,跟iPhone程序对应,在环信SDK未登录情况下,显示登录选项;在环信SDK登录情况下,有三项:会话,好友,群组。
然后,登录选项点击之后,能启动iPhone进行登录操作,因为watch的页面实在是太小了,没有键盘,环信SDK也不支持指纹或者声音登录。
再然后,登录操作成功之后菜单页面显示三项选项,每项点击都可以进入对应页面,显示相应的数据。
最后,进入聊天页面,可以显示已有的聊天记录,可以发送表情。本来想加入发送心跳的功能,但是发现WatchKit不支持watch app获取硬件传感器参数,而且找到了一篇不错的自问自答文章http://www.cocoachina.com/ios/20150323/11396.html
二、在storyboard上拖来拖去
1、点击文件夹下的interface.storyboard文件,右边会出现相应的视图显示。根据第一步的思路,直接拖4个Interface Controller, duang~,duang~,duang~, duang~,就成这个样子了
每个Interface Controller的Identifier属性强烈建议写上,为什么请见第3章。为了便于区分每个controller,个人习惯给每个Interface Controller都填写Title属性。
2、想了一下菜单,会话,好友,群组的功能,显示用table是再好不过的了,直接再拖table到每个controller,然后你就会发现一些奇怪的东西,比如这样的
Table Row Controller是类似于自定义UITableViewCell的东西,默认是继承于NSObject。
Group应该算是个新概念,它的作用是将页面上的控件分块。WatchKit不支持设置frame,目前我知道的,只是支持在横向上“左中右”,在纵向上“上中下”,支持设置大小。如果你想实现一个九宫格样式的页面,比较讨巧的方式是直接用三行Table Row Controller,每行三个button。不讨巧的方法,抱歉,我发现不管怎样排列组合这些属性,都出不来九宫格。
3、在会话页面显示和谁聊天的username,在好友页面显示好友的username,在群组页面显示群组的名字,用Label通通能搞定。
我在group里拖一个label,默认就是这个样子
自己选下不同的选项,看下效果,应该就能理解了。
4、聊天页面比较特殊,需要自己发送的在左边显示,对方发的在右边显示。不过这个很容易实现,只需要发送的和接收的放在两个Table Row Controller里,然后设置空间的Position即可,示意图:
拖完上边这些,页面就差不多了 。在Scheme中选择EMWatchOCDemo WatchKit App,编译 OK,可以进行第3章了。
环信SDK与Apple Watch的结合系列讲解(1)
环信SDK与Apple Watch的结合系列讲解(3) 收起阅读 »