本次会详细讲解将Android的Snake Sample移植到J2ME上,从而比较二者的区别和联系。
在《1.Android SDK Sample-Snake详解》中,我们已经详细介绍了Android实现的Snake项目结构,现在我们要将这个项目用J2ME实现。
一、 J2ME vs. Android
Android的UI实用、方便,而且很美观,基本无需改动且定制方便。而J2ME的高级用户界面比较鸡肋,在现在大多数的应用里都看不到,多数稍微复杂点的界面都是手工画,或是用一些开源的高级UI库。接下来我们简单比较下二者的区别,为Snake项目从Android到J2ME的移植做准备。
1. 平台
J2ME:
开发平台
Android:
操作系统
2. 工程结构
J2ME:
res:资源文件
src:源代码
Android:
src:源代码
res\drawable:图片
res\raw:声音
res\values:字符串
assets:数据文件
3. 安装包
J2ME:
jad,jar
Android:
apk
4. 代码结构
J2ME:
MIDlet,Canvas,采用继承的方式,只有一个MIDlet,一般只有一个Canvas
Android:
Activity,View,采用继承的方式,只有一个Activity,一般只有一个View
5. 入口程序
J2ME:
MIDlet类
Android:
Activity类
6. 主程序结构
J2ME:
package com.deaboway.j2me;
import javax.microedition.midlet.MIDlet;
import javax.microedition.midlet.MIDletStateChangeException;
public class MyMidlet extends MIDlet {
protected void destroyApp(boolean arg0) throws MIDletStateChangeException {
// TODO Auto-generated method stub
}
protected void pauseApp() {
// TODO Auto-generated method stub
}
protected void startApp() throws MIDletStateChangeException {
// TODO Auto-generated method stub
}
}
Android:
package com.deaboway.android;
import android.app.Activity;
import android.os.Bundle;
public class myActivity extends Activity {
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.main);
}
}
7. 生命周期-开始
J2ME:
startApp(),活动状态,启动时调用,初始化。
Android:
onCreate(),返回时也会调用此方法。
onCreate()后调用onStart(),onStart()后调用onResume(),此时Activity进入运行状态。
8. 生命周期-暂停
J2ME:
pauseApp(),暂停状态,如来电时,调用该接口。
Android:
onPause()。
9. 生命周期-销毁
J2ME:
destroyApp(),销毁状态,退出时调用。
Android:
onStop(),程序不可见时调用onDestroy(),程序销毁时调用。
10. 刷新
J2ME:
高级UI组件由内部刷新实现。低级UI,canvas中通过调用线程结合repaint()来刷新,让线程不断循环。低级UI架构可以用MVC方式来实现,建议使用二级缓存。
Android:
高级UIHandler类通过消息的机制刷新。onDraw()刷新接口,低级UI开发者用线程控制更新,在lockCanvas()和unlockCanvasAndPost()方法之间绘制。
如果去读API,我们可以发现J2ME中Canvas的repaint()与Android中View的invalidate()/postInvalidate()方法实现了相同的功能(连说明文字几乎都一样…),但是invalidate()/postInvalidate()两者却有着区别:invalidate() 只能在UI这个线程里通过调用onDraw(Canvas canvas)来update屏幕显示,而postInvalidate()是要在non-UI线程里做同样的事情的。这就要求我们做判断,哪个调用是本 线程的,哪个不是,这在做多线程callback的时候尤为重要。而在J2ME中,不管怎样直接调用repaint()就好了。
11. 绘画
J2ME:
Displayable类。J2me中所有可显示的组件都是直接或间接的继承了Displayable,直接的是Canvas和Screen。不同的继承导致了低级 UI和高级UI的区别。J2me中现成的UI组件都是直接或者间接继承了Screen。只要调用Display.getDisplay(MIDLet instan).setCurrrent(Displayable disp),就可以把组件显示到手机界面上。切换界面的时候也可以使用该接口。
Android:
View类。可见的组件直接或者间接继承了android.view.View。通过 Activity.setContentView(View view)就可以显示在android手机界面上,切换界面的时候也可以使用该接口。如果是直接继承了View而不是Android自带的UI组件,那么 还要自己去实现它的刷新,类似J2me的低级UI组件。
12. 画笔
J2ME:
高级UI组件由内部刷新实现。低级UI,canvas中通过调用线程结合repaint()来刷新,让线程不断循环。低级UI架构可以用MVC方式来实现,建议使用二级缓存。
Android:
Canvas类,Android绘 制的时候会传入一个参数Paint。该对象表示绘制的风格,比如颜色,字体大小,字体格式等。Android的Canvas不同于J2ME的Canvas,它更像于J2ME的Graphics,用来绘制。
13. 全屏
J2ME:
Canvas中SetFullScreenMode()。
Android:
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,WindowManager.LayoutParams.FLAG_FULLSCREEN);requestWindowFeature(Window.FEATURE_NO_TITLE);
14. 获得屏幕尺寸
J2ME:
Canvas类的getHeight()和getWidth()
Android:
Display d = getWindowManager().getDefaultDisplay();
screenWidth = d.getWidth();
screenHeight = d.getHeight();
15. 可绘区域
J2ME:
int clipX = g.getClipX();
int clipY = g.getClipY();
int clipWidth = g.getClipWidth();
int clipHeight = g.getClipHeight();
g.clipRect(x, y, width, height);
g.setClip(clipX, clipY, clipWidth, clipHeight);//释放当前状态
Android:
canvas.save();//保存当前状态
canvas.clipRect(x,y, x+width, y+height)
cavnas.resave();//释放当前状态
16. 清屏操作
J2ME:
g.setColor(Color.WHITE);
g.fillRect(0,0,getWidth(),getHeight());
Android:
// 首先定义paint
Paint paint = new Paint();
// 绘制矩形区域-实心矩形
// 设置颜色
paint.setColor(Color.WHITE);
// 设置样式-填充
paint.setStyle(Style.FILL);
// 绘制一个矩形
canvas.drawRect(new Rect(0, 0, getWidth(), getHeight()), paint);
17. 双缓冲
J2ME:
Image bufImage=Image.createImage(bufWidth, bufHeight);
Graphics bufGraphics=bufImage.getGraphics();
Android:
Bitmap carBuffer = Bitmap.createBitmap(bufWidth, bufHeight, Bitmap.Config.ARGB_4444);
Canvas carGp = new Canvas(carBuffer);
18. 图片类
J2ME:
Image类,Image.createImage(path);
Android:
BitMap类,BitmapFactory.decodeResource(getResources(),R.drawable.map0);
19. 绘制矩形
J2ME:
drawRect的后两个参数为宽度和高度
Android:
drawRect的后两个参数为结束点的坐标
20. 按键
J2ME:
keyPressed()
keyRepeated()
keyReleased()
Android:
onKeyDown()
onKeyUp()
onTracKballEvent()
21. 键值
J2ME:
Canvas.LEFT…
Android:
KeyEvent.KEYCODE_DPAD_LEFT…
22. 触笔
J2ME:
pointerPressed(),pointerReleased(),pointerDragged()
Android:
onTouchEvent()
23. 数据存储
J2ME:
Record Management System (RMS)
Android:
SQLite数据库,SharedPreferences类
24. 连接
J2ME:
从Connector打开,可以直接在Connector.Open时设置连接是否可读写,以及超时设置
Android:
从URL对象打开,必须使用setDoInput(boolean)和setDoOutput(boolean)方法设置,使用setConnectTimeout(int)不仅可以对连接超时进行设置,还能设置超时时间,参数为0时忽略连接超时
25. 游戏开发包
J2ME:
javax.microedition.lcdui.game.*;
GameCanvas/Layer/LayerManager/ Sprite/TiledLayer
Android:
无专门针对游戏的开发包。
26. 音效
J2ME:
Player s=Manager.createPlayer(InputStream);
s.prepare();//创建
s.start();//播放
s.stop();//暂停
s.stop();//关闭
s.release();//释放
Android:
MediaPlayer类处理背景音乐
SoundPool类处理一些简单的音效
27. 显示文本
J2ME:
String
Android:
TextView类
28. 打印信息
J2ME:
System.out.println()
Android:
Log类
二、 迁移关键点
1. 基础类和结构
J2ME程序的主体从Activity改变为MIDlet,TileView从View改变为Canvas,相关的方法也需要进行调整,但是主体结构和逻辑还是一致的。此外,有些J2ME不支持的类,需要做对应处理。
资源获取,从xml改为自行获取:
1
private
void
initSnakeView() {
2
3
//
获取图片资源
4
5
try
{
6
7
imageRED_STAR
=
Image.createImage(
"
/redstar.png
"
);
8
9
imageRED_STAR
=
Utils.zoomImage(imageRED_STAR,mTileSize,mTileSize);
10
11
imageYELLOW_STAR
=
Image.createImage(
"
/yellowstar.png
"
);
12
13
imageYELLOW_STAR
=
Utils.zoomImage(imageYELLOW_STAR,mTileSize,mTileSize);
14
15
imageGREEN_STAR
=
Image.createImage(
"
/greenstar.png
"
);
16
17
imageGREEN_STAR
=
Utils.zoomImage(imageGREEN_STAR,mTileSize,mTileSize);
18
19
}
catch
(Exception e) {
20
21
Log.info(
"
Create Images:
"
+
e);
22
23
}
24
25
//
设置贴片图片数组
26
27
resetTiles(
4
);
28
29
//
把三种图片存到Bitmap对象数组
30
31
loadTile(RED_STAR, imageRED_STAR);
32
33
loadTile(YELLOW_STAR, imageYELLOW_STAR);
34
35
loadTile(GREEN_STAR, imageGREEN_STAR);
36
37
}
ArrayList,用Vector替换掉:
1
//
坐标数组转整数数组,把Coordinate对象的x y放到一个int数组中——用来保存状态
2
3
private
String coordVectorToString(Vector cvec) {
4
5
int
count
=
cvec.size();
6
7
StringBuffer rawArray
=
new
StringBuffer();
8
9
for
(
int
index
=
0
; index
<
count; index
++
) {
10
11
Coordinate c
=
(Coordinate) cvec.elementAt(index);
12
13
rawArray.append(c.x
+
"
,
"
);
14
15
rawArray.append(c.y
+
"
,
"
);
16
17
Log.info(
"
coordVectorToString(), c.x=
"
+
c.x
+
"
,c.y=
"
+
c.y);
18
19
}
20
21
Log.info(
"
coordVectorToString(), rawArray.toString=
"
+
rawArray);
22
23
return
rawArray.toString();
24
25
}
26
27
//
整数数组转坐标数组,把一个int数组中的x y放到Coordinate对象数组中——用来恢复状态
28
29
//
@J2ME 还是用Vector替换ArrayList
30
31
private
Vector coordStringToVector(String raw) {
32
33
Vector coordArrayList
=
new
Vector();
34
35
Log.info(
"
coordStringToVector(), raw=
"
+
raw);
36
37
String[] rawArray
=
Utils.splitUtil(raw,
"
,
"
);
38
39
Log.info(
"
coordStringToVector(), rawArray.length=
"
+
rawArray.length);
40
41
int
coordCount
=
rawArray.length;
42
43
for
(
int
index
=
0
; index
<
coordCount; index
+=
2
) {
44
45
Coordinate c
=
new
Coordinate(Integer.parseInt(rawArray[index]), Integer.parseInt(rawArray[index
+
1
]));
46
47
coordArrayList.addElement(c);
48
49
}
50
51
return
coordArrayList;
52
53
}
Bundle,用RMS实现:
1
/**
2
3
* <p>Title: Snake</p>
4
5
* <p>Copyright: (C) 2011 Gavin's Snake project. Licensed under the Apache License, Version 2.0 (the "License")</p>
6
7
*
@author
Gavin
8
9
*/
10
11
package
com.deaboway.snake.util;
12
13
import
java.io.ByteArrayInputStream;
14
15
import
java.io.ByteArrayOutputStream;
16
17
import
java.io.DataInputStream;
18
19
import
java.io.DataOutputStream;
20
21
import
javax.microedition.rms.RecordStore;
22
23
public
class
Bundle
extends
BaseRMS {
24
25
private
static
String[] SECTION
=
{
26
27
"
\"AL\":
"
,
"
\"DT\":
"
,
28
29
"
\"ND\":
"
,
"
\"MD\":
"
,
30
31
"
\"SC\":
"
,
"
\"ST\":
"
};
32
33
private
static
int
LEN
=
SECTION.length;
34
35
private
static
boolean
inited
=
false
;
36
37
private
static
Bundle INSTANCE;
38
39
public
static
void
INIT(){
40
41
if
(inited)
return
;
42
43
inited
=
true
;
44
45
INSTANCE
=
new
Bundle();
46
47
INSTANCE.loadBundles();
48
49
}
50
51
public
static
Bundle getInstance(){
52
53
return
INSTANCE;
54
55
}
56
57
private
String[] CONTENT
=
new
String[LEN];
58
59
private
Bundle() {
60
61
super
(
"
snake-view
"
);
62
63
}
64
65
public
void
loadBundles() {
66
67
try
{
68
69
this
.open();
70
71
this
.close();
72
73
}
catch
(Exception e) {
74
75
try
{
76
77
this
.close();
78
79
RecordStore.deleteRecordStore(
this
.getRMSName());
80
81
this
.open();
82
83
this
.close();
84
85
}
catch
(Exception ex) {
86
87
}
88
89
}
90
91
}
92
93
public
void
resetBundles() {
94
95
try
{
96
97
this
.close();
98
99
RecordStore.deleteRecordStore(
this
.getRMSName());
100
101
this
.open();
102
103
this
.close();
104
105
}
catch
(Exception ex) {
106
107
}
108
109
}
110
111
public
void
updateBundles()
throws
Exception {
112
113
try
{
114
115
this
.openonly();
116
117
updateData();
118
119
if
(
this
.getRecordStore()
!=
null
)
120
121
this
.close();
122
123
}
catch
(Exception e) {
124
125
throw
new
Exception(
this
.getRMSName()
+
"
::updateBundles::
"
+
e);
126
127
}
128
129
}
130
131
protected
void
loadData()
throws
Exception {
132
133
try
{
134
135
byte
[] record
=
this
.getRecordStore().getRecord(
1
);
136
137
DataInputStream istream
=
new
DataInputStream(
138
139
new
ByteArrayInputStream(record,
0
, record.length));
140
141
String content
=
istream.readUTF();
142
143
int
[] start
=
new
int
[LEN
+
1
];
144
145
for
(
int
i
=
0
;i
<
LEN;i
++
){
146
147
start[i]
=
content.indexOf(SECTION[i]);
148
149
}
150
151
start[LEN]
=
content.length();
152
153
for
(
int
i
=
0
;i
<
LEN;i
++
){
154
155
CONTENT[i]
=
content.substring(start[i]
+
5
,start[i
+
1
]);
156
157
Log.info(
"
CONTENT[
"
+
i
+
"
]=
"
+
CONTENT[i]);
158
159
}
160
161
}
catch
(Exception e) {
162
163
throw
new
Exception(
this
.getRMSName()
+
"
::loadData::
"
+
e);
164
165
}
166
167
}
168
169
protected
void
createDefaultData()
throws
Exception {
170
171
try
{
172
173
ByteArrayOutputStream bstream
=
new
ByteArrayOutputStream(
12
);
174
175
DataOutputStream ostream
=
new
DataOutputStream(bstream);
176
177
CONTENT[
0
]
=
"
9,20,9,7
"
;
178
179
CONTENT[
1
]
=
"
1
"
;
180
181
CONTENT[
2
]
=
"
1
"
;
182
183
CONTENT[
3
]
=
"
600
"
;
184
185
CONTENT[
4
]
=
"
0
"
;
186
187
CONTENT[
5
]
=
"
7,7,6,7,5,7,4,7,3,7,2,7
"
;
188
189
StringBuffer sb
=
new
StringBuffer();
190
191
for
(
int
i
=
0
;i
<
LEN;i
++
){
192
193
sb.append(SECTION[i]);
194
195
sb.append(CONTENT[i]);
196
197
}
198
199
ostream.writeUTF( sb.toString());
200
201
ostream.flush();
202
203
ostream.close();
204
205
byte
[] record
=
bstream.toByteArray();
206
207
this
.getRecordStore().addRecord(record,
0
, record.length);
208
209
}
catch
(Exception e) {
210
211
throw
new
Exception(
this
.getRMSName()
+
"
::createDefaultData::
"
+
e);
212
213
}
214
215
}
216
217
protected
void
updateData()
throws
Exception {
218
219
try
{
220
221
ByteArrayOutputStream bstream
=
new
ByteArrayOutputStream(
12
);
222
223
DataOutputStream ostream
=
new
DataOutputStream(bstream);
224
225
StringBuffer sb
=
new
StringBuffer();
226
227
for
(
int
i
=
0
;i
<
LEN;i
++
){
228
229
sb.append(SECTION[i]);
230
231
sb.append(CONTENT[i]);
232
233
}
234
235
ostream.writeUTF(sb.toString());
236
237
ostream.flush();
238
239
ostream.close();
240
241
byte
[] record
=
bstream.toByteArray();
242
243
this
.getRecordStore().setRecord(
1
, record,
0
, record.length);
244
245
}
catch
(Exception e) {
246
247
throw
new
Exception(
this
.getRMSName()
+
"
::updateData::
"
+
e);
248
249
}
250
251
}
252
253
public
String getValue(
int
key) {
254
255
return
CONTENT[key];
256
257
}
258
259
public
void
setValue(
int
key, String value) {
260
261
CONTENT[key]
=
value;
262
263
}
264
265
}
Log,自己实现Log系统:
1
/**
2
3
* <p>Title: Snake</p>
4
5
* <p>Copyright: (C) 2011 Gavin's Snake project. Licensed under the Apache License, Version 2.0 (the "License")</p>
6
7
*
@author
Gavin
8
9
*/
10
11
package
com.deaboway.snake.util;
12
13
public
class
Log{
14
15
private
static
final
int
FATAL
=
0
;
16
17
private
static
final
int
ERROR
=
1
;
18
19
private
static
final
int
WARN
=
2
;
20
21
private
static
final
int
INFO
=
3
;
22
23
private
static
final
int
DEBUG
=
4
;
24
25
private
static
int
LOG_LEVEL
=
INFO;
26
27
public
static
void
info(String string){
28
29
if
(LOG_LEVEL
>=
INFO){
30
31
System.out.println(
"
[Deaboway][ INFO]
"
+
string);
32
33
}
34
35
}
36
37
public
static
void
debug(String string){
38
39
if
(LOG_LEVEL
>=
DEBUG){
40
41
System.out.println(
"
[Deaboway][DEBUG]
"
+
string);
42
43
}
44
45
}
46
47
public
static
void
warn(String string){
48
49
if
(LOG_LEVEL
>=
WARN){
50
51
System.out.println(
"
[Deaboway][ WARN]
"
+
string);
52
53
}
54
55
}
56
57
public
static
void
error(String string){
58
59
if
(LOG_LEVEL
>=
ERROR){
60
61
System.out.println(
"
[Deaboway][ERROR]
"
+
string);
62
63
}
64
65
}
66
67
public
static
void
fatal(String string){
68
69
if
(LOG_LEVEL
>=
FATAL){
70
71
System.out.println(
"
[Deaboway][FATAL]
"
+
string);
72
73
}
74
75
}
76
77
}
2. TileView类
(1)用Image替换BitMap,“private Image[] mTileArray;”
(2)private final Paint mPaint = new Paint();不再需要了。直接在Graphics中drawImage即可
(3)onSizeChanged() 不会被自动调用,需要在构造函数中主动调用,以实现对应功能。onSizeChanged(this.getWidth(),this.getHeight());
(4)最重要的,用paint替换onDraw,呵呵,Canvas的核心啊!失去它你伤不起!!!咱也试试咆哮体!!!!!!
3. SnakeView类
(1)J2ME 没有Handler,直接用Thread定期repaint()就OK。这里要啰嗦几句。
熟悉Windows编程的朋友可能知道Windows程序是消息驱动的,并且有全局的消息循环系统。而Android应用程序也是消息驱动的,按道理来说也应该提供消息循环机制。实际上Android中也实现了类似Windows的消息循环机制,它通过Looper、Handler来实现消息循环机制,Android消息循环是针对线程的(每个线程都可以有自己的消息队列和消息循环)。
Android系统中Looper负责管理线程的消息队列和消息循环。Handler的作用是把消息加入特定的(Looper)消息队列中,并分发和处理该消息队列中的消息。构造Handler的时候可以指定一个Looper对象,如果不指定则利用当前线程的Looper创建。
一个Activity中可以创建多个工作线程或者其他的组件,如果这些线程或者组件把他们的消息放入Activity的主线程消息队列,那么该消息就会在主线程中处理了。因为主线程一般负责界面的更新操作,并且Android系统中的weget不是线程安全的,所以这种方式可以很好的实现Android界面更新。在Android系统中这种方式有着广泛的运用。
如果另外一个线程要把消息放入主线程的消息队列,就需要通过Handle对象,只要Handler对象以主线程的Looper创建,那么调用Handler的sendMessage等接口,将会把消息放入队列都将是放入主线程的消息队列。并且将会在Handler主线程中调用该handler的handleMessage接口来处理消息。
之所以Android有这些处理,是因为Android平台来说UI控件都没有设计成为线程安全类型,所以需要引入一些同步的机制来使其刷新。而对于J2ME来说,Thread比较简单,直接匿名创建重写run方法,调用start方法执行即可。或者,也可以从Runnable接口继承。实现如下:
1
class
RefreshHandler
extends
Thread {
2
3
public
void
run() {
4
5
while
(
true
) {
6
7
try
{
8
9
//
delay一个延迟时间单位
10
11
Thread.sleep(mMoveDelay);
12
13
}
catch
(Exception e) {
14
15
e.printStackTrace();
16
17
}
18
19
//
更新View对象
20
21
SnakeView.
this
.update();
22
23
//
强制重绘
24
25
SnakeView.
this
.repaint();
26
27
}
28
29
}
30
31
};
(2)直接使用String代替TextView类,在Canvas的paint()中直接绘制各种提示信息。
(3)在一些地方需要主动调用repaint()进行强制重绘。
其它具体参考源代码。
4. Snake类
本类就比较简单了,直接把源代码贴出来如下:
1
/**
2
3
* <p>Title: Snake</p>
4
5
* <p>Copyright: (C) 2011 Gavin's Snake project. Licensed under the Apache License, Version 2.0 (the "License")</p>
6
7
*
@author
Gavin
8
9
*/
10
11
package
com.deaboway.snake;
12
13
import
javax.microedition.lcdui.Display;
14
15
import
javax.microedition.midlet.MIDlet;
16
17
import
javax.microedition.midlet.MIDletStateChangeException;
18
19
import
com.deaboway.snake.util.BaseRMS;
20
21
import
com.deaboway.snake.util.Bundle;
22
23
import
com.deaboway.snake.util.Log;
24
25
/**
26
27
* Snake: a simple game that everyone can enjoy.
28
29
*
30
31
* 贪吃蛇: 经典游戏,在一个花园中找苹果吃,吃了苹果会变长,速度变快。碰到自己和墙就挂掉
32
33
*
34
35
*/
36
37
public
class
Snake
extends
MIDlet {
38
39
public
static
Display display;
40
41
public
static
SnakeView mSnakeView;
42
43
public
static
MIDlet SNAKE;
44
45
public
Snake() {
46
47
Bundle.INIT();
48
49
display
=
Display.getDisplay(
this
);
50
51
SNAKE
=
this
;
52
53
mSnakeView
=
new
SnakeView();
54
55
mSnakeView.setTextView(
""
);
56
57
mSnakeView.setMode(SnakeView.READY);
58
59
display.setCurrent(mSnakeView);
60
61
}
62
63
protected
void
destroyApp(
boolean
arg0)
throws
MIDletStateChangeException {
64
65
mSnakeView.saveState();
66
67
}
68
69
protected
void
pauseApp() {
70
71
mSnakeView.setMode(SnakeView.PAUSE);
72
73
}
74
75
protected
void
startApp()
throws
MIDletStateChangeException {
76
77
//
检查存贮状态以确定是重新开始还是恢复状态
78
79
Log.debug(
"
startApp(), BaseRMS.FIRST=
"
+
BaseRMS.FIRST);
80
81
if
(BaseRMS.FIRST) {
82
83
//
存储状态为空,说明刚启动可以切换到准备状态
84
85
mSnakeView.setMode(SnakeView.READY);
86
87
}
else
{
88
89
//
已经保存过,那么就去恢复原有状态
90
91
//
恢复状态
92
93
if
(
!
mSnakeView.restoreState()) {
94
95
//
恢复状态不成功,设置状态为暂停
96
97
mSnakeView.setMode(SnakeView.PAUSE);
98
99
}
100
101
}
102
103
}
104
105
}
本次就大概介绍这么多,源代码将在下次放出。下次主要讲解源代码的存储和维护,敬请期待。