注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

WorktileCTO揭秘:团队协作工具Worktile技术架构

Worktile自上线两年多以来,以良好的用户体验和稳定的服务,获得了用户的认可和喜爱。截止笔者写这篇文章的时候,已经有超过10万家团队在使用Worktile。作为团队协作工具,从技术上分析首先要解决如下几个问题: 基于Web的跨平台设计,让用户在任何地方...
继续阅读 »
Worktile自上线两年多以来,以良好的用户体验和稳定的服务,获得了用户的认可和喜爱。截止笔者写这篇文章的时候,已经有超过10万家团队在使用Worktile。作为团队协作工具,从技术上分析首先要解决如下几个问题:

  • 基于Web的跨平台设计,让用户在任何地方都可以随时通过浏览器访问

  • Web形态的产品要具有原生客户端的体验,如任务的拖拽等

  • 具有高效的实时消息系统,每个团队成员在Worktile中所做的任何操作,都要实时在其他成员的客户端中自动刷新

  • 服务要稳定,稳定压倒一切



那么Worktile是如何做到这几点的?今天笔者在这篇文章里一一为大家揭秘。

SPA设计

先来说说Worktile中SPA(单页应用程序)设计,作为团队协作工具,需要尽可能减少用户在不同页面之间的跳转,所以从一开始我们就决定Worktile必须是单页应用程序,当时面临的选择有很多,首先我们考虑使用大名鼎鼎的Backbone.js,但是很快又抛弃了,因为在实际使用中Backbone.js太复杂,另一方面开发效率太低,最终我们选择了Google出品的AngularJs,下面这幅图是AngularJS的结构图:


1.jpg


选择它主要基于以下几点考虑:

1. 自动化双向数据绑定功能,这一点在Worktile中非常重要,如任务的状态变化都要实时变更到其他成员,如果具有自动化双向数据绑定功能,只需要绑定到UI的数据源发生变化,UI会自动发生改变,不需要工程师再通过代码去修改UI元素的改变,如下面这段代码:
ng-class="{1:'task-completed-style'}[task.completed]">
id="task_check_{{ task.tid }}"
wt-click="js_complete_task($event, entry, task)">


{{task.name}}
2. 语义化标签,AngularJS在设计之初信奉的理念就是:当编写UI的同时又需要编写业务逻辑时,声明式的代码远比命令式代码要好,命令式的代码更适合写业务逻辑,AngularJS在设计上就通过语义化的标签把对DOM元素的操作和逻辑代码分离,如我们需要展现一个任务列表,只需要下面这段代码即可:

class="slide-trigger"
hide_action="true"
ng-click="locator.openTask(task.pid, task.tid)">

3. 模块化设计,AngularJS堪称模块化设计方面的典范,通过模块化设计我们可以非常好的实现Worktile的工程化,在Worktile中涉及的元素非常多,如有项目、任务、日程、文件、话题、文档等等,而这每一个元素都可以设计为一个模块,如下所示:
(function () {
'use strict';
angular.module('wtApp', [
'wt.project.ctrl',
'wt.team.ctrl',
'wt.task.ctrl',
'wt.event.ctrl',
'wt.post.ctrl',
'wt.file.ctrl',
'wt.page.ctrl',
'wt.mail.ctrl'
]);
}());
4. 引入依赖注入,依赖注入是面向对象中比较成熟的设计模式之一,为了解决面向对象中依赖问题,得到了广泛的应用,AngularJS中大胆使用了依赖注入,极大的减少了各个模块之间的依赖问题:
taskListCtrl.$inject = ['$scope', '$stateParams', 
'$rootScope', '$popbox',
'$location', '$timeout',
'bus', 'globalDataContext',
'locator'];
结合以上特点,我们最终决定了前端框架使用AngularJS来实现,从Worktile上线两年多的表现来看,我们的选择无疑是正确的。当然AngularJS也有一些缺点,在实际使用中还是要根据具体的产品类型来选择使用,另外AngularJS 2.0也已经初见端倪,和AngularJS 1.0有很大的不同,感兴趣的同学可以先去尝鲜一下。
 
服务设计

我们再来看看Worktile的后台服务设计,Worktile的整体服务架构设计如下图所示:


2.jpg



其中前端部分在上面的SPA一节中我们已经说过了,下面一一分析下其他的服务:

1.  API服务,包括Web API、Mobile API、Open API,这些都运行于NodeJS之上,选用NodeJS的原因主要是它的异步事件驱动,对于高并发的支持比较好,另外一个原因是使用简单,对于前后端可以使用同一门语言去开发。

2.  缓存和队列服务,Worktile中的缓存和队列服务都是基于Redis来实现,Redis是一款非常优秀的开源缓存服务,并且可以选择基于内存还是进行数据持久化,它提供的pub/sub模型对于Worktile来说非常重要,对于一些实时性要求不高的处理,我们都是在Redis中pub一条消息,告知其他服务有数据发生了变化,那些服务在接收到Redis中的消息后,根据消息的类型决定应该如何做出处理。

3.  数据库服务,Worktile产品本身的特点决定了它是一个对实时性和性能的要求,远超过对事务性要求的产品,所以在选择数据库时,我们选用了MongoDB数据库,性能高,集群方便,数据以BSON结构存储,和Node.js天生完美结合。

4.  文件预览服务,使用Worktile的同学肯定知道在Worktile中所有的文件都可以做到无需下载到本地,而直接在线查看,这一切都是预览服务的功劳,因为文件类型的各种各样,在实现文件预览时也要根据文件的类型做出不同的处理,针对txt、pdf、代码片段等文本型的文件,我们只需要读取文件中的内容,然后再前端用相应的视图展现出来即可,相对比较简单。但是对于Office类型的文件,如ppt、doc、xls等文件,就不能这么简单的处理,我们希望文件在Worktile中查看的效果和用户在本地使用Word、Excel、PowerPoint查看的效果差不多,经过我们的调研,最终选用了微软官方提供的Office Web App服务。

消息推送

消息推送服务是Worktile最核心的服务之一,前面提到过作为一款团队协作工具,要能够实现非常好的实时性,任何数据的变化都需要及时变更到团队所有成员当前所在的视图,如下面这幅图,是一个典型的任务看板,团队所有成员可能同时在操作当前项目中的任务,每个操作引起看板的变化都会实时更新,不需要用户做任何刷新操作:


3.jpg


为了达到这种效果,需要在Web客户端和服务器之间维持一个长连接,当有任何改变发生时,给客户端发送不同的消息,告知客户端哪些数据发生了变化,如下面是我们为任务定义的消息中的其中几个:

实现实时消息推送,有以下几种方式可供选择:
on_task_trash            : "on_task_trash",
on_task_complete : "on_task_complete",
on_task_move : "on_task_move",
on_task_update : "on_task_update",
on_task_comment : "on_task_comment",
on_task_badges_file : "on_task_badges_file",
on_task_unarchived : "on_task_unarchived",
on_task_badges_check : "on_task_badges_check"
1.  短轮询,页面端通过js定时异步刷新,这种方式优点在于实现简单,但实时效果较差。

2.  长轮询。页面端通过js异步请求服务端,服务端在接收到请求后,如果该次请求没有数据,则挂起这次请求,直到有数据到达或时间片(服务端设定)到,则返回本次请求,客户端接着下一次请求,这种方式对于服务的要求较高,尤其在并发量很大的情况下,对服务端的压力很大。

3.  Websocket。浏览器通过websocket协议连接服务端,实现了浏览器和服务器端的全双工通信。需要服务端和浏览器都支持websocket协议。

在Worktile一开始我们选用了Socket.IO作为消息服务,但是随着访问量的增大,需要做集群化的时候感觉到力不从心,尤其对于Socket.IO状态数据的存储,由于并没有官方的解决方案,当时我们采用了一个第三方的开源项目,使用Redis来存储,引起了一些性能上的问题,在后来重构时选用了基于Erlang语言的开源XMPP服务ejabberd作为我们的消息服务。

ejabberd是xmpp协议的一种实现, xmpp广泛应用于即时通信领域。Xmpp协议的实现有很多种,比如java的openfire,但相较其他实现,ejabberd的并发性能无疑使最优秀的。Xmpp协议的前身是jabber协议,早期的jabber协议主要包括在线状态(presence)、好友花名册(roster)、IQ(Info/Query)几个部分。现在jabber已经成为rfc的官方标准,如rfc2799, rfc4622, rfc6121,以及xmpp的扩展协议(xep)。Worktile就是基于XEP-0124、XEP-0206定义的BOSH扩展协议。

由于自身业务的需要,我们对ejabberd的用户认证和好友列表模块的源码进行修改,通过redis保存用户的在线状态,而不是mnesia和mysql。另外好友这块我们是从已有的数据库中(mongodb)中获取Worktile中项目或团队的成员。Web端通过strophe.js来连接(http-bind),strophe.js可以以长轮询和websocket两种方式来连接,由于ejabberd还没有好的websocket的实现,就采用了BOSH的方式模拟长连接。整个系统的结构如下:


4.jpg


 
作者:李会军 收起阅读 »

值得推荐:理解 JavaScript 的原型链和继承

instanceof 运算符可以用来判断某个构造函数的prototype属性是否存在另外一个要检测对象的原型链上1   什么意思呢? 来个题Function instanceof Object;用高中数学的话就是把x,y代入公式得: insta...
继续阅读 »
instanceof 运算符可以用来判断某个构造函数的prototype属性是否存在另外一个要检测对象的原型链上1
 
什么意思呢?

来个题Function instanceof Object;用高中数学的话就是把x,y代入公式得:

instanceof 运算符可以用来判断Object的 prototype属性 是否存在Function的 原型链 上。
等等,斜体字的这俩到底是什么鬼意思?

prototype属性是原型链吗?

JS是基于原型链面向对象语言,也就是说所有对象都是以对象为模板创建实例的。如果是其他oo语言的背景比如Java或Ruby,都习惯于创建一个class模板,class创建object实例。比如ruby:
  class A
def initialize name @name = name end
def to_s
@name
end
endputs A.new('hehe') # => hehe
这里的类A就是所有 A.new 创建出来的实例的模板而已。而对于原型链语言JS来说,同意的事情要这样做
function A(name){  this.name = name
}
A.prototype.toString = function(){ return this.name
}var a = new A('hehe')
console.log('object name is:' + new A ('hehe')) // => object name is: hehe


  • 这里的怪怪的函数其实就是constructor,相当于ruby例子里的initialize

  • 而prototype上的方法toString也就是类似class模板上的方法。


为什么要把方法绑到prototype上?直接 A.toString…= 不行吗?
在解释prototype之前,先解释一下 new A 到底发生了什么2:
1: // var a = new A('hehe') =>2: var a = new Object();3: a.__proto__ = A.prototype; (proto)4: A.call(a, 'hehe');
其中 A.call 的意思是先把A的this设置为a,然后执行A的body也就是this.name=name

但是 __proto__ 又是什么

__proto__ 才是原型链

__proto__ 是内部 [ [Prototype ]] (说了半天原型链这就是牛逼闪闪的 原型链, 指向对象或者null)的getter和setter方法(已加入ES6规范3,但是还是建议只使用Object.getPrototypeOf())

JS对象能使用它原型链对象的所有方法,比如所有的对象的原型链(的原型链的原型链的原型链…)都最终会指向Object(或null)。因此,所有的对象都能使用Object.prototype上的方法,比如我之前覆盖掉的 toString 本身就是Object.prototype上的方法,如果没有覆盖,它是可以拿到所有Object上的方法的:
a.toString === A.prototype.toString // truea.toLocalString === Object.prototype.toLocalString // truea.__proto__ === A.prototype // true
所以,现在是否可以理解这句话了呢
instanceof 运算符可以用来判断Object的 prototype属性 是否存在Function的 原型链 上。

所以instanceof其实就是
Function.__proto__ === Object.prototype// false
擦,假设失败了呢,让我们来看看为什么不对,Function.__proto__到底指哪去了
Function.__proto__ === Function.prototype//true
原来指向自己的prototype了呢,那就意味着…
Function instanceof Function//true
yes,然而 Function instanceof Object似乎也能解释了
Function.__proto__ === Function.prototypeFunction.__proto__.__proto__ === Object.prototype
所以如果我们让
Function.__proto__.__proto__ = nullFunction instanceof Object//false
这回知道为什么不要用 __proto__ 了吧,一不小心重写了会导致所有继承自它的对象都受影响。
为了养成良好的习惯,实际项目最好使用 getPrototypeOf 取原型链,这里只是为了方便我采用__proto__
下面来看第二个题
Object instanceof Function
难道可以互相链吗?这意味着
Object.__proto__ === Function.prototype// true// 但是Firefox取不到Object.__proto__, 看来做了保护,必须要用// Object.getPrototypeOf(Object) === Function.prototype
要晕了, 忍不住要画个图


1.png

多简单呢,一共就分别有两类:

原型链指向Function.prototype的函数们

原型链指向Object.propotype的对象们

而原型链顶端的Object.prototype就再没有原型链了,所以是空

现在再回头看题目是不是so easy了。

也没什么卵用得 contructor

如果你好奇的在FireFox Console中看一下 a 除了刚才那些玩意,还有一个奇怪的东西


2.png

话说 A 里面这个constructor是个什么鬼,我们来玩它一下
a.constructor === A.prototype.constructor
A.prototype.constructor === A
A.prototype.constructor = nulla.constructor // => nulla instanceof A // true
这只是函数都有的一个玩意而已, 由于js的函数可以作为构造器,也就是可以 new ,所以所有的 函数的prototype.constructor都指向自己,因此所有的 new 出来的对象也都有一个reference能找到自己的构造器。

然而除了这个功能也并没有什么卵用嘛。

真的是这样吗?
.

..



….

…..

……

…….
恩,真的!

Bonus 继承

下面这个是babel 从es6 class
class A{
constructor(name) { this.name= name
}
toString() { return this.name
}
}class B extends A {
toString(){ return this.name + 'b'
}
}
编译出来的ES5继承
function _inherits(subClass, superClass) { 
// 密}var A = (function () { function A(name) { this.name = name;
}

A.prototype.toString = function toString() { return this.name;
}; return A;
})();var B = (function (_A) { function B() { if (_A != null) {
_A.apply(this, arguments);
}
}

_inherits(B, _A);

B.prototype.toString = function toString() { return this.name + 'b';
}; return B;
})(A);
其他地方都不用看了,inherits 函数用到了之前学到的所有玩意,要求实现要满足下列所有的cases,就当是课后练习了:
var a= new A('A');var b= new B('B');
a.constructor === A &&
b.constructor === B &&
a instanceof A &&
b instanceof A &&
b instanceof B


  • https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/instanceof

  • 这里只是意思,但是如果真的改变 __proto__ 是非常低效的https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/proto

  • http://www.ecma-international.org/ecma-262/6.0/#sec-additional-properties-of-the-object.prototype-object



Author: Jichao Ouyang 收起阅读 »

不算“真正的语言”?详说Swift 2.0中的错误处理

苹果公司在今年的全球开发者大会(Worldwide Developers Conference, WWDC)上宣布推出Swift2.0,该语言的首席架构师Chris Lattner表示,Swift 2.0主要在语言基本语法、安全性和格式美观度这三方面进行了改进...
继续阅读 »
苹果公司在今年的全球开发者大会(Worldwide Developers Conference, WWDC)上宣布推出Swift2.0,该语言的首席架构师Chris Lattner表示,Swift 2.0主要在语言基本语法、安全性和格式美观度这三方面进行了改进。除了这些新的功能特性,还有对语法的优化、修饰及美化,最后是Swift 1.x中最具影响力的错误处理机制。
 
历史一瞬:不起眼的开端

我们都知道,Swift语言作为Objective-C当前替代语言被推出,是OS X和iOS应用程序开发的“通用语”。在最初的版本中,Objective-C没有原生的异常处理机制。后来通过添加NSException类,还有 NS_DURING, NS_HANDLER和 NS_ENDHANDLER宏才有了异常处理。这种方案现在被称为“经典的异常处理”,还有这些宏都是基于setjmp()和longjmp()这两个C语言函数的。

异常捕获(exception-catching)看起来如下所示,在NS_DURING和NS_HANDLER宏之间抛出的任何异常都将会导致在NS_HANDLER和NS_ENDHANDLER宏之间执行相应的代码。
NS_DURING  
2. // Call a dangerous method or function that raises an exception:
3. [obj someRiskyMethod];
4.NS_HANDLER
5. NSLog(@"Oh no!");
6. [anotherObj makeItRight];
7.NS_ENDHANDLER
下面是立刻能触发抛出异常的方法(现在仍然可用):

[cpp] view plaincopy
1.- (void)someRiskyMethod
2.{
3. [NSException raise:@"Kablam"
4. format:@"This method is not implemented yet. Do not call!"];
5.}
可以想象,这种手工处理异常的方式戏弄的是早期Cocoa框架程序开发人员。但是这些程序员还不至于到这份儿上,因为他们很少使用这种方式。无论在Cocoa还是Cocoa Touch框架下,异常通常都被归为灾难性的,不可恢复的错误,比如程序员造成的错误。上面的-someRiskyMethod就是很好的例子,由于实现部分没有准备好而引发了异常。在Cocoa和Cocoa Touch框架中,可恢复的错误由稍后讨论的NSError类来处理。

原生的异常处理

我想由于Objective-C中的经典异常处理机制对应的手工处理方式让人感觉闹心,于是苹果公司在Mac OS X 10.3(2003年10月)中发布了原生的异常处理机制,彼时还没有iOS系统。这本质上是将C++的异常处理嫁接到了Objective-C。异常处理的结构目前看起来是这样的:
@try {  
2. [obj someRiskyMethod];
3.}
4.@catch (SomeClass *exception) {
5. // Handle the error.
6. // Can use the exception object to gather information.
7.}
8.@catch (SomeOtherClass *exception) {
9. // ...
10.}
11.@catch (id allTheRest) {
12. // ...
13.}
14.@finally {
15. // Code that is executed whether an exception is thrown or not.
16. // Use for cleanup.
17.}
原生的异常处理使你有机会为每个异常类型指定不同@catch部分。无论@try结果如何,@finally都要执行其对应的代码。

尽管原生的异常处理如所预期的那样抛出一个NSException异常,但是最明确的方法还是“@throw ;”语句。通常你抛出的是NSException实例,但说不定什么对象会被抛出。

NSError

尽管Objective-C原生与经典的异常处理有许多优点,但Cocoa和Cocoa Touch框架应用程序开发人员仍然很少使用异常,而是限制程序出现程序员所导致的不可恢复的错误。使用NSError类处理可恢复的错误,这种方法早于使用异常处理。Swift 1.x也继承了NSError的样式。

在Swift 1.x中,Cocoa和Cocoa Touch的方法和函数可能不会返回一个布尔类型的false或者nil来表示一个失败(failure)的对象。另外,NSErrorPointer对象会被当作一个参数返回特定的失败信息。下面是个典型的例子:

cpp] view plaincopy
1.// A local variable to store an error object if one comes back:
2.var error: NSError?
3.// success is a Bool:
4.let success = someString.writeToURL(someURL,
5. atomically: true,
6. encoding: NSUTF8StringEncoding,
7. error: &error)
8.if !success {
9. // Log information about the error:
10. println("Error writing to URL: \(error!)")
11.}
程序员所导致的错误可以用Swift标准库(Swift Standard Library)函数fatalError("Error message”)来标记,将其在控制台记录为错误消息并无条件中止执行。还可以使用assert(), assertionFailure(), precondition()和preconditionFailure()这些函数。

Swift第一次发布时,一些非苹果平台开发人员已经准备好了火把和干草叉。他们声称Swift不能算是“真正的语言”,因为它缺乏异常处理。但是,Cocoa和Cocoa Touch社区对此不予理睬,我们知道NSError和NSException那个时候就存在了。就我个人而言,我相信苹果公司仍然在思考实现错误和异常处理的正确方式。我还认为直到问题解决了,苹果公司才会公开Swift源码。这一切问题在Swift 2.0中全被扫清了。 
 
Swift 2.0中的错误处理

在Swift 2.0中,如果想要抛出错误,那么抛出的对象必须符合ErrorType协议。可能正如你所愿,NSError就符合该协议。枚举在这里用来给错误进行分类。
enum AwfulError: ErrorType {  
2. case Bad
3. case Worse
4. case Terrible
5.}
然后如果一个可能抛出一个或多个错误的函数或方法会被throws关键字标记:
func doDangerousStuff() throws -> SomeObject {  
2. // If something bad happens throw the error:
3. throw AwfulError.Bad
4.
5. // If something worse happens, throw another error:
6. throw AwfulError.Worse
7.
8. // If something terrible happens, you know what to do:
9. throw AwfulError.Terrible
10.
11. // If you made it here, you can return:
12. return SomeObject()
13.}
为了捕获错误,新型的do-catch语句出现了:
do {  
2. let theResult = try obj.doDangerousStuff()
3.}
4.catch AwfulError.Bad {
5. // Deal with badness.
6.}
7.catch AwfulError.Worse {
8. // Deal with worseness.
9.}
10.catch AwfulError.Terrible {
11. // Deal with terribleness.
12.}
13.catch ErrorType {
14. // Unexpected error!
15.}
这个do-catch语句和switch语句有一些相似之处,被捕获的错误详尽无遗,因此你可以使用这种样式来捕获抛出的错误。还要注意关键字try的使用。它是为了明确地标示抛出的代码行,因此当阅读代码的时候,你能够立刻找到错误在哪里。

关键字try的变体是“try!”。这个关键字大概也适用于那些程序员导致的错误。如果使用“try!”标记一个被调用的抛出对象中的方法,你等于告诉编译器这个错误永远不会发生,并且你也不需要捕获它。如果该语句本身产生了错误(error),应用程序会停止执行,那么你就要开始调试了。
let theResult = try! obj.doDangerousStuff()  
与Cocoa和Cocoa Touch框架间的交互

现在的问题是,你如何在Swift 2.0中处理爷爷级的NSError API呢?苹果公司已经在Swift 2.0中为统一代码行为作了大量工作,并且已经为未来写入Swift的框架准备方法。Cocoa和Cocoa Touch中可以产生NSError实例的方法和函数有苹果公司的签名( signature),可以自动转换为Swift新的错误处理方式。

例如,这个NSString的构造器( initializer)在Swift 1.x中就有以下签名:
convenience init?(contentsOfFile path: String,  
2. encoding enc: UInt,
3. error error: NSErrorPointer)
注意:在Swift 2.0中,构造器不再被标记为failable,它并不需要NSErrorPointer来做参数,而是使用抛出异常的方式显式地指示潜在的失败。

下面的例子使用了这种新的签名:
do {  
2. let str = try NSString(contentsOfFile: "Foo.bar",
3. encoding: NSUTF8StringEncoding)
4.}
5.catch let error as NSError {
6. print(error.localizedDescription)
7.}
注意错误是如何被捕获的,并且如何被转换成了一个NSError实例,这样你就可以获取与其相似API的信息了。事实上,任何ErrorType类型的实例都可以转换成NSError类型。

最后说说@finally

细心的读者可能已经注意到,Swift 2.0引入了一个新的do-catch语句,而不是do-catch-finally。不管是否捕捉到错误的情况下,你如何指定必须运行的代码呢?为此,现在可以使用defer语句,用来推迟代码块的执行直到当前的作用域结束。
// Some scope:  
2.{
3. // Get some resource.
4.
5. defer {
6. // Release resource.
7. }
8.
9. // Do things with the resource.
10. // Possibly return early if an error occurs.
11.
12.} // Deferred code is executed at the end of the scope.
Swift 2.0将Cocoa和Cocoa Touch的错误处理机制凝聚为具有现代风格的用法,这是一项伟大的工作,也会使许多程序员倍感亲切。统一行为是不错的定位,会使Swift语言和其所继承的框架逐步发展。
 
文章来源:Big Nerd Ranch 收起阅读 »

大家怎么实现自定义表情的 有谁说说吗 我是这么实现的 不完美啊

但是不完美  有人有更好得方法吗  求分享
但是不完美  有人有更好得方法吗  求分享

全球扫货指南集成环信移动客服:订单量客单价双丰收

移动互联网时代,同时迎来了全民创业的时代,虽然这两者不能划上等号,但不能否认,移动互联网的普及,给全民创业提供了更多的创新灵感,也赋予了电商更多活力。笔者了解的一些电商,已完全摒弃PC端接入口,而只从移动端进行接入。 在传统大型平台电商的窗口已经被...
继续阅读 »
移动互联网时代,同时迎来了全民创业的时代,虽然这两者不能划上等号,但不能否认,移动互联网的普及,给全民创业提供了更多的创新灵感,也赋予了电商更多活力。笔者了解的一些电商,已完全摒弃PC端接入口,而只从移动端进行接入。


1.jpg


在传统大型平台电商的窗口已经被逐渐关闭的时候,新入场的玩家必须借助移动端和社交实现弯道超车,移动电子商务公司“全球扫货指南”便是中国移动互联网发展的其中一家受益企业,其市场总监左小禛便认为:电子商务的未来是移动电商,而移动电商必须重视移动客服。

移动电商的三个关键点

的确,百度无线数据报告显示,移动互联网创业者开发经验不足1年的人数比例为38.3%,个人开发者比例明显上升,同时11—20人团队开发比例下降,移动开发者生态从量变向质变迁移。此外,移动互联网创业者对商业模式的探索和创新尚无特定模式可以借鉴。上述状况下,移动电商创业者如何实现弯道超车?笔者认为,以下三个关键点需要重点把握:

1、转变思维,认清客户在哪?

移动互联网时代用户数的迅猛增长,已是必然趋势。用户数的急速增长,意味着巨大的机会,同时也意味着巨大的挑战,终端的小型化、多样化,接入方式的多样化都意味着我们处在全新的互联网生态环境中。

Gartner预测,到2017年年底,超过70%的交易将来自于移动端。2014年阿里双十一的现场监控显示,总成交额571亿其中移动端贡献了243亿,移动端交易量比例将有超过PC端的趋势。同时,京东2014年第四季度财报显示移动端的订单量占比接近40%。

因此,移动互联网时代,电商行业应转变思维,认清客户来源趋势,应在移动端的各个接口做好整体布局,无论是营销、服务还是流程,应进行相应梳理。

2、移动互联网的营销,社交为王!

但在移动互联时代,营销渠道越来越多。过去的营销是通过电视、报纸等渠道跟消费者沟通,但移动互联网时代则是分享的时代,免费思维已成为主流,为了让用户喜欢,愿意分享,消费者和商家会直接面对面进行免费试用与体验,通过各种活动进行赠予、使用,增强体验。这也使移动互联网时代的电商营销成本非常巨大。

移动互联网时代的营销除了普通的宣传、引导、试用、免费等传统方式外,更多的具有社交化属性,用户体验变得如此重要,即时通讯所具有的实时性、互动社交性让移动电商更具活力。甚至很多商家都搭建了基于兴趣社交的板块辅助自身电商平台,通过社区导购来降低用户的购买决策成本,在非标品等方面与传统大电商平台竞争获取优势实现弯道超车。

3、移动互联网的客户服务,体验为王!

营销的成功,只能带来一定的流量,但真正的交易与再次销售,则一定是产品本身具有的价值及客户服务所带给客户的体验。我们经常说,客户在哪,客户服务就要跟到哪。客户在移动端,移动端的客户服务如果不到位,不但订单丢失,客户丢失,甚至公司也可能被那些更重视移动端的用户服务体验的公司,更能迅速把握时代大趋势的公司所颠覆。一项研究报告指出,如果做不好客服,辛辛苦苦通过各种渠道引流到自己App、网站、平台、账号的客户,95%都会流失掉。

因此,电商已从PC端转战到了移动端,移动端有其特定的消费属性,无论是营销与服务,应从移动端的消费属性来研究。

移动电商的客服趋势

如果说电商的移动性趋势已得到行业认可,各个大、中型电子商务公司正在奋力奔跑在移动端,正在全力争夺消费者手机上不多的APP安装上。但笔者需要提醒大家的是:太多企业过于重视营销,而对客户服务有所忽视。

客户服务的重要性已不需要赘述,笔者认为移动电商客服有几个趋势将带动电商企业的发展:

1、 多渠道融合,尤其以移动端为主的客服请求将占主导

客户服务请求来源包括传统呼叫中心的语音、基于网页的实时在线聊天系统、工单系统、微博、微信等等。客户来源的多样性,让多渠道融合客服成为必须。另一方面,正如上文所述,如今的消费者正在远离PC和电话,消费者都去了移动端,所以客户服务将以移动端客服请求为主导。

2、 即时通讯(IM)将是最适合移动设备的客服服务形式

微信已成为我们最重要的日常沟通形式,界面简单易操作,不需要任何培训,人人都会使用,最重要的是,这是最适合移动设备的沟通方式。互联网女皇发布的《2015互联网趋势报告》也认为: IM不仅是世界上标准化程度最高的互联网产品,而且是世界上下载和使用量最大的互联网产品,也是用户交流体验最好的产品。异步又实时、个性化又主流、表达能力很强但又迅速等等。

传统400电话虽然也方便迅速,但笔者也经常因为400电话的无尽等待而挂机。虽然对12306这样的订票刚需企业影响不大,但如是电商企业,那客户流失率就太高了。因此,即时通讯式的客服形式非常适合移动设备,一个发送就能即时接送信息,按一个按钮就得到服务,可以秒级接通,并且可以得到7X24小时随时随地的解答。不用去填写工单,也不用在APP中遇到问题的时还要跳出APP外通过电话,邮件,微信,QQ等方式才能后被解决。

3、智能客服技术将得到大量应用

上文提到了即时通讯式的客服形式非常适合移动设备,移动互联网时代,客户手机24 小时随身携带的属性决定了传统5 X8小时的服务形式早已满足不了客户需求。客户需要实现一个发送就能即时接送信息,可以秒级接通,并且可以得到7X24小时随时随地的解答。我们可以想象的是,这个7X24小时随时随地的服务并不是企业派了足够多的人工座席在为客户提供服务,而是智能客服技术的一项应用。

智能客服技术是实时聊天的一种技术应用,因为即时通讯沟通方式最大的优势是可以一对多,即一个客服坐席可以同时和多个人聊天,使用自动菜单导航,语义分析,机器人智能应答,智能知识库等技术的大面积应用,“智能机器人+知识库”可以帮助回答80%的常见问题,可大量减少传统座席人员数。

移动电商客服的三大难点透析

通过笔者对移动电商客服的几个趋势的梳理,我们看到,移动电商的消费群体的特定性,让移动客服与传统客服具有很多的不一致性,移动客服如需要做精做透,以下流程是难点:
 1、 集中的质检系统

传统语音客服因为可以通过录音、检索等方式进行客户服务质量检测,移动客服的即时通讯性、多渠道接入等属性,使客服的质量把控具有一定的难度。

2、 交易行业的大数据积累与分析

大量的移动端客服数据与传统来自于呼叫中心、Web端、微博、微信的客服数据,如何进行统一接入、积累与分析,进行数据挖掘实现商业转化,这是较大的难点也是最重要的商机,客户趋动才是需求的起点,是创新的动力。

3、 即时客服需要即时的信息管理

移动互联网时代,服务的即时性也对管理的即时性提出了很高的要求。移动电子商务公司全球扫货指南市场总监左小禛便深有体会:“移动电商经常会定期或者不定期举行一些活动,活动举办期间,服务请求较平常可能会有十倍甚至更多倍的增长,这样的环境下,如何去调配座席人员?如果知道何种渠道来源的增长比例?服务过程中出现了哪些问题?哪些是共性问题等等。移动环境下,即时客服要求有即时的信息管理。我们采用了环信移动客服,解决了我们客服过程中的很多难题。“

全球扫货指南的移动客服初体验

全球扫货指南是全球首家时尚买手制购物平台。平台上有各类买手推荐的奢侈单品,产品的价格搜索引擎可以帮助用户一键比价,比完之后觉得划算就可以找靠谱买手下单购买。全球扫货指南作为交易平台,实行第三方担保机制,若买卖双方发生纠纷,提供先行赔付。在CEO肖宇眼里,随着这几年电商的蓬勃发展,用户线上消费习惯正在快速被培养。目前客单价千元以上的电商玩家并不多,另一方面用户的消费升级不断加速,奢侈品购买呈年轻化,因此在手机上消费的客单价越来越高,将会给轻奢市场带来巨大的机会。
 
据左小禛介绍,全球扫货指南接入了这种智能移动客服产品后,可以实现快速注册、根据体验指南一两分钟内即可对接商家完成会话体验,同时实现了跨平台多渠道接入:支持 App、微信公众账号、微博、网页等,均可以快速统一接入客户服务后台管理。

其次,满足了开放性与自定义信息:这种新型的移动客服实现了代码开源、UI开源,还提供多套UI模版,这样便于与全球扫货指南自身的APP快速集成,平滑接入。同时第三方集成功能也很强大,可与第三方工单、知识库、CRM 系统等进行扩展集成。

第三,富媒体消息交互体验:移动客服平台提供了基于IM技术的类似微信体验的友好富媒体消息交互,不仅可以实时收发文字、表情,还可以即时收发图片、位置、实时语音、还可自定义消息,大大方便了与客户的沟通交流。

第四,精准客户画像功能:移动客服的IM沟通帮助全球扫货指南辅助判断客户需求与诉求,通过自定义客户分类标签,客户再次访问时可获知客户的类型。并且还支持自定义会话小结,根据会话小结统计会话的分类,通过会话小结追踪客户诉求。通过轨迹分析功能,即通过发送客户访问页面的轨迹,判断客户意图,获取客户个性化细节,了解客户基础信息,分析客户行为,可以极大提高订单效率。

第五,“智能机器人+智能知识库”:作为一套智能化的客服系统,全球扫货指南建立了基于业务的智能知识库,智能辅助归结业务信息和应答客户信息,历史常见问题系统梳理,提高客服效率。“智能机器人+智能知识库”组合目前可自动回复80%常见问题,随着智能知识库的不断训练,这一比例能够提高到90%。

第六、真正实现不丢订单:环信基于IM长连接技术,帮助全球扫货指南实现超线拉取排队会话,让等待时间过长或有订单需求的客户得到优先服务,提升客服效率,即使客户关闭会话,退出App,甚至关闭手机,基于IM长连接技术依然能找回客户会话,提升客服效率,不丢客户订单。同时,基于IM长连接技术,即使在断网或复杂网络切换时也依然能找回客户会话,不丢客户订单。

第七、实现了质检系统透明管理:移动客服自带实时监控系统,可实时监控当前所有会话,管理员在线质检,在线查看会话信息,还提供了历史会话查询、质检考核。通过实时与历史查看,可以统计出客服代表的接入量、在线时间及应答等待时间。

通过销售部和技术部返回的数据报告显示,左小禛判断,通过部署这种新型智能移动云客服,较于传统客服产品节省成本约60%,省电省流量高达80%,同时留住了70%会话客户,订单量和客单价均获得了不同幅度提升。目前全球扫货指南覆盖全球100个顶级奢侈品品牌,超过10万个热门单品。根据贝恩咨询的数据,中国人去年一年消费奢侈品3800亿元,其中70%在海外消费,仅代购市场就超过750亿元,奢侈品的海淘电商无疑是块很诱人的蛋糕。

移动客服的魅力,从上述数据的发布可看出,已超出了大家的想象!原因在于:传统客服体系下,客服只是成本中心,辅助部门;而在移动互联网时候下,客服俨然已成为核心流程之一,竞争力关键要素之一。

一位客服领域大佬曾这样总结这个时代:“伴随着客户互动的革命,从呼叫中心演变到的客户中心作为不断扩展的客户交互平台概念一直经历着深刻的变革。运营日益规模化,精准化、多通路化、智能化和普适化。作为集语音和非语音渠道为一体,人工与智能服务共协同,物理与数字的通路大融合,随着以客户理念,人文为中心时代的来临,客户服务中心正从企业价值链的一测走向价值传递的中央,与惊艳的产品研发一起发挥着客户体验创造的核心角色。”

移动客服,你已不能再等待! 收起阅读 »

环信CEO:环信移动客服技术全解析

摘要:《近匠》第92期,在专注于为开发者提供移动端即时通讯解决方案的同时,环信也开始了在连接人与商业、物与物版块的IM生态布局。环信CEO刘俊彦深剖环信移动客服的核心技术,以及对开源与未来无所不在的客服场景畅想。上线一年多的时间,环信的团队已经扩大到一百多人,...
继续阅读 »
摘要:《近匠》第92期,在专注于为开发者提供移动端即时通讯解决方案的同时,环信也开始了在连接人与商业、物与物版块的IM生态布局。环信CEO刘俊彦深剖环信移动客服的核心技术,以及对开源与未来无所不在的客服场景畅想。上线一年多的时间,环信的团队已经扩大到一百多人,在专注于为开发者提供移动端即时通讯解决方案的同时,环信开始了在连接人与商业、物与物版块的IM生态布局。并于近期推出专为移动端打造的客服平台,与环信IM SDK共享核心代码。本期《近匠》专访环信CEO 刘俊彦,听他讲述打造环信移动客服的核心技术与挑战,以及环信对开源与未来无所不在的客服场景畅想。


1.jpg


图:环信CEO 刘俊彦

CSDN:最初决定打造这样一个移动客服平台有着哪些初衷和考虑?

刘俊彦:首先从现实说起,从去年上线之后,就不断地有海淘、生活O2O等领域的开发者表示,使用环信IM SDK来做手机App的内置客服通道还是很好用的,但希望环信同时还能够提供一个具备客服工作台、工单分配、KPI考核等功能的客服平台。其实我的第一反应是拒绝的,因为这好像与即时通讯本身并没有太大关系,但随着这方面用户需求的增长,我们便开始思考这个问题。

其次,与战略方面也有所契合,我们所希望的是能够通过环信将所有人、物都连接起来,但之前却只拘泥于连接人与人的社交,在环信的版图中缺少着连接人与商业、人和客户的版块。因此,我们决定开发移动客服系统,让用户和商家可以直接在应用中聊起来,打造出一个完整的IM生态蓝图。

CSDN:在环信的产品线中,移动客服平台有着什么样的定位?

刘俊彦:移动客服平台是一个在IM基础上的行业应用,在IM已经覆盖几亿用户之后,我们就在思考下一步做什么?如果一款产品或某项功能能够真真切切地帮助到我们的IM用户,体验更爽,更有粘性,我们都会进行尝试。其实我们做了很多类似的工作,比如最近上线的关键字检测功能,就是为用户提供IM之上的最后一公里的额外的服务。移动客服同样如此,移动客服的核心还是IM,但我们同时还提供全套的完整的客服工作后台,有客户服务支持需求的用户不需要任何开发就可以直接使用,因此,我们将移动客服定位为IM在客户服务行业的垂直应用。

CSDN:云客服模式其实早有应用,相比其他服务提供商,环信移动客服平台有着哪些特色及技术优势?

刘俊彦:当下的客服软件市场分为两部分,一是传统电话呼叫中心的客服,他们的沟通方式以语音沟通为主,第二类是非语音类的客服,一般归纳为新媒体客服。新媒体客服包含微信公众号、微博、网页、App等渠道来源。对网页端的客服渠道来说,其对客服服务器的并发技术要求并不高,因为通常是使用基于轮询的短连接技术。比如每天有十万人找客服咨询聊天,每人聊五分钟,那么平均到每秒钟也就最多2-3个人同时在聊天,服务器只需要支持每秒钟2到3个请求的并发,对服务器压力很小。但手机App客服就不一样了。从技术上来讲,好用的手机App客服技术要求手机与客服服务器之间保持着7x24的长连接。任何时候,只要用户不卸载你的App,客服就永远可以主动的去找到消费者,向他主动发送消息。举个例子,我们有的用户用这个技术做外呼营销。他先在自己的CRM系统里对他的用户挖掘打上标签,比如找到所有的2-4岁小孩的妈妈用户群体。然后向这类妈妈群体群发消息。发出去的促销消息在消费者的App端看来是一条推送过来的聊天消息,首先这个消息不会被错过。然后点击这个推送后就会进入到客服的一对一聊天页面。这样的促销消息发送方式因为是和真人客服之间一对一的双向交互,所以效果比那种单向的广告推送好很多,转化率非常高。类似这样的外呼营销方式,让客服部门从以前的成本中心变成了现在的营收中心。而这些营销方式,如果没有手机APP和客服服务器之间的IM长连接,是不可能做到的。

当然,IM长连接技术对服务器的技术能力以及成本压力也增大许多,因为哪怕一个用户从来没找过客服聊天,我们也要为这个用户在他的手机和服务器端之间维护一个长连接,这个长连接会7X24小时的运行着。通常有100万的月活,就意味着要维护100万的长连接。但这却是环信的优势所在。环信移动客服平台与其他客服提供商最大的区别就是在移动端的IM长连接技术。在这点上,环信基本是垄断地位。

CSDN:可否详解环信移动客服平台的智能应答系统在面对海量请求时,如何做到真正智能地屏蔽与提炼?

刘俊彦:全球有100亿设备24小时连在网上,他们随时随地都能通过移动设备内置的客服通道找到客服,所以移动端的客服要应对的请求可能是海量的。对客服部门来说,利用原本的人工客服团队再加上新型的智能机器人客服系统,可以让问题解决效率呈指数性增长。传统的聊天机器人技术是基于搜索的问答匹配,而环信的智能客服是最新的基于深度机器学习的技术,不仅可以根据上下文进行推理,还能对人工座席和客户的交流进行实时学习和训练,极大降低知识库的维护成本。

目前,智能聊天机器人技术最实际的应用场景是基于文字交互的客服系统,包括短信、微信公众号、App内置客服、网页在线客服等,不仅降低了原本一年四季24小时在线的客服中心成本,更让拥有极致用户体验的新一代客服中心成为可能。

但是,相比之下,人工服务永远是最智能、最好的服务,在产品设计上,应实现人工与智能服务的无缝切换,当智能客服回答比较勉为其难的时候,就直接切换到人工服务,这样才能为客户提供更好的体验。所以客服系统中的聊天机器人技术最重要的是做好人工座席和机器人座席之间的无缝高效的切换,而不用刻意追求智能机器人技术的智能程度。客服系统中的智能聊天机器人只需解决特定领域的知识问题,无需像微软小冰那样陪用户聊人生聊理想。

CSDN:在移动客服平台开发的过程中,遇到过哪些难点与挑战?如何解决的?

刘俊彦:对移动客服来说,有两个技术难点。首当其冲便是在手机上为客户提供服务,IM技术是最适合用来在手机上连接客户和商家的。IM长连接技术能保障客服随时随地主动联系到客户、断网或复杂网络切换时不丢客户,并对电量流量进行优化。同时,也必须让客服人员、商家使用起来非常爽,即客服工作台好不好用、能不能提高客服效率、是否能进行全方位的KPI考核等。

对客服而言,即时通讯可谓是重中之重,在纯社交领域,消息丢失或晚到可能不是太大问题,但客服不一样,如果一条消息丢失或延迟,可能就意味着会发生丢单、客户流失情况,商业损失会非常大。所以对于选购移动客服的商家来说,他们希望移动端的IM技术和支撑客服工作人员的客服后台技术能够由一个厂商来统一提供。而不是说需要客服后台用这家,即时通讯买那家,这样在使用移动客服产品过程中遇到任何问题,都可以通过一个厂商一次性地及时解决。

CSDN:在移动网络环境下,弱网络、丢消息、丢订单等问题天然存在,环信采取了哪些技术手段来解决这一难题?通过哪些机制确保做到真正的不丢消息、不丢客户订单,还能省电省流量?

刘俊彦:这是环信过去一年多一直在做的事情,其实大多数IM厂商也能保证做到,但这的确还存在一些技术难点。首先,如何保证不丢消息?就必须通过一个好的协议来实现,必须保证协议本身是完全可靠,一定不会丢消息的,而在电量、流量的节约方面则包含协议、实现等诸多层面的优化。当然,无论是电量、流量的节约还是保证消息及时到达,除了最根本的协议、架构等,还包括许多细节方面的长时间打磨,才能不断地进步。
举个例子,环信在去年针对用户需求进行了许多优化,其中有一点便是针对来自三四线城市使用2G网络的用户,在弱网络环境下发送图片速度非常慢的问题,进行了切片上传的优化,将一个50K大小的图片切成10片,开10个线程,同时上传,极大地提高了传送的成功率。

CSDN:将UI模板开源让开发者100%自定制是环信一直以来的做法,之前也上线了“更极客 更开源”的IM Geek开发者社区,并将SDK开源,请问环信在开源方面有着哪些具体的计划和措施?

刘俊彦:环信的创始成员都是做开源出身,现在有些人对开源软件存在误解,而我们从不认为开源软件就意味着不好、不安全、低质量,这点从Linux就可以显而易见。我曾在Redhat担任高级开发工程师的工作,现在,许多公司都在使用Linux,也没有人会给予Linux不安全、不好用等差评。我们所希望的,不是自己重新去发明轮子,而是站在前人的肩膀上,取其精华去其糟粕,来进行自己的开发,比如最初在设计环信的通讯协议时,我们就借鉴了一部分XMPP的核心模块精华。

但XMPP协议并非为移动端设计,XMPP出现时还没有移动设备。在PC时代,用户不需要考虑电量、流量等问题,并且也不存在弱网络导致消息丢失的问题。这也就表明,我们必须将其中不好的部分推倒,进行重新设计或重写,开发出适用于移动端的IM协议。当然,XMPP也拥有很多优点,它存在十年,有许多插件,并充分考虑到协议的扩展,它是经过许多人不断摸索制定出来的协议,拥有着非常高的价值。

在使用开源软件的同时,我们也会反哺回馈开源社区,环信公司内的技术团队现在至少有十多个人都是一些知名的开源软件的committer,一直都在为开源软件贡献力量。现在有些公司可能会担心用开源软件出了问题后不可控制,这说明他们并没有用真正开源人的精神去使用开源软件,对于存在瑕疵的地方,就应该去改写代码,让它变得更好,而不是直接弃之如敝履。在开发过程中,如果使用到存在问题的开源软件,我们要做的事情就是去进行修改完善,然后将代码提交回去,让整个开源社区都能收益,这是环信对每个工程师的要求。

无论是将UI模板100%开源还是上线IM Geek开发者社区,我们的真正目的就是为开源社区做贡献,而不只是环信的产品。其实在当下的国内外开源社区方面,IM和移动社交还是空白的存在。现在,很多人都在做朋友圈、附近的人、聊天、匿名聊天或群组等功能,但归纳起来大概只有二三十种模块,剩下的工作便是各种模块的组合及编组。所以我们的想法就是开发者不用自己再去劳心劳力地重新开发一些基础和常见模块,而是通过社区的力量将这些模块都开发并开源出来,甚至包括基于这些模块的完整的应用,统统开源。这样不仅能节省创业团队大量的时间精力,也能降低开发成本,将资金投入到更好的用户体验、设计及运营上。

要经营好开源社区就必须以身作则,环信自身也在不断地贡献开源模块。当然,开源社区光靠一时的热情是远远不够的,未来,环信会为乐于开源的committer提供经济上的资助,只要开源软件做得好,并将其回馈给社区,提供开放下载。我们非常欢迎有开源情怀的开发者投身到开源事业中。

CSDN:环信移动客服在安全及保护用户隐私数据等方面有着哪些具体措施?

刘俊彦:隐私一直是很多公司一直在意的问题,会担心商业数据、用户信息会不会被窃取?环信移动客服对这方面是非常注意的,不会以任何形式碰触用户的聊天记录,用户可以100%随时导出或删除。而在系统保障方面,环信到目前已经趟过了许多雷点,并且内部还有一个每月演习的硬性规定,以此来保障即使发生系统挂掉的情况,核心功能依然可用。此外,环信正在着手“异地多活”,以此来提供更可靠、安全的服务。

CSDN:移动客服系统如何收费?

刘俊彦:IM已成红海,而移动客服则是商业化的企业级服务,需要为企业出现丢单等情况负责。环信移动客服系统的收费标准分为免费和收费用户数量级,2个座席以内可免费使用,超过2个座席的公司,需按每年每座席1500元进行收费。


2.jpg


图:环信移动客服收费标准

CSDN:现在大部分客服请求均来自于移动端,环信对于未来的移动客户服务还有着哪些畅想?

刘俊彦:当下,客户服务软件行业正在经历一场变革,用户获得服务的渠道和行为在发生巨变。随着环信移动客服这样的产品的普及,用户的服务体验将有极大的提升,而依托于技术进步,商家为此投入的人力成本却呈下降趋势。环信已经实现了人与人、人与商业的连接,但其生态布局依然缺少一块,就是人与物、物与物的连接。物联网正在临近,客户服务更将呈现完全不同的场景。我们需要做什么?首先就是物与物之间实现通讯功能的实时数据交换,其次,人如何控制设备?第三,在物与物之上,人和人之间需要沟通。

比如,一个物联网冰箱发生故障后,用户肯定不会再打400电话了。用户只需直接点击冰箱内嵌的客服按钮,即可一键接通,不仅可以和客服视频聊天得到帮助,还可以将冰箱的相关参数发送给客服,快速解决问题。同时,未来的客服一定是内嵌型的,将无处不在,无论是App、智能家居还是车联网,一个按钮所有问题都能搞定。目前,环信已经在着手物联网IM产品研发。 收起阅读 »

App崩溃分析:如何监控http请求并做出优化?

移动互联网时代,移动App与服务器之间的交互越来越频繁,数据量也越来越大,伴随而来的各种网络连接问题也在影响着各App的留存率。如何保证http请求的质量成为开发者们需要解决的一大问题。本文着重讲述http请求遇到的问题以及该如何监控和优化。 http请...
继续阅读 »
移动互联网时代,移动App与服务器之间的交互越来越频繁,数据量也越来越大,伴随而来的各种网络连接问题也在影响着各App的留存率。如何保证http请求的质量成为开发者们需要解决的一大问题。本文着重讲述http请求遇到的问题以及该如何监控和优化。

http请求遇到的问题

在App开发过程中,通常是用无线网络去做测试,这样网络的连通率、速度以及响应时间都是处在一个相对理想的情况下,但是在App发布后,用户使用场景往往是2G、3G,以及一些网络水平初级的地区,网速和连通率都在一个不稳定甚至极低的水平,这时,一系列的问题随之而来,其中我们主要讨论两点:

  • 响应时间:由于网络速度的下降, 响应时间开始变长,同一个url的访问时间可能会成倍增长,增加用户的等待时间;

  • 错误率:由于网络质量的下降,丢包错包概率成倍增长,由于请求的错误也会导致服务器端处理错误率的提高,可能会造成返回数据为空或者错误,致使用户增加使用成本。



如何去优化?

应用向服务器发送http请求,一般都是调用的系统接口或者第三方接口(比如OKHttp):    

1.jpg

这个时候,我们可以在应用调用接口时,加一个收集模块来采集http信息,如图:


2.jpg

Collection module可以用不同的实现方式:iOS利用Runtime通过代码注入的方式去获取相关信息;Android通过自定义URLStreamHandler去获取http信息,之后通过一些统计工具可以直观地去分析和优化应用的网络模块。

如何利用网络监控去做优化?

那么,究竟该如何利用好网络监控来进行应用优化?开发者可以从http响应时间、http错误率、请求量(rpm)和Data I/O四个维度监控http请求,并监测每个url的运营商、终端设备、错误码。同时,地理定位功能可以提供每个地区网络状况的平均值,方便开发者去分析和优化。

  • http响应时间



通过响应时间的长短,可以判断哪个url去优化,如果响应时间长,是不是后台查询过慢?还是因为运营商基站建的少网络信号差?

  • http错误率



通过错误率,可以知道访问某个url时最多的错误码是多少,根据错误码去确定是请求错误还是服务器错误,从而缩小问题的查找范围。

  • 请求量(rpm)



通过查看url的请求量,可以去推断用户的喜好,从而做一些定制化的服务,也可以通过这个指标去确定App端发请求频率是否正常。

  • Data I/O



通过Data I/O,可以知道某个url的请求数据大小是否正常,是否流量过大可以进行压缩从而节省用户的费用。


3.jpg


当然,每个指标不是独立的,而是应该综合来判断。举一个例子,做一个在线图库的应用,用户反映一直刷不出图,那就可以这样推断一下:看http响应时间,如果正常,有可能是App收到数据后显示有问题,如果过长,那么可能是服务器端问题;再看错误率,如果很高,那么有可能是服务器返回结果有问题,如果不高,那么有可能是因为网络信号差导致的。

这个时候,再看一下请求量和Data I/O,如果请求量很小,但是Data I/O很高,那么是否可以优化http请求策略?如果改成每次请求一屏的数据,根据用户的翻页情况发送多次请求,而不是一次去请求几页数据,那么这样数据总量没变,但是用户看到的效果却是每翻一页,只要很少的时间就可以显示出图片,这样就可以提升用户体验。

简单论述了网络问题对于移动App产生的影响,以及如何去监控http请求并做出优化。当然,实现监控的方式有很多,也可以有很多技术上的玩法,但无论技术实现得有多好,如何优化App?提升用户体验和留存率,才是开发者最该关心的问题。
  收起阅读 »

一个社交App是如何构建高伸缩性的交互式系统

一个社交App需实现的功能 用户关注的常规社交功能、活动、地理位置、探索功能、新鲜事、视频照片分享等等,需要提供的功能不胜枚举,所以从技术角度来说,开发者需要解决的问题也是异常复杂的。 当一款社交App发布之初,用户访问量比较小,使用一台服务器就能...
继续阅读 »
一个社交App需实现的功能

用户关注的常规社交功能、活动、地理位置、探索功能、新鲜事、视频照片分享等等,需要提供的功能不胜枚举,所以从技术角度来说,开发者需要解决的问题也是异常复杂的。

当一款社交App发布之初,用户访问量比较小,使用一台服务器就能够支撑全部的访问压力和数据存储需求,但是互联网应用具有病毒式的传播特点。一款App很可能会面临一夜爆红的现象,访问量和数据量在短时间内呈现爆发式增长,这时候会面临的局面是每天上亿PV、数百万新增用户和活跃用户、流量飙升至每秒数百兆。这些对于一个只部署了简单后端架构的应用来讲是无法支撑的,会直接导致服务器响应缓慢甚至超时,以及在高峰期时服务呈现瘫痪状态,使得后端的服务完全无法使用,用户体验急剧下降。本文将会通过一个真实的案例来分享一个社交应用如何构建一个具备高伸缩性的后端系统。

社交App最初部署的后端架构解析

社交App在最初的时候,后端架构相对比较简单,最初是部署在基础网络之上。最前面放置一台绑定了公网IP的nginx服务器作负载均衡,后面放置3台应用服务器来负责处理所有业务上的请求,最后面搭建一台MySQL Database数据库。


1.jpg


 
构建私有网络

随着产品的不断迭代、用户数的持续增长、数据量的积累,App就需要改进自己的后端架构,即开始构建私有网络。用户可以使用私有网络构建自己的网络拓扑——创建路由器和私有网络,将后续加入的用于运行内部服务的主机放置在私用网络中,可以有效地和云平台其他用户主机,在网络上实现100%二层隔离。主机对外开放的仅仅只有80端口,这样系统安全性上多了一层保障。


2.jpg



在上面的架构图中,最前面的是防火墙,后面接负载均衡器,然后接路由器和私有网络,很多互联网应用都存在读多写少的情况,这个比例有时可以达到8:2,所以我们首先通过引入缓存分摊数据库读压力。其次,引入负载均衡器,替换最初架构中的nginx proxy,负责均衡器在这里其主要用于分发请求到后端多台应用服务器,,当其中一台应用服务器挂掉,负载均衡器可以进行自动隔离。

业务分区与扩展

App随着并发访问量和数据量不断增大,首先想到横向扩容Web服务。水平扩容业务服务器的前提是要保证每台服务器都是无状态的,将session信息下放到缓存或数据库中存储,保证请求被负载到任何一台服务器可以正常处理。


3.jpg



从上图中看到,在前一步「构建私有网络」之后,增加了一个新的私有网络来扩展网络层,这里可以利用自有映像功能,将原有的应用服务器制作成模板,后续就可以基于这个模板快速启动新的主机。另外可以利用Auto-scaling(自动横向扩展)功能,根据后端服务器的负载请求,动态调整服务器的数量。

一个社交应用的后端会提供很多服务请求接口,比如添加好友、刷新新鲜事、浏览页面等,可以通过日志分析每一个接口的耗时,将耗时长但非重要业务的请求分到单独的Web服务器上进行处理,从而给主Web服务器留出更多资源去处理关键业务的请求。

面向服务的架构

随着产品功能的不断迭代,业务代码会越来越复杂,出现故障的可能性也在加大,当一个局部功能出现问题时,都会影响整个服务的可用性。此时可以构建面向服务的架构,将一个完整且庞大的服务拆分为一个个的子服务,服务之间通过接口交互。如下图所示:


4.jpg



社交App的服务被拆分成了四个子服务——新鲜事(News Feed)、用户资料(Profile)、广告(Ads)和探索(Explore),不同的服务之间通过消息通信框架(例如ZeroMQ)来进行交互。把一个大服务拆分为几个小的子服务的好处不言而喻,主要是:

  • 故障隔离:子服务出现故障不会影响全局,比如广告业务出现问题并不会让整个App不能使用,依然可以查看新鲜事等;

  • 独立扩展:每一个被拆分出的子服务有着不同的访问压力,比如新鲜事的调用相比一些二级页面的用户资料要高很多,所以前者会被分配更多的Web 服务器;

  • 独立部署:一个大服务的配置因功能过多会异常复杂,一旦被拆分就可根据不同的特性需求定制配置项,从而提高可管理性;

  • 团队协作开发:开发者都有着自己精通的方向,从而提高开发效率;

  • 抽象出数据访问:在后续进行数据层面(数据库、缓存)扩展时,可通过修改子服务的Data Service,实现对下层数据的透明。



数据库Replication

业务增长也会给数据库带来诸多问题,当最初架构中单台数据库(数据库同时提供读和写)不足已支撑起App访问压力时,首先需要做数据副本Replication。市面上常见的MySQL、MongoDB等数据库都提供Replication功能,以MySQL为例,从高层来看,Replication可分成三步:

  • Master将改变记录到二进制日志(binary log)中(这些记录叫做二进制日志事件,binary log events);

  • Slave将Master的binary log events拷贝到它的中继日志(relay log);

  • Slave重做中继日志中的事件,将改变反映它自己的数据。



具体实现该过程的第一部分就是Master记录二进制日志。在每个事务更新数据完成之前,Master在二进制日志记录这些改变。MySQL将事务串行的写入二进制日志,即使事务中的语句都是交叉执行的。在事件写入二进制日志完成后,Master通知存储引擎提交事务。

下一步就是Slave将Master的binary log拷贝到它自己的中继日志。首先,Slave开始一个工作线程——I/O线程。I/O线程在Master上打开一个普通的连接,然后开始binlog dump process。Binlog dump process从Master的二进制日志中读取事件,如果已经跟上Master,它会睡眠并等待Master产生新的事件。I/O线程将这些事件写入中继日志。

SQL slave thread处理该过程的最后一步。SQL线程从中继日志读取事件,更新Slave的数据,使其与Master中的数据一致。只要该线程与I/O线程保持一致,中继日志通常会位于OS的缓存中,所以中继日志的开销很小。

此外,在Master中也有一个工作线程:和其它MySQL的连接一样,Slave在Master中打开一个连接也会使得Master开始一个线程。复制过程有一个很重要的限制——复制在Slave上是串行化的,也就是说Master上的并行更新操作不能在Slave上并行操作。

对于云计算使用者来说,只需要知道数据库的IP和端口即可进行使用。具体实现见下图:


5.jpg

第一步要做的是扩充Slave,将单机Master变成Master+3台Slave的架构,而在其中的Slave上搭建一个内网的负载均衡器(Load Balancer),对于最上层的Data Service来说,只要配置一个MySQL Master节点和一个LB节点即可,今后因业务变化进行增减Slave对上层来说完全是透明的。

此做法可以带来两个好处,第一是提高可用性,若是一台Master出现错误,则可以提升某一台的Slave作为Master继续提供服务,从而保证数据可用性;第二个是分摊读压力,对于一个社交App来说,读写分离是在数据层优化第一步要做的事情,利用上面的架构可以很轻易地做到将读的请求分担到MySQL Slave上进行查询,而写留给Master。但是读写分离时会有数据库一致性的问题,即在数据写至Master之后同步到Slave有一个延迟的时间,对于社交应用来说,这是可以接受的,只要保证数据的最终一致性即可。

在上图的最下面有一个Snapshot,即定期对数据进行冷备份,这不同于单纯对MySQL Master进行复制的Slave,因为线上bug或误操作会删除Master上的数据,这时会立即同步到slave上造成数据丢失这时冷备份Snapshot就会起到数据保护作用。

运行过程中肯定需要监控,用户可以利用Linux上的工具进行统计分析top / iotop / df / free / netstat等工具去监控系统里的各个服务和组件是否正常运行,以及通过日志的信息(http access log / application log / database slow log )分析各个服务的性能瓶颈。

数据分区与扩容

下一步业务的调整要进行数据库的分区和扩容。第一,构建缓存集群,在开始的架构中引用了Memcached缓存,是单机数据库缓存。当数据量增长,,需要把数据分散到多台缓存服务器上,常用的是HashRing算法,好处在于不管是添加结点还是删除结点时,只会使得少部分数据失效。还可以引用NoSQL数据库,这里用到了Redis把社交数据里对于关系要求不强但对查询效率要求很高的数据从MySQL里拿到Redis里存。Redis尤其适合存储列表类数据,比如好友关系列表、排行榜数据等。


6.jpg

除此以外可以考虑做数据分区对于MySQL第一步是垂直拆分,把原来单独的数据库按照功能模块分别拆分成:好友新鲜事、用户资料、广告数据以及探索数据。对于Redis也同样,将原来的单台Redis按照功能模块拆成四个,分别为:排行榜数据、好友、广告数据、探索数据。

接下来会遇到的瓶颈是单表过大的问题,这时候我们需要做水平拆分——把一个表拆分成多个表,需要选取一个分区Key,比如对用户表做拆分时,通常选取User ID。分区key的选择主要是看所有的查询语句频繁使用哪个查询字段,就选择那个字段作为分区key这样能保证大部分的查询可以落在单个数据表上,少量没有带分区Key的查询语句,可能要遍历一遍所有切分后的数据表。

构建完整的测试环境

构建完整测试服务器时需要创建新的路由器和私有网络、独立的网络环境和带宽资源、内网GRE隧道打通路由器、VPN拨入网络和SSH密钥管理。


8.jpg

这个过程你可以创建一个包含所有系统服务的all-in-one的环境,将其制作成自有映像。如果后续你的团队来新的人,需要独立的完整开发环境,只需基于自有镜像快速创建主机即可;还可以利用User Data定制化功能,在主机启动执行一段你上传的脚本,来初始化环境。你可以将这两个功能结合起来用,把所有你所需要用的服务全部安装部署完毕后做成映像,并用User Data脚本从代码库里更新代码。因为代码的变动相对于环境的更新更加频繁,不可能每次代码的更新都要构建一个新的自有镜像。通过这种方式构建起一个完整的测试服务器,让每个工程师都可以有自己独立的测试服务器。

在App发布上线时需要连到线上环境怎么办?这两个网络本身完全100%隔离,可利用GRE隧道的功能,把两个路由器打通,实现测试环境网络和线上生产环境网络的完全连通。

多机房部署与混合组网

为了让后端架构更可靠和业务更稳定,就需要实施多机房部署和混合组网。具体原因有以下三点:

  • 异地容灾:在复杂的网络环境下,机房可能会出现网络状况,导致一些比较关键性的业务的可用性降低,备份机房后可保证服务不会出现明显的长时间中断;

  • 负载分摊:单独一个机房可能不足以支撑全部的请求,这时可以把一部分的请求压力分担到另一个机房;

  • 加速区域访问:在国内网络环境下,南方和北方相互之间网络访问时有较高的延迟。通过做多机房部署实现加速区域用户的访问。




9.jpg


如上所示,有三个机房,中间是QingCloud北京1区机房,负责主营业务。左边是亚太1区机房,主要服务亚太和海外的客户。这两个机房都使用了QingCloud私有网络部署,利用路由器,通过GRE隧道或者IPsec加密隧道的方式进行互通。如果对数据传输过程的安全性要求较高,可以用IPsec的方式把两个机房相互打通,这时的访问只能通过内网IP进行访问。右边是办公室机房,工程师在这个环境下进行开发。

在实现混合组网时,只要机房路由器或者网宽设备支持标准的GRE隧道协议、IP隧道协议,就可以将传统物理世界的机房与路由器连通,并最终打通公有云环境。多机房部署通常见的方案有这些:

  • 异地冷备份



把主机房全套业务在异地重新构建一遍,且不需要提供线上服务,只有在主机房出现故障的时候才切换到备用机房,部署相对要简单一些。但有两方面缺点,一是成本比较高,需要双倍的费用且只是用来做冷备份,平时完全用不上;另外,当主机房突然挂掉时,备用机房再起动起来提供服务,数据需要预热,这是非常缓慢的过程,可能会出现服务响应慢,甚至不能正常提供服务。

  • 异地多活



从易到难有三阶段:第一,反向代理,用户请求到第二个机房,但不做任何处理被转向第一个机房这样会对两地的延时有一定的要求。第二,在第二个机房部署应用服务器和缓存,大部分的数据请求可以从缓存中读取,不用进行跨机房请求,但当缓存失效时,依然落到第一个机房的数据库去查询。所以,这个方式不太彻底;第三,全套服务的部署,包括HTTP服务器、业务服务器、缓存和数据库的 slave。此方式使得进入第二个机房的请求,只需要在机房内就可以完成请求处理,速度更快,但会遇到数据一致性和缓存一致性的问题,针对这点也会有一些解决方法。除了数据同步过程中的不一致问题,还需要面对缓存。

好的系统架构不是设计出来的,而是进化而来的

构建稳定可靠的业务系统需要注意以下这些:

  • 分析用户行为,理解你的业务,如社交、电商、视频;



不同的业务有不同的行业属性和特点,对于社交来讲,比较典型的特点是数据量庞大、数据查询维度多,比如查询6月11日-7月15日在xx咖啡厅我所有好友里拍过照片的人,查询条件包括好友维度、照片维度、地点维度、隐私状态维度等,这时就需要合理的做数据层面的扩展。

  • 电商的特点是定期举办大促销活动,届时会需要大量的计算资源、应用服务器来扛流量峰值,此时可利用云计算平台的弹性实现快速扩展业务,而在自己业务压力、促销来临时调用API接口,及AutoScaling扩展后端计算资源。视频业务有非常明显的流量高峰期和低峰期,流量高峰期通常是白天或者大家晚上下班回家那段时间,晚上2点到早上6点是流量非常低的时候,可利用云计算弹性优势,来调用API方式调整业务带宽资源,从而达到节省成本目的。

  • 合理规划系统,预估系统容量,如 10w / 100w / 1000w PV(DAU):不同的系统容量有可能对应不同架构的部署方式,找到最适合自己的那一个;

  • 系统是可横向扩展的 scalable;

  • 不遗余力地解决单点问题;

  • 为出错而设计design for failure:App的后端架构在开发支出就要为可能出现的各种问题进行准备,比如异地备份等;

  • 设计面向服务的架构,拆分子系统,API交互,异步处理;

  • 构建无处不在的缓存:页面缓存、接口缓存、对象缓存、数据库缓存;

  • 避免过度设计,好的系统架构不是设计出来的,而是进化而来的。


 来源:csdn 收起阅读 »

帮助开源社区解决冲突

在今年于波特兰召开的OSCON开源大会上,唐娜·本杰明(DonnaBenjamin)和吉娜·利金斯(GinaLikins)强强联合,探讨一个有时容易被人忽略的话题:冲突解决。考虑到处理科技冲突的需求日渐增加,甚至连像LinuxKernel这样流行的项目都采用了...
继续阅读 »
在今年于波特兰召开的OSCON开源大会上,唐娜·本杰明(DonnaBenjamin)和吉娜·利金斯(GinaLikins)强强联合,探讨一个有时容易被人忽略的话题:冲突解决。考虑到处理科技冲突的需求日渐增加,甚至连像LinuxKernel这样流行的项目都采用了规范准则,因此,会议注重探讨人际互动也就不足为奇了。

在本次采访中,唐娜和吉娜回答了许多难题,比如在开源社区冲突解决看起来是怎样的,如何与工程师们在这方面开展合作,以及如何鼓励社区把尊重和同情视为解决问题的基础。

吉娜和唐娜,你们都一直在讲冲突解决,我对这个话题也很感兴趣。那么,是什么引起了你们对这个话题的兴趣?

吉娜:我一直热衷于寻找能让我们把网络社区建设得更好的方法。过去,我更关注定义潜在的有害行为(希望以一种幽默的方式让人们看见这些行为)。

但有趣的是,我比较倾向于回避冲突。因此,和唐娜一起工作棒极了。她向我展示了如果处理得当,把冲突视作潜在想法和能量的丰富源泉可以说是好处多多。

唐娜:几年前,Drupal创始人德瑞斯·布塔特(DriesBuytaert)让我加入Drupal社区的工作团队。这是为了让这个巨大且成功的开源社区在管理上更规范更有条理。Drupal社区有一些“未完成的事情”,也就是落实冲突解决的政策和程序。那次工作经历带领着我探索冲突自身的本质。

唐娜住在澳大利亚,吉娜住在美国,你们两个人是怎么碰面并同意在OSCON开源大会上协作演讲的?

吉娜:OSCON开源大会已经同意了唐娜做演讲。她看到我在2015年的ApacheCon上的演讲,并通过Twitter联系上我。最初我们在计划上有些争吵,之后,我们在一个谷歌文档上“一起工作(simul-worked)”,在谷歌环聊(Hangouts)上讨论我们的想法。多亏了先进的科技,如今实现这种跨越全球的合作十分简单。最困难的是我得尽早吃晚饭,好准备接晚上8点的电话!

唐娜:哈哈!我和吉娜还没在现实世界“见过面”。我们期待碰面,然后亲自给我们的演讲稿写上最后一笔。我认为这是开源工作的重要事实。我们联系上来自世界各地的人们,科技促进了我们的交流,我们各自不同的经历也丰富了这些互动。驱动这些互动的正是分享这一目的,以及一同实现某件事物的愿望。我发现事实上对一个人了解的程度在共事时并不重要,这一点常常既令人惊讶却又是非常合理的。但我们在工作过程中会逐渐了解彼此,这真的是一件很有趣的事。你问还有什么?可别跟我提时区的事儿。

参与开源社区是如何引发你们对冲突解决的兴趣的?

吉娜:当人们满怀激情时冲突就几乎是不可避免的了,而在开源社区工作的人们从来都是满怀激情的。此外,开源社区没有任何外在实体可以帮我们“处理问题”,比如人力资源部、法律或政府。开源社区只有我们,而我们需要学着以创造性的方式解决冲突。要让这些方式为我们服务,而不是分裂我们。

唐娜:吉娜总结地非常好。我想我也开始欣赏冲突的潜力了,而且我也热衷于探索让我们以一种更健康的方式认识发生在我们社区的冲突。但是,我打心底里觉得这已经超越开源的范畴。我们也可以学习一些其他的团体、社区和产业“解决”一些此类问题的方式。我想,正如开源瓦解了软件开发和传播的传统模式,我们的社区也可能发现新的方式来应对由于冲突产生的问题、挑战和机会。

在不泄露你们演讲中所有建议的前提下给我讲讲好坏之争的冲突解决是什么样的吧。

吉娜:我认为解决冲突的关键之一是带有同情之心。我知道,我们不是面对面时很难做到这样。如果你看见自己把我弄哭了,你没准就会停下来,想想你说了些什么。但是,由于时间和空间上的距离,人们很容易忘记对话的另一端是真实存在的人,而且这些人值得同情。

唐娜:是的,毫无疑问。同情是创造性冲突解决成功的关键因素之一。另一个呢?是尊重。在武术中尊重你的对手非常重要。我认为我们应该借鉴这个思想,然后把它应用到我们社区的争论中。我的观点是,我们需要把冲突作为一种能源来利用。我们利用冲突深层的、潜在的激情或挫败感,然后转换成专注改变的力量。有很多原因会让人们变得人们暴躁。也许他们只是累了或饿了或者度过了糟糕的一天呢?那么,他们需要的只不过是理解、同情、睡眠或食物。或者也许我们需要解决一个实际问题,以改善每个人的处境。

你们如何确保冲突解决的建议有足够的吸引力,从而与工程师团体产生共鸣?

吉娜:猫的图片吧。另外动画片也很管用。

严格来说,关于这件事的问题之一是,参加这些谈话的人通常是“团队”,就像“我们在跟合唱团谈话。”这些人对冲突解决很感兴趣,也想了解更多信息。因此对他们来说,重要的是如何提供解决问题的有效信息。我觉得挑战在于如何把这个信息传达给不想要或者觉得根本不需要的人。我不确定该怎么做,我也很想听听别人的意见。

唐娜:这个问题问得好。我也不知道该怎么回答。不过我同意吉娜的说法,很多时候我们是在劝说那些已经转变的团队。那些专注工作的人很容易忽视这一问题,他们只想继续工作。有些人认为处理冲突最好的办法就是完全避免冲突。但不是每个人都擅长处理感觉和情感这些事。不过我最关心的是,这是一份实实在在的工作。我觉得我们群体里有太多人要么不重视,要么觉得这根本不是个实实在在的工作。

吉娜我还对你和高校为了让更多人参与开源项目而进行的合作非常感兴趣。说说你最近在做的工作和它们的重要性吧

吉娜:我们在RedHat(红帽)上的大学推广项目包括好几个部分,我参与最多的部分着眼于让更多大学层面的教师能将开源融入到课堂中。为此我们采用了很多方法,包括:

“向讲师讲述教授开源的价值所在;

“编写可以直接融入课堂的开源教材;

“与美国国家科学基金会(NationalScienceFoundation)一起共同赞助叫”教授开源软件体验“(POSSE)的集中研讨会,让教师们了解开源的方方面面。

这些工作之所以重要是因为,大多数计算机科学的大学生根本不知道开源是什么或者有什么用处。过去一年里我花了大量时间跟学生和老师交谈,让我惊讶的是,没几个学生”拥有“开源软件,也没有几个老师提到开源软件,更不用说向学生讲解了。这确实是个问题,因为Linux开发人员的年龄越来越大,而且我们的行业需要更多新的、更年轻的开发人员茁壮成长。

我花了很长时间研究Sugarlabs(一款专注合作的教育工具),然后无意中看到了唐娜博客的引文。你是怎么参与到这个项目里的?

唐娜:Sugarlabs出自”每个孩子一台笔记本计划“。这个项目刚刚公布时,我就在想:“这个项目将会改变世界。”对此我很激动,而且我觉得这个项目确实改变了世界。“百元笔记本电脑”计划之前,世界上大多数人根本买不起这种功能强大的便携设备。不久之后,我们有了上网本,而上网本重新定义了笔记本市场。

从本世纪初开始,我就开始在我的家乡维多利亚州参与了技术教育社区,所以我非常想支持使用科技并用科技进行教育的教育工作者。Sugar是一个可以用来学习的全新界面,假如你喜欢的话,可以在里面看到许多平时见不到的绝佳想法。可惜的是,现在我也不知道Sugar计划进展如何,我很想再去看看现在那里怎么样了。

参加OSCON开源大会的演讲之前,跟我说一件大家应该知道的关于你们的趣闻吧。

吉娜:趣闻是吗?我学过空中飞人,还坐过齐柏林飞艇。
唐娜:真的假的?太棒了。我们应该跑去参加马戏团。我会表演杂耍!明白我说的要在过程中互相了解的意思了吧?咱们波特兰再见吧!
 
内容来源:UDN技术社区 收起阅读 »

技巧分享:impress.js 使用总结

在美团参加 hackathon 时,使用 impress.js 做了一个商家上线流程的复盘工具。觉得 impress.js 很适合用于做 presentation, 因此进行一个简单地总结。   what is impress.js ...
继续阅读 »
在美团参加 hackathon 时,使用 impress.js 做了一个商家上线流程的复盘工具。觉得 impress.js 很适合用于做 presentation, 因此进行一个简单地总结。
 
what is impress.js


1.png



impress.js 是一个用于展示的前端框架,基于大量 css3 的动画等特性。最大的特点是其基于 transform 来构建,通过空间位置的移动以及旋转来体现变化,视觉冲击性很强。
该库需要有很好的 css 功底以及空间想象能力,因为整个 app 完全是靠代码进行编辑,并不是像传统的 ppt 工具进行拖拽以及鼠标点击。

可以在github上查看 example 和 demo。

如何用 impress.js 设计 presentation

其实作者制作的 demo 就几乎将所有可能用到的技巧都包含了,并在 github 中提供了源码,不过如果不仔细读源码的话很可能走很多弯路。

基本思路

其实作者的想法很巧妙也很简单,impress.js 会根据 html 中 step 的顺序来渲染整个页面,对于每个页面来说只有三种重要的属性——scale,position,rotate。

scale 决定了该页的大小。对应属性 width,height
position 决定了在三维空间中的坐标。对应属性 transform
rotate 则是旋转方式。对应属性 rotate[XYZ]
 

  • 绕 X 轴

  • 绕 Y 轴

  • 绕 Z 轴



impress.js 会根据这些属性将每一页进行渲染,最后从第一页开始一步步进行播放,因此这些属性也就决定了补间动画的形式。在开始写代码之前一定要想好空间结构。
 
技巧总结
 
虽然第一眼看过去很容易,可如果想做一个定制性很强的ppt还是会遇到很多问题,在这里总结一些经验与技巧。

设置补间动画

在 impress.js 文件中可以设置一些默认值,我会慢慢对这些参数进行说明。
// some default config values.
var defaults = {
width: 1024,
height: 768,
maxScale: 1,
minScale: 0,

perspective: 1000,

transitionDuration: 700
};
画布大小

width与height是每页的基准长度和宽度,与 scale 相乘之后才是该页的大小,设置偏移量时需要对width与height进行参考。

以下是计算 window 真实比例的代码。
var computeWindowScale = function ( config ) {
var hScale = window.innerHeight / config.height,
wScale = window.innerWidth / config.width,
scale = hScale > wScale ? wScale : hScale;

if (config.maxScale && scale > config.maxScale) {
scale = config.maxScale;
}

if (config.minScale && scale < config.minScale) {
scale = config.minScale;
}

return scale;
};
可以知道画布会随着window的缩放而自动进行缩放,maxScale决定了画布最大值,而minScale决定了最小为多大。千万不要将这里的scale与data-scale搞混淆。

透明度

.impress-enabled .step 调整 opacity 来控制非当前页的透明度

渐变动画时间

需要注意的是在更改 default transitionDuration之后还需要在 css 中修改 transitionDuration。这样才能保证动画的同步。

实用的 class

body 状态

  • impress-disabled is added to body element by the impress.js script

  • impress-enabled after init() function is called



page 状态

additional past, present and future classes are added to step elements。通过这三个状态可以做出很酷的动画效果。

  • future class appears on steps that were not yet visited

  • present class appears on currently visible step - it's different from active class as present class is added when transition finishes (step is entered)

  • past class is added to already visited steps (when the step is left)



插件推荐
 

  • impress-progress.js 显示 ppt 的进度条


 来源:segmentfault 收起阅读 »

Android M Developer Preview 2 发布

Google 更新 Android M 开发者预览版,相比 Lollipop 主要是安全和电池寿命方面的改进。此版本包括更多的授权设置改进,比如指纹验证,外部存储处理的授权方式改进。 Google 强烈建议大家升级到最新版本,最新版本包括最新的平台 APIs...
继续阅读 »
Google 更新 Android M 开发者预览版,相比 Lollipop 主要是安全和电池寿命方面的改进。此版本包括更多的授权设置改进,比如指纹验证,外部存储处理的授权方式改进。

Google 强烈建议大家升级到最新版本,最新版本包括最新的平台 APIs:更新了 Bluetooth Stylus APIs,为 Media API 添加了新的调用,一些类的重构等等。

此外还有一系列的 bug 修复。Google 温馨提示: Messenger 应用还不能很好的在 64 位模拟器上运行,还有些 YouTube 视频分享方面的问题,联系人同步方面的问题,Android M 在 Nexus Player 上的问题等等。
 
原文地址:http://www.oschina.net/news/64112/android-m-developer-preview-update 收起阅读 »

imgeek更新20150710

imgeek更新: 1. “问题”列表页中帖子下面加入“话题”,方便大家知道问题分类  2.更新页脚 3.更新帖子里的显示时间为绝对时间  
imgeek更新:
1. “问题”列表页中帖子下面加入“话题”,方便大家知道问题分类 
2.更新页脚
3.更新帖子里的显示时间为绝对时间
 

实战指导:Ceph存储性能优化总结

最近一直在忙着搞Ceph存储的优化和测试,看了各种资料,但是好像没有一篇文章把其中的方法论交代清楚,所以下文只是一个经验总结,拿出来与大家分享下。   一:优化方法论 做任何事情还是要有个方法论的,“授人以鱼不如授人以渔”的道理吧,方法通了,所有的问题就有了...
继续阅读 »
最近一直在忙着搞Ceph存储的优化和测试,看了各种资料,但是好像没有一篇文章把其中的方法论交代清楚,所以下文只是一个经验总结,拿出来与大家分享下。
 
一:优化方法论

做任何事情还是要有个方法论的,“授人以鱼不如授人以渔”的道理吧,方法通了,所有的问题就有了解决的途径。通过对公开资料的分析进行总结,对分布式存储系统的优化离不开以下几点:

1. 硬件层面
  • 硬件规划
  • SSD选择
  • BIOS设置
2. 软件层面
  • Linux OS
  • Ceph Configurations
  • PG Number调整
  • CRUSH Map
  • 其他因素
硬件优化1. 硬件规划
  • Processor
ceph-osd进程在运行过程中会消耗CPU资源,所以一般会为每一个ceph-osd进程绑定一个CPU核上。当然如果你使用EC方式,可能需要更多的CPU资源。ceph-mon进程并不十分消耗CPU资源,所以不必为ceph-mon进程预留过多的CPU资源。ceph-msd也是非常消耗CPU资源的,所以需要提供更多的CPU资源。
  • 内存
ceph-mon和ceph-mds需要2G内存,每个ceph-osd进程需要1G内存,当然2G更好。
  • 网络规划
万兆网络现在基本上是跑Ceph必备的,网络规划上,也尽量考虑分离cilent和cluster网络。2. SSD选择硬件的选择也直接决定了Ceph集群的性能,从成本考虑,一般选择SATA SSD作为Journal,Intel® SSD DC S3500 Series基本是目前看到的方案中的首选。400G的规格4K随机写可以达到11000 IOPS。如果在预算足够的情况下,推荐使用PCIE SSD,性能会得到进一步提升,但是由于Journal在向数据盘写入数据时Block后续请求,所以Journal的加入并未呈现出想象中的性能提升,但是的确会对Latency有很大的改善。如何确定你的SSD是否适合作为SSD Journal,可以参考SÉBASTIEN HAN的Ceph: How to Test if Your SSD Is Suitable as a Journal Device?,这里面他也列出了常见的SSD的测试结果,从结果来看SATA SSD中,Intel S3500性能表现最好。3. BIOS设置
  • Hyper-Threading(HT)
基本做云平台的,VT和HT打开都是必须的,超线程技术(HT)就是利用特殊的硬件指令,把两个逻辑内核模拟成两个物理芯片,让单个处理器都能使用线程级并行计算,进而兼容多线程操作系统和软件,减少了CPU的闲置时间,提高的CPU的运行效率。
  • 关闭节能
关闭节能后,对性能还是有所提升的,所以坚决调整成性能型(Performance)。当然也可以在操作系统级别进行调整,详细的调整过程请参考链接,但是不知道是不是由于BIOS已经调整的缘故,所以在CentOS 6.6上并没有发现相关的设置。
for CPUFREQ in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do [ -f $CPUFREQ ] || continue; echo -n performance > $CPUFREQ; done 
  • NUMA
简单来说,NUMA思路就是将内存和CPU分割为多个区域,每个区域叫做NODE,然后将NODE高速互联。 node内cpu与内存访问速度快于访问其他node的内存,NUMA可能会在某些情况下影响ceph-osd。解决的方案,一种是通过BIOS关闭NUMA,另外一种就是通过cgroup将ceph-osd进程与某一个CPU Core以及同一NODE下的内存进行绑定。但是第二种看起来更麻烦,所以一般部署的时候可以在系统层面关闭NUMA。CentOS系统下,通过修改 /etc/grub.conf文件,添加numa=off来关闭NUMA。
kernel /vmlinuz-2.6.32-504.12.2.el6.x86_64 ro root=UUID=870d47f8-0357-4a32-909f-74173a9f0633 rd_NO_LUKS rd_NO_LVM LANG=en_US.UTF-8 rd_NO_MD SYSFONT=latarcyrheb-sun16 crashkernel=auto  KEYBOARDTYPE=pc KEYTABLE=us rd_NO_DM   biosdevname=0 numa=off 
二:软件优化 1. Linux OS
  • Kernel pid max
echo 4194303 > /proc/sys/kernel/pid_max 
Jumbo frames, 交换机端需要支持该功能,系统网卡设置才有效果
ifconfig eth0 mtu 9000 
永久设置
echo "MTU=9000" | tee -a /etc/sysconfig/network-script/ifcfg-eth0 2./etc/init.d/networking restart 
read_ahead, 通过数据预读并且记载到随机访问内存方式提高磁盘读操作,查看默认值
cat /sys/block/sda/queue/read_ahead_kb 
根据一些Ceph的公开分享,8192是比较理想的值
echo "8192" > /sys/block/sda/queue/read_ahead_kb 
swappiness, 主要控制系统对swap的使用,这个参数的调整最先见于UnitedStack公开的文档中,猜测调整的原因主要是使用swap会影响系统的性能。
echo "vm.swappiness = 0" | tee -a /etc/sysctl.conf 
I/O Scheduler,关于I/O Scheculder的调整网上已经有很多资料,这里不再赘述,简单说SSD要用noop,SATA/SAS使用deadline。
echo "deadline" > /sys/block/sd[x]/queue/scheduler 2.echo "noop" > /sys/block/sd[x]/queue/scheduler 
cgroup这方面的文章好像比较少,昨天在和Ceph社区交流过程中,Jan Schermer说准备把生产环境中的一些脚本贡献出来,但是暂时还没有,他同时也列举了一些使用cgroup进行隔离的原因。
  • 不在process和thread在不同的core上移动(更好的缓存利用)
  • 减少NUMA的影响
  • 网络和存储控制器影响 - 较小
  • 通过限制cpuset来限制Linux调度域(不确定是不是重要但是是最佳实践)
  • 如果开启了HT,可能会造成OSD在thread1上,KVM在thread2上,并且是同一个core。Core的延迟和性能取决于其他一个线程做什么。


这一点具体实现待补充!!!

2. Ceph Configurations

[global]


1.jpg



查看系统最大文件打开数可以使用命令


2.jpg



调整omap的原因主要是EXT4文件系统默认仅有4K
filestore queue相关的参数对于性能影响很小,参数调整不会对性能优化有本质上提升
 
[osd] - journal


3.jpg



[osd] - journal
 
Ceph OSD Daemon stops writes and synchronizes the journal with the filesystem, allowing Ceph OSD Daemons to trim operations from the journal and reuse the space.
上面这段话的意思就是,Ceph OSD进程在往数据盘上刷数据的过程中,是停止写操作的。
 
[osd] - osd config tuning


4.jpg


 
增加osd op threads和disk threads会带来额外的CPU开销

[osd] - recovery tuning


5.jpg


 
[osd] - client tuning


6.jpg


 
关闭Debug
 
三: PG Number
 
PG和PGP数量一定要根据OSD的数量进行调整,计算公式如下,但是最后算出的结果一定要接近或者等于一个2的指数。
Total PGs = (Total_number_of_OSD * 100) / max_replication_count 
例如15个OSD,副本数为3的情况下,根据公式计算的结果应该为500,最接近512,所以需要设定该pool(volumes)的pg_num和pgp_num都为512.
ceph osd pool set volumes pg_num 512 
2.ceph osd pool set volumes pgp_num 512



4. CRUSH Map

CRUSH是一个非常灵活的方式,CRUSH MAP的调整取决于部署的具体环境,这个可能需要根据具体情况进行分析,这里面就不再赘述了。

5. 其他因素的影响

在今年的(2015年)的Ceph Day上,海云捷迅在调优过程中分享过一个由于在集群中存在一个性能不好的磁盘,导致整个集群性能下降的case。通过osd perf可以提供磁盘latency的状况,同时在运维过程中也可以作为监控的一个重要指标,很明显在下面的例子中,OSD 8的磁盘延时较长,所以需要考虑将该OSD剔除出集群:
ceph osd perf 
osd fs_commit_latency(ms) fs_apply_latency(ms)
osd fs_commit_latency(ms) fs_apply_latency(ms) 
2. 0 14 17
3. 1 14 16
4. 2 10 11
5. 3 4 5
6. 4 13 15
7. 5 17 20
8. 6 15 18
9. 7 14 16
10. 8 299 329



ceph.conf
1.[global] 
2.fsid = 059f27e8-a23f-4587-9033-3e3679d03b31
3.mon_host = 10.10.20.102, 10.10.20.101, 10.10.20.100
4.auth cluster required = cephx
5.auth service required = cephx
6.auth client required = cephx
7.osd pool default size = 3
8.osd pool default min size = 1
9.
10.public network = 10.10.20.0/24
11.cluster network = 10.10.20.0/24
12.
13.max open files = 131072
14.
15.[mon]
16.mon data = /var/lib/ceph/mon/ceph-$id
17.
18.[osd]
19.osd data = /var/lib/ceph/osd/ceph-$id
20.osd journal size = 20000
21.osd mkfs type = xfs
22.osd mkfs options xfs = -f
23.
24.filestore xattr use omap = true
25.filestore min sync interval = 10
26.filestore max sync interval = 15
27.filestore queue max ops = 25000
28.filestore queue max bytes = 10485760
29.filestore queue committing max ops = 5000
30.filestore queue committing max bytes = 10485760000
31.
32.journal max write bytes = 1073714824
33.journal max write entries = 10000
34.journal queue max ops = 50000
35.journal queue max bytes = 10485760000
36.
37.osd max write size = 512
38.osd client message size cap = 2147483648
39.osd deep scrub stride = 131072
40.osd op threads = 8
41.osd disk threads = 4
42.osd map cache size = 1024
43.osd map cache bl size = 128
44.osd mount options xfs = "rw,noexec,nodev,noatime,nodiratime,nobarrier"
45.osd recovery op priority = 4
46.osd recovery max active = 10
47.osd max backfills = 4
48.
49.[client]
50.rbd cache = true
51.rbd cache size = 268435456
52.rbd cache max dirty = 134217728
53.rbd cache max dirty age = 5




总结

优化是一个长期迭代的过程,所有的方法都是别人的,只有在实践过程中才能发现自己的,本篇文章仅仅是一个开始,欢迎各位积极补充,共同完成一篇具有指导性的文章。
作者:xiaoquqi 收起阅读 »

什么叫有专职工程师值守的话题

什么叫有专职工程师值守的话题  如今的社区,热心的人越来越少,当你有问题需要发贴求助的时候,大多会遇到的情况会是这样的:  . 永远不知道问题何时能得到回复  终于有了回复的,还是:  . 无聊捣乱的  . 自动顶贴机的自动回复,后面还带有一串含有Url的...
继续阅读 »
什么叫有专职工程师值守的话题 

如今的社区,热心的人越来越少,当你有问题需要发贴求助的时候,大多会遇到的情况会是这样的: 
. 永远不知道问题何时能得到回复 

终于有了回复的,还是: 
. 无聊捣乱的 
. 自动顶贴机的自动回复,后面还带有一串含有Url的签名 
. 各种奇葩的、不搭调的回复 
. ...... 

最郁闷的是,你的贴子就像一颗石子扔到湖里,到最后,却没有任何涟漪:没.有.回.帖...... 

imgeek努力想改变这种现状,循着极客们开放、分享、协作、创新的精神,我们努力构建一个具有服务质量保障(Service Level Assurance , SLA)的社区
我们努力征集一些热心的技术专家,得到他们的承诺,可以值守一些话题,当有该话题提交的时候,他可以及时的收到消息提醒,这样可以保障他可以及时的看到你提的问题,以便及时回复。 

当然,如果你在提交一个问题之前,可以先搜索一下,说不定你要提的问题已经有人提过并且得到解答。这样可以省却不少你的时间。 

我们努力的寻找一些热心的专家,可以为社区兄弟们解决一些问题,imgeek社区也可以提供一些必要的补贴给你们,你可以填写表格申请成为我们的”社区专家“,我们将会及时联系你 收起阅读 »

imgeek社区更新20150708, 增加”有专职工程师值守的话题"

1.发起问题时新增“有专职工程师值守的话题”,选择“有专职工程师值守“里的话题,支持工程师将会即时收到邮件提醒 2.更改主菜单为"动态"”问答“”文章“ ”活动“, 去掉”发现“,"帮助",折叠”话题“, 3.问题发起者可以设置“最佳答案”,原来只有管理员可以...
继续阅读 »
1.发起问题时新增“有专职工程师值守的话题”,选择“有专职工程师值守“里的话题,支持工程师将会即时收到邮件提醒
2.更改主菜单为"动态"”问答“”文章“ ”活动“, 去掉”发现“,"帮助",折叠”话题“,
3.问题发起者可以设置“最佳答案”,原来只有管理员可以设置
4.主菜单上“活动”前面加上图标
  收起阅读 »

技术分享:Python 并行任务技巧

Python的并发处理能力臭名昭著。先撇开线程以及GIL方面的问题不说,我觉得多线程问题的根源不在技术上而在于理念。大部分关于Pyhon线程和多进程的资料虽然都很不错,但却过于细节。这些资料讲的都是虎头蛇尾,到了真正实际使用的部分却草草结束了。   传统例...
继续阅读 »
Python的并发处理能力臭名昭著。先撇开线程以及GIL方面的问题不说,我觉得多线程问题的根源不在技术上而在于理念。大部分关于Pyhon线程和多进程的资料虽然都很不错,但却过于细节。这些资料讲的都是虎头蛇尾,到了真正实际使用的部分却草草结束了。
 
传统例子
 
在DDG https://duckduckgo.com/ 搜索“Python threading tutorial”关键字,结果基本上却都是相同的类+队列的示例。

标准线程多进程,生产者/消费者示例:


1.jpg



这里是代码截图,如果用其他模式贴出大段代码会很不美观。文本模式点这里 here
Mmm.. 感觉像是java代码
在此我不想印证采用生产者/消费者模式来处理线程/多进程是错误的— 确实没问题。实际上这也是解决很多问题的最佳选择。但是,我却不认为这是日常工作中常用的方式。

问题所在

一开始,你需要一个执行下面操作的铺垫类。接着,你需要创建一个传递对象的队列,并在队列两端实时监听以完成任务。(很有可能需要两个队列互相通信或者存储数据)

Worker越多,问题越大.

下一步,你可能会考虑把这些worker放入一个线程池一边提高Python的处理速度。下面是
IBM tutorial 上关于线程较好的示例代码。这是大家常用到的利用多线程处理web页面的场景
Seriously, Medium. Fix your code support. Code is Here.

感觉效果应该很好,但是看看这些代码!初始化方法、线程跟踪,最糟的是,如果你也和我一样是个容易犯死锁问题的人,这里的join语句就要出错了。这样就开始变得更加复杂了!

到现在为止都做了些什么?基本上没什么。上面的代码都是些基础功能,而且很容易出错。(天啊,我忘了写上在队列对象上调用task_done()方法(我懒得修复这个问题在重新截图)),这真是性价比太低。所幸的是,我们有更好的办法.


2.jpg



引入:Map

Map 是个很酷的小功能,也是简化Python并发代码的关键。对那些不太熟悉Map的来说,它有点类似Lisp.它就是序列化的功能映射功能. e.g.
urls = [', ']
results = map(urllib2.urlopen, urls)
这里调用urlopen方法,并把之前的调用结果全都返回并按顺序存储到一个集合中。这有点类似
results = []
for url in urls:
results.append(urllib2.urlopen(url))
Map能够处理集合按顺序遍历,最终将调用产生的结果保存在一个简单的集合当中。
为什么要提到它?因为在引入需要的包文件后,Map能大大简化并发的复杂度!


3.jpg



支持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.jpg



看吧!只用4行代码就搞定了!其中三行还是固定写法。使用map方法简单的搞定了之前需要40行代码做的事!为了增加趣味性,我分别统计了不同线程池大小的运行时间。


5.png


 
结果:


6.jpg


效果惊人!看来调试一下确实很有用。当线程池大小超过9以后,在我本机上的运行效果已相差无几。

示例 2:

生成上千张图像的缩略图:

现在咱们看一年计算密集型的任务!我最常遇到的这类问题之一就是大量图像文件夹的处理。

其中一项任务就是创建缩略图。这也是并发中比较成熟的一项功能了。

基础单线程创建过程


7.jpg


 
作为示例来说稍微有点复杂。但其实就是传一个文件夹目录进来,获取到里面所有的图片,分别创建好缩略图然后保存到各自的目录当中。

在我的电脑上,处理大约6000张图片大约耗时27.9秒.

如果使用并发map处理替代其中的for循环:


8.jpg


 
只用了5.6 秒!

就改了几行代码速度却能得到如此巨大的提升。最终版本的处理速度还要更快。因为我们将计算密集型与IO密集型任务分派到各自独立的线程和进程当中,这也许会容易造成死锁,但相对于map强劲的功能,通过简单的调试我们最终总能设计出优美、高可靠性的程序。就现在而言,也别无它法。
好了。来感受一下一行代码的并发程序吧。 收起阅读 »

Go语言对Android原生应用开发的支持情况

Google工程师和独立开发人员提出了几份不同的提案,旨在让Go语言支持开发原生的Android应用。这项工作无法让Go语言编写的应用使用Android NDK的全部接口,但有可能使用其中的一个子集。 David Crawshaw是Google的工程师,他写了...
继续阅读 »
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的选项,这个选项可以提供以下功能:
  • 为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已经实现及时通信,怎么及时同步到我的数据库中...
继续阅读 »
我公司想使用环信这个聊天工具,有几个问题请环信工程师帮助解答一下,非常感谢
1. 我自己的本地程序数据库如何与环信数据库对接,比如好友,聊天信息,群组信息
2. 我发信息时,WEB-IM 和 IOS 聊天,用DEMO已经实现及时通信,怎么及时同步到我的数据库中?
3. 添加删除好友增量变化,如何及时同步到我的数据库中?

我是不是可以理解成
比如我添加一个好友,我第一步调用自己程序的方法,把这个操作添加到自己的数据库中,同时再调用环信的方法,就保存在了环信的数据库中?
有没有咱们环信的成功案例,给与我一个最好的解决方案?

如果我使用自己的数据
整体的解决方案是不是这样:
1. 我加载好友及群组数据,数据来源是自己的数据库
2. 我发送消息给好友,需要调用环信的发信息接口
3. 我添加好友删除好友等操作,也是保存到自己的数据库,不需要保存到环信的数据库中
也就是说,我只有在发送和接收消息的时候,才用到环信?

一些问题求解答,确实想使用环信,谢谢
  收起阅读 »

干货:Nginx/LVS/HAProxy 负载均衡软件的优缺点详解

Nginx/LVS/HAProxy是目前使用最广泛的三种负载均衡软件,一般对负载均衡的使用是随着网站规模的提升根据不同的阶段来使用不同的技术,具体的应用需求还得具体分析。 如果是中小型的Web应用,比如日PV小于1000万,用Nginx就完全可以了;如果机器不...
继续阅读 »
Nginx/LVS/HAProxy是目前使用最广泛的三种负载均衡软件,一般对负载均衡的使用是随着网站规模的提升根据不同的阶段来使用不同的技术,具体的应用需求还得具体分析。
如果是中小型的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的缺点:
  • Nginx仅能支持http、https和Email协议,这样就在适用范围上面小些,这个是它的缺点。
  • 对后端服务器的健康检查,只支持通过端口来检测,不支持通过url来检测。不支持Session的直接保持,但能通过ip_hash来解决。
LVSLVS是使用Linux内核集群实现的一个高性能、高可用的负载均衡服务器,它具有很好的可伸缩性(Scalability)、可靠性(Reliability)和可管理性(Manageability)。LVS的优点:
  • 抗负载能力强,工作在网络4层之上,仅作分发之用,没有流量的产生,这个特点也决定了它在负载均衡软件里的性能最强的,对内存和cpu资源消耗比较低。
  • 配置性比较低,这是一个缺点也是一个优点,因为没有可太多配置的东西,所以并不需要太多接触,大大减少了人为出错的几率。
  • 工作稳定,因为其本身抗负载能力很强,自身有完整的双机热备方案,如LVS+Keepalived,不过我们在项目实施中用得最多的还是LVS/DR+Keepalived。
  • 无流量,LVS只分发请求,而流量并不从它本身出去,这点保证了均衡器IO的性能不会收到大流量的影响。
  • 应用范围比较广,因为LVS工作在4层,所以它几乎可以对所有应用做负载均衡,包括http、数据库、在线聊天室等等。
LVS的缺点:
  • 软件本身不支持正则表达式处理,不能做动静分离;而现在许多网站在这方面都有较强的需求,这个是Nginx/HAProxy+Keepalived的优势所在。
  • 如果是网站应用比较庞大的话,LVS/DR+Keepalived实施起来就比较复杂了,特别后面有Windows Server的机器的话,如果实施及配置还有维护过程就比较复杂了,相对而言,Nginx/HAProxy+Keepalived就简单多了。
HAProxyHAProxy的特点:
  • HAProxy也是支持虚拟主机的。
  • HAProxy的优点能够补充Nginx的一些缺点,比如支持Session的保持,Cookie的引导;同时支持通过获取指定的url来检测后端服务器的状态。
  • HAProxy跟LVS类似,本身就只是一款负载均衡软件;单纯从效率上来讲HAProxy会比Nginx有更出色的负载均衡速度,在并发处理上也是优于Nginx的。
  • HAProxy支持TCP协议的负载均衡转发,可以对MySQL读进行负载均衡,对后端的MySQL节点进行检测和负载均衡,大家可以用LVS+Keepalived对MySQL主从做负载均衡。
  • 5、HAProxy负载均衡策略非常多,HAProxy的负载均衡算法现在具体有如下8种:
roundrobin:表示简单的轮询,这个不多说,这个是负载均衡基本都具备的;static-rr:表示根据权重,建议关注;leastconn:表示最少连接者先处理,建议关注;source:表示根据请求源IP,这个跟Nginx的IP_hash机制类似,我们用其作为解决session问题的一种方法,建议关注;ri:表示根据请求的URI;rl_param:表示根据请求的URl参数’balance url_param’ requires an URL parameter name;hdr(name):表示根据HTTP请求头来锁定每一次HTTP请求;rdp-cookie(name):表示根据据cookie(name)来锁定并哈希每一次TCP请求。Nginx和LVS对比总结:Nginx工作在网络的7层,所以它可以针对http应用本身来做分流策略,比如针对域名、目录结构等,相比之下LVS并不具备这样的功能,所以Nginx单凭这点可利用的场合就远多于LVS了;但Nginx有用的这些功能使其可调整度要高于LVS,所以经常要去触碰触碰,触碰多了,人为出问题的几率也就会大。Nginx对网络稳定性的依赖较小,理论上只要ping得通,网页访问正常,Nginx就能连得通,这是Nginx的一大优势!Nginx同时还能区分内外网,如果是同时拥有内外网的节点,就相当于单机拥有了备份线路;LVS就比较依赖于网络环境,目前来看服务器在同一网段内并且LVS使用direct方式分流,效果较能得到保证。另外注意,LVS需要向托管商至少申请多一个ip来做Visual IP,貌似是不能用本身的IP来做VIP的。要做好LVS管理员,确实得跟进学习很多有关网络通信方面的知识,就不再是一个HTTP那么简单了。Nginx安装和配置比较简单,测试起来也很方便,因为它基本能把错误用日志打印出来。LVS的安装和配置、测试就要花比较长的时间了;LVS对网络依赖比较大,很多时候不能配置成功都是因为网络问题而不是配置问题,出了问题要解决也相应的会麻烦得多。Nginx也同样能承受很高负载且稳定,但负载度和稳定度差LVS还有几个等级:Nginx处理所有流量所以受限于机器IO和配置;本身的bug也还是难以避免的。Nginx可以检测到服务器内部的故障,比如根据服务器处理网页返回的状态码、超时等等,并且会把返回错误的请求重新提交到另一个节点。目前LVS中 ldirectd也能支持针对服务器内部的情况来监控,但LVS的原理使其不能重发请求。比如用户正在上传一个文件,而处理该上传的节点刚好在上传过程中出现故障,Nginx会把上传切到另一台服务器重新处理,而LVS就直接断掉了,如果是上传一个很大的文件或者很重要的文件的话,用户可能会因此而恼火。Nginx对请求的异步处理可以帮助节点服务器减轻负载,假如使用apache直接对外服务,那么出现很多的窄带链接时apache服务器将会占用大 量内存而不能释放,使用多一个Nginx做apache代理的话,这些窄带链接会被Nginx挡住,apache上就不会堆积过多的请求,这样就减少了相当多的资源占用。这点使用squid也有相同的作用,即使squid本身配置为不缓存,对apache还是有很大帮助的。Nginx能支持http、https和email(email的功能比较少用),LVS所支持的应用在这点上会比Nginx更多。在使用上,一般最前端所采取的策略应是LVS,也就是DNS的指向应为LVS均衡器,LVS的优点令它非常适合做这个任务。重要的ip地址,最好交由LVS托管,比如数据库的 ip、webservice服务器的ip等等,这些ip地址随着时间推移,使用面会越来越大,如果更换ip则故障会接踵而至。所以将这些重要ip交给 LVS托管是最为稳妥的,这样做的唯一缺点是需要的VIP数量会比较多。Nginx可作为LVS节点机器使用,一是可以利用Nginx的功能,二是可以利用Nginx的性能。当然这一层面也可以直接使用squid,squid的功能方面就比Nginx弱不少了,性能上也有所逊色于Nginx。Nginx也可作为中层代理使用,这一层面Nginx基本上无对手,唯一可以撼动Nginx的就只有lighttpd了,不过lighttpd目前还没有能做到 Nginx完全的功能,配置也不那么清晰易读。另外,中层代理的IP也是重要的,所以中层代理也拥有一个VIP和LVS是最完美的方案了。具体的应用还得具体分析,如果是比较小的网站(日PV小于1000万),用Nginx就完全可以了,如果机器也不少,可以用DNS轮询,LVS所耗费的机器还是比较多的;大型网站或者重要的服务,机器不发愁的时候,要多多考虑利用LVS。现在对网络负载均衡的使用是随着网站规模的提升根据不同的阶段来使用不同的技术:
  • 第一阶段:利用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,开启了移动时代的Andr​​oid纪元(如图1所示),直到现在Android也是唯一能在移动市场上与iOS相抗衡的平台。简单地说,Android与iOS占尽了移动时代的先机(这个故...
继续阅读 »
由Google、HTC、Qualcomm联手打造的第一部Android手机G1,开启了移动时代的Andr​​oid纪元(如图1所示),直到现在Android也是唯一能在移动市场上与iOS相抗衡的平台。简单地说,Android与iOS占尽了移动时代的先机(这个故事要从2007年的iPhone和2008年的Andr​​oid G1开始说起,甚至是更早之前的开发史及并购史),App开发者已在Android及iOS上扎下了深厚的根基,因此再也无力也没有必要去为第三个平台开发或移植自己的App,原因很简单,因为受众太少,支出与收入不成正比。


1.jpg



一开始Android生态系统很简单,就是拉拢App开发者,并且寻求与更多芯片制造商及手机厂商的合作,共同推广Android这个开放平台。但随着Android市场占有率逐步攀升,Google开始一步步地收紧及控制生态系的发展,以期自己能在Android平台上获得更多的利益(简单地说,就是收入,Google希望Android能够为自己带来更多的收益),而不再满足于只是打造及提供Android平台的角色。

随着Android一路开疆辟土,出现在越来越多的移动设备上,再加上Google对Android策略的转变,现今的Andr​​oid生态系统已变得较以往复杂了许多,系统中的角色较以往多了电信运营商、汽车制造商、串流内容及媒体提供商等。Google在全球移动市场的策略也已从“移动优先”转变至“攻占所有屏幕(装置)”,而Android也企图往这个目标上不断迈进(如图2所示)。


2.jpg



时至今日,欧洲、美国、日本的手机制造商节节败退,摩托罗拉、诺基亚、索尼等老牌制造商相继裁员(甚至出售手机及移动业务),而中国手机制造商却不断崛起,中兴、华为、联想、酷派、OPPO、小米等相继杀入手机市场后,如今中国已成为全球最大的智能机生产及销售国,智能机用户数早已超越美国。而新兴的国外市场,如印度、南美、东南亚正刮起一阵低价智能机风暴,Android正是这波低价智能机的推手(如采用MTK芯片的Andr​​oid 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→运营商”,以期收紧分散且碎片化的Andr​​oid生态圈,并获取更大的潜在利益。这一点,我们可以在最新发布的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),做一些基本介绍。


3.JPG



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位,所需要的系统空间较以往的Andr​​oid版本更大,因为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 发布...
继续阅读 »
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 月的排行榜图表


1.jpg


根据上面的图表,很难分析大体情况,所以提供了下面的数值排名。注意下面列出的是前 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传输数据,其对网络性能是非常敏感的,在实战中,经常需要测试当前端到端或端到云的网络性能。在这里我们讨论一下网络性能测试中所涉及到指标,技术和相关工具,以及如何编写自己的网络性能测试工具。   性能指标 先给出几个比较重要的指...
继续阅读 »
实时音视频这种实时业务一般用udp传输数据,其对网络性能是非常敏感的,在实战中,经常需要测试当前端到端或端到云的网络性能。在这里我们讨论一下网络性能测试中所涉及到指标,技术和相关工具,以及如何编写自己的网络性能测试工具。
 
性能指标

先给出几个比较重要的指标的定义以及它们的意义。
 带宽(吞吐量)
  • 单位时间内传输的数据量,单位通常是每秒比特数,记作bps;
  • 带宽反映了网络的传输能力,越大越好;
丢包
  • 数据包丢失个数,等于“发送数据包数” - “接受数据包数”;
  • 丢包反映了网络可靠性,越小越好;
时延
  • 数据包从发送开始到接收到该数据所耗费的时间,单位通常是毫秒;
  • 时延反映了网络的速度,越小越好;
抖动
  • 指时延的变化,即两个数据包时延的差值;
  • 抖动反映了网络的稳定性,越小越好;
乱序
  • 指接收到的数据包顺序和发送顺序不一致的次数;
  • 乱序反映了网络的稳定性,越小越好;
  • 当乱序比较严重时,丢包也会比较严重,所以一般都以丢包指标为主,忽略乱序指标;

 
测试工具

网上有很多测试网络性能的工具,如果它们能满足需求的话,就不用自己再造轮子了。

ping

ping是最常见的,几乎在所有的OS上都有它的存在。 其工作原理如图


1.png



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功能功能强一些,可以测带宽,丢包,抖动, 但是测不了时延。它的工作原理如图:


2.png



服务端:
$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内部最炙手可热的数据分析工具,目前在g...
继续阅读 »
7月5日 Airbnb召开了第一次开放技术大会OpenAir,重点是数据驱动在airbnb开发过程中的实践,在大会上,Airbnb开源了3个大数据神器

Airpal


1.jpg



第一个神器叫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星评价甚至会起到副作用


2.jpg



Airflow

大数据的基础还是data pipeline。Airflow则是Airbnb内部发起、排序、监控data pipeline的工具。


3.gif


来源:用友开发者 收起阅读 »

2015 年度开源项目新秀榜

黑鸭(Black Duck)软件公布了一份名叫“年度开源项目新秀”的报告,介绍了由全球开源协会发起的10个最有趣、最活跃的新项目。 年度开源项目新秀 每年都有上千新的开源项目问世,但只有少数能够真正的吸引我们的关注。一些项目因为利用了当前比较流...
继续阅读 »
黑鸭(Black Duck)软件公布了一份名叫“年度开源项目新秀”的报告,介绍了由全球开源协会发起的10个最有趣、最活跃的新项目。


1.jpg



年度开源项目新秀

每年都有上千新的开源项目问世,但只有少数能够真正的吸引我们的关注。一些项目因为利用了当前比较流行的技术而发展壮大,有一些则真正地开启了一个新的领域。很多开源项目建立的初衷是为了解决一些生产上的问题,还有一些项目则是世界各地志同道合的开发者们共同发起的一个宏伟项目。

从2009年起,开源软件管理公司黑鸭便发起了 年度开源项目新秀 这一活动,它的评选根据 Open Hub 网站(即以前的Ohloh)上的活跃度。今年,我们很荣幸能够报道2015年10大开源项目新秀的得主和2名荣誉奖得主,它们是从上千个开源项目中脱颖而出的。评选采用了加权评分系统,得分标准基于项目的活跃度,交付速度和几个其它因数。

开源俨然成为了产业创新的引擎,就拿今年来说,和Docker容器相关的开源项目在全球各地兴起,这也不恰巧反映了企业最感兴趣的技术领域吗?最后,我们接下来介绍的项目,将会让你了解到全球开源项目的开发者们的在思考什么,这很快将会成为一个指引我们发展的领头羊。

2015年度开源项目新秀: DebOps


2.jpg



DebOps 收集打包了一套 Ansible 方案和规则(Ansible是一种自动化运维工具),可以从1个容器扩展到一个完整的数据中心。它的创始人Maciej Delmanowski将DebOps开源来保证项目长久进行,从而更好的通过外部贡献者的帮助发展下去。

DebOps始创于波兰的一个不起眼大学校园里,他们运营自己的数据中心,一切工作都采用手工配置。有时系统崩溃而导致几天的宕机,这时Delmanowski意识到一个配置管理系统是很有必要的。以Debian作为基础开始,DebOps是一组配置一整个数据基础设施的Ansible方案。此项目已经在许多不同的工作环境下实现,而创始者们则打算继续支持和改进这个项目。

2015年度开源项目新秀: Code Combat


3.jpg



传统的纸笔学习方法已近不能满足技术学科了。然而游戏却有很多人都爱玩,这也就是为什么 CodeCombat 的创始人会去开发一款多人协同编程游戏来教人们如何编码。

刚开始CodeCombat是一个创业想法,但其创始人决定取而代之创建一个开源项目。此想法在社区传播开来,很快不少贡献者加入到项目中来。项目发起仅仅两个月后,这款游戏就被接纳到Google’s Summer of Code活动中。这款游戏吸引了大量玩家,并被翻译成45种语言。CodeCombat希望成为那些想要一边学习代码同时获得乐趣的同学的风向标。

2015年度开源项目新秀: Storj


4.jpg



Storj 是一个点对点的云存储网络,可实现端到端加密,保证用户不用依赖第三方即可传输和共享数据。基于比特币block chain技术和点对点协议,Storj提供安全、私密、加密的云存储。

云数据存储的反对者担心成本开销和漏洞攻击。针对这两个担忧,Storj提供了一个私有云存储市场,用户可以通过Storjcoin X(SJCX) 购买交易存储空间。上传到Storj的文件会被粉碎、加密和存储到整个社区。只有文件所有者拥有密钥加密的信息。

在2014年举办的Texas Bitcoin Conference Hackathon会议上,去中心化的云存储市场概念首次被提出并证明可行。在第一次赢得黑客马拉松活动后,项目创始人们和领导团队利用开放论坛、Reddit、比特币论坛和社交媒体增长成了一个活跃的社区,如今,它们已成为影响Storj发展方向的一个重要组成部分。

2015年度开源项目新秀: Neovim


5.jpg


 
自1991年出现以来,Vim已经成为数以百万计软件开发人员所钟爱的文本编辑器。 而 Neovim 就是它的下一个版本。

在过去的23年里,软件开发生态系统经历了无数增长和创新。Neovim创始人Thiago de Arruda认为Vim缺乏当代元素,跟不上时代的发展。在保留Vim的招牌功能的前提下,Neovim团队同样在寻求改进和发展这个最受欢迎的文本编辑器的技术。早期众筹让Thiago de Arruda可以连续6个月时间投入到此项目。他相信Neovim社区会支持这个项目,激励他继续开发Neovim。

2015年度开源项目新秀: CockroachDB


6.jpg



前谷歌员工开发了一个开源的大型企业数据存储项目 CockroachDB ,它是一个可扩展的、跨地域复制且支持事务的数据存储的解决方案。

为了保证在线的百万兆字节流量业务的质量,Google开发了Spanner系统,这是一个可扩展的,稳定的,支持事务的系统。许多参与开发CockroachDB的团队现在都服务于开源社区。就像真正的蟑螂(cockroach)一样,CockroachDB可以在没有数据头、任意节点失效的情况下正常运行。这个开源项目有很多富有经验的贡献者,创始人们通过社交媒体、Github、网络、会议和聚会结识他们并鼓励他们参与其中。
 
2015年度开源项目新秀: Kubernetes


7.jpg



在将容器化软件到引入开源社区发展时, Docker 是一匹黑马,它创新了一套技术和工具。去年6月谷歌推出了 Kubernetes ,这是一款开源的容器管理工具,用来加快开发和简化操作。

谷歌在它的内部运营上使用容器技术多年了。在2014年夏天的DockerCon上大会上,谷歌这个互联网巨头开源了Kubernetes,Kubernetes的开发是为了满足迅速增长的Docker生态系统的需要。通过和其它的组织、项目合作,比如Red Hat和CoreOS,Kubernetes项目的管理者们推动它登上了Docker Hub的工具下载榜榜首。Kubernetes的开发团队希望扩大这个项目,发展它的社区,这样的话软件开发者就能花更少的时间在管理基础设施上,而更多的去开发他们自己的APP。

2015年度开源项目新秀: Open Bazaar


8.jpg


OpenBazaar是一个使用比特币与其他人交易的去中心化的市场。OpenBazaar这一概念最早在编程马拉松(hackathon)活动中被提出,它的创始人结合了BitTorent、比特币和传统的金融服务方式,创造了一个不受审查的交易平台。OpenBazaar的开发团队在寻求新的成员,而且不久以后他们将极度扩大Open Bazaar社区。Open Bazaar的核心是透明度,其创始人和贡献者的共同目标是在商务交易中掀起一场革命,让他们向着一个真实的、一个无控制的,去中心化的市场奋进。

2015年度开源项目新秀: IPFS


9.jpg


 
IPFS 是一个面向全球的、点对点的分布式版本文件系统。它综合了Git,BitTorrent,HTTP的思想,开启了一个新的数据和数据结构传输协议。

人们所知的开源,它的本意用简单的方法解决复杂的问题,这样产生许多新颖的想法,但是那些强大的项目仅仅是开源社区的冰山一角。IFPS有一个非常激进的团队,这个概念的提出是大胆的,令人惊讶的,有点甚至高不可攀。看起来,一个点对点的分布式文件系统是在寻求将所有的计算设备连在一起。这个可能的 HTTP 替换品通过多种渠道维护着一个社区,包括Git社区和超过100名贡献者的IRC。这个疯狂的想法将在2015年进行软件内部测试。

2015年度开源项目新秀: cAdvisor


10.jpg



cAdvisor (Container Advisor) 是一个针对在运行中的容器进行收集,统计,处理和输出信息的工具,它可以给容器的使用者提供资源的使用情况和工作特性。对于每一个容器,cAdvisor记录着资源的隔离参数,资源使用历史,资源使用历史对比框图,网络状态。这些从容器输出的数据跨越主机传递。

cAdvisor可以在绝大多数的Linux发行版上运行,并且支持包括Docker在内的多种容器类型。事实上它成为了一种容器的代理,并被集成在了很多系统中。cAdvisor在DockerHub下载量也是位居前茅。cAdvisor的开发团队希望把cAdvisor改进到能够更深入地理解应用性能,并且集成到集群系统。

2015年度开源项目新秀: Terraform


11.jpg


Terraform 提供了一些常见设置来创建一个基础设施,从物理机到虚拟机,以及email服务器、DNS服务器等。这个想法包括从家庭个人机解决方案到公共云平台提供的服务。一旦建立好了以后,Terraform可以让运维人员安全又高效地改变你的基础设施,就如同配置一样。

Terraform.io的创始者工作在一个Devops模式的公司,他找到了一个窍门把建立一个完整的数据中心所需的知识结合在一起,可以从添加服务器到支持网络服务的功能齐备的数据中心。基础设施的描述采用高级的配置语法,允许你把数据中心的蓝图按版本管理,并且转换成多种代码。著名开源公司HashiCorp赞助开发这个项目。

荣誉奖: Docker Fig


12.jpg



Drone 是一个基于Docker的持续集成平台,而且它是用Go语言写的。Drone项目不满于现存的设置开发环境的技术和流程。

Drone提供了一个简单的自动测试和持续交付的方法:简单选择一个Docker镜像来满足你的需求,连接并提交至GitHub即可。Drone使用Docker容器来提供隔离的测试环境,让每个项目完全自主控制它的环境,没有传统的服务器管理的负担。Drone背后的100位社区贡献者强烈希望把这个项目带到企业和移动应用程序开发中。

开源新秀


13.jpg


 
作者: Black Duck Software
Linux中国|译者: sevenot 收起阅读 »

环信CEO:“即时通讯云+移动客服”为App打造用户体验闭环

  随着移动互联网的发展,即时通讯、移动客服已经成为了很多移动应用的必备功能,环信作为新晋移动即时通讯PaaS平台服务商,凭借着近期刚刚上线的跨平台移动端客服产品吸引了大量应用开发者的关注。为了进一步了解移动客服产品的发展现状,InfoQ专门对环信联合创始人及...
继续阅读 »


ban9.png


 
随着移动互联网的发展,即时通讯、移动客服已经成为了很多移动应用的必备功能,环信作为新晋移动即时通讯PaaS平台服务商,凭借着近期刚刚上线的跨平台移动端客服产品吸引了大量应用开发者的关注。为了进一步了解移动客服产品的发展现状,InfoQ专门对环信联合创始人及CEO刘俊彦进行了专访。
 
InfoQ:环信成立已经有两年的时间,能聊一下这两年环信的整体发展吗?
 
刘俊彦:环信在2013年4月成立,2014年6月第一个产品”环信即时通讯云”正式上线。然后在今年4月份上线了移动客服产品。在过去的一年里面,环信做了三轮融资,分别是去年5月份的天使轮,去年8月份的A轮,和去年10月份的A+轮。截止到今年5月底,一共有23000款APP在使用环信即时通讯云SDK。环信的SDK覆盖了2.5亿的终端用户,环信即时通讯平台日活是千万级别,每天处理将近两亿条消息。这就是环信过去两年大概的情况。
InfoQ:移动客服可以算是现在移动应用通讯领域的一个刚性需求了,您能否谈一谈移动客服的形式都有哪些?
刘俊彦:虽然移动客服是一种移动互联网时代的新产品,但其实也只是形式上的一个创新。基本上每一个APP的设置页面都会有一个“意见反馈”的功能。其实这个功能就是一个客服功能,只是受限于技术、资源等因素,需要以表单的形式来呈现,有的时候还需要用户选择类别、提交联系方式,很难做到实时更新。

最近这一年,随着移动电商、O2O,在线教育,在线旅游,互联网金融产品的发展,出现了很多形式的客服产品。比如:作为电商,做O2O一定要做售前、售中、售后。这个过程中就涉及到用户与商家的沟通。

常见的沟通形式有4种,第一种是在App里面提供一个按钮,用户点击该按钮直接跳转到QQ,然后通过QQ去完成与商家的交流。

第二种是通过链接跳转微信。第三种形式就是我们最开始提到的表单形式,这种形式是非实时的。

最新的一种形式是用即时通讯的方法来跟商家沟通。用户打开一个窗口,在该窗口可以发语音、图片、文字,可以跟商家进行实时互动,这种形式就类似与微信、旺旺。第四种形式是最受大家欢迎,受O2O的商家、电商、医疗、互联网金融认可度最高的的一种客服形式,这种形式非常有利于用户通过手机与商家进行沟通。

InfoQ:针对这种移动客服的形式,存在哪些技术难点?环信是怎样客服这些难点的?
刘俊彦:用IM来做客服虽然很方便,但它的技术门槛比较高。第一需要你的服务器能够做到千万级、亿级的并发处理。移动客服是基于IM的长连接技术实现的。举个例子,如果某个App有100万日活用户,那么用户的手机和客服服务器之间就存在一百万条长连接。一些大型的App可能会有几百万、几千万的日活用户,那么提供服务的厂家就需要支持几百万、几千万的用户长连接。如果你的平台要支持上几百家厂商,那么平台就需要有支持几千万到几亿用户同时连接的能力。

第二需要做到不丢消息,并且每一条消息能够做到最实时的到达。即时通讯服务是帮助商家来进行销售的,用户可能是在三线城市,也可能是在四线城市,网络环境可能是2G、3G或4G。要做到任何情况下,只要有网络,消息一定能够即时到达是非常困难的。但如果你的平台做不到,就会给商家带来损失。拿电商来举例:一个消费者想买一样东西,发了一条咨询消息,但这个消息丢失了,那么就意味着这个单子丢了。这个用户可能是商家花了很多钱,从其它平台导流过来的,但是因为一条咨询没有即时收到,结果流失了,这样就给商家带来了损失。

环信对于移动客服的技术已经非常成熟,因为环信从去年6月1日正式上线,做的就是即时通讯云。我们在即时通讯云这一块已经做到了全国有2.3万家APP使用,有2.5亿用户,平台的日活用户是几千万。两年多的技术储备让我们敢保证绝对不丢消息,并且消息能够非常实时的到达。

InfoQ:因为领域的不同导致用户流量分布特点也不同,所以说即时通讯服务里面会经常产生波峰波谷。能不能谈一谈环信在这个弹性方面的具体措施呢?
刘俊彦:解决该问题要从技术与非技术两方面着手。非技术其实就只能靠烧钱来解决。我们系统上大概有50%的余量,超过50%的压力之后,就开始加服务器。这样能保证系统在不到50%冗余的情况下运行。当然这也意味着有50%的容量浪费,其实这个“浪费”是应付一些不可预料的波峰和波谷。单个APP的用户行为基本是固定的。社交类APP,大概在晚上十点半会产生波峰。而有一些企业办公类、教育类App是在白天产生波峰。我们为两万多家APP提供服务,综合起来整个波峰和波谷就会比较平均一些。

我们也采取了一些技术手段来解决该问题。现在有一些云服务平台提供秒级计算API。当到半夜两点钟,所有APP都进入波谷后,我们就会调用API释放掉一批服务器。但这样带来的经济效益也不高,因为秒级API走的是另外一套收费体系。

InfoQ:环信开放了UI源码,现在用户可以深度定制应用的UI。环信还建立了自己的开源社区,能不能谈一谈环信在开源方面有什么样的规划?是否考虑给开发者开放更灵活的API,或者是直接开源一些核心技术?
刘俊彦:环信的四位创始人有三位都是长期在开源社区工作的。而我从03年以后,基本上没有做过商业软件。所以开源精神已经深深植入到每一位创始人。

除了UI开源之外,我们还建了一个自己的开源社区。在过去的两年里,我们看到了一个很有意思的现象。很多人用环信来做社交,有单聊、群聊、匿名群聊等等各种玩法。但是归纳之后,大概可分为几十种。所以我们希望通过社区的力量,把这几十种常见的社交模块做出来,然后用开源的形式提供给大家。当用户想要做一款新的社交APP时,基于环信这样的底层云服务模块,有可能会节约几个月的时间。

有了上面的基础,我们就想走的更远一点。当你想做一款APP的时候,你可能用到一个朋友圈的功能,用到一个匿名的功能,我们希望这些功能也变成一个现成的模块,甚至整个APP都能够以开源的形式完整呈现。这样大家在开发一款APP的时候,就像是搭积木,选不同的积木模块,然后把他们拼起来。特别是对于创业者,可以把更多的时间用于提升用户体验。

最近这一个月我们开放了三个应用级别的模块:第一个是凡信,它是高仿微信的一款APP。凡信实现了单聊、群聊、朋友群等功能。这是我们社区里一个网友开发的,他完全无私的把服务器端和客服端代码开源出来。第二个是一个类似于陌陌的陌生人交友APP,功能主要是看附近的人,看到附近的人之后可以跟他聊天。第三个是我们面向企业的开源产品。上面提到的开源项目大家都可以到我们的社区下载,当然你也可以成为这些项目的贡献者,一起来推动这些产品往前走。

InfoQ:最近IT行业内数据中心机房出的问题也很多,环信在异地多活这方面有什么样的规划吗?
刘俊彦:到现在为止,环信的云服务都托管在国内最一流的云服务平台上。按照他们的星级来说,都是最顶尖的机房,可靠性、安全性都是有保证的。但为了给大家提供更可靠、更放心的云服务,“异地多活”已经列入了我们的开发计划,未来的几个月环信的“异地多活”就会正式上线。
InfoQ:最近有消息说环信要开始新一轮的融资,您能不能谈一谈环信在短期内的发展计划呢?
刘俊彦:刚刚我提到过环信在过去一年里进行了三轮融资。我们目前其实正在做B轮融资,因为还在进行中,如果有更多的细节我会尽快告诉大家。

环信发展到现在已成规模,下一步我们会继续巩固环信在“即时通讯云”领域国内第一的位置,我们希望以最低的价格、最好的服务为做社交以及各种应用服务者提供即时通讯功能。其次我们在今年4月份上线了环信移动客服产品。移动客服是我们在即时通讯领域一个很大的扩展。在即时通讯领域有两种场景,一种是在APP里面用户和用户之间进行社交活动,另一种是用户与商家之间的聊天,也就是客服。我们一直认为任何一款APP,都需要即时通讯,那么要达到100%的覆盖,光做社交是无法实现的,所以我们要加上客服这一块。我们最终的愿景是用环信的力量和技术为每一款APP提供即时通讯功能。

  收起阅读 »

盘点:被互联网女皇报告点名,美国14家潜在“独角兽”SaaS公司

 这个slide是从女皇报告中节选出来的,互联网对消费者群体带来了颠覆式的影响,这种影响也诞生了伟大的新时代的互联网公司,Airbnb,uber,instantcart就是其中在住行吃方面的典范。而这种颠覆的影响将迅速扩展到企业端,于是企业SaaS服务拥有巨大...
继续阅读 »
 这个slide是从女皇报告中节选出来的,互联网对消费者群体带来了颠覆式的影响,这种影响也诞生了伟大的新时代的互联网公司,Airbnb,uber,instantcart就是其中在住行吃方面的典范。而这种颠覆的影响将迅速扩展到企业端,于是企业SaaS服务拥有巨大的机会。


1.jpg



而在美国,这些企业服务公司已获得极大的关注:

1、Slack:成立1年多估值28亿美元当人们谈论Slack时,经常会说它特别“有趣”。使用Slack不像是在工作,而是在“放松”,但是在这个过程中却能通过Slack把该做工作切实有效地完成。有3个方面的因素是Slack脱颖而出的关键:用户界面与众不同;使用感受与众不同;交流方式与众不同。


2[1].jpg



 2、Square:移动支付鼻祖,估值60亿美元Twitter之父Jack Dorsey的创办的Square,移动支付公司日交易额已达1亿美元,深入到中小企业、个体零售商和移动商贩里去,并不断拓展到各种O2O的订单支付环节。


3.jpg


  3、Stripe:在线支付挑战者,估值50亿美元由20多岁的两兄弟Patrick Collison和John Collison创办的Stripe为公司提供网上支付的解决方案。在这两位分别从麻省理工和哈佛辍学创业的兄弟看来,PayPal的电子支付流程是在是太繁琐太复杂了,他们的目标是要把在线接收付款服务变成像添加 YouTube 视频一般简单。


4.jpg



  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.jpg



  5、DocuSign:小签名大市场,数字签名公司估值30亿美元DocuSign是一家提供电子签名服务的初创企业,成立于2003年,总部位于旧金山,在全球有10个分支机构,共有1300名员工。通过DocunSign的服务,用户只需通过智能手机或平板电脑即可完成手写签名,免除了用户要通过传真或邮件签名的麻烦;同时DocuSign还通过数字签名等方式验证用户的真伪,从而帮助企业用户安全地在网上获取具有法律效力的电子签名。目前全球188个国家有超过10万家公司以及5000万个人都在使用DocuSign的电子签名服务。


6.jpg



  6、Intercom:应用内的客户沟通工具,估值4亿美元Intercom是一款面向企业用户的客户沟通上SaaS工具,可以让企业与用户的沟通轻易在应用和网站内实现,强大的后台管理系统能够实时监控客户反馈,指派任务并统计客户的订单转换率,而相对于传统的电子邮件营销产品,应用内消息能带来更好的用户参与度(基于回复率)。


conew_7.jpg



  7、Gainsight:帮你留住客户,估值6亿美元Gainsight公司整合了第三方应用,他们评估销售数据,使用日志,支持投票,调研报告,以及其他客户智能资源。通过收集和分析这些数据,让企业减少客户流失率。当客户出现离开风险的时候,Gainsight的系统会发出早期预警,企业因此可以采取行动,挽留客户,另外还可以提供一些工具,识别出那些对企业产品评价较高的客户,这样可以为企业带来提升销售量的机会。


conew_8.jpg



  8、Directly:众包客户服务中心,估值1亿美元Directly以帮助创业公司将客户服务外包给其他人。Directly 希望帮助客户通过提问与回答的方式向用户提供更好的体验,而注册这家网站的一干“专家”则希望能够因此获得报酬。Directly 用户在网站上发布问题后,系统将自动分析问题,并匹配可能解答这些问题的专家,然后通过短信提醒他们。然后专家们便可以通过 Directly 网页端或者 Android 应用进行解答。大多数问题会在数小时内得到妥善解决,一旦对答案感到满意,提问者便可以关注回答者。


conew_9.jpg



  9、Zenefits:疯狂的HR管理工具,2年估值45亿美元Zenefits 为中小企业提供免费的一站式云 HR 管理工具,简单地说,就是让 HR 杂活更简单便捷,包括员工的入职和离职手续办理,工资和福利发放,保险和退休基金办理,缴税缴费,专利追踪等等。和 Uber 一样, Zenefits 在推广过程中也受到了部分地方政府的管制。在美国犹他州, Zenefits 一度被判定为非法,罪名是不正当返利和诱导用户。罪名的由头是, Zenefits 免费提供保险办理的入口,如果用户通过 Zenefits 办理保险,保险公司需要支付给 Zenefits 5%的佣金。这样一来,绕过了保险经销商。


conew_10.jpg



 10、Anaplan:帮助企业业务建模,估值5亿美元Anaplan是一家基于云的企业销售、运营及财务建模与规划公司。其解决方案可以收集企业客户的销售、运营及财务数据,然后运用复杂的模型对企业的绩效、开支等情况进行深入分析,为决策提供支持。


conew_11.jpg



  11、Greenhouse:企业一体化招聘流程,估值3亿美元Greenhouse 是一家招聘领域的SaaS软件服务商,为企业提供招聘管理、求职信管理、面试、人才招聘广告评估等服务,提供求职者比较、推荐等服务。Uber、Pinterest、airbnb Snapchat、和Buzzfeed都是Greenhouse的客户。


conew_12.jpg



  12、Checkr:高效的自动化背景调查平台,估值0.5亿美元Checkr 使用相同的数据源,提供包括社会安全号码、历史地址、性犯罪检索、恐怖分子监控名单、国家犯罪记录、驾驶记录等方面的背景调查。但与传统调查报告不同的是,Checkr 提供了一个客户只需输入名字就能得到数据反馈的 API,简化了调查报告的手工操作过程,使得背景调查更加自动化,从而更容易链接到公司现行的入职流程之中。


conew_13.jpg


 
 13、Guidespark:数字化员工培训资料,估值2亿美元创立于 2008 年,为人力资源管理人员提供沟通和培训解决方案,将纸质文件变成电子文件,并将这些资料移动化。这样能够减少人力资源支持的时间,提升人力资源的效率。


conew_14.jpg


 
 14、Envoy:智能访客管理系统,估值2亿美元智能访客管理系统 Envoy 专门为科技范儿的互联网公司量身打造,简它将整个访客管理流程电子化,提供从登记信息到通知负责人再到访客管理的一条龙服务,大大提升了前台效率。


conew_15.jpg


 
来源:创业投资笔记
作者:苏东 收起阅读 »

群详情获取失败

在确保登录成功的情况下,并且初始化完成,可以进行群创建和聊天的情况下   调用群详情获取 //根据群聊ID从服务器获取群聊信息 EMGroup group =EMGroupManager.getInstance().getGroupFromServer(gro...
继续阅读 »
在确保登录成功的情况下,并且初始化完成,可以进行群创建和聊天的情况下
 
调用群详情获取
//根据群聊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站点中,图片往往是不可或缺的页面元素,尤其在大型网站中,几乎都将面临“海量图片资源”的存储、访问等相关技术问题。在针对图片服务器的架构扩展中,也会历经很多曲折甚至是血泪教训(尤其是早期规划不足,造成后期架构上很难兼容和扩展)。 本文将以一个...
继续阅读 »
在主流的Web站点中,图片往往是不可或缺的页面元素,尤其在大型网站中,几乎都将面临“海量图片资源”的存储、访问等相关技术问题。在针对图片服务器的架构扩展中,也会历经很多曲折甚至是血泪教训(尤其是早期规划不足,造成后期架构上很难兼容和扩展)。

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

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

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

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

用户的访问方式如下:

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

程序上传和写入方式:

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

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

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

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

针对上述最原始的架构,主要面临着如下问题:

  • 随着upload目录中文件越来越多,所在分区(例如D盘)如果出现容量不足,则很难扩容。只能停机后更换更大容量的存储设备,再将旧数据导入。

  • 在部署新版本(部署新版本前通过需要备份)和日常备份website文件的时候,需要同时操作upload目录中的文件,如果考虑到访问量上升,后边部署由多台Web服务器组成的负载均衡集群,集群节点之间如果做好文件实时同步将是个难题。


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

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

  • 优点:配置更加灵活,也能兼容老版本的上传和访问方式。



因为虚拟目录,可以指向本地任意盘符下的任意目录。这样一来,还可以通过接入外置存储,来进行单机的容量扩展。

  • 缺点:部署成由多台Web服务器组成的集群,各个Web服务器(集群节点)之间(虚拟目录下的)需要实时的去同步文件,由于同步效率和实时性的限制,很难保证某一时刻各节点上文件是完全一致的。



基本架构如下图所示:


1.jpg

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

上面提到的几种架构,在上传/下载操作时,都经过了Web服务器(虽然共享存储的这种架构,也可以配置独立域名和站点来提供图片访问,但上传写入仍然得经过Web服务器上的应用程序来处理),这对Web服务器来讲无疑是造成巨大的压力。所以,更建议使用独立的图片服务器和独立的域名,来提供用户图片的上传和访问。
 
独立图片服务器/独立域名的好处
 

  • 图片访问是很消耗服务器资源的(因为会涉及到操作系统的上下文切换和磁盘I/O操作)。分离出来后,Web/App服务器可以更专注发挥动态处理的能力。

  • 独立存储,更方便做扩容、容灾和数据迁移。

  • 浏览器(相同域名下的)并发策略限制,性能损失。

  • 访问图片时,请求信息中总带cookie信息,也会造成性能损失。

  • 方便做图片访问请求的负载均衡,方便应用各种缓存策略(HTTP Header、Proxy Cache等),也更加方便迁移到CDN。


......
 
我们可以使用Lighttpd或者Nginx等轻量级的web服务器来架构独立图片服务器。
 
当前的图片服务器架构(分布式文件系统+CDN)
 
在构建当前的图片服务器架构之前,可以先彻底撇开web服务器,直接配置单独的图片服务器/域名。但面临如下的问题:

  • 旧图片数据怎么办?能否继续兼容旧图片路径访问规则?

  • 独立的图片服务器上需要提供单独的上传写入的接口(服务API对外发布),安全问题如何保证?

  • 同理,假如有多台独立图片服务器,是使用可扩展的共享存储方案,还是采用实时同步机制?


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

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

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


2.jpg

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

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

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

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

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

总结,有关图片服务器架构扩展,大致围绕这些问题展开:
 

  • 容量规划和扩展问题。

  • 数据的同步、冗余和容灾。

  • 硬件设备的成本和可靠性(是普通机械硬盘,还是SSD,或者更高端的存储设备和方案)。

  • 文件系统的选择。根据文件特性(例如文件大小、读写比例等)选择是用ext3/4或者NFS/GFS/TFS这些开源的(分布式)文件系统。

  • 图片的加速访问。采用商用CDN或者自建的代理缓存、web静态缓存架构。

  • 旧图片路径和访问规则的兼容性,应用程序层面的可扩展,上传和访问的性能和安全性等。


收起阅读 »

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

记得在前面的文章中,我带大家一起从源码的角度分析了Android中View的事件分发机制,相信阅读过的朋友对View的事件分发已经有比较深刻的理解了。 还未阅读过的朋友,请先参考上文:http://www.imgeek.org/article/51 ...
继续阅读 »
记得在前面的文章中,我带大家一起从源码的角度分析了Android中View的事件分发机制,相信阅读过的朋友对View的事件分发已经有比较深刻的理解了。

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

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


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

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


7.png



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

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


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

[html] view plaincopy
01. 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.
可以看到,我们在MyLayout中添加了两个按钮,接着在MainActivity中为这两个按钮和MyLayout都注册了监听事件:
myLayout.setOnTouchListener(new OnTouchListener() {  
02. @Override
03. public boolean onTouch(View v, MotionEvent event) {
04. Log.d("TAG", "myLayout on touch");
05. return false;
06. }
07.});
08.button1.setOnClickListener(new OnClickListener() {
09. @Override
10. public void onClick(View v) {
11. Log.d("TAG", "You clicked button1");
12. }
13.});
14.button2.setOnClickListener(new OnClickListener() {
15. @Override
16. public void onClick(View v) {
17. Log.d("TAG", "You clicked button2");
18. }
19.});
 我们在MyLayout的onTouch方法,和Button1、Button2的onClick方法中都打印了一句话。现在运行一下项目,效果图如下所示:


8.png



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


9.png



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

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

查阅文档可以看到,ViewGroup中有一个onInterceptTouchEvent方法,我们来看一下这个方法的源码:
/** 
02. * Implement this method to intercept all touch screen motion events. This
03. * allows you to watch events as they are dispatched to your children, and
04. * take ownership of the current gesture at any point.
05. *
06. *

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. *
  1. You will receive the down event here.
    14. *
  2. 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. *
  3. 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. *
  4. 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.}
如果不看源码你还真可能被这注释吓到了,这么长的英文注释看得头都大了。可是源码竟然如此简单!只有一行代码,返回了一个false!好吧,既然是布尔型的返回,那么只有两种可能,我们在MyLayout中重写这个方法,然后返回一个true试试,代码如下所示:
public class MyLayout extends LinearLayout {  
02.
03. public MyLayout(Context context, AttributeSet attrs) {
04. super(context, attrs);
05. }
06.
07. @Override
08. public boolean onInterceptTouchEvent(MotionEvent ev) {
09. return true;
10. }
11.
12.}
现在再次运行项目,然后分别Button1、Button2和空白区域,打印结果如下所示:


10.png


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

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


11.png



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

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

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

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

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


12.png



现在整个ViewGroup的事件分发流程的分析也就到此结束了,我们最后再来简单梳理一下吧。
 

  •  Android事件分发是先传递到ViewGroup,再由ViewGroup传递到View的。

  •  在ViewGroup中可以通过onInterceptTouchEvent方法对事件传递进行拦截,onInterceptTouchEvent方法返回true代表不允许事件继续向子View传递,返回false代表不对事件进行拦截,默认返回false。

  • 子View中如果将传递的事件消费掉,ViewGroup中将无法接收到任何事件。

  • 好了,Android事件分发机制完全解析到此全部结束,结合上下两篇,相信大家对事件分发的理解已经非常深刻了。


  收起阅读 »

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

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

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


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


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


1.jpg


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

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


2.png


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

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

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


3.png


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

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

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

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

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

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

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

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

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


6.jpg


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

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

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

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

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

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

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

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

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

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

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

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

使用函数作为命名空间

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// todo:
function readFile(fileName) { ... }
在页面中使用require函数:
 




demo





通过这种方式实现的模块化系统通常被称为是CommonJS模块风格的,Node.js正式使用了这种风格的模块化系统。这里只是提供了一个最简单的实现方法,在真实应用中会有更加精致的实现方法。

慢载入模块和AMD

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

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

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

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

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

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

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

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

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

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

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

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

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

小结

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

References

Eloquent JavaScript, chapter 10, Modules
Browserify运行原理分析
Why AMD?
JavaScript模块化知识点小结 收起阅读 »

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

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


41.png




42.png



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


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

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



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

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


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

43.png

 
  • 针对微信全网检测的固定账号做出特定的响应,主要包括一个文本消息响应,一个事件消息响应和一个客服接口调用验证,微信全网检测要求测试的固定账号接收到以上消息后,分别做出如下的响应:接收到TESTCOMPONENT_MSG_TYPE_TEXT这样的文本消息立即回复给粉丝文本内容TESTCOMPONENT_MSG_TYPE_TEXT_callback;接收到事件消息,立即以文本内容的消息格式回复粉丝内容event + “from_callback”,其中event需要根据实际内容替换为具体事件类型;接收到QUERY_AUTH_CODE:query_auth_code  这样的文本消息,需要立即响应空字符串给微信,之后调用客服接口回复粉丝文本消息,内容为:$query_auth_code\$_from_api,其中query_auth_code需要替换为微信实际推送过来的数据。主要的JAVA后台实现代码如下:
 
 /**     * 公众号消息与事件接收     *      * @param request     * @param response     * @throws DocumentException     * @throws AesException     * @throws IOException     */    @RequestMapping(value = "/open/{appid}/callback", method = RequestMethod.POST)    @ResponseStatus(HttpStatus.NO_CONTENT)    public void acceptMessageAndEvent(HttpServletRequest request, HttpServletResponse response) throws IOException, AesException, DocumentException {        String msgSignature = request.getParameter("msg_signature");        if (!StringUtils.isNotBlank(msgSignature))            return;// 微信推送给第三方开放平台的消息一定是加过密的,无消息加密无法解密消息         StringBuilder sb = new StringBuilder();        BufferedReader in = request.getReader();        String line;        while ((line = in.readLine()) != null) {            sb.append(line);        }        in.close();         String xml = sb.toString();        Document doc = DocumentHelper.parseText(xml);        Element rootElt = doc.getRootElement();        String toUserName = rootElt.elementText("ToUserName");         if (StringUtils.equalsIgnoreCase(toUserName, "gh_3c884a361561")) {// 微信全网测试账号            WeixinWholeNetworkTestService.getInstance().checkWeixinAllNetworkCheck(request,response,xml);        }else{            WeixinOpenService.getInstance().processMessageAndEvent(request,xml);            WeixinOpenService.getInstance().output(response, "");        }    }
 
  • 其中gh_3c884a361561这个账号是微信全网接入检测的固定账号,针对全网检测需要对该账号做特出响应,一旦全网接入检测通过,这部分的代码是可以去掉的,只有全网检测的时候才需要这部分代码。

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

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

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

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


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

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

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

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

String url = "https://api.weixin.qq.com/cgi-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设备,作为开发者,一个无法回避的难题就是需要适配各种各样奇奇怪怪的机型。 设备机型不...
继续阅读 »
Android的碎片化已经被喷了好多年,随着国内手机厂商的崛起,碎片化也越来越严重,根据OpenSignal的最新调查,2014年市面上有18796种不同的Android设备,作为开发者,一个无法回避的难题就是需要适配各种各样奇奇怪怪的机型。

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

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

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


51.png


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

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


52.png


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


53.png


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


54.png


PPI 对应屏幕尺寸

DP

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

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

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


55.jpg


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

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



56.png



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


57.png



screens_densitiesSP
 
SP 

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

58.png



changing_android_font_size
 
参考文献

http://developer.android.com/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社区的欢迎。...
继续阅读 »
Android Studio是目前Google官方设计的用于原生Android应用程序开发的IDE。基于JetBrains的IntelliJ IDEA,这是Google I/O 2013第一个宣布的作为Eclipse的继承者,深受广大Android社区的欢迎。在经过漫长的测试阶段后,最终版本于去年12月发布。

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

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

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

安装H.A.X.M

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



31.png




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


312.png




2. genymotion

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



313.png




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

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

3. Android  Drawable Importer

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



314.png






315.png




安装Android  Drawable Importer


Drawable-Importer.gif



4. Android ButterKnife Zelezny

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

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




316.png



以及使用之后:



317.png




安装ButterKnife Zelezny:


ButterKnife-Zelezny.gif



5. Android  Holo Colors Generator

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

安装 Holo Colors Generator:


41.gif



6. Robotium Recorder

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

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

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



412.jpg




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

7.jimu Mirror

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

安装jimu Mirror:


413.gif



8.Strings-xml-tools

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

安装Android Strings.xml tools:


414.gif



您有更优秀的Android Studio插件吗,欢迎在留言中告诉我们~ 收起阅读 »

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

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

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

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

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

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


1.png


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

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

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

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

具体实现步骤如下:

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

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

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

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

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

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

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

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

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

public Demo() {

}

public void hello() {

}

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

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

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

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

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

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

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

public class Demo {
public Demo() {

}

public void hello() {

}

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

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

public Demo() {

}

public void hello() {

}

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

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

工具地址:https://github.com/alexgreenbar/open_tools.git
作者:Alex 收起阅读 »

环信CTO马晓宇:开源重燃移动IM大连接梦想

    2014年,微信凭借着QQ得天独厚的优势牢牢把控着国内的市场,陌陌通过资本力量洗牌一骑绝尘奔赴美国纳斯达克上市,Facebook 拆资190亿美元收购WhatsApp,买下了一个未来。当人们都认为移动IM局面已经尘埃落定时,众多通过移动IM走向社交类的...
继续阅读 »
    2014年,微信凭借着QQ得天独厚的优势牢牢把控着国内的市场,陌陌通过资本力量洗牌一骑绝尘奔赴美国纳斯达克上市,Facebook 拆资190亿美元收购WhatsApp,买下了一个未来。当人们都认为移动IM局面已经尘埃落定时,众多通过移动IM走向社交类的APP也相继拿到风投,加入了这场无法预知的战争。在2015年的今天,移动互联网将进入活跃度时代,随着人们对手机依赖感的提升,用户需求不断在变化,4G的迅速发展,移动IM又掀起一场入口争夺战。WhatsApp股东的知名风投机构红杉资本高管合伙人Aaref Hilaly曾表示,移动聊天正在重新定义社交网络,诸如Facebook等充斥陌生人关系的社交模式,已经显得没有价值。移动IM将迎来一个巨大的创新机会,无论是社交、电商、教育、甚至嵌入式领域,都可以创造一个大链接时代。

     "我们的方向是即时通信云,主要面向多端,除了现有的iOS、Android和Web端,我们实际还在做Linux嵌入式,会支持更多嵌入设备,加入IOT(物体组成的因特网)实现设备之间互联。"

    与记者谈起移动IM的发展以及环信未来的方向,作为环信CTO的马晓宇从移动通信领域的实践经验和感悟来表达了自己的看法。同时作为一个开源理想主义者,他和整个团队致力把环信逐步开源,也把开源运用在整个工作环境里面,让企业跟随着开源世界的脚步,坚定在技术路线上前进,让一切都变得美好起来。


wKiom1UYNwDCJmxQAACwdgCMSuw087.jpg



     马晓宇,环信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月份正式上线。


wKioL1UYOGPjYE2ZAADuoQziNd0748.jpg



环信后台架构图

    记者:在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. 如果别人的回帖对你有帮助,记得点“感谢”
1. 问题尽量描述清楚,一般需要包括平台、编程语言、版本、错误信息
2. 问题是否得到了解答,需要回帖说明,如果有时间可以总结解决过程并贴出来
3. 如果你认同别人的回复,记得点赞; 
4. 如果别人的回帖对你有帮助,记得点“感谢”

APICloud牵手环信 为你的App量身打造精巧丰富社交功能

移动互联网时代,用户对于移动应用的各种功能的要求正变得越来越高,而即时通讯就是其中之一。说到通信,可能首先想到的是微信、陌陌、易信这样的工具,但实际上,IM通信功能被更广泛地用于其他的移动应用中,例如会议系统、协作工具,或是其他的社交平台中的附属功能。App开...
继续阅读 »
移动互联网时代,用户对于移动应用的各种功能的要求正变得越来越高,而即时通讯就是其中之一。说到通信,可能首先想到的是微信、陌陌、易信这样的工具,但实际上,IM通信功能被更广泛地用于其他的移动应用中,例如会议系统、协作工具,或是其他的社交平台中的附属功能。App开发过程中如果让开发者自己去做,不仅耗时耗力,要保证数百万甚至上亿人即时通讯的顺畅、安全,可不是一件易事。

561830035.jpg


环信是国内即时通讯云服务领域第一品牌,提供基于移动互联网的即时通讯能力,如单聊、群聊、发语音、发图片、发位置、实时音频、实时视频等,通过云端开放的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我们到底知道多少呢。请看看下面的问题, 如果这些你都知道,那么本文章的内容就请绕道,如果你还不太清楚,我想看了...
继续阅读 »
    曾经有人这么说过,在iphone里你看到的,摸到的,都是UIView,所以UIView在iphone开发里具有非常重要的作用。那么UIView我们到底知道多少呢。请看看下面的问题, 如果这些你都知道,那么本文章的内容就请绕道,如果你还不太清楚,我想看了下面的内容,你就明白了。
 

  • 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

1.jpg

    使用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

2.jpg

    拉伸视图

    你可以指定视图的某部分为可拉伸的,以便当视图的尺寸改变时只有可拉伸的部分被影响到。可拉伸的部分通常给按钮或者其他的部分为重复模式的视图。由你指定的可拉伸区域允许沿着两条或者其中一条轴拉伸。当然,当一个视图沿着两条轴拉伸的时候,视图的边界必须也定义了一个重复的模式来避免任何的扭曲。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中的坐标系统

7.jpg

 
    因为每个视图和窗口都定义了它自己的本地坐标系统,你需要留意在任何时间内是哪个坐标系统在起作用。每次绘制或者改变一个视图都是基于一个坐标系统的。在某些绘制中会基于视图本身的坐标系统。在某些几何结构变更中是基于父视图的坐标系统的。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之间的关系

6.jpg

    即使你可以独立的改变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 翻转一个视图和它的内容

5.jpg

    重要:如果一个视图的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 与视图对象进行交互

4.jpg


以下的步骤分解了图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)和控...
继续阅读 »
我们知道MVC是个模式,一个开发模式,一个构架模式,一个放之四海而皆为准的模式。
那什么是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  相关人员将会收到邮件提醒,以便及时回复
小窍门:关于环信的问题,提交问题后,
  • 如果是关于android的问题,邀请se_android
  • 如果是关于ios的问题,邀请se_ios
  • 如果是其它问题,记得邀请SE

 相关人员将会收到邮件提醒,以便及时回复

用swift集成iOS聊天页面

今天有人问swift集成问题,临时写了一个。基于ios sdk2.1.8,没有集成实时语音电话页面,但是应该也能用,抛砖引玉。 http://pan.baidu.com/s/1dDlOYzv
今天有人问swift集成问题,临时写了一个。基于ios sdk2.1.8,没有集成实时语音电话页面,但是应该也能用,抛砖引玉。
http://pan.baidu.com/s/1dDlOYzv

开发技巧:iOS 9适配系列教程--后台定位

Demo:GitHub地址:https://github.com/ChenYilong/iOS9AdaptationTips 【iOS9在定位的问题上,有一个坏消息一个好消息】坏消息:如果不适配iOS9,就不能偷偷在后台定位(不带蓝条,见图)!好消息:将允许出...
继续阅读 »

1.jpg


Demo:GitHub地址:https://github.com/ChenYilong/iOS9AdaptationTips
【iOS9在定位的问题上,有一个坏消息一个好消息】坏消息:如果不适配iOS9,就不能偷偷在后台定位(不带蓝条,见图)!好消息:将允许出现这种场景:同一App中的多个location manager:一些只能在前台定位,另一些可在后台定位,并可随时开启或者关闭特定location manager的后台定位。
如果没有请求后台定位的权限,也是可以在后台定位的,不过会带蓝条:
【iOS9在定位的问题上,有一个坏消息一个好消息】坏消息:如果不适配iOS9,就不能偷偷在后台定位(不带蓝条,见图)!好消息:将允许出现这种场景:同一App中的多个location manager:一些只能在前台定位,另一些可在后台定位,并可随时开启或者关闭特定location manager的后台定位。
如果没有请求后台定位的权限,也是可以在后台定位的,不过会带蓝条:

2.jpg


 // 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 配置如下:

1434597259325009.png


对应的 Info.plist 的XML源码是:

4.png


内容来源:ChenYilong 收起阅读 »

开发技巧:iOS界面布局之一——使用autoresizing进行动态布局

autoresizing是iOS开发中传统的布局模式。通过它可以设计控件与其父视图的变换关系。autoresizing是iOS中传统的界面自动布局方式,通过它,当父视图frame变换时,子视图会自动的做出相应的调整。 一、通过代码进行布局 任何一个view...
继续阅读 »
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//与父视图上边间距固定,下边可变
};

下面我们通过效果来看这些属性的作用:

  • 先创建两个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的自动布局属性如下:


这时的效果如下:

124749_YHKL_2340880.png


改变view1的frame如下:
UIView * view1 = [[UIView alloc]initWithFrame:CGRectMake(20, 40, 300, 300)];
效果如下:

2.png

这时view2的下边距离相对父视图是可变的。
设置如下:
view2.autoresizingMask=UIViewAutoresizingFlexibleHeight;
效果如下:

3.png


可以看出,这时子视图的高度是随父视图变化而自动改变的。
如下设置:
view2.autoresizingMask=UIViewAutoresizingFlexibleLeftMargin;
效果如下:

4.png


这时子视图的左边是随父视图变化而可变的。
同理,UIViewAutoresizingFlexibleRightMargin将使子视图右边与父视图的距离可变。
UIViewAutoresizingFlexibleTopMargin将使子视图上边与父视图距离可变。UIViewAutoresizingFlexibleWidth将使子视图的宽度可变。
注意:这些自动布局的属性是可以叠加的,比如保持视图与父视图边距不变,如下设置:
view2.autoresizingMask=UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;
效果如下:

5.png


二、nib文件中可视化设置自动布局
在storyboard中我们可以更加轻松的进行autoresizing自动布局。在view设置栏中有autoresizing这个设置,点中相应的箭头,就是刚才我们探讨的设置选项。并且我们把鼠标放在这个上面的时候,右侧会自动为我们预览效果。

6.png


如果你觉得autoresizing很强大,那么你就太容易满足了,autoresizing可以满足大部分简单的自动布局需求,可是它有一个致命的缺陷,它只能设置子视图相对于父视图的变化,却不能精确这个变化的度是多少,因此对于复杂的精准的布局需求,它就力不从心了。但是有一个好消息告诉你,iOS6之后的autolayout自动布局方案,正是解决复杂布局的好帮手。
  收起阅读 »

移动互联网实时视频通讯之视频采集

一 、前言 一套完整的实时网络视频通讯系统包括视频采集、视频编码、视频传输、视频解码和播放。对于视频采集,大多数视频编码器对输入原始视频的格式要求是YUV420。YUV420格式是YUV格式的一种,YUV分为三个分量,Y代表亮度,也就是灰度值,U和V表示的是...
继续阅读 »
一 、前言

一套完整的实时网络视频通讯系统包括视频采集、视频编码、视频传输、视频解码和播放。对于视频采集,大多数视频编码器对输入原始视频的格式要求是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])里头提出来的。他的提议里包括了四层架构 - 数据,分析,集成,决策。尽管他...
继续阅读 »
RTBDA(Real Time Big Data Analysis)架构是目前大数据分析领域的流行架构理念,最早是由David Smith在他非常流行的博客([Reference 2])里头提出来的。他的提议里包括了四层架构 - 数据,分析,集成,决策。尽管他当初的提议主要是为了进行预测性分析计算,但后来这个架构逐渐演变为大数据分析领域的主流架构。如下图:

1.png


图1 :David Smith 的 RTBDA架构图

今天我们以这个主流架构模式来看看和理解环信分析系统的体系架构。

环信大数据分析系统架构由底向上基本上也可以分四层 :数据,分析,集成,决策。

环信大数据系统的最底层使用的是cassandra数据库,所有需要分析的数据存储在这个数据库里。环信也使用mysql数据库保存与分析系统管理相关的数据。这两个数据库里的所有这些个数据是整个分析系统的基础。数据层之上的第三层是分析层。环信大数据系统分析层使用spark做分析平台。各种社交指标分析算法的实现采用spark环境支持的scala,python 或者java 语言编写。Spark本身包含一个机器学习的库。环信的预测建模以这个库为基础。SPARK平台构成环信指标分析模型计算的基础。

2.png


图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栏  
2015.6.23
1.将”活动“菜单置于顶层
2.修改”活动“报名页
3.”发现“右下页加上赞助商LOGO栏
 

Docker五大优势:持续集成、版本控制、可移植性、隔离性和安全性

对于Docker,应该不需要进行详细的介绍了。它是最火热的开源项目之一,通过在容器中增加一个抽象层(a layer of abstraction),就可以将应用程序部署到容器中。在看似稳定而成熟的场景下,使用Docker的好处越来越多。在这篇文章中,我不谈论D...
继续阅读 »
对于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带来的好处。
  收起阅读 »

深度思考:互联网产业的困境与进化

在互联网、风险投资和资本市场互相结合、互相支持的运作机制下,依托“互联网产业资本市场估值特权”而制造的造富效应是十分惊人的。在这种造富效应的烘托下,互联网产业必然产生一系列的政治、社会、文化影响。这些影响是非常深刻的,它会反馈给科技、产业,影响基本的生活态度,...
继续阅读 »
在互联网、风险投资和资本市场互相结合、互相支持的运作机制下,依托“互联网产业资本市场估值特权”而制造的造富效应是十分惊人的。在这种造富效应的烘托下,互联网产业必然产生一系列的政治、社会、文化影响。这些影响是非常深刻的,它会反馈给科技、产业,影响基本的生活态度,进而影响人类未来的命运。

互联网公司在获得人类社会高科技公司的代表资格之后,开始塑造文化尤其是高智商、高技术人群的文化。如果将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的创立者老沃森和半导体发明者兼企业家肖克利都是非常典型的保守主义者,他们的初衷是为生产、军事服务。

但是,具有强烈传媒性的移动互联网的大发展其实对科技进步有着强需求――因为它催生了对大量高质量的图像、声音、视频的需求,带来了海量数据的传输和处理需求,这需要强大通信技术的支撑。可是经过前几年通信产业的优胜劣汰,西方通信设备企业已经没有几家能支撑研发这样的技术。华为领导人任正非这几年常说“要抢占大数据的战略制高点,占住这个制高点,别人将来想攻下来就难了……小孩拿着手机啪啪啪拍照不删减就往数据中心传,流量超几何级数的增长……华为要做太平洋那么粗(信息)管道的铁皮,全世界能做的没几家”,就是针对这个趋势说的。这个大数据才是真正的大数据技术,硬骨头。

事实上,正是这几年在中国悄然完成的光通信宽带革命支撑了新出现在公众眼前的互联网电视、智能手机产业。如果在西方设备企业已经无法完成这一社会需求的时候,中国的集成电路、光通信、无线通信技术能继续像任正非所说的那样取得主导地位,那么中国就能在未来人类生活――不管它走哪种内容路线――把握住其科技基础。从这个意义上说,中国必须扶植互联网之外的信息技术“中坚”企业。

以上述这种“媒体化”的趋向,互联网领域的成功企业表现出几个重要特点:
  • 其一,著名互联网公司只在人口大国中产生。
因为互联网行业技术门槛不高,关键是谁先发现点子并靠烧钱迅速铺开。爆炸性的铺开需要规模效应,尤其是近十年来兴盛的社交性移动互联网,互联网移动终端普及率达到一定水平的人口大国具有先天优势。因此当前成功的互联网公司几乎只存在于美国和中国(日韩俄等国的网站影响力均难以超出其国门,理论上也就一亿用户上下,这撑不起成功互联网企业):美国是人口大国且可以影响大量其他国家的人口,中国本身就是人口大国且手机、电脑已经普及,两国企业都天然享有十几亿人的市场,极容易成功。人口大国印度目前信息设备普及率还不高但正在迅速提高。因此可以预见,从现在起到2020年,必然会诞生面向印度市场的巨型互联网公司,就看是印度本地公司还是中国公司(或中国风投扶持的印度公司)到印度去占领市场了。另外,这种人口大国占优的网络经济规律这还会产生另一种效果,即未必是最领先、优秀的技术能在竞争中取胜。比如即时通信,日韩的LINE就比微信要好,但是日韩人口太少,而且LINE功能是服务于日韩那种极度先进的网络环境的,这反而限制了LINE在大人口市场的应用,网络属性天生有利于那些“中不溜”的应用技术,所以微信轻松超越了LINE。
  • 其二,成功的互联网公司通常是2C(to consumer,面向个人消费者)而不是2B(to business,面向公司型客户)。
因为2B业务受制于客户的理性和谨慎,其业务会受所属行业系统的一定约束。即便是信息产业历史上赶上最好时机的2B公司,例如1980年代赶上企业后台快速普及微型计算机机遇的微软,2003-2007年赶上中国出口爆炸式增长和江浙中小企业出口相关服务信息剧增机遇的阿里巴巴,也难以实现后来社交属性的Facebook这样的病毒式扩散。2C业务由于个人更容易非理性、情绪化,反而容易渗透。那么,什么样的2C业务最容易实现爆炸增长?从近十年经验看,娱乐化、社交化、主体内容由用户产生,对用户的碎片化休闲时间有充分黏性的网站容易实现。所以,近十年的明星互联网公司多倾向服务于生活、娱乐、社交。
  • 其三,互联网明星公司获得高估值后,其获得的资金将强化上游那些在技术密集型行业中已经获得成功的公司(他们在资本市场上已经不再被青睐)的地位。
因为互联网明星公司聚焦于用户体验,希望保证硬件不出问题,所以倾向于使用已成功公司提供的元件。其结果是,互联网公司或“互联网思维”的公司享受巨大的估值,可以以这个估值不断融资(上市前多次风投投资,上市后多次增发),而所融得的资金以收入形态流进了上游公司的口袋,成了资本市场变相支持已领先的上游公司。比如,小米公司在芯片上就必然使用高通的芯片,至多在红米等廉价品上使用联发科的芯片,乐视公司在液晶面板上必定使用夏普、三星的面板,这就造成强者恒强。近两三年,德州仪器、博通、意法-爱立信等西方居于第三位及以后的企业退出手机芯片领域就是证明。因此,西方半导体等领域不再有风投投入并不意味着其衰弱,而是这个产业已经由若干成熟公司把持。但这一规律对于追赶者如中国高科技公司是不利的,在很多领域,这将维护美日韩的既得利益。幸好,中国凭借本土加亚非拉的巨量人口市场和汇集大量经济型理工人才的巨型公司的内部产业链,还能支持有志气的上游技术公司的发展,例如在手机芯片领域,大量山寨智能手机支持了展讯的崛起,而华为公司凭借自有品牌手机的市场支撑,扶植了海思的成功,这两家中国半导体领军企业是在互联网模式公司之外维系了中国的技术传承的。当前互联网产业的深层次软肋2013年以来,互联网在美国资本市场股价一再高涨,在中国也得到了舆论的追捧。2014年8月,一部名为《互联网时代》的纪录片在中国播出,引发了资本市场和舆论界的极大轰动。这部纪录片的思索是比较深入的,它反复进行一种隐喻,那就是相对于二战前后贯彻科层制的工业时代,互联网时代具有“去中心化”的重要特征。由于中心化暗示着权威,那么去中心化就暗示着“平等”。所以,互联网时代也是平等的时代、大众的时代。这种“互联网是平的”论点过于笼统,有点像互联网的对外形象宣传。“去中心化”网络结构,符合技术趋势性,是存在的,会带来什么样的影响,需要再进行严肃的探讨。笔者相信真正有价值的网络――它很可能是去中心化的,必定是有极深的技术支持而且又能促进技术进步的网络。但当前这种媒体性很强的互联网模式,已经表现出若干深层次软肋。
  • 第一,应用大放异彩的同时基础科技储备开始吃紧。
互联网产业追求个体成功,追求明星效应,所以整天思考的就是大卖,就是用户体验,如何快速想到新的创意,快速变成现实产品,它加大了应用层面创新的动力。因为互联网,信息领域各种新产品、新服务层出不穷,软层面的变化很快。美国风投涉足的其他领域,如生物医药和新能源因为技术原因,更新速度远不如互联网,给风投带来的财富也远不如互联网,可见其中互相强化的机制。但正因为创新集中在应用层面,乃至越来越表层化,当年AT&T进行基础科学知识创造和储备的机制就基本上消失了。基础科技研发时间过长,不确定,吃利润,一旦研究成功又有很强的外部性,与资本市场逻辑存在直接冲突。在工业时代,基础科技只能在不求回报的公立学术机构、国家实验室、富裕商人身后捐赠的实验室(如卡文迪许实验室)、超大型公司内部研发部门里发展出来。二十世纪七八十年代的新型“极客企业”的逆反及其与资本市场的融合好比来了次性格大转变,开始急速消化之前几十年储备的基础科技财富,但是新增补充的基础科技知识资源越来越少。当然,谷歌在发达以后也开始支持很多不赚钱的大型基础性项目(比如谷歌地球、街景等)。但和过去的基础科技机制相比,这些项目通常看起来比较“好玩”,满足极客的好奇心,要让大部分人看得懂,多集中在应用层面(收购并大放光彩的安卓系统也靠近应用层)。而且这些项目更多集中在信息科技领域。对于其他大型制造业、工业系统则无能为力。这正是西方科技进步机制的深层次危机。
  • 第二,放大财富分化。互联网“明星企业”模式会进一步放大财富的分化。
“互联网是平的”论,与世纪之交全球化兴起时“世界是平的”论一样,过于模糊,所指不清。如果从经营的效果上看,互联网让知识门槛相对变低,“变平了”;从经营者角度出发,它让普通人更有上升的机会,普通人的小团队能够通过明星产品一举成名并发展成大公司,“变平了”。但就该经济模式造就的财富分配结果而言,它趋向于更加不平等的结果。1990年代,在资本市场慢牛的带动下,上市企业高管通过期权形态获得的高薪已经为当时的评论家瞠目。但到互联网时代尤其是2010年以来的移动互联网时代,一方面这个行业规模经济的需求更加强烈,强者一统天下的局面更加突出(如百度阿里腾讯),另一方面又叠加了资本市场赋予互联网企业的特权而放大估值(市值财富),互联网上市公司股权市值增值造成的财富分化比传统行业上市公司更加夸张。而这种财富分化会产生激励效应,主导人才流动方向,人才大量流向互联网行业,媒体性的互联网行业的聚焦又不在于钻研积累,结果增加了未来科技基础的脆弱性。
  • 第三,UGC平台泡沫。UGC(User Generated Content,用户生产内容的平台模式)是2004年前后web2.0(博客、维基是第一批2.0产物)的思想精髓,到社交互联网时代放大,是“互联网思维”的核心。
但其实早年的论坛就是UGC的原型。既然互联倾向于走2C的业务模式,UGC就是2C业务最重要的经营哲学。从近十年的效果来看,UGC模式良莠不齐,有些平台网站确实实现了高质量的UGC,但更多的UGC平台,尤其是移动互联网时代大量APP是有问题的。少数高质量的UGC网站,比如维基百科、知乎,扩大了知识网络上的创作源,把大量有才能的人的空余时间利用起来,让他们分享自己的“认知剩余”,进行免费的传播,扩大了这些在纸片时代仅局限于极少数人头脑的知识的影响范围――也就是越来越多的人成为“全能型极客”。但更多UGC的“社交互联网”,出于其利用人类原始本能的动机,刺激人们把碎片化时间都用在闲聊、交朋友、游戏等容易上瘾的“信息消费”上面,对于社会整体并不有利,而且极容易成为谣言的温床。其实,UGC在近两年甚至演化成一种投机懒汉的想法,都希望用户在上面弄内容――但有价值的内容是耗费工夫的,全国的精英也就只能支撑不多的几个UGC平台。
  • 第四,争夺用户导致末日心态。互联网产业重心在于抓用户。
这里又暗藏一个矛盾,首先互联网是免费模式,在实体经济意义上是要大量用户才能支撑起一点收入(比如网络文学,几十个读者才有一人付费)。在广告方面,近几年由于风投蜂拥而至,互联网广告单价有所提高,这其实是在挥霍性的使用用户资源。同时,互联网网站、APP又种类很多,大家都想着占使用者的时间,可是使用者每天就24小时,于是人们很快发现“人的时间”也是一种稀缺资源,成为互联网公司争夺的对象。其结果是:
  • 一方面在资本市场上“打肿脸充胖子”,阅读近几年互联网公司的上市招股说明书,可发现他们特别强调自己的“用户数”,动辄数千万,多则数亿。如果看完若干互联网公司的招股说明书,可能会很奇怪:为什么它们的用户数那么多但我自己却从来没有使用过,也不曾有印象周围的人使用过。答案是利用“用户定义”的伸缩空间,把那种每月登录一次(社交网站)的注册人也算做用户,通过强调用户数来获取估值――实质上大部分用户形成收入的可能性几乎为零。
  • 另一方面,这种对于“人的时间”资源有限的忧虑,又影响了互联网企业家的心态――虽然互联网在不断产生出新的想法,创造出新的边疆,但在“人的时间”这个大陆里面不断加塞新想法将使得互联网世界越来越拥挤,因此对于单个互联网公司来说“资源”是有限的甚至是在萎缩的。


所以从内心上来说,互联网人和19世纪后期以来的那些创业资本家的心态有很大不同,后者相信边疆无限,资源可以不断地被发现并转换成可利用的形态,是乐观派,而前者则认为资源有限,必须先下手为强,而且要主动攻击置对手于死地,在对手处于萌芽状态时就消灭它,这是一种深沉的悲观派,中国互联网圈内崇拜《三体》这部科幻小说就是这种心态的极好反映。而这种基于资源有限的深沉悲观正和资本市场上互联网公司被撑得极大的市值(反映了资本的高期望)形成了巨大的张力。

真正的互联网精神

今天,互联网思维在中国得到了广泛认同。但上文所述的种种偏媒体泡沫化的现象,显然指向其中的问题。那么,有没有互联网精神呢?笔者相信当然是有的,世界上有真正的互联网精神,应该把它同媒体化、泡沫化的伪互联网精神区分开来。那么,真正的互联网精神在哪里?恐怕要从它的源头说起。

正如在互联网UGC的实践中,精英聚在一起还是精英,垃圾聚在一起仍然是垃圾的结果所暗示的,真正的互联网精神是通过共享的网络平台实现的“精英共和制”和“无限边疆观”。当前的互联网模式已经把这种精神在很大程度上掩盖了,变成了比表层工夫――在中国干脆成了比噱头。

需要指出的是,互联网尤其是近几年来的移动互联网带来的对既有资源的更有效利用,是创造了很大价值的。但是,这是在生产力老本上进行资源优化配置,生产力基础还没有被驱动进步。网络本身有可能成为一种先进的生产力(例如,在先进的传输和终端技术的支持下,人们实现知识的快速自动吸收和分享),但这种未来场景靠目前的互联网文化很难实现――因为它意味着生产层面的技术大变革,而当前移动互联网的热门集中在对原有生产力资源的消费上,比如2013年以来最时髦的O2O模式(“线上线下联动”),相当于把人类现代社会中的生活方方面面再用互联网走一遍,按照2012年以来美国资本市场的玩法,每个领域都可以创造一个至少大中型市值水平的上市公司。 收起阅读 »