Android UI主线程如何同非UI线程进行通信

原文 Android Non-UI to UI Thread Communications
作者 Jim White+
翻译 http://zhiwei.li/text/

0. Android的UI线程 和 ANR
在Android平台上, 默认情况下,应用都在一个线程里运行. 这个线程就叫做UI线程. 它频繁地被调用,因为这个县城显示用户界面, 监听用户同应用交互是发生的事件.
如果运行在这个线程里的代码 过分使用这个线程,阻止用户交互(超过5秒钟), 它将引起Android抛出一个 臭名昭著的ANR(Android Not Responsive)错误.

1.
如何对付ANR?你的应用必须创建另外的线程了,将需要长时间运行的工作放到 非UI线程. 如何实现创建另外的线程 有一些选择.
你可以创建并开启你自己的java.lang.Thread
你也可以创建并开启一个AsyncTask(Android自己的线程简化实现机制)

非UI线程处理 长时间运行的工作,比如文件下载.
UI线程还是专注于显示UI和处理用户事件.

生活又重新归于美好了.

然而,不幸地是, 用户界面(UI)不能被非UI线程刷新. 举例来说, 在成功下载玩文件后, 非UI县城不能显示一个AlertDiaglog, 更新TextView窗体, 或者其他任何UI上的变化,来提示文件已经被成功下载了.如果你尝试从非UI线程更新UI. 这个应用可以编译,但是运行时,会得到一个CallFromWrongThreadException的异常.

2.
有几种方法,可以让非UI线程发送请求到UI线程来更新UI.
1)使用runOnUiThread方法
2)使用post()方法
3)使用Handler框架
4)使用广播和广播接受者(还可以带上LocalBroadcastManager)
5)使用AsyncTask的onProgressUpdate()方法

3. 实例设计
为了上面的5种方法, 设计了一个小应用. 它有一个activity(一个屏幕)
含有两个按钮 和一个TextView
一个按钮 用来开启 单独的非UI线程
另外一个 用来 停止 这个线程

运行后, 非UI线程 生成一个随机数, 然后睡眠几秒钟. 这可以模拟长时间运行的工作.
但是, 非UI线程 想将自己每次生成的随机数 显示到TextView中.

sdfds
package li.zhiwei.nonui;

import android.app.Activity;
import android.os.Bundle;

import android.view.View;
import android.widget.Button;
import android.widget.TextView;


public class MainActivity extends Activity {
    private Button startButton, stopButton;
    private TextView resultsTextView;

    DoSomethingThread randomWork;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        startButton = (Button) findViewById(R.id.startButton);
        stopButton = (Button) findViewById(R.id.stopButton);
        resultsTextView = (TextView) findViewById(R.id.resultsTextView);


        View.OnClickListener listener = new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (v == startButton) {
                    startGenerating();
                } else {
                    stopGenerating();
                }
            }
         };

         startButton.setOnClickListener(listener);
         stopButton.setOnClickListener(listener);
    }



    private void startGenerating() {
        randomWork = new DoSomethingThread();
        randomWork.start();
    }


    private void stopGenerating() {
        randomWork.interrupt();
        updateResults(getString(R.string.service_off));
    }


    public void updateResults(String results) {
        resultsTextView.setText(results);
    }


    public class DoSomethingThread extends Thread {
       private static final int DELAY = 5000; // 5 seconds
       private static final int RANDOM_MULTIPLIER = 10;

       @Override
       public void run() {
            while (true) {
               int randNum = (int) (Math.random() * RANDOM_MULTIPLIER);
               publishProgress(randNum);
            try {
               Thread.sleep(DELAY);
            } catch (InterruptedException e) {
               return;
            }
        }
    }

    private void publishProgress(int randNum) {
        final String text = String.format(getString(R.string.service_msg), randNum);
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                updateResults(text);
            }
        });
    }
  }


}

上面演示的是 runOnUiThread 方法, 它定义在Activity类中
实现方法是,通过调用runOnUiThread() 方法,发送消息 requested action 到UI线程的 event queue (事件队列),
UI线程从队列取出action消息,然后执行

使用runOnUiThread的优缺点:
1. runOnUiThread方法定义在Activity类中. 这意味着 非UI线程 必须 有一些知识或者手段
来获取Activity
在我们上面的例子中,这是很容易做到的.因为我们的非UI线程 就作为一个inner class定义在Activity类中.

2. 如果在UI线程调用 runOnUiThread, 那么会直接调用对应的方法, 而不是 发送消息到自己的队列.

直接用post向view组件发消息

            /*
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    updateResults(text);
                }
            });
            */
            resultsTextView.post(new Runnable() {
                @Override
                public void run() {
                    resultsTextView.setText(txt);
                }
            });

因为post()使用了和runOnUiThread()一样的消息队列.所以 优缺点差不多.
尤其是你的非UI线程 必须知道View组件. 这将允许非UI县城避免直接同Activity代码连接.
也就说说你的 非UI线程 之需要知道一个View, 而不必知道Activity或者其他东西.

另外,即使你在UI线程中调用post(),它也会将消息放到队列, 而不是直接运行相应的方法.这跟runOnUiThread()是不同的.

我们知道, 在UI线程中,有一个消息队列. 放到队列的消息被依次处理. 用户事件(比如一个按钮被按下), 引起事件消息被放到这个队列里.
runOnUiThread()和post()方法被调用, 也会放消息到这个队列里.
你可以更直接地使用消息队列. 比如使用Handler框架.
这个框架让你 构建一个 消息处理器 在UI线程里监听 消息.

首先给MainActivity类添加一个inner class
    private static class HandlerExtension extends Handler {

        private final WeakReference currentActivity;

        public HandlerExtension(MainActivity activity){
            currentActivity = new WeakReference(activity);
        }

        @Override
        public void handleMessage(Message message){
            MainActivity activity = currentActivity.get();
            if (activity!= null){
                activity.updateResults(message.getData().getString("result"));
            }
        }
    }

在onCreate()里创建它
    resultHandler = new HandlerExtension(this);

在publishProgress()里发消息给Handler

            Bundle msgBundle = new Bundle();
            msgBundle.putString("result", txt);
            Message msg = new Message();
            msg.setData(msgBundle);
            resultHandler.sendMessage(msg);

这样大费周折的好处是, 非UI类只需要知道 Handler, 不需要知道UI线程任何别的东西
你自己的Handler必须是静态的,并且要对Activity进行弱引用(WeakReference). 这样避免内存泄漏.

使用本地广播

在MainActivity类里创建inner class
    private BroadcastReceiver createBroadcastReceiver() {
        return new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                updateResults(intent.getStringExtra("result"));
            }
        };
    }
在OnCreate()里创建一个广播接受者, 并向 本地广播管理者 注册它
        resultReceiver = createBroadcastReceiver();
        LocalBroadcastManager.getInstance(this).registerReceiver(
                resultReceiver,
                new IntentFilter("li.zhiwei.random.generation"));

为MainActivity添加一个onDestroy()方法
    @Override
    protected void onDestroy() {
        if (resultReceiver != null) {
            LocalBroadcastManager.getInstance(this).unregisterReceiver(
                    resultReceiver);
        }
        super.onDestroy();
    }
在 publishProgress()里发送广播
            Intent intent = new Intent("li.zhiwei.random.generation");
            intent.putExtra("result", txt);
            LocalBroadcastManager.getInstance(MainActivity.this)
                    .sendBroadcast(intent);

发送广播不需要消息队列. 但是它 需要 意图(Intent) 和 Inter listner(这里是broadcast reciever)

使用广播还可以在两个活动之间传递大量的数据

AsyncTask是对Thread+Handler的良好封装

    public class DoSomethingTask extends AsyncTask<void, string,="" void=""> {
        private static final int DELAY = 5000; // 5 seconds
        private static final int RANDOM_MULTIPLIER = 10;

        @Override
        protected void onProgressUpdate(String... values) {
            updateResults(values[0].toString());
            super.onProgressUpdate(values);
        }


        @Override
        protected Void doInBackground(Void... params) {
            String txt="";
            while (true) {
                if (isCancelled()) {
                    break;
                }
                int randNum = (int) (Math.random() * RANDOM_MULTIPLIER);
                txt = String.format(getString(R.string.service_msg), randNum);
                publishProgress(txt);

                try {
                    Thread.sleep(DELAY);
                } catch (InterruptedException e) {
                }
            }
            return null;
        }

    }
开始和停止要改动
    private void startGenerating() {
        randomWork = new DoSomethingTask();
        randomWork.execute();
    }

    private void stopGenerating() {
        randomWork.cancel(true);
        updateResults(getString(R.string.service_off));
    }

据说限制是, AsyncTask只能被执行一次,多次调用会产生异常

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s