By 高焕堂 2012/03/01
1. 向别的行业取经
这是一个日常生活中的模式(即屡试不爽的思考模式)可引导我们设计出框架里的接口(API)。虽然许多人都认为父类别(Super Class,又称基类)是框架的主体,但是我们也可以采取另一个观点,认为API才是框架的主体。基于这个观点,我们可以从日常生活中,或是从别的行业的经验里寻觅一些可引导人们思维的设计模式,其对于软件框架开发者,将有极大的帮助。
2.「引擎/轮盘/轮胎」模式
在汽车架构里,最明显的接口就是「轮盘」。它让人们随时可以更换轮胎,让引擎可以搭配不同的轮胎,于是汽车就能在不同的地形上跑了。例如,换上高速胎就能上高速公路、配上铁链可以上雪山、配上胖胖的轮胎就能上沙滩。

图1、「引擎/轮盘/轮胎」模式
有了轮盘,就能让汽车(引擎)适用于不同的地形;若套用软件的术语,汽车适用于不同地形,其意义就相当于:汽车跨越了不同平台;其简称:跨平台。基于这个模式,人们也创造了一种新的工作模式(Work Pattern):「强龙/地头蛇」分工模式。强龙设计轮盘、生产汽车(引擎);而地头蛇则生产轮胎。这种分工模式还能有效提升客户满意度:客人(买主)需求还没有呈现之前,强龙就能开始生产引擎;一旦客人来了(需求出现了),由地头蛇迅速配上轮胎,就能上路了,客人不亦乐乎。由于能迅速满足客制化的需求,「强龙/地头蛇」分工模式也是企业扩张其市场版图的重要商业模式。例如,大家熟悉的加盟店(如7-Eleven、上岛咖啡等连锁店)就是典型的范例。
3. 软件框架里的EIT(引擎/轮盘/轮胎)模式
3.1 软件API的表示
软件API与汽车轮盘两者的角色是一样的;能让引擎模块随时搭配不同的轮胎模块,满足客制化的需求,如下图:

图2、软件的「引擎/轮盘/轮胎」模式
上图也能表示如下图所示:

图3、软件的「强龙/地头蛇」分工模式
此图凸显了,在买主还没出现之刻,强龙便能设计轮盘API和开发引擎模块在先;而后等待买主来到了,便由地头蛇开发软件轮胎,并装配于接口上。刚才提过了,由于能迅速满足客制化的需求,「强龙/地头蛇」分工模式也是软件企业扩张其市场版图的重要商业模式。上图也能表示如下图:

图4、凸显软件父类别与子类别的关系
此图凸显了,软件的「轮盘父类别」在幕后支撑「轮盘API」。此外,还凸显父类别定义API而子类别来实现API。
3.2 Android应用框架里的范例
3.2.1范例一:ListView
说明
在Android的屏幕画面上想呈现出ListView窗口时,通常会用到Android框架里的ListView和BaseAdapter两个类别:

图5、 ListView的「引擎/轮盘/轮胎」模式
框架里的抽象类别皆含有许多个卡榫(Hook)函数,做为与其子类别的衔接口。例如,在Activity类别里,大家最熟悉它的onCreate()卡榫函数,此外,Activity还含有其它的卡榫函数。同样地,BaseAdapter类别里也含有许多个卡榫函数,而getView()就是其中之一。
前面说过,框架的抽象类别设计在先,之后才配上应用类别,所以在撰写Activity、ListView和BaseAdapter等类别时,其开发者并不知道应用程序里的ListView窗口的内容,所以预留了虚空给应用类别开发者去填补,
框架设计者与应用类别开发者,两者在不同的时间及空间(即地点)合作。 框架设计者在设计应用框架时并不知道应用程序的ListView窗口里各个选项的长相(文字或是图像等),因为这长相的决定是来自于应用程序的用户。所以只有等到应用类别开发者动工之刻才会知道,而框架设计者并不知道。于是,框架设计师负责撰写抽象类别(如BaseAdapter),而应用类别开发者负责撰写子类别(如myAdapter)。两者跨时间和空间的智能会合而成为一支完整好用的手机应用软件。例如,当AP开发者知道他的UI将显示出单纯的文字型ListView窗口时,就可以开始撰写myAdapter子类别了。
在显示出一个 ListView窗口时,是藉由BaseAdapter的4个Hook函数:
★ getCount()
★ getItem()
★ getItemId()
★ getView()
而反向呼叫了myAdapter子类别的:
- getCount()函数而得知此ListView含有N个细项。
- 呼叫N次getItem()函数,每次取得Coll里的一个对象内容。
- 呼叫N次getItemId()函数,询问该对象将出现在画面上ListView里的第几个细项。
- 呼叫N次getView()函数,每次取得一个View对象,将其内容显示于画面上的ListView里。
范例程序
上述画面上ListView里的长相是简单文字而已。你也能依样画葫芦,撰写一个长相较美观的ListView,其范例程序如下:
<<操作情境>>
此AP执行时,画面上显示出一个较美观的ListView窗口,如下图:

<<撰写程序>>
Step-1. 建立一个Android程序项目:

Step-2. 撰写Row类别。
// Row.java
public class Row {
private String text;
private int resid;
Row(String tx, int id){ text = tx; resid = id; }
public int getDwId() { return resid; }
public String getText() { return text; }
public boolean isSelectable() { return true; }
}
Step-3. 撰写BaseAdapter的子类别:myAdapter。
// myAdapter.java
package com.misoo.ex02;
import java.util.ArrayList;
import android.content.Context;
import android.graphics.Color;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
public class myAdapter extends BaseAdapter {
private final int WC = ViewGroup.LayoutParams.WRAP_CONTENT;
private Context ctx;
private ArrayList<Row> coll;
public myAdapter(Context context, ArrayList<Row> list) {
ctx = context; coll = list; }
public int getCount() { return coll.size(); }
public Object getItem(int position) { return coll.get(position); }
public long getItemId(int position) { return position; }
public View getView(int position, View convertView, ViewGroup parent) {
LinearLayout layout = new LinearLayout(ctx);
layout.setOrientation(LinearLayout.HORIZONTAL);
Row rw = (Row)coll.get(position);
ImageView iv = new ImageView(ctx);
iv.setImageResource(rw.getDwId());
iv.setPadding(0, 2, 5, 0);
LinearLayout.LayoutParams param
= new LinearLayout.LayoutParams(WC, WC);
layout.addView(iv, param);
TextView txv = new TextView(ctx); txv.setText(rw.getText());
txv.setTextColor(Color.WHITE); txv.setTextSize(26);
LinearLayout.LayoutParams param2
= new LinearLayout.LayoutParams(WC, WC);
param2.leftMargin = 5; param2.topMargin = 13;
layout.addView(txv, param2);
return layout;
}}
Step-4. 撰写Activity的子类别:myActivity。
// myActivity.java
package com.misoo.ex02;
import java.util.ArrayList;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.AdapterView.OnItemClickListener;
public class myActivity extends Activity implements OnItemClickListener {
private ArrayList<Row> coll;
@Override public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.main);
coll = new ArrayList<Row>();
coll.add(new Row("Play",R.drawable.icon) );
coll.add(new Row("Exit", R.drawable.icon2));
ListView lv = new ListView(this);
lv.setAdapter(new myAdapter(this, coll));
lv.setOnItemClickListener(this);
setContentView(lv);
}
public void onItemClick(AdapterView<?> arg0, View arg1, int arg2, long arg3) {
if(arg2 == 0) {
Row rw = (Row)coll.get(arg2);
setTitle(rw.getText()); }
else if(arg2 == 1) finish();
}}
Android框架提供了BaseAdapter抽象类,并设计了getView()等卡榫函数。于是,框架就基于BaseAdapter类的卡榫函数来与应用类myAdapter搭配起来了。此范例里的myAdapter类与上一个范例的myStringAdapter类属于不同的应用程序,皆能与框架里的BaseAdapter抽象类相结合,就发挥了卡榫函数的功能了。在Template Method模式的指引下,我们对各条指令的角色都清晰有致,因而会觉得上述程序是简单有序的,这是Template Method模式的完美示范。
3.2.2范例二:SurfaceView
说明
SurfaceView是View的子类别,其内嵌了一个用来绘制的Surface。程序里可以控制Surface的大小,SurfaceView可控制Surface的绘图位置。Surface是Z-ordered结构,SurfaceView提供了一个可视区域,限定Surface的可见部分内容。同一个View树状层级体系里的Surface内容可能会被其它兄弟所遮盖(Overlay)。基于「强龙/地头蛇」分工模式,强龙担任:
- 开发引擎模块,也就是SurfaceView(以及SurfaceHolder)类别。
- 设计SurfaceHolder.CallBack接口。
如下图所示:

图6、SurfaecView的「引擎/轮盘/轮胎」模式(一)
接着,由地头蛇来撰写子类别(轮胎模块)。当SurfaceView成为可见时,就会诞生Surface;反之当SurfaceView被隐藏时,就会删除Surface,以便节省资源。当Surface诞生和删除时,SurfaceView会透过SurfaceHolder.CallBack接口来呼叫SurfaceCreated()和 SurfaceDestroyed()等函数。

图7、SurfaecView的「引擎/轮盘/轮胎」模式(二)
范例程序:让图像在SurfaceView里旋转
本范例里,将在MySurfaceView类别里定义一个DrawThread类别,它诞生一个背景执行绪(执行绪,Thread)来执行画图的任务。当UI(主)主执行绪(Main Thread)侦测到绘图画面(Surface)被开启时,就会诞生DrawThread的对象,启动背景执行绪去画图。一直到UI执行绪侦测到绘图画面被关闭时,就停此正在绘图的背景执行绪。
<操作情境>
此程序执行时,出现一个会旋转的图像:

其旋转情形如下:

<撰写步骤>
Step-1: 建立Android项目:
Step-2: 撰写ac01.java类别,其程序代码如下:
//---------- ac01.java 程序代码 --------------------------------
package com.misoo.pkzz;
import android.app.Activity;
import android.os.Bundle;
import android.widget.LinearLayout;
public class ac01 extends Activity {
@Override protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
MySurfaceView mv = new MySurfaceView(this);
LinearLayout layout = new LinearLayout(this);
layout.setOrientation(LinearLayout.VERTICAL);
LinearLayout.LayoutParams param =
new LinearLayout.LayoutParams(200, 150);
param.topMargin = 5;
layout.addView(mv, param);
setContentView(layout);
}}
此程序诞生一个MySurfaceView对象,并加入到layout里。
//---------- MySurfaceView.java 程序代码 --------------------------------
package com.misoo.pkzz;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback {
private SurfaceHolder mHolder;
private DrawThread mThread;
MySurfaceView(Context context) {
super(context);
getHolder().addCallback(this);
}
public void surfaceCreated(SurfaceHolder holder) {
mHolder = holder;
mThread = new DrawThread();
mThread.start();
}
public void surfaceDestroyed(SurfaceHolder holder) {
mThread.finish();
mThread = null; }
public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { }
// ----------------------------------------------------------------------
class DrawThread extends Thread {
int degree = 36;
boolean mFinished = false;
DrawThread()
{ super(); }
@Override public void run() {
Bitmap bmp = BitmapFactory.decodeResource(getResources(),
R.drawable.x_xxx);
Matrix matrix;
degree = 0;
while(!mFinished){
Paint paint = new Paint(); paint.setColor(Color.CYAN);
Canvas cavans = mHolder.lockCanvas();
cavans.drawCircle(80, 80, 45, paint);
//------ rotate -----------------------------
matrix = new Matrix(); matrix.postScale(1.5f, 1.5f);
matrix.postRotate(degree);
Bitmap newBmp = Bitmap.createBitmap(bmp, 0, 0, bmp.getWidth(),
bmp.getHeight(), matrix, true);
cavans.drawBitmap(newBmp, 50, 50, paint);
mHolder.unlockCanvasAndPost(cavans);
degree += 15;
try { Thread.sleep(100);
} catch (Exception e) {}
}}
void finish()
{ mFinished = true; }
}
}
理解了SurfaceView类别的用法之后,你就更会活用SurfaceView类别来进行复杂的3D绘图情境了,如下一节的说明。
3.2.3范例三:GLSurfaceView
Android 平台上的Java版本OpenGL ES速度很慢。不过,你可以利用JNI接口来开发本地的*.so动态库,并利用NDK工具来将*.so和 Java 应用程序代码打包起来成为*.apk而发布出去,以便提高绘图效率。
此外,如果你已经在别的平台上开发了一些C/C++的OpenGL程序,想把这些程序代码移植到Android上,JNI也能协助你将这些C/C++程序与上层Java程序代码结合起来,你就不必将C/C++程序代码改写为Java程序代码了。
Android把opengl的本地实现,JNI,java接口都放在/frameworks /base/opengl下面了,而且它内部还带了一个工具可以生成JNI程序代码。
我们来看看opengl的目录结构:
/include 包含egl和gles所有的标头档
/java/android/opengl 这个目录包含的就是我们3D画图要使用到的GLSurfaceView
/java/com/google/android/gles_jni 这个目录包含一些自动产生的档
/java/javax/microedition/khronos/egl 这就是应用层使用到的egl接口
/java/javax/microedition/khronos/opengl 这就是应用层使用到的opengl接口
/libagl 这个就是opengl主要的实作了
/libs 这里面包含两个库的实作档:libegl.so和libGL|ES_CM.so
/tools 在我的理解这就是一个jni的产生工具
基于「强龙/地头蛇」分工模式,强龙担任:
- 开发引擎模块,也就是GLSurfaceView;它是SurfaceView的子类别。
- 设计Renderer接口。
如下图所示:

图8、GLSurfaecView的「引擎/轮盘/轮胎」模式(一)
接着,由地头蛇来撰写子类别(轮胎模块)。GLSurfaceView会透过Renderer接口myRenderer类别的本地(Native)函数,进而呼叫到myRender_JNI本地模块;如下图所示:

图9、GLSurfaecView的「引擎/轮盘/轮胎」模式(二)
myActivity诞生myGLView对象,接着myGLView诞生myRenderer对象,并呼叫setRenderer()函数将myRenderer对象的指针存入到GLSurfaceView里。这个动作就相当于将轮胎(即myRenderer对象)装配到引擎(即GLSurfaceView对象),让引擎可以透过轮盘来带动轮胎(即透过Renderer接口呼叫到myRenderer对象里的本地模块。
3.2.4范例四:UI事件处理
说明
当Android的应用程序执行时,框架必须接受来自使用者触动UI画面而引发的事件,这就是大家熟悉的UI事件。 由于地头蛇才知道使用者触动此UI画面的意图,也才知道如何处理这些事件。所以,由地头蛇撰写这些事件处理函数(Event Handler)是合理的,于是,地头蛇就将事件处理函数写在myActivity类里面。如下图所示:

图10、UI事件处里的「引擎/轮盘/轮胎」模式(一)
此时,Button、OnClickListerner和myActivity三者之关系,就相当于「引擎、轮盘和轮胎」之关系,也算是「引擎/轮盘/轮胎」模式的运用。
范例程序
本范例里,表现了最简单的UI事件处理机制。
<操作情境>
此程序执行时,在画面上出现一个Button按钮图像:

<撰写步骤>
Step-1: 建立Android项目:
Step-2: 撰写myActivity.java类别,其程序代码如下:
//myActivity.java
package com.misoo.pk01;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
public class myActivity extends Activity implements OnClickListener {
private Button btn;
private int state_var_A = 0;
@Override protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
goto_state_01();
}
private void goto_state_01(){
state_var_A = 1;
show_layout_01();
}
private void goto_state_02(){
state_var_A = 2;
finish();
}
private void show_layout_01(){
btn = new Button(this); btn.setId(101);
btn.setText("Bye!");
btn.setBackgroundResource(R.drawable.bye);
btn.setOnClickListener(this);
setContentView(btn);
}
public void onClick(View v) {
if(v == btn){
if(state_var_A == 1 ) goto_state_02();
}
}
}
在此myActivity类别里,含有一行程序代码:btn.setOnClickListener(this),如下:
public class myActivity extends Activity implements OnClickListener {
………..
private void show_layout_01(){
btn = new Button(this);
……….
btn.setOnClickListener(this);
……….
}
………..
}
因为this代表myActivity类的对象,如下:
框架开发者(即强龙)订定了OnClickListener接口,让框架能透过接口而反向调用地头蛇所写的应用子类别里的事件处理函数。上图也能表达如下:

图11、UI事件处里的「引擎/轮盘/轮胎」模式(二)
在应用程序执行期间,当用户去触动UI画面的Button对象时,就引发出上图所述的onClick事件,此时Button对象透过OnClickListener接口而将onClick事件传递给myActivity里的ec对象,进而调用了地头蛇写在ec类里的onClick()事件处理函数(Event Handler)。
兹将上述的范例改写如下,两者的执行结果是一样的,只是展现出框架设计者预留给地头蛇的弹性空间,以创造出应用子类别时的多样化空间。
//myActivity.java
package com.misoo.pk01;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
public class myActivity extends Activity {
private Button btn;
private int state_var_A = 0;
@Override protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
goto_state_01();
}
private void goto_state_01(){
state_var_A = 1;
show_layout_01();
}
private void goto_state_02(){
state_var_A = 2;
finish();
}
private void show_layout_01(){
btn = new Button(this);
btn.setId(101);
btn.setText("Bye!");
btn.setBackgroundResource(R.drawable.bye);
btn.setOnClickListener(listener);
setContentView(btn);
}
//-----------------------------------------------------------
OnClickListener listener = new OnClickListener() {
@Override
public void onClick(View v) {
if(v == btn){
if(state_var_A == 1 ) goto_state_02();
}
}
};
}
在这应用程序里,有一行程序代码:btn.setOnClickListener(listener),这意味着,在这应用程序执行时,当用户去触动UI画面的Button对象时,就将引发的onClick事件,会透过OnClickListener接口而将onClick事件传递给上述的嵌入类的对象,进而调用该对象里的onClick()函数。◆