本文学习自高焕堂老师的Android从程序员到架构师之路系列教学视频
40 - 认识线程(Thread)模式a
1. 线程(Thread)概念
所谓线程(Thread) 是指一串连续的执行动作,以达成一项目的。現代的电脑内部都有数串连续性的动作同时在进行。也就是有多条线程并行地(Concurrently)执行。
在电脑中,若电脑拥有多颗CPU,则每颗CPU 可各照顾一个线程,于是可多个线程同时间进行。若只有单一CPU,则此CPU可同时(Concurrently)照顾数个线程。无论是多CPU或单一CPU的电脑,多条线程并行地执行,都可增加执行效率。
像Java、C++等现代的电脑语言都能让程序师们能够易于创建多条线程减化GUI 动画的设计工作,也可增加其执行效率。例如,当您想一边看动画,一边听音乐时,计算机能同时产生两个线程──“秀动画”及“播音乐”。甚至可产生另一条线程来为您做特殊服务,如让您可选择动画及音乐。
多条线程能并行地执行同一个类别,或者是不同的类别。在Android平台里也不例外,无论是在Java层或是C++层,都常常见到多条线程并行的情形。
Android采取Java的Thread框架,来协助建立多條线程並行的环境。在Java里,大家已经习惯撰写一个类别来支持Runnable接口,再搭配Thread基类就能顺利诞生一个新线程来执行该类别里的run()函数了。
2. Java的线程框架
Java提供一个Thread基类(Super Class)来支持多线程功能。这个基类协助诞生(小)线程,以及管理(小)线程的进行,让电脑系统更容易取得程序师的指示,然后安排CPU 来运作线程里的指令。
例如,线程所欲达成的任务(Task)是程序师的事,所以程序师应填写线程里的指令,来表达其指示。为配合此部份的运作,Java提供了Runnable接口,其定义了一个run()函数。
于是,程序师可撰写一个应用类别(Application Class)来实作(Implement)此界面,并且把线程的任务写在应用类别的run()函数里,如此即可让(小)线程来执行run()函数里的任务了。
这是几乎每一位Java开发者都常用的多线程(Multi-thread)机制,只是许多人都会用它,却不曾认识它的真实身影:就是一个幕后的框架。由于Android应用程序开发也采用Java语言,所这个Thread框架也成为Android大框架里的一个必备元素。
基于这个框架里的Thread基类和Runnable接口,你就可以撰写应用类别,来实作run()函数了,如下图:
于此图里,框架的Thread基类会先诞生一个小线程,然后该小线程透过Runnable接口,调用(或执行)了Task类别的run()函数。
例如,请看一个Java程序:
// Ex01-01.java
class Task implements Runnable {
public void run() {
int sum = 0;
for (int i = 0; i <= 100; i++)
sum += i;
System.out.println("Result: " + sum);
}
}
public class JMain {
public static void main(String[] args) {
Thread t = new Thread(new Task());
t.start();
System.out.println("Waiting...");
}
}
此时,main()先诞生一个Task类的对象,并且诞生一个Thread基础的对象。接着,执行到下一个指令:t.start();
此时,main()就调用Thread的start()函数;这start()就产生一个小线程去执行run()函数。如下图:
框架的结构而言,上图里的Runnable接口与Thread基类是可以合并起来的。也就是把run()函数写在Thread的子类别里。如下图:
兹撰写一个Java程序(即改写上述的Ex01-01.java)来实现上图:
class myThread extends Thread {
public void run() {
int sum = 0;
for (int i = 0; i <= 100; i++)
sum += i;
System.out.println("Result: " + sum);
}
}
public class JMain {
public static void main(String[] args) {
Thread t = new myThread();
t.start();
System.out.println("Waiting...");
}
}
其诞生一个myThread对象,并且由JMain调用Thread的start()函数。这start()就产生一个小线程去执行 myThread子类别里的run()函数。上图是类关系图,其对象关系图,可表示如下:
41 - 认识线程(Thread)模式b
3. 认识Android的主线程(又称UI线程)
UI线程的责任:迅速处理UI事件
在Android里,关照UI画面的事件(Event)是UI线程的重要职责,而且是它的专属职责,其它子线程并不可以插手存取UI画面上的对象(如TextView)呢!
由于Android希望UI线程能够给予用户的要求做快速的反应。如果UI 线程花费太多时间做幕后的事情,而在UI事件发生之后,让用户等待超过5秒钟而未处理的话,Android就会向用户道歉。
// ac01.java
// ……..
public class ac01 extends Activity implements OnClickListener {
public TextView tv;
private Button btn, btn2, btn3;
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
LinearLayout layout = new LinearLayout(this);
layout.setOrientation(LinearLayout.VERTICAL);
btn = new Button(this); btn.setId(101);
btn.setBackgroundResource(R.drawable.heart);
btn.setText("Block UI thread"); btn.setOnClickListener(this);
LinearLayout.LayoutParams param =
new LinearLayout.LayoutParams(150,50);
param.topMargin = 10;
layout.addView(btn, param);
btn2 = new Button(this); btn2.setId(102);
btn2.setBackgroundResource(R.drawable.heart);
btn2.setText("Show"); btn2.setOnClickListener(this);
layout.addView(btn2, param);
btn3 = new Button(this); btn3.setId(103);
btn3.setBackgroundResource(R.drawable.heart);
btn3.setText("Exit"); btn3.setOnClickListener(this);
layout.addView(btn3, param);
tv = new TextView(this);
tv.setTextColor(Color.WHITE); tv.setText("");
LinearLayout.LayoutParams param2 =
new LinearLayout.LayoutParams(150, 60);
param2.topMargin = 10;
layout.addView(tv, param2);
setContentView(layout);
setTitle("please press <Block...> & <Show> ");
tv.setText("then wait for 5 min...");
}
public void onClick(View v) {
switch(v.getId()){
case 101:
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
break;
case 102: break;
case 103: finish(); break;
}
}
}
連續按下<Block UI thread>和<Show>按鈕,然後等待5秒鐘,就會出現剛才所說的道歉提示
主线程可以诞生多个子线程来分担其工作,尤其是比较冗长费时的幕后服务工作,例如播放动画的背景音乐、或从网络下载影片等。于是,主线程就能专心于处理UI画面的事件了。
再如,当你开发一个游戏程序时,如果你希望游戏循环不受UI事件的干扰,或希望游戏循环(GameLoop)不要阻塞住UI线程的话,就不适合拿UI线程去跑游戏循环了。
42 - 认识线程(Thread)模式c
UI线程的诞生
当我们启动某一支AP时,Android就会诞生新进程(Process),并且将该AP程序加载这新诞生的进程里。每个进程在其诞生时刻,都会诞生一个主线程,又称为UI线程
在进程诞生时刻,除了诞生主线程之外,还会替主线程诞生它专用的Message、Queue和Looper。如下图所示:
这个Main Looper就是让主线程没事时就来执行Looper,确保主线程永远活着而不会死掉;在执行Looper时,会持续观察它的Message Queue是否有新的信息进来;如果有新信息进来的话,主线程就会尽快去处理(响应)它。
在Android环境里,一个应用程序常包含有许多个类别,这些类别可以分布在不同进程里执行,或挤在一个进程里执行。例如有一个应用程序的AndroidManifest.xml文件内容如下:
// AndroidManifest.xml
// ………
<activity android:name=".FirstActivity" android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </activity>
<activity android:name=".LoadActivity">
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
</intent-filter> </activity>
<service android:name=".LoadService" android:process=":remote">
<intent-filter>
<action android:name="com.misoo.pkm.REMOTE_SERVICE" />
</intent-filter> </service>
</application>
</manifest>
Android依据这个文件而将各类别布署于两个进程里执行,如图:
其中,FirstActivity和LoadActivity两个类别会加载预设的进程里。而LoadService则会加载于名为“remote”的独立进程里。
于是,由进程#1的主线程去执行FirstActivity和LoadActivity的onCreate()等函数。而由进程#2的主线程去执行LoadService的onCreate()等函数。
LoadService在独立的进程(名称叫“remote”)里执行。于是,FirstActivity与LoadService之间就属于跨进程的沟通了。这种跨进程的沟通,就是大家熟知的IPC(Inter-Process Communication)机制了。这种IPC机制是透过底层驱动(Driver)来实现的。如下图:
在此图的不同进程里 , 各 有 其 主 线 程(Thread)。由于线程是不能越过进程边界的。所以,当执行LoadActivity的线程必须跨越进 程 去 执 行 LoadService( 的函数 ) 时 ,Android 的内层 Binder System 即 刻 从LoadService所在进程的线程池启动线程(BinderThread) 来 配 合 接 力 , 由 此BinderThread去执行LoadService。
練習:绑定(Bind)远程的Service
Binder System會從進程的線程池(Thread pool)裡啟動一個線程來執行Binder::onTransact()函數。
当Thread_a必须跨越进程去执行JavaBBinder对象时,Android的内层Binder System即刻从myService所在进程的线程池启动线程Thread_x来配合衔接Thread_a线程,由Thread_x去执行JavaBBinder对象。
Android的每一个进程里,通常含有一个线程池,让跨进程的线程得以进行。虽然是由Thread_a与Thread_x相互合作与衔接而完成远距通讯的,但让人们能单纯地认为是单一进程(即Thread_a)跨越到另一个进程去执行JavaBBinder对象。虽然JavaBBinder是C/C++层级的;而myService是Java层级的,两者不同语言,但处于同一个进程,所以Thread_x可以执行到myService对象。
43 - 认识线程(Thread)模式d
4. 细说主线程(UI线程)的角色
近程通信
在Android里,无论组件在那一个进程里执行,于预设情形下,他们都是由该进程里的主线程来负责执行之。
例如下述的范例,由一个Activity启动一个Service,两者都在同一个进程里执行。此时,两者都是由主线程负责执行的。如下图所示:
// ac01.java
//……
public class ac01 extends Activity
implements OnClickListener {
private Button btn, btn2;
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
LinearLayout layout = new LinearLayout(this);
layout.setOrientation(LinearLayout.VERTICAL);
btn = new Button(this); btn.setId(101);
btn.setText("run service");
btn.setBackgroundResource(R.drawable.heart);
btn.setOnClickListener(this);
LinearLayout.LayoutParams param
= new LinearLayout.LayoutParams(135, 50);
param.topMargin = 10; layout.addView(btn, param);
btn2 = new Button(this); btn2.setId(102);
btn2.setText("Exit");
btn2.setBackgroundResource(R.drawable.heart);
btn2.setOnClickListener(this);
layout.addView(btn2, param);
setContentView(layout);
//---------------------------------------
Thread.currentThread().setName(
Thread.currentThread().getName()+"-ac01");
}
public void onClick(View v) {
switch (v.getId()) {
case 101:
this.startService(new Intent(this, myService.class));
break;
case 102:
finish(); break;
}}
}
// myService.java
//……..
public class myService extends Service {
@Override
public void onCreate(){
Thread.currentThread().setName(
Thread.currentThread().getName() + "-myService");
Toast.makeText(this, Thread.currentThread().getName(),
Toast.LENGTH_SHORT).show();
}
@Override
public IBinder onBind(Intent intent){
return null;
}
}
主线程先执行ac01的onCreate()函数,然后,继续执行myService的onCreate()函数。于是,输出了主线程的执行轨迹纪录
除了上述的Activity和Service之外,还有BroadcastReceiver也是一样,是由主线程来执行的。例如,由一个Activity启动一个BroadcastReceiver,两者都在同一个进程里执行。此时,两者都是由主线程负责执行的。如下图所示
// ac01.java
// …….
public class ac01 extends Activity implements OnClickListener {
//…….
public void onCreate(Bundle icicle) {
//………
Thread.currentThread().setName(
Thread.currentThread().getName()+"-ac01");
}
public void onClick(View v) {
switch (v.getId()) {
case 101:
Intent in = new Intent(MY_EVENT);
this.sendBroadcast(in); break;
case 102: finish(); break;
}
}
}
}
// myReceiver.java
//……..
public class myReceiver extends BroadcastReceiver {
@Override public void onReceive(Context context, Intent intent) {
Thread.currentThread().setName(
Thread.currentThread().getName() + "-myReceiver");
Toast.makeText(context,
Thread.currentThread().getName(),
Toast.LENGTH_SHORT).show();
}
}
主线程先执行myActivity的onCreate()函数,之后继续执行myReceiver的onReceive()函数。于是输出了主线程执行的轨迹纪录:
远程通信
如果Activity、Service和BroadcastReceiver三者并不是在同一个进程里执行时,它们之间的通讯就是跨进程通讯(IPC)了。
请先看个范例,它由一个Activity启动一个远距的Service,两者分别在不同的进程里执行,如下图所示:
当Activity与Service(或BroadcastReceiver)之间采用IPC通讯时,意味着两者分别在不同的进程里执行。此时,于预设情形下,Activity、BroadcastReceiver或Service都是由其所属进程里的主线程负责执行之
Android核心的Binder System从”remote”进程的线程池里,启动一个线程(名为”Binder Thread #1”)来执行myBinder的onTransact()函数。 • 依据Binder System的同步(Synchronization)的机制,主线程会等待Binder Thread #1线程执行完毕,才会继续执行下去。
44 - 认识线程(Thread)模式e
5.线程之间的通信架构
认识Looper与Handler对象
当主线程诞生时,就会去执行一个代码循环(Looper),以便持续监视它的信息队列(Message Queue简称MQ)。当UI事件发生了,通常会立即丢一个信息(Message)到MQ,此时主线程就立即从MQ里面取出该信息,并且处理之。
例如,用户在UI画面上按下一个Button按钮时,UI事件发生了,就会丢一些信息到MQ里,其中包括onClick信息,于是,主线程会及时从MQ里取出onClick信息,然后调用Activity的onClick()函数去处理之。
处理完毕之后,主线程又返回去继续执行信息循环,继续监视它的MQ,一直循环下去,直到主线程的生命周期的终了。
通常是进程被删除时,主线程才会被删除
Android里有一个Looper类别,其对象里含有一个信息循环(Message Loop)。也就是说,一个主线程有它自己专属的Looper对象,此线程诞生时,就会执行此对象里的信息循环。此外,一个主线程还会有其专属的MQ信息结构。如下图所示:
由于主线程会持续监视MQ的动态,所以在程序的任何函数,只要将信息(以Message类别的对象表示之)丢入主线程的MQ里,就能与主线程沟通了。
在Android里,也定义了一个Handler类别,在程序的任何函数里,可以诞生Handler对象来将Message对象丢入MQ里,而与主线程进行沟通。
在Android的预设情况下,主线程诞生时,就会拥有自己的Looper对象和MQ(即Message Queue)数据结构。
然而,主线程诞生子线程时,于预设情形下,子线程并不具有自己的Looper对象和MQ。由于没有Looper对象,就没有信息回圈(Message Loop),一旦工作完毕了,此子线程就结束了。
既然没有Looper对象也没有MQ,也就不能接受外来的Message对象了。则别的线程就无法透过MQ来传递信息给它了。
那么,如果别的线程(如主线程)需要与子线程通讯时,该如何呢? 答案是:替它诞生一个Looper对象和一个MQ就行了。
主线程丢信息给自己
Handler是Android框架所提供的基类,用来协助将信息丢到线程的MQ里。
兹撰写个范例程序Rx01,来将信息丢到主线程的MQ里,如下:
// ac01.java
//……..
public class ac01 extends Activity implements OnClickListener {
private Handler h;
public void onCreate(Bundle icicle) {
//……..
h = new Handler(){
public void handleMessage(Message msg) {
setTitle((String)msg.obj);}
};
}
public void onClick(View v) {
switch (v.getId()) {
case 101:
h.removeMessages(0);
Message m = h.obtainMessage(1, 1, 1, "this is my message.");
h.sendMessage(m); // 将Message送入MQ里
break;
case 102:
finish(); break;
}
}
}
当主线程执行到onCreate()函数里的指令:
h = new Handler(){
// ………
}
就诞生一个Handler对象,可透过它来把信息丢到MQ里。
当执行到onClick()函数里的指令:
//………………
h.removeMessages(0);
Message m = h.obtainMessage(1, 1, 1,
"this is my message.");
h.sendMessage(m);
就将Message对象送入MQ里。
当主线程返回到信息回圈时,看到MQ里有个Message对象,就取出来,并执行handleMessage()函数,将Message对象里所含的字符串显示于画面上。
子线程丢信息给主线程
子线程也可以诞生Handler对象来将Message对象丢到主线程的MQ里,又能与主线程通讯了。兹撰写个范例程序Rx02
如下:
// ac01.java
// ……….
public class ac01 extends Activity implements OnClickListener {
private Handler h;
private Timer timer = new Timer();
private int k=0;
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
//………
h = new Handler(){
public void handleMessage(Message msg) {
setTitle((String)msg.obj);}
};
}
public void onClick(View v) {
switch (v.getId()) {
case 101:
TimerTask task = new TimerTask(){
@Override
public void run() {
h.removeMessages(0);
Message m = h.obtainMessage(1, 1, 1,
Thread.currentThread().getName() + " : "+String.valueOf(k++));
h.sendMessage(m);
}
};
timer.schedule(task, 500, 1500); break;
case 102:
finish(); break;
}
}
}
就启动一个Timer的线程,名字叫:”Timer-0”;然后,由它来定时重复执行TimerTask::run()函数,就不断将Message对象丢到主线程的MQ里。此时主线程会持续处理MQ里的Message对象,将其内的字符串显示于画面上。
于是,子执行透过Handler对象而将信息丢到主线程的MQ,进而成功地将信息显示于画面上。
替子线程诞生Looper与MQ
如果别的线程(如主线程)需要与子线程通讯时,该如何呢? 答案是:替它诞生一个Looper对象和一个MQ就行了。兹撰写个范例程序Rx03如下:
// ac01.java
//……
public class ac01 extends Activity implements OnClickListener {
private Thread t;
private Handler h;
private String str;
public void onCreate(Bundle icicle) {
//……..
t = new Thread(new Task());
t.start(); }
public void onClick(View v) {
switch(v.getId()){
case 101:
Message m = h.obtainMessage(1, 33, 1, null);
h.sendMessage(m); break;
case 102: setTitle(str); break;
case 103:
h.getLooper().quit(); finish(); break;
}
}
class Task implements Runnable {
public void run() {
Looper.prepare();
h = new Handler(){
public void handleMessage(Message msg) {
str = Thread.currentThread().getName() +", value=" + String.valueOf(msg.arg1);
}
};
Looper.loop();}
}
}
Step-1: 一开始,由主线程执行onCreate()函数。主线程继续执行到指令:
t = new Thread(new Task());
t.start();
就诞生一个子线程,并启动子线程去执行Task的run()函数,而主线程则返回到信息回圈,并持续监视MQ的动态了。
Step-2: 此时,子线程执行到run()函数里的指令:Looper.prepare();
就诞生一个Looper对象,准备好一个信息回圈(Message Loop) 和MQ数据结构。
继续执行到指令:
h = new Handler(){
//…..
}
就诞生一个Handler对象,可协助将信息丢到子线程的MQ上。
接着继续执行到指令:
Looper.loop();
也就开始执行信息回圈,并持续监视子线程的MQ动态了。
Step-3: 当用户按下UI按钮时,UI事件发生了,Android将此UI事件的信息丢到主线程的MQ,主线程就执行onClick()函数里的指令:
Message m = h.obtainMessage(1, 33, 1, null);
h.sendMessage(m);
主线程藉由h将该Message对象(内含整数值33)丢入子线程的MQ里面,然后主线程返回到它的信息循环(Looper),等待UI画面的事件或信息。
Step-4: 子线程看到MQ有了信息,就会取出来,调用handleMessage()函数:
public void handleMessage(Message msg) {
str = Thread.currentThread().getName() +", value=" + String.valueOf(msg.arg1);}
来处理之,就设定的str的值。请留意,此刻子线程因为不能碰触UI控件,所以无法直接将str值显示于画面上。
Step-5: 当用户按下<show value>按钮时,主线程就执行onClick()函数,将str值显示于画面上。于是,实现主线程与子线程之间的双向沟通了。
45 - 认识线程(Thread)模式f
6. Android UI的单线程环境
單線程程序概念
单线程程序意谓着两个(或多个)线程不能共享对象或变量值。
Android的UI是单线程程序的环境。
UI控件(如Button等)都是由UI线程所创建,内部攸关于UI显示的属性或变量都只有UI线程才能存取(Access)之,别的线程并不能去存取之。
例如下图里的View类别体系,都只限于UI线程才能去执行它们的onDraw()函数,因为它会实际更动到UI的属性。
public class myActivity extends Activity
implements OnClickListener {
private Button ibtn;
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
ibtn = new Button(this);
//…………….
}
// 其它函数
}
由于UI线程来执行onCreate()函数,诞生了Button对象,因而只限UI线程能去存取该对象里攸关UI的属性,其它线程不能去碰它们。
线程安全问题就是如何避免不同线程之间,可能会相互干扰的问题。
虽然两个线程几乎同时先后执行一个类别里的(可能不同)函数,只要不共享对象、或共享变量(例如Android的UI单线程环境),就不会产生干扰现象,也就没有线程安全问题。
换句话说,如果各自使用自己的对象或变量(即不共享对象或变量),就不会干扰到别线程执行的正确性了。
例如下述范例:
// Ex01.java
class Task2{
private int count;
public void init(){ count = 0; }
public void f1() {
for(int i=0; i<3; i++) {
count++;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +"'s count: " + count);
}
}
}
class Task implements Runnable {
public void run() {
Task2 ta2 = new Task2();
ta2.init(); ta2.f1();
}
}
public class JMain {
public static void main(String[] args) {
Task ta = new Task();
Thread t1 = new Thread( ta, "A");
Thread t2 = new Thread( ta, "B");
t1.start();
t2.start();
System.out.println("Waiting...");
}
}
这里,t1和t2线程共享主线程所诞生的ta对象,但是各自诞生了Task2类别之对象。两者各自使用自己的对象(即不共享对象或变量),就不会干扰到别线程的数据。所以输出正确的结果:
SurfaceView与非UI线程
View控件是由UI 线程(主线程)所执行。如果需要去迅速更新UI画面或者UI画图需要较长时间(避免阻塞主线程),就使用SurfaceView。 它可以由背景线程(background thead)来执行,而View只能由UI(主)线程执行画面显示或更新。
在SurfaceView里,非UI线程可以去碰触UI显示,例如将图形绘制于Surface画布上。这SurfaceView内含高效率的rendering机制,能让背景线程快速更新surface的内容,适合演示动画(animation)。
46 - 认识线程(Thread)模式g
7. 线程安全的化解之例
View是一个单线程的类;其意味着:此类的撰写着心中意图只让有一个线程来执行这个类的代码(如函数调用)。
// ac01.java
// ……..
public class ac01 extends Activity implements OnClickListener {
private Button btn;
public void onCreate(Bundle icicle) {
// ……..
btn = new Button(this);
btn.setText(“Exit");
// ……..
}
public void f1() {
// ……..
btn.setText(“OK");
// ……..
}
}
同样地,View的子类开发者也不宜让多线程去执行View(基类)的代码。// ……
public class ac01 extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
okButton ok_btn = new okButton(this);
LinearLayout.LayoutParams param =
new LinearLayout.LayoutParams(ok_btn.get_width(),ok_btn.get_height());
// ……..
}
}
/* ---- okButton ---- */
// ……….
public class okButton extends Button{
public okButton(Context ctx){
super(ctx);
super.setText("OK");
super.setBackgroundResource(R.drawable.ok_blue);
}
public void f1() {
super.setText("Quit");
}
public int get_width(){ return 90; }
public int get_height(){ return 50; }
}
如果共享对象或变量是不可避免的话,就得试图错开线程的执行时刻了。
由于共享对象或变量,若两个线程会争相更改对象的属性值或变量值时,则可能会互相干扰对方的计算过程和结果。 例如:
class Task implements Runnable {
private int count;
public void init(){ count = 0; }
public void f1() {
for(int i=0; i<3; i++) {
count++;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace(); }
System.out.println(Thread.currentThread().getName() +"'s count: " + count);
}
}
public void run() {
this.init();
this.f1();
}
}
public class JMain {
public static void main(String[] args) {
Task ta = new Task();
Thread t1 = new Thread( ta, "A");
Thread t2 = new Thread( ta, "B");
t1.start();
t2.start();
System.out.println("Waiting...");
}
}
由于在这个程序只会诞生myActivity对象,却可能诞生多个Thread对象,可能出现多条线程同时并行(Concurrently)执行run()函数的情形。此时必须特别留意线程冲突问题。也就是多条线程共享变量或对象,导致互相干扰计算中的变量值,因而产生错误的计算结果。
例如,依据上图的设计结构,撰写程序码,可能无意中这会产生冲突了。 • 如下范例
// myActivity.java
//……….
public class myActivity extends Activity
implements OnClickListener, Runnable {
private Button ibtn;
private int sum;
@Override
protected void onCreate(Bundle icicle) {
//………
Thread th1 = new Thread(this); th1.start();
Thread th2 = new Thread(this); th2.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
setTitle(String.valueOf(sum));
}
public void onClick(View v) {
finish();
}
//------------------------------------------
@Override
public void run() {
sum = 0;
for(int i=0; i<10000; i++ )
sum += 1;
} }
第一个线程还没做完run()函数的计算,其后的第二个线程就进来run()函数,并行共享了sum变量值,因而输出错误的结果:11373。
此时,可以使用synchronized机制来错开两个线程,就正确了。例如将上数程序码
修改如下:
// …………
int sum;
Thread th1 = new Thread(this);
th1.start();
Thread th2 = new Thread(this);
th2.start();
Thread.sleep(1000);
setTitle(String.valueOf(sum));
// ………….
@Override
public void run() {
this.exec();
}
public synchronized void exec(){
sum = 0;
for(int i=0; i<10000; i++ ) sum += 1;
}
// end
第二个线程会等待第一个线程离开exec()函数之后才能进入exec(),就不会产生共享sum变量值的现象了。由于变量就存于对象内部,如果不共享对象,就可避免共享内部变量的问题。
47 - 应用Android的UI框架a
以设计游戏循环(GameLoop)为例
1. UI线程、View与onDraw()函数
1.游戏的UI画面通常是由大量美工贴图所构成的,并不会使用一般的Layout来布局,而是使用画布(Canvas)来把图片显示于View的窗口里。
2.在View类里有个onDraw()函数,View类体系里的每一个类都必须覆写(Override) 这 个onDraw()函数,来执行实际绘图的动作。
游戏的基本动作就是不断的进行:绘图和刷新(Refresh)画面。其中,onDraw()函数实践画图,将图形绘制于View的画布(Canvas)上,并显示出来;而invalidate()函数则启动画面的刷新,重新調用一次onDraw()函数。
当我们设计myView子类别时,也必须覆写onDraw()函数。在程序执行时,Android框架会进行反向調用到myView的onDraw()函数来进行画图动作。如下图:
2. 基本游戏循环(GameLoop)
游戏的基本动作就是不断的绕回圈(Loop),重复绘图和刷新画面的动作。最简单的循环实现方式是:在onDraw()函数里調用invalidate()函数,就能刷新画面(重新調用一次onDraw()函数)了。
// myView.java
// ………
public class myView extends View {
private Paint paint= new Paint();
private int line_x = 100, line_y = 100;
private float count = 0;
myView(Context ctx) { super(ctx); }
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//-----------------------------------------------------
if( count > 12) count = 0;
int x = (int) (75.0 * Math.cos(2*Math.PI * count/12.0));
int y = (int) (75.0 * Math.sin(2*Math.PI * count/12.0));
count++;
//---------------------------------------------
canvas.drawColor(Color.WHITE);
paint.setColor(Color.BLACK);
paint.setStrokeWidth(3);
canvas.drawLine(line_x, line_y, line_x+x, line_y+y, paint);
paint.setStrokeWidth(2);
paint.setColor(Color.RED);
canvas.drawRect(line_x-5, line_y - 5, line_x+5, line_y + 5, paint);
paint.setColor(Color.YELLOW);
canvas.drawRect(line_x-3, line_y - 3, line_x+3, line_y + 3, paint);
try {
Thread.sleep(1000);
} catch (InterruptedException ie) {}
invalidate();
}
}
Android中提供了invalidate()来实现画面的刷新:即触发框架重新执行onDraw()函数来绘图及显示。
3. 使用UI线程的MQ(Message Queue)
// myView.java
// ………
public class myView extends View {
// ………
@Override protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// ………
// canvas.drawRect(….);
invalidate();
} }
我们可以透过Message方式来触发UI线程去調用invalidate()函数,而达到重新执行onDraw()来进行重复绘图和刷新画面的动作。
// myView.java
//……..
public class myView extends View {
private Paint paint= new Paint();
private int line_x = 100, int line_y = 100;
private float count = 0;
private myHandler h;
myView(Context ctx)
{ super(ctx);
h = new myHandler();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if( count > 12) count = 0;
int x = (int) (75.0 * Math.cos(2*Math.PI * count/12.0));
int y = (int) (75.0 * Math.sin(2*Math.PI * count/12.0));
count++;
canvas.drawColor(Color.WHITE);
paint.setColor(Color.RED);
paint.setStrokeWidth(3);
canvas.drawLine(line_x, line_y, line_x+x, line_y+y, paint);
paint.setStrokeWidth(2);
paint.setColor(Color.BLUE);
canvas.drawRect(line_x-5, line_y - 5, line_x+5, line_y + 5, paint);
paint.setColor(Color.YELLOW);
canvas.drawRect(line_x-3, line_y - 3, line_x+3, line_y + 3, paint);
h.removeMessages(0);
Message msg = h.obtainMessage(0);
h.sendMessageDelayed(msg, 1000);
}
class myHandler extends Handler {
@Override
public void handleMessage(Message msg) {
invalidate();
}
};
}
使用sendMessageDelayed()函数来暂停一下,延迟数秒钟才传递 Message给UI线程
4. 诞生一个小线程,担任游戏线程
刚才是由UI线程来丢Message到自己的MQ里;也就是UI线程丢Message给自己。同一样地,也可以由其它线程来丢Message到UI线程的MQ里,来触发UI线程去調用invalidate()函数。
// myView.java
// ……….
public class myView extends View {
private Paint paint= new Paint();
private int line_x = 100, line_y = 100;
private float count = 0;
private myHandler h;
myView(Context ctx) { super(ctx); h = new myHandler(); }
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if( count > 12) count = 0;
int x = (int) (75.0 * Math.cos(2*Math.PI * count/12.0));
int y = (int) (75.0 * Math.sin(2*Math.PI * count/12.0));
count++;
canvas.drawColor(Color.WHITE);
paint.setColor(Color.RED);
paint.setStrokeWidth(3);
canvas.drawLine(line_x, line_y, line_x+x, line_y+y, paint);
paint.setStrokeWidth(2);
paint.setColor(Color.BLUE);
canvas.drawRect(line_x-5, line_y - 5, line_x+5, line_y + 5, paint);
paint.setColor(Color.MAGENTA);
canvas.drawRect(line_x-3, line_y - 3, line_x+3, line_y + 3, paint);
//--------------------------------
myThread t = new myThread();
t.start();
}
/