Windows和Android之——GUI多线程编程

提交新文章

2011年12月27日 07:00


WindowsAndroid之——GUI多线程编程

Xu Xing

1.          简介

本文包括下述内容:

GUI的一般原理。

Windows FormsAndroid 的多线程GUI编程的对比。

本文要求读者对Windows Forms( C#)Android GUI编程有基本的了解。

 

2.          GUI的基本元素

GUI系统通常包含下述核心组件:

UI元素:

这些UI元素可以组织成为树形结构;在 AndroidUI元素由View及其子类组成。而在Windows Forms( C#),这些元素由Control组成。UI元素实现对自身大小,位置的管理。并借助图形引擎将自身的内容绘制到指定位置。

图形引擎:

2D或者3D引擎。实现绘制点、线,2D/3D图形的功能。 Android使用了Skia/OpenGL ESWindows Forms可用的有GDI?其实微软使用的什么2D/3D引擎了解与不了解不太重要,会用就可以了。)。

对绘图区域的抽象:

这里的绘图区域可以直接是屏幕区域(X Window)。也可以是一块内存区域(Android)Android提供了Surface对象用于抽象绘图区域。

无论是Android编程,还是Forms编程,最常用的接口其实是UI元素和绘图上下文提供的。直接用到图形引擎和绘图区域的机会比较少。这是因为绘图上下文通常就封装了图形引擎和绘图区域。譬如Android使用Canvas的对象,既提供了2D绘图操作,同时还会被指定一块绘图区域。而Forms也提供了Graphics对象。

事件处理和窗口管理:

这一部分和本文要论述的问题关系不大。而且这些往往都不仅仅是接口层面的东西。暂且不表。

 FormsUI元素类图(部分):

 Forms-UI Elements

AndroidUI元素类图(部分):

Android UI Elements
Android UI Elements2 

 

3.          Android多线程GUI

Android,多线程界面分为两种情况:非UI线程通过异步消息更新UI;非UI线程通过直接操作Surface的形式更新绘图缓冲区,最终实现对屏幕内容的更新。

为了不至于混淆,对Android约定如下:Android UI是指用View及其子类组成的实现。UI线程是指实现对View及其子类进行更新、事件处理的线程。Android UI通过Surface最终显示在屏幕上。但是应用也可以直接操作 Surface往绘图缓冲区绘制内容。

那么,WindowsC#是否也提供了类似的机制和实现呢?这正是本文要通过实例来揭示的内容。

 

3.1       Android:范例HandlerExample介绍

范例HandlerExample说明:UI线程,即Activity所在线程使用了Yes/No按钮和一个SurfaceView、以及提供辅助信息的TextView和布局容器等。UI线程将会负责Yes/No按钮的显示隐藏。而UserThread负责刷新SurfaceView对应的SurfaceUI线程使用mHandlerMain接收来自UserThread的消息以控制Yes/No按钮的显示隐藏。而UserThread使用mHandlerChild接收来自UI线程的消息,以实现在SurfaceView上绘制不同的色块。图示如下:

Android Example 

使用下述的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

AndroidUI是非线程安全的。即如果你要在非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线程,提交RunnableUI线程

*/

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接口获取SurfaceViewSurfaceHolder

*/

       // @Override

       public void surfaceChanged(SurfaceHolder holder, int format, int width,

                     int height) {

              mSurfaceHolder = holder;

       }

/*

2,使用CanvasSurfaceHolder指定的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::sendMessageHandler::post函数用于给相应的MessageQueue队列插入事件。LooperMessageQueueHandler三个对象使得线程(非UI线程和UI线程都可以)之间的异步通信实现起来非常简单。

线程要创建自己的Looper使用下述代码:

Looper.prepare();

Looper.loop();

4.          C#多线程GUI

C#的多线程界面,作者已知两种情况:非UI线程更新UI;非UI线程更新绘图缓冲区。

4.1       C#:UI线程更新UI

所使用的范例:FormsMultiThreadDemo

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”

 Forms Graphics Thread

/*

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(…);

2BackgroundWorker所在线程执行DoWork指定的任务。

3UI线程调用ProgressChanged更新进度。

4UI线程调用RunWorkerCompleted以结束任务。

步骤34其实都封装了BackgroundWorker线程到UI线程间通信的内容。只是看起来不像Android的实现那么直接而已。

5.          总结

UI相关的多线程支持上来看,AndroidC#在功能和接口上差别不是很大。在UI线程和非UI线程间通信、非UI线程更新绘图缓冲区等功能上,两者都提供了较完备的支持。但是在接口的易用上,作者更倾向于AndroidC#发展了这么多年,也许为了后向兼容,在新技术的使用上,肯定没Android这个全新的平台这么彻底。

作者无意厚此薄彼,也无心挑起WindowsAndroid之间的好坏的战争。相比较而言,作者的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