| 2011年12月27日 07:00 | |
Windows和Android之——GUI多线程编程
Xu Xing
1. 简介
本文包括下述内容:
GUI的一般原理。
Windows Forms和Android 的多线程GUI编程的对比。
本文要求读者对Windows Forms( C#)和Android GUI编程有基本的了解。
2. GUI的基本元素
GUI系统通常包含下述核心组件:
UI元素:
这些UI元素可以组织成为树形结构;在 Android,UI元素由View及其子类组成。而在Windows Forms( C#),这些元素由Control组成。UI元素实现对自身大小,位置的管理。并借助图形引擎将自身的内容绘制到指定位置。
图形引擎:
如2D或者3D引擎。实现绘制点、线,2D/3D图形的功能。 Android使用了Skia/OpenGL ES。Windows Forms可用的有GDI(?其实微软使用的什么2D/3D引擎了解与不了解不太重要,会用就可以了。)。
对绘图区域的抽象:
这里的绘图区域可以直接是屏幕区域(如X Window)。也可以是一块内存区域(如Android)。Android提供了Surface对象用于抽象绘图区域。
无论是Android编程,还是Forms编程,最常用的接口其实是UI元素和绘图上下文提供的。直接用到图形引擎和绘图区域的机会比较少。这是因为绘图上下文通常就封装了图形引擎和绘图区域。譬如Android使用Canvas的对象,既提供了2D绘图操作,同时还会被指定一块绘图区域。而Forms也提供了Graphics对象。
事件处理和窗口管理:
这一部分和本文要论述的问题关系不大。而且这些往往都不仅仅是接口层面的东西。暂且不表。
Forms的UI元素类图(部分):
Android的UI元素类图(部分):
3. Android多线程GUI
在Android,多线程界面分为两种情况:非UI线程通过异步消息更新UI;非UI线程通过直接操作Surface的形式更新绘图缓冲区,最终实现对屏幕内容的更新。
为了不至于混淆,对Android约定如下:Android UI是指用View及其子类组成的实现。UI线程是指实现对View及其子类进行更新、事件处理的线程。Android UI通过Surface最终显示在屏幕上。但是应用也可以直接操作 Surface往绘图缓冲区绘制内容。
那么,Windows的C#是否也提供了类似的机制和实现呢?这正是本文要通过实例来揭示的内容。
3.1 Android:范例HandlerExample介绍
范例HandlerExample说明:UI线程,即Activity所在线程使用了Yes/No按钮和一个SurfaceView、以及提供辅助信息的TextView和布局容器等。UI线程将会负责Yes/No按钮的显示隐藏。而UserThread负责刷新SurfaceView对应的Surface。UI线程使用mHandlerMain接收来自UserThread的消息以控制Yes/No按钮的显示隐藏。而UserThread使用mHandlerChild接收来自UI线程的消息,以实现在SurfaceView上绘制不同的色块。图示如下:
使用下述的layout文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
android:id="@+id/widget31"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
xmlns:android="http://schemas.android.com/apk/res/android"
>
<TextView
android:layout_width="fill_parent"
android:layout_height="50dip"
android:textSize="25sp"
android:text="@string/info"
/>
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="50dip">
<Button
android:id="@+id/yes_button"
android:layout_width="50dip"
android:layout_height="50dip"
android:text="@string/yes" >
</Button>
<Button
android:id="@+id/no_button"
android:layout_width="50dip"
android:layout_height="50dip"
android:text="@string/no" >
</Button>
</LinearLayout>
<SurfaceView android:id="@+id/test_surfaceview"
android:layout_width="100dip"
android:layout_height="100dip"/>
</LinearLayout>
本章所介绍的内容都是基于这个范例。
3.2 Android:非UI线程更新UI
Android的UI是非线程安全的。即如果你要在非UI线程更新UI,只能通过往UI线程发送消息或者提交Runnable来实现。非UI线程是不能直接操作UI元素的。
发送消息的方式:
/*
1,非UI线程,发送消息给UI线程的Handler
*/
mHandlerMain.removeMessages(MAIN_MSG_HIDE);
mHandlerMain.sendMessageDelayed(
mHandlerMain.obtainMessage(MAIN_MSG_HIDE), 0);
/*
2,UI线程,接收到来自非UI线程的消息
*/
private Handler mHandlerMain = new Handler() {
//Handler没有任何参数,其使用的事件Loop就是申明Handler所在线程使用的Loop。
//即UI线程。所以其handleMessage的执行也是位于UI线程。
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
…
case MAIN_MSG_HIDE: {
mYesButton.setVisibility(View.INVISIBLE);
mNoButton.setVisibility(View.INVISIBLE);
break;
}
…
}
};
提交Runnable的方式:
/*
1,非UI线程,提交Runnable给UI线程
*/
mHandlerMain.post(mMainRunable);
/*
2, 在UI线程执行mMainRunable
*/
// Use Runnable or Msg, they both work
public Runnable mMainRunable = new Runnable() {
public void run() {
Log.d(TAG, "mMainRunable::run thread id= "
+ Thread.currentThread().getId());
if (true) {
mYesButton.setVisibility(View.VISIBLE);
mNoButton.setVisibility(View.VISIBLE);
}
}
};
对于Android而言,无论使用哪一种机制,最终都是在UI线程里面对UI进行更新。
有时候你会发现,你在非UI线程里面对View进行操作,系统并没有报错。大家应该了解,多线程编程,一次或者若干次的结果正确并不表示这么做就是正确的。
3.3 Android:非UI线程更新绘图缓冲区
Android通常使用SurfaceView(或者其子类GLSurfaceView等)来实现对绘图缓冲区的更新。但是SurfaceView使用的绘图缓冲区(绘图缓冲区其实就是Surface)和UI主线程使用的绘图缓冲区不是同一个。了解过Android Graphics框架的同学都清楚,Android是客户端渲染,服务器合成的机制。每个客户端维护一到多个绘图缓冲区(Surface),譬如这里的HandlerExample最少就有两个Surface:一个用于显示UI主线程的View Tree。另一个用于UserThread的内容显示。
Android要在非UI主线程里面操作绘图缓冲区主要分下述几步:
/*
1,在UI主线程里面通过SurfaceHolder.Callback接口获取SurfaceView的SurfaceHolder
*/
// @Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
mSurfaceHolder = holder;
}
/*
2,使用Canvas往SurfaceHolder指定的Surface绘制图形
*/
void drawColor(Paint paint, int color) {
DisplayMetrics displayMetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
int height = displayMetrics.heightPixels;
int width = displayMetrics.widthPixels;
if (mSurfaceHolder != null) {
Canvas canvas = mSurfaceHolder.lockCanvas(null);
paint.setColor(color);
canvas.drawRect(new RectF(0, 0, width, height), paint);
mSurfaceHolder.unlockCanvasAndPost(canvas);
}
}
3.4 Android:线程的事件循环和线程间通信
Android封装了一个Looper对象。该对象实现了一个循环,用于从MessageQueue里面提取消息,然后将消息分发给对应的Handler。而Handler本身也提供了Handler::sendMessage和Handler::post函数用于给相应的MessageQueue队列插入事件。Looper和MessageQueue,Handler三个对象使得线程(非UI线程和UI线程都可以)之间的异步通信实现起来非常简单。
线程要创建自己的Looper使用下述代码:
Looper.prepare();
Looper.loop();
4. C#多线程GUI
在C#的多线程界面,作者已知两种情况:非UI线程更新UI;非UI线程更新绘图缓冲区。
4.1 C#:非UI线程更新UI
所使用的范例:FormsMultiThreadDemo。
/*
userThread,非UI线程,点击界面Safe Call,
*/
// Form1.cs
private void SetText(string text)
{
// InvokeRequired required compares the thread ID of the
// calling thread to the thread ID of the creating thread.
// If these threads are different, it returns true.
if (this.textBox1.InvokeRequired)
{
SetTextCallback d = new SetTextCallback(SetText);
this.Invoke(d, new object[] { text });
}
Else
{
//如果直接这么做,系统会出错!非UI线程不能直接更新界面
this.textBox1.Text = text;
}
}
因为不知道windows内部实现。所以作者无法去了解this.Invoke的实现。从给用户提供的接口来看,C#和Android其实非常的类似:非UI线程都无法直接操作UI元素。非UI线程最终都是以消息或者特殊调用的方式实现对UI元素的操作。
4.2 C#:非UI线程更新绘图缓冲区
所使用的范例:GraphicsMultiThreadDemo。
功能介绍:程序启动的时候,会在界面显示”Hello,OnPaint”和红色竖排的”Sample Text”。当用户点击”Start Graphics Thread”时,会显示绿色竖排的”Sample Text”
/*
UI线程,点击”Start Graphics Thread”按钮
*/
//MainForm.Designer.cs
private void StartGraphicsThread()
{
this.graphicsThread =new Thread(new ThreadStart(this.GraphicsThread));
this.graphicsThread.Start();
}
/*
Graphics Thread子线程,绘制绿色竖排文字
*/
// MainForm.Designer.cs
private void GraphicsThread()
{
this.customControl.DrawVerticalString();
}
//CustomControl.cs
public void DrawVerticalString()
{
Console.Write("\n***************" + System.Threading.Thread.CurrentThread.ManagedThreadId+"\n");
System.Drawing.Graphics formGraphics = this.CreateGraphics();
string drawString = "Sample Text";
System.Drawing.Font drawFont = new System.Drawing.Font("Arial", 16);
System.Drawing.SolidBrush drawBrush = new System.Drawing.SolidBrush(System.Drawing.Color.Green);
float x = 10.0F;
float y = 50.0F;
System.Drawing.StringFormat drawFormat = new System.Drawing.StringFormat();
drawFormat.FormatFlags = StringFormatFlags.DirectionVertical;
formGraphics.DrawString(drawString, drawFont, drawBrush, x, y, drawFormat);
drawFont.Dispose();
drawBrush.Dispose();
formGraphics.Dispose();
}
从代码逻辑看,C#通过Control::CreateGraphics获得绘图缓冲区,然后往里面绘制内容。至于这块缓冲区和当前Control所使用的缓冲区是否是同一块存储空间,这个也需要微软提供解答。
4.3 C#:非UI线程的事件循环和线程间通信
在作者即将给C#下定论,微软在C#这一块为开发者做的很少的时候。作者发现了BackgroundWorker。这个类封装了事件循环、和UI线程的通信机制。但是看起来确实不如Android的直接。
BackgroundWorker提供了三个主要的接口:
/*
DoWork接口:在非UI线程里面执行
*/
this.backgroundWorker1.DoWork+=new DoWorkEventHandler(backgroundWorker1_DoWork);
/*
ProgressChanged接口:在UI线程里面执行
*/
this.backgroundWorker1.ProgressChanged += new ProgressChangedEventHandler(backgroundWorker1_ProgressChanged);
/*
ProgressChanged接口:在UI线程里面执行
*/
this.backgroundWorker1.RunWorkerCompleted += new RunWorkerCompletedEventHandler(backgroundWorker1_RunWorkerCompleted);
使用BackgroundWorker的主要流程是:
1,启动BackgroundWorker(在UI主线程启动BackgroundWorker肯定是没有问题的。作者没有测试在非UI线程里面启动是否可以)
this.backgroundWorker1.RunWorkerAsync(…);
2,BackgroundWorker所在线程执行DoWork指定的任务。
3,UI线程调用ProgressChanged更新进度。
4,UI线程调用RunWorkerCompleted以结束任务。
步骤3,4其实都封装了BackgroundWorker线程到UI线程间通信的内容。只是看起来不像Android的实现那么直接而已。
5. 总结
从UI相关的多线程支持上来看,Android和C#在功能和接口上差别不是很大。在UI线程和非UI线程间通信、非UI线程更新绘图缓冲区等功能上,两者都提供了较完备的支持。但是在接口的易用上,作者更倾向于Android。C#发展了这么多年,也许为了后向兼容,在新技术的使用上,肯定没Android这个全新的平台这么彻底。
作者无意厚此薄彼,也无心挑起Windows和Android之间的好坏的战争。相比较而言,作者的Android开发经验更久,Windows也不过是刚刚开始写了几个Hello World而已。所以对 Windows的某些技术分析如果有错误,请指正。
一个平台的成功与否,取决于消费者和开发者。谁能够在讨好开发者和消费者之间找到最美妙的平衡,谁就应该能够取得成功。
6. 参考文献
http://msdn.microsoft.com/zh-cn/library/ms741870.aspx
http://msdn.microsoft.com/zh-cn/library/ms171728.aspx
http://msdn.microsoft.com/zh-cn/library/system.windows.forms.control.aspx
http://www.cnblogs.com/dongdonghuihui/archive/2009/07/20/1527151.html
http://developer.android.com/reference/android/view/ViewGroup.html
http://developer.android.com/reference/android/view/View.html
Xing Xu (Intel)
|


