Android – Handler 源码分析

android开发我也是在学习阶段,由于喜欢看源码,所以顺便把经常用到的一些类都看看顺便分析一下他们的工作机制。

Handler在android开发中应该比较常用,主要用于接收异步处理结果的消息并更新UI,比如做了一个天气应用,用户点击刷新的按钮的时候,为了不让界面卡住,这时候应该起一个异步任务去获取数据,然后等数据获取完成之后才更新UI的结果。不同于js更新web上的元素,android的UI更新操作都不是线程安全的,所以需要在同一个线程下才能更新UI,而js是没有多线程的概念的,所有操作都在同一个线程中,所以不会有线程安全的问题。来看看Handler的官方注释:handler.comment

简单翻译一下:一个Handler可以让你用来发送和处理消息(Message),以及消息上附带的Runnable对象,整个是跟消息队列(MessageQueue)一起使用的。每一个Handler实例会关联到一个唯一的线程和该线程的MessageQueue。如果你创建了一个Handler,他将会跟创建这个Handler的线程和该线程的消息队列绑定在一起。也就是说通过把消息发往这个队列和在出列的时候处理他们。Handler一般有两种应用产景,(1)就是调度消息和runnable对象在未来的某个时间点执行(归纳起来就是消息的发送);(2)可以把消息发送到其他线程里面。再简单概括一下就是Handler是跟创建他的线程绑定在一起的,然后通过消息队列方式,实现线程安全的操作。

1. Handler的创建

当你在一个Activity(或者其他)里面new一个Handler的时候,他最终会执行下面这个构造函数:handler.create

第一段if可以无视,是判断Handler是不是静态的,如果不是给你一个警告说可能会内存泄露。Looper.myLooper()其实就是在当前线程的ThreadLocal中获得Looper对象,同时从looper中获取MessageQueue。这里的callback其跟覆盖handleMessage的机制差不多,估计是为了某些地方使用了这种方式,然后一直兼容到现在。

2. 消息的发送机制

当我们调用handler的send()/post()系列的方法的时候(这些方法的调用一般发生在异步处理的任务完成之后,像触发线程的更新操作),handler做了什么操作?他们实际上殊途同归,最终都调用了这个方法:handler.sendMessage

再看看enqueueMessage方法:handler.enqueue

第一句把msg.target设置为当前的handler本身,这一步很重要,因为在这之后,就跟handler没关系了,后面会分析;接着就调用消息队列的入列方法把消息体丢到队列里面排队等待执行。

可以看到,不管是什么方式,最终都是把消息(runnable最后也是包装成消息)丢到消息队列里面。

3. Looper的工作机制

Looper的工作机制就跟他的名字一样,就是一个循环器,作为MessageQueue的消费者,进行消息的出列和处理操作。其核心代码就在loop()方法中,来看看looper

看到for(;;)我们就能猜到这是一个典型的生产者消费者模型了,在for循环里面,looper不断的订阅消息队列的下一个元素(next()方法),然后调用Handler的dispatchMessage方式分发消息给handler进行处理。上面说到设置msg.target那一步很重要就重要在这里。消息处理完之后还可以回收再利用,这点不细说了(如果你的Message是new出来的,回收了也没用,科学的使用方式是使用Message.obtain() 系列的方法来创建消息。)这里消息一个一个处理完之后才会处理下一个,是单线程串行执行的,而且跟创建handler的线程是同一个线程,所以完美的避免的线程安全的问题。那么你的疑问会是,究竟是谁来调用这个loop方法的,不是会卡住吗?是的,这就是精髓所在,这里的looper是UI线程在初始化完所有的UI操作之后调用的,这样一来,就不会有卡住的问题了。

4. 消息分发机制

上面Looper拿到消息之后,会调用Handler的dispatchMessage进行消息的分发操作,这里分发不是简单的调用Handler中的handleMessage方法,而是有其他逻辑在里面的,来看handler.dispatchMessage

惊呆了有木有,handleMessage方法是优先级最低的!为什么呢,因为有Handler有个post方法呀。post方法的参数是一个Runnable对象,然后通过创建一个Message,再把message的callback设置为这个runnable,然后再发送到消息队列里面。post方法的场景是你更新UI的时候需要知道获取到了什么新的数据,然后直接更新。而handleMessage方法可以不需要知道更新了哪些数据,就仅仅更新UI就可以了(当然,神奇的Message里面还配置了bandle,可以传数据的,所以其实都差不多,哈哈)。至于Handler的mCallback成员则是回调函数的通用写法,跟handleMassage的方式差别也不大。到这里就能明白为什么handleMessage在定义的时候不是abstract protected void handleMessage()了吧,因为他确实不是必须的。。。

分析到这里,可以看出,handler之所以可以更新UI,不是系统做了什么神奇般的兼容,而是因为他跟UI线程使用的本来就是同一个线程,UI线程通过Looper.loop来等待消息的分发,handler发送消息后把消息放到消息队列里面,而Looper负责从消息队列里面拿数据,又交给handler进行处理,最终实现了UI的异步更新操作。这是个生产者消费者模型典型应用,其中消息队列的功劳巨大,我们来看看他有哪些功能。如果我们开发中也需要实现这种类似的生产者消费者模型,可以使用这一套机制。需要注意的是,MessageQueue我们不能单独定义来使用,因为其核心API的可访问修饰符都是包级别的,我们不能把代码定义到android.os这个包下面,所以他要通过配合Looper来使用。Looper的核心API都是开放的。

5. 消息队列

enqueueMessage入列,可以把消息放到队列里面,这里队列的底层是android的本地代码实现的,其实可以参照juc里面的DelayQueue的实现,机制差不多,都支持延时出列的,这不过实现方式不同罢了。

next()出列,消息出列,队列的出列优先级是入列的时候定义的时间来决定的,时间值越小(长整形)优先级越高。

RESTful与伪RESTful

REST

关于RESTful,前几年就开始火了,其崛起是跟各大互联网开放平台的OPENAPI息息相关的,twitter的API就是声称完全RESTful的,不过其实呢,还是有些是伪RESTful的。那么,什么是RESTful呢?

REST很早(2000年)就有个博士( Roy Fielding,HTTP规范的主要编写者之一)提出来了,英文全称是REpresentational State Transfer,直译过来就是表述性状态转移,看起来更像物理学概念有木有?其核心理念就是把提供服务的所有事物定义一个ID,而这个ID在HTTP服务中通常是一个URI,然后使用标准的HTTP方法(HTTP Method,比如GET, POST, PUT, DELETE, HEAD,TRACE等)来提供同一个资源的不同服务(通常就是CRUD)。至于无状态这一点,HTTP都是无状态了,实际上也可以不用去深究。

那么,RESTful究竟是怎样一种风格呢?看几个例子:

http://www.zhihu.com/people/jiacheo

http://weibo.com/1790181393/AxqXitFF6

http://weibo.com/1790181393

看出来了没有,这里的URI都是 资源URI+资源ID组成的,也就是说,我们要访问一个资源的某种服务的时候,可以直接附上ID,就能获取了,如果要修改资源呢?HTTP方法中有POST, PUT, DELETE等可以对应用来新增/修改/删除资源的操作,但是现实中,我们大多数人用的浏览器都仅支持GET和POST两个方法,所以这个时候RESTful的理念在WEB站点中就无法完美实现了。上面说到,RESTful火起来那段时间刚好是各大开放平台开始崛起的时间,因为他们的OPENAPI大多数是RESTful风格的,由于提供API比展示页面要简单很多,仅提供CRUD操作就可以实现了,这么一来,HTTP METHOD就足够用了,而且各种编程语言提供的httpclient基本也都支持这几个方法,所以RESTful能在OPENAPI中大行其道跟API的简洁性是分不开的。

不能简单的认为没有后缀名的URI就是RESTful的,也不能认为有后缀名的URI就不是RESTful的,RESTful风格不在于你有没有后缀名(RESTful的API接口中,后缀名通常是为了实现区分返回的数据格式,如xml/json等),而是在于你的URI定义是不是对应到一个资源(这里通常是个名词),而不是一个操作(通常是动词)。由于浏览器不支持更多的HTTP方法,所以伪RESTful诞生了。

怎样的URI是伪RESTful呢?比如说这个:

http://www.zhihu.com/people/edit

这个是以动词结尾的URI,定位到的不是一个资源,而是一个动作(编辑),这个页面主要是用于编辑个人资料的时候使用的,如果要RESTful化,该怎么设计呢?

http://www.zhihu.com/people?to=edit

谁说RESTful不能有URL参数的?没有参数你如何实现复杂的查询(query)?

上面说到twitter的api中也有伪RESTful的,咱们来瞧瞧:

http://api.twitter.com/users/show.format

这里的URI是定位到show这个动作,然后传入的参数是用户的ID, 实际上, 如果遵循RESTful风格的话,可以这么搞

http://api.twitter.com/users/id.format 或者 http://api.twitter.com/users.format?id=

伪RESTful也是有存在的价值的,由于浏览器的原因,仅通过HTTP方法无法支持所有的资源的操作,那么何不把对资源的操作也定义一个URI呢?这样一来,伪RESTful其实对网站的开发者更友好,因为他一看这个URI就能知道对应后台的controller(MVC中的C)中的哪一个处理器,在写代码,调试的时候也有优势,不用去看POST的参数中对应的action(动作)究竟是谁,直接就知道了。

那么RESTful与伪RESTful孰优孰劣呢?其实没有什么定论,适合就好,管他是不是RESTful呢(你是处女座?当我没说)!但是当你在对外宣称你提供的API是标准的RESTful风格的时候,就要好好审视一下自己的API是不是标准的RESTful风格了。

国内的网站RESTful风格的也有,比如新浪微博,他的访问页面基本上都是标准的RESTful风格的(修改数据的URL我没研究,不能下定论)所以关于RESTful与伪RESTful,你是怎么看的呢?

apache & tomcat https配置

出于安全的考虑,一是确实能带来一点安全性提升,二是让用户看起来安全,一些网站开始要使用https协议来提供服务(web,api),刚好我们新产品是面向企业级的用户,需要提供https服务和接口,所以就遇到了配置问题。

首先这篇文章是在你申请好了https(ssl)证书之后该干的事。本文介绍是linux系统下的apache2的https配置,windows下的基本相同,就是一些文件的路径写法不一样还有模块的安装不一样。

使用apache2的话,他自己内建了(build-in)mod_ssl 模块,不需要额外编译和安装,如果你用的apache1.*系列的,可以使用 sudo yum install mod_ssl 来给apache安装mod_ssl

如果是内建的mod_ssl, 不需要再httpd.conf里面配置LoadModule,默认是可以直接用的,如果是自己安装的,则需要LoadModule一下。下面讲具体配置

1. 首先你申请到https证书后,服务提供商会给你如下几个文件。

server.crt

server.key

ca.pem

sub.class1.server.ca.pem

说明一下, server.crt 是服务提供商颁发给你的证书,是一个纯文本文件,一般以

-----BEGIN CERTIFICATE-----

开头,以

-----END CERTIFICATE-----

结尾,前后都没有多余的空格。

server.key就是这个证书的私钥文件,用来解密用的,也是一个纯文本文件,以

-----BEGIN RSA PRIVATE KEY-----

开头,以

-----END RSA PRIVATE KEY-----

结尾,首位也都不带空格。

另外两个*.pem文件,是CA证书,其中ca.pem是root证书,sub.class1.server.ca.pem 则是颁发证书的网站给你的的这个https的证书的服务商证书,也就是说你要认证你的https(ssl)证书必须先通过这个sub.class1.server.ca.pem再通过ca.pem来完成整个链路的验证.

2. 接下来就是apache配置证书文件的路径了

先把上述文件都统一放到一个目录,要注意保障文件的安全,不要被盗用或者删除。这里我们放在 /var/www/ssl 目录下面

打开httpd-ssl.conf文件(一般在%apache安装目录%/conf/extra下面

首先配置证书文件:

SSLCertificateFile "/var/www/ssl/server.crt"
找到相应的项,有的话就覆盖,没有的话就新建一个,指定证书文件的位置
接下来是配置证书私钥文件:
SSLCertificateKeyFile "/var/www/ssl/server.key"
配置方法同上
最后配置证书链路文件路径
上面说到,我们证书要先通过sub....pem然后在通过root认证,所以需要把这两个文件合并。
cat  sub.class1.server.ca.pem  ca.pem  > ca.chain.pem.crt
然后定位到这个ca.chain.pem文件
SSLCertificateChainFile "/www/var/ssl/ca.chain.pem.crt"
至此,整个证书的配置就OK了,可以重启下apache,访问以下你的https://域名/ 就能看到已经生效。
3. 配置完证书后,配置一下apache请求转发给tomcat,有多种方式,可以用mod_jk,也可以用ProxyPass,我这里用ProxyPass。
还是编辑同一个文件httpd-ssl.conf
在<VirtualHost _default_:443>定义块的里面,添加ProxyPass

ProxyPass / ajp://localhost:7204/
ProxyPassReverse / ajp://localhost:7204/

我这里是把请求转发给ajp去处理,tomcat默认都会有ajp的端口,因为我应用比较多,为了区分开了,所以我自己使用了7204端口,如果默认的话,应该是8009端口,是在不清楚可以看下tomcat的配置(%tomcat安装目录%/conf/server.xml内的Connector protocol="AJP/1.3" 这个配置项)
如果我这里使用ProxyPass / http://localhost:8080/ 可不可以,答案是可以的,但是你的tomcat在处理request的时候,无法分辨用户访问的url是不是https的(当然apache可以配置保留用户的原始请求url给tomcat)
4. 大部分的https的ssl证书是颁布给某个子域名的,如果你的所有子域名都需要上https,建议使用购买一个可以多次使用的。对了,一开始可以申请startssl来用一下,可以免费使用30天,测试完了,可以再买高端的。
运行起来了,但遇到如下情况,该怎么办
nosecure
不要慌, 这时候你的https已经生效了,唯一的问题是你的页面请求的资源使用了其他非https的资源,比如cdn的图片,css等的引用,如果是js资源,建议都要使用https,不然浏览器会认为很不安全的哦。

通过apache proxy访问tomcat ERR_CONNECTION_RESET 问题排查

0. 这是个钓鱼贴你造么,看到后面你就造了。

1. 现象, 通过浏览器访问 www.qipeng.com 返回页面链接被重置

查看apache-access 日志, 是有发起请求的, 说明在proxy那里存在问题

2. ping www.qipeng.com 结果是DNS已经生效的
1
3. 改用mod_jk, 而不是proxy来处理请求转发, 发现服务器身上是可以curl通的
2
注意这台curl的机器不是企朋的服务器本身, 而是在服务器上随便找的
4. 然后用本地访问依旧是
3
5. 怀疑是DNS问题, 于是本地配置了hosts, 把www.qipeng.com 强制指向 223.4.49.236
结果还是一样, 本地配置别的域名 weike.taovip.com 指向 223.4.49.236, 访问这个域名, 结果是正确的, 我嚓, 这个正是我的企朋的页面!
4
6. 那么到底他妈的问题出在哪里呢,难道我被我自己墙了?找其他同事, 结果也是一样的- -||
找了半天, 突然想起老大好像昨天刚申请域名备案, 于是开始往这方面靠, 结果真正的原因是域名正在备案中, 期间访问不了, 呵呵! 天朝, 我又涨姿势了!
7. 解决方案
7.1 其实不需要解决, 产品也是刚做, 还没啥用户, 自己本地配置个其他域名先顶一阵等备案好了就OK了.
7.2 真的必须要访问的话可以找个国外的服务器, 先跑一跑, 前提是你的程序没有太多其他的依赖, 否则得不偿失.
7.3 知乎某知友的对策: http://www.zhihu.com/question/19794926

 

Google Java Style 中文翻译

Google Java Style 终于在前不久发布了,抽空学习了一下,同时生成了一个java代码注释风格的中文解释,如果你有兴趣可以看看。

java source code 在此: github下载

代码中的注释基本上已经把Google Java Style中提到的一些要求写进去了,而且是出现在该出现的地方。主要是缩进的地方我跟google要求的2个字符的缩进没有保持一致, Google的这篇文档中要求的是2个字符,而我个人一直以来都是习惯使用tab来缩进,所以这个恕难从命哈。

基本上跟以前的java编码习惯差不多,就把一些Java 7之后的新的语法特性也提到了,更之前的广为流传的Sun的Java Style区别不大。如果你之前是跟Sun的保持一致的,基本上风格上不需要大改的。

其实编码风格无所谓好坏,最重要的是能让别人更容易的读懂你的代码,这并不是说每一行代码都要写注释,注释不能解决一切问题,更重要的是各种变量和名称的命名规范,这个很重要。如果代码本身就能自解释,那才是极好的!

用mongodb构建延时队列

延时队列(DelayQueue)的使用场景有很多,比如订单类的系统,用户创建订单后一段时间内如果没有付款,那么要把用户的这个订单关闭掉,同时把库存还原回去。解决的方案有很多,一种是用定时任务,定时去扫描符合条件的数据出来进行出来,还有一种就是把这个丢到延时队列里面,等时间到了自动出列之后处理。

由于我们业务场景需要实时的延时队列,也就是必须准时处理,如果通过定时扫描的话,如果时间间隔短,会任务太多处理不过来,如果时间间隔长,会导致中间的有一些延时出列了。之前有调研过一些已有的产品,像twitter的beanstalkd,就是用来处理延时的任务的。不过当时由于在测试环境跑起来了,但是在线上却一直不可用,另外也没有人熟悉这个产品,怕后期带来运维的问题,所以也没有用起来,于是决定自己开发了。google一把,很多人在吹嘘用mongodb替换消息队列(MQ),看起来也很简单,于是我们准备试一下,用来做延时队列。这一试,就定下来了,然后在线上跑了1年多了。

先说一下我们部署mongodb的机器配置,4核CPU,8G内存的VM。你没看错,我们所有数据库都部署在淘宝聚石塔的虚拟机上面。这里不是做广告,如果你要做应用,建议不要用聚石塔,哈哈。我们是做淘宝业务,有“安全性”的限制,所以才用。

之所以用mongodb做“队列”,是因为mongodb有一个叫findAndModify的操作,这个操作是原子性的,也就是你可以修改一条记录的同时把老的记录返回。基于这个操作,我们可以在把一条记录标记为处理中的同时获取到这条数据,这样别人如果同时也是进行这样的处理,由于这个操作具备原子性,你们处理的任务不会重复,所以简单实现了出列的问题。所以简单的, 我们可以这样设计我们的延时队列里面的字段设计如下:

timestamp:出列的时间戳,这样我们可以根据系统当前的时间判断该记录要不要出列

status:状态,用来标记该记录目前是可以出列还是已经出列。0、1就可以搞定了

data:数据,表示你要保存到队列中的数据,出列后根据这个数据来处理任务。

定义好了,出列的操作就是:


//获取当前时间戳
long current = getCurrentTimeMillis();
//0是初始化状态,表示可以出列, 1表示已经出列过了.
DBObject obj = collection.findAndModify({status:0, timestamp:{$lte: current}},{$set:{status: 1}});
//没有命中, 说明当前不需要出列
if(obj == null){
return null;
}
//获取数据, 序列化为二进制的话, 可以兼容各种各样的数据格式
byte[] data = obj.get("data");
//反序列化
return SerializableUtil.deserial(data);

上面是伪代码, 既不是java的也不是javascript的.

是不是很简单,我们的队列搞定了。
如果故事到这里就结束了,那我就不写这篇文章了,呵呵呵呵呵。

由于业务的增长,我们在延时队列里的数据越来越多,处理任务的机器不够用了,不得不剥离成多台机来处理这些任务,这时候作为一个“队列”的挑战才刚刚开始。

由于多台机,你必须要让每台机处理的数据没有重复。上面的findAndModify已经很好的满足需求了是不是?是的,确实满足了。但是findAndModify有个致命的弱点,就是不支持批量findAndModify。由于每天要处理上千万的延时数据,如果全部请求都压到mongodb上,我们的mongodb吃不消,关键我们也不想多加机器。所以问题来了,我们要批量的findAndModify。于是乎,我们顺延这findAndModify的特性,自己弄了一套批量操作的findAndModify,代价是引入分布式锁,在批量find和批量modify的这段时间加分布式锁,这样能保证数据不会被重复处理。由于分布式锁我们用了zookeeper来实现,zookeeper的链路稳定性是我见过最差了,没有之一。时不时客户端就会断开连接,时不时客户端就会session expired。而面对这些问题,zookeeper是没有直接提供接口给你解决的,因为他的接口实在是太底层了,估计写那套client的人以前是搞底层系统开发的,所有问题都要调用方来处理,真不是一般的难用。zookeeper的问题始终是可以解决的,但接下来一个更大的问题是队列的吞吐量(qps)。由于改成了批量处理之后,mongodb平时的负载确实变低了好多。而平时一次出列1000条数据,不会带来拥堵的问题,直到那一天。

没错,那一天就是2013年的天猫双十一,我们系统之前为此做了扩容,机器数量从平时的20台加到了50+台。我们预估当天的数据量会达到平日的6倍。但实际上那天的量,远超我们想象。订单量达到了平时的10倍以上,在双十一前10分钟,我们的延时队列就被冲垮了。我着急了,一看,堵了100+W的数据了。过了一个小时之后,这个堵的数据量达到了800+W。眼看着这么多订单,白花花的就溜走了。直到第二天10点,我们终于解决了这个问题。

解决方案很简单粗暴,就是把批量findAndModify这套方案直接干掉,由于总共有6台任务处理机器,我们直接使用单机的模式,一台机器使用一个队列,把分布式锁抛弃掉。也是用批量的方式,批量出列,处理数据,然后批量删除。效率极高,在后面的几次高峰期都顶住了压力,“顺利”度过了双十一。这种模式也带来一个问题,如果处理数据的机器下线了怎么办?这涉及到了数据迁移的问题,好解决,我们写了个小工具,可以很快的把遗留的数据迁移到另外的机器上。

另外的解决方案是把架构搞成C/S的,做成一个单独的server来访问mongodb,任务处理系统都来订阅这个server。这样也不会有锁的问题,只不过当业务量继续增加,需要扩展服务器的时候,同样的问题还是会来临。上面的单机解决方案看起来是很简单,但实际上效果也是很不错的。当你真正贴近业务的时候,你会发现不是要做一个架构多么牛逼的系统,而是应该做一个能简单维护却能很好解决问题的系统。

如果你想设计延时队列,可以先参考java的延时队列的实现:

java.util.concurrent.DelayQueue

如果可以直接在上面加一层持久层,就可以简单实现延时队列了。

一些总结:能不用锁的地方一定不要用锁,锁会把你的性能耗尽。能不用分布式锁的地方一定不要用,如果你的系统需要使用分布式锁,可以想想有没有其他的简单粗暴的方案,也许会有更好的搜获。分布式系统的一个特点是高并发,如果架上了分布式锁,很可能让你的系统变成了串行处理的系统了,这样就违背分布式系统的初衷了。

附 beanstalkd 项目地址:http://kr.github.io/beanstalkd/

 

关于用户研究

昨天晚上,黄猿师兄(@小猪和蜜桃的故事)过来给我们分享了关于用户研究的一些心得,感觉很不错,所以也分享一下。

1. 用户研究是隶属于UED的,在产品的各个阶段都需要用到他。

在产品未出来的时候,用户研究需要先行,进行用户调研,市场调研,竞品分析等等。在产品出来之后需要进行收集用户反馈,可用性测试,用户数据分析来改善产品的用户体验,最后当积累了一定量的大数据的时候,可以通过挖掘用户数据来推断出接下来做什么产品会受用户欢迎。大数据不仅仅是属于后端同学使用的,实际上数据的分析在改善产品方面有很大的作用。

2. 用户体验的五大层面。

从上到下是表现成,框架层,结构层,功能层和战略层。最顶层是用户直接能感受到的东西,而最底层是用户感知不到的,看起来很虚,但却很影响深远的一层。产品的形成,往往是由战略层开始,通过市场调研,发现做某类产品有市场,于是交给下游的产品经理做功能的需求设计,产品经理最后把功能和结构层框架层跟交互设计师一起确定,把产品的交互稿定下来,接下来就是UI表现层了。所以一个产品的产生往往是由战略层来决定的,他一开始影响深远,但随着时间的推移,他的影响会越来越小,直到战略方向要大调整的时候, 整个产品也即将面临大改版了。如下图

用户体验5要素的影响和时间的关系
用户体验5要素的影响和时间的关系

也可以看出,如果一个产品一开始的战略定位不对,后面调整起来,就是全盘都要调整了。(大公司的表现是,当产品换了一个新的老大,往往一个新的老大会想表现自己的‘杰出才能’,调整产品的战略,导致之做的很多工作要重新调整,最终反馈到用户的是:“哇塞,你们又升级啦!”“尼玛!这个按钮怎么没了?”“卧槽,排序算法又变了?” 等等)

3. 用户研究的手段

分类起来可以归结为两种:一种是面向人的定性研究,另一种是面向数据的定量研究

在做面向人的定性研究的时候,必须要把人物角色和使用场景都考虑到:

人物角色:好比说一个产品的使用者,是一个什么样的人?一个淘宝卖家客服,还是白领消费者,还是公司CEO,这些都必须要归类出来。然后对这些人分别进行定性研究。“你不能拿着你的社交APP去问一个还不回走路的小孩他的感觉是什么样的。” 也就是说你要先定位你要分析的人群, 然后再给这些人群归类,通过提取共性,抽象出人物角色。再进行分析。

使用场景:这是一个非常重要的因素却往往容易被忽视。比如你办公司的宽带是20M的,你不能把你的用户请过来你的办公司,让他体验你的产品顺不顺畅,而是应该走到你的用户所在的环境中去对用户进行调研。还有一些土豪公司,在自己的办公楼搞了一个XX产品用户体验式,里面是豪华的设备装修,8G16核的机器,然后邀请一批用户到你这里,给他们冲上一杯香浓的咖啡,然后让他们来体验产品,这种做法是跟用户的真正使用场景完全脱节的,这时候用户给你的反馈,一点用处都没有。

另外必须要注意的是,用户真正的需求,往往不是他们说出来的,而是通过观察感受出来的。人往往会有虚伪的一面,你问他,直接的反馈不一定是真实的反馈。(比如你要做一个男性成人用品的产品,需要知道用户的JJ长度分布情况,然后你调查的结果肯定会比大多数人实际长度长一些,无法做到客观。)

定量分析:

根据数据来做定量分析的结果,一般是比较客观的(数据足够多的时候)。但是数据只是反馈了客观的数据,你不能加上主观的猜测然后刚好符合数据的表现就说明是这样的。比如,你一个功能做上去好久发现没人用,然后数据分析的结果证实了这一点,如果这时候你说这个功能没用是不客观的,你要看这个功能有没有入口,然后在用户的使用场景下,找到这个入口困不困难等等。数据是客观的,但不能为了‘佐证’你某个观点而滥用数据。

数据分析一般是用来验证你产品新功能的决策正不正确。比如你通过ABtest发现新功能的用户活跃度和消费度更高了,说明这是一个好的功能可以上。反之,应该继续改进产品。

数据挖掘不同于数据分析,他问问可以挖出用户潜在的需求,这是用于帮助产品新功能或者说新产品的决策的。比如美国著名的视频网站netflix就是通过大数据分析他们的用户平时都喜欢看什么类型的电视剧,最后发现用户喜欢看剧情片,所以出钱投资拍摄了美剧《纸牌屋》,现在已经第二季了。当然数据挖掘的难度比数据分析更大,更有创造性。

题外话:netflix我用过他们的开源zookeeper客户端curator, 确实把zookeeper那些shi一样的接口包装得容易用了许多。(不过还是有少许bug, session expired后有些时候没有自动重连)如果你要用到zookeeper,可以试试他们的框架。

响应式交互网站

最近把博客升级了一下:

1. 主题换成了响应式(responsive)的主题

2. 评论系统使用disqus

响应式web设计
响应式web设计

为什么要这样呢?

随着移动互联网的崛起,越来越多的网站把他们的站点迁移到移动端,小公司(业务比较单一)的做法是直接做一个APP把web端的内容用app重新实现一把,而大公司一般(由于业务太多)是做一个移动平台的APP的皮,内嵌一个webview控件,里面跑的是H5的代码,可以保证各大智能手机平台(IOS, android)上访问到的内容是一模一样的,同时保证了开发的迭代速度。数据还是来自原来的数据,只不过是输出HTML5,同时做了响应式的交互罢了。这样同时可以保证APP不过于庞大的同时能提供跟web上一样的服务。在以前,wap流行的时候,wap浏览器渲染能力太弱,页面真心难看,所以大公司也不会太关注wap端的发展。但是智能操作系统的告诉发展为h5的流行带来了契机,移动端的体验容易上去了,各大小公司也就都来搞一把了。

响应式交互最早我care是谁提出来的,但是我最早知道的一个响应式交互这个词是来自于titter的bootstrap 的css框架,那时候(2011)还不懂什么是响应式交互,只不过感叹于这个框架的强大,所以用了起来。虽然他不兼容IE6,IE7两个原始浏览器,但是用来做后台系统的管理界面,那就甩出什么extjs,jquery-ui(我们都用过)好几条大街。如果你正好需要快速搭建后台系统,建议使用bootstrap。实际上现在好多创业型的小公司,为了快速开发同事保持界面的整洁,也采用了bootstrap框架来做开发(如果你团队缺乏专业的前端人才,正好拿来使用),兼容的事情,总会有别的框架帮你搞定。(我们的做法是只兼容到IE7,IE6直接提示用户去升级浏览器,否则是无法使用的。)

后来越来越多的地方有人提到了响应式交互,我的理解是,实际上就是自适应式的交互,页面的内容能够根据你浏览器当前的window size来调整内容(要么隐藏,要么换行)的显示,以期望你的网站在各种分辨率下都能正常访问。目前国内大多数网站的大多数页面都还是固定宽度的(900,950,1000等),很少有整体网站做了响应式交互的,因为实在不好实现。像国内UED团队比较活跃的淘宝网,也仅仅是首页和“我的淘宝”做了响应式的设计,除了这两个之外的其他页面都是固定宽度的(1000)同时主内容居中显示。

到了移动端,我们发现这个做响应式页面的难度降低了,因为目前主流的移动平台IOS和Android上的浏览器几乎都是全面支持H5和CSS3的,这样的话再移动端使用CSS3来写响应式的页面难度就大大降低了,这是移动互联网天然的优势,也是他之于pc互联网有大进步的地方。当然目前H5和CSS3在android平台上表现出来的性能还是不佳,导致一些使用app内容webview来加载h5内容的app我们经常要等好久(比如手机淘宝的app,用三星的s4,渲染一个页面还是太久,比iPhone5要慢个1两秒),另外是canvas的性能也不咋地。当然这主要是android系统性能不够优化的原因,但是我们看到android的进步很快,相信很快这两个系统直接的性能差距就会没有了,到时候正是H5和CSS3统一移动端的时候。

所以,你如果打算做一个新时代的网站,注意要多考虑响应式(responsive)交互以及移动互联网。

扩展阅读:

响应式网页设计  http://zh.wikipedia.org/zh-cn/%E5%93%8D%E5%BA%94%E5%BC%8F%E7%BD%91%E9%A1%B5%E8%AE%BE%E8%AE%A1

响应式web设计 http://www.yixieshi.com/ucd/11828.html

java 线程的几种状态

java thread的运行周期中, 有几种状态, 在 java.lang.Thread.State 中有详细定义和说明:

NEW 状态是指线程刚创建, 尚未启动

RUNNABLE 状态是线程正在正常运行中, 当然可能会有某种耗时计算/IO等待的操作/CPU时间片切换等, 这个状态下发生的等待一般是其他系统资源, 而不是锁, Sleep等

BLOCKED  这个状态下, 是在多个线程有同步操作的场景, 比如正在等待另一个线程的synchronized 块的执行释放, 或者可重入的 synchronized块里别人调用wait() 方法, 也就是这里是线程在等待进入临界区

WAITING  这个状态下是指线程拥有了某个锁之后, 调用了他的wait方法, 等待其他线程/锁拥有者调用 notify / notifyAll 一遍该线程可以继续下一步操作, 这里要区分 BLOCKED 和 WATING 的区别, 一个是在临界点外面等待进入, 一个是在理解点里面wait等待别人notify, 线程调用了join方法 join了另外的线程的时候, 也会进入WAITING状态, 等待被他join的线程执行结束

TIMED_WAITING  这个状态就是有限的(时间限制)的WAITING, 一般出现在调用wait(long), join(long)等情况下, 另外一个线程sleep后, 也会进入TIMED_WAITING状态

TERMINATED 这个状态下表示 该线程的run方法已经执行完毕了, 基本上就等于死亡了(当时如果线程被持久持有, 可能不会被回收)

下面谈谈如何让线程进入以上几种状态:

1. NEW, 这个最简单了,  
 
     static void NEW() {
          Thread t = new Thread ();
         System. out.println(t.getState());
    }
 
输出NEW
 
2. RUNNABLE, 也简单, 让一个thread start, 同时代码里面不要sleep或者wait等
 
   private static void RUNNABLE() {
         Thread t = new Thread(){
             
              public void run(){
                  for(int i=0; i<Integer.MAX_VALUE; i++){
                      System. out.println(i);
                 }
             }
             
         };
         
         t.start();
    }
 
 71e94764e28c7f8bbd3ef91c1c0088b4
 
3. BLOCKED, 这个就必须至少两个线程以上, 然后互相等待synchronized 块
          
     private static void BLOCKED() {
         
          final Object lock = new Object();
         
         Runnable run = new Runnable() {
             
              @Override
              public void run() {
                  for(int i=0; i<Integer.MAX_VALUE; i++){
                      
                       synchronized (lock) {
                          System. out.println(i);
                      }
                      
                 }
             }
         };
         
         Thread t1 = new Thread(run);
         t1.setName( "t1");
         Thread t2 = new Thread(run);
         t2.setName( "t2");
         
         t1.start();
         t2.start();
         
    }
 
8e9ad1eadf9d38c0b6c8cb024cb36c0c
这时候, 一个在RUNNABLE, 另一个就会在BLOCKED (等待另一个线程的 System.out.println.. 这是个IO操作, 属于系统资源, 不会造成WAITING等)
 
4. WAITING, 这个需要用到生产者消费者模型, 当生产者生产过慢的时候, 消费者就会等待生产者的下一次notify
 
     private static void WAITING() {
 
          final Object lock = new Object();
         Thread t1 = new Thread(){
              @Override
              public void run() {
                 
                  int i = 0;
                 
                  while(true ){
                       synchronized (lock) {
                           try {
                               lock.wait();
                          } catch (InterruptedException e) {
                          }
                          System. out.println(i++);
                      }
                 }
             }
         };
         
         Thread t2 = new Thread(){
              @Override
              public void run() {
                 
                  while(true ){
                       synchronized (lock) {
                           for(int i = 0; i< 10000000; i++){
                              System. out.println(i);
                          }
                          lock.notifyAll();
                      }
                      
                 }
             }
         };
         
         t1.setName( "^^t1^^");
         t2.setName( "^^t2^^");
         
         t1.start();
         t2.start();
    }
 
 b43a3d9b67bab266ffea4537fb043bba
 
5. TIMED_WAITING, 这个仅需要在4的基础上, 在wait方法加上一个时间参数进行限制就OK了.
 
把4中的synchronized 块改成如下就可以了.
 
synchronized (lock) {
   try {
      lock.wait(60 * 1000L);
   } catch (InterruptedException e) {
   }
   System. out .println(i++);
 }
 
 88d9047d8a709c2d63c695bcf58a0297
另外看stack的输出,  他叫 TIMED_WAITING(on  object monitor) , 说明括号后面还有其他的情况, 比如sleep, 我们直接把t2的for循环改成sleep试试:
 
synchronized (lock) {
    
    try {
          sleep(30*1000L);
    } catch (InterruptedException e) {
    }
    lock.notifyAll();
}
a37ef4c72c00e793f8b6c746d74fd4d9 
 
看到了吧, t2的state是 TIMED_WAITING( sleeping),  而t1依然是on object monitor , 因为t1还是wait在等待t2 notify, 而t2是自己sleep
 
另外, join操作也是进入 on object monitor
 
6. TERMINATED, 这个状态只要线程结束了run方法, 就会进入了...
 
    private static void TERMINATED() {
         Thread t1 = new Thread();
         t1.start();
         System. out.println(t1.getState());
          try {
             Thread. sleep(1000L);
         } catch (InterruptedException e) {
         }
         System. out.println(t1.getState());
    }
输出: 
RUNNABLE
TERMINATED
 
由于线程的start方法是异步启动的, 所以在其执行后立即获取状态有可能才刚进入RUN方法且还未执行完毕
 
 
废话了这么多, 了解线程的状态究竟有什么用?
所以说这是个钓鱼贴么...
 
好吧, 一句话, 在找到系统中的潜在性能瓶颈有作用.
 
当java系统运行慢的时候, 我们想到的应该先找到性能的瓶颈, 而jstack等工具, 通过jvm当前的stack可以看到当前整个vm所有线程的状态, 当我们看到一个线程状态经常处于
WAITING 或者 BLOCKED的时候, 要小心了, 他可能在等待资源经常没有得到释放(当然, 线程池的调度用的也是各种队列各种锁, 要区分一下, 比如下图)
6db341bbd7680bbc2e6ae37a66329397
这是个经典的并发包里面的线程池, 其调度队列用的是LinkedBlockingQueue, 执行take的时候会block住, 等待下一个任务进入队列中, 然后进入执行, 这种理论上不是系统的性能瓶颈, 找瓶颈一般先找自己的代码stack,再去排查那些开源的组件/JDK的问题
 
排查问题的几个思路:
 
0. 如何跟踪一个线程?
看到上面的stack输出没有, 第一行是内容是 threadName priority tid nid desc
更过跟踪tid, nid 都可以唯一找到该线程.
 
1. 发现有线程进入BLOCK, 而且持续好久, 这说明性能瓶颈存在于synchronized块中, 因为他一直block住, 进不去, 说明另一个线程一直没有处理好, 也就这个synchronized块中处理速度比较慢, 然后再深入查看. 当然也有可能同时block的线程太多, 排队太久造成.
 
2. 发现有线程进入WAITING, 而且持续好久, 说明性能瓶颈存在于触发notify的那段逻辑. 当然还有就是同时WAITING的线程过多, 老是等不到释放.
 
3. 线程进入TIME_WAITING 状态且持续好久的, 跟2的排查方式一样.
 
 
上面的黑底白字截图都是通过jstack打印出来的, 可以直接定位到你想知道的线程的执行栈, 这对java性能瓶颈的分析是有极大作用的.
 
NOTE: 上面所有代码都是为了跟踪线程的状态而写的, 千万不要在线上应用中这么写...

struts2 获取文件上传进度方法

一、上传机制

1. struts2 的 Dispacher 类中 wrapRequest 方法, 将http请求头中带有multipart/form-data 开头的请求都会包装成为 MultiPartRequest 的实例

 Dispacher.wrapRequest

这里 MultiPartRequestWrapper 构造方法就是用MultiPartRequest 再包装一层, 同时还会调用他的parse 方法, 进行文件的上传

2. 这里getContainer().getInstance 得到的对象, 是从struts的 ObjectFactory中得到的(可以在对应的struts配置文件的<bean>标签找到), 查看struts默认配置文件 struts-default.xml 可以看到下面这一句

<bean type="org.apache.struts2.dispatcher.multipart.MultiPartRequest" 
name="jakarta" class="org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest" scope="default" optional="true" />

这里的定义, 可以理解为:  Interface obj = cls.newInstance() 写法变成xml而已

default里面定义了两个, name 分别是 struts 和 jakarata, 作用却是完全一样的, 支持文件的分块上传, 这样大文件的上传才不会出问题.

3. 进入JakartaMultiPartRequest 类, 查看 parse方法代码

可以看出, 这里用的是 commons-fileupload 包来做文件上传的.

4. commons-fileupload 包中, ServletFileUpload 类可以使用一个 progressListener 回调对象来监听上传进度, 而这个JakartaMultiPartRequest 类中已经写死了, 没有使用任何listener:

 ServletFileUpload upload = new ServletFileUpload(fac);
 upload.setSizeMax(maxSize);
 List items = upload.parseRequest(createRequestContext(servletRequest));

这时候因为没有注入到监听器, 而且在这里会parseRequest, 也就是把文件上传过来并保存在saveDir目录中, 最后走到action,交给业务去处理。

以上是整个上传的机制,struts2通过封装commons-fileupload,直接免去了文件上传代码的编写,需要做的仅仅是在action里面保存文件以及一些后续工作而已。

但是上面的第4点中提到,因为代码写死了,没有指定监听器,也不能通过注入的形式(因为ServletFileUpload对象是临时new出来的),所以导致文件上传的过程中我们无法知道进度。

二、解决方案

5. 先理解为什么struts2为什么会使用jakarta来包装文件上传的请求,其实是在

     struts.multipart.parser 这个常量中指定的,默认情况下

   <constant name="struts.multipart.parser" value="jakarta"></constant>

   这里的value,就是在struts-default.xml 中定义的bean的name  (见第2节)

6. 所以解决的方法是, 我们自己来实现MultiPartRequest  这个接口, 然后自己定义一个bean, 比如叫 jakartaExt, 然后把5中的parser常量的值设置为jakartaExt, 就可以替换系统默认的包装, 用我们自己定义的了, 这个时候我们就可以给ServletFileUpload对象注入listener了.

注入listener
配置

注: 这里的uploadId仅是为了区分不同的上传线程而已(同一个用户可以开多个页面同时上传)

尝试上传文件, 可以看到日志打出来的结果:

2013-03-06 11:48:15,DEBUG,file upload, url=http://localhost/struts2/upload.htm
2013-03-06 11:48:15,DEBUG,[1362541661052] upload progress:0%
2013-03-06 11:48:28,DEBUG,[1362541661052] upload progress:10%
2013-03-06 11:48:38,DEBUG,[1362541661052] upload progress:20%
2013-03-06 11:48:48,DEBUG,[1362541661052] upload progress:30%
2013-03-06 11:48:58,DEBUG,[1362541661052] upload progress:40%
2013-03-06 11:49:08,DEBUG,[1362541661052] upload progress:50%
2013-03-06 11:49:19,DEBUG,[1362541661052] upload progress:60%
2013-03-06 11:49:30,DEBUG,[1362541661052] upload progress:70%
2013-03-06 11:49:40,DEBUG,[1362541661052] upload progress:80%
2013-03-06 11:49:52,DEBUG,[1362541661052] upload progress:90%
2013-03-06 11:50:05,DEBUG,[1362541661052] upload progress:100%
2013-03-06 11:50:05,INFO,[1362541661052] contentLength:2869568 parse use 109772ms

这样, 我们就可以通过ajax的形式, 从session中获取到对应uploadId的上传进度, 页面也就可以做出进度条了.

7. 另一种解决方案

     就是采用servlet来上传, 自己也用commons-fileupload组件来操作, 要注意的是, 把web.xml中struts2的filter-mapping 不能是 /*, 而是应该对应的 /*.action /*.htm 之类的, 否则如果拦截了所有请求, 这时候会先经过struts2的 filter而导致request被wrap掉(wrap的过程就会有parse的操作),导致servlet有响应的时候, 实际上已经上传好了...

我个人还是倾向6的做法, 因为这时候技能做到上传与业务无关, 也能监控进度, 如果在servlet中做的话, 你每上传一种文件都要一段新的处理逻辑挤进去(因为每种文件的处理逻辑不一样)