关于Java Memory Model

在这篇文章中我只打算讲一下Causality Requirements for Executions所提到的9条规则中的三条(5、6、7),这也是规范中不太容易理解的地方。当然,在此之前我会介绍一下与之相关的背景。我并不能保证这里所有的论述都是正确无误的,所以大家在阅读时如若发现问题还请指出。

1、背景

Java内存模型是由基本模型演化过来的,主要经历三个模型。

1.1、Sequential Consistency Memory Model

首先考虑的是Sequential Consistency Memory Model,它规定所有线程中的action需要按照一个total order来执行(每次执行时这个total order可以不一致),对于在单个线程中所执行的action,其顺序必须和代码中规定的顺序(program order)一致,并且每一步执行对所有线程来说都是可见的。这个模型太过强硬,要求和program order一致,如果使用这个模型,那么很多优化就做不了了,比如指令重排序以及读写缓存等。因此,需要找到一个稍微弱一点的模型。

1.2、Happens-Before Memory Model

接下来考虑的是Happens-Before Memory Model,这个模型规定了一系列happen-before关系,下面列出其中的一部分:

  • 如果x和y在同一个线程,并且在program order下x在y之前,那么x happen-before y
  • 一个volatile变量的写action happen-before后续对该变量的读action
  • 如果x happen-before y,并且y happen-before z,那么x happen-before z
  • ……

规范指出如果第一个action happen-before第二个,那么第一个action必须对第二个可见且排在第二个之前。看上面列出的第一条happen-before关系,这是不是意味着同一个线程内的指令无法重排了呢?当然不是,规范里面还提到一点来允许重排:

It should be noted that the presence of a happens-before relationship between two actions does not necessarily imply that they have to take place in that order in an implementation. If the reordering produces results consistent with a legal execution, it is not illegal.

除了happen-before关系外,Happens-Before Memory Model还对一个读action(r)所能看见的写action(w)进行了规定:

  • w happen-before r,并且不存在其他的写操作w’满足w happen-before w’并且w’ happen-before r
  • w和r之间不存在happen-before关系

第一点还比较好理解,但是第二点又是讲了什么?在阐明之前先引入一个称作data race的概念,在对同一个共享变量所执行的action(读或写)中,如果两个action之间没有happen-before关系,且至少有一个为写action,那么称这两个action之间存在data race。下面看看去掉第二点就会导致什么,假设有两个线程,一个线程有一个对变量的写action,另一个线程有一个对相同变量的读action,这两个线程之间没有做任何同步,也就是说这两个action之间不存在happen-before关系。只根据第一点,那么这里所提到的读action将永远看不见写action,这是不符合常理的,而引入第二点则允许data race的发生。

关于这两点,举个例子说明下:

init:a = 0, b = 0

Thread1        | Thread2
---------------|----------
1, r1 = a      | 3, r2 = b
2, b = 1       | 4, a = 2

列出有效的happen-before关系如下,将a happen-before b记为hb(a, b):

hb(a  = 0, r1 = a)
hb(r1 = a, b  = 1)
hb(b  = 0, r2 = b)
hb(r2 = b, a  = 2)

根据第一条规则,r2 = b可以看见0,r1 = a可以看见0;根据第二条规则,r2 = b可以看见1,r1 = a可以看见2。也就是说,对于这段代码,下面四种结果在该内存模型下都是允许的:

(a) r1 = 0; r2 = 0
(b) r1 = 2; r2 = 0
(c) r1 = 0; r2 = 1
(d) r1 = 2; r2 = 1

得到其他结果比较简单,对于结果d,我们看看如何执行才能得到它。编译器在分析Thread1时会发现1和2之间并没有相关,所以可以因为某些原因将b = 1提前,执行过2后在Thread2中执行3,接着执行4,最后由Thread1执行1,这样就得到了结果d。

到目前来看,Happens-Before Memory Model没有什么问题,但事实上它是有缺陷的。我们将上面的例子修改如下:

init:a = 0, b = 0

Thread1        | Thread2
---------------|----------
r1 = a         | r2 = b
if(r1 != 0)    | if(r2 != 0)
   b = 1       |   a = 2

此时唯一正确的结果是r1 = 0; r2 = 0。但是在Happens-Before Memory Model下,r1 = 2; r2 = 1也是合法的,因为这里happen-before关系不变,r2 = b可以看见b = 1,这将导致a = 2的执行,然后得到r1 = a = 2,到此已经执行完了,当我们拿个r1 = 2这个结果时,后验的发现这确实将导致b = 1的执行,因此r2看见b = 1也就没问题了,这种循环推理被称作因果关系(causality)。看着都感觉有些荒唐,也就是说,抽象的模型已经无法适应现实的程序了,需要改进。

1.3、Java Memory Model

现在我们遇到的问题在于什么时候可以将一个写的位置提前,规范给出:

An action can occur earlier in an execution than it appears in program order. However, that write must have been able to occur in the execution without assuming that any additional reads see values via a data race.

注:引用来自[1]

前面Happens-Before Memory Model中我们讲了一个读action可以看到的写action满足两个条件,一个是满足happen-before关系,另一个是不存在happen-before关系。而这里要求的是如果一个写action可以提前,那么它必须在仅仅看见与其满足happen-before关系的write条件下会发生,而无须考虑不存在happen-before关系的写action。还是看下前面的例子,在Happens-Before Memory Model中我们将b = 1提前了,应用上面的限制我们会发现,在Thread1中happen-before关系如下:

hb(a = 0, r1 = a)
hb(b = 0, r1 = a)
hb(r1 = a, b = 1)

在这种happen-before的限制下,b = 1是无法执行的,因此它不能被提前。

至此,我们得到了Java Memory Model,Java Memory Model仅仅在Happens-Before Memory Model添加了对causality的限制。

另一个需要考虑的问题是,给出一段程序和一个输出结果,如何判断这个结果是否合法呢?思路是,找到所有的合法结果,看看给定的结果是否在其中。事实上,找到这些结果本身是不可能的,但不妨碍我们对简单的案例进行分析。整个过程分为两步:

第一步,列出所有待提交的action,首先提交初始条件里的action,然后根据上文提到的happen-before限制来递归的提交剩下的action,通过这种方式我们可以找到哪些action一定会发生,因而可以将其提前。

第二步,重新开始一个新的提交过程,列出所有待提交的action,首先提交上一步中收集的可以提前的action(由于提交的action是经过检验的action,因此后续提交的读action是可以看见它们),然后递归提交剩下的action,全部提交完后便会得到一个结果,在提交过程中选择不同的提交顺序便会得到不同的结果。

下面通过一个例子(来自JMM规范制定小组所发布的一系列Test Cases)来简要讲一下流程:

init:x = y = 0

Thread1        | Thread2
---------------|----------
r3 = x         | r2 = y
if (r3 == 0)   | x = r2
  x = 42       |    
r1 = x         |   
y = r1         |    

output: r1 == r2 == r3 == 42

如果要得到r2 = 42,则需要将y = 42提前,那么能否这样做呢?编译器在对Thread1做intra-thread分析时会发现写入x的值要么为0要么为42,如果r3 != 0,那么必然有x = 42,如果r3 == 0,那么接下来就会执行x = 42,所以可以保证r1 = x看到的值为42,因此将其改为r1 = 42,将y = r1改为y = 42并提前。第二步中的提交过程如下:

列出所有待提交的action如下,括号里代表当前可以看到的值,这里省略了初始状态x = 0以及y = 0:

r3 = x  (0)
x  = 42
r1 = x  (42)
y  = r1 (42)	# to be committed
r2 = y  (0)
x  = r2

经过分析,y = 42可以提前,因此首先提交y = r1 (42):

y  = r1 (42)    # committed
r3 = x  (0)
x  = 42
r1 = x  (42)	
r2 = y  (0)     # to be committed 可以看见y = r1(42)
x  = r2

起初r2 = y只能看见happen-before关系的write,也就是y = 0,提交y = r1(42)后,r2 = y现在也可以看见与它无happen-before关系的y = 42,为了得到结果,接下来就提交r2 = y:

y  = r1 (42)    
r2 = y  (42)    # committed
r3 = x  (0)
x  = 42
r1 = x  (42)	     
x  = r2 (42)    # to be committed

提交x = r2:

y  = r1 (42)    
r2 = y  (42)   
x  = r2         # committed
r3 = x  (0)     # to be committed, 可以看见x = r2(42) 
x  = 42
r1 = x  (42)	# to be committed, 可以看见x = r2(42)

把剩下的两个action提交了:

y  = r1 (42)    
r2 = y  (42)   
x  = r2 (42)    
r3 = x  (42)     # committed
r1 = x  (42)	 # committed

到此说明了给出的结果是合法的。

2、Rules

这里不打算列出其他规则,以及规范在上下文中所给出的各种形式化定义,只列出开头所提到的三条规则,然后对其进行分析。如果说前面的流程看明白了,那么这里应该是水到渠成,只是形式化的东西多少显得有些抽象。
5 \(W_i|C_{i-1}=W|C_{i-1}\)
6 For any read \(r\in A_i-C_{i-1}\) we have \(W_i(r)\stackrel{hb_i}{\longrightarrow} r\)
7 For any read \(r\in C_i-C_{i-1}\) we have \(W_i(r)\in C_{i-1}\) and \(W(r)\in C_{i-1}\)

先看rule 6,这一条讲了对于当前正在提交的每一个读action (r),它所能看到的写action(w)都必须满足w happen-before r。我们知道happen-before关系是偏序的,不会得到违反因果关系的结果,这一条已经满足了causality requirements。但是只有这一条还不够,它限制了data race的发生,这不是我们所希望的,这也是引入rule 7的原因。根据前面的描述,我们不能让这个读action看到所有与它有data race的写action,一个自然的想法是当这个读action被提交后,它可以看见于它先提交的写action(justified action),因为既然写action已经提交了,说明会发生,那么这个读action可以看见它就是正常的事情。这样操作会导致一个读action在提交时和最终所看到的写action是不一样的。

事实上,我们可以将这里的\(Ai\)放宽为\(A\),改为\(r\in A-C_{i-1}\),意思是,所有未提交的以及当前正在提交的读action所能看到的写action都应该满足happen-before关系。只有当一个读action被提交后,它才有机会看到与它有data race的写action。

接着看rule 5,这一条不是很容易理解,为什么要对上一步的集合\(C_{i-1}\)进行限制呢?在rule 6中我们讲了,一个读action在提交时和最终所看到的写action可能是不一样的,那么改变在什么时候发生呢?根据rule 5,我们可以看出改变发生在提交读action后的下一个步。rule 5规定了对于上一步所得集合\(C_{i-1}\)中的每个读action,在这一步中它所看见的写action需要和最终它所看见的写action一样,也就是说,如果要做出改变,必须在这一步改变完并且固定下来。

如果将rule 5的限制改为\(C_i\)又会怎样呢?根据rule 6,正在提交的读action所看见的写action与其满足happen-before关系,改变后的rule 5又会限制这个所看见的写action与最终该读action所看见的写action一致,也就是说所有提交的读action永远都只能看见与其满足happen-before关系的写action,这依然禁止了data race的发生。

事实上,我们过去过来就在讲同一件事,一个读action在什么时候能看到与其有data race关系的写action。

前两条弄明白了再看rule 7就比较简单了。rule7包含两条规则:

  • \(W_i(r)\in C_{i-1}\)规定了rule 6中满足happen-before的写action必须已经提交过了。
  • \(W(r)\in C_{i-1}\)规定了读action最终所看见的写action也必须已经提交过了,这里有两层意思:
    第一、如果读action所看见的写action不变(即一直是当前这一步中所看见的满足happen-before关系的写action),那么这两个规则表达的是同一个意思,即该写action必须已经提交了。
    第二、如果读action所看见写action会变,根据rule 5,当前所提交的写action在下一步可以改为看见满足data race的写action,但是该写action必须在上一步已经提交过了。

总之,rule 7的两条限制了一个读action所能看见的写action必须在它之前提交。

再来看rule 7的第二个规则。这个规则使用了\(W(r)\),表达的是最终的结果,这里并没有限制改变会发生在哪一个步,根据rule 5我们可以得出改变发生在下一步,因此我们可以将 \(W(r)\in C_{i-1}\)改为\(W_{i+1}(r)\in C_{i-1}\)。事实上,进一步分析可知,当前为第i步,rule 5在这里的上下文中所讲的是下一步i+1,因此在当前这一步中可将rule 5写为\({W_{i+1}|C_{i}=W|C_{i}}\),由于\(r\in C_i-C_{i-1} \in C_i\),所以有\(W_{i+1}(r)=W(r)\),进而得出\(W_{i+1}(r)\in C_{i-1}\)。

3、举例

这一部分根据上面几条rules的描述来检验一下完整的提交流程,所使用的例子在前面出现过,为了方便,这里重新贴出来:

init:x = y = 0

Thread1        | Thread2
---------------|----------
r3 = x         | r2 = y
if (r3 == 0)   | x = r2
  x = 42       |    
r1 = x         |   
y = r1         |    

output: r1 == r2 == r3 == 42

提交流程如下表所示,Action即被操作的对象,Value指当前所写或所读的值,Commited In指明Action是在哪一个步中被提交的,Final Value In指明在哪一步中Action所看见的值被固定下来。

Action    | Value | Commited In | Final Value In | index
----------|-------|-------------|----------------|-------
x  = 0    | 0     | C1          | E1             |   1
y  = 0    | 0     | C1          | E1             |   2
y  = 42   | 42    | C1          | E1             |   3
r2 = y    | 0     | C2          | E2             |   4
r2 = y    | 42    | C2          | E3             |   5
x  = r2   | 42    | C3          | E3             |   6
r3 = x    | 0     | C4          | E4             |   7
r3 = x    | 42    | C4          | E5             |   8
r1 = x    | 0     | C5          | E5             |   9
r1 = x    | 42    | C5          | E6             |   10

根据前面的分析y = 42可以被提前。一开始集合为空集,此时提交r2 = y并不能使其看到42。所以第一步需要提交y = 42以及初始化的两个action,第二步提交r2 = y,注意第4行中Value为0,因为根据rule 6此时r2 = y只能看见与其满足happen-before的值,也就是y = 0。根据rule 5和rule 7,在第三步中我们可以让r2 = y看见y = 42,除此之外,第三步还必须再提交其他的action,不然上一节所提到的几条rule会因为i不满足关系而失效,因此在第三步中我们提交x = r2。从第四步开始我们回到Thread1,首先提交r3 = x,同前面r2 = y的分析,此时r3 = x还只能看见x = 0。接着在第五步中让r3 = x看见x = r2写入的值,并提交r1 = x,同样的道理,提交时只能看见满足happen-before关系的写action,虽然x = 42 happen-before r1 = x,但是根据rule 7,一个读所能看见的写必须先行提交,由于x = 42并未提交,所以r1 = x此时仍然只能看见x = 0。在最后的第六步中,我们将r1 = x修改为可以看见x = r2写入的值,也就是42。到此便完成了全部提交。

参考:
[1]:The Java Memory Model by. Jeremy Manson
[2]:Causality Test Cases
[3]:The Java Language Specification
[4]:The Java Memory Model: a Formal Explanation

「读知乎」修复版

自从我把系统升级到MIUI 6(擦,暴露了)后「读知乎」就不能用了,每次进去之后都像一坨小鸡那样卡死在那里。原来的项目因为版权问题已经停止更新了,因此,我擅作主张修复了一些让我不愉快的bug,并增加了图片放大功能。下面的apk是没有签名的,所以要安装得先删掉之前的官方版本。点击下面图标下载。

歌词字级同步——ProgressText

天天动听里的歌词是字级同步的,在Android中如何实现呢?首先它的字有一层不同颜色的描边,另一方面随着时间推进一种颜色会逐渐代替另一种颜色。

对于第一个问题把 TextView 中的 Paint 改成 getPaint().setStyle(Paint.Style.FILL_AND_STROKE) 是没有用的,因为使用的是和字体相同的颜色来描边。所以这里采用了一个笨办法,真是笨办法,但比用阴影、设置一大一小叠字要好:-)

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    setTextColor(strokeColor);
    getPaint().setStrokeWidth(strokeWidth);
    getPaint().setStyle(Paint.Style.STROKE);
    super.onDraw(canvas);
}

先把字画出来,然后用不同的颜色描下边就可以了。

对于第二个问题要使用到 BitmapShader ,它继承自 Shader,文档里这样描述的:

A subclass of Shader is installed in a Paint calling paint.setShader(shader). After that any object (other than a bitmap) that is drawn with that paint will get its color(s) from the shader.

Paint 绑定 Shader 后,用它画出来的东西颜色都是从 Shader 中来取,Shader 的大小可能比所要画的物体要小,就如铺地板一样,地板总比地面要小,所以 Shader 中引入三种铺砖模式:

  1. CLAMP——用Shader边上的颜色来画超出的范围,原地板是[l,M,r],铺出来是…[l][l][l,M,r][r][r]…
  2. REPEAT——按正常情况下重复,原地板是[L,R],铺出来是[L,R][L,R][L,R]…
  3. MIRROR——边镜像边重复,原地板是[L,R],铺出来是[L,R][R,L][L,R]…

这里设置一个2个像素的BitmapShader,左边和右边像素颜色不一样,设置x方向的铺砖模式为CLAMP,y方向的铺砖模式为REPEAT,然后移动 Shader ,移动可以通过设置 Matrix 实现:

shader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, 
                                  Shader.TileMode.REPEAT);
matrix.setTranslate(pixels, 0);//左右移动pixels
shader.setLocalMatrix(matrix);

最后在先画 Shader,然后去掉 Shader 给字描边:

@Override
protected void onDraw(Canvas canvas) {
    getPaint().setStyle(Paint.Style.FILL);
    getPaint().setShader(shader);
    super.onDraw(canvas);

    getPaint().setShader(null);
    setTextColor(strokeColor);
    getPaint().setStrokeWidth(strokeWidth);
    getPaint().setStyle(Paint.Style.STROKE);
    super.onDraw(canvas);
}

具体实现见https://github.com/withparadox2/ProgressText,首先添加dependencies。
在Layout里声明:

在头部添加:xmlns:custom="http://schemas.android.com/apk/res-auto"
<com.withparadox2.progresstext.ProgressText
    android:id="@+id/progress_text"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textSize="50sp"
    custom:after_progress_color="@android:color/holo_purple"
    custom:before_progress_color="#FFFF00"
    custom:stroke_color="@android:color/black"
    custom:stroke_width="1px"/>

然后就可以设置进度了:

textView = (ProgressText) findViewById(R.id.progress_text);
textView.setText("绿岛小夜曲");
textView.setProgressBypercentage(0.68f);
//textView.setProgressBypixels(150);

效果如下:

jquery实现手电效果

什么是手电效果呢?情景回放:在如炭般的黑夜,你睁开了眼睛,发现四周充斥着到处游荡的黑暗,它们相互挤压相互吞噬,并且随时有可能将孤零零的你也吞噬掉。在慌张中你摸到了一把手电,迅速用手电打出一束强光,看着墙上游移不定的光斑,心理却踏实了许多,这种刺穿黑暗的感觉是很爽的。这就是手电效果!

这里主要实现的功能是,在黑黑的网页中,只有一片亮斑,鼠标移动时,光斑也跟着移动,从而实现局部浏览的效果。目前,我想到了两种方法,一种是背景涂黑法,另一种是前景涂黑法。

背景涂黑法:

这种方法需要将整个网页背景设定为黑色,字体也必须是黑色(和背景一样就可以了),然后做一个白色的光斑,将其设为背景图片并随着鼠标移动,这样就可以通过局部反差显示出字。下面这个是我使用的光斑。


部分css代码:

body{
background-color:#000000;
background-image:url(images/flashlight.png);
background-repeat:no-repeat;
}

部分javascript代码:

 
    $(function(){
	var imgSize = 500;
	$("body").mousemove(function(e){
	    var lightPosition = (e.pageX - imgSize / 2)+'px '
		+(e.pageY - imgSize / 2)+'px';
	    $("body").css('background-position', lightPosition);
	});
    });

具体效果点击这里。这种方法实现起来比较简单,但有一个重大问题是,它对显示的要求比较苛刻,必须是和背景一样的颜色,很多时候这都是不可能的。下面来说另一种方法。

前景涂黑法:

这个方法也很容易想到,先用canvas元素将前景色涂成黑色,相当于给网页遮了一个罩子。然后用clearRect挖一个正方形,用光斑图片填充这个孔。当移动鼠标时,重绘即可。下面这个是我用的光斑,注意与上一个光斑的区别,这里中间白色是透明的,这样视线才能透过去。


部分javascript代码:

 
$(function(){
	var canvas = $('#canvas_cover').get(0);
	var context = canvas.getContext('2d');

        //背景颜色
	var color = 'rgba(0,0,0,1)';

        //光斑图片的左上角在canvas中的x和y值
	var imgX = 0;
	var imgY = 0;

        //光斑图片大小
	var imgSize = 500;

        //canvas高和宽设为与窗口相同大小
	var width, height;
	$(canvas).attr('height', $(window).height());
	$(canvas).attr('width' , $(document).width());
	width = canvas.width;
	height = canvas.height;

        var img = new Image();
	img.src = "images/canvas_flashlight.png";
	$(img).load(function(){
	    context.drawImage(img, imgX, imgY);
	});

	function drawBackground(){
	    context.fillStyle = color;
	    context.fillRect(0, 0, width, height);
	}

	$(canvas).mousemove(function(evt) {
	    var mouseX = evt.clientX - canvas.offsetLeft;
	    var mouseY = evt.clientY - canvas.offsetTop;
	    imgX = mouseX - imgSize / 2;
	    imgY = mouseY - imgSize / 2;

            //清空画布,重绘
	    context.clearRect(0, 0, canvas.width, canvas.height);
            //绘背景
	    drawBackground();
            //挖一个和光斑图片相同大小的孔
	    context.clearRect(imgX, imgY, imgSize, imgSize);
            //用图片填充孔
	    context.drawImage(img, imgX, imgY);
	});
    });

具体效果点击这里。这个方法是比较理想的,但也有问题,由于网页上盖了一层canvas,和网页内容进行交互就变得很困难,有什么办法吗?

武汉理工大学教务处和图书馆Android客户端测试版

这篇文章完全不是为了刷流量,充满正能量,同学们,放心大胆的点吧,内附多张高清无码大图!

1 软件介绍
软件名:WHUT
作者:paradox2
软件版本:V1.0 Beta版
软件功能:可以查看教务处和图书馆的一些基本信息:

2 具体介绍(使用说明书)
一个破软件有什么好介绍的?对,正因为它破,不成熟,我才要花力气去解释它,把它剥开给大家看。

2.1 首先是登陆界面(看时间长了就不觉得丑了,要不你试试!),可以直接点击按钮也可以点击左上角的图标来切换教务处和图书馆,注意:图书馆的帐号为:4个0加上校园卡后6位。如果保存了课表和图书借阅信息,可以直接在这里点击“离线课表”或“离线查询”进行查看。
   

Continue reading

Android与MVC

我做Android开发有几个月了,咳咳,大言不惭啊,我这种无基础无指导无成果的三无成员也敢自称开发者?我是一边学习Android一边预习Java,进步不少,比起之前写个小软件代码全靠拷贝的悲惨情形好多了。当然我遇到的问题数不胜数,今天的问题是如何搭建Android的软件架构。先来看一张图:

这张无码图一下子反应出我现在是处于什么样的水平,所有的类都放在一个包里,混乱一团,没有章法。当然,这样做对于功能不多的小软件来说也有好处,那就是方便,你只需要卯足劲往包里面扔class就行了,根本就不用考虑直观性,易用性和拓展性。但即使有这样实用的理由,我还是受不了,每次想到我在使用一种极其不专业的手段做一件相当专业的事情时,身上就起疙瘩,我就是一个半吊子么?

很久很久以前我就听说过MVC是一种很实用的设计模式,在网络技术上用的尤其广泛,但是它难于设计,这不是说它理解起来有多难,而是要想充分发挥它的功效就得做深入细致的策划,这个是很耗生命值的。这一点说明对于小软件用此种模式可能有点得不偿失。现在先抛开这些无关紧要的说法,来看看如何使用这种模式。

关于Android中MVC模式的实现,我在网上看到截然两种不同的派别。一种就是百度到的中文网页,几乎所有的帖子都认为善解人意的Google已经将MVC模式嵌入到Android开发的程序。你看,人家弄了一个xml,你便可以任意张罗布局,这就是View。接着可以将这些View绑定到Activities声明的控件对象上,就实现了监听控制,这是Controller。而Model则是你开发的各种处理数据的类。当然,我这样说并不是认为它不对,只是感觉这样说和没说一样,看看前面的那张图片,也是用的这样的方法,但是它并没有让我感受到MVC带来的优越性。要么是因为MVC很垃圾,要么就是在Android中实现MVC还需要更深入的探索,毫无疑问,我倾向于第二种说法。

带着这样的疑惑我开始继续寻找一种更加恰当的理解方式,在国外的论坛上,技术达人也为此争论不休,各执己见。作为初学者,残缺的知识储备无法成为我选择观点的助手,因此我更多的是靠感觉来寻找,后来不知怎么就链到了Ivan.Memruk写的一篇文章:android-architecture-message-based-mvc。看到开篇第一句话我就乐了

How do you separate application state, user interaction logic and data presentation in your Android apps?

这不正是我所要找的么。在他的文章中提到要想理解它需要先弄清楚他的另一篇长标题的短文:Use-MVC-and-develop-a-simple-Star-Rating-widget-on-Android。在这篇文章,它用了一个极其简单的例子来阐述搭建MVC模式没有想象中的那么简单。我仔细研读了源码,除了感觉到作者有一种莫名其妙的细致外,还有就是我的知识水平有待提高。接下来介绍一下这个例子,为了不误人子弟,我以翻译为主。

程序功能介绍:

看到上图有5颗星星,当点击一次,黄色的星星会增加一个,当黄色的星星增加到5个再点击,则黄色星星将变为零个,然后就这样不断的循环。

这个以MVC为设计模式的小程序可以用以下几点来描述:

  • 首先,有一个model来保存数据和处理逻辑过程,它不关心数据是怎样显示出来的和用户的动作是如何改变数据的。一个model通常用一种称作监听者的模式来使它的听众,如view,从它那获得数据更新,当然,model本身是不关心这些的。
  • 用一个view从model中获得数据和状态并将它们显示给用户,将view和model分离给我们带来的一个很大好处是可以随意改变view中显示的样子或者方式。
  • 最后需要一个controller,它主要从用户那获得动作,然后来触发model里操作数据的动作,从而来改变数据或状态。如上所说,在一个简单的例子中用controller通常会显得很麻烦,但是要更改触发动作的方式却是方便的。

下面来分别讲解每个模块:

Model模块:

一开始编写这个模块是非常正确的,因为view中要显示的数据和controller 中要调用的方法都是放在model中的,先把逻辑部分搞清楚了,做其他模块才会得心应手。
在编写model时应该主要考虑程序的抽象逻辑而不能包含UI显示,一般情况下就是储存各种控件所需要的数据,并且我们也想在其中添加更新数据的方法和其他逻辑部分。在这个例子中我们存储的是星星的数据,同时包含一个获得数据的getter方法和更新数据的setter方法。你可以看一下完整的代码:

public final class StarRatingModel {
  public static final int MAX_STARS = 5;

  public interface Listener {
    void handleStarRatingChanged(StarRatingModel sender);
  }

  private int stars = 1;

  private List<Listener> listeners = new ArrayList<Listener>();

  public StarRatingModel() {

  }

  public int getStars() {
    return stars;
  }

  public void setStars(int stars) {
    if (stars > MAX_STARS) {
      stars = MAX_STARS;
    } else if (stars < 0) {
      stars = 0;
    }
    if (stars != this.stars) {
      this.stars = stars;

      for (Listener listener : listeners) {
        listener.handleStarRatingChanged(this);
      }
    }
  }

  public void addListener(Listener listener) {
    this.listeners.add(listener);
  }

  public void removeListener(Listener listener) {
    this.listeners.remove(listener);
  }
}

这里遇到的主要问题是怎样保证Model里的数据变化后,View中能够做出相应的改变。在这段代码中采用了监听者的模式,将view中的button控件注册为监听者即listener,通过回调在接口中声明的方法即可实现实时更新。这样做的好处非常明显,Model几乎可以不用考虑View的感受,它怎么做都不为过,作为一个独立体还可以在其他地方得到复用。

Controller模块:

很明显这不是按字母排序来的,但我建议把controller放在第二步。 controller的功能很简单主要是接收用户相应的动作,然后调用model中相关的方法来更新model的数据或状态。一般来说在UI框架下,包括android在内,都是由view来接受用户的原始动作,在这里我们把动作之后要进行的操作放在controller中,从而减轻view的负担,这样它就可以专心致志的来搞它的显示。这样做还有一个优点,当你想改变动作方法时,只需在controller中更改调用的方法即可,比如把增改为减,非常的直观。

public final class StarRatingController {
  private StarRatingModel model;

  public StarRatingController(StarRatingModel model) {
    this.model = model;
  }

  public void handleTap(MotionEvent event) {
    // the old trick with % to wrap around values
    model.setStars((model.getStars() + 1) % (StarRatingModel.MAX_STARS + 1));
  }
}

在view中接受MotionDown的事件event,然后将此event作为参数传给handleTap,从而调用Model中的setstars方法,进而更新model中的数据状态。

View模块:

view,顾名思义是用来显示内容的,一方面接受用户的动作,另一方面要自动更新显示的内容。

为了能够实现自动更新则需要实现listener接口,然后复写其中的handleStarRatingChanged方法,并注册为listener,当用户点击后view便会自动调用复写的handleStarRatingChanged方法,从而实现更新。当然为了防止内存泄露,此程序加入了能够解除listener的功能。

public final class StarRatingView extends View implements StarRatingModel.Listener {

  private StarRatingModel model;
  private StarRatingController controller;

        /* .... */

  public void setModel(StarRatingModel model) {
    if (model == null) {
      throw new NullPointerException("model");
    }

    StarRatingModel oldModel = this.model;
    if (oldModel != null) {
      oldModel.removeListener(this);
    }
    this.model = model;
    this.model.addListener(this);
    this.controller = new StarRatingController(this.model);

    if (oldModel != null) {
      invalidate();
    }
  }

  @Override
  public void handleStarRatingChanged(StarRatingModel sender) {
    invalidate();
  }

        /* .... */
}

当model改变时,view便会调用handleStarRatingChanged方法,通过invalidate重绘,这样就实现了view的更新。下面看看当点击时,view是如何处理动作的:

 @Override
  public boolean onTouchEvent(MotionEvent event) {
    if (event.getAction() == MotionEvent.ACTION_DOWN) {
      controller.handleTap(event);
      return true;
    } else {
      return super.onTouchEvent(event);
    }
  }

如果不用所谓的MVC,那么实现这个功能实在是太简单了,去掉所有和model,controller有关的方法或者变量,只用更改onTouchEvent方法即可,这样就会比之前简单了许多:

private int number = 0;
@Override
	public boolean onTouchEvent(MotionEvent event) {
		if (event.getAction() == MotionEvent.ACTION_DOWN) {
			if(number<5)
			number++;
			else
				number=0;
			invalidate();
			return true;
		} else {
			return super.onTouchEvent(event);
		}
	}

这个例子的源码可以在Use-MVC-and-develop-a-simple-Star-Rating-widget-on-Android找到,在这篇文章中作者考虑的更加详细,他做了一番很犀利的总结,这里就不全搬过来了,意义也不大。当然既然看了这么久,那么我肯定得朝这个方向发展,不然我就是闲的。事实上,网上还有一个更完整的例子,作者也是费了好大的心血来阐述自己的观点,我也打算慢慢的了解一下,点击这里便可看到。

最后再套用一下作者的话:

If you feel safe about every point on that list, you can go ahead and enjoy the graceful architecture of MVC in your Android apps.